@gjermundgaraba/clankerauth-dev 0.5.1 → 0.7.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/README.md CHANGED
@@ -30,6 +30,33 @@ await issuer.close();
30
30
 
31
31
  Each call listens on a random loopback port, creates a fresh SQLite database and secret in a temporary directory, provisions the owner, the resources and the client in-process through the server's administration module, and returns. Only the returned issuer is reached over HTTP. The client is first party: once the owner is signed in, an authorization request redirects straight to the callback with a code and no consent step. `close()` drains in-flight requests, closes the database and deletes the directory. Nothing is shared between calls and nothing survives them.
32
32
 
33
+ ## Provisioning
34
+
35
+ The issuer provisions what a test or a development script would otherwise script by hand against its HTTP API:
36
+
37
+ ```ts
38
+ const key = await issuer.apiKey({ permissions: { [resource.identifier]: ["notes:read"] } });
39
+
40
+ // Needs a cookieDomain: the forward cookie is what a browser holds.
41
+ const { cookie, cookies } = await issuer.ownerSession("http://app.notes.localhost:5173");
42
+ const token = await issuer.ownerToken({
43
+ resource: resource.identifier,
44
+ appOrigin: "http://app.notes.localhost:5173",
45
+ });
46
+ ```
47
+
48
+ `apiKey` returns the secret once. `ownerSession` signs the owner in and seals the forward cookie exactly as `/forward-auth/continue` does for a browser; `cookies` is the name/value form a browser-automation context takes. `ownerToken` runs the same forward-auth check and returns the access token, so a test never assembles a cookie jar.
49
+
50
+ ## A workspace that survives a restart
51
+
52
+ Pass `dataDir` and a fixed `port` to keep the issuer's state:
53
+
54
+ ```ts
55
+ const issuer = await startDisposableIssuer({ ...options, dataDir: ".dev/auth", port: 5174 });
56
+ ```
57
+
58
+ The owner account and password, the signing secret, the registered client and the database are kept in that directory, and provisioning is idempotent, so the next start reuses them: the same sign-in works, API keys minted before still verify, and browser storage keyed by the origin survives. A fixed port is part of that, because the origin is what cookies and registered redirect URIs are keyed by. `close()` leaves the directory alone. Treat it as the whole secret of the workspace and keep it out of version control. Without `dataDir`, nothing changes: a temporary directory, a random port, and everything removed by `close()`.
59
+
33
60
  To develop an app behind forward auth, pass a `cookieDomain`:
34
61
 
35
62
  ```ts
@@ -42,11 +69,65 @@ const issuer = await startDisposableIssuer({
42
69
  issuer.url; // http://auth.notes.localhost:<port>
43
70
  ```
44
71
 
45
- The issuer then serves `/forward-auth?resource=<identifier>`, `/forward-auth/continue` and `/forward-auth/logout`, and shares its forward cookie with every host under the domain, so serve the app on one, such as `app.notes.localhost`. Browsers and Node resolve `.localhost` names to loopback without a hosts-file entry. A signed-in owner's check answers `204` with an `Authorization` header to copy upstream; without a session, a page navigation (`Sec-Fetch-Mode: navigate`) is redirected through sign-in and any other request is `401`. Node's `fetch` always sends `Sec-Fetch-Mode: cors`, so a proxy written in Node must make the check with `node:http` to relay the browser's header. Without `cookieDomain` the issuer is `http://127.0.0.1:<port>` and these routes are not served.
72
+ The issuer then serves `/forward-auth?resource=<identifier>`, `/forward-auth/continue` and `/forward-auth/logout`, and shares its forward cookie with every host under the domain, so serve the app on one, such as `app.notes.localhost`. Browsers and Node resolve `.localhost` names to loopback without a hosts-file entry. A signed-in owner's check answers `204` with an `Authorization` header to copy upstream; without a session, a page navigation (`Sec-Fetch-Mode: navigate`) is redirected through sign-in and any other request is `401`. Without `cookieDomain` the issuer is `http://127.0.0.1:<port>` and these routes are not served.
73
+
74
+ ## The edge
75
+
76
+ `@gjermundgaraba/clankerauth-dev/edge` is the deployment edge for local development: the `forward_auth` check, and for upgrades a reverse proxy. Do not write one again.
77
+
78
+ ```ts
79
+ import { forwardAuth, reserveLoopbackPort } from "@gjermundgaraba/clankerauth-dev/edge";
80
+
81
+ export default defineConfig({
82
+ plugins: [
83
+ forwardAuth({
84
+ issuer: "http://auth.notes.localhost:5174",
85
+ appOrigin: "http://app.notes.localhost:5173",
86
+ resource: "http://app.notes.localhost:5173/",
87
+ sockets: { backend: "http://127.0.0.1:8080", paths: ["/eventlog"] },
88
+ onError: (error) => console.error("[forward-auth]", error.message),
89
+ }),
90
+ ],
91
+ });
92
+ ```
93
+
94
+ Nothing in this package imports Vite, at run time or in its types: the plugin is the plain object Vite accepts. On a plain Node server, `attach(server, options)` installs the same two halves and returns the request middleware; `check(options)` and `middleware(options)` are available on their own.
95
+
96
+ - The check uses `node:http`, never `fetch`: `fetch` owns `Sec-Fetch-Mode`, and that header is what tells the issuer whether a request may be redirected through sign-in. The browser's own header is relayed, along with its cookie, plus `x-forwarded-proto`, `-host` and `-uri`.
97
+ - It has a timeout (5 s by default) and **rejects** rather than deciding when the issuer is unreachable, too slow, or answers `204` with no `Authorization` to forward. `middleware` turns a rejection into a 502 and passes the error to `onError`; an empty credential is never forwarded upstream.
98
+ - A refusal is relayed as the issuer wrote it: its status, its `Location`, its body and its content type. A script that is not signed in reads `{"error":"unauthenticated"}` rather than an empty 401.
99
+ - `publicPaths` defaults to `/mcp`, `/.well-known` and `/healthz`, and a request that already carries an `Authorization` header is never checked: agents onboard themselves and probes run.
100
+ - `upgrade` only takes the paths in `sockets.paths`, so a development server keeps its own hot-reload socket; it returns `false` for everything else, which on a plain Node server means another listener must answer them. Omit `sockets` and it takes none. A checked upgrade is proxied to `sockets.backend`; a half closing normally ends the other half, and only an error destroys. A client that gives up while the issuer is deciding is not proxied at all. A socket error is reported through `onError` and never crashes the server.
101
+ - `reserveLoopbackPort()` claims a free port before anything that must know its own origin up front.
102
+
103
+ The edge's types import `node:http` types, so a TypeScript consumer needs `@types/node`.
46
104
 
