stitchkit 0.80.0 → 0.81.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 (179) hide show
  1. package/dist/agent-runtime/provider-failure.d.ts.map +1 -1
  2. package/dist/agent-runtime-coding-tools.js +1 -1
  3. package/dist/agent-runtime-harness.js +7 -5
  4. package/dist/agent-runtime.js +15 -19
  5. package/dist/application/notification-outbox.d.ts +80 -0
  6. package/dist/application/notification-outbox.d.ts.map +1 -0
  7. package/dist/application/process-lifecycle.d.ts +166 -0
  8. package/dist/application/process-lifecycle.d.ts.map +1 -0
  9. package/dist/application/revision-signal.d.ts +86 -0
  10. package/dist/application/revision-signal.d.ts.map +1 -0
  11. package/dist/application/state-store.d.ts +18 -0
  12. package/dist/application/state-store.d.ts.map +1 -0
  13. package/dist/application.d.ts +4 -0
  14. package/dist/application.d.ts.map +1 -1
  15. package/dist/application.js +725 -1
  16. package/dist/browser/http.d.ts +14 -0
  17. package/dist/browser/http.d.ts.map +1 -1
  18. package/dist/browser/resumable.d.ts +6 -0
  19. package/dist/browser/resumable.d.ts.map +1 -1
  20. package/dist/cli.js +9 -7
  21. package/dist/contract/define.d.ts +27 -4
  22. package/dist/contract/define.d.ts.map +1 -1
  23. package/dist/contract/errors-factory.d.ts +22 -3
  24. package/dist/contract/errors-factory.d.ts.map +1 -1
  25. package/dist/contract/index.d.ts +2 -2
  26. package/dist/contract/index.d.ts.map +1 -1
  27. package/dist/contract/index.js +1 -1
  28. package/dist/geo/index.d.ts +4 -0
  29. package/dist/geo/index.d.ts.map +1 -0
  30. package/dist/geo/maxmind.d.ts +4 -0
  31. package/dist/geo/maxmind.d.ts.map +1 -0
  32. package/dist/geo/resolver.d.ts +9 -0
  33. package/dist/geo/resolver.d.ts.map +1 -0
  34. package/dist/geo/types.d.ts +59 -0
  35. package/dist/geo/types.d.ts.map +1 -0
  36. package/dist/geo.d.ts +3 -0
  37. package/dist/geo.d.ts.map +1 -0
  38. package/dist/geo.js +245 -0
  39. package/dist/{index-9s6z8vgf.js → index-1dd82z59.js} +1 -1
  40. package/dist/{index-vwp69871.js → index-27487ay5.js} +4 -4
  41. package/dist/{index-7ag81v2f.js → index-27gm3d6g.js} +10 -321
  42. package/dist/{index-y01f4223.js → index-2bxcmqkb.js} +127 -43
  43. package/dist/{index-dxesg10d.js → index-3ds42w1w.js} +3 -3
  44. package/dist/index-3xwxfj4z.js +334 -0
  45. package/dist/{index-egaabxkz.js → index-5z8tezpx.js} +2 -2
  46. package/dist/{index-fxz0v7vg.js → index-7evdsyez.js} +1 -83
  47. package/dist/{index-xcetq6j3.js → index-90d7h2cr.js} +32 -13
  48. package/dist/{index-t0z5p5w6.js → index-apn8repm.js} +1 -1
  49. package/dist/{index-758zx2ha.js → index-dggwnnjv.js} +2 -2
  50. package/dist/{index-dwf1f2ap.js → index-f9mb610r.js} +1 -62
  51. package/dist/{index-5mtc969c.js → index-fwbkp2jn.js} +1 -1
  52. package/dist/index-p13mwz16.js +16 -0
  53. package/dist/{index-m9kzax52.js → index-p18f59zy.js} +21 -5
  54. package/dist/{index-hb5pf3s4.js → index-qmgs7y68.js} +1 -1
  55. package/dist/{index-2rwwmr7v.js → index-s4ckmrac.js} +1 -1
  56. package/dist/{index-h4pj6fta.js → index-s8cnx02s.js} +8 -5
  57. package/dist/{index-k9ay6v6d.js → index-vchzar7e.js} +74 -23
  58. package/dist/index-vsbzgd7b.js +84 -0
  59. package/dist/{index-dxjjv399.js → index-xbppf54a.js} +95 -15
  60. package/dist/index-y91ry9h2.js +4 -0
  61. package/dist/index-zcgf3gqf.js +83 -0
  62. package/dist/index.js +17 -13
  63. package/dist/internal/error-message.d.ts +3 -0
  64. package/dist/internal/error-message.d.ts.map +1 -0
  65. package/dist/internal/media-type.d.ts +8 -0
  66. package/dist/internal/media-type.d.ts.map +1 -0
  67. package/dist/internal/route-pattern.d.ts +21 -0
  68. package/dist/internal/route-pattern.d.ts.map +1 -1
  69. package/dist/node.js +9 -6
  70. package/dist/observability/audit.d.ts +28 -1
  71. package/dist/observability/audit.d.ts.map +1 -1
  72. package/dist/observability/bounded-logger.d.ts +20 -0
  73. package/dist/observability/bounded-logger.d.ts.map +1 -0
  74. package/dist/observability/context.d.ts +12 -3
  75. package/dist/observability/context.d.ts.map +1 -1
  76. package/dist/observability/index.d.ts +3 -2
  77. package/dist/observability/index.d.ts.map +1 -1
  78. package/dist/observability/index.js +138 -8
  79. package/dist/observability/sanitize.d.ts +17 -0
  80. package/dist/observability/sanitize.d.ts.map +1 -1
  81. package/dist/primitives.js +1 -1
  82. package/dist/react/query-client.d.ts +33 -0
  83. package/dist/react/query-client.d.ts.map +1 -0
  84. package/dist/react.d.ts +1 -0
  85. package/dist/react.d.ts.map +1 -1
  86. package/dist/react.js +83 -1
  87. package/dist/realtime/registry.d.ts +93 -0
  88. package/dist/realtime/registry.d.ts.map +1 -0
  89. package/dist/release/header.d.ts +9 -0
  90. package/dist/release/header.d.ts.map +1 -0
  91. package/dist/release/marker.d.ts +39 -0
  92. package/dist/release/marker.d.ts.map +1 -0
  93. package/dist/release/socket.d.ts +44 -0
  94. package/dist/release/socket.d.ts.map +1 -0
  95. package/dist/release/watcher.d.ts +66 -0
  96. package/dist/release/watcher.d.ts.map +1 -0
  97. package/dist/release.d.ts +15 -0
  98. package/dist/release.d.ts.map +1 -0
  99. package/dist/release.js +196 -0
  100. package/dist/remote.js +12 -7
  101. package/dist/server/context.d.ts +2 -1
  102. package/dist/server/context.d.ts.map +1 -1
  103. package/dist/server/create.d.ts.map +1 -1
  104. package/dist/server/error-hook.d.ts +17 -16
  105. package/dist/server/error-hook.d.ts.map +1 -1
  106. package/dist/server/file-state-store.d.ts +23 -0
  107. package/dist/server/file-state-store.d.ts.map +1 -0
  108. package/dist/server/implement.d.ts.map +1 -1
  109. package/dist/server/index.d.ts +5 -2
  110. package/dist/server/index.d.ts.map +1 -1
  111. package/dist/server/index.js +469 -11
  112. package/dist/server/middleware/cookies.d.ts +33 -1
  113. package/dist/server/middleware/cookies.d.ts.map +1 -1
  114. package/dist/server/middleware/cors.d.ts +16 -1
  115. package/dist/server/middleware/cors.d.ts.map +1 -1
  116. package/dist/server/openapi.d.ts.map +1 -1
  117. package/dist/server/release-signal.d.ts +35 -0
  118. package/dist/server/release-signal.d.ts.map +1 -0
  119. package/dist/server/router.d.ts.map +1 -1
  120. package/dist/server/types.d.ts +26 -2
  121. package/dist/server/types.d.ts.map +1 -1
  122. package/dist/testing/surface-manifest.d.ts.map +1 -1
  123. package/dist/testing.js +7 -4
  124. package/dist/tool-invoker.js +8 -6
  125. package/dist/tools/download-core.d.ts.map +1 -1
  126. package/dist/tools/remote.d.ts.map +1 -1
  127. package/dist/tools/view-file.d.ts.map +1 -1
  128. package/dist/tools.js +20 -16
  129. package/dist/tracking/attribution.d.ts +51 -0
  130. package/dist/tracking/attribution.d.ts.map +1 -0
  131. package/dist/tracking/beacon.d.ts +14 -0
  132. package/dist/tracking/beacon.d.ts.map +1 -0
  133. package/dist/tracking/clicks.d.ts +46 -0
  134. package/dist/tracking/clicks.d.ts.map +1 -0
  135. package/dist/tracking/client.d.ts +121 -0
  136. package/dist/tracking/client.d.ts.map +1 -0
  137. package/dist/tracking/contract.d.ts +46 -0
  138. package/dist/tracking/contract.d.ts.map +1 -0
  139. package/dist/tracking/delivery.d.ts +24 -0
  140. package/dist/tracking/delivery.d.ts.map +1 -0
  141. package/dist/tracking/host.d.ts +43 -0
  142. package/dist/tracking/host.d.ts.map +1 -0
  143. package/dist/tracking/once-per-page.d.ts +17 -0
  144. package/dist/tracking/once-per-page.d.ts.map +1 -0
  145. package/dist/tracking/outbox-storage-indexeddb.d.ts +12 -0
  146. package/dist/tracking/outbox-storage-indexeddb.d.ts.map +1 -0
  147. package/dist/tracking/outbox-storage-memory.d.ts +12 -0
  148. package/dist/tracking/outbox-storage-memory.d.ts.map +1 -0
  149. package/dist/tracking/outbox.d.ts +98 -0
  150. package/dist/tracking/outbox.d.ts.map +1 -0
  151. package/dist/tracking/schemas.d.ts +183 -0
  152. package/dist/tracking/schemas.d.ts.map +1 -0
  153. package/dist/tracking/scroll.d.ts +22 -0
  154. package/dist/tracking/scroll.d.ts.map +1 -0
  155. package/dist/tracking/sequence-reserve.d.ts +34 -0
  156. package/dist/tracking/sequence-reserve.d.ts.map +1 -0
  157. package/dist/tracking/server/active-interval.d.ts +26 -0
  158. package/dist/tracking/server/active-interval.d.ts.map +1 -0
  159. package/dist/tracking/server/bot.d.ts +9 -0
  160. package/dist/tracking/server/bot.d.ts.map +1 -0
  161. package/dist/tracking/server/disposition.d.ts +55 -0
  162. package/dist/tracking/server/disposition.d.ts.map +1 -0
  163. package/dist/tracking/server/hash.d.ts +13 -0
  164. package/dist/tracking/server/hash.d.ts.map +1 -0
  165. package/dist/tracking/server/presence.d.ts +27 -0
  166. package/dist/tracking/server/presence.d.ts.map +1 -0
  167. package/dist/tracking/server/visit-lease.d.ts +92 -0
  168. package/dist/tracking/server/visit-lease.d.ts.map +1 -0
  169. package/dist/tracking/visible-time.d.ts +38 -0
  170. package/dist/tracking/visible-time.d.ts.map +1 -0
  171. package/dist/tracking-server.d.ts +14 -0
  172. package/dist/tracking-server.d.ts.map +1 -0
  173. package/dist/tracking-server.js +173 -0
  174. package/dist/tracking.d.ts +24 -0
  175. package/dist/tracking.d.ts.map +1 -0
  176. package/dist/tracking.js +1090 -0
  177. package/llms-full.txt +986 -30
  178. package/llms.txt +4 -0
  179. package/package.json +26 -5
