@cauthyhq/node 1.2.1 → 1.3.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 (49) hide show
  1. package/CHANGELOG.md +31 -7
  2. package/README.md +10 -29
  3. package/dist/{cauthy-CCSSj3Lg.d.mts → cauthy-DHRZLndK.d.mts} +31 -147
  4. package/dist/{cauthy-C7gZ-pbv.d.ts → cauthy-JSYh7J2o.d.ts} +31 -147
  5. package/dist/express.d.mts +2 -5
  6. package/dist/express.d.ts +2 -5
  7. package/dist/express.js +1 -2
  8. package/dist/express.js.map +1 -1
  9. package/dist/express.mjs +1 -2
  10. package/dist/express.mjs.map +1 -1
  11. package/dist/fastify.d.mts +1 -3
  12. package/dist/fastify.d.ts +1 -3
  13. package/dist/fastify.js +0 -2
  14. package/dist/fastify.js.map +1 -1
  15. package/dist/fastify.mjs +0 -2
  16. package/dist/fastify.mjs.map +1 -1
  17. package/dist/hono.d.mts +2 -5
  18. package/dist/hono.d.ts +2 -5
  19. package/dist/hono.js +1 -2
  20. package/dist/hono.js.map +1 -1
  21. package/dist/hono.mjs +1 -2
  22. package/dist/hono.mjs.map +1 -1
  23. package/dist/index.d.mts +1 -3
  24. package/dist/index.d.ts +1 -3
  25. package/dist/index.js +159 -524
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +152 -514
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp.js.map +1 -1
  30. package/dist/mcp.mjs.map +1 -1
  31. package/dist/next.d.mts +1 -3
  32. package/dist/next.d.ts +1 -3
  33. package/dist/testing.d.mts +11 -5
  34. package/dist/testing.d.ts +11 -5
  35. package/dist/testing.js +51 -49
  36. package/dist/testing.js.map +1 -1
  37. package/dist/testing.mjs +51 -49
  38. package/dist/testing.mjs.map +1 -1
  39. package/package.json +3 -15
  40. package/dist/identity-BTJ8NB_w.d.mts +0 -27
  41. package/dist/identity-BTJ8NB_w.d.ts +0 -27
  42. package/dist/keys-D5k0omTb.d.mts +0 -64
  43. package/dist/keys-D5k0omTb.d.ts +0 -64
  44. package/dist/keys.d.mts +0 -8
  45. package/dist/keys.d.ts +0 -8
  46. package/dist/keys.js +0 -157
  47. package/dist/keys.js.map +0 -1
  48. package/dist/keys.mjs +0 -129
  49. package/dist/keys.mjs.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import { Server } from 'node:http';
2
- import { I as Identity } from './identity-BTJ8NB_w.mjs';
3
2
 
