@theholocron/holocron-plugin-infisical 2.0.0-alpha.9 → 2.0.1
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/README.md +24 -14
- package/dist/index.d.mts +8 -94
- package/dist/index.mjs +46 -181
- package/package.json +21 -9
package/README.md
CHANGED
|
@@ -31,8 +31,10 @@ Switch by editing `holocron.config.json`.
|
|
|
31
31
|
|
|
32
32
|
## Install
|
|
33
33
|
|
|
34
|
+
<!-- prettier-ignore -->
|
|
34
35
|
```bash
|
|
35
36
|
pnpm add -D @theholocron/holocron-plugin-infisical@alpha
|
|
37
|
+
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
## Auth
|
|
@@ -65,12 +67,14 @@ Universal Auth is a two-step flow (client id + client secret → login
|
|
|
65
67
|
endpoint → short-lived access token). Support for that exchange is
|
|
66
68
|
tracked as a follow-up; for now, use one of the two above.
|
|
67
69
|
|
|
70
|
+
<!-- prettier-ignore -->
|
|
68
71
|
```bash
|
|
69
72
|
# 1. Generate the token per the note above.
|
|
70
73
|
# 2. Hand it off to holocron's keyring (one-shot):
|
|
71
74
|
holocron auth set infisical <TOKEN>
|
|
72
75
|
# 3. Verify (calls GET /v1/workspace and reports accessible workspaces):
|
|
73
76
|
holocron auth check infisical
|
|
77
|
+
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
**If you have a machine identity that's Universal-Auth-only**: add a
|
|
@@ -85,24 +89,28 @@ precedence still work; step 4 quietly falls through.
|
|
|
85
89
|
|
|
86
90
|
## Config
|
|
87
91
|
|
|
92
|
+
<!-- prettier-ignore -->
|
|
88
93
|
```jsonc
|
|
89
94
|
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
"providers": {
|
|
96
|
+
"vault": ["infisical", { "workspace": "<workspace-id>", "environment": "dev" }],
|
|
97
|
+
},
|
|
93
98
|
}
|
|
99
|
+
|
|
94
100
|
```
|
|
95
101
|
|
|
96
102
|
- `workspace` (required) — Infisical workspace (project) id. Find via
|
|
97
103
|
the workspace URL or the API. **Not the workspace slug** — Infisical's
|
|
98
|
-
API
|
|
104
|
+
API prevents slug in this position ([Infisical#1894](https://github.com/Infisical/infisical/issues/1894)).
|
|
99
105
|
- `environment` (required) — Environment slug (usually `dev`, `stg`,
|
|
100
106
|
or `prd`). `list()` reads secrets from this environment.
|
|
101
107
|
|
|
102
108
|
Individual `read` / `write` calls take a fully-qualified reference:
|
|
103
109
|
|
|
110
|
+
<!-- prettier-ignore -->
|
|
104
111
|
```
|
|
105
112
|
infisical://<workspaceId>/<environment>/<name>
|
|
113
|
+
|
|
106
114
|
```
|
|
107
115
|
|
|
108
116
|
The default `workspace` + `environment` in options apply to `list()`,
|
|
@@ -133,19 +141,21 @@ convention):
|
|
|
133
141
|
|
|
134
142
|
Set the `baseUrl` option in `holocron.config.json`:
|
|
135
143
|
|
|
144
|
+
<!-- prettier-ignore -->
|
|
136
145
|
```jsonc
|
|
137
146
|
{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
"providers": {
|
|
148
|
+
"vault": [
|
|
149
|
+
"infisical",
|
|
150
|
+
{
|
|
151
|
+
"workspace": "<id>",
|
|
152
|
+
"environment": "dev",
|
|
153
|
+
"baseUrl": "https://infisical.internal.example.com/api",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
148
157
|
}
|
|
158
|
+
|
|
149
159
|
```
|
|
150
160
|
|
|
151
161
|
## Status
|
package/dist/index.d.mts
CHANGED
|
@@ -1,57 +1,8 @@
|
|
|
1
|
-
import { EnsureResult, Vault } from "@theholocron/cli";
|
|
1
|
+
import { AuthError, EnsureResult, ResolveTokenInput, Vault } from "@theholocron/cli";
|
|
2
|
+
import { InfisicalClient, InfisicalClient as InfisicalClient$1, createInfisicalClient } from "@theholocron/infisical-client";
|
|
2
3
|
|
|
3
4
|
//#region src/auth.d.ts
|
|
4
|
-
|
|
5
|
-
* Token resolution for the Infisical plugin.
|
|
6
|
-
*
|
|
7
|
-
* Resolution order (matches the standard 4-step precedence set by
|
|
8
|
-
* `.notes/tech-auth-bootstrap.spec.md`):
|
|
9
|
-
* 1. explicit `cliToken` argument (from `--token` flag)
|
|
10
|
-
* 2. HOLOCRON_INFISICAL_TOKEN env var (preferred — explicit intent)
|
|
11
|
-
* 3. INFISICAL_TOKEN env var (vendor-native)
|
|
12
|
-
* 4. keyring (com.theholocron.cli / "infisical")
|
|
13
|
-
* 5. AuthError naming all four options + the bootstrap hint
|
|
14
|
-
*/
|
|
15
|
-
declare class AuthError extends Error {
|
|
16
|
-
name: string;
|
|
17
|
-
}
|
|
18
|
-
interface ResolveTokenInput {
|
|
19
|
-
/** From `--token` CLI flag. */
|
|
20
|
-
cliToken?: string;
|
|
21
|
-
/** Env vars; passed in for testability. Defaults to `process.env`. */
|
|
22
|
-
env?: NodeJS.ProcessEnv;
|
|
23
|
-
/** Keyring lookup fn; passed in for testability. Defaults to `getToken(provider)`. */
|
|
24
|
-
keyring?: (provider: string) => string | null;
|
|
25
|
-
}
|
|
26
|
-
declare function resolveToken(input?: ResolveTokenInput): string;
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/rest.d.ts
|
|
29
|
-
/**
|
|
30
|
-
* Thin REST wrapper around https://app.infisical.com/api.
|
|
31
|
-
*
|
|
32
|
-
* Bearer auth, JSON-only bodies, transport-failure wrapping with
|
|
33
|
-
* `status: 0` so orchestrator soft-skip paths see a clear message
|
|
34
|
-
* instead of a generic `TypeError: fetch failed`.
|
|
35
|
-
*/
|
|
36
|
-
interface RestClientOptions {
|
|
37
|
-
token: string;
|
|
38
|
-
fetch?: typeof fetch;
|
|
39
|
-
baseUrl?: string;
|
|
40
|
-
}
|
|
41
|
-
interface RequestOptions {
|
|
42
|
-
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
43
|
-
body?: unknown;
|
|
44
|
-
query?: Record<string, string>;
|
|
45
|
-
/** Treat this response as void even if 200 is returned. */
|
|
46
|
-
expectNoContent?: boolean;
|
|
47
|
-
}
|
|
48
|
-
declare class InfisicalRestClient {
|
|
49
|
-
private readonly token;
|
|
50
|
-
private readonly fetchImpl;
|
|
51
|
-
readonly baseUrl: string;
|
|
52
|
-
constructor(opts: RestClientOptions);
|
|
53
|
-
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
54
|
-
}
|
|
5
|
+
declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
|
|
55
6
|
//#endregion
|
|
56
7
|
//#region src/capabilities/vault.d.ts
|
|
57
8
|
interface InfisicalVaultOptions {
|
|
@@ -61,7 +12,7 @@ interface InfisicalVaultOptions {
|
|
|
61
12
|
environment: string;
|
|
62
13
|
}
|
|
63
14
|
declare class InfisicalVault implements Vault {
|
|
64
|
-
private readonly
|
|
15
|
+
private readonly client;
|
|
65
16
|
readonly key: "vault";
|
|
66
17
|
readonly providerName = "infisical";
|
|
67
18
|
private readonly workspace;
|
|
@@ -69,11 +20,11 @@ declare class InfisicalVault implements Vault {
|
|
|
69
20
|
/**
|
|
70
21
|
* Cache of resolved workspace name/slug → id. `ensureEnvironment`
|
|
71
22
|
* is called once per environment during `holocron setup` (dev / stg
|
|
72
|
-
* / prd), so a single
|
|
23
|
+
* / prd), so a single `GET /v1/workspace` lookup covers all three
|
|
73
24
|
* calls in the same run.
|
|
74
25
|
*/
|
|
75
26
|
private readonly workspaceIdCache;
|
|
76
|
-
constructor(
|
|
27
|
+
constructor(client: InfisicalClient$1, opts: InfisicalVaultOptions);
|
|
77
28
|
read(reference: string): Promise<string>;
|
|
78
29
|
write(reference: string, value: string): Promise<void>;
|
|
79
30
|
list(): Promise<string[]>;
|
|
@@ -81,26 +32,6 @@ declare class InfisicalVault implements Vault {
|
|
|
81
32
|
readEnvironment(environmentId: string): Promise<Record<string, string>>;
|
|
82
33
|
ensureProject(name: string): Promise<EnsureResult>;
|
|
83
34
|
ensureEnvironment(project: string, name: string): Promise<EnsureResult>;
|
|
84
|
-
/**
|
|
85
|
-
* Resolve a workspace name / slug to its Infisical workspace id.
|
|
86
|
-
*
|
|
87
|
-
* `runSetup` calls `ensureEnvironment(config.project.name, envName)`
|
|
88
|
-
* — for Doppler that's directly the API's input, but Infisical's
|
|
89
|
-
* environments endpoint takes the workspace **id** (UUID/nanoid),
|
|
90
|
-
* not the name. This helper does the translation via
|
|
91
|
-
* `GET /v1/workspace` (which the token needs org-level list scope
|
|
92
|
-
* to call).
|
|
93
|
-
*
|
|
94
|
-
* Graceful degrade: for workspace-scoped tokens that 403 on the
|
|
95
|
-
* org-level list, we fall back to treating the input as an id
|
|
96
|
-
* directly. If the operator's `holocron.config.json` names a real
|
|
97
|
-
* id, the subsequent POST works. If they named a slug, the POST
|
|
98
|
-
* 404s and the operator gets a clear "workspace not found" error
|
|
99
|
-
* from the API — better than swallowing the 403 silently.
|
|
100
|
-
*
|
|
101
|
-
* Results are cached per-instance so the 3 back-to-back
|
|
102
|
-
* `ensureEnvironment` calls in `runSetup` share one lookup.
|
|
103
|
-
*/
|
|
104
35
|
private resolveWorkspaceId;
|
|
105
36
|
}
|
|
106
37
|
//#endregion
|
|
@@ -120,16 +51,6 @@ declare class InfisicalVault implements Vault {
|
|
|
120
51
|
* will name the specific workspace + environment the token
|
|
121
52
|
* actually has access to.
|
|
122
53
|
* - 401 or other → token is invalid.
|
|
123
|
-
*
|
|
124
|
-
* The distinction matters: verifyToken answers "is this a real
|
|
125
|
-
* Infisical token?", not "does it have every possible permission?"
|
|
126
|
-
* — per-capability permission failures surface at the actual read /
|
|
127
|
-
* write / list call sites.
|
|
128
|
-
*
|
|
129
|
-
* Kept as a standalone function (not a capability method) so the auth
|
|
130
|
-
* command can call it without initializing the full plugin — plugin
|
|
131
|
-
* construction requires an already-resolved token, which is exactly
|
|
132
|
-
* what we don't have yet at bootstrap time.
|
|
133
54
|
*/
|
|
134
55
|
interface VerifyTokenSuccess {
|
|
135
56
|
ok: true;
|
|
@@ -155,7 +76,7 @@ interface InfisicalPluginOptions extends ResolveTokenInput, InfisicalVaultOption
|
|
|
155
76
|
}
|
|
156
77
|
interface PluginContext {
|
|
157
78
|
options: InfisicalPluginOptions;
|
|
158
|
-
|
|
79
|
+
client: InfisicalClient;
|
|
159
80
|
}
|
|
160
81
|
declare function createContext(options: InfisicalPluginOptions): PluginContext;
|
|
161
82
|
declare function vault(ctx: PluginContext): Vault;
|
|
@@ -168,14 +89,7 @@ declare function createPlugin(options: InfisicalPluginOptions): {
|
|
|
168
89
|
/**
|
|
169
90
|
* One-line hint printed by `holocron auth set infisical` when no
|
|
170
91
|
* token is supplied or the supplied token is rejected.
|
|
171
|
-
*
|
|
172
|
-
* IMPORTANT — the token must be usable as a `Authorization: Bearer`
|
|
173
|
-
* directly. Two token types work: **Personal API Token** (inherits
|
|
174
|
-
* user perms) or a **Token Auth** token on a machine identity (a
|
|
175
|
-
* single long-lived string). Universal Auth's Client Secret is NOT
|
|
176
|
-
* a bearer — it needs a `/v1/auth/universal-auth/login` exchange
|
|
177
|
-
* step this plugin doesn't yet do (Phase 2 follow-up).
|
|
178
92
|
*/
|
|
179
93
|
declare const AUTH_HINT: string;
|
|
180
94
|
//#endregion
|
|
181
|
-
export { AUTH_HINT, AuthError,
|
|
95
|
+
export { AUTH_HINT, AuthError, type InfisicalClient, InfisicalPluginOptions, InfisicalVault, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createInfisicalClient, createPlugin, resolveToken, vault, verifyToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
import { ProviderApiError,
|
|
1
|
+
import { AuthError, ProviderApiError, createResolveToken } from "@theholocron/cli";
|
|
2
|
+
import { createInfisicalClient } from "@theholocron/infisical-client";
|
|
2
3
|
//#region src/auth.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* 2. HOLOCRON_INFISICAL_TOKEN env var (preferred — explicit intent)
|
|
10
|
-
* 3. INFISICAL_TOKEN env var (vendor-native)
|
|
11
|
-
* 4. keyring (com.theholocron.cli / "infisical")
|
|
12
|
-
* 5. AuthError naming all four options + the bootstrap hint
|
|
13
|
-
*/
|
|
14
|
-
var AuthError = class extends Error {
|
|
15
|
-
name = "AuthError";
|
|
16
|
-
};
|
|
17
|
-
function resolveToken(input = {}) {
|
|
18
|
-
const env = input.env ?? process.env;
|
|
19
|
-
const keyring = input.keyring ?? getToken;
|
|
20
|
-
const token = input.cliToken || env["HOLOCRON_INFISICAL_TOKEN"] || env["INFISICAL_TOKEN"] || keyring("infisical");
|
|
21
|
-
if (!token) throw new AuthError("no Infisical token found. Pass --token <TOKEN>, set HOLOCRON_INFISICAL_TOKEN / INFISICAL_TOKEN, or run: holocron auth set infisical <TOKEN>");
|
|
22
|
-
return token;
|
|
23
|
-
}
|
|
4
|
+
const resolveToken = createResolveToken({
|
|
5
|
+
envName: "HOLOCRON_INFISICAL_TOKEN",
|
|
6
|
+
vendorEnvName: "INFISICAL_TOKEN",
|
|
7
|
+
keyringService: "infisical",
|
|
8
|
+
errorMessage: "no Infisical token found. Pass --token <TOKEN>, set HOLOCRON_INFISICAL_TOKEN / INFISICAL_TOKEN, or run: holocron auth set infisical <TOKEN>"
|
|
9
|
+
});
|
|
24
10
|
//#endregion
|
|
25
11
|
//#region src/capabilities/vault.ts
|
|
26
12
|
/**
|
|
@@ -32,13 +18,6 @@ function resolveToken(input = {}) {
|
|
|
32
18
|
* `environment` (slug, typically `dev`/`stg`/`prd`) so `list()` /
|
|
33
19
|
* `environments()` / `readEnvironment()` don't need a three-part ref.
|
|
34
20
|
*
|
|
35
|
-
* Infisical's data model:
|
|
36
|
-
* - Workspace (aka project) — top-level container
|
|
37
|
-
* - Environments — dev / stg / prd, scoped to a workspace
|
|
38
|
-
* - Secrets — scoped to a workspace + environment, with a path
|
|
39
|
-
* (defaults to root `/` in this plugin; a `secretPath` option
|
|
40
|
-
* would extend it but Phase 1 keeps it flat)
|
|
41
|
-
*
|
|
42
21
|
* Write semantics: Infisical's REST API separates CREATE (`POST`) and
|
|
43
22
|
* UPDATE (`PATCH`). We attempt POST first; on the vendor's "already
|
|
44
23
|
* exists" response we PATCH to upsert. The isConflict helper follows
|
|
@@ -49,7 +28,7 @@ function resolveToken(input = {}) {
|
|
|
49
28
|
* workspace / environment create endpoints.
|
|
50
29
|
*/
|
|
51
30
|
var InfisicalVault = class {
|
|
52
|
-
|
|
31
|
+
client;
|
|
53
32
|
key = "vault";
|
|
54
33
|
providerName = "infisical";
|
|
55
34
|
workspace;
|
|
@@ -57,12 +36,12 @@ var InfisicalVault = class {
|
|
|
57
36
|
/**
|
|
58
37
|
* Cache of resolved workspace name/slug → id. `ensureEnvironment`
|
|
59
38
|
* is called once per environment during `holocron setup` (dev / stg
|
|
60
|
-
* / prd), so a single
|
|
39
|
+
* / prd), so a single `GET /v1/workspace` lookup covers all three
|
|
61
40
|
* calls in the same run.
|
|
62
41
|
*/
|
|
63
42
|
workspaceIdCache = /* @__PURE__ */ new Map();
|
|
64
|
-
constructor(
|
|
65
|
-
this.
|
|
43
|
+
constructor(client, opts) {
|
|
44
|
+
this.client = client;
|
|
66
45
|
if (!opts.workspace) throw new Error("InfisicalVault requires `workspace` in options");
|
|
67
46
|
if (!opts.environment) throw new Error("InfisicalVault requires `environment` in options");
|
|
68
47
|
this.workspace = opts.workspace;
|
|
@@ -70,11 +49,12 @@ var InfisicalVault = class {
|
|
|
70
49
|
}
|
|
71
50
|
async read(reference) {
|
|
72
51
|
const parsed = parseReference(reference);
|
|
73
|
-
|
|
52
|
+
const { secret } = await this.client.secrets.get(parsed.name, {
|
|
74
53
|
workspaceId: parsed.workspace,
|
|
75
54
|
environment: parsed.environment,
|
|
76
55
|
secretPath: "/"
|
|
77
|
-
}
|
|
56
|
+
});
|
|
57
|
+
return secret?.secretValue ?? "";
|
|
78
58
|
}
|
|
79
59
|
async write(reference, value) {
|
|
80
60
|
const parsed = parseReference(reference);
|
|
@@ -85,51 +65,40 @@ var InfisicalVault = class {
|
|
|
85
65
|
secretValue: value
|
|
86
66
|
};
|
|
87
67
|
try {
|
|
88
|
-
await this.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
...scope,
|
|
92
|
-
type: "shared"
|
|
93
|
-
}
|
|
68
|
+
await this.client.secrets.create(parsed.name, {
|
|
69
|
+
...scope,
|
|
70
|
+
type: "shared"
|
|
94
71
|
});
|
|
95
|
-
return;
|
|
96
72
|
} catch (err) {
|
|
97
73
|
if (!isConflict(err)) throw err;
|
|
98
|
-
await this.
|
|
99
|
-
method: "PATCH",
|
|
100
|
-
body: scope
|
|
101
|
-
});
|
|
74
|
+
await this.client.secrets.update(parsed.name, scope);
|
|
102
75
|
}
|
|
103
76
|
}
|
|
104
77
|
async list() {
|
|
105
|
-
|
|
78
|
+
const { secrets } = await this.client.secrets.list({
|
|
106
79
|
workspaceId: this.workspace,
|
|
107
80
|
environment: this.environment,
|
|
108
81
|
secretPath: "/"
|
|
109
|
-
}
|
|
82
|
+
});
|
|
83
|
+
return (secrets ?? []).map((s) => s.secretKey ?? "").filter(Boolean);
|
|
110
84
|
}
|
|
111
85
|
async environments() {
|
|
112
|
-
|
|
86
|
+
const { workspace } = await this.client.workspaces.get(this.workspace);
|
|
87
|
+
return (workspace?.environments ?? []).map((e) => e.slug ?? e.name ?? "").filter(Boolean);
|
|
113
88
|
}
|
|
114
89
|
async readEnvironment(environmentId) {
|
|
115
|
-
const
|
|
90
|
+
const { secrets } = await this.client.secrets.list({
|
|
116
91
|
workspaceId: this.workspace,
|
|
117
92
|
environment: environmentId,
|
|
118
93
|
secretPath: "/"
|
|
119
|
-
}
|
|
94
|
+
});
|
|
120
95
|
const out = {};
|
|
121
|
-
for (const s of
|
|
96
|
+
for (const s of secrets ?? []) if (s.secretKey && typeof s.secretValue === "string") out[s.secretKey] = s.secretValue;
|
|
122
97
|
return out;
|
|
123
98
|
}
|
|
124
99
|
async ensureProject(name) {
|
|
125
100
|
try {
|
|
126
|
-
await this.
|
|
127
|
-
method: "POST",
|
|
128
|
-
body: {
|
|
129
|
-
projectName: name,
|
|
130
|
-
slug: name
|
|
131
|
-
}
|
|
132
|
-
});
|
|
101
|
+
await this.client.workspaces.create(name, name);
|
|
133
102
|
return { alreadyExists: false };
|
|
134
103
|
} catch (err) {
|
|
135
104
|
if (isConflict(err)) return { alreadyExists: true };
|
|
@@ -139,44 +108,19 @@ var InfisicalVault = class {
|
|
|
139
108
|
async ensureEnvironment(project, name) {
|
|
140
109
|
const workspaceId = await this.resolveWorkspaceId(project);
|
|
141
110
|
try {
|
|
142
|
-
await this.
|
|
143
|
-
method: "POST",
|
|
144
|
-
body: {
|
|
145
|
-
environmentName: name,
|
|
146
|
-
environmentSlug: name
|
|
147
|
-
}
|
|
148
|
-
});
|
|
111
|
+
await this.client.workspaces.createEnvironment(workspaceId, name, name);
|
|
149
112
|
return { alreadyExists: false };
|
|
150
113
|
} catch (err) {
|
|
151
114
|
if (isConflict(err)) return { alreadyExists: true };
|
|
152
115
|
throw err;
|
|
153
116
|
}
|
|
154
117
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Resolve a workspace name / slug to its Infisical workspace id.
|
|
157
|
-
*
|
|
158
|
-
* `runSetup` calls `ensureEnvironment(config.project.name, envName)`
|
|
159
|
-
* — for Doppler that's directly the API's input, but Infisical's
|
|
160
|
-
* environments endpoint takes the workspace **id** (UUID/nanoid),
|
|
161
|
-
* not the name. This helper does the translation via
|
|
162
|
-
* `GET /v1/workspace` (which the token needs org-level list scope
|
|
163
|
-
* to call).
|
|
164
|
-
*
|
|
165
|
-
* Graceful degrade: for workspace-scoped tokens that 403 on the
|
|
166
|
-
* org-level list, we fall back to treating the input as an id
|
|
167
|
-
* directly. If the operator's `holocron.config.json` names a real
|
|
168
|
-
* id, the subsequent POST works. If they named a slug, the POST
|
|
169
|
-
* 404s and the operator gets a clear "workspace not found" error
|
|
170
|
-
* from the API — better than swallowing the 403 silently.
|
|
171
|
-
*
|
|
172
|
-
* Results are cached per-instance so the 3 back-to-back
|
|
173
|
-
* `ensureEnvironment` calls in `runSetup` share one lookup.
|
|
174
|
-
*/
|
|
175
118
|
async resolveWorkspaceId(nameOrId) {
|
|
176
119
|
const cached = this.workspaceIdCache.get(nameOrId);
|
|
177
120
|
if (cached) return cached;
|
|
178
121
|
try {
|
|
179
|
-
const
|
|
122
|
+
const { workspaces } = await this.client.workspaces.list();
|
|
123
|
+
const match = (workspaces ?? []).find((w) => w.name === nameOrId || w.slug === nameOrId);
|
|
180
124
|
const resolvedId = match?._id ?? match?.id;
|
|
181
125
|
if (resolvedId) {
|
|
182
126
|
this.workspaceIdCache.set(nameOrId, resolvedId);
|
|
@@ -188,10 +132,6 @@ var InfisicalVault = class {
|
|
|
188
132
|
return nameOrId;
|
|
189
133
|
}
|
|
190
134
|
};
|
|
191
|
-
/**
|
|
192
|
-
* Parse `infisical://<workspaceId>/<environment>/<name>` into its
|
|
193
|
-
* parts. Throws when the shape doesn't match.
|
|
194
|
-
*/
|
|
195
135
|
function parseReference(reference) {
|
|
196
136
|
if (!reference.startsWith("infisical://")) throw new ProviderApiError(`Infisical references must start with "infisical://": got "${reference}"`, 400, void 0);
|
|
197
137
|
const parts = reference.slice(12).split("/");
|
|
@@ -203,13 +143,6 @@ function parseReference(reference) {
|
|
|
203
143
|
name: nameParts.join("/")
|
|
204
144
|
};
|
|
205
145
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Infisical returns duplicate-create errors as 400 or 409 depending
|
|
208
|
-
* on the endpoint, with a body message that includes "already exists"
|
|
209
|
-
* (paraphrased). The REST client wraps both as ProviderApiError. We
|
|
210
|
-
* accept 409 outright and treat 400/422 with an "already exists" body
|
|
211
|
-
* as idempotent conflict — same pattern as the Doppler plugin.
|
|
212
|
-
*/
|
|
213
146
|
function isConflict(err) {
|
|
214
147
|
if (!(err instanceof ProviderApiError)) return false;
|
|
215
148
|
if (err.status === 409) return true;
|
|
@@ -221,59 +154,6 @@ function hasAlreadyExistsBody(details) {
|
|
|
221
154
|
return /already exists|duplicate/i.test(details);
|
|
222
155
|
}
|
|
223
156
|
//#endregion
|
|
224
|
-
//#region src/rest.ts
|
|
225
|
-
/**
|
|
226
|
-
* Thin REST wrapper around https://app.infisical.com/api.
|
|
227
|
-
*
|
|
228
|
-
* Bearer auth, JSON-only bodies, transport-failure wrapping with
|
|
229
|
-
* `status: 0` so orchestrator soft-skip paths see a clear message
|
|
230
|
-
* instead of a generic `TypeError: fetch failed`.
|
|
231
|
-
*/
|
|
232
|
-
var InfisicalRestClient = class {
|
|
233
|
-
token;
|
|
234
|
-
fetchImpl;
|
|
235
|
-
baseUrl;
|
|
236
|
-
constructor(opts) {
|
|
237
|
-
this.token = opts.token;
|
|
238
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
239
|
-
let url = opts.baseUrl ?? "https://app.infisical.com/api";
|
|
240
|
-
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
241
|
-
this.baseUrl = url;
|
|
242
|
-
}
|
|
243
|
-
async request(path, opts = {}) {
|
|
244
|
-
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
|
|
245
|
-
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
246
|
-
const fullUrl = url.toString();
|
|
247
|
-
const headers = {
|
|
248
|
-
authorization: `Bearer ${this.token}`,
|
|
249
|
-
accept: "application/json"
|
|
250
|
-
};
|
|
251
|
-
const init = {
|
|
252
|
-
method: opts.method ?? "GET",
|
|
253
|
-
headers
|
|
254
|
-
};
|
|
255
|
-
if (opts.body !== void 0) {
|
|
256
|
-
headers["content-type"] = "application/json";
|
|
257
|
-
init.body = JSON.stringify(opts.body);
|
|
258
|
-
}
|
|
259
|
-
let res;
|
|
260
|
-
try {
|
|
261
|
-
res = await this.fetchImpl(fullUrl, init);
|
|
262
|
-
} catch (err) {
|
|
263
|
-
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
264
|
-
throw new ProviderApiError(`Infisical ${init.method} ${path} failed: ${detail}`, 0, void 0);
|
|
265
|
-
}
|
|
266
|
-
if (!res.ok) {
|
|
267
|
-
const body = await res.text().catch(() => "");
|
|
268
|
-
throw new ProviderApiError(`Infisical ${init.method} ${path} → ${res.status}`, res.status, body);
|
|
269
|
-
}
|
|
270
|
-
if (opts.expectNoContent || res.status === 204) return void 0;
|
|
271
|
-
const text = await res.text();
|
|
272
|
-
if (!text) return void 0;
|
|
273
|
-
return JSON.parse(text);
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
//#endregion
|
|
277
157
|
//#region src/verify-token.ts
|
|
278
158
|
/**
|
|
279
159
|
* `verifyToken` — plugin-level export used by `holocron auth set` +
|
|
@@ -290,26 +170,17 @@ var InfisicalRestClient = class {
|
|
|
290
170
|
* will name the specific workspace + environment the token
|
|
291
171
|
* actually has access to.
|
|
292
172
|
* - 401 or other → token is invalid.
|
|
293
|
-
*
|
|
294
|
-
* The distinction matters: verifyToken answers "is this a real
|
|
295
|
-
* Infisical token?", not "does it have every possible permission?"
|
|
296
|
-
* — per-capability permission failures surface at the actual read /
|
|
297
|
-
* write / list call sites.
|
|
298
|
-
*
|
|
299
|
-
* Kept as a standalone function (not a capability method) so the auth
|
|
300
|
-
* command can call it without initializing the full plugin — plugin
|
|
301
|
-
* construction requires an already-resolved token, which is exactly
|
|
302
|
-
* what we don't have yet at bootstrap time.
|
|
303
173
|
*/
|
|
304
174
|
async function verifyToken(token, opts = {}) {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
175
|
+
const client = createInfisicalClient({
|
|
176
|
+
token,
|
|
177
|
+
baseUrl: opts.baseUrl,
|
|
178
|
+
fetch: opts.fetch
|
|
179
|
+
});
|
|
309
180
|
try {
|
|
310
|
-
const
|
|
311
|
-
const count =
|
|
312
|
-
const first =
|
|
181
|
+
const { workspaces } = await client.workspaces.list();
|
|
182
|
+
const count = workspaces?.length ?? 0;
|
|
183
|
+
const first = workspaces?.[0];
|
|
313
184
|
const label = first?.name ?? first?.slug ?? "no accessible workspaces";
|
|
314
185
|
return {
|
|
315
186
|
ok: true,
|
|
@@ -329,16 +200,17 @@ async function verifyToken(token, opts = {}) {
|
|
|
329
200
|
//#endregion
|
|
330
201
|
//#region src/index.ts
|
|
331
202
|
function createContext(options) {
|
|
332
|
-
const restOpts = { token: resolveToken(options) };
|
|
333
|
-
if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
|
|
334
|
-
if (options.fetch !== void 0) restOpts.fetch = options.fetch;
|
|
335
203
|
return {
|
|
336
204
|
options,
|
|
337
|
-
|
|
205
|
+
client: createInfisicalClient({
|
|
206
|
+
token: resolveToken(options),
|
|
207
|
+
baseUrl: options.baseUrl,
|
|
208
|
+
fetch: options.fetch
|
|
209
|
+
})
|
|
338
210
|
};
|
|
339
211
|
}
|
|
340
212
|
function vault(ctx) {
|
|
341
|
-
return new InfisicalVault(ctx.
|
|
213
|
+
return new InfisicalVault(ctx.client, {
|
|
342
214
|
workspace: ctx.options.workspace,
|
|
343
215
|
environment: ctx.options.environment
|
|
344
216
|
});
|
|
@@ -353,14 +225,7 @@ function createPlugin(options) {
|
|
|
353
225
|
/**
|
|
354
226
|
* One-line hint printed by `holocron auth set infisical` when no
|
|
355
227
|
* token is supplied or the supplied token is rejected.
|
|
356
|
-
*
|
|
357
|
-
* IMPORTANT — the token must be usable as a `Authorization: Bearer`
|
|
358
|
-
* directly. Two token types work: **Personal API Token** (inherits
|
|
359
|
-
* user perms) or a **Token Auth** token on a machine identity (a
|
|
360
|
-
* single long-lived string). Universal Auth's Client Secret is NOT
|
|
361
|
-
* a bearer — it needs a `/v1/auth/universal-auth/login` exchange
|
|
362
|
-
* step this plugin doesn't yet do (Phase 2 follow-up).
|
|
363
228
|
*/
|
|
364
229
|
const AUTH_HINT = "generate a Token Auth token on your machine identity (organization → access control → identities → your identity → add auth method → Token Auth → create token) OR a Personal API Token, then: holocron auth set infisical <TOKEN>. NOT Universal Auth's Client Secret — that needs a login exchange this plugin doesn't do yet.";
|
|
365
230
|
//#endregion
|
|
366
|
-
export { AUTH_HINT, AuthError,
|
|
231
|
+
export { AUTH_HINT, AuthError, InfisicalVault, createContext, createInfisicalClient, createPlugin, resolveToken, vault, verifyToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-infisical",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Holocron plugin for Infisical. Implements the vault capability against Infisical's REST API, plus exports verifyToken + AUTH_HINT for `holocron auth`.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-infisical#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
@@ -21,19 +21,31 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@theholocron/
|
|
24
|
+
"@theholocron/infisical-client": "^1.1.0",
|
|
25
|
+
"@theholocron/cli": "2.0.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"@theholocron/infisical-client": {
|
|
29
|
+
"optional": false
|
|
30
|
+
}
|
|
25
31
|
},
|
|
26
32
|
"devDependencies": {
|
|
27
|
-
"@theholocron/
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
33
|
+
"@theholocron/eslint-config": "^7.3.0",
|
|
34
|
+
"@theholocron/infisical-client": "^1.1.0",
|
|
35
|
+
"@theholocron/tsconfig": "^7.3.0",
|
|
36
|
+
"@theholocron/tsdown-config": "^7.3.0",
|
|
37
|
+
"@theholocron/vitest-config": "^7.3.0",
|
|
38
|
+
"@types/node": "^26",
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
40
|
+
"@vitest/eslint-plugin": "^1.6.23",
|
|
41
|
+
"eslint": "^10.7.0",
|
|
42
|
+
"eslint-plugin-n": "^18.2.2",
|
|
43
|
+
"globals": "^17.7.0",
|
|
32
44
|
"tsdown": "^0.22.3",
|
|
33
45
|
"tsx": "^4.22.4",
|
|
34
46
|
"typescript": "^5.9.3",
|
|
35
|
-
"vitest": "^
|
|
36
|
-
"@theholocron/cli": "2.0.
|
|
47
|
+
"vitest": "^4.1.10",
|
|
48
|
+
"@theholocron/cli": "2.0.1"
|
|
37
49
|
},
|
|
38
50
|
"publishConfig": {
|
|
39
51
|
"access": "public"
|