@@ -13,7 +13,8 @@ import {
13
13
  sseRoute,
14
14
  streamingRoute,
15
15
  webSocketLane
16
- } from "../index-k9ay6v6d.js";
16
+ } from "../index-vchzar7e.js";
17
+ import"../index-y91ry9h2.js";
17
18
  import {
18
19
  withDeadline
19
20
  } from "../index-b9aefrqd.js";
@@ -24,7 +25,7 @@ import {
24
25
  ShutdownStatusSchema,
25
26
  createServerLifecycle
26
27
  } from "../index-3z73fh2c.js";
27
- import"../index-t0z5p5w6.js";
28
+ import"../index-apn8repm.js";
28
29
  import"../index-2qzskm8h.js";
29
30
  import {
30
31
  composeAuthHooks,
@@ -33,12 +34,13 @@ import {
33
34
  defineCookie,
34
35
  deriveCodeChallenge,
35
36
  extractToken,
37
+ parseCookieHeader,
36
38
  parseCookies,
37
39
  serializeCookie,
38
40
  signJwt,
39
41
  verifyJwt,
40
42
  verifyPkce
41
- } from "../index-xcetq6j3.js";
43
+ } from "../index-90d7h2cr.js";
42
44
  import {
43
45
  DEFAULT_CORS_ALLOW_HEADERS,
44
46
  DEFAULT_CORS_EXPOSE_HEADERS,
@@ -52,20 +54,22 @@ import {
52
54
  defineMultipartStream,
53
55
  implement,
54
56
  implementRegistry
55
- } from "../index-m9kzax52.js";
57
+ } from "../index-p18f59zy.js";
56
58
  import {
57
59
  isWithinDir,
58
60
  realPathWithinDir
59
61
  } from "../index-sbdmyz75.js";
