@cauthyhq/node 0.0.0 → 1.0.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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +180 -1
- package/dist/cauthy-B6Sm3T8M.d.ts +1035 -0
- package/dist/cauthy-ClP5orP4.d.mts +1035 -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 +3474 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3410 -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 +62 -0
- package/dist/testing.d.ts +62 -0
- package/dist/testing.js +182 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +156 -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 +185 -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,62 @@
|
|
|
1
|
+
declare class FakeCauthy {
|
|
2
|
+
readonly issuer = "https://id.test";
|
|
3
|
+
users: Map<string, {
|
|
4
|
+
sub: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
active: boolean;
|
|
8
|
+
restricted: boolean;
|
|
9
|
+
anonymous?: boolean;
|
|
10
|
+
verified?: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
codes: Map<string, {
|
|
13
|
+
sub: string;
|
|
14
|
+
challenge: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
}>;
|
|
17
|
+
refresh: Map<string, string>;
|
|
18
|
+
access: Map<string, string>;
|
|
19
|
+
orgs: Map<string, Record<string, unknown>[]>;
|
|
20
|
+
revoked: string[];
|
|
21
|
+
checks: {
|
|
22
|
+
organization: string;
|
|
23
|
+
sub: string;
|
|
24
|
+
body: any;
|
|
25
|
+
}[];
|
|
26
|
+
calls: string[];
|
|
27
|
+
down: boolean;
|
|
28
|
+
expiresIn: number;
|
|
29
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
30
|
+
persistent: boolean | undefined;
|
|
31
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
32
|
+
ownScreens: boolean;
|
|
33
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
34
|
+
tenant: string;
|
|
35
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
36
|
+
passwords: Map<string, string>;
|
|
37
|
+
/** What GET /v1/auth/settings answers. */
|
|
38
|
+
settings: Record<string, unknown>;
|
|
39
|
+
private n;
|
|
40
|
+
constructor();
|
|
41
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
42
|
+
authorize(location: string, sub?: string): string;
|
|
43
|
+
private issue;
|
|
44
|
+
fetch: typeof fetch;
|
|
45
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
46
|
+
private ownScreen;
|
|
47
|
+
}
|
|
48
|
+
/** A browser as far as cookies go: it keeps what it is told to and sends it back. */
|
|
49
|
+
declare class Browser {
|
|
50
|
+
readonly origin: string;
|
|
51
|
+
private readonly options;
|
|
52
|
+
jar: Map<string, string>;
|
|
53
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
54
|
+
constructor(origin?: string, options?: {
|
|
55
|
+
requestHeader?: boolean;
|
|
56
|
+
});
|
|
57
|
+
request(path: string, init?: RequestInit): Request;
|
|
58
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
59
|
+
take(source: Response | string[]): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Browser, FakeCauthy };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
declare class FakeCauthy {
|
|
2
|
+
readonly issuer = "https://id.test";
|
|
3
|
+
users: Map<string, {
|
|
4
|
+
sub: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
active: boolean;
|
|
8
|
+
restricted: boolean;
|
|
9
|
+
anonymous?: boolean;
|
|
10
|
+
verified?: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
codes: Map<string, {
|
|
13
|
+
sub: string;
|
|
14
|
+
challenge: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
}>;
|
|
17
|
+
refresh: Map<string, string>;
|
|
18
|
+
access: Map<string, string>;
|
|
19
|
+
orgs: Map<string, Record<string, unknown>[]>;
|
|
20
|
+
revoked: string[];
|
|
21
|
+
checks: {
|
|
22
|
+
organization: string;
|
|
23
|
+
sub: string;
|
|
24
|
+
body: any;
|
|
25
|
+
}[];
|
|
26
|
+
calls: string[];
|
|
27
|
+
down: boolean;
|
|
28
|
+
expiresIn: number;
|
|
29
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
30
|
+
persistent: boolean | undefined;
|
|
31
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
32
|
+
ownScreens: boolean;
|
|
33
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
34
|
+
tenant: string;
|
|
35
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
36
|
+
passwords: Map<string, string>;
|
|
37
|
+
/** What GET /v1/auth/settings answers. */
|
|
38
|
+
settings: Record<string, unknown>;
|
|
39
|
+
private n;
|
|
40
|
+
constructor();
|
|
41
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
42
|
+
authorize(location: string, sub?: string): string;
|
|
43
|
+
private issue;
|
|
44
|
+
fetch: typeof fetch;
|
|
45
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
46
|
+
private ownScreen;
|
|
47
|
+
}
|
|
48
|
+
/** A browser as far as cookies go: it keeps what it is told to and sends it back. */
|
|
49
|
+
declare class Browser {
|
|
50
|
+
readonly origin: string;
|
|
51
|
+
private readonly options;
|
|
52
|
+
jar: Map<string, string>;
|
|
53
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
54
|
+
constructor(origin?: string, options?: {
|
|
55
|
+
requestHeader?: boolean;
|
|
56
|
+
});
|
|
57
|
+
request(path: string, init?: RequestInit): Request;
|
|
58
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
59
|
+
take(source: Response | string[]): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Browser, FakeCauthy };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(testing_exports);
|
|
27
|
+
var FakeCauthy = class {
|
|
28
|
+
issuer = "https://id.test";
|
|
29
|
+
users = /* @__PURE__ */ new Map();
|
|
30
|
+
codes = /* @__PURE__ */ new Map();
|
|
31
|
+
refresh = /* @__PURE__ */ new Map();
|
|
32
|
+
// refresh token → sub
|
|
33
|
+
access = /* @__PURE__ */ new Map();
|
|
34
|
+
// access token → sub
|
|
35
|
+
orgs = /* @__PURE__ */ new Map();
|
|
36
|
+
// sub → organizations
|
|
37
|
+
revoked = [];
|
|
38
|
+
checks = [];
|
|
39
|
+
calls = [];
|
|
40
|
+
down = false;
|
|
41
|
+
expiresIn = 600;
|
|
42
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
43
|
+
persistent = void 0;
|
|
44
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
45
|
+
ownScreens = false;
|
|
46
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
47
|
+
tenant = "acme";
|
|
48
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
49
|
+
passwords = /* @__PURE__ */ new Map();
|
|
50
|
+
/** What GET /v1/auth/settings answers. */
|
|
51
|
+
settings = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false };
|
|
52
|
+
n = 0;
|
|
53
|
+
constructor() {
|
|
54
|
+
this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
|
|
55
|
+
}
|
|
56
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
57
|
+
authorize(location, sub = "u1") {
|
|
58
|
+
const q = new URL(location).searchParams;
|
|
59
|
+
if (q.get("response_type") !== "code") throw new Error("Cauthy answers unsupported_response_type: response_type=code is required");
|
|
60
|
+
const code = `code-${++this.n}`;
|
|
61
|
+
this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
|
|
62
|
+
return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
|
|
63
|
+
}
|
|
64
|
+
issue(sub) {
|
|
65
|
+
const at = `at-${++this.n}`;
|
|
66
|
+
const rt = `rt-${++this.n}`;
|
|
67
|
+
this.access.set(at, sub);
|
|
68
|
+
this.refresh.set(rt, sub);
|
|
69
|
+
return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...this.persistent === void 0 ? {} : { persistent: this.persistent } };
|
|
70
|
+
}
|
|
71
|
+
fetch = async (input, init) => {
|
|
72
|
+
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
73
|
+
this.calls.push(`${init?.method ?? "GET"} ${url.pathname}`);
|
|
74
|
+
if (this.down) throw new TypeError("fetch failed");
|
|
75
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
76
|
+
const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
|
|
77
|
+
const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
|
|
78
|
+
if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
|
|
79
|
+
if (url.pathname === "/v1/oauth/token") {
|
|
80
|
+
if (form.get("grant_type") === "authorization_code") {
|
|
81
|
+
const c = this.codes.get(form.get("code") ?? "");
|
|
82
|
+
this.codes.delete(form.get("code") ?? "");
|
|
83
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(form.get("code_verifier") ?? ""));
|
|
84
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
85
|
+
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" });
|
|
86
|
+
return reply(200, this.issue(c.sub));
|
|
87
|
+
}
|
|
88
|
+
if (form.get("grant_type") === "refresh_token") {
|
|
89
|
+
const sub2 = this.refresh.get(form.get("refresh_token") ?? "");
|
|
90
|
+
this.refresh.delete(form.get("refresh_token") ?? "");
|
|
91
|
+
if (!sub2 || !this.users.get(sub2)?.active) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
|
|
92
|
+
return reply(200, this.issue(sub2));
|
|
93
|
+
}
|
|
94
|
+
return reply(400, { error: "unsupported_grant_type", error_description: "supported grant types: authorization_code, refresh_token" });
|
|
95
|
+
}
|
|
96
|
+
if (url.pathname === "/v1/oauth/revoke") {
|
|
97
|
+
this.revoked.push(form.get("token") ?? "");
|
|
98
|
+
this.refresh.delete(form.get("token") ?? "");
|
|
99
|
+
return reply(200, {});
|
|
100
|
+
}
|
|
101
|
+
const sub = this.access.get(bearer);
|
|
102
|
+
const user = sub ? this.users.get(sub) : void 0;
|
|
103
|
+
if (url.pathname === "/v1/oauth/userinfo") {
|
|
104
|
+
if (!user) return reply(401, {});
|
|
105
|
+
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 } : {} });
|
|
106
|
+
}
|
|
107
|
+
const asking = url.pathname.match(/^\/v1\/oauth\/organizations\/([^/]+)\/check$/);
|
|
108
|
+
if (asking && init?.method === "POST") {
|
|
109
|
+
if (!user) return reply(401, { errors: [{ code: "unauthorized", message: "invalid or expired token" }] });
|
|
110
|
+
const org = decodeURIComponent(asking[1]);
|
|
111
|
+
const body = JSON.parse(String(init.body));
|
|
112
|
+
this.checks.push({ organization: org, sub: user.sub, body });
|
|
113
|
+
if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: "organization_not_found", message: "organization not found" }] });
|
|
114
|
+
const allowed = body.resource === "invoices" && body.action === "approve" && Number(body.attributes?.amount) <= 1e6;
|
|
115
|
+
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" } });
|
|
116
|
+
}
|
|
117
|
+
if (url.pathname === "/v1/oauth/organizations") {
|
|
118
|
+
if (!user) return reply(401, {});
|
|
119
|
+
const system = url.searchParams.get("system");
|
|
120
|
+
const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system);
|
|
121
|
+
return reply(200, { data, total_count: data.length });
|
|
122
|
+
}
|
|
123
|
+
return reply(404, {});
|
|
124
|
+
};
|
|
125
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
126
|
+
ownScreen(url, init) {
|
|
127
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
128
|
+
const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
|
|
129
|
+
if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
130
|
+
const method = init?.method ?? "GET";
|
|
131
|
+
if (method === "POST" && url.pathname === "/v1/auth/login") {
|
|
132
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
133
|
+
const user = [...this.users.values()].find((u) => u.email === body.email);
|
|
134
|
+
const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
|
|
135
|
+
if (!user || !user.active || password === void 0 || password !== body.password) {
|
|
136
|
+
return refuse(401, "invalid_credentials", "invalid email or password");
|
|
137
|
+
}
|
|
138
|
+
return reply(200, { data: this.issue(user.sub) });
|
|
139
|
+
}
|
|
140
|
+
if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
|
|
141
|
+
if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
|
|
142
|
+
if (method === "GET" && url.pathname === "/v1/auth/webauthn/place") return reply(200, { data: { here: false, on_cauthy: false } });
|
|
143
|
+
return refuse(404, "not_found", "not found");
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var Browser = class {
|
|
147
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
148
|
+
constructor(origin = "https://app.test", options = {}) {
|
|
149
|
+
this.origin = origin;
|
|
150
|
+
this.options = options;
|
|
151
|
+
}
|
|
152
|
+
origin;
|
|
153
|
+
options;
|
|
154
|
+
jar = /* @__PURE__ */ new Map();
|
|
155
|
+
request(path, init = {}) {
|
|
156
|
+
const headers = new Headers(init.headers);
|
|
157
|
+
if (this.jar.size > 0) headers.set("cookie", [...this.jar].map(([k, v]) => `${k}=${v}`).join("; "));
|
|
158
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
159
|
+
if (method !== "GET" && method !== "HEAD" && this.options.requestHeader !== false && !headers.has("x-cauthy-request")) {
|
|
160
|
+
headers.set("x-cauthy-request", "1");
|
|
161
|
+
}
|
|
162
|
+
return new Request(this.origin + path, { ...init, headers });
|
|
163
|
+
}
|
|
164
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
165
|
+
take(source) {
|
|
166
|
+
const cookies = Array.isArray(source) ? source : source.headers.getSetCookie();
|
|
167
|
+
for (const c of cookies) {
|
|
168
|
+
const [pair, ...attrs] = c.split("; ");
|
|
169
|
+
const i = pair.indexOf("=");
|
|
170
|
+
const name = pair.slice(0, i);
|
|
171
|
+
const value = pair.slice(i + 1);
|
|
172
|
+
if (value === "" || attrs.includes("Max-Age=0")) this.jar.delete(name);
|
|
173
|
+
else this.jar.set(name, value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
178
|
+
0 && (module.exports = {
|
|
179
|
+
Browser,
|
|
180
|
+
FakeCauthy
|
|
181
|
+
});
|
|
182
|
+
//# 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\nexport class FakeCauthy {\n readonly issuer = 'https://id.test'\n users = new Map<string, { sub: string; email: string; name: string; active: boolean; restricted: boolean; anonymous?: boolean; verified?: boolean }>()\n codes = new Map<string, { sub: string; challenge: string; redirectUri: string }>()\n refresh = new Map<string, string>() // refresh token → sub\n access = new Map<string, string>() // access token → sub\n orgs = new Map<string, Record<string, unknown>[]>() // sub → organizations\n revoked: string[] = []\n checks: { organization: string; sub: string; body: any }[] = []\n calls: string[] = []\n down = false\n expiresIn = 600\n /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */\n persistent: boolean | undefined = undefined\n /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */\n ownScreens = false\n /** The workspace those screens sign in to, as X-Tenant-Key names it. */\n tenant = 'acme'\n /** Each account's password, by email: only these accounts sign in with one. */\n passwords = new Map<string, string>()\n /** What GET /v1/auth/settings answers. */\n settings: Record<string, unknown> = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false }\n private n = 0\n\n constructor() {\n this.users.set('u1', { sub: 'u1', email: 'ana@acme.com', name: 'Ana', active: true, restricted: false })\n }\n\n /** What the hosted page does once the person has signed in: hands the browser a code. */\n authorize(location: string, sub = 'u1'): string {\n const q = new URL(location).searchParams\n if (q.get('response_type') !== 'code') throw new Error('Cauthy answers unsupported_response_type: response_type=code is required')\n const code = `code-${++this.n}`\n this.codes.set(code, { sub, challenge: q.get('code_challenge') ?? '', redirectUri: q.get('redirect_uri') ?? '' })\n return `${q.get('redirect_uri')}?code=${code}&state=${q.get('state')}`\n }\n\n private issue(sub: string) {\n const at = `at-${++this.n}`\n const rt = `rt-${++this.n}`\n this.access.set(at, sub)\n this.refresh.set(rt, sub)\n return { access_token: at, refresh_token: rt, token_type: 'Bearer', expires_in: this.expiresIn, scope: '', ...(this.persistent === undefined ? {} : { persistent: this.persistent }) }\n }\n\n fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)\n this.calls.push(`${init?.method ?? 'GET'} ${url.pathname}`)\n if (this.down) throw new TypeError('fetch failed')\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ''))\n const bearer = new Headers(init?.headers).get('authorization')?.replace('Bearer ', '') ?? ''\n if (this.ownScreens && url.pathname.startsWith('/v1/auth/')) return this.ownScreen(url, init)\n\n if (url.pathname === '/v1/oauth/token') {\n if (form.get('grant_type') === 'authorization_code') {\n const c = this.codes.get(form.get('code') ?? '')\n this.codes.delete(form.get('code') ?? '')\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(form.get('code_verifier') ?? ''))\n const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n if (!c || c.challenge !== challenge || c.redirectUri !== form.get('redirect_uri')) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(c.sub))\n }\n if (form.get('grant_type') === 'refresh_token') {\n const sub = this.refresh.get(form.get('refresh_token') ?? '')\n this.refresh.delete(form.get('refresh_token') ?? '') // used once\n if (!sub || !this.users.get(sub)?.active) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(sub))\n }\n return reply(400, { error: 'unsupported_grant_type', error_description: 'supported grant types: authorization_code, refresh_token' })\n }\n if (url.pathname === '/v1/oauth/revoke') {\n this.revoked.push(form.get('token') ?? '')\n this.refresh.delete(form.get('token') ?? '')\n return reply(200, {})\n }\n const sub = this.access.get(bearer)\n const user = sub ? this.users.get(sub) : undefined\n if (url.pathname === '/v1/oauth/userinfo') {\n if (!user) return reply(401, {})\n return reply(200, { sub: user.sub, tid: 't1', email: user.email, name: user.name, email_verified: user.verified ?? !user.anonymous, restricted: user.restricted, roles: ['member'], ...(user.anonymous ? { anonymous: true } : {}) })\n }\n const asking = url.pathname.match(/^\\/v1\\/oauth\\/organizations\\/([^/]+)\\/check$/)\n if (asking && init?.method === 'POST') {\n if (!user) return reply(401, { errors: [{ code: 'unauthorized', message: 'invalid or expired token' }] })\n const org = decodeURIComponent(asking[1])\n const body = JSON.parse(String(init.body))\n this.checks.push({ organization: org, sub: user.sub, body })\n if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: 'organization_not_found', message: 'organization not found' }] })\n // An accountant approves invoices up to a million.\n const allowed = body.resource === 'invoices' && body.action === 'approve' && Number(body.attributes?.amount) <= 1_000_000\n return reply(200, { data: allowed\n ? { allowed, reason: 'allowed by role accountant: invoices:approve', rule: { source: 'role', role: 'accountant', kind: '*', resource: 'invoices', action: 'approve', effect: 'allow', condition: 'resource.attrs.amount <= 1000000' } }\n : { allowed, reason: 'no rule allows it' } })\n }\n if (url.pathname === '/v1/oauth/organizations') {\n if (!user) return reply(401, {})\n const system = url.searchParams.get('system')\n const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system)\n return reply(200, { data, total_count: data.length })\n }\n return reply(404, {})\n }\n\n /** The part of /v1/auth/* a password sign-in and the sign-in form read. */\n private ownScreen(url: URL, init: RequestInit | undefined): Response {\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n if (new Headers(init?.headers).get('x-tenant-key') !== this.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n const method = init?.method ?? 'GET'\n if (method === 'POST' && url.pathname === '/v1/auth/login') {\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string }\n const user = [...this.users.values()].find((u) => u.email === body.email)\n const password = body.email === undefined ? undefined : this.passwords.get(body.email)\n if (!user || !user.active || password === undefined || password !== body.password) {\n return refuse(401, 'invalid_credentials', 'invalid email or password')\n }\n return reply(200, { data: this.issue(user.sub) })\n }\n if (method === 'GET' && url.pathname === '/v1/auth/settings') return reply(200, { data: this.settings })\n if (method === 'GET' && (url.pathname === '/v1/auth/social/providers' || url.pathname === '/v1/auth/sso/connections')) return reply(200, { data: [] })\n if (method === 'GET' && url.pathname === '/v1/auth/webauthn/place') return reply(200, { data: { here: false, on_cauthy: false } })\n return refuse(404, 'not_found', 'not found')\n }\n}\n\n/** A browser as far as cookies go: it keeps what it is told to and sends it back. */\nexport class Browser {\n jar = new Map<string, string>()\n /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */\n constructor(readonly origin = 'https://app.test', private readonly options: { requestHeader?: boolean } = {}) {}\n\n request(path: string, init: RequestInit = {}): Request {\n const headers = new Headers(init.headers)\n if (this.jar.size > 0) headers.set('cookie', [...this.jar].map(([k, v]) => `${k}=${v}`).join('; '))\n // What a protocol-1 client sends on every mutation.\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && this.options.requestHeader !== false && !headers.has('x-cauthy-request')) {\n headers.set('x-cauthy-request', '1')\n }\n return new Request(this.origin + path, { ...init, headers })\n }\n\n /** Keeps the cookies of a response, as a browser would. */\n take(source: Response | string[]): void {\n const cookies = Array.isArray(source) ? source : source.headers.getSetCookie()\n for (const c of cookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n const name = pair.slice(0, i)\n const value = pair.slice(i + 1)\n if (value === '' || attrs.includes('Max-Age=0')) this.jar.delete(name)\n else this.jar.set(name, value)\n }\n }\n}\n"],"mappings":"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","names":["sub"]}
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/testing.ts
|
|
2
|
+
var FakeCauthy = class {
|
|
3
|
+
issuer = "https://id.test";
|
|
4
|
+
users = /* @__PURE__ */ new Map();
|
|
5
|
+
codes = /* @__PURE__ */ new Map();
|
|
6
|
+
refresh = /* @__PURE__ */ new Map();
|
|
7
|
+
// refresh token → sub
|
|
8
|
+
access = /* @__PURE__ */ new Map();
|
|
9
|
+
// access token → sub
|
|
10
|
+
orgs = /* @__PURE__ */ new Map();
|
|
11
|
+
// sub → organizations
|
|
12
|
+
revoked = [];
|
|
13
|
+
checks = [];
|
|
14
|
+
calls = [];
|
|
15
|
+
down = false;
|
|
16
|
+
expiresIn = 600;
|
|
17
|
+
/** What token answers say of the session's kind; absent means an older Cauthy that does not say. */
|
|
18
|
+
persistent = void 0;
|
|
19
|
+
/** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */
|
|
20
|
+
ownScreens = false;
|
|
21
|
+
/** The workspace those screens sign in to, as X-Tenant-Key names it. */
|
|
22
|
+
tenant = "acme";
|
|
23
|
+
/** Each account's password, by email: only these accounts sign in with one. */
|
|
24
|
+
passwords = /* @__PURE__ */ new Map();
|
|
25
|
+
/** What GET /v1/auth/settings answers. */
|
|
26
|
+
settings = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false };
|
|
27
|
+
n = 0;
|
|
28
|
+
constructor() {
|
|
29
|
+
this.users.set("u1", { sub: "u1", email: "ana@acme.com", name: "Ana", active: true, restricted: false });
|
|
30
|
+
}
|
|
31
|
+
/** What the hosted page does once the person has signed in: hands the browser a code. */
|
|
32
|
+
authorize(location, sub = "u1") {
|
|
33
|
+
const q = new URL(location).searchParams;
|
|
34
|
+
if (q.get("response_type") !== "code") throw new Error("Cauthy answers unsupported_response_type: response_type=code is required");
|
|
35
|
+
const code = `code-${++this.n}`;
|
|
36
|
+
this.codes.set(code, { sub, challenge: q.get("code_challenge") ?? "", redirectUri: q.get("redirect_uri") ?? "" });
|
|
37
|
+
return `${q.get("redirect_uri")}?code=${code}&state=${q.get("state")}`;
|
|
38
|
+
}
|
|
39
|
+
issue(sub) {
|
|
40
|
+
const at = `at-${++this.n}`;
|
|
41
|
+
const rt = `rt-${++this.n}`;
|
|
42
|
+
this.access.set(at, sub);
|
|
43
|
+
this.refresh.set(rt, sub);
|
|
44
|
+
return { access_token: at, refresh_token: rt, token_type: "Bearer", expires_in: this.expiresIn, scope: "", ...this.persistent === void 0 ? {} : { persistent: this.persistent } };
|
|
45
|
+
}
|
|
46
|
+
fetch = async (input, init) => {
|
|
47
|
+
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
48
|
+
this.calls.push(`${init?.method ?? "GET"} ${url.pathname}`);
|
|
49
|
+
if (this.down) throw new TypeError("fetch failed");
|
|
50
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
51
|
+
const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ""));
|
|
52
|
+
const bearer = new Headers(init?.headers).get("authorization")?.replace("Bearer ", "") ?? "";
|
|
53
|
+
if (this.ownScreens && url.pathname.startsWith("/v1/auth/")) return this.ownScreen(url, init);
|
|
54
|
+
if (url.pathname === "/v1/oauth/token") {
|
|
55
|
+
if (form.get("grant_type") === "authorization_code") {
|
|
56
|
+
const c = this.codes.get(form.get("code") ?? "");
|
|
57
|
+
this.codes.delete(form.get("code") ?? "");
|
|
58
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(form.get("code_verifier") ?? ""));
|
|
59
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
60
|
+
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" });
|
|
61
|
+
return reply(200, this.issue(c.sub));
|
|
62
|
+
}
|
|
63
|
+
if (form.get("grant_type") === "refresh_token") {
|
|
64
|
+
const sub2 = this.refresh.get(form.get("refresh_token") ?? "");
|
|
65
|
+
this.refresh.delete(form.get("refresh_token") ?? "");
|
|
66
|
+
if (!sub2 || !this.users.get(sub2)?.active) return reply(400, { error: "invalid_grant", error_description: "the grant is invalid, expired or already used" });
|
|
67
|
+
return reply(200, this.issue(sub2));
|
|
68
|
+
}
|
|
69
|
+
return reply(400, { error: "unsupported_grant_type", error_description: "supported grant types: authorization_code, refresh_token" });
|
|
70
|
+
}
|
|
71
|
+
if (url.pathname === "/v1/oauth/revoke") {
|
|
72
|
+
this.revoked.push(form.get("token") ?? "");
|
|
73
|
+
this.refresh.delete(form.get("token") ?? "");
|
|
74
|
+
return reply(200, {});
|
|
75
|
+
}
|
|
76
|
+
const sub = this.access.get(bearer);
|
|
77
|
+
const user = sub ? this.users.get(sub) : void 0;
|
|
78
|
+
if (url.pathname === "/v1/oauth/userinfo") {
|
|
79
|
+
if (!user) return reply(401, {});
|
|
80
|
+
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 } : {} });
|
|
81
|
+
}
|
|
82
|
+
const asking = url.pathname.match(/^\/v1\/oauth\/organizations\/([^/]+)\/check$/);
|
|
83
|
+
if (asking && init?.method === "POST") {
|
|
84
|
+
if (!user) return reply(401, { errors: [{ code: "unauthorized", message: "invalid or expired token" }] });
|
|
85
|
+
const org = decodeURIComponent(asking[1]);
|
|
86
|
+
const body = JSON.parse(String(init.body));
|
|
87
|
+
this.checks.push({ organization: org, sub: user.sub, body });
|
|
88
|
+
if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: "organization_not_found", message: "organization not found" }] });
|
|
89
|
+
const allowed = body.resource === "invoices" && body.action === "approve" && Number(body.attributes?.amount) <= 1e6;
|
|
90
|
+
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" } });
|
|
91
|
+
}
|
|
92
|
+
if (url.pathname === "/v1/oauth/organizations") {
|
|
93
|
+
if (!user) return reply(401, {});
|
|
94
|
+
const system = url.searchParams.get("system");
|
|
95
|
+
const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system);
|
|
96
|
+
return reply(200, { data, total_count: data.length });
|
|
97
|
+
}
|
|
98
|
+
return reply(404, {});
|
|
99
|
+
};
|
|
100
|
+
/** The part of /v1/auth/* a password sign-in and the sign-in form read. */
|
|
101
|
+
ownScreen(url, init) {
|
|
102
|
+
const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
103
|
+
const refuse = (status, code, message) => reply(status, { errors: [{ code, message }] });
|
|
104
|
+
if (new Headers(init?.headers).get("x-tenant-key") !== this.tenant) return refuse(404, "tenant_not_found", "no such workspace");
|
|
105
|
+
const method = init?.method ?? "GET";
|
|
106
|
+
if (method === "POST" && url.pathname === "/v1/auth/login") {
|
|
107
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
108
|
+
const user = [...this.users.values()].find((u) => u.email === body.email);
|
|
109
|
+
const password = body.email === void 0 ? void 0 : this.passwords.get(body.email);
|
|
110
|
+
if (!user || !user.active || password === void 0 || password !== body.password) {
|
|
111
|
+
return refuse(401, "invalid_credentials", "invalid email or password");
|
|
112
|
+
}
|
|
113
|
+
return reply(200, { data: this.issue(user.sub) });
|
|
114
|
+
}
|
|
115
|
+
if (method === "GET" && url.pathname === "/v1/auth/settings") return reply(200, { data: this.settings });
|
|
116
|
+
if (method === "GET" && (url.pathname === "/v1/auth/social/providers" || url.pathname === "/v1/auth/sso/connections")) return reply(200, { data: [] });
|
|
117
|
+
if (method === "GET" && url.pathname === "/v1/auth/webauthn/place") return reply(200, { data: { here: false, on_cauthy: false } });
|
|
118
|
+
return refuse(404, "not_found", "not found");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var Browser = class {
|
|
122
|
+
/** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */
|
|
123
|
+
constructor(origin = "https://app.test", options = {}) {
|
|
124
|
+
this.origin = origin;
|
|
125
|
+
this.options = options;
|
|
126
|
+
}
|
|
127
|
+
origin;
|
|
128
|
+
options;
|
|
129
|
+
jar = /* @__PURE__ */ new Map();
|
|
130
|
+
request(path, init = {}) {
|
|
131
|
+
const headers = new Headers(init.headers);
|
|
132
|
+
if (this.jar.size > 0) headers.set("cookie", [...this.jar].map(([k, v]) => `${k}=${v}`).join("; "));
|
|
133
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
134
|
+
if (method !== "GET" && method !== "HEAD" && this.options.requestHeader !== false && !headers.has("x-cauthy-request")) {
|
|
135
|
+
headers.set("x-cauthy-request", "1");
|
|
136
|
+
}
|
|
137
|
+
return new Request(this.origin + path, { ...init, headers });
|
|
138
|
+
}
|
|
139
|
+
/** Keeps the cookies of a response, as a browser would. */
|
|
140
|
+
take(source) {
|
|
141
|
+
const cookies = Array.isArray(source) ? source : source.headers.getSetCookie();
|
|
142
|
+
for (const c of cookies) {
|
|
143
|
+
const [pair, ...attrs] = c.split("; ");
|
|
144
|
+
const i = pair.indexOf("=");
|
|
145
|
+
const name = pair.slice(0, i);
|
|
146
|
+
const value = pair.slice(i + 1);
|
|
147
|
+
if (value === "" || attrs.includes("Max-Age=0")) this.jar.delete(name);
|
|
148
|
+
else this.jar.set(name, value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
export {
|
|
153
|
+
Browser,
|
|
154
|
+
FakeCauthy
|
|
155
|
+
};
|
|
156
|
+
//# 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\nexport class FakeCauthy {\n readonly issuer = 'https://id.test'\n users = new Map<string, { sub: string; email: string; name: string; active: boolean; restricted: boolean; anonymous?: boolean; verified?: boolean }>()\n codes = new Map<string, { sub: string; challenge: string; redirectUri: string }>()\n refresh = new Map<string, string>() // refresh token → sub\n access = new Map<string, string>() // access token → sub\n orgs = new Map<string, Record<string, unknown>[]>() // sub → organizations\n revoked: string[] = []\n checks: { organization: string; sub: string; body: any }[] = []\n calls: string[] = []\n down = false\n expiresIn = 600\n /** What token answers say of the session's kind; absent means an older Cauthy that does not say. */\n persistent: boolean | undefined = undefined\n /** Serve the API an application's own screens use (/v1/auth/*). Off unless a test asks. */\n ownScreens = false\n /** The workspace those screens sign in to, as X-Tenant-Key names it. */\n tenant = 'acme'\n /** Each account's password, by email: only these accounts sign in with one. */\n passwords = new Map<string, string>()\n /** What GET /v1/auth/settings answers. */\n settings: Record<string, unknown> = { allow_password: true, allow_signup: false, allow_passkeys: false, persistent_sessions: false }\n private n = 0\n\n constructor() {\n this.users.set('u1', { sub: 'u1', email: 'ana@acme.com', name: 'Ana', active: true, restricted: false })\n }\n\n /** What the hosted page does once the person has signed in: hands the browser a code. */\n authorize(location: string, sub = 'u1'): string {\n const q = new URL(location).searchParams\n if (q.get('response_type') !== 'code') throw new Error('Cauthy answers unsupported_response_type: response_type=code is required')\n const code = `code-${++this.n}`\n this.codes.set(code, { sub, challenge: q.get('code_challenge') ?? '', redirectUri: q.get('redirect_uri') ?? '' })\n return `${q.get('redirect_uri')}?code=${code}&state=${q.get('state')}`\n }\n\n private issue(sub: string) {\n const at = `at-${++this.n}`\n const rt = `rt-${++this.n}`\n this.access.set(at, sub)\n this.refresh.set(rt, sub)\n return { access_token: at, refresh_token: rt, token_type: 'Bearer', expires_in: this.expiresIn, scope: '', ...(this.persistent === undefined ? {} : { persistent: this.persistent }) }\n }\n\n fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)\n this.calls.push(`${init?.method ?? 'GET'} ${url.pathname}`)\n if (this.down) throw new TypeError('fetch failed')\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const form = new URLSearchParams(init?.body instanceof URLSearchParams ? init.body : String(init?.body ?? ''))\n const bearer = new Headers(init?.headers).get('authorization')?.replace('Bearer ', '') ?? ''\n if (this.ownScreens && url.pathname.startsWith('/v1/auth/')) return this.ownScreen(url, init)\n\n if (url.pathname === '/v1/oauth/token') {\n if (form.get('grant_type') === 'authorization_code') {\n const c = this.codes.get(form.get('code') ?? '')\n this.codes.delete(form.get('code') ?? '')\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(form.get('code_verifier') ?? ''))\n const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n if (!c || c.challenge !== challenge || c.redirectUri !== form.get('redirect_uri')) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(c.sub))\n }\n if (form.get('grant_type') === 'refresh_token') {\n const sub = this.refresh.get(form.get('refresh_token') ?? '')\n this.refresh.delete(form.get('refresh_token') ?? '') // used once\n if (!sub || !this.users.get(sub)?.active) return reply(400, { error: 'invalid_grant', error_description: 'the grant is invalid, expired or already used' })\n return reply(200, this.issue(sub))\n }\n return reply(400, { error: 'unsupported_grant_type', error_description: 'supported grant types: authorization_code, refresh_token' })\n }\n if (url.pathname === '/v1/oauth/revoke') {\n this.revoked.push(form.get('token') ?? '')\n this.refresh.delete(form.get('token') ?? '')\n return reply(200, {})\n }\n const sub = this.access.get(bearer)\n const user = sub ? this.users.get(sub) : undefined\n if (url.pathname === '/v1/oauth/userinfo') {\n if (!user) return reply(401, {})\n return reply(200, { sub: user.sub, tid: 't1', email: user.email, name: user.name, email_verified: user.verified ?? !user.anonymous, restricted: user.restricted, roles: ['member'], ...(user.anonymous ? { anonymous: true } : {}) })\n }\n const asking = url.pathname.match(/^\\/v1\\/oauth\\/organizations\\/([^/]+)\\/check$/)\n if (asking && init?.method === 'POST') {\n if (!user) return reply(401, { errors: [{ code: 'unauthorized', message: 'invalid or expired token' }] })\n const org = decodeURIComponent(asking[1])\n const body = JSON.parse(String(init.body))\n this.checks.push({ organization: org, sub: user.sub, body })\n if (!(this.orgs.get(user.sub) ?? []).some((o) => o.id === org)) return reply(404, { errors: [{ code: 'organization_not_found', message: 'organization not found' }] })\n // An accountant approves invoices up to a million.\n const allowed = body.resource === 'invoices' && body.action === 'approve' && Number(body.attributes?.amount) <= 1_000_000\n return reply(200, { data: allowed\n ? { allowed, reason: 'allowed by role accountant: invoices:approve', rule: { source: 'role', role: 'accountant', kind: '*', resource: 'invoices', action: 'approve', effect: 'allow', condition: 'resource.attrs.amount <= 1000000' } }\n : { allowed, reason: 'no rule allows it' } })\n }\n if (url.pathname === '/v1/oauth/organizations') {\n if (!user) return reply(401, {})\n const system = url.searchParams.get('system')\n const data = (this.orgs.get(user.sub) ?? []).filter((o) => !system || o.system === system)\n return reply(200, { data, total_count: data.length })\n }\n return reply(404, {})\n }\n\n /** The part of /v1/auth/* a password sign-in and the sign-in form read. */\n private ownScreen(url: URL, init: RequestInit | undefined): Response {\n const reply = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })\n const refuse = (status: number, code: string, message: string) => reply(status, { errors: [{ code, message }] })\n if (new Headers(init?.headers).get('x-tenant-key') !== this.tenant) return refuse(404, 'tenant_not_found', 'no such workspace')\n const method = init?.method ?? 'GET'\n if (method === 'POST' && url.pathname === '/v1/auth/login') {\n const body = JSON.parse(String(init?.body ?? '{}')) as { email?: string; password?: string }\n const user = [...this.users.values()].find((u) => u.email === body.email)\n const password = body.email === undefined ? undefined : this.passwords.get(body.email)\n if (!user || !user.active || password === undefined || password !== body.password) {\n return refuse(401, 'invalid_credentials', 'invalid email or password')\n }\n return reply(200, { data: this.issue(user.sub) })\n }\n if (method === 'GET' && url.pathname === '/v1/auth/settings') return reply(200, { data: this.settings })\n if (method === 'GET' && (url.pathname === '/v1/auth/social/providers' || url.pathname === '/v1/auth/sso/connections')) return reply(200, { data: [] })\n if (method === 'GET' && url.pathname === '/v1/auth/webauthn/place') return reply(200, { data: { here: false, on_cauthy: false } })\n return refuse(404, 'not_found', 'not found')\n }\n}\n\n/** A browser as far as cookies go: it keeps what it is told to and sends it back. */\nexport class Browser {\n jar = new Map<string, string>()\n /** `requestHeader: false`: a browser whose client sends no X-Cauthy-Request, as cauthy-auth 0.42's does not. */\n constructor(readonly origin = 'https://app.test', private readonly options: { requestHeader?: boolean } = {}) {}\n\n request(path: string, init: RequestInit = {}): Request {\n const headers = new Headers(init.headers)\n if (this.jar.size > 0) headers.set('cookie', [...this.jar].map(([k, v]) => `${k}=${v}`).join('; '))\n // What a protocol-1 client sends on every mutation.\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && this.options.requestHeader !== false && !headers.has('x-cauthy-request')) {\n headers.set('x-cauthy-request', '1')\n }\n return new Request(this.origin + path, { ...init, headers })\n }\n\n /** Keeps the cookies of a response, as a browser would. */\n take(source: Response | string[]): void {\n const cookies = Array.isArray(source) ? source : source.headers.getSetCookie()\n for (const c of cookies) {\n const [pair, ...attrs] = c.split('; ')\n const i = pair.indexOf('=')\n const name = pair.slice(0, i)\n const value = pair.slice(i + 1)\n if (value === '' || attrs.includes('Max-Age=0')) this.jar.delete(name)\n else this.jar.set(name, value)\n }\n }\n}\n"],"mappings":"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","names":["sub"]}
|