@sponson/adapters 0.4.0 → 0.6.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 +1 -1
- package/dist/algolia.d.ts +6 -0
- package/dist/algolia.js +151 -0
- package/dist/algolia.js.map +1 -0
- package/dist/clerk.js +1 -1
- package/dist/clerk.js.map +1 -1
- package/dist/cloud_run.d.ts +12 -0
- package/dist/cloud_run.js +469 -0
- package/dist/cloud_run.js.map +1 -0
- package/dist/cloudflare.d.ts +25 -0
- package/dist/cloudflare.js +310 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/common.d.ts +2 -6
- package/dist/common.js.map +1 -1
- package/dist/digest.d.ts +46 -0
- package/dist/digest.js +129 -0
- package/dist/digest.js.map +1 -0
- package/dist/digitalocean.d.ts +5 -0
- package/dist/digitalocean.js +243 -0
- package/dist/digitalocean.js.map +1 -0
- package/dist/http-adapter-action.d.ts +63 -0
- package/dist/http-adapter-action.js +369 -0
- package/dist/http-adapter-action.js.map +1 -0
- package/dist/http-adapter-auth.d.ts +94 -0
- package/dist/http-adapter-auth.js +440 -0
- package/dist/http-adapter-auth.js.map +1 -0
- package/dist/http-adapter-client.d.ts +57 -0
- package/dist/http-adapter-client.js +163 -0
- package/dist/http-adapter-client.js.map +1 -0
- package/dist/http-adapter-graphql.d.ts +39 -0
- package/dist/http-adapter-graphql.js +157 -0
- package/dist/http-adapter-graphql.js.map +1 -0
- package/dist/http-adapter-list.d.ts +33 -0
- package/dist/http-adapter-list.js +421 -0
- package/dist/http-adapter-list.js.map +1 -0
- package/dist/http-adapter-resource.d.ts +29 -0
- package/dist/http-adapter-resource.js +462 -0
- package/dist/http-adapter-resource.js.map +1 -0
- package/dist/http-adapter-spec.d.ts +216 -0
- package/dist/http-adapter-spec.js +660 -0
- package/dist/http-adapter-spec.js.map +1 -0
- package/dist/http-adapter.d.ts +16 -0
- package/dist/http-adapter.js +28 -0
- package/dist/http-adapter.js.map +1 -0
- package/dist/http.d.ts +61 -2
- package/dist/http.js +79 -18
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/launchdarkly.d.ts +15 -0
- package/dist/launchdarkly.js +337 -0
- package/dist/launchdarkly.js.map +1 -0
- package/dist/manual.d.ts +13 -0
- package/dist/manual.js +201 -0
- package/dist/manual.js.map +1 -0
- package/dist/neon.js +1 -1
- package/dist/neon.js.map +1 -1
- package/dist/netlify.d.ts +18 -0
- package/dist/netlify.js +436 -0
- package/dist/netlify.js.map +1 -0
- package/dist/planetscale.d.ts +10 -0
- package/dist/planetscale.js +311 -0
- package/dist/planetscale.js.map +1 -0
- package/dist/providers.d.ts +28 -0
- package/dist/providers.js +41 -0
- package/dist/providers.js.map +1 -0
- package/dist/recipes.d.ts +167 -0
- package/dist/recipes.js +246 -0
- package/dist/recipes.js.map +1 -0
- package/dist/secrets.d.ts +44 -0
- package/dist/secrets.js +279 -0
- package/dist/secrets.js.map +1 -1
- package/dist/sigv4.d.ts +56 -0
- package/dist/sigv4.js +118 -0
- package/dist/sigv4.js.map +1 -0
- package/dist/supabase.d.ts +9 -0
- package/dist/supabase.js +385 -0
- package/dist/supabase.js.map +1 -0
- package/dist/vercel.js +1 -1
- package/dist/vercel.js.map +1 -1
- package/dist/xata.d.ts +8 -0
- package/dist/xata.js +142 -0
- package/dist/xata.js.map +1 -0
- package/dist/xml.d.ts +25 -0
- package/dist/xml.js +211 -0
- package/dist/xml.js.map +1 -0
- package/package.json +11 -4
- package/recipes/amplify.yaml +147 -0
- package/recipes/auth0.yaml +178 -0
- package/recipes/azure_swa.yaml +116 -0
- package/recipes/cloudflare.yaml +108 -0
- package/recipes/cockroachdb.yaml +132 -0
- package/recipes/configcat.yaml +123 -0
- package/recipes/datadog.yaml +136 -0
- package/recipes/firebase.yaml +84 -0
- package/recipes/flagsmith.yaml +98 -0
- package/recipes/fly.yaml +97 -0
- package/recipes/fly_machines.yaml +67 -0
- package/recipes/gcs.yaml +102 -0
- package/recipes/github.yaml +124 -0
- package/recipes/growthbook.yaml +124 -0
- package/recipes/heroku.yaml +98 -0
- package/recipes/honeycomb.yaml +89 -0
- package/recipes/inngest.yaml +97 -0
- package/recipes/kinde.yaml +120 -0
- package/recipes/mongodb_atlas.yaml +119 -0
- package/recipes/okta.yaml +177 -0
- package/recipes/posthog.yaml +127 -0
- package/recipes/postmark.yaml +105 -0
- package/recipes/postmark_server.yaml +93 -0
- package/recipes/prisma.yaml +150 -0
- package/recipes/qstash.yaml +134 -0
- package/recipes/railway.yaml +130 -0
- package/recipes/render.yaml +103 -0
- package/recipes/resend.yaml +130 -0
- package/recipes/s3.yaml +108 -0
- package/recipes/sendgrid.yaml +165 -0
- package/recipes/sentry.yaml +158 -0
- package/recipes/split.yaml +121 -0
- package/recipes/statsig.yaml +124 -0
- package/recipes/stripe.yaml +310 -0
- package/recipes/stytch.yaml +113 -0
- package/recipes/svix.yaml +109 -0
- package/recipes/turso.yaml +98 -0
- package/recipes/unleash.yaml +149 -0
- package/recipes/upstash.yaml +130 -0
- package/recipes/vercel.yaml +116 -0
- package/recipes/workos.yaml +70 -0
package/dist/recipes.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipes: verified `http` specs for one provider's operations, shipped as YAML in `packages/adapters/recipes/`
|
|
3
|
+
* (one file per provider), so that a plan line names an operation and fills in its typed params instead of writing
|
|
4
|
+
* request templates (ADR 0020, docs/recipes.md).
|
|
5
|
+
*
|
|
6
|
+
* providers: { http: { cloudflare: { recipe: cloudflare } } } # optional: the recipe's API defaults
|
|
7
|
+
* - { id: dns, adapter: http, op: resource, recipe: cloudflare.dns_cname, zone_id: …, name: …, target: … }
|
|
8
|
+
*
|
|
9
|
+
* A recipe line expands, before anything else reads it, into the `http.resource`, `http.list_item` or `http.action` params the
|
|
10
|
+
* recipe declares, with `{ param: <name> }` values and `{<name>}` path placeholders filled from the line. Identity
|
|
11
|
+
* is never the recipe's text: the ledger records the resolved API block (base URL, credential variable names,
|
|
12
|
+
* headers, encoding) and the keys and record ids the expanded requests produce, exactly as for a hand-written
|
|
13
|
+
* line. Pure apart from reading the shipped files once.
|
|
14
|
+
*/
|
|
15
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { parse as parseYaml } from "yaml";
|
|
18
|
+
import { SponsonError, markerKind } from "@sponson/core";
|
|
19
|
+
import { paramError } from "./common.js";
|
|
20
|
+
import { isObject } from "./http.js";
|
|
21
|
+
/** Where the shipped recipe files are, in the source tree and in the published package alike. */
|
|
22
|
+
export const RECIPES_DIR = fileURLToPath(new URL("../recipes/", import.meta.url));
|
|
23
|
+
/**
|
|
24
|
+
* The environment variables a recipe's API block reads its credential from, in the order the block names them.
|
|
25
|
+
* Empty for a recipe without an API block and for a token a command prints (`az`, `gcloud`: their own login).
|
|
26
|
+
*/
|
|
27
|
+
export function recipeCredentialEnv(recipe) {
|
|
28
|
+
const auth = recipe.api?.auth;
|
|
29
|
+
if (!isObject(auth))
|
|
30
|
+
return [];
|
|
31
|
+
const names = (o, keys) => (isObject(o) ? keys.flatMap((k) => (typeof o[k] === "string" ? [o[k]] : [])) : []);
|
|
32
|
+
if (Array.isArray(auth.headers))
|
|
33
|
+
return auth.headers.flatMap((h) => names(h, ["value_env"]));
|
|
34
|
+
return [
|
|
35
|
+
...names(auth, ["bearer_env", "value_env"]),
|
|
36
|
+
...names(auth.oauth2_client_credentials, ["client_id_env", "client_secret_env"]),
|
|
37
|
+
...names(auth.aws_sigv4, ["access_key_id_env", "secret_access_key_env"]),
|
|
38
|
+
...names(auth.digest, ["user_env", "password_env"]),
|
|
39
|
+
...names(auth.basic, ["user_env", "password_env"]),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
const ADAPTER = "http";
|
|
43
|
+
const cache = new Map();
|
|
44
|
+
/** Recipes made known without a file (`useRecipe`), by provider. */
|
|
45
|
+
const unshipped = new Map();
|
|
46
|
+
/** The providers that have a recipe, sorted. */
|
|
47
|
+
export function recipeNames() {
|
|
48
|
+
const files = readdirSync(RECIPES_DIR)
|
|
49
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
50
|
+
.map((f) => f.slice(0, -".yaml".length));
|
|
51
|
+
return [...new Set([...files, ...unshipped.keys()])].sort();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Make a recipe known for this process as if it were shipped, until the returned function is called. For tests of
|
|
55
|
+
* code that recipes drive (the adapter's own tests use it for a list item recipe); not exported by the package.
|
|
56
|
+
*/
|
|
57
|
+
export function useRecipe(recipe) {
|
|
58
|
+
unshipped.set(recipe.provider, recipe);
|
|
59
|
+
cache.set(recipe.provider, recipe);
|
|
60
|
+
return () => {
|
|
61
|
+
unshipped.delete(recipe.provider);
|
|
62
|
+
cache.delete(recipe.provider);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** A provider's recipe; PARAM_INVALID naming the known ones when there is none. */
|
|
66
|
+
export function loadRecipe(provider, param = "recipe") {
|
|
67
|
+
const cached = cache.get(provider);
|
|
68
|
+
if (cached)
|
|
69
|
+
return cached;
|
|
70
|
+
const known = recipeNames();
|
|
71
|
+
if (!known.includes(provider))
|
|
72
|
+
throw paramError(ADAPTER, `no recipe for \`${provider}\` (known: ${known.join(", ") || "none"})`, param);
|
|
73
|
+
const recipe = parseYaml(readFileSync(`${RECIPES_DIR}${provider}.yaml`, "utf8"));
|
|
74
|
+
// A malformed shipped file is a bug of this package (packages/adapters/src/recipes.test.ts checks every one).
|
|
75
|
+
if (!isObject(recipe) || recipe.provider !== provider || !isObject(recipe.ops) || (recipe.api !== undefined && !isObject(recipe.api))) {
|
|
76
|
+
throw new SponsonError("INTERNAL", `${ADAPTER}: recipes/${provider}.yaml is malformed`, { adapter: ADAPTER, recipe: provider });
|
|
77
|
+
}
|
|
78
|
+
cache.set(provider, recipe);
|
|
79
|
+
return recipe;
|
|
80
|
+
}
|
|
81
|
+
/** Every shipped recipe. */
|
|
82
|
+
export function loadRecipes() {
|
|
83
|
+
return recipeNames().map((p) => loadRecipe(p));
|
|
84
|
+
}
|
|
85
|
+
/** A line's `recipe: <provider>.<op>`, located. */
|
|
86
|
+
export function recipeOp(ref) {
|
|
87
|
+
if (typeof ref !== "string" || !/^[a-z0-9][a-z0-9_-]*\.[a-z0-9_]+$/.test(ref)) {
|
|
88
|
+
throw paramError(ADAPTER, "`recipe` must be `<provider>.<op>`, e.g. `cloudflare.dns_cname` (see docs/recipes.md)", "recipe");
|
|
89
|
+
}
|
|
90
|
+
const [provider, name] = ref.split(".");
|
|
91
|
+
const recipe = loadRecipe(provider);
|
|
92
|
+
const op = recipe.ops[name];
|
|
93
|
+
if (!op)
|
|
94
|
+
throw paramError(ADAPTER, `recipe \`${provider}\` has no op \`${name}\` (known: ${Object.keys(recipe.ops).join(", ")})`, "recipe");
|
|
95
|
+
return { recipe, name, op };
|
|
96
|
+
}
|
|
97
|
+
/** The API a line uses: its `api`, or for a recipe line without one, the recipe's provider. */
|
|
98
|
+
export function lineApi(params) {
|
|
99
|
+
if (params.api !== undefined || typeof params.recipe !== "string")
|
|
100
|
+
return params.api;
|
|
101
|
+
return params.recipe.split(".")[0];
|
|
102
|
+
}
|
|
103
|
+
/** Params of a recipe line that are the line's own, not the recipe's. */
|
|
104
|
+
const LINE_PARAMS = ["api", "recipe", "destroy", "credential"];
|
|
105
|
+
/** A value the engine fills in later or keeps as it is: a reference, a marker, an uninterpolated `${…}`. */
|
|
106
|
+
function isDeferred(v) {
|
|
107
|
+
if (markerKind(v) !== null)
|
|
108
|
+
return true;
|
|
109
|
+
if (typeof v === "string")
|
|
110
|
+
return v.includes("${");
|
|
111
|
+
if (!isObject(v))
|
|
112
|
+
return false;
|
|
113
|
+
const keys = Object.keys(v);
|
|
114
|
+
return keys.length === 1 && ["from", "secret", "keep"].includes(keys[0]);
|
|
115
|
+
}
|
|
116
|
+
const TYPE_CHECK = {
|
|
117
|
+
string: (v) => typeof v === "string",
|
|
118
|
+
integer: (v) => Number.isInteger(v),
|
|
119
|
+
number: (v) => typeof v === "number" && Number.isFinite(v),
|
|
120
|
+
boolean: (v) => typeof v === "boolean",
|
|
121
|
+
list: (v) => Array.isArray(v),
|
|
122
|
+
map: (v) => isObject(v),
|
|
123
|
+
};
|
|
124
|
+
function checkValue(where, name, p, v) {
|
|
125
|
+
if (isDeferred(v))
|
|
126
|
+
return;
|
|
127
|
+
const fail = (why) => paramError(ADAPTER, `${where}: \`${name}\` ${why}`, name);
|
|
128
|
+
if (!TYPE_CHECK[p.type](v))
|
|
129
|
+
throw fail(`must be ${p.type === "integer" ? "an" : "a"} ${p.type} (${p.description})`);
|
|
130
|
+
if (p.enum && !p.enum.some((e) => e === v))
|
|
131
|
+
throw fail(`must be one of ${p.enum.map((e) => JSON.stringify(e)).join(", ")}`);
|
|
132
|
+
if (p.pattern && typeof v === "string" && !new RegExp(p.pattern).test(v))
|
|
133
|
+
throw fail(`must match ${p.pattern} (${p.description})`);
|
|
134
|
+
}
|
|
135
|
+
/** The value of every param the line sets or the recipe defaults, checked; PARAM_INVALID naming the param otherwise. */
|
|
136
|
+
export function recipeValues(params) {
|
|
137
|
+
const { recipe, name, op } = recipeOp(params.recipe);
|
|
138
|
+
const where = `recipe ${recipe.provider}.${name}`;
|
|
139
|
+
const declared = Object.keys(op.params);
|
|
140
|
+
const extra = Object.keys(params).find((k) => !LINE_PARAMS.includes(k) && !declared.includes(k));
|
|
141
|
+
if (extra)
|
|
142
|
+
throw paramError(ADAPTER, `${where}: \`${extra}\` is not one of its params (known: ${declared.join(", ")})`, extra);
|
|
143
|
+
const values = {};
|
|
144
|
+
for (const [p, spec] of Object.entries(op.params)) {
|
|
145
|
+
const v = params[p] ?? spec.default;
|
|
146
|
+
if (v === undefined || v === null) {
|
|
147
|
+
if (spec.required)
|
|
148
|
+
throw paramError(ADAPTER, `${where}: \`${p}\` is required: ${spec.description}`, p);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
checkValue(where, p, spec, v);
|
|
152
|
+
values[p] = v;
|
|
153
|
+
}
|
|
154
|
+
return values;
|
|
155
|
+
}
|
|
156
|
+
/** `{ param: <name> }`, the one template form of a recipe's http spec. */
|
|
157
|
+
function paramRef(v) {
|
|
158
|
+
return isObject(v) && Object.keys(v).length === 1 && typeof v.param === "string" ? v.param : undefined;
|
|
159
|
+
}
|
|
160
|
+
/** `{ join: [ … ] }`'s parts, when `v` is one: the other template form, concatenating whole values. */
|
|
161
|
+
function joinParts(v) {
|
|
162
|
+
return isObject(v) && Object.keys(v).length === 1 && Array.isArray(v.join) ? v.join : undefined;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `{ join: [<part>, …] }`: the parts (literal strings, numbers, or `{ param: <name> }`) as one string, such as a
|
|
166
|
+
* preview URL and a path. A part that is not known yet (a reference, a marker) stands for the whole value, so a
|
|
167
|
+
* pending URL makes the joined value pending; a param without a value removes the value. Never anything richer:
|
|
168
|
+
* a recipe stays data (ADR 0020, "Amendment").
|
|
169
|
+
*/
|
|
170
|
+
function join(parts, values) {
|
|
171
|
+
const filled = parts.map((p) => substitute(p, values));
|
|
172
|
+
if (filled.some((p) => p === undefined))
|
|
173
|
+
return undefined;
|
|
174
|
+
// `${ctx.*}` inside a string joins like any text; a marker or a `{ from }` the engine has not resolved does not.
|
|
175
|
+
const deferred = filled.find((p) => markerKind(p) !== null || isObject(p));
|
|
176
|
+
if (deferred !== undefined)
|
|
177
|
+
return deferred;
|
|
178
|
+
return filled.map((p) => String(p)).join("");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* `tpl` with every `{ param: <name> }` replaced by its value (a param without one removes its map key or list
|
|
182
|
+
* item) and every `{ join: [...] }` by its parts joined.
|
|
183
|
+
*/
|
|
184
|
+
export function substitute(tpl, values) {
|
|
185
|
+
const ref = paramRef(tpl);
|
|
186
|
+
if (ref !== undefined)
|
|
187
|
+
return values[ref];
|
|
188
|
+
const parts = joinParts(tpl);
|
|
189
|
+
if (parts !== undefined)
|
|
190
|
+
return join(parts, values);
|
|
191
|
+
if (Array.isArray(tpl))
|
|
192
|
+
return tpl.map((x) => substitute(x, values)).filter((x) => x !== undefined);
|
|
193
|
+
if (!isObject(tpl))
|
|
194
|
+
return tpl;
|
|
195
|
+
const out = {};
|
|
196
|
+
for (const [k, v] of Object.entries(tpl)) {
|
|
197
|
+
const s = substitute(v, values);
|
|
198
|
+
if (s !== undefined)
|
|
199
|
+
out[k] = s;
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A line's params as the `http.<kind>` op reads them: unchanged without `recipe`, else the recipe's spec with the
|
|
205
|
+
* line's values filled in (`vars` holds every value, for the `{<name>}` path placeholders).
|
|
206
|
+
*/
|
|
207
|
+
export function expandRecipe(params, kind) {
|
|
208
|
+
if (params.recipe === undefined)
|
|
209
|
+
return params;
|
|
210
|
+
const { recipe, name, op } = recipeOp(params.recipe);
|
|
211
|
+
if (op.kind !== kind)
|
|
212
|
+
throw paramError(ADAPTER, `recipe ${recipe.provider}.${name} is an \`http.${op.kind}\` recipe: write \`op: ${op.kind}\``, "recipe");
|
|
213
|
+
const values = recipeValues(params);
|
|
214
|
+
const spec = substitute(op.http, values);
|
|
215
|
+
const destroy = recipeDestroy(`recipe ${recipe.provider}.${name}`, op, params.destroy);
|
|
216
|
+
// A recipe without an API block (only `url` actions) gives the line no `api` unless it writes one.
|
|
217
|
+
const api = recipe.api ? lineApi(params) : params.api;
|
|
218
|
+
const credential = params.credential !== undefined ? { credential: params.credential } : {};
|
|
219
|
+
return { ...spec, ...(api !== undefined ? { api } : {}), vars: values, ...(destroy !== undefined ? { destroy } : {}), ...credential };
|
|
220
|
+
}
|
|
221
|
+
/** The line's `destroy`, or the op's default; `delete` of an op the provider cannot delete is PARAM_INVALID. */
|
|
222
|
+
function recipeDestroy(where, op, line) {
|
|
223
|
+
if (op.destroy === "never" && line !== undefined && line !== "keep") {
|
|
224
|
+
throw paramError(ADAPTER, `${where}: the provider offers no way to delete what it makes, so it is always left in place: remove \`destroy\` or write \`destroy: keep\``, "destroy");
|
|
225
|
+
}
|
|
226
|
+
return line ?? (op.destroy === undefined ? undefined : "keep");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* A `providers.http.<api>` block with `recipe:`, resolved: the recipe's API defaults, then the block's own keys
|
|
230
|
+
* over them. Blocks without `recipe:` are returned as they are. The result never names the recipe, so it (and with
|
|
231
|
+
* it the ledger identity) is the same as a hand-written block with the same values.
|
|
232
|
+
*/
|
|
233
|
+
export function resolveRecipeApi(block, where) {
|
|
234
|
+
if (block.recipe === undefined)
|
|
235
|
+
return block;
|
|
236
|
+
const known = recipeNames();
|
|
237
|
+
const { recipe: provider, ...overrides } = block;
|
|
238
|
+
if (typeof provider !== "string" || !known.includes(provider)) {
|
|
239
|
+
throw new SponsonError("PLAN_INVALID", `${ADAPTER}: ${where}.recipe: no recipe \`${String(provider)}\` (known: ${known.join(", ")})`, { adapter: ADAPTER, key: `${where}.recipe` });
|
|
240
|
+
}
|
|
241
|
+
const api = loadRecipe(provider).api;
|
|
242
|
+
if (!api)
|
|
243
|
+
throw new SponsonError("PLAN_INVALID", `${ADAPTER}: ${where}.recipe: recipe \`${provider}\` has no API block (its ops call the app's own URL)`, { adapter: ADAPTER, key: `${where}.recipe` });
|
|
244
|
+
return { ...api, ...overrides };
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=recipes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"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"}
|
package/dist/secrets.d.ts
CHANGED
|
@@ -26,3 +26,47 @@ export declare function awsSecretsManagerSource(exec?: Exec): SecretSource;
|
|
|
26
26
|
* so a plan never depends on whichever project the runner's gcloud happens to have configured.
|
|
27
27
|
*/
|
|
28
28
|
export declare function gcpSecretManagerSource(exec?: Exec): SecretSource;
|
|
29
|
+
/**
|
|
30
|
+
* `infisical://<projectId>/<env>/[<folder>/…/]<NAME>` → `infisical secrets get`, through the Infisical CLI
|
|
31
|
+
* (https://infisical.com/docs/cli/commands/secrets). The folders become `--path` (default `/`). Credentials come
|
|
32
|
+
* from the CLI's own conventions: `INFISICAL_TOKEN` (or `INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN`) for a machine
|
|
33
|
+
* identity, else the CLI's login; `INFISICAL_DOMAIN` for EU Cloud or a self-hosted instance. The project id is part of
|
|
34
|
+
* the reference because a machine identity has no `.infisical.json` to take it from.
|
|
35
|
+
*
|
|
36
|
+
* JSON output, not `--plain`: with `--plain` an unknown name prints nothing at all (exit 0), indistinguishable from
|
|
37
|
+
* an empty secret; with `--output json` it prints the entry with the value `*not found*`, which is refused here.
|
|
38
|
+
*/
|
|
39
|
+
export declare function infisicalSecretSource(exec?: Exec): SecretSource;
|
|
40
|
+
/**
|
|
41
|
+
* `azure-kv://<vault>/<name>[/<version>]` → `az keyvault secret show`, through the Azure CLI so neither the Azure SDK
|
|
42
|
+
* nor an Entra ID token flow enters the dependency tree. The CLI rather than `GET {vault}/secrets/{name}` with a token
|
|
43
|
+
* from `az account get-access-token`: one process instead of two, and the CLI already knows the vault DNS suffix of
|
|
44
|
+
* the signed-in cloud (`vault.azure.net`, `vault.azure.cn`, `vault.usgovcloudapi.net`) and every way to sign in
|
|
45
|
+
* (`az login`, a service principal, a managed identity, federated CI credentials).
|
|
46
|
+
* https://learn.microsoft.com/en-us/cli/azure/keyvault/secret#az-keyvault-secret-show
|
|
47
|
+
*/
|
|
48
|
+
export declare function azureKeyVaultSource(exec?: Exec): SecretSource;
|
|
49
|
+
/**
|
|
50
|
+
* `bws://<secret-id>` → `bws secret get`, through the Bitwarden Secrets Manager CLI
|
|
51
|
+
* (https://bitwarden.com/help/secrets-manager-cli/). The machine account's access token comes from
|
|
52
|
+
* `BWS_ACCESS_TOKEN`, a self-hosted server from `BWS_SERVER_URL` (both read by the CLI itself: crates/bws/src/cli.rs
|
|
53
|
+
* in bitwarden/sdk-sm). `--color no`: the CLI colours its JSON whenever it believes the terminal supports colour,
|
|
54
|
+
* which `FORCE_COLOR` in CI makes it believe.
|
|
55
|
+
*/
|
|
56
|
+
export declare function bitwardenSecretsManagerSource(exec?: Exec): SecretSource;
|
|
57
|
+
/**
|
|
58
|
+
* `vault://<mount>/<path>#<key>` → HashiCorp Vault's KV v2 "read secret version", `GET /v1/<mount>/data/<path>`
|
|
59
|
+
* (https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2), over the adapters' HTTP client, no SDK. The
|
|
60
|
+
* address is `VAULT_ADDR`, the token `VAULT_TOKEN` (sent as `X-Vault-Token`), and `VAULT_NAMESPACE`, when set, is
|
|
61
|
+
* sent as `X-Vault-Namespace` (Vault Enterprise, HCP Vault Dedicated: https://developer.hashicorp.com/vault/api-docs).
|
|
62
|
+
* The mount is the first segment; the latest version is read. `#key` picks one key of the secret's map.
|
|
63
|
+
*/
|
|
64
|
+
export declare const vaultSecretSource: SecretSource;
|
|
65
|
+
/**
|
|
66
|
+
* `op-connect://<vault>/<item>/[<section>/]<field>` → a 1Password Connect server's REST API
|
|
67
|
+
* (https://www.1password.dev/connect/api-reference/): `GET /v1/vaults/{vault}/items/{item}`, bearer
|
|
68
|
+
* `OP_CONNECT_TOKEN`, at `OP_CONNECT_HOST` (the variables 1Password's own Connect SDKs and `op` read). Vaults and
|
|
69
|
+
* items are named by id or by name / title (looked up with `GET /v1/vaults?filter=name eq "…"` and
|
|
70
|
+
* `GET /v1/vaults/{vault}/items?filter=title eq "…"`; a name must be unique); fields by id or label.
|
|
71
|
+
*/
|
|
72
|
+
export declare const opConnectSecretSource: SecretSource;
|
package/dist/secrets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { SponsonError } from "@sponson/core";
|
|
3
|
+
import { apiClient, isProviderError } from "./http.js";
|
|
3
4
|
/**
|
|
4
5
|
* Runs the real binary from PATH; rejects with its stderr when it fails. A binary that cannot be started keeps the
|
|
5
6
|
* system error's `code` (`ENOENT`, `EACCES`), so the caller can say what is wrong instead of echoing it.
|
|
@@ -183,6 +184,284 @@ export function gcpSecretManagerSource(exec = defaultExec) {
|
|
|
183
184
|
},
|
|
184
185
|
};
|
|
185
186
|
}
|
|
187
|
+
const INFISICAL_FORM = "expected infisical://<projectId>/<env>/<NAME> or infisical://<projectId>/<env>/<folder>/…/<NAME>";
|
|
188
|
+
/**
|
|
189
|
+
* What `infisical secrets get` prints for a name the folder does not hold, in place of a value (Infisical CLI,
|
|
190
|
+
* packages/cmd/secrets.go `getSecretsByNames`). It exits 0 with it, so it must be recognised here, never passed on.
|
|
191
|
+
*/
|
|
192
|
+
const INFISICAL_NOT_FOUND = "*not found*";
|
|
193
|
+
/**
|
|
194
|
+
* `infisical://<projectId>/<env>/[<folder>/…/]<NAME>` → `infisical secrets get`, through the Infisical CLI
|
|
195
|
+
* (https://infisical.com/docs/cli/commands/secrets). The folders become `--path` (default `/`). Credentials come
|
|
196
|
+
* from the CLI's own conventions: `INFISICAL_TOKEN` (or `INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN`) for a machine
|
|
197
|
+
* identity, else the CLI's login; `INFISICAL_DOMAIN` for EU Cloud or a self-hosted instance. The project id is part of
|
|
198
|
+
* the reference because a machine identity has no `.infisical.json` to take it from.
|
|
199
|
+
*
|
|
200
|
+
* JSON output, not `--plain`: with `--plain` an unknown name prints nothing at all (exit 0), indistinguishable from
|
|
201
|
+
* an empty secret; with `--output json` it prints the entry with the value `*not found*`, which is refused here.
|
|
202
|
+
*/
|
|
203
|
+
export function infisicalSecretSource(exec = defaultExec) {
|
|
204
|
+
return {
|
|
205
|
+
scheme: "infisical",
|
|
206
|
+
form: "`infisical://projectId/env/NAME` or `infisical://projectId/env/folder/…/NAME`",
|
|
207
|
+
resolvedBy: "`infisical secrets get NAME --projectId projectId --env env --path /folder/… --output json --silent` (Infisical CLI; credentials from `INFISICAL_TOKEN` or the CLI's login, the instance from `INFISICAL_DOMAIN`). A name the folder does not hold is `SECRET_UNRESOLVED`, never a value.",
|
|
208
|
+
async resolve(ref, env) {
|
|
209
|
+
const parts = stripScheme(ref, "infisical").split("/");
|
|
210
|
+
if (parts.length < 3 || parts.some((p) => p === ""))
|
|
211
|
+
throw unresolved(ref, INFISICAL_FORM);
|
|
212
|
+
const [projectId, environment, ...rest] = parts;
|
|
213
|
+
const name = rest.pop();
|
|
214
|
+
const path = `/${rest.join("/")}`;
|
|
215
|
+
const args = ["secrets", "get", name, "--projectId", projectId, "--env", environment, "--path", path, "--output", "json", "--silent"];
|
|
216
|
+
const out = await runCli(exec, ref, "infisical", args, env, [name, projectId, environment, ...rest]);
|
|
217
|
+
// Never echo the output (or a fragment of it) in these messages: it holds the value.
|
|
218
|
+
const entries = parseJson(out);
|
|
219
|
+
const entry = Array.isArray(entries) ? entries.find((e) => isRecord(e) && e.secretKey === name) : undefined;
|
|
220
|
+
const value = isRecord(entry) ? entry.secretValue : undefined;
|
|
221
|
+
if (entry === undefined || value === INFISICAL_NOT_FOUND)
|
|
222
|
+
throw unresolved(ref, `no secret ${name} in environment ${environment} at ${path}`);
|
|
223
|
+
if (typeof value !== "string")
|
|
224
|
+
throw unresolved(ref, "the Infisical CLI returned no value");
|
|
225
|
+
return value;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const AZURE_KV_FORM = "expected azure-kv://<vault>/<name> or azure-kv://<vault>/<name>/<version>";
|
|
230
|
+
/** Key Vault's own rules: vault names 3-24 of 0-9 a-z A-Z and non-consecutive `-`; secret names 1-127 of 0-9 a-z A-Z `-`; versions 32 characters. */
|
|
231
|
+
const AZURE_VAULT = /^(?!.*--)[A-Za-z0-9-]{3,24}$/;
|
|
232
|
+
const AZURE_SECRET = /^[A-Za-z0-9-]{1,127}$/;
|
|
233
|
+
const AZURE_VERSION = /^[A-Za-z0-9]{32}$/;
|
|
234
|
+
/**
|
|
235
|
+
* `azure-kv://<vault>/<name>[/<version>]` → `az keyvault secret show`, through the Azure CLI so neither the Azure SDK
|
|
236
|
+
* nor an Entra ID token flow enters the dependency tree. The CLI rather than `GET {vault}/secrets/{name}` with a token
|
|
237
|
+
* from `az account get-access-token`: one process instead of two, and the CLI already knows the vault DNS suffix of
|
|
238
|
+
* the signed-in cloud (`vault.azure.net`, `vault.azure.cn`, `vault.usgovcloudapi.net`) and every way to sign in
|
|
239
|
+
* (`az login`, a service principal, a managed identity, federated CI credentials).
|
|
240
|
+
* https://learn.microsoft.com/en-us/cli/azure/keyvault/secret#az-keyvault-secret-show
|
|
241
|
+
*/
|
|
242
|
+
export function azureKeyVaultSource(exec = defaultExec) {
|
|
243
|
+
return {
|
|
244
|
+
scheme: "azure-kv",
|
|
245
|
+
form: "`azure-kv://vault/name` or `azure-kv://vault/name/version`",
|
|
246
|
+
resolvedBy: "`az keyvault secret show --vault-name vault --name name [--version version] --query value --output json` (Azure CLI; credentials and cloud from `az login` and the CLI's other conventions). The version defaults to the latest.",
|
|
247
|
+
async resolve(ref, env) {
|
|
248
|
+
const parts = stripScheme(ref, "azure-kv").split("/");
|
|
249
|
+
if (parts.length < 2 || parts.length > 3)
|
|
250
|
+
throw unresolved(ref, AZURE_KV_FORM);
|
|
251
|
+
const [vault, name, version] = parts;
|
|
252
|
+
if (!AZURE_VAULT.test(vault) || !AZURE_SECRET.test(name) || (version !== undefined && !AZURE_VERSION.test(version)))
|
|
253
|
+
throw unresolved(ref, AZURE_KV_FORM);
|
|
254
|
+
const pinned = version === undefined ? [] : [version];
|
|
255
|
+
// JSON, not `-o tsv`: tsv cannot tell a value's own trailing newline from the one the CLI ends its output with.
|
|
256
|
+
const args = ["keyvault", "secret", "show", "--vault-name", vault, "--name", name, ...pinned.flatMap((v) => ["--version", v]), "--query", "value", "--output", "json"];
|
|
257
|
+
const value = parseJson(await runCli(exec, ref, "az", args, env, [vault, name, ...pinned]));
|
|
258
|
+
if (typeof value !== "string")
|
|
259
|
+
throw unresolved(ref, "the secret has no value");
|
|
260
|
+
return value;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Bitwarden secret ids are UUIDs (`bws secret get` parses its argument as one). */
|
|
265
|
+
const BWS_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
266
|
+
/**
|
|
267
|
+
* `bws://<secret-id>` → `bws secret get`, through the Bitwarden Secrets Manager CLI
|
|
268
|
+
* (https://bitwarden.com/help/secrets-manager-cli/). The machine account's access token comes from
|
|
269
|
+
* `BWS_ACCESS_TOKEN`, a self-hosted server from `BWS_SERVER_URL` (both read by the CLI itself: crates/bws/src/cli.rs
|
|
270
|
+
* in bitwarden/sdk-sm). `--color no`: the CLI colours its JSON whenever it believes the terminal supports colour,
|
|
271
|
+
* which `FORCE_COLOR` in CI makes it believe.
|
|
272
|
+
*/
|
|
273
|
+
export function bitwardenSecretsManagerSource(exec = defaultExec) {
|
|
274
|
+
return {
|
|
275
|
+
scheme: "bws",
|
|
276
|
+
form: "`bws://secret-id`",
|
|
277
|
+
resolvedBy: "`bws secret get secret-id --output json --color no` (Bitwarden Secrets Manager CLI; the machine account's token from `BWS_ACCESS_TOKEN`, a self-hosted server from `BWS_SERVER_URL`). `secret-id` is the secret's UUID.",
|
|
278
|
+
async resolve(ref, env) {
|
|
279
|
+
const id = stripScheme(ref, "bws");
|
|
280
|
+
if (!BWS_ID.test(id))
|
|
281
|
+
throw unresolved(ref, "expected bws://<secret-id>, the secret's UUID");
|
|
282
|
+
const response = parseJson(await runCli(exec, ref, "bws", ["secret", "get", id, "--output", "json", "--color", "no"], env, [id]));
|
|
283
|
+
// Never echo the response (or a fragment of it) in these messages: it holds the value.
|
|
284
|
+
const value = isRecord(response) ? response.value : undefined;
|
|
285
|
+
if (typeof value !== "string")
|
|
286
|
+
throw unresolved(ref, "the Bitwarden CLI returned no value");
|
|
287
|
+
return value;
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// HTTP-backed sources: HashiCorp Vault, 1Password Connect
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
/** A variable the source cannot work without; unset or empty is SECRET_UNRESOLVED naming it. */
|
|
295
|
+
function requiredVar(ref, env, name) {
|
|
296
|
+
const v = env[name];
|
|
297
|
+
if (v === undefined || v === "")
|
|
298
|
+
throw unresolved(ref, `${name} is not set`, { variable: name });
|
|
299
|
+
return v;
|
|
300
|
+
}
|
|
301
|
+
/** A server address from the environment: an absolute http(s) URL. */
|
|
302
|
+
function requiredUrl(ref, env, name) {
|
|
303
|
+
const v = requiredVar(ref, env, name);
|
|
304
|
+
let url;
|
|
305
|
+
try {
|
|
306
|
+
url = new URL(v);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
url = undefined;
|
|
310
|
+
}
|
|
311
|
+
if (url?.protocol !== "http:" && url?.protocol !== "https:")
|
|
312
|
+
throw unresolved(ref, `${name} is not an http(s) URL`, { variable: name });
|
|
313
|
+
return v;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The adapters' HTTP client for a secret service: its timeout (SPONSON_HTTP_TIMEOUT_MS), its retries of a 5xx or a
|
|
317
|
+
* dropped connection (SPONSON_HTTP_RETRIES), and the token masked in any error text it builds.
|
|
318
|
+
*/
|
|
319
|
+
function secretClient(service, baseUrl, token, authHeader, headers, env) {
|
|
320
|
+
return apiClient({ adapter: service, baseUrl, token, authHeader, headers, env, redact: (s) => s.split(token).join("***") });
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* GET a path; any failure is SECRET_UNRESOLVED. A 404 reads as `notFound`. An answer that is not JSON is reported
|
|
324
|
+
* without its text, which is where the value would be; every other failure keeps the client's message (method,
|
|
325
|
+
* path, status and the service's own error, which never holds the value; the token is masked).
|
|
326
|
+
*/
|
|
327
|
+
async function httpRead(ref, api, path, notFound) {
|
|
328
|
+
try {
|
|
329
|
+
return await api.get(path);
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
if (isProviderError(e, "PROVIDER_NOT_FOUND"))
|
|
333
|
+
throw unresolved(ref, notFound, { status: 404 });
|
|
334
|
+
if (isProviderError(e, "PROVIDER_RESPONSE"))
|
|
335
|
+
throw unresolved(ref, `${api.adapter}: GET ${path} answered with something that is not JSON`);
|
|
336
|
+
if (isProviderError(e))
|
|
337
|
+
throw unresolved(ref, e.message, e.details.status === undefined ? {} : { status: e.details.status });
|
|
338
|
+
throw unresolved(ref, e.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const VAULT_FORM = "expected vault://<mount>/<path>#<key>";
|
|
342
|
+
/** `vault://<mount>/<path>#<key>`, split; `.` and `..` segments are refused so a reference cannot climb out of the mount. */
|
|
343
|
+
function vaultRef(ref) {
|
|
344
|
+
const body = stripScheme(ref, "vault");
|
|
345
|
+
const hash = body.indexOf("#");
|
|
346
|
+
const segments = (hash < 0 ? body : body.slice(0, hash)).split("/");
|
|
347
|
+
const key = hash < 0 ? "" : body.slice(hash + 1);
|
|
348
|
+
if (key === "" || segments.length < 2 || segments.some((s) => s === "" || s === "." || s === ".."))
|
|
349
|
+
throw unresolved(ref, VAULT_FORM);
|
|
350
|
+
const [mount, ...path] = segments;
|
|
351
|
+
return { mount, path, key };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* `vault://<mount>/<path>#<key>` → HashiCorp Vault's KV v2 "read secret version", `GET /v1/<mount>/data/<path>`
|
|
355
|
+
* (https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2), over the adapters' HTTP client, no SDK. The
|
|
356
|
+
* address is `VAULT_ADDR`, the token `VAULT_TOKEN` (sent as `X-Vault-Token`), and `VAULT_NAMESPACE`, when set, is
|
|
357
|
+
* sent as `X-Vault-Namespace` (Vault Enterprise, HCP Vault Dedicated: https://developer.hashicorp.com/vault/api-docs).
|
|
358
|
+
* The mount is the first segment; the latest version is read. `#key` picks one key of the secret's map.
|
|
359
|
+
*/
|
|
360
|
+
export const vaultSecretSource = {
|
|
361
|
+
scheme: "vault",
|
|
362
|
+
form: "`vault://mount/path#KEY`",
|
|
363
|
+
resolvedBy: "HashiCorp Vault KV v2, `GET $VAULT_ADDR/v1/mount/data/path` with `X-Vault-Token: $VAULT_TOKEN` (and `X-Vault-Namespace: $VAULT_NAMESPACE` when set); the latest version's `KEY`. The mount is the first segment.",
|
|
364
|
+
async resolve(ref, env) {
|
|
365
|
+
const { mount, path, key } = vaultRef(ref);
|
|
366
|
+
const addr = requiredUrl(ref, env, "VAULT_ADDR");
|
|
367
|
+
const token = requiredVar(ref, env, "VAULT_TOKEN");
|
|
368
|
+
const namespace = env.VAULT_NAMESPACE;
|
|
369
|
+
const api = secretClient("vault", addr, token, "header:X-Vault-Token", namespace ? { "X-Vault-Namespace": namespace } : {}, env);
|
|
370
|
+
const url = `/v1/${encodeURIComponent(mount)}/data/${path.map(encodeURIComponent).join("/")}`;
|
|
371
|
+
const where = `${mount}/${path.join("/")}`;
|
|
372
|
+
// Vault answers 404 both for a path that does not exist and for one the token may not see.
|
|
373
|
+
const body = await httpRead(ref, api, url, `no secret at ${where} (or the token may not read it; a KV v1 mount is not supported)`);
|
|
374
|
+
// Never echo the body (or a fragment of it) in these messages: it holds the values.
|
|
375
|
+
const data = isRecord(body) && isRecord(body.data) ? body.data.data : undefined;
|
|
376
|
+
if (!isRecord(data))
|
|
377
|
+
throw unresolved(ref, `the latest version of ${where} is deleted or destroyed, or the answer is not a KV v2 secret`);
|
|
378
|
+
return scalarField(ref, data, key, where);
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
/** One key of a secret's map as a string; numbers and booleans are written as JSON writes them. */
|
|
382
|
+
function scalarField(ref, fields, key, where) {
|
|
383
|
+
const v = fields[key];
|
|
384
|
+
if (v === undefined)
|
|
385
|
+
throw unresolved(ref, `${where} has no key ${key}`);
|
|
386
|
+
if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean")
|
|
387
|
+
throw unresolved(ref, `key ${key} of ${where} is not a string, number or boolean`);
|
|
388
|
+
return String(v);
|
|
389
|
+
}
|
|
390
|
+
const OP_CONNECT_FORM = "expected op-connect://<vault>/<item>/<field> or op-connect://<vault>/<item>/<section>/<field>";
|
|
391
|
+
/** Vault and item ids as Connect shows them (`ytrfte14kw1uex5txaore1emkz`); anything else is looked up by name. */
|
|
392
|
+
const OP_ID = /^[a-z0-9]{26}$/;
|
|
393
|
+
function opConnectRef(ref) {
|
|
394
|
+
const parts = stripScheme(ref, "op-connect").split("/");
|
|
395
|
+
if (parts.length < 3 || parts.length > 4 || parts.some((p) => p === ""))
|
|
396
|
+
throw unresolved(ref, OP_CONNECT_FORM);
|
|
397
|
+
const [vault, item] = parts;
|
|
398
|
+
return parts.length === 4 ? { vault, item, section: parts[2], field: parts[3] } : { vault, item, field: parts[2] };
|
|
399
|
+
}
|
|
400
|
+
/** A filter value in Connect's SCIM-style `filter` (`name eq "Demo Vault"`), quotes and backslashes escaped. */
|
|
401
|
+
function scimString(s) {
|
|
402
|
+
return `"${s.replace(/[\\"]/g, (c) => `\\${c}`)}"`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The id of the one vault (`attr` = `name`) or item (`title`) called `name`. The filter narrows the list; the exact
|
|
406
|
+
* comparison here decides, so a server that ignored the filter would not change the answer.
|
|
407
|
+
*/
|
|
408
|
+
async function opConnectFind(ref, api, list, attr, name, what) {
|
|
409
|
+
const body = await httpRead(ref, api, `${list}?filter=${encodeURIComponent(`${attr} eq ${scimString(name)}`)}`, `no ${what} named ${name}`);
|
|
410
|
+
const found = (Array.isArray(body) ? body : []).filter((e) => isRecord(e) && e[attr] === name && typeof e.id === "string");
|
|
411
|
+
if (found.length === 0)
|
|
412
|
+
throw unresolved(ref, `no ${what} named ${name}`);
|
|
413
|
+
if (found.length > 1)
|
|
414
|
+
throw unresolved(ref, `${found.length} ${what}s are named ${name}; use the id instead`);
|
|
415
|
+
return found[0].id;
|
|
416
|
+
}
|
|
417
|
+
/** The fields of an item's section, matched by the section's id or label. */
|
|
418
|
+
function inSection(ref, item, fields, section) {
|
|
419
|
+
const sections = (Array.isArray(item.sections) ? item.sections : []).filter(isRecord);
|
|
420
|
+
const ids = new Set(sections.filter((s) => s.id === section || s.label === section).map((s) => s.id));
|
|
421
|
+
if (ids.size === 0)
|
|
422
|
+
throw unresolved(ref, `the item has no section ${section}`);
|
|
423
|
+
return fields.filter((f) => isRecord(f.section) && ids.has(f.section.id));
|
|
424
|
+
}
|
|
425
|
+
/** The value of the one field matched by id, else by label, as `op read` matches them. */
|
|
426
|
+
function opConnectField(ref, item, r) {
|
|
427
|
+
if (!isRecord(item) || !Array.isArray(item.fields))
|
|
428
|
+
throw unresolved(ref, "the item has no fields");
|
|
429
|
+
let fields = item.fields.filter(isRecord);
|
|
430
|
+
if (r.section !== undefined)
|
|
431
|
+
fields = inSection(ref, item, fields, r.section);
|
|
432
|
+
const byId = fields.filter((f) => f.id === r.field);
|
|
433
|
+
const matches = byId.length > 0 ? byId : fields.filter((f) => f.label === r.field);
|
|
434
|
+
if (matches.length === 0)
|
|
435
|
+
throw unresolved(ref, `the item has no field ${r.field}`);
|
|
436
|
+
if (matches.length > 1)
|
|
437
|
+
throw unresolved(ref, `${matches.length} fields of the item are labelled ${r.field}; name its section or use the field id`);
|
|
438
|
+
// Never echo the value (or a fragment of it) in these messages.
|
|
439
|
+
const value = matches[0].value;
|
|
440
|
+
if (typeof value !== "string" || value === "")
|
|
441
|
+
throw unresolved(ref, `field ${r.field} has no value`);
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* `op-connect://<vault>/<item>/[<section>/]<field>` → a 1Password Connect server's REST API
|
|
446
|
+
* (https://www.1password.dev/connect/api-reference/): `GET /v1/vaults/{vault}/items/{item}`, bearer
|
|
447
|
+
* `OP_CONNECT_TOKEN`, at `OP_CONNECT_HOST` (the variables 1Password's own Connect SDKs and `op` read). Vaults and
|
|
448
|
+
* items are named by id or by name / title (looked up with `GET /v1/vaults?filter=name eq "…"` and
|
|
449
|
+
* `GET /v1/vaults/{vault}/items?filter=title eq "…"`; a name must be unique); fields by id or label.
|
|
450
|
+
*/
|
|
451
|
+
export const opConnectSecretSource = {
|
|
452
|
+
scheme: "op-connect",
|
|
453
|
+
form: "`op-connect://vault/item/field` or `op-connect://vault/item/section/field`",
|
|
454
|
+
resolvedBy: "a 1Password Connect server, `GET $OP_CONNECT_HOST/v1/vaults/vault/items/item` with `Authorization: Bearer $OP_CONNECT_TOKEN`. `vault` and `item` are ids or unique names (looked up with `?filter=`); `section` and `field` are ids or labels.",
|
|
455
|
+
async resolve(ref, env) {
|
|
456
|
+
const r = opConnectRef(ref);
|
|
457
|
+
const host = requiredUrl(ref, env, "OP_CONNECT_HOST");
|
|
458
|
+
const api = secretClient("1password-connect", host, requiredVar(ref, env, "OP_CONNECT_TOKEN"), "bearer", {}, env);
|
|
459
|
+
const vaultId = OP_ID.test(r.vault) ? r.vault : await opConnectFind(ref, api, "/v1/vaults", "name", r.vault, "vault");
|
|
460
|
+
const itemId = OP_ID.test(r.item) ? r.item : await opConnectFind(ref, api, `/v1/vaults/${vaultId}/items`, "title", r.item, "item");
|
|
461
|
+
const item = await httpRead(ref, api, `/v1/vaults/${vaultId}/items/${itemId}`, `no item ${r.item} in vault ${r.vault} (or the token may not read it)`);
|
|
462
|
+
return opConnectField(ref, item, r);
|
|
463
|
+
},
|
|
464
|
+
};
|
|
186
465
|
function isRecord(v) {
|
|
187
466
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
188
467
|
}
|