@hubble-ventures/infisicml 1.2.1 → 2.1.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 (61) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.md +123 -118
  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 +52 -43
  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 +15 -14
  20. package/dist/commands/paths.js.map +1 -1
  21. package/dist/commands/pull.d.ts.map +1 -1
  22. package/dist/commands/pull.js +12 -9
  23. package/dist/commands/pull.js.map +1 -1
  24. package/dist/commands/run.d.ts.map +1 -1
  25. package/dist/commands/run.js +10 -7
  26. package/dist/commands/run.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +3 -13
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/config.d.ts +4 -3
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js.map +1 -1
  33. package/dist/github-env.js +1 -1
  34. package/dist/github-env.js.map +1 -1
  35. package/dist/hooks.d.ts +2 -2
  36. package/dist/hooks.js +2 -2
  37. package/dist/include.d.ts +6 -43
  38. package/dist/include.d.ts.map +1 -1
  39. package/dist/include.js +11 -67
  40. package/dist/include.js.map +1 -1
  41. package/dist/index.d.ts +5 -4
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -4
  44. package/dist/index.js.map +1 -1
  45. package/dist/manifest.d.ts +59 -35
  46. package/dist/manifest.d.ts.map +1 -1
  47. package/dist/manifest.js +95 -86
  48. package/dist/manifest.js.map +1 -1
  49. package/dist/pull.d.ts.map +1 -1
  50. package/dist/pull.js +11 -16
  51. package/dist/pull.js.map +1 -1
  52. package/dist/registry.d.ts +28 -5
  53. package/dist/registry.d.ts.map +1 -1
  54. package/dist/registry.js +38 -13
  55. package/dist/registry.js.map +1 -1
  56. package/dist/tree.d.ts +49 -0
  57. package/dist/tree.d.ts.map +1 -0
  58. package/dist/tree.js +190 -0
  59. package/dist/tree.js.map +1 -0
  60. package/package.json +5 -2
  61. package/schema/secrets.schema.json +49 -43
