@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.
Files changed (190) hide show
  1. package/VERSION-PIN.md +12 -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 +134 -13
  8. package/dist/dev-link.d.ts +1 -1
  9. package/dist/dev-link.js +2 -1
  10. package/dist/gateway/admission.d.ts +56 -0
  11. package/dist/gateway/admission.js +134 -0
  12. package/dist/gateway/authorization-cache.d.ts +52 -0
  13. package/dist/gateway/authorization-cache.js +111 -0
  14. package/dist/gateway/control-admission.d.ts +48 -0
  15. package/dist/gateway/control-admission.js +127 -0
  16. package/dist/gateway/control.d.ts +94 -0
  17. package/dist/gateway/control.js +116 -0
  18. package/dist/gateway/hosted-proxy.d.ts +36 -0
  19. package/dist/gateway/hosted-proxy.js +344 -0
  20. package/dist/gateway/release-admission.d.ts +23 -0
  21. package/dist/gateway/release-admission.js +259 -0
  22. package/dist/gateway/server.d.ts +38 -0
  23. package/dist/gateway/server.js +382 -0
  24. package/dist/gateway/session.d.ts +72 -0
  25. package/dist/gateway/session.js +144 -0
  26. package/dist/gateway/usage.d.ts +70 -0
  27. package/dist/gateway/usage.js +116 -0
  28. package/dist/gateway-main.d.ts +1 -0
  29. package/dist/gateway-main.js +114 -0
  30. package/dist/git-clone.d.ts +23 -1
  31. package/dist/git-clone.js +32 -15
  32. package/dist/manifest-next.d.ts +217 -0
  33. package/dist/manifest-next.js +132 -0
  34. package/dist/manifest-static.d.ts +76 -0
  35. package/dist/manifest-static.js +124 -0
  36. package/dist/manifest.d.ts +118 -13
  37. package/dist/manifest.js +12 -119
  38. package/dist/next/bridge.d.ts +47 -0
  39. package/dist/next/bridge.js +236 -0
  40. package/dist/next/browser-logs.d.ts +7 -0
  41. package/dist/next/browser-logs.js +51 -0
  42. package/dist/next/browser.d.ts +6 -0
  43. package/dist/next/browser.js +57 -0
  44. package/dist/next/protocol.d.ts +73 -0
  45. package/dist/next/protocol.js +56 -0
  46. package/dist/next/release.d.ts +15 -0
  47. package/dist/next/release.js +46 -0
  48. package/dist/next/requests.d.ts +15 -0
  49. package/dist/next/requests.js +59 -0
  50. package/dist/next/routes.d.ts +4 -0
  51. package/dist/next/routes.js +21 -0
  52. package/dist/next/streams.d.ts +16 -0
  53. package/dist/next/streams.js +132 -0
  54. package/dist/next/theme.d.ts +10 -0
  55. package/dist/next/theme.js +45 -0
  56. package/dist/next/transport.d.ts +15 -0
  57. package/dist/next/transport.js +13 -0
  58. package/dist/next-build.d.ts +29 -0
  59. package/dist/next-build.js +129 -0
  60. package/dist/next-cache/adapter-runtime.d.ts +51 -0
  61. package/dist/next-cache/adapter-runtime.js +73 -0
  62. package/dist/next-cache/component-handler.d.ts +3 -0
  63. package/dist/next-cache/component-handler.js +10 -0
  64. package/dist/next-cache/components.d.ts +20 -0
  65. package/dist/next-cache/components.js +113 -0
  66. package/dist/next-cache/config.d.ts +9 -0
  67. package/dist/next-cache/config.js +105 -0
  68. package/dist/next-cache/incremental-handler.d.ts +18 -0
  69. package/dist/next-cache/incremental-handler.js +17 -0
  70. package/dist/next-cache/incremental.d.ts +27 -0
  71. package/dist/next-cache/incremental.js +99 -0
  72. package/dist/next-cache/index.d.ts +18 -0
  73. package/dist/next-cache/index.js +16 -0
  74. package/dist/next-cache/memory-store.d.ts +27 -0
  75. package/dist/next-cache/memory-store.js +118 -0
  76. package/dist/next-cache/redis-scripts.d.ts +3 -0
  77. package/dist/next-cache/redis-scripts.js +85 -0
  78. package/dist/next-cache/redis-store.d.ts +18 -0
  79. package/dist/next-cache/redis-store.js +56 -0
  80. package/dist/next-cache/runtime.d.ts +29 -0
  81. package/dist/next-cache/runtime.js +63 -0
  82. package/dist/next-cache/serialization.d.ts +39 -0
  83. package/dist/next-cache/serialization.js +333 -0
  84. package/dist/next-cache/store.d.ts +54 -0
  85. package/dist/next-cache/store.js +45 -0
  86. package/dist/next-cache/valkey-transport.d.ts +52 -0
  87. package/dist/next-cache/valkey-transport.js +189 -0
  88. package/dist/next-dev-control.d.ts +48 -0
  89. package/dist/next-dev-control.js +63 -0
  90. package/dist/next-dev-process.d.ts +17 -0
  91. package/dist/next-dev-process.js +110 -0
  92. package/dist/next-dev.d.ts +12 -0
  93. package/dist/next-dev.js +267 -0
  94. package/dist/next-image-config.d.ts +9 -0
  95. package/dist/next-image-config.js +101 -0
  96. package/dist/next-project.d.ts +22 -0
  97. package/dist/next-project.js +64 -0
  98. package/dist/next-public-fonts-local.d.ts +7 -0
  99. package/dist/next-public-fonts-local.js +64 -0
  100. package/dist/next-public-fonts.d.ts +13 -0
  101. package/dist/next-public-fonts.js +49 -0
  102. package/dist/next-source-hash.d.ts +14 -0
  103. package/dist/next-source-hash.js +18 -0
  104. package/dist/next-source.d.ts +35 -0
  105. package/dist/next-source.js +155 -0
  106. package/dist/next.d.ts +9 -0
  107. package/dist/next.js +5 -0
  108. package/dist/project.d.ts +3 -0
  109. package/dist/project.js +9 -1
  110. package/dist/runtime-contracts.d.ts +229 -0
  111. package/dist/runtime-contracts.js +224 -0
  112. package/dist/sdk.d.ts +2 -2
  113. package/dist/server/broker-contract.d.ts +68 -0
  114. package/dist/server/broker-contract.js +33 -0
  115. package/dist/server/client.d.ts +81 -0
  116. package/dist/server/client.js +159 -0
  117. package/dist/server/logger.d.ts +26 -0
  118. package/dist/server/logger.js +24 -0
  119. package/dist/server/release-forms.d.ts +13 -0
  120. package/dist/server/release-forms.js +124 -0
  121. package/dist/server/request-context.d.ts +75 -0
  122. package/dist/server/request-context.js +107 -0
  123. package/dist/server/runtime-config.d.ts +54 -0
  124. package/dist/server/runtime-config.js +64 -0
  125. package/dist/server.d.ts +8 -0
  126. package/dist/server.js +4 -0
  127. package/dist/templates/nextjs/.artifactignore +14 -0
  128. package/dist/templates/nextjs/README.md +157 -0
  129. package/dist/templates/nextjs/app/about/page.tsx +26 -0
  130. package/dist/templates/nextjs/app/actions.ts +18 -0
  131. package/dist/templates/nextjs/app/api/health/route.ts +6 -0
  132. package/dist/templates/nextjs/app/components/counter.tsx +19 -0
  133. package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
  134. package/dist/templates/nextjs/app/globals.css +4 -0
  135. package/dist/templates/nextjs/app/layout.tsx +27 -0
  136. package/dist/templates/nextjs/app/page.tsx +51 -0
  137. package/dist/templates/nextjs/app/providers.tsx +107 -0
  138. package/dist/templates/nextjs/artifact.bundle.yml +55 -0
  139. package/dist/templates/nextjs/gitignore +9 -0
  140. package/dist/templates/nextjs/next.config.mjs +27 -0
  141. package/dist/templates/nextjs/package.json +32 -0
  142. package/dist/templates/nextjs/pnpm-workspace.yaml +22 -0
  143. package/dist/templates/nextjs/postcss.config.mjs +5 -0
  144. package/dist/templates/nextjs/tsconfig.json +20 -0
  145. package/dist/templates/react-vite/package.json +1 -1
  146. package/dist/trusted-install.js +62 -2
  147. package/dist/tunnel/broker.d.ts +75 -0
  148. package/dist/tunnel/broker.js +463 -0
  149. package/dist/tunnel/client.d.ts +22 -0
  150. package/dist/tunnel/client.js +177 -0
  151. package/dist/tunnel/index.d.ts +6 -0
  152. package/dist/tunnel/index.js +4 -0
  153. package/dist/tunnel/io.d.ts +33 -0
  154. package/dist/tunnel/io.js +108 -0
  155. package/dist/tunnel/peer.d.ts +25 -0
  156. package/dist/tunnel/peer.js +147 -0
  157. package/dist/tunnel/protocol.d.ts +78 -0
  158. package/dist/tunnel/protocol.js +210 -0
  159. package/dist/tunnel/redis-auth.d.ts +37 -0
  160. package/dist/tunnel/redis-auth.js +92 -0
  161. package/dist/tunnel/redis-ownership.d.ts +38 -0
  162. package/dist/tunnel/redis-ownership.js +100 -0
  163. package/dist/tunnel/redis-socket.d.ts +13 -0
  164. package/dist/tunnel/redis-socket.js +48 -0
  165. package/dist/tunnel/redis-types.d.ts +75 -0
  166. package/dist/tunnel/redis-types.js +9 -0
  167. package/dist/tunnel/redis.d.ts +56 -0
  168. package/dist/tunnel/redis.js +404 -0
  169. package/dist/tunnel/transport.d.ts +17 -0
  170. package/dist/tunnel/transport.js +1 -0
  171. package/package.json +67 -4
  172. package/templates/nextjs/.artifactignore +14 -0
  173. package/templates/nextjs/README.md +157 -0
  174. package/templates/nextjs/app/about/page.tsx +26 -0
  175. package/templates/nextjs/app/actions.ts +18 -0
  176. package/templates/nextjs/app/api/health/route.ts +6 -0
  177. package/templates/nextjs/app/components/counter.tsx +19 -0
  178. package/templates/nextjs/app/components/greeting-form.tsx +38 -0
  179. package/templates/nextjs/app/globals.css +4 -0
  180. package/templates/nextjs/app/layout.tsx +27 -0
  181. package/templates/nextjs/app/page.tsx +51 -0
  182. package/templates/nextjs/app/providers.tsx +107 -0
  183. package/templates/nextjs/artifact.bundle.yml +55 -0
  184. package/templates/nextjs/gitignore +9 -0
  185. package/templates/nextjs/next.config.mjs +27 -0
  186. package/templates/nextjs/package.json +32 -0
  187. package/templates/nextjs/pnpm-workspace.yaml +22 -0
  188. package/templates/nextjs/postcss.config.mjs +5 -0
  189. package/templates/nextjs/tsconfig.json +20 -0
  190. package/templates/react-vite/package.json +1 -1
