/**
 * HMAC-signed capability token that lets a SANDBOXED artifact iframe load the
 * workspace images it embeds.
 *
 * Why this exists: artifact iframes run with `sandbox="allow-scripts"` (no
 * `allow-same-origin`) → opaque origin → the `__sg_token` session cookie is
 * NOT sent on `<img>`/CSS asset requests, and an `<img>` can't carry a Bearer
 * header. So authorized image loads need the credential IN THE URL (`?at=`).
 *
 * Security shape (see ~/.claude/plans/artifact-asset-proxy.md):
 *  - **Artifact-scoped allowlist** — the token names the EXACT `assetId/filename`
 *    keys the artifact references (`assets`), not the whole repo. Malicious
 *    artifact JS holding the token can only re-load the images already on the
 *    page, not enumerate the repo.
 *  - **Bound user** — `userId` is re-checked at serve time (`hasRepoAccess`), so
 *    a token stops working the moment the user loses access (modulo browser
 *    cache; the serve path uses revalidate+ETag, not immutable).
 *  - **Purpose-pinned** — verify REJECTS any JWT that isn't `purpose:
 *    'artifact-asset'`, so a session/OAuth JWT signed with the same secret can
 *    never satisfy this gate (and this token can never satisfy a session gate).
 *
 * Signed with the workspace JWT secret via `authService.getJwtSecret()` — one
 * rotation lever, same as `asset-upload-token.ts`.
 *
 * The token is a bearer credential visible to (untrusted) artifact JS. The
 * defenses above (narrow asset allowlist + serve-time access re-check + short
 * TTL) — NOT `connect-src 'none'` — are what make that acceptable: artifact JS
 * can `postMessage` the token out, but it only unlocks this artifact's own
 * images for a user who still has access.
 */
import jwt from 'jsonwebtoken';
/** Thrown by `verifyArtifactAssetToken` on tamper / expiry / wrong secret /
 *  wrong purpose / malformed shape. Callers map it to a fail-closed 404. */
export class ArtifactAssetTokenError extends Error {
    code = 'artifact_asset_token_invalid';
    constructor(message) {
        super(message);
        this.name = 'ArtifactAssetTokenError';
    }
}
const PURPOSE = 'artifact-asset';
export const ARTIFACT_ASSET_TOKEN_TTL_SECONDS = 12 * 60 * 60; // 12h — single source of truth
export function signArtifactAssetToken(payload, secret) {
    return jwt.sign(payload, secret, {
        algorithm: 'HS256',
        expiresIn: ARTIFACT_ASSET_TOKEN_TTL_SECONDS,
    });
}
/**
 * Verify a token and return its payload. Throws `ArtifactAssetTokenError` on:
 *   - tamper (any field mutated) / wrong secret (rotation) / expired / malformed
 *   - wrong or missing `purpose` (rejects session + OAuth JWTs decisively)
 *
 * The caller (asset route, token path) is responsible for the post-verify
 * checks the signature alone can't make:
 *   - `payload.repo === row.repo`
 *   - `${row.assetId}/${row.filename}` ∈ `payload.assets`
 *   - re-resolve `payload.userId` and re-run `hasRepoAccess`
 */
export function verifyArtifactAssetToken(token, secret) {
    let decoded;
    try {
        decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
    }
    catch (err) {
        throw new ArtifactAssetTokenError(err instanceof Error ? err.message : 'token verification failed');
    }
    if (!isPayload(decoded)) {
        throw new ArtifactAssetTokenError('token payload has unexpected shape or purpose');
    }
    return {
        purpose: PURPOSE,
        repo: decoded.repo,
        artifactId: decoded.artifactId,
        assets: decoded.assets,
        userId: decoded.userId,
    };
}
/** Build the canonical allowlist key for an asset. Centralized so the mint,
 *  serve, and tests can't drift on separator/whitespace. */
export function artifactAssetKey(assetId, filename) {
    return `${assetId}/${filename}`;
}
function isPayload(value) {
    if (!value || typeof value !== 'object')
        return false;
    const v = value;
    return (v.purpose === PURPOSE &&
        typeof v.repo === 'string' && v.repo.length > 0 &&
        typeof v.artifactId === 'string' && v.artifactId.length > 0 &&
        Array.isArray(v.assets) && v.assets.every((a) => typeof a === 'string') &&
        typeof v.userId === 'string' && v.userId.length > 0);
}
//# sourceMappingURL=artifact-asset-token.js.map