@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.
- package/CHANGELOG.md +43 -0
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +184 -1
- package/dist/cauthy-Bn-i7cKD.d.mts +1058 -0
- package/dist/cauthy-Wy-YDGrM.d.ts +1058 -0
- package/dist/express.d.mts +25 -0
- package/dist/express.d.ts +25 -0
- package/dist/express.js +103 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +77 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +38 -0
- package/dist/fastify.d.ts +38 -0
- package/dist/fastify.js +105 -0
- package/dist/fastify.js.map +1 -0
- package/dist/fastify.mjs +79 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +24 -0
- package/dist/hono.d.ts +24 -0
- package/dist/hono.js +51 -0
- package/dist/hono.js.map +1 -0
- package/dist/hono.mjs +25 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +3485 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3421 -0
- package/dist/index.mjs.map +1 -0
- package/dist/keys-D5k0omTb.d.mts +64 -0
- package/dist/keys-D5k0omTb.d.ts +64 -0
- package/dist/keys.d.mts +8 -0
- package/dist/keys.d.ts +8 -0
- package/dist/keys.js +157 -0
- package/dist/keys.js.map +1 -0
- package/dist/keys.mjs +129 -0
- package/dist/keys.mjs.map +1 -0
- package/dist/mcp.d.mts +151 -0
- package/dist/mcp.d.ts +151 -0
- package/dist/mcp.js +410 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcp.mjs +382 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/next.d.mts +24 -0
- package/dist/next.d.ts +24 -0
- package/dist/next.js +77 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +52 -0
- package/dist/next.mjs.map +1 -0
- package/dist/testing.d.mts +74 -0
- package/dist/testing.d.ts +74 -0
- package/dist/testing.js +270 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +243 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/theme.d.mts +183 -0
- package/dist/theme.d.ts +183 -0
- package/dist/theme.js +531 -0
- package/dist/theme.js.map +1 -0
- package/dist/theme.mjs +496 -0
- package/dist/theme.mjs.map +1 -0
- package/package.json +190 -4
package/dist/next.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/next.ts
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
function cookiesAfter(header, setCookies) {
|
|
5
|
+
const jar = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const part of (header ?? "").split(";")) {
|
|
7
|
+
const i = part.indexOf("=");
|
|
8
|
+
if (i > 0) jar.set(part.slice(0, i).trim(), part.slice(i + 1).trim());
|
|
9
|
+
}
|
|
10
|
+
for (const c of setCookies) {
|
|
11
|
+
const [pair, ...attrs] = c.split("; ");
|
|
12
|
+
const i = pair.indexOf("=");
|
|
13
|
+
if (i <= 0) continue;
|
|
14
|
+
if (pair.slice(i + 1) === "" || attrs.some((a) => a.toLowerCase() === "max-age=0")) jar.delete(pair.slice(0, i));
|
|
15
|
+
else jar.set(pair.slice(0, i), pair.slice(i + 1));
|
|
16
|
+
}
|
|
17
|
+
return [...jar].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
18
|
+
}
|
|
19
|
+
function cauthyNext(auth) {
|
|
20
|
+
const base = auth.options.basePath;
|
|
21
|
+
return {
|
|
22
|
+
GET: (request) => auth.handler(request),
|
|
23
|
+
POST: (request) => auth.handler(request),
|
|
24
|
+
async session() {
|
|
25
|
+
const h = await headers();
|
|
26
|
+
const origin = `${h.get("x-forwarded-proto") ?? "http"}://${h.get("x-forwarded-host") ?? h.get("host") ?? "localhost"}`;
|
|
27
|
+
return auth.getSession(new Request(`${origin}/`, { headers: new Headers(h) }));
|
|
28
|
+
},
|
|
29
|
+
middleware(options = {}) {
|
|
30
|
+
return async (request) => {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
if (url.pathname === base || url.pathname.startsWith(`${base}/`)) return NextResponse.next();
|
|
33
|
+
const resolved = await auth.resolve(request);
|
|
34
|
+
if (resolved.session === null && options.protect?.(url.pathname)) {
|
|
35
|
+
const to = NextResponse.redirect(new URL(auth.urls.signIn(url.pathname + url.search), url));
|
|
36
|
+
for (const c of resolved.cookies) to.headers.append("set-cookie", c);
|
|
37
|
+
return to;
|
|
38
|
+
}
|
|
39
|
+
if (resolved.cookies.length === 0) return NextResponse.next();
|
|
40
|
+
const forwarded = new Headers(request.headers);
|
|
41
|
+
forwarded.set("cookie", cookiesAfter(request.headers.get("cookie"), resolved.cookies));
|
|
42
|
+
const on = NextResponse.next({ request: { headers: forwarded } });
|
|
43
|
+
for (const c of resolved.cookies) on.headers.append("set-cookie", c);
|
|
44
|
+
return on;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
cauthyNext
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=next.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/next.ts"],"sourcesContent":["// Next.js (App Router): the routes, who is signed in on the server, and a\n// middleware that keeps the session renewed.\n//\n// // lib/auth.ts\n// export const auth = createCauthy()\n// export const cauthy = cauthyNext(auth)\n//\n// // app/api/auth/[...cauthy]/route.ts\n// export const { GET, POST } = cauthy\n//\n// // middleware.ts\n// export default cauthy.middleware({ protect: (path) => path.startsWith('/app') })\n//\n// // a server component, a route handler, a server action\n// const session = await cauthy.session()\n\nimport { headers } from 'next/headers'\nimport { NextResponse } from 'next/server'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\nexport interface CauthyMiddlewareOptions {\n /** The pages only a signed-in person may see: anybody else is sent to sign in, and back. */\n protect?: (pathname: string) => boolean\n}\n\nexport interface CauthyNext {\n GET(request: Request): Promise<Response>\n POST(request: Request): Promise<Response>\n /**\n * Who is signed in, or null: in a server component, a route handler or a\n * server action. A server component cannot set a cookie, so renewing an\n * expired session is the middleware's job: use `middleware()`.\n */\n session(): Promise<Session | null>\n /** For `middleware.ts`: renews the session when it has to be, and guards the pages named in `protect`. */\n middleware(options?: CauthyMiddlewareOptions): (request: Request) => Promise<Response>\n}\n\n/** The request's cookies with the ones a response sets applied, as the browser's next request would carry them. */\nfunction cookiesAfter(header: string | null, setCookies: string[]): string {\n const jar = new Map<string, string>()\n for (const part of (header ?? '').split(';')) {\n const i = part.indexOf('=')\n if (i > 0) jar.set(part.slice(0, i).trim(), part.slice(i + 1).trim())\n }\n for (const c of setCookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n if (i <= 0) continue\n if (pair.slice(i + 1) === '' || attrs.some((a) => a.toLowerCase() === 'max-age=0')) jar.delete(pair.slice(0, i))\n else jar.set(pair.slice(0, i), pair.slice(i + 1))\n }\n return [...jar].map(([k, v]) => `${k}=${v}`).join('; ')\n}\n\nexport function cauthyNext(auth: Cauthy): CauthyNext {\n const base = auth.options.basePath\n return {\n GET: (request) => auth.handler(request),\n POST: (request) => auth.handler(request),\n\n async session() {\n const h = await headers()\n const origin = `${h.get('x-forwarded-proto') ?? 'http'}://${h.get('x-forwarded-host') ?? h.get('host') ?? 'localhost'}`\n return auth.getSession(new Request(`${origin}/`, { headers: new Headers(h) }))\n },\n\n middleware(options = {}) {\n return async (request) => {\n const url = new URL(request.url)\n // The routes renew and clear the session themselves.\n if (url.pathname === base || url.pathname.startsWith(`${base}/`)) return NextResponse.next()\n\n const resolved = await auth.resolve(request)\n if (resolved.session === null && options.protect?.(url.pathname)) {\n const to = NextResponse.redirect(new URL(auth.urls.signIn(url.pathname + url.search), url))\n for (const c of resolved.cookies) to.headers.append('set-cookie', c)\n return to\n }\n if (resolved.cookies.length === 0) return NextResponse.next()\n\n // Renewed: the browser gets the new cookie, and so does what renders\n // this very request, which would otherwise still read the old one.\n const forwarded = new Headers(request.headers)\n forwarded.set('cookie', cookiesAfter(request.headers.get('cookie'), resolved.cookies))\n const on = NextResponse.next({ request: { headers: forwarded } })\n for (const c of resolved.cookies) on.headers.append('set-cookie', c)\n return on\n }\n },\n }\n}\n"],"mappings":";AAgBA,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAuB7B,SAAS,aAAa,QAAuB,YAA8B;AACzE,QAAM,MAAM,oBAAI,IAAoB;AACpC,aAAW,SAAS,UAAU,IAAI,MAAM,GAAG,GAAG;AAC5C,UAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,QAAI,IAAI,EAAG,KAAI,IAAI,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,MAAM,IAAI,CAAC,EAAE,KAAK,CAAC;AAAA,EACtE;AACA,aAAW,KAAK,YAAY;AAC1B,UAAM,CAAC,MAAM,GAAG,KAAK,IAAI,EAAE,MAAM,IAAI;AACrC,UAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,QAAI,KAAK,EAAG;AACZ,QAAI,KAAK,MAAM,IAAI,CAAC,MAAM,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,WAAW,EAAG,KAAI,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC;AAAA,QAC1G,KAAI,IAAI,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAAA,EAClD;AACA,SAAO,CAAC,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,IAAI;AACxD;AAEO,SAAS,WAAW,MAA0B;AACnD,QAAM,OAAO,KAAK,QAAQ;AAC1B,SAAO;AAAA,IACL,KAAK,CAAC,YAAY,KAAK,QAAQ,OAAO;AAAA,IACtC,MAAM,CAAC,YAAY,KAAK,QAAQ,OAAO;AAAA,IAEvC,MAAM,UAAU;AACd,YAAM,IAAI,MAAM,QAAQ;AACxB,YAAM,SAAS,GAAG,EAAE,IAAI,mBAAmB,KAAK,MAAM,MAAM,EAAE,IAAI,kBAAkB,KAAK,EAAE,IAAI,MAAM,KAAK,WAAW;AACrH,aAAO,KAAK,WAAW,IAAI,QAAQ,GAAG,MAAM,KAAK,EAAE,SAAS,IAAI,QAAQ,CAAC,EAAE,CAAC,CAAC;AAAA,IAC/E;AAAA,IAEA,WAAW,UAAU,CAAC,GAAG;AACvB,aAAO,OAAO,YAAY;AACxB,cAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAE/B,YAAI,IAAI,aAAa,QAAQ,IAAI,SAAS,WAAW,GAAG,IAAI,GAAG,EAAG,QAAO,aAAa,KAAK;AAE3F,cAAM,WAAW,MAAM,KAAK,QAAQ,OAAO;AAC3C,YAAI,SAAS,YAAY,QAAQ,QAAQ,UAAU,IAAI,QAAQ,GAAG;AAChE,gBAAM,KAAK,aAAa,SAAS,IAAI,IAAI,KAAK,KAAK,OAAO,IAAI,WAAW,IAAI,MAAM,GAAG,GAAG,CAAC;AAC1F,qBAAW,KAAK,SAAS,QAAS,IAAG,QAAQ,OAAO,cAAc,CAAC;AACnE,iBAAO;AAAA,QACT;AACA,YAAI,SAAS,QAAQ,WAAW,EAAG,QAAO,aAAa,KAAK;AAI5D,cAAM,YAAY,IAAI,QAAQ,QAAQ,OAAO;AAC7C,kBAAU,IAAI,UAAU,aAAa,QAAQ,QAAQ,IAAI,QAAQ,GAAG,SAAS,OAAO,CAAC;AACrF,cAAM,KAAK,aAAa,KAAK,EAAE,SAAS,EAAE,SAAS,UAAU,EAAE,CAAC;AAChE,mBAAW,KAAK,SAAS,QAAS,IAAG,QAAQ,OAAO,cAAc,CAAC;AACnE,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
|
|
3
|
+
declare class FakeCauthy {
|
|
4
|
+
readonly issuer = "https://id.test";
|
|
5
|
+
users: Map<string, {
|
|
6
|
+
sub: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string;
|
|
9
|
+
active: boolean;
|
|
10
|
+
restricted: boolean;
|
|
11
|
+
anonymous?: boolean;
|
|
12
|
+
verified?: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
codes: Map<string, {
|
|
15
|
+
sub: string;
|
|
16
|
+
challenge: string;
|
|
17
|
+
redirectUri: string;
|
|
18
|
+
}>;
|
|
19
|
+
refresh: Map<string, string>;
|
|
20
|
+
access: Map<string, string>;
|
|
21
|
+
orgs: Map<string, Record<string, unknown>[]>;
|
|
22
|
+
revoked: string[];
|
|
23
|
+
checks: {
|
|
24
|
+
organization: string;
|
|
25
|
+
sub: string;
|
|
26
|
+
body: any;
|
|
27
|
+
}[];
|
|
28
|
+
calls: string[];
|
|
29
|
+
down: boolean;
|
|
30
|
+
expiresIn: number;
|
|
31
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
32
|
+
persistent: boolean | undefined;
|
|
33
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
34
|
+
ownScreens: boolean;
|
|
35
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
36
|
+
tenant: string;
|
|
37
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
38
|
+
passwords: Map<string, string>;
|
|
39
|
+
/** What GET /v1/auth/settings answers. */
|
|
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;
|
|
45
|
+
private n;
|
|
46
|
+
constructor();
|
|
47
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
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. */
|
|
50
|
+
private issue;
|
|
51
|
+
fetch: typeof fetch;
|
|
52
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
53
|
+
private ownScreen;
|
|
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>;
|
|
60
|
+
/** A browser as far as cookies go: it keeps what it is told to and sends it back. */
|
|
61
|
+
declare class Browser {
|
|
62
|
+
readonly origin: string;
|
|
63
|
+
private readonly options;
|
|
64
|
+
jar: Map<string, string>;
|
|
65
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
66
|
+
constructor(origin?: string, options?: {
|
|
67
|
+
requestHeader?: boolean;
|
|
68
|
+
});
|
|
69
|
+
request(path: string, init?: RequestInit): Request;
|
|
70
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
71
|
+
take(source: Response | string[]): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Browser, FakeCauthy, serveFakeCauthy };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
|
|
3
|
+
declare class FakeCauthy {
|
|
4
|
+
readonly issuer = "https://id.test";
|
|
5
|
+
users: Map<string, {
|
|
6
|
+
sub: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string;
|
|
9
|
+
active: boolean;
|
|
10
|
+
restricted: boolean;
|
|
11
|
+
anonymous?: boolean;
|
|
12
|
+
verified?: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
codes: Map<string, {
|
|
15
|
+
sub: string;
|
|
16
|
+
challenge: string;
|
|
17
|
+
redirectUri: string;
|
|
18
|
+
}>;
|
|
19
|
+
refresh: Map<string, string>;
|
|
20
|
+
access: Map<string, string>;
|
|
21
|
+
orgs: Map<string, Record<string, unknown>[]>;
|
|
22
|
+
revoked: string[];
|
|
23
|
+
checks: {
|
|
24
|
+
organization: string;
|
|
25
|
+
sub: string;
|
|
26
|
+
body: any;
|
|
27
|
+
}[];
|
|
28
|
+
calls: string[];
|
|
29
|
+
down: boolean;
|
|
30
|
+
expiresIn: number;
|
|
31
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
32
|
+
persistent: boolean | undefined;
|
|
33
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
34
|
+
ownScreens: boolean;
|
|
35
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
36
|
+
tenant: string;
|
|
37
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
38
|
+
passwords: Map<string, string>;
|
|
39
|
+
/** What GET /v1/auth/settings answers. */
|
|
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;
|
|
45
|
+
private n;
|
|
46
|
+
constructor();
|
|
47
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
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. */
|
|
50
|
+
private issue;
|
|
51
|
+
fetch: typeof fetch;
|
|
52
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
53
|
+
private ownScreen;
|
|
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>;
|
|
60
|
+
/** A browser as far as cookies go: it keeps what it is told to and sends it back. */
|
|
61
|
+
declare class Browser {
|
|
62
|
+
readonly origin: string;
|
|
63
|
+
private readonly options;
|
|
64
|
+
jar: Map<string, string>;
|
|
65
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
66
|
+
constructor(origin?: string, options?: {
|
|
67
|
+
requestHeader?: boolean;
|
|
68
|
+
});
|
|
69
|
+
request(path: string, init?: RequestInit): Request;
|
|
70
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
71
|
+
take(source: Response | string[]): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Browser, FakeCauthy, serveFakeCauthy };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
Browser: () => Browser,
|
|
24
|
+
FakeCauthy: () => FakeCauthy,
|
|
25
|
+
serveFakeCauthy: () => serveFakeCauthy
|
|
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"];
|
|
30
|
+
var FakeCauthy = class {
|
|
31
|
+
issuer = "https://id.test";
|
|
32
|
+
users = /* @__PURE__ */ new Map();
|
|
33
|
+
codes = /* @__PURE__ */ new Map();
|
|
34
|
+
refresh = /* @__PURE__ */ new Map();
|
|
35
|
+
// refresh token → sub
|
|
36
|
+
access = /* @__PURE__ */ new Map();
|
|
37
|
+
// access token → sub
|
|
38
|
+
orgs = /* @__PURE__ */ new Map();
|
|
39
|
+
// sub → organizations
|
|
40
|
+
revoked = [];
|
|
41
|
+
checks = [];
|
|
42
|
+
calls = [];
|
|
43
|
+
down = false;
|
|
44
|
+
expiresIn = 600;
|
|
45
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
46
|
+
persistent = void 0;
|
|
47
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
48
|
+
ownScreens = false;
|
|
49
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
50
|
+
tenant = "acme";
|
|
51
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
52
|
+
passwords = /* @__PURE__ */ new Map();
|
|
53
|
+
/** What GET /v1/auth/settings answers. */
|
|
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";
|
|
59
|
+
n = 0;
|
|
60
|
+
constructor() {
|
|
61
|
+
this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
|
|
62
|
+
}
|
|
63
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
64
|
+
authorize(location, sub = "u1") {
|
|
65
|
+
const q = new URL(location).searchParams;
|
|
66
|
+
if (q.get("response_type") !== "code") throw new Error("Cauthy answers unsupported_response_type: response_type=code is required");
|
|
67
|
+
const code = `code-${++this.n}`;
|
|
68
|
+
this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
|
|
69
|
+
return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
|
|
70
|
+
}
|
|
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) {
|
|
73
|
+
const at = `at-${++this.n}`;
|
|
74
|
+
const rt = `rt-${++this.n}`;
|
|
75
|
+
this.access.set(at, sub);
|
|
76
|
+
this.refresh.set(rt, sub);
|
|
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 } };
|
|
79
|
+
}
|
|
80
|
+
fetch = async (input, init) => {
|
|
81
|
+
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
82
|
+
this.calls.push(`${init?.method ?? "GET"} ${url.pathname}`);
|
|
83
|
+
if (this.down) throw new TypeError("fetch failed");
|
|
84
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
85
|
+
const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
|
|
86
|
+
const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
|
|
87
|
+
const admin = adminAnswer(this, url, init);
|
|
88
|
+
if (admin) return admin;
|
|
89
|
+
if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
|
|
90
|
+
if (url.pathname === "/v1/oauth/token") {
|
|
91
|
+
if (form.get("grant_type") === "authorization_code") {
|
|
92
|
+
const c = this.codes.get(form.get("code") ?? "");
|
|
93
|
+
this.codes.delete(form.get("code") ?? "");
|
|
94
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(form.get("code_verifier") ?? ""));
|
|
95
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
96
|
+
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" });
|
|
97
|
+
return reply(200, this.issue(c.sub));
|
|
98
|
+
}
|
|
99
|
+
if (form.get("grant_type") === "refresh_token") {
|
|
100
|
+
const sub2 = this.refresh.get(form.get("refresh_token") ?? "");
|
|
101
|
+
this.refresh.delete(form.get("refresh_token") ?? "");
|
|
102
|
+
if (!sub2 || !this.users.get(sub2)?.active) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
|
|
103
|
+
return reply(200, this.issue(sub2));
|
|
104
|
+
}
|
|
105
|
+
return reply(400, { error: "unsupported_grant_type", error_description: "supported grant types: authorization_code, refresh_token" });
|
|
106
|
+
}
|
|
107
|
+
if (url.pathname === "/v1/oauth/revoke") {
|
|
108
|
+
this.revoked.push(form.get("token") ?? "");
|
|
109
|
+
this.refresh.delete(form.get("token") ?? "");
|
|
110
|
+
return reply(200, {});
|
|
111
|
+
}
|
|
112
|
+
const sub = this.access.get(bearer);
|
|
113
|
+
const user = sub ? this.users.get(sub) : void 0;
|
|
114
|
+
if (url.pathname === "/v1/oauth/userinfo") {
|
|
115
|
+
if (!user) return reply(401, {});
|
|
116
|
+
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 } : {} });
|
|
117
|
+
}
|
|
118
|
+
const asking = url.pathname.match(/^\/v1\/oauth\/organizations\/([^/]+)\/check$/);
|
|
119
|
+
if (asking && init?.method === "POST") {
|
|
120
|
+
if (!user) return reply(401, { errors: [{ code: "unauthorized", message: "invalid or expired token" }] });
|
|
121
|
+
const org = decodeURIComponent(asking[1]);
|
|
122
|
+
const body = JSON.parse(String(init.body));
|
|
123
|
+
this.checks.push({ organization: org, sub: user.sub, body });
|
|
124
|
+
if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: "organization_not_found", message: "organization not found" }] });
|
|
125
|
+
const allowed = body.resource === "invoices" && body.action === "approve" && Number(body.attributes?.amount) <= 1e6;
|
|
126
|
+
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" } });
|
|
127
|
+
}
|
|
128
|
+
if (url.pathname === "/v1/oauth/organizations") {
|
|
129
|
+
if (!user) return reply(401, {});
|
|
130
|
+
const system = url.searchParams.get("system");
|
|
131
|
+
const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system);
|
|
132
|
+
return reply(200, { data, total_count: data.length });
|
|
133
|
+
}
|
|
134
|
+
return reply(404, {});
|
|
135
|
+
};
|
|
136
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
137
|
+
ownScreen(url, init) {
|
|
138
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
139
|
+
const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
|
|
140
|
+
if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
141
|
+
const method = init?.method ?? "GET";
|
|
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");
|
|
144
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
145
|
+
const user = [...this.users.values()].find((u) => u.email === body.email);
|
|
146
|
+
const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
|
|
147
|
+
if (!user || !user.active || password === void 0 || password !== body.password) {
|
|
148
|
+
return refuse(401, "invalid_credentials", "invalid email or password");
|
|
149
|
+
}
|
|
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 } : {} } });
|
|
164
|
+
}
|
|
165
|
+
if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
|
|
166
|
+
if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
|
|
167
|
+
if (method === "GET" && url.pathname === "/v1/auth/webauthn/place") return reply(200, { data: { here: false, on_cauthy: false } });
|
|
168
|
+
return refuse(404, "not_found", "not found");
|
|
169
|
+
}
|
|
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
|
+
}
|
|
233
|
+
var Browser = class {
|
|
234
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
235
|
+
constructor(origin = "https://app.test", options = {}) {
|
|
236
|
+
this.origin = origin;
|
|
237
|
+
this.options = options;
|
|
238
|
+
}
|
|
239
|
+
origin;
|
|
240
|
+
options;
|
|
241
|
+
jar = /* @__PURE__ */ new Map();
|
|
242
|
+
request(path, init = {}) {
|
|
243
|
+
const headers = new Headers(init.headers);
|
|
244
|
+
if (this.jar.size > 0) headers.set("cookie", [...this.jar].map(([k, v]) => `${k}=${v}`).join("; "));
|
|
245
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
246
|
+
if (method !== "GET" && method !== "HEAD" && this.options.requestHeader !== false && !headers.has("x-cauthy-request")) {
|
|
247
|
+
headers.set("x-cauthy-request", "1");
|
|
248
|
+
}
|
|
249
|
+
return new Request(this.origin + path, { ...init, headers });
|
|
250
|
+
}
|
|
251
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
252
|
+
take(source) {
|
|
253
|
+
const cookies = Array.isArray(source) ? source : source.headers.getSetCookie();
|
|
254
|
+
for (const c of cookies) {
|
|
255
|
+
const [pair, ...attrs] = c.split("; ");
|
|
256
|
+
const i = pair.indexOf("=");
|
|
257
|
+
const name = pair.slice(0, i);
|
|
258
|
+
const value = pair.slice(i + 1);
|
|
259
|
+
if (value === "" || attrs.includes("Max-Age=0")) this.jar.delete(name);
|
|
260
|
+
else this.jar.set(name, value);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
265
|
+
0 && (module.exports = {
|
|
266
|
+
Browser,
|
|
267
|
+
FakeCauthy,
|
|
268
|
+
serveFakeCauthy
|
|
269
|
+
});
|
|
270
|
+
//# sourceMappingURL=testing.js.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"]}
|