@theholocron/holocron-plugin-doppler 2.0.0-alpha.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/index.d.mts +7 -57
- package/dist/index.mjs +31 -128
- package/package.json +17 -10
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ plus exports `verifyToken` + `AUTH_HINT` for use by `holocron auth`.
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
<!-- prettier-ignore -->
|
|
11
12
|
```bash
|
|
12
13
|
pnpm add -D @theholocron/holocron-plugin-doppler@alpha
|
|
14
|
+
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
## Auth
|
|
@@ -28,6 +30,7 @@ Token resolution order (matches the standard 4-step precedence set by
|
|
|
28
30
|
Doppler's free tier does not expose Service Accounts (Team+ only). Use
|
|
29
31
|
a Personal Token or CLI token instead.
|
|
30
32
|
|
|
33
|
+
<!-- prettier-ignore -->
|
|
31
34
|
```bash
|
|
32
35
|
# 1. Install the Doppler CLI
|
|
33
36
|
brew install dopplerhq/cli/doppler
|
|
@@ -43,6 +46,7 @@ holocron auth set doppler $(doppler configure get token --plain)
|
|
|
43
46
|
|
|
44
47
|
# 4. Verify:
|
|
45
48
|
holocron auth check doppler
|
|
49
|
+
|
|
46
50
|
```
|
|
47
51
|
|
|
48
52
|
**CI**: the keyring is not available in headless containers. Expose
|
|
@@ -52,12 +56,14 @@ precedence still work; step 4 quietly falls through.
|
|
|
52
56
|
|
|
53
57
|
## Config
|
|
54
58
|
|
|
59
|
+
<!-- prettier-ignore -->
|
|
55
60
|
```jsonc
|
|
56
61
|
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
"providers": {
|
|
63
|
+
"vault": ["doppler", { "project": "my-app", "config": "dev" }],
|
|
64
|
+
},
|
|
60
65
|
}
|
|
66
|
+
|
|
61
67
|
```
|
|
62
68
|
|
|
63
69
|
- `project` (required) — Doppler project name. `read` / `list` /
|
|
@@ -67,8 +73,10 @@ precedence still work; step 4 quietly falls through.
|
|
|
67
73
|
|
|
68
74
|
Individual `read` / `write` calls take a fully-qualified reference:
|
|
69
75
|
|
|
76
|
+
<!-- prettier-ignore -->
|
|
70
77
|
```
|
|
71
78
|
doppler://<project>/<config>/<name>
|
|
79
|
+
|
|
72
80
|
```
|
|
73
81
|
|
|
74
82
|
The default project + config in options apply to `list()`,
|
package/dist/index.d.mts
CHANGED
|
@@ -1,58 +1,8 @@
|
|
|
1
|
-
import { EnsureResult, Vault } from "@theholocron/cli";
|
|
1
|
+
import { AuthError, EnsureResult, ResolveTokenInput, Vault } from "@theholocron/cli";
|
|
2
|
+
import { DopplerClient, createDopplerClient } from "@theholocron/doppler-client";
|
|
2
3
|
|
|
3
4
|
//#region src/auth.d.ts
|
|
4
|
-
|
|
5
|
-
* Token resolution for the Doppler 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_DOPPLER_TOKEN env var (preferred — explicit intent)
|
|
11
|
-
* 3. DOPPLER_TOKEN env var (Doppler-native, works in CI)
|
|
12
|
-
* 4. keyring (com.theholocron.cli / "doppler")
|
|
13
|
-
* 5. AuthError with a hint pointing at `doppler configure`
|
|
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 api.doppler.com/v3.
|
|
31
|
-
*
|
|
32
|
-
* Same pattern as the GitHub / Vercel / Neon REST clients — bearer
|
|
33
|
-
* auth, JSON-only bodies, transport-failure wrapping with `status: 0`
|
|
34
|
-
* so the orchestrator's soft-skip path sees a clear "Doppler GET /path
|
|
35
|
-
* failed" message instead of a generic `TypeError: fetch failed`.
|
|
36
|
-
*/
|
|
37
|
-
interface RestClientOptions {
|
|
38
|
-
token: string;
|
|
39
|
-
fetch?: typeof fetch;
|
|
40
|
-
baseUrl?: string;
|
|
41
|
-
}
|
|
42
|
-
interface RequestOptions {
|
|
43
|
-
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
44
|
-
body?: unknown;
|
|
45
|
-
query?: Record<string, string>;
|
|
46
|
-
/** Treat this response as void even if 200 is returned. */
|
|
47
|
-
expectNoContent?: boolean;
|
|
48
|
-
}
|
|
49
|
-
declare class DopplerRestClient {
|
|
50
|
-
private readonly token;
|
|
51
|
-
private readonly fetchImpl;
|
|
52
|
-
readonly baseUrl: string;
|
|
53
|
-
constructor(opts: RestClientOptions);
|
|
54
|
-
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
55
|
-
}
|
|
5
|
+
declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
|
|
56
6
|
//#endregion
|
|
57
7
|
//#region src/capabilities/vault.d.ts
|
|
58
8
|
interface DopplerVaultOptions {
|
|
@@ -62,12 +12,12 @@ interface DopplerVaultOptions {
|
|
|
62
12
|
config: string;
|
|
63
13
|
}
|
|
64
14
|
declare class DopplerVault implements Vault {
|
|
65
|
-
private readonly
|
|
15
|
+
private readonly client;
|
|
66
16
|
readonly key: "vault";
|
|
67
17
|
readonly providerName = "doppler";
|
|
68
18
|
private readonly project;
|
|
69
19
|
private readonly config;
|
|
70
|
-
constructor(
|
|
20
|
+
constructor(client: DopplerClient, opts: DopplerVaultOptions);
|
|
71
21
|
read(reference: string): Promise<string>;
|
|
72
22
|
write(reference: string, value: string): Promise<void>;
|
|
73
23
|
list(): Promise<string[]>;
|
|
@@ -112,7 +62,7 @@ interface DopplerPluginOptions extends ResolveTokenInput, DopplerVaultOptions {
|
|
|
112
62
|
}
|
|
113
63
|
interface PluginContext {
|
|
114
64
|
options: DopplerPluginOptions;
|
|
115
|
-
|
|
65
|
+
client: DopplerClient;
|
|
116
66
|
}
|
|
117
67
|
declare function createContext(options: DopplerPluginOptions): PluginContext;
|
|
118
68
|
declare function vault(ctx: PluginContext): Vault;
|
|
@@ -128,4 +78,4 @@ declare function createPlugin(options: DopplerPluginOptions): {
|
|
|
128
78
|
*/
|
|
129
79
|
declare const AUTH_HINT: string;
|
|
130
80
|
//#endregion
|
|
131
|
-
export { AUTH_HINT, AuthError, DopplerPluginOptions,
|
|
81
|
+
export { AUTH_HINT, AuthError, DopplerPluginOptions, DopplerVault, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createDopplerClient, 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 { createDopplerClient } from "@theholocron/doppler-client";
|
|
2
3
|
//#region src/auth.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* 2. HOLOCRON_DOPPLER_TOKEN env var (preferred — explicit intent)
|
|
10
|
-
* 3. DOPPLER_TOKEN env var (Doppler-native, works in CI)
|
|
11
|
-
* 4. keyring (com.theholocron.cli / "doppler")
|
|
12
|
-
* 5. AuthError with a hint pointing at `doppler configure`
|
|
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_DOPPLER_TOKEN || env.DOPPLER_TOKEN || keyring("doppler");
|
|
21
|
-
if (!token) throw new AuthError("no Doppler token found. Pass --token <TOKEN>, set HOLOCRON_DOPPLER_TOKEN / DOPPLER_TOKEN, or run: holocron auth set doppler $(doppler configure get token --plain)");
|
|
22
|
-
return token;
|
|
23
|
-
}
|
|
4
|
+
const resolveToken = createResolveToken({
|
|
5
|
+
envName: "HOLOCRON_DOPPLER_TOKEN",
|
|
6
|
+
vendorEnvName: "DOPPLER_TOKEN",
|
|
7
|
+
keyringService: "doppler",
|
|
8
|
+
errorMessage: "no Doppler token found. Pass --token <TOKEN>, set HOLOCRON_DOPPLER_TOKEN / DOPPLER_TOKEN, or run: holocron auth set doppler $(doppler configure get token --plain)"
|
|
9
|
+
});
|
|
24
10
|
//#endregion
|
|
25
11
|
//#region src/capabilities/vault.ts
|
|
26
12
|
/**
|
|
@@ -48,13 +34,13 @@ function resolveToken(input = {}) {
|
|
|
48
34
|
* ProviderApiError with `status: 409` which we catch and swallow.
|
|
49
35
|
*/
|
|
50
36
|
var DopplerVault = class {
|
|
51
|
-
|
|
37
|
+
client;
|
|
52
38
|
key = "vault";
|
|
53
39
|
providerName = "doppler";
|
|
54
40
|
project;
|
|
55
41
|
config;
|
|
56
|
-
constructor(
|
|
57
|
-
this.
|
|
42
|
+
constructor(client, opts) {
|
|
43
|
+
this.client = client;
|
|
58
44
|
if (!opts.project) throw new Error("DopplerVault requires `project` in options");
|
|
59
45
|
if (!opts.config) throw new Error("DopplerVault requires `config` in options");
|
|
60
46
|
this.project = opts.project;
|
|
@@ -62,53 +48,29 @@ var DopplerVault = class {
|
|
|
62
48
|
}
|
|
63
49
|
async read(reference) {
|
|
64
50
|
const parsed = parseReference(reference);
|
|
65
|
-
const res = await this.
|
|
66
|
-
project: parsed.project,
|
|
67
|
-
config: parsed.config,
|
|
68
|
-
name: parsed.name
|
|
69
|
-
} });
|
|
51
|
+
const res = await this.client.secrets.get(parsed.project, parsed.config, parsed.name);
|
|
70
52
|
return res.value?.computed ?? res.value?.raw ?? "";
|
|
71
53
|
}
|
|
72
54
|
async write(reference, value) {
|
|
73
55
|
const parsed = parseReference(reference);
|
|
74
|
-
await this.
|
|
75
|
-
method: "POST",
|
|
76
|
-
body: {
|
|
77
|
-
project: parsed.project,
|
|
78
|
-
config: parsed.config,
|
|
79
|
-
secrets: { [parsed.name]: value }
|
|
80
|
-
}
|
|
81
|
-
});
|
|
56
|
+
await this.client.secrets.update(parsed.project, parsed.config, { [parsed.name]: value });
|
|
82
57
|
}
|
|
83
58
|
async list() {
|
|
84
|
-
const res = await this.
|
|
85
|
-
project: this.project,
|
|
86
|
-
config: this.config
|
|
87
|
-
} });
|
|
59
|
+
const res = await this.client.secrets.list(this.project, this.config);
|
|
88
60
|
return Object.keys(res.secrets ?? {});
|
|
89
61
|
}
|
|
90
62
|
async environments() {
|
|
91
|
-
return ((await this.
|
|
63
|
+
return ((await this.client.environments.list(this.project)).environments ?? []).map((e) => e.slug ?? e.name ?? "").filter(Boolean);
|
|
92
64
|
}
|
|
93
65
|
async readEnvironment(environmentId) {
|
|
94
|
-
const res = await this.
|
|
95
|
-
project: this.project,
|
|
96
|
-
config: environmentId,
|
|
97
|
-
format: "json"
|
|
98
|
-
} });
|
|
66
|
+
const res = await this.client.secrets.download(this.project, environmentId);
|
|
99
67
|
const out = {};
|
|
100
68
|
for (const [k, v] of Object.entries(res)) if (typeof v === "string") out[k] = v;
|
|
101
69
|
return out;
|
|
102
70
|
}
|
|
103
71
|
async ensureProject(name) {
|
|
104
72
|
try {
|
|
105
|
-
await this.
|
|
106
|
-
method: "POST",
|
|
107
|
-
body: {
|
|
108
|
-
name,
|
|
109
|
-
description: `Managed by holocron`
|
|
110
|
-
}
|
|
111
|
-
});
|
|
73
|
+
await this.client.projects.create(name, "Managed by holocron");
|
|
112
74
|
return { alreadyExists: false };
|
|
113
75
|
} catch (err) {
|
|
114
76
|
if (isConflict(err)) return { alreadyExists: true };
|
|
@@ -117,14 +79,7 @@ var DopplerVault = class {
|
|
|
117
79
|
}
|
|
118
80
|
async ensureEnvironment(project, name) {
|
|
119
81
|
try {
|
|
120
|
-
await this.
|
|
121
|
-
method: "POST",
|
|
122
|
-
body: {
|
|
123
|
-
project,
|
|
124
|
-
name,
|
|
125
|
-
slug: name
|
|
126
|
-
}
|
|
127
|
-
});
|
|
82
|
+
await this.client.environments.create(project, name, name);
|
|
128
83
|
return { alreadyExists: false };
|
|
129
84
|
} catch (err) {
|
|
130
85
|
if (isConflict(err)) return { alreadyExists: true };
|
|
@@ -171,60 +126,6 @@ function hasAlreadyExistsBody(details) {
|
|
|
171
126
|
return /already exists/i.test(details);
|
|
172
127
|
}
|
|
173
128
|
//#endregion
|
|
174
|
-
//#region src/rest.ts
|
|
175
|
-
/**
|
|
176
|
-
* Thin REST wrapper around api.doppler.com/v3.
|
|
177
|
-
*
|
|
178
|
-
* Same pattern as the GitHub / Vercel / Neon REST clients — bearer
|
|
179
|
-
* auth, JSON-only bodies, transport-failure wrapping with `status: 0`
|
|
180
|
-
* so the orchestrator's soft-skip path sees a clear "Doppler GET /path
|
|
181
|
-
* failed" message instead of a generic `TypeError: fetch failed`.
|
|
182
|
-
*/
|
|
183
|
-
var DopplerRestClient = class {
|
|
184
|
-
token;
|
|
185
|
-
fetchImpl;
|
|
186
|
-
baseUrl;
|
|
187
|
-
constructor(opts) {
|
|
188
|
-
this.token = opts.token;
|
|
189
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
190
|
-
let url = opts.baseUrl ?? "https://api.doppler.com/v3";
|
|
191
|
-
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
192
|
-
this.baseUrl = url;
|
|
193
|
-
}
|
|
194
|
-
async request(path, opts = {}) {
|
|
195
|
-
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : "/" + path}`);
|
|
196
|
-
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
197
|
-
const fullUrl = url.toString();
|
|
198
|
-
const headers = {
|
|
199
|
-
authorization: `Bearer ${this.token}`,
|
|
200
|
-
accept: "application/json"
|
|
201
|
-
};
|
|
202
|
-
const init = {
|
|
203
|
-
method: opts.method ?? "GET",
|
|
204
|
-
headers
|
|
205
|
-
};
|
|
206
|
-
if (opts.body !== void 0) {
|
|
207
|
-
headers["content-type"] = "application/json";
|
|
208
|
-
init.body = JSON.stringify(opts.body);
|
|
209
|
-
}
|
|
210
|
-
let res;
|
|
211
|
-
try {
|
|
212
|
-
res = await this.fetchImpl(fullUrl, init);
|
|
213
|
-
} catch (err) {
|
|
214
|
-
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
215
|
-
throw new ProviderApiError(`Doppler ${init.method} ${path} failed: ${detail}`, 0, void 0);
|
|
216
|
-
}
|
|
217
|
-
if (!res.ok) {
|
|
218
|
-
const body = await res.text().catch(() => "");
|
|
219
|
-
throw new ProviderApiError(`Doppler ${init.method} ${path} → ${res.status}`, res.status, body);
|
|
220
|
-
}
|
|
221
|
-
if (opts.expectNoContent || res.status === 204) return void 0;
|
|
222
|
-
const text = await res.text();
|
|
223
|
-
if (!text) return void 0;
|
|
224
|
-
return JSON.parse(text);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
//#endregion
|
|
228
129
|
//#region src/verify-token.ts
|
|
229
130
|
/**
|
|
230
131
|
* `verifyToken` — plugin-level export used by `holocron auth set` +
|
|
@@ -237,12 +138,13 @@ var DopplerRestClient = class {
|
|
|
237
138
|
* what we don't have yet at bootstrap time.
|
|
238
139
|
*/
|
|
239
140
|
async function verifyToken(token, opts = {}) {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
141
|
+
const client = createDopplerClient({
|
|
142
|
+
token,
|
|
143
|
+
baseUrl: opts.baseUrl,
|
|
144
|
+
fetch: opts.fetch
|
|
145
|
+
});
|
|
244
146
|
try {
|
|
245
|
-
const me = await
|
|
147
|
+
const me = await client.me.get();
|
|
246
148
|
const workplace = me.workplace?.name ?? me.slug ?? "unknown";
|
|
247
149
|
return {
|
|
248
150
|
ok: true,
|
|
@@ -258,16 +160,17 @@ async function verifyToken(token, opts = {}) {
|
|
|
258
160
|
//#endregion
|
|
259
161
|
//#region src/index.ts
|
|
260
162
|
function createContext(options) {
|
|
261
|
-
const restOpts = { token: resolveToken(options) };
|
|
262
|
-
if (options.baseUrl !== void 0) restOpts.baseUrl = options.baseUrl;
|
|
263
|
-
if (options.fetch !== void 0) restOpts.fetch = options.fetch;
|
|
264
163
|
return {
|
|
265
164
|
options,
|
|
266
|
-
|
|
165
|
+
client: createDopplerClient({
|
|
166
|
+
token: resolveToken(options),
|
|
167
|
+
baseUrl: options.baseUrl,
|
|
168
|
+
fetch: options.fetch
|
|
169
|
+
})
|
|
267
170
|
};
|
|
268
171
|
}
|
|
269
172
|
function vault(ctx) {
|
|
270
|
-
return new DopplerVault(ctx.
|
|
173
|
+
return new DopplerVault(ctx.client, {
|
|
271
174
|
project: ctx.options.project,
|
|
272
175
|
config: ctx.options.config
|
|
273
176
|
});
|
|
@@ -285,4 +188,4 @@ function createPlugin(options) {
|
|
|
285
188
|
*/
|
|
286
189
|
const AUTH_HINT = "install the Doppler CLI (`brew install dopplerhq/cli/doppler`), run `doppler login`, then `holocron auth set doppler $(doppler configure get token --plain)`";
|
|
287
190
|
//#endregion
|
|
288
|
-
export { AUTH_HINT, AuthError,
|
|
191
|
+
export { AUTH_HINT, AuthError, DopplerVault, createContext, createDopplerClient, createPlugin, resolveToken, vault, verifyToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-doppler",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Holocron plugin for Doppler. Implements the vault capability against Doppler's REST API — projects, configs, secrets, plus verifyToken + AUTH_HINT for `holocron auth`.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-doppler#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
@@ -21,19 +21,26 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@theholocron/
|
|
24
|
+
"@theholocron/doppler-client": "^1.1.0",
|
|
25
|
+
"@theholocron/cli": "2.0.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@theholocron/
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"vitest": "^
|
|
28
|
+
"@theholocron/doppler-client": "^1.1.0",
|
|
29
|
+
"@theholocron/eslint-config": "^7.3.0",
|
|
30
|
+
"@theholocron/tsconfig": "^7.3.0",
|
|
31
|
+
"@theholocron/tsdown-config": "^7.3.0",
|
|
32
|
+
"@theholocron/vitest-config": "^7.3.0",
|
|
33
|
+
"@types/node": "^26",
|
|
34
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
35
|
+
"@vitest/eslint-plugin": "^1.6.23",
|
|
36
|
+
"eslint": "^10.7.0",
|
|
37
|
+
"eslint-plugin-n": "^18.2.2",
|
|
38
|
+
"globals": "^17.7.0",
|
|
34
39
|
"tsdown": "^0.22.3",
|
|
35
40
|
"tsx": "^4.22.4",
|
|
36
|
-
"
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.1.10",
|
|
43
|
+
"@theholocron/cli": "2.0.0"
|
|
37
44
|
},
|
|
38
45
|
"publishConfig": {
|
|
39
46
|
"access": "public"
|