Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 45 additions & 25 deletions packages/core/sdk/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,17 +519,20 @@ export const createExecutor = <
return yield* provider.get(id);
}

// Fallback: ask enumerating providers in registration order.
// First non-null wins. Providers that throw are treated as
// "don't have it" and skipped so one flaky provider doesn't
// Fallback: ask every enumerating provider in parallel. First
// non-null in registration order wins. Providers that throw
// are treated as "don't have it" so one flaky provider can't
// block resolution via others.
for (const provider of secretProviders.values()) {
if (!provider.list) continue;
const value = yield* provider
.get(id)
.pipe(Effect.catchAll(() => Effect.succeed(null)));
if (value !== null) return value;
}
const candidates = [...secretProviders.values()].filter(
(p) => p.list,
);
const values = yield* Effect.all(
candidates.map((p) =>
p.get(id).pipe(Effect.catchAll(() => Effect.succeed(null))),
),
{ concurrency: "unbounded" },
);
for (const value of values) if (value !== null) return value;
return null;
});

Expand Down Expand Up @@ -595,11 +598,17 @@ export const createExecutor = <

const secretsRemove = (id: string): Effect.Effect<void, Error> =>
Effect.gen(function* () {
for (const provider of secretProviders.values()) {
if (provider.writable && provider.delete) {
yield* provider.delete(id);
}
}
// Providers don't coordinate on which of them own the id — they
// each get asked. Most calls are no-ops; fan them out so one
// slow provider doesn't serialize the rest.
const deleters = [...secretProviders.values()].filter(
(p): p is typeof p & { delete: NonNullable<typeof p.delete> } =>
!!(p.writable && p.delete),
);
yield* Effect.all(
deleters.map((p) => p.delete(id)),
{ concurrency: "unbounded" },
);
yield* core.delete({
model: "secret",
where: [{ field: "id", value: id }],
Expand Down Expand Up @@ -643,15 +652,26 @@ export const createExecutor = <
);
}

// Then every provider that can enumerate itself. If a provider
// fails to list (unlocked vault, network error), swallow the
// failure and continue — one flaky provider shouldn't block
// the whole list.
for (const [providerKey, provider] of secretProviders.entries()) {
if (!provider.list) continue;
const entries = yield* provider
.list()
.pipe(Effect.catchAll(() => Effect.succeed([] as const)));
// Then every provider that can enumerate itself, in parallel.
// If a provider fails to list (unlocked vault, network error),
// swallow the failure so one flaky provider can't block the
// whole list. Merge in registration order afterwards so the
// "first provider wins" precedence stays deterministic.
const listers = [...secretProviders.entries()].filter(
([, p]) => p.list,
);
const lists = yield* Effect.all(
listers.map(([key, p]) =>
p
.list!()
.pipe(
Effect.catchAll(() => Effect.succeed([] as const)),
Effect.map((entries) => ({ key, entries })),
),
),
{ concurrency: "unbounded" },
);
for (const { key, entries } of lists) {
for (const entry of entries) {
if (byId.has(entry.id)) continue; // core row wins
byId.set(
Expand All @@ -660,7 +680,7 @@ export const createExecutor = <
id: SecretId.make(entry.id),
scopeId: scope.id,
name: entry.name,
provider: providerKey,
provider: key,
createdAt: new Date(),
}),
);
Expand Down
39 changes: 28 additions & 11 deletions packages/plugins/graphql/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,35 @@ export const resolveHeaders = (
secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> },
): Effect.Effect<Record<string, string>, Error> =>
Effect.gen(function* () {
const entries = Object.entries(headers);
// Resolve secret-backed headers in parallel. Missing / failing
// lookups drop the header rather than fail the invocation, same
// as the serial version.
const values = yield* Effect.all(
entries.map(([name, value]) =>
typeof value === "string"
? Effect.succeed<{ readonly name: string; readonly value: string | null }>({
name,
value,
})
: secrets.get(value.secretId).pipe(
Effect.catchAll(() => Effect.succeed<string | null>(null)),
Effect.map((secret) => ({
name,
value:
secret === null
? null
: value.prefix
? `${value.prefix}${secret}`
: secret,
})),
),
),
{ concurrency: "unbounded" },
);
const resolved: Record<string, string> = {};
for (const [name, value] of Object.entries(headers)) {
if (typeof value === "string") {
resolved[name] = value;
} else {
const secret = yield* secrets.get(value.secretId).pipe(
Effect.catchAll(() => Effect.succeed<string | null>(null)),
);
if (secret !== null) {
resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;
}
}
for (const { name, value } of values) {
if (value !== null) resolved[name] = value;
}
return resolved;
});
Expand Down
42 changes: 27 additions & 15 deletions packages/plugins/openapi/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,34 @@ export const resolveHeaders = (
secrets: { readonly get: (id: string) => Effect.Effect<string | null, Error> },
): Effect.Effect<Record<string, string>, Error> =>
Effect.gen(function* () {
const resolved: Record<string, string> = {};
for (const [name, value] of Object.entries(headers)) {
if (typeof value === "string") {
resolved[name] = value;
} else {
const secret = yield* secrets.get(value.secretId);
if (secret === null) {
return yield* Effect.fail(
new Error(
`Failed to resolve secret "${value.secretId}" for header "${name}"`,
const entries = Object.entries(headers);
// Fan out secret lookups: on every invocation, one or two headers
// typically each hit the secret store. Resolving them in parallel
// is a free wall-clock win — preserved order is only needed for
// the final assembly, not the fetches.
const values = yield* Effect.all(
entries.map(([name, value]) =>
typeof value === "string"
? Effect.succeed({ name, value })
: secrets.get(value.secretId).pipe(
Effect.flatMap((secret) =>
secret === null
? Effect.fail(
new Error(
`Failed to resolve secret "${value.secretId}" for header "${name}"`,
),
)
: Effect.succeed({
name,
value: value.prefix ? `${value.prefix}${secret}` : secret,
}),
),
),
);
}
resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;
}
}
),
{ concurrency: "unbounded" },
);
const resolved: Record<string, string> = {};
for (const { name, value } of values) resolved[name] = value;
return resolved;
});

Expand Down
Loading