@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
@@ -1,6 +1,8 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
1
3
  const defineMigration = (definition) => {
2
4
  if (definition.id.trim() === "") {
3
- throw new Error("defineMigration: `id` must be a non-empty string");
5
+ throw new LunoraError$1("INTERNAL", "defineMigration: `id` must be a non-empty string");
4
6
  }
5
7
  return { __lunoraMigration: true, ...definition };
6
8
  };
@@ -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,18 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+ import { v as validateArgs } from './functions-Di9FUNkf.mjs';
3
+
4
+ const defineShape = (definition) => {
5
+ if (definition.table.trim() === "") {
6
+ throw new LunoraError$1("INTERNAL", "defineShape: `table` must be a non-empty string");
7
+ }
8
+ if (definition.columns?.length === 0) {
9
+ throw new LunoraError$1("INTERNAL", "defineShape: `columns` must list at least one column when provided");
10
+ }
11
+ const compileWhere = (context, rawArgs) => {
12
+ const parsed = validateArgs(definition.args ?? {}, rawArgs);
13
+ return definition.where(context, parsed);
14
+ };
15
+ return { __lunoraShape: true, ...definition, compileWhere };
16
+ };
17
+
18
+ export { defineShape };
@@ -1,3 +1,5 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
1
3
  const defineStorageRule = (input) => {
2
4
  return { bucket: input.bucket, on: input.on, prefix: input.prefix, when: input.when };
3
5
  };
