@madauth/server 0.3.0-beta.2 → 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.
@@ -38,7 +38,7 @@ var init_schema = __esm({
38
38
  "src/store/schema.ts"() {
39
39
  "use strict";
40
40
  madauthSchema = {
41
- version: 4,
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
- /** How a user signs in. `key` is `password:<userId>` or `google:<sub>`; later e.g. `totp:<userId>`. */
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 (error) {
1214
- Object.defineProperty(error, "$response", {
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 error)) {
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
- error.message += "\n " + hint;
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 error.$responseBodyText !== "undefined") {
1232
- if (error.$response) {
1233
- error.$response.body = error.$responseBodyText;
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
- error.$metadata = {
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 error;
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 = (body) => {
3207
- if (!body) {
3237
+ calculateBodyLength = (body2) => {
3238
+ if (!body2) {
3208
3239
  return 0;
3209
3240
  }
3210
- if (typeof body === "string") {
3211
- return Buffer.byteLength(body);
3212
- } else if (typeof body.byteLength === "number") {
3213
- return body.byteLength;
3214
- } else if (typeof body.size === "number") {
3215
- return body.size;
3216
- } else if (typeof body.start === "number" && typeof body.end === "number") {
3217
- return body.end + 1 - body.start;
3218
- } else if (body instanceof ReadStream) {
3219
- if (body.path != null) {
3220
- return lstatSync(body.path).size;
3221
- } else if (typeof body.fd === "number") {
3222
- return fstatSync(body.fd).size;
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 ${body}`);
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(error, options = true) {
3297
- return Object.assign(new this(error.message, options), error);
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 (error) {
5225
- console.error(`Failed to construct URL with ${expression}`, error);
5226
- throw error;
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 + body.byteLength + 16;
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(body, headers.byteLength + 12);
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 error = new Error(toUtf83(message2.body));
6617
- error.name = code;
6618
- throw error;
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(body, deserializer) {
6660
- const inputStream = getChunkedStream(body);
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(body, deserializer) {
6720
- const bodyIterable = typeof body[Symbol.asyncIterator] === "function" ? body : readableToIterable(body);
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 body = serializer.flush();
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 body = event[unionMember].body;
6904
+ const body2 = event[unionMember].body;
6874
6905
  if (unionMember === (initialMessageType ?? "initial-response")) {
6875
- const dataObject = await this.deserializer.read(responseSchema, body);
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] = body;
6922
+ out[name] = body2;
6892
6923
  } else if (member2.isStringSchema()) {
6893
- out[name] = (this.serdeContext?.utf8Encoder ?? toUtf8)(body);
6924
+ out[name] = (this.serdeContext?.utf8Encoder ?? toUtf8)(body2);
6894
6925
  } else if (member2.isStructSchema()) {
6895
- out[name] = await this.deserializer.read(member2, body);
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, body, hasBindings, out)
6944
+ [unionMember]: await this.readEventMember(eventStreamSchema, body2, hasBindings, out)
6914
6945
  };
6915
6946
  }
6916
6947
  return {
6917
- [unionMember]: await this.deserializer.read(eventStreamSchema, body)
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, body, hasBindings, out) {
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 : body.byteLength === 0 ? {} : await this.deserializer.read(eventStreamSchema, body);
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 body = typeof messageSerialization === "string" ? (this.serdeContext?.utf8Decoder ?? fromUtf8)(messageSerialization) : messageSerialization;
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 (body && Object.keys(headers).map((str) => str.toLowerCase()).indexOf(CONTENT_LENGTH_HEADER) === -1) {
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(body);
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(error) {
8127
- return error.code === "ERR_HTTP2_STREAM_ERROR" && error.message.includes("NGHTTP2_REFUSED_STREAM");
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 = (error) => error?.$retryable !== void 0;
8135
- isClockSkewCorrectedError = (error) => error.$metadata?.clockSkewCorrected;
8136
- isBrowserNetworkError = (error) => {
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 = error && error instanceof TypeError;
8175
+ const isValid = error2 && error2 instanceof TypeError;
8145
8176
  if (!isValid) {
8146
8177
  return false;
8147
8178
  }
8148
- return errorMessages.has(error.message);
8179
+ return errorMessages.has(error2.message);
8149
8180
  };
8150
- isThrottlingError = (error) => error.$metadata?.httpStatusCode === 429 || THROTTLING_ERROR_CODES.includes(error.name) || error.$retryable?.throttling == true;
8151
- isTransientError = (error, depth = 0) => error?.name !== "AbortError" && (isRetryableByTrait(error) || isClockSkewCorrectedError(error) || error.name === "InvalidSignatureException" && error.message?.includes("Signature expired") || TRANSIENT_ERROR_CODES.includes(error.name) || NODEJS_TIMEOUT_ERROR_CODES.includes(error?.code || "") || NODEJS_NETWORK_ERROR_CODES.includes(error?.code || "") || TRANSIENT_ERROR_STATUS_CODES.includes(error.$metadata?.httpStatusCode || 0) || isBrowserNetworkError(error) || isNodeJsHttp2TransientError(error) || error.cause !== void 0 && depth <= 10 && isTransientError(error.cause, depth + 1));
8152
- isServerError = (error) => {
8153
- if (error.$metadata?.httpStatusCode !== void 0) {
8154
- const statusCode = error.$metadata.httpStatusCode;
8155
- if (500 <= statusCode && statusCode <= 599 && !isTransientError(error)) {
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 = (error) => {
8235
- if (error instanceof Error)
8236
- return error;
8237
- if (error instanceof Object)
8238
- return Object.assign(new Error(), error);
8239
- if (typeof error === "string")
8240
- return new Error(error);
8241
- return new Error(`AWS SDK error wrapper for ${error}`);
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 = (error, logger2) => {
8358
+ getRetryErrorInfo = (error2, logger2) => {
8328
8359
  const errorInfo = {
8329
- error,
8330
- errorType: getRetryErrorType(error)
8360
+ error: error2,
8361
+ errorType: getRetryErrorType(error2)
8331
8362
  };
8332
- const retryAfterHint = parseRetryAfterHeader(error.$response, logger2);
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 = (error) => {
8339
- if (isThrottlingError(error))
8369
+ getRetryErrorType = (error2) => {
8370
+ if (isThrottlingError(error2))
8340
8371
  return "THROTTLING";
8341
- if (isTransientError(error))
8372
+ if (isTransientError(error2))
8342
8373
  return "TRANSIENT";
8343
- if (isServerError(error))
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 (error) {
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: error.$metadata
8919
+ error: error2,
8920
+ metadata: error2.$metadata
8890
8921
  });
8891
- throw error;
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) => (error) => {
9250
- throw error;
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 (error) => {
11004
- const errorException = error;
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 error;
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 (body == void 0) {
11578
+ if (body2 == void 0) {
11548
11579
  return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
11549
- } else if (typeof body === "string" || ArrayBuffer.isView(body) || isArrayBuffer(body)) {
11580
+ } else if (typeof body2 === "string" || ArrayBuffer.isView(body2) || isArrayBuffer(body2)) {
11550
11581
  const hashCtor = new hashConstructor();
11551
- hashCtor.update(toUint8Array(body));
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 (error) {
17186
- if (error?.statusCode === 400) {
17187
- throw Object.assign(error, {
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 (error.message === "TimeoutError" || [403, 404, 405].includes(error.statusCode)) {
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 error = Object.assign(new Error(msg), {
17413
+ const error2 = Object.assign(new Error(msg), {
17383
17414
  name: "TimeoutError",
17384
17415
  code: "ETIMEDOUT"
17385
17416
  });
17386
- req.destroy(error);
17387
- reject(error);
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, body) {
17491
- if (body instanceof Readable4) {
17492
- body.pipe(httpRequest2);
17521
+ function writeBody(httpRequest2, body2) {
17522
+ if (body2 instanceof Readable4) {
17523
+ body2.pipe(httpRequest2);
17493
17524
  return;
17494
17525
  }
17495
- if (body) {
17496
- const isBuffer = Buffer.isBuffer(body);
17497
- const isString = typeof body === "string";
17526
+ if (body2) {
17527
+ const isBuffer = Buffer.isBuffer(body2);
17528
+ const isString = typeof body2 === "string";
17498
17529
  if (isBuffer || isString) {
17499
- if (isBuffer && body.byteLength === 0) {
17530
+ if (isBuffer && body2.byteLength === 0) {
17500
17531
  httpRequest2.end();
17501
17532
  } else {
17502
- httpRequest2.end(body);
17533
+ httpRequest2.end(body2);
17503
17534
  }
17504
17535
  return;
17505
17536
  }
17506
- const uint8 = body;
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(body));
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 error = decorateServiceException(exception, additions);
18425
+ const error2 = decorateServiceException(exception, additions);
18395
18426
  if (msg) {
18396
- error.message = msg;
18397
- }
18398
- const errorObj = error.Error ?? {};
18399
- errorObj.Type = error.Error?.Type;
18400
- errorObj.Code = error.Error?.Code;
18401
- errorObj.Message = error.Error?.message ?? error.Error?.Message ?? msg;
18402
- error.Error = errorObj;
18403
- const reqId = error.$metadata.requestId;
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
- error.RequestId = reqId;
18436
+ error2.RequestId = reqId;
18406
18437
  }
18407
- return error;
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((body) => (context?.utf8Encoder ?? toUtf8)(body));
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 (error) {
21648
+ } catch (error2) {
21618
21649
  }
21619
21650
  return {
21620
21651
  token: newSsoOidcToken.accessToken,
21621
21652
  expiration: newTokenExpiration
21622
21653
  };
21623
- } catch (error) {
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 (error) {
25095
- if (error.name === "AccessDeniedException") {
25096
- const errorType = error.error;
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(error)}. Please re-authenticate using \`aws login\``;
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(error)}. Using existing token until expiry.`);
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(error)}. Please re-authenticate using aws login`, { logger: this.logger });
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 (error) {
25141
- throw new CredentialsProviderError(`Failed to load token from ${tokenFilePath}: ${String(error)}`, {
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 (error) {
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 (error) {
25244
- throw new CredentialsProviderError(`Failed to generate Dpop proof: ${error instanceof Error ? error.message : String(error)}`, { logger: this.logger, tryNextLink: false });
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 (error) {
25374
- throw new CredentialsProviderError(error.message, { logger: logger2 });
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(error) {
28683
- const { name, CancellationReasons } = error;
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: error };
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: error };
28720
+ if (reasons.length && reasons.every((code) => RETRYABLE_REASONS.includes(code))) return { retry: error2 };
28690
28721
  }
28691
- throw error;
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((error) => {
29797
- throw cached[alg] === pending && delete cached[alg], error;
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 error = new JWKSMultipleMatchingKeys();
29824
- throw error[Symbol.asyncIterator] = async function* () {
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
- }, error;
29861
+ }, error2;
29831
29862
  }
29832
29863
  return importWithAlgCache(cached, jwk, entry);
29833
29864
  }, "jwks", {
@@ -30027,7 +30058,10 @@ var WEBHOOK_TYPES = [
30027
30058
  "email.password_reset",
30028
30059
  "user.signed_in",
30029
30060
  "user.deleted",
30030
- "user.claims_changed"
30061
+ "user.claims_changed",
30062
+ "totp.enabled",
30063
+ "totp.disabled",
30064
+ "totp.recovery_code_used"
30031
30065
  ];
30032
30066
  var WEBHOOK_TIMEOUT_MS = 1e4;
30033
30067
  var MAX_LOCALE_LENGTH = 35;
@@ -30051,8 +30085,8 @@ function hmac2(secret, content) {
30051
30085
  const key = Buffer.from(secret.slice(SECRET_PREFIX.length), "base64");
30052
30086
  return createHmac("sha256", key).update(content).digest("base64");
30053
30087
  }
30054
- function signWebhook(secret, id, timestamp, body) {
30055
- return `v1,${hmac2(secret, `${id}.${timestamp}.${body}`)}`;
30088
+ function signWebhook(secret, id, timestamp, body2) {
30089
+ return `v1,${hmac2(secret, `${id}.${timestamp}.${body2}`)}`;
30056
30090
  }
30057
30091
  function createWebhookClient(settings, fetchImpl = fetch) {
30058
30092
  const wants = (type) => settings.events.has(type);
@@ -30062,7 +30096,7 @@ function createWebhookClient(settings, fetchImpl = fetch) {
30062
30096
  if (!wants(type)) return { ok: true, body: void 0 };
30063
30097
  const id = `msg_${randomUUID()}`;
30064
30098
  const timestamp = Math.floor(Date.now() / 1e3);
30065
- const body = JSON.stringify({ type, data });
30099
+ const body2 = JSON.stringify({ type, data });
30066
30100
  let response;
30067
30101
  try {
30068
30102
  response = await fetchImpl(settings.url, {
@@ -30071,9 +30105,9 @@ function createWebhookClient(settings, fetchImpl = fetch) {
30071
30105
  "Content-Type": "application/json",
30072
30106
  "webhook-id": id,
30073
30107
  "webhook-timestamp": String(timestamp),
30074
- "webhook-signature": signWebhook(settings.secret, id, timestamp, body)
30108
+ "webhook-signature": signWebhook(settings.secret, id, timestamp, body2)
30075
30109
  },
30076
- body,
30110
+ body: body2,
30077
30111
  signal: AbortSignal.timeout(timeoutMs)
30078
30112
  });
30079
30113
  } catch (e6) {
@@ -30094,6 +30128,12 @@ function createWebhookClient(settings, fetchImpl = fetch) {
30094
30128
  }
30095
30129
 
30096
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
+ }
30097
30137
  async function resolveEnv(source = process.env) {
30098
30138
  return typeof source === "function" ? source() : source;
30099
30139
  }
@@ -30157,9 +30197,10 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
30157
30197
  const webhook = webhookFromEnv(read("WEBHOOK_URL"), read("WEBHOOK_SECRET"), read("WEBHOOK_EVENTS"));
30158
30198
  const sendsEmails = !!webhook && REQUIRED_EMAIL_TYPES.every((type) => webhook.events.has(type));
30159
30199
  const password = sendsEmails ? { minLength: passwordMinLength(read("PASSWORD_MIN_LENGTH")) } : void 0;
30160
- if (!clientId && !password) {
30200
+ const emailVerification = !!webhook && webhook.events.has("email.verify");
30201
+ if (!clientId && !password && !emailVerification) {
30161
30202
  throw new ConfigError(
30162
- `No sign-in method is configured. Set GOOGLE_CLIENT_ID for Google sign-in, and/or a webhook that sends the e-mails for e-mail & password sign-in: 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.`
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.`
30163
30204
  );
30164
30205
  }
30165
30206
  const store = overrides.store ?? await storeFromDatabaseUrl(read("DATABASE_URL"));
@@ -30187,6 +30228,7 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
30187
30228
  sessionTtlSeconds,
30188
30229
  renewalTtlSeconds,
30189
30230
  cookieDomain: read("COOKIE_DOMAIN"),
30231
+ totpIssuer: read("TOTP_ISSUER"),
30190
30232
  google: clientId ? { clientId, clientSecret: read("GOOGLE_CLIENT_SECRET") } : void 0,
30191
30233
  store,
30192
30234
  password,
@@ -30585,8 +30627,8 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
30585
30627
  const { done, value } = await reader.read();
30586
30628
  if (done) controller.close();
30587
30629
  else controller.enqueue(value);
30588
- } catch (error) {
30589
- controller.error(error);
30630
+ } catch (error2) {
30631
+ controller.error(error2);
30590
30632
  }
30591
30633
  } });
30592
30634
  } else {
@@ -30714,13 +30756,13 @@ var readBodyDirect = (request) => {
30714
30756
  cleanup();
30715
30757
  callback();
30716
30758
  };
30717
- const recoverCompleteBodyAfterDisconnect = (error) => {
30718
- const streamError = incoming.errored ?? error;
30759
+ const recoverCompleteBodyAfterDisconnect = (error2) => {
30760
+ const streamError = incoming.errored ?? error2;
30719
30761
  if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false;
30720
30762
  finish(() => {
30721
30763
  const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks);
30722
30764
  if (recovered instanceof Error) reject(recovered);
30723
- else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming));
30765
+ else if (recovered === void 0) reject(error2 ?? normalizeAbortError(request, incoming));
30724
30766
  else {
30725
30767
  request[bodyBufferKey] = recovered;
30726
30768
  resolve(recovered);
@@ -30738,10 +30780,10 @@ var readBodyDirect = (request) => {
30738
30780
  resolve(buffer);
30739
30781
  });
30740
30782
  };
30741
- const onError = (error) => {
30742
- if (recoverCompleteBodyAfterDisconnect(error)) return;
30783
+ const onError = (error2) => {
30784
+ if (recoverCompleteBodyAfterDisconnect(error2)) return;
30743
30785
  finish(() => {
30744
- reject(error);
30786
+ reject(error2);
30745
30787
  });
30746
30788
  };
30747
30789
  const onClose = () => {
@@ -30935,9 +30977,9 @@ var Response$1 = class Response$12 {
30935
30977
  headers: liveHeaders
30936
30978
  } : this.#init);
30937
30979
  }
30938
- constructor(body, init) {
30980
+ constructor(body2, init) {
30939
30981
  let headers;
30940
- this.#body = body;
30982
+ this.#body = body2;
30941
30983
  if (init instanceof GlobalResponse) {
30942
30984
  const cachedGlobalResponse = init[responseCache];
30943
30985
  if (cachedGlobalResponse) {
@@ -30948,9 +30990,9 @@ var Response$1 = class Response$12 {
30948
30990
  this.#init = init instanceof Response$12 ? init.#init : init;
30949
30991
  headers = new Headers(init.headers);
30950
30992
  } else this.#init = init;
30951
- if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [
30993
+ if (body2 == null || typeof body2 === "string" || typeof body2?.getReader !== "undefined" || body2 instanceof Blob || body2 instanceof Uint8Array) this[cacheKey] = [
30952
30994
  init?.status || 200,
30953
- body ?? null,
30995
+ body2 ?? null,
30954
30996
  headers || init?.headers
30955
30997
  ];
30956
30998
  }
@@ -31034,15 +31076,15 @@ Object.defineProperty(Response$1, "redirect", {
31034
31076
  });
31035
31077
  Object.defineProperty(Response$1, "json", {
31036
31078
  value: function json(data, init) {
31037
- const body = JSON.stringify(data);
31038
- if (body === void 0) throw new TypeError("The data is not JSON serializable");
31079
+ const body2 = JSON.stringify(data);
31080
+ if (body2 === void 0) throw new TypeError("The data is not JSON serializable");
31039
31081
  const initHeaders = init?.headers;
31040
31082
  let headers;
31041
31083
  if (initHeaders) {
31042
31084
  headers = new Headers(initHeaders);
31043
31085
  if (!headers.has("content-type")) headers.set("content-type", "application/json");
31044
31086
  } else headers = { "content-type": "application/json" };
31045
- return new Response$1(body, {
31087
+ return new Response$1(body2, {
31046
31088
  status: init?.status ?? 200,
31047
31089
  statusText: init?.statusText,
31048
31090
  headers
@@ -31055,8 +31097,8 @@ async function readWithoutBlocking(readPromise) {
31055
31097
  return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]);
31056
31098
  }
31057
31099
  function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) {
31058
- const cancel = (error) => {
31059
- reader.cancel(error).catch(() => {
31100
+ const cancel = (error2) => {
31101
+ reader.cancel(error2).catch(() => {
31060
31102
  });
31061
31103
  };
31062
31104
  writable.on("close", cancel);
@@ -31066,8 +31108,8 @@ function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromi
31066
31108
  writable.off("close", cancel);
31067
31109
  writable.off("error", cancel);
31068
31110
  });
31069
- function handleStreamError(error) {
31070
- if (error) writable.destroy(error);
31111
+ function handleStreamError(error2) {
31112
+ if (error2) writable.destroy(error2);
31071
31113
  }
31072
31114
  function onDrain() {
31073
31115
  reader.read().then(flow, handleStreamError);
@@ -31156,8 +31198,8 @@ var makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => {
31156
31198
  };
31157
31199
  var isImmediateCacheableResponse = (res) => {
31158
31200
  if (!(cacheKey in res)) return false;
31159
- const body = res[cacheKey][1];
31160
- return body === null || typeof body === "string" || body instanceof Uint8Array;
31201
+ const body2 = res[cacheKey][1];
31202
+ return body2 === null || typeof body2 === "string" || body2 instanceof Uint8Array;
31161
31203
  };
31162
31204
  var handleRequestError = () => new Response(null, { status: 400 });
31163
31205
  var handleFetchError = (e6) => new Response(null, { status: e6 instanceof Error && (e6.name === "TimeoutError" || e6.constructor.name === "TimeoutError") ? 504 : 500 });
@@ -31193,24 +31235,24 @@ var writeDefaultHeaders = (outgoing, status, length) => {
31193
31235
  });
31194
31236
  };
31195
31237
  var responseViaCache = async (res, outgoing) => {
31196
- let [status, body, header] = res[cacheKey];
31238
+ let [status, body2, header] = res[cacheKey];
31197
31239
  if (!header) {
31198
- if (body === null) {
31240
+ if (body2 === null) {
31199
31241
  outgoing.writeHead(status);
31200
31242
  outgoing.end();
31201
- } else if (typeof body === "string") {
31202
- writeDefaultHeaders(outgoing, status, Buffer.byteLength(body));
31203
- outgoing.end(body);
31204
- } else if (body instanceof Uint8Array) {
31205
- writeDefaultHeaders(outgoing, status, body.byteLength);
31206
- outgoing.end(body);
31207
- } else if (body instanceof Blob) {
31208
- writeDefaultHeaders(outgoing, status, body.size);
31209
- outgoing.end(new Uint8Array(await body.arrayBuffer()));
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()));
31210
31252
  } else {
31211
31253
  outgoing.writeHead(status, { "Content-Type": defaultContentType });
31212
31254
  flushHeaders(outgoing);
31213
- await writeFromReadableStream(body, outgoing)?.catch((e6) => handleResponseError(e6, outgoing));
31255
+ await writeFromReadableStream(body2, outgoing)?.catch((e6) => handleResponseError(e6, outgoing));
31214
31256
  }
31215
31257
  outgoing[outgoingEnded]?.();
31216
31258
  return;
@@ -31220,11 +31262,11 @@ var responseViaCache = async (res, outgoing) => {
31220
31262
  let canAutoLength = true;
31221
31263
  if (header instanceof Headers) {
31222
31264
  hasContentLength = header.has("content-length");
31223
- header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType);
31265
+ header = buildOutgoingHttpHeaders(header, body2 === null ? void 0 : defaultContentType);
31224
31266
  } else if (Array.isArray(header)) {
31225
31267
  const headerObj = new Headers(header);
31226
31268
  hasContentLength = headerObj.has("content-length");
31227
- header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType);
31269
+ header = buildOutgoingHttpHeaders(headerObj, body2 === null ? void 0 : defaultContentType);
31228
31270
  } else {
31229
31271
  plainHeaders = true;
31230
31272
  for (const key in header) {
@@ -31237,21 +31279,21 @@ var responseViaCache = async (res, outgoing) => {
31237
31279
  }
31238
31280
  if (!hasContentLength) {
31239
31281
  let length;
31240
- if (typeof body === "string") length = Buffer.byteLength(body);
31241
- else if (body instanceof Uint8Array) length = body.byteLength;
31242
- else if (body instanceof Blob) length = body.size;
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;
31243
31285
  if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) {
31244
31286
  if (plainHeaders) header = { ...header };
31245
31287
  header["Content-Length"] = length;
31246
31288
  }
31247
31289
  }
31248
31290
  outgoing.writeHead(status, header);
31249
- if (body == null) outgoing.end();
31250
- else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body);
31251
- else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer()));
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()));
31252
31294
  else {
31253
31295
  flushHeaders(outgoing);
31254
- await writeFromReadableStream(body, outgoing)?.catch((e6) => handleResponseError(e6, outgoing));
31296
+ await writeFromReadableStream(body2, outgoing)?.catch((e6) => handleResponseError(e6, outgoing));
31255
31297
  }
31256
31298
  outgoing[outgoingEnded]?.();
31257
31299
  };
@@ -31583,9 +31625,9 @@ var upgradeWebSocket = defineWebSocketHelper(async (c6, events, options) => {
31583
31625
  (options?.onError ?? console.error)(e6);
31584
31626
  }
31585
31627
  });
31586
- ws.on("error", (error) => {
31628
+ ws.on("error", (error2) => {
31587
31629
  try {
31588
- events?.onError?.(new ErrorEvent("error", { error }), ctx);
31630
+ events?.onError?.(new ErrorEvent("error", { error: error2 }), ctx);
31589
31631
  } catch (e6) {
31590
31632
  (options?.onError ?? console.error)(e6);
31591
31633
  }
@@ -31943,10 +31985,10 @@ var HonoRequest = class {
31943
31985
  const { bodyCache, raw: raw2 } = this;
31944
31986
  const cachedBody = bodyCache[key];
31945
31987
  if (cachedBody) return cachedBody;
31946
- for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((body) => {
31947
- if (anyCachedKey === "json") body = JSON.stringify(body);
31988
+ for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((body2) => {
31989
+ if (anyCachedKey === "json") body2 = JSON.stringify(body2);
31948
31990
  const contentType2 = anyCachedKey === "formData" ? void 0 : raw2.headers.get("content-type");
31949
- return new Response(body, { headers: contentType2 ? { "Content-Type": contentType2 } : void 0 })[key]();
31991
+ return new Response(body2, { headers: contentType2 ? { "Content-Type": contentType2 } : void 0 })[key]();
31950
31992
  });
31951
31993
  return bodyCache[key] = raw2[key]();
31952
31994
  };
@@ -32171,7 +32213,7 @@ var setDefaultContentType = (contentType2, headers) => {
32171
32213
  ...headers
32172
32214
  };
32173
32215
  };
32174
- var createResponseInstance = (body, init) => new Response(body, init);
32216
+ var createResponseInstance = (body2, init) => new Response(body2, init);
32175
32217
  var Context = class {
32176
32218
  #rawRequest;
32177
32219
  #req;
@@ -33712,115 +33754,58 @@ function safeEqual(a6, b6) {
33712
33754
  return x2.length === y2.length && timingSafeEqual2(x2, y2);
33713
33755
  }
33714
33756
 
33715
- // src/google.ts
33716
- var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
33717
- var googleJwks;
33718
- function googleKeys() {
33719
- googleJwks ??= createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
33720
- return googleJwks;
33721
- }
33722
- async function verifyGoogleIdToken(idToken, opts) {
33723
- let payload;
33724
- try {
33725
- ({ payload } = await jwtVerify(idToken, opts.keys ?? googleKeys(), {
33726
- issuer: GOOGLE_ISSUERS,
33727
- audience: opts.clientId,
33728
- algorithms: ["RS256", "ES256"]
33729
- }));
33730
- } catch (e6) {
33731
- return { ok: false, error: "verification_failed", reason: e6.message };
33732
- }
33733
- if (typeof payload.sub !== "string" || !payload.sub) {
33734
- return { ok: false, error: "verification_failed", reason: "missing sub" };
33735
- }
33736
- if (payload.nonce !== opts.nonce) {
33737
- return { ok: false, error: "verification_failed", reason: "nonce mismatch" };
33738
- }
33739
- if (typeof payload.email !== "string" || !payload.email || payload.email_verified !== true) {
33740
- return { ok: false, error: "email_unverified", reason: "e-mail address not verified" };
33741
- }
33742
- const profile = { sub: payload.sub, email: payload.email };
33743
- if (typeof payload.name === "string") profile.name = payload.name;
33744
- if (typeof payload.picture === "string") profile.picture = payload.picture;
33745
- return { ok: true, profile };
33746
- }
33747
-
33748
- // src/user.ts
33749
- var SESSION_COOKIE = "madauth_session";
33750
- var SESSION_TYP = "madauth-session+jwt";
33751
- var RENEWAL_COOKIE = "madauth_renewal";
33752
- var RENEWAL_TYP = "madauth-renewal+jwt";
33753
- var EXPIRY_COOKIE = "madauth_session_expires";
33754
-
33755
- // src/tokens.ts
33756
- function randomString(bytes = 32) {
33757
- return base64url_exports.encode(crypto.getRandomValues(new Uint8Array(bytes)));
33758
- }
33759
- async function signToken(keys, issuer, typ, claims, ttlSeconds) {
33760
- return new SignJWT(claims).setProtectedHeader({ alg: SIGNING_ALG, kid: keys.kid, typ }).setIssuer(issuer).setIssuedAt().setExpirationTime(`${ttlSeconds}s`).sign(keys.privateKey);
33761
- }
33762
- async function readToken(keys, issuer, typ, token) {
33763
- if (!token) return null;
33764
- try {
33765
- const { payload } = await jwtVerify(token, keys.publicKey, { issuer, typ, algorithms: [SIGNING_ALG] });
33766
- return payload;
33767
- } catch {
33768
- return null;
33769
- }
33770
- }
33771
- function signSession(keys, issuer, ttlSeconds, user, amr, extra = {}) {
33772
- const claims = {
33773
- ...extra,
33774
- sub: user.id,
33775
- email: user.email,
33776
- email_verified: user.email ? true : void 0,
33777
- name: user.name,
33778
- picture: user.picture,
33779
- claims: user.claims && Object.keys(user.claims).length ? user.claims : void 0,
33780
- amr
33781
- };
33782
- return signToken(keys, issuer, SESSION_TYP, claims, ttlSeconds);
33783
- }
33784
- function signRenewal(keys, issuer, ttlSeconds, user, amr, extra = {}) {
33785
- const claims = {
33786
- ...extra,
33787
- sub: user.id,
33788
- email: user.email,
33789
- email_verified: user.email ? true : void 0,
33790
- name: user.name,
33791
- picture: user.picture,
33792
- amr
33793
- };
33794
- return signToken(keys, issuer, RENEWAL_TYP, claims, ttlSeconds);
33795
- }
33796
- function userFromClaims(claims) {
33797
- const user = { id: claims.sub };
33798
- if (claims.email) user.email = claims.email;
33799
- if (claims.name) user.name = claims.name;
33800
- if (claims.picture) user.picture = claims.picture;
33801
- if (claims.claims && typeof claims.claims === "object" && Object.keys(claims.claims).length) user.claims = claims.claims;
33802
- return user;
33803
- }
33804
-
33805
33757
  // src/users.ts
33806
33758
  import { randomBytes as randomBytes3 } from "crypto";
33807
33759
 
33808
33760
  // src/settings.ts
33809
- var SIGN_IN_METHODS = ["google", "password"];
33761
+ var SIGN_IN_METHODS = ["google", "password", "totp"];
33762
+ var METHODS_WITH_SECOND_FACTOR = ["google", "password"];
33763
+ var SECOND_FACTOR_POLICIES = ["none", "optional", "required"];
33810
33764
  var METHODS_SETTING = "methods";
33811
- function parseMethodSettings(input) {
33765
+ var isSignInMethod = (value) => SIGN_IN_METHODS.includes(value);
33766
+ var isPolicy = (value) => SECOND_FACTOR_POLICIES.includes(value);
33767
+ function parseMethodSettings(input, options = {}) {
33812
33768
  if (!input || typeof input !== "object" || Array.isArray(input)) return null;
33813
33769
  const settings = {};
33814
- for (const [key, value] of Object.entries(input)) {
33815
- if (!SIGN_IN_METHODS.includes(key) || typeof value !== "boolean") return null;
33816
- settings[key] = value;
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;
33817
33784
  }
33818
33785
  return settings;
33819
33786
  }
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
+ }
33820
33802
  var MAX_TRIES = 3;
33821
33803
  var Settings = class {
33822
- constructor(store) {
33804
+ /** `initial` is what counts until the methods are set, see {@link initialMethods}. */
33805
+ constructor(store, initial = {}) {
33823
33806
  this.store = store;
33807
+ this.initial = initial;
33808
+ this.warned = false;
33824
33809
  }
33825
33810
  /** The setting's value, or undefined if it was never set. */
33826
33811
  async get(id) {
@@ -33857,20 +33842,38 @@ var Settings = class {
33857
33842
  async set(id, value, updatedBy) {
33858
33843
  await this.change(id, () => value, updatedBy);
33859
33844
  }
33860
- /** Which sign-in methods are on; a method that was never switched off is on. */
33845
+ /** Which sign-in methods are on, with their settings; the initial ones until an admin set them. */
33861
33846
  async methods() {
33862
- return parseMethodSettings(await this.get(METHODS_SETTING)) ?? {};
33847
+ return this.current(await this.get(METHODS_SETTING));
33863
33848
  }
33864
33849
  /** Changes which methods are on, see {@link change}; `apply` gets the methods as they are now. */
33865
33850
  async changeMethods(apply, updatedBy) {
33866
- return this.change(METHODS_SETTING, (current) => apply(parseMethodSettings(current) ?? {}), 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;
33867
33863
  }
33868
33864
  };
33869
- function isOn(method, available, methods) {
33870
- return available(method) && methods[method] !== false;
33865
+ function isOn(method, configured, methods) {
33866
+ return configured(method) && methods[method] !== void 0;
33867
+ }
33868
+ function policyOf(method, methods) {
33869
+ return methods[method]?.secondFactor ?? "none";
33870
+ }
33871
+ function anyOn(signsUp, methods) {
33872
+ return SIGN_IN_METHODS.some((method) => signsUp(method) && methods[method] !== void 0);
33871
33873
  }
33872
- function anyOn(available, methods) {
33873
- return SIGN_IN_METHODS.some((method) => isOn(method, available, methods));
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");
33874
33877
  }
33875
33878
 
33876
33879
  // src/users.ts
@@ -33883,6 +33886,9 @@ function passwordAccountKey(userId) {
33883
33886
  function googleAccountKey(sub) {
33884
33887
  return `google:${sub}`;
33885
33888
  }
33889
+ function totpAccountKey(userId) {
33890
+ return `totp:${userId}`;
33891
+ }
33886
33892
  function toMadauthUser(user, profile = {}) {
33887
33893
  const result = { id: user.id, email: user.email };
33888
33894
  const name = user.name ?? profile.name;
@@ -33911,18 +33917,32 @@ var Users = class {
33911
33917
  async passwordAccount(userId) {
33912
33918
  return this.findAccountByKey(passwordAccountKey(userId));
33913
33919
  }
33914
- /** How the user signs in, from their accounts: e.g. `['google']` for a user without a password. */
33915
- async signInMethods(userId) {
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) {
33916
33934
  const accounts = await this.store.findMany("account", { userId });
33917
- const methods = accounts.map((account) => String(account.key).split(":")[0]);
33918
- return SIGN_IN_METHODS.filter((method) => methods.includes(method));
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));
33919
33937
  }
33920
33938
  async findAccountByKey(key) {
33921
33939
  return await this.store.findOne("account", { key });
33922
33940
  }
33923
33941
  /**
33924
- * Creates a user with their first account. Resolves to null if the e-mail address or the account key is
33925
- * taken. `account.key` is `password:<userId>` for a password (its hash as `secret`), or e.g. `google:<sub>`.
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.
33926
33946
  */
33927
33947
  async createUser(data) {
33928
33948
  const user = {
@@ -33938,6 +33958,7 @@ var Users = class {
33938
33958
  createdAt: Date.now()
33939
33959
  };
33940
33960
  if (!await this.store.create("user", user)) return null;
33961
+ if (!data.account) return user;
33941
33962
  const account = typeof data.account === "function" ? data.account(user.id) : data.account;
33942
33963
  if (!await this.linkAccount(user.id, account)) {
33943
33964
  await this.store.delete("user", { id: user.id });
@@ -33959,6 +33980,16 @@ var Users = class {
33959
33980
  async deleteAccount(id) {
33960
33981
  await this.store.delete("account", { id });
33961
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
+ }
33962
33993
  /** Adds a sign-in method to a user. Resolves to null if the account key is taken. */
33963
33994
  async linkAccount(userId, account) {
33964
33995
  const record = {
@@ -33969,10 +34000,70 @@ var Users = class {
33969
34000
  failedAttempts: 0,
33970
34001
  lockedUntil: 0,
33971
34002
  email: account.email ?? null,
34003
+ lastUsedStep: account.lastUsedStep ?? 0,
34004
+ setupId: account.setupId ?? null,
33972
34005
  createdAt: Date.now()
33973
34006
  };
33974
34007
  return await this.store.create("account", record) ? record : null;
33975
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
+ }
33976
34067
  /**
33977
34068
  * Deletes the user and everything stored for them. Resolves to false if there is no such user (any more).
33978
34069
  */
@@ -33983,6 +34074,7 @@ var Users = class {
33983
34074
  try {
33984
34075
  await this.store.delete("account", { userId: user.id });
33985
34076
  await this.store.delete("verification", { userId: user.id });
34077
+ await this.store.delete("recoveryCode", { userId: user.id });
33986
34078
  } catch (e6) {
33987
34079
  console.error(`[madauth] Deleted user ${user.id}, but not all of their records: ${e6.message}`);
33988
34080
  }
@@ -34026,6 +34118,11 @@ var Users = class {
34026
34118
  await this.store.create("verification", record);
34027
34119
  return { token, code };
34028
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
+ }
34029
34126
  /** Uses up the verification a link token belongs to. Resolves to its user ID, or null if invalid, expired or used. */
34030
34127
  async consumeLinkToken(token, purpose) {
34031
34128
  const id = hashLinkToken(token);
@@ -34072,6 +34169,215 @@ var Users = class {
34072
34169
  }
34073
34170
  };
34074
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
+
34075
34381
  // src/routes/google.ts
34076
34382
  var NONCE_COOKIE = "madauth_nonce";
34077
34383
  var NONCE_TYP = "madauth-nonce+jwt";
@@ -34082,6 +34388,7 @@ var OAUTH_TTL = 10 * 60;
34082
34388
  var GOOGLE_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
34083
34389
  var GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
34084
34390
  var REDIRECT_ERROR_PARAM = "madauth_error";
34391
+ var REDIRECT_NEXT_PARAM = "madauth_next";
34085
34392
  async function pkceChallenge(verifier) {
34086
34393
  const digest2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
34087
34394
  return base64url_exports.encode(new Uint8Array(digest2));
@@ -34093,7 +34400,7 @@ function googleRoutes(app, ctx, google) {
34093
34400
  const resolveUser = async (profile, locale) => {
34094
34401
  const key = googleAccountKey(profile.sub);
34095
34402
  const emailNormalized = normalizeEmail(profile.email);
34096
- const signedIn = (user) => ({ ok: true, user: toMadauthUser(user, profile), sv: user.sessionVersion });
34403
+ const signedIn = (user) => ({ ok: true, user });
34097
34404
  const account = await users.findAccountByKey(key);
34098
34405
  if (account) {
34099
34406
  const user = await users.findById(account.userId);
@@ -34106,6 +34413,11 @@ function googleRoutes(app, ctx, google) {
34106
34413
  for (let attempt = 0; attempt < 2; attempt++) {
34107
34414
  const existing = await users.findByEmail(emailNormalized);
34108
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
+ }
34109
34421
  await users.verifyByProvider(existing);
34110
34422
  if (!await users.linkAccount(existing.id, { key, email: profile.email })) {
34111
34423
  const linked = await users.findAccountByKey(key);
@@ -34137,7 +34449,7 @@ function googleRoutes(app, ctx, google) {
34137
34449
  };
34138
34450
  const disabled = (c6) => c6.json({ error: "method_disabled", message: "Google sign-in is switched off." }, 403);
34139
34451
  app.post("/auth/google/nonce", async (c6) => {
34140
- if (!await ctx.enabled("google")) return disabled(c6);
34452
+ if (!await ctx.enabled(c6, "google")) return disabled(c6);
34141
34453
  const nonce = randomString();
34142
34454
  const token = await signToken(await keys, issuer, NONCE_TYP, { nonce }, NONCE_TTL);
34143
34455
  setCookie(c6, NONCE_COOKIE, token, {
@@ -34150,16 +34462,16 @@ function googleRoutes(app, ctx, google) {
34150
34462
  return c6.json({ nonce });
34151
34463
  });
34152
34464
  app.post("/auth/google/verify", async (c6) => {
34153
- if (!await ctx.enabled("google")) return disabled(c6);
34154
- const body = await c6.req.json().catch(() => ({}));
34155
- if (typeof body.credential !== "string" || !body.credential) {
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) {
34156
34468
  return c6.json({ error: "verification_failed", message: "credential is missing" }, 400);
34157
34469
  }
34158
34470
  const nonceClaims = await readToken(await keys, issuer, NONCE_TYP, getCookie(c6, NONCE_COOKIE));
34159
34471
  if (!nonceClaims) {
34160
34472
  return c6.json({ error: "verification_failed", message: "nonce cookie missing or expired" }, 401);
34161
34473
  }
34162
- const result = await verifyGoogleIdToken(body.credential, {
34474
+ const result = await verifyGoogleIdToken(body2.credential, {
34163
34475
  clientId: google.clientId,
34164
34476
  nonce: nonceClaims.nonce,
34165
34477
  keys: config.jwksResolver
@@ -34168,17 +34480,16 @@ function googleRoutes(app, ctx, google) {
34168
34480
  return c6.json({ error: result.error, message: result.reason }, result.error === "email_unverified" ? 403 : 401);
34169
34481
  }
34170
34482
  deleteCookie(c6, NONCE_COOKIE, { path: "/auth/google", secure });
34171
- const resolved = await resolveUser(result.profile, localeOf(body.locale));
34483
+ const resolved = await resolveUser(result.profile, localeOf(body2.locale));
34172
34484
  if (!resolved.ok) {
34485
+ if (resolved.error === "other_method") return c6.json({ error: resolved.error, message: resolved.message, methods: resolved.methods }, 403);
34173
34486
  return c6.json({ error: resolved.error, message: resolved.message }, resolved.error === "signup_rejected" ? 403 : 503);
34174
34487
  }
34175
- const user = await ctx.startSession(c6, resolved.user, ["google"], { sv: resolved.sv });
34176
- await ctx.emit("user.signed_in", { user, method: "google" });
34177
- return c6.json({ user });
34488
+ return signInAnswer(c6, await ctx.secondStep(c6, resolved.user, ["google"], "google", result.profile));
34178
34489
  });
34179
34490
  const requireCodeFlow = async (c6, next) => {
34180
34491
  if (!google.clientSecret) return c6.json({ error: "not_found" }, 404);
34181
- if (!await ctx.enabled("google")) return disabled(c6);
34492
+ if (!await ctx.enabled(c6, "google")) return disabled(c6);
34182
34493
  await next();
34183
34494
  };
34184
34495
  app.get("/auth/google/start", requireCodeFlow, async (c6) => {
@@ -34216,9 +34527,10 @@ function googleRoutes(app, ctx, google) {
34216
34527
  const oauth = await readToken(await keys, issuer, OAUTH_TYP, getCookie(c6, OAUTH_COOKIE));
34217
34528
  if (!oauth) return c6.text("Sign-in expired or was started in another browser. Please try again.", 400);
34218
34529
  deleteCookie(c6, OAUTH_COOKIE, { path: "/auth/google", secure });
34219
- const back = (error) => {
34530
+ const back = (error2, next) => {
34220
34531
  const url = new URL(oauth.returnTo);
34221
- if (error) url.hash = `${REDIRECT_ERROR_PARAM}=${error}`;
34532
+ if (error2) url.hash = `${REDIRECT_ERROR_PARAM}=${error2}`;
34533
+ if (next) url.hash = `${REDIRECT_NEXT_PARAM}=${next}`;
34222
34534
  return c6.redirect(url.href, 302);
34223
34535
  };
34224
34536
  if (c6.req.query("error")) return back(c6.req.query("error") === "access_denied" ? "cancelled" : "verification_failed");
@@ -34252,85 +34564,38 @@ function googleRoutes(app, ctx, google) {
34252
34564
  if (!result.ok) return back(result.error);
34253
34565
  const resolved = await resolveUser(result.profile, typeof oauth.locale === "string" ? oauth.locale : void 0);
34254
34566
  if (!resolved.ok) return back(resolved.error);
34255
- const user = await ctx.startSession(c6, resolved.user, ["google"], { sv: resolved.sv });
34256
- await ctx.emit("user.signed_in", { user, method: "google" });
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);
34257
34570
  return back();
34258
34571
  });
34259
34572
  }
34260
34573
 
34261
- // src/routes/password.ts
34574
+ // src/routes/lockout.ts
34262
34575
  var FREE_ATTEMPTS = 5;
34263
34576
  var MAX_LOCK_MS = 15 * 60 * 1e3;
34264
- var MAIL_INTERVAL_MS = 60 * 1e3;
34265
- var VERIFY_TTL_MS = 24 * 60 * 60 * 1e3;
34266
- var RESET_TTL_MS = 30 * 60 * 1e3;
34267
- var VERIFY_LINK_PARAM = "madauth_verify";
34268
- var RESET_LINK_PARAM = "madauth_reset";
34269
- var MAX_NAME_LENGTH = 100;
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
34270
34592
  function passwordRoutes(app, ctx, password, users, webhook) {
34271
34593
  const { minLength } = password;
34272
- const disabled = (c6) => c6.json({ error: "method_disabled", message: "E-mail & password sign-in is switched off." }, 403);
34594
+ const emails = createEmails(ctx, users, webhook);
34273
34595
  app.use("/auth/password/*", async (c6, next) => {
34274
- if (!await ctx.enabled("password")) return disabled(c6);
34596
+ if (!await ctx.enabled(c6, "password")) return error(c6, 403, "method_disabled", "E-mail & password sign-in is switched off.");
34275
34597
  await next();
34276
34598
  });
34277
- const body = async (c6) => {
34278
- const data = await c6.req.json().catch(() => null);
34279
- return data && typeof data === "object" ? data : {};
34280
- };
34281
- const error = (c6, status, code, message2) => c6.json({ error: code, message: message2 }, status);
34282
- const unavailable = (c6) => error(c6, 503, "temporarily_unavailable", "E-mails can not be sent right now. Please try again later.");
34283
- const accepted = (c6) => c6.json({}, 202);
34284
- const redirectTarget = (c6, value) => {
34285
- const url = ctx.allowedUrl(value);
34286
- return url ?? error(c6, 400, "invalid_options", "redirectTo must be an absolute URL whose origin is in ALLOWED_ORIGINS.");
34287
- };
34288
- const sendEmail = async (user, type, data) => {
34289
- if (Date.now() - user.lastMailAt < MAIL_INTERVAL_MS) return true;
34290
- const result = await webhook.call(type, { to: user.email, user: { id: user.id, name: user.name ?? void 0 }, ...data }, WEBHOOK_TIMEOUT_MS);
34291
- if (!result.ok) {
34292
- console.error(`[madauth] Webhook "${type}" for ${user.email} failed: ${result.reason}`);
34293
- return false;
34294
- }
34295
- await users.updateUser(user.id, { lastMailAt: Date.now() });
34296
- return true;
34297
- };
34298
- const sendLinkEmail = async (user, purpose, target, locale) => {
34299
- if (Date.now() - user.lastMailAt < MAIL_INTERVAL_MS) return true;
34300
- const ttl = purpose === "verify" ? VERIFY_TTL_MS : RESET_TTL_MS;
34301
- const { token, code } = await users.issueVerification(user.id, purpose, ttl);
34302
- const link = new URL(target);
34303
- link.hash = `${purpose === "verify" ? VERIFY_LINK_PARAM : RESET_LINK_PARAM}=${token}`;
34304
- return sendEmail(user, purpose === "verify" ? "email.verify" : "email.reset", {
34305
- link: link.href,
34306
- code,
34307
- expiresAt: Date.now() + ttl,
34308
- site: target.host,
34309
- locale
34310
- });
34311
- };
34312
- const sendAccountEmail = async (user, type, target, locale) => {
34313
- if (!webhook.wants(type)) return true;
34314
- const methods = await users.signInMethods(user.id);
34315
- return sendEmail(user, type, { link: target.href, site: target.host, locale, methods });
34316
- };
34317
- const consume = async (data, purpose) => {
34318
- if (typeof data.token === "string" && data.token) {
34319
- return { userId: await users.consumeLinkToken(data.token, purpose), via: "link" };
34320
- }
34321
- if (isValidEmail(data.email) && typeof data.code === "string") {
34322
- const user = await users.findByEmail(normalizeEmail(data.email));
34323
- if (!user) return { userId: null, via: "code" };
34324
- return { ...await users.consumeCode(user, purpose, data.code.replace(/\s/g, "")), via: "code" };
34325
- }
34326
- return { userId: null, via: "code" };
34327
- };
34328
- const invalidVerification = (c6, { via, locked }) => {
34329
- if (locked) {
34330
- return error(c6, 429, "codes_locked", "Too many wrong codes. Please ask for a new e-mail and use the link in it.");
34331
- }
34332
- 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.");
34333
- };
34334
34599
  app.post("/auth/password/signin", async (c6) => {
34335
34600
  const data = await body(c6);
34336
34601
  const wrong = () => error(c6, 401, "invalid_credentials", "E-mail or password is wrong.");
@@ -34342,16 +34607,8 @@ function passwordRoutes(app, ctx, password, users, webhook) {
34342
34607
  await verifyAgainstDummy(pw);
34343
34608
  return wrong();
34344
34609
  }
34345
- const now = Date.now();
34346
- if (account.lockedUntil > now) {
34347
- const seconds = Math.ceil((account.lockedUntil - now) / 1e3);
34348
- return error(c6, 429, "too_many_attempts", `Too many failed attempts. Try again in ${seconds} seconds.`);
34349
- }
34350
- const failed = account.failedAttempts + 1;
34351
- const lockedUntil = failed >= FREE_ATTEMPTS ? now + Math.min(2 ** (failed - FREE_ATTEMPTS) * 1e3, MAX_LOCK_MS) : 0;
34352
- if (!await users.countAttempt(account, { failedAttempts: failed, lockedUntil })) {
34353
- return error(c6, 429, "too_many_attempts", "Too many attempts at once. Please try again.");
34354
- }
34610
+ const refused = await countSignInAttempt(c6, users, account);
34611
+ if (refused) return refused;
34355
34612
  const { ok, needsRehash } = await verifyPassword(pw, account.secret);
34356
34613
  if (!ok) return wrong();
34357
34614
  await users.updateAccount(account.id, {
@@ -34362,26 +34619,24 @@ function passwordRoutes(app, ctx, password, users, webhook) {
34362
34619
  if (!user.emailVerified) {
34363
34620
  return error(c6, 403, "email_unverified", "Please confirm your e-mail address first. We can send the e-mail again.");
34364
34621
  }
34365
- const result = await ctx.startSession(c6, toMadauthUser(user), ["pwd"], { sv: user.sessionVersion });
34366
- await ctx.emit("user.signed_in", { user: result, method: "password" });
34367
- return c6.json({ user: result });
34622
+ return signInAnswer(c6, await ctx.secondStep(c6, user, ["pwd"], "password"));
34368
34623
  });
34369
34624
  app.post("/auth/password/signup", async (c6) => {
34370
34625
  const data = await body(c6);
34371
- const target = redirectTarget(c6, data.redirectTo);
34626
+ const target = redirectTarget(ctx, c6, data.redirectTo);
34372
34627
  if (target instanceof Response) return target;
34373
34628
  if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
34374
34629
  const policy = checkPasswordPolicy(data.password, minLength);
34375
34630
  if (policy) return error(c6, 400, "weak_password", policy);
34376
- const name = typeof data.name === "string" && data.name.trim() ? data.name.trim().slice(0, MAX_NAME_LENGTH) : null;
34631
+ const name = nameOf(data.name);
34377
34632
  const email = data.email.trim();
34378
34633
  const locale = localeOf(data.locale);
34379
34634
  const check2 = await ctx.checkSignup({ email, name: name ?? void 0, locale, method: "password" });
34380
34635
  if (!check2.ok) return check2.reason === "rejected" ? error(c6, 403, "signup_rejected", check2.message) : unavailable(c6);
34381
34636
  const passwordHash = await hashPassword(data.password);
34382
34637
  const existing = await users.findByEmail(normalizeEmail(email));
34383
- if (existing?.emailVerified) {
34384
- return await sendAccountEmail(existing, "email.already_registered", target, locale) ? 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);
34385
34640
  }
34386
34641
  if (existing) {
34387
34642
  if (Date.now() - existing.lastMailAt < MAIL_INTERVAL_MS) return accepted(c6);
@@ -34389,7 +34644,7 @@ function passwordRoutes(app, ctx, password, users, webhook) {
34389
34644
  if (account) await users.updateAccount(account.id, { secret: passwordHash, failedAttempts: 0, lockedUntil: 0 });
34390
34645
  else await users.linkAccount(existing.id, { key: passwordAccountKey(existing.id), secret: passwordHash });
34391
34646
  await users.updateUser(existing.id, { name });
34392
- 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);
34393
34648
  }
34394
34649
  const user = await users.createUser({
34395
34650
  email,
@@ -34400,41 +34655,19 @@ function passwordRoutes(app, ctx, password, users, webhook) {
34400
34655
  });
34401
34656
  if (!user) return accepted(c6);
34402
34657
  await ctx.emit("user.created", { user: toMadauthUser(user), method: "password" });
34403
- return await sendLinkEmail(user, "verify", target, locale) ? accepted(c6) : unavailable(c6);
34404
- });
34405
- app.post("/auth/password/send-verification", async (c6) => {
34406
- const data = await body(c6);
34407
- const target = redirectTarget(c6, data.redirectTo);
34408
- if (target instanceof Response) return target;
34409
- if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
34410
- const user = await users.findByEmail(normalizeEmail(data.email));
34411
- if (user && !user.emailVerified && await users.passwordAccount(user.id)) {
34412
- if (!await sendLinkEmail(user, "verify", target, localeOf(data.locale))) return unavailable(c6);
34413
- }
34414
- return accepted(c6);
34415
- });
34416
- app.post("/auth/password/verify-email", async (c6) => {
34417
- const consumed = await consume(await body(c6), "verify");
34418
- const { userId, via } = consumed;
34419
- const user = userId ? await users.findById(userId) : null;
34420
- if (!user) return invalidVerification(c6, consumed);
34421
- await users.updateUser(user.id, { emailVerified: true, wrongCodes: 0 });
34422
- const result = await ctx.startSession(c6, toMadauthUser(user), ["pwd"], { sv: user.sessionVersion });
34423
- await ctx.emit("email.verified", { user: result, via });
34424
- await ctx.emit("user.signed_in", { user: result, method: "password" });
34425
- return c6.json({ user: result });
34658
+ return await emails.sendLinkEmail(user, "verify", target, locale) ? accepted(c6) : unavailable(c6);
34426
34659
  });
34427
34660
  app.post("/auth/password/send-reset", async (c6) => {
34428
34661
  const data = await body(c6);
34429
- const target = redirectTarget(c6, data.redirectTo);
34662
+ const target = redirectTarget(ctx, c6, data.redirectTo);
34430
34663
  if (target instanceof Response) return target;
34431
34664
  if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
34432
34665
  const user = await users.findByEmail(normalizeEmail(data.email));
34433
34666
  const locale = localeOf(data.locale);
34434
34667
  if (user && await users.passwordAccount(user.id)) {
34435
- if (!await sendLinkEmail(user, "reset", target, locale)) return unavailable(c6);
34668
+ if (!await emails.sendLinkEmail(user, "reset", target, locale)) return unavailable(c6);
34436
34669
  } else if (user) {
34437
- if (!await sendAccountEmail(user, "email.no_password", target, locale)) return unavailable(c6);
34670
+ if (!await emails.sendAccountEmail(user, "email.no_password", target, locale)) return unavailable(c6);
34438
34671
  }
34439
34672
  return accepted(c6);
34440
34673
  });
@@ -34442,20 +34675,359 @@ function passwordRoutes(app, ctx, password, users, webhook) {
34442
34675
  const data = await body(c6);
34443
34676
  const policy = checkPasswordPolicy(data.password, minLength);
34444
34677
  if (policy) return error(c6, 400, "weak_password", policy);
34445
- const consumed = await consume(data, "reset");
34678
+ const consumed = await emails.consume(data, "reset");
34446
34679
  const user = consumed.userId ? await users.findById(consumed.userId) : null;
34447
34680
  const account = user ? await users.passwordAccount(user.id) : null;
34448
- if (!user || !account) return invalidVerification(c6, consumed);
34681
+ if (!user || !account) return emails.invalidVerification(c6, consumed);
34449
34682
  const secret = await hashPassword(data.password);
34450
- if (!await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 })) return invalidVerification(c6, consumed);
34683
+ if (!await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 })) return emails.invalidVerification(c6, consumed);
34451
34684
  const sessionVersion = user.sessionVersion + 1;
34452
34685
  await users.updateUser(user.id, { emailVerified: true, sessionVersion, wrongCodes: 0 });
34453
34686
  await users.clearVerifications(user.id);
34454
- const result = await ctx.startSession(c6, toMadauthUser(user), ["pwd"], { sv: sessionVersion });
34455
- await ctx.emit("email.password_reset", { user: result });
34456
- await ctx.emit("user.signed_in", { user: result, method: "password" });
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"));
34690
+ });
34691
+ }
34692
+
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
+ }
34717
+ }
34718
+ if (bits > 0) out += BASE32[value << 5 - bits & 31];
34719
+ return out;
34720
+ }
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;
34776
+ }
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();
34833
+ }
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.");
34846
+ }
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
+ }
34898
+ }
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" });
34457
34963
  return c6.json({ user: result });
34458
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
+ });
35030
+ }
34459
35031
  }
34460
35032
 
34461
35033
  // src/app.ts
@@ -34465,9 +35037,14 @@ function createApp(config) {
34465
35037
  const keys = importSigningKeys(config.signingKey);
34466
35038
  const secure = issuer.startsWith("https:");
34467
35039
  const isAllowedOrigin = (origin) => !!origin && allowedOrigins.includes(origin);
34468
- const users = new Users(config.store, deriveCodeKey(config.signingKey.d));
34469
- const settings = new Settings(config.store);
34470
- const available = (method) => method === "google" ? !!config.google : !!config.password;
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 };
34471
35048
  const issueSession = async (c6, user, amr, extra) => {
34472
35049
  const signingKeys = await keys;
34473
35050
  const [sessionToken, renewalToken] = await Promise.all([
@@ -34502,6 +35079,29 @@ function createApp(config) {
34502
35079
  deleteCookie(c6, RENEWAL_COOKIE, { path: "/auth", secure });
34503
35080
  deleteCookie(c6, EXPIRY_COOKIE, { path: "/", domain: cookieDomain, secure });
34504
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();
34505
35105
  const ctx = {
34506
35106
  config,
34507
35107
  keys,
@@ -34519,14 +35119,42 @@ function createApp(config) {
34519
35119
  return url;
34520
35120
  },
34521
35121
  async startSession(c6, user, amr, extra) {
35122
+ clearChallenge(c6, secure);
34522
35123
  await issueSession(c6, user, amr, extra);
34523
- return user;
35124
+ return { ...user, amr };
34524
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 };
35141
+ },
35142
+ currentUser: currentSession,
34525
35143
  users,
34526
35144
  settings,
34527
35145
  webhook: config.webhook ? createWebhookClient(config.webhook, config.webhookFetch) : void 0,
34528
- async enabled(method) {
34529
- return isOn(method, available, await settings.methods());
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));
34530
35158
  },
34531
35159
  async checkSignup(data) {
34532
35160
  if (!this.webhook?.wants("signup.before")) return { ok: true };
@@ -34544,7 +35172,9 @@ function createApp(config) {
34544
35172
  },
34545
35173
  async emit(type, data) {
34546
35174
  if (!this.webhook?.wants(type)) return;
34547
- const result = await this.webhook.call(type, data, EVENT_TIMEOUT_MS);
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);
34548
35178
  if (!result.ok) console.error(`[madauth] Webhook "${type}" failed: ${result.reason}`);
34549
35179
  }
34550
35180
  };
@@ -34572,37 +35202,20 @@ function createApp(config) {
34572
35202
  return c6.json({ keys: [publicJwk] });
34573
35203
  });
34574
35204
  app.get("/auth/config", async (c6) => {
34575
- const methods = await settings.methods();
34576
- const on = (method) => isOn(method, available, methods);
35205
+ const current = await ctx.methods(c6);
35206
+ const on = (method) => isOn(method, configured, current);
34577
35207
  return c6.json({
34578
- google: on("google") ? { clientId: config.google.clientId, codeFlow: !!config.google.clientSecret } : null,
34579
- 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 }
34580
35213
  });
34581
35214
  });
35215
+ if (emailVerification && ctx.webhook) emailRoutes(app, ctx, users, ctx.webhook);
34582
35216
  if (config.google) googleRoutes(app, ctx, config.google);
34583
35217
  if (config.password && ctx.webhook) passwordRoutes(app, ctx, config.password, users, ctx.webhook);
34584
- const tokenClaims = async (c6, cookie, typ) => readToken(await keys, issuer, typ, getCookie(c6, cookie));
34585
- const checked = async (claims) => {
34586
- const stored = await users.findById(claims.sub);
34587
- if (!stored || stored.sessionVersion !== claims.sv) return null;
34588
- const { claims: _, ...user } = userFromClaims(claims);
34589
- const current = claimsFromJson(stored.claims);
34590
- return Object.keys(current).length ? { ...user, claims: current } : user;
34591
- };
34592
- const currentSession = async (c6) => {
34593
- const session = await tokenClaims(c6, SESSION_COOKIE, SESSION_TYP);
34594
- const user = session ? await checked(session) : null;
34595
- const upToDate = session && user && sameClaims(user.claims, session.claims) && Date.now() / 1e3 - session.iat <= sessionTtlSeconds / 2;
34596
- if (upToDate) return user;
34597
- const renewal = await tokenClaims(c6, RENEWAL_COOKIE, RENEWAL_TYP);
34598
- const sameSignIn = session && user && renewal && renewal.sub === session.sub && renewal.sv === session.sv && renewal.email === session.email;
34599
- const renewed = sameSignIn ? user : renewal ? await checked(renewal) : null;
34600
- if (renewal && renewed) {
34601
- await issueSession(c6, renewed, renewal.amr, { sv: renewal.sv });
34602
- return renewed;
34603
- }
34604
- return user;
34605
- };
35218
+ totpRoutes(app, ctx, totpKeys);
34606
35219
  app.get("/auth/session", async (c6) => {
34607
35220
  const user = await currentSession(c6);
34608
35221
  if (!user) {
@@ -34613,11 +35226,23 @@ function createApp(config) {
34613
35226
  });
34614
35227
  app.post("/auth/logout", (c6) => {
34615
35228
  clearSession(c6);
35229
+ clearChallenge(c6, secure);
34616
35230
  return c6.body(null, 204);
34617
35231
  });
34618
35232
  app.post("/auth/account/delete", async (c6) => {
34619
35233
  const user = await currentSession(c6);
34620
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
+ }
34621
35246
  await users.deleteById(user.id);
34622
35247
  clearSession(c6);
34623
35248
  await ctx.emit("user.deleted", { user });
@@ -34633,10 +35258,6 @@ function createApp(config) {
34633
35258
  }
34634
35259
  return { email };
34635
35260
  };
34636
- const body = async (c6) => {
34637
- const data = await c6.req.json().catch(() => null);
34638
- return data && typeof data === "object" ? data : {};
34639
- };
34640
35261
  const invalidEmail = (c6) => c6.json({ error: "invalid_email", message: "This is not a valid e-mail address." }, 400);
34641
35262
  const noUser = (c6) => c6.json({ error: "user_not_found", message: "No user has this e-mail address." }, 404);
34642
35263
  app.post("/auth/admin/claims/get", async (c6) => {
@@ -34670,9 +35291,16 @@ function createApp(config) {
34670
35291
  await ctx.emit("user.claims_changed", { userId: user.id, email, claims, by: caller.email });
34671
35292
  return c6.json({ email, userId: user.id, claims });
34672
35293
  });
34673
- const settingsAnswer = (methods) => ({
35294
+ const settingsAnswer = (methods2) => ({
34674
35295
  methods: Object.fromEntries(
34675
- SIGN_IN_METHODS.map((method) => [method, { available: available(method), enabled: isOn(method, available, methods) }])
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
+ ])
34676
35304
  )
34677
35305
  });
34678
35306
  app.post("/auth/admin/settings/get", async (c6) => {
@@ -34685,14 +35313,26 @@ function createApp(config) {
34685
35313
  if ("answer" in caller) return caller.answer;
34686
35314
  const data = await body(c6);
34687
35315
  const invalid2 = (message2) => c6.json({ error: "invalid_settings", message: message2 }, 400);
34688
- const changes = parseMethodSettings(data.methods);
34689
- if (!changes) return invalid2(`methods must be an object with true or false for ${SIGN_IN_METHODS.join(" and/or ")}.`);
34690
- const methods = await settings.changeMethods((current) => {
34691
- const next = { ...current, ...changes };
34692
- return anyOn(available, next) ? next : null;
34693
- }, caller.email);
34694
- if (!methods) return invalid2("At least one sign-in method must stay on.");
34695
- return c6.json(settingsAnswer(methods));
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 });
34696
35336
  });
34697
35337
  return app;
34698
35338
  }