@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.
- package/LICENSE.md +6 -0
- package/README.md +50 -0
- package/dist/data-model.d.mts +231 -156
- package/dist/data-model.d.ts +231 -156
- package/dist/index.d.mts +1191 -811
- package/dist/index.d.ts +1191 -811
- package/dist/index.mjs +20 -15
- package/dist/packem_shared/{LunoraEnvError-DjFkpkSP.mjs → LunoraEnvError-BGmd1Qs0.mjs} +4 -4
- package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +9 -0
- package/dist/packem_shared/{PRESENCE_DEFAULT_TTL_MS-UQuUI5sV.mjs → PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs} +5 -5
- package/dist/packem_shared/{bindOrm-Ce57S3N9.mjs → bindOrm-CaY7Wq9Z.mjs} +42 -23
- package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +107 -0
- package/dist/packem_shared/{composePluginMiddleware-Ck5_TUO8.mjs → composePluginMiddleware-z62dttBo.mjs} +11 -8
- package/dist/packem_shared/{createPolicyDsl-De67zPDS.mjs → createPolicyDsl-By3QB4he.mjs} +4 -1
- package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +58 -0
- package/dist/packem_shared/{defineAggregateIndex-B20MIOmj.mjs → defineAggregateIndex-cdo0g-un.mjs} +108 -8
- package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +35 -0
- package/dist/packem_shared/{defineMigration-CAJLr6fx.mjs → defineMigration-Hx01yIht.mjs} +3 -1
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
- package/dist/packem_shared/defineShape-C5scNOrf.mjs +18 -0
- package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs} +4 -1
- package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
- package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-DCXoYPIk.mjs} +42 -27
- package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-D0Wuki7S.mjs} +36 -7
- package/dist/packem_shared/{mask-eCUYOwhd.mjs → mask-BepaW7YN.mjs} +132 -9
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
- package/dist/packem_shared/{protectPublic-BjFkQ_Or.mjs → protectPublic-BlcGpiRc.mjs} +1 -1
- package/dist/packem_shared/{rls-Bi9HiyDC.mjs → rls-BaDQf7MG.mjs} +34 -6
- package/dist/packem_shared/{run-middleware-CYQOuoV6.mjs → run-middleware-I6EiQfxL.mjs} +3 -1
- package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-6QxzDOcx.mjs} +1 -1
- package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
- package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
- package/dist/rls/testing.d.mts +31 -31
- package/dist/rls/testing.d.ts +31 -31
- package/dist/rls/testing.mjs +1 -1
- package/dist/types.d.mts +957 -433
- package/dist/types.d.ts +957 -433
- package/package.json +6 -5
- package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
- package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
- package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
package/dist/packem_shared/{defineAggregateIndex-B20MIOmj.mjs → defineAggregateIndex-cdo0g-un.mjs}
RENAMED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|
package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs}
RENAMED
|
@@ -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
|
|
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
|
}
|
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
if (
|
|
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(
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 {
|
|
2
|
-
import { r as
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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 = {
|