Skip to content

Commit 91c8900

Browse files
authored
refactor(sections): give secretValuesOf a typed signature and drop the container checks it made necessary (#468)
1 parent aece693 commit 91c8900

7 files changed

Lines changed: 45 additions & 77 deletions

File tree

‎src/sections/contract/module.ts‎

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -756,33 +756,15 @@ export interface DeclaredSecretValue {
756756
}
757757

758758
/**
759-
* The secret values of a list section's declared value, one `extract` per entry. The engine hands it zod's output
760-
* (engine/validate.ts after the shape parse, engine/secrets.ts the validated document), but the erased list
761-
* declaration and a library caller of sectionModule() reach it as `unknown`, so a malformed container or entry
762-
* contributes nothing rather than throwing, and the actionable error always comes from validation.
763-
*/
764-
export function secretValuesOf(
765-
declared: unknown,
766-
extract: (entry: Readonly<Record<string, unknown>>) => readonly DeclaredSecretValue[],
759+
* The secret values of a list section's declared value in either form, one `extract` per entry. Every caller hands
760+
* it zod's output (engine/validate.ts after the shape parse, engine/secrets.ts the validated document), so the
761+
* entries are the section's own type and a malformed value is validation's to report, never this walk's.
762+
*/
763+
export function secretValuesOf<E>(
764+
declared: readonly E[] | { readonly entries: readonly E[] },
765+
extract: (entry: E) => readonly DeclaredSecretValue[],
767766
): DeclaredSecretValue[] {
768-
const isWrapper =
769-
typeof declared === "object" &&
770-
declared !== null &&
771-
!Array.isArray(declared) &&
772-
Array.isArray((declared as { entries?: unknown }).entries);
773-
if (!Array.isArray(declared) && !isWrapper) {
774-
return [];
775-
}
776-
// "keep" is a placeholder: only the entries are read.
777-
const { entries } = undeclaredPolicy(
778-
declared as readonly unknown[] | UndeclaredPolicyList<unknown>,
779-
"keep",
780-
);
781-
return entries.flatMap((entry) =>
782-
typeof entry === "object" && entry !== null && !Array.isArray(entry)
783-
? [...extract(entry as Readonly<Record<string, unknown>>)]
784-
: [],
785-
);
767+
return listEntries(declared).flatMap((entry) => [...extract(entry)]);
786768
}
787769

788770
/**

‎src/sections/contract/plan.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,15 @@ export interface ExecTools {
156156
resolveSecret(reference: string): string;
157157
}
158158

159+
/** What every member of a bound port is, whatever the helper takes. */
160+
type Helper = (...args: never[]) => unknown;
161+
159162
/** The runtime shape of the gated ports: the token is discarded, so the gate is the type alone. */
160-
function gated<T extends object>(bound: T): object {
163+
function gated<T extends { readonly [H in keyof T]: Helper }>(bound: T): object {
161164
return Object.fromEntries(
162-
Object.entries(bound).map(([name, helper]) => [
165+
Object.entries<Helper>(bound).map(([name, helper]) => [
163166
name,
164-
typeof helper === "function"
165-
? (_exec: ExecTools, ...args: unknown[]) => helper(...args)
166-
: helper,
167+
(_exec: ExecTools, ...args: never[]) => helper(...args),
167168
]),
168169
);
169170
}

‎src/sections/environments/index.ts‎

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { SectionFailure } from "../contract/errors.js";
1212

1313
import { liveByIdentity, liveIdentity } from "../contract/live.js";
1414
import {
15-
type DeclaredSecretValue,
1615
declaredEntries,
1716
identifiedBy,
1817
type KeyedListLayering,
@@ -137,21 +136,16 @@ export const environmentsSection = {
137136
endpoints: ENDPOINTS,
138137
graphql: GRAPHQL_OPS,
139138
shape: routed(layeredList(EnvironmentsConfig)),
140-
/**
141-
* Labels carry the environment: sibling environments can declare same-named secrets.
142-
* A malformed container contributes nothing rather than throwing, so the actionable error
143-
* always comes from shape validation.
144-
*/
145-
secretValues(declared: unknown): DeclaredSecretValue[] {
146-
return secretValuesOf(declared, (entry) => {
147-
const env = entry as EnvironmentConfig;
148-
const where =
149-
typeof env.name === "string" ? `environment "${env.name}"` : "an unnamed environment";
150-
return listSecretValues(env.secrets).map(({ label, value }) => ({
151-
label: `${label} of ${where}`,
152-
value,
153-
}));
154-
});
139+
/** Labels carry the environment: sibling environments can declare same-named secrets. */
140+
secretValues(declared) {
141+
return secretValuesOf(declared, (env) =>
142+
env.secrets === undefined
143+
? []
144+
: listSecretValues(env.secrets).map(({ label, value }) => ({
145+
label: `${label} of environment "${env.name}"`,
146+
value,
147+
})),
148+
);
155149
},
156150
validate(desired) {
157151
const issues = IDENTITY.validate(desired);

‎src/sections/shared/list-section.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@ import { snapshotList } from "./list-section-snapshot.js";
2727
import { identityClaims, pathOf, valueAt } from "./list-section-write.js";
2828
import { knobbed, routed } from "./schema-helpers.js";
2929

30-
function secretValuesFor(decl: ErasedDecl<string>, declared: unknown): DeclaredSecretValue[] {
30+
/**
31+
* valueAt() walks the erased entry, so a declared secret field reads as `unknown`: absent on an entry that leaves
32+
* the optional field out, a string otherwise, like the identity beside it. The narrowing is that walk's, not a
33+
* second proof of the shape.
34+
*/
35+
function secretValuesFor(
36+
decl: ErasedDecl<string>,
37+
declared: ErasedDeclared,
38+
): DeclaredSecretValue[] {
3139
const fields = decl.secrets ?? [];
3240
return secretValuesOf(declared, (entry) =>
3341
fields.flatMap((field) => {
@@ -66,7 +74,10 @@ export function listSection<
6674
shape: routed(knobbed(decl.entry)),
6775
...(decl.secrets === undefined
6876
? {}
69-
: { secretValues: (declared: Declared<K>) => secretValuesFor(erased, declared) }),
77+
: {
78+
secretValues: (declared: Declared<K>) =>
79+
secretValuesFor(erased, declared as unknown as ErasedDeclared),
80+
}),
7081
layering: {
7182
keys: (entry) => identityClaims(erased.identity, entry),
7283
keyField: decl.identity.field,

‎src/sections/shared/secrets-engine.ts‎

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,12 @@ export function secretOps<
9393
}
9494

9595
/** Each value is labelled with its entry's secret NAME so a validation error can point at it. */
96-
export function listSecretValues(declared: unknown): DeclaredSecretValue[] {
97-
return secretValuesOf(declared, (entry) => {
98-
if (typeof entry.value !== "string") {
99-
return [];
100-
}
101-
const label =
102-
typeof entry.name === "string"
103-
? `the secret entry "${entry.name}"`
104-
: "an unnamed secret entry";
105-
return [{ label, value: entry.value }];
106-
});
96+
export function listSecretValues(
97+
declared: SecretEntry[] | UndeclaredPolicyList<SecretEntry>,
98+
): DeclaredSecretValue[] {
99+
return secretValuesOf(declared, (entry) => [
100+
{ label: `the secret entry "${entry.name}"`, value: entry.value },
101+
]);
107102
}
108103

109104
export interface SealingKey {

‎test/sections/environments/environments.test.ts‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -556,23 +556,16 @@ describe("environments nested secrets validation and shape", () => {
556556
);
557557
});
558558

559-
test("secretValues walks every entry's secrets list and survives malformed containers", () => {
560-
// The double cast feeds secretValues a pre-validation document slice on purpose: its contract is defensiveness against any merged value.
559+
test("secretValues walks every entry's secrets list in either nested form and labels each by its environment", () => {
561560
const values = environmentsSection.secretValues?.([
562561
{ name: "a", secrets: [{ name: "X", value: "$X" }] },
563562
{ name: "b", secrets: { entries: [{ name: "Y", value: "$Y" }] } },
564563
{ name: "c" },
565-
{ name: "d", secrets: "garbage" },
566-
"not-an-entry",
567-
] as unknown as EnvironmentConfig[]);
564+
]);
568565
expect(values).toEqual([
569566
{ label: 'the secret entry "X" of environment "a"', value: "$X" },
570567
{ label: 'the secret entry "Y" of environment "b"', value: "$Y" },
571568
]);
572-
// A non-list section value contributes nothing (validation reports it).
573-
expect(
574-
environmentsSection.secretValues?.({ not: "a list" } as unknown as EnvironmentConfig[]),
575-
).toEqual([]);
576569
});
577570
});
578571

‎test/sections/webhooks/webhooks.test.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,6 @@ describe("webhooks secretValues", () => {
125125
}),
126126
).toEqual([{ label: 'the webhook "https://a.test" config.secret', value: "$B" }]);
127127
});
128-
129-
test("malformed containers return [] and leave the error to validation", () => {
130-
// The extractor faces any merged value, so the double cast feeds it a pre-validation value on purpose; validation is where the user gets the
131-
// message.
132-
for (const malformed of [null, "hooks", 42, { _undeclared: "keep" }, [null, "x"]]) {
133-
expect(webhooksSection.secretValues?.(malformed as unknown as WebhookConfig[])).toEqual([]);
134-
}
135-
});
136128
});
137129

138130
describe("webhooks plan", () => {

0 commit comments

Comments
 (0)