@lunora/server 1.0.0-alpha.1 → 1.0.0-alpha.10

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.
Files changed (29) hide show
  1. package/README.md +5 -1
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/data-model.d.mts +104 -16
  4. package/dist/data-model.d.ts +104 -16
  5. package/dist/index.d.mts +269 -25
  6. package/dist/index.d.ts +269 -25
  7. package/dist/index.mjs +16 -12
  8. package/dist/packem_shared/{LunoraError-DhggBJZF.mjs → LunoraError-DN7Zhhvu.mjs} +4 -1
  9. package/dist/packem_shared/{definePresence-D5LtwGl0.mjs → PRESENCE_DEFAULT_TTL_MS-D8viLY1S.mjs} +4 -4
  10. package/dist/packem_shared/{bindTableFacade-DCuyr46L.mjs → bindOrm-Ce57S3N9.mjs} +58 -1
  11. package/dist/packem_shared/buildRlsReadRegistry-1jexWrb3.mjs +107 -0
  12. package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +55 -0
  13. package/dist/packem_shared/{defineAggregateIndex-DzqxtAyV.mjs → defineAggregateIndex-ZdyU78gh.mjs} +58 -3
  14. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
  15. package/dist/packem_shared/defineShape-CJ27Wx7o.mjs +17 -0
  16. package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
  17. package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-FLwfsePg.mjs} +1 -1
  18. package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-lxwHTEV3.mjs} +17 -3
  19. package/dist/packem_shared/{mask-CkZJHHMM.mjs → mask-BV_jNzsN.mjs} +2 -2
  20. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
  21. package/dist/packem_shared/{rls-Zhf5wEeJ.mjs → rls-2Jhd0uev.mjs} +22 -4
  22. package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-Cje6Woea.mjs} +1 -1
  23. package/dist/rls/testing.mjs +1 -1
  24. package/dist/types.d.mts +130 -2
  25. package/dist/types.d.ts +130 -2
  26. package/package.json +5 -5
  27. /package/dist/packem_shared/{defineEnv-DjFkpkSP.mjs → LunoraEnvError-DjFkpkSP.mjs} +0 -0
  28. /package/dist/packem_shared/{defineSchemaExtension-Ck5_TUO8.mjs → composePluginMiddleware-Ck5_TUO8.mjs} +0 -0
  29. /package/dist/packem_shared/{definePolicy-De67zPDS.mjs → createPolicyDsl-De67zPDS.mjs} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Validator, Infer, v } from '@lunora/values';
