@lunora/server 1.0.0-alpha.3 → 1.0.0-alpha.30

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 +50 -0
  3. package/dist/data-model.d.mts +231 -156
  4. package/dist/data-model.d.ts +231 -156
  5. package/dist/index.d.mts +1191 -811
  6. package/dist/index.d.ts +1191 -811
  7. package/dist/index.mjs +20 -15
  8. package/dist/packem_shared/{LunoraEnvError-DjFkpkSP.mjs → LunoraEnvError-BGmd1Qs0.mjs} +4 -4
  9. package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +9 -0
  10. package/dist/packem_shared/{PRESENCE_DEFAULT_TTL_MS-UQuUI5sV.mjs → PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs} +5 -5
  11. package/dist/packem_shared/{bindOrm-Ce57S3N9.mjs → bindOrm-CaY7Wq9Z.mjs} +42 -23
  12. package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +107 -0
  13. package/dist/packem_shared/{composePluginMiddleware-Ck5_TUO8.mjs → composePluginMiddleware-z62dttBo.mjs} +11 -8
  14. package/dist/packem_shared/{createPolicyDsl-De67zPDS.mjs → createPolicyDsl-By3QB4he.mjs} +4 -1
  15. package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +58 -0
  16. package/dist/packem_shared/{defineAggregateIndex-B20MIOmj.mjs → defineAggregateIndex-cdo0g-un.mjs} +108 -8
  17. package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +35 -0
  18. package/dist/packem_shared/{defineMigration-CAJLr6fx.mjs → defineMigration-Hx01yIht.mjs} +3 -1
  19. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
  20. package/dist/packem_shared/defineShape-C5scNOrf.mjs +18 -0
  21. package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs} +4 -1
  22. package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
  23. package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-DCXoYPIk.mjs} +42 -27
  24. package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-D0Wuki7S.mjs} +36 -7
  25. package/dist/packem_shared/{mask-eCUYOwhd.mjs → mask-BepaW7YN.mjs} +132 -9
  26. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
  27. package/dist/packem_shared/{protectPublic-BjFkQ_Or.mjs → protectPublic-BlcGpiRc.mjs} +1 -1
  28. package/dist/packem_shared/{rls-Bi9HiyDC.mjs → rls-BaDQf7MG.mjs} +34 -6
  29. package/dist/packem_shared/{run-middleware-CYQOuoV6.mjs → run-middleware-I6EiQfxL.mjs} +3 -1
  30. package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-6QxzDOcx.mjs} +1 -1
  31. package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
  32. package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
  33. package/dist/rls/testing.d.mts +31 -31
  34. package/dist/rls/testing.d.ts +31 -31
  35. package/dist/rls/testing.mjs +1 -1
  36. package/dist/types.d.mts +957 -433
  37. package/dist/types.d.ts +957 -433
  38. package/package.json +6 -5
  39. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  40. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  41. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
@@ -1,5 +1,6 @@
1
+ import { LunoraError as LunoraError$1 } from '@lunora/errors';
1
2
  import { isOrWrapsFromValidator, v } from '@lunora/values';
2
- import { mergeSchemaExtension } from './composePluginMiddleware-Ck5_TUO8.mjs';
3
+ import { mergeSchemaExtension } from './composePluginMiddleware-z62dttBo.mjs';
3
4
 
