@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.
Files changed (129) 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/cloud_run.d.ts +12 -0
  8. package/dist/cloud_run.js +469 -0
  9. package/dist/cloud_run.js.map +1 -0
  10. package/dist/cloudflare.d.ts +25 -0
  11. package/dist/cloudflare.js +310 -0
  12. package/dist/cloudflare.js.map +1 -0
  13. package/dist/common.d.ts +2 -6
  14. package/dist/common.js.map +1 -1
  15. package/dist/digest.d.ts +46 -0
  16. package/dist/digest.js +129 -0
  17. package/dist/digest.js.map +1 -0
  18. package/dist/digitalocean.d.ts +5 -0
  19. package/dist/digitalocean.js +243 -0
  20. package/dist/digitalocean.js.map +1 -0
  21. package/dist/http-adapter-action.d.ts +63 -0
  22. package/dist/http-adapter-action.js +369 -0
  23. package/dist/http-adapter-action.js.map +1 -0
  24. package/dist/http-adapter-auth.d.ts +94 -0
  25. package/dist/http-adapter-auth.js +440 -0
  26. package/dist/http-adapter-auth.js.map +1 -0
  27. package/dist/http-adapter-client.d.ts +57 -0
  28. package/dist/http-adapter-client.js +163 -0
  29. package/dist/http-adapter-client.js.map +1 -0
  30. package/dist/http-adapter-graphql.d.ts +39 -0
  31. package/dist/http-adapter-graphql.js +157 -0
  32. package/dist/http-adapter-graphql.js.map +1 -0
  33. package/dist/http-adapter-list.d.ts +33 -0
  34. package/dist/http-adapter-list.js +421 -0
  35. package/dist/http-adapter-list.js.map +1 -0
  36. package/dist/http-adapter-resource.d.ts +29 -0
  37. package/dist/http-adapter-resource.js +462 -0
  38. package/dist/http-adapter-resource.js.map +1 -0
  39. package/dist/http-adapter-spec.d.ts +216 -0
  40. package/dist/http-adapter-spec.js +660 -0
  41. package/dist/http-adapter-spec.js.map +1 -0
  42. package/dist/http-adapter.d.ts +16 -0
  43. package/dist/http-adapter.js +28 -0
  44. package/dist/http-adapter.js.map +1 -0
  45. package/dist/http.d.ts +61 -2
  46. package/dist/http.js +79 -18
  47. package/dist/http.js.map +1 -1
  48. package/dist/index.d.ts +13 -0
  49. package/dist/index.js +26 -2
  50. package/dist/index.js.map +1 -1
  51. package/dist/launchdarkly.d.ts +15 -0
  52. package/dist/launchdarkly.js +337 -0
  53. package/dist/launchdarkly.js.map +1 -0
  54. package/dist/manual.d.ts +13 -0
  55. package/dist/manual.js +201 -0
  56. package/dist/manual.js.map +1 -0
  57. package/dist/neon.js +1 -1
  58. package/dist/neon.js.map +1 -1
  59. package/dist/netlify.d.ts +18 -0
  60. package/dist/netlify.js +436 -0
  61. package/dist/netlify.js.map +1 -0
  62. package/dist/planetscale.d.ts +10 -0
  63. package/dist/planetscale.js +311 -0
  64. package/dist/planetscale.js.map +1 -0
  65. package/dist/providers.d.ts +28 -0
  66. package/dist/providers.js +41 -0
  67. package/dist/providers.js.map +1 -0
  68. package/dist/recipes.d.ts +167 -0
  69. package/dist/recipes.js +246 -0
  70. package/dist/recipes.js.map +1 -0
  71. package/dist/secrets.d.ts +44 -0
  72. package/dist/secrets.js +279 -0
  73. package/dist/secrets.js.map +1 -1
  74. package/dist/sigv4.d.ts +56 -0
  75. package/dist/sigv4.js +118 -0
  76. package/dist/sigv4.js.map +1 -0
  77. package/dist/supabase.d.ts +9 -0
  78. package/dist/supabase.js +385 -0
  79. package/dist/supabase.js.map +1 -0
  80. package/dist/vercel.js +1 -1
  81. package/dist/vercel.js.map +1 -1
  82. package/dist/xata.d.ts +8 -0
  83. package/dist/xata.js +142 -0
  84. package/dist/xata.js.map +1 -0
  85. package/dist/xml.d.ts +25 -0
  86. package/dist/xml.js +211 -0
  87. package/dist/xml.js.map +1 -0
  88. package/package.json +11 -4
  89. package/recipes/amplify.yaml +147 -0
  90. package/recipes/auth0.yaml +178 -0
  91. package/recipes/azure_swa.yaml +116 -0
  92. package/recipes/cloudflare.yaml +108 -0
  93. package/recipes/cockroachdb.yaml +132 -0
  94. package/recipes/configcat.yaml +123 -0
  95. package/recipes/datadog.yaml +136 -0
  96. package/recipes/firebase.yaml +84 -0
  97. package/recipes/flagsmith.yaml +98 -0
  98. package/recipes/fly.yaml +97 -0
  99. package/recipes/fly_machines.yaml +67 -0
  100. package/recipes/gcs.yaml +102 -0
  101. package/recipes/github.yaml +124 -0
  102. package/recipes/growthbook.yaml +124 -0
  103. package/recipes/heroku.yaml +98 -0
  104. package/recipes/honeycomb.yaml +89 -0
  105. package/recipes/inngest.yaml +97 -0
  106. package/recipes/kinde.yaml +120 -0
  107. package/recipes/mongodb_atlas.yaml +119 -0
  108. package/recipes/okta.yaml +177 -0
  109. package/recipes/posthog.yaml +127 -0
  110. package/recipes/postmark.yaml +105 -0
  111. package/recipes/postmark_server.yaml +93 -0
  112. package/recipes/prisma.yaml +150 -0
  113. package/recipes/qstash.yaml +134 -0
  114. package/recipes/railway.yaml +130 -0
  115. package/recipes/render.yaml +103 -0
  116. package/recipes/resend.yaml +130 -0
  117. package/recipes/s3.yaml +108 -0
  118. package/recipes/sendgrid.yaml +165 -0
  119. package/recipes/sentry.yaml +158 -0
  120. package/recipes/split.yaml +121 -0
  121. package/recipes/statsig.yaml +124 -0
  122. package/recipes/stripe.yaml +310 -0
  123. package/recipes/stytch.yaml +113 -0
  124. package/recipes/svix.yaml +109 -0
  125. package/recipes/turso.yaml +98 -0
  126. package/recipes/unleash.yaml +149 -0
  127. package/recipes/upstash.yaml +130 -0
  128. package/recipes/vercel.yaml +116 -0
  129. package/recipes/workos.yaml +70 -0
