@cybernetyx1/atlasflow-runtime 0.1.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.
@@ -0,0 +1,1161 @@
1
+ // src/url-safety.ts
2
+ function isUnsafeNetworkHost(hostname) {
3
+ const host = normalizeNetworkHost(hostname);
4
+ if (!host || host === "localhost" || host.endsWith(".localhost")) return true;
5
+ const ipv4 = parseIpv4(host);
6
+ if (ipv4) return isUnsafeIpv4(ipv4);
7
+ if (host.includes(":")) return isUnsafeIpv6(host);
8
+ return false;
9
+ }
10
+ function normalizeNetworkHost(hostname) {
11
+ return hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
12
+ }
13
+ function isNetworkAddressLiteral(hostname) {
14
+ const host = normalizeNetworkHost(hostname);
15
+ return parseIpv4(host) !== null || host.includes(":");
16
+ }
17
+ function parseIpv4(host) {
18
+ const parts = host.split(".");
19
+ if (parts.length !== 4) return null;
20
+ const bytes = parts.map((part) => /^(0|[1-9]\d{0,2})$/.test(part) ? Number(part) : NaN);
21
+ return bytes.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255) ? bytes : null;
22
+ }
23
+ function isUnsafeIpv4(bytes) {
24
+ const a = bytes[0] ?? -1;
25
+ const b = bytes[1] ?? -1;
26
+ if (a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 192 && b === 168) return true;
27
+ if (a === 172 && b >= 16 && b <= 31) return true;
28
+ if (a === 100 && b >= 64 && b <= 127) return true;
29
+ if (a === 192 && b === 0) return true;
30
+ return false;
31
+ }
32
+ function isUnsafeIpv6(host) {
33
+ if (host === "::" || host === "::1" || /^0(?::0){7}$/.test(host) || /^0(?::0){6}:1$/.test(host)) return true;
34
+ const mapped = /(?:^|:)ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
35
+ if (mapped) {
36
+ const ipv4 = parseIpv4(mapped[1]);
37
+ if (ipv4) return isUnsafeIpv4(ipv4);
38
+ }
39
+ const first = host.split(":").find((part) => part.length > 0) ?? "";
40
+ const value = Number.parseInt(first, 16);
41
+ if (!Number.isFinite(value)) return false;
42
+ return (value & 65024) === 64512 || (value & 65472) === 65152;
43
+ }
44
+
45
+ // src/store.ts
46
+ var InMemorySessionStore = class {
47
+ #data = /* @__PURE__ */ new Map();
48
+ async get(key) {
49
+ const d = this.#data.get(key);
50
+ return d ? structuredClone(d) : null;
51
+ }
52
+ async put(key, data) {
53
+ this.#data.set(key, structuredClone(data));
54
+ }
55
+ async delete(key) {
56
+ this.#data.delete(key);
57
+ }
58
+ };
59
+ var MAX_RUNS = 1e3;
60
+ var MAX_EVENTS_PER_RUN = 2e3;
61
+ var InMemoryRunStore = class {
62
+ #runs = /* @__PURE__ */ new Map();
63
+ #events = /* @__PURE__ */ new Map();
64
+ async create(record) {
65
+ this.#runs.set(record.runId, { ...record });
66
+ if (!this.#events.has(record.runId)) this.#events.set(record.runId, []);
67
+ if (this.#runs.size > MAX_RUNS) {
68
+ const oldest = this.#runs.keys().next().value;
69
+ if (oldest !== void 0) {
70
+ this.#runs.delete(oldest);
71
+ this.#events.delete(oldest);
72
+ }
73
+ }
74
+ }
75
+ async update(runId, patch) {
76
+ const existing = this.#runs.get(runId);
77
+ if (existing) this.#runs.set(runId, { ...existing, ...patch });
78
+ }
79
+ async get(runId) {
80
+ const r = this.#runs.get(runId);
81
+ return r ? { ...r } : null;
82
+ }
83
+ async list(filter) {
84
+ let items = [...this.#runs.values()].sort((a, b) => b.startedAt - a.startedAt);
85
+ if (filter?.status) items = items.filter((r) => r.status === filter.status);
86
+ if (filter?.agent) items = items.filter((r) => r.agent === filter.agent);
87
+ if (filter?.limit) items = items.slice(0, filter.limit);
88
+ return items.map((r) => ({ ...r }));
89
+ }
90
+ async claimLease(runId, owner, now, ttlMs) {
91
+ const existing = this.#runs.get(runId);
92
+ if (!existing || existing.status !== "running") return false;
93
+ if (existing.leaseOwner && existing.leaseOwner !== owner && (existing.leaseExpiresAt ?? 0) > now) return false;
94
+ this.#runs.set(runId, { ...existing, leaseOwner: owner, leaseAcquiredAt: now, leaseExpiresAt: now + ttlMs });
95
+ return true;
96
+ }
97
+ async claimQueued(runId, owner, now, ttlMs) {
98
+ const existing = this.#runs.get(runId);
99
+ if (!existing || existing.status !== "queued") return false;
100
+ this.#runs.set(runId, { ...existing, status: "running", leaseOwner: owner, leaseAcquiredAt: now, leaseExpiresAt: now + ttlMs });
101
+ return true;
102
+ }
103
+ async heartbeatLease(runId, owner, now, ttlMs) {
104
+ const existing = this.#runs.get(runId);
105
+ if (!existing || existing.status !== "running" || existing.leaseOwner !== owner) return false;
106
+ this.#runs.set(runId, { ...existing, leaseExpiresAt: now + ttlMs });
107
+ return true;
108
+ }
109
+ async releaseLease(runId, owner) {
110
+ const existing = this.#runs.get(runId);
111
+ if (!existing || existing.leaseOwner !== owner) return;
112
+ const { leaseOwner: _owner, leaseExpiresAt: _expires, leaseAcquiredAt: _acquired, ...rest } = existing;
113
+ this.#runs.set(runId, rest);
114
+ }
115
+ async appendEvent(runId, event) {
116
+ const log = this.#events.get(runId);
117
+ if (!log) return;
118
+ if (log.length < MAX_EVENTS_PER_RUN) log.push(event);
119
+ }
120
+ async events(runId) {
121
+ return [...this.#events.get(runId) ?? []];
122
+ }
123
+ async eventCount(runId) {
124
+ return this.#events.get(runId)?.length ?? 0;
125
+ }
126
+ };
127
+ var MAX_STREAMS = 1e3;
128
+ var MAX_STREAM_MESSAGES_PER_STREAM = 5e3;
129
+ var PRODUCER_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
130
+ var InMemoryStreamStore = class {
131
+ #streams = /* @__PURE__ */ new Map();
132
+ async create(path, opts = {}) {
133
+ const existing = this.#streams.get(path);
134
+ if (existing) {
135
+ const contentType2 = opts.contentType === void 0 ? existing.record.contentType : normalizeStreamContentType(opts.contentType);
136
+ if (normalizeStreamContentType(existing.record.contentType) !== contentType2 || opts.ttlSeconds !== existing.record.ttlSeconds || opts.expiresAt !== existing.record.expiresAt || (opts.initialData?.length ?? 0) > 0) {
137
+ throw new Error("stream already exists with different configuration");
138
+ }
139
+ return { record: cloneStreamRecord(existing.record), created: false };
140
+ }
141
+ let contentType = normalizeStreamContentType(opts.contentType);
142
+ let forkOffset = "-1";
143
+ let inheritedMessages = [];
144
+ if (opts.fork) {
145
+ const source = this.#streams.get(opts.fork.path);
146
+ if (!source) throw new Error("fork source not found");
147
+ contentType = resolveForkContentType(source.record, opts.contentType);
148
+ forkOffset = resolveForkOffset(source.record, opts.fork.offset);
149
+ const forkIndex = parseStreamMessageIndex(forkOffset);
150
+ inheritedMessages = source.messages.filter((message) => message.index <= forkIndex).map(cloneStreamMessage);
151
+ }
152
+ const now = Date.now();
153
+ const record = {
154
+ path,
155
+ contentType,
156
+ closed: opts.closed === true,
157
+ currentOffset: forkOffset,
158
+ createdAt: now,
159
+ updatedAt: now,
160
+ ...opts.ttlSeconds !== void 0 ? { ttlSeconds: opts.ttlSeconds } : {},
161
+ ...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {}
162
+ };
163
+ const entry = { record, messages: inheritedMessages };
164
+ this.#streams.set(path, entry);
165
+ if (this.#streams.size > MAX_STREAMS) {
166
+ const oldest = this.#streams.keys().next().value;
167
+ if (oldest !== void 0) this.#streams.delete(oldest);
168
+ }
169
+ if ((opts.initialData?.length ?? 0) > 0) {
170
+ const normalized = normalizeStreamBody(opts.initialData, contentType, true);
171
+ if (normalized.length > 0) appendStreamMessage(entry, normalized);
172
+ }
173
+ return { record: cloneStreamRecord(entry.record), created: true };
174
+ }
175
+ async get(path) {
176
+ const entry = this.#streams.get(path);
177
+ return entry ? cloneStreamRecord(entry.record) : null;
178
+ }
179
+ async delete(path) {
180
+ return this.#streams.delete(path);
181
+ }
182
+ async append(path, data, opts) {
183
+ const entry = this.#streams.get(path);
184
+ if (!entry) return null;
185
+ assertStreamContentType(entry.record.contentType, opts.contentType);
186
+ if (entry.record.closed) throw new Error("stream is closed");
187
+ assertStreamSeq(entry.record, opts.seq);
188
+ const normalized = normalizeStreamBody(data, entry.record.contentType, false);
189
+ const message = appendStreamMessage(entry, normalized);
190
+ commitStreamSeq(entry.record, opts.seq);
191
+ entry.record.closed = opts.close === true;
192
+ entry.record.updatedAt = Date.now();
193
+ return { record: cloneStreamRecord(entry.record), message: cloneStreamMessage(message) };
194
+ }
195
+ async appendWithProducer(path, data, opts) {
196
+ const entry = this.#streams.get(path);
197
+ if (!entry) return null;
198
+ if (entry.record.closed) {
199
+ return closedStreamProducerAppend(entry.record, opts.producer);
200
+ }
201
+ assertStreamContentType(entry.record.contentType, opts.contentType);
202
+ const producerResult = validateStreamProducer(entry.record, opts.producer);
203
+ if (producerResult.status !== "accepted") {
204
+ return { record: cloneStreamRecord(entry.record), message: null, producerResult };
205
+ }
206
+ assertStreamSeq(entry.record, opts.seq);
207
+ const normalized = normalizeStreamBody(data, entry.record.contentType, false);
208
+ const message = appendStreamMessage(entry, normalized);
209
+ commitStreamSeq(entry.record, opts.seq);
210
+ commitStreamProducer(entry.record, producerResult);
211
+ if (opts.close) {
212
+ entry.record.closed = true;
213
+ entry.record.closedBy = { ...opts.producer };
214
+ entry.record.updatedAt = Date.now();
215
+ }
216
+ return {
217
+ record: cloneStreamRecord(entry.record),
218
+ message: cloneStreamMessage(message),
219
+ producerResult,
220
+ streamClosed: opts.close === true
221
+ };
222
+ }
223
+ async close(path) {
224
+ const entry = this.#streams.get(path);
225
+ if (!entry) return null;
226
+ entry.record.closed = true;
227
+ entry.record.updatedAt = Date.now();
228
+ return cloneStreamRecord(entry.record);
229
+ }
230
+ async closeWithProducer(path, producer) {
231
+ const entry = this.#streams.get(path);
232
+ if (!entry) return null;
233
+ if (entry.record.closed) {
234
+ if (producerMatchesClosedBy(entry.record, producer)) {
235
+ return { record: cloneStreamRecord(entry.record), alreadyClosed: true, producerResult: { status: "duplicate", lastSeq: producer.seq } };
236
+ }
237
+ return { record: cloneStreamRecord(entry.record), alreadyClosed: true, producerResult: { status: "stream_closed" } };
238
+ }
239
+ const producerResult = validateStreamProducer(entry.record, producer);
240
+ if (producerResult.status !== "accepted") {
241
+ return { record: cloneStreamRecord(entry.record), alreadyClosed: false, producerResult };
242
+ }
243
+ commitStreamProducer(entry.record, producerResult);
244
+ entry.record.closed = true;
245
+ entry.record.closedBy = { ...producer };
246
+ entry.record.updatedAt = Date.now();
247
+ return { record: cloneStreamRecord(entry.record), alreadyClosed: false, producerResult };
248
+ }
249
+ async messages(path) {
250
+ return (this.#streams.get(path)?.messages ?? []).map(cloneStreamMessage);
251
+ }
252
+ async list() {
253
+ return [...this.#streams.keys()];
254
+ }
255
+ };
256
+ var DurableStreamSubscriptionStoreBase = class {
257
+ constructor(streams, deliveryFetch = fetch, webhookOptions = {}) {
258
+ this.streams = streams;
259
+ this.deliveryFetch = deliveryFetch;
260
+ this.webhookOptions = webhookOptions;
261
+ }
262
+ streams;
263
+ deliveryFetch;
264
+ webhookOptions;
265
+ async createOrConfirm(id, input) {
266
+ const existing = await this.load(id);
267
+ const configHash = stableSubscriptionConfigHash(input);
268
+ if (existing) {
269
+ if (existing.configHash !== configHash) {
270
+ return subscriptionError(409, "SUBSCRIPTION_ALREADY_EXISTS", "Subscription already exists with different configuration");
271
+ }
272
+ return { ok: true, value: { subscription: cloneSubscriptionRecord(existing), created: false } };
273
+ }
274
+ if (input.type === "webhook") {
275
+ const urlValidation = await validateWebhookUrl(input.webhookUrl, this.webhookOptions);
276
+ if (!urlValidation.ok) return subscriptionError(400, "INVALID_REQUEST", urlValidation.message);
277
+ const headerValidation = validateWebhookHeaders(input.webhookHeaders, this.webhookOptions);
278
+ if (!headerValidation.ok) return subscriptionError(400, "INVALID_REQUEST", headerValidation.message);
279
+ }
280
+ const record = {
281
+ id,
282
+ type: input.type,
283
+ streams: [],
284
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
285
+ status: "active",
286
+ configHash,
287
+ generation: 0,
288
+ ...input.wakeStream ? { wakeStream: input.wakeStream } : {},
289
+ ...input.leaseTtlMs !== void 0 ? { leaseTtlMs: input.leaseTtlMs } : {},
290
+ ...input.webhookUrl ? { webhookUrl: input.webhookUrl } : {},
291
+ ...input.webhookHeaders ? { webhookHeaders: { ...input.webhookHeaders } } : {},
292
+ ...input.pattern ? { pattern: input.pattern } : {},
293
+ ...input.description ? { description: input.description } : {}
294
+ };
295
+ for (const streamPath of input.streams) {
296
+ await this.linkStream(record, streamPath, "explicit", await this.tailOffset(streamPath));
297
+ }
298
+ if (input.pattern) {
299
+ for (const streamPath of await this.listStreamPaths()) {
300
+ if (globMatches(input.pattern, streamPath)) {
301
+ await this.linkStream(record, streamPath, "glob", await this.tailOffset(streamPath));
302
+ }
303
+ }
304
+ }
305
+ await this.save(record);
306
+ return { ok: true, value: { subscription: cloneSubscriptionRecord(record), created: true } };
307
+ }
308
+ async get(id) {
309
+ const record = await this.load(id);
310
+ return record ? cloneSubscriptionRecord(record) : null;
311
+ }
312
+ async delete(id) {
313
+ return this.remove(id);
314
+ }
315
+ async addExplicitStreams(id, streams) {
316
+ const record = await this.load(id);
317
+ if (!record) return false;
318
+ for (const streamPath of streams) {
319
+ await this.linkStream(record, streamPath, "explicit", await this.tailOffset(streamPath));
320
+ }
321
+ await this.save(record);
322
+ return true;
323
+ }
324
+ async removeExplicitStream(id, streamPath) {
325
+ const record = await this.load(id);
326
+ if (!record) return false;
327
+ const normalized = normalizeSubscriptionPath(streamPath);
328
+ const link = record.streams.find((item) => item.path === normalized);
329
+ if (!link) return true;
330
+ link.linkTypes = link.linkTypes.filter((type) => type !== "explicit");
331
+ if (link.linkTypes.length === 0) record.streams = record.streams.filter((item) => item !== link);
332
+ await this.save(record);
333
+ return true;
334
+ }
335
+ async notifyStreamAppend(streamPath, message) {
336
+ const normalized = normalizeSubscriptionPath(streamPath);
337
+ for (const record of await this.listRecords()) {
338
+ if (record.pattern && globMatches(record.pattern, normalized)) {
339
+ const existing = record.streams.find((stream) => stream.path === normalized);
340
+ await this.linkStream(record, normalized, "glob", existing?.ackedOffset ?? "-1");
341
+ }
342
+ if (record.streams.some((stream) => stream.path === normalized)) {
343
+ if (record.type === "webhook") await this.deliverWebhookPending(record, normalized, message);
344
+ else await this.createWakeIfPending(record, normalized);
345
+ await this.save(record);
346
+ }
347
+ }
348
+ }
349
+ async notifyStreamDelete(streamPath) {
350
+ const normalized = normalizeSubscriptionPath(streamPath);
351
+ for (const record of await this.listRecords()) {
352
+ if (record.type === "webhook" && record.streams.some((stream) => stream.path === normalized)) {
353
+ await this.deliverWebhookDelete(record, normalized);
354
+ }
355
+ const next = record.streams.filter((stream) => stream.path !== normalized);
356
+ if (next.length !== record.streams.length) {
357
+ record.streams = next;
358
+ await this.save(record);
359
+ }
360
+ }
361
+ }
362
+ async claim(id, worker) {
363
+ const record = await this.load(id);
364
+ if (!record) return subscriptionError(404, "SUBSCRIPTION_NOT_FOUND", "Subscription not found");
365
+ if (record.type !== "pull-wake") return subscriptionError(400, "INVALID_REQUEST", "Only pull-wake subscriptions can be claimed");
366
+ this.clearExpiredLease(record);
367
+ if (record.holder) {
368
+ return subscriptionError(409, "ALREADY_CLAIMED", "Subscription already has a live claimant", {
369
+ currentHolder: record.holder,
370
+ generation: record.generation
371
+ });
372
+ }
373
+ if (!await this.hasPendingWork(record)) {
374
+ await this.save(record);
375
+ return subscriptionError(409, "NO_PENDING_WORK", "Subscription has no pending work");
376
+ }
377
+ if (!record.wakeId) await this.createWake(record, await this.firstPendingStream(record));
378
+ record.holder = worker;
379
+ record.token = randomSubscriptionToken("subtok");
380
+ record.leaseExpiresAt = Date.now() + pullWakeLeaseTtl(record);
381
+ await this.save(record);
382
+ return {
383
+ ok: true,
384
+ value: {
385
+ wakeId: record.wakeId,
386
+ generation: record.generation,
387
+ token: record.token,
388
+ streams: await this.streamInfos(record),
389
+ leaseTtlMs: pullWakeLeaseTtl(record)
390
+ }
391
+ };
392
+ }
393
+ async ack(id, token, input) {
394
+ const record = await this.load(id);
395
+ if (!record) return subscriptionError(404, "SUBSCRIPTION_NOT_FOUND", "Subscription not found");
396
+ const fenced = this.validateWakeToken(record, token, input);
397
+ if (fenced) return fenced;
398
+ const ackError = await this.applyAcks(record, input);
399
+ if (ackError) return ackError;
400
+ record.leaseExpiresAt = Date.now() + pullWakeLeaseTtl(record);
401
+ let nextWake = false;
402
+ if (input.done === true) {
403
+ this.clearLease(record);
404
+ nextWake = await this.createWakeIfPending(record, await this.firstPendingStream(record));
405
+ }
406
+ await this.save(record);
407
+ return { ok: true, value: { ok: true, nextWake } };
408
+ }
409
+ async release(id, token, input) {
410
+ const record = await this.load(id);
411
+ if (!record) return subscriptionError(404, "SUBSCRIPTION_NOT_FOUND", "Subscription not found");
412
+ const fenced = this.validateWakeToken(record, token, input);
413
+ if (fenced) return fenced;
414
+ this.clearLease(record);
415
+ await this.createWakeIfPending(record, await this.firstPendingStream(record));
416
+ await this.save(record);
417
+ return { ok: true, value: void 0 };
418
+ }
419
+ async streamInfos(subscription) {
420
+ const infos = [];
421
+ for (const link of subscription.streams) {
422
+ const tailOffset = await this.tailOffset(link.path);
423
+ infos.push({
424
+ path: link.path,
425
+ linkType: link.linkTypes.includes("explicit") ? "explicit" : "glob",
426
+ ackedOffset: link.ackedOffset,
427
+ tailOffset,
428
+ hasPending: compareSubscriptionOffsets(tailOffset, link.ackedOffset) > 0
429
+ });
430
+ }
431
+ return infos;
432
+ }
433
+ async linkStream(record, streamPath, linkType, ackedOffset) {
434
+ const normalized = normalizeSubscriptionPath(streamPath);
435
+ const existing = record.streams.find((stream) => stream.path === normalized);
436
+ if (existing) {
437
+ if (!existing.linkTypes.includes(linkType)) existing.linkTypes.push(linkType);
438
+ return;
439
+ }
440
+ record.streams.push({ path: normalized, linkTypes: [linkType], ackedOffset });
441
+ }
442
+ async createWakeIfPending(record, triggeredBy) {
443
+ this.clearExpiredLease(record);
444
+ if (record.wakeId || record.holder) return false;
445
+ if (!await this.hasPendingWork(record)) return false;
446
+ await this.createWake(record, triggeredBy || await this.firstPendingStream(record));
447
+ return true;
448
+ }
449
+ async createWake(record, triggeredBy) {
450
+ record.generation += 1;
451
+ record.wakeId = randomSubscriptionToken("wake");
452
+ record.wakeSnapshot = Object.fromEntries((await this.streamInfos(record)).map((stream) => [stream.path, stream.tailOffset]));
453
+ await this.writeWakeEvent(record, triggeredBy);
454
+ }
455
+ async writeWakeEvent(record, triggeredBy) {
456
+ if (!record.wakeStream) return;
457
+ const wakeStream = normalizeSubscriptionPath(record.wakeStream);
458
+ if (!await this.streams.get(wakeStream)) return;
459
+ const event = {
460
+ type: "wake",
461
+ subscription_id: record.id,
462
+ stream: triggeredBy,
463
+ generation: record.generation,
464
+ ts: Date.now()
465
+ };
466
+ try {
467
+ await this.streams.append(wakeStream, new TextEncoder().encode(JSON.stringify(event)), { contentType: "application/json" });
468
+ } catch {
469
+ }
470
+ }
471
+ async hasPendingWork(record) {
472
+ return (await this.streamInfos(record)).some((stream) => stream.hasPending);
473
+ }
474
+ async firstPendingStream(record) {
475
+ return (await this.streamInfos(record)).find((stream) => stream.hasPending)?.path ?? record.streams[0]?.path ?? "";
476
+ }
477
+ validateWakeToken(record, token, input) {
478
+ this.clearExpiredLease(record);
479
+ if (!record.token || token !== record.token) return subscriptionError(401, "TOKEN_INVALID", "Token invalid");
480
+ if (input.generation !== record.generation || input.wakeId !== record.wakeId) {
481
+ return subscriptionError(409, "FENCED", "Wake generation is stale");
482
+ }
483
+ return null;
484
+ }
485
+ async applyAcks(record, input) {
486
+ if (!input.acks) return null;
487
+ for (const ack of input.acks) {
488
+ const stream = normalizeSubscriptionPath(ack.stream ?? ack.path ?? "");
489
+ const link = record.streams.find((item) => item.path === stream);
490
+ if (!stream || !link) return subscriptionError(409, "INVALID_OFFSET", "Ack references an unknown subscription stream");
491
+ if (ack.offset === "-1") return subscriptionError(409, "INVALID_OFFSET", "Ack offset must not be -1");
492
+ if (compareSubscriptionOffsets(ack.offset, link.ackedOffset) < 0) {
493
+ return subscriptionError(409, "INVALID_OFFSET", "Ack offset regresses the committed cursor");
494
+ }
495
+ if (compareSubscriptionOffsets(ack.offset, await this.tailOffset(stream)) > 0) {
496
+ return subscriptionError(409, "INVALID_OFFSET", "Ack offset is beyond stream tail");
497
+ }
498
+ }
499
+ for (const ack of input.acks) {
500
+ const stream = normalizeSubscriptionPath(ack.stream ?? ack.path ?? "");
501
+ const link = record.streams.find((item) => item.path === stream);
502
+ if (link) link.ackedOffset = ack.offset;
503
+ }
504
+ return null;
505
+ }
506
+ clearLease(record) {
507
+ delete record.holder;
508
+ delete record.token;
509
+ delete record.wakeId;
510
+ delete record.wakeSnapshot;
511
+ delete record.leaseExpiresAt;
512
+ }
513
+ clearExpiredLease(record) {
514
+ if (record.leaseExpiresAt !== void 0 && record.leaseExpiresAt <= Date.now()) this.clearLease(record);
515
+ }
516
+ async listStreamPaths() {
517
+ if (!("list" in this.streams) || typeof this.streams.list !== "function") return [];
518
+ return this.streams.list();
519
+ }
520
+ async tailOffset(streamPath) {
521
+ return (await this.streams.get(normalizeSubscriptionPath(streamPath)))?.currentOffset ?? "-1";
522
+ }
523
+ async deliverWebhookPending(record, triggeredBy, message) {
524
+ if (!record.webhookUrl) return;
525
+ const links = record.streams.filter((stream) => stream.path === normalizeSubscriptionPath(triggeredBy));
526
+ for (const link of links) {
527
+ const messages = (await this.streams.messages(link.path)).filter((item) => compareSubscriptionOffsets(item.offset, link.ackedOffset) > 0);
528
+ for (const item of messages.length > 0 ? messages : message ? [message] : []) {
529
+ if (compareSubscriptionOffsets(item.offset, link.ackedOffset) <= 0) continue;
530
+ const delivered = await this.deliverWebhookAppend(record, link.path, item);
531
+ if (!delivered) return;
532
+ link.ackedOffset = item.offset;
533
+ }
534
+ }
535
+ }
536
+ async deliverWebhookAppend(record, streamPath, message) {
537
+ const stream = await this.streams.get(streamPath);
538
+ const body = webhookPayload(record, "stream.append", streamPath, {
539
+ offset: message.offset,
540
+ index: message.index,
541
+ content_type: stream?.contentType,
542
+ data_base64: message.dataBase64,
543
+ created_at: new Date(message.createdAt).toISOString()
544
+ });
545
+ return this.postWebhook(record, body);
546
+ }
547
+ async deliverWebhookDelete(record, streamPath) {
548
+ if (!record.webhookUrl) return;
549
+ await this.postWebhook(record, webhookPayload(record, "stream.delete", streamPath));
550
+ }
551
+ async postWebhook(record, payload) {
552
+ if (!record.webhookUrl) return true;
553
+ const urlValidation = await validateWebhookUrl(record.webhookUrl, this.webhookOptions);
554
+ if (!urlValidation.ok) {
555
+ record.webhookLastError = urlValidation.message;
556
+ return false;
557
+ }
558
+ try {
559
+ const res = await this.deliveryFetch(record.webhookUrl, {
560
+ method: "POST",
561
+ headers: {
562
+ "content-type": "application/json",
563
+ ...sanitizeWebhookHeaders(record.webhookHeaders, this.webhookOptions)
564
+ },
565
+ redirect: "manual",
566
+ body: JSON.stringify(payload)
567
+ });
568
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
569
+ record.webhookLastSuccessAt = (/* @__PURE__ */ new Date()).toISOString();
570
+ delete record.webhookLastError;
571
+ return true;
572
+ } catch (err) {
573
+ record.webhookLastError = err instanceof Error ? err.message : String(err);
574
+ return false;
575
+ }
576
+ }
577
+ };
578
+ var InMemorySubscriptionStore = class extends DurableStreamSubscriptionStoreBase {
579
+ #subscriptions = /* @__PURE__ */ new Map();
580
+ constructor(streams, deliveryFetch, webhookOptions) {
581
+ super(streams, deliveryFetch, webhookOptions);
582
+ }
583
+ async load(id) {
584
+ const record = this.#subscriptions.get(id);
585
+ return record ? cloneSubscriptionRecord(record) : null;
586
+ }
587
+ async save(record) {
588
+ this.#subscriptions.set(record.id, cloneSubscriptionRecord(record));
589
+ }
590
+ async remove(id) {
591
+ return this.#subscriptions.delete(id);
592
+ }
593
+ async listRecords() {
594
+ return [...this.#subscriptions.values()].map(cloneSubscriptionRecord);
595
+ }
596
+ };
597
+ function inMemoryAdapter(options = {}) {
598
+ const streams = new InMemoryStreamStore();
599
+ return {
600
+ sessions: new InMemorySessionStore(),
601
+ runs: new InMemoryRunStore(),
602
+ streams,
603
+ subscriptions: new InMemorySubscriptionStore(streams, void 0, options.webhooks)
604
+ };
605
+ }
606
+ var EVENT_KEY = (runId, index) => `e:${runId}:${String(index).padStart(12, "0")}`;
607
+ var STREAM_RECORD_KEY = (path) => `ds:r:${encodeURIComponent(path)}`;
608
+ var STREAM_MESSAGE_PREFIX = (path) => `ds:m:${encodeURIComponent(path)}:`;
609
+ var STREAM_MESSAGE_KEY = (path, index) => `${STREAM_MESSAGE_PREFIX(path)}${String(index).padStart(12, "0")}`;
610
+ var SUBSCRIPTION_RECORD_KEY = (id) => `ds:sub:${encodeURIComponent(id)}`;
611
+ var SUBSCRIPTION_RECORD_PREFIX = "ds:sub:";
612
+ function kvAdapter(kv, options = {}) {
613
+ const streams = {
614
+ async create(path, opts = {}) {
615
+ const existing = await kv.get(STREAM_RECORD_KEY(path));
616
+ if (existing) {
617
+ const contentType2 = opts.contentType === void 0 ? existing.contentType : normalizeStreamContentType(opts.contentType);
618
+ if (normalizeStreamContentType(existing.contentType) !== contentType2 || opts.ttlSeconds !== existing.ttlSeconds || opts.expiresAt !== existing.expiresAt || (opts.initialData?.length ?? 0) > 0) {
619
+ throw new Error("stream already exists with different configuration");
620
+ }
621
+ return { record: existing, created: false };
622
+ }
623
+ let contentType = normalizeStreamContentType(opts.contentType);
624
+ let currentOffset = "-1";
625
+ let inheritedMessages = [];
626
+ if (opts.fork) {
627
+ const source = await kv.get(STREAM_RECORD_KEY(opts.fork.path));
628
+ if (!source) throw new Error("fork source not found");
629
+ contentType = resolveForkContentType(source, opts.contentType);
630
+ currentOffset = resolveForkOffset(source, opts.fork.offset);
631
+ const forkIndex = parseStreamMessageIndex(currentOffset);
632
+ const sourceMessages = await kv.list({ prefix: STREAM_MESSAGE_PREFIX(opts.fork.path) });
633
+ inheritedMessages = [...sourceMessages.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, value]) => value).filter((message) => message.index <= forkIndex).map(cloneStreamMessage);
634
+ }
635
+ const now = Date.now();
636
+ let record = {
637
+ path,
638
+ contentType,
639
+ closed: opts.closed === true,
640
+ currentOffset,
641
+ createdAt: now,
642
+ updatedAt: now,
643
+ ...opts.ttlSeconds !== void 0 ? { ttlSeconds: opts.ttlSeconds } : {},
644
+ ...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {}
645
+ };
646
+ await kv.put(STREAM_RECORD_KEY(path), record);
647
+ for (const message of inheritedMessages) {
648
+ await kv.put(STREAM_MESSAGE_KEY(path, message.index), message);
649
+ }
650
+ if ((opts.initialData?.length ?? 0) > 0) {
651
+ const normalized = normalizeStreamBody(opts.initialData, contentType, true);
652
+ if (normalized.length > 0) {
653
+ const message = makeStreamMessage(parseStreamMessageIndex(record.currentOffset) + 1, normalized);
654
+ record = { ...record, currentOffset: message.offset, updatedAt: message.createdAt };
655
+ await kv.put(STREAM_MESSAGE_KEY(path, message.index), message);
656
+ await kv.put(STREAM_RECORD_KEY(path), record);
657
+ }
658
+ }
659
+ return { record, created: true };
660
+ },
661
+ async get(path) {
662
+ return await kv.get(STREAM_RECORD_KEY(path)) ?? null;
663
+ },
664
+ async delete(path) {
665
+ const existing = await kv.get(STREAM_RECORD_KEY(path));
666
+ if (!existing) return false;
667
+ await kv.delete(STREAM_RECORD_KEY(path));
668
+ const messages = await kv.list({ prefix: STREAM_MESSAGE_PREFIX(path) });
669
+ await Promise.all([...messages.keys()].map((key) => kv.delete(key)));
670
+ return true;
671
+ },
672
+ async append(path, data, opts) {
673
+ const existing = await kv.get(STREAM_RECORD_KEY(path));
674
+ if (!existing) return null;
675
+ assertStreamContentType(existing.contentType, opts.contentType);
676
+ if (existing.closed) throw new Error("stream is closed");
677
+ assertStreamSeq(existing, opts.seq);
678
+ const normalized = normalizeStreamBody(data, existing.contentType, false);
679
+ const currentIndex = parseStreamMessageIndex(existing.currentOffset);
680
+ const message = makeStreamMessage(currentIndex + 1, normalized);
681
+ const record = {
682
+ ...existing,
683
+ currentOffset: message.offset,
684
+ ...opts.seq !== void 0 ? { lastSeq: opts.seq } : {},
685
+ closed: opts.close === true,
686
+ updatedAt: message.createdAt
687
+ };
688
+ await kv.put(STREAM_MESSAGE_KEY(path, message.index), message);
689
+ await kv.put(STREAM_RECORD_KEY(path), record);
690
+ return { record, message };
691
+ },
692
+ async appendWithProducer(path, data, opts) {
693
+ const existing = await kv.get(STREAM_RECORD_KEY(path));
694
+ if (!existing) return null;
695
+ if (existing.closed) return closedStreamProducerAppend(existing, opts.producer);
696
+ assertStreamContentType(existing.contentType, opts.contentType);
697
+ const producerResult = validateStreamProducer(existing, opts.producer);
698
+ if (producerResult.status !== "accepted") return { record: existing, message: null, producerResult };
699
+ assertStreamSeq(existing, opts.seq);
700
+ const normalized = normalizeStreamBody(data, existing.contentType, false);
701
+ const currentIndex = parseStreamMessageIndex(existing.currentOffset);
702
+ const message = makeStreamMessage(currentIndex + 1, normalized);
703
+ const record = {
704
+ ...existing,
705
+ currentOffset: message.offset,
706
+ ...opts.seq !== void 0 ? { lastSeq: opts.seq } : {},
707
+ closed: opts.close === true,
708
+ updatedAt: message.createdAt
709
+ };
710
+ commitStreamProducer(record, producerResult);
711
+ if (opts.close) record.closedBy = { ...opts.producer };
712
+ await kv.put(STREAM_MESSAGE_KEY(path, message.index), message);
713
+ await kv.put(STREAM_RECORD_KEY(path), record);
714
+ return { record, message, producerResult, streamClosed: opts.close === true };
715
+ },
716
+ async close(path) {
717
+ const existing = await kv.get(STREAM_RECORD_KEY(path));
718
+ if (!existing) return null;
719
+ const record = { ...existing, closed: true, updatedAt: Date.now() };
720
+ await kv.put(STREAM_RECORD_KEY(path), record);
721
+ return record;
722
+ },
723
+ async closeWithProducer(path, producer) {
724
+ const existing = await kv.get(STREAM_RECORD_KEY(path));
725
+ if (!existing) return null;
726
+ if (existing.closed) {
727
+ if (producerMatchesClosedBy(existing, producer)) {
728
+ return { record: existing, alreadyClosed: true, producerResult: { status: "duplicate", lastSeq: producer.seq } };
729
+ }
730
+ return { record: existing, alreadyClosed: true, producerResult: { status: "stream_closed" } };
731
+ }
732
+ const producerResult = validateStreamProducer(existing, producer);
733
+ if (producerResult.status !== "accepted") return { record: existing, alreadyClosed: false, producerResult };
734
+ const record = { ...existing, closed: true, closedBy: { ...producer }, updatedAt: Date.now() };
735
+ commitStreamProducer(record, producerResult);
736
+ await kv.put(STREAM_RECORD_KEY(path), record);
737
+ return { record, alreadyClosed: false, producerResult };
738
+ },
739
+ async messages(path) {
740
+ const map = await kv.list({ prefix: STREAM_MESSAGE_PREFIX(path) });
741
+ return [...map.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, v]) => v);
742
+ },
743
+ async list() {
744
+ const map = await kv.list({ prefix: "ds:r:" });
745
+ return [...map.values()].map((record) => record.path);
746
+ }
747
+ };
748
+ return {
749
+ sessions: {
750
+ async get(key) {
751
+ return await kv.get(`s:${key}`) ?? null;
752
+ },
753
+ async put(key, data) {
754
+ await kv.put(`s:${key}`, data);
755
+ },
756
+ async delete(key) {
757
+ await kv.delete(`s:${key}`);
758
+ }
759
+ },
760
+ runs: {
761
+ async create(record) {
762
+ await kv.put(`r:${record.runId}`, record);
763
+ },
764
+ async update(runId, patch) {
765
+ const existing = await kv.get(`r:${runId}`);
766
+ if (existing) await kv.put(`r:${runId}`, { ...existing, ...patch });
767
+ },
768
+ async get(runId) {
769
+ return await kv.get(`r:${runId}`) ?? null;
770
+ },
771
+ async list(filter) {
772
+ const map = await kv.list({ prefix: "r:" });
773
+ let items = [...map.values()].sort((a, b) => b.startedAt - a.startedAt);
774
+ if (filter?.status) items = items.filter((r) => r.status === filter.status);
775
+ if (filter?.agent) items = items.filter((r) => r.agent === filter.agent);
776
+ if (filter?.limit) items = items.slice(0, filter.limit);
777
+ return items;
778
+ },
779
+ async claimLease(runId, owner, now, ttlMs) {
780
+ const existing = await kv.get(`r:${runId}`);
781
+ if (!existing || existing.status !== "running") return false;
782
+ if (existing.leaseOwner && existing.leaseOwner !== owner && (existing.leaseExpiresAt ?? 0) > now) return false;
783
+ await kv.put(`r:${runId}`, { ...existing, leaseOwner: owner, leaseAcquiredAt: now, leaseExpiresAt: now + ttlMs });
784
+ return true;
785
+ },
786
+ async claimQueued(runId, owner, now, ttlMs) {
787
+ const existing = await kv.get(`r:${runId}`);
788
+ if (!existing || existing.status !== "queued") return false;
789
+ await kv.put(`r:${runId}`, { ...existing, status: "running", leaseOwner: owner, leaseAcquiredAt: now, leaseExpiresAt: now + ttlMs });
790
+ return true;
791
+ },
792
+ async heartbeatLease(runId, owner, now, ttlMs) {
793
+ const existing = await kv.get(`r:${runId}`);
794
+ if (!existing || existing.status !== "running" || existing.leaseOwner !== owner) return false;
795
+ await kv.put(`r:${runId}`, { ...existing, leaseExpiresAt: now + ttlMs });
796
+ return true;
797
+ },
798
+ async releaseLease(runId, owner) {
799
+ const existing = await kv.get(`r:${runId}`);
800
+ if (!existing || existing.leaseOwner !== owner) return;
801
+ const { leaseOwner: _owner, leaseExpiresAt: _expires, leaseAcquiredAt: _acquired, ...rest } = existing;
802
+ await kv.put(`r:${runId}`, rest);
803
+ },
804
+ async appendEvent(runId, event) {
805
+ await kv.put(EVENT_KEY(runId, event.index), event);
806
+ },
807
+ async events(runId) {
808
+ const map = await kv.list({ prefix: `e:${runId}:` });
809
+ return [...map.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, v]) => v);
810
+ },
811
+ async eventCount(runId) {
812
+ const map = await kv.list({ prefix: `e:${runId}:` });
813
+ return map.size;
814
+ }
815
+ },
816
+ streams,
817
+ subscriptions: new KvSubscriptionStore(kv, streams, options.webhooks)
818
+ };
819
+ }
820
+ var KvSubscriptionStore = class extends DurableStreamSubscriptionStoreBase {
821
+ constructor(kv, streams, webhookOptions) {
822
+ super(streams, void 0, webhookOptions);
823
+ this.kv = kv;
824
+ }
825
+ kv;
826
+ async load(id) {
827
+ const record = await this.kv.get(SUBSCRIPTION_RECORD_KEY(id));
828
+ return record ? cloneSubscriptionRecord(record) : null;
829
+ }
830
+ async save(record) {
831
+ await this.kv.put(SUBSCRIPTION_RECORD_KEY(record.id), cloneSubscriptionRecord(record));
832
+ }
833
+ async remove(id) {
834
+ const existing = await this.load(id);
835
+ if (!existing) return false;
836
+ await this.kv.delete(SUBSCRIPTION_RECORD_KEY(id));
837
+ return true;
838
+ }
839
+ async listRecords() {
840
+ const map = await this.kv.list({ prefix: SUBSCRIPTION_RECORD_PREFIX });
841
+ return [...map.values()].map(cloneSubscriptionRecord);
842
+ }
843
+ };
844
+ function normalizeStreamContentType(value) {
845
+ const normalized = value?.split(";")[0]?.trim().toLowerCase();
846
+ return normalized && /^[\w.+-]+\/[\w.+-]+$/.test(normalized) ? normalized : "application/octet-stream";
847
+ }
848
+ function isJsonStreamContentType(contentType) {
849
+ return normalizeStreamContentType(contentType) === "application/json";
850
+ }
851
+ function streamMessageBytes(message) {
852
+ return base64ToBytes(message.dataBase64);
853
+ }
854
+ function appendStreamMessage(entry, data) {
855
+ if (entry.messages.length >= MAX_STREAM_MESSAGES_PER_STREAM) throw new Error("stream message limit exceeded");
856
+ const message = makeStreamMessage(entry.messages.length, data);
857
+ entry.messages.push(message);
858
+ entry.record.currentOffset = message.offset;
859
+ entry.record.updatedAt = message.createdAt;
860
+ return message;
861
+ }
862
+ function assertStreamSeq(record, seq) {
863
+ if (seq === void 0) return;
864
+ if (seq.length === 0) throw new Error("Invalid Stream-Seq");
865
+ if (record.lastSeq !== void 0 && seq <= record.lastSeq) throw new Error("Stream sequence conflict");
866
+ }
867
+ function commitStreamSeq(record, seq) {
868
+ if (seq !== void 0) record.lastSeq = seq;
869
+ }
870
+ function resolveForkContentType(source, requested) {
871
+ const sourceContentType = normalizeStreamContentType(source.contentType);
872
+ if (requested === void 0) return sourceContentType;
873
+ const requestedContentType = normalizeStreamContentType(requested);
874
+ if (requestedContentType !== sourceContentType) throw new Error("Content-type mismatch");
875
+ return requestedContentType;
876
+ }
877
+ function resolveForkOffset(source, requested) {
878
+ const offset = requested ?? source.currentOffset;
879
+ const index = parseStreamMessageIndex(offset);
880
+ const tail = parseStreamMessageIndex(source.currentOffset);
881
+ if (index > tail) throw new Error("Fork offset is beyond source tail");
882
+ return offset;
883
+ }
884
+ function makeStreamMessage(index, data) {
885
+ return { index, offset: formatStoreOffset(index), dataBase64: bytesToBase64(data), createdAt: Date.now() };
886
+ }
887
+ function formatStoreOffset(index) {
888
+ if (index < 0) return "-1";
889
+ return `0000000000000000_${String(index).padStart(16, "0")}`;
890
+ }
891
+ function parseStreamMessageIndex(offset) {
892
+ if (offset === "-1") return -1;
893
+ const match = /^\d+_(\d+)$/.exec(offset);
894
+ if (!match) throw new Error(`Invalid stream offset: ${offset}`);
895
+ return Number(match[1]);
896
+ }
897
+ function normalizeStreamBody(data, contentType, initialCreate) {
898
+ if (!isJsonStreamContentType(contentType)) return data;
899
+ let parsed;
900
+ try {
901
+ parsed = JSON.parse(new TextDecoder().decode(data));
902
+ } catch {
903
+ throw new Error("Invalid JSON");
904
+ }
905
+ if (Array.isArray(parsed)) {
906
+ if (parsed.length === 0) {
907
+ if (initialCreate) return new Uint8Array(0);
908
+ throw new Error("Empty arrays are not allowed");
909
+ }
910
+ return new TextEncoder().encode(JSON.stringify(parsed));
911
+ }
912
+ return new TextEncoder().encode(JSON.stringify([parsed]));
913
+ }
914
+ function assertStreamContentType(expected, actual) {
915
+ if (normalizeStreamContentType(expected) !== normalizeStreamContentType(actual)) throw new Error("Content-type mismatch");
916
+ }
917
+ function cloneStreamRecord(record) {
918
+ return {
919
+ ...record,
920
+ ...record.producers ? { producers: structuredClone(record.producers) } : {},
921
+ ...record.closedBy ? { closedBy: { ...record.closedBy } } : {}
922
+ };
923
+ }
924
+ function cloneStreamMessage(message) {
925
+ return { ...message };
926
+ }
927
+ function cloneSubscriptionRecord(record) {
928
+ return structuredClone(record);
929
+ }
930
+ function normalizeSubscriptionPath(path) {
931
+ return path.replace(/^\/+/, "").replace(/\/+$/, "").split("/").filter((part) => part.length > 0 && part !== "." && part !== "..").join("/");
932
+ }
933
+ function stableSubscriptionConfigHash(input) {
934
+ const canonical = JSON.stringify({
935
+ type: input.type,
936
+ pattern: input.pattern,
937
+ streams: [...new Set(input.streams.map(normalizeSubscriptionPath))].sort(),
938
+ wakeStream: input.wakeStream ? normalizeSubscriptionPath(input.wakeStream) : void 0,
939
+ leaseTtlMs: input.leaseTtlMs,
940
+ webhookUrl: input.webhookUrl,
941
+ webhookHeaders: input.webhookHeaders ? Object.fromEntries(Object.entries(input.webhookHeaders).sort(([a], [b]) => a.localeCompare(b))) : void 0,
942
+ description: input.description
943
+ });
944
+ let hash = 2166136261;
945
+ for (let i = 0; i < canonical.length; i++) {
946
+ hash ^= canonical.charCodeAt(i);
947
+ hash = Math.imul(hash, 16777619);
948
+ }
949
+ return (hash >>> 0).toString(16).padStart(8, "0");
950
+ }
951
+ async function validateWebhookUrl(value, options) {
952
+ if (!value) return { ok: false, message: "webhook subscriptions require a webhook_url" };
953
+ let url;
954
+ try {
955
+ url = new URL(value);
956
+ } catch {
957
+ return { ok: false, message: "webhook_url must be a valid URL" };
958
+ }
959
+ if (options.allowUnsafeWebhookUrls) return { ok: true };
960
+ if (url.protocol !== "https:") return { ok: false, message: "webhook_url must use https unless unsafe local webhooks are explicitly enabled" };
961
+ if (!webhookHostAllowed(url.hostname, options.allowedWebhookHosts)) return { ok: false, message: "webhook_url host is not in the configured allowlist" };
962
+ if (isUnsafeNetworkHost(url.hostname)) return { ok: false, message: "webhook_url must not target localhost, private, loopback, link-local, or unspecified hosts" };
963
+ let resolved;
964
+ try {
965
+ resolved = await resolveWebhookHostAddresses(url.hostname, options);
966
+ } catch (err) {
967
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
968
+ }
969
+ const unsafe = resolved.find(isUnsafeNetworkHost);
970
+ if (unsafe) return { ok: false, message: `webhook_url hostname resolves to an unsafe address (${unsafe})` };
971
+ return { ok: true };
972
+ }
973
+ function validateWebhookHeaders(headers, options) {
974
+ if (!headers || options.allowSensitiveWebhookHeaders) return { ok: true };
975
+ const blocked = Object.keys(headers).find(isSensitiveWebhookHeader);
976
+ return blocked ? { ok: false, message: `webhook header "${blocked}" is not allowed` } : { ok: true };
977
+ }
978
+ function sanitizeWebhookHeaders(headers, options) {
979
+ if (!headers) return {};
980
+ if (options.allowSensitiveWebhookHeaders) return { ...headers };
981
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => !isSensitiveWebhookHeader(name)));
982
+ }
983
+ function isSensitiveWebhookHeader(name) {
984
+ const normalized = name.toLowerCase();
985
+ return normalized === "authorization" || normalized === "cookie" || normalized === "proxy-authorization" || normalized === "set-cookie";
986
+ }
987
+ function webhookHostAllowed(hostname, allowedHosts) {
988
+ if (!allowedHosts || allowedHosts.length === 0) return true;
989
+ const host = normalizeNetworkHost(hostname);
990
+ return allowedHosts.some((entry) => {
991
+ const allowed = normalizeNetworkHost(entry);
992
+ if (!allowed) return false;
993
+ if (allowed.startsWith("*.")) {
994
+ const suffix = allowed.slice(1);
995
+ return host.endsWith(suffix) && host.length > suffix.length;
996
+ }
997
+ return host === allowed;
998
+ });
999
+ }
1000
+ async function resolveWebhookHostAddresses(hostname, options) {
1001
+ const host = normalizeNetworkHost(hostname);
1002
+ if (isNetworkAddressLiteral(host)) return [host];
1003
+ if (options.resolveWebhookHost === false) return [];
1004
+ const resolver = options.resolveWebhookHost ?? ((name) => dohResolveWebhookHost(name, options.dnsTimeoutMs));
1005
+ let addresses;
1006
+ try {
1007
+ addresses = await resolver(host);
1008
+ } catch (err) {
1009
+ const reason = err instanceof Error ? err.message : String(err);
1010
+ throw new Error(`webhook_url hostname could not be resolved safely: ${reason}`);
1011
+ }
1012
+ const clean = [...new Set(addresses.map(normalizeNetworkHost).filter(Boolean))];
1013
+ if (clean.length === 0) throw new Error("webhook_url hostname could not be resolved safely");
1014
+ return clean;
1015
+ }
1016
+ async function dohResolveWebhookHost(hostname, timeoutMs = 1500) {
1017
+ const controller = new AbortController();
1018
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1019
+ try {
1020
+ const [a, aaaa] = await Promise.all([dohQuery(hostname, "A", controller.signal), dohQuery(hostname, "AAAA", controller.signal)]);
1021
+ return [...a, ...aaaa];
1022
+ } finally {
1023
+ clearTimeout(timer);
1024
+ }
1025
+ }
1026
+ async function dohQuery(hostname, type, signal) {
1027
+ const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname)}&type=${type}`;
1028
+ const res = await fetch(url, { headers: { accept: "application/dns-json" }, signal });
1029
+ if (!res.ok) throw new Error(`DNS ${type} lookup failed with HTTP ${res.status}`);
1030
+ const body = await res.json();
1031
+ const expected = type === "A" ? 1 : 28;
1032
+ return (body.Answer ?? []).filter((answer) => answer.type === expected && typeof answer.data === "string").map((answer) => answer.data);
1033
+ }
1034
+ function pullWakeLeaseTtl(record) {
1035
+ return record.leaseTtlMs ?? 3e4;
1036
+ }
1037
+ function webhookPayload(record, type, streamPath, extra = {}) {
1038
+ return {
1039
+ type,
1040
+ subscription_id: record.id,
1041
+ stream: streamPath,
1042
+ generation: record.generation,
1043
+ ts: Date.now(),
1044
+ ...extra
1045
+ };
1046
+ }
1047
+ function subscriptionError(status, code, message, extra = {}) {
1048
+ return { ok: false, status, error: { code, message, ...extra } };
1049
+ }
1050
+ function compareSubscriptionOffsets(a, b) {
1051
+ const ai = subscriptionOffsetIndex(a);
1052
+ const bi = subscriptionOffsetIndex(b);
1053
+ return ai === bi ? 0 : ai < bi ? -1 : 1;
1054
+ }
1055
+ function subscriptionOffsetIndex(offset) {
1056
+ if (offset === "-1") return -1;
1057
+ const match = /^\d+_(\d+)$/.exec(offset);
1058
+ if (!match) return Number.NEGATIVE_INFINITY;
1059
+ return Number(match[1]);
1060
+ }
1061
+ function globMatches(pattern, value) {
1062
+ const escaped = normalizeSubscriptionPath(pattern).split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1063
+ return new RegExp(`^${escaped}$`).test(value);
1064
+ }
1065
+ function randomSubscriptionToken(prefix) {
1066
+ const cryptoObj = globalThis.crypto;
1067
+ if (cryptoObj && "randomUUID" in cryptoObj && typeof cryptoObj.randomUUID === "function") {
1068
+ return `${prefix}_${cryptoObj.randomUUID()}`;
1069
+ }
1070
+ const bytes = new Uint8Array(16);
1071
+ cryptoObj?.getRandomValues?.(bytes);
1072
+ return `${prefix}_${bytesToBase64(bytes).replace(/[^A-Za-z0-9]/g, "").slice(0, 24)}`;
1073
+ }
1074
+ function bytesToBase64(bytes) {
1075
+ let binary = "";
1076
+ for (let i = 0; i < bytes.length; i += 32768) {
1077
+ binary += String.fromCharCode(...bytes.slice(i, i + 32768));
1078
+ }
1079
+ return btoa(binary);
1080
+ }
1081
+ function base64ToBytes(value) {
1082
+ const binary = atob(value);
1083
+ const bytes = new Uint8Array(binary.length);
1084
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1085
+ return bytes;
1086
+ }
1087
+ function validateStreamProducer(record, producer) {
1088
+ cleanupExpiredProducers(record);
1089
+ const state = record.producers?.[producer.producerId];
1090
+ const now = Date.now();
1091
+ if (!state) {
1092
+ if (producer.seq !== 0) return { status: "sequence_gap", expectedSeq: 0, receivedSeq: producer.seq };
1093
+ return {
1094
+ status: "accepted",
1095
+ isNew: true,
1096
+ producerId: producer.producerId,
1097
+ proposedState: { epoch: producer.epoch, lastSeq: 0, lastUpdated: now }
1098
+ };
1099
+ }
1100
+ if (producer.epoch < state.epoch) return { status: "stale_epoch", currentEpoch: state.epoch };
1101
+ if (producer.epoch > state.epoch) {
1102
+ if (producer.seq !== 0) return { status: "invalid_epoch_seq" };
1103
+ return {
1104
+ status: "accepted",
1105
+ isNew: true,
1106
+ producerId: producer.producerId,
1107
+ proposedState: { epoch: producer.epoch, lastSeq: 0, lastUpdated: now }
1108
+ };
1109
+ }
1110
+ if (producer.seq <= state.lastSeq) return { status: "duplicate", lastSeq: state.lastSeq };
1111
+ if (producer.seq === state.lastSeq + 1) {
1112
+ return {
1113
+ status: "accepted",
1114
+ isNew: false,
1115
+ producerId: producer.producerId,
1116
+ proposedState: { epoch: producer.epoch, lastSeq: producer.seq, lastUpdated: now }
1117
+ };
1118
+ }
1119
+ return { status: "sequence_gap", expectedSeq: state.lastSeq + 1, receivedSeq: producer.seq };
1120
+ }
1121
+ function commitStreamProducer(record, result) {
1122
+ if (result.status !== "accepted") return;
1123
+ record.producers = { ...record.producers ?? {}, [result.producerId]: { ...result.proposedState } };
1124
+ }
1125
+ function cleanupExpiredProducers(record) {
1126
+ if (!record.producers) return;
1127
+ const now = Date.now();
1128
+ const next = {};
1129
+ for (const [id, state] of Object.entries(record.producers)) {
1130
+ if (now - state.lastUpdated <= PRODUCER_STATE_TTL_MS) next[id] = state;
1131
+ }
1132
+ record.producers = Object.keys(next).length ? next : void 0;
1133
+ }
1134
+ function producerMatchesClosedBy(record, producer) {
1135
+ return record.closedBy?.producerId === producer.producerId && record.closedBy.epoch === producer.epoch && record.closedBy.seq === producer.seq;
1136
+ }
1137
+ function closedStreamProducerAppend(record, producer) {
1138
+ if (producerMatchesClosedBy(record, producer)) {
1139
+ return {
1140
+ record: cloneStreamRecord(record),
1141
+ message: null,
1142
+ streamClosed: true,
1143
+ producerResult: { status: "duplicate", lastSeq: producer.seq }
1144
+ };
1145
+ }
1146
+ return { record: cloneStreamRecord(record), message: null, streamClosed: true, producerResult: { status: "stream_closed" } };
1147
+ }
1148
+
1149
+ export {
1150
+ isUnsafeNetworkHost,
1151
+ InMemorySessionStore,
1152
+ InMemoryRunStore,
1153
+ InMemoryStreamStore,
1154
+ DurableStreamSubscriptionStoreBase,
1155
+ InMemorySubscriptionStore,
1156
+ inMemoryAdapter,
1157
+ kvAdapter,
1158
+ normalizeStreamContentType,
1159
+ isJsonStreamContentType,
1160
+ streamMessageBytes
1161
+ };