@multiplatform.one/keycloak 6.6.0 → 7.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 (82) hide show
  1. package/dist/cjs/index.cjs +129 -55
  2. package/dist/cjs/index.native.cjs +239 -130
  3. package/dist/esm/index.js +131 -57
  4. package/dist/esm/index.native.js +239 -130
  5. package/package.json +16 -11
  6. package/src/betterAuth/client.ts +3 -1
  7. package/src/betterAuth/expoAuthFlow.native.spec.ts +307 -0
  8. package/src/betterAuth/expoAuthFlow.native.ts +189 -55
  9. package/src/frappeToken.native.spec.ts +104 -0
  10. package/src/frappeToken.native.ts +46 -2
  11. package/src/frappeToken.spec.ts +141 -0
  12. package/src/frappeToken.ts +106 -14
  13. package/src/index.ts +1 -1
  14. package/src/keycloak/index.ts +24 -7
  15. package/src/keycloak/index.webext.ts +16 -0
  16. package/src/oauth/authorizationUrl.spec.ts +83 -0
  17. package/src/oauth/authorizationUrl.ts +74 -0
  18. package/src/oauth/callback.spec.ts +72 -0
  19. package/src/oauth/callback.ts +46 -0
  20. package/src/oauth/callbackPage.spec.ts +61 -0
  21. package/src/oauth/callbackPage.ts +57 -0
  22. package/src/oauth/endpoints.spec.ts +193 -0
  23. package/src/oauth/endpoints.ts +135 -0
  24. package/src/oauth/errors.ts +18 -0
  25. package/src/oauth/form.spec.ts +35 -0
  26. package/src/oauth/form.ts +30 -0
  27. package/src/oauth/idToken.spec.ts +191 -0
  28. package/src/oauth/idToken.ts +153 -0
  29. package/src/oauth/index.ts +10 -0
  30. package/src/oauth/jwt.ts +90 -0
  31. package/src/oauth/loopbackFlow.spec.ts +226 -0
  32. package/src/oauth/loopbackFlow.ts +114 -0
  33. package/src/oauth/pkce.spec.ts +60 -0
  34. package/src/oauth/pkce.ts +80 -0
  35. package/src/oauth/tokenExchange.spec.ts +331 -0
  36. package/src/oauth/tokenExchange.ts +232 -0
  37. package/src/one.ts +30 -3
  38. package/src/provider/AfterAuth.tsx +7 -0
  39. package/src/provider/authProvider/index.native.tsx +37 -4
  40. package/src/session/index.ts +2 -2
  41. package/src/state.ts +5 -2
  42. package/types/betterAuth/client.d.ts +2 -791
  43. package/types/betterAuth/client.d.ts.map +1 -1
  44. package/types/betterAuth/expoAuthFlow.native.d.ts +50 -3
  45. package/types/betterAuth/expoAuthFlow.native.d.ts.map +1 -1
  46. package/types/frappeToken.d.ts +14 -1
  47. package/types/frappeToken.d.ts.map +1 -1
  48. package/types/frappeToken.native.d.ts +16 -2
  49. package/types/frappeToken.native.d.ts.map +1 -1
  50. package/types/index.d.ts +1 -1
  51. package/types/index.d.ts.map +1 -1
  52. package/types/keycloak/index.d.ts.map +1 -1
  53. package/types/keycloak/index.webext.d.ts.map +1 -1
  54. package/types/oauth/authorizationUrl.d.ts +32 -0
  55. package/types/oauth/authorizationUrl.d.ts.map +1 -0
  56. package/types/oauth/callback.d.ts +26 -0
  57. package/types/oauth/callback.d.ts.map +1 -0
  58. package/types/oauth/callbackPage.d.ts +34 -0
  59. package/types/oauth/callbackPage.d.ts.map +1 -0
  60. package/types/oauth/endpoints.d.ts +68 -0
  61. package/types/oauth/endpoints.d.ts.map +1 -0
  62. package/types/oauth/errors.d.ts +17 -0
  63. package/types/oauth/errors.d.ts.map +1 -0
  64. package/types/oauth/form.d.ts +16 -0
  65. package/types/oauth/form.d.ts.map +1 -0
  66. package/types/oauth/idToken.d.ts +83 -0
  67. package/types/oauth/idToken.d.ts.map +1 -0
  68. package/types/oauth/index.d.ts +11 -0
  69. package/types/oauth/index.d.ts.map +1 -0
  70. package/types/oauth/jwt.d.ts +31 -0
  71. package/types/oauth/jwt.d.ts.map +1 -0
  72. package/types/oauth/loopbackFlow.d.ts +64 -0
  73. package/types/oauth/loopbackFlow.d.ts.map +1 -0
  74. package/types/oauth/pkce.d.ts +49 -0
  75. package/types/oauth/pkce.d.ts.map +1 -0
  76. package/types/oauth/tokenExchange.d.ts +89 -0
  77. package/types/oauth/tokenExchange.d.ts.map +1 -0
  78. package/types/one.d.ts +7 -0
  79. package/types/one.d.ts.map +1 -1
  80. package/types/provider/AfterAuth.d.ts.map +1 -1
  81. package/types/provider/authProvider/index.native.d.ts.map +1 -1
  82. package/types/state.d.ts.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/betterAuth/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAU6hC,CAAC;iBAAe,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAAy3C,CAAC;aAAwC,CAAC;mBAA2C,CAAC;eAAiC,CAAC;;;;;iBAA+U,CAAC;iBAAmC,CAAC;;YAAmD,CAAC;gBAAuC,CAAC;gBAA2C,CAAC;sBAAwC,CAAC;cAAwC,CAAC;cAA8C,CAAC;eAA+B,CAAC;mBAAyG,CAAC;yBAAuB,CAAC;;eAAyC,CAAC;;;aAA0G,CAAC;YAA+B,CAAC;;;;;;;;;;;;YAA+e,CAAC;aAAgB,CAAC;cAAiB,CAAC;cAAiB,CAAC;;aAA8F,CAAC;oBAAiE,CAAC;cAAgC,CAAC;mBAAkG,CAAC;yBAA0E,CAAC;qBAAwC,CAAC;;;uBAAkF,CAAC;+GAAuL,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAHzuJ;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/betterAuth/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQrD,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAOnF;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC"}
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Hand-rolled Better Auth flow for native (Expo) — fetch + expo-web-browser +
3
- * SecureStore, NO better-auth client packages.
2
+ * Hand-rolled Better Auth flow for native (Expo) — fetch + expo-web-browser
3
+ * (ASWebAuthenticationSession / Chrome Custom Tabs) + SecureStore, NO
4
+ * better-auth client packages.
4
5
  *
