@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/CHANGELOG.md +71 -0
- package/README.md +81 -24
- package/dist/{cauthy-B6Sm3T8M.d.ts → cauthy-C7gZ-pbv.d.ts} +88 -3
- package/dist/{cauthy-ClP5orP4.d.mts → cauthy-CCSSj3Lg.d.mts} +88 -3
- package/dist/express.d.mts +2 -1
- package/dist/express.d.ts +2 -1
- package/dist/fastify.d.mts +2 -1
- package/dist/fastify.d.ts +2 -1
- package/dist/hono.d.mts +2 -1
- package/dist/hono.d.ts +2 -1
- package/dist/identity-BTJ8NB_w.d.mts +27 -0
- package/dist/identity-BTJ8NB_w.d.ts +27 -0
- package/dist/index.d.mts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +686 -66
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +681 -66
- package/dist/index.mjs.map +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/keys.mjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.mjs.map +1 -1
- package/dist/next.d.mts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/testing.d.mts +49 -1
- package/dist/testing.d.ts +49 -1
- package/dist/testing.js +488 -5
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +486 -4
- package/dist/testing.mjs.map +1 -1
- package/package.json +8 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|