@madauth/server 0.3.0-beta.1 → 0.4.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 +3 -2
- package/dist/bin.js +104 -42
- package/dist/chunk-3PACOYTG.js +1185 -0
- package/dist/{chunk-DCDTO5LG.js → chunk-A7UYZRBL.js} +1 -0
- package/dist/{chunk-CD7Y4T66.js → chunk-GA4TF7EI.js} +24 -2
- package/dist/{chunk-ODITZLMM.js → chunk-ISJGOCF3.js} +258 -84
- package/dist/{chunk-56MOVFO4.js → chunk-OMIX3ERC.js} +11 -2
- package/dist/{chunk-NPNM26J3.js → chunk-Y7ZSUROX.js} +5 -1
- package/dist/{config-DEBYi8hb.d.ts → config-DkE_2D3-.d.ts} +5 -2
- package/dist/dynamodb.js +1 -1
- package/dist/entry/azure.d.ts +3 -3
- package/dist/entry/azure.js +4 -4
- package/dist/entry/lambda.d.ts +3 -3
- package/dist/entry/lambda.js +4 -4
- package/dist/entry/node.d.ts +3 -3
- package/dist/entry/node.js +4 -4
- package/dist/index.d.ts +29 -14
- package/dist/index.js +10 -6
- package/dist/{schema-3yhCc2jH.d.ts → schema-DTeiSuQB.d.ts} +39 -2
- package/dist/sqlite.js +2 -2
- package/dist/standalone/azure.mjs +1331 -676
- package/dist/standalone/lambda.mjs +1319 -664
- package/dist/standalone/main.mjs +1191 -536
- package/dist/store/dynamodb.d.ts +1 -1
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -0
- package/dist/{user-BMWEWREH.d.ts → user-BZs84sui.d.ts} +5 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/{webhook-DCDP6SaE.d.ts → webhook-B6P50ocX.d.ts} +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ZSBKNV5W.js +0 -713
package/dist/standalone/main.mjs
CHANGED
|
@@ -38,7 +38,7 @@ var init_schema = __esm({
|
|
|
38
38
|
"src/store/schema.ts"() {
|
|
39
39
|
"use strict";
|
|
40
40
|
madauthSchema = {
|
|
41
|
-
version:
|
|
41
|
+
version: 5,
|
|
42
42
|
models: {
|
|
43
43
|
/** Someone who signed in, whichever way: the e-mail address is the identity shared by the sign-in methods. */
|
|
44
44
|
user: {
|
|
@@ -57,7 +57,10 @@ var init_schema = __esm({
|
|
|
57
57
|
createdAt: { type: "number" }
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* How a user signs in. `key` is `password:<userId>`, `google:<sub>` or `totp:<userId>` (the authenticator
|
|
62
|
+
* app, whose `secret` is encrypted).
|
|
63
|
+
*/
|
|
61
64
|
account: {
|
|
62
65
|
fields: {
|
|
63
66
|
id: { type: "string", primaryKey: true },
|
|
@@ -68,6 +71,25 @@ var init_schema = __esm({
|
|
|
68
71
|
lockedUntil: { type: "number" },
|
|
69
72
|
/** The address the provider reported at the last sign-in, e.g. Google's current primary address (since version 4). */
|
|
70
73
|
email: { type: "string", nullable: true },
|
|
74
|
+
/** The time step of the last authenticator code that was accepted, so a code works once; 0 before (since version 5). */
|
|
75
|
+
lastUsedStep: { type: "number" },
|
|
76
|
+
/**
|
|
77
|
+
* The setup an authenticator secret came from; its recovery codes carry the same id, so the app and
|
|
78
|
+
* its codes always belong together. Empty for other accounts (since version 5).
|
|
79
|
+
*/
|
|
80
|
+
setupId: { type: "string", nullable: true },
|
|
81
|
+
createdAt: { type: "number" }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* A recovery code of a user's authenticator app (since version 5). `id` is the code's keyed hash; the
|
|
86
|
+
* record goes when the code is used. It works only while the account's `setupId` is its own.
|
|
87
|
+
*/
|
|
88
|
+
recoveryCode: {
|
|
89
|
+
fields: {
|
|
90
|
+
id: { type: "string", primaryKey: true },
|
|
91
|
+
userId: { type: "string", index: true },
|
|
92
|
+
setupId: { type: "string" },
|
|
71
93
|
createdAt: { type: "number" }
|
|
72
94
|
}
|
|
73
95
|
},
|
|
@@ -99,7 +121,7 @@ var init_schema = __esm({
|
|
|
99
121
|
|
|
100
122
|
// src/store/sql.ts
|
|
101
123
|
function tableName(model) {
|
|
102
|
-
return `madauth_${model}`;
|
|
124
|
+
return `madauth_${columnName(model)}`;
|
|
103
125
|
}
|
|
104
126
|
function columnName(field) {
|
|
105
127
|
return field.replace(/[A-Z]/g, (c6) => `_${c6.toLowerCase()}`);
|
|
@@ -167,6 +189,15 @@ var init_sql = __esm({
|
|
|
167
189
|
`DROP TABLE ${tableName("role")};`,
|
|
168
190
|
createTablesSql(dialect, { version: 4, models: { setting: madauthSchema.models.setting } })
|
|
169
191
|
].join("\n")
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
// The authenticator app: which code was used last, and the recovery codes.
|
|
195
|
+
version: 5,
|
|
196
|
+
sql: (dialect) => [
|
|
197
|
+
`ALTER TABLE ${tableName("account")} ADD COLUMN ${columnName("lastUsedStep")} ${columnTypes[dialect].number} NOT NULL DEFAULT 0;`,
|
|
198
|
+
`ALTER TABLE ${tableName("account")} ADD COLUMN ${columnName("setupId")} ${columnTypes[dialect].string};`,
|
|
199
|
+
createTablesSql(dialect, { version: 5, models: { recoveryCode: madauthSchema.models.recoveryCode } })
|
|
200
|
+
].join("\n")
|
|
170
201
|
}
|
|
171
202
|
];
|
|
172
203
|
}
|
|
@@ -1210,17 +1241,17 @@ var init_schemaDeserializationMiddleware = __esm({
|
|
|
1210
1241
|
response,
|
|
1211
1242
|
output: parsed
|
|
1212
1243
|
};
|
|
1213
|
-
} catch (
|
|
1214
|
-
Object.defineProperty(
|
|
1244
|
+
} catch (error2) {
|
|
1245
|
+
Object.defineProperty(error2, "$response", {
|
|
1215
1246
|
value: response,
|
|
1216
1247
|
enumerable: false,
|
|
1217
1248
|
writable: false,
|
|
1218
1249
|
configurable: false
|
|
1219
1250
|
});
|
|
1220
|
-
if (!("$metadata" in
|
|
1251
|
+
if (!("$metadata" in error2)) {
|
|
1221
1252
|
const hint = `Deserialization error: to see the raw response, inspect the hidden field {error}.$response on this object.`;
|
|
1222
1253
|
try {
|
|
1223
|
-
|
|
1254
|
+
error2.message += "\n " + hint;
|
|
1224
1255
|
} catch (ignored) {
|
|
1225
1256
|
if (!context.logger || context.logger?.constructor?.name === "NoOpLogger") {
|
|
1226
1257
|
console.warn(hint);
|
|
@@ -1228,16 +1259,16 @@ var init_schemaDeserializationMiddleware = __esm({
|
|
|
1228
1259
|
context.logger?.warn?.(hint);
|
|
1229
1260
|
}
|
|
1230
1261
|
}
|
|
1231
|
-
if (typeof
|
|
1232
|
-
if (
|
|
1233
|
-
|
|
1262
|
+
if (typeof error2.$responseBodyText !== "undefined") {
|
|
1263
|
+
if (error2.$response) {
|
|
1264
|
+
error2.$response.body = error2.$responseBodyText;
|
|
1234
1265
|
}
|
|
1235
1266
|
}
|
|
1236
1267
|
try {
|
|
1237
1268
|
if (HttpResponse.isInstance(response)) {
|
|
1238
1269
|
const { headers = {}, statusCode } = response;
|
|
1239
1270
|
const headerEntries = Object.entries(headers);
|
|
1240
|
-
|
|
1271
|
+
error2.$metadata = {
|
|
1241
1272
|
httpStatusCode: statusCode,
|
|
1242
1273
|
requestId: findHeader(/^x-[\w-]+-request-?id$/, headerEntries),
|
|
1243
1274
|
extendedRequestId: findHeader(/^x-[\w-]+-id-2$/, headerEntries),
|
|
@@ -1247,7 +1278,7 @@ var init_schemaDeserializationMiddleware = __esm({
|
|
|
1247
1278
|
} catch (ignored) {
|
|
1248
1279
|
}
|
|
1249
1280
|
}
|
|
1250
|
-
throw
|
|
1281
|
+
throw error2;
|
|
1251
1282
|
}
|
|
1252
1283
|
};
|
|
1253
1284
|
findHeader = (pattern, headers) => {
|
|
@@ -3203,26 +3234,26 @@ var calculateBodyLength;
|
|
|
3203
3234
|
var init_calculateBodyLength = __esm({
|
|
3204
3235
|
"../../node_modules/@smithy/core/dist-es/submodules/serde/util-body-length/calculateBodyLength.js"() {
|
|
3205
3236
|
"use strict";
|
|
3206
|
-
calculateBodyLength = (
|
|
3207
|
-
if (!
|
|
3237
|
+
calculateBodyLength = (body2) => {
|
|
3238
|
+
if (!body2) {
|
|
3208
3239
|
return 0;
|
|
3209
3240
|
}
|
|
3210
|
-
if (typeof
|
|
3211
|
-
return Buffer.byteLength(
|
|
3212
|
-
} else if (typeof
|
|
3213
|
-
return
|
|
3214
|
-
} else if (typeof
|
|
3215
|
-
return
|
|
3216
|
-
} else if (typeof
|
|
3217
|
-
return
|
|
3218
|
-
} else if (
|
|
3219
|
-
if (
|
|
3220
|
-
return lstatSync(
|
|
3221
|
-
} else if (typeof
|
|
3222
|
-
return fstatSync(
|
|
3241
|
+
if (typeof body2 === "string") {
|
|
3242
|
+
return Buffer.byteLength(body2);
|
|
3243
|
+
} else if (typeof body2.byteLength === "number") {
|
|
3244
|
+
return body2.byteLength;
|
|
3245
|
+
} else if (typeof body2.size === "number") {
|
|
3246
|
+
return body2.size;
|
|
3247
|
+
} else if (typeof body2.start === "number" && typeof body2.end === "number") {
|
|
3248
|
+
return body2.end + 1 - body2.start;
|
|
3249
|
+
} else if (body2 instanceof ReadStream) {
|
|
3250
|
+
if (body2.path != null) {
|
|
3251
|
+
return lstatSync(body2.path).size;
|
|
3252
|
+
} else if (typeof body2.fd === "number") {
|
|
3253
|
+
return fstatSync(body2.fd).size;
|
|
3223
3254
|
}
|
|
3224
3255
|
}
|
|
3225
|
-
throw new Error(`Body Length computation failed for ${
|
|
3256
|
+
throw new Error(`Body Length computation failed for ${body2}`);
|
|
3226
3257
|
};
|
|
3227
3258
|
}
|
|
3228
3259
|
});
|
|
@@ -3293,8 +3324,8 @@ var init_ProviderError = __esm({
|
|
|
3293
3324
|
Object.setPrototypeOf(this, _ProviderError.prototype);
|
|
3294
3325
|
logger2?.debug?.(`@smithy/property-provider ${tryNextLink ? "->" : "(!)"} ${message2}`);
|
|
3295
3326
|
}
|
|
3296
|
-
static from(
|
|
3297
|
-
return Object.assign(new this(
|
|
3327
|
+
static from(error2, options = true) {
|
|
3328
|
+
return Object.assign(new this(error2.message, options), error2);
|
|
3298
3329
|
}
|
|
3299
3330
|
};
|
|
3300
3331
|
}
|
|
@@ -5221,9 +5252,9 @@ var init_getEndpointUrl = __esm({
|
|
|
5221
5252
|
if (typeof expression === "string") {
|
|
5222
5253
|
try {
|
|
5223
5254
|
return new URL(expression);
|
|
5224
|
-
} catch (
|
|
5225
|
-
console.error(`Failed to construct URL with ${expression}`,
|
|
5226
|
-
throw
|
|
5255
|
+
} catch (error2) {
|
|
5256
|
+
console.error(`Failed to construct URL with ${expression}`, error2);
|
|
5257
|
+
throw error2;
|
|
5227
5258
|
}
|
|
5228
5259
|
}
|
|
5229
5260
|
throw new EndpointError(`Endpoint URL must be a string, got ${typeof expression}`);
|
|
@@ -6391,9 +6422,9 @@ var init_EventStreamCodec = __esm({
|
|
|
6391
6422
|
}
|
|
6392
6423
|
};
|
|
6393
6424
|
}
|
|
6394
|
-
encode({ headers: rawHeaders, body }) {
|
|
6425
|
+
encode({ headers: rawHeaders, body: body2 }) {
|
|
6395
6426
|
const headers = this.headerMarshaller.format(rawHeaders);
|
|
6396
|
-
const length = headers.byteLength +
|
|
6427
|
+
const length = headers.byteLength + body2.byteLength + 16;
|
|
6397
6428
|
const out = new Uint8Array(length);
|
|
6398
6429
|
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
6399
6430
|
const checksum = new Crc32Node();
|
|
@@ -6402,14 +6433,14 @@ var init_EventStreamCodec = __esm({
|
|
|
6402
6433
|
checksum.update(out.subarray(0, 8));
|
|
6403
6434
|
view.setUint32(8, checksum.digestSync(), false);
|
|
6404
6435
|
out.set(headers, 12);
|
|
6405
|
-
out.set(
|
|
6436
|
+
out.set(body2, headers.byteLength + 12);
|
|
6406
6437
|
checksum.update(out.subarray(8, length - 4));
|
|
6407
6438
|
view.setUint32(length - 4, checksum.digestSync(), false);
|
|
6408
6439
|
return out;
|
|
6409
6440
|
}
|
|
6410
6441
|
decode(message2) {
|
|
6411
|
-
const { headers, body } = splitMessage(message2);
|
|
6412
|
-
return { headers: this.headerMarshaller.parse(headers), body };
|
|
6442
|
+
const { headers, body: body2 } = splitMessage(message2);
|
|
6443
|
+
return { headers: this.headerMarshaller.parse(headers), body: body2 };
|
|
6413
6444
|
}
|
|
6414
6445
|
formatHeaders(rawHeaders) {
|
|
6415
6446
|
return this.headerMarshaller.format(rawHeaders);
|
|
@@ -6613,9 +6644,9 @@ function getMessageUnmarshaller(deserializer, toUtf83) {
|
|
|
6613
6644
|
const exception = { [code]: message2 };
|
|
6614
6645
|
const deserializedException = await deserializer(exception);
|
|
6615
6646
|
if (deserializedException.$unknown) {
|
|
6616
|
-
const
|
|
6617
|
-
|
|
6618
|
-
throw
|
|
6647
|
+
const error2 = new Error(toUtf83(message2.body));
|
|
6648
|
+
error2.name = code;
|
|
6649
|
+
throw error2;
|
|
6619
6650
|
}
|
|
6620
6651
|
throw deserializedException[code];
|
|
6621
6652
|
} else if (messageType === "event") {
|
|
@@ -6656,8 +6687,8 @@ var init_EventStreamMarshaller = __esm({
|
|
|
6656
6687
|
this.eventStreamCodec = new EventStreamCodec(utf8Encoder, utf8Decoder);
|
|
6657
6688
|
this.utfEncoder = utf8Encoder;
|
|
6658
6689
|
}
|
|
6659
|
-
deserialize(
|
|
6660
|
-
const inputStream = getChunkedStream(
|
|
6690
|
+
deserialize(body2, deserializer) {
|
|
6691
|
+
const inputStream = getChunkedStream(body2);
|
|
6661
6692
|
return new SmithyMessageDecoderStream({
|
|
6662
6693
|
messageStream: new MessageDecoderStream({ inputStream, decoder: this.eventStreamCodec }),
|
|
6663
6694
|
deserializer: getMessageUnmarshaller(deserializer, this.utfEncoder)
|
|
@@ -6716,8 +6747,8 @@ var init_EventStreamMarshaller2 = __esm({
|
|
|
6716
6747
|
utf8Encoder
|
|
6717
6748
|
});
|
|
6718
6749
|
}
|
|
6719
|
-
deserialize(
|
|
6720
|
-
const bodyIterable = typeof
|
|
6750
|
+
deserialize(body2, deserializer) {
|
|
6751
|
+
const bodyIterable = typeof body2[Symbol.asyncIterator] === "function" ? body2 : readableToIterable(body2);
|
|
6721
6752
|
return this.universalMarshaller.deserialize(bodyIterable, deserializer);
|
|
6722
6753
|
}
|
|
6723
6754
|
serialize(input, serializer) {
|
|
@@ -6813,11 +6844,11 @@ var init_EventStreamSerde = __esm({
|
|
|
6813
6844
|
":content-type": { type: "string", value: defaultContentType2 }
|
|
6814
6845
|
};
|
|
6815
6846
|
serializer.write(requestSchema, initialRequest);
|
|
6816
|
-
const
|
|
6847
|
+
const body2 = serializer.flush();
|
|
6817
6848
|
yield {
|
|
6818
6849
|
[initialRequestMarker]: true,
|
|
6819
6850
|
headers,
|
|
6820
|
-
body
|
|
6851
|
+
body: body2
|
|
6821
6852
|
};
|
|
6822
6853
|
}
|
|
6823
6854
|
for await (const page of eventStream) {
|
|
@@ -6841,7 +6872,7 @@ var init_EventStreamSerde = __esm({
|
|
|
6841
6872
|
break;
|
|
6842
6873
|
}
|
|
6843
6874
|
}
|
|
6844
|
-
const { additionalHeaders, body, eventType, explicitPayloadContentType } = this.writeEventBody(unionMember, unionSchema, event);
|
|
6875
|
+
const { additionalHeaders, body: body2, eventType, explicitPayloadContentType } = this.writeEventBody(unionMember, unionSchema, event);
|
|
6845
6876
|
const headers = {
|
|
6846
6877
|
":event-type": { type: "string", value: eventType },
|
|
6847
6878
|
":message-type": { type: "string", value: "event" },
|
|
@@ -6850,7 +6881,7 @@ var init_EventStreamSerde = __esm({
|
|
|
6850
6881
|
};
|
|
6851
6882
|
return {
|
|
6852
6883
|
headers,
|
|
6853
|
-
body
|
|
6884
|
+
body: body2
|
|
6854
6885
|
};
|
|
6855
6886
|
});
|
|
6856
6887
|
}
|
|
@@ -6870,9 +6901,9 @@ var init_EventStreamSerde = __esm({
|
|
|
6870
6901
|
break;
|
|
6871
6902
|
}
|
|
6872
6903
|
}
|
|
6873
|
-
const
|
|
6904
|
+
const body2 = event[unionMember].body;
|
|
6874
6905
|
if (unionMember === (initialMessageType ?? "initial-response")) {
|
|
6875
|
-
const dataObject = await this.deserializer.read(responseSchema,
|
|
6906
|
+
const dataObject = await this.deserializer.read(responseSchema, body2);
|
|
6876
6907
|
delete dataObject[eventStreamMember];
|
|
6877
6908
|
return {
|
|
6878
6909
|
[initialResponseMarker]: true,
|
|
@@ -6888,11 +6919,11 @@ var init_EventStreamSerde = __esm({
|
|
|
6888
6919
|
hasBindings = hasBindings || Boolean(eventHeader || eventPayload);
|
|
6889
6920
|
if (eventPayload) {
|
|
6890
6921
|
if (member2.isBlobSchema()) {
|
|
6891
|
-
out[name] =
|
|
6922
|
+
out[name] = body2;
|
|
6892
6923
|
} else if (member2.isStringSchema()) {
|
|
6893
|
-
out[name] = (this.serdeContext?.utf8Encoder ?? toUtf8)(
|
|
6924
|
+
out[name] = (this.serdeContext?.utf8Encoder ?? toUtf8)(body2);
|
|
6894
6925
|
} else if (member2.isStructSchema()) {
|
|
6895
|
-
out[name] = await this.deserializer.read(member2,
|
|
6926
|
+
out[name] = await this.deserializer.read(member2, body2);
|
|
6896
6927
|
}
|
|
6897
6928
|
} else if (eventHeader) {
|
|
6898
6929
|
const value = event[unionMember].headers[name]?.value;
|
|
@@ -6910,11 +6941,11 @@ var init_EventStreamSerde = __esm({
|
|
|
6910
6941
|
}
|
|
6911
6942
|
}
|
|
6912
6943
|
return {
|
|
6913
|
-
[unionMember]: await this.readEventMember(eventStreamSchema,
|
|
6944
|
+
[unionMember]: await this.readEventMember(eventStreamSchema, body2, hasBindings, out)
|
|
6914
6945
|
};
|
|
6915
6946
|
}
|
|
6916
6947
|
return {
|
|
6917
|
-
[unionMember]: await this.deserializer.read(eventStreamSchema,
|
|
6948
|
+
[unionMember]: await this.deserializer.read(eventStreamSchema, body2)
|
|
6918
6949
|
};
|
|
6919
6950
|
} else {
|
|
6920
6951
|
return {
|
|
@@ -6952,7 +6983,7 @@ var init_EventStreamSerde = __esm({
|
|
|
6952
6983
|
}
|
|
6953
6984
|
};
|
|
6954
6985
|
}
|
|
6955
|
-
async readEventMember(eventStreamSchema,
|
|
6986
|
+
async readEventMember(eventStreamSchema, body2, hasBindings, out) {
|
|
6956
6987
|
let ErrCtor;
|
|
6957
6988
|
const staticStructuralSchema = eventStreamSchema.getSchema();
|
|
6958
6989
|
if (Array.isArray(staticStructuralSchema) && staticStructuralSchema[0] === -3) {
|
|
@@ -6961,7 +6992,7 @@ var init_EventStreamSerde = __esm({
|
|
|
6961
6992
|
this.compositeErrorRegistry?.copyFrom(nsRegistry);
|
|
6962
6993
|
ErrCtor = (this.compositeErrorRegistry ?? nsRegistry)?.getErrorCtor(staticStructuralSchema);
|
|
6963
6994
|
}
|
|
6964
|
-
const dataObject = hasBindings ? out :
|
|
6995
|
+
const dataObject = hasBindings ? out : body2.byteLength === 0 ? {} : await this.deserializer.read(eventStreamSchema, body2);
|
|
6965
6996
|
if (ErrCtor) {
|
|
6966
6997
|
const message2 = dataObject.message ?? dataObject.Message ?? "Unknown";
|
|
6967
6998
|
const metadata = {};
|
|
@@ -7039,9 +7070,9 @@ var init_EventStreamSerde = __esm({
|
|
|
7039
7070
|
}
|
|
7040
7071
|
}
|
|
7041
7072
|
const messageSerialization = serializer.flush() ?? new Uint8Array();
|
|
7042
|
-
const
|
|
7073
|
+
const body2 = typeof messageSerialization === "string" ? (this.serdeContext?.utf8Decoder ?? fromUtf8)(messageSerialization) : messageSerialization;
|
|
7043
7074
|
return {
|
|
7044
|
-
body,
|
|
7075
|
+
body: body2,
|
|
7045
7076
|
eventType,
|
|
7046
7077
|
explicitPayloadContentType,
|
|
7047
7078
|
additionalHeaders
|
|
@@ -7995,10 +8026,10 @@ function contentLengthMiddleware(bodyLengthChecker) {
|
|
|
7995
8026
|
return (next) => async (args) => {
|
|
7996
8027
|
const request = args.request;
|
|
7997
8028
|
if (HttpRequest.isInstance(request)) {
|
|
7998
|
-
const { body, headers } = request;
|
|
7999
|
-
if (
|
|
8029
|
+
const { body: body2, headers } = request;
|
|
8030
|
+
if (body2 && Object.keys(headers).map((str) => str.toLowerCase()).indexOf(CONTENT_LENGTH_HEADER) === -1) {
|
|
8000
8031
|
try {
|
|
8001
|
-
const length = bodyLengthChecker(
|
|
8032
|
+
const length = bodyLengthChecker(body2);
|
|
8002
8033
|
if (length != null) {
|
|
8003
8034
|
request.headers = {
|
|
8004
8035
|
...request.headers,
|
|
@@ -8123,17 +8154,17 @@ var init_constants3 = __esm({
|
|
|
8123
8154
|
});
|
|
8124
8155
|
|
|
8125
8156
|
// ../../node_modules/@smithy/core/dist-es/submodules/retry/service-error-classification/service-error-classification.js
|
|
8126
|
-
function isNodeJsHttp2TransientError(
|
|
8127
|
-
return
|
|
8157
|
+
function isNodeJsHttp2TransientError(error2) {
|
|
8158
|
+
return error2.code === "ERR_HTTP2_STREAM_ERROR" && error2.message.includes("NGHTTP2_REFUSED_STREAM");
|
|
8128
8159
|
}
|
|
8129
8160
|
var isRetryableByTrait, isClockSkewCorrectedError, isBrowserNetworkError, isThrottlingError, isTransientError, isServerError;
|
|
8130
8161
|
var init_service_error_classification = __esm({
|
|
8131
8162
|
"../../node_modules/@smithy/core/dist-es/submodules/retry/service-error-classification/service-error-classification.js"() {
|
|
8132
8163
|
"use strict";
|
|
8133
8164
|
init_constants3();
|
|
8134
|
-
isRetryableByTrait = (
|
|
8135
|
-
isClockSkewCorrectedError = (
|
|
8136
|
-
isBrowserNetworkError = (
|
|
8165
|
+
isRetryableByTrait = (error2) => error2?.$retryable !== void 0;
|
|
8166
|
+
isClockSkewCorrectedError = (error2) => error2.$metadata?.clockSkewCorrected;
|
|
8167
|
+
isBrowserNetworkError = (error2) => {
|
|
8137
8168
|
const errorMessages = /* @__PURE__ */ new Set([
|
|
8138
8169
|
"Failed to fetch",
|
|
8139
8170
|
"NetworkError when attempting to fetch resource",
|
|
@@ -8141,18 +8172,18 @@ var init_service_error_classification = __esm({
|
|
|
8141
8172
|
"Load failed",
|
|
8142
8173
|
"Network request failed"
|
|
8143
8174
|
]);
|
|
8144
|
-
const isValid =
|
|
8175
|
+
const isValid = error2 && error2 instanceof TypeError;
|
|
8145
8176
|
if (!isValid) {
|
|
8146
8177
|
return false;
|
|
8147
8178
|
}
|
|
8148
|
-
return errorMessages.has(
|
|
8179
|
+
return errorMessages.has(error2.message);
|
|
8149
8180
|
};
|
|
8150
|
-
isThrottlingError = (
|
|
8151
|
-
isTransientError = (
|
|
8152
|
-
isServerError = (
|
|
8153
|
-
if (
|
|
8154
|
-
const statusCode =
|
|
8155
|
-
if (500 <= statusCode && statusCode <= 599 && !isTransientError(
|
|
8181
|
+
isThrottlingError = (error2) => error2.$metadata?.httpStatusCode === 429 || THROTTLING_ERROR_CODES.includes(error2.name) || error2.$retryable?.throttling == true;
|
|
8182
|
+
isTransientError = (error2, depth = 0) => error2?.name !== "AbortError" && (isRetryableByTrait(error2) || isClockSkewCorrectedError(error2) || error2.name === "InvalidSignatureException" && error2.message?.includes("Signature expired") || TRANSIENT_ERROR_CODES.includes(error2.name) || NODEJS_TIMEOUT_ERROR_CODES.includes(error2?.code || "") || NODEJS_NETWORK_ERROR_CODES.includes(error2?.code || "") || TRANSIENT_ERROR_STATUS_CODES.includes(error2.$metadata?.httpStatusCode || 0) || isBrowserNetworkError(error2) || isNodeJsHttp2TransientError(error2) || error2.cause !== void 0 && depth <= 10 && isTransientError(error2.cause, depth + 1));
|
|
8183
|
+
isServerError = (error2) => {
|
|
8184
|
+
if (error2.$metadata?.httpStatusCode !== void 0) {
|
|
8185
|
+
const statusCode = error2.$metadata.httpStatusCode;
|
|
8186
|
+
if (500 <= statusCode && statusCode <= 599 && !isTransientError(error2)) {
|
|
8156
8187
|
return true;
|
|
8157
8188
|
}
|
|
8158
8189
|
return false;
|
|
@@ -8231,14 +8262,14 @@ var asSdkError;
|
|
|
8231
8262
|
var init_util = __esm({
|
|
8232
8263
|
"../../node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/util.js"() {
|
|
8233
8264
|
"use strict";
|
|
8234
|
-
asSdkError = (
|
|
8235
|
-
if (
|
|
8236
|
-
return
|
|
8237
|
-
if (
|
|
8238
|
-
return Object.assign(new Error(),
|
|
8239
|
-
if (typeof
|
|
8240
|
-
return new Error(
|
|
8241
|
-
return new Error(`AWS SDK error wrapper for ${
|
|
8265
|
+
asSdkError = (error2) => {
|
|
8266
|
+
if (error2 instanceof Error)
|
|
8267
|
+
return error2;
|
|
8268
|
+
if (error2 instanceof Object)
|
|
8269
|
+
return Object.assign(new Error(), error2);
|
|
8270
|
+
if (typeof error2 === "string")
|
|
8271
|
+
return new Error(error2);
|
|
8272
|
+
return new Error(`AWS SDK error wrapper for ${error2}`);
|
|
8242
8273
|
};
|
|
8243
8274
|
}
|
|
8244
8275
|
});
|
|
@@ -8324,23 +8355,23 @@ var init_retryMiddleware = __esm({
|
|
|
8324
8355
|
init_util();
|
|
8325
8356
|
cooldown = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8326
8357
|
isRetryStrategyV2 = (retryStrategy) => typeof retryStrategy.acquireInitialRetryToken !== "undefined" && typeof retryStrategy.refreshRetryTokenForRetry !== "undefined" && typeof retryStrategy.recordSuccess !== "undefined";
|
|
8327
|
-
getRetryErrorInfo = (
|
|
8358
|
+
getRetryErrorInfo = (error2, logger2) => {
|
|
8328
8359
|
const errorInfo = {
|
|
8329
|
-
error,
|
|
8330
|
-
errorType: getRetryErrorType(
|
|
8360
|
+
error: error2,
|
|
8361
|
+
errorType: getRetryErrorType(error2)
|
|
8331
8362
|
};
|
|
8332
|
-
const retryAfterHint = parseRetryAfterHeader(
|
|
8363
|
+
const retryAfterHint = parseRetryAfterHeader(error2.$response, logger2);
|
|
8333
8364
|
if (retryAfterHint) {
|
|
8334
8365
|
errorInfo.retryAfterHint = retryAfterHint;
|
|
8335
8366
|
}
|
|
8336
8367
|
return errorInfo;
|
|
8337
8368
|
};
|
|
8338
|
-
getRetryErrorType = (
|
|
8339
|
-
if (isThrottlingError(
|
|
8369
|
+
getRetryErrorType = (error2) => {
|
|
8370
|
+
if (isThrottlingError(error2))
|
|
8340
8371
|
return "THROTTLING";
|
|
8341
|
-
if (isTransientError(
|
|
8372
|
+
if (isTransientError(error2))
|
|
8342
8373
|
return "TRANSIENT";
|
|
8343
|
-
if (isServerError(
|
|
8374
|
+
if (isServerError(error2))
|
|
8344
8375
|
return "SERVER_ERROR";
|
|
8345
8376
|
return "CLIENT_ERROR";
|
|
8346
8377
|
};
|
|
@@ -8877,7 +8908,7 @@ var init_loggerMiddleware = __esm({
|
|
|
8877
8908
|
metadata: $metadata
|
|
8878
8909
|
});
|
|
8879
8910
|
return response;
|
|
8880
|
-
} catch (
|
|
8911
|
+
} catch (error2) {
|
|
8881
8912
|
const { clientName, commandName, logger: logger2, dynamoDbDocumentClientOptions = {} } = context;
|
|
8882
8913
|
const { overrideInputFilterSensitiveLog } = dynamoDbDocumentClientOptions;
|
|
8883
8914
|
const inputFilterSensitiveLog = overrideInputFilterSensitiveLog ?? context.inputFilterSensitiveLog;
|
|
@@ -8885,10 +8916,10 @@ var init_loggerMiddleware = __esm({
|
|
|
8885
8916
|
clientName,
|
|
8886
8917
|
commandName,
|
|
8887
8918
|
input: inputFilterSensitiveLog(args.input),
|
|
8888
|
-
error,
|
|
8889
|
-
metadata:
|
|
8919
|
+
error: error2,
|
|
8920
|
+
metadata: error2.$metadata
|
|
8890
8921
|
});
|
|
8891
|
-
throw
|
|
8922
|
+
throw error2;
|
|
8892
8923
|
}
|
|
8893
8924
|
};
|
|
8894
8925
|
loggerMiddlewareOptions = {
|
|
@@ -9246,8 +9277,8 @@ var init_httpSigningMiddleware = __esm({
|
|
|
9246
9277
|
"use strict";
|
|
9247
9278
|
init_protocols();
|
|
9248
9279
|
init_transport();
|
|
9249
|
-
defaultErrorHandler = (signingProperties) => (
|
|
9250
|
-
throw
|
|
9280
|
+
defaultErrorHandler = (signingProperties) => (error2) => {
|
|
9281
|
+
throw error2;
|
|
9251
9282
|
};
|
|
9252
9283
|
defaultSuccessHandler = (httpResponse, signingProperties) => {
|
|
9253
9284
|
};
|
|
@@ -11000,8 +11031,8 @@ var init_AwsSdkSigV4Signer = __esm({
|
|
|
11000
11031
|
return signedRequest;
|
|
11001
11032
|
}
|
|
11002
11033
|
errorHandler(signingProperties) {
|
|
11003
|
-
return (
|
|
11004
|
-
const errorException =
|
|
11034
|
+
return (error2) => {
|
|
11035
|
+
const errorException = error2;
|
|
11005
11036
|
if (!signingProperties._disableClockSkewCorrection) {
|
|
11006
11037
|
const serverTime = errorException.ServerTime ?? getDateHeader(errorException.$response);
|
|
11007
11038
|
if (serverTime) {
|
|
@@ -11019,7 +11050,7 @@ var init_AwsSdkSigV4Signer = __esm({
|
|
|
11019
11050
|
}
|
|
11020
11051
|
}
|
|
11021
11052
|
}
|
|
11022
|
-
throw
|
|
11053
|
+
throw error2;
|
|
11023
11054
|
};
|
|
11024
11055
|
}
|
|
11025
11056
|
successHandler(httpResponse, signingProperties) {
|
|
@@ -11536,7 +11567,7 @@ var init_getPayloadHash = __esm({
|
|
|
11536
11567
|
init_serde();
|
|
11537
11568
|
init_serde();
|
|
11538
11569
|
init_constants6();
|
|
11539
|
-
getPayloadHash = async ({ headers, body }, hashConstructor) => {
|
|
11570
|
+
getPayloadHash = async ({ headers, body: body2 }, hashConstructor) => {
|
|
11540
11571
|
for (const headerName in headers) {
|
|
11541
11572
|
if (!hasOwn(headers, headerName))
|
|
11542
11573
|
continue;
|
|
@@ -11544,11 +11575,11 @@ var init_getPayloadHash = __esm({
|
|
|
11544
11575
|
return headers[headerName];
|
|
11545
11576
|
}
|
|
11546
11577
|
}
|
|
11547
|
-
if (
|
|
11578
|
+
if (body2 == void 0) {
|
|
11548
11579
|
return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
11549
|
-
} else if (typeof
|
|
11580
|
+
} else if (typeof body2 === "string" || ArrayBuffer.isView(body2) || isArrayBuffer(body2)) {
|
|
11550
11581
|
const hashCtor = new hashConstructor();
|
|
11551
|
-
hashCtor.update(toUint8Array(
|
|
11582
|
+
hashCtor.update(toUint8Array(body2));
|
|
11552
11583
|
return toHex(await hashCtor.digest());
|
|
11553
11584
|
}
|
|
11554
11585
|
return UNSIGNED_PAYLOAD;
|
|
@@ -17182,12 +17213,12 @@ var init_fromInstanceMetadata = __esm({
|
|
|
17182
17213
|
let token;
|
|
17183
17214
|
try {
|
|
17184
17215
|
token = (await getMetadataToken({ ...endpoint, timeout })).toString();
|
|
17185
|
-
} catch (
|
|
17186
|
-
if (
|
|
17187
|
-
throw Object.assign(
|
|
17216
|
+
} catch (error2) {
|
|
17217
|
+
if (error2?.statusCode === 400) {
|
|
17218
|
+
throw Object.assign(error2, {
|
|
17188
17219
|
message: "EC2 Metadata token request returned error"
|
|
17189
17220
|
});
|
|
17190
|
-
} else if (
|
|
17221
|
+
} else if (error2.message === "TimeoutError" || [403, 404, 405].includes(error2.statusCode)) {
|
|
17191
17222
|
disableFetchToken = true;
|
|
17192
17223
|
}
|
|
17193
17224
|
logger2?.debug("AWS SDK Instance Metadata", "using v1 fallback (initial)");
|
|
@@ -17379,12 +17410,12 @@ var init_set_request_timeout = __esm({
|
|
|
17379
17410
|
return timing.setTimeout(() => {
|
|
17380
17411
|
let msg = `@smithy/node-http-handler - [${throwOnRequestTimeout ? "ERROR" : "WARN"}] a request has exceeded the configured ${timeoutInMs} ms requestTimeout.`;
|
|
17381
17412
|
if (throwOnRequestTimeout) {
|
|
17382
|
-
const
|
|
17413
|
+
const error2 = Object.assign(new Error(msg), {
|
|
17383
17414
|
name: "TimeoutError",
|
|
17384
17415
|
code: "ETIMEDOUT"
|
|
17385
17416
|
});
|
|
17386
|
-
req.destroy(
|
|
17387
|
-
reject(
|
|
17417
|
+
req.destroy(error2);
|
|
17418
|
+
reject(error2);
|
|
17388
17419
|
} else {
|
|
17389
17420
|
msg += ` Init client requestHandler with throwOnRequestTimeout=true to turn this into an error.`;
|
|
17390
17421
|
logger2?.warn?.(msg);
|
|
@@ -17487,28 +17518,28 @@ async function writeRequestBody(httpRequest2, request, maxContinueTimeoutMs = MI
|
|
|
17487
17518
|
writeBody(httpRequest2, request.body);
|
|
17488
17519
|
}
|
|
17489
17520
|
}
|
|
17490
|
-
function writeBody(httpRequest2,
|
|
17491
|
-
if (
|
|
17492
|
-
|
|
17521
|
+
function writeBody(httpRequest2, body2) {
|
|
17522
|
+
if (body2 instanceof Readable4) {
|
|
17523
|
+
body2.pipe(httpRequest2);
|
|
17493
17524
|
return;
|
|
17494
17525
|
}
|
|
17495
|
-
if (
|
|
17496
|
-
const isBuffer = Buffer.isBuffer(
|
|
17497
|
-
const isString = typeof
|
|
17526
|
+
if (body2) {
|
|
17527
|
+
const isBuffer = Buffer.isBuffer(body2);
|
|
17528
|
+
const isString = typeof body2 === "string";
|
|
17498
17529
|
if (isBuffer || isString) {
|
|
17499
|
-
if (isBuffer &&
|
|
17530
|
+
if (isBuffer && body2.byteLength === 0) {
|
|
17500
17531
|
httpRequest2.end();
|
|
17501
17532
|
} else {
|
|
17502
|
-
httpRequest2.end(
|
|
17533
|
+
httpRequest2.end(body2);
|
|
17503
17534
|
}
|
|
17504
17535
|
return;
|
|
17505
17536
|
}
|
|
17506
|
-
const uint8 =
|
|
17537
|
+
const uint8 = body2;
|
|
17507
17538
|
if (typeof uint8 === "object" && uint8.buffer && typeof uint8.byteOffset === "number" && typeof uint8.byteLength === "number") {
|
|
17508
17539
|
httpRequest2.end(Buffer.from(uint8.buffer, uint8.byteOffset, uint8.byteLength));
|
|
17509
17540
|
return;
|
|
17510
17541
|
}
|
|
17511
|
-
httpRequest2.end(Buffer.from(
|
|
17542
|
+
httpRequest2.end(Buffer.from(body2));
|
|
17512
17543
|
return;
|
|
17513
17544
|
}
|
|
17514
17545
|
httpRequest2.end();
|
|
@@ -18391,20 +18422,20 @@ var init_ProtocolLib = __esm({
|
|
|
18391
18422
|
decorateServiceException(exception, additions = {}) {
|
|
18392
18423
|
if (this.queryCompat) {
|
|
18393
18424
|
const msg = exception.Message ?? additions.Message;
|
|
18394
|
-
const
|
|
18425
|
+
const error2 = decorateServiceException(exception, additions);
|
|
18395
18426
|
if (msg) {
|
|
18396
|
-
|
|
18397
|
-
}
|
|
18398
|
-
const errorObj =
|
|
18399
|
-
errorObj.Type =
|
|
18400
|
-
errorObj.Code =
|
|
18401
|
-
errorObj.Message =
|
|
18402
|
-
|
|
18403
|
-
const reqId =
|
|
18427
|
+
error2.message = msg;
|
|
18428
|
+
}
|
|
18429
|
+
const errorObj = error2.Error ?? {};
|
|
18430
|
+
errorObj.Type = error2.Error?.Type;
|
|
18431
|
+
errorObj.Code = error2.Error?.Code;
|
|
18432
|
+
errorObj.Message = error2.Error?.message ?? error2.Error?.Message ?? msg;
|
|
18433
|
+
error2.Error = errorObj;
|
|
18434
|
+
const reqId = error2.$metadata.requestId;
|
|
18404
18435
|
if (reqId) {
|
|
18405
|
-
|
|
18436
|
+
error2.RequestId = reqId;
|
|
18406
18437
|
}
|
|
18407
|
-
return
|
|
18438
|
+
return error2;
|
|
18408
18439
|
}
|
|
18409
18440
|
return decorateServiceException(exception, additions);
|
|
18410
18441
|
}
|
|
@@ -18695,7 +18726,7 @@ var init_common = __esm({
|
|
|
18695
18726
|
"use strict";
|
|
18696
18727
|
init_protocols();
|
|
18697
18728
|
init_serde();
|
|
18698
|
-
collectBodyString = (streamBody, context) => collectBody(streamBody, context).then((
|
|
18729
|
+
collectBodyString = (streamBody, context) => collectBody(streamBody, context).then((body2) => (context?.utf8Encoder ?? toUtf8)(body2));
|
|
18699
18730
|
}
|
|
18700
18731
|
});
|
|
18701
18732
|
|
|
@@ -21614,13 +21645,13 @@ var init_fromSso = __esm({
|
|
|
21614
21645
|
expiresAt: newTokenExpiration.toISOString(),
|
|
21615
21646
|
refreshToken: newSsoOidcToken.refreshToken
|
|
21616
21647
|
});
|
|
21617
|
-
} catch (
|
|
21648
|
+
} catch (error2) {
|
|
21618
21649
|
}
|
|
21619
21650
|
return {
|
|
21620
21651
|
token: newSsoOidcToken.accessToken,
|
|
21621
21652
|
expiration: newTokenExpiration
|
|
21622
21653
|
};
|
|
21623
|
-
} catch (
|
|
21654
|
+
} catch (error2) {
|
|
21624
21655
|
validateTokenExpiry(existingToken);
|
|
21625
21656
|
return existingToken;
|
|
21626
21657
|
}
|
|
@@ -25091,9 +25122,9 @@ var init_LoginCredentialsFetcher = __esm({
|
|
|
25091
25122
|
};
|
|
25092
25123
|
await this.saveToken(updatedToken);
|
|
25093
25124
|
return this.toCredentials(updatedToken.accessToken);
|
|
25094
|
-
} catch (
|
|
25095
|
-
if (
|
|
25096
|
-
const errorType =
|
|
25125
|
+
} catch (error2) {
|
|
25126
|
+
if (error2.name === "AccessDeniedException") {
|
|
25127
|
+
const errorType = error2.error;
|
|
25097
25128
|
let message2;
|
|
25098
25129
|
switch (errorType) {
|
|
25099
25130
|
case "TOKEN_EXPIRED":
|
|
@@ -25106,7 +25137,7 @@ var init_LoginCredentialsFetcher = __esm({
|
|
|
25106
25137
|
message2 = "Unable to refresh credentials due to insufficient permissions. You may be missing permission for the 'CreateOAuth2Token' action.";
|
|
25107
25138
|
break;
|
|
25108
25139
|
default:
|
|
25109
|
-
message2 = `Failed to refresh token: ${String(
|
|
25140
|
+
message2 = `Failed to refresh token: ${String(error2)}. Please re-authenticate using \`aws login\``;
|
|
25110
25141
|
}
|
|
25111
25142
|
throw new CredentialsProviderError(message2, {
|
|
25112
25143
|
logger: this.logger,
|
|
@@ -25115,10 +25146,10 @@ var init_LoginCredentialsFetcher = __esm({
|
|
|
25115
25146
|
}
|
|
25116
25147
|
const tokenExpiry2 = new Date(freshToken.accessToken.expiresAt).getTime();
|
|
25117
25148
|
if (tokenExpiry2 > Date.now()) {
|
|
25118
|
-
this.logger?.warn?.(`Failed to refresh token: ${String(
|
|
25149
|
+
this.logger?.warn?.(`Failed to refresh token: ${String(error2)}. Using existing token until expiry.`);
|
|
25119
25150
|
return this.toCredentials(freshToken.accessToken);
|
|
25120
25151
|
}
|
|
25121
|
-
throw new CredentialsProviderError(`Failed to refresh token: ${String(
|
|
25152
|
+
throw new CredentialsProviderError(`Failed to refresh token: ${String(error2)}. Please re-authenticate using aws login`, { logger: this.logger });
|
|
25122
25153
|
}
|
|
25123
25154
|
}
|
|
25124
25155
|
async loadToken() {
|
|
@@ -25137,8 +25168,8 @@ var init_LoginCredentialsFetcher = __esm({
|
|
|
25137
25168
|
});
|
|
25138
25169
|
}
|
|
25139
25170
|
return token;
|
|
25140
|
-
} catch (
|
|
25141
|
-
throw new CredentialsProviderError(`Failed to load token from ${tokenFilePath}: ${String(
|
|
25171
|
+
} catch (error2) {
|
|
25172
|
+
throw new CredentialsProviderError(`Failed to load token from ${tokenFilePath}: ${String(error2)}`, {
|
|
25142
25173
|
logger: this.logger,
|
|
25143
25174
|
tryNextLink: false
|
|
25144
25175
|
});
|
|
@@ -25149,7 +25180,7 @@ var init_LoginCredentialsFetcher = __esm({
|
|
|
25149
25180
|
const directory = dirname(tokenFilePath);
|
|
25150
25181
|
try {
|
|
25151
25182
|
await fs2.mkdir(directory, { recursive: true });
|
|
25152
|
-
} catch (
|
|
25183
|
+
} catch (error2) {
|
|
25153
25184
|
}
|
|
25154
25185
|
await fs2.writeFile(tokenFilePath, JSON.stringify(token, null, 2), "utf8");
|
|
25155
25186
|
}
|
|
@@ -25240,8 +25271,8 @@ var init_LoginCredentialsFetcher = __esm({
|
|
|
25240
25271
|
const rawSignature = this.derToRawSignature(asn1Signature);
|
|
25241
25272
|
const signatureB64 = rawSignature.toString("base64url");
|
|
25242
25273
|
return `${message2}.${signatureB64}`;
|
|
25243
|
-
} catch (
|
|
25244
|
-
throw new CredentialsProviderError(`Failed to generate Dpop proof: ${
|
|
25274
|
+
} catch (error2) {
|
|
25275
|
+
throw new CredentialsProviderError(`Failed to generate Dpop proof: ${error2 instanceof Error ? error2.message : String(error2)}`, { logger: this.logger, tryNextLink: false });
|
|
25245
25276
|
}
|
|
25246
25277
|
}
|
|
25247
25278
|
};
|
|
@@ -25370,8 +25401,8 @@ var init_resolveProcessCredentials = __esm({
|
|
|
25370
25401
|
throw Error(`Profile ${profileName} credential_process returned invalid JSON.`);
|
|
25371
25402
|
}
|
|
25372
25403
|
return getValidatedProcessCredentials(profileName, data, profiles);
|
|
25373
|
-
} catch (
|
|
25374
|
-
throw new CredentialsProviderError(
|
|
25404
|
+
} catch (error2) {
|
|
25405
|
+
throw new CredentialsProviderError(error2.message, { logger: logger2 });
|
|
25375
25406
|
}
|
|
25376
25407
|
} else {
|
|
25377
25408
|
throw new CredentialsProviderError(`Profile ${profileName} did not contain credential_process.`, { logger: logger2 });
|
|
@@ -28679,16 +28710,16 @@ function stillMatches(where) {
|
|
|
28679
28710
|
});
|
|
28680
28711
|
return { text: parts.join(" AND "), names, values };
|
|
28681
28712
|
}
|
|
28682
|
-
function failedWrite(
|
|
28683
|
-
const { name, CancellationReasons } =
|
|
28713
|
+
function failedWrite(error2) {
|
|
28714
|
+
const { name, CancellationReasons } = error2;
|
|
28684
28715
|
if (name === "ConditionalCheckFailedException") return "refused";
|
|
28685
|
-
if (name && RETRYABLE_ERRORS.includes(name)) return { retry:
|
|
28716
|
+
if (name && RETRYABLE_ERRORS.includes(name)) return { retry: error2 };
|
|
28686
28717
|
if (name === "TransactionCanceledException") {
|
|
28687
28718
|
const reasons = (CancellationReasons ?? []).map((r6) => r6.Code ?? "None").filter((code) => code !== "None");
|
|
28688
28719
|
if (reasons.includes("ConditionalCheckFailed")) return "refused";
|
|
28689
|
-
if (reasons.length && reasons.every((code) => RETRYABLE_REASONS.includes(code))) return { retry:
|
|
28720
|
+
if (reasons.length && reasons.every((code) => RETRYABLE_REASONS.includes(code))) return { retry: error2 };
|
|
28690
28721
|
}
|
|
28691
|
-
throw
|
|
28722
|
+
throw error2;
|
|
28692
28723
|
}
|
|
28693
28724
|
function createDynamoDbAdapter(options) {
|
|
28694
28725
|
const { tableName: tableName2 } = options;
|
|
@@ -29793,8 +29824,8 @@ async function importWithAlgCache(cache7, jwk, entry) {
|
|
|
29793
29824
|
if (key.type !== "public")
|
|
29794
29825
|
throw new JWKSInvalid("JSON Web Key Set members must be public keys");
|
|
29795
29826
|
return cached[alg] = key, key;
|
|
29796
|
-
}).catch((
|
|
29797
|
-
throw cached[alg] === pending && delete cached[alg],
|
|
29827
|
+
}).catch((error2) => {
|
|
29828
|
+
throw cached[alg] === pending && delete cached[alg], error2;
|
|
29798
29829
|
});
|
|
29799
29830
|
cached[alg] = pending;
|
|
29800
29831
|
}
|
|
@@ -29820,14 +29851,14 @@ function createLocalJWKSet(jwks) {
|
|
|
29820
29851
|
if (!length)
|
|
29821
29852
|
throw new JWKSNoMatchingKey();
|
|
29822
29853
|
if (length !== 1) {
|
|
29823
|
-
const
|
|
29824
|
-
throw
|
|
29854
|
+
const error2 = new JWKSMultipleMatchingKeys();
|
|
29855
|
+
throw error2[Symbol.asyncIterator] = async function* () {
|
|
29825
29856
|
for (const jwk2 of candidates)
|
|
29826
29857
|
try {
|
|
29827
29858
|
yield await importWithAlgCache(cached, jwk2, entry);
|
|
29828
29859
|
} catch {
|
|
29829
29860
|
}
|
|
29830
|
-
},
|
|
29861
|
+
}, error2;
|
|
29831
29862
|
}
|
|
29832
29863
|
return importWithAlgCache(cached, jwk, entry);
|
|
29833
29864
|
}, "jwks", {
|
|
@@ -30020,13 +30051,17 @@ var WEBHOOK_TYPES = [
|
|
|
30020
30051
|
"email.verify",
|
|
30021
30052
|
"email.reset",
|
|
30022
30053
|
"email.already_registered",
|
|
30054
|
+
"email.no_password",
|
|
30023
30055
|
"signup.before",
|
|
30024
30056
|
"user.created",
|
|
30025
30057
|
"email.verified",
|
|
30026
30058
|
"email.password_reset",
|
|
30027
30059
|
"user.signed_in",
|
|
30028
30060
|
"user.deleted",
|
|
30029
|
-
"user.claims_changed"
|
|
30061
|
+
"user.claims_changed",
|
|
30062
|
+
"totp.enabled",
|
|
30063
|
+
"totp.disabled",
|
|
30064
|
+
"totp.recovery_code_used"
|
|
30030
30065
|
];
|
|
30031
30066
|
var WEBHOOK_TIMEOUT_MS = 1e4;
|
|
30032
30067
|
var MAX_LOCALE_LENGTH = 35;
|
|
@@ -30050,8 +30085,8 @@ function hmac2(secret, content) {
|
|
|
30050
30085
|
const key = Buffer.from(secret.slice(SECRET_PREFIX.length), "base64");
|
|
30051
30086
|
return createHmac("sha256", key).update(content).digest("base64");
|
|
30052
30087
|
}
|
|
30053
|
-
function signWebhook(secret, id, timestamp,
|
|
30054
|
-
return `v1,${hmac2(secret, `${id}.${timestamp}.${
|
|
30088
|
+
function signWebhook(secret, id, timestamp, body2) {
|
|
30089
|
+
return `v1,${hmac2(secret, `${id}.${timestamp}.${body2}`)}`;
|
|
30055
30090
|
}
|
|
30056
30091
|
function createWebhookClient(settings, fetchImpl = fetch) {
|
|
30057
30092
|
const wants = (type) => settings.events.has(type);
|
|
@@ -30061,7 +30096,7 @@ function createWebhookClient(settings, fetchImpl = fetch) {
|
|
|
30061
30096
|
if (!wants(type)) return { ok: true, body: void 0 };
|
|
30062
30097
|
const id = `msg_${randomUUID()}`;
|
|
30063
30098
|
const timestamp = Math.floor(Date.now() / 1e3);
|
|
30064
|
-
const
|
|
30099
|
+
const body2 = JSON.stringify({ type, data });
|
|
30065
30100
|
let response;
|
|
30066
30101
|
try {
|
|
30067
30102
|
response = await fetchImpl(settings.url, {
|
|
@@ -30070,9 +30105,9 @@ function createWebhookClient(settings, fetchImpl = fetch) {
|
|
|
30070
30105
|
"Content-Type": "application/json",
|
|
30071
30106
|
"webhook-id": id,
|
|
30072
30107
|
"webhook-timestamp": String(timestamp),
|
|
30073
|
-
"webhook-signature": signWebhook(settings.secret, id, timestamp,
|
|
30108
|
+
"webhook-signature": signWebhook(settings.secret, id, timestamp, body2)
|
|
30074
30109
|
},
|
|
30075
|
-
body,
|
|
30110
|
+
body: body2,
|
|
30076
30111
|
signal: AbortSignal.timeout(timeoutMs)
|
|
30077
30112
|
});
|
|
30078
30113
|
} catch (e6) {
|
|
@@ -30093,6 +30128,12 @@ function createWebhookClient(settings, fetchImpl = fetch) {
|
|
|
30093
30128
|
}
|
|
30094
30129
|
|
|
30095
30130
|
// src/config.ts
|
|
30131
|
+
function sendsVerificationEmails(config) {
|
|
30132
|
+
return !!config.webhook?.events.has("email.verify");
|
|
30133
|
+
}
|
|
30134
|
+
function methodConfig(config) {
|
|
30135
|
+
return { google: config.google, password: config.password, emailVerification: sendsVerificationEmails(config) };
|
|
30136
|
+
}
|
|
30096
30137
|
async function resolveEnv(source = process.env) {
|
|
30097
30138
|
return typeof source === "function" ? source() : source;
|
|
30098
30139
|
}
|
|
@@ -30156,9 +30197,10 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
|
|
|
30156
30197
|
const webhook = webhookFromEnv(read("WEBHOOK_URL"), read("WEBHOOK_SECRET"), read("WEBHOOK_EVENTS"));
|
|
30157
30198
|
const sendsEmails = !!webhook && REQUIRED_EMAIL_TYPES.every((type) => webhook.events.has(type));
|
|
30158
30199
|
const password = sendsEmails ? { minLength: passwordMinLength(read("PASSWORD_MIN_LENGTH")) } : void 0;
|
|
30159
|
-
|
|
30200
|
+
const emailVerification = !!webhook && webhook.events.has("email.verify");
|
|
30201
|
+
if (!clientId && !password && !emailVerification) {
|
|
30160
30202
|
throw new ConfigError(
|
|
30161
|
-
`No sign-in method is configured. Set GOOGLE_CLIENT_ID for Google sign-in, and/or a webhook that sends the e-mails
|
|
30203
|
+
`No sign-in method is configured. Set GOOGLE_CLIENT_ID for Google sign-in, and/or a webhook that sends the e-mails: e-mail & password sign-in needs email.verify and email.reset, sign-up with the authenticator app needs email.verify. 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,email.no_password. See docs/server.md.`
|
|
30162
30204
|
);
|
|
30163
30205
|
}
|
|
30164
30206
|
const store = overrides.store ?? await storeFromDatabaseUrl(read("DATABASE_URL"));
|
|
@@ -30186,6 +30228,7 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
|
|
|
30186
30228
|
sessionTtlSeconds,
|
|
30187
30229
|
renewalTtlSeconds,
|
|
30188
30230
|
cookieDomain: read("COOKIE_DOMAIN"),
|
|
30231
|
+
totpIssuer: read("TOTP_ISSUER"),
|
|
30189
30232
|
google: clientId ? { clientId, clientSecret: read("GOOGLE_CLIENT_SECRET") } : void 0,
|
|
30190
30233
|
store,
|
|
30191
30234
|
password,
|
|
@@ -30267,7 +30310,7 @@ WEBHOOK_SECRET=${generateWebhookSecret()}
|
|
|
30267
30310
|
throw new ConfigError(
|
|
30268
30311
|
`WEBHOOK_EVENTS is not set. List the types your webhook receiver handles; only these are sent. E-mail & password sign-in is on when they include ${REQUIRED_EMAIL_TYPES.join(" and ")}. For a receiver that sends the e-mails:
|
|
30269
30312
|
|
|
30270
|
-
WEBHOOK_EVENTS=email.verify,email.reset,email.already_registered
|
|
30313
|
+
WEBHOOK_EVENTS=email.verify,email.reset,email.already_registered,email.no_password
|
|
30271
30314
|
|
|
30272
30315
|
For one that only wants to know who signed in:
|
|
30273
30316
|
|
|
@@ -30584,8 +30627,8 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
|
|
|
30584
30627
|
const { done, value } = await reader.read();
|
|
30585
30628
|
if (done) controller.close();
|
|
30586
30629
|
else controller.enqueue(value);
|
|
30587
|
-
} catch (
|
|
30588
|
-
controller.error(
|
|
30630
|
+
} catch (error2) {
|
|
30631
|
+
controller.error(error2);
|
|
30589
30632
|
}
|
|
30590
30633
|
} });
|
|
30591
30634
|
} else {
|
|
@@ -30713,13 +30756,13 @@ var readBodyDirect = (request) => {
|
|
|
30713
30756
|
cleanup();
|
|
30714
30757
|
callback();
|
|
30715
30758
|
};
|
|
30716
|
-
const recoverCompleteBodyAfterDisconnect = (
|
|
30717
|
-
const streamError = incoming.errored ??
|
|
30759
|
+
const recoverCompleteBodyAfterDisconnect = (error2) => {
|
|
30760
|
+
const streamError = incoming.errored ?? error2;
|
|
30718
30761
|
if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false;
|
|
30719
30762
|
finish(() => {
|
|
30720
30763
|
const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks);
|
|
30721
30764
|
if (recovered instanceof Error) reject(recovered);
|
|
30722
|
-
else if (recovered === void 0) reject(
|
|
30765
|
+
else if (recovered === void 0) reject(error2 ?? normalizeAbortError(request, incoming));
|
|
30723
30766
|
else {
|
|
30724
30767
|
request[bodyBufferKey] = recovered;
|
|
30725
30768
|
resolve(recovered);
|
|
@@ -30737,10 +30780,10 @@ var readBodyDirect = (request) => {
|
|
|
30737
30780
|
resolve(buffer);
|
|
30738
30781
|
});
|
|
30739
30782
|
};
|
|
30740
|
-
const onError = (
|
|
30741
|
-
if (recoverCompleteBodyAfterDisconnect(
|
|
30783
|
+
const onError = (error2) => {
|
|
30784
|
+
if (recoverCompleteBodyAfterDisconnect(error2)) return;
|
|
30742
30785
|
finish(() => {
|
|
30743
|
-
reject(
|
|
30786
|
+
reject(error2);
|
|
30744
30787
|
});
|
|
30745
30788
|
};
|
|
30746
30789
|
const onClose = () => {
|
|
@@ -30934,9 +30977,9 @@ var Response$1 = class Response$12 {
|
|
|
30934
30977
|
headers: liveHeaders
|
|
30935
30978
|
} : this.#init);
|
|
30936
30979
|
}
|
|
30937
|
-
constructor(
|
|
30980
|
+
constructor(body2, init) {
|
|
30938
30981
|
let headers;
|
|
30939
|
-
this.#body =
|
|
30982
|
+
this.#body = body2;
|
|
30940
30983
|
if (init instanceof GlobalResponse) {
|
|
30941
30984
|
const cachedGlobalResponse = init[responseCache];
|
|
30942
30985
|
if (cachedGlobalResponse) {
|
|
@@ -30947,9 +30990,9 @@ var Response$1 = class Response$12 {
|
|
|
30947
30990
|
this.#init = init instanceof Response$12 ? init.#init : init;
|
|
30948
30991
|
headers = new Headers(init.headers);
|
|
30949
30992
|
} else this.#init = init;
|
|
30950
|
-
if (
|
|
30993
|
+
if (body2 == null || typeof body2 === "string" || typeof body2?.getReader !== "undefined" || body2 instanceof Blob || body2 instanceof Uint8Array) this[cacheKey] = [
|
|
30951
30994
|
init?.status || 200,
|
|
30952
|
-
|
|
30995
|
+
body2 ?? null,
|
|
30953
30996
|
headers || init?.headers
|
|
30954
30997
|
];
|
|
30955
30998
|
}
|
|
@@ -31033,15 +31076,15 @@ Object.defineProperty(Response$1, "redirect", {
|
|
|
31033
31076
|
});
|
|
31034
31077
|
Object.defineProperty(Response$1, "json", {
|
|
31035
31078
|
value: function json(data, init) {
|
|
31036
|
-
const
|
|
31037
|
-
if (
|
|
31079
|
+
const body2 = JSON.stringify(data);
|
|
31080
|
+
if (body2 === void 0) throw new TypeError("The data is not JSON serializable");
|
|
31038
31081
|
const initHeaders = init?.headers;
|
|
31039
31082
|
let headers;
|
|
31040
31083
|
if (initHeaders) {
|
|
31041
31084
|
headers = new Headers(initHeaders);
|
|
31042
31085
|
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
31043
31086
|
} else headers = { "content-type": "application/json" };
|
|
31044
|
-
return new Response$1(
|
|
31087
|
+
return new Response$1(body2, {
|
|
31045
31088
|
status: init?.status ?? 200,
|
|
31046
31089
|
statusText: init?.statusText,
|
|
31047
31090
|
headers
|
|
@@ -31054,8 +31097,8 @@ async function readWithoutBlocking(readPromise) {
|
|
|
31054
31097
|
return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]);
|
|
31055
31098
|
}
|
|
31056
31099
|
function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) {
|
|
31057
|
-
const cancel = (
|
|
31058
|
-
reader.cancel(
|
|
31100
|
+
const cancel = (error2) => {
|
|
31101
|
+
reader.cancel(error2).catch(() => {
|
|
31059
31102
|
});
|
|
31060
31103
|
};
|
|
31061
31104
|
writable.on("close", cancel);
|
|
@@ -31065,8 +31108,8 @@ function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromi
|
|
|
31065
31108
|
writable.off("close", cancel);
|
|
31066
31109
|
writable.off("error", cancel);
|
|
31067
31110
|
});
|
|
31068
|
-
function handleStreamError(
|
|
31069
|
-
if (
|
|
31111
|
+
function handleStreamError(error2) {
|
|
31112
|
+
if (error2) writable.destroy(error2);
|
|
31070
31113
|
}
|
|
31071
31114
|
function onDrain() {
|
|
31072
31115
|
reader.read().then(flow, handleStreamError);
|
|
@@ -31155,8 +31198,8 @@ var makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => {
|
|
|
31155
31198
|
};
|
|
31156
31199
|
var isImmediateCacheableResponse = (res) => {
|
|
31157
31200
|
if (!(cacheKey in res)) return false;
|
|
31158
|
-
const
|
|
31159
|
-
return
|
|
31201
|
+
const body2 = res[cacheKey][1];
|
|
31202
|
+
return body2 === null || typeof body2 === "string" || body2 instanceof Uint8Array;
|
|
31160
31203
|
};
|
|
31161
31204
|
var handleRequestError = () => new Response(null, { status: 400 });
|
|
31162
31205
|
var handleFetchError = (e6) => new Response(null, { status: e6 instanceof Error && (e6.name === "TimeoutError" || e6.constructor.name === "TimeoutError") ? 504 : 500 });
|
|
@@ -31192,24 +31235,24 @@ var writeDefaultHeaders = (outgoing, status, length) => {
|
|
|
31192
31235
|
});
|
|
31193
31236
|
};
|
|
31194
31237
|
var responseViaCache = async (res, outgoing) => {
|
|
31195
|
-
let [status,
|
|
31238
|
+
let [status, body2, header] = res[cacheKey];
|
|
31196
31239
|
if (!header) {
|
|
31197
|
-
if (
|
|
31240
|
+
if (body2 === null) {
|
|
31198
31241
|
outgoing.writeHead(status);
|
|
31199
31242
|
outgoing.end();
|
|
31200
|
-
} else if (typeof
|
|
31201
|
-
writeDefaultHeaders(outgoing, status, Buffer.byteLength(
|
|
31202
|
-
outgoing.end(
|
|
31203
|
-
} else if (
|
|
31204
|
-
writeDefaultHeaders(outgoing, status,
|
|
31205
|
-
outgoing.end(
|
|
31206
|
-
} else if (
|
|
31207
|
-
writeDefaultHeaders(outgoing, status,
|
|
31208
|
-
outgoing.end(new Uint8Array(await
|
|
31243
|
+
} else if (typeof body2 === "string") {
|
|
31244
|
+
writeDefaultHeaders(outgoing, status, Buffer.byteLength(body2));
|
|
31245
|
+
outgoing.end(body2);
|
|
31246
|
+
} else if (body2 instanceof Uint8Array) {
|
|
31247
|
+
writeDefaultHeaders(outgoing, status, body2.byteLength);
|
|
31248
|
+
outgoing.end(body2);
|
|
31249
|
+
} else if (body2 instanceof Blob) {
|
|
31250
|
+
writeDefaultHeaders(outgoing, status, body2.size);
|
|
31251
|
+
outgoing.end(new Uint8Array(await body2.arrayBuffer()));
|
|
31209
31252
|
} else {
|
|
31210
31253
|
outgoing.writeHead(status, { "Content-Type": defaultContentType });
|
|
31211
31254
|
flushHeaders(outgoing);
|
|
31212
|
-
await writeFromReadableStream(
|
|
31255
|
+
await writeFromReadableStream(body2, outgoing)?.catch((e6) => handleResponseError(e6, outgoing));
|
|
31213
31256
|
}
|
|
31214
31257
|
outgoing[outgoingEnded]?.();
|
|
31215
31258
|
return;
|
|
@@ -31219,11 +31262,11 @@ var responseViaCache = async (res, outgoing) => {
|
|
|
31219
31262
|
let canAutoLength = true;
|
|
31220
31263
|
if (header instanceof Headers) {
|
|
31221
31264
|
hasContentLength = header.has("content-length");
|
|
31222
|
-
header = buildOutgoingHttpHeaders(header,
|
|
31265
|
+
header = buildOutgoingHttpHeaders(header, body2 === null ? void 0 : defaultContentType);
|
|
31223
31266
|
} else if (Array.isArray(header)) {
|
|
31224
31267
|
const headerObj = new Headers(header);
|
|
31225
31268
|
hasContentLength = headerObj.has("content-length");
|
|
31226
|
-
header = buildOutgoingHttpHeaders(headerObj,
|
|
31269
|
+
header = buildOutgoingHttpHeaders(headerObj, body2 === null ? void 0 : defaultContentType);
|
|
31227
31270
|
} else {
|
|
31228
31271
|
plainHeaders = true;
|
|
31229
31272
|
for (const key in header) {
|
|
@@ -31236,21 +31279,21 @@ var responseViaCache = async (res, outgoing) => {
|
|
|
31236
31279
|
}
|
|
31237
31280
|
if (!hasContentLength) {
|
|
31238
31281
|
let length;
|
|
31239
|
-
if (typeof
|
|
31240
|
-
else if (
|
|
31241
|
-
else if (
|
|
31282
|
+
if (typeof body2 === "string") length = Buffer.byteLength(body2);
|
|
31283
|
+
else if (body2 instanceof Uint8Array) length = body2.byteLength;
|
|
31284
|
+
else if (body2 instanceof Blob) length = body2.size;
|
|
31242
31285
|
if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) {
|
|
31243
31286
|
if (plainHeaders) header = { ...header };
|
|
31244
31287
|
header["Content-Length"] = length;
|
|
31245
31288
|
}
|
|
31246
31289
|
}
|
|
31247
31290
|
outgoing.writeHead(status, header);
|
|
31248
|
-
if (
|
|
31249
|
-
else if (typeof
|
|
31250
|
-
else if (
|
|
31291
|
+
if (body2 == null) outgoing.end();
|
|
31292
|
+
else if (typeof body2 === "string" || body2 instanceof Uint8Array) outgoing.end(body2);
|
|
31293
|
+
else if (body2 instanceof Blob) outgoing.end(new Uint8Array(await body2.arrayBuffer()));
|
|
31251
31294
|
else {
|
|
31252
31295
|
flushHeaders(outgoing);
|
|
31253
|
-
await writeFromReadableStream(
|
|
31296
|
+
await writeFromReadableStream(body2, outgoing)?.catch((e6) => handleResponseError(e6, outgoing));
|
|
31254
31297
|
}
|
|
31255
31298
|
outgoing[outgoingEnded]?.();
|
|
31256
31299
|
};
|
|
@@ -31582,9 +31625,9 @@ var upgradeWebSocket = defineWebSocketHelper(async (c6, events, options) => {
|
|
|
31582
31625
|
(options?.onError ?? console.error)(e6);
|
|
31583
31626
|
}
|
|
31584
31627
|
});
|
|
31585
|
-
ws.on("error", (
|
|
31628
|
+
ws.on("error", (error2) => {
|
|
31586
31629
|
try {
|
|
31587
|
-
events?.onError?.(new ErrorEvent("error", { error }), ctx);
|
|
31630
|
+
events?.onError?.(new ErrorEvent("error", { error: error2 }), ctx);
|
|
31588
31631
|
} catch (e6) {
|
|
31589
31632
|
(options?.onError ?? console.error)(e6);
|
|
31590
31633
|
}
|
|
@@ -31942,10 +31985,10 @@ var HonoRequest = class {
|
|
|
31942
31985
|
const { bodyCache, raw: raw2 } = this;
|
|
31943
31986
|
const cachedBody = bodyCache[key];
|
|
31944
31987
|
if (cachedBody) return cachedBody;
|
|
31945
|
-
for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((
|
|
31946
|
-
if (anyCachedKey === "json")
|
|
31988
|
+
for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((body2) => {
|
|
31989
|
+
if (anyCachedKey === "json") body2 = JSON.stringify(body2);
|
|
31947
31990
|
const contentType2 = anyCachedKey === "formData" ? void 0 : raw2.headers.get("content-type");
|
|
31948
|
-
return new Response(
|
|
31991
|
+
return new Response(body2, { headers: contentType2 ? { "Content-Type": contentType2 } : void 0 })[key]();
|
|
31949
31992
|
});
|
|
31950
31993
|
return bodyCache[key] = raw2[key]();
|
|
31951
31994
|
};
|
|
@@ -32170,7 +32213,7 @@ var setDefaultContentType = (contentType2, headers) => {
|
|
|
32170
32213
|
...headers
|
|
32171
32214
|
};
|
|
32172
32215
|
};
|
|
32173
|
-
var createResponseInstance = (
|
|
32216
|
+
var createResponseInstance = (body2, init) => new Response(body2, init);
|
|
32174
32217
|
var Context = class {
|
|
32175
32218
|
#rawRequest;
|
|
32176
32219
|
#req;
|
|
@@ -33711,98 +33754,129 @@ function safeEqual(a6, b6) {
|
|
|
33711
33754
|
return x2.length === y2.length && timingSafeEqual2(x2, y2);
|
|
33712
33755
|
}
|
|
33713
33756
|
|
|
33714
|
-
// src/
|
|
33715
|
-
|
|
33716
|
-
|
|
33717
|
-
|
|
33718
|
-
|
|
33719
|
-
|
|
33757
|
+
// src/users.ts
|
|
33758
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
33759
|
+
|
|
33760
|
+
// src/settings.ts
|
|
33761
|
+
var SIGN_IN_METHODS = ["google", "password", "totp"];
|
|
33762
|
+
var METHODS_WITH_SECOND_FACTOR = ["google", "password"];
|
|
33763
|
+
var SECOND_FACTOR_POLICIES = ["none", "optional", "required"];
|
|
33764
|
+
var METHODS_SETTING = "methods";
|
|
33765
|
+
var isSignInMethod = (value) => SIGN_IN_METHODS.includes(value);
|
|
33766
|
+
var isPolicy = (value) => SECOND_FACTOR_POLICIES.includes(value);
|
|
33767
|
+
function parseMethodSettings(input, options = {}) {
|
|
33768
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
33769
|
+
const settings = {};
|
|
33770
|
+
for (const [method, value] of Object.entries(input)) {
|
|
33771
|
+
if (!isSignInMethod(method)) return null;
|
|
33772
|
+
if (typeof value === "boolean") {
|
|
33773
|
+
if (!options.booleans) return null;
|
|
33774
|
+
if (value) settings[method] = {};
|
|
33775
|
+
continue;
|
|
33776
|
+
}
|
|
33777
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
33778
|
+
const setting = {};
|
|
33779
|
+
for (const [field, fieldValue] of Object.entries(value)) {
|
|
33780
|
+
if (field !== "secondFactor" || method === "totp" || !isPolicy(fieldValue)) return null;
|
|
33781
|
+
setting.secondFactor = fieldValue;
|
|
33782
|
+
}
|
|
33783
|
+
settings[method] = setting;
|
|
33784
|
+
}
|
|
33785
|
+
return settings;
|
|
33720
33786
|
}
|
|
33721
|
-
|
|
33722
|
-
|
|
33723
|
-
|
|
33724
|
-
|
|
33725
|
-
|
|
33726
|
-
|
|
33727
|
-
|
|
33728
|
-
|
|
33729
|
-
|
|
33730
|
-
|
|
33787
|
+
function isConfigured(config, method) {
|
|
33788
|
+
if (method === "google") return !!config.google;
|
|
33789
|
+
if (method === "password") return !!config.password;
|
|
33790
|
+
return true;
|
|
33791
|
+
}
|
|
33792
|
+
function canSignUp(config, method) {
|
|
33793
|
+
return method === "totp" ? !!config.emailVerification : isConfigured(config, method);
|
|
33794
|
+
}
|
|
33795
|
+
function initialMethods(config) {
|
|
33796
|
+
const methods = {};
|
|
33797
|
+
if (config.google) methods.google = { secondFactor: "none" };
|
|
33798
|
+
if (config.password) methods.password = { secondFactor: "none" };
|
|
33799
|
+
if (!config.google && !config.password && config.emailVerification) methods.totp = {};
|
|
33800
|
+
return methods;
|
|
33801
|
+
}
|
|
33802
|
+
var MAX_TRIES = 3;
|
|
33803
|
+
var Settings = class {
|
|
33804
|
+
/** `initial` is what counts until the methods are set, see {@link initialMethods}. */
|
|
33805
|
+
constructor(store, initial = {}) {
|
|
33806
|
+
this.store = store;
|
|
33807
|
+
this.initial = initial;
|
|
33808
|
+
this.warned = false;
|
|
33731
33809
|
}
|
|
33732
|
-
|
|
33733
|
-
|
|
33810
|
+
/** The setting's value, or undefined if it was never set. */
|
|
33811
|
+
async get(id) {
|
|
33812
|
+
return (await this.read(id)).value;
|
|
33734
33813
|
}
|
|
33735
|
-
if
|
|
33736
|
-
|
|
33814
|
+
/** The stored JSON and what it holds; both undefined if the setting was never set. */
|
|
33815
|
+
async read(id) {
|
|
33816
|
+
const record = await this.store.findOne("setting", { id });
|
|
33817
|
+
if (typeof record?.value !== "string") return {};
|
|
33818
|
+
try {
|
|
33819
|
+
return { json: record.value, value: JSON.parse(record.value) };
|
|
33820
|
+
} catch {
|
|
33821
|
+
return { json: record.value };
|
|
33822
|
+
}
|
|
33737
33823
|
}
|
|
33738
|
-
|
|
33739
|
-
|
|
33824
|
+
/**
|
|
33825
|
+
* Changes the setting: `apply` gets the current value and returns the new one, or null to refuse the
|
|
33826
|
+
* change. The write only lands if nobody changed the setting since `apply` read it; otherwise `apply` runs
|
|
33827
|
+
* again with the newer value. Resolves to the new value, or null if `apply` refused.
|
|
33828
|
+
* `updatedBy` is the address of the admin, or null for the command line.
|
|
33829
|
+
*/
|
|
33830
|
+
async change(id, apply, updatedBy) {
|
|
33831
|
+
for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
|
|
33832
|
+
const { json: json2, value } = await this.read(id);
|
|
33833
|
+
const next = apply(value);
|
|
33834
|
+
if (next === null) return null;
|
|
33835
|
+
const patch = { value: JSON.stringify(next), updatedAt: Date.now(), updatedBy };
|
|
33836
|
+
const written = json2 === void 0 ? await this.store.create("setting", { id, ...patch }) : await this.store.update("setting", { id, value: json2 }, patch) === 1;
|
|
33837
|
+
if (written) return next;
|
|
33838
|
+
}
|
|
33839
|
+
throw new Error(`Setting "${id}" kept changing; try again.`);
|
|
33740
33840
|
}
|
|
33741
|
-
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
33745
|
-
|
|
33746
|
-
|
|
33747
|
-
|
|
33748
|
-
var SESSION_COOKIE = "madauth_session";
|
|
33749
|
-
var SESSION_TYP = "madauth-session+jwt";
|
|
33750
|
-
var RENEWAL_COOKIE = "madauth_renewal";
|
|
33751
|
-
var RENEWAL_TYP = "madauth-renewal+jwt";
|
|
33752
|
-
var EXPIRY_COOKIE = "madauth_session_expires";
|
|
33753
|
-
|
|
33754
|
-
// src/tokens.ts
|
|
33755
|
-
function randomString(bytes = 32) {
|
|
33756
|
-
return base64url_exports.encode(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
33757
|
-
}
|
|
33758
|
-
async function signToken(keys, issuer, typ, claims, ttlSeconds) {
|
|
33759
|
-
return new SignJWT(claims).setProtectedHeader({ alg: SIGNING_ALG, kid: keys.kid, typ }).setIssuer(issuer).setIssuedAt().setExpirationTime(`${ttlSeconds}s`).sign(keys.privateKey);
|
|
33760
|
-
}
|
|
33761
|
-
async function readToken(keys, issuer, typ, token) {
|
|
33762
|
-
if (!token) return null;
|
|
33763
|
-
try {
|
|
33764
|
-
const { payload } = await jwtVerify(token, keys.publicKey, { issuer, typ, algorithms: [SIGNING_ALG] });
|
|
33765
|
-
return payload;
|
|
33766
|
-
} catch {
|
|
33767
|
-
return null;
|
|
33841
|
+
/** Replaces the setting. */
|
|
33842
|
+
async set(id, value, updatedBy) {
|
|
33843
|
+
await this.change(id, () => value, updatedBy);
|
|
33844
|
+
}
|
|
33845
|
+
/** Which sign-in methods are on, with their settings; the initial ones until an admin set them. */
|
|
33846
|
+
async methods() {
|
|
33847
|
+
return this.current(await this.get(METHODS_SETTING));
|
|
33768
33848
|
}
|
|
33849
|
+
/** Changes which methods are on, see {@link change}; `apply` gets the methods as they are now. */
|
|
33850
|
+
async changeMethods(apply, updatedBy) {
|
|
33851
|
+
return this.change(METHODS_SETTING, (current) => apply(this.current(current)), updatedBy);
|
|
33852
|
+
}
|
|
33853
|
+
/** The stored methods, or the initial ones if nothing (usable) is stored. */
|
|
33854
|
+
current(stored) {
|
|
33855
|
+
if (stored === void 0) return this.initial;
|
|
33856
|
+
const parsed = parseMethodSettings(stored);
|
|
33857
|
+
if (parsed) return parsed;
|
|
33858
|
+
if (!this.warned) {
|
|
33859
|
+
this.warned = true;
|
|
33860
|
+
console.warn("[madauth] The stored sign-in methods are not in the form this version expects (they were set by an older one?). Set them again with set-methods; until then the initial methods count.");
|
|
33861
|
+
}
|
|
33862
|
+
return this.initial;
|
|
33863
|
+
}
|
|
33864
|
+
};
|
|
33865
|
+
function isOn(method, configured, methods) {
|
|
33866
|
+
return configured(method) && methods[method] !== void 0;
|
|
33769
33867
|
}
|
|
33770
|
-
function
|
|
33771
|
-
|
|
33772
|
-
...extra,
|
|
33773
|
-
sub: user.id,
|
|
33774
|
-
email: user.email,
|
|
33775
|
-
email_verified: user.email ? true : void 0,
|
|
33776
|
-
name: user.name,
|
|
33777
|
-
picture: user.picture,
|
|
33778
|
-
claims: user.claims && Object.keys(user.claims).length ? user.claims : void 0,
|
|
33779
|
-
amr
|
|
33780
|
-
};
|
|
33781
|
-
return signToken(keys, issuer, SESSION_TYP, claims, ttlSeconds);
|
|
33868
|
+
function policyOf(method, methods) {
|
|
33869
|
+
return methods[method]?.secondFactor ?? "none";
|
|
33782
33870
|
}
|
|
33783
|
-
function
|
|
33784
|
-
|
|
33785
|
-
...extra,
|
|
33786
|
-
sub: user.id,
|
|
33787
|
-
email: user.email,
|
|
33788
|
-
email_verified: user.email ? true : void 0,
|
|
33789
|
-
name: user.name,
|
|
33790
|
-
picture: user.picture,
|
|
33791
|
-
amr
|
|
33792
|
-
};
|
|
33793
|
-
return signToken(keys, issuer, RENEWAL_TYP, claims, ttlSeconds);
|
|
33871
|
+
function anyOn(signsUp, methods) {
|
|
33872
|
+
return SIGN_IN_METHODS.some((method) => signsUp(method) && methods[method] !== void 0);
|
|
33794
33873
|
}
|
|
33795
|
-
function
|
|
33796
|
-
|
|
33797
|
-
|
|
33798
|
-
if (claims.name) user.name = claims.name;
|
|
33799
|
-
if (claims.picture) user.picture = claims.picture;
|
|
33800
|
-
if (claims.claims && typeof claims.claims === "object" && Object.keys(claims.claims).length) user.claims = claims.claims;
|
|
33801
|
-
return user;
|
|
33874
|
+
function authenticatorOn(configured, methods) {
|
|
33875
|
+
if (isOn("totp", configured, methods)) return true;
|
|
33876
|
+
return METHODS_WITH_SECOND_FACTOR.some((method) => isOn(method, configured, methods) && policyOf(method, methods) !== "none");
|
|
33802
33877
|
}
|
|
33803
33878
|
|
|
33804
33879
|
// src/users.ts
|
|
33805
|
-
import { randomBytes as randomBytes3 } from "crypto";
|
|
33806
33880
|
var MAX_CODE_ATTEMPTS = 5;
|
|
33807
33881
|
var MAX_WRONG_CODES = 10;
|
|
33808
33882
|
var newId = (prefix) => `${prefix}_${randomBytes3(16).toString("base64url")}`;
|
|
@@ -33812,6 +33886,9 @@ function passwordAccountKey(userId) {
|
|
|
33812
33886
|
function googleAccountKey(sub) {
|
|
33813
33887
|
return `google:${sub}`;
|
|
33814
33888
|
}
|
|
33889
|
+
function totpAccountKey(userId) {
|
|
33890
|
+
return `totp:${userId}`;
|
|
33891
|
+
}
|
|
33815
33892
|
function toMadauthUser(user, profile = {}) {
|
|
33816
33893
|
const result = { id: user.id, email: user.email };
|
|
33817
33894
|
const name = user.name ?? profile.name;
|
|
@@ -33840,12 +33917,32 @@ var Users = class {
|
|
|
33840
33917
|
async passwordAccount(userId) {
|
|
33841
33918
|
return this.findAccountByKey(passwordAccountKey(userId));
|
|
33842
33919
|
}
|
|
33920
|
+
/** The user's authenticator app, if they set one up. */
|
|
33921
|
+
async totpAccount(userId) {
|
|
33922
|
+
return this.findAccountByKey(totpAccountKey(userId));
|
|
33923
|
+
}
|
|
33924
|
+
/** Whether the user has any way to sign in; a sign-up with the authenticator app has none until it is set up. */
|
|
33925
|
+
async hasAccounts(userId) {
|
|
33926
|
+
return (await this.store.findMany("account", { userId }, { limit: 1 })).length > 0;
|
|
33927
|
+
}
|
|
33928
|
+
/**
|
|
33929
|
+
* The ways the user can sign in, from their accounts: e.g. `['google']` for a user without a password.
|
|
33930
|
+
* The authenticator app counts only when it signs in on its own (`methods` lists it); as a second
|
|
33931
|
+
* factor it is a step of another method, not a way in.
|
|
33932
|
+
*/
|
|
33933
|
+
async signInMethods(userId, methods) {
|
|
33934
|
+
const accounts = await this.store.findMany("account", { userId });
|
|
33935
|
+
const keys = accounts.map((account) => String(account.key).split(":")[0]);
|
|
33936
|
+
return SIGN_IN_METHODS.filter((method) => keys.includes(method) && (method !== "totp" || methods.totp !== void 0));
|
|
33937
|
+
}
|
|
33843
33938
|
async findAccountByKey(key) {
|
|
33844
33939
|
return await this.store.findOne("account", { key });
|
|
33845
33940
|
}
|
|
33846
33941
|
/**
|
|
33847
|
-
* Creates a user with their first account. Resolves to null if the e-mail address or the
|
|
33848
|
-
* taken. `account.key` is `password:<userId>` for a password (its hash as `secret`), or
|
|
33942
|
+
* Creates a user, with their first account if given. Resolves to null if the e-mail address or the
|
|
33943
|
+
* account key is taken. `account.key` is `password:<userId>` for a password (its hash as `secret`), or
|
|
33944
|
+
* e.g. `google:<sub>`. A sign-up with the authenticator app creates the user without an account: the app
|
|
33945
|
+
* is set up once the address is confirmed.
|
|
33849
33946
|
*/
|
|
33850
33947
|
async createUser(data) {
|
|
33851
33948
|
const user = {
|
|
@@ -33861,6 +33958,7 @@ var Users = class {
|
|
|
33861
33958
|
createdAt: Date.now()
|
|
33862
33959
|
};
|
|
33863
33960
|
if (!await this.store.create("user", user)) return null;
|
|
33961
|
+
if (!data.account) return user;
|
|
33864
33962
|
const account = typeof data.account === "function" ? data.account(user.id) : data.account;
|
|
33865
33963
|
if (!await this.linkAccount(user.id, account)) {
|
|
33866
33964
|
await this.store.delete("user", { id: user.id });
|
|
@@ -33882,6 +33980,16 @@ var Users = class {
|
|
|
33882
33980
|
async deleteAccount(id) {
|
|
33883
33981
|
await this.store.delete("account", { id });
|
|
33884
33982
|
}
|
|
33983
|
+
/** Removes a sign-in method by its key. Resolves to false if there was none. */
|
|
33984
|
+
async unlinkAccount(key) {
|
|
33985
|
+
return await this.store.delete("account", { key }) === 1;
|
|
33986
|
+
}
|
|
33987
|
+
/** Removes the user's authenticator app and recovery codes. Resolves to false if they had no app. */
|
|
33988
|
+
async removeTotp(userId) {
|
|
33989
|
+
const removed = await this.unlinkAccount(totpAccountKey(userId));
|
|
33990
|
+
await this.clearRecoveryCodes(userId);
|
|
33991
|
+
return removed;
|
|
33992
|
+
}
|
|
33885
33993
|
/** Adds a sign-in method to a user. Resolves to null if the account key is taken. */
|
|
33886
33994
|
async linkAccount(userId, account) {
|
|
33887
33995
|
const record = {
|
|
@@ -33892,10 +34000,70 @@ var Users = class {
|
|
|
33892
34000
|
failedAttempts: 0,
|
|
33893
34001
|
lockedUntil: 0,
|
|
33894
34002
|
email: account.email ?? null,
|
|
34003
|
+
lastUsedStep: account.lastUsedStep ?? 0,
|
|
34004
|
+
setupId: account.setupId ?? null,
|
|
33895
34005
|
createdAt: Date.now()
|
|
33896
34006
|
};
|
|
33897
34007
|
return await this.store.create("account", record) ? record : null;
|
|
33898
34008
|
}
|
|
34009
|
+
/**
|
|
34010
|
+
* Writes the secret of a confirmed setup over the authenticator account, only if the account still has the
|
|
34011
|
+
* setup it had when this setup started (`replaces`): the write count decides, so of two setups confirmed
|
|
34012
|
+
* at once exactly one wins, and the other is told to start again.
|
|
34013
|
+
*/
|
|
34014
|
+
async confirmTotpSetup(accountId, replaces, patch) {
|
|
34015
|
+
return await this.store.update("account", { id: accountId, setupId: replaces }, patch) === 1;
|
|
34016
|
+
}
|
|
34017
|
+
/**
|
|
34018
|
+
* Marks the time step of an accepted authenticator code as used and resets the failed attempts, only if
|
|
34019
|
+
* no other request accepted a code meanwhile: the write count decides, so the same code sent twice signs
|
|
34020
|
+
* in once.
|
|
34021
|
+
*/
|
|
34022
|
+
async useTotpStep(account, step) {
|
|
34023
|
+
const changed = await this.store.update(
|
|
34024
|
+
"account",
|
|
34025
|
+
{ id: account.id, lastUsedStep: account.lastUsedStep ?? null },
|
|
34026
|
+
{ lastUsedStep: step, failedAttempts: 0, lockedUntil: 0 }
|
|
34027
|
+
);
|
|
34028
|
+
return changed === 1;
|
|
34029
|
+
}
|
|
34030
|
+
/** Replaces the user's recovery codes with new ones, given as their hashes. */
|
|
34031
|
+
/** Stores the recovery codes of a setup (their hashes), next to whatever codes the user has. */
|
|
34032
|
+
async addRecoveryCodes(userId, setupId, hashes) {
|
|
34033
|
+
const createdAt = Date.now();
|
|
34034
|
+
for (const id of hashes) await this.store.create("recoveryCode", { id, userId, setupId, createdAt });
|
|
34035
|
+
}
|
|
34036
|
+
/** New recovery codes for the user's current app: the codes they had before stop working. */
|
|
34037
|
+
async replaceRecoveryCodes(account, hashes) {
|
|
34038
|
+
const before = await this.store.findMany("recoveryCode", { userId: account.userId });
|
|
34039
|
+
await this.addRecoveryCodes(account.userId, account.setupId ?? "", hashes);
|
|
34040
|
+
for (const old of before) await this.store.delete("recoveryCode", { id: old.id });
|
|
34041
|
+
}
|
|
34042
|
+
/**
|
|
34043
|
+
* Uses up the recovery code with this hash for the user's authenticator account. Resolves to false if it
|
|
34044
|
+
* is not theirs, belongs to another setup than the account's, or was used already.
|
|
34045
|
+
*/
|
|
34046
|
+
async consumeRecoveryCode(account, hash) {
|
|
34047
|
+
const record = await this.store.findOne("recoveryCode", { id: hash });
|
|
34048
|
+
if (!record || record.userId !== account.userId || record.setupId !== (account.setupId ?? "")) return false;
|
|
34049
|
+
return await this.store.delete("recoveryCode", { id: hash }) === 1;
|
|
34050
|
+
}
|
|
34051
|
+
/** How many recovery codes of the account's setup are left. */
|
|
34052
|
+
async countRecoveryCodes(account) {
|
|
34053
|
+
const codes = await this.store.findMany("recoveryCode", { userId: account.userId });
|
|
34054
|
+
return codes.filter((code) => code.setupId === (account.setupId ?? "")).length;
|
|
34055
|
+
}
|
|
34056
|
+
/** Deletes the user's recovery codes: all of them, or only those of one setup, or all but one setup's. */
|
|
34057
|
+
async clearRecoveryCodes(userId, which = {}) {
|
|
34058
|
+
if (which.only === void 0 && which.except === void 0) {
|
|
34059
|
+
await this.store.delete("recoveryCode", { userId });
|
|
34060
|
+
return;
|
|
34061
|
+
}
|
|
34062
|
+
for (const code of await this.store.findMany("recoveryCode", { userId })) {
|
|
34063
|
+
const match2 = which.only !== void 0 ? code.setupId === which.only : code.setupId !== which.except;
|
|
34064
|
+
if (match2) await this.store.delete("recoveryCode", { id: code.id });
|
|
34065
|
+
}
|
|
34066
|
+
}
|
|
33899
34067
|
/**
|
|
33900
34068
|
* Deletes the user and everything stored for them. Resolves to false if there is no such user (any more).
|
|
33901
34069
|
*/
|
|
@@ -33906,6 +34074,7 @@ var Users = class {
|
|
|
33906
34074
|
try {
|
|
33907
34075
|
await this.store.delete("account", { userId: user.id });
|
|
33908
34076
|
await this.store.delete("verification", { userId: user.id });
|
|
34077
|
+
await this.store.delete("recoveryCode", { userId: user.id });
|
|
33909
34078
|
} catch (e6) {
|
|
33910
34079
|
console.error(`[madauth] Deleted user ${user.id}, but not all of their records: ${e6.message}`);
|
|
33911
34080
|
}
|
|
@@ -33914,8 +34083,9 @@ var Users = class {
|
|
|
33914
34083
|
async updateUser(id, patch) {
|
|
33915
34084
|
await this.store.update("user", { id }, patch);
|
|
33916
34085
|
}
|
|
34086
|
+
/** Changes an account. Resolves to false if it is gone meanwhile. */
|
|
33917
34087
|
async updateAccount(id, patch) {
|
|
33918
|
-
await this.store.update("account", { id }, patch);
|
|
34088
|
+
return await this.store.update("account", { id }, patch) === 1;
|
|
33919
34089
|
}
|
|
33920
34090
|
/**
|
|
33921
34091
|
* Counts a sign-in attempt: writes `patch` only if the account's `failedAttempts` is still the value that
|
|
@@ -33948,6 +34118,11 @@ var Users = class {
|
|
|
33948
34118
|
await this.store.create("verification", record);
|
|
33949
34119
|
return { token, code };
|
|
33950
34120
|
}
|
|
34121
|
+
/** The user ID a link token belongs to, without using the verification up; null if invalid, expired or used. */
|
|
34122
|
+
async peekLinkToken(token, purpose) {
|
|
34123
|
+
const record = await this.store.findOne("verification", { id: hashLinkToken(token), purpose });
|
|
34124
|
+
return record && record.expiresAt > Date.now() ? record.userId : null;
|
|
34125
|
+
}
|
|
33951
34126
|
/** Uses up the verification a link token belongs to. Resolves to its user ID, or null if invalid, expired or used. */
|
|
33952
34127
|
async consumeLinkToken(token, purpose) {
|
|
33953
34128
|
const id = hashLinkToken(token);
|
|
@@ -33994,6 +34169,215 @@ var Users = class {
|
|
|
33994
34169
|
}
|
|
33995
34170
|
};
|
|
33996
34171
|
|
|
34172
|
+
// src/routes/helpers.ts
|
|
34173
|
+
async function body(c6) {
|
|
34174
|
+
const data = await c6.req.json().catch(() => null);
|
|
34175
|
+
return data && typeof data === "object" ? data : {};
|
|
34176
|
+
}
|
|
34177
|
+
function error(c6, status, code, message2) {
|
|
34178
|
+
return c6.json({ error: code, message: message2 }, status);
|
|
34179
|
+
}
|
|
34180
|
+
var unavailable = (c6) => error(c6, 503, "temporarily_unavailable", "E-mails can not be sent right now. Please try again later.");
|
|
34181
|
+
var accepted = (c6) => c6.json({}, 202);
|
|
34182
|
+
var noSession = (c6) => error(c6, 401, "no_session", "Please sign in first.");
|
|
34183
|
+
function redirectTarget(ctx, c6, value) {
|
|
34184
|
+
const url = ctx.allowedUrl(value);
|
|
34185
|
+
return url ?? error(c6, 400, "invalid_options", "redirectTo must be an absolute URL whose origin is in ALLOWED_ORIGINS.");
|
|
34186
|
+
}
|
|
34187
|
+
var MAX_NAME_LENGTH = 100;
|
|
34188
|
+
function nameOf(value) {
|
|
34189
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, MAX_NAME_LENGTH) : null;
|
|
34190
|
+
}
|
|
34191
|
+
function signInAnswer(c6, step) {
|
|
34192
|
+
if ("disabled" in step) return error(c6, 403, "method_disabled", "This way of signing in is switched off.");
|
|
34193
|
+
if ("next" in step) {
|
|
34194
|
+
return step.next === "totp" ? c6.json({ error: "totp_required", message: "Enter the code from your authenticator app to finish signing in.", method: step.method }, 401) : c6.json({ error: "totp_setup_required", message: "Set up an authenticator app to finish signing in.", method: step.method }, 401);
|
|
34195
|
+
}
|
|
34196
|
+
return c6.json({ user: step.user });
|
|
34197
|
+
}
|
|
34198
|
+
|
|
34199
|
+
// src/routes/emails.ts
|
|
34200
|
+
var MAIL_INTERVAL_MS = 60 * 1e3;
|
|
34201
|
+
var VERIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
34202
|
+
var RESET_TTL_MS = 30 * 60 * 1e3;
|
|
34203
|
+
var VERIFY_LINK_PARAM = "madauth_verify";
|
|
34204
|
+
var RESET_LINK_PARAM = "madauth_reset";
|
|
34205
|
+
function createEmails(ctx, users, webhook) {
|
|
34206
|
+
const sendEmail = async (user, type, data) => {
|
|
34207
|
+
if (Date.now() - user.lastMailAt < MAIL_INTERVAL_MS) return true;
|
|
34208
|
+
const result = await webhook.call(type, { to: user.email, user: { id: user.id, name: user.name ?? void 0 }, ...data }, WEBHOOK_TIMEOUT_MS);
|
|
34209
|
+
if (!result.ok) {
|
|
34210
|
+
console.error(`[madauth] Webhook "${type}" for ${user.email} failed: ${result.reason}`);
|
|
34211
|
+
return false;
|
|
34212
|
+
}
|
|
34213
|
+
await users.updateUser(user.id, { lastMailAt: Date.now() });
|
|
34214
|
+
return true;
|
|
34215
|
+
};
|
|
34216
|
+
return {
|
|
34217
|
+
sendEmail,
|
|
34218
|
+
async sendLinkEmail(user, purpose, target, locale) {
|
|
34219
|
+
if (Date.now() - user.lastMailAt < MAIL_INTERVAL_MS) return true;
|
|
34220
|
+
const ttl = purpose === "verify" ? VERIFY_TTL_MS : RESET_TTL_MS;
|
|
34221
|
+
const { token, code } = await users.issueVerification(user.id, purpose, ttl);
|
|
34222
|
+
const link = new URL(target);
|
|
34223
|
+
link.hash = `${purpose === "verify" ? VERIFY_LINK_PARAM : RESET_LINK_PARAM}=${token}`;
|
|
34224
|
+
return sendEmail(user, purpose === "verify" ? "email.verify" : "email.reset", {
|
|
34225
|
+
link: link.href,
|
|
34226
|
+
code,
|
|
34227
|
+
expiresAt: Date.now() + ttl,
|
|
34228
|
+
site: target.host,
|
|
34229
|
+
locale
|
|
34230
|
+
});
|
|
34231
|
+
},
|
|
34232
|
+
async sendAccountEmail(user, type, target, locale) {
|
|
34233
|
+
if (!webhook.wants(type)) return true;
|
|
34234
|
+
const methods = await users.signInMethods(user.id, await ctx.settings.methods());
|
|
34235
|
+
return sendEmail(user, type, { link: target.href, site: target.host, locale, methods });
|
|
34236
|
+
},
|
|
34237
|
+
async consume(data, purpose) {
|
|
34238
|
+
if (typeof data.token === "string" && data.token) {
|
|
34239
|
+
return { userId: await users.consumeLinkToken(data.token, purpose), via: "link" };
|
|
34240
|
+
}
|
|
34241
|
+
if (isValidEmail(data.email) && typeof data.code === "string") {
|
|
34242
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34243
|
+
if (!user) return { userId: null, via: "code" };
|
|
34244
|
+
return { ...await users.consumeCode(user, purpose, data.code.replace(/\s/g, "")), via: "code" };
|
|
34245
|
+
}
|
|
34246
|
+
return { userId: null, via: "code" };
|
|
34247
|
+
},
|
|
34248
|
+
invalidVerification(c6, { via, locked }) {
|
|
34249
|
+
if (locked) {
|
|
34250
|
+
return error(c6, 429, "codes_locked", "Too many wrong codes. Please ask for a new e-mail and use the link in it.");
|
|
34251
|
+
}
|
|
34252
|
+
return via === "link" ? error(c6, 400, "link_invalid", "The link is invalid, expired or was already used.") : error(c6, 400, "code_invalid", "The code is wrong or expired.");
|
|
34253
|
+
}
|
|
34254
|
+
};
|
|
34255
|
+
}
|
|
34256
|
+
|
|
34257
|
+
// src/routes/email.ts
|
|
34258
|
+
function emailRoutes(app, ctx, users, webhook) {
|
|
34259
|
+
const emails = createEmails(ctx, users, webhook);
|
|
34260
|
+
const signUpMethod = async (userId) => await users.passwordAccount(userId) ? "password" : "totp";
|
|
34261
|
+
app.post("/auth/email/send-verification", async (c6) => {
|
|
34262
|
+
const data = await body(c6);
|
|
34263
|
+
const target = redirectTarget(ctx, c6, data.redirectTo);
|
|
34264
|
+
if (target instanceof Response) return target;
|
|
34265
|
+
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
34266
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34267
|
+
if (user && !user.emailVerified && await ctx.enabled(c6, await signUpMethod(user.id))) {
|
|
34268
|
+
if (!await emails.sendLinkEmail(user, "verify", target, localeOf(data.locale))) return unavailable(c6);
|
|
34269
|
+
}
|
|
34270
|
+
return accepted(c6);
|
|
34271
|
+
});
|
|
34272
|
+
const disabled = (c6) => error(c6, 403, "method_disabled", "This way of signing in is switched off.");
|
|
34273
|
+
app.post("/auth/email/verify", async (c6) => {
|
|
34274
|
+
const data = await body(c6);
|
|
34275
|
+
const waiting = typeof data.token === "string" && data.token ? await users.peekLinkToken(data.token, "verify") : isValidEmail(data.email) ? (await users.findByEmail(normalizeEmail(data.email)))?.id ?? null : null;
|
|
34276
|
+
if (waiting && !await ctx.enabled(c6, await signUpMethod(waiting))) return disabled(c6);
|
|
34277
|
+
const consumed = await emails.consume(data, "verify");
|
|
34278
|
+
const { userId, via } = consumed;
|
|
34279
|
+
const user = userId ? await users.findById(userId) : null;
|
|
34280
|
+
if (!user) return emails.invalidVerification(c6, consumed);
|
|
34281
|
+
const method = await signUpMethod(user.id);
|
|
34282
|
+
if (!await ctx.enabled(c6, method)) return disabled(c6);
|
|
34283
|
+
await users.updateUser(user.id, { emailVerified: true, wrongCodes: 0 });
|
|
34284
|
+
const verified = { ...user, emailVerified: true, wrongCodes: 0 };
|
|
34285
|
+
await ctx.emit("email.verified", { user: toMadauthUser(verified), via });
|
|
34286
|
+
return signInAnswer(c6, await ctx.secondStep(c6, verified, method === "password" ? ["pwd"] : [], method));
|
|
34287
|
+
});
|
|
34288
|
+
}
|
|
34289
|
+
|
|
34290
|
+
// src/google.ts
|
|
34291
|
+
var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
34292
|
+
var googleJwks;
|
|
34293
|
+
function googleKeys() {
|
|
34294
|
+
googleJwks ??= createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
|
|
34295
|
+
return googleJwks;
|
|
34296
|
+
}
|
|
34297
|
+
async function verifyGoogleIdToken(idToken, opts) {
|
|
34298
|
+
let payload;
|
|
34299
|
+
try {
|
|
34300
|
+
({ payload } = await jwtVerify(idToken, opts.keys ?? googleKeys(), {
|
|
34301
|
+
issuer: GOOGLE_ISSUERS,
|
|
34302
|
+
audience: opts.clientId,
|
|
34303
|
+
algorithms: ["RS256", "ES256"]
|
|
34304
|
+
}));
|
|
34305
|
+
} catch (e6) {
|
|
34306
|
+
return { ok: false, error: "verification_failed", reason: e6.message };
|
|
34307
|
+
}
|
|
34308
|
+
if (typeof payload.sub !== "string" || !payload.sub) {
|
|
34309
|
+
return { ok: false, error: "verification_failed", reason: "missing sub" };
|
|
34310
|
+
}
|
|
34311
|
+
if (payload.nonce !== opts.nonce) {
|
|
34312
|
+
return { ok: false, error: "verification_failed", reason: "nonce mismatch" };
|
|
34313
|
+
}
|
|
34314
|
+
if (typeof payload.email !== "string" || !payload.email || payload.email_verified !== true) {
|
|
34315
|
+
return { ok: false, error: "email_unverified", reason: "e-mail address not verified" };
|
|
34316
|
+
}
|
|
34317
|
+
const profile = { sub: payload.sub, email: payload.email };
|
|
34318
|
+
if (typeof payload.name === "string") profile.name = payload.name;
|
|
34319
|
+
if (typeof payload.picture === "string") profile.picture = payload.picture;
|
|
34320
|
+
return { ok: true, profile };
|
|
34321
|
+
}
|
|
34322
|
+
|
|
34323
|
+
// src/user.ts
|
|
34324
|
+
var SESSION_COOKIE = "madauth_session";
|
|
34325
|
+
var SESSION_TYP = "madauth-session+jwt";
|
|
34326
|
+
var RENEWAL_COOKIE = "madauth_renewal";
|
|
34327
|
+
var RENEWAL_TYP = "madauth-renewal+jwt";
|
|
34328
|
+
var EXPIRY_COOKIE = "madauth_session_expires";
|
|
34329
|
+
|
|
34330
|
+
// src/tokens.ts
|
|
34331
|
+
function randomString(bytes = 32) {
|
|
34332
|
+
return base64url_exports.encode(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
34333
|
+
}
|
|
34334
|
+
async function signToken(keys, issuer, typ, claims, ttlSeconds) {
|
|
34335
|
+
return new SignJWT(claims).setProtectedHeader({ alg: SIGNING_ALG, kid: keys.kid, typ }).setIssuer(issuer).setIssuedAt().setExpirationTime(`${ttlSeconds}s`).sign(keys.privateKey);
|
|
34336
|
+
}
|
|
34337
|
+
async function readToken(keys, issuer, typ, token) {
|
|
34338
|
+
if (!token) return null;
|
|
34339
|
+
try {
|
|
34340
|
+
const { payload } = await jwtVerify(token, keys.publicKey, { issuer, typ, algorithms: [SIGNING_ALG] });
|
|
34341
|
+
return payload;
|
|
34342
|
+
} catch {
|
|
34343
|
+
return null;
|
|
34344
|
+
}
|
|
34345
|
+
}
|
|
34346
|
+
function signSession(keys, issuer, ttlSeconds, user, amr, extra = {}) {
|
|
34347
|
+
const claims = {
|
|
34348
|
+
...extra,
|
|
34349
|
+
sub: user.id,
|
|
34350
|
+
email: user.email,
|
|
34351
|
+
email_verified: user.email ? true : void 0,
|
|
34352
|
+
name: user.name,
|
|
34353
|
+
picture: user.picture,
|
|
34354
|
+
claims: user.claims && Object.keys(user.claims).length ? user.claims : void 0,
|
|
34355
|
+
amr
|
|
34356
|
+
};
|
|
34357
|
+
return signToken(keys, issuer, SESSION_TYP, claims, ttlSeconds);
|
|
34358
|
+
}
|
|
34359
|
+
function signRenewal(keys, issuer, ttlSeconds, user, amr, extra = {}) {
|
|
34360
|
+
const claims = {
|
|
34361
|
+
...extra,
|
|
34362
|
+
sub: user.id,
|
|
34363
|
+
email: user.email,
|
|
34364
|
+
email_verified: user.email ? true : void 0,
|
|
34365
|
+
name: user.name,
|
|
34366
|
+
picture: user.picture,
|
|
34367
|
+
amr
|
|
34368
|
+
};
|
|
34369
|
+
return signToken(keys, issuer, RENEWAL_TYP, claims, ttlSeconds);
|
|
34370
|
+
}
|
|
34371
|
+
function userFromClaims(claims) {
|
|
34372
|
+
const user = { id: claims.sub };
|
|
34373
|
+
if (claims.email) user.email = claims.email;
|
|
34374
|
+
if (claims.name) user.name = claims.name;
|
|
34375
|
+
if (claims.picture) user.picture = claims.picture;
|
|
34376
|
+
if (claims.claims && typeof claims.claims === "object" && Object.keys(claims.claims).length) user.claims = claims.claims;
|
|
34377
|
+
if (Array.isArray(claims.amr)) user.amr = claims.amr.filter((value) => typeof value === "string");
|
|
34378
|
+
return user;
|
|
34379
|
+
}
|
|
34380
|
+
|
|
33997
34381
|
// src/routes/google.ts
|
|
33998
34382
|
var NONCE_COOKIE = "madauth_nonce";
|
|
33999
34383
|
var NONCE_TYP = "madauth-nonce+jwt";
|
|
@@ -34004,6 +34388,7 @@ var OAUTH_TTL = 10 * 60;
|
|
|
34004
34388
|
var GOOGLE_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
34005
34389
|
var GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
34006
34390
|
var REDIRECT_ERROR_PARAM = "madauth_error";
|
|
34391
|
+
var REDIRECT_NEXT_PARAM = "madauth_next";
|
|
34007
34392
|
async function pkceChallenge(verifier) {
|
|
34008
34393
|
const digest2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
34009
34394
|
return base64url_exports.encode(new Uint8Array(digest2));
|
|
@@ -34015,7 +34400,7 @@ function googleRoutes(app, ctx, google) {
|
|
|
34015
34400
|
const resolveUser = async (profile, locale) => {
|
|
34016
34401
|
const key = googleAccountKey(profile.sub);
|
|
34017
34402
|
const emailNormalized = normalizeEmail(profile.email);
|
|
34018
|
-
const signedIn = (user) => ({ ok: true, user
|
|
34403
|
+
const signedIn = (user) => ({ ok: true, user });
|
|
34019
34404
|
const account = await users.findAccountByKey(key);
|
|
34020
34405
|
if (account) {
|
|
34021
34406
|
const user = await users.findById(account.userId);
|
|
@@ -34028,6 +34413,11 @@ function googleRoutes(app, ctx, google) {
|
|
|
34028
34413
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
34029
34414
|
const existing = await users.findByEmail(emailNormalized);
|
|
34030
34415
|
if (existing) {
|
|
34416
|
+
if (existing.emailVerified && await users.hasAccounts(existing.id)) {
|
|
34417
|
+
const methods = await users.signInMethods(existing.id, await ctx.settings.methods());
|
|
34418
|
+
const how = methods.map((method) => ({ password: "a password", totp: "an authenticator app", google: "Google" })[method]).join(" or ");
|
|
34419
|
+
return { ok: false, error: "other_method", message: `This e-mail address signs in with ${how || "another method"}. Use that instead of Google.`, methods };
|
|
34420
|
+
}
|
|
34031
34421
|
await users.verifyByProvider(existing);
|
|
34032
34422
|
if (!await users.linkAccount(existing.id, { key, email: profile.email })) {
|
|
34033
34423
|
const linked = await users.findAccountByKey(key);
|
|
@@ -34059,7 +34449,7 @@ function googleRoutes(app, ctx, google) {
|
|
|
34059
34449
|
};
|
|
34060
34450
|
const disabled = (c6) => c6.json({ error: "method_disabled", message: "Google sign-in is switched off." }, 403);
|
|
34061
34451
|
app.post("/auth/google/nonce", async (c6) => {
|
|
34062
|
-
if (!await ctx.enabled("google")) return disabled(c6);
|
|
34452
|
+
if (!await ctx.enabled(c6, "google")) return disabled(c6);
|
|
34063
34453
|
const nonce = randomString();
|
|
34064
34454
|
const token = await signToken(await keys, issuer, NONCE_TYP, { nonce }, NONCE_TTL);
|
|
34065
34455
|
setCookie(c6, NONCE_COOKIE, token, {
|
|
@@ -34072,16 +34462,16 @@ function googleRoutes(app, ctx, google) {
|
|
|
34072
34462
|
return c6.json({ nonce });
|
|
34073
34463
|
});
|
|
34074
34464
|
app.post("/auth/google/verify", async (c6) => {
|
|
34075
|
-
if (!await ctx.enabled("google")) return disabled(c6);
|
|
34076
|
-
const
|
|
34077
|
-
if (typeof
|
|
34465
|
+
if (!await ctx.enabled(c6, "google")) return disabled(c6);
|
|
34466
|
+
const body2 = await c6.req.json().catch(() => ({}));
|
|
34467
|
+
if (typeof body2.credential !== "string" || !body2.credential) {
|
|
34078
34468
|
return c6.json({ error: "verification_failed", message: "credential is missing" }, 400);
|
|
34079
34469
|
}
|
|
34080
34470
|
const nonceClaims = await readToken(await keys, issuer, NONCE_TYP, getCookie(c6, NONCE_COOKIE));
|
|
34081
34471
|
if (!nonceClaims) {
|
|
34082
34472
|
return c6.json({ error: "verification_failed", message: "nonce cookie missing or expired" }, 401);
|
|
34083
34473
|
}
|
|
34084
|
-
const result = await verifyGoogleIdToken(
|
|
34474
|
+
const result = await verifyGoogleIdToken(body2.credential, {
|
|
34085
34475
|
clientId: google.clientId,
|
|
34086
34476
|
nonce: nonceClaims.nonce,
|
|
34087
34477
|
keys: config.jwksResolver
|
|
@@ -34090,17 +34480,16 @@ function googleRoutes(app, ctx, google) {
|
|
|
34090
34480
|
return c6.json({ error: result.error, message: result.reason }, result.error === "email_unverified" ? 403 : 401);
|
|
34091
34481
|
}
|
|
34092
34482
|
deleteCookie(c6, NONCE_COOKIE, { path: "/auth/google", secure });
|
|
34093
|
-
const resolved = await resolveUser(result.profile, localeOf(
|
|
34483
|
+
const resolved = await resolveUser(result.profile, localeOf(body2.locale));
|
|
34094
34484
|
if (!resolved.ok) {
|
|
34485
|
+
if (resolved.error === "other_method") return c6.json({ error: resolved.error, message: resolved.message, methods: resolved.methods }, 403);
|
|
34095
34486
|
return c6.json({ error: resolved.error, message: resolved.message }, resolved.error === "signup_rejected" ? 403 : 503);
|
|
34096
34487
|
}
|
|
34097
|
-
|
|
34098
|
-
await ctx.emit("user.signed_in", { user, method: "google" });
|
|
34099
|
-
return c6.json({ user });
|
|
34488
|
+
return signInAnswer(c6, await ctx.secondStep(c6, resolved.user, ["google"], "google", result.profile));
|
|
34100
34489
|
});
|
|
34101
34490
|
const requireCodeFlow = async (c6, next) => {
|
|
34102
34491
|
if (!google.clientSecret) return c6.json({ error: "not_found" }, 404);
|
|
34103
|
-
if (!await ctx.enabled("google")) return disabled(c6);
|
|
34492
|
+
if (!await ctx.enabled(c6, "google")) return disabled(c6);
|
|
34104
34493
|
await next();
|
|
34105
34494
|
};
|
|
34106
34495
|
app.get("/auth/google/start", requireCodeFlow, async (c6) => {
|
|
@@ -34138,9 +34527,10 @@ function googleRoutes(app, ctx, google) {
|
|
|
34138
34527
|
const oauth = await readToken(await keys, issuer, OAUTH_TYP, getCookie(c6, OAUTH_COOKIE));
|
|
34139
34528
|
if (!oauth) return c6.text("Sign-in expired or was started in another browser. Please try again.", 400);
|
|
34140
34529
|
deleteCookie(c6, OAUTH_COOKIE, { path: "/auth/google", secure });
|
|
34141
|
-
const back = (
|
|
34530
|
+
const back = (error2, next) => {
|
|
34142
34531
|
const url = new URL(oauth.returnTo);
|
|
34143
|
-
if (
|
|
34532
|
+
if (error2) url.hash = `${REDIRECT_ERROR_PARAM}=${error2}`;
|
|
34533
|
+
if (next) url.hash = `${REDIRECT_NEXT_PARAM}=${next}`;
|
|
34144
34534
|
return c6.redirect(url.href, 302);
|
|
34145
34535
|
};
|
|
34146
34536
|
if (c6.req.query("error")) return back(c6.req.query("error") === "access_denied" ? "cancelled" : "verification_failed");
|
|
@@ -34174,80 +34564,38 @@ function googleRoutes(app, ctx, google) {
|
|
|
34174
34564
|
if (!result.ok) return back(result.error);
|
|
34175
34565
|
const resolved = await resolveUser(result.profile, typeof oauth.locale === "string" ? oauth.locale : void 0);
|
|
34176
34566
|
if (!resolved.ok) return back(resolved.error);
|
|
34177
|
-
const
|
|
34178
|
-
|
|
34567
|
+
const step = await ctx.secondStep(c6, resolved.user, ["google"], "google", result.profile);
|
|
34568
|
+
if ("disabled" in step) return back("method_disabled");
|
|
34569
|
+
if ("next" in step) return back(void 0, step.next);
|
|
34179
34570
|
return back();
|
|
34180
34571
|
});
|
|
34181
34572
|
}
|
|
34182
34573
|
|
|
34183
|
-
// src/routes/
|
|
34574
|
+
// src/routes/lockout.ts
|
|
34184
34575
|
var FREE_ATTEMPTS = 5;
|
|
34185
34576
|
var MAX_LOCK_MS = 15 * 60 * 1e3;
|
|
34186
|
-
|
|
34187
|
-
|
|
34188
|
-
|
|
34189
|
-
|
|
34190
|
-
|
|
34191
|
-
|
|
34577
|
+
async function countSignInAttempt(c6, users, account, now = Date.now()) {
|
|
34578
|
+
const tooMany = (message2) => c6.json({ error: "too_many_attempts", message: message2 }, 429);
|
|
34579
|
+
if (account.lockedUntil > now) {
|
|
34580
|
+
const seconds = Math.ceil((account.lockedUntil - now) / 1e3);
|
|
34581
|
+
return tooMany(`Too many failed attempts. Try again in ${seconds} seconds.`);
|
|
34582
|
+
}
|
|
34583
|
+
const failed = account.failedAttempts + 1;
|
|
34584
|
+
const lockedUntil = failed >= FREE_ATTEMPTS ? now + Math.min(2 ** (failed - FREE_ATTEMPTS) * 1e3, MAX_LOCK_MS) : 0;
|
|
34585
|
+
if (!await users.countAttempt(account, { failedAttempts: failed, lockedUntil })) {
|
|
34586
|
+
return tooMany("Too many attempts at once. Please try again.");
|
|
34587
|
+
}
|
|
34588
|
+
return null;
|
|
34589
|
+
}
|
|
34590
|
+
|
|
34591
|
+
// src/routes/password.ts
|
|
34192
34592
|
function passwordRoutes(app, ctx, password, users, webhook) {
|
|
34193
34593
|
const { minLength } = password;
|
|
34194
|
-
const
|
|
34594
|
+
const emails = createEmails(ctx, users, webhook);
|
|
34195
34595
|
app.use("/auth/password/*", async (c6, next) => {
|
|
34196
|
-
if (!await ctx.enabled("password")) return
|
|
34596
|
+
if (!await ctx.enabled(c6, "password")) return error(c6, 403, "method_disabled", "E-mail & password sign-in is switched off.");
|
|
34197
34597
|
await next();
|
|
34198
34598
|
});
|
|
34199
|
-
const body = async (c6) => {
|
|
34200
|
-
const data = await c6.req.json().catch(() => null);
|
|
34201
|
-
return data && typeof data === "object" ? data : {};
|
|
34202
|
-
};
|
|
34203
|
-
const error = (c6, status, code, message2) => c6.json({ error: code, message: message2 }, status);
|
|
34204
|
-
const unavailable = (c6) => error(c6, 503, "temporarily_unavailable", "E-mails can not be sent right now. Please try again later.");
|
|
34205
|
-
const accepted = (c6) => c6.json({}, 202);
|
|
34206
|
-
const redirectTarget = (c6, value) => {
|
|
34207
|
-
const url = ctx.allowedUrl(value);
|
|
34208
|
-
return url ?? error(c6, 400, "invalid_options", "redirectTo must be an absolute URL whose origin is in ALLOWED_ORIGINS.");
|
|
34209
|
-
};
|
|
34210
|
-
const sendEmail = async (user, type, data) => {
|
|
34211
|
-
if (Date.now() - user.lastMailAt < MAIL_INTERVAL_MS) return true;
|
|
34212
|
-
const result = await webhook.call(type, { to: user.email, user: { id: user.id, name: user.name ?? void 0 }, ...data }, WEBHOOK_TIMEOUT_MS);
|
|
34213
|
-
if (!result.ok) {
|
|
34214
|
-
console.error(`[madauth] Webhook "${type}" for ${user.email} failed: ${result.reason}`);
|
|
34215
|
-
return false;
|
|
34216
|
-
}
|
|
34217
|
-
await users.updateUser(user.id, { lastMailAt: Date.now() });
|
|
34218
|
-
return true;
|
|
34219
|
-
};
|
|
34220
|
-
const sendLinkEmail = async (user, purpose, target, locale) => {
|
|
34221
|
-
if (Date.now() - user.lastMailAt < MAIL_INTERVAL_MS) return true;
|
|
34222
|
-
const ttl = purpose === "verify" ? VERIFY_TTL_MS : RESET_TTL_MS;
|
|
34223
|
-
const { token, code } = await users.issueVerification(user.id, purpose, ttl);
|
|
34224
|
-
const link = new URL(target);
|
|
34225
|
-
link.hash = `${purpose === "verify" ? VERIFY_LINK_PARAM : RESET_LINK_PARAM}=${token}`;
|
|
34226
|
-
return sendEmail(user, purpose === "verify" ? "email.verify" : "email.reset", {
|
|
34227
|
-
link: link.href,
|
|
34228
|
-
code,
|
|
34229
|
-
expiresAt: Date.now() + ttl,
|
|
34230
|
-
site: target.host,
|
|
34231
|
-
locale
|
|
34232
|
-
});
|
|
34233
|
-
};
|
|
34234
|
-
const consume = async (data, purpose) => {
|
|
34235
|
-
if (typeof data.token === "string" && data.token) {
|
|
34236
|
-
return { userId: await users.consumeLinkToken(data.token, purpose), via: "link" };
|
|
34237
|
-
}
|
|
34238
|
-
if (isValidEmail(data.email) && typeof data.code === "string") {
|
|
34239
|
-
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34240
|
-
if (!user) return { userId: null, via: "code" };
|
|
34241
|
-
return { ...await users.consumeCode(user, purpose, data.code.replace(/\s/g, "")), via: "code" };
|
|
34242
|
-
}
|
|
34243
|
-
return { userId: null, via: "code" };
|
|
34244
|
-
};
|
|
34245
|
-
const invalidVerification = (c6, { via, locked }) => {
|
|
34246
|
-
if (locked) {
|
|
34247
|
-
return error(c6, 429, "codes_locked", "Too many wrong codes. Please ask for a new e-mail and use the link in it.");
|
|
34248
|
-
}
|
|
34249
|
-
return via === "link" ? error(c6, 400, "link_invalid", "The link is invalid, expired or was already used.") : error(c6, 400, "code_invalid", "The code is wrong or expired.");
|
|
34250
|
-
};
|
|
34251
34599
|
app.post("/auth/password/signin", async (c6) => {
|
|
34252
34600
|
const data = await body(c6);
|
|
34253
34601
|
const wrong = () => error(c6, 401, "invalid_credentials", "E-mail or password is wrong.");
|
|
@@ -34259,16 +34607,8 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34259
34607
|
await verifyAgainstDummy(pw);
|
|
34260
34608
|
return wrong();
|
|
34261
34609
|
}
|
|
34262
|
-
const
|
|
34263
|
-
if (
|
|
34264
|
-
const seconds = Math.ceil((account.lockedUntil - now) / 1e3);
|
|
34265
|
-
return error(c6, 429, "too_many_attempts", `Too many failed attempts. Try again in ${seconds} seconds.`);
|
|
34266
|
-
}
|
|
34267
|
-
const failed = account.failedAttempts + 1;
|
|
34268
|
-
const lockedUntil = failed >= FREE_ATTEMPTS ? now + Math.min(2 ** (failed - FREE_ATTEMPTS) * 1e3, MAX_LOCK_MS) : 0;
|
|
34269
|
-
if (!await users.countAttempt(account, { failedAttempts: failed, lockedUntil })) {
|
|
34270
|
-
return error(c6, 429, "too_many_attempts", "Too many attempts at once. Please try again.");
|
|
34271
|
-
}
|
|
34610
|
+
const refused = await countSignInAttempt(c6, users, account);
|
|
34611
|
+
if (refused) return refused;
|
|
34272
34612
|
const { ok, needsRehash } = await verifyPassword(pw, account.secret);
|
|
34273
34613
|
if (!ok) return wrong();
|
|
34274
34614
|
await users.updateAccount(account.id, {
|
|
@@ -34279,28 +34619,24 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34279
34619
|
if (!user.emailVerified) {
|
|
34280
34620
|
return error(c6, 403, "email_unverified", "Please confirm your e-mail address first. We can send the e-mail again.");
|
|
34281
34621
|
}
|
|
34282
|
-
|
|
34283
|
-
await ctx.emit("user.signed_in", { user: result, method: "password" });
|
|
34284
|
-
return c6.json({ user: result });
|
|
34622
|
+
return signInAnswer(c6, await ctx.secondStep(c6, user, ["pwd"], "password"));
|
|
34285
34623
|
});
|
|
34286
34624
|
app.post("/auth/password/signup", async (c6) => {
|
|
34287
34625
|
const data = await body(c6);
|
|
34288
|
-
const target = redirectTarget(c6, data.redirectTo);
|
|
34626
|
+
const target = redirectTarget(ctx, c6, data.redirectTo);
|
|
34289
34627
|
if (target instanceof Response) return target;
|
|
34290
34628
|
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
34291
34629
|
const policy = checkPasswordPolicy(data.password, minLength);
|
|
34292
34630
|
if (policy) return error(c6, 400, "weak_password", policy);
|
|
34293
|
-
const name =
|
|
34631
|
+
const name = nameOf(data.name);
|
|
34294
34632
|
const email = data.email.trim();
|
|
34295
34633
|
const locale = localeOf(data.locale);
|
|
34296
34634
|
const check2 = await ctx.checkSignup({ email, name: name ?? void 0, locale, method: "password" });
|
|
34297
34635
|
if (!check2.ok) return check2.reason === "rejected" ? error(c6, 403, "signup_rejected", check2.message) : unavailable(c6);
|
|
34298
34636
|
const passwordHash = await hashPassword(data.password);
|
|
34299
34637
|
const existing = await users.findByEmail(normalizeEmail(email));
|
|
34300
|
-
if (existing?.emailVerified) {
|
|
34301
|
-
|
|
34302
|
-
const sent = await sendEmail(existing, "email.already_registered", { link: target.href, site: target.host, locale });
|
|
34303
|
-
return sent ? accepted(c6) : unavailable(c6);
|
|
34638
|
+
if (existing?.emailVerified && await users.hasAccounts(existing.id)) {
|
|
34639
|
+
return await emails.sendAccountEmail(existing, "email.already_registered", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34304
34640
|
}
|
|
34305
34641
|
if (existing) {
|
|
34306
34642
|
if (Date.now() - existing.lastMailAt < MAIL_INTERVAL_MS) return accepted(c6);
|
|
@@ -34308,7 +34644,7 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34308
34644
|
if (account) await users.updateAccount(account.id, { secret: passwordHash, failedAttempts: 0, lockedUntil: 0 });
|
|
34309
34645
|
else await users.linkAccount(existing.id, { key: passwordAccountKey(existing.id), secret: passwordHash });
|
|
34310
34646
|
await users.updateUser(existing.id, { name });
|
|
34311
|
-
return await sendLinkEmail({ ...existing, name }, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34647
|
+
return await emails.sendLinkEmail({ ...existing, name }, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34312
34648
|
}
|
|
34313
34649
|
const user = await users.createUser({
|
|
34314
34650
|
email,
|
|
@@ -34319,38 +34655,19 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34319
34655
|
});
|
|
34320
34656
|
if (!user) return accepted(c6);
|
|
34321
34657
|
await ctx.emit("user.created", { user: toMadauthUser(user), method: "password" });
|
|
34322
|
-
return await sendLinkEmail(user, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34323
|
-
});
|
|
34324
|
-
app.post("/auth/password/send-verification", async (c6) => {
|
|
34325
|
-
const data = await body(c6);
|
|
34326
|
-
const target = redirectTarget(c6, data.redirectTo);
|
|
34327
|
-
if (target instanceof Response) return target;
|
|
34328
|
-
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
34329
|
-
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34330
|
-
if (user && !user.emailVerified && await users.passwordAccount(user.id)) {
|
|
34331
|
-
if (!await sendLinkEmail(user, "verify", target, localeOf(data.locale))) return unavailable(c6);
|
|
34332
|
-
}
|
|
34333
|
-
return accepted(c6);
|
|
34334
|
-
});
|
|
34335
|
-
app.post("/auth/password/verify-email", async (c6) => {
|
|
34336
|
-
const consumed = await consume(await body(c6), "verify");
|
|
34337
|
-
const { userId, via } = consumed;
|
|
34338
|
-
const user = userId ? await users.findById(userId) : null;
|
|
34339
|
-
if (!user) return invalidVerification(c6, consumed);
|
|
34340
|
-
await users.updateUser(user.id, { emailVerified: true, wrongCodes: 0 });
|
|
34341
|
-
const result = await ctx.startSession(c6, toMadauthUser(user), ["pwd"], { sv: user.sessionVersion });
|
|
34342
|
-
await ctx.emit("email.verified", { user: result, via });
|
|
34343
|
-
await ctx.emit("user.signed_in", { user: result, method: "password" });
|
|
34344
|
-
return c6.json({ user: result });
|
|
34658
|
+
return await emails.sendLinkEmail(user, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34345
34659
|
});
|
|
34346
34660
|
app.post("/auth/password/send-reset", async (c6) => {
|
|
34347
34661
|
const data = await body(c6);
|
|
34348
|
-
const target = redirectTarget(c6, data.redirectTo);
|
|
34662
|
+
const target = redirectTarget(ctx, c6, data.redirectTo);
|
|
34349
34663
|
if (target instanceof Response) return target;
|
|
34350
34664
|
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
34351
34665
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34352
|
-
|
|
34353
|
-
|
|
34666
|
+
const locale = localeOf(data.locale);
|
|
34667
|
+
if (user && await users.passwordAccount(user.id)) {
|
|
34668
|
+
if (!await emails.sendLinkEmail(user, "reset", target, locale)) return unavailable(c6);
|
|
34669
|
+
} else if (user) {
|
|
34670
|
+
if (!await emails.sendAccountEmail(user, "email.no_password", target, locale)) return unavailable(c6);
|
|
34354
34671
|
}
|
|
34355
34672
|
return accepted(c6);
|
|
34356
34673
|
});
|
|
@@ -34358,89 +34675,359 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34358
34675
|
const data = await body(c6);
|
|
34359
34676
|
const policy = checkPasswordPolicy(data.password, minLength);
|
|
34360
34677
|
if (policy) return error(c6, 400, "weak_password", policy);
|
|
34361
|
-
const consumed = await consume(data, "reset");
|
|
34678
|
+
const consumed = await emails.consume(data, "reset");
|
|
34362
34679
|
const user = consumed.userId ? await users.findById(consumed.userId) : null;
|
|
34363
|
-
|
|
34680
|
+
const account = user ? await users.passwordAccount(user.id) : null;
|
|
34681
|
+
if (!user || !account) return emails.invalidVerification(c6, consumed);
|
|
34364
34682
|
const secret = await hashPassword(data.password);
|
|
34365
|
-
|
|
34366
|
-
if (account) await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 });
|
|
34367
|
-
else await users.linkAccount(user.id, { key: passwordAccountKey(user.id), secret });
|
|
34683
|
+
if (!await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 })) return emails.invalidVerification(c6, consumed);
|
|
34368
34684
|
const sessionVersion = user.sessionVersion + 1;
|
|
34369
34685
|
await users.updateUser(user.id, { emailVerified: true, sessionVersion, wrongCodes: 0 });
|
|
34370
34686
|
await users.clearVerifications(user.id);
|
|
34371
|
-
const
|
|
34372
|
-
await ctx.emit("email.password_reset", { user:
|
|
34373
|
-
await ctx.
|
|
34374
|
-
return c6.json({ user: result });
|
|
34687
|
+
const reset = { ...user, emailVerified: true, sessionVersion, wrongCodes: 0 };
|
|
34688
|
+
await ctx.emit("email.password_reset", { user: toMadauthUser(reset) });
|
|
34689
|
+
return signInAnswer(c6, await ctx.secondStep(c6, reset, ["pwd"], "password"));
|
|
34375
34690
|
});
|
|
34376
34691
|
}
|
|
34377
34692
|
|
|
34378
|
-
// src/
|
|
34379
|
-
|
|
34380
|
-
var
|
|
34381
|
-
|
|
34382
|
-
|
|
34383
|
-
|
|
34384
|
-
|
|
34385
|
-
|
|
34386
|
-
|
|
34693
|
+
// src/totp.ts
|
|
34694
|
+
import { createCipheriv, createDecipheriv, createHmac as createHmac4, hkdfSync as hkdfSync2, randomBytes as randomBytes4, randomInt as randomInt2 } from "crypto";
|
|
34695
|
+
var TOTP_STEP_SECONDS = 30;
|
|
34696
|
+
var TOTP_DIGITS = 6;
|
|
34697
|
+
var TOTP_WINDOW = 1;
|
|
34698
|
+
var RECOVERY_CODES = 10;
|
|
34699
|
+
var SECRET_BYTES = 20;
|
|
34700
|
+
var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
34701
|
+
var RECOVERY_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
34702
|
+
var RECOVERY_CODE_LENGTH = 10;
|
|
34703
|
+
var ENCRYPTION_VERSION = "v1";
|
|
34704
|
+
var IV_BYTES = 12;
|
|
34705
|
+
var TAG_BYTES = 16;
|
|
34706
|
+
function base32Encode(bytes) {
|
|
34707
|
+
let bits = 0;
|
|
34708
|
+
let value = 0;
|
|
34709
|
+
let out = "";
|
|
34710
|
+
for (const byte of bytes) {
|
|
34711
|
+
value = (value << 8 | byte) & 65535;
|
|
34712
|
+
bits += 8;
|
|
34713
|
+
while (bits >= 5) {
|
|
34714
|
+
out += BASE32[value >>> bits - 5 & 31];
|
|
34715
|
+
bits -= 5;
|
|
34716
|
+
}
|
|
34387
34717
|
}
|
|
34388
|
-
|
|
34718
|
+
if (bits > 0) out += BASE32[value << 5 - bits & 31];
|
|
34719
|
+
return out;
|
|
34389
34720
|
}
|
|
34390
|
-
|
|
34391
|
-
|
|
34392
|
-
|
|
34393
|
-
|
|
34721
|
+
function hotp(secret, counter, digits = TOTP_DIGITS) {
|
|
34722
|
+
const message2 = Buffer.alloc(8);
|
|
34723
|
+
message2.writeBigUInt64BE(BigInt(counter));
|
|
34724
|
+
const digest2 = createHmac4("sha1", secret).update(message2).digest();
|
|
34725
|
+
const offset = digest2[digest2.length - 1] & 15;
|
|
34726
|
+
const binary = (digest2[offset] & 127) << 24 | digest2[offset + 1] << 16 | digest2[offset + 2] << 8 | digest2[offset + 3];
|
|
34727
|
+
return (binary % 10 ** digits).toString().padStart(digits, "0");
|
|
34728
|
+
}
|
|
34729
|
+
function stepOf(nowMs) {
|
|
34730
|
+
return Math.floor(nowMs / 1e3 / TOTP_STEP_SECONDS);
|
|
34731
|
+
}
|
|
34732
|
+
function totp(secret, step, digits = TOTP_DIGITS) {
|
|
34733
|
+
return hotp(secret, step, digits);
|
|
34734
|
+
}
|
|
34735
|
+
function verifyTotp(secret, code, nowMs, window = TOTP_WINDOW) {
|
|
34736
|
+
const given = code.replace(/\s/g, "");
|
|
34737
|
+
if (!new RegExp(`^\\d{${TOTP_DIGITS}}$`).test(given)) return null;
|
|
34738
|
+
const now = stepOf(nowMs);
|
|
34739
|
+
let found = null;
|
|
34740
|
+
for (let step = now - window; step <= now + window; step++) {
|
|
34741
|
+
if (safeEqual(totp(secret, step), given) && found === null) found = step;
|
|
34742
|
+
}
|
|
34743
|
+
return found;
|
|
34744
|
+
}
|
|
34745
|
+
function generateTotpSecret() {
|
|
34746
|
+
return randomBytes4(SECRET_BYTES);
|
|
34747
|
+
}
|
|
34748
|
+
function otpauthUri(options) {
|
|
34749
|
+
const issuer = encodeURIComponent(options.issuer);
|
|
34750
|
+
const account = encodeURIComponent(options.account);
|
|
34751
|
+
const secret = base32Encode(options.secret);
|
|
34752
|
+
return `otpauth://totp/${issuer}:${account}?secret=${secret}&issuer=${issuer}&algorithm=SHA1&digits=${TOTP_DIGITS}&period=${TOTP_STEP_SECONDS}`;
|
|
34753
|
+
}
|
|
34754
|
+
function deriveTotpKey(signingKeyD) {
|
|
34755
|
+
return Buffer.from(hkdfSync2("sha256", Buffer.from(signingKeyD, "base64url"), "", "madauth totp secret", 32));
|
|
34756
|
+
}
|
|
34757
|
+
function encryptSecret(key, secret) {
|
|
34758
|
+
const iv = randomBytes4(IV_BYTES);
|
|
34759
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
34760
|
+
const encrypted = Buffer.concat([cipher.update(secret), cipher.final()]);
|
|
34761
|
+
const parts = [iv, encrypted, cipher.getAuthTag()].map((part) => part.toString("base64url"));
|
|
34762
|
+
return [ENCRYPTION_VERSION, ...parts].join(".");
|
|
34763
|
+
}
|
|
34764
|
+
function decryptSecret(key, stored) {
|
|
34765
|
+
const [version, iv, encrypted, tag2, ...rest] = stored.split(".");
|
|
34766
|
+
if (version !== ENCRYPTION_VERSION || !iv || !encrypted || !tag2 || rest.length) return null;
|
|
34767
|
+
const ivBytes = Buffer.from(iv, "base64url");
|
|
34768
|
+
const tagBytes = Buffer.from(tag2, "base64url");
|
|
34769
|
+
if (ivBytes.length !== IV_BYTES || tagBytes.length !== TAG_BYTES) return null;
|
|
34770
|
+
try {
|
|
34771
|
+
const decipher = createDecipheriv("aes-256-gcm", key, ivBytes);
|
|
34772
|
+
decipher.setAuthTag(tagBytes);
|
|
34773
|
+
return Buffer.concat([decipher.update(Buffer.from(encrypted, "base64url")), decipher.final()]);
|
|
34774
|
+
} catch {
|
|
34775
|
+
return null;
|
|
34394
34776
|
}
|
|
34395
|
-
|
|
34396
|
-
|
|
34397
|
-
|
|
34777
|
+
}
|
|
34778
|
+
function generateRecoveryCodes() {
|
|
34779
|
+
const codes = /* @__PURE__ */ new Set();
|
|
34780
|
+
while (codes.size < RECOVERY_CODES) {
|
|
34781
|
+
let code = "";
|
|
34782
|
+
for (let i6 = 0; i6 < RECOVERY_CODE_LENGTH; i6++) code += RECOVERY_ALPHABET[randomInt2(RECOVERY_ALPHABET.length)];
|
|
34783
|
+
codes.add(`${code.slice(0, 5)}-${code.slice(5)}`);
|
|
34784
|
+
}
|
|
34785
|
+
return [...codes];
|
|
34786
|
+
}
|
|
34787
|
+
function normalizeRecoveryCode(text) {
|
|
34788
|
+
if (typeof text !== "string") return null;
|
|
34789
|
+
const code = text.toLowerCase().replace(/[\s-]/g, "");
|
|
34790
|
+
return code.length === RECOVERY_CODE_LENGTH && [...code].every((char) => RECOVERY_ALPHABET.includes(char)) ? code : null;
|
|
34791
|
+
}
|
|
34792
|
+
function hashRecoveryCode(codeKey, code) {
|
|
34793
|
+
return hashCode(codeKey, `recovery:${code}`);
|
|
34794
|
+
}
|
|
34795
|
+
|
|
34796
|
+
// src/routes/totp.ts
|
|
34797
|
+
var CHALLENGE_COOKIE = "madauth_challenge";
|
|
34798
|
+
var CHALLENGE_TYP = "madauth-challenge+jwt";
|
|
34799
|
+
var CHALLENGE_TTL = 10 * 60;
|
|
34800
|
+
var SETUP_COOKIE = "madauth_totp_setup";
|
|
34801
|
+
var SETUP_TYP = "madauth-totp-setup+jwt";
|
|
34802
|
+
var SETUP_TTL = 10 * 60;
|
|
34803
|
+
var COOKIE_PATH = "/auth/totp";
|
|
34804
|
+
var cookieOptions = (secure) => ({ path: COOKIE_PATH, httpOnly: true, secure, sameSite: "Strict" });
|
|
34805
|
+
async function setChallenge(c6, ctx, challenge) {
|
|
34806
|
+
const token = await signToken(await ctx.keys, ctx.config.issuer, CHALLENGE_TYP, { ...challenge }, CHALLENGE_TTL);
|
|
34807
|
+
setCookie(c6, CHALLENGE_COOKIE, token, { ...cookieOptions(ctx.secure), maxAge: CHALLENGE_TTL });
|
|
34808
|
+
}
|
|
34809
|
+
function clearChallenge(c6, secure) {
|
|
34810
|
+
deleteCookie(c6, CHALLENGE_COOKIE, cookieOptions(secure));
|
|
34811
|
+
}
|
|
34812
|
+
async function readChallenge(c6, ctx, next) {
|
|
34813
|
+
const claims = await readToken(await ctx.keys, ctx.config.issuer, CHALLENGE_TYP, getCookie(c6, CHALLENGE_COOKIE));
|
|
34814
|
+
return claims && claims.next === next && typeof claims.sub === "string" && Array.isArray(claims.amr) ? claims : null;
|
|
34815
|
+
}
|
|
34816
|
+
async function checkCode(c6, ctx, keys, user, account, data, wrong = () => error(c6, 401, "code_invalid", "The code is wrong or was already used.")) {
|
|
34817
|
+
const { users } = ctx;
|
|
34818
|
+
const refused = await countSignInAttempt(c6, users, account);
|
|
34819
|
+
if (refused) return refused;
|
|
34820
|
+
if (typeof data.recoveryCode === "string") {
|
|
34821
|
+
const code = normalizeRecoveryCode(data.recoveryCode);
|
|
34822
|
+
if (!code || !await users.consumeRecoveryCode(account, hashRecoveryCode(keys.codeKey, code))) return wrong();
|
|
34823
|
+
await users.updateAccount(account.id, { failedAttempts: 0, lockedUntil: 0 });
|
|
34824
|
+
const remaining = await users.countRecoveryCodes(account);
|
|
34825
|
+
await ctx.emit("totp.recovery_code_used", { user: toMadauthUser(user), remaining });
|
|
34826
|
+
return "recovery_code";
|
|
34827
|
+
}
|
|
34828
|
+
if (typeof data.code !== "string" || !account.secret) return wrong();
|
|
34829
|
+
const secret = decryptSecret(keys.totpKey, account.secret);
|
|
34830
|
+
if (!secret) {
|
|
34831
|
+
console.error(`[madauth] The authenticator secret of user ${user.id} can't be read: was MADAUTH_SIGNING_KEY changed?`);
|
|
34832
|
+
return wrong();
|
|
34398
34833
|
}
|
|
34399
|
-
|
|
34400
|
-
|
|
34401
|
-
|
|
34402
|
-
|
|
34403
|
-
|
|
34404
|
-
|
|
34405
|
-
|
|
34406
|
-
|
|
34834
|
+
const step = verifyTotp(secret, data.code, Date.now(), TOTP_WINDOW);
|
|
34835
|
+
if (step === null || step <= (account.lastUsedStep ?? 0) || !await users.useTotpStep(account, step)) return wrong();
|
|
34836
|
+
return "totp";
|
|
34837
|
+
}
|
|
34838
|
+
function totpRoutes(app, ctx, keys) {
|
|
34839
|
+
const { config, secure, users } = ctx;
|
|
34840
|
+
const { issuer } = config;
|
|
34841
|
+
const cookie = cookieOptions(secure);
|
|
34842
|
+
const methodsNow = (c6) => ctx.methods(c6);
|
|
34843
|
+
app.use(`${COOKIE_PATH}/*`, async (c6, next) => {
|
|
34844
|
+
if (!authenticatorOn(ctx.configured, await methodsNow(c6))) {
|
|
34845
|
+
return error(c6, 403, "method_disabled", "The authenticator app is switched off.");
|
|
34407
34846
|
}
|
|
34408
|
-
|
|
34409
|
-
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
34414
|
-
|
|
34415
|
-
|
|
34416
|
-
|
|
34417
|
-
|
|
34418
|
-
|
|
34419
|
-
|
|
34420
|
-
|
|
34421
|
-
|
|
34422
|
-
|
|
34847
|
+
await next();
|
|
34848
|
+
});
|
|
34849
|
+
const standalone = async (c6) => isOn("totp", ctx.configured, await methodsNow(c6));
|
|
34850
|
+
const notStandalone = (c6) => error(c6, 403, "method_disabled", "Signing in with the authenticator app alone is switched off.");
|
|
34851
|
+
const methodStillOn = async (c6, challenge) => isOn(challenge.method, ctx.configured, await methodsNow(c6));
|
|
34852
|
+
const methodOff = (c6) => error(c6, 403, "method_disabled", "This way of signing in is switched off.");
|
|
34853
|
+
const signedIn = async (c6) => {
|
|
34854
|
+
const user = await ctx.currentUser(c6);
|
|
34855
|
+
return user ? users.findById(user.id) : null;
|
|
34856
|
+
};
|
|
34857
|
+
const whoSetsUp = async (c6) => {
|
|
34858
|
+
const challenge = await readChallenge(c6, ctx, "totp-setup");
|
|
34859
|
+
const waiting = challenge ? await users.findById(challenge.sub) : null;
|
|
34860
|
+
if (challenge && waiting && waiting.sessionVersion === challenge.sv) return { user: waiting, challenge };
|
|
34861
|
+
const user = await signedIn(c6);
|
|
34862
|
+
return user ? { user, challenge: null } : null;
|
|
34863
|
+
};
|
|
34864
|
+
const newRecoveryCodes = () => {
|
|
34865
|
+
const recoveryCodes = generateRecoveryCodes();
|
|
34866
|
+
return { recoveryCodes, hashes: recoveryCodes.map((code) => hashRecoveryCode(keys.codeKey, normalizeRecoveryCode(code))) };
|
|
34867
|
+
};
|
|
34868
|
+
const issuerLabel = (c6) => {
|
|
34869
|
+
if (config.totpIssuer) return config.totpIssuer;
|
|
34870
|
+
const origin = c6.req.header("origin");
|
|
34871
|
+
return URL.canParse(origin ?? "") ? new URL(origin).hostname : new URL(issuer).hostname;
|
|
34872
|
+
};
|
|
34873
|
+
const finishSignIn = async (c6, user, challenge, via) => {
|
|
34874
|
+
const result = await ctx.startSession(c6, toMadauthUser(user, challenge.profile), [...challenge.amr, "otp"], { sv: user.sessionVersion });
|
|
34875
|
+
await ctx.emit("user.signed_in", { user: result, method: challenge.method, ...challenge.amr.length ? { secondFactor: via } : {} });
|
|
34876
|
+
return result;
|
|
34877
|
+
};
|
|
34878
|
+
app.post(`${COOKIE_PATH}/setup`, async (c6) => {
|
|
34879
|
+
const who = await whoSetsUp(c6);
|
|
34880
|
+
if (!who) return noSession(c6);
|
|
34881
|
+
if (who.challenge && !await methodStillOn(c6, who.challenge)) return methodOff(c6);
|
|
34882
|
+
const existing = await users.totpAccount(who.user.id);
|
|
34883
|
+
let approved;
|
|
34884
|
+
if (existing) {
|
|
34885
|
+
const previous = await readToken(await ctx.keys, issuer, SETUP_TYP, getCookie(c6, SETUP_COOKIE));
|
|
34886
|
+
const holds = previous?.sub === who.user.id && typeof previous.approved === "number" && Date.now() - previous.approved < SETUP_TTL * 1e3;
|
|
34887
|
+
if (holds) {
|
|
34888
|
+
approved = previous.approved;
|
|
34889
|
+
} else {
|
|
34890
|
+
const data = await body(c6);
|
|
34891
|
+
if (data.code === void 0 && data.recoveryCode === void 0) {
|
|
34892
|
+
return error(c6, 401, "code_required", "Enter a code from your current authenticator app (or a recovery code) to replace it.");
|
|
34893
|
+
}
|
|
34894
|
+
const via = await checkCode(c6, ctx, keys, who.user, existing, data);
|
|
34895
|
+
if (via instanceof Response) return via;
|
|
34896
|
+
approved = Date.now();
|
|
34897
|
+
}
|
|
34423
34898
|
}
|
|
34424
|
-
|
|
34425
|
-
|
|
34426
|
-
|
|
34427
|
-
|
|
34428
|
-
|
|
34429
|
-
|
|
34430
|
-
|
|
34431
|
-
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34899
|
+
const secret = generateTotpSecret();
|
|
34900
|
+
const claims = {
|
|
34901
|
+
sub: who.user.id,
|
|
34902
|
+
setup: newId("tsu"),
|
|
34903
|
+
secret: encryptSecret(keys.totpKey, secret),
|
|
34904
|
+
...existing ? { replaces: existing.setupId } : {},
|
|
34905
|
+
...approved ? { approved } : {}
|
|
34906
|
+
};
|
|
34907
|
+
const token = await signToken(await ctx.keys, issuer, SETUP_TYP, claims, SETUP_TTL);
|
|
34908
|
+
setCookie(c6, SETUP_COOKIE, token, { ...cookie, maxAge: SETUP_TTL });
|
|
34909
|
+
return c6.json({ secret: base32Encode(secret), uri: otpauthUri({ issuer: issuerLabel(c6), account: who.user.email, secret }) });
|
|
34910
|
+
});
|
|
34911
|
+
app.post(`${COOKIE_PATH}/confirm`, async (c6) => {
|
|
34912
|
+
const who = await whoSetsUp(c6);
|
|
34913
|
+
if (!who) return noSession(c6);
|
|
34914
|
+
const { user, challenge } = who;
|
|
34915
|
+
if (challenge && !await methodStillOn(c6, challenge)) return methodOff(c6);
|
|
34916
|
+
const data = await body(c6);
|
|
34917
|
+
const setup = await readToken(await ctx.keys, issuer, SETUP_TYP, getCookie(c6, SETUP_COOKIE));
|
|
34918
|
+
const secret = setup && setup.sub === user.id && typeof setup.secret === "string" && typeof setup.setup === "string" ? decryptSecret(keys.totpKey, setup.secret) : null;
|
|
34919
|
+
if (!setup || !secret) return error(c6, 400, "setup_expired", "The setup has expired or was started in another browser. Please start again.");
|
|
34920
|
+
const step = typeof data.code === "string" ? verifyTotp(secret, data.code, Date.now(), TOTP_WINDOW) : null;
|
|
34921
|
+
if (step === null) return error(c6, 400, "code_invalid", "The code is wrong. Enter the current code from the app.");
|
|
34922
|
+
const { recoveryCodes, hashes } = newRecoveryCodes();
|
|
34923
|
+
await users.addRecoveryCodes(user.id, setup.setup, hashes);
|
|
34924
|
+
const patch = { secret: setup.secret, setupId: setup.setup, failedAttempts: 0, lockedUntil: 0, lastUsedStep: step };
|
|
34925
|
+
const current = setup.replaces === void 0 ? null : await users.totpAccount(user.id);
|
|
34926
|
+
const written = setup.replaces === void 0 ? !!await users.linkAccount(user.id, { key: totpAccountKey(user.id), ...patch }) : !!current && await users.confirmTotpSetup(current.id, setup.replaces, patch);
|
|
34927
|
+
if (!written) {
|
|
34928
|
+
await users.clearRecoveryCodes(user.id, { only: setup.setup });
|
|
34929
|
+
deleteCookie(c6, SETUP_COOKIE, cookie);
|
|
34930
|
+
return error(c6, 400, "setup_expired", "Your authenticator app was set up or removed elsewhere meanwhile. Please start again.");
|
|
34931
|
+
}
|
|
34932
|
+
await users.clearRecoveryCodes(user.id, { except: setup.setup });
|
|
34933
|
+
deleteCookie(c6, SETUP_COOKIE, cookie);
|
|
34934
|
+
await ctx.emit("totp.enabled", { user: toMadauthUser(user) });
|
|
34935
|
+
if (!challenge) return c6.json({ recoveryCodes });
|
|
34936
|
+
const result = await finishSignIn(c6, user, challenge, "totp");
|
|
34937
|
+
return c6.json({ user: result, recoveryCodes });
|
|
34938
|
+
});
|
|
34939
|
+
app.post(`${COOKIE_PATH}/verify`, async (c6) => {
|
|
34940
|
+
const expired = () => error(c6, 401, "challenge_expired", "The sign-in has expired. Please sign in again.");
|
|
34941
|
+
const challenge = await readChallenge(c6, ctx, "totp");
|
|
34942
|
+
if (!challenge) return expired();
|
|
34943
|
+
if (!await methodStillOn(c6, challenge)) return methodOff(c6);
|
|
34944
|
+
const user = await users.findById(challenge.sub);
|
|
34945
|
+
const account = user && user.sessionVersion === challenge.sv ? await users.totpAccount(user.id) : null;
|
|
34946
|
+
if (!user || !account) return expired();
|
|
34947
|
+
const via = await checkCode(c6, ctx, keys, user, account, await body(c6));
|
|
34948
|
+
if (via instanceof Response) return via;
|
|
34949
|
+
return c6.json({ user: await finishSignIn(c6, user, challenge, via) });
|
|
34950
|
+
});
|
|
34951
|
+
app.post(`${COOKIE_PATH}/signin`, async (c6) => {
|
|
34952
|
+
if (!await standalone(c6)) return notStandalone(c6);
|
|
34953
|
+
const data = await body(c6);
|
|
34954
|
+
const wrong = () => error(c6, 401, "invalid_credentials", "E-mail or code is wrong.");
|
|
34955
|
+
if (!isValidEmail(data.email)) return wrong();
|
|
34956
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34957
|
+
const account = user ? await users.totpAccount(user.id) : null;
|
|
34958
|
+
if (!user || !user.emailVerified || !account?.secret) return wrong();
|
|
34959
|
+
const via = await checkCode(c6, ctx, keys, user, account, data, wrong);
|
|
34960
|
+
if (via instanceof Response) return via;
|
|
34961
|
+
const result = await ctx.startSession(c6, toMadauthUser(user), ["otp"], { sv: user.sessionVersion });
|
|
34962
|
+
await ctx.emit("user.signed_in", { user: result, method: "totp" });
|
|
34963
|
+
return c6.json({ user: result });
|
|
34964
|
+
});
|
|
34965
|
+
app.post(`${COOKIE_PATH}/remove`, async (c6) => {
|
|
34966
|
+
const user = await signedIn(c6);
|
|
34967
|
+
if (!user) return noSession(c6);
|
|
34968
|
+
const methods = await methodsNow(c6);
|
|
34969
|
+
const required = METHODS_WITH_SECOND_FACTOR.filter((method) => isOn(method, ctx.configured, methods) && policyOf(method, methods) === "required");
|
|
34970
|
+
if (required.length) {
|
|
34971
|
+
const names = required.map((method) => method === "google" ? "Google" : "e-mail & password");
|
|
34972
|
+
return error(c6, 403, "required_by_policy", `Signing in with ${names.join(" and ")} requires the authenticator app, so it can't be removed.`);
|
|
34973
|
+
}
|
|
34974
|
+
const account = await users.totpAccount(user.id);
|
|
34975
|
+
if (!account) return c6.json({});
|
|
34976
|
+
if (!(await users.signInMethods(user.id, methods)).some((method) => method !== "totp" && isOn(method, ctx.configured, methods))) {
|
|
34977
|
+
return error(c6, 403, "only_method", "The authenticator app is the only way you sign in, so it can't be removed. Delete your account instead.");
|
|
34978
|
+
}
|
|
34979
|
+
const via = await checkCode(c6, ctx, keys, user, account, await body(c6));
|
|
34980
|
+
if (via instanceof Response) return via;
|
|
34981
|
+
await users.removeTotp(user.id);
|
|
34982
|
+
await ctx.emit("totp.disabled", { user: toMadauthUser(user) });
|
|
34983
|
+
return c6.json({});
|
|
34984
|
+
});
|
|
34985
|
+
app.post(`${COOKIE_PATH}/recovery-codes`, async (c6) => {
|
|
34986
|
+
const user = await signedIn(c6);
|
|
34987
|
+
if (!user) return noSession(c6);
|
|
34988
|
+
const account = await users.totpAccount(user.id);
|
|
34989
|
+
if (!account) return error(c6, 400, "no_authenticator", "No authenticator app is set up.");
|
|
34990
|
+
const via = await checkCode(c6, ctx, keys, user, account, await body(c6));
|
|
34991
|
+
if (via instanceof Response) return via;
|
|
34992
|
+
const { recoveryCodes, hashes } = newRecoveryCodes();
|
|
34993
|
+
await users.replaceRecoveryCodes(account, hashes);
|
|
34994
|
+
return c6.json({ recoveryCodes });
|
|
34995
|
+
});
|
|
34996
|
+
app.get(`${COOKIE_PATH}/status`, async (c6) => {
|
|
34997
|
+
const user = await signedIn(c6);
|
|
34998
|
+
if (!user) return noSession(c6);
|
|
34999
|
+
const account = await users.totpAccount(user.id);
|
|
35000
|
+
return c6.json({ enabled: !!account, recoveryCodesLeft: account ? await users.countRecoveryCodes(account) : 0 });
|
|
35001
|
+
});
|
|
35002
|
+
if (ctx.webhook && ctx.emailVerification) {
|
|
35003
|
+
const emails = createEmails(ctx, users, ctx.webhook);
|
|
35004
|
+
app.post(`${COOKIE_PATH}/signup`, async (c6) => {
|
|
35005
|
+
if (!await standalone(c6)) return notStandalone(c6);
|
|
35006
|
+
const data = await body(c6);
|
|
35007
|
+
const target = redirectTarget(ctx, c6, data.redirectTo);
|
|
35008
|
+
if (target instanceof Response) return target;
|
|
35009
|
+
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
35010
|
+
const name = nameOf(data.name);
|
|
35011
|
+
const email = data.email.trim();
|
|
35012
|
+
const locale = localeOf(data.locale);
|
|
35013
|
+
const check2 = await ctx.checkSignup({ email, name: name ?? void 0, locale, method: "totp" });
|
|
35014
|
+
if (!check2.ok) return check2.reason === "rejected" ? error(c6, 403, "signup_rejected", check2.message) : unavailable(c6);
|
|
35015
|
+
const existing = await users.findByEmail(normalizeEmail(email));
|
|
35016
|
+
if (existing?.emailVerified && await users.hasAccounts(existing.id)) {
|
|
35017
|
+
return await emails.sendAccountEmail(existing, "email.already_registered", target, locale) ? accepted(c6) : unavailable(c6);
|
|
35018
|
+
}
|
|
35019
|
+
if (existing) {
|
|
35020
|
+
if (Date.now() - existing.lastMailAt < MAIL_INTERVAL_MS) return accepted(c6);
|
|
35021
|
+
if (!existing.emailVerified) await users.unlinkAccount(passwordAccountKey(existing.id));
|
|
35022
|
+
await users.updateUser(existing.id, { name });
|
|
35023
|
+
return await emails.sendLinkEmail({ ...existing, name }, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
35024
|
+
}
|
|
35025
|
+
const user = await users.createUser({ email, emailNormalized: normalizeEmail(email), name, emailVerified: false });
|
|
35026
|
+
if (!user) return accepted(c6);
|
|
35027
|
+
await ctx.emit("user.created", { user: toMadauthUser(user), method: "totp" });
|
|
35028
|
+
return await emails.sendLinkEmail(user, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
35029
|
+
});
|
|
34437
35030
|
}
|
|
34438
|
-
};
|
|
34439
|
-
function isOn(method, available, methods) {
|
|
34440
|
-
return available(method) && methods[method] !== false;
|
|
34441
|
-
}
|
|
34442
|
-
function anyOn(available, methods) {
|
|
34443
|
-
return SIGN_IN_METHODS.some((method) => isOn(method, available, methods));
|
|
34444
35031
|
}
|
|
34445
35032
|
|
|
34446
35033
|
// src/app.ts
|
|
@@ -34450,9 +35037,14 @@ function createApp(config) {
|
|
|
34450
35037
|
const keys = importSigningKeys(config.signingKey);
|
|
34451
35038
|
const secure = issuer.startsWith("https:");
|
|
34452
35039
|
const isAllowedOrigin = (origin) => !!origin && allowedOrigins.includes(origin);
|
|
34453
|
-
const
|
|
34454
|
-
const
|
|
34455
|
-
const
|
|
35040
|
+
const codeKey = deriveCodeKey(config.signingKey.d);
|
|
35041
|
+
const users = new Users(config.store, codeKey);
|
|
35042
|
+
const methods = methodConfig(config);
|
|
35043
|
+
const settings = new Settings(config.store, initialMethods(methods));
|
|
35044
|
+
const configured = (method) => isConfigured(methods, method);
|
|
35045
|
+
const signsUp = (method) => canSignUp(methods, method);
|
|
35046
|
+
const emailVerification = sendsVerificationEmails(config);
|
|
35047
|
+
const totpKeys = { totpKey: deriveTotpKey(config.signingKey.d), codeKey };
|
|
34456
35048
|
const issueSession = async (c6, user, amr, extra) => {
|
|
34457
35049
|
const signingKeys = await keys;
|
|
34458
35050
|
const [sessionToken, renewalToken] = await Promise.all([
|
|
@@ -34487,6 +35079,29 @@ function createApp(config) {
|
|
|
34487
35079
|
deleteCookie(c6, RENEWAL_COOKIE, { path: "/auth", secure });
|
|
34488
35080
|
deleteCookie(c6, EXPIRY_COOKIE, { path: "/", domain: cookieDomain, secure });
|
|
34489
35081
|
};
|
|
35082
|
+
const tokenClaims = async (c6, cookie, typ) => readToken(await keys, issuer, typ, getCookie(c6, cookie));
|
|
35083
|
+
const checked = async (claims) => {
|
|
35084
|
+
const stored = await users.findById(claims.sub);
|
|
35085
|
+
if (!stored || stored.sessionVersion !== claims.sv) return null;
|
|
35086
|
+
const { claims: _, ...user } = userFromClaims(claims);
|
|
35087
|
+
const current = claimsFromJson(stored.claims);
|
|
35088
|
+
return Object.keys(current).length ? { ...user, claims: current } : user;
|
|
35089
|
+
};
|
|
35090
|
+
const currentSession = async (c6) => {
|
|
35091
|
+
const session = await tokenClaims(c6, SESSION_COOKIE, SESSION_TYP);
|
|
35092
|
+
const user = session ? await checked(session) : null;
|
|
35093
|
+
const upToDate = session && user && sameClaims(user.claims, session.claims) && Date.now() / 1e3 - session.iat <= sessionTtlSeconds / 2;
|
|
35094
|
+
if (upToDate) return user;
|
|
35095
|
+
const renewal = await tokenClaims(c6, RENEWAL_COOKIE, RENEWAL_TYP);
|
|
35096
|
+
const sameSignIn = session && user && renewal && renewal.sub === session.sub && renewal.sv === session.sv && renewal.email === session.email;
|
|
35097
|
+
const renewed = sameSignIn ? user : renewal ? await checked(renewal) : null;
|
|
35098
|
+
if (renewal && renewed) {
|
|
35099
|
+
await issueSession(c6, renewed, renewal.amr, { sv: renewal.sv });
|
|
35100
|
+
return renewed;
|
|
35101
|
+
}
|
|
35102
|
+
return user;
|
|
35103
|
+
};
|
|
35104
|
+
const methodsOf = /* @__PURE__ */ new WeakMap();
|
|
34490
35105
|
const ctx = {
|
|
34491
35106
|
config,
|
|
34492
35107
|
keys,
|
|
@@ -34504,14 +35119,42 @@ function createApp(config) {
|
|
|
34504
35119
|
return url;
|
|
34505
35120
|
},
|
|
34506
35121
|
async startSession(c6, user, amr, extra) {
|
|
35122
|
+
clearChallenge(c6, secure);
|
|
34507
35123
|
await issueSession(c6, user, amr, extra);
|
|
34508
|
-
return user;
|
|
35124
|
+
return { ...user, amr };
|
|
35125
|
+
},
|
|
35126
|
+
async secondStep(c6, user, amr, method, profile) {
|
|
35127
|
+
const methods2 = await this.methods(c6);
|
|
35128
|
+
if (!isOn(method, configured, methods2)) return { disabled: true };
|
|
35129
|
+
const enrolled = await users.totpAccount(user.id);
|
|
35130
|
+
const policy = method === "totp" ? "required" : policyOf(method, methods2);
|
|
35131
|
+
const next = policy === "none" ? null : enrolled ? "totp" : policy === "required" ? "totp-setup" : null;
|
|
35132
|
+
if (next) {
|
|
35133
|
+
clearSession(c6);
|
|
35134
|
+
const known = { ...profile?.name ? { name: profile.name } : {}, ...profile?.picture ? { picture: profile.picture } : {} };
|
|
35135
|
+
await setChallenge(c6, this, { sub: user.id, sv: user.sessionVersion, amr, method, next, ...Object.keys(known).length ? { profile: known } : {} });
|
|
35136
|
+
return { next, method };
|
|
35137
|
+
}
|
|
35138
|
+
const result = await this.startSession(c6, toMadauthUser(user, profile), amr, { sv: user.sessionVersion });
|
|
35139
|
+
await this.emit("user.signed_in", { user: result, method });
|
|
35140
|
+
return { user: result };
|
|
34509
35141
|
},
|
|
35142
|
+
currentUser: currentSession,
|
|
34510
35143
|
users,
|
|
34511
35144
|
settings,
|
|
34512
35145
|
webhook: config.webhook ? createWebhookClient(config.webhook, config.webhookFetch) : void 0,
|
|
34513
|
-
|
|
34514
|
-
|
|
35146
|
+
configured,
|
|
35147
|
+
emailVerification,
|
|
35148
|
+
methods(c6) {
|
|
35149
|
+
let reading = methodsOf.get(c6.req.raw);
|
|
35150
|
+
if (!reading) {
|
|
35151
|
+
reading = settings.methods();
|
|
35152
|
+
methodsOf.set(c6.req.raw, reading);
|
|
35153
|
+
}
|
|
35154
|
+
return reading;
|
|
35155
|
+
},
|
|
35156
|
+
async enabled(c6, method) {
|
|
35157
|
+
return isOn(method, configured, await this.methods(c6));
|
|
34515
35158
|
},
|
|
34516
35159
|
async checkSignup(data) {
|
|
34517
35160
|
if (!this.webhook?.wants("signup.before")) return { ok: true };
|
|
@@ -34529,7 +35172,9 @@ function createApp(config) {
|
|
|
34529
35172
|
},
|
|
34530
35173
|
async emit(type, data) {
|
|
34531
35174
|
if (!this.webhook?.wants(type)) return;
|
|
34532
|
-
const
|
|
35175
|
+
const user = data.user;
|
|
35176
|
+
const payload = user && typeof user === "object" && "amr" in user ? { ...data, user: { ...user, amr: void 0 } } : data;
|
|
35177
|
+
const result = await this.webhook.call(type, payload, EVENT_TIMEOUT_MS);
|
|
34533
35178
|
if (!result.ok) console.error(`[madauth] Webhook "${type}" failed: ${result.reason}`);
|
|
34534
35179
|
}
|
|
34535
35180
|
};
|
|
@@ -34557,37 +35202,20 @@ function createApp(config) {
|
|
|
34557
35202
|
return c6.json({ keys: [publicJwk] });
|
|
34558
35203
|
});
|
|
34559
35204
|
app.get("/auth/config", async (c6) => {
|
|
34560
|
-
const
|
|
34561
|
-
const on = (method) => isOn(method,
|
|
35205
|
+
const current = await ctx.methods(c6);
|
|
35206
|
+
const on = (method) => isOn(method, configured, current);
|
|
34562
35207
|
return c6.json({
|
|
34563
|
-
google: on("google") ? { clientId: config.google.clientId, codeFlow: !!config.google.clientSecret } : null,
|
|
34564
|
-
password: on("password") ? { minLength: config.password.minLength } : null
|
|
35208
|
+
google: on("google") ? { clientId: config.google.clientId, codeFlow: !!config.google.clientSecret, secondFactor: policyOf("google", current) } : null,
|
|
35209
|
+
password: on("password") ? { minLength: config.password.minLength, secondFactor: policyOf("password", current) } : null,
|
|
35210
|
+
// The app can be set up when it is in use at all; `signIn` says whether it signs in on its own.
|
|
35211
|
+
totp: authenticatorOn(configured, current) ? { signIn: on("totp"), signUp: on("totp") && emailVerification } : null,
|
|
35212
|
+
email: { verification: emailVerification }
|
|
34565
35213
|
});
|
|
34566
35214
|
});
|
|
35215
|
+
if (emailVerification && ctx.webhook) emailRoutes(app, ctx, users, ctx.webhook);
|
|
34567
35216
|
if (config.google) googleRoutes(app, ctx, config.google);
|
|
34568
35217
|
if (config.password && ctx.webhook) passwordRoutes(app, ctx, config.password, users, ctx.webhook);
|
|
34569
|
-
|
|
34570
|
-
const checked = async (claims) => {
|
|
34571
|
-
const stored = await users.findById(claims.sub);
|
|
34572
|
-
if (!stored || stored.sessionVersion !== claims.sv) return null;
|
|
34573
|
-
const { claims: _, ...user } = userFromClaims(claims);
|
|
34574
|
-
const current = claimsFromJson(stored.claims);
|
|
34575
|
-
return Object.keys(current).length ? { ...user, claims: current } : user;
|
|
34576
|
-
};
|
|
34577
|
-
const currentSession = async (c6) => {
|
|
34578
|
-
const session = await tokenClaims(c6, SESSION_COOKIE, SESSION_TYP);
|
|
34579
|
-
const user = session ? await checked(session) : null;
|
|
34580
|
-
const upToDate = session && user && sameClaims(user.claims, session.claims) && Date.now() / 1e3 - session.iat <= sessionTtlSeconds / 2;
|
|
34581
|
-
if (upToDate) return user;
|
|
34582
|
-
const renewal = await tokenClaims(c6, RENEWAL_COOKIE, RENEWAL_TYP);
|
|
34583
|
-
const sameSignIn = session && user && renewal && renewal.sub === session.sub && renewal.sv === session.sv && renewal.email === session.email;
|
|
34584
|
-
const renewed = sameSignIn ? user : renewal ? await checked(renewal) : null;
|
|
34585
|
-
if (renewal && renewed) {
|
|
34586
|
-
await issueSession(c6, renewed, renewal.amr, { sv: renewal.sv });
|
|
34587
|
-
return renewed;
|
|
34588
|
-
}
|
|
34589
|
-
return user;
|
|
34590
|
-
};
|
|
35218
|
+
totpRoutes(app, ctx, totpKeys);
|
|
34591
35219
|
app.get("/auth/session", async (c6) => {
|
|
34592
35220
|
const user = await currentSession(c6);
|
|
34593
35221
|
if (!user) {
|
|
@@ -34598,11 +35226,23 @@ function createApp(config) {
|
|
|
34598
35226
|
});
|
|
34599
35227
|
app.post("/auth/logout", (c6) => {
|
|
34600
35228
|
clearSession(c6);
|
|
35229
|
+
clearChallenge(c6, secure);
|
|
34601
35230
|
return c6.body(null, 204);
|
|
34602
35231
|
});
|
|
34603
35232
|
app.post("/auth/account/delete", async (c6) => {
|
|
34604
35233
|
const user = await currentSession(c6);
|
|
34605
35234
|
if (!user) return c6.json({ error: "no_session" }, 401);
|
|
35235
|
+
const stored = await users.findById(user.id);
|
|
35236
|
+
const protects = stored && authenticatorOn(configured, await ctx.methods(c6));
|
|
35237
|
+
const authenticator = protects ? await users.totpAccount(user.id) : null;
|
|
35238
|
+
if (stored && authenticator) {
|
|
35239
|
+
const data = await body(c6);
|
|
35240
|
+
if (data.code === void 0 && data.recoveryCode === void 0) {
|
|
35241
|
+
return error(c6, 401, "code_required", "Enter a code from your authenticator app (or a recovery code) to delete your account.");
|
|
35242
|
+
}
|
|
35243
|
+
const via = await checkCode(c6, ctx, totpKeys, stored, authenticator, data);
|
|
35244
|
+
if (via instanceof Response) return via;
|
|
35245
|
+
}
|
|
34606
35246
|
await users.deleteById(user.id);
|
|
34607
35247
|
clearSession(c6);
|
|
34608
35248
|
await ctx.emit("user.deleted", { user });
|
|
@@ -34618,10 +35258,6 @@ function createApp(config) {
|
|
|
34618
35258
|
}
|
|
34619
35259
|
return { email };
|
|
34620
35260
|
};
|
|
34621
|
-
const body = async (c6) => {
|
|
34622
|
-
const data = await c6.req.json().catch(() => null);
|
|
34623
|
-
return data && typeof data === "object" ? data : {};
|
|
34624
|
-
};
|
|
34625
35261
|
const invalidEmail = (c6) => c6.json({ error: "invalid_email", message: "This is not a valid e-mail address." }, 400);
|
|
34626
35262
|
const noUser = (c6) => c6.json({ error: "user_not_found", message: "No user has this e-mail address." }, 404);
|
|
34627
35263
|
app.post("/auth/admin/claims/get", async (c6) => {
|
|
@@ -34655,9 +35291,16 @@ function createApp(config) {
|
|
|
34655
35291
|
await ctx.emit("user.claims_changed", { userId: user.id, email, claims, by: caller.email });
|
|
34656
35292
|
return c6.json({ email, userId: user.id, claims });
|
|
34657
35293
|
});
|
|
34658
|
-
const settingsAnswer = (
|
|
35294
|
+
const settingsAnswer = (methods2) => ({
|
|
34659
35295
|
methods: Object.fromEntries(
|
|
34660
|
-
SIGN_IN_METHODS.map((method) => [
|
|
35296
|
+
SIGN_IN_METHODS.map((method) => [
|
|
35297
|
+
method,
|
|
35298
|
+
{
|
|
35299
|
+
configured: configured(method),
|
|
35300
|
+
enabled: isOn(method, configured, methods2),
|
|
35301
|
+
...method === "totp" ? {} : { secondFactor: policyOf(method, methods2) }
|
|
35302
|
+
}
|
|
35303
|
+
])
|
|
34661
35304
|
)
|
|
34662
35305
|
});
|
|
34663
35306
|
app.post("/auth/admin/settings/get", async (c6) => {
|
|
@@ -34670,14 +35313,26 @@ function createApp(config) {
|
|
|
34670
35313
|
if ("answer" in caller) return caller.answer;
|
|
34671
35314
|
const data = await body(c6);
|
|
34672
35315
|
const invalid2 = (message2) => c6.json({ error: "invalid_settings", message: message2 }, 400);
|
|
34673
|
-
const
|
|
34674
|
-
if (!
|
|
34675
|
-
|
|
34676
|
-
|
|
34677
|
-
|
|
34678
|
-
}
|
|
34679
|
-
|
|
34680
|
-
return
|
|
35316
|
+
const next = parseMethodSettings(data.methods, { booleans: true });
|
|
35317
|
+
if (!next) {
|
|
35318
|
+
return invalid2(
|
|
35319
|
+
`methods must list the sign-in methods that are on (${SIGN_IN_METHODS.join(", ")}), each as true, {} or { "secondFactor": "none" | "optional" | "required" } (google and password), e.g. { "google": {}, "password": { "secondFactor": "optional" } }. A method that is not listed is off.`
|
|
35320
|
+
);
|
|
35321
|
+
}
|
|
35322
|
+
const methods2 = await settings.changeMethods(() => anyOn(signsUp, next) ? next : null, caller.email);
|
|
35323
|
+
if (!methods2) return invalid2("At least one sign-in method that creates users must stay on: Google, e-mail & password, or the authenticator app with sign-up by e-mail.");
|
|
35324
|
+
return c6.json(settingsAnswer(methods2));
|
|
35325
|
+
});
|
|
35326
|
+
app.post("/auth/admin/totp/remove", async (c6) => {
|
|
35327
|
+
const caller = await admin(c6);
|
|
35328
|
+
if ("answer" in caller) return caller.answer;
|
|
35329
|
+
const data = await body(c6);
|
|
35330
|
+
if (!isValidEmail(data.email)) return invalidEmail(c6);
|
|
35331
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
35332
|
+
if (!user) return noUser(c6);
|
|
35333
|
+
const removed = await users.removeTotp(user.id);
|
|
35334
|
+
if (removed) await ctx.emit("totp.disabled", { user: toMadauthUser(user), by: caller.email });
|
|
35335
|
+
return c6.json({ email: user.emailNormalized, userId: user.id, enabled: false });
|
|
34681
35336
|
});
|
|
34682
35337
|
return app;
|
|
34683
35338
|
}
|