@madauth/server 0.1.0

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/index.js ADDED
@@ -0,0 +1,32 @@
1
+ import {
2
+ columnName,
3
+ createTablesSql,
4
+ madauthSchema,
5
+ tableName
6
+ } from "./chunk-3WIWWKRI.js";
7
+ import {
8
+ REDIRECT_ERROR_PARAM,
9
+ createApp
10
+ } from "./chunk-TEMZRMFF.js";
11
+ import "./chunk-PRRKLEW7.js";
12
+ import {
13
+ ConfigError,
14
+ envVars,
15
+ loadConfig
16
+ } from "./chunk-V2XKGETK.js";
17
+ import {
18
+ generateSigningKey
19
+ } from "./chunk-JDQLSL36.js";
20
+ import "./chunk-AVZE6FFR.js";
21
+ export {
22
+ ConfigError,
23
+ REDIRECT_ERROR_PARAM,
24
+ columnName,
25
+ createApp,
26
+ createTablesSql,
27
+ envVars,
28
+ generateSigningKey,
29
+ loadConfig,
30
+ madauthSchema,
31
+ tableName
32
+ };
@@ -0,0 +1,137 @@
1
+ /** A value of a record field. */
2
+ type Value = string | number | boolean | null;
3
+ /** A record: field name → value. Field names are the camelCase names from {@link madauthSchema}. */
4
+ type Row = Record<string, Value>;
5
+ /** Filter: every field must equal the given value (`null` matches empty fields). */
6
+ type Where = Record<string, Value>;
7
+ /**
8
+ * Stores madAuth's records in a database. madAuth does all the security work (hashing, throttling,
9
+ * single-use links); an adapter only stores the models of {@link madauthSchema}.
10
+ *
11
+ * Check an implementation with `storeAdapterContract` from `@madauth/server/testing`.
12
+ */
13
+ interface StoreAdapter {
14
+ /** Inserts a record. Resolves to false and writes nothing if a `unique` or primary-key value already exists. */
15
+ create(model: string, data: Row): Promise<boolean>;
16
+ findOne(model: string, where: Where): Promise<Row | null>;
17
+ findMany(model: string, where: Where, opts?: {
18
+ limit?: number;
19
+ }): Promise<Row[]>;
20
+ /** Changes the given fields of the matching records. Resolves to the number of records changed. */
21
+ update(model: string, where: Where, patch: Row): Promise<number>;
22
+ /**
23
+ * Deletes the matching records. Resolves to the number of records deleted; madAuth relies on this count
24
+ * to use a link or code only once, so it must be exact when two requests delete the same record.
25
+ */
26
+ delete(model: string, where: Where): Promise<number>;
27
+ }
28
+ type FieldType = 'string' | 'number' | 'boolean';
29
+ interface FieldDef {
30
+ type: FieldType;
31
+ primaryKey?: boolean;
32
+ unique?: boolean;
33
+ /** Looked up often, so it should have an index. */
34
+ index?: boolean;
35
+ nullable?: boolean;
36
+ }
37
+ interface Schema {
38
+ /** Increases with every change to the models. */
39
+ version: number;
40
+ models: Record<string, {
41
+ fields: Record<string, FieldDef>;
42
+ }>;
43
+ }
44
+ /** The records madAuth stores. Timestamps are milliseconds since 1970. */
45
+ declare const madauthSchema: {
46
+ readonly version: 1;
47
+ readonly models: {
48
+ readonly user: {
49
+ readonly fields: {
50
+ readonly id: {
51
+ readonly type: "string";
52
+ readonly primaryKey: true;
53
+ };
54
+ readonly email: {
55
+ readonly type: "string";
56
+ };
57
+ readonly emailNormalized: {
58
+ readonly type: "string";
59
+ readonly unique: true;
60
+ };
61
+ readonly emailVerified: {
62
+ readonly type: "boolean";
63
+ };
64
+ readonly name: {
65
+ readonly type: "string";
66
+ readonly nullable: true;
67
+ };
68
+ readonly sessionVersion: {
69
+ readonly type: "number";
70
+ };
71
+ readonly lastMailAt: {
72
+ readonly type: "number";
73
+ };
74
+ readonly createdAt: {
75
+ readonly type: "number";
76
+ };
77
+ };
78
+ };
79
+ /** How a user signs in. `key` is e.g. `password:<userId>`; later `google:<sub>` or `totp:<userId>`. */
80
+ readonly account: {
81
+ readonly fields: {
82
+ readonly id: {
83
+ readonly type: "string";
84
+ readonly primaryKey: true;
85
+ };
86
+ readonly userId: {
87
+ readonly type: "string";
88
+ readonly index: true;
89
+ };
90
+ readonly key: {
91
+ readonly type: "string";
92
+ readonly unique: true;
93
+ };
94
+ readonly secret: {
95
+ readonly type: "string";
96
+ readonly nullable: true;
97
+ };
98
+ readonly failedAttempts: {
99
+ readonly type: "number";
100
+ };
101
+ readonly lockedUntil: {
102
+ readonly type: "number";
103
+ };
104
+ readonly createdAt: {
105
+ readonly type: "number";
106
+ };
107
+ };
108
+ };
109
+ /** A pending e-mail verification or password reset. `id` is the SHA-256 of the link token. */
110
+ readonly verification: {
111
+ readonly fields: {
112
+ readonly id: {
113
+ readonly type: "string";
114
+ readonly primaryKey: true;
115
+ };
116
+ readonly userId: {
117
+ readonly type: "string";
118
+ readonly index: true;
119
+ };
120
+ readonly purpose: {
121
+ readonly type: "string";
122
+ };
123
+ readonly codeHash: {
124
+ readonly type: "string";
125
+ };
126
+ readonly attempts: {
127
+ readonly type: "number";
128
+ };
129
+ readonly expiresAt: {
130
+ readonly type: "number";
131
+ };
132
+ };
133
+ };
134
+ };
135
+ };
136
+
137
+ export { type FieldDef as F, type Row as R, type StoreAdapter as S, type Value as V, type Where as W, type Schema as a, type FieldType as b, madauthSchema as m };
package/dist/sqlite.js ADDED
@@ -0,0 +1,100 @@
1
+ import {
2
+ columnName,
3
+ createTablesSql,
4
+ madauthSchema,
5
+ tableName
6
+ } from "./chunk-3WIWWKRI.js";
7
+
8
+ // src/store/sqlite.ts
9
+ function createSqliteAdapter(path) {
10
+ const { DatabaseSync } = process.getBuiltinModule("node:sqlite");
11
+ const db = new DatabaseSync(path);
12
+ db.exec("PRAGMA busy_timeout = 5000");
13
+ if (path !== ":memory:") db.exec("PRAGMA journal_mode = WAL");
14
+ migrate(db);
15
+ const fieldsOf = (model) => {
16
+ const fields = madauthSchema.models[model]?.fields;
17
+ if (!fields) throw new Error(`Unknown model "${model}"`);
18
+ return fields;
19
+ };
20
+ const column = (fields, field) => {
21
+ if (!fields[field]) throw new Error(`Unknown field "${field}"`);
22
+ return columnName(field);
23
+ };
24
+ const toSql = (value) => typeof value === "boolean" ? value ? 1 : 0 : value;
25
+ const whereSql = (fields, where) => {
26
+ const parts = [];
27
+ const params = [];
28
+ for (const [field, value] of Object.entries(where)) {
29
+ if (value === null) {
30
+ parts.push(`${column(fields, field)} IS NULL`);
31
+ } else {
32
+ parts.push(`${column(fields, field)} = ?`);
33
+ params.push(toSql(value));
34
+ }
35
+ }
36
+ return { sql: parts.length ? ` WHERE ${parts.join(" AND ")}` : "", params };
37
+ };
38
+ const fromSql = (fields, raw) => {
39
+ const row = {};
40
+ for (const [field, def] of Object.entries(fields)) {
41
+ const value = raw[columnName(field)];
42
+ row[field] = def.type === "boolean" && value !== null ? value === 1 : value;
43
+ }
44
+ return row;
45
+ };
46
+ return {
47
+ async create(model, data) {
48
+ const fields = fieldsOf(model);
49
+ const names = Object.keys(data);
50
+ const sql = `INSERT INTO ${tableName(model)} (${names.map((f) => column(fields, f)).join(", ")}) VALUES (${names.map(() => "?").join(", ")}) ON CONFLICT DO NOTHING`;
51
+ return Number(db.prepare(sql).run(...names.map((f) => toSql(data[f]))).changes) === 1;
52
+ },
53
+ async findOne(model, where) {
54
+ const fields = fieldsOf(model);
55
+ const w = whereSql(fields, where);
56
+ const raw = db.prepare(`SELECT * FROM ${tableName(model)}${w.sql} LIMIT 1`).get(...w.params);
57
+ return raw ? fromSql(fields, raw) : null;
58
+ },
59
+ async findMany(model, where, opts = {}) {
60
+ const fields = fieldsOf(model);
61
+ const w = whereSql(fields, where);
62
+ const limit = opts.limit === void 0 ? "" : ` LIMIT ${Math.max(0, Math.floor(opts.limit))}`;
63
+ return db.prepare(`SELECT * FROM ${tableName(model)}${w.sql}${limit}`).all(...w.params).map((raw) => fromSql(fields, raw));
64
+ },
65
+ async update(model, where, patch) {
66
+ const fields = fieldsOf(model);
67
+ const names = Object.keys(patch);
68
+ if (!names.length) return 0;
69
+ const w = whereSql(fields, where);
70
+ const sql = `UPDATE ${tableName(model)} SET ${names.map((f) => `${column(fields, f)} = ?`).join(", ")}${w.sql}`;
71
+ return Number(db.prepare(sql).run(...names.map((f) => toSql(patch[f])), ...w.params).changes);
72
+ },
73
+ async delete(model, where) {
74
+ const fields = fieldsOf(model);
75
+ const w = whereSql(fields, where);
76
+ return Number(db.prepare(`DELETE FROM ${tableName(model)}${w.sql}`).run(...w.params).changes);
77
+ }
78
+ };
79
+ }
80
+ function migrate(db) {
81
+ const { user_version: version } = db.prepare("PRAGMA user_version").get();
82
+ if (version > madauthSchema.version) {
83
+ throw new Error(
84
+ `The database uses madAuth schema version ${version}, but this madAuth only knows version ${madauthSchema.version}. Update madAuth.`
85
+ );
86
+ }
87
+ if (version === madauthSchema.version) return;
88
+ db.exec("BEGIN");
89
+ try {
90
+ db.exec(createTablesSql("sqlite"));
91
+ db.exec(`PRAGMA user_version = ${madauthSchema.version}`);
92
+ db.exec("COMMIT");
93
+ } catch (e) {
94
+ db.exec("ROLLBACK");
95
+ throw e;
96
+ }
97
+ }
98
+ export {
99
+ createSqliteAdapter
100
+ };