@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,134 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Upstash QStash recipes (its own API and token; Upstash Redis is recipes/upstash.yaml).
3
+ # Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
4
+ provider: qstash
5
+ title: Upstash QStash
6
+ category: queues
7
+ verified_on: "2026-10-11"
8
+ docs:
9
+ - title: Authentication
10
+ url: https://upstash.com/docs/qstash/api/authentication
11
+ - title: Create a schedule
12
+ url: https://upstash.com/docs/qstash/api-reference/schedules/create-a-schedule
13
+ - title: Get a schedule
14
+ url: https://upstash.com/docs/qstash/api-reference/schedules/get-a-schedule
15
+ - title: Delete a schedule
16
+ url: https://upstash.com/docs/qstash/api-reference/schedules/delete-a-schedule
17
+ - title: API examples (raw destination URL in the path)
18
+ url: https://upstash.com/docs/qstash/overall/apiexamples
19
+ - title: Multi-region (base URLs, tokens)
20
+ url: https://upstash.com/docs/qstash/howto/multi-region
21
+ api:
22
+ base_url: https://qstash.upstash.io
23
+ base_url_env: QSTASH_API_URL
24
+ auth: { bearer_env: QSTASH_TOKEN }
25
+ detect:
26
+ deps: ["@upstash/qstash"]
27
+ env: ["QSTASH_*"]
28
+ starter:
29
+ - op: schedule
30
+ values: { schedule_id: "cron-${ctx.scope}" }
31
+ where:
32
+ destination: "the preview's endpoint the schedule calls: its URL plus the path (a `{ from }` reference cannot carry a path)"
33
+ assumptions:
34
+ - id: QS1
35
+ verified: true
36
+ text: "Requests authenticate with the QStash token as `Authorization: Bearer`; the SDKs read it from `QSTASH_TOKEN`."
37
+ source: https://upstash.com/docs/qstash/api/authentication
38
+ - id: QS2
39
+ verified: true
40
+ text: "Each region (`qstash.upstash.io` = EU, `qstash-us-east-1.upstash.io`) has its own token and its own schedules; the default here is the EU URL, and a US account overrides `base_url` in its block."
41
+ source: https://upstash.com/docs/qstash/howto/multi-region
42
+ - id: QS3
43
+ verified: true
44
+ text: "`POST /v2/schedules/{destination}` creates a schedule: the cron in `Upstash-Cron`, the method used to call the destination in `Upstash-Method` (default POST), the request body as the message delivered on every run. It answers `{ scheduleId }`."
45
+ source: https://upstash.com/docs/qstash/api-reference/schedules/create-a-schedule
46
+ - id: QS4
47
+ verified: true
48
+ text: "With `Upstash-Schedule-Id`, a schedule with that id is updated with the new settings if it exists (an upsert): the id is the client's, the read path names it, and the same request is both create and update."
49
+ source: https://upstash.com/docs/qstash/api-reference/schedules/create-a-schedule
50
+ - id: QS5
51
+ verified: true
52
+ text: "The official example writes the destination URL in the path as it is (`/v2/schedules/https://example.com`), hence the `{+destination}` placeholder."
53
+ source: https://upstash.com/docs/qstash/overall/apiexamples
54
+ - id: QS6
55
+ verified: false
56
+ text: "A destination URL with a query string arrives intact with its `?` escaped as `%3F` (a raw `?` would end the path); not documented."
57
+ - id: QS7
58
+ verified: true
59
+ text: "`GET /v2/schedules/{scheduleId}` answers the schedule bare with `scheduleId`, `cron`, `destination`, `method` and `body`, and 404 when there is none; `DELETE` on the same path removes it."
60
+ source: https://upstash.com/docs/qstash/api-reference/schedules/get-a-schedule
61
+ - id: QS8
62
+ verified: false
63
+ text: "Deleting a schedule that is already gone answers 404 (only 200 is documented), which destroy treats as done."
64
+ source: https://upstash.com/docs/qstash/api-reference/schedules/delete-a-schedule
65
+ - id: QS9
66
+ verified: true
67
+ text: "Schedule ids are alphanumerics, hyphens, periods and underscores (400 otherwise); a cron may carry a `CRON_TZ=<zone>` prefix and runs at most once a minute."
68
+ source: https://upstash.com/docs/qstash/api-reference/schedules/create-a-schedule
69
+ - id: QS10
70
+ verified: false
71
+ text: "`GET` returns `cron`, `destination` and `method` exactly as they were sent (the cron string unnormalised), so comparing them is not drift on every plan."
72
+ source: https://upstash.com/docs/qstash/api-reference/schedules/get-a-schedule
73
+ ops:
74
+ schedule:
75
+ kind: resource
76
+ title: Schedule
77
+ summary: "A cron schedule that calls a preview's URL, such as `/api/cron` of `pr-42`, under a schedule id per scope. Created and updated by the same upsert (QS4); a console edit of its cron, destination or method is drift; destroy deletes it. The message body is sent on create and update but not compared."
78
+ covers: [qstash-schedule]
79
+ params:
80
+ schedule_id:
81
+ type: string
82
+ required: true
83
+ pattern: "^[A-Za-z0-9._-]+$"
84
+ description: The schedule's id, chosen by the plan (one per scope).
85
+ example: "cron-${ctx.scope}"
86
+ destination:
87
+ type: string
88
+ required: true
89
+ pattern: "^https?://"
90
+ description: The URL the schedule calls.
91
+ example: "https://${ctx.scope}.preview.example.com/api/cron"
92
+ cron:
93
+ type: string
94
+ required: true
95
+ description: "The cron expression, optionally prefixed `CRON_TZ=<zone> `."
96
+ example: "*/15 * * * *"
97
+ method:
98
+ type: string
99
+ default: POST
100
+ enum: [GET, POST, PUT, PATCH, DELETE]
101
+ description: The HTTP method used to call the destination.
102
+ body:
103
+ type: map
104
+ description: The JSON message delivered on every run (sent, not compared).
105
+ http:
106
+ read: { path: "/v2/schedules/{schedule_id}" }
107
+ create:
108
+ path: "/v2/schedules/{+destination}"
109
+ headers:
110
+ Upstash-Schedule-Id: { param: schedule_id }
111
+ Upstash-Cron: { param: cron }
112
+ Upstash-Method: { param: method }
113
+ send_fields: false
114
+ body: { param: body }
115
+ update:
116
+ method: POST
117
+ path: "/v2/schedules/{+destination}"
118
+ headers:
119
+ Upstash-Schedule-Id: { param: schedule_id }
120
+ Upstash-Cron: { param: cron }
121
+ Upstash-Method: { param: method }
122
+ send_fields: false
123
+ body: { param: body }
124
+ id_path: /scheduleId
125
+ fields:
126
+ cron: { param: cron }
127
+ destination: { param: destination }
128
+ method: { param: method }
129
+ sim:
130
+ collection: /v2/schedules
131
+ header_fields: { upstash-schedule-id: scheduleId, upstash-cron: cron, upstash-method: method }
132
+ suffix_field: destination
133
+ live:
134
+ params: { destination: SPONSON_LIVE_QSTASH_DESTINATION }
@@ -0,0 +1,130 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Railway public GraphQL API recipes. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ provider: railway
4
+ title: Railway
5
+ category: deploy
6
+ verified_on: "2026-10-11"
7
+ docs:
8
+ - title: Public API (endpoint, tokens, errors)
9
+ url: https://docs.railway.com/reference/public-api
10
+ - title: Manage Variables with the Public API
11
+ url: https://docs.railway.com/guides/manage-variables
12
+ - title: Environments (PR environments)
13
+ url: https://docs.railway.com/reference/environments
14
+ - title: CLI (RAILWAY_TOKEN and RAILWAY_API_TOKEN)
15
+ url: https://docs.railway.com/guides/cli
16
+ - title: railwayapp/cli GraphQL schema
17
+ url: https://github.com/railwayapp/cli/blob/master/src/gql/schema.json
18
+ api:
19
+ base_url: https://backboard.railway.com/graphql/v2
20
+ base_url_env: RAILWAY_API_URL
21
+ auth: { bearer_env: RAILWAY_API_TOKEN }
22
+ detect:
23
+ env: ["RAILWAY_*"]
24
+ files: [railway.json, railway.toml]
25
+ starter:
26
+ - op: variable
27
+ where:
28
+ project_id: "Railway → your project → Settings → Project ID, or `railway status --json`"
29
+ environment_id: "the pull request environment Railway created: its Settings, or `railway environment`"
30
+ assumptions:
31
+ - id: RY1
32
+ verified: true
33
+ text: "The public API is one GraphQL endpoint, `https://backboard.railway.com/graphql/v2`; account, workspace and OAuth tokens are sent as `Authorization: Bearer <token>`, project tokens in `Project-Access-Token` instead (a project token therefore needs a hand-written block with `auth: { header: Project-Access-Token, value_env: RAILWAY_TOKEN }`)."
34
+ source: https://docs.railway.com/reference/public-api
35
+ - id: RY2
36
+ verified: true
37
+ text: "The Railway CLI reads an account or workspace token from `RAILWAY_API_TOKEN` and a project token from `RAILWAY_TOKEN`, so the bearer credential of this recipe is `RAILWAY_API_TOKEN`."
38
+ source: https://docs.railway.com/guides/cli
39
+ - id: RY3
40
+ verified: true
41
+ text: "Failures may be HTTP 200 with an `errors` array (a denied query among them); 400 is a parse or validation failure, 429 a rate limit with `Retry-After`. Queries are safe to retry; a mutation only when no answer arrived."
42
+ source: https://docs.railway.com/reference/public-api
43
+ - id: RY4
44
+ verified: true
45
+ text: "`variables(projectId, environmentId, serviceId?, unrendered?)` answers the variables of a service in an environment (without `serviceId`, the environment's shared variables) as a map of name to value; `unrendered: true` answers references such as `${{Postgres.DATABASE_URL}}` as written instead of resolved, so a value is compared as it was set."
46
+ source: https://docs.railway.com/guides/manage-variables
47
+ - id: RY5
48
+ verified: true
49
+ text: "`variableUpsert(input: { projectId, environmentId, serviceId?, name, value, skipDeploys? })` creates or replaces one variable (without `serviceId`, a shared variable); by default it triggers a redeploy of the service (`skipDeploys` defaults to false)."
50
+ source: https://docs.railway.com/guides/manage-variables
51
+ - id: RY6
52
+ verified: true
53
+ text: "`variableDelete(input: { projectId, environmentId, serviceId?, name })` deletes one variable."
54
+ source: https://docs.railway.com/guides/manage-variables
55
+ - id: RY7
56
+ verified: true
57
+ text: "Railway creates a PR environment when a pull request is opened (with PR environments enabled) and deletes it, with its variables, when the pull request is merged or closed. The recipe takes the environment's id as a param: the plan copies it from the dashboard, `railway status`, or a line that outputs it; the recipe does not look environments up by name (their naming is not documented)."
58
+ source: https://docs.railway.com/reference/environments
59
+ - id: RY8
60
+ verified: false
61
+ text: "Deleting a variable whose environment Railway already removed (the pull request closed) is an error saying the environment was not found, which the adapter reads as already gone; deleting a variable that does not exist answers `true`."
62
+ - id: RY9
63
+ verified: false
64
+ text: "Sponson does not wait for the redeploy a write triggers (RY5): a step that needs the running service to see the value waits on Railway's deployment status itself. A reference value written as `${{…}}` is not supported in a plan, where `${…}` is Sponson's own interpolation."
65
+ ops:
66
+ variable:
67
+ kind: resource
68
+ title: Variable of a service in an environment
69
+ summary: "One variable of a service (or, without `service_id`, a shared variable) in a Railway environment, typically the PR environment Railway created. Upserted with `variableUpsert`, read unrendered from `variables`, removed with `variableDelete`. A console edit of the value is drift. Railway redeploys the service after each write unless `skip_deploys` is set; Sponson does not wait for that deploy (RY9)."
70
+ covers: [railway-variables]
71
+ params:
72
+ project_id:
73
+ type: string
74
+ required: true
75
+ description: The project's id.
76
+ example: prj-railway-demo
77
+ environment_id:
78
+ type: string
79
+ required: true
80
+ description: The environment's id (for a PR environment, the one Railway created for the pull request; RY7).
81
+ example: env-pr-42
82
+ service_id:
83
+ type: string
84
+ description: The service's id; without it, the variable is shared by the environment's services.
85
+ example: svc-web
86
+ name:
87
+ type: string
88
+ required: true
89
+ pattern: "^[A-Za-z_][A-Za-z0-9_]*$"
90
+ description: The variable's name.
91
+ example: API_URL
92
+ value:
93
+ type: string
94
+ required: true
95
+ description: The variable's value (may be a `{ secret }` reference).
96
+ example: "https://api-${ctx.scope}.example.app"
97
+ skip_deploys:
98
+ type: boolean
99
+ description: Do not redeploy the service after a write (Railway's default is to redeploy).
100
+ http:
101
+ read:
102
+ graphql:
103
+ query: "query variables($projectId: String!, $environmentId: String!, $serviceId: String, $unrendered: Boolean) { variables(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId, unrendered: $unrendered) }"
104
+ variables: { projectId: { param: project_id }, environmentId: { param: environment_id }, serviceId: { param: service_id }, unrendered: true }
105
+ data_path: { join: ["/variables/", { param: name }] }
106
+ create:
107
+ graphql:
108
+ query: "mutation variableUpsert($input: VariableUpsertInput!) { variableUpsert(input: $input) }"
109
+ variables: { input: { projectId: { param: project_id }, environmentId: { param: environment_id }, serviceId: { param: service_id }, name: { param: name }, value: { param: value }, skipDeploys: { param: skip_deploys } } }
110
+ idempotent: true
111
+ update:
112
+ graphql:
113
+ query: "mutation variableUpsert($input: VariableUpsertInput!) { variableUpsert(input: $input) }"
114
+ variables: { input: { projectId: { param: project_id }, environmentId: { param: environment_id }, serviceId: { param: service_id }, name: { param: name }, value: { param: value }, skipDeploys: { param: skip_deploys } } }
115
+ idempotent: true
116
+ delete:
117
+ graphql:
118
+ query: "mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }"
119
+ variables: { input: { projectId: { param: project_id }, environmentId: { param: environment_id }, serviceId: { param: service_id }, name: { param: name } } }
120
+ fields:
121
+ value: { param: value }
122
+ sim:
123
+ provider: railway
124
+ seed: { environments: { env-pr-42: { projectId: prj-railway-demo, name: pr-42 } } }
125
+ present: /variables/env-pr-42~1svc-web/API_URL
126
+ drift:
127
+ changed: { vars.env-pr-42.svc-web.API_URL: "https://somewhere-else.example" }
128
+ missing: { vars.env-pr-42.svc-web.API_URL: delete }
129
+ live:
130
+ params: { project_id: SPONSON_LIVE_RAILWAY_PROJECT_ID, environment_id: SPONSON_LIVE_RAILWAY_ENVIRONMENT_ID, service_id: SPONSON_LIVE_RAILWAY_SERVICE_ID }
@@ -0,0 +1,103 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Render API recipes. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ provider: render
4
+ title: Render
5
+ category: deploy
6
+ verified_on: "2026-10-11"
7
+ docs:
8
+ - title: Authentication
9
+ url: https://api-docs.render.com/reference/authentication
10
+ - title: Retrieve environment variable
11
+ url: https://api-docs.render.com/reference/retrieve-env-var
12
+ - title: Add or update environment variable
13
+ url: https://api-docs.render.com/reference/update-env-var
14
+ - title: Delete environment variable
15
+ url: https://api-docs.render.com/reference/delete-env-var
16
+ - title: Update environment variables (replace all; deploy behaviour)
17
+ url: https://api-docs.render.com/reference/update-env-vars-for-service
18
+ - title: Service previews
19
+ url: https://render.com/docs/service-previews
20
+ - title: Render CLI (RENDER_API_KEY)
21
+ url: https://render.com/docs/cli
22
+ api:
23
+ base_url: https://api.render.com/v1
24
+ base_url_env: RENDER_API_URL
25
+ auth: { bearer_env: RENDER_API_KEY }
26
+ detect:
27
+ env: ["RENDER_*"]
28
+ files: [render.yaml]
29
+ starter:
30
+ - op: env_var
31
+ where:
32
+ service_id: "Render Dashboard → the preview service: the `srv-…` id in its URL"
33
+ assumptions:
34
+ - id: RE1
35
+ verified: true
36
+ text: "Requests authenticate with an API key as `Authorization: Bearer`; the Render CLI reads it from `RENDER_API_KEY`."
37
+ source: https://api-docs.render.com/reference/authentication
38
+ - id: RE2
39
+ verified: true
40
+ text: "`GET /services/{serviceId}/env-vars/{envVarKey}` answers the variable bare, `{ key, value }`, and 404 when it does not exist."
41
+ source: https://api-docs.render.com/reference/retrieve-env-var
42
+ - id: RE3
43
+ verified: true
44
+ text: "`PUT /services/{serviceId}/env-vars/{envVarKey}` with `{ value }` adds the variable or replaces its value (200, `{ key, value }`): a create whose answer was lost is the same request again, and the variable's name is its identity."
45
+ source: https://api-docs.render.com/reference/update-env-var
46
+ - id: RE4
47
+ verified: true
48
+ text: "`DELETE /services/{serviceId}/env-vars/{envVarKey}` removes the variable (204)."
49
+ source: https://api-docs.render.com/reference/delete-env-var
50
+ - id: RE5
51
+ verified: false
52
+ text: "Deleting a variable that is already gone answers 404 (documented only as \"Unable to find the requested resource\"), which destroy treats as done."
53
+ source: https://api-docs.render.com/reference/delete-env-var
54
+ - id: RE6
55
+ verified: true
56
+ text: "Changing environment variables through the API does not deploy the service, whatever its autoDeploy setting (stated for the replace-all endpoint); a running preview sees a new value only after its next deploy."
57
+ source: https://api-docs.render.com/reference/update-env-vars-for-service
58
+ - id: RE7
59
+ verified: false
60
+ text: "The single-variable PUT does not deploy either (its page is silent; assumed like the replace-all endpoint)."
61
+ - id: RE8
62
+ verified: true
63
+ text: "Render creates service previews and preview environments itself (per pull request, from the service's Previews setting or `render.yaml`); a preview copies the base service's variables once, when it is created. No API looks up the preview of pull request N, so the line takes the service's id."
64
+ source: https://render.com/docs/service-previews
65
+ - id: RE9
66
+ verified: false
67
+ text: "`GET` returns the value in plain text for every variable, including ones generated with `generateValue`, so the value can be compared for drift."
68
+ source: https://api-docs.render.com/reference/retrieve-env-var
69
+ ops:
70
+ env_var:
71
+ kind: resource
72
+ title: Service environment variable
73
+ summary: "One environment variable on a Render service, such as a preview service's `API_URL`, written by name (`PUT` adds or replaces it). A console edit of its value is drift; destroy deletes it. The service's id is a param: Render creates preview services itself, and writing a variable does not deploy (RE6)."
74
+ covers: [render-env]
75
+ params:
76
+ service_id:
77
+ type: string
78
+ required: true
79
+ pattern: "^[a-z]{3}-[a-z0-9]+$"
80
+ description: The service's id (`srv-…`, in the service's dashboard URL and settings).
81
+ example: srv-d0a1b2c3d4e5f6g7h8i9
82
+ key:
83
+ type: string
84
+ required: true
85
+ pattern: "^[A-Za-z_][A-Za-z0-9_]*$"
86
+ description: The variable's name.
87
+ example: API_URL
88
+ value:
89
+ type: string
90
+ required: true
91
+ description: "The variable's value: a literal, `{ from }` or `{ secret }`."
92
+ example: "https://api-${ctx.scope}.example.com"
93
+ http:
94
+ read: { path: "/services/{service_id}/env-vars/{key}" }
95
+ create: { method: PUT, path: "/services/{service_id}/env-vars/{key}" }
96
+ update: { method: PUT }
97
+ id_path: /key
98
+ fields:
99
+ value: { param: value }
100
+ sim:
101
+ collection: "/services/{service_id}/env-vars"
102
+ live:
103
+ params: { service_id: SPONSON_LIVE_RENDER_SERVICE_ID }
@@ -0,0 +1,130 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Resend API recipes. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ provider: resend
4
+ title: Resend
5
+ category: email
6
+ verified_on: "2026-10-11"
7
+ docs:
8
+ - title: Create API key
9
+ url: https://resend.com/docs/api-reference/api-keys/create-api-key
10
+ - title: List API keys
11
+ url: https://resend.com/docs/api-reference/api-keys/list-api-keys
12
+ - title: Delete API key
13
+ url: https://resend.com/docs/api-reference/api-keys/delete-api-key
14
+ - title: Create webhook
15
+ url: https://resend.com/docs/api-reference/webhooks/create-webhook
16
+ - title: Retrieve webhook
17
+ url: https://resend.com/docs/api-reference/webhooks/get-webhook
18
+ - title: List webhooks
19
+ url: https://resend.com/docs/api-reference/webhooks/list-webhooks
20
+ - title: Update webhook
21
+ url: https://resend.com/docs/api-reference/webhooks/update-webhook
22
+ - title: Delete webhook
23
+ url: https://resend.com/docs/api-reference/webhooks/delete-webhook
24
+ api:
25
+ base_url: https://api.resend.com
26
+ base_url_env: RESEND_API_URL
27
+ auth: { bearer_env: RESEND_API_KEY }
28
+ detect:
29
+ deps: [resend]
30
+ env: ["RESEND_*"]
31
+ starter:
32
+ - op: api_key
33
+ values: { name: "sponson-${ctx.env}-${ctx.scope}" }
34
+ assumptions:
35
+ - id: RS1
36
+ verified: true
37
+ text: "`POST /api-keys` creates an API key from `name` (at most 50 characters), `permission` (`full_access` or `sending_access`) and `domain_id` (with `sending_access` only), answering `{ id, token }`; requests carry `Authorization: Bearer <key>`."
38
+ source: https://resend.com/docs/api-reference/api-keys/create-api-key
39
+ - id: RS2
40
+ verified: true
41
+ text: "`GET /api-keys` answers `{ object: list, has_more, data: [{ id, name, created_at, last_used_at }] }`, every key when no `limit` is sent, and never the token: the token is shown only in the create answer, so it is a once-only output."
42
+ source: https://resend.com/docs/api-reference/api-keys/list-api-keys
43
+ - id: RS3
44
+ verified: true
45
+ text: "`DELETE /api-keys/{api_key_id}` removes a key; there is no endpoint to read or change one key, so its permission and domain are sent on create and never compared."
46
+ source: https://resend.com/docs/api-reference/api-keys/delete-api-key
47
+ - id: RS4
48
+ verified: true
49
+ text: "`POST /webhooks` creates a webhook from `endpoint` and `events` (both required), answering `{ object, id, signing_secret }`."
50
+ source: https://resend.com/docs/api-reference/webhooks/create-webhook
51
+ - id: RS5
52
+ verified: true
53
+ text: "`GET /webhooks/{webhook_id}` answers the webhook with `status` (`enabled`/`disabled`), `endpoint`, `events` and `signing_secret`: the secret stays readable, so it is an ordinary sensitive output."
54
+ source: https://resend.com/docs/api-reference/webhooks/get-webhook
55
+ - id: RS6
56
+ verified: true
57
+ text: "`GET /webhooks` answers `{ object: list, has_more, data: [{ id, created_at, status, endpoint, events }] }`, every webhook when no `limit` is sent."
58
+ source: https://resend.com/docs/api-reference/webhooks/list-webhooks
59
+ - id: RS7
60
+ verified: true
61
+ text: "`PATCH /webhooks/{webhook_id}` changes `endpoint`, `events` and `status`; `DELETE` on the same path removes the webhook."
62
+ source: https://resend.com/docs/api-reference/webhooks/update-webhook
63
+ - id: RS8
64
+ verified: false
65
+ text: "Key names and webhook endpoints are not unique in Resend: the recipe takes the name (keys) and the endpoint URL (webhooks) as the identity, and refuses (PARAM_INVALID) when two match, so give each environment its own name and URL."
66
+ - id: RS9
67
+ verified: false
68
+ text: "Creating a key or a webhook needs a `full_access` key; a deleted object answers 404."
69
+ ops:
70
+ api_key:
71
+ kind: resource
72
+ title: API key
73
+ summary: "A restricted API key per environment, such as `sponson-preview-pr-42` allowed only to send from one domain. Its token is a once-only output: pass it to the app's environment line in the same run (`{ from: <line>.token }`); later runs keep what that line holds, and `apply --recreate <line>` issues a new one. Only its existence is compared for drift: Resend shows no permission or domain for a key."
74
+ covers: [resend-key-webhook]
75
+ params:
76
+ name:
77
+ type: string
78
+ required: true
79
+ pattern: "^.{1,50}$"
80
+ description: The key's name, unique among the account's keys (RS8); its identity.
81
+ example: "sponson-${ctx.env}-${ctx.scope}"
82
+ permission:
83
+ type: string
84
+ default: sending_access
85
+ enum: [full_access, sending_access]
86
+ description: What the key may do.
87
+ domain_id:
88
+ type: string
89
+ description: With `sending_access`, the only domain the key may send from.
90
+ http:
91
+ find:
92
+ path: /api-keys
93
+ list_path: /data
94
+ match: { name: { param: name } }
95
+ create:
96
+ path: /api-keys
97
+ body: { permission: { param: permission }, domain_id: { param: domain_id } }
98
+ delete: { path: "/api-keys/{id}" }
99
+ outputs:
100
+ token: { path: /token, sensitive: true, once: true }
101
+ webhook:
102
+ kind: resource
103
+ title: Webhook
104
+ summary: "A webhook delivering an environment's email events to its URL, such as the preview's `/api/resend` handler. Located by its endpoint URL; a console edit of its events is drift (its enabled/disabled status is left to the console: the create request takes none). Its signing secret is a sensitive output, readable on every run."
105
+ covers: [resend-key-webhook]
106
+ params:
107
+ endpoint:
108
+ type: string
109
+ required: true
110
+ pattern: "^https://\\S+$"
111
+ description: The URL events are delivered to; its identity (RS8).
112
+ example: "https://${ctx.scope}.preview.example.com/api/resend"
113
+ events:
114
+ type: list
115
+ required: true
116
+ description: The event types to deliver, such as `email.delivered` and `email.bounced`.
117
+ example: [email.delivered, email.bounced]
118
+ http:
119
+ find:
120
+ path: /webhooks
121
+ list_path: /data
122
+ match: { endpoint: { param: endpoint } }
123
+ read: { path: "/webhooks/{id}" }
124
+ create: { path: /webhooks }
125
+ update: { method: PATCH, path: "/webhooks/{id}" }
126
+ delete: { path: "/webhooks/{id}" }
127
+ fields:
128
+ events: { param: events }
129
+ outputs:
130
+ signing_secret: { path: /signing_secret, sensitive: true }
@@ -0,0 +1,108 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # S3 API recipes (Amazon S3, and Cloudflare R2 through its S3-compatible endpoint): XML documents, every request
3
+ # signed with AWS Signature Version 4. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
4
+ provider: s3
5
+ title: Amazon S3 / Cloudflare R2 (S3 API)
6
+ category: cors
7
+ verified_on: "2026-10-11"
8
+ docs:
9
+ - title: PutBucketCors
10
+ url: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html
11
+ - title: GetBucketCors
12
+ url: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html
13
+ - title: DeleteBucketCors
14
+ url: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html
15
+ - title: R2 S3 API compatibility
16
+ url: https://developers.cloudflare.com/r2/api/s3/api/
17
+ base_url_example: https://assets.s3.us-east-1.amazonaws.com
18
+ api:
19
+ base_url_env: S3_API_URL
20
+ encoding: xml
21
+ auth:
22
+ aws_sigv4: { region: us-east-1, service: s3, access_key_id_env: AWS_ACCESS_KEY_ID, secret_access_key_env: AWS_SECRET_ACCESS_KEY }
23
+ detect:
24
+ deps: ["@aws-sdk/client-s3"]
25
+ starter:
26
+ - op: bucket_cors
27
+ preview_url: origin
28
+ values: { rule_id: "sponson-${ctx.scope}" }
29
+ where:
30
+ base_url: "the bucket's endpoint: `https://<bucket>.s3.<region>.amazonaws.com`, or R2's `https://<account id>.r2.cloudflarestorage.com/<bucket>`"
31
+ assumptions:
32
+ - id: SS1
33
+ verified: true
34
+ text: "`PUT /?cors` on the bucket's endpoint replaces its whole CORS configuration with a `CORSConfiguration` document of up to 100 `CORSRule`s (each `ID`, `AllowedOrigin`s, `AllowedMethod`s, `AllowedHeader`s, `ExposeHeader`s, `MaxAgeSeconds`) and requires `Content-MD5`, which the SigV4 signer adds for `s3`."
35
+ source: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html
36
+ - id: SS2
37
+ verified: true
38
+ text: "`GET /?cors` answers the configuration as XML."
39
+ source: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html
40
+ - id: SS3
41
+ verified: true
42
+ text: "`DELETE /?cors` removes the configuration (the way to leave a bucket with no rule)."
43
+ source: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html
44
+ - id: SS4
45
+ verified: true
46
+ text: "R2 implements PutBucketCors, GetBucketCors and DeleteBucketCors at `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`; its SigV4 region is `auto`, and `us-east-1` is accepted as an alias, so the recipe's region works for R2 unchanged."
47
+ source: https://developers.cloudflare.com/r2/api/s3/api/
48
+ - id: SS5
49
+ verified: false
50
+ text: "A bucket without a CORS configuration answers 404 `NoSuchCORSConfiguration` (read as an empty list, `parent.if_missing: empty`), and an empty configuration is refused as `MalformedXML` (so the last rule is removed with DELETE, `parent.when_empty: delete`)."
51
+ - id: SS6
52
+ verified: false
53
+ text: "R2 accepts path-style requests (`https://<ACCOUNT_ID>.r2.cloudflarestorage.com/<bucket>/?cors`), so its base URL names the bucket in the path."
54
+ - id: SS7
55
+ verified: false
56
+ text: "S3 answers a rule's elements as they were written; a rule's `ID` identifies it among the bucket's rules (IDs are not required to be unique by S3, so other writers must not reuse the recipe's)."
57
+ ops:
58
+ bucket_cors:
59
+ kind: list_item
60
+ title: CORS rule of a bucket
61
+ summary: "A CORS rule of an S3 or R2 bucket allowing the preview's origin, identified by its `ID` (such as `sponson-pr-42`); every other rule is left as it is. The base URL names the bucket: `https://<bucket>.s3.<region>.amazonaws.com` (set `auth.aws_sigv4.region` to the bucket's region), or `https://<account>.r2.cloudflarestorage.com/<bucket>` for R2. A console edit of the rule is drift; destroy removes only this rule (the whole configuration when it was the last one)."
62
+ covers: [s3-r2-cors]
63
+ params:
64
+ rule_id:
65
+ type: string
66
+ required: true
67
+ pattern: "^.{1,255}$"
68
+ description: The rule's `ID`, at most 255 characters; its identity (SS7).
69
+ example: "sponson-${ctx.scope}"
70
+ origin:
71
+ type: string
72
+ required: true
73
+ pattern: "^(https?://\\S+|\\*)$"
74
+ description: The origin allowed (the preview's URL, without a path).
75
+ example: "https://${ctx.scope}.preview.example.app"
76
+ methods:
77
+ type: list
78
+ default: [GET]
79
+ description: "The methods allowed: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`."
80
+ allowed_headers:
81
+ type: list
82
+ description: "Request headers a preflight may ask for (`*` for any); absent: none."
83
+ expose_headers:
84
+ type: list
85
+ description: Response headers the browser may read; absent, none.
86
+ max_age_seconds:
87
+ type: string
88
+ default: "3000"
89
+ pattern: "^[0-9]+$"
90
+ description: How long a preflight answer may be cached, in seconds (a string, as XML carries it).
91
+ http:
92
+ parent: { path: "/?cors", method: PUT, if_missing: empty, when_empty: delete }
93
+ list_path: /CORSConfiguration/CORSRule
94
+ key_field: ID
95
+ item:
96
+ AllowedHeader: { param: allowed_headers }
97
+ AllowedMethod: { param: methods }
98
+ AllowedOrigin: { param: origin }
99
+ ExposeHeader: { param: expose_headers }
100
+ ID: { param: rule_id }
101
+ MaxAgeSeconds: { param: max_age_seconds }
102
+ sim:
103
+ provider: aws
104
+ base_path: /aws/s3/assets
105
+ seed: { buckets: { assets: { cors: null } } }
106
+ present: /buckets/assets/cors/0
107
+ drift:
108
+ missing: { s3.assets.cors: "remove:sponson-pr-42" }