62
+ import {
63
+ getTraceId
64
+ } from "../index-vsbzgd7b.js";
60
65
  import {
61
66
  extractIp,
62
67
  generateTraceId,
63
68
  getClientInfo,
64
- getTraceId,
65
69
  isPublicIp,
66
70
  resolveSocketIp,
67
71
  resolveTraceId
68
- } from "../index-dwf1f2ap.js";
72
+ } from "../index-f9mb610r.js";
69
73
  import {
70
74
  jsonSchemaFields,
71
75
  toJsonSchema
@@ -74,18 +78,18 @@ import {
74
78
  inputIsQuery,
75
79
  parseSSE,
76
80
  streamSSE
77
- } from "../index-hb5pf3s4.js";
81
+ } from "../index-qmgs7y68.js";
78
82
  import {
79
83
  errorCode,
80
84
  formatZodError,
81
85
  normalizeError,
82
86
  zodIssues
83
- } from "../index-fxz0v7vg.js";
87
+ } from "../index-7evdsyez.js";
84
88
  import {
85
89
  DEFAULT_CONTRACT_STREAM_FRAME_BYTES,
86
90
  joinRoutePath,
87
91
  parseTrailingWildcard
88
- } from "../index-y01f4223.js";
92
+ } from "../index-2bxcmqkb.js";
89
93
  import"../index-6k1937bx.js";
