@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.
- package/CHANGELOG.md +103 -1
- package/README.md +123 -118
- package/dist/aliases.d.ts +11 -5
- package/dist/aliases.d.ts.map +1 -1
- package/dist/aliases.js +20 -12
- package/dist/aliases.js.map +1 -1
- package/dist/ci-skip.d.ts +32 -15
- package/dist/ci-skip.d.ts.map +1 -1
- package/dist/ci-skip.js +48 -39
- package/dist/ci-skip.js.map +1 -1
- package/dist/commands/export-gha.d.ts +16 -0
- package/dist/commands/export-gha.d.ts.map +1 -1
- package/dist/commands/export-gha.js +52 -43
- package/dist/commands/export-gha.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +17 -25
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/paths.d.ts.map +1 -1
- package/dist/commands/paths.js +15 -14
- package/dist/commands/paths.js.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +12 -9
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +10 -7
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +3 -13
- package/dist/commands/validate.js.map +1 -1
- package/dist/config.d.ts +4 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/github-env.js +1 -1
- package/dist/github-env.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +2 -2
- package/dist/include.d.ts +6 -43
- package/dist/include.d.ts.map +1 -1
- package/dist/include.js +11 -67
- package/dist/include.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +59 -35
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +95 -86
- package/dist/manifest.js.map +1 -1
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +11 -16
- package/dist/pull.js.map +1 -1
- package/dist/registry.d.ts +28 -5
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +38 -13
- package/dist/registry.js.map +1 -1
- package/dist/tree.d.ts +49 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +190 -0
- package/dist/tree.js.map +1 -0
- package/package.json +5 -2
- 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
|
-
[
|
|
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.
|
|
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 —
|
|
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: "
|
|
39
|
+
hooks: { advertiseKeys: [{ envVar: "INFISICML_FLY_KEYS", scope: "runtime" }] },
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
2. Add a `secrets.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
79
|
-
| `validate` | Validate every
|
|
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
|
|
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: `
|
|
136
|
-
- **Output file** — `output` sets the written filename (default `.env.secrets`), placed **next to the manifest**. Because each package owns its own
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
- **Fetch mode** — `fetch: "keys"` requests only the keys
|
|
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::`
|
|
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 (
|
|
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,
|
|
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.
|
|
155
|
-
|
|
156
|
-
build's env (
|
|
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
|
-
```
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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**
|
|
177
|
-
server secrets in the same folders are never emitted.
|
|
178
|
-
|
|
179
|
-
- **
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 one — but 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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
```
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
217
|
-
|
|
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
|
-
- **
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
`
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
- **
|
|
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
|
|
241
|
-
|
|
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
|
|
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
|
|
285
|
-
|
|
286
|
-
git
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
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 {
|
|
1
|
+
import type { CompiledFolder } from "./tree.js";
|
|
2
2
|
export type ResolvedAlias = {
|
|
3
3
|
source: string;
|
|
4
4
|
targets: string[];
|
|
5
5
|
};
|
|
6
|
-
/**
|
|
7
|
-
|
|
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 —
|
|
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>,
|
|
25
|
+
export declare function applyAliases(merged: Record<string, string>, folders: CompiledFolder[]): Record<string, string>;
|
|
20
26
|
//# sourceMappingURL=aliases.d.ts.map
|
package/dist/aliases.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aliases.d.ts","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
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
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 —
|
|
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,
|
|
30
|
+
export function applyAliases(merged, folders) {
|
|
23
31
|
const out = { ...merged };
|
|
24
|
-
for (const { source, targets } of resolveAliases(
|
|
32
|
+
for (const { source, targets } of resolveAliases(folders)) {
|
|
25
33
|
const value = merged[source];
|
|
26
34
|
if (value === undefined)
|
|
27
35
|
continue;
|
package/dist/aliases.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aliases.js","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAIA
|
|
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"}
|