47
105
  Two optional test hooks help exercise integrations:
48
106
 
49
107
  - `cimdTransport(input, init)` replaces outbound Client ID Metadata Document retrieval and returns a `Response` or `Promise<Response>`. Its arguments match `fetch`. Use it to serve fixture metadata for HTTPS client IDs without an external server. When omitted, the issuer uses its secure metadata transport.
50
108
  - `onRequest({ method, url })` observes actual incoming HTTP requests at the issuer listener, such as discovery, JWKS, dynamic client registration, and token requests. Provisioning is in-process, so the observer sees only the test's own traffic. `url` is a `URL`; headers and bodies are never passed. The callback runs synchronously before handling each request. A thrown error makes that request fail with HTTP 500, so keep observers nonthrowing.
51
109
 
110
+ ## A fake issuer for tests
111
+
112
+ `@gjermundgaraba/clankerauth-dev/testing` is the issuer an application test usually wants: the two endpoints a resource server actually calls, and nothing else. It signs with a real EdDSA key and publishes it as JWKS, so every token still travels the resource server's own verifier — signature, issuer, audience, type, claims, scopes — without starting the whole issuer.
113
+
114
+ ```ts
115
+ import { startFakeIssuer } from "@gjermundgaraba/clankerauth-dev/testing";
116
+
117
+ const auth = await startFakeIssuer({
118
+ resource: "http://127.0.0.1:8080/",
119
+ scopes: ["notes:read", "notes:write"],
120
+ });
121
+
122
+ auth.issuer; // configure the resource server with this
123
+ const token = await auth.sign(); // every claim is overridable: auth.sign({ exp, aud, scope })
124
+ const key = auth.apiKey(["notes:read"]); // and auth.apiKey(scopes, someOtherResource)
125
+ auth.revoke(key); // the next verification is 401, online, as the real issuer behaves
126
+ auth.fail(503); // both endpoints answer this instead; auth.fail() restores them
127
+ auth.verifications(); // how many API-key verifications were served
128
+ await auth.close();
129
+ ```
130
+
131
+ Use it for what a real issuer makes slow or awkward: a token that must expire in two seconds, a key granted on someone else's resource, an outage. Use `startDisposableIssuer` when the protocol itself is what the test is about — sign-in, consent, forward auth, dynamic registration.
132
+
52
133
  MIT licensed. The bundled third-party licenses are listed in `dist/THIRD_PARTY_NOTICES.txt`.
@@ -472,7 +472,7 @@ SOFTWARE.
472
472
 
473
473
  ---
474
474
 
475
- @gjermundgaraba/effect-actions@0.2.0
475
+ @gjermundgaraba/effect-actions@0.4.0
476
476
  LICENSE
477
477
  MIT License
478
478
 