90
94
  import {
91
95
  AppError,
@@ -99,10 +103,223 @@ import {
99
103
  rateLimited,
100
104
  unauthorized
101
105
  } from "../index-0w9abg87.js";
106
+ import"../index-zcgf3gqf.js";
102
107
  import"../index-kzfs85xp.js";
103
108
  import {
104
109
  isRecord
105
110
  } from "../index-77fekveh.js";
111
+ // src/realtime/registry.ts
112
+ var authorizedRoom = Symbol("stitchkit.authorized-room");
113
+ function freezeRoom(name) {
114
+ return Object.freeze({ name, [authorizedRoom]: true });
115
+ }
116
+ function bindSocketRegistry(server, options) {
117
+ const members = new Map;
118
+ const minted = new WeakSet;
119
+ let revision = 0;
120
+ let closed = false;
121
+ const setups = new Set;
122
+ const maxBufferedFrames = Math.max(1, options.maxBufferedFrames ?? 1000);
123
+ const token = (name) => {
124
+ const created = freezeRoom(name);
125
+ minted.add(created);
126
+ return created;
127
+ };
128
+ const assertToken = (room) => {
129
+ if (!minted.has(room))
130
+ throw new Error("Room was not authorized by this registry");
131
+ return room.name;
132
+ };
133
+ const leaveQuietly = async (member, room) => {
134
+ try {
135
+ await member.connection.raw.leave(room);
136
+ } catch {}
137
+ };
138
+ const remove = async (member) => {
139
+ if (member.disconnected)
140
+ return;
141
+ member.disconnected = true;
142
+ member.connection.raw.off?.("disconnect", member.disconnect);
143
+ members.delete(member.connection.raw.id);
144
+ for (const room of member.joined)
145
+ await leaveQuietly(member, room);
146
+ member.joined.clear();
147
+ member.buffer = null;
148
+ revision += 1;
149
+ };
150
+ const replayMember = async (member) => {
151
+ if (!options.replay || member.disconnected || closed)
152
+ return !member.disconnected && !closed;
153
+ const attempts = Math.max(1, options.replayAttempts ?? 3);
154
+ for (let attempt = 0;attempt < attempts && !member.disconnected && !closed; attempt += 1) {
155
+ member.buffer = [];
156
+ member.overflow = false;
157
+ const before = await options.revision?.();
158
+ if (member.disconnected || closed)
159
+ break;
160
+ const frames = await options.replay({
161
+ identity: member.identity,
162
+ rooms: Object.freeze([...member.joined].map(token)),
163
+ revision: before
164
+ });
165
+ const after = await options.revision?.();
166
+ if (member.disconnected || closed)
167
+ break;
168
+ if (options.revision && !Object.is(before, after))
169
+ continue;
170
+ if (member.overflow)
171
+ continue;
172
+ for (const frame of frames)
173
+ member.connection.events.emit(frame.event, ...frame.args);
174
+ const buffered = member.buffer;
175
+ member.buffer = null;
176
+ for (const deferred of buffered ?? [])
177
+ deferred();
178
+ return !member.disconnected && !closed;
179
+ }
180
+ member.buffer = null;
181
+ member.overflow = false;
182
+ if (!member.disconnected)
183
+ await options.onResyncRequired?.(member.connection.raw.id, member.identity);
184
+ return false;
185
+ };
186
+ const resyncMember = (member) => {
187
+ if (member.replayPromise)
188
+ return member.replayPromise;
189
+ const active = replayMember(member).finally(() => {
190
+ if (member.replayPromise === active)
191
+ member.replayPromise = undefined;
192
+ });
193
+ member.replayPromise = active;
194
+ return active;
195
+ };
196
+ const setupConnection = async (connection) => {
197
+ if (closed)
198
+ return;
199
+ const member = {
200
+ connection,
201
+ identity: connection.raw.data,
202
+ authorized: new Set,
203
+ joined: new Set,
204
+ buffer: null,
205
+ overflow: false,
206
+ disconnected: false,
207
+ disconnect: () => {
208
+ return;
209
+ }
210
+ };
211
+ member.disconnect = () => void remove(member);
212
+ connection.raw.on("disconnect", member.disconnect);
213
+ members.set(connection.raw.id, member);
214
+ try {
215
+ const authorized = new Set(await options.rooms(connection.raw.data));
216
+ if (member.disconnected || closed) {
217
+ await remove(member);
218
+ return;
219
+ }
220
+ member.authorized = authorized;
221
+ for (const name of authorized) {
222
+ await connection.raw.join(name);
223
+ if (member.disconnected || closed) {
224
+ await leaveQuietly(member, name);
225
+ await remove(member);
226
+ return;
227
+ }
228
+ member.joined.add(name);
229
+ }
230
+ if (closed) {
231
+ await remove(member);
232
+ return;
233
+ }
234
+ revision += 1;
235
+ await resyncMember(member);
236
+ } catch {
237
+ await remove(member);
238
+ }
239
+ };
240
+ const stopListening = server.onConnection((connection) => {
241
+ let setup;
242
+ setup = setupConnection(connection).finally(() => setups.delete(setup));
243
+ setups.add(setup);
244
+ return setup;
245
+ });
246
+ return {
247
+ room: token,
248
+ async join(socketId, room) {
249
+ const name = assertToken(room);
250
+ const member = members.get(socketId);
251
+ if (!member)
252
+ throw new Error(`Unknown socket: ${socketId}`);
253
+ if (!member.authorized.has(name))
254
+ throw new Error(`Socket is not authorized for room: ${name}`);
255
+ if (member.joined.has(name))
256
+ return;
257
+ await member.connection.raw.join(name);
258
+ if (!member.disconnected)
259
+ member.joined.add(name);
260
+ revision += 1;
261
+ },
262
+ async leave(socketId, room) {
263
+ const name = assertToken(room);
264
+ const member = members.get(socketId);
265
+ if (!member?.joined.has(name))
266
+ return;
267
+ await member.connection.raw.leave(name);
268
+ member.joined.delete(name);
269
+ revision += 1;
270
+ },
271
+ emitTo(room, event, ...args) {
272
+ const name = assertToken(room);
273
+ let recipients = 0;
274
+ for (const member of members.values()) {
275
+ if (!member.joined.has(name) || member.disconnected)
276
+ continue;
277
+ recipients += 1;
278
+ if (!member.buffer) {
279
+ member.connection.events.emit(event, ...args);
280
+ } else if (member.buffer.length >= maxBufferedFrames) {
281
+ member.overflow = true;
282
+ } else {
283
+ member.buffer.push(() => member.connection.events.emit(event, ...args));
284
+ }
285
+ }
286
+ return recipients;
287
+ },
288
+ resync: async (socketId) => {
289
+ const member = members.get(socketId);
290
+ return member ? resyncMember(member) : false;
291
+ },
292
+ snapshot() {
293
+ const connections = [...members.values()].map((member) => Object.freeze({
294
+ socketId: member.connection.raw.id,
295
+ identity: member.identity,
296
+ rooms: Object.freeze([...member.joined].sort()),
297
+ replaying: member.buffer !== null
298
+ }));
299
+ const roomMap = {};
300
+ for (const entry of connections) {
301
+ for (const name of entry.rooms) {
302
+ roomMap[name] = Object.freeze([...roomMap[name] ?? [], entry.socketId]);
303
+ }
304
+ }
305
+ return Object.freeze({
306
+ revision,
307
+ connections: Object.freeze(connections),
308
+ rooms: Object.freeze(roomMap)
309
+ });
310
+ },
311
+ async unbind() {
312
+ if (closed)
313
+ return;
314
+ closed = true;
315
+ stopListening();
316
+ await Promise.allSettled([...setups]);
317
+ const active = [...members.values()];
318
+ for (const member of active)
319
+ await remove(member);
320
+ }
321
+ };
322
+ }
106
323
  // src/server/bun.ts
107
324
  import { chmodSync, statSync, unlinkSync } from "node:fs";
108
325
  function reclaimStaleUnixSocket(path) {
@@ -389,10 +606,15 @@ function cacheHeaders(maxAge, scope = "public") {
389
606
  }
390
607
  // src/server/error-hook.ts
391
608
  function createErrorHook(config) {
609
+ if (config.vocabulary && (config.codeMap !== undefined || config.unmappedCode !== undefined)) {
610
+ throw new Error("[stitchkit] createErrorHook: `vocabulary` carries the wire map — it cannot be combined with `codeMap` or `unmappedCode`");
611
+ }
392
612
  return async (ctx, error, endpoint) => {
393
613
  const appErr = normalizeError(error);
394
614
  const stitchCode = isStitchErrorCode(appErr.code) ? appErr.code : undefined;
395
- const mapped = stitchCode === undefined ? undefined : config.codeMap?.[stitchCode];
615
+ const vocabularyMap = config.vocabulary?.codeMap;
616
+ const configuredMap = vocabularyMap ?? config.codeMap;
617
+ const mapped = stitchCode === undefined ? undefined : configuredMap?.[stitchCode];
396
618
  const fallback = mapped === undefined && stitchCode !== undefined ? typeof config.unmappedCode === "function" ? config.unmappedCode(stitchCode) : config.unmappedCode : undefined;
397
619
  const code = mapped ?? fallback ?? appErr.code;
398
620
  const info = {
@@ -775,6 +997,221 @@ async function serveFile(req, opts) {
775
997
  base.set("Content-Length", String(size));
776
998
  return new Response(isHead ? null : file, { status: 200, headers: base });
777
999
  }
1000
+ // src/server/file-state-store.ts
1001
+ import { mkdir, open, readdir, readFile as readFile2, rename, stat as stat2, unlink } from "node:fs/promises";
1002
+ import { basename, dirname, join } from "node:path";
1003
+ var ABANDONED_LOCK_MULTIPLIER = 10;
1004
+ var ABANDONED_HEARTBEATS = 30;
1005
+ var MIN_HEARTBEAT_MS = 10;
1006
+ var delay = (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds));
1007
+ async function missing(error) {
1008
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1009
+ }
1010
+ function existing(error) {
1011
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
1012
+ }
1013
+ async function ownerState(lockPath) {
1014
+ let owner;
1015
+ try {
1016
+ owner = JSON.parse(await readFile2(lockPath, "utf8"));
1017
+ } catch {
1018
+ return "unknown";
1019
+ }
1020
+ if (typeof owner !== "object" || owner === null || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) {
1021
+ return "unknown";
1022
+ }
1023
+ try {
1024
+ process.kill(owner.pid, 0);
1025
+ return "alive";
1026
+ } catch (error) {
1027
+ return error instanceof Error && "code" in error && error.code === "ESRCH" ? "gone" : "unknown";
1028
+ }
1029
+ }
1030
+ async function sweepTemporaries(path, staleLockMs) {
1031
+ const prefix = `${basename(path)}.tmp.`;
1032
+ const directory = dirname(path);
1033
+ let entries;
1034
+ try {
1035
+ entries = await readdir(directory);
1036
+ } catch {
1037
+ return;
1038
+ }
1039
+ for (const entry of entries) {
1040
+ if (!entry.startsWith(prefix))
1041
+ continue;
1042
+ const candidate = join(directory, entry);
1043
+ const info = await stat2(candidate).catch(() => null);
1044
+ if (info && Date.now() - info.mtimeMs > staleLockMs) {
1045
+ await unlink(candidate).catch(() => {
1046
+ return;
1047
+ });
1048
+ }
1049
+ }
1050
+ }
1051
+ async function reclaim(lockPath, observed, staleLockMs, abandonedAfterMs) {
1052
+ const guardPath = `${lockPath}.reclaim`;
1053
+ let guard;
1054
+ try {
1055
+ guard = await open(guardPath, "wx");
1056
+ } catch (error) {
1057
+ if (!existing(error))
1058
+ throw error;
1059
+ const orphan = await stat2(guardPath).catch(() => null);
1060
+ if (orphan && Date.now() - orphan.mtimeMs > staleLockMs) {
1061
+ await unlink(guardPath).catch(() => {
1062
+ return;
1063
+ });
1064
+ }
1065
+ return false;
1066
+ }
1067
+ try {
1068
+ const current = await stat2(lockPath).catch(() => null);
1069
+ if (!current || current.dev !== observed.dev || current.ino !== observed.ino || current.mtimeMs !== observed.mtimeMs) {
1070
+ return false;
1071
+ }
1072
+ const owner = await ownerState(lockPath);
1073
+ const age = Date.now() - current.mtimeMs;
1074
+ if (owner !== "gone" && age < abandonedAfterMs)
1075
+ return false;
1076
+ await unlink(lockPath);
1077
+ return true;
1078
+ } finally {
1079
+ await guard.close().catch(() => {
1080
+ return;
1081
+ });
1082
+ await unlink(guardPath).catch(() => {
1083
+ return;
1084
+ });
1085
+ }
1086
+ }
1087
+ function createFileStateStore(path, options) {
1088
+ const lockPath = `${path}.lock`;
1089
+ const lockTimeoutMs = options.lockTimeoutMs ?? 1e4;
1090
+ const staleLockMs = options.staleLockMs ?? 3000;
1091
+ const retryMs = options.retryMs ?? 10;
1092
+ const heartbeatMs = Math.max(MIN_HEARTBEAT_MS, Math.floor(staleLockMs / 3));
1093
+ const abandonedAfterMs = Math.max(staleLockMs * ABANDONED_LOCK_MULTIPLIER, heartbeatMs * ABANDONED_HEARTBEATS);
1094
+ if (staleLockMs >= lockTimeoutMs) {
1095
+ throw new Error(`[stitchkit] createFileStateStore: staleLockMs (${staleLockMs}) must be below lockTimeoutMs (${lockTimeoutMs}), or a crashed holder blocks every update until the timeout`);
1096
+ }
1097
+ let sweptTemporaries = false;
1098
+ const parseFile = async () => {
1099
+ let source;
1100
+ try {
1101
+ source = await readFile2(path, "utf8");
1102
+ } catch (error) {
1103
+ if (await missing(error))
1104
+ return null;
1105
+ throw error;
1106
+ }
1107
+ try {
1108
+ return options.schema.parse(JSON.parse(source));
1109
+ } catch (error) {
1110
+ await options.onCorrupt?.({ path, error });
1111
+ if ((options.corrupt ?? "throw") === "empty")
1112
+ return null;
1113
+ throw error;
1114
+ }
1115
+ };
1116
+ const acquire = async () => {
1117
+ await mkdir(dirname(path), { recursive: true });
1118
+ if (!sweptTemporaries) {
1119
+ sweptTemporaries = true;
1120
+ await sweepTemporaries(path, staleLockMs);
1121
+ }
1122
+ const deadline = Date.now() + lockTimeoutMs;
1123
+ while (true) {
1124
+ try {
1125
+ const handle = await open(lockPath, "wx");
1126
+ const token = crypto.randomUUID();
1127
+ try {
1128
+ await handle.writeFile(JSON.stringify({ token, pid: process.pid, acquiredAt: Date.now() }));
1129
+ await handle.sync();
1130
+ } catch (error) {
1131
+ await handle.close().catch(() => {
1132
+ return;
1133
+ });
1134
+ await unlink(lockPath).catch(() => {
1135
+ return;
1136
+ });
1137
+ throw error;
1138
+ }
1139
+ return {
1140
+ refresh: async () => {
1141
+ const now = new Date;
1142
+ await handle.utimes(now, now);
1143
+ },
1144
+ release: async () => {
1145
+ await handle.close();
1146
+ try {
1147
+ const owner = JSON.parse(await readFile2(lockPath, "utf8"));
1148
+ if (isRecord(owner) && owner.token === token)
1149
+ await unlink(lockPath);
1150
+ } catch (error) {
1151
+ if (!await missing(error))
1152
+ throw error;
1153
+ }
1154
+ }
1155
+ };
1156
+ } catch (error) {
1157
+ if (!existing(error))
1158
+ throw error;
1159
+ try {
1160
+ const lock = await stat2(lockPath);
1161
+ if (Date.now() - lock.mtimeMs > staleLockMs) {
1162
+ if (await reclaim(lockPath, lock, staleLockMs, abandonedAfterMs))
1163
+ continue;
1164
+ }
1165
+ } catch (statError) {
1166
+ if (await missing(statError))
1167
+ continue;
1168
+ throw statError;
1169
+ }
1170
+ if (Date.now() >= deadline) {
1171
+ throw new Error(`[stitchkit] timed out acquiring state lock for ${path}`);
1172
+ }
1173
+ await delay(retryMs);
1174
+ }
1175
+ }
1176
+ };
1177
+ const write = async (state) => {
1178
+ const validated = options.schema.parse(state);
1179
+ const temporary = `${path}.tmp.${process.pid}.${crypto.randomUUID()}`;
1180
+ const handle = await open(temporary, "wx");
1181
+ try {
1182
+ try {
1183
+ await handle.writeFile(`${JSON.stringify(validated)}
1184
+ `);
1185
+ await handle.sync();
1186
+ } finally {
1187
+ await handle.close();
1188
+ }
1189
+ await rename(temporary, path);
1190
+ } catch (error) {
1191
+ await unlink(temporary).catch(() => {
1192
+ return;
1193
+ });
1194
+ throw error;
1195
+ }
1196
+ };
1197
+ return {
1198
+ read: parseFile,
1199
+ async update(transition) {
1200
+ const lock = await acquire();
1201
+ const heartbeat = setInterval(() => void lock.refresh().catch(() => {
1202
+ return;
1203
+ }), heartbeatMs);
1204
+ try {
1205
+ const next = await transition(await parseFile());
1206
+ await write(next.state);
1207
+ return next.result;
1208
+ } finally {
1209
+ clearInterval(heartbeat);
1210
+ await lock.release();
1211
+ }
1212
+ }
1213
+ };
1214
+ }
778
1215
  // src/server/lifecycle.ts
779
1216
  function composeLifecycleHooks(...hooks) {
780
1217
  return {
@@ -1105,9 +1542,10 @@ function generateOpenApiDocument(config) {
1105
1542
  };
1106
1543
  } else if (method.inputSchema) {
1107
1544
  const required = jsonSchemaFields(safeJson(method.inputSchema, "input")).some((f) => f.required);
1545
+ const schema = safeJson(method.inputSchema, "input");
1108
1546
  operation.requestBody = {
1109
1547
  required,
1110
- content: { "application/json": { schema: safeJson(method.inputSchema, "input") } }
1548
+ content: method.safelistedBody ? { "application/json": { schema }, "text/plain": { schema } } : { "application/json": { schema } }
1111
1549
  };
1112
1550
  }
1113
1551
  }
@@ -1198,6 +1636,22 @@ async function parseBody(req, schema) {
1198
1636
  const parsed = schema.safeParse(raw);
1199
1637
  return parsed.success ? parsed.data : null;
1200
1638
  }
1639
+ // src/server/release-signal.ts
1640
+ function bindReleaseRefreshSignal(marker, options = {}) {
1641
+ const signal = options.signal ?? "SIGUSR2";
1642
+ const source = options.source ?? process;
1643
+ const handler = () => {
1644
+ try {
1645
+ options.onRefresh?.(marker.refresh());
1646
+ } catch (error) {
1647
+ try {
1648
+ options.onError?.(error);
1649
+ } catch {}
1650
+ }
1651
+ };
1652
+ source.on(signal, handler);
1653
+ return () => source.off(signal, handler);
1654
+ }
1201
1655
  export {
1202
1656
  zodIssues,
1203
1657
  webSocketLane,
@@ -1220,6 +1674,7 @@ export {
1220
1674
  parseSSE,
1221
1675
  parseMultipart,
1222
1676
  parseCookies,
1677
+ parseCookieHeader,
1223
1678
  parseByteRange,
1224
1679
  parseBody,
1225
1680
  openApiRoute,
@@ -1255,6 +1710,7 @@ export {
1255
1710
  createImplementRegistry,
1256
1711
  createImplement,
1257
1712
  createHandler,
1713
+ createFileStateStore,
1258
1714
  createEventBus,
1259
1715
  createErrorHook,
1260
1716
  createCache,
@@ -1267,6 +1723,8 @@ export {
1267
1723
  composeLifecycleHooks,
1268
1724
  composeAuthHooks,
1269
1725
  cacheHeaders,
1726
+ bindSocketRegistry,
1727
+ bindReleaseRefreshSignal,
1270
1728
  bindRealtimeServer,
1271
1729
  bindProcessSignals,
1272
1730
  badRequest,
@@ -1,4 +1,29 @@
1
+ /**
2
+ * Every `name=value` pair of a `Cookie` header, in header order, duplicates
3
+ * kept. The order is the browser's (RFC 6265 §5.4: longer path first, then
4
+ * earlier creation time) and a server "SHOULD NOT rely upon" it (§4.2.2) — so
5
+ * this list is what a reader consults when it needs to see *all* candidates
6
+ * rather than the one the order happened to put last.
7
+ */
8
+ export declare function parseCookieHeader(header: string | null): Array<[string, string]>;
9
+ /**
10
+ * The `Cookie` header as a record. Two cookies with one name collapse to the
11
+ * **last** occurrence — a fact of the record shape, not a rule about cookies;
12
+ * `parseCookieHeader` keeps both, and `defineCookie` lets a handle declare
13
+ * which it wants.
14
+ */
1
15
  export declare function parseCookies(header: string | null): Record<string, string>;
16
+ /**
17
+ * What `get` answers when the request carries more than one cookie of this
18
+ * name. The browser decides their order, so `'last'` and `'first'` are each a
19
+ * coin the browser flips; they exist so an application can *name* the
20
+ * behaviour it relies on. `'reject'` refuses to flip it: two **different**
21
+ * values yield `undefined`, so a session reader sees "no session" instead of
22
+ * a session that works in one browser and not another. Two identical values
23
+ * are one value — a host cookie and a parent-domain cookie set by the same
24
+ * application — and are returned.
25
+ */
26
+ export type CookieDuplicatesPolicy = 'last' | 'first' | 'reject';
2
27
  export interface CookieOptions {
3
28
  maxAge?: number;
4
29
  path?: string;
@@ -6,12 +31,19 @@ export interface CookieOptions {
6
31
  secure?: boolean;
7
32
  httpOnly?: boolean;
8
33
  sameSite?: 'Strict' | 'Lax' | 'None';
34
+ /** How `get` resolves two cookies of this name; default `'last'`. */
35
+ duplicates?: CookieDuplicatesPolicy;
9
36
  }
10
37
  export declare function serializeCookie(name: string, value: string, opts?: CookieOptions): string;
11
38
  /** A cookie's name + options bundled into a typed read/set/clear handle. */
12
39
  export interface CookieDef {
13
- /** Read this cookie's value from a request (`undefined` if absent). */
40
+ /**
41
+ * Read this cookie's value from a request (`undefined` if absent). With two
42
+ * cookies of this name the answer follows the handle's `duplicates` policy.
43
+ */
14
44
  get(req: Request): string | undefined;
45
+ /** Every value of this name the request carries, in header order. */
46
+ getAll(req: Request): string[];
15
47
  /** Build a `Set-Cookie` header value for `value` with the baked-in options. */
16
48
  set(value: string): string;
17
49
  /** Build a `Set-Cookie` header value that clears the cookie. */
@@ -1 +1 @@
1
- {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/cookies.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB1E;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,aAAkB,GACvB,MAAM,CASR;AAED,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,GAAG,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,+EAA+E;IAC/E,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,gEAAgE;IAChE,KAAK,IAAI,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAahF"}
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/cookies.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAmBhF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ1E;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,qEAAqE;IACrE,UAAU,CAAC,EAAE,sBAAsB,CAAC;CACrC;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,aAAkB,GACvB,MAAM,CASR;AAED,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,qEAAqE;IACrE,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAAC;IAC/B,+EAA+E;IAC/E,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,gEAAgE;IAChE,KAAK,IAAI,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAwBhF"}
@@ -43,8 +43,12 @@ export declare const DEFAULT_CORS_ALLOW_HEADERS = "Content-Type, Authorization,
43
43
  * Note the deliberate asymmetry with {@link DEFAULT_CORS_ALLOW_HEADERS}:
44
44
  * inbound the id may arrive as `X-Trace-Id`, outbound it is always
45
45
  * `X-Request-Id` — one name on the wire out, no alias.
46
+ *
47
+ * `X-Build-Id` is the release a handler configured with a release marker
48
+ * names on every response; a cross-origin page reads it to learn it is stale
49
+ * (`stitchkit/release`).
46
50
  */
47
- export declare const DEFAULT_CORS_EXPOSE_HEADERS = "Content-Disposition, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, X-Request-Id";
51
+ export declare const DEFAULT_CORS_EXPOSE_HEADERS = "Content-Disposition, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, X-Request-Id, X-Build-Id";
48
52
  /**
49
53
  * Reject an unsafe CORS config at construction. `credentials: true` with a
50
54
  * wildcard origin would reflect *any* caller's `Origin` with
@@ -52,6 +56,17 @@ export declare const DEFAULT_CORS_EXPOSE_HEADERS = "Content-Disposition, Content
52
56
  * requests. Credentials require an explicit origin allow-list.
53
57
  */
54
58
  export declare function assertCorsConfig(config: CorsConfig): void;
59
+ /**
60
+ * Is `requestOrigin` on the server's **explicit** allow-list? The question a
61
+ * safelisted request body asks before it is read (→ ADR 0165), and it is
62
+ * deliberately stricter than `resolveOrigin`: a wildcard is not an allow-list
63
+ * (a simple request is sent with cookies from any site, so `'*'` would admit
64
+ * every site), `null` is what a sandboxed iframe, a cross-origin redirect and
65
+ * `file://` send and names no site, and an absent header is no answer at all.
66
+ * A string origin is compared, not echoed. Case-insensitive, like
67
+ * `resolveOrigin`'s list branch.
68
+ */
69
+ export declare function isOriginAllowed(config: CorsConfig | undefined, requestOrigin: string | null | undefined): boolean;
55
70
  export declare function corsHeaders(config: CorsConfig, requestOrigin?: string | null): Record<string, string>;
56
71
  export declare function corsPreflightResponse(config: CorsConfig, req: Request): Response;
57
72
  //# sourceMappingURL=cors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/cors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,qEAC6B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,2BAA2B,yGACgE,CAAC;AAEzG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAoBzD;AAwBD,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BxB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,QAAQ,CAKhF"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/cors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,qEAC6B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,2BAA2B,qHAC4E,CAAC;AAErH;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAoBzD;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,OAAO,CAMT;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BxB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,QAAQ,CAKhF"}