@fluixi/oauth2 1.0.0-alpha.3 → 1.0.0-alpha.5

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.
@@ -1,7 +1,7 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  import {
3
3
  __export
4
- } from "./chunk-4GSQS5YG.mjs";
4
+ } from "./chunk-DCBOCQLJ.mjs";
5
5
 
6
6
  // ../../node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/browser/runtime/webcrypto.js
7
7
  var webcrypto_default = crypto;
package/dist/browser.cjs CHANGED
@@ -165,9 +165,11 @@ async function loadJose2() {
165
165
  );
166
166
  }
167
167
  }
168
- async function createDpopKey(alg = "ES256") {
168
+ async function createDpopKey(alg = "ES256", options = {}) {
169
169
  const jose = await loadJose2();
170
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
170
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
171
+ extractable: options.extractable ?? false
172
+ });
171
173
  const publicJwk = { ...await jose.exportJWK(publicKey) };
172
174
  return {
173
175
  privateKey,
package/dist/browser.mjs CHANGED
@@ -131,9 +131,11 @@ async function loadJose2() {
131
131
  );
132
132
  }
133
133
  }
134
- async function createDpopKey(alg = "ES256") {
134
+ async function createDpopKey(alg = "ES256", options = {}) {
135
135
  const jose = await loadJose2();
136
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
136
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
137
+ extractable: options.extractable ?? false
138
+ });
137
139
  const publicJwk = { ...await jose.exportJWK(publicKey) };
