Skip to content

Commit ec4ef94

Browse files
refactor(metadata-protocol): move the credential carry-forward group out of protocol.ts into protocol-credential-carry-forward.ts (#25467 cut 9) (#25731)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XNBLp4i8Un5xSqTRfqBZH2G)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Cut 9 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–8 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642 and #25719. The claim is `6391854672`; the scope is the carry-forward credential group the cut 8 report (`6391184870`) proposed from Release `6385903828`, as the server seat approved it. Four `ObjectStackProtocolImplementation` members move unedited into a new module, `packages/metadata-protocol/src/protocol-credential-carry-forward.ts` (#23050 shape B; one getter, `credentialChannels`). `protocol.ts` loses 26 net lines (29,821 to 29,795). A fifth member of the group, `readCodeLayerForCarryForward`, stays in the class (see "Readers of the moved lines"). The cut also replaces cut 7's placeholder header line in `protocol-package-install.ts` with a written header (comment only). The cut is move-only, and the card stays open for later cuts. Branch `claude/project-thread-uly5ki` is the harness-pinned branch for this thread. It names no issue, so the claim comment is the identity record. It holds one commit on `main` `e5cf9f049c`. **Serial with the file's in-flight PRs.** Measured just before this PR was opened: none of the 19 open PRs touches `packages/metadata-protocol/src/` (each PR's file list read in full; #25718's 9,209-file list was checked by `git diff --name-only` against its merge base, since the files API caps at 3,000). `git log e5cf9f0..origin/main -- packages/metadata-protocol/` is empty at `43edf90b5`; `main`'s two new commits touch other packages, and `git merge-tree` of this head with `origin/main` is clean. ## What moves | Member | Base line | Visibility | Tokens (params + return + body) | |---|---|---|---| | `credentialChannelFor` | `:6872` | private | 24 | | `carryForwardRedactedCredentials` | `:9190` | private | 103 | | `storedBodyForCarryForward` | `:9215` | private | 167 | | `restoredCredentialPathsFor` | `:9264` | private | 202 | ## The shape (#23050 shape B, as cuts 1–8 apply it) - **The class keeps every member** with its name, docblock, signature and position, less `async`. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25467 cut 9]`. - **Typing.** `CredentialCarryForwardHost` types every member as the class member itself (`ObjectStackProtocolImplementation['m']`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads. Each stub annotates its literal as `ThisParameterType` of `typeof fn`. No cast, and no member changes visibility. - **Members read:** the field `credentialChannels`, as a getter; the methods `credentialChannelFor`, `getOverlayRepo`, `readCodeLayerForCarryForward` and `storedBodyForCarryForward`, as late-bound closures. No field is assigned. - **`credentialChannelFor` moves cleanly**, as the order asked to measure first. Its other readers (the cut 8 host literals in `restoredBodyDerivation` and `refuseRestoredBodyTheSaveDoorRefuses`, `saveMetaItem`, `promoteDraftForPublish`) still call the class method, which keeps its signature, so neither `protocol-authoring-gate.ts` nor `AuthoringGateHost` changes. No pin reads it by path. ## Module-level declarations, by reader - **None moves, and none gains `export`.** No module-level declaration of `protocol.ts` is read by the moved bodies; everything they read comes from the module `protocol.ts` imports it from (`metadata-redaction.ts`, `@objectstack/spec/shared`). From `protocol.ts` the new module imports types only (`ObjectStackProtocolImplementation`, `MetadataCredentialChannel`), so there is no runtime module cycle. - **Imports in `protocol.ts`.** `carryForwardRedactedValues` and `redactedPathsCarriedForward` lose their last reader and leave the `metadata-redaction.js` import list (`hasMetadataRedactor` keeps a reader and stays). The one new statement is the import of the new module. ## The published `.d.ts` The series bar is ruling `6348891160`, sorted-union identity, measured first. Base `e5cf9f049c` and head `428f9aa9` were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree (`turbo run build --filter='@objectstack/metadata-protocol^...'` once, then the package's `src/` swapped tree-only to base for the base build and restored from `HEAD` under a trap, `git diff HEAD` empty after). - **Raw sha256**: `325970c1661efaa6a3762ce7de9e6bb205ea16099784f2b6c5950d62d175b331` at base and at head, on both `dist/index.d.ts` and `dist/index.d.cts`, 772,838 bytes each (the hash cut 8 reported). `diff` exits 0. **Raw identity holds**, so sorted-union identity holds too. - **Normalised sha256**: `9562749edf7b8bba67ed2f1779799a68b3d6934a8605074934572d9507c55ba6` at both ends, both files (109 string-literal union runs sorted). This run's normaliser is its own script, so the hash is not comparable with cut 8's normalised figure; the raw hashes are. - **The two builds differ** where they should: `index.js` hashes `12c3ff329045d303…` at base (cut 8's head hash) and `a41e81cf2349514e…` at head; `storedBodyForCarryForward.call` and `credentialChannelFor.call` each occur 0 times in the base bundle and once in the head bundle. - **Positive control** on a copy of the head `index.d.ts`, the first `private engine;` changed to `engine;`: raw `3c2a24456c659dbf…`, normalised `66c9d4dcf48487eb…`. Both move. - **Export tables.** The ESM and CJS bundles, loaded at runtime from the package, each export the same 78 names at base and head; the sorted list hashes to `9f18a0177ac736ed…` on all four. None of the moved functions or the host type appears. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only - **Bodies, by AST token** (TypeScript scanner over parameters less the added `this`, return type and body): 496 tokens compared, **0 differ**. The parameter and body text, comments included, is also equal with whitespace collapsed, for all four. Negative control: one changed token in `restoredCredentialPathsFor`'s body (`held.length > 0` to `> 1`, on a copy) makes the check report `differ=1`. - **Class side.** 299 members at base and at head. The 295 that did not move are byte-identical to base. Each of the 4 stubs sits at its base position and keeps its docblock and signature byte-identical, less `async`. - **Top level of `protocol.ts`.** 235 of the 236 base statements other than the class are byte-identical, leading comments included. The other one is the `metadata-redaction.js` import that loses two names. Head adds one statement, the new module's import. - **Size.** +239 / −60 over 3 files, 299 changed lines. `protocol-credential-carry-forward.ts` has 164 lines. ## Readers of the moved lines Every test and script that reads `protocol.ts` by path (86 files) was grepped for each moved member's name and for the calls their bodies make. - **`protocol.lookup-artifact-item-call-sites.test.ts`** records `readCodeLayerForCarryForward: lookupArtifactItem(type, name, pkg)` as a site it reads in `protocol.ts`, keyed by a four-space class-member header. Moving that body would take the site out of the file the pin reads and leave a forwarding closure (`this.lookupArtifactItem(...a)`) in its place. **Verdict: `readCodeLayerForCarryForward` stays in the class**, the same disposition the Release gives `viewContainerNameCollisionRefusal` for the same pin. `storedBodyForCarryForward` reaches it through the host. The pin is unchanged and green. - **`protocol.view-expansion-reconcile-call-sites.test.ts`** censuses `this.getOverlayRepo()` acquisitions in `protocol.ts`. `restoredCredentialPathsFor`'s `const repo = this.getOverlayRepo();` is the census's exempt spelling and leaves with the body (14 to 13 such lines), and the body makes no stored-row write. **Verdict: no edit**, as cuts 5 and 6 moved the same line. The census stays green and keeps its population of writes. - No other hit. No cross-package test or script names any moved member (the hypothesis in the order holds), and no `docs/` or `content/` page does. - **Bracketed-refusal pin** (`PRODUCERS`): the new module has no `new Error(` (0), so it gets no row. - **ADR anchors and decision letters.** `check:adr-anchors` output is identical at both ends: 85 anchored files, **1,846 → 1,846** decision-letter citations. No paragraph of the `protocol.ts` anchor entry describes the carry-forward, so the new module gets no entry. - **Durability checker** (`check-durability-degradation-log-level --list`): identical at both ends once `[@:][0-9]+` is stripped (159 lines). The gate exits 0 at both ends with identical output. - **Engine-double contract** (`check-engine-double-contract`): exits 0 at both ends. The `updateData`/`deleteData` seams keep their entries at new line numbers (14065 → 14039, 14090 → 14064). `productionFiles` is 3,684 at base and 3,686 at head: +1 is the new file, +1 is the gitignored `packages/spec/src/migrations/registry.ts`, which the spec build writes in the head tree and the base tree had not built. - **Identical output at both ends:** `check-adr-symbol-anchors`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, `check-filter-alias-parity`. `check-nul-bytes` differs only in the scanned file count, +1. `check-platform-checklist --changed-from origin/main`: OK, 29 anchors judged, 0 lost. - **Governed surface.** `check-governed-merges --branch claude/project-thread-uly5ki`: 0 of 3 paths hit the register, NOT governed; 299 changed lines, under the 3,000-line human-merge bar. ## Verification - `pnpm --filter @objectstack/metadata-protocol typecheck` exits 0. `eslint --no-inline-config packages/metadata-protocol` exits 0. - **`packages/metadata-protocol` suite**, base and head, compared per test name and status (vitest `--maxWorkers=2`, JSON reporter, each run under the shared verify lock): - Base `e5cf9f049c`: 21,504 tests (21,485 passed, 0 failed, 19 skipped), 266 files. - Head `428f9aa9`: 21,504 tests (21,485 passed, 0 failed, 19 skipped), 266 files. - None lost, none added, none changed status. The base leg is the package `src/` swapped tree-only from `e5cf9f049c` and restored from `HEAD` under a trap; `git diff HEAD` empty and `git status` clean after. The suite includes both pins named above (15 tests, all passed at head). - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 53 gates on 3 changed paths. 52 exit 0 at head. `check-plugin-teardown-shape --self-test` and `check:lean-entry-closure` first answered PREREQUISITE NOT MET (a pinned fixture commit outside the shallow clone; an unbuilt `objectql/dist`) and exit 0 once the commit was fetched and objectql built. `check:dual-build-cjs-loads` is NOT MEASURED locally: it needs every package's `dist/` and is left to CI; this package's CJS bundle was loaded directly above. `--ran` reconciles 53 of 53: 52 run, 1 NOT-MEASURED, 0 UNRUN. ## Acceptance notes - `protocol-package-install.ts` (cut 7) now carries a written header in the shape of the other cut modules: which members are getters (`engine`, `getServicesRegistry`), which are callbacks (`packagePersistService`, `packageInstallUndo`), the three helpers that moved with the bodies, and the two `protocol.ts` declarations it imports. Comment only; that file has no other change. - The class docblock of `carryForwardRedactedCredentials` keeps its `{@link carryForwardRedactedValues}`, which `protocol.ts` no longer imports. The docblock is unchanged by design (move-only); the link names the function in `metadata-redaction.ts`. ## Carry-forward Unchanged from cut 8, plus one: public members whose return type is inferred, the bodies kept by a reader (`probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `packagelessAddress`, `refuseUnmintableMetaType`, `metaTypeNamespaceExists`, `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, `reportPageRequiresAbsentAtLoad`, the 7 `applyRegistryWriteThrough` callers, and now `readCodeLayerForCarryForward`), and the private-static readers stay in `protocol.ts`. --- _Generated by [Claude Code](https://claude.ai/code/session_018R7PXvRqqGBhprFi6EWucU)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f3fea0a commit ec4ef94

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)