@cauthyhq/node 1.2.0 → 1.3.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/CHANGELOG.md +30 -0
- package/README.md +16 -28
- package/dist/{cauthy-CCSSj3Lg.d.mts → cauthy-CsWUgyv_.d.mts} +42 -145
- package/dist/{cauthy-C7gZ-pbv.d.ts → cauthy-DHLBEuqP.d.ts} +42 -145
- package/dist/express.d.mts +2 -5
- package/dist/express.d.ts +2 -5
- package/dist/express.js +1 -2
- package/dist/express.js.map +1 -1
- package/dist/express.mjs +1 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +1 -3
- package/dist/fastify.d.ts +1 -3
- package/dist/fastify.js +0 -2
- package/dist/fastify.js.map +1 -1
- package/dist/fastify.mjs +0 -2
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +2 -5
- package/dist/hono.d.ts +2 -5
- package/dist/hono.js +1 -2
- package/dist/hono.js.map +1 -1
- package/dist/hono.mjs +1 -2
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +1 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +187 -530
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -520
- package/dist/index.mjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.mjs.map +1 -1
- package/dist/next.d.mts +1 -3
- package/dist/next.d.ts +1 -3
- package/dist/testing.d.mts +11 -5
- package/dist/testing.d.ts +11 -5
- package/dist/testing.js +52 -50
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +52 -50
- package/dist/testing.mjs.map +1 -1
- package/package.json +5 -14
- package/dist/identity-BTJ8NB_w.d.mts +0 -27
- package/dist/identity-BTJ8NB_w.d.ts +0 -27
- package/dist/keys-D5k0omTb.d.mts +0 -64
- package/dist/keys-D5k0omTb.d.ts +0 -64
- package/dist/keys.d.mts +0 -8
- package/dist/keys.d.ts +0 -8
- package/dist/keys.js +0 -157
- package/dist/keys.js.map +0 -1
- package/dist/keys.mjs +0 -129
- package/dist/keys.mjs.map +0 -1
package/dist/testing.d.mts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
53
|
-
|
|
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 once
|
|
55
|
+
* connected by a (retired) contract has its key on `pact.<link>.service` and its people
|
|
56
|
+
* on `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
|
|
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
|
|
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
|
-
/**
|
|
53
|
-
|
|
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 once
|
|
55
|
+
* connected by a (retired) contract has its key on `pact.<link>.service` and its people
|
|
56
|
+
* on `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
|
|
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
|
|
289
|
+
const secrets = other ? [key.secret, other.secret] : [key.secret];
|
|
305
290
|
return {
|
|
306
|
-
cookie:
|
|
307
|
-
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
|
|
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
|
-
/**
|
|
351
|
-
|
|
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 once
|
|
337
|
+
* connected by a (retired) contract has its key on `pact.<link>.service` and its people
|
|
338
|
+
* on `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
|
|
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
|
}
|
|
@@ -471,7 +462,7 @@ var FakeCauthy = class {
|
|
|
471
462
|
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
463
|
}
|
|
473
464
|
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) });
|
|
465
|
+
const body = JSON.stringify({ data: { ...this.identityOf(kid), contract: null } });
|
|
475
466
|
const etag = `"${b64uEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body)))).slice(0, 16)}"`;
|
|
476
467
|
const headers = { etag, "cache-control": "private, max-age=300" };
|
|
477
468
|
if (new Headers(init?.headers).get("if-none-match") === etag) return new Response(null, { status: 304, headers });
|
|
@@ -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
|
-
|
|
492
|
-
|
|
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
|
|
496
|
-
this.refresh.
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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 (
|
|
595
|
-
|
|
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();
|