Skip to content

Commit 428f9aa

Browse files
committed
refactor(metadata-protocol): move the credential carry-forward group out of protocol.ts into protocol-credential-carry-forward.ts (#25467 cut 9)
credentialChannelFor, carryForwardRedactedCredentials, storedBodyForCarryForward and restoredCredentialPathsFor move unedited (shape B: getter credentialChannels; callbacks credentialChannelFor, getOverlayRepo, readCodeLayerForCarryForward, storedBodyForCarryForward). readCodeLayerForCarryForward stays in the class: the lookupArtifactItem call-site pin reads it in protocol.ts. Cut 7's module gets its written header in place of the placeholder line (comment only). Claude-Session: https://claude.ai/code/session_018R7PXvRqqGBhprFi6EWucU Co-authored-by: Claude <noreply@anthropic.com>
1 parent e5cf9f0 commit 428f9aa

3 files changed

Lines changed: 239 additions & 60 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 9] The credential carry-forward of `ObjectStackProtocolImplementation`
5+
* — the registered credential channel lookup (`credentialChannelFor`), the
6+
* save's write-path inverse of the read exits' credential redaction
7+
* (`carryForwardRedactedCredentials`), the stored body it compares against
8+
* (`storedBodyForCarryForward`) and the runtime authoring gate's half of the
9+
* same context (`restoredCredentialPathsFor`) — moved out of `protocol.ts`
10+
* whole. One member of the group stays in the class:
11+
* `readCodeLayerForCarryForward`, the code-layer read the stored body falls
12+
* back to, a `lookupArtifactItem(` site that
13+
* `protocol.lookup-artifact-item-call-sites.test.ts` reads in `protocol.ts`.
14+
*
15+
* A MOVE, not a redesign. Each function below is the body of the
16+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
17+
* re-indented one level and otherwise unedited: the same reads, in the same
18+
* order, with no `try`/`catch` added where the carry-forward forbids one. No
19+
* module-level declaration of `protocol.ts` is read by these bodies, so none
20+
* moved with them.
21+
*
22+
* ## How the moved bodies reach the class
23+
*
24+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
25+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
26+
* The class keeps every method — the published `dist/*.d.ts` lists each one's
27+
* name, docblock and declared signature — and each body is one literal of
28+
* exactly the {@link CredentialCarryForwardHost} members that method reads,
29+
* plus a `.call`. A method is handed over as a closure that calls it on the
30+
* protocol at call time, so a test double or spy set on the instance is still
31+
* the one the body reaches, and the moved bodies that call one another do so
32+
* through the protocol, exactly as before. `credentialChannelFor` keeps its
33+
* other readers in the class, the cut 8 restore doors among them, and they
34+
* still call the method.
35+
*
36+
* The one field the bodies read, `credentialChannels`, goes as a GETTER, so
37+
* the read happens when the body makes it, as for every field in this series.
38+
*
39+
* Everything else the bodies read is imported from the module `protocol.ts`
40+
* imports it from (`metadata-redaction.ts`, `@objectstack/spec/shared`); from
41+
* `protocol.ts` itself this module imports types only, so there is no
42+
* runtime module cycle. Nothing re-exports this module, so the published
43+
* surface does not move.
44+
*/
45+
46+
import type { MetadataCredentialChannel, ObjectStackProtocolImplementation } from './protocol.js';
47+
import type { SysMetadataRepository } from './sys-metadata-repository.js';
48+
import { carryForwardRedactedValues, hasMetadataRedactor, redactedPathsCarriedForward } from './metadata-redaction.js';
49+
import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
50+
51+
/**
52+
* The `ObjectStackProtocolImplementation` members the functions below read —
53+
* every one of them, and no other; each function's `this` is the `Pick` of the
54+
* members IT reads. Each is typed AS the class member (an indexed access on
55+
* the class), so a change to a member's signature on the class is a change
56+
* here too, and naming a member the class does not have is a compile error.
57+
* See the module header for which members are getters.
58+
*/
59+
export interface CredentialCarryForwardHost {
60+
credentialChannelFor: ObjectStackProtocolImplementation['credentialChannelFor'];
61+
readonly credentialChannels: ObjectStackProtocolImplementation['credentialChannels'];
62+
getOverlayRepo: ObjectStackProtocolImplementation['getOverlayRepo'];
63+
readCodeLayerForCarryForward: ObjectStackProtocolImplementation['readCodeLayerForCarryForward'];
64+
storedBodyForCarryForward: ObjectStackProtocolImplementation['storedBodyForCarryForward'];
65+
}
66+
67+
/** The body of `ObjectStackProtocolImplementation#credentialChannelFor`; the method's docblock there is its contract. */
68+
export function credentialChannelFor(
69+
this: Pick<CredentialCarryForwardHost, 'credentialChannels'>,
70+
type: string): MetadataCredentialChannel | undefined {
71+
return this.credentialChannels.get(PLURAL_TO_SINGULAR[type] ?? type);
72+
}
73+
74+
/** The body of `ObjectStackProtocolImplementation#carryForwardRedactedCredentials`; the method's docblock there is its contract. */
75+
export async function carryForwardRedactedCredentials(
76+
this: Pick<CredentialCarryForwardHost, 'storedBodyForCarryForward'>,
77+
args: {
78+
type: string;
79+
repo: SysMetadataRepository;
80+
ref: Parameters<SysMetadataRepository['get']>[0];
81+
state: 'draft' | 'active';
82+
packageId: string | null;
83+
item: any;
84+
/** [#22877] The bundle member the save addresses (its body's). */
85+
member?: string;
86+
}): Promise<any> {
87+
if (!hasMetadataRedactor(args.type)) return args.item;
88+
const body = await this.storedBodyForCarryForward(args);
89+
if (!body) return args.item;
90+
return carryForwardRedactedValues(args.type, args.item, body);
91+
}
92+
93+
/** The body of `ObjectStackProtocolImplementation#storedBodyForCarryForward`; the method's docblock there is its contract. */
94+
export async function storedBodyForCarryForward(
95+
this: Pick<CredentialCarryForwardHost, 'readCodeLayerForCarryForward'>,
96+
args: {
97+
type: string;
98+
repo: SysMetadataRepository;
99+
ref: Parameters<SysMetadataRepository['get']>[0];
100+
state: 'draft' | 'active';
101+
packageId: string | null;
102+
member?: string;
103+
}): Promise<unknown> {
104+
// [#22128] The row this save overwrites, and with no draft row yet the
105+
// active row the draft overlays: the one in the package the draft is
106+
// stamped into (`packageId`, the inherited binding included).
107+
const target = await args.repo.headAt(args.ref, {
108+
state: args.state,
109+
packageId: args.packageId,
110+
member: args.member,
111+
});
112+
let stored = target.head;
113+
if (!stored && args.state === 'draft') {
114+
stored = (await args.repo.headAt(args.ref, {
115+
state: 'active',
116+
packageId: target.packageId,
117+
member: args.member,
118+
})).head;
119+
}
120+
// [#20552] NO overlay row at either state: the body the read served is
121+
// the CODE layer, so that is what the incoming body is compared with.
122+
// Without this a code-authored item — an inbound flow whose secret is a
123+
// literal in the app's source, the shipped reference shape — had its
124+
// FIRST save persist an overlay row with the credential dropped, and
125+
// an overlay wins every later merge.
126+
return stored?.body ?? await this.readCodeLayerForCarryForward(args.type, args.ref.name, args.packageId);
127+
}
128+
129+
/** The body of `ObjectStackProtocolImplementation#restoredCredentialPathsFor`; the method's docblock there is its contract. */
130+
export async function restoredCredentialPathsFor(
131+
this: Pick<CredentialCarryForwardHost, 'credentialChannelFor' | 'getOverlayRepo' | 'storedBodyForCarryForward'>,
132+
args: {
133+
type: string;
134+
name: string;
135+
packageId: string | null;
136+
item: unknown;
137+
}): Promise<readonly string[]> {
138+
if (!hasMetadataRedactor(args.type)) return [];
139+
const repo = this.getOverlayRepo();
140+
const ref = {
141+
type: args.type,
142+
name: args.name,
143+
org: 'env',
144+
} as Parameters<typeof repo.get>[0];
145+
const body = await this.storedBodyForCarryForward({
146+
type: args.type,
147+
repo,
148+
ref,
149+
// The gate judges `active` writes only (#4463 D1), so this is the
150+
// state a gated save's carry-forward reads.
151+
state: 'active',
152+
packageId: args.packageId,
153+
});
154+
const carried = body ? redactedPathsCarriedForward(args.type, args.item, body) : [];
155+
// [#20790] A credential the write-only channel holds is withheld from
156+
// the stored body too, so the carry-forward restores nothing there —
157+
// and it is still present: the channel keeps it across this save.
158+
const held = await this.credentialChannelFor(args.type)?.heldPaths({
159+
name: args.name,
160+
state: 'active',
161+
item: args.item,
162+
});
163+
return held && held.length > 0 ? [...new Set([...carried, ...held])] : carried;
164+
}

