@lunora/server 0.0.0 → 1.0.0-alpha.1

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 (39) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +130 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/data-model.d.mts +328 -0
  5. package/dist/data-model.d.ts +328 -0
  6. package/dist/data-model.mjs +1 -0
  7. package/dist/drizzle.d.mts +1 -0
  8. package/dist/drizzle.d.ts +1 -0
  9. package/dist/drizzle.mjs +1 -0
  10. package/dist/index.d.mts +1741 -0
  11. package/dist/index.d.ts +1741 -0
  12. package/dist/index.mjs +24 -0
  13. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +51 -0
  14. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +11 -0
  15. package/dist/packem_shared/bindTableFacade-DCuyr46L.mjs +71 -0
  16. package/dist/packem_shared/defineAggregateIndex-DzqxtAyV.mjs +236 -0
  17. package/dist/packem_shared/defineEnv-DjFkpkSP.mjs +187 -0
  18. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +8 -0
  19. package/dist/packem_shared/definePolicy-De67zPDS.mjs +29 -0
  20. package/dist/packem_shared/definePresence-D5LtwGl0.mjs +114 -0
  21. package/dist/packem_shared/defineSchemaExtension-Ck5_TUO8.mjs +100 -0
  22. package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +20 -0
  23. package/dist/packem_shared/httpAction-B7FYUEgr.mjs +340 -0
  24. package/dist/packem_shared/initLunora-CATvPsVt.mjs +86 -0
  25. package/dist/packem_shared/mask-CkZJHHMM.mjs +211 -0
  26. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +13 -0
  27. package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +15 -0
  28. package/dist/packem_shared/rls-Zhf5wEeJ.mjs +551 -0
  29. package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +18 -0
  30. package/dist/packem_shared/storageRules-4a30FSpI.mjs +88 -0
  31. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +135 -0
  32. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +135 -0
  33. package/dist/rls/testing.d.mts +63 -0
  34. package/dist/rls/testing.d.ts +63 -0
  35. package/dist/rls/testing.mjs +49 -0
  36. package/dist/types.d.mts +1029 -0
  37. package/dist/types.d.ts +1029 -0
  38. package/dist/types.mjs +31 -0
  39. package/package.json +59 -17
