Skip to content

client.projects.listTemplates() targets /api/v1/cloud/templates, which nothing mounts #3702

Description

@os-zhuang

Found by the control-plane ledger built for #3655 — the first thing that has ever been able to see this, because it is the first guard that spans the framework↔cloud boundary.

The gap

listTemplates: async () => {
  const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/templates`);
  return this.unwrapResponse<{ templates: Array<{ id; label; description; category? }>; total: number }>(res);
},

/api/v1/cloud/templates is mounted by nothing:

  • cloud repo — all 17 registrars in cloud-artifact-api-plugin.ts were enumerated by driving them against a capturing mock IHttpServer. 91 registrations, 90 distinct. None is /cloud/templates.
  • framework — the string occurs exactly once in this repo: line 1282 of packages/client/src/index.ts, the call itself. http-dispatcher.ts has a handleCloud doc block listing the /cloud/* routes, left behind when those routes were relocated to cloud; it does not list templates either, and the method it documents no longer exists.

"Templates" are real as a data concept — sys_package_templates, a filtered sys_package view where is_starter = true, rendered as a console page — but there has never been an HTTP route that lists them.

So the method 404s. It is the sixth instance of the the method exists ≠ the method can be called class this audit family has turned up, after analytics.explain / analytics.meta (#3584), meta.getView (#3611), and i18n.getTranslations / getFieldLabels (#3636).

Why nothing caught it

The capstone (#3642) drives every SDK method and matches its URL against the union of the in-repo ledgers, but exempts the /api/v1/cloud/ prefix wholesale — the framework does not serve those routes and cannot vouch for them (#3655). listTemplates sat inside that exemption. The cloud-side ledger is what sees it, because it is the only place the mounted route set and the SDK are both in scope.

Options

  1. Trim the method. Nothing in either repo calls it, and the console reads starter packages through the sys_package view rather than an API. Smallest correct change; the blast radius is any external caller, who is 404-ing today regardless.
  2. Mount the route in cloud. Reasonable if templates are meant to be an API concept — but it needs a decision about what a "template" is on the wire. The client's declared shape ({ id, label, description, category? }) does not match sys_package's columns, so it is not a straight passthrough, and picking the mapping is a product/spec call, not an implementation detail.
  3. Leave it and document the method as unimplemented — the weakest option, since the type signature promises a resolved value.

Recommendation: option 1. Option 2 is defensible if starter templates are on the roadmap as an API, but shipping a route to satisfy a method nobody calls is the wrong order — add the method back with the route when there is a consumer.

Guard already in place

Whichever way this goes, the cloud-side ledger already pins it: UNMOUNTED_CLIENT_METHODS asserts the method still matches no mounted route, so mounting the route fails the test and forces an sdk ledger row; and the list is asserted to equal exactly ['projects.listTemplates'], so a second dead method cannot hide behind this one.

Refs: objectstack-ai/cloud (control-plane ledger, #3655), #3642 (the capstone whose exemption hid this), #3563 (the audit family).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions