@happyvertical/smrt-core 0.46.0 → 0.47.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.
Files changed (90) hide show
  1. package/AGENTS.md +39 -0
  2. package/agents/generators.md +19 -5
  3. package/agents/system-diagnostics.md +43 -0
  4. package/dist/browser.js +2 -1
  5. package/dist/change-feed.d.ts +8 -0
  6. package/dist/change-feed.d.ts.map +1 -1
  7. package/dist/change-feed.js +1 -1
  8. package/dist/change-feed.js.map +1 -1
  9. package/dist/decorators/compatibility.d.ts +46 -0
  10. package/dist/decorators/compatibility.d.ts.map +1 -1
  11. package/dist/decorators/compatibility.js +48 -5
  12. package/dist/decorators/compatibility.js.map +1 -1
  13. package/dist/decorators/index.d.ts +119 -1
  14. package/dist/decorators/index.d.ts.map +1 -1
  15. package/dist/decorators/index.js +52 -8
  16. package/dist/decorators/index.js.map +1 -1
  17. package/dist/generators/custom-action.d.ts +379 -2
  18. package/dist/generators/custom-action.d.ts.map +1 -1
  19. package/dist/generators/custom-action.js +694 -17
  20. package/dist/generators/custom-action.js.map +1 -1
  21. package/dist/generators/index.d.ts +1 -1
  22. package/dist/generators/index.d.ts.map +1 -1
  23. package/dist/generators/index.js +2 -2
  24. package/dist/generators/preflight-route.d.ts +12 -10
  25. package/dist/generators/preflight-route.d.ts.map +1 -1
  26. package/dist/generators/preflight-route.js +46 -14
  27. package/dist/generators/preflight-route.js.map +1 -1
  28. package/dist/generators/rest.d.ts +44 -0
  29. package/dist/generators/rest.d.ts.map +1 -1
  30. package/dist/generators/rest.js +84 -9
  31. package/dist/generators/rest.js.map +1 -1
  32. package/dist/generators.js +2 -2
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -4
  36. package/dist/knowledge.d.ts.map +1 -1
  37. package/dist/knowledge.js +59 -15
  38. package/dist/knowledge.js.map +1 -1
  39. package/dist/manifest/static-manifest.d.ts.map +1 -1
  40. package/dist/manifest/static-manifest.js +125 -53
  41. package/dist/manifest/static-manifest.js.map +1 -1
  42. package/dist/manifest/store.js +1 -1
  43. package/dist/manifest/store.js.map +1 -1
  44. package/dist/manifest.json +184 -53
  45. package/dist/postgres-permissions.d.ts +9 -1
  46. package/dist/postgres-permissions.d.ts.map +1 -1
  47. package/dist/postgres-permissions.js +127 -13
  48. package/dist/postgres-permissions.js.map +1 -1
  49. package/dist/registry/index.d.ts +1 -1
  50. package/dist/registry/index.d.ts.map +1 -1
  51. package/dist/registry/shared-state.d.ts +19 -0
  52. package/dist/registry/shared-state.d.ts.map +1 -1
  53. package/dist/registry/shared-state.js +16 -1
  54. package/dist/registry/shared-state.js.map +1 -1
  55. package/dist/registry.d.ts +136 -1
  56. package/dist/registry.d.ts.map +1 -1
  57. package/dist/registry.js +231 -1
  58. package/dist/registry.js.map +1 -1
  59. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  60. package/dist/scanner/manifest-generator.js +18 -16
  61. package/dist/scanner/manifest-generator.js.map +1 -1
  62. package/dist/scanner/types.d.ts +59 -6
  63. package/dist/scanner/types.d.ts.map +1 -1
  64. package/dist/scanner/types.js.map +1 -1
  65. package/dist/smrt-knowledge.json +42 -35
  66. package/dist/system/diagnostics.d.ts +299 -0
  67. package/dist/system/diagnostics.d.ts.map +1 -0
  68. package/dist/system/diagnostics.js +530 -0
  69. package/dist/system/diagnostics.js.map +1 -0
  70. package/dist/system/index.d.ts +1 -0
  71. package/dist/system/index.d.ts.map +1 -1
  72. package/dist/system/index.js +2 -1
  73. package/dist/utils/scanner-module.d.ts +16 -0
  74. package/dist/utils/scanner-module.d.ts.map +1 -1
  75. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  76. package/dist/vite-plugin/api-client-entries.js +10 -8
  77. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  78. package/dist/vite-plugin/index.d.ts +34 -1
  79. package/dist/vite-plugin/index.d.ts.map +1 -1
  80. package/dist/vite-plugin/index.js +82 -3
  81. package/dist/vite-plugin/index.js.map +1 -1
  82. package/dist/vite-plugin/sveltekit-generator.d.ts +28 -2
  83. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  84. package/dist/vite-plugin/sveltekit-generator.js +121 -51
  85. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  86. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  87. package/dist/vite-plugin/web-collections.js +1 -1
  88. package/dist/vite-plugin/web-collections.js.map +1 -1
  89. package/dist/vite-plugin.js +2 -2
  90. package/package.json +16 -11
