@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,143 @@
1
+ import { createHmac, createPrivateKey, randomUUID, sign, timingSafeEqual } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { artifactRequestContextSchema } from '../server/request-context.js';
4
+ import { normalizePublicOrigin } from '../tunnel/protocol.js';
5
+ import { artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
6
+ export const GATEWAY_SESSION_COOKIE = '__Host-atlas-artifact-session';
7
+ export function gatewayReleaseCookieName(deploymentId) {
8
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(deploymentId))
9
+ throw new Error('Invalid deployment identifier');
10
+ return `${GATEWAY_SESSION_COOKIE}-${deploymentId}`;
11
+ }
12
+ const origin = z.string().refine(value => { try {
13
+ return normalizePublicOrigin(value) === value;
14
+ }
15
+ catch {
16
+ return false;
17
+ } });
18
+ const id = z.string().min(1).max(255);
19
+ const sessionSchema = z.object({
20
+ version: z.literal(1), kind: z.enum(['development', 'hosted']),
21
+ sessionId: id.nullable(), deploymentId: id.nullable(),
22
+ frameworkBuildId: artifactNextFrameworkBuildIdSchema.nullable(), projectId: id, ownerId: id,
23
+ environment: id, publicOrigin: origin, expiresAt: z.number().int().positive(), maxExpiresAt: z.number().int().positive().optional(), published: z.boolean(),
24
+ bridge: artifactRequestContextSchema.shape.bridge,
25
+ lattice: artifactRequestContextSchema.shape.lattice.optional(),
26
+ taskRoutes: artifactRequestContextSchema.shape.taskRoutes,
27
+ }).strict().refine(value => value.kind === 'development'
28
+ ? value.sessionId !== null && value.deploymentId === null && value.frameworkBuildId === null && !value.published
29
+ : value.sessionId === null && value.deploymentId !== null && value.frameworkBuildId !== null);
30
+ function sameSessionScope(left, right) {
31
+ return left.ownerId === right.ownerId && left.projectId === right.projectId &&
32
+ left.environment === right.environment && left.publicOrigin === right.publicOrigin &&
33
+ left.kind === right.kind && left.published === right.published;
34
+ }
35
+ /** Active use renews the idle window, but never extends the original admission's eight-hour bound. */
36
+ export function renewGatewaySession({ session, authorizedExpiresAt, now = Date.now() }) {
37
+ if (session.expiresAt <= now || session.expiresAt - now > 15 * 60_000)
38
+ return null;
39
+ const expiresAt = Math.min(authorizedExpiresAt, session.maxExpiresAt ?? session.expiresAt);
40
+ return expiresAt > session.expiresAt ? { ...session, expiresAt } : null;
41
+ }
42
+ export function gatewaySessionCookies({ session, key, cookie, renewal = false, now = Date.now() }) {
43
+ const maxAge = Math.max(0, Math.floor((session.expiresAt - now) / 1000));
44
+ const value = `${encodeGatewaySession({ session, key })}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${maxAge}`;
45
+ const current = renewal ? decodeGatewaySession({ cookie, key, origin: session.publicOrigin, now }) : session;
46
+ if (!current || !sameSessionScope(current, session))
47
+ return [];
48
+ const sameTarget = current.deploymentId === session.deploymentId && current.sessionId === session.sessionId;
49
+ const cookies = sameTarget ? [`${GATEWAY_SESSION_COOKIE}=${value}`] : [];
50
+ if (session.deploymentId)
51
+ cookies.push(`${gatewayReleaseCookieName(session.deploymentId)}=${value}`);
52
+ if (!renewal) {
53
+ const selected = session.deploymentId ? gatewayReleaseCookieName(session.deploymentId) : '';
54
+ const retained = [...new Set((cookie ?? '').split(';').map(item => item.trim().split('=', 1)[0])
55
+ .filter(name => name.startsWith(`${GATEWAY_SESSION_COOKIE}-`) && name !== selected && /^[a-zA-Z0-9_-]+$/.test(name)))];
56
+ const sameScope = [], revoked = [];
57
+ for (const name of retained) {
58
+ const release = decodeSignedCookie({ cookie, key, origin: session.publicOrigin, now,
59
+ deploymentId: name.slice(GATEWAY_SESSION_COOKIE.length + 1) });
60
+ if (release && sameSessionScope(release, session))
61
+ sameScope.push(name);
62
+ else
63
+ revoked.push(name);
64
+ }
65
+ for (const name of [...revoked, ...sameScope.slice(0, Math.max(0, sameScope.length - 5))]) {
66
+ cookies.push(`${name}=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0`);
67
+ }
68
+ }
69
+ return cookies;
70
+ }
71
+ function mac(value, key) {
72
+ if (Buffer.byteLength(key) < 32)
73
+ throw new Error('Gateway session key requires at least 32 bytes');
74
+ return createHmac('sha256', key).update(value).digest();
75
+ }
76
+ export function encodeGatewaySession({ session, key }) {
77
+ const payload = Buffer.from(JSON.stringify(sessionSchema.parse(session))).toString('base64url');
78
+ return `${payload}.${mac(payload, key).toString('base64url')}`;
79
+ }
80
+ function decodeSignedCookie({ cookie, key, origin: expectedOrigin, now = Date.now(), deploymentId }) {
81
+ try {
82
+ if (!cookie || cookie.length > 32768)
83
+ return null;
84
+ const name = deploymentId ? gatewayReleaseCookieName(deploymentId) : GATEWAY_SESSION_COOKIE;
85
+ const values = cookie.split(';').map(value => value.trim()).filter(value => value.startsWith(`${name}=`));
86
+ if (values.length !== 1)
87
+ return null;
88
+ const [payload, signature, extra] = values[0].slice(name.length + 1).split('.');
89
+ if (!payload || !signature || extra || !/^[A-Za-z0-9_-]+$/.test(payload) || !/^[A-Za-z0-9_-]+$/.test(signature))
90
+ return null;
91
+ const expected = mac(payload, key), actual = Buffer.from(signature, 'base64url');
92
+ if (actual.length !== expected.length || !timingSafeEqual(expected, actual))
93
+ return null;
94
+ const session = sessionSchema.parse(JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')));
95
+ return session.expiresAt > now && session.publicOrigin === expectedOrigin &&
96
+ (!deploymentId || session.deploymentId === deploymentId) ? session : null;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /** Retained releases preserve tabs, never a different login or runtime scope. */
103
+ export function decodeGatewaySession(input) {
104
+ const current = decodeSignedCookie({ ...input, deploymentId: undefined });
105
+ if (!current)
106
+ return null;
107
+ const matchesBuild = (candidate) => !input.frameworkBuildId || candidate.frameworkBuildId === input.frameworkBuildId;
108
+ if (input.deploymentId) {
109
+ const selected = current.deploymentId === input.deploymentId ? current : decodeSignedCookie(input);
110
+ return selected && sameSessionScope(current, selected) && matchesBuild(selected) ? selected : null;
111
+ }
112
+ // Next's build id identifies immutable assets, not an activation. A rollback
113
+ // can reuse that build: prefer the current activation when it matches.
114
+ if (matchesBuild(current))
115
+ return current;
116
+ const names = [...new Set((input.cookie ?? '').split(';').map(item => item.trim().split('=', 1)[0])
117
+ .filter(name => name.startsWith(`${GATEWAY_SESSION_COOKIE}-`) && /^[a-zA-Z0-9_-]+$/.test(name)))];
118
+ const matches = names.map(name => decodeSignedCookie({ ...input,
119
+ deploymentId: name.slice(GATEWAY_SESSION_COOKIE.length + 1),
120
+ })).filter((candidate) => candidate !== null && sameSessionScope(current, candidate) && matchesBuild(candidate));
121
+ // Several retained activations may share identical output. Prefer the latest
122
+ // admitted session deterministically; every candidate is reauthorized below.
123
+ return matches.sort((left, right) => right.expiresAt - left.expiresAt || (left.deploymentId ?? '').localeCompare(right.deploymentId ?? ''))[0] ?? null;
124
+ }
125
+ export function signGatewayContext({ session, method, path, privateKey, keyId, now = Math.floor(Date.now() / 1000), capabilities = [], serviceAccountId = null, requestId = randomUUID() }) {
126
+ const context = artifactRequestContextSchema.parse({
127
+ v: 1, iss: 'atlas-artifact-gateway', aud: `artifact:${session.projectId}`,
128
+ sub: session.ownerId, projectId: session.projectId, environment: session.environment,
129
+ mode: session.kind === 'development' ? 'development' : session.published ? 'published' : 'preview',
130
+ deploymentId: session.deploymentId, devSessionId: session.sessionId, requestId,
131
+ method, path, iat: now, exp: now + 60, capabilities, serviceAccountId,
132
+ bridge: session.bridge, theme: 'system',
133
+ lattice: session.lattice ?? { runId: null, nodeId: null, stepId: null, processId: null },
134
+ ...(session.taskRoutes ? { taskRoutes: session.taskRoutes } : {}),
135
+ });
136
+ const header = Buffer.from(JSON.stringify({ alg: 'EdDSA', typ: 'JWT', kid: keyId })).toString('base64url');
137
+ const payload = Buffer.from(JSON.stringify(context)).toString('base64url');
138
+ const key = createPrivateKey(privateKey);
139
+ if (key.asymmetricKeyType !== 'ed25519')
140
+ throw new Error('Gateway context key must be Ed25519');
141
+ const signed = `${header}.${payload}`;
142
+ return `${signed}.${sign(null, Buffer.from(signed), key).toString('base64url')}`;
143
+ }
@@ -0,0 +1,37 @@
1
+ export interface GatewayUsageEvent {
2
+ eventId: string;
3
+ requestId: string;
4
+ projectId: string;
5
+ deploymentId?: string;
6
+ devSessionId?: string;
7
+ environment: string;
8
+ startedAt: string;
9
+ endedAt: string;
10
+ measurements: {
11
+ requestCount: 1;
12
+ durationMs: number;
13
+ };
14
+ outcome: 'success' | 'error' | 'cancelled';
15
+ httpStatus: number;
16
+ }
17
+ /** Bounded delivery of observations; accepted records are deduplicated durably by Atlas.
18
+ * Process loss can drop pending observations, so these are not provider billing totals.
19
+ */
20
+ export declare class GatewayUsageReporter {
21
+ private readonly options;
22
+ private readonly queue;
23
+ private readonly pending;
24
+ constructor(options: {
25
+ send: (event: GatewayUsageEvent) => Promise<void>;
26
+ onFailure: (event: {
27
+ kind: string;
28
+ requestId: string;
29
+ }) => void;
30
+ capacity?: number;
31
+ concurrency?: number;
32
+ });
33
+ record(event: GatewayUsageEvent): void;
34
+ private pump;
35
+ private deliver;
36
+ flush(): Promise<void>;
37
+ }
@@ -0,0 +1,46 @@
1
+ /** Bounded delivery of observations; accepted records are deduplicated durably by Atlas.
2
+ * Process loss can drop pending observations, so these are not provider billing totals.
3
+ */
4
+ export class GatewayUsageReporter {
5
+ options;
6
+ queue = [];
7
+ pending = new Set();
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ record(event) {
12
+ if (this.queue.length + this.pending.size >= (this.options.capacity ?? 1024)) {
13
+ this.options.onFailure({ kind: 'usage_queue_full', requestId: event.requestId });
14
+ return;
15
+ }
16
+ this.queue.push(event);
17
+ this.pump();
18
+ }
19
+ pump() {
20
+ while (this.queue.length && this.pending.size < (this.options.concurrency ?? 8)) {
21
+ const event = this.queue.shift();
22
+ const pending = this.deliver(event).finally(() => { this.pending.delete(pending); this.pump(); });
23
+ this.pending.add(pending);
24
+ }
25
+ }
26
+ async deliver(event) {
27
+ for (let attempt = 0; attempt < 3; attempt++) {
28
+ try {
29
+ await this.options.send(event);
30
+ return;
31
+ }
32
+ catch (error) {
33
+ const status = typeof error === 'object' && error !== null && 'status' in error ? error.status : undefined;
34
+ if (typeof status === 'number' && status >= 400 && status < 500 && status !== 429)
35
+ break;
36
+ if (attempt < 2)
37
+ await new Promise(resolve => setTimeout(resolve, 100 * 2 ** attempt));
38
+ }
39
+ }
40
+ this.options.onFailure({ kind: 'usage_delivery_failed', requestId: event.requestId });
41
+ }
42
+ async flush() {
43
+ while (this.pending.size)
44
+ await Promise.all(this.pending);
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { Redis } from 'ioredis';
3
+ import { ArtifactGatewayControl } from './gateway/control.js';
4
+ import { createArtifactGateway } from './gateway/server.js';
5
+ import { RedisTunnelOwnership, RedisTunnelRelay } from './tunnel/redis.js';
6
+ import { ArtifactRequestAdmission } from './gateway/admission.js';
7
+ function required(name) {
8
+ const value = process.env[name];
9
+ if (!value)
10
+ throw new Error(`Missing required gateway configuration: ${name}`);
11
+ return value;
12
+ }
13
+ /** A separate process/identity from Atlas and all application/build containers. */
14
+ async function main() {
15
+ if (process.env.ARTIFACT_NEXT_ENABLED !== 'true')
16
+ throw new Error('Artifact Next gateway is disabled');
17
+ const redisUrl = required('ARTIFACT_NEXT_REDIS_URL');
18
+ if (new URL(redisUrl).protocol !== 'rediss:')
19
+ throw new Error('Artifact tunnel routing requires private Redis with TLS');
20
+ const namespace = required('APP_ENV'), nodeId = randomUUID();
21
+ const redisOptions = {
22
+ enableOfflineQueue: false, autoResendUnfulfilledCommands: false, maxRetriesPerRequest: 0,
23
+ lazyConnect: true, connectTimeout: 10_000,
24
+ tls: { rejectUnauthorized: true, ...(process.env.REDIS_CA_CERT ? { ca: process.env.REDIS_CA_CERT } : {}) },
25
+ };
26
+ const command = new Redis(redisUrl, redisOptions);
27
+ const publisher = new Redis(redisUrl, redisOptions);
28
+ const subscriber = new Redis(redisUrl, { ...redisOptions, autoResubscribe: false });
29
+ const clients = [command, publisher, subscriber];
30
+ for (const client of clients)
31
+ client.on('error', () => {
32
+ // Provider messages may include connection strings; keep them out of logs.
33
+ process.stderr.write(`${JSON.stringify({ event: 'artifact_gateway.redis_unavailable', nodeId })}\n`);
34
+ });
35
+ await Promise.all(clients.map(client => client.connect()));
36
+ const ownership = new RedisTunnelOwnership({ command, namespace });
37
+ const relay = new RedisTunnelRelay({ nodeId, namespace, ownership, publisher, subscriber });
38
+ await relay.start();
39
+ let stopping = false;
40
+ subscriber.on('ready', () => { if (!stopping)
41
+ void relay.start().catch(() => undefined); });
42
+ const controlUrl = required('ARTIFACT_NEXT_CONTROL_PLANE_URL');
43
+ const gateway = createArtifactGateway({
44
+ control: new ArtifactGatewayControl({ url: controlUrl, token: required('ARTIFACT_NEXT_CONTROL_TOKEN') }),
45
+ runtimeDomain: required('ARTIFACT_NEXT_RUNTIME_DOMAIN'),
46
+ parentOrigin: process.env.ARTIFACT_NEXT_PARENT_ORIGIN ?? new URL(controlUrl).origin,
47
+ gatewayOrigin: required('ARTIFACT_NEXT_GATEWAY_URL'),
48
+ sessionKey: required('ARTIFACT_NEXT_SESSION_KEY'),
49
+ contextPrivateKey: required('ARTIFACT_NEXT_CONTEXT_PRIVATE_KEY'),
50
+ contextKeyId: required('ARTIFACT_NEXT_CONTEXT_KEY_ID'),
51
+ nodeId, routing: ownership, relay,
52
+ admission: new ArtifactRequestAdmission({ redis: command, namespace }),
53
+ resolveLease: sessionId => ownership.resolveLease(sessionId),
54
+ renewLease: lease => ownership.renew(lease, nodeId),
55
+ ready: () => clients.every(client => client.status === 'ready') && relay.isReady(),
56
+ onError: event => process.stderr.write(`${JSON.stringify({ event: 'artifact_gateway.request', ...event, nodeId })}\n`),
57
+ });
58
+ const port = Number(process.env.PORT ?? '8080');
59
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535)
60
+ throw new Error('Invalid gateway PORT');
61
+ await new Promise((resolve, reject) => {
62
+ gateway.server.once('error', reject);
63
+ gateway.server.listen(port, '0.0.0.0', resolve);
64
+ });
65
+ process.stdout.write(`${JSON.stringify({ event: 'artifact_gateway.ready', nodeId, port })}\n`);
66
+ const stop = async () => {
67
+ if (stopping)
68
+ return;
69
+ stopping = true;
70
+ const deadline = setTimeout(() => { gateway.server.closeAllConnections(); process.exit(1); }, 9_000);
71
+ deadline.unref();
72
+ await gateway.drain();
73
+ await relay.close();
74
+ for (const client of clients)
75
+ client.disconnect();
76
+ clearTimeout(deadline);
77
+ };
78
+ process.once('SIGTERM', () => { void stop(); });
79
+ process.once('SIGINT', () => { void stop(); });
80
+ }
81
+ void main().catch(() => {
82
+ process.stderr.write(`${JSON.stringify({ event: 'artifact_gateway.start_failed' })}\n`);
83
+ process.exit(1);
84
+ });
@@ -15,11 +15,29 @@ export type RunGitClone = (args: {
15
15
  destDir: string;
16
16
  /** Optional branch, tag, or commit SHA. */
17
17
  ref?: string;
18
+ /**
19
+ * `git clone --depth`. Omitted for commit-SHA refs: those clone the
20
+ * default branch in full, then check the SHA out.
21
+ */
22
+ depth?: number;
18
23
  /** PAT used as Basic-auth password (username `git`). */
19
24
  pat: string;
20
25
  /** Hard ceiling before the git child is killed (default 5m). */
21
26
  timeoutMs?: number;
22
27
  }) => Promise<void>;
28
+ /**
29
+ * Argv for `git clone`, excluding the askpass environment.
30
+ * Positive `depth` is a shallow tip clone. Commit SHAs ignore `depth`.
31
+ */
32
+ export declare function gitCloneArgs({ cloneUrl, destDir, ref, depth, }: {
33
+ cloneUrl: string;
34
+ destDir: string;
35
+ ref?: string;
36
+ depth?: number;
37
+ }): {
38
+ args: string[];
39
+ shaRef?: string;
40
+ };
23
41
  /**
24
42
  * Run `fn` with GIT_ASKPASS set to a short-lived script that answers username
25
43
  * with `git` and password with `pat`. The script is deleted afterward.
@@ -31,11 +49,15 @@ export declare function withGitAskpass<T>({ pat, fn, }: {
31
49
  /**
32
50
  * Clone with a clean URL + PAT via askpass. Branch/tag refs use `--branch`;
33
51
  * commit SHAs clone default HEAD then `git checkout <sha>`.
52
+ * `depth` (repos clone passes 1) fetches only that many commits — a full
53
+ * history walk against git-service reads every object from object storage
54
+ * before the pack starts, which stalls large repos.
34
55
  */
35
- export declare function runGitClone({ cloneUrl, destDir, ref, pat, timeoutMs, }: {
56
+ export declare function runGitClone({ cloneUrl, destDir, ref, depth, pat, timeoutMs, }: {
36
57
  cloneUrl: string;
37
58
  destDir: string;
38
59
  ref?: string;
60
+ depth?: number;
39
61
  pat: string;
40
62
  timeoutMs?: number;
41
63
  }): Promise<void>;
package/dist/git-clone.js CHANGED
@@ -55,6 +55,28 @@ const GIT_LOW_SPEED_LIMIT_BYTES = 1000;
55
55
  const GIT_LOW_SPEED_TIME_SEC = 180;
56
56
  /** Backstop hard deadline: SIGKILL the child even if it trickles above the low-speed floor. */
57
57
  const GIT_CLONE_TIMEOUT_MS = 6 * 60_000;
58
+ /**
59
+ * Argv for `git clone`, excluding the askpass environment.
60
+ * Positive `depth` is a shallow tip clone. Commit SHAs ignore `depth`.
61
+ */
62
+ export function gitCloneArgs({ cloneUrl, destDir, ref, depth, }) {
63
+ const shaRef = ref && looksLikeCommitSha(ref) ? ref : undefined;
64
+ const branchRef = ref && !shaRef ? ref : undefined;
65
+ const args = [
66
+ '-c',
67
+ `http.lowSpeedLimit=${GIT_LOW_SPEED_LIMIT_BYTES}`,
68
+ '-c',
69
+ `http.lowSpeedTime=${GIT_LOW_SPEED_TIME_SEC}`,
70
+ 'clone',
71
+ ];
72
+ if (depth != null && depth > 0 && !shaRef) {
73
+ args.push('--depth', String(depth));
74
+ }
75
+ if (branchRef)
76
+ args.push('--branch', branchRef);
77
+ args.push(cloneUrl, destDir);
78
+ return shaRef ? { args, shaRef } : { args };
79
+ }
58
80
  /** First non-flag arg (the git subcommand), skipping `-c <value>` config pairs. */
59
81
  function gitVerb(args) {
60
82
  for (let i = 0; i < args.length; i++) {
@@ -142,8 +164,11 @@ export async function withGitAskpass({ pat, fn, }) {
142
164
  /**
143
165
  * Clone with a clean URL + PAT via askpass. Branch/tag refs use `--branch`;
144
166
  * commit SHAs clone default HEAD then `git checkout <sha>`.
167
+ * `depth` (repos clone passes 1) fetches only that many commits — a full
168
+ * history walk against git-service reads every object from object storage
169
+ * before the pack starts, which stalls large repos.
145
170
  */
146
- export async function runGitClone({ cloneUrl, destDir, ref, pat, timeoutMs, }) {
171
+ export async function runGitClone({ cloneUrl, destDir, ref, depth, pat, timeoutMs, }) {
147
172
  let cleanUrl = cloneUrl;
148
173
  try {
149
174
  const parsed = new URL(cloneUrl);
@@ -156,23 +181,15 @@ export async function runGitClone({ cloneUrl, destDir, ref, pat, timeoutMs, }) {
156
181
  catch {
157
182
  // leave as-is; git will fail loudly
158
183
  }
159
- // Global `-c` config must precede the subcommand. Low-speed settings bound
160
- // the fetch so a stalled transport aborts fast rather than wedging the deploy.
161
- const lowSpeed = [
162
- '-c',
163
- `http.lowSpeedLimit=${GIT_LOW_SPEED_LIMIT_BYTES}`,
164
- '-c',
165
- `http.lowSpeedTime=${GIT_LOW_SPEED_TIME_SEC}`,
166
- ];
167
184
  await withGitAskpass({
168
185
  pat,
169
186
  fn: async (env) => {
170
- const args = [...lowSpeed, 'clone'];
171
- const shaRef = ref && looksLikeCommitSha(ref) ? ref : undefined;
172
- const branchRef = ref && !shaRef ? ref : undefined;
173
- if (branchRef)
174
- args.push('--branch', branchRef);
175
- args.push(cleanUrl, destDir);
187
+ const { args, shaRef } = gitCloneArgs({
188
+ cloneUrl: cleanUrl,
189
+ destDir,
190
+ ...(ref ? { ref } : {}),
191
+ ...(depth != null ? { depth } : {}),
192
+ });
176
193
  try {
177
194
  await spawnGit({ args, env, ...(timeoutMs ? { timeoutMs } : {}) });
178
195
  }
@@ -0,0 +1,217 @@
1
+ import { z } from 'zod';
2
+ /** Approved major/minor profile. Builds additionally record exact patch versions. */
3
+ export declare const ARTIFACT_NEXT_RUNTIME: Readonly<{
4
+ sdk: "sequence";
5
+ node: 24;
6
+ react: 19;
7
+ next: "16.3";
8
+ }>;
9
+ /** Admission policy for new development sessions and builds; retained releases keep their recorded toolchain. */
10
+ export declare const ARTIFACT_NEXT_APPROVED_PACKAGES: Readonly<{
11
+ next: "16.3.4";
12
+ react: "19.2.6";
13
+ 'react-dom': "19.2.6";
14
+ }>;
15
+ export declare const ARTIFACT_NEXT_RESOURCE_DEFAULTS: {
16
+ cpu: number;
17
+ memory_mb: number;
18
+ concurrency: number;
19
+ min_instances: number;
20
+ max_instances: number;
21
+ timeout_seconds: number;
22
+ };
23
+ export declare const artifactNextServerCapabilitiesSchema: z.ZodPrefault<z.ZodObject<{
24
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
25
+ viewer: "viewer";
26
+ editor: "editor";
27
+ admin: "admin";
28
+ }>>>;
29
+ data: z.ZodPrefault<z.ZodObject<{
30
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
31
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
32
+ }, z.core.$strict>>;
33
+ api: z.ZodPrefault<z.ZodObject<{
34
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
35
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
36
+ }, z.core.$strict>>;
37
+ functions: z.ZodPrefault<z.ZodObject<{
38
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
+ }, z.core.$strict>>;
40
+ }, z.core.$strict>>;
41
+ export type ArtifactNextServerCapabilities = z.infer<typeof artifactNextServerCapabilitiesSchema>;
42
+ export declare const artifactNextResourceLimitsSchema: z.ZodPrefault<z.ZodObject<{
43
+ cpu: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<4>, z.ZodLiteral<8>]>>;
44
+ memory_mb: z.ZodDefault<z.ZodNumber>;
45
+ concurrency: z.ZodDefault<z.ZodNumber>;
46
+ min_instances: z.ZodDefault<z.ZodNumber>;
47
+ max_instances: z.ZodDefault<z.ZodNumber>;
48
+ timeout_seconds: z.ZodDefault<z.ZodNumber>;
49
+ }, z.core.$strict>>;
50
+ export declare const artifactNextSecretNameSchema: z.ZodString;
51
+ export declare const artifactNextSecretBindingSchema: z.ZodObject<{
52
+ name: z.ZodString;
53
+ secret_id: z.ZodUUID;
54
+ version: z.ZodNumber;
55
+ }, z.core.$strict>;
56
+ /** Pinned public assets fetched by the trusted preparation stage before offline compilation. */
57
+ export declare const artifactNextPublicFontSchema: z.ZodObject<{
58
+ path: z.ZodString;
59
+ url: z.ZodString;
60
+ sha256: z.ZodString;
61
+ }, z.core.$strict>;
62
+ export type ArtifactNextPublicFont = z.infer<typeof artifactNextPublicFontSchema>;
63
+ export declare const artifactNextBuildConfigSchema: z.ZodPrefault<z.ZodObject<{
64
+ public_env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
65
+ public_fonts: z.ZodDefault<z.ZodArray<z.ZodObject<{
66
+ path: z.ZodString;
67
+ url: z.ZodString;
68
+ sha256: z.ZodString;
69
+ }, z.core.$strict>>>;
70
+ }, z.core.$strict>>;
71
+ export declare const artifactNextServerSchema: z.ZodPrefault<z.ZodObject<{
72
+ capabilities: z.ZodPrefault<z.ZodObject<{
73
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
74
+ viewer: "viewer";
75
+ editor: "editor";
76
+ admin: "admin";
77
+ }>>>;
78
+ data: z.ZodPrefault<z.ZodObject<{
79
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
80
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
81
+ }, z.core.$strict>>;
82
+ api: z.ZodPrefault<z.ZodObject<{
83
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
84
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
85
+ }, z.core.$strict>>;
86
+ functions: z.ZodPrefault<z.ZodObject<{
87
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
88
+ }, z.core.$strict>>;
89
+ }, z.core.$strict>>;
90
+ service_account: z.ZodOptional<z.ZodString>;
91
+ secrets: z.ZodDefault<z.ZodArray<z.ZodObject<{
92
+ name: z.ZodString;
93
+ secret_id: z.ZodUUID;
94
+ version: z.ZodNumber;
95
+ }, z.core.$strict>>>;
96
+ egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
97
+ resources: z.ZodPrefault<z.ZodObject<{
98
+ cpu: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<4>, z.ZodLiteral<8>]>>;
99
+ memory_mb: z.ZodDefault<z.ZodNumber>;
100
+ concurrency: z.ZodDefault<z.ZodNumber>;
101
+ min_instances: z.ZodDefault<z.ZodNumber>;
102
+ max_instances: z.ZodDefault<z.ZodNumber>;
103
+ timeout_seconds: z.ZodDefault<z.ZodNumber>;
104
+ }, z.core.$strict>>;
105
+ cache: z.ZodOptional<z.ZodObject<{
106
+ capacity_gb: z.ZodNumber;
107
+ }, z.core.$strict>>;
108
+ }, z.core.$strict>>;
109
+ export declare const artifactStudioNextManifestSchema: z.ZodObject<{
110
+ bundle: z.ZodObject<{
111
+ name: z.ZodString;
112
+ schema_version: z.ZodLiteral<2>;
113
+ }, z.core.$strict>;
114
+ artifact: z.ZodObject<{
115
+ project_id: z.ZodString;
116
+ title: z.ZodString;
117
+ description: z.ZodOptional<z.ZodString>;
118
+ type: z.ZodLiteral<"nextjs">;
119
+ app_root: z.ZodDefault<z.ZodString>;
120
+ }, z.core.$strict>;
121
+ runtime: z.ZodPrefault<z.ZodObject<{
122
+ sdk: z.ZodDefault<z.ZodLiteral<"sequence">>;
123
+ node: z.ZodDefault<z.ZodLiteral<24>>;
124
+ react: z.ZodDefault<z.ZodLiteral<19>>;
125
+ next: z.ZodDefault<z.ZodLiteral<"16.3">>;
126
+ }, z.core.$strict>>;
127
+ capabilities: z.ZodPrefault<z.ZodObject<{
128
+ allow_downloads: z.ZodDefault<z.ZodBoolean>;
129
+ allow_popups: z.ZodDefault<z.ZodBoolean>;
130
+ roles: z.ZodOptional<z.ZodObject<{
131
+ definitions: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
132
+ description: z.ZodOptional<z.ZodString>;
133
+ default: z.ZodDefault<z.ZodBoolean>;
134
+ inherits: z.ZodDefault<z.ZodArray<z.ZodString>>;
135
+ grants: z.ZodDefault<z.ZodArray<z.ZodString>>;
136
+ }, z.core.$strict>>>;
137
+ capabilities: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
138
+ description: z.ZodOptional<z.ZodString>;
139
+ }, z.core.$strict>>>;
140
+ }, z.core.$strict>>;
141
+ uses: z.ZodDefault<z.ZodArray<z.ZodString>>;
142
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
143
+ viewer: "viewer";
144
+ editor: "editor";
145
+ admin: "admin";
146
+ }>>>;
147
+ data: z.ZodPrefault<z.ZodObject<{
148
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
149
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
150
+ }, z.core.$strict>>;
151
+ api: z.ZodPrefault<z.ZodObject<{
152
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
153
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
154
+ }, z.core.$strict>>;
155
+ functions: z.ZodPrefault<z.ZodObject<{
156
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
157
+ }, z.core.$strict>>;
158
+ }, z.core.$strict>>;
159
+ build: z.ZodPrefault<z.ZodObject<{
160
+ public_env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
161
+ public_fonts: z.ZodDefault<z.ZodArray<z.ZodObject<{
162
+ path: z.ZodString;
163
+ url: z.ZodString;
164
+ sha256: z.ZodString;
165
+ }, z.core.$strict>>>;
166
+ }, z.core.$strict>>;
167
+ server: z.ZodPrefault<z.ZodObject<{
168
+ capabilities: z.ZodPrefault<z.ZodObject<{
169
+ storage: z.ZodOptional<z.ZodRecord<z.ZodUUID, z.ZodEnum<{
170
+ viewer: "viewer";
171
+ editor: "editor";
172
+ admin: "admin";
173
+ }>>>;
174
+ data: z.ZodPrefault<z.ZodObject<{
175
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
176
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
177
+ }, z.core.$strict>>;
178
+ api: z.ZodPrefault<z.ZodObject<{
179
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
180
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
181
+ }, z.core.$strict>>;
182
+ functions: z.ZodPrefault<z.ZodObject<{
183
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
184
+ }, z.core.$strict>>;
185
+ }, z.core.$strict>>;
186
+ service_account: z.ZodOptional<z.ZodString>;
187
+ secrets: z.ZodDefault<z.ZodArray<z.ZodObject<{
188
+ name: z.ZodString;
189
+ secret_id: z.ZodUUID;
190
+ version: z.ZodNumber;
191
+ }, z.core.$strict>>>;
192
+ egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
193
+ resources: z.ZodPrefault<z.ZodObject<{
194
+ cpu: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<4>, z.ZodLiteral<8>]>>;
195
+ memory_mb: z.ZodDefault<z.ZodNumber>;
196
+ concurrency: z.ZodDefault<z.ZodNumber>;
197
+ min_instances: z.ZodDefault<z.ZodNumber>;
198
+ max_instances: z.ZodDefault<z.ZodNumber>;
199
+ timeout_seconds: z.ZodDefault<z.ZodNumber>;
200
+ }, z.core.$strict>>;
201
+ cache: z.ZodOptional<z.ZodObject<{
202
+ capacity_gb: z.ZodNumber;
203
+ }, z.core.$strict>>;
204
+ }, z.core.$strict>>;
205
+ targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
206
+ url: z.ZodString;
207
+ }, z.core.$strict>>>;
208
+ sidebarIcon: z.ZodOptional<z.ZodString>;
209
+ sidebarBadge: z.ZodOptional<z.ZodObject<{
210
+ endpoint: z.ZodString;
211
+ tone: z.ZodOptional<z.ZodEnum<{
212
+ alert: "alert";
213
+ muted: "muted";
214
+ }>>;
215
+ }, z.core.$strict>>;
216
+ }, z.core.$strict>;
217
+ export type ArtifactStudioNextManifest = z.infer<typeof artifactStudioNextManifestSchema>;