@sequenceholdings/artifact-studio 0.5.0 → 0.6.2
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.
- package/README.md +43 -0
- package/VERSION-PIN.md +27 -0
- package/dist/cli.js +1 -1
- package/dist/manifest-next-ingress.d.ts +213 -0
- package/dist/manifest-next-ingress.js +187 -0
- package/dist/manifest-next-reach.d.ts +40 -0
- package/dist/manifest-next-reach.js +48 -0
- package/dist/manifest-next.d.ts +133 -8
- package/dist/manifest-next.js +7 -46
- package/dist/manifest-static.d.ts +6 -0
- package/dist/manifest-static.js +10 -0
- package/dist/manifest.d.ts +67 -0
- package/dist/manifest.js +7 -0
- package/dist/offline/connector.d.ts +2 -0
- package/dist/offline/connector.js +6 -2
- package/dist/offline/management-types.d.ts +25 -0
- package/dist/offline/management-types.js +1 -0
- package/dist/runtime-contracts.js +1 -1
- package/dist/sdk.d.ts +8 -1
- package/dist/server/api-client.d.ts +27 -0
- package/dist/server/api-client.js +39 -0
- package/dist/server/broker-contract.d.ts +33 -6
- package/dist/server/broker-contract.js +20 -2
- package/dist/server/client.d.ts +32 -45
- package/dist/server/client.js +72 -101
- package/dist/server/connection-retry.d.ts +12 -0
- package/dist/server/connection-retry.js +40 -0
- package/dist/server/errors.d.ts +37 -2
- package/dist/server/errors.js +66 -6
- package/dist/server/external-context.d.ts +21 -0
- package/dist/server/external-context.js +19 -0
- package/dist/server/ingress-control-contract.d.ts +272 -0
- package/dist/server/ingress-control-contract.js +196 -0
- package/dist/server/ingress-routes.d.ts +76 -0
- package/dist/server/ingress-routes.js +150 -0
- package/dist/server/internal/signed-token.d.ts +15 -0
- package/dist/server/internal/signed-token.js +67 -0
- package/dist/server/internal/v1-context.d.ts +18 -0
- package/dist/server/internal/v1-context.js +26 -0
- package/dist/server/internal/verify-any.d.ts +22 -0
- package/dist/server/internal/verify-any.js +61 -0
- package/dist/server/logger.d.ts +1 -1
- package/dist/server/principal.d.ts +48 -0
- package/dist/server/principal.js +39 -0
- package/dist/server/processes.d.ts +56 -0
- package/dist/server/processes.js +13 -0
- package/dist/server/request-context.d.ts +121 -5
- package/dist/server/request-context.js +105 -34
- package/dist/server/runtime-config.d.ts +1 -1
- package/dist/server/testing.d.ts +40 -0
- package/dist/server/testing.js +86 -0
- package/dist/server.d.ts +9 -4
- package/dist/server.js +5 -2
- package/dist/templates/nextjs/README.md +9 -0
- package/dist/templates/nextjs/package.json +3 -1
- package/dist/templates/nextjs/tests/server.test.mjs +25 -0
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/protocol.js +2 -4
- package/dist/tunnel/reserved-headers.d.ts +7 -0
- package/dist/tunnel/reserved-headers.js +20 -0
- package/package.json +19 -3
- package/templates/nextjs/README.md +9 -0
- package/templates/nextjs/package.json +3 -1
- package/templates/nextjs/tests/server.test.mjs +25 -0
- package/templates/react-vite/package.json +1 -1
|
@@ -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
|
+
// verifyAnyArtifactContext canonicalises `path` (canonicalizeIngressPath) before comparing it.
|
|
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,272 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Snapshot bodies above this size are refused by the gateway. */
|
|
3
|
+
export declare const INGRESS_SNAPSHOT_MAX_BYTES: number;
|
|
4
|
+
export declare const INGRESS_AUDIT_BATCH_MAX_EVENTS = 500;
|
|
5
|
+
/** The pass lifetime cap a catalog entry may set (`maxPassSeconds`). */
|
|
6
|
+
export declare const INGRESS_MAX_PASS_SECONDS = 900;
|
|
7
|
+
export declare const INGRESS_IDP_ALGORITHMS: readonly ["RS256", "ES256", "PS256"];
|
|
8
|
+
/** A canonical ingress host: lowercase `ingress-<24 hex>.<runtime domain>`, no port, no trailing dot. */
|
|
9
|
+
export declare const ingressHostSchema: z.ZodString;
|
|
10
|
+
/** Kid of a key that signs external (v2) contexts; Atlas's context key id grammar, with the prefix. */
|
|
11
|
+
export declare const ingressExternalKidSchema: z.ZodString;
|
|
12
|
+
/** `"<METHOD> <template>"`, as `matchIngressRoute` names a route. */
|
|
13
|
+
export declare const ingressRouteIdSchema: z.ZodString;
|
|
14
|
+
/** Where an ingress host proxies. `development` is honoured only by a gateway with APP_ENV=development. */
|
|
15
|
+
export declare const ingressSnapshotTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
16
|
+
kind: z.ZodLiteral<"published">;
|
|
17
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
18
|
+
kind: z.ZodLiteral<"development">;
|
|
19
|
+
sessionId: z.ZodString;
|
|
20
|
+
}, z.core.$strict>], "kind">;
|
|
21
|
+
export declare const ingressSnapshotIngressSchema: z.ZodObject<{
|
|
22
|
+
id: z.ZodString;
|
|
23
|
+
provider: z.ZodString;
|
|
24
|
+
allowedOrigins: z.ZodArray<z.ZodString>;
|
|
25
|
+
headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
26
|
+
routes: z.ZodArray<z.ZodObject<{
|
|
27
|
+
path: z.ZodString;
|
|
28
|
+
methods: z.ZodArray<z.ZodEnum<{
|
|
29
|
+
GET: "GET";
|
|
30
|
+
POST: "POST";
|
|
31
|
+
PUT: "PUT";
|
|
32
|
+
PATCH: "PATCH";
|
|
33
|
+
DELETE: "DELETE";
|
|
34
|
+
}>>;
|
|
35
|
+
fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
max_body_bytes: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
responses: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
38
|
+
"application/json": "application/json";
|
|
39
|
+
"application/problem+json": "application/problem+json";
|
|
40
|
+
"application/pdf": "application/pdf";
|
|
41
|
+
"application/zip": "application/zip";
|
|
42
|
+
"image/png": "image/png";
|
|
43
|
+
"image/jpeg": "image/jpeg";
|
|
44
|
+
"image/webp": "image/webp";
|
|
45
|
+
"text/csv": "text/csv";
|
|
46
|
+
"application/octet-stream": "application/octet-stream";
|
|
47
|
+
}>>>;
|
|
48
|
+
}, z.core.$strict>>;
|
|
49
|
+
epoch: z.ZodNumber;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
export declare const ingressSnapshotHostSchema: z.ZodObject<{
|
|
52
|
+
host: z.ZodString;
|
|
53
|
+
projectId: z.ZodString;
|
|
54
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
55
|
+
kind: z.ZodLiteral<"published">;
|
|
56
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
57
|
+
kind: z.ZodLiteral<"development">;
|
|
58
|
+
sessionId: z.ZodString;
|
|
59
|
+
}, z.core.$strict>], "kind">;
|
|
60
|
+
ingresses: z.ZodArray<z.ZodObject<{
|
|
61
|
+
id: z.ZodString;
|
|
62
|
+
provider: z.ZodString;
|
|
63
|
+
allowedOrigins: z.ZodArray<z.ZodString>;
|
|
64
|
+
headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
65
|
+
routes: z.ZodArray<z.ZodObject<{
|
|
66
|
+
path: z.ZodString;
|
|
67
|
+
methods: z.ZodArray<z.ZodEnum<{
|
|
68
|
+
GET: "GET";
|
|
69
|
+
POST: "POST";
|
|
70
|
+
PUT: "PUT";
|
|
71
|
+
PATCH: "PATCH";
|
|
72
|
+
DELETE: "DELETE";
|
|
73
|
+
}>>;
|
|
74
|
+
fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
|
|
75
|
+
max_body_bytes: z.ZodDefault<z.ZodNumber>;
|
|
76
|
+
responses: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
77
|
+
"application/json": "application/json";
|
|
78
|
+
"application/problem+json": "application/problem+json";
|
|
79
|
+
"application/pdf": "application/pdf";
|
|
80
|
+
"application/zip": "application/zip";
|
|
81
|
+
"image/png": "image/png";
|
|
82
|
+
"image/jpeg": "image/jpeg";
|
|
83
|
+
"image/webp": "image/webp";
|
|
84
|
+
"text/csv": "text/csv";
|
|
85
|
+
"application/octet-stream": "application/octet-stream";
|
|
86
|
+
}>>>;
|
|
87
|
+
}, z.core.$strict>>;
|
|
88
|
+
epoch: z.ZodNumber;
|
|
89
|
+
}, z.core.$strict>>;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
/** The non-secret per-provider summary the gateway's exchange precheck needs. */
|
|
92
|
+
export declare const ingressSnapshotProviderSchema: z.ZodObject<{
|
|
93
|
+
issuer: z.ZodString;
|
|
94
|
+
algorithms: z.ZodArray<z.ZodEnum<{
|
|
95
|
+
RS256: "RS256";
|
|
96
|
+
ES256: "ES256";
|
|
97
|
+
PS256: "PS256";
|
|
98
|
+
}>>;
|
|
99
|
+
}, z.core.$strict>;
|
|
100
|
+
export declare const ingressSnapshotSchema: z.ZodObject<{
|
|
101
|
+
policyEpoch: z.ZodNumber;
|
|
102
|
+
hosts: z.ZodArray<z.ZodObject<{
|
|
103
|
+
host: z.ZodString;
|
|
104
|
+
projectId: z.ZodString;
|
|
105
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
106
|
+
kind: z.ZodLiteral<"published">;
|
|
107
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
108
|
+
kind: z.ZodLiteral<"development">;
|
|
109
|
+
sessionId: z.ZodString;
|
|
110
|
+
}, z.core.$strict>], "kind">;
|
|
111
|
+
ingresses: z.ZodArray<z.ZodObject<{
|
|
112
|
+
id: z.ZodString;
|
|
113
|
+
provider: z.ZodString;
|
|
114
|
+
allowedOrigins: z.ZodArray<z.ZodString>;
|
|
115
|
+
headers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
116
|
+
routes: z.ZodArray<z.ZodObject<{
|
|
117
|
+
path: z.ZodString;
|
|
118
|
+
methods: z.ZodArray<z.ZodEnum<{
|
|
119
|
+
GET: "GET";
|
|
120
|
+
POST: "POST";
|
|
121
|
+
PUT: "PUT";
|
|
122
|
+
PATCH: "PATCH";
|
|
123
|
+
DELETE: "DELETE";
|
|
124
|
+
}>>;
|
|
125
|
+
fresh_auth_seconds: z.ZodOptional<z.ZodNumber>;
|
|
126
|
+
max_body_bytes: z.ZodDefault<z.ZodNumber>;
|
|
127
|
+
responses: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
128
|
+
"application/json": "application/json";
|
|
129
|
+
"application/problem+json": "application/problem+json";
|
|
130
|
+
"application/pdf": "application/pdf";
|
|
131
|
+
"application/zip": "application/zip";
|
|
132
|
+
"image/png": "image/png";
|
|
133
|
+
"image/jpeg": "image/jpeg";
|
|
134
|
+
"image/webp": "image/webp";
|
|
135
|
+
"text/csv": "text/csv";
|
|
136
|
+
"application/octet-stream": "application/octet-stream";
|
|
137
|
+
}>>>;
|
|
138
|
+
}, z.core.$strict>>;
|
|
139
|
+
epoch: z.ZodNumber;
|
|
140
|
+
}, z.core.$strict>>;
|
|
141
|
+
}, z.core.$strict>>;
|
|
142
|
+
providers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
143
|
+
issuer: z.ZodString;
|
|
144
|
+
algorithms: z.ZodArray<z.ZodEnum<{
|
|
145
|
+
RS256: "RS256";
|
|
146
|
+
ES256: "ES256";
|
|
147
|
+
PS256: "PS256";
|
|
148
|
+
}>>;
|
|
149
|
+
}, z.core.$strict>>;
|
|
150
|
+
}, z.core.$strict>;
|
|
151
|
+
export type IngressSnapshot = z.infer<typeof ingressSnapshotSchema>;
|
|
152
|
+
export type IngressSnapshotHost = z.infer<typeof ingressSnapshotHostSchema>;
|
|
153
|
+
export declare const ingressExchangeRequestSchema: z.ZodObject<{
|
|
154
|
+
host: z.ZodString;
|
|
155
|
+
ingress: z.ZodString;
|
|
156
|
+
origin: z.ZodNullable<z.ZodString>;
|
|
157
|
+
token: z.ZodString;
|
|
158
|
+
clientIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
|
|
159
|
+
userAgent: z.ZodString;
|
|
160
|
+
requestId: z.ZodUUID;
|
|
161
|
+
}, z.core.$strict>;
|
|
162
|
+
export type IngressExchangeRequest = z.infer<typeof ingressExchangeRequestSchema>;
|
|
163
|
+
export declare const ingressExchangeResponseSchema: z.ZodObject<{
|
|
164
|
+
principal: z.ZodObject<{
|
|
165
|
+
email: z.ZodString;
|
|
166
|
+
subject: z.ZodString;
|
|
167
|
+
authTime: z.ZodNullable<z.ZodNumber>;
|
|
168
|
+
amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
169
|
+
issuer: z.ZodString;
|
|
170
|
+
clientId: z.ZodString;
|
|
171
|
+
emailVerified: z.ZodLiteral<true>;
|
|
172
|
+
tokenIssuedAt: z.ZodNumber;
|
|
173
|
+
}, z.core.$strict>;
|
|
174
|
+
bindingEpoch: z.ZodNumber;
|
|
175
|
+
passTtlSeconds: z.ZodNumber;
|
|
176
|
+
}, z.core.$strict>;
|
|
177
|
+
export type IngressExchangeResponse = z.infer<typeof ingressExchangeResponseSchema>;
|
|
178
|
+
export declare const ingressGrantRequestSchema: z.ZodObject<{
|
|
179
|
+
host: z.ZodString;
|
|
180
|
+
ingress: z.ZodString;
|
|
181
|
+
provider: z.ZodString;
|
|
182
|
+
subjectHash: z.ZodString;
|
|
183
|
+
tokenIssuedAt: z.ZodNumber;
|
|
184
|
+
passJti: z.ZodUUID;
|
|
185
|
+
method: z.ZodEnum<{
|
|
186
|
+
GET: "GET";
|
|
187
|
+
POST: "POST";
|
|
188
|
+
PUT: "PUT";
|
|
189
|
+
PATCH: "PATCH";
|
|
190
|
+
DELETE: "DELETE";
|
|
191
|
+
}>;
|
|
192
|
+
routeId: z.ZodString;
|
|
193
|
+
}, z.core.$strict>;
|
|
194
|
+
export type IngressGrantRequest = z.infer<typeof ingressGrantRequestSchema>;
|
|
195
|
+
export declare const ingressGrantResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
196
|
+
mode: z.ZodLiteral<"published">;
|
|
197
|
+
deploymentId: z.ZodString;
|
|
198
|
+
frameworkBuildId: z.ZodString;
|
|
199
|
+
upstreamUrl: z.ZodString;
|
|
200
|
+
upstreamAudience: z.ZodOptional<z.ZodString>;
|
|
201
|
+
projectId: z.ZodString;
|
|
202
|
+
environment: z.ZodString;
|
|
203
|
+
serviceAccountId: z.ZodString;
|
|
204
|
+
resources: z.ZodObject<{
|
|
205
|
+
concurrency: z.ZodNumber;
|
|
206
|
+
max_instances: z.ZodNumber;
|
|
207
|
+
timeout_seconds: z.ZodNumber;
|
|
208
|
+
}, z.core.$strict>;
|
|
209
|
+
trustedExternalKids: z.ZodArray<z.ZodString>;
|
|
210
|
+
externalCapable: z.ZodLiteral<true>;
|
|
211
|
+
policyEpoch: z.ZodNumber;
|
|
212
|
+
expiresAt: z.ZodNumber;
|
|
213
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
214
|
+
mode: z.ZodLiteral<"development">;
|
|
215
|
+
devSessionId: z.ZodString;
|
|
216
|
+
deploymentId: z.ZodNull;
|
|
217
|
+
projectId: z.ZodString;
|
|
218
|
+
environment: z.ZodString;
|
|
219
|
+
serviceAccountId: z.ZodString;
|
|
220
|
+
resources: z.ZodObject<{
|
|
221
|
+
concurrency: z.ZodNumber;
|
|
222
|
+
max_instances: z.ZodNumber;
|
|
223
|
+
timeout_seconds: z.ZodNumber;
|
|
224
|
+
}, z.core.$strict>;
|
|
225
|
+
trustedExternalKids: z.ZodArray<z.ZodString>;
|
|
226
|
+
externalCapable: z.ZodLiteral<true>;
|
|
227
|
+
policyEpoch: z.ZodNumber;
|
|
228
|
+
expiresAt: z.ZodNumber;
|
|
229
|
+
}, z.core.$strict>], "mode">;
|
|
230
|
+
export type IngressGrantResponse = z.infer<typeof ingressGrantResponseSchema>;
|
|
231
|
+
/**
|
|
232
|
+
* Logout through a pass the gateway verified (design T22, R2). There is deliberately no `at`:
|
|
233
|
+
* Atlas takes the revocation time from its own clock (`T = now`, "every IdP token issued at or
|
|
234
|
+
* before now") and applies `revoked_before = greatest(revoked_before, T)`, so a control-token
|
|
235
|
+
* holder can never date a revocation into the future and lock a subject out past their next
|
|
236
|
+
* sign-in. It is monotonic and never unblocks. `host`, `ingress` and `passJti` attest the pass the
|
|
237
|
+
* gateway verified, so Atlas can audit the revoke and refuse one for a provider the host's ingress
|
|
238
|
+
* does not use; `tokenIssuedAt` is that pass's IdP `iat`.
|
|
239
|
+
*/
|
|
240
|
+
export declare const ingressRevokeRequestSchema: z.ZodObject<{
|
|
241
|
+
host: z.ZodString;
|
|
242
|
+
ingress: z.ZodString;
|
|
243
|
+
provider: z.ZodString;
|
|
244
|
+
subject: z.ZodString;
|
|
245
|
+
passJti: z.ZodUUID;
|
|
246
|
+
tokenIssuedAt: z.ZodNumber;
|
|
247
|
+
}, z.core.$strict>;
|
|
248
|
+
export type IngressRevokeRequest = z.infer<typeof ingressRevokeRequestSchema>;
|
|
249
|
+
export declare const ingressAuditEventSchema: z.ZodObject<{
|
|
250
|
+
ts: z.ZodNumber;
|
|
251
|
+
host: z.ZodString;
|
|
252
|
+
ingress: z.ZodNullable<z.ZodString>;
|
|
253
|
+
reason: z.ZodString;
|
|
254
|
+
status: z.ZodNumber;
|
|
255
|
+
subjectHash: z.ZodOptional<z.ZodString>;
|
|
256
|
+
clientIp: z.ZodNullable<z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>>;
|
|
257
|
+
passId: z.ZodOptional<z.ZodUUID>;
|
|
258
|
+
}, z.core.$strict>;
|
|
259
|
+
export declare const ingressAuditBatchSchema: z.ZodObject<{
|
|
260
|
+
events: z.ZodArray<z.ZodObject<{
|
|
261
|
+
ts: z.ZodNumber;
|
|
262
|
+
host: z.ZodString;
|
|
263
|
+
ingress: z.ZodNullable<z.ZodString>;
|
|
264
|
+
reason: z.ZodString;
|
|
265
|
+
status: z.ZodNumber;
|
|
266
|
+
subjectHash: z.ZodOptional<z.ZodString>;
|
|
267
|
+
clientIp: z.ZodNullable<z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>>;
|
|
268
|
+
passId: z.ZodOptional<z.ZodUUID>;
|
|
269
|
+
}, z.core.$strict>>;
|
|
270
|
+
}, z.core.$strict>;
|
|
271
|
+
export type IngressAuditEvent = z.infer<typeof ingressAuditEventSchema>;
|
|
272
|
+
export type IngressAuditBatch = z.infer<typeof ingressAuditBatchSchema>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { artifactNextExternalIngressSchema, artifactNextIngressIdSchema, artifactNextIngressProviderRefSchema, } from '../manifest-next-ingress.js';
|
|
3
|
+
import { artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
|
|
4
|
+
import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, artifactExternalPrincipalSchema, artifactIpAddressSchema, artifactOriginSchema, } from './request-context.js';
|
|
5
|
+
import { INGRESS_ROUTE_METHODS, parseRouteTemplate } from './ingress-routes.js';
|
|
6
|
+
/**
|
|
7
|
+
* Bodies of the gateway↔Atlas control routes for the external ingress (design "First call",
|
|
8
|
+
* "Steady state", §(e), §(g) "Gateway denials"). Schemas only: Atlas's `control/ingress-*` routes,
|
|
9
|
+
* the gateway and the development-mode tooling all import these, and nothing redefines them.
|
|
10
|
+
*/
|
|
11
|
+
const id = z.string().min(1).max(255);
|
|
12
|
+
const epochSeconds = z.number().int().nonnegative();
|
|
13
|
+
const counter = z.number().int().nonnegative();
|
|
14
|
+
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, 'Expected a lowercase SHA-256 digest');
|
|
15
|
+
/** Snapshot bodies above this size are refused by the gateway. */
|
|
16
|
+
export const INGRESS_SNAPSHOT_MAX_BYTES = 1024 * 1024;
|
|
17
|
+
export const INGRESS_AUDIT_BATCH_MAX_EVENTS = 500;
|
|
18
|
+
/** The pass lifetime cap a catalog entry may set (`maxPassSeconds`). */
|
|
19
|
+
export const INGRESS_MAX_PASS_SECONDS = 900;
|
|
20
|
+
export const INGRESS_IDP_ALGORITHMS = ['RS256', 'ES256', 'PS256'];
|
|
21
|
+
/** A canonical ingress host: lowercase `ingress-<24 hex>.<runtime domain>`, no port, no trailing dot. */
|
|
22
|
+
export const ingressHostSchema = z.string().max(253).regex(/^ingress-[a-f0-9]{24}(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/, 'Expected a canonical ingress host');
|
|
23
|
+
/** Kid of a key that signs external (v2) contexts; Atlas's context key id grammar, with the prefix. */
|
|
24
|
+
export const ingressExternalKidSchema = z.string().max(64).regex(/^[A-Za-z0-9._-]+$/)
|
|
25
|
+
.refine((kid) => kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX) && kid.length > ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX.length, 'External context kids carry the external- prefix');
|
|
26
|
+
/** `"<METHOD> <template>"`, as `matchIngressRoute` names a route. */
|
|
27
|
+
export const ingressRouteIdSchema = z.string().max(520).refine((routeId) => {
|
|
28
|
+
const separator = routeId.indexOf(' ');
|
|
29
|
+
return INGRESS_ROUTE_METHODS.includes(routeId.slice(0, separator)) &&
|
|
30
|
+
parseRouteTemplate(routeId.slice(separator + 1)) !== null;
|
|
31
|
+
}, 'Expected "<METHOD> <route template>"');
|
|
32
|
+
// GET control/ingress-snapshot -----------------------------------------------------------------
|
|
33
|
+
/** Where an ingress host proxies. `development` is honoured only by a gateway with APP_ENV=development. */
|
|
34
|
+
export const ingressSnapshotTargetSchema = z.discriminatedUnion('kind', [
|
|
35
|
+
z.strictObject({ kind: z.literal('published') }),
|
|
36
|
+
z.strictObject({ kind: z.literal('development'), sessionId: id }),
|
|
37
|
+
]);
|
|
38
|
+
export const ingressSnapshotIngressSchema = z.strictObject({
|
|
39
|
+
id: artifactNextIngressIdSchema,
|
|
40
|
+
provider: artifactNextIngressProviderRefSchema,
|
|
41
|
+
/** The live binding's origins, already narrowed to the catalog's. */
|
|
42
|
+
allowedOrigins: z.array(artifactOriginSchema).max(64),
|
|
43
|
+
headers: artifactNextExternalIngressSchema.shape.headers,
|
|
44
|
+
/** As declared in the manifest the target serves. */
|
|
45
|
+
routes: artifactNextExternalIngressSchema.shape.routes,
|
|
46
|
+
/** The binding's write counter; a cache-buster only. */
|
|
47
|
+
epoch: counter,
|
|
48
|
+
});
|
|
49
|
+
export const ingressSnapshotHostSchema = z.strictObject({
|
|
50
|
+
host: ingressHostSchema,
|
|
51
|
+
projectId: id,
|
|
52
|
+
target: ingressSnapshotTargetSchema,
|
|
53
|
+
ingresses: z.array(ingressSnapshotIngressSchema).min(1).max(4),
|
|
54
|
+
}).superRefine((entry, ctx) => {
|
|
55
|
+
if (new Set(entry.ingresses.map((ingress) => ingress.id)).size !== entry.ingresses.length) {
|
|
56
|
+
ctx.addIssue({ code: 'custom', path: ['ingresses'], message: 'Ingress ids must be unique per host' });
|
|
57
|
+
}
|
|
58
|
+
// Loopback http origins exist only for development catalog entries (design C-8); a published
|
|
59
|
+
// host never carries one, whatever the catalog loader let through.
|
|
60
|
+
if (entry.target.kind !== 'published')
|
|
61
|
+
return;
|
|
62
|
+
for (const [index, ingress] of entry.ingresses.entries()) {
|
|
63
|
+
for (const [originIndex, origin] of ingress.allowedOrigins.entries()) {
|
|
64
|
+
if (!origin.startsWith('https://')) {
|
|
65
|
+
ctx.addIssue({ code: 'custom', path: ['ingresses', index, 'allowedOrigins', originIndex], message: 'A published ingress host allows https origins only' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
/** The non-secret per-provider summary the gateway's exchange precheck needs. */
|
|
71
|
+
export const ingressSnapshotProviderSchema = z.strictObject({
|
|
72
|
+
issuer: z.string().min(1).max(2048),
|
|
73
|
+
algorithms: z.array(z.enum(INGRESS_IDP_ALGORITHMS)).min(1).max(INGRESS_IDP_ALGORITHMS.length),
|
|
74
|
+
});
|
|
75
|
+
export const ingressSnapshotSchema = z.strictObject({
|
|
76
|
+
policyEpoch: counter,
|
|
77
|
+
hosts: z.array(ingressSnapshotHostSchema).max(10_000),
|
|
78
|
+
providers: z.record(artifactNextIngressProviderRefSchema, ingressSnapshotProviderSchema),
|
|
79
|
+
}).superRefine((snapshot, ctx) => {
|
|
80
|
+
const hosts = new Set();
|
|
81
|
+
for (const [index, entry] of snapshot.hosts.entries()) {
|
|
82
|
+
if (hosts.has(entry.host))
|
|
83
|
+
ctx.addIssue({ code: 'custom', path: ['hosts', index, 'host'], message: 'Snapshot hosts must be unique' });
|
|
84
|
+
hosts.add(entry.host);
|
|
85
|
+
for (const [ingressIndex, ingress] of entry.ingresses.entries()) {
|
|
86
|
+
if (!Object.hasOwn(snapshot.providers, ingress.provider)) {
|
|
87
|
+
ctx.addIssue({ code: 'custom', path: ['hosts', index, 'ingresses', ingressIndex, 'provider'], message: 'Every ingress provider needs a snapshot provider summary' });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// POST control/ingress-exchange ----------------------------------------------------------------
|
|
93
|
+
export const ingressExchangeRequestSchema = z.strictObject({
|
|
94
|
+
host: ingressHostSchema,
|
|
95
|
+
ingress: artifactNextIngressIdSchema,
|
|
96
|
+
/** The request's `Origin`; null for a non-browser caller. Atlas requires it within the binding's origins. */
|
|
97
|
+
origin: artifactOriginSchema.nullable(),
|
|
98
|
+
/** The identity provider's access token, without the `Bearer ` scheme. */
|
|
99
|
+
token: z.string().min(1).max(8192),
|
|
100
|
+
/** The single edge-verified `CF-Connecting-IP`. */
|
|
101
|
+
clientIp: artifactIpAddressSchema,
|
|
102
|
+
userAgent: artifactExternalPrincipalSchema.shape.client.shape.userAgent,
|
|
103
|
+
/** Gateway-generated; never client-supplied. */
|
|
104
|
+
requestId: z.uuid(),
|
|
105
|
+
});
|
|
106
|
+
export const ingressExchangeResponseSchema = z.strictObject({
|
|
107
|
+
/** As Atlas verified it. `tokenIssuedAt` is the IdP `iat`, which revocation compares. */
|
|
108
|
+
principal: artifactExternalPrincipalSchema.pick({
|
|
109
|
+
issuer: true, subject: true, clientId: true, email: true, emailVerified: true, authTime: true, amr: true, tokenIssuedAt: true,
|
|
110
|
+
}).strict(),
|
|
111
|
+
bindingEpoch: counter,
|
|
112
|
+
passTtlSeconds: z.number().int().min(1).max(INGRESS_MAX_PASS_SECONDS),
|
|
113
|
+
});
|
|
114
|
+
// POST control/ingress-grant -------------------------------------------------------------------
|
|
115
|
+
export const ingressGrantRequestSchema = z.strictObject({
|
|
116
|
+
host: ingressHostSchema,
|
|
117
|
+
ingress: artifactNextIngressIdSchema,
|
|
118
|
+
provider: artifactNextIngressProviderRefSchema,
|
|
119
|
+
/** `sha256(subject)`, lowercase hex. */
|
|
120
|
+
subjectHash: sha256Hex,
|
|
121
|
+
tokenIssuedAt: epochSeconds,
|
|
122
|
+
passJti: z.uuid(),
|
|
123
|
+
method: z.enum(INGRESS_ROUTE_METHODS),
|
|
124
|
+
routeId: ingressRouteIdSchema,
|
|
125
|
+
}).superRefine((request, ctx) => {
|
|
126
|
+
if (!request.routeId.startsWith(`${request.method} `))
|
|
127
|
+
ctx.addIssue({ code: 'custom', path: ['routeId'], message: 'routeId must name the request method' });
|
|
128
|
+
});
|
|
129
|
+
const grantCommon = {
|
|
130
|
+
projectId: id,
|
|
131
|
+
environment: id,
|
|
132
|
+
/** The ingress's own service account: `ingressServiceAccounts[ingress]`, never the app's. */
|
|
133
|
+
serviceAccountId: id,
|
|
134
|
+
resources: z.strictObject({
|
|
135
|
+
concurrency: z.number().int().min(1).max(80),
|
|
136
|
+
max_instances: z.number().int().min(1).max(100),
|
|
137
|
+
timeout_seconds: z.number().int().min(1).max(300),
|
|
138
|
+
}),
|
|
139
|
+
/** External kids the target trusts; the gateway refuses to sign (503) unless its current kid is listed. */
|
|
140
|
+
trustedExternalKids: z.array(ingressExternalKidSchema).min(1).max(4),
|
|
141
|
+
externalCapable: z.literal(true),
|
|
142
|
+
policyEpoch: counter,
|
|
143
|
+
expiresAt: z.number().int().positive(),
|
|
144
|
+
};
|
|
145
|
+
export const ingressGrantResponseSchema = z.discriminatedUnion('mode', [
|
|
146
|
+
z.strictObject({
|
|
147
|
+
...grantCommon,
|
|
148
|
+
mode: z.literal('published'),
|
|
149
|
+
deploymentId: id,
|
|
150
|
+
frameworkBuildId: artifactNextFrameworkBuildIdSchema,
|
|
151
|
+
upstreamUrl: z.string().url(),
|
|
152
|
+
upstreamAudience: z.string().url().optional(),
|
|
153
|
+
}),
|
|
154
|
+
/** Local development only: the gateway proxies over the session's tunnel instead of an upstream URL. */
|
|
155
|
+
z.strictObject({
|
|
156
|
+
...grantCommon,
|
|
157
|
+
mode: z.literal('development'),
|
|
158
|
+
devSessionId: id,
|
|
159
|
+
deploymentId: z.null(),
|
|
160
|
+
}),
|
|
161
|
+
]);
|
|
162
|
+
// POST control/ingress-revoke ------------------------------------------------------------------
|
|
163
|
+
/**
|
|
164
|
+
* Logout through a pass the gateway verified (design T22, R2). There is deliberately no `at`:
|
|
165
|
+
* Atlas takes the revocation time from its own clock (`T = now`, "every IdP token issued at or
|
|
166
|
+
* before now") and applies `revoked_before = greatest(revoked_before, T)`, so a control-token
|
|
167
|
+
* holder can never date a revocation into the future and lock a subject out past their next
|
|
168
|
+
* sign-in. It is monotonic and never unblocks. `host`, `ingress` and `passJti` attest the pass the
|
|
169
|
+
* gateway verified, so Atlas can audit the revoke and refuse one for a provider the host's ingress
|
|
170
|
+
* does not use; `tokenIssuedAt` is that pass's IdP `iat`.
|
|
171
|
+
*/
|
|
172
|
+
export const ingressRevokeRequestSchema = z.strictObject({
|
|
173
|
+
host: ingressHostSchema,
|
|
174
|
+
ingress: artifactNextIngressIdSchema,
|
|
175
|
+
provider: artifactNextIngressProviderRefSchema,
|
|
176
|
+
subject: id,
|
|
177
|
+
passJti: z.uuid(),
|
|
178
|
+
tokenIssuedAt: epochSeconds,
|
|
179
|
+
});
|
|
180
|
+
// POST control/ingress-audit -------------------------------------------------------------------
|
|
181
|
+
export const ingressAuditEventSchema = z.strictObject({
|
|
182
|
+
/** Epoch milliseconds when the gateway refused the request. */
|
|
183
|
+
ts: z.number().int().nonnegative(),
|
|
184
|
+
host: ingressHostSchema,
|
|
185
|
+
/** Null when the refusal came before an ingress was known (edge header, unknown route). */
|
|
186
|
+
ingress: artifactNextIngressIdSchema.nullable(),
|
|
187
|
+
reason: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/, 'Expected a snake_case reason'),
|
|
188
|
+
status: z.number().int().min(400).max(599),
|
|
189
|
+
subjectHash: sha256Hex.optional(),
|
|
190
|
+
/** Null when the request carried no single valid `CF-Connecting-IP`. */
|
|
191
|
+
clientIp: artifactIpAddressSchema.nullable(),
|
|
192
|
+
passId: z.uuid().optional(),
|
|
193
|
+
});
|
|
194
|
+
export const ingressAuditBatchSchema = z.strictObject({
|
|
195
|
+
events: z.array(ingressAuditEventSchema).min(1).max(INGRESS_AUDIT_BATCH_MAX_EVENTS),
|
|
196
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound route grammar for an Artifact Next external ingress (design §(a) "Paths").
|
|
3
|
+
*
|
|
4
|
+
* One module owns the three steps every component must agree on: the gateway canonicalises the
|
|
5
|
+
* raw request target and matches it, Atlas re-matches the signed path on the active manifest, and
|
|
6
|
+
* the SDK binds the context to the route it serves. The static wildcard matcher
|
|
7
|
+
* (`manifest-static.ts`) is deliberately not used: it prefix-matches, and a route must not.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INGRESS_ROUTE_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
10
|
+
export type IngressRouteMethod = typeof INGRESS_ROUTE_METHODS[number];
|
|
11
|
+
/** Longest raw request target the canonicaliser looks at, the signed context path's own cap. */
|
|
12
|
+
export declare const MAX_INGRESS_PATH_LENGTH = 8192;
|
|
13
|
+
export interface CanonicalIngressPath {
|
|
14
|
+
/** The canonical pathname, the only part routes match. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** The raw query string without its `?`, kept verbatim; `''` when there is none. */
|
|
17
|
+
query: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Canonicalises a raw origin-form request target (`/path?query`). Returns `null`, so the caller
|
|
21
|
+
* answers 404, when the pathname is not already canonical: it differs from what the URL parser
|
|
22
|
+
* makes of it, contains an empty, `.` or `..` segment (a trailing slash is an empty segment), or
|
|
23
|
+
* contains `%`; or when the query is not already in the form the URL parser serializes it to.
|
|
24
|
+
* The query string is never used for matching and is returned verbatim.
|
|
25
|
+
*/
|
|
26
|
+
export declare function canonicalizeIngressPath(raw: string): CanonicalIngressPath | null;
|
|
27
|
+
/** The single string the gateway signs, Atlas re-matches and the SDK binds: path plus raw query. */
|
|
28
|
+
export declare function signedIngressPath({ path, query }: CanonicalIngressPath): string;
|
|
29
|
+
export type IngressRouteSegment = {
|
|
30
|
+
kind: 'literal';
|
|
31
|
+
value: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'placeholder';
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
export interface ParsedIngressRouteTemplate {
|
|
37
|
+
template: string;
|
|
38
|
+
segments: readonly IngressRouteSegment[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parses a declared route template such as `/api/borrower/documents/{documentId}/notes`.
|
|
42
|
+
* Literal segments use the Lattice `managed_server` path grammar; a `{name}` placeholder is a
|
|
43
|
+
* whole segment. Returns `null` for anything else: no `/api/` prefix, a wildcard, `%`, a query,
|
|
44
|
+
* a trailing slash, an empty, `.` or `..` segment, or a repeated placeholder name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseRouteTemplate(template: string): ParsedIngressRouteTemplate | null;
|
|
47
|
+
export interface IngressRouteDeclaration {
|
|
48
|
+
path: string;
|
|
49
|
+
methods: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
export interface IngressRouteMatch<Route extends IngressRouteDeclaration = IngressRouteDeclaration> {
|
|
52
|
+
/** `"<METHOD> <template>"`, stable across releases while the declaration is unchanged. */
|
|
53
|
+
routeId: string;
|
|
54
|
+
params: Readonly<Record<string, string>>;
|
|
55
|
+
route: Route;
|
|
56
|
+
}
|
|
57
|
+
export declare function ingressRouteId({ method, template }: {
|
|
58
|
+
method: string;
|
|
59
|
+
template: string;
|
|
60
|
+
}): string;
|
|
61
|
+
/**
|
|
62
|
+
* Exact-matches a method and a canonical pathname (from `canonicalizeIngressPath(...).path`, never
|
|
63
|
+
* the query) against declared routes. Methods compare exactly. An unparseable declaration never
|
|
64
|
+
* matches, and a path that two declarations both match returns `null`: the manifest refuses
|
|
65
|
+
* overlapping routes, so ambiguity here means the declarations did not come from a valid manifest.
|
|
66
|
+
*/
|
|
67
|
+
export declare function matchIngressRoute<Route extends IngressRouteDeclaration>(routes: readonly Route[], method: string, canonicalPath: string): IngressRouteMatch<Route> | null;
|
|
68
|
+
/**
|
|
69
|
+
* Whether one request path could match both templates: same segment count, and at every position
|
|
70
|
+
* either equal literals or at least one placeholder. Conservative: a placeholder is treated as
|
|
71
|
+
* matching any literal opposite it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function ingressRouteTemplatesOverlap({ left, right }: {
|
|
74
|
+
left: ParsedIngressRouteTemplate;
|
|
75
|
+
right: ParsedIngressRouteTemplate;
|
|
76
|
+
}): boolean;
|