package/CHANGELOG.md CHANGED
@@ -4,6 +4,107 @@ 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.1.0] - 2026-07-18
8
+
9
+ ### Added
10
+
11
+ - **YAML manifests, as the primary format.** Each package's manifest may now be
12
+ `secrets.yaml`, `secrets.yml`, or `secrets.json` — the same schema either way.
13
+ YAML is the default and reads best for hand-authored manifests; JSON stays
14
+ fully supported (handy for generated manifests). Discovery prefers YAML, but a
15
+ directory containing **more than one** manifest file is a **hard error** rather
16
+ than a silent pick — a stale extra manifest must never change which secret tree
17
+ a non-interactive lane (`export-gha` → `GITHUB_ENV`) writes. No migration is
18
+ required — existing `secrets.json` files keep working unchanged.
19
+
20
+ ```yaml
21
+ # secrets.yaml
22
+ secrets:
23
+ - clerk:
24
+ - CLERK_PUBLISHABLE_KEY: VITE_CLERK_PUBLISHABLE_KEY
25
+ - posthog:
26
+ - POSTHOG_PROJECT_TOKEN
27
+ ```
28
+
29
+ The ambiguity error is **scoped to the package actually being loaded**:
30
+ discovery now enumerates packages without parsing them, so a single ambiguous
31
+ (or invalid) directory no longer aborts commands that don't touch it — `pull web`
32
+ or `paths web` succeed even if an unrelated `apps/legacy/` holds two manifests;
33
+ only a command that loads that package (`pull legacy`, `pull` with no ids,
34
+ `list`, `validate`) surfaces the error.
35
+
36
+ New public API: `findManifestFile`, `hasManifestFile`, `loadManifestFromDir`,
37
+ `parseManifestFile`, `MANIFEST_FILENAMES`, the `ManifestFile` / `ManifestFormat`
38
+ types, plus `discoverPackages` / `loadPackage` and the `PackageRef` type for
39
+ two-phase (enumerate-then-load) discovery. A discovered `PackageManifest` now
40
+ carries the `file` it was loaded from.
41
+
42
+ ## [2.0.0] - 2026-07-17
43
+
44
+ ### Changed (breaking — manifest format)
45
+
46
+ - **The manifest is now a `secrets` array of folders.** The flat `paths` +
47
+ `include` + `aliases` trio is replaced by a single `secrets` array that mirrors
48
+ your Infisical structure. Each of the three old fields was really an attribute
49
+ of a specific `(folder, key)` pair; the tree binds them together, so a key's
50
+ allowlist membership and its alias live in one place, provenance is explicit,
51
+ and the manifest reads like the vault. `secrets` is an array of folder objects
52
+ `{ name: [ ...contents ] }`; inside a folder's array, a **bare string** is a
53
+ plain key, an object with a **string value** `{ SOURCE: "TARGET" }` is an
54
+ alias, and an object with an **array value** `{ sub: [ ... ] }` is a **real
55
+ subfolder** (nesting mirrors the vault tree — `sub` under `posthog` reads
56
+ `/posthog/sub`). Value type discriminates alias vs subfolder, so subfolders
57
+ need no leading `/`. Fan a key out to several targets by repeating the alias,
58
+ one target each.
59
+
60
+ ```jsonc
61
+ // before (v1) // after (v2)
62
+ { {
63
+ "paths": ["posthog"], "secrets": [
64
+ "aliases": { { "posthog": [
65
+ "POSTHOG_PROJECT_TOKEN": { "POSTHOG_PROJECT_TOKEN": "VITE_POSTHOG_KEY" }
66
+ "VITE_POSTHOG_KEY" ] }
67
+ }, ]
68
+ "include": ["POSTHOG_PROJECT_TOKEN"] }
69
+ }
70
+ ```
71
+
72
+ - **Default-deny everywhere.** There is no wildcard: every emitted key must be
73
+ named, and an empty folder array is a schema error. The tree *is* the
74
+ allowlist, so the separate `include` field is **removed** — a folder emits
75
+ exactly the keys it declares, from that folder (provenance-aware selection).
76
+ - **`fetch: "keys"` no longer needs an allowlist.** Because the tree always
77
+ names every canonical key (an alias entry's key *is* the real vault key),
78
+ key mode requests those names directly — the v1 reverse-mapping from alias
79
+ target to source is gone, and `keys` is always satisfiable. The
80
+ `fetch`-requires-`include` cross-check is removed.
81
+ - **Optional keys stay env-scoped** via `environments.<slug>.optionalKeys`
82
+ (unchanged), now referencing declared canonical key names.
83
+ - **Profiles** carry an alternate `secrets` array (replacing the base tree when
84
+ `--profile` is set) plus an optional `fetch`; `profiles.<name>.paths` /
85
+ `.include` are gone.
86
+ - **Programmatic API.** `resolvePaths` / `resolveInclude` / `applyInclude` /
87
+ `resolveFetchKeys` / `checkFetchIncludeConsistency` / `fetchManifestSecrets` /
88
+ `selectEmittedSecrets` are replaced by `resolveCompiledFolders`, `compileTree`,
89
+ `fetchCompiledFolders`, and `materializeSecrets` (the latter applies aliases
90
+ and enforces missing keys **per folder**, before merging, so a key name shared
91
+ across folders keeps each folder's value and miss); `enforceIncludeKnown` is
92
+ renamed `enforceKnownKeys`. New exports: `compileTree`, `treeSchema`,
93
+ `materializeSecrets`, and the `CompiledFolder` / `CompiledKey` / `FolderEntry` /
94
+ `FolderArray` / `SecretsTree` / `FolderSecrets` types.
95
+ - **Schema** rewritten around the `secrets` array and published at `@2`.
96
+
97
+ ### Migration
98
+
99
+ Convert each `secrets.json`: replace `paths`/`include`/`aliases` with a
100
+ `secrets` array of `{ folder: [ ...keys ] }` objects; list plain keys as bare
101
+ strings and each `aliases` source as a `{ SOURCE: "TARGET" }` object (repeat for
102
+ several targets); keep only the keys you want (the array is the allowlist).
103
+ Point `$schema` at `…/infisicml@2/…`.
104
+ Behavior note: an alias entry now emits **both** its canonical name and the
105
+ target(s) — v1 could suppress the canonical via `include`; v2 does not (the
106
+ security boundary is default-deny provenance, not alias suppression).
107
+
7
108
  ## [1.2.1] - 2026-07-17
8
109
 
9
110
  ### Changed (BREAKING)
@@ -97,7 +198,8 @@ changes to either ship only in a new major.
97
198
  - **Published JSON Schema** for `secrets.json`, served from
98
199
  `https://cdn.jsdelivr.net/npm/@hubble-ventures/infisicml@1/schema/secrets.schema.json`.
99
200
 
100
- [1.2.1]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.2.1
201
+ [2.1.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v2.1.0
202
+ [2.0.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v2.0.0
101
203
  [1.2.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.2.0
102
204
  [1.1.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.1.0
103
205
  [1.0.0]: https://github.com/hubble-ventures/infisicml/releases/tag/v1.0.0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Infisical Secret Orchestration** — federated, per-package secret manifests for monorepos, unified across local development and CI.
4
4
 
5
- [Infisical](https://infisical.com) gives you a vault and the primitives to read from it (a CLI, a REST API, machine-identity auth, a GitHub Action). All of those operate on **one folder / one environment / one process at a time**. Infisicml is the layer above that: it lets each package in a monorepo declare which vault folders it needs in a committed `secrets.json`, then materializes those secrets **the same way** whether you're a developer running a local pull or a CI job exporting into `GITHUB_ENV`.
5
+ [Infisical](https://infisical.com) gives you a vault and the primitives to read from it (a CLI, a REST API, machine-identity auth, a GitHub Action). All of those operate on **one folder / one environment / one process at a time**. Infisicml is the layer above that: it lets each package in a monorepo declare which vault folders it needs in a committed manifest (`secrets.yaml`, or `secrets.json`), then materializes those secrets **the same way** whether you're a developer running a local pull or a CI job exporting into `GITHUB_ENV`.
6
6
 
7
7
  > Infisicml orchestrates Infisical. It stores no secrets and never sees your vault contents at rest — the vault, auth, and secret values stay in Infisical.
8
8
 
@@ -10,11 +10,11 @@
10
10
 
11
11
  | Problem | Native Infisical | Infisicml |
12
12
  |---|---|---|
13
- | N packages each need a different slice of the vault | Hand-write export commands per app×folder | One committed `secrets.json` per package, auto-discovered |
13
+ | N packages each need a different slice of the vault | Hand-write export commands per app×folder | One committed manifest (`secrets.yaml`/`.json`) per package, auto-discovered |
14
14
  | Dev uses the CLI, CI uses the API | Two integrations kept in sync by hand | One `SecretsProvider` abstraction; identical downstream logic |
15
15
  | Vault names a secret once, but Vite wants `VITE_*` and Next wants `NEXT_PUBLIC_*` | Each workflow re-derives the prefixed copy | Declare `aliases` once; applied in dev **and** CI |
16
16
  | Skip the pull in CI when vars are already injected | Bash guards in every workflow | `ci.skipWhenEnv` / `ci.stubInCi` policy |
17
- | A deploy needs exactly the runtime keys, not the deploy creds | Hand-maintained allowlist | `profiles` + `advertiseKeys` hook — `secrets.json` is the source of truth |
17
+ | A deploy needs exactly the runtime keys, not the deploy creds | Hand-maintained allowlist | `profiles` + `advertiseKeys` hook — the manifest is the source of truth |
18
18
  | A client must read a shared vendor folder without ever receiving its server secrets | Split the vault or accept over-fetch | `fetch: "keys"` requests only the named keys — the vault never transmits the rest |
19
19
 
20
20
  ## Install
@@ -36,35 +36,55 @@ 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:
44
-
45
- ```jsonc
46
- {
47
- "$schema": "https://cdn.jsdelivr.net/npm/@hubble-ventures/infisicml@1/schema/secrets.schema.json",
48
- "paths": ["clerk", "posthog"],
49
- "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
- }
59
- }
43
+ 2. Add a `secrets.yaml` next to each package (YAML is the primary format;
44
+ `secrets.json` is equally supported — see the note below). `secrets` is an
45
+ **array of folders** mirroring your Infisical structure; each folder is
46
+ `name: [ ...contents ]`. Inside a folder's array, a **bare string** is a
47
+ plain key, an entry with a **string value** `SOURCE: TARGET` is an alias, and
48
+ an entry with an **array value** `sub: [ ... ]` is a subfolder. Every emitted
49
+ key is named (default-deny; there is no wildcard).
50
+
51
+ ```yaml
52
+ # yaml-language-server: $schema=https://cdn.jsdelivr.net/npm/@hubble-ventures/infisicml@2/schema/secrets.schema.json
53
+ output: .env # written next to this manifest; defaults to .env.secrets
54
+ secrets:
55
+ # Canonical vault key → the prefixed name a build tool expects.
56
+ - clerk:
57
+ - CLERK_PUBLISHABLE_KEY: VITE_CLERK_PUBLISHABLE_KEY
58
+ # Fan one canonical key out to several prefixes: repeat the alias, one
59
+ # target each (an array value would mean a subfolder, not multi-target).
60
+ - google:
61
+ - GOOGLE_MAPS_API_KEY: EXPO_PUBLIC_GOOGLE_MAPS_API_KEY
62
+ - GOOGLE_MAPS_API_KEY: VITE_GOOGLE_MAPS_API_KEY
63
+ - posthog:
64
+ - POSTHOG_PROJECT_TOKEN
65
+ # A real Infisical subfolder at /posthog/eu.
66
+ - eu:
67
+ - POSTHOG_EU_HOST
60
68
  ```
61
69
 
70
+ An alias entry emits **both** its canonical name and the target. Keys you
71
+ don't name are never pulled, so a client package can share a vendor folder yet
72
+ emit only the public key — the server secret is simply undeclared.
73
+
74
+ > **Format:** each package's manifest may be `secrets.yaml`, `secrets.yml`, or
75
+ > `secrets.json` — the same schema either way. YAML is the default and reads
76
+ > best for hand-authored manifests; JSON is handy for generated ones. If a
77
+ > directory has more than one, `secrets.yaml` wins. See
78
+ > [`examples/yaml/secrets.yaml`](./examples/yaml/secrets.yaml) and
79
+ > [`examples/json/secrets.json`](./examples/json/secrets.json) for the same manifest in
80
+ > both formats.
81
+
62
82
  3. Pull secrets into gitignored `.env.secrets` files:
63
83
 
64
84
  ```bash
65
85
  npx infisicml pull # every package
66
86
  npx infisicml pull web api --force # specific ids, bypass the exists-check
67
- npx infisicml validate # check every secrets.json against the schema
87
+ npx infisicml validate # check every manifest against the schema
68
88
  ```
69
89
 
70
90
  See [`examples/`](./examples) for a fuller config and manifest.
@@ -75,9 +95,9 @@ See [`examples/`](./examples) for a fuller config and manifest.
75
95
  |---|---|
76
96
  | `pull [ids...]` | Write `.env.secrets` locally (uses `infisical export`) |
77
97
  | `export-gha <id>` | Mask + append secrets to `GITHUB_ENV` (REST API + GitHub OIDC) |
78
- | `list` | Show every manifest and its Infisical paths |
79
- | `validate` | Validate every `secrets.json` against the schema |
80
- | `paths <id>` | Print resolved paths (`--comma` for scripting) |
98
+ | `list` | Show every manifest's folder tree and declared keys |
99
+ | `validate` | Validate every manifest against the schema |
100
+ | `paths <id>` | Print resolved folder paths (`--comma` for scripting) |
81
101
  | `run <id> -- <cmd...>` | Thin `infisical run` wrapper (prefer `pull` + `.env.secrets`) |
82
102
 
83
103
  Common flags: `--env`, `--profile`, `--force`, `--here` (pull the cwd package), `--turbo` (always write, for Turbo caching).
@@ -125,96 +145,81 @@ Log in once as yourself; infisicml shells out to `infisical export` under your s
125
145
 
126
146
  ```bash
127
147
  infisical login # authenticate as you
128
- npx infisicml pull # write .env.secrets next to every secrets.json
148
+ npx infisicml pull # write .env.secrets next to every manifest
129
149
  ```
130
150
 
131
151
  Load `.env.secrets` however your dev runtime already loads env files. See [`examples/local-dev.md`](./examples/local-dev.md) for package-script and task-runner wiring.
132
152
 
133
153
  ## Concepts
134
154
 
135
- - **Manifest (`secrets.json`)** — per package: `paths` (vault folders), optional `profiles`, `aliases`, `include`, `fetch`, `ci`, `environments`, `output`.
136
- - **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.
155
+ - **Manifest (`secrets.yaml` or `secrets.json`)** — per package: `secrets` (an array of folders → keys), optional `profiles`, `fetch`, `ci`, `environments`, `output`. YAML is the default format; JSON is equally supported. When a directory has both, `secrets.yaml` wins.
156
+ - **Output file** — `output` sets the written filename (default `.env.secrets`), placed **next to the manifest**. Because each package owns its own manifest, 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.
157
+ - **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, a mapping with a string value `SOURCE: TARGET` is an alias, and a mapping 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.
158
+ - **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`).
159
+ - **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.
160
+ - **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
161
  - **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.
162
+ - **Optional keys** — `environments.<slug>.optionalKeys` downgrade a missing declared key to a `::notice::` instead of a failure.
143
163
  - **Advertise-keys hooks** — publish runtime key *names* (never values) to `GITHUB_ENV` for deploy forwarding.
144
164
 
145
- ### Key selection (`include`)
165
+ ### Key selection (the tree is the allowlist)
146
166
 
147
167
  Vaults are often organized by **vendor, not by access scope** — a single `/stripe` folder
148
168
  holds both a server secret and a client-public key:
149
169
 
150
170
  ```
151
- /stripe → STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY
171
+ /stripe → STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY
152
172
  ```
153
173
 
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.
174
+ A web/mobile client needs **only** the publishable key. Because the tree is default-deny, you
175
+ name that one key and nothing else is pulled — `STRIPE_SECRET_KEY` is simply never declared,
176
+ so it can't reach a Vite/Expo client build's env (or, via the CI action, `GITHUB_ENV`).
159
177
 
160
- ```jsonc
161
- // apps/web/secrets.json — a client package
162
- {
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"
172
- ]
173
- }
178
+ ```yaml
179
+ # apps/web/secrets.yaml — a client package
180
+ secrets:
181
+ - stripe:
182
+ - STRIPE_PUBLISHABLE_KEY: EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY
183
+ - google:
184
+ - GOOGLE_MAPS_API_KEY: EXPO_PUBLIC_GOOGLE_MAPS_API_KEY
185
+ - vercel:
186
+ - VERCEL_AUTOMATION_BYPASS_SECRET
174
187
  ```
175
188
 
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).
189
+ The written `.env.secrets` (and, in CI, `GITHUB_ENV`) contains **only** the declared keys and
190
+ their alias targets — server secrets in the same folders are never emitted.
191
+
192
+ - **Provenance** — a key is emitted from the folder it's declared under, and only that folder.
193
+ Selection is per-folder, so the same name in two folders doesn't cross-contaminate.
194
+ - **Alias emits both names** an alias entry copies the canonical key to its target; *both*
195
+ the canonical name and the target land in the output. (To emit only a prefixed name, don't
196
+ declare the canonical onebut the canonical publishable key is usually harmless.)
197
+ - **Unknown keys** — a declared key that its folder didn't produce is an error (fail the pull /
198
+ CI step), **unless** it's in `environments.<slug>.optionalKeys`, which downgrades it to a
199
+ `::notice::`. Enforced the same way in `pull` and `export-gha`.
200
+ - **Profiles** a profile supplies its own `secrets`, which **replaces** the root tree for that
201
+ profile.
192
202
 
193
203
  ### Wire-level least privilege (`fetch: "keys"`)
194
204
 
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"`:
205
+ By default (`fetch: "folder"`) infisicml pulls each whole folder, then selects the declared
206
+ keys before writing the output. The undeclared values still travel over the wire into your
207
+ machine / the CI runner — they're just never written to `.env` or `GITHUB_ENV`. For most
208
+ setups that's fine. When you need the vault to **not even transmit** the secrets you don't
209
+ use, set `fetch: "keys"`:
200
210
 
201
- ```jsonc
202
- // apps/web/secrets.json — client package, strict wire-level least privilege
203
- {
204
- "paths": ["stripe", "google"],
205
- "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"
212
- ]
213
- }
211
+ ```yaml
212
+ # apps/web/secrets.yaml — client package, strict wire-level least privilege
213
+ fetch: keys
214
+ secrets:
215
+ - stripe:
216
+ - STRIPE_PUBLISHABLE_KEY: EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY
217
+ - google:
218
+ - GOOGLE_MAPS_API_KEY
214
219
  ```
215
220
 
216
- In `keys` mode infisicml emits **only** the keys `include` resolves to. Where the guarantee
217
- bites depends on the lane:
221
+ In `keys` mode infisicml reads **only** the declared keys. Where the guarantee bites depends on
222
+ the lane:
218
223
 
219
224
  - **CI (`export-gha`)** — true **wire-level** least privilege: each key is fetched with the
220
225
  single-secret REST endpoint (`GET /api/v3/secrets/raw/{name}`), so the other keys in
@@ -225,23 +230,18 @@ bites depends on the lane:
225
230
  once and selects the keys. It narrows what's **written to disk**, not what the vault
226
231
  transmits — on your own machine, where folder mode already lands.
227
232
 
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
233
+ - **No allowlist needed** the tree already names every canonical key (an alias entry's key
234
+ *is* the real vault key), so `keys` mode requests exactly those names directly — no reverse
235
+ mapping, and `keys` is always satisfiable.
236
+ - **Imports still resolve** a key surfaced into a folder via an Infisical import is fetched in
237
+ `keys` mode too (the per-key read follows imports but returns only that one secret), so
238
+ enabling `keys` never silently drops an import-backed key.
239
+ - **Same emit result** everything after the fetch (aliases, unknown-key enforcement,
240
+ `optionalKeys`) is identical to folder mode; only what's read narrows.
241
+ - **Cost** in CI, one request **per key** instead of one per folder; the folder list becomes
239
242
  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.
243
+ - **Profiles** — a profile may set its own `fetch`, which **replaces** the root value. A deploy
244
+ profile can stay in `folder` mode while the runtime default is `keys`, or vice versa.
245
245
 
246
246
  ### Non-secret defaults (`.env.sample`)
247
247
 
@@ -278,19 +278,24 @@ for (const manifest of discoverManifests(config)) {
278
278
 
279
279
  ## Releasing
280
280
 
281
- Releases are cut by pushing a semver tag; [`.github/workflows/release.yml`](./.github/workflows/release.yml) does the rest:
281
+ **Releases are automatic you don't tag anything.** Bump the version in a PR
282
+ and merging it to `main` cuts the release:
282
283
 
283
284
  ```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
285
+ npm run release -- minor # or patch / major / 2.1.0 — bumps package.json + rebuilds dist
286
+ # edit CHANGELOG.md: add the new version's section
287
+ git commit -am "release: v2.1.0"
288
+ # open a PR, merge it → release fires
287
289
  ```
288
290
 
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.
291
+ On the push to `main`, [`.github/workflows/release.yml`](./.github/workflows/release.yml)
292
+ reads `package.json`; if that version isn't tagged yet it builds + tests, **creates and
293
+ pushes the `vX.Y.Z` tag**, publishes to npm via **trusted publishing** (OIDC no
294
+ `NPM_TOKEN` secret, provenance attached automatically), creates a GitHub Release from the
295
+ matching [`CHANGELOG.md`](./CHANGELOG.md) section, and moves the floating major tag (e.g.
296
+ `v2`). A main push that doesn't bump the version is a no-op, and the publish step is
297
+ idempotent — a version already on npm is skipped. Manually pushing a `vX.Y.Z` tag still
298
+ works if you ever need it.
294
299
 
295
300
  > **First publish (one time).** The `@hubble-ventures` npm **organization** must exist,
296
301
  > and npm trusted publishing can only be configured against an *existing* package — so
@@ -304,12 +309,12 @@ version already on npm is skipped.
304
309
  > (`publishConfig.access: public` in `package.json` enforces this even if the flag is
305
310
  > omitted.) Then, in the package's npm settings, add a **trusted publisher** for
306
311
  > `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.
312
+ > job sets none — the OIDC claims must match). Every subsequent version bump merged to
313
+ > `main` publishes automatically with no token.
309
314
 
310
315
  ## Design
311
316
 
312
- Everything repo-specific lives in `infisicml.config` — package discovery, the project-id source, the OIDC audience, and deploy-time key advertisement are all configuration, so nothing about one repo's layout is baked into the tool. The manifest contract (`secrets.json`) is the stable public interface, versioned via the [published JSON Schema](./schema/secrets.schema.json).
317
+ Everything repo-specific lives in `infisicml.config` — package discovery, the project-id source, the OIDC audience, and deploy-time key advertisement are all configuration, so nothing about one repo's layout is baked into the tool. The manifest contract (`secrets.yaml` or `secrets.json`) is the stable public interface, versioned via the [published JSON Schema](./schema/secrets.schema.json).
313
318
 
314
319
  ## License
315
320
 
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"}