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
17 changes: 17 additions & 0 deletions .changeset/29342-export-withheld-connector-maps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/runtime': patch
'@objectstack/cloud-connection': patch
'@objectstack/spec': patch
---

feat(runtime): a package export names each connector map it withheld, and both install modes of install-local name each one as incomplete

Clause-②: yes (widening)
Metadata-protocol: none

`GET /api/v1/packages/:id/export` writes served items, and the served read withholds four connector maps whole: `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers` and `providerConfig.transport.env`. An exported connector used to arrive with such a map simply absent, so an import could not tell a withheld map from one the package never had. Now:

- **The export lists each one.** The manifest's `withheldMaps` (declared on `PackageExportManifestSchema`) holds `{ type: 'connector', name, path }` for every map withheld. Each exported connector's stored body is judged by the same `connector` redactor the served read ran, read through the protocol's by-name execution face. Only the position leaves that read: no header or variable name, and no value. A package that withheld nothing carries no key. An export that cannot judge (a protocol without `getMetaItemForExecution`, or a connector that read answers no body for) fails rather than omitting the key, because from an exporter that writes it, an absent key means nothing was withheld.
- **Both install modes name each one.** `POST /api/v1/marketplace/install-local`, managed and unmanaged, accepts the package and answers `withheldMaps`, the list as the export wrote it, plus one `warn` line naming every map. The key is left out when the body carries no list, so that answer's key set is unchanged.
- **One narrowing, at the door.** A `withheldMaps` value the export's declaration refuses is now answered `PLUGIN_MANIFEST_INVALID` (400 for an inline manifest, 502 for a cloud snapshot) before any write, in either mode, the way an invalid `installModes` already is. Before this change the door ignored the key, whatever its value. No export wrote the key before this release, so only a hand-written list can be refused. Fix: send the list exactly as the export wrote it, or drop it.
- **Spec.** The `[PLANNED — not yet served]` marker is dropped from `withheldMaps`' describe, and the docblocks now say the key is served.
11 changes: 2 additions & 9 deletions content/docs/references/api/package-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ declared a VERSION rollback against the live COMMIT-rollback path;
`RollbackToPackageCommitResponseSchema` below is the true contract,
authored after that retirement per the ruling's sequencing (3A).

