@sequenceholdings/artifact-studio 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/VERSION-PIN.md +20 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/offline/connector.d.ts +2 -0
  4. package/dist/offline/connector.js +6 -2
  5. package/dist/offline/management-types.d.ts +25 -0
  6. package/dist/offline/management-types.js +1 -0
  7. package/dist/runtime-contracts.js +1 -1
  8. package/dist/sdk.d.ts +8 -1
  9. package/dist/server/api-client.d.ts +26 -0
  10. package/dist/server/api-client.js +38 -0
  11. package/dist/server/client.d.ts +32 -45
  12. package/dist/server/client.js +41 -85
  13. package/dist/server/connection-retry.d.ts +12 -0
  14. package/dist/server/connection-retry.js +40 -0
  15. package/dist/server/errors.d.ts +37 -2
  16. package/dist/server/errors.js +66 -6
  17. package/dist/server/external-context.d.ts +21 -0
  18. package/dist/server/external-context.js +19 -0
  19. package/dist/server/internal/signed-token.d.ts +15 -0
  20. package/dist/server/internal/signed-token.js +67 -0
  21. package/dist/server/internal/v1-context.d.ts +18 -0
  22. package/dist/server/internal/v1-context.js +26 -0
  23. package/dist/server/internal/verify-any.d.ts +22 -0
  24. package/dist/server/internal/verify-any.js +53 -0
  25. package/dist/server/logger.d.ts +1 -1
  26. package/dist/server/principal.d.ts +48 -0
  27. package/dist/server/principal.js +39 -0
  28. package/dist/server/request-context.d.ts +115 -3
  29. package/dist/server/request-context.js +101 -32
  30. package/dist/server/testing.d.ts +40 -0
  31. package/dist/server/testing.js +86 -0
  32. package/dist/server.d.ts +9 -4
  33. package/dist/server.js +5 -2
  34. package/dist/templates/nextjs/package.json +3 -1
  35. package/dist/templates/nextjs/tests/server.test.mjs +25 -0
  36. package/dist/templates/react-vite/package.json +1 -1
  37. package/package.json +11 -3
  38. package/templates/nextjs/package.json +3 -1
  39. package/templates/nextjs/tests/server.test.mjs +25 -0
  40. package/templates/react-vite/package.json +1 -1
@@ -1,18 +1,53 @@
1
+ /** Errors the server SDK raises, and the step-up (RFC 9470) mapping a handler returns. */
1
2
  import { z } from 'zod';
3
+ export const INSUFFICIENT_USER_AUTHENTICATION = 'insufficient_user_authentication';
2
4
  const NO_REFUSAL = { code: null, detail: null };
