@fluixi/oauth2 1.0.0-alpha.1

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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api-guard.cjs +185 -0
  3. package/dist/api-guard.d.ts +94 -0
  4. package/dist/api-guard.d.ts.map +1 -0
  5. package/dist/api-guard.js +127 -0
  6. package/dist/api-guard.mjs +141 -0
  7. package/dist/browser-FDUOEUM2.mjs +3544 -0
  8. package/dist/browser-ONMHX3Z3.mjs +3545 -0
  9. package/dist/browser.cjs +735 -0
  10. package/dist/browser.d.ts +3 -0
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +448 -0
  13. package/dist/browser.mjs +704 -0
  14. package/dist/callback.cjs +125 -0
  15. package/dist/callback.d.ts +113 -0
  16. package/dist/callback.d.ts.map +1 -0
  17. package/dist/callback.js +183 -0
  18. package/dist/callback.mjs +106 -0
  19. package/dist/chunk-25WE6UWI.mjs +10 -0
  20. package/dist/chunk-4NSOGPLM.mjs +15 -0
  21. package/dist/chunk-7P6ASYW6.mjs +9 -0
  22. package/dist/chunk-OGSFNTKA.mjs +13 -0
  23. package/dist/client-auth.cjs +115 -0
  24. package/dist/client-auth.d.ts +43 -0
  25. package/dist/client-auth.d.ts.map +1 -0
  26. package/dist/client-auth.js +112 -0
  27. package/dist/client-auth.mjs +84 -0
  28. package/dist/discovery.cjs +86 -0
  29. package/dist/discovery.d.ts +15 -0
  30. package/dist/discovery.d.ts.map +1 -0
  31. package/dist/discovery.js +51 -0
  32. package/dist/discovery.mjs +63 -0
  33. package/dist/dpop-LVPWWADA.mjs +80 -0
  34. package/dist/dpop-NS7NKOWM.mjs +79 -0
  35. package/dist/dpop-store.cjs +152 -0
  36. package/dist/dpop-store.d.ts +37 -0
  37. package/dist/dpop-store.d.ts.map +1 -0
  38. package/dist/dpop-store.js +119 -0
  39. package/dist/dpop-store.mjs +121 -0
  40. package/dist/dpop.cjs +117 -0
  41. package/dist/dpop.d.ts +72 -0
  42. package/dist/dpop.d.ts.map +1 -0
  43. package/dist/dpop.js +114 -0
  44. package/dist/dpop.mjs +86 -0
  45. package/dist/env-config.cjs +366 -0
  46. package/dist/env-config.d.ts +103 -0
  47. package/dist/env-config.d.ts.map +1 -0
  48. package/dist/env-config.js +184 -0
  49. package/dist/env-config.mjs +343 -0
  50. package/dist/env-credentials.cjs +82 -0
  51. package/dist/env-credentials.d.ts +68 -0
  52. package/dist/env-credentials.d.ts.map +1 -0
  53. package/dist/env-credentials.js +76 -0
  54. package/dist/env-credentials.mjs +51 -0
  55. package/dist/index.cjs +940 -0
  56. package/dist/index.d.ts +104 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +101 -0
  59. package/dist/index.mjs +909 -0
  60. package/dist/pkce.cjs +87 -0
  61. package/dist/pkce.d.ts +58 -0
  62. package/dist/pkce.d.ts.map +1 -0
  63. package/dist/pkce.js +112 -0
  64. package/dist/pkce.mjs +66 -0
  65. package/dist/presets.cjs +240 -0
  66. package/dist/presets.d.ts +140 -0
  67. package/dist/presets.d.ts.map +1 -0
  68. package/dist/presets.js +182 -0
  69. package/dist/presets.mjs +217 -0
  70. package/dist/return-to.cjs +46 -0
  71. package/dist/return-to.d.ts +49 -0
  72. package/dist/return-to.d.ts.map +1 -0
  73. package/dist/return-to.js +71 -0
  74. package/dist/return-to.mjs +25 -0
  75. package/dist/roles.cjs +56 -0
  76. package/dist/roles.d.ts +35 -0
  77. package/dist/roles.d.ts.map +1 -0
  78. package/dist/roles.js +37 -0
  79. package/dist/roles.mjs +35 -0
  80. package/dist/server-client.cjs +134 -0
  81. package/dist/server-client.d.ts +12 -0
  82. package/dist/server-client.d.ts.map +1 -0
  83. package/dist/server-client.js +107 -0
  84. package/dist/server-client.mjs +111 -0
  85. package/dist/server.cjs +470 -0
  86. package/dist/server.d.ts +89 -0
  87. package/dist/server.d.ts.map +1 -0
  88. package/dist/server.js +360 -0
  89. package/dist/server.mjs +441 -0
  90. package/dist/service-client.cjs +244 -0
  91. package/dist/service-client.d.ts +89 -0
  92. package/dist/service-client.d.ts.map +1 -0
  93. package/dist/service-client.js +112 -0
  94. package/dist/service-client.mjs +213 -0
  95. package/dist/service.cjs +903 -0
  96. package/dist/service.d.ts +96 -0
  97. package/dist/service.d.ts.map +1 -0
  98. package/dist/service.js +129 -0
  99. package/dist/service.mjs +874 -0
  100. package/dist/tokens.cjs +29 -0
  101. package/dist/tokens.d.ts +13 -0
  102. package/dist/tokens.d.ts.map +1 -0
  103. package/dist/tokens.js +21 -0
  104. package/dist/tokens.mjs +8 -0
  105. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  106. package/dist/types.cjs +33 -0
  107. package/dist/types.d.ts +239 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +23 -0
  110. package/dist/types.mjs +12 -0
  111. package/dist/verify.cjs +724 -0
  112. package/dist/verify.d.ts +205 -0
  113. package/dist/verify.d.ts.map +1 -0
  114. package/dist/verify.js +371 -0
  115. package/dist/verify.mjs +590 -0
  116. package/package.json +121 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Where a completed login is allowed to land.
