@spfn/auth 0.3.0-beta.22 → 0.3.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +97 -2
- package/dist/errors.js +63 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +35 -2
- package/dist/index.js +61 -1
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-B7N8gux0.d.ts → machine-principals-CaEFq61K.d.ts} +397 -5
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +521 -9
- package/dist/server.js +1801 -839
- package/dist/server.js.map +1 -1
- package/migrations/20260918162828_handy_titania/migration.sql +35 -0
- package/migrations/20260918162828_handy_titania/snapshot.json +5948 -0
- package/package.json +1 -1
|
@@ -148,6 +148,14 @@ interface LogoutParams {
|
|
|
148
148
|
}
|
|
149
149
|
interface ChangePasswordParams {
|
|
150
150
|
userId: number;
|
|
151
|
+
/**
|
|
152
|
+
* The device key this request is signed with.
|
|
153
|
+
*
|
|
154
|
+
* Only read to measure the second-factor window of an enrolled account —
|
|
155
|
+
* an account with nothing enrolled is answered exactly as before, so this
|
|
156
|
+
* adds no refusal for anybody who has not opted in.
|
|
157
|
+
*/
|
|
158
|
+
keyId: string;
|
|
151
159
|
currentPassword?: string;
|
|
152
160
|
newPassword: string;
|
|
153
161
|
passwordHash?: string;
|
|
@@ -166,6 +174,11 @@ declare function loginService(params: LoginParams): Promise<LoginResult>;
|
|
|
166
174
|
declare function logoutService(params: LogoutParams): Promise<void>;
|
|
167
175
|
/**
|
|
168
176
|
* Change user password
|
|
177
|
+
*
|
|
178
|
+
* An enrolled account steps up first (#95): a stolen session must not be able
|
|
179
|
+
* to take the account over by setting a new password. An unenrolled account is
|
|
180
|
+
* unaffected — including the OAuth-only account with no password and a key
|
|
181
|
+
* older than ten minutes, which still sets a first password and gets a 200.
|
|
169
182
|
*/
|
|
170
183
|
declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
|
|
171
184
|
|
|
@@ -822,6 +835,21 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
822
835
|
identity: undefined;
|
|
823
836
|
generated: undefined;
|
|
824
837
|
}>;
|
|
838
|
+
secondFactor: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
|
|
839
|
+
name: string;
|
|
840
|
+
tableName: "passkeys";
|
|
841
|
+
dataType: "boolean";
|
|
842
|
+
data: boolean;
|
|
843
|
+
driverParam: boolean;
|
|
844
|
+
notNull: true;
|
|
845
|
+
hasDefault: true;
|
|
846
|
+
isPrimaryKey: false;
|
|
847
|
+
isAutoincrement: false;
|
|
848
|
+
hasRuntimeDefault: false;
|
|
849
|
+
enumValues: undefined;
|
|
850
|
+
identity: undefined;
|
|
851
|
+
generated: undefined;
|
|
852
|
+
}>;
|
|
825
853
|
lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
|
|
826
854
|
name: string;
|
|
827
855
|
tableName: "passkeys";
|
|
@@ -953,6 +981,11 @@ interface StartPasskeyEnrollmentParams {
|
|
|
953
981
|
/**
|
|
954
982
|
* Step 1 of enrollment — options for `navigator.credentials.create()`.
|
|
955
983
|
*
|
|
984
|
+
* An enrolled account steps up first, and every account then meets the
|
|
985
|
+
* recent-authentication rule this route has always had (#95). The order is what
|
|
986
|
+
* makes the two independent: `assertStepUp` is a no-op for an unenrolled
|
|
987
|
+
* account, so the answer such a caller gets is byte-for-byte today's.
|
|
988
|
+
*
|
|
956
989
|
* `excludeCredentials` lists the caller's **live** passkeys only, so the
|
|
957
990
|
* authenticator quietly refuses one already enrolled here. Revoked ones are left
|
|
958
991
|
* out on purpose: they must not be re-enrolled either, and the check that
|
|
@@ -1040,8 +1073,10 @@ interface RevokePasskeyParams {
|
|
|
1040
1073
|
/**
|
|
1041
1074
|
* Retire a passkey.
|
|
1042
1075
|
*
|
|
1043
|
-
* Gated on
|
|
1044
|
-
*
|
|
1076
|
+
* Gated on the second factor for an enrolled account, and then — for every
|
|
1077
|
+
* account, enrolled or not — on recent authentication, because someone who
|
|
1078
|
+
* walked up to an unlocked laptop should not be able to strip the account's
|
|
1079
|
+
* credentials; and on the
|
|
1045
1080
|
* last-recovery-credential guard, because there is no undo for the state that
|
|
1046
1081
|
* would leave.
|
|
1047
1082
|
*
|
|
@@ -1056,6 +1091,318 @@ declare function revokePasskeyService(params: RevokePasskeyParams): Promise<{
|
|
|
1056
1091
|
passkeyId: string;
|
|
1057
1092
|
}>;
|
|
1058
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* @spfn/auth - Second-Factor Verification Entity
|
|
1096
|
+
*
|
|
1097
|
+
* When each device last proved the account's second factor. One row per device
|
|
1098
|
+
* key, which is what makes the step-up window mean "this device, recently"
|
|
1099
|
+
* rather than "somebody, somewhere, recently".
|
|
1100
|
+
*
|
|
1101
|
+
* `key_id` is the primary key and a foreign key onto `user_public_keys.key_id`,
|
|
1102
|
+
* so the row dies with the device it belongs to — a revoked key cannot leave a
|
|
1103
|
+
* verification behind for the next key that reuses its id, and deleting an
|
|
1104
|
+
* account takes these with it. A rotation is the one moment the row moves
|
|
1105
|
+
* rather than dying: rotating is already proof of the same device, so
|
|
1106
|
+
* `rotateKeyService` and the login rotation carry the verification across, or
|
|
1107
|
+
* the window would silently expire every time the proxy rotated a key.
|
|
1108
|
+
*/
|
|
1109
|
+
/**
|
|
1110
|
+
* Which credential satisfied the step-up.
|
|
1111
|
+
*
|
|
1112
|
+
* Recorded for the owner-facing audit an app may build on it, and for support:
|
|
1113
|
+
* "recovery" is the one that should be rare, and an account stepping up with
|
|
1114
|
+
* recovery codes repeatedly has lost its authenticator.
|
|
1115
|
+
*/
|
|
1116
|
+
declare const MFA_VERIFICATION_METHODS: readonly ["totp", "recovery", "passkey"];
|
|
1117
|
+
type MfaVerificationMethod = typeof MFA_VERIFICATION_METHODS[number];
|
|
1118
|
+
declare const mfaVerifications: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
1119
|
+
name: "mfa_verifications";
|
|
1120
|
+
schema: string;
|
|
1121
|
+
columns: {
|
|
1122
|
+
keyId: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
|
|
1123
|
+
name: string;
|
|
1124
|
+
tableName: "mfa_verifications";
|
|
1125
|
+
dataType: "string";
|
|
1126
|
+
data: string;
|
|
1127
|
+
driverParam: string;
|
|
1128
|
+
notNull: true;
|
|
1129
|
+
hasDefault: false;
|
|
1130
|
+
isPrimaryKey: false;
|
|
1131
|
+
isAutoincrement: false;
|
|
1132
|
+
hasRuntimeDefault: false;
|
|
1133
|
+
enumValues: undefined;
|
|
1134
|
+
identity: undefined;
|
|
1135
|
+
generated: undefined;
|
|
1136
|
+
}>;
|
|
1137
|
+
userId: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
|
|
1138
|
+
name: string;
|
|
1139
|
+
tableName: "mfa_verifications";
|
|
1140
|
+
dataType: "number int53";
|
|
1141
|
+
data: number;
|
|
1142
|
+
driverParam: string | number;
|
|
1143
|
+
notNull: true;
|
|
1144
|
+
hasDefault: false;
|
|
1145
|
+
isPrimaryKey: false;
|
|
1146
|
+
isAutoincrement: false;
|
|
1147
|
+
hasRuntimeDefault: false;
|
|
1148
|
+
enumValues: undefined;
|
|
1149
|
+
identity: undefined;
|
|
1150
|
+
generated: undefined;
|
|
1151
|
+
}>;
|
|
1152
|
+
method: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["totp", "recovery", "passkey"] & [string, ...string[]]>>, {
|
|
1153
|
+
name: string;
|
|
1154
|
+
tableName: "mfa_verifications";
|
|
1155
|
+
dataType: "string enum";
|
|
1156
|
+
data: "totp" | "recovery" | "passkey";
|
|
1157
|
+
driverParam: string;
|
|
1158
|
+
notNull: true;
|
|
1159
|
+
hasDefault: false;
|
|
1160
|
+
isPrimaryKey: false;
|
|
1161
|
+
isAutoincrement: false;
|
|
1162
|
+
hasRuntimeDefault: false;
|
|
1163
|
+
enumValues: ["totp", "recovery", "passkey"] & [string, ...string[]];
|
|
1164
|
+
identity: undefined;
|
|
1165
|
+
generated: undefined;
|
|
1166
|
+
}>;
|
|
1167
|
+
verifiedAt: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>>, {
|
|
1168
|
+
name: string;
|
|
1169
|
+
tableName: "mfa_verifications";
|
|
1170
|
+
dataType: "object date";
|
|
1171
|
+
data: Date;
|
|
1172
|
+
driverParam: string;
|
|
1173
|
+
notNull: true;
|
|
1174
|
+
hasDefault: true;
|
|
1175
|
+
isPrimaryKey: false;
|
|
1176
|
+
isAutoincrement: false;
|
|
1177
|
+
hasRuntimeDefault: false;
|
|
1178
|
+
enumValues: undefined;
|
|
1179
|
+
identity: undefined;
|
|
1180
|
+
generated: undefined;
|
|
1181
|
+
}>;
|
|
1182
|
+
};
|
|
1183
|
+
dialect: "pg";
|
|
1184
|
+
}>;
|
|
1185
|
+
type MfaVerification = typeof mfaVerifications.$inferSelect;
|
|
1186
|
+
type NewMfaVerification = typeof mfaVerifications.$inferInsert;
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* @spfn/auth - Second Factor Service
|
|
1190
|
+
*
|
|
1191
|
+
* An optional second step the owner enrols, that the package then asks for at
|
|
1192
|
+
* the moments a stolen first credential would be used. Two forms: a TOTP
|
|
1193
|
+
* authenticator app, or a passkey the owner already enrolled and has marked as
|
|
1194
|
+
* satisfying a step-up. Ten single-use recovery codes come with either.
|
|
1195
|
+
*
|
|
1196
|
+
* The rule the whole feature hangs on is the one in `assertStepUp`: an account
|
|
1197
|
+
* that never enrolled is answered exactly as it is today, on every route. That
|
|
1198
|
+
* is why the enrolment check comes first and returns, before any passkey
|
|
1199
|
+
* configuration is read and before any second query is made — an app with no
|
|
1200
|
+
* passkeys configured must still be able to change a password.
|
|
1201
|
+
*
|
|
1202
|
+
* The step-up window is per **device key**, not per account: "this device
|
|
1203
|
+
* proved the second factor recently" is the claim a sensitive change needs, and
|
|
1204
|
+
* a verification made on a phone must not authorize a change from a laptop. A
|
|
1205
|
+
* key rotation carries the row across, because rotating is already proof of the
|
|
1206
|
+
* same device.
|
|
1207
|
+
*
|
|
1208
|
+
* A secret and a recovery code appear in exactly three response bodies —
|
|
1209
|
+
* `totp/enroll`, `totp/confirm` and `recovery/regenerate` — and nowhere else:
|
|
1210
|
+
* not in `status`, not in an event, and not in a log line.
|
|
1211
|
+
*/
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Does this account have a second factor at all?
|
|
1215
|
+
*
|
|
1216
|
+
* One statement (`repositories/mfa-enrolment.repository.ts`), because every
|
|
1217
|
+
* sensitive route and every device registration asks it, including the
|
|
1218
|
+
* overwhelming majority that will answer false.
|
|
1219
|
+
*/
|
|
1220
|
+
declare function mfaEnrolledForUser(userId: number): Promise<boolean>;
|
|
1221
|
+
interface AssertStepUpParams {
|
|
1222
|
+
userId: number;
|
|
1223
|
+
/** The device key this request is signed with — the window is its window. */
|
|
1224
|
+
keyId: string;
|
|
1225
|
+
/** Override the configured window. Callers needing a tighter one pass it. */
|
|
1226
|
+
maxAgeMs?: number;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Refuse a sensitive change unless this device proved the second factor recently.
|
|
1230
|
+
*
|
|
1231
|
+
* **No-op for an account with no second factor.** That is the contract of the
|
|
1232
|
+
* whole feature: `changePassword` and `revokeAllKeys` have no recency gate
|
|
1233
|
+
* today, and adding one for people who never opted in would be a new refusal on
|
|
1234
|
+
* routes that never had one. Two of the callers do have a gate of their own —
|
|
1235
|
+
* the passkey routes' `assertRecentAuthentication` — and they still run it
|
|
1236
|
+
* afterwards; this adds a rule for enrolled accounts rather than replacing one.
|
|
1237
|
+
*
|
|
1238
|
+
* The unenrolled path therefore costs one indexed lookup and touches no passkey
|
|
1239
|
+
* configuration, which is what lets an app with no passkeys at all call it.
|
|
1240
|
+
*
|
|
1241
|
+
* @throws StepUpRequiredError when the account is enrolled and the window has passed
|
|
1242
|
+
*/
|
|
1243
|
+
declare function assertStepUp(params: AssertStepUpParams): Promise<void>;
|
|
1244
|
+
/**
|
|
1245
|
+
* Carry a device's verification onto the key that replaces it.
|
|
1246
|
+
*
|
|
1247
|
+
* Called from the two rotation seams — `rotateKeyService`, and the `oldKeyId`
|
|
1248
|
+
* rotation every login path runs through `registerPublicKeyService`. Without
|
|
1249
|
+
* it the window would expire silently on every rotation, which the web proxy
|
|
1250
|
+
* does at each login: a user who stepped up a minute ago would be asked again
|
|
1251
|
+
* with nothing to connect it to.
|
|
1252
|
+
*/
|
|
1253
|
+
declare function carryStepUpVerification(userId: number, fromKeyId: string, toKeyId: string): Promise<void>;
|
|
1254
|
+
interface TotpEnrolmentResult {
|
|
1255
|
+
/** The base32 secret, shown once. Never logged, never returned again. */
|
|
1256
|
+
secret: string;
|
|
1257
|
+
/** The same secret as the URI an authenticator app scans. */
|
|
1258
|
+
otpauthUri: string;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Mint a secret and park it unconfirmed.
|
|
1262
|
+
*
|
|
1263
|
+
* Calling this twice replaces the unconfirmed row and its failure counter — a
|
|
1264
|
+
* user who closed the app before scanning starts over, which is also the
|
|
1265
|
+
* documented remedy for a row that spent its five confirm attempts. A
|
|
1266
|
+
* **confirmed** enrolment is refused instead: replacing a working second factor
|
|
1267
|
+
* is `disable` followed by a fresh enrolment, both step-up gated, so nobody
|
|
1268
|
+
* holding only a stolen session can swap an authenticator for their own.
|
|
1269
|
+
*
|
|
1270
|
+
* @throws MfaAlreadyEnrolledError when a confirmed enrolment already exists
|
|
1271
|
+
*/
|
|
1272
|
+
declare function startTotpEnrolmentService(userId: number): Promise<TotpEnrolmentResult>;
|
|
1273
|
+
interface ConfirmTotpParams {
|
|
1274
|
+
userId: number;
|
|
1275
|
+
/** The device this enrolment is being confirmed from — it gets the first verification. */
|
|
1276
|
+
keyId: string;
|
|
1277
|
+
code: string;
|
|
1278
|
+
}
|
|
1279
|
+
interface ConfirmTotpResult {
|
|
1280
|
+
/** The ten plaintext codes, shown once. */
|
|
1281
|
+
recoveryCodes: string[];
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Spend the first code, which is what turns an enrolment into a second factor.
|
|
1285
|
+
*
|
|
1286
|
+
* Five wrong codes delete the unconfirmed row: at that point the person is
|
|
1287
|
+
* reading the wrong entry in their app, and a fresh `enroll` is both the remedy
|
|
1288
|
+
* and what resets the counter. A confirmed row is never deleted this way.
|
|
1289
|
+
*
|
|
1290
|
+
* The route deliberately runs this outside a transaction, so the counter
|
|
1291
|
+
* survives the refusal that raised it; the success path opens its own, because
|
|
1292
|
+
* the confirmation, the first verification and the recovery codes have to
|
|
1293
|
+
* commit together or not at all.
|
|
1294
|
+
*
|
|
1295
|
+
* @throws MfaNotEnrolledError | MfaVerificationFailedError
|
|
1296
|
+
*/
|
|
1297
|
+
declare function confirmTotpEnrolmentService(params: ConfirmTotpParams): Promise<ConfirmTotpResult>;
|
|
1298
|
+
/**
|
|
1299
|
+
* Replace the account's recovery codes, retiring every earlier one.
|
|
1300
|
+
*
|
|
1301
|
+
* The old generation's rows stay, unspent and unreachable: a code written down
|
|
1302
|
+
* last year is refused with the same body as one that never existed, and the
|
|
1303
|
+
* record of which generation a spent code came from survives.
|
|
1304
|
+
*
|
|
1305
|
+
* @throws MfaNotEnrolledError on an account with no second factor
|
|
1306
|
+
*/
|
|
1307
|
+
declare function regenerateRecoveryCodesService(userId: number): Promise<{
|
|
1308
|
+
recoveryCodes: string[];
|
|
1309
|
+
}>;
|
|
1310
|
+
/**
|
|
1311
|
+
* Take the second factor off the account entirely.
|
|
1312
|
+
*
|
|
1313
|
+
* Idempotent: an account with nothing enrolled is a success, because "make sure
|
|
1314
|
+
* MFA is off" has already happened. The passkeys themselves are untouched —
|
|
1315
|
+
* only their second-factor marks go, since a credential that signs the owner in
|
|
1316
|
+
* is not something this route may remove.
|
|
1317
|
+
*/
|
|
1318
|
+
declare function disableMfaService(userId: number): Promise<void>;
|
|
1319
|
+
interface MarkPasskeyParams {
|
|
1320
|
+
userId: number;
|
|
1321
|
+
passkeyId: string;
|
|
1322
|
+
secondFactor: boolean;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Mark or unmark one of the caller's passkeys as their second factor.
|
|
1326
|
+
*
|
|
1327
|
+
* Unmarking the last one is allowed, and leaves the account unenrolled. It is
|
|
1328
|
+
* deliberately independent of `assertNotLastRecoveryCredential`, which guards
|
|
1329
|
+
* `passkeys/revoke`: that rule protects a way *into* the account, and a
|
|
1330
|
+
* second-factor mark is not one — the credential still signs the owner in
|
|
1331
|
+
* afterwards, unchanged.
|
|
1332
|
+
*
|
|
1333
|
+
* @throws PasskeyNotFoundError for a credential that is not theirs, or revoked
|
|
1334
|
+
*/
|
|
1335
|
+
declare function markPasskeySecondFactorService(params: MarkPasskeyParams): Promise<MfaStatus>;
|
|
1336
|
+
/** What the account surface shows about the second factor. No secret is in it. */
|
|
1337
|
+
interface MfaStatus {
|
|
1338
|
+
enrolled: boolean;
|
|
1339
|
+
/** Which second factors are live: `'totp'`, `'passkey'`, or both. */
|
|
1340
|
+
methods: ('totp' | 'passkey')[];
|
|
1341
|
+
/** Unspent codes of the current generation, for the "2 left" warning. */
|
|
1342
|
+
recoveryCodesRemaining: number;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* The account's second-factor state.
|
|
1346
|
+
*
|
|
1347
|
+
* Carries no secret, no otpauth URI and no recovery code — only the counts and
|
|
1348
|
+
* names an account screen needs. An unconfirmed enrolment is not a method: it
|
|
1349
|
+
* gates nothing, so reporting it would tell the owner they are protected when
|
|
1350
|
+
* they are not.
|
|
1351
|
+
*/
|
|
1352
|
+
declare function mfaStatusService(userId: number): Promise<MfaStatus>;
|
|
1353
|
+
/**
|
|
1354
|
+
* Options for a step-up by passkey assertion.
|
|
1355
|
+
*
|
|
1356
|
+
* `allowCredentials` is empty, as it is for a sign-in and for the same reason
|
|
1357
|
+
* (D3): the discoverable credential on the device names itself, and the owner
|
|
1358
|
+
* and the second-factor mark are checked when the assertion comes back. The
|
|
1359
|
+
* challenge is minted with kind `'mfa'` and this account's id, so it cannot be
|
|
1360
|
+
* presented to `passkeys/login/verify` and a sign-in challenge cannot be
|
|
1361
|
+
* presented here.
|
|
1362
|
+
*/
|
|
1363
|
+
declare function startStepUpService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
|
|
1364
|
+
interface StepUpParams {
|
|
1365
|
+
userId: number;
|
|
1366
|
+
/** The device the verification is recorded against. */
|
|
1367
|
+
keyId: string;
|
|
1368
|
+
code?: string;
|
|
1369
|
+
recoveryCode?: string;
|
|
1370
|
+
response?: AuthenticationResponseJSON;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Re-prove the second factor on this device, refreshing its window.
|
|
1374
|
+
*
|
|
1375
|
+
* The escape hatch the exempt registration channels need: a device-code
|
|
1376
|
+
* approval and a passkey sign-in register a key with no verification against
|
|
1377
|
+
* it, by design, so the session they create would otherwise fail every
|
|
1378
|
+
* sensitive change with no way forward.
|
|
1379
|
+
*
|
|
1380
|
+
* @throws ValidationError when the body names none or more than one input
|
|
1381
|
+
* @throws MfaNotEnrolledError | MfaVerificationFailedError
|
|
1382
|
+
*/
|
|
1383
|
+
declare function stepUpService(params: StepUpParams): Promise<void>;
|
|
1384
|
+
/**
|
|
1385
|
+
* Check exactly one of the three proofs, and say which one it was.
|
|
1386
|
+
*
|
|
1387
|
+
* Exactly one: two inputs in a body is a caller trying combinations, and none
|
|
1388
|
+
* is a malformed request. Neither is a failed verification, so both are a 400
|
|
1389
|
+
* rather than the uniform 401 a wrong proof gets.
|
|
1390
|
+
*
|
|
1391
|
+
* @throws ValidationError | MfaVerificationFailedError
|
|
1392
|
+
*/
|
|
1393
|
+
declare function verifySecondFactor(params: StepUpParams): Promise<MfaVerificationMethod>;
|
|
1394
|
+
/**
|
|
1395
|
+
* Drop enrolments nobody ever confirmed.
|
|
1396
|
+
*
|
|
1397
|
+
* A secret handed out and abandoned is a credential sitting in a table doing
|
|
1398
|
+
* nothing; a day is long enough for anyone who meant to finish.
|
|
1399
|
+
*
|
|
1400
|
+
* @returns number of rows deleted
|
|
1401
|
+
*/
|
|
1402
|
+
declare function sweepUnconfirmedMfaService(): Promise<{
|
|
1403
|
+
deleted: number;
|
|
1404
|
+
}>;
|
|
1405
|
+
|
|
1059
1406
|
/**
|
|
1060
1407
|
* Auth provider type
|
|
1061
1408
|
*
|
|
@@ -1070,10 +1417,16 @@ declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TL
|
|
|
1070
1417
|
* - OAuth 기존 사용자 로그인 시
|
|
1071
1418
|
* - 기기 코드 승인이 소비되어 새 기기 키가 등록될 때 (provider: 'device')
|
|
1072
1419
|
*
|
|
1420
|
+
* `mfaEnrolled` is computed as the event is emitted (#95) and is the whole of
|
|
1421
|
+
* the package's opinion about the second factor: it never blocks an account
|
|
1422
|
+
* that has none, and this is the hook an app uses to offer enrolment at a first
|
|
1423
|
+
* login. It says nothing about *which* factor and carries no secret.
|
|
1424
|
+
*
|
|
1073
1425
|
* @example
|
|
1074
1426
|
* ```typescript
|
|
1075
1427
|
* authLoginEvent.subscribe(async (payload) => {
|
|
1076
1428
|
* await analytics.trackLogin(payload.userId, payload.provider);
|
|
1429
|
+
* if (!payload.mfaEnrolled) await suggestSecondFactor(payload.userId);
|
|
1077
1430
|
* });
|
|
1078
1431
|
* ```
|
|
1079
1432
|
*/
|
|
@@ -1081,7 +1434,8 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
|
|
|
1081
1434
|
email?: string | undefined;
|
|
1082
1435
|
phone?: string | undefined;
|
|
1083
1436
|
userId: string;
|
|
1084
|
-
provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "
|
|
1437
|
+
provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "passkey" | "device";
|
|
1438
|
+
mfaEnrolled: boolean;
|
|
1085
1439
|
}>;
|
|
1086
1440
|
/**
|
|
1087
1441
|
* Where a device key was registered — the door the new device came through.
|
|
@@ -1116,6 +1470,10 @@ type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
|
|
|
1116
1470
|
* Neither the full fingerprint nor the public key is carried. The prefix is
|
|
1117
1471
|
* enough to point at one entry of `listKeys`, which is what a notice needs.
|
|
1118
1472
|
*
|
|
1473
|
+
* `mfaEnrolled` is computed as the event is emitted (#95), so a notice about a
|
|
1474
|
+
* new device can also be the moment an app offers a second factor to the
|
|
1475
|
+
* accounts that have none.
|
|
1476
|
+
*
|
|
1119
1477
|
* @example
|
|
1120
1478
|
* ```typescript
|
|
1121
1479
|
* authDeviceRegisteredEvent.subscribe(async ({ userId, deviceName, ip, channel }) => {
|
|
@@ -1133,7 +1491,8 @@ declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
|
|
|
1133
1491
|
userId: string;
|
|
1134
1492
|
fingerprintPrefix: string;
|
|
1135
1493
|
createdAtMillis: number;
|
|
1136
|
-
|
|
1494
|
+
mfaEnrolled: boolean;
|
|
1495
|
+
channel: "password" | "register" | "oauth-native" | "passkey" | "oauth" | "invitation" | "signup-link" | "password-reset" | "device-code";
|
|
1137
1496
|
}>;
|
|
1138
1497
|
/**
|
|
1139
1498
|
* auth.register - 회원가입 성공 이벤트
|
|
@@ -2272,6 +2631,39 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
|
|
|
2272
2631
|
}, {}, {
|
|
2273
2632
|
passkeyId: string;
|
|
2274
2633
|
}>;
|
|
2634
|
+
mfaTotpEnroll: _spfn_core_route.RouteDef<{
|
|
2635
|
+
body: _sinclair_typebox.TObject<{}>;
|
|
2636
|
+
}, {}, TotpEnrolmentResult>;
|
|
2637
|
+
mfaTotpConfirm: _spfn_core_route.RouteDef<{
|
|
2638
|
+
body: _sinclair_typebox.TObject<{
|
|
2639
|
+
code: _sinclair_typebox.TString;
|
|
2640
|
+
}>;
|
|
2641
|
+
}, {}, ConfirmTotpResult>;
|
|
2642
|
+
mfaDisable: _spfn_core_route.RouteDef<{
|
|
2643
|
+
body: _sinclair_typebox.TObject<{}>;
|
|
2644
|
+
}, {}, void>;
|
|
2645
|
+
mfaMarkPasskey: _spfn_core_route.RouteDef<{
|
|
2646
|
+
body: _sinclair_typebox.TObject<{
|
|
2647
|
+
passkeyId: _sinclair_typebox.TString;
|
|
2648
|
+
secondFactor: _sinclair_typebox.TBoolean;
|
|
2649
|
+
}>;
|
|
2650
|
+
}, {}, MfaStatus>;
|
|
2651
|
+
mfaRegenerateRecoveryCodes: _spfn_core_route.RouteDef<{
|
|
2652
|
+
body: _sinclair_typebox.TObject<{}>;
|
|
2653
|
+
}, {}, {
|
|
2654
|
+
recoveryCodes: string[];
|
|
2655
|
+
}>;
|
|
2656
|
+
mfaStatus: _spfn_core_route.RouteDef<{}, {}, MfaStatus>;
|
|
2657
|
+
mfaStepUp: _spfn_core_route.RouteDef<{
|
|
2658
|
+
body: _sinclair_typebox.TObject<{
|
|
2659
|
+
code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
|
|
2660
|
+
recoveryCode: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
|
|
2661
|
+
response: _sinclair_typebox.TOptional<_sinclair_typebox.TUnknown>;
|
|
2662
|
+
}>;
|
|
2663
|
+
}, {}, void>;
|
|
2664
|
+
mfaStepUpOptions: _spfn_core_route.RouteDef<{
|
|
2665
|
+
body: _sinclair_typebox.TObject<{}>;
|
|
2666
|
+
}, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
|
|
2275
2667
|
logout: _spfn_core_route.RouteDef<{}, {}, void>;
|
|
2276
2668
|
rotateKey: _spfn_core_route.RouteDef<{}, {
|
|
2277
2669
|
body: _sinclair_typebox.TObject<{
|
|
@@ -3084,4 +3476,4 @@ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
|
|
|
3084
3476
|
*/
|
|
3085
3477
|
declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
|
|
3086
3478
|
|
|
3087
|
-
export { type
|
|
3479
|
+
export { type AuthRegisterPayload as $, type AuthInitOptions as A, type ApproveDeviceAuthParams as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type AssertStepUpParams as E, type FinishPasskeyEnrollmentResult as F, type AuthDeletionCancelledPayload as G, type AuthDeletionCompletedPayload as H, type IssueOneTimeTokenResult as I, type AuthDeletionRequestedPayload as J, type KeySummary as K, type LoginResult as L, type MfaStatus as M, type NewPasskey as N, type OAuthStartResult as O, type PermissionConfig as P, type AuthDeviceRegisteredPayload as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type TotpEnrolmentResult as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type AuthLoginPayload as W, type AuthPasswordResetPayload as X, type AuthProfileOutcome as Y, type AuthProfileVerifier as Z, AuthProviderSchema as _, type RegisterResult as a, type UnlinkNotification as a$, type ChangePasswordParams as a0, type CompletePasswordResetParams as a1, type CompleteSignupParams as a2, type ConfirmPasswordResetParams as a3, type ConfirmSignupLinkParams as a4, type ConfirmTotpParams as a5, type DenyDeviceAuthParams as a6, type DeviceAuthApprovedResult as a7, type DeviceAuthInfoParams as a8, type DeviceAuthPendingResult as a9, type OAuthStartParams as aA, type OAuthTokens as aB, type OAuthUnlinkedPayload as aC, PASSKEY_DEVICE_TYPES as aD, PASSKEY_LABEL_MAX_LENGTH as aE, type PasskeyDeviceType as aF, PasswordSchema as aG, PhoneSchema as aH, PlatformSchema as aI, type PollDeviceAuthParams as aJ, type PollDeviceAuthResult as aK, PublicKeySchema as aL, type RecentAuthenticationParams as aM, type RegisterParams as aN, type RegisterPublicKeyParams as aO, type RenamePasskeyParams as aP, type RequestPasswordResetParams as aQ, type RequestSignupLinkParams as aR, type RevokeAllKeysParams as aS, type RevokeKeyParams as aT, type RevokePasskeyParams as aU, type RotateKeyParams as aV, type SendVerificationCodeParams as aW, type StartDeviceAuthParams as aX, type StartPasskeyEnrollmentParams as aY, type StepUpParams as aZ, TargetTypeSchema as a_, DeviceAuthPollResponseSchema as aa, DeviceNameSchema as ab, type DeviceRegistrationChannel as ac, EmailSchema as ad, FingerprintSchema as ae, type FinishPasskeyEnrollmentParams as af, type FinishPasskeyLoginParams as ag, type InvitationAcceptedPayload as ah, type InvitationCreatedPayload as ai, KEY_FINGERPRINT_PREFIX_LENGTH as aj, KeyIdSchema as ak, type LoginParams as al, type LogoutParams as am, MFA_VERIFICATION_METHODS as an, type MachinePrincipal as ao, type MachineVerifierRegistration as ap, type MarkPasskeyParams as aq, type NativeVerifyOptions as ar, type NewMfaVerification as as, type NormalizedIdentity as at, type OAuth2AuthorizeParams as au, type OAuth2ScopeDescription as av, type OAuthCallbackParams as aw, type OAuthCallbackResult as ax, type OAuthCodeExchangeOptions as ay, type OAuthNativeParams as az, type RequestSignupLinkResult as b, registerAuthProfile as b$, UnlinkNotifyRejection as b0, type UnlinkNotifyRequest as b1, type UnlinkNotifyResult as b2, UserCodeSchema as b3, VerificationPurposeSchema as b4, type VerifyCodeParams as b5, type VerifyCodeResult as b6, approveDeviceAuthService as b7, approveOAuth2AuthorizeService as b8, assertNotLastRecoveryCredential as b9, getGoogleAccessToken as bA, getMachinePrincipal as bB, getOAuthProvider as bC, getRegisteredProviders as bD, invitationAcceptedEvent as bE, invitationCreatedEvent as bF, isOAuthProviderEnabled as bG, issueOneTimeTokenService as bH, listKeysService as bI, listOAuth2GrantsService as bJ, listPasskeysService as bK, loginService as bL, logoutService as bM, machineAuth as bN, markPasskeySecondFactorService as bO, mfaEnrolledForUser as bP, mfaStatusService as bQ, mfaVerifications as bR, oauthCallbackService as bS, oauthNativeService as bT, oauthStartService as bU, oauthUnlinkNotifyService as bV, oauthUnlinkedEvent as bW, optionalAuth as bX, passkeys as bY, pollDeviceAuthService as bZ, regenerateRecoveryCodesService as b_, assertRecentAuthentication as ba, assertStepUp as bb, authDeletionCancelledEvent as bc, authDeletionCompletedEvent as bd, authDeletionRequestedEvent as be, authDeviceRegisteredEvent as bf, authLoginEvent as bg, authPasswordResetEvent as bh, authRegisterEvent as bi, authenticate as bj, buildOAuthErrorUrl as bk, carryStepUpVerification as bl, changePasswordService as bm, completePasswordResetService as bn, completeSignupService as bo, confirmPasswordResetService as bp, confirmSignupLinkService as bq, confirmTotpEnrolmentService as br, denyDeviceAuthService as bs, denyOAuth2AuthorizeService as bt, describeOAuth2AuthorizeRequestService as bu, disableMfaService as bv, finishPasskeyEnrollmentService as bw, finishPasskeyLoginService as bx, getDeviceAuthInfoService as by, getEnabledOAuthProviders as bz, type RequestPasswordResetResult as c, registerMachineVerifier as c0, registerOAuthProvider as c1, registerPublicKeyService as c2, registerService as c3, renamePasskeyService as c4, requestPasswordResetService as c5, requestSignupLinkService as c6, requireEnabledProvider as c7, requireMachineScope as c8, resolveAuthenticatedUser as c9, revokeAllKeysService as ca, revokeAllOAuth2GrantsForUser as cb, revokeKeyService as cc, revokeOAuth2GrantService as cd, revokePasskeyService as ce, rotateKeyService as cf, runAuthProfile as cg, selectAuthProfile as ch, sendVerificationCodeService as ci, startDeviceAuthService as cj, startPasskeyEnrollmentService as ck, startPasskeyLoginService as cl, startStepUpService as cm, startTotpEnrolmentService as cn, stepUpService as co, sweepUnconfirmedMfaService as cp, verifyCodeService as cq, verifyOneTimeTokenService as cr, verifySecondFactor as cs, type ConfirmPasswordResetResult as d, type StartDeviceAuthResult as e, type PasskeySummary as f, type ConfirmTotpResult as g, type RotateKeyResult as h, type RevokeAllKeysResult as i, type OAuthNativeResult as j, type ProfileInfo as k, type OAuth2ConsentView as l, mainAuthRouter as m, type OAuth2AuthorizationCodeIssued as n, type OAuth2GrantSummary as o, type AuthSession as p, PERMISSION_CATEGORIES as q, type PermissionCategory as r, VERIFICATION_TARGET_TYPES as s, type VerificationPurpose as t, type VerificationTargetType as u, type OAuthProvider as v, type Passkey as w, type MfaVerification as x, type MfaVerificationMethod as y, type AuthContext as z };
|