@lunora/auth 1.0.0-alpha.4 → 1.0.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/README.md +55 -0
- package/dist/adapter.d.mts +35 -35
- package/dist/adapter.d.ts +35 -35
- package/dist/adapter.mjs +1 -47
- package/dist/audit.d.mts +114 -0
- package/dist/audit.d.ts +114 -0
- package/dist/audit.mjs +11 -0
- package/dist/email-guard.d.mts +122 -0
- package/dist/email-guard.d.ts +122 -0
- package/dist/email-guard.mjs +1 -0
- package/dist/index.d.mts +460 -146
- package/dist/index.d.ts +460 -146
- package/dist/index.mjs +1 -12
- package/dist/middleware.d.mts +156 -155
- package/dist/middleware.d.ts +156 -155
- package/dist/middleware.mjs +1 -53
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs +1 -0
- package/dist/packem_shared/LunoraAuthAdminError-BiNYZM9j.mjs +1 -0
- package/dist/packem_shared/authAuditHook-Dx3sqf3G.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-ChiudSmt.mjs +1 -0
- package/dist/packem_shared/create-auth.d-De6IOirt.d.mts +128 -0
- package/dist/packem_shared/create-auth.d-De6IOirt.d.ts +128 -0
- package/dist/packem_shared/createAuth-DS6PL8Mb.mjs +1 -0
- package/dist/packem_shared/emailGateDatabaseHooks-DzBD1Qoq.mjs +1 -0
- package/dist/packem_shared/sessionPresets-DpEFjXKV.mjs +1 -0
- package/dist/plugins-client.mjs +1 -2
- package/dist/plugins.mjs +1 -22
- package/dist/schema.d.mts +39 -39
- package/dist/schema.d.ts +39 -39
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.d.mts +28 -28
- package/dist/sql-store.d.ts +28 -28
- package/dist/sql-store.mjs +1 -162
- package/dist/store.d.mts +49 -31
- package/dist/store.d.ts +49 -31
- package/dist/store.mjs +1 -170
- package/dist/turnstile-middleware.d.mts +55 -55
- package/dist/turnstile-middleware.d.ts +55 -55
- package/dist/turnstile-middleware.mjs +1 -45
- package/dist/turnstile.d.mts +42 -59
- package/dist/turnstile.d.ts +42 -59
- package/dist/turnstile.mjs +1 -61
- package/package.json +19 -6
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs +0 -249
- package/dist/packem_shared/compileMigrationsSql-wZH3oXDu.mjs +0 -28
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.mts +0 -58
- package/dist/packem_shared/create-auth.d-M36jwG_Y.d.ts +0 -58
- package/dist/packem_shared/createAuth-B-tvsvQU.mjs +0 -56
- package/dist/packem_shared/sessionPresets-B95rXrd8.mjs +0 -35
package/dist/sql-store.mjs
CHANGED
|
@@ -1,162 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const NEVER = { params: [], sql: "0" };
|
|
3
|
-
const sided = (column, insensitive) => insensitive ? { column: `LOWER(${column})`, placeholder: "LOWER(?)" } : { column, placeholder: "?" };
|
|
4
|
-
const patternFragment = (column, value, operator, insensitive) => {
|
|
5
|
-
if (typeof value !== "string") {
|
|
6
|
-
return NEVER;
|
|
7
|
-
}
|
|
8
|
-
const side = sided(column, insensitive);
|
|
9
|
-
if (operator === "contains") {
|
|
10
|
-
return { params: [value], sql: `instr(${side.column}, ${side.placeholder}) > 0` };
|
|
11
|
-
}
|
|
12
|
-
if (operator === "starts_with") {
|
|
13
|
-
return { params: [value], sql: `instr(${side.column}, ${side.placeholder}) = 1` };
|
|
14
|
-
}
|
|
15
|
-
return { params: [value, value], sql: `substr(${side.column}, -length(?)) = ${side.placeholder}` };
|
|
16
|
-
};
|
|
17
|
-
const equalityFragment = (column, value, insensitive, negated) => {
|
|
18
|
-
if (value === null) {
|
|
19
|
-
return { params: [], sql: `${column} IS ${negated ? "NOT " : ""}NULL` };
|
|
20
|
-
}
|
|
21
|
-
const side = sided(column, insensitive);
|
|
22
|
-
const symbol = negated ? "<>" : "=";
|
|
23
|
-
return { params: [value], sql: `${side.column} ${symbol} ${side.placeholder}` };
|
|
24
|
-
};
|
|
25
|
-
const inFragment = (column, value, negated, insensitive) => {
|
|
26
|
-
const values = Array.isArray(value) ? value : [];
|
|
27
|
-
if (values.length === 0) {
|
|
28
|
-
return { params: [], sql: negated ? "1" : "0" };
|
|
29
|
-
}
|
|
30
|
-
const side = sided(column, insensitive);
|
|
31
|
-
const placeholders = values.map(() => side.placeholder).join(", ");
|
|
32
|
-
return { params: [...values], sql: `${side.column} ${negated ? "NOT IN" : "IN"} (${placeholders})` };
|
|
33
|
-
};
|
|
34
|
-
const compileClause = (clause) => {
|
|
35
|
-
const column = quoteId(clause.field);
|
|
36
|
-
const { mode, operator, value } = clause;
|
|
37
|
-
const insensitive = mode === "insensitive";
|
|
38
|
-
switch (operator) {
|
|
39
|
-
case "contains":
|
|
40
|
-
case "ends_with":
|
|
41
|
-
case "starts_with": {
|
|
42
|
-
return patternFragment(column, value, operator, insensitive);
|
|
43
|
-
}
|
|
44
|
-
case "gt": {
|
|
45
|
-
return { params: [value], sql: `${column} > ?` };
|
|
46
|
-
}
|
|
47
|
-
case "gte": {
|
|
48
|
-
return { params: [value], sql: `${column} >= ?` };
|
|
49
|
-
}
|
|
50
|
-
case "in": {
|
|
51
|
-
return inFragment(column, value, false, insensitive);
|
|
52
|
-
}
|
|
53
|
-
case "lt": {
|
|
54
|
-
return { params: [value], sql: `${column} < ?` };
|
|
55
|
-
}
|
|
56
|
-
case "lte": {
|
|
57
|
-
return { params: [value], sql: `${column} <= ?` };
|
|
58
|
-
}
|
|
59
|
-
case "ne": {
|
|
60
|
-
return equalityFragment(column, value, insensitive, true);
|
|
61
|
-
}
|
|
62
|
-
case "not_in": {
|
|
63
|
-
return inFragment(column, value, true, insensitive);
|
|
64
|
-
}
|
|
65
|
-
// "eq" and the default: null-aware equality.
|
|
66
|
-
default: {
|
|
67
|
-
return equalityFragment(column, value, insensitive, false);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const compileWhere = (where) => {
|
|
72
|
-
if (where.length === 0) {
|
|
73
|
-
return { params: [], sql: "" };
|
|
74
|
-
}
|
|
75
|
-
let accumulator = compileClause(where[0]);
|
|
76
|
-
for (const clause of where.slice(1)) {
|
|
77
|
-
const fragment = compileClause(clause);
|
|
78
|
-
const connector = clause.connector === "OR" ? "OR" : "AND";
|
|
79
|
-
accumulator = { params: [...accumulator.params, ...fragment.params], sql: `(${accumulator.sql} ${connector} ${fragment.sql})` };
|
|
80
|
-
}
|
|
81
|
-
return accumulator;
|
|
82
|
-
};
|
|
83
|
-
const whereSuffix = (fragment) => fragment.sql ? ` WHERE ${fragment.sql}` : "";
|
|
84
|
-
const createSqlAuthStore = (executor) => {
|
|
85
|
-
const selectRows = (model, where) => {
|
|
86
|
-
const fragment = compileWhere(where);
|
|
87
|
-
return executor.all(`SELECT * FROM ${quoteId(model)}${whereSuffix(fragment)}`, fragment.params);
|
|
88
|
-
};
|
|
89
|
-
return {
|
|
90
|
-
consumeOne: async (model, where) => {
|
|
91
|
-
const fragment = compileWhere(where);
|
|
92
|
-
const table = quoteId(model);
|
|
93
|
-
const [row] = await executor.all(
|
|
94
|
-
`DELETE FROM ${table} WHERE rowid IN (SELECT rowid FROM ${table}${whereSuffix(fragment)} LIMIT 1) RETURNING *`,
|
|
95
|
-
fragment.params
|
|
96
|
-
);
|
|
97
|
-
return row;
|
|
98
|
-
},
|
|
99
|
-
count: async (model, where) => {
|
|
100
|
-
const fragment = compileWhere(where);
|
|
101
|
-
const [row] = await executor.all(`SELECT COUNT(*) AS __count FROM ${quoteId(model)}${whereSuffix(fragment)}`, fragment.params);
|
|
102
|
-
return Number(row?.["__count"] ?? 0);
|
|
103
|
-
},
|
|
104
|
-
create: async (model, data) => {
|
|
105
|
-
const columns = Object.keys(data);
|
|
106
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
107
|
-
const sql = `INSERT INTO ${quoteId(model)} (${columns.map((column) => quoteId(column)).join(", ")}) VALUES (${placeholders})`;
|
|
108
|
-
await executor.run(
|
|
109
|
-
sql,
|
|
110
|
-
columns.map((column) => data[column])
|
|
111
|
-
);
|
|
112
|
-
return { ...data };
|
|
113
|
-
},
|
|
114
|
-
read: async (model, query) => {
|
|
115
|
-
const fragment = compileWhere(query.where);
|
|
116
|
-
const parameters = [...fragment.params];
|
|
117
|
-
let sql = `SELECT * FROM ${quoteId(model)}${whereSuffix(fragment)}`;
|
|
118
|
-
if (query.sortBy) {
|
|
119
|
-
sql += ` ORDER BY ${quoteId(query.sortBy.field)} ${query.sortBy.direction === "asc" ? "ASC" : "DESC"}`;
|
|
120
|
-
}
|
|
121
|
-
if (query.limit !== void 0) {
|
|
122
|
-
sql += " LIMIT ?";
|
|
123
|
-
parameters.push(Math.trunc(query.limit));
|
|
124
|
-
}
|
|
125
|
-
if (query.offset) {
|
|
126
|
-
sql += `${query.limit === void 0 ? " LIMIT -1" : ""} OFFSET ?`;
|
|
127
|
-
parameters.push(Math.trunc(query.offset));
|
|
128
|
-
}
|
|
129
|
-
return executor.all(sql, parameters);
|
|
130
|
-
},
|
|
131
|
-
remove: async (model, where) => {
|
|
132
|
-
const fragment = compileWhere(where);
|
|
133
|
-
const deleted = await executor.all(`DELETE FROM ${quoteId(model)}${whereSuffix(fragment)} RETURNING *`, fragment.params);
|
|
134
|
-
return deleted.length;
|
|
135
|
-
},
|
|
136
|
-
update: async (model, where, values) => {
|
|
137
|
-
const columns = Object.keys(values);
|
|
138
|
-
if (columns.length === 0) {
|
|
139
|
-
return selectRows(model, where);
|
|
140
|
-
}
|
|
141
|
-
const assignments = columns.map((column) => `${quoteId(column)} = ?`).join(", ");
|
|
142
|
-
const fragment = compileWhere(where);
|
|
143
|
-
return executor.all(`UPDATE ${quoteId(model)} SET ${assignments}${whereSuffix(fragment)} RETURNING *`, [
|
|
144
|
-
...columns.map((column) => values[column]),
|
|
145
|
-
...fragment.params
|
|
146
|
-
]);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
};
|
|
150
|
-
const d1Executor = (database) => {
|
|
151
|
-
return {
|
|
152
|
-
all: async (sql, parameters) => {
|
|
153
|
-
const result = await database.prepare(sql).bind(...parameters).all();
|
|
154
|
-
return result.results ?? [];
|
|
155
|
-
},
|
|
156
|
-
run: async (sql, parameters) => {
|
|
157
|
-
await database.prepare(sql).bind(...parameters).run();
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
export { createSqlAuthStore, d1Executor };
|
|
1
|
+
const c=r=>`"${r.replaceAll('"','""')}"`,I={params:[],sql:"0"},E=(r,e)=>e?{column:`LOWER(${r})`,placeholder:"LOWER(?)"}:{column:r,placeholder:"?"},N=(r,e,s,n)=>{if(typeof e!="string")return I;const a=E(r,n);return s==="contains"?{params:[e],sql:`instr(${a.column}, ${a.placeholder}) > 0`}:s==="starts_with"?{params:[e],sql:`instr(${a.column}, ${a.placeholder}) = 1`}:{params:[e,e],sql:`substr(${a.column}, -length(?)) = ${a.placeholder}`}},R=(r,e,s,n)=>{if(e===null)return{params:[],sql:`${r} IS ${n?"NOT ":""}NULL`};const a=E(r,s),t=n?"<>":"=";return{params:[e],sql:`${a.column} ${t} ${a.placeholder}`}},T=(r,e,s,n)=>{const a=Array.isArray(e)?e:[];if(a.length===0)return{params:[],sql:s?"1":"0"};const t=E(r,n),l=a.map(()=>t.placeholder).join(", ");return{params:[...a],sql:`${t.column} ${s?"NOT IN":"IN"} (${l})`}},d=r=>{const e=c(r.field),{mode:s,operator:n,value:a}=r,t=s==="insensitive";switch(n){case"contains":case"ends_with":case"starts_with":return N(e,a,n,t);case"gt":return{params:[a],sql:`${e} > ?`};case"gte":return{params:[a],sql:`${e} >= ?`};case"in":return T(e,a,!1,t);case"lt":return{params:[a],sql:`${e} < ?`};case"lte":return{params:[a],sql:`${e} <= ?`};case"ne":return R(e,a,t,!0);case"not_in":return T(e,a,!0,t);default:return R(e,a,t,!1)}},p=r=>{if(r.length===0)return{params:[],sql:""};let e=d(r[0]);for(const s of r.slice(1)){const n=d(s),a=s.connector==="OR"?"OR":"AND";e={params:[...e.params,...n.params],sql:`(${e.sql} ${a} ${n.sql})`}}return e},$=r=>r.sql?` WHERE ${r.sql}`:"",f=r=>{const e=(s,n)=>{const a=p(n);return r.all(`SELECT * FROM ${c(s)}${$(a)}`,a.params)};return{consumeOne:async(s,n)=>{const a=p(n),t=c(s),[l]=await r.all(`DELETE FROM ${t} WHERE rowid IN (SELECT rowid FROM ${t}${$(a)} LIMIT 1) RETURNING *`,a.params);return l},count:async(s,n)=>{const a=p(n),[t]=await r.all(`SELECT COUNT(*) AS __count FROM ${c(s)}${$(a)}`,a.params);return Number(t?.__count??0)},create:async(s,n)=>{const a=Object.keys(n),t=a.map(()=>"?").join(", "),l=`INSERT INTO ${c(s)} (${a.map(o=>c(o)).join(", ")}) VALUES (${t})`;return await r.run(l,a.map(o=>n[o])),{...n}},incrementOne:async(s,n,a,t)=>{const l=c(s),o=Object.keys(a),i=t?Object.keys(t):[],m=p(n);if(o.length===0&&i.length===0){const[u]=await r.all(`SELECT * FROM ${l}${$(m)} LIMIT 1`,m.params);return u}const h=[...o.map(u=>`${c(u)} = COALESCE(${c(u)}, 0) + ?`),...i.map(u=>`${c(u)} = ?`)].join(", "),[O]=await r.all(`UPDATE ${l} SET ${h} WHERE rowid IN (SELECT rowid FROM ${l}${$(m)} LIMIT 1) RETURNING *`,[...o.map(u=>a[u]),...i.map(u=>t[u]),...m.params]);return O},read:async(s,n)=>{const a=p(n.where),t=[...a.params];let l=`SELECT * FROM ${c(s)}${$(a)}`;return n.sortBy&&(l+=` ORDER BY ${c(n.sortBy.field)} ${n.sortBy.direction==="asc"?"ASC":"DESC"}`),n.limit!==void 0&&(l+=" LIMIT ?",t.push(Math.trunc(n.limit))),n.offset&&(l+=`${n.limit===void 0?" LIMIT -1":""} OFFSET ?`,t.push(Math.trunc(n.offset))),r.all(l,t)},remove:async(s,n)=>{const a=p(n);return(await r.all(`DELETE FROM ${c(s)}${$(a)} RETURNING *`,a.params)).length},update:async(s,n,a)=>{const t=Object.keys(a);if(t.length===0)return e(s,n);const l=t.map(i=>`${c(i)} = ?`).join(", "),o=p(n);return r.all(`UPDATE ${c(s)} SET ${l}${$(o)} RETURNING *`,[...t.map(i=>a[i]),...o.params])}}},q=r=>({all:async(e,s)=>(await r.prepare(e).bind(...s).all()).results??[],run:async(e,s)=>{await r.prepare(e).bind(...s).run()}});export{f as createSqlAuthStore,q as d1Executor};
|
package/dist/store.d.mts
CHANGED
|
@@ -2,10 +2,10 @@ import { CustomAdapter } from 'better-auth/adapters';
|
|
|
2
2
|
/** A stored auth row — an opaque bag of columns keyed by better-auth field name. */
|
|
3
3
|
type AuthRow = Record<string, unknown>;
|
|
4
4
|
/**
|
|
5
|
-
* One normalized better-auth where clause. Derived from {@link CustomAdapter}'s
|
|
6
|
-
* own method signature (rather than re-declared) so a better-auth change to the
|
|
7
|
-
* clause shape surfaces as a compile error here, not a silent mis-match.
|
|
8
|
-
*/
|
|
5
|
+
* One normalized better-auth where clause. Derived from {@link CustomAdapter}'s
|
|
6
|
+
* own method signature (rather than re-declared) so a better-auth change to the
|
|
7
|
+
* clause shape surfaces as a compile error here, not a silent mis-match.
|
|
8
|
+
*/
|
|
9
9
|
type AuthWhereClause = NonNullable<Parameters<CustomAdapter["findOne"]>[0]["where"]>[number];
|
|
10
10
|
/** Read query handed to {@link AuthStore.read}: a where filter plus optional sort/window. */
|
|
11
11
|
interface AuthQuery {
|
|
@@ -18,31 +18,49 @@ interface AuthQuery {
|
|
|
18
18
|
where: ReadonlyArray<AuthWhereClause>;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* The minimal table-addressed store the `lunoraAuthAdapter` drives. This is the
|
|
22
|
-
* seam a Lunora runtime binds to its ORM: back each method with `ctx.db` over
|
|
23
|
-
* the global (D1) auth tables that `authTables(...)` generates, and better-auth's
|
|
24
|
-
* reads/writes flow through Lunora's data layer (triggers, aggregates, OCC)
|
|
25
|
-
* instead of better-auth's own adapter. Field names and table (`model`) names are
|
|
26
|
-
* already the database names better-auth resolved from the schema, so a store
|
|
27
|
-
* passes them straight through.
|
|
28
|
-
*
|
|
29
|
-
* {@link createMemoryAuthStore} is a reference in-memory implementation (also
|
|
30
|
-
* what the tests run better-auth against); `createSqlAuthStore` is the SQL one.
|
|
31
|
-
*/
|
|
21
|
+
* The minimal table-addressed store the `lunoraAuthAdapter` drives. This is the
|
|
22
|
+
* seam a Lunora runtime binds to its ORM: back each method with `ctx.db` over
|
|
23
|
+
* the global (D1) auth tables that `authTables(...)` generates, and better-auth's
|
|
24
|
+
* reads/writes flow through Lunora's data layer (triggers, aggregates, OCC)
|
|
25
|
+
* instead of better-auth's own adapter. Field names and table (`model`) names are
|
|
26
|
+
* already the database names better-auth resolved from the schema, so a store
|
|
27
|
+
* passes them straight through.
|
|
28
|
+
*
|
|
29
|
+
* {@link createMemoryAuthStore} is a reference in-memory implementation (also
|
|
30
|
+
* what the tests run better-auth against); `createSqlAuthStore` is the SQL one.
|
|
31
|
+
*/
|
|
32
32
|
interface AuthStore {
|
|
33
33
|
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
* Atomically delete **at most one** row in `model` matching `where` and
|
|
35
|
+
* return it (or `undefined` if none matched). Backs better-auth's
|
|
36
|
+
* single-use-token consume (OTP / magic-link / email-verification /
|
|
37
|
+
* password-reset): implementing it natively — one round trip that finds and
|
|
38
|
+
* deletes in a single statement — closes the read-then-delete race the
|
|
39
|
+
* factory's `findMany` + `deleteMany` fallback would otherwise leave open.
|
|
40
|
+
*/
|
|
41
41
|
consumeOne: (model: string, where: ReadonlyArray<AuthWhereClause>) => Promise<AuthRow | undefined>;
|
|
42
42
|
/** Count rows in `model` matching `where` (empty `where` = all rows). */
|
|
43
43
|
count: (model: string, where: ReadonlyArray<AuthWhereClause>) => Promise<number>;
|
|
44
44
|
/** Insert `data` into `model`; return the stored row (the adapter pre-fills `id`). */
|
|
45
45
|
create: (model: string, data: AuthRow) => Promise<AuthRow>;
|
|
46
|
+
/**
|
|
47
|
+
* Atomically apply signed numeric deltas to **at most one** row in `model`
|
|
48
|
+
* matching `where`, then return the updated row (or `undefined` if the guard
|
|
49
|
+
* matched none). For each `increment` entry it applies `field = field + delta`
|
|
50
|
+
* (a negative delta decrements); the optional `set` map assigns absolute
|
|
51
|
+
* values in the same step. The `where` clause is both selector **and** guard
|
|
52
|
+
* — comparison operators are honoured, so a guard like
|
|
53
|
+
* `{ field: "count", operator: "lt", value: max }` only mutates the row while
|
|
54
|
+
* it still satisfies the predicate.
|
|
55
|
+
*
|
|
56
|
+
* Backs better-auth's durable (`storage: "database"`) rate limiter, whose
|
|
57
|
+
* counter rides these tables. Implementing it natively — one statement that
|
|
58
|
+
* guards, increments, and returns — gives the **one-winner-across-isolates**
|
|
59
|
+
* guarantee the read-then-update fallback cannot: on Workers two concurrent
|
|
60
|
+
* requests would otherwise both read `count=4` and both write `5`, letting a
|
|
61
|
+
* `max` of 5 pass 6+. Same race-closing rationale as {@link AuthStore.consumeOne}.
|
|
62
|
+
*/
|
|
63
|
+
incrementOne: (model: string, where: ReadonlyArray<AuthWhereClause>, increment: Record<string, number>, set?: AuthRow) => Promise<AuthRow | undefined>;
|
|
46
64
|
/** Read rows from `model` honouring the filter/sort/window in `query`. */
|
|
47
65
|
read: (model: string, query: AuthQuery) => Promise<AuthRow[]>;
|
|
48
66
|
/** Delete rows in `model` matching `where`; return how many were removed. */
|
|
@@ -51,16 +69,16 @@ interface AuthStore {
|
|
|
51
69
|
update: (model: string, where: ReadonlyArray<AuthWhereClause>, values: AuthRow) => Promise<AuthRow[]>;
|
|
52
70
|
}
|
|
53
71
|
/**
|
|
54
|
-
* Evaluate a better-auth where clause list against a row. Clauses fold
|
|
55
|
-
* left-to-right by their `connector` (`AND` by default, `OR` when set) — the
|
|
56
|
-
* same precedence better-auth's own adapters use. An empty list matches every
|
|
57
|
-
* row. Exported so any in-memory-style {@link AuthStore} can reuse it.
|
|
58
|
-
*/
|
|
72
|
+
* Evaluate a better-auth where clause list against a row. Clauses fold
|
|
73
|
+
* left-to-right by their `connector` (`AND` by default, `OR` when set) — the
|
|
74
|
+
* same precedence better-auth's own adapters use. An empty list matches every
|
|
75
|
+
* row. Exported so any in-memory-style {@link AuthStore} can reuse it.
|
|
76
|
+
*/
|
|
59
77
|
declare const matchesWhere: (row: AuthRow, where: ReadonlyArray<AuthWhereClause>) => boolean;
|
|
60
78
|
/**
|
|
61
|
-
* Reference in-memory {@link AuthStore} — used to run better-auth end to end in
|
|
62
|
-
* tests, and a worked example of the contract a Lunora-`ctx.db`-backed store
|
|
63
|
-
* fulfils. Not for production (state is per-instance and non-durable).
|
|
64
|
-
*/
|
|
79
|
+
* Reference in-memory {@link AuthStore} — used to run better-auth end to end in
|
|
80
|
+
* tests, and a worked example of the contract a Lunora-`ctx.db`-backed store
|
|
81
|
+
* fulfils. Not for production (state is per-instance and non-durable).
|
|
82
|
+
*/
|
|
65
83
|
declare const createMemoryAuthStore: () => AuthStore;
|
|
66
84
|
export { AuthQuery, type AuthRow, AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere };
|
package/dist/store.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { CustomAdapter } from 'better-auth/adapters';
|
|
|
2
2
|
/** A stored auth row — an opaque bag of columns keyed by better-auth field name. */
|
|
3
3
|
type AuthRow = Record<string, unknown>;
|
|
4
4
|
/**
|
|
5
|
-
* One normalized better-auth where clause. Derived from {@link CustomAdapter}'s
|
|
6
|
-
* own method signature (rather than re-declared) so a better-auth change to the
|
|
7
|
-
* clause shape surfaces as a compile error here, not a silent mis-match.
|
|
8
|
-
*/
|
|
5
|
+
* One normalized better-auth where clause. Derived from {@link CustomAdapter}'s
|
|
6
|
+
* own method signature (rather than re-declared) so a better-auth change to the
|
|
7
|
+
* clause shape surfaces as a compile error here, not a silent mis-match.
|
|
8
|
+
*/
|
|
9
9
|
type AuthWhereClause = NonNullable<Parameters<CustomAdapter["findOne"]>[0]["where"]>[number];
|
|
10
10
|
/** Read query handed to {@link AuthStore.read}: a where filter plus optional sort/window. */
|
|
11
11
|
interface AuthQuery {
|
|
@@ -18,31 +18,49 @@ interface AuthQuery {
|
|
|
18
18
|
where: ReadonlyArray<AuthWhereClause>;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* The minimal table-addressed store the `lunoraAuthAdapter` drives. This is the
|
|
22
|
-
* seam a Lunora runtime binds to its ORM: back each method with `ctx.db` over
|
|
23
|
-
* the global (D1) auth tables that `authTables(...)` generates, and better-auth's
|
|
24
|
-
* reads/writes flow through Lunora's data layer (triggers, aggregates, OCC)
|
|
25
|
-
* instead of better-auth's own adapter. Field names and table (`model`) names are
|
|
26
|
-
* already the database names better-auth resolved from the schema, so a store
|
|
27
|
-
* passes them straight through.
|
|
28
|
-
*
|
|
29
|
-
* {@link createMemoryAuthStore} is a reference in-memory implementation (also
|
|
30
|
-
* what the tests run better-auth against); `createSqlAuthStore` is the SQL one.
|
|
31
|
-
*/
|
|
21
|
+
* The minimal table-addressed store the `lunoraAuthAdapter` drives. This is the
|
|
22
|
+
* seam a Lunora runtime binds to its ORM: back each method with `ctx.db` over
|
|
23
|
+
* the global (D1) auth tables that `authTables(...)` generates, and better-auth's
|
|
24
|
+
* reads/writes flow through Lunora's data layer (triggers, aggregates, OCC)
|
|
25
|
+
* instead of better-auth's own adapter. Field names and table (`model`) names are
|
|
26
|
+
* already the database names better-auth resolved from the schema, so a store
|
|
27
|
+
* passes them straight through.
|
|
28
|
+
*
|
|
29
|
+
* {@link createMemoryAuthStore} is a reference in-memory implementation (also
|
|
30
|
+
* what the tests run better-auth against); `createSqlAuthStore` is the SQL one.
|
|
31
|
+
*/
|
|
32
32
|
interface AuthStore {
|
|
33
33
|
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
* Atomically delete **at most one** row in `model` matching `where` and
|
|
35
|
+
* return it (or `undefined` if none matched). Backs better-auth's
|
|
36
|
+
* single-use-token consume (OTP / magic-link / email-verification /
|
|
37
|
+
* password-reset): implementing it natively — one round trip that finds and
|
|
38
|
+
* deletes in a single statement — closes the read-then-delete race the
|
|
39
|
+
* factory's `findMany` + `deleteMany` fallback would otherwise leave open.
|
|
40
|
+
*/
|
|
41
41
|
consumeOne: (model: string, where: ReadonlyArray<AuthWhereClause>) => Promise<AuthRow | undefined>;
|
|
42
42
|
/** Count rows in `model` matching `where` (empty `where` = all rows). */
|
|
43
43
|
count: (model: string, where: ReadonlyArray<AuthWhereClause>) => Promise<number>;
|
|
44
44
|
/** Insert `data` into `model`; return the stored row (the adapter pre-fills `id`). */
|
|
45
45
|
create: (model: string, data: AuthRow) => Promise<AuthRow>;
|
|
46
|
+
/**
|
|
47
|
+
* Atomically apply signed numeric deltas to **at most one** row in `model`
|
|
48
|
+
* matching `where`, then return the updated row (or `undefined` if the guard
|
|
49
|
+
* matched none). For each `increment` entry it applies `field = field + delta`
|
|
50
|
+
* (a negative delta decrements); the optional `set` map assigns absolute
|
|
51
|
+
* values in the same step. The `where` clause is both selector **and** guard
|
|
52
|
+
* — comparison operators are honoured, so a guard like
|
|
53
|
+
* `{ field: "count", operator: "lt", value: max }` only mutates the row while
|
|
54
|
+
* it still satisfies the predicate.
|
|
55
|
+
*
|
|
56
|
+
* Backs better-auth's durable (`storage: "database"`) rate limiter, whose
|
|
57
|
+
* counter rides these tables. Implementing it natively — one statement that
|
|
58
|
+
* guards, increments, and returns — gives the **one-winner-across-isolates**
|
|
59
|
+
* guarantee the read-then-update fallback cannot: on Workers two concurrent
|
|
60
|
+
* requests would otherwise both read `count=4` and both write `5`, letting a
|
|
61
|
+
* `max` of 5 pass 6+. Same race-closing rationale as {@link AuthStore.consumeOne}.
|
|
62
|
+
*/
|
|
63
|
+
incrementOne: (model: string, where: ReadonlyArray<AuthWhereClause>, increment: Record<string, number>, set?: AuthRow) => Promise<AuthRow | undefined>;
|
|
46
64
|
/** Read rows from `model` honouring the filter/sort/window in `query`. */
|
|
47
65
|
read: (model: string, query: AuthQuery) => Promise<AuthRow[]>;
|
|
48
66
|
/** Delete rows in `model` matching `where`; return how many were removed. */
|
|
@@ -51,16 +69,16 @@ interface AuthStore {
|
|
|
51
69
|
update: (model: string, where: ReadonlyArray<AuthWhereClause>, values: AuthRow) => Promise<AuthRow[]>;
|
|
52
70
|
}
|
|
53
71
|
/**
|
|
54
|
-
* Evaluate a better-auth where clause list against a row. Clauses fold
|
|
55
|
-
* left-to-right by their `connector` (`AND` by default, `OR` when set) — the
|
|
56
|
-
* same precedence better-auth's own adapters use. An empty list matches every
|
|
57
|
-
* row. Exported so any in-memory-style {@link AuthStore} can reuse it.
|
|
58
|
-
*/
|
|
72
|
+
* Evaluate a better-auth where clause list against a row. Clauses fold
|
|
73
|
+
* left-to-right by their `connector` (`AND` by default, `OR` when set) — the
|
|
74
|
+
* same precedence better-auth's own adapters use. An empty list matches every
|
|
75
|
+
* row. Exported so any in-memory-style {@link AuthStore} can reuse it.
|
|
76
|
+
*/
|
|
59
77
|
declare const matchesWhere: (row: AuthRow, where: ReadonlyArray<AuthWhereClause>) => boolean;
|
|
60
78
|
/**
|
|
61
|
-
* Reference in-memory {@link AuthStore} — used to run better-auth end to end in
|
|
62
|
-
* tests, and a worked example of the contract a Lunora-`ctx.db`-backed store
|
|
63
|
-
* fulfils. Not for production (state is per-instance and non-durable).
|
|
64
|
-
*/
|
|
79
|
+
* Reference in-memory {@link AuthStore} — used to run better-auth end to end in
|
|
80
|
+
* tests, and a worked example of the contract a Lunora-`ctx.db`-backed store
|
|
81
|
+
* fulfils. Not for production (state is per-instance and non-durable).
|
|
82
|
+
*/
|
|
65
83
|
declare const createMemoryAuthStore: () => AuthStore;
|
|
66
84
|
export { AuthQuery, type AuthRow, AuthStore, type AuthWhereClause, createMemoryAuthStore, matchesWhere };
|
package/dist/store.mjs
CHANGED
|
@@ -1,170 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const isNullish = (value) => value === void 0 || value === null;
|
|
3
|
-
const looseEquals = (left, right, insensitive) => {
|
|
4
|
-
if (insensitive) {
|
|
5
|
-
const a = asString(left);
|
|
6
|
-
const b = asString(right);
|
|
7
|
-
if (a !== void 0 && b !== void 0) {
|
|
8
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return left === right;
|
|
12
|
-
};
|
|
13
|
-
const matchesPattern = (cell, value, operator, insensitive) => {
|
|
14
|
-
const haystack = asString(cell);
|
|
15
|
-
const needle = asString(value);
|
|
16
|
-
if (haystack === void 0 || needle === void 0) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
const [a, b] = insensitive ? [haystack.toLowerCase(), needle.toLowerCase()] : [haystack, needle];
|
|
20
|
-
if (operator === "contains") {
|
|
21
|
-
return a.includes(b);
|
|
22
|
-
}
|
|
23
|
-
return operator === "starts_with" ? a.startsWith(b) : a.endsWith(b);
|
|
24
|
-
};
|
|
25
|
-
const evaluateClause = (row, clause) => {
|
|
26
|
-
const { field, mode, operator, value } = clause;
|
|
27
|
-
const cell = row[field];
|
|
28
|
-
const insensitive = mode === "insensitive";
|
|
29
|
-
switch (operator) {
|
|
30
|
-
case "contains":
|
|
31
|
-
case "ends_with":
|
|
32
|
-
case "starts_with": {
|
|
33
|
-
return matchesPattern(cell, value, operator, insensitive);
|
|
34
|
-
}
|
|
35
|
-
case "gt": {
|
|
36
|
-
return !isNullish(value) && cell > value;
|
|
37
|
-
}
|
|
38
|
-
case "gte": {
|
|
39
|
-
return !isNullish(value) && cell >= value;
|
|
40
|
-
}
|
|
41
|
-
case "in": {
|
|
42
|
-
return Array.isArray(value) && value.some((entry) => looseEquals(cell, entry, insensitive));
|
|
43
|
-
}
|
|
44
|
-
case "lt": {
|
|
45
|
-
return !isNullish(value) && cell < value;
|
|
46
|
-
}
|
|
47
|
-
case "lte": {
|
|
48
|
-
return !isNullish(value) && cell <= value;
|
|
49
|
-
}
|
|
50
|
-
case "ne": {
|
|
51
|
-
return !looseEquals(cell, value, insensitive);
|
|
52
|
-
}
|
|
53
|
-
case "not_in": {
|
|
54
|
-
return Array.isArray(value) && !value.some((entry) => looseEquals(cell, entry, insensitive));
|
|
55
|
-
}
|
|
56
|
-
// "eq" and the default: null-aware equality.
|
|
57
|
-
default: {
|
|
58
|
-
return isNullish(value) ? isNullish(cell) : looseEquals(cell, value, insensitive);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const compareCells = (a, b) => {
|
|
63
|
-
if (typeof a === "string" && typeof b === "string") {
|
|
64
|
-
return a.localeCompare(b);
|
|
65
|
-
}
|
|
66
|
-
const left = Number(a);
|
|
67
|
-
const right = Number(b);
|
|
68
|
-
if (left < right) {
|
|
69
|
-
return -1;
|
|
70
|
-
}
|
|
71
|
-
return left > right ? 1 : 0;
|
|
72
|
-
};
|
|
73
|
-
const sortRows = (rows, sortBy) => {
|
|
74
|
-
const direction = sortBy.direction === "asc" ? 1 : -1;
|
|
75
|
-
return rows.toSorted((left, right) => {
|
|
76
|
-
const a = left[sortBy.field];
|
|
77
|
-
const b = right[sortBy.field];
|
|
78
|
-
if (isNullish(a) && isNullish(b)) {
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
if (isNullish(a)) {
|
|
82
|
-
return -direction;
|
|
83
|
-
}
|
|
84
|
-
if (isNullish(b)) {
|
|
85
|
-
return direction;
|
|
86
|
-
}
|
|
87
|
-
return compareCells(a, b) * direction;
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
const matchesWhere = (row, where) => {
|
|
91
|
-
let result = true;
|
|
92
|
-
for (const [index, clause] of where.entries()) {
|
|
93
|
-
const clauseResult = evaluateClause(row, clause);
|
|
94
|
-
if (index === 0) {
|
|
95
|
-
result = clauseResult;
|
|
96
|
-
} else if (clause.connector === "OR") {
|
|
97
|
-
result = result || clauseResult;
|
|
98
|
-
} else {
|
|
99
|
-
result = result && clauseResult;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
};
|
|
104
|
-
const createMemoryAuthStore = () => {
|
|
105
|
-
const tables = /* @__PURE__ */ new Map();
|
|
106
|
-
const tableOf = (model) => {
|
|
107
|
-
const existing = tables.get(model);
|
|
108
|
-
if (existing) {
|
|
109
|
-
return existing;
|
|
110
|
-
}
|
|
111
|
-
const created = [];
|
|
112
|
-
tables.set(model, created);
|
|
113
|
-
return created;
|
|
114
|
-
};
|
|
115
|
-
return {
|
|
116
|
-
consumeOne: (model, where) => {
|
|
117
|
-
const table = tableOf(model);
|
|
118
|
-
const index = table.findIndex((row2) => matchesWhere(row2, where));
|
|
119
|
-
if (index === -1) {
|
|
120
|
-
return Promise.resolve(void 0);
|
|
121
|
-
}
|
|
122
|
-
const [row] = table.splice(index, 1);
|
|
123
|
-
return Promise.resolve(row ? { ...row } : void 0);
|
|
124
|
-
},
|
|
125
|
-
count: (model, where) => Promise.resolve(tableOf(model).filter((row) => matchesWhere(row, where)).length),
|
|
126
|
-
create: (model, data) => {
|
|
127
|
-
const row = { ...data };
|
|
128
|
-
tableOf(model).push(row);
|
|
129
|
-
return Promise.resolve({ ...row });
|
|
130
|
-
},
|
|
131
|
-
read: (model, query) => {
|
|
132
|
-
let rows = tableOf(model).filter((row) => matchesWhere(row, query.where));
|
|
133
|
-
if (query.sortBy) {
|
|
134
|
-
rows = sortRows(rows, query.sortBy);
|
|
135
|
-
}
|
|
136
|
-
if (query.offset) {
|
|
137
|
-
rows = rows.slice(query.offset);
|
|
138
|
-
}
|
|
139
|
-
if (query.limit !== void 0) {
|
|
140
|
-
rows = rows.slice(0, query.limit);
|
|
141
|
-
}
|
|
142
|
-
return Promise.resolve(
|
|
143
|
-
rows.map((row) => {
|
|
144
|
-
return { ...row };
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
|
-
},
|
|
148
|
-
remove: (model, where) => {
|
|
149
|
-
const table = tableOf(model);
|
|
150
|
-
const kept = table.filter((row) => !matchesWhere(row, where));
|
|
151
|
-
const removed = table.length - kept.length;
|
|
152
|
-
table.length = 0;
|
|
153
|
-
table.push(...kept);
|
|
154
|
-
return Promise.resolve(removed);
|
|
155
|
-
},
|
|
156
|
-
update: (model, where, values) => {
|
|
157
|
-
const matched = tableOf(model).filter((row) => matchesWhere(row, where));
|
|
158
|
-
for (const row of matched) {
|
|
159
|
-
Object.assign(row, values);
|
|
160
|
-
}
|
|
161
|
-
return Promise.resolve(
|
|
162
|
-
matched.map((row) => {
|
|
163
|
-
return { ...row };
|
|
164
|
-
})
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
export { createMemoryAuthStore, matchesWhere };
|
|
1
|
+
const m=i=>typeof i=="string"?i:void 0,a=i=>i==null,f=(i,n,r)=>{if(r){const o=m(i),t=m(n);if(o!==void 0&&t!==void 0)return o.toLowerCase()===t.toLowerCase()}return i===n},d=(i,n,r,o)=>{const t=m(i),e=m(n);if(t===void 0||e===void 0)return!1;const[s,c]=o?[t.toLowerCase(),e.toLowerCase()]:[t,e];return r==="contains"?s.includes(c):r==="starts_with"?s.startsWith(c):s.endsWith(c)},v=(i,n)=>{const{field:r,mode:o,operator:t,value:e}=n,s=i[r],c=o==="insensitive";switch(t){case"contains":case"ends_with":case"starts_with":return d(s,e,t,c);case"gt":return!a(e)&&s>e;case"gte":return!a(e)&&s>=e;case"in":return Array.isArray(e)&&e.some(l=>f(s,l,c));case"lt":return!a(e)&&s<e;case"lte":return!a(e)&&s<=e;case"ne":return!f(s,e,c);case"not_in":return Array.isArray(e)&&!e.some(l=>f(s,l,c));default:return a(e)?a(s):f(s,e,c)}},h=(i,n)=>{if(typeof i=="string"&&typeof n=="string")return i.localeCompare(n);const r=Number(i),o=Number(n);return r<o?-1:r>o?1:0},p=(i,n)=>{const r=n.direction==="asc"?1:-1;return i.toSorted((o,t)=>{const e=o[n.field],s=t[n.field];return a(e)&&a(s)?0:a(e)?-r:a(s)?r:h(e,s)*r})},u=(i,n)=>{let r=!0;for(const[o,t]of n.entries()){const e=v(i,t);o===0?r=e:t.connector==="OR"?r=r||e:r=r&&e}return r},g=()=>{const i=new Map,n=r=>{const o=i.get(r);if(o)return o;const t=[];return i.set(r,t),t};return{consumeOne:(r,o)=>{const t=n(r),e=t.findIndex(c=>u(c,o));if(e===-1)return Promise.resolve(void 0);const[s]=t.splice(e,1);return Promise.resolve(s?{...s}:void 0)},count:(r,o)=>Promise.resolve(n(r).filter(t=>u(t,o)).length),create:(r,o)=>{const t={...o};return n(r).push(t),Promise.resolve({...t})},incrementOne:(r,o,t,e)=>{const s=n(r).find(c=>u(c,o));if(!s)return Promise.resolve(void 0);for(const[c,l]of Object.entries(t))s[c]=(typeof s[c]=="number"?s[c]:0)+l;return e&&Object.assign(s,e),Promise.resolve({...s})},read:(r,o)=>{let t=n(r).filter(e=>u(e,o.where));return o.sortBy&&(t=p(t,o.sortBy)),o.offset&&(t=t.slice(o.offset)),o.limit!==void 0&&(t=t.slice(0,o.limit)),Promise.resolve(t.map(e=>({...e})))},remove:(r,o)=>{const t=n(r),e=t.filter(c=>!u(c,o)),s=t.length-e.length;return t.length=0,t.push(...e),Promise.resolve(s)},update:(r,o,t)=>{const e=n(r).filter(s=>u(s,o));for(const s of e)Object.assign(s,t);return Promise.resolve(e.map(s=>({...s})))}}};export{g as createMemoryAuthStore,u as matchesWhere};
|