@sequenceholdings/artifact-studio 0.7.0 → 0.8.0
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/dist/build.js +4 -2
- package/dist/gateway/bootstrap-page.js +10 -2
- package/dist/gateway/control.d.ts +49 -0
- package/dist/gateway/control.js +65 -3
- package/dist/gateway/release-admission.d.ts +5 -0
- package/dist/gateway/release-admission.js +19 -0
- package/dist/gateway/server.d.ts +3 -0
- package/dist/gateway/server.js +17 -24
- package/dist/gateway/session.d.ts +1 -0
- package/dist/gateway/session.js +5 -2
- package/dist/gateway/static-files.d.ts +66 -0
- package/dist/gateway/static-files.js +112 -0
- package/dist/manifest-next.d.ts +13 -0
- package/dist/manifest-next.js +5 -0
- package/dist/manifest-static.d.ts +8 -0
- package/dist/manifest-static.js +5 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/server/broker-contract.d.ts +12 -0
- package/dist/server/broker-contract.js +11 -0
- package/dist/server/client.d.ts +3 -0
- package/dist/server/client.js +2 -0
- package/dist/server/groups.d.ts +32 -0
- package/dist/server/groups.js +28 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/source-boundary.d.ts +6 -0
- package/dist/source-boundary.js +20 -7
- package/dist/templates/react-vite/package.json +5 -2
- package/dist/templates/react-vite/tsconfig.json +17 -0
- package/package.json +4 -4
- package/templates/react-vite/package.json +5 -2
- package/templates/react-vite/tsconfig.json +17 -0
package/dist/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, realpath, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { dirname, relative, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -122,7 +122,9 @@ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompat
|
|
|
122
122
|
? collectCandidateTokenCompatibilityDiagnostics({ files: source.files })
|
|
123
123
|
: []),
|
|
124
124
|
];
|
|
125
|
-
|
|
125
|
+
// Vite resolves imports to physical paths, including macOS /tmp -> /private/tmp.
|
|
126
|
+
// Give Vite and the source boundary the same physical root.
|
|
127
|
+
const root = await realpath(source.root);
|
|
126
128
|
const transformedCssModules = new Map();
|
|
127
129
|
const result = await viteBuild({
|
|
128
130
|
root,
|
|
@@ -21,6 +21,14 @@ export function bootstrapLandingUrl(route, origin) {
|
|
|
21
21
|
return root;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Transparent while it connects, so the frame keeps the host's own background through the exchange
|
|
26
|
+
* and the redirect (a white page reading "Connecting" flashed between Atlas and the app). Only a
|
|
27
|
+
* failure is shown, on its own light card so it reads over either host theme.
|
|
28
|
+
*/
|
|
29
|
+
const BOOTSTRAP_STYLE = 'html,body{margin:0;height:100%;background:transparent}'
|
|
30
|
+
+ 'body{display:flex;align-items:center;justify-content:center;font:14px/1.4 system-ui,sans-serif}'
|
|
31
|
+
+ '#status:empty{display:none}#status{margin:16px;padding:12px 16px;border-radius:8px;background:#fff;color:#27272a;box-shadow:0 1px 3px rgba(0,0,0,.2)}';
|
|
24
32
|
/** The bootstrap ticket travels in a fragment and a POST body, never a request URL or log. The page
|
|
25
33
|
* exchanges it only inside the Atlas frame; browsers without Fetch Metadata are covered by the script check. */
|
|
26
34
|
export function bootstrapPage({ response, parentOrigin }) {
|
|
@@ -28,7 +36,7 @@ export function bootstrapPage({ response, parentOrigin }) {
|
|
|
28
36
|
response.writeHead(200, {
|
|
29
37
|
'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer',
|
|
30
38
|
'x-content-type-options': 'nosniff',
|
|
31
|
-
'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; frame-ancestors ${parentOrigin}; base-uri 'none'; form-action 'none'`,
|
|
39
|
+
'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; frame-ancestors ${parentOrigin}; base-uri 'none'; form-action 'none'`,
|
|
32
40
|
});
|
|
33
|
-
response.end(`<!doctype html><meta charset="utf-8"><title>
|
|
41
|
+
response.end(`<!doctype html><meta charset="utf-8"><title>Opening application</title><style nonce="${nonce}">${BOOTSTRAP_STYLE}</style><p id="status" role="status"></p><script nonce="${nonce}">(() => {const landing=${bootstrapLandingUrl.toString()};const fragment=new URLSearchParams(location.hash.slice(1));const ticket=fragment.get('ticket');history.replaceState(null,'',location.pathname);if(!ticket||window.top===window){document.getElementById('status').textContent='Open this application from Atlas.';return;}fetch('/_sequence/session',{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json'},body:JSON.stringify({ticket})}).then(r=>{if(!r.ok)throw new Error();location.replace(landing(fragment.get('route'),location.origin));}).catch(()=>{document.getElementById('status').textContent='This preview expired. Open it again from Atlas.';});})();</script>`);
|
|
34
42
|
}
|
|
@@ -67,6 +67,36 @@ export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
67
67
|
}, z.core.$strip>;
|
|
68
68
|
}, z.core.$strip>;
|
|
69
69
|
}, z.core.$strip>>;
|
|
70
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
71
|
+
kind: z.ZodLiteral<"static">;
|
|
72
|
+
deploymentId: z.ZodString;
|
|
73
|
+
contentHash: z.ZodString;
|
|
74
|
+
active: z.ZodBoolean;
|
|
75
|
+
published: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
projectId: z.ZodString;
|
|
77
|
+
ownerId: z.ZodString;
|
|
78
|
+
environment: z.ZodString;
|
|
79
|
+
publicOrigin: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
80
|
+
expiresAt: z.ZodNumber;
|
|
81
|
+
maxExpiresAt: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
83
|
+
serviceAccountId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
84
|
+
lattice: z.ZodOptional<z.ZodObject<{
|
|
85
|
+
runId: z.ZodNullable<z.ZodString>;
|
|
86
|
+
nodeId: z.ZodNullable<z.ZodString>;
|
|
87
|
+
stepId: z.ZodNullable<z.ZodString>;
|
|
88
|
+
processId: z.ZodNullable<z.ZodString>;
|
|
89
|
+
}, z.core.$strict>>;
|
|
90
|
+
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
91
|
+
manifest: z.ZodOptional<z.ZodObject<{
|
|
92
|
+
server: z.ZodObject<{
|
|
93
|
+
resources: z.ZodObject<{
|
|
94
|
+
concurrency: z.ZodNumber;
|
|
95
|
+
max_instances: z.ZodNumber;
|
|
96
|
+
timeout_seconds: z.ZodNumber;
|
|
97
|
+
}, z.core.$strip>;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
}, z.core.$strip>>;
|
|
70
100
|
}, z.core.$strip>], "kind">;
|
|
71
101
|
export type GatewayTarget = z.infer<typeof gatewayTargetSchema>;
|
|
72
102
|
declare const latticeInvokeGrantSchema: z.ZodObject<{
|
|
@@ -91,6 +121,15 @@ declare const latticeInvokeGrantSchema: z.ZodObject<{
|
|
|
91
121
|
}, z.core.$strict>;
|
|
92
122
|
}, z.core.$strict>;
|
|
93
123
|
export type LatticeInvokeGrant = z.infer<typeof latticeInvokeGrantSchema>;
|
|
124
|
+
/** Paths a static deployment serves: its page, entry bundle, source map and lazy chunks. */
|
|
125
|
+
export declare const STATIC_PAGE = "index.html";
|
|
126
|
+
/** The static bundle ceiling (30 MiB) plus headroom; a larger file is a platform fault, never served. */
|
|
127
|
+
export declare const STATIC_FILE_MAX_BYTES: number;
|
|
128
|
+
export interface StaticArtifactFile {
|
|
129
|
+
body: Buffer;
|
|
130
|
+
contentType: string;
|
|
131
|
+
etag?: string;
|
|
132
|
+
}
|
|
94
133
|
export declare class GatewayControlError extends Error {
|
|
95
134
|
readonly status: number;
|
|
96
135
|
constructor(status: number);
|
|
@@ -156,6 +195,16 @@ export declare class ArtifactGatewayControl {
|
|
|
156
195
|
target: GatewayTarget;
|
|
157
196
|
}>;
|
|
158
197
|
authorize(session: GatewaySession): Promise<GatewayTarget>;
|
|
198
|
+
/**
|
|
199
|
+
* One immutable file of a static deployment (the page, entry bundle, source map or a chunk), raw
|
|
200
|
+
* bytes up to the static bundle ceiling. The gateway authorized the browser's session first; this
|
|
201
|
+
* call carries only the gateway's own credential.
|
|
202
|
+
*/
|
|
203
|
+
staticFile({ deploymentId, path, parentOrigin }: {
|
|
204
|
+
deploymentId: string;
|
|
205
|
+
path: string;
|
|
206
|
+
parentOrigin: string;
|
|
207
|
+
}): Promise<StaticArtifactFile>;
|
|
159
208
|
/** SHA-256 both sides so a wrong bearer still takes constant time. */
|
|
160
209
|
tokenMatches(presented: string): boolean;
|
|
161
210
|
latticeInvoke(body: {
|
package/dist/gateway/control.js
CHANGED
|
@@ -20,6 +20,8 @@ const base = {
|
|
|
20
20
|
export const gatewayTargetSchema = z.discriminatedUnion('kind', [
|
|
21
21
|
z.object({ ...base, kind: z.literal('development'), sessionId: id, epoch: z.number().int().positive() }),
|
|
22
22
|
z.object({ ...base, kind: z.literal('hosted'), deploymentId: id, frameworkBuildId: artifactNextFrameworkBuildIdSchema, upstreamUrl: z.string().url(), upstreamAudience: z.string().url().optional(), runtimeTransport: z.enum(['cloud-run', 'local-docker']).optional(), active: z.boolean(), published: z.boolean().optional() }),
|
|
23
|
+
// A static (React/Vite) deployment: the gateway serves its immutable files itself, fetched from Atlas.
|
|
24
|
+
z.object({ ...base, kind: z.literal('static'), deploymentId: id, contentHash: z.string().min(1).max(128), active: z.boolean(), published: z.boolean().optional() }),
|
|
23
25
|
]);
|
|
24
26
|
const bootstrapSchema = z.object({ parentOrigin: z.string().transform(normalizePublicOrigin), channelId: id, nonce: z.string().min(16).max(255) });
|
|
25
27
|
const usageResultSchema = z.object({ rejected: z.number().int().nonnegative() });
|
|
@@ -41,6 +43,39 @@ const latticeInvokeGrantSchema = z.object({
|
|
|
41
43
|
timeout_seconds: z.number().int().min(1).max(300),
|
|
42
44
|
}).strict(),
|
|
43
45
|
}).strict();
|
|
46
|
+
/** Paths a static deployment serves: its page, entry bundle, source map and lazy chunks. */
|
|
47
|
+
export const STATIC_PAGE = 'index.html';
|
|
48
|
+
const STATIC_FILE_PATH = /^(?:index\.html|bundle\.js|sourcemap\.json|chunks\/[A-Za-z0-9._-]{1,200})$/;
|
|
49
|
+
/** The static bundle ceiling (30 MiB) plus headroom; a larger file is a platform fault, never served. */
|
|
50
|
+
export const STATIC_FILE_MAX_BYTES = 32 * 1024 * 1024;
|
|
51
|
+
const STATIC_CONTENT_TYPES = new Set(['text/html', 'application/javascript', 'text/javascript', 'application/json', 'text/css']);
|
|
52
|
+
/** Reads at most `maxBytes` of a response body, refusing (502) anything larger. */
|
|
53
|
+
async function readBounded(response, maxBytes) {
|
|
54
|
+
if (Number(response.headers.get('content-length')) > maxBytes) {
|
|
55
|
+
await response.body?.cancel().catch(() => undefined);
|
|
56
|
+
throw new GatewayControlError(502);
|
|
57
|
+
}
|
|
58
|
+
const reader = response.body?.getReader();
|
|
59
|
+
if (!reader)
|
|
60
|
+
throw new GatewayControlError(502);
|
|
61
|
+
const chunks = [];
|
|
62
|
+
let length = 0;
|
|
63
|
+
try {
|
|
64
|
+
for (;;) {
|
|
65
|
+
const { done, value } = await reader.read();
|
|
66
|
+
if (done)
|
|
67
|
+
break;
|
|
68
|
+
length += value.length;
|
|
69
|
+
if (length > maxBytes)
|
|
70
|
+
throw new GatewayControlError(502);
|
|
71
|
+
chunks.push(value);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat(chunks);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await reader.cancel().catch(() => undefined);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
44
79
|
export class GatewayControlError extends Error {
|
|
45
80
|
status;
|
|
46
81
|
constructor(status) {
|
|
@@ -186,11 +221,11 @@ export class ArtifactGatewayControl {
|
|
|
186
221
|
version: 1, kind: target.kind, projectId: target.projectId, ownerId: target.ownerId,
|
|
187
222
|
environment: target.environment, publicOrigin: target.publicOrigin,
|
|
188
223
|
sessionId: target.kind === 'development' ? target.sessionId : null,
|
|
189
|
-
deploymentId: target.kind === '
|
|
224
|
+
deploymentId: target.kind === 'development' ? null : target.deploymentId,
|
|
190
225
|
frameworkBuildId: target.kind === 'hosted' ? target.frameworkBuildId : null,
|
|
191
226
|
expiresAt: Math.min(target.expiresAt, target.maxExpiresAt ?? target.expiresAt, Date.now() + 8 * 60 * 60_000),
|
|
192
227
|
maxExpiresAt: Math.min(target.maxExpiresAt ?? (target.kind === 'development' ? target.expiresAt : Date.now() + 8 * 60 * 60_000), Date.now() + 8 * 60 * 60_000),
|
|
193
|
-
published: target.kind
|
|
228
|
+
published: target.kind !== 'development' && (target.published ?? target.active), bridge,
|
|
194
229
|
...(target.lattice ? { lattice: target.lattice } : {}),
|
|
195
230
|
...(target.taskRoutes ? { taskRoutes: target.taskRoutes } : {}),
|
|
196
231
|
};
|
|
@@ -206,10 +241,37 @@ export class ArtifactGatewayControl {
|
|
|
206
241
|
const target = gatewayTargetSchema.parse(value);
|
|
207
242
|
if (target.kind !== session.kind || target.projectId !== session.projectId || target.ownerId !== session.ownerId ||
|
|
208
243
|
target.environment !== session.environment || target.publicOrigin !== session.publicOrigin || target.expiresAt <= Date.now() ||
|
|
209
|
-
(target.kind === 'development' ? target.sessionId !== session.sessionId
|
|
244
|
+
(target.kind === 'development' ? target.sessionId !== session.sessionId
|
|
245
|
+
: target.deploymentId !== session.deploymentId || (target.kind === 'hosted' && target.frameworkBuildId !== session.frameworkBuildId)))
|
|
210
246
|
throw new GatewayControlError(403);
|
|
211
247
|
return target;
|
|
212
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* One immutable file of a static deployment (the page, entry bundle, source map or a chunk), raw
|
|
251
|
+
* bytes up to the static bundle ceiling. The gateway authorized the browser's session first; this
|
|
252
|
+
* call carries only the gateway's own credential.
|
|
253
|
+
*/
|
|
254
|
+
async staticFile({ deploymentId, path, parentOrigin }) {
|
|
255
|
+
if (!STATIC_FILE_PATH.test(path) || !/^[A-Za-z0-9_-]{1,128}$/.test(deploymentId))
|
|
256
|
+
throw new GatewayControlError(404);
|
|
257
|
+
const signal = AbortSignal.timeout(15_000);
|
|
258
|
+
const credential = await this.credential(signal);
|
|
259
|
+
const query = path === STATIC_PAGE ? `?parentOrigin=${encodeURIComponent(parentOrigin)}` : '';
|
|
260
|
+
const response = await (this.options.fetch ?? fetch)(`${this.origin}${ARTIFACT_NEXT_CONTROL_PATH}/static/${deploymentId}/${path}${query}`, {
|
|
261
|
+
headers: { authorization: `Bearer ${credential}` }, signal, redirect: 'error',
|
|
262
|
+
});
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
await response.body?.cancel().catch(() => undefined);
|
|
265
|
+
throw new GatewayControlError(response.status);
|
|
266
|
+
}
|
|
267
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
268
|
+
if (!STATIC_CONTENT_TYPES.has(contentType.split(';', 1)[0].trim().toLowerCase())) {
|
|
269
|
+
await response.body?.cancel().catch(() => undefined);
|
|
270
|
+
throw new GatewayControlError(502);
|
|
271
|
+
}
|
|
272
|
+
const body = await readBounded(response, STATIC_FILE_MAX_BYTES);
|
|
273
|
+
return { body, contentType, etag: response.headers.get('etag') ?? undefined };
|
|
274
|
+
}
|
|
213
275
|
/** SHA-256 both sides so a wrong bearer still takes constant time. */
|
|
214
276
|
tokenMatches(presented) {
|
|
215
277
|
if (presented.length === 0 || presented.length > 1024)
|
|
@@ -20,4 +20,9 @@ export declare function admitHostedMutation({ request, expectedDeploymentId, exp
|
|
|
20
20
|
}): Promise<{
|
|
21
21
|
body?: AsyncIterable<Uint8Array>;
|
|
22
22
|
}>;
|
|
23
|
+
/** The release a request names: the deployment header, or the Next build id (header or `dpl` query), never conflicting. */
|
|
24
|
+
export declare function releaseHints(request: IncomingMessage): {
|
|
25
|
+
deploymentId?: string;
|
|
26
|
+
frameworkBuildId?: string;
|
|
27
|
+
};
|
|
23
28
|
export {};
|
|
@@ -257,3 +257,22 @@ export async function admitHostedMutation({ request, expectedDeploymentId, expec
|
|
|
257
257
|
}
|
|
258
258
|
return { body: continuation() };
|
|
259
259
|
}
|
|
260
|
+
/** The release a request names: the deployment header, or the Next build id (header or `dpl` query), never conflicting. */
|
|
261
|
+
export function releaseHints(request) {
|
|
262
|
+
function uniqueHeader(name) {
|
|
263
|
+
const value = request.headers[name];
|
|
264
|
+
if (value === undefined)
|
|
265
|
+
return undefined;
|
|
266
|
+
const count = request.rawHeaders.filter((_, index) => index % 2 === 0 && request.rawHeaders[index].toLowerCase() === name).length;
|
|
267
|
+
if (typeof value !== 'string' || count !== 1 || !artifactNextFrameworkBuildIdSchema.safeParse(value).success)
|
|
268
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
const deploymentId = uniqueHeader('x-sequence-artifact-release');
|
|
272
|
+
const frameworkHeader = uniqueHeader('x-deployment-id');
|
|
273
|
+
const query = new URL(request.url ?? '/', 'https://gateway.invalid').searchParams.getAll('dpl');
|
|
274
|
+
if (query.length > 1 || (query.length === 1 && !artifactNextFrameworkBuildIdSchema.safeParse(query[0]).success) ||
|
|
275
|
+
(frameworkHeader && query.length === 1 && frameworkHeader !== query[0]))
|
|
276
|
+
throw new ReleaseAdmissionError('conflicting-release');
|
|
277
|
+
return { deploymentId, frameworkBuildId: frameworkHeader ?? query[0] };
|
|
278
|
+
}
|
package/dist/gateway/server.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type GatewaySessionSameSite } from './session.js';
|
|
|
5
5
|
import { type ArtifactRequestAdmission } from './admission.js';
|
|
6
6
|
import { type GatewayControlAdmission } from './control-admission.js';
|
|
7
7
|
import { type ArtifactGatewayIngressOptions } from './ingress.js';
|
|
8
|
+
import { StaticArtifactFiles } from './static-files.js';
|
|
8
9
|
export interface ArtifactGatewayOptions {
|
|
9
10
|
control: ArtifactGatewayControl;
|
|
10
11
|
runtimeDomain: string;
|
|
@@ -28,6 +29,8 @@ export interface ArtifactGatewayOptions {
|
|
|
28
29
|
controlAdmission?: Pick<GatewayControlAdmission, 'admit'>;
|
|
29
30
|
/** External ingress front door (design §(e)). Absent: every `ingress-*` host answers 404. */
|
|
30
31
|
ingress?: ArtifactGatewayIngressOptions;
|
|
32
|
+
/** Static deployments' files (default: fetched from Atlas, cached by deployment within the default byte budget). */
|
|
33
|
+
staticFiles?: Pick<StaticArtifactFiles, 'get'>;
|
|
31
34
|
/** Reuse of a positive Atlas authorization (default GATEWAY_AUTHORIZATION_TTL_MS; 0 disables). */
|
|
32
35
|
authorizationCacheTtlMs?: number;
|
|
33
36
|
onError?: (event: {
|
package/dist/gateway/server.js
CHANGED
|
@@ -8,15 +8,15 @@ import { decodeGatewaySession, gatewaySessionCookies, renewGatewaySession, signG
|
|
|
8
8
|
import { proxyHostedArtifact } from './hosted-proxy.js';
|
|
9
9
|
import { bootstrapPage } from './bootstrap-page.js';
|
|
10
10
|
import { routeSequenceRequest } from './lattice-invoke.js';
|
|
11
|
-
import { admitHostedMutation, ReleaseAdmissionError } from './release-admission.js';
|
|
11
|
+
import { admitHostedMutation, ReleaseAdmissionError, releaseHints } from './release-admission.js';
|
|
12
12
|
import { ArtifactAdmissionError } from './admission.js';
|
|
13
13
|
import { gatewayClientAddress } from './control-admission.js';
|
|
14
14
|
import { gatewayAuthorizationKey, PositiveDecisionCache, tunnelAuthorizationKey } from './authorization-cache.js';
|
|
15
15
|
import { gatewayElapsedMs, GatewayUsageReporter, snapshotGatewayTimings } from './usage.js';
|
|
16
|
-
import { artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
|
|
17
16
|
import { createDevelopmentIngressProxy } from './ingress-development.js';
|
|
18
17
|
import { createIngressHandler, handleClientErrors } from './ingress-handler.js';
|
|
19
18
|
import { resemblesIngressHost } from './ingress.js';
|
|
19
|
+
import { serveStaticTarget, StaticArtifactFiles } from './static-files.js';
|
|
20
20
|
function json(response, status, error) {
|
|
21
21
|
if (response.headersSent) {
|
|
22
22
|
response.destroy();
|
|
@@ -42,24 +42,6 @@ function header(request, key) {
|
|
|
42
42
|
function isServiceWorkerScript(request) {
|
|
43
43
|
return request.headers['service-worker'] !== undefined || request.headers['sec-fetch-dest'] === 'serviceworker';
|
|
44
44
|
}
|
|
45
|
-
function releaseHints(request) {
|
|
46
|
-
function uniqueHeader(name) {
|
|
47
|
-
const value = request.headers[name];
|
|
48
|
-
if (value === undefined)
|
|
49
|
-
return undefined;
|
|
50
|
-
const count = request.rawHeaders.filter((_, index) => index % 2 === 0 && request.rawHeaders[index].toLowerCase() === name).length;
|
|
51
|
-
if (typeof value !== 'string' || count !== 1 || !artifactNextFrameworkBuildIdSchema.safeParse(value).success)
|
|
52
|
-
throw new ReleaseAdmissionError('conflicting-release');
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
const deploymentId = uniqueHeader('x-sequence-artifact-release');
|
|
56
|
-
const frameworkHeader = uniqueHeader('x-deployment-id');
|
|
57
|
-
const query = new URL(request.url ?? '/', 'https://gateway.invalid').searchParams.getAll('dpl');
|
|
58
|
-
if (query.length > 1 || (query.length === 1 && !artifactNextFrameworkBuildIdSchema.safeParse(query[0]).success) ||
|
|
59
|
-
(frameworkHeader && query.length === 1 && frameworkHeader !== query[0]))
|
|
60
|
-
throw new ReleaseAdmissionError('conflicting-release');
|
|
61
|
-
return { deploymentId, frameworkBuildId: frameworkHeader ?? query[0] };
|
|
62
|
-
}
|
|
63
45
|
async function boundedJson(request) {
|
|
64
46
|
if (header(request, 'content-type')?.split(';', 1)[0] !== 'application/json')
|
|
65
47
|
throw new GatewayControlError(400);
|
|
@@ -74,6 +56,12 @@ async function boundedJson(request) {
|
|
|
74
56
|
}
|
|
75
57
|
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
76
58
|
}
|
|
59
|
+
/** Active use renews the session cookie within its admission bound. */
|
|
60
|
+
function renewCookie({ options, request, response, session, target }) {
|
|
61
|
+
const renewed = renewGatewaySession({ session, authorizedExpiresAt: target.expiresAt });
|
|
62
|
+
if (renewed)
|
|
63
|
+
response.setHeader('set-cookie', gatewaySessionCookies({ session: renewed, key: options.sessionKey, cookie: header(request, 'cookie'), renewal: true, sameSite: options.sessionCookieSameSite }));
|
|
64
|
+
}
|
|
77
65
|
/** Probes reach the container by its own address; an application host keeps its own /health route. */
|
|
78
66
|
function isReadinessRequest({ request, gatewayOrigin, runtimeDomain }) {
|
|
79
67
|
if (request.method !== 'GET')
|
|
@@ -109,6 +97,7 @@ export function createArtifactGateway(options) {
|
|
|
109
97
|
const usage = new GatewayUsageReporter({ send: events => options.control.usage(events), onFailure: failure => options.onError?.(failure) });
|
|
110
98
|
let draining = false;
|
|
111
99
|
const isIngressRequest = (request) => resemblesIngressHost({ rawHost: header(request, 'host'), runtimeDomain: options.runtimeDomain });
|
|
100
|
+
const staticFiles = options.staticFiles ?? new StaticArtifactFiles({ fetch: ({ deploymentId, path }) => options.control.staticFile({ deploymentId, path, parentOrigin }) });
|
|
112
101
|
/** Admission for the unauthenticated endpoints; the release frees the in-flight permit. */
|
|
113
102
|
async function admitControlCall(endpoint, request) {
|
|
114
103
|
if (!options.controlAdmission)
|
|
@@ -183,7 +172,8 @@ export function createArtifactGateway(options) {
|
|
|
183
172
|
? authorizations.getObserved(lookup) : { promise: authorizations.refresh(lookup), source: 'refresh' };
|
|
184
173
|
const target = await decision.promise;
|
|
185
174
|
const requestId = randomUUID();
|
|
186
|
-
|
|
175
|
+
// A static deployment has no server of its own to read a request context.
|
|
176
|
+
const result = { session, target, requestId, authorizationCache: decision.source, requestContext: target.kind === 'static' ? '' : signGatewayContext({
|
|
187
177
|
session: { ...session, lattice: target.lattice, taskRoutes: target.taskRoutes }, method, path: request.url ?? '/', privateKey: options.contextPrivateKey, keyId: options.contextKeyId,
|
|
188
178
|
capabilities: target.capabilities, serviceAccountId: target.serviceAccountId, requestId,
|
|
189
179
|
}) };
|
|
@@ -305,6 +295,11 @@ export function createArtifactGateway(options) {
|
|
|
305
295
|
}
|
|
306
296
|
const authorizationStarted = performance.now();
|
|
307
297
|
const { session, target, requestContext, requestId, authorizationCache } = await authorize(request);
|
|
298
|
+
// Immutable files Atlas already authorized this session for: no admission, context or usage.
|
|
299
|
+
if (target.kind === 'static') {
|
|
300
|
+
await serveStaticTarget({ request, response, target, files: staticFiles, renew: () => renewCookie({ options, request, response, session, target }), parentOrigin: session.bridge.parentOrigin });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
308
303
|
const gatewayTimings = { authorizationMs: gatewayElapsedMs(authorizationStarted), authorizationCache };
|
|
309
304
|
let reported = false;
|
|
310
305
|
const report = () => {
|
|
@@ -327,9 +322,7 @@ export function createArtifactGateway(options) {
|
|
|
327
322
|
report();
|
|
328
323
|
return;
|
|
329
324
|
}
|
|
330
|
-
|
|
331
|
-
if (renewed)
|
|
332
|
-
response.setHeader('set-cookie', gatewaySessionCookies({ session: renewed, key: options.sessionKey, cookie: header(request, 'cookie'), renewal: true, sameSite: options.sessionCookieSameSite }));
|
|
325
|
+
renewCookie({ options, request, response, session, target });
|
|
333
326
|
// Authorization precedes every response, including Next static assets and cached pages.
|
|
334
327
|
response.setHeader('content-security-policy', `frame-ancestors ${session.bridge.parentOrigin}`);
|
|
335
328
|
response.setHeader('referrer-policy', 'same-origin');
|
package/dist/gateway/session.js
CHANGED
|
@@ -17,7 +17,7 @@ catch {
|
|
|
17
17
|
} });
|
|
18
18
|
const id = z.string().min(1).max(255);
|
|
19
19
|
const sessionSchema = z.object({
|
|
20
|
-
version: z.literal(1), kind: z.enum(['development', 'hosted']),
|
|
20
|
+
version: z.literal(1), kind: z.enum(['development', 'hosted', 'static']),
|
|
21
21
|
sessionId: id.nullable(), deploymentId: id.nullable(),
|
|
22
22
|
frameworkBuildId: artifactNextFrameworkBuildIdSchema.nullable(), projectId: id, ownerId: id,
|
|
23
23
|
environment: id, publicOrigin: origin, expiresAt: z.number().int().positive(), maxExpiresAt: z.number().int().positive().optional(), published: z.boolean(),
|
|
@@ -26,7 +26,10 @@ const sessionSchema = z.object({
|
|
|
26
26
|
taskRoutes: artifactRequestContextSchema.shape.taskRoutes,
|
|
27
27
|
}).strict().refine(value => value.kind === 'development'
|
|
28
28
|
? value.sessionId !== null && value.deploymentId === null && value.frameworkBuildId === null && !value.published
|
|
29
|
-
|
|
29
|
+
// A static deployment has no Next build: its assets are named by the deployment itself.
|
|
30
|
+
: value.kind === 'static'
|
|
31
|
+
? value.sessionId === null && value.deploymentId !== null && value.frameworkBuildId === null
|
|
32
|
+
: value.sessionId === null && value.deploymentId !== null && value.frameworkBuildId !== null);
|
|
30
33
|
function sameSessionScope(left, right) {
|
|
31
34
|
return left.ownerId === right.ownerId && left.projectId === right.projectId &&
|
|
32
35
|
left.environment === right.environment && left.publicOrigin === right.publicOrigin &&
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { type GatewayTarget, type StaticArtifactFile } from './control.js';
|
|
3
|
+
/** Default byte budget for cached static files on one gateway replica (the gateway runs with 1 GiB). */
|
|
4
|
+
export declare const STATIC_CACHE_DEFAULT_BYTES: number;
|
|
5
|
+
/**
|
|
6
|
+
* Files of static deployments, cached by deployment and path. A deployment's files never change,
|
|
7
|
+
* so an entry is valid until evicted (least recently used, bounded by bytes). Concurrent misses for
|
|
8
|
+
* one file share a single fetch. The page is never cached: Atlas renders it, and its bridge script
|
|
9
|
+
* follows Atlas releases.
|
|
10
|
+
*/
|
|
11
|
+
export declare class StaticArtifactFiles {
|
|
12
|
+
private readonly options;
|
|
13
|
+
private readonly entries;
|
|
14
|
+
private readonly inflight;
|
|
15
|
+
private bytes;
|
|
16
|
+
constructor(options: {
|
|
17
|
+
fetch: (input: {
|
|
18
|
+
deploymentId: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}) => Promise<StaticArtifactFile>;
|
|
21
|
+
maxBytes?: number;
|
|
22
|
+
});
|
|
23
|
+
get cachedBytes(): number;
|
|
24
|
+
get({ deploymentId, path }: {
|
|
25
|
+
deploymentId: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}): Promise<StaticArtifactFile & {
|
|
28
|
+
etag: string;
|
|
29
|
+
}>;
|
|
30
|
+
private remember;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The policy a static application runs under: the sandbox's former `<meta>` policy, now a header
|
|
34
|
+
* on the application's own origin (so `'self'` is the application, never Atlas), plus what a meta
|
|
35
|
+
* tag cannot express: `frame-ancestors` keeps the page inside Atlas.
|
|
36
|
+
*/
|
|
37
|
+
export declare function staticContentSecurityPolicy(parentOrigin: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Where a static request is served from: `/_artifact/<deployment>/<file>` for assets, the page for
|
|
40
|
+
* every other path (in-app routes live in the hash or the page's own history). Null: not this
|
|
41
|
+
* deployment's file.
|
|
42
|
+
*/
|
|
43
|
+
export declare function staticFileForRequest({ url, deploymentId }: {
|
|
44
|
+
url: string;
|
|
45
|
+
deploymentId: string;
|
|
46
|
+
}): string | null;
|
|
47
|
+
/** Writes one authorized static file: GET or HEAD, revalidated on every use (each request is authorized). */
|
|
48
|
+
export declare function sendStaticFile({ request, response, file, parentOrigin }: {
|
|
49
|
+
request: IncomingMessage;
|
|
50
|
+
response: ServerResponse;
|
|
51
|
+
file: StaticArtifactFile & {
|
|
52
|
+
etag: string;
|
|
53
|
+
};
|
|
54
|
+
parentOrigin: string;
|
|
55
|
+
}): void;
|
|
56
|
+
/** Answers one request for an authorized static target: reads only, and only this deployment's files. */
|
|
57
|
+
export declare function serveStaticTarget({ request, response, target, files, renew, parentOrigin }: {
|
|
58
|
+
request: IncomingMessage;
|
|
59
|
+
response: ServerResponse;
|
|
60
|
+
target: Extract<GatewayTarget, {
|
|
61
|
+
kind: 'static';
|
|
62
|
+
}>;
|
|
63
|
+
files: Pick<StaticArtifactFiles, 'get'>;
|
|
64
|
+
renew: () => void;
|
|
65
|
+
parentOrigin: string;
|
|
66
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { GatewayControlError, STATIC_PAGE } from './control.js';
|
|
3
|
+
/** Default byte budget for cached static files on one gateway replica (the gateway runs with 1 GiB). */
|
|
4
|
+
export const STATIC_CACHE_DEFAULT_BYTES = 192 * 1024 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Files of static deployments, cached by deployment and path. A deployment's files never change,
|
|
7
|
+
* so an entry is valid until evicted (least recently used, bounded by bytes). Concurrent misses for
|
|
8
|
+
* one file share a single fetch. The page is never cached: Atlas renders it, and its bridge script
|
|
9
|
+
* follows Atlas releases.
|
|
10
|
+
*/
|
|
11
|
+
export class StaticArtifactFiles {
|
|
12
|
+
options;
|
|
13
|
+
entries = new Map();
|
|
14
|
+
inflight = new Map();
|
|
15
|
+
bytes = 0;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
get cachedBytes() { return this.bytes; }
|
|
20
|
+
async get({ deploymentId, path }) {
|
|
21
|
+
if (path === STATIC_PAGE)
|
|
22
|
+
return withEtag(await this.options.fetch({ deploymentId, path }));
|
|
23
|
+
const key = `${deploymentId}/${path}`;
|
|
24
|
+
const hit = this.entries.get(key);
|
|
25
|
+
if (hit) {
|
|
26
|
+
// Refresh recency: a Map iterates in insertion order, so re-insert at the end.
|
|
27
|
+
this.entries.delete(key);
|
|
28
|
+
this.entries.set(key, hit);
|
|
29
|
+
return hit;
|
|
30
|
+
}
|
|
31
|
+
const pending = this.inflight.get(key);
|
|
32
|
+
if (pending)
|
|
33
|
+
return pending;
|
|
34
|
+
const load = this.options.fetch({ deploymentId, path }).then(file => {
|
|
35
|
+
const entry = withEtag(file);
|
|
36
|
+
this.remember(key, entry);
|
|
37
|
+
return entry;
|
|
38
|
+
}).finally(() => { this.inflight.delete(key); });
|
|
39
|
+
this.inflight.set(key, load);
|
|
40
|
+
return load;
|
|
41
|
+
}
|
|
42
|
+
remember(key, entry) {
|
|
43
|
+
const limit = this.options.maxBytes ?? STATIC_CACHE_DEFAULT_BYTES;
|
|
44
|
+
if (entry.body.length > limit)
|
|
45
|
+
return;
|
|
46
|
+
this.entries.set(key, entry);
|
|
47
|
+
this.bytes += entry.body.length;
|
|
48
|
+
for (const [oldest, value] of this.entries) {
|
|
49
|
+
if (this.bytes <= limit)
|
|
50
|
+
break;
|
|
51
|
+
this.entries.delete(oldest);
|
|
52
|
+
this.bytes -= value.body.length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function withEtag(file) {
|
|
57
|
+
return { ...file, etag: file.etag ?? `"${createHash('sha256').update(file.body).digest('base64url')}"` };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The policy a static application runs under: the sandbox's former `<meta>` policy, now a header
|
|
61
|
+
* on the application's own origin (so `'self'` is the application, never Atlas), plus what a meta
|
|
62
|
+
* tag cannot express: `frame-ancestors` keeps the page inside Atlas.
|
|
63
|
+
*/
|
|
64
|
+
export function staticContentSecurityPolicy(parentOrigin) {
|
|
65
|
+
return [
|
|
66
|
+
"default-src 'none'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", "style-src 'self' 'unsafe-inline'",
|
|
67
|
+
"img-src 'self' data: blob: https:", "font-src 'self' data:", "connect-src 'self' blob:", "worker-src 'self' blob:",
|
|
68
|
+
"object-src blob:", "frame-src blob:", `frame-ancestors ${parentOrigin}`, "base-uri 'none'", "form-action 'none'",
|
|
69
|
+
].join('; ');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Where a static request is served from: `/_artifact/<deployment>/<file>` for assets, the page for
|
|
73
|
+
* every other path (in-app routes live in the hash or the page's own history). Null: not this
|
|
74
|
+
* deployment's file.
|
|
75
|
+
*/
|
|
76
|
+
export function staticFileForRequest({ url, deploymentId }) {
|
|
77
|
+
const pathname = new URL(url, 'https://static.invalid').pathname;
|
|
78
|
+
const prefix = '/_artifact/';
|
|
79
|
+
if (!pathname.startsWith(prefix))
|
|
80
|
+
return STATIC_PAGE;
|
|
81
|
+
const [requested, ...rest] = pathname.slice(prefix.length).split('/');
|
|
82
|
+
if (requested !== deploymentId)
|
|
83
|
+
return null;
|
|
84
|
+
const file = rest.join('/');
|
|
85
|
+
return /^(?:bundle\.js|sourcemap\.json|chunks\/[A-Za-z0-9._-]{1,200})$/.test(file) ? file : null;
|
|
86
|
+
}
|
|
87
|
+
/** Writes one authorized static file: GET or HEAD, revalidated on every use (each request is authorized). */
|
|
88
|
+
export function sendStaticFile({ request, response, file, parentOrigin }) {
|
|
89
|
+
const headers = {
|
|
90
|
+
'content-type': file.contentType, etag: file.etag, 'cache-control': 'private, no-cache',
|
|
91
|
+
'content-security-policy': staticContentSecurityPolicy(parentOrigin),
|
|
92
|
+
'x-content-type-options': 'nosniff', 'referrer-policy': 'same-origin',
|
|
93
|
+
};
|
|
94
|
+
if (request.headers['if-none-match'] === file.etag) {
|
|
95
|
+
response.writeHead(304, headers);
|
|
96
|
+
response.end();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
response.writeHead(200, { ...headers, 'content-length': String(file.body.length) });
|
|
100
|
+
response.end(request.method === 'HEAD' ? undefined : file.body);
|
|
101
|
+
}
|
|
102
|
+
/** Answers one request for an authorized static target: reads only, and only this deployment's files. */
|
|
103
|
+
export async function serveStaticTarget({ request, response, target, files, renew, parentOrigin }) {
|
|
104
|
+
if (!['GET', 'HEAD'].includes(request.method ?? 'GET'))
|
|
105
|
+
throw new GatewayControlError(405);
|
|
106
|
+
const path = staticFileForRequest({ url: request.url ?? '/', deploymentId: target.deploymentId });
|
|
107
|
+
if (!path)
|
|
108
|
+
throw new GatewayControlError(404);
|
|
109
|
+
const file = await files.get({ deploymentId: target.deploymentId, path });
|
|
110
|
+
renew();
|
|
111
|
+
sendStaticFile({ request, response, file, parentOrigin });
|
|
112
|
+
}
|
package/dist/manifest-next.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodOb
|
|
|
28
28
|
audit: z.ZodOptional<z.ZodObject<{
|
|
29
29
|
service: z.ZodString;
|
|
30
30
|
}, z.core.$strict>>;
|
|
31
|
+
groups: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
read: z.ZodBoolean;
|
|
33
|
+
}, z.core.$strict>>;
|
|
31
34
|
storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
|
|
32
35
|
viewer: "viewer";
|
|
33
36
|
editor: "editor";
|
|
@@ -89,6 +92,9 @@ export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
|
|
|
89
92
|
audit: z.ZodOptional<z.ZodObject<{
|
|
90
93
|
service: z.ZodString;
|
|
91
94
|
}, z.core.$strict>>;
|
|
95
|
+
groups: z.ZodOptional<z.ZodObject<{
|
|
96
|
+
read: z.ZodBoolean;
|
|
97
|
+
}, z.core.$strict>>;
|
|
92
98
|
storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
|
|
93
99
|
viewer: "viewer";
|
|
94
100
|
editor: "editor";
|
|
@@ -280,6 +286,9 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
|
|
|
280
286
|
audit: z.ZodOptional<z.ZodObject<{
|
|
281
287
|
service: z.ZodString;
|
|
282
288
|
}, z.core.$strict>>;
|
|
289
|
+
groups: z.ZodOptional<z.ZodObject<{
|
|
290
|
+
read: z.ZodBoolean;
|
|
291
|
+
}, z.core.$strict>>;
|
|
283
292
|
storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
|
|
284
293
|
viewer: "viewer";
|
|
285
294
|
editor: "editor";
|
|
@@ -401,6 +410,10 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
|
|
|
401
410
|
mobile: z.ZodOptional<z.ZodEnum<{
|
|
402
411
|
responsive: "responsive";
|
|
403
412
|
}>>;
|
|
413
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
414
|
+
alpha: "alpha";
|
|
415
|
+
beta: "beta";
|
|
416
|
+
}>>;
|
|
404
417
|
}, z.core.$strip>>;
|
|
405
418
|
sidebarIcon: z.ZodOptional<z.ZodString>;
|
|
406
419
|
sidebarBadge: z.ZodOptional<z.ZodObject<{
|
package/dist/manifest-next.js
CHANGED
|
@@ -15,9 +15,14 @@ export const ARTIFACT_NEXT_RESOURCE_DEFAULTS = {
|
|
|
15
15
|
};
|
|
16
16
|
export { artifactNextAuditCapabilitySchema };
|
|
17
17
|
export * from './manifest-next-ingress.js';
|
|
18
|
+
/** Caller-scoped group membership. A boolean, not a list of group ids: the directory decides which groups the signed-in user is in. */
|
|
19
|
+
const artifactNextGroupsCapabilitySchema = z.strictObject({
|
|
20
|
+
read: z.boolean(),
|
|
21
|
+
});
|
|
18
22
|
export const artifactNextServerCapabilitiesSchema = z.strictObject({
|
|
19
23
|
...reachShape,
|
|
20
24
|
audit: artifactNextAuditCapabilitySchema.optional(),
|
|
25
|
+
groups: artifactNextGroupsCapabilitySchema.optional(),
|
|
21
26
|
}).prefault({});
|
|
22
27
|
// Reuse role semantics and validation; only the new manifest rejects unknown keys.
|
|
23
28
|
const strictCustomRoles = customRolesBlockSchema.safeExtend({
|
|
@@ -12,6 +12,10 @@ export declare const artifactPresentationSchema: z.ZodObject<{
|
|
|
12
12
|
mobile: z.ZodOptional<z.ZodEnum<{
|
|
13
13
|
responsive: "responsive";
|
|
14
14
|
}>>;
|
|
15
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
alpha: "alpha";
|
|
17
|
+
beta: "beta";
|
|
18
|
+
}>>;
|
|
15
19
|
}, z.core.$strip>;
|
|
16
20
|
export type ArtifactPresentation = z.infer<typeof artifactPresentationSchema>;
|
|
17
21
|
export declare const artifactOfflineCapabilitySchema: z.ZodObject<{
|
|
@@ -95,6 +99,10 @@ export declare const artifactStudioStaticManifestSchema: z.ZodObject<{
|
|
|
95
99
|
mobile: z.ZodOptional<z.ZodEnum<{
|
|
96
100
|
responsive: "responsive";
|
|
97
101
|
}>>;
|
|
102
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
103
|
+
alpha: "alpha";
|
|
104
|
+
beta: "beta";
|
|
105
|
+
}>>;
|
|
98
106
|
}, z.core.$strip>>;
|
|
99
107
|
offline: z.ZodOptional<z.ZodObject<{
|
|
100
108
|
mode: z.ZodEnum<{
|
package/dist/manifest-static.js
CHANGED
|
@@ -16,6 +16,11 @@ export const sidebarBadgeSchema = z.object({
|
|
|
16
16
|
});
|
|
17
17
|
export const artifactPresentationSchema = z.object({
|
|
18
18
|
mobile: z.enum(['responsive']).optional(),
|
|
19
|
+
/**
|
|
20
|
+
* Lifecycle stage the Atlas shell shows as an Alpha/Beta badge beside the
|
|
21
|
+
* app's name. Omit for GA. Mirrors `App.status` for native apps.
|
|
22
|
+
*/
|
|
23
|
+
status: z.enum(['alpha', 'beta']).optional(),
|
|
19
24
|
});
|
|
20
25
|
export const artifactOfflineCapabilitySchema = z.object({
|
|
21
26
|
mode: z.enum(['read', 'read-write']),
|
package/dist/manifest.d.ts
CHANGED
|
@@ -69,6 +69,10 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
|
|
|
69
69
|
mobile: z.ZodOptional<z.ZodEnum<{
|
|
70
70
|
responsive: "responsive";
|
|
71
71
|
}>>;
|
|
72
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
73
|
+
alpha: "alpha";
|
|
74
|
+
beta: "beta";
|
|
75
|
+
}>>;
|
|
72
76
|
}, z.core.$strip>>;
|
|
73
77
|
offline: z.ZodOptional<z.ZodObject<{
|
|
74
78
|
mode: z.ZodEnum<{
|
|
@@ -163,6 +167,9 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
|
|
|
163
167
|
audit: z.ZodOptional<z.ZodObject<{
|
|
164
168
|
service: z.ZodString;
|
|
165
169
|
}, z.core.$strict>>;
|
|
170
|
+
groups: z.ZodOptional<z.ZodObject<{
|
|
171
|
+
read: z.ZodBoolean;
|
|
172
|
+
}, z.core.$strict>>;
|
|
166
173
|
storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
|
|
167
174
|
viewer: "viewer";
|
|
168
175
|
editor: "editor";
|
|
@@ -284,6 +291,10 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
|
|
|
284
291
|
mobile: z.ZodOptional<z.ZodEnum<{
|
|
285
292
|
responsive: "responsive";
|
|
286
293
|
}>>;
|
|
294
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
295
|
+
alpha: "alpha";
|
|
296
|
+
beta: "beta";
|
|
297
|
+
}>>;
|
|
287
298
|
}, z.core.$strip>>;
|
|
288
299
|
sidebarIcon: z.ZodOptional<z.ZodString>;
|
|
289
300
|
sidebarBadge: z.ZodOptional<z.ZodObject<{
|
|
@@ -18,6 +18,10 @@ export declare function isArtifactApiPath(path: string): boolean;
|
|
|
18
18
|
export declare const ARTIFACT_PROCESS_RUN_STATUSES: readonly ["pending", "running", "completed", "failed", "cancelled", "timed_out"];
|
|
19
19
|
/** Lattice lists at most this many runs per call. */
|
|
20
20
|
export declare const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
|
|
21
|
+
/** One page of the caller's own group memberships. */
|
|
22
|
+
export declare const ARTIFACT_GROUP_MEMBERSHIPS_LIMIT = 100;
|
|
23
|
+
/** How far a caller may page their own memberships. The directory is not walked past this. */
|
|
24
|
+
export declare const ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE = 100;
|
|
21
25
|
/** A run page can filter to at most this many entities of one type. */
|
|
22
26
|
export declare const ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT = 500;
|
|
23
27
|
/** A run share adds collaborators to at most this many explicitly assigned human nodes. */
|
|
@@ -298,6 +302,14 @@ export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
298
302
|
group: "group";
|
|
299
303
|
}>>>;
|
|
300
304
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
305
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
306
|
+
operation: z.ZodLiteral<"groups_memberships">;
|
|
307
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
308
|
+
service: "service";
|
|
309
|
+
user: "user";
|
|
310
|
+
}>>;
|
|
311
|
+
page: z.ZodOptional<z.ZodNumber>;
|
|
312
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
301
313
|
}, z.core.$strict>, z.ZodObject<{
|
|
302
314
|
operation: z.ZodLiteral<"flags">;
|
|
303
315
|
mode: z.ZodDefault<z.ZodEnum<{
|
|
@@ -31,6 +31,10 @@ const latticeVersion = z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/);
|
|
|
31
31
|
export const ARTIFACT_PROCESS_RUN_STATUSES = ['pending', 'running', 'completed', 'failed', 'cancelled', 'timed_out'];
|
|
32
32
|
/** Lattice lists at most this many runs per call. */
|
|
33
33
|
export const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
|
|
34
|
+
/** One page of the caller's own group memberships. */
|
|
35
|
+
export const ARTIFACT_GROUP_MEMBERSHIPS_LIMIT = 100;
|
|
36
|
+
/** How far a caller may page their own memberships. The directory is not walked past this. */
|
|
37
|
+
export const ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE = 100;
|
|
34
38
|
/** A run page can filter to at most this many entities of one type. */
|
|
35
39
|
export const ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT = 500;
|
|
36
40
|
/** A run share adds collaborators to at most this many explicitly assigned human nodes. */
|
|
@@ -225,6 +229,13 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
|
225
229
|
types: z.array(z.enum(ARTIFACT_PROCESS_PRINCIPAL_TYPES)).min(1).max(2).optional(),
|
|
226
230
|
limit: z.number().int().min(1).max(ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT).optional(),
|
|
227
231
|
}).strict(),
|
|
232
|
+
// The signed-in user's own group memberships. No search, no admin directory, no group-id allowlist:
|
|
233
|
+
// the directory returns the groups this user is in. Service mode is refused by the handler.
|
|
234
|
+
z.object({
|
|
235
|
+
operation: z.literal('groups_memberships'), mode,
|
|
236
|
+
page: z.number().int().min(1).max(ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE).optional(),
|
|
237
|
+
limit: z.number().int().min(1).max(ARTIFACT_GROUP_MEMBERSHIPS_LIMIT).optional(),
|
|
238
|
+
}).strict(),
|
|
228
239
|
// Evaluates feature flags as Atlas evaluates its own: for the user, or with no user in service mode.
|
|
229
240
|
// A read: it needs no manifest declaration, as the browser bridge's flags.isEnabled needs none.
|
|
230
241
|
z.object({ operation: z.literal('flags'), mode, names: z.array(flagName).min(1).max(ARTIFACT_FLAGS_PER_REQUEST) }).strict(),
|
package/dist/server/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { storageClient } from '@sequenceholdings/storage';
|
|
2
2
|
import { type OperationExecutor } from '@sequenceholdings/orm/client';
|
|
3
3
|
import { type ArtifactAuditAction, type ArtifactServerMode } from './broker-contract.js';
|
|
4
|
+
import { type ArtifactServerGroups } from './groups.js';
|
|
4
5
|
import { type ArtifactServerProcesses } from './processes.js';
|
|
5
6
|
import { type ArtifactOrmJsonScalars } from '../orm-json-scalars.js';
|
|
6
7
|
import { createArtifactLogger, type ArtifactLogRecord } from './logger.js';
|
|
@@ -10,6 +11,7 @@ import { createApiClient } from './api-client.js';
|
|
|
10
11
|
import { type ArtifactPrincipal } from './principal.js';
|
|
11
12
|
import { type ArtifactRuntimeConfig, type ArtifactRequestHeaders } from './runtime-config.js';
|
|
12
13
|
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './errors.js';
|
|
14
|
+
export type * from './groups.js';
|
|
13
15
|
export type * from './processes.js';
|
|
14
16
|
export type { ArtifactServerFetchOptions } from './api-client.js';
|
|
15
17
|
/**
|
|
@@ -82,6 +84,7 @@ export interface ArtifactServerClient<TContext extends ArtifactServerContext = A
|
|
|
82
84
|
invoke<TOutput = unknown>(id: string, params?: Record<string, unknown>): Promise<TOutput>;
|
|
83
85
|
};
|
|
84
86
|
processes: ArtifactServerProcesses;
|
|
87
|
+
groups: ArtifactServerGroups;
|
|
85
88
|
flags: ArtifactServerFlags;
|
|
86
89
|
audit: ArtifactServerAudit;
|
|
87
90
|
orm(input: ArtifactOrmOptions): OperationExecutor;
|
package/dist/server/client.js
CHANGED
|
@@ -3,6 +3,7 @@ import { StorageApiError, storageClient, storageErrorSchema } from '@sequencehol
|
|
|
3
3
|
import { OrmOperationError } from '@sequenceholdings/orm/client';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { artifactOrmIdempotencyKeySchema, artifactServerRequestSchema } from './broker-contract.js';
|
|
6
|
+
import { createArtifactGroupsClient } from './groups.js';
|
|
6
7
|
import { createArtifactProcessesClient } from './processes.js';
|
|
7
8
|
import { assertJsonScalarsAcknowledged, ORM_JSON_SCALARS_HEADER } from '../orm-json-scalars.js';
|
|
8
9
|
import { createArtifactLogger } from './logger.js';
|
|
@@ -96,6 +97,7 @@ function buildClient(args) {
|
|
|
96
97
|
},
|
|
97
98
|
},
|
|
98
99
|
processes: createArtifactProcessesClient({ mode, json }),
|
|
100
|
+
groups: createArtifactGroupsClient({ mode, json }),
|
|
99
101
|
flags: {
|
|
100
102
|
evaluate: evaluateFlags,
|
|
101
103
|
isEnabled: async (flagName) => (await evaluateFlags([flagName]))[flagName] === true,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
|
|
2
|
+
/** One group the signed-in user belongs to. `userRole` is `admin` when they administer it. */
|
|
3
|
+
export interface ArtifactGroupMembership {
|
|
4
|
+
id: string;
|
|
5
|
+
userRole: 'admin' | 'member';
|
|
6
|
+
memberCount: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ArtifactGroupMembershipsQuery {
|
|
9
|
+
page?: number;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ArtifactGroupMembershipsPage {
|
|
13
|
+
groups: ArtifactGroupMembership[];
|
|
14
|
+
pagination: {
|
|
15
|
+
page: number;
|
|
16
|
+
limit: number;
|
|
17
|
+
totalCount: number;
|
|
18
|
+
totalPages: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface ArtifactServerGroups {
|
|
22
|
+
/**
|
|
23
|
+
* The signed-in user's group memberships. Requires `server.capabilities.groups.read`.
|
|
24
|
+
* Atlas decides which groups they are in; this call cannot name groups, search the directory,
|
|
25
|
+
* or read another user's memberships.
|
|
26
|
+
*/
|
|
27
|
+
list(query?: ArtifactGroupMembershipsQuery): Promise<ArtifactGroupMembershipsPage>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createArtifactGroupsClient({ mode, json }: {
|
|
30
|
+
mode: ArtifactServerMode;
|
|
31
|
+
json: (operation: ArtifactServerRequest) => Promise<unknown>;
|
|
32
|
+
}): ArtifactServerGroups;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ARTIFACT_GROUP_MEMBERSHIPS_LIMIT, artifactServerRequestSchema, } from './broker-contract.js';
|
|
3
|
+
import { ArtifactServerError } from './errors.js';
|
|
4
|
+
const membershipsPageSchema = z.object({
|
|
5
|
+
groups: z.array(z.object({
|
|
6
|
+
id: z.string().min(1).max(255),
|
|
7
|
+
userRole: z.enum(['admin', 'member']),
|
|
8
|
+
memberCount: z.number().int().nonnegative(),
|
|
9
|
+
}).strict()),
|
|
10
|
+
pagination: z.object({
|
|
11
|
+
page: z.number().int().min(1),
|
|
12
|
+
limit: z.number().int().min(1).max(ARTIFACT_GROUP_MEMBERSHIPS_LIMIT),
|
|
13
|
+
totalCount: z.number().int().nonnegative(),
|
|
14
|
+
totalPages: z.number().int().nonnegative(),
|
|
15
|
+
}).strict(),
|
|
16
|
+
}).strict();
|
|
17
|
+
export function createArtifactGroupsClient({ mode, json }) {
|
|
18
|
+
return {
|
|
19
|
+
async list(query = {}) {
|
|
20
|
+
const parsed = membershipsPageSchema.safeParse(await json(artifactServerRequestSchema.parse({
|
|
21
|
+
operation: 'groups_memberships', mode, ...query,
|
|
22
|
+
})));
|
|
23
|
+
if (!parsed.success)
|
|
24
|
+
throw new ArtifactServerError(502, 'invalid_response', 'Atlas returned a malformed groups response');
|
|
25
|
+
return parsed.data;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import 'server-only';
|
|
|
2
2
|
export { createArtifactServerClient } from './server/client.js';
|
|
3
3
|
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
4
4
|
export type { ArtifactAtlasRefusal } from './server/errors.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, ArtifactProcessRunSummary, ArtifactProcessRunsPage, ArtifactProcessRunsPageQuery, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessShareRun, ArtifactProcessShareRunResult, ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunSharingAuthoritiesPage, ArtifactProcessRunSharingAuthority, ArtifactProcessRunSharingDelegate, ArtifactProcessRevokeRunSharingAuthority, ArtifactProcessRevokeRunSharingAuthorityResult, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerProcesses, } from './server/client.js';
|
|
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_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.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, ArtifactProcessRunSummary, ArtifactProcessRunsPage, ArtifactProcessRunsPageQuery, ArtifactProcessRunsQuery, ArtifactProcessRunViewerChange, ArtifactProcessRunViewers, ArtifactProcessStart, ArtifactProcessStepRecord, ArtifactProcessShareRun, ArtifactProcessShareRunResult, ArtifactProcessRunSharingAuthoritiesQuery, ArtifactProcessRunSharingAuthoritiesPage, ArtifactProcessRunSharingAuthority, ArtifactProcessRunSharingDelegate, ArtifactProcessRevokeRunSharingAuthority, ArtifactProcessRevokeRunSharingAuthorityResult, ArtifactProcessTask, ArtifactProcessTasksQuery, ArtifactServerAudit, ArtifactServerClient, ArtifactServerClientOptions, ArtifactServerContext, ArtifactServerFetchOptions, ArtifactServerFlags, ArtifactServerGroups, ArtifactServerProcesses, ArtifactGroupMembership, ArtifactGroupMembershipsPage, ArtifactGroupMembershipsQuery, } from './server/client.js';
|
|
6
|
+
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_GROUP_MEMBERSHIPS_LIMIT, ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
7
7
|
export type { ArtifactAuditAction, ArtifactProcessPrincipalType, ArtifactProcessRunStatus, ArtifactProcessTaskView, } from './server/broker-contract.js';
|
|
8
8
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
9
9
|
export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
export { createArtifactServerClient } from './server/client.js';
|
|
3
3
|
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './server/errors.js';
|
|
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_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
4
|
+
export { ARTIFACT_AUDIT_ACTIONS, ARTIFACT_AUDIT_RESOURCES_PER_EVENT, ARTIFACT_FLAGS_PER_REQUEST, ARTIFACT_GROUP_MEMBERSHIPS_LIMIT, ARTIFACT_GROUP_MEMBERSHIPS_MAX_PAGE, ARTIFACT_PROCESS_PRINCIPAL_SEARCH_LIMIT, ARTIFACT_PROCESS_PRINCIPAL_TYPES, ARTIFACT_PROCESS_PRINCIPALS_PER_CALL, ARTIFACT_PROCESS_RUN_STATUSES, ARTIFACT_PROCESS_RUNS_LIMIT, ARTIFACT_PROCESS_RUN_ENTITIES_LIMIT, ARTIFACT_PROCESS_SHARE_NODES_LIMIT, ARTIFACT_PROCESS_SHARING_AUTHORITIES_LIMIT, ARTIFACT_PROCESS_TASK_VIEWS, ARTIFACT_PROCESS_TASKS_LIMIT, } from './server/broker-contract.js';
|
|
5
5
|
export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
|
|
6
6
|
export { ArtifactContextError, getArtifactBrowserBootstrap } from './server/request-context.js';
|
|
7
7
|
export { verifyArtifactExternalContext } from './server/external-context.js';
|
|
@@ -11,3 +11,9 @@ export interface ArtifactSourceBoundaryOptions {
|
|
|
11
11
|
* allowed because they are external to the artifact by design.
|
|
12
12
|
*/
|
|
13
13
|
export declare function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }: ArtifactSourceBoundaryOptions): Plugin;
|
|
14
|
+
/**
|
|
15
|
+
* Whether `filePath` is inside a `node_modules` directory. Bundlers pass
|
|
16
|
+
* Windows paths with forward slashes, so compare in that form whatever the
|
|
17
|
+
* platform separator is.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isNodeModulesPath(filePath: string): boolean;
|
package/dist/source-boundary.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
1
2
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
3
|
/**
|
|
3
4
|
* Artifact source must be self-contained: allowing a relative import to escape
|
|
@@ -7,8 +8,8 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
7
8
|
* allowed because they are external to the artifact by design.
|
|
8
9
|
*/
|
|
9
10
|
export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
|
|
10
|
-
const rootResolved = resolve(root);
|
|
11
|
-
const trustedRoots = trustedExternalRoots.map((trustedRoot) => resolve(trustedRoot));
|
|
11
|
+
const rootResolved = realpathSync(resolve(root));
|
|
12
|
+
const trustedRoots = trustedExternalRoots.map((trustedRoot) => realpathSync(resolve(trustedRoot)));
|
|
12
13
|
return {
|
|
13
14
|
name: 'reject-artifact-outside-source',
|
|
14
15
|
enforce: 'pre',
|
|
@@ -17,15 +18,22 @@ export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
|
|
|
17
18
|
if (!resolved || resolved.external || resolved.id.startsWith('\0'))
|
|
18
19
|
return resolved;
|
|
19
20
|
const filePath = resolved.id.split('?')[0];
|
|
20
|
-
if (!isAbsolute(filePath)
|
|
21
|
+
if (!isAbsolute(filePath))
|
|
22
|
+
return resolved;
|
|
23
|
+
// Vite may return a physical path for a root reached through /tmp or
|
|
24
|
+
// another symlink. Check physical paths on both sides of the boundary;
|
|
25
|
+
// this also prevents an in-root symlink from admitting an outside file.
|
|
26
|
+
const realFilePath = realpathSync(filePath);
|
|
27
|
+
if (isPathWithin(rootResolved, realFilePath))
|
|
21
28
|
return resolved;
|
|
22
29
|
const importerPath = importer?.split('?')[0];
|
|
23
30
|
const importerIsTrustedExternal = importerPath != null &&
|
|
24
|
-
(isNodeModulesPath(importerPath) ||
|
|
31
|
+
(isNodeModulesPath(importerPath) ||
|
|
32
|
+
(isAbsolute(importerPath) && isTrustedExternalPath(realpathSync(importerPath), trustedRoots)));
|
|
25
33
|
if (isNodeModulesPath(filePath) && (isBareImport(source) || importerIsTrustedExternal)) {
|
|
26
34
|
return resolved;
|
|
27
35
|
}
|
|
28
|
-
if (isTrustedExternalPath(
|
|
36
|
+
if (isTrustedExternalPath(realFilePath, trustedRoots))
|
|
29
37
|
return resolved;
|
|
30
38
|
const from = importer
|
|
31
39
|
? ` (imported from ${relative(rootResolved, importer.split('?')[0])})`
|
|
@@ -46,8 +54,13 @@ function isPathWithin(root, candidate) {
|
|
|
46
54
|
function isTrustedExternalPath(filePath, trustedRoots) {
|
|
47
55
|
return trustedRoots.some((trustedRoot) => isPathWithin(trustedRoot, filePath));
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Whether `filePath` is inside a `node_modules` directory. Bundlers pass
|
|
59
|
+
* Windows paths with forward slashes, so compare in that form whatever the
|
|
60
|
+
* platform separator is.
|
|
61
|
+
*/
|
|
62
|
+
export function isNodeModulesPath(filePath) {
|
|
63
|
+
return filePath.replaceAll('\\', '/').includes('/node_modules/');
|
|
51
64
|
}
|
|
52
65
|
function isBareImport(source) {
|
|
53
66
|
return !source.startsWith('.') && !isAbsolute(source);
|
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
7
7
|
"build": "vite build",
|
|
8
|
-
"preview": "vite preview"
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"type-check": "tsc --noEmit"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {},
|
|
11
12
|
"devDependencies": {
|
|
12
|
-
"@sequenceholdings/artifact-studio": "^0.
|
|
13
|
+
"@sequenceholdings/artifact-studio": "^0.8.0",
|
|
13
14
|
"@sequenceholdings/atlas-ui": "^3.1.1",
|
|
14
15
|
"@tailwindcss/postcss": "^4.2.2",
|
|
15
16
|
"@tailwindcss/vite": "^4.2.2",
|
|
16
17
|
"@tanstack/react-query": "^4.36.1",
|
|
18
|
+
"@types/react": "^18.3.31",
|
|
19
|
+
"@types/react-dom": "^18.3.7",
|
|
17
20
|
"@vitejs/plugin-react": "^5.0.0",
|
|
18
21
|
"react": "^18.2.0",
|
|
19
22
|
"react-dom": "^18.2.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"types": ["vite/client"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "vite.config.ts"]
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.8.0",
|
|
13
13
|
"description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
@@ -159,6 +159,7 @@
|
|
|
159
159
|
"sonner": "^2.0.7",
|
|
160
160
|
"tailwindcss": "^4.1.8",
|
|
161
161
|
"tw-animate-css": "^1.4.0",
|
|
162
|
+
"typescript": "^5.6.0",
|
|
162
163
|
"vite": "8.0.16",
|
|
163
164
|
"zod": "^4.1.13",
|
|
164
165
|
"ws": "^8.21.0",
|
|
@@ -166,14 +167,13 @@
|
|
|
166
167
|
"google-auth-library": "^10.5.0",
|
|
167
168
|
"server-only": "0.0.1",
|
|
168
169
|
"parse5": "7.3.0",
|
|
169
|
-
"@sequenceholdings/lattice-form-renderer": "0.2.0",
|
|
170
170
|
"@sequenceholdings/storage": "0.1.1",
|
|
171
|
-
"@sequenceholdings/orm": "2.6.
|
|
171
|
+
"@sequenceholdings/orm": "2.6.1",
|
|
172
|
+
"@sequenceholdings/lattice-form-renderer": "0.2.0"
|
|
172
173
|
},
|
|
173
174
|
"devDependencies": {
|
|
174
175
|
"@types/js-yaml": "^4.0.9",
|
|
175
176
|
"@types/node": "^22.0.0",
|
|
176
|
-
"typescript": "^5.6.0",
|
|
177
177
|
"vitest": "^4.1.11",
|
|
178
178
|
"@types/ws": "^8.18.1"
|
|
179
179
|
},
|
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
7
7
|
"build": "vite build",
|
|
8
|
-
"preview": "vite preview"
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"type-check": "tsc --noEmit"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {},
|
|
11
12
|
"devDependencies": {
|
|
12
|
-
"@sequenceholdings/artifact-studio": "^0.
|
|
13
|
+
"@sequenceholdings/artifact-studio": "^0.8.0",
|
|
13
14
|
"@sequenceholdings/atlas-ui": "^3.1.1",
|
|
14
15
|
"@tailwindcss/postcss": "^4.2.2",
|
|
15
16
|
"@tailwindcss/vite": "^4.2.2",
|
|
16
17
|
"@tanstack/react-query": "^4.36.1",
|
|
18
|
+
"@types/react": "^18.3.31",
|
|
19
|
+
"@types/react-dom": "^18.3.7",
|
|
17
20
|
"@vitejs/plugin-react": "^5.0.0",
|
|
18
21
|
"react": "^18.2.0",
|
|
19
22
|
"react-dom": "^18.2.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"types": ["vite/client"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "vite.config.ts"]
|
|
17
|
+
}
|