@cauthyhq/node 1.0.0 → 1.2.0

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.
package/dist/testing.js CHANGED
@@ -21,9 +21,297 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var testing_exports = {};
22
22
  __export(testing_exports, {
23
23
  Browser: () => Browser,
24
- FakeCauthy: () => FakeCauthy
24
+ FakeCauthy: () => FakeCauthy,
25
+ serveFakeCauthy: () => serveFakeCauthy
25
26
  });
26
27
  module.exports = __toCommonJS(testing_exports);
28
+ var import_node_http = require("http");
29
+
30
+ // src/sha256.ts
31
+ var K = new Uint32Array([
32
+ 1116352408,
33
+ 1899447441,
34
+ 3049323471,
35
+ 3921009573,
36
+ 961987163,
37
+ 1508970993,
38
+ 2453635748,
39
+ 2870763221,
40
+ 3624381080,
41
+ 310598401,
42
+ 607225278,
43
+ 1426881987,
44
+ 1925078388,
45
+ 2162078206,
46
+ 2614888103,
47
+ 3248222580,
48
+ 3835390401,
49
+ 4022224774,
50
+ 264347078,
51
+ 604807628,
52
+ 770255983,
53
+ 1249150122,
54
+ 1555081692,
55
+ 1996064986,
56
+ 2554220882,
57
+ 2821834349,
58
+ 2952996808,
59
+ 3210313671,
60
+ 3336571891,
61
+ 3584528711,
62
+ 113926993,
63
+ 338241895,
64
+ 666307205,
65
+ 773529912,
66
+ 1294757372,
67
+ 1396182291,
68
+ 1695183700,
69
+ 1986661051,
70
+ 2177026350,
71
+ 2456956037,
72
+ 2730485921,
73
+ 2820302411,
74
+ 3259730800,
75
+ 3345764771,
76
+ 3516065817,
77
+ 3600352804,
78
+ 4094571909,
79
+ 275423344,
80
+ 430227734,
81
+ 506948616,
82
+ 659060556,
83
+ 883997877,
84
+ 958139571,
85
+ 1322822218,
86
+ 1537002063,
87
+ 1747873779,
88
+ 1955562222,
89
+ 2024104815,
90
+ 2227730452,
91
+ 2361852424,
92
+ 2428436474,
93
+ 2756734187,
94
+ 3204031479,
95
+ 3329325298
96
+ ]);
97
+ function sha256(data) {
98
+ const h = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
99
+ const len = data.length;
100
+ const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
101
+ padded.set(data);
102
+ padded[len] = 128;
103
+ const view = new DataView(padded.buffer);
104
+ view.setUint32(padded.length - 8, Math.floor(len / 536870912));
105
+ view.setUint32(padded.length - 4, len << 3 >>> 0);
106
+ const w = new Uint32Array(64);
107
+ const rotr = (x, n) => x >>> n | x << 32 - n;
108
+ for (let off = 0; off < padded.length; off += 64) {
109
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
110
+ for (let i = 16; i < 64; i++) {
111
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
112
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
113
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
114
+ }
115
+ let [a, b, c, d, e, f, g, hh] = h;
116
+ for (let i = 0; i < 64; i++) {
117
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
118
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
119
+ hh = g;
120
+ g = f;
121
+ f = e;
122
+ e = d + t1 >>> 0;
123
+ d = c;
124
+ c = b;
125
+ b = a;
126
+ a = t1 + t2 >>> 0;
127
+ }
128
+ h[0] += a;
129
+ h[1] += b;
130
+ h[2] += c;
131
+ h[3] += d;
132
+ h[4] += e;
133
+ h[5] += f;
134
+ h[6] += g;
135
+ h[7] += hh;
136
+ }
137
+ const out = new Uint8Array(32);
138
+ const ov = new DataView(out.buffer);
139
+ for (let i = 0; i < 8; i++) ov.setUint32(i * 4, h[i]);
140
+ return out;
141
+ }
142
+
143
+ // src/types.ts
144
+ var CauthyConfigError = class extends Error {
145
+ constructor(message) {
146
+ super(`@cauthyhq/node: ${message}`);
147
+ this.name = "CauthyConfigError";
148
+ }
149
+ };
150
+ var CauthyKeyError = class extends CauthyConfigError {
151
+ constructor(reason, message) {
152
+ super(`CAUTHY_KEY: ${message} (${reason})`);
153
+ this.reason = reason;
154
+ this.name = "CauthyKeyError";
155
+ }
156
+ reason;
157
+ };
158
+
159
+ // src/cauthykey.ts
160
+ var KEY_PREFIX = "cauthy1";
161
+ var MAX_LEN = 512;
162
+ var enc = new TextEncoder();
163
+ var dec = new TextDecoder("utf-8", { fatal: true });
164
+ function b64uEncode(b) {
165
+ let bin = "";
166
+ for (const x of b) bin += String.fromCharCode(x);
167
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
168
+ }
169
+ function b64uDecode(s) {
170
+ if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1) return null;
171
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
172
+ const out = Uint8Array.from(bin, (c) => c.charCodeAt(0));
173
+ return b64uEncode(out) === s ? out : null;
174
+ }
175
+ function checksum(body) {
176
+ return b64uEncode(sha256(enc.encode(body)).slice(0, 6));
177
+ }
178
+ function printableAscii(s) {
179
+ return /^[\x21-\x7e]*$/.test(s);
180
+ }
181
+ function validIssuer(s) {
182
+ if (s === "" || !printableAscii(s)) return false;
183
+ let u;
184
+ try {
185
+ u = new URL(s);
186
+ } catch {
187
+ return false;
188
+ }
189
+ if (u.origin !== s) return false;
190
+ if (/^\[::ffff:/i.test(u.host)) return false;
191
+ if (u.protocol === "https:") return true;
192
+ return u.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
193
+ }
194
+ var fail = (reason, message) => {
195
+ throw new CauthyKeyError(reason, message);
196
+ };
197
+ var NOT_A_KEY = "not a Cauthy key (copy it again from the dashboard)";
198
+ function parseKey(text) {
199
+ const s = text.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, "");
200
+ if (s === "" || s.length > MAX_LEN || !printableAscii(s)) fail("key_format", NOT_A_KEY);
201
+ const head = s.split(".", 1)[0];
202
+ if (head !== KEY_PREFIX) {
203
+ if (/^cauthy[0-9]+$/.test(head)) fail("key_version", `this key is for a newer SDK (${head}): update @cauthyhq/node`);
204
+ fail("key_format", NOT_A_KEY);
205
+ }
206
+ const parts = s.split(".");
207
+ if (parts.length !== 5 || parts.some((p) => p === "")) fail("key_format", NOT_A_KEY);
208
+ if (!constantTimeEqual(checksum(parts.slice(0, 4).join(".")), parts[4])) {
209
+ fail("key_checksum", "the key is damaged (part of it is missing or changed): copy it again from the dashboard");
210
+ }
211
+ const issuerBytes = b64uDecode(parts[1]);
212
+ let issuer = "";
213
+ try {
214
+ issuer = issuerBytes ? dec.decode(issuerBytes) : "";
215
+ } catch {
216
+ issuer = "";
217
+ }
218
+ if (!validIssuer(issuer)) fail("key_issuer", "the key names no valid Cauthy address");
219
+ const clientBytes = b64uDecode(parts[2]);
220
+ const clientId = clientBytes ? String.fromCharCode(...clientBytes) : "";
221
+ if (!/^[A-Za-z0-9._-]{1,128}$/.test(clientId)) fail("key_client", "the key names no valid client");
222
+ const secret = b64uDecode(parts[3]);
223
+ if (parts[3].length !== 43 || !secret || secret.length !== 32) fail("key_secret", "the key's secret is not 32 bytes");
224
+ return { issuer, clientId, secret: parts[3] };
225
+ }
226
+ function composeKey(issuer, clientId, secret) {
227
+ const body = `${KEY_PREFIX}.${b64uEncode(enc.encode(issuer))}.${b64uEncode(enc.encode(clientId))}.${secret}`;
228
+ const key = `${body}.${checksum(body)}`;
229
+ parseKey(key);
230
+ return key;
231
+ }
232
+ function constantTimeEqual(a, b) {
233
+ if (a.length !== b.length) return false;
234
+ let d = 0;
235
+ for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
236
+ return d === 0;
237
+ }
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
+ // src/keyderive.ts
264
+ var enc2 = new TextEncoder();
265
+ var PKCS8_ED25519 = Uint8Array.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
266
+ var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
267
+ async function seedOf(secret) {
268
+ const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
269
+ return new Uint8Array(await crypto.subtle.deriveBits(
270
+ { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: enc2.encode("cauthy/sign/v1") },
271
+ ikm,
272
+ 256
273
+ ));
274
+ }
275
+ async function thumbprint(x) {
276
+ return b64uEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", enc2.encode(`{"crv":"Ed25519","kty":"OKP","x":"${x}"}`))));
277
+ }
278
+ async function signerOf(secret) {
279
+ const seed = await seedOf(secret);
280
+ const der = new Uint8Array(PKCS8_ED25519.length + 32);
281
+ der.set(PKCS8_ED25519);
282
+ der.set(seed, PKCS8_ED25519.length);
283
+ const readable = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, true, ["sign"]);
284
+ const { x } = await crypto.subtle.exportKey("jwk", readable);
285
+ const signing = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, false, ["sign"]);
286
+ der.fill(0);
287
+ seed.fill(0);
288
+ const publicJwk = { kty: "OKP", crv: "Ed25519", x };
289
+ const kid = await thumbprint(publicJwk.x);
290
+ const header = b64uEncode(enc2.encode(JSON.stringify({ alg: "EdDSA", kid })));
291
+ return {
292
+ kid,
293
+ publicJwk,
294
+ async assertion(clientId, tokenEndpoint, jti = crypto.randomUUID(), iat = Math.floor(Date.now() / 1e3)) {
295
+ const claims = { iss: clientId, sub: clientId, aud: tokenEndpoint, jti, iat, exp: iat + 120 };
296
+ const input = `${header}.${b64uEncode(enc2.encode(JSON.stringify(claims)))}`;
297
+ const sig = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, signing, enc2.encode(input)));
298
+ return `${input}.${b64uEncode(sig)}`;
299
+ }
300
+ };
301
+ }
302
+ function deriveFromKey(key, previous) {
303
+ const other = previous && previous.secret !== key.secret ? previous : void 0;
304
+ const rings = deriveKeys(key.secret, other?.secret);
305
+ 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 })) } : {}
310
+ };
311
+ }
312
+
313
+ // src/testing.ts
314
+ var ADMIN_SETTINGS = ["allow_signup", "allow_password", "allow_magic_link", "allow_passkeys", "confirm_email", "verify_grace_days"];
27
315
  var FakeCauthy = class {
28
316
  issuer = "https://id.test";
29
317
  users = /* @__PURE__ */ new Map();
@@ -49,7 +337,26 @@ var FakeCauthy = class {
49
337
  passwords = /* @__PURE__ */ new Map();
50
338
  /** What GET /v1/auth/settings answers. */
51
339
  settings = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false };
340
+ /** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
341
+ adminToken = "";
342
+ /** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
343
+ clientId = "acme-crm";
344
+ /** The application's registered origins, as GET /v1/connect/self answers them (app_urls). */
345
+ appUrls = ["http://localhost:3000"];
346
+ /** The application's system, as GET /v1/connect/self answers it. */
347
+ system = "acme-crm";
348
+ /** The workspace's id, as GET /v1/connect/self answers it. */
349
+ tenantId = "t-acme";
350
+ /** What GET /v1/connect/self answers as `contract`. */
351
+ contract = null;
352
+ /** Every call to the token and revoke endpoints, with its form and headers: what a test reads of how the application authenticated. */
353
+ oauthRequests = [];
52
354
  n = 0;
355
+ keys = /* @__PURE__ */ new Map();
356
+ jtis = /* @__PURE__ */ new Set();
357
+ systemTokens = /* @__PURE__ */ new Map();
358
+ // access token → kid
359
+ keyClients = /* @__PURE__ */ new Set();
53
360
  constructor() {
54
361
  this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
55
362
  }
@@ -61,12 +368,83 @@ var FakeCauthy = class {
61
368
  this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
62
369
  return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
63
370
  }
64
- issue(sub) {
371
+ /**
372
+ * Makes a CAUTHY_KEY for the application's client and registers its public
373
+ * half, as Cauthy's dashboard does in the browser. `issuer` is the address
374
+ * the application will reach this Cauthy at (the served one, over HTTP).
375
+ * From then on that client authenticates with assertions only.
376
+ */
377
+ async issueKey(issuer = this.issuer, options = {}) {
378
+ const clientId = options.clientId ?? this.clientId;
379
+ const key = composeKey(issuer.replace(/\/+$/, ""), clientId, b64uEncode(crypto.getRandomValues(new Uint8Array(32))));
380
+ const signer = await deriveFromKey(parseKey(key)).signer;
381
+ this.keys.set(signer.kid, { x: signer.publicJwk.x, clientId, issuer: parseKey(key).issuer, revoked: false });
382
+ this.keyClients.add(clientId);
383
+ return key;
384
+ }
385
+ /** Revokes a registered key, as the dashboard's Revoke does: refused from now on. */
386
+ revokeKey(kid) {
387
+ const k = this.keys.get(kid);
388
+ if (k) k.revoked = true;
389
+ }
390
+ /** The kids registered, revoked or not. */
391
+ registeredKids() {
392
+ return [...this.keys.keys()];
393
+ }
394
+ /** Checks an RFC 7523 assertion of a registered Ed25519 key: the kid it signed with, or null. */
395
+ async assertionOf(form) {
396
+ if (form.get("client_assertion_type") !== ASSERTION_TYPE) return null;
397
+ const jws = form.get("client_assertion") ?? "";
398
+ const [h, p, sig, extra] = jws.split(".");
399
+ if (!h || !p || !sig || extra !== void 0) return null;
400
+ const dec2 = new TextDecoder();
401
+ let header;
402
+ let claims;
403
+ try {
404
+ header = JSON.parse(dec2.decode(b64uDecode(h) ?? new Uint8Array()));
405
+ claims = JSON.parse(dec2.decode(b64uDecode(p) ?? new Uint8Array()));
406
+ } catch {
407
+ return null;
408
+ }
409
+ const kid = typeof header.kid === "string" ? header.kid : "";
410
+ const k = this.keys.get(kid);
411
+ const clientId = form.get("client_id") ?? "";
412
+ if (header.alg !== "EdDSA" || !k || k.revoked || k.clientId !== clientId) return null;
413
+ if (claims.iss !== clientId || claims.sub !== clientId || claims.aud !== `${k.issuer}/v1/oauth/token`) return null;
414
+ const now = Math.floor(Date.now() / 1e3);
415
+ if (typeof claims.exp !== "number" || claims.exp <= now || claims.exp - now > 330) return null;
416
+ if (typeof claims.jti !== "string" || claims.jti === "" || this.jtis.has(claims.jti)) return null;
417
+ const signature = b64uDecode(sig);
418
+ if (!signature) return null;
419
+ const pub = await crypto.subtle.importKey("jwk", { kty: "OKP", crv: "Ed25519", x: k.x }, { name: "Ed25519" }, false, ["verify"]);
420
+ if (!await crypto.subtle.verify({ name: "Ed25519" }, pub, new Uint8Array(signature), new TextEncoder().encode(`${h}.${p}`))) return null;
421
+ this.jtis.add(claims.jti);
422
+ return kid;
423
+ }
424
+ /** What GET /v1/connect/self answers for a key. */
425
+ identityOf(kid) {
426
+ const k = this.keys.get(kid);
427
+ return {
428
+ issuer: k.issuer,
429
+ audience: k.issuer,
430
+ tenant: { id: this.tenantId, slug: this.tenant },
431
+ system: this.system,
432
+ client_id: k.clientId,
433
+ sign_in_client_id: k.clientId,
434
+ app_urls: [...this.appUrls],
435
+ callback_path: "/api/auth/callback",
436
+ key: { kid, retires_at: null },
437
+ contract: this.contract
438
+ };
439
+ }
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) {
65
442
  const at = `at-${++this.n}`;
66
443
  const rt = `rt-${++this.n}`;
67
444
  this.access.set(at, sub);
68
445
  this.refresh.set(rt, sub);
69
- return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...this.persistent === void 0 ? {} : { persistent: this.persistent } };
446
+ const persistent = this.persistent ?? kind;
447
+ return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...persistent === void 0 ? {} : { persistent } };
70
448
  }
