@sequenceholdings/artifact-studio 0.2.6 → 0.3.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.
Files changed (181) hide show
  1. package/VERSION-PIN.md +8 -1
  2. package/dist/api.d.ts +11 -1
  3. package/dist/api.js +26 -9
  4. package/dist/build.d.ts +2 -2
  5. package/dist/build.js +4 -0
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.js +105 -10
  8. package/dist/dev-link.d.ts +1 -1
  9. package/dist/dev-link.js +2 -1
  10. package/dist/gateway/admission.d.ts +55 -0
  11. package/dist/gateway/admission.js +132 -0
  12. package/dist/gateway/control.d.ts +91 -0
  13. package/dist/gateway/control.js +113 -0
  14. package/dist/gateway/hosted-proxy.d.ts +36 -0
  15. package/dist/gateway/hosted-proxy.js +347 -0
  16. package/dist/gateway/release-admission.d.ts +23 -0
  17. package/dist/gateway/release-admission.js +259 -0
  18. package/dist/gateway/server.d.ts +29 -0
  19. package/dist/gateway/server.js +298 -0
  20. package/dist/gateway/session.d.ts +71 -0
  21. package/dist/gateway/session.js +143 -0
  22. package/dist/gateway/usage.d.ts +37 -0
  23. package/dist/gateway/usage.js +46 -0
  24. package/dist/gateway-main.d.ts +1 -0
  25. package/dist/gateway-main.js +84 -0
  26. package/dist/git-clone.d.ts +23 -1
  27. package/dist/git-clone.js +32 -15
  28. package/dist/manifest-next.d.ts +217 -0
  29. package/dist/manifest-next.js +120 -0
  30. package/dist/manifest-static.d.ts +76 -0
  31. package/dist/manifest-static.js +124 -0
  32. package/dist/manifest.d.ts +118 -13
  33. package/dist/manifest.js +12 -119
  34. package/dist/next/bridge.d.ts +47 -0
  35. package/dist/next/bridge.js +229 -0
  36. package/dist/next/browser-logs.d.ts +7 -0
  37. package/dist/next/browser-logs.js +51 -0
  38. package/dist/next/browser.d.ts +6 -0
  39. package/dist/next/browser.js +57 -0
  40. package/dist/next/protocol.d.ts +73 -0
  41. package/dist/next/protocol.js +56 -0
  42. package/dist/next/release.d.ts +15 -0
  43. package/dist/next/release.js +46 -0
  44. package/dist/next/requests.d.ts +15 -0
  45. package/dist/next/requests.js +59 -0
  46. package/dist/next/routes.d.ts +4 -0
  47. package/dist/next/routes.js +21 -0
  48. package/dist/next/streams.d.ts +16 -0
  49. package/dist/next/streams.js +132 -0
  50. package/dist/next/theme.d.ts +10 -0
  51. package/dist/next/theme.js +45 -0
  52. package/dist/next/transport.d.ts +15 -0
  53. package/dist/next/transport.js +13 -0
  54. package/dist/next-build.d.ts +29 -0
  55. package/dist/next-build.js +129 -0
  56. package/dist/next-cache/adapter-runtime.d.ts +51 -0
  57. package/dist/next-cache/adapter-runtime.js +73 -0
  58. package/dist/next-cache/component-handler.d.ts +3 -0
  59. package/dist/next-cache/component-handler.js +10 -0
  60. package/dist/next-cache/components.d.ts +20 -0
  61. package/dist/next-cache/components.js +113 -0
  62. package/dist/next-cache/config.d.ts +9 -0
  63. package/dist/next-cache/config.js +78 -0
  64. package/dist/next-cache/incremental-handler.d.ts +18 -0
  65. package/dist/next-cache/incremental-handler.js +17 -0
  66. package/dist/next-cache/incremental.d.ts +27 -0
  67. package/dist/next-cache/incremental.js +99 -0
  68. package/dist/next-cache/index.d.ts +18 -0
  69. package/dist/next-cache/index.js +16 -0
  70. package/dist/next-cache/memory-store.d.ts +27 -0
  71. package/dist/next-cache/memory-store.js +118 -0
  72. package/dist/next-cache/redis-scripts.d.ts +3 -0
  73. package/dist/next-cache/redis-scripts.js +85 -0
  74. package/dist/next-cache/redis-store.d.ts +18 -0
  75. package/dist/next-cache/redis-store.js +56 -0
  76. package/dist/next-cache/runtime.d.ts +29 -0
  77. package/dist/next-cache/runtime.js +63 -0
  78. package/dist/next-cache/serialization.d.ts +39 -0
  79. package/dist/next-cache/serialization.js +333 -0
  80. package/dist/next-cache/store.d.ts +54 -0
  81. package/dist/next-cache/store.js +45 -0
  82. package/dist/next-cache/valkey-transport.d.ts +52 -0
  83. package/dist/next-cache/valkey-transport.js +189 -0
  84. package/dist/next-dev-control.d.ts +48 -0
  85. package/dist/next-dev-control.js +63 -0
  86. package/dist/next-dev-process.d.ts +17 -0
  87. package/dist/next-dev-process.js +110 -0
  88. package/dist/next-dev.d.ts +12 -0
  89. package/dist/next-dev.js +267 -0
  90. package/dist/next-image-config.d.ts +9 -0
  91. package/dist/next-image-config.js +101 -0
  92. package/dist/next-project.d.ts +22 -0
  93. package/dist/next-project.js +64 -0
  94. package/dist/next-public-fonts-local.d.ts +7 -0
  95. package/dist/next-public-fonts-local.js +64 -0
  96. package/dist/next-public-fonts.d.ts +13 -0
  97. package/dist/next-public-fonts.js +49 -0
  98. package/dist/next-source-hash.d.ts +14 -0
  99. package/dist/next-source-hash.js +18 -0
  100. package/dist/next-source.d.ts +35 -0
  101. package/dist/next-source.js +155 -0
  102. package/dist/next.d.ts +9 -0
  103. package/dist/next.js +5 -0
  104. package/dist/project.d.ts +3 -0
  105. package/dist/project.js +9 -1
  106. package/dist/runtime-contracts.d.ts +229 -0
  107. package/dist/runtime-contracts.js +224 -0
  108. package/dist/sdk.d.ts +2 -2
  109. package/dist/server/broker-contract.d.ts +68 -0
  110. package/dist/server/broker-contract.js +33 -0
  111. package/dist/server/client.d.ts +67 -0
  112. package/dist/server/client.js +159 -0
  113. package/dist/server/logger.d.ts +26 -0
  114. package/dist/server/logger.js +24 -0
  115. package/dist/server/release-forms.d.ts +13 -0
  116. package/dist/server/release-forms.js +124 -0
  117. package/dist/server/request-context.d.ts +74 -0
  118. package/dist/server/request-context.js +103 -0
  119. package/dist/server/runtime-config.d.ts +53 -0
  120. package/dist/server/runtime-config.js +64 -0
  121. package/dist/server.d.ts +8 -0
  122. package/dist/server.js +4 -0
  123. package/dist/templates/nextjs/.artifactignore +14 -0
  124. package/dist/templates/nextjs/README.md +151 -0
  125. package/dist/templates/nextjs/app/about/page.tsx +26 -0
  126. package/dist/templates/nextjs/app/actions.ts +18 -0
  127. package/dist/templates/nextjs/app/api/health/route.ts +6 -0
  128. package/dist/templates/nextjs/app/components/counter.tsx +19 -0
  129. package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
  130. package/dist/templates/nextjs/app/globals.css +4 -0
  131. package/dist/templates/nextjs/app/layout.tsx +27 -0
  132. package/dist/templates/nextjs/app/page.tsx +51 -0
  133. package/dist/templates/nextjs/app/providers.tsx +107 -0
  134. package/dist/templates/nextjs/artifact.bundle.yml +55 -0
  135. package/dist/templates/nextjs/next.config.mjs +27 -0
  136. package/dist/templates/nextjs/package.json +32 -0
  137. package/dist/templates/nextjs/pnpm-workspace.yaml +8 -0
  138. package/dist/templates/nextjs/postcss.config.mjs +5 -0
  139. package/dist/templates/nextjs/tsconfig.json +20 -0
  140. package/dist/templates/react-vite/package.json +1 -1
  141. package/dist/tunnel/broker.d.ts +75 -0
  142. package/dist/tunnel/broker.js +451 -0
  143. package/dist/tunnel/client.d.ts +22 -0
  144. package/dist/tunnel/client.js +177 -0
  145. package/dist/tunnel/index.d.ts +6 -0
  146. package/dist/tunnel/index.js +4 -0
  147. package/dist/tunnel/io.d.ts +33 -0
  148. package/dist/tunnel/io.js +108 -0
  149. package/dist/tunnel/peer.d.ts +25 -0
  150. package/dist/tunnel/peer.js +147 -0
  151. package/dist/tunnel/protocol.d.ts +69 -0
  152. package/dist/tunnel/protocol.js +196 -0
  153. package/dist/tunnel/redis-ownership.d.ts +38 -0
  154. package/dist/tunnel/redis-ownership.js +100 -0
  155. package/dist/tunnel/redis-socket.d.ts +13 -0
  156. package/dist/tunnel/redis-socket.js +48 -0
  157. package/dist/tunnel/redis-types.d.ts +70 -0
  158. package/dist/tunnel/redis-types.js +9 -0
  159. package/dist/tunnel/redis.d.ts +53 -0
  160. package/dist/tunnel/redis.js +391 -0
  161. package/dist/tunnel/transport.d.ts +17 -0
  162. package/dist/tunnel/transport.js +1 -0
  163. package/package.json +67 -4
  164. package/templates/nextjs/.artifactignore +14 -0
  165. package/templates/nextjs/README.md +151 -0
  166. package/templates/nextjs/app/about/page.tsx +26 -0
  167. package/templates/nextjs/app/actions.ts +18 -0
  168. package/templates/nextjs/app/api/health/route.ts +6 -0
  169. package/templates/nextjs/app/components/counter.tsx +19 -0
  170. package/templates/nextjs/app/components/greeting-form.tsx +38 -0
  171. package/templates/nextjs/app/globals.css +4 -0
  172. package/templates/nextjs/app/layout.tsx +27 -0
  173. package/templates/nextjs/app/page.tsx +51 -0
  174. package/templates/nextjs/app/providers.tsx +107 -0
  175. package/templates/nextjs/artifact.bundle.yml +55 -0
  176. package/templates/nextjs/next.config.mjs +27 -0
  177. package/templates/nextjs/package.json +32 -0
  178. package/templates/nextjs/pnpm-workspace.yaml +8 -0
  179. package/templates/nextjs/postcss.config.mjs +5 -0
  180. package/templates/nextjs/tsconfig.json +20 -0
  181. package/templates/react-vite/package.json +1 -1
