@madauth/server 0.1.0 → 0.2.0-beta.1
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/README.md +17 -7
- package/dist/bin.js +284 -22
- package/dist/{chunk-V2XKGETK.js → chunk-5CKEMC6M.js} +163 -28
- package/dist/{chunk-AVZE6FFR.js → chunk-6EJDVZCD.js} +8 -3
- package/dist/{chunk-PRRKLEW7.js → chunk-7CTAB3QV.js} +21 -0
- package/dist/{chunk-3WIWWKRI.js → chunk-CNL562LZ.js} +17 -42
- package/dist/chunk-XNBWZZHT.js +60 -0
- package/dist/{chunk-TEMZRMFF.js → chunk-YALS7OMV.js} +162 -42
- package/dist/{config-KQvE2imU.d.ts → config-BkxADgtp.d.ts} +16 -4
- package/dist/dynamodb.js +244 -0
- package/dist/entry/azure.d.ts +5 -5
- package/dist/entry/azure.js +9 -7
- package/dist/entry/lambda.d.ts +5 -5
- package/dist/entry/lambda.js +9 -7
- package/dist/entry/node.d.ts +5 -8
- package/dist/entry/node.js +8 -6
- package/dist/index.d.ts +12 -7
- package/dist/index.js +12 -8
- package/dist/{schema-Dotc6LU7.d.ts → schema-BSgYVRpd.d.ts} +28 -1
- package/dist/sqlite.js +7 -4
- package/dist/standalone/azure.mjs +611 -78
- package/dist/standalone/lambda.mjs +611 -78
- package/dist/standalone/main.mjs +29508 -592
- package/dist/store/dynamodb.d.ts +19 -0
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +65 -2
- package/dist/{user-3DDyggFV.d.ts → user-C1A3BfOo.d.ts} +2 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/{webhook-CT80emTQ.d.ts → webhook-ltwulgcB.d.ts} +3 -3
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +12 -2
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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)
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
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
|
+
};
|