@cauthyhq/node 0.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +8 -0
  4. package/README.md +184 -1
  5. package/dist/cauthy-Bn-i7cKD.d.mts +1058 -0
  6. package/dist/cauthy-Wy-YDGrM.d.ts +1058 -0
  7. package/dist/express.d.mts +25 -0
  8. package/dist/express.d.ts +25 -0
  9. package/dist/express.js +103 -0
  10. package/dist/express.js.map +1 -0
  11. package/dist/express.mjs +77 -0
  12. package/dist/express.mjs.map +1 -0
  13. package/dist/fastify.d.mts +38 -0
  14. package/dist/fastify.d.ts +38 -0
  15. package/dist/fastify.js +105 -0
  16. package/dist/fastify.js.map +1 -0
  17. package/dist/fastify.mjs +79 -0
  18. package/dist/fastify.mjs.map +1 -0
  19. package/dist/hono.d.mts +24 -0
  20. package/dist/hono.d.ts +24 -0
  21. package/dist/hono.js +51 -0
  22. package/dist/hono.js.map +1 -0
  23. package/dist/hono.mjs +25 -0
  24. package/dist/hono.mjs.map +1 -0
  25. package/dist/index.d.mts +138 -0
  26. package/dist/index.d.ts +138 -0
  27. package/dist/index.js +3485 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/index.mjs +3421 -0
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/keys-D5k0omTb.d.mts +64 -0
  32. package/dist/keys-D5k0omTb.d.ts +64 -0
  33. package/dist/keys.d.mts +8 -0
  34. package/dist/keys.d.ts +8 -0
  35. package/dist/keys.js +157 -0
  36. package/dist/keys.js.map +1 -0
  37. package/dist/keys.mjs +129 -0
  38. package/dist/keys.mjs.map +1 -0
  39. package/dist/mcp.d.mts +151 -0
  40. package/dist/mcp.d.ts +151 -0
  41. package/dist/mcp.js +410 -0
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/mcp.mjs +382 -0
  44. package/dist/mcp.mjs.map +1 -0
  45. package/dist/next.d.mts +24 -0
  46. package/dist/next.d.ts +24 -0
  47. package/dist/next.js +77 -0
  48. package/dist/next.js.map +1 -0
  49. package/dist/next.mjs +52 -0
  50. package/dist/next.mjs.map +1 -0
  51. package/dist/testing.d.mts +74 -0
  52. package/dist/testing.d.ts +74 -0
  53. package/dist/testing.js +270 -0
  54. package/dist/testing.js.map +1 -0
  55. package/dist/testing.mjs +243 -0
  56. package/dist/testing.mjs.map +1 -0
  57. package/dist/theme.d.mts +183 -0
  58. package/dist/theme.d.ts +183 -0
  59. package/dist/theme.js +531 -0
  60. package/dist/theme.js.map +1 -0
  61. package/dist/theme.mjs +496 -0
  62. package/dist/theme.mjs.map +1 -0
  63. package/package.json +190 -4