Two declarations are not transcriptions but DECLARED AHEAD OF THEIR
PRODUCER (ADR-0049), each describe opening `[PLANNED — not yet served]`
until the change that serves it drops the marker:
`PackageExportManifestSchema.withheldMaps` (#28640 ruling ③A, served by
#29342) and `PackageExportBundleSchema` (`GET /packages/export`, #28914).
Their own docblocks say what is not served yet.

<Callout type="info">
**Source:** `packages/spec/src/api/package-lifecycle.zod.ts`
</Callout>
Expand Down Expand Up @@ -335,7 +328,7 @@ Whole-deployment export, the answer of `GET /packages/export` (`os package expor
| **namespace** | `string` | optional | The namespace the package was installed with, as its registry row's manifest declares it; absent when the row declares none. An install of the export keeps it instead of deriving one from the id, so a `.custom` pair is reinstalled under its own namespace rather than `custom`. |
| **dependencies** | `Record<string, string>` | optional | The packages this package depends on, each id mapped to its version range, as its registry row's manifest declares them; absent when the row declares none. An install of the export orders it after each of them (ADR-0141 D5), and a `.custom` pair keeps the edge to its base that admits its overrides (ADR-0146 D2). |
| **label** | `string` | optional | Display label, when the package declares one. |
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | [PLANNED — not yet served] Declared ahead of its producer (ADR-0049): no export writes this list yet and no install door reads it yet. Every map the export withheld WHOLE because a served read never carries it: a connector's header and environment-variable maps. The item is written with that map absent, so once served an import reads this list to tell an incomplete map from one the package never had, and names each one for an operator to re-enter. Positions only: no header or variable name is listed. Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. |
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | Every map the export withheld WHOLE because a served read never carries it: a connector's header and environment-variable maps. The item is written with that map absent, so an import reads this list to tell an incomplete map from one the package never had, and names each one for an operator to re-enter. Positions only: no header or variable name is listed. Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. |


---
Expand All @@ -352,7 +345,7 @@ Whole-deployment export, the answer of `GET /packages/export` (`os package expor
| **namespace** | `string` | optional | The namespace the package was installed with, as its registry row's manifest declares it; absent when the row declares none. An install of the export keeps it instead of deriving one from the id, so a `.custom` pair is reinstalled under its own namespace rather than `custom`. |
| **dependencies** | `Record<string, string>` | optional | The packages this package depends on, each id mapped to its version range, as its registry row's manifest declares them; absent when the row declares none. An install of the export orders it after each of them (ADR-0141 D5), and a `.custom` pair keeps the edge to its base that admits its overrides (ADR-0146 D2). |
| **label** | `string` | optional | Display label, when the package declares one. |
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | [PLANNED — not yet served] Declared ahead of its producer (ADR-0049): no export writes this list yet and no install door reads it yet. Every map the export withheld WHOLE because a served read never carries it: a connector's header and environment-variable maps. The item is written with that map absent, so once served an import reads this list to tell an incomplete map from one the package never had, and names each one for an operator to re-enter. Positions only: no header or variable name is listed. Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. |
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | Every map the export withheld WHOLE because a served read never carries it: a connector's header and environment-variable maps. The item is written with that map absent, so an import reads this list to tell an incomplete map from one the package never had, and names each one for an operator to re-enter. Positions only: no header or variable name is listed. Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. |

### Nested Shape: `PackageExportManifest.withheldMaps[number]`

Expand Down
93 changes: 90 additions & 3 deletions packages/cloud-connection/src/marketplace-install-local-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
* none of which writes, and before any write.
* A mode the package's `installModes` does not permit answers
* `422 INSTALL_MODE_NOT_PERMITTED`, before any write.
* Either mode reads the body's `withheldMaps` — the connector header
* and environment maps a package export withheld whole (#28640
* ruling ③A) — accepts the package, and names each map in its
* answer's `withheldMaps` and in one `warn` line; a list the export's
* declaration refuses answers `PLUGIN_MANIFEST_INVALID` (400 inline,
* 502 cloud), before any write.
*
* [ADR-0126 §6 wall 3] A managed RE-install over an installed package
* asks the security plugin's access upgrade check
Expand Down Expand Up @@ -187,6 +193,9 @@ import {
unmanagedInstallValueCarries,
type InstallMode,
} from '@objectstack/spec/kernel';
// [#28640 ruling ③A] The export's list of the connector maps it withheld whole,
// read by both install modes through its one declaration.
import { PackageExportManifestSchema, type PackageExportManifest } from '@objectstack/spec/api';
// [#21762] ADR-0087 D1's protocol handshake, its brand predicate and the ONE
// answer both package-install doors give its refusal. Imported from the
// producer, never restated: `POST /api/v1/packages` answers through the same
Expand Down Expand Up @@ -602,6 +611,57 @@ function isPlainObject(value: unknown): value is Record<string, any> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

/** One entry of an export's `withheldMaps` ({@link PackageExportManifestSchema}). */
type WithheldMap = NonNullable<PackageExportManifest['withheldMaps']>[number];

/**
* [#28640 ruling ③A] The connector maps the export that produced this body
* withheld whole — its `withheldMaps`, parsed through the export's own
* declaration ({@link PackageExportManifestSchema}), by reference. Read by
* BOTH install modes before any write, so each answer names every map that
* arrives incomplete instead of letting it read as one the package never had.
*
* An absent list is `[]`: a body that is not an export, or an export that
* withheld nothing. A list the declaration refuses is refused here, the way
* an invalid `installModes` is ({@link judgeInstallMode}): it can only have
* been written by hand, and guessing which maps it meant is the silent answer
* the list exists to replace. The import never refuses a package FOR an
* incomplete map (the ruling: accept, and name each one).
*/
function judgeWithheldMaps(
manifest: Record<string, any>,
manifestId: string,
invalidManifestStatus: 400 | 502,
): { ok: true; withheldMaps: WithheldMap[] } | { ok: false; status: number; error: { code: string; message: string } } {
const declared = PackageExportManifestSchema.shape.withheldMaps.safeParse(manifest.withheldMaps);
if (!declared.success) {
const [issue] = declared.error.issues;
return {
ok: false,
status: invalidManifestStatus,
error: {
code: 'PLUGIN_MANIFEST_INVALID',
message: `Package '${manifestId}' carries an invalid \`withheldMaps\`: ${issue?.message ?? 'refused by its declaration'}`
+ `${issue && issue.path.length > 0 ? ` (at \`withheldMaps.${issue.path.join('.')}\`)` : ''}. `
+ 'Send the list as the package export wrote it, or omit it.',
},
};
}
return { ok: true, withheldMaps: declared.data ?? [] };
}

/**
* [#28640 ruling ③A] The operator's line for an install that received
* withheld maps: each one named, once, at `warn` — the connector is installed
* and runs, without the headers or environment variables the export held back.
*/
function withheldMapsLogLine(manifestId: string, mode: InstallMode, withheldMaps: readonly WithheldMap[]): string {
const named = withheldMaps.map((m) => `${m.type} '${m.name}' ${m.path}`).join(', ');
return `[MarketplaceInstallLocal] installed ${manifestId} ${mode} with ${withheldMaps.length} connector map(s) the `
+ `package export withheld whole, each now absent: ${named}. The connector sends none of those headers or `
+ 'environment variables until each map is supplied again where the connector is authored.';
}

/**
* [ADR-0131 D6] The package manifest an install body DECLARES — where each of
* the two single-package shapes this door accepts carries it: a compiled
Expand Down Expand Up @@ -1925,6 +1985,15 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
}, UNRUNNABLE_REFUSAL_STATUS);
}

// 1d'. [#28640 ruling ③A] THE MAPS THE EXPORT WITHHELD, read for both
// modes before any write: each answer names every connector map
// that arrives incomplete ({@link judgeWithheldMaps}). Accepted,
// never refused for, as the ruling has it; only a list the export's
// declaration refuses is.
const withheld = judgeWithheldMaps(manifest, manifestId, inlineManifest ? 400 : 502);
if (!withheld.ok) return c.json({ success: false, error: withheld.error }, withheld.status as any);
const { withheldMaps } = withheld;

// ── [ADR-0131 D6] The `unmanaged` install: a copy, not a registration.
// The package's metadata is copied ONCE into a new writable base in
// the environment ledger by the import verb, and the steps below —
Expand All @@ -1940,6 +2009,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
version,
versionId: resolvedVersionId,
userId,
withheldMaps,
});
}

Expand Down Expand Up @@ -2308,6 +2378,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
// The door is named only when it can store one of them: a listing that
// says none is writable (a walled posture) gets the other places.
const door = described?.writable === undefined || unsetRefs.some((r) => described.writable!.has(r));
if (withheldMaps.length > 0) ctx.logger?.warn?.(withheldMapsLogLine(manifestId, 'managed', withheldMaps));

return c.json({
success: true,
Expand All @@ -2333,6 +2404,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
// that declares none, so that answer's key set is unchanged.
...(datasources !== undefined ? { datasources } : {}),
...(unsetRefs.length > 0 ? { credentialNote: installedCredentialNote(unsetRefs, { door }) } : {}),
// [#28640 ruling ③A] Each connector map the package export
// withheld whole, so it arrived absent. Omitted when there are
// none, so that answer's key set is unchanged.
...(withheldMaps.length > 0 ? { withheldMaps } : {}),
upgradedFrom: conflict === 'marketplace' ? 'previous-marketplace-version' : null,
// [ADR-0146 D2, #28410] Names installed dependents already held,
// undeclared, that this version now ships (step 2d). Omitted when
Expand Down Expand Up @@ -3538,14 +3613,21 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
* either spelling, and the copy does not keep, with the package's
* reason — is the verb's own
* list, relayed as the verb states it, so the installer reads why the
* vendor locked an item before editing it.
* vendor locked an item before editing it;
* - `withheldMaps` — every connector map the package export withheld
* whole, read by {@link handleInstall} before the verb runs — is on the
* answer and on a `warn` line: the copy carries each map absent.
*/
private installUnmanaged = async (
c: any,
ctx: PluginContext,
args: { manifest: Record<string, any>; manifestId: string; version: string; versionId: string; userId: string | null },
args: {
manifest: Record<string, any>; manifestId: string; version: string; versionId: string; userId: string | null;
/** [#28640 ruling ③A] The connector maps the export withheld ({@link judgeWithheldMaps}). */
withheldMaps: readonly WithheldMap[];
},
): Promise<Response> => {
const { manifest, manifestId, version, versionId, userId } = args;
const { manifest, manifestId, version, versionId, userId, withheldMaps } = args;
let protocol: UnmanagedImporter | undefined;
try { protocol = ctx.getService<UnmanagedImporter>('protocol'); } catch { /* no protocol service */ }
if (!protocol || typeof protocol.importUnmanagedPackage !== 'function') {
Expand Down Expand Up @@ -3598,6 +3680,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
+ `${installedFrom.packageId}${installedFrom.version ? `@${installedFrom.version}` : ''}`
+ `${userId ? ` by ${userId}` : ''}. Nothing is registered under '${manifestId}'; the copy follows no upgrade.`,
);
if (withheldMaps.length > 0) ctx.logger?.warn?.(withheldMapsLogLine(manifestId, 'unmanaged', withheldMaps));
const declaresSeeds = unmanagedInstallValueCarries(packageBody.data);
return c.json({
success: true,
Expand All @@ -3613,6 +3696,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
copiedCount: result.copiedCount,
copied: result.copied,
strippedLocks: result.strippedLocks,
// [#28640 ruling ③A] Each connector map the package export
// withheld whole, so its copy carries it absent. Omitted when
// there are none, so that answer's key set is unchanged.
...(withheldMaps.length > 0 ? { withheldMaps: [...withheldMaps] } : {}),
...(declaresSeeds
? {
seeded: {
Expand Down
Loading
Loading