@@ -7299,7 +7299,7 @@ export class ObjectStackProtocolImplementation implements
72997299 * was carried, which is the silent deletion this method exists to prevent.
73007300 * The read is skipped entirely — no extra query, for any save — when the
73017301 * type has no registered redactor, which is every type but `datasource`
7302- * today .
7302+ * (built in) and `flow` (registered by the automation plugin, #20552) .
73037303 */
73047304 private async carryForwardRedactedCredentials(args: {
73057305 type: string;
@@ -7320,11 +7320,53 @@ export class ObjectStackProtocolImplementation implements
73207320 packageId: args.packageId,
73217321 });
73227322 }
7323- const body = stored?.body;
7323+ // [#20552] NO overlay row at either state: the body the read served is
7324+ // the CODE layer, so that is what the incoming body is compared with.
7325+ // Without this a code-authored item — an inbound flow whose secret is a
7326+ // literal in the app's source, the shipped reference shape — had its
7327+ // FIRST save persist an overlay row with the credential dropped, and
7328+ // an overlay wins every later merge.
7329+ const body = stored?.body ?? await this.readCodeLayerForCarryForward(args.type, args.ref.name, args.packageId);
73247330 if (!body) return args.item;
73257331 return carryForwardRedactedValues(args.type, args.item, body);
73267332 }
73277333
7334+ /**
7335+ * [#20552] The body a read served for an item with NO overlay row — the
7336+ * CODE layer, resolved in the order `getMetaItemLayered` resolves its `code`
7337+ * layer: the MetadataService item, else the loaded artifact's item, else the
7338+ * SchemaRegistry item (with the plural/singular retry). The artifact lookup
7339+ * comes before the plain registry key because an overlay hydrated into that
7340+ * key can shadow it (see {@link lookupArtifactItem}); here there is no
7341+ * overlay row, so a hydrated copy would be stale.
7342+ *
7343+ * Type-agnostic, like the carry-forward it feeds: a code-defined datasource
7344+ * gets the same first-save protection as a code-authored flow.
7345+ *
7346+ * ⛔ No `try`/`catch` — the carry-forward's own rule. A MetadataService read
7347+ * that throws fails the save, and one that reports itself degraded with
7348+ * nothing found anywhere fails it as the same 503 the read doors answer:
7349+ * the alternative is persisting a body whose credential this save could not
7350+ * confirm was carried.
7351+ */
7352+ private async readCodeLayerForCarryForward(
7353+ type: string,
7354+ name: string,
7355+ packageId: string | null,
7356+ ): Promise<unknown> {
7357+ const pkg = packageId ?? undefined;
7358+ const fromService = await this.readItemFromMetadataService(type, name, pkg);
7359+ if (fromService.data !== undefined && fromService.data !== null) return fromService.data;
7360+ let item: unknown = this.lookupArtifactItem(type, name, pkg) ?? this.engine.registry.getItem(type, name, pkg);
7361+ if (item === undefined) {
7362+ const alt = PLURAL_TO_SINGULAR[type] ?? SINGULAR_TO_PLURAL[type];
7363+ if (alt) item = this.engine.registry.getItem(alt, name, pkg);
7364+ }
7365+ if (item !== undefined && item !== null) return item;
7366+ if (fromService.degraded) this.throwMetadataServiceUnavailable(fromService.errors);
7367+ return undefined;
7368+ }
7369+
73287370 /**
73297371 * [#5840] Read ONE item from the `metadata` service, keeping the ADR-0110
73307372 * D3 verdict instead of flattening it into `undefined`.
0 commit comments