3
5
  export class ArtifactServerError extends Error {
4
6
  status;
5
7
  code;
8
+ /** A named refusal a handler can act on, such as `insufficient_user_authentication`. */
9
+ reason;
10
+ /** With `insufficient_user_authentication`: the sign-in age, in seconds, the operation requires. */
11
+ maxAge;
6
12
  /** Atlas's own account of the refusal; both null when the response had no readable envelope. */
7
13
  atlas;
8
- constructor(status, code, message, atlas = NO_REFUSAL) {
14
+ constructor(status, code, message, details = {}) {
9
15
  super(message);
10
16
  this.status = status;
11
17
  this.code = code;
12
18
  this.name = 'ArtifactServerError';
13
- this.atlas = atlas;
19
+ this.reason = details.reason;
20
+ this.maxAge = details.maxAge;
21
+ this.atlas = details.atlas ?? NO_REFUSAL;
14
22
  }
15
23
  }
24
+ /** A user-mode call made for a caller that may act only through `asService()`. */
25
+ export class ArtifactPrincipalError extends ArtifactServerError {
26
+ constructor(reason) {
27
+ super(403, reason, 'External callers reach platform services only through asService()', { reason });
28
+ this.name = 'ArtifactPrincipalError';
29
+ }
30
+ }
31
+ /**
32
+ * The step-up refusal in a broker 401 body (`{reason|error: 'insufficient_user_authentication',
33
+ * maxAge|max_age}`), or null. A missing or malformed age asks for a fresh sign-in (0).
34
+ */
35
+ export function stepUpError(status, body) {
36
+ if (status !== 401 || body === null || typeof body !== 'object')
37
+ return null;
38
+ const named = ('reason' in body && body.reason === INSUFFICIENT_USER_AUTHENTICATION) ||
39
+ ('error' in body && body.error === INSUFFICIENT_USER_AUTHENTICATION);
40
+ if (!named)
41
+ return null;
42
+ const age = 'maxAge' in body ? body.maxAge : 'max_age' in body ? body.max_age : undefined;
43
+ const maxAge = typeof age === 'number' && Number.isSafeInteger(age) && age >= 0 ? age : 0;
44
+ return new ArtifactServerError(401, INSUFFICIENT_USER_AUTHENTICATION, 'A more recent sign-in is required', { reason: INSUFFICIENT_USER_AUTHENTICATION, maxAge });
45
+ }
46
+ export async function stepUpErrorFromResponse(response) {
47
+ if (response.status !== 401)
48
+ return null;
49
+ return stepUpError(response.status, await response.json().catch(() => null));
50
+ }
16
51
  /** Atlas answers a refusal as `{ detail, code? }`; anything else (a proxy page, an empty body) reads as no envelope. */
17
52
  const atlasRefusalSchema = z.object({ detail: z.string().optional(), code: z.string().optional() });
18
53
  /**
@@ -20,10 +55,35 @@ const atlasRefusalSchema = z.object({ detail: z.string().optional(), code: z.str
20
55
  * and `error.atlas` its machine `code`, so a consumer can branch on a Lattice refusal
21
56
  * (`EDGE_ID_MISMATCH`, `WORKFLOW_NOT_RUNNING`, …) the way a caller of the route itself would.
22
57
  */
23
- export async function platformRequestError({ response, operation }) {
24
- const envelope = atlasRefusalSchema.safeParse(await response.json().catch(() => undefined));
58
+ export function platformRequestError({ status, body, operation }) {
59
+ const envelope = atlasRefusalSchema.safeParse(body);
25
60
  const detail = envelope.success ? envelope.data.detail ?? null : null;
26
61
  const code = envelope.success ? envelope.data.code ?? null : null;
27
- const message = `Atlas rejected the ${operation} request (${response.status})${detail ? `: ${detail}` : ''}`;
28
- return new ArtifactServerError(response.status, 'platform_request_failed', message, { code, detail });
62
+ const message = `Atlas rejected the ${operation} request (${status})${detail ? `: ${detail}` : ''}`;
63
+ return new ArtifactServerError(status, 'platform_request_failed', message, { atlas: { code, detail } });
64
+ }
65
+ /**
66
+ * The error for a refused broker response: a step-up refusal keeps its reason and maxAge, any other
67
+ * refusal keeps Atlas's `{ detail, code }`. The body is read once, so neither reading starves the other.
68
+ */
69
+ export async function refusalFromResponse({ response, operation }) {
70
+ const body = await response.json().catch(() => undefined);
71
+ return stepUpError(response.status, body) ?? platformRequestError({ status: response.status, body, operation });
72
+ }
73
+ /**
74
+ * The RFC 9470 response for a step-up refusal, for the app to return as is: the caller signs in
75
+ * again with `max_age` and retries.
76
+ */
77
+ export function stepUpResponse(error) {
78
+ if (error.reason !== INSUFFICIENT_USER_AUTHENTICATION)
79
+ throw new TypeError('stepUpResponse requires an insufficient_user_authentication error');
80
+ const maxAge = error.maxAge !== undefined && Number.isSafeInteger(error.maxAge) && error.maxAge >= 0 ? error.maxAge : 0;
81
+ return new Response(JSON.stringify({ error: INSUFFICIENT_USER_AUTHENTICATION, max_age: maxAge }), {
82
+ status: 401,
83
+ headers: {
84
+ 'content-type': 'application/json',
85
+ 'cache-control': 'no-store',
86
+ 'www-authenticate': `Bearer error="${INSUFFICIENT_USER_AUTHENTICATION}", max_age=${maxAge}`,
87
+ },
88
+ });
29
89
  }
@@ -0,0 +1,21 @@
1
+ import { type ArtifactGatewayPublicKeys, type RequestContextV2External } from './request-context.js';
2
+ /** The ingresses (manifest `server.ingress` names) whose external callers a handler accepts. */
3
+ export interface ArtifactExternalAccept {
4
+ external: readonly string[];
5
+ }
6
+ /**
7
+ * Verifies an external (v2) request context for a handler that explicitly accepts external
8
+ * callers. Unlike the v1 verifier, `method` and `path` are mandatory: an external context is
9
+ * bound to the one route it was signed for. Refuses a v1 context (`external_context_required`)
10
+ * and a context from an ingress the handler did not name (`external_ingress_not_accepted`).
11
+ */
12
+ export declare function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, now }: {
13
+ token: string | null;
14
+ publicKeys: ArtifactGatewayPublicKeys;
15
+ projectId: string;
16
+ environment: string;
17
+ method: string;
18
+ path: string;
19
+ accept: ArtifactExternalAccept;
20
+ now?: number;
21
+ }): RequestContextV2External;
@@ -0,0 +1,19 @@
1
+ import { ArtifactContextError } from './request-context.js';
2
+ import { verifyAnyArtifactContext } from './internal/verify-any.js';
3
+ /**
4
+ * Verifies an external (v2) request context for a handler that explicitly accepts external
5
+ * callers. Unlike the v1 verifier, `method` and `path` are mandatory: an external context is
6
+ * bound to the one route it was signed for. Refuses a v1 context (`external_context_required`)
7
+ * and a context from an ingress the handler did not name (`external_ingress_not_accepted`).
8
+ */
9
+ export function verifyArtifactExternalContext({ token, publicKeys, projectId, environment, method, path, accept, now }) {
10
+ if (!method || !path)
11
+ throw new ArtifactContextError();
12
+ // TODO(C1): canonicalizeIngressPath(path) at the path comparison in internal/verify-any.ts, which the broker shares.
13
+ const context = verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, now });
14
+ if (context.v !== 2)
15
+ throw new ArtifactContextError('external_context_required');
16
+ if (!accept.external.includes(context.principal.ingress))
17
+ throw new ArtifactContextError('external_ingress_not_accepted');
18
+ return context;
19
+ }
@@ -0,0 +1,15 @@
1
+ /** Kids of keys that sign external (v2) contexts carry this prefix, and only those kids do. */
2
+ export declare const ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX = "external-";
3
+ /**
4
+ * True when the token names an external key or claims the external version. Unverified: only the
5
+ * SDK's own refusal and verifier dispatch may use it, never a trust decision, so it stays internal.
6
+ */
7
+ export declare function isExternalContextToken(token: string | null): boolean;
8
+ /** Verifies the EdDSA signature against the pinned key set and returns the still-untyped payload. */
9
+ export declare function verifySignedContext({ token, publicKeys }: {
10
+ token: string | null;
11
+ publicKeys: Readonly<Record<string, string>>;
12
+ }): {
13
+ kid: string;
14
+ payload: unknown;
15
+ };
@@ -0,0 +1,67 @@
1
+ import { createPublicKey, verify } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ /**
4
+ * Signature handling shared by every Artifact context verifier. It decides nothing about which
5
+ * context versions a caller accepts: the public v1 verifier, the external verifier and the
6
+ * broker's dispatching verifier each apply their own version and kid rules on top of it.
7
+ *
8
+ * Lives under `server/internal/`, which the package exports map does not expose.
9
+ */
10
+ const MAX_CONTEXT_TOKEN_LENGTH = 32768;
11
+ /** Kids of keys that sign external (v2) contexts carry this prefix, and only those kids do. */
12
+ export const ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX = 'external-';
13
+ const headerSchema = z.object({ alg: z.literal('EdDSA'), typ: z.literal('JWT'), kid: z.string().min(1).max(255) }).strict();
14
+ class SignedContextError extends Error {
15
+ constructor() { super('Invalid signed Artifact context'); this.name = 'SignedContextError'; }
16
+ }
17
+ function decodeSegment(value) {
18
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
19
+ throw new SignedContextError();
20
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
21
+ }
22
+ function splitToken(token) {
23
+ if (!token || token.length > MAX_CONTEXT_TOKEN_LENGTH)
24
+ throw new SignedContextError();
25
+ const parts = token.split('.');
26
+ if (parts.length !== 3)
27
+ throw new SignedContextError();
28
+ const [header, payload, signature] = parts;
29
+ return [header, payload, signature];
30
+ }
31
+ /**
32
+ * Reads the kid and the version claim WITHOUT verifying the signature. Use it only to refuse
33
+ * early or to choose which verifier runs; never to trust a claim. Null when the token is malformed.
34
+ */
35
+ function peekSignedContext(token) {
36
+ try {
37
+ const [encodedHeader, encodedPayload] = splitToken(token);
38
+ const header = headerSchema.parse(decodeSegment(encodedHeader));
39
+ const payload = decodeSegment(encodedPayload);
40
+ const v = payload !== null && typeof payload === 'object' && 'v' in payload ? payload.v : undefined;
41
+ return { kid: header.kid, v };
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * True when the token names an external key or claims the external version. Unverified: only the
49
+ * SDK's own refusal and verifier dispatch may use it, never a trust decision, so it stays internal.
50
+ */
51
+ export function isExternalContextToken(token) {
52
+ const peeked = peekSignedContext(token);
53
+ return peeked !== null && (peeked.kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX) || peeked.v === 2);
54
+ }
55
+ /** Verifies the EdDSA signature against the pinned key set and returns the still-untyped payload. */
56
+ export function verifySignedContext({ token, publicKeys }) {
57
+ const [encodedHeader, encodedPayload, signature] = splitToken(token);
58
+ const header = headerSchema.parse(decodeSegment(encodedHeader));
59
+ const pem = Object.hasOwn(publicKeys, header.kid) ? publicKeys[header.kid] : undefined;
60
+ if (!pem || !/^[A-Za-z0-9_-]+$/.test(signature))
61
+ throw new SignedContextError();
62
+ const key = createPublicKey(pem);
63
+ if (key.asymmetricKeyType !== 'ed25519' ||
64
+ !verify(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), key, Buffer.from(signature, 'base64url')))
65
+ throw new SignedContextError();
66
+ return { kid: header.kid, payload: decodeSegment(encodedPayload) };
67
+ }
@@ -0,0 +1,18 @@
1
+ import { type ArtifactRequestContext } from '../request-context.js';
2
+ /**
3
+ * The v1 rules that apply once a signature has been verified and the caller has applied its own
4
+ * kid and version rules: schema, then the project, environment, clock and route bindings. Every
5
+ * failure is an ArtifactContextError. Internal: it trusts `payload`, so it must only ever see a
6
+ * payload whose signature was just verified.
7
+ *
8
+ * Shared by the public v1 verifier and the broker's dispatching verifier, so each applies its own
9
+ * gate in front of it and pays for one signature check.
10
+ */
11
+ export declare function parseV1Context({ payload, projectId, environment, method, path, now }: {
12
+ payload: unknown;
13
+ projectId: string;
14
+ environment: string;
15
+ method?: string;
16
+ path?: string;
17
+ now: number;
18
+ }): ArtifactRequestContext;
@@ -0,0 +1,26 @@
1
+ import { ArtifactContextError, artifactRequestContextSchema } from '../request-context.js';
2
+ /**
3
+ * The v1 rules that apply once a signature has been verified and the caller has applied its own
4
+ * kid and version rules: schema, then the project, environment, clock and route bindings. Every
5
+ * failure is an ArtifactContextError. Internal: it trusts `payload`, so it must only ever see a
6
+ * payload whose signature was just verified.
7
+ *
8
+ * Shared by the public v1 verifier and the broker's dispatching verifier, so each applies its own
9
+ * gate in front of it and pays for one signature check.
10
+ */
11
+ export function parseV1Context({ payload, projectId, environment, method, path, now }) {
12
+ const parsed = artifactRequestContextSchema.safeParse(payload);
13
+ if (!parsed.success)
14
+ throw new ArtifactContextError();
15
+ const context = parsed.data;
16
+ if (context.projectId !== projectId || context.environment !== environment ||
17
+ context.exp <= now || context.iat > now + 5 ||
18
+ (method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
19
+ throw new ArtifactContextError();
20
+ Object.freeze(context.bridge);
21
+ Object.freeze(context.lattice);
22
+ Object.freeze(context.capabilities);
23
+ if (context.taskRoutes)
24
+ Object.freeze(context.taskRoutes);
25
+ return Object.freeze(context);
26
+ }
@@ -0,0 +1,22 @@
1
+ import { type ArtifactGatewayPublicKeys, type ArtifactRequestContext, type RequestContextV2External } from '../request-context.js';
2
+ export type AnyArtifactRequestContext = ArtifactRequestContext | RequestContextV2External;
3
+ /**
4
+ * Version-dispatching verifier for platform code that must accept both Atlas-user (v1) and
5
+ * external (v2) contexts, such as the broker. Applications never get this: it is not exported
6
+ * from `./server` or `./server/context`, and the exports map does not expose `server/internal`.
7
+ *
8
+ * Kid and version are paired both ways: an `external-` kid must sign a v2 context, and a v2
9
+ * context must be signed by an `external-` kid. This pairing is the only thing standing between
10
+ * an `external-` kid and the v1 rules: the v1 branch goes straight to the shared v1 parser, not
11
+ * through the public verifier's own external gate. A v2 context in `development` mode is refused
12
+ * until development leases can admit it. The signature is checked once per call.
13
+ */
14
+ export declare function verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, now }: {
15
+ token: string | null;
16
+ publicKeys: ArtifactGatewayPublicKeys;
17
+ projectId: string;
18
+ environment: string;
19
+ method?: string;
20
+ path?: string;
21
+ now?: number;
22
+ }): AnyArtifactRequestContext;
@@ -0,0 +1,53 @@
1
+ import { ArtifactContextError, requestContextV2ExternalSchema, } from '../request-context.js';
2
+ import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, verifySignedContext } from './signed-token.js';
3
+ import { parseV1Context } from './v1-context.js';
4
+ /**
5
+ * Version-dispatching verifier for platform code that must accept both Atlas-user (v1) and
6
+ * external (v2) contexts, such as the broker. Applications never get this: it is not exported
7
+ * from `./server` or `./server/context`, and the exports map does not expose `server/internal`.
8
+ *
9
+ * Kid and version are paired both ways: an `external-` kid must sign a v2 context, and a v2
10
+ * context must be signed by an `external-` kid. This pairing is the only thing standing between
11
+ * an `external-` kid and the v1 rules: the v1 branch goes straight to the shared v1 parser, not
12
+ * through the public verifier's own external gate. A v2 context in `development` mode is refused
13
+ * until development leases can admit it. The signature is checked once per call.
14
+ */
15
+ export function verifyAnyArtifactContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
16
+ let kid;
17
+ let payload;
18
+ try {
19
+ ({ kid, payload } = verifySignedContext({ token, publicKeys }));
20
+ }
21
+ catch {
22
+ throw new ArtifactContextError();
23
+ }
24
+ const external = kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX);
25
+ const claimsV2 = payload !== null && typeof payload === 'object' && 'v' in payload && payload.v === 2;
26
+ if (external !== claimsV2)
27
+ throw new ArtifactContextError();
28
+ if (!claimsV2)
29
+ return parseV1Context({ payload, projectId, environment, method, path, now });
30
+ const parsed = requestContextV2ExternalSchema.safeParse(payload);
31
+ if (!parsed.success)
32
+ throw new ArtifactContextError();
33
+ const context = parsed.data;
34
+ if (context.mode !== 'published')
35
+ throw new ArtifactContextError('external_mode_not_accepted');
36
+ if (context.projectId !== projectId || context.environment !== environment ||
37
+ context.exp <= now || context.iat > now + 5 ||
38
+ // TODO(C1): canonicalizeIngressPath(path) at this comparison (external-context.ts passes the raw route path).
39
+ (method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
40
+ throw new ArtifactContextError();
41
+ return freezeExternalContext(context);
42
+ }
43
+ function freezeExternalContext(context) {
44
+ const { principal } = context;
45
+ Object.freeze(principal.pass);
46
+ Object.freeze(principal.client);
47
+ if (principal.amr)
48
+ Object.freeze(principal.amr);
49
+ Object.freeze(principal);
50
+ Object.freeze(context.crit);
51
+ Object.freeze(context.ext);
52
+ return Object.freeze(context);
53
+ }
@@ -17,7 +17,7 @@ export interface ArtifactLogRecord extends ArtifactLogMetadata {
17
17
  }
18
18
  /** Structured events deliberately exclude request bodies, URLs, arbitrary messages and error objects. */
19
19
  export declare function createArtifactLogger({ context, sink }: {
20
- context: ArtifactRequestContext;
20
+ context: Pick<ArtifactRequestContext, 'projectId' | 'requestId' | 'deploymentId' | 'devSessionId'>;
21
21
  sink?: (record: ArtifactLogRecord) => void;
22
22
  }): {
23
23
  info: (event: string, metadata?: ArtifactLogMetadata) => void;
@@ -0,0 +1,48 @@
1
+ import { type ArtifactRequestContext, type RequestContextV2External } from './request-context.js';
2
+ /** A caller admitted through a manifest-declared ingress, as the gateway verified it. */
3
+ export interface ArtifactExternalPrincipal {
4
+ readonly kind: 'external';
5
+ /** Routing label of the ingress that admitted the caller. Not an authorization attribute. */
6
+ readonly ingress: string;
7
+ readonly provider: string;
8
+ readonly issuer: string;
9
+ /** The identity provider's raw subject (not `ext|`-namespaced). */
10
+ readonly subject: string;
11
+ /** Normalized and verified. */
12
+ readonly email: string;
13
+ readonly emailVerified: true;
14
+ readonly authTime: number | null;
15
+ readonly amr: readonly string[] | null;
16
+ readonly tokenIssuedAt: number;
17
+ readonly client: {
18
+ readonly ip: string;
19
+ /** Observed, not proven: the browser chooses it. */
20
+ readonly userAgent: string;
21
+ readonly userAgentSha256: string;
22
+ readonly origin: string | null;
23
+ };
24
+ }
25
+ /** Who this request acts for. Only `external` callers come through an ingress. */
26
+ export type ArtifactPrincipal = {
27
+ readonly kind: 'atlas_user';
28
+ readonly sub: string;
29
+ } | {
30
+ readonly kind: 'platform_service';
31
+ readonly sub: string;
32
+ } | ArtifactExternalPrincipal;
33
+ export declare function principalFromContext(context: ArtifactRequestContext | RequestContextV2External): ArtifactPrincipal;
34
+ /** The evidence variables an external caller's verified sign-in supplies. */
35
+ export interface ArtifactExternalEvidence {
36
+ authSubject: string;
37
+ actorEmail: string;
38
+ authTime: number | null;
39
+ authMethods: readonly string[] | null;
40
+ ipAddress: string;
41
+ userAgent: string;
42
+ }
43
+ /**
44
+ * The sanctioned mapping from a verified external principal to evidence variables. On
45
+ * manifest-declared evidence operations the broker overwrites (or refuses) these from the same
46
+ * principal, so a mistake here cannot misattribute evidence there.
47
+ */
48
+ export declare function externalEvidence(principal: ArtifactPrincipal): ArtifactExternalEvidence;
@@ -0,0 +1,39 @@
1
+ import { ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX } from './request-context.js';
2
+ export function principalFromContext(context) {
3
+ if (context.v === 1) {
4
+ return Object.freeze(context.sub.startsWith(ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX)
5
+ ? { kind: 'platform_service', sub: context.sub }
6
+ : { kind: 'atlas_user', sub: context.sub });
7
+ }
8
+ const { principal } = context;
9
+ return Object.freeze({
10
+ kind: 'external',
11
+ ingress: principal.ingress,
12
+ provider: principal.provider,
13
+ issuer: principal.issuer,
14
+ subject: principal.subject,
15
+ email: principal.email,
16
+ emailVerified: principal.emailVerified,
17
+ authTime: principal.authTime,
18
+ amr: principal.amr,
19
+ tokenIssuedAt: principal.tokenIssuedAt,
20
+ client: Object.freeze({ ...principal.client }),
21
+ });
22
+ }
23
+ /**
24
+ * The sanctioned mapping from a verified external principal to evidence variables. On
25
+ * manifest-declared evidence operations the broker overwrites (or refuses) these from the same
26
+ * principal, so a mistake here cannot misattribute evidence there.
27
+ */
28
+ export function externalEvidence(principal) {
29
+ if (principal.kind !== 'external')
30
+ throw new TypeError('externalEvidence requires an external principal');
31
+ return {
32
+ authSubject: principal.subject,
33
+ actorEmail: principal.email,
34
+ authTime: principal.authTime,
35
+ authMethods: principal.amr,
36
+ ipAddress: principal.client.ip,
37
+ userAgent: principal.client.userAgent,
38
+ };
39
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { NextArtifactBridgeBootstrap, NextArtifactInitialContext } from '../next/protocol.js';
3
+ import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX } from './internal/signed-token.js';
3
4
  export declare const ARTIFACT_REQUEST_CONTEXT_HEADER = "x-sequence-artifact-context";
4
5
  export declare const ARTIFACT_RUNTIME_TOKEN_HEADER = "x-sequence-artifact-runtime";
5
6
  export declare const ARTIFACT_SERVER_REQUEST_PATH = "/api/artifact-studio/server/request";
@@ -61,10 +62,121 @@ export declare const artifactRequestContextSchema: z.ZodObject<{
61
62
  }, z.core.$strict>;
62
63
  export type ArtifactRequestContext = z.infer<typeof artifactRequestContextSchema>;
63
64
  export type ArtifactGatewayPublicKeys = Readonly<Record<string, string>>;
65
+ export { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX };
66
+ /**
67
+ * Prefix of a platform service account's `sub`. Mirrors Atlas's `SERVICE_ACCOUNT_SUB_PREFIX`
68
+ * (atlas/src/server/lib/auth0-sub.ts); Atlas's auth0-sub test pins the two together.
69
+ */
70
+ export declare const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = "service-account|";
71
+ /** An external context's lifetime cap. External contexts never carry a Lattice run. */
72
+ export declare const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
73
+ /** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
74
+ export declare const artifactExternalPrincipalSchema: z.ZodObject<{
75
+ kind: z.ZodLiteral<"external">;
76
+ ingress: z.ZodString;
77
+ provider: z.ZodString;
78
+ issuer: z.ZodString;
79
+ subject: z.ZodString;
80
+ clientId: z.ZodString;
81
+ email: z.ZodString;
82
+ emailVerified: z.ZodLiteral<true>;
83
+ authTime: z.ZodNullable<z.ZodNumber>;
84
+ amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
85
+ tokenIssuedAt: z.ZodNumber;
86
+ pass: z.ZodObject<{
87
+ id: z.ZodUUID;
88
+ kid: z.ZodString;
89
+ issuedAt: z.ZodNumber;
90
+ exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
91
+ }, z.core.$strict>;
92
+ client: z.ZodObject<{
93
+ ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
94
+ userAgent: z.ZodString;
95
+ userAgentSha256: z.ZodString;
96
+ origin: z.ZodNullable<z.ZodString>;
97
+ }, z.core.$strict>;
98
+ }, z.core.$strict>;
99
+ /**
100
+ * External request context (v2): a caller that is not an Atlas user, admitted through a
101
+ * manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
102
+ * unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
103
+ * verifier understands none, so it must be empty. Security-relevant additions bump `v`.
104
+ *
105
+ * Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
106
+ * explicit external APIs do.
107
+ */
108
+ export declare const requestContextV2ExternalSchema: z.ZodObject<{
109
+ v: z.ZodLiteral<2>;
110
+ iss: z.ZodLiteral<"atlas-artifact-gateway">;
111
+ aud: z.ZodString;
112
+ sub: z.ZodString;
113
+ projectId: z.ZodString;
114
+ environment: z.ZodString;
115
+ mode: z.ZodEnum<{
116
+ published: "published";
117
+ development: "development";
118
+ }>;
119
+ deploymentId: z.ZodNullable<z.ZodString>;
120
+ devSessionId: z.ZodNullable<z.ZodString>;
121
+ requestId: z.ZodString;
122
+ jti: z.ZodUUID;
123
+ method: z.ZodEnum<{
124
+ GET: "GET";
125
+ POST: "POST";
126
+ DELETE: "DELETE";
127
+ HEAD: "HEAD";
128
+ PUT: "PUT";
129
+ PATCH: "PATCH";
130
+ OPTIONS: "OPTIONS";
131
+ }>;
132
+ path: z.ZodString;
133
+ iat: z.ZodNumber;
134
+ exp: z.ZodNumber;
135
+ serviceAccountId: z.ZodString;
136
+ principal: z.ZodObject<{
137
+ kind: z.ZodLiteral<"external">;
138
+ ingress: z.ZodString;
139
+ provider: z.ZodString;
140
+ issuer: z.ZodString;
141
+ subject: z.ZodString;
142
+ clientId: z.ZodString;
143
+ email: z.ZodString;
144
+ emailVerified: z.ZodLiteral<true>;
145
+ authTime: z.ZodNullable<z.ZodNumber>;
146
+ amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
147
+ tokenIssuedAt: z.ZodNumber;
148
+ pass: z.ZodObject<{
149
+ id: z.ZodUUID;
150
+ kid: z.ZodString;
151
+ issuedAt: z.ZodNumber;
152
+ exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
153
+ }, z.core.$strict>;
154
+ client: z.ZodObject<{
155
+ ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
156
+ userAgent: z.ZodString;
157
+ userAgentSha256: z.ZodString;
158
+ origin: z.ZodNullable<z.ZodString>;
159
+ }, z.core.$strict>;
160
+ }, z.core.$strict>;
161
+ crit: z.ZodArray<z.ZodString>;
162
+ ext: z.ZodRecord<z.ZodString, z.ZodUnknown>;
163
+ }, z.core.$strict>;
164
+ export type RequestContextV2External = z.infer<typeof requestContextV2ExternalSchema>;
165
+ /** Why a context was refused, when the refusal has a name a handler can act on. */
166
+ export type ArtifactContextErrorReason = 'external_context_not_accepted' | 'external_context_required' | 'external_ingress_not_accepted' | 'external_mode_not_accepted';
64
167
  export declare class ArtifactContextError extends Error {
65
- constructor();
168
+ readonly reason: ArtifactContextErrorReason | undefined;
169
+ constructor(reason?: ArtifactContextErrorReason);
66
170
  }
67
- /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
171
+ /**
172
+ * Only the gateway owns private keys. Applications pin platform-provided public keys.
173
+ *
174
+ * Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
175
+ * token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
176
+ * before any other check, so a borrower context can never reach a handler that reads `sub` and
177
+ * `capabilities` as an Atlas user. External contexts are verified only through
178
+ * `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
179
+ */
68
180
  export declare function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now }: {
69
181
  token: string | null;
70
182
  publicKeys: ArtifactGatewayPublicKeys;
@@ -76,6 +188,6 @@ export declare function verifyArtifactRequestContext({ token, publicKeys, projec
76
188
  now?: number;
77
189
  }): ArtifactRequestContext;
78
190
  /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
79
- export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext): NextArtifactBridgeBootstrap & {
191
+ export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext | RequestContextV2External): NextArtifactBridgeBootstrap & {
80
192
  initialContext: NextArtifactInitialContext;
81
193
  };