Skip to content

Commit c8428d6

Browse files
committed
feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import
PackageExportManifestSchema gains the fixed key withheldMaps: each map the export withheld whole ({ type: 'connector', name, path }, path one of the connector redactor's four map positions). UNMANAGED_INSTALL_KEY_DISPOSITIONS classifies it door, and the export's label (declared by the export contract, never classified) door too, so an unmanaged import accepts an export as written. The table's totality test now reads the export's fixed keys as the third schema a package body is read with. Claude-Session: https://claude.ai/code/session_01ADHkmPi3pqv7Vkx4kymD87 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 04e0c2c commit c8428d6

4 files changed

Lines changed: 117 additions & 16 deletions

File tree

‎packages/spec/src/api/package-lifecycle.test.ts‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
GetPackageCustomizationsResponseSchema,
2525
} from './package-lifecycle.zod';
2626
import { PackagePublishResultSchema as SystemPackagePublishResultSchema } from '../system/metadata-persistence.zod';
27+
import { CONNECTOR_CREDENTIAL_MAP_PATHS, redactConnectorItem } from '../integration/connector-credential-redaction';
2728

2829
describe('the ruled re-export of PackagePublishResultSchema into the `/api` namespace', () => {
2930
it('is the SAME declaration as the `/system` original — a re-export, never a second copy', () => {
@@ -195,7 +196,7 @@ describe('RollbackToPackageCommitResponseSchema declares the COMMIT-rollback bod
195196
});
196197
});
197198

198-
describe('PackageExportManifestSchema declares the four fixed keys and stays open', () => {
199+
describe('PackageExportManifestSchema declares the five fixed keys and stays open', () => {
199200
/** A verbatim-shaped capture of a real `assemblePackageManifest` return. */
200201
const realResponse = {
201202
id: 'com.example.crm',
@@ -212,7 +213,7 @@ describe('PackageExportManifestSchema declares the four fixed keys and stays ope
212213
if (result.success) expect(result.data).toEqual(realResponse);
213214
});
214215

215-
it('requires exactly the four fixed keys — a manifest with no items still parses', () => {
216+
it('requires only the three identity keys — a manifest with no items still parses', () => {
216217
const bare = { id: 'com.example.empty', name: 'empty', version: '1.0.0' };
217218
expect(PackageExportManifestSchema.safeParse(bare).success).toBe(true);
218219
});
@@ -222,6 +223,50 @@ describe('PackageExportManifestSchema declares the four fixed keys and stays ope
222223
});
223224
});
224225

226+
describe('PackageExportManifestSchema.withheldMaps names each map the export withheld whole (#28640 ruling ③A)', () => {
227+
const identity = { id: 'com.example.crm', name: 'example-crm', version: '2.1.0' };
228+
/** The connector item as the export writes it: served, so its header map is already absent. */
229+
const servedConnector = { name: 'stripe', label: 'Stripe', type: 'api', provider: 'rest', providerConfig: { baseUrl: 'https://api.stripe.example' } };
230+
231+
it('parses an export that lists a withheld map and PRESERVES it — the item body stays as served', () => {
232+
const manifest = {
233+
...identity,
234+
connectors: [servedConnector],
235+
withheldMaps: [{ type: 'connector', name: 'stripe', path: 'providerConfig.headers' }],
236+
};
237+
const result = PackageExportManifestSchema.safeParse(manifest);
238+
expect(result.success).toBe(true);
239+
if (result.success) expect(result.data).toEqual(manifest);
240+
});
241+
242+
it('is optional — an export that withheld nothing, or one from before the key, still parses', () => {
243+
expect(PackageExportManifestSchema.safeParse(identity).success).toBe(true);
244+
});
245+
246+
it('spells each position exactly as the connector redactor reports it — the same four maps, nothing else', () => {
247+
const declared = [...PackageExportManifestSchema.shape.withheldMaps.unwrap().element.shape.path.options];
248+
// The redactor's own list of the maps it withholds whole …
249+
expect(declared).toEqual(CONNECTOR_CREDENTIAL_MAP_PATHS.map((p) => p.join('.')));
250+
// … and the spelling its `redactedKeys` names them by, read off a real redaction.
251+
const map = { Authorization: 'x' };
252+
const { redactedKeys } = redactConnectorItem({
253+
...servedConnector,
254+
providerConfig: { headers: map, defaultHeaders: map, transport: { headers: map, env: { TOKEN: 'x' } } },
255+
});
256+
expect([...redactedKeys].sort()).toEqual([...declared].sort());
257+
});
258+
259+
it.each([
260+
['a position outside the four', { type: 'connector', name: 'stripe', path: 'auth.token' }],
261+
['a position spelled without its providerConfig root', { type: 'connector', name: 'stripe', path: 'defaultHeaders' }],
262+
['a type no export withholds a map of yet', { type: 'webhook', name: 'hook', path: 'providerConfig.headers' }],
263+
['a list of the withheld header names — maps are withheld whole', { type: 'connector', name: 'stripe', path: 'providerConfig.headers', keys: ['Authorization'] }],
264+
['an entry with no item name', { type: 'connector', path: 'providerConfig.headers' }],
265+
])('refuses %s', (_label, entry) => {
266+
expect(PackageExportManifestSchema.safeParse({ ...identity, withheldMaps: [entry] }).success).toBe(false);
267+
});
268+
});
269+
225270
describe('ReassignOrphanedMetadataResponseSchema declares the adopt-orphans body', () => {
226271
/** A verbatim-shaped capture of a real `reassignOrphanedMetadata` return. */
227272
const realResponse = {

‎packages/spec/src/api/package-lifecycle.zod.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,43 @@ export const RollbackToPackageCommitResponseSchema = lazySchema(() => z.object({
175175
* (`runtime/src/domains/packages.ts`) builds the key set DYNAMICALLY from the
176176
* metadata type registry — one plural key per type present
177177
* (`manifest[plural] = items.map(clean)`), partitioning `views` per #5320 —
178-
* plus the four fixed keys below. Only the fixed keys are pinnable;
178+
* plus the five fixed keys below. Only the fixed keys are pinnable;
179179
* enumerating the registry here would freeze this contract against future
180180
* metadata types, so everything else deliberately falls to the open
181181
* catch-all. Freezes nothing.
182+
*
183+
* `withheldMaps` (#28640 ruling ③A) names each map position the export
184+
* withheld whole. The export reads SERVED items, so the connector redactor
185+
* (`CONNECTOR_CREDENTIAL_MAP_PATHS`, `integration/connector-credential-redaction.ts`)
186+
* has already dropped each of its four header / env maps, and the item is
187+
* written with the map simply absent; this list is how an import tells an
188+
* incomplete map from one that was never written. Positions only — never a
189+
* header or variable name — so the credential heuristic stays the
190+
* redactor's, not a contract of the export. The path vocabulary is the
191+
* redactor's own `redactedKeys` spelling of those four positions, and
192+
* `package-lifecycle.test.ts` holds the two equal.
182193
*/
183194
export const PackageExportManifestSchema = lazySchema(() => z.object({
184195
id: z.string().describe('The exported package\'s id.'),
185196
name: z.string().describe('The exported package\'s machine name.'),
186197
version: z.string().describe('The exported package\'s version.'),
187198
label: z.string().optional().describe('Display label, when the package declares one.'),
199+
withheldMaps: z.array(z.object({
200+
type: z.literal('connector').describe('Metadata type of the item whose map was withheld.'),
201+
name: z.string().describe('Name of the item whose map was withheld.'),
202+
path: z.enum([
203+
'providerConfig.headers',
204+
'providerConfig.defaultHeaders',
205+
'providerConfig.transport.headers',
206+
'providerConfig.transport.env',
207+
]).describe('Dot path of the withheld map inside the item.'),
208+
}).strict()).optional().describe(
209+
'Every map the export withheld WHOLE because a served read never carries it: a connector\'s '
210+
+ 'header and environment-variable maps. The item is written with that map absent, so an import '
211+
+ 'reads this list to tell an incomplete map from one the package never had, and names each one '
212+
+ 'for an operator to re-enter. Positions only: no header or variable name is listed. Absent '
213+
+ 'when the export withheld nothing.',
214+
),
188215
}).catchall(z.unknown().describe(
189216
'One key per metadata type present in the package (plural spelling, e.g. '
190217
+ '`objects`, `views`), each an array of cleaned item bodies. The key set '

‎packages/spec/src/kernel/unmanaged-install.test.ts‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
* authoring refusal and by the install door's import planner
77
* (`@objectstack/metadata-protocol`).
88
*
9-
* Held TOTAL here, beside both schemas a package body is read with, so the PR
10-
* that adds a key to `ManifestSchema` or to the stack definition is the PR that
11-
* reddens — not a later install that refuses the key with no reason on record.
9+
* Held TOTAL here, beside the three schemas a package body is read with, so the
10+
* PR that adds a key to `ManifestSchema`, to the stack definition or to the
11+
* package export's fixed keys is the PR that reddens — not a later install that
12+
* refuses the key with no reason on record.
1213
*/
1314
import { describe, expect, it } from 'vitest';
1415
import { ManifestSchema } from './manifest.zod';
1516
import { ObjectStackDefinitionSchema } from '../stack.zod';
17+
import { PackageExportManifestSchema } from '../api/package-lifecycle.zod';
1618
import { PLURAL_TO_SINGULAR } from '../meta-spelling/manifest-collection-spelling';
1719
import { ASSEMBLED_VIEW_ITEMS_KEY } from '../ui/assembled-views.zod';
1820
import {
@@ -27,8 +29,10 @@ const collections = new Set([...Object.keys(PLURAL_TO_SINGULAR), ASSEMBLED_VIEW_
2729
const tombstoned = (schema: unknown) =>
2830
/^\[REMOVED\]/.test(String((schema as { description?: unknown } | undefined)?.description ?? ''));
2931
const stackShape = (ObjectStackDefinitionSchema as unknown as { shape: Record<string, unknown> }).shape;
32+
/** The export's FIXED keys — its open catch-all carries the collections, judged by the registry. */
33+
const exportShape = PackageExportManifestSchema.shape;
3034

31-
describe('the unmanaged-install key classification is total against both schemas a package body is read with', () => {
35+
describe('the unmanaged-install key classification is total against the three schemas a package body is read with', () => {
3236
it('classifies every live ManifestSchema key', () => {
3337
const missing = Object.entries(ManifestSchema.shape)
3438
.filter(([key, schema]) => !collections.has(key) && !tombstoned(schema) && !(key in UNMANAGED_INSTALL_KEY_DISPOSITIONS))
@@ -41,8 +45,13 @@ describe('the unmanaged-install key classification is total against both schemas
4145
expect(missing).toEqual([]);
4246
});
4347

44-
it('classifies nothing neither schema declares — a stale row is a lie about a key', () => {
45-
const declared = new Set([...Object.keys(ManifestSchema.shape), ...Object.keys(stackShape)]);
48+
it('classifies every fixed key of the package export — the body an import elsewhere is handed', () => {
49+
const missing = Object.keys(exportShape).filter((key) => !(key in UNMANAGED_INSTALL_KEY_DISPOSITIONS));
50+
expect(missing).toEqual([]);
51+
});
52+
53+
it('classifies nothing no schema declares — a stale row is a lie about a key', () => {
54+
const declared = new Set([...Object.keys(ManifestSchema.shape), ...Object.keys(stackShape), ...Object.keys(exportShape)]);
4655
expect(Object.keys(UNMANAGED_INSTALL_KEY_DISPOSITIONS).filter((key) => !declared.has(key))).toEqual([]);
4756
});
4857

@@ -55,6 +64,14 @@ describe('the unmanaged-install key classification is total against both schemas
5564
expect(UNMANAGED_INSTALL_KEY_DISPOSITIONS.data).toBe('door');
5665
});
5766

67+
it('leaves the export\'s `withheldMaps` and `label` to the door — an export is importable as written', () => {
68+
// `withheldMaps` (#28640 ruling ③A) is read by the door to name each incomplete map; the
69+
// export's `label` has no slot on the base's closed install manifest. Either one left
70+
// unclassified refuses every export that carries it as an unrecognised key.
71+
expect(UNMANAGED_INSTALL_KEY_DISPOSITIONS.withheldMaps).toBe('door');
72+
expect(UNMANAGED_INSTALL_KEY_DISPOSITIONS.label).toBe('door');
73+
});
74+
5875
it('refuses exactly code, uncopyable content and an artifact envelope', () => {
5976
expect([...UNMANAGED_INSTALL_REFUSING_DISPOSITIONS].sort()).toEqual(['code', 'envelope', 'uncopyable']);
6077
});

‎packages/spec/src/kernel/unmanaged-install.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
* metadata type's own `allowRuntimeCreate` flag in
1818
* `DEFAULT_METADATA_TYPE_REGISTRY`, the flag the save door reads.
1919
*
20-
* `unmanaged-install.test.ts` holds the table TOTAL against both schemas a
21-
* package body is read with — `ManifestSchema` and `ObjectStackDefinitionSchema`
22-
* — so a key either schema gains reddens a spec test on the PR that adds it,
23-
* instead of being refused at an install door with no reason on record.
20+
* `unmanaged-install.test.ts` holds the table TOTAL against the three schemas a
21+
* package body is read with — `ManifestSchema`, `ObjectStackDefinitionSchema`
22+
* and the fixed keys of `PackageExportManifestSchema` (`@objectstack/spec/api`),
23+
* the body `GET /packages/:id/export` writes for an import elsewhere — so a key
24+
* any of them gains reddens a spec test on the PR that adds it, instead of
25+
* being refused at an install door with no reason on record.
2426
*/
2527

2628
/**
@@ -39,7 +41,8 @@
3941
* - `code` — refused: code, or a descriptor of a code-bearing artifact.
4042
* - `uncopyable` — refused: package content the environment ledger has no row for.
4143
* - `envelope` — refused: it describes an artifact, not one package.
42-
* - `door` — not the copy's: read or applied by the install door, and
44+
* - `door` — not the copy's: read or applied by the install door (or,
45+
* for an export's display `label`, set aside by it), and
4346
* never written onto the base.
4447
* - `deployment` — not the copy's: deployment configuration no install applies.
4548
*/
@@ -62,8 +65,8 @@ export const UNMANAGED_INSTALL_REFUSING_DISPOSITIONS: ReadonlySet<UnmanagedInsta
6265

6366
/**
6467
* Every non-collection key a package body may carry — the union of
65-
* `ManifestSchema`'s live keys and the stack definition's non-collection keys —
66-
* classified for an unmanaged install.
68+
* `ManifestSchema`'s live keys, the stack definition's non-collection keys and
69+
* the package export's fixed keys — classified for an unmanaged install.
6770
*/
6871
export const UNMANAGED_INSTALL_KEY_DISPOSITIONS: Readonly<Record<string, UnmanagedInstallKeyDisposition>> = {
6972
// ── The package's identity ──
@@ -112,6 +115,15 @@ export const UNMANAGED_INSTALL_KEY_DISPOSITIONS: Readonly<Record<string, Unmanag
112115
// Read by the door to judge the requested mode; the base is the
113116
// environment's own package and makes no promise about how it installs.
114117
installModes: 'door',
118+
// [#28640 ruling ③A] The export's list of the maps it withheld whole: the
119+
// door reads it to name each incomplete map in its answer. It describes the
120+
// exchange, not the package, so the base never carries it.
121+
withheldMaps: 'door',
122+
// The export's display label. No manifest the base is installed with
123+
// declares one (`PackageInstallManifestSchema` is closed and has no
124+
// `label`), so carrying it would refuse the base; the base is named by the
125+
// request's `targetName` or the source `name`.
126+
label: 'door',
115127

116128
// ── Deployment configuration: read by a stack's boot, by no install ──
117129
i18n: 'deployment',

0 commit comments

Comments
 (0)