@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 CHANGED
@@ -104,12 +104,16 @@ Import it for its side-effect (it self-registers); it must run before the proxy
104
104
  // app/api/rpc/[routeName]/route.ts
105
105
  import '@spfn/auth/nextjs/api'; // side-effect: registers auth interceptors
106
106
  import { createRpcProxy } from '@spfn/core/nextjs/server';
107
- import { authRouteMap } from '@spfn/auth';
108
107
  import { routeMap } from '@/generated/route-map';
109
108
 
110
- export const { GET, POST } = createRpcProxy({ routeMap: { ...routeMap, ...authRouteMap } });
109
+ export const { GET, POST } = createRpcProxy({ routeMap });
111
110
  ```
112
111
 
112
+ No auth route map is merged: the generated `routeMap` carries the routes of every package
113
+ router the app router mounts with `.packages()`, `authRouter`'s included. `authRouteMap` is
114
+ still exported and `{ ...routeMap, ...authRouteMap }` is still harmless — the two hold the
115
+ same entries — but it is a no-op.
116
+
113
117
  ### 4. Run migrations
114
118
 
115
119
  ```bash
@@ -499,6 +503,31 @@ const answer = await authApi.pollDeviceAuth.call({ body: { deviceCode } });
499
503
  // → { status: 'approved', userId, publicId, email?, phone?, passwordChangeRequired }
500
504
  ```
501
505
 
506
+ Or long-poll: send `waitMillis` and the server holds a pending request until the owner
507
+ answers or the wait runs out, so the device learns of an approval the moment it is made
508
+ instead of at its next tick.
509
+
510
+ ```typescript
511
+ let answer;
512
+
513
+ do
514
+ {
515
+ // Held up to 20s (the server's maxWaitMs caps it). A pending answer takes the time
516
+ // already waited off intervalMillis — 0 after a full wait, so ask again at once.
517
+ // An error ends the loop, as before.
518
+ answer = await authApi.pollDeviceAuth.call({ body: { deviceCode, waitMillis: 20_000 } });
519
+
520
+ if (answer.status === 'pending' && answer.intervalMillis > 0)
521
+ {
522
+ await new Promise(resolve => setTimeout(resolve, answer.intervalMillis));
523
+ }
524
+ }
525
+ while (answer.status === 'pending');
526
+ ```
527
+
528
+ Keep the loop's sleep on `intervalMillis > 0`. It covers a server that answered without
529
+ waiting — an older one that ignores the field — so the loop never spins.
530
+
502
531
  ```typescript
503
532
  // On the signed-in device — the user typed the code they read off the other screen.
504
533
  const asking = await authApi.getDeviceAuthInfo.call({ body: { userCode } });
@@ -553,22 +582,36 @@ two ways in are indistinguishable.
553
582
  who cannot authenticate, so `publicKey`, `keyId` and `fingerprint` carry length limits —
554
583
  generous next to a real key (an RSA-2048 SPKI is 392 base64 characters against a 2048 limit)
555
584
  and small next to the megabyte that would otherwise sit in a table no job clears.
585
+ - **A long poll holds no transaction.** The wait is route middleware in front of the poll's
586
+ `Transactional()`, so a waiting device does not pin a pooled connection, and the answer is
587
+ judged inside the transaction exactly as a poll without `waitMillis` is — same atomicity,
588
+ same database-error answers. Approve, deny and a global revocation wake a poll parked in the
589
+ same process after they commit. A poll parked on another instance re-reads its record every
590
+ second, so an approval committed elsewhere reaches it within about a second. A device that
591
+ hangs up mid-wait is not judged, so an approval it can no longer hear waits for its next poll;
592
+ at most three polls wait on one code at a time — a fourth is answered at once; and a server
593
+ that starts shutting down ends every wait with a pending answer rather than a cut connection.
556
594
  - **Clock skew cannot affect this.** Every timestamp in the decision is the server's. The
557
595
  `expiresAtMillis` in the start response is for the waiting device's countdown display, and
558
596
  nothing the client believes about the time reaches the server's judgement.
559
597
 
560
- Two knobs, both announced to the waiting device in the start response and therefore resolved
561
- at lifecycle time rather than read per call:
598
+ Three knobs, resolved at lifecycle time rather than read per call — the first two are
599
+ announced to the waiting device in the start response:
562
600
 
563
601
  ```typescript
564
602
  createAuthLifecycle({
565
603
  deviceAuth: {
566
604
  ttlMs: 10 * 60 * 1000, // how long a code lives. default 10 minutes
567
605
  intervalMs: 5000, // poll interval the server asks for. default 5s
606
+ maxWaitMs: 20_000, // longest a long poll is held. default 20s
568
607
  },
569
608
  })
570
609
  ```
571
610
 
611
+ Keep `maxWaitMs` under the idle timeout of every proxy and load balancer in front of the
612
+ server. A long poll cut off by one reaches the device as a network error, not as a pending
613
+ answer — Google Cloud's load balancer closes a backend request at 30 seconds by default.
614
+
572
615
  No job sweeps the table. Rows are judged by `expiresAt` whenever they are read or moved, so a
573
616
  stale row authorizes nothing; it only keeps its user code out of circulation, and 31⁸ codes do
574
617
  not run out.
@@ -3231,9 +3274,8 @@ export type AppRouter = typeof appRouter;
3231
3274
  // app/api/rpc/[routeName]/route.ts
3232
3275
  import '@spfn/auth/nextjs/api';
3233
3276
  import { createRpcProxy } from '@spfn/core/nextjs/server';
3234
- import { authRouteMap } from '@spfn/auth';
3235
- import { routeMap } from '@/generated/route-map';
3236
- export const { GET, POST } = createRpcProxy({ routeMap: { ...routeMap, ...authRouteMap } });
3277
+ import { routeMap } from '@/generated/route-map'; // already holds authRouter's routes
3278
+ export const { GET, POST } = createRpcProxy({ routeMap });
3237
3279
 
3238
3280
  // any client component
3239
3281
  import { authApi } from '@spfn/auth';
@@ -1419,7 +1419,7 @@ function isAppKind(kind) {
1419
1419
  }
1420
1420
 
1421
1421
  // src/server/client-proof/contract-bundle.ts
1422
- var CONTRACT_VERSION = "0.13.0";
1422
+ var CONTRACT_VERSION = "0.13.1";
1423
1423
  var CONTRACT_MAJOR = 0;
1424
1424
  var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
1425
1425
  function required(name, type) {
@@ -1754,7 +1754,8 @@ var CONTRACT_TYPES = [
1754
1754
  {
1755
1755
  name: "PollDeviceAuthRequest",
1756
1756
  fields: [
1757
- required("deviceCode", "string")
1757
+ required("deviceCode", "string"),
1758
+ optional("waitMillis", "integer")
1758
1759
  ]
1759
1760
  },
1760
1761
  /**