@@ -0,0 +1,243 @@
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"];
4
+ var FakeCauthy = class {
5
+ issuer = "https://id.test";
6
+ users = /* @__PURE__ */ new Map();
7
+ codes = /* @__PURE__ */ new Map();
8
+ refresh = /* @__PURE__ */ new Map();
9
+ // refresh token → sub
10
+ access = /* @__PURE__ */ new Map();
11
+ // access token → sub
12
+ orgs = /* @__PURE__ */ new Map();
13
+ // sub → organizations
14
+ revoked = [];
15
+ checks = [];
16
+ calls = [];
17
+ down = false;
18
+ expiresIn = 600;
19
+ /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
20
+ persistent = void 0;
21
+ /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
22
+ ownScreens = false;
23
+ /** The workspace those screens sign in to, as X-Tenant-Key names it. */
24
+ tenant = "acme";
25
+ /** Each account's password, by email: only these accounts sign in with one. */
26
+ passwords = /* @__PURE__ */ new Map();
27
+ /** What GET /v1/auth/settings answers. */
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";
33
+ n = 0;
34
+ constructor() {
35
+ this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
36
+ }
37
+ /** What the hosted page does once the person has signed in: hands the browser a code. */
38
+ authorize(location, sub = "u1") {
39
+ const q = new URL(location).searchParams;
40
+ if (q.get("response_type") !== "code") throw new Error("Cauthy answers unsupported_response_type: response_type=code is required");
41
+ const code = `code-${++this.n}`;
42
+ this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
43
+ return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
44
+ }
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) {
47
+ const at = `at-${++this.n}`;
48
+ const rt = `rt-${++this.n}`;
49
+ this.access.set(at, sub);
50
+ this.refresh.set(rt, sub);
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 } };
53
+ }
54
+ fetch = async (input, init) => {
55
+ const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
56
+ this.calls.push(`${init?.method ?? "GET"} ${url.pathname}`);
57
+ if (this.down) throw new TypeError("fetch failed");
58
+ const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
59
+ const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
60
+ const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
61
+ const admin = adminAnswer(this, url, init);
62
+ if (admin) return admin;
63
+ if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
64
+ if (url.pathname === "/v1/oauth/token") {
65
+ if (form.get("grant_type") === "authorization_code") {
66
+ const c = this.codes.get(form.get("code") ?? "");
67
+ this.codes.delete(form.get("code") ?? "");
68
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(form.get("code_verifier") ?? ""));
69
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
70
+ 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" });
71
+ return reply(200, this.issue(c.sub));
72
+ }
73
+ if (form.get("grant_type") === "refresh_token") {
74
+ const sub2 = this.refresh.get(form.get("refresh_token") ?? "");
75
+ this.refresh.delete(form.get("refresh_token") ?? "");
76
+ if (!sub2 || !this.users.get(sub2)?.active) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
77
+ return reply(200, this.issue(sub2));
78
+ }
79
+ return reply(400, { error: "unsupported_grant_type", error_description: "supported grant types: authorization_code, refresh_token" });
80
+ }
81
+ if (url.pathname === "/v1/oauth/revoke") {
82
+ this.revoked.push(form.get("token") ?? "");
83
+ this.refresh.delete(form.get("token") ?? "");
84
+ return reply(200, {});
85
+ }
86
+ const sub = this.access.get(bearer);
87
+ const user = sub ? this.users.get(sub) : void 0;
88
+ if (url.pathname === "/v1/oauth/userinfo") {
89
+ if (!user) return reply(401, {});
90
+ 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 } : {} });
91
+ }
92
+ const asking = url.pathname.match(/^\/v1\/oauth\/organizations\/([^/]+)\/check$/);
93
+ if (asking && init?.method === "POST") {
94
+ if (!user) return reply(401, { errors: [{ code: "unauthorized", message: "invalid or expired token" }] });
95
+ const org = decodeURIComponent(asking[1]);
96
+ const body = JSON.parse(String(init.body));
97
+ this.checks.push({ organization: org, sub: user.sub, body });
98
+ if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: "organization_not_found", message: "organization not found" }] });
99
+ const allowed = body.resource === "invoices" && body.action === "approve" && Number(body.attributes?.amount) <= 1e6;
100
+ return reply(200, { data: allowed ? { 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" } } : { allowed, reason: "no rule allows it" } });
101
+ }
102
+ if (url.pathname === "/v1/oauth/organizations") {
103
+ if (!user) return reply(401, {});
104
+ const system = url.searchParams.get("system");
105
+ const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system);
106
+ return reply(200, { data, total_count: data.length });
107
+ }
108
+ return reply(404, {});
109
+ };
110
+ /** The part of /v1/auth/* a password sign-in and the sign-in form read. */
111
+ ownScreen(url, init) {
112
+ const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
113
+ const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
114
+ if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
115
+ const method = init?.method ?? "GET";
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");
118
+ const body = JSON.parse(String(init?.body ?? "{}"));
119
+ const user = [...this.users.values()].find((u) => u.email === body.email);
120
+ const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
121
+ if (!user || !user.active || password === void 0 || password !== body.password) {
122
+ return refuse(401, "invalid_credentials", "invalid email or password");
123
+ }
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 } : {} } });
138
+ }
139
+ if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
140
+ if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
141
+ if (method === "GET" && url.pathname === "/v1/auth/webauthn/place") return reply(200, { data: { here: false, on_cauthy: false } });
142
+ return refuse(404, "not_found", "not found");
143
+ }
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
+ }
207
+ var Browser = class {
208
+ /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
209
+ constructor(origin = "https://app.test", options = {}) {
210
+ this.origin = origin;
211
+ this.options = options;
212
+ }
213
+ origin;
214
+ options;
215
+ jar = /* @__PURE__ */ new Map();
216
+ request(path, init = {}) {
217
+ const headers = new Headers(init.headers);
218
+ if (this.jar.size > 0) headers.set("cookie", [...this.jar].map(([k, v]) => `${k}=${v}`).join("; "));
219
+ const method = (init.method ?? "GET").toUpperCase();
220
+ if (method !== "GET" && method !== "HEAD" && this.options.requestHeader !== false && !headers.has("x-cauthy-request")) {
221
+ headers.set("x-cauthy-request", "1");
222
+ }
223
+ return new Request(this.origin + path, { ...init, headers });
224
+ }
225
+ /** Keeps the cookies of a response, as a browser would. */
226
+ take(source) {
227
+ const cookies = Array.isArray(source) ? source : source.headers.getSetCookie();
228
+ for (const c of cookies) {
229
+ const [pair, ...attrs] = c.split("; ");
230
+ const i = pair.indexOf("=");
231
+ const name = pair.slice(0, i);
232
+ const value = pair.slice(i + 1);
233
+ if (value === "" || attrs.includes("Max-Age=0")) this.jar.delete(name);
234
+ else this.jar.set(name, value);
235
+ }
236
+ }
237
+ };
238
+ export {
239
+ Browser,
240
+ FakeCauthy,
241
+ serveFakeCauthy
242
+ };
243
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +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\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"]}
@@ -0,0 +1,183 @@
1
+ /** How an application looks on Cauthy's hosted pages. Every value is optional. */
2
+ /** One font file the application serves: a family in one weight (or a range) and one style. */
3
+ interface ThemeFont {
4
+ family: string;
5
+ /** An https address of a `.woff2` file. It must answer with `Access-Control-Allow-Origin` for Cauthy's origin. */
6
+ url: string;
7
+ /** `400`, or `"100 900"` for a variable font. Default: 400. */
8
+ weight?: number | string;
9
+ style?: 'normal' | 'italic';
10
+ }
11
+ /** What an application says on its sign-in page, in one language. */
12
+ interface ThemeTexts {
13
+ /** Replaces "Sign in". */
14
+ heading?: string;
15
+ /** Replaces "to continue to …". */
16
+ subheading?: string;
17
+ /** The messages of the split layout's panel, shown one at a time (up to 5). */
18
+ panel?: string[];
19
+ /** Links at the foot of the split layout: privacy, terms (up to 4). */
20
+ links?: {
21
+ label: string;
22
+ url: string;
23
+ }[];
24
+ }
25
+ interface CauthyTheme {
26
+ /** The page. Colours are `#rrggbb` or `rgba(r, g, b, a)`. */
27
+ background?: string;
28
+ /** The card, and inputs. */
29
+ surface?: string;
30
+ /** Headings and what is typed. */
31
+ text?: string;
32
+ /** Paragraphs, labels, small print. */
33
+ textMuted?: string;
34
+ border?: string;
35
+ /** The fields and the secondary buttons, when they are not the card's colours. */
36
+ inputBackground?: string;
37
+ inputBorder?: string;
38
+ /** The main button, and what is written on it. */
39
+ primary?: string;
40
+ primaryText?: string;
41
+ /** Links, and the focus ring. */
42
+ accent?: string;
43
+ error?: string;
44
+ errorBackground?: string;
45
+ success?: string;
46
+ successBackground?: string;
47
+ /** Corners of cards and inputs, in pixels (0–40). */
48
+ radius?: number;
49
+ /** Corners of buttons, in pixels (0–40). */
50
+ buttonRadius?: number;
51
+ /** A CSS font-family list: the application's typeface first, a system font after it. */
52
+ fontFamily?: string;
53
+ /** The files of the typefaces `fontFamily` names, for people who do not have them installed. */
54
+ fonts?: ThemeFont[];
55
+ /** An image shown where "Cauthy" is, over https. */
56
+ logoUrl?: string;
57
+ /**
58
+ * How the sign-in pages are laid out: `card` (the default, one card in the
59
+ * middle) or `split` (the form on one side, a panel that presents the
60
+ * application on the other).
61
+ */
62
+ layout?: 'card' | 'split';
63
+ /** The colours the split layout's backdrop blends, one to four. */
64
+ panelColors?: string[];
65
+ /** The colour of what is written on the panel. */
66
+ panelText?: string;
67
+ /** An https image shown over the panel's messages. */
68
+ panelIconUrl?: string;
69
+ /** The application's own words on the sign-in page, by language (`en`, `es`). */
70
+ texts?: Record<string, ThemeTexts>;
71
+ /**
72
+ * The application's Stackmyth tokens (`--sm-*`), from `tokensFromStackmyth`.
73
+ * Cauthy's pages are drawn with Stackmyth's components, so these make its
74
+ * buttons and fields the application's own. They are laid over the named values.
75
+ */
76
+ tokens?: Record<string, string>;
77
+ }
78
+ interface StackmythThemeOptions {
79
+ /**
80
+ * The named theme to read, when the stylesheet keeps several under
81
+ * `[data-theme=…]` (`"acme"`). Without it only what applies to every
82
+ * page (`:root`) is read.
83
+ */
84
+ theme?: string;
85
+ /** Which of the application's two modes to take. Default: `light`. */
86
+ mode?: 'light' | 'dark';
87
+ logoUrl?: string;
88
+ }
89
+ interface ImportedTheme {
90
+ theme: CauthyTheme;
91
+ /** Tokens that were there and could not be carried over, and why. */
92
+ warnings: string[];
93
+ }
94
+ interface StackmythTokens {
95
+ /** Every `--sm-*` token the stylesheets set for the page, ready for `theme.tokens`. */
96
+ tokens: Record<string, string>;
97
+ /** Tokens that were there and Cauthy would not take, and why. */
98
+ warnings: string[];
99
+ }
100
+ /**
101
+ * Reads an application's Stackmyth tokens out of its stylesheets: every
102
+ * `--sm-*` custom property its theme sets, in the order the sheets are given
103
+ * (a later one wins). Its own names — a brand palette the tokens point at —
104
+ * are written in, so what comes out stands on Stackmyth's names alone.
105
+ *
106
+ * Cauthy's sign-in pages are drawn with Stackmyth's components: these tokens
107
+ * are what makes them look like the application.
108
+ */
109
+ declare function tokensFromStackmyth(css: string | string[], options?: StackmythThemeOptions): StackmythTokens;
110
+ /** A colour as Cauthy takes it — `#rrggbb` or `rgba(…)` — or null when it is none it can read. */
111
+ declare function toColor(value: string): string | null;
112
+ /**
113
+ * Reads an application's Stackmyth theme out of its stylesheets.
114
+ *
115
+ * Give it the CSS that declares the tokens, in the order the application
116
+ * loads it — Stackmyth's own (`@stackmyth/core/core.vars.css`, or a theme
117
+ * file) and then the application's brand file that re-points them. Tokens
118
+ * that point at the application's own variables are followed. The
119
+ * application's dark mode is left out unless asked for.
120
+ */
121
+ declare function themeFromStackmyth(css: string | string[], options?: StackmythThemeOptions): ImportedTheme;
122
+ /** The values of a theme a stylesheet can say. */
123
+ type ThemeField = 'background' | 'surface' | 'text' | 'textMuted' | 'border' | 'inputBackground' | 'inputBorder' | 'primary' | 'primaryText' | 'accent' | 'error' | 'errorBackground' | 'success' | 'successBackground' | 'radius' | 'buttonRadius' | 'fontFamily';
124
+ /** How a stylesheet names its look: Stackmyth's tokens, shadcn/ui's (and Tailwind's), or the application's own. */
125
+ type CssFormat = 'stackmyth' | 'shadcn' | 'custom';
126
+ interface CssThemeOptions {
127
+ /** Which names to read. Default: `auto`, told from the stylesheet itself. */
128
+ format?: CssFormat | 'auto';
129
+ /** Which of the application's two modes to take. Default: `light`. */
130
+ mode?: 'light' | 'dark';
131
+ /** The named theme to read, when the stylesheet keeps several under `[data-theme=…]`. */
132
+ theme?: string;
133
+ /** For `custom`: the variable each value is read from, `{ primary: '--brand' }`. */
134
+ map?: Partial<Record<ThemeField, string>>;
135
+ }
136
+ interface CssImportedTheme extends ImportedTheme {
137
+ /** The names that were read. */
138
+ format: CssFormat;
139
+ }
140
+ /** The custom properties a stylesheet sets for the page, by name: what a person picks from to map their own. */
141
+ declare function cssVariables(css: string | string[], options?: Pick<CssThemeOptions, 'mode' | 'theme'>): string[];
142
+ /**
143
+ * Reads an application's look out of its stylesheets, whatever names it uses:
144
+ * Stackmyth's tokens, shadcn/ui's (`--primary`, `--radius`, on Tailwind 3 or
145
+ * 4), or the application's own variables given in `map`. What comes out is a
146
+ * theme of named values, never CSS.
147
+ */
148
+ declare function themeFromCss(css: string | string[], options?: CssThemeOptions): CssImportedTheme;
149
+ /**
150
+ * A theme as the CSS variables of `@cauthyhq/react/styles.css` (stage 5) (`--cauthy-*`), for
151
+ * the forms an application shows on its own pages. The page's own background,
152
+ * the layout and the words are Cauthy's pages' matter, and left out.
153
+ */
154
+ declare function themeVariables(theme: CauthyTheme): Record<string, string>;
155
+ /** A theme as Cauthy's API names its values. */
156
+ declare function toWire(theme: CauthyTheme): Record<string, unknown>;
157
+ /** A theme as Cauthy answers it, in this package's names. */
158
+ declare function fromWire(wire: Record<string, unknown>): CauthyTheme;
159
+ interface PushThemeOptions {
160
+ /** Cauthy's address. Default: `CAUTHY_ISSUER`. */
161
+ issuer?: string;
162
+ /** The application's client id. Default: `CAUTHY_CLIENT_ID`. */
163
+ clientId?: string;
164
+ /** The workspace's slug. Default: `CAUTHY_TENANT`. */
165
+ tenant?: string;
166
+ /**
167
+ * A personal access token with `oauth_clients:manage`. Default:
168
+ * `CAUTHY_TOKEN`. It belongs in the deploy pipeline, never in the application.
169
+ */
170
+ token?: string;
171
+ theme: CauthyTheme;
172
+ fetch?: typeof fetch;
173
+ /** How long the call may take, in milliseconds, before it fails with a `TimeoutError`. Default: 5000. */
174
+ timeoutMs?: number;
175
+ }
176
+ /**
177
+ * Gives the application its look on Cauthy's pages. It replaces the theme: a
178
+ * value left out goes back to Cauthy's own. Run it when the brand changes —
179
+ * from a script or the deploy — not on every request.
180
+ */
181
+ declare function pushTheme(o: PushThemeOptions): Promise<void>;
182
+
183
+ export { type CauthyTheme, type CssFormat, type CssImportedTheme, type CssThemeOptions, type ImportedTheme, type PushThemeOptions, type StackmythThemeOptions, type StackmythTokens, type ThemeField, type ThemeFont, type ThemeTexts, cssVariables, fromWire, pushTheme, themeFromCss, themeFromStackmyth, themeVariables, toColor, toWire, tokensFromStackmyth };