|
| 1 | +--- |
| 2 | +"@objectstack/rest": minor |
| 3 | +--- |
| 4 | + |
| 5 | +`GET /api/v1/packages`, `GET /api/v1/packages/:id` and `DELETE /api/v1/packages/:id` have one implementation: the runtime dispatcher's `/packages` domain. `@objectstack/rest`'s `registerPackageRoutes` no longer mounts its own copies of those three routes; it mounts `POST /api/v1/packages/publish` and nothing else. |
| 6 | + |
| 7 | +The two copies had already diverged, and a comment in the REST registrar claimed its copies shadowed the dispatcher's while on a stock boot they were never mounted at all (the registrar decided at registration time, before the `package` service had registered). One URL, one body, ruled on #14503. |
| 8 | + |
| 9 | +What changes on the wire, for a deployment whose composition really did reach the REST copies: |
| 10 | + |
| 11 | +- `GET /packages/:id` answers `{ success: true, data: <row> }` — the installed-package row directly under `data`. FROM `data.package` TO `data`. There is no `{ package }` wrapper. |
| 12 | +- The rows on `GET /packages` and the row on `GET /packages/:id` carry no `source: 'registry' | 'database' | 'both'` key. **Deliberately removed**, not ported: it had no reader outside the REST registrar's own tests — none in this repo's production code, the Console, the docs or the OpenAPI document, and the SDK declined to declare it twice on purpose. |
| 13 | +- `GET /packages` and `GET /packages/:id` read the **installed** packages from the in-memory registry (`registry.getAllPackages()` / `registry.getPackage(id)`) and nothing else. The REST copies merged the durable `sys_packages` rows (`PackageService.list()` / `.get(id, version)`) into the registry set, so a package **published but not installed** was listed there and gettable there; on the surviving door it is neither. **Deliberately removed** with the routes, not silently dropped: the published-artifact store keeps its own surface (`POST /packages/publish` here, the marketplace browse elsewhere), and the family this door serves is the installed set. |
| 14 | +- `?version=` is not read on `GET /packages/:id` or `DELETE /packages/:id`, so its repeated-parameter refusal (`400 VALIDATION_ERROR` on `?version=a&version=b`) is gone with it. **Deliberately removed**: the single implementation reads the installed package from the registry, and a version-scoped durable lookup was a behaviour only the REST copy had. The one in-tree sender is the SDK's `ScopedEnvironmentClient.packages.get(id, version?)`, whose binding is tracked on #12034. |
| 15 | +- A missing package answers `404 RESOURCE_NOT_FOUND` with the message `Package '<id>' not found` (the dispatcher's spelling) instead of `Package "<id>" was not found.`. |
| 16 | +- `DELETE /packages/:id` uninstalls the package (registry plus persisted metadata rows, `?keepData=true` to keep the object tables); the REST copy's version-scoped delete of a published artifact is gone. |
| 17 | +- **The uninstall's tenancy width narrows.** The REST copy called `protocol.deletePackage({ packageId, allTenants: true })` — a package-wide uninstall across every tenant, the width #7705 case 4 pinned on purpose because that registrar had no organization to resolve. The surviving door calls `protocol.deletePackage({ packageId, organizationId?, keepData? })` with the organization it resolves for the caller (`resolveActiveOrganizationId`), so a `DELETE /packages/:id` that used to reach the REST copy now removes the package's metadata for the caller's active organization, not for all tenants. **Deliberately narrowed**, not silently dropped: one door, one width, and it is the width the dispatcher has always answered on every stock boot. |
| 18 | +- **Capability refusals answer a different `error.code`.** On all three routes a caller holding neither `manage_metadata` (write) nor `studio.access` / `setup.access` (read) is refused with `403 PERMISSION_DENIED`. FROM `403 FORBIDDEN` TO `403 PERMISSION_DENIED`: the removed REST copies emitted `sendError(res, 403, 'FORBIDDEN', …)` explicitly, while the dispatcher's `requireManageMetadata` / `requireReadCapability` (`packages/runtime/src/domains/packages.ts`) call `deps.error(message, 403)` with no code and `packages/runtime/src/error-envelope.ts` derives one from the status — `standardErrorCodeForHttpStatus(403)` = `PERMISSION_DENIED`. **Same status, same message**: the two cohort messages ("Managing packages requires the `manage_metadata` capability." and "Reading packages requires the `studio.access` or `setup.access` capability.") are identical on both doors. Both codes are ADR-0112 standard members, so the envelope shape is unchanged; what moves is that a client branching on `err.code === 'FORBIDDEN'` for a package read or delete refusal stops matching on any composition that really did reach the REST copies. |
| 19 | + |
| 20 | +`POST /api/v1/packages/publish` is unchanged. |
| 21 | + |
| 22 | +Spec conformance on the surviving door is claimed for `GET /packages/:id` **only**: its `{ success, data: <row>, meta }` is exactly `GetInstalledPackageResponseSchema` (`packages/spec/src/api/package-api.zod.ts`, `data: InstalledPackageSchema` bare). The other two routes do **not** match their declarations, and the REST copies did not either — this drift is **pre-existing, not introduced by this release**, and is carded on #16781: |
| 23 | + |
| 24 | +- `GET /packages` answers `{ packages, total }`, while `ListInstalledPackagesResponseSchema` requires `hasMore` (and declares `enabled` / `limit` / `cursor` inputs the door does not read). |
| 25 | +- `DELETE /packages/:id` answers `{ success, registryRemoved, persisted }`, while `UninstallPackageApiResponseSchema` requires `packageId`. |
| 26 | + |
| 27 | +Nothing in this release changes either shape; with one implementation there is now exactly one thing to reconcile, and #16781 carries that reconciliation together with the `responseSchema` pins the runtime ledger rows for `packages.list` / `packages.uninstall` still lack. |
| 28 | + |
| 29 | +`GET /discovery` on the REST server now advertises `routes.packages` on every boot — the family base under which its publish route is mounted — instead of only when its own copy of the list route had been mounted at start. On a stock `objectstack serve` boot that copy never was (the `package` service registers after the REST plugin starts), so discovery omitted `routes.packages` while the dispatcher served the family; the SDK's convention fallback covered it. |
| 30 | + |
| 31 | +The three removed REST rows are gone from `REST_ROUTE_LEDGER`; the runtime route ledger carries the surviving routes. |
| 32 | + |
| 33 | +The environment-scoped mount (`/environments/:environmentId/packages…`) is served by the same dispatcher domain **only where the `@objectstack/hono` catch-all is mounted** (`createHonoApp`): the catch-all strips the environment prefix and hands the request to the domain. The dispatcher plugin's own explicit mounts (`plugin-hono-server`) register `/packages*` at the **unscoped** prefix only, and that plugin's sole route into the dispatcher (`setFallbackHandler`) serves declarative `apis:` endpoints, not domains. So a host composed as `plugin-hono-server` + the REST plugin with `enableProjectScoping: true` + the dispatcher plugin, **without** `createHonoApp`, had exactly one door for scoped package reads and deletes — the REST mirror this release removes — and after it has none: the scoped `GET /environments/:id/packages`, `GET /environments/:id/packages/:id` and `DELETE /environments/:id/packages/:id` answer the transport's plain 404 there. That composition is reachable from the open-core CLI when the standalone boot is skipped (`shouldBootWithLibrary()` false — any host config, or `OS_MODE=off`) and `api.enableProjectScoping` is forwarded verbatim. Every consumer population reachable from this repo is zero for the scoped mount (no in-repo production caller of `ScopedEnvironmentClient.packages.*`, no Console call to a scoped `/packages` URL); it is stated here so it is a known gap rather than a silent one. On a `plugin-hono-server` composition with `enableProjectScoping` and no `createHonoApp`, the scoped `/api/v1/environments/:id/packages[/:id]` routes have no door until #16781 lands (ruled C′ on #14503). |
0 commit comments