@hearthlog/ledger 0.0.0-stage → 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.
- package/LICENSE +235 -0
- package/README.md +173 -2
- package/contracts/append-hash.json +30 -0
- package/docs/database-structure.md +176 -0
- package/docs/event-store-service-design.md +124 -0
- package/docs/event-store-service.http +182 -0
- package/docs/event-store-service.openapi.yaml +1651 -0
- package/package.json +43 -3
- package/scripts/logical-backup.ts +656 -0
- package/src/action-batches.ts +65 -0
- package/src/app-registry.ts +479 -0
- package/src/auth.ts +186 -0
- package/src/contracts.ts +251 -0
- package/src/domain.ts +245 -0
- package/src/errors.ts +50 -0
- package/src/event-validation.ts +181 -0
- package/src/fence.ts +76 -0
- package/src/http.ts +656 -0
- package/src/index.ts +3 -0
- package/src/ledger-app.ts +2057 -0
- package/src/rpc.ts +42 -0
- package/src/schema.ts +236 -0
- package/src/schemas.ts +429 -0
- package/src/sqlite-export.ts +136 -0
- package/src/transfer.ts +219 -0
package/src/http.ts
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { env } from "cloudflare:workers";
|
|
2
|
+
import { Elysia } from "elysia";
|
|
3
|
+
import { CloudflareAdapter } from "elysia/adapter/cloudflare-worker";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
authorizeAdmin,
|
|
7
|
+
bearerToken,
|
|
8
|
+
originAllowed,
|
|
9
|
+
requestOrigin,
|
|
10
|
+
verifyAccess,
|
|
11
|
+
} from "./auth";
|
|
12
|
+
import { INTERNAL_SOCKET_ACCESS_HEADER, type AccessClaims } from "./contracts";
|
|
13
|
+
import {
|
|
14
|
+
errorResponse,
|
|
15
|
+
jsonError,
|
|
16
|
+
LedgerError,
|
|
17
|
+
normalizeError,
|
|
18
|
+
validationError,
|
|
19
|
+
} from "./errors";
|
|
20
|
+
import { RETRY_AFTER_SECONDS } from "./fence";
|
|
21
|
+
import type { LedgerApp } from "./ledger-app";
|
|
22
|
+
import type { RpcResult } from "./rpc";
|
|
23
|
+
import {
|
|
24
|
+
apiModels,
|
|
25
|
+
appendEventsBody,
|
|
26
|
+
applicationParams,
|
|
27
|
+
createApplicationBody,
|
|
28
|
+
createLogBody,
|
|
29
|
+
eventPolicyBody,
|
|
30
|
+
fenceBody,
|
|
31
|
+
importBeginBody,
|
|
32
|
+
importFinishBody,
|
|
33
|
+
importRecordsBody,
|
|
34
|
+
forkLogBody,
|
|
35
|
+
logVisibilityBody,
|
|
36
|
+
galleryPhotoReferenceParams,
|
|
37
|
+
logLineageBody,
|
|
38
|
+
eventsQuery,
|
|
39
|
+
logParams,
|
|
40
|
+
socketQuery,
|
|
41
|
+
adminEventParams,
|
|
42
|
+
adminEventsQuery,
|
|
43
|
+
adminConnectionsQuery,
|
|
44
|
+
} from "./schemas";
|
|
45
|
+
|
|
46
|
+
const registry = () => env.APP_REGISTRY.getByName("registry-v1");
|
|
47
|
+
|
|
48
|
+
const app = new Elysia({
|
|
49
|
+
adapter: CloudflareAdapter,
|
|
50
|
+
normalize: false,
|
|
51
|
+
})
|
|
52
|
+
.model(apiModels)
|
|
53
|
+
.onRequest(({ request }) => {
|
|
54
|
+
const origin = requestOrigin(request);
|
|
55
|
+
if (!originAllowed(env, origin)) {
|
|
56
|
+
return withCors(
|
|
57
|
+
jsonError("origin_not_allowed", "origin is not allowed", 403),
|
|
58
|
+
origin,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (request.method === "OPTIONS") return preflight(origin);
|
|
62
|
+
})
|
|
63
|
+
.onError(({ code, error, request }) => {
|
|
64
|
+
const origin = requestOrigin(request);
|
|
65
|
+
if (error instanceof LedgerError) {
|
|
66
|
+
return withCors(errorResponse(error), origin);
|
|
67
|
+
}
|
|
68
|
+
if (code === "PARSE") {
|
|
69
|
+
return withCors(
|
|
70
|
+
errorResponse(
|
|
71
|
+
validationError("request body must be valid JSON"),
|
|
72
|
+
),
|
|
73
|
+
origin,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (code === "VALIDATION") {
|
|
77
|
+
return withCors(
|
|
78
|
+
errorResponse(validationError(error.message)),
|
|
79
|
+
origin,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (code === "NOT_FOUND") {
|
|
83
|
+
return withCors(
|
|
84
|
+
jsonError("not_found", "resource not found", 404),
|
|
85
|
+
origin,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
console.error(
|
|
89
|
+
JSON.stringify({
|
|
90
|
+
message: "Ledger Worker request failed",
|
|
91
|
+
path: new URL(request.url).pathname,
|
|
92
|
+
error: normalizeError(error).message,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
return withCors(
|
|
96
|
+
jsonError("internal_error", "internal error", 500),
|
|
97
|
+
origin,
|
|
98
|
+
);
|
|
99
|
+
})
|
|
100
|
+
.get(
|
|
101
|
+
"/health",
|
|
102
|
+
async ({ request }) =>
|
|
103
|
+
rpcResponse(await registry().health(), requestOrigin(request)),
|
|
104
|
+
{ response: { 204: "HealthResponse" } },
|
|
105
|
+
)
|
|
106
|
+
.all("/health", ({ request }) => notFound(request))
|
|
107
|
+
.all("/_internal/*", ({ request }) => notFound(request))
|
|
108
|
+
.group("/admin/v1", (admin) =>
|
|
109
|
+
admin
|
|
110
|
+
.resolve(async ({ request }) => {
|
|
111
|
+
await administrator(request);
|
|
112
|
+
return { administrator: true as const };
|
|
113
|
+
})
|
|
114
|
+
.get(
|
|
115
|
+
"/apps",
|
|
116
|
+
async ({ request }) =>
|
|
117
|
+
rpcResponse(
|
|
118
|
+
await registry().listApplications(),
|
|
119
|
+
requestOrigin(request),
|
|
120
|
+
),
|
|
121
|
+
{ response: { 200: "ApplicationListResponse" } },
|
|
122
|
+
)
|
|
123
|
+
.get(
|
|
124
|
+
"/apps/:appId/logs",
|
|
125
|
+
async ({ params, request }) => {
|
|
126
|
+
const application = await administratorApplication(
|
|
127
|
+
params.appId,
|
|
128
|
+
);
|
|
129
|
+
return rpcResponse(
|
|
130
|
+
await env.LEDGER_APP.getByName(
|
|
131
|
+
application.id,
|
|
132
|
+
).administratorListLogs(application.id),
|
|
133
|
+
requestOrigin(request),
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
params: applicationParams,
|
|
138
|
+
response: { 200: "LogListResponse" },
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
.post(
|
|
142
|
+
"/apps/:appId/logs/:logId/lineage",
|
|
143
|
+
async ({ params, body, request }) => {
|
|
144
|
+
const application = await administratorApplication(params.appId);
|
|
145
|
+
return rpcResponse(
|
|
146
|
+
await env.LEDGER_APP.getByName(application.id)
|
|
147
|
+
.administratorSetLogLineage(application.id, params.logId, body),
|
|
148
|
+
requestOrigin(request),
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
{ params: adminEventParams, body: logLineageBody, response: { 200: "LogResponse" } },
|
|
152
|
+
)
|
|
153
|
+
.get("/registry/export/sqlite", async ({ request }) =>
|
|
154
|
+
withCors(
|
|
155
|
+
await registry().administratorExportSqlite(),
|
|
156
|
+
requestOrigin(request),
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
.get(
|
|
160
|
+
"/apps/:appId/export/sqlite",
|
|
161
|
+
async ({ params, request }) => {
|
|
162
|
+
const application = await administratorApplication(
|
|
163
|
+
params.appId,
|
|
164
|
+
);
|
|
165
|
+
return withCors(
|
|
166
|
+
await env.LEDGER_APP.getByName(
|
|
167
|
+
application.id,
|
|
168
|
+
).administratorExportSqlite(application.id),
|
|
169
|
+
requestOrigin(request),
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
{ params: applicationParams },
|
|
173
|
+
)
|
|
174
|
+
.get(
|
|
175
|
+
"/apps/:appId/connections",
|
|
176
|
+
async ({ params, query, request }) => {
|
|
177
|
+
const application = await administratorApplication(
|
|
178
|
+
params.appId,
|
|
179
|
+
);
|
|
180
|
+
return rpcResponse(
|
|
181
|
+
await env.LEDGER_APP.getByName(
|
|
182
|
+
application.id,
|
|
183
|
+
).administratorConnections(
|
|
184
|
+
application.id,
|
|
185
|
+
query.afterId ? { afterId: query.afterId } : {},
|
|
186
|
+
),
|
|
187
|
+
requestOrigin(request),
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
params: applicationParams,
|
|
192
|
+
query: adminConnectionsQuery,
|
|
193
|
+
response: { 200: "AdminConnectionsResponse" },
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
.get(
|
|
197
|
+
"/apps/:appId/export",
|
|
198
|
+
async ({ params, request }) => {
|
|
199
|
+
const application = await administratorApplication(
|
|
200
|
+
params.appId,
|
|
201
|
+
);
|
|
202
|
+
return withCors(
|
|
203
|
+
await env.LEDGER_APP.getByName(
|
|
204
|
+
application.id,
|
|
205
|
+
).administratorExportApplication(application),
|
|
206
|
+
requestOrigin(request),
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
{ params: applicationParams },
|
|
210
|
+
)
|
|
211
|
+
.get(
|
|
212
|
+
"/apps/:appId/metrics",
|
|
213
|
+
async ({ params, request }) => {
|
|
214
|
+
const application = await administratorApplication(
|
|
215
|
+
params.appId,
|
|
216
|
+
);
|
|
217
|
+
return rpcResponse(
|
|
218
|
+
await env.LEDGER_APP.getByName(
|
|
219
|
+
application.id,
|
|
220
|
+
).administratorMetrics(application.id),
|
|
221
|
+
requestOrigin(request),
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
params: applicationParams,
|
|
226
|
+
response: { 200: "AdminMetricsResponse" },
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
.get(
|
|
230
|
+
"/apps/:appId/logs/:logId/events",
|
|
231
|
+
async ({ params, query, request }) => {
|
|
232
|
+
const application = await administratorApplication(
|
|
233
|
+
params.appId,
|
|
234
|
+
);
|
|
235
|
+
return rpcResponse(
|
|
236
|
+
await env.LEDGER_APP.getByName(
|
|
237
|
+
application.id,
|
|
238
|
+
).administratorListEvents(
|
|
239
|
+
application.id,
|
|
240
|
+
params.logId,
|
|
241
|
+
query,
|
|
242
|
+
),
|
|
243
|
+
requestOrigin(request),
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
params: adminEventParams,
|
|
248
|
+
query: adminEventsQuery,
|
|
249
|
+
response: { 200: "AdminEventsResponse" },
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
.post(
|
|
253
|
+
"/apps",
|
|
254
|
+
async ({ body, request }) =>
|
|
255
|
+
rpcResponse(
|
|
256
|
+
await registry().createApplication(body),
|
|
257
|
+
requestOrigin(request),
|
|
258
|
+
),
|
|
259
|
+
{
|
|
260
|
+
body: createApplicationBody,
|
|
261
|
+
response: { 201: "ApplicationTokenResponse" },
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
.get(
|
|
265
|
+
"/apps/:appId/transfer",
|
|
266
|
+
async ({ params, request }) => {
|
|
267
|
+
const application = await administratorApplication(params.appId);
|
|
268
|
+
return withCors(
|
|
269
|
+
await env.LEDGER_APP.getByName(application.id).administratorTransferExport(application.id),
|
|
270
|
+
requestOrigin(request),
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
{ params: applicationParams },
|
|
274
|
+
)
|
|
275
|
+
.get(
|
|
276
|
+
"/apps/:appId/checksums",
|
|
277
|
+
async ({ params, request }) => {
|
|
278
|
+
const application = await administratorApplication(params.appId);
|
|
279
|
+
return rpcResponse(
|
|
280
|
+
await env.LEDGER_APP.getByName(application.id).administratorChecksums(application.id),
|
|
281
|
+
requestOrigin(request),
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
{ params: applicationParams },
|
|
285
|
+
)
|
|
286
|
+
.post(
|
|
287
|
+
"/import",
|
|
288
|
+
async ({ body, request }) =>
|
|
289
|
+
rpcResponse(
|
|
290
|
+
await registry().beginImport({
|
|
291
|
+
id: body.id,
|
|
292
|
+
name: body.name,
|
|
293
|
+
createdAt: body.createdAt,
|
|
294
|
+
policy: body.policy ? JSON.stringify(body.policy) : null,
|
|
295
|
+
}),
|
|
296
|
+
requestOrigin(request),
|
|
297
|
+
),
|
|
298
|
+
{ body: importBeginBody },
|
|
299
|
+
)
|
|
300
|
+
.post(
|
|
301
|
+
"/apps/:appId/import",
|
|
302
|
+
async ({ params, body, request }) => {
|
|
303
|
+
const importing = await registry().importingApplication(params.appId);
|
|
304
|
+
if (!importing.ok) return rpcResponse(importing, requestOrigin(request));
|
|
305
|
+
return rpcResponse(
|
|
306
|
+
await env.LEDGER_APP.getByName(importing.body.id).importRecords(importing.body.id, body.records),
|
|
307
|
+
requestOrigin(request),
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
{ params: applicationParams, body: importRecordsBody },
|
|
311
|
+
)
|
|
312
|
+
.post(
|
|
313
|
+
"/apps/:appId/import/abort",
|
|
314
|
+
async ({ params, request }) =>
|
|
315
|
+
rpcResponse(await registry().abortImport(params.appId), requestOrigin(request)),
|
|
316
|
+
{ params: applicationParams },
|
|
317
|
+
)
|
|
318
|
+
.post(
|
|
319
|
+
"/apps/:appId/import/finish",
|
|
320
|
+
async ({ params, body, request }) =>
|
|
321
|
+
rpcResponse(await registry().finishImport(params.appId, body), requestOrigin(request)),
|
|
322
|
+
{ params: applicationParams, body: importFinishBody },
|
|
323
|
+
)
|
|
324
|
+
.get(
|
|
325
|
+
"/apps/:appId/fence",
|
|
326
|
+
async ({ params, request }) =>
|
|
327
|
+
rpcResponse(await registry().getApplicationFence(params.appId), requestOrigin(request)),
|
|
328
|
+
{ params: applicationParams },
|
|
329
|
+
)
|
|
330
|
+
.put(
|
|
331
|
+
"/apps/:appId/fence",
|
|
332
|
+
async ({ params, body, request }) =>
|
|
333
|
+
rpcResponse(await registry().setApplicationFence(params.appId, body), requestOrigin(request)),
|
|
334
|
+
{ params: applicationParams, body: fenceBody },
|
|
335
|
+
)
|
|
336
|
+
.get(
|
|
337
|
+
"/apps/:appId/policy",
|
|
338
|
+
async ({ params, request }) =>
|
|
339
|
+
rpcResponse(
|
|
340
|
+
await registry().getApplicationPolicy(params.appId),
|
|
341
|
+
requestOrigin(request),
|
|
342
|
+
),
|
|
343
|
+
{ params: applicationParams },
|
|
344
|
+
)
|
|
345
|
+
.put(
|
|
346
|
+
"/apps/:appId/policy",
|
|
347
|
+
async ({ params, body, request }) =>
|
|
348
|
+
rpcResponse(
|
|
349
|
+
await registry().setApplicationPolicy(params.appId, body),
|
|
350
|
+
requestOrigin(request),
|
|
351
|
+
),
|
|
352
|
+
{ params: applicationParams, body: eventPolicyBody },
|
|
353
|
+
)
|
|
354
|
+
.post(
|
|
355
|
+
"/apps/:appId/rotate-token",
|
|
356
|
+
async ({ params, request }) =>
|
|
357
|
+
rpcResponse(
|
|
358
|
+
await registry().rotateApplicationToken(params.appId),
|
|
359
|
+
requestOrigin(request),
|
|
360
|
+
),
|
|
361
|
+
{
|
|
362
|
+
params: applicationParams,
|
|
363
|
+
response: { 200: "ApplicationTokenResponse" },
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
.post(
|
|
367
|
+
"/apps/:appId/revoke",
|
|
368
|
+
async ({ params, request }) =>
|
|
369
|
+
rpcResponse(
|
|
370
|
+
await registry().revokeApplication(params.appId),
|
|
371
|
+
requestOrigin(request),
|
|
372
|
+
),
|
|
373
|
+
{
|
|
374
|
+
params: applicationParams,
|
|
375
|
+
response: { 200: "ApplicationResponse" },
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
.all("/*", ({ request }) => notFound(request))
|
|
379
|
+
.all("", ({ request }) => notFound(request)),
|
|
380
|
+
)
|
|
381
|
+
.group("", (authenticated) =>
|
|
382
|
+
authenticated
|
|
383
|
+
.resolve(async ({ request }) => ({
|
|
384
|
+
access: await accessClaims(request),
|
|
385
|
+
}))
|
|
386
|
+
.get(
|
|
387
|
+
"/logs/:logId/events/socket",
|
|
388
|
+
async ({ access, params, query, request }) => {
|
|
389
|
+
const origin = requestOrigin(request);
|
|
390
|
+
const response = await ledger(access).fetch(
|
|
391
|
+
socketUpgradeRequest(
|
|
392
|
+
request,
|
|
393
|
+
access,
|
|
394
|
+
params.logId,
|
|
395
|
+
query.afterVersion ?? 0,
|
|
396
|
+
query.replayVersion ?? 0,
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
return response.status === 101
|
|
400
|
+
? response
|
|
401
|
+
: withCors(response, origin);
|
|
402
|
+
},
|
|
403
|
+
{ params: logParams, query: socketQuery },
|
|
404
|
+
)
|
|
405
|
+
.get(
|
|
406
|
+
"/session",
|
|
407
|
+
async ({ access, request }) =>
|
|
408
|
+
rpcResponse(
|
|
409
|
+
await ledger(access).getSession(access),
|
|
410
|
+
requestOrigin(request),
|
|
411
|
+
),
|
|
412
|
+
{ response: { 200: "SessionResponse" } },
|
|
413
|
+
)
|
|
414
|
+
.get(
|
|
415
|
+
"/logs",
|
|
416
|
+
async ({ access, request }) =>
|
|
417
|
+
rpcResponse(
|
|
418
|
+
await ledger(access).listLogs(access),
|
|
419
|
+
requestOrigin(request),
|
|
420
|
+
),
|
|
421
|
+
{ response: { 200: "LogListResponse" } },
|
|
422
|
+
)
|
|
423
|
+
.post(
|
|
424
|
+
"/logs",
|
|
425
|
+
async ({ access, body, request }) =>
|
|
426
|
+
rpcResponse(
|
|
427
|
+
await ledger(access).createLog(access, body),
|
|
428
|
+
requestOrigin(request),
|
|
429
|
+
),
|
|
430
|
+
{
|
|
431
|
+
body: createLogBody,
|
|
432
|
+
response: { 201: "CreateLogResponse" },
|
|
433
|
+
},
|
|
434
|
+
)
|
|
435
|
+
.get(
|
|
436
|
+
"/logs/:logId",
|
|
437
|
+
async ({ access, params, request }) =>
|
|
438
|
+
rpcResponse(
|
|
439
|
+
await ledger(access).getLog(access, params.logId),
|
|
440
|
+
requestOrigin(request),
|
|
441
|
+
),
|
|
442
|
+
{
|
|
443
|
+
params: logParams,
|
|
444
|
+
response: { 200: "LogResponse" },
|
|
445
|
+
},
|
|
446
|
+
)
|
|
447
|
+
.get(
|
|
448
|
+
"/logs/:logId/gallery-photos/:photoId/files/:fileVersionId/reference",
|
|
449
|
+
async ({ access, params, request }) =>
|
|
450
|
+
rpcResponse(
|
|
451
|
+
await ledger(access).getGalleryPhotoReference(
|
|
452
|
+
access, params.logId, params.photoId, params.fileVersionId,
|
|
453
|
+
),
|
|
454
|
+
requestOrigin(request),
|
|
455
|
+
),
|
|
456
|
+
{
|
|
457
|
+
params: galleryPhotoReferenceParams,
|
|
458
|
+
response: { 200: 'GalleryPhotoReferenceResponse' },
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
.post(
|
|
462
|
+
"/logs/:logId/forks",
|
|
463
|
+
async ({ access, params, body, request }) =>
|
|
464
|
+
rpcResponse(
|
|
465
|
+
await ledger(access).forkLog(access, params.logId, body),
|
|
466
|
+
requestOrigin(request),
|
|
467
|
+
),
|
|
468
|
+
{ params: logParams, body: forkLogBody, response: { 201: "ForkLogResponse" } },
|
|
469
|
+
)
|
|
470
|
+
.post(
|
|
471
|
+
"/logs/:logId/visibility",
|
|
472
|
+
async ({ access, params, body, request }) =>
|
|
473
|
+
rpcResponse(
|
|
474
|
+
await ledger(access).setLogVisibility(access, params.logId, body),
|
|
475
|
+
requestOrigin(request),
|
|
476
|
+
),
|
|
477
|
+
{ params: logParams, body: logVisibilityBody, response: { 200: "LogResponse" } },
|
|
478
|
+
)
|
|
479
|
+
.post(
|
|
480
|
+
"/logs/:logId/finalize-fork",
|
|
481
|
+
async ({ access, params, request }) =>
|
|
482
|
+
rpcResponse(
|
|
483
|
+
await ledger(access).finalizeFork(access, params.logId),
|
|
484
|
+
requestOrigin(request),
|
|
485
|
+
),
|
|
486
|
+
{ params: logParams, response: { 200: "ForkLogResponse" } },
|
|
487
|
+
)
|
|
488
|
+
.delete(
|
|
489
|
+
"/logs/:logId",
|
|
490
|
+
async ({ access, params, request }) =>
|
|
491
|
+
rpcResponse(
|
|
492
|
+
await ledger(access).deleteLog(access, params.logId),
|
|
493
|
+
requestOrigin(request),
|
|
494
|
+
),
|
|
495
|
+
{
|
|
496
|
+
params: logParams,
|
|
497
|
+
response: { 204: "EmptyResponse" },
|
|
498
|
+
},
|
|
499
|
+
)
|
|
500
|
+
.get(
|
|
501
|
+
"/logs/:logId/events",
|
|
502
|
+
async ({ access, params, query, request }) =>
|
|
503
|
+
rpcResponse(
|
|
504
|
+
await ledger(access).listEvents(
|
|
505
|
+
access,
|
|
506
|
+
params.logId,
|
|
507
|
+
query.afterVersion,
|
|
508
|
+
query.limit,
|
|
509
|
+
Number(request.headers.get("X-Ledger-Replay-Version") ?? 0),
|
|
510
|
+
),
|
|
511
|
+
requestOrigin(request),
|
|
512
|
+
),
|
|
513
|
+
{
|
|
514
|
+
params: logParams,
|
|
515
|
+
query: eventsQuery,
|
|
516
|
+
response: { 200: "EventBatchResponse" },
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
.post(
|
|
520
|
+
"/logs/:logId/events",
|
|
521
|
+
async ({ access, body, params, request }) =>
|
|
522
|
+
rpcResponse(
|
|
523
|
+
await ledger(access).appendEvents(
|
|
524
|
+
access,
|
|
525
|
+
params.logId,
|
|
526
|
+
body,
|
|
527
|
+
Number(request.headers.get("X-Ledger-Replay-Version") ?? 0),
|
|
528
|
+
request.headers.get("X-Ledger-Actor"),
|
|
529
|
+
),
|
|
530
|
+
requestOrigin(request),
|
|
531
|
+
),
|
|
532
|
+
{
|
|
533
|
+
body: appendEventsBody,
|
|
534
|
+
params: logParams,
|
|
535
|
+
response: { 200: "AppendEventsResponse" },
|
|
536
|
+
},
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
.all("/*", async ({ request }) => authenticatedNotFound(request))
|
|
540
|
+
.all("/", async ({ request }) => authenticatedNotFound(request))
|
|
541
|
+
.compile();
|
|
542
|
+
|
|
543
|
+
export default app;
|
|
544
|
+
|
|
545
|
+
function ledger(access: AccessClaims): DurableObjectStub<LedgerApp> {
|
|
546
|
+
return env.LEDGER_APP.getByName(access.appId);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function administratorApplication(appId: string) {
|
|
550
|
+
const result = await registry().getApplication(appId);
|
|
551
|
+
if (!result.ok) throw new LedgerError(result.status, result.body);
|
|
552
|
+
return result.body;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function accessClaims(request: Request): Promise<AccessClaims> {
|
|
556
|
+
try {
|
|
557
|
+
return await verifyAccess(env, bearerToken(request));
|
|
558
|
+
} catch {
|
|
559
|
+
throw new LedgerError(401, {
|
|
560
|
+
error: "invalid bearer token",
|
|
561
|
+
code: "unauthorized",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function administrator(request: Request): Promise<void> {
|
|
567
|
+
try {
|
|
568
|
+
await authorizeAdmin(request, env);
|
|
569
|
+
} catch {
|
|
570
|
+
throw new LedgerError(401, {
|
|
571
|
+
error: "invalid administrator token",
|
|
572
|
+
code: "unauthorized",
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function authenticatedNotFound(request: Request): Promise<Response> {
|
|
578
|
+
const path = new URL(request.url).pathname;
|
|
579
|
+
if (path.startsWith("/_internal/")) return notFound(request);
|
|
580
|
+
if (path.startsWith("/admin/v1/")) {
|
|
581
|
+
await administrator(request);
|
|
582
|
+
} else {
|
|
583
|
+
await accessClaims(request);
|
|
584
|
+
}
|
|
585
|
+
return notFound(request);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function socketUpgradeRequest(
|
|
589
|
+
request: Request,
|
|
590
|
+
access: AccessClaims,
|
|
591
|
+
logId: string,
|
|
592
|
+
afterVersion: number,
|
|
593
|
+
replayVersion: number,
|
|
594
|
+
): Request {
|
|
595
|
+
const url = new URL(request.url);
|
|
596
|
+
url.pathname = `/logs/${logId}/events/socket`;
|
|
597
|
+
url.search = "";
|
|
598
|
+
url.searchParams.set("afterVersion", String(afterVersion));
|
|
599
|
+
url.searchParams.set("replayVersion", String(replayVersion));
|
|
600
|
+
|
|
601
|
+
const forwarded = new Request(url, request);
|
|
602
|
+
forwarded.headers.delete("authorization");
|
|
603
|
+
forwarded.headers.set(
|
|
604
|
+
INTERNAL_SOCKET_ACCESS_HEADER,
|
|
605
|
+
JSON.stringify(access),
|
|
606
|
+
);
|
|
607
|
+
return forwarded;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function notFound(request: Request): Response {
|
|
611
|
+
return withCors(
|
|
612
|
+
jsonError("not_found", "resource not found", 404),
|
|
613
|
+
requestOrigin(request),
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function rpcResponse<T>(result: RpcResult<T>, origin: string | null): Response {
|
|
618
|
+
const response =
|
|
619
|
+
result.body === null
|
|
620
|
+
? new Response(null, { status: result.status })
|
|
621
|
+
: Response.json(result.body, { status: result.status });
|
|
622
|
+
// A paused workspace tells clients when to try again; the outbox retries 503 without parking.
|
|
623
|
+
if (result.status === 503 && !result.ok && result.body.code === "tenant_migrating") {
|
|
624
|
+
response.headers.set("Retry-After", String(RETRY_AFTER_SECONDS));
|
|
625
|
+
}
|
|
626
|
+
return withCors(response, origin);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function preflight(origin: string | null): Response {
|
|
630
|
+
const headers = corsHeaders(origin);
|
|
631
|
+
headers.set("Access-Control-Max-Age", "86400");
|
|
632
|
+
return new Response(null, { status: 204, headers });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function withCors(response: Response, origin: string | null): Response {
|
|
636
|
+
const headers = new Headers(response.headers);
|
|
637
|
+
for (const [name, value] of corsHeaders(origin)) headers.set(name, value);
|
|
638
|
+
return new Response(response.body, {
|
|
639
|
+
status: response.status,
|
|
640
|
+
statusText: response.statusText,
|
|
641
|
+
headers,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function corsHeaders(origin: string | null): Headers {
|
|
646
|
+
const headers = new Headers({
|
|
647
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
648
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type, X-Ledger-Replay-Version",
|
|
649
|
+
"Cache-Control": "no-store",
|
|
650
|
+
});
|
|
651
|
+
if (origin !== null) {
|
|
652
|
+
headers.set("Access-Control-Allow-Origin", origin);
|
|
653
|
+
headers.set("Vary", "Origin");
|
|
654
|
+
}
|
|
655
|
+
return headers;
|
|
656
|
+
}
|
package/src/index.ts
ADDED