mrplex 0.0.1
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 +21 -0
- package/README.md +284 -0
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.js +48 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +13 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/format.d.ts +22 -0
- package/dist/cli/format.js +148 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/main.d.ts +16 -0
- package/dist/cli/main.js +1518 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/client/kernel-client.d.ts +97 -0
- package/dist/client/kernel-client.js +14 -0
- package/dist/client/kernel-client.js.map +1 -0
- package/dist/client/local.d.ts +26 -0
- package/dist/client/local.js +103 -0
- package/dist/client/local.js.map +1 -0
- package/dist/client/remote-mcp.d.ts +35 -0
- package/dist/client/remote-mcp.js +174 -0
- package/dist/client/remote-mcp.js.map +1 -0
- package/dist/embed/backfill.d.ts +24 -0
- package/dist/embed/backfill.js +33 -0
- package/dist/embed/backfill.js.map +1 -0
- package/dist/embed/chunker.d.ts +31 -0
- package/dist/embed/chunker.js +121 -0
- package/dist/embed/chunker.js.map +1 -0
- package/dist/embed/cmd-hook.d.ts +21 -0
- package/dist/embed/cmd-hook.js +100 -0
- package/dist/embed/cmd-hook.js.map +1 -0
- package/dist/embed/config.d.ts +61 -0
- package/dist/embed/config.js +109 -0
- package/dist/embed/config.js.map +1 -0
- package/dist/embed/hook.d.ts +38 -0
- package/dist/embed/hook.js +56 -0
- package/dist/embed/hook.js.map +1 -0
- package/dist/embed/http-hook.d.ts +13 -0
- package/dist/embed/http-hook.js +39 -0
- package/dist/embed/http-hook.js.map +1 -0
- package/dist/embed/worker.d.ts +62 -0
- package/dist/embed/worker.js +228 -0
- package/dist/embed/worker.js.map +1 -0
- package/dist/fixtures/hwe/characters/albion-ever.md +12 -0
- package/dist/fixtures/hwe/characters/cent.md +11 -0
- package/dist/fixtures/hwe/concepts/cent-demiurge-resonance.md +10 -0
- package/dist/fixtures/hwe/concepts/phase-looper.md +10 -0
- package/dist/fixtures/hwe/entities/demiurge.md +11 -0
- package/dist/fixtures/hwe/entities/monad.md +11 -0
- package/dist/fixtures/hwe/moc/globocorp-network.md +19 -0
- package/dist/fixtures/hwe/organizations/globocorp.md +11 -0
- package/dist/fixtures/hwe/readme.md +24 -0
- package/dist/fixtures/starship/crew/aria-okonkwo.md +14 -0
- package/dist/fixtures/starship/crew/bexley-orr.md +14 -0
- package/dist/fixtures/starship/crew/dax-thorne.md +14 -0
- package/dist/fixtures/starship/crew/isolde-marsh.md +16 -0
- package/dist/fixtures/starship/crew/kestrel-vance.md +15 -0
- package/dist/fixtures/starship/crew/quill-vasquez.md +14 -0
- package/dist/fixtures/starship/crew/soren-halloway.md +16 -0
- package/dist/fixtures/starship/encounters/the-drift-choir.md +14 -0
- package/dist/fixtures/starship/encounters/the-hollow-signal.md +16 -0
- package/dist/fixtures/starship/encounters/the-wandering-buoy.md +14 -0
- package/dist/fixtures/starship/equipment/coolant-loop-b.md +13 -0
- package/dist/fixtures/starship/equipment/emergency-ration-bars.md +12 -0
- package/dist/fixtures/starship/equipment/long-range-sensors.md +13 -0
- package/dist/fixtures/starship/equipment/plasma-manifold-3.md +15 -0
- package/dist/fixtures/starship/equipment/shuttle-corvid.md +13 -0
- package/dist/fixtures/starship/logs/marsh-4419-1.md +13 -0
- package/dist/fixtures/starship/logs/okonkwo-4415-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4413-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4420-1.md +12 -0
- package/dist/fixtures/starship/logs/thorne-4413-2.md +13 -0
- package/dist/fixtures/starship/logs/vance-4419-2.md +13 -0
- package/dist/fixtures/starship/misc/the-galley-ficus.md +11 -0
- package/dist/fixtures/starship/missions/the-cinder-run.md +17 -0
- package/dist/fixtures/starship/missions/the-drift-choir.md +17 -0
- package/dist/fixtures/starship/missions/the-hollow-signal.md +17 -0
- package/dist/fixtures/starship/missions/the-silent-beacon.md +17 -0
- package/dist/fixtures/starship/moc/crew.md +19 -0
- package/dist/fixtures/starship/moc/encounters.md +14 -0
- package/dist/fixtures/starship/moc/missions.md +15 -0
- package/dist/fixtures/starship/readme.md +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/auth/glob.d.ts +46 -0
- package/dist/kernel/auth/glob.js +112 -0
- package/dist/kernel/auth/glob.js.map +1 -0
- package/dist/kernel/auth/scope.d.ts +53 -0
- package/dist/kernel/auth/scope.js +83 -0
- package/dist/kernel/auth/scope.js.map +1 -0
- package/dist/kernel/casefold.d.ts +37 -0
- package/dist/kernel/casefold.js +44 -0
- package/dist/kernel/casefold.js.map +1 -0
- package/dist/kernel/constants.d.ts +21 -0
- package/dist/kernel/constants.js +22 -0
- package/dist/kernel/constants.js.map +1 -0
- package/dist/kernel/context.d.ts +70 -0
- package/dist/kernel/context.js +114 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/deletion.d.ts +50 -0
- package/dist/kernel/deletion.js +83 -0
- package/dist/kernel/deletion.js.map +1 -0
- package/dist/kernel/diff.d.ts +36 -0
- package/dist/kernel/diff.js +61 -0
- package/dist/kernel/diff.js.map +1 -0
- package/dist/kernel/errors.d.ts +37 -0
- package/dist/kernel/errors.js +55 -0
- package/dist/kernel/errors.js.map +1 -0
- package/dist/kernel/frontmatter-input.d.ts +32 -0
- package/dist/kernel/frontmatter-input.js +85 -0
- package/dist/kernel/frontmatter-input.js.map +1 -0
- package/dist/kernel/graph.d.ts +34 -0
- package/dist/kernel/graph.js +559 -0
- package/dist/kernel/graph.js.map +1 -0
- package/dist/kernel/history.d.ts +74 -0
- package/dist/kernel/history.js +143 -0
- package/dist/kernel/history.js.map +1 -0
- package/dist/kernel/kernel.d.ts +162 -0
- package/dist/kernel/kernel.js +675 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/kernel/path-config.d.ts +101 -0
- package/dist/kernel/path-config.js +171 -0
- package/dist/kernel/path-config.js.map +1 -0
- package/dist/kernel/query/ast.d.ts +24 -0
- package/dist/kernel/query/ast.js +52 -0
- package/dist/kernel/query/ast.js.map +1 -0
- package/dist/kernel/query/cel-parse.d.ts +37 -0
- package/dist/kernel/query/cel-parse.js +88 -0
- package/dist/kernel/query/cel-parse.js.map +1 -0
- package/dist/kernel/query/degrees.d.ts +31 -0
- package/dist/kernel/query/degrees.js +135 -0
- package/dist/kernel/query/degrees.js.map +1 -0
- package/dist/kernel/query/graph-ast.d.ts +51 -0
- package/dist/kernel/query/graph-ast.js +139 -0
- package/dist/kernel/query/graph-ast.js.map +1 -0
- package/dist/kernel/query/query.d.ts +56 -0
- package/dist/kernel/query/query.js +276 -0
- package/dist/kernel/query/query.js.map +1 -0
- package/dist/kernel/validation.d.ts +64 -0
- package/dist/kernel/validation.js +183 -0
- package/dist/kernel/validation.js.map +1 -0
- package/dist/kernel/version-id.d.ts +10 -0
- package/dist/kernel/version-id.js +21 -0
- package/dist/kernel/version-id.js.map +1 -0
- package/dist/kernel/wire.d.ts +148 -0
- package/dist/kernel/wire.js +6 -0
- package/dist/kernel/wire.js.map +1 -0
- package/dist/links/backfill.d.ts +28 -0
- package/dist/links/backfill.js +33 -0
- package/dist/links/backfill.js.map +1 -0
- package/dist/links/extract.d.ts +29 -0
- package/dist/links/extract.js +233 -0
- package/dist/links/extract.js.map +1 -0
- package/dist/links/link-config.d.ts +70 -0
- package/dist/links/link-config.js +107 -0
- package/dist/links/link-config.js.map +1 -0
- package/dist/links/maintain.d.ts +37 -0
- package/dist/links/maintain.js +91 -0
- package/dist/links/maintain.js.map +1 -0
- package/dist/links/repair.d.ts +46 -0
- package/dist/links/repair.js +96 -0
- package/dist/links/repair.js.map +1 -0
- package/dist/links/resolve.d.ts +41 -0
- package/dist/links/resolve.js +121 -0
- package/dist/links/resolve.js.map +1 -0
- package/dist/links/stale.d.ts +32 -0
- package/dist/links/stale.js +73 -0
- package/dist/links/stale.js.map +1 -0
- package/dist/markdown/content-hash.d.ts +34 -0
- package/dist/markdown/content-hash.js +46 -0
- package/dist/markdown/content-hash.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +69 -0
- package/dist/markdown/frontmatter.js +147 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/hash-backfill.d.ts +22 -0
- package/dist/markdown/hash-backfill.js +43 -0
- package/dist/markdown/hash-backfill.js.map +1 -0
- package/dist/mcp/query-syntax.d.ts +20 -0
- package/dist/mcp/query-syntax.js +194 -0
- package/dist/mcp/query-syntax.js.map +1 -0
- package/dist/mcp/render.d.ts +31 -0
- package/dist/mcp/render.js +110 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts +60 -0
- package/dist/mcp/server.js +170 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +93 -0
- package/dist/mcp/tools.js +1218 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/rest/conditional.d.ts +37 -0
- package/dist/rest/conditional.js +58 -0
- package/dist/rest/conditional.js.map +1 -0
- package/dist/rest/negotiate.d.ts +33 -0
- package/dist/rest/negotiate.js +54 -0
- package/dist/rest/negotiate.js.map +1 -0
- package/dist/rest/routes.d.ts +37 -0
- package/dist/rest/routes.js +744 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/seed/fixture-seed.d.ts +35 -0
- package/dist/seed/fixture-seed.js +75 -0
- package/dist/seed/fixture-seed.js.map +1 -0
- package/dist/seed/starship-config.d.ts +3 -0
- package/dist/seed/starship-config.js +5 -0
- package/dist/seed/starship-config.js.map +1 -0
- package/dist/server/headers.d.ts +36 -0
- package/dist/server/headers.js +30 -0
- package/dist/server/headers.js.map +1 -0
- package/dist/server/http-error.d.ts +53 -0
- package/dist/server/http-error.js +104 -0
- package/dist/server/http-error.js.map +1 -0
- package/dist/server/serve.d.ts +56 -0
- package/dist/server/serve.js +135 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/shell/audit.d.ts +32 -0
- package/dist/shell/audit.js +28 -0
- package/dist/shell/audit.js.map +1 -0
- package/dist/shell/guard.d.ts +50 -0
- package/dist/shell/guard.js +176 -0
- package/dist/shell/guard.js.map +1 -0
- package/dist/shell/keys.d.ts +36 -0
- package/dist/shell/keys.js +64 -0
- package/dist/shell/keys.js.map +1 -0
- package/dist/shell/login.d.ts +56 -0
- package/dist/shell/login.js +130 -0
- package/dist/shell/login.js.map +1 -0
- package/dist/shell/oidc.d.ts +62 -0
- package/dist/shell/oidc.js +102 -0
- package/dist/shell/oidc.js.map +1 -0
- package/dist/shell/policy.d.ts +100 -0
- package/dist/shell/policy.js +232 -0
- package/dist/shell/policy.js.map +1 -0
- package/dist/shell/proxy-policy.d.ts +34 -0
- package/dist/shell/proxy-policy.js +120 -0
- package/dist/shell/proxy-policy.js.map +1 -0
- package/dist/shell/proxy.d.ts +47 -0
- package/dist/shell/proxy.js +253 -0
- package/dist/shell/proxy.js.map +1 -0
- package/dist/shell/serve.d.ts +86 -0
- package/dist/shell/serve.js +0 -0
- package/dist/shell/serve.js.map +1 -0
- package/dist/shell/stdio.d.ts +55 -0
- package/dist/shell/stdio.js +63 -0
- package/dist/shell/stdio.js.map +1 -0
- package/dist/storage/registry.d.ts +20 -0
- package/dist/storage/registry.js +38 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/search-plan.d.ts +93 -0
- package/dist/storage/search-plan.js +15 -0
- package/dist/storage/search-plan.js.map +1 -0
- package/dist/storage/types.d.ts +448 -0
- package/dist/storage/types.js +14 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/versions-since.d.ts +72 -0
- package/dist/storage/versions-since.js +92 -0
- package/dist/storage/versions-since.js.map +1 -0
- package/dist/storage-postgres/adapter.d.ts +17 -0
- package/dist/storage-postgres/adapter.js +782 -0
- package/dist/storage-postgres/adapter.js.map +1 -0
- package/dist/storage-postgres/compile-postgres.d.ts +22 -0
- package/dist/storage-postgres/compile-postgres.js +830 -0
- package/dist/storage-postgres/compile-postgres.js.map +1 -0
- package/dist/storage-postgres/errors.d.ts +34 -0
- package/dist/storage-postgres/errors.js +50 -0
- package/dist/storage-postgres/errors.js.map +1 -0
- package/dist/storage-postgres/migrations/0001_init.sql +113 -0
- package/dist/storage-postgres/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-postgres/migrations/index.d.ts +22 -0
- package/dist/storage-postgres/migrations/index.js +70 -0
- package/dist/storage-postgres/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/adapter.d.ts +2 -0
- package/dist/storage-sqlite/adapter.js +688 -0
- package/dist/storage-sqlite/adapter.js.map +1 -0
- package/dist/storage-sqlite/compile-filter.d.ts +41 -0
- package/dist/storage-sqlite/compile-filter.js +881 -0
- package/dist/storage-sqlite/compile-filter.js.map +1 -0
- package/dist/storage-sqlite/compile-sqlite.d.ts +19 -0
- package/dist/storage-sqlite/compile-sqlite.js +176 -0
- package/dist/storage-sqlite/compile-sqlite.js.map +1 -0
- package/dist/storage-sqlite/migrations/0001_init.sql +123 -0
- package/dist/storage-sqlite/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-sqlite/migrations/index.d.ts +6 -0
- package/dist/storage-sqlite/migrations/index.js +41 -0
- package/dist/storage-sqlite/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/vec.d.ts +40 -0
- package/dist/storage-sqlite/vec.js +62 -0
- package/dist/storage-sqlite/vec.js.map +1 -0
- package/dist/sync/cursor.d.ts +36 -0
- package/dist/sync/cursor.js +66 -0
- package/dist/sync/cursor.js.map +1 -0
- package/dist/sync/daemon.d.ts +40 -0
- package/dist/sync/daemon.js +202 -0
- package/dist/sync/daemon.js.map +1 -0
- package/dist/sync/feed.d.ts +38 -0
- package/dist/sync/feed.js +125 -0
- package/dist/sync/feed.js.map +1 -0
- package/dist/sync/fs-store.d.ts +8 -0
- package/dist/sync/fs-store.js +76 -0
- package/dist/sync/fs-store.js.map +1 -0
- package/dist/sync/intrinsics.d.ts +52 -0
- package/dist/sync/intrinsics.js +76 -0
- package/dist/sync/intrinsics.js.map +1 -0
- package/dist/sync/paths.d.ts +38 -0
- package/dist/sync/paths.js +56 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/push.d.ts +48 -0
- package/dist/sync/push.js +328 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/reconcile.d.ts +54 -0
- package/dist/sync/reconcile.js +252 -0
- package/dist/sync/reconcile.js.map +1 -0
- package/dist/sync/run.d.ts +27 -0
- package/dist/sync/run.js +46 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log — auth-shell plan §1 (audit), decision 8; noauth-plan decision 7.
|
|
3
|
+
*
|
|
4
|
+
* Append-only JSONL, one line per authenticated call: `{ ts, principal, op,
|
|
5
|
+
* repo, path, outcome }` (+ engine `error` when the forwarded call failed).
|
|
6
|
+
* Written by the guard's audit sink — a single choke point, so no surface can
|
|
7
|
+
* forget to log. The engine's `author` column is *attribution* (who wrote the
|
|
8
|
+
* content); this log is *accountability* (who called the shell). Different
|
|
9
|
+
* facts, stored in different places on purpose.
|
|
10
|
+
*/
|
|
11
|
+
import type { AuditSink } from "./guard.js";
|
|
12
|
+
/** One serialized audit record. Field order is stable for greppability. */
|
|
13
|
+
export type AuditRecord = {
|
|
14
|
+
ts: string;
|
|
15
|
+
principal: string;
|
|
16
|
+
op: string;
|
|
17
|
+
repo?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
outcome: "ok" | "forbidden";
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Build an `AuditSink` bound to a principal that appends JSONL to `path`. The
|
|
24
|
+
* guard is principal-agnostic (it knows only the entitlement), so the principal
|
|
25
|
+
* is closed over here — one sink per authenticated session/request.
|
|
26
|
+
*
|
|
27
|
+
* Appends are synchronous: an audit record must be durable before the call's
|
|
28
|
+
* effect is observable, and the volume (one line per op) doesn't justify an
|
|
29
|
+
* async writer. A write failure throws — losing the audit trail silently is
|
|
30
|
+
* worse than failing the call.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fileAuditSink(path: string, principal: string): AuditSink;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log — auth-shell plan §1 (audit), decision 8; noauth-plan decision 7.
|
|
3
|
+
*
|
|
4
|
+
* Append-only JSONL, one line per authenticated call: `{ ts, principal, op,
|
|
5
|
+
* repo, path, outcome }` (+ engine `error` when the forwarded call failed).
|
|
6
|
+
* Written by the guard's audit sink — a single choke point, so no surface can
|
|
7
|
+
* forget to log. The engine's `author` column is *attribution* (who wrote the
|
|
8
|
+
* content); this log is *accountability* (who called the shell). Different
|
|
9
|
+
* facts, stored in different places on purpose.
|
|
10
|
+
*/
|
|
11
|
+
import { appendFileSync } from "node:fs";
|
|
12
|
+
/**
|
|
13
|
+
* Build an `AuditSink` bound to a principal that appends JSONL to `path`. The
|
|
14
|
+
* guard is principal-agnostic (it knows only the entitlement), so the principal
|
|
15
|
+
* is closed over here — one sink per authenticated session/request.
|
|
16
|
+
*
|
|
17
|
+
* Appends are synchronous: an audit record must be durable before the call's
|
|
18
|
+
* effect is observable, and the volume (one line per op) doesn't justify an
|
|
19
|
+
* async writer. A write failure throws — losing the audit trail silently is
|
|
20
|
+
* worse than failing the call.
|
|
21
|
+
*/
|
|
22
|
+
export function fileAuditSink(path, principal) {
|
|
23
|
+
return (event) => {
|
|
24
|
+
const record = { ts: new Date().toISOString(), principal, ...event };
|
|
25
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/shell/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAczC;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,SAAiB;IAC3D,OAAO,CAAC,KAAiB,EAAE,EAAE;QAC3B,MAAM,MAAM,GAAgB,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,CAAC;QAClF,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guardKernel — the access-and-identity decorator (auth-shell plan §1, WS2).
|
|
3
|
+
*
|
|
4
|
+
* The shell's core is NOT a proxy: it is an object with the same shape as the
|
|
5
|
+
* `Kernel` interface whose every method
|
|
6
|
+
* • stamps `ctx.author` from the entitlement (never trusts the caller's,
|
|
7
|
+
* unless `impersonate`),
|
|
8
|
+
* • forwards reads with `ctx.scope = entitlement.read` (the engine does the
|
|
9
|
+
* filtering — search ranking, pagination, graph traversal stay kernel-side),
|
|
10
|
+
* • enforces write policy against the TYPED call arguments using
|
|
11
|
+
* `entitlement.write` and the engine's own glob dialect,
|
|
12
|
+
* • gates destructive ops on `entitlement.destructive`,
|
|
13
|
+
* • and forwards to the wrapped kernel.
|
|
14
|
+
*
|
|
15
|
+
* The kernel never learns any of this exists. The shell imports only the
|
|
16
|
+
* kernel's public seam (`Kernel`, `CallContext`, `ScopeClaim`, the glob engine
|
|
17
|
+
* via `normalizeClaims`); nothing here reaches into engine internals, so the
|
|
18
|
+
* whole module could be extracted to its own package without touching engine
|
|
19
|
+
* code — that extractability is the test of the boundary (plan §intro).
|
|
20
|
+
*
|
|
21
|
+
* Write enforcement replays the old design §8.2 rules (plan decision 5):
|
|
22
|
+
* both-endpoints on moves, system-sigil endpoints skipped (the kernel's
|
|
23
|
+
* validatePath owns that namespace), and write does NOT imply read — so writes
|
|
24
|
+
* forward with no read-scope, or the engine's repo-visibility check would
|
|
25
|
+
* block a write to a repo the caller can't read (plan decision, §1 writes).
|
|
26
|
+
*/
|
|
27
|
+
import type { Kernel } from "../kernel/kernel.js";
|
|
28
|
+
import type { Entitlement } from "./policy.js";
|
|
29
|
+
/**
|
|
30
|
+
* One audited call. The guard supplies the operation-shaped fields; the sink's
|
|
31
|
+
* owner (embedded serve / stdio launcher) adds `ts` and `principal` — the guard
|
|
32
|
+
* is principal-agnostic, it only knows the compiled entitlement.
|
|
33
|
+
*/
|
|
34
|
+
export type AuditEvent = {
|
|
35
|
+
/** Dotted op name, e.g. `docs.put`, `repos.delete`, `query`. */
|
|
36
|
+
op: string;
|
|
37
|
+
repo?: string;
|
|
38
|
+
path?: string;
|
|
39
|
+
outcome: "ok" | "forbidden";
|
|
40
|
+
/** Kernel error code when the forwarded call threw (outcome stays "ok" — the
|
|
41
|
+
* shell allowed it; the engine rejected it for its own reasons). */
|
|
42
|
+
error?: string;
|
|
43
|
+
};
|
|
44
|
+
export type AuditSink = (event: AuditEvent) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a full-trust kernel in an entitlement. The returned object satisfies the
|
|
47
|
+
* `Kernel` interface exactly, so any surface (embedded HTTP, stdio MCP) mounts
|
|
48
|
+
* it unchanged.
|
|
49
|
+
*/
|
|
50
|
+
export declare function guardKernel(kernel: Kernel, entitlement: Entitlement, audit?: AuditSink): Kernel;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guardKernel — the access-and-identity decorator (auth-shell plan §1, WS2).
|
|
3
|
+
*
|
|
4
|
+
* The shell's core is NOT a proxy: it is an object with the same shape as the
|
|
5
|
+
* `Kernel` interface whose every method
|
|
6
|
+
* • stamps `ctx.author` from the entitlement (never trusts the caller's,
|
|
7
|
+
* unless `impersonate`),
|
|
8
|
+
* • forwards reads with `ctx.scope = entitlement.read` (the engine does the
|
|
9
|
+
* filtering — search ranking, pagination, graph traversal stay kernel-side),
|
|
10
|
+
* • enforces write policy against the TYPED call arguments using
|
|
11
|
+
* `entitlement.write` and the engine's own glob dialect,
|
|
12
|
+
* • gates destructive ops on `entitlement.destructive`,
|
|
13
|
+
* • and forwards to the wrapped kernel.
|
|
14
|
+
*
|
|
15
|
+
* The kernel never learns any of this exists. The shell imports only the
|
|
16
|
+
* kernel's public seam (`Kernel`, `CallContext`, `ScopeClaim`, the glob engine
|
|
17
|
+
* via `normalizeClaims`); nothing here reaches into engine internals, so the
|
|
18
|
+
* whole module could be extracted to its own package without touching engine
|
|
19
|
+
* code — that extractability is the test of the boundary (plan §intro).
|
|
20
|
+
*
|
|
21
|
+
* Write enforcement replays the old design §8.2 rules (plan decision 5):
|
|
22
|
+
* both-endpoints on moves, system-sigil endpoints skipped (the kernel's
|
|
23
|
+
* validatePath owns that namespace), and write does NOT imply read — so writes
|
|
24
|
+
* forward with no read-scope, or the engine's repo-visibility check would
|
|
25
|
+
* block a write to a repo the caller can't read (plan decision, §1 writes).
|
|
26
|
+
*/
|
|
27
|
+
import { claimsGrantRead as claimsCoverPath, normalizeClaims } from "../kernel/auth/scope.js";
|
|
28
|
+
import { pathIsInSystemNamespace } from "../kernel/deletion.js";
|
|
29
|
+
import { forbidden } from "../kernel/errors.js";
|
|
30
|
+
import { HARDCODED_DEFAULTS } from "../kernel/path-config.js";
|
|
31
|
+
/**
|
|
32
|
+
* Wrap a full-trust kernel in an entitlement. The returned object satisfies the
|
|
33
|
+
* `Kernel` interface exactly, so any surface (embedded HTTP, stdio MCP) mounts
|
|
34
|
+
* it unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export function guardKernel(kernel, entitlement, audit) {
|
|
37
|
+
const writeMatchers = normalizeClaims(entitlement.write);
|
|
38
|
+
const sigils = HARDCODED_DEFAULTS.system_sigils;
|
|
39
|
+
/** Identity stamped on writes — the caller's author only when impersonating. */
|
|
40
|
+
function authorFor(ctx) {
|
|
41
|
+
return entitlement.impersonate && ctx.author !== undefined ? ctx.author : entitlement.author;
|
|
42
|
+
}
|
|
43
|
+
/** Context for a read: the entitlement's read scope, nothing caller-supplied. */
|
|
44
|
+
function readCtx() {
|
|
45
|
+
return { scope: entitlement.read };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Context for a write/destructive op: the derived author and NO scope. Write
|
|
49
|
+
* does not imply read (plan decision 5), so forwarding a read scope here would
|
|
50
|
+
* make the engine's repo-visibility check reject a legitimate write.
|
|
51
|
+
*/
|
|
52
|
+
function writeCtx(ctx) {
|
|
53
|
+
return { author: authorFor(ctx) };
|
|
54
|
+
}
|
|
55
|
+
function emit(event) {
|
|
56
|
+
audit?.(event);
|
|
57
|
+
}
|
|
58
|
+
/** Enforce write policy on one endpoint. System-sigil paths are the kernel's
|
|
59
|
+
* to police (validatePath), so the shell skips them. */
|
|
60
|
+
function assertWritable(op, repo, path) {
|
|
61
|
+
if (pathIsInSystemNamespace(path, sigils))
|
|
62
|
+
return;
|
|
63
|
+
if (!claimsCoverPath(writeMatchers, repo, path)) {
|
|
64
|
+
emit({ op, repo, path, outcome: "forbidden" });
|
|
65
|
+
throw forbidden();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function assertDestructive(op, repo) {
|
|
69
|
+
if (!entitlement.destructive) {
|
|
70
|
+
emit({ op, repo, outcome: "forbidden" });
|
|
71
|
+
throw forbidden();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Forward a call, auditing the outcome (ok, or ok-with-engine-error). */
|
|
75
|
+
async function forward(op, meta, run) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await run();
|
|
78
|
+
emit({ op, ...meta, outcome: "ok" });
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
emit({ op, ...meta, outcome: "ok", error: errorCode(err) });
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
repos: {
|
|
88
|
+
list: (_ctx, opts) => forward("repos.list", {}, () => kernel.repos.list(readCtx(), opts)),
|
|
89
|
+
get: (_ctx, slug) => forward("repos.get", { repo: slug }, () => kernel.repos.get(readCtx(), slug)),
|
|
90
|
+
create: async (ctx, slug) => {
|
|
91
|
+
assertDestructive("repos.create", slug);
|
|
92
|
+
return forward("repos.create", { repo: slug }, () => kernel.repos.create(writeCtx(ctx), slug));
|
|
93
|
+
},
|
|
94
|
+
rename: async (ctx, slug, newSlug) => {
|
|
95
|
+
assertDestructive("repos.rename", slug);
|
|
96
|
+
return forward("repos.rename", { repo: slug }, () => kernel.repos.rename(writeCtx(ctx), slug, newSlug));
|
|
97
|
+
},
|
|
98
|
+
delete: async (ctx, slug) => {
|
|
99
|
+
assertDestructive("repos.delete", slug);
|
|
100
|
+
return forward("repos.delete", { repo: slug }, () => kernel.repos.delete(writeCtx(ctx), slug));
|
|
101
|
+
},
|
|
102
|
+
set_path_config: async (ctx, slug, config) => {
|
|
103
|
+
assertDestructive("repos.set_path_config", slug);
|
|
104
|
+
return forward("repos.set_path_config", { repo: slug }, () => kernel.repos.set_path_config(writeCtx(ctx), slug, config));
|
|
105
|
+
},
|
|
106
|
+
set_link_config: async (ctx, slug, config) => {
|
|
107
|
+
assertDestructive("repos.set_link_config", slug);
|
|
108
|
+
return forward("repos.set_link_config", { repo: slug }, () => kernel.repos.set_link_config(writeCtx(ctx), slug, config));
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
docs: {
|
|
112
|
+
get: (_ctx, repo, path) => forward("docs.get", { repo, path }, () => kernel.docs.get(readCtx(), repo, path)),
|
|
113
|
+
get_many: (_ctx, repo, paths) => forward("docs.get_many", { repo }, () => kernel.docs.get_many(readCtx(), repo, paths)),
|
|
114
|
+
get_version: (_ctx, repo, versionId) => forward("docs.get_version", { repo }, () => kernel.docs.get_version(readCtx(), repo, versionId)),
|
|
115
|
+
history: (_ctx, repo, path, opts) => forward("docs.history", { repo, path }, () => kernel.docs.history(readCtx(), repo, path, opts)),
|
|
116
|
+
diff: (_ctx, repo, path, from, to) => forward("docs.diff", { repo, path }, () => kernel.docs.diff(readCtx(), repo, path, from, to)),
|
|
117
|
+
create: async (ctx, repo, path, input) => {
|
|
118
|
+
assertWritable("docs.create", repo, path);
|
|
119
|
+
return forward("docs.create", { repo, path }, () => kernel.docs.create(writeCtx(ctx), repo, path, input));
|
|
120
|
+
},
|
|
121
|
+
put: async (ctx, repo, prevVersionId, path, input) => {
|
|
122
|
+
// A put may move the doc. Enforce write on the destination and — when
|
|
123
|
+
// the source differs — on the source too (both-endpoints, §8.2). The
|
|
124
|
+
// source path is the path of the version being superseded; resolve it
|
|
125
|
+
// with full visibility (the guard is trusted to look up what it must
|
|
126
|
+
// police), so a write-without-read grant still works.
|
|
127
|
+
const source = await kernel.docs.get_version({}, repo, prevVersionId);
|
|
128
|
+
assertWritable("docs.put", repo, source.path);
|
|
129
|
+
assertWritable("docs.put", repo, path);
|
|
130
|
+
return forward("docs.put", { repo, path }, () => kernel.docs.put(writeCtx(ctx), repo, prevVersionId, path, input));
|
|
131
|
+
},
|
|
132
|
+
delete: async (ctx, repo, prevVersionId) => {
|
|
133
|
+
const target = await kernel.docs.get_version({}, repo, prevVersionId);
|
|
134
|
+
assertWritable("docs.delete", repo, target.path);
|
|
135
|
+
return forward("docs.delete", { repo, path: target.path }, () => kernel.docs.delete(writeCtx(ctx), repo, prevVersionId));
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
links: {
|
|
139
|
+
backfill: async (ctx, repo) => {
|
|
140
|
+
assertDestructive("links.backfill", repo);
|
|
141
|
+
return forward("links.backfill", { repo }, () => kernel.links.backfill(writeCtx(ctx), repo));
|
|
142
|
+
},
|
|
143
|
+
// Read-only listing; scoped to read visibility like any other read.
|
|
144
|
+
stale: (_ctx, repo) => forward("links.stale", { repo }, () => kernel.links.stale(readCtx(), repo)),
|
|
145
|
+
repair: async (ctx, repo, opts) => {
|
|
146
|
+
const dryRun = opts?.dry_run ?? false;
|
|
147
|
+
// A live repair mutates docs → destructive. A dry run only plans → a
|
|
148
|
+
// read, bounded by read visibility (which docs the caller may see).
|
|
149
|
+
if (!dryRun)
|
|
150
|
+
assertDestructive("links.repair", repo);
|
|
151
|
+
const forwardCtx = dryRun
|
|
152
|
+
? { scope: entitlement.read }
|
|
153
|
+
: { author: authorFor(ctx), scope: entitlement.read };
|
|
154
|
+
return forward("links.repair", { repo }, () => kernel.links.repair(forwardCtx, repo, opts));
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
query: (_ctx, spec) => forward("query", {}, () => kernel.query(readCtx(), spec)),
|
|
158
|
+
// Read-only neighborhood expansion; scoped to read visibility. Traversal
|
|
159
|
+
// stays kernel-side (the engine applies scope∧filter as visibility).
|
|
160
|
+
graph: (_ctx, spec) => forward("graph", { repo: spec.repo }, () => kernel.graph(readCtx(), spec)),
|
|
161
|
+
history: {
|
|
162
|
+
// Read-only change feed; the engine applies read scope to each ref's
|
|
163
|
+
// endpoints (a ref is delivered only if the caller can see either end).
|
|
164
|
+
since: (_ctx, input) => forward("history.since", { repo: input.repo }, () => kernel.history.since(readCtx(), input)),
|
|
165
|
+
index: (_ctx, input) => forward("history.index", { repo: input.repo }, () => kernel.history.index(readCtx(), input)),
|
|
166
|
+
list: (_ctx, input) => forward("history.list", { repo: input.repo }, () => kernel.history.list(readCtx(), input)),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function errorCode(err) {
|
|
171
|
+
if (err && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
172
|
+
return err.code;
|
|
173
|
+
}
|
|
174
|
+
return "error";
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/shell/guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API keys — auth-shell plan §1 (authn front 1), decision 9.
|
|
3
|
+
*
|
|
4
|
+
* High-entropy shell-generated secrets, stored as `sha256:<hex>` hashes in the
|
|
5
|
+
* policy file itself. There is no key database: issuance is a line appended to
|
|
6
|
+
* a diffable file (`key mint`), revocation is deleting the line. A key is a
|
|
7
|
+
* non-human-chosen 256-bit random secret, so a plain sha256 — no KDF, no salt —
|
|
8
|
+
* is the right primitive: deterministic lookup, and there is no low-entropy
|
|
9
|
+
* password to protect against brute force (the old design §8.1 argument,
|
|
10
|
+
* carried over intact).
|
|
11
|
+
*
|
|
12
|
+
* Presented to the shell as `Authorization: Bearer <plaintext-key>`.
|
|
13
|
+
*/
|
|
14
|
+
import type { Policy } from "./policy.js";
|
|
15
|
+
/**
|
|
16
|
+
* Mint a fresh plaintext key and its policy-file hash. The plaintext is shown
|
|
17
|
+
* to the operator ONCE (by `key mint`) and never stored; only the hash lives
|
|
18
|
+
* in the policy file. base64url keeps it copy-paste-safe in shell/env/headers.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mintKey(): {
|
|
21
|
+
plaintext: string;
|
|
22
|
+
hash: string;
|
|
23
|
+
};
|
|
24
|
+
/** The `sha256:<hex>` hash of a plaintext key — the policy-file storage form. */
|
|
25
|
+
export declare function hashKey(plaintext: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a presented plaintext key to its principal id, or null if no
|
|
28
|
+
* principal lists its hash. Comparison is constant-time per candidate hash so
|
|
29
|
+
* a timing side-channel can't distinguish "no such key" from "wrong key".
|
|
30
|
+
*/
|
|
31
|
+
export declare function principalForKey(policy: Policy, plaintext: string): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Extract a bearer token from an `Authorization` header value, or null. Case-
|
|
34
|
+
* insensitive on the scheme per RFC 7235; the token is returned verbatim.
|
|
35
|
+
*/
|
|
36
|
+
export declare function bearerToken(authorization: string | undefined): string | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API keys — auth-shell plan §1 (authn front 1), decision 9.
|
|
3
|
+
*
|
|
4
|
+
* High-entropy shell-generated secrets, stored as `sha256:<hex>` hashes in the
|
|
5
|
+
* policy file itself. There is no key database: issuance is a line appended to
|
|
6
|
+
* a diffable file (`key mint`), revocation is deleting the line. A key is a
|
|
7
|
+
* non-human-chosen 256-bit random secret, so a plain sha256 — no KDF, no salt —
|
|
8
|
+
* is the right primitive: deterministic lookup, and there is no low-entropy
|
|
9
|
+
* password to protect against brute force (the old design §8.1 argument,
|
|
10
|
+
* carried over intact).
|
|
11
|
+
*
|
|
12
|
+
* Presented to the shell as `Authorization: Bearer <plaintext-key>`.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
15
|
+
/** Bytes of entropy in a minted key. 32 = 256 bits. */
|
|
16
|
+
const KEY_BYTES = 32;
|
|
17
|
+
/**
|
|
18
|
+
* Mint a fresh plaintext key and its policy-file hash. The plaintext is shown
|
|
19
|
+
* to the operator ONCE (by `key mint`) and never stored; only the hash lives
|
|
20
|
+
* in the policy file. base64url keeps it copy-paste-safe in shell/env/headers.
|
|
21
|
+
*/
|
|
22
|
+
export function mintKey() {
|
|
23
|
+
const plaintext = randomBytes(KEY_BYTES).toString("base64url");
|
|
24
|
+
return { plaintext, hash: hashKey(plaintext) };
|
|
25
|
+
}
|
|
26
|
+
/** The `sha256:<hex>` hash of a plaintext key — the policy-file storage form. */
|
|
27
|
+
export function hashKey(plaintext) {
|
|
28
|
+
const hex = createHash("sha256").update(plaintext, "utf8").digest("hex");
|
|
29
|
+
return `sha256:${hex}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a presented plaintext key to its principal id, or null if no
|
|
33
|
+
* principal lists its hash. Comparison is constant-time per candidate hash so
|
|
34
|
+
* a timing side-channel can't distinguish "no such key" from "wrong key".
|
|
35
|
+
*/
|
|
36
|
+
export function principalForKey(policy, plaintext) {
|
|
37
|
+
const presented = Buffer.from(hashKey(plaintext), "utf8");
|
|
38
|
+
let match = null;
|
|
39
|
+
for (const [id, principal] of Object.entries(policy.principals)) {
|
|
40
|
+
for (const stored of principal.keys ?? []) {
|
|
41
|
+
const storedBuf = Buffer.from(stored, "utf8");
|
|
42
|
+
// Length differs only for a malformed stored hash (validation forbids
|
|
43
|
+
// it), but timingSafeEqual throws on unequal lengths, so guard it.
|
|
44
|
+
if (storedBuf.length === presented.length && timingSafeEqual(storedBuf, presented)) {
|
|
45
|
+
// Don't early-return: keep scanning so total work doesn't leak which
|
|
46
|
+
// principal matched. Record the first (and, given unique keys, only) hit.
|
|
47
|
+
if (match === null)
|
|
48
|
+
match = id;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return match;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract a bearer token from an `Authorization` header value, or null. Case-
|
|
56
|
+
* insensitive on the scheme per RFC 7235; the token is returned verbatim.
|
|
57
|
+
*/
|
|
58
|
+
export function bearerToken(authorization) {
|
|
59
|
+
if (!authorization)
|
|
60
|
+
return null;
|
|
61
|
+
const m = /^Bearer[ \t]+(.+)$/i.exec(authorization.trim());
|
|
62
|
+
return m ? m[1].trim() : null;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/shell/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGvE,uDAAuD;AACvD,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB;;;;GAIG;AACH,MAAM,UAAU,OAAO;IACrB,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/D,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;AACjD,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,OAAO,CAAC,SAAiB;IACvC,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzE,OAAO,UAAU,GAAG,EAAE,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,SAAiB;IAC/D,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1D,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,KAAK,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAChE,KAAK,MAAM,MAAM,IAAI,SAAS,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;YAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC9C,sEAAsE;YACtE,mEAAmE;YACnE,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,IAAI,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;gBACnF,qEAAqE;gBACrE,0EAA0E;gBAC1E,IAAI,KAAK,KAAK,IAAI;oBAAE,KAAK,GAAG,EAAE,CAAC;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,aAAiC;IAC3D,IAAI,CAAC,aAAa;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth device-authorization login — auth-shell plan §1 (login), WS5.
|
|
3
|
+
*
|
|
4
|
+
* `mrplex login` runs the OAuth 2.0 **device authorization flow** (RFC 8628):
|
|
5
|
+
* request a device+user code, show the user a URL and code to enter, then poll
|
|
6
|
+
* the token endpoint until they approve. The resulting access + refresh tokens
|
|
7
|
+
* are cached under the config dir (mode 600); `mcp-stdio` uses the cached
|
|
8
|
+
* access token when no explicit credential is given, and refreshes it mid-
|
|
9
|
+
* session (recompiling the entitlement on refresh, so long-lived agent sessions
|
|
10
|
+
* neither die at token expiry nor outlive a policy change indefinitely).
|
|
11
|
+
*
|
|
12
|
+
* The IdP is the authorization server; mrplex is only the resource server, so
|
|
13
|
+
* this file speaks the standard endpoints and stores tokens — it verifies
|
|
14
|
+
* nothing (that's oidc.ts, on the serving side).
|
|
15
|
+
*/
|
|
16
|
+
export type DeviceFlowConfig = {
|
|
17
|
+
/** OAuth device-authorization endpoint. */
|
|
18
|
+
deviceAuthorizationEndpoint: string;
|
|
19
|
+
/** OAuth token endpoint. */
|
|
20
|
+
tokenEndpoint: string;
|
|
21
|
+
clientId: string;
|
|
22
|
+
/** Space-delimited scopes (e.g. "openid email profile offline_access"). */
|
|
23
|
+
scope?: string;
|
|
24
|
+
/**
|
|
25
|
+
* OAuth `audience` — the API the token is for. Auth0 (and some other IdPs)
|
|
26
|
+
* only issue a *JWT* access token when the device-authorization request names
|
|
27
|
+
* an audience; without it they return an opaque token that fails the shell's
|
|
28
|
+
* JWKS verification. Sent in the device-authorization request when set. This
|
|
29
|
+
* is the shell's `--oidc-audience` value.
|
|
30
|
+
*/
|
|
31
|
+
audience?: string;
|
|
32
|
+
};
|
|
33
|
+
export type TokenSet = {
|
|
34
|
+
access_token: string;
|
|
35
|
+
refresh_token?: string;
|
|
36
|
+
/** Absolute expiry (epoch ms), computed from `expires_in` at grant time. */
|
|
37
|
+
expires_at?: number;
|
|
38
|
+
token_type?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Where the token cache lives — `${XDG_CONFIG_HOME}/mrplex/token.json`. */
|
|
41
|
+
export declare function tokenCachePath(): string;
|
|
42
|
+
export declare function loadTokenSet(): TokenSet | null;
|
|
43
|
+
export declare function saveTokenSet(tokens: TokenSet): void;
|
|
44
|
+
/** A token is fresh if it exists and isn't within `skewMs` of expiry. */
|
|
45
|
+
export declare function isFresh(tokens: TokenSet, skewMs?: number): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Run the device flow to completion. Prompts via `prompt` (defaults to stderr),
|
|
48
|
+
* polls the token endpoint honoring `interval` and `slow_down`, and returns the
|
|
49
|
+
* granted token set. Throws on denial, expiry, or endpoint error.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deviceFlowLogin(config: DeviceFlowConfig, prompt?: (msg: string) => void): Promise<TokenSet>;
|
|
52
|
+
/**
|
|
53
|
+
* Exchange a refresh token for a fresh access token. Returns the new token set,
|
|
54
|
+
* carrying the refresh token forward if the IdP didn't rotate it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function refreshTokens(config: Pick<DeviceFlowConfig, "tokenEndpoint" | "clientId">, refreshToken: string): Promise<TokenSet>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth device-authorization login — auth-shell plan §1 (login), WS5.
|
|
3
|
+
*
|
|
4
|
+
* `mrplex login` runs the OAuth 2.0 **device authorization flow** (RFC 8628):
|
|
5
|
+
* request a device+user code, show the user a URL and code to enter, then poll
|
|
6
|
+
* the token endpoint until they approve. The resulting access + refresh tokens
|
|
7
|
+
* are cached under the config dir (mode 600); `mcp-stdio` uses the cached
|
|
8
|
+
* access token when no explicit credential is given, and refreshes it mid-
|
|
9
|
+
* session (recompiling the entitlement on refresh, so long-lived agent sessions
|
|
10
|
+
* neither die at token expiry nor outlive a policy change indefinitely).
|
|
11
|
+
*
|
|
12
|
+
* The IdP is the authorization server; mrplex is only the resource server, so
|
|
13
|
+
* this file speaks the standard endpoints and stores tokens — it verifies
|
|
14
|
+
* nothing (that's oidc.ts, on the serving side).
|
|
15
|
+
*/
|
|
16
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
/** Where the token cache lives — `${XDG_CONFIG_HOME}/mrplex/token.json`. */
|
|
20
|
+
export function tokenCachePath() {
|
|
21
|
+
const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
22
|
+
return join(xdg, "mrplex", "token.json");
|
|
23
|
+
}
|
|
24
|
+
export function loadTokenSet() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(tokenCachePath(), "utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function saveTokenSet(tokens) {
|
|
33
|
+
const path = tokenCachePath();
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
writeFileSync(path, `${JSON.stringify(tokens, null, 2)}\n`, { mode: 0o600 });
|
|
36
|
+
try {
|
|
37
|
+
chmodSync(path, 0o600);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// POSIX mode unsupported (Windows) — best-effort.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** A token is fresh if it exists and isn't within `skewMs` of expiry. */
|
|
44
|
+
export function isFresh(tokens, skewMs = 30_000) {
|
|
45
|
+
if (tokens.expires_at === undefined)
|
|
46
|
+
return true; // no expiry info → assume usable
|
|
47
|
+
return Date.now() + skewMs < tokens.expires_at;
|
|
48
|
+
}
|
|
49
|
+
function toTokenSet(raw) {
|
|
50
|
+
const set = { access_token: String(raw.access_token) };
|
|
51
|
+
if (typeof raw.refresh_token === "string")
|
|
52
|
+
set.refresh_token = raw.refresh_token;
|
|
53
|
+
if (typeof raw.token_type === "string")
|
|
54
|
+
set.token_type = raw.token_type;
|
|
55
|
+
if (typeof raw.expires_in === "number")
|
|
56
|
+
set.expires_at = Date.now() + raw.expires_in * 1000;
|
|
57
|
+
return set;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Run the device flow to completion. Prompts via `prompt` (defaults to stderr),
|
|
61
|
+
* polls the token endpoint honoring `interval` and `slow_down`, and returns the
|
|
62
|
+
* granted token set. Throws on denial, expiry, or endpoint error.
|
|
63
|
+
*/
|
|
64
|
+
export async function deviceFlowLogin(config, prompt = (m) => process.stderr.write(`${m}\n`)) {
|
|
65
|
+
const authResp = await postForm(config.deviceAuthorizationEndpoint, {
|
|
66
|
+
client_id: config.clientId,
|
|
67
|
+
scope: config.scope ?? "openid email profile offline_access",
|
|
68
|
+
// Only include audience when set — some IdPs reject an empty/unknown one.
|
|
69
|
+
...(config.audience !== undefined ? { audience: config.audience } : {}),
|
|
70
|
+
});
|
|
71
|
+
if (!authResp.ok) {
|
|
72
|
+
throw new Error(`device authorization failed: HTTP ${authResp.status}`);
|
|
73
|
+
}
|
|
74
|
+
const auth = (await authResp.json());
|
|
75
|
+
const uri = auth.verification_uri_complete ?? auth.verification_uri;
|
|
76
|
+
prompt(`To sign in, visit:\n ${uri}\nand enter code: ${auth.user_code}`);
|
|
77
|
+
let intervalMs = (auth.interval ?? 5) * 1000;
|
|
78
|
+
const deadline = Date.now() + auth.expires_in * 1000;
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
await sleep(intervalMs);
|
|
81
|
+
const tokenResp = await postForm(config.tokenEndpoint, {
|
|
82
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
83
|
+
device_code: auth.device_code,
|
|
84
|
+
client_id: config.clientId,
|
|
85
|
+
});
|
|
86
|
+
const body = (await tokenResp.json());
|
|
87
|
+
if (tokenResp.ok)
|
|
88
|
+
return toTokenSet(body);
|
|
89
|
+
// Standard device-flow polling errors (RFC 8628 §3.5).
|
|
90
|
+
const error = String(body.error ?? "");
|
|
91
|
+
if (error === "authorization_pending")
|
|
92
|
+
continue;
|
|
93
|
+
if (error === "slow_down") {
|
|
94
|
+
intervalMs += 5_000;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`device token request failed: ${error || `HTTP ${tokenResp.status}`}`);
|
|
98
|
+
}
|
|
99
|
+
throw new Error("device flow timed out before the user approved");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Exchange a refresh token for a fresh access token. Returns the new token set,
|
|
103
|
+
* carrying the refresh token forward if the IdP didn't rotate it.
|
|
104
|
+
*/
|
|
105
|
+
export async function refreshTokens(config, refreshToken) {
|
|
106
|
+
const resp = await postForm(config.tokenEndpoint, {
|
|
107
|
+
grant_type: "refresh_token",
|
|
108
|
+
refresh_token: refreshToken,
|
|
109
|
+
client_id: config.clientId,
|
|
110
|
+
});
|
|
111
|
+
const body = (await resp.json());
|
|
112
|
+
if (!resp.ok) {
|
|
113
|
+
throw new Error(`token refresh failed: ${String(body.error ?? `HTTP ${resp.status}`)}`);
|
|
114
|
+
}
|
|
115
|
+
const next = toTokenSet(body);
|
|
116
|
+
if (next.refresh_token === undefined)
|
|
117
|
+
next.refresh_token = refreshToken;
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
function postForm(url, form) {
|
|
121
|
+
return fetch(url, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
124
|
+
body: new URLSearchParams(form).toString(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function sleep(ms) {
|
|
128
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/shell/login.ts"],"names":[],"mappings":"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"}
|