@cauthyhq/node 1.0.0 → 1.1.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 +43 -0
- package/README.md +4 -0
- package/dist/{cauthy-ClP5orP4.d.mts → cauthy-Bn-i7cKD.d.mts} +24 -1
- package/dist/{cauthy-B6Sm3T8M.d.ts → cauthy-Wy-YDGrM.d.ts} +24 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/fastify.d.ts +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/hono.d.ts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -7
- 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 -1
- package/dist/next.d.ts +1 -1
- package/dist/testing.d.mts +13 -1
- package/dist/testing.d.ts +13 -1
- package/dist/testing.js +93 -5
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +91 -4
- package/dist/testing.mjs.map +1 -1
- package/package.json +8 -3
package/dist/testing.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
|
|
1
3
|
declare class FakeCauthy {
|
|
2
4
|
readonly issuer = "https://id.test";
|
|
3
5
|
users: Map<string, {
|
|
@@ -36,15 +38,25 @@ declare class FakeCauthy {
|
|
|
36
38
|
passwords: Map<string, string>;
|
|
37
39
|
/** What GET /v1/auth/settings answers. */
|
|
38
40
|
settings: Record<string, unknown>;
|
|
41
|
+
/** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
|
|
42
|
+
adminToken: string;
|
|
43
|
+
/** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
|
|
44
|
+
clientId: string;
|
|
39
45
|
private n;
|
|
40
46
|
constructor();
|
|
41
47
|
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
42
48
|
authorize(location: string, sub?: string): string;
|
|
49
|
+
/** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */
|
|
43
50
|
private issue;
|
|
44
51
|
fetch: typeof fetch;
|
|
45
52
|
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
46
53
|
private ownScreen;
|
|
47
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Serves a FakeCauthy over HTTP on 127.0.0.1, for a process outside this one
|
|
57
|
+
* (the conformance runner) to reach it, as `--cauthy http://127.0.0.1:<port>`.
|
|
58
|
+
*/
|
|
59
|
+
declare function serveFakeCauthy(fake: FakeCauthy, port: number): Promise<Server>;
|
|
48
60
|
/** A browser as far as cookies go: it keeps what it is told to and sends it back. */
|
|
49
61
|
declare class Browser {
|
|
50
62
|
readonly origin: string;
|
|
@@ -59,4 +71,4 @@ declare class Browser {
|
|
|
59
71
|
take(source: Response | string[]): void;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
export { Browser, FakeCauthy };
|
|
74
|
+
export { Browser, FakeCauthy, serveFakeCauthy };
|
package/dist/testing.js
CHANGED
|
@@ -21,9 +21,12 @@ 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
|
+
var ADMIN_SETTINGS = ["allow_signup", "allow_password", "allow_magic_link", "allow_passkeys", "confirm_email", "verify_grace_days"];
|
|
27
30
|
var FakeCauthy = class {
|
|
28
31
|
issuer = "https://id.test";
|
|
29
32
|
users = /* @__PURE__ */ new Map();
|
|
@@ -49,6 +52,10 @@ var FakeCauthy = class {
|
|
|
49
52
|
passwords = /* @__PURE__ */ new Map();
|
|
50
53
|
/** What GET /v1/auth/settings answers. */
|
|
51
54
|
settings = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false };
|
|
55
|
+
/** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
|
|
56
|
+
adminToken = "";
|
|
57
|
+
/** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
|
|
58
|
+
clientId = "acme-crm";
|
|
52
59
|
n = 0;
|
|
53
60
|
constructor() {
|
|
54
61
|
this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
|
|
@@ -61,12 +68,14 @@ var FakeCauthy = class {
|
|
|
61
68
|
this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
|
|
62
69
|
return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
|
|
63
70
|
}
|
|
64
|
-
|
|
71
|
+
/** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */
|
|
72
|
+
issue(sub, kind) {
|
|
65
73
|
const at = `at-${++this.n}`;
|
|
66
74
|
const rt = `rt-${++this.n}`;
|
|
67
75
|
this.access.set(at, sub);
|
|
68
76
|
this.refresh.set(rt, sub);
|
|
69
|
-
|
|
77
|
+
const persistent = this.persistent ?? kind;
|
|
78
|
+
return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...persistent === void 0 ? {} : { persistent } };
|
|
70
79
|
}
|
|
71
80
|
fetch = async (input, init) => {
|
|
72
81
|
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
@@ -75,6 +84,8 @@ var FakeCauthy = class {
|
|
|
75
84
|
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
76
85
|
const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
|
|
77
86
|
const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
|
|
87
|
+
const admin = adminAnswer(this, url, init);
|
|
88
|
+
if (admin) return admin;
|
|
78
89
|
if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
|
|
79
90
|
if (url.pathname === "/v1/oauth/token") {
|
|
80
91
|
if (form.get("grant_type") === "authorization_code") {
|
|
@@ -129,13 +140,27 @@ var FakeCauthy = class {
|
|
|
129
140
|
if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
130
141
|
const method = init?.method ?? "GET";
|
|
131
142
|
if (method === "POST" && url.pathname === "/v1/auth/login") {
|
|
143
|
+
if (this.settings.allow_password === false) return refuse(403, "password_disabled", "this workspace does not take passwords: sign in another way");
|
|
132
144
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
133
145
|
const user = [...this.users.values()].find((u) => u.email === body.email);
|
|
134
146
|
const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
|
|
135
147
|
if (!user || !user.active || password === void 0 || password !== body.password) {
|
|
136
148
|
return refuse(401, "invalid_credentials", "invalid email or password");
|
|
137
149
|
}
|
|
138
|
-
|
|
150
|
+
const kind = this.settings.persistent_sessions === true ? new Headers(init?.headers).get("x-cauthy-remember") === "1" : void 0;
|
|
151
|
+
return reply(200, { data: this.issue(user.sub, kind) });
|
|
152
|
+
}
|
|
153
|
+
if (method === "POST" && url.pathname === "/v1/auth/register") {
|
|
154
|
+
if (this.settings.allow_signup === false) return refuse(403, "signups_disabled", "this workspace is not accepting new accounts");
|
|
155
|
+
if (this.settings.allow_password === false) return refuse(403, "password_disabled", "this workspace does not take passwords: sign in another way");
|
|
156
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
157
|
+
if (!body.email || !body.password) return refuse(400, "validation_error", "email and password are required");
|
|
158
|
+
if ([...this.users.values()].some((u) => u.email === body.email)) return refuse(409, "email_taken", "an account with this email exists");
|
|
159
|
+
const confirm = this.settings.confirm_email === true;
|
|
160
|
+
const sub = `u-${++this.n}`;
|
|
161
|
+
this.users.set(sub, { sub, email: body.email, name: body.display_name ?? "", active: !confirm, restricted: false, verified: !confirm });
|
|
162
|
+
this.passwords.set(body.email, body.password);
|
|
163
|
+
return reply(201, { data: { user_id: sub, email: body.email, ...confirm ? { verification_required: true } : {} } });
|
|
139
164
|
}
|
|
140
165
|
if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
|
|
141
166
|
if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
|
|
@@ -143,6 +168,68 @@ var FakeCauthy = class {
|
|
|
143
168
|
return refuse(404, "not_found", "not found");
|
|
144
169
|
}
|
|
145
170
|
};
|
|
171
|
+
function adminAnswer(fake, url, init) {
|
|
172
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
173
|
+
const route = `${method} ${url.pathname}`;
|
|
174
|
+
const client = method === "PATCH" ? url.pathname.match(/^\/v1\/oauth\/clients\/([^/]+)$/) : null;
|
|
175
|
+
const user = method === "DELETE" ? url.pathname.match(/^\/v1\/users\/([^/]+)$/) : null;
|
|
176
|
+
const known = ["GET /v1/settings/auth", "PUT /v1/settings/auth", "GET /v1/oauth/clients", "GET /v1/users"].includes(route);
|
|
177
|
+
if (!known && !client && !user) return null;
|
|
178
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
179
|
+
const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
|
|
180
|
+
const headers = new Headers(init?.headers);
|
|
181
|
+
if (fake.adminToken === "" || headers.get("authorization") !== `Bearer ${fake.adminToken}`) return refuse(401, "unauthorized", "invalid or expired token");
|
|
182
|
+
if (headers.get("x-tenant-key") !== fake.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
183
|
+
if (route === "GET /v1/settings/auth") return reply(200, { data: fake.settings });
|
|
184
|
+
if (route === "PUT /v1/settings/auth") {
|
|
185
|
+
const change = JSON.parse(String(init?.body ?? "{}"));
|
|
186
|
+
const next = { ...fake.settings };
|
|
187
|
+
for (const key of ADMIN_SETTINGS) if (change[key] !== void 0 && change[key] !== null) next[key] = change[key];
|
|
188
|
+
if (next.allow_password === false && next.allow_magic_link !== true && next.allow_passkeys !== true) {
|
|
189
|
+
return refuse(409, "last_method", "the change would leave no way to sign in");
|
|
190
|
+
}
|
|
191
|
+
fake.settings = next;
|
|
192
|
+
return reply(200, { data: fake.settings });
|
|
193
|
+
}
|
|
194
|
+
const persistent = () => fake.settings.persistent_sessions === true;
|
|
195
|
+
if (route === "GET /v1/oauth/clients") return reply(200, { data: [{ client_id: fake.clientId, allow_persistent_sessions: persistent() }], total_count: 1 });
|
|
196
|
+
if (client) {
|
|
197
|
+
if (decodeURIComponent(client[1]) !== fake.clientId) return refuse(404, "client_not_found", "no such client");
|
|
198
|
+
const change = JSON.parse(String(init?.body ?? "{}"));
|
|
199
|
+
if (typeof change.allow_persistent_sessions === "boolean") fake.settings = { ...fake.settings, persistent_sessions: change.allow_persistent_sessions };
|
|
200
|
+
return reply(200, { data: { client_id: fake.clientId, allow_persistent_sessions: persistent() } });
|
|
201
|
+
}
|
|
202
|
+
if (route === "GET /v1/users") {
|
|
203
|
+
const q = (url.searchParams.get("q") ?? "").toLowerCase();
|
|
204
|
+
const data = [...fake.users.values()].filter((u) => u.email.toLowerCase().includes(q)).map((u) => ({ id: u.sub, email: u.email, name: u.name }));
|
|
205
|
+
return reply(200, { data, total_count: data.length });
|
|
206
|
+
}
|
|
207
|
+
const id = decodeURIComponent(user[1]);
|
|
208
|
+
const gone = fake.users.get(id);
|
|
209
|
+
if (!gone) return refuse(404, "user_not_found", "no such user");
|
|
210
|
+
fake.users.delete(id);
|
|
211
|
+
fake.passwords.delete(gone.email);
|
|
212
|
+
return new Response(null, { status: 204 });
|
|
213
|
+
}
|
|
214
|
+
function serveFakeCauthy(fake, port) {
|
|
215
|
+
const server = (0, import_node_http.createServer)(async (req, res) => {
|
|
216
|
+
const chunks = [];
|
|
217
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
218
|
+
const headers = new Headers();
|
|
219
|
+
for (const [k, v] of Object.entries(req.headers)) if (typeof v === "string") headers.set(k, v);
|
|
220
|
+
const answer = await fake.fetch(`${fake.issuer}${req.url ?? "/"}`, {
|
|
221
|
+
method: req.method,
|
|
222
|
+
headers,
|
|
223
|
+
...chunks.length > 0 ? { body: Buffer.concat(chunks).toString("utf8") } : {}
|
|
224
|
+
});
|
|
225
|
+
res.writeHead(answer.status, Object.fromEntries(answer.headers));
|
|
226
|
+
res.end(Buffer.from(await answer.arrayBuffer()));
|
|
227
|
+
});
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
server.once("error", reject);
|
|
230
|
+
server.listen(port, "127.0.0.1", () => resolve(server));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
146
233
|
var Browser = class {
|
|
147
234
|
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
148
235
|
constructor(origin = "https://app.test", options = {}) {
|
|
@@ -177,6 +264,7 @@ var Browser = class {
|
|
|
177
264
|
// Annotate the CommonJS export names for ESM import in node:
|
|
178
265
|
0 && (module.exports = {
|
|
179
266
|
Browser,
|
|
180
|
-
FakeCauthy
|
|
267
|
+
FakeCauthy,
|
|
268
|
+
serveFakeCauthy
|
|
181
269
|
});
|
|
182
270
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// A Cauthy that lives in memory, and a browser that keeps cookies: what the\n// tests sign in against. Exported as `@cauthyhq/node/testing`, for tests only:\n// it signs in anyone it was told about, and checks nothing a real Cauthy does.\n\nexport class FakeCauthy {\n readonly issuer = 'https://id.test'\n users = new Map<string, { sub: string; email: string; name: string; active: boolean; restricted: boolean; anonymous?: boolean; verified?: boolean }>()\n codes = new Map<string, { sub: string; challenge: string; redirectUri: string }>()\n refresh = new Map<string, string>() // refresh token → sub\n access = new Map<string, string>() // access token → sub\n orgs = new Map<string, Record<string, unknown>[]>() // sub → organizations\n revoked: string[] = []\n checks: { organization: string; sub: string; body: any }[] = []\n calls: string[] = []\n down = false\n expiresIn = 600\n /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */\n persistent: boolean | undefined = undefined\n /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */\n ownScreens = false\n /** The workspace those screens sign in to, as X-Tenant-Key names it. */\n tenant = 'acme'\n /** Each account's password, by email: only these accounts sign in with one. */\n passwords = new Map<string, string>()\n /** What GET /v1/auth/settings answers. */\n settings: Record<string, unknown> = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false }\n private n = 0\n\n constructor() {\n this.users.set('u1', { sub: 'u1', email: 'ana@acme.com', name: 'Ana', active: true, restricted: false })\n }\n\n /** What the hosted page does once the person has signed in: hands the browser a code. */\n authorize(location: string, sub = 'u1'): string {\n const q = new URL(location).searchParams\n if (q.get('response_type') !== 'code') throw new Error('Cauthy answers unsupported_response_type: response_type=code is required')\n const code = `code-${++this.n}`\n this.codes.set(code, { sub, challenge: q.get('code_challenge') ?? '', redirectUri: q.get('redirect_uri') ?? '' })\n return `${q.get('redirect_uri')}?code=${code}&state=${q.get('state')}`\n }\n\n private issue(sub: string) {\n const at = `at-${++this.n}`\n const rt = `rt-${++this.n}`\n this.access.set(at, sub)\n this.refresh.set(rt, sub)\n return { access_token: at, refresh_token: rt, token_type: 'Bearer', expires_in: this.expiresIn, scope: '', ...(this.persistent === undefined ? {} : { persistent: this.persistent }) }\n }\n\n fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)\n this.calls.push(`${init?.method ?? 'GET'} ${url.pathname}`)\n if (this.down) throw new TypeError('fetch failed')\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ''))\n const bearer = new Headers(init?.headers).get('authorization')?.replace('Bearer ', '') ?? ''\n if (this.ownScreens && url.pathname.startsWith('/v1/auth/')) return this.ownScreen(url, init)\n\n if (url.pathname === '/v1/oauth/token') {\n if (form.get('grant_type') === 'authorization_code') {\n const c = this.codes.get(form.get('code') ?? '')\n this.codes.delete(form.get('code') ?? '')\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(form.get('code_verifier') ?? ''))\n const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n 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' })\n return reply(200, this.issue(c.sub))\n }\n if (form.get('grant_type') === 'refresh_token') {\n const sub = this.refresh.get(form.get('refresh_token') ?? '')\n this.refresh.delete(form.get('refresh_token') ?? '') // used once\n if (!sub || !this.users.get(sub)?.active) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(sub))\n }\n return reply(400, { error: 'unsupported_grant_type', error_description: 'supported grant types: authorization_code, refresh_token' })\n }\n if (url.pathname === '/v1/oauth/revoke') {\n this.revoked.push(form.get('token') ?? '')\n this.refresh.delete(form.get('token') ?? '')\n return reply(200, {})\n }\n const sub = this.access.get(bearer)\n const user = sub ? this.users.get(sub) : undefined\n if (url.pathname === '/v1/oauth/userinfo') {\n if (!user) return reply(401, {})\n return reply(200, { sub: user.sub, tid: 't1', email: user.email, name: user.name, email_verified: user.verified ?? !user.anonymous, restricted: user.restricted, roles: ['member'], ...(user.anonymous ? { anonymous: true } : {}) })\n }\n const asking = url.pathname.match(/^\\/v1\\/oauth\\/organizations\\/([^/]+)\\/check$/)\n if (asking && init?.method === 'POST') {\n if (!user) return reply(401, { errors: [{ code: 'unauthorized', message: 'invalid or expired token' }] })\n const org = decodeURIComponent(asking[1])\n const body = JSON.parse(String(init.body))\n this.checks.push({ organization: org, sub: user.sub, body })\n if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: 'organization_not_found', message: 'organization not found' }] })\n // An accountant approves invoices up to a million.\n const allowed = body.resource === 'invoices' && body.action === 'approve' && Number(body.attributes?.amount) <= 1_000_000\n return reply(200, { data: allowed\n ? { allowed, reason: 'allowed by role accountant: invoices:approve', rule: { source: 'role', role: 'accountant', kind: '*', resource: 'invoices', action: 'approve', effect: 'allow', condition: 'resource.attrs.amount <= 1000000' } }\n : { allowed, reason: 'no rule allows it' } })\n }\n if (url.pathname === '/v1/oauth/organizations') {\n if (!user) return reply(401, {})\n const system = url.searchParams.get('system')\n const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system)\n return reply(200, { data, total_count: data.length })\n }\n return reply(404, {})\n }\n\n /** The part of /v1/auth/* a password sign-in and the sign-in form read. */\n private ownScreen(url: URL, init: RequestInit | undefined): Response {\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n if (new Headers(init?.headers).get('x-tenant-key') !== this.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n const method = init?.method ?? 'GET'\n if (method === 'POST' && url.pathname === '/v1/auth/login') {\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string }\n const user = [...this.users.values()].find((u) => u.email === body.email)\n const password = body.email === undefined ? undefined : this.passwords.get(body.email)\n if (!user || !user.active || password === undefined || password !== body.password) {\n return refuse(401, 'invalid_credentials', 'invalid email or password')\n }\n return reply(200, { data: this.issue(user.sub) })\n }\n if (method === 'GET' && url.pathname === '/v1/auth/settings') return reply(200, { data: this.settings })\n if (method === 'GET' && (url.pathname === '/v1/auth/social/providers' || url.pathname === '/v1/auth/sso/connections')) return reply(200, { data: [] })\n if (method === 'GET' && url.pathname === '/v1/auth/webauthn/place') return reply(200, { data: { here: false, on_cauthy: false } })\n return refuse(404, 'not_found', 'not found')\n }\n}\n\n/** A browser as far as cookies go: it keeps what it is told to and sends it back. */\nexport class Browser {\n jar = new Map<string, string>()\n /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */\n constructor(readonly origin = 'https://app.test', private readonly options: { requestHeader?: boolean } = {}) {}\n\n request(path: string, init: RequestInit = {}): Request {\n const headers = new Headers(init.headers)\n if (this.jar.size > 0) headers.set('cookie', [...this.jar].map(([k, v]) => `${k}=${v}`).join('; '))\n // What a protocol-1 client sends on every mutation.\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && this.options.requestHeader !== false && !headers.has('x-cauthy-request')) {\n headers.set('x-cauthy-request', '1')\n }\n return new Request(this.origin + path, { ...init, headers })\n }\n\n /** Keeps the cookies of a response, as a browser would. */\n take(source: Response | string[]): void {\n const cookies = Array.isArray(source) ? source : source.headers.getSetCookie()\n for (const c of cookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n const name = pair.slice(0, i)\n const value = pair.slice(i + 1)\n if (value === '' || attrs.includes('Max-Age=0')) this.jar.delete(name)\n else this.jar.set(name, value)\n }\n }\n}\n"],"mappings":"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","names":["sub"]}
|
|
1
|
+
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// A Cauthy that lives in memory, and a browser that keeps cookies: what the\n// tests sign in against. Exported as `@cauthyhq/node/testing`, for tests only:\n// it signs in anyone it was told about, and checks nothing a real Cauthy does.\n\nimport { createServer, type Server } from 'node:http'\n\n/** The settings the fake's admin API changes (PUT /v1/settings/auth), as the dashboard's switches do. */\nconst ADMIN_SETTINGS = ['allow_signup', 'allow_password', 'allow_magic_link', 'allow_passkeys', 'confirm_email', 'verify_grace_days'] as const\n\nexport class FakeCauthy {\n readonly issuer = 'https://id.test'\n users = new Map<string, { sub: string; email: string; name: string; active: boolean; restricted: boolean; anonymous?: boolean; verified?: boolean }>()\n codes = new Map<string, { sub: string; challenge: string; redirectUri: string }>()\n refresh = new Map<string, string>() // refresh token → sub\n access = new Map<string, string>() // access token → sub\n orgs = new Map<string, Record<string, unknown>[]>() // sub → organizations\n revoked: string[] = []\n checks: { organization: string; sub: string; body: any }[] = []\n calls: string[] = []\n down = false\n expiresIn = 600\n /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */\n persistent: boolean | undefined = undefined\n /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */\n ownScreens = false\n /** The workspace those screens sign in to, as X-Tenant-Key names it. */\n tenant = 'acme'\n /** Each account's password, by email: only these accounts sign in with one. */\n passwords = new Map<string, string>()\n /** What GET /v1/auth/settings answers. */\n settings: Record<string, unknown> = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false }\n /** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */\n adminToken = ''\n /** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */\n clientId = 'acme-crm'\n private n = 0\n\n constructor() {\n this.users.set('u1', { sub: 'u1', email: 'ana@acme.com', name: 'Ana', active: true, restricted: false })\n }\n\n /** What the hosted page does once the person has signed in: hands the browser a code. */\n authorize(location: string, sub = 'u1'): string {\n const q = new URL(location).searchParams\n if (q.get('response_type') !== 'code') throw new Error('Cauthy answers unsupported_response_type: response_type=code is required')\n const code = `code-${++this.n}`\n this.codes.set(code, { sub, challenge: q.get('code_challenge') ?? '', redirectUri: q.get('redirect_uri') ?? '' })\n return `${q.get('redirect_uri')}?code=${code}&state=${q.get('state')}`\n }\n\n /** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */\n private issue(sub: string, kind?: boolean) {\n const at = `at-${++this.n}`\n const rt = `rt-${++this.n}`\n this.access.set(at, sub)\n this.refresh.set(rt, sub)\n const persistent = this.persistent ?? kind\n return { access_token: at, refresh_token: rt, token_type: 'Bearer', expires_in: this.expiresIn, scope: '', ...(persistent === undefined ? {} : { persistent }) }\n }\n\n fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)\n this.calls.push(`${init?.method ?? 'GET'} ${url.pathname}`)\n if (this.down) throw new TypeError('fetch failed')\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ''))\n const bearer = new Headers(init?.headers).get('authorization')?.replace('Bearer ', '') ?? ''\n const admin = adminAnswer(this, url, init)\n if (admin) return admin\n if (this.ownScreens && url.pathname.startsWith('/v1/auth/')) return this.ownScreen(url, init)\n\n if (url.pathname === '/v1/oauth/token') {\n if (form.get('grant_type') === 'authorization_code') {\n const c = this.codes.get(form.get('code') ?? '')\n this.codes.delete(form.get('code') ?? '')\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(form.get('code_verifier') ?? ''))\n const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n 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' })\n return reply(200, this.issue(c.sub))\n }\n if (form.get('grant_type') === 'refresh_token') {\n const sub = this.refresh.get(form.get('refresh_token') ?? '')\n this.refresh.delete(form.get('refresh_token') ?? '') // used once\n if (!sub || !this.users.get(sub)?.active) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(sub))\n }\n return reply(400, { error: 'unsupported_grant_type', error_description: 'supported grant types: authorization_code, refresh_token' })\n }\n if (url.pathname === '/v1/oauth/revoke') {\n this.revoked.push(form.get('token') ?? '')\n this.refresh.delete(form.get('token') ?? '')\n return reply(200, {})\n }\n const sub = this.access.get(bearer)\n const user = sub ? this.users.get(sub) : undefined\n if (url.pathname === '/v1/oauth/userinfo') {\n if (!user) return reply(401, {})\n return reply(200, { sub: user.sub, tid: 't1', email: user.email, name: user.name, email_verified: user.verified ?? !user.anonymous, restricted: user.restricted, roles: ['member'], ...(user.anonymous ? { anonymous: true } : {}) })\n }\n const asking = url.pathname.match(/^\\/v1\\/oauth\\/organizations\\/([^/]+)\\/check$/)\n if (asking && init?.method === 'POST') {\n if (!user) return reply(401, { errors: [{ code: 'unauthorized', message: 'invalid or expired token' }] })\n const org = decodeURIComponent(asking[1])\n const body = JSON.parse(String(init.body))\n this.checks.push({ organization: org, sub: user.sub, body })\n if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: 'organization_not_found', message: 'organization not found' }] })\n // An accountant approves invoices up to a million.\n const allowed = body.resource === 'invoices' && body.action === 'approve' && Number(body.attributes?.amount) <= 1_000_000\n return reply(200, { data: allowed\n ? { allowed, reason: 'allowed by role accountant: invoices:approve', rule: { source: 'role', role: 'accountant', kind: '*', resource: 'invoices', action: 'approve', effect: 'allow', condition: 'resource.attrs.amount <= 1000000' } }\n : { allowed, reason: 'no rule allows it' } })\n }\n if (url.pathname === '/v1/oauth/organizations') {\n if (!user) return reply(401, {})\n const system = url.searchParams.get('system')\n const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system)\n return reply(200, { data, total_count: data.length })\n }\n return reply(404, {})\n }\n\n /** The part of /v1/auth/* a password sign-in and the sign-in form read. */\n private ownScreen(url: URL, init: RequestInit | undefined): Response {\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n if (new Headers(init?.headers).get('x-tenant-key') !== this.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n const method = init?.method ?? 'GET'\n if (method === 'POST' && url.pathname === '/v1/auth/login') {\n if (this.settings.allow_password === false) return refuse(403, 'password_disabled', 'this workspace does not take passwords: sign in another way')\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string }\n const user = [...this.users.values()].find((u) => u.email === body.email)\n const password = body.email === undefined ? undefined : this.passwords.get(body.email)\n if (!user || !user.active || password === undefined || password !== body.password) {\n return refuse(401, 'invalid_credentials', 'invalid email or password')\n }\n // A long session only when the person asked and the client allows it.\n const kind = this.settings.persistent_sessions === true ? new Headers(init?.headers).get('x-cauthy-remember') === '1' : undefined\n return reply(200, { data: this.issue(user.sub, kind) })\n }\n if (method === 'POST' && url.pathname === '/v1/auth/register') {\n if (this.settings.allow_signup === false) return refuse(403, 'signups_disabled', 'this workspace is not accepting new accounts')\n if (this.settings.allow_password === false) return refuse(403, 'password_disabled', 'this workspace does not take passwords: sign in another way')\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string; display_name?: string }\n if (!body.email || !body.password) return refuse(400, 'validation_error', 'email and password are required')\n if ([...this.users.values()].some((u) => u.email === body.email)) return refuse(409, 'email_taken', 'an account with this email exists')\n // With confirm_email the account waits for its address (verify_grace_days 0 in the fake).\n const confirm = this.settings.confirm_email === true\n const sub = `u-${++this.n}`\n this.users.set(sub, { sub, email: body.email, name: body.display_name ?? '', active: !confirm, restricted: false, verified: !confirm })\n this.passwords.set(body.email, body.password)\n return reply(201, { data: { user_id: sub, email: body.email, ...(confirm ? { verification_required: true } : {}) } })\n }\n if (method === 'GET' && url.pathname === '/v1/auth/settings') return reply(200, { data: this.settings })\n if (method === 'GET' && (url.pathname === '/v1/auth/social/providers' || url.pathname === '/v1/auth/sso/connections')) return reply(200, { data: [] })\n if (method === 'GET' && url.pathname === '/v1/auth/webauthn/place') return reply(200, { data: { here: false, on_cauthy: false } })\n return refuse(404, 'not_found', 'not found')\n }\n}\n\n/**\n * The part of Cauthy's admin API the conformance runner's dashboard-switch\n * checks drive: GET and PUT /v1/settings/auth, GET /v1/oauth/clients,\n * PATCH /v1/oauth/clients/{id}, GET /v1/users and DELETE /v1/users/{id}.\n * null when the request is none of them.\n */\nfunction adminAnswer(fake: FakeCauthy, url: URL, init: RequestInit | undefined): Response | null {\n const method = (init?.method ?? 'GET').toUpperCase()\n const route = `${method} ${url.pathname}`\n const client = method === 'PATCH' ? url.pathname.match(/^\\/v1\\/oauth\\/clients\\/([^/]+)$/) : null\n const user = method === 'DELETE' ? url.pathname.match(/^\\/v1\\/users\\/([^/]+)$/) : null\n const known = ['GET /v1/settings/auth', 'PUT /v1/settings/auth', 'GET /v1/oauth/clients', 'GET /v1/users'].includes(route)\n if (!known && !client && !user) return null\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n const headers = new Headers(init?.headers)\n if (fake.adminToken === '' || headers.get('authorization') !== `Bearer ${fake.adminToken}`) return refuse(401, 'unauthorized', 'invalid or expired token')\n if (headers.get('x-tenant-key') !== fake.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n if (route === 'GET /v1/settings/auth') return reply(200, { data: fake.settings })\n if (route === 'PUT /v1/settings/auth') {\n const change = JSON.parse(String(init?.body ?? '{}')) as Record<string, unknown>\n const next = { ...fake.settings }\n for (const key of ADMIN_SETTINGS) if (change[key] !== undefined && change[key] !== null) next[key] = change[key]\n if (next.allow_password === false && next.allow_magic_link !== true && next.allow_passkeys !== true) {\n return refuse(409, 'last_method', 'the change would leave no way to sign in')\n }\n fake.settings = next\n return reply(200, { data: fake.settings })\n }\n const persistent = () => fake.settings.persistent_sessions === true\n if (route === 'GET /v1/oauth/clients') return reply(200, { data: [{ client_id: fake.clientId, allow_persistent_sessions: persistent() }], total_count: 1 })\n if (client) {\n if (decodeURIComponent(client[1]) !== fake.clientId) return refuse(404, 'client_not_found', 'no such client')\n const change = JSON.parse(String(init?.body ?? '{}')) as { allow_persistent_sessions?: unknown }\n if (typeof change.allow_persistent_sessions === 'boolean') fake.settings = { ...fake.settings, persistent_sessions: change.allow_persistent_sessions }\n return reply(200, { data: { client_id: fake.clientId, allow_persistent_sessions: persistent() } })\n }\n if (route === 'GET /v1/users') {\n const q = (url.searchParams.get('q') ?? '').toLowerCase()\n const data = [...fake.users.values()].filter((u) => u.email.toLowerCase().includes(q)).map((u) => ({ id: u.sub, email: u.email, name: u.name }))\n return reply(200, { data, total_count: data.length })\n }\n const id = decodeURIComponent(user![1])\n const gone = fake.users.get(id)\n if (!gone) return refuse(404, 'user_not_found', 'no such user')\n fake.users.delete(id)\n fake.passwords.delete(gone.email)\n return new Response(null, { status: 204 })\n}\n\n/**\n * Serves a FakeCauthy over HTTP on 127.0.0.1, for a process outside this one\n * (the conformance runner) to reach it, as `--cauthy http://127.0.0.1:<port>`.\n */\nexport function serveFakeCauthy(fake: FakeCauthy, port: number): Promise<Server> {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) if (typeof v === 'string') headers.set(k, v)\n const answer = await fake.fetch(`${fake.issuer}${req.url ?? '/'}`, {\n method: req.method,\n headers,\n ...(chunks.length > 0 ? { body: Buffer.concat(chunks).toString('utf8') } : {}),\n })\n res.writeHead(answer.status, Object.fromEntries(answer.headers))\n res.end(Buffer.from(await answer.arrayBuffer()))\n })\n return new Promise((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, '127.0.0.1', () => resolve(server))\n })\n}\n\n/** A browser as far as cookies go: it keeps what it is told to and sends it back. */\nexport class Browser {\n jar = new Map<string, string>()\n /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */\n constructor(readonly origin = 'https://app.test', private readonly options: { requestHeader?: boolean } = {}) {}\n\n request(path: string, init: RequestInit = {}): Request {\n const headers = new Headers(init.headers)\n if (this.jar.size > 0) headers.set('cookie', [...this.jar].map(([k, v]) => `${k}=${v}`).join('; '))\n // What a protocol-1 client sends on every mutation.\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && this.options.requestHeader !== false && !headers.has('x-cauthy-request')) {\n headers.set('x-cauthy-request', '1')\n }\n return new Request(this.origin + path, { ...init, headers })\n }\n\n /** Keeps the cookies of a response, as a browser would. */\n take(source: Response | string[]): void {\n const cookies = Array.isArray(source) ? source : source.headers.getSetCookie()\n for (const c of cookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n const name = pair.slice(0, i)\n const value = pair.slice(i + 1)\n if (value === '' || attrs.includes('Max-Age=0')) this.jar.delete(name)\n else this.jar.set(name, value)\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,uBAA0C;AAG1C,IAAM,iBAAiB,CAAC,gBAAgB,kBAAkB,oBAAoB,kBAAkB,iBAAiB,mBAAmB;AAE7H,IAAM,aAAN,MAAiB;AAAA,EACb,SAAS;AAAA,EAClB,QAAQ,oBAAI,IAAyI;AAAA,EACrJ,QAAQ,oBAAI,IAAqE;AAAA,EACjF,UAAU,oBAAI,IAAoB;AAAA;AAAA,EAClC,SAAS,oBAAI,IAAoB;AAAA;AAAA,EACjC,OAAO,oBAAI,IAAuC;AAAA;AAAA,EAClD,UAAoB,CAAC;AAAA,EACrB,SAA6D,CAAC;AAAA,EAC9D,QAAkB,CAAC;AAAA,EACnB,OAAO;AAAA,EACP,YAAY;AAAA;AAAA,EAEZ,aAAkC;AAAA;AAAA,EAElC,aAAa;AAAA;AAAA,EAEb,SAAS;AAAA;AAAA,EAET,YAAY,oBAAI,IAAoB;AAAA;AAAA,EAEpC,WAAoC,EAAE,gBAAgB,MAAM,cAAc,OAAO,gBAAgB,OAAO,qBAAqB,MAAM;AAAA;AAAA,EAEnI,aAAa;AAAA;AAAA,EAEb,WAAW;AAAA,EACH,IAAI;AAAA,EAEZ,cAAc;AACZ,SAAK,MAAM,IAAI,MAAM,EAAE,KAAK,MAAM,OAAO,gBAAgB,MAAM,OAAO,QAAQ,MAAM,YAAY,MAAM,CAAC;AAAA,EACzG;AAAA;AAAA,EAGA,UAAU,UAAkB,MAAM,MAAc;AAC9C,UAAM,IAAI,IAAI,IAAI,QAAQ,EAAE;AAC5B,QAAI,EAAE,IAAI,eAAe,MAAM,OAAQ,OAAM,IAAI,MAAM,0EAA0E;AACjI,UAAM,OAAO,QAAQ,EAAE,KAAK,CAAC;AAC7B,SAAK,MAAM,IAAI,MAAM,EAAE,KAAK,WAAW,EAAE,IAAI,gBAAgB,KAAK,IAAI,aAAa,EAAE,IAAI,cAAc,KAAK,GAAG,CAAC;AAChH,WAAO,GAAG,EAAE,IAAI,cAAc,CAAC,SAAS,IAAI,UAAU,EAAE,IAAI,OAAO,CAAC;AAAA,EACtE;AAAA;AAAA,EAGQ,MAAM,KAAa,MAAgB;AACzC,UAAM,KAAK,MAAM,EAAE,KAAK,CAAC;AACzB,UAAM,KAAK,MAAM,EAAE,KAAK,CAAC;AACzB,SAAK,OAAO,IAAI,IAAI,GAAG;AACvB,SAAK,QAAQ,IAAI,IAAI,GAAG;AACxB,UAAM,aAAa,KAAK,cAAc;AACtC,WAAO,EAAE,cAAc,IAAI,eAAe,IAAI,YAAY,UAAU,YAAY,KAAK,WAAW,OAAO,IAAI,GAAI,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW,EAAG;AAAA,EACjK;AAAA,EAEA,QAAsB,OAAO,OAAO,SAAS;AAC3C,UAAM,MAAM,IAAI,IAAI,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM,GAAG;AACrG,SAAK,MAAM,KAAK,GAAG,MAAM,UAAU,KAAK,IAAI,IAAI,QAAQ,EAAE;AAC1D,QAAI,KAAK,KAAM,OAAM,IAAI,UAAU,cAAc;AACjD,UAAM,QAAQ,CAAC,QAAgB,SAC7B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EAAE,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,EAAE,CAAC;AAChG,UAAM,OAAO,IAAI,gBAAgB,MAAM,gBAAgB,kBAAkB,KAAK,OAAO,OAAO,MAAM,QAAQ,EAAE,CAAC;AAC7G,UAAM,SAAS,IAAI,QAAQ,MAAM,OAAO,EAAE,IAAI,eAAe,GAAG,QAAQ,WAAW,EAAE,KAAK;AAC1F,UAAM,QAAQ,YAAY,MAAM,KAAK,IAAI;AACzC,QAAI,MAAO,QAAO;AAClB,QAAI,KAAK,cAAc,IAAI,SAAS,WAAW,WAAW,EAAG,QAAO,KAAK,UAAU,KAAK,IAAI;AAE5F,QAAI,IAAI,aAAa,mBAAmB;AACtC,UAAI,KAAK,IAAI,YAAY,MAAM,sBAAsB;AACnD,cAAM,IAAI,KAAK,MAAM,IAAI,KAAK,IAAI,MAAM,KAAK,EAAE;AAC/C,aAAK,MAAM,OAAO,KAAK,IAAI,MAAM,KAAK,EAAE;AACxC,cAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,KAAK,IAAI,eAAe,KAAK,EAAE,CAAC;AAC9G,cAAM,YAAY,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,MAAM,CAAC,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAChI,YAAI,CAAC,KAAK,EAAE,cAAc,aAAa,EAAE,gBAAgB,KAAK,IAAI,cAAc,EAAG,QAAO,MAAM,KAAK,EAAE,OAAO,iBAAiB,mBAAmB,gDAAgD,CAAC;AACnM,eAAO,MAAM,KAAK,KAAK,MAAM,EAAE,GAAG,CAAC;AAAA,MACrC;AACA,UAAI,KAAK,IAAI,YAAY,MAAM,iBAAiB;AAC9C,cAAMA,OAAM,KAAK,QAAQ,IAAI,KAAK,IAAI,eAAe,KAAK,EAAE;AAC5D,aAAK,QAAQ,OAAO,KAAK,IAAI,eAAe,KAAK,EAAE;AACnD,YAAI,CAACA,QAAO,CAAC,KAAK,MAAM,IAAIA,IAAG,GAAG,OAAQ,QAAO,MAAM,KAAK,EAAE,OAAO,iBAAiB,mBAAmB,gDAAgD,CAAC;AAC1J,eAAO,MAAM,KAAK,KAAK,MAAMA,IAAG,CAAC;AAAA,MACnC;AACA,aAAO,MAAM,KAAK,EAAE,OAAO,0BAA0B,mBAAmB,2DAA2D,CAAC;AAAA,IACtI;AACA,QAAI,IAAI,aAAa,oBAAoB;AACvC,WAAK,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,EAAE;AACzC,WAAK,QAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,EAAE;AAC3C,aAAO,MAAM,KAAK,CAAC,CAAC;AAAA,IACtB;AACA,UAAM,MAAM,KAAK,OAAO,IAAI,MAAM;AAClC,UAAM,OAAO,MAAM,KAAK,MAAM,IAAI,GAAG,IAAI;AACzC,QAAI,IAAI,aAAa,sBAAsB;AACzC,UAAI,CAAC,KAAM,QAAO,MAAM,KAAK,CAAC,CAAC;AAC/B,aAAO,MAAM,KAAK,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,MAAM,gBAAgB,KAAK,YAAY,CAAC,KAAK,WAAW,YAAY,KAAK,YAAY,OAAO,CAAC,QAAQ,GAAG,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,IAAI,CAAC,EAAG,CAAC;AAAA,IACtO;AACA,UAAM,SAAS,IAAI,SAAS,MAAM,8CAA8C;AAChF,QAAI,UAAU,MAAM,WAAW,QAAQ;AACrC,UAAI,CAAC,KAAM,QAAO,MAAM,KAAK,EAAE,QAAQ,CAAC,EAAE,MAAM,gBAAgB,SAAS,2BAA2B,CAAC,EAAE,CAAC;AACxG,YAAM,MAAM,mBAAmB,OAAO,CAAC,CAAC;AACxC,YAAM,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC;AACzC,WAAK,OAAO,KAAK,EAAE,cAAc,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAC3D,UAAI,EAAE,KAAK,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,EAAG,QAAO,MAAM,KAAK,EAAE,QAAQ,CAAC,EAAE,MAAM,0BAA0B,SAAS,yBAAyB,CAAC,EAAE,CAAC;AAErK,YAAM,UAAU,KAAK,aAAa,cAAc,KAAK,WAAW,aAAa,OAAO,KAAK,YAAY,MAAM,KAAK;AAChH,aAAO,MAAM,KAAK,EAAE,MAAM,UACtB,EAAE,SAAS,QAAQ,gDAAgD,MAAM,EAAE,QAAQ,QAAQ,MAAM,cAAc,MAAM,KAAK,UAAU,YAAY,QAAQ,WAAW,QAAQ,SAAS,WAAW,mCAAmC,EAAE,IACpO,EAAE,SAAS,QAAQ,oBAAoB,EAAE,CAAC;AAAA,IAChD;AACA,QAAI,IAAI,aAAa,2BAA2B;AAC9C,UAAI,CAAC,KAAM,QAAO,MAAM,KAAK,CAAC,CAAC;AAC/B,YAAM,SAAS,IAAI,aAAa,IAAI,QAAQ;AAC5C,YAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,WAAW,MAAM;AACzF,aAAO,MAAM,KAAK,EAAE,MAAM,aAAa,KAAK,OAAO,CAAC;AAAA,IACtD;AACA,WAAO,MAAM,KAAK,CAAC,CAAC;AAAA,EACtB;AAAA;AAAA,EAGQ,UAAU,KAAU,MAAyC;AACnE,UAAM,QAAQ,CAAC,QAAgB,SAC7B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EAAE,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,EAAE,CAAC;AAChG,UAAM,SAAS,CAAC,QAAgB,MAAc,YAAoB,MAAM,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,QAAQ,CAAC,EAAE,CAAC;AAC/G,QAAI,IAAI,QAAQ,MAAM,OAAO,EAAE,IAAI,cAAc,MAAM,KAAK,OAAQ,QAAO,OAAO,KAAK,oBAAoB,mBAAmB;AAC9H,UAAM,SAAS,MAAM,UAAU;AAC/B,QAAI,WAAW,UAAU,IAAI,aAAa,kBAAkB;AAC1D,UAAI,KAAK,SAAS,mBAAmB,MAAO,QAAO,OAAO,KAAK,qBAAqB,6DAA6D;AACjJ,YAAM,OAAO,KAAK,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AAClD,YAAM,OAAO,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK,KAAK;AACxE,YAAM,WAAW,KAAK,UAAU,SAAY,SAAY,KAAK,UAAU,IAAI,KAAK,KAAK;AACrF,UAAI,CAAC,QAAQ,CAAC,KAAK,UAAU,aAAa,UAAa,aAAa,KAAK,UAAU;AACjF,eAAO,OAAO,KAAK,uBAAuB,2BAA2B;AAAA,MACvE;AAEA,YAAM,OAAO,KAAK,SAAS,wBAAwB,OAAO,IAAI,QAAQ,MAAM,OAAO,EAAE,IAAI,mBAAmB,MAAM,MAAM;AACxH,aAAO,MAAM,KAAK,EAAE,MAAM,KAAK,MAAM,KAAK,KAAK,IAAI,EAAE,CAAC;AAAA,IACxD;AACA,QAAI,WAAW,UAAU,IAAI,aAAa,qBAAqB;AAC7D,UAAI,KAAK,SAAS,iBAAiB,MAAO,QAAO,OAAO,KAAK,oBAAoB,8CAA8C;AAC/H,UAAI,KAAK,SAAS,mBAAmB,MAAO,QAAO,OAAO,KAAK,qBAAqB,6DAA6D;AACjJ,YAAM,OAAO,KAAK,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AAClD,UAAI,CAAC,KAAK,SAAS,CAAC,KAAK,SAAU,QAAO,OAAO,KAAK,oBAAoB,iCAAiC;AAC3G,UAAI,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,KAAK,KAAK,EAAG,QAAO,OAAO,KAAK,eAAe,mCAAmC;AAEvI,YAAM,UAAU,KAAK,SAAS,kBAAkB;AAChD,YAAM,MAAM,KAAK,EAAE,KAAK,CAAC;AACzB,WAAK,MAAM,IAAI,KAAK,EAAE,KAAK,OAAO,KAAK,OAAO,MAAM,KAAK,gBAAgB,IAAI,QAAQ,CAAC,SAAS,YAAY,OAAO,UAAU,CAAC,QAAQ,CAAC;AACtI,WAAK,UAAU,IAAI,KAAK,OAAO,KAAK,QAAQ;AAC5C,aAAO,MAAM,KAAK,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,KAAK,OAAO,GAAI,UAAU,EAAE,uBAAuB,KAAK,IAAI,CAAC,EAAG,EAAE,CAAC;AAAA,IACtH;AACA,QAAI,WAAW,SAAS,IAAI,aAAa,oBAAqB,QAAO,MAAM,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;AACvG,QAAI,WAAW,UAAU,IAAI,aAAa,+BAA+B,IAAI,aAAa,4BAA6B,QAAO,MAAM,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;AACrJ,QAAI,WAAW,SAAS,IAAI,aAAa,0BAA2B,QAAO,MAAM,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,WAAW,MAAM,EAAE,CAAC;AACjI,WAAO,OAAO,KAAK,aAAa,WAAW;AAAA,EAC7C;AACF;AAQA,SAAS,YAAY,MAAkB,KAAU,MAAgD;AAC/F,QAAM,UAAU,MAAM,UAAU,OAAO,YAAY;AACnD,QAAM,QAAQ,GAAG,MAAM,IAAI,IAAI,QAAQ;AACvC,QAAM,SAAS,WAAW,UAAU,IAAI,SAAS,MAAM,iCAAiC,IAAI;AAC5F,QAAM,OAAO,WAAW,WAAW,IAAI,SAAS,MAAM,wBAAwB,IAAI;AAClF,QAAM,QAAQ,CAAC,yBAAyB,yBAAyB,yBAAyB,eAAe,EAAE,SAAS,KAAK;AACzH,MAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAM,QAAO;AACvC,QAAM,QAAQ,CAAC,QAAgB,SAC7B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EAAE,QAAQ,SAAS,EAAE,gBAAgB,mBAAmB,EAAE,CAAC;AAChG,QAAM,SAAS,CAAC,QAAgB,MAAc,YAAoB,MAAM,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,QAAQ,CAAC,EAAE,CAAC;AAC/G,QAAM,UAAU,IAAI,QAAQ,MAAM,OAAO;AACzC,MAAI,KAAK,eAAe,MAAM,QAAQ,IAAI,eAAe,MAAM,UAAU,KAAK,UAAU,GAAI,QAAO,OAAO,KAAK,gBAAgB,0BAA0B;AACzJ,MAAI,QAAQ,IAAI,cAAc,MAAM,KAAK,OAAQ,QAAO,OAAO,KAAK,oBAAoB,mBAAmB;AAC3G,MAAI,UAAU,wBAAyB,QAAO,MAAM,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;AAChF,MAAI,UAAU,yBAAyB;AACrC,UAAM,SAAS,KAAK,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AACpD,UAAM,OAAO,EAAE,GAAG,KAAK,SAAS;AAChC,eAAW,OAAO,eAAgB,KAAI,OAAO,GAAG,MAAM,UAAa,OAAO,GAAG,MAAM,KAAM,MAAK,GAAG,IAAI,OAAO,GAAG;AAC/G,QAAI,KAAK,mBAAmB,SAAS,KAAK,qBAAqB,QAAQ,KAAK,mBAAmB,MAAM;AACnG,aAAO,OAAO,KAAK,eAAe,0CAA0C;AAAA,IAC9E;AACA,SAAK,WAAW;AAChB,WAAO,MAAM,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;AAAA,EAC3C;AACA,QAAM,aAAa,MAAM,KAAK,SAAS,wBAAwB;AAC/D,MAAI,UAAU,wBAAyB,QAAO,MAAM,KAAK,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,UAAU,2BAA2B,WAAW,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC;AAC1J,MAAI,QAAQ;AACV,QAAI,mBAAmB,OAAO,CAAC,CAAC,MAAM,KAAK,SAAU,QAAO,OAAO,KAAK,oBAAoB,gBAAgB;AAC5G,UAAM,SAAS,KAAK,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AACpD,QAAI,OAAO,OAAO,8BAA8B,UAAW,MAAK,WAAW,EAAE,GAAG,KAAK,UAAU,qBAAqB,OAAO,0BAA0B;AACrJ,WAAO,MAAM,KAAK,EAAE,MAAM,EAAE,WAAW,KAAK,UAAU,2BAA2B,WAAW,EAAE,EAAE,CAAC;AAAA,EACnG;AACA,MAAI,UAAU,iBAAiB;AAC7B,UAAM,KAAK,IAAI,aAAa,IAAI,GAAG,KAAK,IAAI,YAAY;AACxD,UAAM,OAAO,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,YAAY,EAAE,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,OAAO,MAAM,EAAE,KAAK,EAAE;AAC/I,WAAO,MAAM,KAAK,EAAE,MAAM,aAAa,KAAK,OAAO,CAAC;AAAA,EACtD;AACA,QAAM,KAAK,mBAAmB,KAAM,CAAC,CAAC;AACtC,QAAM,OAAO,KAAK,MAAM,IAAI,EAAE;AAC9B,MAAI,CAAC,KAAM,QAAO,OAAO,KAAK,kBAAkB,cAAc;AAC9D,OAAK,MAAM,OAAO,EAAE;AACpB,OAAK,UAAU,OAAO,KAAK,KAAK;AAChC,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAC3C;AAMO,SAAS,gBAAgB,MAAkB,MAA+B;AAC/E,QAAM,aAAS,+BAAa,OAAO,KAAK,QAAQ;AAC9C,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,IAAK,QAAO,KAAK,KAAe;AAC1D,UAAM,UAAU,IAAI,QAAQ;AAC5B,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,OAAO,EAAG,KAAI,OAAO,MAAM,SAAU,SAAQ,IAAI,GAAG,CAAC;AAC7F,UAAM,SAAS,MAAM,KAAK,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,OAAO,GAAG,IAAI;AAAA,MACjE,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA,GAAI,OAAO,SAAS,IAAI,EAAE,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,EAAE,IAAI,CAAC;AAAA,IAC9E,CAAC;AACD,QAAI,UAAU,OAAO,QAAQ,OAAO,YAAY,OAAO,OAAO,CAAC;AAC/D,QAAI,IAAI,OAAO,KAAK,MAAM,OAAO,YAAY,CAAC,CAAC;AAAA,EACjD,CAAC;AACD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAO,KAAK,SAAS,MAAM;AAC3B,WAAO,OAAO,MAAM,aAAa,MAAM,QAAQ,MAAM,CAAC;AAAA,EACxD,CAAC;AACH;AAGO,IAAM,UAAN,MAAc;AAAA;AAAA,EAGnB,YAAqB,SAAS,oBAAqC,UAAuC,CAAC,GAAG;AAAzF;AAA8C;AAAA,EAA4C;AAAA,EAA1F;AAAA,EAA8C;AAAA,EAFnE,MAAM,oBAAI,IAAoB;AAAA,EAI9B,QAAQ,MAAc,OAAoB,CAAC,GAAY;AACrD,UAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,QAAI,KAAK,IAAI,OAAO,EAAG,SAAQ,IAAI,UAAU,CAAC,GAAG,KAAK,GAAG,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAElG,UAAM,UAAU,KAAK,UAAU,OAAO,YAAY;AAClD,QAAI,WAAW,SAAS,WAAW,UAAU,KAAK,QAAQ,kBAAkB,SAAS,CAAC,QAAQ,IAAI,kBAAkB,GAAG;AACrH,cAAQ,IAAI,oBAAoB,GAAG;AAAA,IACrC;AACA,WAAO,IAAI,QAAQ,KAAK,SAAS,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC;AAAA,EAC7D;AAAA;AAAA,EAGA,KAAK,QAAmC;AACtC,UAAM,UAAU,MAAM,QAAQ,MAAM,IAAI,SAAS,OAAO,QAAQ,aAAa;AAC7E,eAAW,KAAK,SAAS;AACvB,YAAM,CAAC,MAAM,GAAG,KAAK,IAAI,EAAE,MAAM,IAAI;AACrC,YAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,YAAM,OAAO,KAAK,MAAM,GAAG,CAAC;AAC5B,YAAM,QAAQ,KAAK,MAAM,IAAI,CAAC;AAC9B,UAAI,UAAU,MAAM,MAAM,SAAS,WAAW,EAAG,MAAK,IAAI,OAAO,IAAI;AAAA,UAChE,MAAK,IAAI,IAAI,MAAM,KAAK;AAAA,IAC/B;AAAA,EACF;AACF;","names":["sub"]}
|
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// src/testing.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
var ADMIN_SETTINGS = ["allow_signup", "allow_password", "allow_magic_link", "allow_passkeys", "confirm_email", "verify_grace_days"];
|
|
2
4
|
var FakeCauthy = class {
|
|
3
5
|
issuer = "https://id.test";
|
|
4
6
|
users = /* @__PURE__ */ new Map();
|
|
@@ -24,6 +26,10 @@ var FakeCauthy = class {
|
|
|
24
26
|
passwords = /* @__PURE__ */ new Map();
|
|
25
27
|
/** What GET /v1/auth/settings answers. */
|
|
26
28
|
settings = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false };
|
|
29
|
+
/** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */
|
|
30
|
+
adminToken = "";
|
|
31
|
+
/** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */
|
|
32
|
+
clientId = "acme-crm";
|
|
27
33
|
n = 0;
|
|
28
34
|
constructor() {
|
|
29
35
|
this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
|
|
@@ -36,12 +42,14 @@ var FakeCauthy = class {
|
|
|
36
42
|
this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
|
|
37
43
|
return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
|
|
38
44
|
}
|
|
39
|
-
|
|
45
|
+
/** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */
|
|
46
|
+
issue(sub, kind) {
|
|
40
47
|
const at = `at-${++this.n}`;
|
|
41
48
|
const rt = `rt-${++this.n}`;
|
|
42
49
|
this.access.set(at, sub);
|
|
43
50
|
this.refresh.set(rt, sub);
|
|
44
|
-
|
|
51
|
+
const persistent = this.persistent ?? kind;
|
|
52
|
+
return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...persistent === void 0 ? {} : { persistent } };
|
|
45
53
|
}
|
|
46
54
|
fetch = async (input, init) => {
|
|
47
55
|
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
@@ -50,6 +58,8 @@ var FakeCauthy = class {
|
|
|
50
58
|
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
51
59
|
const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
|
|
52
60
|
const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
|
|
61
|
+
const admin = adminAnswer(this, url, init);
|
|
62
|
+
if (admin) return admin;
|
|
53
63
|
if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
|
|
54
64
|
if (url.pathname === "/v1/oauth/token") {
|
|
55
65
|
if (form.get("grant_type") === "authorization_code") {
|
|
@@ -104,13 +114,27 @@ var FakeCauthy = class {
|
|
|
104
114
|
if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
105
115
|
const method = init?.method ?? "GET";
|
|
106
116
|
if (method === "POST" && url.pathname === "/v1/auth/login") {
|
|
117
|
+
if (this.settings.allow_password === false) return refuse(403, "password_disabled", "this workspace does not take passwords: sign in another way");
|
|
107
118
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
108
119
|
const user = [...this.users.values()].find((u) => u.email === body.email);
|
|
109
120
|
const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
|
|
110
121
|
if (!user || !user.active || password === void 0 || password !== body.password) {
|
|
111
122
|
return refuse(401, "invalid_credentials", "invalid email or password");
|
|
112
123
|
}
|
|
113
|
-
|
|
124
|
+
const kind = this.settings.persistent_sessions === true ? new Headers(init?.headers).get("x-cauthy-remember") === "1" : void 0;
|
|
125
|
+
return reply(200, { data: this.issue(user.sub, kind) });
|
|
126
|
+
}
|
|
127
|
+
if (method === "POST" && url.pathname === "/v1/auth/register") {
|
|
128
|
+
if (this.settings.allow_signup === false) return refuse(403, "signups_disabled", "this workspace is not accepting new accounts");
|
|
129
|
+
if (this.settings.allow_password === false) return refuse(403, "password_disabled", "this workspace does not take passwords: sign in another way");
|
|
130
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
131
|
+
if (!body.email || !body.password) return refuse(400, "validation_error", "email and password are required");
|
|
132
|
+
if ([...this.users.values()].some((u) => u.email === body.email)) return refuse(409, "email_taken", "an account with this email exists");
|
|
133
|
+
const confirm = this.settings.confirm_email === true;
|
|
134
|
+
const sub = `u-${++this.n}`;
|
|
135
|
+
this.users.set(sub, { sub, email: body.email, name: body.display_name ?? "", active: !confirm, restricted: false, verified: !confirm });
|
|
136
|
+
this.passwords.set(body.email, body.password);
|
|
137
|
+
return reply(201, { data: { user_id: sub, email: body.email, ...confirm ? { verification_required: true } : {} } });
|
|
114
138
|
}
|
|
115
139
|
if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
|
|
116
140
|
if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
|
|
@@ -118,6 +142,68 @@ var FakeCauthy = class {
|
|
|
118
142
|
return refuse(404, "not_found", "not found");
|
|
119
143
|
}
|
|
120
144
|
};
|
|
145
|
+
function adminAnswer(fake, url, init) {
|
|
146
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
147
|
+
const route = `${method} ${url.pathname}`;
|
|
148
|
+
const client = method === "PATCH" ? url.pathname.match(/^\/v1\/oauth\/clients\/([^/]+)$/) : null;
|
|
149
|
+
const user = method === "DELETE" ? url.pathname.match(/^\/v1\/users\/([^/]+)$/) : null;
|
|
150
|
+
const known = ["GET /v1/settings/auth", "PUT /v1/settings/auth", "GET /v1/oauth/clients", "GET /v1/users"].includes(route);
|
|
151
|
+
if (!known && !client && !user) return null;
|
|
152
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
153
|
+
const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
|
|
154
|
+
const headers = new Headers(init?.headers);
|
|
155
|
+
if (fake.adminToken === "" || headers.get("authorization") !== `Bearer ${fake.adminToken}`) return refuse(401, "unauthorized", "invalid or expired token");
|
|
156
|
+
if (headers.get("x-tenant-key") !== fake.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
157
|
+
if (route === "GET /v1/settings/auth") return reply(200, { data: fake.settings });
|
|
158
|
+
if (route === "PUT /v1/settings/auth") {
|
|
159
|
+
const change = JSON.parse(String(init?.body ?? "{}"));
|
|
160
|
+
const next = { ...fake.settings };
|
|
161
|
+
for (const key of ADMIN_SETTINGS) if (change[key] !== void 0 && change[key] !== null) next[key] = change[key];
|
|
162
|
+
if (next.allow_password === false && next.allow_magic_link !== true && next.allow_passkeys !== true) {
|
|
163
|
+
return refuse(409, "last_method", "the change would leave no way to sign in");
|
|
164
|
+
}
|
|
165
|
+
fake.settings = next;
|
|
166
|
+
return reply(200, { data: fake.settings });
|
|
167
|
+
}
|
|
168
|
+
const persistent = () => fake.settings.persistent_sessions === true;
|
|
169
|
+
if (route === "GET /v1/oauth/clients") return reply(200, { data: [{ client_id: fake.clientId, allow_persistent_sessions: persistent() }], total_count: 1 });
|
|
170
|
+
if (client) {
|
|
171
|
+
if (decodeURIComponent(client[1]) !== fake.clientId) return refuse(404, "client_not_found", "no such client");
|
|
172
|
+
const change = JSON.parse(String(init?.body ?? "{}"));
|
|
173
|
+
if (typeof change.allow_persistent_sessions === "boolean") fake.settings = { ...fake.settings, persistent_sessions: change.allow_persistent_sessions };
|
|
174
|
+
return reply(200, { data: { client_id: fake.clientId, allow_persistent_sessions: persistent() } });
|
|
175
|
+
}
|
|
176
|
+
if (route === "GET /v1/users") {
|
|
177
|
+
const q = (url.searchParams.get("q") ?? "").toLowerCase();
|
|
178
|
+
const data = [...fake.users.values()].filter((u) => u.email.toLowerCase().includes(q)).map((u) => ({ id: u.sub, email: u.email, name: u.name }));
|
|
179
|
+
return reply(200, { data, total_count: data.length });
|
|
180
|
+
}
|
|
181
|
+
const id = decodeURIComponent(user[1]);
|
|
182
|
+
const gone = fake.users.get(id);
|
|
183
|
+
if (!gone) return refuse(404, "user_not_found", "no such user");
|
|
184
|
+
fake.users.delete(id);
|
|
185
|
+
fake.passwords.delete(gone.email);
|
|
186
|
+
return new Response(null, { status: 204 });
|
|
187
|
+
}
|
|
188
|
+
function serveFakeCauthy(fake, port) {
|
|
189
|
+
const server = createServer(async (req, res) => {
|
|
190
|
+
const chunks = [];
|
|
191
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
192
|
+
const headers = new Headers();
|
|
193
|
+
for (const [k, v] of Object.entries(req.headers)) if (typeof v === "string") headers.set(k, v);
|
|
194
|
+
const answer = await fake.fetch(`${fake.issuer}${req.url ?? "/"}`, {
|
|
195
|
+
method: req.method,
|
|
196
|
+
headers,
|
|
197
|
+
...chunks.length > 0 ? { body: Buffer.concat(chunks).toString("utf8") } : {}
|
|
198
|
+
});
|
|
199
|
+
res.writeHead(answer.status, Object.fromEntries(answer.headers));
|
|
200
|
+
res.end(Buffer.from(await answer.arrayBuffer()));
|
|
201
|
+
});
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
server.once("error", reject);
|
|
204
|
+
server.listen(port, "127.0.0.1", () => resolve(server));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
121
207
|
var Browser = class {
|
|
122
208
|
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
123
209
|
constructor(origin = "https://app.test", options = {}) {
|
|
@@ -151,6 +237,7 @@ var Browser = class {
|
|
|
151
237
|
};
|
|
152
238
|
export {
|
|
153
239
|
Browser,
|
|
154
|
-
FakeCauthy
|
|
240
|
+
FakeCauthy,
|
|
241
|
+
serveFakeCauthy
|
|
155
242
|
};
|
|
156
243
|
//# sourceMappingURL=testing.mjs.map
|
package/dist/testing.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// A Cauthy that lives in memory, and a browser that keeps cookies: what the\n// tests sign in against. Exported as `@cauthyhq/node/testing`, for tests only:\n// it signs in anyone it was told about, and checks nothing a real Cauthy does.\n\nexport class FakeCauthy {\n readonly issuer = 'https://id.test'\n users = new Map<string, { sub: string; email: string; name: string; active: boolean; restricted: boolean; anonymous?: boolean; verified?: boolean }>()\n codes = new Map<string, { sub: string; challenge: string; redirectUri: string }>()\n refresh = new Map<string, string>() // refresh token → sub\n access = new Map<string, string>() // access token → sub\n orgs = new Map<string, Record<string, unknown>[]>() // sub → organizations\n revoked: string[] = []\n checks: { organization: string; sub: string; body: any }[] = []\n calls: string[] = []\n down = false\n expiresIn = 600\n /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */\n persistent: boolean | undefined = undefined\n /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */\n ownScreens = false\n /** The workspace those screens sign in to, as X-Tenant-Key names it. */\n tenant = 'acme'\n /** Each account's password, by email: only these accounts sign in with one. */\n passwords = new Map<string, string>()\n /** What GET /v1/auth/settings answers. */\n settings: Record<string, unknown> = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false }\n private n = 0\n\n constructor() {\n this.users.set('u1', { sub: 'u1', email: 'ana@acme.com', name: 'Ana', active: true, restricted: false })\n }\n\n /** What the hosted page does once the person has signed in: hands the browser a code. */\n authorize(location: string, sub = 'u1'): string {\n const q = new URL(location).searchParams\n if (q.get('response_type') !== 'code') throw new Error('Cauthy answers unsupported_response_type: response_type=code is required')\n const code = `code-${++this.n}`\n this.codes.set(code, { sub, challenge: q.get('code_challenge') ?? '', redirectUri: q.get('redirect_uri') ?? '' })\n return `${q.get('redirect_uri')}?code=${code}&state=${q.get('state')}`\n }\n\n private issue(sub: string) {\n const at = `at-${++this.n}`\n const rt = `rt-${++this.n}`\n this.access.set(at, sub)\n this.refresh.set(rt, sub)\n return { access_token: at, refresh_token: rt, token_type: 'Bearer', expires_in: this.expiresIn, scope: '', ...(this.persistent === undefined ? {} : { persistent: this.persistent }) }\n }\n\n fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)\n this.calls.push(`${init?.method ?? 'GET'} ${url.pathname}`)\n if (this.down) throw new TypeError('fetch failed')\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ''))\n const bearer = new Headers(init?.headers).get('authorization')?.replace('Bearer ', '') ?? ''\n if (this.ownScreens && url.pathname.startsWith('/v1/auth/')) return this.ownScreen(url, init)\n\n if (url.pathname === '/v1/oauth/token') {\n if (form.get('grant_type') === 'authorization_code') {\n const c = this.codes.get(form.get('code') ?? '')\n this.codes.delete(form.get('code') ?? '')\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(form.get('code_verifier') ?? ''))\n const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n 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' })\n return reply(200, this.issue(c.sub))\n }\n if (form.get('grant_type') === 'refresh_token') {\n const sub = this.refresh.get(form.get('refresh_token') ?? '')\n this.refresh.delete(form.get('refresh_token') ?? '') // used once\n if (!sub || !this.users.get(sub)?.active) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(sub))\n }\n return reply(400, { error: 'unsupported_grant_type', error_description: 'supported grant types: authorization_code, refresh_token' })\n }\n if (url.pathname === '/v1/oauth/revoke') {\n this.revoked.push(form.get('token') ?? '')\n this.refresh.delete(form.get('token') ?? '')\n return reply(200, {})\n }\n const sub = this.access.get(bearer)\n const user = sub ? this.users.get(sub) : undefined\n if (url.pathname === '/v1/oauth/userinfo') {\n if (!user) return reply(401, {})\n return reply(200, { sub: user.sub, tid: 't1', email: user.email, name: user.name, email_verified: user.verified ?? !user.anonymous, restricted: user.restricted, roles: ['member'], ...(user.anonymous ? { anonymous: true } : {}) })\n }\n const asking = url.pathname.match(/^\\/v1\\/oauth\\/organizations\\/([^/]+)\\/check$/)\n if (asking && init?.method === 'POST') {\n if (!user) return reply(401, { errors: [{ code: 'unauthorized', message: 'invalid or expired token' }] })\n const org = decodeURIComponent(asking[1])\n const body = JSON.parse(String(init.body))\n this.checks.push({ organization: org, sub: user.sub, body })\n if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: 'organization_not_found', message: 'organization not found' }] })\n // An accountant approves invoices up to a million.\n const allowed = body.resource === 'invoices' && body.action === 'approve' && Number(body.attributes?.amount) <= 1_000_000\n return reply(200, { data: allowed\n ? { allowed, reason: 'allowed by role accountant: invoices:approve', rule: { source: 'role', role: 'accountant', kind: '*', resource: 'invoices', action: 'approve', effect: 'allow', condition: 'resource.attrs.amount <= 1000000' } }\n : { allowed, reason: 'no rule allows it' } })\n }\n if (url.pathname === '/v1/oauth/organizations') {\n if (!user) return reply(401, {})\n const system = url.searchParams.get('system')\n const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system)\n return reply(200, { data, total_count: data.length })\n }\n return reply(404, {})\n }\n\n /** The part of /v1/auth/* a password sign-in and the sign-in form read. */\n private ownScreen(url: URL, init: RequestInit | undefined): Response {\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n if (new Headers(init?.headers).get('x-tenant-key') !== this.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n const method = init?.method ?? 'GET'\n if (method === 'POST' && url.pathname === '/v1/auth/login') {\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string }\n const user = [...this.users.values()].find((u) => u.email === body.email)\n const password = body.email === undefined ? undefined : this.passwords.get(body.email)\n if (!user || !user.active || password === undefined || password !== body.password) {\n return refuse(401, 'invalid_credentials', 'invalid email or password')\n }\n return reply(200, { data: this.issue(user.sub) })\n }\n if (method === 'GET' && url.pathname === '/v1/auth/settings') return reply(200, { data: this.settings })\n if (method === 'GET' && (url.pathname === '/v1/auth/social/providers' || url.pathname === '/v1/auth/sso/connections')) return reply(200, { data: [] })\n if (method === 'GET' && url.pathname === '/v1/auth/webauthn/place') return reply(200, { data: { here: false, on_cauthy: false } })\n return refuse(404, 'not_found', 'not found')\n }\n}\n\n/** A browser as far as cookies go: it keeps what it is told to and sends it back. */\nexport class Browser {\n jar = new Map<string, string>()\n /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */\n constructor(readonly origin = 'https://app.test', private readonly options: { requestHeader?: boolean } = {}) {}\n\n request(path: string, init: RequestInit = {}): Request {\n const headers = new Headers(init.headers)\n if (this.jar.size > 0) headers.set('cookie', [...this.jar].map(([k, v]) => `${k}=${v}`).join('; '))\n // What a protocol-1 client sends on every mutation.\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && this.options.requestHeader !== false && !headers.has('x-cauthy-request')) {\n headers.set('x-cauthy-request', '1')\n }\n return new Request(this.origin + path, { ...init, headers })\n }\n\n /** Keeps the cookies of a response, as a browser would. */\n take(source: Response | string[]): void {\n const cookies = Array.isArray(source) ? source : source.headers.getSetCookie()\n for (const c of cookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n const name = pair.slice(0, i)\n const value = pair.slice(i + 1)\n if (value === '' || attrs.includes('Max-Age=0')) this.jar.delete(name)\n else this.jar.set(name, value)\n }\n }\n}\n"],"mappings":"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","names":["sub"]}
|
|
1
|
+
{"version":3,"sources":["../src/testing.ts"],"sourcesContent":["// A Cauthy that lives in memory, and a browser that keeps cookies: what the\n// tests sign in against. Exported as `@cauthyhq/node/testing`, for tests only:\n// it signs in anyone it was told about, and checks nothing a real Cauthy does.\n\nimport { createServer, type Server } from 'node:http'\n\n/** The settings the fake's admin API changes (PUT /v1/settings/auth), as the dashboard's switches do. */\nconst ADMIN_SETTINGS = ['allow_signup', 'allow_password', 'allow_magic_link', 'allow_passkeys', 'confirm_email', 'verify_grace_days'] as const\n\nexport class FakeCauthy {\n readonly issuer = 'https://id.test'\n users = new Map<string, { sub: string; email: string; name: string; active: boolean; restricted: boolean; anonymous?: boolean; verified?: boolean }>()\n codes = new Map<string, { sub: string; challenge: string; redirectUri: string }>()\n refresh = new Map<string, string>() // refresh token → sub\n access = new Map<string, string>() // access token → sub\n orgs = new Map<string, Record<string, unknown>[]>() // sub → organizations\n revoked: string[] = []\n checks: { organization: string; sub: string; body: any }[] = []\n calls: string[] = []\n down = false\n expiresIn = 600\n /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */\n persistent: boolean | undefined = undefined\n /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */\n ownScreens = false\n /** The workspace those screens sign in to, as X-Tenant-Key names it. */\n tenant = 'acme'\n /** Each account's password, by email: only these accounts sign in with one. */\n passwords = new Map<string, string>()\n /** What GET /v1/auth/settings answers. */\n settings: Record<string, unknown> = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false }\n /** The bearer the admin API takes (settings, the application's client, accounts); '' refuses every admin call. */\n adminToken = ''\n /** The application's client, as the admin API lists it; its allow_persistent_sessions is settings.persistent_sessions. */\n clientId = 'acme-crm'\n private n = 0\n\n constructor() {\n this.users.set('u1', { sub: 'u1', email: 'ana@acme.com', name: 'Ana', active: true, restricted: false })\n }\n\n /** What the hosted page does once the person has signed in: hands the browser a code. */\n authorize(location: string, sub = 'u1'): string {\n const q = new URL(location).searchParams\n if (q.get('response_type') !== 'code') throw new Error('Cauthy answers unsupported_response_type: response_type=code is required')\n const code = `code-${++this.n}`\n this.codes.set(code, { sub, challenge: q.get('code_challenge') ?? '', redirectUri: q.get('redirect_uri') ?? '' })\n return `${q.get('redirect_uri')}?code=${code}&state=${q.get('state')}`\n }\n\n /** A token answer. `kind` is what a sign-in asked for (remember) when the client allows long sessions; `persistent` wins when set. */\n private issue(sub: string, kind?: boolean) {\n const at = `at-${++this.n}`\n const rt = `rt-${++this.n}`\n this.access.set(at, sub)\n this.refresh.set(rt, sub)\n const persistent = this.persistent ?? kind\n return { access_token: at, refresh_token: rt, token_type: 'Bearer', expires_in: this.expiresIn, scope: '', ...(persistent === undefined ? {} : { persistent }) }\n }\n\n fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)\n this.calls.push(`${init?.method ?? 'GET'} ${url.pathname}`)\n if (this.down) throw new TypeError('fetch failed')\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ''))\n const bearer = new Headers(init?.headers).get('authorization')?.replace('Bearer ', '') ?? ''\n const admin = adminAnswer(this, url, init)\n if (admin) return admin\n if (this.ownScreens && url.pathname.startsWith('/v1/auth/')) return this.ownScreen(url, init)\n\n if (url.pathname === '/v1/oauth/token') {\n if (form.get('grant_type') === 'authorization_code') {\n const c = this.codes.get(form.get('code') ?? '')\n this.codes.delete(form.get('code') ?? '')\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(form.get('code_verifier') ?? ''))\n const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n 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' })\n return reply(200, this.issue(c.sub))\n }\n if (form.get('grant_type') === 'refresh_token') {\n const sub = this.refresh.get(form.get('refresh_token') ?? '')\n this.refresh.delete(form.get('refresh_token') ?? '') // used once\n if (!sub || !this.users.get(sub)?.active) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(sub))\n }\n return reply(400, { error: 'unsupported_grant_type', error_description: 'supported grant types: authorization_code, refresh_token' })\n }\n if (url.pathname === '/v1/oauth/revoke') {\n this.revoked.push(form.get('token') ?? '')\n this.refresh.delete(form.get('token') ?? '')\n return reply(200, {})\n }\n const sub = this.access.get(bearer)\n const user = sub ? this.users.get(sub) : undefined\n if (url.pathname === '/v1/oauth/userinfo') {\n if (!user) return reply(401, {})\n return reply(200, { sub: user.sub, tid: 't1', email: user.email, name: user.name, email_verified: user.verified ?? !user.anonymous, restricted: user.restricted, roles: ['member'], ...(user.anonymous ? { anonymous: true } : {}) })\n }\n const asking = url.pathname.match(/^\\/v1\\/oauth\\/organizations\\/([^/]+)\\/check$/)\n if (asking && init?.method === 'POST') {\n if (!user) return reply(401, { errors: [{ code: 'unauthorized', message: 'invalid or expired token' }] })\n const org = decodeURIComponent(asking[1])\n const body = JSON.parse(String(init.body))\n this.checks.push({ organization: org, sub: user.sub, body })\n if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: 'organization_not_found', message: 'organization not found' }] })\n // An accountant approves invoices up to a million.\n const allowed = body.resource === 'invoices' && body.action === 'approve' && Number(body.attributes?.amount) <= 1_000_000\n return reply(200, { data: allowed\n ? { allowed, reason: 'allowed by role accountant: invoices:approve', rule: { source: 'role', role: 'accountant', kind: '*', resource: 'invoices', action: 'approve', effect: 'allow', condition: 'resource.attrs.amount <= 1000000' } }\n : { allowed, reason: 'no rule allows it' } })\n }\n if (url.pathname === '/v1/oauth/organizations') {\n if (!user) return reply(401, {})\n const system = url.searchParams.get('system')\n const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system)\n return reply(200, { data, total_count: data.length })\n }\n return reply(404, {})\n }\n\n /** The part of /v1/auth/* a password sign-in and the sign-in form read. */\n private ownScreen(url: URL, init: RequestInit | undefined): Response {\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n if (new Headers(init?.headers).get('x-tenant-key') !== this.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n const method = init?.method ?? 'GET'\n if (method === 'POST' && url.pathname === '/v1/auth/login') {\n if (this.settings.allow_password === false) return refuse(403, 'password_disabled', 'this workspace does not take passwords: sign in another way')\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string }\n const user = [...this.users.values()].find((u) => u.email === body.email)\n const password = body.email === undefined ? undefined : this.passwords.get(body.email)\n if (!user || !user.active || password === undefined || password !== body.password) {\n return refuse(401, 'invalid_credentials', 'invalid email or password')\n }\n // A long session only when the person asked and the client allows it.\n const kind = this.settings.persistent_sessions === true ? new Headers(init?.headers).get('x-cauthy-remember') === '1' : undefined\n return reply(200, { data: this.issue(user.sub, kind) })\n }\n if (method === 'POST' && url.pathname === '/v1/auth/register') {\n if (this.settings.allow_signup === false) return refuse(403, 'signups_disabled', 'this workspace is not accepting new accounts')\n if (this.settings.allow_password === false) return refuse(403, 'password_disabled', 'this workspace does not take passwords: sign in another way')\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string; display_name?: string }\n if (!body.email || !body.password) return refuse(400, 'validation_error', 'email and password are required')\n if ([...this.users.values()].some((u) => u.email === body.email)) return refuse(409, 'email_taken', 'an account with this email exists')\n // With confirm_email the account waits for its address (verify_grace_days 0 in the fake).\n const confirm = this.settings.confirm_email === true\n const sub = `u-${++this.n}`\n this.users.set(sub, { sub, email: body.email, name: body.display_name ?? '', active: !confirm, restricted: false, verified: !confirm })\n this.passwords.set(body.email, body.password)\n return reply(201, { data: { user_id: sub, email: body.email, ...(confirm ? { verification_required: true } : {}) } })\n }\n if (method === 'GET' && url.pathname === '/v1/auth/settings') return reply(200, { data: this.settings })\n if (method === 'GET' && (url.pathname === '/v1/auth/social/providers' || url.pathname === '/v1/auth/sso/connections')) return reply(200, { data: [] })\n if (method === 'GET' && url.pathname === '/v1/auth/webauthn/place') return reply(200, { data: { here: false, on_cauthy: false } })\n return refuse(404, 'not_found', 'not found')\n }\n}\n\n/**\n * The part of Cauthy's admin API the conformance runner's dashboard-switch\n * checks drive: GET and PUT /v1/settings/auth, GET /v1/oauth/clients,\n * PATCH /v1/oauth/clients/{id}, GET /v1/users and DELETE /v1/users/{id}.\n * null when the request is none of them.\n */\nfunction adminAnswer(fake: FakeCauthy, url: URL, init: RequestInit | undefined): Response | null {\n const method = (init?.method ?? 'GET').toUpperCase()\n const route = `${method} ${url.pathname}`\n const client = method === 'PATCH' ? url.pathname.match(/^\\/v1\\/oauth\\/clients\\/([^/]+)$/) : null\n const user = method === 'DELETE' ? url.pathname.match(/^\\/v1\\/users\\/([^/]+)$/) : null\n const known = ['GET /v1/settings/auth', 'PUT /v1/settings/auth', 'GET /v1/oauth/clients', 'GET /v1/users'].includes(route)\n if (!known && !client && !user) return null\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n const headers = new Headers(init?.headers)\n if (fake.adminToken === '' || headers.get('authorization') !== `Bearer ${fake.adminToken}`) return refuse(401, 'unauthorized', 'invalid or expired token')\n if (headers.get('x-tenant-key') !== fake.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n if (route === 'GET /v1/settings/auth') return reply(200, { data: fake.settings })\n if (route === 'PUT /v1/settings/auth') {\n const change = JSON.parse(String(init?.body ?? '{}')) as Record<string, unknown>\n const next = { ...fake.settings }\n for (const key of ADMIN_SETTINGS) if (change[key] !== undefined && change[key] !== null) next[key] = change[key]\n if (next.allow_password === false && next.allow_magic_link !== true && next.allow_passkeys !== true) {\n return refuse(409, 'last_method', 'the change would leave no way to sign in')\n }\n fake.settings = next\n return reply(200, { data: fake.settings })\n }\n const persistent = () => fake.settings.persistent_sessions === true\n if (route === 'GET /v1/oauth/clients') return reply(200, { data: [{ client_id: fake.clientId, allow_persistent_sessions: persistent() }], total_count: 1 })\n if (client) {\n if (decodeURIComponent(client[1]) !== fake.clientId) return refuse(404, 'client_not_found', 'no such client')\n const change = JSON.parse(String(init?.body ?? '{}')) as { allow_persistent_sessions?: unknown }\n if (typeof change.allow_persistent_sessions === 'boolean') fake.settings = { ...fake.settings, persistent_sessions: change.allow_persistent_sessions }\n return reply(200, { data: { client_id: fake.clientId, allow_persistent_sessions: persistent() } })\n }\n if (route === 'GET /v1/users') {\n const q = (url.searchParams.get('q') ?? '').toLowerCase()\n const data = [...fake.users.values()].filter((u) => u.email.toLowerCase().includes(q)).map((u) => ({ id: u.sub, email: u.email, name: u.name }))\n return reply(200, { data, total_count: data.length })\n }\n const id = decodeURIComponent(user![1])\n const gone = fake.users.get(id)\n if (!gone) return refuse(404, 'user_not_found', 'no such user')\n fake.users.delete(id)\n fake.passwords.delete(gone.email)\n return new Response(null, { status: 204 })\n}\n\n/**\n * Serves a FakeCauthy over HTTP on 127.0.0.1, for a process outside this one\n * (the conformance runner) to reach it, as `--cauthy http://127.0.0.1:<port>`.\n */\nexport function serveFakeCauthy(fake: FakeCauthy, port: number): Promise<Server> {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) if (typeof v === 'string') headers.set(k, v)\n const answer = await fake.fetch(`${fake.issuer}${req.url ?? '/'}`, {\n method: req.method,\n headers,\n ...(chunks.length > 0 ? { body: Buffer.concat(chunks).toString('utf8') } : {}),\n })\n res.writeHead(answer.status, Object.fromEntries(answer.headers))\n res.end(Buffer.from(await answer.arrayBuffer()))\n })\n return new Promise((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, '127.0.0.1', () => resolve(server))\n })\n}\n\n/** A browser as far as cookies go: it keeps what it is told to and sends it back. */\nexport class Browser {\n jar = new Map<string, string>()\n /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */\n constructor(readonly origin = 'https://app.test', private readonly options: { requestHeader?: boolean } = {}) {}\n\n request(path: string, init: RequestInit = {}): Request {\n const headers = new Headers(init.headers)\n if (this.jar.size > 0) headers.set('cookie', [...this.jar].map(([k, v]) => `${k}=${v}`).join('; '))\n // What a protocol-1 client sends on every mutation.\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && this.options.requestHeader !== false && !headers.has('x-cauthy-request')) {\n headers.set('x-cauthy-request', '1')\n }\n return new Request(this.origin + path, { ...init, headers })\n }\n\n /** Keeps the cookies of a response, as a browser would. */\n take(source: Response | string[]): void {\n const cookies = Array.isArray(source) ? source : source.headers.getSetCookie()\n for (const c of cookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n const name = pair.slice(0, i)\n const value = pair.slice(i + 1)\n if (value === '' || attrs.includes('Max-Age=0')) this.jar.delete(name)\n else this.jar.set(name, value)\n }\n }\n}\n"],"mappings":"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","names":["sub"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cauthyhq/node",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Cauthy for Node.js backends: sign-in, cookie sessions, organizations and Pact contract mode, behind the auth-bff protocol.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Vennet Developers",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"files": [
|
|
114
114
|
"dist",
|
|
115
115
|
"README.md",
|
|
116
|
+
"CHANGELOG.md",
|
|
116
117
|
"LICENSE",
|
|
117
118
|
"NOTICE"
|
|
118
119
|
],
|
|
@@ -175,7 +176,7 @@
|
|
|
175
176
|
"@types/react": "19.2.17",
|
|
176
177
|
"cauthy-auth": "0.42.0",
|
|
177
178
|
"express": "5.3.0",
|
|
178
|
-
"fastify": "5.8
|
|
179
|
+
"fastify": "5.12.8",
|
|
179
180
|
"hono": "4.13.13",
|
|
180
181
|
"next": "15.5.29",
|
|
181
182
|
"publint": "0.3.25",
|
|
@@ -184,6 +185,10 @@
|
|
|
184
185
|
"tsup": "8.5.1",
|
|
185
186
|
"tsx": "4.22.4",
|
|
186
187
|
"typescript": "5.9.3",
|
|
187
|
-
"vitest": "
|
|
188
|
+
"vitest": "4.1.12"
|
|
189
|
+
},
|
|
190
|
+
"overrides": {
|
|
191
|
+
"postcss": "8.5.32",
|
|
192
|
+
"esbuild": "0.28.2"
|
|
188
193
|
}
|
|
189
194
|
}
|