138
140
  return {
139
141
  privateKey,
@@ -1,4 +1,4 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __export = (target, all) => {
4
4
  for (var name in all)
@@ -1,4 +1,4 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
 
3
3
  // src/types.ts
4
4
  var OAuthError = class extends Error {
@@ -1,13 +1,13 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  import {
3
3
  OAuthError
4
- } from "./chunk-IZ3BKW3J.mjs";
5
- import "./chunk-4GSQS5YG.mjs";
4
+ } from "./chunk-UDYQMLN5.mjs";
5
+ import "./chunk-DCBOCQLJ.mjs";
6
6
 
7
7
  // src/dpop.ts
8
8
  async function loadJose() {
9
9
  try {
10
- return await import("./browser-QQDMUXPB.mjs");
10
+ return await import("./browser-QTUJRR3J.mjs");
11
11
  } catch {
12
12
  throw new OAuthError(
13
13
  "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
@@ -15,9 +15,11 @@ async function loadJose() {
15
15
  );
16
16
  }
17
17
  }
18
- async function createDpopKey(alg = "ES256") {
18
+ async function createDpopKey(alg = "ES256", options = {}) {
19
19
  const jose = await loadJose();
20
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
20
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
21
+ extractable: options.extractable ?? false
22
+ });
21
23
  const publicJwk = { ...await jose.exportJWK(publicKey) };
22
24
  return {
23
25
  privateKey,
@@ -26,6 +28,22 @@ async function createDpopKey(alg = "ES256") {
26
28
  alg
27
29
  };
28
30
  }
31
+ async function exportDpopKey(key) {
32
+ const jose = await loadJose();
33
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
34
+ }
35
+ var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
36
+ async function importDpopKey(stored) {
37
+ const jose = await loadJose();
38
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
39
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
40
+ return {
41
+ privateKey,
42
+ publicJwk,
43
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
44
+ alg: stored.alg
45
+ };
46
+ }
29
47
  async function thumbprintOf(jwk) {
30
48
  const jose = await loadJose();
31
49
  return jose.calculateJwkThumbprint(jwk, "sha256");
@@ -73,6 +91,8 @@ function boundKeyOf(claims) {
73
91
  export {
74
92
  boundKeyOf,
75
93
  createDpopKey,
94
+ exportDpopKey,
95
+ importDpopKey,
76
96
  nonceFrom,
77
97
  proofFor,
78
98
  thumbprintOf,
@@ -14,9 +14,11 @@ async function loadJose() {
14
14
  );
15
15
  }
16
16
  }
17
- async function createDpopKey(alg = "ES256") {
17
+ async function createDpopKey(alg = "ES256", options = {}) {
18
18
  const jose = await loadJose();
19
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
19
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
20
+ extractable: options.extractable ?? false
21
+ });
20
22
  const publicJwk = { ...await jose.exportJWK(publicKey) };
21
23
  return {
22
24
  privateKey,
@@ -25,6 +27,22 @@ async function createDpopKey(alg = "ES256") {
25
27
  alg
26
28
  };
27
29
  }
30
+ async function exportDpopKey(key) {
31
+ const jose = await loadJose();
32
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
33
+ }
34
+ var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
35
+ async function importDpopKey(stored) {
36
+ const jose = await loadJose();
37
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
38
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
39
+ return {
40
+ privateKey,
41
+ publicJwk,
42
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
43
+ alg: stored.alg
44
+ };
45
+ }
28
46
  async function thumbprintOf(jwk) {
29
47
  const jose = await loadJose();
30
48
  return jose.calculateJwkThumbprint(jwk, "sha256");
@@ -72,6 +90,8 @@ function boundKeyOf(claims) {
72
90
  export {
73
91
  boundKeyOf,
74
92
  createDpopKey,
93
+ exportDpopKey,
94
+ importDpopKey,
75
95
  nonceFrom,
76
96
  proofFor,
77
97
  thumbprintOf,
@@ -56,9 +56,11 @@ async function loadJose() {
56
56
  );
57
57
  }
58
58
  }
59
- async function createDpopKey(alg = "ES256") {
59
+ async function createDpopKey(alg = "ES256", options = {}) {
60
60
  const jose = await loadJose();
61
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
61
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
62
+ extractable: options.extractable ?? false
63
+ });
62
64
  const publicJwk = { ...await jose.exportJWK(publicKey) };
63
65
  return {
64
66
  privateKey,
@@ -21,9 +21,11 @@ async function loadJose() {
21
21
  );
22
22
  }
23
23
  }
24
- async function createDpopKey(alg = "ES256") {
24
+ async function createDpopKey(alg = "ES256", options = {}) {
25
25
  const jose = await loadJose();
26
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
26
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
27
+ extractable: options.extractable ?? false
28
+ });
27
29
  const publicJwk = { ...await jose.exportJWK(publicKey) };
28
30
  return {
29
31
  privateKey,
package/dist/dpop.cjs CHANGED
@@ -32,6 +32,8 @@ var dpop_exports = {};
32
32
  __export(dpop_exports, {
33
33
  boundKeyOf: () => boundKeyOf,
34
34
  createDpopKey: () => createDpopKey,
35
+ exportDpopKey: () => exportDpopKey,
36
+ importDpopKey: () => importDpopKey,
35
37
  nonceFrom: () => nonceFrom,
36
38
  proofFor: () => proofFor,
37
39
  thumbprintOf: () => thumbprintOf,
@@ -60,9 +62,11 @@ async function loadJose() {
60
62
  );
61
63
  }
62
64
  }
63
- async function createDpopKey(alg = "ES256") {
65
+ async function createDpopKey(alg = "ES256", options = {}) {
64
66
  const jose = await loadJose();
65
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
67
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
68
+ extractable: options.extractable ?? false
69
+ });
66
70
  const publicJwk = { ...await jose.exportJWK(publicKey) };
67
71
  return {
68
72
  privateKey,
@@ -71,6 +75,22 @@ async function createDpopKey(alg = "ES256") {
71
75
  alg
72
76
  };
73
77
  }
78
+ async function exportDpopKey(key) {
79
+ const jose = await loadJose();
80
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
81
+ }
82
+ var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
83
+ async function importDpopKey(stored) {
84
+ const jose = await loadJose();
85
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
86
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
87
+ return {
88
+ privateKey,
89
+ publicJwk,
90
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
91
+ alg: stored.alg
92
+ };
93
+ }
74
94
  async function thumbprintOf(jwk) {
75
95
  const jose = await loadJose();
76
96
  return jose.calculateJwkThumbprint(jwk, "sha256");
package/dist/dpop.d.ts CHANGED
@@ -25,7 +25,24 @@ export interface DpopKey {
25
25
  * attacker who can run script can use the key while they are there, and cannot take it
26
26
  * away to use later.
27
27
  */
28
- export declare function createDpopKey(alg?: string): Promise<DpopKey>;
28
+ export declare function createDpopKey(alg?: string, options?: {
29
+ extractable?: boolean;
30
+ }): Promise<DpopKey>;
31
+ /**
32
+ * A key as a server session stores it: the private JWK and its algorithm.
33
+ *
34
+ * Only for a key held by a server, inside an encrypted httpOnly cookie. There it is bytes
35
+ * no page can read, and the session has nowhere else to keep it between requests. A
36
+ * browser key stays non-extractable in IndexedDB instead.
37
+ */
38
+ export interface StoredDpopKey {
39
+ jwk: Record<string, unknown>;
40
+ alg: string;
41
+ }
42
+ /** Export a key made with `extractable: true`, for a server session to store. */
43
+ export declare function exportDpopKey(key: DpopKey): Promise<StoredDpopKey>;
44
+ /** Rebuild a key a server session stored. The thumbprint comes out the same, which is the point. */
45
+ export declare function importDpopKey(stored: StoredDpopKey): Promise<DpopKey>;
29
46
  /** The thumbprint of a public JWK, for comparing against a token's `cnf.jkt`. */
30
47
  export declare function thumbprintOf(jwk: Record<string, unknown>): Promise<string>;
31
48
  export interface ProofOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../src/dpop.ts"],"names":[],"mappings":"AAkBA,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,iEAAiE;IACjE,UAAU,EAAE,SAAS,CAAC;IACtB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,GAAG,EAAE,MAAM,CAAC;CACb;AAcD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,GAAG,SAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAWnE;AAED,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhF;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAsCnF;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAGvE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAGzE"}
1
+ {"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../src/dpop.ts"],"names":[],"mappings":"AAkBA,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,iEAAiE;IACjE,UAAU,EAAE,SAAS,CAAC;IACtB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,GAAG,EAAE,MAAM,CAAC;CACb;AAcD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,GAAG,SAAU,EACb,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAGxE;AAID,oGAAoG;AACpG,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAW3E;AAED,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhF;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAsCnF;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAGvE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAGzE"}
package/dist/dpop.js CHANGED
@@ -35,9 +35,11 @@ async function loadJose() {
35
35
  * attacker who can run script can use the key while they are there, and cannot take it
36
36
  * away to use later.
37
37
  */
38
- export async function createDpopKey(alg = 'ES256') {
38
+ export async function createDpopKey(alg = 'ES256', options = {}) {
39
39
  const jose = await loadJose();
40
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
40
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
41
+ extractable: options.extractable ?? false,
42
+ });
41
43
  const publicJwk = { ...(await jose.exportJWK(publicKey)) };
42
44
  return {
43
45
  privateKey: privateKey,
@@ -46,6 +48,25 @@ export async function createDpopKey(alg = 'ES256') {
46
48
  alg,
47
49
  };
48
50
  }
51
+ /** Export a key made with `extractable: true`, for a server session to store. */
52
+ export async function exportDpopKey(key) {
53
+ const jose = await loadJose();
54
+ return { jwk: { ...(await jose.exportJWK(key.privateKey)) }, alg: key.alg };
55
+ }
56
+ const PRIVATE_MEMBERS = new Set(['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth']);
57
+ /** Rebuild a key a server session stored. The thumbprint comes out the same, which is the point. */
58
+ export async function importDpopKey(stored) {
59
+ const jose = await loadJose();
60
+ const privateKey = (await jose.importJWK(stored.jwk, stored.alg));
61
+ // The public half is the private JWK without its private members (RFC 7518 §6.2.2, §6.3.2).
62
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
63
+ return {
64
+ privateKey,
65
+ publicJwk,
66
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, 'sha256'),
67
+ alg: stored.alg,
68
+ };
69
+ }
49
70
  /** The thumbprint of a public JWK, for comparing against a token's `cnf.jkt`. */
50
71
  export async function thumbprintOf(jwk) {
51
72
  const jose = await loadJose();
package/dist/dpop.mjs CHANGED
@@ -21,9 +21,11 @@ async function loadJose() {
21
21
  );
22
22
  }
23
23
  }
24
- async function createDpopKey(alg = "ES256") {
24
+ async function createDpopKey(alg = "ES256", options = {}) {
25
25
  const jose = await loadJose();
26
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
26
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
27
+ extractable: options.extractable ?? false
28
+ });
27
29
  const publicJwk = { ...await jose.exportJWK(publicKey) };
28
30
  return {
29
31
  privateKey,
@@ -32,6 +34,22 @@ async function createDpopKey(alg = "ES256") {
32
34
  alg
33
35
  };
34
36
  }
37
+ async function exportDpopKey(key) {
38
+ const jose = await loadJose();
39
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
40
+ }
41
+ var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
42
+ async function importDpopKey(stored) {
43
+ const jose = await loadJose();
44
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
45
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
46
+ return {
47
+ privateKey,
48
+ publicJwk,
49
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
50
+ alg: stored.alg
51
+ };
52
+ }
35
53
  async function thumbprintOf(jwk) {
36
54
  const jose = await loadJose();
37
55
  return jose.calculateJwkThumbprint(jwk, "sha256");
@@ -79,6 +97,8 @@ function boundKeyOf(claims) {
79
97
  export {
80
98
  boundKeyOf,
81
99
  createDpopKey,
100
+ exportDpopKey,
101
+ importDpopKey,
82
102
  nonceFrom,
83
103
  proofFor,
84
104
  thumbprintOf,
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -213,9 +213,11 @@ async function loadJose2() {
213
213
  );
214
214
  }
215
215
  }
216
- async function createDpopKey(alg = "ES256") {
216
+ async function createDpopKey(alg = "ES256", options = {}) {
217
217
  const jose = await loadJose2();
218
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
218
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
219
+ extractable: options.extractable ?? false
220
+ });
219
221
  const publicJwk = { ...await jose.exportJWK(publicKey) };
220
222
  return {
221
223
  privateKey,
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
- import "./chunk-4GSQS5YG.mjs";
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ import "./chunk-DCBOCQLJ.mjs";
3
3
 
4
4
  // src/service.ts
5
5
  import { createSession } from "@fluixi/session";
@@ -111,7 +111,7 @@ function resolveMethod(auth) {
111
111
  }
112
112
  async function loadJose() {
113
113
  try {
114
- return await import("./browser-QQDMUXPB.mjs");
114
+ return await import("./browser-QTUJRR3J.mjs");
115
115
  } catch {
116
116
  throw new OAuthError(
117
117
  "private_key_jwt needs `jose`. Install it: npm i jose. It is an optional peer, so a client using a secret does not carry it.",
@@ -151,7 +151,7 @@ async function applyClientAuth(auth, endpoint, headers, form) {
151
151
  // src/dpop.ts
152
152
  async function loadJose2() {
153
153
  try {
154
- return await import("./browser-QQDMUXPB.mjs");
154
+ return await import("./browser-QTUJRR3J.mjs");
155
155
  } catch {
156
156
  throw new OAuthError(
157
157
  "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
@@ -159,9 +159,11 @@ async function loadJose2() {
159
159
  );
160
160
  }
161
161
  }
162
- async function createDpopKey(alg = "ES256") {
162
+ async function createDpopKey(alg = "ES256", options = {}) {
163
163
  const jose = await loadJose2();
164
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
164
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
165
+ extractable: options.extractable ?? false
166
+ });
165
167
  const publicJwk = { ...await jose.exportJWK(publicKey) };
166
168
  return {
167
169
  privateKey,
package/dist/server.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.3 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -207,8 +207,106 @@ function expiryFrom(expiresIn, now = Date.now()) {
207
207
  return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
208
208
  }
209
209
 
210
+ // src/dpop.ts
211
+ async function loadJose2() {
212
+ try {
213
+ return await import("jose");
214
+ } catch {
215
+ throw new OAuthError(
216
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
217
+ "jose_missing"
218
+ );
219
+ }
220
+ }
221
+ async function createDpopKey(alg = "ES256", options = {}) {
222
+ const jose = await loadJose2();
223
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
224
+ extractable: options.extractable ?? false
225
+ });
226
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
227
+ return {
228
+ privateKey,
229
+ publicJwk,
230
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
231
+ alg
232
+ };
233
+ }
234
+ async function exportDpopKey(key) {
235
+ const jose = await loadJose2();
236
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
237
+ }
238
+ var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
239
+ async function importDpopKey(stored) {
240
+ const jose = await loadJose2();
241
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
242
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
243
+ return {
244
+ privateKey,
245
+ publicJwk,
246
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
247
+ alg: stored.alg
248
+ };
249
+ }
250
+ async function proofFor(key, options) {
251
+ const jose = await loadJose2();
252
+ const htu = (() => {
253
+ try {
254
+ const url = new URL(options.url);
255
+ url.search = "";
256
+ url.hash = "";
257
+ return url.toString();
258
+ } catch {
259
+ return options.url;
260
+ }
261
+ })();
262
+ const claims = {
263
+ htm: options.method.toUpperCase(),
264
+ htu,
265
+ jti: crypto.randomUUID()
266
+ };
267
+ if (options.accessToken) {
268
+ const digest = await crypto.subtle.digest(
269
+ "SHA-256",
270
+ new TextEncoder().encode(options.accessToken)
271
+ );
272
+ let binary = "";
273
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
274
+ claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
275
+ }
276
+ if (options.nonce) claims.nonce = options.nonce;
277
+ return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
278
+ }
279
+ function nonceFrom(response) {
280
+ return response.headers.get("dpop-nonce");
281
+ }
282
+
210
283
  // src/server.ts
211
284
  var import_app = require("@fluixi/start/app");
285
+ var dpopNonces = /* @__PURE__ */ new Map();
286
+ async function prove(headers, key, method, url, accessToken) {
287
+ headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
288
+ }
289
+ function learnNonce(url, response) {
290
+ const offered = nonceFrom(response);
291
+ if (!offered) return false;
292
+ const origin = new URL(url).origin;
293
+ const fresh = offered !== dpopNonces.get(origin);
294
+ dpopNonces.set(origin, offered);
295
+ return fresh && !response.ok;
296
+ }
297
+ async function withToken(url, session) {
298
+ const key = session.dpop && session.tokenType?.toLowerCase() === "dpop" ? await importDpopKey(session.dpop) : null;
299
+ const send = async () => {
300
+ const headers = {
301
+ authorization: `${key ? "DPoP" : "Bearer"} ${session.accessToken}`
302
+ };
303
+ if (key) await prove(headers, key, "GET", url, session.accessToken);
304
+ return fetch(url, { headers });
305
+ };
306
+ let response = await send();
307
+ if (key && learnNonce(url, response)) response = await send();
308
+ return response;
309
+ }
212
310
  function oauthRoutes(config) {
213
311
  const base = (config.basePath ?? "/auth").replace(/\/$/, "");
214
312
  const scopes = config.scopes ?? ["openid", "profile", "email"];
@@ -240,14 +338,19 @@ function oauthRoutes(config) {
240
338
  keyId: config.keyId,
241
339
  assertionAlgorithm: config.assertionAlgorithm
242
340
  };
243
- async function tokenRequest(body) {
341
+ async function tokenRequest(body, key = null) {
244
342
  const { token } = await resolve();
245
- const headers = {
246
- "content-type": "application/x-www-form-urlencoded"
343
+ const attempt = async () => {
344
+ const headers = {
345
+ "content-type": "application/x-www-form-urlencoded"
346
+ };
347
+ const form = new URLSearchParams(body);
348
+ await applyClientAuth(clientAuth, token, headers, form);
349
+ if (key) await prove(headers, key, "POST", token);
350
+ return fetch(token, { method: "POST", headers, body: form.toString() });
247
351
  };
248
- const form = new URLSearchParams(body);
249
- await applyClientAuth(clientAuth, token, headers, form);
250
- const response = await fetch(token, { method: "POST", headers, body: form.toString() });
352
+ let response = await attempt();
353
+ if (key && learnNonce(token, response)) response = await attempt();
251
354
  const payload = await response.json().catch(() => ({}));
252
355
  if (!response.ok) {
253
356
  throw new OAuthError(
@@ -314,17 +417,23 @@ function oauthRoutes(config) {
314
417
  if (!pending || !state || !timingSafeEqual(pending.state, state)) {
315
418
  return json({ error: "state_mismatch" }, 400);
316
419
  }
317
- const payload = await tokenRequest({
318
- grant_type: "authorization_code",
319
- code,
320
- redirect_uri: config.redirectUri,
321
- code_verifier: pending.verifier
322
- });
420
+ const key = config.dpop ? await createDpopKey(config.dpopAlgorithm, { extractable: true }) : null;
421
+ const payload = await tokenRequest(
422
+ {
423
+ grant_type: "authorization_code",
424
+ code,
425
+ redirect_uri: config.redirectUri,
426
+ code_verifier: pending.verifier
427
+ },
428
+ key
429
+ );
323
430
  const signedIn = await cookie.write({
324
431
  accessToken: String(payload.access_token ?? ""),
325
432
  refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : void 0,
326
433
  idToken: typeof payload.id_token === "string" ? payload.id_token : void 0,
327
- expiresAt: expiryFrom(payload.expires_in)
434
+ expiresAt: expiryFrom(payload.expires_in),
435
+ tokenType: typeof payload.token_type === "string" ? payload.token_type : void 0,
436
+ dpop: key ? await exportDpopKey(key) : void 0
328
437
  });
329
438
  return new Response(null, {
330
439
  status: 302,
@@ -344,17 +453,20 @@ function oauthRoutes(config) {
344
453
  if (route === "/refresh") {
345
454
  const current = await session();
346
455
  if (!current?.refreshToken) return json({ error: "no_session" }, 401);
347
- const payload = await tokenRequest({
348
- grant_type: "refresh_token",
349
- refresh_token: current.refreshToken
350
- });
456
+ const key = current.dpop ? await importDpopKey(current.dpop) : null;
457
+ const payload = await tokenRequest(
458
+ { grant_type: "refresh_token", refresh_token: current.refreshToken },
459
+ key
460
+ );
351
461
  const refreshed = await cookie.write({
352
462
  accessToken: String(payload.access_token ?? ""),
353
463
  // A provider that rotates refresh tokens sends a new one; one that does not
354
464
  // expects the old one to keep working.
355
465
  refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
356
466
  idToken: current.idToken,
357
- expiresAt: expiryFrom(payload.expires_in)
467
+ expiresAt: expiryFrom(payload.expires_in),
468
+ tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
469
+ dpop: current.dpop
358
470
  });
359
471
  return json({ ok: true }, 200, { "set-cookie": refreshed });
360
472
  }
@@ -363,9 +475,7 @@ function oauthRoutes(config) {
363
475
  if (!current?.accessToken) return json({ error: "no_session" }, 401);
364
476
  const { userinfo } = await resolve();
365
477
  if (!userinfo) return json({ error: "no_userinfo" }, 501);
366
- const response = await fetch(userinfo, {
367
- headers: { authorization: `Bearer ${current.accessToken}` }
368
- });
478
+ const response = await withToken(userinfo, current);
369
479
  if (!response.ok) return json({ error: "userinfo_failed" }, response.status);
370
480
  const claims = await response.json();
371
481
  return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
@@ -455,9 +565,7 @@ function sessionLocals(config) {
455
565
  try {
456
566
  const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
457
567
  if (endpoints?.userinfo) {
458
- const response = await fetch(endpoints.userinfo, {
459
- headers: { authorization: `Bearer ${session.accessToken}` }
460
- });
568
+ const response = await withToken(endpoints.userinfo, session);
461
569
  if (response.ok) {
462
570
  const claims = await response.json();
463
571
  user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
package/dist/server.d.ts CHANGED
@@ -53,6 +53,17 @@ export interface OAuthRoutesConfig {
53
53
  userCacheMs?: number;
54
54
  /** Map the provider's userinfo onto the app's own shape. */
55
55
  mapUser?: (claims: Record<string, unknown>, accessToken: string) => unknown;
56
+ /**
57
+ * Bind tokens to a key with DPoP (RFC 9449), which a provider may require: an Okta org
58
+ * that mandates it refuses a server-mode client without it.
59
+ *
60
+ * The key is made per session at the code exchange and kept in the encrypted session
61
+ * cookie, since a refresh token is bound to the key that obtained it and a refresh with
62
+ * another key is refused. Needs `jose`.
63
+ */
64
+ dpop?: boolean;
65
+ /** Default `ES256`. Must be in the provider's `dpop_signing_alg_values_supported`. */
66
+ dpopAlgorithm?: string;
56
67
  }
57
68
  /**
58
69
  * The middleware.