@hubble-ventures/infisicml 1.2.1 → 2.0.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +94 -94
  3. package/dist/aliases.d.ts +11 -5
  4. package/dist/aliases.d.ts.map +1 -1
  5. package/dist/aliases.js +20 -12
  6. package/dist/aliases.js.map +1 -1
  7. package/dist/ci-skip.d.ts +32 -15
  8. package/dist/ci-skip.d.ts.map +1 -1
  9. package/dist/ci-skip.js +48 -39
  10. package/dist/ci-skip.js.map +1 -1
  11. package/dist/commands/export-gha.d.ts +16 -0
  12. package/dist/commands/export-gha.d.ts.map +1 -1
  13. package/dist/commands/export-gha.js +48 -39
  14. package/dist/commands/export-gha.js.map +1 -1
  15. package/dist/commands/list.d.ts.map +1 -1
  16. package/dist/commands/list.js +17 -25
  17. package/dist/commands/list.js.map +1 -1
  18. package/dist/commands/paths.d.ts.map +1 -1
  19. package/dist/commands/paths.js +11 -10
  20. package/dist/commands/paths.js.map +1 -1
  21. package/dist/commands/run.d.ts.map +1 -1
  22. package/dist/commands/run.js +6 -3
  23. package/dist/commands/run.js.map +1 -1
  24. package/dist/commands/validate.d.ts.map +1 -1
  25. package/dist/commands/validate.js +1 -11
  26. package/dist/commands/validate.js.map +1 -1
  27. package/dist/github-env.js +1 -1
  28. package/dist/github-env.js.map +1 -1
  29. package/dist/include.d.ts +6 -43
  30. package/dist/include.d.ts.map +1 -1
  31. package/dist/include.js +11 -67
  32. package/dist/include.js.map +1 -1
  33. package/dist/index.d.ts +4 -3
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +4 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/manifest.d.ts +14 -35
  38. package/dist/manifest.d.ts.map +1 -1
  39. package/dist/manifest.js +26 -85
  40. package/dist/manifest.js.map +1 -1
  41. package/dist/pull.d.ts.map +1 -1
  42. package/dist/pull.js +11 -16
  43. package/dist/pull.js.map +1 -1
  44. package/dist/tree.d.ts +49 -0
  45. package/dist/tree.d.ts.map +1 -0
  46. package/dist/tree.js +190 -0
  47. package/dist/tree.js.map +1 -0
  48. package/package.json +4 -2
  49. package/schema/secrets.schema.json +49 -43
