@cauthyhq/node 1.2.0 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Fixed
6
+
7
+ - **Moving to `CAUTHY_KEY`**: a session sealed with `CAUTHY_SECRET`, `PACT_ROOT_KEY` or `CAUTHY_KEY_PREVIOUS` is sealed again with the key on the first request that reads it through the handler or an adapter (`GET <basePath>/session`, `req.cauthy()`, …), not only when its access token is renewed. A session read during the window now outlives the old secret's removal.
8
+ - `PACT_ROOT_KEY_PREVIOUS` beside `CAUTHY_KEY` and `PACT_ROOT_KEY` is read too: sessions sealed before a root rotation that was still open at the move stay readable. 1.2.0 read the current root only.
9
+ - A session cookie that no configured key opens is still cleared, and is now said once per process (`console.warn`), naming the secret to keep beside `CAUTHY_KEY` for the window.
10
+ - Covered end to end with the published packages: sessions signed in by `cauthy-auth` 0.41.0 and 0.42.0 (contract, `PACT_ROOT_KEY`, `keys`) and by `@cauthyhq/node` 1.1.0 (`CAUTHY_SECRET`), read by 1.2 at `GET /api/id/session`, fresh and renewed.
11
+
3
12
  ## 1.2.0
4
13
 
5
14
  One variable for every application: `CAUTHY_KEY`.
package/README.md CHANGED
@@ -110,8 +110,10 @@ new is written with it.
110
110
 
111
111
  **Moving from the variables of 1.1:** on the application's card, **Move to
112
112
  CAUTHY_KEY**. Deploy with `CAUTHY_KEY` and keep `CAUTHY_SECRET` for the window
113
- (old sessions stay readable; one warning says to remove it), remove the other
114
- variables, then **Remove the secret**. The variables of 1.1 and `PACT_ROOT_KEY`
113
+ (old sessions stay readable, and each is sealed again with the key the first
114
+ time it is read; one warning says to remove it), remove the other variables,
115
+ then **Remove the secret**. A session nobody used during the window is lost
116
+ with the secret. The variables of 1.1 and `PACT_ROOT_KEY`
115
117
  still work in 1.x with one `DeprecationWarning` per process and leave in 2.0.
116
118
 
117
119
  ## Verifying a token
@@ -195,7 +197,7 @@ export const auth = createCauthy({ contract: { graph, system: 'acme-crm' } })
195
197
 
196
198
  The key proves the link's service client (`pact.<link>.service`); people sign in through its public client (`pact.<link>`). The key must name the graph's Cauthy (`key_contract_issuer` otherwise). The application publishes the key's public half at the contract's `jwks_url` path (`/.well-known/pact-jwks.json` by default), and the previous key's too during a rotation. The Express, Hono and Fastify adapters route that path to the handler as they do `/api/auth`, so those need no route of their own for it. **Next.js and TanStack Start still need a route file** for it that calls `auth.handler(request)`.
197
199
 
198
- The 1.1 way (`PACT_ROOT_KEY` and `keys: postgresKeyStore(query)`) keeps working through 1.x with a deprecation warning. Moving: generate `CAUTHY_KEY` on the application's card, deploy it with `PACT_ROOT_KEY` and the `keys` option still beside it for the window (old sessions stay readable, its RS256 keys stay published beside the new Ed25519 key), then remove `PACT_ROOT_KEY`, the option and the table.
200
+ The 1.1 way (`PACT_ROOT_KEY` and `keys: postgresKeyStore(query)`) keeps working through 1.x with a deprecation warning. Moving: generate `CAUTHY_KEY` on the application's card, deploy it with `PACT_ROOT_KEY` (and `PACT_ROOT_KEY_PREVIOUS`, if a root rotation is open) and the `keys` option still beside it for the window (old sessions stay readable and are sealed again with the key the first time they are read, its RS256 keys stay published beside the new Ed25519 key), then remove `PACT_ROOT_KEY`, the option and the table. A session nobody used during the window is lost with the root key.
199
201
 
200
202
  It refuses to start when any variable `CAUTHY_KEY` replaces is also set, when the link names its workspace by slug and the graph has no `tenant_id` for it yet, when a workspace named by id is pinned to a different `tenant_id`, when `system_proof` is not `off`, `auto` or `required`, or when the link asks for a system proof without a `service` block. When the link sets `require_verified_email`, a person whose address is not confirmed gets no session. See [`contract/pact.md`](https://github.com/vennet-developers/cauthy-sdk-spec/blob/main/contract/pact.md).
201
203
 
package/dist/index.js CHANGED
@@ -122,9 +122,12 @@ async function unseal(sealed, key) {
122
122
  }
123
123
  }