@@ -1 +1 @@
1
- {"version":3,"file":"custom-action.js","names":[],"sources":["../../src/generators/custom-action.ts"],"sourcesContent":["/**\n * Canonical custom-action metadata and transport-safe result helpers.\n *\n * Custom actions are intentionally distinct from generated CRUD. Their target\n * is derived from method metadata: instance methods target an item and static\n * methods target the collection. API route configuration may shape an HTTP\n * route, but it cannot change a method's receiver.\n */\n\nimport type { ToolEffect } from '../registry/types.js';\nimport type { MethodDefinition } from '../scanner/types.js';\nimport { convertTypeToJsonSchema } from '../tools/tool-generator.js';\n\nexport type CustomActionScope = 'item' | 'collection';\nexport type { ToolEffect } from '../registry/types.js';\n\n/**\n * Public method names declared directly on `SmrtObject`/`SmrtClass`\n * (`src/object.ts`, `src/class.ts`) that form the constructor → `initialize()`\n * → `save()`/`delete()`/`loadFromId()` lifecycle and its immediate supporting\n * mechanism: identity/persistence bookkeeping, transaction binding, and\n * serialization. `save()` is what generated `create`/`update` call;\n * `initialize()` is what `get`/`list` hydration calls; `loadFromId()`/\n * `loadFromSlug()` are what `get()` calls; `toJSON()`/`toPublicJSON()` are\n * what every read serializes through. None of these are a subclass-specific\n * operation, so none is exposed as a generated CLI/MCP custom action --\n * even when a subclass declares its own override (e.g. `User.save()` at\n * `packages/users/src/models/User.ts`). An override is still the same\n * lifecycle operation, not a new one (#2638). `delete` itself is a CRUD verb\n * `packages/cli/src/cli-generator.ts`'s `CLIGenerator`/`MCPGenerator` already\n * special-case, so it is not repeated here.\n *\n * Scope is deliberately narrower than \"every public method on\n * SmrtObject/SmrtClass/SmrtCollection\":\n *\n * - AI operations `is()`/`do()`/`describe()` are declared on `SmrtObject`\n * but are explicitly designed to be overridden with domain-specific\n * behavior and exposed as a distinct action -- confirmed by existing,\n * intentional coverage\n * (`vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.\n * describe(tone)` with its own declared API route). Excluding them here\n * would regress real, working behavior. (The sibling\n * `generators/cli-commands.spec.ts` fixture that used to cover the same\n * `describe()` custom action was retired with core's `CLIGenerator`,\n * #2664; this remaining fixture still exercises the behavior.)\n * - Relationship loading (`loadRelated`/`loadRelatedMany`/`getRelated`/\n * `isRelatedLoaded`), memory (`remember`/`recall`/`recallAll`/`forget`/\n * `forgetScope`), embeddings (`generateEmbeddings`/`getEmbedding`/\n * `hasStaleEmbeddings`/`clearEmbeddings`), and AI-usage introspection\n * (`getAiUsageSnapshot`/`resetAiUsage`/`listAiUsage`/`summarizeAiUsage`)\n * are generic capabilities a class may legitimately want to trigger or\n * report on as a distinct action (e.g. \"regenerate this record's\n * embeddings\"), not \"the mechanism behind CRUD\" the way `save`/\n * `initialize` are. Nothing in the measured #2638 residual touches them,\n * so blanket-excluding them is a separate, unevidenced call this fix does\n * not make.\n * - `SmrtCollection`'s own surface (`count`, `facets`, `query`, `findOne`,\n * `generateMissingEmbeddings`, ...) is excluded entirely for the same\n * reason: the measured residual is 100% item-class overrides (`User`,\n * `Invoice`, `Payment`, ...), never a collection-class override, and\n * `count`/`facets` read as genuinely distinct query capabilities in\n * existing fixtures elsewhere in the repo (e.g.\n * `packages/content/src/server/content-list-actions.test.ts`). Extending\n * this rule to `SmrtCollection` needs its own review, not a ride-along\n * here.\n *\n * This can only be a METHOD-NAME list, not a reuse of the existing\n * class-name sets (`FRAMEWORK_METHOD_BASE_NAMES` in\n * `scanner/manifest-generator.ts`, the registry's own by-name skip in\n * `registry/inheritance-resolver.ts`, or the broader 8-class\n * `FRAMEWORK_BASE_CLASSES` those two modules also cross-reference for the\n * unrelated schema-field-merge question): those sets answer \"is this\n * ancestor one of the excluded classes\", which only ever decides whether to\n * MERGE an ancestor's methods onto a subclass that does not declare them.\n * A locally declared override -- the #2638 case -- IS the subclass's own\n * method; there is no ancestor lookup to skip, because the method that\n * needs excluding was never inherited in the first place. The same\n * plumbing-vs-operation judgment #2624 made at the class level has to be\n * re-expressed as a method-name list to reach the override case too, which\n * is why this is a fifth name list rather than a reuse of one of the four.\n *\n * A static, hand-maintained list (not runtime introspection of the actual\n * `SmrtObject`/`SmrtClass` prototypes) matches the existing pattern for all\n * four sibling lists above, and keeps this transport-neutral module usable\n * from a pure manifest/AST build path (`vite-plugin/sveltekit-generator.ts`)\n * that has no live class registry to introspect. Re-derive this list from\n * those two files' own public method signatures if they change.\n */\nexport const FRAMEWORK_LIFECYCLE_METHOD_NAMES: ReadonlySet<string> = new Set([\n // SmrtClass (src/class.ts) — resource/transaction plumbing.\n 'destroy',\n 'withDatabase',\n // SmrtObject (src/object.ts) — identity/persistence lifecycle mechanism.\n 'initialize',\n 'loadDataFromDb',\n 'getFields',\n 'toJSON',\n 'toPlainObject',\n 'toPublicJSON',\n 'getId',\n 'getSlug',\n 'getSavedId',\n 'isSaved',\n 'save',\n 'claimRevision',\n 'classifyConstraintError',\n 'loadFromId',\n 'loadFromSlug',\n 'markAsPersisted',\n 'requireInsertOnSave',\n 'withTransaction',\n]);\n\n/**\n * True when `methodName` is one of the universal `SmrtObject`/`SmrtClass`\n * lifecycle methods above — the mechanism behind generated CRUD, never a\n * subclass-specific custom action, even when the subclass declares its own\n * override.\n */\nexport function isFrameworkLifecycleMethod(methodName: string): boolean {\n return FRAMEWORK_LIFECYCLE_METHOD_NAMES.has(methodName);\n}\n\n/** Minimal shape `resolveCustomActionNames` needs from a method entry. */\nexport interface ResolvableMethod {\n isPublic: boolean;\n}\n\n/**\n * Resolve the effective set of custom (non-CRUD) command/tool names exposed\n * for an object, given its transport config's `include`/`exclude` and its\n * method map: every public method, minus CRUD verbs, minus framework\n * lifecycle methods, restricted to `include` when present and always minus\n * `exclude`. Its sole caller as of #2664 (`CLIGenerator` and the\n * `generateCLIModule()` virtual module were retired):\n *\n * - `findCliApiCoherenceViolations`'s bare-`cli: true`/`cli: {}` branch\n * (over the static manifest, no explicit `include`) -- see\n * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts`.\n *\n * NOT the one universal resolution, and deliberately not reused by every\n * caller that resolves a CLI command set:\n *\n * - Core's now-retired `CLIGenerator.assertCommandExposed()` (#2664) did not\n * call this for its custom-method branch — it checked\n * `isFrameworkLifecycleMethod()` directly plus its own inline\n * public/include/exclude logic, so it could give a distinct error message\n * per failure reason (unknown vs. not public vs. not enabled vs. lifecycle\n * method) rather than a single boolean membership test. The shipped local\n * CLI's `generateObjectCommands()` (`packages/cli/src/cli-generator.ts`)\n * has no equivalent of that gate at all today — it filters on reserved-CRUD\n * name collision, `isPublic`, and `exclude`/`include`, but never\n * `isFrameworkLifecycleMethod()`, so a locally overridden lifecycle method\n * IS a reachable command there (see `knowledge.ts`'s `configuredOperations()`\n * docblock for the same caveat).\n * - `findCliApiCoherenceViolations`'s EXPLICIT-`cli.include` branch\n * deliberately bypasses this function too: an `include` entry naming a\n * typo, a getter, or a private/protected method must still surface as\n * \"unreachable\" at build time (the pre-#2638 behavior), and this function\n * can only ever return names that exist in the manifest's `methods` map —\n * it would silently drop such an entry instead of flagging it. See\n * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts` for the\n * full rationale; do not \"simplify\" that branch onto this function.\n *\n * `crudActionNames` stays a parameter even though every caller now passes\n * {@link CRUD_OPERATIONS} (#2665 retired the last of the inline copies at\n * `vite-plugin/sveltekit-generator.ts`, following `CLIGenerator`'s #2646\n * switch; `vite-plugin/index.ts`'s copy backed the `generateCLIModule()`\n * emitter #2664 later retired, so `index.ts` no longer calls this function\n * or imports `CRUD_OPERATIONS` at all). Removing the parameter is a separate,\n * unevidenced call this fix does not make -- it would foreclose a caller\n * that legitimately needs a different verb set, and no such need has been\n * demonstrated either way.\n */\nexport function resolveCustomActionNames(\n methods: Iterable<[string, ResolvableMethod]>,\n config: { include?: string[]; exclude?: string[] } | undefined,\n crudActionNames: readonly string[],\n): Set<string> {\n const included = config?.include;\n const excluded = config?.exclude ?? [];\n const result = new Set<string>();\n for (const [name, method] of methods) {\n if (crudActionNames.includes(name)) continue;\n if (isFrameworkLifecycleMethod(name)) continue;\n if (!method.isPublic) continue;\n if (excluded.includes(name)) continue;\n if (included !== undefined && !included.includes(name)) continue;\n result.add(name);\n }\n return result;\n}\n\n/**\n * The CRUD verbs a generated surface emits directly. A method whose name\n * collides with one of these may not be exposed as a custom action under that\n * name: the generated operation already claims it, so a second command/tool\n * would land on a name that is taken.\n *\n * This is a NAMESPACE rule, independent of where the method came from — a\n * class's own `list()` collides exactly as a merged ancestor's does (#2646).\n *\n * What a collision means differs by EMITTER, so consult the one you are\n * changing rather than assuming a single rule. The reservation lives at each\n * emission site, not here, and several emitters still keep their own inline\n * verb array — find them with a multi-line-tolerant search (#2665 turned the\n * single-line form of this grep blind to a wrapped literal like\n * `templates/default-ui.ts`'s `CRUD_OPERATIONS_FOR_BROWSER_TEMPLATE`):\n *\n * rg -U \"'list',\\s*'get',\\s*'create',\\s*'update',\\s*'delete'\" packages --type ts | grep -v include:\n *\n * This is a STARTING POINT, not a closed inventory: the `include:` filter\n * only drops a single-line `include: [...]` block, so a wrapped one (e.g.\n * `packages/content/src/content.ts`) still surfaces, and the pattern also\n * matches non-decorator uses of the same five-word literal that have nothing\n * to do with this collision rule (e.g. `packages/smrt-workbench/src/\n * discovery.ts`'s `CRUD_ACTIONS`, `packages/smrt-dev-mcp/.../\n * introspect-project.ts`'s `DEFAULT_MCP_OPERATIONS`). Triage each hit rather\n * than trusting the raw list. Within `packages/core/src/vite-plugin/`\n * specifically, the emitter-relevant survivors as of #2665 are\n * `generated-client.ts` and `templates/default-ui.ts` (the latter\n * deliberate, value-pinned by `issue-2665-crud-verb-consolidation.spec.ts`);\n * `scanner/manifest-generator.ts` and `packages/users/src/sveltekit/\n * resource-list-handler.ts` are outside this PR's scope.\n *\n * The sites this rule was audited against (#2646), NOT an exhaustive\n * inventory:\n *\n * - `generators/mcp.ts` — unconditional, case-folded. `executeAction` switches\n * on the verb parsed out of the tool id, so `${object}_list` runs the\n * built-in list whichever branch emitted it: the class's method could never\n * run, and emitting one would hand the caller an operation `include` never\n * named.\n * - `packages/cli/src/cli-generator.ts`, behind the shipped `smrt` object\n * commands — reserves only where the CRUD command is actually emitted, and\n * reserves the command NAMES AND THEIR ALIASES (`ls`, `show`, `new`, `edit`,\n * `rm`), because lookup matches aliases too (#2648). The set is derived from\n * the commands actually pushed, so it cannot drift from those aliases. Each\n * command carries its own handler invoking the class's method, so with\n * `cli: { include: ['list', 'get'] }` neither a public `create()` nor an\n * `edit()` collides with anything and both stay reachable.\n * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, via\n * {@link isCrudOperation} (#2665; previously its own `STANDARD_API_ACTIONS`\n * copy).\n * - `vite-plugin/web-collections.ts` + `tool-schema.ts` — case-folded (ids are\n * lowercased whole like MCP's), CONDITIONAL, and scoped PER COLLECTION.\n * Applied in both of `selectWebMcpToolEntries`' loops and at the shared\n * `buildWebToolDescriptorsForHost` choke point, which the legacy\n * per-collection descriptor export also passes through (#2648).\n *\n * Conditional, unlike `generators/mcp.ts`: MCP dispatch parses the verb out\n * of the tool id, so `${obj}_list` can only ever run the built-in list, but a\n * WebMCP descriptor carries its own `route` from `resolveApiActionRouteConfig`\n * — with `api: { include: ['List'] }`, `product_list` dispatches to\n * `/products/List` and is the only tool for that id, so reserving it would\n * make a custom-action-only model undiscoverable.\n *\n * Per collection, not per host: the id prefix is the OWNER's `className`, so\n * every host mapping to one collection shares the namespace. A host-local\n * check lets a model that excludes `list` but declares `List()` claim\n * `product_list`, after which a sibling's real `list` is dropped by the\n * fold-dedupe. The emitted-verb set is the union over the collection's hosts.\n *\n * `resolveApiActionSet` stays exact-match: REST routes keep declared casing,\n * so `/products/List` is genuinely distinct from `/products`.\n *\n * `vite-plugin/api-client-entries.ts` and `vite-plugin/sveltekit-generator.ts`\n * now import {@link CRUD_OPERATIONS} (or {@link isCrudOperation} where a bare\n * `.includes()` on the readonly tuple failed the stricter\n * `tsconfig.typecheck.json`) rather than keeping their own verb copies\n * (#2665). `vite-plugin/index.ts`'s copy backed `generateCLIModule()`, which\n * #2664 retired along with the rest of the unused `smrt-virt-cli` module, so\n * `index.ts` no longer imports anything from this module at all.\n * `vite-plugin/templates/default-ui.ts` keeps its\n * verb list as a local literal deliberately: `src/vite-plugin/templates/**`\n * is excluded from both tsconfigs and the vite-dts build graph, and the\n * package build copies that directory to `dist/` verbatim rather than\n * compiling it -- see the module's own comment. Its `.ts` source is never\n * resolved or bundled by anything in this package, so a static import of the\n * Node-side `isCrudOperation`/`CRUD_OPERATIONS` (which pull in\n * `tools/tool-generator.js`) would never be satisfied there. The\n * consolidation test instead asserts the literal's *value* matches\n * {@link CRUD_OPERATIONS} rather than assuming it imports it. Consolidating\n * the lists did not change any of the\n * per-emitter RULES documented above -- each site still decides case-folding\n * and conditionality for itself.\n *\n * Read the list through {@link isCrudOperation} or {@link isCrudToolAction}\n * rather than re-declaring it; the two differ only in case folding, because the\n * transports namespace differently (see below).\n */\nexport const CRUD_OPERATIONS = [\n 'list',\n 'get',\n 'create',\n 'update',\n 'delete',\n] as const;\n\n/**\n * Exact-match test for a case-SENSITIVE surface. The CLI keeps a method's\n * declared casing in its command name (`${object}:${methodName}`) and resolves\n * an action by exact match, so `foo:List` is a distinct, callable custom\n * command that must not be folded into `foo:list`.\n */\nexport function isCrudOperation(name: string): boolean {\n return (CRUD_OPERATIONS as readonly string[]).includes(name);\n}\n\n/**\n * Case-FOLDED test for a lowercase tool namespace. MCP tool identifiers are\n * lowercased whole (`` `${object}_${methodName}`.toLowerCase() ``) for a stable\n * protocol vocabulary, so a method named `List` lands on the identifier\n * `object_list` that the generated CRUD tool already owns. The namespace key is\n * the lowercased name, so the collision test has to be too (#2646).\n */\nexport function isCrudToolAction(name: string): boolean {\n return isCrudOperation(name.toLowerCase());\n}\n\nexport interface CustomActionMetadata {\n scope: CustomActionScope;\n /** An item-targeted action requires its target identifier. */\n idRequired: boolean;\n /** Present only when scanner/manifest metadata is available. */\n parameters?: MethodDefinition['parameters'];\n /** Collection actions on a model class invoke its static method. */\n isStatic: boolean;\n /** Browser/agent-visible effect. Omitted declarations fail closed. */\n effect?: ToolEffect;\n /** Whether repeating the action with the same arguments is safe. */\n idempotent?: boolean;\n /** Whether the opaque action may interact outside the SMRT application. */\n openWorld?: boolean;\n}\n\n/** Fully resolved metadata returned by {@link resolveCustomActionMetadata}. */\nexport type ResolvedCustomActionMetadata = CustomActionMetadata &\n Required<Pick<CustomActionMetadata, 'effect' | 'idempotent' | 'openWorld'>>;\n\n/**\n * Return the transport field for a method parameter. Flat tool and CLI\n * transports reserve `id` for receiver parsing even when a collection action\n * rejects it, so every action parameter named `id` is exposed as `actionId`.\n * REST already has separate path/body namespaces.\n */\nexport function customActionParameterInputName(\n _metadata: Pick<CustomActionMetadata, 'idRequired'>,\n parameterName: string,\n): string {\n return parameterName === 'id' ? 'actionId' : parameterName;\n}\n\nexport interface ResolveCustomActionMetadataOptions {\n actionName: string;\n method?: {\n isStatic?: boolean;\n parameters?: MethodDefinition['parameters'];\n };\n apiConfig?: unknown;\n /** Collection-class actions have a collection receiver even when non-static. */\n defaultScope?: CustomActionScope;\n}\n\ntype JsonSchema = Record<string, unknown>;\ntype ToolArgs = Record<string, unknown>;\n\n/**\n * Resolve the target contract shared by MCP, generated REST, CLI discovery,\n * and WebMCP. A missing manifest method retains the historical item-shaped\n * schema; runtime callers may still provide their legacy collection fallback.\n */\nexport function resolveCustomActionMetadata(\n options: ResolveCustomActionMetadataOptions,\n): ResolvedCustomActionMetadata {\n const defaultScope =\n options.defaultScope ?? (options.method?.isStatic ? 'collection' : 'item');\n const requestedScope = readConfiguredScope(\n options.apiConfig,\n options.actionName,\n );\n // A route-only scope override cannot manufacture a receiver. A normal\n // instance method is always item-targeted; a static model method and a\n // recognized collection-class method are always collection-targeted. Keep\n // a matching explicit value for diagnostics/config round-tripping only.\n const scope = requestedScope === defaultScope ? requestedScope : defaultScope;\n const configured = readConfiguredToolMetadata(\n options.apiConfig,\n options.actionName,\n );\n const effect = configured.effect ?? 'destructive';\n\n return {\n scope,\n idRequired: scope === 'item',\n ...(options.method?.parameters\n ? { parameters: options.method.parameters }\n : {}),\n isStatic: options.method?.isStatic === true,\n effect,\n // Per-field fail-closed default (#2587, CapabilityDeclaration in\n // @happyvertical/smrt-types): an omitted `idempotent` resolves to\n // `false` regardless of the declared `effect` — a declared 'read'\n // action is not guaranteed idempotent (e.g. a dequeue-shaped read that\n // advances state), so a caller who wants the idempotent hint must\n // declare it explicitly.\n idempotent: configured.idempotent ?? false,\n openWorld: configured.openWorld ?? true,\n };\n}\n\n/** Build the custom-action portion of an MCP/WebMCP JSON Schema. */\nexport function buildCustomActionInputSchema(\n metadata: CustomActionMetadata,\n): JsonSchema {\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n\n if (metadata.idRequired) {\n properties.id = {\n type: 'string',\n description: 'ID of the object to execute action on',\n };\n required.push('id');\n }\n\n // Absent metadata is the legacy options-bag contract. Do not infer direct\n // positional arguments from runtime function arity: it is lossy after\n // transpilation and would make discovery non-deterministic.\n if (!metadata.parameters) {\n properties.options = {\n type: 'object',\n description: 'Additional options for the custom action',\n additionalProperties: true,\n };\n } else if (\n metadata.parameters.length === 1 &&\n metadata.parameters[0]?.name === 'options'\n ) {\n const parameter = metadata.parameters[0];\n properties.options = {\n ...convertTypeToJsonSchema(parameter.type),\n description: 'Options for the custom action',\n ...(parameter.default !== undefined\n ? { default: parameter.default }\n : {}),\n };\n if (!parameter.optional) required.push('options');\n } else {\n for (const parameter of metadata.parameters) {\n const inputName = customActionParameterInputName(\n metadata,\n parameter.name,\n );\n properties[inputName] = {\n ...convertTypeToJsonSchema(parameter.type),\n ...(parameter.default !== undefined\n ? { default: parameter.default }\n : {}),\n };\n if (!parameter.optional) required.push(inputName);\n }\n }\n\n return {\n type: 'object',\n properties,\n ...(required.length > 0 ? { required } : {}),\n };\n}\n\n/**\n * Translate a transport object into the method's call arguments. Legacy\n * actions retain their single options-bag invocation; scanner metadata enables\n * an exact positional projection without changing legacy action behavior.\n */\nexport function buildCustomActionInvocationArgs(\n metadata: CustomActionMetadata,\n args: ToolArgs,\n): unknown[] {\n const { id: _id, options, ...directArgs } = args;\n\n if (!metadata.parameters) {\n return [\n isRecord(options) && Object.keys(options).length > 0\n ? options\n : directArgs,\n ];\n }\n if (metadata.parameters.length === 0) return [];\n if (\n metadata.parameters.length === 1 &&\n metadata.parameters[0]?.name === 'options'\n ) {\n // Preserve `undefined` (and an explicit `null`) so JavaScript default\n // parameter initializers and intentional null handling retain their native\n // semantics. Legacy options bags still receive an empty object below.\n return [options];\n }\n return metadata.parameters.map(\n (parameter) =>\n args[customActionParameterInputName(metadata, parameter.name)],\n );\n}\n\n/**\n * Domain-neutral returned-failure convention for custom actions. The explicit\n * `ok: false` marker prevents successful opaque values such as `{ code,\n * message }` from being reclassified as failures by a transport.\n */\nexport interface CustomActionFailure {\n ok: false;\n code: string;\n message: string;\n status: number;\n details?: unknown;\n retryable?: boolean;\n correlationId?: string;\n}\n\n/** Stable MCP `_meta` member shared with the app discovery contract (#2181). */\nexport const SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY = 'io.happyvertical/smrt';\n\n/**\n * Detect, validate, and redact an explicitly returned custom-action failure.\n * Unknown return values remain opaque successes. `status` defaults to 400 so\n * REST callers receive non-2xx semantics even when an adapter omits it.\n */\nexport function normalizeCustomActionFailure(\n value: unknown,\n): CustomActionFailure | undefined {\n if (!isRecord(value) || value.ok !== false) return undefined;\n if (typeof value.code !== 'string' || typeof value.message !== 'string') {\n return undefined;\n }\n\n const status =\n typeof value.status === 'number' &&\n Number.isInteger(value.status) &&\n value.status >= 400 &&\n value.status <= 599\n ? value.status\n : 400;\n\n return {\n ok: false,\n code: value.code,\n message: redactText(value.message),\n status,\n ...(Object.hasOwn(value, 'details')\n ? { details: redactValue(value.details) }\n : {}),\n ...(typeof value.retryable === 'boolean'\n ? { retryable: value.retryable }\n : {}),\n ...(typeof value.correlationId === 'string'\n ? { correlationId: value.correlationId }\n : {}),\n };\n}\n\nfunction readConfiguredScope(\n apiConfig: unknown,\n actionName: string,\n): CustomActionScope | undefined {\n if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return undefined;\n const route = apiConfig.routes[actionName];\n return isRecord(route) &&\n (route.scope === 'item' || route.scope === 'collection')\n ? route.scope\n : undefined;\n}\n\nfunction readConfiguredToolMetadata(\n apiConfig: unknown,\n actionName: string,\n): {\n effect?: ToolEffect;\n idempotent?: boolean;\n openWorld?: boolean;\n} {\n if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return {};\n const route = apiConfig.routes[actionName];\n if (!isRecord(route)) return {};\n const effect =\n route.effect === 'read' ||\n route.effect === 'write' ||\n route.effect === 'destructive'\n ? route.effect\n : undefined;\n if (\n effect === 'read' &&\n (route.method === 'PUT' ||\n route.method === 'PATCH' ||\n route.method === 'DELETE')\n ) {\n throw new Error(\n `Custom action ${actionName} cannot declare a read effect for a ${route.method} route`,\n );\n }\n return {\n ...(effect ? { effect } : {}),\n ...(typeof route.idempotent === 'boolean'\n ? { idempotent: route.idempotent }\n : {}),\n ...(typeof route.openWorld === 'boolean'\n ? { openWorld: route.openWorld }\n : {}),\n };\n}\n\nfunction redactValue(value: unknown, seen = new WeakSet<object>()): unknown {\n if (typeof value === 'string') return redactText(value);\n if (value === null || typeof value !== 'object') return value;\n if (seen.has(value)) return '[REDACTED]';\n seen.add(value);\n if (Array.isArray(value))\n return value.map((entry) => redactValue(entry, seen));\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) return '[REDACTED]';\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value)) {\n result[key] = isSensitiveKey(key)\n ? '[REDACTED]'\n : redactValue(nested, seen);\n }\n return result;\n}\n\nfunction redactText(value: string): string {\n return value\n .replace(/\\bBearer\\s+[^\\s,;]+/giu, 'Bearer [REDACTED]')\n .replace(\n /\\b(token|secret|password|api[_-]?key)=([^\\s&]+)/giu,\n '$1=[REDACTED]',\n );\n}\n\nfunction isSensitiveKey(key: string): boolean {\n return /(?:token|secret|password|authorization|cookie|credential|api[_-]?key)/iu.test(\n key,\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,IAAa,mDAAwD,IAAI,IAAI;CAE3E;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;AAQD,SAAgB,2BAA2B,YAA6B;CACtE,OAAO,iCAAiC,IAAI,UAAU;AACxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,yBACd,SACA,QACA,iBACa;CACb,MAAM,WAAW,QAAQ;CACzB,MAAM,WAAW,QAAQ,WAAW,CAAC;CACrC,MAAM,yBAAS,IAAI,IAAY;CAC/B,KAAK,MAAM,CAAC,MAAM,WAAW,SAAS;EACpC,IAAI,gBAAgB,SAAS,IAAI,GAAG;EACpC,IAAI,2BAA2B,IAAI,GAAG;EACtC,IAAI,CAAC,OAAO,UAAU;EACtB,IAAI,SAAS,SAAS,IAAI,GAAG;EAC7B,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,SAAS,IAAI,GAAG;EACxD,OAAO,IAAI,IAAI;CACjB;CACA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoGA,IAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;AACF;;;;;;;AAQA,SAAgB,gBAAgB,MAAuB;CACrD,OAAQ,gBAAsC,SAAS,IAAI;AAC7D;;;;;;;;AASA,SAAgB,iBAAiB,MAAuB;CACtD,OAAO,gBAAgB,KAAK,YAAY,CAAC;AAC3C;;;;;;;AA4BA,SAAgB,+BACd,WACA,eACQ;CACR,OAAO,kBAAkB,OAAO,aAAa;AAC/C;;;;;;AAqBA,SAAgB,4BACd,SAC8B;CAC9B,MAAM,eACJ,QAAQ,iBAAiB,QAAQ,QAAQ,WAAW,eAAe;CACrE,MAAM,iBAAiB,oBACrB,QAAQ,WACR,QAAQ,UACV;CAKA,MAAM,QAAQ,mBAAmB,eAAe,iBAAiB;CACjE,MAAM,aAAa,2BACjB,QAAQ,WACR,QAAQ,UACV;CACA,MAAM,SAAS,WAAW,UAAU;CAEpC,OAAO;EACL;EACA,YAAY,UAAU;EACtB,GAAI,QAAQ,QAAQ,aAChB,EAAE,YAAY,QAAQ,OAAO,WAAW,IACxC,CAAC;EACL,UAAU,QAAQ,QAAQ,aAAa;EACvC;EAOA,YAAY,WAAW,cAAc;EACrC,WAAW,WAAW,aAAa;CACrC;AACF;;AAGA,SAAgB,6BACd,UACY;CACZ,MAAM,aAAyC,CAAC;CAChD,MAAM,WAAqB,CAAC;CAE5B,IAAI,SAAS,YAAY;EACvB,WAAW,KAAK;GACd,MAAM;GACN,aAAa;EACf;EACA,SAAS,KAAK,IAAI;CACpB;CAKA,IAAI,CAAC,SAAS,YACZ,WAAW,UAAU;EACnB,MAAM;EACN,aAAa;EACb,sBAAsB;CACxB;MACK,IACL,SAAS,WAAW,WAAW,KAC/B,SAAS,WAAW,EAAE,EAAE,SAAS,WACjC;EACA,MAAM,YAAY,SAAS,WAAW;EACtC,WAAW,UAAU;GACnB,GAAG,wBAAwB,UAAU,IAAI;GACzC,aAAa;GACb,GAAI,UAAU,YAAY,KAAA,IACtB,EAAE,SAAS,UAAU,QAAQ,IAC7B,CAAC;EACP;EACA,IAAI,CAAC,UAAU,UAAU,SAAS,KAAK,SAAS;CAClD,OACE,KAAK,MAAM,aAAa,SAAS,YAAY;EAC3C,MAAM,YAAY,+BAChB,UACA,UAAU,IACZ;EACA,WAAW,aAAa;GACtB,GAAG,wBAAwB,UAAU,IAAI;GACzC,GAAI,UAAU,YAAY,KAAA,IACtB,EAAE,SAAS,UAAU,QAAQ,IAC7B,CAAC;EACP;EACA,IAAI,CAAC,UAAU,UAAU,SAAS,KAAK,SAAS;CAClD;CAGF,OAAO;EACL,MAAM;EACN;EACA,GAAI,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;CAC5C;AACF;;;;;;AAOA,SAAgB,gCACd,UACA,MACW;CACX,MAAM,EAAE,IAAI,KAAK,SAAS,GAAG,eAAe;CAE5C,IAAI,CAAC,SAAS,YACZ,OAAO,CACL,SAAS,OAAO,KAAK,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,IAC/C,UACA,UACN;CAEF,IAAI,SAAS,WAAW,WAAW,GAAG,OAAO,CAAC;CAC9C,IACE,SAAS,WAAW,WAAW,KAC/B,SAAS,WAAW,EAAE,EAAE,SAAS,WAKjC,OAAO,CAAC,OAAO;CAEjB,OAAO,SAAS,WAAW,KACxB,cACC,KAAK,+BAA+B,UAAU,UAAU,IAAI,EAChE;AACF;;AAkBA,IAAa,wCAAwC;;;;;;AAOrD,SAAgB,6BACd,OACiC;CACjC,IAAI,CAAC,SAAS,KAAK,KAAK,MAAM,OAAO,OAAO,OAAO,KAAA;CACnD,IAAI,OAAO,MAAM,SAAS,YAAY,OAAO,MAAM,YAAY,UAC7D;CAGF,MAAM,SACJ,OAAO,MAAM,WAAW,YACxB,OAAO,UAAU,MAAM,MAAM,KAC7B,MAAM,UAAU,OAChB,MAAM,UAAU,MACZ,MAAM,SACN;CAEN,OAAO;EACL,IAAI;EACJ,MAAM,MAAM;EACZ,SAAS,WAAW,MAAM,OAAO;EACjC;EACA,GAAI,OAAO,OAAO,OAAO,SAAS,IAC9B,EAAE,SAAS,YAAY,MAAM,OAAO,EAAE,IACtC,CAAC;EACL,GAAI,OAAO,MAAM,cAAc,YAC3B,EAAE,WAAW,MAAM,UAAU,IAC7B,CAAC;EACL,GAAI,OAAO,MAAM,kBAAkB,WAC/B,EAAE,eAAe,MAAM,cAAc,IACrC,CAAC;CACP;AACF;AAEA,SAAS,oBACP,WACA,YAC+B;CAC/B,IAAI,CAAC,SAAS,SAAS,KAAK,CAAC,SAAS,UAAU,MAAM,GAAG,OAAO,KAAA;CAChE,MAAM,QAAQ,UAAU,OAAO;CAC/B,OAAO,SAAS,KAAK,MAClB,MAAM,UAAU,UAAU,MAAM,UAAU,gBACzC,MAAM,QACN,KAAA;AACN;AAEA,SAAS,2BACP,WACA,YAKA;CACA,IAAI,CAAC,SAAS,SAAS,KAAK,CAAC,SAAS,UAAU,MAAM,GAAG,OAAO,CAAC;CACjE,MAAM,QAAQ,UAAU,OAAO;CAC/B,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO,CAAC;CAC9B,MAAM,SACJ,MAAM,WAAW,UACjB,MAAM,WAAW,WACjB,MAAM,WAAW,gBACb,MAAM,SACN,KAAA;CACN,IACE,WAAW,WACV,MAAM,WAAW,SAChB,MAAM,WAAW,WACjB,MAAM,WAAW,WAEnB,MAAM,IAAI,MACR,iBAAiB,WAAW,sCAAsC,MAAM,OAAO,OACjF;CAEF,OAAO;EACL,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,OAAO,MAAM,eAAe,YAC5B,EAAE,YAAY,MAAM,WAAW,IAC/B,CAAC;EACL,GAAI,OAAO,MAAM,cAAc,YAC3B,EAAE,WAAW,MAAM,UAAU,IAC7B,CAAC;CACP;AACF;AAEA,SAAS,YAAY,OAAgB,uBAAO,IAAI,QAAgB,GAAY;CAC1E,IAAI,OAAO,UAAU,UAAU,OAAO,WAAW,KAAK;CACtD,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO;CAC5B,KAAK,IAAI,KAAK;CACd,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,UAAU,YAAY,OAAO,IAAI,CAAC;CACtD,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,IAAI,cAAc,OAAO,aAAa,cAAc,MAAM,OAAO;CACjE,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,KAAK,GAC9C,OAAO,OAAO,eAAe,GAAG,IAC5B,eACA,YAAY,QAAQ,IAAI;CAE9B,OAAO;AACT;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,MACJ,QAAQ,0BAA0B,mBAAmB,CAAC,CACtD,QACC,sDACA,eACF;AACJ;AAEA,SAAS,eAAe,KAAsB;CAC5C,OAAO,0EAA0E,KAC/E,GACF;AACF;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E"}
1
+ {"version":3,"file":"custom-action.js","names":[],"sources":["../../src/generators/custom-action.ts"],"sourcesContent":["/**\n * Canonical custom-action metadata and transport-safe result helpers.\n *\n * Custom actions are intentionally distinct from generated CRUD. Their target\n * is derived from method metadata: instance methods target an item and static\n * methods target the collection. API route configuration may shape an HTTP\n * route, but it cannot change a method's receiver.\n */\n\nimport type { ApiHttpMethod, ToolEffect } from '../registry/types.js';\nimport type {\n MethodDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport { convertTypeToJsonSchema } from '../tools/tool-generator.js';\n\nexport type CustomActionScope = 'item' | 'collection';\nexport type { ToolEffect } from '../registry/types.js';\n\n/**\n * Public method names declared directly on `SmrtObject`/`SmrtClass`\n * (`src/object.ts`, `src/class.ts`) that form the constructor → `initialize()`\n * → `save()`/`delete()`/`loadFromId()` lifecycle and its immediate supporting\n * mechanism: identity/persistence bookkeeping, transaction binding, and\n * serialization. `save()` is what generated `create`/`update` call;\n * `initialize()` is what `get`/`list` hydration calls; `loadFromId()`/\n * `loadFromSlug()` are what `get()` calls; `toJSON()`/`toPublicJSON()` are\n * what every read serializes through. None of these are a subclass-specific\n * operation, so none is exposed as a generated CLI/MCP custom action --\n * even when a subclass declares its own override (e.g. `User.save()` at\n * `packages/users/src/models/User.ts`). An override is still the same\n * lifecycle operation, not a new one (#2638). `delete` itself is a CRUD verb\n * `packages/cli/src/cli-generator.ts`'s `CLIGenerator`/`MCPGenerator` already\n * special-case, so it is not repeated here.\n *\n * Scope is deliberately narrower than \"every public method on\n * SmrtObject/SmrtClass/SmrtCollection\":\n *\n * - AI operations `is()`/`do()`/`describe()` are declared on `SmrtObject`\n * but are explicitly designed to be overridden with domain-specific\n * behavior and exposed as a distinct action -- confirmed by existing,\n * intentional coverage\n * (`vite-plugin/generated-client-integration.test.ts`'s `ArtCollection.\n * describe(tone)` with its own declared API route). Excluding them here\n * would regress real, working behavior. (The sibling\n * `generators/cli-commands.spec.ts` fixture that used to cover the same\n * `describe()` custom action was retired with core's `CLIGenerator`,\n * #2664; this remaining fixture still exercises the behavior.)\n * - Relationship loading (`loadRelated`/`loadRelatedMany`/`getRelated`/\n * `isRelatedLoaded`), memory (`remember`/`recall`/`recallAll`/`forget`/\n * `forgetScope`), embeddings (`generateEmbeddings`/`getEmbedding`/\n * `hasStaleEmbeddings`/`clearEmbeddings`), and AI-usage introspection\n * (`getAiUsageSnapshot`/`resetAiUsage`/`listAiUsage`/`summarizeAiUsage`)\n * are generic capabilities a class may legitimately want to trigger or\n * report on as a distinct action (e.g. \"regenerate this record's\n * embeddings\"), not \"the mechanism behind CRUD\" the way `save`/\n * `initialize` are. Nothing in the measured #2638 residual touches them,\n * so blanket-excluding them is a separate, unevidenced call this fix does\n * not make.\n * - `SmrtCollection`'s own surface (`count`, `facets`, `query`, `findOne`,\n * `generateMissingEmbeddings`, ...) is excluded entirely for the same\n * reason: the measured residual is 100% item-class overrides (`User`,\n * `Invoice`, `Payment`, ...), never a collection-class override, and\n * `count`/`facets` read as genuinely distinct query capabilities in\n * existing fixtures elsewhere in the repo (e.g.\n * `packages/content/src/server/content-list-actions.test.ts`). Extending\n * this rule to `SmrtCollection` needs its own review, not a ride-along\n * here.\n *\n * This can only be a METHOD-NAME list, not a reuse of the existing\n * class-name sets (`FRAMEWORK_METHOD_BASE_NAMES` in\n * `scanner/manifest-generator.ts`, the registry's own by-name skip in\n * `registry/inheritance-resolver.ts`, or the broader 8-class\n * `FRAMEWORK_BASE_CLASSES` those two modules also cross-reference for the\n * unrelated schema-field-merge question): those sets answer \"is this\n * ancestor one of the excluded classes\", which only ever decides whether to\n * MERGE an ancestor's methods onto a subclass that does not declare them.\n * A locally declared override -- the #2638 case -- IS the subclass's own\n * method; there is no ancestor lookup to skip, because the method that\n * needs excluding was never inherited in the first place. The same\n * plumbing-vs-operation judgment #2624 made at the class level has to be\n * re-expressed as a method-name list to reach the override case too, which\n * is why this is a fifth name list rather than a reuse of one of the four.\n *\n * A static, hand-maintained list (not runtime introspection of the actual\n * `SmrtObject`/`SmrtClass` prototypes) matches the existing pattern for all\n * four sibling lists above, and keeps this transport-neutral module usable\n * from a pure manifest/AST build path (`vite-plugin/sveltekit-generator.ts`)\n * that has no live class registry to introspect. Re-derive this list from\n * those two files' own public method signatures if they change.\n */\nexport const FRAMEWORK_LIFECYCLE_METHOD_NAMES: ReadonlySet<string> = new Set([\n // SmrtClass (src/class.ts) — resource/transaction plumbing.\n 'destroy',\n 'withDatabase',\n // SmrtObject (src/object.ts) — identity/persistence lifecycle mechanism.\n 'initialize',\n 'loadDataFromDb',\n 'getFields',\n 'toJSON',\n 'toPlainObject',\n 'toPublicJSON',\n 'getId',\n 'getSlug',\n 'getSavedId',\n 'isSaved',\n 'save',\n 'claimRevision',\n 'classifyConstraintError',\n 'loadFromId',\n 'loadFromSlug',\n 'markAsPersisted',\n 'requireInsertOnSave',\n 'withTransaction',\n]);\n\n/**\n * True when `methodName` is one of the universal `SmrtObject`/`SmrtClass`\n * lifecycle methods above — the mechanism behind generated CRUD, never a\n * subclass-specific custom action, even when the subclass declares its own\n * override.\n */\nexport function isFrameworkLifecycleMethod(methodName: string): boolean {\n return FRAMEWORK_LIFECYCLE_METHOD_NAMES.has(methodName);\n}\n\n/** Minimal shape `resolveCustomActionNames` needs from a method entry. */\nexport interface ResolvableMethod {\n isPublic: boolean;\n}\n\n/**\n * Resolve the effective set of custom (non-CRUD) command/tool names exposed\n * for an object, given its transport config's `include`/`exclude` and its\n * method map: every public method, minus CRUD verbs, minus framework\n * lifecycle methods, restricted to `include` when present and always minus\n * `exclude`. Its sole caller as of #2664 (`CLIGenerator` and the\n * `generateCLIModule()` virtual module were retired):\n *\n * - `findCliApiCoherenceViolations`'s bare-`cli: true`/`cli: {}` branch\n * (over the static manifest, no explicit `include`) -- see\n * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts`.\n *\n * NOT the one universal resolution, and deliberately not reused by every\n * caller that resolves a CLI command set:\n *\n * - Core's now-retired `CLIGenerator.assertCommandExposed()` (#2664) did not\n * call this for its custom-method branch — it checked\n * `isFrameworkLifecycleMethod()` directly plus its own inline\n * public/include/exclude logic, so it could give a distinct error message\n * per failure reason (unknown vs. not public vs. not enabled vs. lifecycle\n * method) rather than a single boolean membership test. The shipped local\n * CLI's `generateObjectCommands()` (`packages/cli/src/cli-generator.ts`)\n * has no equivalent of that gate at all today — it filters on reserved-CRUD\n * name collision, `isPublic`, and `exclude`/`include`, but never\n * `isFrameworkLifecycleMethod()`, so a locally overridden lifecycle method\n * IS a reachable command there (see `knowledge.ts`'s `configuredOperations()`\n * docblock for the same caveat).\n * - `findCliApiCoherenceViolations`'s EXPLICIT-`cli.include` branch\n * deliberately bypasses this function too: an `include` entry naming a\n * typo, a getter, or a private/protected method must still surface as\n * \"unreachable\" at build time (the pre-#2638 behavior), and this function\n * can only ever return names that exist in the manifest's `methods` map —\n * it would silently drop such an entry instead of flagging it. See\n * `resolveCliActionSet` in `vite-plugin/sveltekit-generator.ts` for the\n * full rationale; do not \"simplify\" that branch onto this function.\n *\n * `crudActionNames` stays a parameter even though every caller now passes\n * {@link CRUD_OPERATIONS} (#2665 retired the last of the inline copies at\n * `vite-plugin/sveltekit-generator.ts`, following `CLIGenerator`'s #2646\n * switch; `vite-plugin/index.ts`'s copy backed the `generateCLIModule()`\n * emitter #2664 later retired, so `index.ts` no longer calls this function\n * or imports `CRUD_OPERATIONS` at all). Removing the parameter is a separate,\n * unevidenced call this fix does not make -- it would foreclose a caller\n * that legitimately needs a different verb set, and no such need has been\n * demonstrated either way.\n */\nexport function resolveCustomActionNames(\n methods: Iterable<[string, ResolvableMethod]>,\n config: { include?: string[]; exclude?: string[] } | undefined,\n crudActionNames: readonly string[],\n): Set<string> {\n const included = config?.include;\n const excluded = config?.exclude ?? [];\n const result = new Set<string>();\n for (const [name, method] of methods) {\n if (crudActionNames.includes(name)) continue;\n if (isFrameworkLifecycleMethod(name)) continue;\n if (!method.isPublic) continue;\n if (excluded.includes(name)) continue;\n if (included !== undefined && !included.includes(name)) continue;\n result.add(name);\n }\n return result;\n}\n\n/**\n * The CRUD verbs a generated surface emits directly. A method whose name\n * collides with one of these may not be exposed as a custom action under that\n * name: the generated operation already claims it, so a second command/tool\n * would land on a name that is taken.\n *\n * This is a NAMESPACE rule, independent of where the method came from — a\n * class's own `list()` collides exactly as a merged ancestor's does (#2646).\n *\n * What a collision means differs by EMITTER, so consult the one you are\n * changing rather than assuming a single rule. The reservation lives at each\n * emission site, not here, and several emitters still keep their own inline\n * verb array — find them with a multi-line-tolerant search (#2665 turned the\n * single-line form of this grep blind to a wrapped literal like\n * `templates/default-ui.ts`'s `CRUD_OPERATIONS_FOR_BROWSER_TEMPLATE`):\n *\n * rg -U \"'list',\\s*'get',\\s*'create',\\s*'update',\\s*'delete'\" packages --type ts | grep -v include:\n *\n * This is a STARTING POINT, not a closed inventory: the `include:` filter\n * only drops a single-line `include: [...]` block, so a wrapped one (e.g.\n * `packages/content/src/content.ts`) still surfaces, and the pattern also\n * matches non-decorator uses of the same five-word literal that have nothing\n * to do with this collision rule (e.g. `packages/smrt-workbench/src/\n * discovery.ts`'s `CRUD_ACTIONS`, `packages/smrt-dev-mcp/.../\n * introspect-project.ts`'s `DEFAULT_MCP_OPERATIONS`). Triage each hit rather\n * than trusting the raw list. Within `packages/core/src/vite-plugin/`\n * specifically, the emitter-relevant survivors as of #2665 are\n * `generated-client.ts` and `templates/default-ui.ts` (the latter\n * deliberate, value-pinned by `issue-2665-crud-verb-consolidation.spec.ts`);\n * `scanner/manifest-generator.ts` and `packages/users/src/sveltekit/\n * resource-list-handler.ts` are outside this PR's scope.\n *\n * The sites this rule was audited against (#2646), NOT an exhaustive\n * inventory:\n *\n * - `generators/mcp.ts` — unconditional, case-folded. `executeAction` switches\n * on the verb parsed out of the tool id, so `${object}_list` runs the\n * built-in list whichever branch emitted it: the class's method could never\n * run, and emitting one would hand the caller an operation `include` never\n * named.\n * - `packages/cli/src/cli-generator.ts`, behind the shipped `smrt` object\n * commands — reserves only where the CRUD command is actually emitted, and\n * reserves the command NAMES AND THEIR ALIASES (`ls`, `show`, `new`, `edit`,\n * `rm`), because lookup matches aliases too (#2648). The set is derived from\n * the commands actually pushed, so it cannot drift from those aliases. Each\n * command carries its own handler invoking the class's method, so with\n * `cli: { include: ['list', 'get'] }` neither a public `create()` nor an\n * `edit()` collides with anything and both stay reachable.\n * - `vite-plugin/sveltekit-generator.ts` — unconditional, exact, via\n * {@link isCrudOperation} (#2665; previously its own `STANDARD_API_ACTIONS`\n * copy).\n * - `vite-plugin/web-collections.ts` + `tool-schema.ts` — case-folded (ids are\n * lowercased whole like MCP's), CONDITIONAL, and scoped PER COLLECTION.\n * Applied in both of `selectWebMcpToolEntries`' loops and at the shared\n * `buildWebToolDescriptorsForHost` choke point, which the legacy\n * per-collection descriptor export also passes through (#2648).\n *\n * Conditional, unlike `generators/mcp.ts`: MCP dispatch parses the verb out\n * of the tool id, so `${obj}_list` can only ever run the built-in list, but a\n * WebMCP descriptor carries its own `route` from `resolveApiActionRouteConfig`\n * — with `api: { include: ['List'] }`, `product_list` dispatches to\n * `/products/List` and is the only tool for that id, so reserving it would\n * make a custom-action-only model undiscoverable.\n *\n * Per collection, not per host: the id prefix is the OWNER's `className`, so\n * every host mapping to one collection shares the namespace. A host-local\n * check lets a model that excludes `list` but declares `List()` claim\n * `product_list`, after which a sibling's real `list` is dropped by the\n * fold-dedupe. The emitted-verb set is the union over the collection's hosts.\n *\n * `resolveApiActionSet` stays exact-match: REST routes keep declared casing,\n * so `/products/List` is genuinely distinct from `/products`.\n *\n * `vite-plugin/api-client-entries.ts` and `vite-plugin/sveltekit-generator.ts`\n * now import {@link CRUD_OPERATIONS} (or {@link isCrudOperation} where a bare\n * `.includes()` on the readonly tuple failed the stricter\n * `tsconfig.typecheck.json`) rather than keeping their own verb copies\n * (#2665). `vite-plugin/index.ts`'s copy backed `generateCLIModule()`, which\n * #2664 retired along with the rest of the unused `smrt-virt-cli` module, so\n * `index.ts` no longer imports anything from this module at all.\n * `vite-plugin/templates/default-ui.ts` keeps its\n * verb list as a local literal deliberately: `src/vite-plugin/templates/**`\n * is excluded from both tsconfigs and the vite-dts build graph, and the\n * package build copies that directory to `dist/` verbatim rather than\n * compiling it -- see the module's own comment. Its `.ts` source is never\n * resolved or bundled by anything in this package, so a static import of the\n * Node-side `isCrudOperation`/`CRUD_OPERATIONS` (which pull in\n * `tools/tool-generator.js`) would never be satisfied there. The\n * consolidation test instead asserts the literal's *value* matches\n * {@link CRUD_OPERATIONS} rather than assuming it imports it. Consolidating\n * the lists did not change any of the\n * per-emitter RULES documented above -- each site still decides case-folding\n * and conditionality for itself.\n *\n * Read the list through {@link isCrudOperation} or {@link isCrudToolAction}\n * rather than re-declaring it; the two differ only in case folding, because the\n * transports namespace differently (see below).\n */\nexport const CRUD_OPERATIONS = [\n 'list',\n 'get',\n 'create',\n 'update',\n 'delete',\n] as const;\n\n/**\n * Exact-match test for a case-SENSITIVE surface. The CLI keeps a method's\n * declared casing in its command name (`${object}:${methodName}`) and resolves\n * an action by exact match, so `foo:List` is a distinct, callable custom\n * command that must not be folded into `foo:list`.\n */\nexport function isCrudOperation(name: string): boolean {\n return (CRUD_OPERATIONS as readonly string[]).includes(name);\n}\n\n/**\n * Case-FOLDED test for a lowercase tool namespace. MCP tool identifiers are\n * lowercased whole (`` `${object}_${methodName}`.toLowerCase() ``) for a stable\n * protocol vocabulary, so a method named `List` lands on the identifier\n * `object_list` that the generated CRUD tool already owns. The namespace key is\n * the lowercased name, so the collision test has to be too (#2646).\n */\nexport function isCrudToolAction(name: string): boolean {\n return isCrudOperation(name.toLowerCase());\n}\n\nexport interface CustomActionMetadata {\n scope: CustomActionScope;\n /** An item-targeted action requires its target identifier. */\n idRequired: boolean;\n /** Present only when scanner/manifest metadata is available. */\n parameters?: MethodDefinition['parameters'];\n /** Collection actions on a model class invoke its static method. */\n isStatic: boolean;\n /** Browser/agent-visible effect. Omitted declarations fail closed. */\n effect?: ToolEffect;\n /** Whether repeating the action with the same arguments is safe. */\n idempotent?: boolean;\n /** Whether the opaque action may interact outside the SMRT application. */\n openWorld?: boolean;\n}\n\n/** Fully resolved metadata returned by {@link resolveCustomActionMetadata}. */\nexport type ResolvedCustomActionMetadata = CustomActionMetadata &\n Required<Pick<CustomActionMetadata, 'effect' | 'idempotent' | 'openWorld'>>;\n\n/**\n * Return the transport field for a method parameter. Flat tool and CLI\n * transports reserve `id` for receiver parsing even when a collection action\n * rejects it, so every action parameter named `id` is exposed as `actionId`.\n * REST already has separate path/body namespaces.\n */\nexport function customActionParameterInputName(\n _metadata: Pick<CustomActionMetadata, 'idRequired'>,\n parameterName: string,\n): string {\n return parameterName === 'id' ? 'actionId' : parameterName;\n}\n\nexport interface ResolveCustomActionMetadataOptions {\n actionName: string;\n method?: {\n isStatic?: boolean;\n parameters?: MethodDefinition['parameters'];\n /**\n * The method's `@method()` config, whose options win field by field over\n * the class-level `api.routes` entry for the same action (#2686).\n */\n decoratorConfig?: Record<string, unknown>;\n };\n apiConfig?: unknown;\n /** Collection-class actions have a collection receiver even when non-static. */\n defaultScope?: CustomActionScope;\n}\n\ntype JsonSchema = Record<string, unknown>;\ntype ToolArgs = Record<string, unknown>;\n\n/**\n * Resolve the target contract shared by MCP, generated REST, CLI discovery,\n * and WebMCP. A missing manifest method retains the historical item-shaped\n * schema; runtime callers may still provide their legacy collection fallback.\n */\nexport function resolveCustomActionMetadata(\n options: ResolveCustomActionMetadataOptions,\n): ResolvedCustomActionMetadata {\n const defaultScope =\n options.defaultScope ?? (options.method?.isStatic ? 'collection' : 'item');\n const requestedScope = readConfiguredScope(options);\n // A DECLARED scope -- from `api.routes[action].scope` or `@method({ scope })`\n // -- cannot manufacture a receiver. A normal instance method is always\n // item-targeted; a static model method and a recognized collection-class\n // method are always collection-targeted. A matching explicit value is kept\n // for diagnostics/config round-tripping only; a contradicting one is\n // reported by `resolveDeclaredScopeMismatch` at build time rather than\n // relocating the method (#2686).\n const scope = requestedScope === defaultScope ? requestedScope : defaultScope;\n const configured = readConfiguredToolMetadata(options);\n const effect = configured.effect ?? 'destructive';\n\n return {\n scope,\n idRequired: scope === 'item',\n ...(options.method?.parameters\n ? { parameters: options.method.parameters }\n : {}),\n isStatic: options.method?.isStatic === true,\n effect,\n // Per-field fail-closed default (#2587, CapabilityDeclaration in\n // @happyvertical/smrt-types): an omitted `idempotent` resolves to\n // `false` regardless of the declared `effect` — a declared 'read'\n // action is not guaranteed idempotent (e.g. a dequeue-shaped read that\n // advances state), so a caller who wants the idempotent hint must\n // declare it explicitly.\n idempotent: configured.idempotent ?? false,\n openWorld: configured.openWorld ?? true,\n };\n}\n\n/**\n * The declared scope, when it contradicts the receiver the method actually\n * has; `undefined` when there is no declaration or it agrees.\n *\n * A scope is a DECLARATION about a method, not a relocation of it: nothing in\n * a config can move an instance method onto the class. Silently ignoring a\n * contradiction leaves an author believing a route exists at a collection URL\n * that was never written, so the generators report this at build time\n * (#2686).\n */\nexport function resolveDeclaredScopeMismatch(options: {\n actionName: string;\n method?: ExposableMethod;\n apiConfig?: unknown;\n /** The receiver-derived scope, as the emitter computed it. */\n effectiveScope: CustomActionScope;\n}): CustomActionScope | undefined {\n const declared = resolveEffectiveActionMetadata({\n actionName: options.actionName,\n ...(options.method ? { method: options.method } : {}),\n apiConfig: options.apiConfig,\n }).scope;\n if (!declared || declared === options.effectiveScope) return undefined;\n return declared;\n}\n\n/** Build the custom-action portion of an MCP/WebMCP JSON Schema. */\nexport function buildCustomActionInputSchema(\n metadata: CustomActionMetadata,\n): JsonSchema {\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n\n if (metadata.idRequired) {\n properties.id = {\n type: 'string',\n description: 'ID of the object to execute action on',\n };\n required.push('id');\n }\n\n // Absent metadata is the legacy options-bag contract. Do not infer direct\n // positional arguments from runtime function arity: it is lossy after\n // transpilation and would make discovery non-deterministic.\n if (!metadata.parameters) {\n properties.options = {\n type: 'object',\n description: 'Additional options for the custom action',\n additionalProperties: true,\n };\n } else if (\n metadata.parameters.length === 1 &&\n metadata.parameters[0]?.name === 'options'\n ) {\n const parameter = metadata.parameters[0];\n properties.options = {\n ...convertTypeToJsonSchema(parameter.type),\n description: 'Options for the custom action',\n ...(parameter.default !== undefined\n ? { default: parameter.default }\n : {}),\n };\n if (!parameter.optional) required.push('options');\n } else {\n for (const parameter of metadata.parameters) {\n const inputName = customActionParameterInputName(\n metadata,\n parameter.name,\n );\n properties[inputName] = {\n ...convertTypeToJsonSchema(parameter.type),\n ...(parameter.default !== undefined\n ? { default: parameter.default }\n : {}),\n };\n if (!parameter.optional) required.push(inputName);\n }\n }\n\n return {\n type: 'object',\n properties,\n ...(required.length > 0 ? { required } : {}),\n };\n}\n\n/**\n * Translate a transport object into the method's call arguments. Legacy\n * actions retain their single options-bag invocation; scanner metadata enables\n * an exact positional projection without changing legacy action behavior.\n */\nexport function buildCustomActionInvocationArgs(\n metadata: CustomActionMetadata,\n args: ToolArgs,\n): unknown[] {\n const { id: _id, options, ...directArgs } = args;\n\n if (!metadata.parameters) {\n return [\n isRecord(options) && Object.keys(options).length > 0\n ? options\n : directArgs,\n ];\n }\n if (metadata.parameters.length === 0) return [];\n if (\n metadata.parameters.length === 1 &&\n metadata.parameters[0]?.name === 'options'\n ) {\n // Preserve `undefined` (and an explicit `null`) so JavaScript default\n // parameter initializers and intentional null handling retain their native\n // semantics. Legacy options bags still receive an empty object below.\n return [options];\n }\n return metadata.parameters.map((parameter) =>\n coerceCustomActionArgument(\n args[customActionParameterInputName(metadata, parameter.name)],\n parameter.type,\n ),\n );\n}\n\n/**\n * Domain-neutral returned-failure convention for custom actions. The explicit\n * `ok: false` marker prevents successful opaque values such as `{ code,\n * message }` from being reclassified as failures by a transport.\n */\nexport interface CustomActionFailure {\n ok: false;\n code: string;\n message: string;\n status: number;\n details?: unknown;\n retryable?: boolean;\n correlationId?: string;\n}\n\n/** Stable MCP `_meta` member shared with the app discovery contract (#2181). */\nexport const SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY = 'io.happyvertical/smrt';\n\n/**\n * Detect, validate, and redact an explicitly returned custom-action failure.\n * Unknown return values remain opaque successes. `status` defaults to 400 so\n * REST callers receive non-2xx semantics even when an adapter omits it.\n */\nexport function normalizeCustomActionFailure(\n value: unknown,\n): CustomActionFailure | undefined {\n if (!isRecord(value) || value.ok !== false) return undefined;\n if (typeof value.code !== 'string' || typeof value.message !== 'string') {\n return undefined;\n }\n\n const status =\n typeof value.status === 'number' &&\n Number.isInteger(value.status) &&\n value.status >= 400 &&\n value.status <= 599\n ? value.status\n : 400;\n\n return {\n ok: false,\n code: value.code,\n message: redactText(value.message),\n status,\n ...(Object.hasOwn(value, 'details')\n ? { details: redactValue(value.details) }\n : {}),\n ...(typeof value.retryable === 'boolean'\n ? { retryable: value.retryable }\n : {}),\n ...(typeof value.correlationId === 'string'\n ? { correlationId: value.correlationId }\n : {}),\n };\n}\n\nfunction readConfiguredScope(\n options: ResolveCustomActionMetadataOptions,\n): CustomActionScope | undefined {\n return resolveEffectiveActionMetadata({\n actionName: options.actionName,\n ...(options.method ? { method: options.method } : {}),\n apiConfig: options.apiConfig,\n }).scope;\n}\n\nfunction readConfiguredToolMetadata(\n options: ResolveCustomActionMetadataOptions,\n): {\n effect?: ToolEffect;\n idempotent?: boolean;\n openWorld?: boolean;\n} {\n const effective = resolveEffectiveActionMetadata({\n actionName: options.actionName,\n ...(options.method ? { method: options.method } : {}),\n apiConfig: options.apiConfig,\n });\n // Validated against the EFFECTIVE verb, so a `@method({ httpMethod })`\n // override is checked the same way a legacy `routes[action].method` is.\n if (\n effective.effect === 'read' &&\n (effective.httpMethod === 'PUT' ||\n effective.httpMethod === 'PATCH' ||\n effective.httpMethod === 'DELETE')\n ) {\n throw new Error(\n `Custom action ${options.actionName} cannot declare a read effect for a ${effective.httpMethod} route`,\n );\n }\n return {\n ...(effective.effect ? { effect: effective.effect } : {}),\n ...(effective.idempotent !== undefined\n ? { idempotent: effective.idempotent }\n : {}),\n ...(effective.openWorld !== undefined\n ? { openWorld: effective.openWorld }\n : {}),\n };\n}\n\nfunction redactValue(value: unknown, seen = new WeakSet<object>()): unknown {\n if (typeof value === 'string') return redactText(value);\n if (value === null || typeof value !== 'object') return value;\n if (seen.has(value)) return '[REDACTED]';\n seen.add(value);\n if (Array.isArray(value))\n return value.map((entry) => redactValue(entry, seen));\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) return '[REDACTED]';\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value)) {\n result[key] = isSensitiveKey(key)\n ? '[REDACTED]'\n : redactValue(nested, seen);\n }\n return result;\n}\n\nfunction redactText(value: string): string {\n return value\n .replace(/\\bBearer\\s+[^\\s,;]+/giu, 'Bearer [REDACTED]')\n .replace(\n /\\b(token|secret|password|api[_-]?key)=([^\\s&]+)/giu,\n '$1=[REDACTED]',\n );\n}\n\nfunction isSensitiveKey(key: string): boolean {\n return /(?:token|secret|password|authorization|cookie|credential|api[_-]?key)/iu.test(\n key,\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/* ------------------------------------------------------------------------ *\n * @method() metadata and the API wire-ability gate (#2686)\n * ------------------------------------------------------------------------ */\n\n/**\n * The `@method()` decorator's options, as they reach a consumer.\n *\n * Narrowed from the manifest's untyped `MethodDefinition.decoratorConfig` by\n * {@link readMethodDecoratorConfig}. The authoring type is `MethodOptions` in\n * `decorators/index.ts`; this is the read side, and it is deliberately\n * defensive — every field is validated, and a malformed one is dropped rather\n * than trusted, the same stance `readConfiguredToolMetadata` takes for a\n * scanned `api.routes` entry.\n */\nexport interface MethodDecoratorConfig {\n /**\n * `false` withholds a method the wire-ability heuristic accepted; `true`\n * exposes one it rejected.\n *\n * `true` bypasses the HEURISTIC only. It cannot manufacture a receiver, undo\n * `api: false`, escape an `include`/`exclude` boundary, reach a non-public\n * method, or claim a CRUD verb the generated operation already owns — and it\n * does not hydrate a parameter the transport cannot build (a model instance\n * still arrives as whatever JSON the caller sent).\n */\n expose?: boolean;\n /** Why the method is withheld. Reported by the knowledge artifact. */\n reason?: string;\n /** HTTP verb for the generated route. Migrates from `api.routes[m].method`. */\n httpMethod?: ApiHttpMethod;\n /** Route path segment(s). Migrates from `api.routes[m].path`. */\n path?: string;\n /**\n * Declared receiver scope. Migrates from `api.routes[m].scope`.\n *\n * DECLARATIVE, not relocating: the executable receiver decides (an instance\n * method is item-scoped, a static or collection-class method is\n * collection-scoped), exactly as `api.routes[m].scope` already behaves. A\n * mismatch keeps the receiver and reports a diagnostic.\n */\n scope?: CustomActionScope;\n /** Browser/agent-visible effect. Migrates from `api.routes[m].effect`. */\n effect?: ToolEffect;\n /** Whether repeating the action with the same arguments is safe. */\n idempotent?: boolean;\n /** Whether the action may interact outside the SMRT application. */\n openWorld?: boolean;\n /** AI/tool description. Migrates from `ai.descriptions[m]`. */\n description?: string;\n}\n\n/**\n * The only parameter facts the wire-ability test reads.\n *\n * Deliberately looser than the manifest's `MethodParameterDefinition`: callers\n * outside the vite plugin hold their own structural view of a registered\n * method (`@happyvertical/smrt-users`' `MethodLike`, for one) and must be able\n * to ask this question without first widening their type to the manifest's.\n */\nexport interface WireableParameter {\n name: string;\n type?: string | undefined;\n /** See `MethodParameterDefinition.typeUnresolved`. */\n typeUnresolved?: boolean | undefined;\n /** See `MethodParameterDefinition.memberTypes`. */\n memberTypes?: readonly string[] | undefined;\n /**\n * See `MethodParameterDefinition.unionBranches`. Preferred over\n * `memberTypes` when present: it keeps each union branch's members attached\n * to that branch instead of flattening them together (#2686).\n */\n unionBranches?:\n | readonly {\n type: string;\n memberTypes?: readonly string[] | undefined;\n }[]\n | undefined;\n}\n\n/** Minimal method shape the exposure resolver reads. */\nexport interface ExposableMethod {\n isPublic?: boolean;\n isStatic?: boolean;\n parameters?: readonly WireableParameter[] | undefined;\n decoratorConfig?: Record<string, unknown>;\n}\n\n/**\n * Read and validate the `@method()` config the scanner put on a manifest\n * method. Returns `undefined` for an undecorated method.\n */\nexport function readMethodDecoratorConfig(\n method: ExposableMethod | undefined,\n): MethodDecoratorConfig | undefined {\n const raw = method?.decoratorConfig;\n if (!isRecord(raw)) return undefined;\n\n const scope =\n raw.scope === 'item' || raw.scope === 'collection' ? raw.scope : undefined;\n const effect =\n raw.effect === 'read' ||\n raw.effect === 'write' ||\n raw.effect === 'destructive'\n ? raw.effect\n : undefined;\n\n return {\n ...(typeof raw.expose === 'boolean' ? { expose: raw.expose } : {}),\n ...(typeof raw.reason === 'string' ? { reason: raw.reason } : {}),\n ...(isApiHttpMethod(raw.httpMethod) ? { httpMethod: raw.httpMethod } : {}),\n ...(typeof raw.path === 'string' ? { path: raw.path } : {}),\n ...(scope ? { scope } : {}),\n ...(effect ? { effect } : {}),\n ...(typeof raw.idempotent === 'boolean'\n ? { idempotent: raw.idempotent }\n : {}),\n ...(typeof raw.openWorld === 'boolean' ? { openWorld: raw.openWorld } : {}),\n ...(typeof raw.description === 'string'\n ? { description: raw.description }\n : {}),\n };\n}\n\nfunction isApiHttpMethod(value: unknown): value is ApiHttpMethod {\n return (\n value === 'GET' ||\n value === 'POST' ||\n value === 'PUT' ||\n value === 'PATCH' ||\n value === 'DELETE'\n );\n}\n\n/**\n * JavaScript values a JSON request body or query string cannot carry, keyed by\n * the type NAME the manifest records for them.\n *\n * Deliberately a name list rather than \"anything not primitive\": the manifest\n * cannot tell an interface from a class, so an unrecognized capitalized name\n * is assumed to be a plain data bag (see {@link isWireableTypeName}). These are\n * the well-known exceptions where that assumption is wrong for every project.\n * Model classes are excluded separately, by asking the manifest.\n */\nconst NON_SERIALIZABLE_TYPE_NAMES: ReadonlySet<string> = new Set([\n 'Function',\n // Lowercase PRIMITIVES that JSON still cannot carry. `isWireableTypeName`\n // accepts an unrecognized name as a data bag, which silently swept these in:\n // `JSON.stringify` throws on a bigint and drops a symbol, and neither\n // invocation path converts one, so a routed method received a string or\n // `undefined` where it declared `bigint` (#2686).\n 'bigint',\n 'symbol',\n 'Buffer',\n 'ArrayBuffer',\n 'SharedArrayBuffer',\n 'DataView',\n 'Uint8Array',\n 'Int8Array',\n 'Uint16Array',\n 'Int16Array',\n 'Uint32Array',\n 'Int32Array',\n 'Float32Array',\n 'Float64Array',\n 'BigInt64Array',\n 'BigUint64Array',\n 'Blob',\n 'File',\n 'FormData',\n 'ReadableStream',\n 'WritableStream',\n 'TransformStream',\n 'Stream',\n 'Readable',\n 'Writable',\n 'Request',\n 'Response',\n 'Headers',\n 'URL',\n 'URLSearchParams',\n 'AbortSignal',\n 'AbortController',\n 'Map',\n 'Set',\n 'WeakMap',\n 'WeakSet',\n 'RegExp',\n 'Error',\n 'Symbol',\n 'Promise',\n 'SmrtDatabase',\n 'DatabaseInterface',\n 'SmrtCollection',\n 'SmrtObject',\n]);\n\n/**\n * Generic container names whose ARGUMENTS carry the payload. `Record` is\n * included: its value type is checked, its key type is always a string-ish\n * index and never a receiver.\n */\nconst JSON_CONTAINER_TYPE_NAMES: ReadonlySet<string> = new Set([\n 'Array',\n 'ReadonlyArray',\n 'Record',\n 'Partial',\n 'Required',\n 'Readonly',\n 'Pick',\n 'Omit',\n 'NonNullable',\n]);\n\n/** Primitive/JSON-native type names a wire request can always carry. */\nconst JSON_PRIMITIVE_TYPE_NAMES: ReadonlySet<string> = new Set([\n 'string',\n 'number',\n 'boolean',\n 'null',\n 'undefined',\n 'void',\n 'any',\n 'unknown',\n 'object',\n // A Date-typed parameter is hydrated from its ISO string by the generated\n // handler and by the runtime REST dispatcher -- see\n // `coerceCustomActionArgument`. Without that hydration a Date parameter is\n // NOT wire-able, so the two must stay together.\n 'Date',\n]);\n\n/** Options that let the wire-ability test consult the surrounding manifest. */\nexport interface WireabilityOptions {\n /**\n * True when `name` identifies a class the manifest knows about — a model,\n * collection, or junction. Such a parameter wants a live instance with\n * methods and a database binding; JSON cannot produce one.\n *\n * OPTIONAL, and its absence is a documented widening: without a class\n * inventory the test cannot distinguish `Asset` from `AssetOptions`, so it\n * accepts both — and because every OTHER rejection still applies, the caller\n * then disagrees with the emitters on exactly the largest group of withheld\n * methods. Build it with {@link createManifestClassNamePredicate} from a\n * manifest, or {@link createClassNamePredicate} from a live registry's class\n * names. The parameter stays optional only because `resolveApiActionSet`'s\n * arguments are public API and optional there.\n */\n isModelClassName?: (name: string) => boolean;\n}\n\n/** Result of testing one method (or one parameter) for wire-ability. */\nexport interface WireabilityVerdict {\n wireable: boolean;\n /** Present only when `wireable` is false. */\n reason?: string;\n}\n\nconst WIREABLE: WireabilityVerdict = { wireable: true };\n\n/**\n * True when a manifest type NAME can be carried by a JSON request body.\n *\n * The default is ACCEPT: the manifest records types as strings and cannot tell\n * `RunContentReviewOptions` (an interface — a plain bag) from `Content` (a\n * model class). Rejecting every unrecognized capitalized name would withhold\n * routes from the overwhelmingly common options-bag shape, so an unrecognized\n * name is assumed to be a bag and rejection is driven by positive evidence:\n * a known non-serializable runtime type, a manifest class, or a bare type\n * parameter.\n */\nfunction classifyTypeName(\n typeName: string,\n options: WireabilityOptions,\n depth: number,\n): WireabilityVerdict {\n const type = typeName.trim();\n if (type === '') return WIREABLE;\n\n // `Date` is wire-able only where something hydrates it, and both invocation\n // paths hydrate the TOP-LEVEL parameter alone (`declaredTypeAcceptsDate` over\n // `parameter.type`). A nested `options: { start: Date }` passed the gate on\n // its `memberTypes` and then reached the method as the raw ISO string, so the\n // first `start.getTime()` threw. Accepting `Date` and hydrating it is one\n // decision; at a depth nothing hydrates, the answer has to be no (#2686).\n if (type === 'Date' && depth > 0) {\n return {\n wireable: false,\n reason:\n '`Date` is only hydrated as a top-level parameter, so a nested one arrives as a string',\n };\n }\n\n // Union: one JSON-shaped member is enough, because the caller can always\n // choose that branch. `addReference(content: Content | string)` already\n // accepts an id string and is genuinely reachable over HTTP (#2686).\n //\n // Split on TOP-LEVEL `|` only. A naive `split('|')` tore\n // `Array<Asset | string>` into `Array<Asset` and `string`, and the truncated\n // first fragment matched no rule and fell through to the default-accept path\n // — certifying a container of model instances as wire-able while the\n // union-free `Array<Asset>` was correctly rejected.\n const unionParts = splitTopLevel(type, '|');\n if (unionParts.length > 1) {\n const branches = unionParts.filter(\n (branch) => branch !== 'null' && branch !== 'undefined',\n );\n if (branches.length === 0) return WIREABLE;\n const verdicts = branches.map((branch) =>\n // Same depth, not depth + 1: a union branch occupies the SAME syntactic\n // position as the union, so `Date | null` is still the top-level\n // parameter that `declaredTypeAcceptsDate` hydrates. Only an array\n // element or type argument is genuinely nested (#2686).\n classifyTypeName(branch, options, depth),\n );\n if (verdicts.some((verdict) => verdict.wireable)) return WIREABLE;\n return {\n wireable: false,\n reason: `every branch of \\`${type}\\` is unreachable over HTTP (${verdicts[0]?.reason ?? 'not JSON-shaped'})`,\n };\n }\n\n if (type.endsWith('[]')) {\n return classifyTypeName(type.slice(0, -2), options, depth + 1);\n }\n\n // String/number/boolean literal types.\n if (/^'.*'$/su.test(type) || /^-?\\d/u.test(type)) return WIREABLE;\n\n const generic = /^([\\w$.]+)\\s*<(.*)>$/su.exec(type);\n if (generic) {\n const base = generic[1];\n if (JSON_CONTAINER_TYPE_NAMES.has(base)) {\n // Depth-bounded: the manifest stores the type as flat text, and a deeply\n // nested generic contributes nothing the gate can act on.\n if (depth >= WIREABILITY_MAX_DEPTH) return WIREABLE;\n const args = splitTopLevel(generic[2], ',');\n for (const arg of args) {\n const verdict = classifyTypeName(arg, options, depth + 1);\n if (!verdict.wireable) return verdict;\n }\n return WIREABLE;\n }\n return classifyTypeName(base, options, depth + 1);\n }\n\n // `this` in a parameter position is an instance of the declaring model\n // class, so it is rejected for the same reason a named model class is —\n // with its own wording, since \"`this` is a runtime value\" reads as nonsense.\n if (type === 'this') {\n return {\n wireable: false,\n reason:\n '`this` is an instance of the declaring model class, not JSON data',\n };\n }\n if (NON_SERIALIZABLE_TYPE_NAMES.has(type)) {\n return {\n wireable: false,\n reason: `\\`${type}\\` is a runtime value a JSON request cannot carry`,\n };\n }\n if (JSON_PRIMITIVE_TYPE_NAMES.has(type)) return WIREABLE;\n\n // A bare type parameter (`T`, `T1`, `TResult`) has no shape at all to\n // validate or build, so it can never be certified wire-able.\n if (/^T(?:[0-9]|[A-Z][A-Za-z0-9]*)?$/u.test(type) || /^[A-Z]$/u.test(type)) {\n return {\n wireable: false,\n reason: `\\`${type}\\` is an unresolved type parameter`,\n };\n }\n\n // Qualified names (`ns.Thing`) are judged on their final segment, which is\n // what the manifest registers a class under.\n const simpleName = type.includes('.')\n ? (type.split('.').pop() as string)\n : type;\n if (options.isModelClassName?.(simpleName)) {\n return {\n wireable: false,\n reason: `\\`${simpleName}\\` is a model class instance, not JSON data`,\n };\n }\n\n return WIREABLE;\n}\n\n/** Recursion bound for generic type arguments. */\nconst WIREABILITY_MAX_DEPTH = 6;\n\n/**\n * Class names a manifest knows about, cached per manifest object.\n *\n * The manifest is rebuilt, never mutated in place, so identity is a safe cache\n * key; a `WeakMap` keeps a discarded manifest's set collectable.\n */\nconst manifestClassNameCache = new WeakMap<\n SmartObjectManifest,\n ReadonlySet<string>\n>();\n\n/**\n * Build the `isModelClassName` predicate {@link classifyMethodWireability}\n * needs, from a manifest.\n *\n * Both the SIMPLE and QUALIFIED name of every manifest class are registered: a\n * parameter is annotated with the simple name in source, but a qualified name\n * can reach the predicate through a `TSQualifiedName` annotation.\n *\n * Returns `undefined` for a missing manifest, which widens the gate — see\n * {@link WireabilityOptions.isModelClassName}.\n */\nexport function createManifestClassNamePredicate(\n manifest: SmartObjectManifest | undefined,\n): ((name: string) => boolean) | undefined {\n if (!manifest) return undefined;\n let names = manifestClassNameCache.get(manifest);\n if (!names) {\n const collected = new Set<string>();\n for (const [key, object] of Object.entries(manifest.objects ?? {})) {\n collected.add(key);\n if (object?.className) collected.add(object.className);\n if (object?.qualifiedName) collected.add(object.qualifiedName);\n }\n names = collected;\n manifestClassNameCache.set(manifest, names);\n }\n return createClassNamePredicate(names);\n}\n\n/**\n * The same predicate from a bare name list, for a caller whose class inventory\n * is the live `ObjectRegistry` rather than a manifest — notably\n * `@happyvertical/smrt-users`' CLI resource listing, which iterates\n * `ObjectRegistry.getAllClasses()` and has no manifest to hand.\n *\n * Exported so that caller does not grow its own copy: without a predicate the\n * gate half-applies (every rejection EXCEPT model instances), which is worse\n * than either extreme because the consumer then disagrees with the emitters on\n * exactly the largest group of withheld methods.\n */\nexport function createClassNamePredicate(\n names: Iterable<string>,\n): (name: string) => boolean {\n const resolved = names instanceof Set ? names : new Set(names);\n return (name: string) => resolved.has(name);\n}\n\n/**\n * Split a type string on `delimiter`, but only where it appears OUTSIDE every\n * bracket pair — so `Record<string, Asset | null>` splits into two parts on\n * `,` and one part on `|`, never into truncated fragments like `Record<string`.\n *\n * A naive `split()` on either delimiter produces fragments that match no\n * classification rule and are therefore accepted by the default-accept path,\n * silently widening the gate. Both the union test and the type-argument scan\n * read this one implementation.\n */\nfunction splitTopLevel(source: string, delimiter: ',' | '|'): string[] {\n const parts: string[] = [];\n let depth = 0;\n let start = 0;\n for (let index = 0; index < source.length; index += 1) {\n const char = source[index];\n if (char === '<' || char === '(' || char === '[' || char === '{')\n depth += 1;\n else if (char === '>' || char === ')' || char === ']' || char === '}')\n depth -= 1;\n else if (char === delimiter && depth === 0) {\n parts.push(source.slice(start, index));\n start = index + 1;\n }\n }\n parts.push(source.slice(start));\n return parts.map((part) => part.trim()).filter(Boolean);\n}\n\n/**\n * Whether one declared parameter can be built from a JSON request body or\n * query string.\n */\nexport function classifyParameterWireability(\n parameter: WireableParameter,\n options: WireabilityOptions = {},\n): WireabilityVerdict {\n // A rest parameter has no stable name in a body (`options['...args']`), so\n // no transport can project one. This is independent of its element type.\n if (parameter.name.startsWith('...')) {\n return {\n wireable: false,\n reason: `rest parameter \\`${parameter.name}\\` cannot be projected from a request body`,\n };\n }\n\n // Fail closed on scanner uncertainty. `type` reads `'any'` for an\n // intersection or tuple the scanner could not express, and treating that as\n // the author's explicit `any` would route a method nobody certified (#2686).\n if (parameter.typeUnresolved) {\n return {\n wireable: false,\n reason: `the declared type of \\`${parameter.name}\\` could not be resolved by the scanner`,\n };\n }\n\n // A top-level union is wire-able when ANY branch is, and each branch's\n // inline members belong to THAT branch. The flattened `memberTypes` below\n // cannot express this: for `{ callback: () => void } | string` it reports a\n // bare `Function` and rejects a parameter every caller can satisfy with the\n // string branch. Prefer the per-branch view whenever the scanner supplied\n // one; manifests generated before #2686 have none and fall through (#2686).\n const unionBranches = parameter.unionBranches;\n if (unionBranches && unionBranches.length > 0) {\n for (const branch of unionBranches) {\n const rejectedMember = (branch.memberTypes ?? []).find(\n (memberType) => !classifyTypeName(memberType, options, 1).wireable,\n );\n if (rejectedMember !== undefined) continue;\n if (classifyTypeName(branch.type, options, 0).wireable) return WIREABLE;\n }\n return {\n wireable: false,\n reason: `parameter \\`${parameter.name}\\`: no branch of \\`${parameter.type ?? 'any'}\\` can be built from a JSON request`,\n };\n }\n\n for (const memberType of parameter.memberTypes ?? []) {\n const verdict = classifyTypeName(memberType, options, 1);\n if (!verdict.wireable) {\n return {\n wireable: false,\n reason: `\\`${parameter.name}\\` contains a member where ${verdict.reason}`,\n };\n }\n }\n\n const verdict = classifyTypeName(parameter.type ?? 'any', options, 0);\n if (verdict.wireable) return WIREABLE;\n return {\n wireable: false,\n reason: `parameter \\`${parameter.name}\\`: ${verdict.reason}`,\n };\n}\n\n/**\n * Whether every declared parameter of a method can be built from a JSON\n * request body or query string.\n *\n * A method with NO manifest parameter metadata is wire-able: that is the\n * legacy options-bag contract every transport already supports, and absent\n * metadata is not evidence of a hostile signature.\n */\nexport function classifyMethodWireability(\n method: Pick<ExposableMethod, 'parameters'>,\n options: WireabilityOptions = {},\n): WireabilityVerdict {\n for (const parameter of method.parameters ?? []) {\n const verdict = classifyParameterWireability(parameter, options);\n if (!verdict.wireable) return verdict;\n }\n return WIREABLE;\n}\n\n/**\n * Why a public method is not reachable as a generated API action.\n *\n * Machine-readable so callers can react differently per cause: the route\n * emitters warn on `no-receiver` (a configuration mistake worth shouting\n * about) and stay quiet on the rest, while the knowledge artifact reports the\n * accompanying `reason` text for every one of them (#2686).\n */\nexport type ApiMethodRejectionCode =\n | 'api-disabled'\n | 'crud-reserved'\n | 'not-public'\n | 'lifecycle-method'\n | 'excluded'\n | 'not-included'\n | 'withheld'\n | 'not-wireable'\n | 'no-receiver';\n\n/** Verdict of {@link resolveApiMethodExposure}. */\nexport interface ApiMethodExposure {\n exposed: boolean;\n /** Present only when `exposed` is false. */\n code?: ApiMethodRejectionCode;\n /** Human-readable explanation, present only when `exposed` is false. */\n reason?: string;\n}\n\nexport interface ResolveApiMethodExposureOptions extends WireabilityOptions {\n actionName: string;\n method: ExposableMethod;\n /** The class's scanned `api` config (`decoratorConfig.api`). */\n apiConfig?: unknown;\n /**\n * True when the HOST is a collection class, which emits only\n * collection-scoped routes. Drives the receiver check.\n */\n isCollectionClass?: boolean;\n}\n\nconst EXPOSED: ApiMethodExposure = { exposed: true };\n\n/**\n * The single decision every generated-API consumer asks: is this method\n * reachable as a custom REST action, and if not, why?\n *\n * ONE resolver, four consumers — both SvelteKit route emitters\n * (`generateRoutesForObject`, `generateCollectionRoutesForObject`), the\n * cli↔api coherence resolver (`resolveApiActionSet`), and the knowledge\n * artifact's API projection. They previously each re-derived a subset: the\n * emitters filtered on `shouldIncludeInApi` plus their own receiver skip,\n * `resolveApiActionSet` mirrored both, and `knowledge.ts` mirrored the\n * receiver half a third time. A gate added to only one of them would report a\n * method as unavailable while still writing its route file, which is the exact\n * incoherence this issue exists to close (#2686).\n *\n * Order matters, and is the tested precedence contract:\n *\n * 1. `api: false` — the class has no REST surface at all.\n * 2. A CRUD verb — the generated operation already owns the name (#2646).\n * 3. Non-public — never a surface.\n * 4. A framework lifecycle method (`save`, `initialize`, `toJSON`, ...) — the\n * mechanism behind generated CRUD, not a distinct operation, even when a\n * subclass declares its own override. `CLIGenerator` and `MCPGenerator`\n * already gate on this; REST did not, and this is where it joins them\n * (#2638, #2657).\n * 5. `api.exclude` — an explicit withdrawal.\n * 6. `api.include` — an explicit allowlist boundary.\n * 7. `@method({ expose: false })` — an explicit withdrawal that outranks every\n * remaining rule, including a legacy `api.routes` entry for the same\n * method. This is why the decorator is `@method()` and not `@action()`:\n * declaring something an action in order to say it is not one contradicts\n * itself.\n * 8. Explicit legacy exposure — a name listed in `api.include` or carrying an\n * `api.routes` entry is a DECLARATION that this method is a route, made\n * before the heuristic existed. It bypasses the heuristic. This is the\n * documented compatibility exception that makes \"nothing breaks\" true for\n * the 42 existing route entries; without it, migrating a class to the new\n * gate could silently drop a route its author had spelled out.\n * 9. `@method({ expose: true })` — bypasses the heuristic, and NOTHING else.\n * It cannot manufacture a receiver (step 10 still applies), reach a\n * non-public method, or hydrate a parameter the transport cannot build.\n * 10. Wire-ability — every parameter must be constructible from JSON.\n * 11. Receiver — a collection class emits only collection-scoped routes, and a\n * model class cannot host a collection-scoped instance method.\n */\nexport function resolveApiMethodExposure(\n options: ResolveApiMethodExposureOptions,\n): ApiMethodExposure {\n const { actionName, method, apiConfig, isCollectionClass = false } = options;\n\n if (apiConfig === false) {\n return { exposed: false, code: 'api-disabled', reason: 'api is disabled' };\n }\n if (isCrudOperation(actionName)) {\n return {\n exposed: false,\n code: 'crud-reserved',\n reason: `\\`${actionName}\\` is reserved by the generated CRUD operation of the same name`,\n };\n }\n if (method.isPublic === false) {\n return {\n exposed: false,\n code: 'not-public',\n reason: 'not a public method',\n };\n }\n if (isFrameworkLifecycleMethod(actionName)) {\n return {\n exposed: false,\n code: 'lifecycle-method',\n reason: `\\`${actionName}\\` is a framework lifecycle method, not a distinct operation`,\n };\n }\n\n const config = getIncludeExclude(apiConfig);\n if (config.exclude?.includes(actionName)) {\n return {\n exposed: false,\n code: 'excluded',\n reason: 'listed in api.exclude',\n };\n }\n const includedExplicitly = config.include?.includes(actionName) === true;\n if (config.include !== undefined && !includedExplicitly) {\n return {\n exposed: false,\n code: 'not-included',\n reason: 'not listed in api.include',\n };\n }\n\n const declared = readMethodDecoratorConfig(method);\n if (declared?.expose === false) {\n return {\n exposed: false,\n code: 'withheld',\n reason: declared.reason ?? 'withheld by @method({ expose: false })',\n };\n }\n\n const hasLegacyRoute =\n readApiRouteConfig(apiConfig, actionName) !== undefined;\n const bypassesHeuristic =\n declared?.expose === true || includedExplicitly || hasLegacyRoute;\n\n if (!bypassesHeuristic) {\n const wireability = classifyMethodWireability(method, {\n ...(options.isModelClassName\n ? { isModelClassName: options.isModelClassName }\n : {}),\n });\n if (!wireability.wireable) {\n return {\n exposed: false,\n code: 'not-wireable',\n reason: `not routed: ${wireability.reason}`,\n };\n }\n }\n\n const receiver = resolveActionReceiver(\n actionName,\n method,\n apiConfig,\n isCollectionClass,\n );\n if (!receiver.hosted) {\n return { exposed: false, code: 'no-receiver', reason: receiver.reason };\n }\n\n return EXPOSED;\n}\n\n/**\n * Whether the resolved scope has an executable receiver on this host, matching\n * both route emitters' own skips exactly.\n *\n * UNREACHABLE BY CONSTRUCTION, and deliberately kept:\n * {@link resolveCustomActionMetadata} already collapses a contradicting\n * declared scope back to the receiver-derived one, so the resolved scope always\n * equals `defaultScope` and neither branch below can fire. That was equally\n * true of the two `console.warn` skips in `generateRoutesForObject` /\n * `generateCollectionRoutesForObject` that this replaced — moving them here\n * changed nothing about when they fire, and dropping them would remove the only\n * structural guard should that collapse ever be relaxed.\n *\n * The signal a developer actually sees for a contradicting declaration is\n * {@link resolveDeclaredScopeMismatch}, reported by the emitters at build time.\n */\nfunction resolveActionReceiver(\n actionName: string,\n method: ExposableMethod,\n apiConfig: unknown,\n isCollectionClass: boolean,\n): { hosted: true } | { hosted: false; reason: string } {\n const defaultScope: CustomActionScope = isCollectionClass\n ? 'collection'\n : method.isStatic\n ? 'collection'\n : 'item';\n let scope: CustomActionScope;\n try {\n scope = resolveCustomActionMetadata({\n actionName,\n // Only `isStatic` and `decoratorConfig` decide the receiver; the\n // parameter list is irrelevant here, and forwarding this module's looser\n // `WireableParameter[]` view into the manifest-shaped option would force\n // every caller to widen its own method type for no benefit.\n method: {\n ...(method.isStatic !== undefined ? { isStatic: method.isStatic } : {}),\n ...(method.decoratorConfig\n ? { decoratorConfig: method.decoratorConfig }\n : {}),\n },\n apiConfig,\n defaultScope,\n }).scope;\n } catch {\n // The shared resolver validates as it resolves (a `read` effect on a\n // PUT/PATCH/DELETE route throws). One malformed action must not fail a\n // whole build or knowledge projection, and a route-only override cannot\n // change the receiver anyway -- fall back to it.\n scope = defaultScope;\n }\n\n if (isCollectionClass) {\n if (scope !== 'collection') {\n return {\n hosted: false,\n reason:\n 'collection class methods only support collection-scoped API routes',\n };\n }\n return { hosted: true };\n }\n if (scope === 'collection' && method.isStatic !== true) {\n return {\n hosted: false,\n reason: 'collection API routes require a static method',\n };\n }\n return { hosted: true };\n}\n\n/**\n * The effective route/tool metadata for one custom action, with `@method()`\n * winning FIELD BY FIELD over the class-level `api.routes` map and\n * `ai.descriptions`.\n *\n * Field-by-field, not wholesale: `@method({ description: '...' })` on a class\n * that already declares `routes: { runReview: { method: 'POST', path:\n * 'reviews' } }` must not silently reset that verb and path to their defaults.\n * Only options the decorator actually supplies override their legacy\n * counterparts (#2686).\n */\nexport interface EffectiveActionMetadata {\n httpMethod?: ApiHttpMethod;\n path?: string;\n scope?: CustomActionScope;\n effect?: ToolEffect;\n idempotent?: boolean;\n openWorld?: boolean;\n description?: string;\n}\n\nexport function resolveEffectiveActionMetadata(options: {\n actionName: string;\n method?: ExposableMethod;\n apiConfig?: unknown;\n aiConfig?: unknown;\n}): EffectiveActionMetadata {\n const route = readApiRouteConfig(options.apiConfig, options.actionName);\n const declared = readMethodDecoratorConfig(options.method);\n const legacyDescription = readAiDescription(\n options.aiConfig,\n options.actionName,\n );\n\n const httpMethod =\n declared?.httpMethod ?? normalizeRouteHttpMethod(route?.method);\n const path =\n declared?.path ??\n (typeof route?.path === 'string' ? route.path : undefined);\n const scope = declared?.scope ?? normalizeRouteScope(route?.scope);\n const effect = declared?.effect ?? normalizeRouteEffect(route?.effect);\n const idempotent =\n declared?.idempotent ??\n (typeof route?.idempotent === 'boolean' ? route.idempotent : undefined);\n const openWorld =\n declared?.openWorld ??\n (typeof route?.openWorld === 'boolean' ? route.openWorld : undefined);\n const description = declared?.description ?? legacyDescription;\n\n return {\n ...(httpMethod ? { httpMethod } : {}),\n ...(path !== undefined ? { path } : {}),\n ...(scope ? { scope } : {}),\n ...(effect ? { effect } : {}),\n ...(idempotent !== undefined ? { idempotent } : {}),\n ...(openWorld !== undefined ? { openWorld } : {}),\n ...(description !== undefined ? { description } : {}),\n };\n}\n\nfunction normalizeRouteHttpMethod(value: unknown): ApiHttpMethod | undefined {\n return isApiHttpMethod(value) ? value : undefined;\n}\n\nfunction normalizeRouteScope(value: unknown): CustomActionScope | undefined {\n return value === 'item' || value === 'collection' ? value : undefined;\n}\n\nfunction normalizeRouteEffect(value: unknown): ToolEffect | undefined {\n return value === 'read' || value === 'write' || value === 'destructive'\n ? value\n : undefined;\n}\n\nfunction readAiDescription(\n aiConfig: unknown,\n actionName: string,\n): string | undefined {\n if (!isRecord(aiConfig) || !isRecord(aiConfig.descriptions)) return undefined;\n const description = aiConfig.descriptions[actionName];\n return typeof description === 'string' ? description : undefined;\n}\n\nfunction readApiRouteConfig(\n apiConfig: unknown,\n actionName: string,\n): Record<string, unknown> | undefined {\n if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return undefined;\n const route = apiConfig.routes[actionName];\n return isRecord(route) ? route : undefined;\n}\n\n/**\n * Narrow a scanned transport config's `include`/`exclude`.\n *\n * A non-array value is treated as unset rather than throwing later on\n * `.includes()` — the same defensive stance every other reader of scanned\n * decorator config takes, because this data came from an AST, not a compiler.\n */\nfunction getIncludeExclude(config: unknown): {\n include?: string[];\n exclude?: string[];\n} {\n if (config === true || config === undefined || !isRecord(config)) return {};\n return {\n ...(Array.isArray(config.include) ? { include: config.include } : {}),\n ...(Array.isArray(config.exclude) ? { exclude: config.exclude } : {}),\n };\n}\n\n/**\n * Whether a method's `@method()` declaration is also a RUNTIME REST route\n * declaration, the way an `api.routes[m]` entry is.\n *\n * The runtime `APIGenerator` transport is deliberately declaration-gated: it\n * serves a custom collection action only where one was declared, because its URL\n * shape supports a single segment and an undeclared public method has never had\n * a route there. `dispatchCustomCollectionAction` and the `isRestActionRoutable`\n * preflight prediction must agree on that gate exactly, so both read this (#2686).\n *\n * True for any option that migrates from `ApiCustomRouteConfig` — its complete\n * field set is `scope`, `method`, `path`, `effect`, `idempotent`, `openWorld` —\n * because a legacy `routes: { m: { effect: 'write' } }` entry with no path or\n * verb already dispatches at `POST /<collection>/m`, and migrating it onto the\n * method must not silently delete that endpoint. Also true for an explicit\n * `expose: true`, which is a stronger statement that the method is an action\n * than an empty route entry is.\n *\n * FALSE for a bare `@method()` and for a `description`-only one. Neither\n * migrates from a route entry — `description` migrates from `ai.descriptions`,\n * and a bare decorator is a review marker — so counting them would hand the\n * runtime transport endpoints it never served.\n */\nexport function declaresRuntimeRestRoute(\n method: ExposableMethod | undefined,\n): boolean {\n // `expose: false` outranks every other option, including one that would\n // otherwise declare a route. A predicate that still reported such an action\n // routable would make browser-plane preflight answer `allow` for an operation\n // the transport declines — the false-`allow` preflight exists to prevent.\n if (readMethodDecoratorConfig(method)?.expose === false) return false;\n return declaresRuntimeRestRouteShape(method);\n}\n\n/**\n * Whether the author WROTE a runtime REST route declaration on this method,\n * ignoring whether they then withheld it.\n *\n * Deliberately distinct from {@link declaresRuntimeRestRoute}: the dispatcher\n * must still SEE a withheld declaration in order to refuse it explicitly. This\n * router resolves `POST /<collection>/<segment>` to `create` when nothing\n * claims the segment, so dropping a withheld action from the candidate set\n * would turn a request aimed at an explicitly withheld operation into a silent\n * row insert. The candidate set reads this; the preflight PREDICTION reads\n * {@link declaresRuntimeRestRoute}, which adds the `expose: false` veto —\n * \"there is a declaration here\" and \"it is reachable\" are different questions.\n */\nexport function declaresRuntimeRestRouteShape(\n method: ExposableMethod | undefined,\n): boolean {\n const declared = readMethodDecoratorConfig(method);\n if (!declared) return false;\n return (\n declared.httpMethod !== undefined ||\n declared.path !== undefined ||\n declared.scope !== undefined ||\n declared.effect !== undefined ||\n declared.idempotent !== undefined ||\n declared.openWorld !== undefined ||\n declared.expose !== undefined\n );\n}\n\n/**\n * Coerce one transport-supplied argument into the runtime value the declared\n * parameter type needs.\n *\n * Today that means exactly one conversion: a `Date` parameter, which the\n * wire-ability heuristic accepts as JSON-shaped. JSON has no date type, so a\n * caller can only send an ISO string (or an epoch number) and the receiving\n * method — which calls `getTime()`, or hands the value to a query builder that\n * expects a `Date` — would otherwise get a string. Accepting `Date` as\n * wire-able and NOT hydrating it here would generate a route that 500s, so the\n * two are one decision (#2686).\n *\n * Deliberately narrow:\n * - Only a TOP-LEVEL declared parameter is converted. A `Date` nested inside a\n * named options bag is invisible to the manifest (the bag is accepted\n * heuristically, its members unresolved), so it is not hydrated and the\n * method must accept the string itself.\n * - An already-`Date` value, and anything that is not a string or finite\n * number, passes through untouched, so a runtime caller invoking the same\n * helper is never degraded.\n * - An unparseable string passes through as-is rather than becoming an\n * `Invalid Date`, leaving the method's own validation in charge of the error\n * message.\n */\nexport function coerceCustomActionArgument(\n value: unknown,\n declaredType: string | undefined,\n): unknown {\n if (!declaredType || !declaredTypeAcceptsDate(declaredType)) return value;\n return toCustomActionDate(value);\n}\n\n/**\n * The `Date` half of {@link coerceCustomActionArgument}, exported on its own\n * because generated SvelteKit route code calls it directly: the generator\n * already knows at build time which parameters are `Date`-typed, so the\n * emitted handler names the conversion rather than re-deriving it from a type\n * string at runtime. Both paths share this one implementation so the two\n * transports cannot drift.\n */\nexport function toCustomActionDate(value: unknown): unknown {\n if (value instanceof Date) return value;\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value);\n }\n if (typeof value !== 'string' || value.trim() === '') return value;\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? value : parsed;\n}\n\n/**\n * Decode a `number`-typed action argument that arrived over a QUERY STRING.\n *\n * A GET handler builds its options from `URLSearchParams`, so every value is a\n * string: `limit: number` reached the method as `'2'` and any arithmetic on it\n * silently produced string concatenation or `NaN` (#2686). A JSON body needs no\n * such repair, which is why this is emitted only on GET routes.\n *\n * Leaves anything it cannot decode alone, so a malformed value reaches the\n * method's own validation rather than becoming a silent `NaN`.\n */\nexport function toCustomActionNumber(value: unknown): unknown {\n if (typeof value === 'number') return value;\n if (typeof value !== 'string' || value.trim() === '') return value;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : value;\n}\n\n/**\n * The `boolean` counterpart to {@link toCustomActionNumber}. A query string\n * carries `?active=false`, and the bare string `'false'` is TRUTHY — the most\n * dangerous of these coercions, since it inverts a guard rather than degrading\n * it. Only the four canonical spellings decode; anything else is left for the\n * method's own validation.\n */\nexport function toCustomActionBoolean(value: unknown): unknown {\n if (typeof value === 'boolean') return value;\n if (typeof value !== 'string') return value;\n const normalized = value.trim().toLowerCase();\n if (normalized === 'true' || normalized === '1') return true;\n if (normalized === 'false' || normalized === '0') return false;\n return value;\n}\n\n/**\n * The query-string decoder a GET route should apply to one parameter, or\n * `undefined` when the value passes through untouched.\n *\n * Mirrors {@link declaredTypeAcceptsDate}: a nullish branch does not change the\n * representation, but a genuine alternative (`number | string`) means the\n * method already accepts what the query string sends, so nothing is decoded.\n */\nexport function queryStringDecoderFor(\n declaredType: string | undefined,\n):\n | 'toCustomActionDate'\n | 'toCustomActionNumber'\n | 'toCustomActionBoolean'\n | undefined {\n if (!declaredType) return undefined;\n if (declaredTypeAcceptsDate(declaredType)) return 'toCustomActionDate';\n const branches = splitTopLevel(declaredType, '|')\n .map((branch) => branch.trim())\n .filter((branch) => branch !== 'null' && branch !== 'undefined');\n if (branches.length === 0) return undefined;\n if (branches.every((branch) => branch === 'number')) {\n return 'toCustomActionNumber';\n }\n if (branches.every((branch) => branch === 'boolean')) {\n return 'toCustomActionBoolean';\n }\n return undefined;\n}\n\n/**\n * True when the declared type is a `Date` and nothing else.\n *\n * `Date | null` and `Date | undefined` qualify — a nullish branch is not an\n * alternative representation. `Date | string` deliberately does NOT: that\n * signature already accepts the string a JSON caller sends, so the method's\n * own handling is authoritative and converting behind its back would change\n * which branch it takes.\n */\nexport function declaredTypeAcceptsDate(declaredType: string): boolean {\n const branches = declaredType\n .split('|')\n .map((branch) => branch.trim())\n .filter((branch) => branch !== 'null' && branch !== 'undefined');\n return branches.length > 0 && branches.every((branch) => branch === 'Date');\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  * @smrt/core generators - Create REST APIs and MCP servers from SMRT objects
3
3
  */
4
4
  export { canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, PRIVATE_READ_CACHE_CONTROL, type ReadCacheControlOptions, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized, } from './conditional-get';
5
- export { buildCustomActionInputSchema, buildCustomActionInvocationArgs, CRUD_OPERATIONS, type CustomActionFailure, type CustomActionMetadata, type CustomActionScope, customActionParameterInputName, isCrudOperation, isCrudToolAction, normalizeCustomActionFailure, type ResolveCustomActionMetadataOptions, type ResolvedCustomActionMetadata, resolveCustomActionMetadata, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, type ToolEffect, } from './custom-action';
5
+ export { type ApiMethodExposure, type ApiMethodRejectionCode, buildCustomActionInputSchema, buildCustomActionInvocationArgs, CRUD_OPERATIONS, type CustomActionFailure, type CustomActionMetadata, type CustomActionScope, classifyMethodWireability, classifyParameterWireability, coerceCustomActionArgument, createClassNamePredicate, createManifestClassNamePredicate, customActionParameterInputName, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, type EffectiveActionMetadata, type ExposableMethod, isCrudOperation, isCrudToolAction, type MethodDecoratorConfig, normalizeCustomActionFailure, queryStringDecoderFor, type ResolveApiMethodExposureOptions, type ResolveCustomActionMetadataOptions, type ResolvedCustomActionMetadata, readMethodDecoratorConfig, resolveApiMethodExposure, resolveCustomActionMetadata, resolveDeclaredScopeMismatch, resolveEffectiveActionMetadata, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, type ToolEffect, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber, type WireabilityOptions, type WireabilityVerdict, type WireableParameter, } from './custom-action';
6
6
  export { buildChangeEventStream, type ChangeEventStreamOptions, changeEventSubscribersAtCapacity, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, eventStreamCapacityExceededResponse, normalizeEventsMaxSubscribers, signalVisibleToTenant, tryReserveChangeEventSubscriberSlot, } from './events-route';
7
7
  export type { MCPConfig, MCPContext, MCPRequest, MCPResponse, MCPTool, MCPToolListCacheHint, MCPToolListCacheOptions, } from './mcp';
8
8
  export { MCP_STABLE_CATALOG_TTL_MS, MCPGenerator, resolveMCPToolListCacheHint, sortMCPTools, } from './mcp';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,4BAA4B,EAC5B,+BAA+B,EAC/B,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,8BAA8B,EAC9B,eAAe,EACf,gBAAgB,EAChB,4BAA4B,EAC5B,KAAK,kCAAkC,EACvC,KAAK,4BAA4B,EACjC,2BAA2B,EAC3B,qCAAqC,EACrC,KAAK,UAAU,GAChB,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,2BAA2B,EAC3B,8BAA8B,EAC9B,kCAAkC,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,qBAAqB,EACrB,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,yBAAyB,EACzB,YAAY,EACZ,2BAA2B,EAC3B,YAAY,GACb,MAAM,OAAO,CAAC;AAIf,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,oBAAoB,EACpB,iBAAiB,EACjB,6BAA6B,EAC7B,gCAAgC,EAChC,KAAK,yBAAyB,EAC9B,KAAK,6BAA6B,EAClC,KAAK,6BAA6B,EAClC,2BAA2B,EAC3B,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EACL,YAAY,EACZ,6BAA6B,EAC7B,gBAAgB,EAChB,eAAe,GAChB,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,+BAA+B,EAC/B,eAAe,EACf,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,0BAA0B,EAC1B,wBAAwB,EACxB,gCAAgC,EAChC,8BAA8B,EAC9B,uBAAuB,EACvB,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,eAAe,EACf,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,KAAK,+BAA+B,EACpC,KAAK,kCAAkC,EACvC,KAAK,4BAA4B,EACjC,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,EAC3B,4BAA4B,EAC5B,8BAA8B,EAC9B,qCAAqC,EACrC,KAAK,UAAU,EACf,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,2BAA2B,EAC3B,8BAA8B,EAC9B,kCAAkC,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,qBAAqB,EACrB,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,yBAAyB,EACzB,YAAY,EACZ,2BAA2B,EAC3B,YAAY,GACb,MAAM,OAAO,CAAC;AAIf,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,oBAAoB,EACpB,iBAAiB,EACjB,6BAA6B,EAC7B,gCAAgC,EAChC,KAAK,yBAAyB,EAC9B,KAAK,6BAA6B,EAClC,KAAK,6BAA6B,EAClC,2BAA2B,EAC3B,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EACL,YAAY,EACZ,6BAA6B,EAC7B,gBAAgB,EAChB,eAAe,GAChB,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { CRUD_OPERATIONS, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, customActionParameterInputName, isCrudOperation, isCrudToolAction, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
1
+ import { CRUD_OPERATIONS, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, classifyMethodWireability, classifyParameterWireability, coerceCustomActionArgument, createClassNamePredicate, createManifestClassNamePredicate, customActionParameterInputName, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, isCrudOperation, isCrudToolAction, normalizeCustomActionFailure, queryStringDecoderFor, readMethodDecoratorConfig, resolveApiMethodExposure, resolveCustomActionMetadata, resolveDeclaredScopeMismatch, resolveEffectiveActionMetadata, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber } from "./custom-action.js";
2
2
  import { PRIVATE_READ_CACHE_CONTROL, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized } from "./conditional-get.js";
3
3
  import { DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, buildChangeEventStream, changeEventSubscribersAtCapacity, eventStreamCapacityExceededResponse, normalizeEventsMaxSubscribers, signalVisibleToTenant, tryReserveChangeEventSubscriberSlot } from "./events-route.js";
4
4
  import { runWithTenantGate, setTenantEntryPointRunner } from "./tenant-gate.js";
@@ -7,4 +7,4 @@ import { PLAYBOOK_PREFLIGHT_CAPABILITY, PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT, handle
7
7
  import { normalizeTypedHttpError } from "./typed-http-error.js";
8
8
  import { APIGenerator, computeRuntimeWebManifestHash, createRestServer, startRestServer } from "./rest.js";
9
9
  import { generateOpenAPISpec, setupSwaggerUI } from "./swagger.js";
10
- export { APIGenerator, CRUD_OPERATIONS, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, PLAYBOOK_PREFLIGHT_CAPABILITY, PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, customActionParameterInputName, eventStreamCapacityExceededResponse, generateOpenAPISpec, handlePlaybookPreflightRoute, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, isApiActionEnabledForObject, isCrudOperation, isCrudToolAction, isRestActionRoutable, isRestRoutePublic, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, normalizeTypedHttpError, resolveCustomActionMetadata, resolveMCPToolListCacheHint, resolveReadCacheControl, resolveRegisteredObjectName, resolveTenantEtagDiscriminator, restFieldReadPermissions, restMethodForApiAction, runWithTenantGate, setTenantEntryPointRunner, setupSwaggerUI, signalVisibleToTenant, sortMCPTools, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
10
+ export { APIGenerator, CRUD_OPERATIONS, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, PLAYBOOK_PREFLIGHT_CAPABILITY, PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, classifyMethodWireability, classifyParameterWireability, coerceCustomActionArgument, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createClassNamePredicate, createManifestClassNamePredicate, createRestServer, customActionParameterInputName, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, eventStreamCapacityExceededResponse, generateOpenAPISpec, handlePlaybookPreflightRoute, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, isApiActionEnabledForObject, isCrudOperation, isCrudToolAction, isRestActionRoutable, isRestRoutePublic, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, normalizeTypedHttpError, queryStringDecoderFor, readMethodDecoratorConfig, resolveApiMethodExposure, resolveCustomActionMetadata, resolveDeclaredScopeMismatch, resolveEffectiveActionMetadata, resolveMCPToolListCacheHint, resolveReadCacheControl, resolveRegisteredObjectName, resolveTenantEtagDiscriminator, restFieldReadPermissions, restMethodForApiAction, runWithTenantGate, setTenantEntryPointRunner, setupSwaggerUI, signalVisibleToTenant, sortMCPTools, startRestServer, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
@@ -98,12 +98,13 @@ export declare function resolveRegisteredObjectName(model: string): string | und
98
98
  * The HTTP method a REST action is served by.
99
99
  *
100
100
  * CRUD actions map to their fixed verbs. A custom action is served under the
101
- * method its own route config declares (`api.routes[action].method`, defaulting
102
- * to `POST` exactly as `dispatchCustomCollectionAction` does), because guessing
103
- * `POST` for a declared `GET` action would make preflight report a false `deny`
104
- * on a `public: 'read'` model — hiding a playbook the caller can actually run,
105
- * which is the tool-listing case this exists to serve. Without an `objectName`
106
- * to read the declaration from, the fail-closed `POST` remains.
101
+ * method its own declaration supplies — `@method({ httpMethod })` first, then
102
+ * `api.routes[action].method`, defaulting to `POST` exactly as
103
+ * `dispatchCustomCollectionAction` does — because guessing `POST` for a
104
+ * declared `GET` action would make preflight report a false `deny` on a
105
+ * `public: 'read'` model, hiding a playbook the caller can actually run, which
106
+ * is the tool-listing case this exists to serve. Without an `objectName` to
107
+ * read the declaration from, the fail-closed `POST` remains.
107
108
  */
108
109
  export declare function restMethodForApiAction(action: string, objectName?: string): string;
109
110
  /**
@@ -124,13 +125,14 @@ export declare function isApiActionEnabledForObject(objectName: string | undefin
124
125
  * Whether `action` names a route the generated REST surface can actually
125
126
  * dispatch on `objectName`.
126
127
  *
127
- * CRUD actions always have a route. A custom action exists only when the
128
- * decorator declares it in `api.routes` — `dispatchCustomCollectionAction`
129
- * iterates exactly that map, so an action absent from it can never execute no
128
+ * CRUD actions always have a route. A custom action exists only when it is
129
+ * DECLARED — historically an `api.routes` entry, and since #2686 also a
130
+ * `@method()` supplying route-shaping metadata. `dispatchCustomCollectionAction`
131
+ * iterates exactly that union, so an action outside it can never execute no
130
132
  * matter what `include`/`exclude` say. Exposure alone would report a typo'd or
131
133
  * removed custom action as `allow`, and an agent would then start the earlier
132
134
  * steps of a non-atomic playbook before dying on it — the failure preflight
133
- * exists to prevent.
135
+ * exists to prevent. This prediction and that dispatch must stay one rule.
134
136
  */
135
137
  export declare function isRestActionRoutable(objectName: string | undefined, action: string): boolean;
136
138
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"preflight-route.d.ts","sourceRoot":"","sources":["../../src/generators/preflight-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,8EAA8E;AAC9E,eAAO,MAAM,gCAAgC,eAAe,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;;EAI/B,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0CAA0C;IAC1C,KAAK,EAAE,SAAS,CAAC;IACjB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,8EAA8E;IAC9E,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uCAAuC;IACvC,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,6BAA6B,KACnC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAYD;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,QAAQ,CAAC,CAwBnB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK7E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CA4BR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAsBT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAkBT;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,MAAM,EAAE,CAiBV"}
1
+ {"version":3,"file":"preflight-route.d.ts","sourceRoot":"","sources":["../../src/generators/preflight-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAWH,8EAA8E;AAC9E,eAAO,MAAM,gCAAgC,eAAe,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;;EAI/B,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0CAA0C;IAC1C,KAAK,EAAE,SAAS,CAAC;IACjB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,8EAA8E;IAC9E,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uCAAuC;IACvC,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,6BAA6B,KACnC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAYD;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,QAAQ,CAAC,CAwBnB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAgCR;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAsBT;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAuCT;AAeD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,MAAM,EAAE,CAiBV"}
@@ -1,3 +1,4 @@
1
+ import { declaresRuntimeRestRoute, readMethodDecoratorConfig, resolveEffectiveActionMetadata } from "./custom-action.js";
1
2
  import { ObjectRegistry } from "../registry.js";
2
3
  import { PRIVATE_READ_CACHE_CONTROL } from "./conditional-get.js";
3
4
  //#region src/generators/preflight-route.ts
@@ -91,12 +92,13 @@ function resolveRegisteredObjectName(model) {
91
92
  * The HTTP method a REST action is served by.
92
93
  *
93
94
  * CRUD actions map to their fixed verbs. A custom action is served under the
94
- * method its own route config declares (`api.routes[action].method`, defaulting
95
- * to `POST` exactly as `dispatchCustomCollectionAction` does), because guessing
96
- * `POST` for a declared `GET` action would make preflight report a false `deny`
97
- * on a `public: 'read'` model — hiding a playbook the caller can actually run,
98
- * which is the tool-listing case this exists to serve. Without an `objectName`
99
- * to read the declaration from, the fail-closed `POST` remains.
95
+ * method its own declaration supplies — `@method({ httpMethod })` first, then
96
+ * `api.routes[action].method`, defaulting to `POST` exactly as
97
+ * `dispatchCustomCollectionAction` does — because guessing `POST` for a
98
+ * declared `GET` action would make preflight report a false `deny` on a
99
+ * `public: 'read'` model, hiding a playbook the caller can actually run, which
100
+ * is the tool-listing case this exists to serve. Without an `objectName` to
101
+ * read the declaration from, the fail-closed `POST` remains.
100
102
  */
101
103
  function restMethodForApiAction(action, objectName) {
102
104
  switch (action) {
@@ -109,8 +111,22 @@ function restMethodForApiAction(action, objectName) {
109
111
  }
110
112
  if (!objectName) return "POST";
111
113
  const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
112
- if (!apiConfig || typeof apiConfig !== "object" || !apiConfig.routes) return "POST";
113
- return (apiConfig.routes[action]?.method ?? "POST").toUpperCase();
114
+ if (!apiConfig || typeof apiConfig !== "object") return "POST";
115
+ return (resolveEffectiveActionMetadata({
116
+ actionName: action,
117
+ ...readRegisteredMethod(objectName, action) ? { method: readRegisteredMethod(objectName, action) } : {},
118
+ apiConfig
119
+ }).httpMethod ?? "POST").toUpperCase();
120
+ }
121
+ /**
122
+ * The method view backing `action`: the manifest entry when the registry has
123
+ * one, with its `@method()` config backfilled from the live decorator store
124
+ * when it does not — the unscanned-runtime posture, where reading only the
125
+ * manifest would silently ignore `@method({ expose: false })` (#2686). Mirrors
126
+ * `APIGenerator.runtimeMethod`, which decides the dispatch this predicts.
127
+ */
128
+ function readRegisteredMethod(objectName, action) {
129
+ return ObjectRegistry.resolveRuntimeMethod(objectName, action).method;
114
130
  }
115
131
  /**
116
132
  * Whether `@smrt({ api })` exposes `action` on `objectName`.
@@ -139,20 +155,36 @@ function isApiActionEnabledForObject(objectName, action) {
139
155
  * Whether `action` names a route the generated REST surface can actually
140
156
  * dispatch on `objectName`.
141
157
  *
142
- * CRUD actions always have a route. A custom action exists only when the
143
- * decorator declares it in `api.routes` — `dispatchCustomCollectionAction`
144
- * iterates exactly that map, so an action absent from it can never execute no
158
+ * CRUD actions always have a route. A custom action exists only when it is
159
+ * DECLARED — historically an `api.routes` entry, and since #2686 also a
160
+ * `@method()` supplying route-shaping metadata. `dispatchCustomCollectionAction`
161
+ * iterates exactly that union, so an action outside it can never execute no
145
162
  * matter what `include`/`exclude` say. Exposure alone would report a typo'd or
146
163
  * removed custom action as `allow`, and an agent would then start the earlier
147
164
  * steps of a non-atomic playbook before dying on it — the failure preflight
148
- * exists to prevent.
165
+ * exists to prevent. This prediction and that dispatch must stay one rule.
149
166
  */
150
167
  function isRestActionRoutable(objectName, action) {
151
168
  if (!objectName) return false;
152
169
  if (action === "list" || action === "get" || action === "create" || action === "update" || action === "delete") return true;
153
170
  const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
154
- if (!apiConfig || typeof apiConfig !== "object" || !apiConfig.routes) return false;
155
- return Object.hasOwn(apiConfig.routes, action);
171
+ if (!apiConfig || typeof apiConfig !== "object") return false;
172
+ const registered = readRegisteredMethod(objectName, action);
173
+ if (readMethodDecoratorConfig(registered)?.expose === false) return false;
174
+ if (apiConfig.routes && Object.hasOwn(apiConfig.routes, action)) return isHostableByRuntimeRest(objectName, action);
175
+ return declaresRuntimeRestRoute(registered) && isHostableByRuntimeRest(objectName, action);
176
+ }
177
+ /**
178
+ * Whether this transport can HOST the action at all.
179
+ *
180
+ * It serves only collection-scoped custom actions, so an item-scoped
181
+ * declaration — the `@method()` decorator's most common shape, on a model
182
+ * instance method — answers 404 in dispatch. Predicting `allow` for it is the
183
+ * false-`allow` this module exists to prevent, so routability is the
184
+ * conjunction of "declared" and "hostable" (#2686).
185
+ */
186
+ function isHostableByRuntimeRest(objectName, action) {
187
+ return ObjectRegistry.isCollectionHostedMethod(objectName, action);
156
188
  }
157
189
  /**
158
190
  * Fail-closed authorization posture (#1540): true only when the object opts out
@@ -1 +1 @@
1
- {"version":3,"file":"preflight-route.js","names":[],"sources":["../../src/generators/preflight-route.ts"],"sourcesContent":["/**\n * Generated `_preflight` HTTP route for browser-plane playbook preflight\n * (issue #2590).\n *\n * Handles `GET {basePath}/_preflight?key=<playbook key>` in the REST generator:\n * an advisory, read-effect, idempotent report of what a caller's playbook would\n * be allowed to do — *predicted*, never granted. Every step is authorized again\n * where it executes; this route is capability **selection**, never\n * authorization (epic #2585 invariant 2).\n *\n * ## `authMiddleware` is never invoked here\n *\n * Generated REST authorization is\n * `authMiddleware?: (objectName, action) => (req) => Promise<Request | Response>`\n * — request-bound, `Response`-returning rather than boolean, and free to consult\n * session stores, rate-limit, or audit. It is not a dry-run predicate, and a\n * synthetic-`Request` dry run of it would be a side effect the caller never\n * asked for.\n *\n * That is enforced **structurally**, not by discipline:\n * {@link PlaybookPreflightRouteOptions} has no `authMiddleware` member and no\n * function-valued auth member of any kind. `rest.ts` passes the boolean\n * `appAuthConfigured` and nothing else, so there is no handle in this module to\n * invoke even by mistake. The app-auth layer is consequently reported as\n * `unknown`, which is the honest answer.\n *\n * ## Why the provider is injected\n *\n * Resolution and verdict shaping live in `@happyvertical/smrt-playbooks`, which\n * depends on this package. Core therefore owns the route and the static-layer\n * facts (`ObjectRegistry` is core's), and takes the evaluator as a seam — the\n * dependency stays one-way.\n */\n\nimport { ObjectRegistry } from '../registry.js';\nimport { PRIVATE_READ_CACHE_CONTROL } from './conditional-get.js';\n\n/** Path segment the preflight route is served at, under the API base path. */\nexport const PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT = '_preflight';\n\n/**\n * Capability classification of the preflight route: a read, safely repeatable,\n * and closed-world. It is admitted by a default read-only browser exposure\n * policy without an opt-in, because it reports on the system rather than\n * changing it.\n */\nexport const PLAYBOOK_PREFLIGHT_CAPABILITY = Object.freeze({\n effect: 'read',\n idempotent: true,\n openWorld: false,\n} as const);\n\n/**\n * The request handed to a preflight provider. Deliberately carries no request,\n * no headers, and no auth handle — only the caller-scoped facts the static\n * layers need.\n */\nexport interface PlaybookPreflightRouteRequest {\n /** The requested playbook key. */\n key: string;\n /** Always `'browser'` from this route. */\n plane: 'browser';\n /** The caller's published permission slugs, when the API context has them. */\n permissions?: Iterable<string>;\n /** Whether an app auth middleware is wired. A boolean; never the function. */\n appAuthConfigured: boolean;\n /** The generator's `APIContext.db`. */\n db?: unknown;\n}\n\n/**\n * Host-supplied browser-plane preflight evaluator, wired from\n * `@happyvertical/smrt-playbooks`. Returns a JSON-serializable report.\n */\nexport type PlaybookPreflightProvider = (\n request: PlaybookPreflightRouteRequest,\n) => Promise<unknown>;\n\n/**\n * Options for {@link handlePlaybookPreflightRoute}.\n *\n * There is intentionally no `authMiddleware` member — see the module docs.\n */\nexport interface PlaybookPreflightRouteOptions {\n provider?: PlaybookPreflightProvider;\n permissions?: Iterable<string>;\n appAuthConfigured: boolean;\n db?: unknown;\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': PRIVATE_READ_CACHE_CONTROL,\n },\n });\n}\n\n/**\n * Serves `GET {basePath}/_preflight?key=<key>`.\n *\n * Unresolvable keys are the provider's concern: it returns the same uniform\n * \"unavailable\" body for an unknown key as for an unauthorized one, and this\n * route serves whatever it returns with an unconditional 200, so the HTTP layer\n * adds no way to tell them apart either.\n */\nexport async function handlePlaybookPreflightRoute(\n req: Request,\n options: PlaybookPreflightRouteOptions,\n): Promise<Response> {\n if (!options.provider) {\n // Route not wired for this deployment. Says nothing about any key.\n return jsonResponse({ error: 'Not found' }, 404);\n }\n\n if (req.method !== 'GET') {\n return jsonResponse({ error: 'Method not allowed' }, 405);\n }\n\n const key = new URL(req.url).searchParams.get('key');\n if (!key) {\n return jsonResponse({ error: \"Query parameter 'key' is required\" }, 400);\n }\n\n const report = await options.provider({\n key,\n plane: 'browser',\n ...(options.permissions ? { permissions: options.permissions } : {}),\n appAuthConfigured: options.appAuthConfigured,\n db: options.db,\n });\n\n return jsonResponse(report, 200);\n}\n\n/**\n * Resolves a qualified model reference (`@happyvertical/smrt-commerce:Order`)\n * to the name the registry and the REST generator key configuration by.\n *\n * Returns `undefined` for anything this build does not register, so the caller\n * fails closed rather than reporting on a model that has no route.\n */\nexport function resolveRegisteredObjectName(model: string): string | undefined {\n const registered = model.includes(':')\n ? ObjectRegistry.getClassByQualifiedName(model)\n : ObjectRegistry.getClass(model);\n return registered?.name;\n}\n\n/**\n * The HTTP method a REST action is served by.\n *\n * CRUD actions map to their fixed verbs. A custom action is served under the\n * method its own route config declares (`api.routes[action].method`, defaulting\n * to `POST` exactly as `dispatchCustomCollectionAction` does), because guessing\n * `POST` for a declared `GET` action would make preflight report a false `deny`\n * on a `public: 'read'` model — hiding a playbook the caller can actually run,\n * which is the tool-listing case this exists to serve. Without an `objectName`\n * to read the declaration from, the fail-closed `POST` remains.\n */\nexport function restMethodForApiAction(\n action: string,\n objectName?: string,\n): string {\n switch (action) {\n case 'list':\n case 'get':\n return 'GET';\n case 'create':\n return 'POST';\n case 'update':\n return 'PUT';\n case 'delete':\n return 'DELETE';\n default:\n break;\n }\n\n if (!objectName) {\n return 'POST';\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object' || !apiConfig.routes) {\n return 'POST';\n }\n\n const route = (apiConfig.routes as Record<string, { method?: string }>)[\n action\n ];\n return (route?.method ?? 'POST').toUpperCase();\n}\n\n/**\n * Whether `@smrt({ api })` exposes `action` on `objectName`.\n *\n * The generator's own action gate, lifted to a module function so preflight and\n * the live route read the same rule from one place. Accepts any action name so\n * a custom action is gated by the same `include`/`exclude` lists.\n *\n * @remarks An absent `objectName` returns `true` — fail-**open**, because the\n * live route reaches this only after it has already resolved a model, and an\n * unnamed object there means \"not gated by this rule\". A caller *predicting*\n * rather than serving has no such guarantee and must reject an unresolvable\n * model before asking (see `createRestPreflightLayerSource`).\n */\nexport function isApiActionEnabledForObject(\n objectName: string | undefined,\n action: string,\n): boolean {\n if (!objectName) {\n return true;\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName).api;\n\n if (apiConfig === false) {\n return false;\n }\n\n if (apiConfig && typeof apiConfig === 'object') {\n if (apiConfig.include && !apiConfig.include.includes(action)) {\n return false;\n }\n\n if (apiConfig.exclude?.includes(action)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Whether `action` names a route the generated REST surface can actually\n * dispatch on `objectName`.\n *\n * CRUD actions always have a route. A custom action exists only when the\n * decorator declares it in `api.routes` — `dispatchCustomCollectionAction`\n * iterates exactly that map, so an action absent from it can never execute no\n * matter what `include`/`exclude` say. Exposure alone would report a typo'd or\n * removed custom action as `allow`, and an agent would then start the earlier\n * steps of a non-atomic playbook before dying on it — the failure preflight\n * exists to prevent.\n */\nexport function isRestActionRoutable(\n objectName: string | undefined,\n action: string,\n): boolean {\n if (!objectName) return false;\n if (\n action === 'list' ||\n action === 'get' ||\n action === 'create' ||\n action === 'update' ||\n action === 'delete'\n ) {\n return true;\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object' || !apiConfig.routes) {\n return false;\n }\n\n return Object.hasOwn(apiConfig.routes as Record<string, unknown>, action);\n}\n\n/**\n * Fail-closed authorization posture (#1540): true only when the object opts out\n * of auth via `@smrt({ api: { public } })` — `true` for every method, `'read'`\n * for safe (GET) methods only.\n */\nexport function isRestRoutePublic(\n objectName: string | undefined,\n method: string,\n): boolean {\n if (!objectName) return false;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') return false;\n const publicAccess = (apiConfig as { public?: boolean | 'read' }).public;\n if (publicAccess === true) return true;\n if (publicAccess === 'read') return method.toUpperCase() === 'GET';\n return false;\n}\n\n/**\n * Field-level read-permission slugs declared by `objectName`.\n *\n * Reads the model's own and inherited fields only — deliberately **not** the\n * STI base-plus-descendants union used for cache policy. That union answers\n * \"does any variant carry a read-permission field?\"; used here it would report\n * a sibling variant's slugs as required for a step naming this concrete model.\n */\nexport function restFieldReadPermissions(\n objectName: string | undefined,\n): string[] {\n if (!objectName) return [];\n const registered = ObjectRegistry.getClass(objectName);\n const className = registered?.qualifiedName ?? registered?.name ?? objectName;\n const fields =\n registered?.inheritedFields ?? ObjectRegistry.getFields(className);\n const slugs = new Set<string>();\n for (const [, def] of fields) {\n const slug =\n typeof def?.readPermission === 'string'\n ? def.readPermission\n : typeof def?._meta?.readPermission === 'string'\n ? (def._meta.readPermission as string)\n : undefined;\n if (slug) slugs.add(slug);\n }\n return [...slugs].sort();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"preflight-route.js","names":[],"sources":["../../src/generators/preflight-route.ts"],"sourcesContent":["/**\n * Generated `_preflight` HTTP route for browser-plane playbook preflight\n * (issue #2590).\n *\n * Handles `GET {basePath}/_preflight?key=<playbook key>` in the REST generator:\n * an advisory, read-effect, idempotent report of what a caller's playbook would\n * be allowed to do — *predicted*, never granted. Every step is authorized again\n * where it executes; this route is capability **selection**, never\n * authorization (epic #2585 invariant 2).\n *\n * ## `authMiddleware` is never invoked here\n *\n * Generated REST authorization is\n * `authMiddleware?: (objectName, action) => (req) => Promise<Request | Response>`\n * — request-bound, `Response`-returning rather than boolean, and free to consult\n * session stores, rate-limit, or audit. It is not a dry-run predicate, and a\n * synthetic-`Request` dry run of it would be a side effect the caller never\n * asked for.\n *\n * That is enforced **structurally**, not by discipline:\n * {@link PlaybookPreflightRouteOptions} has no `authMiddleware` member and no\n * function-valued auth member of any kind. `rest.ts` passes the boolean\n * `appAuthConfigured` and nothing else, so there is no handle in this module to\n * invoke even by mistake. The app-auth layer is consequently reported as\n * `unknown`, which is the honest answer.\n *\n * ## Why the provider is injected\n *\n * Resolution and verdict shaping live in `@happyvertical/smrt-playbooks`, which\n * depends on this package. Core therefore owns the route and the static-layer\n * facts (`ObjectRegistry` is core's), and takes the evaluator as a seam — the\n * dependency stays one-way.\n */\n\nimport { ObjectRegistry } from '../registry.js';\nimport type { MethodDefinition } from '../scanner/types.js';\nimport { PRIVATE_READ_CACHE_CONTROL } from './conditional-get.js';\nimport {\n declaresRuntimeRestRoute,\n readMethodDecoratorConfig,\n resolveEffectiveActionMetadata,\n} from './custom-action.js';\n\n/** Path segment the preflight route is served at, under the API base path. */\nexport const PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT = '_preflight';\n\n/**\n * Capability classification of the preflight route: a read, safely repeatable,\n * and closed-world. It is admitted by a default read-only browser exposure\n * policy without an opt-in, because it reports on the system rather than\n * changing it.\n */\nexport const PLAYBOOK_PREFLIGHT_CAPABILITY = Object.freeze({\n effect: 'read',\n idempotent: true,\n openWorld: false,\n} as const);\n\n/**\n * The request handed to a preflight provider. Deliberately carries no request,\n * no headers, and no auth handle — only the caller-scoped facts the static\n * layers need.\n */\nexport interface PlaybookPreflightRouteRequest {\n /** The requested playbook key. */\n key: string;\n /** Always `'browser'` from this route. */\n plane: 'browser';\n /** The caller's published permission slugs, when the API context has them. */\n permissions?: Iterable<string>;\n /** Whether an app auth middleware is wired. A boolean; never the function. */\n appAuthConfigured: boolean;\n /** The generator's `APIContext.db`. */\n db?: unknown;\n}\n\n/**\n * Host-supplied browser-plane preflight evaluator, wired from\n * `@happyvertical/smrt-playbooks`. Returns a JSON-serializable report.\n */\nexport type PlaybookPreflightProvider = (\n request: PlaybookPreflightRouteRequest,\n) => Promise<unknown>;\n\n/**\n * Options for {@link handlePlaybookPreflightRoute}.\n *\n * There is intentionally no `authMiddleware` member — see the module docs.\n */\nexport interface PlaybookPreflightRouteOptions {\n provider?: PlaybookPreflightProvider;\n permissions?: Iterable<string>;\n appAuthConfigured: boolean;\n db?: unknown;\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': PRIVATE_READ_CACHE_CONTROL,\n },\n });\n}\n\n/**\n * Serves `GET {basePath}/_preflight?key=<key>`.\n *\n * Unresolvable keys are the provider's concern: it returns the same uniform\n * \"unavailable\" body for an unknown key as for an unauthorized one, and this\n * route serves whatever it returns with an unconditional 200, so the HTTP layer\n * adds no way to tell them apart either.\n */\nexport async function handlePlaybookPreflightRoute(\n req: Request,\n options: PlaybookPreflightRouteOptions,\n): Promise<Response> {\n if (!options.provider) {\n // Route not wired for this deployment. Says nothing about any key.\n return jsonResponse({ error: 'Not found' }, 404);\n }\n\n if (req.method !== 'GET') {\n return jsonResponse({ error: 'Method not allowed' }, 405);\n }\n\n const key = new URL(req.url).searchParams.get('key');\n if (!key) {\n return jsonResponse({ error: \"Query parameter 'key' is required\" }, 400);\n }\n\n const report = await options.provider({\n key,\n plane: 'browser',\n ...(options.permissions ? { permissions: options.permissions } : {}),\n appAuthConfigured: options.appAuthConfigured,\n db: options.db,\n });\n\n return jsonResponse(report, 200);\n}\n\n/**\n * Resolves a qualified model reference (`@happyvertical/smrt-commerce:Order`)\n * to the name the registry and the REST generator key configuration by.\n *\n * Returns `undefined` for anything this build does not register, so the caller\n * fails closed rather than reporting on a model that has no route.\n */\nexport function resolveRegisteredObjectName(model: string): string | undefined {\n const registered = model.includes(':')\n ? ObjectRegistry.getClassByQualifiedName(model)\n : ObjectRegistry.getClass(model);\n return registered?.name;\n}\n\n/**\n * The HTTP method a REST action is served by.\n *\n * CRUD actions map to their fixed verbs. A custom action is served under the\n * method its own declaration supplies — `@method({ httpMethod })` first, then\n * `api.routes[action].method`, defaulting to `POST` exactly as\n * `dispatchCustomCollectionAction` does — because guessing `POST` for a\n * declared `GET` action would make preflight report a false `deny` on a\n * `public: 'read'` model, hiding a playbook the caller can actually run, which\n * is the tool-listing case this exists to serve. Without an `objectName` to\n * read the declaration from, the fail-closed `POST` remains.\n */\nexport function restMethodForApiAction(\n action: string,\n objectName?: string,\n): string {\n switch (action) {\n case 'list':\n case 'get':\n return 'GET';\n case 'create':\n return 'POST';\n case 'update':\n return 'PUT';\n case 'delete':\n return 'DELETE';\n default:\n break;\n }\n\n if (!objectName) {\n return 'POST';\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') {\n return 'POST';\n }\n\n const effective = resolveEffectiveActionMetadata({\n actionName: action,\n ...(readRegisteredMethod(objectName, action)\n ? { method: readRegisteredMethod(objectName, action) }\n : {}),\n apiConfig,\n });\n return (effective.httpMethod ?? 'POST').toUpperCase();\n}\n\n/**\n * The method view backing `action`: the manifest entry when the registry has\n * one, with its `@method()` config backfilled from the live decorator store\n * when it does not — the unscanned-runtime posture, where reading only the\n * manifest would silently ignore `@method({ expose: false })` (#2686). Mirrors\n * `APIGenerator.runtimeMethod`, which decides the dispatch this predicts.\n */\nfunction readRegisteredMethod(\n objectName: string,\n action: string,\n): MethodDefinition | undefined {\n return ObjectRegistry.resolveRuntimeMethod(objectName, action).method;\n}\n\n/**\n * Whether `@smrt({ api })` exposes `action` on `objectName`.\n *\n * The generator's own action gate, lifted to a module function so preflight and\n * the live route read the same rule from one place. Accepts any action name so\n * a custom action is gated by the same `include`/`exclude` lists.\n *\n * @remarks An absent `objectName` returns `true` — fail-**open**, because the\n * live route reaches this only after it has already resolved a model, and an\n * unnamed object there means \"not gated by this rule\". A caller *predicting*\n * rather than serving has no such guarantee and must reject an unresolvable\n * model before asking (see `createRestPreflightLayerSource`).\n */\nexport function isApiActionEnabledForObject(\n objectName: string | undefined,\n action: string,\n): boolean {\n if (!objectName) {\n return true;\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName).api;\n\n if (apiConfig === false) {\n return false;\n }\n\n if (apiConfig && typeof apiConfig === 'object') {\n if (apiConfig.include && !apiConfig.include.includes(action)) {\n return false;\n }\n\n if (apiConfig.exclude?.includes(action)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Whether `action` names a route the generated REST surface can actually\n * dispatch on `objectName`.\n *\n * CRUD actions always have a route. A custom action exists only when it is\n * DECLARED — historically an `api.routes` entry, and since #2686 also a\n * `@method()` supplying route-shaping metadata. `dispatchCustomCollectionAction`\n * iterates exactly that union, so an action outside it can never execute no\n * matter what `include`/`exclude` say. Exposure alone would report a typo'd or\n * removed custom action as `allow`, and an agent would then start the earlier\n * steps of a non-atomic playbook before dying on it — the failure preflight\n * exists to prevent. This prediction and that dispatch must stay one rule.\n */\nexport function isRestActionRoutable(\n objectName: string | undefined,\n action: string,\n): boolean {\n if (!objectName) return false;\n if (\n action === 'list' ||\n action === 'get' ||\n action === 'create' ||\n action === 'update' ||\n action === 'delete'\n ) {\n return true;\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') {\n return false;\n }\n\n const registered = readRegisteredMethod(objectName, action);\n\n // `@method({ expose: false })` outranks a legacy `api.routes` entry for the\n // same method, and dispatch honors that. Checking it BEFORE the routes map is\n // what keeps the two one rule: otherwise a withheld action that still carried\n // a route declaration predicted `allow` for an operation the transport\n // refuses (#2686).\n if (readMethodDecoratorConfig(registered)?.expose === false) return false;\n\n if (\n apiConfig.routes &&\n Object.hasOwn(apiConfig.routes as Record<string, unknown>, action)\n ) {\n return isHostableByRuntimeRest(objectName, action);\n }\n\n // Shared with `APIGenerator.declaredCollectionActions`, which decides the\n // dispatch this predicate exists to predict.\n return (\n declaresRuntimeRestRoute(registered) &&\n isHostableByRuntimeRest(objectName, action)\n );\n}\n\n/**\n * Whether this transport can HOST the action at all.\n *\n * It serves only collection-scoped custom actions, so an item-scoped\n * declaration — the `@method()` decorator's most common shape, on a model\n * instance method — answers 404 in dispatch. Predicting `allow` for it is the\n * false-`allow` this module exists to prevent, so routability is the\n * conjunction of \"declared\" and \"hostable\" (#2686).\n */\nfunction isHostableByRuntimeRest(objectName: string, action: string): boolean {\n return ObjectRegistry.isCollectionHostedMethod(objectName, action);\n}\n\n/**\n * Fail-closed authorization posture (#1540): true only when the object opts out\n * of auth via `@smrt({ api: { public } })` — `true` for every method, `'read'`\n * for safe (GET) methods only.\n */\nexport function isRestRoutePublic(\n objectName: string | undefined,\n method: string,\n): boolean {\n if (!objectName) return false;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') return false;\n const publicAccess = (apiConfig as { public?: boolean | 'read' }).public;\n if (publicAccess === true) return true;\n if (publicAccess === 'read') return method.toUpperCase() === 'GET';\n return false;\n}\n\n/**\n * Field-level read-permission slugs declared by `objectName`.\n *\n * Reads the model's own and inherited fields only — deliberately **not** the\n * STI base-plus-descendants union used for cache policy. That union answers\n * \"does any variant carry a read-permission field?\"; used here it would report\n * a sibling variant's slugs as required for a step naming this concrete model.\n */\nexport function restFieldReadPermissions(\n objectName: string | undefined,\n): string[] {\n if (!objectName) return [];\n const registered = ObjectRegistry.getClass(objectName);\n const className = registered?.qualifiedName ?? registered?.name ?? objectName;\n const fields =\n registered?.inheritedFields ?? ObjectRegistry.getFields(className);\n const slugs = new Set<string>();\n for (const [, def] of fields) {\n const slug =\n typeof def?.readPermission === 'string'\n ? def.readPermission\n : typeof def?._meta?.readPermission === 'string'\n ? (def._meta.readPermission as string)\n : undefined;\n if (slug) slugs.add(slug);\n }\n return [...slugs].sort();\n}\n"],"mappings":"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"}