@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
@@ -0,0 +1,107 @@
1
+ import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-2Jhd0uev.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 };
@@ -0,0 +1,55 @@
1
+ const NON_SECRET_BINDING_METHODS = [
2
+ "put",
3
+ // KV / R2
4
+ "list",
5
+ // KV / R2
6
+ "delete",
7
+ // KV / R2
8
+ "getWithMetadata",
9
+ // KV
10
+ "head",
11
+ // R2
12
+ "createMultipartUpload",
13
+ // R2
14
+ "idFromName",
15
+ // Durable Object namespace
16
+ "newUniqueId",
17
+ // Durable Object namespace
18
+ "getByName",
19
+ // Durable Object namespace
20
+ "send",
21
+ // Queue producer
22
+ "sendBatch",
23
+ // Queue producer
24
+ "writeDataPoint",
25
+ // Analytics Engine dataset
26
+ "create",
27
+ // Workflows binding (get(id) + create/createBatch)
28
+ "createBatch"
29
+ // Workflows binding
30
+ ];
31
+ const isSecretBinding = (value) => {
32
+ if (typeof value !== "object" || value === null) {
33
+ return false;
34
+ }
35
+ const record = value;
36
+ if (typeof record.get !== "function") {
37
+ return false;
38
+ }
39
+ return !NON_SECRET_BINDING_METHODS.some((method) => typeof record[method] === "function");
40
+ };
41
+ const createSecrets = (env) => {
42
+ return {
43
+ get: async (name) => {
44
+ const binding = env[name];
45
+ if (!isSecretBinding(binding)) {
46
+ throw new Error(
47
+ `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.`
48
+ );
49
+ }
50
+ return binding.get();
51
+ }
52
+ };
53
+ };
54
+
55
+ export { createSecrets };
@@ -1,5 +1,5 @@
1
- import { isOrWrapsFromValidator } from '@lunora/values';
2
- import { mergeSchemaExtension } from './defineSchemaExtension-Ck5_TUO8.mjs';
1
+ import { isOrWrapsFromValidator, v } from '@lunora/values';
2
+ import { mergeSchemaExtension } from './composePluginMiddleware-Ck5_TUO8.mjs';
3
3
 
