@konneal/engine 0.2.22 → 0.2.24

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.
Files changed (32) hide show
  1. package/dist/ask-OO6UUIFF.js +12 -0
  2. package/dist/{chunk-RESCBSX6.js → chunk-434NRPSS.js} +1 -1
  3. package/dist/{chunk-BV7IKH3N.js → chunk-FYGJDJPN.js} +2 -2
  4. package/dist/chunk-HXWDVAVN.js +24441 -0
  5. package/dist/{chunk-YTWPZE5O.js → chunk-HYI32HMI.js} +0 -32
  6. package/dist/{chunk-M24JWPVN.js → chunk-IM5KMIL6.js} +9 -9
  7. package/dist/{chunk-COKARI7O.js → chunk-IOZAZA7Y.js} +2 -2
  8. package/dist/{chunk-IROEVHEV.js → chunk-PZLYPUYJ.js} +4 -4
  9. package/dist/{chunk-FNJ457SE.js → chunk-RZ3UZVRR.js} +1 -1
  10. package/dist/livedata.d.ts +3 -4
  11. package/dist/oidc-refresh.d.ts +19 -0
  12. package/dist/oidc.d.ts +1 -0
  13. package/dist/refresh-core.d.ts +28 -0
  14. package/dist/search-OJ776DMH.js +12 -0
  15. package/dist/worker_mcp/src/index.js +3 -3
  16. package/dist/worker_public/src/config.js +2 -2
  17. package/dist/worker_public/src/index.js +9 -9
  18. package/dist/worker_public/src/profile.js +1 -1
  19. package/dist/worker_public/src/refusal.js +2 -2
  20. package/dist/worker_public/src/requestScope.js +3 -3
  21. package/package.json +2 -3
  22. package/workers/worker_public/src/aggregation.ts +1 -1
  23. package/workers/worker_public/src/auth.ts +45 -10
  24. package/workers/worker_public/src/codecs.ts +33 -32
  25. package/workers/worker_public/src/conditions.ts +2 -2
  26. package/workers/worker_public/src/livedata.ts +35 -0
  27. package/workers/worker_public/src/oidc-refresh.ts +76 -0
  28. package/workers/worker_public/src/oidc.ts +1 -0
  29. package/workers/worker_public/src/refresh-core.ts +67 -0
  30. package/dist/ask-HLELROYT.js +0 -12
  31. package/dist/chunk-4XBU4J6B.js +0 -4149
  32. package/dist/search-GVDELP3N.js +0 -12