package/dist/index.mjs ADDED
@@ -0,0 +1,24 @@
1
+ export { default as asBucketStorage } from './packem_shared/asBucketStorage-Cnxd9y2q.mjs';
2
+ export { initLunora } from './packem_shared/initLunora-CATvPsVt.mjs';
3
+ export { LunoraEnvError, defineEnv, redactSecrets } from './packem_shared/defineEnv-DjFkpkSP.mjs';
4
+ export { LunoraError } from './packem_shared/LunoraError-DhggBJZF.mjs';
5
+ export { bindOrm, bindTableFacade } from './packem_shared/bindTableFacade-DCuyr46L.mjs';
6
+ export { httpAction, httpRoute, httpRouter, serveStorageObject } from './packem_shared/httpAction-B7FYUEgr.mjs';
7
+ export { onConnect, onDisconnect } from './packem_shared/onConnect-CIPXKPyw.mjs';
8
+ export { defineMigration } from './packem_shared/defineMigration-CAJLr6fx.mjs';
9
+ export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/defineSchemaExtension-Ck5_TUO8.mjs';
10
+ export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/definePresence-D5LtwGl0.mjs';
11
+ export { protectPublic } from './packem_shared/protectPublic-BjFkQ_Or.mjs';
12
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-DzqxtAyV.mjs';
13
+ export { anyApi } from './types.mjs';
14
+ export { cronJobs } from '@lunora/scheduler';
15
+ export { ValidationError, v } from '@lunora/values';
16
+ export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/definePolicy-De67zPDS.mjs';
17
+ export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-qu0mpilX.mjs';
18
+ export { mask } from './packem_shared/mask-CkZJHHMM.mjs';
19
+ export { rls } from './packem_shared/rls-Zhf5wEeJ.mjs';
20
+ export { storageRules } from './packem_shared/storageRules-4a30FSpI.mjs';
21
+
22
+ const VERSION = "0.0.0";
23
+
24
+ export { VERSION };
@@ -0,0 +1,51 @@
1
+ const CODE_STATUS = {
2
+ BAD_REQUEST: 400,
3
+ CONFLICT: 409,
4
+ /**
5
+ * `count()` invoked against a table whose context carries an active RLS
6
+ * policy. The operation itself is unsupported in an RLS-restricted reader
7
+ * (kitcn's documented constraint) — the request is well-formed and the
8
+ * caller is authorized, so this is a 422 (semantic conflict) rather than a
9
+ * 403 (policy denial).
10
+ */
11
+ COUNT_RLS_UNSUPPORTED: 422,
12
+ FORBIDDEN: 403,
13
+ INTERNAL_SERVER_ERROR: 500,
14
+ /**
15
+ * An analytical reduction (`aggregate` / `groupBy`) was invoked over a
16
+ * column that the procedure's `mask()` middleware redacts. A masked column
17
+ * can't be summed, averaged, or grouped without leaking the very values the
18
+ * mask hides (a group key *is* the raw value; an aggregate is computed from
19
+ * it), so the operation fails closed. The request is well-formed and the
20
+ * caller is authorized — this is a 422 (semantic conflict), mirroring
21
+ * `COUNT_RLS_UNSUPPORTED`.
22
+ */
23
+ MASK_UNSUPPORTED: 422,
24
+ NOT_FOUND: 404,
25
+ NOT_IMPLEMENTED: 501,
26
+ /**
27
+ * A write policy's `when` returned a relation-crossing predicate
28
+ * (`some`/`none`/`every`/`is`/`isNot`). The in-memory write-policy evaluator
29
+ * has no child fetcher and cannot resolve a relation node, so the policy is
30
+ * unsupported as written. Relation predicates are valid in *read* policies
31
+ * and query `where` clauses (the pre-resolver handles them there). The
32
+ * request is well-formed; this is a 422 (semantic conflict), mirroring the
33
+ * sibling `*_UNSUPPORTED` codes.
34
+ */
35
+ RELATION_PREDICATE_UNSUPPORTED: 422,
36
+ TOO_MANY_REQUESTS: 429,
37
+ UNAUTHORIZED: 401,
38
+ UNPROCESSABLE: 422
39
+ };
40
+ class LunoraError extends Error {
41
+ name = "LunoraError";
42
+ code;
43
+ status;
44
+ constructor(code, message) {
45
+ super(message ?? code);
46
+ this.code = code;
47
+ this.status = CODE_STATUS[code];
48
+ }
49
+ }
50
+
51
+ export { LunoraError };
@@ -0,0 +1,11 @@
1
+ const asBucketStorage = (raw) => {
2
+ const candidate = raw ?? {};
3
+ if (typeof candidate.bucket === "function") {
4
+ return candidate;
5
+ }
6
+ const self = { ...candidate, bucketName: "default" };
7
+ self.bucket = () => self;
8
+ return self;
9
+ };
10
+
11
+ export { asBucketStorage as default };
@@ -0,0 +1,71 @@
1
+ const bindTableFacade = (writer, tableName) => {
2
+ return {
3
+ aggregate: (options) => writer.aggregate(tableName, options),
4
+ count: (where) => writer.count(tableName, where),
5
+ delete: (id) => writer.delete(id, tableName),
6
+ // `deleteMany`/`patchMany` forward the bound `tableName` as `expectedTable`
7
+ // (threaded through the writer + the RLS middleware's per-id gate) so every
8
+ // batched id is scoped to this table — the same IDOR guard the single-row
9
+ // `delete`/`patch` apply. `patchMany` maps the facade's `values` payload to
10
+ // the writer's `{ id, patch }` shape.
11
+ deleteMany: (ids, options) => {
12
+ if (writer.deleteMany === void 0) {
13
+ throw new Error(`ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
14
+ }
15
+ return writer.deleteMany(ids, options, tableName);
16
+ },
17
+ findFirst: (args) => writer.findFirst(tableName, args),
18
+ findFirstOrThrow: (args) => writer.findFirstOrThrow(tableName, args),
19
+ findMany: (args) => writer.findMany(tableName, args),
20
+ get: (id) => writer.get(id, tableName),
21
+ groupBy: (options) => writer.groupBy(tableName, options),
22
+ insert: (document) => writer.insert(tableName, document),
23
+ insertMany: (documents, options) => {
24
+ if (writer.insertMany === void 0) {
25
+ throw new Error(`ctx.db.${tableName}.insertMany is unavailable: this writer has no batch insert`);
26
+ }
27
+ return writer.insertMany(tableName, documents, options);
28
+ },
29
+ patch: (id, patch) => writer.patch(id, patch, tableName),
30
+ patchMany: (patches, options) => {
31
+ if (writer.patchMany === void 0) {
32
+ throw new Error(`ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
33
+ }
34
+ return writer.patchMany(
35
+ patches.map((entry) => {
36
+ return { id: entry.id, patch: entry.values };
37
+ }),
38
+ options,
39
+ tableName
40
+ );
41
+ },
42
+ rank: (indexName, options) => writer.rank(tableName, indexName, options),
43
+ rankPage: (indexName, options) => writer.rankPage(tableName, indexName, options),
44
+ replace: (id, document) => writer.replace(id, document, tableName),
45
+ withSearchIndex: (indexName, search) => writer.query(tableName).withSearchIndex(indexName, search)
46
+ };
47
+ };
48
+ const bindOrm = (facade) => {
49
+ const resolve = (table) => {
50
+ const bound = facade[table];
51
+ if (!bound) {
52
+ throw new Error(`unknown table: ${table}`);
53
+ }
54
+ return bound;
55
+ };
56
+ return {
57
+ delete: (table, id) => resolve(table).delete(id),
58
+ insert: (table) => {
59
+ return { values: (document) => resolve(table).insert(document) };
60
+ },
61
+ query: facade,
62
+ replace: (table, id) => {
63
+ return { with: (document) => resolve(table).replace(id, document) };
64
+ },
65
+ update: (table, id) => {
66
+ return { set: (values) => resolve(table).patch(id, values) };
67
+ }
68
+ };
69
+ };
70
+
71
+ export { bindOrm, bindTableFacade };
@@ -0,0 +1,236 @@
1
+ import { isOrWrapsFromValidator } from '@lunora/values';
2
+ import { mergeSchemaExtension } from './defineSchemaExtension-Ck5_TUO8.mjs';
3
+
4
+ const relationBuilder = {
5
+ many: (table, options) => {
6
+ return { field: options.field, kind: "many", references: options.references ?? "_id", table };
7
+ },
8
+ one: (table, options) => {
9
+ return { field: options.field, kind: "one", onDelete: options.onDelete, references: options.references ?? "_id", table };
10
+ }
11
+ };
12
+ const makeTrigger = (timing, op, handler) => {
13
+ return { handler, op, timing };
14
+ };
15
+ const createTriggerBuilder = () => {
16
+ return {
17
+ afterDelete: (handler) => makeTrigger("after", "delete", handler),
18
+ afterInsert: (handler) => makeTrigger("after", "insert", handler),
19
+ afterUpdate: (handler) => makeTrigger("after", "update", handler),
20
+ beforeDelete: (handler) => makeTrigger("before", "delete", handler),
21
+ beforeInsert: (handler) => makeTrigger("before", "insert", handler),
22
+ beforeUpdate: (handler) => makeTrigger("before", "update", handler)
23
+ };
24
+ };
25
+ const defineTable = (shape) => {
26
+ for (const [columnName, validator] of Object.entries(shape)) {
27
+ if (isOrWrapsFromValidator(validator)) {
28
+ throw new Error(`defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
29
+ }
30
+ }
31
+ const aggregateIndexes = [];
32
+ const indexes = [];
33
+ const rankIndexes = [];
34
+ const relations = {};
35
+ const searchIndexes = [];
36
+ const triggers = {};
37
+ const triggerBuilder = createTriggerBuilder();
38
+ const vectorIndexes = [];
39
+ let shardMode = { kind: "root" };
40
+ let isExternallyManaged = false;
41
+ let isPublic = false;
42
+ const builder = {
43
+ aggregateIndex(name, options) {
44
+ const op = options?.op ?? "count";
45
+ if (op !== "count" && !options?.field) {
46
+ throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
47
+ }
48
+ aggregateIndexes.push({
49
+ by: options?.by,
50
+ field: options?.field,
51
+ name,
52
+ // `on` is filled in by `defineSchema` once the table is keyed; we
53
+ // stash the placeholder so the AggregateIndexDefinition shape stays
54
+ // straightforward for D1/DO consumers (who read `on`).
55
+ on: "",
56
+ op,
57
+ where: options?.where
58
+ });
59
+ return builder;
60
+ },
61
+ get aggregateIndexes() {
62
+ return aggregateIndexes;
63
+ },
64
+ externallyManaged() {
65
+ isExternallyManaged = true;
66
+ return builder;
67
+ },
68
+ global(options) {
69
+ shardMode = { backend: options?.backend ?? "d1", kind: "global" };
70
+ return builder;
71
+ },
72
+ get isExternallyManaged() {
73
+ return isExternallyManaged;
74
+ },
75
+ get isPublic() {
76
+ return isPublic;
77
+ },
78
+ index(name, fields, options) {
79
+ indexes.push({ fields, name, unique: options?.unique ?? false });
80
+ return builder;
81
+ },
82
+ get indexes() {
83
+ return indexes;
84
+ },
85
+ public() {
86
+ isPublic = true;
87
+ return builder;
88
+ },
89
+ rankIndex(name, options) {
90
+ if (!options.sortBy || options.sortBy.length === 0) {
91
+ throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
92
+ }
93
+ const sortBy = options.sortBy.map((key) => {
94
+ return {
95
+ direction: key.direction ?? "asc",
96
+ field: key.field
97
+ };
98
+ });
99
+ rankIndexes.push({
100
+ name,
101
+ // `on` is filled in by `defineSchema` once the table is keyed —
102
+ // same pattern as `aggregateIndex`.
103
+ on: "",
104
+ partitionBy: options.partitionBy,
105
+ sortBy,
106
+ where: options.where
107
+ });
108
+ return builder;
109
+ },
110
+ get rankIndexes() {
111
+ return rankIndexes;
112
+ },
113
+ get relationMap() {
114
+ return relations;
115
+ },
116
+ relations(build) {
117
+ Object.assign(relations, build(relationBuilder));
118
+ return builder;
119
+ },
120
+ searchIndex(name, options) {
121
+ searchIndexes.push({ field: options.field, filterFields: options.filterFields, name });
122
+ return builder;
123
+ },
124
+ get searchIndexes() {
125
+ return searchIndexes;
126
+ },
127
+ shape,
128
+ shardBy(field) {
129
+ shardMode = { field, kind: "shardBy" };
130
+ return builder;
131
+ },
132
+ get shardMode() {
133
+ return shardMode;
134
+ },
135
+ get triggerMap() {
136
+ return triggers;
137
+ },
138
+ triggers(build) {
139
+ Object.assign(triggers, build(triggerBuilder));
140
+ return builder;
141
+ },
142
+ get vectorIndexes() {
143
+ return vectorIndexes;
144
+ },
145
+ vectorize(field, options) {
146
+ vectorIndexes.push({
147
+ dimensions: options.dimensions,
148
+ embed: options.embed,
149
+ field,
150
+ metadata: options.metadata,
151
+ metric: options.metric,
152
+ name: options.index
153
+ });
154
+ return builder;
155
+ }
156
+ };
157
+ return builder;
158
+ };
159
+ const defineVectorIndex = (options) => {
160
+ return {
161
+ dimensions: options.dimensions,
162
+ embed: options.embed,
163
+ kind: "vectorIndex",
164
+ metadata: options.metadata,
165
+ metric: options.metric,
166
+ select: options.source.select,
167
+ table: options.source.table
168
+ };
169
+ };
170
+ const defineAggregateIndex = (name, options) => {
171
+ const op = options.op ?? "count";
172
+ if (op !== "count" && !options.field) {
173
+ throw new Error(`aggregateIndex "${name}": op "${op}" requires a "field"`);
174
+ }
175
+ return { by: options.by, field: options.field, name, on: options.on, op, where: options.where };
176
+ };
177
+ const defineRankIndex = (name, options) => {
178
+ if (!options.sortBy || options.sortBy.length === 0) {
179
+ throw new Error(`rankIndex "${name}": "sortBy" is required and must list at least one key`);
180
+ }
181
+ const sortBy = options.sortBy.map((key) => {
182
+ return {
183
+ direction: key.direction ?? "asc",
184
+ field: key.field
185
+ };
186
+ });
187
+ return { name, on: options.table, partitionBy: options.partitionBy, sortBy, where: options.where };
188
+ };
189
+ const withExtend = (schema) => {
190
+ return {
191
+ ...schema,
192
+ extend(extension) {
193
+ return withExtend(mergeSchemaExtension(schema, extension));
194
+ },
195
+ rls(mode) {
196
+ return withExtend({ ...schema, rlsMode: mode });
197
+ }
198
+ };
199
+ };
200
+ const fillIndexTableNames = (tables) => {
201
+ for (const [tableName, table] of Object.entries(tables)) {
202
+ for (const index of table.aggregateIndexes) {
203
+ if (index.on === "") {
204
+ index.on = tableName;
205
+ }
206
+ }
207
+ for (const index of table.rankIndexes) {
208
+ if (index.on === "") {
209
+ index.on = tableName;
210
+ }
211
+ }
212
+ }
213
+ };
214
+ const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
215
+ for (const index of Object.values(aggregateIndexes)) {
216
+ const table = tables[index.on];
217
+ if (!table) {
218
+ throw new Error(`defineAggregateIndex "${index.name}": unknown table "${index.on}"`);
219
+ }
220
+ table.aggregateIndexes.push(index);
221
+ }
222
+ for (const index of Object.values(rankIndexes)) {
223
+ const table = tables[index.on];
224
+ if (!table) {
225
+ throw new Error(`defineRankIndex "${index.name}": unknown table "${index.on}"`);
226
+ }
227
+ table.rankIndexes.push(index);
228
+ }
229
+ };
230
+ const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
231
+ fillIndexTableNames(tables);
232
+ attachStandaloneIndexes(tables, aggregateIndexes, rankIndexes);
233
+ return withExtend({ tables, vectorIndexes });
234
+ };
235
+
236
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex };
@@ -0,0 +1,187 @@
1
+ import { optionalInner } from '@lunora/values';
2
+
3
+ const SECRET_VALUE_PREFIXES = [
4
+ /^sk_/u,
5
+ /^pk_/u,
6
+ /^rk_/u,
7
+ /^ghp_/u,
8
+ /^gho_/u,
9
+ /^ghs_/u,
10
+ /^ghr_/u,
11
+ /^github_pat_/u,
12
+ /^xox[baprs]-/u,
13
+ /^AKIA/u,
14
+ /^AIza/u,
15
+ /^Bearer\s/u
16
+ ];
17
+ const HIGH_ENTROPY_TOKEN = /[\w./+-]{24,}/gu;
18
+ const STANDALONE_TOKEN = /^[\w./+-]+$/u;
19
+ const EMBEDDED_PREFIXED_TOKEN_SHORT = /\b(?:(?:sk|pk|rk|ghp|gho|ghs|ghr)_[\w./+-]*|github_pat_[\w./+-]*|xox[baprs]-[\w./+-]*)/gu;
20
+ const EMBEDDED_PREFIXED_TOKEN_LONG = /\b(?:AKIA|AIza)[\w./+-]+|Bearer\s+[\w./+-]+/gu;
21
+ const URL_CREDENTIAL = /\b([a-z][\w.+-]*:\/\/[\w.%+-]+):[\w.%+-]+@/gu;
22
+ const REDACTED = "[redacted]";
23
+ const SECRET_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
24
+ const looksLikeSecretValue = (value) => {
25
+ if (SECRET_VALUE_PREFIXES.some((prefix) => prefix.test(value))) {
26
+ return true;
27
+ }
28
+ const trimmed = value.trim();
29
+ return trimmed.length >= 24 && STANDALONE_TOKEN.test(trimmed);
30
+ };
31
+ const QUOTED_VALUE = /(["'])(?<inner>(?:\\.|(?!\1).)*)\1/gu;
32
+ const KEYED_VALUE = /\b(?<key>[A-Za-z_]\w*)\s*[=:]\s*\S+/gu;
33
+ const redactSecrets = (message) => {
34
+ let out = message;
35
+ out = out.replaceAll(QUOTED_VALUE, (match, ...groups) => {
36
+ const named = groups.at(-1);
37
+ return named?.inner !== void 0 && looksLikeSecretValue(named.inner) ? REDACTED : match;
38
+ });
39
+ out = out.replaceAll(URL_CREDENTIAL, (_match, prefix) => `${prefix}:${REDACTED}@`);
40
+ out = out.replaceAll(EMBEDDED_PREFIXED_TOKEN_SHORT, REDACTED);
41
+ out = out.replaceAll(EMBEDDED_PREFIXED_TOKEN_LONG, REDACTED);
42
+ out = out.replaceAll(KEYED_VALUE, (match, ...groups) => {
43
+ const named = groups.at(-1);
44
+ return named?.key !== void 0 && SECRET_KEY.test(named.key) ? `${named.key}=${REDACTED}` : match;
45
+ });
46
+ out = out.replaceAll(HIGH_ENTROPY_TOKEN, REDACTED);
47
+ return out;
48
+ };
49
+ const redactValueForKey = (message, key, raw) => {
50
+ const masked = redactSecrets(message);
51
+ if (typeof raw === "string" && raw !== "" && SECRET_KEY.test(key)) {
52
+ return masked.replaceAll(raw, REDACTED);
53
+ }
54
+ return masked;
55
+ };
56
+ class LunoraEnvError extends Error {
57
+ name = "LunoraEnvError";
58
+ failures;
59
+ constructor(failures) {
60
+ const summary = failures.map((failure) => ` - ${failure.key}: ${failure.message}`).join("\n");
61
+ super(`Invalid environment (${String(failures.length)} key(s)):
62
+ ${summary}`);
63
+ this.failures = failures;
64
+ }
65
+ }
66
+ const TRUE_VALUES = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
67
+ const FALSE_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
68
+ const INTEGER_LITERAL = /^-?\d+$/u;
69
+ const unwrapKind = (validator) => {
70
+ if (validator.kind !== "optional") {
71
+ return validator.kind;
72
+ }
73
+ const inner = optionalInner(validator);
74
+ return inner ? unwrapKind(inner) : validator.kind;
75
+ };
76
+ const coerce = (validator, raw) => {
77
+ if (typeof raw !== "string") {
78
+ return raw;
79
+ }
80
+ switch (unwrapKind(validator)) {
81
+ case "bigint": {
82
+ return INTEGER_LITERAL.test(raw.trim()) ? BigInt(raw.trim()) : raw;
83
+ }
84
+ case "boolean": {
85
+ const lowered = raw.trim().toLowerCase();
86
+ if (TRUE_VALUES.has(lowered)) {
87
+ return true;
88
+ }
89
+ if (FALSE_VALUES.has(lowered)) {
90
+ return false;
91
+ }
92
+ return raw;
93
+ }
94
+ case "number": {
95
+ const trimmed = raw.trim();
96
+ if (trimmed === "") {
97
+ return raw;
98
+ }
99
+ const parsed = Number(trimmed);
100
+ return Number.isNaN(parsed) ? raw : parsed;
101
+ }
102
+ default: {
103
+ return raw;
104
+ }
105
+ }
106
+ };
107
+ const validateKey = (key, validator, env, failures) => {
108
+ const raw = env[key];
109
+ if (raw === void 0 && validator.kind === "optional") {
110
+ return { ok: true, value: void 0 };
111
+ }
112
+ const result = validator.safeParse(coerce(validator, raw));
113
+ if (result.ok) {
114
+ return { ok: true, value: result.value };
115
+ }
116
+ failures.push({ key, message: redactValueForKey(result.error.message, key, raw) });
117
+ return { ok: false };
118
+ };
119
+ const requireEnvObject = (env) => {
120
+ if (typeof env !== "object" || env === null) {
121
+ throw new LunoraEnvError([{ key: "<env>", message: `expected an object, received ${env === null ? "null" : typeof env}` }]);
122
+ }
123
+ return env;
124
+ };
125
+ const defineEnv = (shape) => {
126
+ const keys = Object.keys(shape);
127
+ const cache = /* @__PURE__ */ new WeakMap();
128
+ const accessor = ((env) => {
129
+ const source = requireEnvObject(env);
130
+ let resolved = cache.get(source);
131
+ if (resolved === void 0) {
132
+ resolved = /* @__PURE__ */ new Map();
133
+ cache.set(source, resolved);
134
+ }
135
+ const cached = resolved;
136
+ const read = (property) => {
137
+ if (cached.has(property)) {
138
+ return cached.get(property);
139
+ }
140
+ const failures = [];
141
+ const outcome = validateKey(property, shape[property], source, failures);
142
+ if (!outcome.ok) {
143
+ throw new LunoraEnvError(failures);
144
+ }
145
+ cached.set(property, outcome.value);
146
+ return outcome.value;
147
+ };
148
+ return /* @__PURE__ */ new Proxy({}, {
149
+ get(_target, property) {
150
+ if (typeof property !== "string" || !(property in shape)) {
151
+ return void 0;
152
+ }
153
+ return read(property);
154
+ },
155
+ getOwnPropertyDescriptor(_target, property) {
156
+ if (typeof property === "string" && property in shape) {
157
+ return { configurable: true, enumerable: true, value: read(property), writable: false };
158
+ }
159
+ return void 0;
160
+ },
161
+ has(_target, property) {
162
+ return typeof property === "string" && property in shape;
163
+ },
164
+ ownKeys() {
165
+ return keys;
166
+ }
167
+ });
168
+ });
169
+ accessor.parse = (env) => {
170
+ const source = requireEnvObject(env);
171
+ const failures = [];
172
+ const out = {};
173
+ for (const key of keys) {
174
+ const outcome = validateKey(key, shape[key], source, failures);
175
+ if (outcome.ok && outcome.value !== void 0) {
176
+ out[key] = outcome.value;
177
+ }
178
+ }
179
+ if (failures.length > 0) {
180
+ throw new LunoraEnvError(failures);
181
+ }
182
+ return out;
183
+ };
184
+ return accessor;
185
+ };
186
+
187
+ export { LunoraEnvError, defineEnv, redactSecrets };
@@ -0,0 +1,8 @@
1
+ const defineMigration = (definition) => {
2
+ if (definition.id.trim() === "") {
3
+ throw new Error("defineMigration: `id` must be a non-empty string");
4
+ }
5
+ return { __lunoraMigration: true, ...definition };
6
+ };
7
+
8
+ export { defineMigration };
@@ -0,0 +1,29 @@
1
+ const definePolicy = (input) => {
2
+ return { on: input.on, table: input.table, when: input.when };
3
+ };
4
+ const createPolicyDsl = () => (input) => {
5
+ return { on: input.on, table: input.table, when: input.when };
6
+ };
7
+ const definePermission = (name, options = {}) => {
8
+ return { name, ...options };
9
+ };
10
+ const definePolicies = (policies) => {
11
+ const seenWhenByKey = /* @__PURE__ */ new Map();
12
+ for (const policy of policies) {
13
+ const key = JSON.stringify([policy.table, policy.on]);
14
+ const whens = seenWhenByKey.get(key) ?? /* @__PURE__ */ new Set();
15
+ if (whens.has(policy.when)) {
16
+ throw new Error(
17
+ `definePolicies: duplicate policy for (table "${policy.table}", on "${policy.on}") — the same decision function is registered more than once. Multiple distinct policies per (table, on) are allowed (reads OR, writes AND); remove the duplicate.`
18
+ );
19
+ }
20
+ whens.add(policy.when);
21
+ seenWhenByKey.set(key, whens);
22
+ }
23
+ return policies;
24
+ };
25
+ const defineRole = (name, options = {}) => {
26
+ return { name, ...options };
27
+ };
28
+
29
+ export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole };