@fluixi/oauth2 1.0.0-alpha.1 → 1.0.0-alpha.10

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 (45) hide show
  1. package/dist/{browser-ONMHX3Z3.mjs → browser-36FHB6KG.mjs} +2 -2
  2. package/dist/browser.cjs +4 -2
  3. package/dist/browser.mjs +4 -2
  4. package/dist/callback.cjs +6 -5
  5. package/dist/callback.mjs +1 -0
  6. package/dist/{chunk-4NSOGPLM.mjs → chunk-F52RUVDM.mjs} +1 -1
  7. package/dist/{chunk-25WE6UWI.mjs → chunk-OGS6MQ3D.mjs} +1 -1
  8. package/dist/{dpop-LVPWWADA.mjs → dpop-GOWCCCFR.mjs} +26 -6
  9. package/dist/{dpop-NS7NKOWM.mjs → dpop-YPGCGL7S.mjs} +22 -2
  10. package/dist/dpop-store.cjs +4 -2
  11. package/dist/dpop-store.mjs +4 -2
  12. package/dist/dpop.cjs +22 -2
  13. package/dist/dpop.d.ts +18 -1
  14. package/dist/dpop.d.ts.map +1 -1
  15. package/dist/dpop.js +23 -2
  16. package/dist/dpop.mjs +22 -2
  17. package/dist/index.cjs +28 -3
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.mjs +31 -6
  22. package/dist/module.cjs +44 -0
  23. package/dist/module.d.ts +13 -0
  24. package/dist/module.d.ts.map +1 -0
  25. package/dist/module.js +22 -0
  26. package/dist/module.mjs +23 -0
  27. package/dist/server.cjs +138 -25
  28. package/dist/server.d.ts +20 -0
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +79 -17
  31. package/dist/server.mjs +140 -27
  32. package/dist/service-client.cjs +1 -1
  33. package/dist/service-client.mjs +3 -3
  34. package/dist/service.cjs +12 -2
  35. package/dist/service.d.ts +2 -18
  36. package/dist/service.d.ts.map +1 -1
  37. package/dist/service.js +11 -0
  38. package/dist/service.mjs +12 -2
  39. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  40. package/dist/types.d.ts +4 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +0 -12
  43. package/dist/verify.cjs +24 -3
  44. package/dist/verify.mjs +6 -6
  45. package/package.json +15 -15
package/dist/server.mjs CHANGED
@@ -1,5 +1,5 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.1 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
- import "./chunk-25WE6UWI.mjs";
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.10 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ import "./chunk-OGS6MQ3D.mjs";
3
3
 
4
4
  // src/server.ts
5
5
  import { createSessionCookie } from "@fluixi/session/server";
@@ -53,7 +53,7 @@ function resolveMethod(auth) {
53
53
  }