@@ -0,0 +1,85 @@
1
+ // One metadata hash holds the incarnation, epoch and complete tag history. Redis
2
+ // eviction is key-atomic: tags cannot disappear independently and revive an entry.
3
+ const common = `
4
+ local clock = redis.call('TIME')
5
+ local now = tonumber(clock[1]) * 1000 + math.floor(tonumber(clock[2]) / 1000)
6
+ local function decode(value)
7
+ if not value then return nil end
8
+ local ok, parsed = pcall(cjson.decode, value)
9
+ if ok and type(parsed) == 'table' then return parsed end
10
+ return nil
11
+ end
12
+ local function initialize(nonce, retention)
13
+ local incarnation = redis.call('HGET', KEYS[1], 'incarnation')
14
+ if not incarnation then
15
+ redis.call('DEL', KEYS[1])
16
+ redis.call('HSET', KEYS[1], 'incarnation', nonce, 'epoch', '0')
17
+ incarnation = nonce
18
+ end
19
+ redis.call('PEXPIRE', KEYS[1], retention)
20
+ return incarnation
21
+ end
22
+ `;
23
+ export const NEXT_CACHE_READ_SCRIPT = `${common}
24
+ local incarnation = initialize(ARGV[1], ARGV[2])
25
+ local epoch = tonumber(redis.call('HGET', KEYS[1], 'epoch'))
26
+ local entry = decode(redis.call('GET', KEYS[2]))
27
+ local ticket = {incarnation=incarnation, epoch=epoch, version=entry and entry.version or '', startedAt=now}
28
+ local result = {ticket=ticket}
29
+ if not entry or entry.incarnation ~= incarnation or entry.expiresAt <= now then
30
+ return cjson.encode(result)
31
+ end
32
+ local tags = decode(ARGV[3])
33
+ for _, tag in ipairs(entry.tags) do table.insert(tags, tag) end
34
+ local stale = false
35
+ for _, tag in ipairs(tags) do
36
+ local raw = redis.call('HGET', KEYS[1], 'tag:' .. tag)
37
+ local state = decode(raw)
38
+ if raw and not state then return cjson.encode(result) end
39
+ if state then
40
+ if entry.epoch < state.floor then return cjson.encode(result) end
41
+ for _, event in ipairs(state.events) do
42
+ if event.epoch > entry.epoch then
43
+ if event.expireAt >= 0 and event.expireAt <= now then return cjson.encode(result) end
44
+ stale = true
45
+ end
46
+ end
47
+ end
48
+ end
49
+ result.entry = {payload=entry.payload, stale=stale}
50
+ return cjson.encode(result)
51
+ `;
52
+ export const NEXT_CACHE_COMMIT_SCRIPT = `${common}
53
+ local ticket = decode(ARGV[1])
54
+ if redis.call('HGET', KEYS[1], 'incarnation') ~= ticket.incarnation then return 0 end
55
+ if tonumber(redis.call('HGET', KEYS[1], 'epoch')) ~= ticket.epoch then return 0 end
56
+ if now - ticket.startedAt > tonumber(ARGV[7]) or now < ticket.startedAt then return 0 end
57
+ local existing = decode(redis.call('GET', KEYS[2]))
58
+ if (existing and existing.version or '') ~= ticket.version then return 0 end
59
+ local entry = {
60
+ incarnation=ticket.incarnation, epoch=ticket.epoch, version=ARGV[2],
61
+ tags=decode(ARGV[3]), payload=ARGV[4], expiresAt=now+tonumber(ARGV[5])
62
+ }
63
+ redis.call('SET', KEYS[2], cjson.encode(entry), 'PX', ARGV[5])
64
+ redis.call('PEXPIRE', KEYS[1], ARGV[6])
65
+ return 1
66
+ `;
67
+ export const NEXT_CACHE_INVALIDATE_SCRIPT = `${common}
68
+ local tags = decode(ARGV[3])
69
+ if #tags == 0 then return 1 end
70
+ -- Bound metadata growth. Rotating its incarnation safely invalidates all entries.
71
+ if redis.call('HLEN', KEYS[1]) + #tags > tonumber(ARGV[5]) + 2 then redis.call('DEL', KEYS[1]) end
72
+ initialize(ARGV[1], ARGV[2])
73
+ local epoch = redis.call('HINCRBY', KEYS[1], 'epoch', 1)
74
+ local expireAt = tonumber(ARGV[4]) < 0 and -1 or now + tonumber(ARGV[4])
75
+ for _, tag in ipairs(tags) do
76
+ local state = decode(redis.call('HGET', KEYS[1], 'tag:' .. tag)) or {floor=0, events={}}
77
+ table.insert(state.events, {epoch=epoch, expireAt=expireAt})
78
+ if #state.events > 16 then
79
+ local removed = table.remove(state.events, 1)
80
+ state.floor = removed.epoch
81
+ end
82
+ redis.call('HSET', KEYS[1], 'tag:' .. tag, cjson.encode(state))
83
+ end
84
+ return 1
85
+ `;
@@ -0,0 +1,18 @@
1
+ import { type CacheCommit, type CacheLocation, type CacheLookup, type NextCacheNamespace, type NextCacheStore } from './store.js';
2
+ import type { RedisScriptExecutor } from './valkey-transport.js';
3
+ /** Shared Redis/Valkey store. No process-local invalidation state or fallback. */
4
+ export declare class RedisNextCacheStore implements NextCacheStore {
5
+ private readonly executor;
6
+ constructor({ executor }: {
7
+ executor: RedisScriptExecutor;
8
+ });
9
+ read(input: CacheLocation & {
10
+ tags: string[];
11
+ }): Promise<CacheLookup>;
12
+ commit(input: CacheCommit): Promise<boolean>;
13
+ invalidate(input: {
14
+ namespace: NextCacheNamespace;
15
+ tags: string[];
16
+ expireAfterMs?: number;
17
+ }): Promise<void>;
18
+ }
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { cacheDigest, cacheEntryKey, cacheNamespacePrefix, MAX_CACHE_ENTRY_AGE_MS, MAX_CACHE_WRITE_AGE_MS, MAX_NAMESPACE_TAGS, normalizeCacheTags, validateCacheCommit, validateExpiration, } from './store.js';
4
+ import { NEXT_CACHE_COMMIT_SCRIPT, NEXT_CACHE_INVALIDATE_SCRIPT, NEXT_CACHE_READ_SCRIPT } from './redis-scripts.js';
5
+ const lookupSchema = z.object({
6
+ ticket: z.object({
7
+ incarnation: z.string().uuid(), epoch: z.number().int().nonnegative(),
8
+ version: z.string().max(36), startedAt: z.number().int().nonnegative(),
9
+ }),
10
+ entry: z.object({ payload: z.string(), stale: z.boolean() }).optional(),
11
+ });
12
+ const METADATA_RETENTION_MS = MAX_CACHE_ENTRY_AGE_MS + MAX_CACHE_WRITE_AGE_MS;
13
+ /** Shared Redis/Valkey store. No process-local invalidation state or fallback. */
14
+ export class RedisNextCacheStore {
15
+ executor;
16
+ constructor({ executor }) {
17
+ this.executor = executor;
18
+ }
19
+ async read(input) {
20
+ const tags = normalizeCacheTags(input.tags).map(cacheDigest);
21
+ const result = await this.executor.eval({
22
+ script: NEXT_CACHE_READ_SCRIPT,
23
+ keys: [`${cacheNamespacePrefix(input.namespace)}:meta`, cacheEntryKey(input)],
24
+ arguments: [randomUUID(), String(METADATA_RETENTION_MS), JSON.stringify(tags)],
25
+ });
26
+ if (typeof result !== 'string')
27
+ throw new Error('Invalid Next cache response');
28
+ return lookupSchema.parse(JSON.parse(result));
29
+ }
30
+ async commit(input) {
31
+ validateCacheCommit(input);
32
+ const result = await this.executor.eval({
33
+ script: NEXT_CACHE_COMMIT_SCRIPT,
34
+ keys: [`${cacheNamespacePrefix(input.namespace)}:meta`, cacheEntryKey(input)],
35
+ arguments: [JSON.stringify(input.ticket), randomUUID(), JSON.stringify(input.tags.map(cacheDigest)),
36
+ input.payload, String(input.ttlMs), String(METADATA_RETENTION_MS), String(MAX_CACHE_WRITE_AGE_MS)],
37
+ });
38
+ if (result !== 0 && result !== 1)
39
+ throw new Error('Invalid Next cache commit response');
40
+ return result === 1;
41
+ }
42
+ async invalidate(input) {
43
+ validateExpiration(input.expireAfterMs);
44
+ const tags = normalizeCacheTags(input.tags).map(cacheDigest);
45
+ if (!tags.length)
46
+ return;
47
+ const result = await this.executor.eval({
48
+ script: NEXT_CACHE_INVALIDATE_SCRIPT,
49
+ keys: [`${cacheNamespacePrefix(input.namespace)}:meta`],
50
+ arguments: [randomUUID(), String(METADATA_RETENTION_MS), JSON.stringify(tags),
51
+ String(input.expireAfterMs ?? 0), String(MAX_NAMESPACE_TAGS)],
52
+ });
53
+ if (result !== 1)
54
+ throw new Error('Next cache invalidation failed');
55
+ }
56
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export declare const artifactNextCacheBindingSchema: z.ZodObject<{
3
+ namespace: z.ZodObject<{
4
+ applicationId: z.ZodString;
5
+ environment: z.ZodString;
6
+ releaseId: z.ZodString;
7
+ generation: z.ZodString;
8
+ }, z.core.$strict>;
9
+ connection: z.ZodObject<{
10
+ host: z.ZodString;
11
+ port: z.ZodNumber;
12
+ serverIdentity: z.ZodString;
13
+ caCertificates: z.ZodArray<z.ZodString>;
14
+ }, z.core.$strict>;
15
+ }, z.core.$strict>;
16
+ export declare function createArtifactNextRuntimeCache({ env }?: {
17
+ env?: NodeJS.ProcessEnv;
18
+ }): {
19
+ IncrementalCacheHandler: import("./incremental.js").NextIncrementalCacheHandlerConstructor;
20
+ cacheComponents: import("./components.js").NextComponentCacheHandler;
21
+ verifyReadiness(): Promise<void>;
22
+ };
23
+ export declare function getArtifactNextRuntimeCache(): {
24
+ IncrementalCacheHandler: import("./incremental.js").NextIncrementalCacheHandlerConstructor;
25
+ cacheComponents: import("./components.js").NextComponentCacheHandler;
26
+ verifyReadiness(): Promise<void>;
27
+ };
28
+ /** Trusted startup calls this before serving; unlike cache reads, failures propagate. */
29
+ export declare function verifyArtifactNextCacheReadiness(): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { createComponentCacheHandler } from './components.js';
4
+ import { createIncrementalCacheHandler } from './incremental.js';
5
+ import { LocalNextCacheStore } from './memory-store.js';
6
+ import { RedisNextCacheStore } from './redis-store.js';
7
+ import { ValkeyScriptExecutor } from './valkey-transport.js';
8
+ const namespaceSchema = z.strictObject({
9
+ applicationId: z.string().min(1).max(256), environment: z.string().min(1).max(256),
10
+ releaseId: z.string().min(1).max(256), generation: z.string().min(1).max(256),
11
+ });
12
+ export const artifactNextCacheBindingSchema = z.strictObject({
13
+ namespace: namespaceSchema,
14
+ connection: z.strictObject({
15
+ host: z.string().min(1).max(253), port: z.number().int().min(1).max(65535),
16
+ serverIdentity: z.string().min(1).max(253), caCertificates: z.array(z.string().min(1).max(16384)).min(1).max(16),
17
+ }),
18
+ });
19
+ function disabledStore() {
20
+ return {
21
+ async read() { return { ticket: { incarnation: randomUUID(), epoch: 0, version: '', startedAt: Date.now() } }; },
22
+ async commit() { return false; },
23
+ async invalidate() { },
24
+ };
25
+ }
26
+ export function createArtifactNextRuntimeCache({ env = process.env } = {}) {
27
+ let store;
28
+ let namespace;
29
+ const mode = env.ARTIFACT_NEXT_CACHE_MODE;
30
+ if (mode === 'hosted') {
31
+ const binding = artifactNextCacheBindingSchema.parse(JSON.parse(env.ARTIFACT_NEXT_CACHE_BINDING ?? 'null'));
32
+ store = new RedisNextCacheStore({ executor: new ValkeyScriptExecutor({ connection: binding.connection }) });
33
+ namespace = binding.namespace;
34
+ }
35
+ else if (mode === 'local' || mode === 'build' || mode === 'disabled') {
36
+ if (env.ARTIFACT_NEXT_CACHE_BINDING)
37
+ throw new Error('Only hosted cache mode may receive a private binding');
38
+ if (env.K_SERVICE && mode !== 'disabled')
39
+ throw new Error('Hosted Next runtimes cannot use a process-local cache');
40
+ store = mode === 'disabled' ? disabledStore() : new LocalNextCacheStore({ purpose: 'local-development' });
41
+ namespace = { applicationId: 'disposable', environment: mode, releaseId: 'local', generation: randomUUID() };
42
+ }
43
+ else {
44
+ throw new Error('Artifact Next cache mode must be explicitly configured before starting the server');
45
+ }
46
+ const options = { store, namespace };
47
+ return {
48
+ IncrementalCacheHandler: createIncrementalCacheHandler(options), cacheComponents: createComponentCacheHandler(options),
49
+ async verifyReadiness() {
50
+ if (mode === 'hosted')
51
+ await store.read({ namespace, key: '__platform_readiness', tags: [] });
52
+ },
53
+ };
54
+ }
55
+ let handlers;
56
+ export function getArtifactNextRuntimeCache() {
57
+ handlers ??= createArtifactNextRuntimeCache();
58
+ return handlers;
59
+ }
60
+ /** Trusted startup calls this before serving; unlike cache reads, failures propagate. */
61
+ export async function verifyArtifactNextCacheReadiness() {
62
+ await getArtifactNextRuntimeCache().verifyReadiness();
63
+ }
@@ -0,0 +1,39 @@
1
+ import { Buffer } from 'node:buffer';
2
+ export interface CacheSerializationLimits {
3
+ /** Maximum UTF-8 bytes in the complete serialized record, including tags. */
4
+ maxBytes: number;
5
+ /** Maximum nesting depth; the root value has depth zero. */
6
+ maxDepth: number;
7
+ /** Maximum cumulative value count, including map keys and values. */
8
+ maxMembers: number;
9
+ }
10
+ export declare const DEFAULT_CACHE_SERIALIZATION_LIMITS: Readonly<CacheSerializationLimits>;
11
+ type SerializationErrorCode = 'invalid_value' | 'limit_exceeded' | 'invalid_payload' | 'stream_timeout' | 'stream_aborted';
12
+ export declare class CacheSerializationError extends Error {
13
+ readonly code: SerializationErrorCode;
14
+ constructor({ code, message }: {
15
+ code: SerializationErrorCode;
16
+ message: string;
17
+ });
18
+ }
19
+ /** Encodes only inert values, without invoking toJSON, getters or object constructors. */
20
+ export declare function serializeCacheValue({ value, limits }: {
21
+ value: unknown;
22
+ limits?: Partial<CacheSerializationLimits>;
23
+ }): string;
24
+ export declare function deserializeCacheValue({ serialized, limits }: {
25
+ serialized: string;
26
+ limits?: Partial<CacheSerializationLimits>;
27
+ }): unknown;
28
+ /** Consumes an owned stream. Callers must pass a cache branch, not a response's live body. */
29
+ export declare function readCacheStream({ stream, maxBytes, timeoutMs, maxChunks, signal, }: {
30
+ stream: ReadableStream<Uint8Array>;
31
+ maxBytes?: number;
32
+ timeoutMs?: number;
33
+ maxChunks?: number;
34
+ signal?: AbortSignal;
35
+ }): Promise<Buffer>;
36
+ export declare function createCacheStream({ bytes }: {
37
+ bytes: Uint8Array;
38
+ }): ReadableStream<Uint8Array>;
39
+ export {};
@@ -0,0 +1,333 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { performance } from 'node:perf_hooks';
3
+ export const DEFAULT_CACHE_SERIALIZATION_LIMITS = Object.freeze({
4
+ maxBytes: 16 * 1024 * 1024,
5
+ maxDepth: 64,
6
+ maxMembers: 100_000,
7
+ });
8
+ export class CacheSerializationError extends Error {
9
+ code;
10
+ constructor({ code, message }) {
11
+ super(message);
12
+ this.name = 'CacheSerializationError';
13
+ this.code = code;
14
+ }
15
+ }
16
+ function fail({ code, message }) {
17
+ throw new CacheSerializationError({ code, message });
18
+ }
19
+ function positiveInteger({ value, name }) {
20
+ if (!Number.isSafeInteger(value) || value <= 0) {
21
+ fail({ code: 'invalid_value', message: `${name} must be a positive safe integer` });
22
+ }
23
+ return value;
24
+ }
25
+ function resolveLimits({ limits }) {
26
+ const resolved = { ...DEFAULT_CACHE_SERIALIZATION_LIMITS, ...limits };
27
+ for (const [name, value] of Object.entries(resolved))
28
+ positiveInteger({ value, name });
29
+ // Even a caller-supplied limit must not allow unbounded JavaScript recursion.
30
+ if (resolved.maxDepth > 128)
31
+ fail({ code: 'invalid_value', message: 'maxDepth cannot exceed 128' });
32
+ return resolved;
33
+ }
34
+ class ValueBudget {
35
+ limits;
36
+ members = 0;
37
+ constructor(limits) {
38
+ this.limits = limits;
39
+ }
40
+ visit({ depth }) {
41
+ if (depth > this.limits.maxDepth)
42
+ fail({ code: 'limit_exceeded', message: 'Cache value depth limit exceeded' });
43
+ this.members += 1;
44
+ this.checkMembers({ additional: 0 });
45
+ }
46
+ checkMembers({ additional }) {
47
+ if (additional > this.limits.maxMembers - this.members) {
48
+ fail({ code: 'limit_exceeded', message: 'Cache value member limit exceeded' });
49
+ }
50
+ }
51
+ }
52
+ const UNSAFE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
53
+ function checkKey({ key, code }) {
54
+ if (UNSAFE_KEYS.has(key))
55
+ fail({ code, message: 'Unsafe cache record key' });
56
+ }
57
+ function numberPayload({ value }) {
58
+ if (Object.is(value, -0))
59
+ return '-0';
60
+ return Number.isFinite(value) ? value : String(value);
61
+ }
62
+ class ValueWriter extends ValueBudget {
63
+ bytes = 0;
64
+ parts = [];
65
+ ancestors = new Set();
66
+ write({ text }) {
67
+ this.bytes += Buffer.byteLength(text);
68
+ if (this.bytes > this.limits.maxBytes)
69
+ fail({ code: 'limit_exceeded', message: 'Cache value byte limit exceeded' });
70
+ this.parts.push(text);
71
+ }
72
+ writeString({ value }) {
73
+ if (Buffer.byteLength(value) > this.limits.maxBytes - this.bytes) {
74
+ fail({ code: 'limit_exceeded', message: 'Cache value byte limit exceeded' });
75
+ }
76
+ this.write({ text: JSON.stringify(value) });
77
+ }
78
+ encode({ value, depth }) {
79
+ this.visit({ depth });
80
+ if (value === null || value === undefined) {
81
+ this.write({ text: value === null ? '["null"]' : '["undefined"]' });
82
+ }
83
+ else if (typeof value === 'string') {
84
+ this.write({ text: '["string",' });
85
+ this.writeString({ value });
86
+ this.write({ text: ']' });
87
+ }
88
+ else if (typeof value === 'boolean' || typeof value === 'number') {
89
+ const payload = typeof value === 'number' ? numberPayload({ value }) : value;
90
+ this.write({ text: `["${typeof value}",${JSON.stringify(payload)}]` });
91
+ }
92
+ else if (typeof value === 'object') {
93
+ this.encodeObject({ value, depth });
94
+ }
95
+ else {
96
+ fail({ code: 'invalid_value', message: 'Unsupported cache value type' });
97
+ }
98
+ }
99
+ encodeObject({ value, depth }) {
100
+ if (this.ancestors.has(value))
101
+ fail({ code: 'invalid_value', message: 'Cyclic cache value' });
102
+ this.ancestors.add(value);
103
+ try {
104
+ const prototype = Object.getPrototypeOf(value);
105
+ if (Buffer.isBuffer(value) && prototype === Buffer.prototype)
106
+ this.encodeBytes({ value, tag: 'buffer' });
107
+ else if (value instanceof Uint8Array && prototype === Uint8Array.prototype)
108
+ this.encodeBytes({ value, tag: 'bytes' });
109
+ else if (value instanceof Map && prototype === Map.prototype)
110
+ this.encodeMap({ value, depth });
111
+ else if (Array.isArray(value) && prototype === Array.prototype)
112
+ this.encodeArray({ value, depth });
113
+ else if (prototype === Object.prototype || prototype === null)
114
+ this.encodeRecord({ value, depth, nullPrototype: prototype === null });
115
+ else
116
+ fail({ code: 'invalid_value', message: 'Unsupported cache value prototype' });
117
+ }
118
+ finally {
119
+ this.ancestors.delete(value);
120
+ }
121
+ }
122
+ encodeBytes({ value, tag }) {
123
+ if (Math.ceil(value.byteLength / 3) * 4 > this.limits.maxBytes - this.bytes) {
124
+ fail({ code: 'limit_exceeded', message: 'Cache value byte limit exceeded' });
125
+ }
126
+ const encoded = Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString('base64');
127
+ this.write({ text: `["${tag}","${encoded}"]` });
128
+ }
129
+ encodeMap({ value, depth }) {
130
+ this.checkMembers({ additional: value.size * 2 });
131
+ if (Reflect.ownKeys(value).length)
132
+ fail({ code: 'invalid_value', message: 'Cache maps cannot have custom properties' });
133
+ this.write({ text: '["map",[' });
134
+ let separator = '';
135
+ for (const [key, member] of value) {
136
+ this.write({ text: `${separator}[` });
137
+ this.encode({ value: key, depth: depth + 1 });
138
+ this.write({ text: ',' });
139
+ this.encode({ value: member, depth: depth + 1 });
140
+ this.write({ text: ']' });
141
+ separator = ',';
142
+ }
143
+ this.write({ text: ']]' });
144
+ }
145
+ encodeArray({ value, depth }) {
146
+ this.checkMembers({ additional: value.length });
147
+ if (Reflect.ownKeys(value).length !== value.length + 1) {
148
+ fail({ code: 'invalid_value', message: 'Cache arrays must be dense and cannot have custom properties' });
149
+ }
150
+ this.write({ text: '["array",[' });
151
+ for (let index = 0; index < value.length; index += 1) {
152
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
153
+ if (!descriptor || !('value' in descriptor))
154
+ fail({ code: 'invalid_value', message: 'Cache arrays cannot have accessors or holes' });
155
+ if (index)
156
+ this.write({ text: ',' });
157
+ this.encode({ value: descriptor.value, depth: depth + 1 });
158
+ }
159
+ this.write({ text: ']]' });
160
+ }
161
+ encodeRecord({ value, depth, nullPrototype }) {
162
+ const keys = Reflect.ownKeys(value);
163
+ this.checkMembers({ additional: keys.length });
164
+ this.write({ text: nullPrototype ? '["null-record",[' : '["record",[' });
165
+ let separator = '';
166
+ for (const key of keys) {
167
+ if (typeof key !== 'string')
168
+ fail({ code: 'invalid_value', message: 'Cache records cannot have symbol keys' });
169
+ checkKey({ key, code: 'invalid_value' });
170
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
171
+ if (!descriptor || !('value' in descriptor) || !descriptor.enumerable) {
172
+ fail({ code: 'invalid_value', message: 'Cache records must contain enumerable data properties' });
173
+ }
174
+ this.write({ text: `${separator}[` });
175
+ this.writeString({ value: key });
176
+ this.write({ text: ',' });
177
+ this.encode({ value: descriptor.value, depth: depth + 1 });
178
+ this.write({ text: ']' });
179
+ separator = ',';
180
+ }
181
+ this.write({ text: ']]' });
182
+ }
183
+ finish() { return this.parts.join(''); }
184
+ }
185
+ /** Encodes only inert values, without invoking toJSON, getters or object constructors. */
186
+ export function serializeCacheValue({ value, limits }) {
187
+ const writer = new ValueWriter(resolveLimits({ limits }));
188
+ writer.write({ text: '["artifact-next-cache",1,' });
189
+ writer.encode({ value, depth: 0 });
190
+ writer.write({ text: ']' });
191
+ return writer.finish();
192
+ }
193
+ function invalidPayload() {
194
+ return fail({ code: 'invalid_payload', message: 'Invalid serialized cache value' });
195
+ }
196
+ function decodeNumber({ value }) {
197
+ if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0))
198
+ return value;
199
+ switch (value) {
200
+ case '-0': return -0;
201
+ case 'NaN': return Number.NaN;
202
+ case 'Infinity': return Infinity;
203
+ case '-Infinity': return -Infinity;
204
+ default: return invalidPayload();
205
+ }
206
+ }
207
+ function decodeBytes({ value }) {
208
+ if (typeof value !== 'string' || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
209
+ return invalidPayload();
210
+ }
211
+ const decoded = Buffer.from(value, 'base64');
212
+ if (decoded.toString('base64') !== value)
213
+ return invalidPayload();
214
+ return decoded;
215
+ }
216
+ function decodeEntries({ value, depth, budget, tag }) {
217
+ budget.checkMembers({ additional: value.length * (tag === 'map' ? 2 : 1) });
218
+ if (tag === 'array')
219
+ return value.map((member) => decodeValue({ value: member, depth: depth + 1, budget }));
220
+ const map = new Map();
221
+ const record = tag === 'null-record' ? Object.create(null) : {};
222
+ for (const entry of value) {
223
+ if (!Array.isArray(entry) || entry.length !== 2)
224
+ return invalidPayload();
225
+ if (tag === 'map') {
226
+ const key = decodeValue({ value: entry[0], depth: depth + 1, budget });
227
+ if (map.has(key))
228
+ return invalidPayload();
229
+ map.set(key, decodeValue({ value: entry[1], depth: depth + 1, budget }));
230
+ }
231
+ else {
232
+ const key = entry[0];
233
+ if (typeof key !== 'string' || Object.hasOwn(record, key))
234
+ return invalidPayload();
235
+ checkKey({ key, code: 'invalid_payload' });
236
+ record[key] = decodeValue({ value: entry[1], depth: depth + 1, budget });
237
+ }
238
+ }
239
+ return tag === 'map' ? map : record;
240
+ }
241
+ function decodeValue({ value, depth, budget }) {
242
+ budget.visit({ depth });
243
+ if (!Array.isArray(value))
244
+ return invalidPayload();
245
+ const tag = value[0];
246
+ if (tag === 'undefined' || tag === 'null') {
247
+ if (value.length !== 1)
248
+ return invalidPayload();
249
+ return tag === 'null' ? null : undefined;
250
+ }
251
+ if (value.length !== 2)
252
+ return invalidPayload();
253
+ const payload = value[1];
254
+ if (tag === 'string' && typeof payload === 'string')
255
+ return payload;
256
+ if (tag === 'boolean' && typeof payload === 'boolean')
257
+ return payload;
258
+ if (tag === 'number')
259
+ return decodeNumber({ value: payload });
260
+ if (tag === 'buffer' || tag === 'bytes') {
261
+ const bytes = decodeBytes({ value: payload });
262
+ return tag === 'buffer' ? bytes : new Uint8Array(bytes);
263
+ }
264
+ if ((tag === 'array' || tag === 'record' || tag === 'null-record' || tag === 'map') && Array.isArray(payload)) {
265
+ return decodeEntries({ value: payload, depth, budget, tag });
266
+ }
267
+ return invalidPayload();
268
+ }
269
+ export function deserializeCacheValue({ serialized, limits }) {
270
+ const budget = new ValueBudget(resolveLimits({ limits }));
271
+ if (Buffer.byteLength(serialized) > budget.limits.maxBytes)
272
+ fail({ code: 'limit_exceeded', message: 'Cache value byte limit exceeded' });
273
+ let envelope;
274
+ try {
275
+ envelope = JSON.parse(serialized);
276
+ }
277
+ catch {
278
+ return invalidPayload();
279
+ }
280
+ if (!Array.isArray(envelope) || envelope.length !== 3 || envelope[0] !== 'artifact-next-cache' || envelope[1] !== 1) {
281
+ return invalidPayload();
282
+ }
283
+ return decodeValue({ value: envelope[2], depth: 0, budget });
284
+ }
285
+ /** Consumes an owned stream. Callers must pass a cache branch, not a response's live body. */
286
+ export async function readCacheStream({ stream, maxBytes = DEFAULT_CACHE_SERIALIZATION_LIMITS.maxBytes, timeoutMs = 10_000, maxChunks = 65_536, signal, }) {
287
+ for (const [name, value] of Object.entries({ maxBytes, timeoutMs, maxChunks }))
288
+ positiveInteger({ value, name });
289
+ if (timeoutMs > 2_147_483_647)
290
+ fail({ code: 'invalid_value', message: 'Stream timeout exceeds timer range' });
291
+ const reader = stream.getReader();
292
+ let interrupt = () => { };
293
+ const interrupted = new Promise((_resolve, reject) => { interrupt = reject; });
294
+ const timedOut = () => new CacheSerializationError({ code: 'stream_timeout', message: 'Cache stream read timed out' });
295
+ const aborted = () => new CacheSerializationError({ code: 'stream_aborted', message: 'Cache stream read aborted' });
296
+ const onAbort = () => interrupt(aborted());
297
+ const timer = setTimeout(() => interrupt(timedOut()), timeoutMs);
298
+ const deadline = performance.now() + timeoutMs;
299
+ signal?.addEventListener('abort', onAbort, { once: true });
300
+ const chunks = [];
301
+ let bytes = 0;
302
+ try {
303
+ while (true) {
304
+ if (signal?.aborted)
305
+ throw aborted();
306
+ if (performance.now() >= deadline)
307
+ throw timedOut();
308
+ const result = await Promise.race([reader.read(), interrupted]);
309
+ if (result.done)
310
+ return Buffer.concat(chunks, bytes);
311
+ if (!(result.value instanceof Uint8Array))
312
+ fail({ code: 'invalid_value', message: 'Cache stream must contain bytes' });
313
+ bytes += result.value.byteLength;
314
+ if (bytes > maxBytes || chunks.length >= maxChunks)
315
+ fail({ code: 'limit_exceeded', message: 'Cache stream byte or chunk limit exceeded' });
316
+ chunks.push(Buffer.from(result.value));
317
+ }
318
+ }
319
+ catch (error) {
320
+ // Cancellation hooks belong to the producer and may never settle; do not let them defeat the deadline.
321
+ void reader.cancel(error).catch(() => { });
322
+ throw error;
323
+ }
324
+ finally {
325
+ clearTimeout(timer);
326
+ signal?.removeEventListener('abort', onAbort);
327
+ reader.releaseLock();
328
+ }
329
+ }
330
+ export function createCacheStream({ bytes }) {
331
+ const copy = Buffer.from(bytes);
332
+ return new ReadableStream({ start(controller) { controller.enqueue(copy); controller.close(); } });
333
+ }
@@ -0,0 +1,54 @@
1
+ /** A fresh generation is mandatory for every activation, including rollback. */
2
+ export interface NextCacheNamespace {
3
+ applicationId: string;
4
+ environment: string;
5
+ releaseId: string;
6
+ generation: string;
7
+ }
8
+ export interface CacheWriteTicket {
9
+ incarnation: string;
10
+ epoch: number;
11
+ version: string;
12
+ startedAt: number;
13
+ }
14
+ export interface CacheLookup {
15
+ ticket: CacheWriteTicket;
16
+ entry?: {
17
+ payload: string;
18
+ stale: boolean;
19
+ };
20
+ }
21
+ export interface CacheLocation {
22
+ namespace: NextCacheNamespace;
23
+ key: string;
24
+ }
25
+ export interface CacheCommit extends CacheLocation {
26
+ ticket: CacheWriteTicket;
27
+ payload: string;
28
+ tags: string[];
29
+ ttlMs: number;
30
+ }
31
+ /** Each method is atomic. Cache writes must use the ticket from before rendering. */
32
+ export interface NextCacheStore {
33
+ read(input: CacheLocation & {
34
+ tags: string[];
35
+ }): Promise<CacheLookup>;
36
+ commit(input: CacheCommit): Promise<boolean>;
37
+ invalidate(input: {
38
+ namespace: NextCacheNamespace;
39
+ tags: string[];
40
+ expireAfterMs?: number;
41
+ }): Promise<void>;
42
+ }
43
+ export declare const MAX_CACHE_TAGS = 128;
44
+ export declare const MAX_CACHE_TAG_BYTES = 256;
45
+ export declare const MAX_CACHE_ENTRY_BYTES: number;
46
+ export declare const MAX_CACHE_ENTRY_AGE_MS: number;
47
+ export declare const MAX_CACHE_WRITE_AGE_MS: number;
48
+ export declare const MAX_NAMESPACE_TAGS = 10000;
49
+ export declare function cacheDigest(value: string): string;
50
+ export declare function cacheNamespacePrefix(namespace: NextCacheNamespace): string;
51
+ export declare function cacheEntryKey({ namespace, key }: CacheLocation): string;
52
+ export declare function normalizeCacheTags(tags: string[]): string[];
53
+ export declare function validateCacheCommit(input: CacheCommit): void;
54
+ export declare function validateExpiration(expireAfterMs?: number): void;