@penvhq/provider-github 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,341 @@
1
- import { PenvError, ProviderFactoryContext, ProjectionProvider, SecretScope, ProjectionSecret, ParameterRef, PenvConfig, PenvErrorLike } from '@penvhq/core';
1
+ /** Identifies one parameter, independent of scope. `redis/password`. */
2
+ interface ParameterRef {
3
+ /** Namespace folder segments. `[]` for a root parameter. */
4
+ readonly namespace: readonly string[];
5
+ /** The parameter's own name. `password`. */
6
+ readonly name: string;
7
+ }
8
+ /**
9
+ * The provider config types penv knows about, keyed by package name. Empty here,
10
+ * deliberately: core owns the `Provider` contract and must not know which
11
+ * implementations exist. Each provider package augments this interface with its
12
+ * own config shape under its own name —
13
+ *
14
+ * ```ts
15
+ * declare module "@penvhq/core" {
16
+ * interface ProviderConfigMap {
17
+ * "@penvhq/provider-vault": VaultProviderConfig;
18
+ * }
19
+ * }
20
+ * ```
21
+ *
22
+ * — so the compile-time union is exactly the set of providers the project has
23
+ * installed, and {@link defineConfig} can hold a known `type`'s fields to the
24
+ * provider's own declaration while leaving an unknown `type` the open base shape.
25
+ */
26
+ interface ProviderConfigMap {
27
+ }
28
+ /** The provider package names whose config types are installed and merged in. */
29
+ type KnownProviderType = keyof ProviderConfigMap & string;
30
+ interface ProviderConfig {
31
+ /**
32
+ * The provider package's fully-qualified name — `"@penvhq/provider-vault"`.
33
+ * The name is the import specifier: penv resolves it from the project's own
34
+ * `node_modules`, so declaring a provider and installing its package are the
35
+ * same decision stated twice, and the config never needs a second field to
36
+ * say where the implementation lives.
37
+ */
38
+ readonly type: KnownProviderType | (string & {});
39
+ /**
40
+ * The place inside the provider that penv maps the tree onto. The format is
41
+ * the provider's own — a Vault KV base path, a Kubernetes
42
+ * `namespace/secretName` — and its package's config type documents it; the
43
+ * field name never changes between providers.
44
+ */
45
+ readonly location?: string;
46
+ /** Fields beyond `location` belong to the provider's own config type. */
47
+ readonly [key: string]: unknown;
48
+ }
49
+ /**
50
+ * What penv hands a provider package's `penvProviderFactory` to build a provider
51
+ * for one project. Declared here because it is the seam every provider package
52
+ * builds against — the CLI supplies it, the package consumes it, and neither
53
+ * imports the other's internals.
54
+ */
55
+ interface ProviderFactoryContext {
56
+ /**
57
+ * The project root — the directory holding `penv.config.ts` — absolute. It is
58
+ * the project rather than any directory inside it because where penv keeps its
59
+ * own state is penv's business: a provider that needs a path derives it from
60
+ * the root, and one that needs none is unaffected when the layout moves.
61
+ */
62
+ readonly root: string;
63
+ /**
64
+ * Required because a provider parses environment segments, and a segment is an
65
+ * environment only if the config declares it — never inferred from the store.
66
+ */
67
+ readonly config: PenvConfig;
68
+ /**
69
+ * The one environment's own `providers.*` entry, when building its declared
70
+ * source of truth. Carries provider-side settings — the `location` above all —
71
+ * that the config authored, never inferred.
72
+ */
73
+ readonly providerConfig?: ProviderConfig;
74
+ /**
75
+ * The environment this provider is the source of truth *for*, when that is
76
+ * what is being built.
77
+ */
78
+ readonly environment?: string;
79
+ }
80
+ /**
81
+ * The schema's inferred shape, registered by the schema module so the config
82
+ * file can be typed against it. Empty here, deliberately — core must not depend
83
+ * on Zod or know any one project's schema. The scaffolded `penv.schema.ts`
84
+ * augments it with the *inferred* shape (computed where Zod lives), beside the
85
+ * `z.object` it registers — the thin `.penv/env.ts` wrapper only re-exports that
86
+ * shape and loads it:
87
+ *
88
+ * ```ts
89
+ * declare module "@penvhq/core" {
90
+ * interface PenvSchemaShape {
91
+ * readonly shape: z.infer<typeof schema>;
92
+ * }
93
+ * }
94
+ * ```
95
+ *
96
+ * The augmentation is type-only and erased at runtime, so nothing cycles: the
97
+ * config never imports the schema module, yet `override` keys autocomplete from
98
+ * it whenever both files sit in one TypeScript program. A project that never
99
+ * registers a shape keeps the open `string` keys.
100
+ */
101
+ interface PenvSchemaShape {
102
+ }
103
+ /** camelCase → kebab-case at the type level, mirroring `kebabSegment` exactly. */
104
+ type KebabCase<S extends string> = S extends `${infer Head}${infer Tail}` ? Head extends Uppercase<Head> ? Head extends Lowercase<Head> ? `${Head}${KebabCase<Tail>}` : `-${Lowercase<Head>}${KebabCase<Tail>}` : `${Head}${KebabCase<Tail>}` : S;
105
+ type ParameterIdsOf<T> = {
106
+ [K in keyof T & string]: NonNullable<T[K]> extends readonly unknown[] ? KebabCase<K> : NonNullable<T[K]> extends Date ? KebabCase<K> : NonNullable<T[K]> extends Record<string, unknown> ? `${KebabCase<K>}/${ParameterIdsOf<NonNullable<T[K]>>}` : KebabCase<K>;
107
+ }[keyof T & string];
108
+ /** The registered shape, or `never` when no schema module has registered one. */
109
+ type RegisteredShape = PenvSchemaShape extends {
110
+ readonly shape: infer S;
111
+ } ? S : never;
112
+ /**
113
+ * The `override` block's key type: every parameter id the registered schema
114
+ * implies — so a typo is a compile error — or plain `string` when no shape is
115
+ * registered.
116
+ */
117
+ type OverrideKey = [RegisteredShape] extends [never] ? string : ParameterIdsOf<RegisteredShape>;
118
+ /** The `override` block: parameter id → the exact variable a consumer expects. */
119
+ type OverrideBlock = Readonly<Partial<Record<OverrideKey, string>>>;
120
+ /**
121
+ * What a provider's store can honestly do — the declaration that replaced the
122
+ * old sink/provider split. The distinction the split guarded is real (GitHub
123
+ * Actions Secrets never returns a value) and lives here now, in the contract,
124
+ * instead of in a second config key the user had to learn.
125
+ */
126
+ interface ProviderCapabilities {
127
+ /**
128
+ * What the store holds. `records`: penv records verbatim — opaque envelope
129
+ * strings at every scope, meta as a sibling record — the shape the behavioural
130
+ * contract suite gates. `projection`: a resolved projection — generated
131
+ * variable names, both `.local` scopes skipped, plaintext for the destination
132
+ * to re-seal — the shape a CI secret store consumes.
133
+ */
134
+ readonly holds: "records" | "projection";
135
+ /**
136
+ * Whether stored values can be read back. GitHub's API returns names and
137
+ * timestamps, never values, so it declares `false` — and `pull` materialises
138
+ * names and meta while `doctor` reports value drift as unknown, never as
139
+ * clean.
140
+ */
141
+ readonly readsValues: boolean;
142
+ }
143
+ /**
144
+ * Where one environment's encryption key comes from. Declared, never guessed: a
145
+ * key source penv picked for you is a key you did not choose.
146
+ */
147
+ interface KeyConfig {
148
+ readonly source: "env" | "keychain";
149
+ /**
150
+ * Names the key. Written into every value file sealed under it, so it must
151
+ * outlive any one machine — and cannot contain `:`, which separates the
152
+ * envelope's fields.
153
+ */
154
+ readonly id: string;
155
+ }
156
+ interface PenvConfig {
157
+ /**
158
+ * The whitelist of valid environment names — the only source of truth for
159
+ * what counts as an environment. Segments are matched against this list,
160
+ * never inferred from folders or filenames.
161
+ */
162
+ readonly environments: readonly string[];
163
+ readonly providers: Readonly<Record<string, ProviderConfig>>;
164
+ /**
165
+ * The environment a command acts on when `--env` is absent and nothing in the
166
+ * environment says otherwise. It must be one of {@link environments}.
167
+ *
168
+ * A declared decision, not inference: invariant 10 is untouched, because the
169
+ * name still comes from this file rather than from a branch, a folder, or
170
+ * `NODE_ENV`. It exists so the daily command is `penv run -- pnpm dev` instead
171
+ * of a flag retyped all day. CI names `--env` anyway — a pipeline that leans on
172
+ * this key is one config edit away from deploying the wrong environment.
173
+ */
174
+ readonly defaultEnvironment?: string;
175
+ /**
176
+ * Where the module holding the schema lives, relative to this config.
177
+ * Defaults to `.penv/env.ts`.
178
+ *
179
+ * A path rather than a convention because the file is the user's (invariant 2)
180
+ * — a file penv insists on owning the location of is not fully theirs, and
181
+ * `src/env.ts` is where most projects would put it. Nothing downstream moves
182
+ * when it does: consumers import `@env`, and the alias is what penv writes.
183
+ */
184
+ readonly schemaFile?: string;
185
+ /**
186
+ * The variable-name prefixes a framework inlines into its client bundle —
187
+ * `NEXT_PUBLIC_`, `VITE_`.
188
+ *
189
+ * penv does not enforce these; the framework already does. Declaring them is
190
+ * what lets `doctor` catch the one mistake neither penv nor the framework can
191
+ * catch alone: a parameter meta declares `secret` whose name makes the
192
+ * framework ship it to a browser. To the framework the prefix *is* the intent,
193
+ * so only penv — holding both the policy and the name — can see the
194
+ * contradiction.
195
+ */
196
+ readonly publicPrefixes?: readonly string[];
197
+ /**
198
+ * Overrides the generated variable for a parameter, when a consumer demands a
199
+ * name the default transform would not produce — the WorkOS SDK reading
200
+ * `NEXT_PUBLIC_WORKOS_REDIRECT_URI`, a deploy target expecting `DATABASE_URL`
201
+ * spelled its way. One override bends the name for every consumer at once:
202
+ * `generate`, `push`, and the ambient mirror all read it. Collision-checked.
203
+ *
204
+ * Keys are parameter ids (`workos/redirect-uri`). When the schema module
205
+ * registers its shape (see {@link PenvSchemaShape}), the keys narrow to the
206
+ * parameters the schema actually declares — a typo'd id becomes a compile
207
+ * error instead of an override that silently never applies.
208
+ */
209
+ readonly override?: OverrideBlock;
210
+ /**
211
+ * Where each environment's encryption key lives. An environment with no entry
212
+ * has no key source, which is not the same as having no key — see `keys.ts`.
213
+ */
214
+ readonly keys?: Readonly<Record<string, KeyConfig>>;
215
+ }
216
+ /**
217
+ * Which destination store a value lands in. The two members are penv's own
218
+ * precedence axis wearing the destination's names: the unscoped default is the
219
+ * value every context falls back to (`repository`), an environment-scoped value
220
+ * belongs to exactly one (`environment`). GitHub resolves the two in penv's own
221
+ * order — environment over repository — so the cascade is reproduced by the
222
+ * destination's native mechanism rather than flattened at the boundary.
223
+ */
224
+ type SecretScope = {
225
+ readonly kind: "repository";
226
+ } | {
227
+ readonly kind: "environment";
228
+ readonly environment: string;
229
+ };
230
+ /** One secret as a value-withholding store reports it: a name and when it last changed, never a value. */
231
+ interface ProjectionSecret {
232
+ readonly name: string;
233
+ /** The destination's last-modified time, ISO 8601. Compared against penv's last-push time to catch a hand-edit. */
234
+ readonly updatedAt: string;
235
+ }
236
+ /**
237
+ * A provider whose store holds a resolved *projection* rather than penv records
238
+ * — generated variable names, both `.local` scopes skipped, plaintext the
239
+ * destination re-seals under its own custody. It declares
240
+ * `capabilities.holds: "projection"` and satisfies this contract instead of the
241
+ * seven-method record contract, which its store cannot honestly implement: a
242
+ * `read` that can never return a value would make every downstream check read
243
+ * an unreadable store as an empty one.
244
+ *
245
+ * penv resolves the tree; the projection receives it. `push` speaks this
246
+ * surface; `pull` materialises what `list` can honestly give — names and
247
+ * timestamps, never values.
248
+ */
249
+ interface ProjectionProvider {
250
+ readonly type: string;
251
+ readonly capabilities: ProviderCapabilities & {
252
+ readonly holds: "projection";
253
+ };
254
+ /**
255
+ * Confirms the destination is reachable before the first push. Every name is
256
+ * already judged up front, so a push never places half its secrets and then
257
+ * hits a reserved name; this closes the other pre-push gap — the destination
258
+ * being unreachable. GitHub through `gh` checks it is installed, authenticated,
259
+ * and can reach this repository's secrets, and refuses loudly rather than
260
+ * falling back. Resolves when it is safe to push.
261
+ */
262
+ verify(): Promise<void>;
263
+ /**
264
+ * Sends one value to the destination, which seals it under its own key. The
265
+ * value crosses in plaintext because a CI runner holds no penv key — penv's
266
+ * encryption stops at the projection and the destination's custody takes over.
267
+ */
268
+ push(name: string, value: string, scope: SecretScope): Promise<void>;
269
+ /**
270
+ * The names the destination holds at a scope, each with its last-modified
271
+ * time. Listing names is the one read a value-withholding store allows.
272
+ */
273
+ list(scope: SecretScope): Promise<ProjectionSecret[]>;
274
+ /**
275
+ * The destination's own name grammar, judged before the first PUT. The rules
276
+ * are the destination's (GitHub's reserved `GITHUB_` prefix, its charset, its
277
+ * case-insensitivity), so the provider owns the check — the CLI only insists
278
+ * it runs before anything is pushed, never mid-push.
279
+ */
280
+ checkNames?(refs: readonly ParameterRef[], config: PenvConfig): PenvErrorLike[];
281
+ /**
282
+ * Whether the destination-side target an environment's push lands in exists
283
+ * yet — a GitHub deployment environment, say. Implemented only where the
284
+ * destination has such a notion.
285
+ */
286
+ targetExists?(environment: string): Promise<boolean>;
287
+ /**
288
+ * Creates the destination-side target for an environment. Never called
289
+ * without an explicit go-ahead: the CLI prompts (or `--yes` pre-approves) and
290
+ * only then asks the provider, which stays non-interactive. Creation on an
291
+ * explicit answer, never a guess — a typo'd environment name must not summon
292
+ * infrastructure.
293
+ */
294
+ ensureTarget?(environment: string): Promise<void>;
295
+ }
296
+ /**
297
+ * The error shape {@link ProjectionProvider.checkNames} returns — structurally
298
+ * `PenvError`, stated as an interface so the contract does not force provider
299
+ * packages to subclass core's error class. It is also what a refusal thrown by a
300
+ * self-contained extension arrives as: `isPenvErrorLike` is the test penv runs
301
+ * on one, since the class it was built from is the extension's own copy.
302
+ */
303
+ interface PenvErrorLike extends Error {
304
+ readonly code: string;
305
+ readonly remedy?: string | undefined;
306
+ }
307
+
308
+ /**
309
+ * Named errors. Every message names the parameter and environment, says what is
310
+ * wrong, and says how to fix it. Never `Something went wrong`.
311
+ *
312
+ * A penv error also renders itself the way the CLI renders one. The CLI can
313
+ * format what it catches, but the refusals thrown by the application's bridge
314
+ * are caught by nobody — an app started without `penv run` prints them through
315
+ * Node's default uncaught-exception handler, which prints `stack`. So `stack`
316
+ * carries the refusal first and the remedy behind the same arrow every command
317
+ * prints, and the frames below it are the caller's, not penv's.
318
+ */
319
+
320
+ declare class PenvError extends Error {
321
+ readonly name: string;
322
+ /** A stable, machine-readable discriminator. */
323
+ readonly code: string;
324
+ /** What the user should do about it. */
325
+ readonly remedy: string | undefined;
326
+ /** The message alone, without the remedy the constructor folds into it. */
327
+ readonly summary: string;
328
+ constructor(code: string, message: string, remedy?: string);
329
+ /**
330
+ * Drops every frame from `below` upward, so the stack shows where the
331
+ * application called in rather than the path penv took inside itself. The
332
+ * caller names its own entry point; nothing here guesses which files are
333
+ * penv's.
334
+ */
335
+ hideFramesAbove(below: (...args: never[]) => unknown): this;
336
+ /** The refusal as every penv command prints it: the message, then the remedy. */
337
+ toString(): string;
338
+ }
2
339
 
3
340
  /**
4
341
  * The provider's own errors, extending penv's base so they print with a remedy and
@@ -45,17 +382,6 @@ declare class GithubUnavailableError extends PenvError {
45
382
  * learns GitHub vocabulary and the provider never parses config.
46
383
  */
47
384
 
48
- declare module "@penvhq/core" {
49
- interface ProviderConfigMap {
50
- "@penvhq/provider-github": {
51
- /**
52
- * The repository penv maps the projection onto — `owner/repo`. Left
53
- * unset, `gh` resolves it from the working directory.
54
- */
55
- readonly location?: string;
56
- };
57
- }
58
- }
59
385
  /** Builds the GitHub provider for one environment's declared destination. */
60
386
  declare function penvProviderFactory(context: ProviderFactoryContext): ProjectionProvider;
61
387