@lunora/server 1.0.0-alpha.2 → 1.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +51 -1
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/data-model.d.mts +158 -32
  5. package/dist/data-model.d.ts +158 -32
  6. package/dist/index.d.mts +459 -117
  7. package/dist/index.d.ts +459 -117
  8. package/dist/index.mjs +20 -15
  9. package/dist/packem_shared/{LunoraEnvError-DjFkpkSP.mjs → LunoraEnvError-BGmd1Qs0.mjs} +4 -4
  10. package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +9 -0
  11. package/dist/packem_shared/{PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs → PRESENCE_DEFAULT_TTL_MS-BtPCiNLW.mjs} +4 -4
  12. package/dist/packem_shared/bindOrm-Bfgl7f-Q.mjs +146 -0
  13. package/dist/packem_shared/buildRlsReadRegistry-D54vUQe4.mjs +107 -0
  14. package/dist/packem_shared/{composePluginMiddleware-Ck5_TUO8.mjs → composePluginMiddleware-CcTj1_v2.mjs} +9 -6
  15. package/dist/packem_shared/{createPolicyDsl-De67zPDS.mjs → createPolicyDsl-By3QB4he.mjs} +4 -1
  16. package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +58 -0
  17. package/dist/packem_shared/{defineAggregateIndex-DxSso0rH.mjs → defineAggregateIndex-znq4ygKQ.mjs} +69 -10
  18. package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +35 -0
  19. package/dist/packem_shared/{defineMigration-CAJLr6fx.mjs → defineMigration-Hx01yIht.mjs} +3 -1
  20. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
  21. package/dist/packem_shared/defineShape-C5scNOrf.mjs +18 -0
  22. package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs} +4 -1
  23. package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
  24. package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-DCXoYPIk.mjs} +42 -27
  25. package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-sQUqaejx.mjs} +18 -4
  26. package/dist/packem_shared/{mask-Jc84C_hK.mjs → mask-BT1YgRbF.mjs} +89 -9
  27. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
  28. package/dist/packem_shared/{protectPublic-BjFkQ_Or.mjs → protectPublic-BlcGpiRc.mjs} +1 -1
  29. package/dist/packem_shared/{rls-BDKRbMCA.mjs → rls-B_ZWgslr.mjs} +51 -7
  30. package/dist/packem_shared/{run-middleware-CYQOuoV6.mjs → run-middleware-I6EiQfxL.mjs} +3 -1
  31. package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-6QxzDOcx.mjs} +1 -1
  32. package/dist/packem_shared/{types.d-DmvyEMD6.d.mts → types.d-BB3pjV0m.d.mts} +10 -4
  33. package/dist/packem_shared/{types.d-BDY0FYHK.d.ts → types.d-Cxl6ndhm.d.ts} +10 -4
  34. package/dist/rls/testing.d.mts +1 -1
  35. package/dist/rls/testing.d.ts +1 -1
  36. package/dist/rls/testing.mjs +1 -1
  37. package/dist/types.d.mts +206 -5
  38. package/dist/types.d.ts +206 -5
  39. package/package.json +6 -5
  40. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  41. package/dist/packem_shared/bindOrm-DCuyr46L.mjs +0 -71
package/dist/index.mjs CHANGED
@@ -1,23 +1,28 @@
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/LunoraEnvError-DjFkpkSP.mjs';
4
- export { LunoraError } from './packem_shared/LunoraError-DhggBJZF.mjs';
5
- export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-DCuyr46L.mjs';
6
- export { httpAction, httpRoute, httpRouter, serveStorageObject } from './packem_shared/httpAction-B7FYUEgr.mjs';
2
+ export { initLunora } from './packem_shared/initLunora-sQUqaejx.mjs';
3
+ export { createSecrets } from './packem_shared/createSecrets-DwaR2rNG.mjs';
4
+ export { LunoraEnvError, defineEnv, redactSecrets } from './packem_shared/LunoraEnvError-BGmd1Qs0.mjs';
5
+ export { LunoraError } from './packem_shared/LunoraError-WbxmrpxR.mjs';
6
+ export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-Bfgl7f-Q.mjs';
7
+ export { httpAction, httpRoute, httpRouter, isSafeHeaderValue, serveStorageObject } from './packem_shared/httpAction-DCXoYPIk.mjs';
8
+ export { defineIdentity } from './packem_shared/defineIdentity-DiX4zM9x.mjs';
7
9
  export { onConnect, onDisconnect } from './packem_shared/onConnect-CIPXKPyw.mjs';