package/dist/edge.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage, Server, ServerResponse } from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+ export interface CheckOptions {
4
+ /** The issuer origin serving `/forward-auth`, under the apps' shared cookie domain. */
5
+ readonly issuer: string;
6
+ /** The origin browsers reach this app at. Its host and scheme are forwarded. */
7
+ readonly appOrigin: string;
8
+ /**
9
+ * The resource identifier to ask for. One resource per app is a string; an app whose
10
+ * surfaces are separate resources passes a function of the request path.
11
+ */
12
+ readonly resource: string | ((path: string) => string);
13
+ /** How long to wait for the issuer. Default 5000 ms. */
14
+ readonly timeoutMs?: number;
15
+ }
16
+ /** What the issuer decided about one request, including the refusal it wrote. */
17
+ export interface Decision {
18
+ readonly status: number;
19
+ readonly authorization: string | undefined;
20
+ readonly location: string | undefined;
21
+ /** The issuer's own response body, relayed verbatim so a refusal reads the same here. */
22
+ readonly body: Buffer;
23
+ readonly contentType: string | undefined;
24
+ }
25
+ /** The request fields a check reads. A Node `IncomingMessage` already is one. */
26
+ export interface CheckedRequest {
27
+ readonly url?: string | undefined;
28
+ readonly headers: IncomingHttpHeaders;
29
+ }
30
+ /**
31
+ * Ask the issuer whether one request may proceed. Rejects instead of deciding when the
32
+ * issuer is unreachable, too slow, or answers `204` without the credential to forward:
33
+ * those are a broken edge, not a refusal to show a browser.
34
+ */
35
+ export declare const check: (options: CheckOptions) => (request: CheckedRequest) => Promise<Decision>;
36
+ /** The upgrades this edge owns, and where it proxies them once they are checked. */
37
+ export interface SocketOptions {
38
+ /** Where a checked upgrade is proxied, such as `http://127.0.0.1:8080`. */
39
+ readonly backend: string;
40
+ /**
41
+ * Paths whose upgrades this edge owns. Everything else, including a development
42
+ * server's own hot-reload socket, is left untouched.
43
+ */
44
+ readonly paths: readonly string[];
45
+ }
46
+ export interface EdgeOptions extends CheckOptions {
47
+ /**
48
+ * Path prefixes answered without a check, because their callers bring their own
49
+ * credential or must work without one. Defaults to `/mcp`, `/.well-known`, `/healthz`.
50
+ */
51
+ readonly publicPaths?: readonly string[];
52
+ /** Omit it and every upgrade is left to whoever else listens. */
53
+ readonly sockets?: SocketOptions;
54
+ /** Called with anything this edge swallowed, so a dev server can print it. */
55
+ readonly onError?: (error: Error) => void;
56
+ }
57
+ /**
58
+ * Connect-style middleware: an unauthenticated navigation is redirected through the
59
+ * issuer and back, anything else is refused in place, and a request that already
60
+ * carries a credential passes straight through.
61
+ */
62
+ export declare const middleware: (options: EdgeOptions) => (request: CheckedRequest, response: ServerResponse, next: () => void) => void;
63
+ /**
64
+ * Handle one `upgrade`. Returns `false` for a path this edge does not own, so a
65
+ * development server keeps its own hot-reload socket. An upgrade is never a
66
+ * navigation, so an unauthenticated one is refused in place: the page signs in and
67
+ * the client reconnects.
68
+ */
69
+ export declare const upgrade: (options: EdgeOptions) => (request: IncomingMessage, socket: Duplex, head: Buffer) => boolean;
70
+ /** Install both halves on a plain Node server, for an edge that is not a Vite plugin. */
71
+ export declare const attach: (server: Server, options: EdgeOptions) => (request: CheckedRequest, response: ServerResponse, next: () => void) => void;
72
+ /** The development server this plugin attaches to. Structural, so Vite is never imported. */
73
+ export interface DevServerLike {
74
+ readonly middlewares: {
75
+ use(handler: (request: IncomingMessage, response: ServerResponse, next: () => void) => void): void;
76
+ };
77
+ readonly httpServer?: {
78
+ on(event: "upgrade", listener: (request: IncomingMessage, socket: Duplex, head: Buffer) => void): void;
79
+ } | null | undefined;
80
+ }
81
+ /**
82
+ * A Vite plugin applying the same edge. Nothing here imports Vite: the object is
83
+ * structurally what Vite accepts, so this package never puts Vite in a consumer's
84
+ * dependency graph or its type graph.
85
+ */
86
+ export declare const forwardAuth: (options: EdgeOptions) => {
87
+ name: string;
88
+ apply: "serve";
89
+ configureServer: (server: DevServerLike) => void;
90
+ };
91
+ /** Claim a free loopback port before anything that must know its own origin up front. */
92
+ export declare const reserveLoopbackPort: () => Promise<number>;
package/dist/edge.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{createServer as e,request as t}from"node:http";const n=[`/mcp`,`/.well-known`,`/healthz`],r=e=>typeof e==`function`,i=e=>{let t=e.search(/[?#]/u);return t===-1?e:e.slice(0,t)},a=e=>e===void 0?``:Array.isArray(e)?e.join(`, `):String(e),o=e=>{let n=new URL(e.appOrigin),o=e.timeoutMs??5e3,s=t=>r(e.resource)?e.resource(t):e.resource;return r=>new Promise((c,l)=>{let u=r.url??`/`,d=new URL(`/forward-auth`,e.issuer);d.searchParams.set(`resource`,s(i(u)));let f=t(d,{headers:{cookie:a(r.headers.cookie),"sec-fetch-mode":a(r.headers[`sec-fetch-mode`]),"x-forwarded-proto":n.protocol.slice(0,-1),"x-forwarded-host":n.host,"x-forwarded-uri":u}},e=>{let t=e.statusCode??502,{authorization:n,location:r}=e.headers;if(t===204&&n===void 0){e.resume(),l(/* @__PURE__ */ Error(`Forward auth answered 204 without an Authorization header to forward`));return}let i=[];e.on(`data`,e=>i.push(e)),e.on(`error`,l),e.on(`end`,()=>c({status:t,authorization:n,location:r,body:Buffer.concat(i),contentType:e.headers[`content-type`]}))});f.setTimeout(o,()=>{f.destroy(/* @__PURE__ */ Error(`Forward auth did not answer within ${o}ms`))}),f.on(`error`,l),f.end()})},s=(e,t)=>t.some(t=>e===t||e.startsWith(`${t}/`)),c=e=>e instanceof Error?e:Error(String(e)),l=e=>{let t=o(e),r=e.publicPaths??[...n];return(n,a,o)=>{let l=i(n.url??`/`);if(n.headers.authorization!==void 0||s(l,r)){o();return}t(n).then(e=>{if(e.status===204){n.headers.authorization=e.authorization,o();return}let t={};e.location!==void 0&&(t.location=e.location),e.contentType!==void 0&&(t[`content-type`]=e.contentType),a.writeHead(e.status,t),a.end(e.body)},t=>{let n=c(t);e.onError?.(n),a.writeHead(502,{"content-type":`text/plain`}),a.end(n.message)})}},u=e=>Object.fromEntries(Object.entries(e).flatMap(([e,t])=>t===void 0?[]:[[e,Array.isArray(t)?t.join(`, `):t]])),d=e=>{let n=o(e);return(r,a,o)=>{let l=i(r.url??`/`),d=e.sockets;if(d===void 0||!s(l,d.paths))return!1;let f=new URL(d.backend),p=(t,n,r)=>{r&&e.onError?.(r),a.end(`HTTP/1.1 ${t} ${n}\r\nConnection: close\r\n\r\n`)};a.on(`error`,t=>{e.onError?.(t),a.destroy()});let m=n=>{let i=t({hostname:f.hostname,port:f.port,path:r.url??`/`,headers:{...u(r.headers),authorization:n}});i.on(`upgrade`,(t,n,r)=>{let i=`HTTP/1.1 101 Switching Protocols\r
2
+ `;for(let e=0;e<t.rawHeaders.length;e+=2)i+=`${t.rawHeaders[e]}: ${t.rawHeaders[e+1]}\r\n`;a.write(`${i}\r\n`),r.length&&a.write(r),o.length&&n.write(o),n.on(`error`,t=>{e.onError?.(t),a.destroy()}),a.once(`close`,()=>n.end()),n.once(`close`,()=>a.end()),a.pipe(n),n.pipe(a)}),i.on(`response`,e=>{e.resume(),p(e.statusCode??502,`Upgrade refused`)}),i.on(`error`,e=>p(502,`Bad Gateway`,e)),i.end()};return n(r).then(e=>{if(!a.destroyed){if(e.status!==204||e.authorization===void 0){p(401,`Unauthorized`);return}m(e.authorization)}},e=>{a.destroyed||p(502,`Bad Gateway`,c(e))}),!0}},f=(e,t)=>{let n=l(t),r=d(t);return e.on(`upgrade`,(e,t,n)=>{r(e,t,n)}),n},p=e=>{let t=l(e),n=d(e);return{name:`clankerauth-forward-auth`,apply:`serve`,configureServer:e=>{e.middlewares.use(t),e.httpServer?.on(`upgrade`,(e,t,r)=>{n(e,t,r)})}}},m=async()=>{let t=e();await new Promise(e=>t.listen(0,`127.0.0.1`,e));let n=t.address();if(!(n instanceof Object))throw Error(`Could not reserve a loopback port`);return await new Promise((e,n)=>t.close(t=>t?n(t):e())),n.port};export{f as attach,o as check,p as forwardAuth,l as middleware,m as reserveLoopbackPort,d as upgrade};
@@ -0,0 +1 @@
1
+ const e=new TextEncoder,t=new TextDecoder,n=new TextDecoder(`utf-8`,{fatal:!0}),r=2**32;function i(...e){let t=e.reduce((e,{length:t})=>e+t,0),n=new Uint8Array(t),r=0;for(let t of e)n.set(t,r),r+=t.length;return n}function a(e,t,n){if(t<0||t>=r)throw RangeError(`value must be >= 0 and <= ${r-1}. Received ${t}`);e.set([t>>>24,t>>>16,t>>>8,t&255],n)}function o(e){let t=Math.floor(e/r),n=e%r,i=/* @__PURE__ */ new Uint8Array(8);return a(i,t,0),a(i,n,4),i}function s(e){let t=/* @__PURE__ */ new Uint8Array(4);return a(t,e),t}const c=/[^\x00-\x7f]/;function l(t){if(typeof t==`string`&&t.length>=128){if(c.test(t))throw TypeError(`non-ASCII string encountered in encode()`);return e.encode(t)}let n=new Uint8Array(t.length);for(let e=0;e<t.length;e++){let r=t.charCodeAt(e);if(r>127)throw TypeError(`non-ASCII string encountered in encode()`);n[e]=r}return n}function u(e,t=!1){if(Uint8Array.prototype.toBase64)return e.toBase64({alphabet:t?`base64url`:`base64`,omitPadding:t});let n=32768,r=[];for(let t=0;t<e.length;t+=n)r.push(String.fromCharCode.apply(null,e.subarray(t,t+n)));let i=btoa(r.join(``));return t?i.replace(/=/g,``).replace(/\+/g,`-`).replace(/\//g,`_`):i}function d(e,t=!1){if(Uint8Array.fromBase64)return Uint8Array.fromBase64(e,{alphabet:t?`base64url`:`base64`});if(t){if(e.includes(`+`)||e.includes(`/`))throw TypeError(`Invalid base64url`);e=e.replace(/-/g,`+`).replace(/_/g,`/`)}let n=atob(e),r=new Uint8Array(n.length);for(let e=0;e<n.length;e++)r[e]=n.charCodeAt(e);return r}async function f(e,t){let n=`SHA-${e.slice(-3)}`;return new Uint8Array(await crypto.subtle.digest(n,t))}var p=class extends Error{static code=`ERR_JOSE_GENERIC`;code=`ERR_JOSE_GENERIC`;constructor(e,t){super(e,t),this.name=this.constructor.name,Error.captureStackTrace?.(this,this.constructor)}},m=class extends p{static code=`ERR_JWT_CLAIM_VALIDATION_FAILED`;code=`ERR_JWT_CLAIM_VALIDATION_FAILED`;claim;reason;payload;constructor(e,t,n=`unspecified`,r=`unspecified`){super(e,{cause:{claim:n,reason:r,payload:t}}),this.claim=n,this.reason=r,this.payload=t}},h=class extends p{static code=`ERR_JWT_EXPIRED`;code=`ERR_JWT_EXPIRED`;claim;reason;payload;constructor(e,t,n=`unspecified`,r=`unspecified`){super(e,{cause:{claim:n,reason:r,payload:t}}),this.claim=n,this.reason=r,this.payload=t}},g=class extends p{static code=`ERR_JOSE_ALG_NOT_ALLOWED`;code=`ERR_JOSE_ALG_NOT_ALLOWED`},_=class extends p{static code=`ERR_JOSE_NOT_SUPPORTED`;code=`ERR_JOSE_NOT_SUPPORTED`},v=class extends p{static code=`ERR_JWE_DECRYPTION_FAILED`;code=`ERR_JWE_DECRYPTION_FAILED`;constructor(e=`decryption operation failed`,t){super(e,t)}},y=class extends p{static code=`ERR_JWE_INVALID`;code=`ERR_JWE_INVALID`},b=class extends p{static code=`ERR_JWS_INVALID`;code=`ERR_JWS_INVALID`},x=class extends p{static code=`ERR_JWT_INVALID`;code=`ERR_JWT_INVALID`},ee=class extends p{static code=`ERR_JWK_INVALID`;code=`ERR_JWK_INVALID`},te=class extends p{static code=`ERR_JWKS_INVALID`;code=`ERR_JWKS_INVALID`},ne=class extends p{static code=`ERR_JWKS_NO_MATCHING_KEY`;code=`ERR_JWKS_NO_MATCHING_KEY`;constructor(e=`no applicable key found in the JSON Web Key Set`,t){super(e,t)}},re=class extends p{[Symbol.asyncIterator]=async function*(){};static code=`ERR_JWKS_MULTIPLE_MATCHING_KEYS`;code=`ERR_JWKS_MULTIPLE_MATCHING_KEYS`;constructor(e=`multiple matching keys found in the JSON Web Key Set`,t){super(e,t)}},ie=class extends p{static code=`ERR_JWKS_TIMEOUT`;code=`ERR_JWKS_TIMEOUT`;constructor(e=`request timed out`,t){super(e,t)}},ae=class extends p{static code=`ERR_JWS_SIGNATURE_VERIFICATION_FAILED`;code=`ERR_JWS_SIGNATURE_VERIFICATION_FAILED`;constructor(e=`signature verification failed`,t){super(e,t)}};function S(e){try{return d(typeof e==`string`?e:t.decode(e),!0)}catch(e){throw TypeError(`The input to be decoded is not correctly encoded.`,{cause:e})}}function C(t){return u(typeof t==`string`?e.encode(t):t,!0)}function oe(e,t){if(!(e instanceof Uint8Array))throw TypeError(`${t} must be an instance of Uint8Array`)}function w(e){if(typeof e!=`object`||!e||Object.prototype.toString.call(e)!==`[object Object]`)return!1;let t=Object.getPrototypeOf(e);return t===null||Object.getPrototypeOf(t)===null}function se(e){return w(e)&&Array.isArray(e.keys)&&Array.from(e.keys).every(w)}function ce(...e){let t=/* @__PURE__ */ new Set;for(let n of e)if(n)for(let e of Object.keys(n)){if(t.has(e))return!1;t.add(e)}return!0}function le(e,t){if(e!==void 0)throw TypeError(`${t} can only be called once`)}function ue(e,t,n){try{return S(e)}catch{throw new n(`Failed to base64url decode the ${t}`)}}function de(e,t,n){try{return l(e)}catch{throw new n(`The ${t} is not a valid base64url string`)}}function fe(e,t,r){let i;try{i=JSON.parse(n.decode(S(e)))}catch{throw new t(r)}if(!w(i))throw new t(r);return i}const pe={__proto__:null,b64:!0},me={__proto__:null};function he(e,t){if(t!==void 0&&(!Array.isArray(t)||t.some(e=>typeof e!=`string`)))throw TypeError(`"${e}" option must be an array of strings`);return t===void 0?void 0:new Set(t)}function ge(e,t){let{crit:n}=t??{};if(Array.isArray(n)&&new Set(n).size!==n.length)throw new e(`"crit" (Critical) Header Parameter MUST NOT contain duplicate values`)}function _e(e,t,n,r,i){if(i.crit!==void 0&&r?.crit===void 0)throw new e(`"crit" (Critical) Header Parameter MUST be integrity protected`);if(!r||r.crit===void 0)return[];if(!Array.isArray(r.crit)||r.crit.length===0||r.crit.some(e=>typeof e!=`string`||e.length===0))throw new e(`"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present`);let a=n===void 0?t:{__proto__:null,...n,...t};for(let t of r.crit){if(!(t in a))throw new _(`Extension Header Parameter "${t}" is not recognized`);if(!Object.hasOwn(i,t)||i[t]===void 0)throw new e(`Extension Header Parameter "${t}" is missing`);if(a[t]&&(!Object.hasOwn(r,t)||r[t]===void 0))throw new e(`Extension Header Parameter "${t}" MUST be integrity protected`)}return r.crit}function ve(e,t){if(t.includes(`b64`)){let t=e.b64;if(typeof t!=`boolean`)throw new b(`The "b64" (base64url-encode payload) Header Parameter must be a boolean`);return t}return!0}function T(e,t){let n,r;try{n=JSON.stringify(t),r=JSON.parse(n)}catch(t){throw new e(`JOSE Header is not valid JSON`,{cause:t})}if(!w(r))throw new e(`JOSE Header is not a JSON object`);return[r,n]}const ye=e=>e[Symbol.toStringTag],be=(e,t,n)=>{let{alg:r}=e;if(t.use!==void 0){let e=n===`sign`||n===`verify`?`sig`:`enc`;if(t.use!==e)throw TypeError(`Invalid key for this operation, its "use" must be "${e}" when present`)}if(t.alg!==void 0&&t.alg!==r)throw TypeError(`Invalid key for this operation, its "alg" must be "${r}" when present`);if(Array.isArray(t.key_ops)){let r=n===`encrypt`||n===`decrypt`?e.ops?.[n===`encrypt`?0:1]:n;if(r&&!t.key_ops.includes(r))throw TypeError(`Invalid key for this operation, its "key_ops" must include "${r}" when present`)}};async function xe(e,t,n){let{alg:r,secret:i}=e,a=n===`decrypt`||n===`sign`;if(i&&t instanceof Uint8Array)return t;let o,s;if(w(t)){if(o=Ne(t),typeof o.kty!=`string`)throw Oe(r,t,i);if(!(i?o.kty===`oct`&&typeof o.k==`string`:o.kty!==`oct`&&(a?o.kty===`AKP`&&typeof o.priv==`string`||typeof o.d==`string`:o.d===void 0&&o.priv===void 0)))throw TypeError(i?`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`:`JSON Web Key for this operation must be a ${a?`private`:`public`} JWK`);if(be(e,o,n),o.kty===`oct`)return S(o.k);if(!Object.isFrozen(t)){let{key_ops:e}=t;Array.isArray(e)&&Object.freeze(e),Object.freeze(t)}}else{if(!Te(t))throw Oe(r,t,i);let e=i?`secret`:a?`private`:`public`;if(t.type!==e&&(i||[`secret`,`public`,`private`].includes(t.type)))throw TypeError(`${ye(t)} instances must be of type "${e}" for the ${r} algorithm`);if(D(t))return t;if(s=t,s.type===`secret`)return s.export()}E||=/* @__PURE__ */ new WeakMap;let c=t,l=E.get(c);if(l?.[r])return l[r];if(l||E.set(c,l={}),s&&typeof s.toCryptoKey==`function`){let t=s.type===`public`,n=Se[s.asymmetricKeyDetails?.namedCurve],i=e.resolve?.({crv:n,asymmetricKeyType:s.asymmetricKeyType})??e.subtle;return l[r]=s.toCryptoKey(i,t,e.usages[+!t])}return o??=s.export({format:`jwk`}),o.alg=r,l[r]=await Fe(e,o)}let E;const Se={__proto__:null,prime256v1:`P-256`,secp384r1:`P-384`,secp521r1:`P-521`};function Ce(e){if(!D(e))throw Error(`CryptoKey instance expected`)}const D=e=>{if(e?.[Symbol.toStringTag]===`CryptoKey`)return!0;try{return e instanceof CryptoKey}catch{return!1}},we=e=>e?.[Symbol.toStringTag]===`KeyObject`,Te=e=>D(e)||we(e);function Ee(e,t,...n){if(n.length>2){let t=n.pop();e+=`one of type ${n.join(`, `)}, or ${t}.`}else n.length===2?e+=`one of type ${n[0]} or ${n[1]}.`:e+=`of type ${n[0]}.`;return t==null?e+=` Received ${t}`:typeof t==`function`&&t.name?e+=` Received function ${t.name}`:typeof t==`object`&&t&&t.constructor?.name&&(e+=` Received an instance of ${t.constructor.name}`),e}const De=(e,...t)=>Ee(`Key must be `,e,...t);function Oe(e,t,n){let r=[`CryptoKey`,`KeyObject`,`JSON Web Key`];return n&&r.push(`Uint8Array`),TypeError(Ee(`Key for the ${e} algorithm must be `,t,...r))}const O=(e,t=`algorithm.name`)=>/* @__PURE__ */ TypeError(`CryptoKey does not support this operation, its ${t} must be ${e}`);function ke(e,t){if(t&&!e.usages.includes(t))throw TypeError(`CryptoKey does not support this operation, its usages must include ${t}.`)}function Ae(e,t){let{modulusLength:n}=t.algorithm;if(typeof n!=`number`||n<2048)throw TypeError(`${e} requires key modulusLength to be 2048 bits or larger`)}function je(e,t,n){let r=e.algorithm;if(r.name!==t.name)throw O(t.name);if(t.hash&&r.hash?.name!==t.hash)throw O(t.hash,`algorithm.hash`);if(t.namedCurve&&r.namedCurve!==t.namedCurve)throw O(t.namedCurve,`algorithm.namedCurve`);if(t.length!==void 0&&r.length!==t.length)throw O(t.length,`algorithm.length`);ke(e,n)}function Me(e){return{__proto__:null,...e}}function Ne(e){let t=Me(e);if(t.ext!==void 0&&typeof t.ext!=`boolean`)throw TypeError(`"ext" (Extractable) Parameter must be a boolean`);if(t.key_ops!==void 0){let e=t.key_ops,n=Array.isArray(e)?[...e]:void 0;if(!n||n.some(e=>typeof e!=`string`)||new Set(n).size!==n.length)throw TypeError(`"key_ops" (Key Operations) Parameter must be an array of unique strings`);t.key_ops=n}return t}function Pe(e){if(e!==void 0&&typeof e!=`boolean`)throw TypeError(`"extractable" option must be a boolean`);return e}async function Fe(e,t,n){if(!e.kty.includes(t.kty))throw new _(`Invalid or unsupported JWK "alg" (Algorithm) Parameter value`);let r=e.resolve?.({kty:t.kty,crv:t.crv})??e.subtle,i=!!(t.d||t.priv),a={...t,ext:n??t.ext};return a.kty!==`AKP`&&delete a.alg,delete a.use,crypto.subtle.importKey(`jwk`,a,r,a.ext??!i,t.key_ops??e.usages[+!!i])}async function Ie(e,t,n,r=!1){return e instanceof Uint8Array&&(e=await crypto.subtle.importKey(`raw`,e,t,r,[n])),je(e,t,n),e}function k(e){let t={__proto__:null};for(let n in e)t[n]={...e[n],alg:n};return t}const Le=[[`encrypt`,`wrapKey`],[`decrypt`,`unwrapKey`]],Re=[[],[`deriveBits`]],A=[[],[]];function j(e){return{kty:[`RSA`],mode:`key-encryption`,subtle:{name:`RSA-OAEP`,hash:`SHA-${e}`},usages:Le,ops:[`wrapKey`,`unwrapKey`]}}function M(e){return{kty:[`EC`,`OKP`],mode:e,subtle:{name:`ECDH`},resolve:({kty:e,crv:t,asymmetricKeyType:n})=>{if(t===`X25519`||n===`x25519`)return{name:`X25519`};if(e===`OKP`)throw new _(`Invalid or unsupported JWK "alg" (Algorithm) Parameter value`);return{name:`ECDH`,namedCurve:t}},usages:Re,ops:[void 0,`deriveBits`]}}function N(e,t=!1){return{kty:[`oct`],mode:`key-wrapping`,secret:!0,subtle:{name:t?`AES-GCM`:`AES-KW`,length:e},usages:A,ops:t?[`encrypt`,`decrypt`]:[`wrapKey`,`unwrapKey`]}}function P(){return{kty:[`oct`],mode:`key-wrapping`,secret:!0,subtle:{name:`PBKDF2`},usages:A,ops:[`deriveBits`,`deriveBits`]}}const F=k({dir:{kty:[`oct`],mode:`direct-encryption`,secret:!0,subtle:{name:`AES-GCM`},usages:A,ops:[`encrypt`,`decrypt`]},"RSA-OAEP":j(1),"RSA-OAEP-256":j(256),"RSA-OAEP-384":j(384),"RSA-OAEP-512":j(512),"ECDH-ES":M(`direct-key-agreement`),"ECDH-ES+A128KW":M(`key-agreement-with-key-wrapping`),"ECDH-ES+A192KW":M(`key-agreement-with-key-wrapping`),"ECDH-ES+A256KW":M(`key-agreement-with-key-wrapping`),A128KW:N(128),A192KW:N(192),A256KW:N(256),A128GCMKW:N(128,!0),A192GCMKW:N(192,!0),A256GCMKW:N(256,!0),"PBES2-HS256+A128KW":P(),"PBES2-HS384+A192KW":P(),"PBES2-HS512+A256KW":P()}),ze=[`encrypt`,`decrypt`];function I(e,t=!1){return{kty:[`oct`],secret:!0,subtle:{name:t?`AES-CBC`:`AES-GCM`,length:e},usages:A,ops:ze,cekBits:e,ivBits:t?128:96,cbc:t}}const Be=k({A128GCM:I(128),A192GCM:I(192),A256GCM:I(256),"A128CBC-HS256":I(256,!0),"A192CBC-HS384":I(384,!0),"A256CBC-HS512":I(512,!0)});function Ve(e,t){throw new _(`Invalid or unsupported "${e}" (JWE ${t}) header value`)}function He(e){return(typeof e==`string`?F[e]:void 0)??Ve(`alg`,`Algorithm`)}function Ue(e){return e.mode===`key-wrapping`||e.mode===`key-encryption`||e.mode===`key-agreement-with-key-wrapping`}function We(e){return(typeof e==`string`?Be[e]:void 0)??Ve(`enc`,`Encryption Algorithm`)}const L=[[`verify`],[`sign`]];function R(e){let t={name:`HMAC`,hash:`SHA-${e}`};return{kty:[`oct`],secret:!0,subtle:t,signing:t,usages:L}}function z(e,t){let n={name:t?`RSA-PSS`:`RSASSA-PKCS1-v1_5`,hash:`SHA-${e}`};return{kty:[`RSA`],subtle:n,signing:t?{...n,saltLength:t}:n,usages:L,minRsaBits:2048}}function B(e,t){return{kty:[`EC`],crv:e,subtle:{name:`ECDSA`,namedCurve:e},signing:{name:`ECDSA`,hash:`SHA-${t}`},usages:L}}function Ge(){let e={name:`Ed25519`};return{kty:[`OKP`],crv:`Ed25519`,subtle:e,signing:e,usages:L}}function V(e){let t={name:`ML-DSA-${e}`};return{kty:[`AKP`],subtle:t,signing:t,usages:L}}const H=k({HS256:R(256),HS384:R(384),HS512:R(512),RS256:z(256),RS384:z(384),RS512:z(512),PS256:z(256,32),PS384:z(384,48),PS512:z(512,64),ES256:B(`P-256`,256),ES384:B(`P-384`,384),ES512:B(`P-521`,512),EdDSA:Ge(),Ed25519:Ge(),"ML-DSA-44":V(44),"ML-DSA-65":V(65),"ML-DSA-87":V(87)});function Ke(e){let t=typeof e==`string`?H[e]:void 0;if(!t)throw new _(`alg ${e} is not supported either by JOSE or your javascript runtime`);return t}const U=e=>Math.floor(e.getTime()/1e3),qe={s:1,m:60,h:3600,d:86400,w:604800,y:31557600},Je=/^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i,W=`check_failed`;function G(){throw TypeError(`Invalid time period format`)}function K(e){typeof e!=`string`&&G();let t=Je.exec(e);(!t||t[4]&&t[1])&&G();let n=parseFloat(t[2]),r=Math.round(n*qe[t[3][0].toLowerCase()]);return Number.isFinite(r)||G(),t[1]===`-`||t[4]===`ago`?-r:r}function q(e,t){if(!Number.isFinite(t))throw TypeError(`Invalid ${e} input`);return t}function J(e,t){if(typeof t!=`string`)throw TypeError(`"${e}" claim must be a string`)}function Ye(e){if(typeof e!=`string`&&(!Array.isArray(e)||Array.from(e).some(e=>typeof e!=`string`)))throw TypeError(`"aud" claim must be a string or an array of strings`)}function Y(e,t){return typeof e==`number`?q(t,e):e instanceof Date?q(t,U(e)):U(/* @__PURE__ */ new Date)+K(e)}const Xe=e=>{let t=e.toLowerCase();return e.includes(`/`)?t:`application/${t}`},Ze=(e,t)=>typeof e==`string`?t.includes(e):Array.isArray(e)?t.some(t=>e.includes(t)):!1;function X(e,t,n=!1){let r=e[t];if(r!==void 0||n){if(typeof r!=`number`)throw new m(`"${t}" claim must be a number`,e,t,`invalid`);return r}}function Z(e,t){throw new m(`unexpected "${t}" claim value`,e,t,W)}function Qe(e,t,r={}){let i;try{i=JSON.parse(n.decode(t))}catch{}if(!w(i))throw new x(`JWT Claims Set must be a top-level JSON object`);let{typ:a}=r;if(a!==void 0&&(typeof e.typ!=`string`||Xe(e.typ)!==Xe(a)))throw new m(`unexpected "typ" JWT header value`,i,`typ`,W);let{requiredClaims:o=[],issuer:s,subject:c,audience:l,maxTokenAge:u}=r,d=[...o];u!==void 0&&d.push(`iat`),l!==void 0&&d.push(`aud`),c!==void 0&&d.push(`sub`),s!==void 0&&d.push(`iss`);for(let e of new Set(d.reverse()))if(!Object.hasOwn(i,e))throw new m(`missing required "${e}" claim`,i,e,`missing`);s!==void 0&&!(Array.isArray(s)?s:[s]).includes(i.iss)&&Z(i,`iss`),c!==void 0&&i.sub!==c&&Z(i,`sub`),l!==void 0&&!Ze(i.aud,typeof l==`string`?[l]:l)&&Z(i,`aud`);let{clockTolerance:f}=r,p=0;if(typeof f==`string`)p=K(f);else if(f!==void 0){if(typeof f!=`number`)throw TypeError(`Invalid clockTolerance option type`);p=f}q(`clockTolerance option`,p);let{currentDate:g}=r,_=q(`currentDate option`,U(g===void 0?/* @__PURE__ */ new Date:g)),v=X(i,`iat`,u!==void 0),y=X(i,`nbf`);if(y!==void 0&&y>_+p)throw new m(`"nbf" claim timestamp check failed`,i,`nbf`,W);let b=X(i,`exp`);if(b!==void 0&&b<=_-p)throw new h(`"exp" claim timestamp check failed`,i,`exp`,W);if(u!==void 0){let e=_-v,t=q(`maxTokenAge option`,typeof u==`number`?u:K(u));if(e-p>t)throw new h(`"iat" claim timestamp check failed (too far in the past)`,i,`iat`,W);if(e<-p)throw new m(`"iat" claim timestamp check failed (it should be in the past)`,i,`iat`,W)}return i}let Q;function $(e){return Q.get(e)}function $e(t){let n=$(t);for(let e of[`iat`,`nbf`,`exp`]){let t=n[e];if(typeof t==`number`&&!Number.isFinite(t))throw TypeError(`"${e}" claim must be a finite number`)}return e.encode(JSON.stringify(n))}function et(e,t){return $(e)[t]}var tt=class{constructor(e={}){if(!w(e))throw TypeError(`JWT Claims Set MUST be an object`);(Q||=/* @__PURE__ */ new WeakMap).set(this,structuredClone(e))}setIssuer(e){return J(`iss`,e),$(this).iss=e,this}setSubject(e){return J(`sub`,e),$(this).sub=e,this}setAudience(e){return Ye(e),$(this).aud=e,this}setJti(e){return J(`jti`,e),$(this).jti=e,this}setNotBefore(e){return $(this).nbf=Y(e,`setNotBefore`),this}setExpirationTime(e){return $(this).exp=Y(e,`setExpirationTime`),this}setIssuedAt(e){let t=$(this);return t.iat=e===void 0?U(/* @__PURE__ */ new Date):typeof e==`string`?q(`setIssuedAt`,U(/* @__PURE__ */ new Date)+K(e)):Y(e,`setIssuedAt`),this}};async function nt(t,n,r){let[a,o,s,c]=t,u=``;if(o!==void 0){let e=T(b,o);o=e[0],u=C(e[1])}if(s!==void 0&&(s=T(b,s)[0]),!o&&!s)throw new b(`either setProtectedHeader or setUnprotectedHeader must be called before #sign()`);if(!ce(o,s))throw new b(`JWS Protected and JWS Unprotected Header Parameter names must be disjoint`);let d={...o,...s};ge(b,o);let f=ve(o,_e(b,pe,c,o,d));f||r?.();let{alg:p}=d;if(typeof p!=`string`||!p)throw new b(`JWS "alg" (Algorithm) Header Parameter missing or invalid`);let m=Ke(p),h=``,g=a,_;if(f){let n=t[4];n?(h=n[0]??=C(a),g=n[1]??=l(h)):(h=C(a),_=e.encode(`${u}.${h}`))}_??=i(l(u),l(`.`),g);let v=await Ie(await xe(m,n,`sign`),m.subtle,`sign`);m.minRsaBits&&Ae(m.alg,v);let y={signature:C(new Uint8Array(await crypto.subtle.sign(m.signing,v,_))),payload:h};return o&&(y.protected=u),s&&(y.header=s),[y,f]}async function rt(e,t,n,r,i){let[a]=await nt([e,t,void 0,n],r,i);return`${a.protected}.${a.payload}.${a.signature}`}const it=tt;var at=class extends it{#e;setProtectedHeader(e){return le(this.#e,`setProtectedHeader`),this.#e=e,this}async sign(e,t){return rt($e(this),this.#e,t?.crit,e,()=>{throw new x(`JWTs MUST NOT use unencoded payload`)})}};function ot(e=`JWK "alg" (Algorithm) Parameter`){throw new _(`Invalid or unsupported ${e} value`)}function st(e,t){return(typeof e==`string`?H[e]??F[e]:void 0)??ot(t)}async function ct(e){if(we(e)){if(e.type===`secret`)e=e.export();else return e.export({format:`jwk`})}if(e instanceof Uint8Array)return{kty:`oct`,k:C(e)};if(!D(e))throw TypeError(De(e,`CryptoKey`,`KeyObject`,`Uint8Array`));if(!e.extractable)throw TypeError(`non-extractable CryptoKey cannot be exported as a JWK`);let t=await crypto.subtle.exportKey(`jwk`,e);delete t.ext,delete t.key_ops,delete t.use,t.kty!==`AKP`&&delete t.alg;for(let e of Object.keys(t))t[e]===void 0&&delete t[e];return t}function lt(e){let t=e?.modulusLength??2048;if(typeof t!=`number`||!Number.isInteger(t)||t<2048)throw new _(`Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used`);return t}async function ut(e,t){let n=Pe(t?.extractable),r=st(e,`"alg" (Algorithm)`);r.secret&&ot(`"alg" (Algorithm)`);let i;if(r.resolve){let e=t?.crv??`P-256`;if(![`P-256`,`P-384`,`P-521`,`X25519`].includes(e))throw new _(`Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, and X25519`);i=r.resolve({crv:e})}else{if(r.crv!==void 0&&t?.crv!==void 0&&t.crv!==r.crv)throw new _(`Invalid or unsupported crv option provided, the only supported value for ${e} is ${r.crv}`);i=r.kty[0]===`RSA`?{...r.subtle,publicExponent:Uint8Array.of(1,0,1),modulusLength:lt(t)}:r.subtle}return crypto.subtle.generateKey(i,n??!1,[...r.usages[1],...r.usages[0]])}export{ne as $,le as A,ve as B,Ne as C,Pe as D,Me as E,se as F,g as G,ge as H,w as I,v as J,p as K,fe as L,ue as M,de as N,me as O,ce as P,re as Q,T as R,Fe as S,Ie as T,S as U,_e as V,C as W,ee as X,y as Y,te as Z,Ae as _,tt as a,x as at,D as b,Qe as c,f as ct,F as d,s as dt,ie as et,Ue as f,o as ft,je as g,Ce as h,at as i,h as it,oe as j,pe as k,H as l,l as lt,We as m,ct as n,ae as nt,et as o,i as ot,He as p,_ as q,st as r,m as rt,$e as s,t as st,ut as t,b as tt,Ke as u,n as ut,ke as v,xe as w,Te as x,De as y,he as z};
package/dist/index.d.ts CHANGED
@@ -8,22 +8,63 @@ export interface DisposableIssuerOptions {
8
8
  * `http://127.0.0.1:<port>` and the forward-auth routes are not served.
9
9
  */
10
10
  cookieDomain?: string;
11
+ /**
12
+ * Keep the issuer's state in this directory instead of a temporary one: the owner
13
+ * account and password, the signing secret, the registered client and the database
14
+ * survive `close()` and the next start. Provisioning is idempotent, so the same
15
+ * resources and client are reused. The directory is never deleted. Treat it as the
16
+ * whole secret of the workspace, and keep it out of version control.
17
+ */
18
+ dataDir?: string;
19
+ /**
20
+ * Listen on this port instead of a random one. A persistent workspace needs a fixed
21
+ * one, because the origin is what browser storage, cookies and registered redirect
22
+ * URIs are keyed by. Default `0`.
23
+ */
24
+ port?: number;
11
25
  /** Test hook replacing outbound CIMD metadata retrieval. Defaults to the secure transport. */
12
26
  cimdTransport?: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>;
13
27
  /** Test hook observing incoming HTTP requests, including provisioning. No headers or bodies. */
14
28
  onRequest?: (request: { method: string; url: URL }) => void;
15
29
  }
16
30
 
31
+ /** What a persistent workspace keeps between runs. Written by this package only. */
32
+ export interface Identity {
33
+ email: string;
34
+ password: string;
35
+ secret: string;
36
+ clientId?: string;
37
+ clientSecret?: string;
38
+ }
39
+
40
+ /** A browser-equivalent owner session: the forward cookie, as a header and as pairs. */
41
+ export interface OwnerSession {
42
+ cookie: string;
43
+ cookies: Array<{ name: string; value: string }>;
44
+ }
45
+
17
46
  export interface DisposableIssuer {
18
47
  issuer: string;
19
48
  clientId: string;
20
49
  clientSecret: string;
21
50
  owner: { email: string; password: string };
22
51
  url: string;
52
+ /** The port the issuer listens on, which is also the apps' port under a cookie domain. */
53
+ port: number;
23
54
  directory: string;
24
- /** Drain requests, close SQLite, and remove all temporary identity state. Idempotent. */
55
+ /** Mint a scoped API key and return its secret. Permissions are resource identifier to scopes. */
56
+ apiKey(options: { name?: string; permissions: Record<string, string[]> }): Promise<string>;
57
+ /**
58
+ * Sign the owner in and seal the forward cookie for `appOrigin`, exactly as a browser
59
+ * does through `/forward-auth/continue`. Needs `cookieDomain`. The `cookies` array is
60
+ * what a browser automation harness adds to a context.
61
+ */
62
+ ownerSession(appOrigin: string): Promise<OwnerSession>;
63
+ /** An access token for one resource, obtained through the same forward-auth check. */
64
+ ownerToken(options: { resource: string; appOrigin: string }): Promise<string>;
65
+ /** Drain requests and close SQLite. Removes the directory only when it is temporary. Idempotent. */
25
66
  close(): Promise<void>;
26
67
  }
27
68
 
28
- /** Start a fresh issuer on a random loopback port. The caller owns signals and must await close(). */
69
+ /** Start an issuer on a loopback port. The caller owns signals and must await close(). */
29
70
  export function startDisposableIssuer(options: DisposableIssuerOptions): Promise<DisposableIssuer>;