71
449
  fetch = async (input, init) => {
72
450
  const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
@@ -75,8 +453,36 @@ var FakeCauthy = class {
75
453
  const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
76
454
  const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
77
455
  const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
456
+ const admin = adminAnswer(this, url, init);
457
+ if (admin) return admin;
78
458
  if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
459
+ const oauth = url.pathname === "/v1/oauth/token" || url.pathname === "/v1/oauth/revoke";
460
+ if (oauth) {
461
+ this.oauthRequests.push({ path: url.pathname, form: Object.fromEntries(form), headers: Object.fromEntries(new Headers(init?.headers)) });
462
+ }
463
+ const asserted = oauth && form.has("client_assertion") ? await this.assertionOf(form) : null;
464
+ const keyClient = this.keyClients.has(form.get("client_id") ?? "");
465
+ if (oauth && (keyClient || form.has("client_assertion")) && asserted === null) {
466
+ return reply(401, { error: "invalid_client", error_description: "client authentication failed" });
467
+ }
468
+ if (url.pathname === "/v1/connect/self") {
469
+ const kid = this.systemTokens.get(bearer);
470
+ if (kid === void 0) {
471
+ return reply(this.access.has(bearer) ? 403 : 401, { errors: [{ code: this.access.has(bearer) ? "not_a_system_token" : "invalid_token", message: "a system token is required" }] });
472
+ }
473
+ if (this.keys.get(kid)?.revoked) return reply(401, { errors: [{ code: "invalid_token", message: "the key was revoked" }] });
474
+ const body = JSON.stringify({ data: this.identityOf(kid) });
475
+ const etag = `"${b64uEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body)))).slice(0, 16)}"`;
476
+ const headers = { etag, "cache-control": "private, max-age=300" };
477
+ if (new Headers(init?.headers).get("if-none-match") === etag) return new Response(null, { status: 304, headers });
478
+ return new Response(body, { status: 200, headers: { "content-type": "application/json", ...headers } });
479
+ }
79
480
  if (url.pathname === "/v1/oauth/token") {
481
+ if (form.get("grant_type") === "client_credentials" && asserted !== null) {
482
+ const token = `sys-${++this.n}`;
483
+ this.systemTokens.set(token, asserted);
484
+ return reply(200, { access_token: token, token_type: "Bearer", expires_in: this.expiresIn });
485
+ }
80
486
  if (form.get("grant_type") === "authorization_code") {
81
487
  const c = this.codes.get(form.get("code") ?? "");
82
488
  this.codes.delete(form.get("code") ?? "");
@@ -129,13 +535,27 @@ var FakeCauthy = class {
129
535
  if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
130
536
  const method = init?.method ?? "GET";
131
537
  if (method === "POST" && url.pathname === "/v1/auth/login") {
538
+ if (this.settings.allow_password === false) return refuse(403, "password_disabled", "this workspace does not take passwords: sign in another way");
132
539
  const body = JSON.parse(String(init?.body ?? "{}"));
133
540
  const user = [...this.users.values()].find((u) => u.email === body.email);
134
541
  const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
135
542
  if (!user || !user.active || password === void 0 || password !== body.password) {
136
543
  return refuse(401, "invalid_credentials", "invalid email or password");
137
544
  }
138
- return reply(200, { data: this.issue(user.sub) });
545
+ const kind = this.settings.persistent_sessions === true ? new Headers(init?.headers).get("x-cauthy-remember") === "1" : void 0;
546
+ return reply(200, { data: this.issue(user.sub, kind) });
547
+ }
548
+ if (method === "POST" && url.pathname === "/v1/auth/register") {
549
+ if (this.settings.allow_signup === false) return refuse(403, "signups_disabled", "this workspace is not accepting new accounts");
550
+ if (this.settings.allow_password === false) return refuse(403, "password_disabled", "this workspace does not take passwords: sign in another way");
551
+ const body = JSON.parse(String(init?.body ?? "{}"));
552
+ if (!body.email || !body.password) return refuse(400, "validation_error", "email and password are required");
553
+ if ([...this.users.values()].some((u) => u.email === body.email)) return refuse(409, "email_taken", "an account with this email exists");
554
+ const confirm = this.settings.confirm_email === true;
555
+ const sub = `u-${++this.n}`;
556
+ this.users.set(sub, { sub, email: body.email, name: body.display_name ?? "", active: !confirm, restricted: false, verified: !confirm });
557
+ this.passwords.set(body.email, body.password);
558
+ return reply(201, { data: { user_id: sub, email: body.email, ...confirm ? { verification_required: true } : {} } });
139
559
  }
140
560
  if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
141
561
  if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
@@ -143,6 +563,68 @@ var FakeCauthy = class {
143
563
  return refuse(404, "not_found", "not found");
144
564
  }
145
565
  };
566
+ function adminAnswer(fake, url, init) {
567
+ const method = (init?.method ?? "GET").toUpperCase();
568
+ const route = `${method} ${url.pathname}`;
569
+ const client = method === "PATCH" ? url.pathname.match(/^\/v1\/oauth\/clients\/([^/]+)$/) : null;
570
+ const user = method === "DELETE" ? url.pathname.match(/^\/v1\/users\/([^/]+)$/) : null;
571
+ const known = ["GET /v1/settings/auth", "PUT /v1/settings/auth", "GET /v1/oauth/clients", "GET /v1/users"].includes(route);
572
+ if (!known && !client && !user) return null;
573
+ const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
574
+ const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
575
+ const headers = new Headers(init?.headers);
576
+ if (fake.adminToken === "" || headers.get("authorization") !== `Bearer ${fake.adminToken}`) return refuse(401, "unauthorized", "invalid or expired token");
577
+ if (headers.get("x-tenant-key") !== fake.tenant) return refuse(404, "tenant_not_found", "no such workspace");
578
+ if (route === "GET /v1/settings/auth") return reply(200, { data: fake.settings });
579
+ if (route === "PUT /v1/settings/auth") {
580
+ const change = JSON.parse(String(init?.body ?? "{}"));
581
+ const next = { ...fake.settings };
582
+ for (const key of ADMIN_SETTINGS) if (change[key] !== void 0 && change[key] !== null) next[key] = change[key];
583
+ if (next.allow_password === false && next.allow_magic_link !== true && next.allow_passkeys !== true) {
584
+ return refuse(409, "last_method", "the change would leave no way to sign in");
585
+ }
586
+ fake.settings = next;
587
+ return reply(200, { data: fake.settings });
588
+ }
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 });
591
+ if (client) {
592
+ if (decodeURIComponent(client[1]) !== fake.clientId) return refuse(404, "client_not_found", "no such client");
593
+ 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() } });
596
+ }
597
+ if (route === "GET /v1/users") {
598
+ const q = (url.searchParams.get("q") ?? "").toLowerCase();
599
+ const data = [...fake.users.values()].filter((u) => u.email.toLowerCase().includes(q)).map((u) => ({ id: u.sub, email: u.email, name: u.name }));
600
+ return reply(200, { data, total_count: data.length });
601
+ }
602
+ const id = decodeURIComponent(user[1]);
603
+ const gone = fake.users.get(id);
604
+ if (!gone) return refuse(404, "user_not_found", "no such user");
605
+ fake.users.delete(id);
606
+ fake.passwords.delete(gone.email);
607
+ return new Response(null, { status: 204 });
608
+ }
609
+ function serveFakeCauthy(fake, port) {
610
+ const server = (0, import_node_http.createServer)(async (req, res) => {
611
+ const chunks = [];
612
+ for await (const chunk of req) chunks.push(chunk);
613
+ const headers = new Headers();
614
+ for (const [k, v] of Object.entries(req.headers)) if (typeof v === "string") headers.set(k, v);
615
+ const answer = await fake.fetch(`${fake.issuer}${req.url ?? "/"}`, {
616
+ method: req.method,
617
+ headers,
618
+ ...chunks.length > 0 ? { body: Buffer.concat(chunks).toString("utf8") } : {}
619
+ });
620
+ res.writeHead(answer.status, Object.fromEntries(answer.headers));
621
+ res.end(Buffer.from(await answer.arrayBuffer()));
622
+ });
623
+ return new Promise((resolve, reject) => {
624
+ server.once("error", reject);
625
+ server.listen(port, "127.0.0.1", () => resolve(server));
626
+ });
627
+ }
146
628
  var Browser = class {
147
629
  /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
148
630
  constructor(origin = "https://app.test", options = {}) {
@@ -177,6 +659,7 @@ var Browser = class {
177
659
  // Annotate the CommonJS export names for ESM import in node:
178
660
  0 && (module.exports = {
179
661
  Browser,
180
- FakeCauthy
662
+ FakeCauthy,
663
+ serveFakeCauthy
181
664
  });
182
665
  //# sourceMappingURL=testing.js.map