@sponson/adapters 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +1 -1
  2. package/dist/algolia.d.ts +6 -0
  3. package/dist/algolia.js +151 -0
  4. package/dist/algolia.js.map +1 -0
  5. package/dist/clerk.js +1 -1
  6. package/dist/clerk.js.map +1 -1
  7. package/dist/cloudflare.d.ts +25 -0
  8. package/dist/cloudflare.js +310 -0
  9. package/dist/cloudflare.js.map +1 -0
  10. package/dist/common.d.ts +2 -6
  11. package/dist/common.js.map +1 -1
  12. package/dist/digest.d.ts +46 -0
  13. package/dist/digest.js +129 -0
  14. package/dist/digest.js.map +1 -0
  15. package/dist/digitalocean.d.ts +5 -0
  16. package/dist/digitalocean.js +243 -0
  17. package/dist/digitalocean.js.map +1 -0
  18. package/dist/http-adapter-action.d.ts +63 -0
  19. package/dist/http-adapter-action.js +369 -0
  20. package/dist/http-adapter-action.js.map +1 -0
  21. package/dist/http-adapter-auth.d.ts +88 -0
  22. package/dist/http-adapter-auth.js +437 -0
  23. package/dist/http-adapter-auth.js.map +1 -0
  24. package/dist/http-adapter-client.d.ts +51 -0
  25. package/dist/http-adapter-client.js +156 -0
  26. package/dist/http-adapter-client.js.map +1 -0
  27. package/dist/http-adapter-graphql.d.ts +39 -0
  28. package/dist/http-adapter-graphql.js +157 -0
  29. package/dist/http-adapter-graphql.js.map +1 -0
  30. package/dist/http-adapter-list.d.ts +33 -0
  31. package/dist/http-adapter-list.js +421 -0
  32. package/dist/http-adapter-list.js.map +1 -0
  33. package/dist/http-adapter-resource.d.ts +29 -0
  34. package/dist/http-adapter-resource.js +462 -0
  35. package/dist/http-adapter-resource.js.map +1 -0
  36. package/dist/http-adapter-spec.d.ts +216 -0
  37. package/dist/http-adapter-spec.js +660 -0
  38. package/dist/http-adapter-spec.js.map +1 -0
  39. package/dist/http-adapter.d.ts +16 -0
  40. package/dist/http-adapter.js +28 -0
  41. package/dist/http-adapter.js.map +1 -0
  42. package/dist/http.d.ts +61 -2
  43. package/dist/http.js +79 -18
  44. package/dist/http.js.map +1 -1
  45. package/dist/index.d.ts +12 -0
  46. package/dist/index.js +24 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/launchdarkly.d.ts +15 -0
  49. package/dist/launchdarkly.js +337 -0
  50. package/dist/launchdarkly.js.map +1 -0
  51. package/dist/manual.d.ts +13 -0
  52. package/dist/manual.js +201 -0
  53. package/dist/manual.js.map +1 -0
  54. package/dist/neon.js +1 -1
  55. package/dist/neon.js.map +1 -1
  56. package/dist/netlify.d.ts +18 -0
  57. package/dist/netlify.js +436 -0
  58. package/dist/netlify.js.map +1 -0
  59. package/dist/planetscale.d.ts +10 -0
  60. package/dist/planetscale.js +311 -0
  61. package/dist/planetscale.js.map +1 -0
  62. package/dist/providers.d.ts +26 -0
  63. package/dist/providers.js +36 -0
  64. package/dist/providers.js.map +1 -0
  65. package/dist/recipes.d.ts +167 -0
  66. package/dist/recipes.js +246 -0
  67. package/dist/recipes.js.map +1 -0
  68. package/dist/secrets.d.ts +44 -0
  69. package/dist/secrets.js +279 -0
  70. package/dist/secrets.js.map +1 -1
  71. package/dist/sigv4.d.ts +56 -0
  72. package/dist/sigv4.js +118 -0
  73. package/dist/sigv4.js.map +1 -0
  74. package/dist/supabase.d.ts +9 -0
  75. package/dist/supabase.js +385 -0
  76. package/dist/supabase.js.map +1 -0
  77. package/dist/vercel.js +1 -1
  78. package/dist/vercel.js.map +1 -1
  79. package/dist/xata.d.ts +8 -0
  80. package/dist/xata.js +142 -0
  81. package/dist/xata.js.map +1 -0
  82. package/dist/xml.d.ts +25 -0
  83. package/dist/xml.js +211 -0
  84. package/dist/xml.js.map +1 -0
  85. package/package.json +11 -4
  86. package/recipes/amplify.yaml +147 -0
  87. package/recipes/auth0.yaml +178 -0
  88. package/recipes/azure_swa.yaml +116 -0
  89. package/recipes/cloud_run.yaml +97 -0
  90. package/recipes/cloudflare.yaml +108 -0
  91. package/recipes/cockroachdb.yaml +132 -0
  92. package/recipes/configcat.yaml +123 -0
  93. package/recipes/datadog.yaml +136 -0
  94. package/recipes/firebase.yaml +84 -0
  95. package/recipes/flagsmith.yaml +98 -0
  96. package/recipes/fly.yaml +97 -0
  97. package/recipes/fly_machines.yaml +67 -0
  98. package/recipes/gcs.yaml +102 -0
  99. package/recipes/github.yaml +123 -0
  100. package/recipes/growthbook.yaml +124 -0
  101. package/recipes/heroku.yaml +98 -0
  102. package/recipes/honeycomb.yaml +89 -0
  103. package/recipes/inngest.yaml +97 -0
  104. package/recipes/kinde.yaml +120 -0
  105. package/recipes/mongodb_atlas.yaml +119 -0
  106. package/recipes/okta.yaml +177 -0
  107. package/recipes/posthog.yaml +127 -0
  108. package/recipes/postmark.yaml +105 -0
  109. package/recipes/postmark_server.yaml +93 -0
  110. package/recipes/prisma.yaml +150 -0
  111. package/recipes/qstash.yaml +134 -0
  112. package/recipes/railway.yaml +130 -0
  113. package/recipes/render.yaml +103 -0
  114. package/recipes/resend.yaml +130 -0
  115. package/recipes/s3.yaml +108 -0
  116. package/recipes/sendgrid.yaml +165 -0
  117. package/recipes/sentry.yaml +158 -0
  118. package/recipes/split.yaml +121 -0
  119. package/recipes/statsig.yaml +124 -0
  120. package/recipes/stripe.yaml +310 -0
  121. package/recipes/stytch.yaml +113 -0
  122. package/recipes/svix.yaml +109 -0
  123. package/recipes/turso.yaml +98 -0
  124. package/recipes/unleash.yaml +149 -0
  125. package/recipes/upstash.yaml +130 -0
  126. package/recipes/vercel.yaml +116 -0
  127. package/recipes/workos.yaml +70 -0
