@lunora/server 1.0.0-alpha.1 → 1.0.0-alpha.11
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/README.md +5 -1
- package/__assets__/package-og.svg +1 -1
- package/dist/data-model.d.mts +104 -16
- package/dist/data-model.d.ts +104 -16
- package/dist/index.d.mts +342 -28
- package/dist/index.d.ts +342 -28
- package/dist/index.mjs +17 -12
- package/dist/packem_shared/{LunoraError-DhggBJZF.mjs → LunoraError-DN7Zhhvu.mjs} +4 -1
- package/dist/packem_shared/{definePresence-D5LtwGl0.mjs → PRESENCE_DEFAULT_TTL_MS-D8viLY1S.mjs} +4 -4
- package/dist/packem_shared/{bindTableFacade-DCuyr46L.mjs → bindOrm-Ce57S3N9.mjs} +58 -1
- package/dist/packem_shared/buildRlsReadRegistry-1jexWrb3.mjs +107 -0
- package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +55 -0
- package/dist/packem_shared/{defineAggregateIndex-DzqxtAyV.mjs → defineAggregateIndex-ZdyU78gh.mjs} +58 -3
- package/dist/packem_shared/defineIdentity-B_9YD46A.mjs +34 -0
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
- package/dist/packem_shared/defineShape-CJ27Wx7o.mjs +17 -0
- package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
- package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-FLwfsePg.mjs} +1 -1
- package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-lxwHTEV3.mjs} +17 -3
- package/dist/packem_shared/{mask-CkZJHHMM.mjs → mask-BV_jNzsN.mjs} +2 -2
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
- package/dist/packem_shared/{rls-Zhf5wEeJ.mjs → rls-2Jhd0uev.mjs} +22 -4
- package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-Cje6Woea.mjs} +1 -1
- package/dist/packem_shared/{types.d-DmvyEMD6.d.mts → types.d-BB3pjV0m.d.mts} +10 -4
- package/dist/packem_shared/{types.d-BDY0FYHK.d.ts → types.d-Cxl6ndhm.d.ts} +10 -4
- package/dist/rls/testing.d.mts +1 -1
- package/dist/rls/testing.d.ts +1 -1
- package/dist/rls/testing.mjs +1 -1
- package/dist/types.d.mts +130 -2
- package/dist/types.d.ts +130 -2
- package/package.json +5 -5
- /package/dist/packem_shared/{defineEnv-DjFkpkSP.mjs → LunoraEnvError-DjFkpkSP.mjs} +0 -0
- /package/dist/packem_shared/{defineSchemaExtension-Ck5_TUO8.mjs → composePluginMiddleware-Ck5_TUO8.mjs} +0 -0
- /package/dist/packem_shared/{definePolicy-De67zPDS.mjs → createPolicyDsl-De67zPDS.mjs} +0 -0
|
@@ -1,4 +1,42 @@
|
|
|
1
|
+
const isUniqueConflict = (error) => typeof error === "object" && error !== null && error.code === "CONFLICT" && error.kind === "unique";
|
|
2
|
+
const buildUpsertWhere = (tableName, target, create) => {
|
|
3
|
+
const fields = typeof target === "string" ? [target] : target;
|
|
4
|
+
if (fields.length === 0) {
|
|
5
|
+
throw new Error(`ctx.db.${tableName}.upsert: "target" must name at least one field`);
|
|
6
|
+
}
|
|
7
|
+
const where = {};
|
|
8
|
+
for (const field of fields) {
|
|
9
|
+
if (!(field in create)) {
|
|
10
|
+
throw new Error(`ctx.db.${tableName}.upsert: target field "${field}" is missing from the create document`);
|
|
11
|
+
}
|
|
12
|
+
where[field] = create[field];
|
|
13
|
+
}
|
|
14
|
+
return where;
|
|
15
|
+
};
|
|
1
16
|
const bindTableFacade = (writer, tableName) => {
|
|
17
|
+
const insert = async (document, options) => {
|
|
18
|
+
if (options?.skipDuplicates !== true) {
|
|
19
|
+
return writer.insert(tableName, document);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return await writer.insert(tableName, document);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isUniqueConflict(error)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const upsert = async ({ create, target, update }) => {
|
|
31
|
+
const where = buildUpsertWhere(tableName, target, create);
|
|
32
|
+
const existing = await writer.findFirst(tableName, { where });
|
|
33
|
+
if (existing && typeof existing["_id"] === "string") {
|
|
34
|
+
await writer.patch(existing["_id"], update ?? create, tableName);
|
|
35
|
+
return { created: false, id: existing["_id"] };
|
|
36
|
+
}
|
|
37
|
+
const id = await writer.insert(tableName, create);
|
|
38
|
+
return { created: true, id };
|
|
39
|
+
};
|
|
2
40
|
return {
|
|
3
41
|
aggregate: (options) => writer.aggregate(tableName, options),
|
|
4
42
|
count: (where) => writer.count(tableName, where),
|
|
@@ -14,12 +52,17 @@ const bindTableFacade = (writer, tableName) => {
|
|
|
14
52
|
}
|
|
15
53
|
return writer.deleteMany(ids, options, tableName);
|
|
16
54
|
},
|
|
55
|
+
// `exists` reuses `findFirst` (RLS-filtered, indexed when a `.withIndex`-able
|
|
56
|
+
// `where` is supplied) and only asks whether a row came back — no count scan.
|
|
57
|
+
exists: async (where) => await writer.findFirst(tableName, where === void 0 ? void 0 : { where }) !== null,
|
|
17
58
|
findFirst: (args) => writer.findFirst(tableName, args),
|
|
18
59
|
findFirstOrThrow: (args) => writer.findFirstOrThrow(tableName, args),
|
|
19
60
|
findMany: (args) => writer.findMany(tableName, args),
|
|
20
61
|
get: (id) => writer.get(id, tableName),
|
|
21
62
|
groupBy: (options) => writer.groupBy(tableName, options),
|
|
22
|
-
|
|
63
|
+
// Physical removal — bypasses `.softDelete()`. RLS gates it as a delete.
|
|
64
|
+
hardDelete: (id) => writer.delete(id, tableName, { hard: true }),
|
|
65
|
+
insert,
|
|
23
66
|
insertMany: (documents, options) => {
|
|
24
67
|
if (writer.insertMany === void 0) {
|
|
25
68
|
throw new Error(`ctx.db.${tableName}.insertMany is unavailable: this writer has no batch insert`);
|
|
@@ -42,6 +85,20 @@ const bindTableFacade = (writer, tableName) => {
|
|
|
42
85
|
rank: (indexName, options) => writer.rank(tableName, indexName, options),
|
|
43
86
|
rankPage: (indexName, options) => writer.rankPage(tableName, indexName, options),
|
|
44
87
|
replace: (id, document) => writer.replace(id, document, tableName),
|
|
88
|
+
restore: async (id) => {
|
|
89
|
+
if (!writer.restore) {
|
|
90
|
+
throw new Error(`ctx.db.${tableName}.restore is unavailable: this writer has no restore (is the table .softDelete()?)`);
|
|
91
|
+
}
|
|
92
|
+
await writer.restore(id, tableName);
|
|
93
|
+
},
|
|
94
|
+
upsert,
|
|
95
|
+
upsertMany: async ({ rows, target }) => {
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
results.push(await upsert({ create: row.create, target, update: row.update }));
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
},
|
|
45
102
|
withSearchIndex: (indexName, search) => writer.query(tableName).withSearchIndex(indexName, search)
|
|
46
103
|
};
|
|
47
104
|
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-2Jhd0uev.mjs';
|
|
2
|
+
import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
|
|
3
|
+
|
|
4
|
+
const FALSE_PREDICATE = { OR: [] };
|
|
5
|
+
const readEntryTags = (entry) => {
|
|
6
|
+
const hoisted = entry?.rls?.tags;
|
|
7
|
+
if (hoisted) {
|
|
8
|
+
return hoisted;
|
|
9
|
+
}
|
|
10
|
+
const tag = readRlsTag(entry);
|
|
11
|
+
return tag ? [tag] : [];
|
|
12
|
+
};
|
|
13
|
+
const addTagToRegistry = (byTable, tag) => {
|
|
14
|
+
const rolePermissions = indexRolePermissions(tag.roles);
|
|
15
|
+
const seenWhenByTable = /* @__PURE__ */ new Map();
|
|
16
|
+
const policiesByTable = /* @__PURE__ */ new Map();
|
|
17
|
+
for (const policy of tag.policies) {
|
|
18
|
+
if (policy.on !== "read") {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const seen = seenWhenByTable.get(policy.table) ?? /* @__PURE__ */ new Set();
|
|
22
|
+
if (seen.has(policy.when)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
seen.add(policy.when);
|
|
26
|
+
seenWhenByTable.set(policy.table, seen);
|
|
27
|
+
const list = policiesByTable.get(policy.table) ?? [];
|
|
28
|
+
list.push(policy);
|
|
29
|
+
policiesByTable.set(policy.table, list);
|
|
30
|
+
}
|
|
31
|
+
for (const [table, policies] of policiesByTable) {
|
|
32
|
+
const groups = byTable.get(table) ?? [];
|
|
33
|
+
groups.push({ policies, rolePermissions });
|
|
34
|
+
byTable.set(table, groups);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const isFalsePredicate = (where) => {
|
|
38
|
+
const or = where.OR;
|
|
39
|
+
return Array.isArray(or) && or.length === 0 && Object.keys(where).length === 1;
|
|
40
|
+
};
|
|
41
|
+
const andMerge = (injected, caller) => {
|
|
42
|
+
if (!injected || Object.keys(injected).length === 0) {
|
|
43
|
+
return caller;
|
|
44
|
+
}
|
|
45
|
+
if (isFalsePredicate(injected)) {
|
|
46
|
+
return injected;
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(caller).length === 0) {
|
|
49
|
+
return injected;
|
|
50
|
+
}
|
|
51
|
+
return { AND: [injected, caller] };
|
|
52
|
+
};
|
|
53
|
+
const evaluateGroupBaseWhere = (group, request) => {
|
|
54
|
+
const granted = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const roleName of request.roles) {
|
|
56
|
+
for (const name of group.rolePermissions.get(roleName) ?? []) {
|
|
57
|
+
granted.add(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return computeReadBaseWhere(group.policies, {
|
|
61
|
+
auth: {
|
|
62
|
+
can: (permission) => granted.has(permissionName(permission)),
|
|
63
|
+
identity: request.identity,
|
|
64
|
+
roles: request.roles,
|
|
65
|
+
userId: request.userId
|
|
66
|
+
},
|
|
67
|
+
ctx: request.ctx
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const resolveReadBaseWhere = (registry, request) => {
|
|
71
|
+
const groups = registry.byTable.get(request.table);
|
|
72
|
+
if (!groups || groups.length === 0) {
|
|
73
|
+
return request.rlsRequired && !request.tablePublic ? FALSE_PREDICATE : void 0;
|
|
74
|
+
}
|
|
75
|
+
const predicates = [];
|
|
76
|
+
for (const group of groups) {
|
|
77
|
+
const base = evaluateGroupBaseWhere(group, request);
|
|
78
|
+
if (base === void 0) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
if (isFalsePredicate(base)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
predicates.push(base);
|
|
85
|
+
}
|
|
86
|
+
if (predicates.length === 0) {
|
|
87
|
+
return FALSE_PREDICATE;
|
|
88
|
+
}
|
|
89
|
+
return predicates.length === 1 ? predicates[0] : { OR: predicates };
|
|
90
|
+
};
|
|
91
|
+
const buildRlsReadRegistry = (functions) => {
|
|
92
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
93
|
+
const seenTags = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const entry of functions) {
|
|
95
|
+
for (const tag of readEntryTags(entry)) {
|
|
96
|
+
if (seenTags.has(tag)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
seenTags.add(tag);
|
|
100
|
+
addTagToRegistry(byTable, tag);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { byTable };
|
|
104
|
+
};
|
|
105
|
+
const composeShapeReadWhere = (registry, request) => andMerge(resolveReadBaseWhere(registry, request), request.shapeWhere);
|
|
106
|
+
|
|
107
|
+
export { buildRlsReadRegistry, composeShapeReadWhere };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const NON_SECRET_BINDING_METHODS = [
|
|
2
|
+
"put",
|
|
3
|
+
// KV / R2
|
|
4
|
+
"list",
|
|
5
|
+
// KV / R2
|
|
6
|
+
"delete",
|
|
7
|
+
// KV / R2
|
|
8
|
+
"getWithMetadata",
|
|
9
|
+
// KV
|
|
10
|
+
"head",
|
|
11
|
+
// R2
|
|
12
|
+
"createMultipartUpload",
|
|
13
|
+
// R2
|
|
14
|
+
"idFromName",
|
|
15
|
+
// Durable Object namespace
|
|
16
|
+
"newUniqueId",
|
|
17
|
+
// Durable Object namespace
|
|
18
|
+
"getByName",
|
|
19
|
+
// Durable Object namespace
|
|
20
|
+
"send",
|
|
21
|
+
// Queue producer
|
|
22
|
+
"sendBatch",
|
|
23
|
+
// Queue producer
|
|
24
|
+
"writeDataPoint",
|
|
25
|
+
// Analytics Engine dataset
|
|
26
|
+
"create",
|
|
27
|
+
// Workflows binding (get(id) + create/createBatch)
|
|
28
|
+
"createBatch"
|
|
29
|
+
// Workflows binding
|
|
30
|
+
];
|
|
31
|
+
const isSecretBinding = (value) => {
|
|
32
|
+
if (typeof value !== "object" || value === null) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const record = value;
|
|
36
|
+
if (typeof record.get !== "function") {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return !NON_SECRET_BINDING_METHODS.some((method) => typeof record[method] === "function");
|
|
40
|
+
};
|
|
41
|
+
const createSecrets = (env) => {
|
|
42
|
+
return {
|
|
43
|
+
get: async (name) => {
|
|
44
|
+
const binding = env[name];
|
|
45
|
+
if (!isSecretBinding(binding)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`ctx.secrets: no Secrets Store binding named "${name}". Add a \`secrets_store_secrets[]\` entry (binding "${name}", pointing at your store + secret) to wrangler.jsonc — \`ctx.secrets\` reads a Secrets Store binding, not a plain \`.dev.vars\` value.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return binding.get();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { createSecrets };
|
package/dist/packem_shared/{defineAggregateIndex-DzqxtAyV.mjs → defineAggregateIndex-ZdyU78gh.mjs}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isOrWrapsFromValidator } from '@lunora/values';
|
|
2
|
-
import { mergeSchemaExtension } from './
|
|
1
|
+
import { isOrWrapsFromValidator, v } from '@lunora/values';
|
|
2
|
+
import { mergeSchemaExtension } from './composePluginMiddleware-Ck5_TUO8.mjs';
|
|
3
3
|
|
|
4
4
|
const relationBuilder = {
|
|
5
5
|
many: (table, options) => {
|
|
@@ -22,7 +22,8 @@ const createTriggerBuilder = () => {
|
|
|
22
22
|
beforeUpdate: (handler) => makeTrigger("before", "update", handler)
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
|
-
const defineTable = (
|
|
25
|
+
const defineTable = (inputShape) => {
|
|
26
|
+
const shape = { ...inputShape };
|
|
26
27
|
for (const [columnName, validator] of Object.entries(shape)) {
|
|
27
28
|
if (isOrWrapsFromValidator(validator)) {
|
|
28
29
|
throw new Error(`defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
|
|
@@ -39,6 +40,8 @@ const defineTable = (shape) => {
|
|
|
39
40
|
let shardMode = { kind: "root" };
|
|
40
41
|
let isExternallyManaged = false;
|
|
41
42
|
let isPublic = false;
|
|
43
|
+
let softDelete;
|
|
44
|
+
let externalSource;
|
|
42
45
|
const builder = {
|
|
43
46
|
aggregateIndex(name, options) {
|
|
44
47
|
const op = options?.op ?? "count";
|
|
@@ -61,6 +64,9 @@ const defineTable = (shape) => {
|
|
|
61
64
|
get aggregateIndexes() {
|
|
62
65
|
return aggregateIndexes;
|
|
63
66
|
},
|
|
67
|
+
get externalSource() {
|
|
68
|
+
return externalSource;
|
|
69
|
+
},
|
|
64
70
|
externallyManaged() {
|
|
65
71
|
isExternallyManaged = true;
|
|
66
72
|
return builder;
|
|
@@ -132,6 +138,28 @@ const defineTable = (shape) => {
|
|
|
132
138
|
get shardMode() {
|
|
133
139
|
return shardMode;
|
|
134
140
|
},
|
|
141
|
+
get softDeleteMode() {
|
|
142
|
+
return softDelete;
|
|
143
|
+
},
|
|
144
|
+
source(definition) {
|
|
145
|
+
if (!definition.binding) {
|
|
146
|
+
throw new Error("source: `binding` is required (the wrangler Hyperdrive binding name)");
|
|
147
|
+
}
|
|
148
|
+
if (!definition.query) {
|
|
149
|
+
throw new Error("source: `query` is required (the tenant-membership SQL)");
|
|
150
|
+
}
|
|
151
|
+
externalSource = definition;
|
|
152
|
+
isExternallyManaged = true;
|
|
153
|
+
return builder;
|
|
154
|
+
},
|
|
155
|
+
softDelete(options) {
|
|
156
|
+
const field = options?.field ?? "deletedAt";
|
|
157
|
+
softDelete = { field };
|
|
158
|
+
if (!(field in shape)) {
|
|
159
|
+
shape[field] = v.optional(v.number().nullable());
|
|
160
|
+
}
|
|
161
|
+
return builder;
|
|
162
|
+
},
|
|
135
163
|
get triggerMap() {
|
|
136
164
|
return triggers;
|
|
137
165
|
},
|
|
@@ -192,6 +220,9 @@ const withExtend = (schema) => {
|
|
|
192
220
|
extend(extension) {
|
|
193
221
|
return withExtend(mergeSchemaExtension(schema, extension));
|
|
194
222
|
},
|
|
223
|
+
jurisdiction(_jurisdiction) {
|
|
224
|
+
return withExtend(schema);
|
|
225
|
+
},
|
|
195
226
|
rls(mode) {
|
|
196
227
|
return withExtend({ ...schema, rlsMode: mode });
|
|
197
228
|
}
|
|
@@ -227,9 +258,33 @@ const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
|
|
|
227
258
|
table.rankIndexes.push(index);
|
|
228
259
|
}
|
|
229
260
|
};
|
|
261
|
+
const validateExternalSources = (tables) => {
|
|
262
|
+
for (const [name, table] of Object.entries(tables)) {
|
|
263
|
+
const source = table.externalSource;
|
|
264
|
+
if (!source) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (table.shardMode.kind === "global") {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`defineSchema: table "${name}" cannot be both .source() and .global() — a sourced table materializes into a shard DO's SQLite, a global table lives in the external tier`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (table.shardMode.kind === "shardBy" && !source.tenantBy) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`defineSchema: sourced + .shardBy() table "${name}" needs a \`tenantBy\` mapper — without it every tenant's DO would run the same unscoped query and replicate the whole multitenant table (a cross-tenant leak). Add \`tenantBy: (shardKey) => [shardKey]\` binding the shard key into the query's parameters.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (source.mode === "incremental") {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`defineSchema: table "${name}" uses \`mode: "incremental"\`, which is not yet implemented — only "full-pull" (the default) is supported. Remove \`mode\` or set it to "full-pull".`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
230
284
|
const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
|
|
231
285
|
fillIndexTableNames(tables);
|
|
232
286
|
attachStandaloneIndexes(tables, aggregateIndexes, rankIndexes);
|
|
287
|
+
validateExternalSources(tables);
|
|
233
288
|
return withExtend({ tables, vectorIndexes });
|
|
234
289
|
};
|
|
235
290
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parseValidatorMap, ValidationError } from '@lunora/values';
|
|
2
|
+
|
|
3
|
+
const NON_STRING_USER_ID_KINDS = /* @__PURE__ */ new Set([
|
|
4
|
+
"array",
|
|
5
|
+
"bigint",
|
|
6
|
+
"boolean",
|
|
7
|
+
"bytes",
|
|
8
|
+
"date",
|
|
9
|
+
"null",
|
|
10
|
+
"number",
|
|
11
|
+
"object",
|
|
12
|
+
"optional",
|
|
13
|
+
"record",
|
|
14
|
+
"timestamp"
|
|
15
|
+
]);
|
|
16
|
+
const defineIdentity = (claims, options = {}) => {
|
|
17
|
+
const map = claims;
|
|
18
|
+
const userIdValidator = map["userId"];
|
|
19
|
+
if (userIdValidator === void 0 || NON_STRING_USER_ID_KINDS.has(userIdValidator.kind)) {
|
|
20
|
+
throw new Error("defineIdentity: the claim map must declare a required string `userId` validator (e.g. `v.string()` or `v.id(...)`)");
|
|
21
|
+
}
|
|
22
|
+
const onInvalid = options.onInvalid ?? "anonymous";
|
|
23
|
+
const validate = (identity) => {
|
|
24
|
+
try {
|
|
25
|
+
parseValidatorMap(map, identity, "identity");
|
|
26
|
+
return { ok: true };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return { error: error instanceof ValidationError ? error.message : String(error), ok: false };
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return { __lunoraIdentity: true, claims: map, onInvalid, validate };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { defineIdentity };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { v as validateArgs } from './functions-Di9FUNkf.mjs';
|
|
2
|
+
|
|
3
|
+
const defineMutator = (definition) => {
|
|
4
|
+
const handler = async (context, rawArgs) => {
|
|
5
|
+
const parsed = validateArgs(definition.args ?? {}, rawArgs);
|
|
6
|
+
return definition.server(context, parsed);
|
|
7
|
+
};
|
|
8
|
+
return { __lunoraMutator: true, ...definition, handler, kind: "mutation" };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { defineMutator };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { v as validateArgs } from './functions-Di9FUNkf.mjs';
|
|
2
|
+
|
|
3
|
+
const defineShape = (definition) => {
|
|
4
|
+
if (definition.table.trim() === "") {
|
|
5
|
+
throw new Error("defineShape: `table` must be a non-empty string");
|
|
6
|
+
}
|
|
7
|
+
if (definition.columns?.length === 0) {
|
|
8
|
+
throw new Error("defineShape: `columns` must list at least one column when provided");
|
|
9
|
+
}
|
|
10
|
+
const compileWhere = (context, rawArgs) => {
|
|
11
|
+
const parsed = validateArgs(definition.args ?? {}, rawArgs);
|
|
12
|
+
return definition.where(context, parsed);
|
|
13
|
+
};
|
|
14
|
+
return { __lunoraShape: true, ...definition, compileWhere };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { defineShape };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseValidatorMap, ValidationError } from '@lunora/values';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import { LunoraError } from './LunoraError-
|
|
3
|
+
import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
|
|
4
4
|
|
|
5
5
|
const httpAction = (handler) => async (c) => handler(c.get("lunora"), c.req.raw);
|
|
6
6
|
const httpRouter = () => {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v as validateArgs } from './functions-Di9FUNkf.mjs';
|
|
2
|
+
import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
|
|
2
3
|
import { r as runMiddlewareChain } from './run-middleware-CYQOuoV6.mjs';
|
|
3
4
|
|
|
4
|
-
const validateArgs = (validators, args) => parseValidatorMap(validators, args, "args");
|
|
5
|
-
|
|
6
5
|
const runMiddleware = (middlewares, baseContext) => runMiddlewareChain(middlewares, baseContext, (context) => context);
|
|
7
6
|
const makeHandler = (args, middlewares, userHandler, output) => async (context, rawArgs) => {
|
|
8
7
|
const parsed = validateArgs(args, rawArgs);
|
|
@@ -35,16 +34,29 @@ const makeStreamHandler = (args, middlewares, userHandler) => (context, rawArgs,
|
|
|
35
34
|
}
|
|
36
35
|
})();
|
|
37
36
|
};
|
|
37
|
+
const collectRls = (middlewares) => {
|
|
38
|
+
const tags = [];
|
|
39
|
+
for (const middleware of middlewares) {
|
|
40
|
+
const tag = readRlsTag(middleware);
|
|
41
|
+
if (!tag) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
tags.push(tag);
|
|
45
|
+
}
|
|
46
|
+
return tags.length > 0 ? { tags } : void 0;
|
|
47
|
+
};
|
|
38
48
|
const makeBuilder = (kind, state, visibility) => {
|
|
39
49
|
return {
|
|
40
50
|
__lunoraProcedure: kind,
|
|
41
51
|
...visibility ? { __lunoraVisibility: visibility } : {},
|
|
42
52
|
input: (validators) => makeBuilder(kind, { ...state, args: { ...state.args, ...validators } }, visibility),
|
|
43
53
|
[kind]: (userHandler) => {
|
|
54
|
+
const rls = collectRls(state.middlewares);
|
|
44
55
|
return {
|
|
45
56
|
args: state.args,
|
|
46
57
|
handler: makeHandler(state.args, state.middlewares, userHandler, state.output),
|
|
47
58
|
kind,
|
|
59
|
+
...rls ? { rls } : {},
|
|
48
60
|
...visibility ? { visibility } : {}
|
|
49
61
|
};
|
|
50
62
|
},
|
|
@@ -55,10 +67,12 @@ const makeBuilder = (kind, state, visibility) => {
|
|
|
55
67
|
// unconditionally keeps the runtime free of per-kind branching.
|
|
56
68
|
...kind === "query" ? {
|
|
57
69
|
stream: (userHandler) => {
|
|
70
|
+
const rls = collectRls(state.middlewares);
|
|
58
71
|
return {
|
|
59
72
|
args: state.args,
|
|
60
73
|
handler: makeStreamHandler(state.args, state.middlewares, userHandler),
|
|
61
74
|
kind: "stream",
|
|
75
|
+
...rls ? { rls } : {},
|
|
62
76
|
...visibility ? { visibility } : {}
|
|
63
77
|
};
|
|
64
78
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { LunoraError } from './LunoraError-
|
|
2
|
-
import { bindTableFacade, bindOrm } from './
|
|
1
|
+
import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
|
|
2
|
+
import { bindTableFacade, bindOrm } from './bindOrm-Ce57S3N9.mjs';
|
|
3
3
|
|
|
4
4
|
const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
|
|
5
5
|
const indexRolePermissions = (roles) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const RLS_TAG = /* @__PURE__ */ Symbol.for("lunora.rls.middleware-policies");
|
|
2
|
+
const tagRlsMiddleware = (middleware, tag) => {
|
|
3
|
+
Object.defineProperty(middleware, RLS_TAG, { configurable: true, enumerable: false, value: tag });
|
|
4
|
+
return middleware;
|
|
5
|
+
};
|
|
6
|
+
const readRlsTag = (middleware) => {
|
|
7
|
+
if (middleware === null || typeof middleware !== "function" && typeof middleware !== "object") {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
return middleware[RLS_TAG];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { readRlsTag as r, tagRlsMiddleware as t };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { LunoraError } from './LunoraError-
|
|
2
|
-
import { bindTableFacade, bindOrm } from './
|
|
1
|
+
import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
|
|
2
|
+
import { bindTableFacade, bindOrm } from './bindOrm-Ce57S3N9.mjs';
|
|
3
|
+
import { t as tagRlsMiddleware } from './policy-tag-DvpVH2tv.mjs';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_BATCH_LIMIT = 500;
|
|
5
6
|
const assertBatchLimit = (count, limit, op) => {
|
|
@@ -324,7 +325,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
|
|
|
324
325
|
restrictsCounts: (args.restrictsCounts ?? false) || restricts
|
|
325
326
|
});
|
|
326
327
|
},
|
|
327
|
-
delete: (id, expectedTable) => gateById(id, "delete", (writer) => writer.delete(id, expectedTable), void 0, expectedTable),
|
|
328
|
+
delete: (id, expectedTable, options) => gateById(id, "delete", (writer) => writer.delete(id, expectedTable, options), void 0, expectedTable),
|
|
328
329
|
async deleteMany(ids, options, expectedTable) {
|
|
329
330
|
assertBatchLimit(ids.length, options?.limit, "deleteMany");
|
|
330
331
|
for (const id of ids) {
|
|
@@ -441,6 +442,22 @@ const wrapDatabase = (base, raw, perTable, context) => {
|
|
|
441
442
|
}
|
|
442
443
|
return reader.filter((document) => matchesWhere(document, baseWhere));
|
|
443
444
|
},
|
|
445
|
+
// Restore clears the soft-delete marker — a by-id un-delete, gated as an
|
|
446
|
+
// "update" (the policy that governs patch). No post-image check: the
|
|
447
|
+
// writer owns the marker field, so we only enforce the pre-image USING.
|
|
448
|
+
restore: (id, expectedTable) => gateById(
|
|
449
|
+
id,
|
|
450
|
+
"update",
|
|
451
|
+
async (writer) => {
|
|
452
|
+
const perform = writer.restore ?? raw.restore;
|
|
453
|
+
if (!perform) {
|
|
454
|
+
throw new LunoraError("BAD_REQUEST", "restore is not supported by this writer");
|
|
455
|
+
}
|
|
456
|
+
await perform(id, expectedTable);
|
|
457
|
+
},
|
|
458
|
+
void 0,
|
|
459
|
+
expectedTable
|
|
460
|
+
),
|
|
444
461
|
replace: (id, document, expectedTable) => gateById(
|
|
445
462
|
id,
|
|
446
463
|
"update",
|
|
@@ -516,7 +533,7 @@ const indexRolePermissions = (roles) => {
|
|
|
516
533
|
const rls = (policies, options = {}) => {
|
|
517
534
|
const perTable = indexByTable(policies);
|
|
518
535
|
const rolePermissions = indexRolePermissions(options.roles);
|
|
519
|
-
|
|
536
|
+
const middleware = async ({ ctx, next }) => {
|
|
520
537
|
const auth = ctx.auth ?? {};
|
|
521
538
|
const identity = await auth.getIdentity?.() ?? null;
|
|
522
539
|
const roles = auth.roles ?? [];
|
|
@@ -546,6 +563,7 @@ const rls = (policies, options = {}) => {
|
|
|
546
563
|
}
|
|
547
564
|
return next({ ctx: extension });
|
|
548
565
|
};
|
|
566
|
+
return tagRlsMiddleware(middleware, { policies, roles: options.roles ?? [] });
|
|
549
567
|
};
|
|
550
568
|
|
|
551
569
|
export { computeReadBaseWhere, evaluateWrite, indexRolePermissions, matchesWhere, permissionName, rls };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LunoraError } from './LunoraError-
|
|
1
|
+
import { LunoraError } from './LunoraError-DN7Zhhvu.mjs';
|
|
2
2
|
|
|
3
3
|
const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
|
|
4
4
|
const indexRolePermissions = (roles = []) => {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/rls/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { P as PolicyOperation, R as Role, a as Policy } from "../packem_shared/types.d-
|
|
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
|
package/dist/rls/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { P as PolicyOperation, R as Role, a as Policy } from "../packem_shared/types.d-
|
|
1
|
+
import { P as PolicyOperation, R as Role, a as Policy } from "../packem_shared/types.d-Cxl6ndhm.js";
|
|
2
2
|
import "../data-model.js";
|
|
3
3
|
/**
|
|
4
4
|
* The slice of a request identity a policy reads. Mirrors the
|
package/dist/rls/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-
|
|
1
|
+
import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-2Jhd0uev.mjs';
|
|
2
2
|
|
|
3
3
|
const expectPolicy = (policies, options = {}) => {
|
|
4
4
|
const rolePermissions = indexRolePermissions(options.roles);
|