@sequenceholdings/artifact-studio 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION-PIN.md +12 -1
- package/dist/api.d.ts +11 -1
- package/dist/api.js +26 -9
- package/dist/build.d.ts +2 -2
- package/dist/build.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -13
- package/dist/dev-link.d.ts +1 -1
- package/dist/dev-link.js +2 -1
- package/dist/gateway/admission.d.ts +56 -0
- package/dist/gateway/admission.js +134 -0
- package/dist/gateway/authorization-cache.d.ts +52 -0
- package/dist/gateway/authorization-cache.js +111 -0
- package/dist/gateway/control-admission.d.ts +48 -0
- package/dist/gateway/control-admission.js +127 -0
- package/dist/gateway/control.d.ts +94 -0
- package/dist/gateway/control.js +116 -0
- package/dist/gateway/hosted-proxy.d.ts +36 -0
- package/dist/gateway/hosted-proxy.js +344 -0
- package/dist/gateway/release-admission.d.ts +23 -0
- package/dist/gateway/release-admission.js +259 -0
- package/dist/gateway/server.d.ts +38 -0
- package/dist/gateway/server.js +382 -0
- package/dist/gateway/session.d.ts +72 -0
- package/dist/gateway/session.js +144 -0
- package/dist/gateway/usage.d.ts +70 -0
- package/dist/gateway/usage.js +116 -0
- package/dist/gateway-main.d.ts +1 -0
- package/dist/gateway-main.js +114 -0
- package/dist/git-clone.d.ts +23 -1
- package/dist/git-clone.js +32 -15
- package/dist/manifest-next.d.ts +217 -0
- package/dist/manifest-next.js +132 -0
- package/dist/manifest-static.d.ts +76 -0
- package/dist/manifest-static.js +124 -0
- package/dist/manifest.d.ts +118 -13
- package/dist/manifest.js +12 -119
- package/dist/next/bridge.d.ts +47 -0
- package/dist/next/bridge.js +236 -0
- package/dist/next/browser-logs.d.ts +7 -0
- package/dist/next/browser-logs.js +51 -0
- package/dist/next/browser.d.ts +6 -0
- package/dist/next/browser.js +57 -0
- package/dist/next/protocol.d.ts +73 -0
- package/dist/next/protocol.js +56 -0
- package/dist/next/release.d.ts +15 -0
- package/dist/next/release.js +46 -0
- package/dist/next/requests.d.ts +15 -0
- package/dist/next/requests.js +59 -0
- package/dist/next/routes.d.ts +4 -0
- package/dist/next/routes.js +21 -0
- package/dist/next/streams.d.ts +16 -0
- package/dist/next/streams.js +132 -0
- package/dist/next/theme.d.ts +10 -0
- package/dist/next/theme.js +45 -0
- package/dist/next/transport.d.ts +15 -0
- package/dist/next/transport.js +13 -0
- package/dist/next-build.d.ts +29 -0
- package/dist/next-build.js +129 -0
- package/dist/next-cache/adapter-runtime.d.ts +51 -0
- package/dist/next-cache/adapter-runtime.js +73 -0
- package/dist/next-cache/component-handler.d.ts +3 -0
- package/dist/next-cache/component-handler.js +10 -0
- package/dist/next-cache/components.d.ts +20 -0
- package/dist/next-cache/components.js +113 -0
- package/dist/next-cache/config.d.ts +9 -0
- package/dist/next-cache/config.js +105 -0
- package/dist/next-cache/incremental-handler.d.ts +18 -0
- package/dist/next-cache/incremental-handler.js +17 -0
- package/dist/next-cache/incremental.d.ts +27 -0
- package/dist/next-cache/incremental.js +99 -0
- package/dist/next-cache/index.d.ts +18 -0
- package/dist/next-cache/index.js +16 -0
- package/dist/next-cache/memory-store.d.ts +27 -0
- package/dist/next-cache/memory-store.js +118 -0
- package/dist/next-cache/redis-scripts.d.ts +3 -0
- package/dist/next-cache/redis-scripts.js +85 -0
- package/dist/next-cache/redis-store.d.ts +18 -0
- package/dist/next-cache/redis-store.js +56 -0
- package/dist/next-cache/runtime.d.ts +29 -0
- package/dist/next-cache/runtime.js +63 -0
- package/dist/next-cache/serialization.d.ts +39 -0
- package/dist/next-cache/serialization.js +333 -0
- package/dist/next-cache/store.d.ts +54 -0
- package/dist/next-cache/store.js +45 -0
- package/dist/next-cache/valkey-transport.d.ts +52 -0
- package/dist/next-cache/valkey-transport.js +189 -0
- package/dist/next-dev-control.d.ts +48 -0
- package/dist/next-dev-control.js +63 -0
- package/dist/next-dev-process.d.ts +17 -0
- package/dist/next-dev-process.js +110 -0
- package/dist/next-dev.d.ts +12 -0
- package/dist/next-dev.js +267 -0
- package/dist/next-image-config.d.ts +9 -0
- package/dist/next-image-config.js +101 -0
- package/dist/next-project.d.ts +22 -0
- package/dist/next-project.js +64 -0
- package/dist/next-public-fonts-local.d.ts +7 -0
- package/dist/next-public-fonts-local.js +64 -0
- package/dist/next-public-fonts.d.ts +13 -0
- package/dist/next-public-fonts.js +49 -0
- package/dist/next-source-hash.d.ts +14 -0
- package/dist/next-source-hash.js +18 -0
- package/dist/next-source.d.ts +35 -0
- package/dist/next-source.js +155 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +5 -0
- package/dist/project.d.ts +3 -0
- package/dist/project.js +9 -1
- package/dist/runtime-contracts.d.ts +229 -0
- package/dist/runtime-contracts.js +224 -0
- package/dist/sdk.d.ts +2 -2
- package/dist/server/broker-contract.d.ts +68 -0
- package/dist/server/broker-contract.js +33 -0
- package/dist/server/client.d.ts +81 -0
- package/dist/server/client.js +159 -0
- package/dist/server/logger.d.ts +26 -0
- package/dist/server/logger.js +24 -0
- package/dist/server/release-forms.d.ts +13 -0
- package/dist/server/release-forms.js +124 -0
- package/dist/server/request-context.d.ts +75 -0
- package/dist/server/request-context.js +107 -0
- package/dist/server/runtime-config.d.ts +54 -0
- package/dist/server/runtime-config.js +64 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +4 -0
- package/dist/templates/nextjs/.artifactignore +14 -0
- package/dist/templates/nextjs/README.md +157 -0
- package/dist/templates/nextjs/app/about/page.tsx +26 -0
- package/dist/templates/nextjs/app/actions.ts +18 -0
- package/dist/templates/nextjs/app/api/health/route.ts +6 -0
- package/dist/templates/nextjs/app/components/counter.tsx +19 -0
- package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/dist/templates/nextjs/app/globals.css +4 -0
- package/dist/templates/nextjs/app/layout.tsx +27 -0
- package/dist/templates/nextjs/app/page.tsx +51 -0
- package/dist/templates/nextjs/app/providers.tsx +107 -0
- package/dist/templates/nextjs/artifact.bundle.yml +55 -0
- package/dist/templates/nextjs/gitignore +9 -0
- package/dist/templates/nextjs/next.config.mjs +27 -0
- package/dist/templates/nextjs/package.json +32 -0
- package/dist/templates/nextjs/pnpm-workspace.yaml +22 -0
- package/dist/templates/nextjs/postcss.config.mjs +5 -0
- package/dist/templates/nextjs/tsconfig.json +20 -0
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/trusted-install.js +62 -2
- package/dist/tunnel/broker.d.ts +75 -0
- package/dist/tunnel/broker.js +463 -0
- package/dist/tunnel/client.d.ts +22 -0
- package/dist/tunnel/client.js +177 -0
- package/dist/tunnel/index.d.ts +6 -0
- package/dist/tunnel/index.js +4 -0
- package/dist/tunnel/io.d.ts +33 -0
- package/dist/tunnel/io.js +108 -0
- package/dist/tunnel/peer.d.ts +25 -0
- package/dist/tunnel/peer.js +147 -0
- package/dist/tunnel/protocol.d.ts +78 -0
- package/dist/tunnel/protocol.js +210 -0
- package/dist/tunnel/redis-auth.d.ts +37 -0
- package/dist/tunnel/redis-auth.js +92 -0
- package/dist/tunnel/redis-ownership.d.ts +38 -0
- package/dist/tunnel/redis-ownership.js +100 -0
- package/dist/tunnel/redis-socket.d.ts +13 -0
- package/dist/tunnel/redis-socket.js +48 -0
- package/dist/tunnel/redis-types.d.ts +75 -0
- package/dist/tunnel/redis-types.js +9 -0
- package/dist/tunnel/redis.d.ts +56 -0
- package/dist/tunnel/redis.js +404 -0
- package/dist/tunnel/transport.d.ts +17 -0
- package/dist/tunnel/transport.js +1 -0
- package/package.json +67 -4
- package/templates/nextjs/.artifactignore +14 -0
- package/templates/nextjs/README.md +157 -0
- package/templates/nextjs/app/about/page.tsx +26 -0
- package/templates/nextjs/app/actions.ts +18 -0
- package/templates/nextjs/app/api/health/route.ts +6 -0
- package/templates/nextjs/app/components/counter.tsx +19 -0
- package/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/templates/nextjs/app/globals.css +4 -0
- package/templates/nextjs/app/layout.tsx +27 -0
- package/templates/nextjs/app/page.tsx +51 -0
- package/templates/nextjs/app/providers.tsx +107 -0
- package/templates/nextjs/artifact.bundle.yml +55 -0
- package/templates/nextjs/gitignore +9 -0
- package/templates/nextjs/next.config.mjs +27 -0
- package/templates/nextjs/package.json +32 -0
- package/templates/nextjs/pnpm-workspace.yaml +22 -0
- package/templates/nextjs/postcss.config.mjs +5 -0
- package/templates/nextjs/tsconfig.json +20 -0
- package/templates/react-vite/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ARTIFACT_NEXT_RUNTIME, artifactNextSecretNameSchema } from './manifest-next.js';
|
|
3
|
+
import { MAX_STUDIO_PATH_LENGTH } from './paths.js';
|
|
4
|
+
export { resolveArtifactNextImages } from './next-image-config.js';
|
|
5
|
+
/** Shared by local collection, repository collection and the bounded source-upload API. */
|
|
6
|
+
export const ARTIFACT_NEXT_SOURCE_LIMITS = Object.freeze({
|
|
7
|
+
maxFiles: 2_000,
|
|
8
|
+
maxFileBytes: 10 * 1024 * 1024,
|
|
9
|
+
maxTotalBytes: 32 * 1024 * 1024,
|
|
10
|
+
});
|
|
11
|
+
const identifierSchema = z.string().min(1).max(256).regex(/^[^\s\u0000-\u001f\u007f]+$/);
|
|
12
|
+
const timestampSchema = z.iso.datetime();
|
|
13
|
+
const exactVersionSchema = z.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, 'An exact, stable toolchain version is required');
|
|
14
|
+
export const artifactSha256DigestSchema = z.string().regex(/^[a-f0-9]{64}$/, 'Expected a lowercase SHA-256 digest');
|
|
15
|
+
/** Immutable Next deploymentId from compiled output; distinct from the Atlas activation ID. */
|
|
16
|
+
export const artifactNextFrameworkBuildIdSchema = z.string().min(1).max(128).refine((value) => !/[^A-Za-z0-9_-]/.test(value), 'Expected a canonical Next.js deploymentId containing only letters, digits, underscores and hyphens');
|
|
17
|
+
const imageDigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/, 'Expected a SHA-256 image digest');
|
|
18
|
+
const imageReferenceSchema = z.string().max(1024).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*(?::[1-9][0-9]{0,4})?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+@sha256:[a-f0-9]{64}$/, 'Expected a registry image reference pinned by digest');
|
|
19
|
+
const excludedSourceSegments = new Set([
|
|
20
|
+
'.git', 'node_modules', '.next', '.npmrc', '.netrc', '.aws', '.ssh', '.config',
|
|
21
|
+
'dist', '.artifact-studio', '.git-credentials', '.pypirc', '.azure', '.gcloud', '.doppler',
|
|
22
|
+
'.turbo', 'coverage', '.docker',
|
|
23
|
+
]);
|
|
24
|
+
/** Shared upload exclusion policy. Content scanning and filesystem containment remain separate checks. */
|
|
25
|
+
export function isExcludedArtifactSourcePath({ path }) {
|
|
26
|
+
return path.split('/').some((segment) => {
|
|
27
|
+
const normalized = segment.toLowerCase();
|
|
28
|
+
return excludedSourceSegments.has(normalized)
|
|
29
|
+
|| normalized.startsWith('.env') || normalized.startsWith('.yarnrc')
|
|
30
|
+
|| /(?:^|[-_.])(?:credentials?|service[-_]?account)(?:[-_.].*)?\.json$/.test(normalized)
|
|
31
|
+
|| /\.(pem|key|p12|pfx|jks)$/.test(normalized);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** POSIX source paths only; filesystem readers must additionally reject symlink escapes. */
|
|
35
|
+
export const artifactSourcePathSchema = z.string().min(1).max(MAX_STUDIO_PATH_LENGTH).refine((path) => {
|
|
36
|
+
if (path !== path.trim() || /[\\%:\u0000-\u001f\u007f]/.test(path))
|
|
37
|
+
return false;
|
|
38
|
+
return !isExcludedArtifactSourcePath({ path })
|
|
39
|
+
&& path.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
40
|
+
}, 'Source paths must be relative and exclude credentials, dependencies and build output');
|
|
41
|
+
/** An exact browser origin, not a URL prefix or an allowlist wildcard. */
|
|
42
|
+
export const artifactRuntimeOriginSchema = z.string().max(2048).refine((value) => {
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(value);
|
|
45
|
+
return url.protocol === 'https:' && url.origin === value && !url.hostname.includes('*');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}, 'Expected an exact, canonical HTTPS origin');
|
|
51
|
+
const sourceFileSchema = z.strictObject({
|
|
52
|
+
path: artifactSourcePathSchema,
|
|
53
|
+
sha256: artifactSha256DigestSchema,
|
|
54
|
+
sizeBytes: z.number().int().nonnegative().max(ARTIFACT_NEXT_SOURCE_LIMITS.maxFileBytes),
|
|
55
|
+
});
|
|
56
|
+
/** Metadata points to an immutable snapshot; file bytes travel through the source-upload API. */
|
|
57
|
+
export const artifactNextBuildInputSchema = z.strictObject({
|
|
58
|
+
schemaVersion: z.literal(1),
|
|
59
|
+
framework: z.literal('nextjs'),
|
|
60
|
+
projectId: identifierSchema,
|
|
61
|
+
source: z.strictObject({
|
|
62
|
+
snapshotId: identifierSchema,
|
|
63
|
+
sha256: artifactSha256DigestSchema,
|
|
64
|
+
git: z.strictObject({
|
|
65
|
+
repositoryId: identifierSchema,
|
|
66
|
+
commitSha: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/, 'Expected a full Git commit SHA'),
|
|
67
|
+
}),
|
|
68
|
+
files: z.array(sourceFileSchema).min(1).max(ARTIFACT_NEXT_SOURCE_LIMITS.maxFiles),
|
|
69
|
+
}),
|
|
70
|
+
lockfile: z.strictObject({ path: z.literal('pnpm-lock.yaml'), sha256: artifactSha256DigestSchema }),
|
|
71
|
+
toolchain: z.strictObject({
|
|
72
|
+
node: exactVersionSchema.refine((version) => version.startsWith(`${ARTIFACT_NEXT_RUNTIME.node}.`), 'Unsupported Node runtime'),
|
|
73
|
+
next: exactVersionSchema.refine((version) => version.startsWith(`${ARTIFACT_NEXT_RUNTIME.next}.`), 'Unsupported Next.js release line'),
|
|
74
|
+
react: exactVersionSchema.refine((version) => version.startsWith(`${ARTIFACT_NEXT_RUNTIME.react}.`), 'Unsupported React release line'),
|
|
75
|
+
packageManager: z.strictObject({ name: z.literal('pnpm'), version: exactVersionSchema }),
|
|
76
|
+
builderImageDigest: imageDigestSchema,
|
|
77
|
+
}),
|
|
78
|
+
}).superRefine((value, ctx) => {
|
|
79
|
+
if (value.source.files.reduce((total, file) => total + file.sizeBytes, 0) > ARTIFACT_NEXT_SOURCE_LIMITS.maxTotalBytes) {
|
|
80
|
+
ctx.addIssue({ code: 'custom', path: ['source', 'files'], message: 'Source snapshot exceeds the upload size limit' });
|
|
81
|
+
}
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
for (const [index, file] of value.source.files.entries()) {
|
|
84
|
+
if (seen.has(file.path)) {
|
|
85
|
+
ctx.addIssue({ code: 'custom', path: ['source', 'files', index, 'path'], message: 'Duplicate source path' });
|
|
86
|
+
}
|
|
87
|
+
seen.add(file.path);
|
|
88
|
+
}
|
|
89
|
+
for (const [index, file] of value.source.files.entries()) {
|
|
90
|
+
const segments = file.path.split('/');
|
|
91
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
92
|
+
if (seen.has(segments.slice(0, depth).join('/'))) {
|
|
93
|
+
ctx.addIssue({ code: 'custom', path: ['source', 'files', index, 'path'], message: 'A source file cannot also be a directory' });
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const lockfile = value.source.files.find((file) => file.path === value.lockfile.path);
|
|
99
|
+
if (!lockfile || lockfile.sha256 !== value.lockfile.sha256) {
|
|
100
|
+
ctx.addIssue({ code: 'custom', path: ['lockfile'], message: 'The pinned lockfile must match a file in the source snapshot' });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const immutableObjectSchema = z.strictObject({ objectId: identifierSchema, sha256: artifactSha256DigestSchema });
|
|
104
|
+
export const artifactDeploymentOutputSchema = z.discriminatedUnion('kind', [
|
|
105
|
+
z.strictObject({ kind: z.literal('static'), bundle: immutableObjectSchema }),
|
|
106
|
+
z.strictObject({
|
|
107
|
+
kind: z.literal('next-server'),
|
|
108
|
+
frameworkBuildId: artifactNextFrameworkBuildIdSchema,
|
|
109
|
+
imageRef: imageReferenceSchema,
|
|
110
|
+
assets: immutableObjectSchema,
|
|
111
|
+
buildInputSha256: artifactSha256DigestSchema,
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
const operationFields = {
|
|
115
|
+
id: identifierSchema,
|
|
116
|
+
projectId: identifierSchema,
|
|
117
|
+
environment: identifierSchema,
|
|
118
|
+
kind: z.enum(['build', 'deploy', 'preview', 'rollback', 'suspend', 'delete']),
|
|
119
|
+
deploymentId: identifierSchema.optional(),
|
|
120
|
+
createdAt: timestampSchema,
|
|
121
|
+
updatedAt: timestampSchema,
|
|
122
|
+
};
|
|
123
|
+
const operationProgressSchema = z.strictObject({
|
|
124
|
+
stage: z.string().min(1).max(128),
|
|
125
|
+
percent: z.number().min(0).max(100).optional(),
|
|
126
|
+
});
|
|
127
|
+
export const artifactOperationSchema = z.discriminatedUnion('status', [
|
|
128
|
+
z.strictObject({ ...operationFields, status: z.literal('queued') }),
|
|
129
|
+
z.strictObject({ ...operationFields, status: z.literal('running'), progress: operationProgressSchema.optional() }),
|
|
130
|
+
z.strictObject({ ...operationFields, status: z.literal('succeeded'), completedAt: timestampSchema }),
|
|
131
|
+
z.strictObject({
|
|
132
|
+
...operationFields,
|
|
133
|
+
status: z.literal('failed'),
|
|
134
|
+
completedAt: timestampSchema,
|
|
135
|
+
error: z.strictObject({ code: identifierSchema, message: z.string().min(1).max(4096) }),
|
|
136
|
+
}),
|
|
137
|
+
z.strictObject({ ...operationFields, status: z.literal('cancelled'), completedAt: timestampSchema }),
|
|
138
|
+
]).superRefine((value, ctx) => {
|
|
139
|
+
if (Date.parse(value.updatedAt) < Date.parse(value.createdAt)) {
|
|
140
|
+
ctx.addIssue({ code: 'custom', path: ['updatedAt'], message: 'An operation cannot be updated before it was created' });
|
|
141
|
+
}
|
|
142
|
+
if ('completedAt' in value && (Date.parse(value.completedAt) < Date.parse(value.createdAt)
|
|
143
|
+
|| Date.parse(value.completedAt) > Date.parse(value.updatedAt))) {
|
|
144
|
+
ctx.addIssue({ code: 'custom', path: ['completedAt'], message: 'Completion must be between creation and the last update' });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
/** Runtime bootstrap is ephemeral. Do not persist bootstrap URLs in deployment history. */
|
|
148
|
+
export const artifactRuntimeSessionSchema = z.strictObject({
|
|
149
|
+
id: identifierSchema,
|
|
150
|
+
projectId: identifierSchema,
|
|
151
|
+
environment: identifierSchema,
|
|
152
|
+
mode: z.enum(['local', 'preview', 'published']),
|
|
153
|
+
origin: artifactRuntimeOriginSchema,
|
|
154
|
+
parentOrigin: artifactRuntimeOriginSchema,
|
|
155
|
+
bootstrapUrl: z.url().max(4096),
|
|
156
|
+
bridge: z.strictObject({ protocolVersion: z.literal(1), channelId: identifierSchema }),
|
|
157
|
+
expiresAt: timestampSchema,
|
|
158
|
+
}).superRefine((value, ctx) => {
|
|
159
|
+
let validBootstrap = false;
|
|
160
|
+
try {
|
|
161
|
+
const url = new URL(value.bootstrapUrl);
|
|
162
|
+
validBootstrap = url.origin === value.origin && !url.username && !url.password && !url.hash;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// URL syntax failures are also reported by the field schema; never throw from safeParse.
|
|
166
|
+
}
|
|
167
|
+
if (!validBootstrap) {
|
|
168
|
+
ctx.addIssue({ code: 'custom', path: ['bootstrapUrl'], message: 'Bootstrap must use the exact runtime origin without credentials or a fragment' });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
/** Public session metadata contains no tunnel bearer, local credentials or managed secrets. */
|
|
172
|
+
export const artifactLocalDevSessionSchema = z.strictObject({
|
|
173
|
+
id: identifierSchema,
|
|
174
|
+
ownerId: identifierSchema,
|
|
175
|
+
projectId: identifierSchema,
|
|
176
|
+
environment: identifierSchema,
|
|
177
|
+
origin: artifactRuntimeOriginSchema,
|
|
178
|
+
sourceRevision: artifactSha256DigestSchema.optional(),
|
|
179
|
+
revision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
180
|
+
state: z.enum(['starting', 'compiling', 'ready', 'reconnecting', 'failed', 'offline', 'revoked', 'expired']),
|
|
181
|
+
lease: z.strictObject({
|
|
182
|
+
id: identifierSchema,
|
|
183
|
+
fence: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
184
|
+
expiresAt: timestampSchema,
|
|
185
|
+
}),
|
|
186
|
+
createdAt: timestampSchema,
|
|
187
|
+
updatedAt: timestampSchema,
|
|
188
|
+
expiresAt: timestampSchema,
|
|
189
|
+
}).superRefine((value, ctx) => {
|
|
190
|
+
const created = Date.parse(value.createdAt);
|
|
191
|
+
const updated = Date.parse(value.updatedAt);
|
|
192
|
+
const expires = Date.parse(value.expiresAt);
|
|
193
|
+
const leaseExpires = Date.parse(value.lease.expiresAt);
|
|
194
|
+
if (updated < created || expires <= created || leaseExpires <= created || leaseExpires > expires) {
|
|
195
|
+
ctx.addIssue({ code: 'custom', path: ['lease', 'expiresAt'], message: 'Session timestamps must be ordered and the lease cannot outlive its session' });
|
|
196
|
+
}
|
|
197
|
+
if (['starting', 'compiling', 'ready', 'reconnecting'].includes(value.state) && updated >= leaseExpires) {
|
|
198
|
+
ctx.addIssue({ code: 'custom', path: ['updatedAt'], message: 'An active session must have an unexpired lease at its last update' });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
export const artifactActivationSchema = z.strictObject({
|
|
202
|
+
id: identifierSchema,
|
|
203
|
+
projectId: identifierSchema,
|
|
204
|
+
environment: identifierSchema,
|
|
205
|
+
deploymentId: identifierSchema,
|
|
206
|
+
configurationSha256: artifactSha256DigestSchema,
|
|
207
|
+
policySha256: artifactSha256DigestSchema,
|
|
208
|
+
secretVersions: z.array(z.strictObject({
|
|
209
|
+
binding: artifactNextSecretNameSchema,
|
|
210
|
+
secretId: identifierSchema,
|
|
211
|
+
version: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
212
|
+
})).max(100),
|
|
213
|
+
cacheGeneration: identifierSchema,
|
|
214
|
+
activatedAt: timestampSchema,
|
|
215
|
+
activatedBy: identifierSchema,
|
|
216
|
+
}).superRefine((value, ctx) => {
|
|
217
|
+
const bindings = new Set();
|
|
218
|
+
for (const [index, secret] of value.secretVersions.entries()) {
|
|
219
|
+
if (bindings.has(secret.binding)) {
|
|
220
|
+
ctx.addIssue({ code: 'custom', path: ['secretVersions', index, 'binding'], message: 'Secret binding names must be unique' });
|
|
221
|
+
}
|
|
222
|
+
bindings.add(secret.binding);
|
|
223
|
+
}
|
|
224
|
+
});
|
package/dist/sdk.d.ts
CHANGED
|
@@ -161,7 +161,7 @@ export declare function createArtifactStorageClient(sdk?: SequenceArtifactSdk):
|
|
|
161
161
|
fileId: string;
|
|
162
162
|
uploadUrl: string | null;
|
|
163
163
|
expiresAt: string;
|
|
164
|
-
state: "aborted" | "
|
|
164
|
+
state: "aborted" | "complete" | "pending";
|
|
165
165
|
versionId: string | null;
|
|
166
166
|
maxBytes: number;
|
|
167
167
|
}>;
|
|
@@ -170,7 +170,7 @@ export declare function createArtifactStorageClient(sdk?: SequenceArtifactSdk):
|
|
|
170
170
|
fileId: string;
|
|
171
171
|
uploadUrl: string | null;
|
|
172
172
|
expiresAt: string;
|
|
173
|
-
state: "aborted" | "
|
|
173
|
+
state: "aborted" | "complete" | "pending";
|
|
174
174
|
versionId: string | null;
|
|
175
175
|
maxBytes: number;
|
|
176
176
|
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const artifactServerMethodSchema: z.ZodEnum<{
|
|
3
|
+
GET: "GET";
|
|
4
|
+
POST: "POST";
|
|
5
|
+
DELETE: "DELETE";
|
|
6
|
+
HEAD: "HEAD";
|
|
7
|
+
PUT: "PUT";
|
|
8
|
+
PATCH: "PATCH";
|
|
9
|
+
}>;
|
|
10
|
+
export declare const artifactServerModeSchema: z.ZodDefault<z.ZodEnum<{
|
|
11
|
+
user: "user";
|
|
12
|
+
service: "service";
|
|
13
|
+
}>>;
|
|
14
|
+
/** Reject paths that intermediaries can normalize into a different authorization target. */
|
|
15
|
+
export declare function isArtifactApiPath(path: string): boolean;
|
|
16
|
+
/** Atlas validates these operations against current manifest, bindings and principal grants. */
|
|
17
|
+
export declare const artifactServerRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
18
|
+
operation: z.ZodLiteral<"api">;
|
|
19
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
20
|
+
user: "user";
|
|
21
|
+
service: "service";
|
|
22
|
+
}>>;
|
|
23
|
+
method: z.ZodEnum<{
|
|
24
|
+
GET: "GET";
|
|
25
|
+
POST: "POST";
|
|
26
|
+
DELETE: "DELETE";
|
|
27
|
+
HEAD: "HEAD";
|
|
28
|
+
PUT: "PUT";
|
|
29
|
+
PATCH: "PATCH";
|
|
30
|
+
}>;
|
|
31
|
+
path: z.ZodString;
|
|
32
|
+
body: z.ZodOptional<z.ZodUnknown>;
|
|
33
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
|
+
operation: z.ZodLiteral<"orm">;
|
|
35
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
36
|
+
user: "user";
|
|
37
|
+
service: "service";
|
|
38
|
+
}>>;
|
|
39
|
+
namespace: z.ZodString;
|
|
40
|
+
op: z.ZodString;
|
|
41
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
42
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
43
|
+
operation: z.ZodLiteral<"storage">;
|
|
44
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
45
|
+
user: "user";
|
|
46
|
+
service: "service";
|
|
47
|
+
}>>;
|
|
48
|
+
method: z.ZodEnum<{
|
|
49
|
+
GET: "GET";
|
|
50
|
+
POST: "POST";
|
|
51
|
+
DELETE: "DELETE";
|
|
52
|
+
HEAD: "HEAD";
|
|
53
|
+
PUT: "PUT";
|
|
54
|
+
PATCH: "PATCH";
|
|
55
|
+
}>;
|
|
56
|
+
path: z.ZodString;
|
|
57
|
+
body: z.ZodOptional<z.ZodUnknown>;
|
|
58
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
59
|
+
operation: z.ZodLiteral<"function">;
|
|
60
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
61
|
+
user: "user";
|
|
62
|
+
service: "service";
|
|
63
|
+
}>>;
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
66
|
+
}, z.core.$strict>], "operation">;
|
|
67
|
+
export type ArtifactServerRequest = z.infer<typeof artifactServerRequestSchema>;
|
|
68
|
+
export type ArtifactServerMode = z.infer<typeof artifactServerModeSchema>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const artifactServerMethodSchema = z.enum(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
3
|
+
export const artifactServerModeSchema = z.enum(['user', 'service']).default('user');
|
|
4
|
+
/** Reject paths that intermediaries can normalize into a different authorization target. */
|
|
5
|
+
export function isArtifactApiPath(path) {
|
|
6
|
+
if (!path.startsWith('/api/') || path.startsWith('//') || /[\\\r\n\u0000#]/.test(path))
|
|
7
|
+
return false;
|
|
8
|
+
const pathname = path.split('?')[0];
|
|
9
|
+
if (/%(?:2e|2f|5c|25)/i.test(pathname) || pathname.includes('//'))
|
|
10
|
+
return false;
|
|
11
|
+
if (pathname.split('/').some(part => part === '.' || part === '..'))
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
return new URL(path, 'https://artifact.invalid').pathname === pathname;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const path = z.string().min(6).max(8192).refine(isArtifactApiPath, 'Expected a canonical Atlas API path');
|
|
21
|
+
const method = artifactServerMethodSchema;
|
|
22
|
+
const mode = artifactServerModeSchema;
|
|
23
|
+
const id = z.string().min(1).max(255).regex(/^[a-zA-Z0-9_-]+$/);
|
|
24
|
+
/** Atlas validates these operations against current manifest, bindings and principal grants. */
|
|
25
|
+
export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
26
|
+
z.object({ operation: z.literal('api'), mode, method, path, body: z.unknown().optional() }).strict(),
|
|
27
|
+
z.object({ operation: z.literal('orm'), mode, namespace: id, op: z.string().regex(/^sha256:[a-f0-9]{64}$/), variables: z.record(z.string(), z.unknown()).optional() }).strict(),
|
|
28
|
+
z.object({ operation: z.literal('storage'), mode, method, path: path.refine(value => {
|
|
29
|
+
const pathname = value.split('?')[0];
|
|
30
|
+
return pathname === '/api/storage' || pathname.startsWith('/api/storage/');
|
|
31
|
+
}, 'Expected a Storage API path'), body: z.unknown().optional() }).strict(),
|
|
32
|
+
z.object({ operation: z.literal('function'), mode, id, params: z.record(z.string(), z.unknown()).optional() }).strict(),
|
|
33
|
+
]);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { storageClient } from '@sequenceholdings/storage';
|
|
2
|
+
import { type OperationExecutor } from '@sequenceholdings/orm/client';
|
|
3
|
+
import type { SequenceApiFetchResponse } from '../sdk.js';
|
|
4
|
+
import { type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
|
|
5
|
+
import { createArtifactLogger, type ArtifactLogRecord } from './logger.js';
|
|
6
|
+
import { type ArtifactRequestContext } from './request-context.js';
|
|
7
|
+
import { type ArtifactRuntimeConfig, type ArtifactRequestHeaders } from './runtime-config.js';
|
|
8
|
+
export declare class ArtifactServerError extends Error {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly code: string;
|
|
11
|
+
constructor(status: number, code: string, message: string);
|
|
12
|
+
}
|
|
13
|
+
export interface ArtifactServerFetchOptions {
|
|
14
|
+
method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
15
|
+
body?: unknown;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The namespace's generated operations.manifest.json. Only each operation's persisted hash
|
|
20
|
+
* is read, so the file can be passed as imported: a JSON import types `kind` as string.
|
|
21
|
+
*/
|
|
22
|
+
export interface ArtifactOrmOperations {
|
|
23
|
+
readonly manifestVersion?: number;
|
|
24
|
+
readonly schemaHash?: string;
|
|
25
|
+
readonly opsHash?: string;
|
|
26
|
+
readonly operations: Readonly<Record<string, {
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
readonly kind?: string;
|
|
29
|
+
readonly requires?: unknown;
|
|
30
|
+
}>>;
|
|
31
|
+
}
|
|
32
|
+
export interface ArtifactServerClient {
|
|
33
|
+
context: ArtifactRequestContext;
|
|
34
|
+
mode: ArtifactServerMode;
|
|
35
|
+
log: ReturnType<typeof createArtifactLogger>;
|
|
36
|
+
api: ReturnType<typeof createApiClient>;
|
|
37
|
+
storage: ReturnType<typeof storageClient>;
|
|
38
|
+
functions: {
|
|
39
|
+
invoke<TOutput = unknown>(id: string, params?: Record<string, unknown>): Promise<TOutput>;
|
|
40
|
+
};
|
|
41
|
+
orm(input: {
|
|
42
|
+
namespace: string;
|
|
43
|
+
manifest: ArtifactOrmOperations;
|
|
44
|
+
}): OperationExecutor;
|
|
45
|
+
asService(): ArtifactServerClient;
|
|
46
|
+
}
|
|
47
|
+
/** One instance per incoming request. Never cache it at module scope. */
|
|
48
|
+
export declare function createArtifactServerClient({ headers, runtime, fetch: fetcher, method, path, logSink }: {
|
|
49
|
+
headers: ArtifactRequestHeaders;
|
|
50
|
+
/** Optional explicit platform config; otherwise reads SEQUENCE_ARTIFACT_RUNTIME_FILE. */
|
|
51
|
+
runtime?: ArtifactRuntimeConfig;
|
|
52
|
+
fetch?: typeof globalThis.fetch;
|
|
53
|
+
method?: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
logSink?: (record: ArtifactLogRecord) => void;
|
|
56
|
+
}): Promise<ArtifactServerClient>;
|
|
57
|
+
type ClientArguments = {
|
|
58
|
+
context: ArtifactRequestContext;
|
|
59
|
+
token: string;
|
|
60
|
+
getRuntime: () => Promise<ArtifactRuntimeConfig>;
|
|
61
|
+
fetcher: typeof globalThis.fetch;
|
|
62
|
+
mode: ArtifactServerMode;
|
|
63
|
+
logSink?: (record: ArtifactLogRecord) => void;
|
|
64
|
+
};
|
|
65
|
+
type BrokerTransport = ReturnType<typeof createBrokerTransport>;
|
|
66
|
+
declare function createBrokerTransport({ context, token, getRuntime, fetcher }: ClientArguments): (input: ArtifactServerRequest, signal?: AbortSignal) => Promise<Response>;
|
|
67
|
+
declare function createApiClient({ mode, request, json }: {
|
|
68
|
+
mode: ArtifactServerMode;
|
|
69
|
+
request: BrokerTransport;
|
|
70
|
+
json: (operation: ArtifactServerRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
71
|
+
}): {
|
|
72
|
+
request: (path: string, options?: ArtifactServerFetchOptions) => Promise<Response>;
|
|
73
|
+
fetch<TBody = unknown>(path: string, options?: ArtifactServerFetchOptions): Promise<SequenceApiFetchResponse<TBody>>;
|
|
74
|
+
stream(path: string, options?: ArtifactServerFetchOptions): Promise<ReadableStream<Uint8Array<ArrayBuffer>>>;
|
|
75
|
+
get: <T = unknown>(path: string) => Promise<T>;
|
|
76
|
+
post: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
77
|
+
put: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
78
|
+
patch: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
79
|
+
delete: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
80
|
+
};
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { StorageApiError, storageClient, storageErrorSchema } from '@sequenceholdings/storage';
|
|
2
|
+
import { OrmOperationError } from '@sequenceholdings/orm/client';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { artifactServerRequestSchema } from './broker-contract.js';
|
|
5
|
+
import { createArtifactLogger } from './logger.js';
|
|
6
|
+
import { ARTIFACT_REQUEST_CONTEXT_HEADER, ARTIFACT_RUNTIME_TOKEN_HEADER, ARTIFACT_SERVER_REQUEST_PATH, verifyArtifactRequestContext } from './request-context.js';
|
|
7
|
+
import { artifactRuntimeConfigSchema, readArtifactRuntimeConfig } from './runtime-config.js';
|
|
8
|
+
export class ArtifactServerError extends Error {
|
|
9
|
+
status;
|
|
10
|
+
code;
|
|
11
|
+
constructor(status, code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = 'ArtifactServerError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const graphqlResponseSchema = z.object({
|
|
19
|
+
data: z.unknown().optional(),
|
|
20
|
+
errors: z.array(z.object({ message: z.string(), extensions: z.object({ code: z.string().optional() }).passthrough().optional() })).optional(),
|
|
21
|
+
});
|
|
22
|
+
const responseHeaders = ['content-type', 'content-disposition', 'content-length', 'retry-after'];
|
|
23
|
+
const maxBodyBytes = 1024 * 1024;
|
|
24
|
+
/** One instance per incoming request. Never cache it at module scope. */
|
|
25
|
+
export async function createArtifactServerClient({ headers, runtime, fetch: fetcher = globalThis.fetch, method, path, logSink }) {
|
|
26
|
+
const config = runtime ? artifactRuntimeConfigSchema.parse(runtime) : await readArtifactRuntimeConfig();
|
|
27
|
+
if (!config)
|
|
28
|
+
throw new ArtifactServerError(401, 'runtime_not_connected', 'Run seq-studio artifact dev to connect platform services, or use a hosted preview.');
|
|
29
|
+
const token = headers.get(ARTIFACT_REQUEST_CONTEXT_HEADER);
|
|
30
|
+
const context = verifyArtifactRequestContext({ token, publicKeys: config.publicKeys, projectId: config.projectId, environment: config.environment, method, path });
|
|
31
|
+
if (!token || (config.devSessionId && context.devSessionId !== config.devSessionId) ||
|
|
32
|
+
(config.deploymentId && context.deploymentId !== config.deploymentId))
|
|
33
|
+
throw new ArtifactServerError(401, 'invalid_runtime_session', 'Artifact request does not belong to this runtime');
|
|
34
|
+
const getRuntime = async () => {
|
|
35
|
+
const current = runtime ? config : await readArtifactRuntimeConfig();
|
|
36
|
+
if (!current || current.projectId !== config.projectId || current.environment !== config.environment || current.atlasUrl !== config.atlasUrl || current.devSessionId !== config.devSessionId || current.deploymentId !== config.deploymentId)
|
|
37
|
+
throw new ArtifactServerError(401, 'runtime_changed', 'Artifact runtime disconnected or changed; reload the preview');
|
|
38
|
+
return current;
|
|
39
|
+
};
|
|
40
|
+
return buildClient({ context, token, getRuntime, fetcher, mode: 'user', logSink });
|
|
41
|
+
}
|
|
42
|
+
function buildClient(args) {
|
|
43
|
+
const { context, mode, logSink } = args;
|
|
44
|
+
const request = createBrokerTransport(args);
|
|
45
|
+
const json = async (operation, signal) => {
|
|
46
|
+
const response = await request(operation, signal);
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new ArtifactServerError(response.status, 'platform_request_failed', `Atlas rejected the ${operation.operation} request (${response.status})`);
|
|
49
|
+
return response.status === 204 ? undefined : response.json();
|
|
50
|
+
};
|
|
51
|
+
// storageClient retries replay-safe calls on 503 permissions_pending / storage_busy, so Storage
|
|
52
|
+
// failures keep the envelope's status and code rather than collapsing into platform_request_failed.
|
|
53
|
+
const storageJson = async (operation) => {
|
|
54
|
+
const response = await request(operation);
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw await storageApiError(response);
|
|
57
|
+
return response.status === 204 ? undefined : response.json();
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
context,
|
|
61
|
+
mode,
|
|
62
|
+
log: createArtifactLogger({ context, sink: logSink }),
|
|
63
|
+
api: createApiClient({ mode, request, json }),
|
|
64
|
+
storage: storageClient({ request: (method, path, body) => storageJson(artifactServerRequestSchema.parse({ operation: 'storage', mode, method, path, body })) }),
|
|
65
|
+
functions: {
|
|
66
|
+
invoke: async (id, params) => {
|
|
67
|
+
// The caller supplies its application's function output type; Atlas owns runtime validation.
|
|
68
|
+
return await json(artifactServerRequestSchema.parse({ operation: 'function', mode, id, params }));
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
orm: ({ namespace, manifest }) => ({
|
|
72
|
+
async execute(op, variables = {}) {
|
|
73
|
+
const hash = op.startsWith('sha256:') ? op : manifest.operations[op]?.hash;
|
|
74
|
+
if (!hash)
|
|
75
|
+
throw new ArtifactServerError(400, 'unknown_orm_operation', 'ORM operation is not in the generated manifest; run seq-studio orm generate');
|
|
76
|
+
const response = await request(artifactServerRequestSchema.parse({ operation: 'orm', mode, namespace, op: hash, variables }));
|
|
77
|
+
const parsed = graphqlResponseSchema.safeParse(await response.json().catch(() => null));
|
|
78
|
+
if (!parsed.success)
|
|
79
|
+
throw new OrmOperationError('Malformed Data API response', response.status, 'invalid_response');
|
|
80
|
+
const first = parsed.data.errors?.[0];
|
|
81
|
+
if (!response.ok || first)
|
|
82
|
+
throw new OrmOperationError(first?.message ?? `Data API request failed (${response.status})`, response.status, first?.extensions?.code ?? null, parsed.data.errors);
|
|
83
|
+
if (parsed.data.data === undefined || parsed.data.data === null)
|
|
84
|
+
throw new OrmOperationError('Malformed Data API response: missing data', response.status, 'invalid_response');
|
|
85
|
+
// Mirrors the generated ORM executor contract: consumers provide their generated result type.
|
|
86
|
+
return parsed.data.data;
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
/** Deliberate escalation, still clamped to a separately attached service identity. */
|
|
90
|
+
asService() {
|
|
91
|
+
if (!context.serviceAccountId)
|
|
92
|
+
throw new ArtifactServerError(403, 'service_identity_unavailable', 'No service identity is attached to this runtime');
|
|
93
|
+
return buildClient({ ...args, mode: 'service' });
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Mirrors createStorageClient: Storage's `{ error: { code, message } }` envelope survives the broker. */
|
|
98
|
+
async function storageApiError(response) {
|
|
99
|
+
const envelope = storageErrorSchema.safeParse(await response.json().catch(() => undefined));
|
|
100
|
+
return new StorageApiError(response.status, envelope.success ? envelope.data.error.code : 'request_failed', envelope.success ? envelope.data.error.message : `Storage request failed (${response.status})`);
|
|
101
|
+
}
|
|
102
|
+
function createBrokerTransport({ context, token, getRuntime, fetcher }) {
|
|
103
|
+
return async (input, signal) => {
|
|
104
|
+
if (context.exp <= Math.floor(Date.now() / 1000))
|
|
105
|
+
throw new ArtifactServerError(401, 'request_context_expired', 'Artifact request context expired; begin a new request');
|
|
106
|
+
const operation = artifactServerRequestSchema.parse(input);
|
|
107
|
+
if (operation.mode === 'service' && !context.serviceAccountId)
|
|
108
|
+
throw new ArtifactServerError(403, 'service_identity_unavailable', 'No service identity is attached to this runtime');
|
|
109
|
+
const body = JSON.stringify(operation);
|
|
110
|
+
if (Buffer.byteLength(body, 'utf8') > maxBodyBytes)
|
|
111
|
+
throw new ArtifactServerError(413, 'request_too_large', 'Server SDK requests are limited to 1 MiB; use Storage uploads for files');
|
|
112
|
+
const runtime = await getRuntime();
|
|
113
|
+
return fetcher(`${runtime.atlasUrl}${ARTIFACT_SERVER_REQUEST_PATH}`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
[ARTIFACT_RUNTIME_TOKEN_HEADER]: runtime.sessionToken,
|
|
118
|
+
[ARTIFACT_REQUEST_CONTEXT_HEADER]: token,
|
|
119
|
+
},
|
|
120
|
+
body,
|
|
121
|
+
cache: 'no-store',
|
|
122
|
+
redirect: 'error',
|
|
123
|
+
credentials: 'omit',
|
|
124
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000),
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function createApiClient({ mode, request, json }) {
|
|
129
|
+
const operation = (path, options = {}) => artifactServerRequestSchema.parse({ operation: 'api', mode, method: options.method ?? 'GET', path, body: options.body });
|
|
130
|
+
const fetchResponse = (path, options = {}) => request(operation(path, options), options.signal);
|
|
131
|
+
const typedJson = async (path, options = {}) => {
|
|
132
|
+
// Explicit caller result types match the existing browser SDK; the server validates endpoint schemas.
|
|
133
|
+
return await json(operation(path, options), options.signal);
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
request: fetchResponse,
|
|
137
|
+
async fetch(path, options = {}) {
|
|
138
|
+
const response = await fetchResponse(path, options);
|
|
139
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
140
|
+
const body = response.status === 204 ? undefined : contentType.includes('json') ? await response.json() : await response.arrayBuffer();
|
|
141
|
+
// The caller declares the endpoint's response type, consistent with SequenceApiFetchResponse.
|
|
142
|
+
return { ok: response.ok, status: response.status, body: body, headers: Object.fromEntries(responseHeaders.flatMap(key => {
|
|
143
|
+
const value = response.headers.get(key);
|
|
144
|
+
return value === null ? [] : [[key, value]];
|
|
145
|
+
})) };
|
|
146
|
+
},
|
|
147
|
+
async stream(path, options = {}) {
|
|
148
|
+
const response = await fetchResponse(path, options);
|
|
149
|
+
if (!response.ok || !response.body)
|
|
150
|
+
throw new ArtifactServerError(response.status, 'stream_unavailable', 'Atlas did not return a readable response');
|
|
151
|
+
return response.body;
|
|
152
|
+
},
|
|
153
|
+
get: (path) => typedJson(path),
|
|
154
|
+
post: (path, body) => typedJson(path, { method: 'POST', body }),
|
|
155
|
+
put: (path, body) => typedJson(path, { method: 'PUT', body }),
|
|
156
|
+
patch: (path, body) => typedJson(path, { method: 'PATCH', body }),
|
|
157
|
+
delete: (path, body) => typedJson(path, { method: 'DELETE', body }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ArtifactRequestContext } from './request-context.js';
|
|
2
|
+
export interface ArtifactLogMetadata {
|
|
3
|
+
resourceId?: string;
|
|
4
|
+
operationId?: string;
|
|
5
|
+
durationMs?: number;
|
|
6
|
+
count?: number;
|
|
7
|
+
status?: number;
|
|
8
|
+
errorCode?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ArtifactLogRecord extends ArtifactLogMetadata {
|
|
11
|
+
event: string;
|
|
12
|
+
level: 'info' | 'warn' | 'error';
|
|
13
|
+
projectId: string;
|
|
14
|
+
requestId: string;
|
|
15
|
+
deploymentId: string | null;
|
|
16
|
+
devSessionId: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Structured events deliberately exclude request bodies, URLs, arbitrary messages and error objects. */
|
|
19
|
+
export declare function createArtifactLogger({ context, sink }: {
|
|
20
|
+
context: ArtifactRequestContext;
|
|
21
|
+
sink?: (record: ArtifactLogRecord) => void;
|
|
22
|
+
}): {
|
|
23
|
+
info: (event: string, metadata?: ArtifactLogMetadata) => void;
|
|
24
|
+
warn: (event: string, metadata?: ArtifactLogMetadata) => void;
|
|
25
|
+
error: (event: string, metadata?: ArtifactLogMetadata) => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Structured events deliberately exclude request bodies, URLs, arbitrary messages and error objects. */
|
|
2
|
+
export function createArtifactLogger({ context, sink = record => console[record.level](record) }) {
|
|
3
|
+
const write = (level, event, metadata = {}) => {
|
|
4
|
+
if (!/^[a-z][a-z0-9_.-]{0,95}$/.test(event))
|
|
5
|
+
throw new Error('Log event must be a static dot-separated identifier');
|
|
6
|
+
const safe = {};
|
|
7
|
+
for (const key of ['durationMs', 'count', 'status']) {
|
|
8
|
+
const value = metadata[key];
|
|
9
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
10
|
+
safe[key] = value;
|
|
11
|
+
}
|
|
12
|
+
for (const key of ['resourceId', 'operationId', 'errorCode']) {
|
|
13
|
+
const value = metadata[key];
|
|
14
|
+
if (typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,128}$/.test(value))
|
|
15
|
+
safe[key] = value;
|
|
16
|
+
}
|
|
17
|
+
sink({ ...safe, event, level, projectId: context.projectId, requestId: context.requestId, deploymentId: context.deploymentId, devSessionId: context.devSessionId });
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
info: (event, metadata) => write('info', event, metadata),
|
|
21
|
+
warn: (event, metadata) => write('warn', event, metadata),
|
|
22
|
+
error: (event, metadata) => write('error', event, metadata),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TransformStream } from 'node:stream/web';
|
|
2
|
+
/**
|
|
3
|
+
* Stamp forms with the document's release identity, including GET forms whose
|
|
4
|
+
* submitter can override the method to POST. The gateway removes the reserved
|
|
5
|
+
* field from GET query strings before forwarding them to application code.
|
|
6
|
+
* Only use on decompressed UTF-8 HTML; remove Content-Length and ETag upstream.
|
|
7
|
+
* Named hidden inputs are skipped by React 19 hydration, preserving RSC markup.
|
|
8
|
+
* The gateway must reject missing/conflicting markers before admitting mutations.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createReleaseFormTransform({ deploymentId, documentUrl }: {
|
|
11
|
+
deploymentId: string;
|
|
12
|
+
documentUrl: string;
|
|
13
|
+
}): TransformStream<Uint8Array, Uint8Array>;
|