@sequenceholdings/artifact-studio 0.6.6 → 0.7.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/README.md +96 -4
- package/VERSION-PIN.md +22 -0
- package/dist/cli.js +4 -6
- package/dist/gateway/abort.d.ts +6 -0
- package/dist/gateway/abort.js +16 -0
- package/dist/gateway/authorization-cache.d.ts +24 -0
- package/dist/gateway/authorization-cache.js +21 -3
- package/dist/gateway/bootstrap-page.d.ts +16 -0
- package/dist/gateway/bootstrap-page.js +34 -0
- package/dist/gateway/control-config.d.ts +21 -0
- package/dist/gateway/control-config.js +18 -0
- package/dist/gateway/control.d.ts +12 -0
- package/dist/gateway/control.js +21 -4
- package/dist/gateway/hosted-proxy.d.ts +3 -0
- package/dist/gateway/hosted-proxy.js +12 -2
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +20 -20
- package/dist/gateway/session.d.ts +8 -8
- package/dist/gateway/session.js +4 -9
- package/dist/gateway/usage.d.ts +13 -0
- package/dist/gateway/usage.js +18 -0
- package/dist/gateway-main.js +8 -5
- package/dist/git-clone.d.ts +1 -3
- package/dist/git-clone.js +6 -8
- package/dist/manifest-next-browser-data.d.ts +17 -0
- package/dist/manifest-next-browser-data.js +46 -0
- package/dist/manifest-next.d.ts +7 -4
- package/dist/manifest-next.js +3 -0
- package/dist/manifest.d.ts +6 -4
- package/dist/runtime-contracts.d.ts +8 -0
- package/dist/runtime-contracts.js +10 -0
- package/dist/sdk-orm.d.ts +3 -3
- package/dist/server/internal/verify-any.d.ts +14 -0
- package/dist/server/internal/verify-any.js +27 -1
- package/dist/source-resolver.js +7 -16
- package/dist/templates/nextjs/README.md +7 -0
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/trusted-install.d.ts +9 -2
- package/dist/trusted-install.js +9 -2
- package/package.json +3 -3
- package/templates/nextjs/README.md +7 -0
- package/templates/react-vite/package.json +1 -1
|
@@ -38,16 +38,16 @@ export declare function renewGatewaySession({ session, authorizedExpiresAt, now
|
|
|
38
38
|
now?: number;
|
|
39
39
|
}): GatewaySession | null;
|
|
40
40
|
/**
|
|
41
|
-
* `Strict`
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* Partitioned
|
|
41
|
+
* `Strict` when every app origin sits under Atlas's own HTTPS host, so the iframe is same-site with
|
|
42
|
+
* its parent. Anywhere else — app origins on seqholdingsusercontent.com, or the local stack's
|
|
43
|
+
* http://localhost Atlas — a browser drops a `Strict` cookie inside the Atlas iframe, so the cookie
|
|
44
|
+
* is `SameSite=None; Partitioned`: the browser keys it to Atlas's top-level site and never sends it
|
|
45
|
+
* under any other. Cross-site writes are refused on `Origin` either way, not on SameSite.
|
|
45
46
|
*/
|
|
46
47
|
export type GatewaySessionSameSite = 'Strict' | 'None';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
appEnv: string;
|
|
48
|
+
export declare function gatewaySessionSameSite({ parentOrigin, runtimeDomain }: {
|
|
49
|
+
parentOrigin: string;
|
|
50
|
+
runtimeDomain: string;
|
|
51
51
|
}): GatewaySessionSameSite;
|
|
52
52
|
export declare function gatewaySessionCookies({ session, key, cookie, renewal, now, sameSite }: {
|
|
53
53
|
session: GatewaySession;
|
package/dist/gateway/session.js
CHANGED
|
@@ -39,15 +39,10 @@ export function renewGatewaySession({ session, authorizedExpiresAt, now = Date.n
|
|
|
39
39
|
const expiresAt = Math.min(authorizedExpiresAt, session.maxExpiresAt ?? session.expiresAt);
|
|
40
40
|
return expiresAt > session.expiresAt ? { ...session, expiresAt } : null;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (value !== 'none')
|
|
47
|
-
throw new Error('ARTIFACT_NEXT_SESSION_COOKIE_SAMESITE must be "strict" or "none"');
|
|
48
|
-
if (appEnv !== 'development')
|
|
49
|
-
throw new Error('ARTIFACT_NEXT_SESSION_COOKIE_SAMESITE=none is only for a local development gateway');
|
|
50
|
-
return 'None';
|
|
42
|
+
export function gatewaySessionSameSite({ parentOrigin, runtimeDomain }) {
|
|
43
|
+
const parent = new URL(parentOrigin);
|
|
44
|
+
const underParent = runtimeDomain === parent.hostname || runtimeDomain.endsWith(`.${parent.hostname}`);
|
|
45
|
+
return parent.protocol === 'https:' && underParent ? 'Strict' : 'None';
|
|
51
46
|
}
|
|
52
47
|
function cookieAttributes(sameSite) {
|
|
53
48
|
return `Path=/; HttpOnly; Secure; ${sameSite === 'None' ? 'SameSite=None; Partitioned' : 'SameSite=Strict'}`;
|
package/dist/gateway/usage.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** Closed, non-sensitive phase observations. An absent phase did not finish before reporting. */
|
|
2
|
+
export interface GatewayTimings {
|
|
3
|
+
authorizationMs?: number;
|
|
4
|
+
authorizationCache?: 'hit' | 'shared' | 'load' | 'refresh';
|
|
5
|
+
admissionMs?: number;
|
|
6
|
+
identityTokenMs?: number;
|
|
7
|
+
/** Transport and app work until response headers, not pure app execution time. */
|
|
8
|
+
upstreamHeadersMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function gatewayElapsedMs(started: number): number;
|
|
11
|
+
/** Detach the once-only usage event from work that can settle after cancellation. */
|
|
12
|
+
export declare function snapshotGatewayTimings(timings: GatewayTimings): Readonly<GatewayTimings>;
|
|
1
13
|
export interface GatewayUsageEvent {
|
|
2
14
|
eventId: string;
|
|
3
15
|
requestId: string;
|
|
@@ -22,6 +34,7 @@ export interface GatewayUsageEvent {
|
|
|
22
34
|
};
|
|
23
35
|
outcome: 'success' | 'error' | 'cancelled';
|
|
24
36
|
httpStatus: number;
|
|
37
|
+
gatewayTimings?: Readonly<GatewayTimings>;
|
|
25
38
|
}
|
|
26
39
|
export type GatewayUsageFailure = {
|
|
27
40
|
kind: 'usage_queue_full' | 'usage_delivery_failed' | 'usage_delivery_rejected' | 'usage_events_rejected' | 'usage_lost_on_shutdown';
|
package/dist/gateway/usage.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
const MAX_GATEWAY_TIMING_MS = 600_000;
|
|
2
|
+
export function gatewayElapsedMs(started) {
|
|
3
|
+
return Math.min(MAX_GATEWAY_TIMING_MS, Math.max(0, Math.round(performance.now() - started)));
|
|
4
|
+
}
|
|
5
|
+
/** Detach the once-only usage event from work that can settle after cancellation. */
|
|
6
|
+
export function snapshotGatewayTimings(timings) {
|
|
7
|
+
const snapshot = {};
|
|
8
|
+
const durationKeys = ['authorizationMs', 'admissionMs', 'identityTokenMs', 'upstreamHeadersMs'];
|
|
9
|
+
for (const key of durationKeys) {
|
|
10
|
+
const value = timings[key];
|
|
11
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= MAX_GATEWAY_TIMING_MS)
|
|
12
|
+
snapshot[key] = value;
|
|
13
|
+
}
|
|
14
|
+
const source = timings.authorizationCache;
|
|
15
|
+
if (source === 'hit' || source === 'shared' || source === 'load' || source === 'refresh')
|
|
16
|
+
snapshot.authorizationCache = source;
|
|
17
|
+
return Object.freeze(snapshot);
|
|
18
|
+
}
|
|
1
19
|
export const GATEWAY_USAGE_LIMITS = Object.freeze({ capacity: 10_000, maxBatch: 100, flushIntervalMs: 1_000, concurrency: 4 });
|
|
2
20
|
const ATTEMPTS = 3;
|
|
3
21
|
/**
|
package/dist/gateway-main.js
CHANGED
|
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { isIP } from 'node:net';
|
|
3
3
|
import { Redis } from 'ioredis';
|
|
4
4
|
import { ArtifactGatewayControl } from './gateway/control.js';
|
|
5
|
+
import { resolveGatewayControlConfig } from './gateway/control-config.js';
|
|
6
|
+
import { hostedIdentityToken } from './gateway/hosted-proxy.js';
|
|
5
7
|
import { createArtifactGateway } from './gateway/server.js';
|
|
6
8
|
import { gatewaySessionSameSite } from './gateway/session.js';
|
|
7
9
|
import { RedisTunnelOwnership, RedisTunnelRelay } from './tunnel/redis.js';
|
|
@@ -58,9 +60,9 @@ async function main() {
|
|
|
58
60
|
let stopping = false;
|
|
59
61
|
subscriber.on('ready', () => { if (!stopping)
|
|
60
62
|
void relay.start().catch(() => undefined); });
|
|
61
|
-
const
|
|
63
|
+
const controlConfig = resolveGatewayControlConfig({ env: process.env, identityToken: hostedIdentityToken });
|
|
62
64
|
const controlToken = required('ARTIFACT_NEXT_CONTROL_TOKEN');
|
|
63
|
-
const control = new ArtifactGatewayControl({ url:
|
|
65
|
+
const control = new ArtifactGatewayControl({ url: controlConfig.url, token: controlToken, identity: controlConfig.identity });
|
|
64
66
|
const contextPrivateKey = required('ARTIFACT_NEXT_CONTEXT_PRIVATE_KEY');
|
|
65
67
|
const contextKeyId = required('ARTIFACT_NEXT_CONTEXT_KEY_ID');
|
|
66
68
|
const admission = new ArtifactRequestAdmission({ redis: command, namespace });
|
|
@@ -73,13 +75,14 @@ async function main() {
|
|
|
73
75
|
else
|
|
74
76
|
refuse(failure.kind); },
|
|
75
77
|
});
|
|
78
|
+
const runtimeDomain = required('ARTIFACT_NEXT_RUNTIME_DOMAIN');
|
|
76
79
|
const gateway = createArtifactGateway({
|
|
77
80
|
control,
|
|
78
|
-
runtimeDomain
|
|
79
|
-
parentOrigin:
|
|
81
|
+
runtimeDomain,
|
|
82
|
+
parentOrigin: controlConfig.parentOrigin,
|
|
80
83
|
gatewayOrigin: required('ARTIFACT_NEXT_GATEWAY_URL'),
|
|
81
84
|
sessionKey,
|
|
82
|
-
sessionCookieSameSite: gatewaySessionSameSite({
|
|
85
|
+
sessionCookieSameSite: gatewaySessionSameSite({ parentOrigin: controlConfig.parentOrigin, runtimeDomain }),
|
|
83
86
|
contextPrivateKey,
|
|
84
87
|
contextKeyId,
|
|
85
88
|
nodeId, routing: ownership, relay,
|
package/dist/git-clone.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Read the git PAT from the environment (`ATLAS_GIT_PAT`), trimmed. */
|
|
2
|
-
export declare function resolveGitPatFromEnv(): string | undefined;
|
|
3
1
|
/** Full or abbreviated hex SHAs — not valid as `git clone --branch`. */
|
|
4
2
|
export declare function looksLikeCommitSha(ref: string): boolean;
|
|
5
3
|
/**
|
|
@@ -20,7 +18,7 @@ export type RunGitClone = (args: {
|
|
|
20
18
|
* default branch in full, then check the SHA out.
|
|
21
19
|
*/
|
|
22
20
|
depth?: number;
|
|
23
|
-
/**
|
|
21
|
+
/** Login access token used as Basic-auth password (username `git`). */
|
|
24
22
|
pat: string;
|
|
25
23
|
/** Hard ceiling before the git child is killed (default 5m). */
|
|
26
24
|
timeoutMs?: number;
|
package/dist/git-clone.js
CHANGED
|
@@ -5,20 +5,15 @@
|
|
|
5
5
|
* re-exports it from `src/repos/git-clone.ts` so the `repos` tool and the
|
|
6
6
|
* artifact `--repo` deploy path share one clone path.
|
|
7
7
|
*
|
|
8
|
-
* Credentials are supplied via an ephemeral GIT_ASKPASS script so the
|
|
8
|
+
* Credentials are supplied via an ephemeral GIT_ASKPASS script so the token
|
|
9
9
|
* never lands in `remote.origin.url` or the git argv. Callers pass a clean
|
|
10
|
-
* HTTPS clone URL plus the
|
|
10
|
+
* HTTPS clone URL plus the login token separately.
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { createScrubbedChildEnv } from './child-environment.js';
|
|
17
|
-
/** Read the git PAT from the environment (`ATLAS_GIT_PAT`), trimmed. */
|
|
18
|
-
export function resolveGitPatFromEnv() {
|
|
19
|
-
const value = process.env.ATLAS_GIT_PAT?.trim();
|
|
20
|
-
return value || undefined;
|
|
21
|
-
}
|
|
22
17
|
/** Full or abbreviated hex SHAs — not valid as `git clone --branch`. */
|
|
23
18
|
export function looksLikeCommitSha(ref) {
|
|
24
19
|
return /^[0-9a-f]{7,40}$/i.test(ref);
|
|
@@ -151,9 +146,12 @@ export async function withGitAskpass({ pat, fn, }) {
|
|
|
151
146
|
GIT_TERMINAL_PROMPT: '0',
|
|
152
147
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
153
148
|
SEQ_STUDIO_GIT_ASKPASS_PASSWORD: pat,
|
|
154
|
-
GIT_CONFIG_COUNT: '
|
|
149
|
+
GIT_CONFIG_COUNT: '2',
|
|
155
150
|
GIT_CONFIG_KEY_0: 'credential.helper',
|
|
156
151
|
GIT_CONFIG_VALUE_0: '',
|
|
152
|
+
// Never redirect a platform credential, even to another path on this host.
|
|
153
|
+
GIT_CONFIG_KEY_1: 'http.followRedirects',
|
|
154
|
+
GIT_CONFIG_VALUE_1: 'false',
|
|
157
155
|
},
|
|
158
156
|
}));
|
|
159
157
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ArtifactStudioCustomRoles } from './manifest-custom-roles.js';
|
|
3
|
+
/** Additional browser admission rules; neither these rules nor data reach grant access. */
|
|
4
|
+
export declare const artifactNextBrowserDataSchema: z.ZodPrefault<z.ZodObject<{
|
|
5
|
+
operations: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
+
required_capabilities: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
|
|
7
|
+
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
8
|
+
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
9
|
+
}, z.core.$strict>>;
|
|
10
|
+
export type ArtifactNextBrowserData = z.infer<typeof artifactNextBrowserDataSchema>;
|
|
11
|
+
/** Restriction references must be backed by this browser's reach and declared artifact pins. */
|
|
12
|
+
export declare function validateBrowserDataAdmission({ data, uses, roles, ctx }: {
|
|
13
|
+
data: ArtifactNextBrowserData;
|
|
14
|
+
uses: readonly string[];
|
|
15
|
+
roles?: ArtifactStudioCustomRoles;
|
|
16
|
+
ctx: z.RefinementCtx;
|
|
17
|
+
}): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { capabilityPinsSchema } from './manifest-custom-roles.js';
|
|
3
|
+
import { boundedRef, ormOperationRef, reachShape } from './manifest-next-reach.js';
|
|
4
|
+
const browserOperationRef = boundedRef.refine((ref) => ormOperationRef.test(ref), 'Browser ORM operations name each operation: namespace/ops/Operation');
|
|
5
|
+
/** Additional browser admission rules; neither these rules nor data reach grant access. */
|
|
6
|
+
export const artifactNextBrowserDataSchema = z.strictObject({
|
|
7
|
+
...reachShape.data.unwrap().shape,
|
|
8
|
+
// Preserve absence: existing manifests use table-set admission. An explicit [] denies all.
|
|
9
|
+
operations: z.array(browserOperationRef).max(256).refine((operations) => new Set(operations).size === operations.length, 'Browser ORM operations must be unique').optional(),
|
|
10
|
+
// Keys name source namespaces, before development/preview namespace bindings are applied.
|
|
11
|
+
required_capabilities: z.preprocess((value, ctx) => {
|
|
12
|
+
// Zod records skip this own key before validating keys; reject it rather than silently drop it.
|
|
13
|
+
if (value !== null && typeof value === 'object' && Object.hasOwn(value, '__proto__')) {
|
|
14
|
+
ctx.addIssue({ code: 'custom', path: ['__proto__'], message: 'Capability guard keys are ORM source namespaces' });
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}, z.record(z.string().regex(/^[a-z][a-z0-9_]{0,40}$/, 'Capability guard keys are ORM source namespaces'), capabilityPinsSchema.unwrap().min(1).refine((pins) => new Set(pins).size === pins.length, 'Required browser ORM capabilities must be unique'))).optional(),
|
|
18
|
+
}).prefault({});
|
|
19
|
+
/** Restriction references must be backed by this browser's reach and declared artifact pins. */
|
|
20
|
+
export function validateBrowserDataAdmission({ data, uses, roles, ctx }) {
|
|
21
|
+
const reached = new Set([...data.read, ...data.write].map((ref) => ref.split('/', 1)[0]));
|
|
22
|
+
for (const [index, operation] of (data.operations ?? []).entries()) {
|
|
23
|
+
const namespace = operation.split('/', 1)[0];
|
|
24
|
+
if (!reached.has(namespace)) {
|
|
25
|
+
ctx.addIssue({
|
|
26
|
+
code: 'custom', path: ['capabilities', 'data', 'operations', index],
|
|
27
|
+
message: `${operation} names a namespace that browser capabilities.data.read or write does not reach`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const declared = new Set([...Object.keys(roles?.capabilities ?? {}), ...uses]);
|
|
32
|
+
for (const [namespace, pins] of Object.entries(data.required_capabilities ?? {})) {
|
|
33
|
+
const path = ['capabilities', 'data', 'required_capabilities', namespace];
|
|
34
|
+
if (!reached.has(namespace)) {
|
|
35
|
+
ctx.addIssue({ code: 'custom', path, message: `${namespace} is not reached by browser capabilities.data.read or write` });
|
|
36
|
+
}
|
|
37
|
+
for (const [index, pin] of pins.entries()) {
|
|
38
|
+
if (!declared.has(pin)) {
|
|
39
|
+
ctx.addIssue({
|
|
40
|
+
code: 'custom', path: [...path, index],
|
|
41
|
+
message: `Required capability "${pin}" must be declared in capabilities.roles.capabilities or capabilities.uses`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/manifest-next.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare const ARTIFACT_NEXT_RESOURCE_DEFAULTS: {
|
|
|
22
22
|
timeout_seconds: number;
|
|
23
23
|
};
|
|
24
24
|
export { artifactNextAuditCapabilitySchema };
|
|
25
|
+
export type { ArtifactNextBrowserData } from './manifest-next-browser-data.js';
|
|
25
26
|
export * from './manifest-next-ingress.js';
|
|
26
27
|
export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodObject<{
|
|
27
28
|
audit: z.ZodOptional<z.ZodObject<{
|
|
@@ -228,6 +229,12 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
|
|
|
228
229
|
next: z.ZodDefault<z.ZodLiteral<"16.3">>;
|
|
229
230
|
}, z.core.$strict>>;
|
|
230
231
|
capabilities: z.ZodPrefault<z.ZodObject<{
|
|
232
|
+
data: z.ZodPrefault<z.ZodObject<{
|
|
233
|
+
operations: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
234
|
+
required_capabilities: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
|
|
235
|
+
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
236
|
+
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
237
|
+
}, z.core.$strict>>;
|
|
231
238
|
allow_downloads: z.ZodDefault<z.ZodBoolean>;
|
|
232
239
|
allow_popups: z.ZodDefault<z.ZodBoolean>;
|
|
233
240
|
roles: z.ZodOptional<z.ZodObject<{
|
|
@@ -247,10 +254,6 @@ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
|
|
|
247
254
|
editor: "editor";
|
|
248
255
|
admin: "admin";
|
|
249
256
|
}>>>;
|
|
250
|
-
data: z.ZodPrefault<z.ZodObject<{
|
|
251
|
-
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
252
|
-
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
253
|
-
}, z.core.$strict>>;
|
|
254
257
|
api: z.ZodPrefault<z.ZodObject<{
|
|
255
258
|
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
256
259
|
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
package/dist/manifest-next.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ontologyContractSchema } from '@sequenceholdings/orm/canonical';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from './manifest-custom-roles.js';
|
|
4
|
+
import { artifactNextBrowserDataSchema, validateBrowserDataAdmission } from './manifest-next-browser-data.js';
|
|
4
5
|
import { isManagedServerInvokePath } from './server/managed-server-path.js';
|
|
5
6
|
import { artifactNextIngressSchema, validateIngressAgainstServer } from './manifest-next-ingress.js';
|
|
6
7
|
import { artifactNextAuditCapabilitySchema, reachShape, serviceAccountRef } from './manifest-next-reach.js';
|
|
@@ -25,6 +26,7 @@ const strictCustomRoles = customRolesBlockSchema.safeExtend({
|
|
|
25
26
|
}).strict();
|
|
26
27
|
const browserCapabilities = z.strictObject({
|
|
27
28
|
...reachShape,
|
|
29
|
+
data: artifactNextBrowserDataSchema,
|
|
28
30
|
allow_downloads: z.boolean().default(false),
|
|
29
31
|
allow_popups: z.boolean().default(false),
|
|
30
32
|
roles: strictCustomRoles.optional(),
|
|
@@ -125,6 +127,7 @@ export const artifactStudioNextManifestSchema = z.strictObject({
|
|
|
125
127
|
sidebarIcon: z.string().max(64).optional(),
|
|
126
128
|
sidebarBadge: sidebarBadgeSchema.strict().optional(),
|
|
127
129
|
}).superRefine((manifest, ctx) => {
|
|
130
|
+
validateBrowserDataAdmission({ ...manifest.capabilities, ctx });
|
|
128
131
|
for (const message of validateCapabilityPins({ uses: manifest.capabilities.uses, roles: manifest.capabilities.roles })) {
|
|
129
132
|
ctx.addIssue({ code: 'custom', path: ['capabilities', 'uses'], message });
|
|
130
133
|
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -112,6 +112,12 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
|
|
|
112
112
|
next: z.ZodDefault<z.ZodLiteral<"16.3">>;
|
|
113
113
|
}, z.core.$strict>>;
|
|
114
114
|
capabilities: z.ZodPrefault<z.ZodObject<{
|
|
115
|
+
data: z.ZodPrefault<z.ZodObject<{
|
|
116
|
+
operations: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
117
|
+
required_capabilities: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
|
|
118
|
+
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
119
|
+
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
120
|
+
}, z.core.$strict>>;
|
|
115
121
|
allow_downloads: z.ZodDefault<z.ZodBoolean>;
|
|
116
122
|
allow_popups: z.ZodDefault<z.ZodBoolean>;
|
|
117
123
|
roles: z.ZodOptional<z.ZodObject<{
|
|
@@ -131,10 +137,6 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
|
|
|
131
137
|
editor: "editor";
|
|
132
138
|
admin: "admin";
|
|
133
139
|
}>>>;
|
|
134
|
-
data: z.ZodPrefault<z.ZodObject<{
|
|
135
|
-
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
136
|
-
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
137
|
-
}, z.core.$strict>>;
|
|
138
140
|
api: z.ZodPrefault<z.ZodObject<{
|
|
139
141
|
read: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
140
142
|
write: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export { resolveArtifactNextImages } from './next-image-config.js';
|
|
3
|
+
/** Atlas routes the gateway calls; every one authenticates the gateway itself. */
|
|
4
|
+
export declare const ARTIFACT_NEXT_CONTROL_PATH = "/api/artifact-studio/next/control";
|
|
5
|
+
/**
|
|
6
|
+
* The identity-token audience for gateway control calls: Atlas's public origin plus the control
|
|
7
|
+
* path, whichever address the call is routed to. Path-scoped, so a token minted for another
|
|
8
|
+
* Atlas audience is never accepted here.
|
|
9
|
+
*/
|
|
10
|
+
export declare function artifactNextControlAudience(atlasOrigin: string): string;
|
|
3
11
|
/** Shared by local collection, repository collection and the bounded source-upload API. */
|
|
4
12
|
export declare const ARTIFACT_NEXT_SOURCE_LIMITS: Readonly<{
|
|
5
13
|
maxFiles: 2000;
|
|
@@ -3,6 +3,16 @@ import { ARTIFACT_NEXT_RUNTIME, artifactNextSecretNameSchema } from './manifest-
|
|
|
3
3
|
import { MAX_STUDIO_PATH_LENGTH } from './paths.js';
|
|
4
4
|
import { isLoopbackHostname } from './loopback.js';
|
|
5
5
|
export { resolveArtifactNextImages } from './next-image-config.js';
|
|
6
|
+
/** Atlas routes the gateway calls; every one authenticates the gateway itself. */
|
|
7
|
+
export const ARTIFACT_NEXT_CONTROL_PATH = '/api/artifact-studio/next/control';
|
|
8
|
+
/**
|
|
9
|
+
* The identity-token audience for gateway control calls: Atlas's public origin plus the control
|
|
10
|
+
* path, whichever address the call is routed to. Path-scoped, so a token minted for another
|
|
11
|
+
* Atlas audience is never accepted here.
|
|
12
|
+
*/
|
|
13
|
+
export function artifactNextControlAudience(atlasOrigin) {
|
|
14
|
+
return `${new URL(atlasOrigin).origin}${ARTIFACT_NEXT_CONTROL_PATH}`;
|
|
15
|
+
}
|
|
6
16
|
/** Shared by local collection, repository collection and the bounded source-upload API. */
|
|
7
17
|
export const ARTIFACT_NEXT_SOURCE_LIMITS = Object.freeze({
|
|
8
18
|
maxFiles: 2_000,
|
package/dist/sdk-orm.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type SqlOperationExecutor } from '@sequenceholdings/orm/client';
|
|
2
2
|
import { type ArtifactOrmJsonScalars } from './orm-json-scalars.js';
|
|
3
3
|
import { type SequenceApi } from './sdk.js';
|
|
4
4
|
export interface ArtifactOrmExecutorConfig {
|
|
5
5
|
namespace: string;
|
|
6
6
|
/** The generated operations.manifest.json, validated before any host call. */
|
|
7
|
-
manifest
|
|
7
|
+
manifest?: unknown;
|
|
8
8
|
/** Optional action-scoped host client (for example, withTraceAction's client). */
|
|
9
9
|
sdk?: {
|
|
10
10
|
api: Pick<SequenceApi, 'fetch'>;
|
|
@@ -21,4 +21,4 @@ export interface ArtifactOrmExecutorConfig {
|
|
|
21
21
|
* Declare the namespace path in capabilities.api.write (queries also use POST).
|
|
22
22
|
* Namespace grants, capabilities, row policies, and write checks still apply.
|
|
23
23
|
*/
|
|
24
|
-
export declare function createArtifactOrmExecutor({ namespace, manifest, sdk, jsonScalars }: ArtifactOrmExecutorConfig):
|
|
24
|
+
export declare function createArtifactOrmExecutor({ namespace, manifest, sdk, jsonScalars }: ArtifactOrmExecutorConfig): SqlOperationExecutor;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { type ArtifactGatewayPublicKeys, type ArtifactRequestContext, type RequestContextV2External } from '../request-context.js';
|
|
2
2
|
export type AnyArtifactRequestContext = ArtifactRequestContext | RequestContextV2External;
|
|
3
|
+
/**
|
|
4
|
+
* Optional human-directory prefetch for hosted admission, never an authorization result. The
|
|
5
|
+
* signed project is not yet bound to the credential's database record. After loading that record,
|
|
6
|
+
* the caller must still run its ordinary verifier with the runtime's keys and authoritative
|
|
7
|
+
* project, rechecking the clock after the await. Ineligible or invalid contexts return null so
|
|
8
|
+
* credential failures and external admission keep their existing checks and error ordering.
|
|
9
|
+
*/
|
|
10
|
+
export declare function preverifyHostedBrowserContext({ token, publicKeys, environment, deploymentId, now }: {
|
|
11
|
+
token: string | null;
|
|
12
|
+
publicKeys: ArtifactGatewayPublicKeys;
|
|
13
|
+
environment: string;
|
|
14
|
+
deploymentId: string;
|
|
15
|
+
now?: number;
|
|
16
|
+
}): ArtifactRequestContext | null;
|
|
3
17
|
/**
|
|
4
18
|
* Version-dispatching verifier for platform code that must accept both Atlas-user (v1) and
|
|
5
19
|
* external (v2) contexts, such as the broker. Applications never get this: it is not exported
|
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
import { ArtifactContextError, requestContextV2ExternalSchema, } from '../request-context.js';
|
|
1
|
+
import { ArtifactContextError, ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX, artifactRequestContextSchema, requestContextV2ExternalSchema, } from '../request-context.js';
|
|
2
2
|
import { ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX, verifySignedContext } from './signed-token.js';
|
|
3
3
|
import { parseV1Context } from './v1-context.js';
|
|
4
4
|
import { canonicalizeIngressPath, signedIngressPath } from '../ingress-routes.js';
|
|
5
|
+
/**
|
|
6
|
+
* Optional human-directory prefetch for hosted admission, never an authorization result. The
|
|
7
|
+
* signed project is not yet bound to the credential's database record. After loading that record,
|
|
8
|
+
* the caller must still run its ordinary verifier with the runtime's keys and authoritative
|
|
9
|
+
* project, rechecking the clock after the await. Ineligible or invalid contexts return null so
|
|
10
|
+
* credential failures and external admission keep their existing checks and error ordering.
|
|
11
|
+
*/
|
|
12
|
+
export function preverifyHostedBrowserContext({ token, publicKeys, environment, deploymentId, now = Math.floor(Date.now() / 1000) }) {
|
|
13
|
+
try {
|
|
14
|
+
const { kid, payload } = verifySignedContext({ token, publicKeys });
|
|
15
|
+
if (kid.startsWith(ARTIFACT_EXTERNAL_CONTEXT_KID_PREFIX))
|
|
16
|
+
return null;
|
|
17
|
+
const parsed = artifactRequestContextSchema.safeParse(payload);
|
|
18
|
+
if (!parsed.success)
|
|
19
|
+
return null;
|
|
20
|
+
const context = parsed.data;
|
|
21
|
+
if (context.mode === 'development' || context.deploymentId !== deploymentId ||
|
|
22
|
+
context.sub.startsWith(ARTIFACT_SERVICE_ACCOUNT_SUB_PREFIX))
|
|
23
|
+
return null;
|
|
24
|
+
// Self-binding is safe only for this prefetch: ordinary admission binds the stored project.
|
|
25
|
+
return parseV1Context({ payload: context, projectId: context.projectId, environment, now });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
5
31
|
/**
|
|
6
32
|
* Version-dispatching verifier for platform code that must accept both Atlas-user (v1) and
|
|
7
33
|
* external (v2) contexts, such as the broker. Applications never get this: it is not exported
|
package/dist/source-resolver.js
CHANGED
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { createScrubbedChildEnv } from './child-environment.js';
|
|
7
7
|
import { materializeRepo, resolveCommitSha, resolveRepo } from './git-service-client.js';
|
|
8
|
-
import { gitServiceCloneUrl,
|
|
8
|
+
import { gitServiceCloneUrl, runGitClone } from './git-clone.js';
|
|
9
9
|
/** True for sources that must be fetched from a remote (git-service / git-url). */
|
|
10
10
|
export function isRemoteSpec(spec) {
|
|
11
11
|
return spec.kind !== 'local';
|
|
@@ -136,21 +136,12 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
136
136
|
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, ${count} file(s), M2M)`);
|
|
137
137
|
return await materialized({ gitCommit: sha, gitBranch: ref, gitDirty: false });
|
|
138
138
|
}
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (pat) {
|
|
146
|
-
const cloneUrl = gitServiceCloneUrl(opts.baseUrl, repo.id);
|
|
147
|
-
await runGitClone({ cloneUrl, destDir: dest, pat, ref: sha });
|
|
148
|
-
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, git clone)`);
|
|
149
|
-
return await materialized({ gitCommit: sha, gitBranch: ref, gitDirty: false });
|
|
150
|
-
}
|
|
151
|
-
throw new Error('Deploying from --repo requires a git PAT. Set ATLAS_GIT_PAT to a token with the ' +
|
|
152
|
-
'repo:read scope (create one in Atlas → Settings → Tokens, or `seq-studio auth pat create`). ' +
|
|
153
|
-
'The git-service clone uses smart-HTTP, which authenticates by PAT — the same token you clone the repo with.');
|
|
139
|
+
// Use the same selected human identity for metadata and Git transport.
|
|
140
|
+
// An ambient PAT must not silently change the principal on the clone.
|
|
141
|
+
const cloneUrl = gitServiceCloneUrl(opts.baseUrl, repo.id);
|
|
142
|
+
await runGitClone({ cloneUrl, destDir: dest, pat: opts.token, ref: sha });
|
|
143
|
+
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, git clone)`);
|
|
144
|
+
return await materialized({ gitCommit: sha, gitBranch: ref, gitDirty: false });
|
|
154
145
|
}
|
|
155
146
|
const { sha, branch } = cloneGitUrl(spec.url, spec.ref, dest);
|
|
156
147
|
console.log(`[seq-studio] source: ${redactGitUrl(spec.url)}${spec.ref ? `@${spec.ref}` : ''} (${sha.slice(0, 10)})`);
|
|
@@ -179,6 +179,13 @@ own test credentials, mocks or an existing governed function. Use a hosted
|
|
|
179
179
|
preview for application code that needs a managed secret. Never put secrets in
|
|
180
180
|
`build.public_env`, `NEXT_PUBLIC_*`, client components or committed files.
|
|
181
181
|
|
|
182
|
+
Cookies your application sets itself should use a `__Host-` name with `Secure`,
|
|
183
|
+
`HttpOnly`, `Path=/` and no `Domain`. Applications on one deployment share a
|
|
184
|
+
registrable domain, and only a `__Host-` cookie is guaranteed to come from your
|
|
185
|
+
own origin rather than a neighbour's. When the application origin is on a
|
|
186
|
+
different site from its Atlas frame, a cookie it needs there must also be
|
|
187
|
+
`SameSite=None; Partitioned`, or the browser drops it.
|
|
188
|
+
|
|
182
189
|
The local process is ordinary code running as your operating-system user.
|
|
183
190
|
Atlas governs access to platform resources; it does not sandbox your laptop's
|
|
184
191
|
filesystem or all outbound network connections.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@sequenceholdings/artifact-studio": "^0.
|
|
12
|
+
"@sequenceholdings/artifact-studio": "^0.7.0",
|
|
13
13
|
"@sequenceholdings/atlas-ui": "^3.1.1",
|
|
14
14
|
"@tailwindcss/postcss": "^4.2.2",
|
|
15
15
|
"@tailwindcss/vite": "^4.2.2",
|
|
@@ -7,7 +7,14 @@ export declare const PNPM_WORKSPACE_FILE_RE: RegExp;
|
|
|
7
7
|
/** Platform peers the CLI aliases at build time — must not be prod-installed remotely. */
|
|
8
8
|
export declare const REMOTE_INSTALL_PLATFORM_PACKAGES: Set<string>;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* `pnpm install`
|
|
10
|
+
* Trust gate for remote static React/Vite artifact installs (called by
|
|
11
|
+
* `prepare-build.ts` before `pnpm install`); also reused for remote process repos.
|
|
12
|
+
* It rejects install-control files and non-registry dependency specs.
|
|
13
|
+
*
|
|
14
|
+
* Hosted Next.js builds use `prepareHostedBuildSource` in Atlas
|
|
15
|
+
* `next-hosted/source-policy.ts` and a controlled install workspace from
|
|
16
|
+
* `hostedInstallWorkspace` in `next-hosted/release-age.ts` instead. Do not apply
|
|
17
|
+
* this static gate to a Next.js template: its workspace file and approved
|
|
18
|
+
* platform dependencies follow the hosted policy.
|
|
12
19
|
*/
|
|
13
20
|
export declare function assertTrustedArtifactInstallTree(dir: string): void;
|
package/dist/trusted-install.js
CHANGED
|
@@ -225,8 +225,15 @@ function assertChainguardLockfile(dir) {
|
|
|
225
225
|
assertParsedPackageResolutions(lockfileText);
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
228
|
-
*
|
|
229
|
-
* `pnpm install`
|
|
228
|
+
* Trust gate for remote static React/Vite artifact installs (called by
|
|
229
|
+
* `prepare-build.ts` before `pnpm install`); also reused for remote process repos.
|
|
230
|
+
* It rejects install-control files and non-registry dependency specs.
|
|
231
|
+
*
|
|
232
|
+
* Hosted Next.js builds use `prepareHostedBuildSource` in Atlas
|
|
233
|
+
* `next-hosted/source-policy.ts` and a controlled install workspace from
|
|
234
|
+
* `hostedInstallWorkspace` in `next-hosted/release-age.ts` instead. Do not apply
|
|
235
|
+
* this static gate to a Next.js template: its workspace file and approved
|
|
236
|
+
* platform dependencies follow the hosted policy.
|
|
230
237
|
*/
|
|
231
238
|
export function assertTrustedArtifactInstallTree(dir) {
|
|
232
239
|
for (const rel of listRelativeFiles(dir)) {
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.7.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": {
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
"server-only": "0.0.1",
|
|
168
168
|
"parse5": "7.3.0",
|
|
169
169
|
"@sequenceholdings/lattice-form-renderer": "0.2.0",
|
|
170
|
-
"@sequenceholdings/
|
|
171
|
-
"@sequenceholdings/
|
|
170
|
+
"@sequenceholdings/storage": "0.1.1",
|
|
171
|
+
"@sequenceholdings/orm": "2.6.0"
|
|
172
172
|
},
|
|
173
173
|
"devDependencies": {
|
|
174
174
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -179,6 +179,13 @@ own test credentials, mocks or an existing governed function. Use a hosted
|
|
|
179
179
|
preview for application code that needs a managed secret. Never put secrets in
|
|
180
180
|
`build.public_env`, `NEXT_PUBLIC_*`, client components or committed files.
|
|
181
181
|
|
|
182
|
+
Cookies your application sets itself should use a `__Host-` name with `Secure`,
|
|
183
|
+
`HttpOnly`, `Path=/` and no `Domain`. Applications on one deployment share a
|
|
184
|
+
registrable domain, and only a `__Host-` cookie is guaranteed to come from your
|
|
185
|
+
own origin rather than a neighbour's. When the application origin is on a
|
|
186
|
+
different site from its Atlas frame, a cookie it needs there must also be
|
|
187
|
+
`SameSite=None; Partitioned`, or the browser drops it.
|
|
188
|
+
|
|
182
189
|
The local process is ordinary code running as your operating-system user.
|
|
183
190
|
Atlas governs access to platform resources; it does not sandbox your laptop's
|
|
184
191
|
filesystem or all outbound network connections.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@sequenceholdings/artifact-studio": "^0.
|
|
12
|
+
"@sequenceholdings/artifact-studio": "^0.7.0",
|
|
13
13
|
"@sequenceholdings/atlas-ui": "^3.1.1",
|
|
14
14
|
"@tailwindcss/postcss": "^4.2.2",
|
|
15
15
|
"@tailwindcss/vite": "^4.2.2",
|