theokit 0.50.2 → 0.52.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/dist/action-protocol-tGm3NKRg.d.ts +170 -0
- package/dist/{actions-virtual-module-2STC6RV3.js → actions-virtual-module-SNG4JC35.js} +6 -6
- package/dist/{actions-virtual-module-ESKVARP4.js → actions-virtual-module-VJYKUIXE.js} +6 -5
- package/dist/actions-virtual-module-VJYKUIXE.js.map +1 -0
- package/dist/adapters/agent-mount.js +1 -1
- package/dist/{agent-7RP3VTSI.js → agent-AHGYRVQX.js} +4 -4
- package/dist/{app-typed-client-NYZ2PLNC.js → app-typed-client-FQVMEG4Z.js} +6 -6
- package/dist/{app-typed-client-CEW2KGXR.js → app-typed-client-ZZIASI4A.js} +6 -5
- package/dist/app-typed-client-ZZIASI4A.js.map +1 -0
- package/dist/{build-4LPUE2H3.js → build-3AYYQZGQ.js} +6 -6
- package/dist/{chunk-MLWETKFA.js → chunk-2E7JLV7L.js} +2 -2
- package/dist/{chunk-5K7WIT3Z.js → chunk-5BOKW3GX.js} +5 -172
- package/dist/chunk-5BOKW3GX.js.map +1 -0
- package/dist/{chunk-EVZKNA2W.js → chunk-6AM7HIRM.js} +2 -2
- package/dist/{chunk-QNGS7EVS.js → chunk-E53UIFH6.js} +2 -2
- package/dist/chunk-E53UIFH6.js.map +1 -0
- package/dist/{chunk-ECTUUAMR.js → chunk-I4ELCXNV.js} +10 -10
- package/dist/{chunk-L4VFNFDL.js → chunk-IRASEXOG.js} +29 -1
- package/dist/{chunk-L4VFNFDL.js.map → chunk-IRASEXOG.js.map} +1 -1
- package/dist/{chunk-LLMTORUY.js → chunk-MBFD6NHB.js} +10 -10
- package/dist/{chunk-3MIGBPTW.js → chunk-PFAVZL4F.js} +3 -3
- package/dist/{chunk-IQRL5KZL.js → chunk-VGE6AKEJ.js} +2 -2
- package/dist/{chunk-6J4JWHDJ.js → chunk-VHB2FSPM.js} +16 -2
- package/dist/chunk-VHB2FSPM.js.map +1 -0
- package/dist/{chunk-KUZVRJ7M.js → chunk-XLLW46MB.js} +3 -3
- package/dist/{chunk-XS7EX55U.js → chunk-YYOBMOO4.js} +16 -2
- package/dist/chunk-YYOBMOO4.js.map +1 -0
- package/dist/{chunk-DVYILYM6.js → chunk-YZCD3DWE.js} +2 -2
- package/dist/{chunk-FD4ZDTQ6.js → chunk-Z7CJHDLS.js} +29 -1
- package/dist/{chunk-FD4ZDTQ6.js.map → chunk-Z7CJHDLS.js.map} +1 -1
- package/dist/chunk-ZDUO4IYN.js +174 -0
- package/dist/chunk-ZDUO4IYN.js.map +1 -0
- package/dist/cli/index.js +8 -8
- package/dist/client/index.d.ts +117 -1
- package/dist/client/index.js +144 -3
- package/dist/client/index.js.map +1 -1
- package/dist/{dev-Z4YLROZD.js → dev-W7OXHMSP.js} +8 -8
- package/dist/{dev-emit-MVAJAFGX.js → dev-emit-G2E2U3NP.js} +3 -3
- package/dist/{dev-emit-BFNMDCPB.js → dev-emit-SPSV6UU7.js} +5 -5
- package/dist/{index-B_Pk2JXn.d.ts → index-C8YN9dpF.d.ts} +3 -165
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/{internal-api-53L6FIQ2.js → internal-api-EL25ADPR.js} +6 -6
- package/dist/{internal-api-IXSWWH2N.js → internal-api-S6BGJCZP.js} +6 -5
- package/dist/{mcp-RKD4QQFS.js → mcp-7BXBN7PD.js} +4 -4
- package/dist/{openapi-VFPEZSRI.js → openapi-2IBBOS3Q.js} +5 -5
- package/dist/{preview-LB3YL6U5.js → preview-4ZTFJG2D.js} +3 -3
- package/dist/{routes-BPLU3ISV.js → routes-PP5PHHLR.js} +3 -3
- package/dist/server/http/index.d.ts +2 -1
- package/dist/server/http/index.js +9 -7
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +11 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/scan/index.js +2 -2
- package/dist/{server-boundary-ACFCBLZ4.js → server-boundary-RNM25WWD.js} +6 -6
- package/dist/{server-boundary-RBMMPWCV.js → server-boundary-XRXVVUMO.js} +6 -5
- package/dist/server-boundary-XRXVVUMO.js.map +1 -0
- package/dist/{start-G2JS6422.js → start-WILEDDW2.js} +7 -7
- package/dist/vite-plugin/index.js +6 -5
- package/dist/{vite-plugin-4VM5EUQL.js → vite-plugin-2XM35VNV.js} +8 -8
- package/package.json +1 -1
- package/dist/actions-virtual-module-ESKVARP4.js.map +0 -1
- package/dist/app-typed-client-CEW2KGXR.js.map +0 -1
- package/dist/chunk-5K7WIT3Z.js.map +0 -1
- package/dist/chunk-6J4JWHDJ.js.map +0 -1
- package/dist/chunk-QNGS7EVS.js.map +0 -1
- package/dist/chunk-XS7EX55U.js.map +0 -1
- package/dist/server-boundary-RBMMPWCV.js.map +0 -1
- /package/dist/{actions-virtual-module-2STC6RV3.js.map → actions-virtual-module-SNG4JC35.js.map} +0 -0
- /package/dist/{agent-7RP3VTSI.js.map → agent-AHGYRVQX.js.map} +0 -0
- /package/dist/{app-typed-client-NYZ2PLNC.js.map → app-typed-client-FQVMEG4Z.js.map} +0 -0
- /package/dist/{build-4LPUE2H3.js.map → build-3AYYQZGQ.js.map} +0 -0
- /package/dist/{chunk-MLWETKFA.js.map → chunk-2E7JLV7L.js.map} +0 -0
- /package/dist/{chunk-EVZKNA2W.js.map → chunk-6AM7HIRM.js.map} +0 -0
- /package/dist/{chunk-ECTUUAMR.js.map → chunk-I4ELCXNV.js.map} +0 -0
- /package/dist/{chunk-LLMTORUY.js.map → chunk-MBFD6NHB.js.map} +0 -0
- /package/dist/{chunk-3MIGBPTW.js.map → chunk-PFAVZL4F.js.map} +0 -0
- /package/dist/{chunk-IQRL5KZL.js.map → chunk-VGE6AKEJ.js.map} +0 -0
- /package/dist/{chunk-KUZVRJ7M.js.map → chunk-XLLW46MB.js.map} +0 -0
- /package/dist/{chunk-DVYILYM6.js.map → chunk-YZCD3DWE.js.map} +0 -0
- /package/dist/{dev-Z4YLROZD.js.map → dev-W7OXHMSP.js.map} +0 -0
- /package/dist/{dev-emit-MVAJAFGX.js.map → dev-emit-G2E2U3NP.js.map} +0 -0
- /package/dist/{dev-emit-BFNMDCPB.js.map → dev-emit-SPSV6UU7.js.map} +0 -0
- /package/dist/{internal-api-53L6FIQ2.js.map → internal-api-EL25ADPR.js.map} +0 -0
- /package/dist/{internal-api-IXSWWH2N.js.map → internal-api-S6BGJCZP.js.map} +0 -0
- /package/dist/{mcp-RKD4QQFS.js.map → mcp-7BXBN7PD.js.map} +0 -0
- /package/dist/{openapi-VFPEZSRI.js.map → openapi-2IBBOS3Q.js.map} +0 -0
- /package/dist/{preview-LB3YL6U5.js.map → preview-4ZTFJG2D.js.map} +0 -0
- /package/dist/{routes-BPLU3ISV.js.map → routes-PP5PHHLR.js.map} +0 -0
- /package/dist/{server-boundary-ACFCBLZ4.js.map → server-boundary-RNM25WWD.js.map} +0 -0
- /package/dist/{start-G2JS6422.js.map → start-WILEDDW2.js.map} +0 -0
- /package/dist/{vite-plugin-4VM5EUQL.js.map → vite-plugin-2XM35VNV.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/_internal/compare-by-code-unit.ts","../src/server/_internal/scan-walker.ts","../src/server/scan/errors.ts","../src/server/scan/file-stamp-cache.ts"],"sourcesContent":["/**\n * Total order over strings by UTF-16 code unit — the ordering build-time output\n * must use.\n *\n * `localeCompare` is the wrong instrument here, and reads like the right one,\n * which is why it kept being chosen. With no locale argument it uses the default\n * collator, and Node derives that from `LC_ALL`/`LANG`: under `sv-SE` an `ä`\n * sorts after `z`, under `en-US` before `a`. A scanner ordering that way emits a\n * different manifest per machine — and where the order being emitted is an\n * execution order, it decides which middleware runs first\n * (usetheokit/theokit#346, usetheokit/theokit#351).\n *\n * What build output needs from a comparator is that it be total and stable.\n * Being alphabetical for a reader of one particular language is not a property\n * it needs, and is the one that costs reproducibility.\n *\n * Presentation code is the opposite case and should keep `localeCompare`: a list\n * a human reads should collate the way that human's language collates.\n *\n * @internal\n */\nexport function compareByCodeUnit(a: string, b: string): number {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks directories derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { readdirSync } from 'node:fs'\nimport type { Dirent } from 'node:fs'\nimport { extname, join, resolve } from 'node:path'\n\nimport { compareByCodeUnit } from './compare-by-code-unit.js'\n\n/**\n * Options for walkSourceFiles.\n *\n * Sequential by design — callers (route precedence) depend on insertion\n * order (EC-19 documented decision). Async-callback support is implicit\n * via JavaScript's serial await loop.\n *\n * Tested on macOS + Linux. Windows long-path support not validated (EC-20).\n */\ninterface WalkOptions {\n /** File extensions to include (e.g., new Set(['.ts', '.tsx'])). */\n extensions: ReadonlySet<string>\n /** Skip directories whose name starts with any of these (default: ['_', '.']). */\n skipPrefixes?: readonly string[]\n /**\n * Read one directory. Defaults to `readdirSync`.\n *\n * Injected for one reason: the ordering below cannot be tested against a real\n * filesystem. `readdirSync` returns whatever the filesystem returns — creation\n * order on tmpfs, hash order on ext4 with `dir_index` — and on the machine\n * this was written on those four names came back already sorted, so a\n * name-based test passed before the sort existed and proved nothing. A test\n * that agrees with the code because both read the same accidental order is\n * `B-022`, and writing one here would have been that item committed inside its\n * own fix.\n */\n readDir?: (dir: string) => Dirent[]\n}\n\n/**\n * Recursively walk `root`, invoking `onFile(absPath)` for every file matching\n * `opts.extensions`. Directories whose name starts with any `skipPrefixes`\n * char are skipped (defaults to `_` and `.`).\n *\n * Entries are emitted in UTF-16 code-unit order, files and directories in one\n * total order. Without it the walker passed the filesystem's order through to\n * six scanners — routes, actions, websockets, cron, agents, jobs — and only one\n * re-orders afterwards, so five emitted a different manifest per machine. Where\n * that order is an execution order it decides what runs first (#346, B-004).\n *\n * Replaces 3 near-identical recursive walkers in scan.ts, action-scan.ts,\n * ws-scan.ts (PV-3 — DRY consolidation). Resolves T3.1 of\n * architecture-review-remediation-plan. Six call sites consume it today, not\n * three; the count in this paragraph was true when it was written.\n *\n * Symlink loops are NOT tracked — callers must avoid them or pre-resolve\n * via `fs.realpath`. EC-11 documented but not implemented (rare in dev).\n */\nexport function walkSourceFiles(\n root: string,\n opts: WalkOptions,\n onFile: (absPath: string) => void,\n): void {\n const skipPrefixes = opts.skipPrefixes ?? ['_', '.']\n const readDir = opts.readDir ?? ((dir: string) => readdirSync(dir, { withFileTypes: true }))\n const visit = (dir: string): void => {\n let entries\n try {\n entries = readDir(dir)\n } catch {\n // Silently skip unreadable directories — caller controls discoverability\n return\n }\n // One total order over files and directories together, so a name sorts the\n // same way whichever it is. Sorting the two groups separately would emit a\n // different interleaving than a single pass, which is an ordering decision\n // nobody made (#346).\n const ordered = [...entries].sort((a, b) => compareByCodeUnit(a.name, b.name))\n for (const entry of ordered) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {\n visit(fullPath)\n } else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {\n onFile(resolve(fullPath))\n }\n }\n }\n visit(root)\n}\n","import { compareByCodeUnit } from '../_internal/compare-by-code-unit.js'\n\n/**\n * Router-convention errors thrown by the server-route scanner.\n *\n *\n * theokit 0.4.0+ enforces directory-nested file-system routing\n * (`auth/[provider]/login.ts`) and REJECTS dotted-basename routes\n * (`auth.[provider].login.ts`) because the legacy regex extracted the\n * dotted basename incorrectly — `params.provider` was undefined at request\n * time. Decision recorded in the plan above (`g6-router-convention-plan.md`) and CHANGELOG 0.4.0\n * (no standalone ADR was cut for this router-convention change).\n */\n\n/**\n * Canonical migration guide URL. T4.2 establishes this as the authoritative\n * landing page. EC-3: error message uses this constant so the URL never\n * drifts from the doc location.\n */\nexport const ROUTER_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/0.3-to-0.4-router'\n\ninterface RouterConventionErrorOptions {\n /** Absolute path of the offending route file. */\n file: string\n /** Suggested directory-nested replacement path (relative, e.g. `routes/auth/[provider]/login.ts`). */\n suggestion: string\n /** Migration guide URL (defaults to `ROUTER_MIGRATION_GUIDE_URL`). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file uses the legacy\n * dotted-basename convention (`auth.[provider].login.ts`).\n *\n * The error is FAIL-FAST by design — running with a route that has wrong\n * `paramNames` produces silent 404s at request time, which is strictly\n * worse than a build-time error.\n */\nexport class RouterConventionError extends Error {\n override readonly name = 'RouterConventionError'\n readonly file: string\n readonly suggestion: string\n readonly migrationUrl: string\n\n constructor(opts: RouterConventionErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? ROUTER_MIGRATION_GUIDE_URL\n const message = [\n `Router convention violation: dotted route basename is not supported in theokit 0.4+.`,\n ``,\n ` File: ${opts.file}`,\n ` Use directory-nested form: ${opts.suggestion}`,\n ``,\n `Migration guide: ${migrationUrl}`,\n `Run \\`theokit migrate router\\` to convert all dotted basenames automatically.`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.suggestion = opts.suggestion\n this.migrationUrl = migrationUrl\n }\n}\n\n/**\n * Canonical landing page for the policy-declaration migration. Held as a\n * constant for the same reason as {@link ROUTER_MIGRATION_GUIDE_URL}: the error\n * message and the doc cannot drift apart if only one of them names the URL.\n *\n * Module-local, unlike its sibling: nothing outside this file needs to name it,\n * and an export with no consumer is what `knip` exists to refuse.\n */\nconst ROUTE_POLICY_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/route-policy'\n\ninterface MissingRoutePolicyErrorOptions {\n /** Absolute path of the route file. */\n file: string\n /** The URL path the file resolves to, so the message reads like the app, not like the disk. */\n routePath: string\n /** The exported HTTP methods that declare no policy. */\n methods: string[]\n /** Migration guide URL (defaults to {@link ROUTE_POLICY_MIGRATION_GUIDE_URL}). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file exports an HTTP method that\n * declares no access policy (ADR 0001, Decision point 5).\n *\n * Absence used to mean \"not declared\", which every reader and every transport\n * had to interpret as open. The interpretation was the bug: a route nobody\n * thought about looked exactly like a route deliberately left open. This error\n * is where the two stop looking alike.\n *\n * It fires at scan time — `theo build`, `theo start`, `theo dev`, `theo routes`\n * and every deployment adapter go through the same scanner — so the answer\n * arrives before a request does, in the same place the scanner already refuses\n * a dotted basename and a collision with the reserved batch path.\n */\nexport class MissingRoutePolicyError extends Error {\n override readonly name = 'MissingRoutePolicyError'\n readonly file: string\n readonly routePath: string\n readonly methods: string[]\n readonly migrationUrl: string\n\n constructor(opts: MissingRoutePolicyErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? ROUTE_POLICY_MIGRATION_GUIDE_URL\n const methods = [...opts.methods].sort(compareByCodeUnit)\n const message = [\n `Route policy not declared: every route says who may call it (ADR 0001).`,\n ``,\n ` File: ${opts.file}`,\n ` Route: ${opts.routePath}`,\n ` Missing: ${methods.join(', ')}`,\n ``,\n `Add a policy to each method listed above:`,\n ``,\n // The import is part of the remedy, not decoration. This message shipped\n // naming `requireOwner` while no entry point exported it, so it read as\n // actionable and was not. `tests/unit/policy-gate-remedy-is-importable.test.ts`\n // asserts that every symbol named here is reachable from the path named here.\n ` import { route, requireOwner } from 'theokit/server/define'`,\n ``,\n ` export const ${methods[0] ?? 'GET'} = route()`,\n ` .policy('public') // anyone may call this`,\n ` .handler(...)`,\n ` .build()`,\n ``,\n ` export const ${methods[0] ?? 'GET'} = route()`,\n ` .policy(({ subject, params }) => requireOwner(subject, ownerOf(params.id)))`,\n ` .handler(...)`,\n ` .build()`,\n ``,\n `Writing 'public' is a decision and not a default. It states that this route`,\n `is open to anyone who can reach it, and it is greppable, so how much of the`,\n `app is open becomes a number somebody can read.`,\n ``,\n `Migration guide: ${migrationUrl}`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.routePath = opts.routePath\n this.methods = methods\n this.migrationUrl = migrationUrl\n }\n}\n\n/**\n * Canonical landing page for the agent-policy migration. Module-local for the same reason as its\n * route sibling: nothing outside this file names it, and an export with no consumer is what `knip`\n * exists to refuse.\n */\nconst AGENT_POLICY_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/agent-policy'\n\ninterface MissingAgentPolicyErrorOptions {\n /** Absolute path of the agent file. */\n file: string\n /** The URL the agent is served at, so the message reads like the app rather than like the disk. */\n agentPath: string\n /** Migration guide URL (defaults to {@link AGENT_POLICY_MIGRATION_GUIDE_URL}). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanAgents` when an agent file declares no access policy\n * (ADR 0001 Decision point 5, extended to the agent surface by usetheokit/theokit#365).\n *\n * ## Why absence had to stop meaning open HERE and not at runtime\n *\n * The agent endpoints resume a conversation the CALLER names, and they are dispatched before route\n * matching, so no route, no middleware and no `server/context.ts` ever saw those URLs. A caller\n * holding a conversation id and no credential read that conversation back.\n *\n * There is no safe runtime default for that. Refusing every caller-named session id would break\n * multi-turn chat, which is the base case; admitting them is the defect. So the decision moves to\n * where a person can answer it once, in the file that owns the agent — the same place and the same\n * reasoning as the route gate, which is why this error reads like its sibling.\n *\n * ## What it costs\n *\n * Every existing application with an agent fails its next build until it adds one line. That is the\n * half of ADR 0001 the ADR itself called breaking, and the trade it names: a build error that\n * points at a file beats a silent 200 that hands over somebody else's conversation.\n */\nexport class MissingAgentPolicyError extends Error {\n override readonly name = 'MissingAgentPolicyError'\n readonly file: string\n readonly agentPath: string\n readonly migrationUrl: string\n\n constructor(opts: MissingAgentPolicyErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? AGENT_POLICY_MIGRATION_GUIDE_URL\n const message = [\n `Agent policy not declared: every agent says who may run it (ADR 0001).`,\n ``,\n ` File: ${opts.file}`,\n ` Agent: ${opts.agentPath}`,\n ``,\n `This one declaration covers every endpoint the agent exposes - the run, the`,\n `thread routes, the pending-approval listing, the approve route and MCP - because`,\n `they all reach the same conversation and the same paused tools.`,\n ``,\n `Add ONE of these to the file above:`,\n ``,\n ` export const policy = 'public' // anyone who can reach it may run it`,\n ``,\n ` import { requireOwner } from 'theokit/server/define'`,\n ``,\n ` // subject <- what server/context.ts put on ctx.subject`,\n ` // params <- { agent, endpoint, sessionId?, approvalId? }`,\n ` // body <- the parsed chat body, on the endpoints that have one`,\n ` export const policy = ({ subject, params }) =>`,\n ` requireOwner(subject, ownerOf(params.sessionId))`,\n ``,\n `Writing 'public' is a decision and not a default. The endpoint resumes whatever`,\n `conversation the CALLER names, so 'public' means any caller holding an id may read`,\n `and continue that conversation - a capability model, which is legitimate when it is`,\n `the choice somebody made and greppable once it is written down.`,\n ``,\n `Migration guide: ${migrationUrl}`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.agentPath = opts.agentPath\n this.migrationUrl = migrationUrl\n }\n}\n","/**\n * Memoise per source file, invalidated by the file's own stamp.\n *\n * ## Why this exists\n *\n * `theokit dev` re-scans the routes and agents directories on EVERY request, so any work that\n * parses a file with the TypeScript AST turns a build-time check into per-request latency.\n * `agent-scan.ts` recognised this and carried its own `Map` keyed on `path:mtimeMs:size`;\n * `scan.ts` — with far more files — carried neither the cache nor the reasoning, and the\n * route-policy gate then added a SECOND full parse of every route file on top of the one\n * `detectExportedHttpMethods` had been paying since before it (usetheokit/theokit#417).\n *\n * Lifting the mechanism here rather than copying it is the point: two hand-rolled caches keyed\n * \"the same way\" are two chances for the keys to stop being the same.\n *\n * ## Why the stamp and not a watcher\n *\n * `mtimeMs` and `size` together come from the `statSync` the scanner is doing anyway, so an edit\n * invalidates the entry without anyone remembering to, and a `theokit build` — one process, one\n * scan — never notices the cache exists.\n *\n * The known limit, stated rather than discovered: a rewrite that preserves BOTH mtime and size is\n * invisible. That is a deliberate trade every mtime cache makes, and the alternative (hashing the\n * contents) reads the file to avoid reading the file.\n */\nimport { statSync } from 'node:fs'\n\nexport interface FileStampCache<T> {\n /** The memoised value for `filePath`, computing it when the file's stamp has changed. */\n get: (filePath: string, compute: () => T) => T\n /** Test seam — a module-level cache would otherwise outlive a fixture directory. */\n clear: () => void\n}\n\nexport function createFileStampCache<T>(): FileStampCache<T> {\n const entries = new Map<string, T>()\n\n return {\n get(filePath, compute) {\n // A path the scanner itself discovered by globbing the project, never caller input at runtime.\n // eslint-disable-next-line security/detect-non-literal-fs-filename -- see above\n const stat = statSync(filePath)\n const key = `${filePath}:${String(stat.mtimeMs)}:${String(stat.size)}`\n\n const hit = entries.get(key)\n // `has` rather than a truthiness check on the value: `false`, `0` and `''` are all legitimate\n // results to cache, and a truthiness test would recompute them on every call — which is the\n // defect this file exists to remove, wearing a subtler hat.\n if (hit !== undefined || entries.has(key)) return hit as T\n\n const value = compute()\n entries.set(key, value)\n return value\n },\n clear() {\n entries.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/server/_internal/compare-by-code-unit.ts","../src/server/_internal/scan-walker.ts","../src/server/scan/errors.ts","../src/server/scan/file-stamp-cache.ts"],"sourcesContent":["/**\n * Total order over strings by UTF-16 code unit — the ordering build-time output\n * must use.\n *\n * `localeCompare` is the wrong instrument here, and reads like the right one,\n * which is why it kept being chosen. With no locale argument it uses the default\n * collator, and Node derives that from `LC_ALL`/`LANG`: under `sv-SE` an `ä`\n * sorts after `z`, under `en-US` before `a`. A scanner ordering that way emits a\n * different manifest per machine — and where the order being emitted is an\n * execution order, it decides which middleware runs first\n * (usetheokit/theokit#346, usetheokit/theokit#351).\n *\n * What build output needs from a comparator is that it be total and stable.\n * Being alphabetical for a reader of one particular language is not a property\n * it needs, and is the one that costs reproducibility.\n *\n * Presentation code is the opposite case and should keep `localeCompare`: a list\n * a human reads should collate the way that human's language collates.\n *\n * @internal\n */\nexport function compareByCodeUnit(a: string, b: string): number {\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks directories derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { readdirSync } from 'node:fs'\nimport type { Dirent } from 'node:fs'\nimport { extname, join, resolve } from 'node:path'\n\nimport { compareByCodeUnit } from './compare-by-code-unit.js'\n\n/**\n * Options for walkSourceFiles.\n *\n * Sequential by design — callers (route precedence) depend on insertion\n * order (EC-19 documented decision). Async-callback support is implicit\n * via JavaScript's serial await loop.\n *\n * Tested on macOS + Linux. Windows long-path support not validated (EC-20).\n */\ninterface WalkOptions {\n /** File extensions to include (e.g., new Set(['.ts', '.tsx'])). */\n extensions: ReadonlySet<string>\n /** Skip directories whose name starts with any of these (default: ['_', '.']). */\n skipPrefixes?: readonly string[]\n /**\n * Read one directory. Defaults to `readdirSync`.\n *\n * Injected for one reason: the ordering below cannot be tested against a real\n * filesystem. `readdirSync` returns whatever the filesystem returns — creation\n * order on tmpfs, hash order on ext4 with `dir_index` — and on the machine\n * this was written on those four names came back already sorted, so a\n * name-based test passed before the sort existed and proved nothing. A test\n * that agrees with the code because both read the same accidental order is\n * `B-022`, and writing one here would have been that item committed inside its\n * own fix.\n */\n readDir?: (dir: string) => Dirent[]\n}\n\n/**\n * Recursively walk `root`, invoking `onFile(absPath)` for every file matching\n * `opts.extensions`. Directories whose name starts with any `skipPrefixes`\n * char are skipped (defaults to `_` and `.`).\n *\n * Entries are emitted in UTF-16 code-unit order, files and directories in one\n * total order. Without it the walker passed the filesystem's order through to\n * six scanners — routes, actions, websockets, cron, agents, jobs — and only one\n * re-orders afterwards, so five emitted a different manifest per machine. Where\n * that order is an execution order it decides what runs first (#346, B-004).\n *\n * Replaces 3 near-identical recursive walkers in scan.ts, action-scan.ts,\n * ws-scan.ts (PV-3 — DRY consolidation). Resolves T3.1 of\n * architecture-review-remediation-plan. Six call sites consume it today, not\n * three; the count in this paragraph was true when it was written.\n *\n * Symlink loops are NOT tracked — callers must avoid them or pre-resolve\n * via `fs.realpath`. EC-11 documented but not implemented (rare in dev).\n */\nexport function walkSourceFiles(\n root: string,\n opts: WalkOptions,\n onFile: (absPath: string) => void,\n): void {\n const skipPrefixes = opts.skipPrefixes ?? ['_', '.']\n const readDir = opts.readDir ?? ((dir: string) => readdirSync(dir, { withFileTypes: true }))\n const visit = (dir: string): void => {\n let entries\n try {\n entries = readDir(dir)\n } catch {\n // Silently skip unreadable directories — caller controls discoverability\n return\n }\n // One total order over files and directories together, so a name sorts the\n // same way whichever it is. Sorting the two groups separately would emit a\n // different interleaving than a single pass, which is an ordering decision\n // nobody made (#346).\n const ordered = [...entries].sort((a, b) => compareByCodeUnit(a.name, b.name))\n for (const entry of ordered) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {\n visit(fullPath)\n } else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {\n onFile(resolve(fullPath))\n }\n }\n }\n visit(root)\n}\n","import { compareByCodeUnit } from '../_internal/compare-by-code-unit.js'\n\n/**\n * Router-convention errors thrown by the server-route scanner.\n *\n *\n * theokit 0.4.0+ enforces directory-nested file-system routing\n * (`auth/[provider]/login.ts`) and REJECTS dotted-basename routes\n * (`auth.[provider].login.ts`) because the legacy regex extracted the\n * dotted basename incorrectly — `params.provider` was undefined at request\n * time. Decision recorded in the plan above (`g6-router-convention-plan.md`) and CHANGELOG 0.4.0\n * (no standalone ADR was cut for this router-convention change).\n */\n\n/**\n * Canonical migration guide URL. T4.2 establishes this as the authoritative\n * landing page. EC-3: error message uses this constant so the URL never\n * drifts from the doc location.\n */\nexport const ROUTER_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/0.3-to-0.4-router'\n\ninterface RouterConventionErrorOptions {\n /** Absolute path of the offending route file. */\n file: string\n /** Suggested directory-nested replacement path (relative, e.g. `routes/auth/[provider]/login.ts`). */\n suggestion: string\n /** Migration guide URL (defaults to `ROUTER_MIGRATION_GUIDE_URL`). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file uses the legacy\n * dotted-basename convention (`auth.[provider].login.ts`).\n *\n * The error is FAIL-FAST by design — running with a route that has wrong\n * `paramNames` produces silent 404s at request time, which is strictly\n * worse than a build-time error.\n */\nexport class RouterConventionError extends Error {\n override readonly name = 'RouterConventionError'\n readonly file: string\n readonly suggestion: string\n readonly migrationUrl: string\n\n constructor(opts: RouterConventionErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? ROUTER_MIGRATION_GUIDE_URL\n const message = [\n `Router convention violation: dotted route basename is not supported in theokit 0.4+.`,\n ``,\n ` File: ${opts.file}`,\n ` Use directory-nested form: ${opts.suggestion}`,\n ``,\n `Migration guide: ${migrationUrl}`,\n `Run \\`theokit migrate router\\` to convert all dotted basenames automatically.`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.suggestion = opts.suggestion\n this.migrationUrl = migrationUrl\n }\n}\n\n/**\n * Canonical landing page for the policy-declaration migration. Held as a\n * constant for the same reason as {@link ROUTER_MIGRATION_GUIDE_URL}: the error\n * message and the doc cannot drift apart if only one of them names the URL.\n *\n * Module-local, unlike its sibling: nothing outside this file needs to name it,\n * and an export with no consumer is what `knip` exists to refuse.\n */\nconst ROUTE_POLICY_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/route-policy'\n\ninterface MissingRoutePolicyErrorOptions {\n /** Absolute path of the route file. */\n file: string\n /** The URL path the file resolves to, so the message reads like the app, not like the disk. */\n routePath: string\n /** The exported HTTP methods that declare no policy. */\n methods: string[]\n /** Migration guide URL (defaults to {@link ROUTE_POLICY_MIGRATION_GUIDE_URL}). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file exports an HTTP method that\n * declares no access policy (ADR 0001, Decision point 5).\n *\n * Absence used to mean \"not declared\", which every reader and every transport\n * had to interpret as open. The interpretation was the bug: a route nobody\n * thought about looked exactly like a route deliberately left open. This error\n * is where the two stop looking alike.\n *\n * It fires at scan time — `theo build`, `theo start`, `theo dev`, `theo routes`\n * and every deployment adapter go through the same scanner — so the answer\n * arrives before a request does, in the same place the scanner already refuses\n * a dotted basename and a collision with the reserved batch path.\n */\nexport class MissingRoutePolicyError extends Error {\n override readonly name = 'MissingRoutePolicyError'\n readonly file: string\n readonly routePath: string\n readonly methods: string[]\n readonly migrationUrl: string\n\n constructor(opts: MissingRoutePolicyErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? ROUTE_POLICY_MIGRATION_GUIDE_URL\n const methods = [...opts.methods].sort(compareByCodeUnit)\n const message = [\n `Route policy not declared: every route says who may call it (ADR 0001).`,\n ``,\n ` File: ${opts.file}`,\n ` Route: ${opts.routePath}`,\n ` Missing: ${methods.join(', ')}`,\n ``,\n `Add a policy to each method listed above:`,\n ``,\n // The import is part of the remedy, not decoration. This message shipped\n // naming `requireOwner` while no entry point exported it, so it read as\n // actionable and was not. `tests/unit/policy-gate-remedy-is-importable.test.ts`\n // asserts that every symbol named here is reachable from the path named here.\n ` import { route, requireOwner } from 'theokit/server/define'`,\n ``,\n ` export const ${methods[0] ?? 'GET'} = route()`,\n ` .policy('public') // anyone may call this`,\n ` .handler(...)`,\n ` .build()`,\n ``,\n ` export const ${methods[0] ?? 'GET'} = route()`,\n ` .policy(({ subject, params }) => requireOwner(subject, ownerOf(params.id)))`,\n ` .handler(...)`,\n ` .build()`,\n ``,\n `Writing 'public' is a decision and not a default. It states that this route`,\n `is open to anyone who can reach it, and it is greppable, so how much of the`,\n `app is open becomes a number somebody can read.`,\n ``,\n `Migration guide: ${migrationUrl}`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.routePath = opts.routePath\n this.methods = methods\n this.migrationUrl = migrationUrl\n }\n}\n\n/**\n * Canonical landing page for the agent-policy migration. Module-local for the same reason as its\n * route sibling: nothing outside this file names it, and an export with no consumer is what `knip`\n * exists to refuse.\n */\nconst AGENT_POLICY_MIGRATION_GUIDE_URL = 'https://theokit.dev/migration/agent-policy'\n\ninterface MissingAgentPolicyErrorOptions {\n /** Absolute path of the agent file. */\n file: string\n /** The URL the agent is served at, so the message reads like the app rather than like the disk. */\n agentPath: string\n /** Migration guide URL (defaults to {@link AGENT_POLICY_MIGRATION_GUIDE_URL}). */\n migrationUrl?: string\n}\n\n/**\n * Thrown by `scanAgents` when an agent file declares no access policy\n * (ADR 0001 Decision point 5, extended to the agent surface by usetheokit/theokit#365).\n *\n * ## Why absence had to stop meaning open HERE and not at runtime\n *\n * The agent endpoints resume a conversation the CALLER names, and they are dispatched before route\n * matching, so no route, no middleware and no `server/context.ts` ever saw those URLs. A caller\n * holding a conversation id and no credential read that conversation back.\n *\n * There is no safe runtime default for that. Refusing every caller-named session id would break\n * multi-turn chat, which is the base case; admitting them is the defect. So the decision moves to\n * where a person can answer it once, in the file that owns the agent — the same place and the same\n * reasoning as the route gate, which is why this error reads like its sibling.\n *\n * ## What it costs\n *\n * Every existing application with an agent fails its next build until it adds one line. That is the\n * half of ADR 0001 the ADR itself called breaking, and the trade it names: a build error that\n * points at a file beats a silent 200 that hands over somebody else's conversation.\n */\nexport class MissingAgentPolicyError extends Error {\n override readonly name = 'MissingAgentPolicyError'\n readonly file: string\n readonly agentPath: string\n readonly migrationUrl: string\n\n constructor(opts: MissingAgentPolicyErrorOptions) {\n const migrationUrl = opts.migrationUrl ?? AGENT_POLICY_MIGRATION_GUIDE_URL\n const message = [\n `Agent policy not declared: every agent says who may run it (ADR 0001).`,\n ``,\n ` File: ${opts.file}`,\n ` Agent: ${opts.agentPath}`,\n ``,\n `This one declaration covers every endpoint the agent exposes - the run, the`,\n `thread routes, the pending-approval listing, the approve route and MCP - because`,\n `they all reach the same conversation and the same paused tools.`,\n ``,\n `Add ONE of these to the file above:`,\n ``,\n ` export const policy = 'public' // anyone who can reach it may run it`,\n ``,\n ` import { requireOwner } from 'theokit/server/define'`,\n ``,\n ` // subject <- what server/context.ts put on ctx.subject`,\n ` // params <- { agent, endpoint, sessionId?, approvalId? }`,\n ` // body <- the parsed chat body, on the endpoints that have one`,\n ` export const policy = ({ subject, params }) =>`,\n ` requireOwner(subject, ownerOf(params.sessionId))`,\n ``,\n `Writing 'public' is a decision and not a default. The endpoint resumes whatever`,\n `conversation the CALLER names, so 'public' means any caller holding an id may read`,\n `and continue that conversation - a capability model, which is legitimate when it is`,\n `the choice somebody made and greppable once it is written down.`,\n ``,\n `Migration guide: ${migrationUrl}`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.agentPath = opts.agentPath\n this.migrationUrl = migrationUrl\n }\n}\n\ninterface RedundantApiSegmentErrorOptions {\n /** Absolute path of the offending route file. */\n file: string\n /** The path it resolves to today, with the doubled prefix. */\n doubledRoutePath: string\n /** Where the file belongs, relative and ready to copy. */\n suggestion: string\n /** The typed-client chain it produces today, with the redundant segment. */\n doubledClientChain: string\n}\n\n/**\n * Thrown by `scanServerRoutes` when a route file sits under `routes/api/`.\n *\n * `routes/` is already served under `/api`, so the directory doubles the prefix — and it does so\n * in two places at once, which is why this is an error rather than a lint.\n *\n * The URL is the visible half: `routes/api/auth/callback.ts` answers at `/api/api/auth/callback`,\n * which is not the redirect URI anybody registered with an identity provider. The second half\n * survives a reader's attention: `.theokit/client.d.ts` mirrors the file tree into the typed\n * client, so the same file produces `client.api.auth.callback.get()` — an `api` segment that reads\n * as a typo and is not one.\n *\n * Refusing rather than silently stripping the segment is deliberate. Collapsing would swap one\n * silent behaviour for another, and would let `routes/api/foo.ts` and `routes/foo.ts` resolve to\n * one URL — a collision needing its own error anyway.\n *\n * The `/api` prefix itself is not the defect. It is the boundary between what the server answers\n * and what the SPA answers, and three framework namespaces live under it (`/api/__actions/`,\n * `/api/agents/`, `/api/__theo_batch__`).\n */\nexport class RedundantApiSegmentError extends Error {\n override readonly name = 'RedundantApiSegmentError'\n readonly file: string\n readonly doubledRoutePath: string\n readonly suggestion: string\n\n constructor(opts: RedundantApiSegmentErrorOptions) {\n const message = [\n `Redundant 'api' directory: routes/ is already served under /api.`,\n ``,\n ` File: ${opts.file}`,\n ` Answers: ${opts.doubledRoutePath}`,\n ` Client: ${opts.doubledClientChain}`,\n ``,\n `Move it up one level:`,\n ``,\n ` ${opts.suggestion}`,\n ``,\n `Both halves are wrong from one cause. The URL is not the one you would register with an`,\n `identity provider or call from anywhere, and the generated typed client in`,\n `.theokit/client.d.ts carries the same redundant segment.`,\n ].join('\\n')\n super(message)\n this.file = opts.file\n this.doubledRoutePath = opts.doubledRoutePath\n this.suggestion = opts.suggestion\n }\n}\n","/**\n * Memoise per source file, invalidated by the file's own stamp.\n *\n * ## Why this exists\n *\n * `theokit dev` re-scans the routes and agents directories on EVERY request, so any work that\n * parses a file with the TypeScript AST turns a build-time check into per-request latency.\n * `agent-scan.ts` recognised this and carried its own `Map` keyed on `path:mtimeMs:size`;\n * `scan.ts` — with far more files — carried neither the cache nor the reasoning, and the\n * route-policy gate then added a SECOND full parse of every route file on top of the one\n * `detectExportedHttpMethods` had been paying since before it (usetheokit/theokit#417).\n *\n * Lifting the mechanism here rather than copying it is the point: two hand-rolled caches keyed\n * \"the same way\" are two chances for the keys to stop being the same.\n *\n * ## Why the stamp and not a watcher\n *\n * `mtimeMs` and `size` together come from the `statSync` the scanner is doing anyway, so an edit\n * invalidates the entry without anyone remembering to, and a `theokit build` — one process, one\n * scan — never notices the cache exists.\n *\n * The known limit, stated rather than discovered: a rewrite that preserves BOTH mtime and size is\n * invisible. That is a deliberate trade every mtime cache makes, and the alternative (hashing the\n * contents) reads the file to avoid reading the file.\n */\nimport { statSync } from 'node:fs'\n\nexport interface FileStampCache<T> {\n /** The memoised value for `filePath`, computing it when the file's stamp has changed. */\n get: (filePath: string, compute: () => T) => T\n /** Test seam — a module-level cache would otherwise outlive a fixture directory. */\n clear: () => void\n}\n\nexport function createFileStampCache<T>(): FileStampCache<T> {\n const entries = new Map<string, T>()\n\n return {\n get(filePath, compute) {\n // A path the scanner itself discovered by globbing the project, never caller input at runtime.\n // eslint-disable-next-line security/detect-non-literal-fs-filename -- see above\n const stat = statSync(filePath)\n const key = `${filePath}:${String(stat.mtimeMs)}:${String(stat.size)}`\n\n const hit = entries.get(key)\n // `has` rather than a truthiness check on the value: `false`, `0` and `''` are all legitimate\n // results to cache, and a truthiness test would recompute them on every call — which is the\n // defect this file exists to remove, wearing a subtler hat.\n if (hit !== undefined || entries.has(key)) return hit as T\n\n const value = compute()\n entries.set(key, value)\n return value\n },\n clear() {\n entries.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/core/contracts/action-protocol.ts
|
|
2
|
+
var CODE_TO_STATUS = {
|
|
3
|
+
VALIDATION_ERROR: 422,
|
|
4
|
+
BAD_REQUEST: 400,
|
|
5
|
+
UNAUTHORIZED: 401,
|
|
6
|
+
FORBIDDEN: 403,
|
|
7
|
+
NOT_FOUND: 404,
|
|
8
|
+
METHOD_NOT_ALLOWED: 405,
|
|
9
|
+
CONFLICT: 409,
|
|
10
|
+
CONTENT_TOO_LARGE: 413,
|
|
11
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
12
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
13
|
+
TOO_MANY_REQUESTS: 429,
|
|
14
|
+
INTERNAL_SERVER_ERROR: 500
|
|
15
|
+
};
|
|
16
|
+
var STATUS_TO_CODE = {
|
|
17
|
+
400: "BAD_REQUEST",
|
|
18
|
+
401: "UNAUTHORIZED",
|
|
19
|
+
403: "FORBIDDEN",
|
|
20
|
+
404: "NOT_FOUND",
|
|
21
|
+
405: "METHOD_NOT_ALLOWED",
|
|
22
|
+
409: "CONFLICT",
|
|
23
|
+
413: "PAYLOAD_TOO_LARGE",
|
|
24
|
+
415: "UNSUPPORTED_MEDIA_TYPE",
|
|
25
|
+
422: "VALIDATION_ERROR",
|
|
26
|
+
429: "TOO_MANY_REQUESTS",
|
|
27
|
+
500: "INTERNAL_SERVER_ERROR"
|
|
28
|
+
};
|
|
29
|
+
var ActionError = class _ActionError extends Error {
|
|
30
|
+
// Discriminator widened to the full union so subclasses can narrow to their
|
|
31
|
+
// specific literal (TypeScript would otherwise reject the override). Concrete
|
|
32
|
+
// values are still always exact literals at runtime.
|
|
33
|
+
type = "TheoActionError";
|
|
34
|
+
code;
|
|
35
|
+
status;
|
|
36
|
+
constructor(params) {
|
|
37
|
+
super(params.message ?? params.code);
|
|
38
|
+
this.code = params.code;
|
|
39
|
+
this.status = _ActionError.codeToStatus(params.code);
|
|
40
|
+
if (params.stack) {
|
|
41
|
+
this.stack = params.stack;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* G5 T2.4 — canonical envelope view of the action error. Maps the G3
|
|
46
|
+
* ActionErrorCode to a canonical TheoErrorCode (VALIDATION_ERROR ↔
|
|
47
|
+
* UNPROCESSABLE_ENTITY, CONTENT_TOO_LARGE ↔ PAYLOAD_TOO_LARGE) so consumer
|
|
48
|
+
* UI / SDK code can switch on the unified envelope.
|
|
49
|
+
*
|
|
50
|
+
* Subclasses override to populate `ext` (see `ActionInputError.envelope`).
|
|
51
|
+
*/
|
|
52
|
+
get envelope() {
|
|
53
|
+
return {
|
|
54
|
+
code: _ActionError.toTheoErrorCode(this.code),
|
|
55
|
+
message: this.message
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Translate G3 ActionErrorCode → canonical TheoErrorCode (blueprint
|
|
60
|
+
* Recommendations § "G3 ActionError becomes inaugural envelope user").
|
|
61
|
+
*/
|
|
62
|
+
static toTheoErrorCode(code) {
|
|
63
|
+
if (code === "VALIDATION_ERROR") return "UNPROCESSABLE_ENTITY";
|
|
64
|
+
if (code === "CONTENT_TOO_LARGE") return "PAYLOAD_TOO_LARGE";
|
|
65
|
+
return code;
|
|
66
|
+
}
|
|
67
|
+
static codeToStatus(code) {
|
|
68
|
+
return CODE_TO_STATUS[code];
|
|
69
|
+
}
|
|
70
|
+
static statusToCode(status) {
|
|
71
|
+
return STATUS_TO_CODE[status] ?? "INTERNAL_SERVER_ERROR";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a serialized error JSON back into the typed class hierarchy.
|
|
75
|
+
* Distinguishes `TheoActionInputError` (with `issues` array) from
|
|
76
|
+
* `TheoActionError` via the `type` discriminator. Falls back to
|
|
77
|
+
* `INTERNAL_SERVER_ERROR` for malformed bodies (non-object, missing
|
|
78
|
+
* `type`, unknown `code`).
|
|
79
|
+
*/
|
|
80
|
+
static fromJson(body) {
|
|
81
|
+
if (typeof body !== "object" || body === null) {
|
|
82
|
+
return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
|
|
83
|
+
}
|
|
84
|
+
const obj = body;
|
|
85
|
+
if (obj.type === "TheoActionInputError" && Array.isArray(obj.issues)) {
|
|
86
|
+
return new ActionInputError(obj.issues);
|
|
87
|
+
}
|
|
88
|
+
if (obj.type === "TheoActionError" && typeof obj.code === "string" && obj.code in CODE_TO_STATUS) {
|
|
89
|
+
return new _ActionError({
|
|
90
|
+
code: obj.code,
|
|
91
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var ActionInputError = class extends ActionError {
|
|
98
|
+
type = "TheoActionInputError";
|
|
99
|
+
issues;
|
|
100
|
+
fields;
|
|
101
|
+
constructor(rawIssues) {
|
|
102
|
+
super({ code: "VALIDATION_ERROR", message: "Validation failed" });
|
|
103
|
+
this.issues = extractUniversalIssues(rawIssues);
|
|
104
|
+
this.fields = buildFieldsMap(this.issues);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* G5 T2.4 — envelope view with ValidationFieldsExt populated from .fields.
|
|
108
|
+
* UI consumers can `switch (env.code)` on `UNPROCESSABLE_ENTITY` and read
|
|
109
|
+
* `(env.ext as ValidationFieldsExt).fields` for field-level rendering.
|
|
110
|
+
*/
|
|
111
|
+
get envelope() {
|
|
112
|
+
return {
|
|
113
|
+
code: "UNPROCESSABLE_ENTITY",
|
|
114
|
+
message: this.message,
|
|
115
|
+
ext: { fields: this.fields }
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function buildFieldsMap(issues) {
|
|
120
|
+
const fields = {};
|
|
121
|
+
const seen = /* @__PURE__ */ new Set();
|
|
122
|
+
for (const issue of issues) {
|
|
123
|
+
const key = issue.path.length === 0 ? "" : issue.path.join(".");
|
|
124
|
+
const dedupeKey = `${key}\0${issue.message}`;
|
|
125
|
+
if (seen.has(dedupeKey)) continue;
|
|
126
|
+
seen.add(dedupeKey);
|
|
127
|
+
const bucket = fields[key] ?? [];
|
|
128
|
+
bucket.push(issue.message);
|
|
129
|
+
fields[key] = bucket;
|
|
130
|
+
}
|
|
131
|
+
return fields;
|
|
132
|
+
}
|
|
133
|
+
function extractUniversalIssues(raw) {
|
|
134
|
+
if (!Array.isArray(raw)) return [];
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const entry of raw) {
|
|
137
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
138
|
+
const obj = entry;
|
|
139
|
+
if (!Array.isArray(obj.path)) continue;
|
|
140
|
+
if (typeof obj.message !== "string") continue;
|
|
141
|
+
const path = [];
|
|
142
|
+
let pathValid = true;
|
|
143
|
+
for (const seg of obj.path) {
|
|
144
|
+
if (typeof seg === "string" || typeof seg === "number") {
|
|
145
|
+
path.push(seg);
|
|
146
|
+
} else {
|
|
147
|
+
pathValid = false;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!pathValid) continue;
|
|
152
|
+
out.push({
|
|
153
|
+
path,
|
|
154
|
+
message: obj.message,
|
|
155
|
+
code: typeof obj.code === "string" ? obj.code : void 0
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function isActionError(value) {
|
|
161
|
+
return value instanceof ActionError;
|
|
162
|
+
}
|
|
163
|
+
function isInputError(value) {
|
|
164
|
+
return value instanceof ActionInputError;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
ActionError,
|
|
169
|
+
ActionInputError,
|
|
170
|
+
extractUniversalIssues,
|
|
171
|
+
isActionError,
|
|
172
|
+
isInputError
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=chunk-ZDUO4IYN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/contracts/action-protocol.ts"],"mappings":"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","names":[]}
|
package/dist/cli/index.js
CHANGED
|
@@ -15,12 +15,12 @@ function cliVersion() {
|
|
|
15
15
|
// src/cli/index.ts
|
|
16
16
|
var cli = cac("theokit");
|
|
17
17
|
cli.command("dev", "Start development server").option("--port <port>", "Port number").action(async (options) => {
|
|
18
|
-
const { devCommand } = await import("../dev-
|
|
18
|
+
const { devCommand } = await import("../dev-W7OXHMSP.js");
|
|
19
19
|
await devCommand({ port: options.port ? Number(options.port) : void 0 });
|
|
20
20
|
});
|
|
21
21
|
cli.command("build", "Build for production").option("--target <target>", "Deploy target (node, vercel, cloudflare)").action(async (options) => {
|
|
22
22
|
try {
|
|
23
|
-
const { buildCommand } = await import("../build-
|
|
23
|
+
const { buildCommand } = await import("../build-3AYYQZGQ.js");
|
|
24
24
|
await buildCommand({ target: options.target });
|
|
25
25
|
} catch (err) {
|
|
26
26
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -32,7 +32,7 @@ cli.command("build", "Build for production").option("--target <target>", "Deploy
|
|
|
32
32
|
});
|
|
33
33
|
cli.command("start", "Start production server").option("--port <port>", "Port number").action(async (options) => {
|
|
34
34
|
try {
|
|
35
|
-
const { startCommand } = await import("../start-
|
|
35
|
+
const { startCommand } = await import("../start-WILEDDW2.js");
|
|
36
36
|
await startCommand({ port: options.port ? Number(options.port) : void 0 });
|
|
37
37
|
} catch (err) {
|
|
38
38
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -44,7 +44,7 @@ cli.command("start", "Start production server").option("--port <port>", "Port nu
|
|
|
44
44
|
});
|
|
45
45
|
cli.command("preview", "Build for production, then serve it \u2014 one step (B-030)").option("--port <port>", "Port number").option("--target <target>", "Deploy target (node, vercel, cloudflare)").action(async (options) => {
|
|
46
46
|
try {
|
|
47
|
-
const { previewCommand } = await import("../preview-
|
|
47
|
+
const { previewCommand } = await import("../preview-4ZTFJG2D.js");
|
|
48
48
|
await previewCommand({
|
|
49
49
|
port: options.port ? Number(options.port) : void 0,
|
|
50
50
|
target: options.target
|
|
@@ -77,7 +77,7 @@ cli.command(
|
|
|
77
77
|
"Run an agent in the terminal (stream + tool calls + approval)"
|
|
78
78
|
).action(async (name, message) => {
|
|
79
79
|
try {
|
|
80
|
-
const { agentCommand } = await import("../agent-
|
|
80
|
+
const { agentCommand } = await import("../agent-AHGYRVQX.js");
|
|
81
81
|
const { sawError } = await agentCommand(name, message);
|
|
82
82
|
if (sawError) process.exit(1);
|
|
83
83
|
} catch (err) {
|
|
@@ -108,7 +108,7 @@ cli.command("agent sessions gc", "Collect old transcripts for this project (dry
|
|
|
108
108
|
});
|
|
109
109
|
cli.command("mcp <agent>", "Serve an agent as an MCP server over stdio (for desktop MCP clients)").action(async (agent) => {
|
|
110
110
|
try {
|
|
111
|
-
const { mcpCommand } = await import("../mcp-
|
|
111
|
+
const { mcpCommand } = await import("../mcp-7BXBN7PD.js");
|
|
112
112
|
await mcpCommand(agent);
|
|
113
113
|
} catch (err) {
|
|
114
114
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -120,7 +120,7 @@ cli.command("mcp <agent>", "Serve an agent as an MCP server over stdio (for desk
|
|
|
120
120
|
});
|
|
121
121
|
cli.command("routes", "List all routes, actions, and WebSocket endpoints").action(async () => {
|
|
122
122
|
try {
|
|
123
|
-
const { routesCommand } = await import("../routes-
|
|
123
|
+
const { routesCommand } = await import("../routes-PP5PHHLR.js");
|
|
124
124
|
await routesCommand();
|
|
125
125
|
} catch (err) {
|
|
126
126
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -173,7 +173,7 @@ cli.command(
|
|
|
173
173
|
"Generate <distDir>/openapi.json from route schemas (opt-in via config.openapi)"
|
|
174
174
|
).option("--dry-run", "Print the document to stdout without writing to disk (EC-3)").action(async (options) => {
|
|
175
175
|
try {
|
|
176
|
-
const { openapiCommand } = await import("../openapi-
|
|
176
|
+
const { openapiCommand } = await import("../openapi-2IBBOS3Q.js");
|
|
177
177
|
await openapiCommand({ dryRun: Boolean(options.dryRun) });
|
|
178
178
|
} catch (err) {
|
|
179
179
|
const msg = err instanceof Error ? err.message : String(err);
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { z } from 'zod';
|
|
|
2
2
|
import { T as TheoErrorEnvelope } from '../error-envelope-DG47lt8E.js';
|
|
3
3
|
export { FetchOptionsLike, Fetcher, QueryKey, UseTheoQueryConfig, buildUseTheoQueryConfig, stableQueryKey } from '../react-query/index.js';
|
|
4
4
|
export { AgentClient, AgentClientState, AgentHandle, AgentStreamInterruptedError, AgentTransport, ApprovalDecision, ChannelPushSource, ChannelTransport, ChannelTransportOptions, ChannelTurnHandlers, HttpTransport, HttpTransportOptions, InProcessApprovalRequestLike, InProcessAwaitApproval, InProcessRunInput, InProcessRunner, InProcessTransport, InProcessTransportOptions, RequestContext, agentHandle, consumeChunkStream, consumeUIMessageStream, isAgentHandle, responseToChunkStream } from '@theokit/agents/client';
|
|
5
|
+
import { A as ActionError } from '../action-protocol-tGm3NKRg.js';
|
|
6
|
+
export { a as ActionErrorCode, b as ActionInputError, c as ActionResult, i as isActionError, d as isInputError } from '../action-protocol-tGm3NKRg.js';
|
|
5
7
|
export { PendingApproval, UseAgentOptions, UseAgentReturn, UseAgentStatus, useAgent } from '@theokit/agents/client/react';
|
|
6
8
|
export { AgentClientHandle, CreateAgentClientOptions, createAgentClient } from './core.js';
|
|
7
9
|
import * as react from 'react';
|
|
@@ -118,6 +120,120 @@ interface Batcher {
|
|
|
118
120
|
}
|
|
119
121
|
declare function createBatcher(options: BatcherOptions): Batcher;
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* usetheokit/theokit#453 — the framework-agnostic store behind {@link useAction}.
|
|
125
|
+
*
|
|
126
|
+
* `core/contracts/action-protocol.ts` opens by describing itself as the "cross-boundary contract
|
|
127
|
+
* for `defineAction` + `useAction`" and points the client half at `@theokit/react/useAction` — a
|
|
128
|
+
* package outside this repository, with one version, no `repository` field, and a
|
|
129
|
+
* `@theokit/sdk ^1.1.0` peer against a published 4.x. The server half of that contract has always
|
|
130
|
+
* lived here; this is the client half arriving.
|
|
131
|
+
*
|
|
132
|
+
* The state machine lives in a store rather than in the hook, following `useAgent`: it is testable
|
|
133
|
+
* with no DOM, and a non-React surface can subscribe to it directly. The hook in `use-action.ts` is
|
|
134
|
+
* a `useSyncExternalStore` binding over it.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* What a generated callable resolves.
|
|
138
|
+
*
|
|
139
|
+
* Deliberately NOT `ActionResult`, whose `error` is an `ActionError` instance: the envelope crosses
|
|
140
|
+
* the wire as JSON, so what arrives is a plain object, and a callable typed against the class would
|
|
141
|
+
* be one no honest implementation can satisfy. `error` is `unknown` here and normalized on the way
|
|
142
|
+
* into the state.
|
|
143
|
+
*
|
|
144
|
+
* Both keys are present — the generated facade always emits both — which is also what tells an
|
|
145
|
+
* envelope apart from a payload that merely has a field named `data` or `error`.
|
|
146
|
+
*/
|
|
147
|
+
interface ActionEnvelope<TData = unknown> {
|
|
148
|
+
readonly data: TData | undefined;
|
|
149
|
+
readonly error: unknown;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A callable shaped like the ones `@theo/actions` generates. They resolve an
|
|
153
|
+
* {@link ActionEnvelope} rather than throwing — including for network failure — but a hand-written
|
|
154
|
+
* callable that resolves a bare value or throws is handled too, since consumers pass both.
|
|
155
|
+
*/
|
|
156
|
+
type ActionCallable<TInput = unknown, TData = unknown> = (input: TInput) => Promise<ActionEnvelope<TData> | TData>;
|
|
157
|
+
type ActionStatus = 'idle' | 'pending' | 'error' | 'success';
|
|
158
|
+
/** The observable state, one frozen object per transition (`useSyncExternalStore` contract). */
|
|
159
|
+
interface ActionState<TInput = unknown, TData = unknown> {
|
|
160
|
+
readonly status: ActionStatus;
|
|
161
|
+
readonly data: TData | undefined;
|
|
162
|
+
readonly error: ActionError | undefined;
|
|
163
|
+
/** The input of the most recent call — what a retry button replays. */
|
|
164
|
+
readonly variables: TInput | undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Holds `{status, data, error, variables}` for one action callable and notifies subscribers on each
|
|
168
|
+
* transition.
|
|
169
|
+
*/
|
|
170
|
+
declare class ActionClient<TInput = unknown, TData = unknown> {
|
|
171
|
+
#private;
|
|
172
|
+
constructor(action: ActionCallable<TInput, TData>);
|
|
173
|
+
/** Subscribe to state changes; returns an unsubscribe fn. */
|
|
174
|
+
subscribe: (listener: () => void) => (() => void);
|
|
175
|
+
/**
|
|
176
|
+
* The current snapshot, stable by reference until the next transition. Allocating here instead
|
|
177
|
+
* would re-render a React consumer forever.
|
|
178
|
+
*/
|
|
179
|
+
getSnapshot: () => ActionState<TInput, TData>;
|
|
180
|
+
/** Invoke the action, resolving its data or rejecting with a typed {@link ActionError}. */
|
|
181
|
+
mutateAsync: (input: TInput) => Promise<TData>;
|
|
182
|
+
/**
|
|
183
|
+
* Fire-and-forget: the outcome lands in the state, and the returned promise is consumed here so a
|
|
184
|
+
* failure does not surface as an unhandled rejection in a component that only reads `isError`.
|
|
185
|
+
*/
|
|
186
|
+
mutate: (input: TInput) => void;
|
|
187
|
+
/** Back to idle, discarding whatever is in flight. */
|
|
188
|
+
reset: () => void;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* usetheokit/theokit#453 — call a server action from a component and track its state.
|
|
193
|
+
*
|
|
194
|
+
* The framework already generates the typed callable (`@theo/actions`), serves it at
|
|
195
|
+
* `/api/__actions/`, and defines the error hierarchy it answers with. This is the last piece:
|
|
196
|
+
*
|
|
197
|
+
* ```tsx
|
|
198
|
+
* import { actions } from '@theo/actions'
|
|
199
|
+
* import { useAction } from 'theokit/client'
|
|
200
|
+
*
|
|
201
|
+
* function SaveButton() {
|
|
202
|
+
* const save = useAction(actions.saveMemory)
|
|
203
|
+
* return (
|
|
204
|
+
* <button disabled={save.isPending} onClick={() => save.mutate({ content: 'hi' })}>
|
|
205
|
+
* {save.isPending ? 'Saving…' : 'Save'}
|
|
206
|
+
* </button>
|
|
207
|
+
* )
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* A failure lands in `error` as the protocol's own {@link ActionError} — so a validation failure is
|
|
212
|
+
* an `ActionInputError` with its `fields` map intact, which is what a form library binds to.
|
|
213
|
+
*
|
|
214
|
+
* The state machine lives in {@link ActionClient}; this is a `useSyncExternalStore` binding over it,
|
|
215
|
+
* the same shape as `useAgent`.
|
|
216
|
+
*/
|
|
217
|
+
interface UseActionResult<TInput = unknown, TData = unknown> {
|
|
218
|
+
/** The data of the most recent successful call. */
|
|
219
|
+
data: TData | undefined;
|
|
220
|
+
/** The typed error of the most recent failed call. */
|
|
221
|
+
error: ActionError | undefined;
|
|
222
|
+
isIdle: boolean;
|
|
223
|
+
isPending: boolean;
|
|
224
|
+
isError: boolean;
|
|
225
|
+
isSuccess: boolean;
|
|
226
|
+
/** The input of the most recent call — what a retry button replays. */
|
|
227
|
+
variables: TInput | undefined;
|
|
228
|
+
/** Fire and forget; read the outcome off `isPending` / `error`. */
|
|
229
|
+
mutate: (input: TInput) => void;
|
|
230
|
+
/** Await the data, or catch the typed {@link ActionError}. */
|
|
231
|
+
mutateAsync: (input: TInput) => Promise<TData>;
|
|
232
|
+
/** Back to idle, discarding whatever is in flight. */
|
|
233
|
+
reset: () => void;
|
|
234
|
+
}
|
|
235
|
+
declare function useAction<TInput = unknown, TData = unknown>(action: ActionCallable<TInput, TData>): UseActionResult<TInput, TData>;
|
|
236
|
+
|
|
121
237
|
type PrefetchBehavior = 'none' | 'intent' | 'viewport';
|
|
122
238
|
interface LinkProps extends LinkProps$1 {
|
|
123
239
|
/** Prefetch strategy. Default: 'intent' (on hover + focus). */
|
|
@@ -299,4 +415,4 @@ declare function mountMcpApp(container: HTMLElement, resource: {
|
|
|
299
415
|
html: string;
|
|
300
416
|
}, opts: McpAppHostOptions): McpAppHandle;
|
|
301
417
|
|
|
302
|
-
export { type BatchRequest, type BatchResponse, type BatchTransport, type Batcher, type BatcherOptions, type CreateAppClientOptions, type GuestMessage, Image, type ImageProps, type InferBody, type InferQuery, type InferResponse, Link, type LinkProps, MCP_APP_SANDBOX, type McpAppHandle, type McpAppHostOptions, Metadata, type MetadataProps, type PrefetchBehavior, TheoFetchError, type TheoFetchOptions, createAppClient, createBatcher, createGuestMessageHandler, mountMcpApp, theoFetch };
|
|
418
|
+
export { type ActionCallable, ActionClient, type ActionEnvelope, ActionError, type ActionState, type ActionStatus, type BatchRequest, type BatchResponse, type BatchTransport, type Batcher, type BatcherOptions, type CreateAppClientOptions, type GuestMessage, Image, type ImageProps, type InferBody, type InferQuery, type InferResponse, Link, type LinkProps, MCP_APP_SANDBOX, type McpAppHandle, type McpAppHostOptions, Metadata, type MetadataProps, type PrefetchBehavior, TheoFetchError, type TheoFetchOptions, type UseActionResult, createAppClient, createBatcher, createGuestMessageHandler, mountMcpApp, theoFetch, useAction };
|