@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.
- package/dist/ask-OO6UUIFF.js +12 -0
- package/dist/{chunk-RESCBSX6.js → chunk-434NRPSS.js} +1 -1
- package/dist/{chunk-BV7IKH3N.js → chunk-FYGJDJPN.js} +2 -2
- package/dist/chunk-HXWDVAVN.js +24441 -0
- package/dist/{chunk-YTWPZE5O.js → chunk-HYI32HMI.js} +0 -32
- package/dist/{chunk-M24JWPVN.js → chunk-IM5KMIL6.js} +9 -9
- package/dist/{chunk-COKARI7O.js → chunk-IOZAZA7Y.js} +2 -2
- package/dist/{chunk-IROEVHEV.js → chunk-PZLYPUYJ.js} +4 -4
- package/dist/{chunk-FNJ457SE.js → chunk-RZ3UZVRR.js} +1 -1
- package/dist/livedata.d.ts +3 -4
- package/dist/oidc-refresh.d.ts +19 -0
- package/dist/oidc.d.ts +1 -0
- package/dist/refresh-core.d.ts +28 -0
- package/dist/search-OJ776DMH.js +12 -0
- package/dist/worker_mcp/src/index.js +3 -3
- package/dist/worker_public/src/config.js +2 -2
- package/dist/worker_public/src/index.js +9 -9
- package/dist/worker_public/src/profile.js +1 -1
- package/dist/worker_public/src/refusal.js +2 -2
- package/dist/worker_public/src/requestScope.js +3 -3
- package/package.json +2 -3
- package/workers/worker_public/src/aggregation.ts +1 -1
- package/workers/worker_public/src/auth.ts +45 -10
- package/workers/worker_public/src/codecs.ts +33 -32
- package/workers/worker_public/src/conditions.ts +2 -2
- package/workers/worker_public/src/livedata.ts +35 -0
- package/workers/worker_public/src/oidc-refresh.ts +76 -0
- package/workers/worker_public/src/oidc.ts +1 -0
- package/workers/worker_public/src/refresh-core.ts +67 -0
- package/dist/ask-HLELROYT.js +0 -12
- package/dist/chunk-4XBU4J6B.js +0 -4149
- 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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/ask-HLELROYT.js
DELETED
|
@@ -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
|
-
};
|