@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,404 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { DEFAULT_TUNNEL_LIMITS, parseTunnelFrame } from './protocol.js';
3
+ import { redisIdentifier as identifier, redisNamespaceSchema as namespaceSchema, redisLeaseSchema as leaseSchema } from './redis-ownership.js';
4
+ import { RelaySocket } from './redis-socket.js';
5
+ import { deriveRelayFrameKey, RelayFrameAuthenticator } from './redis-auth.js';
6
+ export { RedisTunnelOwnership } from './redis-ownership.js';
7
+ /** Internal Redis channels contain authorized application traffic. Deploy them
8
+ * on the environment's private, authenticated Redis and disable command replay.
9
+ * Every message is authenticated with a gateway-only key: other Redis clients
10
+ * can neither inject, redirect nor replay tunnel frames. */
11
+ export class RedisTunnelRelay {
12
+ options;
13
+ started = false;
14
+ closed = false;
15
+ attached = new Map();
16
+ connected = new Map();
17
+ frames;
18
+ publishing = Promise.resolve();
19
+ receiving = Promise.resolve();
20
+ outgoingBytes = 0;
21
+ incomingBytes = 0;
22
+ generation = 0;
23
+ starting;
24
+ constructor(options) {
25
+ this.options = options;
26
+ identifier.parse(options.nodeId);
27
+ namespaceSchema.parse(options.namespace);
28
+ if (options.namespace !== options.ownership.namespace || Object.is(options.publisher, options.subscriber))
29
+ throw new Error('Separate, environment-scoped Redis clients are required');
30
+ this.frames = new RelayFrameAuthenticator(deriveRelayFrameKey({ secret: options.secret, namespace: options.namespace }));
31
+ if (options.connectTimeoutMs !== undefined && (!Number.isSafeInteger(options.connectTimeoutMs) || options.connectTimeoutMs < 1 || options.connectTimeoutMs > 60_000))
32
+ throw new Error('Invalid tunnel connect timeout');
33
+ const policy = options.publisher.options;
34
+ if (policy.enableOfflineQueue !== false || policy.autoResendUnfulfilledCommands !== false || policy.maxRetriesPerRequest !== 0) {
35
+ throw new Error('Tunnel Redis publisher must disable offline queues, command replay and retries');
36
+ }
37
+ }
38
+ channel(nodeId) { return `artifact-tunnel:${this.options.namespace}:node:${identifier.parse(nodeId)}`; }
39
+ assertStarted() { if (!this.started || this.closed)
40
+ throw new Error('Tunnel relay is not subscribed'); }
41
+ isReady() { return this.started && !this.closed; }
42
+ async start() {
43
+ if (this.closed)
44
+ throw new Error('Tunnel relay is closed');
45
+ if (this.started)
46
+ return;
47
+ if (this.starting)
48
+ return this.starting;
49
+ this.starting = this.subscribe();
50
+ try {
51
+ await this.starting;
52
+ }
53
+ finally {
54
+ this.starting = undefined;
55
+ }
56
+ }
57
+ async subscribe() {
58
+ const generation = this.generation;
59
+ this.removeListeners();
60
+ this.options.subscriber.on('message', this.onMessage);
61
+ for (const event of ['error', 'close', 'end', 'reconnecting'])
62
+ this.options.subscriber.on(event, this.onRedisFailure);
63
+ try {
64
+ await this.options.subscriber.subscribe(this.channel(this.options.nodeId));
65
+ if (this.closed || generation !== this.generation)
66
+ throw new Error('Tunnel relay subscription interrupted');
67
+ this.started = true;
68
+ }
69
+ catch (error) {
70
+ this.removeListeners();
71
+ throw error;
72
+ }
73
+ }
74
+ removeListeners() {
75
+ this.options.subscriber.off('message', this.onMessage);
76
+ for (const event of ['error', 'close', 'end', 'reconnecting'])
77
+ this.options.subscriber.off(event, this.onRedisFailure);
78
+ }
79
+ onRedisFailure = () => {
80
+ this.generation++;
81
+ this.started = false;
82
+ for (const owner of this.attached.values())
83
+ this.closeOwner(owner);
84
+ for (const connection of this.connected.values())
85
+ connection.socket.terminate();
86
+ };
87
+ /**
88
+ * A local overload or malformed envelope resets relayed traffic like a Redis failure (in-flight
89
+ * streams fail closed), but the subscriber is still connected and emits no 'ready' event for
90
+ * gateway-main to restart on. Resubscribe now so a transient burst cannot leave the node unready.
91
+ */
92
+ onRelayOverload = () => {
93
+ this.onRedisFailure();
94
+ if (!this.closed)
95
+ void this.start().catch(() => undefined);
96
+ };
97
+ onMessage = (channel, raw) => {
98
+ if (!this.started || channel !== this.channel(this.options.nodeId))
99
+ return;
100
+ // Authenticate before anything else: a message without a valid MAC, fresh timestamp and new
101
+ // sequence is dropped without resetting live traffic, so a Redis client cannot inject or DoS.
102
+ const envelope = this.frames.open({ channel, message: raw });
103
+ if (!envelope) {
104
+ this.options.onRejectedFrame?.();
105
+ return;
106
+ }
107
+ const bytes = Buffer.byteLength(raw);
108
+ if (this.incomingBytes + bytes > DEFAULT_TUNNEL_LIMITS.maxBufferedBytes) {
109
+ this.onRelayOverload();
110
+ return;
111
+ }
112
+ this.incomingBytes += bytes;
113
+ const generation = this.generation;
114
+ this.receiving = this.receiving.then(async () => {
115
+ if (this.started && generation === this.generation)
116
+ await this.receive(envelope);
117
+ }).catch(() => this.onRelayOverload()).finally(() => { this.incomingBytes -= bytes; });
118
+ };
119
+ publish(nodeId, envelope) {
120
+ const channel = this.channel(nodeId);
121
+ // Sealed in call order, so each destination sees this node's sequence numbers ascending.
122
+ const payload = this.frames.seal({ channel, envelope });
123
+ const bytes = Buffer.byteLength(payload);
124
+ if (this.outgoingBytes + bytes > DEFAULT_TUNNEL_LIMITS.maxBufferedBytes)
125
+ return Promise.reject(new Error('Tunnel relay publish queue exceeded'));
126
+ this.outgoingBytes += bytes;
127
+ const generation = this.generation;
128
+ const sending = this.publishing.then(async () => {
129
+ if (envelope.type !== 'close' && (generation !== this.generation || !this.started || this.closed))
130
+ throw new Error('Tunnel relay generation changed');
131
+ if (await this.options.publisher.publish(channel, payload) < 1)
132
+ throw new Error('Tunnel node is offline');
133
+ });
134
+ this.publishing = sending.catch(() => undefined).finally(() => { this.outgoingBytes -= bytes; });
135
+ return sending;
136
+ }
137
+ envelope(lease, connection, type, destination, frame) {
138
+ return { type, destination, source: this.options.nodeId, connection, sessionId: lease.sessionId, epoch: lease.epoch, ...(frame === undefined ? {} : { frame }) };
139
+ }
140
+ async current(lease, nodeId) {
141
+ const owner = await this.options.ownership.lookup(lease.sessionId);
142
+ if (lease.environment !== this.options.namespace || owner?.epoch !== lease.epoch || owner.nodeId !== nodeId)
143
+ return false;
144
+ lease.expiresAt = owner.expiresAt;
145
+ return owner.expiresAt > Date.now();
146
+ }
147
+ async verifiedLease(lease) {
148
+ const input = leaseSchema.parse(lease);
149
+ const recorded = await this.options.ownership.resolveLease(input.sessionId);
150
+ if (!recorded || input.environment !== this.options.namespace || Object.keys(recorded).some((key) => Reflect.get(recorded, key) !== Reflect.get(input, key)))
151
+ throw new Error('Tunnel lease is not current');
152
+ return input;
153
+ }
154
+ async attach(input, socket) {
155
+ try {
156
+ await this.attachOwned(input, socket);
157
+ }
158
+ catch (error) {
159
+ socket.terminate();
160
+ if (this.attached.get(input.sessionId)?.lease.epoch !== input.epoch) {
161
+ await this.options.ownership.release(input.sessionId, input.epoch, this.options.nodeId).catch(() => undefined);
162
+ }
163
+ throw error;
164
+ }
165
+ }
166
+ async attachOwned(input, socket) {
167
+ this.assertStarted();
168
+ const lease = await this.verifiedLease(input);
169
+ if (!(await this.current(lease, this.options.nodeId)) || socket.readyState !== 1)
170
+ throw new Error('Tunnel is owned by another node');
171
+ this.assertStarted();
172
+ const previous = this.attached.get(lease.sessionId);
173
+ if (previous && previous.lease.epoch >= lease.epoch)
174
+ throw new Error('Duplicate tunnel owner');
175
+ if (previous)
176
+ this.closeOwner(previous);
177
+ let checking = false;
178
+ const timer = setInterval(() => {
179
+ for (const remote of owner.remotes.values())
180
+ if (Date.now() - remote.lastSeen > 15_000) {
181
+ this.closeRemote(owner, remote);
182
+ void this.publish(remote.nodeId, this.envelope(lease, remote.connection, 'close', 'requester')).catch(() => undefined);
183
+ }
184
+ if (checking || owner.stopped)
185
+ return;
186
+ checking = true;
187
+ void this.current(lease, this.options.nodeId).then((valid) => { if (!valid)
188
+ this.closeOwner(owner); })
189
+ .catch(() => this.closeOwner(owner)).finally(() => { checking = false; });
190
+ }, 5000);
191
+ timer.unref();
192
+ const owner = { lease, socket, timer, remotes: new Map(), streams: new Map(), receiving: Promise.resolve(), queuedBytes: 0, stopped: false };
193
+ this.attached.set(lease.sessionId, owner);
194
+ socket.on('message', (data, binary) => this.fromDeveloper(owner, data, binary));
195
+ socket.once('close', () => this.closeOwner(owner));
196
+ socket.on('error', () => this.closeOwner(owner));
197
+ }
198
+ async connect(input) {
199
+ this.assertStarted();
200
+ const lease = await this.verifiedLease(input);
201
+ const owner = await this.options.ownership.lookup(lease.sessionId);
202
+ if (!owner || owner.epoch !== lease.epoch)
203
+ throw new Error('Tunnel owner unavailable');
204
+ this.assertStarted();
205
+ const id = randomUUID();
206
+ return new Promise((resolve, reject) => {
207
+ const socket = new RelaySocket(async (frame) => {
208
+ if (!(await this.current(lease, owner.nodeId)))
209
+ throw new Error('Tunnel owner changed');
210
+ await this.publish(owner.nodeId, this.envelope(lease, id, 'frame', 'owner', frame));
211
+ }, () => {
212
+ this.closeConnection(id, connection);
213
+ void this.publish(owner.nodeId, this.envelope(lease, id, 'close', 'owner')).catch(() => undefined);
214
+ });
215
+ const timer = setTimeout(() => this.closeConnection(id, connection), this.options.connectTimeoutMs ?? 5000);
216
+ timer.unref();
217
+ const heartbeat = setInterval(() => {
218
+ if (socket.readyState === 1)
219
+ void this.publish(owner.nodeId, this.envelope(lease, id, 'heartbeat', 'owner')).catch(() => socket.terminate());
220
+ }, 5000);
221
+ heartbeat.unref();
222
+ const connection = { lease, ownerNode: owner.nodeId, socket, timer, heartbeat, resolve, reject };
223
+ this.connected.set(id, connection);
224
+ void this.publish(owner.nodeId, this.envelope(lease, id, 'open', 'owner')).catch(() => this.closeConnection(id, connection));
225
+ });
226
+ }
227
+ async receive(message) {
228
+ if (message.destination === 'requester') {
229
+ await this.toRequester(message);
230
+ return;
231
+ }
232
+ const owner = this.attached.get(message.sessionId);
233
+ if (!owner || owner.lease.epoch !== message.epoch || !(await this.current(owner.lease, this.options.nodeId))) {
234
+ if (owner && owner.lease.epoch === message.epoch)
235
+ this.closeOwner(owner);
236
+ await this.publish(message.source, { ...message, type: 'close', destination: 'requester', source: this.options.nodeId, frame: undefined }).catch(() => undefined);
237
+ return;
238
+ }
239
+ if (message.type === 'revoke') {
240
+ this.closeOwner(owner);
241
+ return;
242
+ }
243
+ const remote = owner.remotes.get(message.connection);
244
+ if (message.type === 'open') {
245
+ if (remote || owner.remotes.size >= DEFAULT_TUNNEL_LIMITS.maxStreams)
246
+ throw new Error('Duplicate or excessive tunnel connections');
247
+ owner.remotes.set(message.connection, { nodeId: message.source, connection: message.connection, lastSeen: Date.now() });
248
+ await this.publish(message.source, this.envelope(owner.lease, message.connection, 'ready', 'requester'));
249
+ return;
250
+ }
251
+ if (!remote || remote.nodeId !== message.source)
252
+ return;
253
+ remote.lastSeen = Date.now();
254
+ if (message.type === 'heartbeat')
255
+ return;
256
+ if (message.type === 'close') {
257
+ this.closeRemote(owner, remote);
258
+ return;
259
+ }
260
+ if (message.type !== 'frame' || message.frame === undefined)
261
+ throw new Error('Invalid owner relay message');
262
+ await this.toDeveloper(owner, remote, message.frame);
263
+ }
264
+ async toRequester(message) {
265
+ const connection = this.connected.get(message.connection);
266
+ if (!connection || connection.ownerNode !== message.source || connection.lease.sessionId !== message.sessionId || connection.lease.epoch !== message.epoch)
267
+ return;
268
+ if (message.type === 'close' || !(await this.current(connection.lease, connection.ownerNode))) {
269
+ this.closeConnection(message.connection, connection);
270
+ return;
271
+ }
272
+ if (message.type === 'ready') {
273
+ if (connection.socket.readyState !== 0)
274
+ throw new Error('Duplicate tunnel ready');
275
+ clearTimeout(connection.timer);
276
+ connection.socket.readyState = 1;
277
+ connection.resolve(connection.socket);
278
+ }
279
+ else if (message.type === 'frame' && message.frame !== undefined && connection.socket.readyState === 1) {
280
+ parseTunnelFrame(message.frame);
281
+ connection.socket.emit('message', Buffer.from(message.frame), false);
282
+ }
283
+ else
284
+ throw new Error('Invalid requester relay message');
285
+ }
286
+ async toDeveloper(owner, remote, raw) {
287
+ const frame = parseTunnelFrame(raw);
288
+ if (frame.type === 'ready' || frame.type === 'response' || frame.type === 'ws-open')
289
+ throw new Error('Invalid requester frame direction');
290
+ const route = owner.streams.get(frame.id);
291
+ if (frame.type === 'request') {
292
+ if (route || owner.streams.size >= DEFAULT_TUNNEL_LIMITS.maxStreams) {
293
+ this.closeRemote(owner, remote);
294
+ await this.publish(remote.nodeId, this.envelope(owner.lease, remote.connection, 'close', 'requester'));
295
+ return;
296
+ }
297
+ owner.streams.set(frame.id, remote);
298
+ }
299
+ else if (!route || route.connection !== remote.connection || route.nodeId !== remote.nodeId)
300
+ return;
301
+ if (frame.type === 'cancel' || frame.type === 'ws-close')
302
+ owner.streams.delete(frame.id);
303
+ await this.writeDeveloper(owner, raw);
304
+ }
305
+ writeDeveloper(owner, raw) {
306
+ if (owner.stopped || owner.socket.readyState !== 1 || owner.socket.bufferedAmount + Buffer.byteLength(raw) > DEFAULT_TUNNEL_LIMITS.maxBufferedBytes) {
307
+ this.closeOwner(owner);
308
+ return Promise.reject(new Error('Developer tunnel disconnected or saturated'));
309
+ }
310
+ return new Promise((resolve, reject) => owner.socket.send(raw, (error) => error ? reject(error) : resolve()));
311
+ }
312
+ fromDeveloper(owner, data, binary) {
313
+ if (owner.stopped)
314
+ return;
315
+ if (binary || !(typeof data === 'string' || Buffer.isBuffer(data))) {
316
+ this.closeOwner(owner);
317
+ return;
318
+ }
319
+ const bytes = Buffer.byteLength(data);
320
+ if (bytes > DEFAULT_TUNNEL_LIMITS.maxFrameBytes || owner.queuedBytes + bytes > DEFAULT_TUNNEL_LIMITS.maxBufferedBytes) {
321
+ this.closeOwner(owner);
322
+ return;
323
+ }
324
+ const raw = typeof data === 'string' ? data : data.toString('utf8');
325
+ owner.queuedBytes += bytes;
326
+ owner.receiving = owner.receiving.then(async () => {
327
+ if (owner.stopped)
328
+ return;
329
+ if (!(await this.current(owner.lease, this.options.nodeId))) {
330
+ this.closeOwner(owner);
331
+ return;
332
+ }
333
+ const frame = parseTunnelFrame(data);
334
+ if (frame.type === 'request' || frame.type === 'ready')
335
+ throw new Error('Developer cannot initiate requests or readiness');
336
+ const remote = owner.streams.get(frame.id);
337
+ if (!remote)
338
+ return;
339
+ await this.publish(remote.nodeId, this.envelope(owner.lease, remote.connection, 'frame', 'requester', raw));
340
+ if (frame.type === 'end' || frame.type === 'cancel' || frame.type === 'ws-close')
341
+ owner.streams.delete(frame.id);
342
+ }).catch(() => this.closeOwner(owner)).finally(() => { owner.queuedBytes -= bytes; });
343
+ }
344
+ closeRemote(owner, remote) {
345
+ owner.remotes.delete(remote.connection);
346
+ for (const [id, route] of owner.streams) {
347
+ if (route.connection !== remote.connection)
348
+ continue;
349
+ owner.streams.delete(id);
350
+ void this.writeDeveloper(owner, JSON.stringify({ type: 'cancel', id })).catch(() => this.closeOwner(owner));
351
+ }
352
+ }
353
+ closeOwner(owner) {
354
+ if (owner.stopped)
355
+ return;
356
+ owner.stopped = true;
357
+ clearInterval(owner.timer);
358
+ if (this.attached.get(owner.lease.sessionId) === owner)
359
+ this.attached.delete(owner.lease.sessionId);
360
+ owner.socket.terminate();
361
+ for (const remote of owner.remotes.values())
362
+ void this.publish(remote.nodeId, this.envelope(owner.lease, remote.connection, 'close', 'requester')).catch(() => undefined);
363
+ owner.remotes.clear();
364
+ owner.streams.clear();
365
+ void this.options.ownership.release(owner.lease.sessionId, owner.lease.epoch, this.options.nodeId).catch(() => undefined);
366
+ }
367
+ closeConnection(id, connection) {
368
+ if (this.connected.get(id) !== connection)
369
+ return;
370
+ this.connected.delete(id);
371
+ clearTimeout(connection.timer);
372
+ clearInterval(connection.heartbeat);
373
+ connection.socket.finish();
374
+ connection.reject(new Error('Tunnel owner disconnected'));
375
+ }
376
+ disconnect(sessionId) {
377
+ identifier.parse(sessionId);
378
+ const owner = this.attached.get(sessionId);
379
+ if (owner)
380
+ this.closeOwner(owner);
381
+ for (const [id, connection] of this.connected)
382
+ if (connection.lease.sessionId === sessionId)
383
+ this.closeConnection(id, connection);
384
+ if (!owner)
385
+ void this.options.ownership.lookup(sessionId).then((remote) => {
386
+ if (remote)
387
+ return this.publish(remote.nodeId, { type: 'revoke', destination: 'owner', source: this.options.nodeId, connection: randomUUID(), sessionId, epoch: remote.epoch });
388
+ }).catch(() => undefined);
389
+ }
390
+ async close() {
391
+ if (this.closed)
392
+ return;
393
+ this.closed = true;
394
+ this.started = false;
395
+ this.generation++;
396
+ for (const owner of this.attached.values())
397
+ this.closeOwner(owner);
398
+ for (const connection of this.connected.values())
399
+ connection.socket.terminate();
400
+ await this.publishing;
401
+ this.removeListeners();
402
+ await this.options.subscriber.unsubscribe(this.channel(this.options.nodeId));
403
+ }
404
+ }
@@ -0,0 +1,17 @@
1
+ import type { EventEmitter } from 'node:events';
2
+ import type { TunnelLease } from './broker.js';
3
+ /** WebSocket-compatible framed transport. Remote transports use the same
4
+ * end-to-end ACK protocol; publishing a frame is not acknowledgement. */
5
+ export interface TunnelSocket extends EventEmitter {
6
+ readonly readyState: number;
7
+ readonly bufferedAmount: number;
8
+ send(data: string, callback: (error?: Error) => void): void;
9
+ terminate(): void;
10
+ }
11
+ export interface TunnelRelay {
12
+ isReady?(): boolean;
13
+ attach(lease: TunnelLease, socket: TunnelSocket): Promise<void>;
14
+ connect(lease: TunnelLease): Promise<TunnelSocket>;
15
+ disconnect(sessionId: string): void;
16
+ close(): Promise<void>;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "0.2.6",
12
+ "version": "0.3.1",
13
13
  "description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
14
14
  "type": "module",
15
15
  "exports": {
@@ -64,6 +64,62 @@
64
64
  "./child-environment": {
65
65
  "types": "./dist/child-environment.d.ts",
66
66
  "default": "./dist/child-environment.js"
67
+ },
68
+ "./manifest": {
69
+ "types": "./dist/manifest.d.ts",
70
+ "default": "./dist/manifest.js"
71
+ },
72
+ "./runtime-contracts": {
73
+ "types": "./dist/runtime-contracts.d.ts",
74
+ "default": "./dist/runtime-contracts.js"
75
+ },
76
+ "./artifact-ignore": {
77
+ "types": "./dist/artifact-ignore.d.ts",
78
+ "default": "./dist/artifact-ignore.js"
79
+ },
80
+ "./next-source-hash": {
81
+ "types": "./dist/next-source-hash.d.ts",
82
+ "default": "./dist/next-source-hash.js"
83
+ },
84
+ "./next-public-fonts": {
85
+ "types": "./dist/next-public-fonts.d.ts",
86
+ "default": "./dist/next-public-fonts.js"
87
+ },
88
+ "./server": {
89
+ "types": "./dist/server.d.ts",
90
+ "default": "./dist/server.js"
91
+ },
92
+ "./server/context": {
93
+ "types": "./dist/server/request-context.d.ts",
94
+ "default": "./dist/server/request-context.js"
95
+ },
96
+ "./server/broker-contract": {
97
+ "types": "./dist/server/broker-contract.d.ts",
98
+ "default": "./dist/server/broker-contract.js"
99
+ },
100
+ "./server/release-forms": {
101
+ "types": "./dist/server/release-forms.d.ts",
102
+ "default": "./dist/server/release-forms.js"
103
+ },
104
+ "./next": {
105
+ "types": "./dist/next.d.ts",
106
+ "default": "./dist/next.js"
107
+ },
108
+ "./next/protocol": {
109
+ "types": "./dist/next/protocol.d.ts",
110
+ "default": "./dist/next/protocol.js"
111
+ },
112
+ "./next-cache": {
113
+ "types": "./dist/next-cache/index.d.ts",
114
+ "default": "./dist/next-cache/index.js"
115
+ },
116
+ "./next-cache/config": {
117
+ "types": "./dist/next-cache/config.d.ts",
118
+ "default": "./dist/next-cache/config.js"
119
+ },
120
+ "./tunnel": {
121
+ "types": "./dist/tunnel/index.d.ts",
122
+ "default": "./dist/tunnel/index.js"
67
123
  }
68
124
  },
69
125
  "files": [
@@ -72,7 +128,7 @@
72
128
  "VERSION-PIN.md"
73
129
  ],
74
130
  "dependencies": {
75
- "@sequenceholdings/atlas-ui": "3.1.2",
131
+ "@sequenceholdings/atlas-ui": "3.3.0",
76
132
  "@tailwindcss/postcss": "^4.1.8",
77
133
  "@tailwindcss/vite": "^4.1.8",
78
134
  "@tanstack/react-query": "^4.36.1",
@@ -85,14 +141,21 @@
85
141
  "tw-animate-css": "^1.4.0",
86
142
  "vite": "8.0.16",
87
143
  "zod": "^4.1.13",
144
+ "ws": "^8.21.0",
145
+ "ioredis": "^5.10.0",
146
+ "google-auth-library": "^10.5.0",
147
+ "server-only": "0.0.1",
148
+ "parse5": "7.3.0",
88
149
  "@sequenceholdings/lattice-form-renderer": "0.1.3",
89
- "@sequenceholdings/storage": "0.1.0"
150
+ "@sequenceholdings/storage": "0.1.0",
151
+ "@sequenceholdings/orm": "2.1.0"
90
152
  },
91
153
  "devDependencies": {
92
154
  "@types/js-yaml": "^4.0.9",
93
155
  "@types/node": "^22.0.0",
94
156
  "typescript": "^5.6.0",
95
- "vitest": "^4.1.11"
157
+ "vitest": "^4.1.11",
158
+ "@types/ws": "^8.18.1"
96
159
  },
97
160
  "scripts": {
98
161
  "build": "rm -rf dist && tsc && node scripts/copy-templates.mjs",
@@ -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