4
4
  const relationBuilder = {
5
5
  many: (table, options) => {
@@ -22,7 +22,8 @@ const createTriggerBuilder = () => {
22
22
  beforeUpdate: (handler) => makeTrigger("before", "update", handler)
23
23
  };
24
24
  };
25
- const defineTable = (shape) => {
25
+ const defineTable = (inputShape) => {
26
+ const shape = { ...inputShape };
26
27
  for (const [columnName, validator] of Object.entries(shape)) {
27
28
  if (isOrWrapsFromValidator(validator)) {
28
29
  throw new Error(`defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
@@ -39,6 +40,8 @@ const defineTable = (shape) => {
39
40
  let shardMode = { kind: "root" };
40
41
  let isExternallyManaged = false;
41
42
  let isPublic = false;
43
+ let softDelete;
44
+ let externalSource;
42
45
  const builder = {
43
46
  aggregateIndex(name, options) {
44
47
  const op = options?.op ?? "count";
@@ -61,6 +64,9 @@ const defineTable = (shape) => {
61
64
  get aggregateIndexes() {
62
65
  return aggregateIndexes;
63
66
  },
67
+ get externalSource() {
68
+ return externalSource;
69
+ },
64
70
  externallyManaged() {
65
71
  isExternallyManaged = true;
66
72
  return builder;
@@ -132,6 +138,28 @@ const defineTable = (shape) => {
132
138
  get shardMode() {
133
139
  return shardMode;
134
140
  },
141
+ get softDeleteMode() {
142
+ return softDelete;
143
+ },
144
+ source(definition) {
145
+ if (!definition.binding) {
146
+ throw new Error("source: `binding` is required (the wrangler Hyperdrive binding name)");
147
+ }
148
+ if (!definition.query) {
149
+ throw new Error("source: `query` is required (the tenant-membership SQL)");
150
+ }
151
+ externalSource = definition;
152
+ isExternallyManaged = true;
153
+ return builder;
154
+ },
155
+ softDelete(options) {
156
+ const field = options?.field ?? "deletedAt";
157
+ softDelete = { field };
158
+ if (!(field in shape)) {
159
+ shape[field] = v.optional(v.number().nullable());
160
+ }
161
+ return builder;
162
+ },
135
163
  get triggerMap() {
136
164
  return triggers;
137
165
  },
@@ -192,6 +220,9 @@ const withExtend = (schema) => {
192
220
  extend(extension) {
193
221
  return withExtend(mergeSchemaExtension(schema, extension));
194
222
  },
223
+ jurisdiction(_jurisdiction) {
224
+ return withExtend(schema);
225
+ },
195
226
  rls(mode) {
196
227
  return withExtend({ ...schema, rlsMode: mode });
197
228
  }
@@ -227,9 +258,33 @@ const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
227
258
  table.rankIndexes.push(index);
228
259
  }
229
260
  };
261
+ const validateExternalSources = (tables) => {
262
+ for (const [name, table] of Object.entries(tables)) {
263
+ const source = table.externalSource;
264
+ if (!source) {
265
+ continue;
266
+ }
267
+ if (table.shardMode.kind === "global") {
268
+ throw new Error(
269
+ `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`
270
+ );
271
+ }
272
+ if (table.shardMode.kind === "shardBy" && !source.tenantBy) {
273
+ throw new Error(
274
+ `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.`
275
+ );
276
+ }
277
+ if (source.mode === "incremental") {
278
+ throw new Error(
279
+ `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".`
280
+ );
281
+ }
282
+ }
283
+ };
230
284
  const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
231
285
  fillIndexTableNames(tables);
232
286
  attachStandaloneIndexes(tables, aggregateIndexes, rankIndexes);
287
+ validateExternalSources(tables);
233
288
  return withExtend({ tables, vectorIndexes });
234
289
  };
235
290
 
@@ -0,0 +1,11 @@
1
+ import { v as validateArgs } from './functions-Di9FUNkf.mjs';
2
+
3
+ const defineMutator = (definition) => {
4
+ const handler = async (context, rawArgs) => {
5
+ const parsed = validateArgs(definition.args ?? {}, rawArgs);
6
+ return definition.server(context, parsed);
7
+ };
8
+ return { __lunoraMutator: true, ...definition, handler, kind: "mutation" };
9
+ };
10
+
11
+ export { defineMutator };
@@ -0,0 +1,17 @@
1
+ import { v as validateArgs } from './functions-Di9FUNkf.mjs';
2
+
3
+ const defineShape = (definition) => {
4
+ if (definition.table.trim() === "") {
5
+ throw new Error("defineShape: `table` must be a non-empty string");
6
+ }
7
+ if (definition.columns?.length === 0) {
8
+ throw new Error("defineShape: `columns` must list at least one column when provided");
9
+ }
10
+ const compileWhere = (context, rawArgs) => {
11
+ const parsed = validateArgs(definition.args ?? {}, rawArgs);
12
+ return definition.where(context, parsed);
13
+ };
14
+ return { __lunoraShape: true, ...definition, compileWhere };
15
+ };
16
+
17
+ export { defineShape };
@@ -0,0 +1,5 @@
1
+ import { parseValidatorMap } from '@lunora/values';
2
+
3
+ const validateArgs = (validators, args) => parseValidatorMap(validators, args, "args");
4
+
5
+ export { validateArgs as v };
@@ -1,6 +1,6 @@
1
1
  import { parseValidatorMap, ValidationError } from '@lunora/values';
2
2
  import { Hono } from 'hono';
3
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
3
+ import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
4
4
 
5
5
  const httpAction = (handler) => async (c) => handler(c.get("lunora"), c.req.raw);
6
6
  const httpRouter = () => {
@@ -1,8 +1,7 @@
1
- import { parseValidatorMap } from '@lunora/values';
1
+ import { v as validateArgs } from './functions-Di9FUNkf.mjs';
2
+ import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
2
3
  import { r as runMiddlewareChain } from './run-middleware-CYQOuoV6.mjs';
3
4
 
4
- const validateArgs = (validators, args) => parseValidatorMap(validators, args, "args");
5
-
6
5
  const runMiddleware = (middlewares, baseContext) => runMiddlewareChain(middlewares, baseContext, (context) => context);
7
6
  const makeHandler = (args, middlewares, userHandler, output) => async (context, rawArgs) => {
8
7
  const parsed = validateArgs(args, rawArgs);
@@ -35,16 +34,29 @@ const makeStreamHandler = (args, middlewares, userHandler) => (context, rawArgs,
35
34
  }
36
35
  })();
37
36
  };
