@lunora/server 0.0.0 → 1.0.0-alpha.2

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/LunoraEnvError-DjFkpkSP.mjs +187 -0
  14. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +51 -0
  15. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs +114 -0
  16. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +11 -0
  17. package/dist/packem_shared/bindOrm-DCuyr46L.mjs +71 -0
  18. package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +100 -0
  19. package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +29 -0
  20. package/dist/packem_shared/defineAggregateIndex-DxSso0rH.mjs +236 -0
  21. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +8 -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-Jc84C_hK.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-BDKRbMCA.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 +1051 -0
  37. package/dist/types.d.ts +1051 -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/LunoraEnvError-DjFkpkSP.mjs';
4
+ export { LunoraError } from './packem_shared/LunoraError-DhggBJZF.mjs';
5
+ export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-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/composePluginMiddleware-Ck5_TUO8.mjs';
10
+ export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs';
11
+ export { protectPublic } from './packem_shared/protectPublic-BjFkQ_Or.mjs';
12
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-DxSso0rH.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/createPolicyDsl-De67zPDS.mjs';
17
+ export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-qu0mpilX.mjs';
18
+ export { mask } from './packem_shared/mask-Jc84C_hK.mjs';
19
+ export { rls } from './packem_shared/rls-BDKRbMCA.mjs';
20
+ export { storageRules } from './packem_shared/storageRules-4a30FSpI.mjs';
21
+
22
+ const VERSION = "0.0.0";
23
+
24
+ export { VERSION };
@@ -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,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,114 @@
1
+ import { v } from '@lunora/values';
2
+ import { initLunora } from './initLunora-CATvPsVt.mjs';
3
+ import { LunoraError } from './LunoraError-DhggBJZF.mjs';
4
+ import { onDisconnect } from './onConnect-CIPXKPyw.mjs';
5
+ import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-Ck5_TUO8.mjs';
6
+ import { defineTable } from './defineAggregateIndex-DxSso0rH.mjs';
7
+
8
+ const DEFAULT_TTL_MS = 3e4;
9
+ const MAX_DATA_BYTES = 4096;
10
+ const PRESENCE_KEY = "presence";
11
+ const PRESENCE_BARE_TABLE = "present";
12
+ const PRESENCE_TABLE = `${PRESENCE_KEY}_${PRESENCE_BARE_TABLE}`;
13
+ const presenceExtension = defineSchemaExtension(PRESENCE_KEY, {
14
+ tables: {
15
+ [PRESENCE_BARE_TABLE]: defineTable({
16
+ data: v.optional(v.record(v.string(), v.any())),
17
+ lastSeen: v.number(),
18
+ roomId: v.string(),
19
+ sessionId: v.string(),
20
+ userId: v.optional(v.string())
21
+ }).index("byRoomSession", ["roomId", "sessionId"]).index("byRoom", ["roomId"])
22
+ }
23
+ });
24
+ const { mutation, query } = initLunora.dataModel().create();
25
+ const definePresence = (options = {}) => {
26
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
27
+ const disconnectGraceMs = Math.max(0, Math.min(options.disconnectGraceMs ?? 0, ttlMs));
28
+ const heartbeat = mutation.input({
29
+ data: v.optional(v.record(v.string(), v.any())),
30
+ roomId: v.string(),
31
+ sessionId: v.string()
32
+ }).mutation(async ({ args, ctx: context }) => {
33
+ const lastSeen = Date.now();
34
+ const userId = context.auth.userId ?? void 0;
35
+ if (args.data !== void 0 && JSON.stringify(args.data).length > MAX_DATA_BYTES) {
36
+ throw new LunoraError("BAD_REQUEST", `presence data exceeds the ${String(MAX_DATA_BYTES)}-byte limit`);
37
+ }
38
+ const existing = await context.db.query(PRESENCE_TABLE).withIndex("byRoomSession", (q) => q.eq("roomId", args.roomId).eq("sessionId", args.sessionId)).first();
39
+ if (existing && (existing["userId"] ?? void 0) !== userId) {
40
+ throw new LunoraError("FORBIDDEN", "presence heartbeat denied: this (roomId, sessionId) is held by another identity");
41
+ }
42
+ const row = {
43
+ lastSeen,
44
+ roomId: args.roomId,
45
+ sessionId: args.sessionId,
46
+ ...args.data === void 0 ? {} : { data: args.data },
47
+ ...userId === void 0 ? {} : { userId }
48
+ };
49
+ await (existing ? context.db.patch(existing["_id"], row) : context.db.insert(PRESENCE_TABLE, row));
50
+ return { lastSeen };
51
+ });
52
+ const listPresent = query.input({ roomId: v.string() }).query(async ({ args, ctx: context }) => {
53
+ const cutoff = Date.now() - ttlMs;
54
+ const rows = await context.db.query(PRESENCE_TABLE).withIndex("byRoom", (q) => q.eq("roomId", args.roomId)).collect();
55
+ const live = rows.filter((row) => row["lastSeen"] > cutoff).toSorted((a, b) => b["lastSeen"] - a["lastSeen"]);
56
+ const seenUsers = /* @__PURE__ */ new Set();
57
+ const members = [];
58
+ for (const row of live) {
59
+ const userId = row["userId"];
60
+ if (userId !== void 0) {
61
+ if (seenUsers.has(userId)) {
62
+ continue;
63
+ }
64
+ seenUsers.add(userId);
65
+ }
66
+ const member = {
67
+ lastSeen: row["lastSeen"],
68
+ roomId: row["roomId"]
69
+ };
70
+ if (userId !== void 0) {
71
+ member.userId = userId;
72
+ }
73
+ if (row["data"] !== void 0) {
74
+ member.data = row["data"];
75
+ }
76
+ members.push(member);
77
+ }
78
+ return members;
79
+ });
80
+ const sweep = mutation.input({ roomId: v.string() }).mutation(async ({ args, ctx: context }) => {
81
+ const cutoff = Date.now() - ttlMs;
82
+ const stale = await context.db.query(PRESENCE_TABLE).withIndex("byRoom", (q) => q.eq("roomId", args.roomId)).filter((row) => row["lastSeen"] <= cutoff).collect();
83
+ await Promise.all(stale.map((row) => context.db.delete(row["_id"])));
84
+ return { deleted: stale.length };
85
+ });
86
+ const internalSweep = { ...sweep, visibility: "internal" };
87
+ const disconnect = onDisconnect(async (context, event) => {
88
+ const roomId = event.context?.["roomId"];
89
+ const sessionId = event.context?.["sessionId"];
90
+ if (typeof roomId !== "string" || typeof sessionId !== "string") {
91
+ return;
92
+ }
93
+ const existing = await context.db.query(PRESENCE_TABLE).withIndex("byRoomSession", (q) => q.eq("roomId", roomId).eq("sessionId", sessionId)).first();
94
+ if (!existing) {
95
+ return;
96
+ }
97
+ const verifiedUserId = event.userId ?? void 0;
98
+ if ((existing["userId"] ?? void 0) !== verifiedUserId) {
99
+ return;
100
+ }
101
+ if (disconnectGraceMs === 0) {
102
+ await context.db.delete(existing["_id"]);
103
+ return;
104
+ }
105
+ const agedLastSeen = Math.min(existing["lastSeen"], Date.now() + disconnectGraceMs - ttlMs);
106
+ await context.db.patch(existing["_id"], { lastSeen: agedLastSeen });
107
+ });
108
+ return defineComponent(PRESENCE_KEY, {
109
+ extension: presenceExtension,
110
+ functions: { disconnect, heartbeat, listPresent, sweep: internalSweep }
111
+ });
112
+ };
113
+
114
+ export { DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension };
@@ -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,100 @@
1
+ import { r as runMiddlewareChain } from './run-middleware-CYQOuoV6.mjs';
2
+
3
+ const prefixTableName = (key, bareName) => `${key}_${bareName}`;
4
+ const rewriteReference = (target, key, bareNames) => bareNames.has(target) ? prefixTableName(key, target) : target;
5
+ const rewriteTableReferences = (table, key, bareNames, ownBareName) => {
6
+ const ownPrefixed = prefixTableName(key, ownBareName);
7
+ const rewriteOn = (on) => on === "" ? ownPrefixed : rewriteReference(on, key, bareNames);
8
+ const relationMap = {};
9
+ for (const [accessor, relation] of Object.entries(table.relationMap)) {
10
+ relationMap[accessor] = { ...relation, table: rewriteReference(relation.table, key, bareNames) };
11
+ }
12
+ const aggregateIndexes = table.aggregateIndexes.map((index) => {
13
+ return { ...index, on: rewriteOn(index.on) };
14
+ });
15
+ const rankIndexes = table.rankIndexes.map((index) => {
16
+ return { ...index, on: rewriteOn(index.on) };
17
+ });
18
+ return { ...table, aggregateIndexes, rankIndexes, relationMap };
19
+ };
20
+ const defineSchemaExtension = (key, options) => {
21
+ if (!key) {
22
+ throw new Error("defineSchemaExtension: `key` is required and must be a non-empty string");
23
+ }
24
+ return {
25
+ key,
26
+ tables: options.tables,
27
+ ...options.vectorIndexes ? { vectorIndexes: options.vectorIndexes } : {}
28
+ };
29
+ };
30
+ const definePlugin = (key, options) => {
31
+ if (!key) {
32
+ throw new Error("definePlugin: `key` is required and must be a non-empty string");
33
+ }
34
+ if (options.extension && options.extension.key !== key) {
35
+ throw new Error(`definePlugin("${key}"): extension key "${options.extension.key}" does not match plugin key`);
36
+ }
37
+ return {
38
+ key,
39
+ ...options.extension ? { extension: options.extension } : {},
40
+ ...options.middleware ? { middleware: options.middleware } : {}
41
+ };
42
+ };
43
+ const defineComponent = (key, options) => {
44
+ const plugin = definePlugin(key, {
45
+ ...options.extension ? { extension: options.extension } : {},
46
+ ...options.middleware ? { middleware: options.middleware } : {}
47
+ });
48
+ return {
49
+ ...plugin,
50
+ functions: options.functions ?? {}
51
+ };
52
+ };
53
+ const mergeSchemaExtension = (base, extension) => {
54
+ const { key } = extension;
55
+ const bareNames = new Set(Object.keys(extension.tables));
56
+ const merged = { ...base.tables };
57
+ for (const [bareName, table] of Object.entries(extension.tables)) {
58
+ const prefixed = prefixTableName(key, bareName);
59
+ if (Object.hasOwn(merged, prefixed)) {
60
+ throw new Error(
61
+ `defineSchema(...).extend("${key}"): table "${prefixed}" already exists in the base schema — another extension with the same key already contributed it`
62
+ );
63
+ }
64
+ merged[prefixed] = rewriteTableReferences(table, key, bareNames, bareName);
65
+ }
66
+ const mergedVectorIndexes = { ...base.vectorIndexes };
67
+ if (extension.vectorIndexes) {
68
+ for (const [bareIndexName, index] of Object.entries(extension.vectorIndexes)) {
69
+ const prefixed = prefixTableName(key, bareIndexName);
70
+ if (Object.hasOwn(mergedVectorIndexes, prefixed)) {
71
+ throw new Error(
72
+ `defineSchema(...).extend("${key}"): vector index "${prefixed}" already exists in the base schema — another extension with the same key already contributed it`
73
+ );
74
+ }
75
+ mergedVectorIndexes[prefixed] = { ...index, table: rewriteReference(index.table, key, bareNames) };
76
+ }
77
+ }
78
+ return {
79
+ // Preserve secure-by-default RLS across `.extend(...)` — a plugin's
80
+ // tables join an `.rls("required")` schema as protected-by-default too.
81
+ rlsMode: base.rlsMode,
82
+ tables: merged,
83
+ vectorIndexes: mergedVectorIndexes
84
+ };
85
+ };
86
+ const installPlugins = (base, plugins) => {
87
+ let schema = base;
88
+ for (const plugin of plugins) {
89
+ if (plugin.extension) {
90
+ schema = mergeSchemaExtension(schema, plugin.extension);
91
+ }
92
+ }
93
+ return schema;
94
+ };
95
+ const composePluginMiddleware = (plugins) => {
96
+ const middlewares = plugins.map((plugin) => plugin.middleware).filter((middleware) => middleware !== void 0);
97
+ return (async ({ ctx, next }) => runMiddlewareChain(middlewares, ctx, (context) => next({ ctx: context })));
98
+ };
99
+
100
+ export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension };
@@ -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 };