@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
|
@@ -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";
|
|
@@ -10,6 +11,8 @@ export declare const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
|
|
|
10
11
|
* window applies only when the signed context names a Lattice run.
|
|
11
12
|
*/
|
|
12
13
|
export declare const ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS = 300;
|
|
14
|
+
/** An exact origin: HTTPS, or HTTP on a loopback host. */
|
|
15
|
+
export declare const artifactOriginSchema: z.ZodString;
|
|
13
16
|
/** Public request context. No bearer credentials or secret values belong here. */
|
|
14
17
|
export declare const artifactRequestContextSchema: z.ZodObject<{
|
|
15
18
|
v: z.ZodLiteral<1>;
|
|
@@ -30,10 +33,10 @@ export declare const artifactRequestContextSchema: z.ZodObject<{
|
|
|
30
33
|
method: z.ZodEnum<{
|
|
31
34
|
GET: "GET";
|
|
32
35
|
POST: "POST";
|
|
33
|
-
DELETE: "DELETE";
|
|
34
|
-
HEAD: "HEAD";
|
|
35
36
|
PUT: "PUT";
|
|
36
37
|
PATCH: "PATCH";
|
|
38
|
+
DELETE: "DELETE";
|
|
39
|
+
HEAD: "HEAD";
|
|
37
40
|
OPTIONS: "OPTIONS";
|
|
38
41
|
}>;
|
|
39
42
|
path: z.ZodString;
|
|
@@ -61,10 +64,123 @@ export declare const artifactRequestContextSchema: z.ZodObject<{
|
|
|
61
64
|
}, z.core.$strict>;
|
|
62
65
|
export type ArtifactRequestContext = z.infer<typeof artifactRequestContextSchema>;
|
|
63
66
|
export type ArtifactGatewayPublicKeys = Readonly<Record<string, string>>;
|
|
67
|
+
export { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX };
|
|
68
|
+
/**
|
|
69
|
+
* Prefix of a platform service account's `sub`. Mirrors Atlas's `SERVICE_ACCOUNT_SUB_PREFIX`
|
|
70
|
+
* (atlas/src/server/lib/auth0-sub.ts); Atlas's auth0-sub test pins the two together.
|
|
71
|
+
*/
|
|
72
|
+
export declare const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = "service-account|";
|
|
73
|
+
/** An external context's lifetime cap. External contexts never carry a Lattice run. */
|
|
74
|
+
export declare const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
|
|
75
|
+
/** An IPv4 or IPv6 address, as the gateway observed it. The control contract reuses it. */
|
|
76
|
+
export declare const artifactIpAddressSchema: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
|
|
77
|
+
/** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
|
|
78
|
+
export declare const artifactExternalPrincipalSchema: z.ZodObject<{
|
|
79
|
+
kind: z.ZodLiteral<"external">;
|
|
80
|
+
ingress: z.ZodString;
|
|
81
|
+
provider: z.ZodString;
|
|
82
|
+
issuer: z.ZodString;
|
|
83
|
+
subject: z.ZodString;
|
|
84
|
+
clientId: z.ZodString;
|
|
85
|
+
email: z.ZodString;
|
|
86
|
+
emailVerified: z.ZodLiteral<true>;
|
|
87
|
+
authTime: z.ZodNullable<z.ZodNumber>;
|
|
88
|
+
amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
89
|
+
tokenIssuedAt: z.ZodNumber;
|
|
90
|
+
pass: z.ZodObject<{
|
|
91
|
+
id: z.ZodUUID;
|
|
92
|
+
kid: z.ZodString;
|
|
93
|
+
issuedAt: z.ZodNumber;
|
|
94
|
+
exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
|
|
95
|
+
}, z.core.$strict>;
|
|
96
|
+
client: z.ZodObject<{
|
|
97
|
+
ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
|
|
98
|
+
userAgent: z.ZodString;
|
|
99
|
+
userAgentSha256: z.ZodString;
|
|
100
|
+
origin: z.ZodNullable<z.ZodString>;
|
|
101
|
+
}, z.core.$strict>;
|
|
102
|
+
}, z.core.$strict>;
|
|
103
|
+
/**
|
|
104
|
+
* External request context (v2): a caller that is not an Atlas user, admitted through a
|
|
105
|
+
* manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
|
|
106
|
+
* unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
|
|
107
|
+
* verifier understands none, so it must be empty. Security-relevant additions bump `v`.
|
|
108
|
+
*
|
|
109
|
+
* Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
|
|
110
|
+
* explicit external APIs do.
|
|
111
|
+
*/
|
|
112
|
+
export declare const requestContextV2ExternalSchema: z.ZodObject<{
|
|
113
|
+
v: z.ZodLiteral<2>;
|
|
114
|
+
iss: z.ZodLiteral<"atlas-artifact-gateway">;
|
|
115
|
+
aud: z.ZodString;
|
|
116
|
+
sub: z.ZodString;
|
|
117
|
+
projectId: z.ZodString;
|
|
118
|
+
environment: z.ZodString;
|
|
119
|
+
mode: z.ZodEnum<{
|
|
120
|
+
published: "published";
|
|
121
|
+
development: "development";
|
|
122
|
+
}>;
|
|
123
|
+
deploymentId: z.ZodNullable<z.ZodString>;
|
|
124
|
+
devSessionId: z.ZodNullable<z.ZodString>;
|
|
125
|
+
requestId: z.ZodString;
|
|
126
|
+
jti: z.ZodUUID;
|
|
127
|
+
method: z.ZodEnum<{
|
|
128
|
+
GET: "GET";
|
|
129
|
+
POST: "POST";
|
|
130
|
+
PUT: "PUT";
|
|
131
|
+
PATCH: "PATCH";
|
|
132
|
+
DELETE: "DELETE";
|
|
133
|
+
HEAD: "HEAD";
|
|
134
|
+
OPTIONS: "OPTIONS";
|
|
135
|
+
}>;
|
|
136
|
+
path: z.ZodString;
|
|
137
|
+
iat: z.ZodNumber;
|
|
138
|
+
exp: z.ZodNumber;
|
|
139
|
+
serviceAccountId: z.ZodString;
|
|
140
|
+
principal: z.ZodObject<{
|
|
141
|
+
kind: z.ZodLiteral<"external">;
|
|
142
|
+
ingress: z.ZodString;
|
|
143
|
+
provider: z.ZodString;
|
|
144
|
+
issuer: z.ZodString;
|
|
145
|
+
subject: z.ZodString;
|
|
146
|
+
clientId: z.ZodString;
|
|
147
|
+
email: z.ZodString;
|
|
148
|
+
emailVerified: z.ZodLiteral<true>;
|
|
149
|
+
authTime: z.ZodNullable<z.ZodNumber>;
|
|
150
|
+
amr: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
151
|
+
tokenIssuedAt: z.ZodNumber;
|
|
152
|
+
pass: z.ZodObject<{
|
|
153
|
+
id: z.ZodUUID;
|
|
154
|
+
kid: z.ZodString;
|
|
155
|
+
issuedAt: z.ZodNumber;
|
|
156
|
+
exchangeIp: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
|
|
157
|
+
}, z.core.$strict>;
|
|
158
|
+
client: z.ZodObject<{
|
|
159
|
+
ip: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6]>;
|
|
160
|
+
userAgent: z.ZodString;
|
|
161
|
+
userAgentSha256: z.ZodString;
|
|
162
|
+
origin: z.ZodNullable<z.ZodString>;
|
|
163
|
+
}, z.core.$strict>;
|
|
164
|
+
}, z.core.$strict>;
|
|
165
|
+
crit: z.ZodArray<z.ZodString>;
|
|
166
|
+
ext: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
167
|
+
}, z.core.$strict>;
|
|
168
|
+
export type RequestContextV2External = z.infer<typeof requestContextV2ExternalSchema>;
|
|
169
|
+
/** Why a context was refused, when the refusal has a name a handler can act on. */
|
|
170
|
+
export type ArtifactContextErrorReason = 'external_context_not_accepted' | 'external_context_required' | 'external_ingress_not_accepted' | 'external_mode_not_accepted';
|
|
64
171
|
export declare class ArtifactContextError extends Error {
|
|
65
|
-
|
|
172
|
+
readonly reason: ArtifactContextErrorReason | undefined;
|
|
173
|
+
constructor(reason?: ArtifactContextErrorReason);
|
|
66
174
|
}
|
|
67
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Only the gateway owns private keys. Applications pin platform-provided public keys.
|
|
177
|
+
*
|
|
178
|
+
* Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
|
|
179
|
+
* token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
|
|
180
|
+
* before any other check, so a borrower context can never reach a handler that reads `sub` and
|
|
181
|
+
* `capabilities` as an Atlas user. External contexts are verified only through
|
|
182
|
+
* `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
|
|
183
|
+
*/
|
|
68
184
|
export declare function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now }: {
|
|
69
185
|
token: string | null;
|
|
70
186
|
publicKeys: ArtifactGatewayPublicKeys;
|
|
@@ -76,6 +192,6 @@ export declare function verifyArtifactRequestContext({ token, publicKeys, projec
|
|
|
76
192
|
now?: number;
|
|
77
193
|
}): ArtifactRequestContext;
|
|
78
194
|
/** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
|
|
79
|
-
export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext): NextArtifactBridgeBootstrap & {
|
|
195
|
+
export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext | RequestContextV2External): NextArtifactBridgeBootstrap & {
|
|
80
196
|
initialContext: NextArtifactInitialContext;
|
|
81
197
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createPublicKey, verify } from 'node:crypto';
|
|
2
1
|
import { z } from 'zod';
|
|
3
2
|
import { isLoopbackHostname } from '../loopback.js';
|
|
3
|
+
import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, isExternalContextToken, verifySignedContext } from './internal/signed-token.js';
|
|
4
|
+
import { parseV1Context } from './internal/v1-context.js';
|
|
4
5
|
export const ARTIFACT_REQUEST_CONTEXT_HEADER = 'x-sequence-artifact-context';
|
|
5
6
|
export const ARTIFACT_RUNTIME_TOKEN_HEADER = 'x-sequence-artifact-runtime';
|
|
6
7
|
export const ARTIFACT_SERVER_REQUEST_PATH = '/api/artifact-studio/server/request';
|
|
@@ -12,7 +13,8 @@ export const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
|
|
|
12
13
|
*/
|
|
13
14
|
export const ARTIFACT_LATTICE_CONTEXT_MAX_AGE_SECONDS = 300;
|
|
14
15
|
const id = z.string().min(1).max(255);
|
|
15
|
-
|
|
16
|
+
/** An exact origin: HTTPS, or HTTP on a loopback host. */
|
|
17
|
+
export const artifactOriginSchema = z.string().max(2048).refine(value => {
|
|
16
18
|
try {
|
|
17
19
|
const url = new URL(value);
|
|
18
20
|
return value === url.origin && (url.protocol === 'https:' ||
|
|
@@ -45,7 +47,7 @@ export const artifactRequestContextSchema = z.object({
|
|
|
45
47
|
exp: z.number().int().positive(),
|
|
46
48
|
capabilities: z.array(id).max(256),
|
|
47
49
|
serviceAccountId: id.nullable(),
|
|
48
|
-
bridge: z.object({ parentOrigin:
|
|
50
|
+
bridge: z.object({ parentOrigin: artifactOriginSchema, channelId: id, nonce: z.string().min(16).max(255) }).strict(),
|
|
49
51
|
theme: z.enum(['light', 'dark', 'system']),
|
|
50
52
|
lattice: z.object({ runId: id.nullable(), nodeId: id.nullable(), stepId: id.nullable(), processId: id.nullable() }).strict(),
|
|
51
53
|
taskRoutes: z.array(z.string().min(1).max(512)).max(16).refine(routes => routes.join('').length <= 1024).optional(),
|
|
@@ -60,49 +62,118 @@ export const artifactRequestContextSchema = z.object({
|
|
|
60
62
|
if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
|
|
61
63
|
ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
|
|
62
64
|
});
|
|
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 const ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX = 'service-account|';
|
|
71
|
+
/** An external context's lifetime cap. External contexts never carry a Lattice run. */
|
|
72
|
+
export const ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS = 60;
|
|
73
|
+
/** An IPv4 or IPv6 address, as the gateway observed it. The control contract reuses it. */
|
|
74
|
+
export const artifactIpAddressSchema = z.union([z.ipv4(), z.ipv6()]);
|
|
75
|
+
/** Signed by the gateway after it strips control characters and truncates; refused here otherwise. */
|
|
76
|
+
const observedUserAgent = z.string().max(512).refine(value => !/[\u0000-\u001f\u007f]/.test(value), 'Expected a user agent without control characters');
|
|
77
|
+
/** The external caller as the gateway verified it at pass exchange. Strict: see the v2 key policy below. */
|
|
78
|
+
export const artifactExternalPrincipalSchema = z.object({
|
|
79
|
+
kind: z.literal('external'),
|
|
80
|
+
/** Routing label for the manifest's ingress declaration. Not an authorization attribute. */
|
|
81
|
+
ingress: id,
|
|
82
|
+
provider: id,
|
|
83
|
+
issuer: z.string().min(1).max(2048),
|
|
84
|
+
subject: id,
|
|
85
|
+
clientId: id,
|
|
86
|
+
/** Normalized (trimmed, lowercase) by Atlas at exchange. */
|
|
87
|
+
email: z.string().min(3).max(320).refine(value => value === value.trim().toLowerCase() && value.includes('@'), 'Expected a normalized email'),
|
|
88
|
+
emailVerified: z.literal(true),
|
|
89
|
+
authTime: z.number().int().nonnegative().nullable(),
|
|
90
|
+
amr: z.array(z.string().min(1).max(64)).max(16).nullable(),
|
|
91
|
+
tokenIssuedAt: z.number().int().nonnegative(),
|
|
92
|
+
pass: z.object({ id: z.uuid(), kid: id, issuedAt: z.number().int().nonnegative(), exchangeIp: artifactIpAddressSchema }).strict(),
|
|
93
|
+
client: z.object({
|
|
94
|
+
ip: artifactIpAddressSchema,
|
|
95
|
+
userAgent: observedUserAgent,
|
|
96
|
+
userAgentSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
97
|
+
origin: artifactOriginSchema.nullable(),
|
|
98
|
+
}).strict(),
|
|
99
|
+
}).strict();
|
|
100
|
+
/**
|
|
101
|
+
* External request context (v2): a caller that is not an Atlas user, admitted through a
|
|
102
|
+
* manifest-declared ingress. Key policy: any unknown top-level or `principal` key is refused
|
|
103
|
+
* unless it sits under `ext`. `crit` is reserved for extensions a verifier must understand; this
|
|
104
|
+
* verifier understands none, so it must be empty. Security-relevant additions bump `v`.
|
|
105
|
+
*
|
|
106
|
+
* Public verifiers ({@link verifyArtifactRequestContext}) never accept this version; only the
|
|
107
|
+
* explicit external APIs do.
|
|
108
|
+
*/
|
|
109
|
+
export const requestContextV2ExternalSchema = z.object({
|
|
110
|
+
v: z.literal(2),
|
|
111
|
+
iss: z.literal('atlas-artifact-gateway'),
|
|
112
|
+
aud: z.string().min(1).max(300),
|
|
113
|
+
sub: id.refine(value => value.startsWith('ext|'), 'External subjects are namespaced ext|<provider>|<subject>'),
|
|
114
|
+
projectId: id,
|
|
115
|
+
environment: id,
|
|
116
|
+
mode: z.enum(['published', 'development']),
|
|
117
|
+
deploymentId: id.nullable(),
|
|
118
|
+
devSessionId: id.nullable(),
|
|
119
|
+
requestId: id,
|
|
120
|
+
jti: z.uuid(),
|
|
121
|
+
method: artifactRequestContextSchema.shape.method,
|
|
122
|
+
path: artifactRequestContextSchema.shape.path,
|
|
123
|
+
iat: z.number().int().nonnegative(),
|
|
124
|
+
exp: z.number().int().positive(),
|
|
125
|
+
/** The ingress's own service account; external principals act only through service mode. */
|
|
126
|
+
serviceAccountId: id,
|
|
127
|
+
principal: artifactExternalPrincipalSchema,
|
|
128
|
+
crit: z.array(z.string()).length(0),
|
|
129
|
+
ext: z.record(z.string().min(1).max(64), z.unknown()),
|
|
130
|
+
}).strict().superRefine((value, ctx) => {
|
|
131
|
+
if (value.aud !== `artifact:${value.projectId}`)
|
|
132
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid Artifact audience' });
|
|
133
|
+
if (value.sub !== `ext|${value.principal.provider}|${value.principal.subject}`)
|
|
134
|
+
ctx.addIssue({ code: 'custom', message: 'External subject does not match its principal' });
|
|
135
|
+
if (value.exp <= value.iat || value.exp - value.iat > ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS)
|
|
136
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid Artifact context lifetime' });
|
|
137
|
+
if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
|
|
138
|
+
ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
|
|
139
|
+
});
|
|
63
140
|
export class ArtifactContextError extends Error {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
141
|
+
reason;
|
|
142
|
+
constructor(reason) {
|
|
143
|
+
super('Missing or invalid Artifact gateway request context');
|
|
144
|
+
this.name = 'ArtifactContextError';
|
|
145
|
+
this.reason = reason;
|
|
146
|
+
}
|
|
70
147
|
}
|
|
71
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Only the gateway owns private keys. Applications pin platform-provided public keys.
|
|
150
|
+
*
|
|
151
|
+
* Accepts v1 (Atlas user and platform service) contexts only. An external (v2) context, or any
|
|
152
|
+
* token signed with an `external-` kid, is refused with reason `external_context_not_accepted`
|
|
153
|
+
* before any other check, so a borrower context can never reach a handler that reads `sub` and
|
|
154
|
+
* `capabilities` as an Atlas user. External contexts are verified only through
|
|
155
|
+
* `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`.
|
|
156
|
+
*/
|
|
72
157
|
export function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
|
|
158
|
+
if (isExternalContextToken(token))
|
|
159
|
+
throw new ArtifactContextError('external_context_not_accepted');
|
|
160
|
+
let payload;
|
|
73
161
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const parts = token.split('.');
|
|
77
|
-
if (parts.length !== 3)
|
|
78
|
-
throw new ArtifactContextError();
|
|
79
|
-
const [encodedHeader, encodedPayload, signature] = parts;
|
|
80
|
-
const header = z.object({ alg: z.literal('EdDSA'), typ: z.literal('JWT'), kid: id }).strict().parse(decode(encodedHeader));
|
|
81
|
-
const pem = Object.hasOwn(publicKeys, header.kid) ? publicKeys[header.kid] : undefined;
|
|
82
|
-
if (!pem || !/^[A-Za-z0-9_-]+$/.test(signature))
|
|
83
|
-
throw new ArtifactContextError();
|
|
84
|
-
const key = createPublicKey(pem);
|
|
85
|
-
if (key.asymmetricKeyType !== 'ed25519' ||
|
|
86
|
-
!verify(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), key, Buffer.from(signature, 'base64url')))
|
|
87
|
-
throw new ArtifactContextError();
|
|
88
|
-
const context = artifactRequestContextSchema.parse(decode(encodedPayload));
|
|
89
|
-
if (context.projectId !== projectId || context.environment !== environment ||
|
|
90
|
-
context.exp <= now || context.iat > now + 5 ||
|
|
91
|
-
(method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
|
|
162
|
+
const verified = verifySignedContext({ token, publicKeys });
|
|
163
|
+
if (verified.kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX))
|
|
92
164
|
throw new ArtifactContextError();
|
|
93
|
-
|
|
94
|
-
Object.freeze(context.lattice);
|
|
95
|
-
Object.freeze(context.capabilities);
|
|
96
|
-
if (context.taskRoutes)
|
|
97
|
-
Object.freeze(context.taskRoutes);
|
|
98
|
-
return Object.freeze(context);
|
|
165
|
+
payload = verified.payload;
|
|
99
166
|
}
|
|
100
167
|
catch {
|
|
101
168
|
throw new ArtifactContextError();
|
|
102
169
|
}
|
|
170
|
+
return parseV1Context({ payload, projectId, environment, method, path, now });
|
|
103
171
|
}
|
|
104
172
|
/** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
|
|
105
173
|
export function getArtifactBrowserBootstrap(context) {
|
|
174
|
+
// An external caller has no Atlas bridge, theme or Lattice run to hand to a browser.
|
|
175
|
+
if (context.v !== 1)
|
|
176
|
+
throw new ArtifactContextError('external_context_not_accepted');
|
|
106
177
|
return {
|
|
107
178
|
parentOrigin: context.bridge.parentOrigin,
|
|
108
179
|
channelId: context.bridge.channelId,
|
|
@@ -32,7 +32,7 @@ export declare function getArtifactRequestContext({ headers, runtime, method, pa
|
|
|
32
32
|
devSessionId: string | null;
|
|
33
33
|
requestId: string;
|
|
34
34
|
jti: string;
|
|
35
|
-
method: "GET" | "POST" | "
|
|
35
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
36
36
|
path: string;
|
|
37
37
|
iat: number;
|
|
38
38
|
exp: number;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
import { type RequestContextV2External } from './request-context.js';
|
|
3
|
+
import { type ArtifactRuntimeConfig } from './runtime-config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Test helpers for applications that accept external callers: sign an external (v2) context
|
|
6
|
+
* with a test key, and run code against an explicit runtime configuration. For unit tests and
|
|
7
|
+
* local `next dev` only. A context signed here verifies only against the matching public key,
|
|
8
|
+
* which a hosted runtime never pins.
|
|
9
|
+
*/
|
|
10
|
+
type ExternalPrincipalClaims = RequestContextV2External['principal'];
|
|
11
|
+
export interface SignTestExternalContextInput {
|
|
12
|
+
privateKey: KeyObject | string;
|
|
13
|
+
/** Use an `external-` kid; verifiers refuse any other kid on an external context. */
|
|
14
|
+
kid: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
environment: string;
|
|
17
|
+
method: string;
|
|
18
|
+
path: string;
|
|
19
|
+
/** Overrides on a verified-borrower default principal. */
|
|
20
|
+
principal?: Partial<Omit<ExternalPrincipalClaims, 'kind'>>;
|
|
21
|
+
serviceAccountId: string;
|
|
22
|
+
/** Defaults to `published`. Verifiers refuse `development` until development leases admit it. */
|
|
23
|
+
mode?: RequestContextV2External['mode'];
|
|
24
|
+
deploymentId?: string | null;
|
|
25
|
+
devSessionId?: string | null;
|
|
26
|
+
/** Seconds since the epoch; defaults to now. */
|
|
27
|
+
now?: number;
|
|
28
|
+
/** Defaults to and may not exceed 60. */
|
|
29
|
+
ttlSeconds?: number;
|
|
30
|
+
/** Overrides applied after the defaults, unvalidated, for negative tests. */
|
|
31
|
+
claims?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export declare function signTestExternalContext({ privateKey, kid, projectId, environment, method, path, principal, serviceAccountId, mode, deploymentId, devSessionId, now, ttlSeconds, claims, }: SignTestExternalContextInput): string;
|
|
34
|
+
/**
|
|
35
|
+
* Runs `fn` with the hosted runtime configuration set to `config` (and no runtime file), then
|
|
36
|
+
* restores the previous environment. The override is process-wide: do not run overlapping calls.
|
|
37
|
+
* Refuses to run in a production build.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withTestRuntimeConfig<T>(config: ArtifactRuntimeConfig, fn: () => Promise<T> | T): Promise<T>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, randomUUID, sign } from 'node:crypto';
|
|
2
|
+
import { ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS, requestContextV2ExternalSchema } from './request-context.js';
|
|
3
|
+
import { artifactRuntimeConfigSchema } from './runtime-config.js';
|
|
4
|
+
const defaultUserAgent = 'Mozilla/5.0 (test)';
|
|
5
|
+
export function signTestExternalContext({ privateKey, kid, projectId, environment, method, path, principal = {}, serviceAccountId, mode = 'published', deploymentId, devSessionId, now = Math.floor(Date.now() / 1000), ttlSeconds = ARTIFACT_EXTERNAL_CONTEXT_MAX_AGE_SECONDS, claims = {}, }) {
|
|
6
|
+
const userAgent = principal.client?.userAgent ?? defaultUserAgent;
|
|
7
|
+
const resolvedPrincipal = {
|
|
8
|
+
kind: 'external',
|
|
9
|
+
ingress: 'borrower',
|
|
10
|
+
provider: 'test-provider',
|
|
11
|
+
issuer: 'https://idp.example.test/',
|
|
12
|
+
subject: 'test|borrower-1',
|
|
13
|
+
clientId: 'test-client',
|
|
14
|
+
email: 'borrower@example.test',
|
|
15
|
+
emailVerified: true,
|
|
16
|
+
authTime: now - 30,
|
|
17
|
+
amr: ['pwd'],
|
|
18
|
+
tokenIssuedAt: now - 30,
|
|
19
|
+
...principal,
|
|
20
|
+
pass: { id: randomUUID(), kid: 'pass-1', issuedAt: now - 10, exchangeIp: '203.0.113.10', ...principal.pass },
|
|
21
|
+
client: {
|
|
22
|
+
ip: '203.0.113.10',
|
|
23
|
+
userAgentSha256: createHash('sha256').update(userAgent).digest('hex'),
|
|
24
|
+
origin: 'https://borrower.example.test',
|
|
25
|
+
...principal.client,
|
|
26
|
+
userAgent,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const published = mode === 'published';
|
|
30
|
+
const payload = {
|
|
31
|
+
v: 2,
|
|
32
|
+
iss: 'atlas-artifact-gateway',
|
|
33
|
+
aud: `artifact:${projectId}`,
|
|
34
|
+
sub: `ext|${resolvedPrincipal.provider}|${resolvedPrincipal.subject}`,
|
|
35
|
+
projectId,
|
|
36
|
+
environment,
|
|
37
|
+
mode,
|
|
38
|
+
deploymentId: deploymentId !== undefined ? deploymentId : published ? 'test-deployment' : null,
|
|
39
|
+
devSessionId: devSessionId !== undefined ? devSessionId : published ? null : 'test-dev-session',
|
|
40
|
+
requestId: randomUUID(),
|
|
41
|
+
jti: randomUUID(),
|
|
42
|
+
method: method.toUpperCase(),
|
|
43
|
+
path,
|
|
44
|
+
iat: now,
|
|
45
|
+
exp: now + ttlSeconds,
|
|
46
|
+
serviceAccountId,
|
|
47
|
+
principal: resolvedPrincipal,
|
|
48
|
+
crit: [],
|
|
49
|
+
ext: {},
|
|
50
|
+
...claims,
|
|
51
|
+
};
|
|
52
|
+
// Catch a mistaken default early; `claims` overrides are for deliberately invalid contexts.
|
|
53
|
+
if (Object.keys(claims).length === 0)
|
|
54
|
+
requestContextV2ExternalSchema.parse(payload);
|
|
55
|
+
const key = typeof privateKey === 'string' ? createPrivateKey(privateKey) : privateKey;
|
|
56
|
+
const unsigned = [{ alg: 'EdDSA', typ: 'JWT', kid }, payload]
|
|
57
|
+
.map(part => Buffer.from(JSON.stringify(part)).toString('base64url')).join('.');
|
|
58
|
+
return `${unsigned}.${sign(null, Buffer.from(unsigned), key).toString('base64url')}`;
|
|
59
|
+
}
|
|
60
|
+
const runtimeEnvironmentKeys = ['SEQUENCE_ARTIFACT_RUNTIME_FILE', 'SEQUENCE_ARTIFACT_RUNTIME_CONFIG', 'SEQUENCE_ARTIFACT_RUNTIME_TOKEN'];
|
|
61
|
+
/**
|
|
62
|
+
* Runs `fn` with the hosted runtime configuration set to `config` (and no runtime file), then
|
|
63
|
+
* restores the previous environment. The override is process-wide: do not run overlapping calls.
|
|
64
|
+
* Refuses to run in a production build.
|
|
65
|
+
*/
|
|
66
|
+
export async function withTestRuntimeConfig(config, fn) {
|
|
67
|
+
if (process.env.NODE_ENV === 'production')
|
|
68
|
+
throw new Error('withTestRuntimeConfig is for tests and local development only');
|
|
69
|
+
const { sessionToken, ...hosted } = artifactRuntimeConfigSchema.parse(config);
|
|
70
|
+
const previous = Object.fromEntries(runtimeEnvironmentKeys.map(key => [key, process.env[key]]));
|
|
71
|
+
delete process.env.SEQUENCE_ARTIFACT_RUNTIME_FILE;
|
|
72
|
+
process.env.SEQUENCE_ARTIFACT_RUNTIME_CONFIG = JSON.stringify(hosted);
|
|
73
|
+
process.env.SEQUENCE_ARTIFACT_RUNTIME_TOKEN = sessionToken;
|
|
74
|
+
try {
|
|
75
|
+
return await fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
for (const key of runtimeEnvironmentKeys) {
|
|
79
|
+
const value = previous[key];
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
delete process.env[key];
|
|
82
|
+
else
|
|
83
|
+
process.env[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
export { createArtifactServerClient
|
|
2
|
+
export { createArtifactServerClient } from './server/client.js';
|
|
3
|
+
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
3
4
|
export type { ArtifactAtlasRefusal } from './server/errors.js';
|
|
4
|
-
export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
|
|
5
|
+
export type { ArtifactAuditEvent, ArtifactOrmOperations, ArtifactOrmOptions, ArtifactProcessAdvance, ArtifactProcessAdvanceResult, ArtifactProcessArtifactRef, ArtifactProcessAssign, ArtifactProcessAssignResult, ArtifactProcessDefinition, ArtifactProcessDescribeQuery, ArtifactProcessEdge, ArtifactProcessHumanNode, ArtifactProcessNode, ArtifactProcessNodeBase, ArtifactProcessNodeState, ArtifactProcessPrincipal, ArtifactProcessPrincipalSearch, ArtifactProcessPrincipals, ArtifactProcessRun, ArtifactProcessRunRecord, ArtifactProcessRunSnapshot, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
|
|
5
6
|
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
6
7
|
export type { ArtifactAuditAction, ArtifactProcessPrincipalType, ArtifactProcessRunStatus, ArtifactProcessTaskView, } from './server/broker-contract.js';
|
|
7
8
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
8
9
|
export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
|
|
9
|
-
export { getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
10
|
-
export type { ArtifactRequestContext } from './server/request-context.js';
|
|
10
|
+
export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
11
|
+
export type { ArtifactContextErrorReason, ArtifactRequestContext, RequestContextV2External } from './server/request-context.js';
|
|
12
|
+
export { verifyArtifactExternalContext } from './server/external-context.js';
|
|
13
|
+
export type { ArtifactExternalAccept } from './server/external-context.js';
|
|
14
|
+
export { externalEvidence } from './server/principal.js';
|
|
15
|
+
export type { ArtifactExternalEvidence, ArtifactExternalPrincipal, ArtifactPrincipal } from './server/principal.js';
|
|
11
16
|
export type { ArtifactLogMetadata, ArtifactLogRecord } from './server/logger.js';
|
|
12
17
|
export type { ArtifactOrmJsonScalars } from './orm-json-scalars.js';
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
|
-
export { createArtifactServerClient
|
|
2
|
+
export { createArtifactServerClient } from './server/client.js';
|
|
3
|
+
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
3
4
|
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
4
5
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
5
|
-
export { getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
6
|
+
export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
7
|
+
export { verifyArtifactExternalContext } from './server/external-context.js';
|
|
8
|
+
export { externalEvidence } from './server/principal.js';
|
|
@@ -97,6 +97,11 @@ only runs that user may view, `getRun` needs Viewer on the run and returns
|
|
|
97
97
|
every viewer — keep the data your pages need in your own ORM namespace. Reads
|
|
98
98
|
work from pages and GET route handlers; starting a run does not.
|
|
99
99
|
|
|
100
|
+
To follow a run as it moves, poll `processes.listEvents({ runId, since, limit })`
|
|
101
|
+
with the `event_cursor` from `getRun`, then with the `cursor` each page returns:
|
|
102
|
+
every event arrives once, oldest first, in the shape the run's stream sends. It
|
|
103
|
+
needs Viewer on the run, like `getRun`.
|
|
104
|
+
|
|
100
105
|
The same `processes.read` declaration lets you render the process itself.
|
|
101
106
|
`processes.describe({ process, version })` returns the applied template a run
|
|
102
107
|
is filled in from — `{ id, name, version, new_runs_disabled, start_node_id,
|
|
@@ -128,6 +133,10 @@ groups })` reassigns a parked step (its advancers become exactly those
|
|
|
128
133
|
principals) and `processes.grantViewers` / `processes.revokeViewers({ runId,
|
|
129
134
|
users, groups })` share a run or take a share back; all three need the user to
|
|
130
135
|
own the run, and `processes.listViewers(runId)` shows the run's stored shares.
|
|
136
|
+
`processes.cancel(runId)` cancels a run, and `processes.resetStep({ runId,
|
|
137
|
+
stepId })` stops a generating agent step without cancelling its run (the run
|
|
138
|
+
re-parks at the node's reset edge); both need the user's `invoker` relation on
|
|
139
|
+
the run, as the Lattice routes do.
|
|
131
140
|
Users are Auth0 subs and groups are chat-group ids — take them from
|
|
132
141
|
`processes.searchPrincipals({ q, types, limit })`, which finds people in the
|
|
133
142
|
user's own tenant and groups they can see, as the Lattice picker does. The
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"dev": "next dev --webpack --hostname 127.0.0.1",
|
|
11
11
|
"build": "next build",
|
|
12
12
|
"start": "next start --hostname 127.0.0.1",
|
|
13
|
-
"
|
|
13
|
+
"lint": "next typegen && tsc --noEmit --noUnusedLocals --noUnusedParameters --noFallthroughCasesInSwitch",
|
|
14
|
+
"type-check": "next typegen && tsc --noEmit",
|
|
15
|
+
"test": "node --test tests/*.test.mjs"
|
|
14
16
|
},
|
|
15
17
|
"dependencies": {
|
|
16
18
|
"@sequenceholdings/artifact-studio": "{{artifactStudioVersion}}",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { greet } from '../app/actions.ts'
|
|
4
|
+
import { GET } from '../app/api/health/route.ts'
|
|
5
|
+
|
|
6
|
+
test('greeting validates input and returns the server action result', async () => {
|
|
7
|
+
const data = new FormData()
|
|
8
|
+
data.set('name', ' Atlas ')
|
|
9
|
+
assert.deepEqual(await greet({ message: '', error: null }, data), {
|
|
10
|
+
message: 'Hello, Atlas. Your server action ran successfully.', error: null,
|
|
11
|
+
})
|
|
12
|
+
for (const name of ['', ' '.repeat(3), 'a'.repeat(81)]) {
|
|
13
|
+
data.set('name', name)
|
|
14
|
+
const result = await greet({ message: '', error: null }, data)
|
|
15
|
+
assert.equal(result.message, '')
|
|
16
|
+
assert.equal(result.error, 'Enter a name between 1 and 80 characters.')
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('health route returns uncached JSON without any runtime bindings', async () => {
|
|
21
|
+
const response = GET()
|
|
22
|
+
assert.equal(response.status, 200)
|
|
23
|
+
assert.equal(response.headers.get('cache-control'), 'no-store')
|
|
24
|
+
assert.deepEqual(await response.json(), { ok: true, framework: 'nextjs' })
|
|
25
|
+
})
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@sequenceholdings/artifact-studio": "^0.
|
|
12
|
+
"@sequenceholdings/artifact-studio": "^0.6.0",
|
|
13
13
|
"@sequenceholdings/atlas-ui": "^3.1.1",
|
|
14
14
|
"@tailwindcss/postcss": "^4.2.2",
|
|
15
15
|
"@tailwindcss/vite": "^4.2.2",
|
package/dist/tunnel/protocol.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { isLoopbackHostname } from '../loopback.js';
|
|
4
|
+
import { isPrivateRequestHeader } from './reserved-headers.js';
|
|
4
5
|
/** Version 1 is negotiated by the tunnel connection, not by individual streams. */
|
|
5
6
|
export const TUNNEL_PROTOCOL = 'artifact-tunnel.v1';
|
|
6
7
|
export const DEFAULT_TUNNEL_LIMITS = Object.freeze({
|
|
@@ -143,10 +144,7 @@ function removePrivateHeaders(headers) {
|
|
|
143
144
|
const nominated = new Set((Array.isArray(connection) ? connection.join(',') : connection ?? '')
|
|
144
145
|
.split(',').map((name) => name.trim().toLowerCase()));
|
|
145
146
|
for (const name of headers.keys()) {
|
|
146
|
-
if (HOP_HEADERS.has(name) || nominated.has(name) || name
|
|
147
|
-
name === 'authorization' || name === 'remote-user' || name === 'x-remote-user' ||
|
|
148
|
-
name === 'x-serverless-authorization' || name === 'x-goog-iap-jwt-assertion' ||
|
|
149
|
-
/^(?:x-forwarded-|x-sequence-|x-artifact-|x-atlas-|x-user-|x-auth-|x-authenticated-|x-goog-authenticated-|proxy-)/.test(name))
|
|
147
|
+
if (HOP_HEADERS.has(name) || nominated.has(name) || isPrivateRequestHeader(name))
|
|
150
148
|
headers.delete(name);
|
|
151
149
|
}
|
|
152
150
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Lowercase header name the proxy strips from every request it forwards to an app. */
|
|
2
|
+
export declare function isPrivateRequestHeader(name: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Client-address headers an external ingress drops: on an ingress the client address comes only
|
|
5
|
+
* from the edge-verified `CF-Connecting-IP`, never from a header the caller can set.
|
|
6
|
+
*/
|
|
7
|
+
export declare const INGRESS_CLIENT_ADDRESS_HEADERS: readonly string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request headers the platform owns. The proxy strips them before a request reaches app code
|
|
3
|
+
* (`removePrivateHeaders` in `protocol.ts`), so a manifest may never declare one as an app header.
|
|
4
|
+
* Dependency-free: browser code that parses manifests imports it.
|
|
5
|
+
*/
|
|
6
|
+
const PRIVATE_REQUEST_HEADER_NAMES = new Set([
|
|
7
|
+
'forwarded', 'authorization', 'remote-user', 'x-remote-user', 'x-serverless-authorization', 'x-goog-iap-jwt-assertion',
|
|
8
|
+
]);
|
|
9
|
+
const PRIVATE_REQUEST_HEADER_PREFIX = /^(?:x-forwarded-|x-sequence-|x-artifact-|x-atlas-|x-user-|x-auth-|x-authenticated-|x-goog-authenticated-|proxy-)/;
|
|
10
|
+
/** Lowercase header name the proxy strips from every request it forwards to an app. */
|
|
11
|
+
export function isPrivateRequestHeader(name) {
|
|
12
|
+
return PRIVATE_REQUEST_HEADER_NAMES.has(name) || PRIVATE_REQUEST_HEADER_PREFIX.test(name);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Client-address headers an external ingress drops: on an ingress the client address comes only
|
|
16
|
+
* from the edge-verified `CF-Connecting-IP`, never from a header the caller can set.
|
|
17
|
+
*/
|
|
18
|
+
export const INGRESS_CLIENT_ADDRESS_HEADERS = Object.freeze([
|
|
19
|
+
'true-client-ip', 'x-real-ip', 'x-client-ip', 'client-ip', 'x-cluster-client-ip',
|
|
20
|
+
]);
|