@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 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: &lt;token>`
3827
- * DELETE /revoke header: `x-lunora-session-token: &lt;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.&lt;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&lt;table, Set&lt;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: &lt;token>`
3827
- * DELETE /revoke header: `x-lunora-session-token: &lt;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.&lt;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&lt;table, Set&lt;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-DZEhUeyI.mjs';
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-ilPZsVwu.mjs';
29
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-BM4TOOvf.mjs';
30
- export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs';
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-DZEhUeyI.mjs';
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
- return { error: { code: "SHAPE_RESOLVE_FAILED", message: error instanceof Error ? error.message : "shape resolve failed" } };
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 jsonResponse = (body, status = 200, bookmark) => {
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(commitCursor === void 0 ? { result: cachedValue } : { commitCursor, result: cachedValue }, 200, this.currentResponseBookmark);
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 message = error instanceof Error ? error.message : String(error);
4266
+ const { body, status } = toErrorBody(error, { fallbackCode: "BATCH_ENTRY_FAILED" });
4267
4267
  return {
4268
- body: { error: { code: "BATCH_ENTRY_FAILED", message } },
4268
+ body: { error: body },
4269
4269
  bookmark: void 0,
4270
4270
  id: entry?.id,
4271
- status: 500
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(401, { error: { code: "UNAUTHORIZED", message: "missing or invalid SessionDO secret" } });
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(404, { error: { code: "NOT_FOUND", message: "no such session route" } });
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(400, { error: "invalid_request" });
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(400, { error: "invalid_request" });
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(400, { error: "invalid_request" });
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(400, { error: "invalid_request" });
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(201, { token, ...record });
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(400, { error: { code: "INVALID_INPUT", message: "token required" } });
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(404, { error: { code: "NOT_FOUND", message: "session not found" } });
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(404, { error: { code: "EXPIRED", message: "session expired" } });
164
+ return jsonResponse({ error: { code: "EXPIRED", message: "session expired" } }, 404);
167
165
  }
168
- return jsonResponse(200, { token, ...record });
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(400, { error: { code: "INVALID_INPUT", message: "token required" } });
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(200, { ok: true });
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(400, { error: { code: "BAD_REQUEST", message: "invalid JSON body" } }) };
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(400, { error: { code: "BAD_REQUEST", message: "table and shardKey required" } })
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(404, { error: { code: "NOT_FOUND", message: `unknown shard-registry route ${request.method} ${url.pathname}` } });
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(400, { error: { code: "BAD_REQUEST", message: "missing required query parameter: table" } });
84
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: "missing required query parameter: table" } }, 400);
87
85
  }
88
- return jsonResponse(200, { shardKeys: [...this.tables.get(table) ?? []] });
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(200, { changed: false, ok: true });
101
+ return jsonResponse({ changed: false, ok: true }, 200);
104
102
  }
105
103
  set.add(shardKey);
106
104
  await this.persist();
107
- return jsonResponse(200, { changed: true, ok: true });
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(200, { tables: out });
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(200, { changed: false, ok: true });
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(200, { changed: true, ok: true });
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 code = error.code ?? "INSERT_FAILED";
111
- const message = error instanceof Error ? error.message : String(error);
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) => {
@@ -0,0 +1,3 @@
1
+ const jsonResponse = (body, status = 200, headers) => Response.json(body, { headers: { "content-type": "application/json", ...headers }, status });
2
+
3
+ export { jsonResponse as j };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.23",
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.1",
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
  },