8
- export { defineMigration } from './packem_shared/defineMigration-CAJLr6fx.mjs';
9
- export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/composePluginMiddleware-Ck5_TUO8.mjs';
10
- export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs';
11
- export { protectPublic } from './packem_shared/protectPublic-BjFkQ_Or.mjs';
12
- export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-DxSso0rH.mjs';
10
+ export { defineMigration } from './packem_shared/defineMigration-Hx01yIht.mjs';
11
+ export { defineMutator } from './packem_shared/defineMutator-EIXAWhs9.mjs';
12
+ export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/composePluginMiddleware-CcTj1_v2.mjs';
13
+ export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-BtPCiNLW.mjs';
14
+ export { protectPublic } from './packem_shared/protectPublic-BlcGpiRc.mjs';
15
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-znq4ygKQ.mjs';
16
+ export { defineShape } from './packem_shared/defineShape-C5scNOrf.mjs';
13
17
  export { anyApi } from './types.mjs';
14
18
  export { cronJobs } from '@lunora/scheduler';
15
19
  export { ValidationError, v } from '@lunora/values';
16
- export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/createPolicyDsl-De67zPDS.mjs';
17
- export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-qu0mpilX.mjs';
18
- export { mask } from './packem_shared/mask-Jc84C_hK.mjs';
19
- export { rls } from './packem_shared/rls-BDKRbMCA.mjs';
20
- export { storageRules } from './packem_shared/storageRules-4a30FSpI.mjs';
20
+ export { buildRlsReadRegistry, composeShapeReadWhere } from './packem_shared/buildRlsReadRegistry-D54vUQe4.mjs';
21
+ export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/createPolicyDsl-By3QB4he.mjs';
22
+ export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-B5nL4Z1P.mjs';
23
+ export { mask } from './packem_shared/mask-BT1YgRbF.mjs';
24
+ export { rls } from './packem_shared/rls-B_ZWgslr.mjs';
25
+ export { storageRules } from './packem_shared/storageRules-6QxzDOcx.mjs';
21
26
 
22
27
  const VERSION = "0.0.0";
23
28
 
@@ -1,3 +1,4 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
1
2
  import { optionalInner } from '@lunora/values';
2
3
 
3
4
  const SECRET_VALUE_PREFIXES = [
@@ -53,13 +54,12 @@ const redactValueForKey = (message, key, raw) => {
53
54
  }
54
55
  return masked;
55
56
  };
