@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 +9 -0
- package/README.md +5 -3
- package/dist/index.js +23 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
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
|
|
114
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 ?
|
|
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
|
|
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) };
|