@@ -7,7 +9,8 @@ const defineStorageRules = (rules) => {
7
9
  const key = JSON.stringify([rule.bucket, rule.on, rule.prefix]);
8
10
  const whens = seenWhenByKey.get(key) ?? /* @__PURE__ */ new Set();
9
11
  if (whens.has(rule.when)) {
10
- throw new Error(
12
+ throw new LunoraError$1(
13
+ "INTERNAL",
11
14
  `defineStorageRules: duplicate rule for (bucket "${rule.bucket}", on "${rule.on}"${rule.prefix === void 0 ? "" : `, prefix "${rule.prefix}"`}) — the same decision function is registered more than once. Multiple distinct rules per (bucket, on) are allowed (they OR); remove the duplicate.`
12
15
  );
13
16
  }
@@ -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,7 @@
1
+ import { toErrorBody } from '@lunora/errors';
1
2
  import { parseValidatorMap, ValidationError } from '@lunora/values';
2
3
  import { Hono } from 'hono';
3
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
4
+ import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
4
5
 
5
6
  const httpAction = (handler) => async (c) => handler(c.get("lunora"), c.req.raw);
6
7
  const httpRouter = () => {
@@ -109,7 +110,11 @@ const errorResponse = (error) => {
109
110
  return Response.json({ code: "BAD_REQUEST", error: error.message }, { status: 400 });
110
111
  }
111
112
  if (error instanceof LunoraError) {
112
- return Response.json({ code: error.code, error: error.message }, { status: error.status });
113
+ const { body, redacted, status } = toErrorBody(error, { fallbackCode: "INTERNAL_SERVER_ERROR", redactedMessage: "Internal error" });
114
+ if (redacted) {
115
+ console.error("[lunora] http action error (redacted on the wire):", error);
116
+ }
117
+ return Response.json({ code: body.code, error: body.message }, { status });
113
118
  }
114
119
  throw error;
115
120
  };
@@ -121,18 +126,25 @@ const buildRouteHandler = (state, userHandler) => async (c) => {
121
126
  const body = Object.keys(state.body).length > 0 ? await parseBody(state.body, c) : {};
122
127
  const result = await userHandler({ body, ctx: context, params, searchParams });
123
128
  const payload = state.output ? applyOutput(state.output, result) : result;
124
- return payload === void 0 ? new Response(null, { status: 204 }) : Response.json(payload);
129
+ const headers = {};
130
+ if (state.cacheControl) {
131
+ headers["cache-control"] = state.cacheControl;
132
+ }
133
+ if (state.cacheTag) {
134
+ headers["cache-tag"] = state.cacheTag;
135
+ }
136
+ if (state.vary) {
137
+ headers.vary = state.vary;
138
+ }
139
+ const hasCacheHeaders = Object.keys(headers).length > 0;
140
+ if (payload === void 0) {
141
+ return new Response(null, { headers: hasCacheHeaders ? headers : void 0, status: 204 });
142
+ }
143
+ return Response.json(payload, { headers: hasCacheHeaders ? headers : void 0 });
125
144
  } catch (error) {
126
145
  return errorResponse(error);
127
146
  }
128
147
  };
129
- const isLunoraErrorLike = (error) => {
130
- if (!error || typeof error !== "object") {
131
- return false;
132
- }
133
- const candidate = error;
134
- return candidate.name === "LunoraError" && typeof candidate.code === "string" && typeof candidate.message === "string";
135
- };
136
148
  const sseFrame = (chunk, event) => {
137
149
  const data = JSON.stringify(chunk);
138
150
  const prefix = event ? `event: ${event}
@@ -193,39 +205,42 @@ const buildStreamHandler = (state, userHandler) => (
193
205
  }
194
206
  controller.enqueue(encoder.encode(sseFrame({}, "complete")));
195
207
  } catch (error) {
196
- let payload;
197
- if (isLunoraErrorLike(error)) {
198
- payload = { code: error.code, message: error.message };
199
- } else {
208
+ const { body, redacted } = toErrorBody(error, { fallbackCode: "INTERNAL_SERVER_ERROR", redactedMessage: "Internal error" });
209
+ if (redacted) {
200
210
  console.error("[lunora] unhandled stream handler error:", error);
201
- payload = { code: "INTERNAL_SERVER_ERROR", message: "Internal error" };
202
211
  }
203
- controller.enqueue(encoder.encode(sseFrame(payload, "error")));
212
+ controller.enqueue(encoder.encode(sseFrame({ code: body.code, message: body.message }, "error")));
204
213
  } finally {
205
214
  request.signal.removeEventListener("abort", onAbort);
206
215
  controller.close();
207
216
  }
208
217
  }
209
218
  });
210
- return new Response(stream, {
211
- headers: {
212
- "cache-control": "no-cache, no-transform",
213
- "content-type": "text/event-stream; charset=utf-8",
214
- // Hint to proxies (including Cloudflare's own buffering layer)
215
- // that this response must not be coalesced.
216
- "x-accel-buffering": "no"
217
- }
218
- });
219
+ const headers = {
220
+ // SSE responses must stay uncacheable so proxies don't buffer or
221
+ // coalesce live frames. `cacheControl()` is intentionally ignored
222
+ // for stream() routes; `cacheTag`/`vary` are also omitted because
223
+ // they only make sense alongside a cacheable response.
224
+ "cache-control": "no-cache, no-transform",
225
+ "content-type": "text/event-stream; charset=utf-8",
226
+ // Hint to proxies (including Cloudflare's own buffering layer)
227
+ // that this response must not be coalesced.
228
+ "x-accel-buffering": "no"
229
+ };
230
+ return new Response(stream, { headers });
219
231
  }
220
232
  );
221
233
  const makeRouteBuilder = (state) => {
222
234
  return {
223
235
  body: (validators) => makeRouteBuilder({ ...state, body: { ...state.body, ...validators } }),
236
+ cacheControl: (value) => makeRouteBuilder({ ...state, cacheControl: value }),
237
+ cacheTag: (value) => makeRouteBuilder({ ...state, cacheTag: value }),
224
238
  handler: (userHandler) => buildRouteHandler(state, userHandler),
225
239
  output: (validator) => makeRouteBuilder({ ...state, output: validator }),
226
240
  params: (validators) => makeRouteBuilder({ ...state, params: { ...state.params, ...validators } }),
227
241
  searchParams: (validators) => makeRouteBuilder({ ...state, searchParams: { ...state.searchParams, ...validators } }),
228
- stream: (userHandler) => buildStreamHandler(state, userHandler)
242
+ stream: (userHandler) => buildStreamHandler(state, userHandler),
243
+ vary: (value) => makeRouteBuilder({ ...state, vary: value })
229
244
  };
230
245
  };
231
246
  const makeRouteFactory = (method) => (path) => makeRouteBuilder({ body: {}, method, params: {}, path, searchParams: {} });
@@ -337,4 +352,4 @@ const serveStorageObject = async (context, key, request) => {
337
352
  });
338
353
  };
339
354
 
340
- export { httpAction, httpRoute, httpRouter, serveStorageObject };
355
+ export { httpAction, httpRoute, httpRouter, isSafeHeaderValue, serveStorageObject };
@@ -1,7 +1,6 @@
1
- import { parseValidatorMap } from '@lunora/values';
2
- import { r as runMiddlewareChain } from './run-middleware-CYQOuoV6.mjs';
3
-
4
- const validateArgs = (validators, args) => parseValidatorMap(validators, args, "args");
1
+ import { v as validateArgs } from './functions-Di9FUNkf.mjs';
2
+ import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
3
+ import { r as runMiddlewareChain } from './run-middleware-I6EiQfxL.mjs';
5
4
 
6
5
  const runMiddleware = (middlewares, baseContext) => runMiddlewareChain(middlewares, baseContext, (context) => context);
7
6
  const makeHandler = (args, middlewares, userHandler, output) => async (context, 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 './bindOrm-DCuyr46L.mjs';
1
+ import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Bfgl7f-Q.mjs';
3
3
 
4
4
  const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
5
5
  const indexRolePermissions = (roles) => {
@@ -46,6 +46,29 @@ const maskRow = (row, columns, base) => {
46
46
  const maskPage = (page, columns, base) => {
47
47
  return { ...page, page: page.page.map((row) => maskRow(row, columns, base)) };
48
48
  };
49
+ const assertIndexFieldsAllowed = (builderCallback, columns, tableName, method) => {
50
+ if (typeof builderCallback !== "function") {
51
+ return;
52
+ }
53
+ const referenced = /* @__PURE__ */ new Set();
54
+ const makeRecorder = () => /* @__PURE__ */ new Proxy(
55
+ {},
56
+ {
57
+ get: () => (field) => {
58
+ if (typeof field === "string") {
59
+ referenced.add(field);
60
+ }
61
+ return makeRecorder();
62
+ }
63
+ }
64
+ );
65
+ builderCallback(makeRecorder());
66
+ for (const field of referenced) {
67
+ if (field in columns) {
68
+ throw new LunoraError("MASK_UNSUPPORTED", `${method}() filtering "${tableName}" by masked column "${field}" is not supported`);
69
+ }
70
+ }
71
+ };
49
72
  const isFacadeEntry = (value) => {
50
73
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
51
74
  return false;
@@ -54,21 +77,27 @@ const isFacadeEntry = (value) => {
54
77
  return typeof candidate["findMany"] === "function" && typeof candidate["withSearchIndex"] === "function";
55
78
  };
56
79
  const wrapDatabase = (base, perTable, context) => {
57
- const wrapReader = (reader, columns) => {
80
+ const wrapReader = (reader, columns, tableName) => {
58
81
  return {
59
82
  collect: async () => {
60
83
  const rows = await reader.collect();
61
84
  return rows.map((row) => maskRow(row, columns, context));
62
85
  },
86
+ // SECURITY (value oracle): the predicate must see the MASKED row, not
87
+ // the raw stored row — otherwise a caller can `.filter(d => d.ssn ===
88
+ // guess)` to read the value the mask hides. Masking before the
89
+ // predicate keeps filtering on non-masked columns working while
90
+ // redacting masked cells the predicate can observe.
63
91
  filter: (predicate) => wrapReader(
64
- reader.filter((document) => predicate(document)),
65
- columns
92
+ reader.filter((document) => predicate(maskRow(document, columns, context))),
93
+ columns,
94
+ tableName
66
95
  ),
67
96
  first: async () => {
68
97
  const row = await reader.first();
69
98
  return row ? maskRow(row, columns, context) : null;
70
99
  },
71
- order: (direction) => wrapReader(reader.order(direction), columns),
100
+ order: (direction) => wrapReader(reader.order(direction), columns, tableName),
72
101
  paginate: async (options) => maskPage(await reader.paginate(options), columns, context),
73
102
  take: async (limit) => {
74
103
  const rows = await reader.take(limit);
@@ -78,8 +107,19 @@ const wrapDatabase = (base, perTable, context) => {
78
107
  const row = await reader.unique();
79
108
  return row ? maskRow(row, columns, context) : null;
80
109
  },
81
- withIndex: (indexName, range) => wrapReader(reader.withIndex(indexName, range), columns),
82
- withSearchIndex: (indexName, search) => wrapReader(reader.withSearchIndex(indexName, search), columns)
110
+ // SECURITY (value oracle): reject before delegating when the range /
111
+ // search references a masked column — an index range or search term
112
+ // over a masked column is the same value oracle as a masked-column
113
+ // `where`, so it must fail closed (see `assertIndexFieldsAllowed`).
114
+ // Reads over NON-masked columns pass through and still mask output.
115
+ withIndex: (indexName, range) => {
116
+ assertIndexFieldsAllowed(range, columns, tableName, "withIndex");
117
+ return wrapReader(reader.withIndex(indexName, range), columns, tableName);
118
+ },
119
+ withSearchIndex: (indexName, search) => {
120
+ assertIndexFieldsAllowed(search, columns, tableName, "withSearchIndex");
121
+ return wrapReader(reader.withSearchIndex(indexName, search), columns, tableName);
122
+ }
83
123
  };
84
124
  };
85
125
  const locate = async (id, expectedTable) => {
@@ -120,23 +160,63 @@ const wrapDatabase = (base, perTable, context) => {
120
160
  throw new LunoraError("MASK_UNSUPPORTED", `${method}() over masked column "${offending}" on "${tableName}" is not supported`);
121
161
  }
122
162
  };
163
+ const collectWhereFields = (where, into) => {
164
+ if (!where || typeof where !== "object" || Array.isArray(where)) {
165
+ return;
166
+ }
167
+ for (const [key, value] of Object.entries(where)) {
168
+ if (key === "AND" || key === "OR") {
169
+ if (Array.isArray(value)) {
170
+ for (const clause of value) {
171
+ collectWhereFields(clause, into);
172
+ }
173
+ }
174
+ } else if (key === "NOT") {
175
+ collectWhereFields(value, into);
176
+ } else if (!key.startsWith("__")) {
177
+ into.add(key);
178
+ }
179
+ }
180
+ };
181
+ const assertWhereAllowed = (tableName, where, method) => {
182
+ const columns = perTable.get(tableName);
183
+ if (!columns || where === void 0) {
184
+ return;
185
+ }
186
+ const referenced = /* @__PURE__ */ new Set();
187
+ collectWhereFields(where, referenced);
188
+ for (const field of referenced) {
189
+ if (field in columns) {
190
+ throw new LunoraError("MASK_UNSUPPORTED", `${method}() filtering "${tableName}" by masked column "${field}" is not supported`);
191
+ }
192
+ }
193
+ };
123
194
  const wrapped = {
124
195
  ...base,
125
196
  aggregate(tableName, options) {
126
197
  assertReductionAllowed(tableName, [options.field], "aggregate");
127
198
  return base.aggregate(tableName, options);
128
199
  },
200
+ count(tableName, whereOrArgs) {
201
+ const wrapper = whereOrArgs && typeof whereOrArgs === "object" && !Array.isArray(whereOrArgs) ? whereOrArgs : void 0;
202
+ const where = wrapper && ("where" in wrapper || "baseWhere" in wrapper || "restrictsCounts" in wrapper) ? wrapper.where : whereOrArgs;
203
+ assertWhereAllowed(tableName, where, "count");
204
+ return base.count(tableName, whereOrArgs);
205
+ },
129
206
  async findFirst(tableName, args) {
207
+ assertWhereAllowed(tableName, args?.where, "findFirst");
130
208
  const row = await base.findFirst(tableName, args);
131
209
  const columns = perTable.get(tableName);
132
210
  return row && columns ? maskRow(row, columns, context) : row;
133
211
  },
134
212
  async findFirstOrThrow(tableName, args) {
213
+ assertWhereAllowed(tableName, args?.where, "findFirstOrThrow");
135
214
  const row = await base.findFirstOrThrow(tableName, args);
136
215
  const columns = perTable.get(tableName);
137
216
  return columns ? maskRow(row, columns, context) : row;
138
217
  },
139
218
  async findMany(tableName, args) {
219
+ assertWhereAllowed(tableName, args?.where, "findMany");
140
220
  const page = await base.findMany(tableName, args);
141
221
  const columns = perTable.get(tableName);
142
222
  return columns ? maskPage(page, columns, context) : page;
@@ -156,7 +236,7 @@ const wrapDatabase = (base, perTable, context) => {
156
236
  query(tableName) {
157
237
  const reader = base.query(tableName);
158
238
  const columns = perTable.get(tableName);
159
- return columns ? wrapReader(reader, columns) : reader;
239
+ return columns ? wrapReader(reader, columns, tableName) : reader;
160
240
  },
161
241
  async rankPage(tableName, indexName, options) {
162
242
  const page = await base.rankPage(tableName, indexName, options);
@@ -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,4 +1,4 @@
1
- import { r as runMiddlewareChain } from './run-middleware-CYQOuoV6.mjs';
1
+ import { r as runMiddlewareChain } from './run-middleware-I6EiQfxL.mjs';
2
2
 
3
3
  const protectPublic = (options) => {
4
4
  const chain = [options.rateLimit, options.captcha, ...options.use ?? []].filter(
@@ -1,5 +1,6 @@
1
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
2
- import { bindTableFacade, bindOrm } from './bindOrm-DCuyr46L.mjs';
1
+ import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Bfgl7f-Q.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) => {
@@ -244,7 +245,6 @@ const isFacadeEntry = (value) => {
244
245
  return typeof candidate["findMany"] === "function" && typeof candidate["withSearchIndex"] === "function";
245
246
  };
246
247
  const wrapDatabase = (base, raw, perTable, context) => {
247
- const route = (tableName) => perTable.has(tableName) ? raw : base;
248
248
  const readBaseCache = /* @__PURE__ */ new Map();
249
249
  const readBase = (tableName) => {
250
250
  const cached = readBaseCache.get(tableName);
@@ -262,6 +262,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
262
262
  readBaseCache.set(tableName, result);
263
263
  return result;
264
264
  };
265
+ const route = (tableName) => readBase(tableName).restricts ? raw : base;
265
266
  const relationReadFilter = (table) => readBase(table).baseWhere;
266
267
  const locateRow = async (id, expectedTable) => {
267
268
  if (raw.lookupById) {
@@ -299,7 +300,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
299
300
  const nextRow = computeNextRow ? computeNextRow(located.row) : void 0;
300
301
  const writeOk = evaluateWrite(policies, op, { ...context, row: located.row }, nextRow);
301
302
  if (!writeOk) {
302
- throw new LunoraError("FORBIDDEN", `${op} on "${located.tableName}" denied by policy`);
303
+ throw new LunoraError("FORBIDDEN", `${op} denied by policy`);
303
304
  }
304
305
  }
305
306
  return perform(raw);
@@ -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) {
@@ -332,6 +333,16 @@ const wrapDatabase = (base, raw, perTable, context) => {
332
333
  }
333
334
  return { deleted: ids.length };
334
335
  },
336
+ async deleteWhere(tableName, where, options) {
337
+ const { baseWhere } = readBase(tableName);
338
+ const resolved = await route(tableName).findMany(tableName, {
339
+ baseWhere: mergeBaseWhere(where, baseWhere),
340
+ relationBaseWhere: relationReadFilter
341
+ });
342
+ const ids = resolved.page.map((row) => String(row["_id"]));
343
+ assertBatchLimit(ids.length, options?.limit, "deleteWhere");
344
+ return wrapped.deleteMany(ids, options);
345
+ },
335
346
  async findFirst(tableName, args) {
336
347
  const { baseWhere } = readBase(tableName);
337
348
  return route(tableName).findFirst(tableName, {
@@ -365,7 +376,10 @@ const wrapDatabase = (base, raw, perTable, context) => {
365
376
  return base.get(id, expectedTable);
366
377
  }
367
378
  const { baseWhere, restricts } = readBase(located.tableName);
368
- if (!restricts || !baseWhere) {
379
+ if (!restricts) {
380
+ return base.get(id, expectedTable);
381
+ }
382
+ if (!baseWhere) {
369
383
  return located.row;
370
384
  }
371
385
  const allowed = await raw.findFirst(located.tableName, { baseWhere, limit: 1, where: { _id: id } });
@@ -432,6 +446,19 @@ const wrapDatabase = (base, raw, perTable, context) => {
432
446
  expectedTable
433
447
  );
434
448
  }
449
+ return { patched: patches.length };
450
+ },
451
+ async patchWhere(tableName, args, options) {
452
+ const { baseWhere } = readBase(tableName);
453
+ const resolved = await route(tableName).findMany(tableName, {
454
+ baseWhere: mergeBaseWhere(args.where, baseWhere),
455
+ relationBaseWhere: relationReadFilter
456
+ });
457
+ const patches = resolved.page.map((row) => {
458
+ return { id: String(row["_id"]), patch: args.patch };
459
+ });
460
+ assertBatchLimit(patches.length, options?.limit, "patchWhere");
461
+ return wrapped.patchMany(patches, options);
435
462
  },
436
463
  query(tableName) {
437
464
  const { baseWhere } = readBase(tableName);
@@ -441,6 +468,22 @@ const wrapDatabase = (base, raw, perTable, context) => {
441
468
  }
442
469
  return reader.filter((document) => matchesWhere(document, baseWhere));
443
470
  },
471
+ // Restore clears the soft-delete marker — a by-id un-delete, gated as an
472
+ // "update" (the policy that governs patch). No post-image check: the
473
+ // writer owns the marker field, so we only enforce the pre-image USING.
474
+ restore: (id, expectedTable) => gateById(
475
+ id,
476
+ "update",
477
+ async (writer) => {
478
+ const perform = writer.restore ?? raw.restore;
479
+ if (!perform) {
480
+ throw new LunoraError("BAD_REQUEST", "restore is not supported by this writer");
481
+ }
482
+ await perform(id, expectedTable);
483
+ },
484
+ void 0,
485
+ expectedTable
486
+ ),
444
487
  replace: (id, document, expectedTable) => gateById(
445
488
  id,
446
489
  "update",
@@ -516,7 +559,7 @@ const indexRolePermissions = (roles) => {
516
559
  const rls = (policies, options = {}) => {
517
560
  const perTable = indexByTable(policies);
518
561
  const rolePermissions = indexRolePermissions(options.roles);
519
- return async ({ ctx, next }) => {
562
+ const middleware = async ({ ctx, next }) => {
520
563
  const auth = ctx.auth ?? {};
521
564
  const identity = await auth.getIdentity?.() ?? null;
522
565
  const roles = auth.roles ?? [];
@@ -546,6 +589,7 @@ const rls = (policies, options = {}) => {
546
589
  }
547
590
  return next({ ctx: extension });
548
591
  };
592
+ return tagRlsMiddleware(middleware, { policies, roles: options.roles ?? [] });
549
593
  };
550
594
 
551
595
  export { computeReadBaseWhere, evaluateWrite, indexRolePermissions, matchesWhere, permissionName, rls };
@@ -1,8 +1,10 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
+
1
3
  const runMiddlewareChain = async (middlewares, baseContext, terminal) => {
2
4
  let lastIndex = -1;
3
5
  const dispatch = async (index, context) => {
4
6
  if (index <= lastIndex) {
5
- throw new Error("middleware next() called multiple times");
7
+ throw new LunoraError$1("INTERNAL", "middleware next() called multiple times");
6
8
  }
7
9
  lastIndex = index;
8
10
  const middleware = middlewares[index];
@@ -1,4 +1,4 @@
1
- import { LunoraError } from './LunoraError-DhggBJZF.mjs';
1
+ import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
2
2
 
3
3
  const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
4
4
  const indexRolePermissions = (roles = []) => {
@@ -45,10 +45,10 @@ type PolicyDecisionOf<DM, REL extends Record<keyof DM, object>, T extends keyof
45
45
  * unknown table, a stray column, or a relation predicate naming a relation the
46
46
  * table does not declare is a compile error rather than a silent runtime deny.
47
47
  */
48
- interface TypedDefinePolicyInput<DM, REL extends Record<keyof DM, object>, T extends keyof DM, Context = unknown> {
48
+ interface TypedDefinePolicyInput<DM, REL extends Record<keyof DM, object>, T extends keyof DM, Context = unknown, Identity = Record<string, unknown>> {
49
49
  on: PolicyOperation;
50
50
  table: T;
51
- when: (context: PolicyContext<Context>) => PolicyDecisionOf<DM, REL, T>;
51
+ when: (context: PolicyContext<Context, Identity>) => PolicyDecisionOf<DM, REL, T>;
52
52
  }
53
53
  /**
54
54
  * Context handed to a policy. `auth.roles` is the per-request role list,
@@ -59,7 +59,7 @@ interface TypedDefinePolicyInput<DM, REL extends Record<keyof DM, object>, T ext
59
59
  * pre-write row; for `insert` it is the candidate document. `ctx` is the full
60
60
  * procedure context the middleware closed over.
61
61
  */
62
- interface PolicyContext<Context = unknown> {
62
+ interface PolicyContext<Context = unknown, Identity = Record<string, unknown>> {
63
63
  readonly auth: {
64
64
  /**
65
65
  * `true` when any of the request's `roles` grants `permission` (passed
@@ -68,7 +68,13 @@ interface PolicyContext<Context = unknown> {
68
68
  * when none of the request's roles lists the permission.
69
69
  */
70
70
  readonly can: (permission: Permission | string) => boolean;
71
- readonly identity?: Record<string, unknown> | null;
71
+ /**
72
+ * The resolved identity, typed to the `Identity` type parameter bound on
73
+ * `createPolicyDsl` — e.g. the app's `defineIdentity(...)` claim type via
74
+ * the `InferIdentity` helper; otherwise the untyped claim bag.
75
+ * `undefined`/`null` when the request is anonymous.
76
+ */
77
+ readonly identity?: Identity | null;
72
78
  readonly roles: ReadonlyArray<string>;
73
79
  readonly userId: null | string;
74
80
  };
@@ -45,10 +45,10 @@ type PolicyDecisionOf<DM, REL extends Record<keyof DM, object>, T extends keyof
45
45
  * unknown table, a stray column, or a relation predicate naming a relation the
46
46
  * table does not declare is a compile error rather than a silent runtime deny.
47
47
  */
48
- interface TypedDefinePolicyInput<DM, REL extends Record<keyof DM, object>, T extends keyof DM, Context = unknown> {
48
+ interface TypedDefinePolicyInput<DM, REL extends Record<keyof DM, object>, T extends keyof DM, Context = unknown, Identity = Record<string, unknown>> {
49
49
  on: PolicyOperation;
50
50
  table: T;
51
- when: (context: PolicyContext<Context>) => PolicyDecisionOf<DM, REL, T>;
51
+ when: (context: PolicyContext<Context, Identity>) => PolicyDecisionOf<DM, REL, T>;
52
52
  }
53
53
  /**
54
54
  * Context handed to a policy. `auth.roles` is the per-request role list,
@@ -59,7 +59,7 @@ interface TypedDefinePolicyInput<DM, REL extends Record<keyof DM, object>, T ext
59
59
  * pre-write row; for `insert` it is the candidate document. `ctx` is the full
60
60
  * procedure context the middleware closed over.
61
61
  */
62
- interface PolicyContext<Context = unknown> {
62
+ interface PolicyContext<Context = unknown, Identity = Record<string, unknown>> {
63
63
  readonly auth: {
64
64
  /**
65
65
  * `true` when any of the request's `roles` grants `permission` (passed
@@ -68,7 +68,13 @@ interface PolicyContext<Context = unknown> {
68
68
  * when none of the request's roles lists the permission.
69
69
  */
70
70
  readonly can: (permission: Permission | string) => boolean;
71
- readonly identity?: Record<string, unknown> | null;
71
+ /**
72
+ * The resolved identity, typed to the `Identity` type parameter bound on
73
+ * `createPolicyDsl` — e.g. the app's `defineIdentity(...)` claim type via
74
+ * the `InferIdentity` helper; otherwise the untyped claim bag.
75
+ * `undefined`/`null` when the request is anonymous.
76
+ */
77
+ readonly identity?: Identity | null;
72
78
  readonly roles: ReadonlyArray<string>;
73
79
  readonly userId: null | string;
74
80
  };
@@ -1,4 +1,4 @@
1
- import { P as PolicyOperation, R as Role, a as Policy } from "../packem_shared/types.d-DmvyEMD6.mjs";
1
+ import { P as PolicyOperation, R as Role, a as Policy } from "../packem_shared/types.d-BB3pjV0m.mjs";
2
2
  import "../data-model.mjs";
3
3
  /**
4
4
  * The slice of a request identity a policy reads. Mirrors the