@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,98 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Flagsmith Admin (management) API recipes, authenticated by an organisation API key. Format and checklist:
3
+ # docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
4
+ provider: flagsmith
5
+ title: Flagsmith
6
+ category: flags
7
+ verified_on: "2026-10-11"
8
+ docs:
9
+ - title: Admin API authentication
10
+ url: https://docs.flagsmith.com/integrating-with-flagsmith/flagsmith-api-overview/admin-api/authentication
11
+ - title: Admin API overview
12
+ url: https://docs.flagsmith.com/integrating-with-flagsmith/flagsmith-api-overview/admin-api
13
+ - title: Flagsmith API OpenAPI document
14
+ url: https://api.flagsmith.com/api/v1/swagger.json
15
+ api:
16
+ base_url: https://api.flagsmith.com/api/v1
17
+ base_url_env: FLAGSMITH_API_URL
18
+ auth: { header: Authorization, value_env: FLAGSMITH_MASTER_API_KEY, prefix: "Api-Key " }
19
+ detect:
20
+ deps: [flagsmith, flagsmith-nodejs, react-native-flagsmith]
21
+ env: ["FLAGSMITH_*"]
22
+ starter:
23
+ - op: environment
24
+ values: { name: "preview-${ctx.scope}" }
25
+ where:
26
+ project_id: "Flagsmith → your project: the number in its URL (`/project/<id>/…`)"
27
+ assumptions:
28
+ - id: FS1
29
+ verified: true
30
+ text: "The Admin API authenticates with an organisation API key as `Authorization: Api-Key <key>`; Bearer is only for short-lived OIDC-exchanged tokens."
31
+ source: https://docs.flagsmith.com/integrating-with-flagsmith/flagsmith-api-overview/admin-api/authentication
32
+ - id: FS2
33
+ verified: true
34
+ text: "`POST /environments/` creates an environment from `name` and `project` (both required) and optional `description`, answering it (201) with its integer `id` and its `api_key`, the client-side environment key SDKs use."
35
+ source: https://api.flagsmith.com/api/v1/swagger.json
36
+ - id: FS3
37
+ verified: true
38
+ text: "`GET /environments/?project=<id>` lists a project's environments as `{ count, next, previous, results: [...] }`."
39
+ source: https://api.flagsmith.com/api/v1/swagger.json
40
+ - id: FS4
41
+ verified: true
42
+ text: "`GET`, `PATCH` and `DELETE /environments/{api_key}/` read, update and delete one environment (DELETE answers 204)."
43
+ source: https://api.flagsmith.com/api/v1/swagger.json
44
+ - id: FS5
45
+ verified: false
46
+ text: "A project has fewer environments than one page of the list holds, so `find` sees them all (`next` is a URL, which the http adapter does not follow)."
47
+ - id: FS6
48
+ verified: false
49
+ text: "A new environment starts with every feature of the project at its default state, and environment names are unique in a project in practice (two with one name make `find` refuse with PARAM_INVALID)."
50
+ - id: FS7
51
+ verified: false
52
+ text: "`description` is answered as sent; `FLAGSMITH_MASTER_API_KEY` is the variable Flagsmith's Terraform provider reads the key from."
53
+ - id: FS8
54
+ verified: false
55
+ text: "Identity and segment overrides are not recipes: Edge projects (Flagsmith SaaS's default) address identities by UUID through `edge-identities`, and a segment override is two objects (a feature segment, then a feature state) created in order."
56
+ - id: FS9
57
+ verified: false
58
+ text: "The create takes the project id as a string of digits (`\"1234\"`) as well as a number, as Django REST Framework's primary-key fields do."
59
+ ops:
60
+ environment:
61
+ kind: resource
62
+ title: Environment per preview
63
+ summary: "A Flagsmith environment per scope in a project, such as `preview-pr-42`, starting from the project's default flag states; its client-side environment key is the output `api_key` for the preview's SDK configuration. Located by name in the project; a console edit of its description is drift; destroy deletes the environment with its flag states and identities."
64
+ covers: [flagsmith-override]
65
+ params:
66
+ project_id:
67
+ type: string
68
+ required: true
69
+ pattern: "^[0-9]+$"
70
+ description: The project's id, digits as a string (FS9).
71
+ example: "1234"
72
+ name:
73
+ type: string
74
+ required: true
75
+ description: The environment's name; its identity in the project (FS6).
76
+ example: "preview-${ctx.scope}"
77
+ description:
78
+ type: string
79
+ default: Preview environment managed by Sponson
80
+ description: The environment's description.
81
+ http:
82
+ find:
83
+ path: "/environments/?project={project_id}"
84
+ list_path: /results
85
+ match: { name: { param: name } }
86
+ read: { path: "/environments/{id}/" }
87
+ create:
88
+ path: /environments/
89
+ body: { project: { param: project_id } }
90
+ update: { method: PATCH }
91
+ id_path: /api_key
92
+ fields:
93
+ description: { param: description }
94
+ outputs:
95
+ api_key: /api_key
96
+ environment_id: /id
97
+ live:
98
+ params: { project_id: SPONSON_LIVE_FLAGSMITH_PROJECT_ID }
@@ -0,0 +1,97 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Fly.io GraphQL API recipes (app secrets). Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ # The per-PR app itself is in fly_machines.yaml (the Machines API has another base URL).
4
+ provider: fly
5
+ title: Fly.io
6
+ category: deploy
7
+ verified_on: "2026-10-11"
8
+ docs:
9
+ - title: Secrets and Fly Apps
10
+ url: https://docs.fly.io/apps/secrets
11
+ - title: superfly/fly-go resource_secrets.go (the GraphQL operations flyctl sends)
12
+ url: https://github.com/superfly/fly-go/blob/main/resource_secrets.go
13
+ - title: superfly/fly-go schema.graphql
14
+ url: https://github.com/superfly/fly-go/blob/main/schema.graphql
15
+ - title: Working with the Machines API (tokens)
16
+ url: https://docs.fly.io/machines/api/working-with-machines-api/
17
+ api:
18
+ base_url: https://api.fly.io/graphql
19
+ base_url_env: FLY_API_URL
20
+ auth: { bearer_env: FLY_API_TOKEN }
21
+ detect:
22
+ env: ["FLY_*"]
23
+ files: [fly.toml]
24
+ starter:
25
+ - op: secret
26
+ where:
27
+ app: "the preview's Fly app: `app` in fly.toml, or the app per pull request a `fly_machines.app` line creates"
28
+ assumptions:
29
+ - id: FS1
30
+ verified: true
31
+ text: "`query ($appName: String!) { app(name: $appName) { secrets { name digest createdAt } } }` lists an app's secrets; values are never returned (Fly does not allow reading plain-text secret values), so the recipe locates a secret by name and compares nothing else."
32
+ source: https://github.com/superfly/fly-go/blob/main/resource_secrets.go
33
+ - id: FS2
34
+ verified: true
35
+ text: "`mutation($input: SetSecretsInput!) { setSecrets(input: $input) { release { id version } } }` with `{ appId, secrets: [{ key, value }] }` sets secrets; `unsetSecrets(input: { appId, keys })` removes them."
36
+ source: https://github.com/superfly/fly-go/blob/main/resource_secrets.go
37
+ - id: FS3
38
+ verified: true
39
+ text: "Plain-text values cannot be read back (`fly secrets list` shows a name, a digest and a date), so a value changed in the console, or in the plan, is not detected: drift is the secret's presence only. To change a value, run `sponson apply --destroy` for the scope (or remove the line, apply, and add it back)."
40
+ source: https://docs.fly.io/apps/secrets
41
+ - id: FS4
42
+ verified: true
43
+ text: "Machines read secrets as environment variables at boot: a secret set without updating the Machines (as `fly secrets set --stage` does) reaches only Machines started or updated afterwards. Order the line before the PR's `fly deploy`; after a later change run `fly secrets deploy`."
44
+ source: https://docs.fly.io/apps/secrets
45
+ - id: FS5
46
+ verified: false
47
+ text: "A token from `fly tokens create` (`FlyV1 fm2_…`) or `fly auth token` is accepted as `Authorization: Bearer <token>` by the GraphQL API, as the Machines API documents for the same `FLY_API_TOKEN`. (fly-go itself sends a macaroon as `FlyV1 <token>`; a block that needs that sends it with `auth: { header: Authorization, value_env: FLY_API_TOKEN }`.)"
48
+ - id: FS6
49
+ verified: false
50
+ text: "Unsetting a secret of an app that no longer exists answers an error saying the app could not be found, which the adapter reads as already gone; unsetting an absent key of an existing app is not an error. setSecrets through GraphQL updates no Machines (FS4). Current flyctl sets secrets through the Machines API instead; the GraphQL mutations are not marked deprecated."
51
+ ops:
52
+ secret:
53
+ kind: resource
54
+ title: App secret
55
+ summary: "One secret of a Fly app (typically the app made for the pull request, `fly_machines.app`), set with `setSecrets` and removed with `unsetSecrets`. Fly never returns secret values, so only its presence is compared: a secret unset in the console is recreated, a changed value is not noticed (FS3). Machines see it once started or updated after the write: order it before the PR's deploy (FS4)."
56
+ covers: [fly-secrets]
57
+ params:
58
+ app:
59
+ type: string
60
+ required: true
61
+ description: The app's name.
62
+ example: "demo-${ctx.scope}"
63
+ name:
64
+ type: string
65
+ required: true
66
+ pattern: "^[A-Za-z_][A-Za-z0-9_]*$"
67
+ description: The secret's name (the environment variable the Machines see).
68
+ example: DATABASE_URL
69
+ value:
70
+ type: string
71
+ required: true
72
+ description: The secret's value, normally a `{ secret }` reference. It is sent, never stored or compared.
73
+ example: "postgres://app@db.internal:5432/${ctx.scope}"
74
+ http:
75
+ find:
76
+ graphql:
77
+ query: "query ($appName: String!) { app(name: $appName) { secrets { name digest createdAt } } }"
78
+ variables: { appName: { param: app } }
79
+ data_path: /app/secrets
80
+ match: { name: { param: name } }
81
+ create:
82
+ graphql:
83
+ query: "mutation($input: SetSecretsInput!) { setSecrets(input: $input) { release { id version } } }"
84
+ variables: { input: { appId: { param: app }, secrets: [{ key: { param: name }, value: { param: value } }] } }
85
+ idempotent: true
86
+ delete:
87
+ graphql:
88
+ query: "mutation($input: UnsetSecretsInput!) { unsetSecrets(input: $input) { release { id version } } }"
89
+ variables: { input: { appId: { param: app }, keys: [{ param: name }] } }
90
+ sim:
91
+ provider: fly
92
+ seed: { apps: { demo-pr-42: {} } }
93
+ present: /apps/demo-pr-42/secrets/DATABASE_URL
94
+ drift:
95
+ missing: { secrets.demo-pr-42.DATABASE_URL: delete }
96
+ live:
97
+ params: { app: SPONSON_LIVE_FLY_APP }
@@ -0,0 +1,67 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Fly.io Machines API recipes (the per-PR app). Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ # The app's secrets are in fly.yaml (the GraphQL API has another base URL).
4
+ provider: fly_machines
5
+ title: Fly.io Machines API
6
+ category: deploy
7
+ verified_on: "2026-10-11"
8
+ docs:
9
+ - title: Working with the Machines API
10
+ url: https://docs.fly.io/machines/api/working-with-machines-api/
11
+ - title: Apps resource
12
+ url: https://docs.fly.io/machines/api/apps-resource/
13
+ api:
14
+ base_url: https://api.machines.dev/v1
15
+ base_url_env: FLY_MACHINES_API_URL
16
+ auth: { bearer_env: FLY_API_TOKEN }
17
+ assumptions:
18
+ - id: FM1
19
+ verified: true
20
+ text: "The Machines API's public base URL is `https://api.machines.dev`, its paths start with `/v1`, and requests carry `Authorization: Bearer <fly_api_token>`."
21
+ source: https://docs.fly.io/machines/api/working-with-machines-api/
22
+ - id: FM2
23
+ verified: true
24
+ text: "`POST /v1/apps` with `app_name` (unique) and `org_slug` (both required) and optional `network` creates an app and answers 201 with `id` and `created_at`."
25
+ source: https://docs.fly.io/machines/api/apps-resource/
26
+ - id: FM3
27
+ verified: true
28
+ text: "`GET /v1/apps/{app_name}` answers the app (`id`, `name`, `status`, `organization { name, slug }`), so the name the plan chooses locates it."
29
+ source: https://docs.fly.io/machines/api/apps-resource/
30
+ - id: FM4
31
+ verified: true
32
+ text: "`DELETE /v1/apps/{app_name}` answers 202; Machines should be stopped first, and `?force=true` stops them and deletes the app at once, which is what the recipe's destroy sends (a PR app has the PR's Machines running)."
33
+ source: https://docs.fly.io/machines/api/apps-resource/
34
+ - id: FM5
35
+ verified: false
36
+ text: "Deleting the app deletes its secrets and its Machines with it; an app name another organization holds is refused with a conflict, which the adapter then fails to read (403/404) and reports."
37
+ ops:
38
+ app:
39
+ kind: resource
40
+ title: App
41
+ summary: "A Fly app per pull request, such as `demo-${ctx.scope}`, in an organization: created with `POST /v1/apps`, located by its name, deleted with its Machines on destroy (`?force=true`). Its secrets are `fly.secret` lines; the Machines are what the PR's `fly deploy` creates. Nothing about the app is compared besides its existence."
42
+ covers: [fly-secrets]
43
+ params:
44
+ app_name:
45
+ type: string
46
+ required: true
47
+ pattern: "^[a-z0-9][a-z0-9-]*$"
48
+ description: The app's name, unique across Fly.io.
49
+ example: "demo-${ctx.scope}"
50
+ org_slug:
51
+ type: string
52
+ required: true
53
+ description: The organization the app is created in.
54
+ example: acme
55
+ network:
56
+ type: string
57
+ description: A private network (6PN) to put the app on.
58
+ http:
59
+ read: { path: "/apps/{app_name}" }
60
+ create:
61
+ path: /apps
62
+ body: { app_name: { param: app_name }, org_slug: { param: org_slug }, network: { param: network } }
63
+ delete: { path: "/apps/{app_name}?force=true" }
64
+ sim:
65
+ id_from: app_name
66
+ live:
67
+ params: { org_slug: SPONSON_LIVE_FLY_ORG }
@@ -0,0 +1,102 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # Google Cloud Storage JSON API recipes (Firebase Storage buckets too), authenticated by the token gcloud prints.
3
+ # Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
4
+ provider: gcs
5
+ title: Google Cloud Storage (Firebase Storage)
6
+ category: cors
7
+ verified_on: "2026-10-11"
8
+ docs:
9
+ - title: "Buckets: patch"
10
+ url: https://docs.cloud.google.com/storage/docs/json_api/v1/buckets/patch
11
+ - title: Buckets resource (cors)
12
+ url: https://docs.cloud.google.com/storage/docs/json_api/v1/buckets
13
+ - title: Set up and view CORS configurations
14
+ url: https://docs.cloud.google.com/storage/docs/using-cors
15
+ - title: Performance tips (patch semantics)
16
+ url: https://docs.cloud.google.com/storage/docs/json_api/v1/how-tos/performance
17
+ api:
18
+ base_url: https://storage.googleapis.com/storage/v1
19
+ base_url_env: GCS_API_URL
20
+ auth: { command: [gcloud, auth, print-access-token] }
21
+ detect:
22
+ deps: ["@google-cloud/storage"]
23
+ starter:
24
+ - op: bucket_cors
25
+ preview_url: origin
26
+ where:
27
+ bucket: "`gcloud storage buckets list`, or Firebase console → Storage (the bucket's name)"
28
+ assumptions:
29
+ - id: GC1
30
+ verified: true
31
+ text: "The JSON API takes an OAuth access token as `Authorization: Bearer`; Google's own example gets it from `gcloud auth print-access-token`."
32
+ source: https://docs.cloud.google.com/storage/docs/using-cors
33
+ - id: GC2
34
+ verified: true
35
+ text: "`GET /b/{bucket}?fields=cors` answers the bucket's CORS configuration; `PATCH /b/{bucket}?fields=cors` with `{ cors: [...] }` sets it, and `{ cors: [] }` removes it."
36
+ source: https://docs.cloud.google.com/storage/docs/using-cors
37
+ - id: GC3
38
+ verified: true
39
+ text: "A patch that contains a list replaces the whole list: individual entries cannot be added or removed, so the whole `cors` list is written back."
40
+ source: https://docs.cloud.google.com/storage/docs/json_api/v1/how-tos/performance
41
+ - id: GC4
42
+ verified: true
43
+ text: "Each `cors` entry has `origin` (list), `method` (list), `responseHeader` (list) and `maxAgeSeconds` (integer)."
44
+ source: https://docs.cloud.google.com/storage/docs/json_api/v1/buckets
45
+ - id: GC5
46
+ verified: true
47
+ text: "`PATCH` takes `ifMetagenerationMatch`, a precondition on the bucket's metageneration. The recipe does not send it (the http adapter has no preconditions); Sponson's own writers are serialised by the parent lock, a writer outside Sponson can still race."
48
+ source: https://docs.cloud.google.com/storage/docs/json_api/v1/buckets/patch
49
+ - id: GC6
50
+ verified: true
51
+ text: "Changing a bucket's CORS needs the `storage.buckets.update` permission."
52
+ source: https://docs.cloud.google.com/storage/docs/json_api/v1/buckets/patch
53
+ - id: GC7
54
+ verified: false
55
+ text: "A bucket without a CORS configuration answers `{}` (no `cors` field), read as an empty list, and entries are answered as written, in order."
56
+ - id: GC8
57
+ verified: false
58
+ text: "An entry is identified by its first origin: the recipe writes one origin per entry, and no other entry of the bucket starts with the same origin."
59
+ ops:
60
+ bucket_cors:
61
+ kind: list_item
62
+ title: CORS entry of a bucket
63
+ summary: "An entry of a bucket's `cors` list allowing one origin, such as the preview's URL on the Firebase Storage bucket `my-app.appspot.com`, with its methods, response headers and max age. The rest of the list is left as it is; a console edit of the entry is drift, and destroy removes only this entry."
64
+ covers: [gcs-cors]
65
+ params:
66
+ bucket:
67
+ type: string
68
+ required: true
69
+ description: The bucket's name (Firebase Storage's default bucket is `<project>.appspot.com` or `<project>.firebasestorage.app`).
70
+ example: my-app.appspot.com
71
+ origin:
72
+ type: string
73
+ required: true
74
+ pattern: "^(https?://\\S+|\\*)$"
75
+ description: The origin allowed, without a path (the preview's URL); the entry's identity (GC8).
76
+ example: "https://${ctx.scope}.preview.example.app"
77
+ methods:
78
+ type: list
79
+ default: [GET, HEAD]
80
+ description: The methods allowed.
81
+ response_headers:
82
+ type: list
83
+ default: [Content-Type]
84
+ description: The response headers the browser may read.
85
+ max_age_seconds:
86
+ type: integer
87
+ default: 3600
88
+ description: How long a preflight answer may be cached.
89
+ http:
90
+ parent: { path: "/b/{bucket}?fields=cors", method: PATCH }
91
+ list_path: /cors
92
+ key_field: /origin/0
93
+ item:
94
+ origin: [{ param: origin }]
95
+ method: { param: methods }
96
+ responseHeader: { param: response_headers }
97
+ maxAgeSeconds: { param: max_age_seconds }
98
+ sim:
99
+ objects:
100
+ "/b/{bucket}": { cors: [{ origin: [https://app.example.com], method: [GET], responseHeader: [Content-Type], maxAgeSeconds: 3600 }] }
101
+ live:
102
+ params: { bucket: SPONSON_LIVE_GCS_BUCKET }
@@ -0,0 +1,123 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # GitHub REST API recipes. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ provider: github
4
+ title: GitHub
5
+ category: webhooks
6
+ verified_on: "2026-10-11"
7
+ docs:
8
+ - title: REST API endpoints for repository webhooks
9
+ url: https://docs.github.com/en/rest/repos/webhooks
10
+ - title: Authenticating to the REST API
11
+ url: https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api
12
+ - title: Getting started with the REST API (headers, API versions)
13
+ url: https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api
14
+ api:
15
+ base_url: https://api.github.com
16
+ base_url_env: GITHUB_API_URL
17
+ auth: { bearer_env: GITHUB_TOKEN }
18
+ headers: { X-GitHub-Api-Version: "2022-11-28", User-Agent: sponson }
19
+ detect:
20
+ deps: ["@octokit/webhooks", probot]
21
+ env: [GITHUB_WEBHOOK_SECRET]
22
+ starter:
23
+ - op: repo_webhook
24
+ where:
25
+ url: "the preview's webhook handler: its URL plus the handler's path (a `{ from }` reference cannot carry a path)"
26
+ assumptions:
27
+ - id: GH1
28
+ verified: true
29
+ text: "`POST /repos/{owner}/{repo}/hooks` creates a repository webhook from `name: web`, `config` (`url`, `content_type`, `secret`, `insecure_ssl`), `events` and `active`, answering 201 with the hook (its integer `id` and `config`), authenticated with `Authorization: Bearer <token>`."
30
+ source: https://docs.github.com/en/rest/repos/webhooks
31
+ - id: GH2
32
+ verified: true
33
+ text: "`GET /repos/{owner}/{repo}/hooks` answers a bare array of hooks, each with `config.url`; it pages with `per_page` (at most 100) and `page`."
34
+ source: https://docs.github.com/en/rest/repos/webhooks
35
+ - id: GH3
36
+ verified: true
37
+ text: "`PATCH /repos/{owner}/{repo}/hooks/{hook_id}` updates `config`, `events` (replacing the list) and `active`; a secret set before is removed unless the same or a new one is sent, so every update sends the config's `url` and `secret` again."
38
+ source: https://docs.github.com/en/rest/repos/webhooks
39
+ - id: GH4
40
+ verified: true
41
+ text: "`DELETE /repos/{owner}/{repo}/hooks/{hook_id}` answers 204, and 404 when the hook is gone."
42
+ source: https://docs.github.com/en/rest/repos/webhooks
43
+ - id: GH5
44
+ verified: true
45
+ text: "Each hook should have a unique config, and hooks may share one only when their events do not overlap. The recipe takes the URL as the hook's identity; two hooks with the same URL make `find` refuse (PARAM_INVALID) instead of guessing."
46
+ source: https://docs.github.com/en/rest/repos/webhooks
47
+ - id: GH6
48
+ verified: false
49
+ text: "The secret is write-only: answers carry it masked or not at all, so it is sent on create and update but never compared, and a secret changed in the plan reaches GitHub only with the next change of a compared field. Keep it in a secret manager and reference it with `{ secret: … }`."
50
+ - id: GH7
51
+ verified: false
52
+ text: "A repository has at most 100 hooks for this purpose (GitHub limits hooks per event), so the first page of `?per_page=100` holds the one `find` looks for."
53
+ - id: GH8
54
+ verified: false
55
+ text: "`config.content_type` and `config.insecure_ssl` are answered as sent (`json`/`form`, the strings `\"0\"`/`\"1\"`), and `Accept: application/json` is served like `application/vnd.github+json`."
56
+ - id: GH9
57
+ verified: false
58
+ text: "The token needs the repository permission Webhooks (write) or the classic `admin:repo_hook` scope; the `GITHUB_TOKEN` of GitHub Actions does not have it, so use a GitHub App token or a fine-grained token. `GITHUB_API_URL` (set by Actions) points at GitHub Enterprise Server where that is the host."
59
+ ops:
60
+ repo_webhook:
61
+ kind: resource
62
+ title: Repository webhook
63
+ summary: A repository webhook delivering to an environment's URL, such as the preview deployment's `/api/github` handler. Located by its URL; a console edit of its events, its active flag, its content type or its TLS check is drift. The secret is sent but not compared (GH6).
64
+ covers: [github-webhook]
65
+ params:
66
+ owner:
67
+ type: string
68
+ required: true
69
+ description: The repository's owner (user or organization).
70
+ example: acme
71
+ repo:
72
+ type: string
73
+ required: true
74
+ description: The repository's name.
75
+ example: app
76
+ url:
77
+ type: string
78
+ required: true
79
+ pattern: "^https?://\\S+$"
80
+ description: The URL GitHub delivers to; it identifies the hook in the repository.
81
+ example: "https://${ctx.scope}.preview.example.com/api/github"
82
+ events:
83
+ type: list
84
+ default: [push]
85
+ description: The events that trigger a delivery (`["*"]` for all).
86
+ active:
87
+ type: boolean
88
+ default: true
89
+ description: Whether deliveries are sent.
90
+ content_type:
91
+ type: string
92
+ default: json
93
+ enum: [json, form]
94
+ description: The payload's media type.
95
+ insecure_ssl:
96
+ type: string
97
+ default: "0"
98
+ enum: ["0", "1"]
99
+ description: '"1" skips verifying the TLS certificate of the URL.'
100
+ secret:
101
+ type: string
102
+ description: "The secret deliveries are signed with (write-only; GH6). Use `{ secret: … }`."
103
+ http:
104
+ find:
105
+ path: "/repos/{owner}/{repo}/hooks?per_page=100"
106
+ match: { /config/url: { param: url } }
107
+ create:
108
+ path: "/repos/{owner}/{repo}/hooks"
109
+ body: { name: web, config: { secret: { param: secret } } }
110
+ update:
111
+ method: PATCH
112
+ path: "/repos/{owner}/{repo}/hooks/{id}"
113
+ body: { config: { url: { param: url }, secret: { param: secret } } }
114
+ delete: { path: "/repos/{owner}/{repo}/hooks/{id}" }
115
+ fields:
116
+ active: { param: active }
117
+ events: { param: events }
118
+ /config/content_type: { param: content_type }
119
+ /config/insecure_ssl: { param: insecure_ssl }
120
+ sim:
121
+ numeric_ids: true
122
+ live:
123
+ params: { owner: SPONSON_LIVE_GITHUB_OWNER, repo: SPONSON_LIVE_GITHUB_REPO }
@@ -0,0 +1,124 @@
1
+ # yaml-language-server: $schema=../../../schema/recipe.schema.json
2
+ # GrowthBook REST API recipes. Format and checklist: docs/adr/0020-recipes.md, CONTRIBUTING.md "Adding a recipe".
3
+ provider: growthbook
4
+ title: GrowthBook
5
+ category: flags
6
+ verified_on: "2026-10-11"
7
+ docs:
8
+ - title: REST API reference (authentication, versioning)
9
+ url: https://docs.growthbook.io/api
10
+ - title: Get a feature (v2)
11
+ url: https://docs.growthbook.io/api#operation/getFeatureV2
12
+ - title: Update a feature (v2)
13
+ url: https://docs.growthbook.io/api#operation/updateFeatureV2
14
+ - title: OpenAPI spec
15
+ url: https://api.growthbook.io/api/v1/openapi.yaml
16
+ api:
17
+ base_url: https://api.growthbook.io/api
18
+ base_url_env: GROWTHBOOK_API_URL
19
+ auth: { bearer_env: GROWTHBOOK_API_KEY }
20
+ detect:
21
+ deps: ["@growthbook/"]
22
+ env: ["GROWTHBOOK_*"]
23
+ starter:
24
+ - op: force_rule
25
+ values: { description: "Sponson preview ${ctx.scope}", condition: "{\"preview\": \"${ctx.scope}\"}" }
26
+ where:
27
+ feature_id: "GrowthBook → Features: the feature's key"
28
+ environment: "GrowthBook → Settings → Environments: the one previews evaluate in"
29
+ assumptions:
30
+ - id: GB1
31
+ verified: true
32
+ text: "GrowthBook Cloud's API is at `https://api.growthbook.io/api` (self-hosted: its API host plus `/api`), authenticated with a secret key or personal access token as `Authorization: Bearer`."
33
+ source: https://docs.growthbook.io/api
34
+ - id: GB2
35
+ verified: true
36
+ text: "The v1 feature endpoints (rules per environment) are deprecated; in v2 a feature's rules are one top-level `rules` array, each rule naming its `environments` (or `allEnvironments`)."
37
+ source: https://docs.growthbook.io/api#operation/getFeatureV2
38
+ - id: GB3
39
+ verified: true
40
+ text: "`GET /v2/features/{id}` answers `{ feature: { …, rules: [...] } }`."
41
+ source: https://docs.growthbook.io/api#operation/getFeatureV2
42
+ - id: GB4
43
+ verified: true
44
+ text: "`POST /v2/features/{id}` patch-merges the top-level fields it is sent, and `rules`, when sent, replaces the whole array: so the array is read, the rule added or changed, and the whole array sent back. Rules read with GET may be posted back unchanged. The update publishes a new revision at once."
45
+ source: https://docs.growthbook.io/api#operation/updateFeatureV2
46
+ - id: GB5
47
+ verified: true
48
+ text: "A force rule is `{ type: force, description, condition, value, enabled, allEnvironments, environments }`; `condition` is a JSON query serialised as a string and `value` is a string (`\"true\"` for a boolean feature)."
49
+ source: https://api.growthbook.io/api/v1/openapi.yaml
50
+ - id: GB6
51
+ verified: false
52
+ text: "Rule ids may be chosen by the client but what the server does with them is not documented, so the rule is identified by its `description`, which it stores as written; two rules with that description are one rule to Sponson."
53
+ - id: GB7
54
+ verified: false
55
+ text: "A rule is read back with `condition` exactly as sent (not reformatted), so the compared fields do not show as changed on every plan."
56
+ - id: GB8
57
+ verified: true
58
+ text: "When the feature requires approval, the update answers 422 with the gates that blocked it, unless the organisation lets the REST API bypass approvals; the apply then fails."
59
+ source: https://docs.growthbook.io/api#operation/updateFeatureV2
60
+ ops:
61
+ force_rule:
62
+ kind: list_item
63
+ title: Feature force rule
64
+ summary: "A force rule on an existing feature that serves a value to the preview in one environment, such as `value \"true\"` when `{\"preview\": \"pr-42\"}`. Added after the feature's other rules (rules are evaluated in order: an earlier rule that matches the preview wins). A console edit of its condition, value, environments or enabled state is drift. Destroy removes the rule."
65
+ covers: [growthbook-force-rule]
66
+ params:
67
+ feature_id:
68
+ type: string
69
+ required: true
70
+ description: The feature's key.
71
+ example: new-checkout
72
+ environment:
73
+ type: string
74
+ required: true
75
+ description: The environment the rule applies in.
76
+ example: staging
77
+ description:
78
+ type: string
79
+ required: true
80
+ description: The rule's description, unique among the feature's rules; it identifies the rule.
81
+ example: "Sponson preview ${ctx.scope}"
82
+ condition:
83
+ type: string
84
+ required: true
85
+ description: The targeting condition, a JSON query as a string.
86
+ example: "{\"preview\": \"${ctx.scope}\"}"
87
+ value:
88
+ type: string
89
+ required: true
90
+ description: The value served, as a string (`"true"` for a boolean feature, JSON for a JSON feature).
91
+ example: "true"
92
+ enabled:
93
+ type: boolean
94
+ default: true
95
+ description: Whether the rule is on.
96
+ http:
97
+ parent: { path: "/v2/features/{feature_id}", method: POST, item_path: /feature }
98
+ list_path: /rules
99
+ key_field: description
100
+ item:
101
+ description: { param: description }
102
+ type: force
103
+ condition: { param: condition }
104
+ value: { param: value }
105
+ enabled: { param: enabled }
106
+ allEnvironments: false
107
+ environments: [{ param: environment }]
108
+ sim:
109
+ objects:
110
+ "/v2/features/{feature_id}":
111
+ id: new-checkout
112
+ valueType: boolean
113
+ defaultValue: "false"
114
+ rules:
115
+ - id: fr_internal
116
+ type: force
117
+ description: Internal users
118
+ condition: "{\"internal\": true}"
119
+ value: "true"
120
+ enabled: true
121
+ allEnvironments: true
122
+ environments: []
123
+ live:
124
+ params: { feature_id: SPONSON_LIVE_GROWTHBOOK_FEATURE_ID, environment: SPONSON_LIVE_GROWTHBOOK_ENVIRONMENT }