‎packages/metadata-protocol/src/protocol-package-install.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
/** [#25467 cut 7] HEADER PLACEHOLDER */
3+
/**
4+
* [#25467 cut 7] Package install and update of `ObjectStackProtocolImplementation`
5+
* — the install primitive (`installPackage`), the manifest edit behind
6+
* `PATCH /packages/:id` (`updatePackage`), the `package` service's write half
7+
* both of them persist through (`packagePersistService`) and the undo a failed
8+
* install runs (`packageInstallUndo`) — moved out of `protocol.ts` whole. They
9+
* were the tail of the class.
10+
*
11+
* A MOVE, not a redesign. Each function below is the body of the
12+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
13+
* re-indented one level and otherwise unedited: the same parses, writes and
14+
* undo, refusing exactly where they did with the same codes and messages. The
15+
* module-level declarations only these bodies read (`persistPackageManifest`,
16+
* `snapshotPackageRow`, `restorePackageRow`) moved with them, also unedited.
17+
*
18+
* ## How the moved bodies reach the class
19+
*
20+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
21+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
22+
* The class keeps every method — the published `dist/*.d.ts` lists each one's
23+
* name, docblock and declared signature — and each body is one literal of
24+
* exactly the {@link PackageInstallHost} members that method reads, plus a
25+
* `.call`. A method is handed over as a closure that calls it on the protocol
26+
* at call time, so a test double or spy set on the instance is still the one
27+
* the body reaches, and the moved bodies that call one another
28+
* (`packagePersistService`, `packageInstallUndo`) do so through the protocol,
29+
* exactly as before.
30+
*
31+
* The fields the bodies read, `engine` and `getServicesRegistry`, go as
32+
* GETTERS, so each read happens when the body makes it: tests replace them on
33+
* a constructed protocol.
34+
*
35+
* The other module-level declarations the bodies read stay in `protocol.ts`,
36+
* where code that did not move reads them too, and are imported from there:
37+
* `packagePersistFailureError`, which the package delete also throws, and the
38+
* type `PackagePersistService`, which the class's own signatures name. The
39+
* module cycle is evaluation-safe because the one runtime import is read only
40+
* inside a function body, never while the two modules are first evaluated.
41+
* `index.ts` names what it publishes from `protocol.ts` one by one, so the
42+
* `export` they gained for this keeps them off the published surface, and
43+
* nothing re-exports this module, so the published surface does not move.
44+
*/
445

