Repository navigation
Expand file tree
/
Copy pathProviderSecretResolver.ts
More file actions
135 lines (130 loc) · 5.84 KB
/
Copy pathProviderSecretResolver.ts
File metadata and controls
135 lines (130 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* ProviderSecretResolver: turns environment values that name a secret (such as
* a 1Password secret source) into the secrets they name, once, and holds them
* in memory.
*
* Every provider instance resolves its environment when the driver builds it,
* and a single instance can rebuild several times per session. Shelling out
* to `op` on each of those is slow (seconds) and, worse, can put a biometric
* prompt in front of a user who only started a thread. The resolver therefore
* caches by secret (reference plus account) for the lifetime of the process; `invalidate` is wired
* to the Settings refresh button, which is the user's way of saying "go read
* it again" after rotating a credential.
*
* @module provider/ProviderSecretResolver
*/
import type {
OnePasswordAccountSummary,
ProviderInstanceEnvironment,
ProviderSecretSource,
SecretProviderId,
SecretSourceCheckResult,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import type { ResolvedProviderEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
import type { ProviderSecretReference } from "./ProviderSecretReference.ts";
/**
* An instance environment after its secret references have been read.
*
* `unresolved` names the variables the instance configures but whose value
* could not be read. They are tracked separately because being absent from
* `variables` is not enough: the child environment starts from the server's
* own, so a name left alone keeps whatever the server inherited under it.
* That is the wrong credential precisely when the user asked for a specific
* one, and it hides as "authenticated".
*/
export interface ResolvedProviderInstanceEnvironment {
readonly variables: ResolvedProviderEnvironment;
readonly unresolved: ReadonlyArray<string>;
}
export interface ProviderSecretResolverShape {
/**
* Replace every secret the environment names with its value. Literal values
* pass through untouched.
*
* Never fails. A reference that cannot be read (1Password locked, `op` not
* installed, item deleted) is reported as unresolved rather than
* substituted with an empty string, so the provider reports the honest
* "unauthenticated" instead of failing later with a credential that looks
* present and is not. The instance registry unsets those names,
* which is what keeps the provider off a same-named credential the server
* happens to have inherited.
*/
readonly resolve: (
environment: ProviderInstanceEnvironment | undefined,
) => Effect.Effect<ResolvedProviderInstanceEnvironment>;
/**
* Resolve `references` ahead of the callers that will ask for them, in as
* few reads as the secret store allows.
*
* Unlocking is charged per invocation of the store's CLI, not per secret, so
* a caller about to build ten instances that each resolve their own
* environment would otherwise cost ten authorizations. Priming turns that
* into one.
*
* Purely an optimization, and deliberately best effort: it never fails, and
* a batch that does not come back leaves the cache exactly as cold as it
* found it, so `resolve` falls back to reading one reference at a time with
* the same per-variable failure isolation it has always had.
*/
readonly prime: (references: ReadonlyArray<ProviderSecretReference>) => Effect.Effect<void>;
/**
* Drop cached secrets so the next `resolve` re-reads them from the store:
* every secret by default, or only those read through the named providers
* given, so editing one provider never re-prompts for the others. Callers
* that need the new value to reach a running provider must also rebuild the
* instance: a provider process keeps the environment it was spawned with.
*/
readonly invalidate: (providers?: ReadonlySet<SecretProviderId>) => Effect.Effect<void>;
/**
* Read one secret source now, for the Settings "check" button, and report
* whether it resolved. Reads fresh, bypassing the cache, and stores a
* successful read in the cache so the next provider refresh does not prompt
* again. Never fails and never returns the value; a failure carries the
* store's own reason.
*/
readonly check: (source: ProviderSecretSource) => Effect.Effect<SecretSourceCheckResult>;
/**
* The 1Password accounts signed in on this machine, so settings can offer
* them instead of asking the user to type one. Reads local `op` config only,
* so it never prompts. Empty when `op` is missing or fails.
*/
readonly listOnePasswordAccounts: Effect.Effect<ReadonlyArray<OnePasswordAccountSummary>>;
}
/**
* Defaults to handing every literal back untouched, which is what a build
* without secret-store integration behaves like, and what tests want unless
* they are testing resolution itself. A secret source has no literal form, so
* it is reported unresolved.
*/
const resolveWithoutSecretStore = (
environment: ProviderInstanceEnvironment | undefined,
): ResolvedProviderInstanceEnvironment => {
const variables: Array<ResolvedProviderEnvironment[number]> = [];
const unresolved: Array<string> = [];
for (const { name, value, sensitive } of environment ?? []) {
if (typeof value === "string") {
variables.push({ name, value, sensitive });
} else {
unresolved.push(name);
}
}
return { variables, unresolved };
};
export class ProviderSecretResolver extends Context.Reference<ProviderSecretResolverShape>(
"t3/provider/ProviderSecretResolver",
{
defaultValue: () => ({
resolve: (environment) => Effect.sync(() => resolveWithoutSecretStore(environment)),
prime: () => Effect.void,
invalidate: () => Effect.void,
check: () =>
Effect.succeed({
_tag: "Failed",
detail: "Secret stores are not available on this server",
} as const),
listOnePasswordAccounts: Effect.succeed([]),
}),
},
) {}