@invokable/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Invokable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @invokable/server
2
+
3
+ Self-hostable device-code auth for [invokable](https://github.com/beinvokable/invokable)
4
+ tools. Implements the five endpoints `@invokable/core`'s `login` command talks to.
5
+
6
+ ## Use
7
+
8
+ ```js
9
+ import { createServer } from 'node:http';
10
+ import { invokableAuth, memoryStore } from '@invokable/server';
11
+ import { nodeListener } from '@invokable/server/node';
12
+
13
+ const handler = invokableAuth({
14
+ store: memoryStore(), // swap for a durable store
15
+ tokenPrefix: 'mtl',
16
+ requireSession: (request) => getUserFromCookie(request), // your login
17
+ approvePage: ({ device, user }) => renderMyBrandedPage(device, user),
18
+ tokenTtl: null, // long-lived; revocation only
19
+ });
20
+
21
+ createServer(nodeListener(handler)).listen(8787);
22
+ ```
23
+
24
+ `invokableAuth` returns a fetch-style `(Request) => Promise<Response | null>`.
25
+ `null` means "not my route", so it composes with your own routing. Hono, Deno and
26
+ workers can call it directly; `./node` adapts it to `node:http` and Express
27
+ (`expressMiddleware`).
28
+
29
+ A runnable version is in
30
+ [`examples/server`](../../examples/server/server.mjs).
31
+
32
+ ## Endpoints
33
+
34
+ | Method | Path | Purpose |
35
+ |---|---|---|
36
+ | `POST` | `/device/start` | Issues a device code + user code |
37
+ | `GET` | `/device?code=…` | The approval page a human sees |
38
+ | `POST` | `/device/approve` | Records the decision — **requires a session** |
39
+ | `POST` | `/device/token` | Polled by the CLI until approved |
40
+ | `GET` | `/cli/whoami` | Identity behind the bearer token |
41
+ | `POST` | `/cli/logout` | Revokes the bearer token |
42
+
43
+ ## What it does about security
44
+
45
+ - **Tokens are stored hashed.** `<prefix>_<32 chars base62>` is returned to the
46
+ client once; only a SHA-256 digest is persisted. A dump of the store does not
47
+ yield working credentials.
48
+ - **Device codes are single-use.** Issuing a token marks the device `consumed`,
49
+ so a leaked device code cannot be replayed into a second credential.
50
+ - **Polling is rate-limited.** Polling faster than the advertised interval gets
51
+ `slow_down`, not a token.
52
+ - **Approval requires a session.** `requireSession` returning null means nothing
53
+ can be approved, and the endpoint answers 401.
54
+ - **User codes avoid ambiguous characters** (no `0/O/1/I/L`): they get read aloud
55
+ and typed by hand.
56
+
57
+ ## What it does *not* do — read before deploying
58
+
59
+ **Device-code phishing is the attack this flow is exposed to.** An attacker
60
+ starts a login on their own machine, sends you the code, and asks you to approve
61
+ it; the token is then issued to *them*, against *your* identity. Nothing in the
62
+ protocol prevents this — the defence is the user recognising a login they did not
63
+ start.
64
+
65
+ The default approval page therefore shows the tool, version, and hostname the
66
+ device reported, and says plainly that you should only approve a login you just
67
+ started. **If you supply your own `approvePage`, keep that.** A page that shows
68
+ only a code and an Approve button is materially less safe.
69
+
70
+ Also not handled here, and yours to add:
71
+
72
+ - **CSRF on `/device/approve`.** The handler checks a session, not a CSRF token.
73
+ If you serve the default form-based page from a cookie-authenticated origin,
74
+ add your framework's CSRF protection.
75
+ - **Rate limiting on `/device/start`.** Nothing stops an attacker minting device
76
+ codes in bulk.
77
+ - **Durable storage.** `memoryStore()` loses every token on restart. It is for
78
+ development and tests. A Postgres store is on the roadmap.
79
+ - **Audit logging.** The store records who approved what and when; surfacing it
80
+ is the host application's job.
@@ -0,0 +1,40 @@
1
+ import { CheckpointVerifier, type CheckpointFailure } from './checkpoints.js';
2
+ export interface CheckpointRoutesOptions {
3
+ verifier: CheckpointVerifier;
4
+ /**
5
+ * Identifies the caller from the request, so a fingerprint issued to one
6
+ * subject cannot be verified by another. Returning null still allows
7
+ * issuance; supply it in production.
8
+ */
9
+ identify?: (request: Request) => string | null | Promise<string | null>;
10
+ }
11
+ /** Maps a verification failure onto the 409 the client turns into exit 12. */
12
+ export declare function staleResponse(reason: CheckpointFailure, retryCommand?: string, detail?: string): Response;
13
+ /**
14
+ * `POST /checkpoints` (issue) and `POST /checkpoints/verify` (non-consuming
15
+ * check). Returns null for paths it does not own.
16
+ *
17
+ * Verification here is deliberately NOT consuming: the approval is burned by
18
+ * `verifyCheckpoint` on the action request itself, so the approval is spent by
19
+ * the operation it authorised rather than by a preflight call.
20
+ */
21
+ export declare function checkpointRoutes(options: CheckpointRoutesOptions): (request: Request) => Promise<Response | null>;
22
+ export interface VerifyCheckpointOptions {
23
+ verifier: CheckpointVerifier;
24
+ /**
25
+ * Which requests require an approval. Defaults to every non-GET request,
26
+ * which is safe-by-default but usually too broad — narrow it to the routes
27
+ * that actually spend.
28
+ */
29
+ requiresApproval?: (request: Request) => boolean;
30
+ /** Recomputes the subject for the incoming request, to bind gate to target. */
31
+ subjectFor?: (request: Request) => string | Promise<string>;
32
+ }
33
+ /**
34
+ * Middleware that performs steps 1-4 of spec 5.8 on the action request, burning
35
+ * the approval as the action is authorised.
36
+ *
37
+ * Returns a 409 Response to reject, or null to let the request proceed.
38
+ */
39
+ export declare function verifyCheckpoint(options: VerifyCheckpointOptions): (request: Request) => Promise<Response | null>;
40
+ //# sourceMappingURL=checkpoint-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-routes.d.ts","sourceRoot":"","sources":["../src/checkpoint-routes.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAGlB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzE;AAkBD,8EAA8E;AAC9E,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,EACzB,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,CAaV;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,IAGlC,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA+CzE;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACjD,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,IAOnC,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA0BxE"}
@@ -0,0 +1,112 @@
1
+ import { stableStringify } from './stable-json.js';
2
+ import { CheckpointVerifier, hashSummary, parseCheckpointHeader, } from './checkpoints.js';
3
+ function json(body, status = 200) {
4
+ return new Response(JSON.stringify(body), {
5
+ status,
6
+ headers: { 'content-type': 'application/json' },
7
+ });
8
+ }
9
+ const STALE_MESSAGE = {
10
+ not_found: 'No approval matches that fingerprint.',
11
+ gate_mismatch: 'That approval was issued for a different gate.',
12
+ subject_mismatch: 'That approval was issued for a different target.',
13
+ mismatch: 'The plan changed since this approval was issued.',
14
+ expired: 'This approval has expired.',
15
+ consumed: 'This approval was already used.',
16
+ };
17
+ /** Maps a verification failure onto the 409 the client turns into exit 12. */
18
+ export function staleResponse(reason, retryCommand, detail) {
19
+ return json({
20
+ error: 'checkpoint_stale',
21
+ code: 'checkpoint_stale',
22
+ // The detail names a wiring mistake precisely; without it a subject
23
+ // mismatch is indistinguishable from a forged fingerprint.
24
+ message: detail ? `${STALE_MESSAGE[reason]} (${detail})` : STALE_MESSAGE[reason],
25
+ reason,
26
+ remediation: retryCommand ?? 'Re-run the command without --approve to get a fresh plan.',
27
+ }, 409);
28
+ }
29
+ /**
30
+ * `POST /checkpoints` (issue) and `POST /checkpoints/verify` (non-consuming
31
+ * check). Returns null for paths it does not own.
32
+ *
33
+ * Verification here is deliberately NOT consuming: the approval is burned by
34
+ * `verifyCheckpoint` on the action request itself, so the approval is spent by
35
+ * the operation it authorised rather than by a preflight call.
36
+ */
37
+ export function checkpointRoutes(options) {
38
+ const { verifier, identify } = options;
39
+ return async function handle(request) {
40
+ const url = new URL(request.url);
41
+ const path = url.pathname.replace(/\/+$/, '') || '/';
42
+ const method = request.method.toUpperCase();
43
+ if (path.endsWith('/checkpoints') && method === 'POST') {
44
+ const body = (await request.json().catch(() => ({})));
45
+ const gate = String(body['gate'] ?? '');
46
+ if (!gate)
47
+ return json({ error: 'usage', message: '`gate` is required.' }, 400);
48
+ const subject = String(body['subject'] ?? '');
49
+ const issuedTo = (await identify?.(request)) ?? undefined;
50
+ const record = await verifier.issue({
51
+ gate,
52
+ subject,
53
+ summaryHash: hashSummary(stableStringify(body['summary'] ?? null)),
54
+ ...(issuedTo !== undefined ? { issuedTo } : {}),
55
+ });
56
+ return json({
57
+ fingerprint: record.fingerprint,
58
+ gate: record.gate,
59
+ expiresAt: new Date(record.expiresAt).toISOString(),
60
+ });
61
+ }
62
+ if (path.endsWith('/checkpoints/verify') && method === 'POST') {
63
+ const body = (await request.json().catch(() => ({})));
64
+ const gate = String(body['gate'] ?? '');
65
+ const subject = String(body['subject'] ?? '');
66
+ const fingerprint = String(body['fingerprint'] ?? '');
67
+ const expected = body['summaryHash'];
68
+ const result = await verifier.verify({
69
+ gate,
70
+ subject,
71
+ fingerprint,
72
+ ...(typeof expected === 'string' ? { expectedSummaryHash: expected } : {}),
73
+ });
74
+ if (!result.ok)
75
+ return staleResponse(result.reason ?? 'not_found', undefined, result.detail);
76
+ return json({ valid: true, gate, expiresAt: new Date(result.record.expiresAt).toISOString() });
77
+ }
78
+ return null;
79
+ };
80
+ }
81
+ /**
82
+ * Middleware that performs steps 1-4 of spec 5.8 on the action request, burning
83
+ * the approval as the action is authorised.
84
+ *
85
+ * Returns a 409 Response to reject, or null to let the request proceed.
86
+ */
87
+ export function verifyCheckpoint(options) {
88
+ const { verifier, requiresApproval = (req) => req.method.toUpperCase() !== 'GET', subjectFor, } = options;
89
+ return async function check(request) {
90
+ if (!requiresApproval(request))
91
+ return null;
92
+ const header = parseCheckpointHeader(request.headers.get('x-invokable-checkpoint'));
93
+ if (!header) {
94
+ return json({
95
+ error: 'checkpoint_required',
96
+ code: 'checkpoint_stale',
97
+ message: 'This action requires an approval fingerprint.',
98
+ remediation: 'Re-run the command without --approve to get a fresh plan.',
99
+ }, 409);
100
+ }
101
+ const subject = subjectFor ? await subjectFor(request) : '';
102
+ const result = await verifier.consume({
103
+ gate: header.gate,
104
+ subject,
105
+ fingerprint: header.fingerprint,
106
+ });
107
+ if (!result.ok)
108
+ return staleResponse(result.reason ?? 'not_found', undefined, result.detail);
109
+ return null;
110
+ };
111
+ }
112
+ //# sourceMappingURL=checkpoint-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-routes.js","sourceRoot":"","sources":["../src/checkpoint-routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ export interface FingerprintInput {
2
+ gate: string;
3
+ subject: string;
4
+ summaryHash: string;
5
+ issuedAt: number;
6
+ }
7
+ /**
8
+ * 16 base32 characters = 80 bits. Sufficient because a fingerprint is scoped to
9
+ * (gate, subject), is consumed once, and expires — it is not a bearer secret.
10
+ */
11
+ export declare function computeFingerprint(secret: string, input: FingerprintInput): string;
12
+ export declare function hashSummary(canonical: string): string;
13
+ export interface CheckpointRecord {
14
+ fingerprint: string;
15
+ gate: string;
16
+ subject: string;
17
+ summaryHash: string;
18
+ issuedAt: number;
19
+ expiresAt: number;
20
+ consumed: boolean;
21
+ consumedAt?: number;
22
+ /** Whoever the token belonged to when the checkpoint was issued. */
23
+ issuedTo?: string;
24
+ }
25
+ export interface CheckpointStore {
26
+ createCheckpoint(record: CheckpointRecord): Promise<void>;
27
+ /**
28
+ * Looks up by fingerprint alone. The gate and subject are compared by the
29
+ * verifier rather than folded into the query, so that an approval issued for
30
+ * a different target can be reported as such instead of as "unknown".
31
+ */
32
+ findCheckpoint(fingerprint: string): Promise<CheckpointRecord | null>;
33
+ consumeCheckpoint(fingerprint: string, at: number): Promise<boolean>;
34
+ }
35
+ export declare function memoryCheckpointStore(): CheckpointStore & {
36
+ _records: Map<string, CheckpointRecord>;
37
+ };
38
+ export type CheckpointFailure = 'not_found' | 'gate_mismatch' | 'subject_mismatch' | 'mismatch' | 'expired' | 'consumed';
39
+ export interface VerifyResult {
40
+ ok: boolean;
41
+ reason?: CheckpointFailure;
42
+ record?: CheckpointRecord;
43
+ /** Extra context for a mismatch, safe to show a developer. */
44
+ detail?: string;
45
+ }
46
+ export interface CheckpointVerifierOptions {
47
+ secret: string;
48
+ /** Honoured for 24h after rotation, so in-flight approvals keep working. */
49
+ previousSecret?: string;
50
+ store: CheckpointStore;
51
+ /** Default 24 hours (spec 5.8). */
52
+ ttlMs?: number;
53
+ now?: () => number;
54
+ }
55
+ export declare class CheckpointVerifier {
56
+ private readonly secret;
57
+ private readonly previousSecret;
58
+ private readonly store;
59
+ private readonly ttlMs;
60
+ private readonly now;
61
+ constructor(options: CheckpointVerifierOptions);
62
+ issue(input: {
63
+ gate: string;
64
+ subject: string;
65
+ summaryHash: string;
66
+ issuedTo?: string;
67
+ }): Promise<CheckpointRecord>;
68
+ /**
69
+ * Steps 1-3 of spec 5.8: the record exists, the MAC still recomputes from
70
+ * stored state, and it is neither expired nor already consumed.
71
+ *
72
+ * `expectedSummaryHash` is the caller's view of current state. Supplying it
73
+ * is what makes an approval *stale* when the plan changed underneath.
74
+ */
75
+ verify(input: {
76
+ gate: string;
77
+ subject: string;
78
+ fingerprint: string;
79
+ expectedSummaryHash?: string;
80
+ }): Promise<VerifyResult>;
81
+ /** Step 4: verify, then burn. Returns false if it could not be consumed. */
82
+ consume(input: {
83
+ gate: string;
84
+ subject: string;
85
+ fingerprint: string;
86
+ expectedSummaryHash?: string;
87
+ }): Promise<VerifyResult>;
88
+ }
89
+ export declare function parseCheckpointHeader(value: string | null): {
90
+ gate: string;
91
+ fingerprint: string;
92
+ } | null;
93
+ //# sourceMappingURL=checkpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoints.d.ts","sourceRoot":"","sources":["../src/checkpoints.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAKlF;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAErD;AASD,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D;;;;OAIG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACtE,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,wBAAgB,qBAAqB,IAAI,eAAe,GAAG;IACzD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACzC,CAoBA;AAED,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,eAAe,GACf,kBAAkB,GAClB,UAAU,GACV,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;IACvB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,EAAE,yBAAyB;IAWxC,KAAK,CAAC,KAAK,EAAE;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuB7B;;;;;;OAMG;IACG,MAAM,CAAC,KAAK,EAAE;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,GAAG,OAAO,CAAC,YAAY,CAAC;IAuDzB,4EAA4E;IACtE,OAAO,CAAC,KAAK,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,GAAG,OAAO,CAAC,YAAY,CAAC;CAO1B;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAK9C"}
@@ -0,0 +1,179 @@
1
+ import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
2
+ /**
3
+ * Server-issued checkpoint fingerprints (spec 5.8).
4
+ *
5
+ * The fingerprint is an HMAC, not a hash of the summary, so it cannot be
6
+ * computed by anyone who merely saw the summary — including the agent. What it
7
+ * buys is freshness (an approval cannot outlive the state it described) and
8
+ * single use (an approval cannot be replayed). It does not contain a hostile
9
+ * agent: see the security note in the package README.
10
+ */
11
+ const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
12
+ function base32(buffer) {
13
+ let bits = 0;
14
+ let value = 0;
15
+ let out = '';
16
+ for (const byte of buffer) {
17
+ value = (value << 8) | byte;
18
+ bits += 8;
19
+ while (bits >= 5) {
20
+ out += BASE32[(value >>> (bits - 5)) & 31];
21
+ bits -= 5;
22
+ }
23
+ }
24
+ if (bits > 0)
25
+ out += BASE32[(value << (5 - bits)) & 31];
26
+ return out;
27
+ }
28
+ /**
29
+ * 16 base32 characters = 80 bits. Sufficient because a fingerprint is scoped to
30
+ * (gate, subject), is consumed once, and expires — it is not a bearer secret.
31
+ */
32
+ export function computeFingerprint(secret, input) {
33
+ const mac = createHmac('sha256', secret)
34
+ .update(`${input.gate}|${input.subject}|${input.summaryHash}|${input.issuedAt}`, 'utf8')
35
+ .digest();
36
+ return base32(mac).slice(0, 16);
37
+ }
38
+ export function hashSummary(canonical) {
39
+ return createHash('sha256').update(canonical, 'utf8').digest('hex');
40
+ }
41
+ function constantTimeEqual(a, b) {
42
+ const bufA = Buffer.from(a, 'utf8');
43
+ const bufB = Buffer.from(b, 'utf8');
44
+ if (bufA.length !== bufB.length)
45
+ return false;
46
+ return timingSafeEqual(bufA, bufB);
47
+ }
48
+ export function memoryCheckpointStore() {
49
+ const records = new Map();
50
+ return {
51
+ _records: records,
52
+ async createCheckpoint(record) {
53
+ records.set(record.fingerprint, record);
54
+ },
55
+ async findCheckpoint(fingerprint) {
56
+ return records.get(fingerprint) ?? null;
57
+ },
58
+ async consumeCheckpoint(fingerprint, at) {
59
+ const found = records.get(fingerprint);
60
+ // Consumption must be atomic: two concurrent requests carrying the same
61
+ // fingerprint must not both succeed. A real store does this with a
62
+ // conditional update; the Map is single-threaded, so the check suffices.
63
+ if (!found || found.consumed)
64
+ return false;
65
+ records.set(fingerprint, { ...found, consumed: true, consumedAt: at });
66
+ return true;
67
+ },
68
+ };
69
+ }
70
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
71
+ export class CheckpointVerifier {
72
+ secret;
73
+ previousSecret;
74
+ store;
75
+ ttlMs;
76
+ now;
77
+ constructor(options) {
78
+ if (!options.secret) {
79
+ throw new TypeError('CheckpointVerifier requires a secret.');
80
+ }
81
+ this.secret = options.secret;
82
+ this.previousSecret = options.previousSecret;
83
+ this.store = options.store;
84
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
85
+ this.now = options.now ?? (() => Date.now());
86
+ }
87
+ async issue(input) {
88
+ const issuedAt = this.now();
89
+ const fingerprint = computeFingerprint(this.secret, {
90
+ gate: input.gate,
91
+ subject: input.subject,
92
+ summaryHash: input.summaryHash,
93
+ issuedAt,
94
+ });
95
+ const record = {
96
+ fingerprint,
97
+ gate: input.gate,
98
+ subject: input.subject,
99
+ summaryHash: input.summaryHash,
100
+ issuedAt,
101
+ expiresAt: issuedAt + this.ttlMs,
102
+ consumed: false,
103
+ ...(input.issuedTo !== undefined ? { issuedTo: input.issuedTo } : {}),
104
+ };
105
+ await this.store.createCheckpoint(record);
106
+ return record;
107
+ }
108
+ /**
109
+ * Steps 1-3 of spec 5.8: the record exists, the MAC still recomputes from
110
+ * stored state, and it is neither expired nor already consumed.
111
+ *
112
+ * `expectedSummaryHash` is the caller's view of current state. Supplying it
113
+ * is what makes an approval *stale* when the plan changed underneath.
114
+ */
115
+ async verify(input) {
116
+ const record = await this.store.findCheckpoint(input.fingerprint);
117
+ if (!record)
118
+ return { ok: false, reason: 'not_found' };
119
+ // Reported separately from "not found". A gate or subject that disagrees
120
+ // between the CLI and the middleware is a wiring mistake, and calling it an
121
+ // unknown fingerprint sends the integrator looking in the wrong place.
122
+ if (record.gate !== input.gate) {
123
+ return {
124
+ ok: false,
125
+ reason: 'gate_mismatch',
126
+ record,
127
+ detail: `issued for gate "${record.gate}", presented for "${input.gate}"`,
128
+ };
129
+ }
130
+ if (record.subject !== input.subject) {
131
+ return {
132
+ ok: false,
133
+ reason: 'subject_mismatch',
134
+ record,
135
+ detail: `issued for subject ${JSON.stringify(record.subject)}, presented for ` +
136
+ `${JSON.stringify(input.subject)} — the \`subject\` passed to checkpoint() must ` +
137
+ 'equal what `subjectFor` returns in verifyCheckpoint()',
138
+ };
139
+ }
140
+ const recomputes = [this.secret, this.previousSecret]
141
+ .filter((s) => Boolean(s))
142
+ .some((secret) => constantTimeEqual(computeFingerprint(secret, {
143
+ gate: record.gate,
144
+ subject: record.subject,
145
+ summaryHash: record.summaryHash,
146
+ issuedAt: record.issuedAt,
147
+ }), input.fingerprint));
148
+ if (!recomputes)
149
+ return { ok: false, reason: 'mismatch', record };
150
+ if (input.expectedSummaryHash !== undefined &&
151
+ !constantTimeEqual(record.summaryHash, input.expectedSummaryHash)) {
152
+ return { ok: false, reason: 'mismatch', record };
153
+ }
154
+ if (record.expiresAt <= this.now())
155
+ return { ok: false, reason: 'expired', record };
156
+ if (record.consumed)
157
+ return { ok: false, reason: 'consumed', record };
158
+ return { ok: true, record };
159
+ }
160
+ /** Step 4: verify, then burn. Returns false if it could not be consumed. */
161
+ async consume(input) {
162
+ const result = await this.verify(input);
163
+ if (!result.ok)
164
+ return result;
165
+ const consumed = await this.store.consumeCheckpoint(input.fingerprint, this.now());
166
+ if (!consumed)
167
+ return { ok: false, reason: 'consumed', ...(result.record ? { record: result.record } : {}) };
168
+ return result;
169
+ }
170
+ }
171
+ export function parseCheckpointHeader(value) {
172
+ if (!value)
173
+ return null;
174
+ const at = value.lastIndexOf('@');
175
+ if (at <= 0 || at === value.length - 1)
176
+ return null;
177
+ return { gate: value.slice(0, at), fingerprint: value.slice(at + 1) };
178
+ }
179
+ //# sourceMappingURL=checkpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoints.js","sourceRoot":"","sources":["../src/checkpoints.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { AuthStore, DeviceRecord } from './store.js';
2
+ export interface SessionUser {
3
+ subject: string;
4
+ orgId?: string;
5
+ displayName?: string;
6
+ }
7
+ export interface ApprovePageContext {
8
+ userCode: string;
9
+ /** Null when no device is pending for that code. */
10
+ device: DeviceRecord | null;
11
+ user: SessionUser | null;
12
+ }
13
+ export interface InvokableAuthOptions {
14
+ store: AuthStore;
15
+ /**
16
+ * Resolves the browser session on the approval page. Return null for a signed
17
+ * -out visitor; the handler will not approve anything without a user.
18
+ */
19
+ requireSession: (request: Request) => SessionUser | null | Promise<SessionUser | null>;
20
+ /** Renders the branded approval page. A plain default is used when omitted. */
21
+ approvePage?: (ctx: ApprovePageContext) => string | Promise<string>;
22
+ /** Token prefix, e.g. `mtl`. Appears in the credential and in `doctor`. */
23
+ tokenPrefix?: string;
24
+ /** Milliseconds; `null` means long-lived with revocation only. */
25
+ tokenTtl?: number | null;
26
+ /** How long a device code stays valid. Default 15 minutes. */
27
+ deviceCodeTtlMs?: number;
28
+ /** Minimum seconds between polls before answering `slow_down`. Default 5. */
29
+ pollIntervalSeconds?: number;
30
+ /** Injectable clock, for tests. */
31
+ now?: () => number;
32
+ }
33
+ /**
34
+ * The five device-flow endpoints of spec 5.4, as a fetch-style handler:
35
+ *
36
+ * POST /device/start → device + user code
37
+ * GET /device?code=… → approval page
38
+ * POST /device/approve → records the user's decision (session required)
39
+ * POST /device/token → polled by the CLI until approved
40
+ * POST /cli/logout → revokes the bearer token
41
+ * GET /cli/whoami → identity behind the bearer token
42
+ *
43
+ * Returns null for paths it does not own, so a host application can mount it
44
+ * alongside its own routes.
45
+ */
46
+ export declare function invokableAuth(options: InvokableAuthOptions): (request: Request) => Promise<Response | null>;
47
+ export type InvokableAuthHandler = ReturnType<typeof invokableAuth>;
48
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAe,MAAM,YAAY,CAAC;AAEvE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACvF,+EAA+E;IAC/E,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AA6DD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,IAsB5B,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAoJzE;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC"}