1
+ import { Validator, Infer, ValidatorMap, InferValidatorMap, v } from '@lunora/values';
2
2
  export { type ColumnValidator, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
3
- import { ArgsValidator, InferArgs, RegisteredAction, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, RelationDefinition, GlobalBackend, OnDeleteAction, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.js";
3
+ import { ArgsValidator, InferArgs, RegisteredAction, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.js";
4
4
  export { type AnyApi, type AuthState, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.js";
5
5
  import { Context, Hono } from 'hono';
6
6
  import { b as Permission, R as Role, T as TypedDefinePolicyInput, a as Policy, D as DefinePolicyInput, W as WhereInput, c as RlsOptions } from "./packem_shared/types.d-BDY0FYHK.js";
@@ -187,6 +187,13 @@ declare const initLunora: {
187
187
  dataModel: <DataModel>() => DataModelInit<DataModel>;
188
188
  };
189
189
  /**
190
+ * Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
191
+ * `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
192
+ * absent or non-Secrets-Store binding throws a directed error pointing at the
193
+ * wrangler config; the lookup is lazy, so an unused secret never resolves.
194
+ */
195
+ declare const createSecrets: (env: Record<string, unknown>) => Secrets;
196
+ /**
190
197
  * Redact secrets from a free-form message. Masks, in order: any quoted value
191
198
  * whose contents look like a credential (so a value surfaced as `received string
192
199
  * "sk_live_…"` is masked even though the surrounding text is not a token); a
@@ -276,7 +283,10 @@ declare const defineEnv: <S extends EnvShape>(shape: S) => EnvAccessor<S>;
276
283
  * The runtime's structural error mapper keys off `name === "LunoraError"` plus
277
284
  * the numeric `status`, so throwing one of these from a handler or middleware
278
285
  * yields the right RPC/HTTP status without any further wiring. `code` carries
279
- * the machine-readable reason for clients.
286
+ * the machine-readable reason for clients; the optional `data` carries a
287
+ * structured, JSON+wire-encodable payload propagated verbatim to the client
288
+ * (e.g. `{ retryAfterMs }`). Only an explicit `LunoraError`'s `data` crosses the
289
+ * wire — an unhandled throw is still redacted to a generic message server-side.
280
290
  */
281
291
  declare const CODE_STATUS: {
282
292
  readonly BAD_REQUEST: 400;
@@ -322,24 +332,11 @@ declare class LunoraError extends Error {
322
332
  override readonly name = "LunoraError";
323
333
  readonly code: LunoraErrorCode;
324
334
  readonly status: number;
325
- constructor(code: LunoraErrorCode, message?: string);
335
+ /** Structured, JSON+wire-encodable payload surfaced to the client alongside `code`. */
336
+ readonly data?: unknown;
337
+ constructor(code: LunoraErrorCode, message?: string, data?: unknown);
326
338
  }
327
339
  /**
328
- * The per-table `ctx.db` accessor (the `ctx.db.messages.findMany(...)` form) and
329
- * the kitcn-style `ctx.orm` namespace, as plain runtime helpers. This is the ONE
330
- * source of truth for the facade shape, shared by two callers so they can never
331
- * drift (a drift here is security-relevant — a facade accessor the RLS
332
- * middleware forgot to re-bind would read around policy). `@lunora/codegen`
333
- * emits `ctx.db`/`ctx.orm` by calling these over the raw shard writer (and the
334
- * D1 `globalDb` writer for `.global()` tables); the RLS middleware re-binds the
335
- * policy tables by calling them over the policy-enforcing wrapped writer.
336
- *
337
- * `bindTableFacade(writer, table)` pins `tableName` on the structural writer so
338
- * callers address rows by id (`get`/`delete`/`patch`/`replace`) or by the bound
339
- * table (everything else). The binding is identical regardless of which writer
340
- * is passed — that's the whole point.
341
- */
342
- /**
343
340
  * Minimal structural writer the facade binds over. Declared with **method**
344
341
  * syntax (not arrow properties) so a more-specifically-typed writer — both
345
342
  * `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
@@ -349,7 +346,9 @@ declare class LunoraError extends Error {
349
346
  interface FacadeWriterLike {
350
347
  aggregate(tableName: string, options: unknown): Promise<unknown>;
351
348
  count(tableName: string, where?: unknown): Promise<number>;
352
- delete(id: string, expectedTable?: string): Promise<void>;
349
+ delete(id: string, expectedTable?: string, options?: {
350
+ hard?: boolean;
351
+ }): Promise<void>;
353
352
  deleteMany?(ids: ReadonlyArray<string>, options?: {
354
353
  limit?: number;
355
354
  }, expectedTable?: string): Promise<{
@@ -377,6 +376,7 @@ interface FacadeWriterLike {
377
376
  rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
378
377
  rankPage(tableName: string, indexName: string, options?: unknown): Promise<unknown>;
379
378
  replace(id: string, document: Record<string, unknown>, expectedTable?: string): Promise<void>;
379
+ restore?(id: string, expectedTable?: string): Promise<void>;
380
380
  }
381
381
  /** The per-table accessor object returned for the `ctx.db` table form. */
382
382
  interface FacadeEntry {
@@ -388,12 +388,16 @@ interface FacadeEntry {
388
388
  }) => Promise<{
389
389
  deleted: number;
390
390
  }>;
391
+ /** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
392
+ exists: (where?: unknown) => Promise<boolean>;
391
393
  findFirst: (args?: unknown) => Promise<unknown>;
392
394
  findFirstOrThrow: (args?: unknown) => Promise<unknown>;
393
395
  findMany: (args?: unknown) => Promise<unknown>;
394
396
  get: (id: string) => Promise<unknown>;
395
397
  groupBy: (options: unknown) => Promise<unknown>;
396
- insert: (document: Record<string, unknown>) => Promise<string>;
398
+ /** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
399
+ hardDelete: (id: string) => Promise<void>;
400
+ insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
397
401
  insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
398
402
  limit?: number;
399
403
  }) => Promise<string[]>;
@@ -407,8 +411,47 @@ interface FacadeEntry {
407
411
  rank: (indexName: string, options: unknown) => Promise<unknown>;
408
412
  rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
409
413
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
414
+ /** Un-soft-delete a row: clears the `.softDelete()` marker (by-id, so it reaches a row list reads hide). */
415
+ restore: (id: string) => Promise<void>;
416
+ /** Insert when no row matches `target`, else patch the match. Composes `findFirst` + `insert`/`patch`, so RLS applies to each step. */
417
+ upsert: (args: UpsertArgs) => Promise<UpsertResult>;
418
+ /** Sequential `upsert` over many rows sharing one `target`; returns one result per input row in order. */
419
+ upsertMany: (args: UpsertManyArgs) => Promise<UpsertResult[]>;
410
420
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
411
421
  }
422
+ /** Options accepted by the per-table `insert` accessor. */
423
+ interface FacadeInsertOptions {
424
+ /**
425
+ * When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
426
+ * silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
427
+ * other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
428
+ */
429
+ skipDuplicates?: boolean;
430
+ }
431
+ /** The conflict target for `upsert`/`upsertMany`: one field name or a tuple of them. */
432
+ type UpsertTarget = ReadonlyArray<string> | string;
433
+ /** Argument to the per-table `upsert` accessor. */
434
+ interface UpsertArgs {
435
+ /** Document inserted when no existing row matches the `target`. */
436
+ create: Record<string, unknown>;
437
+ /** Field(s) — typically a `.unique()` column or unique index — used to look up an existing row. */
438
+ target: UpsertTarget;
439
+ /** Patch applied when an existing row matches the `target`. Defaults to `create`. */
440
+ update?: Record<string, unknown>;
441
+ }
442
+ /** Result of an `upsert`: the row's id and whether it was freshly inserted (`true`) or updated (`false`). */
443
+ interface UpsertResult {
444
+ created: boolean;
445
+ id: string;
446
+ }
447
+ /** Argument to the per-table `upsertMany` accessor — a shared `target` plus per-row create/update payloads. */
448
+ interface UpsertManyArgs {
449
+ rows: ReadonlyArray<{
450
+ create: Record<string, unknown>;
451
+ update?: Record<string, unknown>;
452
+ }>;
453
+ target: UpsertTarget;
454
+ }
412
455
  /**
413
456
  * Bind a structural writer to one table, producing its `ctx.db` table accessor.
414
457
  *
@@ -426,7 +469,7 @@ declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) =>
426
469
  interface OrmLike {
427
470
  delete: (table: string, id: string) => Promise<void>;
428
471
  insert: (table: string) => {
429
- values: (document: Record<string, unknown>) => Promise<string>;
472
+ values: (document: Record<string, unknown>) => Promise<null | string>;
430
473
  };
431
474
  query: Record<string, FacadeEntry>;
432
475
  replace: (table: string, id: string) => {
@@ -858,6 +901,55 @@ interface RegisteredMigration extends MigrationDefinition {
858
901
  /** Declare an online data migration. See the module docs for runtime semantics. */
859
902
  declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
860
903
  /**
904
+ * A mutator declaration. `server` is authoritative; `client` is the optimistic
905
+ * twin (optional — omit it to let the optimistic write fall through to the
906
+ * server round-trip with no local preview). Both receive the same validated
907
+ * `args`.
908
+ */
909
+ interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
910
+ /**
911
+ * Validator for the mutator's arguments. Validated on the DO before `server`
912
+ * runs and (when present) on the client before `client` runs, so both impls
913
+ * see the same parsed shape. Omit for a parameterless mutator.
914
+ */
915
+ readonly args?: Args;
916
+ /**
917
+ * Optimistic client implementation. Runs in a TanStack DB transaction
918
+ * against the local collections; its writes are applied immediately and
919
+ * automatically rolled back / rebased as the authoritative result syncs
920
+ * back. Pure and side-effect-free beyond the local store. Omit to skip the
921
+ * local preview.
922
+ */
923
+ readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
924
+ /**
925
+ * Authoritative server implementation. Runs inside the shard DO with a full
926
+ * {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
927
+ * and poke back to subscribers. This is the source of truth — the client
928
+ * impl is only a prediction of it.
929
+ */
930
+ readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
931
+ }
932
+ /**
933
+ * A {@link MutatorDefinition} plus the codegen discovery marker and a
934
+ * dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
935
+ * invokes a mutator exactly like a registered procedure.
936
+ */
937
+ interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
938
+ readonly __lunoraMutator: true;
939
+ /** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
940
+ readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
941
+ /**
942
+ * Marks the dispatch kind so codegen can register the mutator in the same
943
+ * `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
944
+ * reads `kind === "mutation"` to wrap the authoritative `server` impl in the
945
+ * shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
946
+ * ordinary `mutation`.
947
+ */
948
+ readonly kind: "mutation";
949
+ }
950
+ /** Declare a custom mutator. See the module docs for runtime semantics. */
951
+ declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
952
+ /**
861
953
  * The prefixed tables a single plugin `P` contributes, or an empty map when it
862
954
  * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
863
955
  * so {@link InstalledTables} can fold a tuple of plugins.
@@ -1168,6 +1260,35 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1168
1260
  }) => TableBuilder<Shape>;
1169
1261
  /** Route storage by the named field — one DO per distinct value. */
1170
1262
  shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
1263
+ /**
1264
+ * Turn on soft delete. Adds a nullable timestamp column (`options.field`,
1265
+ * default `deletedAt`) and changes `ctx.db.&lt;table>.delete()` to **set** it
1266
+ * instead of removing the row; `onDelete: "cascade"` children are recursively
1267
+ * soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
1268
+ * `aggregate`/relation loads) then hide soft-deleted rows unless they pass
1269
+ * `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
1270
+ * `restore()` still address the row directly. `hardDelete()` physically
1271
+ * removes it (cascading as a real delete). Note: `includeDeleted` is a read
1272
+ * scope, not access control — anyone who can run the read can set it; a unique
1273
+ * index still rejects a new row that collides with a soft-deleted one (the row
1274
+ * physically persists).
1275
+ */
1276
+ softDelete: (options?: {
1277
+ field?: string;
1278
+ }) => TableBuilder<Shape>;
1279
+ /**
1280
+ * Materialize this table from an external Postgres/MySQL behind Cloudflare
1281
+ * Hyperdrive (plan 077). A system-driven poll loop reads the tenant slice
1282
+ * (`query`, with params bound from `tenantBy`) and lands it in the DO's SQLite,
1283
+ * after which `defineShape` carries it to clients unchanged. Implies
1284
+ * `.externallyManaged()` (rows come from the ingest loop, not user mutations).
1285
+ *
1286
+ * Orthogonal to `.shardBy()` — combine them for per-tenant DOs. **Under
1287
+ * `.shardBy()` `tenantBy` is mandatory** (the tenant-isolation boundary); the
1288
+ * `external_source_unscoped` advisor lint fails the build when it is absent, and
1289
+ * `external_source_on_global` rejects combining `.source()` with `.global()`.
1290
+ */
1291
+ source: (definition: ExternalSourceDefinition) => TableBuilder<Shape>;
1171
1292
  /** Declare named lifecycle triggers fired inline within the write path. */
1172
1293
  triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
1173
1294
  /** Declare a vector index over a single text field on this table. */
@@ -1190,7 +1311,7 @@ interface VectorIndexOptions {
1190
1311
  * Build a table definition. Returned object is both the table definition (for
1191
1312
  * `defineSchema`) and a fluent builder for indexes + sharding metadata.
1192
1313
  */
1193
- declare const defineTable: <Shape extends Record<string, Validator>>(shape: Shape) => TableBuilder<Shape>;
1314
+ declare const defineTable: <Shape extends Record<string, Validator>>(inputShape: Shape) => TableBuilder<Shape>;
1194
1315
  /**
1195
1316
  * Declare a standalone vector index (DSL Shape B). Pass the returned value in
1196
1317
  * the `vectorIndexes` map of {@link defineSchema} when the source is derived
@@ -1262,6 +1383,28 @@ type ExtendableSchema<T extends Record<string, TableDefinition>> = {
1262
1383
  readonly key: Key;
1263
1384
  }) => ExtendableSchema<PrefixedTables<X, Key> & T>;
1264
1385
  /**
1386
+ * Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
1387
+ * the scheduler, and `ctx.containers` — to a Cloudflare data-residency
1388
+ * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1389
+ * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1390
+ * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1391
+ * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1392
+ *
1393
+ * ⚠️ **Set this once, before your first deploy — changing or removing it
1394
+ * strands data.** A Durable Object name maps to a *different* ID in each
1395
+ * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1396
+ * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1397
+ * old jurisdiction's DOs and is no longer reachable. There is no in-place
1398
+ * migration — you would have to export from the old jurisdiction and import
1399
+ * into the new one.
1400
+ *
1401
+ * Note: this pins **DO-backed** state only. D1-backed state — `.global()`
1402
+ * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1403
+ * settings, not this option.
1404
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1405
+ */
1406
+ jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
1407
+ /**
1265
1408
  * Turn on secure-by-default RLS for the whole schema. Every table is then
1266
1409
  * protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1267
1410
  * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
@@ -1562,7 +1705,9 @@ interface DatabaseWriterLike {
1562
1705
  */
1563
1706
  aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
1564
1707
  count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
1565
- delete: (id: string, expectedTable?: string) => Promise<void>;
1708
+ delete: (id: string, expectedTable?: string, options?: {
1709
+ hard?: boolean;
1710
+ }) => Promise<void>;
1566
1711
  deleteMany: (ids: ReadonlyArray<string>, options?: {
1567
1712
  limit?: number;
1568
1713
  }, expectedTable?: string) => Promise<{
@@ -1633,6 +1778,7 @@ interface DatabaseWriterLike {
1633
1778
  */
1634
1779
  rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
1635
1780
  replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1781
+ restore?: (id: string, expectedTable?: string) => Promise<void>;
1636
1782
  }
1637
1783
  /**
1638
1784
  * What a procedure's `ctx.db` must structurally satisfy for the middleware
@@ -1656,6 +1802,104 @@ interface RlsContextIn {
1656
1802
  }
1657
1803
  declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
1658
1804
  /**
1805
+ * One `rls()` tag's read policies for a table, paired with the role→permission
1806
+ * grants of that SAME middleware. Keeping the role map per-group is what lets a
1807
+ * policy's `auth.can(...)` resolve against its own middleware's roles — never a
1808
+ * permission registered on a different `rls()` step.
1809
+ */
1810
+ interface ScopedReadPolicies {
1811
+ readonly policies: ReadonlyArray<Policy>;
1812
+ readonly rolePermissions: ReadonlyMap<string, ReadonlySet<string>>;
1813
+ }
1814
+ /** Table-indexed read-policy groups, each scoped to the roles of the rls() middleware that declared it. */
1815
+ interface RlsReadRegistry {
1816
+ readonly byTable: ReadonlyMap<string, ReadonlyArray<ScopedReadPolicies>>;
1817
+ }
1818
+ /** The trusted, server-resolved facts a shape's RLS evaluation runs under. */
1819
+ interface ShapeReadWhereRequest {
1820
+ /** The shape ctx (the procedure context a policy `when` reads as `ctx`). */
1821
+ readonly ctx: unknown;
1822
+ /** Resolved identity claims (the socket's verified identity), or `null` when anonymous. */
1823
+ readonly identity: Record<string, unknown> | null;
1824
+ /** `true` when the schema is `.rls("required")` — gates the fail-closed branch. */
1825
+ readonly rlsRequired: boolean;
1826
+ /** Role labels the request carries (drives `auth.can(...)`). */
1827
+ readonly roles: ReadonlyArray<string>;
1828
+ /** The shape's own predicate (`where(ctx, args)`). */
1829
+ readonly shapeWhere: WhereInput;
1830
+ /** Logical table the shape replicates. */
1831
+ readonly table: string;
1832
+ /** `true` when the table is `.public()` (exempt from `.rls("required")` denial). */
1833
+ readonly tablePublic: boolean;
1834
+ /** Verified user id, or `null` when anonymous. */
1835
+ readonly userId: null | string;
1836
+ }
1837
+ /**
1838
+ * Build the read-policy registry from the registered functions (pass
1839
+ * `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
1840
+ * grouped per `rls()` middleware so each group keeps its own role→permission map
1841
+ * (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
1842
+ * several procedures (a shared `const guard = rls(...)`) is folded once. This
1843
+ * mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
1844
+ * resolves against the roles of the middleware that declared it, never a union.
1845
+ */
1846
+ declare const buildRlsReadRegistry: (functions: Iterable<unknown>) => RlsReadRegistry;
1847
+ /**
1848
+ * Compute the effective `where` a shape replicates: the table's RLS read
1849
+ * base-where AND the shape's own predicate. Returns the shape predicate
1850
+ * unchanged for a table with no read policy (a `.public()` or non-RLS table),
1851
+ * and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
1852
+ * exposes a protected, policy-less table.
1853
+ */
1854
+ declare const composeShapeReadWhere: (registry: RlsReadRegistry, request: ShapeReadWhereRequest) => WhereInput;
1855
+ /**
1856
+ * A shape declaration. `where` receives the trusted procedure context and the
1857
+ * validated client args and returns the same {@link WhereInput} shape the RLS
1858
+ * DSL uses, so the DO can AND-merge it with the table's read base-where via the
1859
+ * existing where-compiler (zero second predicate implementation).
1860
+ */
1861
+ interface ShapeDefinition<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> {
1862
+ /**
1863
+ * Validator for the client-supplied shape parameters. Validated on the DO
1864
+ * before `where` runs, so a malformed `args` envelope is rejected at the
1865
+ * subscription boundary rather than silently widening the partition. Omit
1866
+ * for a parameterless shape.
1867
+ */
1868
+ readonly args?: Args;
1869
+ /**
1870
+ * Project the replicated rows to these columns (the system columns `_id` and
1871
+ * `_creationTime` are always included). Omit to replicate every column. An
1872
+ * empty array is rejected — it would replicate no data, which is never the
1873
+ * intent.
1874
+ */
1875
+ readonly columns?: ReadonlyArray<string>;
1876
+ /** Logical table this shape replicates a partition of. */
1877
+ readonly table: string;
1878
+ /**
1879
+ * Predicate selecting the rows this shape replicates. AND-composed with the
1880
+ * table's RLS read base-where on the DO. Runs server-side with a trusted
1881
+ * `ctx` (identity/auth the client can't forge) and the validated client
1882
+ * `args`; returns a {@link WhereInput} using the same operator set as the
1883
+ * SQL compiler (`eq`/`in`/`lt`/… + `AND`/`OR`/`NOT`).
1884
+ */
1885
+ readonly where: (context: Context, args: InferValidatorMap<Args>) => WhereInput;
1886
+ }
1887
+ /** A {@link ShapeDefinition} plus the codegen discovery marker and a dispatch-shaped `compileWhere`. */
1888
+ interface RegisteredShape<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> extends ShapeDefinition<Args, Context> {
1889
+ readonly __lunoraShape: true;
1890
+ /**
1891
+ * Validate `rawArgs`, then evaluate `where` under the trusted `ctx` and
1892
+ * return its {@link WhereInput}. Used by the generated DO's `resolveShape`
1893
+ * override: `ctx` is erased to `unknown` at this dispatch boundary (the DO
1894
+ * builds it from the socket's verified identity and hands it back as the
1895
+ * concrete {@link QueryContext} the predicate expects), exactly like
1896
+ * `RegisteredLunoraFunction.handler` erases its context.
1897
+ */
1898
+ readonly compileWhere: (context: unknown, rawArgs: Record<string, unknown>) => WhereInput;
1899
+ }
1900
+ /** Declare a replication shape. See the module docs for runtime semantics. */
1901
+ declare const defineShape: <Args extends ValidatorMap = ValidatorMap, Context = QueryCtx>(definition: ShapeDefinition<Args, Context>) => RegisteredShape<Args, Context>;
1902
+ /**
1659
1903
  * Operations a storage rule can gate. `read` covers `download` / `getMetadata`
1660
1904
  * / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
1661
1905
  * `delete` is `delete`; `list` is a prefix listing (governed via the file
@@ -1738,4 +1982,4 @@ interface StorageContextIn {
1738
1982
  }
1739
1983
  declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
1740
1984
  declare const VERSION = "0.0.0";
1741
- export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, type DataModelInit, type DefineComponentOptions, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExtendableSchema, type FacadeEntry, type FacadeWriterLike, type FunctionKind, type HttpActionCtx, type HttpActionHandler, type HttpMethod, type HttpRoute, type HttpRouteBuilder, type HttpRouteFactory, type HttpRouteHandlerOptions, type HttpStreamHandlerOptions, type InferArgs, type InferEnv, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraErrorCode, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationDefinition, type MigrationDocument, type MigrationTransform, type MutationBuilder, type MutationCtx, type OnDeleteAction, type OneRelation, type OrmLike, DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, type Permission, type Plugin, type Policy, type PrefixedTables, type PresenceComponent, type PresenceFunctions, type PresenceMember, type ProtectPublicOptions, type QueryBuilder, type QueryCtx, type RankIndexDefinition, type RankIndexOptions, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type Role, type Schema, type SchemaExtension, type StorageOperation, type StorageRule, type StorageRuleContext, type StorageRuleDecision, type StorageRulesOptions, type TableBuilder, type TableDefinition, type TerminalKind, type TriggerBuilder, type TriggerDefinition, type TypedDefinePolicyInput, VERSION, type VectorEmbedder, type VectorIndexDefinition, type VectorIndexOptions, type VectorMetric, type VectorizeOptions, type WhereInput, asBucketStorage, bindOrm, bindTableFacade, composePluginMiddleware, createPolicyDsl, defineAggregateIndex, defineComponent, defineEnv, defineMigration, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, httpAction, httpRoute, httpRouter, initLunora, installPlugins, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules };
1985
+ export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, type DataModelInit, type DefineComponentOptions, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExtendableSchema, type FacadeEntry, type FacadeWriterLike, type FunctionKind, type HttpActionCtx, type HttpActionHandler, type HttpMethod, type HttpRoute, type HttpRouteBuilder, type HttpRouteFactory, type HttpRouteHandlerOptions, type HttpStreamHandlerOptions, type InferArgs, type InferEnv, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraErrorCode, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationDefinition, type MigrationDocument, type MigrationTransform, type MutationBuilder, type MutationCtx, type MutatorDefinition, type OnDeleteAction, type OneRelation, type OrmLike, DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, type Permission, type Plugin, type Policy, type PrefixedTables, type PresenceComponent, type PresenceFunctions, type PresenceMember, type ProtectPublicOptions, type QueryBuilder, type QueryCtx, type RankIndexDefinition, type RankIndexOptions, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredMutator, type RegisteredQuery, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, type StorageOperation, type StorageRule, type StorageRuleContext, type StorageRuleDecision, type StorageRulesOptions, type TableBuilder, type TableDefinition, type TerminalKind, type TriggerBuilder, type TriggerDefinition, type TypedDefinePolicyInput, VERSION, type VectorEmbedder, type VectorIndexDefinition, type VectorIndexOptions, type VectorMetric, type VectorizeOptions, type WhereInput, asBucketStorage, bindOrm, bindTableFacade, buildRlsReadRegistry, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, httpAction, httpRoute, httpRouter, initLunora, installPlugins, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules };
package/dist/index.mjs CHANGED
@@ -1,23 +1,27 @@
1
1
  export { default as asBucketStorage } from './packem_shared/asBucketStorage-Cnxd9y2q.mjs';
2
- export { initLunora } from './packem_shared/initLunora-CATvPsVt.mjs';
3
- export { LunoraEnvError, defineEnv, redactSecrets } from './packem_shared/defineEnv-DjFkpkSP.mjs';
4
- export { LunoraError } from './packem_shared/LunoraError-DhggBJZF.mjs';
5
- export { bindOrm, bindTableFacade } from './packem_shared/bindTableFacade-DCuyr46L.mjs';
6
- export { httpAction, httpRoute, httpRouter, serveStorageObject } from './packem_shared/httpAction-B7FYUEgr.mjs';
2
+ export { initLunora } from './packem_shared/initLunora-lxwHTEV3.mjs';
3
+ export { createSecrets } from './packem_shared/createSecrets-TsIP9lOa.mjs';
4
+ export { LunoraEnvError, defineEnv, redactSecrets } from './packem_shared/LunoraEnvError-DjFkpkSP.mjs';
5
+ export { LunoraError } from './packem_shared/LunoraError-DN7Zhhvu.mjs';
6
+ export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-Ce57S3N9.mjs';
7
+ export { httpAction, httpRoute, httpRouter, serveStorageObject } from './packem_shared/httpAction-FLwfsePg.mjs';
7
8
  export { onConnect, onDisconnect } from './packem_shared/onConnect-CIPXKPyw.mjs';
8
9
  export { defineMigration } from './packem_shared/defineMigration-CAJLr6fx.mjs';
9
- export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/defineSchemaExtension-Ck5_TUO8.mjs';
10
- export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/definePresence-D5LtwGl0.mjs';
10
+ export { defineMutator } from './packem_shared/defineMutator-EIXAWhs9.mjs';
11
+ export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/composePluginMiddleware-Ck5_TUO8.mjs';
12
+ export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-D8viLY1S.mjs';
11
13
  export { protectPublic } from './packem_shared/protectPublic-BjFkQ_Or.mjs';
12
- export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-DzqxtAyV.mjs';
14
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-ZdyU78gh.mjs';
15
+ export { defineShape } from './packem_shared/defineShape-CJ27Wx7o.mjs';
13
16
  export { anyApi } from './types.mjs';
14
17
  export { cronJobs } from '@lunora/scheduler';
15
18
  export { ValidationError, v } from '@lunora/values';
16
- export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/definePolicy-De67zPDS.mjs';
19
+ export { buildRlsReadRegistry, composeShapeReadWhere } from './packem_shared/buildRlsReadRegistry-1jexWrb3.mjs';
20
+ export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/createPolicyDsl-De67zPDS.mjs';
17
21
  export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-qu0mpilX.mjs';
18
- export { mask } from './packem_shared/mask-CkZJHHMM.mjs';
19
- export { rls } from './packem_shared/rls-Zhf5wEeJ.mjs';
20
- export { storageRules } from './packem_shared/storageRules-4a30FSpI.mjs';
22
+ export { mask } from './packem_shared/mask-BV_jNzsN.mjs';
23
+ export { rls } from './packem_shared/rls-2Jhd0uev.mjs';
24
+ export { storageRules } from './packem_shared/storageRules-Cje6Woea.mjs';
21
25
 
22
26
  const VERSION = "0.0.0";
23
27
 
@@ -41,10 +41,13 @@ class LunoraError extends Error {
41
41
  name = "LunoraError";
42
42
  code;
43
43
  status;
44
- constructor(code, message) {
44
+ /** Structured, JSON+wire-encodable payload surfaced to the client alongside `code`. */
45
+ data;
46
+ constructor(code, message, data) {
45
47
  super(message ?? code);
46
48
  this.code = code;
47
49
  this.status = CODE_STATUS[code];
50
+ this.data = data;
48
51
  }
49
52
  }
50
53
 
@@ -1,9 +1,9 @@
1
1
  import { v } from '@lunora/values';
2
- import { initLunora } from './initLunora-CATvPsVt.mjs';
3
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
2
+ import { initLunora } from './initLunora-lxwHTEV3.mjs';
3
+ import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
4
4
  import { onDisconnect } from './onConnect-CIPXKPyw.mjs';
5
- import { defineSchemaExtension, defineComponent } from './defineSchemaExtension-Ck5_TUO8.mjs';
6
- import { defineTable } from './defineAggregateIndex-DzqxtAyV.mjs';
5
+ import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-Ck5_TUO8.mjs';
6
+ import { defineTable } from './defineAggregateIndex-ZdyU78gh.mjs';
7
7
 
8
8
  const DEFAULT_TTL_MS = 3e4;
9
9
  const MAX_DATA_BYTES = 4096;
@@ -1,4 +1,42 @@
1
+ const isUniqueConflict = (error) => typeof error === "object" && error !== null && error.code === "CONFLICT" && error.kind === "unique";
2
+ const buildUpsertWhere = (tableName, target, create) => {
3
+ const fields = typeof target === "string" ? [target] : target;
4
+ if (fields.length === 0) {
5
+ throw new Error(`ctx.db.${tableName}.upsert: "target" must name at least one field`);
6
+ }
7
+ const where = {};
8
+ for (const field of fields) {
9
+ if (!(field in create)) {
10
+ throw new Error(`ctx.db.${tableName}.upsert: target field "${field}" is missing from the create document`);
11
+ }
12
+ where[field] = create[field];
13
+ }
14
+ return where;
15
+ };
1
16
  const bindTableFacade = (writer, tableName) => {
17
+ const insert = async (document, options) => {
18
+ if (options?.skipDuplicates !== true) {
19
+ return writer.insert(tableName, document);
20
+ }
21
+ try {
22
+ return await writer.insert(tableName, document);
23
+ } catch (error) {
24
+ if (isUniqueConflict(error)) {
25
+ return null;
26
+ }
27
+ throw error;
28
+ }
29
+ };
30
+ const upsert = async ({ create, target, update }) => {
31
+ const where = buildUpsertWhere(tableName, target, create);
32
+ const existing = await writer.findFirst(tableName, { where });
33
+ if (existing && typeof existing["_id"] === "string") {
34
+ await writer.patch(existing["_id"], update ?? create, tableName);
35
+ return { created: false, id: existing["_id"] };
36
+ }
37
+ const id = await writer.insert(tableName, create);
38
+ return { created: true, id };
39
+ };
2
40
  return {
3
41
  aggregate: (options) => writer.aggregate(tableName, options),
4
42
  count: (where) => writer.count(tableName, where),
@@ -14,12 +52,17 @@ const bindTableFacade = (writer, tableName) => {
14
52
  }
15
53
  return writer.deleteMany(ids, options, tableName);
16
54
  },
55
+ // `exists` reuses `findFirst` (RLS-filtered, indexed when a `.withIndex`-able
56
+ // `where` is supplied) and only asks whether a row came back — no count scan.
57
+ exists: async (where) => await writer.findFirst(tableName, where === void 0 ? void 0 : { where }) !== null,
17
58
  findFirst: (args) => writer.findFirst(tableName, args),
18
59
  findFirstOrThrow: (args) => writer.findFirstOrThrow(tableName, args),
19
60
  findMany: (args) => writer.findMany(tableName, args),
20
61
  get: (id) => writer.get(id, tableName),
21
62
  groupBy: (options) => writer.groupBy(tableName, options),
22
- insert: (document) => writer.insert(tableName, document),
63
+ // Physical removal — bypasses `.softDelete()`. RLS gates it as a delete.
64
+ hardDelete: (id) => writer.delete(id, tableName, { hard: true }),
65
+ insert,
23
66
  insertMany: (documents, options) => {
24
67
  if (writer.insertMany === void 0) {
25
68
  throw new Error(`ctx.db.${tableName}.insertMany is unavailable: this writer has no batch insert`);
@@ -42,6 +85,20 @@ const bindTableFacade = (writer, tableName) => {
42
85
  rank: (indexName, options) => writer.rank(tableName, indexName, options),
43
86
  rankPage: (indexName, options) => writer.rankPage(tableName, indexName, options),
44
87
  replace: (id, document) => writer.replace(id, document, tableName),
88
+ restore: async (id) => {
89
+ if (!writer.restore) {
90
+ throw new Error(`ctx.db.${tableName}.restore is unavailable: this writer has no restore (is the table .softDelete()?)`);
91
+ }
92
+ await writer.restore(id, tableName);
93
+ },
94
+ upsert,
95
+ upsertMany: async ({ rows, target }) => {
96
+ const results = [];
97
+ for (const row of rows) {
98
+ results.push(await upsert({ create: row.create, target, update: row.update }));
99
+ }
100
+ return results;
101
+ },
45
102
  withSearchIndex: (indexName, search) => writer.query(tableName).withSearchIndex(indexName, search)
46
103
  };
47
104
  };