37
+ const collectRls = (middlewares) => {
38
+ const tags = [];
39
+ for (const middleware of middlewares) {
40
+ const tag = readRlsTag(middleware);
41
+ if (!tag) {
42
+ continue;
43
+ }
44
+ tags.push(tag);
45
+ }
46
+ return tags.length > 0 ? { tags } : void 0;
47
+ };
38
48
  const makeBuilder = (kind, state, visibility) => {
39
49
  return {
40
50
  __lunoraProcedure: kind,
41
51
  ...visibility ? { __lunoraVisibility: visibility } : {},
42
52
  input: (validators) => makeBuilder(kind, { ...state, args: { ...state.args, ...validators } }, visibility),
43
53
  [kind]: (userHandler) => {
54
+ const rls = collectRls(state.middlewares);
44
55
  return {
45
56
  args: state.args,
46
57
  handler: makeHandler(state.args, state.middlewares, userHandler, state.output),
47
58
  kind,
59
+ ...rls ? { rls } : {},
48
60
  ...visibility ? { visibility } : {}
49
61
  };
50
62
  },
@@ -55,10 +67,12 @@ const makeBuilder = (kind, state, visibility) => {
55
67
  // unconditionally keeps the runtime free of per-kind branching.
56
68
  ...kind === "query" ? {
57
69
  stream: (userHandler) => {
70
+ const rls = collectRls(state.middlewares);
58
71
  return {
59
72
  args: state.args,
60
73
  handler: makeStreamHandler(state.args, state.middlewares, userHandler),
61
74
  kind: "stream",
75
+ ...rls ? { rls } : {},
62
76
  ...visibility ? { visibility } : {}
63
77
  };
64
78
  }
@@ -1,5 +1,5 @@
1
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
2
- import { bindTableFacade, bindOrm } from './bindTableFacade-DCuyr46L.mjs';
1
+ import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Ce57S3N9.mjs';
3
3
 
4
4
  const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
5
5
  const indexRolePermissions = (roles) => {
@@ -0,0 +1,13 @@
1
+ const RLS_TAG = /* @__PURE__ */ Symbol.for("lunora.rls.middleware-policies");
2
+ const tagRlsMiddleware = (middleware, tag) => {
3
+ Object.defineProperty(middleware, RLS_TAG, { configurable: true, enumerable: false, value: tag });
4
+ return middleware;
5
+ };
6
+ const readRlsTag = (middleware) => {
7
+ if (middleware === null || typeof middleware !== "function" && typeof middleware !== "object") {
8
+ return void 0;
9
+ }
10
+ return middleware[RLS_TAG];
11
+ };
12
+
13
+ export { readRlsTag as r, tagRlsMiddleware as t };
@@ -1,5 +1,6 @@
1
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
2
- import { bindTableFacade, bindOrm } from './bindTableFacade-DCuyr46L.mjs';
1
+ import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Ce57S3N9.mjs';
3
+ import { t as tagRlsMiddleware } from './policy-tag-DvpVH2tv.mjs';
3
4
 
4
5
  const DEFAULT_BATCH_LIMIT = 500;
5
6
  const assertBatchLimit = (count, limit, op) => {
@@ -324,7 +325,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
324
325
  restrictsCounts: (args.restrictsCounts ?? false) || restricts
325
326
  });
326
327
  },
327
- delete: (id, expectedTable) => gateById(id, "delete", (writer) => writer.delete(id, expectedTable), void 0, expectedTable),
328
+ delete: (id, expectedTable, options) => gateById(id, "delete", (writer) => writer.delete(id, expectedTable, options), void 0, expectedTable),
328
329
  async deleteMany(ids, options, expectedTable) {
329
330
  assertBatchLimit(ids.length, options?.limit, "deleteMany");
330
331
  for (const id of ids) {
@@ -441,6 +442,22 @@ const wrapDatabase = (base, raw, perTable, context) => {
441
442
  }
442
443
  return reader.filter((document) => matchesWhere(document, baseWhere));
443
444
  },
445
+ // Restore clears the soft-delete marker — a by-id un-delete, gated as an
446
+ // "update" (the policy that governs patch). No post-image check: the
447
+ // writer owns the marker field, so we only enforce the pre-image USING.
448
+ restore: (id, expectedTable) => gateById(
449
+ id,
450
+ "update",
451
+ async (writer) => {
452
+ const perform = writer.restore ?? raw.restore;
453
+ if (!perform) {
454
+ throw new LunoraError("BAD_REQUEST", "restore is not supported by this writer");
455
+ }
456
+ await perform(id, expectedTable);
457
+ },
458
+ void 0,
459
+ expectedTable
460
+ ),
444
461
  replace: (id, document, expectedTable) => gateById(
445
462
  id,
446
463
  "update",
@@ -516,7 +533,7 @@ const indexRolePermissions = (roles) => {
516
533
  const rls = (policies, options = {}) => {
517
534
  const perTable = indexByTable(policies);
518
535
  const rolePermissions = indexRolePermissions(options.roles);
519
- return async ({ ctx, next }) => {
536
+ const middleware = async ({ ctx, next }) => {
520
537
  const auth = ctx.auth ?? {};
521
538
  const identity = await auth.getIdentity?.() ?? null;
522
539
  const roles = auth.roles ?? [];
@@ -546,6 +563,7 @@ const rls = (policies, options = {}) => {
546
563
  }
547
564
  return next({ ctx: extension });
548
565
  };
566
+ return tagRlsMiddleware(middleware, { policies, roles: options.roles ?? [] });
549
567
  };
550
568
 
551
569
  export { computeReadBaseWhere, evaluateWrite, indexRolePermissions, matchesWhere, permissionName, rls };
@@ -1,4 +1,4 @@
1
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
1
+ import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
2
2
 
3
3
  const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
4
4
  const indexRolePermissions = (roles = []) => {
@@ -1,4 +1,4 @@
1
- import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-Zhf5wEeJ.mjs';
1
+ import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-2Jhd0uev.mjs';
2
2
 
3
3
  const expectPolicy = (policies, options = {}) => {
4
4
  const rolePermissions = indexRolePermissions(options.roles);