3
+ *
4
+ * A login flow carries a destination so a visitor resumes where they were interrupted,
5
+ * and that destination arrives from outside: a query parameter, a caller's argument, a
6
+ * value read back out of storage. Redirecting to it unchecked is an open redirect, and a
7
+ * login is the worst place to have one. The visitor has just authenticated, so the page
8
+ * they land on is trusted by them and reached with a live session.
9
+ *
10
+ * Sealing the value does not help. A signed cookie cannot be tampered with, and an
11
+ * attacker putting `?returnTo=https://evil.example/` on a link has no need to tamper: the
12
+ * application seals the attacker's value itself and honours it afterwards.
13
+ *
14
+ * So the rule is what the value is allowed to be, rather than where it came from. A path
15
+ * on this origin, and nothing else.
16
+ */
17
+ /**
18
+ * The destination to use, which is `candidate` when it is safe and the fallback when not.
19
+ *
20
+ * Refused, with reasons:
21
+ *
22
+ * - an absolute URL on another origin, which is the open redirect itself
23
+ * - `//host`, which a browser reads as protocol-relative and therefore another origin
24
+ * - `\\host`, which some browsers normalise to the same thing
25
+ * - a scheme of any kind, so `javascript:` and `data:` cannot be reached
26
+ * - a control character, which can be used to smuggle a newline past a naive check
27
+ * - anything that is not a path, since a bare `dashboard` resolves differently depending
28
+ * on where it is used
29
+ */
30
+ export function safeReturnTo(candidate, options = {}) {
31
+ const fallback = options.fallback ?? '/';
32
+ if (!candidate || typeof candidate !== 'string')
33
+ return fallback;
34
+ const value = candidate.trim();
35
+ if (!value)
36
+ return fallback;
37
+ // A newline or a NUL can split a header or end a string early in whatever reads it next.
38
+ if (/[\u0000-\u001f\u007f]/.test(value))
39
+ return fallback;
40
+ // Protocol-relative in both spellings. `//evil.test` is another origin, and a backslash
41
+ // is normalised to a slash by enough browsers to count.
42
+ if (/^[/\\]{2}/.test(value))
43
+ return fallback;
44
+ // A path on this origin, which is the ordinary case and the only one needing no origin
45
+ // to compare against.
46
+ if (value.startsWith('/'))
47
+ return value;
48
+ // Anything else has to be an absolute URL on an origin this application accepts.
49
+ const here = options.origin ?? (typeof location === 'undefined' ? undefined : location.origin);
50
+ const allowed = [here, ...(options.allowedOrigins ?? [])].filter(Boolean);
51
+ if (!allowed.length)
52
+ return fallback;
53
+ let url;
54
+ try {
55
+ url = new URL(value);
56
+ }
57
+ catch {
58
+ // Not a URL and not a path: a bare word, which resolves differently depending on the
59
+ // page it is used from.
60
+ return fallback;
61
+ }
62
+ // Compared as whole origins. A prefix test would accept
63
+ // `https://app.example.com.evil.test`, which is a different site.
64
+ if (!allowed.includes(url.origin))
65
+ return fallback;
66
+ // Only http and https, whatever the origin list says: an allowed origin cannot make
67
+ // `javascript:` safe, and `new URL('javascript:...')` has an origin of `null` anyway.
68
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
69
+ return fallback;
70
+ return url.toString();
71
+ }
@@ -0,0 +1,25 @@
1
+ // src/return-to.ts
2
+ function safeReturnTo(candidate, options = {}) {
3
+ const fallback = options.fallback ?? "/";
4
+ if (!candidate || typeof candidate !== "string") return fallback;
5
+ const value = candidate.trim();
6
+ if (!value) return fallback;
7
+ if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
8
+ if (/^[/\\]{2}/.test(value)) return fallback;
9
+ if (value.startsWith("/")) return value;
10
+ const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
11
+ const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
12
+ if (!allowed.length) return fallback;
13
+ let url;
14
+ try {
15
+ url = new URL(value);
16
+ } catch {
17
+ return fallback;
18
+ }
19
+ if (!allowed.includes(url.origin)) return fallback;
20
+ if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
21
+ return url.toString();
22
+ }
23
+ export {
24
+ safeReturnTo
25
+ };
package/dist/roles.cjs ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/roles.ts
21
+ var roles_exports = {};
22
+ __export(roles_exports, {
23
+ rolesFrom: () => rolesFrom
24
+ });
25
+ module.exports = __toCommonJS(roles_exports);
26
+ var rolesFrom = {
27
+ /** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
28
+ keycloakRealm: (claims) => {
29
+ const realm = claims.realm_access;
30
+ return realm?.roles ?? [];
31
+ },
32
+ /** Keycloak, roles scoped to one client rather than the realm. */
33
+ keycloakClient: (clientId) => (claims) => {
34
+ const all = claims.resource_access;
35
+ return all?.[clientId]?.roles ?? [];
36
+ },
37
+ /** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
38
+ groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
39
+ /** Cognito, which namespaces the claim. */
40
+ cognito: (claims) => {
41
+ const groups = claims["cognito:groups"];
42
+ return Array.isArray(groups) ? groups : [];
43
+ },
44
+ /**
45
+ * Auth0, which requires a namespaced custom claim added by an action.
46
+ *
47
+ * `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
48
+ * that is not a URI, so an empty list here usually means the namespace, not the action.
49
+ */
50
+ namespaced: (claim) => (claims) => {
51
+ const value = claims[claim];
52
+ return Array.isArray(value) ? value : [];
53
+ },
54
+ /** The `scope` claim read as a role list, for a provider issuing no roles at all. */
55
+ scope: (claims) => typeof claims.scope === "string" ? claims.scope.split(" ") : []
56
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Reading roles off a token's claims.
3
+ *
4
+ * Its own module because it is pure data access and a browser bundle wants it too: presets
5
+ * name one per provider, and an app's shared config picks one. Kept here, clear of
6
+ * `verify.ts`, so importing it never drags the verifier and its `node:` imports into a
7
+ * client build.
8
+ */
9
+ import type { TokenClaims } from './verify.js';
10
+ /**
11
+ * Where each provider puts roles.
12
+ *
13
+ * There is no default because they genuinely disagree, and guessing wrong means a guard that
14
+ * reads an empty list and refuses everyone. Each of these was read off a real token.
15
+ */
16
+ export declare const rolesFrom: {
17
+ /** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
18
+ keycloakRealm: (claims: TokenClaims) => string[];
19
+ /** Keycloak, roles scoped to one client rather than the realm. */
20
+ keycloakClient: (clientId: string) => (claims: TokenClaims) => string[];
21
+ /** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
22
+ groups: (claims: TokenClaims) => string[];
23
+ /** Cognito, which namespaces the claim. */
24
+ cognito: (claims: TokenClaims) => string[];
25
+ /**
26
+ * Auth0, which requires a namespaced custom claim added by an action.
27
+ *
28
+ * `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
29
+ * that is not a URI, so an empty list here usually means the namespace, not the action.
30
+ */
31
+ namespaced: (claim: string) => (claims: TokenClaims) => string[];
32
+ /** The `scope` claim read as a role list, for a provider issuing no roles at all. */
33
+ scope: (claims: TokenClaims) => string[];
34
+ };
35
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,SAAS;IACpB,sFAAsF;4BAC9D,WAAW,KAAG,MAAM,EAAE;IAK9C,kEAAkE;+BAErD,MAAM,MAChB,QAAQ,WAAW,KAAG,MAAM,EAAE;IAKjC,uFAAuF;qBACtE,WAAW,KAAG,MAAM,EAAE;IAGvC,2CAA2C;sBACzB,WAAW,KAAG,MAAM,EAAE;IAKxC;;;;;OAKG;wBAEO,MAAM,MACb,QAAQ,WAAW,KAAG,MAAM,EAAE;IAKjC,qFAAqF;oBACrE,WAAW,KAAG,MAAM,EAAE;CAEvC,CAAC"}
package/dist/roles.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Where each provider puts roles.
3
+ *
4
+ * There is no default because they genuinely disagree, and guessing wrong means a guard that
5
+ * reads an empty list and refuses everyone. Each of these was read off a real token.
6
+ */
7
+ export const rolesFrom = {
8
+ /** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
9
+ keycloakRealm: (claims) => {
10
+ const realm = claims.realm_access;
11
+ return realm?.roles ?? [];
12
+ },
13
+ /** Keycloak, roles scoped to one client rather than the realm. */
14
+ keycloakClient: (clientId) => (claims) => {
15
+ const all = claims.resource_access;
16
+ return all?.[clientId]?.roles ?? [];
17
+ },
18
+ /** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
19
+ groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
20
+ /** Cognito, which namespaces the claim. */
21
+ cognito: (claims) => {
22
+ const groups = claims['cognito:groups'];
23
+ return Array.isArray(groups) ? groups : [];
24
+ },
25
+ /**
26
+ * Auth0, which requires a namespaced custom claim added by an action.
27
+ *
28
+ * `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
29
+ * that is not a URI, so an empty list here usually means the namespace, not the action.
30
+ */
31
+ namespaced: (claim) => (claims) => {
32
+ const value = claims[claim];
33
+ return Array.isArray(value) ? value : [];
34
+ },
35
+ /** The `scope` claim read as a role list, for a provider issuing no roles at all. */
36
+ scope: (claims) => typeof claims.scope === 'string' ? claims.scope.split(' ') : [],
37
+ };
package/dist/roles.mjs ADDED
@@ -0,0 +1,35 @@
1
+ // src/roles.ts
2
+ var rolesFrom = {
3
+ /** Keycloak, realm roles. Where it puts them unless a client mapper is configured. */
4
+ keycloakRealm: (claims) => {
5
+ const realm = claims.realm_access;
6
+ return realm?.roles ?? [];
7
+ },
8
+ /** Keycloak, roles scoped to one client rather than the realm. */
9
+ keycloakClient: (clientId) => (claims) => {
10
+ const all = claims.resource_access;
11
+ return all?.[clientId]?.roles ?? [];
12
+ },
13
+ /** Okta, and anything else using a groups claim. Needs a groups claim on the token. */
14
+ groups: (claims) => Array.isArray(claims.groups) ? claims.groups : [],
15
+ /** Cognito, which namespaces the claim. */
16
+ cognito: (claims) => {
17
+ const groups = claims["cognito:groups"];
18
+ return Array.isArray(groups) ? groups : [];
19
+ },
20
+ /**
21
+ * Auth0, which requires a namespaced custom claim added by an action.
22
+ *
23
+ * `rolesFrom.namespaced('https://adafri.com/roles')`. Auth0 silently drops a custom claim
24
+ * that is not a URI, so an empty list here usually means the namespace, not the action.
25
+ */
26
+ namespaced: (claim) => (claims) => {
27
+ const value = claims[claim];
28
+ return Array.isArray(value) ? value : [];
29
+ },
30
+ /** The `scope` claim read as a role list, for a provider issuing no roles at all. */
31
+ scope: (claims) => typeof claims.scope === "string" ? claims.scope.split(" ") : []
32
+ };
33
+ export {
34
+ rolesFrom
35
+ };
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server-client.ts
21
+ var server_client_exports = {};
22
+ __export(server_client_exports, {
23
+ COOKIE_SESSION: () => COOKIE_SESSION,
24
+ createServerClient: () => createServerClient
25
+ });
26
+ module.exports = __toCommonJS(server_client_exports);
27
+
28
+ // src/return-to.ts
29
+ function safeReturnTo(candidate, options = {}) {
30
+ const fallback = options.fallback ?? "/";
31
+ if (!candidate || typeof candidate !== "string") return fallback;
32
+ const value = candidate.trim();
33
+ if (!value) return fallback;
34
+ if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
35
+ if (/^[/\\]{2}/.test(value)) return fallback;
36
+ if (value.startsWith("/")) return value;
37
+ const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
38
+ const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
39
+ if (!allowed.length) return fallback;
40
+ let url;
41
+ try {
42
+ url = new URL(value);
43
+ } catch {
44
+ return fallback;
45
+ }
46
+ if (!allowed.includes(url.origin)) return fallback;
47
+ if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
48
+ return url.toString();
49
+ }
50
+
51
+ // src/types.ts
52
+ var OAuthError = class extends Error {
53
+ constructor(message, code, description) {
54
+ super(message);
55
+ this.code = code;
56
+ this.description = description;
57
+ this.name = "OAuthError";
58
+ }
59
+ };
60
+
61
+ // src/server-client.ts
62
+ var COOKIE_SESSION = "cookie";
63
+ function createServerClient(config, fetcher = fetch) {
64
+ const base = (config.basePath ?? "/auth").replace(/\/$/, "");
65
+ const url = (path) => `${base}${path}`;
66
+ const call = (path, init = {}) => fetcher(url(path), { credentials: "same-origin", ...init });
67
+ return {
68
+ async login(options = {}) {
69
+ const target = new URL(url("/login"), location.origin);
70
+ if (options.returnTo) target.searchParams.set("returnTo", options.returnTo);
71
+ for (const [key, value] of Object.entries(options.params ?? {})) {
72
+ target.searchParams.set(key, value);
73
+ }
74
+ location.assign(target.toString());
75
+ },
76
+ async handleCallback(params) {
77
+ const response = await call("/callback", {
78
+ method: "POST",
79
+ headers: { "content-type": "application/json" },
80
+ body: JSON.stringify(params)
81
+ });
82
+ if (!response.ok) {
83
+ const body = await response.json().catch(() => ({}));
84
+ throw new OAuthError(
85
+ String(body.error ?? `Callback failed: ${response.status}`),
86
+ String(body.error ?? "callback_failed")
87
+ );
88
+ }
89
+ return { accessToken: COOKIE_SESSION };
90
+ },
91
+ async refresh() {
92
+ const response = await call("/refresh", { method: "POST" });
93
+ if (!response.ok) {
94
+ throw new OAuthError("The server could not extend the session.", "refresh_failed");
95
+ }
96
+ return { accessToken: COOKIE_SESSION };
97
+ },
98
+ async fetchUser() {
99
+ const custom = config.userInfo;
100
+ const response = custom?.url ? await fetcher(custom.url, {
101
+ method: custom.method ?? "GET",
102
+ credentials: custom.credentials ?? "same-origin",
103
+ headers: { accept: "application/json", ...custom.headers }
104
+ }) : await call("/me");
105
+ if (!response.ok) {
106
+ throw new OAuthError(`The server refused the user request: ${response.status}`, "me_failed");
107
+ }
108
+ const raw = await response.json();
109
+ return custom?.map ? await custom.map(raw, COOKIE_SESSION) : raw;
110
+ },
111
+ /**
112
+ * Retire the session's tokens, which only the server can do here.
113
+ *
114
+ * The browser holds a cookie and no token, so it has nothing to send to a revocation
115
+ * endpoint. This asks the server's own route, which opens the cookie and revokes what
116
+ * is inside.
117
+ */
118
+ async revoke(_token, hint = "refresh_token") {
119
+ await call(`/revoke?hint=${hint}`, { method: "POST" }).catch(() => void 0);
120
+ },
121
+ async logout(options = {}) {
122
+ await call("/logout", { method: "POST" }).catch(() => void 0);
123
+ const target = options.returnTo ? safeReturnTo(options.returnTo, {
124
+ fallback: config.postLogoutRedirectUri ?? "/",
125
+ allowedOrigins: config.allowedReturnOrigins
126
+ }) : config.postLogoutRedirectUri ?? "/";
127
+ if (typeof location !== "undefined") location.assign(target);
128
+ },
129
+ async restore() {
130
+ const response = await call("/me");
131
+ return response.ok ? { accessToken: COOKIE_SESSION } : null;
132
+ }
133
+ };
134
+ }
@@ -0,0 +1,12 @@
1
+ import type { AuthClient, ServerConfig } from './types.js';
2
+ /**
3
+ * Stands in for the access token the caller never sees.
4
+ *
5
+ * `@fluixi/session` is built around a token string, and here the credential is a cookie
6
+ * the browser attaches on its own. A constant keeps the session's state machine working
7
+ * without inventing a token, and nothing ever sends it anywhere: requests carry the
8
+ * cookie.
9
+ */
10
+ export declare const COOKIE_SESSION = "cookie";
11
+ export declare function createServerClient(config: ServerConfig, fetcher?: typeof fetch): AuthClient;
12
+ //# sourceMappingURL=server-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-client.d.ts","sourceRoot":"","sources":["../src/server-client.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAgC,YAAY,EAAU,MAAM,YAAY,CAAC;AAEjG;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,OAAO,KAAa,GAC5B,UAAU,CAgGZ"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The browser half of a server-held session.
3
+ *
4
+ * Every method is a call to a route the application's own server exposes, which is what
5
+ * `@fluixi/oauth2/server` provides. No token reaches this file: the server sets an
6
+ * httpOnly cookie, so a script on the page has nothing to read and an XSS cannot lift a
7
+ * session.
8
+ *
9
+ * The shape is the same `AuthClient` the browser implementation satisfies, so the
10
+ * service above does not know which one it has.
11
+ */
12
+ import { safeReturnTo } from './return-to.js';
13
+ import { OAuthError } from './types.js';
14
+ /**
15
+ * Stands in for the access token the caller never sees.
16
+ *
17
+ * `@fluixi/session` is built around a token string, and here the credential is a cookie
18
+ * the browser attaches on its own. A constant keeps the session's state machine working
19
+ * without inventing a token, and nothing ever sends it anywhere: requests carry the
20
+ * cookie.
21
+ */
22
+ export const COOKIE_SESSION = 'cookie';
23
+ export function createServerClient(config, fetcher = fetch) {
24
+ const base = (config.basePath ?? '/auth').replace(/\/$/, '');
25
+ const url = (path) => `${base}${path}`;
26
+ /** Same credentials rule for every call: the cookie, and never a bearer token. */
27
+ const call = (path, init = {}) => fetcher(url(path), { credentials: 'same-origin', ...init });
28
+ return {
29
+ async login(options = {}) {
30
+ // The server builds the authorization URL, because it holds the PKCE verifier and
31
+ // the client secret. The browser only navigates.
32
+ const target = new URL(url('/login'), location.origin);
33
+ if (options.returnTo)
34
+ target.searchParams.set('returnTo', options.returnTo);
35
+ for (const [key, value] of Object.entries(options.params ?? {})) {
36
+ target.searchParams.set(key, value);
37
+ }
38
+ location.assign(target.toString());
39
+ },
40
+ async handleCallback(params) {
41
+ // The server owns the exchange; the browser is telling it the callback arrived.
42
+ // In the usual deployment the redirect URI points at the server route directly and
43
+ // this is never called, which is why it is tolerant about what it gets back.
44
+ const response = await call('/callback', {
45
+ method: 'POST',
46
+ headers: { 'content-type': 'application/json' },
47
+ body: JSON.stringify(params),
48
+ });
49
+ if (!response.ok) {
50
+ const body = (await response.json().catch(() => ({})));
51
+ throw new OAuthError(String(body.error ?? `Callback failed: ${response.status}`), String(body.error ?? 'callback_failed'));
52
+ }
53
+ return { accessToken: COOKIE_SESSION };
54
+ },
55
+ async refresh() {
56
+ const response = await call('/refresh', { method: 'POST' });
57
+ if (!response.ok) {
58
+ throw new OAuthError('The server could not extend the session.', 'refresh_failed');
59
+ }
60
+ return { accessToken: COOKIE_SESSION };
61
+ },
62
+ async fetchUser() {
63
+ const custom = config.userInfo;
64
+ // A URL here points at the app's own API rather than the session route. The cookie
65
+ // still authenticates it, which is why `credentials` stays on either way.
66
+ const response = custom?.url
67
+ ? await fetcher(custom.url, {
68
+ method: custom.method ?? 'GET',
69
+ credentials: custom.credentials ?? 'same-origin',
70
+ headers: { accept: 'application/json', ...custom.headers },
71
+ })
72
+ : await call('/me');
73
+ if (!response.ok) {
74
+ throw new OAuthError(`The server refused the user request: ${response.status}`, 'me_failed');
75
+ }
76
+ const raw = (await response.json());
77
+ return custom?.map ? (await custom.map(raw, COOKIE_SESSION)) : raw;
78
+ },
79
+ /**
80
+ * Retire the session's tokens, which only the server can do here.
81
+ *
82
+ * The browser holds a cookie and no token, so it has nothing to send to a revocation
83
+ * endpoint. This asks the server's own route, which opens the cookie and revokes what
84
+ * is inside.
85
+ */
86
+ async revoke(_token, hint = 'refresh_token') {
87
+ await call(`/revoke?hint=${hint}`, { method: 'POST' }).catch(() => undefined);
88
+ },
89
+ async logout(options = {}) {
90
+ await call('/logout', { method: 'POST' }).catch(() => undefined);
91
+ const target = options.returnTo
92
+ ? safeReturnTo(options.returnTo, {
93
+ fallback: config.postLogoutRedirectUri ?? '/',
94
+ allowedOrigins: config.allowedReturnOrigins,
95
+ })
96
+ : (config.postLogoutRedirectUri ?? '/');
97
+ if (typeof location !== 'undefined')
98
+ location.assign(target);
99
+ },
100
+ async restore() {
101
+ // The cookie is either valid or it is not, and only the server knows. A successful
102
+ // `/me` is the answer.
103
+ const response = await call('/me');
104
+ return response.ok ? { accessToken: COOKIE_SESSION } : null;
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,111 @@
1
+ // src/return-to.ts
2
+ function safeReturnTo(candidate, options = {}) {
3
+ const fallback = options.fallback ?? "/";
4
+ if (!candidate || typeof candidate !== "string") return fallback;
5
+ const value = candidate.trim();
6
+ if (!value) return fallback;
7
+ if (/[\u0000-\u001f\u007f]/.test(value)) return fallback;
8
+ if (/^[/\\]{2}/.test(value)) return fallback;
9
+ if (value.startsWith("/")) return value;
10
+ const here = options.origin ?? (typeof location === "undefined" ? void 0 : location.origin);
11
+ const allowed = [here, ...options.allowedOrigins ?? []].filter(Boolean);
12
+ if (!allowed.length) return fallback;
13
+ let url;
14
+ try {
15
+ url = new URL(value);
16
+ } catch {
17
+ return fallback;
18
+ }
19
+ if (!allowed.includes(url.origin)) return fallback;
20
+ if (url.protocol !== "http:" && url.protocol !== "https:") return fallback;
21
+ return url.toString();
22
+ }
23
+
24
+ // src/types.ts
25
+ var OAuthError = class extends Error {
26
+ constructor(message, code, description) {
27
+ super(message);
28
+ this.code = code;
29
+ this.description = description;
30
+ this.name = "OAuthError";
31
+ }
32
+ };
33
+
34
+ // src/server-client.ts
35
+ var COOKIE_SESSION = "cookie";
36
+ function createServerClient(config, fetcher = fetch) {
37
+ const base = (config.basePath ?? "/auth").replace(/\/$/, "");
38
+ const url = (path) => `${base}${path}`;
39
+ const call = (path, init = {}) => fetcher(url(path), { credentials: "same-origin", ...init });
40
+ return {
41
+ async login(options = {}) {
42
+ const target = new URL(url("/login"), location.origin);
43
+ if (options.returnTo) target.searchParams.set("returnTo", options.returnTo);
44
+ for (const [key, value] of Object.entries(options.params ?? {})) {
45
+ target.searchParams.set(key, value);
46
+ }
47
+ location.assign(target.toString());
48
+ },
49
+ async handleCallback(params) {
50
+ const response = await call("/callback", {
51
+ method: "POST",
52
+ headers: { "content-type": "application/json" },
53
+ body: JSON.stringify(params)
54
+ });
55
+ if (!response.ok) {
56
+ const body = await response.json().catch(() => ({}));
57
+ throw new OAuthError(
58
+ String(body.error ?? `Callback failed: ${response.status}`),
59
+ String(body.error ?? "callback_failed")
60
+ );
61
+ }
62
+ return { accessToken: COOKIE_SESSION };
63
+ },
64
+ async refresh() {
65
+ const response = await call("/refresh", { method: "POST" });
66
+ if (!response.ok) {
67
+ throw new OAuthError("The server could not extend the session.", "refresh_failed");
68
+ }
69
+ return { accessToken: COOKIE_SESSION };
70
+ },
71
+ async fetchUser() {
72
+ const custom = config.userInfo;
73
+ const response = custom?.url ? await fetcher(custom.url, {
74
+ method: custom.method ?? "GET",
75
+ credentials: custom.credentials ?? "same-origin",
76
+ headers: { accept: "application/json", ...custom.headers }
77
+ }) : await call("/me");
78
+ if (!response.ok) {
79
+ throw new OAuthError(`The server refused the user request: ${response.status}`, "me_failed");
80
+ }
81
+ const raw = await response.json();
82
+ return custom?.map ? await custom.map(raw, COOKIE_SESSION) : raw;
83
+ },
84
+ /**
85
+ * Retire the session's tokens, which only the server can do here.
86
+ *
87
+ * The browser holds a cookie and no token, so it has nothing to send to a revocation
88
+ * endpoint. This asks the server's own route, which opens the cookie and revokes what
89
+ * is inside.
90
+ */
91
+ async revoke(_token, hint = "refresh_token") {
92
+ await call(`/revoke?hint=${hint}`, { method: "POST" }).catch(() => void 0);
93
+ },
94
+ async logout(options = {}) {
95
+ await call("/logout", { method: "POST" }).catch(() => void 0);
96
+ const target = options.returnTo ? safeReturnTo(options.returnTo, {
97
+ fallback: config.postLogoutRedirectUri ?? "/",
98
+ allowedOrigins: config.allowedReturnOrigins
99
+ }) : config.postLogoutRedirectUri ?? "/";
100
+ if (typeof location !== "undefined") location.assign(target);
101
+ },
102
+ async restore() {
103
+ const response = await call("/me");
104
+ return response.ok ? { accessToken: COOKIE_SESSION } : null;
105
+ }
106
+ };
107
+ }
108
+ export {
109
+ COOKIE_SESSION,
110
+ createServerClient
111
+ };