124
124
  async function unsealAny(sealed, ring) {
125
- for (const key of ring) {
126
- const value = await unseal(sealed, key);
127
- if (value !== null) return value;
125
+ return (await unsealIndexed(sealed, ring))?.value ?? null;
126
+ }
127
+ async function unsealIndexed(sealed, ring) {
128
+ for (let index = 0; index < ring.length; index++) {
129
+ const value = await unseal(sealed, ring[index]);
130
+ if (value !== null) return { value, index };
128
131
  }
129
132
  return null;
130
133
  }
@@ -2372,12 +2375,13 @@ function byKey(given) {
2372
2375
  }
2373
2376
  const legacySecret = env3("CAUTHY_SECRET") || void 0;
2374
2377
  const legacyRoot = env3("PACT_ROOT_KEY") || void 0;
2378
+ const legacyRootPrevious = legacyRoot ? env3("PACT_ROOT_KEY_PREVIOUS") || void 0 : void 0;
2375
2379
  if (legacySecret) deprecated("CAUTHY_SECRET beside CAUTHY_KEY", "It is read only for sessions made before the move: remove it after the rotation window.");
2376
2380
  if (legacyRoot) deprecated("PACT_ROOT_KEY beside CAUTHY_KEY", "It is read only for sessions made before the move: remove it, the keys option and its table after the rotation window.");
2377
2381
  if (given.keys && !(legacyRoot && given.contract)) {
2378
2382
  throw new CauthyConfigError("with CAUTHY_KEY the application keeps no key store: remove keys (it is read only beside PACT_ROOT_KEY and a contract, while moving)");
2379
2383
  }
2380
- return { key, previous, legacySecret, legacyRoot };
2384
+ return { key, previous, legacySecret, legacyRoot, legacyRootPrevious };
2381
2385
  }
2382
2386
  var UnknownOriginError = class extends Error {
2383
2387
  constructor(origin, registered) {
@@ -2417,6 +2421,12 @@ function byContract(given) {
2417
2421
  return { contract, root, previous: given.rootKeyPrevious ?? env3("PACT_ROOT_KEY_PREVIOUS") };
2418
2422
  }
2419
2423
  var toldOfHeader = false;
2424
+ var toldOfUnopened = false;
2425
+ function toldUnopened() {
2426
+ if (toldOfUnopened) return;
2427
+ toldOfUnopened = true;
2428
+ console.warn("@cauthyhq/node: a session cookie arrived that no key of this application opens; it was cleared and the person is signed out. Moving to CAUTHY_KEY, keep the secret that sealed the old sessions (CAUTHY_SECRET, or PACT_ROOT_KEY and PACT_ROOT_KEY_PREVIOUS) beside it for the rotation window; rotating the key, set CAUTHY_KEY_PREVIOUS.");
2429
+ }
2420
2430
  function createCauthy(given = {}) {
2421
2431
  const keyed = byKey(given);
2422
2432
  const pacted = given.contract && !keyed ? byContract(given) : void 0;
@@ -2438,11 +2448,11 @@ function createCauthy(given = {}) {
2438
2448
  }, () => void 0);
2439
2449
  const derived = pacted ? deriveKeys(pacted.root, pacted.previous) : void 0;
2440
2450
  const single = derived || fromKey ? void 0 : keyOf(secret).then((k) => [k]);
2441
- const legacyRings = keyed?.legacyRoot ? deriveKeys(keyed.legacyRoot) : void 0;
2451
+ const legacyRings = keyed?.legacyRoot ? deriveKeys(keyed.legacyRoot, keyed.legacyRootPrevious) : void 0;
2442
2452
  const legacySingle = keyed?.legacySecret ? keyOf(keyed.legacySecret) : void 0;
2443
2453
  const withLegacy = async (own, purpose) => [
2444
2454
  ...await own,
2445
- ...legacyRings ? [(await legacyRings[purpose])[0]] : [],
2455
+ ...legacyRings ? await legacyRings[purpose] : [],
2446
2456
  ...legacySingle ? [await legacySingle] : []
2447
2457
  ];
2448
2458
  const cookieKeys = fromKey ? withLegacy(fromKey.cookie, "cookie") : derived?.cookie ?? single;
@@ -2608,10 +2618,11 @@ function createCauthy(given = {}) {
2608
2618
  async function read(request) {
2609
2619
  const had = readCookies(request.headers.get("cookie"));
2610
2620
  const raw = readParts(had, cookieName);
2611
- if (raw === null) return { had, value: null, present: false };
2612
- const value = await unsealAny(raw, await cookieKeys);
2621
+ if (raw === null) return { had, value: null, present: false, stale: false };
2622
+ const opened = await unsealIndexed(raw, await cookieKeys);
2623
+ const value = opened?.value ?? null;
2613
2624
  const sound = value !== null && typeof value.at === "string" && typeof value.rt === "string" && typeof value.exp === "number";
2614
- return { had, value: sound ? value : null, present: true };
2625
+ return { had, value: sound ? value : null, present: true, stale: sound && opened.index > 0 };
2615
2626
  }
2616
2627
  async function write(request, s, had) {
2617
2628
  return writeParts(cookieName, await seal(s, (await cookieKeys)[0]), attrs(request, s.p === 1 ? PERSISTENT_SECONDS : void 0), had);
@@ -2648,12 +2659,13 @@ function createCauthy(given = {}) {
2648
2659
  return running;
2649
2660
  }
2650
2661
  async function resolve2(request) {
2651
- const { had, value, present } = await read(request);
2662
+ const { had, value, present, stale } = await read(request);
2652
2663
  if (value === null) {
2664
+ if (present) toldUnopened();
2653
2665
  return { session: null, status: "unauthenticated", cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] };
2654
2666
  }
2655
2667
  if (unverified(value.u)) return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };
2656
- if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: [] };
2668
+ if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: stale ? await write(request, value, had) : [] };
2657
2669
  const renewed = await renew(value);
2658
2670
  if (renewed === "ended") {
2659
2671
  return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };