diff --git a/.changeset/drop-dead-list-templates.md b/.changeset/drop-dead-list-templates.md new file mode 100644 index 0000000000..992488ee45 --- /dev/null +++ b/.changeset/drop-dead-list-templates.md @@ -0,0 +1,33 @@ +--- +"@objectstack/client": minor +--- + +feat(client)!: delete `projects.listTemplates()` — it targeted a route nothing +has ever mounted (#3702, #3655 finding) + +`client.projects.listTemplates()` built `GET /api/v1/cloud/templates`. That +path is mounted by **nothing**: none of the 17 registrars in `cloud`'s +`cloud-artifact-api-plugin.ts` (91 registrations, enumerated by driving them +against a capturing mock `IHttpServer`), and nothing in this repo — the string +occurred exactly once in each repo, at the call itself. Every invocation was a +404 with a type signature promising a resolved value. + +"Templates" are real as **data** — `sys_package_templates`, the +`is_starter = true` view over `sys_package`, rendered as a console page — but +there has never been an HTTP route that lists them, and no caller in either +repo (nor in `objectui`) used the method. Mounting a route to satisfy a method +nobody calls is the wrong order: the client's declared shape +(`{ id, label, description, category? }`) does not match `sys_package`'s +columns, so picking that mapping is a product decision, not an implementation +detail. The method returns when a route exists to back it. + +Sixth instance of the `the method exists ≠ the method can be called` class this +audit family keeps finding, after `analytics.explain` / `analytics.meta` +(#3584), `meta.getView` (#3611) and `i18n.getTranslations` / `getFieldLabels` +(#3636) — and the first one only a cross-repo guard could see. The framework +capstone (#3642) exempts the `/api/v1/cloud/` prefix wholesale, because this +repo does not serve those routes; `cloud`'s control-plane ledger (#3655) is +where the mounted set and the SDK are both in scope, and it pins the absence. + +Callers who somehow depended on it were already receiving a 404; read starter +packages through the `sys_package` view (`is_starter = true`) instead. diff --git a/docs/audits/2026-07-dispatcher-client-route-coverage.md b/docs/audits/2026-07-dispatcher-client-route-coverage.md index f6bfa7e36f..47106ace9b 100644 --- a/docs/audits/2026-07-dispatcher-client-route-coverage.md +++ b/docs/audits/2026-07-dispatcher-client-route-coverage.md @@ -320,8 +320,8 @@ tranche 3 — 90 routes, each with a reviewed disposition, enumerated for real b driving all 17 registrars against a capturing mock `IHttpServer`. The client half is the part that actually closes the boundary. Without it a -route could be renamed there, the ledger updated to match, and all 23 methods -404 with the server-side guard still green — so +route could be renamed there, the ledger updated to match, and every +`projects.*` method 404 with the server-side guard still green — so `projects-namespace-coverage.test.ts` drives the real SDK with a recording `fetch` and matches each URL against the ledger. Mutation-checked against exactly that scenario. @@ -335,11 +335,13 @@ exactly that scenario. Two findings, both pinned by tests rather than prose: -- **`projects.listTemplates` is dead** — it builds `/api/v1/cloud/templates`, +- **`projects.listTemplates` was dead** — it built `/api/v1/cloud/templates`, which the string search finds exactly once in each repo: the call itself. - Templates exist as a filtered `sys_package` view, never as a route (#3702). - The sixth instance of the `#3584 / #3611 / #3636` class, and the first one - only a cross-repo guard could have seen. + Templates exist as a filtered `sys_package` view, never as a route. The + sixth instance of the `#3584 / #3611 / #3636` class, and the first one only + a cross-repo guard could have seen. **Removed in #3702** — there was no + route to reconcile the method against, and no caller in either repo; it + returns when a route exists to back it, with an `sdk` ledger row proving so. - **A duplicate route registration** — `POST /actions/sys_environment/:actionName` mounted twice with an identical path, the second commented "legacy alias" and aliasing nothing (cloud#887). diff --git a/packages/client/src/client-url-conformance.test.ts b/packages/client/src/client-url-conformance.test.ts index 1fec2244e2..274f8cc9a2 100644 --- a/packages/client/src/client-url-conformance.test.ts +++ b/packages/client/src/client-url-conformance.test.ts @@ -139,7 +139,10 @@ function matches(verb: string, path: string): Pattern | undefined { * it is a statement about where the coverage lives, not that there is none. * * That guard immediately found `projects.listTemplates` building - * `/api/v1/cloud/templates`, which no registrar in either repo mounts (#3702). + * `/api/v1/cloud/templates`, which no registrar in either repo mounts — the + * sixth instance of the class above, and the first one only a cross-repo guard + * could see. The method has since been deleted (#3702): the route was never + * mounted anywhere, so there was nothing to reconcile it against. * * Exempt by PREFIX, and bounded from both ends: the assertions below pin which * methods are allowed to use it, so the hole cannot quietly widen into a place diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index f25c0e17fd..e50aa8203d 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1274,14 +1274,13 @@ export class ObjectStackClient { return this.unwrapResponse<{ drivers: Array<{ name: string; driverId: string }>; total: number }>(res); }, - /** - * List available project templates. Templates are seeded into the project - * database once at provisioning time when `template_id` is supplied. - */ - listTemplates: async () => { - const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/templates`); - return this.unwrapResponse<{ templates: Array<{ id: string; label: string; description: string; category?: string }>; total: number }>(res); - }, + // The former `listTemplates` was removed in #3702 — it built + // `GET /api/v1/cloud/templates`, which nothing mounts in this repo or in + // `cloud` (the string occurred exactly once in each: at the call itself), + // so every invocation was a 404. Templates are a DATA concept — the + // `sys_package_templates` view over `sys_package` (`is_starter = true`) — + // never a route; `cloud`'s ledger pins that absence deliberately. It comes + // back when a route exists to back it, with an `sdk` ledger row proving so. /** * Per-project package installation management (Power Apps "solution" model).