Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/drop-dead-list-templates.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 8 additions & 6 deletions docs/audits/2026-07-dispatcher-client-route-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).
Expand Down
5 changes: 4 additions & 1 deletion packages/client/src/client-url-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 7 additions & 8 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading