@lunora/do 1.0.0-alpha.22 → 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
@@ -2460,6 +2460,7 @@ declare const ADMIN_FUNCTIONS: {
2460
2460
  readonly applyCdc: "__lunora_admin__:applyCdc";
2461
2461
  readonly cdcSync: "__lunora_admin__:cdcSync";
2462
2462
  readonly clearCapturedMail: "__lunora_admin__:clearCapturedMail";
2463
+ readonly clearQueueMessages: "__lunora_admin__:clearQueueMessages";
2463
2464
  readonly clearTable: "__lunora_admin__:clearTable";
2464
2465
  readonly createWorkflowInstance: "__lunora_admin__:createWorkflowInstance";
2465
2466
  readonly deleteRows: "__lunora_admin__:deleteRows";
@@ -2478,6 +2479,7 @@ declare const ADMIN_FUNCTIONS: {
2478
2479
  readonly getLogs: "__lunora_admin__:getLogs";
2479
2480
  readonly getMetrics: "__lunora_admin__:getMetrics";
2480
2481
  readonly getPitrBookmark: "__lunora_admin__:getPitrBookmark";
2482
+ readonly getQueueMessages: "__lunora_admin__:getQueueMessages";
2481
2483
  readonly getRequestLog: "__lunora_admin__:getRequestLog";
2482
2484
  readonly getSecurityAudit: "__lunora_admin__:getSecurityAudit";
2483
2485
  readonly getSettings: "__lunora_admin__:getSettings";
@@ -2496,10 +2498,13 @@ declare const ADMIN_FUNCTIONS: {
2496
2498
  readonly recordAuthEvent: "__lunora_admin__:recordAuthEvent";
2497
2499
  readonly recordContainerEvent: "__lunora_admin__:recordContainerEvent";
2498
2500
  readonly recordMail: "__lunora_admin__:recordMail";
2501
+ readonly recordQueueMessage: "__lunora_admin__:recordQueueMessage";
2502
+ readonly replayQueueMessage: "__lunora_admin__:replayQueueMessage";
2499
2503
  readonly rlsPolicies: "__lunora_admin__:rlsPolicies";
2500
2504
  readonly runAs: "__lunora_admin__:runAs";
2501
2505
  readonly runMigration: "__lunora_admin__:runMigration";
2502
2506
  readonly runSql: "__lunora_admin__:runSql";
2507
+ readonly sendQueueMessage: "__lunora_admin__:sendQueueMessage";
2503
2508
  readonly sendTestMail: "__lunora_admin__:sendTestMail";
2504
2509
  readonly storageOrphans: "__lunora_admin__:storageOrphans";
2505
2510
  readonly storageReferences: "__lunora_admin__:storageReferences";
@@ -3801,43 +3806,6 @@ declare const MIN_AUTH_SECRET_LENGTH = 32;
3801
3806
  * a *missing* token is never itself a finding here (introspection is simply off).
3802
3807
  */
3803
3808
  declare const buildSecurityAudit: (rawEnv: unknown) => SecurityAuditResult;
3804
- /**
3805
- * Durable Object that owns auth session state.
3806
- *
3807
- * `@lunora/auth` used to write sessions directly into D1 alongside user
3808
- * records. That worked but coupled session lifecycle to a global database —
3809
- * every read had to cross the region, every write contended with user
3810
- * inserts. SessionDO owns sessions in a DO-local KV store: same-prefix
3811
- * tokens co-locate (via `idFromName(token.slice(0, 16))`) so the DO instance
3812
- * count stays bounded; reads and writes never round-trip to D1.
3813
- *
3814
- * Wire shape: HTTP only, never RPC. The auth package calls
3815
- *
3816
- * `await env.SESSION.get(env.SESSION.idFromName(prefix)).fetch(...)`
3817
- *
3818
- * with one of:
3819
- *
3820
- * POST /create body: { token, userId, ttlSeconds }
3821
- * GET /get header: `x-lunora-session-token: &lt;token>`
3822
- * DELETE /revoke header: `x-lunora-session-token: &lt;token>`
3823
- *
3824
- * Every request must additionally carry an `x-lunora-session-secret` header
3825
- * whose value matches `env.SESSION_DO_SECRET`. The DO is reachable from any
3826
- * worker bound to its namespace, so a shared secret is the only thing that
3827
- * prevents a compromised or misbehaving worker from reading arbitrary
3828
- * sessions — the binding alone is not an auth surface.
3829
- *
3830
- * The DO returns JSON bodies that `@lunora/auth` reshapes into its public
3831
- * `AuthSession` type. Keep the surface narrow — anything more elaborate
3832
- * should ride on top via a wrapper, not by widening this contract.
3833
- *
3834
- * # Subclassing
3835
- *
3836
- * Apps subclass `SessionDO` (or use the codegen subclass) and register the
3837
- * subclass in `wrangler.jsonc` as `SESSION`. The platform DO binding requires
3838
- * a concrete `DurableObject` class today; the structural state shape used by
3839
- * the unit tests is preserved so plain-object doubles still work.
3840
- */
3841
3809
  /** Default TTL for new sessions (7 days), matching `@lunora/auth`. */
3842
3810
  declare const SESSION_DO_TTL_DEFAULT: number;
3843
3811
  /** Hard ceiling on the requested TTL — 90 days. Longer sessions should ride on top via refresh. */
@@ -5655,6 +5623,54 @@ declare abstract class ShardDO {
5655
5623
  */
5656
5624
  private handleSendTestMail;
5657
5625
  /**
5626
+ * Serve `__lunora_admin__:recordQueueMessage` — the capture sink the generated
5627
+ * worker `queue()` handler (via `@lunora/queue`'s `dispatchQueueBatch`) posts
5628
+ * every consumed message batch to. Records it into the reserved
5629
+ * `__lunora_queue_messages` table (bounded, auto-trimmed) so the studio Queues
5630
+ * panel shows one unified consumed-message log across every push consumer. Like
5631
+ * the mail catcher, the gate is the admin token alone — a token holder can
5632
+ * already mutate the shard, so a token-gated capture insert adds no privilege.
5633
+ */
5634
+ private handleRecordQueueMessage;
5635
+ /** Empty the dev queue consumed-message log (studio "clear log" action). Admin-gated by the caller. */
5636
+ private handleClearQueueMessages;
5637
+ /**
5638
+ * Serve `__lunora_admin__:sendQueueMessage` — the studio's "Send test message"
5639
+ * button. Resolves the declared queue's `QUEUE_*` producer binding and calls
5640
+ * `.send(body, { delaySeconds?, contentType? })`, or `.sendBatch(...)` when a
5641
+ * `batch` array is supplied. No SQLite write happens here (the message is only
5642
+ * captured once a consumer processes it), so this only records an audit entry.
5643
+ * Admin-gated by `handleAdminRpc`'s caller.
5644
+ */
5645
+ private handleSendQueueMessage;
5646
+ /**
5647
+ * Serve `__lunora_admin__:replayQueueMessage` — the studio's one-click replay /
5648
+ * DLQ redrive. Looks the captured row up by id, resolves the destination export
5649
+ * (explicit `target` → the parent queue when the message was captured off a
5650
+ * dead-letter queue → the queue it was consumed from), and re-enqueues the
5651
+ * stored body onto that producer. Records an audit entry; no SQLite write beyond
5652
+ * that (the replayed message is re-captured when a consumer processes it).
5653
+ * Admin-gated by `handleAdminRpc`'s caller.
5654
+ */
5655
+ private handleReplayQueueMessage;
5656
+ /**
5657
+ * Resolve a declared queue's runtime producer binding from this shard's `env`.
5658
+ * Looks the `exportName` up in {@link queuesMetadata} (the codegen subclass's
5659
+ * statically-discovered list) to find its generated `QUEUE_*` binding, then
5660
+ * reads `env[binding]` and validates it carries `send`/`sendBatch`. A bad export
5661
+ * name or a missing/malformed binding throws a 400 `LunoraError` so the studio
5662
+ * surfaces an actionable message. Mirrors {@link resolveWorkflowBinding}.
5663
+ */
5664
+ private resolveQueueBinding;
5665
+ /**
5666
+ * Pick the replay destination export for a captured message's origin queue.
5667
+ * When the message was consumed off a queue that is another queue's dead-letter
5668
+ * queue, prefer that PARENT queue's producer (a DLQ usually has no producer of
5669
+ * its own) so replay redrives onto the original; otherwise re-enqueue onto the
5670
+ * queue the message came from. Returns `undefined` when neither is declared.
5671
+ */
5672
+ private resolveReplayTarget;
5673
+ /**
5658
5674
  * Append one durable audit entry for a state-changing admin op that just
5659
5675
  * succeeded, folding the acting user (from `getCurrentUserId`) into `detail`.
5660
5676
  * Called only on the success path, so a rejected/validated op leaves no
@@ -5860,6 +5876,17 @@ declare abstract class ShardDO {
5860
5876
  * JSON memo still suppresses byte-identical pushes).
5861
5877
  */
5862
5878
  private readAdminCapturedMail;
5879
+ /**
5880
+ * Resolve a `getQueueMessages` admin read — the dev queue catcher's consumed
5881
+ * message log (`queue-catcher.ts`), newest-first, optionally filtered to one
5882
+ * queue. Best-effort: a SQL failure returns an empty log rather than throwing.
5883
+ * Reported against the {@link QUEUE_TABLE} so this read participates in
5884
+ * table-scoped subscription invalidation, but new captures arrive via the
5885
+ * worker→root-shard `recordQueueMessage` write, which (like the mail catcher)
5886
+ * inserts directly without a `flushChangedTables` — so the panel refreshes on
5887
+ * its poll (`useAutoRefresh`) rather than a live push.
5888
+ */
5889
+ private readAdminQueueMessages;
5863
5890
  /** Resolve a `readTablePage` admin read, parsing the loosely-typed args into the reader's options. */
5864
5891
  private readAdminTablePage;
5865
5892
  /**
@@ -6389,42 +6416,6 @@ declare abstract class ShardDO {
6389
6416
  private deliverWhisperLocal;
6390
6417
  private readAttachment;
6391
6418
  }
6392
- /**
6393
- * Durable Object that owns the live set of shard keys per sharded table.
6394
- *
6395
- * The query coordinator (`@lunora/runtime`) fans out cross-shard reads to
6396
- * every live shard. With the static registry, the app supplies the shard
6397
- * key list at boot — which is fine for fixed-cardinality deployments
6398
- * (a known set of tenants) and unworkable for dynamic ones (one shard per
6399
- * user-created channel, organisation, project, …).
6400
- *
6401
- * `ShardRegistryDO` is the persistent source of truth. A worker:
6402
- *
6403
- * - calls `POST /register {table, shardKey}` when a sharded table first
6404
- * sees a write on a new key (typically from `ctx.db.&lt;table>.insert` via
6405
- * the worker's onWrite hook, fired through `ctx.waitUntil` so the
6406
- * user-facing write doesn't pay the registry round-trip);
6407
- * - calls `POST /unregister {table, shardKey}` when a shard is decommissioned;
6408
- * - calls `GET /list?table=X` to materialise the fan-out target list. The
6409
- * client (`createDynamicShardRegistry` in `@lunora/runtime`) caches the
6410
- * answer with a small TTL so a wide fan-out doesn't pay a registry
6411
- * round-trip on every call.
6412
- *
6413
- * Single-instance contract: deploy one DO instance per environment, by
6414
- * convention named {@link SHARD_REGISTRY_DO_NAME}. The DO is small (just a
6415
- * `Map&lt;table, Set&lt;shardKey>>`) and writes are infrequent (only on first-seen
6416
- * shardKey per table), so a single instance is sufficient up to tens of
6417
- * thousands of distinct shard keys.
6418
- *
6419
- * Wire shape: HTTP only, never RPC.
6420
- *
6421
- * POST /register body: { table, shardKey }
6422
- * POST /unregister body: { table, shardKey }
6423
- * GET /list?table=...
6424
- * GET /snapshot (debug: returns the full table → [keys] map)
6425
- *
6426
- * Responses are JSON; the client shapes them. Keep the surface narrow.
6427
- */
6428
6419
  /** Conventional DO instance name, passed to `idFromName` to address the single registry instance. */
6429
6420
  declare const SHARD_REGISTRY_DO_NAME: string;
6430
6421
  /**
package/dist/index.d.ts CHANGED
@@ -2460,6 +2460,7 @@ declare const ADMIN_FUNCTIONS: {
2460
2460
  readonly applyCdc: "__lunora_admin__:applyCdc";
2461
2461
  readonly cdcSync: "__lunora_admin__:cdcSync";
2462
2462
  readonly clearCapturedMail: "__lunora_admin__:clearCapturedMail";
2463
+ readonly clearQueueMessages: "__lunora_admin__:clearQueueMessages";
2463
2464
  readonly clearTable: "__lunora_admin__:clearTable";
2464
2465
  readonly createWorkflowInstance: "__lunora_admin__:createWorkflowInstance";
2465
2466
  readonly deleteRows: "__lunora_admin__:deleteRows";
@@ -2478,6 +2479,7 @@ declare const ADMIN_FUNCTIONS: {
2478
2479
  readonly getLogs: "__lunora_admin__:getLogs";
2479
2480
  readonly getMetrics: "__lunora_admin__:getMetrics";
2480
2481
  readonly getPitrBookmark: "__lunora_admin__:getPitrBookmark";
2482
+ readonly getQueueMessages: "__lunora_admin__:getQueueMessages";
2481
2483
  readonly getRequestLog: "__lunora_admin__:getRequestLog";
2482
2484
  readonly getSecurityAudit: "__lunora_admin__:getSecurityAudit";
2483
2485
  readonly getSettings: "__lunora_admin__:getSettings";
@@ -2496,10 +2498,13 @@ declare const ADMIN_FUNCTIONS: {
2496
2498
  readonly recordAuthEvent: "__lunora_admin__:recordAuthEvent";
2497
2499
  readonly recordContainerEvent: "__lunora_admin__:recordContainerEvent";
2498
2500
  readonly recordMail: "__lunora_admin__:recordMail";
2501
+ readonly recordQueueMessage: "__lunora_admin__:recordQueueMessage";
2502
+ readonly replayQueueMessage: "__lunora_admin__:replayQueueMessage";
2499
2503
  readonly rlsPolicies: "__lunora_admin__:rlsPolicies";
2500
2504
  readonly runAs: "__lunora_admin__:runAs";
2501
2505
  readonly runMigration: "__lunora_admin__:runMigration";
2502
2506
  readonly runSql: "__lunora_admin__:runSql";
2507
+ readonly sendQueueMessage: "__lunora_admin__:sendQueueMessage";
2503
2508
  readonly sendTestMail: "__lunora_admin__:sendTestMail";
2504
2509
  readonly storageOrphans: "__lunora_admin__:storageOrphans";
2505
2510
  readonly storageReferences: "__lunora_admin__:storageReferences";
@@ -3801,43 +3806,6 @@ declare const MIN_AUTH_SECRET_LENGTH = 32;
3801
3806
  * a *missing* token is never itself a finding here (introspection is simply off).
3802
3807
  */
3803
3808
  declare const buildSecurityAudit: (rawEnv: unknown) => SecurityAuditResult;
3804
- /**
3805
- * Durable Object that owns auth session state.
3806
- *
3807
- * `@lunora/auth` used to write sessions directly into D1 alongside user
3808
- * records. That worked but coupled session lifecycle to a global database —
3809
- * every read had to cross the region, every write contended with user
3810
- * inserts. SessionDO owns sessions in a DO-local KV store: same-prefix
3811
- * tokens co-locate (via `idFromName(token.slice(0, 16))`) so the DO instance
3812
- * count stays bounded; reads and writes never round-trip to D1.
3813
- *
3814
- * Wire shape: HTTP only, never RPC. The auth package calls
3815
- *
3816
- * `await env.SESSION.get(env.SESSION.idFromName(prefix)).fetch(...)`
3817
- *
3818
- * with one of:
3819
- *
3820
- * POST /create body: { token, userId, ttlSeconds }
3821
- * GET /get header: `x-lunora-session-token: &lt;token>`
3822
- * DELETE /revoke header: `x-lunora-session-token: &lt;token>`
3823
- *
3824
- * Every request must additionally carry an `x-lunora-session-secret` header
3825
- * whose value matches `env.SESSION_DO_SECRET`. The DO is reachable from any
3826
- * worker bound to its namespace, so a shared secret is the only thing that
3827
- * prevents a compromised or misbehaving worker from reading arbitrary
3828
- * sessions — the binding alone is not an auth surface.
3829
- *
3830
- * The DO returns JSON bodies that `@lunora/auth` reshapes into its public
3831
- * `AuthSession` type. Keep the surface narrow — anything more elaborate
3832
- * should ride on top via a wrapper, not by widening this contract.
3833
- *
3834
- * # Subclassing
3835
- *
3836
- * Apps subclass `SessionDO` (or use the codegen subclass) and register the
3837
- * subclass in `wrangler.jsonc` as `SESSION`. The platform DO binding requires
3838
- * a concrete `DurableObject` class today; the structural state shape used by
3839
- * the unit tests is preserved so plain-object doubles still work.
3840
- */
3841
3809
  /** Default TTL for new sessions (7 days), matching `@lunora/auth`. */
3842
3810
  declare const SESSION_DO_TTL_DEFAULT: number;
3843
3811
  /** Hard ceiling on the requested TTL — 90 days. Longer sessions should ride on top via refresh. */
@@ -5655,6 +5623,54 @@ declare abstract class ShardDO {
5655
5623
  */
5656
5624
  private handleSendTestMail;
5657
5625
  /**
5626
+ * Serve `__lunora_admin__:recordQueueMessage` — the capture sink the generated
5627
+ * worker `queue()` handler (via `@lunora/queue`'s `dispatchQueueBatch`) posts
5628
+ * every consumed message batch to. Records it into the reserved
5629
+ * `__lunora_queue_messages` table (bounded, auto-trimmed) so the studio Queues
5630
+ * panel shows one unified consumed-message log across every push consumer. Like
5631
+ * the mail catcher, the gate is the admin token alone — a token holder can
5632
+ * already mutate the shard, so a token-gated capture insert adds no privilege.
5633
+ */
5634
+ private handleRecordQueueMessage;
5635
+ /** Empty the dev queue consumed-message log (studio "clear log" action). Admin-gated by the caller. */
5636
+ private handleClearQueueMessages;
5637
+ /**
5638
+ * Serve `__lunora_admin__:sendQueueMessage` — the studio's "Send test message"
5639
+ * button. Resolves the declared queue's `QUEUE_*` producer binding and calls
5640
+ * `.send(body, { delaySeconds?, contentType? })`, or `.sendBatch(...)` when a
5641
+ * `batch` array is supplied. No SQLite write happens here (the message is only
5642
+ * captured once a consumer processes it), so this only records an audit entry.
5643
+ * Admin-gated by `handleAdminRpc`'s caller.
5644
+ */
5645
+ private handleSendQueueMessage;
5646
+ /**
5647
+ * Serve `__lunora_admin__:replayQueueMessage` — the studio's one-click replay /
5648
+ * DLQ redrive. Looks the captured row up by id, resolves the destination export
5649
+ * (explicit `target` → the parent queue when the message was captured off a
5650
+ * dead-letter queue → the queue it was consumed from), and re-enqueues the
5651
+ * stored body onto that producer. Records an audit entry; no SQLite write beyond
5652
+ * that (the replayed message is re-captured when a consumer processes it).
5653
+ * Admin-gated by `handleAdminRpc`'s caller.
5654
+ */
5655
+ private handleReplayQueueMessage;
5656
+ /**
5657
+ * Resolve a declared queue's runtime producer binding from this shard's `env`.
5658
+ * Looks the `exportName` up in {@link queuesMetadata} (the codegen subclass's
5659
+ * statically-discovered list) to find its generated `QUEUE_*` binding, then
5660
+ * reads `env[binding]` and validates it carries `send`/`sendBatch`. A bad export
5661
+ * name or a missing/malformed binding throws a 400 `LunoraError` so the studio
5662
+ * surfaces an actionable message. Mirrors {@link resolveWorkflowBinding}.
5663
+ */
5664
+ private resolveQueueBinding;
5665
+ /**
5666
+ * Pick the replay destination export for a captured message's origin queue.
5667
+ * When the message was consumed off a queue that is another queue's dead-letter
5668
+ * queue, prefer that PARENT queue's producer (a DLQ usually has no producer of
5669
+ * its own) so replay redrives onto the original; otherwise re-enqueue onto the
5670
+ * queue the message came from. Returns `undefined` when neither is declared.
5671
+ */
5672
+ private resolveReplayTarget;
5673
+ /**
5658
5674
  * Append one durable audit entry for a state-changing admin op that just
5659
5675
  * succeeded, folding the acting user (from `getCurrentUserId`) into `detail`.
5660
5676
  * Called only on the success path, so a rejected/validated op leaves no
@@ -5860,6 +5876,17 @@ declare abstract class ShardDO {
5860
5876
  * JSON memo still suppresses byte-identical pushes).
5861
5877
  */
5862
5878
  private readAdminCapturedMail;
5879
+ /**
5880
+ * Resolve a `getQueueMessages` admin read — the dev queue catcher's consumed
5881
+ * message log (`queue-catcher.ts`), newest-first, optionally filtered to one
5882
+ * queue. Best-effort: a SQL failure returns an empty log rather than throwing.
5883
+ * Reported against the {@link QUEUE_TABLE} so this read participates in
5884
+ * table-scoped subscription invalidation, but new captures arrive via the
5885
+ * worker→root-shard `recordQueueMessage` write, which (like the mail catcher)
5886
+ * inserts directly without a `flushChangedTables` — so the panel refreshes on
5887
+ * its poll (`useAutoRefresh`) rather than a live push.
5888
+ */
5889
+ private readAdminQueueMessages;
5863
5890
  /** Resolve a `readTablePage` admin read, parsing the loosely-typed args into the reader's options. */
5864
5891
  private readAdminTablePage;
5865
5892
  /**
@@ -6389,42 +6416,6 @@ declare abstract class ShardDO {
6389
6416
  private deliverWhisperLocal;
6390
6417
  private readAttachment;
6391
6418
  }
6392
- /**
6393
- * Durable Object that owns the live set of shard keys per sharded table.
6394
- *
6395
- * The query coordinator (`@lunora/runtime`) fans out cross-shard reads to
6396
- * every live shard. With the static registry, the app supplies the shard
6397
- * key list at boot — which is fine for fixed-cardinality deployments
6398
- * (a known set of tenants) and unworkable for dynamic ones (one shard per
6399
- * user-created channel, organisation, project, …).
6400
- *
6401
- * `ShardRegistryDO` is the persistent source of truth. A worker:
6402
- *
6403
- * - calls `POST /register {table, shardKey}` when a sharded table first
6404
- * sees a write on a new key (typically from `ctx.db.&lt;table>.insert` via
6405
- * the worker's onWrite hook, fired through `ctx.waitUntil` so the
6406
- * user-facing write doesn't pay the registry round-trip);
6407
- * - calls `POST /unregister {table, shardKey}` when a shard is decommissioned;
6408
- * - calls `GET /list?table=X` to materialise the fan-out target list. The
6409
- * client (`createDynamicShardRegistry` in `@lunora/runtime`) caches the
6410
- * answer with a small TTL so a wide fan-out doesn't pay a registry
6411
- * round-trip on every call.
6412
- *
6413
- * Single-instance contract: deploy one DO instance per environment, by
6414
- * convention named {@link SHARD_REGISTRY_DO_NAME}. The DO is small (just a
6415
- * `Map&lt;table, Set&lt;shardKey>>`) and writes are infrequent (only on first-seen
6416
- * shardKey per table), so a single instance is sufficient up to tens of
6417
- * thousands of distinct shard keys.
6418
- *
6419
- * Wire shape: HTTP only, never RPC.
6420
- *
6421
- * POST /register body: { table, shardKey }
6422
- * POST /unregister body: { table, shardKey }
6423
- * GET /list?table=...
6424
- * GET /snapshot (debug: returns the full table → [keys] map)
6425
- *
6426
- * Responses are JSON; the client shapes them. Keep the surface narrow.
6427
- */
6428
6419
  /** Conventional DO instance name, passed to `idFromName` to address the single registry instance. */
6429
6420
  declare const SHARD_REGISTRY_DO_NAME: string;
6430
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';
@@ -11,7 +11,7 @@ export { diffExternalSource } from './packem_shared/diffExternalSource-Cx9HUPJj.
11
11
  export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-DNcvoLRT.mjs';
12
12
  export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-BptUN8CR.mjs';
13
13
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
14
- export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-DSUQ5fX9.mjs';
14
+ export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-BbQdj8h1.mjs';
15
15
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
16
16
  export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
17
17
  export { default as NotFoundError } from './packem_shared/NotFoundError-C70b9hLw.mjs';
@@ -19,15 +19,15 @@ export { armRestore, readBookmark } from './packem_shared/armRestore-BJk53Ro8.mj
19
19
  export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-BvZdFUBT.mjs';
20
20
  export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
21
21
  export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-BYlSGY0N.mjs';
22
- export { serveRelationFanout } from './packem_shared/serveRelationFanout-C5axmGDj.mjs';
22
+ export { serveRelationFanout } from './packem_shared/serveRelationFanout-BhZF9AuB.mjs';
23
23
  export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-CjM9Y1_G.mjs';
24
24
  export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-CAwZfp-5.mjs';
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-BQoX61l-.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';
@@ -7,6 +7,7 @@ const ADMIN_FUNCTIONS = {
7
7
  applyCdc: "__lunora_admin__:applyCdc",
8
8
  cdcSync: "__lunora_admin__:cdcSync",
9
9
  clearCapturedMail: "__lunora_admin__:clearCapturedMail",
10
+ clearQueueMessages: "__lunora_admin__:clearQueueMessages",
10
11
  clearTable: "__lunora_admin__:clearTable",
11
12
  createWorkflowInstance: "__lunora_admin__:createWorkflowInstance",
12
13
  deleteRows: "__lunora_admin__:deleteRows",
@@ -25,6 +26,7 @@ const ADMIN_FUNCTIONS = {
25
26
  getLogs: "__lunora_admin__:getLogs",
26
27
  getMetrics: "__lunora_admin__:getMetrics",
27
28
  getPitrBookmark: "__lunora_admin__:getPitrBookmark",
29
+ getQueueMessages: "__lunora_admin__:getQueueMessages",
28
30
  getRequestLog: "__lunora_admin__:getRequestLog",
29
31
  getSecurityAudit: "__lunora_admin__:getSecurityAudit",
30
32
  getSettings: "__lunora_admin__:getSettings",
@@ -44,10 +46,13 @@ const ADMIN_FUNCTIONS = {
44
46
  recordAuthEvent: "__lunora_admin__:recordAuthEvent",
45
47
  recordContainerEvent: "__lunora_admin__:recordContainerEvent",
46
48
  recordMail: "__lunora_admin__:recordMail",
49
+ recordQueueMessage: "__lunora_admin__:recordQueueMessage",
50
+ replayQueueMessage: "__lunora_admin__:replayQueueMessage",
47
51
  rlsPolicies: "__lunora_admin__:rlsPolicies",
48
52
  runAs: "__lunora_admin__:runAs",
49
53
  runMigration: "__lunora_admin__:runMigration",
50
54
  runSql: "__lunora_admin__:runSql",
55
+ sendQueueMessage: "__lunora_admin__:sendQueueMessage",
51
56
  sendTestMail: "__lunora_admin__:sendTestMail",
52
57
  storageOrphans: "__lunora_admin__:storageOrphans",
53
58
  storageReferences: "__lunora_admin__:storageReferences",
@@ -1,12 +1,13 @@
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';
8
9
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
9
- import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-DSUQ5fX9.mjs';
10
+ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-BbQdj8h1.mjs';
10
11
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
11
12
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
12
13
  import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
@@ -24,12 +25,12 @@ const MAX_BATCH_ENTRIES = 500;
24
25
 
25
26
  const AUDIT_LOG_TABLE = "__lunora_audit__";
26
27
  const AUDIT_LOG_RETENTION = 1e3;
27
- const runSql$2 = (sql, query, ...params) => {
28
+ const runSql$3 = (sql, query, ...params) => {
28
29
  const runner = sql.exec;
29
30
  return runner.call(sql, query, ...params);
30
31
  };
31
32
  const ensureAuditTable = (sql) => {
32
- runSql$2(
33
+ runSql$3(
33
34
  sql,
34
35
  `CREATE TABLE IF NOT EXISTS "${AUDIT_LOG_TABLE}" (
35
36
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -43,7 +44,7 @@ const ensureAuditTable = (sql) => {
43
44
  };
44
45
  const appendAuditEntry = (sql, entry) => {
45
46
  ensureAuditTable(sql);
46
- runSql$2(
47
+ runSql$3(
47
48
  sql,
48
49
  `INSERT INTO "${AUDIT_LOG_TABLE}" (ts, op, "table", id, detail) VALUES (?, ?, ?, ?, ?)`,
49
50
  entry.ts,
@@ -55,13 +56,13 @@ const appendAuditEntry = (sql, entry) => {
55
56
  // eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an op with no associated table/id/detail.
56
57
  entry.detail === void 0 ? null : JSON.stringify(entry.detail)
57
58
  );
58
- runSql$2(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
59
+ runSql$3(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
59
60
  };
60
61
  const readAuditLog = (sql, options = {}) => {
61
62
  ensureAuditTable(sql);
62
63
  const sinceSeq = options.sinceSeq ?? 0;
63
64
  const limit = Math.max(1, Math.min(options.limit ?? AUDIT_LOG_RETENTION, 1e4));
64
- const rows = runSql$2(
65
+ const rows = runSql$3(
65
66
  sql,
66
67
  `SELECT seq, ts, op, "table", id, detail FROM "${AUDIT_LOG_TABLE}" WHERE seq > ? ORDER BY seq DESC LIMIT ?`,
67
68
  sinceSeq,
@@ -117,7 +118,7 @@ const buildBatchEntryRequest = (batchRequest, entry) => {
117
118
  const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
118
119
  const QUERY_METRICS_MAX_SQL_LEN = 512;
119
120
  const QUERY_METRICS_MAX_STATEMENTS = 500;
120
- const runSql$1 = (sql, query, ...params) => {
121
+ const runSql$2 = (sql, query, ...params) => {
121
122
  const runner = sql.exec;
122
123
  return runner.call(sql, query, ...params);
123
124
  };
@@ -129,7 +130,7 @@ const normalizeSql = (sql) => {
129
130
  return normalized;
130
131
  };
131
132
  const ensureQueryMetricsTable = (sql) => {
132
- runSql$1(
133
+ runSql$2(
133
134
  sql,
134
135
  `CREATE TABLE IF NOT EXISTS "${QUERY_METRICS_TABLE}" (
135
136
  normalized_sql TEXT PRIMARY KEY,
@@ -146,10 +147,10 @@ const recordQueryMetric = (sql, rawSql, durationMs, rowsRead, rowsWritten) => {
146
147
  return;
147
148
  }
148
149
  ensureQueryMetricsTable(sql);
149
- const countRow = runSql$1(sql, `SELECT COUNT(*) AS n FROM "${QUERY_METRICS_TABLE}"`).one();
150
+ const countRow = runSql$2(sql, `SELECT COUNT(*) AS n FROM "${QUERY_METRICS_TABLE}"`).one();
150
151
  const count = countRow.n;
151
152
  if (count >= QUERY_METRICS_MAX_STATEMENTS) {
152
- const existing = runSql$1(sql, `SELECT COUNT(*) AS c FROM "${QUERY_METRICS_TABLE}" WHERE normalized_sql = ?`, normalized).one();
153
+ const existing = runSql$2(sql, `SELECT COUNT(*) AS c FROM "${QUERY_METRICS_TABLE}" WHERE normalized_sql = ?`, normalized).one();
153
154
  if (existing.c === 0) {
154
155
  return;
155
156
  }
@@ -161,11 +162,11 @@ const recordQueryMetric = (sql, rawSql, durationMs, rowsRead, rowsWritten) => {
161
162
  total_duration_ms = total_duration_ms + excluded.total_duration_ms,
162
163
  rows_read = rows_read + excluded.rows_read,
163
164
  rows_written = rows_written + excluded.rows_written`;
164
- runSql$1(sql, upsertSql, normalized, durationMs, rowsRead, rowsWritten);
165
+ runSql$2(sql, upsertSql, normalized, durationMs, rowsRead, rowsWritten);
165
166
  };
166
167
  const readQueryMetrics = (sql) => {
167
168
  ensureQueryMetricsTable(sql);
168
- const rows = runSql$1(
169
+ const rows = runSql$2(
169
170
  sql,
170
171
  `SELECT normalized_sql, exec_count, total_duration_ms, rows_read, rows_written FROM "${QUERY_METRICS_TABLE}" ORDER BY total_duration_ms DESC`
171
172
  ).toArray();
@@ -180,6 +181,128 @@ const readQueryMetrics = (sql) => {
180
181
  });
181
182
  };
182
183
 
184
+ const QUEUE_TABLE = "__lunora_queue_messages";
185
+ const QUEUE_RETENTION = 500;
186
+ const MAX_BODY_CHARS = 128 * 1024;
187
+ const runSql$1 = (sql, query, ...params) => {
188
+ const runner = sql.exec;
189
+ return runner.call(sql, query, ...params);
190
+ };
191
+ const orNull = (value) => (
192
+ // eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an absent column.
193
+ value ?? null
194
+ );
195
+ const TRUNCATION_SUFFIX = "… [truncated by the dev queue catcher]";
196
+ const UNSERIALIZABLE_MARKER = "[unserializable message body]";
197
+ const encodeBody = (value) => {
198
+ if (value === void 0) {
199
+ return "null";
200
+ }
201
+ try {
202
+ const encoded = JSON.stringify(value);
203
+ if (encoded.length > MAX_BODY_CHARS) {
204
+ return JSON.stringify(`${encoded.slice(0, MAX_BODY_CHARS)}${TRUNCATION_SUFFIX}`);
205
+ }
206
+ return encoded;
207
+ } catch {
208
+ return JSON.stringify(UNSERIALIZABLE_MARKER);
209
+ }
210
+ };
211
+ const isLossyBody = (body) => typeof body === "string" && (body === UNSERIALIZABLE_MARKER || body.endsWith(TRUNCATION_SUFFIX));
212
+ const decodeBody = (value) => {
213
+ if (value === null || value === void 0 || value === "") {
214
+ return void 0;
215
+ }
216
+ try {
217
+ return JSON.parse(value);
218
+ } catch {
219
+ return void 0;
220
+ }
221
+ };
222
+ const ensureQueueTable = (sql) => {
223
+ runSql$1(
224
+ sql,
225
+ `CREATE TABLE IF NOT EXISTS "${QUEUE_TABLE}" (
226
+ id TEXT PRIMARY KEY,
227
+ captured_at INTEGER NOT NULL,
228
+ message_id TEXT NOT NULL,
229
+ queue TEXT NOT NULL,
230
+ export_name TEXT,
231
+ body TEXT NOT NULL,
232
+ attempts INTEGER NOT NULL,
233
+ outcome TEXT NOT NULL,
234
+ error TEXT,
235
+ dead_lettered INTEGER NOT NULL,
236
+ message_ts INTEGER NOT NULL
237
+ )`
238
+ );
239
+ };
240
+ const recordQueueMessages = (sql, inputs, capturedAt) => {
241
+ ensureQueueTable(sql);
242
+ for (const input of inputs) {
243
+ runSql$1(
244
+ sql,
245
+ `INSERT INTO "${QUEUE_TABLE}" (id, captured_at, message_id, queue, export_name, body, attempts, outcome, error, dead_lettered, message_ts)
246
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
247
+ crypto.randomUUID(),
248
+ capturedAt,
249
+ input.messageId,
250
+ input.queue,
251
+ orNull(input.exportName),
252
+ encodeBody(input.body),
253
+ input.attempts,
254
+ input.outcome,
255
+ orNull(input.error),
256
+ input.deadLettered === true ? 1 : 0,
257
+ input.timestamp
258
+ );
259
+ }
260
+ runSql$1(
261
+ sql,
262
+ `DELETE FROM "${QUEUE_TABLE}"
263
+ WHERE id NOT IN (
264
+ SELECT id FROM "${QUEUE_TABLE}" ORDER BY captured_at DESC, id DESC LIMIT ?
265
+ )`,
266
+ QUEUE_RETENTION
267
+ );
268
+ return { recorded: inputs.length };
269
+ };
270
+ const rowToEntry = (row) => {
271
+ return {
272
+ attempts: row.attempts,
273
+ body: decodeBody(row.body),
274
+ capturedAt: row.captured_at,
275
+ deadLettered: row.dead_lettered === 1,
276
+ error: row.error ?? void 0,
277
+ exportName: row.export_name ?? void 0,
278
+ id: row.id,
279
+ messageId: row.message_id,
280
+ outcome: row.outcome,
281
+ queue: row.queue,
282
+ timestamp: row.message_ts
283
+ };
284
+ };
285
+ const readQueueMessages = (sql, options = {}) => {
286
+ ensureQueueTable(sql);
287
+ const limit = Math.min(Math.max(options.limit ?? 100, 1), QUEUE_RETENTION);
288
+ const filterQueue = typeof options.queue === "string" && options.queue.length > 0 ? options.queue : void 0;
289
+ const where = filterQueue === void 0 ? "" : `WHERE queue = ?`;
290
+ const params = filterQueue === void 0 ? [limit] : [filterQueue, limit];
291
+ const rows = runSql$1(sql, `SELECT * FROM "${QUEUE_TABLE}" ${where} ORDER BY captured_at DESC, id DESC LIMIT ?`, ...params).toArray();
292
+ return { entries: rows.map((row) => rowToEntry(row)) };
293
+ };
294
+ const readQueueMessageById = (sql, id) => {
295
+ ensureQueueTable(sql);
296
+ const rows = runSql$1(sql, `SELECT * FROM "${QUEUE_TABLE}" WHERE id = ? LIMIT 1`, id).toArray();
297
+ const row = rows[0];
298
+ return row === void 0 ? void 0 : rowToEntry(row);
299
+ };
300
+ const clearQueueMessages = (sql) => {
301
+ ensureQueueTable(sql);
302
+ runSql$1(sql, `DELETE FROM "${QUEUE_TABLE}"`);
303
+ return { cleared: true };
304
+ };
305
+
183
306
  const RELAY_NAME_INFIX = "::relay::";
184
307
  const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
185
308
  const parseRelayName = (name) => {
@@ -642,7 +765,8 @@ class OwnerRelay extends RelayLink {
642
765
  try {
643
766
  resolved = this.host.resolveShape(request.name, request.args, identity);
644
767
  } catch (error) {
645
- 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 } };
646
770
  }
647
771
  if (resolved === void 0 || resolved.global === true) {
648
772
  return { error: { code: "SHAPE_NOT_FOUND", message: `shape not relayable: ${request.name}` } };
@@ -1512,6 +1636,84 @@ Verify your email: ${link}`,
1512
1636
  to: recipient
1513
1637
  };
1514
1638
  };
1639
+ const parseRecordQueueMessageArgs = (args) => {
1640
+ const bad = (message) => {
1641
+ throw Object.assign(new Error(`recordQueueMessage: ${message}`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1642
+ };
1643
+ const raw = args["messages"];
1644
+ if (!Array.isArray(raw)) {
1645
+ bad("`messages` must be an array");
1646
+ }
1647
+ const outcomes = /* @__PURE__ */ new Set(["ack", "error", "retry"]);
1648
+ return raw.map((entry, index) => {
1649
+ if (typeof entry !== "object" || entry === null) {
1650
+ bad(`\`messages[${String(index)}]\` must be an object`);
1651
+ }
1652
+ const record = entry;
1653
+ const messageId = typeof record["messageId"] === "string" ? record["messageId"] : "";
1654
+ const queue = typeof record["queue"] === "string" ? record["queue"] : "";
1655
+ const outcome = typeof record["outcome"] === "string" ? record["outcome"] : "";
1656
+ if (messageId === "") {
1657
+ bad(`\`messages[${String(index)}].messageId\` is required`);
1658
+ }
1659
+ if (queue === "") {
1660
+ bad(`\`messages[${String(index)}].queue\` is required`);
1661
+ }
1662
+ if (!outcomes.has(outcome)) {
1663
+ bad(`\`messages[${String(index)}].outcome\` must be one of ack | error | retry`);
1664
+ }
1665
+ const { attempts, timestamp } = record;
1666
+ return {
1667
+ attempts: typeof attempts === "number" && Number.isFinite(attempts) ? attempts : 1,
1668
+ body: record["body"],
1669
+ deadLettered: record["deadLettered"] === true,
1670
+ error: typeof record["error"] === "string" ? record["error"] : void 0,
1671
+ exportName: typeof record["exportName"] === "string" ? record["exportName"] : void 0,
1672
+ messageId,
1673
+ outcome,
1674
+ queue,
1675
+ timestamp: typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : 0
1676
+ };
1677
+ });
1678
+ };
1679
+ const MAX_QUEUE_SEND_BATCH = 100;
1680
+ const parseSendQueueMessageArgs = (args) => {
1681
+ const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
1682
+ if (exportName === "") {
1683
+ throw Object.assign(new Error("sendQueueMessage: `exportName` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1684
+ }
1685
+ const delayRaw = args["delaySeconds"];
1686
+ if (delayRaw !== void 0 && (typeof delayRaw !== "number" || !Number.isFinite(delayRaw) || delayRaw < 0)) {
1687
+ throw Object.assign(new Error("sendQueueMessage: `delaySeconds` must be a non-negative number"), {
1688
+ code: "BAD_REQUEST",
1689
+ name: "LunoraError",
1690
+ status: 400
1691
+ });
1692
+ }
1693
+ const batch = Array.isArray(args["batch"]) ? args["batch"] : void 0;
1694
+ if (batch !== void 0 && (batch.length === 0 || batch.length > MAX_QUEUE_SEND_BATCH)) {
1695
+ throw Object.assign(new Error(`sendQueueMessage: \`batch\` must contain between 1 and ${String(MAX_QUEUE_SEND_BATCH)} messages`), {
1696
+ code: "BAD_REQUEST",
1697
+ name: "LunoraError",
1698
+ status: 400
1699
+ });
1700
+ }
1701
+ return {
1702
+ batch,
1703
+ body: args["body"],
1704
+ contentType: typeof args["contentType"] === "string" ? args["contentType"] : void 0,
1705
+ delaySeconds: delayRaw,
1706
+ exportName
1707
+ };
1708
+ };
1709
+ const parseReplayQueueMessageArgs = (args) => {
1710
+ const id = typeof args["id"] === "string" ? args["id"].trim() : "";
1711
+ if (id === "") {
1712
+ throw Object.assign(new Error("replayQueueMessage: `id` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
1713
+ }
1714
+ const target = typeof args["target"] === "string" && args["target"].trim() !== "" ? args["target"].trim() : void 0;
1715
+ return { id, target };
1716
+ };
1515
1717
  const parseRankBeforeArgs = (args) => {
1516
1718
  const table = typeof args["table"] === "string" ? args["table"] : "";
1517
1719
  const index = typeof args["index"] === "string" ? args["index"] : "";
@@ -1642,13 +1844,7 @@ const parseCdcSyncArgs = (args) => {
1642
1844
  };
1643
1845
  return { limit: toCount(args["limit"]), sinceSeq: toCount(args["sinceSeq"]) ?? 0 };
1644
1846
  };
1645
- const jsonResponse = (body, status = 200, bookmark) => {
1646
- const headers = { "content-type": "application/json" };
1647
- if (bookmark) {
1648
- headers["x-d1-bookmark"] = bookmark;
1649
- }
1650
- return Response.json(body, { headers, status });
1651
- };
1847
+ const bookmarkHeaders = (bookmark) => bookmark ? { "x-d1-bookmark": bookmark } : void 0;
1652
1848
  const parseIdentityHeader = (raw) => {
1653
1849
  if (!raw) {
1654
1850
  return void 0;
@@ -2194,7 +2390,7 @@ class ShardDO {
2194
2390
  try {
2195
2391
  if (payload.functionPath.startsWith(RELATION_FUNCTION_PREFIX)) {
2196
2392
  const value = await this.runRelationFanoutRead(payload.functionPath, payload.args ?? {});
2197
- return jsonResponse(value, 200, this.currentResponseBookmark);
2393
+ return jsonResponse(value, 200, bookmarkHeaders(this.currentResponseBookmark));
2198
2394
  }
2199
2395
  const mutatorClass = this.isCustomMutator(payload.functionPath) ? this.classifyClientMutation() : void 0;
2200
2396
  this.currentMutatorClass = mutatorClass;
@@ -3239,7 +3435,7 @@ class ShardDO {
3239
3435
  }
3240
3436
  this.recordFunctionCall(functionPath, Date.now() - dispatchStartedAt, void 0, this.currentScannedTables, this.currentIndexHits);
3241
3437
  if (mutatorClass.kind === "already") {
3242
- return jsonResponse({ lastMutationId: mutatorClass.expected - 1, result: null }, 200, this.currentResponseBookmark);
3438
+ return jsonResponse({ lastMutationId: mutatorClass.expected - 1, result: null }, 200, bookmarkHeaders(this.currentResponseBookmark));
3243
3439
  }
3244
3440
  return jsonResponse(
3245
3441
  {
@@ -3250,7 +3446,7 @@ class ShardDO {
3250
3446
  }
3251
3447
  },
3252
3448
  409,
3253
- this.currentResponseBookmark
3449
+ bookmarkHeaders(this.currentResponseBookmark)
3254
3450
  );
3255
3451
  }
3256
3452
  /**
@@ -3269,7 +3465,11 @@ class ShardDO {
3269
3465
  return this.buildDispatchResponse(mutatorClass, cachedValue);
3270
3466
  }
3271
3467
  const commitCursor = this.mutationCommitCursor();
3272
- 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
+ );
3273
3473
  }
3274
3474
  /**
3275
3475
  * The CDC cursor a just-committed plain mutation landed at — the post-write
@@ -3293,10 +3493,10 @@ class ShardDO {
3293
3493
  */
3294
3494
  buildDispatchResponse(mutatorClass, result) {
3295
3495
  if (mutatorClass?.kind === "next") {
3296
- return jsonResponse({ lastMutationId: this.currentRequestClientSeq, result }, 200, this.currentResponseBookmark);
3496
+ return jsonResponse({ lastMutationId: this.currentRequestClientSeq, result }, 200, bookmarkHeaders(this.currentResponseBookmark));
3297
3497
  }
3298
3498
  const commitCursor = this.mutationCommitCursor();
3299
- return jsonResponse(commitCursor === void 0 ? { result } : { commitCursor, result }, 200, this.currentResponseBookmark);
3499
+ return jsonResponse(commitCursor === void 0 ? { result } : { commitCursor, result }, 200, bookmarkHeaders(this.currentResponseBookmark));
3300
3500
  }
3301
3501
  /**
3302
3502
  * Commit a mutation's replay bookkeeping — the `(identity, mutationId)`
@@ -4055,7 +4255,7 @@ class ShardDO {
4055
4255
  }
4056
4256
  results.push({ body: outcome.body, id: outcome.id, status: outcome.status });
4057
4257
  }
4058
- return jsonResponse({ results }, 200, latestBookmark);
4258
+ return jsonResponse({ results }, 200, bookmarkHeaders(latestBookmark));
4059
4259
  }
4060
4260
  /** Dispatch one batch entry through the single-call `/rpc` path and capture its envelope (plan 088). */
4061
4261
  async dispatchBatchEntry(batchRequest, entry) {
@@ -4063,12 +4263,12 @@ class ShardDO {
4063
4263
  const response = await this.fetch(buildBatchEntryRequest(batchRequest, entry));
4064
4264
  return { body: await response.json(), bookmark: response.headers.get("x-d1-bookmark") ?? void 0, id: entry.id, status: response.status };
4065
4265
  } catch (error) {
4066
- const message = error instanceof Error ? error.message : String(error);
4266
+ const { body, status } = toErrorBody(error, { fallbackCode: "BATCH_ENTRY_FAILED" });
4067
4267
  return {
4068
- body: { error: { code: "BATCH_ENTRY_FAILED", message } },
4268
+ body: { error: body },
4069
4269
  bookmark: void 0,
4070
4270
  id: entry?.id,
4071
- status: 500
4271
+ status
4072
4272
  };
4073
4273
  }
4074
4274
  }
@@ -4186,6 +4386,18 @@ class ShardDO {
4186
4386
  if (functionPath === ADMIN_FUNCTIONS.sendTestMail) {
4187
4387
  return this.handleSendTestMail(args);
4188
4388
  }
4389
+ if (functionPath === ADMIN_FUNCTIONS.recordQueueMessage) {
4390
+ return this.handleRecordQueueMessage(args);
4391
+ }
4392
+ if (functionPath === ADMIN_FUNCTIONS.clearQueueMessages) {
4393
+ return this.handleClearQueueMessages();
4394
+ }
4395
+ if (functionPath === ADMIN_FUNCTIONS.sendQueueMessage) {
4396
+ return this.handleSendQueueMessage(args);
4397
+ }
4398
+ if (functionPath === ADMIN_FUNCTIONS.replayQueueMessage) {
4399
+ return this.handleReplayQueueMessage(args);
4400
+ }
4189
4401
  if (functionPath === ADMIN_FUNCTIONS.createWorkflowInstance) {
4190
4402
  return this.handleCreateWorkflowInstance(args);
4191
4403
  }
@@ -4395,6 +4607,127 @@ class ShardDO {
4395
4607
  const result = recordCapturedMail(this.state.storage.sql, input, Date.now());
4396
4608
  return jsonResponse({ result }, 200);
4397
4609
  }
4610
+ /**
4611
+ * Serve `__lunora_admin__:recordQueueMessage` — the capture sink the generated
4612
+ * worker `queue()` handler (via `@lunora/queue`'s `dispatchQueueBatch`) posts
4613
+ * every consumed message batch to. Records it into the reserved
4614
+ * `__lunora_queue_messages` table (bounded, auto-trimmed) so the studio Queues
4615
+ * panel shows one unified consumed-message log across every push consumer. Like
4616
+ * the mail catcher, the gate is the admin token alone — a token holder can
4617
+ * already mutate the shard, so a token-gated capture insert adds no privilege.
4618
+ */
4619
+ handleRecordQueueMessage(args) {
4620
+ const messages = parseRecordQueueMessageArgs(args);
4621
+ const result = recordQueueMessages(this.state.storage.sql, messages, Date.now());
4622
+ return jsonResponse({ result }, 200);
4623
+ }
4624
+ /** Empty the dev queue consumed-message log (studio "clear log" action). Admin-gated by the caller. */
4625
+ handleClearQueueMessages() {
4626
+ const result = clearQueueMessages(this.state.storage.sql);
4627
+ return jsonResponse({ result }, 200);
4628
+ }
4629
+ /**
4630
+ * Serve `__lunora_admin__:sendQueueMessage` — the studio's "Send test message"
4631
+ * button. Resolves the declared queue's `QUEUE_*` producer binding and calls
4632
+ * `.send(body, { delaySeconds?, contentType? })`, or `.sendBatch(...)` when a
4633
+ * `batch` array is supplied. No SQLite write happens here (the message is only
4634
+ * captured once a consumer processes it), so this only records an audit entry.
4635
+ * Admin-gated by `handleAdminRpc`'s caller.
4636
+ */
4637
+ async handleSendQueueMessage(args) {
4638
+ const parsed = parseSendQueueMessageArgs(args);
4639
+ const { binding } = this.resolveQueueBinding(parsed.exportName);
4640
+ let sent;
4641
+ if (parsed.batch === void 0) {
4642
+ await binding.send(parsed.body, { contentType: parsed.contentType, delaySeconds: parsed.delaySeconds });
4643
+ sent = 1;
4644
+ } else {
4645
+ await binding.sendBatch(
4646
+ parsed.batch.map((body) => {
4647
+ return { body, contentType: parsed.contentType, delaySeconds: parsed.delaySeconds };
4648
+ })
4649
+ );
4650
+ sent = parsed.batch.length;
4651
+ }
4652
+ this.recordAudit("sendQueueMessage", { detail: { count: sent, exportName: parsed.exportName } });
4653
+ return jsonResponse({ result: { sent } }, 200);
4654
+ }
4655
+ /**
4656
+ * Serve `__lunora_admin__:replayQueueMessage` — the studio's one-click replay /
4657
+ * DLQ redrive. Looks the captured row up by id, resolves the destination export
4658
+ * (explicit `target` → the parent queue when the message was captured off a
4659
+ * dead-letter queue → the queue it was consumed from), and re-enqueues the
4660
+ * stored body onto that producer. Records an audit entry; no SQLite write beyond
4661
+ * that (the replayed message is re-captured when a consumer processes it).
4662
+ * Admin-gated by `handleAdminRpc`'s caller.
4663
+ */
4664
+ async handleReplayQueueMessage(args) {
4665
+ const parsed = parseReplayQueueMessageArgs(args);
4666
+ const row = readQueueMessageById(this.state.storage.sql, parsed.id);
4667
+ if (row === void 0) {
4668
+ throw Object.assign(new Error(`replayQueueMessage: captured message "${parsed.id}" was not found`), {
4669
+ code: "BAD_REQUEST",
4670
+ name: "LunoraError",
4671
+ status: 404
4672
+ });
4673
+ }
4674
+ if (isLossyBody(row.body)) {
4675
+ throw Object.assign(
4676
+ new Error(`replayQueueMessage: captured message "${parsed.id}" has a truncated or unserializable body and can't be replayed faithfully`),
4677
+ { code: "BAD_REQUEST", name: "LunoraError", status: 422 }
4678
+ );
4679
+ }
4680
+ const target = parsed.target ?? this.resolveReplayTarget(row.queue) ?? row.exportName;
4681
+ if (typeof target !== "string" || target === "") {
4682
+ throw Object.assign(new Error(`replayQueueMessage: captured message "${parsed.id}" has no declared producer to replay onto (pass \`target\`)`), {
4683
+ code: "BAD_REQUEST",
4684
+ name: "LunoraError",
4685
+ status: 400
4686
+ });
4687
+ }
4688
+ const { binding } = this.resolveQueueBinding(target);
4689
+ await binding.send(row.body);
4690
+ this.recordAudit("replayQueueMessage", { detail: { messageId: row.messageId, target }, id: parsed.id });
4691
+ return jsonResponse({ result: { sent: 1, target } }, 200);
4692
+ }
4693
+ /**
4694
+ * Resolve a declared queue's runtime producer binding from this shard's `env`.
4695
+ * Looks the `exportName` up in {@link queuesMetadata} (the codegen subclass's
4696
+ * statically-discovered list) to find its generated `QUEUE_*` binding, then
4697
+ * reads `env[binding]` and validates it carries `send`/`sendBatch`. A bad export
4698
+ * name or a missing/malformed binding throws a 400 `LunoraError` so the studio
4699
+ * surfaces an actionable message. Mirrors {@link resolveWorkflowBinding}.
4700
+ */
4701
+ resolveQueueBinding(exportName) {
4702
+ const metadata = this.queuesMetadata().queues.find((queue) => queue.exportName === exportName);
4703
+ if (!metadata) {
4704
+ throw Object.assign(new Error(`queue "${exportName}" is not declared`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
4705
+ }
4706
+ const binding = this.env?.[metadata.binding];
4707
+ if (typeof binding !== "object" || binding === null || typeof binding.send !== "function") {
4708
+ throw Object.assign(new Error(`queue binding "${metadata.binding}" is not available on this deployment`), {
4709
+ code: "BAD_REQUEST",
4710
+ name: "LunoraError",
4711
+ status: 400
4712
+ });
4713
+ }
4714
+ return { binding, metadata };
4715
+ }
4716
+ /**
4717
+ * Pick the replay destination export for a captured message's origin queue.
4718
+ * When the message was consumed off a queue that is another queue's dead-letter
4719
+ * queue, prefer that PARENT queue's producer (a DLQ usually has no producer of
4720
+ * its own) so replay redrives onto the original; otherwise re-enqueue onto the
4721
+ * queue the message came from. Returns `undefined` when neither is declared.
4722
+ */
4723
+ resolveReplayTarget(queueName) {
4724
+ const { queues } = this.queuesMetadata();
4725
+ const parent = queues.find((queue) => queue.deadLetterQueue === queueName);
4726
+ if (parent !== void 0) {
4727
+ return parent.exportName;
4728
+ }
4729
+ return queues.find((queue) => queue.name === queueName)?.exportName;
4730
+ }
4398
4731
  /**
4399
4732
  * Append one durable audit entry for a state-changing admin op that just
4400
4733
  * succeeded, folding the acting user (from `getCurrentUserId`) into `detail`.
@@ -4816,6 +5149,9 @@ class ShardDO {
4816
5149
  if (functionPath === ADMIN_FUNCTIONS.getCapturedMail) {
4817
5150
  return this.readAdminCapturedMail(sql, args);
4818
5151
  }
5152
+ if (functionPath === ADMIN_FUNCTIONS.getQueueMessages) {
5153
+ return this.readAdminQueueMessages(sql, args);
5154
+ }
4819
5155
  return void 0;
4820
5156
  }
4821
5157
  // eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `readAdmin*` resolvers
@@ -4846,6 +5182,28 @@ class ShardDO {
4846
5182
  }
4847
5183
  return { result, tables: /* @__PURE__ */ new Set([MAIL_TABLE]) };
4848
5184
  }
5185
+ /**
5186
+ * Resolve a `getQueueMessages` admin read — the dev queue catcher's consumed
5187
+ * message log (`queue-catcher.ts`), newest-first, optionally filtered to one
5188
+ * queue. Best-effort: a SQL failure returns an empty log rather than throwing.
5189
+ * Reported against the {@link QUEUE_TABLE} so this read participates in
5190
+ * table-scoped subscription invalidation, but new captures arrive via the
5191
+ * worker→root-shard `recordQueueMessage` write, which (like the mail catcher)
5192
+ * inserts directly without a `flushChangedTables` — so the panel refreshes on
5193
+ * its poll (`useAutoRefresh`) rather than a live push.
5194
+ */
5195
+ // eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `readAdmin*` resolvers
5196
+ readAdminQueueMessages(sql, args) {
5197
+ const limit = typeof args["limit"] === "number" ? args["limit"] : void 0;
5198
+ const queue = typeof args["queue"] === "string" ? args["queue"] : void 0;
5199
+ let result;
5200
+ try {
5201
+ result = readQueueMessages(sql, { limit, queue });
5202
+ } catch {
5203
+ result = { entries: [] };
5204
+ }
5205
+ return { result, tables: /* @__PURE__ */ new Set([QUEUE_TABLE]) };
5206
+ }
4849
5207
  /** Resolve a `readTablePage` admin read, parsing the loosely-typed args into the reader's options. */
4850
5208
  readAdminTablePage(sql, args) {
4851
5209
  const table = typeof args["table"] === "string" ? args["table"] : "";
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-DSUQ5fX9.mjs';
1
+ import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-BbQdj8h1.mjs';
2
2
 
3
3
  const serveRelationFanout = async (schema, database, functionPath, args) => {
4
4
  const table = typeof args["table"] === "string" ? args["table"] : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.22",
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
  },