56
- class LunoraEnvError extends Error {
57
- name = "LunoraEnvError";
57
+ class LunoraEnvError extends LunoraError$1 {
58
58
  failures;
59
59
  constructor(failures) {
60
60
  const summary = failures.map((failure) => ` - ${failure.key}: ${failure.message}`).join("\n");
61
- super(`Invalid environment (${String(failures.length)} key(s)):
62
- ${summary}`);
61
+ super("ENV_INVALID", `Invalid environment (${String(failures.length)} key(s)):
62
+ ${summary}`, { name: "LunoraEnvError" });
63
63
  this.failures = failures;
64
64
  }
65
65
  }
@@ -0,0 +1,9 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
3
+ class LunoraError extends LunoraError$1 {
4
+ constructor(code, message, data) {
5
+ super(code, message, { data });
6
+ }
7
+ }
8
+
9
+ export { LunoraError };
@@ -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-sQUqaejx.mjs';
3
+ import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
4
4
  import { onDisconnect } from './onConnect-CIPXKPyw.mjs';
5
- import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-Ck5_TUO8.mjs';
6
- import { defineTable } from './defineAggregateIndex-DxSso0rH.mjs';
5
+ import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-CcTj1_v2.mjs';
6
+ import { defineTable } from './defineAggregateIndex-znq4ygKQ.mjs';
7
7
 
8
8
  const DEFAULT_TTL_MS = 3e4;
9
9
  const MAX_DATA_BYTES = 4096;
@@ -0,0 +1,146 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
3
+ const isUniqueConflict = (error) => typeof error === "object" && error !== null && error.code === "CONFLICT" && error.kind === "unique";
4
+ const buildUpsertWhere = (tableName, target, create) => {
5
+ const fields = typeof target === "string" ? [target] : target;
6
+ if (fields.length === 0) {
7
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.upsert: "target" must name at least one field`);
8
+ }
9
+ const where = {};
10
+ for (const field of fields) {
11
+ if (!(field in create)) {
12
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.upsert: target field "${field}" is missing from the create document`);
13
+ }
14
+ where[field] = create[field];
15
+ }
16
+ return where;
17
+ };
18
+ const bindTableFacade = (writer, tableName) => {
19
+ const insert = async (document, options) => {
20
+ if (options?.skipDuplicates !== true) {
21
+ return writer.insert(tableName, document);
22
+ }
23
+ try {
24
+ return await writer.insert(tableName, document);
25
+ } catch (error) {
26
+ if (isUniqueConflict(error)) {
27
+ return null;
28
+ }
29
+ throw error;
30
+ }
31
+ };
32
+ const upsert = async ({ create, target, update }) => {
33
+ const where = buildUpsertWhere(tableName, target, create);
34
+ const existing = await writer.findFirst(tableName, { where });
35
+ if (existing && typeof existing["_id"] === "string") {
36
+ await writer.patch(existing["_id"], update ?? create, tableName);
37
+ return { created: false, id: existing["_id"] };
38
+ }
39
+ const id = await writer.insert(tableName, create);
40
+ return { created: true, id };
41
+ };
42
+ return {
43
+ aggregate: (options) => writer.aggregate(tableName, options),
44
+ count: (where) => writer.count(tableName, where),
45
+ delete: (id) => writer.delete(id, tableName),
46
+ // `deleteMany`/`patchMany` forward the bound `tableName` as `expectedTable`
47
+ // for id-based calls (threaded through the writer + the RLS middleware's
48
+ // per-id gate) so every batched id is scoped to this table — the same IDOR
49
+ // guard the single-row `delete`/`patch` apply. The where-based form routes
50
+ // through the structural writer's `deleteMany(tableName, { where })`.
51
+ // `patchMany` maps the facade's `values` payload to the writer's
52
+ // `{ id, patch }` shape.
53
+ deleteMany: (first, options) => {
54
+ if (Array.isArray(first)) {
55
+ if (writer.deleteMany === void 0) {
56
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
57
+ }
58
+ return writer.deleteMany(first, options, tableName);
59
+ }
60
+ if (writer.deleteWhere === void 0) {
61
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.deleteMany({ where }) is unavailable: this writer has no where-based delete`);
62
+ }
63
+ const whereArgs = first;
64
+ return writer.deleteWhere(tableName, whereArgs.where, { limit: whereArgs.limit });
65
+ },
66
+ // `exists` reuses `findFirst` (RLS-filtered, indexed when a `.withIndex`-able
67
+ // `where` is supplied) and only asks whether a row came back — no count scan.
68
+ exists: async (where) => await writer.findFirst(tableName, where === void 0 ? void 0 : { where }) !== null,
69
+ findFirst: (args) => writer.findFirst(tableName, args),
70
+ findFirstOrThrow: (args) => writer.findFirstOrThrow(tableName, args),
71
+ findMany: (args) => writer.findMany(tableName, args),
72
+ get: (id) => writer.get(id, tableName),
73
+ groupBy: (options) => writer.groupBy(tableName, options),
74
+ // Physical removal — bypasses `.softDelete()`. RLS gates it as a delete.
75
+ hardDelete: (id) => writer.delete(id, tableName, { hard: true }),
76
+ insert,
77
+ insertMany: (documents, options) => {
78
+ if (writer.insertMany === void 0) {
79
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.insertMany is unavailable: this writer has no batch insert`);
80
+ }
81
+ return writer.insertMany(tableName, documents, options);
82
+ },
83
+ patch: (id, patch) => writer.patch(id, patch, tableName),
84
+ patchMany: (first, options) => {
85
+ if (Array.isArray(first)) {
86
+ if (writer.patchMany === void 0) {
87
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
88
+ }
89
+ return writer.patchMany(
90
+ first.map((entry) => {
91
+ return { id: entry.id, patch: entry.values };
92
+ }),
93
+ options,
94
+ tableName
95
+ );
96
+ }
97
+ if (writer.patchWhere === void 0) {
98
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.patchMany({ where, values }) is unavailable: this writer has no where-based patch`);
99
+ }
100
+ const whereArgs = first;
101
+ return writer.patchWhere(tableName, { patch: whereArgs.values, where: whereArgs.where }, { limit: whereArgs.limit });
102
+ },
103
+ rank: (indexName, options) => writer.rank(tableName, indexName, options),
104
+ rankPage: (indexName, options) => writer.rankPage(tableName, indexName, options),
105
+ replace: (id, document) => writer.replace(id, document, tableName),
106
+ restore: async (id) => {
107
+ if (!writer.restore) {
108
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.restore is unavailable: this writer has no restore (is the table .softDelete()?)`);
109
+ }
110
+ await writer.restore(id, tableName);
111
+ },
112
+ upsert,
113
+ upsertMany: async ({ rows, target }) => {
114
+ const results = [];
115
+ for (const row of rows) {
116
+ results.push(await upsert({ create: row.create, target, update: row.update }));
117
+ }
118
+ return results;
119
+ },
120
+ withSearchIndex: (indexName, search) => writer.query(tableName).withSearchIndex(indexName, search)
121
+ };
122
+ };
123
+ const bindOrm = (facade) => {
124
+ const resolve = (table) => {
125
+ const bound = facade[table];
126
+ if (!bound) {
127
+ throw new LunoraError$1("INTERNAL", `unknown table: ${table}`);
128
+ }
129
+ return bound;
130
+ };
131
+ return {
132
+ delete: (table, id) => resolve(table).delete(id),
133
+ insert: (table) => {
134
+ return { values: (document) => resolve(table).insert(document) };
135
+ },
136
+ query: facade,
137
+ replace: (table, id) => {
138
+ return { with: (document) => resolve(table).replace(id, document) };
139
+ },
140
+ update: (table, id) => {
141
+ return { set: (values) => resolve(table).patch(id, values) };
142
+ }
143
+ };
144
+ };
145
+
146
+ export { bindOrm, bindTableFacade };
@@ -0,0 +1,107 @@
1
+ import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-B_ZWgslr.mjs';
2
+ import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
3
+
4
+ const FALSE_PREDICATE = { OR: [] };
5
+ const readEntryTags = (entry) => {
6
+ const hoisted = entry?.rls?.tags;
7
+ if (hoisted) {
8
+ return hoisted;
9
+ }
10
+ const tag = readRlsTag(entry);
11
+ return tag ? [tag] : [];
12
+ };
13
+ const addTagToRegistry = (byTable, tag) => {
14
+ const rolePermissions = indexRolePermissions(tag.roles);
15
+ const seenWhenByTable = /* @__PURE__ */ new Map();
16
+ const policiesByTable = /* @__PURE__ */ new Map();
17
+ for (const policy of tag.policies) {
18
+ if (policy.on !== "read") {
19
+ continue;
20
+ }
21
+ const seen = seenWhenByTable.get(policy.table) ?? /* @__PURE__ */ new Set();
22
+ if (seen.has(policy.when)) {
23
+ continue;
24
+ }
25
+ seen.add(policy.when);
26
+ seenWhenByTable.set(policy.table, seen);
27
+ const list = policiesByTable.get(policy.table) ?? [];
28
+ list.push(policy);
29
+ policiesByTable.set(policy.table, list);
30
+ }
31
+ for (const [table, policies] of policiesByTable) {
32
+ const groups = byTable.get(table) ?? [];
33
+ groups.push({ policies, rolePermissions });
34
+ byTable.set(table, groups);
35
+ }
36
+ };
37
+ const isFalsePredicate = (where) => {
38
+ const or = where.OR;
39
+ return Array.isArray(or) && or.length === 0 && Object.keys(where).length === 1;
40
+ };
41
+ const andMerge = (injected, caller) => {
42
+ if (!injected || Object.keys(injected).length === 0) {
43
+ return caller;
44
+ }
45
+ if (isFalsePredicate(injected)) {
46
+ return injected;
47
+ }
48
+ if (Object.keys(caller).length === 0) {
49
+ return injected;
50
+ }
51
+ return { AND: [injected, caller] };
52
+ };
53
+ const evaluateGroupBaseWhere = (group, request) => {
54
+ const granted = /* @__PURE__ */ new Set();
55
+ for (const roleName of request.roles) {
56
+ for (const name of group.rolePermissions.get(roleName) ?? []) {
57
+ granted.add(name);
58
+ }
59
+ }
60
+ return computeReadBaseWhere(group.policies, {
61
+ auth: {
62
+ can: (permission) => granted.has(permissionName(permission)),
63
+ identity: request.identity,
64
+ roles: request.roles,
65
+ userId: request.userId
66
+ },
67
+ ctx: request.ctx
68
+ });
69
+ };
70
+ const resolveReadBaseWhere = (registry, request) => {
71
+ const groups = registry.byTable.get(request.table);
72
+ if (!groups || groups.length === 0) {
73
+ return request.rlsRequired && !request.tablePublic ? FALSE_PREDICATE : void 0;
74
+ }
75
+ const predicates = [];
76
+ for (const group of groups) {
77
+ const base = evaluateGroupBaseWhere(group, request);
78
+ if (base === void 0) {
79
+ return void 0;
80
+ }
81
+ if (isFalsePredicate(base)) {
82
+ continue;
83
+ }
84
+ predicates.push(base);
85
+ }
86
+ if (predicates.length === 0) {
87
+ return FALSE_PREDICATE;
88
+ }
89
+ return predicates.length === 1 ? predicates[0] : { OR: predicates };
90
+ };
91
+ const buildRlsReadRegistry = (functions) => {
92
+ const byTable = /* @__PURE__ */ new Map();
93
+ const seenTags = /* @__PURE__ */ new Set();
94
+ for (const entry of functions) {
95
+ for (const tag of readEntryTags(entry)) {
96
+ if (seenTags.has(tag)) {
97
+ continue;
98
+ }
99
+ seenTags.add(tag);
100
+ addTagToRegistry(byTable, tag);
101
+ }
102
+ }
103
+ return { byTable };
104
+ };
105
+ const composeShapeReadWhere = (registry, request) => andMerge(resolveReadBaseWhere(registry, request), request.shapeWhere);
106
+
107
+ export { buildRlsReadRegistry, composeShapeReadWhere };
@@ -1,4 +1,5 @@
1
- import { r as runMiddlewareChain } from './run-middleware-CYQOuoV6.mjs';
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+ import { r as runMiddlewareChain } from './run-middleware-I6EiQfxL.mjs';
2
3
 
