@rodit/rodit-auth-be 9.11.20 → 9.13.0

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
@@ -2,6 +2,106 @@
2
2
 
3
3
  All notable changes to `@rodit/rodit-auth-be` are documented here.
4
4
 
5
+ ## [9.13.0] — 2026-07-15
6
+
7
+ ### Added
8
+
9
+ - **Federated login across APIs in the same SR/CR family.** A client RODiT
10
+ issued for API A can log into API B via
11
+ `login_server({ apiEndpoint: '<federated URL>' })`. Mutual auth remains
12
+ optional; federation is login → remint a local JWT (foreign JWTs are not
13
+ accepted without re-login).
14
+ - **JWT claim `rodit_subjectuniqueidentifier_url` (always present).** Same
15
+ unset-as-`null` rule as `config_*` in 9.12:
16
+ - Same-API login: claim is `null`; `iss` = receiving server URL
17
+ - Federated login: claim = federated API URL; `iss` = client home
18
+ `subjectuniqueidentifier_url`
19
+ - Server validation treats non-empty claim strings as federated JWTs
20
+ (`isNonEmptyUrlClaim`); renewal always copies/normalizes to `null` or the
21
+ URL (9.12 tokens without the claim renew into `null`)
22
+ - **Client MITM check.** After crypto validation, `login_server` verifies that
23
+ a federated JWT's `rodit_subjectuniqueidentifier_url` matches the intended
24
+ `apiEndpoint` and that `iss` matches the client home URL. Failures:
25
+ `FEDERATED_ISSUER_MISSING`, `FEDERATED_ISSUER_MISMATCH`.
26
+ - **Exported helpers:** `normalizeUrlWithoutPort`, `isNonEmptyUrlClaim`,
27
+ `isFederatedRoditLogin`, `validateFederatedLoginTarget`.
28
+
29
+ ## [9.12.0] — 2026-07-02
30
+
31
+ ### Fixed
32
+
33
+ - **Multi-peer inbound webhook verification.** Inbound webhook signatures were
34
+ verified against `stateManager.getPeerBase64urlJwkPublicKey()`, a single
35
+ mutable "current peer" slot. When an agent was connected to several peers the
36
+ slot was clobbered by the most recent peer, so webhooks from other senders
37
+ (including the central API) failed with a false `401`
38
+ `WEBHOOK_SIGNATURE_INVALID` / `PEER_KEY_UNAVAILABLE`.
39
+
40
+ ### Added
41
+
42
+ - **Self-identifying webhooks.** The signer's Ed25519 public key now travels
43
+ with each webhook, so verification no longer depends on a shared "current
44
+ peer" slot and is correct when connected to many peers at once:
45
+ - `send_webhook` advertises the signer identity via `X-Rodit-Public-Key`,
46
+ `X-Rodit-Implicit-Account` (hex of the signing key), and `X-Rodit-Token-Id`.
47
+ - `webhookHandler.extractWebhookSignerKey(headers)` reads that key directly
48
+ (implicit account is authoritative on its own; a raw advertised key must
49
+ agree with it). Exported from the package root as `extractWebhookSignerKey`.
50
+ - **Signer ↔ session authorization gate.** A valid signature only proves the
51
+ sender holds the advertised private key; it does not prove that identity is a
52
+ peer we established a session with. `createWebhookAuthenticationMiddleware`
53
+ now enforces, after the signature verifies, that the (signed) `session_id`
54
+ maps to a live session whose `ownerId` equals the verified signer's implicit
55
+ account. New rejections: `401 WEBHOOK_SESSION_REQUIRED`,
56
+ `401 WEBHOOK_SESSION_INVALID`, `403 WEBHOOK_SIGNER_SESSION_MISMATCH`
57
+ (bypassed only under `NODE_ENV=test` or
58
+ `SECURITY_OPTIONS.BYPASS_WEBHOOK_VERIFICATION`). The validated binding is
59
+ exposed as `req.webhook_session`, `req.webhook_session_id`, and
60
+ `req.webhook_signer_implicit_account`.
61
+ - **Webhook ↔ session correlation.** `send_webhook` stamps the originating
62
+ session id into the **signed** payload (`session_id`) so receivers can link an
63
+ inbound webhook to the session opened at login:
64
+ - `extractWebhookSessionId({ headers, rawPayload, parsedBody })` returns the
65
+ session id (signed payload authoritative, `X-Rodit-Session-Id` header as
66
+ fallback). Exported from the package root and from `webhookHandler`.
67
+ - `createPublicKeyMiddleware` sets `req.webhook_session_id` for downstream
68
+ handlers.
69
+ - The session id is resolved without requiring a request context: it falls
70
+ back to **session storage** (the SessionManager singleton, in-memory by
71
+ default) via `findSessionsByRoditId(options.sessionRoditId)`, so a JWT issuer
72
+ (`login_client`) can stamp the shared session id for out-of-band webhooks.
73
+ - `RoditClient.sendWebhook` / `sendWebhookToEndpoint` / `sendWakeHook` /
74
+ `sendAgentHook` accept an `options` argument (`sessionId`, `sessionRoditId`)
75
+ and default `sessionId` to the client's own `this.sessionId` (set at
76
+ `login_server`), so a peer that initiated the login stamps its session
77
+ automatically.
78
+ - **Client-side session recording (symmetric session store).** The
79
+ `SessionManager` facility used by servers to track client sessions is now also
80
+ used by clients to record the sessions they open:
81
+ - `SessionManager.recordSession({ id, roditId, ... })` stores an
82
+ externally-issued session under its own id (idempotent), tagged
83
+ `origin: "client"`. `createSession` now tags issued sessions
84
+ `origin: "server"`.
85
+ - `SessionManager.hasSession(id)` quietly reports whether an id is a known,
86
+ live session (no warn logs, no lastAccessedAt bump); `forgetSession(id)`
87
+ removes one.
88
+ - `RoditClient.login_server` records each opened session keyed by the server's
89
+ roditId, so a client connected to many servers can cross-reference an inbound
90
+ webhook's `session_id` to a live session (multi-peer safe, unlike the single
91
+ `this.sessionId` slot). `clearSession` forgets it.
92
+ - `RoditClient.isKnownSession(id)` cross-references a webhook's session id
93
+ against the client's open sessions.
94
+ - Issuer-side webhook session lookup only considers `origin !== "client"`
95
+ sessions, so mutual peers don't cross-stamp.
96
+
97
+ ### Changed
98
+
99
+ - **`createPublicKeyMiddleware`** extracts the verification key from the webhook
100
+ itself (`extractWebhookSignerKey`) instead of the mutable
101
+ `stateManager.getPeerBase64urlJwkPublicKey()` slot, and attaches
102
+ `req.webhook_session_id`. Authentication (key from the webhook) and
103
+ authorization (binding to a session) are now cleanly separated.
104
+
5
105
  ## [8.0.0] — 2026-05-02
6
106
 
7
107
  ### Breaking