4
3
  declare class FakeCauthy {
5
4
  readonly issuer = "https://id.test";
@@ -16,6 +15,7 @@ declare class FakeCauthy {
16
15
  sub: string;
17
16
  challenge: string;
18
17
  redirectUri: string;
18
+ clientId?: string;
19
19
  }>;
20
20
  refresh: Map<string, string>;
21
21
  access: Map<string, string>;
@@ -41,7 +41,7 @@ declare class FakeCauthy {
41
41
  settings: Record<string, unknown>;
42
42
  /** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
43
43
  adminToken: string;
44
- /** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
44
+ /** The application's client, as the admin API lists it. */
45
45
  clientId: string;
46
46
  /** The application's registered origins, as GET /v1/connect/self answers them (app_urls). */
47
47
  appUrls: string[];
@@ -49,8 +49,13 @@ declare class FakeCauthy {
49
49
  system: string;
50
50
  /** The workspace's id, as GET /v1/connect/self answers it. */
51
51
  tenantId: string;
52
- /** What GET /v1/connect/self answers as `contract`. */
53
- contract: Identity['contract'];
52
+ /**
53
+ * The client people sign in through, by the key's client, as GET
54
+ * /v1/connect/self answers it (sign_in_client_id): an application can
55
+ * have its key on a system client (`pact.<link>.service`) and its people
56
+ * on another (`pact.<link>`). Absent: the key's own client.
57
+ */
58
+ signInClients: Map<string, string>;
54
59
  /** Every call to the token and revoke endpoints, with its form and headers: what a test reads of how the application authenticated. */
55
60
  oauthRequests: {
56
61
  path: string;
@@ -62,6 +67,7 @@ declare class FakeCauthy {
62
67
  private jtis;
63
68
  private systemTokens;
64
69
  private keyClients;
70
+ private refreshClients;
65
71
  constructor();
66
72
  /** What the hosted page does once the person has signed in: hands the browser a code. */
67
73
  authorize(location: string, sub?: string): string;
@@ -82,7 +88,7 @@ declare class FakeCauthy {
82
88
  private assertionOf;
83
89
  /** What GET /v1/connect/self answers for a key. */
84
90
  private identityOf;
85
- /** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */
91
+ /** A token answer. `kind` is what a sign-in asked for (remember) when the workspace allows long sessions; `persistent` wins when set. */
86
92
  private issue;
87
93
  fetch: typeof fetch;
88
94
  /** The part of /v1/auth/* a password sign-in and the sign-in form read. */
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Server } from 'node:http';
2
- import { I as Identity } from './identity-BTJ8NB_w.js';
3
2
 
4
3
  declare class FakeCauthy {
5
4
  readonly issuer = "https://id.test";
@@ -16,6 +15,7 @@ declare class FakeCauthy {
16
15
  sub: string;
17
16
  challenge: string;
18
17
  redirectUri: string;
18
+ clientId?: string;
19
19
  }>;
20
20
  refresh: Map<string, string>;
21
21
  access: Map<string, string>;
@@ -41,7 +41,7 @@ declare class FakeCauthy {
41
41
  settings: Record<string, unknown>;
42
42
  /** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
43
43
  adminToken: string;
44
- /** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
44
+ /** The application's client, as the admin API lists it. */
45
45
  clientId: string;
46
46
  /** The application's registered origins, as GET /v1/connect/self answers them (app_urls). */
47
47
  appUrls: string[];
@@ -49,8 +49,13 @@ declare class FakeCauthy {
49
49
  system: string;
50
50
  /** The workspace's id, as GET /v1/connect/self answers it. */
51
51
  tenantId: string;
52
- /** What GET /v1/connect/self answers as `contract`. */
53
- contract: Identity['contract'];
52
+ /**
53
+ * The client people sign in through, by the key's client, as GET
54
+ * /v1/connect/self answers it (sign_in_client_id): an application can
55
+ * have its key on a system client (`pact.<link>.service`) and its people
56
+ * on another (`pact.<link>`). Absent: the key's own client.
57
+ */
58
+ signInClients: Map<string, string>;
54
59
  /** Every call to the token and revoke endpoints, with its form and headers: what a test reads of how the application authenticated. */
55
60
  oauthRequests: {
56
61
  path: string;
@@ -62,6 +67,7 @@ declare class FakeCauthy {
62
67
  private jtis;
63
68
  private systemTokens;
64
69
  private keyClients;
70
+ private refreshClients;
65
71
  constructor();
66
72
  /** What the hosted page does once the person has signed in: hands the browser a code. */
67
73
  authorize(location: string, sub?: string): string;
@@ -82,7 +88,7 @@ declare class FakeCauthy {
82
88
  private assertionOf;
83
89
  /** What GET /v1/connect/self answers for a key. */
84
90
  private identityOf;
85
- /** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */
91
+ /** A token answer. `kind` is what a sign-in asked for (remember) when the workspace allows long sessions; `persistent` wins when set. */
86
92
  private issue;
87
93
  fetch: typeof fetch;
88
94
  /** The part of /v1/auth/* a password sign-in and the sign-in form read. */
package/dist/testing.js CHANGED
@@ -236,34 +236,19 @@ function constantTimeEqual(a, b) {
236
236
  return d === 0;
237
237
  }
238
238
 
239
- // src/rootkey.ts
240
- var encoder = new TextEncoder();
241
- var MIN_ROOT_KEY = 32;
242
- async function derive(root, purpose) {
243
- const ikm = await crypto.subtle.importKey("raw", encoder.encode(root), "HKDF", false, ["deriveBits"]);
244
- const bits = await crypto.subtle.deriveBits(
245
- { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: encoder.encode(`pact/${purpose}/v1`) },
246
- ikm,
247
- 256
248
- );
249
- return new Uint8Array(bits);
250
- }
251
- function deriveKeys(root, previous) {
252
- if (root.length < MIN_ROOT_KEY) {
253
- throw new CauthyConfigError(`rootKey (PACT_ROOT_KEY) must be at least ${MIN_ROOT_KEY} characters: 32 random bytes, e.g. openssl rand -base64 32`);
254
- }
255
- if (previous !== void 0 && previous !== "" && previous.length < MIN_ROOT_KEY) {
256
- throw new CauthyConfigError(`the previous root key (PACT_ROOT_KEY_PREVIOUS) must be at least ${MIN_ROOT_KEY} characters`);
257
- }
258
- const roots = previous && previous !== root ? [root, previous] : [root];
259
- const ring = (purpose) => Promise.all(roots.map((r) => derive(r, purpose)));
260
- return { seal: ring("seal"), cookie: ring("cookie"), flow: ring("flow") };
261
- }
262
-
263
239
  // src/keyderive.ts
264
240
  var enc2 = new TextEncoder();
265
241
  var PKCS8_ED25519 = Uint8Array.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
266
242
  var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
243
+ async function ringKey(secret, purpose) {
244
+ const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
245
+ return new Uint8Array(await crypto.subtle.deriveBits(
246
+ { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: enc2.encode(`pact/${purpose}/v1`) },
247
+ ikm,
248
+ 256
249
+ ));
250
+ }
251
+ var ringOf = (secrets, purpose) => Promise.all(secrets.map((s) => ringKey(s, purpose)));
267
252
  async function seedOf(secret) {
268
253
  const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
269
254
  return new Uint8Array(await crypto.subtle.deriveBits(
@@ -301,17 +286,16 @@ async function signerOf(secret) {
301
286
  }
302
287
  function deriveFromKey(key, previous) {
303
288
  const other = previous && previous.secret !== key.secret ? previous : void 0;
304
- const rings = deriveKeys(key.secret, other?.secret);
289
+ const secrets = other ? [key.secret, other.secret] : [key.secret];
305
290
  return {
306
- cookie: rings.cookie,
307
- flow: rings.flow,
308
- signer: signerOf(key.secret),
309
- ...other ? { previousJwk: signerOf(other.secret).then((s) => ({ ...s.publicJwk, kid: s.kid })) } : {}
291
+ cookie: ringOf(secrets, "cookie"),
292
+ flow: ringOf(secrets, "flow"),
293
+ signer: signerOf(key.secret)
310
294
  };
311
295
  }
312
296
 
313
297
  // src/testing.ts
314
- var ADMIN_SETTINGS = ["allow_signup", "allow_password", "allow_magic_link", "allow_passkeys", "confirm_email", "verify_grace_days"];
298
+ var ADMIN_SETTINGS = ["allow_signup", "allow_password", "allow_magic_link", "allow_passkeys", "confirm_email", "verify_grace_days", "persistent_sessions"];
315
299
  var FakeCauthy = class {
316
300
  issuer = "https://id.test";
317
301
  users = /* @__PURE__ */ new Map();
@@ -339,7 +323,7 @@ var FakeCauthy = class {
339
323
  settings = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false };
340
324
  /** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
341
325
  adminToken = "";
342
- /** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
326
+ /** The application's client, as the admin API lists it. */
343
327
  clientId = "acme-crm";
344
328
  /** The application's registered origins, as GET /v1/connect/self answers them (app_urls). */
345
329
  appUrls = ["http://localhost:3000"];
@@ -347,8 +331,13 @@ var FakeCauthy = class {
347
331
  system = "acme-crm";
348
332
  /** The workspace's id, as GET /v1/connect/self answers it. */
349
333
  tenantId = "t-acme";
350
- /** What GET /v1/connect/self answers as `contract`. */
351
- contract = null;
334
+ /**
335
+ * The client people sign in through, by the key's client, as GET
336
+ * /v1/connect/self answers it (sign_in_client_id): an application can
337
+ * have its key on a system client (`pact.<link>.service`) and its people
338
+ * on another (`pact.<link>`). Absent: the key's own client.
339
+ */
340
+ signInClients = /* @__PURE__ */ new Map();
352
341
  /** Every call to the token and revoke endpoints, with its form and headers: what a test reads of how the application authenticated. */
353
342
  oauthRequests = [];
354
343
  n = 0;
@@ -357,6 +346,8 @@ var FakeCauthy = class {
357
346
  systemTokens = /* @__PURE__ */ new Map();
358
347
  // access token → kid
359
348
  keyClients = /* @__PURE__ */ new Set();
349
+ refreshClients = /* @__PURE__ */ new Map();
350
+ // refresh token → the client it was issued to
360
351
  constructor() {
361
352
  this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
362
353
  }
@@ -365,7 +356,7 @@ var FakeCauthy = class {
365
356
  const q = new URL(location).searchParams;
366
357
  if (q.get("response_type") !== "code") throw new Error("Cauthy answers unsupported_response_type: response_type=code is required");
367
358
  const code = `code-${++this.n}`;
368
- this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
359
+ this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "", clientId: q.get("client_id") ?? void 0 });
369
360
  return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
370
361
  }
371
362
  /**
@@ -430,19 +421,19 @@ var FakeCauthy = class {
430
421
  tenant: { id: this.tenantId, slug: this.tenant },
431
422
  system: this.system,
432
423
  client_id: k.clientId,
433
- sign_in_client_id: k.clientId,
424
+ sign_in_client_id: this.signInClients.get(k.clientId) ?? k.clientId,
434
425
  app_urls: [...this.appUrls],
435
426
  callback_path: "/api/auth/callback",
436
- key: { kid, retires_at: null },
437
- contract: this.contract
427
+ key: { kid, retires_at: null }
438
428
  };
439
429
  }
440
- /** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */
441
- issue(sub, kind) {
430
+ /** A token answer. `kind` is what a sign-in asked for (remember) when the workspace allows long sessions; `persistent` wins when set. */
431
+ issue(sub, kind, clientId) {
442
432
  const at = `at-${++this.n}`;
443
433
  const rt = `rt-${++this.n}`;
444
434
  this.access.set(at, sub);
445
435
  this.refresh.set(rt, sub);
436
+ if (clientId) this.refreshClients.set(rt, clientId);
446
437
  const persistent = this.persistent ?? kind;
447
438
  return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...persistent === void 0 ? {} : { persistent } };
448
439
  }
@@ -488,14 +479,20 @@ var FakeCauthy = class {
488
479
  this.codes.delete(form.get("code") ?? "");
489
480
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(form.get("code_verifier") ?? ""));
490
481
  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
491
- if (!c || c.challenge !== challenge || c.redirectUri !== form.get("redirect_uri")) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
492
- return reply(200, this.issue(c.sub));
482
+ const wrongClient = c?.clientId !== void 0 && c.clientId !== form.get("client_id");
483
+ if (!c || wrongClient || c.challenge !== challenge || c.redirectUri !== form.get("redirect_uri")) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
484
+ return reply(200, this.issue(c.sub, void 0, form.get("client_id") ?? void 0));
493
485
  }
494
486
  if (form.get("grant_type") === "refresh_token") {
495
- const sub2 = this.refresh.get(form.get("refresh_token") ?? "");
496
- this.refresh.delete(form.get("refresh_token") ?? "");
497
- if (!sub2 || !this.users.get(sub2)?.active) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
498
- return reply(200, this.issue(sub2));
487
+ const rt = form.get("refresh_token") ?? "";
488
+ const sub2 = this.refresh.get(rt);
489
+ const owner = this.refreshClients.get(rt);
490
+ this.refresh.delete(rt);
491
+ this.refreshClients.delete(rt);
492
+ if (!sub2 || !this.users.get(sub2)?.active || owner !== void 0 && owner !== form.get("client_id")) {
493
+ return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
494
+ }
495
+ return reply(200, this.issue(sub2, void 0, owner ?? form.get("client_id") ?? void 0));
499
496
  }
500
497
  return reply(400, { error: "unsupported_grant_type", error_description: "supported grant types: authorization_code, refresh_token" });
501
498
  }
@@ -586,13 +583,18 @@ function adminAnswer(fake, url, init) {
586
583
  fake.settings = next;
587
584
  return reply(200, { data: fake.settings });
588
585
  }
589
- const persistent = () => fake.settings.persistent_sessions === true;
590
- if (route === "GET /v1/oauth/clients") return reply(200, { data: [{ client_id: fake.clientId, allow_persistent_sessions: persistent() }], total_count: 1 });
586
+ if (route === "GET /v1/oauth/clients") return reply(200, { data: [{ client_id: fake.clientId }], total_count: 1 });
591
587
  if (client) {
592
588
  if (decodeURIComponent(client[1]) !== fake.clientId) return refuse(404, "client_not_found", "no such client");
593
589
  const change = JSON.parse(String(init?.body ?? "{}"));
594
- if (typeof change.allow_persistent_sessions === "boolean") fake.settings = { ...fake.settings, persistent_sessions: change.allow_persistent_sessions };
595
- return reply(200, { data: { client_id: fake.clientId, allow_persistent_sessions: persistent() } });
590
+ if (change.allow_persistent_sessions !== void 0 && change.allow_persistent_sessions !== null) {
591
+ return refuse(
592
+ 400,
593
+ "persistent_sessions_moved",
594
+ "allow_persistent_sessions is no longer a client's: it is the workspace's persistent_sessions (PUT /v1/settings/auth)"
595
+ );
596
+ }
597
+ return reply(200, { data: { client_id: fake.clientId } });
596
598
  }
597
599
  if (route === "GET /v1/users") {
598
600
  const q = (url.searchParams.get("q") ?? "").toLowerCase();