@@ -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" }
@@ -0,0 +1,165 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Twilio SendGrid v3 API recipes. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ provider: sendgrid
4
+ title: SendGrid
5
+ category: email
6
+ verified_on: "2026-10-11"
7
+ docs:
8
+ - title: Create an Event Webhook
9
+ url: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/create-an-event-webhook
10
+ - title: Get all Event Webhooks
11
+ url: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/get-all-event-webhooks
12
+ - title: Get an Event Webhook
13
+ url: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/get-an-event-webhook
14
+ - title: Update an Event Webhook
15
+ url: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/update-an-event-webhook
16
+ - title: Delete an Event Webhook
17
+ url: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/delete-an-event-webhook
18
+ - title: Create a parse setting
19
+ url: https://www.twilio.com/docs/sendgrid/api-reference/settings-inbound-parse/create-a-parse-setting
20
+ - title: Retrieve a specific parse setting
21
+ url: https://www.twilio.com/docs/sendgrid/api-reference/settings-inbound-parse/retrieve-a-specific-parse-setting
22
+ - title: Update a parse setting
23
+ url: https://www.twilio.com/docs/sendgrid/api-reference/settings-inbound-parse/update-a-parse-setting
24
+ - title: Delete a parse setting
25
+ url: https://www.twilio.com/docs/sendgrid/api-reference/settings-inbound-parse/delete-a-parse-setting
26
+ api:
27
+ base_url: https://api.sendgrid.com
28
+ base_url_env: SENDGRID_API_URL
29
+ auth: { bearer_env: SENDGRID_API_KEY }
30
+ detect:
31
+ deps: ["@sendgrid/"]
32
+ env: ["SENDGRID_*"]
33
+ starter:
34
+ - op: event_webhook
35
+ where:
36
+ url: "the preview's event handler: its URL plus the handler's path (a `{ from }` reference cannot carry a path)"
37
+ assumptions:
38
+ - id: SG1
39
+ verified: true
40
+ text: "`POST /v3/user/webhooks/event/settings` creates an Event Webhook from `url` (required), `enabled`, one boolean per event type (`processed`, `delivered`, `dropped`, `deferred`, `bounce`, `open`, `click`, `spam_report`, `unsubscribe`, `group_unsubscribe`, `group_resubscribe`) and `friendly_name`, answering 201 with the webhook and its string `id`; requests carry the API key as `Authorization: Bearer`. EU regional subusers use `https://api.eu.sendgrid.com`."
41
+ source: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/create-an-event-webhook
42
+ - id: SG2
43
+ verified: true
44
+ text: "Each URL can be used by only one Event Webhook (a second create answers 400 naming the existing id), so the URL identifies the webhook; the number of webhooks is limited by the plan (400 beyond it)."
45
+ source: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/create-an-event-webhook
46
+ - id: SG3
47
+ verified: true
48
+ text: "`GET /v3/user/webhooks/event/settings/all` answers `{ max_allowed, webhooks: [...] }` with every webhook's settings and `id`."
49
+ source: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/get-all-event-webhooks
50
+ - id: SG4
51
+ verified: true
52
+ text: "`PATCH /v3/user/webhooks/event/settings/{id}` requires `url` and updates the fields sent; `DELETE` on the same path answers 204, and 404 for an unknown id. Without an id these paths act on the oldest webhook, which the recipe never does (its paths always carry the id)."
53
+ source: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/update-an-event-webhook
54
+ - id: SG5
55
+ verified: true
56
+ text: "Signature verification and its public key are set through a separate endpoint and are not part of the webhook's create or update; this recipe does not manage them."
57
+ source: https://www.twilio.com/docs/sendgrid/api-reference/webhooks/create-an-event-webhook
58
+ - id: SG6
59
+ verified: true
60
+ text: "`POST /v3/user/webhooks/parse/settings` creates an Inbound Parse setting from `url`, `hostname`, `spam_check` and `send_raw`, answering 201 with those four fields bare; `GET`, `PATCH` and `DELETE` (204) `/v3/user/webhooks/parse/settings/{hostname}` read, update and remove it. The hostname must be on a domain authenticated in SendGrid, with an MX record pointing at SendGrid."
61
+ source: https://www.twilio.com/docs/sendgrid/api-reference/settings-inbound-parse/create-a-parse-setting
62
+ - id: SG7
63
+ verified: false
64
+ text: "A PATCH that leaves out an event flag keeps its current value (the documentation's examples send only `url`); the recipe sends every flag it compares anyway."
65
+ - id: SG8
66
+ verified: false
67
+ text: "A parse setting for a hostname that exists already is refused with an error status (not documented which); the next run then finds it by its hostname."
68
+ ops:
69
+ event_webhook:
70
+ kind: resource
71
+ title: Event Webhook
72
+ summary: An Event Webhook delivering an environment's email events to its URL, such as the preview's `/api/sendgrid/events` handler. Located by its URL (unique in SendGrid); a console edit of its enabled flag, an event flag or its friendly name is drift.
73
+ covers: [sendgrid-webhook]
74
+ params:
75
+ url:
76
+ type: string
77
+ required: true
78
+ pattern: "^https?://\\S+$"
79
+ description: The URL events are posted to; its identity (SG2).
80
+ example: "https://${ctx.scope}.preview.example.com/api/sendgrid/events"
81
+ enabled:
82
+ type: boolean
83
+ default: true
84
+ description: Whether events are posted.
85
+ friendly_name:
86
+ type: string
87
+ description: A label for the webhook (not unique).
88
+ example: "sponson ${ctx.env} ${ctx.scope}"
89
+ processed: { type: boolean, default: false, description: Post `processed` events. }
90
+ delivered: { type: boolean, default: true, description: Post `delivered` events. }
91
+ dropped: { type: boolean, default: true, description: Post `dropped` events. }
92
+ deferred: { type: boolean, default: false, description: Post `deferred` events. }
93
+ bounce: { type: boolean, default: true, description: Post `bounce` events. }
94
+ open: { type: boolean, default: false, description: Post `open` events. }
95
+ click: { type: boolean, default: false, description: Post `click` events. }
96
+ spam_report: { type: boolean, default: true, description: Post `spam_report` events. }
97
+ unsubscribe: { type: boolean, default: false, description: Post `unsubscribe` events. }
98
+ group_unsubscribe: { type: boolean, default: false, description: Post `group_unsubscribe` events. }
99
+ group_resubscribe: { type: boolean, default: false, description: Post `group_resubscribe` events. }
100
+ http:
101
+ find:
102
+ path: /v3/user/webhooks/event/settings/all
103
+ list_path: /webhooks
104
+ match: { url: { param: url } }
105
+ create: { path: /v3/user/webhooks/event/settings }
106
+ update:
107
+ method: PATCH
108
+ path: "/v3/user/webhooks/event/settings/{id}"
109
+ body: { url: { param: url } }
110
+ delete: { path: "/v3/user/webhooks/event/settings/{id}" }
111
+ fields:
112
+ enabled: { param: enabled }
113
+ friendly_name: { param: friendly_name }
114
+ processed: { param: processed }
115
+ delivered: { param: delivered }
116
+ dropped: { param: dropped }
117
+ deferred: { param: deferred }
118
+ bounce: { param: bounce }
119
+ open: { param: open }
120
+ click: { param: click }
121
+ spam_report: { param: spam_report }
122
+ unsubscribe: { param: unsubscribe }
123
+ group_unsubscribe: { param: group_unsubscribe }
124
+ group_resubscribe: { param: group_resubscribe }
125
+ parse_setting:
126
+ kind: resource
127
+ title: Inbound Parse setting
128
+ summary: An Inbound Parse host per environment, such as `pr-42.parse.example.com`, posting the mail it receives to the environment's URL. Named by its hostname; a console edit of its URL, spam check or raw flag is drift. The hostname's domain must be authenticated in SendGrid and its MX record point at SendGrid (a wildcard record serves every environment).
129
+ covers: [sendgrid-webhook]
130
+ params:
131
+ hostname:
132
+ type: string
133
+ required: true
134
+ pattern: "^[a-z0-9.-]+$"
135
+ description: The host mail is received for, lower-case; its identity.
136
+ example: "${ctx.scope}.parse.example.com"
137
+ url:
138
+ type: string
139
+ required: true
140
+ pattern: "^https?://\\S+$"
141
+ description: The URL parsed mail is posted to.
142
+ example: "https://${ctx.scope}.preview.example.com/api/sendgrid/inbound"
143
+ spam_check:
144
+ type: boolean
145
+ default: false
146
+ description: Check mail for spam before posting it.
147
+ send_raw:
148
+ type: boolean
149
+ default: false
150
+ description: Post the original MIME message.
151
+ http:
152
+ read: { path: "/v3/user/webhooks/parse/settings/{hostname}" }
153
+ create:
154
+ path: /v3/user/webhooks/parse/settings
155
+ body: { hostname: { param: hostname } }
156
+ update: { method: PATCH }
157
+ id_path: /hostname
158
+ fields:
159
+ url: { param: url }
160
+ spam_check: { param: spam_check }
161
+ send_raw: { param: send_raw }
162
+ sim:
163
+ id_from: hostname
164
+ live:
165
+ params: { hostname: SPONSON_LIVE_SENDGRID_PARSE_HOSTNAME }