@spfn/auth 0.3.0-beta.26 → 0.3.0-beta.27
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 +49 -7
- package/dist/client-proof.js +3 -2
- package/dist/client-proof.js.map +1 -1
- package/dist/index.d.ts +194 -189
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-CdEgxOB1.d.ts → machine-principals-BvRw8b8t.d.ts} +18 -0
- package/dist/server.d.ts +37 -4
- package/dist/server.js +210 -33
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -8505,6 +8505,77 @@ var init_mfa_enrolment_repository = __esm({
|
|
|
8505
8505
|
}
|
|
8506
8506
|
});
|
|
8507
8507
|
|
|
8508
|
+
// src/server/lib/device-auth-waiters.ts
|
|
8509
|
+
import { onAfterCommit } from "@spfn/core/db";
|
|
8510
|
+
function announceDeviceAuthAnswered(ids) {
|
|
8511
|
+
if (ids.length === 0) {
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
onAfterCommit(() => {
|
|
8515
|
+
for (const id26 of ids) {
|
|
8516
|
+
wake(id26);
|
|
8517
|
+
}
|
|
8518
|
+
});
|
|
8519
|
+
}
|
|
8520
|
+
function wake(id26) {
|
|
8521
|
+
const parked = waiters.get(id26);
|
|
8522
|
+
if (!parked) {
|
|
8523
|
+
return;
|
|
8524
|
+
}
|
|
8525
|
+
waiters.delete(id26);
|
|
8526
|
+
for (const resolve of parked) {
|
|
8527
|
+
resolve();
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
function waitForDeviceAuthAnswer(id26, timeoutMs, signal) {
|
|
8531
|
+
if (signal?.aborted) {
|
|
8532
|
+
return Promise.resolve();
|
|
8533
|
+
}
|
|
8534
|
+
return new Promise((resolve) => {
|
|
8535
|
+
const parked = waiters.get(id26) ?? /* @__PURE__ */ new Set();
|
|
8536
|
+
const done = () => {
|
|
8537
|
+
clearTimeout(timer);
|
|
8538
|
+
signal?.removeEventListener("abort", done);
|
|
8539
|
+
unpark(id26, parked, done);
|
|
8540
|
+
resolve();
|
|
8541
|
+
};
|
|
8542
|
+
const timer = setTimeout(done, timeoutMs);
|
|
8543
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
8544
|
+
parked.add(done);
|
|
8545
|
+
waiters.set(id26, parked);
|
|
8546
|
+
});
|
|
8547
|
+
}
|
|
8548
|
+
function unpark(id26, parked, resolver) {
|
|
8549
|
+
parked.delete(resolver);
|
|
8550
|
+
if (parked.size === 0 && waiters.get(id26) === parked) {
|
|
8551
|
+
waiters.delete(id26);
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
8554
|
+
function waitingOnDeviceAuth(id26) {
|
|
8555
|
+
return waiting.get(id26) ?? 0;
|
|
8556
|
+
}
|
|
8557
|
+
async function holdDeviceAuthWait(id26, wait) {
|
|
8558
|
+
waiting.set(id26, waitingOnDeviceAuth(id26) + 1);
|
|
8559
|
+
try {
|
|
8560
|
+
return await wait();
|
|
8561
|
+
} finally {
|
|
8562
|
+
const left = waitingOnDeviceAuth(id26) - 1;
|
|
8563
|
+
if (left > 0) {
|
|
8564
|
+
waiting.set(id26, left);
|
|
8565
|
+
} else {
|
|
8566
|
+
waiting.delete(id26);
|
|
8567
|
+
}
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
8570
|
+
var waiters, waiting;
|
|
8571
|
+
var init_device_auth_waiters = __esm({
|
|
8572
|
+
"src/server/lib/device-auth-waiters.ts"() {
|
|
8573
|
+
"use strict";
|
|
8574
|
+
waiters = /* @__PURE__ */ new Map();
|
|
8575
|
+
waiting = /* @__PURE__ */ new Map();
|
|
8576
|
+
}
|
|
8577
|
+
});
|
|
8578
|
+
|
|
8508
8579
|
// src/server/repositories/device-authorizations.repository.ts
|
|
8509
8580
|
import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
|
|
8510
8581
|
import { eq as eq14, and as and14, gt as gt7, inArray as inArray2, sql as sql9 } from "drizzle-orm";
|
|
@@ -8513,6 +8584,7 @@ var init_device_authorizations_repository = __esm({
|
|
|
8513
8584
|
"src/server/repositories/device-authorizations.repository.ts"() {
|
|
8514
8585
|
"use strict";
|
|
8515
8586
|
init_device_authorizations();
|
|
8587
|
+
init_device_auth_waiters();
|
|
8516
8588
|
notExpired = () => gt7(deviceAuthorizations.expiresAt, sql9`now()`);
|
|
8517
8589
|
DeviceAuthorizationsRepository = class extends BaseRepository14 {
|
|
8518
8590
|
/**
|
|
@@ -8555,6 +8627,19 @@ var init_device_authorizations_repository = __esm({
|
|
|
8555
8627
|
const result = await this.readDb.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
|
|
8556
8628
|
return result[0] ?? null;
|
|
8557
8629
|
}
|
|
8630
|
+
/**
|
|
8631
|
+
* `findByDeviceCodeHash`, read from the primary even outside a transaction.
|
|
8632
|
+
*
|
|
8633
|
+
* For the long poll's wait, which runs before any transaction opens: it is
|
|
8634
|
+
* woken right after an answer commits, and a replica that has not caught up
|
|
8635
|
+
* yet would show it the record still pending and send it back to sleep.
|
|
8636
|
+
*
|
|
8637
|
+
* Write primary.
|
|
8638
|
+
*/
|
|
8639
|
+
async findByDeviceCodeHashOnPrimary(deviceCodeHash) {
|
|
8640
|
+
const result = await this.db.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
|
|
8641
|
+
return result[0] ?? null;
|
|
8642
|
+
}
|
|
8558
8643
|
/**
|
|
8559
8644
|
* Bind the approving user and move the record to `approved`, but only from
|
|
8560
8645
|
* `pending`.
|
|
@@ -8572,7 +8657,7 @@ var init_device_authorizations_repository = __esm({
|
|
|
8572
8657
|
notExpired()
|
|
8573
8658
|
)
|
|
8574
8659
|
).returning();
|
|
8575
|
-
return result[0] ?? null;
|
|
8660
|
+
return this.answered(result[0] ?? null);
|
|
8576
8661
|
}
|
|
8577
8662
|
/**
|
|
8578
8663
|
* Move the record to `denied`, but only from `pending`.
|
|
@@ -8589,7 +8674,7 @@ var init_device_authorizations_repository = __esm({
|
|
|
8589
8674
|
notExpired()
|
|
8590
8675
|
)
|
|
8591
8676
|
).returning();
|
|
8592
|
-
return result[0] ?? null;
|
|
8677
|
+
return this.answered(result[0] ?? null);
|
|
8593
8678
|
}
|
|
8594
8679
|
/**
|
|
8595
8680
|
* Refuse every authorization a user still has in flight.
|
|
@@ -8617,12 +8702,14 @@ var init_device_authorizations_repository = __esm({
|
|
|
8617
8702
|
* @returns the rows this call refused
|
|
8618
8703
|
*/
|
|
8619
8704
|
async denyAllActiveByUserId(userId) {
|
|
8620
|
-
|
|
8705
|
+
const denied = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
8621
8706
|
and14(
|
|
8622
8707
|
eq14(deviceAuthorizations.userId, userId),
|
|
8623
8708
|
inArray2(deviceAuthorizations.status, ["pending", "approved"])
|
|
8624
8709
|
)
|
|
8625
8710
|
).returning();
|
|
8711
|
+
announceDeviceAuthAnswered(denied.map((record) => record.id));
|
|
8712
|
+
return denied;
|
|
8626
8713
|
}
|
|
8627
8714
|
/**
|
|
8628
8715
|
* Spend an approved record, but only from `approved`, and address it by the
|
|
@@ -8645,6 +8732,13 @@ var init_device_authorizations_repository = __esm({
|
|
|
8645
8732
|
).returning();
|
|
8646
8733
|
return result[0] ?? null;
|
|
8647
8734
|
}
|
|
8735
|
+
/** Wake the polls parked on a record this call moved, and hand the row back. */
|
|
8736
|
+
answered(record) {
|
|
8737
|
+
if (record) {
|
|
8738
|
+
announceDeviceAuthAnswered([record.id]);
|
|
8739
|
+
}
|
|
8740
|
+
return record;
|
|
8741
|
+
}
|
|
8648
8742
|
};
|
|
8649
8743
|
deviceAuthorizationsRepository = new DeviceAuthorizationsRepository();
|
|
8650
8744
|
}
|
|
@@ -11587,7 +11681,7 @@ import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/a
|
|
|
11587
11681
|
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
11588
11682
|
|
|
11589
11683
|
// src/server/services/device-registration.service.ts
|
|
11590
|
-
import { onAfterCommit } from "@spfn/core/db";
|
|
11684
|
+
import { onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
|
|
11591
11685
|
|
|
11592
11686
|
// src/server/events/index.ts
|
|
11593
11687
|
init_esm();
|
|
@@ -11739,7 +11833,7 @@ init_repositories();
|
|
|
11739
11833
|
var DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH = 12;
|
|
11740
11834
|
async function emitDeviceRegistered(row, channel) {
|
|
11741
11835
|
const mfaEnrolled = await mfaEnrolmentRepository.isEnrolled(row.userId);
|
|
11742
|
-
|
|
11836
|
+
onAfterCommit2(() => authDeviceRegisteredEvent.emit({
|
|
11743
11837
|
userId: String(row.userId),
|
|
11744
11838
|
keyId: row.keyId,
|
|
11745
11839
|
algorithm: row.algorithm,
|
|
@@ -11757,7 +11851,7 @@ async function emitDeviceRegistered(row, channel) {
|
|
|
11757
11851
|
// src/server/services/mfa.service.ts
|
|
11758
11852
|
init_logger();
|
|
11759
11853
|
init_config();
|
|
11760
|
-
import { onAfterCommit as
|
|
11854
|
+
import { onAfterCommit as onAfterCommit3, runInTransaction as runInTransaction2 } from "@spfn/core/db";
|
|
11761
11855
|
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
11762
11856
|
import {
|
|
11763
11857
|
MfaAlreadyEnrolledError,
|
|
@@ -12230,7 +12324,7 @@ async function releaseDeferredAnnouncements(row, key) {
|
|
|
12230
12324
|
return;
|
|
12231
12325
|
}
|
|
12232
12326
|
const mfaEnrolled = await mfaEnrolledForUser(row.userId);
|
|
12233
|
-
|
|
12327
|
+
onAfterCommit3(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
|
|
12234
12328
|
}
|
|
12235
12329
|
async function verifyMfaChallengeService(params) {
|
|
12236
12330
|
const challengeHash = hashCredential(params.challenge);
|
|
@@ -12324,7 +12418,7 @@ async function countConfirmFailure(userId) {
|
|
|
12324
12418
|
function readSecret(secretEnc, userId) {
|
|
12325
12419
|
const { value, needsRotation } = decryptMfaSecret(secretEnc, userId);
|
|
12326
12420
|
if (needsRotation) {
|
|
12327
|
-
|
|
12421
|
+
onAfterCommit3(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
|
|
12328
12422
|
}
|
|
12329
12423
|
return value;
|
|
12330
12424
|
}
|
|
@@ -12633,7 +12727,7 @@ init_key_policy();
|
|
|
12633
12727
|
// src/server/services/account-deletion.service.ts
|
|
12634
12728
|
init_repositories();
|
|
12635
12729
|
import { ValidationError as ValidationError4, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
|
|
12636
|
-
import { runInTransaction as runInTransaction3, onAfterCommit as
|
|
12730
|
+
import { runInTransaction as runInTransaction3, onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
|
|
12637
12731
|
import { sendEmail as sendEmail3 } from "@spfn/notification/server";
|
|
12638
12732
|
import {
|
|
12639
12733
|
InvalidCredentialsError,
|
|
@@ -12790,13 +12884,13 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
12790
12884
|
await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
|
|
12791
12885
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
12792
12886
|
await revokeAllOAuth2GrantsForUser(user.id);
|
|
12793
|
-
|
|
12887
|
+
onAfterCommit4(() => authDeletionRequestedEvent.emit({
|
|
12794
12888
|
userId: String(user.id),
|
|
12795
12889
|
userPublicId: user.publicId,
|
|
12796
12890
|
purgeScheduledAt: purgeScheduledAt.toISOString(),
|
|
12797
12891
|
requestedBy
|
|
12798
12892
|
}));
|
|
12799
|
-
|
|
12893
|
+
onAfterCommit4(() => notifyDeletionRequested(user, purgeScheduledAt));
|
|
12800
12894
|
if (gracePeriodDays === 0) {
|
|
12801
12895
|
await purgePendingRequest(request);
|
|
12802
12896
|
}
|
|
@@ -12828,11 +12922,11 @@ async function cancelAccountDeletionService(params) {
|
|
|
12828
12922
|
throw new DeletionNotRequestedError();
|
|
12829
12923
|
}
|
|
12830
12924
|
await usersRepository.reactivateFromPendingDeletion(user.id);
|
|
12831
|
-
|
|
12925
|
+
onAfterCommit4(() => authDeletionCancelledEvent.emit({
|
|
12832
12926
|
userId: String(user.id),
|
|
12833
12927
|
userPublicId: user.publicId
|
|
12834
12928
|
}));
|
|
12835
|
-
|
|
12929
|
+
onAfterCommit4(() => notifyDeletionCancelled(user));
|
|
12836
12930
|
return { userId: String(user.id) };
|
|
12837
12931
|
}
|
|
12838
12932
|
async function anonymizeUser(user) {
|
|
@@ -12919,9 +13013,9 @@ async function purgePendingRequest(request) {
|
|
|
12919
13013
|
}
|
|
12920
13014
|
const { email, publicId } = purgedUser;
|
|
12921
13015
|
if (email) {
|
|
12922
|
-
|
|
13016
|
+
onAfterCommit4(() => notifyPurgeFinal(email));
|
|
12923
13017
|
}
|
|
12924
|
-
|
|
13018
|
+
onAfterCommit4(() => authDeletionCompletedEvent.emit({
|
|
12925
13019
|
userPublicId: publicId,
|
|
12926
13020
|
purgeStrategy
|
|
12927
13021
|
}));
|
|
@@ -13275,7 +13369,7 @@ init_repositories();
|
|
|
13275
13369
|
import { env as env11 } from "@spfn/auth/config";
|
|
13276
13370
|
import { PasswordResetLinkError, PasswordResetSessionError } from "@spfn/auth/errors";
|
|
13277
13371
|
import { ValidationError as ValidationError6 } from "@spfn/core/errors";
|
|
13278
|
-
import { onAfterCommit as
|
|
13372
|
+
import { onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
|
|
13279
13373
|
init_key_policy();
|
|
13280
13374
|
async function activeUserOf(userId) {
|
|
13281
13375
|
const user = await usersRepository.findByIdOnPrimary(userId);
|
|
@@ -13376,7 +13470,7 @@ async function completePasswordResetService(params) {
|
|
|
13376
13470
|
throw new PasswordResetSessionError();
|
|
13377
13471
|
}
|
|
13378
13472
|
const registered = await replaceCredentials(row, user, params);
|
|
13379
|
-
|
|
13473
|
+
onAfterCommit5(() => authPasswordResetEvent.emit({
|
|
13380
13474
|
userId: String(user.id),
|
|
13381
13475
|
email: row.email
|
|
13382
13476
|
}));
|
|
@@ -13468,21 +13562,26 @@ import {
|
|
|
13468
13562
|
DeviceAuthDeniedError,
|
|
13469
13563
|
InvalidKeyFingerprintError as InvalidKeyFingerprintError2
|
|
13470
13564
|
} from "@spfn/auth/errors";
|
|
13471
|
-
import { onAfterCommit as
|
|
13565
|
+
import { onAfterCommit as onAfterCommit6 } from "@spfn/core/db";
|
|
13566
|
+
import { getShutdownManager } from "@spfn/core/server";
|
|
13472
13567
|
|
|
13473
13568
|
// src/server/lib/device-auth-config.ts
|
|
13474
13569
|
var DEFAULT_DEVICE_AUTH_TTL_MS = 10 * 60 * 1e3;
|
|
13475
13570
|
var DEFAULT_DEVICE_AUTH_INTERVAL_MS = 5 * 1e3;
|
|
13571
|
+
var DEFAULT_DEVICE_AUTH_MAX_WAIT_MS = 20 * 1e3;
|
|
13476
13572
|
var config2 = {
|
|
13477
13573
|
ttlMs: DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
13478
|
-
intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS
|
|
13574
|
+
intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
13575
|
+
maxWaitMs: DEFAULT_DEVICE_AUTH_MAX_WAIT_MS
|
|
13479
13576
|
};
|
|
13480
13577
|
function configureDeviceAuth(options) {
|
|
13481
13578
|
const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_AUTH_TTL_MS;
|
|
13482
13579
|
const intervalMs = options?.intervalMs ?? DEFAULT_DEVICE_AUTH_INTERVAL_MS;
|
|
13580
|
+
const maxWaitMs = options?.maxWaitMs ?? DEFAULT_DEVICE_AUTH_MAX_WAIT_MS;
|
|
13483
13581
|
assertWholeMillis("ttlMs", ttlMs);
|
|
13484
13582
|
assertWholeMillis("intervalMs", intervalMs);
|
|
13485
|
-
|
|
13583
|
+
assertWholeMillis("maxWaitMs", maxWaitMs);
|
|
13584
|
+
config2 = { ttlMs, intervalMs, maxWaitMs };
|
|
13486
13585
|
}
|
|
13487
13586
|
function assertWholeMillis(name, value) {
|
|
13488
13587
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -13495,6 +13594,9 @@ function getDeviceAuthConfig() {
|
|
|
13495
13594
|
return config2;
|
|
13496
13595
|
}
|
|
13497
13596
|
|
|
13597
|
+
// src/server/services/device-auth.service.ts
|
|
13598
|
+
init_device_auth_waiters();
|
|
13599
|
+
|
|
13498
13600
|
// src/server/lib/device-code.ts
|
|
13499
13601
|
import { createHash, randomBytes, randomInt } from "crypto";
|
|
13500
13602
|
var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
@@ -13524,6 +13626,8 @@ function hashDeviceCode(deviceCode) {
|
|
|
13524
13626
|
// src/server/services/device-auth.service.ts
|
|
13525
13627
|
init_key_policy();
|
|
13526
13628
|
var USER_CODE_ATTEMPTS = 3;
|
|
13629
|
+
var WAIT_RECHECK_MS = 1e3;
|
|
13630
|
+
var MAX_WAITERS_PER_RECORD = 3;
|
|
13527
13631
|
function assertActionable(record) {
|
|
13528
13632
|
if (!record || record.status === "consumed") {
|
|
13529
13633
|
throw new DeviceAuthNotFoundError();
|
|
@@ -13623,6 +13727,37 @@ async function denyDeviceAuthService(params) {
|
|
|
13623
13727
|
);
|
|
13624
13728
|
}
|
|
13625
13729
|
}
|
|
13730
|
+
async function waitForDeviceAuthAnswerService(params) {
|
|
13731
|
+
const requested = Math.min(params.waitMillis, getDeviceAuthConfig().maxWaitMs);
|
|
13732
|
+
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
13733
|
+
const record = requested > 0 ? await readWaitable(deviceCodeHash) : null;
|
|
13734
|
+
if (!record || waitingOnDeviceAuth(record.id) >= MAX_WAITERS_PER_RECORD) {
|
|
13735
|
+
return 0;
|
|
13736
|
+
}
|
|
13737
|
+
const startedAt = Date.now();
|
|
13738
|
+
const deadline = Math.min(startedAt + requested, record.expiresAt.getTime());
|
|
13739
|
+
await holdDeviceAuthWait(record.id, () => waitUntil(record.id, deviceCodeHash, deadline, params.signal));
|
|
13740
|
+
return Date.now() - startedAt;
|
|
13741
|
+
}
|
|
13742
|
+
async function waitUntil(id26, deviceCodeHash, deadline, signal) {
|
|
13743
|
+
while (!signal?.aborted && !getShutdownManager().isShuttingDown()) {
|
|
13744
|
+
const remaining = deadline - Date.now();
|
|
13745
|
+
if (remaining <= 0) {
|
|
13746
|
+
return;
|
|
13747
|
+
}
|
|
13748
|
+
await waitForDeviceAuthAnswer(id26, Math.min(remaining, WAIT_RECHECK_MS), signal);
|
|
13749
|
+
if (!await readWaitable(deviceCodeHash)) {
|
|
13750
|
+
return;
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
}
|
|
13754
|
+
async function readWaitable(deviceCodeHash) {
|
|
13755
|
+
const record = await deviceAuthorizationsRepository.findByDeviceCodeHashOnPrimary(deviceCodeHash).catch(() => null);
|
|
13756
|
+
return isWaitable(record) ? record : null;
|
|
13757
|
+
}
|
|
13758
|
+
function isWaitable(record) {
|
|
13759
|
+
return record?.status === "pending" && record.expiresAt.getTime() > Date.now();
|
|
13760
|
+
}
|
|
13626
13761
|
async function pollDeviceAuthService(params) {
|
|
13627
13762
|
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
13628
13763
|
const record = assertActionable(
|
|
@@ -13632,7 +13767,10 @@ async function pollDeviceAuthService(params) {
|
|
|
13632
13767
|
throw new DeviceAuthDeniedError();
|
|
13633
13768
|
}
|
|
13634
13769
|
if (record.status === "pending") {
|
|
13635
|
-
return {
|
|
13770
|
+
return {
|
|
13771
|
+
status: "pending",
|
|
13772
|
+
intervalMillis: Math.max(0, getDeviceAuthConfig().intervalMs - (params.waitedMillis ?? 0))
|
|
13773
|
+
};
|
|
13636
13774
|
}
|
|
13637
13775
|
const consumed = await deviceAuthorizationsRepository.consumeApproved(deviceCodeHash);
|
|
13638
13776
|
if (!consumed) {
|
|
@@ -13688,7 +13826,7 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13688
13826
|
...loginBindingFields(registeredBinding(registered))
|
|
13689
13827
|
};
|
|
13690
13828
|
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13691
|
-
|
|
13829
|
+
onAfterCommit6(() => authLoginEvent.emit({
|
|
13692
13830
|
userId: String(user.id),
|
|
13693
13831
|
provider: "device",
|
|
13694
13832
|
email: result.email,
|
|
@@ -13701,7 +13839,7 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13701
13839
|
// src/server/services/passkey.service.ts
|
|
13702
13840
|
init_logger();
|
|
13703
13841
|
init_config();
|
|
13704
|
-
import { onAfterCommit as
|
|
13842
|
+
import { onAfterCommit as onAfterCommit7, runInTransaction as runInTransaction4 } from "@spfn/core/db";
|
|
13705
13843
|
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
13706
13844
|
import {
|
|
13707
13845
|
AccountDisabledError as AccountDisabledError3,
|
|
@@ -13819,7 +13957,7 @@ async function finishPasskeyEnrollmentService(params) {
|
|
|
13819
13957
|
aaguid: verified.aaguid,
|
|
13820
13958
|
label: params.label ?? null
|
|
13821
13959
|
});
|
|
13822
|
-
|
|
13960
|
+
onAfterCommit7(() => passkeyEnrolledEvent.emit({
|
|
13823
13961
|
userId: String(params.userId),
|
|
13824
13962
|
passkeyId: String(row.id),
|
|
13825
13963
|
label: row.label ?? void 0
|
|
@@ -13919,7 +14057,7 @@ async function startSession(user, params) {
|
|
|
13919
14057
|
...loginBindingFields(registeredBinding(registered))
|
|
13920
14058
|
};
|
|
13921
14059
|
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13922
|
-
|
|
14060
|
+
onAfterCommit7(() => authLoginEvent.emit({
|
|
13923
14061
|
userId: String(user.id),
|
|
13924
14062
|
provider: "passkey",
|
|
13925
14063
|
email: result.email,
|
|
@@ -14002,7 +14140,7 @@ async function revokePasskeyService(params) {
|
|
|
14002
14140
|
if (!revoked) {
|
|
14003
14141
|
throw new PasskeyNotFoundError2();
|
|
14004
14142
|
}
|
|
14005
|
-
|
|
14143
|
+
onAfterCommit7(() => passkeyRevokedEvent.emit({
|
|
14006
14144
|
userId: String(params.userId),
|
|
14007
14145
|
passkeyId: String(revoked.id),
|
|
14008
14146
|
reason: "user"
|
|
@@ -15830,7 +15968,7 @@ async function oauthUnlinkNotifyService(provider, notification) {
|
|
|
15830
15968
|
}
|
|
15831
15969
|
|
|
15832
15970
|
// src/server/services/oauth-native.service.ts
|
|
15833
|
-
import { runInTransaction as runInTransaction5, onAfterCommit as
|
|
15971
|
+
import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit8 } from "@spfn/core/db";
|
|
15834
15972
|
import {
|
|
15835
15973
|
InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
|
|
15836
15974
|
NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
|
|
@@ -15903,7 +16041,7 @@ async function persistNativeLogin(identity, params) {
|
|
|
15903
16041
|
}
|
|
15904
16042
|
await updateLastLoginService(userId);
|
|
15905
16043
|
const mfaEnrolled = isNewUser ? false : await mfaEnrolledForUser(userId);
|
|
15906
|
-
|
|
16044
|
+
onAfterCommit8(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
|
|
15907
16045
|
return { mfaRequired: false, userId: String(userId), keyId: params.keyId, isNewUser };
|
|
15908
16046
|
}, { context: "auth:oauth-native" });
|
|
15909
16047
|
}
|
|
@@ -16937,6 +17075,31 @@ function attestedClientIp(c) {
|
|
|
16937
17075
|
return provenance.webProxy ? provenance.ip ?? null : null;
|
|
16938
17076
|
}
|
|
16939
17077
|
|
|
17078
|
+
// src/server/middleware/device-auth-long-poll.ts
|
|
17079
|
+
var DEVICE_AUTH_WAITED_MILLIS = "deviceAuthWaitedMillis";
|
|
17080
|
+
function deviceAuthLongPoll() {
|
|
17081
|
+
return async (c, next) => {
|
|
17082
|
+
const target = waitTarget(await c.req.json().catch(() => null));
|
|
17083
|
+
if (!target) {
|
|
17084
|
+
return next();
|
|
17085
|
+
}
|
|
17086
|
+
const signal = c.req.raw.signal;
|
|
17087
|
+
const waitedMillis = await waitForDeviceAuthAnswerService({ ...target, signal });
|
|
17088
|
+
if (signal.aborted) {
|
|
17089
|
+
return c.body(null, 204);
|
|
17090
|
+
}
|
|
17091
|
+
c.set(DEVICE_AUTH_WAITED_MILLIS, waitedMillis);
|
|
17092
|
+
return next();
|
|
17093
|
+
};
|
|
17094
|
+
}
|
|
17095
|
+
function waitTarget(body) {
|
|
17096
|
+
const { deviceCode, waitMillis } = body ?? {};
|
|
17097
|
+
if (typeof deviceCode !== "string" || !Number.isInteger(waitMillis) || waitMillis <= 0) {
|
|
17098
|
+
return null;
|
|
17099
|
+
}
|
|
17100
|
+
return { deviceCode, waitMillis };
|
|
17101
|
+
}
|
|
17102
|
+
|
|
16940
17103
|
// src/server/routes/auth/index.ts
|
|
16941
17104
|
var sendVerificationCode = route.post("/_auth/codes").input({
|
|
16942
17105
|
body: Type.Object({
|
|
@@ -17095,11 +17258,23 @@ var startDeviceAuth = route.post("/_auth/device/start").input({
|
|
|
17095
17258
|
});
|
|
17096
17259
|
var pollDeviceAuth = route.post("/_auth/device/poll").input({
|
|
17097
17260
|
body: Type.Object({
|
|
17098
|
-
deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" })
|
|
17261
|
+
deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" }),
|
|
17262
|
+
waitMillis: Type.Optional(Type.Integer({
|
|
17263
|
+
minimum: 0,
|
|
17264
|
+
description: "Longest to hold the request while nobody has answered; capped by the server"
|
|
17265
|
+
}))
|
|
17099
17266
|
})
|
|
17100
|
-
}).use([
|
|
17267
|
+
}).use([
|
|
17268
|
+
rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }),
|
|
17269
|
+
deviceAuthLongPoll(),
|
|
17270
|
+
Transactional()
|
|
17271
|
+
]).skip(["auth"]).handler(async (c) => {
|
|
17101
17272
|
const { body } = await c.data();
|
|
17102
|
-
return await pollDeviceAuthService({
|
|
17273
|
+
return await pollDeviceAuthService({
|
|
17274
|
+
deviceCode: body.deviceCode,
|
|
17275
|
+
...deviceProvenance(c.raw),
|
|
17276
|
+
waitedMillis: Number(c.raw.get(DEVICE_AUTH_WAITED_MILLIS) ?? 0)
|
|
17277
|
+
});
|
|
17103
17278
|
});
|
|
17104
17279
|
var getDeviceAuthInfo = route.post("/_auth/device/info").input({
|
|
17105
17280
|
body: Type.Object({
|
|
@@ -18176,7 +18351,7 @@ var CORE_PREREQUISITE_OPERATIONS = [
|
|
|
18176
18351
|
|
|
18177
18352
|
// src/server/client-proof/contract-bundle.ts
|
|
18178
18353
|
init_wire_headers();
|
|
18179
|
-
var CONTRACT_VERSION = "0.13.
|
|
18354
|
+
var CONTRACT_VERSION = "0.13.1";
|
|
18180
18355
|
var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
|
|
18181
18356
|
function required(name, type) {
|
|
18182
18357
|
return { name, type, optional: false };
|
|
@@ -18510,7 +18685,8 @@ var CONTRACT_TYPES = [
|
|
|
18510
18685
|
{
|
|
18511
18686
|
name: "PollDeviceAuthRequest",
|
|
18512
18687
|
fields: [
|
|
18513
|
-
required("deviceCode", "string")
|
|
18688
|
+
required("deviceCode", "string"),
|
|
18689
|
+
optional("waitMillis", "integer")
|
|
18514
18690
|
]
|
|
18515
18691
|
},
|
|
18516
18692
|
/**
|
|
@@ -21479,6 +21655,7 @@ export {
|
|
|
21479
21655
|
DEFAULT_DELETION_PURGE_STRATEGY,
|
|
21480
21656
|
DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
21481
21657
|
DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
21658
|
+
DEFAULT_DEVICE_AUTH_MAX_WAIT_MS,
|
|
21482
21659
|
DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
21483
21660
|
DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
21484
21661
|
DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON,
|