54
54
  async function loadJose() {
55
55
  try {
56
- return await import("./browser-ONMHX3Z3.mjs");
56
+ return await import("./browser-36FHB6KG.mjs");
57
57
  } catch {
58
58
  throw new OAuthError(
59
59
  "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.",
@@ -173,7 +173,106 @@ function expiryFrom(expiresIn, now = Date.now()) {
173
173
  return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
174
174
  }
175
175
 
176
+ // src/dpop.ts
177
+ async function loadJose2() {
178
+ try {
179
+ return await import("./browser-36FHB6KG.mjs");
180
+ } catch {
181
+ throw new OAuthError(
182
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
183
+ "jose_missing"
184
+ );
185
+ }
186
+ }
187
+ async function createDpopKey(alg = "ES256", options = {}) {
188
+ const jose = await loadJose2();
189
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
190
+ extractable: options.extractable ?? false
191
+ });
192
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
193
+ return {
194
+ privateKey,
195
+ publicJwk,
196
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
197
+ alg
198
+ };
199
+ }
200
+ async function exportDpopKey(key) {
201
+ const jose = await loadJose2();
202
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
203
+ }
204
+ var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
205
+ async function importDpopKey(stored) {
206
+ const jose = await loadJose2();
207
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
208
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
209
+ return {
210
+ privateKey,
211
+ publicJwk,
212
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
213
+ alg: stored.alg
214
+ };
215
+ }
216
+ async function proofFor(key, options) {
217
+ const jose = await loadJose2();
218
+ const htu = (() => {
219
+ try {
220
+ const url = new URL(options.url);
221
+ url.search = "";
222
+ url.hash = "";
223
+ return url.toString();
224
+ } catch {
225
+ return options.url;
226
+ }
227
+ })();
228
+ const claims = {
229
+ htm: options.method.toUpperCase(),
230
+ htu,
231
+ jti: crypto.randomUUID()
232
+ };
233
+ if (options.accessToken) {
234
+ const digest = await crypto.subtle.digest(
235
+ "SHA-256",
236
+ new TextEncoder().encode(options.accessToken)
237
+ );
238
+ let binary = "";
239
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
240
+ claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
241
+ }
242
+ if (options.nonce) claims.nonce = options.nonce;
243
+ return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
244
+ }
245
+ function nonceFrom(response) {
246
+ return response.headers.get("dpop-nonce");
247
+ }
248
+
176
249
  // src/server.ts
250
+ import { defineModule } from "@fluixi/start/app";
251
+ var dpopNonces = /* @__PURE__ */ new Map();
252
+ async function prove(headers, key, method, url, accessToken) {
253
+ headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
254
+ }
255
+ function learnNonce(url, response) {
256
+ const offered = nonceFrom(response);
257
+ if (!offered) return false;
258
+ const origin = new URL(url).origin;
259
+ const fresh = offered !== dpopNonces.get(origin);
260
+ dpopNonces.set(origin, offered);
261
+ return fresh && !response.ok;
262
+ }
263
+ async function withToken(url, session) {
264
+ const key = session.dpop && session.tokenType?.toLowerCase() === "dpop" ? await importDpopKey(session.dpop) : null;
265
+ const send = async () => {
266
+ const headers = {
267
+ authorization: `${key ? "DPoP" : "Bearer"} ${session.accessToken}`
268
+ };
269
+ if (key) await prove(headers, key, "GET", url, session.accessToken);
270
+ return fetch(url, { headers });
271
+ };
272
+ let response = await send();
273
+ if (key && learnNonce(url, response)) response = await send();
274
+ return response;
275
+ }
177
276
  function oauthRoutes(config) {
178
277
  const base = (config.basePath ?? "/auth").replace(/\/$/, "");
179
278
  const scopes = config.scopes ?? ["openid", "profile", "email"];
@@ -205,14 +304,19 @@ function oauthRoutes(config) {
205
304
  keyId: config.keyId,
206
305
  assertionAlgorithm: config.assertionAlgorithm
207
306
  };
208
- async function tokenRequest(body) {
307
+ async function tokenRequest(body, key = null) {
209
308
  const { token } = await resolve();
210
- const headers = {
211
- "content-type": "application/x-www-form-urlencoded"
309
+ const attempt = async () => {
310
+ const headers = {
311
+ "content-type": "application/x-www-form-urlencoded"
312
+ };
313
+ const form = new URLSearchParams(body);
314
+ await applyClientAuth(clientAuth, token, headers, form);
315
+ if (key) await prove(headers, key, "POST", token);
316
+ return fetch(token, { method: "POST", headers, body: form.toString() });
212
317
  };
213
- const form = new URLSearchParams(body);
214
- await applyClientAuth(clientAuth, token, headers, form);
215
- const response = await fetch(token, { method: "POST", headers, body: form.toString() });
318
+ let response = await attempt();
319
+ if (key && learnNonce(token, response)) response = await attempt();
216
320
  const payload = await response.json().catch(() => ({}));
217
321
  if (!response.ok) {
218
322
  throw new OAuthError(
@@ -279,17 +383,23 @@ function oauthRoutes(config) {
279
383
  if (!pending || !state || !timingSafeEqual(pending.state, state)) {
280
384
  return json({ error: "state_mismatch" }, 400);
281
385
  }
282
- const payload = await tokenRequest({
283
- grant_type: "authorization_code",
284
- code,
285
- redirect_uri: config.redirectUri,
286
- code_verifier: pending.verifier
287
- });
386
+ const key = config.dpop ? await createDpopKey(config.dpopAlgorithm, { extractable: true }) : null;
387
+ const payload = await tokenRequest(
388
+ {
389
+ grant_type: "authorization_code",
390
+ code,
391
+ redirect_uri: config.redirectUri,
392
+ code_verifier: pending.verifier
393
+ },
394
+ key
395
+ );
288
396
  const signedIn = await cookie.write({
289
397
  accessToken: String(payload.access_token ?? ""),
290
398
  refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : void 0,
291
399
  idToken: typeof payload.id_token === "string" ? payload.id_token : void 0,
292
- expiresAt: expiryFrom(payload.expires_in)
400
+ expiresAt: expiryFrom(payload.expires_in),
401
+ tokenType: typeof payload.token_type === "string" ? payload.token_type : void 0,
402
+ dpop: key ? await exportDpopKey(key) : void 0
293
403
  });
294
404
  return new Response(null, {
295
405
  status: 302,
@@ -309,17 +419,20 @@ function oauthRoutes(config) {
309
419
  if (route === "/refresh") {
310
420
  const current = await session();
311
421
  if (!current?.refreshToken) return json({ error: "no_session" }, 401);
312
- const payload = await tokenRequest({
313
- grant_type: "refresh_token",
314
- refresh_token: current.refreshToken
315
- });
422
+ const key = current.dpop ? await importDpopKey(current.dpop) : null;
423
+ const payload = await tokenRequest(
424
+ { grant_type: "refresh_token", refresh_token: current.refreshToken },
425
+ key
426
+ );
316
427
  const refreshed = await cookie.write({
317
428
  accessToken: String(payload.access_token ?? ""),
318
429
  // A provider that rotates refresh tokens sends a new one; one that does not
319
430
  // expects the old one to keep working.
320
431
  refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
321
432
  idToken: current.idToken,
322
- expiresAt: expiryFrom(payload.expires_in)
433
+ expiresAt: expiryFrom(payload.expires_in),
434
+ tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
435
+ dpop: current.dpop
323
436
  });
324
437
  return json({ ok: true }, 200, { "set-cookie": refreshed });
325
438
  }
@@ -328,9 +441,7 @@ function oauthRoutes(config) {
328
441
  if (!current?.accessToken) return json({ error: "no_session" }, 401);
329
442
  const { userinfo } = await resolve();
330
443
  if (!userinfo) return json({ error: "no_userinfo" }, 501);
331
- const response = await fetch(userinfo, {
332
- headers: { authorization: `Bearer ${current.accessToken}` }
333
- });
444
+ const response = await withToken(userinfo, current);
334
445
  if (!response.ok) return json({ error: "userinfo_failed" }, response.status);
335
446
  const claims = await response.json();
336
447
  return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
@@ -420,9 +531,7 @@ function sessionLocals(config) {
420
531
  try {
421
532
  const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
422
533
  if (endpoints?.userinfo) {
423
- const response = await fetch(endpoints.userinfo, {
424
- headers: { authorization: `Bearer ${session.accessToken}` }
425
- });
534
+ const response = await withToken(endpoints.userinfo, session);
426
535
  if (response.ok) {
427
536
  const claims = await response.json();
428
537
  user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
@@ -435,7 +544,11 @@ function sessionLocals(config) {
435
544
  return next();
436
545
  };
437
546
  }
547
+ function authServerModule(config) {
548
+ return defineModule({ name: "oauth2-server", middleware: [oauthRoutes(config), sessionLocals(config)] });
549
+ }
438
550
  export {
551
+ authServerModule,
439
552
  oauthRoutes,
440
553
  sessionLocals
441
554
  };
@@ -1,4 +1,4 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.1 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.10 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -1,5 +1,5 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.1 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
- import "./chunk-25WE6UWI.mjs";
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.10 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ import "./chunk-OGS6MQ3D.mjs";
3
3
 
4
4
  // src/types.ts
5
5
  var OAuthError = class extends Error {
@@ -42,7 +42,7 @@ function assertUsable(auth, who) {
42
42
  }
43
43
  async function loadJose() {
44
44
  try {
45
- return await import("./browser-ONMHX3Z3.mjs");
45
+ return await import("./browser-36FHB6KG.mjs");
46
46
  } catch {
47
47
  throw new OAuthError(
48
48
  "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.",
package/dist/service.cjs CHANGED
@@ -34,6 +34,7 @@ __export(service_exports, {
34
34
  paramsFromUrl: () => paramsFromUrl
35
35
  });
36
36
  module.exports = __toCommonJS(service_exports);
37
+ var import_signal = require("@fluixi/reactive/signal");
37
38
  var import_session = require("@fluixi/session");
38
39
  var import_client = require("@fluixi/auth/client");
39
40
 
@@ -191,9 +192,11 @@ async function loadJose2() {
191
192
  );
192
193
  }
193
194
  }
194
- async function createDpopKey(alg = "ES256") {
195
+ async function createDpopKey(alg = "ES256", options = {}) {
195
196
  const jose = await loadJose2();
196
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
197
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
198
+ extractable: options.extractable ?? false
199
+ });
197
200
  const publicJwk = { ...await jose.exportJWK(publicKey) };
198
201
  return {
199
202
  privateKey,
@@ -829,6 +832,13 @@ function clientFor(options) {
829
832
  return options.config.tokens === "server" ? createServerClient(options.config) : createBrowserClient(options.config);
830
833
  }
831
834
  function createAuthService(options) {
835
+ const tokens = options.config.tokens;
836
+ const scope = (0, import_signal.openReactiveScope)({ kind: "service", name: "AuthService", ...tokens ? { detail: `tokens: ${tokens}` } : {} });
837
+ const service = (0, import_signal.withReactiveScope)(scope, () => buildAuthService(options));
838
+ (0, import_signal.markReactiveScope)(service, scope);
839
+ return service;
840
+ }
841
+ function buildAuthService(options) {
832
842
  const client = clientFor(options);
833
843
  let latest = null;
834
844
  const session = (0, import_session.createSession)({
package/dist/service.d.ts CHANGED
@@ -1,19 +1,5 @@
1
- /**
2
- * The application-facing half.
3
- *
4
- * `AuthClient` knows the protocol. This knows the application: who is signed in, whether
5
- * the session is still good, when to refresh, and where to send someone who is not
6
- * allowed in. It is built on `@fluixi/session`, which already owns that state machine,
7
- * rather than a second one written here.
8
- *
9
- * The mapping onto a session adapter is the part worth explaining. `createSession`'s
10
- * `login(creds)` means "turn these credentials into a token". In a redirect flow the
11
- * credential is the authorization code on the callback URL, so `login` is the code
12
- * exchange and the redirect itself is a separate call that never returns. Everything
13
- * else lines up directly: `fetchUser`, `refresh`, `logout` and `getExpiry`.
14
- */
15
1
  import { type AccessControl, type Session } from '@fluixi/session';
16
- import { createAuthHooks } from '@fluixi/auth/client';
2
+ import { createAuthHooks, type AuthHooksOptions } from '@fluixi/auth/client';
17
3
  import type { AuthClient, AuthConfig, CallbackParams, LoginOptions } from './types.js';
18
4
  /** The credential a redirect flow produces. */
19
5
  export type AuthCredentials = CallbackParams;
@@ -55,9 +41,7 @@ export interface AuthServiceOptions<U, P = unknown, Q = (p: P) => boolean> {
55
41
  * client-side navigation; leave it out and it assigns `location`, which reloads the
56
42
  * document and discards the reactive tree.
57
43
  */
58
- navigate?: {
59
- handler: () => (to: string, options?: unknown) => void;
60
- };
44
+ navigate?: AuthHooksOptions['navigate'];
61
45
  /** Where a signed-out visitor is sent. Default `/login`. */
62
46
  loginPath?: string;
63
47
  /** Where a signed-in visitor is sent from a guest-only page. Default `/`. */
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAM7E,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAU,MAAM,YAAY,CAAC;AAE/F,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC;AAE7C,MAAM,WAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO;IACvE,mDAAmD;IACnD,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAChD,+FAA+F;IAC/F,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,qCAAqC;IACrC,WAAW,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAE/C;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACxC,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAChE,SAAQ,OAAO,CAAC,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAChD,IAAI,CACF,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAC3C,gBAAgB,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CACxE;IACH,oFAAoF;IACpF,MAAM,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,cAAc,CAWhE;AAUD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,EACrE,OAAO,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACnC,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAStB"}
package/dist/service.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * exchange and the redirect itself is a separate call that never returns. Everything
13
13
  * else lines up directly: `fetchUser`, `refresh`, `logout` and `getExpiry`.
14
14
  */
15
+ import { markReactiveScope, openReactiveScope, withReactiveScope } from '@fluixi/reactive/signal';
15
16
  import { createSession } from '@fluixi/session';
16
17
  import { createAuthHooks } from '@fluixi/auth/client';
17
18
  import { expiryFromJwt } from './pkce.js';
@@ -48,6 +49,16 @@ function clientFor(options) {
48
49
  : createBrowserClient(options.config);
49
50
  }
50
51
  export function createAuthService(options) {
52
+ // One group in the devtools graph for everything the service holds: the session's `user`,
53
+ // `token`, `status` and the rest. Marked on the instance, so `authModule(auth)` places the
54
+ // group under the module that provides it.
55
+ const tokens = options.config.tokens;
56
+ const scope = openReactiveScope({ kind: 'service', name: 'AuthService', ...(tokens ? { detail: `tokens: ${tokens}` } : {}) });
57
+ const service = withReactiveScope(scope, () => buildAuthService(options));
58
+ markReactiveScope(service, scope);
59
+ return service;
60
+ }
61
+ function buildAuthService(options) {
51
62
  const client = clientFor(options);
52
63
  // Held so `getExpiry` can answer for a token the session hands back, and so a
53
64
  // server-mode session has something to report when the token is a cookie.
package/dist/service.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import "./chunk-7P6ASYW6.mjs";
2
2
 
3
3
  // src/service.ts
4
+ import { markReactiveScope, openReactiveScope, withReactiveScope } from "@fluixi/reactive/signal";
4
5
  import { createSession } from "@fluixi/session";
5
6
  import { createAuthHooks } from "@fluixi/auth/client";
6
7
 
@@ -158,9 +159,11 @@ async function loadJose2() {
158
159
  );
159
160
  }
160
161
  }
161
- async function createDpopKey(alg = "ES256") {
162
+ async function createDpopKey(alg = "ES256", options = {}) {
162
163
  const jose = await loadJose2();
163
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: false });
164
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
165
+ extractable: options.extractable ?? false
166
+ });
164
167
  const publicJwk = { ...await jose.exportJWK(publicKey) };
165
168
  return {
166
169
  privateKey,
@@ -796,6 +799,13 @@ function clientFor(options) {
796
799
  return options.config.tokens === "server" ? createServerClient(options.config) : createBrowserClient(options.config);
797
800
  }
798
801
  function createAuthService(options) {
802
+ const tokens = options.config.tokens;
803
+ const scope = openReactiveScope({ kind: "service", name: "AuthService", ...tokens ? { detail: `tokens: ${tokens}` } : {} });
804
+ const service = withReactiveScope(scope, () => buildAuthService(options));
805
+ markReactiveScope(service, scope);
806
+ return service;
807
+ }
808
+ function buildAuthService(options) {
799
809
  const client = clientFor(options);
800
810
  let latest = null;
801
811
  const session = createSession({