4
5
  const relationBuilder = {
5
6
  many: (table, options) => {
@@ -26,10 +27,11 @@ const defineTable = (inputShape) => {
26
27
  const shape = { ...inputShape };
27
28
  for (const [columnName, validator] of Object.entries(shape)) {
28
29
  if (isOrWrapsFromValidator(validator)) {
29
- 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`);
30
31
  }
31
32
  }
32
33
  const aggregateIndexes = [];
34
+ const geoIndexes = [];
33
35
  const indexes = [];
34
36
  const rankIndexes = [];
35
37
  const relations = {};
@@ -41,11 +43,13 @@ const defineTable = (inputShape) => {
41
43
  let isExternallyManaged = false;
42
44
  let isPublic = false;
43
45
  let softDelete;
46
+ let ttl;
47
+ let externalSource;
44
48
  const builder = {
45
49
  aggregateIndex(name, options) {
46
50
  const op = options?.op ?? "count";
47
51
  if (op !== "count" && !options?.field) {
48
- throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
52
+ throw new LunoraError$1("INTERNAL", `aggregateIndex "${name}": op "${op}" requires a "field"`);
49
53
  }
50
54
  aggregateIndexes.push({
51
55
  by: options?.by,
@@ -63,10 +67,20 @@ const defineTable = (inputShape) => {
63
67
  get aggregateIndexes() {
64
68
  return aggregateIndexes;
65
69
  },
70
+ get externalSource() {
71
+ return externalSource;
72
+ },
66
73
  externallyManaged() {
67
74
  isExternallyManaged = true;
68
75
  return builder;
69
76
  },
77
+ geoIndex(name, options) {
78
+ geoIndexes.push({ field: options.field, name, precision: options.precision });
79
+ return builder;
80
+ },
81
+ get geoIndexes() {
82
+ return geoIndexes;
83
+ },
70
84
  global(options) {
71
85
  shardMode = { backend: options?.backend ?? "d1", kind: "global" };
72
86
  return builder;
@@ -90,7 +104,7 @@ const defineTable = (inputShape) => {
90
104
  },
91
105
  rankIndex(name, options) {
92
106
  if (!options.sortBy || options.sortBy.length === 0) {
93
- throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
107
+ throw new LunoraError$1("INTERNAL", `rankIndex "${name}": "sortBy" is required and must list at least one key`);
94
108
  }
95
109
  const sortBy = options.sortBy.map((key) => {
96
110
  return {
@@ -137,6 +151,17 @@ const defineTable = (inputShape) => {
137
151
  get softDeleteMode() {
138
152
  return softDelete;
139
153
  },
154
+ source(definition) {
155
+ if (!definition.binding) {
156
+ throw new LunoraError$1("INTERNAL", "source: `binding` is required (the wrangler Hyperdrive binding name)");
157
+ }
158
+ if (!definition.query) {
159
+ throw new LunoraError$1("INTERNAL", "source: `query` is required (the tenant-membership SQL)");
160
+ }
161
+ externalSource = definition;
162
+ isExternallyManaged = true;
163
+ return builder;
164
+ },
140
165
  softDelete(options) {
141
166
  const field = options?.field ?? "deletedAt";
142
167
  softDelete = { field };
@@ -152,6 +177,13 @@ const defineTable = (inputShape) => {
152
177
  Object.assign(triggers, build(triggerBuilder));
153
178
  return builder;
154
179
  },
180
+ ttl(field, options) {
181
+ ttl = { after: options?.after, field };
182
+ return builder;
183
+ },
184
+ get ttlPolicy() {
185
+ return ttl;
186
+ },
155
187
  get vectorIndexes() {
156
188
  return vectorIndexes;
157
189
  },
@@ -183,13 +215,13 @@ const defineVectorIndex = (options) => {
183
215
  const defineAggregateIndex = (name, options) => {
184
216
  const op = options.op ?? "count";
185
217
  if (op !== "count" && !options.field) {
186
- throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
218
+ throw new LunoraError$1("INTERNAL", `aggregateIndex "${name}": op "${op}" requires a "field"`);
187
219
  }
188
220
  return { by: options.by, field: options.field, name, on: options.on, op, where: options.where };
189
221
  };
190
222
  const defineRankIndex = (name, options) => {
191
223
  if (!options.sortBy || options.sortBy.length === 0) {
192
- throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
224
+ throw new LunoraError$1("INTERNAL", `rankIndex "${name}": "sortBy" is required and must list at least one key`);
193
225
  }
194
226
  const sortBy = options.sortBy.map((key) => {
195
227
  return {
@@ -205,6 +237,9 @@ const withExtend = (schema) => {
205
237
  extend(extension) {
206
238
  return withExtend(mergeSchemaExtension(schema, extension));
207
239
  },
240
+ jurisdiction(_jurisdiction) {
241
+ return withExtend(schema);
242
+ },
208
243
  rls(mode) {
209
244
  return withExtend({ ...schema, rlsMode: mode });
210
245
  }
@@ -228,21 +263,86 @@ const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
228
263
  for (const index of Object.values(aggregateIndexes)) {
229
264
  const table = tables[index.on];
230
265
  if (!table) {
231
- throw new Error(`defineAggregateIndex "${index.name}": unknown table "${index.on}"`);
266
+ throw new LunoraError$1("INTERNAL", `defineAggregateIndex "${index.name}": unknown table "${index.on}"`);
232
267
  }
233
268
  table.aggregateIndexes.push(index);
234
269
  }
235
270
  for (const index of Object.values(rankIndexes)) {
236
271
  const table = tables[index.on];
237
272
  if (!table) {
238
- throw new Error(`defineRankIndex "${index.name}": unknown table "${index.on}"`);
273
+ throw new LunoraError$1("INTERNAL", `defineRankIndex "${index.name}": unknown table "${index.on}"`);
239
274
  }
240
275
  table.rankIndexes.push(index);
241
276
  }
242
277
  };
278
+ const strayIncrementalKnob = (source) => {
279
+ if (source.cursor) {
280
+ return "cursor";
281
+ }
282
+ if (source.reconcileEveryMs !== void 0) {
283
+ return "reconcileEveryMs";
284
+ }
285
+ if (source.softDeleteColumn !== void 0) {
286
+ return "softDeleteColumn";
287
+ }
288
+ return void 0;
289
+ };
290
+ const validateExternalSourceMode = (name, source) => {
291
+ const isIncremental = source.mode === "incremental";
292
+ if (source.mode !== void 0 && source.mode !== "full-pull" && !isIncremental) {
293
+ throw new LunoraError$1(
294
+ "INTERNAL",
295
+ `defineSchema: table "${name}" uses \`mode: ${JSON.stringify(source.mode)}\` — supported modes are "full-pull" (default) and "incremental".`
296
+ );
297
+ }
298
+ if (!isIncremental) {
299
+ const strayKnob = strayIncrementalKnob(source);
300
+ if (strayKnob) {
301
+ throw new LunoraError$1(
302
+ "INTERNAL",
303
+ `defineSchema: table "${name}" sets \`${strayKnob}\` but is not \`mode: "incremental"\` — that knob only applies to incremental ingest. Set \`mode: "incremental"\` or remove \`${strayKnob}\`.`
304
+ );
305
+ }
306
+ return;
307
+ }
308
+ if (!source.cursor?.column || !source.cursor.query) {
309
+ throw new LunoraError$1(
310
+ "INTERNAL",
311
+ `defineSchema: table "${name}" is \`mode: "incremental"\` but has no \`cursor\` — add \`cursor: { column: "updated_at", query: "… WHERE … > $N" }\` binding the watermark as the trailing parameter.`
312
+ );
313
+ }
314
+ if (source.reconcileEveryMs === void 0 && source.softDeleteColumn === void 0) {
315
+ throw new LunoraError$1(
316
+ "INTERNAL",
317
+ `defineSchema: table "${name}" is \`mode: "incremental"\` with no delete-visibility path — an incremental pull never sees upstream deletes, so it would accumulate phantom rows. Add \`reconcileEveryMs\` (a periodic full-pull sweep) or \`softDeleteColumn\` (an upstream tombstone column).`
318
+ );
319
+ }
320
+ };
321
+ const validateExternalSources = (tables) => {
322
+ for (const [name, table] of Object.entries(tables)) {
323
+ const source = table.externalSource;
324
+ if (!source) {
325
+ continue;
326
+ }
327
+ if (table.shardMode.kind === "global") {
328
+ throw new LunoraError$1(
329
+ "INTERNAL",
330
+ `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`
331
+ );
332
+ }
333
+ if (table.shardMode.kind === "shardBy" && !source.tenantBy) {
334
+ throw new LunoraError$1(
335
+ "INTERNAL",
336
+ `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.`
337
+ );
338
+ }
339
+ validateExternalSourceMode(name, source);
340
+ }
341
+ };
243
342
  const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
244
343
  fillIndexTableNames(tables);
245
344
  attachStandaloneIndexes(tables, aggregateIndexes, rankIndexes);
345
+ validateExternalSources(tables);
246
346
  return withExtend({ tables, vectorIndexes });
247
347
  };
248
348
 
@@ -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 };
@@ -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,17 +34,32 @@ 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,
57
+ ...state.expose ? { expose: state.expose } : {},
46
58
  handler: makeHandler(state.args, state.middlewares, userHandler, state.output),