@@ -0,0 +1,660 @@
1
+ /**
2
+ * The generic `http` adapter's plan syntax, parsed and checked: the per-API provider block, the params of
3
+ * `http.resource` and `http.list_item`, JSON pointers and path placeholders. Pure: no I/O. Every malformed spec is
4
+ * a PARAM_INVALID (or PLAN_INVALID for the provider block) naming the field at fault. See ADR 0017 and
5
+ * docs/plan-format.md (`http.resource`, `http.list_item`).
6
+ */
7
+ import { canonicalJson, markerKind, sha256 } from "@sponson/core";
8
+ import { paramError } from "./common.js";
9
+ import { isObject } from "./http.js";
10
+ import { HEADER_NAME, envName, parseAuth, planError } from "./http-adapter-auth.js";
11
+ import { parseGraphql, usesId } from "./http-adapter-graphql.js";
12
+ import { expandRecipe, lineApi, resolveRecipeApi } from "./recipes.js";
13
+ export const ADAPTER = "http";
14
+ // ---------------------------------------------------------------------------
15
+ // JSON pointers (RFC 6901) and field names
16
+ // ---------------------------------------------------------------------------
17
+ /** A JSON pointer's reference tokens. `""` is the whole document. */
18
+ export function pointerTokens(pointer) {
19
+ if (pointer === "")
20
+ return [];
21
+ return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
22
+ }
23
+ /** A field as the plan writes it: `name` (a top-level field) or a JSON pointer `/config/url`. */
24
+ export function fieldTokens(field) {
25
+ return field.startsWith("/") ? pointerTokens(field) : [field];
26
+ }
27
+ /** The value at `tokens` in `doc`, or undefined. */
28
+ export function at(doc, tokens) {
29
+ let cur = doc;
30
+ for (const t of tokens) {
31
+ if (Array.isArray(cur) && /^\d+$/.test(t))
32
+ cur = cur[Number(t)];
33
+ else if (isObject(cur))
34
+ cur = cur[t];
35
+ else
36
+ return undefined;
37
+ }
38
+ return cur;
39
+ }
40
+ /** A copy of `doc` (an object) with `value` set at `tokens`, creating objects on the way. */
41
+ export function setAt(doc, tokens, value) {
42
+ if (tokens.length === 0)
43
+ return isObject(value) ? { ...value } : doc;
44
+ const [head, ...rest] = tokens;
45
+ const child = isObject(doc[head]) ? doc[head] : {};
46
+ return { ...doc, [head]: rest.length === 0 ? value : setAt(child, rest, value) };
47
+ }
48
+ /** Objects merged recursively (`b` wins); anything else is replaced by `b`. */
49
+ export function deepMerge(a, b) {
50
+ const out = { ...a };
51
+ for (const [k, v] of Object.entries(b))
52
+ out[k] = isObject(v) && isObject(out[k]) ? deepMerge(out[k], v) : v;
53
+ return out;
54
+ }
55
+ /** The request body that sets every field of `fields` (names or pointers), skipping `skip`. */
56
+ export function bodyOf(fields, skip = () => false) {
57
+ let body = {};
58
+ for (const [f, v] of Object.entries(fields))
59
+ if (!skip(v))
60
+ body = setAt(body, fieldTokens(f), v);
61
+ return body;
62
+ }
63
+ /** `item` reduced to the declared fields, in the one canonical form both hashes and diffs use. Absent is null. */
64
+ export function projection(item, fields) {
65
+ return Object.fromEntries(fields.map((f) => [f, at(item, fieldTokens(f)) ?? null]));
66
+ }
67
+ /** The hash of a resource's declared state: `sha256(canonicalJson(projection))`. */
68
+ export function stateHash(state) {
69
+ return sha256(canonicalJson(state));
70
+ }
71
+ /** True when the value is `{ keep: true }`'s marker. */
72
+ export function isKeep(v) {
73
+ return markerKind(v) === "keep";
74
+ }
75
+ /** The first pending or secret marker anywhere in `v`, if any. */
76
+ export function firstMarker(v) {
77
+ const kind = markerKind(v);
78
+ if (kind === "pending" || kind === "secret")
79
+ return v;
80
+ if (Array.isArray(v))
81
+ return v.map(firstMarker).find((m) => m !== undefined);
82
+ if (isObject(v))
83
+ return Object.values(v).map(firstMarker).find((m) => m !== undefined);
84
+ return undefined;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Param checks
88
+ // ---------------------------------------------------------------------------
89
+ function fail(message, param) {
90
+ return paramError(ADAPTER, message, param);
91
+ }
92
+ function optionalObject(v, param) {
93
+ if (v === undefined || v === null)
94
+ return undefined;
95
+ if (!isObject(v))
96
+ throw fail(`\`${param}\` must be a map`, param);
97
+ return v;
98
+ }
99
+ function requiredObject(v, param) {
100
+ const o = optionalObject(v, param);
101
+ if (!o)
102
+ throw fail(`\`${param}\` is required`, param);
103
+ return o;
104
+ }
105
+ /** A path: starts with `/`; pending markers are allowed in the `{name}` values only, not here. */
106
+ function pathParam(v, param) {
107
+ if (typeof v !== "string" || !v.startsWith("/"))
108
+ throw fail(`\`${param}\` must be a path starting with "/" (relative to the API's base_url)`, param);
109
+ return v;
110
+ }
111
+ function pointerParam(v, param, fallback) {
112
+ if (v === undefined || v === null)
113
+ return fallback;
114
+ if (typeof v !== "string" || (v !== "" && !v.startsWith("/")))
115
+ throw fail(`\`${param}\` must be a JSON pointer: "" for the whole body, or "/field/sub"`, param);
116
+ return v;
117
+ }
118
+ function fieldParam(v, param) {
119
+ if (typeof v !== "string" || v === "")
120
+ throw fail(`\`${param}\` must be a field name or a JSON pointer such as "/config/url"`, param);
121
+ return v;
122
+ }
123
+ function methodParam(v, param, allowed, fallback) {
124
+ if (v === undefined || v === null)
125
+ return fallback;
126
+ const m = typeof v === "string" ? v.toUpperCase() : "";
127
+ if (!allowed.includes(m))
128
+ throw fail(`\`${param}\` must be one of ${allowed.join(", ")}`, param);
129
+ return m;
130
+ }
131
+ /** `{name}` (the value URI-encoded as one segment) or `{+name}` (reserved expansion, RFC 6570: `/` and `:` kept). */
132
+ const PLACEHOLDER = /\{(\+?)([A-Za-z_][A-Za-z0-9_]*)\}/g;
133
+ /** The `{name}` and `{+name}` placeholders of a path, by name. */
134
+ export function placeholders(path) {
135
+ return [...path.matchAll(PLACEHOLDER)].map((m) => m[2]);
136
+ }
137
+ /**
138
+ * A value as a path placeholder: one segment, URI-encoded; or, for `{+name}`, a URL or path written as it is
139
+ * (QStash takes the destination URL raw in the path), with only what cannot stand in a path escaped.
140
+ */
141
+ function encodeSegment(value, reserved) {
142
+ return reserved ? encodeURI(value).replace(/[?#]/g, (c) => encodeURIComponent(c)) : encodeURIComponent(value);
143
+ }
144
+ /** Every placeholder of `path` must be `{id}` (when `id` is allowed) or a key of `vars`. */
145
+ function checkPlaceholders(path, param, vars, idAllowed) {
146
+ for (const name of placeholders(path)) {
147
+ if (name === "id" && idAllowed)
148
+ continue;
149
+ if (!(name in vars))
150
+ throw fail(`\`${param}\` uses \`{${name}}\`, which is ${name === "id" ? "not known here (the id exists only once the object does)" : "not declared in `vars`"}`, param);
151
+ }
152
+ }
153
+ /**
154
+ * `path` with `{id}` and every `{var}` substituted (URI-encoded; `{+var}` keeps `/` and `:`); undefined when a value
155
+ * is not known yet (a pending reference).
156
+ */
157
+ export function fillPath(path, vars, id) {
158
+ const value = (name) => (name === "id" && id !== undefined ? id : vars[name]);
159
+ const known = (v) => markerKind(v) === null && v !== undefined && v !== null && typeof v !== "object";
160
+ if (!placeholders(path).every((name) => known(value(name))))
161
+ return undefined;
162
+ return path.replace(PLACEHOLDER, (_m, plus, name) => encodeSegment(String(value(name)), plus === "+"));
163
+ }
164
+ const API_KEYS = ["base_url", "base_url_env", "auth", "headers", "encoding", "media_type", "production", "refuse_credential"];
165
+ function parseEncoding(v, where) {
166
+ if (v === undefined || v === "json" || v === "form" || v === "xml")
167
+ return v ?? "json";
168
+ throw planError("must be `json` (the default), `form` (application/x-www-form-urlencoded) or `xml` (S3-style XML documents)", where);
169
+ }
170
+ function parseHeaders(v, where) {
171
+ if (v === undefined)
172
+ return {};
173
+ if (!isObject(v))
174
+ throw planError("must be a map of header name to string value", where);
175
+ for (const [k, x] of Object.entries(v)) {
176
+ if (!HEADER_NAME.test(k) || typeof x !== "string")
177
+ throw planError(`\`${k}\` must be a header name with a string value`, where);
178
+ if (/^(authorization|accept|content-type)$/i.test(k))
179
+ throw planError(`\`${k}\` is set by Sponson; configure the credential under \`auth\` and a versioned media type under \`media_type\``, where);
180
+ }
181
+ return v;
182
+ }
183
+ function parseMediaType(v, where, encoding) {
184
+ if (v === undefined)
185
+ return undefined;
186
+ if (typeof v !== "string" || !/^[\w.+-]+\/[\w.+-]+(\s*;.*)?$/.test(v))
187
+ throw planError("must be a media type such as application/vnd.atlas.2023-01-01+json", where);
188
+ if (encoding !== "json")
189
+ throw planError("applies to `encoding: json` only", where);
190
+ return v;
191
+ }
192
+ function parseProduction(v, where) {
193
+ if (v === undefined || typeof v === "boolean")
194
+ return v ?? false;
195
+ throw planError("must be true (lines through this API write production and need approval) or false", where);
196
+ }
197
+ /**
198
+ * `OpSpec.writesEnvironment` for both ops: `production` when the line's API block says `production: true` (a live
199
+ * Stripe account, a production tenant), so the run needs approval whatever its environment; otherwise not
200
+ * environment-specific. `provider` is the block `apiBlock` narrowed to, already checked.
201
+ */
202
+ export function apiEnvironment(_params, _ctx, provider) {
203
+ return provider?.production === true ? "production" : null;
204
+ }
205
+ /**
206
+ * One API block, checked. `where` names it in messages (`providers.http.statsig`). `authOptional`: a block without
207
+ * `auth` sends no credential (the `manual` adapter's verify requests to public endpoints); the `http` adapter's
208
+ * blocks always need one.
209
+ */
210
+ export function parseApi(block, where, opts = {}) {
211
+ const base = block.base_url;
212
+ if (typeof base !== "string" || !/^https?:\/\/[^\s]+$/.test(base))
213
+ throw planError("`base_url` must be an http(s) URL", where);
214
+ const unknown = Object.keys(block).filter((k) => !API_KEYS.includes(k));
215
+ if (unknown.length)
216
+ throw planError(`unknown key \`${unknown[0]}\` (known: ${API_KEYS.join(", ")})`, where);
217
+ const encoding = parseEncoding(block.encoding, `${where}.encoding`);
218
+ const mediaType = parseMediaType(block.media_type, `${where}.media_type`, encoding);
219
+ const cfg = {
220
+ baseUrl: base,
221
+ auth: block.auth === undefined && opts.authOptional ? { kind: "none" } : parseAuth(block.auth, `${where}.auth`, base),
222
+ headers: parseHeaders(block.headers, `${where}.headers`),
223
+ encoding,
224
+ ...(mediaType ? { mediaType } : {}),
225
+ production: parseProduction(block.production, `${where}.production`),
226
+ };
227
+ if (block.base_url_env !== undefined)
228
+ cfg.baseUrlEnv = envName(block.base_url_env, `${where}.base_url_env`, false);
229
+ if (block.refuse_credential !== undefined)
230
+ cfg.refuse = parseRefuse(block.refuse_credential, `${where}.refuse_credential`);
231
+ return cfg;
232
+ }
233
+ function parseRefuse(v, where) {
234
+ const prefixes = isObject(v) ? v.prefixes : undefined;
235
+ if (!isObject(v) || !Array.isArray(prefixes) || prefixes.length === 0 || !prefixes.every((p) => typeof p === "string" && p.length >= 3)) {
236
+ throw planError("must be `{ prefixes: [sk_live_, …], unless_env: NAME }`: credential prefixes to refuse, and the variable that allows them", where);
237
+ }
238
+ const extra = Object.keys(v).find((k) => k !== "prefixes" && k !== "unless_env");
239
+ if (extra)
240
+ throw planError(`unknown key \`${extra}\` (known: prefixes, unless_env)`, where);
241
+ return { prefixes: prefixes, unlessEnv: envName(v.unless_env, `${where}.unless_env`, false) };
242
+ }
243
+ /**
244
+ * `OpSpec.providerFor` for both ops: the `providers.http.<api>` block the line's `api` names, checked. Lines of
245
+ * different APIs therefore have independent ledger identities.
246
+ */
247
+ export function apiBlock(block, params) {
248
+ const api = lineApi(params);
249
+ if (typeof api !== "string" || api === "")
250
+ throw fail("`api` is required: the name of an API configured under `providers.http`", "api");
251
+ const where = `providers.http.${api}`;
252
+ const recipe = typeof params.recipe === "string" ? params.recipe.split(".")[0] : undefined;
253
+ // A recipe line needs no block of its own: `api` defaults to the recipe's provider, and so does its block.
254
+ const raw = block[api] ?? (recipe === api ? { recipe } : undefined);
255
+ if (!isObject(raw)) {
256
+ const known = Object.keys(block);
257
+ throw planError(`no API \`${api}\` is configured (known: ${known.length ? known.join(", ") : "none"})`, where);
258
+ }
259
+ const cfg = resolveRecipeApi(raw, where);
260
+ if (raw.recipe !== undefined && recipe !== undefined && recipe !== raw.recipe)
261
+ throw planError(`uses recipe \`${String(raw.recipe)}\`, but the line's recipe is \`${String(params.recipe)}\``, where);
262
+ if (raw.recipe !== undefined && raw.refuse_credential !== undefined) {
263
+ // A recipe's refusal is a safety rule (Stripe: never live keys from a preview), not a default to tune per plan.
264
+ throw planError("`refuse_credential` comes from the recipe and cannot be overridden; its `unless_env` variable is the opt-out", `${where}.refuse_credential`);
265
+ }
266
+ if (raw.recipe !== undefined && cfg.base_url === undefined)
267
+ throw planError(`recipe \`${String(raw.recipe)}\` has no default \`base_url\` (each account has its own): set \`base_url\` in this block`, where);
268
+ lineCredentialFits(params, parseApi(cfg, where), where);
269
+ return cfg;
270
+ }
271
+ /** A line's `credential` replaces a single static credential's variable: the block must have one. */
272
+ function lineCredentialFits(params, api, where) {
273
+ if (params.credential === undefined || api.auth.kind === "bearer" || api.auth.kind === "header")
274
+ return;
275
+ throw fail(`\`credential\` replaces the value of a single static credential (\`bearer_env\`, or \`header\` with \`value_env\`); ${where} authenticates with \`${api.auth.kind}\``, "credential");
276
+ }
277
+ /** A `credential` as the plan may write it: a reference (as written), its resolved value, or a marker. */
278
+ function credentialShape(v) {
279
+ if (isKeep(v))
280
+ return false;
281
+ if (typeof v === "string" || markerKind(v) !== null)
282
+ return true;
283
+ return isObject(v) && Object.keys(v).length === 1 && (typeof v.from === "string" || typeof v.secret === "string");
284
+ }
285
+ /**
286
+ * A line's `credential` (ADR 0017, "Line credentials"): the value that replaces the API block's credential variable
287
+ * for this line, as resolved (a string, or a marker while it is pending), or the reference as written. Never part
288
+ * of identity. Destroy works from the ledger alone, which cannot reach it, so the line must say `destroy: keep`.
289
+ */
290
+ function parseCredential(params, destroy) {
291
+ const v = params.credential;
292
+ if (v === undefined)
293
+ return {};
294
+ if (!credentialShape(v))
295
+ throw fail("`credential` must be `{ from: <line>.<output> }` (a sensitive output) or `{ secret: <reference> }`", "credential");
296
+ if (v === "")
297
+ throw fail("`credential` resolved to an empty value", "credential");
298
+ if (destroy !== "keep") {
299
+ throw fail("a line with `credential` must say `destroy: keep`: destroy works from the ledger alone, which never holds the credential, so what the line makes goes away with the object the credential belongs to (a Postmark server takes its webhooks with it)", "destroy");
300
+ }
301
+ return { credential: v };
302
+ }
303
+ function parseDestroy(v) {
304
+ if (v === undefined || v === "delete" || v === "keep")
305
+ return v ?? "delete";
306
+ throw fail("`destroy` must be `delete` (the default) or `keep` (leave it in place when the scope is destroyed)", "destroy");
307
+ }
308
+ function statusList(v, param) {
309
+ if (v === undefined)
310
+ return [];
311
+ const list = Array.isArray(v) ? v : [v];
312
+ if (!list.every((s) => Number.isInteger(s) && s >= 400 && s <= 599))
313
+ throw fail(`\`${param}\` must be a list of HTTP error statuses (400–599)`, param);
314
+ return list;
315
+ }
316
+ function contentTypeParam(v, param) {
317
+ if (v === undefined)
318
+ return undefined;
319
+ if (typeof v !== "string" || !/^[\w.+-]+\/[\w.+-]+(\s*;.*)?$/.test(v))
320
+ throw fail(`\`${param}\` must be a media type such as application/merge-patch+json`, param);
321
+ return v;
322
+ }
323
+ function apiParam(params) {
324
+ const api = params.api;
325
+ if (typeof api !== "string" || api === "")
326
+ throw fail("`api` is required: the name of an API configured under `providers.http`", "api");
327
+ return api;
328
+ }
329
+ function noUnknownParams(params, known, op) {
330
+ const extra = Object.keys(params).find((k) => !known.includes(k));
331
+ if (extra)
332
+ throw fail(`\`${extra}\` is not a parameter of http.${op} (known: ${known.join(", ")})`, extra);
333
+ }
334
+ const RESOURCE_PARAMS = ["api", "credential", "vars", "create", "read", "find", "update", "delete", "destroy", "item_path", "id_path", "fields", "outputs", "exists_status", "gone_status"];
335
+ /** Per-request headers: names Sponson does not set itself; values are checked once resolved (`requestHeaders`). */
336
+ function requestHeadersParam(v, param) {
337
+ const o = optionalObject(v, param) ?? {};
338
+ for (const k of Object.keys(o)) {
339
+ if (!HEADER_NAME.test(k))
340
+ throw fail(`\`${param}.${k}\` is not a header name`, `${param}.${k}`);
341
+ if (/^(authorization|accept|content-type|idempotency-key)$/i.test(k))
342
+ throw fail(`\`${param}.${k}\` is set by Sponson or the API block`, `${param}.${k}`);
343
+ }
344
+ return o;
345
+ }
346
+ /** `{ graphql }` in place of a REST request: nothing else may be next to it. */
347
+ function asGraphql(o, param, idAllowed) {
348
+ if (o.graphql === undefined)
349
+ return undefined;
350
+ const extra = Object.keys(o).find((k) => k !== "graphql");
351
+ if (extra)
352
+ throw fail(`\`${param}.${extra}\` does not apply to a GraphQL request (the query and its variables say everything)`, `${param}.${extra}`);
353
+ return { method: "POST", path: "", body: {}, headers: {}, sendFields: true, graphql: parseGraphql(o.graphql, `${param}.graphql`, idAllowed) };
354
+ }
355
+ function parseRequest(v, param, methods, fallback, vars, idAllowed, defaultPath) {
356
+ const o = requiredObject(v, param);
357
+ const gql = asGraphql(o, param, idAllowed);
358
+ if (gql)
359
+ return gql;
360
+ const path = o.path === undefined && defaultPath !== undefined ? defaultPath : pathParam(o.path, `${param}.path`);
361
+ checkPlaceholders(path, `${param}.path`, vars, idAllowed);
362
+ const contentType = contentTypeParam(o.content_type, `${param}.content_type`);
363
+ if (o.send_fields !== undefined && typeof o.send_fields !== "boolean")
364
+ throw fail(`\`${param}.send_fields\` must be true or false`, `${param}.send_fields`);
365
+ return {
366
+ method: methodParam(o.method, `${param}.method`, methods, fallback),
367
+ path,
368
+ body: optionalObject(o.body, `${param}.body`) ?? {},
369
+ ...(contentType ? { contentType } : {}),
370
+ headers: requestHeadersParam(o.headers, `${param}.headers`),
371
+ sendFields: o.send_fields !== false,
372
+ };
373
+ }
374
+ /** A request's headers with their resolved values, as strings; PARAM_INVALID for a value that is not a scalar. */
375
+ export function requestHeaders(req, param) {
376
+ const out = {};
377
+ for (const [k, v] of Object.entries(req.headers)) {
378
+ if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean")
379
+ throw fail(`\`${param}.headers.${k}\` must be a string, number or boolean`, `${param}.headers.${k}`);
380
+ out[k] = String(v);
381
+ }
382
+ return out;
383
+ }
384
+ function parseCreate(v, vars) {
385
+ const r = parseRequest(v, "create", ["POST", "PUT", "PATCH"], "POST", vars, false);
386
+ if (r.graphql)
387
+ return { ...r, idempotencyKey: false, locate: false };
388
+ const { idempotency_key: key, locate } = v;
389
+ if (key !== undefined && typeof key !== "boolean")
390
+ throw fail("`create.idempotency_key` must be true or false", "create.idempotency_key");
391
+ if (locate !== undefined && typeof locate !== "boolean")
392
+ throw fail("`create.locate` must be true or false", "create.locate");
393
+ return { ...r, idempotencyKey: key === true, locate: locate === true };
394
+ }
395
+ function parseFind(v, vars) {
396
+ const o = optionalObject(v, "find");
397
+ if (!o)
398
+ return undefined;
399
+ if (o.graphql !== undefined)
400
+ return parseGraphqlFind(o);
401
+ const path = pathParam(o.path, "find.path");
402
+ checkPlaceholders(path, "find.path", vars, false);
403
+ const match = requiredObject(o.match, "find.match");
404
+ if (Object.keys(match).length === 0)
405
+ throw fail("`find.match` must name at least one field", "find.match");
406
+ const spec = { path, listPath: pointerParam(o.list_path, "find.list_path", ""), match };
407
+ if (o.next !== undefined) {
408
+ const param = o.cursor_param === undefined ? "cursor" : o.cursor_param;
409
+ if (typeof param !== "string" || param === "")
410
+ throw fail("`find.cursor_param` must be a query parameter name", "find.cursor_param");
411
+ spec.next = { path: pointerParam(o.next, "find.next", ""), param };
412
+ }
413
+ return spec;
414
+ }
415
+ /** A find whose list comes from a GraphQL query: no path, no cursor pages. */
416
+ function parseGraphqlFind(o) {
417
+ const extra = Object.keys(o).find((k) => !["graphql", "list_path", "match"].includes(k));
418
+ if (extra)
419
+ throw fail(`\`find.${extra}\` does not apply to a GraphQL find (known: graphql, list_path, match)`, `find.${extra}`);
420
+ const match = requiredObject(o.match, "find.match");
421
+ if (Object.keys(match).length === 0)
422
+ throw fail("`find.match` must name at least one field", "find.match");
423
+ return { path: "", graphql: parseGraphql(o.graphql, "find.graphql", false), listPath: pointerParam(o.list_path, "find.list_path", ""), match };
424
+ }
425
+ function parseRead(v, vars, find) {
426
+ const o = optionalObject(v, "read");
427
+ if (!o) {
428
+ if (!find)
429
+ throw fail("needs `read` (a path or a GraphQL query that names the object) or `find` (a list and the fields that identify it)", "read");
430
+ return undefined;
431
+ }
432
+ if (o.graphql !== undefined) {
433
+ const gql = asGraphql(o, "read", true).graphql;
434
+ if (usesId(gql.variables) && !find)
435
+ throw fail('`read.graphql.variables` uses "{id}", so the object can only be located with `find` (the id is not in the plan)', "read.graphql.variables");
436
+ return { path: "", graphql: gql };
437
+ }
438
+ const path = pathParam(o.path, "read.path");
439
+ if (placeholders(path).includes("id") && !find)
440
+ throw fail("`read.path` uses `{id}`, so the object can only be located with `find` (the id is not in the plan)", "read.path");
441
+ checkPlaceholders(path, "read.path", vars, true);
442
+ return { path };
443
+ }
444
+ /** Update and delete go to the read path by default: the object's own URL. */
445
+ function parseDelete(v, vars, read) {
446
+ if (v === undefined && (read === undefined || read.graphql))
447
+ throw fail("`delete` is required: `{ path: /things/{id} }` or `{ graphql: { query: … } }` (no `read.path` to default to)", "delete");
448
+ const r = parseRequest(v ?? {}, "delete", ["DELETE", "POST"], "DELETE", vars, true, read?.graphql ? undefined : read?.path);
449
+ if (r.graphql)
450
+ return { method: r.method, path: r.path, graphql: r.graphql };
451
+ if (Object.keys(r.body).length === 0)
452
+ return { method: r.method, path: r.path };
453
+ if (r.method !== "POST")
454
+ throw fail("`delete.body` needs `method: POST` (an archive request such as `{ active: false }`)", "delete.body");
455
+ // The body travels in the record id, which the ledger keeps: plain values only, never a reference or a secret.
456
+ if (firstMarker(r.body) !== undefined || JSON.stringify(r.body).includes("${"))
457
+ throw fail("`delete.body` must hold literal values only", "delete.body");
458
+ return { method: r.method, path: r.path, body: r.body };
459
+ }
460
+ function parseOutputs(v) {
461
+ const o = optionalObject(v, "outputs") ?? {};
462
+ const out = {};
463
+ for (const [name, decl] of Object.entries(o)) {
464
+ const param = `outputs.${name}`;
465
+ if (name === "id")
466
+ throw fail("`outputs.id` is reserved: the object's id is always the output `id`", param);
467
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
468
+ throw fail(`output name \`${name}\` must be letters, digits and _`, param);
469
+ if (typeof decl === "string")
470
+ out[name] = { path: pointerParam(decl, param, ""), sensitive: false, once: false };
471
+ else if (isObject(decl))
472
+ out[name] = outputDecl(decl, param);
473
+ else
474
+ throw fail(`\`${param}\` must be a JSON pointer, or \`{ path, sensitive: true, once?: true }\``, param);
475
+ }
476
+ return out;
477
+ }
478
+ /** `{ path, sensitive?, once? }`. A value shown only at creation is a credential, so `once` requires `sensitive`. */
479
+ function outputDecl(decl, param) {
480
+ for (const flag of ["sensitive", "once"]) {
481
+ if (decl[flag] !== undefined && typeof decl[flag] !== "boolean")
482
+ throw fail(`\`${param}.${flag}\` must be true or false`, `${param}.${flag}`);
483
+ }
484
+ if (decl.once === true && decl.sensitive !== true)
485
+ throw fail(`\`${param}\` is \`once\` (shown only when the object is created), so it must be \`sensitive: true\` too`, `${param}.sensitive`);
486
+ return { path: pointerParam(decl.path, `${param}.path`, ""), sensitive: decl.sensitive === true, once: decl.once === true };
487
+ }
488
+ function parseFields(v, param) {
489
+ const o = optionalObject(v, param) ?? {};
490
+ for (const f of Object.keys(o))
491
+ fieldParam(f, `${param}.${f}`);
492
+ return o;
493
+ }
494
+ /** `http.resource` params, checked; PARAM_INVALID naming the field otherwise. */
495
+ export function parseResource(line) {
496
+ const params = expandRecipe(line, "resource");
497
+ noUnknownParams(params, RESOURCE_PARAMS, "resource");
498
+ const api = apiParam(params);
499
+ const vars = optionalObject(params.vars, "vars") ?? {};
500
+ const find = parseFind(params.find, vars);
501
+ const read = parseRead(params.read, vars, find);
502
+ const destroy = parseDestroy(params.destroy);
503
+ const spec = {
504
+ api,
505
+ vars,
506
+ ...parseCredential(params, destroy),
507
+ create: parseCreate(params.create, vars),
508
+ ...(find ? { find } : {}),
509
+ ...(read ? { read } : {}),
510
+ delete: parseDelete(params.delete, vars, read),
511
+ destroy,
512
+ itemPath: pointerParam(params.item_path, "item_path", ""),
513
+ idPath: pointerParam(params.id_path, "id_path", "/id"),
514
+ fields: parseFields(params.fields, "fields"),
515
+ outputs: parseOutputs(params.outputs),
516
+ existsStatus: statusList(params.exists_status, "exists_status"),
517
+ goneStatus: statusList(params.gone_status, "gone_status"),
518
+ };
519
+ if (params.update !== undefined)
520
+ spec.update = parseRequest(params.update, "update", ["PATCH", "PUT", "POST"], "PATCH", vars, true, read?.graphql ? undefined : read?.path);
521
+ noSecretIn(find?.graphql?.variables, "find.graphql.variables");
522
+ noSecretIn(read?.graphql?.variables, "read.graphql.variables");
523
+ noSecretIn(spec.delete.graphql?.variables, "delete.graphql.variables");
524
+ return spec;
525
+ }
526
+ /**
527
+ * The variables of a locating or deleting GraphQL request become part of the resource key or the record id, which
528
+ * receipts keep: a `{ secret }` there is refused (it would be masked, and the key or the delete broken).
529
+ */
530
+ function noSecretIn(v, param) {
531
+ if (v !== undefined && firstSecret(v))
532
+ throw fail(`\`${param}\` may not hold a \`{ secret }\`: it becomes part of the resource key or the delete request kept in receipts`, param);
533
+ }
534
+ function firstSecret(v) {
535
+ if (markerKind(v) === "secret")
536
+ return true;
537
+ if (Array.isArray(v))
538
+ return v.some(firstSecret);
539
+ return isObject(v) && Object.values(v).some(firstSecret);
540
+ }
541
+ /**
542
+ * `OpSpec.credentialInputs` for both ops: the line's `credential`, which the engine checks is a masked reference.
543
+ * Its own rules (its form, `destroy: keep`) are checked here too, so a wrong line fails before any request.
544
+ */
545
+ export function credentialInputs(kind) {
546
+ return (line) => {
547
+ if (line.credential === undefined)
548
+ return [];
549
+ const params = expandRecipe(line, kind);
550
+ parseCredential(params, parseDestroy(params.destroy));
551
+ return ["credential"];
552
+ };
553
+ }
554
+ /** The outputs one `http.resource` line declares: always `id`, plus its `outputs` (`once` ones as in ADR 0018). */
555
+ export function resourceOutputs(line) {
556
+ const params = expandRecipe(line, "resource");
557
+ const out = { id: { available: "immediate" } };
558
+ for (const [name, d] of Object.entries(parseOutputs(params.outputs)))
559
+ out[name] = { available: "immediate", ...(d.sensitive ? { sensitive: true } : {}), ...(d.once ? { once: true } : {}) };
560
+ return out;
561
+ }
562
+ const LIST_PARAMS = ["api", "credential", "vars", "parent", "list_path", "shape", "separator", "item", "key_field", "value", "destroy"];
563
+ function parseParent(v, vars) {
564
+ const o = requiredObject(v, "parent");
565
+ const path = pathParam(o.path, "parent.path");
566
+ checkPlaceholders(path, "parent.path", vars, false);
567
+ const readPath = o.read_path === undefined ? path : pathParam(o.read_path, "parent.read_path");
568
+ checkPlaceholders(readPath, "parent.read_path", vars, false);
569
+ const ifMissing = oneOf(o.if_missing, ["error", "empty"], "parent.if_missing", "`error` (the default: a parent that is not there is an error) or `empty` (a 404 reads as an empty collection, S3 CORS)");
570
+ const whenEmpty = oneOf(o.when_empty, ["write", "delete"], "parent.when_empty", "`write` (the default: write the empty collection) or `delete` (DELETE `parent.path` instead, S3 CORS)");
571
+ const contentType = contentTypeParam(o.content_type, "parent.content_type");
572
+ const writeListPath = o.write_list_path === undefined ? undefined : pointerParam(o.write_list_path, "parent.write_list_path", "");
573
+ return {
574
+ path,
575
+ readPath,
576
+ readMethod: methodParam(o.read_method, "parent.read_method", ["GET", "POST"], "GET"),
577
+ method: methodParam(o.method, "parent.method", ["PATCH", "PUT", "POST"], "PATCH"),
578
+ send: parseSend(o.send),
579
+ itemPath: pointerParam(o.item_path, "parent.item_path", ""),
580
+ ifMissing,
581
+ whenEmpty,
582
+ ...(contentType ? { contentType } : {}),
583
+ ...(writeListPath !== undefined ? { writeListPath } : {}),
584
+ };
585
+ }
586
+ /** One of `allowed` (the first when absent); PARAM_INVALID saying what each means otherwise. */
587
+ function oneOf(v, allowed, param, meaning) {
588
+ if (v === undefined)
589
+ return allowed[0];
590
+ if (typeof v === "string" && allowed.includes(v))
591
+ return v;
592
+ throw fail(`\`${param}\` must be ${meaning}`, param);
593
+ }
594
+ function parseSend(v) {
595
+ if (v === undefined || v === "field" || v === "parent" || v === "collection")
596
+ return v ?? "field";
597
+ if (Array.isArray(v) && v.length > 0 && v.every((f) => typeof f === "string" && f !== ""))
598
+ return v;
599
+ throw fail("`parent.send` must be `field` (send only the list; a map entry as a merge patch), `collection` (send only the list, a map whole), `parent` (send the whole object back) or a list of the object's fields to send back with it", "parent.send");
600
+ }
601
+ function scalarItem(v, what) {
602
+ if (isObject(v) || Array.isArray(v))
603
+ throw fail(`\`item\` must be ${what}`, "item");
604
+ if (isKeep(v))
605
+ throw fail("`item` cannot be `{ keep: true }`: it is the item's identity", "item");
606
+ return v;
607
+ }
608
+ function parseArrayItem(v, keyField) {
609
+ if (!isObject(v)) {
610
+ if (keyField !== undefined)
611
+ throw fail("`key_field` applies only when `item` is a map", "key_field");
612
+ return { item: scalarItem(v, "a single value or a map, not a list") };
613
+ }
614
+ const field = fieldParam(keyField, "key_field");
615
+ const key = at(v, fieldTokens(field));
616
+ if (key === undefined || isObject(key) || Array.isArray(key) || isKeep(key))
617
+ throw fail(`\`item\` must set \`${field}\` (its \`key_field\`) to a value`, `item.${field}`);
618
+ return { item: v, keyField: field };
619
+ }
620
+ function parseShape(v) {
621
+ if (v === undefined || v === "array" || v === "delimited" || v === "map")
622
+ return v ?? "array";
623
+ throw fail("`shape` must be `array` (the default), `delimited` or `map`", "shape");
624
+ }
625
+ /** The shape-specific params: what identifies the item, and what else the shape takes. */
626
+ function parseShaped(params, shape) {
627
+ if (params.item === undefined || params.item === null)
628
+ throw fail("`item` is required: the value to keep in the list (for `shape: map`, the entry's name)", "item");
629
+ const only = (name, allowed) => {
630
+ if (!allowed && params[name] !== undefined)
631
+ throw fail(`\`${name}\` does not apply to \`shape: ${shape}\``, name);
632
+ };
633
+ only("key_field", shape === "array");
634
+ only("value", shape === "map");
635
+ only("separator", shape === "delimited");
636
+ if (shape === "array")
637
+ return { separator: ",", ...parseArrayItem(params.item, params.key_field) };
638
+ const sep = params.separator ?? ",";
639
+ if (typeof sep !== "string" || sep.trim() === "")
640
+ throw fail("`separator` must be a non-blank string, e.g. \",\"", "separator");
641
+ if (shape === "delimited")
642
+ return { separator: sep, item: scalarItem(params.item, "a single string for `shape: delimited`") };
643
+ if (params.value === undefined)
644
+ throw fail("`value` is required for `shape: map`: the entry's value (`null` is not a value: it deletes)", "value");
645
+ return { separator: sep, item: scalarItem(params.item, "the entry's name for `shape: map`"), value: params.value };
646
+ }
647
+ /** `http.list_item` params, checked; PARAM_INVALID naming the field otherwise. */
648
+ export function parseListItem(line) {
649
+ const params = expandRecipe(line, "list_item");
650
+ noUnknownParams(params, LIST_PARAMS, "list_item");
651
+ const api = apiParam(params);
652
+ const vars = optionalObject(params.vars, "vars") ?? {};
653
+ const shape = parseShape(params.shape);
654
+ const listPath = pointerParam(params.list_path, "list_path", "");
655
+ if (listPath === "" && shape !== "map")
656
+ throw fail("`list_path` is required: the JSON pointer to the list in the parent, e.g. /allowed_origins", "list_path");
657
+ const destroy = parseDestroy(params.destroy);
658
+ return { api, vars, ...parseCredential(params, destroy), parent: parseParent(params.parent, vars), listPath, shape, ...parseShaped(params, shape), destroy };
659
+ }
660
+ //# sourceMappingURL=http-adapter-spec.js.map