546
import type { ObjectStackProtocolImplementation, PackagePersistService } from './protocol.js';
647
import { packagePersistFailureError } from './protocol.js';

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 33 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,8 @@ import {
236236
// and `getData` serve `sys_metadata` / `sys_metadata_history` rows, whose
237237
// `metadata` column is the same stored body, through the same redactor.
238238
import {
239-
carryForwardRedactedValues,
240239
hasMetadataRedactor,
241240
redactMetadataItem,
242-
redactedPathsCarriedForward,
243241
redactStoredMetadataRow,
244242
redactStoredMetadataRows,
245243
storedMetadataBodyGroupingRefusal,
@@ -376,6 +374,13 @@ import {
376374
restoredBodyWriter,
377375
runAuthoringGate,
378376
} from './protocol-authoring-gate.js';
377+
// [#25467 cut 9] The credential carry-forward and the credential channel lookup.
378+
import {
379+
carryForwardRedactedCredentials,
380+
credentialChannelFor,
381+
restoredCredentialPathsFor,
382+
storedBodyForCarryForward,
383+
} from './protocol-credential-carry-forward.js';
379384

380385
/**
381386
* Canonical Zod schema per metadata type lives in
@@ -6870,7 +6875,12 @@ export class ObjectStackProtocolImplementation implements
68706875

68716876
/** [#20790] The registered credential channel of `type`, if any. */
68726877
private credentialChannelFor(type: string): MetadataCredentialChannel | undefined {
6873-
return this.credentialChannels.get(PLURAL_TO_SINGULAR[type] ?? type);
6878+
// [#25467 cut 9] The body moved to `protocol-credential-carry-forward.ts` (see `CredentialCarryForwardHost`).
6879+
const readCredentialChannels = () => this.credentialChannels;
6880+
const host: ThisParameterType<typeof credentialChannelFor> = {
6881+
get credentialChannels() { return readCredentialChannels(); },
6882+
};
6883+
return credentialChannelFor.call(host, type);
68746884
}
68756885

68766886
/**
@@ -9187,7 +9197,7 @@ export class ObjectStackProtocolImplementation implements
91879197
* type has no registered redactor, which is every type but `datasource`
91889198
* (built in) and `flow` (registered by the automation plugin, #20552).
91899199
*/
9190-
private async carryForwardRedactedCredentials(args: {
9200+
private carryForwardRedactedCredentials(args: {
91919201
type: string;
91929202
repo: SysMetadataRepository;
91939203
ref: Parameters<SysMetadataRepository['get']>[0];
@@ -9197,10 +9207,11 @@ export class ObjectStackProtocolImplementation implements
91979207
/** [#22877] The bundle member the save addresses (its body's). */
91989208
member?: string;
91999209
}): Promise<any> {
9200-
if (!hasMetadataRedactor(args.type)) return args.item;
9201-
const body = await this.storedBodyForCarryForward(args);
9202-
if (!body) return args.item;
9203-
return carryForwardRedactedValues(args.type, args.item, body);
9210+
// [#25467 cut 9] The body moved to `protocol-credential-carry-forward.ts` (see `CredentialCarryForwardHost`).
9211+
const host: ThisParameterType<typeof carryForwardRedactedCredentials> = {
9212+
storedBodyForCarryForward: (...a) => this.storedBodyForCarryForward(...a),
9213+
};
9214+
return carryForwardRedactedCredentials.call(host, args);
92049215
}
92059216

92069217
/**
@@ -9212,37 +9223,19 @@ export class ObjectStackProtocolImplementation implements
92129223
* read exits redacted — and read with no `try`/`catch`, for the reason the
92139224
* carry-forward gives.
92149225
*/
9215-
private async storedBodyForCarryForward(args: {
9226+
private storedBodyForCarryForward(args: {
92169227
type: string;
92179228
repo: SysMetadataRepository;
92189229
ref: Parameters<SysMetadataRepository['get']>[0];
92199230
state: 'draft' | 'active';
92209231
packageId: string | null;
92219232
member?: string;
92229233
}): Promise<unknown> {
9223-
// [#22128] The row this save overwrites, and with no draft row yet the
9224-
// active row the draft overlays: the one in the package the draft is
9225-
// stamped into (`packageId`, the inherited binding included).
9226-
const target = await args.repo.headAt(args.ref, {
9227-
state: args.state,
9228-
packageId: args.packageId,
9229-
member: args.member,
9230-
});
9231-
let stored = target.head;
9232-
if (!stored && args.state === 'draft') {
9233-
stored = (await args.repo.headAt(args.ref, {
9234-
state: 'active',
9235-
packageId: target.packageId,
9236-
member: args.member,
9237-
})).head;
9238-
}
9239-
// [#20552] NO overlay row at either state: the body the read served is
9240-
// the CODE layer, so that is what the incoming body is compared with.
9241-
// Without this a code-authored item — an inbound flow whose secret is a
9242-
// literal in the app's source, the shipped reference shape — had its
9243-
// FIRST save persist an overlay row with the credential dropped, and
9244-
// an overlay wins every later merge.
9245-
return stored?.body ?? await this.readCodeLayerForCarryForward(args.type, args.ref.name, args.packageId);
9234+
// [#25467 cut 9] The body moved to `protocol-credential-carry-forward.ts` (see `CredentialCarryForwardHost`).
9235+
const host: ThisParameterType<typeof storedBodyForCarryForward> = {
9236+
readCodeLayerForCarryForward: (...a) => this.readCodeLayerForCarryForward(...a),
9237+
};
9238+
return storedBodyForCarryForward.call(host, args);
92469239
}
92479240

92489241
/**
@@ -9261,38 +9254,19 @@ export class ObjectStackProtocolImplementation implements
92619254
* row. The cost is one more indexed read on an active save of a type with a
92629255
* registered redactor, and nothing for any other type.
92639256
*/
9264-
private async restoredCredentialPathsFor(args: {
9257+
private restoredCredentialPathsFor(args: {
92659258
type: string;
92669259
name: string;
92679260
packageId: string | null;
92689261
item: unknown;
92699262
}): Promise<readonly string[]> {
9270-
if (!hasMetadataRedactor(args.type)) return [];
9271-
const repo = this.getOverlayRepo();
9272-
const ref = {
9273-
type: args.type,
9274-
name: args.name,
9275-
org: 'env',
9276-
} as Parameters<typeof repo.get>[0];
9277-
const body = await this.storedBodyForCarryForward({
9278-
type: args.type,
9279-
repo,
9280-
ref,
9281-
// The gate judges `active` writes only (#4463 D1), so this is the
9282-
// state a gated save's carry-forward reads.
9283-
state: 'active',
9284-
packageId: args.packageId,
9285-
});
9286-
const carried = body ? redactedPathsCarriedForward(args.type, args.item, body) : [];
9287-
// [#20790] A credential the write-only channel holds is withheld from
9288-
// the stored body too, so the carry-forward restores nothing there —
9289-
// and it is still present: the channel keeps it across this save.
9290-
const held = await this.credentialChannelFor(args.type)?.heldPaths({
9291-
name: args.name,
9292-
state: 'active',
9293-
item: args.item,
9294-
});
9295-
return held && held.length > 0 ? [...new Set([...carried, ...held])] : carried;
9263+
// [#25467 cut 9] The body moved to `protocol-credential-carry-forward.ts` (see `CredentialCarryForwardHost`).
9264+
const host: ThisParameterType<typeof restoredCredentialPathsFor> = {
9265+
credentialChannelFor: (...a) => this.credentialChannelFor(...a),
9266+
getOverlayRepo: (...a) => this.getOverlayRepo(...a),
9267+
storedBodyForCarryForward: (...a) => this.storedBodyForCarryForward(...a),
9268+
};
9269+
return restoredCredentialPathsFor.call(host, args);
92969270
}
92979271

92989272
/**

0 commit comments

Comments
 (0)