@hearth-auth/sdk 0.0.1 → 1.0.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.
Files changed (83) hide show
  1. package/dist/admin.d.ts +43 -0
  2. package/dist/admin.js +126 -0
  3. package/dist/admin.js.map +1 -0
  4. package/dist/browser-auth.d.ts +32 -0
  5. package/dist/browser-auth.js +99 -0
  6. package/dist/browser-auth.js.map +1 -0
  7. package/dist/claims.d.ts +86 -0
  8. package/dist/claims.js +137 -0
  9. package/dist/claims.js.map +1 -0
  10. package/dist/client.d.ts +77 -0
  11. package/dist/client.js +190 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors.d.ts +114 -0
  14. package/{src/errors.ts → dist/errors.js} +83 -97
  15. package/dist/errors.js.map +1 -0
  16. package/dist/hearth-client.d.ts +133 -0
  17. package/dist/hearth-client.js +192 -0
  18. package/dist/hearth-client.js.map +1 -0
  19. package/dist/hearth.d.ts +105 -0
  20. package/dist/hearth.js +109 -0
  21. package/dist/hearth.js.map +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +22 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/introspection-client.d.ts +59 -0
  26. package/dist/introspection-client.js +36 -0
  27. package/dist/introspection-client.js.map +1 -0
  28. package/dist/jwks-client.d.ts +28 -0
  29. package/dist/jwks-client.js +28 -0
  30. package/dist/jwks-client.js.map +1 -0
  31. package/dist/middleware.d.ts +38 -0
  32. package/dist/middleware.js +51 -0
  33. package/dist/middleware.js.map +1 -0
  34. package/dist/pkce.d.ts +64 -0
  35. package/dist/pkce.js +64 -0
  36. package/dist/pkce.js.map +1 -0
  37. package/dist/react.d.ts +32 -0
  38. package/dist/react.js +41 -0
  39. package/dist/react.js.map +1 -0
  40. package/dist/session-version-cache.d.ts +50 -0
  41. package/dist/session-version-cache.js +129 -0
  42. package/dist/session-version-cache.js.map +1 -0
  43. package/dist/types.d.ts +168 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +13 -4
  47. package/CHANGELOG.md +0 -12
  48. package/src/admin.ts +0 -157
  49. package/src/browser-auth.ts +0 -130
  50. package/src/claims.ts +0 -180
  51. package/src/client.ts +0 -251
  52. package/src/generated/google/api/annotations_pb.ts +0 -44
  53. package/src/generated/google/api/http_pb.ts +0 -467
  54. package/src/generated/hearth/authz/v1/authz_pb.ts +0 -593
  55. package/src/generated/hearth/cluster/v1/raft_pb.ts +0 -183
  56. package/src/generated/hearth/events/v1/audit_pb.ts +0 -886
  57. package/src/generated/hearth/identity/v1/identity_pb.ts +0 -1673
  58. package/src/generated/hearth/identity/v1/oauth_pb.ts +0 -1138
  59. package/src/generated/hearth/rbac/v1/rbac_pb.ts +0 -2000
  60. package/src/hearth-client.ts +0 -288
  61. package/src/hearth.ts +0 -224
  62. package/src/index.ts +0 -106
  63. package/src/introspection-client.ts +0 -83
  64. package/src/jwks-client.ts +0 -45
  65. package/src/middleware.ts +0 -82
  66. package/src/pkce.ts +0 -129
  67. package/src/react.tsx +0 -57
  68. package/src/session-version-cache.ts +0 -167
  69. package/src/types.ts +0 -188
  70. package/tests/admin-crud.test.ts +0 -97
  71. package/tests/auth-flow.test.ts +0 -75
  72. package/tests/authorize.test.ts +0 -386
  73. package/tests/claims.test.ts +0 -159
  74. package/tests/hasPermission.test.ts +0 -152
  75. package/tests/hearth-client.test.ts +0 -243
  76. package/tests/helpers.ts +0 -90
  77. package/tests/jwks.test.ts +0 -62
  78. package/tests/pkce.test.ts +0 -210
  79. package/tests/react-useHasPermission.test.tsx +0 -92
  80. package/tests/required-action.test.ts +0 -276
  81. package/tests/session-version.test.ts +0 -391
  82. package/tsconfig.json +0 -16
  83. package/vitest.config.ts +0 -8
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Low-level RFC 7662 token introspection client.
3
+ *
4
+ * Results are never cached — per RFC 7662 §2.1, token state can change
5
+ * at any time. Full error taxonomy will be added in §3.
6
+ */
7
+ export class IntrospectionClient {
8
+ endpoint;
9
+ clientId;
10
+ clientSecret;
11
+ httpTimeout;
12
+ constructor(config) {
13
+ this.endpoint = config.introspectionEndpoint;
14
+ this.clientId = config.clientId;
15
+ this.clientSecret = config.clientSecret;
16
+ this.httpTimeout = config.httpTimeout ?? 10_000;
17
+ }
18
+ /** Introspect a token. Never cached per RFC 7662 §2.1. */
19
+ async introspect(token) {
20
+ const credentials = btoa(`${this.clientId}:${this.clientSecret}`);
21
+ const resp = await fetch(this.endpoint, {
22
+ method: "POST",
23
+ headers: {
24
+ Authorization: `Basic ${credentials}`,
25
+ "Content-Type": "application/x-www-form-urlencoded",
26
+ },
27
+ body: new URLSearchParams({ token }),
28
+ signal: AbortSignal.timeout(this.httpTimeout),
29
+ });
30
+ if (!resp.ok) {
31
+ throw new Error(`Introspection endpoint returned HTTP ${resp.status}`);
32
+ }
33
+ return resp.json();
34
+ }
35
+ }
36
+ //# sourceMappingURL=introspection-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection-client.js","sourceRoot":"","sources":["../src/introspection-client.ts"],"names":[],"mappings":"AA8CA;;;;;GAKG;AACH,MAAM,OAAO,mBAAmB;IACb,QAAQ,CAAS;IACjB,QAAQ,CAAS;IACjB,YAAY,CAAS;IAC7B,WAAW,CAAS;IAE7B,YAAY,MAAiC;QAC3C,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,qBAAqB,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAChC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;QACxC,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC;IAClD,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,UAAU,CAAC,KAAa;QAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QAClE,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE;YACtC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,SAAS,WAAW,EAAE;gBACrC,cAAc,EAAE,mCAAmC;aACpD;YACD,IAAI,EAAE,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC;YACpC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;SAC9C,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,EAAkC,CAAC;IACrD,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import type { JsonWebKey } from "./types.js";
2
+ /** Configuration for {@link JwksClient}. */
3
+ export interface JwksClientConfig {
4
+ /** URL of the JWKS endpoint (e.g. from OIDC discovery `jwks_uri`). */
5
+ jwksUri: string;
6
+ /**
7
+ * Override cache TTL in milliseconds.
8
+ * When absent, the client respects `Cache-Control: max-age` from the JWKS
9
+ * response and falls back to 5 minutes.
10
+ */
11
+ ttl?: number;
12
+ /** Timeout for outbound HTTP calls in milliseconds. Default: 10 000. */
13
+ httpTimeout?: number;
14
+ }
15
+ /**
16
+ * Low-level JWKS fetcher.
17
+ *
18
+ * Fetches the JSON Web Key Set from the configured endpoint.
19
+ * Full caching and rotation logic will be added in §2.
20
+ */
21
+ export declare class JwksClient {
22
+ private readonly jwksUri;
23
+ readonly ttl: number | undefined;
24
+ readonly httpTimeout: number;
25
+ constructor(config: JwksClientConfig);
26
+ /** Fetch the current JWKS keys from the endpoint. */
27
+ fetchKeys(): Promise<JsonWebKey[]>;
28
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Low-level JWKS fetcher.
3
+ *
4
+ * Fetches the JSON Web Key Set from the configured endpoint.
5
+ * Full caching and rotation logic will be added in §2.
6
+ */
7
+ export class JwksClient {
8
+ jwksUri;
9
+ ttl;
10
+ httpTimeout;
11
+ constructor(config) {
12
+ this.jwksUri = config.jwksUri;
13
+ this.ttl = config.ttl;
14
+ this.httpTimeout = config.httpTimeout ?? 10_000;
15
+ }
16
+ /** Fetch the current JWKS keys from the endpoint. */
17
+ async fetchKeys() {
18
+ const resp = await fetch(this.jwksUri, {
19
+ signal: AbortSignal.timeout(this.httpTimeout),
20
+ });
21
+ if (!resp.ok) {
22
+ throw new Error(`JWKS fetch failed with HTTP ${resp.status}`);
23
+ }
24
+ const doc = (await resp.json());
25
+ return doc.keys;
26
+ }
27
+ }
28
+ //# sourceMappingURL=jwks-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwks-client.js","sourceRoot":"","sources":["../src/jwks-client.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AACH,MAAM,OAAO,UAAU;IACJ,OAAO,CAAS;IACxB,GAAG,CAAqB;IACxB,WAAW,CAAS;IAE7B,YAAY,MAAwB;QAClC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC;IAClD,CAAC;IAED,qDAAqD;IACrD,KAAK,CAAC,SAAS;QACb,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE;YACrC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;SAC9C,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAA2B,CAAC;QAC1D,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,38 @@
1
+ import type { HearthClient } from "./hearth-client.js";
2
+ import type { AccessTokenAuthorizationMode, AuthorizePermissionOptions } from "./types.js";
3
+ /** Options for {@link requirePermission}. */
4
+ export interface RequirePermissionOptions extends AuthorizePermissionOptions {
5
+ /**
6
+ * Which permission delivery mode the resource server expects.
7
+ *
8
+ * MUST be set explicitly — the middleware MUST NOT auto-detect the mode from
9
+ * JWT claim presence. Absence of a `permissions` claim in the token does not
10
+ * change behavior (per HEA-923 design constraint).
11
+ */
12
+ mode: AccessTokenAuthorizationMode;
13
+ /** HearthClient instance used for network calls in decision/introspection modes. */
14
+ client: HearthClient;
15
+ }
16
+ /**
17
+ * A synchronous-or-async gate that returns `true` iff the token holder has
18
+ * the given permission under the configured mode.
19
+ */
20
+ export type PermissionChecker = (token: string) => Promise<boolean>;
21
+ /**
22
+ * Returns a mode-aware permission checker for the given `permission`.
23
+ *
24
+ * Behaviour by mode:
25
+ * - **embedded** — decodes the JWT locally and checks the `permissions` claim.
26
+ * No network traffic. Returns `false` when the claim is absent; DOES NOT
27
+ * fall back to network (design constraint: absence of claims ≠ switch mode).
28
+ * - **decision** — calls `client.authorize(token, permission, opts)` which
29
+ * POSTs to `POST /oauth/authorize`. Fail-closed on network/server errors.
30
+ * - **introspection** — calls `client.introspectionClient().introspect(token)`,
31
+ * validates the echoed `mode` field if present, then checks the returned
32
+ * `permissions` array. Throws {@link AuthorizationModeMismatchError} if the
33
+ * server echoes a mode that differs from `opts.mode`.
34
+ *
35
+ * @param permission - The permission string to check (e.g. `"docs.write"`).
36
+ * @param opts - Mode, client reference, and optional scoping parameters.
37
+ */
38
+ export declare function requirePermission(permission: string, opts: RequirePermissionOptions): PermissionChecker;
@@ -0,0 +1,51 @@
1
+ import { decodeJwt } from "jose";
2
+ import { AuthorizationModeMismatchError } from "./errors.js";
3
+ /**
4
+ * Returns a mode-aware permission checker for the given `permission`.
5
+ *
6
+ * Behaviour by mode:
7
+ * - **embedded** — decodes the JWT locally and checks the `permissions` claim.
8
+ * No network traffic. Returns `false` when the claim is absent; DOES NOT
9
+ * fall back to network (design constraint: absence of claims ≠ switch mode).
10
+ * - **decision** — calls `client.authorize(token, permission, opts)` which
11
+ * POSTs to `POST /oauth/authorize`. Fail-closed on network/server errors.
12
+ * - **introspection** — calls `client.introspectionClient().introspect(token)`,
13
+ * validates the echoed `mode` field if present, then checks the returned
14
+ * `permissions` array. Throws {@link AuthorizationModeMismatchError} if the
15
+ * server echoes a mode that differs from `opts.mode`.
16
+ *
17
+ * @param permission - The permission string to check (e.g. `"docs.write"`).
18
+ * @param opts - Mode, client reference, and optional scoping parameters.
19
+ */
20
+ export function requirePermission(permission, opts) {
21
+ const { mode, client, organizationId, resource } = opts;
22
+ switch (mode) {
23
+ case "embedded":
24
+ return async (token) => {
25
+ let claims = null;
26
+ try {
27
+ claims = decodeJwt(token);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ const perms = claims["permissions"];
33
+ return Array.isArray(perms) && perms.includes(permission);
34
+ };
35
+ case "decision":
36
+ return async (token) => client.authorize(token, permission, { organizationId, resource });
37
+ case "introspection":
38
+ return async (token) => {
39
+ const ic = await client.introspectionClient();
40
+ const result = await ic.introspect(token);
41
+ // Validate mode echo when present — catches misconfigured deployments.
42
+ if (result.mode !== undefined && result.mode !== "introspection") {
43
+ throw new AuthorizationModeMismatchError("introspection", String(result.mode));
44
+ }
45
+ if (!result.active)
46
+ return false;
47
+ return (Array.isArray(result.permissions) && result.permissions.includes(permission));
48
+ };
49
+ }
50
+ }
51
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAwB7D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,IAA8B;IAE9B,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAExD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU;YACb,OAAO,KAAK,EAAE,KAAa,EAAoB,EAAE;gBAC/C,IAAI,MAAM,GAAmC,IAAI,CAAC;gBAClD,IAAI,CAAC;oBACH,MAAM,GAAG,SAAS,CAAC,KAAK,CAA4B,CAAC;gBACvD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;gBACpC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAC5D,CAAC,CAAC;QAEJ,KAAK,UAAU;YACb,OAAO,KAAK,EAAE,KAAa,EAAoB,EAAE,CAC/C,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEtE,KAAK,eAAe;YAClB,OAAO,KAAK,EAAE,KAAa,EAAoB,EAAE;gBAC/C,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;gBAC9C,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;gBAE1C,uEAAuE;gBACvE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;oBACjE,MAAM,IAAI,8BAA8B,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBACjF,CAAC;gBAED,IAAI,CAAC,MAAM,CAAC,MAAM;oBAAE,OAAO,KAAK,CAAC;gBACjC,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,CAC7E,CAAC;YACJ,CAAC,CAAC;IACN,CAAC;AACH,CAAC"}
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /** Minimal interface required by {@link startLogin} — satisfied by {@link HearthApiClient}. */
2
+ interface DiscoverySource {
3
+ discovery(): Promise<Record<string, unknown>>;
4
+ }
5
+ /** Generate a cryptographically random RFC 7636 code verifier (256-bit / 32 bytes). */
6
+ export declare function generateCodeVerifier(): string;
7
+ /** Derive the S256 code challenge from a verifier (RFC 7636 §4.2). */
8
+ export declare function generateCodeChallenge(verifier: string): Promise<string>;
9
+ /** Options for {@link buildAuthorizationUrl}. */
10
+ export interface BuildAuthorizationUrlOptions {
11
+ /** OIDC `authorization_endpoint` from the discovery document. */
12
+ authorizationEndpoint: string;
13
+ /** OAuth 2.0 client ID. */
14
+ clientId: string;
15
+ /** Redirect URI registered for this client. */
16
+ redirectUri: string;
17
+ /** Base64url-encoded S256 code challenge (from {@link generateCodeChallenge}). */
18
+ codeChallenge: string;
19
+ /** OAuth 2.0 scope string. Default: `"openid profile email"`. */
20
+ scope?: string;
21
+ /** CSRF state token. Auto-generated (16 random bytes) when absent. */
22
+ state?: string;
23
+ }
24
+ /** Return value of {@link buildAuthorizationUrl}. */
25
+ export interface AuthorizationUrlResult {
26
+ /** Full authorization redirect URL to navigate the browser to. */
27
+ url: string;
28
+ /** State value embedded in the URL — persist for CSRF validation in the callback. */
29
+ state: string;
30
+ }
31
+ /** Build the full authorization redirect URL for an RFC 7636 PKCE flow. */
32
+ export declare function buildAuthorizationUrl(opts: BuildAuthorizationUrlOptions): AuthorizationUrlResult;
33
+ /** Options for {@link startLogin}. */
34
+ export interface StartLoginOptions {
35
+ /** OAuth 2.0 client ID. */
36
+ clientId: string;
37
+ /** Redirect URI registered for this client. */
38
+ redirectUri: string;
39
+ /** OAuth 2.0 scope string. Default: `"openid profile email"`. */
40
+ scope?: string;
41
+ /** CSRF state token. Auto-generated when absent. */
42
+ state?: string;
43
+ }
44
+ /** Return value of {@link startLogin}. */
45
+ export interface StartLoginResult {
46
+ /** Full authorization URL — redirect the browser here to begin login. */
47
+ url: string;
48
+ /** OAuth 2.0 state value — persist for CSRF validation in the callback. */
49
+ state: string;
50
+ /**
51
+ * RFC 7636 code verifier — persist (e.g. `sessionStorage`) and pass as
52
+ * `codeVerifier` to `handleCallback()` during the token exchange step.
53
+ */
54
+ codeVerifier: string;
55
+ }
56
+ /**
57
+ * One-shot PKCE login initiation: discovers the authorization endpoint,
58
+ * generates a code verifier/challenge, and builds the redirect URL.
59
+ *
60
+ * The caller MUST persist `codeVerifier` and `state` (e.g. in `sessionStorage`)
61
+ * before navigating to `url`, and pass them to `handleCallback()` on return.
62
+ */
63
+ export declare function startLogin(client: DiscoverySource, opts: StartLoginOptions): Promise<StartLoginResult>;
64
+ export {};
package/dist/pkce.js ADDED
@@ -0,0 +1,64 @@
1
+ /** Generate a cryptographically random RFC 7636 code verifier (256-bit / 32 bytes). */
2
+ export function generateCodeVerifier() {
3
+ const bytes = new Uint8Array(32);
4
+ crypto.getRandomValues(bytes);
5
+ return base64urlEncode(bytes);
6
+ }
7
+ /** Derive the S256 code challenge from a verifier (RFC 7636 §4.2). */
8
+ export async function generateCodeChallenge(verifier) {
9
+ const data = new TextEncoder().encode(verifier);
10
+ const hash = await crypto.subtle.digest("SHA-256", data);
11
+ return base64urlEncode(new Uint8Array(hash));
12
+ }
13
+ /** Build the full authorization redirect URL for an RFC 7636 PKCE flow. */
14
+ export function buildAuthorizationUrl(opts) {
15
+ const state = opts.state ?? generateState();
16
+ const params = new URLSearchParams({
17
+ response_type: "code",
18
+ client_id: opts.clientId,
19
+ redirect_uri: opts.redirectUri,
20
+ code_challenge: opts.codeChallenge,
21
+ code_challenge_method: "S256",
22
+ scope: opts.scope ?? "openid profile email",
23
+ state,
24
+ });
25
+ return { url: `${opts.authorizationEndpoint}?${params.toString()}`, state };
26
+ }
27
+ /**
28
+ * One-shot PKCE login initiation: discovers the authorization endpoint,
29
+ * generates a code verifier/challenge, and builds the redirect URL.
30
+ *
31
+ * The caller MUST persist `codeVerifier` and `state` (e.g. in `sessionStorage`)
32
+ * before navigating to `url`, and pass them to `handleCallback()` on return.
33
+ */
34
+ export async function startLogin(client, opts) {
35
+ const doc = await client.discovery();
36
+ const authorizationEndpoint = doc["authorization_endpoint"];
37
+ if (!authorizationEndpoint) {
38
+ throw new Error("startLogin: authorization_endpoint not found in OIDC discovery document");
39
+ }
40
+ const codeVerifier = generateCodeVerifier();
41
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
42
+ const { url, state } = buildAuthorizationUrl({
43
+ authorizationEndpoint,
44
+ clientId: opts.clientId,
45
+ redirectUri: opts.redirectUri,
46
+ codeChallenge,
47
+ scope: opts.scope,
48
+ state: opts.state,
49
+ });
50
+ return { url, state, codeVerifier };
51
+ }
52
+ function generateState() {
53
+ const bytes = new Uint8Array(16);
54
+ crypto.getRandomValues(bytes);
55
+ return base64urlEncode(bytes);
56
+ }
57
+ function base64urlEncode(input) {
58
+ let binary = "";
59
+ for (const byte of input) {
60
+ binary += String.fromCharCode(byte);
61
+ }
62
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
63
+ }
64
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAKA,uFAAuF;AACvF,MAAM,UAAU,oBAAoB;IAClC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,QAAgB;IAC1D,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACzD,OAAO,eAAe,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC;AA0BD,2EAA2E;AAC3E,MAAM,UAAU,qBAAqB,CACnC,IAAkC;IAElC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,aAAa,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,aAAa,EAAE,MAAM;QACrB,SAAS,EAAE,IAAI,CAAC,QAAQ;QACxB,YAAY,EAAE,IAAI,CAAC,WAAW;QAC9B,cAAc,EAAE,IAAI,CAAC,aAAa;QAClC,qBAAqB,EAAE,MAAM;QAC7B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,sBAAsB;QAC3C,KAAK;KACN,CAAC,CAAC;IACH,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,qBAAqB,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;AAC9E,CAAC;AA2BD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAuB,EACvB,IAAuB;IAEvB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;IACrC,MAAM,qBAAqB,GAAG,GAAG,CAAC,wBAAwB,CAAuB,CAAC;IAClF,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,yEAAyE,CAC1E,CAAC;IACJ,CAAC;IACD,MAAM,YAAY,GAAG,oBAAoB,EAAE,CAAC;IAC5C,MAAM,aAAa,GAAG,MAAM,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAChE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAAC;QAC3C,qBAAqB;QACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,aAAa;QACb,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC,CAAC;IACH,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;AACtC,CAAC;AAED,SAAS,aAAa;IACpB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,eAAe,CAAC,KAAiB;IACxC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC"}
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import type { HearthFacade } from "./hearth.js";
3
+ /**
4
+ * React context carrying a {@link HearthFacade} down the tree.
5
+ *
6
+ * The default value is `null`; the hooks treat a `null` context as
7
+ * unauthenticated and return `false`.
8
+ */
9
+ export declare const HearthContext: React.Context<HearthFacade | null>;
10
+ /** Props for {@link HearthProvider}. */
11
+ export interface HearthProviderProps {
12
+ client: HearthFacade;
13
+ children: React.ReactNode;
14
+ }
15
+ /**
16
+ * Provides a {@link HearthFacade} to descendants via {@link HearthContext}.
17
+ *
18
+ * Wrap your React tree once with this after calling `createHearth(...)`.
19
+ */
20
+ export declare function HearthProvider(props: HearthProviderProps): React.ReactElement;
21
+ /**
22
+ * Returns `true` iff the nearest {@link HearthProvider} client reports
23
+ * the permission as present in the JWT claim set. Returns `false`
24
+ * when no provider is mounted.
25
+ */
26
+ export declare function useHasPermission(permission: string): boolean;
27
+ /** Returns `true` iff the JWT `roles` claim contains `role`. */
28
+ export declare function useHasRole(role: string): boolean;
29
+ /** Returns `true` iff the JWT `groups` claim contains `group`. */
30
+ export declare function useInGroup(group: string): boolean;
31
+ /** Returns `true` iff the JWT `oid` claim equals `org`. */
32
+ export declare function useInOrg(org: string): boolean;
package/dist/react.js ADDED
@@ -0,0 +1,41 @@
1
+ import * as React from "react";
2
+ /**
3
+ * React context carrying a {@link HearthFacade} down the tree.
4
+ *
5
+ * The default value is `null`; the hooks treat a `null` context as
6
+ * unauthenticated and return `false`.
7
+ */
8
+ export const HearthContext = React.createContext(null);
9
+ /**
10
+ * Provides a {@link HearthFacade} to descendants via {@link HearthContext}.
11
+ *
12
+ * Wrap your React tree once with this after calling `createHearth(...)`.
13
+ */
14
+ export function HearthProvider(props) {
15
+ return React.createElement(HearthContext.Provider, { value: props.client }, props.children);
16
+ }
17
+ /**
18
+ * Returns `true` iff the nearest {@link HearthProvider} client reports
19
+ * the permission as present in the JWT claim set. Returns `false`
20
+ * when no provider is mounted.
21
+ */
22
+ export function useHasPermission(permission) {
23
+ const client = React.useContext(HearthContext);
24
+ return client !== null && client.hasPermission(permission);
25
+ }
26
+ /** Returns `true` iff the JWT `roles` claim contains `role`. */
27
+ export function useHasRole(role) {
28
+ const client = React.useContext(HearthContext);
29
+ return client !== null && client.hasRole(role);
30
+ }
31
+ /** Returns `true` iff the JWT `groups` claim contains `group`. */
32
+ export function useInGroup(group) {
33
+ const client = React.useContext(HearthContext);
34
+ return client !== null && client.inGroup(group);
35
+ }
36
+ /** Returns `true` iff the JWT `oid` claim equals `org`. */
37
+ export function useInOrg(org) {
38
+ const client = React.useContext(HearthContext);
39
+ return client !== null && client.inOrg(org);
40
+ }
41
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,CAAsB,IAAI,CAAC,CAAC;AAQ5E;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,OAAO,KAAK,CAAC,aAAa,CACxB,aAAa,CAAC,QAAQ,EACtB,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,EACvB,KAAK,CAAC,QAAQ,CACf,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB;IACjD,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAC/C,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;AAC7D,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAC/C,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAC/C,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAC/C,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { SessionVersionConfig } from "./types.js";
2
+ /**
3
+ * Client-side cache of per-session minimum accepted `sv` values.
4
+ *
5
+ * Polls `GET /oauth/session-versions` at `cfg.pollIntervalMs` intervals and
6
+ * applies delta entries to an in-memory `Map<sessionId, bigint>`. Used by
7
+ * `createHearth()` to validate the `sv` claim in access tokens without any
8
+ * per-request network call.
9
+ *
10
+ * Background poll errors are swallowed; the cache age then grows and eventually
11
+ * trips the stale threshold, triggering fail-closed behaviour (§ 8.1).
12
+ */
13
+ export declare class SessionVersionCache {
14
+ private readonly baseUrl;
15
+ private readonly realmId;
16
+ private readonly cfg;
17
+ private readonly versions;
18
+ private lastRefreshed;
19
+ private seq;
20
+ private pollTimer;
21
+ constructor(baseUrl: string, realmId: string, cfg: SessionVersionConfig);
22
+ /**
23
+ * Kicks off the initial snapshot fetch (async, non-blocking) and starts the
24
+ * background poll loop. Call once after construction.
25
+ *
26
+ * If `staleThresholdMs <= pollIntervalMs` a console warning is emitted.
27
+ * Until the first snapshot completes, `age()` returns `Infinity` which
28
+ * will trip the stale threshold if `staleThresholdMs` is finite.
29
+ */
30
+ start(): void;
31
+ /** Stops the background poll timer. Call when disposing the Hearth facade. */
32
+ stop(): void;
33
+ /** Returns milliseconds since the cache was last successfully refreshed. */
34
+ age(): number;
35
+ /**
36
+ * Validates the `sv` claim against the local cache.
37
+ *
38
+ * - Absent `sv` or absent `sid` → no-op (backward compat, RFC § 8.2).
39
+ * - Cache age > `staleThresholdMs` → throws {@link SessionVersionCacheStaleError}.
40
+ * - `sv < minSv` → throws {@link SessionVersionRevokedError}.
41
+ *
42
+ * When `onStale` is `"introspect"`, callers should catch
43
+ * {@link SessionVersionCacheStaleError} and fall back to the introspection
44
+ * endpoint, which performs a fresh server-side check.
45
+ */
46
+ validateSv(sv: bigint | undefined, sessionId: string | undefined): void;
47
+ private fetchSnapshot;
48
+ private schedulePoll;
49
+ private poll;
50
+ }
@@ -0,0 +1,129 @@
1
+ import { SessionVersionCacheStaleError, SessionVersionRevokedError, } from "./errors.js";
2
+ /**
3
+ * Client-side cache of per-session minimum accepted `sv` values.
4
+ *
5
+ * Polls `GET /oauth/session-versions` at `cfg.pollIntervalMs` intervals and
6
+ * applies delta entries to an in-memory `Map<sessionId, bigint>`. Used by
7
+ * `createHearth()` to validate the `sv` claim in access tokens without any
8
+ * per-request network call.
9
+ *
10
+ * Background poll errors are swallowed; the cache age then grows and eventually
11
+ * trips the stale threshold, triggering fail-closed behaviour (§ 8.1).
12
+ */
13
+ export class SessionVersionCache {
14
+ baseUrl;
15
+ realmId;
16
+ cfg;
17
+ versions = new Map();
18
+ lastRefreshed = 0;
19
+ seq = 0;
20
+ pollTimer = null;
21
+ constructor(baseUrl, realmId, cfg) {
22
+ this.baseUrl = baseUrl.replace(/\/$/, "");
23
+ this.realmId = realmId;
24
+ this.cfg = cfg;
25
+ }
26
+ /**
27
+ * Kicks off the initial snapshot fetch (async, non-blocking) and starts the
28
+ * background poll loop. Call once after construction.
29
+ *
30
+ * If `staleThresholdMs <= pollIntervalMs` a console warning is emitted.
31
+ * Until the first snapshot completes, `age()` returns `Infinity` which
32
+ * will trip the stale threshold if `staleThresholdMs` is finite.
33
+ */
34
+ start() {
35
+ if (this.cfg.staleThresholdMs <= this.cfg.pollIntervalMs) {
36
+ console.warn("[hearth] sessionVersions.staleThresholdMs must be > pollIntervalMs " +
37
+ `(stale=${this.cfg.staleThresholdMs}ms, poll=${this.cfg.pollIntervalMs}ms). ` +
38
+ "Recommended: staleThresholdMs = pollIntervalMs × 3.");
39
+ }
40
+ void this.fetchSnapshot().catch(() => undefined);
41
+ this.schedulePoll();
42
+ }
43
+ /** Stops the background poll timer. Call when disposing the Hearth facade. */
44
+ stop() {
45
+ if (this.pollTimer !== null) {
46
+ clearTimeout(this.pollTimer);
47
+ this.pollTimer = null;
48
+ }
49
+ }
50
+ /** Returns milliseconds since the cache was last successfully refreshed. */
51
+ age() {
52
+ if (this.lastRefreshed === 0)
53
+ return Number.POSITIVE_INFINITY;
54
+ return Date.now() - this.lastRefreshed;
55
+ }
56
+ /**
57
+ * Validates the `sv` claim against the local cache.
58
+ *
59
+ * - Absent `sv` or absent `sid` → no-op (backward compat, RFC § 8.2).
60
+ * - Cache age > `staleThresholdMs` → throws {@link SessionVersionCacheStaleError}.
61
+ * - `sv < minSv` → throws {@link SessionVersionRevokedError}.
62
+ *
63
+ * When `onStale` is `"introspect"`, callers should catch
64
+ * {@link SessionVersionCacheStaleError} and fall back to the introspection
65
+ * endpoint, which performs a fresh server-side check.
66
+ */
67
+ validateSv(sv, sessionId) {
68
+ if (sv === undefined || sessionId === undefined)
69
+ return;
70
+ const ageMs = this.age();
71
+ if (ageMs > this.cfg.staleThresholdMs) {
72
+ throw new SessionVersionCacheStaleError(isFinite(ageMs) ? ageMs : -1, this.cfg.onStale);
73
+ }
74
+ const minSv = this.versions.get(sessionId) ?? 1n;
75
+ if (sv < minSv) {
76
+ throw new SessionVersionRevokedError(sessionId, sv, minSv);
77
+ }
78
+ }
79
+ // ── Private ─────────────────────────────────────────────────────────────────
80
+ async fetchSnapshot() {
81
+ const url = `${this.baseUrl}/oauth/session-versions/snapshot?realm=${encodeURIComponent(this.realmId)}`;
82
+ const resp = await fetch(url, {
83
+ headers: { Authorization: `Bearer ${this.cfg.serviceToken}` },
84
+ });
85
+ if (!resp.ok) {
86
+ throw new Error(`SV snapshot fetch failed: HTTP ${resp.status}`);
87
+ }
88
+ const data = (await resp.json());
89
+ this.versions.clear();
90
+ for (const [sid, minSv] of Object.entries(data.versions)) {
91
+ this.versions.set(sid, BigInt(minSv));
92
+ }
93
+ this.seq = data.current_seq;
94
+ this.lastRefreshed = Date.now();
95
+ }
96
+ schedulePoll() {
97
+ this.pollTimer = setTimeout(() => {
98
+ void this.poll()
99
+ .catch(() => undefined)
100
+ .finally(() => this.schedulePoll());
101
+ }, this.cfg.pollIntervalMs);
102
+ }
103
+ async poll() {
104
+ const url = `${this.baseUrl}/oauth/session-versions?since=${this.seq}` +
105
+ `&realm=${encodeURIComponent(this.realmId)}`;
106
+ const resp = await fetch(url, {
107
+ headers: { Authorization: `Bearer ${this.cfg.serviceToken}` },
108
+ });
109
+ if (resp.status === 204) {
110
+ this.lastRefreshed = Date.now();
111
+ return;
112
+ }
113
+ if (resp.status === 400) {
114
+ // Sequence predates retention window — must re-seed from snapshot.
115
+ await this.fetchSnapshot();
116
+ return;
117
+ }
118
+ if (!resp.ok) {
119
+ throw new Error(`SV delta poll failed: HTTP ${resp.status}`);
120
+ }
121
+ const data = (await resp.json());
122
+ for (const delta of data.deltas) {
123
+ this.versions.set(delta.session_id, BigInt(delta.min_sv));
124
+ }
125
+ this.seq = data.next_seq;
126
+ this.lastRefreshed = Date.now();
127
+ }
128
+ }
129
+ //# sourceMappingURL=session-version-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-version-cache.js","sourceRoot":"","sources":["../src/session-version-cache.ts"],"names":[],"mappings":"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"}