@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/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
- return await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
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
- onAfterCommit(() => authDeviceRegisteredEvent.emit({
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 onAfterCommit2, runInTransaction as runInTransaction2 } from "@spfn/core/db";
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
- onAfterCommit2(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
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
- onAfterCommit2(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
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 onAfterCommit3 } from "@spfn/core/db";
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
- onAfterCommit3(() => authDeletionRequestedEvent.emit({
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
- onAfterCommit3(() => notifyDeletionRequested(user, purgeScheduledAt));
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
- onAfterCommit3(() => authDeletionCancelledEvent.emit({
12925
+ onAfterCommit4(() => authDeletionCancelledEvent.emit({
12832
12926
  userId: String(user.id),
12833
12927
  userPublicId: user.publicId
12834
12928
  }));
12835
- onAfterCommit3(() => notifyDeletionCancelled(user));
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
- onAfterCommit3(() => notifyPurgeFinal(email));
13016
+ onAfterCommit4(() => notifyPurgeFinal(email));
12923
13017
  }
12924
- onAfterCommit3(() => authDeletionCompletedEvent.emit({
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 onAfterCommit4 } from "@spfn/core/db";
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
- onAfterCommit4(() => authPasswordResetEvent.emit({
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 onAfterCommit5 } from "@spfn/core/db";
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
- config2 = { ttlMs, intervalMs };
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 { status: "pending", intervalMillis: getDeviceAuthConfig().intervalMs };
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
- onAfterCommit5(() => authLoginEvent.emit({
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 onAfterCommit6, runInTransaction as runInTransaction4 } from "@spfn/core/db";
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
- onAfterCommit6(() => passkeyEnrolledEvent.emit({
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
- onAfterCommit6(() => authLoginEvent.emit({
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
- onAfterCommit6(() => passkeyRevokedEvent.emit({
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 onAfterCommit7 } from "@spfn/core/db";
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
- onAfterCommit7(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
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([rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
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({ ...body, ...deviceProvenance(c.raw) });
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.0";
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,