package/CHANGELOG.md CHANGED
@@ -4,6 +4,72 @@ All notable changes to `infisicml` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.0.0] - 2026-07-17
8
+
9
+ ### Changed (breaking — manifest format)
10
+
11
+ - **The manifest is now a `secrets` array of folders.** The flat `paths` +
12
+ `include` + `aliases` trio is replaced by a single `secrets` array that mirrors
13
+ your Infisical structure. Each of the three old fields was really an attribute
14
+ of a specific `(folder, key)` pair; the tree binds them together, so a key's
15
+ allowlist membership and its alias live in one place, provenance is explicit,
16
+ and the manifest reads like the vault. `secrets` is an array of folder objects
17
+ `{ name: [ ...contents ] }`; inside a folder's array, a **bare string** is a
18
+ plain key, an object with a **string value** `{ SOURCE: "TARGET" }` is an
19
+ alias, and an object with an **array value** `{ sub: [ ... ] }` is a **real
20
+ subfolder** (nesting mirrors the vault tree — `sub` under `posthog` reads
21
+ `/posthog/sub`). Value type discriminates alias vs subfolder, so subfolders
22
+ need no leading `/`. Fan a key out to several targets by repeating the alias,
23
+ one target each.
24
+
25
+ ```jsonc
26
+ // before (v1) // after (v2)
27
+ { {
28
+ "paths": ["posthog"], "secrets": [
29
+ "aliases": { { "posthog": [
30
+ "POSTHOG_PROJECT_TOKEN": { "POSTHOG_PROJECT_TOKEN": "VITE_POSTHOG_KEY" }
31
+ "VITE_POSTHOG_KEY" ] }
32
+ }, ]
33
+ "include": ["POSTHOG_PROJECT_TOKEN"] }
34
+ }
35
+ ```
36
+
37
+ - **Default-deny everywhere.** There is no wildcard: every emitted key must be
38
+ named, and an empty folder array is a schema error. The tree *is* the
39
+ allowlist, so the separate `include` field is **removed** — a folder emits
40
+ exactly the keys it declares, from that folder (provenance-aware selection).
41
+ - **`fetch: "keys"` no longer needs an allowlist.** Because the tree always
42
+ names every canonical key (an alias entry's key *is* the real vault key),
43
+ key mode requests those names directly — the v1 reverse-mapping from alias
44
+ target to source is gone, and `keys` is always satisfiable. The
45
+ `fetch`-requires-`include` cross-check is removed.
46
+ - **Optional keys stay env-scoped** via `environments.<slug>.optionalKeys`
47
+ (unchanged), now referencing declared canonical key names.
48
+ - **Profiles** carry an alternate `secrets` array (replacing the base tree when
49
+ `--profile` is set) plus an optional `fetch`; `profiles.<name>.paths` /
50
+ `.include` are gone.
51
+ - **Programmatic API.** `resolvePaths` / `resolveInclude` / `applyInclude` /
52
+ `resolveFetchKeys` / `checkFetchIncludeConsistency` / `fetchManifestSecrets` /
53
+ `selectEmittedSecrets` are replaced by `resolveCompiledFolders`, `compileTree`,
54
+ `fetchCompiledFolders`, and `materializeSecrets` (the latter applies aliases
55
+ and enforces missing keys **per folder**, before merging, so a key name shared
56
+ across folders keeps each folder's value and miss); `enforceIncludeKnown` is
57
+ renamed `enforceKnownKeys`. New exports: `compileTree`, `treeSchema`,
58
+ `materializeSecrets`, and the `CompiledFolder` / `CompiledKey` / `FolderEntry` /
59
+ `FolderArray` / `SecretsTree` / `FolderSecrets` types.
60
+ - **Schema** rewritten around the `secrets` array and published at `@2`.
61
+
62
+ ### Migration
63
+
64
+ Convert each `secrets.json`: replace `paths`/`include`/`aliases` with a
65
+ `secrets` array of `{ folder: [ ...keys ] }` objects; list plain keys as bare
66
+ strings and each `aliases` source as a `{ SOURCE: "TARGET" }` object (repeat for
67
+ several targets); keep only the keys you want (the array is the allowlist).
68
+ Point `$schema` at `…/infisicml@2/…`.
69
+ Behavior note: an alias entry now emits **both** its canonical name and the
70
+ target(s) — v1 could suppress the canonical via `include`; v2 does not (the
71
+ security boundary is default-deny provenance, not alias suppression).
72
+
7
73
  ## [1.2.1] - 2026-07-17
8
74
 
9
75
  ### Changed (BREAKING)
@@ -97,7 +163,6 @@ changes to either ship only in a new major.
97
163
  - **Published JSON Schema** for `secrets.json`, served from
98
164
  `https://cdn.jsdelivr.net/npm/@hubble-ventures/infisicml@1/schema/secrets.schema.json`.
99
165
 
100
- [1.2.1]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.2.1
101
166
  [1.2.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.2.0
102
167
  [1.1.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.1.0
103
168
  [1.0.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.0.0
package/README.md CHANGED
@@ -36,29 +36,43 @@ Requires the [`infisical` CLI](https://infisical.com/docs/cli/overview) for loca
36
36
  projectIdEnvFile: ".env.infisical", // provides INFISICAL_PROJECT_ID
37
37
  discovery: { roots: ["apps", "services"], packages: [{ id: "postgres", dir: "infra/postgres" }] },
38
38
  auth: { oidcAudience: "https://github.com/your-org" },
39
- hooks: { advertiseKeys: [{ envVar: "INFISCML_FLY_KEYS", scope: "runtime" }] },
39
+ hooks: { advertiseKeys: [{ envVar: "INFISICML_FLY_KEYS", scope: "runtime" }] },
40
40
  });
41
41
  ```
42
42
 
43
- 2. Add a `secrets.json` next to each package:
43
+ 2. Add a `secrets.json` next to each package. `secrets` is an **array of
44
+ folders** mirroring your Infisical structure; each folder is
45
+ `{ "name": [ ...contents ] }`. Inside a folder's array, a **bare string** is a
46
+ plain key, an object with a **string value** `{ "SOURCE": "TARGET" }` is an
47
+ alias, and an object with an **array value** `{ "sub": [ ... ] }` is a
48
+ subfolder. Every emitted key is named (default-deny; there is no wildcard).
44
49
 
45
50
  ```jsonc
46
51
  {
47
- "$schema": "https://cdn.jsdelivr.net/npm/@hubble-ventures/infisicml@1/schema/secrets.schema.json",
48
- "paths": ["clerk", "posthog"],
52
+ "$schema": "https://cdn.jsdelivr.net/npm/@hubble-ventures/infisicml@2/schema/secrets.schema.json",
49
53
  "output": ".env", // written next to this manifest; defaults to .env.secrets
50
- "aliases": {
51
- // One canonical vault key → several prefixed copies in a single output.
52
- "GOOGLE_MAPS_API_KEY": [
53
- "EXPO_PUBLIC_GOOGLE_MAPS_API_KEY",
54
- "VITE_GOOGLE_MAPS_API_KEY"
55
- ],
56
- // A single target is also fine (string form).
57
- "CLERK_PUBLISHABLE_KEY": "VITE_CLERK_PUBLISHABLE_KEY"
58
- }
54
+ "secrets": [
55
+ // Canonical vault key → the prefixed name a build tool expects.
56
+ { "clerk": [{ "CLERK_PUBLISHABLE_KEY": "VITE_CLERK_PUBLISHABLE_KEY" }] },
57
+ // Fan one canonical key out to several prefixes: repeat the alias, one
58
+ // target each (an array value would mean a subfolder, not multi-target).
59
+ { "google": [
60
+ { "GOOGLE_MAPS_API_KEY": "EXPO_PUBLIC_GOOGLE_MAPS_API_KEY" },
61
+ { "GOOGLE_MAPS_API_KEY": "VITE_GOOGLE_MAPS_API_KEY" }
62
+ ] },
63
+ { "posthog": [
64
+ "POSTHOG_PROJECT_TOKEN",
65
+ // A real Infisical subfolder at /posthog/eu.
66
+ { "eu": ["POSTHOG_EU_HOST"] }
67
+ ] }
68
+ ]
59
69
  }
60
70
  ```
61
71
 
72
+ An alias entry emits **both** its canonical name and the target. Keys you
73
+ don't name are never pulled, so a client package can share a vendor folder yet
74
+ emit only the public key — the server secret is simply undeclared.
75
+
62
76
  3. Pull secrets into gitignored `.env.secrets` files:
63
77
 
64
78
  ```bash
@@ -75,9 +89,9 @@ See [`examples/`](./examples) for a fuller config and manifest.
75
89
  |---|---|
76
90
  | `pull [ids...]` | Write `.env.secrets` locally (uses `infisical export`) |
77
91
  | `export-gha <id>` | Mask + append secrets to `GITHUB_ENV` (REST API + GitHub OIDC) |
78
- | `list` | Show every manifest and its Infisical paths |
92
+ | `list` | Show every manifest's folder tree and declared keys |
79
93
  | `validate` | Validate every `secrets.json` against the schema |
80
- | `paths <id>` | Print resolved paths (`--comma` for scripting) |
94
+ | `paths <id>` | Print resolved folder paths (`--comma` for scripting) |
81
95
  | `run <id> -- <cmd...>` | Thin `infisical run` wrapper (prefer `pull` + `.env.secrets`) |
82
96
 
83
97
  Common flags: `--env`, `--profile`, `--force`, `--here` (pull the cwd package), `--turbo` (always write, for Turbo caching).
@@ -132,89 +146,75 @@ Load `.env.secrets` however your dev runtime already loads env files. See [`exam
132
146
 
133
147
  ## Concepts
134
148
 
135
- - **Manifest (`secrets.json`)** — per package: `paths` (vault folders), optional `profiles`, `aliases`, `include`, `fetch`, `ci`, `environments`, `output`.
149
+ - **Manifest (`secrets.json`)** — per package: `secrets` (an array of folders → keys), optional `profiles`, `fetch`, `ci`, `environments`, `output`.
136
150
  - **Output file** — `output` sets the written filename (default `.env.secrets`), placed **next to the manifest**. Because each package owns its own `secrets.json`, this gives a distinct file per package: a root manifest with `"output": ".env.local"` writes the repo-root `.env.local`; a manifest in `apps/backend` with `"output": ".env"` writes `apps/backend/.env`. `output` is a filename only (no path separators) — to target a different directory, place the manifest in that directory.
137
- - **Profiles** — named path sets that *replace* base `paths` when `--profile` is set. Base paths are runtime secrets; profile-only paths (e.g. `fly`) are deploy/release credentials.
138
- - **Aliases** — copy a canonical secret to extra tool-specific names. Each source maps to **one target (string) or many (array)**, so a single vault key can fan out to every framework prefix (`EXPO_PUBLIC_*`, `VITE_*`, `NEXT_PUBLIC_*`) in one output. Real secrets of a target name always win; the operation is idempotent and never overwrites.
139
- - **Include (key allowlist)** — `include` emits **only** the listed keys from whatever the folders yielded (default-deny key selection). Omit it and every key is emitted (unchanged). See [Key selection](#key-selection-include) below.
140
- - **Fetch mode** — `fetch: "keys"` requests only the keys `include` resolves to, so the vault never transmits the rest (wire-level least privilege). Default `"folder"` reads whole folders and filters locally. See [Wire-level least privilege](#wire-level-least-privilege-fetch-keys) below.
151
+ - **Secrets tree** — an **array of folders** mirroring your Infisical vault; each folder is `{ "name": [ ...contents ] }`. Inside the array: a bare string is a plain key, an object with a string value `{ "SOURCE": "TARGET" }` is an alias, and an object with an array value `{ "sub": [ ... ] }` is a real subfolder. Every emitted key is named — there is no wildcard, so a folder never leaks a key you didn't declare (default-deny). See [Key selection](#key-selection-the-tree-is-the-allowlist) below.
152
+ - **Profiles** — named alternate trees that *replace* the base `secrets` when `--profile` is set. The base tree is runtime secrets; a profile can add deploy/release folders (e.g. `fly`).
153
+ - **Aliases** — an alias entry `{ "SOURCE": "TARGET" }` maps a canonical vault key to one target. To fan a single key out to every framework prefix (`EXPO_PUBLIC_*`, `VITE_*`, `NEXT_PUBLIC_*`), repeat the entry one target each (an array value means a subfolder). Both the canonical name and each target are emitted. Real secrets of a target name always win; the operation is idempotent and never overwrites.
154
+ - **Fetch mode** — `fetch: "keys"` requests only the declared keys, so the vault never transmits the rest (wire-level least privilege). Default `"folder"` reads whole folders and selects the declared keys locally. See [Wire-level least privilege](#wire-level-least-privilege-fetch-keys) below.
141
155
  - **CI skip/stub** — `ci.skipWhenEnv` skips the pull when all listed vars are already set in CI; `ci.stubInCi` always stubs in CI. Both write a `.env.secrets` from `process.env` instead of calling Infisical.
142
- - **Optional keys** — `environments.<slug>.optionalKeys` downgrade a missing key to a `::notice::` in `export-gha` instead of a failure.
156
+ - **Optional keys** — `environments.<slug>.optionalKeys` downgrade a missing declared key to a `::notice::` instead of a failure.
143
157
  - **Advertise-keys hooks** — publish runtime key *names* (never values) to `GITHUB_ENV` for deploy forwarding.
144
158
 
145
- ### Key selection (`include`)
159
+ ### Key selection (the tree is the allowlist)
146
160
 
147
161
  Vaults are often organized by **vendor, not by access scope** — a single `/stripe` folder
148
162
  holds both a server secret and a client-public key:
149
163
 
150
164
  ```
151
- /stripe → STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY
165
+ /stripe → STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY
152
166
  ```
153
167
 
154
- A web/mobile client needs **only** the publishable key. With folder-only selection it would
155
- be forced to pull the whole folderwriting `STRIPE_SECRET_KEY` into a Vite/Expo client
156
- build's env (and, via the CI action, into `GITHUB_ENV` for jobs that shouldn't see it).
157
-
158
- `include` is a **default-deny allowlist**: only the listed env var names are emitted.
168
+ A web/mobile client needs **only** the publishable key. Because the tree is default-deny, you
169
+ name that one key and nothing else is pulled — `STRIPE_SECRET_KEY` is simply never declared,
170
+ so it can't reach a Vite/Expo client build's env (or, via the CI action, `GITHUB_ENV`).
159
171
 
160
172
  ```jsonc
161
173
  // apps/web/secrets.json — a client package
162
174
  {
163
- "paths": ["stripe", "google", "vercel"],
164
- "aliases": {
165
- "STRIPE_PUBLISHABLE_KEY": ["EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY"],
166
- "GOOGLE_MAPS_API_KEY": ["EXPO_PUBLIC_GOOGLE_MAPS_API_KEY"]
167
- },
168
- "include": [
169
- "EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY",
170
- "EXPO_PUBLIC_GOOGLE_MAPS_API_KEY",
171
- "EXPO_PUBLIC_VERCEL_AUTOMATION_BYPASS_SECRET"
175
+ "secrets": [
176
+ { "stripe": [{ "STRIPE_PUBLISHABLE_KEY": "EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY" }] },
177
+ { "google": [{ "GOOGLE_MAPS_API_KEY": "EXPO_PUBLIC_GOOGLE_MAPS_API_KEY" }] },
178
+ { "vercel": ["VERCEL_AUTOMATION_BYPASS_SECRET"] }
172
179
  ]
173
180
  }
174
181
  ```
175
182
 
176
- The written `.env.secrets` (and, in CI, `GITHUB_ENV`) contains **only** those three keys
177
- server secrets in the same folders are never emitted.
178
-
179
- - **Ordering** — folders are pulled, then `aliases` are applied, then `include` filters the
180
- **final** set of names. So aliasing a canonical key to a public name and listing only the
181
- public name in `include` drops the canonical key (and every other folder key). An alias
182
- whose source isn't in `include` still emits its target.
183
- - **Names, not paths**`include` is a flat list of env var names; key names must be unique
184
- across a package's folders.
185
- - **Unknown keys** a name in `include` that no folder produced is an error (fail the pull /
186
- CI step), **unless** it's also in `environments.<slug>.optionalKeys`, which downgrades it to
187
- a `::notice::`. Enforced the same way in `pull` and `export-gha`.
188
- - **Profiles** — a profile may set its own `include`, which **replaces** the root `include`
189
- for that profile (like `profiles.<name>.paths`). A profile without `include` inherits the
190
- root one.
191
- - **Backward compatible** — omit `include` and behavior is unchanged (every key is emitted).
183
+ The written `.env.secrets` (and, in CI, `GITHUB_ENV`) contains **only** the declared keys and
184
+ their alias targets — server secrets in the same folders are never emitted.
185
+
186
+ - **Provenance** — a key is emitted from the folder it's declared under, and only that folder.
187
+ Selection is per-folder, so the same name in two folders doesn't cross-contaminate.
188
+ - **Alias emits both names** an alias entry copies the canonical key to its target; *both*
189
+ the canonical name and the target land in the output. (To emit only a prefixed name, don't
190
+ declare the canonical onebut the canonical publishable key is usually harmless.)
191
+ - **Unknown keys** — a declared key that its folder didn't produce is an error (fail the pull /
192
+ CI step), **unless** it's in `environments.<slug>.optionalKeys`, which downgrades it to a
193
+ `::notice::`. Enforced the same way in `pull` and `export-gha`.
194
+ - **Profiles** a profile supplies its own `secrets`, which **replaces** the root tree for that
195
+ profile.
192
196
 
193
197
  ### Wire-level least privilege (`fetch: "keys"`)
194
198
 
195
- `include` on its own is a **post-fetch filter**: infisicml pulls each whole folder, then drops
196
- the unwanted keys before writing the output. The excluded values still travel over the wire
197
- into your machine / the CI runner — they're just never written to `.env` or `GITHUB_ENV`. For
198
- most setups that's fine. When you need the vault to **not even transmit** the secrets you
199
- don't use, set `fetch: "keys"`:
199
+ By default (`fetch: "folder"`) infisicml pulls each whole folder, then selects the declared
200
+ keys before writing the output. The undeclared values still travel over the wire into your
201
+ machine / the CI runner — they're just never written to `.env` or `GITHUB_ENV`. For most
202
+ setups that's fine. When you need the vault to **not even transmit** the secrets you don't
203
+ use, set `fetch: "keys"`:
200
204
 
201
205
  ```jsonc
202
206
  // apps/web/secrets.json — client package, strict wire-level least privilege
203
207
  {
204
- "paths": ["stripe", "google"],
205
208
  "fetch": "keys",
206
- "aliases": {
207
- "STRIPE_PUBLISHABLE_KEY": ["EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY"]
208
- },
209
- "include": [
210
- "EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY",
211
- "GOOGLE_MAPS_API_KEY"
209
+ "secrets": [
210
+ { "stripe": [{ "STRIPE_PUBLISHABLE_KEY": "EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY" }] },
211
+ { "google": ["GOOGLE_MAPS_API_KEY"] }
212
212
  ]
213
213
  }
214
214
  ```
215
215
 
216
- In `keys` mode infisicml emits **only** the keys `include` resolves to. Where the guarantee
217
- bites depends on the lane:
216
+ In `keys` mode infisicml reads **only** the declared keys. Where the guarantee bites depends on
217
+ the lane:
218
218
 
219
219
  - **CI (`export-gha`)** — true **wire-level** least privilege: each key is fetched with the
220
220
  single-secret REST endpoint (`GET /api/v3/secrets/raw/{name}`), so the other keys in
@@ -225,23 +225,18 @@ bites depends on the lane:
225
225
  once and selects the keys. It narrows what's **written to disk**, not what the vault
226
226
  transmits — on your own machine, where folder mode already lands.
227
227
 
228
- - **Requires `include`**key mode fetches exactly what `include` names, so an allowlist is
229
- mandatory (root or the active profile). `validate` and the pull/CI step both enforce this.
230
- - **Aliases are reverse-mapped** — `include` names the *final* (post-alias) keys, so infisicml
231
- fetches the canonical vault source behind each alias target. Above, listing
232
- `EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY` fetches the canonical `STRIPE_PUBLISHABLE_KEY`.
233
- - **Imports still resolve** — a key surfaced into a `paths` folder via an Infisical import is
234
- fetched in `keys` mode too (the per-key read follows imports but returns only that one
235
- secret), so enabling `keys` never silently drops an import-backed key.
236
- - **Same emit result** everything after the fetch (aliases, `include` filtering, unknown-key
237
- enforcement, `optionalKeys`) is identical to folder mode; only what's read narrows.
238
- - **Cost** — in CI, one request **per key** instead of one per folder; the `paths` list becomes
228
+ - **No allowlist needed** the tree already names every canonical key (an alias entry's key
229
+ *is* the real vault key), so `keys` mode requests exactly those names directly — no reverse
230
+ mapping, and `keys` is always satisfiable.
231
+ - **Imports still resolve** a key surfaced into a folder via an Infisical import is fetched in
232
+ `keys` mode too (the per-key read follows imports but returns only that one secret), so
233
+ enabling `keys` never silently drops an import-backed key.
234
+ - **Same emit result** everything after the fetch (aliases, unknown-key enforcement,
235
+ `optionalKeys`) is identical to folder mode; only what's read narrows.
236
+ - **Cost** in CI, one request **per key** instead of one per folder; the folder list becomes
239
237
  advisory (`infisicml paths` notes this).
240
- - **Profiles** — a profile may set its own `fetch`, which **replaces** the root value (like
241
- `paths` / `include`). A deploy profile can stay in `folder` mode while the runtime default
242
- is `keys`, or vice versa.
243
- - **Backward compatible** — omit `fetch` (or set `"folder"`) and behavior is byte-for-byte
244
- unchanged.
238
+ - **Profiles** — a profile may set its own `fetch`, which **replaces** the root value. A deploy
239
+ profile can stay in `folder` mode while the runtime default is `keys`, or vice versa.
245
240
 
246
241
  ### Non-secret defaults (`.env.sample`)
247
242
 
@@ -278,19 +273,24 @@ for (const manifest of discoverManifests(config)) {
278
273
 
279
274
  ## Releasing
280
275
 
281
- Releases are cut by pushing a semver tag; [`.github/workflows/release.yml`](./.github/workflows/release.yml) does the rest:
276
+ **Releases are automatic you don't tag anything.** Bump the version in a PR
277
+ and merging it to `main` cuts the release:
282
278
 
283
279
  ```bash
284
- npm version 1.2.3 --no-git-tag-version
285
- git commit -am "release: v1.2.3"
286
- git tag v1.2.3 && git push origin main --tags
280
+ npm run release -- minor # or patch / major / 2.1.0 — bumps package.json + rebuilds dist
281
+ # edit CHANGELOG.md: add the new version's section
282
+ git commit -am "release: v2.1.0"
283
+ # open a PR, merge it → release fires
287
284
  ```
288
285
 
289
- The workflow builds + tests the tagged commit, publishes to npm via **trusted
290
- publishing** (OIDC no `NPM_TOKEN` secret, provenance attached automatically),
291
- creates a GitHub Release from the matching [`CHANGELOG.md`](./CHANGELOG.md) section, and
292
- moves the floating `v1` tag to the release commit. The publish step is idempotent — a
293
- version already on npm is skipped.
286
+ On the push to `main`, [`.github/workflows/release.yml`](./.github/workflows/release.yml)
287
+ reads `package.json`; if that version isn't tagged yet it builds + tests, **creates and
288
+ pushes the `vX.Y.Z` tag**, publishes to npm via **trusted publishing** (OIDC no
289
+ `NPM_TOKEN` secret, provenance attached automatically), creates a GitHub Release from the
290
+ matching [`CHANGELOG.md`](./CHANGELOG.md) section, and moves the floating major tag (e.g.
291
+ `v2`). A main push that doesn't bump the version is a no-op, and the publish step is
292
+ idempotent — a version already on npm is skipped. Manually pushing a `vX.Y.Z` tag still
293
+ works if you ever need it.
294
294
 
295
295
  > **First publish (one time).** The `@hubble-ventures` npm **organization** must exist,
296
296
  > and npm trusted publishing can only be configured against an *existing* package — so
@@ -304,8 +304,8 @@ version already on npm is skipped.
304
304
  > (`publishConfig.access: public` in `package.json` enforces this even if the flag is
305
305
  > omitted.) Then, in the package's npm settings, add a **trusted publisher** for
306
306
  > `hubble-ventures/infisicml` → workflow `release.yml`, with **no environment** (the release
307
- > job sets none — the OIDC claims must match). Every subsequent tag push publishes
308
- > automatically with no token.
307
+ > job sets none — the OIDC claims must match). Every subsequent version bump merged to
308
+ > `main` publishes automatically with no token.
309
309
 
310
310
  ## Design
311
311
 
package/dist/aliases.d.ts CHANGED
@@ -1,20 +1,26 @@
1
- import type { SecretsManifest } from "./manifest.js";
1
+ import type { CompiledFolder } from "./tree.js";
2
2
  export type ResolvedAlias = {
3
3
  source: string;
4
4
  targets: string[];
5
5
  };
6
- /** Normalize the manifest's alias map to a list of {source, targets[]}. */
7
- export declare function resolveAliases(manifest: SecretsManifest): ResolvedAlias[];
6
+ /**
7
+ * Collect the alias `source -> targets` pairs from the compiled folders. Each
8
+ * `aliased` entry in the tree names a canonical vault key (the source) and the
9
+ * extra env var name(s) a build/runtime expects (the targets).
10
+ */
11
+ export declare function resolveAliases(folders: CompiledFolder[]): ResolvedAlias[];
8
12
  /**
9
13
  * Copy each aliased source secret's value to its target env var name(s).
10
14
  *
11
15
  * Used wherever the CLI materializes secrets (CI `export-gha`, local
12
16
  * `.env.secrets` pull) so the conventional, tool-specific name each deployment
13
- * expects is always present — see the `aliases` doc in manifest.ts.
17
+ * expects is always present — the vault names a secret once (e.g. /posthog
18
+ * exposes POSTHOG_PROJECT_TOKEN), but build tools inline it by a tool-specific
19
+ * name (Vite reads VITE_*, Next reads NEXT_PUBLIC_*).
14
20
  *
15
21
  * Returns a new object. An absent source is skipped, and an existing target (a
16
22
  * real secret of that name) is never overwritten, so real values win over
17
23
  * aliases and the operation is idempotent.
18
24
  */
19
- export declare function applyAliases(merged: Record<string, string>, manifest: SecretsManifest): Record<string, string>;
25
+ export declare function applyAliases(merged: Record<string, string>, folders: CompiledFolder[]): Record<string, string>;
20
26
  //# sourceMappingURL=aliases.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"aliases.d.ts","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAElE,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa,EAAE,CAOzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,QAAQ,EAAE,eAAe,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB"}
1
+ {"version":3,"file":"aliases.d.ts","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,aAAa,EAAE,CAUzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,EAAE,cAAc,EAAE,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB"}
package/dist/aliases.js CHANGED
@@ -1,27 +1,35 @@
1
- /** Normalize the manifest's alias map to a list of {source, targets[]}. */
2
- export function resolveAliases(manifest) {
3
- const aliases = manifest.aliases;
4
- if (!aliases)
5
- return [];
6
- return Object.entries(aliases).map(([source, value]) => ({
7
- source,
8
- targets: Array.isArray(value) ? value : [value],
9
- }));
1
+ /**
2
+ * Collect the alias `source -> targets` pairs from the compiled folders. Each
3
+ * `aliased` entry in the tree names a canonical vault key (the source) and the
4
+ * extra env var name(s) a build/runtime expects (the targets).
5
+ */
6
+ export function resolveAliases(folders) {
7
+ const out = [];
8
+ for (const folder of folders) {
9
+ for (const key of folder.keys) {
10
+ if (key.aliases.length > 0) {
11
+ out.push({ source: key.key, targets: key.aliases });
12
+ }
13
+ }
14
+ }
15
+ return out;
10
16
  }
11
17
  /**
12
18
  * Copy each aliased source secret's value to its target env var name(s).
13
19
  *
14
20
  * Used wherever the CLI materializes secrets (CI `export-gha`, local
15
21
  * `.env.secrets` pull) so the conventional, tool-specific name each deployment
16
- * expects is always present — see the `aliases` doc in manifest.ts.
22
+ * expects is always present — the vault names a secret once (e.g. /posthog
23
+ * exposes POSTHOG_PROJECT_TOKEN), but build tools inline it by a tool-specific
24
+ * name (Vite reads VITE_*, Next reads NEXT_PUBLIC_*).
17
25
  *
18
26
  * Returns a new object. An absent source is skipped, and an existing target (a
19
27
  * real secret of that name) is never overwritten, so real values win over
20
28
  * aliases and the operation is idempotent.
21
29
  */
22
- export function applyAliases(merged, manifest) {
30
+ export function applyAliases(merged, folders) {
23
31
  const out = { ...merged };
24
- for (const { source, targets } of resolveAliases(manifest)) {
32
+ for (const { source, targets } of resolveAliases(folders)) {
25
33
  const value = merged[source];
26
34
  if (value === undefined)
27
35
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"aliases.js","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAIA,2EAA2E;AAC3E,MAAM,UAAU,cAAc,CAAC,QAAyB;IACtD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACjC,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QACvD,MAAM;QACN,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;KAChD,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA8B,EAC9B,QAAyB;IAEzB,MAAM,GAAG,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1B,KAAK,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7B,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS;gBAAE,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;QACrD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"aliases.js","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAyB;IACtD,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAC9B,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA8B,EAC9B,OAAyB;IAEzB,MAAM,GAAG,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1B,KAAK,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7B,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS;gBAAE,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;QACrD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/dist/ci-skip.d.ts CHANGED
@@ -1,26 +1,43 @@
1
- import { type SecretsManifest } from "./manifest.js";
1
+ import type { SecretsManifest } from "./manifest.js";
2
2
  import type { SecretsProvider } from "./providers/types.js";
3
+ import type { CompiledFolder } from "./tree.js";
3
4
  export type { SecretsProvider };
5
+ /** A folder's declared keys paired with the values that folder actually held. */
6
+ export type FolderSecrets = {
7
+ folder: CompiledFolder;
8
+ /** Declared keys present in this folder (canonical, pre-alias). */
9
+ selected: Record<string, string>;
10
+ };
4
11
  export declare function mergeFolderSecrets(chunks: Record<string, string>[]): Record<string, string>;
5
- export declare function fetchSecretsForPaths(provider: SecretsProvider, envName: string, paths: string[]): Promise<Record<string, string>>;
6
12
  /**
7
- * `fetch: "keys"` counterpart to {@link fetchSecretsForPaths}: request only the
8
- * given canonical keys from each folder and merge. A key absent from one folder
9
- * is simply not returned by that folder; the merge (last folder wins, matching
10
- * the folder path) collects it from whichever folder holds it.
13
+ * Fetch each compiled folder and select *that folder's* declared keys from it,
14
+ * honoring the resolved `fetch` mode. `keys` mode requests only the declared
15
+ * keys per folder (the tree names the exact canonical vault keys, so no
16
+ * allowlist reverse-map is needed the alias *source* is the real key);
17
+ * `folder` mode reads the whole folder and picks the declared keys locally.
18
+ *
19
+ * Returns one {@link FolderSecrets} per folder so downstream steps keep folder
20
+ * provenance: two folders declaring the same key name are distinct entries here,
21
+ * and only collapse (last-wins) at the final merge — see {@link materializeSecrets}.
11
22
  */
12
- export declare function fetchSecretsForKeys(provider: SecretsProvider, envName: string, paths: string[], keys: string[]): Promise<Record<string, string>>;
23
+ export declare function fetchCompiledFolders(provider: SecretsProvider, envName: string, folders: CompiledFolder[], fetchMode: "folder" | "keys"): Promise<FolderSecrets[]>;
13
24
  /**
14
- * Fetch a manifest's secrets for the given folder set, honoring its resolved
15
- * `fetch` mode: `keys` requests only the canonical keys `include` resolves to
16
- * (least privilege at the wire), `folder` reads whole folders. `keys` requires
17
- * an `include` allowlist the whole point is to name exactly what to fetch.
25
+ * Turn per-folder fetched secrets into the final emit map, preserving folder
26
+ * provenance:
27
+ *
28
+ * - **Missing keys are checked per folder/key pair, before merging.** A key
29
+ * declared in `/a` but absent from `/a` is unknown even if a same-named key in
30
+ * `/b` was produced — so a genuine miss can't be masked by another folder.
31
+ * - **Aliases expand from the folder-local value.** `/a`'s `TOKEN -> A_TOKEN`
32
+ * uses `/a`'s `TOKEN`, and `/b`'s `TOKEN -> B_TOKEN` uses `/b`'s — the flat
33
+ * merge (which keeps only one `TOKEN`) can no longer route one folder's secret
34
+ * to another folder's alias.
18
35
  *
19
- * Callers pass an explicit `paths` subset (export-gha splits runtime vs
20
- * deploy-only) rather than re-deriving it, so the runtime/deploy provenance the
21
- * caller already computed is preserved.
36
+ * Folder-local aliased maps then merge in tree order (last-wins on a genuine
37
+ * cross-folder name collision). A declared key absent from its folder fails
38
+ * unless its name is in `optionalKeys` for the environment.
22
39
  */
23
- export declare function fetchManifestSecrets(provider: SecretsProvider, envName: string, paths: string[], manifest: SecretsManifest, profile?: string): Promise<Record<string, string>>;
40
+ export declare function materializeSecrets(folderSecrets: FolderSecrets[], optionalKeys: string[]): Record<string, string>;
24
41
  export declare function isCi(): boolean;
25
42
  export declare function shouldSkipInfisicalPull(manifest: SecretsManifest, force: boolean): boolean;
26
43
  export declare function keysForCiStub(manifest: SecretsManifest): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"ci-skip.d.ts","sourceRoot":"","sources":["../src/ci-skip.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAMjC;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAMjC;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,eAAe,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAuBjC;AAED,wBAAgB,IAAI,IAAI,OAAO,CAG9B;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,OAAO,GACb,OAAO,CAgBT;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,EAAE,CAEjE"}
1
+ {"version":3,"file":"ci-skip.d.ts","sourceRoot":"","sources":["../src/ci-skip.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,iFAAiF;AACjF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,EAAE,EACzB,SAAS,EAAE,QAAQ,GAAG,MAAM,GAC3B,OAAO,CAAC,aAAa,EAAE,CAAC,CAe1B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,aAAa,EAAE,EAC9B,YAAY,EAAE,MAAM,EAAE,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED,wBAAgB,IAAI,IAAI,OAAO,CAG9B;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,OAAO,GACb,OAAO,CAgBT;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,EAAE,CAEjE"}