@madauth/server 0.1.0 → 0.2.0-beta.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.
@@ -3,10 +3,12 @@ import {
3
3
  generateSigningKey
4
4
  } from "./chunk-JDQLSL36.js";
5
5
  import {
6
+ DEV_WEBHOOK_RECEIVER_URL,
7
+ REQUIRED_EMAIL_TYPES,
6
8
  WEBHOOK_TYPES,
7
9
  checkWebhookSecret,
8
10
  generateWebhookSecret
9
- } from "./chunk-AVZE6FFR.js";
11
+ } from "./chunk-6EJDVZCD.js";
10
12
 
11
13
  // src/config.ts
12
14
  var envVars = {
@@ -21,9 +23,14 @@ var envVars = {
21
23
  WEBHOOK_SECRET: false,
22
24
  WEBHOOK_EVENTS: false,
23
25
  SESSION_TTL: false,
26
+ SESSION_RENEWAL_TTL: false,
24
27
  COOKIE_DOMAIN: false
25
28
  };
29
+ async function resolveEnv(source = process.env) {
30
+ return typeof source === "function" ? source() : source;
31
+ }
26
32
  var DEFAULT_SESSION_TTL = 8 * 60 * 60;
33
+ var DEFAULT_RENEWAL_TTL = 30 * 24 * 60 * 60;
27
34
  var DEFAULT_PASSWORD_MIN_LENGTH = 8;
28
35
  var ConfigError = class extends Error {
29
36
  constructor() {
@@ -79,17 +86,16 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
79
86
  if (clientId && !clientId.endsWith(".apps.googleusercontent.com")) {
80
87
  throw new ConfigError(`GOOGLE_CLIENT_ID must end with ".apps.googleusercontent.com" but is "${clientId}".`);
81
88
  }
82
- const webhook = webhookFromEnv(read("WEBHOOK_URL"), read("WEBHOOK_SECRET"), read("WEBHOOK_EVENTS"));
89
+ const sendsEmails = !!(overrides.store || read("DATABASE_URL"));
90
+ const webhook = webhookFromEnv(read("WEBHOOK_URL"), read("WEBHOOK_SECRET"), read("WEBHOOK_EVENTS"), sendsEmails);
91
+ if (sendsEmails && !webhook) {
92
+ throw new ConfigError(
93
+ `WEBHOOK_URL is not set. E-mail & password sign-in sends its e-mails through your webhook. For development, run the receiver at ${DEV_WEBHOOK_RECEIVER_URL} and set WEBHOOK_URL=http://localhost:8790/webhook and WEBHOOK_EVENTS=${REQUIRED_EMAIL_TYPES.join(",")},email.already_registered. See "Webhooks" in docs/server.md.`
94
+ );
95
+ }
83
96
  const store = overrides.store ?? await storeFromDatabaseUrl(read("DATABASE_URL"));
84
97
  let password;
85
- if (store) {
86
- if (!webhook) {
87
- throw new ConfigError(
88
- 'WEBHOOK_URL is not set. E-mail & password sign-in sends its e-mails through your webhook. For development, run the example receiver (npm run dev:webhooks) and set WEBHOOK_URL=http://localhost:8790/webhook. See "Webhooks" in docs/server.md.'
89
- );
90
- }
91
- password = { minLength: passwordMinLength(read("PASSWORD_MIN_LENGTH")), store };
92
- }
98
+ if (store) password = { minLength: passwordMinLength(read("PASSWORD_MIN_LENGTH")), store };
93
99
  if (!clientId && !password) {
94
100
  throw new ConfigError(
95
101
  "No sign-in method is configured. Set GOOGLE_CLIENT_ID for Google sign-in and/or DATABASE_URL (e.g. sqlite:./madauth.db) for e-mail & password sign-in. See docs/server.md."
@@ -100,11 +106,19 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
100
106
  if (!Number.isInteger(sessionTtlSeconds) || sessionTtlSeconds < 60) {
101
107
  throw new ConfigError(`SESSION_TTL must be a whole number of seconds (at least 60) but is "${ttlValue}".`);
102
108
  }
109
+ const renewalValue = read("SESSION_RENEWAL_TTL");
110
+ const renewalTtlSeconds = renewalValue ? Number(renewalValue) : Math.max(DEFAULT_RENEWAL_TTL, sessionTtlSeconds);
111
+ if (!Number.isInteger(renewalTtlSeconds) || renewalTtlSeconds < sessionTtlSeconds) {
112
+ throw new ConfigError(
113
+ `SESSION_RENEWAL_TTL must be a whole number of seconds, not less than SESSION_TTL (${sessionTtlSeconds}), but is "${renewalValue}".`
114
+ );
115
+ }
103
116
  return {
104
117
  issuer,
105
118
  signingKey,
106
119
  allowedOrigins,
107
120
  sessionTtlSeconds,
121
+ renewalTtlSeconds,
108
122
  cookieDomain: read("COOKIE_DOMAIN"),
109
123
  google: clientId ? { clientId, clientSecret: read("GOOGLE_CLIENT_SECRET") } : void 0,
110
124
  password,
@@ -112,9 +126,12 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
112
126
  };
113
127
  }
114
128
  async function loadUserStoreConfig(env) {
129
+ return { store: await loadStore(env), minLength: passwordMinLength(readEnv(env, "PASSWORD_MIN_LENGTH")) };
130
+ }
131
+ async function loadStore(env) {
115
132
  const store = await storeFromDatabaseUrl(readEnv(env, "DATABASE_URL"));
116
133
  if (!store) throw new ConfigError("Set DATABASE_URL to manage users.");
117
- return { store, minLength: passwordMinLength(readEnv(env, "PASSWORD_MIN_LENGTH")) };
134
+ return store;
118
135
  }
119
136
  function passwordMinLength(value) {
120
137
  const minLength = value ? Number(value) : DEFAULT_PASSWORD_MIN_LENGTH;
@@ -125,9 +142,10 @@ function passwordMinLength(value) {
125
142
  }
126
143
  async function storeFromDatabaseUrl(url) {
127
144
  if (!url) return void 0;
145
+ if (url.startsWith("dynamodb:")) return dynamoDbStore(url.slice("dynamodb:".length));
128
146
  if (!url.startsWith("sqlite:")) {
129
147
  throw new ConfigError(
130
- `DATABASE_URL must start with "sqlite:" (e.g. sqlite:/data/madauth.db) but is "${url}". For other databases, pass your own store adapter (see "Custom store adapter" in docs/server.md).`
148
+ `DATABASE_URL must start with "sqlite:" (e.g. sqlite:/data/madauth.db) or "dynamodb:" (e.g. dynamodb:madauth) but is "${url}". For other databases, pass your own store adapter (see "Custom store adapter" in docs/server.md).`
131
149
  );
132
150
  }
133
151
  const path = url.slice("sqlite:".length);
@@ -139,8 +157,25 @@ async function storeFromDatabaseUrl(url) {
139
157
  throw new ConfigError(`DATABASE_URL: could not open "${path}": ${e.message}`);
140
158
  }
141
159
  }
160
+ async function dynamoDbStore(tableName) {
161
+ if (!tableName) throw new ConfigError("DATABASE_URL needs a table name, e.g. dynamodb:madauth");
162
+ if (!/^[\w.-]{3,255}$/.test(tableName)) {
163
+ throw new ConfigError(
164
+ `DATABASE_URL=dynamodb: needs a DynamoDB table name (3 to 255 letters, digits, _ . -) but has "${tableName}"`
165
+ );
166
+ }
167
+ try {
168
+ await import("@aws-sdk/client-dynamodb");
169
+ } catch (e) {
170
+ throw new ConfigError(
171
+ `DATABASE_URL=dynamodb: needs the AWS SDK. Install it next to madAuth: npm install @aws-sdk/client-dynamodb (${e.message})`
172
+ );
173
+ }
174
+ const { createDynamoDbAdapter } = await import("./dynamodb.js");
175
+ return createDynamoDbAdapter({ tableName });
176
+ }
142
177
  var LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]", "host.docker.internal"];
143
- function webhookFromEnv(url, secret, events) {
178
+ function webhookFromEnv(url, secret, events, sendsEmails) {
144
179
  if (!url) {
145
180
  if (secret || events) throw new ConfigError("WEBHOOK_SECRET and WEBHOOK_EVENTS need WEBHOOK_URL.");
146
181
  return void 0;
@@ -166,13 +201,29 @@ WEBHOOK_SECRET=${generateWebhookSecret()}
166
201
  if (problem) {
167
202
  throw new ConfigError(`WEBHOOK_SECRET ${problem}. Generate one with: npx @madauth/server generate-webhook-secret`);
168
203
  }
169
- let selected = null;
170
- if (events) {
171
- selected = new Set(events.split(",").map((e) => e.trim()).filter(Boolean));
172
- const unknown = [...selected].filter((e) => !WEBHOOK_TYPES.includes(e));
173
- if (unknown.length) {
174
- throw new ConfigError(`WEBHOOK_EVENTS: unknown type ${unknown.join(", ")}. Known types: ${WEBHOOK_TYPES.join(", ")}.`);
175
- }
204
+ const selected = new Set((events ?? "").split(",").map((e) => e.trim()).filter(Boolean));
205
+ const allTypes = `All types: ${WEBHOOK_TYPES.join(", ")}. See "Webhooks" in docs/server.md.`;
206
+ if (!selected.size) {
207
+ throw new ConfigError(
208
+ "WEBHOOK_EVENTS is not set. List the types your webhook receiver handles; only these are sent. " + (sendsEmails ? `E-mail & password sign-in needs ${REQUIRED_EMAIL_TYPES.join(" and ")}. For a receiver that sends the e-mails:
209
+
210
+ WEBHOOK_EVENTS=email.verify,email.reset,email.already_registered
211
+
212
+ ` : "For example:\n\nWEBHOOK_EVENTS=user.signed_in\n\n") + allTypes
213
+ );
214
+ }
215
+ const unknown = [...selected].filter((e) => !WEBHOOK_TYPES.includes(e));
216
+ if (unknown.length) {
217
+ throw new ConfigError(`WEBHOOK_EVENTS: unknown type ${unknown.join(", ")}. Known types: ${WEBHOOK_TYPES.join(", ")}.`);
218
+ }
219
+ const missing = sendsEmails ? REQUIRED_EMAIL_TYPES.filter((type) => !selected.has(type)) : [];
220
+ if (missing.length) {
221
+ throw new ConfigError(
222
+ `WEBHOOK_EVENTS lacks ${missing.join(" and ")}. E-mail & password sign-in can't work without these e-mails, so your webhook receiver must send them:
223
+
224
+ WEBHOOK_EVENTS=${[...selected, ...missing].join(",")}
225
+ `
226
+ );
176
227
  }
177
228
  return { url: parsed.href, secret, events: selected };
178
229
  }
@@ -252,9 +303,49 @@ function safeEqual(a, b) {
252
303
  return x.length === y.length && timingSafeEqual(x, y);
253
304
  }
254
305
 
306
+ // src/roles.ts
307
+ var ADMIN_ROLE = "admin";
308
+ var MAX_ROLES = 20;
309
+ var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
310
+ function parseRoles(input) {
311
+ if (!Array.isArray(input)) return null;
312
+ if (!input.every((role) => typeof role === "string" && ROLE_NAME.test(role))) return null;
313
+ const roles = [...new Set(input)].sort();
314
+ return roles.length <= MAX_ROLES ? roles : null;
315
+ }
316
+ function sameRoles(a, b) {
317
+ return a.length === b.length && a.every((role, i) => role === b[i]);
318
+ }
319
+ var Roles = class {
320
+ constructor(store) {
321
+ this.store = store;
322
+ }
323
+ /** The address's roles, sorted; empty if it has none. */
324
+ async get(emailNormalized) {
325
+ const record = await this.store.findOne("role", { id: emailNormalized });
326
+ return typeof record?.roles === "string" && record.roles ? record.roles.split(" ").filter(Boolean) : [];
327
+ }
328
+ /** Replaces the address's roles. `updatedBy` is the address of the admin, or null for the command line. */
329
+ async set(emailNormalized, roles, updatedBy) {
330
+ if (!roles.length) {
331
+ await this.remove(emailNormalized);
332
+ return;
333
+ }
334
+ const data = { roles: roles.join(" "), updatedAt: Date.now(), updatedBy };
335
+ if (await this.store.update("role", { id: emailNormalized }, data) === 1) return;
336
+ if (!await this.store.create("role", { id: emailNormalized, ...data })) {
337
+ await this.store.update("role", { id: emailNormalized }, data);
338
+ }
339
+ }
340
+ async remove(emailNormalized) {
341
+ await this.store.delete("role", { id: emailNormalized });
342
+ }
343
+ };
344
+
255
345
  // src/users.ts
256
346
  import { randomBytes as randomBytes2 } from "crypto";
257
347
  var MAX_CODE_ATTEMPTS = 5;
348
+ var MAX_WRONG_CODES = 10;
258
349
  var newId = (prefix) => `${prefix}_${randomBytes2(16).toString("base64url")}`;
259
350
  function passwordAccountKey(userId) {
260
351
  return `password:${userId}`;
@@ -289,6 +380,7 @@ var Users = class {
289
380
  name: data.name,
290
381
  sessionVersion: 0,
291
382
  lastMailAt: 0,
383
+ wrongCodes: 0,
292
384
  createdAt: now
293
385
  };
294
386
  if (!await this.store.create("user", user)) return null;
@@ -307,6 +399,22 @@ var Users = class {
307
399
  }
308
400
  return user;
309
401
  }
402
+ /**
403
+ * Deletes the user with this e-mail address and everything stored for them. Resolves to the deleted
404
+ * user's ID, or null if there is no such user (any more).
405
+ */
406
+ async deleteByEmail(emailNormalized) {
407
+ const user = await this.findByEmail(emailNormalized);
408
+ if (!user) return null;
409
+ if (await this.store.delete("user", { id: user.id }) !== 1) return null;
410
+ try {
411
+ await this.store.delete("account", { userId: user.id });
412
+ await this.store.delete("verification", { userId: user.id });
413
+ } catch (e) {
414
+ console.error(`[madauth] Deleted user ${user.id}, but not all of their records: ${e.message}`);
415
+ }
416
+ return user.id;
417
+ }
310
418
  async updateUser(id, patch) {
311
419
  await this.store.update("user", { id }, patch);
312
420
  }
@@ -350,17 +458,38 @@ var Users = class {
350
458
  const record = await this.store.findOne("verification", { id, purpose });
351
459
  return record ? this.consume(record) : null;
352
460
  }
353
- /** Uses up the user's verification if `code` matches. Too many wrong codes delete it. */
354
- async consumeCode(userId, purpose, code) {
355
- const record = await this.store.findOne("verification", { userId, purpose });
356
- if (!record) return null;
461
+ /**
462
+ * Uses up the user's verification if `code` matches. Too many wrong codes delete it, and after
463
+ * MAX_WRONG_CODES wrong codes in a row the user's codes stop working until an e-mail link is used.
464
+ */
465
+ async consumeCode(user, purpose, code) {
466
+ if ((user.wrongCodes ?? 0) >= MAX_WRONG_CODES) return { userId: null, locked: true };
467
+ const record = await this.store.findOne("verification", { userId: user.id, purpose });
468
+ if (!record) return { userId: null, locked: false };
357
469
  const attempts = record.attempts + 1;
358
- if (await this.store.update("verification", { id: record.id, attempts: record.attempts }, { attempts }) !== 1) return null;
470
+ if (await this.store.update("verification", { id: record.id, attempts: record.attempts }, { attempts }) !== 1) {
471
+ return { userId: null, locked: false };
472
+ }
473
+ const wrongCodes = await this.countWrongCode(user);
474
+ if (wrongCodes === null) return { userId: null, locked: false };
475
+ if (wrongCodes > MAX_WRONG_CODES) return { userId: null, locked: true };
359
476
  if (!safeEqual(hashCode(this.key, code), record.codeHash)) {
360
477
  if (attempts >= MAX_CODE_ATTEMPTS) await this.store.delete("verification", { id: record.id });
361
- return null;
478
+ return { userId: null, locked: wrongCodes >= MAX_WRONG_CODES };
479
+ }
480
+ return { userId: await this.consume(record), locked: false };
481
+ }
482
+ /** Counts a code for the user and resolves to the new count; null if other requests kept changing it. */
483
+ async countWrongCode(user) {
484
+ let current = user;
485
+ for (let attempt = 0; attempt < 3 && current; attempt++) {
486
+ const wrongCodes = (current.wrongCodes ?? 0) + 1;
487
+ if (await this.store.update("user", { id: user.id, wrongCodes: current.wrongCodes ?? null }, { wrongCodes }) === 1) {
488
+ return wrongCodes;
489
+ }
490
+ current = await this.findById(user.id);
362
491
  }
363
- return this.consume(record);
492
+ return null;
364
493
  }
365
494
  async consume(record) {
366
495
  const deleted = await this.store.delete("verification", { id: record.id });
@@ -378,9 +507,15 @@ export {
378
507
  normalizeEmail,
379
508
  isValidEmail,
380
509
  deriveCodeKey,
510
+ ADMIN_ROLE,
511
+ parseRoles,
512
+ sameRoles,
513
+ Roles,
381
514
  Users,
382
515
  envVars,
516
+ resolveEnv,
383
517
  ConfigError,
384
518
  loadConfig,
385
- loadUserStoreConfig
519
+ loadUserStoreConfig,
520
+ loadStore
386
521
  };
@@ -8,10 +8,13 @@ var WEBHOOK_TYPES = [
8
8
  "user.created",
9
9
  "email.verified",
10
10
  "password.reset",
11
- "user.signed_in"
11
+ "user.signed_in",
12
+ "user.deleted",
13
+ "roles.changed"
12
14
  ];
13
- var isEmailType = (type) => type.startsWith("email.") && type !== "email.verified";
15
+ var REQUIRED_EMAIL_TYPES = ["email.verify", "email.reset"];
14
16
  var SECRET_PREFIX = "whsec_";
17
+ var DEV_WEBHOOK_RECEIVER_URL = "https://github.com/inouiw/madAuth-webhooks/tree/main/dev-receiver";
15
18
  var WEBHOOK_TOLERANCE_SECONDS = 5 * 60;
16
19
  function generateWebhookSecret() {
17
20
  return `${SECRET_PREFIX}${randomBytes(32).toString("base64")}`;
@@ -50,7 +53,7 @@ function verifyWebhook(secret, headers, body, options = {}) {
50
53
  });
51
54
  }
52
55
  function createWebhookClient(settings, fetchImpl = fetch) {
53
- const wants = (type) => isEmailType(type) || !settings.events || settings.events.has(type);
56
+ const wants = (type) => settings.events.has(type);
54
57
  return {
55
58
  wants,
56
59
  async call(type, data, timeoutMs) {
@@ -90,6 +93,8 @@ function createWebhookClient(settings, fetchImpl = fetch) {
90
93
 
91
94
  export {
92
95
  WEBHOOK_TYPES,
96
+ REQUIRED_EMAIL_TYPES,
97
+ DEV_WEBHOOK_RECEIVER_URL,
93
98
  WEBHOOK_TOLERANCE_SECONDS,
94
99
  generateWebhookSecret,
95
100
  checkWebhookSecret,
@@ -5,6 +5,9 @@ import {
5
5
  // src/user.ts
6
6
  var SESSION_COOKIE = "madauth_session";
7
7
  var SESSION_TYP = "madauth-session+jwt";
8
+ var RENEWAL_COOKIE = "madauth_renewal";
9
+ var RENEWAL_TYP = "madauth-renewal+jwt";
10
+ var EXPIRY_COOKIE = "madauth_session_expires";
8
11
 
9
12
  // src/tokens.ts
10
13
  import { SignJWT, base64url, jwtVerify } from "jose";
@@ -31,24 +34,42 @@ function signSession(keys, issuer, ttlSeconds, user, amr, extra = {}) {
31
34
  email_verified: user.email ? true : void 0,
32
35
  name: user.name,
33
36
  picture: user.picture,
37
+ roles: user.roles?.length ? user.roles : void 0,
34
38
  amr
35
39
  };
36
40
  return signToken(keys, issuer, SESSION_TYP, claims, ttlSeconds);
37
41
  }
42
+ function signRenewal(keys, issuer, ttlSeconds, user, amr, extra = {}) {
43
+ const claims = {
44
+ ...extra,
45
+ sub: user.id,
46
+ email: user.email,
47
+ email_verified: user.email ? true : void 0,
48
+ name: user.name,
49
+ picture: user.picture,
50
+ amr
51
+ };
52
+ return signToken(keys, issuer, RENEWAL_TYP, claims, ttlSeconds);
53
+ }
38
54
  function userFromClaims(claims) {
39
55
  const user = { id: claims.sub };
40
56
  if (claims.email) user.email = claims.email;
41
57
  if (claims.name) user.name = claims.name;
42
58
  if (claims.picture) user.picture = claims.picture;
59
+ if (Array.isArray(claims.roles) && claims.roles.length) user.roles = claims.roles;
43
60
  return user;
44
61
  }
45
62
 
46
63
  export {
47
64
  SESSION_COOKIE,
48
65
  SESSION_TYP,
66
+ RENEWAL_COOKIE,
67
+ RENEWAL_TYP,
68
+ EXPIRY_COOKIE,
49
69
  randomString,
50
70
  signToken,
51
71
  readToken,
52
72
  signSession,
73
+ signRenewal,
53
74
  userFromClaims
54
75
  };
@@ -1,44 +1,6 @@
1
- // src/store/schema.ts
2
- var madauthSchema = {
3
- version: 1,
4
- models: {
5
- user: {
6
- fields: {
7
- id: { type: "string", primaryKey: true },
8
- email: { type: "string" },
9
- emailNormalized: { type: "string", unique: true },
10
- emailVerified: { type: "boolean" },
11
- name: { type: "string", nullable: true },
12
- sessionVersion: { type: "number" },
13
- lastMailAt: { type: "number" },
14
- createdAt: { type: "number" }
15
- }
16
- },
17
- /** How a user signs in. `key` is e.g. `password:<userId>`; later `google:<sub>` or `totp:<userId>`. */
18
- account: {
19
- fields: {
20
- id: { type: "string", primaryKey: true },
21
- userId: { type: "string", index: true },
22
- key: { type: "string", unique: true },
23
- secret: { type: "string", nullable: true },
24
- failedAttempts: { type: "number" },
25
- lockedUntil: { type: "number" },
26
- createdAt: { type: "number" }
27
- }
28
- },
29
- /** A pending e-mail verification or password reset. `id` is the SHA-256 of the link token. */
30
- verification: {
31
- fields: {
32
- id: { type: "string", primaryKey: true },
33
- userId: { type: "string", index: true },
34
- purpose: { type: "string" },
35
- codeHash: { type: "string" },
36
- attempts: { type: "number" },
37
- expiresAt: { type: "number" }
38
- }
39
- }
40
- }
41
- };
1
+ import {
2
+ madauthSchema
3
+ } from "./chunk-XNBWZZHT.js";
42
4
 
43
5
  // src/store/sql.ts
44
6
  function tableName(model) {
@@ -53,6 +15,19 @@ var columnTypes = {
53
15
  postgres: { string: "TEXT", number: "DOUBLE PRECISION", boolean: "BOOLEAN" },
54
16
  mysql: { string: "TEXT", number: "DOUBLE", boolean: "BOOLEAN" }
55
17
  };
18
+ var upgrades = [
19
+ {
20
+ version: 2,
21
+ sql: (dialect) => `ALTER TABLE ${tableName("user")} ADD COLUMN ${columnName("wrongCodes")} ${columnTypes[dialect].number} NOT NULL DEFAULT 0;`
22
+ },
23
+ {
24
+ version: 3,
25
+ sql: (dialect) => createTablesSql(dialect, { version: 3, models: { role: madauthSchema.models.role } })
26
+ }
27
+ ];
28
+ function upgradeTablesSql(dialect, fromVersion) {
29
+ return upgrades.filter((step) => step.version > fromVersion).map((step) => step.sql(dialect)).join("\n");
30
+ }
56
31
  function createTablesSql(dialect, schema = madauthSchema) {
57
32
  const statements = [];
58
33
  for (const [model, { fields }] of Object.entries(schema.models)) {
@@ -74,8 +49,8 @@ ${columns.join(",\n")}
74
49
  }
75
50
 
76
51
  export {
77
- madauthSchema,
78
52
  tableName,
79
53
  columnName,
54
+ upgradeTablesSql,
80
55
  createTablesSql
81
56
  };
@@ -0,0 +1,60 @@
1
+ // src/store/schema.ts
2
+ var madauthSchema = {
3
+ version: 3,
4
+ models: {
5
+ user: {
6
+ fields: {
7
+ id: { type: "string", primaryKey: true },
8
+ email: { type: "string" },
9
+ emailNormalized: { type: "string", unique: true },
10
+ emailVerified: { type: "boolean" },
11
+ name: { type: "string", nullable: true },
12
+ sessionVersion: { type: "number" },
13
+ lastMailAt: { type: "number" },
14
+ /** Wrong e-mail codes in a row, counted across e-mails (since version 2). */
15
+ wrongCodes: { type: "number" },
16
+ createdAt: { type: "number" }
17
+ }
18
+ },
19
+ /** How a user signs in. `key` is e.g. `password:<userId>`; later `google:<sub>` or `totp:<userId>`. */
20
+ account: {
21
+ fields: {
22
+ id: { type: "string", primaryKey: true },
23
+ userId: { type: "string", index: true },
24
+ key: { type: "string", unique: true },
25
+ secret: { type: "string", nullable: true },
26
+ failedAttempts: { type: "number" },
27
+ lockedUntil: { type: "number" },
28
+ createdAt: { type: "number" }
29
+ }
30
+ },
31
+ /** A pending e-mail verification or password reset. `id` is the SHA-256 of the link token. */
32
+ verification: {
33
+ fields: {
34
+ id: { type: "string", primaryKey: true },
35
+ userId: { type: "string", index: true },
36
+ purpose: { type: "string" },
37
+ codeHash: { type: "string" },
38
+ attempts: { type: "number" },
39
+ expiresAt: { type: "number" }
40
+ }
41
+ },
42
+ /**
43
+ * The roles of an e-mail address (since version 3). `id` is the normalized address, so the roles
44
+ * apply whichever way its owner signs in. `roles` holds the names separated by spaces.
45
+ */
46
+ role: {
47
+ fields: {
48
+ id: { type: "string", primaryKey: true },
49
+ roles: { type: "string" },
50
+ updatedAt: { type: "number" },
51
+ /** The address of the admin who set them; empty when set from the command line. */
52
+ updatedBy: { type: "string", nullable: true }
53
+ }
54
+ }
55
+ }
56
+ };
57
+
58
+ export {
59
+ madauthSchema
60
+ };