5
6
  * Why not @better-auth/expo/client: its import graph drags better-auth's
6
7
  * cookies module -> @better-auth/core db schema -> zod into the Hermes bundle,
@@ -12,7 +13,14 @@
12
13
  */
13
14
  export interface ExpoAuthFlow {
14
15
  signIn(provider: string, callbackURL?: string): Promise<void>;
15
- signOut(): Promise<void>;
16
+ /**
17
+ * Ends the Better Auth session AND (when the server exposes the
18
+ * keycloak-sso plugin) the Keycloak SSO session, riding the OIDC
19
+ * end-session endpoint through the same browser that holds the SSO
20
+ * cookie. `postLogoutRedirectUri` is where that browser hands control
21
+ * back (defaults to the app's own runtime URL).
22
+ */
23
+ signOut(postLogoutRedirectUri?: string): Promise<void>;
16
24
  getSession(): Promise<{
17
25
  user?: {
18
26
  id: string;
@@ -26,6 +34,45 @@ export interface ExpoAuthFlow {
26
34
  accessTokenExpiresAt?: string;
27
35
  } | null>;
28
36
  }
37
+ /**
38
+ * Rewrite a loopback host for the device the app actually runs on:
39
+ * `localhost` inside the iOS simulator can resolve to `::1`, where unrelated
40
+ * daemons may squat the port (the reason the web env pins 127.0.0.1), and
41
+ * Android emulators reach the host machine at 10.0.2.2, never loopback.
42
+ * Non-loopback hosts (LAN IPs, real domains) pass through untouched.
43
+ *
44
+ * String-based on purpose: React Native's minimal URL polyfill does not
45
+ * implement the `hostname` SETTER (reads work, mutation throws), so a
46
+ * new-URL-and-assign rewrite silently no-ops on device.
47
+ */
48
+ export declare function rewriteLoopbackHostForNative(configured: string): string;
49
+ /**
50
+ * Loosen a return URL for prefix matching: drop query/fragment and collapse
51
+ * the slash run after the scheme. `Linking.createURL("/")` yields
52
+ * `multiplatform-one:///` (empty authority) but iOS delivers the redirect as
53
+ * `multiplatform-one://?cookie=…` — a strict startsWith between the two
54
+ * misses, leaving the sign-in promise pending forever (observed on the
55
+ * 2026-08-13 simulator run).
56
+ */
57
+ export declare function normalizeForReturnMatch(url: string): string;
58
+ /**
59
+ * Give a bare-scheme URL (`multiplatform-one:///` — what Linking.createURL
60
+ * emits for custom app schemes: empty authority) a host segment. OIDC
61
+ * post-logout redirects need it: Keycloak's redirect builder (Java
62
+ * UriBuilder) throws "empty host" appending the `state` param to an
63
+ * authority-less URI, answering 500 "We are sorry" instead of returning to
64
+ * the app. URLs that already carry an authority (exp://host:port/…) pass
65
+ * through untouched.
66
+ */
67
+ export declare function ensureReturnUrlAuthority(url: string, segment: string): string;
68
+ /**
69
+ * Pull the Better Auth session cookie out of an OAuth return URL without
70
+ * `new URL()`. React Native's URL polyfill throws or drops search on
71
+ * authority-less custom schemes (`multiplatform-one://?cookie=…` — the
72
+ * shape iOS actually delivers). Mirrors `URLSearchParams.get("cookie")`
73
+ * including decodeURIComponent.
74
+ */
75
+ export declare function extractCookieFromReturnUrl(returnUrl: string): string | null;
29
76
  export declare function getActiveExpoAuthFlow(): ExpoAuthFlow | null;
30
77
  export declare function createExpoAuthFlow(baseURL: string, scheme?: string): ExpoAuthFlow;
31
78
  //# sourceMappingURL=expoAuthFlow.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"expoAuthFlow.native.d.ts","sourceRoot":"","sources":["../../src/betterAuth/expoAuthFlow.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6EH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACvF,mEAAmE;IACnE,cAAc,CACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CAC5E;AAOD,wBAAgB,qBAAqB,IAAI,YAAY,GAAG,IAAI,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAkIjF"}
1
+ {"version":3,"file":"expoAuthFlow.native.d.ts","sourceRoot":"","sources":["../../src/betterAuth/expoAuthFlow.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmFH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,UAAU,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACvF,mEAAmE;IACnE,cAAc,CACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CAC5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB3E;AAqED,wBAAgB,qBAAqB,IAAI,YAAY,GAAG,IAAI,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAgHjF"}
@@ -11,6 +11,12 @@
11
11
  * cookie) and sends `Authorization: Bearer <token>` on every REST request
12
12
  * and socket handshake — the same transport native uses. The bench's
13
13
  * frappe_keycloak auth hook validates the JWT against the realm public key.
14
+ *
15
+ * Failures are classified, not collapsed:
16
+ * - 401/403, or 400 `FAILED_TO_GET_ACCESS_TOKEN` (server already decided
17
+ * the refresh token is dead): signed-out cooldown + session notify.
18
+ * - 5xx `TOKEN_REFRESH_UNAVAILABLE`, other 5xx, network: retry with
19
+ * backoff, keep the session. A Keycloak restart must not look like logout.
14
20
  */
15
21
  /**
16
22
  * Returns a valid Keycloak access token, or undefined when signed out.
@@ -19,9 +25,16 @@
19
25
  * concurrent data request: parallel callers share the in-flight fetch, and a
20
26
  * signed-out answer short-circuits further attempts for a few seconds. A real
21
27
  * sign-in arrives via the OAuth redirect (full navigation), which resets this
22
- * module state anyway.
28
+ * module state anyway. Transient Keycloak failures retry before giving up,
29
+ * and never start the signed-out cooldown.
23
30
  */
24
31
  export declare function getKeycloakBearerToken(): Promise<string | undefined>;
25
32
  /** Drop the cached token (sign-out / fresh sign-in). */
26
33
  export declare function clearKeycloakBearerToken(): void;
34
+ /**
35
+ * Bust the client cache and re-fetch when the tab is focused again, so a
36
+ * front-channel logout (or Keycloak recovery) in another window is noticed
37
+ * without a full reload. Returns an unsubscribe. No-op on the server.
38
+ */
39
+ export declare function watchKeycloakBearerTokenRevalidate(): () => void;
27
40
  //# sourceMappingURL=frappeToken.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU1E;AAuCD,wDAAwD;AACxD,wBAAgB,wBAAwB,SAIvC"}
1
+ {"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgDH;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW1E;AA4DD,wDAAwD;AACxD,wBAAgB,wBAAwB,SAKvC;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,IAAI,CAiB/D"}
@@ -8,8 +8,22 @@
8
8
  * from better-auth's /get-access-token endpoint (which refreshes server-side
9
9
  * when expired), authenticated with the SecureStore cookie jar.
10
10
  */
11
- /** Returns a valid Keycloak access token, or undefined when signed out. */
11
+ /**
12
+ * Returns a valid Keycloak access token, or undefined when signed out.
13
+ *
14
+ * Signed-out clients get a single 401 per cooldown window instead of one per
15
+ * concurrent collection sync: parallel callers share the in-flight fetch, and
16
+ * a signed-out answer short-circuits further attempts for a few seconds. The
17
+ * AuthProvider clears this module state on sign-in / sign-out.
18
+ */
12
19
  export declare function getKeycloakBearerToken(): Promise<string | undefined>;
13
- /** Drop the cached token (sign-out). */
20
+ /** Drop the cached token (sign-out / fresh sign-in). */
14
21
  export declare function clearKeycloakBearerToken(): void;
22
+ /**
23
+ * Bust the client cache and re-fetch when the app returns to the
24
+ * foreground, so a completed ASWebAuthenticationSession (or an SLO in
25
+ * Safari that the Linking listener missed) is noticed without a reload.
26
+ * Returns an unsubscribe. AfterAuth mounts this once.
27
+ */
28
+ export declare function watchKeycloakBearerTokenRevalidate(): () => void;
15
29
  //# sourceMappingURL=frappeToken.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frappeToken.native.d.ts","sourceRoot":"","sources":["../src/frappeToken.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,2EAA2E;AAC3E,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB1E;AAED,wCAAwC;AACxC,wBAAgB,wBAAwB,SAEvC"}
1
+ {"version":3,"file":"frappeToken.native.d.ts","sourceRoot":"","sources":["../src/frappeToken.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS1E;AA2BD,wDAAwD;AACxD,wBAAgB,wBAAwB,SAIvC;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,IAAI,CAS/D"}
package/types/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export * from "./provider/index";
6
6
  export * from "./session/index";
7
7
  export * from "./token";
8
8
  export * from "./types";
9
- export { getKeycloakBearerToken } from "./frappeToken";
9
+ export { getKeycloakBearerToken, watchKeycloakBearerTokenRevalidate } from "./frappeToken";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,kCAAkC,EAAE,MAAM,eAAe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAKjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAI7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY,GAAG,OAAO;IAcrE,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAqBjD;AAED,wBAAgB,WAAW,gCAwB1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAMjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAI7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY,GAAG,OAAO;IAcrE,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAqCjD;AAED,wBAAgB,WAAW,gCAwB1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.webext.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.webext.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAKjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAG7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY;IAQ3D,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAiBjD;AAED,wBAAgB,WAAW,4CAK1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.webext.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.webext.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAMjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAG7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY;IAQ3D,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAgCjD;AAED,wBAAgB,WAAW,4CAK1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * File: /src/oauth/authorizationUrl.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ */
5
+ import type { OidcEndpoints } from "./endpoints";
6
+ export interface AuthorizationUrlParams {
7
+ endpoints: OidcEndpoints;
8
+ clientId: string;
9
+ redirectUri: string;
10
+ state: string;
11
+ codeChallenge: string;
12
+ /**
13
+ * Echoed in the id_token's `nonce` claim (OIDC Core §3.1.2.1). Required
14
+ * rather than optional: an authorization request built without one yields
15
+ * an ID token with nothing to bind it to this login.
16
+ */
17
+ nonce: string;
18
+ /** Defaults to the OIDC minimum plus the claims BaseKeycloak reads. */
19
+ scopes?: string[];
20
+ prompt?: "none" | "login" | "consent" | "select_account";
21
+ loginHint?: string;
22
+ }
23
+ export declare const defaultScopes: string[];
24
+ export declare function buildAuthorizationUrl({ endpoints, clientId, redirectUri, state, codeChallenge, nonce, scopes, prompt, loginHint, }: AuthorizationUrlParams): string;
25
+ export interface LogoutUrlParams {
26
+ endpoints: OidcEndpoints;
27
+ clientId: string;
28
+ idToken?: string;
29
+ postLogoutRedirectUri?: string;
30
+ }
31
+ export declare function buildLogoutUrl({ endpoints, clientId, idToken, postLogoutRedirectUri, }: LogoutUrlParams): string;
32
+ //# sourceMappingURL=authorizationUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorizationUrl.d.ts","sourceRoot":"","sources":["../../src/oauth/authorizationUrl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,UAAiC,CAAC;AAE5D,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,WAAW,EACX,KAAK,EACL,aAAa,EACb,KAAK,EACL,MAAsB,EACtB,MAAM,EACN,SAAS,GACV,EAAE,sBAAsB,GAAG,MAAM,CAajC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,QAAQ,EACR,OAAO,EACP,qBAAqB,GACtB,EAAE,eAAe,GAAG,MAAM,CAQ1B"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * File: /src/oauth/callback.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ */
5
+ export type CallbackQuery = Record<string, string | undefined>;
6
+ /**
7
+ * Is this request the redirect for the login that generated `expectedState`?
8
+ *
9
+ * Anything on the machine can hit 127.0.0.1, so the listener needs to tell a
10
+ * stray local request from the real callback *before* it decides it is done
11
+ * waiting. A request that fails this is noise: it gets a 404 and the listener
12
+ * keeps listening. Without that distinction, any unprivileged local process
13
+ * that races the browser to the port can deny sign-in — it cannot get a code
14
+ * exchanged, because `parseAuthorizationCallback` checks state again, but it
15
+ * can make the real callback arrive at a closed socket.
16
+ */
17
+ export declare function isCallbackForState(query: CallbackQuery, expectedState: string): boolean;
18
+ /**
19
+ * Validate the redirect's query and return the authorization code.
20
+ *
21
+ * The state check is the CSRF defence for the loopback listener: anything on
22
+ * the machine can hit 127.0.0.1, so a callback that does not echo the state we
23
+ * generated is rejected rather than exchanged.
24
+ */
25
+ export declare function parseAuthorizationCallback(query: CallbackQuery, expectedState: string): string;
26
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAEvF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAc9F"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * File: /src/oauth/callbackPage.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * The HTML the loopback listener serves back into the browser tab the
6
+ * redirect lands in.
7
+ *
8
+ * It lives in the OAuth core rather than in the GJS listener for two reasons:
9
+ * markup assembly is pure, so it can be unit tested in CI where nothing runs
10
+ * gjs; and putting the only place this flow emits HTML behind one escaped
11
+ * renderer means there is a single thing to audit.
12
+ *
13
+ * Two rules, both load-bearing:
14
+ *
15
+ * 1. **Nothing the authorization server sent is rendered.** The failure
16
+ * page's copy is fixed. Keycloak's `error_description` arrives on a
17
+ * redirect that any local process can forge, so treating it as markup
18
+ * turned an error page into a script-injection sink; the app still
19
+ * receives the full text through the callback query and surfaces it as
20
+ * an `OAuthError` where it is a string, not a document.
21
+ * 2. **Everything interpolated is escaped anyway.** Belt and braces: the
22
+ * inputs are constants today, and this way they can stop being
23
+ * constants without that being a vulnerability.
24
+ */
25
+ /** Escape text for an HTML text node or a double-quoted attribute. */
26
+ export declare function escapeHtml(value: string): string;
27
+ export interface CallbackPageContent {
28
+ heading: string;
29
+ detail: string;
30
+ }
31
+ export declare const callbackPageSuccess: CallbackPageContent;
32
+ export declare const callbackPageFailure: CallbackPageContent;
33
+ export declare function renderCallbackPage({ heading, detail }: CallbackPageContent): string;
34
+ //# sourceMappingURL=callbackPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callbackPage.d.ts","sourceRoot":"","sources":["../../src/oauth/callbackPage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,mBAAmB,EAAE,mBAGjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,mBAGjC,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAAG,MAAM,CAMnF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * File: /src/oauth/endpoints.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * Keycloak's OIDC endpoints are positional under /realms/<realm>, so they can
6
+ * be derived without a discovery round trip. Deriving beats fetching
7
+ * .well-known here: the GNOME target builds these before it has a working
8
+ * token, and one less network hop is one less failure mode at startup.
9
+ *
10
+ * This is also the transport-security choke point, and `OidcEndpoints` is
11
+ * branded so that it is one by construction rather than by convention: no
12
+ * hand-written object satisfies the type, so every code path that talks to
13
+ * Keycloak — the authorization URL, the token exchange, the refresh, the
14
+ * back-channel logout — necessarily derives its URLs from `keycloakEndpoints`,
15
+ * and rejecting a plaintext issuer here rejects it everywhere. That matters
16
+ * more than usual for this flow:
17
+ * `validateIdToken` skips signature verification on the strength of OIDC Core
18
+ * §3.1.3.7, whose precondition is that the token arrived directly from the
19
+ * token endpoint *over TLS*. Without an enforced https floor that precondition
20
+ * is an assumption, and an assumption is not a control.
21
+ */
22
+ declare const oidcEndpointsBrand: unique symbol;
23
+ export interface OidcEndpoints {
24
+ /**
25
+ * Phantom brand — in the type, never on the object. It is what turns the
26
+ * choke point described above from a convention into something the
27
+ * compiler holds: an object literal cannot satisfy this interface, so the
28
+ * only way a caller gets an `OidcEndpoints` is out of `keycloakEndpoints`,
29
+ * which runs `assertSecureIssuer` first. Without it a consumer could hand
30
+ * `exchangeAuthorizationCode` a plaintext `token` URL of its own making
31
+ * and the https floor would never have been consulted.
32
+ */
33
+ readonly [oidcEndpointsBrand]: true;
34
+ /** The `iss` claim an ID token minted by this realm must carry. */
35
+ issuer: string;
36
+ authorization: string;
37
+ token: string;
38
+ endSession: string;
39
+ userinfo: string;
40
+ revocation: string;
41
+ }
42
+ export interface RealmLocation {
43
+ /** Keycloak base URL, e.g. `https://kc.example.com`. */
44
+ url: string;
45
+ realm: string;
46
+ /**
47
+ * Development-only escape hatch for a Keycloak running on the same machine
48
+ * — `http://localhost:8080`, the docker realm, a stand-in IdP in a test.
49
+ *
50
+ * Deliberately narrow: it permits `http` for **loopback hosts only**
51
+ * (`localhost`, `127.0.0.0/8`, `::1`). A plaintext URL pointing anywhere
52
+ * else stays rejected whatever this is set to, because the traffic would
53
+ * leave the machine. Deliberately explicit: there is no silent fallback and
54
+ * no environment sniffing, so a build that talks plaintext to a real realm
55
+ * has to have said so in its own source.
56
+ */
57
+ allowInsecureLoopbackHttp?: boolean;
58
+ }
59
+ /**
60
+ * Reject any issuer the OIDC Core §3.1.3.7 carve-out would not cover.
61
+ *
62
+ * Exported so a caller can check a configured URL before it builds anything
63
+ * from it — the message names the offending host rather than just failing.
64
+ */
65
+ export declare function assertSecureIssuer({ url, allowInsecureLoopbackHttp }: RealmLocation): void;
66
+ export declare function keycloakEndpoints(location: RealmLocation): OidcEndpoints;
67
+ export {};
68
+ //# sourceMappingURL=endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../../src/oauth/endpoints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,CAAC,MAAM,kBAAkB,EAAE,OAAO,MAAM,CAAC;AAEhD,MAAM,WAAW,aAAa;IAC5B;;;;;;;;OAQG;IACH,QAAQ,CAAC,CAAC,kBAAkB,CAAC,EAAE,IAAI,CAAC;IACpC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;OAUG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAuBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,GAAG,EAAE,yBAAyB,EAAE,EAAE,aAAa,GAAG,IAAI,CA0B1F;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa,CAexE"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * File: /src/oauth/errors.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ */
5
+ /** An OAuth 2.0 / OIDC protocol failure, carrying the spec's `error` code. */
6
+ export declare class OAuthError extends Error {
7
+ /** The `error` field from the authorization or token response. */
8
+ readonly code: string;
9
+ /** The `error_description` field, when the server sent one. */
10
+ readonly description?: string | undefined;
11
+ constructor(
12
+ /** The `error` field from the authorization or token response. */
13
+ code: string, message?: string,
14
+ /** The `error_description` field, when the server sent one. */
15
+ description?: string | undefined);
16
+ }
17
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/oauth/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8EAA8E;AAC9E,qBAAa,UAAW,SAAQ,KAAK;IAEjC,kEAAkE;aAClD,IAAI,EAAE,MAAM;IAE5B,+DAA+D;aAC/C,WAAW,CAAC,EAAE,MAAM;;IAJpC,kEAAkE;IAClD,IAAI,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,MAAM;IAChB,+DAA+D;IAC/C,WAAW,CAAC,EAAE,MAAM,YAAA;CAKvC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * File: /src/oauth/form.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * application/x-www-form-urlencoded serialization.
6
+ *
7
+ * URLSearchParams would do this in one line, but GJS has no such global —
8
+ * the GNOME app only gets one from the react-gnome polyfills, and the auth
9
+ * entries deliberately run without them. Twelve lines here keeps the OAuth
10
+ * core dependent on nothing but the language.
11
+ *
12
+ * Matches URLSearchParams byte for byte, including the space-as-"+" rule that
13
+ * the form-urlencoded media type requires.
14
+ */
15
+ export declare function encodeForm(params: Record<string, string>): string;
16
+ //# sourceMappingURL=form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/oauth/form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAIjE"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * File: /src/oauth/idToken.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * ID token validation for the loopback flow (OIDC Core §3.1.3.7).
6
+ *
7
+ * ## What is checked, and what is deliberately not
8
+ *
9
+ * Checked: `iss`, `aud`, `azp`, `exp`, `iat`, `nbf`, `nonce`, `sub`, and that
10
+ * the header does not claim `alg: none`.
11
+ *
12
+ * **Not checked: the signature.** This is a deliberate decision, not an
13
+ * oversight, and it rests on a precondition that is enforced rather than
14
+ * assumed. OIDC Core §3.1.3.7 rule 6 says a client MAY skip signature
15
+ * validation when the ID token is received "directly from the Token Endpoint"
16
+ * over a TLS-protected channel, because TLS server authentication already
17
+ * proves who minted it. Both halves of that are true here and neither is left
18
+ * to chance:
19
+ *
20
+ * 1. *Directly from the token endpoint.* `validateIdToken` is only ever
21
+ * called from `tokenExchange.ts`, on the parsed body of a POST this
22
+ * process just made to `endpoints.token`. No ID token from a redirect,
23
+ * a front channel or a caller reaches it — there is no implicit flow
24
+ * here and no `response_type` that returns a token on the redirect.
25
+ * 2. *Over TLS.* `keycloakEndpoints` refuses to derive a token endpoint for
26
+ * a plaintext issuer unless the host is loopback **and** the caller
27
+ * passed `allowInsecureLoopbackHttp`. See endpoints.ts.
28
+ *
29
+ * The alternative is a JWKS fetch plus RS256 verification, and it is not
30
+ * available: GJS has no `node:crypto` and no WebCrypto (`globalThis.crypto`
31
+ * is undefined as of gjs 1.88), so RSA verification would mean hand-rolling
32
+ * modular exponentiation over bignums in JavaScript. Hand-rolled RSA in an
33
+ * auth path is a worse risk than the carve-out the spec explicitly grants.
34
+ *
35
+ * If this code ever accepts an ID token from anywhere other than a direct
36
+ * token-endpoint response — a front-channel redirect, a push from a server,
37
+ * a cached token from another process — the carve-out stops applying and the
38
+ * signature has to be verified for real.
39
+ *
40
+ * ## What the carve-out does not excuse
41
+ *
42
+ * Nothing above says anything about the claims. A validly-signed token minted
43
+ * for a different client in the same realm, or one that expired last week, is
44
+ * still the wrong token; `azp`/`aud` and `exp` are what catch those, and they
45
+ * are checked unconditionally.
46
+ */
47
+ export interface IdTokenClaims {
48
+ iss?: string;
49
+ aud?: string | string[];
50
+ azp?: string;
51
+ exp?: number;
52
+ iat?: number;
53
+ nbf?: number;
54
+ nonce?: string;
55
+ sub?: string;
56
+ [claim: string]: unknown;
57
+ }
58
+ export interface IdTokenValidation {
59
+ /** `endpoints.issuer` — the exact string the `iss` claim must equal. */
60
+ issuer: string;
61
+ clientId: string;
62
+ /**
63
+ * The nonce sent on the authorization request. When supplied the token's
64
+ * `nonce` must match it exactly, which is what stops a token captured from
65
+ * one login being replayed into another.
66
+ *
67
+ * Omitted only on the refresh grant, where there is no fresh authorization
68
+ * request to bind to and the token store does not carry the original nonce
69
+ * across process restarts. The refreshed token is still bound to this
70
+ * client by `azp`/`aud` and to this realm by `iss`.
71
+ */
72
+ nonce?: string;
73
+ /** Epoch seconds. Injected by tests; defaults to the wall clock. */
74
+ now?: number;
75
+ /** Allowance for clock skew between this machine and Keycloak. */
76
+ clockToleranceSeconds?: number;
77
+ }
78
+ /**
79
+ * Validate an ID token that came straight off a token-endpoint response, and
80
+ * return its claims. Throws `OAuthError("invalid_id_token")` on any failure.
81
+ */
82
+ export declare function validateIdToken(idToken: string, validation: IdTokenValidation): IdTokenClaims;
83
+ //# sourceMappingURL=idToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idToken.d.ts","sourceRoot":"","sources":["../../src/oauth/idToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAiBD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,GAAG,aAAa,CAkD7F"}
@@ -0,0 +1,11 @@
1
+ export * from "./authorizationUrl";
2
+ export * from "./callback";
3
+ export * from "./callbackPage";
4
+ export * from "./endpoints";
5
+ export * from "./errors";
6
+ export * from "./idToken";
7
+ export * from "./jwt";
8
+ export * from "./loopbackFlow";
9
+ export * from "./pkce";
10
+ export * from "./tokenExchange";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * File: /src/oauth/jwt.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * Unverified JWT decoding — the inverse of pkce.ts's base64url encoder, and
6
+ * hand-rolled for the same reason: GJS has no `atob` and no `Buffer`, and the
7
+ * OAuth core has to run unchanged on GJS, node and the browser.
8
+ *
9
+ * "Unverified" is load-bearing. Nothing in this file looks at the signature.
10
+ * Decoding a JWT tells you what the bytes claim, not whether the claim is
11
+ * true; `validateIdToken` in idToken.ts is what turns the former into the
12
+ * latter, and it is the only thing that should be feeding these results into
13
+ * a decision.
14
+ */
15
+ /**
16
+ * base64url per RFC 4648 §5 — URL-safe alphabet, padding optional.
17
+ *
18
+ * Strict on input: an unknown character or a 1-character tail (which no
19
+ * base64 encoding can produce) is a malformed token, not something to
20
+ * silently round down.
21
+ */
22
+ export declare function base64UrlDecode(value: string): Uint8Array;
23
+ export interface DecodedJwt {
24
+ header: Record<string, unknown>;
25
+ payload: Record<string, unknown>;
26
+ /** Present so a caller can see there is one; nothing here verifies it. */
27
+ signature: string;
28
+ }
29
+ /** Decode a compact-serialization JWS without verifying anything about it. */
30
+ export declare function decodeJwt(token: string): DecodedJwt;
31
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/oauth/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CA2BzD;AAeD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAUnD"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * File: /src/oauth/loopbackFlow.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * Authorization Code + PKCE with a loopback redirect — the flow RFC 8252
6
+ * ("OAuth 2.0 for Native Apps", §7.3) prescribes for desktop applications.
7
+ *
8
+ * The app binds an ephemeral port on 127.0.0.1, hands Keycloak that port as
9
+ * the redirect URI, and opens the system browser. The user authenticates in a
10
+ * real browser — with their password manager, their existing SSO cookie and a
11
+ * verifiable address bar — and Keycloak redirects back to the listener, which
12
+ * closes as soon as the code arrives.
13
+ *
14
+ * Why not the alternatives:
15
+ *
16
+ * - A custom URI scheme (`myapp://callback`) needs an installed .desktop
17
+ * file registering `x-scheme-handler/myapp` plus a desktop-database
18
+ * refresh. That cannot work from a dev tree run with `gjs -m dist/...`,
19
+ * and on a multi-app desktop any other app can register the same scheme.
20
+ * - The device authorization grant avoids the redirect entirely, but it has
21
+ * to be enabled per-client on the realm, and it downgrades the UX to
22
+ * retyping a code for a GUI app that can perfectly well open a browser.
23
+ * - An embedded webview would put the app between the user and their
24
+ * credentials, which RFC 8252 §8.12 explicitly rules out.
25
+ *
26
+ * Everything the flow touches is injected, so the whole orchestration is
27
+ * exercised in unit tests with no browser, no sockets and no GJS.
28
+ */
29
+ import { type CallbackQuery } from "./callback";
30
+ import { type PkceCrypto } from "./pkce";
31
+ import { type FetchLike, type TokenSet } from "./tokenExchange";
32
+ /** A bound loopback socket waiting for exactly one redirect. */
33
+ export interface LoopbackListener {
34
+ /** The `http://127.0.0.1:<port>/…` URI the socket is bound to. */
35
+ redirectUri: string;
36
+ waitForCallback(): Promise<CallbackQuery>;
37
+ close(): void;
38
+ }
39
+ export interface LoopbackTransport {
40
+ crypto: PkceCrypto;
41
+ fetch: FetchLike;
42
+ openBrowser(url: string): void | Promise<void>;
43
+ /**
44
+ * Bind a loopback socket for one login.
45
+ *
46
+ * `expectedState` is handed over at bind time rather than at wait time so
47
+ * the listener can tell the real redirect from a stray local request from
48
+ * the very first packet — there is no window in which it does not yet know
49
+ * what it is waiting for.
50
+ */
51
+ listen(expectedState: string): Promise<LoopbackListener>;
52
+ }
53
+ export interface LoopbackLoginConfig {
54
+ url: string;
55
+ realm: string;
56
+ clientId: string;
57
+ /** See `RealmLocation.allowInsecureLoopbackHttp` — development only. */
58
+ allowInsecureLoopbackHttp?: boolean;
59
+ scopes?: string[];
60
+ prompt?: "none" | "login" | "consent" | "select_account";
61
+ loginHint?: string;
62
+ }
63
+ export declare function runLoopbackLogin(transport: LoopbackTransport, config: LoopbackLoginConfig): Promise<TokenSet>;
64
+ //# sourceMappingURL=loopbackFlow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopbackFlow.d.ts","sourceRoot":"","sources":["../../src/oauth/loopbackFlow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAAE,KAAK,aAAa,EAA8B,MAAM,YAAY,CAAC;AAG5E,OAAO,EAAiD,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AACxF,OAAO,EAA6B,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3F,gEAAgE;AAChE,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;;;;;OAOG;IACH,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAwCnB"}