@konneal/engine 0.2.22 → 0.2.23

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.
@@ -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
- };