From 3128642fd60833d130844364f1b5e6c872efafb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 09:18:15 +0000 Subject: [PATCH] fix(metadata-protocol): parse `manifest.id` at the protocol install primitive (#19417) `ObjectStackProtocolImplementation.installPackage` spread the request into `any` and handed it to `SchemaRegistry.installPackage` with a second `as any`, so an id `MANIFEST_ID_PATTERN` refuses installed and persisted while `defineStack()`, `os build`, `os validate` and the publish face all refused the same id. #19473 closed the HTTP door, which is one CALLER of this primitive; `duplicatePackage` is a second and an embedder is a third. The gate asks the declaration by reference (`ManifestSchema.shape.id`) and surfaces its own sentence (`manifestIdRefusal`) rather than rewording it, ahead of every write and every derivation. `duplicatePackage` parses its target id at the top of the method, because its manifest write sits inside a best-effort `catch {}` that would otherwise swallow the refusal and report success. Both namespace derivations on the duplicate path move from a raw `id.split('.').pop()` to the spec helper `deriveNamespaceFromPackageId`, the one `installPackage` already used: the target namespace is spliced into every copied object name, and the Studio's default `-copy` derived `leave-copy`, minting names the object declaration refuses. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf --- ...ol-install-primitive-parses-manifest-id.md | 90 +++++++ .../src/protocol.install-manifest-id.test.ts | 244 ++++++++++++++++++ packages/metadata-protocol/src/protocol.ts | 134 +++++++++- 3 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 .changeset/19417-protocol-install-primitive-parses-manifest-id.md create mode 100644 packages/metadata-protocol/src/protocol.install-manifest-id.test.ts diff --git a/.changeset/19417-protocol-install-primitive-parses-manifest-id.md b/.changeset/19417-protocol-install-primitive-parses-manifest-id.md new file mode 100644 index 00000000000..26f4f5c6726 --- /dev/null +++ b/.changeset/19417-protocol-install-primitive-parses-manifest-id.md @@ -0,0 +1,90 @@ +--- +'@objectstack/metadata-protocol': minor +--- + +fix(metadata-protocol): the protocol install primitive parses the manifest's `id` leg, and the duplicate door parses its target id (#19417) + +Clause-②: no (narrowing) + +**BREAKING for callers of the protocol install and duplicate doors** — +`ObjectStackProtocolImplementation.installPackage` and `duplicatePackage` now +refuse a package id that is not reverse-domain notation, throwing a `400`-tagged +error carrying the declaration's own sentence. Both used to install and report +success. + +The accept set only shrinks back to what the published declaration has always +said. `MANIFEST_ID_PATTERN` is declared once in +`packages/spec/src/kernel/manifest.zod.ts` and referenced by both faces of one +identity — `ManifestSchema.id`, what an author writes, and +`PackageSchema.manifestId`, what the registry stores and publishes by. +`installPackage` parsed nothing at all: it spread the request into `any` and +handed it to `SchemaRegistry.installPackage` with a second `as any`, so +`id: 'pkg-a'` — or `com.example.my_erp` — installed and PERSISTED while +`defineStack()`, `os build`, `os validate` and the publish face all refused the +same id. That is «declared ≠ enforced» on a published contract, and nothing in +`packages/spec` moves for it: the declaration was already right. + +**Why the primitive and not only a door.** #19473 landed the same parse at the +HTTP door (`POST /api/v1/packages`). That door is ONE caller of this primitive — +it routes through `protocol.installPackage` whenever the protocol service +resolves. `duplicatePackage` is a second, and an embedder holding the protocol +object is a third. A gate on one door buys that door; this one is on the method +every caller passes through. + +The gate asks the declaration **by reference** — `ManifestSchema.shape.id` — +rather than keeping a copy of the grammar, so a future move of the +reverse-domain rule reaches this seam with no further edit. The sentence the +caller reads is the declaration's own (`manifestIdRefusal`), **surfaced rather +than reworded**: it names the key, echoes the value, lists the two examples and +carries a suggestion arm that verifies its candidate against the pattern before +offering it. Installing `id: 'com.example.my_erp'` now throws, with: + +```text +Invalid package id 'com.example.my_erp' on `manifest.id`. Expected +reverse-domain notation ('com.steedos.crm', 'org.apache.superset') — lowercase +dot-separated segments; hyphens allowed inside a segment, underscores are not. +Did you mean 'com.example.my-erp'? +``` + +**The duplicate door refuses BEFORE it mints anything.** `duplicatePackage` +builds its target manifest and writes it through `installPackage` inside a +deliberately best-effort `catch {}` — a refusal raised only there would be +swallowed and the caller would read `success: true` on a package with no +manifest row. So the target id is parsed at the top of the method, ahead of the +row scan and ahead of the copy loop, and the refusal names the key the caller +actually wrote (`targetPackageId`). + +**One assumption, one implementation.** The duplicate door derived both +namespaces with a raw `id.split('.').pop()` while `installPackage` derived the +same default with the spec helper `deriveNamespaceFromPackageId`, which +sanitises to the namespace charset, truncates to 20 and answers `null` when +nothing valid comes out. That mattered: the target namespace is spliced into +every copied object name as `${namespace}_${short}`, and an object name is +`/^[a-z_][a-z0-9_]*$/`. The Studio's own default duplicate id — +`-copy` — therefore minted `leave-copy_ticket`, a name the object +declaration refuses. Both sides now use the helper, so a duplicate of +`com.example.leave` into `com.example.leave-copy` is namespaced `leave_copy`. +An explicitly declared `targetNamespace` still wins untouched; when neither an +explicit nor a derivable namespace exists the door refuses loudly, naming +`targetNamespace` as the remedy, instead of renaming rows with an empty prefix. + +**What is not affected.** Boot-time and in-process installs that reach +`SchemaRegistry.installPackage` / `ObjectQL.registerApp` directly never pass +through this primitive, so nothing about how a package is loaded from disk or +registered by a plugin changes. A conforming manifest installs exactly as +before, versionless and namespace-less manifests included — the version default +and the namespace default still run, now behind the id gate rather than ahead of +it. + +**Scope — the `id` leg alone.** `InstallPackageRequestSchema` / `ManifestSchema` +are still not parsed whole here. The residual classes the HTTP door's own +docblock records are untouched by this change and are each their own narrowing +of a published contract. + +**If you are refused.** Give the package an id in reverse-domain notation — +lowercase dot-separated segments, hyphens allowed inside a segment, underscores +not. The refusal names the key, echoes what you wrote and, where a mechanical +repair exists, offers one it has already checked against the rule, so the +prescription arrives with the failure rather than in a changelog. + + diff --git a/packages/metadata-protocol/src/protocol.install-manifest-id.test.ts b/packages/metadata-protocol/src/protocol.install-manifest-id.test.ts new file mode 100644 index 00000000000..e9672690124 --- /dev/null +++ b/packages/metadata-protocol/src/protocol.install-manifest-id.test.ts @@ -0,0 +1,244 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#19417] The protocol install primitive parses `manifest.id` through the + * declaration that owns it. + * + * --------------------------------------------------------------------------- + * The gap these pins close + * --------------------------------------------------------------------------- + * `MANIFEST_ID_PATTERN` (`packages/spec/src/kernel/manifest.zod.ts`) is the + * reverse-domain rule declared ONCE and referenced by both faces of the package + * identity — `ManifestSchema.id` (what an author writes) and + * `PackageSchema.manifestId` (what the registry stores and publishes by). + * `ObjectStackProtocolImplementation.installPackage` never parsed it: the + * request was spread into `any` and handed to the registry with a second + * `as any`, so `pkg-a` and `com.example.my_erp` installed and persisted while + * `defineStack()`, `os build`, `os validate` and the publish face all refused + * the same id. + * + * #19473 landed the same parse at the HTTP door (`POST /packages`, + * `packages/runtime/src/domains/packages.ts`). That door is ONE caller of this + * primitive; `duplicatePackage` is a second and an embedder holding the + * protocol object is a third — which is why the gate belongs here. + * + * --------------------------------------------------------------------------- + * Both directions are pinned, deliberately + * --------------------------------------------------------------------------- + * A refusal pin alone cannot tell "the rule is enforced" from "this door stopped + * installing anything": every refusal case is answered by a lit control on a + * conforming id that still installs, and by the assertion that the registry was + * never reached on the refused ones. + * + * The refusal text is compared against `manifestIdRefusal` itself rather than + * retyped here: the sentence is the declaration's, SURFACED, and a pin that + * restated it would go green on a reworded fourth sentence for one rule. + */ +import { describe, it, expect, vi } from 'vitest'; +import { manifestIdRefusal } from '@objectstack/spec/kernel'; +import { ObjectStackProtocolImplementation } from './protocol.js'; + +/** Ids `MANIFEST_ID_PATTERN` refuses, one per reason it refuses them. */ +const REFUSED = [ + ['a bare word — no reverse-domain prefix at all', 'pkg-a'], + ['an underscore inside a segment', 'com.example.my_erp'], + ['the empty string', ''], + ['a segment opening with a digit', 'com.4example.crm'], +] as const; + +/** Ids the declaration admits — the lit controls. */ +const ADMITTED = ['com.example.crm', 'com.example.my-erp', 'org.apache.superset'] as const; + +function makeImpl() { + const registryCalls: Array<{ manifest: any }> = []; + const engine = { + registry: { + installPackage: (manifest: any) => { + registryCalls.push({ manifest }); + return { manifest, status: 'installed', enabled: true }; + }, + }, + find: async () => [], + }; + const publish = vi.fn(async () => ({ success: true })); + const services = new Map([['package', { publish }]]); + const impl = new ObjectStackProtocolImplementation(engine as never, () => services); + return { impl, registryCalls, publish }; +} + +/** The thrown refusal, or a failure naming what happened instead. */ +async function refusalOf(run: () => Promise): Promise { + try { + await run(); + } catch (e) { + return e; + } + throw new Error('expected the call to be refused, but it resolved'); +} + +describe('[#19417] installPackage parses `manifest.id` through its declaration', () => { + for (const [why, id] of REFUSED) { + it(`refuses ${why} (${JSON.stringify(id)}) before any write`, async () => { + const { impl, registryCalls, publish } = makeImpl(); + const err = await refusalOf(() => (impl as any).installPackage({ + manifest: { id, name: 'X', version: '1.0.0' }, + })); + // The envelope: an HTTP boundary answers 400, not the 500 an + // unannotated throw earns (`resolveThrownHttpError`). + expect(err.statusCode).toBe(400); + // The sentence is the DECLARATION's, surfaced — not this door's. + expect(err.message).toBe(manifestIdRefusal('manifest.id', id)); + // Neither writer ran: not the in-memory registry, not the durable row. + expect(registryCalls).toHaveLength(0); + expect(publish).not.toHaveBeenCalled(); + }); + } + + it('refuses a manifest carrying no `id` at all, and still never writes', async () => { + const { impl, registryCalls, publish } = makeImpl(); + const err = await refusalOf(() => (impl as any).installPackage({ + manifest: { name: 'X', version: '1.0.0' }, + })); + expect(err.statusCode).toBe(400); + expect(registryCalls).toHaveLength(0); + expect(publish).not.toHaveBeenCalled(); + }); + + it('carries the declaration\'s mechanical repair, not just its rule', async () => { + const { impl } = makeImpl(); + const err = await refusalOf(() => (impl as any).installPackage({ + manifest: { id: 'com.example.my_erp', version: '1.0.0' }, + })); + // The repair arm is the whole difference between a rule restated and a + // fix; `manifestIdRefusal` verifies its candidate before offering it. + expect(err.message).toContain('com.example.my-erp'); + }); + + for (const id of ADMITTED) { + it(`lit control — '${id}' still installs`, async () => { + const { impl, registryCalls } = makeImpl(); + const res: any = await (impl as any).installPackage({ + manifest: { id, name: 'X', version: '1.0.0' }, + }); + expect(registryCalls).toHaveLength(1); + expect(registryCalls[0].manifest.id).toBe(id); + expect(res.package.status).toBe('installed'); + }); + } + + it('the raw value is parsed — a padded id is refused, not laundered by a trim', async () => { + const { impl, registryCalls } = makeImpl(); + const err = await refusalOf(() => (impl as any).installPackage({ + manifest: { id: ' com.example.crm ', version: '1.0.0' }, + })); + expect(err.statusCode).toBe(400); + expect(registryCalls).toHaveLength(0); + }); +}); + +/** A duplicate-door harness: one source package holding one object row. */ +function makeDuplicateImpl(sourceId = 'com.example.leave') { + const rows = [{ + id: 'r_1', + type: 'object', + name: 'leave_ticket', + organization_id: null, + package_id: sourceId, + state: 'active', + metadata: JSON.stringify({ name: 'leave_ticket', label: 'Ticket' }), + }]; + const installed: any[] = []; + const engine: any = { + find: vi.fn(async () => rows), + registry: { + getPackage: vi.fn(() => ({ + manifest: { id: sourceId, name: 'Leave', namespace: 'leave', version: '1.0.0' }, + })), + installPackage: vi.fn((manifest: any) => { + installed.push(manifest); + return { manifest, status: 'installed', enabled: true }; + }), + }, + }; + const impl = new ObjectStackProtocolImplementation(engine as never, () => new Map()); + const saveMetaItem = vi.spyOn(impl, 'saveMetaItem' as never); + (saveMetaItem as any).mockResolvedValue({ success: true } as never); + return { impl, engine, installed, saveMetaItem }; +} + +describe('[#19417] duplicatePackage refuses a target id the declaration refuses', () => { + for (const [why, id] of REFUSED) { + it(`refuses ${why} (${JSON.stringify(id)}) before anything is minted`, async () => { + const { impl, engine, installed, saveMetaItem } = makeDuplicateImpl(); + const err = await refusalOf(() => (impl as any).duplicatePackage({ + sourcePackageId: 'com.example.leave', + targetPackageId: id, + })); + expect(err.statusCode).toBe(400); + // The key named is the one the caller actually wrote. + expect(err.message).toBe(manifestIdRefusal('targetPackageId', id)); + // ⭐ Nothing was minted and nothing was scanned: the refusal + // precedes the manifest write AND the copy loop, so no empty shell + // is left behind. The manifest write below sits inside a + // best-effort `catch {}` — a refusal raised only there would be + // swallowed and reported as `success: true`. + expect(installed).toHaveLength(0); + expect(engine.registry.installPackage).not.toHaveBeenCalled(); + expect(engine.find).not.toHaveBeenCalled(); + expect(saveMetaItem).not.toHaveBeenCalled(); + }); + } + + it('lit control — a conforming target still duplicates, rows and all', async () => { + const { impl, installed, saveMetaItem } = makeDuplicateImpl(); + const res: any = await (impl as any).duplicatePackage({ + sourcePackageId: 'com.example.leave', + targetPackageId: 'com.example.leave-copy', + }); + expect(res.success).toBe(true); + expect(res.copiedCount).toBe(1); + expect(installed).toHaveLength(1); + expect(installed[0].id).toBe('com.example.leave-copy'); + expect(saveMetaItem).toHaveBeenCalledTimes(1); + }); +}); + +describe('[#19417] duplicatePackage derives its namespace with the spec helper', () => { + it('the Studio default `-copy` yields a LEGAL object-name prefix', async () => { + const { impl, installed, saveMetaItem } = makeDuplicateImpl(); + await (impl as any).duplicatePackage({ + sourcePackageId: 'com.example.leave', + targetPackageId: 'com.example.leave-copy', + }); + // `deriveNamespaceFromPackageId` sanitises the hyphen; the raw + // `split('.').pop()` this replaced answered 'leave-copy', and an object + // name is /^[a-z_][a-z0-9_]*$/ — so the copy used to be minted under + // names the object declaration refuses. + expect(installed[0].namespace).toBe('leave_copy'); + const written = (saveMetaItem as any).mock.calls.map((c: any[]) => c[0].name); + expect(written).toEqual(['leave_copy_ticket']); + }); + + it('an explicit `targetNamespace` still wins', async () => { + const { impl, installed } = makeDuplicateImpl(); + await (impl as any).duplicatePackage({ + sourcePackageId: 'com.example.leave', + targetPackageId: 'com.example.leave-copy', + targetNamespace: 'leave2', + }); + expect(installed[0].namespace).toBe('leave2'); + }); + + it('refuses loudly when no namespace can be derived, naming the remedy', async () => { + const { impl, installed } = makeDuplicateImpl(); + const err = await refusalOf(() => (impl as any).duplicatePackage({ + sourcePackageId: 'com.example.leave', + // Admitted by the id pattern, but a single-letter final segment + // cannot carry the namespace charset's 2–20 char rule. + targetPackageId: 'com.example.a', + })); + expect(err.statusCode).toBe(400); + expect(err.message).toContain('targetNamespace'); + expect(installed).toHaveLength(0); + }); +}); diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index aaa92be21a6..be9dd23ad9c 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -137,6 +137,9 @@ import { type MetadataProvenance, } from '@objectstack/spec/kernel'; import { validateObjectNamespacePrefix, deriveNamespaceFromPackageId } from '@objectstack/spec/kernel'; +// [#19417] The package-id declaration and its remedy text, imported to be +// SURFACED rather than restated — see the gate at the top of `installPackage`. +import { ManifestSchema, manifestIdRefusal } from '@objectstack/spec/kernel'; import { stripReadDecorations } from '@objectstack/spec/kernel'; import { REFERENCE_SITES } from './reference-sites.js'; // [#5488] The `@objectstack/spec/api` import that stood here — `ApiEndpointSchema`, @@ -19267,12 +19270,79 @@ export class ObjectStackProtocolImplementation implements copied: Array<{ type: string; name: string }>; failed: Array<{ type: string; name: string; error: string }>; }> { + // [#19417] ⭐ THE TARGET ID IS PARSED BEFORE ANYTHING IS MINTED — same + // declaration, same surfaced sentence, one key over. + // + // This door builds `dupManifest` with `id: request.targetPackageId` and + // writes it through {@link installPackage}, so the gate there already + // covers the WRITE. It does not cover this method, for two reasons that + // are both about where the refusal lands: + // + // ① the `installPackage` call below sits inside a `catch {}` that is + // deliberately best-effort (a manifest row is not worth aborting a + // copy for), so a refusal raised THERE would be swallowed here and + // the caller would read `success: true` on a package with no + // manifest row — a silent partial state, strictly worse than the + // 201 this card set out to close; + // ② the refusal has to precede the MUTATIONS, and the manifest write + // is not the first of them. #14451 already established the position + // for this door's other precondition: `duplicatePackage` mints the + // target package record ahead of its copy loop, so a refusal any + // later leaves the empty shell behind. + // + // The key named is `targetPackageId`, because that is the authoring + // path the caller actually wrote — `manifestIdRefusal` takes the key so + // one declaration can name itself correctly at every door. + const declaredTargetId = ManifestSchema.shape.id.safeParse(request.targetPackageId); + if (!declaredTargetId.success) { + throw Object.assign( + new Error(manifestIdRefusal('targetPackageId', request.targetPackageId)), + { statusCode: 400 }, + ); + } + const registry: any = (this.engine as any).registry; const srcPkg = registry?.getPackage?.(request.sourcePackageId); + // [#19417] ⭐ ONE ASSUMPTION, ONE IMPLEMENTATION. Both namespaces were + // derived here by a RAW `id.split('.').pop()`, while + // {@link installPackage} derives the very same default with the spec + // helper `deriveNamespaceFromPackageId` — which sanitises to the + // namespace charset, truncates to 20 and answers `null` when nothing + // valid comes out. The raw copy did none of that, and `targetNs` is not + // a label: it is spliced into every copied OBJECT name as + // `${targetNs}_${short}`, and an object name is + // `/^[a-z_][a-z0-9_]*$/` (`packages/spec/src/data/object.zod.ts`). + // + // So the Studio's own default duplicate id — `-copy`, the + // value both objectui duplicate dialogs prefill — derived the namespace + // `leave-copy` and minted object names `leave-copy_ticket`, which the + // object declaration refuses. The helper answers `leave_copy`. The + // source side is the same rule read backwards: the prefix these rows + // actually carry is the one `installPackage` stamped, i.e. the helper's, + // so matching them with the raw split found nothing and the copy landed + // under the SOURCE's names — the collision the re-namespacing exists to + // prevent. + // + // ⛔ An explicitly declared `targetNamespace` still wins untouched: it + // is the caller's decision and this seam is about the DEFAULT. const sourceNs: string = - (srcPkg?.manifest?.namespace as string) ?? (request.sourcePackageId.split('.').pop() ?? ''); - const targetNs: string = - request.targetNamespace ?? (request.targetPackageId.split('.').pop() ?? request.targetPackageId); + (srcPkg?.manifest?.namespace as string) ?? (deriveNamespaceFromPackageId(request.sourcePackageId) ?? ''); + const targetNs: string | null = + request.targetNamespace ?? deriveNamespaceFromPackageId(request.targetPackageId); + if (!targetNs) { + // Reachable only for an id the pattern admits but the namespace + // charset cannot carry (a single-letter final segment), or for an + // explicit `targetNamespace: ''`. Loud, with the remedy — never a + // copy renamed with an empty prefix. + throw Object.assign( + new Error( + `Cannot derive a package namespace from '${request.targetPackageId}'. ` + + 'Pass `targetNamespace` explicitly — a lowercase letter followed by ' + + '1–19 letters, digits or underscores.', + ), + { statusCode: 400 }, + ); + } const where: Record = { package_id: request.sourcePackageId, state: 'active' }; // [#7819 tier 2] Copy the source's env-wide (`organization_id IS NULL`) @@ -22511,6 +22581,64 @@ export class ObjectStackProtocolImplementation implements * flag arms below. */ async installPackage(request: InstallPackageRequest): Promise { + // [#19417] ⭐ THE PRIMITIVE PARSES THE `id` LEG — the declaration, by + // reference, ahead of every write and every derivation below. + // + // `MANIFEST_ID_PATTERN` (`packages/spec/src/kernel/manifest.zod.ts`) is + // the reverse-domain rule declared ONCE and referenced by BOTH faces of + // this identity — `ManifestSchema.id`, what an author writes, and + // `PackageSchema.manifestId`, what the registry stores and publishes by. + // This primitive read `request.manifest` POSITIONALLY: it spread the + // request into `any` and handed it to + // `this.engine.registry.installPackage` with a second `as any`, so + // `id: 'pkg-a'` — or `com.example.my_erp` — installed and PERSISTED, + // while `defineStack()`, `os build`, `os validate` and the publish face + // all refuse the same id. That is «declared ≠ enforced» on a PUBLISHED + // contract, the shape Prime Directive #10 refuses outright and + // 北极星 clause 4 names in as many words: + // 「错的必须被**响亮拒绝**并给处方,**永不静默落库**」. + // + // ⭐ THE GATE IS ON THE PRIMITIVE, NOT ON ONE DOOR. #19473 landed the + // same parse at `POST /packages` + // (`packages/runtime/src/domains/packages.ts`), which is ONE caller of + // this method — that door calls `protocol.installPackage` whenever the + // protocol service resolves, and falls back to the bare registry write + // when it does not. {@link duplicatePackage} below is a second caller, + // and an embedder holding this object is a third. Gating a door alone + // buys that door; the rule belongs where every caller passes. + // + // ⭐ THE SENTENCE IS THE DECLARATION'S, NOT THIS FILE'S — surfaced + // rather than reworded, exactly as #19473 did it: `manifestIdRefusal` + // names the key, echoes what the author wrote, lists the examples, and + // offers a mechanical repair only after VERIFYING that candidate + // against the pattern. A fourth sentence for one rule would drop the + // repair. + // + // The throw carries `statusCode` so an HTTP boundary answers `400` + // rather than the `500` an unannotated throw earns + // (`resolveThrownHttpError`, `@objectstack/types`) — the same status + // the HTTP door serves for this refusal, and the spelling this file + // already uses for its `404`. + // + // ⛔ THE RAW VALUE IS PARSED, before the spread and before the version + // and namespace defaults: an id the declaration refuses must never + // reach `deriveNamespaceFromPackageId` below, which already ASSUMES the + // reverse-domain shape it was never given a chance to enforce. + // + // ⛔ SCOPE — THE `id` LEG ALONE. `InstallPackageRequestSchema` / + // `ManifestSchema` are still not parsed whole here; the residual + // classes the HTTP door's own docblock records are untouched by this + // and are each their own narrowing of a published contract. + const rawId = (request.manifest as { id?: unknown } | undefined)?.id; + const declaredId = ManifestSchema.shape.id.safeParse(rawId); + if (!declaredId.success) { + const [issue] = declaredId.error.issues; + throw Object.assign( + new Error(issue?.message || manifestIdRefusal('manifest.id', rawId)), + { statusCode: 400 }, + ); + } + // #2532 — runtime-created base packages routinely arrive versionless // ({id, name} from the builder / Setup). `sys_packages.version` is NOT // NULL, and the old guard here (`pkgSvc?.publish && manifest.version`)