@lunora/server 1.0.0-alpha.31 → 1.0.0-alpha.32
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/dist/data-model.mjs +0 -1
- package/dist/drizzle.mjs +1 -1
- package/dist/index.d.mts +105 -8
- package/dist/index.d.ts +105 -8
- package/dist/index.mjs +1 -29
- package/dist/packem_shared/LunoraEnvError-CgpI2Mm_.mjs +3 -0
- package/dist/packem_shared/LunoraError-LVhdU0Lo.mjs +1 -0
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-CF_9uiu1.mjs +1 -0
- package/dist/packem_shared/allowAll-BnyNbJZT.mjs +1 -0
- package/dist/packem_shared/asBucketStorage-1pFfH-Tn.mjs +1 -0
- package/dist/packem_shared/bindOrm-Bp9hsM2q.mjs +1 -0
- package/dist/packem_shared/buildRlsReadRegistry-WdiqSj87.mjs +1 -0
- package/dist/packem_shared/composePluginMiddleware-COr09CXA.mjs +1 -0
- package/dist/packem_shared/createPolicyDsl-sV1swpkD.mjs +1 -0
- package/dist/packem_shared/createSecrets-CgVPiW2C.mjs +1 -0
- package/dist/packem_shared/defineAggregateIndex-CYuH6IbJ.mjs +1 -0
- package/dist/packem_shared/defineIdentity-B7gfAgxx.mjs +1 -0
- package/dist/packem_shared/defineMigration-Bfpwxv2f.mjs +1 -0
- package/dist/packem_shared/defineMutator-8Kz0j1Kt.mjs +1 -0
- package/dist/packem_shared/defineShape-CyuRfwHz.mjs +1 -0
- package/dist/packem_shared/defineStorageRule-BDu01PUn.mjs +1 -0
- package/dist/packem_shared/functions-CDC08CWY.mjs +1 -0
- package/dist/packem_shared/httpAction-C14NuF3V.mjs +4 -0
- package/dist/packem_shared/initLunora-CHh1EmR4.mjs +1 -0
- package/dist/packem_shared/mask-C6Bi78qj.mjs +1 -0
- package/dist/packem_shared/onConnect-CEtRmUpJ.mjs +1 -0
- package/dist/packem_shared/policy-tag-Dprt9JWo.mjs +1 -0
- package/dist/packem_shared/protectPublic-BhKewPqm.mjs +1 -0
- package/dist/packem_shared/rls-_iVsPvhX.mjs +1 -0
- package/dist/packem_shared/run-middleware-BeEEqmdE.mjs +1 -0
- package/dist/packem_shared/storageRules-BptPZbi8.mjs +1 -0
- package/dist/rls/testing.mjs +1 -49
- package/dist/types.d.mts +76 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.mjs +1 -31
- package/package.json +1 -1
- package/dist/packem_shared/LunoraEnvError-BGmd1Qs0.mjs +0 -187
- package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +0 -9
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs +0 -114
- package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
- package/dist/packem_shared/bindOrm-CaY7Wq9Z.mjs +0 -147
- package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +0 -107
- package/dist/packem_shared/composePluginMiddleware-z62dttBo.mjs +0 -103
- package/dist/packem_shared/createPolicyDsl-By3QB4he.mjs +0 -32
- package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +0 -58
- package/dist/packem_shared/defineAggregateIndex-cdo0g-un.mjs +0 -349
- package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +0 -35
- package/dist/packem_shared/defineMigration-Hx01yIht.mjs +0 -10
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
- package/dist/packem_shared/defineShape-C5scNOrf.mjs +0 -18
- package/dist/packem_shared/defineStorageRule-B5nL4Z1P.mjs +0 -23
- package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
- package/dist/packem_shared/httpAction-DCXoYPIk.mjs +0 -355
- package/dist/packem_shared/initLunora-D0Wuki7S.mjs +0 -115
- package/dist/packem_shared/mask-BepaW7YN.mjs +0 -334
- package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
- package/dist/packem_shared/protectPublic-BlcGpiRc.mjs +0 -15
- package/dist/packem_shared/rls-BaDQf7MG.mjs +0 -595
- package/dist/packem_shared/run-middleware-I6EiQfxL.mjs +0 -20
- package/dist/packem_shared/storageRules-6QxzDOcx.mjs +0 -88
package/dist/rls/testing.mjs
CHANGED
|
@@ -1,49 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const expectPolicy = (policies, options = {}) => {
|
|
4
|
-
const rolePermissions = indexRolePermissions(options.roles);
|
|
5
|
-
const context = options.ctx ?? {};
|
|
6
|
-
return {
|
|
7
|
-
as: (identity) => {
|
|
8
|
-
const auth = identity ?? {};
|
|
9
|
-
const roles = auth.roles ?? [];
|
|
10
|
-
const granted = /* @__PURE__ */ new Set();
|
|
11
|
-
for (const roleName of roles) {
|
|
12
|
-
for (const name of rolePermissions.get(roleName) ?? []) {
|
|
13
|
-
granted.add(name);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
const baseContext = {
|
|
17
|
-
auth: {
|
|
18
|
-
can: (permission) => granted.has(permissionName(permission)),
|
|
19
|
-
// eslint-disable-next-line unicorn/no-null -- PolicyContext.auth.identity is a public `… | null` type, mirroring the middleware
|
|
20
|
-
identity: auth.identity ?? null,
|
|
21
|
-
roles,
|
|
22
|
-
// eslint-disable-next-line unicorn/no-null -- PolicyContext.auth.userId is a public `null | string` type, mirroring the middleware
|
|
23
|
-
userId: auth.userId ?? null
|
|
24
|
-
},
|
|
25
|
-
ctx: context
|
|
26
|
-
};
|
|
27
|
-
const can = (op, table, row, nextRow) => {
|
|
28
|
-
const tablePolicies = policies.filter((policy) => policy.table === table);
|
|
29
|
-
if (tablePolicies.length === 0) {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
if (op === "read") {
|
|
33
|
-
if (!tablePolicies.some((policy) => policy.on === "read")) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
const baseWhere = computeReadBaseWhere(tablePolicies, baseContext);
|
|
37
|
-
return baseWhere === void 0 || matchesWhere(row ?? {}, baseWhere);
|
|
38
|
-
}
|
|
39
|
-
return evaluateWrite(tablePolicies, op, { ...baseContext, row }, nextRow);
|
|
40
|
-
};
|
|
41
|
-
return {
|
|
42
|
-
can,
|
|
43
|
-
cannot: (op, table, row, nextRow) => !can(op, table, row, nextRow)
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export { expectPolicy };
|
|
1
|
+
import{indexRolePermissions as y,computeReadBaseWhere as I,matchesWhere as P,evaluateWrite as W,permissionName as g}from"../packem_shared/rls-_iVsPvhX.mjs";const w=(f,i={})=>{const m=y(i.roles),h=i.ctx??{};return{as:p=>{const s=p??{},c=s.roles??[],u=new Set;for(const e of c)for(const t of m.get(e)??[])u.add(t);const l={auth:{can:e=>u.has(g(e)),identity:s.identity??null,roles:c,userId:s.userId??null},ctx:h},d=(e,t,r,a)=>{const n=f.filter(o=>o.table===t);if(n.length===0)return!0;if(e==="read"){if(!n.some(x=>x.on==="read"))return!0;const o=I(n,l);return o===void 0||P(r??{},o)}return W(n,e,{...l,row:r},a)};return{can:d,cannot:(e,t,r,a)=>!d(e,t,r,a)}}}};export{w as expectPolicy};
|
package/dist/types.d.mts
CHANGED
|
@@ -282,6 +282,17 @@ interface TableDefinition<Shape extends Record<string, Validator> = Record<strin
|
|
|
282
282
|
* Has no effect when the schema does not require RLS.
|
|
283
283
|
*/
|
|
284
284
|
isPublic?: boolean;
|
|
285
|
+
/**
|
|
286
|
+
* Set by `.ownedBy(field)` — the column holding the owning user's id (named
|
|
287
|
+
* `ownerField`, a data field, rather than colliding with the fluent
|
|
288
|
+
* `.ownedBy()` builder method — same convention as `shardBy()`/`shardMode`).
|
|
289
|
+
*
|
|
290
|
+
* A shape over this table with `owner: true` derives its predicate from this
|
|
291
|
+
* field, so "only the owner may replicate these rows" is declared once on the
|
|
292
|
+
* table instead of being restated in every shape's `where`. Absent ⇒ the table
|
|
293
|
+
* has no single owning column and a shape must spell its predicate out.
|
|
294
|
+
*/
|
|
295
|
+
ownerField?: string;
|
|
285
296
|
/**
|
|
286
297
|
* Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
|
|
287
298
|
* a sorted companion table per declared rank with a btree on
|
|
@@ -532,6 +543,30 @@ interface SystemDatabaseReader {
|
|
|
532
543
|
* actual SQL implementation lives in `@lunora/do`; these are signatures only.
|
|
533
544
|
*/
|
|
534
545
|
interface DatabaseReader {
|
|
546
|
+
/**
|
|
547
|
+
* The throwing sibling of {@link DatabaseReader.normalizeId}: brand `id` as an
|
|
548
|
+
* {@link Id} for `tableName`, or throw `BAD_REQUEST` when it is not structurally
|
|
549
|
+
* an id. Pure — it never reads the database, so a valid id for a row that
|
|
550
|
+
* doesn't exist still returns.
|
|
551
|
+
*
|
|
552
|
+
* This is the **parse boundary** for an id that arrived as a plain `string`: a
|
|
553
|
+
* wire payload, a mutator's args, a change plan computed on the client. The
|
|
554
|
+
* alternative is `value as Id<"table">` at every such call site — an assertion,
|
|
555
|
+
* not a check, and one that has to be repeated for every table a helper is
|
|
556
|
+
* generic over:
|
|
557
|
+
*
|
|
558
|
+
* ```ts
|
|
559
|
+
* for (const patch of plan.patches) {
|
|
560
|
+
* await ctx.db.patch(ctx.db.asId("nodes", patch.id), patch.fields);
|
|
561
|
+
* }
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* Ids are opaque strings, so the check is exactly `normalizeId`'s: it rejects
|
|
565
|
+
* empty, whitespace-bearing, and NUL-bearing values, not "an id that isn't in
|
|
566
|
+
* this table". Use it to get the brand honestly, and `get()` to learn whether
|
|
567
|
+
* the row exists.
|
|
568
|
+
*/
|
|
569
|
+
asId: <T extends string>(tableName: T, id: string) => Id<T>;
|
|
535
570
|
get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
|
|
536
571
|
/**
|
|
537
572
|
* Validate an untrusted `id` string against the structural shape of an id
|
|
@@ -686,6 +721,24 @@ interface InsertManyOptions extends BatchWriteOptions {
|
|
|
686
721
|
}
|
|
687
722
|
interface DatabaseWriter extends DatabaseReader {
|
|
688
723
|
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
724
|
+
/**
|
|
725
|
+
* Delete EVERY row in `tableName`, chunking internally until the table is
|
|
726
|
+
* empty — the erasure primitive.
|
|
727
|
+
*
|
|
728
|
+
* Unlike `deleteWhere(tableName, {})` there is **no batch cap**: a
|
|
729
|
+
* `BATCH_LIMIT_EXCEEDED` at row 501 of an account deletion is a bug, not a
|
|
730
|
+
* safety rail. Rows still go through the single-row delete pipeline, so
|
|
731
|
+
* triggers, cascades, companions, CDC, and live subscriptions stay correct.
|
|
732
|
+
*
|
|
733
|
+
* On a `.softDelete()` table the default flips the marker column; pass
|
|
734
|
+
* `{ hard: true }` to remove the rows physically (what GDPR erasure means).
|
|
735
|
+
*/
|
|
736
|
+
deleteAll: (tableName: string, options?: {
|
|
737
|
+
chunkSize?: number;
|
|
738
|
+
hard?: boolean;
|
|
739
|
+
}) => Promise<{
|
|
740
|
+
deleted: number;
|
|
741
|
+
}>;
|
|
689
742
|
/**
|
|
690
743
|
* Delete many rows by id in one call. Each id is deleted through the full
|
|
691
744
|
* single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
|
|
@@ -793,6 +846,29 @@ interface DatabaseWriter extends DatabaseReader {
|
|
|
793
846
|
patched: number;
|
|
794
847
|
}>;
|
|
795
848
|
replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
|
|
849
|
+
/**
|
|
850
|
+
* Erase every shard-local table — the account-deletion / tenant-teardown
|
|
851
|
+
* primitive. Sweeps the schema's non-`.global()` tables with
|
|
852
|
+
* {@link DatabaseWriter.deleteAll}`({ hard: true })` and returns the per-table
|
|
853
|
+
* counts.
|
|
854
|
+
*
|
|
855
|
+
* `.global()` tables are skipped by design: their rows live in D1 and are shared
|
|
856
|
+
* across shards, so "wipe this shard" must not reach them. Restrict the sweep
|
|
857
|
+
* with `options.tables`, or spare one with `options.exclude` (e.g. an audit log
|
|
858
|
+
* that must outlive the data).
|
|
859
|
+
*
|
|
860
|
+
* ```ts
|
|
861
|
+
* export const deleteAccount = internalMutation({ handler: async ({ ctx }) => ctx.db.wipeShard() });
|
|
862
|
+
* ```
|
|
863
|
+
*/
|
|
864
|
+
wipeShard: (options?: {
|
|
865
|
+
chunkSize?: number;
|
|
866
|
+
exclude?: ReadonlyArray<string>;
|
|
867
|
+
tables?: ReadonlyArray<string>;
|
|
868
|
+
}) => Promise<{
|
|
869
|
+
deleted: number;
|
|
870
|
+
tables: Record<string, number>;
|
|
871
|
+
}>;
|
|
796
872
|
}
|
|
797
873
|
/** Authenticated identity surfaced into every context. */
|
|
798
874
|
interface AuthState {
|
package/dist/types.d.ts
CHANGED
|
@@ -282,6 +282,17 @@ interface TableDefinition<Shape extends Record<string, Validator> = Record<strin
|
|
|
282
282
|
* Has no effect when the schema does not require RLS.
|
|
283
283
|
*/
|
|
284
284
|
isPublic?: boolean;
|
|
285
|
+
/**
|
|
286
|
+
* Set by `.ownedBy(field)` — the column holding the owning user's id (named
|
|
287
|
+
* `ownerField`, a data field, rather than colliding with the fluent
|
|
288
|
+
* `.ownedBy()` builder method — same convention as `shardBy()`/`shardMode`).
|
|
289
|
+
*
|
|
290
|
+
* A shape over this table with `owner: true` derives its predicate from this
|
|
291
|
+
* field, so "only the owner may replicate these rows" is declared once on the
|
|
292
|
+
* table instead of being restated in every shape's `where`. Absent ⇒ the table
|
|
293
|
+
* has no single owning column and a shape must spell its predicate out.
|
|
294
|
+
*/
|
|
295
|
+
ownerField?: string;
|
|
285
296
|
/**
|
|
286
297
|
* Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
|
|
287
298
|
* a sorted companion table per declared rank with a btree on
|
|
@@ -532,6 +543,30 @@ interface SystemDatabaseReader {
|
|
|
532
543
|
* actual SQL implementation lives in `@lunora/do`; these are signatures only.
|
|
533
544
|
*/
|
|
534
545
|
interface DatabaseReader {
|
|
546
|
+
/**
|
|
547
|
+
* The throwing sibling of {@link DatabaseReader.normalizeId}: brand `id` as an
|
|
548
|
+
* {@link Id} for `tableName`, or throw `BAD_REQUEST` when it is not structurally
|
|
549
|
+
* an id. Pure — it never reads the database, so a valid id for a row that
|
|
550
|
+
* doesn't exist still returns.
|
|
551
|
+
*
|
|
552
|
+
* This is the **parse boundary** for an id that arrived as a plain `string`: a
|
|
553
|
+
* wire payload, a mutator's args, a change plan computed on the client. The
|
|
554
|
+
* alternative is `value as Id<"table">` at every such call site — an assertion,
|
|
555
|
+
* not a check, and one that has to be repeated for every table a helper is
|
|
556
|
+
* generic over:
|
|
557
|
+
*
|
|
558
|
+
* ```ts
|
|
559
|
+
* for (const patch of plan.patches) {
|
|
560
|
+
* await ctx.db.patch(ctx.db.asId("nodes", patch.id), patch.fields);
|
|
561
|
+
* }
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* Ids are opaque strings, so the check is exactly `normalizeId`'s: it rejects
|
|
565
|
+
* empty, whitespace-bearing, and NUL-bearing values, not "an id that isn't in
|
|
566
|
+
* this table". Use it to get the brand honestly, and `get()` to learn whether
|
|
567
|
+
* the row exists.
|
|
568
|
+
*/
|
|
569
|
+
asId: <T extends string>(tableName: T, id: string) => Id<T>;
|
|
535
570
|
get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
|
|
536
571
|
/**
|
|
537
572
|
* Validate an untrusted `id` string against the structural shape of an id
|
|
@@ -686,6 +721,24 @@ interface InsertManyOptions extends BatchWriteOptions {
|
|
|
686
721
|
}
|
|
687
722
|
interface DatabaseWriter extends DatabaseReader {
|
|
688
723
|
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
724
|
+
/**
|
|
725
|
+
* Delete EVERY row in `tableName`, chunking internally until the table is
|
|
726
|
+
* empty — the erasure primitive.
|
|
727
|
+
*
|
|
728
|
+
* Unlike `deleteWhere(tableName, {})` there is **no batch cap**: a
|
|
729
|
+
* `BATCH_LIMIT_EXCEEDED` at row 501 of an account deletion is a bug, not a
|
|
730
|
+
* safety rail. Rows still go through the single-row delete pipeline, so
|
|
731
|
+
* triggers, cascades, companions, CDC, and live subscriptions stay correct.
|
|
732
|
+
*
|
|
733
|
+
* On a `.softDelete()` table the default flips the marker column; pass
|
|
734
|
+
* `{ hard: true }` to remove the rows physically (what GDPR erasure means).
|
|
735
|
+
*/
|
|
736
|
+
deleteAll: (tableName: string, options?: {
|
|
737
|
+
chunkSize?: number;
|
|
738
|
+
hard?: boolean;
|
|
739
|
+
}) => Promise<{
|
|
740
|
+
deleted: number;
|
|
741
|
+
}>;
|
|
689
742
|
/**
|
|
690
743
|
* Delete many rows by id in one call. Each id is deleted through the full
|
|
691
744
|
* single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
|
|
@@ -793,6 +846,29 @@ interface DatabaseWriter extends DatabaseReader {
|
|
|
793
846
|
patched: number;
|
|
794
847
|
}>;
|
|
795
848
|
replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
|
|
849
|
+
/**
|
|
850
|
+
* Erase every shard-local table — the account-deletion / tenant-teardown
|
|
851
|
+
* primitive. Sweeps the schema's non-`.global()` tables with
|
|
852
|
+
* {@link DatabaseWriter.deleteAll}`({ hard: true })` and returns the per-table
|
|
853
|
+
* counts.
|
|
854
|
+
*
|
|
855
|
+
* `.global()` tables are skipped by design: their rows live in D1 and are shared
|
|
856
|
+
* across shards, so "wipe this shard" must not reach them. Restrict the sweep
|
|
857
|
+
* with `options.tables`, or spare one with `options.exclude` (e.g. an audit log
|
|
858
|
+
* that must outlive the data).
|
|
859
|
+
*
|
|
860
|
+
* ```ts
|
|
861
|
+
* export const deleteAccount = internalMutation({ handler: async ({ ctx }) => ctx.db.wipeShard() });
|
|
862
|
+
* ```
|
|
863
|
+
*/
|
|
864
|
+
wipeShard: (options?: {
|
|
865
|
+
chunkSize?: number;
|
|
866
|
+
exclude?: ReadonlyArray<string>;
|
|
867
|
+
tables?: ReadonlyArray<string>;
|
|
868
|
+
}) => Promise<{
|
|
869
|
+
deleted: number;
|
|
870
|
+
tables: Record<string, number>;
|
|
871
|
+
}>;
|
|
796
872
|
}
|
|
797
873
|
/** Authenticated identity surfaced into every context. */
|
|
798
874
|
interface AuthState {
|
package/dist/types.mjs
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const anyApi = /* @__PURE__ */ new Proxy(
|
|
3
|
-
{},
|
|
4
|
-
{
|
|
5
|
-
get(_target, namespace) {
|
|
6
|
-
const cached = namespaceCache.get(namespace);
|
|
7
|
-
if (cached) {
|
|
8
|
-
return cached;
|
|
9
|
-
}
|
|
10
|
-
const refCache = /* @__PURE__ */ new Map();
|
|
11
|
-
const nsProxy = /* @__PURE__ */ new Proxy(
|
|
12
|
-
{},
|
|
13
|
-
{
|
|
14
|
-
get(_inner, functionName) {
|
|
15
|
-
const cachedRef = refCache.get(functionName);
|
|
16
|
-
if (cachedRef) {
|
|
17
|
-
return cachedRef;
|
|
18
|
-
}
|
|
19
|
-
const ref = { __lunoraRef: `${String(namespace)}:${String(functionName)}` };
|
|
20
|
-
refCache.set(functionName, ref);
|
|
21
|
-
return ref;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
);
|
|
25
|
-
namespaceCache.set(namespace, nsProxy);
|
|
26
|
-
return nsProxy;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
export { anyApi };
|
|
1
|
+
const a=new Map,u=new Proxy({},{get(i,t){const e=a.get(t);if(e)return e;const r=new Map,o=new Proxy({},{get(c,n){const s=r.get(n);if(s)return s;const g={__lunoraRef:`${String(t)}:${String(n)}`};return r.set(n,g),g}});return a.set(t,o),o}});export{u as anyApi};
|
package/package.json
CHANGED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { LunoraError as LunoraError$1 } from '@lunora/errors';
|
|
2
|
-
import { optionalInner } from '@lunora/values';
|
|
3
|
-
|
|
4
|
-
const SECRET_VALUE_PREFIXES = [
|
|
5
|
-
/^sk_/u,
|
|
6
|
-
/^pk_/u,
|
|
7
|
-
/^rk_/u,
|
|
8
|
-
/^ghp_/u,
|
|
9
|
-
/^gho_/u,
|
|
10
|
-
/^ghs_/u,
|
|
11
|
-
/^ghr_/u,
|
|
12
|
-
/^github_pat_/u,
|
|
13
|
-
/^xox[baprs]-/u,
|
|
14
|
-
/^AKIA/u,
|
|
15
|
-
/^AIza/u,
|
|
16
|
-
/^Bearer\s/u
|
|
17
|
-
];
|
|
18
|
-
const HIGH_ENTROPY_TOKEN = /[\w./+-]{24,}/gu;
|
|
19
|
-
const STANDALONE_TOKEN = /^[\w./+-]+$/u;
|
|
20
|
-
const EMBEDDED_PREFIXED_TOKEN_SHORT = /\b(?:(?:sk|pk|rk|ghp|gho|ghs|ghr)_[\w./+-]*|github_pat_[\w./+-]*|xox[baprs]-[\w./+-]*)/gu;
|
|
21
|
-
const EMBEDDED_PREFIXED_TOKEN_LONG = /\b(?:AKIA|AIza)[\w./+-]+|Bearer\s+[\w./+-]+/gu;
|
|
22
|
-
const URL_CREDENTIAL = /\b([a-z][\w.+-]*:\/\/[\w.%+-]+):[\w.%+-]+@/gu;
|
|
23
|
-
const REDACTED = "[redacted]";
|
|
24
|
-
const SECRET_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
|
|
25
|
-
const looksLikeSecretValue = (value) => {
|
|
26
|
-
if (SECRET_VALUE_PREFIXES.some((prefix) => prefix.test(value))) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
const trimmed = value.trim();
|
|
30
|
-
return trimmed.length >= 24 && STANDALONE_TOKEN.test(trimmed);
|
|
31
|
-
};
|
|
32
|
-
const QUOTED_VALUE = /(["'])(?<inner>(?:\\.|(?!\1).)*)\1/gu;
|
|
33
|
-
const KEYED_VALUE = /\b(?<key>[A-Za-z_]\w*)\s*[=:]\s*\S+/gu;
|
|
34
|
-
const redactSecrets = (message) => {
|
|
35
|
-
let out = message;
|
|
36
|
-
out = out.replaceAll(QUOTED_VALUE, (match, ...groups) => {
|
|
37
|
-
const named = groups.at(-1);
|
|
38
|
-
return named?.inner !== void 0 && looksLikeSecretValue(named.inner) ? REDACTED : match;
|
|
39
|
-
});
|
|
40
|
-
out = out.replaceAll(URL_CREDENTIAL, (_match, prefix) => `${prefix}:${REDACTED}@`);
|
|
41
|
-
out = out.replaceAll(EMBEDDED_PREFIXED_TOKEN_SHORT, REDACTED);
|
|
42
|
-
out = out.replaceAll(EMBEDDED_PREFIXED_TOKEN_LONG, REDACTED);
|
|
43
|
-
out = out.replaceAll(KEYED_VALUE, (match, ...groups) => {
|
|
44
|
-
const named = groups.at(-1);
|
|
45
|
-
return named?.key !== void 0 && SECRET_KEY.test(named.key) ? `${named.key}=${REDACTED}` : match;
|
|
46
|
-
});
|
|
47
|
-
out = out.replaceAll(HIGH_ENTROPY_TOKEN, REDACTED);
|
|
48
|
-
return out;
|
|
49
|
-
};
|
|
50
|
-
const redactValueForKey = (message, key, raw) => {
|
|
51
|
-
const masked = redactSecrets(message);
|
|
52
|
-
if (typeof raw === "string" && raw !== "" && SECRET_KEY.test(key)) {
|
|
53
|
-
return masked.replaceAll(raw, REDACTED);
|
|
54
|
-
}
|
|
55
|
-
return masked;
|
|
56
|
-
};
|
|
57
|
-
class LunoraEnvError extends LunoraError$1 {
|
|
58
|
-
failures;
|
|
59
|
-
constructor(failures) {
|
|
60
|
-
const summary = failures.map((failure) => ` - ${failure.key}: ${failure.message}`).join("\n");
|
|
61
|
-
super("ENV_INVALID", `Invalid environment (${String(failures.length)} key(s)):
|
|
62
|
-
${summary}`, { name: "LunoraEnvError" });
|
|
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 };
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { v } from '@lunora/values';
|
|
2
|
-
import { initLunora } from './initLunora-D0Wuki7S.mjs';
|
|
3
|
-
import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
|
|
4
|
-
import { onDisconnect } from './onConnect-CIPXKPyw.mjs';
|
|
5
|
-
import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-z62dttBo.mjs';
|
|
6
|
-
import { defineTable } from './defineAggregateIndex-cdo0g-un.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 && new TextEncoder().encode(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 };
|
|
@@ -1,11 +0,0 @@
|
|
|
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 };
|