@lunora/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21
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.md +6 -0
- package/README.md +17 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +561 -80
- package/dist/index.d.ts +561 -80
- package/dist/index.mjs +9 -7
- package/dist/packem_shared/{DEFAULT_REGISTRY_CACHE_TTL_MS-BpCwo_mo.mjs → DEFAULT_REGISTRY_CACHE_TTL_MS-B3pA7aXp.mjs} +9 -5
- package/dist/packem_shared/LunoraError-Bpb9EFJ3.mjs +22 -0
- package/dist/packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs +8 -0
- package/dist/packem_shared/applyJurisdiction-BkZtTkct.mjs +20 -0
- package/dist/packem_shared/composeIdentityResolvers-XGjO7V1J.mjs +55 -0
- package/dist/packem_shared/{composeWorker-BmfKl1ei.mjs → composeWorker-Dpw9d1s5.mjs} +707 -91
- package/dist/packem_shared/{createCrossShardRelationCapabilities-C0KOf7er.mjs → createCrossShardRelationCapabilities-CbcWjkAn.mjs} +4 -2
- package/dist/packem_shared/{createQueryCoordinator-DbxC7iUz.mjs → createQueryCoordinator-DNCJzOZE.mjs} +12 -3
- package/dist/packem_shared/{decorateResponse-DbISh_Wi.mjs → decorateResponse-CsZc49QC.mjs} +42 -9
- package/package.json +5 -2
- package/dist/packem_shared/LunoraError-CL0aOtpo.mjs +0 -46
- package/dist/packem_shared/resolveShard-DDkzWtrU.mjs +0 -9
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isLunoraError, toErrorBody } from '@lunora/errors';
|
|
2
|
+
import { NOOP_EXECUTION_CONTEXT } from './NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
|
|
3
|
+
import { LunoraError, toErrorResponse } from './LunoraError-Bpb9EFJ3.mjs';
|
|
4
|
+
import { wrapResolverWithContract } from './composeIdentityResolvers-XGjO7V1J.mjs';
|
|
5
|
+
export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-XGjO7V1J.mjs';
|
|
2
6
|
import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
|
|
3
|
-
import { resolveShard } from './
|
|
4
|
-
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-
|
|
7
|
+
import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
|
|
8
|
+
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-CsZc49QC.mjs';
|
|
9
|
+
|
|
10
|
+
const RELAY_NAME_INFIX = "::relay::";
|
|
11
|
+
const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
|
|
5
12
|
|
|
6
13
|
const AUTH_BASE = "/_lunora/admin/auth";
|
|
7
14
|
const AUTH_ADMIN_ERROR_STATUS = {
|
|
15
|
+
INVITER_REQUIRED: 400,
|
|
16
|
+
ORG_SLUG_INVALID: 400,
|
|
17
|
+
ORG_SLUG_TAKEN: 409,
|
|
8
18
|
PASSWORD_TOO_LONG: 400,
|
|
9
19
|
PASSWORD_TOO_SHORT: 400,
|
|
10
20
|
USER_ALREADY_EXISTS: 409,
|
|
@@ -34,6 +44,23 @@ const parseRoleInput = (value) => {
|
|
|
34
44
|
return void 0;
|
|
35
45
|
};
|
|
36
46
|
const optionalBodyString = (body, field) => typeof body[field] === "string" ? body[field] : void 0;
|
|
47
|
+
const optionalBodyObject = (body, field) => {
|
|
48
|
+
const value = body[field];
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
50
|
+
};
|
|
51
|
+
const requirePermission = (body) => {
|
|
52
|
+
const value = body["permission"];
|
|
53
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
54
|
+
throw new LunoraError("`permission` object is required", { code: "BAD_REQUEST", status: 400 });
|
|
55
|
+
}
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [resource, actions] of Object.entries(value)) {
|
|
58
|
+
if (Array.isArray(actions) && actions.every((action) => typeof action === "string")) {
|
|
59
|
+
out[resource] = actions;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
37
64
|
const AUTH_ROUTES = {
|
|
38
65
|
[`${AUTH_BASE}/capabilities`]: {
|
|
39
66
|
build: () => {
|
|
@@ -100,6 +127,34 @@ const AUTH_ROUTES = {
|
|
|
100
127
|
http: "GET",
|
|
101
128
|
method: "listInvitations"
|
|
102
129
|
},
|
|
130
|
+
[`${AUTH_BASE}/config`]: {
|
|
131
|
+
build: () => {
|
|
132
|
+
return {};
|
|
133
|
+
},
|
|
134
|
+
http: "GET",
|
|
135
|
+
method: "config"
|
|
136
|
+
},
|
|
137
|
+
[`${AUTH_BASE}/organizations/teams`]: {
|
|
138
|
+
build: ({ paging, query }) => {
|
|
139
|
+
return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
|
|
140
|
+
},
|
|
141
|
+
http: "GET",
|
|
142
|
+
method: "listTeams"
|
|
143
|
+
},
|
|
144
|
+
[`${AUTH_BASE}/organizations/teams/members`]: {
|
|
145
|
+
build: ({ paging, query }) => {
|
|
146
|
+
return { ...paging, teamId: requireQuery$1(query, "teamId") };
|
|
147
|
+
},
|
|
148
|
+
http: "GET",
|
|
149
|
+
method: "listTeamMembers"
|
|
150
|
+
},
|
|
151
|
+
[`${AUTH_BASE}/organizations/roles`]: {
|
|
152
|
+
build: ({ paging, query }) => {
|
|
153
|
+
return { ...paging, organizationId: requireQuery$1(query, "organizationId") };
|
|
154
|
+
},
|
|
155
|
+
http: "GET",
|
|
156
|
+
method: "listOrgRoles"
|
|
157
|
+
},
|
|
103
158
|
// --- mutations (POST) -------------------------------------------------------
|
|
104
159
|
[`${AUTH_BASE}/users/create`]: {
|
|
105
160
|
build: ({ body }) => {
|
|
@@ -232,6 +287,137 @@ const AUTH_ROUTES = {
|
|
|
232
287
|
http: "POST",
|
|
233
288
|
method: "cancelInvitation",
|
|
234
289
|
returns: "void"
|
|
290
|
+
},
|
|
291
|
+
[`${AUTH_BASE}/organizations/create`]: {
|
|
292
|
+
build: ({ body }) => {
|
|
293
|
+
return {
|
|
294
|
+
logo: optionalBodyString(body, "logo"),
|
|
295
|
+
metadata: optionalBodyObject(body, "metadata"),
|
|
296
|
+
name: requireBodyString(body, "name"),
|
|
297
|
+
ownerId: optionalBodyString(body, "ownerId"),
|
|
298
|
+
slug: optionalBodyString(body, "slug")
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
http: "POST",
|
|
302
|
+
method: "createOrganization"
|
|
303
|
+
},
|
|
304
|
+
[`${AUTH_BASE}/organizations/update`]: {
|
|
305
|
+
build: ({ body }) => {
|
|
306
|
+
return {
|
|
307
|
+
logo: optionalBodyString(body, "logo"),
|
|
308
|
+
metadata: optionalBodyObject(body, "metadata"),
|
|
309
|
+
name: optionalBodyString(body, "name"),
|
|
310
|
+
organizationId: requireBodyString(body, "organizationId"),
|
|
311
|
+
slug: optionalBodyString(body, "slug")
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
http: "POST",
|
|
315
|
+
method: "updateOrganization"
|
|
316
|
+
},
|
|
317
|
+
[`${AUTH_BASE}/organizations/remove`]: {
|
|
318
|
+
build: ({ body }) => {
|
|
319
|
+
return { organizationId: requireBodyString(body, "organizationId") };
|
|
320
|
+
},
|
|
321
|
+
http: "POST",
|
|
322
|
+
method: "deleteOrganization",
|
|
323
|
+
returns: "void"
|
|
324
|
+
},
|
|
325
|
+
[`${AUTH_BASE}/organizations/members/add`]: {
|
|
326
|
+
build: ({ body }) => {
|
|
327
|
+
return {
|
|
328
|
+
organizationId: requireBodyString(body, "organizationId"),
|
|
329
|
+
role: optionalBodyString(body, "role"),
|
|
330
|
+
userId: requireBodyString(body, "userId")
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
http: "POST",
|
|
334
|
+
method: "addMember"
|
|
335
|
+
},
|
|
336
|
+
[`${AUTH_BASE}/organizations/members/invite`]: {
|
|
337
|
+
build: ({ body }) => {
|
|
338
|
+
return {
|
|
339
|
+
email: requireBodyString(body, "email"),
|
|
340
|
+
inviterId: optionalBodyString(body, "inviterId"),
|
|
341
|
+
organizationId: requireBodyString(body, "organizationId"),
|
|
342
|
+
role: optionalBodyString(body, "role")
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
http: "POST",
|
|
346
|
+
method: "inviteMember"
|
|
347
|
+
},
|
|
348
|
+
[`${AUTH_BASE}/organizations/members/role`]: {
|
|
349
|
+
build: ({ body }) => {
|
|
350
|
+
const role = parseRoleInput(body["role"]);
|
|
351
|
+
if (role === void 0 || typeof role === "string" && role.trim() === "") {
|
|
352
|
+
throw new LunoraError("`role` is required", { code: "BAD_REQUEST", status: 400 });
|
|
353
|
+
}
|
|
354
|
+
return { memberId: requireBodyString(body, "memberId"), role };
|
|
355
|
+
},
|
|
356
|
+
http: "POST",
|
|
357
|
+
method: "updateMemberRole"
|
|
358
|
+
},
|
|
359
|
+
[`${AUTH_BASE}/organizations/teams/create`]: {
|
|
360
|
+
build: ({ body }) => {
|
|
361
|
+
return { name: requireBodyString(body, "name"), organizationId: requireBodyString(body, "organizationId") };
|
|
362
|
+
},
|
|
363
|
+
http: "POST",
|
|
364
|
+
method: "createTeam"
|
|
365
|
+
},
|
|
366
|
+
[`${AUTH_BASE}/organizations/teams/update`]: {
|
|
367
|
+
build: ({ body }) => {
|
|
368
|
+
return { name: requireBodyString(body, "name"), teamId: requireBodyString(body, "teamId") };
|
|
369
|
+
},
|
|
370
|
+
http: "POST",
|
|
371
|
+
method: "updateTeam"
|
|
372
|
+
},
|
|
373
|
+
[`${AUTH_BASE}/organizations/teams/remove`]: {
|
|
374
|
+
build: ({ body }) => {
|
|
375
|
+
return { teamId: requireBodyString(body, "teamId") };
|
|
376
|
+
},
|
|
377
|
+
http: "POST",
|
|
378
|
+
method: "removeTeam",
|
|
379
|
+
returns: "void"
|
|
380
|
+
},
|
|
381
|
+
[`${AUTH_BASE}/organizations/teams/members/add`]: {
|
|
382
|
+
build: ({ body }) => {
|
|
383
|
+
return { teamId: requireBodyString(body, "teamId"), userId: requireBodyString(body, "userId") };
|
|
384
|
+
},
|
|
385
|
+
http: "POST",
|
|
386
|
+
method: "addTeamMember"
|
|
387
|
+
},
|
|
388
|
+
[`${AUTH_BASE}/organizations/teams/members/remove`]: {
|
|
389
|
+
build: ({ body }) => {
|
|
390
|
+
return { teamMemberId: requireBodyString(body, "teamMemberId") };
|
|
391
|
+
},
|
|
392
|
+
http: "POST",
|
|
393
|
+
method: "removeTeamMember",
|
|
394
|
+
returns: "void"
|
|
395
|
+
},
|
|
396
|
+
[`${AUTH_BASE}/organizations/roles/create`]: {
|
|
397
|
+
build: ({ body }) => {
|
|
398
|
+
return {
|
|
399
|
+
organizationId: requireBodyString(body, "organizationId"),
|
|
400
|
+
permission: requirePermission(body),
|
|
401
|
+
role: requireBodyString(body, "role")
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
http: "POST",
|
|
405
|
+
method: "createOrgRole"
|
|
406
|
+
},
|
|
407
|
+
[`${AUTH_BASE}/organizations/roles/update`]: {
|
|
408
|
+
build: ({ body }) => {
|
|
409
|
+
return { permission: requirePermission(body), roleId: requireBodyString(body, "roleId") };
|
|
410
|
+
},
|
|
411
|
+
http: "POST",
|
|
412
|
+
method: "updateOrgRole"
|
|
413
|
+
},
|
|
414
|
+
[`${AUTH_BASE}/organizations/roles/remove`]: {
|
|
415
|
+
build: ({ body }) => {
|
|
416
|
+
return { roleId: requireBodyString(body, "roleId") };
|
|
417
|
+
},
|
|
418
|
+
http: "POST",
|
|
419
|
+
method: "deleteOrgRole",
|
|
420
|
+
returns: "void"
|
|
235
421
|
}
|
|
236
422
|
};
|
|
237
423
|
const buildAuthAdminRoutes = (deps) => {
|
|
@@ -244,15 +430,15 @@ const buildAuthAdminRoutes = (deps) => {
|
|
|
244
430
|
}
|
|
245
431
|
const candidate = error;
|
|
246
432
|
const code = typeof candidate.code === "string" ? candidate.code : "AUTH_ADMIN_ERROR";
|
|
247
|
-
|
|
248
|
-
throw new LunoraError(
|
|
433
|
+
console.error("[lunora] auth admin operation failed:", error);
|
|
434
|
+
throw new LunoraError("auth admin operation failed", { code, status: AUTH_ADMIN_ERROR_STATUS[code] ?? 500 });
|
|
249
435
|
}
|
|
250
436
|
};
|
|
251
437
|
const handle = async (request, descriptor) => {
|
|
438
|
+
deps.assertAdmin(request);
|
|
252
439
|
if (request.method !== descriptor.http) {
|
|
253
440
|
throw new LunoraError(`Auth admin endpoint requires ${descriptor.http}`, { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
254
441
|
}
|
|
255
|
-
deps.assertAdmin(request);
|
|
256
442
|
const admin = deps.getAuthAdmin();
|
|
257
443
|
if (admin === void 0) {
|
|
258
444
|
throw new LunoraError("auth endpoints require an `authAdmin` on the worker", { code: "AUTH_NOT_CONFIGURED", status: 400 });
|
|
@@ -278,6 +464,45 @@ const buildAuthAdminRoutes = (deps) => {
|
|
|
278
464
|
return routes;
|
|
279
465
|
};
|
|
280
466
|
|
|
467
|
+
const MAX_BATCH_ENTRIES = 500;
|
|
468
|
+
|
|
469
|
+
const normalizeBatchCall = (raw, index, defaultShard) => {
|
|
470
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
471
|
+
throw new LunoraError("each batch call must be an object", { code: "BAD_REQUEST", status: 400 });
|
|
472
|
+
}
|
|
473
|
+
const call = raw;
|
|
474
|
+
if (typeof call.functionPath !== "string") {
|
|
475
|
+
throw new LunoraError("each batch call needs a string `functionPath`", { code: "BAD_REQUEST", status: 400 });
|
|
476
|
+
}
|
|
477
|
+
if (call.functionPath.startsWith("__lunora_relation__:") || call.functionPath.startsWith("__lunora_admin__")) {
|
|
478
|
+
throw new LunoraError("reserved function path cannot be batched", { code: "FORBIDDEN", status: 403 });
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
entry: {
|
|
482
|
+
args: call.args === void 0 ? {} : call.args,
|
|
483
|
+
clientId: typeof call.clientId === "string" ? call.clientId : void 0,
|
|
484
|
+
clientSeq: typeof call.clientSeq === "number" ? call.clientSeq : void 0,
|
|
485
|
+
functionPath: call.functionPath,
|
|
486
|
+
id: typeof call.id === "number" ? call.id : index,
|
|
487
|
+
mutationId: typeof call.mutationId === "string" ? call.mutationId : void 0
|
|
488
|
+
},
|
|
489
|
+
shardKey: typeof call.shardKey === "string" ? call.shardKey : defaultShard
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
const groupBatchCallsByShard = (calls, defaultShard) => {
|
|
493
|
+
if (calls.length > MAX_BATCH_ENTRIES) {
|
|
494
|
+
throw new LunoraError(`RPC batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit`, { code: "BAD_REQUEST", status: 400 });
|
|
495
|
+
}
|
|
496
|
+
const groups = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const [index, raw] of calls.entries()) {
|
|
498
|
+
const { entry, shardKey } = normalizeBatchCall(raw, index, defaultShard);
|
|
499
|
+
const group = groups.get(shardKey) ?? [];
|
|
500
|
+
group.push(entry);
|
|
501
|
+
groups.set(shardKey, group);
|
|
502
|
+
}
|
|
503
|
+
return groups;
|
|
504
|
+
};
|
|
505
|
+
|
|
281
506
|
const MAX_BODY_BYTES = 1048576;
|
|
282
507
|
const readBodyTextWithLimit = async (request, limit = MAX_BODY_BYTES) => {
|
|
283
508
|
if (!request.body) {
|
|
@@ -335,9 +560,9 @@ const readBodyBytesWithLimit = async (request, limit = MAX_BODY_BYTES) => {
|
|
|
335
560
|
}
|
|
336
561
|
return out.buffer;
|
|
337
562
|
};
|
|
338
|
-
const readJsonBodyWithLimit = async (request) => {
|
|
563
|
+
const readJsonBodyWithLimit = async (request, limit = MAX_BODY_BYTES) => {
|
|
339
564
|
try {
|
|
340
|
-
const text = await readBodyTextWithLimit(request);
|
|
565
|
+
const text = await readBodyTextWithLimit(request, limit);
|
|
341
566
|
return text === "" ? {} : JSON.parse(text);
|
|
342
567
|
} catch (error) {
|
|
343
568
|
if (error instanceof LunoraError) {
|
|
@@ -597,14 +822,14 @@ const partitionExportTables = (options, tables) => {
|
|
|
597
822
|
}
|
|
598
823
|
return { globalTables, shardLocalTables };
|
|
599
824
|
};
|
|
600
|
-
const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow) => {
|
|
825
|
+
const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace) => {
|
|
601
826
|
if (tables !== void 0 && shardLocalTables.length === 0) {
|
|
602
827
|
return;
|
|
603
828
|
}
|
|
604
829
|
const exportTables = tables === void 0 ? [] : shardLocalTables;
|
|
605
830
|
const probeFallback = tables === void 0 ? collectKnownTables() : [];
|
|
606
831
|
const probeTables = exportTables.length > 0 ? exportTables : probeFallback;
|
|
607
|
-
const result = await coordinator.orchestrateExport(
|
|
832
|
+
const result = await coordinator.orchestrateExport(namespace, {
|
|
608
833
|
args: { tables: exportTables },
|
|
609
834
|
headers: forwardedHeaders,
|
|
610
835
|
tables: probeTables
|
|
@@ -618,9 +843,9 @@ const exportShardLocalRows = async (options, coordinator, forwardedHeaders, tabl
|
|
|
618
843
|
}
|
|
619
844
|
}
|
|
620
845
|
};
|
|
621
|
-
const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow) => {
|
|
846
|
+
const streamExportRows = async (options, coordinator, forwardedHeaders, tables, writeRow, namespace) => {
|
|
622
847
|
const { globalTables, shardLocalTables } = partitionExportTables(options, tables);
|
|
623
|
-
await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow);
|
|
848
|
+
await exportShardLocalRows(options, coordinator, forwardedHeaders, tables, shardLocalTables, writeRow, namespace);
|
|
624
849
|
const exportGlobalsFunction = options.exportGlobals;
|
|
625
850
|
const wantGlobals = tables === void 0 || globalTables.length > 0;
|
|
626
851
|
if (wantGlobals && exportGlobalsFunction) {
|
|
@@ -740,7 +965,7 @@ const mergeImportResult = (totals, result) => {
|
|
|
740
965
|
}
|
|
741
966
|
totals.conflicts += result.conflicts;
|
|
742
967
|
};
|
|
743
|
-
const streamingImport = async (request, options, forwardedHeaders) => {
|
|
968
|
+
const streamingImport = async (request, options, forwardedHeaders, namespace) => {
|
|
744
969
|
const defaultShard = options.defaultShardKey ?? "__root__";
|
|
745
970
|
const { errors, globalRows, perShard } = await bucketImportStream(request, options, defaultShard);
|
|
746
971
|
const totals = { conflicts: 0, errors, inserted: {} };
|
|
@@ -749,7 +974,7 @@ const streamingImport = async (request, options, forwardedHeaders) => {
|
|
|
749
974
|
if (!coordinator) {
|
|
750
975
|
throw new LunoraError("Import endpoint requires a `queryCoordinator` on the worker", { code: "BAD_REQUEST", status: 400 });
|
|
751
976
|
}
|
|
752
|
-
const result = await coordinator.orchestrateImport(
|
|
977
|
+
const result = await coordinator.orchestrateImport(namespace, {
|
|
753
978
|
batches: [...perShard.values()],
|
|
754
979
|
headers: forwardedHeaders
|
|
755
980
|
});
|
|
@@ -966,7 +1191,129 @@ const buildIntrospectionAdminRoutes = (deps) => {
|
|
|
966
1191
|
};
|
|
967
1192
|
};
|
|
968
1193
|
|
|
969
|
-
const
|
|
1194
|
+
const KV_NAMESPACES_PATH = "/_lunora/admin/kv/namespaces";
|
|
1195
|
+
const KV_KEYS_PATH = "/_lunora/admin/kv/keys";
|
|
1196
|
+
const KV_VALUE_PATH = "/_lunora/admin/kv/value";
|
|
1197
|
+
const KV_VALUE_MAX_BODY_BYTES = 32 * 1048576;
|
|
1198
|
+
const KV_MIN_EXPIRATION_SECONDS = 60;
|
|
1199
|
+
const buildKvAdminRoutes = (deps) => {
|
|
1200
|
+
const { readJsonBody, requireAdminOption } = deps;
|
|
1201
|
+
const gate = (request) => requireAdminOption(request, deps.kvIntrospector, {
|
|
1202
|
+
code: "KV_NOT_CONFIGURED",
|
|
1203
|
+
message: "KV endpoints require a `kvIntrospector` on the worker"
|
|
1204
|
+
});
|
|
1205
|
+
const ok = (payload) => Response.json(payload, { headers: { "content-type": "application/json" }, status: 200 });
|
|
1206
|
+
const requireNamespaceAndKey = (request, verb) => {
|
|
1207
|
+
const url = new URL(request.url);
|
|
1208
|
+
const namespace = url.searchParams.get("namespace") ?? "";
|
|
1209
|
+
const key = url.searchParams.get("key") ?? "";
|
|
1210
|
+
if (namespace === "") {
|
|
1211
|
+
throw new LunoraError(`KV-value ${verb} request requires a \`namespace\` query parameter`, { code: "BAD_REQUEST", status: 400 });
|
|
1212
|
+
}
|
|
1213
|
+
if (key === "") {
|
|
1214
|
+
throw new LunoraError(`KV-value ${verb} request requires a \`key\` query parameter`, { code: "BAD_REQUEST", status: 400 });
|
|
1215
|
+
}
|
|
1216
|
+
return { key, namespace };
|
|
1217
|
+
};
|
|
1218
|
+
const requireKnownNamespace = async (introspector, namespace) => {
|
|
1219
|
+
const namespaces = await introspector.listNamespaces();
|
|
1220
|
+
if (!namespaces.some((entry) => entry.binding === namespace)) {
|
|
1221
|
+
throw new LunoraError(`Unknown KV namespace binding \`${namespace}\``, { code: "NOT_FOUND", status: 404 });
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
const handleKvNamespaces = async (request) => {
|
|
1225
|
+
if (request.method !== "GET") {
|
|
1226
|
+
throw new LunoraError("KV-namespaces endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
1227
|
+
}
|
|
1228
|
+
return ok({ namespaces: await gate(request).listNamespaces() });
|
|
1229
|
+
};
|
|
1230
|
+
const handleKvKeys = async (request) => {
|
|
1231
|
+
if (request.method !== "GET") {
|
|
1232
|
+
throw new LunoraError("KV-keys endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
1233
|
+
}
|
|
1234
|
+
const introspector = gate(request);
|
|
1235
|
+
const url = new URL(request.url);
|
|
1236
|
+
const namespace = url.searchParams.get("namespace") ?? "";
|
|
1237
|
+
if (namespace === "") {
|
|
1238
|
+
throw new LunoraError("KV-keys request requires a `namespace` query parameter", { code: "BAD_REQUEST", status: 400 });
|
|
1239
|
+
}
|
|
1240
|
+
const prefix = url.searchParams.get("prefix") ?? void 0;
|
|
1241
|
+
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
1242
|
+
const limitRaw = url.searchParams.get("limit");
|
|
1243
|
+
const parsedLimit = limitRaw === null ? void 0 : Number.parseInt(limitRaw, 10);
|
|
1244
|
+
if (parsedLimit !== void 0 && (!Number.isInteger(parsedLimit) || parsedLimit < 1)) {
|
|
1245
|
+
throw new LunoraError("KV-keys `limit` must be a positive integer", { code: "BAD_REQUEST", status: 400 });
|
|
1246
|
+
}
|
|
1247
|
+
const limit = parsedLimit === void 0 ? void 0 : Math.min(parsedLimit, 1e3);
|
|
1248
|
+
await requireKnownNamespace(introspector, namespace);
|
|
1249
|
+
return ok(await introspector.listKeys({ cursor, limit, namespace, prefix }));
|
|
1250
|
+
};
|
|
1251
|
+
const handleKvValueGet = async (request) => {
|
|
1252
|
+
const introspector = gate(request);
|
|
1253
|
+
const params = requireNamespaceAndKey(request, "GET");
|
|
1254
|
+
await requireKnownNamespace(introspector, params.namespace);
|
|
1255
|
+
return ok(await introspector.getValue(params));
|
|
1256
|
+
};
|
|
1257
|
+
const handleKvValuePut = async (request) => {
|
|
1258
|
+
const introspector = gate(request);
|
|
1259
|
+
const candidate = await readJsonBody(request, KV_VALUE_MAX_BODY_BYTES);
|
|
1260
|
+
if (typeof candidate.namespace !== "string" || candidate.namespace === "") {
|
|
1261
|
+
throw new LunoraError("KV-value PUT request requires a `namespace` string", { code: "BAD_REQUEST", status: 400 });
|
|
1262
|
+
}
|
|
1263
|
+
if (typeof candidate.key !== "string" || candidate.key === "") {
|
|
1264
|
+
throw new LunoraError("KV-value PUT request requires a `key` string", { code: "BAD_REQUEST", status: 400 });
|
|
1265
|
+
}
|
|
1266
|
+
if (typeof candidate.value !== "string") {
|
|
1267
|
+
throw new LunoraError("KV-value PUT request requires a `value` string", { code: "BAD_REQUEST", status: 400 });
|
|
1268
|
+
}
|
|
1269
|
+
if (candidate.expirationTtl !== void 0 && (typeof candidate.expirationTtl !== "number" || !Number.isInteger(candidate.expirationTtl) || candidate.expirationTtl < KV_MIN_EXPIRATION_SECONDS)) {
|
|
1270
|
+
throw new LunoraError("KV-value PUT `expirationTtl` must be an integer ≥ 60", { code: "BAD_REQUEST", status: 400 });
|
|
1271
|
+
}
|
|
1272
|
+
const minExpiration = Math.floor(Date.now() / 1e3) + KV_MIN_EXPIRATION_SECONDS;
|
|
1273
|
+
if (candidate.expiration !== void 0 && (typeof candidate.expiration !== "number" || !Number.isInteger(candidate.expiration) || candidate.expiration < minExpiration)) {
|
|
1274
|
+
throw new LunoraError("KV-value PUT `expiration` must be a Unix-seconds timestamp at least 60 seconds in the future", {
|
|
1275
|
+
code: "BAD_REQUEST",
|
|
1276
|
+
status: 400
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
await requireKnownNamespace(introspector, candidate.namespace);
|
|
1280
|
+
await introspector.putValue({
|
|
1281
|
+
expiration: candidate.expiration,
|
|
1282
|
+
expirationTtl: candidate.expirationTtl,
|
|
1283
|
+
key: candidate.key,
|
|
1284
|
+
metadata: candidate.metadata,
|
|
1285
|
+
namespace: candidate.namespace,
|
|
1286
|
+
value: candidate.value
|
|
1287
|
+
});
|
|
1288
|
+
return ok({ ok: true });
|
|
1289
|
+
};
|
|
1290
|
+
const handleKvValueDelete = async (request) => {
|
|
1291
|
+
const introspector = gate(request);
|
|
1292
|
+
const params = requireNamespaceAndKey(request, "DELETE");
|
|
1293
|
+
await requireKnownNamespace(introspector, params.namespace);
|
|
1294
|
+
await introspector.deleteKey(params);
|
|
1295
|
+
return ok({ deleted: true });
|
|
1296
|
+
};
|
|
1297
|
+
const kvValueHandlers = {
|
|
1298
|
+
DELETE: handleKvValueDelete,
|
|
1299
|
+
GET: handleKvValueGet,
|
|
1300
|
+
PUT: handleKvValuePut
|
|
1301
|
+
};
|
|
1302
|
+
const handleKvValue = (request) => {
|
|
1303
|
+
const handler = kvValueHandlers[request.method];
|
|
1304
|
+
if (!handler) {
|
|
1305
|
+
throw new LunoraError("KV-value endpoint requires GET, PUT, or DELETE", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
1306
|
+
}
|
|
1307
|
+
return handler(request);
|
|
1308
|
+
};
|
|
1309
|
+
return {
|
|
1310
|
+
[KV_NAMESPACES_PATH]: handleKvNamespaces,
|
|
1311
|
+
[KV_KEYS_PATH]: handleKvKeys,
|
|
1312
|
+
[KV_VALUE_PATH]: handleKvValue
|
|
1313
|
+
};
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const MIGRATE_PATH$1 = "/_lunora/migrate";
|
|
970
1317
|
const PITR_PATH = "/_lunora/admin/pitr";
|
|
971
1318
|
const RANK_PATH = "/_lunora/admin/rank";
|
|
972
1319
|
const RANKPAGE_PATH = "/_lunora/admin/rankpage";
|
|
@@ -1223,7 +1570,7 @@ const buildOrchestrationAdminRoutes = (deps) => {
|
|
|
1223
1570
|
return forwardToShard(shardDO, pitr.shardKey ?? defaultShard, forwarded);
|
|
1224
1571
|
};
|
|
1225
1572
|
return {
|
|
1226
|
-
[MIGRATE_PATH]: handleMigrate,
|
|
1573
|
+
[MIGRATE_PATH$1]: handleMigrate,
|
|
1227
1574
|
[PITR_PATH]: handlePitr,
|
|
1228
1575
|
[RANK_PATH]: handleRank,
|
|
1229
1576
|
[RANKPAGE_PATH]: handleRankPage,
|
|
@@ -1502,7 +1849,7 @@ const buildWorkflowsAdminRoutes = (deps) => {
|
|
|
1502
1849
|
assertAdmin(request);
|
|
1503
1850
|
const client = resolveWorkflowsClient(env);
|
|
1504
1851
|
if (!client) {
|
|
1505
|
-
return
|
|
1852
|
+
return Response.json({ configured: false, instances: [], page: 1, perPage: 0, totalCount: 0 });
|
|
1506
1853
|
}
|
|
1507
1854
|
const workflowName = requireQuery(url, "name");
|
|
1508
1855
|
const status = toInstanceStatus(url.searchParams.get("status"));
|
|
@@ -1554,9 +1901,14 @@ const buildWorkflowsAdminRoutes = (deps) => {
|
|
|
1554
1901
|
|
|
1555
1902
|
const NDJSON_ENCODER = new TextEncoder();
|
|
1556
1903
|
const RPC_PATH = "/_lunora/rpc";
|
|
1904
|
+
const RPC_BATCH_PATH = "/_lunora/rpc-batch";
|
|
1557
1905
|
const WS_PATH = "/_lunora/ws";
|
|
1558
1906
|
const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
|
|
1559
1907
|
const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
|
|
1908
|
+
const ADMIN_PATH_PREFIX = "/_lunora/admin/";
|
|
1909
|
+
const MIGRATE_PATH = "/_lunora/migrate";
|
|
1910
|
+
const STATUS_PATH = "/_lunora/status";
|
|
1911
|
+
const isAdminPath = (pathname) => pathname.startsWith(ADMIN_PATH_PREFIX) || pathname === MIGRATE_PATH;
|
|
1560
1912
|
const DEFAULT_AUTH_BASE_PATH = "/api/auth";
|
|
1561
1913
|
const RECORD_AUTH_EVENT_OP = "__lunora_admin__:recordAuthEvent";
|
|
1562
1914
|
const AUTH_ATTEMPT_SEGMENTS = ["/sign-in", "/sign-up", "/callback"];
|
|
@@ -1569,7 +1921,7 @@ const isAuthAttemptPath = (pathname, basePath) => {
|
|
|
1569
1921
|
return AUTH_ATTEMPT_SEGMENTS.some((segment) => suffix === segment || suffix.startsWith(`${segment}/`));
|
|
1570
1922
|
};
|
|
1571
1923
|
const buildErrorEvent = (functionPath, durationMs, error, extra) => {
|
|
1572
|
-
const mappable =
|
|
1924
|
+
const mappable = isLunoraError(error);
|
|
1573
1925
|
const code = mappable ? error.code : "INTERNAL_SERVER_ERROR";
|
|
1574
1926
|
const status = mappable ? error.status : 500;
|
|
1575
1927
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1598,6 +1950,8 @@ const resolveForwardContext = async (request, env, resolveIdentity) => {
|
|
|
1598
1950
|
const cookie = request.headers.get("cookie");
|
|
1599
1951
|
const bookmark = request.headers.get("x-d1-bookmark");
|
|
1600
1952
|
const mutationId = request.headers.get("x-lunora-mutation-id");
|
|
1953
|
+
const clientId = request.headers.get("x-lunora-client-id");
|
|
1954
|
+
const clientSeq = request.headers.get("x-lunora-client-seq");
|
|
1601
1955
|
if (authorization) {
|
|
1602
1956
|
headers["authorization"] = authorization;
|
|
1603
1957
|
}
|
|
@@ -1610,6 +1964,12 @@ const resolveForwardContext = async (request, env, resolveIdentity) => {
|
|
|
1610
1964
|
if (mutationId) {
|
|
1611
1965
|
headers["x-lunora-mutation-id"] = mutationId;
|
|
1612
1966
|
}
|
|
1967
|
+
if (clientId) {
|
|
1968
|
+
headers["x-lunora-client-id"] = clientId;
|
|
1969
|
+
}
|
|
1970
|
+
if (clientSeq) {
|
|
1971
|
+
headers["x-lunora-client-seq"] = clientSeq;
|
|
1972
|
+
}
|
|
1613
1973
|
const clientIp = request.headers.get("cf-connecting-ip");
|
|
1614
1974
|
if (clientIp) {
|
|
1615
1975
|
headers["x-lunora-client-ip"] = clientIp;
|
|
@@ -1662,6 +2022,12 @@ const validateFanOut = (fanOut) => {
|
|
|
1662
2022
|
}
|
|
1663
2023
|
return spec;
|
|
1664
2024
|
};
|
|
2025
|
+
const logRpcDebug = (env, envelope) => {
|
|
2026
|
+
if (!env?.LUNORA_DEBUG_RPC) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
console.warn(`[lunora:rpc] ${envelope.fanOut ? "fan-out" : `shard=${envelope.shardKey ?? "(root)"}`} ${envelope.functionPath}`);
|
|
2030
|
+
};
|
|
1665
2031
|
const parseEnvelope = async (request) => {
|
|
1666
2032
|
const text = await readBodyTextWithLimit(request);
|
|
1667
2033
|
let body;
|
|
@@ -1681,9 +2047,18 @@ const parseEnvelope = async (request) => {
|
|
|
1681
2047
|
throw new LunoraError("RPC `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
|
|
1682
2048
|
}
|
|
1683
2049
|
const envelope = body;
|
|
2050
|
+
const fanOut = validateFanOut(envelope.fanOut);
|
|
2051
|
+
const args = envelope.args ?? {};
|
|
2052
|
+
if (fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
|
|
2053
|
+
const requestedTable = args.table;
|
|
2054
|
+
if (typeof requestedTable === "string" && requestedTable !== fanOut.table) {
|
|
2055
|
+
throw new LunoraError("RPC `args.table` must match the authorized `fanOut.table` for a relation fan-out", { code: "BAD_REQUEST", status: 400 });
|
|
2056
|
+
}
|
|
2057
|
+
args.table = fanOut.table;
|
|
2058
|
+
}
|
|
1684
2059
|
return {
|
|
1685
|
-
args
|
|
1686
|
-
fanOut
|
|
2060
|
+
args,
|
|
2061
|
+
fanOut,
|
|
1687
2062
|
functionPath: envelope.functionPath,
|
|
1688
2063
|
shardKey: envelope.shardKey
|
|
1689
2064
|
};
|
|
@@ -1692,6 +2067,41 @@ const forwardToShard = async (namespace, shardKey, request) => {
|
|
|
1692
2067
|
const stub = resolveShard(namespace, shardKey);
|
|
1693
2068
|
return stub.fetch(request);
|
|
1694
2069
|
};
|
|
2070
|
+
const relayProbeCache = /* @__PURE__ */ new Map();
|
|
2071
|
+
const RELAY_PROBE_TTL_MS = 5e3;
|
|
2072
|
+
const probeRelayCount = async (namespace, shardKey) => {
|
|
2073
|
+
const now = Date.now();
|
|
2074
|
+
const cached = relayProbeCache.get(shardKey);
|
|
2075
|
+
if (cached !== void 0 && cached.expiresMs > now) {
|
|
2076
|
+
return cached.relayCount;
|
|
2077
|
+
}
|
|
2078
|
+
let relayCount = 0;
|
|
2079
|
+
try {
|
|
2080
|
+
const response = await resolveShard(namespace, shardKey).fetch(new Request("https://shard.internal/_lunora/route"));
|
|
2081
|
+
if (response.ok) {
|
|
2082
|
+
const body = await response.json();
|
|
2083
|
+
const reported = body.relayCount;
|
|
2084
|
+
if (typeof reported === "number" && reported > 0) {
|
|
2085
|
+
relayCount = Math.floor(reported);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
} catch {
|
|
2089
|
+
relayCount = 0;
|
|
2090
|
+
}
|
|
2091
|
+
relayProbeCache.set(shardKey, { expiresMs: now + RELAY_PROBE_TTL_MS, relayCount });
|
|
2092
|
+
return relayCount;
|
|
2093
|
+
};
|
|
2094
|
+
const resolveShardBindingName = (env, namespace) => {
|
|
2095
|
+
if (env === null || typeof env !== "object") {
|
|
2096
|
+
return void 0;
|
|
2097
|
+
}
|
|
2098
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2099
|
+
if (value === namespace) {
|
|
2100
|
+
return key;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return void 0;
|
|
2104
|
+
};
|
|
1695
2105
|
const constantTimeEqual = (expected, supplied) => {
|
|
1696
2106
|
const max = Math.max(expected.length, supplied.length);
|
|
1697
2107
|
let diff = expected.length ^ supplied.length;
|
|
@@ -1740,6 +2150,9 @@ const checkAdminWsToken = (request, expected) => {
|
|
|
1740
2150
|
};
|
|
1741
2151
|
const createWorker = (options) => {
|
|
1742
2152
|
const defaultShard = options.defaultShardKey ?? "__root__";
|
|
2153
|
+
const publicResolveIdentity = wrapResolverWithContract(options.resolveIdentity, options.identity);
|
|
2154
|
+
const shardDO = applyJurisdiction(options.shardDO, options.jurisdiction);
|
|
2155
|
+
const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
|
|
1743
2156
|
let envAdminToken;
|
|
1744
2157
|
const effectiveAdminToken = () => options.adminToken ?? envAdminToken;
|
|
1745
2158
|
const resolveAdminTokenFromEnv = (env) => {
|
|
@@ -1751,46 +2164,64 @@ const createWorker = (options) => {
|
|
|
1751
2164
|
envAdminToken = value;
|
|
1752
2165
|
}
|
|
1753
2166
|
};
|
|
1754
|
-
const
|
|
2167
|
+
const accessAdminGrants = /* @__PURE__ */ new WeakSet();
|
|
2168
|
+
const requestIsAdmin = (request) => checkAdminAuth(request, effectiveAdminToken()) || accessAdminGrants.has(request);
|
|
2169
|
+
const resolveAdminForwardContext = async (request, env) => {
|
|
2170
|
+
const context = await resolveForwardContext(request, env, options.resolveIdentity);
|
|
2171
|
+
if (accessAdminGrants.has(request) && context.headers["authorization"] === void 0) {
|
|
2172
|
+
const token = effectiveAdminToken();
|
|
2173
|
+
if (token !== void 0) {
|
|
2174
|
+
context.headers["authorization"] = `Bearer ${token}`;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return context;
|
|
2178
|
+
};
|
|
1755
2179
|
let warnedUnauthenticatedShardAccess = false;
|
|
1756
|
-
const
|
|
1757
|
-
if (
|
|
2180
|
+
const guardUnauthenticatedShardAccess = (kind) => {
|
|
2181
|
+
if (!options.allowUnauthenticatedShardAccess) {
|
|
2182
|
+
const callback = kind === "fan-out" ? "authorizeFanOut" : "authorizeShard";
|
|
2183
|
+
throw new LunoraError(
|
|
2184
|
+
`${kind} access is default-denied: configure \`${callback}\` on the worker, or set \`allowUnauthenticatedShardAccess: true\` to explicitly allow unauthenticated ${kind} access (relying solely on per-row RLS).`,
|
|
2185
|
+
{ code: kind === "fan-out" ? "FORBIDDEN_FANOUT" : "FORBIDDEN_SHARD", status: 403 }
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
if (warnedUnauthenticatedShardAccess) {
|
|
1758
2189
|
return;
|
|
1759
2190
|
}
|
|
1760
2191
|
warnedUnauthenticatedShardAccess = true;
|
|
1761
2192
|
console.warn(
|
|
1762
2193
|
[
|
|
1763
|
-
`[lunora] SECURITY:
|
|
2194
|
+
`[lunora] SECURITY: serving ${kind} access with \`allowUnauthenticatedShardAccess: true\` and no \`authorizeShard\`/\`authorizeFanOut\` — `,
|
|
1764
2195
|
`any caller (including unauthenticated ones) can target any shard / fan out across the table. `,
|
|
1765
|
-
`
|
|
2196
|
+
`This is safe only if every table is protected by per-row RLS. Configure \`authorizeShard\`/\`authorizeFanOut\` to gate it.`
|
|
1766
2197
|
].join("")
|
|
1767
2198
|
);
|
|
1768
2199
|
};
|
|
1769
2200
|
const orchestrationAdminRoutes = buildOrchestrationAdminRoutes({
|
|
1770
2201
|
defaultShard,
|
|
1771
2202
|
forwardToShard,
|
|
1772
|
-
isAdmin:
|
|
2203
|
+
isAdmin: requestIsAdmin,
|
|
1773
2204
|
queryCoordinator: options.queryCoordinator,
|
|
1774
|
-
resolveForwardContext:
|
|
1775
|
-
shardDO
|
|
2205
|
+
resolveForwardContext: resolveAdminForwardContext,
|
|
2206
|
+
shardDO
|
|
1776
2207
|
});
|
|
1777
|
-
const dispatchToShard = async (functionPath, args, shardKey) => {
|
|
2208
|
+
const dispatchToShard = async (functionPath, args, shardKey, mutationId) => {
|
|
1778
2209
|
if (options.authorizeShard) {
|
|
1779
2210
|
const allowed = await options.authorizeShard(null, shardKey);
|
|
1780
2211
|
if (!allowed) {
|
|
1781
2212
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
1782
2213
|
}
|
|
1783
2214
|
}
|
|
2215
|
+
const headers = { "content-type": "application/json", "x-lunora-system": "1" };
|
|
2216
|
+
if (mutationId !== void 0 && mutationId.length > 0) {
|
|
2217
|
+
headers["x-lunora-mutation-id"] = mutationId;
|
|
2218
|
+
}
|
|
1784
2219
|
const forwarded = new Request("https://shard.internal/rpc", {
|
|
1785
|
-
// `x-lunora-system` marks this as a trusted server-initiated dispatch
|
|
1786
|
-
// so the shard may run `internal` functions (scheduled/cron jobs are
|
|
1787
|
-
// typically internal). Authorization was already enforced above; this
|
|
1788
|
-
// header is set only here, never on the client RPC path.
|
|
1789
2220
|
body: JSON.stringify({ args, functionPath }),
|
|
1790
|
-
headers
|
|
2221
|
+
headers,
|
|
1791
2222
|
method: "POST"
|
|
1792
2223
|
});
|
|
1793
|
-
return forwardToShard(
|
|
2224
|
+
return forwardToShard(shardDO, shardKey, forwarded);
|
|
1794
2225
|
};
|
|
1795
2226
|
const startCronWorkflow = async (binding, job, env) => {
|
|
1796
2227
|
const candidate = env?.[binding];
|
|
@@ -1835,7 +2266,7 @@ const createWorker = (options) => {
|
|
|
1835
2266
|
}
|
|
1836
2267
|
};
|
|
1837
2268
|
const handleRunCronJob = async (request, env) => {
|
|
1838
|
-
if (!
|
|
2269
|
+
if (!requestIsAdmin(request)) {
|
|
1839
2270
|
throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
1840
2271
|
}
|
|
1841
2272
|
if (request.method !== "POST") {
|
|
@@ -1858,12 +2289,12 @@ const createWorker = (options) => {
|
|
|
1858
2289
|
};
|
|
1859
2290
|
const releasePoolSlot = async (candidate) => {
|
|
1860
2291
|
const pool = typeof candidate.pool === "string" && candidate.pool.length > 0 ? candidate.pool : void 0;
|
|
1861
|
-
if (!pool || !
|
|
2292
|
+
if (!pool || !schedulerDO || typeof candidate.id !== "string") {
|
|
1862
2293
|
return;
|
|
1863
2294
|
}
|
|
1864
2295
|
const instanceName = typeof candidate.instanceName === "string" && candidate.instanceName.length > 0 ? candidate.instanceName : "default";
|
|
1865
2296
|
try {
|
|
1866
|
-
await
|
|
2297
|
+
await schedulerDO.get(schedulerDO.idFromName(instanceName)).fetch(
|
|
1867
2298
|
new Request("https://scheduler.internal/complete", {
|
|
1868
2299
|
body: JSON.stringify({ id: candidate.id, pool }),
|
|
1869
2300
|
headers: { "content-type": "application/json" },
|
|
@@ -1903,23 +2334,24 @@ const createWorker = (options) => {
|
|
|
1903
2334
|
}
|
|
1904
2335
|
const args = candidate.args ?? {};
|
|
1905
2336
|
const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
|
|
1906
|
-
const
|
|
2337
|
+
const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
|
|
2338
|
+
const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId);
|
|
1907
2339
|
await releasePoolSlot(candidate);
|
|
1908
2340
|
return response;
|
|
1909
2341
|
};
|
|
1910
2342
|
const dataMovementAdminRoutes = buildDataMovementAdminRoutes({
|
|
1911
2343
|
applyGlobals: options.applyGlobals,
|
|
1912
|
-
isAdmin:
|
|
2344
|
+
isAdmin: requestIsAdmin,
|
|
1913
2345
|
knownTables: () => collectKnownTables(),
|
|
1914
2346
|
queryCoordinator: options.queryCoordinator,
|
|
1915
|
-
resolveForwardContext:
|
|
1916
|
-
shardDO
|
|
1917
|
-
streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow),
|
|
1918
|
-
streamingImport: (request, headers) => streamingImport(request, options, headers),
|
|
2347
|
+
resolveForwardContext: resolveAdminForwardContext,
|
|
2348
|
+
shardDO,
|
|
2349
|
+
streamExportRows: (coordinator, headers, tables, writeRow) => streamExportRows(options, coordinator, headers, tables, writeRow, shardDO),
|
|
2350
|
+
streamingImport: (request, headers) => streamingImport(request, options, headers, shardDO),
|
|
1919
2351
|
syncGlobals: options.syncGlobals
|
|
1920
2352
|
});
|
|
1921
2353
|
const assertAdminAuthorized = (request) => {
|
|
1922
|
-
if (!
|
|
2354
|
+
if (!requestIsAdmin(request)) {
|
|
1923
2355
|
throw new LunoraError("admin endpoint requires a valid admin bearer", { code: "ADMIN_FORBIDDEN", status: 403 });
|
|
1924
2356
|
}
|
|
1925
2357
|
};
|
|
@@ -1946,17 +2378,17 @@ const createWorker = (options) => {
|
|
|
1946
2378
|
};
|
|
1947
2379
|
};
|
|
1948
2380
|
const requireSchedulerNamespace = () => {
|
|
1949
|
-
if (
|
|
2381
|
+
if (schedulerDO === void 0) {
|
|
1950
2382
|
throw new LunoraError("scheduled endpoints require a `schedulerDO` namespace on the worker", { code: "SCHEDULER_NOT_CONFIGURED", status: 400 });
|
|
1951
2383
|
}
|
|
1952
|
-
return
|
|
2384
|
+
return schedulerDO;
|
|
1953
2385
|
};
|
|
1954
2386
|
const resolveSchedulerStub = (request) => {
|
|
1955
2387
|
assertAdminAuthorized(request);
|
|
1956
2388
|
return resolveShard(requireSchedulerNamespace(), options.schedulerInstanceName ?? "default");
|
|
1957
2389
|
};
|
|
1958
2390
|
const scheduledAdminRoutes = buildScheduledAdminRoutes({
|
|
1959
|
-
checkWsAdmin: (request) =>
|
|
2391
|
+
checkWsAdmin: (request) => requestIsAdmin(request) || checkAdminWsToken(request, effectiveAdminToken()),
|
|
1960
2392
|
requireSchedulerNamespace,
|
|
1961
2393
|
resolveSchedulerStub,
|
|
1962
2394
|
schedulerInstanceName: options.schedulerInstanceName ?? "default"
|
|
@@ -1984,6 +2416,11 @@ const createWorker = (options) => {
|
|
|
1984
2416
|
requireAdminOption,
|
|
1985
2417
|
vectorIntrospector: options.vectorIntrospector
|
|
1986
2418
|
});
|
|
2419
|
+
const kvAdminRoutes = buildKvAdminRoutes({
|
|
2420
|
+
kvIntrospector: options.kvIntrospector,
|
|
2421
|
+
readJsonBody: readJsonBodyWithLimit,
|
|
2422
|
+
requireAdminOption
|
|
2423
|
+
});
|
|
1987
2424
|
const introspectionAdminRoutes = buildIntrospectionAdminRoutes({
|
|
1988
2425
|
assertAdmin: assertAdminAuthorized,
|
|
1989
2426
|
options: {
|
|
@@ -1997,8 +2434,8 @@ const createWorker = (options) => {
|
|
|
1997
2434
|
queryParameter,
|
|
1998
2435
|
requireAdminOption
|
|
1999
2436
|
});
|
|
2000
|
-
const buildHttpActionContext = async (request, env) => {
|
|
2001
|
-
const { claims, headers, userId } = await resolveForwardContext(request, env,
|
|
2437
|
+
const buildHttpActionContext = async (request, env, context) => {
|
|
2438
|
+
const { claims, headers, userId } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2002
2439
|
const run = async (reference, args = {}) => {
|
|
2003
2440
|
const functionPath = reference.__lunoraRef;
|
|
2004
2441
|
if (typeof functionPath !== "string") {
|
|
@@ -2009,7 +2446,7 @@ const createWorker = (options) => {
|
|
|
2009
2446
|
headers,
|
|
2010
2447
|
method: "POST"
|
|
2011
2448
|
});
|
|
2012
|
-
const response = await forwardToShard(
|
|
2449
|
+
const response = await forwardToShard(shardDO, defaultShard, forwarded);
|
|
2013
2450
|
const payload = await response.json();
|
|
2014
2451
|
if (payload.error) {
|
|
2015
2452
|
throw new LunoraError(payload.error.message ?? "shard RPC failed", {
|
|
@@ -2024,6 +2461,7 @@ const createWorker = (options) => {
|
|
|
2024
2461
|
getIdentity: () => Promise.resolve(claims),
|
|
2025
2462
|
userId
|
|
2026
2463
|
},
|
|
2464
|
+
cache: context.cache,
|
|
2027
2465
|
fetch: globalThis.fetch.bind(globalThis),
|
|
2028
2466
|
runAction: run,
|
|
2029
2467
|
runMutation: run,
|
|
@@ -2034,7 +2472,7 @@ const createWorker = (options) => {
|
|
|
2034
2472
|
if (!options.httpRouter) {
|
|
2035
2473
|
return void 0;
|
|
2036
2474
|
}
|
|
2037
|
-
const httpContext = await buildHttpActionContext(request, env);
|
|
2475
|
+
const httpContext = await buildHttpActionContext(request, env, context);
|
|
2038
2476
|
try {
|
|
2039
2477
|
return await options.httpRouter.fetch(request, { ...env, __lunoraCtx: httpContext }, context);
|
|
2040
2478
|
} catch (error) {
|
|
@@ -2046,15 +2484,19 @@ const createWorker = (options) => {
|
|
|
2046
2484
|
if (request.headers.get("Upgrade") !== "websocket") {
|
|
2047
2485
|
throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
|
|
2048
2486
|
}
|
|
2487
|
+
const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
|
|
2488
|
+
if (blockedUpgrade) {
|
|
2489
|
+
return blockedUpgrade;
|
|
2490
|
+
}
|
|
2049
2491
|
const shardKey = url.searchParams.get("shard") ?? defaultShard;
|
|
2050
|
-
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env,
|
|
2492
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2051
2493
|
if (options.authorizeShard) {
|
|
2052
2494
|
const allowed = await options.authorizeShard(identity, shardKey);
|
|
2053
2495
|
if (!allowed) {
|
|
2054
2496
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
2055
2497
|
}
|
|
2056
2498
|
} else if (shardKey !== defaultShard) {
|
|
2057
|
-
|
|
2499
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2058
2500
|
}
|
|
2059
2501
|
const upgradeHeaders = new Headers(request.headers);
|
|
2060
2502
|
upgradeHeaders.delete("x-lunora-userid");
|
|
@@ -2072,31 +2514,45 @@ const createWorker = (options) => {
|
|
|
2072
2514
|
if (forwardedExp !== void 0) {
|
|
2073
2515
|
upgradeHeaders.set("x-lunora-identity-exp", forwardedExp);
|
|
2074
2516
|
}
|
|
2075
|
-
|
|
2517
|
+
const binding = resolveShardBindingName(env, options.shardDO);
|
|
2518
|
+
if (binding !== void 0) {
|
|
2519
|
+
upgradeHeaders.set("x-lunora-shard-binding", binding);
|
|
2520
|
+
const relayCount = await probeRelayCount(shardDO, shardKey);
|
|
2521
|
+
if (relayCount > 0) {
|
|
2522
|
+
const target = relayName(shardKey, Math.floor(Math.random() * relayCount));
|
|
2523
|
+
return forwardToShard(shardDO, target, new Request(request, { headers: upgradeHeaders }));
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
|
|
2527
|
+
};
|
|
2528
|
+
const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
|
|
2529
|
+
if (options.authorizeFanOut) {
|
|
2530
|
+
const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
|
|
2531
|
+
if (!allowed) {
|
|
2532
|
+
throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
|
|
2533
|
+
}
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
if (functionPath.startsWith("__lunora_relation__:")) {
|
|
2537
|
+
throw new LunoraError("reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker", {
|
|
2538
|
+
code: "FORBIDDEN_FANOUT",
|
|
2539
|
+
status: 403
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
if (options.authorizeShard) {
|
|
2543
|
+
throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
|
|
2544
|
+
code: "FORBIDDEN_FANOUT",
|
|
2545
|
+
status: 403
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
guardUnauthenticatedShardAccess("fan-out");
|
|
2076
2549
|
};
|
|
2077
2550
|
const authorizeRpcEnvelope = async (envelope, identity) => {
|
|
2551
|
+
if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_admin__:")) {
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2078
2554
|
if (envelope.fanOut) {
|
|
2079
|
-
|
|
2080
|
-
const allowed = await options.authorizeFanOut(identity, envelope.fanOut.table, envelope.functionPath);
|
|
2081
|
-
if (!allowed) {
|
|
2082
|
-
throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
|
|
2083
|
-
}
|
|
2084
|
-
} else if (envelope.functionPath.startsWith("__lunora_relation__:")) {
|
|
2085
|
-
throw new LunoraError(
|
|
2086
|
-
"reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker",
|
|
2087
|
-
{
|
|
2088
|
-
code: "FORBIDDEN_FANOUT",
|
|
2089
|
-
status: 403
|
|
2090
|
-
}
|
|
2091
|
-
);
|
|
2092
|
-
} else if (options.authorizeShard) {
|
|
2093
|
-
throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
|
|
2094
|
-
code: "FORBIDDEN_FANOUT",
|
|
2095
|
-
status: 403
|
|
2096
|
-
});
|
|
2097
|
-
} else {
|
|
2098
|
-
warnUnauthenticatedShardAccessOnce("fan-out");
|
|
2099
|
-
}
|
|
2555
|
+
await authorizeFanOutEnvelope(envelope.fanOut, envelope.functionPath, identity);
|
|
2100
2556
|
return;
|
|
2101
2557
|
}
|
|
2102
2558
|
if (options.authorizeShard) {
|
|
@@ -2106,7 +2562,7 @@ const createWorker = (options) => {
|
|
|
2106
2562
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
2107
2563
|
}
|
|
2108
2564
|
} else if (envelope.shardKey !== void 0 && envelope.shardKey !== defaultShard) {
|
|
2109
|
-
|
|
2565
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2110
2566
|
}
|
|
2111
2567
|
};
|
|
2112
2568
|
const dispatchSingleShard = async (functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
|
|
@@ -2118,7 +2574,7 @@ const createWorker = (options) => {
|
|
|
2118
2574
|
method: "POST"
|
|
2119
2575
|
});
|
|
2120
2576
|
try {
|
|
2121
|
-
const response = await forwardToShard(
|
|
2577
|
+
const response = await forwardToShard(shardDO, shardKey, forwarded);
|
|
2122
2578
|
emitRpcEvent(
|
|
2123
2579
|
observability,
|
|
2124
2580
|
{
|
|
@@ -2147,6 +2603,7 @@ const createWorker = (options) => {
|
|
|
2147
2603
|
throw new LunoraError("RPC endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
2148
2604
|
}
|
|
2149
2605
|
const envelope = await parseEnvelope(request);
|
|
2606
|
+
logRpcDebug(env, envelope);
|
|
2150
2607
|
if (envelope.fanOut && envelope.shardKey) {
|
|
2151
2608
|
throw new LunoraError("RPC envelope cannot set both `shardKey` and `fanOut`", { code: "BAD_REQUEST", status: 400 });
|
|
2152
2609
|
}
|
|
@@ -2162,14 +2619,14 @@ const createWorker = (options) => {
|
|
|
2162
2619
|
status: 400
|
|
2163
2620
|
});
|
|
2164
2621
|
}
|
|
2165
|
-
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env,
|
|
2622
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2166
2623
|
await authorizeRpcEnvelope(envelope, identity);
|
|
2167
2624
|
{
|
|
2168
2625
|
const rpcStartedAt = Date.now();
|
|
2169
2626
|
const { observability } = options;
|
|
2170
2627
|
const sinkContext = context ? {
|
|
2171
2628
|
waitUntil: (promise) => {
|
|
2172
|
-
context.waitUntil(promise);
|
|
2629
|
+
context.waitUntil?.(promise);
|
|
2173
2630
|
}
|
|
2174
2631
|
} : void 0;
|
|
2175
2632
|
if (envelope.fanOut) {
|
|
@@ -2181,7 +2638,7 @@ const createWorker = (options) => {
|
|
|
2181
2638
|
});
|
|
2182
2639
|
}
|
|
2183
2640
|
try {
|
|
2184
|
-
const result = await coordinator.fanOut(
|
|
2641
|
+
const result = await coordinator.fanOut(shardDO, {
|
|
2185
2642
|
args: envelope.args ?? {},
|
|
2186
2643
|
fanOut: envelope.fanOut,
|
|
2187
2644
|
functionPath: envelope.functionPath,
|
|
@@ -2218,13 +2675,132 @@ const createWorker = (options) => {
|
|
|
2218
2675
|
return dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
|
|
2219
2676
|
}
|
|
2220
2677
|
};
|
|
2678
|
+
const handleBatchRpc = async (request, env, context) => {
|
|
2679
|
+
if (request.method !== "POST") {
|
|
2680
|
+
throw new LunoraError("RPC batch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
2681
|
+
}
|
|
2682
|
+
const text = await readBodyTextWithLimit(request);
|
|
2683
|
+
let body;
|
|
2684
|
+
try {
|
|
2685
|
+
body = JSON.parse(text);
|
|
2686
|
+
} catch {
|
|
2687
|
+
throw new LunoraError("RPC batch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
|
|
2688
|
+
}
|
|
2689
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
2690
|
+
throw new LunoraError("RPC batch body must be an object", { code: "BAD_REQUEST", status: 400 });
|
|
2691
|
+
}
|
|
2692
|
+
const { calls } = body;
|
|
2693
|
+
if (!Array.isArray(calls)) {
|
|
2694
|
+
throw new LunoraError("RPC batch `calls` must be an array", { code: "BAD_REQUEST", status: 400 });
|
|
2695
|
+
}
|
|
2696
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
|
|
2697
|
+
const groups = groupBatchCallsByShard(calls, defaultShard);
|
|
2698
|
+
await Promise.all(
|
|
2699
|
+
[...groups.entries()].flatMap(
|
|
2700
|
+
([shardKey, entries]) => entries.map((entry) => authorizeRpcEnvelope({ functionPath: entry.functionPath, shardKey }, identity))
|
|
2701
|
+
)
|
|
2702
|
+
);
|
|
2703
|
+
const { observability } = options;
|
|
2704
|
+
const sinkContext = context ? {
|
|
2705
|
+
waitUntil: (promise) => {
|
|
2706
|
+
context.waitUntil?.(promise);
|
|
2707
|
+
}
|
|
2708
|
+
} : void 0;
|
|
2709
|
+
const results = [];
|
|
2710
|
+
let latestBookmark;
|
|
2711
|
+
const slotError = (entry, status, code, message) => {
|
|
2712
|
+
return { body: { error: { code, message } }, id: entry.id, status };
|
|
2713
|
+
};
|
|
2714
|
+
const failSubBatch = (entries, status, code, message, eventFor) => {
|
|
2715
|
+
for (const entry of entries) {
|
|
2716
|
+
emitRpcEvent(observability, eventFor(entry), sinkContext);
|
|
2717
|
+
results.push(slotError(entry, status, code, message));
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2720
|
+
const emitEntryEvents = (entries, shardKey, durationMs, statusById, fallbackStatus) => {
|
|
2721
|
+
for (const entry of entries) {
|
|
2722
|
+
const status = statusById.get(entry.id) ?? fallbackStatus;
|
|
2723
|
+
const ok = status < 400;
|
|
2724
|
+
emitRpcEvent(
|
|
2725
|
+
observability,
|
|
2726
|
+
{
|
|
2727
|
+
durationMs,
|
|
2728
|
+
functionPath: entry.functionPath,
|
|
2729
|
+
ok,
|
|
2730
|
+
shardKey,
|
|
2731
|
+
...ok ? {} : { error: { code: "SHARD_ERROR", message: `batched call returned ${String(status)}`, status } }
|
|
2732
|
+
},
|
|
2733
|
+
sinkContext
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
await Promise.all(
|
|
2738
|
+
[...groups.entries()].map(async ([shardKey, entries]) => {
|
|
2739
|
+
const headers = new Headers(forwardedHeaders);
|
|
2740
|
+
headers.set("content-type", "application/json");
|
|
2741
|
+
const subRequest = new Request("https://shard.internal/rpc-batch", { body: JSON.stringify({ calls: entries }), headers, method: "POST" });
|
|
2742
|
+
const subStartedAt = Date.now();
|
|
2743
|
+
let response;
|
|
2744
|
+
try {
|
|
2745
|
+
response = await forwardToShard(shardDO, shardKey, subRequest);
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
const durationMs2 = Date.now() - subStartedAt;
|
|
2748
|
+
const { body: errorBody } = toErrorBody(error, { fallbackCode: "SHARD_UNAVAILABLE", redactedMessage: "shard unavailable" });
|
|
2749
|
+
failSubBatch(
|
|
2750
|
+
entries,
|
|
2751
|
+
502,
|
|
2752
|
+
errorBody.code,
|
|
2753
|
+
errorBody.message,
|
|
2754
|
+
(entry) => buildErrorEvent(entry.functionPath, durationMs2, error, { shardKey })
|
|
2755
|
+
);
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const durationMs = Date.now() - subStartedAt;
|
|
2759
|
+
const bookmark = response.headers.get("x-d1-bookmark");
|
|
2760
|
+
if (bookmark) {
|
|
2761
|
+
latestBookmark = bookmark;
|
|
2762
|
+
}
|
|
2763
|
+
let parsed;
|
|
2764
|
+
try {
|
|
2765
|
+
parsed = await response.json();
|
|
2766
|
+
} catch {
|
|
2767
|
+
const message = `shard batch returned a non-JSON response (${String(response.status)})`;
|
|
2768
|
+
failSubBatch(entries, response.status, "SHARD_ERROR", message, (entry) => {
|
|
2769
|
+
return {
|
|
2770
|
+
durationMs,
|
|
2771
|
+
error: { code: "SHARD_ERROR", message, status: response.status },
|
|
2772
|
+
functionPath: entry.functionPath,
|
|
2773
|
+
ok: false,
|
|
2774
|
+
shardKey
|
|
2775
|
+
};
|
|
2776
|
+
});
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
const entryResults = Array.isArray(parsed.results) ? parsed.results : [];
|
|
2780
|
+
const statusById = new Map(entryResults.map((entry) => [entry.id, entry.status ?? response.status]));
|
|
2781
|
+
const seenIds = new Set(entryResults.map((entry) => entry.id));
|
|
2782
|
+
emitEntryEvents(entries, shardKey, durationMs, statusById, response.status);
|
|
2783
|
+
results.push(...entryResults);
|
|
2784
|
+
for (const entry of entries) {
|
|
2785
|
+
if (!seenIds.has(entry.id)) {
|
|
2786
|
+
results.push(slotError(entry, response.status, "SHARD_ERROR", `shard batch omitted result for call ${String(entry.id)}`));
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
})
|
|
2790
|
+
);
|
|
2791
|
+
const responseHeaders = { "content-type": "application/json" };
|
|
2792
|
+
if (latestBookmark !== void 0) {
|
|
2793
|
+
responseHeaders["x-d1-bookmark"] = latestBookmark;
|
|
2794
|
+
}
|
|
2795
|
+
return Response.json({ results }, { headers: responseHeaders, status: 200 });
|
|
2796
|
+
};
|
|
2221
2797
|
const serverQuery = async (request, env, reference, args = {}, callOptions = {}) => {
|
|
2222
2798
|
try {
|
|
2223
2799
|
const functionPath = reference.__lunoraRef;
|
|
2224
2800
|
if (typeof functionPath !== "string") {
|
|
2225
2801
|
throw new LunoraError("serverQuery: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
|
|
2226
2802
|
}
|
|
2227
|
-
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env,
|
|
2803
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2228
2804
|
await authorizeRpcEnvelope({ functionPath, shardKey: callOptions.shardKey }, identity);
|
|
2229
2805
|
const shardKey = callOptions.shardKey ?? defaultShard;
|
|
2230
2806
|
return await dispatchSingleShard(functionPath, args, shardKey, forwardedHeaders);
|
|
@@ -2290,7 +2866,7 @@ const createWorker = (options) => {
|
|
|
2290
2866
|
streamController.enqueue(encoded);
|
|
2291
2867
|
};
|
|
2292
2868
|
try {
|
|
2293
|
-
await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow);
|
|
2869
|
+
await streamExportRows(options, coordinator, forwardedHeaders, tables, writeRow, shardDO);
|
|
2294
2870
|
streamController.close();
|
|
2295
2871
|
} catch (error) {
|
|
2296
2872
|
streamError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -2359,7 +2935,7 @@ const createWorker = (options) => {
|
|
|
2359
2935
|
headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
|
|
2360
2936
|
method: "POST"
|
|
2361
2937
|
});
|
|
2362
|
-
await forwardToShard(
|
|
2938
|
+
await forwardToShard(shardDO, defaultShard, recordRequest);
|
|
2363
2939
|
} catch {
|
|
2364
2940
|
}
|
|
2365
2941
|
};
|
|
@@ -2373,13 +2949,21 @@ const createWorker = (options) => {
|
|
|
2373
2949
|
}
|
|
2374
2950
|
const basePath = options.authBasePath ?? DEFAULT_AUTH_BASE_PATH;
|
|
2375
2951
|
if (isAuthAttemptPath(url.pathname, basePath)) {
|
|
2376
|
-
context.waitUntil(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
|
|
2952
|
+
context.waitUntil?.(recordAuthAttempt(env, authResponse.status >= 400 ? "fail" : "ok"));
|
|
2377
2953
|
}
|
|
2378
2954
|
return authResponse;
|
|
2379
2955
|
};
|
|
2956
|
+
const customRoutes = options.routes !== void 0 && Object.keys(options.routes).length > 0 ? options.routes : void 0;
|
|
2380
2957
|
const internalRoutes = {
|
|
2958
|
+
[STATUS_PATH]: (request) => {
|
|
2959
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
2960
|
+
return new Response(void 0, { headers: { allow: "GET, HEAD" }, status: 405 });
|
|
2961
|
+
}
|
|
2962
|
+
return Response.json({ ok: true }, { headers: { "cache-control": "no-store" } });
|
|
2963
|
+
},
|
|
2381
2964
|
[WS_PATH]: (request, env, url) => handleWebSocketUpgrade(request, env, url),
|
|
2382
2965
|
[RPC_PATH]: (request, env, _url, context) => handleRpc(request, env, context),
|
|
2966
|
+
[RPC_BATCH_PATH]: (request, env, _url, context) => handleBatchRpc(request, env, context),
|
|
2383
2967
|
[SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
|
|
2384
2968
|
[CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
|
|
2385
2969
|
// Extracted handler clusters built above, merged in (mirroring the auth
|
|
@@ -2393,6 +2977,7 @@ const createWorker = (options) => {
|
|
|
2393
2977
|
...workflowsAdminRoutes,
|
|
2394
2978
|
...storageAdminRoutes,
|
|
2395
2979
|
...vectorAdminRoutes,
|
|
2980
|
+
...kvAdminRoutes,
|
|
2396
2981
|
...introspectionAdminRoutes,
|
|
2397
2982
|
// `/_lunora/admin/auth/*` — the whole user-management plane, one route per
|
|
2398
2983
|
// `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
|
|
@@ -2413,6 +2998,17 @@ const createWorker = (options) => {
|
|
|
2413
2998
|
resolvedSecurity = resolveSecurity(options.security, env ?? {});
|
|
2414
2999
|
}
|
|
2415
3000
|
};
|
|
3001
|
+
const applyAdminGate = async (request, pathname) => {
|
|
3002
|
+
if (options.adminGate === void 0 || !isAdminPath(pathname)) {
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
try {
|
|
3006
|
+
if (await options.adminGate(request)) {
|
|
3007
|
+
accessAdminGrants.add(request);
|
|
3008
|
+
}
|
|
3009
|
+
} catch {
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
2416
3012
|
const handle = async (request, env, context) => {
|
|
2417
3013
|
const url = new URL(request.url);
|
|
2418
3014
|
if (request.method === "POST" || request.method === "PUT") {
|
|
@@ -2425,13 +3021,16 @@ const createWorker = (options) => {
|
|
|
2425
3021
|
if (authResponse) {
|
|
2426
3022
|
return authResponse;
|
|
2427
3023
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
3024
|
+
if (customRoutes) {
|
|
3025
|
+
const methodAndPath = `${request.method} ${url.pathname}`;
|
|
3026
|
+
const route = customRoutes[methodAndPath] ?? customRoutes[url.pathname];
|
|
3027
|
+
if (route) {
|
|
3028
|
+
return route(request, env, context);
|
|
3029
|
+
}
|
|
2432
3030
|
}
|
|
2433
3031
|
const internalRoute = internalRoutes[url.pathname];
|
|
2434
3032
|
if (internalRoute) {
|
|
3033
|
+
await applyAdminGate(request, url.pathname);
|
|
2435
3034
|
return internalRoute(request, env, url, context);
|
|
2436
3035
|
}
|
|
2437
3036
|
const httpRouteResponse = await dispatchHttpRoute(request, env, context);
|
|
@@ -2443,7 +3042,7 @@ const createWorker = (options) => {
|
|
|
2443
3042
|
return {
|
|
2444
3043
|
async fetch(request, env, context) {
|
|
2445
3044
|
if (options.passThroughOnException) {
|
|
2446
|
-
context.passThroughOnException();
|
|
3045
|
+
context.passThroughOnException?.();
|
|
2447
3046
|
}
|
|
2448
3047
|
ensureSecurityResolved(env);
|
|
2449
3048
|
resolveAdminTokenFromEnv(env);
|
|
@@ -2462,6 +3061,9 @@ const createWorker = (options) => {
|
|
|
2462
3061
|
return decorateResponse(toErrorResponse(error), request, resolvedSecurity);
|
|
2463
3062
|
}
|
|
2464
3063
|
},
|
|
3064
|
+
async queue(batch, env, context) {
|
|
3065
|
+
await options.queue?.(batch, env, context);
|
|
3066
|
+
},
|
|
2465
3067
|
async scheduled(controller, env, context) {
|
|
2466
3068
|
await handleScheduled(controller, env, context);
|
|
2467
3069
|
},
|
|
@@ -2492,10 +3094,24 @@ const withFrameworkWorker = (host, optionsInput) => {
|
|
|
2492
3094
|
const optionsFactory = optionsInput;
|
|
2493
3095
|
return {
|
|
2494
3096
|
fetch: (request, env, context) => build(optionsFactory(env)).fetch(request, env, context),
|
|
3097
|
+
queue: (batch, env, context) => build(optionsFactory(env)).queue?.(batch, env, context) ?? Promise.resolve(),
|
|
2495
3098
|
scheduled: (controller, env, context) => build(optionsFactory(env)).scheduled(controller, env, context),
|
|
2496
3099
|
serverQuery: (request, env, reference, args, options) => build(optionsFactory(env)).serverQuery(request, env, reference, args, options)
|
|
2497
3100
|
};
|
|
2498
3101
|
};
|
|
3102
|
+
const resolveLunoraOptions = (options, env) => {
|
|
3103
|
+
if (typeof options === "function") {
|
|
3104
|
+
return options(env);
|
|
3105
|
+
}
|
|
3106
|
+
const shardDO = options.shardDO ?? env?.SHARD;
|
|
3107
|
+
if (!shardDO) {
|
|
3108
|
+
throw new LunoraError(
|
|
3109
|
+
"@lunora/runtime: no shard Durable Object namespace found. Bind `SHARD` in wrangler.jsonc, or pass `createLunoraHandler({ shardDO: env.MY_SHARD })`."
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
return { ...options, shardDO };
|
|
3113
|
+
};
|
|
3114
|
+
const createLunoraHandler = (options = {}) => (request, env, context) => createWorker(resolveLunoraOptions(options, env)).fetch(request, env, context ?? NOOP_EXECUTION_CONTEXT);
|
|
2499
3115
|
const defineRpcEnvelope = (envelope) => envelope;
|
|
2500
3116
|
|
|
2501
|
-
export { composeWorker, createWorker, defineRpcEnvelope, withFrameworkWorker };
|
|
3117
|
+
export { NOOP_EXECUTION_CONTEXT, composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker };
|