3
4
  const prefixTableName = (key, bareName) => `${key}_${bareName}`;
4
5
  const rewriteReference = (target, key, bareNames) => bareNames.has(target) ? prefixTableName(key, target) : target;
@@ -19,7 +20,7 @@ const rewriteTableReferences = (table, key, bareNames, ownBareName) => {
19
20
  };
20
21
  const defineSchemaExtension = (key, options) => {
21
22
  if (!key) {
22
- throw new Error("defineSchemaExtension: `key` is required and must be a non-empty string");
23
+ throw new LunoraError$1("INTERNAL", "defineSchemaExtension: `key` is required and must be a non-empty string");
23
24
  }
24
25
  return {
25
26
  key,
@@ -29,10 +30,10 @@ const defineSchemaExtension = (key, options) => {
29
30
  };
30
31
  const definePlugin = (key, options) => {
31
32
  if (!key) {
32
- throw new Error("definePlugin: `key` is required and must be a non-empty string");
33
+ throw new LunoraError$1("INTERNAL", "definePlugin: `key` is required and must be a non-empty string");
33
34
  }
34
35
  if (options.extension && options.extension.key !== key) {
35
- throw new Error(`definePlugin("${key}"): extension key "${options.extension.key}" does not match plugin key`);
36
+ throw new LunoraError$1("INTERNAL", `definePlugin("${key}"): extension key "${options.extension.key}" does not match plugin key`);
36
37
  }
37
38
  return {
38
39
  key,
@@ -57,7 +58,8 @@ const mergeSchemaExtension = (base, extension) => {
57
58
  for (const [bareName, table] of Object.entries(extension.tables)) {
58
59
  const prefixed = prefixTableName(key, bareName);
59
60
  if (Object.hasOwn(merged, prefixed)) {
60
- throw new Error(
61
+ throw new LunoraError$1(
62
+ "INTERNAL",
61
63
  `defineSchema(...).extend("${key}"): table "${prefixed}" already exists in the base schema — another extension with the same key already contributed it`
62
64
  );
63
65
  }
@@ -68,7 +70,8 @@ const mergeSchemaExtension = (base, extension) => {
68
70
  for (const [bareIndexName, index] of Object.entries(extension.vectorIndexes)) {
69
71
  const prefixed = prefixTableName(key, bareIndexName);
70
72
  if (Object.hasOwn(mergedVectorIndexes, prefixed)) {
71
- throw new Error(
73
+ throw new LunoraError$1(
74
+ "INTERNAL",
72
75
  `defineSchema(...).extend("${key}"): vector index "${prefixed}" already exists in the base schema — another extension with the same key already contributed it`
73
76
  );
74
77
  }
@@ -1,3 +1,5 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
1
3
  const definePolicy = (input) => {
2
4
  return { on: input.on, table: input.table, when: input.when };
3
5
  };
@@ -13,7 +15,8 @@ const definePolicies = (policies) => {
13
15
  const key = JSON.stringify([policy.table, policy.on]);
14
16
  const whens = seenWhenByKey.get(key) ?? /* @__PURE__ */ new Set();
15
17
  if (whens.has(policy.when)) {
16
- throw new Error(
18
+ throw new LunoraError$1(
19
+ "INTERNAL",
17
20
  `definePolicies: duplicate policy for (table "${policy.table}", on "${policy.on}") — the same decision function is registered more than once. Multiple distinct policies per (table, on) are allowed (reads OR, writes AND); remove the duplicate.`
18
21
  );
19
22
  }
@@ -0,0 +1,58 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
3
+ const NON_SECRET_BINDING_METHODS = [
4
+ "put",
5
+ // KV / R2
6
+ "list",
7
+ // KV / R2
8
+ "delete",
9
+ // KV / R2
10
+ "getWithMetadata",
11
+ // KV
12
+ "head",
13
+ // R2
14
+ "createMultipartUpload",
15
+ // R2
16
+ "idFromName",
17
+ // Durable Object namespace
18
+ "newUniqueId",
19
+ // Durable Object namespace
20
+ "getByName",
21
+ // Durable Object namespace
22
+ "send",
23
+ // Queue producer
24
+ "sendBatch",
25
+ // Queue producer
26
+ "writeDataPoint",
27
+ // Analytics Engine dataset
28
+ "create",
29
+ // Workflows binding (get(id) + create/createBatch)
30
+ "createBatch"
31
+ // Workflows binding
32
+ ];
33
+ const isSecretBinding = (value) => {
34
+ if (typeof value !== "object" || value === null) {
35
+ return false;
36
+ }
37
+ const record = value;
38
+ if (typeof record.get !== "function") {
39
+ return false;
40
+ }
41
+ return !NON_SECRET_BINDING_METHODS.some((method) => typeof record[method] === "function");
42
+ };
43
+ const createSecrets = (env) => {
44
+ return {
45
+ get: async (name) => {
46
+ const binding = env[name];
47
+ if (!isSecretBinding(binding)) {
48
+ throw new LunoraError$1(
49
+ "INTERNAL",
50
+ `ctx.secrets: no Secrets Store binding named "${name}". Add a \`secrets_store_secrets[]\` entry (binding "${name}", pointing at your store + secret) to wrangler.jsonc — \`ctx.secrets\` reads a Secrets Store binding, not a plain \`.dev.vars\` value.`
51
+ );
52
+ }
53
+ return binding.get();
54
+ }
55
+ };
56
+ };
57
+
58
+ export { createSecrets };
@@ -1,5 +1,6 @@
1
- import { isOrWrapsFromValidator } from '@lunora/values';
2
- import { mergeSchemaExtension } from './composePluginMiddleware-Ck5_TUO8.mjs';
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+ import { isOrWrapsFromValidator, v } from '@lunora/values';
3
+ import { mergeSchemaExtension } from './composePluginMiddleware-CcTj1_v2.mjs';
3
4
 
4
5
  const relationBuilder = {
5
6
  many: (table, options) => {
@@ -22,10 +23,11 @@ const createTriggerBuilder = () => {
22
23
  beforeUpdate: (handler) => makeTrigger("before", "update", handler)
23
24
  };
24
25
  };
25
- const defineTable = (shape) => {
26
+ const defineTable = (inputShape) => {
27
+ const shape = { ...inputShape };
26
28
  for (const [columnName, validator] of Object.entries(shape)) {
27
29
  if (isOrWrapsFromValidator(validator)) {
28
- throw new Error(`defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
30
+ throw new LunoraError$1("INTERNAL", `defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
29
31
  }
30
32
  }
31
33
  const aggregateIndexes = [];
@@ -39,11 +41,13 @@ const defineTable = (shape) => {
39
41
  let shardMode = { kind: "root" };
40
42
  let isExternallyManaged = false;
41
43
  let isPublic = false;
44
+ let softDelete;
45
+ let externalSource;
42
46
  const builder = {
43
47
  aggregateIndex(name, options) {
44
48
  const op = options?.op ?? "count";
45
49
  if (op !== "count" && !options?.field) {
46
- throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
50
+ throw new LunoraError$1("INTERNAL", `aggregateIndex "${name}": op "${op}" requires a "field"`);
47
51
  }
48
52
  aggregateIndexes.push({
49
53
  by: options?.by,
@@ -61,6 +65,9 @@ const defineTable = (shape) => {
61
65
  get aggregateIndexes() {
62
66
  return aggregateIndexes;
63
67
  },
68
+ get externalSource() {
69
+ return externalSource;
70
+ },
64
71
  externallyManaged() {
65
72
  isExternallyManaged = true;
66
73
  return builder;
@@ -88,7 +95,7 @@ const defineTable = (shape) => {
88
95
  },
89
96
  rankIndex(name, options) {
90
97
  if (!options.sortBy || options.sortBy.length === 0) {
91
- throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
98
+ throw new LunoraError$1("INTERNAL", `rankIndex "${name}": "sortBy" is required and must list at least one key`);
92
99
  }
93
100
  const sortBy = options.sortBy.map((key) => {
94
101
  return {
@@ -132,6 +139,28 @@ const defineTable = (shape) => {
132
139
  get shardMode() {
133
140
  return shardMode;
134
141
  },
142
+ get softDeleteMode() {
143
+ return softDelete;
144
+ },
145
+ source(definition) {
146
+ if (!definition.binding) {
147
+ throw new LunoraError$1("INTERNAL", "source: `binding` is required (the wrangler Hyperdrive binding name)");
148
+ }
149
+ if (!definition.query) {
150
+ throw new LunoraError$1("INTERNAL", "source: `query` is required (the tenant-membership SQL)");
151
+ }
152
+ externalSource = definition;
153
+ isExternallyManaged = true;
154
+ return builder;
155
+ },
156
+ softDelete(options) {
157
+ const field = options?.field ?? "deletedAt";
158
+ softDelete = { field };
159
+ if (!(field in shape)) {
160
+ shape[field] = v.optional(v.number().nullable());
161
+ }
162
+ return builder;
163
+ },
135
164
  get triggerMap() {
136
165
  return triggers;
137
166
  },
@@ -170,13 +199,13 @@ const defineVectorIndex = (options) => {
170
199
  const defineAggregateIndex = (name, options) => {
171
200
  const op = options.op ?? "count";
172
201
  if (op !== "count" && !options.field) {
173
- throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
202
+ throw new LunoraError$1("INTERNAL", `aggregateIndex "${name}": op "${op}" requires a "field"`);
174
203
  }
175
204
  return { by: options.by, field: options.field, name, on: options.on, op, where: options.where };
176
205
  };
177
206
  const defineRankIndex = (name, options) => {
178
207
  if (!options.sortBy || options.sortBy.length === 0) {
179
- throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
208
+ throw new LunoraError$1("INTERNAL", `rankIndex "${name}": "sortBy" is required and must list at least one key`);
180
209
  }
181
210
  const sortBy = options.sortBy.map((key) => {
182
211
  return {
@@ -192,6 +221,9 @@ const withExtend = (schema) => {
192
221
  extend(extension) {
193
222
  return withExtend(mergeSchemaExtension(schema, extension));
194
223
  },
224
+ jurisdiction(_jurisdiction) {
225
+ return withExtend(schema);
226
+ },
195
227
  rls(mode) {
196
228
  return withExtend({ ...schema, rlsMode: mode });
197
229
  }
@@ -215,21 +247,48 @@ const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
215
247
  for (const index of Object.values(aggregateIndexes)) {
216
248
  const table = tables[index.on];
217
249
  if (!table) {
218
- throw new Error(`defineAggregateIndex "${index.name}": unknown table "${index.on}"`);
250
+ throw new LunoraError$1("INTERNAL", `defineAggregateIndex "${index.name}": unknown table "${index.on}"`);
219
251
  }
220
252
  table.aggregateIndexes.push(index);
221
253
  }
222
254
  for (const index of Object.values(rankIndexes)) {
223
255
  const table = tables[index.on];
224
256
  if (!table) {
225
- throw new Error(`defineRankIndex "${index.name}": unknown table "${index.on}"`);
257
+ throw new LunoraError$1("INTERNAL", `defineRankIndex "${index.name}": unknown table "${index.on}"`);
226
258
  }
227
259
  table.rankIndexes.push(index);
228
260
  }
229
261
  };
262
+ const validateExternalSources = (tables) => {
263
+ for (const [name, table] of Object.entries(tables)) {
264
+ const source = table.externalSource;
265
+ if (!source) {
266
+ continue;
267
+ }
268
+ if (table.shardMode.kind === "global") {
269
+ throw new LunoraError$1(
270
+ "INTERNAL",
271
+ `defineSchema: table "${name}" cannot be both .source() and .global() — a sourced table materializes into a shard DO's SQLite, a global table lives in the external tier`
272
+ );
273
+ }
274
+ if (table.shardMode.kind === "shardBy" && !source.tenantBy) {
275
+ throw new LunoraError$1(
276
+ "INTERNAL",
277
+ `defineSchema: sourced + .shardBy() table "${name}" needs a \`tenantBy\` mapper — without it every tenant's DO would run the same unscoped query and replicate the whole multitenant table (a cross-tenant leak). Add \`tenantBy: (shardKey) => [shardKey]\` binding the shard key into the query's parameters.`
278
+ );
279
+ }
280
+ if (source.mode === "incremental") {
281
+ throw new LunoraError$1(
282
+ "INTERNAL",
283
+ `defineSchema: table "${name}" uses \`mode: "incremental"\`, which is not yet implemented — only "full-pull" (the default) is supported. Remove \`mode\` or set it to "full-pull".`
284
+ );
285
+ }
286
+ }
287
+ };
230
288
  const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
231
289
  fillIndexTableNames(tables);
232
290
  attachStandaloneIndexes(tables, aggregateIndexes, rankIndexes);
291
+ validateExternalSources(tables);
233
292
  return withExtend({ tables, vectorIndexes });
234
293
  };
235
294
 
@@ -0,0 +1,35 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+ import { parseValidatorMap, ValidationError } from '@lunora/values';
3
+
4
+ const NON_STRING_USER_ID_KINDS = /* @__PURE__ */ new Set([
5
+ "array",
6
+ "bigint",
7
+ "boolean",
8
+ "bytes",
9
+ "date",
10
+ "null",
11
+ "number",
12
+ "object",
13
+ "optional",
14
+ "record",
15
+ "timestamp"
16
+ ]);
17
+ const defineIdentity = (claims, options = {}) => {
18
+ const map = claims;
19
+ const userIdValidator = map["userId"];
20
+ if (userIdValidator === void 0 || NON_STRING_USER_ID_KINDS.has(userIdValidator.kind)) {
21
+ throw new LunoraError$1("INTERNAL", "defineIdentity: the claim map must declare a required string `userId` validator (e.g. `v.string()` or `v.id(...)`)");
22
+ }
23
+ const onInvalid = options.onInvalid ?? "anonymous";
24
+ const validate = (identity) => {
25
+ try {
26
+ parseValidatorMap(map, identity, "identity");
27
+ return { ok: true };
28
+ } catch (error) {
29
+ return { error: error instanceof ValidationError ? error.message : String(error), ok: false };
30
+ }
31
+ };
32
+ return { __lunoraIdentity: true, claims: map, onInvalid, validate };
33
+ };
34
+
35
+ export { defineIdentity };