47
59
  kind,
48
- ...visibility ? { visibility } : {}
60
+ ...rls ? { rls } : {},
61
+ ...visibility ? { visibility } : {},
62
+ ...state.x402 ? { x402: state.x402 } : {}
49
63
  };
50
64
  },
51
65
  output: (validator) => makeBuilder(kind, { ...state, output: validator }, visibility),
@@ -55,15 +69,30 @@ const makeBuilder = (kind, state, visibility) => {
55
69
  // unconditionally keeps the runtime free of per-kind branching.
56
70
  ...kind === "query" ? {
57
71
  stream: (userHandler) => {
72
+ const rls = collectRls(state.middlewares);
58
73
  return {
59
74
  args: state.args,
75
+ ...state.expose ? { expose: state.expose } : {},
60
76
  handler: makeStreamHandler(state.args, state.middlewares, userHandler),
61
77
  kind: "stream",
62
- ...visibility ? { visibility } : {}
78
+ ...rls ? { rls } : {},
79
+ ...visibility ? { visibility } : {},
80
+ ...state.x402 ? { x402: state.x402 } : {}
63
81
  };
64
82
  }
65
83
  } : {},
66
- use: (middleware) => makeBuilder(kind, { ...state, middlewares: [...state.middlewares, middleware] }, visibility)
84
+ use: (middleware) => makeBuilder(kind, { ...state, middlewares: [...state.middlewares, middleware] }, visibility),
85
+ // `.expose({ rest: true })` publishes a PUBLIC procedure on the REST surface
86
+ // (plan 167). Public-only for the same reason as `.x402`: an internal
87
+ // function is server-to-server and never reachable over HTTP, so there is
88
+ // nothing to expose. Default-closed — omitting the modifier keeps the
89
+ // procedure RPC-only.
90
+ ...visibility ? {} : { expose: (config) => makeBuilder(kind, { ...state, expose: config }, visibility) },
91
+ // `.x402({ price })` marks a public procedure as paid. It's public-only:
92
+ // internal functions are server-to-server (cron/scheduler/`ctx.run*`) and
93
+ // never reachable via a client RPC, so there's nothing to charge. Omitting
94
+ // it on internal builders keeps the runtime shape aligned with the types.
95
+ ...visibility ? {} : { x402: (config) => makeBuilder(kind, { ...state, x402: config }, visibility) }
67
96
  };
68
97
  };
69
98
  const initLunora = {