@lunora/do 1.0.0-alpha.23 → 1.0.0-alpha.24
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/dist/index.d.mts +0 -73
- package/dist/index.d.ts +0 -73
- package/dist/index.mjs +4 -4
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-BM4TOOvf.mjs → ROOT_DO_SIZE_WARN_BYTES-KQQiPDQ3.mjs} +19 -19
- package/dist/packem_shared/{SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs → SESSION_DO_TTL_DEFAULT-D71QAL5h.mjs} +15 -17
- package/dist/packem_shared/{SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs → SHARD_REGISTRY_DO_NAME-D99roc-r.mjs} +12 -14
- package/dist/packem_shared/{exportShardRows-DZEhUeyI.mjs → exportShardRows-Dy3oFZ26.mjs} +4 -3
- package/dist/packem_shared/json-response-BdbtpOhm.mjs +3 -0
- package/package.json +2 -2
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/dist/index.d.mts
CHANGED
|
@@ -3806,43 +3806,6 @@ declare const MIN_AUTH_SECRET_LENGTH = 32;
|
|
|
3806
3806
|
* a *missing* token is never itself a finding here (introspection is simply off).
|
|
3807
3807
|
*/
|
|
3808
3808
|
declare const buildSecurityAudit: (rawEnv: unknown) => SecurityAuditResult;
|
|
3809
|
-
/**
|
|
3810
|
-
* Durable Object that owns auth session state.
|
|
3811
|
-
*
|
|
3812
|
-
* `@lunora/auth` used to write sessions directly into D1 alongside user
|
|
3813
|
-
* records. That worked but coupled session lifecycle to a global database —
|
|
3814
|
-
* every read had to cross the region, every write contended with user
|
|
3815
|
-
* inserts. SessionDO owns sessions in a DO-local KV store: same-prefix
|
|
3816
|
-
* tokens co-locate (via `idFromName(token.slice(0, 16))`) so the DO instance
|
|
3817
|
-
* count stays bounded; reads and writes never round-trip to D1.
|
|
3818
|
-
*
|
|
3819
|
-
* Wire shape: HTTP only, never RPC. The auth package calls
|
|
3820
|
-
*
|
|
3821
|
-
* `await env.SESSION.get(env.SESSION.idFromName(prefix)).fetch(...)`
|
|
3822
|
-
*
|
|
3823
|
-
* with one of:
|
|
3824
|
-
*
|
|
3825
|
-
* POST /create body: { token, userId, ttlSeconds }
|
|
3826
|
-
* GET /get header: `x-lunora-session-token: <token>`
|
|
3827
|
-
* DELETE /revoke header: `x-lunora-session-token: <token>`
|
|
3828
|
-
*
|
|
3829
|
-
* Every request must additionally carry an `x-lunora-session-secret` header
|
|
3830
|
-
* whose value matches `env.SESSION_DO_SECRET`. The DO is reachable from any
|
|
3831
|
-
* worker bound to its namespace, so a shared secret is the only thing that
|
|
3832
|
-
* prevents a compromised or misbehaving worker from reading arbitrary
|
|
3833
|
-
* sessions — the binding alone is not an auth surface.
|
|
3834
|
-
*
|
|
3835
|
-
* The DO returns JSON bodies that `@lunora/auth` reshapes into its public
|
|
3836
|
-
* `AuthSession` type. Keep the surface narrow — anything more elaborate
|
|
3837
|
-
* should ride on top via a wrapper, not by widening this contract.
|
|
3838
|
-
*
|
|
3839
|
-
* # Subclassing
|
|
3840
|
-
*
|
|
3841
|
-
* Apps subclass `SessionDO` (or use the codegen subclass) and register the
|
|
3842
|
-
* subclass in `wrangler.jsonc` as `SESSION`. The platform DO binding requires
|
|
3843
|
-
* a concrete `DurableObject` class today; the structural state shape used by
|
|
3844
|
-
* the unit tests is preserved so plain-object doubles still work.
|
|
3845
|
-
*/
|
|
3846
3809
|
/** Default TTL for new sessions (7 days), matching `@lunora/auth`. */
|
|
3847
3810
|
declare const SESSION_DO_TTL_DEFAULT: number;
|
|
3848
3811
|
/** Hard ceiling on the requested TTL — 90 days. Longer sessions should ride on top via refresh. */
|
|
@@ -6453,42 +6416,6 @@ declare abstract class ShardDO {
|
|
|
6453
6416
|
private deliverWhisperLocal;
|
|
6454
6417
|
private readAttachment;
|
|
6455
6418
|
}
|
|
6456
|
-
/**
|
|
6457
|
-
* Durable Object that owns the live set of shard keys per sharded table.
|
|
6458
|
-
*
|
|
6459
|
-
* The query coordinator (`@lunora/runtime`) fans out cross-shard reads to
|
|
6460
|
-
* every live shard. With the static registry, the app supplies the shard
|
|
6461
|
-
* key list at boot — which is fine for fixed-cardinality deployments
|
|
6462
|
-
* (a known set of tenants) and unworkable for dynamic ones (one shard per
|
|
6463
|
-
* user-created channel, organisation, project, …).
|
|
6464
|
-
*
|
|
6465
|
-
* `ShardRegistryDO` is the persistent source of truth. A worker:
|
|
6466
|
-
*
|
|
6467
|
-
* - calls `POST /register {table, shardKey}` when a sharded table first
|
|
6468
|
-
* sees a write on a new key (typically from `ctx.db.<table>.insert` via
|
|
6469
|
-
* the worker's onWrite hook, fired through `ctx.waitUntil` so the
|
|
6470
|
-
* user-facing write doesn't pay the registry round-trip);
|
|
6471
|
-
* - calls `POST /unregister {table, shardKey}` when a shard is decommissioned;
|
|
6472
|
-
* - calls `GET /list?table=X` to materialise the fan-out target list. The
|
|
6473
|
-
* client (`createDynamicShardRegistry` in `@lunora/runtime`) caches the
|
|
6474
|
-
* answer with a small TTL so a wide fan-out doesn't pay a registry
|
|
6475
|
-
* round-trip on every call.
|
|
6476
|
-
*
|
|
6477
|
-
* Single-instance contract: deploy one DO instance per environment, by
|
|
6478
|
-
* convention named {@link SHARD_REGISTRY_DO_NAME}. The DO is small (just a
|
|
6479
|
-
* `Map<table, Set<shardKey>>`) and writes are infrequent (only on first-seen
|
|
6480
|
-
* shardKey per table), so a single instance is sufficient up to tens of
|
|
6481
|
-
* thousands of distinct shard keys.
|
|
6482
|
-
*
|
|
6483
|
-
* Wire shape: HTTP only, never RPC.
|
|
6484
|
-
*
|
|
6485
|
-
* POST /register body: { table, shardKey }
|
|
6486
|
-
* POST /unregister body: { table, shardKey }
|
|
6487
|
-
* GET /list?table=...
|
|
6488
|
-
* GET /snapshot (debug: returns the full table → [keys] map)
|
|
6489
|
-
*
|
|
6490
|
-
* Responses are JSON; the client shapes them. Keep the surface narrow.
|
|
6491
|
-
*/
|
|
6492
6419
|
/** Conventional DO instance name, passed to `idFromName` to address the single registry instance. */
|
|
6493
6420
|
declare const SHARD_REGISTRY_DO_NAME: string;
|
|
6494
6421
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -3806,43 +3806,6 @@ declare const MIN_AUTH_SECRET_LENGTH = 32;
|
|
|
3806
3806
|
* a *missing* token is never itself a finding here (introspection is simply off).
|
|
3807
3807
|
*/
|
|
3808
3808
|
declare const buildSecurityAudit: (rawEnv: unknown) => SecurityAuditResult;
|
|
3809
|
-
/**
|
|
3810
|
-
* Durable Object that owns auth session state.
|
|
3811
|
-
*
|
|
3812
|
-
* `@lunora/auth` used to write sessions directly into D1 alongside user
|
|
3813
|
-
* records. That worked but coupled session lifecycle to a global database —
|
|
3814
|
-
* every read had to cross the region, every write contended with user
|
|
3815
|
-
* inserts. SessionDO owns sessions in a DO-local KV store: same-prefix
|
|
3816
|
-
* tokens co-locate (via `idFromName(token.slice(0, 16))`) so the DO instance
|
|
3817
|
-
* count stays bounded; reads and writes never round-trip to D1.
|
|
3818
|
-
*
|
|
3819
|
-
* Wire shape: HTTP only, never RPC. The auth package calls
|
|
3820
|
-
*
|
|
3821
|
-
* `await env.SESSION.get(env.SESSION.idFromName(prefix)).fetch(...)`
|
|
3822
|
-
*
|
|
3823
|
-
* with one of:
|
|
3824
|
-
*
|
|
3825
|
-
* POST /create body: { token, userId, ttlSeconds }
|
|
3826
|
-
* GET /get header: `x-lunora-session-token: <token>`
|
|
3827
|
-
* DELETE /revoke header: `x-lunora-session-token: <token>`
|
|
3828
|
-
*
|
|
3829
|
-
* Every request must additionally carry an `x-lunora-session-secret` header
|
|
3830
|
-
* whose value matches `env.SESSION_DO_SECRET`. The DO is reachable from any
|
|
3831
|
-
* worker bound to its namespace, so a shared secret is the only thing that
|
|
3832
|
-
* prevents a compromised or misbehaving worker from reading arbitrary
|
|
3833
|
-
* sessions — the binding alone is not an auth surface.
|
|
3834
|
-
*
|
|
3835
|
-
* The DO returns JSON bodies that `@lunora/auth` reshapes into its public
|
|
3836
|
-
* `AuthSession` type. Keep the surface narrow — anything more elaborate
|
|
3837
|
-
* should ride on top via a wrapper, not by widening this contract.
|
|
3838
|
-
*
|
|
3839
|
-
* # Subclassing
|
|
3840
|
-
*
|
|
3841
|
-
* Apps subclass `SessionDO` (or use the codegen subclass) and register the
|
|
3842
|
-
* subclass in `wrangler.jsonc` as `SESSION`. The platform DO binding requires
|
|
3843
|
-
* a concrete `DurableObject` class today; the structural state shape used by
|
|
3844
|
-
* the unit tests is preserved so plain-object doubles still work.
|
|
3845
|
-
*/
|
|
3846
3809
|
/** Default TTL for new sessions (7 days), matching `@lunora/auth`. */
|
|
3847
3810
|
declare const SESSION_DO_TTL_DEFAULT: number;
|
|
3848
3811
|
/** Hard ceiling on the requested TTL — 90 days. Longer sessions should ride on top via refresh. */
|
|
@@ -6453,42 +6416,6 @@ declare abstract class ShardDO {
|
|
|
6453
6416
|
private deliverWhisperLocal;
|
|
6454
6417
|
private readAttachment;
|
|
6455
6418
|
}
|
|
6456
|
-
/**
|
|
6457
|
-
* Durable Object that owns the live set of shard keys per sharded table.
|
|
6458
|
-
*
|
|
6459
|
-
* The query coordinator (`@lunora/runtime`) fans out cross-shard reads to
|
|
6460
|
-
* every live shard. With the static registry, the app supplies the shard
|
|
6461
|
-
* key list at boot — which is fine for fixed-cardinality deployments
|
|
6462
|
-
* (a known set of tenants) and unworkable for dynamic ones (one shard per
|
|
6463
|
-
* user-created channel, organisation, project, …).
|
|
6464
|
-
*
|
|
6465
|
-
* `ShardRegistryDO` is the persistent source of truth. A worker:
|
|
6466
|
-
*
|
|
6467
|
-
* - calls `POST /register {table, shardKey}` when a sharded table first
|
|
6468
|
-
* sees a write on a new key (typically from `ctx.db.<table>.insert` via
|
|
6469
|
-
* the worker's onWrite hook, fired through `ctx.waitUntil` so the
|
|
6470
|
-
* user-facing write doesn't pay the registry round-trip);
|
|
6471
|
-
* - calls `POST /unregister {table, shardKey}` when a shard is decommissioned;
|
|
6472
|
-
* - calls `GET /list?table=X` to materialise the fan-out target list. The
|
|
6473
|
-
* client (`createDynamicShardRegistry` in `@lunora/runtime`) caches the
|
|
6474
|
-
* answer with a small TTL so a wide fan-out doesn't pay a registry
|
|
6475
|
-
* round-trip on every call.
|
|
6476
|
-
*
|
|
6477
|
-
* Single-instance contract: deploy one DO instance per environment, by
|
|
6478
|
-
* convention named {@link SHARD_REGISTRY_DO_NAME}. The DO is small (just a
|
|
6479
|
-
* `Map<table, Set<shardKey>>`) and writes are infrequent (only on first-seen
|
|
6480
|
-
* shardKey per table), so a single instance is sufficient up to tens of
|
|
6481
|
-
* thousands of distinct shard keys.
|
|
6482
|
-
*
|
|
6483
|
-
* Wire shape: HTTP only, never RPC.
|
|
6484
|
-
*
|
|
6485
|
-
* POST /register body: { table, shardKey }
|
|
6486
|
-
* POST /unregister body: { table, shardKey }
|
|
6487
|
-
* GET /list?table=...
|
|
6488
|
-
* GET /snapshot (debug: returns the full table → [keys] map)
|
|
6489
|
-
*
|
|
6490
|
-
* Responses are JSON; the client shapes them. Keep the surface narrow.
|
|
6491
|
-
*/
|
|
6492
6419
|
/** Conventional DO instance name, passed to `idFromName` to address the single registry instance. */
|
|
6493
6420
|
declare const SHARD_REGISTRY_DO_NAME: string;
|
|
6494
6421
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { exportShardRows, exportShardTable, importShardRows, parseExportShardArgs, parseImportShardArgs, selectExportTables, validateImportRow } from './packem_shared/exportShardRows-
|
|
1
|
+
export { exportShardRows, exportShardTable, importShardRows, parseExportShardArgs, parseImportShardArgs, selectExportTables, validateImportRow } from './packem_shared/exportShardRows-Dy3oFZ26.mjs';
|
|
2
2
|
export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, normalizeCountArgument, throwingScheduler } from './packem_shared/AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs';
|
|
3
3
|
export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
|
|
4
4
|
export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
|
|
@@ -25,9 +25,9 @@ export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from
|
|
|
25
25
|
export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-DnjkqVgY.mjs';
|
|
26
26
|
export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
|
|
27
27
|
export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
|
|
28
|
-
export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-
|
|
29
|
-
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-
|
|
30
|
-
export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-
|
|
28
|
+
export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-D71QAL5h.mjs';
|
|
29
|
+
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-KQQiPDQ3.mjs';
|
|
30
|
+
export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
|
|
31
31
|
export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-dDcFE1YZ.mjs';
|
|
32
32
|
export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
|
|
33
33
|
export { ConflictError } from './packem_shared/ConflictError-CLoq37xH.mjs';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { LunoraError, toErrorBody } from '@lunora/errors';
|
|
2
2
|
import { drizzle } from 'drizzle-orm/durable-sqlite';
|
|
3
|
+
import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
|
|
3
4
|
import { e as encodeWire, a as awaitWsDrain, t as trySendFrame, d as decodeWire, s as subscriptionListDeltas, b as sendDeltaFrames } from './subscription-delivery-CK8qga-k.mjs';
|
|
4
|
-
import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-
|
|
5
|
+
import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-Dy3oFZ26.mjs';
|
|
5
6
|
import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
|
|
6
7
|
import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-DB3IYUR3.mjs';
|
|
7
8
|
import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
|
|
@@ -764,7 +765,8 @@ class OwnerRelay extends RelayLink {
|
|
|
764
765
|
try {
|
|
765
766
|
resolved = this.host.resolveShape(request.name, request.args, identity);
|
|
766
767
|
} catch (error) {
|
|
767
|
-
|
|
768
|
+
const { body } = toErrorBody(error, { fallbackCode: "SHAPE_RESOLVE_FAILED", redactedMessage: "shape resolution failed" });
|
|
769
|
+
return { error: { code: body.code, message: body.message } };
|
|
768
770
|
}
|
|
769
771
|
if (resolved === void 0 || resolved.global === true) {
|
|
770
772
|
return { error: { code: "SHAPE_NOT_FOUND", message: `shape not relayable: ${request.name}` } };
|
|
@@ -1842,13 +1844,7 @@ const parseCdcSyncArgs = (args) => {
|
|
|
1842
1844
|
};
|
|
1843
1845
|
return { limit: toCount(args["limit"]), sinceSeq: toCount(args["sinceSeq"]) ?? 0 };
|
|
1844
1846
|
};
|
|
1845
|
-
const
|
|
1846
|
-
const headers = { "content-type": "application/json" };
|
|
1847
|
-
if (bookmark) {
|
|
1848
|
-
headers["x-d1-bookmark"] = bookmark;
|
|
1849
|
-
}
|
|
1850
|
-
return Response.json(body, { headers, status });
|
|
1851
|
-
};
|
|
1847
|
+
const bookmarkHeaders = (bookmark) => bookmark ? { "x-d1-bookmark": bookmark } : void 0;
|
|
1852
1848
|
const parseIdentityHeader = (raw) => {
|
|
1853
1849
|
if (!raw) {
|
|
1854
1850
|
return void 0;
|
|
@@ -2394,7 +2390,7 @@ class ShardDO {
|
|
|
2394
2390
|
try {
|
|
2395
2391
|
if (payload.functionPath.startsWith(RELATION_FUNCTION_PREFIX)) {
|
|
2396
2392
|
const value = await this.runRelationFanoutRead(payload.functionPath, payload.args ?? {});
|
|
2397
|
-
return jsonResponse(value, 200, this.currentResponseBookmark);
|
|
2393
|
+
return jsonResponse(value, 200, bookmarkHeaders(this.currentResponseBookmark));
|
|
2398
2394
|
}
|
|
2399
2395
|
const mutatorClass = this.isCustomMutator(payload.functionPath) ? this.classifyClientMutation() : void 0;
|
|
2400
2396
|
this.currentMutatorClass = mutatorClass;
|
|
@@ -3439,7 +3435,7 @@ class ShardDO {
|
|
|
3439
3435
|
}
|
|
3440
3436
|
this.recordFunctionCall(functionPath, Date.now() - dispatchStartedAt, void 0, this.currentScannedTables, this.currentIndexHits);
|
|
3441
3437
|
if (mutatorClass.kind === "already") {
|
|
3442
|
-
return jsonResponse({ lastMutationId: mutatorClass.expected - 1, result: null }, 200, this.currentResponseBookmark);
|
|
3438
|
+
return jsonResponse({ lastMutationId: mutatorClass.expected - 1, result: null }, 200, bookmarkHeaders(this.currentResponseBookmark));
|
|
3443
3439
|
}
|
|
3444
3440
|
return jsonResponse(
|
|
3445
3441
|
{
|
|
@@ -3450,7 +3446,7 @@ class ShardDO {
|
|
|
3450
3446
|
}
|
|
3451
3447
|
},
|
|
3452
3448
|
409,
|
|
3453
|
-
this.currentResponseBookmark
|
|
3449
|
+
bookmarkHeaders(this.currentResponseBookmark)
|
|
3454
3450
|
);
|
|
3455
3451
|
}
|
|
3456
3452
|
/**
|
|
@@ -3469,7 +3465,11 @@ class ShardDO {
|
|
|
3469
3465
|
return this.buildDispatchResponse(mutatorClass, cachedValue);
|
|
3470
3466
|
}
|
|
3471
3467
|
const commitCursor = this.mutationCommitCursor();
|
|
3472
|
-
return jsonResponse(
|
|
3468
|
+
return jsonResponse(
|
|
3469
|
+
commitCursor === void 0 ? { result: cachedValue } : { commitCursor, result: cachedValue },
|
|
3470
|
+
200,
|
|
3471
|
+
bookmarkHeaders(this.currentResponseBookmark)
|
|
3472
|
+
);
|
|
3473
3473
|
}
|
|
3474
3474
|
/**
|
|
3475
3475
|
* The CDC cursor a just-committed plain mutation landed at — the post-write
|
|
@@ -3493,10 +3493,10 @@ class ShardDO {
|
|
|
3493
3493
|
*/
|
|
3494
3494
|
buildDispatchResponse(mutatorClass, result) {
|
|
3495
3495
|
if (mutatorClass?.kind === "next") {
|
|
3496
|
-
return jsonResponse({ lastMutationId: this.currentRequestClientSeq, result }, 200, this.currentResponseBookmark);
|
|
3496
|
+
return jsonResponse({ lastMutationId: this.currentRequestClientSeq, result }, 200, bookmarkHeaders(this.currentResponseBookmark));
|
|
3497
3497
|
}
|
|
3498
3498
|
const commitCursor = this.mutationCommitCursor();
|
|
3499
|
-
return jsonResponse(commitCursor === void 0 ? { result } : { commitCursor, result }, 200, this.currentResponseBookmark);
|
|
3499
|
+
return jsonResponse(commitCursor === void 0 ? { result } : { commitCursor, result }, 200, bookmarkHeaders(this.currentResponseBookmark));
|
|
3500
3500
|
}
|
|
3501
3501
|
/**
|
|
3502
3502
|
* Commit a mutation's replay bookkeeping — the `(identity, mutationId)`
|
|
@@ -4255,7 +4255,7 @@ class ShardDO {
|
|
|
4255
4255
|
}
|
|
4256
4256
|
results.push({ body: outcome.body, id: outcome.id, status: outcome.status });
|
|
4257
4257
|
}
|
|
4258
|
-
return jsonResponse({ results }, 200, latestBookmark);
|
|
4258
|
+
return jsonResponse({ results }, 200, bookmarkHeaders(latestBookmark));
|
|
4259
4259
|
}
|
|
4260
4260
|
/** Dispatch one batch entry through the single-call `/rpc` path and capture its envelope (plan 088). */
|
|
4261
4261
|
async dispatchBatchEntry(batchRequest, entry) {
|
|
@@ -4263,12 +4263,12 @@ class ShardDO {
|
|
|
4263
4263
|
const response = await this.fetch(buildBatchEntryRequest(batchRequest, entry));
|
|
4264
4264
|
return { body: await response.json(), bookmark: response.headers.get("x-d1-bookmark") ?? void 0, id: entry.id, status: response.status };
|
|
4265
4265
|
} catch (error) {
|
|
4266
|
-
const
|
|
4266
|
+
const { body, status } = toErrorBody(error, { fallbackCode: "BATCH_ENTRY_FAILED" });
|
|
4267
4267
|
return {
|
|
4268
|
-
body: { error:
|
|
4268
|
+
body: { error: body },
|
|
4269
4269
|
bookmark: void 0,
|
|
4270
4270
|
id: entry?.id,
|
|
4271
|
-
status
|
|
4271
|
+
status
|
|
4272
4272
|
};
|
|
4273
4273
|
}
|
|
4274
4274
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
|
|
2
|
+
|
|
1
3
|
const SESSION_DO_TTL_DEFAULT = 7 * 24 * 60 * 60;
|
|
2
4
|
const SESSION_DO_TTL_MAX = 90 * 24 * 60 * 60;
|
|
3
5
|
const SESSION_GC_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -7,10 +9,6 @@ const SESSION_TOKEN_PATTERN = /^[\w-]+$/;
|
|
|
7
9
|
const MIN_TOKEN_LENGTH = 32;
|
|
8
10
|
const MAX_TOKEN_LENGTH = 256;
|
|
9
11
|
const MAX_USER_ID_LENGTH = 256;
|
|
10
|
-
const jsonResponse = (status, body) => Response.json(body, {
|
|
11
|
-
headers: { "content-type": "application/json" },
|
|
12
|
-
status
|
|
13
|
-
});
|
|
14
12
|
const constantTimeEqual = (a, b) => {
|
|
15
13
|
const max = Math.max(a.length, b.length);
|
|
16
14
|
let diff = a.length ^ b.length;
|
|
@@ -68,7 +66,7 @@ class SessionDO {
|
|
|
68
66
|
async fetch(request) {
|
|
69
67
|
const env = this.env ?? {};
|
|
70
68
|
if (!isAuthorized(request, env)) {
|
|
71
|
-
return jsonResponse(
|
|
69
|
+
return jsonResponse({ error: { code: "UNAUTHORIZED", message: "missing or invalid SessionDO secret" } }, 401);
|
|
72
70
|
}
|
|
73
71
|
const url = new URL(request.url);
|
|
74
72
|
if (request.method === "POST" && url.pathname === "/create") {
|
|
@@ -80,7 +78,7 @@ class SessionDO {
|
|
|
80
78
|
if (request.method === "DELETE" && url.pathname === "/revoke") {
|
|
81
79
|
return this.handleRevoke(request);
|
|
82
80
|
}
|
|
83
|
-
return jsonResponse(
|
|
81
|
+
return jsonResponse({ error: { code: "NOT_FOUND", message: "no such session route" } }, 404);
|
|
84
82
|
}
|
|
85
83
|
/**
|
|
86
84
|
* Sweep expired session records. Lazy expiry-on-read ({@link handleGet})
|
|
@@ -116,25 +114,25 @@ class SessionDO {
|
|
|
116
114
|
try {
|
|
117
115
|
body = await request.json();
|
|
118
116
|
} catch {
|
|
119
|
-
return jsonResponse(
|
|
117
|
+
return jsonResponse({ error: "invalid_request" }, 400);
|
|
120
118
|
}
|
|
121
119
|
const token = validateToken(body.token);
|
|
122
120
|
if (token === void 0) {
|
|
123
|
-
return jsonResponse(
|
|
121
|
+
return jsonResponse({ error: "invalid_request" }, 400);
|
|
124
122
|
}
|
|
125
123
|
const { userId } = body;
|
|
126
124
|
if (typeof userId !== "string" || userId.length === 0 || userId.length > MAX_USER_ID_LENGTH) {
|
|
127
|
-
return jsonResponse(
|
|
125
|
+
return jsonResponse({ error: "invalid_request" }, 400);
|
|
128
126
|
}
|
|
129
127
|
const ttlSeconds = resolveTtlSeconds(body.ttlSeconds);
|
|
130
128
|
if (ttlSeconds === void 0) {
|
|
131
|
-
return jsonResponse(
|
|
129
|
+
return jsonResponse({ error: "invalid_request" }, 400);
|
|
132
130
|
}
|
|
133
131
|
const now = Date.now();
|
|
134
132
|
const record = { createdAt: now, expiresAt: now + ttlSeconds * 1e3, userId };
|
|
135
133
|
await this.state.storage.put(`s:${token}`, record);
|
|
136
134
|
await this.armGcAlarm();
|
|
137
|
-
return jsonResponse(
|
|
135
|
+
return jsonResponse({ token, ...record }, 201);
|
|
138
136
|
}
|
|
139
137
|
/**
|
|
140
138
|
* Ensure a GC alarm is pending. Only sets one when none is currently
|
|
@@ -155,25 +153,25 @@ class SessionDO {
|
|
|
155
153
|
async handleGet(request) {
|
|
156
154
|
const token = request.headers.get(SESSION_TOKEN_HEADER);
|
|
157
155
|
if (!token) {
|
|
158
|
-
return jsonResponse(
|
|
156
|
+
return jsonResponse({ error: { code: "INVALID_INPUT", message: "token required" } }, 400);
|
|
159
157
|
}
|
|
160
158
|
const record = await this.state.storage.get(`s:${token}`);
|
|
161
159
|
if (!record) {
|
|
162
|
-
return jsonResponse(
|
|
160
|
+
return jsonResponse({ error: { code: "NOT_FOUND", message: "session not found" } }, 404);
|
|
163
161
|
}
|
|
164
162
|
if (record.expiresAt < Date.now()) {
|
|
165
163
|
await this.state.storage.delete(`s:${token}`);
|
|
166
|
-
return jsonResponse(
|
|
164
|
+
return jsonResponse({ error: { code: "EXPIRED", message: "session expired" } }, 404);
|
|
167
165
|
}
|
|
168
|
-
return jsonResponse(
|
|
166
|
+
return jsonResponse({ token, ...record }, 200);
|
|
169
167
|
}
|
|
170
168
|
async handleRevoke(request) {
|
|
171
169
|
const token = request.headers.get(SESSION_TOKEN_HEADER);
|
|
172
170
|
if (!token) {
|
|
173
|
-
return jsonResponse(
|
|
171
|
+
return jsonResponse({ error: { code: "INVALID_INPUT", message: "token required" } }, 400);
|
|
174
172
|
}
|
|
175
173
|
await this.state.storage.delete(`s:${token}`);
|
|
176
|
-
return jsonResponse(
|
|
174
|
+
return jsonResponse({ ok: true }, 200);
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
177
|
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
+
import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
|
|
2
|
+
|
|
1
3
|
const SHARD_REGISTRY_DO_NAME = "__lunora_shard_registry__";
|
|
2
4
|
const STORAGE_KEY = "__tables__";
|
|
3
|
-
const jsonResponse = (status, body) => Response.json(body, {
|
|
4
|
-
headers: { "content-type": "application/json" },
|
|
5
|
-
status
|
|
6
|
-
});
|
|
7
5
|
const readTableShardBody = async (request) => {
|
|
8
6
|
let body;
|
|
9
7
|
try {
|
|
10
8
|
body = await request.json();
|
|
11
9
|
} catch {
|
|
12
|
-
return { kind: "error", response: jsonResponse(
|
|
10
|
+
return { kind: "error", response: jsonResponse({ error: { code: "BAD_REQUEST", message: "invalid JSON body" } }, 400) };
|
|
13
11
|
}
|
|
14
12
|
const table = typeof body.table === "string" ? body.table.trim() : "";
|
|
15
13
|
const shardKey = typeof body.shardKey === "string" ? body.shardKey.trim() : "";
|
|
16
14
|
if (!table || !shardKey) {
|
|
17
15
|
return {
|
|
18
16
|
kind: "error",
|
|
19
|
-
response: jsonResponse(
|
|
17
|
+
response: jsonResponse({ error: { code: "BAD_REQUEST", message: "table and shardKey required" } }, 400)
|
|
20
18
|
};
|
|
21
19
|
}
|
|
22
20
|
return { kind: "ok", value: { shardKey, table } };
|
|
@@ -56,7 +54,7 @@ class ShardRegistryDO {
|
|
|
56
54
|
if (request.method === "GET" && url.pathname === "/snapshot") {
|
|
57
55
|
return this.handleSnapshot();
|
|
58
56
|
}
|
|
59
|
-
return jsonResponse(
|
|
57
|
+
return jsonResponse({ error: { code: "NOT_FOUND", message: `unknown shard-registry route ${request.method} ${url.pathname}` } }, 404);
|
|
60
58
|
}
|
|
61
59
|
/**
|
|
62
60
|
* Load the persisted snapshot exactly once. `blockConcurrencyWhile`
|
|
@@ -83,9 +81,9 @@ class ShardRegistryDO {
|
|
|
83
81
|
handleList(url) {
|
|
84
82
|
const table = url.searchParams.get("table");
|
|
85
83
|
if (!table) {
|
|
86
|
-
return jsonResponse(
|
|
84
|
+
return jsonResponse({ error: { code: "BAD_REQUEST", message: "missing required query parameter: table" } }, 400);
|
|
87
85
|
}
|
|
88
|
-
return jsonResponse(
|
|
86
|
+
return jsonResponse({ shardKeys: [...this.tables.get(table) ?? []] }, 200);
|
|
89
87
|
}
|
|
90
88
|
async handleRegister(request) {
|
|
91
89
|
const parsed = await readTableShardBody(request);
|
|
@@ -100,11 +98,11 @@ class ShardRegistryDO {
|
|
|
100
98
|
this.tables.set(table, set);
|
|
101
99
|
}
|
|
102
100
|
if (set.has(shardKey)) {
|
|
103
|
-
return jsonResponse(
|
|
101
|
+
return jsonResponse({ changed: false, ok: true }, 200);
|
|
104
102
|
}
|
|
105
103
|
set.add(shardKey);
|
|
106
104
|
await this.persist();
|
|
107
|
-
return jsonResponse(
|
|
105
|
+
return jsonResponse({ changed: true, ok: true }, 200);
|
|
108
106
|
});
|
|
109
107
|
}
|
|
110
108
|
handleSnapshot() {
|
|
@@ -112,7 +110,7 @@ class ShardRegistryDO {
|
|
|
112
110
|
for (const [table, set] of this.tables) {
|
|
113
111
|
out[table] = [...set];
|
|
114
112
|
}
|
|
115
|
-
return jsonResponse(
|
|
113
|
+
return jsonResponse({ tables: out }, 200);
|
|
116
114
|
}
|
|
117
115
|
async handleUnregister(request) {
|
|
118
116
|
const parsed = await readTableShardBody(request);
|
|
@@ -123,14 +121,14 @@ class ShardRegistryDO {
|
|
|
123
121
|
return this.state.blockConcurrencyWhile(async () => {
|
|
124
122
|
const set = this.tables.get(table);
|
|
125
123
|
if (!set?.has(shardKey)) {
|
|
126
|
-
return jsonResponse(
|
|
124
|
+
return jsonResponse({ changed: false, ok: true }, 200);
|
|
127
125
|
}
|
|
128
126
|
set.delete(shardKey);
|
|
129
127
|
if (set.size === 0) {
|
|
130
128
|
this.tables.delete(table);
|
|
131
129
|
}
|
|
132
130
|
await this.persist();
|
|
133
|
-
return jsonResponse(
|
|
131
|
+
return jsonResponse({ changed: true, ok: true }, 200);
|
|
134
132
|
});
|
|
135
133
|
}
|
|
136
134
|
/** Serialize the in-memory map to a single JSON-safe object and put. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toErrorBody } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const DEFAULT_BATCH_SIZE = 200;
|
|
2
4
|
const selectExportTables = (schema, requested) => {
|
|
3
5
|
const isShardLocal = (table) => {
|
|
@@ -107,9 +109,8 @@ const importOneRow = async (writer, schema, row, line) => {
|
|
|
107
109
|
await writer.insert(table, doc, { allowExplicitId: true });
|
|
108
110
|
return { kind: "inserted", table };
|
|
109
111
|
} catch (error) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
return { error: { code, line, message, table }, kind: "error" };
|
|
112
|
+
const { body } = toErrorBody(error, { fallbackCode: "INSERT_FAILED" });
|
|
113
|
+
return { error: { code: body.code, line, message: body.message, table }, kind: "error" };
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
116
|
const importShardRows = async (writer, schema, args) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/do",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.24",
|
|
4
4
|
"description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.2",
|
|
50
50
|
"@visulima/redact": "3.0.0-alpha.14",
|
|
51
51
|
"drizzle-orm": "^0.45.2"
|
|
52
52
|
},
|