@durable-streams/server 0.3.2 → 0.3.3
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.
- package/dist/index.cjs +1297 -260
- package/dist/index.d.cts +236 -2
- package/dist/index.d.ts +236 -2
- package/dist/index.js +1344 -312
- package/package.json +3 -3
- package/src/crypto.ts +217 -0
- package/src/file-store.ts +187 -144
- package/src/glob.ts +70 -0
- package/src/index.ts +14 -0
- package/src/log.ts +56 -0
- package/src/server.ts +75 -26
- package/src/store.ts +59 -7
- package/src/subscription-manager.ts +882 -0
- package/src/subscription-routes.ts +504 -0
- package/src/subscription-types.ts +80 -0
- package/src/types.ts +8 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LEASE_TTL_MS,
|
|
3
|
+
MAX_LEASE_TTL_MS,
|
|
4
|
+
MIN_LEASE_TTL_MS,
|
|
5
|
+
normalizeRelativePath,
|
|
6
|
+
} from "./subscription-manager"
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from "node:http"
|
|
8
|
+
import type { SubscriptionManager } from "./subscription-manager"
|
|
9
|
+
import type {
|
|
10
|
+
SubscriptionCallbackRequest,
|
|
11
|
+
SubscriptionCreateInput,
|
|
12
|
+
SubscriptionErrorCode,
|
|
13
|
+
SubscriptionType,
|
|
14
|
+
} from "./subscription-types"
|
|
15
|
+
|
|
16
|
+
const RESERVED_CONTROL_PREFIX = `/v1/stream/__ds`
|
|
17
|
+
const SUBSCRIPTION_PREFIX = `${RESERVED_CONTROL_PREFIX}/subscriptions/`
|
|
18
|
+
const JWKS_PATH = `${RESERVED_CONTROL_PREFIX}/jwks.json`
|
|
19
|
+
|
|
20
|
+
interface ParsedRoute {
|
|
21
|
+
subscriptionId: string
|
|
22
|
+
action:
|
|
23
|
+
| `base`
|
|
24
|
+
| `streams`
|
|
25
|
+
| `stream`
|
|
26
|
+
| `callback`
|
|
27
|
+
| `claim`
|
|
28
|
+
| `ack`
|
|
29
|
+
| `release`
|
|
30
|
+
streamPath?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ERROR_STATUS: Record<SubscriptionErrorCode, number> = {
|
|
34
|
+
INVALID_REQUEST: 400,
|
|
35
|
+
SUBSCRIPTION_NOT_FOUND: 404,
|
|
36
|
+
SUBSCRIPTION_ALREADY_EXISTS: 409,
|
|
37
|
+
WEBHOOK_URL_REJECTED: 400,
|
|
38
|
+
TOKEN_INVALID: 401,
|
|
39
|
+
TOKEN_EXPIRED: 401,
|
|
40
|
+
FENCED: 409,
|
|
41
|
+
ALREADY_CLAIMED: 409,
|
|
42
|
+
NO_PENDING_WORK: 409,
|
|
43
|
+
INVALID_OFFSET: 409,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class SubscriptionRoutes {
|
|
47
|
+
private readonly manager: SubscriptionManager
|
|
48
|
+
|
|
49
|
+
constructor(manager: SubscriptionManager) {
|
|
50
|
+
this.manager = manager
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async handleRequest(
|
|
54
|
+
method: string,
|
|
55
|
+
path: string,
|
|
56
|
+
req: IncomingMessage,
|
|
57
|
+
res: ServerResponse
|
|
58
|
+
): Promise<boolean> {
|
|
59
|
+
if (path === JWKS_PATH) {
|
|
60
|
+
this.handleJwks(method, res)
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const route = this.parseRoute(path)
|
|
65
|
+
if (!route) {
|
|
66
|
+
if (
|
|
67
|
+
path === RESERVED_CONTROL_PREFIX ||
|
|
68
|
+
path.startsWith(`${RESERVED_CONTROL_PREFIX}/`)
|
|
69
|
+
) {
|
|
70
|
+
this.writeError(
|
|
71
|
+
res,
|
|
72
|
+
404,
|
|
73
|
+
`SUBSCRIPTION_NOT_FOUND`,
|
|
74
|
+
`Durable Streams control route not found`
|
|
75
|
+
)
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
switch (route.action) {
|
|
83
|
+
case `base`:
|
|
84
|
+
await this.handleBase(route, method, req, res)
|
|
85
|
+
return true
|
|
86
|
+
case `streams`:
|
|
87
|
+
await this.handleStreams(route, method, req, res)
|
|
88
|
+
return true
|
|
89
|
+
case `stream`:
|
|
90
|
+
this.handleStream(route, method, res)
|
|
91
|
+
return true
|
|
92
|
+
case `callback`:
|
|
93
|
+
await this.handleCallback(route, req, res)
|
|
94
|
+
return true
|
|
95
|
+
case `claim`:
|
|
96
|
+
await this.handleClaim(route, req, res)
|
|
97
|
+
return true
|
|
98
|
+
case `ack`:
|
|
99
|
+
await this.handleAck(route, req, res)
|
|
100
|
+
return true
|
|
101
|
+
case `release`:
|
|
102
|
+
await this.handleRelease(route, req, res)
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof SyntaxError) {
|
|
107
|
+
this.writeError(res, 400, `INVALID_REQUEST`, `Invalid JSON body`)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
throw err
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async handleBase(
|
|
115
|
+
route: ParsedRoute,
|
|
116
|
+
method: string,
|
|
117
|
+
req: IncomingMessage,
|
|
118
|
+
res: ServerResponse
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
if (method === `PUT`) {
|
|
121
|
+
const parsed = await this.readJson(req)
|
|
122
|
+
const input = this.parseCreateInput(parsed)
|
|
123
|
+
if (`error` in input) {
|
|
124
|
+
this.writeError(res, 400, `INVALID_REQUEST`, input.error)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = this.manager.createOrConfirm(
|
|
129
|
+
route.subscriptionId,
|
|
130
|
+
input.value
|
|
131
|
+
)
|
|
132
|
+
if (`error` in result) {
|
|
133
|
+
this.writeError(
|
|
134
|
+
res,
|
|
135
|
+
ERROR_STATUS[result.error.code],
|
|
136
|
+
result.error.code,
|
|
137
|
+
result.error.message
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.writeJson(
|
|
143
|
+
res,
|
|
144
|
+
result.created ? 201 : 200,
|
|
145
|
+
this.manager.serialize(result.subscription)
|
|
146
|
+
)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (method === `GET`) {
|
|
151
|
+
const subscription = this.manager.get(route.subscriptionId)
|
|
152
|
+
if (!subscription) {
|
|
153
|
+
this.writeError(
|
|
154
|
+
res,
|
|
155
|
+
404,
|
|
156
|
+
`SUBSCRIPTION_NOT_FOUND`,
|
|
157
|
+
`Subscription not found`
|
|
158
|
+
)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
this.writeJson(res, 200, this.manager.serialize(subscription))
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (method === `DELETE`) {
|
|
166
|
+
this.manager.delete(route.subscriptionId)
|
|
167
|
+
res.writeHead(204)
|
|
168
|
+
res.end()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.methodNotAllowed(res)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private handleJwks(method: string, res: ServerResponse): void {
|
|
176
|
+
if (method !== `GET`) {
|
|
177
|
+
this.methodNotAllowed(res)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
res.writeHead(200, {
|
|
181
|
+
"content-type": `application/jwk-set+json`,
|
|
182
|
+
"cache-control": `public, max-age=300`,
|
|
183
|
+
})
|
|
184
|
+
res.end(JSON.stringify(this.manager.getWebhookJwks()))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async handleStreams(
|
|
188
|
+
route: ParsedRoute,
|
|
189
|
+
method: string,
|
|
190
|
+
req: IncomingMessage,
|
|
191
|
+
res: ServerResponse
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (method !== `POST`) {
|
|
194
|
+
this.methodNotAllowed(res)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
const parsed = await this.readJson(req)
|
|
198
|
+
const streams = (parsed as { streams?: unknown }).streams
|
|
199
|
+
if (
|
|
200
|
+
!Array.isArray(streams) ||
|
|
201
|
+
streams.some(
|
|
202
|
+
(stream) => typeof stream !== `string` || stream.length === 0
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
205
|
+
this.writeError(
|
|
206
|
+
res,
|
|
207
|
+
400,
|
|
208
|
+
`INVALID_REQUEST`,
|
|
209
|
+
`streams must be a non-empty string array`
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
const ok = this.manager.addExplicitStreams(
|
|
214
|
+
route.subscriptionId,
|
|
215
|
+
streams.map(normalizeRelativePath)
|
|
216
|
+
)
|
|
217
|
+
if (!ok) {
|
|
218
|
+
this.writeError(
|
|
219
|
+
res,
|
|
220
|
+
404,
|
|
221
|
+
`SUBSCRIPTION_NOT_FOUND`,
|
|
222
|
+
`Subscription not found`
|
|
223
|
+
)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
res.writeHead(204)
|
|
227
|
+
res.end()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private handleStream(
|
|
231
|
+
route: ParsedRoute,
|
|
232
|
+
method: string,
|
|
233
|
+
res: ServerResponse
|
|
234
|
+
): void {
|
|
235
|
+
if (method !== `DELETE`) {
|
|
236
|
+
this.methodNotAllowed(res)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
const ok = this.manager.removeExplicitStream(
|
|
240
|
+
route.subscriptionId,
|
|
241
|
+
route.streamPath ?? ``
|
|
242
|
+
)
|
|
243
|
+
if (!ok) {
|
|
244
|
+
this.writeError(
|
|
245
|
+
res,
|
|
246
|
+
404,
|
|
247
|
+
`SUBSCRIPTION_NOT_FOUND`,
|
|
248
|
+
`Subscription not found`
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
res.writeHead(204)
|
|
253
|
+
res.end()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async handleCallback(
|
|
257
|
+
route: ParsedRoute,
|
|
258
|
+
req: IncomingMessage,
|
|
259
|
+
res: ServerResponse
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const token = this.readBearerToken(req)
|
|
262
|
+
if (!token) {
|
|
263
|
+
this.writeError(
|
|
264
|
+
res,
|
|
265
|
+
401,
|
|
266
|
+
`TOKEN_INVALID`,
|
|
267
|
+
`Missing or malformed Authorization header`
|
|
268
|
+
)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
const body = (await this.readJson(req)) as SubscriptionCallbackRequest
|
|
272
|
+
const result = await this.manager.handleWebhookCallback(
|
|
273
|
+
route.subscriptionId,
|
|
274
|
+
token,
|
|
275
|
+
body
|
|
276
|
+
)
|
|
277
|
+
this.writeManagerResult(res, result)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async handleClaim(
|
|
281
|
+
route: ParsedRoute,
|
|
282
|
+
req: IncomingMessage,
|
|
283
|
+
res: ServerResponse
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const parsed = await this.readJson(req)
|
|
286
|
+
const worker = (parsed as { worker?: unknown }).worker
|
|
287
|
+
if (typeof worker !== `string` || worker.length === 0) {
|
|
288
|
+
this.writeError(
|
|
289
|
+
res,
|
|
290
|
+
400,
|
|
291
|
+
`INVALID_REQUEST`,
|
|
292
|
+
`worker must be a non-empty string`
|
|
293
|
+
)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
const result = await this.manager.claim(route.subscriptionId, worker)
|
|
297
|
+
this.writeManagerResult(res, result)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async handleAck(
|
|
301
|
+
route: ParsedRoute,
|
|
302
|
+
req: IncomingMessage,
|
|
303
|
+
res: ServerResponse
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
const token = this.readBearerToken(req)
|
|
306
|
+
if (!token) {
|
|
307
|
+
this.writeError(
|
|
308
|
+
res,
|
|
309
|
+
401,
|
|
310
|
+
`TOKEN_INVALID`,
|
|
311
|
+
`Missing or malformed Authorization header`
|
|
312
|
+
)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
const body = (await this.readJson(req)) as SubscriptionCallbackRequest
|
|
316
|
+
const result = await this.manager.ack(route.subscriptionId, token, body)
|
|
317
|
+
this.writeManagerResult(res, result)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async handleRelease(
|
|
321
|
+
route: ParsedRoute,
|
|
322
|
+
req: IncomingMessage,
|
|
323
|
+
res: ServerResponse
|
|
324
|
+
): Promise<void> {
|
|
325
|
+
const token = this.readBearerToken(req)
|
|
326
|
+
if (!token) {
|
|
327
|
+
this.writeError(
|
|
328
|
+
res,
|
|
329
|
+
401,
|
|
330
|
+
`TOKEN_INVALID`,
|
|
331
|
+
`Missing or malformed Authorization header`
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
const body = (await this.readJson(req)) as SubscriptionCallbackRequest
|
|
336
|
+
const result = await this.manager.release(route.subscriptionId, token, body)
|
|
337
|
+
this.writeManagerResult(res, result)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private parseCreateInput(
|
|
341
|
+
value: unknown
|
|
342
|
+
): { value: SubscriptionCreateInput } | { error: string } {
|
|
343
|
+
if (!value || typeof value !== `object`) {
|
|
344
|
+
return { error: `Request body must be a JSON object` }
|
|
345
|
+
}
|
|
346
|
+
const payload = value as Record<string, unknown>
|
|
347
|
+
if (payload.type !== `webhook` && payload.type !== `pull-wake`) {
|
|
348
|
+
return { error: `type must be "webhook" or "pull-wake"` }
|
|
349
|
+
}
|
|
350
|
+
const type = payload.type as SubscriptionType
|
|
351
|
+
const pattern =
|
|
352
|
+
typeof payload.pattern === `string` && payload.pattern.length > 0
|
|
353
|
+
? normalizeRelativePath(payload.pattern)
|
|
354
|
+
: undefined
|
|
355
|
+
const streams =
|
|
356
|
+
Array.isArray(payload.streams) && payload.streams.length > 0
|
|
357
|
+
? payload.streams.map((stream) =>
|
|
358
|
+
typeof stream === `string` ? normalizeRelativePath(stream) : null
|
|
359
|
+
)
|
|
360
|
+
: []
|
|
361
|
+
if (streams.some((stream) => stream === null)) {
|
|
362
|
+
return { error: `streams must contain only strings` }
|
|
363
|
+
}
|
|
364
|
+
if (!pattern && streams.length === 0) {
|
|
365
|
+
return { error: `At least one of pattern or streams is required` }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const leaseTtl =
|
|
369
|
+
payload.lease_ttl_ms === undefined
|
|
370
|
+
? DEFAULT_LEASE_TTL_MS
|
|
371
|
+
: payload.lease_ttl_ms
|
|
372
|
+
if (
|
|
373
|
+
typeof leaseTtl !== `number` ||
|
|
374
|
+
!Number.isInteger(leaseTtl) ||
|
|
375
|
+
leaseTtl < MIN_LEASE_TTL_MS ||
|
|
376
|
+
leaseTtl > MAX_LEASE_TTL_MS
|
|
377
|
+
) {
|
|
378
|
+
return { error: `lease_ttl_ms must be an integer from 1000 to 600000` }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let webhook: { url: string } | undefined
|
|
382
|
+
if (type === `webhook`) {
|
|
383
|
+
const rawWebhook = payload.webhook
|
|
384
|
+
if (!rawWebhook || typeof rawWebhook !== `object`) {
|
|
385
|
+
return { error: `webhook subscriptions require webhook.url` }
|
|
386
|
+
}
|
|
387
|
+
const url = (rawWebhook as { url?: unknown }).url
|
|
388
|
+
if (typeof url !== `string` || url.length === 0) {
|
|
389
|
+
return { error: `webhook subscriptions require webhook.url` }
|
|
390
|
+
}
|
|
391
|
+
webhook = { url }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const wakeStream =
|
|
395
|
+
typeof payload.wake_stream === `string` && payload.wake_stream.length > 0
|
|
396
|
+
? normalizeRelativePath(payload.wake_stream)
|
|
397
|
+
: undefined
|
|
398
|
+
if (type === `pull-wake` && !wakeStream) {
|
|
399
|
+
return { error: `pull-wake subscriptions require wake_stream` }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
value: {
|
|
404
|
+
type,
|
|
405
|
+
pattern,
|
|
406
|
+
streams: streams as Array<string>,
|
|
407
|
+
webhook,
|
|
408
|
+
wake_stream: wakeStream,
|
|
409
|
+
lease_ttl_ms: leaseTtl,
|
|
410
|
+
description:
|
|
411
|
+
typeof payload.description === `string`
|
|
412
|
+
? payload.description
|
|
413
|
+
: undefined,
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private parseRoute(path: string): ParsedRoute | null {
|
|
419
|
+
if (!path.startsWith(SUBSCRIPTION_PREFIX)) return null
|
|
420
|
+
|
|
421
|
+
const rest = path.slice(SUBSCRIPTION_PREFIX.length)
|
|
422
|
+
const parts = rest.split(`/`)
|
|
423
|
+
const subscriptionId = parts[0] ? decodeURIComponent(parts[0]) : ``
|
|
424
|
+
if (!subscriptionId) return null
|
|
425
|
+
|
|
426
|
+
const tail = parts.slice(1)
|
|
427
|
+
if (tail.length === 0) {
|
|
428
|
+
return { subscriptionId, action: `base` }
|
|
429
|
+
}
|
|
430
|
+
if (tail[0] === `streams` && tail.length === 1) {
|
|
431
|
+
return { subscriptionId, action: `streams` }
|
|
432
|
+
}
|
|
433
|
+
if (tail[0] === `streams` && tail.length > 1) {
|
|
434
|
+
return {
|
|
435
|
+
subscriptionId,
|
|
436
|
+
action: `stream`,
|
|
437
|
+
streamPath: normalizeRelativePath(
|
|
438
|
+
decodeURIComponent(tail.slice(1).join(`/`))
|
|
439
|
+
),
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
tail.length === 1 &&
|
|
444
|
+
[`callback`, `claim`, `ack`, `release`].includes(tail[0]!)
|
|
445
|
+
) {
|
|
446
|
+
return {
|
|
447
|
+
subscriptionId,
|
|
448
|
+
action: tail[0] as ParsedRoute[`action`],
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return null
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private readBearerToken(req: IncomingMessage): string | null {
|
|
456
|
+
const authHeader = req.headers.authorization
|
|
457
|
+
if (!authHeader || !authHeader.startsWith(`Bearer `)) return null
|
|
458
|
+
return authHeader.slice(`Bearer `.length)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async readJson(req: IncomingMessage): Promise<unknown> {
|
|
462
|
+
const chunks: Array<Buffer> = []
|
|
463
|
+
for await (const chunk of req) {
|
|
464
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
465
|
+
}
|
|
466
|
+
const raw = Buffer.concat(chunks).toString(`utf8`)
|
|
467
|
+
return raw.length > 0 ? JSON.parse(raw) : {}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private writeManagerResult(
|
|
471
|
+
res: ServerResponse,
|
|
472
|
+
result: { status: number; body?: Record<string, unknown> }
|
|
473
|
+
): void {
|
|
474
|
+
if (result.status === 204) {
|
|
475
|
+
res.writeHead(204)
|
|
476
|
+
res.end()
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
this.writeJson(res, result.status, result.body ?? {})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private writeJson(
|
|
483
|
+
res: ServerResponse,
|
|
484
|
+
status: number,
|
|
485
|
+
body: Record<string, unknown>
|
|
486
|
+
): void {
|
|
487
|
+
res.writeHead(status, { "content-type": `application/json` })
|
|
488
|
+
res.end(JSON.stringify(body))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private writeError(
|
|
492
|
+
res: ServerResponse,
|
|
493
|
+
status: number,
|
|
494
|
+
code: SubscriptionErrorCode,
|
|
495
|
+
message: string
|
|
496
|
+
): void {
|
|
497
|
+
this.writeJson(res, status, { error: { code, message } })
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private methodNotAllowed(res: ServerResponse): void {
|
|
501
|
+
res.writeHead(405, { "content-type": `text/plain` })
|
|
502
|
+
res.end(`Method not allowed`)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type SubscriptionType = `webhook` | `pull-wake`
|
|
2
|
+
export type SubscriptionStatus = `active` | `failed`
|
|
3
|
+
export type SubscriptionLinkType = `glob` | `explicit`
|
|
4
|
+
|
|
5
|
+
export interface SubscriptionStreamLink {
|
|
6
|
+
path: string
|
|
7
|
+
link_types: Set<SubscriptionLinkType>
|
|
8
|
+
acked_offset: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SubscriptionWebhookConfig {
|
|
12
|
+
url: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SubscriptionRecord {
|
|
16
|
+
id: string
|
|
17
|
+
type: SubscriptionType
|
|
18
|
+
pattern?: string
|
|
19
|
+
webhook?: SubscriptionWebhookConfig
|
|
20
|
+
wake_stream?: string
|
|
21
|
+
lease_ttl_ms: number
|
|
22
|
+
description?: string
|
|
23
|
+
created_at: string
|
|
24
|
+
status: SubscriptionStatus
|
|
25
|
+
config_hash: string
|
|
26
|
+
streams: Map<string, SubscriptionStreamLink>
|
|
27
|
+
generation: number
|
|
28
|
+
wake_id: string | null
|
|
29
|
+
wake_snapshot: Map<string, string>
|
|
30
|
+
token: string | null
|
|
31
|
+
holder: string | null
|
|
32
|
+
lease_timer: ReturnType<typeof setTimeout> | null
|
|
33
|
+
retry_count: number
|
|
34
|
+
retry_timer: ReturnType<typeof setTimeout> | null
|
|
35
|
+
next_attempt_at: number | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SubscriptionStreamInfo {
|
|
39
|
+
path: string
|
|
40
|
+
link_type: SubscriptionLinkType
|
|
41
|
+
acked_offset: string
|
|
42
|
+
tail_offset: string
|
|
43
|
+
has_pending: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SubscriptionCreateInput {
|
|
47
|
+
type: SubscriptionType
|
|
48
|
+
pattern?: string
|
|
49
|
+
streams: Array<string>
|
|
50
|
+
webhook?: { url: string }
|
|
51
|
+
wake_stream?: string
|
|
52
|
+
lease_ttl_ms: number
|
|
53
|
+
description?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SubscriptionCallbackRequest {
|
|
57
|
+
wake_id?: string
|
|
58
|
+
generation?: number
|
|
59
|
+
acks?: Array<{ stream?: string; path?: string; offset: string }>
|
|
60
|
+
done?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SubscriptionErrorCode =
|
|
64
|
+
| `INVALID_REQUEST`
|
|
65
|
+
| `SUBSCRIPTION_NOT_FOUND`
|
|
66
|
+
| `SUBSCRIPTION_ALREADY_EXISTS`
|
|
67
|
+
| `WEBHOOK_URL_REJECTED`
|
|
68
|
+
| `TOKEN_INVALID`
|
|
69
|
+
| `TOKEN_EXPIRED`
|
|
70
|
+
| `FENCED`
|
|
71
|
+
| `ALREADY_CLAIMED`
|
|
72
|
+
| `NO_PENDING_WORK`
|
|
73
|
+
| `INVALID_OFFSET`
|
|
74
|
+
|
|
75
|
+
export interface SubscriptionError {
|
|
76
|
+
code: SubscriptionErrorCode
|
|
77
|
+
message: string
|
|
78
|
+
current_holder?: string
|
|
79
|
+
generation?: number
|
|
80
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -206,6 +206,14 @@ export interface TestServerOptions {
|
|
|
206
206
|
* Default: October 9, 2024 00:00:00 UTC.
|
|
207
207
|
*/
|
|
208
208
|
cursorEpoch?: Date
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Enable webhook subscriptions.
|
|
212
|
+
* Pull-wake subscription routes are always mounted, but type=webhook creates
|
|
213
|
+
* are rejected unless this is true.
|
|
214
|
+
* Default: false.
|
|
215
|
+
*/
|
|
216
|
+
webhooks?: boolean
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
/**
|