@@ -0,0 +1,124 @@
1
+ import { Tokenizer, TokenizerMode } from 'parse5';
2
+ import { TransformStream } from 'node:stream/web';
3
+ const MAX_TOKEN_CHARACTERS = 64 * 1024;
4
+ const PARSE_CHUNK_CHARACTERS = 8 * 1024;
5
+ const rawTextElements = new Set(['style', 'xmp', 'iframe', 'noembed', 'noframes']);
6
+ /**
7
+ * Use the HTML tokenizer rather than a DOM: streaming RSC documents can contain
8
+ * arbitrarily long scripts, and serializing a DOM would change application HTML.
9
+ * The exact parse5 version is pinned because its tokenizer is an internal API.
10
+ */
11
+ class BoundedHtmlTokenizer extends Tokenizer {
12
+ completedToken = null;
13
+ prepareToken(token) {
14
+ super.prepareToken(token);
15
+ this.completedToken = token;
16
+ }
17
+ _callState(codePoint) {
18
+ super._callState(codePoint);
19
+ const pending = this.currentToken;
20
+ if (pending !== this.completedToken && pending?.location &&
21
+ this.preprocessor.offset - pending.location.startOffset > MAX_TOKEN_CHARACTERS) {
22
+ throw new Error('Artifact HTML contains a token exceeding the 64 KiB limit');
23
+ }
24
+ }
25
+ releaseConsumedText() {
26
+ // There is no DOM consumer. Do not retain a script/text token between writes.
27
+ this._emitCurrentCharacterToken(null);
28
+ this.preprocessor.dropParsedChunk();
29
+ }
30
+ }
31
+ function resolvesToApplication({ action, baseUrl, documentUrl }) {
32
+ try {
33
+ // Missing/empty form actions resolve to the document, not its <base> URL.
34
+ const target = !action ? documentUrl : new URL(action, baseUrl);
35
+ return target.origin === documentUrl.origin && !target.username && !target.password;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Stamp forms with the document's release identity, including GET forms whose
43
+ * submitter can override the method to POST. The gateway removes the reserved
44
+ * field from GET query strings before forwarding them to application code.
45
+ * Only use on decompressed UTF-8 HTML; remove Content-Length and ETag upstream.
46
+ * Named hidden inputs are skipped by React 19 hydration, preserving RSC markup.
47
+ * The gateway must reject missing/conflicting markers before admitting mutations.
48
+ */
49
+ export function createReleaseFormTransform({ deploymentId, documentUrl }) {
50
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(deploymentId))
51
+ throw new Error('Invalid Artifact release identifier');
52
+ const document = new URL(documentUrl);
53
+ if (!['https:', 'http:'].includes(document.protocol))
54
+ throw new Error('Invalid Artifact document URL');
55
+ const marker = `<input type="hidden" name="__sequence_release" value="${deploymentId}">`;
56
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
57
+ const encoder = new TextEncoder();
58
+ let baseUrl = document;
59
+ let hasBase = false;
60
+ let sourceOffset = 0;
61
+ let insertionOffsets = [];
62
+ const tokenizer = new BoundedHtmlTokenizer({ sourceCodeLocationInfo: true }, {
63
+ onStartTag(token) {
64
+ const attributes = new Map(token.attrs.map(attribute => [attribute.name, attribute.value]));
65
+ if (token.tagName === 'base' && !hasBase && attributes.has('href')) {
66
+ hasBase = true;
67
+ try {
68
+ baseUrl = new URL(attributes.get('href') ?? '', document);
69
+ }
70
+ catch { /* Invalid base falls back to document. */ }
71
+ }
72
+ if (token.tagName === 'form' &&
73
+ resolvesToApplication({ action: attributes.get('action'), baseUrl, documentUrl: document }) && token.location) {
74
+ insertionOffsets.push(token.location.endOffset);
75
+ }
76
+ if (token.tagName === 'script')
77
+ tokenizer.state = TokenizerMode.SCRIPT_DATA;
78
+ else if (rawTextElements.has(token.tagName))
79
+ tokenizer.state = TokenizerMode.RAWTEXT;
80
+ else if (token.tagName === 'textarea' || token.tagName === 'title')
81
+ tokenizer.state = TokenizerMode.RCDATA;
82
+ else if (token.tagName === 'plaintext')
83
+ tokenizer.state = TokenizerMode.PLAINTEXT;
84
+ },
85
+ onEndTag() { },
86
+ onComment() { },
87
+ onDoctype() { },
88
+ onEof() { },
89
+ onCharacter() { },
90
+ onNullCharacter() { },
91
+ onWhitespaceCharacter() { },
92
+ });
93
+ function transformText({ text, controller, final = false }) {
94
+ // Bound tokenizer work and buffers even if upstream sends a very large chunk.
95
+ for (let offset = 0; offset < text.length;) {
96
+ let end = Math.min(offset + PARSE_CHUNK_CHARACTERS, text.length);
97
+ // Do not re-encode one half of a decoded astral Unicode character.
98
+ const last = text.charCodeAt(end - 1);
99
+ if (end < text.length && last >= 0xd800 && last <= 0xdbff)
100
+ end--;
101
+ const part = text.slice(offset, end);
102
+ offset = end;
103
+ insertionOffsets = [];
104
+ tokenizer.write(part, false);
105
+ tokenizer.releaseConsumedText();
106
+ let partOffset = 0;
107
+ let output = '';
108
+ for (const insertionOffset of insertionOffsets) {
109
+ const relative = insertionOffset - sourceOffset;
110
+ output += part.slice(partOffset, relative) + marker;
111
+ partOffset = relative;
112
+ }
113
+ output += part.slice(partOffset);
114
+ sourceOffset += part.length;
115
+ controller.enqueue(encoder.encode(output));
116
+ }
117
+ if (final)
118
+ tokenizer.write('', true);
119
+ }
120
+ return new TransformStream({
121
+ transform(chunk, controller) { transformText({ text: decoder.decode(chunk, { stream: true }), controller }); },
122
+ flush(controller) { transformText({ text: decoder.decode(), controller, final: true }); },
123
+ });
124
+ }
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ import type { NextArtifactBridgeBootstrap, NextArtifactInitialContext } from '../next/protocol.js';
3
+ export declare const ARTIFACT_REQUEST_CONTEXT_HEADER = "x-sequence-artifact-context";
4
+ export declare const ARTIFACT_RUNTIME_TOKEN_HEADER = "x-sequence-artifact-runtime";
5
+ export declare const ARTIFACT_SERVER_REQUEST_PATH = "/api/artifact-studio/server/request";
6
+ export declare const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
7
+ /** Public request context. No bearer credentials or secret values belong here. */
8
+ export declare const artifactRequestContextSchema: z.ZodObject<{
9
+ v: z.ZodLiteral<1>;
10
+ iss: z.ZodLiteral<"atlas-artifact-gateway">;
11
+ aud: z.ZodString;
12
+ sub: z.ZodString;
13
+ projectId: z.ZodString;
14
+ environment: z.ZodString;
15
+ mode: z.ZodEnum<{
16
+ preview: "preview";
17
+ published: "published";
18
+ development: "development";
19
+ }>;
20
+ deploymentId: z.ZodNullable<z.ZodString>;
21
+ devSessionId: z.ZodNullable<z.ZodString>;
22
+ requestId: z.ZodString;
23
+ method: z.ZodEnum<{
24
+ GET: "GET";
25
+ POST: "POST";
26
+ DELETE: "DELETE";
27
+ HEAD: "HEAD";
28
+ PUT: "PUT";
29
+ PATCH: "PATCH";
30
+ OPTIONS: "OPTIONS";
31
+ }>;
32
+ path: z.ZodString;
33
+ iat: z.ZodNumber;
34
+ exp: z.ZodNumber;
35
+ capabilities: z.ZodArray<z.ZodString>;
36
+ serviceAccountId: z.ZodNullable<z.ZodString>;
37
+ bridge: z.ZodObject<{
38
+ parentOrigin: z.ZodString;
39
+ channelId: z.ZodString;
40
+ nonce: z.ZodString;
41
+ }, z.core.$strict>;
42
+ theme: z.ZodEnum<{
43
+ dark: "dark";
44
+ light: "light";
45
+ system: "system";
46
+ }>;
47
+ lattice: z.ZodObject<{
48
+ runId: z.ZodNullable<z.ZodString>;
49
+ nodeId: z.ZodNullable<z.ZodString>;
50
+ stepId: z.ZodNullable<z.ZodString>;
51
+ processId: z.ZodNullable<z.ZodString>;
52
+ }, z.core.$strict>;
53
+ taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ }, z.core.$strict>;
55
+ export type ArtifactRequestContext = z.infer<typeof artifactRequestContextSchema>;
56
+ export type ArtifactGatewayPublicKeys = Readonly<Record<string, string>>;
57
+ export declare class ArtifactContextError extends Error {
58
+ constructor();
59
+ }
60
+ /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
61
+ export declare function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now }: {
62
+ token: string | null;
63
+ publicKeys: ArtifactGatewayPublicKeys;
64
+ projectId: string;
65
+ environment: string;
66
+ /** Route handlers supply these; RSC callers use the signed gateway request metadata. */
67
+ method?: string;
68
+ path?: string;
69
+ now?: number;
70
+ }): ArtifactRequestContext;
71
+ /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
72
+ export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext): NextArtifactBridgeBootstrap & {
73
+ initialContext: NextArtifactInitialContext;
74
+ };
@@ -0,0 +1,103 @@
1
+ import { createPublicKey, verify } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ export const ARTIFACT_REQUEST_CONTEXT_HEADER = 'x-sequence-artifact-context';
4
+ export const ARTIFACT_RUNTIME_TOKEN_HEADER = 'x-sequence-artifact-runtime';
5
+ export const ARTIFACT_SERVER_REQUEST_PATH = '/api/artifact-studio/server/request';
6
+ export const ARTIFACT_CONTEXT_MAX_AGE_SECONDS = 60;
7
+ const id = z.string().min(1).max(255);
8
+ const originSchema = z.string().max(2048).refine(value => {
9
+ try {
10
+ const url = new URL(value);
11
+ return value === url.origin && (url.protocol === 'https:' ||
12
+ (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)));
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }, 'Expected an HTTPS origin (HTTP is allowed only on loopback)');
18
+ /** Public request context. No bearer credentials or secret values belong here. */
19
+ export const artifactRequestContextSchema = z.object({
20
+ v: z.literal(1),
21
+ iss: z.literal('atlas-artifact-gateway'),
22
+ aud: z.string().min(1).max(300),
23
+ sub: id,
24
+ projectId: id,
25
+ environment: id,
26
+ mode: z.enum(['development', 'preview', 'published']),
27
+ deploymentId: id.nullable(),
28
+ devSessionId: id.nullable(),
29
+ requestId: id,
30
+ method: z.enum(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
31
+ path: z.string().min(1).max(8192).refine(value => value.startsWith('/') &&
32
+ !value.startsWith('//') && !/[\\\r\n\u0000#]/.test(value), 'Expected an origin-relative request path'),
33
+ iat: z.number().int().nonnegative(),
34
+ exp: z.number().int().positive(),
35
+ capabilities: z.array(id).max(256),
36
+ serviceAccountId: id.nullable(),
37
+ bridge: z.object({ parentOrigin: originSchema, channelId: id, nonce: z.string().min(16).max(255) }).strict(),
38
+ theme: z.enum(['light', 'dark', 'system']),
39
+ lattice: z.object({ runId: id.nullable(), nodeId: id.nullable(), stepId: id.nullable(), processId: id.nullable() }).strict(),
40
+ taskRoutes: z.array(z.string().min(1).max(512)).max(16).refine(routes => routes.join('').length <= 1024).optional(),
41
+ }).strict().superRefine((value, ctx) => {
42
+ if (value.aud !== `artifact:${value.projectId}`)
43
+ ctx.addIssue({ code: 'custom', message: 'Invalid Artifact audience' });
44
+ if (value.exp <= value.iat || value.exp - value.iat > ARTIFACT_CONTEXT_MAX_AGE_SECONDS)
45
+ ctx.addIssue({ code: 'custom', message: 'Invalid Artifact context lifetime' });
46
+ if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
47
+ ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
48
+ });
49
+ export class ArtifactContextError extends Error {
50
+ constructor() { super('Missing or invalid Artifact gateway request context'); this.name = 'ArtifactContextError'; }
51
+ }
52
+ function decode(value) {
53
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
54
+ throw new ArtifactContextError();
55
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
56
+ }
57
+ /** Only the gateway owns private keys. Applications pin platform-provided public keys. */
58
+ export function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
59
+ try {
60
+ if (!token || token.length > 32768)
61
+ throw new ArtifactContextError();
62
+ const parts = token.split('.');
63
+ if (parts.length !== 3)
64
+ throw new ArtifactContextError();
65
+ const [encodedHeader, encodedPayload, signature] = parts;
66
+ const header = z.object({ alg: z.literal('EdDSA'), typ: z.literal('JWT'), kid: id }).strict().parse(decode(encodedHeader));
67
+ const pem = Object.hasOwn(publicKeys, header.kid) ? publicKeys[header.kid] : undefined;
68
+ if (!pem || !/^[A-Za-z0-9_-]+$/.test(signature))
69
+ throw new ArtifactContextError();
70
+ const key = createPublicKey(pem);
71
+ if (key.asymmetricKeyType !== 'ed25519' ||
72
+ !verify(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), key, Buffer.from(signature, 'base64url')))
73
+ throw new ArtifactContextError();
74
+ const context = artifactRequestContextSchema.parse(decode(encodedPayload));
75
+ if (context.projectId !== projectId || context.environment !== environment ||
76
+ context.exp <= now || context.iat > now + 5 ||
77
+ (method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
78
+ throw new ArtifactContextError();
79
+ Object.freeze(context.bridge);
80
+ Object.freeze(context.lattice);
81
+ Object.freeze(context.capabilities);
82
+ if (context.taskRoutes)
83
+ Object.freeze(context.taskRoutes);
84
+ return Object.freeze(context);
85
+ }
86
+ catch {
87
+ throw new ArtifactContextError();
88
+ }
89
+ }
90
+ /** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
91
+ export function getArtifactBrowserBootstrap(context) {
92
+ return {
93
+ parentOrigin: context.bridge.parentOrigin,
94
+ channelId: context.bridge.channelId,
95
+ nonce: context.bridge.nonce,
96
+ deploymentId: context.deploymentId,
97
+ initialContext: {
98
+ theme: { tokens: null, theme: null, dark: context.theme === 'dark', vars: {} },
99
+ lattice: context.lattice,
100
+ route: context.path,
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ export declare const artifactRuntimeConfigSchema: z.ZodObject<{
3
+ atlasUrl: z.ZodString;
4
+ projectId: z.ZodString;
5
+ environment: z.ZodString;
6
+ sessionToken: z.ZodString;
7
+ publicKeys: z.ZodRecord<z.ZodString, z.ZodString>;
8
+ devSessionId: z.ZodOptional<z.ZodString>;
9
+ deploymentId: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strict>;
11
+ export type ArtifactRuntimeConfig = z.infer<typeof artifactRuntimeConfigSchema>;
12
+ export type ArtifactRequestHeaders = Pick<Headers, 'get'>;
13
+ /** Credentials are platform-provided and rotated by the CLI, never inferred from ambient cloud auth. */
14
+ export declare function readArtifactRuntimeConfig({ file }?: {
15
+ file?: string;
16
+ }): Promise<ArtifactRuntimeConfig | null>;
17
+ /** Null permits a standalone localhost page; platform services still require a verified gateway request. */
18
+ export declare function getArtifactRequestContext({ headers, runtime, method, path }: {
19
+ headers: ArtifactRequestHeaders;
20
+ runtime?: ArtifactRuntimeConfig;
21
+ method?: string;
22
+ path?: string;
23
+ }): Promise<{
24
+ v: 1;
25
+ iss: "atlas-artifact-gateway";
26
+ aud: string;
27
+ sub: string;
28
+ projectId: string;
29
+ environment: string;
30
+ mode: "preview" | "published" | "development";
31
+ deploymentId: string | null;
32
+ devSessionId: string | null;
33
+ requestId: string;
34
+ method: "GET" | "POST" | "DELETE" | "HEAD" | "PUT" | "PATCH" | "OPTIONS";
35
+ path: string;
36
+ iat: number;
37
+ exp: number;
38
+ capabilities: string[];
39
+ serviceAccountId: string | null;
40
+ bridge: {
41
+ parentOrigin: string;
42
+ channelId: string;
43
+ nonce: string;
44
+ };
45
+ theme: "dark" | "light" | "system";
46
+ lattice: {
47
+ runId: string | null;
48
+ nodeId: string | null;
49
+ stepId: string | null;
50
+ processId: string | null;
51
+ };
52
+ taskRoutes?: string[] | undefined;
53
+ } | null>;
@@ -0,0 +1,64 @@
1
+ import { constants } from 'node:fs';
2
+ import { open } from 'node:fs/promises';
3
+ import { z } from 'zod';
4
+ import { ARTIFACT_REQUEST_CONTEXT_HEADER, verifyArtifactRequestContext } from './request-context.js';
5
+ const atlasOrigin = z.string().refine(value => {
6
+ try {
7
+ const url = new URL(value);
8
+ return value === url.origin && (url.protocol === 'https:' ||
9
+ (url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)));
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }, 'Atlas URL must be an HTTPS origin (or loopback HTTP)');
15
+ export const artifactRuntimeConfigSchema = z.object({
16
+ atlasUrl: atlasOrigin,
17
+ projectId: z.string().min(1).max(255),
18
+ environment: z.string().min(1).max(255),
19
+ sessionToken: z.string().min(16).max(16384),
20
+ publicKeys: z.record(z.string().min(1).max(255), z.string().min(1).max(8192)),
21
+ devSessionId: z.string().min(1).max(255).optional(),
22
+ deploymentId: z.string().min(1).max(255).optional(),
23
+ }).strict().refine(value => !(value.devSessionId && value.deploymentId), 'Runtime cannot be both local and hosted');
24
+ /** Credentials are platform-provided and rotated by the CLI, never inferred from ambient cloud auth. */
25
+ export async function readArtifactRuntimeConfig({ file = process.env.SEQUENCE_ARTIFACT_RUNTIME_FILE } = {}) {
26
+ if (!file) {
27
+ const config = process.env.SEQUENCE_ARTIFACT_RUNTIME_CONFIG;
28
+ const sessionToken = process.env.SEQUENCE_ARTIFACT_RUNTIME_TOKEN;
29
+ if (config === undefined && sessionToken === undefined)
30
+ return null;
31
+ if (!config || !sessionToken || config.length > 65536)
32
+ throw new Error('Hosted Artifact runtime configuration is incomplete');
33
+ // Only the platform's explicit configuration is considered; never ambient cloud or CLI credentials.
34
+ const parsed = JSON.parse(config);
35
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
36
+ throw new Error('Invalid hosted Artifact runtime configuration');
37
+ return artifactRuntimeConfigSchema.parse({ ...parsed, sessionToken });
38
+ }
39
+ const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
40
+ try {
41
+ const stat = await handle.stat();
42
+ if (!stat.isFile() || stat.size > 65536 || (process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
43
+ throw new Error('Artifact runtime credential file must be owner-readable only, regular, and no larger than 64 KiB');
44
+ return artifactRuntimeConfigSchema.parse(JSON.parse(await handle.readFile('utf8')));
45
+ }
46
+ finally {
47
+ await handle.close();
48
+ }
49
+ }
50
+ /** Null permits a standalone localhost page; platform services still require a verified gateway request. */
51
+ export async function getArtifactRequestContext({ headers, runtime, method, path }) {
52
+ const token = headers.get(ARTIFACT_REQUEST_CONTEXT_HEADER);
53
+ if (!token)
54
+ return null;
55
+ const config = runtime ? artifactRuntimeConfigSchema.parse(runtime) : await readArtifactRuntimeConfig();
56
+ if (!config)
57
+ throw new Error('Artifact runtime is not connected. Run seq-studio artifact dev or configure the hosted runtime.');
58
+ const context = verifyArtifactRequestContext({ token, publicKeys: config.publicKeys, projectId: config.projectId, environment: config.environment, method, path });
59
+ if (config.devSessionId && context.devSessionId !== config.devSessionId)
60
+ throw new Error('Artifact request belongs to a different development session');
61
+ if (config.deploymentId && context.deploymentId !== config.deploymentId)
62
+ throw new Error('Artifact request belongs to a different deployment');
63
+ return context;
64
+ }
@@ -0,0 +1,8 @@
1
+ import 'server-only';
2
+ export { createArtifactServerClient, ArtifactServerError } from './server/client.js';
3
+ export type { ArtifactServerClient, ArtifactServerFetchOptions } from './server/client.js';
4
+ export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
5
+ export type { ArtifactRuntimeConfig, ArtifactRequestHeaders } from './server/runtime-config.js';
6
+ export { getArtifactBrowserBootstrap } from './server/request-context.js';
7
+ export type { ArtifactRequestContext } from './server/request-context.js';
8
+ export type { ArtifactLogMetadata, ArtifactLogRecord } from './server/logger.js';
package/dist/server.js ADDED
@@ -0,0 +1,4 @@
1
+ import 'server-only';
2
+ export { createArtifactServerClient, ArtifactServerError } from './server/client.js';
3
+ export { getArtifactRequestContext, readArtifactRuntimeConfig } from './server/runtime-config.js';
4
+ export { getArtifactBrowserBootstrap } from './server/request-context.js';
@@ -0,0 +1,14 @@
1
+ .git/
2
+ node_modules/
3
+ .next/
4
+ next-env.d.ts
5
+ .artifact/
6
+ .artifact-studio/
7
+ # Environment files are excluded by the source packager independently.
8
+ .env
9
+ .env.local
10
+ .env.development.local
11
+ .env.production.local
12
+ tsconfig.tsbuildinfo
13
+ coverage/
14
+ .DS_Store
@@ -0,0 +1,151 @@
1
+ # Next.js on Artifact Studio
2
+
3
+ This app uses Next.js App Router, React 19 and Node 24. Its source and development
4
+ server live on your machine; Atlas manages access, previews and deployment. You
5
+ do not need GCP credentials or a cloud devbox.
6
+
7
+ ## Get started
8
+
9
+ Install Node 24, pnpm and a released `seq-studio` version with Next.js support.
10
+ Your Atlas environment must have the Next.js runtime enabled.
11
+
12
+ From this directory:
13
+
14
+ ```sh
15
+ pnpm install
16
+ seq-studio envs list
17
+ seq-studio login --env <your-environment>
18
+ seq-studio artifact link --env <your-environment>
19
+ seq-studio artifact dev --env <your-environment>
20
+ ```
21
+
22
+ Use an environment shown by `envs list`; for an OpCo, log in to that environment's
23
+ realm. The CLI opens an owner-only preview in Atlas and connects it to Next.js
24
+ over an authenticated outbound tunnel. Keep the process running while you edit.
25
+ Use Ctrl+C to stop it. When your machine is offline, the preview is offline too.
26
+
27
+ Local development uses Webpack with native Fast Refresh. This is the supported
28
+ bundler for the pinned Next.js release; it preserves client navigation during
29
+ updates through the Atlas preview. Dependency changes restart the
30
+ local server; linked package edits rebuild and restart it. Changes to
31
+ `artifact.bundle.yml` end the current session—restart `artifact dev` to authorize
32
+ the new configuration and permissions.
33
+
34
+ Commit the generated `pnpm-lock.yaml` with the app source. The workspace policy
35
+ keeps the seven-day dependency age gate and explicit install-script decisions.
36
+ Do not commit registry credentials or a project `.npmrc`.
37
+
38
+ For a standalone UI check without Atlas, run `pnpm dev` and open the loopback URL.
39
+ Atlas SDK calls require an authorized Atlas development session. The CLI sets
40
+ `ARTIFACT_STUDIO_DEV_ORIGIN` for the exact preview host; do not replace it with a
41
+ wildcard. Native Next.js routing is used throughout—no hash router or `basePath`.
42
+
43
+ ## What to edit
44
+
45
+ | File | Purpose |
46
+ | --- | --- |
47
+ | `app/page.tsx` | Request-time Server Component and client interaction examples |
48
+ | `app/actions.ts` | Validated server action; this example stores no data |
49
+ | `app/api/health/route.ts` | App-owned route handler |
50
+ | `app/providers.tsx` | Client boundary for the Atlas bridge, UI portals and React Query |
51
+ | `app/about/page.tsx` | Nested route for exercising Atlas and Next navigation |
52
+ | `artifact.bundle.yml` | Browser/server permissions and hosted runtime requirements |
53
+
54
+ `app/layout.tsx` verifies the gateway's request context and passes only safe UI
55
+ bootstrap data into the browser. Import Atlas UI components through
56
+ `@sequenceholdings/atlas-ui/next`, which preserves their React client boundary.
57
+ Keep `@sequenceholdings/artifact-studio/server` imports in
58
+ Server Components, server actions or route handlers. Framework route/layout and
59
+ configuration files use the default exports required by Next.js; other modules
60
+ use named exports.
61
+
62
+ Try incrementing the counter, then changing its label in
63
+ `app/components/counter.tsx`. Compatible edits preserve its value through Fast
64
+ Refresh. Submit the greeting form to exercise a server action; server validation
65
+ still applies when browser validation is bypassed.
66
+
67
+ ## Data and secrets
68
+
69
+ Browser permissions and server permissions are separate in the manifest. Add
70
+ only the capabilities the application needs, then configure authorized resource
71
+ bindings in Atlas. Local development uses explicit nonproduction resources and
72
+ never falls back to production when a binding is missing.
73
+
74
+ Managed secrets stay in hosted runtimes. For local integration tests, use your
75
+ own test credentials, mocks or an existing governed function. Use a hosted
76
+ preview for application code that needs a managed secret. Never put secrets in
77
+ `build.public_env`, `NEXT_PUBLIC_*`, client components or committed files.
78
+
79
+ The local process is ordinary code running as your operating-system user.
80
+ Atlas governs access to platform resources; it does not sandbox your laptop's
81
+ filesystem or all outbound network connections.
82
+
83
+ ## Build and deploy
84
+
85
+ ```sh
86
+ pnpm type-check
87
+ pnpm build
88
+ seq-studio artifact plan --env <your-environment>
89
+ seq-studio artifact deploy --repo <namespace>/<repository> --ref main --env <your-environment>
90
+ ```
91
+
92
+ Push the reviewed application source and lockfile to its Git Service repository
93
+ before deploying. The local build is a validation step. Atlas performs the official isolated build
94
+ from recorded source and a lockfile, then operates the hosted Next.js server.
95
+ Deployments do not depend on your laptop staying online. Reviewers use hosted
96
+ previews rather than your owner-only local session.
97
+
98
+ For official releases to deployed environments, use a clean reviewed `main`
99
+ revision under the existing Artifact deployment policy. Inspect build/deployment
100
+ status, permissions and runtime diagnostics in Artifact Studio. Failed runtime
101
+ checks do not replace the previous healthy release.
102
+
103
+ The default scaffold is dynamic and declares no distributed cache. Enable managed
104
+ cache capacity in Atlas before adopting framework features that require shared
105
+ cross-replica caching; local Fast Refresh is not a test of distributed cache
106
+ behavior. Do not add a production cache endpoint or managed credentials to local
107
+ environment files.
108
+
109
+ ## Toolchain updates
110
+
111
+ This scaffold pins Next.js `16.3.4`, React/React DOM `19.2.6` and pnpm `10.28.2`,
112
+ with Node major `24`. These are its selected versions, not a claim that they are
113
+ the latest security releases. Use your platform's reviewed compatible versions
114
+ when updating an existing app. The CLI and build path check the platform's exact
115
+ approved Next/React selection; a different patch needs a coordinated platform
116
+ update, even if it belongs to the same framework profile.
117
+
118
+ For an approved patch, update the dependency declarations and lockfile together,
119
+ run the local type/build checks, then verify a hosted preview before requesting
120
+ an official repository-backed deployment. Existing immutable deployments keep
121
+ their previous code until rebuilt and activated. Your platform team maintains
122
+ build/runtime images and handles any urgent security response; this workflow
123
+ does not require application authors to obtain cloud-provider credentials.
124
+
125
+ ## Images and fonts
126
+
127
+ Keep static images and other binary assets in `public/`. Prefer committed WOFF2
128
+ fonts loaded with `next/font/local`; compilation runs without unrestricted
129
+ network access.
130
+
131
+ For a public font prepared by the platform, add `build.public_fonts` entries with
132
+ `path`, `url` and the actual lowercase `sha256` digest. Targets must be
133
+ `public/fonts/<filename>.woff2`, without nested directories. Use ASCII
134
+ letters/digits, hyphens or underscores for the basename, starting with a letter
135
+ or digit. URLs must use HTTPS on `fonts.gstatic.com` with its versioned
136
+ `/s/<family>/v<revision>/<file>.woff2` format and no query, fragment or custom
137
+ port. The platform allows
138
+ up to eight files, 2 MiB each and 8 MiB total, with no redirects. Connected local
139
+ development prepares the same files. Existing matching files are reused;
140
+ mismatches fail without overwriting your file. Load the result with
141
+ `next/font/local`. A plain `pnpm build` uses files already present locally and
142
+ does not prepare declared fonts. Do not use `next/font/google` to download during the
143
+ network-blocked compilation stage.
144
+
145
+ For a remote optimized image, add the exact HTTPS host to both Next's
146
+ `images.remotePatterns` and the manifest's `server.egress`. Narrow the pathname
147
+ to the content needed by the app. Wildcard hosts, custom ports, redirects,
148
+ custom loaders, SVG optimization and private-IP destinations are unsupported.
149
+ Remote bodies are capped at 10 MiB, or a lower configured limit. Keep the
150
+ `withArtifactNextConfig` wrapper in place and verify remote images in a hosted
151
+ preview under the application's actual egress policy.
@@ -0,0 +1,26 @@
1
+ import Link from 'next/link'
2
+
3
+ export default function AboutPage() {
4
+ return (
5
+ <main className="mx-auto max-w-3xl px-roomy py-10 sm:px-8">
6
+ <Link href="/" className="text-sm font-medium text-primary underline-offset-4 hover:underline">
7
+ Back to your application
8
+ </Link>
9
+ <h1 className="mb-6 mt-8 text-2xl font-semibold">One application, two runtimes</h1>
10
+ <div className="space-y-4 text-foreground-secondary">
11
+ <p>
12
+ Server Components, actions and route handlers execute on your local
13
+ Next.js process during development and in the managed runtime after deployment.
14
+ </p>
15
+ <p>
16
+ Client Components provide interactive UI in the browser. Import Atlas UI
17
+ components from a client module and use the server SDK only in server code.
18
+ </p>
19
+ <p>
20
+ Atlas controls access, resource bindings and deployment. Local development
21
+ uses explicit nonproduction bindings; managed secrets stay in hosted runtimes.
22
+ </p>
23
+ </div>
24
+ </main>
25
+ )
26
+ }
@@ -0,0 +1,18 @@
1
+ 'use server'
2
+
3
+ export interface GreetingState {
4
+ message: string
5
+ error: string | null
6
+ }
7
+
8
+ export async function greet(
9
+ _previousState: GreetingState,
10
+ formData: FormData,
11
+ ): Promise<GreetingState> {
12
+ const input = formData.get('name')
13
+ const name = typeof input === 'string' ? input.trim() : ''
14
+ if (name.length === 0 || name.length > 80) {
15
+ return { message: '', error: 'Enter a name between 1 and 80 characters.' }
16
+ }
17
+ return { message: `Hello, ${name}. Your server action ran successfully.`, error: null }
18
+ }
@@ -0,0 +1,6 @@
1
+ export function GET() {
2
+ return Response.json(
3
+ { ok: true, framework: 'nextjs' },
4
+ { headers: { 'Cache-Control': 'no-store' } },
5
+ )
6
+ }