@@ -65,6 +65,41 @@ const EXCHANGED_KEY = (sessionHash: string) => `ossx:${sessionHash}`;
65
65
  * window (the OP token's own TTL, minus a small margin). Called ONCE
66
66
  * per sign-in from the auth callback; the token never persists past
67
67
  * the window. */
68
+ // The session's refresh-token store (oidc-refresh.ts): the OP's refresh
69
+ // grant is what makes session renewal a RE-JUDGMENT of live standing
70
+ // instead of a re-stamp of frozen claims. Same KV discipline as the
71
+ // access token: keyed by the session token's hash, TTL = the grant's
72
+ // own life, never D1.
73
+ const REFRESH_KEY = (sessionHash: string) => `ort:${sessionHash}`;
74
+ const REFRESH_TTL_SEC = 30 * 24 * 60 * 60; // the OP's grant TTL default
75
+
76
+ export async function retainRefreshToken(env: any, sessionRaw: string, refreshToken: string): Promise<void> {
77
+ try {
78
+ await env.CACHE.put(REFRESH_KEY(await sha256Hex(sessionRaw)), JSON.stringify({ token: refreshToken }), {
79
+ expirationTtl: REFRESH_TTL_SEC,
80
+ });
81
+ } catch {
82
+ // a KV hiccup degrades to the legacy staleness, never blocks sign-in
83
+ }
84
+ }
85
+
86
+ export async function readRefreshToken(env: any, sessionRaw: string): Promise<string | null> {
87
+ try {
88
+ const hit = await env.CACHE.get(REFRESH_KEY(await sha256Hex(sessionRaw)), "json");
89
+ return typeof hit?.token === "string" ? hit.token : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export async function dropRefreshToken(env: any, sessionRaw: string): Promise<void> {
96
+ try {
97
+ await env.CACHE.delete(REFRESH_KEY(await sha256Hex(sessionRaw)));
98
+ } catch {
99
+ // best effort
100
+ }
101
+ }
102
+
68
103
  export async function retainOpAccessToken(
69
104
  env: any,
70
105
  sessionRaw: string,
@@ -0,0 +1,76 @@
1
+ // The session's lifeline (TODO.identity-rp/01): a session's claims are
2
+ // frozen at mint — a deactivated account, a revoked role, a new avatar
3
+ // would all surface weeks late. So renewal is not a re-stamp of the old
4
+ // claims: when a session ages past the threshold, the RP presents the
5
+ // OP's refresh token, and the OP's refresh grant re-judges the LIVE
6
+ // standing (deactivation kills the grant family → the next renewal ends
7
+ // the session, and the response carries today's name/picture/roles).
8
+ // The rotated refresh token must be stored after every use — the OP's
9
+ // rotation is one-time, and a reuse verdict revokes the whole family.
10
+
11
+ import { discoverIssuer, validateIdToken, type OidcIdTokenClaims } from "./oidc.ts";
12
+ import { renewedSessionClaims, refreshGrant, type OidcMetadata } from "./refresh-core.ts";
13
+ import type { SessionClaims } from "./session";
14
+ import { readRefreshToken, dropRefreshToken, retainRefreshToken } from "./livedata";
15
+
16
+
17
+
18
+ export interface RenewedSession {
19
+ claims: Omit<SessionClaims, "iat" | "exp">;
20
+ refreshToken: string;
21
+ }
22
+
23
+ export type RenewOutcome =
24
+ | { kind: "ok"; renewed: RenewedSession }
25
+ | { kind: "revoked" }
26
+ | { kind: "unavailable" };
27
+
28
+ export async function renewSessionClaims(
29
+ env: any,
30
+ cfg: { issuer: string; clientId: string; clientSecret?: string; sessionSecret: string },
31
+ session: SessionClaims,
32
+ sessionRaw: string,
33
+ ): Promise<RenewOutcome> {
34
+ const stored = await readRefreshToken(env, sessionRaw);
35
+ if (!stored) return { kind: "unavailable" };
36
+ let meta;
37
+ try {
38
+ meta = (await discoverIssuer(cfg.issuer)) as unknown as OidcMetadata;
39
+ } catch {
40
+ return { kind: "unavailable" };
41
+ }
42
+ const granted = await refreshGrant(meta, cfg, stored);
43
+ // the OP's refusal IS the revocation signal (deactivated, signed out
44
+ // everywhere, expired) — the renewal ends; unavailability keeps the
45
+ // session for the next load
46
+ if (granted.kind === "refused") {
47
+ await dropRefreshToken(env, sessionRaw);
48
+ return { kind: "revoked" };
49
+ }
50
+ if (granted.kind === "unavailable") return granted;
51
+ const token = granted.token;
52
+ if (typeof token.refresh_token !== "string" || !token.refresh_token) {
53
+ // a rotation without a successor: the OP is telling us the grant is
54
+ // finished (narrowed away) — treat as revoked
55
+ await dropRefreshToken(env, sessionRaw);
56
+ return { kind: "revoked" };
57
+ }
58
+ let claims: OidcIdTokenClaims;
59
+ try {
60
+ claims = await validateIdToken(token.id_token, {
61
+ issuer: cfg.issuer,
62
+ clientId: cfg.clientId,
63
+ nonce: undefined as unknown as string,
64
+ jwksUri: meta.jwks_uri,
65
+ });
66
+ } catch {
67
+ return { kind: "unavailable" };
68
+ }
69
+ const fresh = renewedSessionClaims(session, claims);
70
+ if (!fresh) {
71
+ await dropRefreshToken(env, sessionRaw);
72
+ return { kind: "revoked" };
73
+ }
74
+ await retainRefreshToken(env, sessionRaw, token.refresh_token);
75
+ return { kind: "ok", renewed: { claims: fresh, refreshToken: token.refresh_token } };
76
+ }
@@ -152,6 +152,7 @@ export function buildAuthorizationUrl(
152
152
  export interface OidcTokenResponse {
153
153
  id_token: string;
154
154
  access_token?: string;
155
+ refresh_token?: string;
155
156
  token_type?: string;
156
157
  expires_in?: number;
157
158
  }
@@ -0,0 +1,67 @@
1
+ // The dependency-free core of the session renewal (node-loadable for
2
+ // unit tests — the law that produced verdict-parse.ts): the refresh
3
+ // grant's backchannel classification and the claims merge with its
4
+ // sub guard. The stateful orchestration lives in oidc-refresh.ts.
5
+ import type { OidcIdTokenClaims, OidcTokenResponse } from "./oidc.ts";
6
+ import type { SessionClaims } from "./session";
7
+
8
+ export interface OidcMetadata {
9
+ token_endpoint: string;
10
+ jwks_uri: string;
11
+ }
12
+
13
+ export type RefreshResult =
14
+ | { kind: "ok"; token: OidcTokenResponse }
15
+ | { kind: "refused" }
16
+ | { kind: "unavailable" };
17
+
18
+ /** The refresh grant's backchannel: the same shape as the code
19
+ * exchange's POST (the OP's WAF wants the issuer's own origin), with
20
+ * the three outcomes the renewal policy needs — a 400 is the OP
21
+ * REFUSING (revocation, expiry: dead), a network throw is
22
+ * unavailability (keep the session, retry next load). */
23
+ export async function refreshGrant(
24
+ meta: OidcMetadata,
25
+ cfg: { clientId: string; clientSecret?: string },
26
+ refreshToken: string,
27
+ ): Promise<RefreshResult> {
28
+ const body = new URLSearchParams({
29
+ grant_type: "refresh_token",
30
+ refresh_token: refreshToken,
31
+ client_id: cfg.clientId,
32
+ });
33
+ const headers: Record<string, string> = { "content-type": "application/x-www-form-urlencoded" };
34
+ headers.origin = new URL(meta.token_endpoint).origin;
35
+ if (cfg.clientSecret) {
36
+ headers.authorization = `Basic ${btoa(`${encodeURIComponent(cfg.clientId)}:${encodeURIComponent(cfg.clientSecret)}`)}`;
37
+ }
38
+ let json: unknown;
39
+ try {
40
+ const res = await fetch(meta.token_endpoint, { method: "POST", headers, body });
41
+ json = await res.json().catch(() => ({}));
42
+ if (!res.ok) return { kind: "refused" };
43
+ } catch {
44
+ return { kind: "unavailable" };
45
+ }
46
+ const token = json as Partial<OidcTokenResponse>;
47
+ if (typeof token?.id_token !== "string") return { kind: "unavailable" };
48
+ return { kind: "ok", token: token as OidcTokenResponse };
49
+ }
50
+
51
+ /** The fresh session claims from the refresh grant's ID token. The sub
52
+ * MUST match (a different account is a provisioning error, never an
53
+ * update); every profile field falls back to the standing session's so
54
+ * a claim the OP omits never erases what the user already had. */
55
+ export function renewedSessionClaims(
56
+ session: SessionClaims,
57
+ claims: OidcIdTokenClaims,
58
+ ): Omit<SessionClaims, "iat" | "exp"> | null {
59
+ if (claims.sub !== session.sub) return null;
60
+ return {
61
+ sub: session.sub,
62
+ name: typeof claims.name === "string" && claims.name ? claims.name : session.name,
63
+ email: typeof claims.email === "string" && claims.email ? claims.email : session.email,
64
+ picture: typeof claims.picture === "string" && claims.picture ? claims.picture : session.picture,
65
+ roles: Array.isArray(claims.roles) ? claims.roles.map(String) : session.roles,
66
+ };
67
+ }
@@ -1,12 +0,0 @@
1
- import {
2
- handleAsk
3
- } from "./chunk-M24JWPVN.js";
4
- import "./chunk-4XBU4J6B.js";
5
- import "./chunk-BV7IKH3N.js";
6
- import "./chunk-FNJ457SE.js";
7
- import "./chunk-COKARI7O.js";
8
- import "./chunk-RESCBSX6.js";
9
- import "./chunk-YTWPZE5O.js";
10
- export {
11
- handleAsk
12
- };