@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,79 @@
1
+ import {
2
+ OAuthError
3
+ } from "./chunk-OGSFNTKA.mjs";
4
+ import "./chunk-7P6ASYW6.mjs";
5
+
6
+ // src/dpop.ts
7
+ async function loadJose() {
8
+ try {
9
+ return await import("./browser-FDUOEUM2.mjs");
10
+ } catch {
11
+ throw new OAuthError(
12
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
13
+ "jose_missing"
14
+ );
15
+ }
16
+ }
17
+ async function createDpopKey(alg = "ES256") {
18
+ const jose = await loadJose();
19
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
20
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
21
+ return {
22
+ privateKey,
23
+ publicJwk,
24
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
25
+ alg
26
+ };
27
+ }
28
+ async function thumbprintOf(jwk) {
29
+ const jose = await loadJose();
30
+ return jose.calculateJwkThumbprint(jwk, "sha256");
31
+ }
32
+ async function proofFor(key, options) {
33
+ const jose = await loadJose();
34
+ const htu = (() => {
35
+ try {
36
+ const url = new URL(options.url);
37
+ url.search = "";
38
+ url.hash = "";
39
+ return url.toString();
40
+ } catch {
41
+ return options.url;
42
+ }
43
+ })();
44
+ const claims = {
45
+ htm: options.method.toUpperCase(),
46
+ htu,
47
+ jti: crypto.randomUUID()
48
+ };
49
+ if (options.accessToken) {
50
+ const digest = await crypto.subtle.digest(
51
+ "SHA-256",
52
+ new TextEncoder().encode(options.accessToken)
53
+ );
54
+ let binary = "";
55
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
56
+ claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
57
+ }
58
+ if (options.nonce) claims.nonce = options.nonce;
59
+ return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
60
+ }
61
+ function nonceFrom(response) {
62
+ return response.headers.get("dpop-nonce");
63
+ }
64
+ function wantsNonce(error, response) {
65
+ const code = typeof error === "string" ? error : error?.error;
66
+ return code === "use_dpop_nonce" || !!response && !!nonceFrom(response);
67
+ }
68
+ function boundKeyOf(claims) {
69
+ const cnf = claims.cnf;
70
+ return typeof cnf?.jkt === "string" ? cnf.jkt : null;
71
+ }
72
+ export {
73
+ boundKeyOf,
74
+ createDpopKey,
75
+ nonceFrom,
76
+ proofFor,
77
+ thumbprintOf,
78
+ wantsNonce
79
+ };
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/dpop-store.ts
31
+ var dpop_store_exports = {};
32
+ __export(dpop_store_exports, {
33
+ forgetKey: () => forgetKey,
34
+ keyFor: () => keyFor
35
+ });
36
+ module.exports = __toCommonJS(dpop_store_exports);
37
+
38
+ // src/types.ts
39
+ var OAuthError = class extends Error {
40
+ constructor(message, code, description) {
41
+ super(message);
42
+ this.code = code;
43
+ this.description = description;
44
+ this.name = "OAuthError";
45
+ }
46
+ };
47
+
48
+ // src/dpop.ts
49
+ async function loadJose() {
50
+ try {
51
+ return await import("jose");
52
+ } catch {
53
+ throw new OAuthError(
54
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
55
+ "jose_missing"
56
+ );
57
+ }
58
+ }
59
+ async function createDpopKey(alg = "ES256") {
60
+ const jose = await loadJose();
61
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
62
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
63
+ return {
64
+ privateKey,
65
+ publicJwk,
66
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
67
+ alg
68
+ };
69
+ }
70
+
71
+ // src/dpop-store.ts
72
+ var DB = "fluixi-oauth2-dpop";
73
+ var STORE = "keys";
74
+ function available() {
75
+ return typeof indexedDB !== "undefined";
76
+ }
77
+ function open() {
78
+ return new Promise((resolve, reject) => {
79
+ const request = indexedDB.open(DB, 1);
80
+ request.onupgradeneeded = () => {
81
+ const db = request.result;
82
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
83
+ };
84
+ request.onsuccess = () => resolve(request.result);
85
+ request.onerror = () => reject(request.error ?? new Error("indexedDB.open failed"));
86
+ });
87
+ }
88
+ function act(mode, run) {
89
+ return open().then(
90
+ (db) => new Promise((resolve, reject) => {
91
+ const tx = db.transaction(STORE, mode);
92
+ const request = run(tx.objectStore(STORE));
93
+ request.onsuccess = () => resolve(request.result);
94
+ request.onerror = () => reject(request.error ?? new Error("DPoP key request failed"));
95
+ tx.oncomplete = () => db.close();
96
+ })
97
+ );
98
+ }
99
+ function usable(value) {
100
+ const stored = value;
101
+ return !!stored && typeof stored.thumbprint === "string" && typeof stored.alg === "string" && !!stored.publicJwk && // A CryptoKey survives the round trip as a CryptoKey. Anything else means the record was
102
+ // written by something other than this, and binding to it would fail at the provider.
103
+ typeof stored.privateKey?.type === "string";
104
+ }
105
+ async function keyFor(clientKey, alg = "ES256", onEphemeral) {
106
+ if (!available()) {
107
+ onEphemeral?.("This environment has no IndexedDB, so the key lasts only as long as this document.");
108
+ return createDpopKey(alg);
109
+ }
110
+ try {
111
+ const found = await act("readonly", (store) => store.get(clientKey));
112
+ if (usable(found) && found.alg === alg) {
113
+ return {
114
+ privateKey: found.privateKey,
115
+ publicJwk: found.publicJwk,
116
+ thumbprint: found.thumbprint,
117
+ alg: found.alg
118
+ };
119
+ }
120
+ } catch (cause) {
121
+ onEphemeral?.(
122
+ `The stored DPoP key could not be read (${cause instanceof Error ? cause.message : String(cause)}), so a new one was made.`
123
+ );
124
+ }
125
+ const key = await createDpopKey(alg);
126
+ try {
127
+ await act(
128
+ "readwrite",
129
+ (store) => store.put(
130
+ {
131
+ privateKey: key.privateKey,
132
+ publicJwk: key.publicJwk,
133
+ thumbprint: key.thumbprint,
134
+ alg: key.alg
135
+ },
136
+ clientKey
137
+ )
138
+ );
139
+ } catch (cause) {
140
+ onEphemeral?.(
141
+ `The DPoP key could not be stored (${cause instanceof Error ? cause.message : String(cause)}), so a refresh after the next navigation will be refused.`
142
+ );
143
+ }
144
+ return key;
145
+ }
146
+ async function forgetKey(clientKey) {
147
+ if (!available()) return;
148
+ try {
149
+ await act("readwrite", (store) => store.delete(clientKey));
150
+ } catch {
151
+ }
152
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Keeping a DPoP key across page loads.
3
+ *
4
+ * RFC 9449 §5 binds a refresh token to the key that obtained it, and a refresh must be
5
+ * proved with that same key. A key held only in a variable does not survive a navigation,
6
+ * so the refresh token it came with is unusable from the next document onwards. Okta says so
7
+ * plainly: `invalid_dpop_proof: Invalid DPoP Public Key binding in refresh token`. Browser
8
+ * mode reaches a new document on every sign in, because the callback navigates, so without
9
+ * persistence DPoP and refresh cannot both work.
10
+ *
11
+ * IndexedDB rather than `localStorage`, because it stores a `CryptoKey` itself. The
12
+ * structured clone of a key created with `extractable: false` keeps that property, so the
13
+ * private half is written by the browser, read back by the browser, and never becomes bytes
14
+ * any script can see. `localStorage` holds strings, which would mean exporting the key, and
15
+ * an exported key in storage is a password with extra steps.
16
+ *
17
+ * One key per client, so two clients on the same origin cannot bind to each other's.
18
+ */
19
+ import { type DpopKey } from './dpop.js';
20
+ /**
21
+ * The key for `clientKey`, from storage when one is there and newly made when not.
22
+ *
23
+ * A failure to persist is not a failure to authenticate: the key is still returned, so the
24
+ * flow continues with a key that lasts as long as the document. `onEphemeral` says when that
25
+ * happened, because the consequence is real and silent otherwise, a refresh will be refused
26
+ * after the next navigation.
27
+ */
28
+ export declare function keyFor(clientKey: string, alg?: string, onEphemeral?: (reason: string) => void): Promise<DpopKey>;
29
+ /**
30
+ * Forget the key for `clientKey`.
31
+ *
32
+ * Part of signing out. A key left behind is not a credential on its own, since the tokens
33
+ * bound to it are gone, but the next sign in should not be provably the same client as the
34
+ * last: that is a correlator handed to the provider for nothing.
35
+ */
36
+ export declare function forgetKey(clientKey: string): Promise<void>;
37
+ //# sourceMappingURL=dpop-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpop-store.d.ts","sourceRoot":"","sources":["../src/dpop-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AA6DxD;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,SAAS,EAAE,MAAM,EACjB,GAAG,SAAU,EACb,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GACrC,OAAO,CAAC,OAAO,CAAC,CA4ClB;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhE"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Keeping a DPoP key across page loads.
3
+ *
4
+ * RFC 9449 §5 binds a refresh token to the key that obtained it, and a refresh must be
5
+ * proved with that same key. A key held only in a variable does not survive a navigation,
6
+ * so the refresh token it came with is unusable from the next document onwards. Okta says so
7
+ * plainly: `invalid_dpop_proof: Invalid DPoP Public Key binding in refresh token`. Browser
8
+ * mode reaches a new document on every sign in, because the callback navigates, so without
9
+ * persistence DPoP and refresh cannot both work.
10
+ *
11
+ * IndexedDB rather than `localStorage`, because it stores a `CryptoKey` itself. The
12
+ * structured clone of a key created with `extractable: false` keeps that property, so the
13
+ * private half is written by the browser, read back by the browser, and never becomes bytes
14
+ * any script can see. `localStorage` holds strings, which would mean exporting the key, and
15
+ * an exported key in storage is a password with extra steps.
16
+ *
17
+ * One key per client, so two clients on the same origin cannot bind to each other's.
18
+ */
19
+ import { createDpopKey } from './dpop.js';
20
+ const DB = 'fluixi-oauth2-dpop';
21
+ const STORE = 'keys';
22
+ /** Whether this environment can persist a key at all. */
23
+ function available() {
24
+ return typeof indexedDB !== 'undefined';
25
+ }
26
+ function open() {
27
+ return new Promise((resolve, reject) => {
28
+ const request = indexedDB.open(DB, 1);
29
+ request.onupgradeneeded = () => {
30
+ const db = request.result;
31
+ if (!db.objectStoreNames.contains(STORE))
32
+ db.createObjectStore(STORE);
33
+ };
34
+ request.onsuccess = () => resolve(request.result);
35
+ request.onerror = () => reject(request.error ?? new Error('indexedDB.open failed'));
36
+ });
37
+ }
38
+ /** One transaction, wrapped so a caller sees a promise rather than three events. */
39
+ function act(mode, run) {
40
+ return open().then((db) => new Promise((resolve, reject) => {
41
+ const tx = db.transaction(STORE, mode);
42
+ const request = run(tx.objectStore(STORE));
43
+ request.onsuccess = () => resolve(request.result);
44
+ request.onerror = () => reject(request.error ?? new Error('DPoP key request failed'));
45
+ tx.oncomplete = () => db.close();
46
+ }));
47
+ }
48
+ /** Whether what came back is a usable key rather than a partial or stale record. */
49
+ function usable(value) {
50
+ const stored = value;
51
+ return (!!stored &&
52
+ typeof stored.thumbprint === 'string' &&
53
+ typeof stored.alg === 'string' &&
54
+ !!stored.publicJwk &&
55
+ // A CryptoKey survives the round trip as a CryptoKey. Anything else means the record was
56
+ // written by something other than this, and binding to it would fail at the provider.
57
+ typeof stored.privateKey?.type === 'string');
58
+ }
59
+ /**
60
+ * The key for `clientKey`, from storage when one is there and newly made when not.
61
+ *
62
+ * A failure to persist is not a failure to authenticate: the key is still returned, so the
63
+ * flow continues with a key that lasts as long as the document. `onEphemeral` says when that
64
+ * happened, because the consequence is real and silent otherwise, a refresh will be refused
65
+ * after the next navigation.
66
+ */
67
+ export async function keyFor(clientKey, alg = 'ES256', onEphemeral) {
68
+ if (!available()) {
69
+ onEphemeral?.('This environment has no IndexedDB, so the key lasts only as long as this document.');
70
+ return createDpopKey(alg);
71
+ }
72
+ try {
73
+ const found = await act('readonly', (store) => store.get(clientKey));
74
+ // The algorithm is part of the identity: a config changed from ES256 to RS256 must not
75
+ // keep proving with the old key, since the provider was told which to expect.
76
+ if (usable(found) && found.alg === alg) {
77
+ return {
78
+ privateKey: found.privateKey,
79
+ publicJwk: found.publicJwk,
80
+ thumbprint: found.thumbprint,
81
+ alg: found.alg,
82
+ };
83
+ }
84
+ }
85
+ catch (cause) {
86
+ onEphemeral?.(`The stored DPoP key could not be read (${cause instanceof Error ? cause.message : String(cause)}), so a new one was made.`);
87
+ }
88
+ const key = await createDpopKey(alg);
89
+ try {
90
+ await act('readwrite', (store) => store.put({
91
+ privateKey: key.privateKey,
92
+ publicJwk: key.publicJwk,
93
+ thumbprint: key.thumbprint,
94
+ alg: key.alg,
95
+ }, clientKey));
96
+ }
97
+ catch (cause) {
98
+ // Private browsing and a full disk both land here.
99
+ onEphemeral?.(`The DPoP key could not be stored (${cause instanceof Error ? cause.message : String(cause)}), so a refresh after the next navigation will be refused.`);
100
+ }
101
+ return key;
102
+ }
103
+ /**
104
+ * Forget the key for `clientKey`.
105
+ *
106
+ * Part of signing out. A key left behind is not a credential on its own, since the tokens
107
+ * bound to it are gone, but the next sign in should not be provably the same client as the
108
+ * last: that is a correlator handed to the provider for nothing.
109
+ */
110
+ export async function forgetKey(clientKey) {
111
+ if (!available())
112
+ return;
113
+ try {
114
+ await act('readwrite', (store) => store.delete(clientKey));
115
+ }
116
+ catch {
117
+ // Nothing useful to do. The tokens are already gone, which is what signs someone out.
118
+ }
119
+ }
@@ -0,0 +1,121 @@
1
+ import "./chunk-7P6ASYW6.mjs";
2
+
3
+ // src/types.ts
4
+ var OAuthError = class extends Error {
5
+ constructor(message, code, description) {
6
+ super(message);
7
+ this.code = code;
8
+ this.description = description;
9
+ this.name = "OAuthError";
10
+ }
11
+ };
12
+
13
+ // src/dpop.ts
14
+ async function loadJose() {
15
+ try {
16
+ return await import("./browser-FDUOEUM2.mjs");
17
+ } catch {
18
+ throw new OAuthError(
19
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
20
+ "jose_missing"
21
+ );
22
+ }
23
+ }
24
+ async function createDpopKey(alg = "ES256") {
25
+ const jose = await loadJose();
26
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
27
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
28
+ return {
29
+ privateKey,
30
+ publicJwk,
31
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
32
+ alg
33
+ };
34
+ }
35
+
36
+ // src/dpop-store.ts
37
+ var DB = "fluixi-oauth2-dpop";
38
+ var STORE = "keys";
39
+ function available() {
40
+ return typeof indexedDB !== "undefined";
41
+ }
42
+ function open() {
43
+ return new Promise((resolve, reject) => {
44
+ const request = indexedDB.open(DB, 1);
45
+ request.onupgradeneeded = () => {
46
+ const db = request.result;
47
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
48
+ };
49
+ request.onsuccess = () => resolve(request.result);
50
+ request.onerror = () => reject(request.error ?? new Error("indexedDB.open failed"));
51
+ });
52
+ }
53
+ function act(mode, run) {
54
+ return open().then(
55
+ (db) => new Promise((resolve, reject) => {
56
+ const tx = db.transaction(STORE, mode);
57
+ const request = run(tx.objectStore(STORE));
58
+ request.onsuccess = () => resolve(request.result);
59
+ request.onerror = () => reject(request.error ?? new Error("DPoP key request failed"));
60
+ tx.oncomplete = () => db.close();
61
+ })
62
+ );
63
+ }
64
+ function usable(value) {
65
+ const stored = value;
66
+ return !!stored && typeof stored.thumbprint === "string" && typeof stored.alg === "string" && !!stored.publicJwk && // A CryptoKey survives the round trip as a CryptoKey. Anything else means the record was
67
+ // written by something other than this, and binding to it would fail at the provider.
68
+ typeof stored.privateKey?.type === "string";
69
+ }
70
+ async function keyFor(clientKey, alg = "ES256", onEphemeral) {
71
+ if (!available()) {
72
+ onEphemeral?.("This environment has no IndexedDB, so the key lasts only as long as this document.");
73
+ return createDpopKey(alg);
74
+ }
75
+ try {
76
+ const found = await act("readonly", (store) => store.get(clientKey));
77
+ if (usable(found) && found.alg === alg) {
78
+ return {
79
+ privateKey: found.privateKey,
80
+ publicJwk: found.publicJwk,
81
+ thumbprint: found.thumbprint,
82
+ alg: found.alg
83
+ };
84
+ }
85
+ } catch (cause) {
86
+ onEphemeral?.(
87
+ `The stored DPoP key could not be read (${cause instanceof Error ? cause.message : String(cause)}), so a new one was made.`
88
+ );
89
+ }
90
+ const key = await createDpopKey(alg);
91
+ try {
92
+ await act(
93
+ "readwrite",
94
+ (store) => store.put(
95
+ {
96
+ privateKey: key.privateKey,
97
+ publicJwk: key.publicJwk,
98
+ thumbprint: key.thumbprint,
99
+ alg: key.alg
100
+ },
101
+ clientKey
102
+ )
103
+ );
104
+ } catch (cause) {
105
+ onEphemeral?.(
106
+ `The DPoP key could not be stored (${cause instanceof Error ? cause.message : String(cause)}), so a refresh after the next navigation will be refused.`
107
+ );
108
+ }
109
+ return key;
110
+ }
111
+ async function forgetKey(clientKey) {
112
+ if (!available()) return;
113
+ try {
114
+ await act("readwrite", (store) => store.delete(clientKey));
115
+ } catch {
116
+ }
117
+ }
118
+ export {
119
+ forgetKey,
120
+ keyFor
121
+ };
package/dist/dpop.cjs ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/dpop.ts
31
+ var dpop_exports = {};
32
+ __export(dpop_exports, {
33
+ boundKeyOf: () => boundKeyOf,
34
+ createDpopKey: () => createDpopKey,
35
+ nonceFrom: () => nonceFrom,
36
+ proofFor: () => proofFor,
37
+ thumbprintOf: () => thumbprintOf,
38
+ wantsNonce: () => wantsNonce
39
+ });
40
+ module.exports = __toCommonJS(dpop_exports);
41
+
42
+ // src/types.ts
43
+ var OAuthError = class extends Error {
44
+ constructor(message, code, description) {
45
+ super(message);
46
+ this.code = code;
47
+ this.description = description;
48
+ this.name = "OAuthError";
49
+ }
50
+ };
51
+
52
+ // src/dpop.ts
53
+ async function loadJose() {
54
+ try {
55
+ return await import("jose");
56
+ } catch {
57
+ throw new OAuthError(
58
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
59
+ "jose_missing"
60
+ );
61
+ }
62
+ }
63
+ async function createDpopKey(alg = "ES256") {
64
+ const jose = await loadJose();
65
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
66
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
67
+ return {
68
+ privateKey,
69
+ publicJwk,
70
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
71
+ alg
72
+ };
73
+ }
74
+ async function thumbprintOf(jwk) {
75
+ const jose = await loadJose();
76
+ return jose.calculateJwkThumbprint(jwk, "sha256");
77
+ }
78
+ async function proofFor(key, options) {
79
+ const jose = await loadJose();
80
+ const htu = (() => {
81
+ try {
82
+ const url = new URL(options.url);
83
+ url.search = "";
84
+ url.hash = "";
85
+ return url.toString();
86
+ } catch {
87
+ return options.url;
88
+ }
89
+ })();
90
+ const claims = {
91
+ htm: options.method.toUpperCase(),
92
+ htu,
93
+ jti: crypto.randomUUID()
94
+ };
95
+ if (options.accessToken) {
96
+ const digest = await crypto.subtle.digest(
97
+ "SHA-256",
98
+ new TextEncoder().encode(options.accessToken)
99
+ );
100
+ let binary = "";
101
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
102
+ claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
103
+ }
104
+ if (options.nonce) claims.nonce = options.nonce;
105
+ return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
106
+ }
107
+ function nonceFrom(response) {
108
+ return response.headers.get("dpop-nonce");
109
+ }
110
+ function wantsNonce(error, response) {
111
+ const code = typeof error === "string" ? error : error?.error;
112
+ return code === "use_dpop_nonce" || !!response && !!nonceFrom(response);
113
+ }
114
+ function boundKeyOf(claims) {
115
+ const cnf = claims.cnf;
116
+ return typeof cnf?.jkt === "string" ? cnf.jkt : null;
117
+ }