@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,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,75 @@
|
|
|
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
|
+
jti: z.ZodUUID;
|
|
24
|
+
method: z.ZodEnum<{
|
|
25
|
+
GET: "GET";
|
|
26
|
+
POST: "POST";
|
|
27
|
+
DELETE: "DELETE";
|
|
28
|
+
HEAD: "HEAD";
|
|
29
|
+
PUT: "PUT";
|
|
30
|
+
PATCH: "PATCH";
|
|
31
|
+
OPTIONS: "OPTIONS";
|
|
32
|
+
}>;
|
|
33
|
+
path: z.ZodString;
|
|
34
|
+
iat: z.ZodNumber;
|
|
35
|
+
exp: z.ZodNumber;
|
|
36
|
+
capabilities: z.ZodArray<z.ZodString>;
|
|
37
|
+
serviceAccountId: z.ZodNullable<z.ZodString>;
|
|
38
|
+
bridge: z.ZodObject<{
|
|
39
|
+
parentOrigin: z.ZodString;
|
|
40
|
+
channelId: z.ZodString;
|
|
41
|
+
nonce: z.ZodString;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
theme: z.ZodEnum<{
|
|
44
|
+
dark: "dark";
|
|
45
|
+
light: "light";
|
|
46
|
+
system: "system";
|
|
47
|
+
}>;
|
|
48
|
+
lattice: z.ZodObject<{
|
|
49
|
+
runId: z.ZodNullable<z.ZodString>;
|
|
50
|
+
nodeId: z.ZodNullable<z.ZodString>;
|
|
51
|
+
stepId: z.ZodNullable<z.ZodString>;
|
|
52
|
+
processId: z.ZodNullable<z.ZodString>;
|
|
53
|
+
}, z.core.$strict>;
|
|
54
|
+
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
}, z.core.$strict>;
|
|
56
|
+
export type ArtifactRequestContext = z.infer<typeof artifactRequestContextSchema>;
|
|
57
|
+
export type ArtifactGatewayPublicKeys = Readonly<Record<string, string>>;
|
|
58
|
+
export declare class ArtifactContextError extends Error {
|
|
59
|
+
constructor();
|
|
60
|
+
}
|
|
61
|
+
/** Only the gateway owns private keys. Applications pin platform-provided public keys. */
|
|
62
|
+
export declare function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now }: {
|
|
63
|
+
token: string | null;
|
|
64
|
+
publicKeys: ArtifactGatewayPublicKeys;
|
|
65
|
+
projectId: string;
|
|
66
|
+
environment: string;
|
|
67
|
+
/** Route handlers supply these; RSC callers use the signed gateway request metadata. */
|
|
68
|
+
method?: string;
|
|
69
|
+
path?: string;
|
|
70
|
+
now?: number;
|
|
71
|
+
}): ArtifactRequestContext;
|
|
72
|
+
/** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
|
|
73
|
+
export declare function getArtifactBrowserBootstrap(context: ArtifactRequestContext): NextArtifactBridgeBootstrap & {
|
|
74
|
+
initialContext: NextArtifactInitialContext;
|
|
75
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
/** Correlates logs, usage and audit records for the originating request. */
|
|
30
|
+
requestId: id,
|
|
31
|
+
/** Unique per signed context. Atlas budgets delegated calls per jti until the context expires. */
|
|
32
|
+
jti: z.uuid(),
|
|
33
|
+
/** The originating browser request's method. Atlas only delegates reads for GET, HEAD and OPTIONS. */
|
|
34
|
+
method: z.enum(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
|
|
35
|
+
path: z.string().min(1).max(8192).refine(value => value.startsWith('/') &&
|
|
36
|
+
!value.startsWith('//') && !/[\\\r\n\u0000#]/.test(value), 'Expected an origin-relative request path'),
|
|
37
|
+
iat: z.number().int().nonnegative(),
|
|
38
|
+
exp: z.number().int().positive(),
|
|
39
|
+
capabilities: z.array(id).max(256),
|
|
40
|
+
serviceAccountId: id.nullable(),
|
|
41
|
+
bridge: z.object({ parentOrigin: originSchema, channelId: id, nonce: z.string().min(16).max(255) }).strict(),
|
|
42
|
+
theme: z.enum(['light', 'dark', 'system']),
|
|
43
|
+
lattice: z.object({ runId: id.nullable(), nodeId: id.nullable(), stepId: id.nullable(), processId: id.nullable() }).strict(),
|
|
44
|
+
taskRoutes: z.array(z.string().min(1).max(512)).max(16).refine(routes => routes.join('').length <= 1024).optional(),
|
|
45
|
+
}).strict().superRefine((value, ctx) => {
|
|
46
|
+
if (value.aud !== `artifact:${value.projectId}`)
|
|
47
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid Artifact audience' });
|
|
48
|
+
if (value.exp <= value.iat || value.exp - value.iat > ARTIFACT_CONTEXT_MAX_AGE_SECONDS)
|
|
49
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid Artifact context lifetime' });
|
|
50
|
+
if (value.mode === 'development' ? !value.devSessionId || value.deploymentId !== null : !value.deploymentId || value.devSessionId !== null)
|
|
51
|
+
ctx.addIssue({ code: 'custom', message: 'Artifact runtime identity does not match its mode' });
|
|
52
|
+
});
|
|
53
|
+
export class ArtifactContextError extends Error {
|
|
54
|
+
constructor() { super('Missing or invalid Artifact gateway request context'); this.name = 'ArtifactContextError'; }
|
|
55
|
+
}
|
|
56
|
+
function decode(value) {
|
|
57
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
58
|
+
throw new ArtifactContextError();
|
|
59
|
+
return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
60
|
+
}
|
|
61
|
+
/** Only the gateway owns private keys. Applications pin platform-provided public keys. */
|
|
62
|
+
export function verifyArtifactRequestContext({ token, publicKeys, projectId, environment, method, path, now = Math.floor(Date.now() / 1000) }) {
|
|
63
|
+
try {
|
|
64
|
+
if (!token || token.length > 32768)
|
|
65
|
+
throw new ArtifactContextError();
|
|
66
|
+
const parts = token.split('.');
|
|
67
|
+
if (parts.length !== 3)
|
|
68
|
+
throw new ArtifactContextError();
|
|
69
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
70
|
+
const header = z.object({ alg: z.literal('EdDSA'), typ: z.literal('JWT'), kid: id }).strict().parse(decode(encodedHeader));
|
|
71
|
+
const pem = Object.hasOwn(publicKeys, header.kid) ? publicKeys[header.kid] : undefined;
|
|
72
|
+
if (!pem || !/^[A-Za-z0-9_-]+$/.test(signature))
|
|
73
|
+
throw new ArtifactContextError();
|
|
74
|
+
const key = createPublicKey(pem);
|
|
75
|
+
if (key.asymmetricKeyType !== 'ed25519' ||
|
|
76
|
+
!verify(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), key, Buffer.from(signature, 'base64url')))
|
|
77
|
+
throw new ArtifactContextError();
|
|
78
|
+
const context = artifactRequestContextSchema.parse(decode(encodedPayload));
|
|
79
|
+
if (context.projectId !== projectId || context.environment !== environment ||
|
|
80
|
+
context.exp <= now || context.iat > now + 5 ||
|
|
81
|
+
(method !== undefined && context.method !== method.toUpperCase()) || (path !== undefined && context.path !== path))
|
|
82
|
+
throw new ArtifactContextError();
|
|
83
|
+
Object.freeze(context.bridge);
|
|
84
|
+
Object.freeze(context.lattice);
|
|
85
|
+
Object.freeze(context.capabilities);
|
|
86
|
+
if (context.taskRoutes)
|
|
87
|
+
Object.freeze(context.taskRoutes);
|
|
88
|
+
return Object.freeze(context);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new ArtifactContextError();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Safe props for the client bridge; intentionally excludes actor and runtime credentials. */
|
|
95
|
+
export function getArtifactBrowserBootstrap(context) {
|
|
96
|
+
return {
|
|
97
|
+
parentOrigin: context.bridge.parentOrigin,
|
|
98
|
+
channelId: context.bridge.channelId,
|
|
99
|
+
nonce: context.bridge.nonce,
|
|
100
|
+
deploymentId: context.deploymentId,
|
|
101
|
+
initialContext: {
|
|
102
|
+
theme: { tokens: null, theme: null, dark: context.theme === 'dark', vars: {} },
|
|
103
|
+
lattice: context.lattice,
|
|
104
|
+
route: context.path,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
jti: string;
|
|
35
|
+
method: "GET" | "POST" | "DELETE" | "HEAD" | "PUT" | "PATCH" | "OPTIONS";
|
|
36
|
+
path: string;
|
|
37
|
+
iat: number;
|
|
38
|
+
exp: number;
|
|
39
|
+
capabilities: string[];
|
|
40
|
+
serviceAccountId: string | null;
|
|
41
|
+
bridge: {
|
|
42
|
+
parentOrigin: string;
|
|
43
|
+
channelId: string;
|
|
44
|
+
nonce: string;
|
|
45
|
+
};
|
|
46
|
+
theme: "dark" | "light" | "system";
|
|
47
|
+
lattice: {
|
|
48
|
+
runId: string | null;
|
|
49
|
+
nodeId: string | null;
|
|
50
|
+
stepId: string | null;
|
|
51
|
+
processId: string | null;
|
|
52
|
+
};
|
|
53
|
+
taskRoutes?: string[] | undefined;
|
|
54
|
+
} | 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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
export { createArtifactServerClient, ArtifactServerError } from './server/client.js';
|
|
3
|
+
export type { ArtifactOrmOperations, 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,157 @@
|
|
|
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
|
+
ORM access is declared per table and per mutation: list `<namespace>/<table>`
|
|
75
|
+
under `data.read` for every table a query reads (queries that follow
|
|
76
|
+
platform-user relations also read `<namespace>/platformUsers`). Under
|
|
77
|
+
`data.write`, use `<namespace>/ops/<Operation>` for each persisted mutation the
|
|
78
|
+
app runs, or `<namespace>/<table>` for table-level coverage.
|
|
79
|
+
|
|
80
|
+
Managed secrets stay in hosted runtimes. For local integration tests, use your
|
|
81
|
+
own test credentials, mocks or an existing governed function. Use a hosted
|
|
82
|
+
preview for application code that needs a managed secret. Never put secrets in
|
|
83
|
+
`build.public_env`, `NEXT_PUBLIC_*`, client components or committed files.
|
|
84
|
+
|
|
85
|
+
The local process is ordinary code running as your operating-system user.
|
|
86
|
+
Atlas governs access to platform resources; it does not sandbox your laptop's
|
|
87
|
+
filesystem or all outbound network connections.
|
|
88
|
+
|
|
89
|
+
## Build and deploy
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pnpm type-check
|
|
93
|
+
pnpm build
|
|
94
|
+
seq-studio artifact plan --env <your-environment>
|
|
95
|
+
seq-studio artifact deploy --repo <namespace>/<repository> --ref main --env <your-environment>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Push the reviewed application source and lockfile to its Git Service repository
|
|
99
|
+
before deploying. The local build is a validation step. Atlas performs the official isolated build
|
|
100
|
+
from recorded source and a lockfile, then operates the hosted Next.js server.
|
|
101
|
+
Deployments do not depend on your laptop staying online. Reviewers use hosted
|
|
102
|
+
previews rather than your owner-only local session.
|
|
103
|
+
|
|
104
|
+
For official releases to deployed environments, use a clean reviewed `main`
|
|
105
|
+
revision under the existing Artifact deployment policy. Inspect build/deployment
|
|
106
|
+
status, permissions and runtime diagnostics in Artifact Studio. Failed runtime
|
|
107
|
+
checks do not replace the previous healthy release.
|
|
108
|
+
|
|
109
|
+
The default scaffold is dynamic and declares no distributed cache. Enable managed
|
|
110
|
+
cache capacity in Atlas before adopting framework features that require shared
|
|
111
|
+
cross-replica caching; local Fast Refresh is not a test of distributed cache
|
|
112
|
+
behavior. Do not add a production cache endpoint or managed credentials to local
|
|
113
|
+
environment files.
|
|
114
|
+
|
|
115
|
+
## Toolchain updates
|
|
116
|
+
|
|
117
|
+
This scaffold pins Next.js `16.3.4`, React/React DOM `19.2.6` and pnpm `10.28.2`,
|
|
118
|
+
with Node major `24`. These are its selected versions, not a claim that they are
|
|
119
|
+
the latest security releases. Use your platform's reviewed compatible versions
|
|
120
|
+
when updating an existing app. The CLI and build path check the platform's exact
|
|
121
|
+
approved Next/React selection; a different patch needs a coordinated platform
|
|
122
|
+
update, even if it belongs to the same framework profile.
|
|
123
|
+
|
|
124
|
+
For an approved patch, update the dependency declarations and lockfile together,
|
|
125
|
+
run the local type/build checks, then verify a hosted preview before requesting
|
|
126
|
+
an official repository-backed deployment. Existing immutable deployments keep
|
|
127
|
+
their previous code until rebuilt and activated. Your platform team maintains
|
|
128
|
+
build/runtime images and handles any urgent security response; this workflow
|
|
129
|
+
does not require application authors to obtain cloud-provider credentials.
|
|
130
|
+
|
|
131
|
+
## Images and fonts
|
|
132
|
+
|
|
133
|
+
Keep static images and other binary assets in `public/`. Prefer committed WOFF2
|
|
134
|
+
fonts loaded with `next/font/local`; compilation runs without unrestricted
|
|
135
|
+
network access.
|
|
136
|
+
|
|
137
|
+
For a public font prepared by the platform, add `build.public_fonts` entries with
|
|
138
|
+
`path`, `url` and the actual lowercase `sha256` digest. Targets must be
|
|
139
|
+
`public/fonts/<filename>.woff2`, without nested directories. Use ASCII
|
|
140
|
+
letters/digits, hyphens or underscores for the basename, starting with a letter
|
|
141
|
+
or digit. URLs must use HTTPS on `fonts.gstatic.com` with its versioned
|
|
142
|
+
`/s/<family>/v<revision>/<file>.woff2` format and no query, fragment or custom
|
|
143
|
+
port. The platform allows
|
|
144
|
+
up to eight files, 2 MiB each and 8 MiB total, with no redirects. Connected local
|
|
145
|
+
development prepares the same files. Existing matching files are reused;
|
|
146
|
+
mismatches fail without overwriting your file. Load the result with
|
|
147
|
+
`next/font/local`. A plain `pnpm build` uses files already present locally and
|
|
148
|
+
does not prepare declared fonts. Do not use `next/font/google` to download during the
|
|
149
|
+
network-blocked compilation stage.
|
|
150
|
+
|
|
151
|
+
For a remote optimized image, add the exact HTTPS host to both Next's
|
|
152
|
+
`images.remotePatterns` and the manifest's `server.egress`. Narrow the pathname
|
|
153
|
+
to the content needed by the app. Wildcard hosts, custom ports, redirects,
|
|
154
|
+
custom loaders, SVG optimization and private-IP destinations are unsupported.
|
|
155
|
+
Remote bodies are capped at 10 MiB, or a lower configured limit. Keep the
|
|
156
|
+
`withArtifactNextConfig` wrapper in place and verify remote images in a hosted
|
|
157
|
+
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
|
+
}
|