atmn 2.0.41 → 2.0.43

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 (40) hide show
  1. package/dist/bin.js +405 -392
  2. package/dist/index.js +20 -21
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/package.json +2 -2
  5. package/src/actions/pull/webhooks/appendWebhook.ts +6 -4
  6. package/src/actions/pull/webhooks/applyWebhooksPull.ts +28 -105
  7. package/src/actions/pull/webhooks/deleteWebhook.ts +59 -0
  8. package/src/actions/pull/webhooks/locateWebhook.ts +9 -1
  9. package/src/actions/pull/webhooks/readWebhookEnvs.ts +7 -12
  10. package/src/actions/pull/webhooks/types.ts +3 -2
  11. package/src/actions/pull/webhooks/updateWebhook.ts +37 -42
  12. package/src/actions/pull/webhooks/webhookEnvSkipWarning.ts +29 -0
  13. package/src/actions/pull/webhooks/webhookPullEnvs.ts +5 -5
  14. package/src/actions/pull.ts +1 -1
  15. package/src/actions/push.ts +30 -12
  16. package/src/actions/webhooks/applyWebhooks.ts +41 -10
  17. package/src/actions/webhooks/previewWebhookEnvs.ts +213 -0
  18. package/src/actions/webhooks/resolveWebhooksForEnv.ts +5 -16
  19. package/src/actions/webhooks/types/webhookEnv.ts +1 -3
  20. package/src/actions/webhooks/webhookEnvFailures.ts +27 -0
  21. package/src/actions/webhooks/webhookSecretName.ts +10 -9
  22. package/src/actions/webhooks/writeWebhookSecrets.ts +5 -2
  23. package/src/auth/runOAuthFlow.ts +1 -1
  24. package/src/cli.ts +23 -25
  25. package/src/generated/apiRoutes.ts +43 -12
  26. package/src/generated/client.ts +10 -10
  27. package/src/generated/emit.ts +4 -4
  28. package/src/generated/emitRuntime.ts +3 -3
  29. package/src/generated/lintRules.ts +16 -20
  30. package/src/generated/lintRuntime.ts +10 -0
  31. package/src/generated/skills.ts +10 -10
  32. package/src/generated/webhooks.ts +7 -9
  33. package/src/generated/wire.ts +0 -1
  34. package/src/render/renderPreview.ts +25 -9
  35. package/src/render/renderWebhooks.ts +9 -3
  36. package/src/actions/pull/webhooks/removeWebhookEnv.ts +0 -141
  37. package/src/actions/webhooks/isDashboardWebhook.ts +0 -9
  38. package/src/actions/webhooks/newWebhookId.ts +0 -4
  39. package/src/actions/webhooks/previewWebhooks.ts +0 -40
  40. package/src/actions/webhooks/resolveWebhookEnv.ts +0 -34
package/dist/index.js CHANGED
@@ -2234,28 +2234,38 @@ var LINT_RULES = {
2234
2234
  webhooks: {
2235
2235
  label: "webhook",
2236
2236
  idField: "id",
2237
- required: ["events", "id", "url"],
2237
+ required: ["env", "id", "url"],
2238
2238
  fields: {
2239
2239
  id: {
2240
2240
  minLength: 1,
2241
2241
  maxLength: 256,
2242
2242
  pattern: "^[a-zA-Z0-9_-]+$"
2243
2243
  },
2244
- events: {
2245
- minItems: 1
2244
+ env: {
2245
+ pattern: "^[a-z0-9_-]+$"
2246
+ },
2247
+ url: {
2248
+ pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
2246
2249
  }
2247
2250
  },
2248
2251
  rules: [
2252
+ {
2253
+ kind: "notMap",
2254
+ field: "url",
2255
+ because: 'Each webhook() is one endpoint in one env: split into one webhook() per env, e.g. webhook({ id, env: "live", url }).'
2256
+ },
2249
2257
  {
2250
2258
  kind: "unique",
2251
2259
  field: "id",
2252
- because: "Two webhooks claiming one id race to define the same endpoint."
2260
+ alongside: "env",
2261
+ because: "Two webhooks claiming one id in one env race to define the same endpoint."
2253
2262
  },
2254
2263
  {
2255
2264
  kind: "unique",
2256
2265
  field: "id",
2266
+ alongside: "env",
2257
2267
  asEnvName: true,
2258
- because: "Each webhook's signing secret is saved as AUTUMN_WEBHOOK_<ID>_SECRET, so these two would overwrite each other's secret. Rename one."
2268
+ because: "Each webhook's signing secret is saved under its id in an env var name, so these two would overwrite each other's secret. Rename one."
2259
2269
  },
2260
2270
  {
2261
2271
  kind: "rejects",
@@ -2269,12 +2279,6 @@ var LINT_RULES = {
2269
2279
  prefix: "vercel.",
2270
2280
  because: "vercel.* events can't be mixed with other events in one webhook: they're delivered from a separate app. Make one webhook for each."
2271
2281
  },
2272
- {
2273
- kind: "nonEmpty",
2274
- field: "url",
2275
- warning: true,
2276
- because: "With no environment key the webhook is registered nowhere. Add `live`, `sandbox` or a sandbox's slug."
2277
- },
2278
2282
  {
2279
2283
  kind: "knownValues",
2280
2284
  field: "events",
@@ -2296,14 +2300,6 @@ var LINT_RULES = {
2296
2300
  because: "isn't known to this atmn version; the server will check it."
2297
2301
  }
2298
2302
  ]
2299
- },
2300
- "webhooks.url": {
2301
- keys: {
2302
- pattern: "^[a-z0-9_-]+$"
2303
- },
2304
- values: {
2305
- pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
2306
- }
2307
2303
  }
2308
2304
  };
2309
2305
 
@@ -2524,6 +2520,10 @@ var entryRuleFailures = ({
2524
2520
  return [];
2525
2521
  return value.filter((item) => typeof item === "string" && !rule.values.includes(item)).map((item) => `\`${item}\` ${rule.because}`);
2526
2522
  }
2523
+ case "notMap": {
2524
+ const value = entry[rule.field];
2525
+ return isEntry(value) ? [`${rule.field} is a map. ${rule.because}`] : [];
2526
+ }
2527
2527
  case "uniformPrefix": {
2528
2528
  const value = entry[rule.field];
2529
2529
  if (!Array.isArray(value))
@@ -3014,8 +3014,7 @@ var CATALOG_HINTS = hintsOf({
3014
3014
  "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers",
3015
3015
  "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
3016
3016
  "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.modelMarkups",
3017
- "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups",
3018
- "webhooks.url"
3017
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups"
3019
3018
  ],
3020
3019
  frozenPaths: [
3021
3020
  "plans.licenses.metadata",
@@ -1 +1 @@
1
- {"version":"7.0.0-dev.20260511.1","root":["../src/bin.ts","../src/cli.ts","../src/index.ts","../src/version.ts","../src/actions/env.ts","../src/actions/login.ts","../src/actions/pull.ts","../src/actions/push.ts","../src/actions/api/callApi.ts","../src/actions/api/registerApiCommands.ts","../src/actions/env/fetchOrgInfo.ts","../src/actions/env/types/orgInfo.ts","../src/actions/init/runInit.ts","../src/actions/login/keyless.ts","../src/actions/pull/appendPlanVersionFixture.ts","../src/actions/pull/applyMappings.ts","../src/actions/pull/applyPreview.ts","../src/actions/pull/applySettingsPreview.ts","../src/actions/pull/changedFixtureKeys.ts","../src/actions/pull/elementStatesPlanId.ts","../src/actions/pull/listSourceFiles.ts","../src/actions/pull/locateFixture.ts","../src/actions/pull/pruneUnpulledPlanIds.ts","../src/actions/pull/resolveCollectionTarget.ts","../src/actions/pull/rewriteConfig.ts","../src/actions/pull/scaffoldConfig.ts","../src/actions/pull/webhooks/appendWebhook.ts","../src/actions/pull/webhooks/applyWebhooksPull.ts","../src/actions/pull/webhooks/locateWebhook.ts","../src/actions/pull/webhooks/readWebhookEnvs.ts","../src/actions/pull/webhooks/removeWebhookEnv.ts","../src/actions/pull/webhooks/types.ts","../src/actions/pull/webhooks/updateWebhook.ts","../src/actions/pull/webhooks/webhookPullEnvs.ts","../src/actions/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.ts","../src/actions/push/pushLanesError.ts","../src/actions/reset/runReset.ts","../src/actions/sandbox/createSandbox.ts","../src/actions/sandbox/deleteSandbox.ts","../src/actions/sandbox/listSandboxes.ts","../src/actions/sandbox/useSandbox.ts","../src/actions/sandbox/withSandboxScopeHint.ts","../src/actions/sandbox/types/sandboxClient.ts","../src/actions/skills/ignoreBundledSkills.ts","../src/actions/skills/skills.ts","../src/actions/webhooks/applyWebhooks.ts","../src/actions/webhooks/isDashboardWebhook.ts","../src/actions/webhooks/newWebhookId.ts","../src/actions/webhooks/previewWebhooks.ts","../src/actions/webhooks/resolveWebhookEnv.ts","../src/actions/webhooks/resolveWebhooksForEnv.ts","../src/actions/webhooks/webhookSecretName.ts","../src/actions/webhooks/writeWebhookSecrets.ts","../src/actions/webhooks/types/webhookEnv.ts","../src/auth/announceAuthorizationUrl.ts","../src/auth/buildAuthorizationUrl.ts","../src/auth/callbackPages.ts","../src/auth/createOrgApiKeys.ts","../src/auth/keyless.ts","../src/auth/oauthConfig.ts","../src/auth/runOAuthFlow.ts","../src/auth/browser/openSystemBrowser.ts","../src/auth/browser/tryOpenBrowser.ts","../src/auth/browser/watchLauncher.ts","../src/auth/types/browserOpener.ts","../src/auth/types/impersonationTokens.ts","../src/auth/types/oauthTokens.ts","../src/auth/types/orgApiKeys.ts","../src/config/configPackageName.ts","../src/config/legacyConfig.ts","../src/config/loadConfig.ts","../src/env/assertSandboxTarget.ts","../src/env/loadEnv.ts","../src/env/resolveTarget.ts","../src/env/sandboxKeyName.ts","../src/generated/apiRoutes.ts","../src/generated/client.ts","../src/generated/emit.ts","../src/generated/emitRuntime.ts","../src/generated/features.ts","../src/generated/isLocalWebhookUrl.ts","../src/generated/labels.ts","../src/generated/licenses.ts","../src/generated/lintRules.ts","../src/generated/lintRuntime.ts","../src/generated/mappingAssignments.ts","../src/generated/plans.ts","../src/generated/referralPrograms.ts","../src/generated/rewards.ts","../src/generated/sandboxName.ts","../src/generated/settings.ts","../src/generated/skills.ts","../src/generated/variants.ts","../src/generated/webhooks.ts","../src/generated/wire.ts","../src/http/autumnFetch.ts","../src/project/chooseConfigDir.ts","../src/project/resolveProject.ts","../src/project/rootMarker.ts","../src/prompt/prompt.ts","../src/prompt/select.ts","../src/render/renderEnv.ts","../src/render/renderPreview.ts","../src/render/renderSandboxes.ts","../src/render/renderWebhooks.ts","../src/render/stripTerminalControls.ts","../src/repo/findRepoRoot.ts","../src/surgery/appendPropertyEdit.ts","../src/surgery/appendToArray.ts","../src/surgery/appendToBinding.ts","../src/surgery/appendToCollection.ts","../src/surgery/appendToFixtureArray.ts","../src/surgery/arrayBinding.ts","../src/surgery/deleteFixtureLiteral.ts","../src/surgery/deleteReference.ts","../src/surgery/ensureBuilderImport.ts","../src/surgery/findFixture.ts","../src/surgery/fixtureEdit.ts","../src/surgery/fixtureLocation.ts","../src/surgery/insertCollection.ts","../src/surgery/insertFirstProperty.ts","../src/surgery/patchFixturePaths.ts","../src/surgery/patchFixtureProperty.ts","../src/surgery/patchSingletonProperty.ts","../src/surgery/readFixtureValue.ts","../src/surgery/replaceFixture.ts","../src/surgery/setFixtureProperty.ts","../src/surgery/staticFixtureRule.ts","../test/apiCommand.test.ts","../test/authorizationUrl.test.ts","../test/autumnFetch.test.ts","../test/backfill.test.ts","../test/backfillInlineVariants.test.ts","../test/backfillSlug.test.ts","../test/backfillVariants.test.ts","../test/chooseConfigDir.test.ts","../test/cliTargetFlags.test.ts","../test/client.test.ts","../test/configPackageName.test.ts","../test/emitFixture.test.ts","../test/env.test.ts","../test/envWriter.test.ts","../test/ignoreBundledSkills.test.ts","../test/init.test.ts","../test/keyless.test.ts","../test/legacyConfig.test.ts","../test/lintDocument.test.ts","../test/loadConfigNode.test.ts","../test/loadConfigV2.test.ts","../test/locateFixtureUnique.test.ts","../test/login.test.ts","../test/nodeNextTypes.test.ts","../test/patchFixturePaths.test.ts","../test/plans.test.ts","../test/previewIsEmpty.test.ts","../test/prompt.test.ts","../test/pull.test.ts","../test/pullByReference.test.ts","../test/pullFieldLevel.test.ts","../test/pullMappings.test.ts","../test/pullOmitDefaults.test.ts","../test/pullPlans.test.ts","../test/pullStatesSlugs.test.ts","../test/pullTwoCollections.test.ts","../test/readFixtureValue.test.ts","../test/renameHint.test.ts","../test/renderPreview.test.ts","../test/renderSandboxes.test.ts","../test/renderSettings.test.ts","../test/reset.test.ts","../test/resolveCollectionTarget.test.ts","../test/resolveProject.test.ts","../test/resolveTarget.test.ts","../test/sandboxActions.test.ts","../test/sandboxKeyName.test.ts","../test/sandboxUse.test.ts","../test/settingsActions.test.ts","../test/settingsWire.test.ts","../test/skills.test.ts","../test/watchLauncher.test.ts","../test/wireDocument.test.ts","../test/helpers/targetFor.ts","../test/surgery/appendToCollection.test.ts","../test/surgery/deleteFixtureLiteral.test.ts","../test/surgery/deleteReference.test.ts","../test/surgery/findFixture.test.ts","../test/surgery/fixtureLocation.test.ts","../test/surgery/insertCollection.test.ts","../test/surgery/insertFirstProperty.test.ts","../test/surgery/patchFixtureProperty.test.ts","../test/surgery/patchSingletonProperty.test.ts","../test/surgery/replaceFixture.test.ts","../test/surgery/staticFixtureRule.test.ts","../test/webhooks/webhooksEnv.test.ts","../test/webhooks/webhooksLint.test.ts","../test/webhooks/webhooksPull.test.ts","../test/webhooks/webhooksPullEnvs.test.ts","../test/webhooks/webhooksPush.test.ts"]}
1
+ {"version":"7.0.0-dev.20260511.1","root":["../src/bin.ts","../src/cli.ts","../src/index.ts","../src/version.ts","../src/actions/env.ts","../src/actions/login.ts","../src/actions/pull.ts","../src/actions/push.ts","../src/actions/api/callApi.ts","../src/actions/api/registerApiCommands.ts","../src/actions/env/fetchOrgInfo.ts","../src/actions/env/types/orgInfo.ts","../src/actions/init/runInit.ts","../src/actions/login/keyless.ts","../src/actions/pull/appendPlanVersionFixture.ts","../src/actions/pull/applyMappings.ts","../src/actions/pull/applyPreview.ts","../src/actions/pull/applySettingsPreview.ts","../src/actions/pull/changedFixtureKeys.ts","../src/actions/pull/elementStatesPlanId.ts","../src/actions/pull/listSourceFiles.ts","../src/actions/pull/locateFixture.ts","../src/actions/pull/pruneUnpulledPlanIds.ts","../src/actions/pull/resolveCollectionTarget.ts","../src/actions/pull/rewriteConfig.ts","../src/actions/pull/scaffoldConfig.ts","../src/actions/pull/webhooks/appendWebhook.ts","../src/actions/pull/webhooks/applyWebhooksPull.ts","../src/actions/pull/webhooks/deleteWebhook.ts","../src/actions/pull/webhooks/locateWebhook.ts","../src/actions/pull/webhooks/readWebhookEnvs.ts","../src/actions/pull/webhooks/types.ts","../src/actions/pull/webhooks/updateWebhook.ts","../src/actions/pull/webhooks/webhookEnvSkipWarning.ts","../src/actions/pull/webhooks/webhookPullEnvs.ts","../src/actions/push/backfillInternalIds.ts","../src/actions/push/deprecatedFields.ts","../src/actions/push/pushLanesError.ts","../src/actions/reset/runReset.ts","../src/actions/sandbox/createSandbox.ts","../src/actions/sandbox/deleteSandbox.ts","../src/actions/sandbox/listSandboxes.ts","../src/actions/sandbox/useSandbox.ts","../src/actions/sandbox/withSandboxScopeHint.ts","../src/actions/sandbox/types/sandboxClient.ts","../src/actions/skills/ignoreBundledSkills.ts","../src/actions/skills/skills.ts","../src/actions/webhooks/applyWebhooks.ts","../src/actions/webhooks/previewWebhookEnvs.ts","../src/actions/webhooks/resolveWebhooksForEnv.ts","../src/actions/webhooks/webhookEnvFailures.ts","../src/actions/webhooks/webhookSecretName.ts","../src/actions/webhooks/writeWebhookSecrets.ts","../src/actions/webhooks/types/webhookEnv.ts","../src/auth/announceAuthorizationUrl.ts","../src/auth/buildAuthorizationUrl.ts","../src/auth/callbackPages.ts","../src/auth/createOrgApiKeys.ts","../src/auth/keyless.ts","../src/auth/oauthConfig.ts","../src/auth/runOAuthFlow.ts","../src/auth/browser/openSystemBrowser.ts","../src/auth/browser/tryOpenBrowser.ts","../src/auth/browser/watchLauncher.ts","../src/auth/types/browserOpener.ts","../src/auth/types/impersonationTokens.ts","../src/auth/types/oauthTokens.ts","../src/auth/types/orgApiKeys.ts","../src/config/configPackageName.ts","../src/config/legacyConfig.ts","../src/config/loadConfig.ts","../src/env/assertSandboxTarget.ts","../src/env/loadEnv.ts","../src/env/resolveTarget.ts","../src/env/sandboxKeyName.ts","../src/generated/apiRoutes.ts","../src/generated/client.ts","../src/generated/emit.ts","../src/generated/emitRuntime.ts","../src/generated/features.ts","../src/generated/isLocalWebhookUrl.ts","../src/generated/labels.ts","../src/generated/licenses.ts","../src/generated/lintRules.ts","../src/generated/lintRuntime.ts","../src/generated/mappingAssignments.ts","../src/generated/plans.ts","../src/generated/referralPrograms.ts","../src/generated/rewards.ts","../src/generated/sandboxName.ts","../src/generated/settings.ts","../src/generated/skills.ts","../src/generated/variants.ts","../src/generated/webhooks.ts","../src/generated/wire.ts","../src/http/autumnFetch.ts","../src/project/chooseConfigDir.ts","../src/project/resolveProject.ts","../src/project/rootMarker.ts","../src/prompt/prompt.ts","../src/prompt/select.ts","../src/render/renderEnv.ts","../src/render/renderPreview.ts","../src/render/renderSandboxes.ts","../src/render/renderWebhooks.ts","../src/render/stripTerminalControls.ts","../src/repo/findRepoRoot.ts","../src/surgery/appendPropertyEdit.ts","../src/surgery/appendToArray.ts","../src/surgery/appendToBinding.ts","../src/surgery/appendToCollection.ts","../src/surgery/appendToFixtureArray.ts","../src/surgery/arrayBinding.ts","../src/surgery/deleteFixtureLiteral.ts","../src/surgery/deleteReference.ts","../src/surgery/ensureBuilderImport.ts","../src/surgery/findFixture.ts","../src/surgery/fixtureEdit.ts","../src/surgery/fixtureLocation.ts","../src/surgery/insertCollection.ts","../src/surgery/insertFirstProperty.ts","../src/surgery/patchFixturePaths.ts","../src/surgery/patchFixtureProperty.ts","../src/surgery/patchSingletonProperty.ts","../src/surgery/readFixtureValue.ts","../src/surgery/replaceFixture.ts","../src/surgery/setFixtureProperty.ts","../src/surgery/staticFixtureRule.ts","../test/apiCommand.test.ts","../test/authorizationUrl.test.ts","../test/autumnFetch.test.ts","../test/backfill.test.ts","../test/backfillInlineVariants.test.ts","../test/backfillSlug.test.ts","../test/backfillVariants.test.ts","../test/chooseConfigDir.test.ts","../test/cliTargetFlags.test.ts","../test/client.test.ts","../test/configPackageName.test.ts","../test/emitFixture.test.ts","../test/env.test.ts","../test/envWriter.test.ts","../test/ignoreBundledSkills.test.ts","../test/init.test.ts","../test/keyless.test.ts","../test/legacyConfig.test.ts","../test/lintDocument.test.ts","../test/loadConfigNode.test.ts","../test/loadConfigV2.test.ts","../test/locateFixtureUnique.test.ts","../test/login.test.ts","../test/nodeNextTypes.test.ts","../test/patchFixturePaths.test.ts","../test/plans.test.ts","../test/previewIsEmpty.test.ts","../test/prompt.test.ts","../test/pull.test.ts","../test/pullByReference.test.ts","../test/pullFieldLevel.test.ts","../test/pullMappings.test.ts","../test/pullOmitDefaults.test.ts","../test/pullPlans.test.ts","../test/pullStatesSlugs.test.ts","../test/pullTwoCollections.test.ts","../test/readFixtureValue.test.ts","../test/renameHint.test.ts","../test/renderPreview.test.ts","../test/renderSandboxes.test.ts","../test/renderSettings.test.ts","../test/reset.test.ts","../test/resolveCollectionTarget.test.ts","../test/resolveProject.test.ts","../test/resolveTarget.test.ts","../test/sandboxActions.test.ts","../test/sandboxKeyName.test.ts","../test/sandboxUse.test.ts","../test/settingsActions.test.ts","../test/settingsWire.test.ts","../test/skills.test.ts","../test/watchLauncher.test.ts","../test/wireDocument.test.ts","../test/helpers/targetFor.ts","../test/surgery/appendToCollection.test.ts","../test/surgery/deleteFixtureLiteral.test.ts","../test/surgery/deleteReference.test.ts","../test/surgery/findFixture.test.ts","../test/surgery/fixtureLocation.test.ts","../test/surgery/insertCollection.test.ts","../test/surgery/insertFirstProperty.test.ts","../test/surgery/patchFixtureProperty.test.ts","../test/surgery/patchSingletonProperty.test.ts","../test/surgery/replaceFixture.test.ts","../test/surgery/staticFixtureRule.test.ts","../test/webhooks/webhooksEnv.test.ts","../test/webhooks/webhooksLint.test.ts","../test/webhooks/webhooksPull.test.ts","../test/webhooks/webhooksPullEnvs.test.ts","../test/webhooks/webhooksPush.test.ts","../test/webhooks/webhooksRender.test.ts"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atmn",
3
- "version": "2.0.41",
3
+ "version": "2.0.43",
4
4
  "license": "MIT",
5
5
  "description": "The CLI for Autumn — define your pricing in code, sync it to Autumn, and keep everything in version control.",
6
6
  "bin": {
@@ -49,5 +49,5 @@
49
49
  "open": "10.2.0",
50
50
  "yaml": "2.9.0"
51
51
  },
52
- "gitHead": "bbfad2d5f31b4fb3d1ce9df14ef2654fe12b8a7d"
52
+ "gitHead": "f52f8ea6a3ce297f99e4ec2cde6218fdcb73f80b"
53
53
  }
@@ -13,7 +13,7 @@ import type { PullFiles, RemoteWebhook } from "./types";
13
13
  const SPEC = SYNCED_LISTS.webhooks;
14
14
  const COLLECTION = "webhooks";
15
15
 
16
- /** The fixture a remote webhook reads as from one env: its url under that env alone. */
16
+ /** The fixture one env's remote endpoint reads as. */
17
17
  const fixtureRowOf = ({
18
18
  webhook,
19
19
  envKey,
@@ -22,13 +22,15 @@ const fixtureRowOf = ({
22
22
  envKey: string;
23
23
  }): Record<string, unknown> => ({
24
24
  id: webhook.id,
25
- url: { [envKey]: webhook.url },
26
- events: webhook.events,
25
+ [SPEC.envField]: envKey,
26
+ url: webhook.url,
27
+ // No list is every event; `events` is left out rather than written as `[]`.
28
+ ...(webhook.events.length > 0 ? { events: webhook.events } : {}),
27
29
  ...(webhook.description ? { description: webhook.description } : {}),
28
30
  ...(webhook.disabled ? { disabled: true } : {}),
29
31
  });
30
32
 
31
- /** Rule 1: a remote webhook the config never names is appended for this env. */
33
+ /** An endpoint the config never names is appended as this env's entry. */
32
34
  export const appendWebhook = ({
33
35
  pull,
34
36
  webhook,
@@ -1,7 +1,5 @@
1
- import { isDashboardWebhook } from "../../webhooks/isDashboardWebhook";
2
- import { newWebhookId } from "../../webhooks/newWebhookId";
3
1
  import { appendWebhook } from "./appendWebhook";
4
- import { removeWebhookEnv } from "./removeWebhookEnv";
2
+ import { deleteWebhook } from "./deleteWebhook";
5
3
  import type {
6
4
  PullFiles,
7
5
  RemoteWebhook,
@@ -10,27 +8,18 @@ import type {
10
8
  } from "./types";
11
9
  import { updateWebhook } from "./updateWebhook";
12
10
 
13
- const isVercelWebhook = (events: unknown): boolean =>
14
- Array.isArray(events) &&
15
- events.length > 0 &&
16
- events.every(
17
- (event) => typeof event === "string" && event.startsWith("vercel."),
18
- );
19
-
20
- /** The config as it reads once this env's remote webhook is written into it. */
11
+ /** The config entry as it reads once this env's endpoint is written into it. */
21
12
  const statedFrom = ({
22
- row,
23
13
  webhook,
24
14
  envKey,
25
15
  }: {
26
- row: StatedWebhook | undefined;
27
16
  webhook: RemoteWebhook;
28
17
  envKey: string;
29
18
  }): StatedWebhook => ({
30
- ...row,
31
19
  id: webhook.id,
32
- url: { ...row?.url, [envKey]: webhook.url },
33
- events: webhook.events.length > 0 ? webhook.events : row?.events,
20
+ env: envKey,
21
+ url: webhook.url,
22
+ events: webhook.events.length > 0 ? webhook.events : undefined,
34
23
  description: webhook.description,
35
24
  disabled: webhook.disabled,
36
25
  });
@@ -42,118 +31,52 @@ const merge = (target: WebhookEditResult, source: WebhookEditResult): void => {
42
31
  };
43
32
 
44
33
  /**
45
- * One env's webhooks written back into the config. Only `url[envKey]` is
46
- * touched on the url map; every other env's key, comment and ordering stays.
47
- * A dashboard webhook is matched to the config by URL, never duplicated: the
48
- * next push adopts it by that URL, keeping its signing secret.
34
+ * One env's endpoints written back into the config, one entry each, found by
35
+ * `(env, id)`. A dashboard endpoint is its own `ep_` id; other envs' entries,
36
+ * comments and ordering stay.
49
37
  */
50
38
  export const applyWebhooksPull = ({
51
39
  pull,
52
40
  remote,
53
41
  stated,
54
42
  envKey,
55
- newId = newWebhookId,
56
43
  }: {
57
44
  pull: PullFiles;
58
45
  remote: RemoteWebhook[];
59
46
  /** The config's webhooks as evaluated; absent when it states none. */
60
47
  stated: StatedWebhook[] | undefined;
61
48
  envKey: string;
62
- /** Mints the id a dashboard webhook is written under. */
63
- newId?: () => string;
64
49
  }): WebhookEditResult & { stated: StatedWebhook[] } => {
65
50
  const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
66
- const statedById = new Map((stated ?? []).map((row) => [row.id, row]));
67
- // What the next env pulled into the same files sees as the config.
68
- const next = new Map(statedById);
69
- // An id the config states is the config's, whatever its shape.
70
- const fromDashboard = (webhook: RemoteWebhook): boolean =>
71
- !statedById.has(webhook.id) && isDashboardWebhook(webhook);
72
- const present = new Set<string>();
51
+ const rows = stated ?? [];
52
+ const inEnv = new Map(
53
+ rows.filter((row) => row.env === envKey).map((row) => [row.id, row]),
54
+ );
55
+ const next = rows.filter((row) => row.env !== envKey);
73
56
 
74
57
  for (const webhook of remote) {
75
- if (fromDashboard(webhook)) {
76
- // Vercel and other webhooks live in separate apps: a URL only
77
- // matches within one.
78
- const sameApp = (row: StatedWebhook) =>
79
- isVercelWebhook(row.events) === isVercelWebhook(webhook.events);
80
- // Another env's copy of this URL fills in this env's key rather than
81
- // becoming a second webhook.
82
- const represented =
83
- (stated ?? []).find(
84
- (row) => row.url?.[envKey] === webhook.url && sameApp(row),
85
- ) ??
86
- (stated ?? []).find(
87
- (row) =>
88
- row.url?.[envKey] === undefined &&
89
- Object.values(row.url ?? {}).includes(webhook.url) &&
90
- sameApp(row),
91
- );
92
- if (represented !== undefined) {
93
- present.add(represented.id);
94
- next.set(
95
- represented.id,
96
- statedFrom({
97
- row: represented,
98
- webhook: { ...webhook, id: represented.id },
99
- envKey,
100
- }),
101
- );
102
- merge(
103
- result,
104
- updateWebhook({
105
- pull,
106
- webhook: { ...webhook, id: represented.id },
107
- stated: represented,
108
- envKey,
109
- }),
110
- );
111
- continue;
112
- }
113
- if (webhook.events.length === 0) {
114
- result.lines.push(
115
- `· webhook ${webhook.id} receives every event; give it an event list in the dashboard, or add it to your config, to manage it here`,
116
- );
117
- continue;
118
- }
119
- const id = newId();
120
- const failure = appendWebhook({
121
- pull,
122
- webhook: { ...webhook, id },
123
- envKey,
124
- });
125
- if (failure === null) {
126
- next.set(
127
- id,
128
- statedFrom({ row: undefined, webhook: { ...webhook, id }, envKey }),
129
- );
130
- result.lines.push(
131
- `+ webhook ${id} (made in the dashboard; push adopts it by URL)`,
132
- );
133
- } else result.unlocated.push({ id, action: failure });
134
- continue;
135
- }
136
- present.add(webhook.id);
137
- const row = statedById.get(webhook.id);
138
- next.set(webhook.id, statedFrom({ row, webhook, envKey }));
58
+ const row = inEnv.get(webhook.id);
139
59
  if (row !== undefined) {
140
60
  merge(result, updateWebhook({ pull, webhook, stated: row, envKey }));
61
+ next.push(statedFrom({ webhook, envKey }));
141
62
  continue;
142
63
  }
143
64
  const failure = appendWebhook({ pull, webhook, envKey });
144
- if (failure === null) result.lines.push(`+ webhook ${webhook.id}`);
145
- else result.unlocated.push({ id: webhook.id, action: failure });
65
+ if (failure !== null) {
66
+ result.unlocated.push({ id: webhook.id, action: failure });
67
+ continue;
68
+ }
69
+ result.lines.push(`+ webhook ${webhook.id} (${envKey})`);
70
+ next.push(statedFrom({ webhook, envKey }));
146
71
  }
147
72
 
148
- for (const row of stated ?? []) {
149
- if (present.has(row.id) || row.url?.[envKey] === undefined) continue;
150
- const removed = removeWebhookEnv({ pull, stated: row, envKey });
73
+ const remoteIds = new Set(remote.map(({ id }) => id));
74
+ for (const row of inEnv.values()) {
75
+ if (remoteIds.has(row.id)) continue;
76
+ const removed = deleteWebhook({ pull, stated: row, envKey });
151
77
  merge(result, removed);
152
- // A key left in the source (code, or unlocated) is still the config's.
153
- if (removed.lines.length === 0) continue;
154
- const { [envKey]: _removed, ...url } = row.url;
155
- if (Object.keys(url).length === 0) next.delete(row.id);
156
- else next.set(row.id, { ...row, url });
78
+ // An entry left in the source (unlocated) is still the config's.
79
+ if (removed.lines.length === 0) next.push(row);
157
80
  }
158
- return { ...result, stated: [...next.values()] };
81
+ return { ...result, stated: next };
159
82
  };
@@ -0,0 +1,59 @@
1
+ import { SYNCED_LISTS } from "../../../generated/emit";
2
+ import { deleteFixtureLiteral } from "../../../surgery/deleteFixtureLiteral";
3
+ import { deleteReference } from "../../../surgery/deleteReference";
4
+ import { resolveCollectionTarget } from "../resolveCollectionTarget";
5
+ import { locateWebhook, webhookWhere } from "./locateWebhook";
6
+ import type { PullFiles, StatedWebhook, WebhookEditResult } from "./types";
7
+
8
+ const SPEC = SYNCED_LISTS.webhooks;
9
+
10
+ /** The server has no such endpoint in this env: its entry goes, with any export reference left pointing at it. */
11
+ export const deleteWebhook = ({
12
+ pull,
13
+ stated,
14
+ envKey,
15
+ }: {
16
+ pull: PullFiles;
17
+ stated: StatedWebhook;
18
+ envKey: string;
19
+ }): WebhookEditResult => {
20
+ const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
21
+ const located = locateWebhook({ pull, id: stated.id, envKey });
22
+ const removed =
23
+ located === null
24
+ ? null
25
+ : deleteFixtureLiteral({
26
+ source: located.source,
27
+ builder: SPEC.builder,
28
+ idField: SPEC.idField,
29
+ id: stated.id,
30
+ where: webhookWhere({ envKey }),
31
+ });
32
+ if (located === null || removed === null) {
33
+ result.unlocated.push({
34
+ id: stated.id,
35
+ action: `delete the ${envKey} webhook by hand`,
36
+ });
37
+ return result;
38
+ }
39
+ pull.files.set(located.file, removed.source);
40
+ if (removed.exportedName !== undefined) {
41
+ const referenceFiles = new Set([pull.configPath]);
42
+ const target = resolveCollectionTarget({
43
+ configPath: pull.configPath,
44
+ files: pull.files,
45
+ collection: "webhooks",
46
+ });
47
+ if (target !== null) referenceFiles.add(target.file);
48
+ for (const file of referenceFiles)
49
+ pull.files.set(
50
+ file,
51
+ deleteReference({
52
+ source: pull.files.get(file) ?? "",
53
+ name: removed.exportedName,
54
+ }),
55
+ );
56
+ }
57
+ result.lines.push(`- webhook ${stated.id} (${envKey})`);
58
+ return result;
59
+ };
@@ -4,13 +4,15 @@ import type { PullFiles } from "./types";
4
4
 
5
5
  const SPEC = SYNCED_LISTS.webhooks;
6
6
 
7
- /** A `webhook({...})` call with this id, even when some values are code. */
7
+ /** The `webhook({...})` with this id in this env, even when some values are code. */
8
8
  export const locateWebhook = ({
9
9
  pull,
10
10
  id,
11
+ envKey,
11
12
  }: {
12
13
  pull: PullFiles;
13
14
  id: string;
15
+ envKey: string;
14
16
  }): LocatedFixture | null =>
15
17
  locateFixture({
16
18
  configPath: pull.configPath,
@@ -18,5 +20,11 @@ export const locateWebhook = ({
18
20
  builder: SPEC.builder,
19
21
  idField: SPEC.idField,
20
22
  id,
23
+ where: webhookWhere({ envKey }),
21
24
  allowDynamic: true,
22
25
  });
26
+
27
+ /** Narrows a lookup of one id to the entry in this env. */
28
+ export const webhookWhere = ({ envKey }: { envKey: string }) => [
29
+ { field: SPEC.envField, equals: envKey },
30
+ ];
@@ -1,14 +1,10 @@
1
- import { AutumnApiError } from "../../../generated/client";
2
1
  import type { RemoteWebhook } from "./types";
3
- import { ForeignOrgKeyError, type WebhookPullEnv } from "./webhookPullEnvs";
4
-
5
- const isRejectedKey = (error: unknown): boolean =>
6
- error instanceof AutumnApiError &&
7
- (error.status === 401 || error.status === 403);
2
+ import { webhookEnvSkipWarning } from "./webhookEnvSkipWarning";
3
+ import type { WebhookPullEnv } from "./webhookPullEnvs";
8
4
 
9
5
  /**
10
- * Lists every env in parallel. A rejected key, or one for another org, skips
11
- * its env with one warning; any other failure fails the pull.
6
+ * Lists every env in parallel. A rejected key, one for another org, or a 404
7
+ * (a deleted sandbox) skips its env with one warning; anything else fails the pull.
12
8
  */
13
9
  export const readWebhookEnvs = async ({
14
10
  envs,
@@ -27,10 +23,9 @@ export const readWebhookEnvs = async ({
27
23
  ]);
28
24
  return { envKey, list };
29
25
  } catch (error) {
30
- if (error instanceof ForeignOrgKeyError)
31
- return `⚠ webhooks: skipped ${env.label} (${env.keyName} belongs to another org)`;
32
- if (!isRejectedKey(error)) throw error;
33
- return `⚠ webhooks: skipped ${env.label} (${env.keyName} was rejected)`;
26
+ const warning = webhookEnvSkipWarning({ env, error });
27
+ if (warning === undefined) throw error;
28
+ return warning;
34
29
  }
35
30
  }),
36
31
  );
@@ -2,10 +2,11 @@ import type { ListWebhooksResponse } from "../../../generated/client";
2
2
 
3
3
  export type RemoteWebhook = ListWebhooksResponse["list"][number];
4
4
 
5
- /** A config webhook as evaluated: `url` still holds every env's value. */
5
+ /** A config webhook as evaluated: one endpoint in one env. */
6
6
  export type StatedWebhook = Record<string, unknown> & {
7
7
  id: string;
8
- url?: Record<string, unknown>;
8
+ env?: unknown;
9
+ url?: unknown;
9
10
  };
10
11
 
11
12
  /** The in-memory sources a pull edits, plus where the config lives. */
@@ -1,7 +1,7 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { SYNCED_LISTS } from "../../../generated/emit";
3
3
  import { patchFixturePaths } from "../../../surgery/patchFixturePaths";
4
- import { locateWebhook } from "./locateWebhook";
4
+ import { locateWebhook, webhookWhere } from "./locateWebhook";
5
5
  import type {
6
6
  PullFiles,
7
7
  RemoteWebhook,
@@ -25,8 +25,11 @@ const fieldAssignments = ({
25
25
  stated: StatedWebhook;
26
26
  }): Assignment[] => {
27
27
  const assignments: Assignment[] = [];
28
- // `[]` is a dashboard webhook receiving every event: not a list a config can state.
29
- if (webhook.events.length > 0 && !sameEvents(stated.events, webhook.events))
28
+ // No list is every event, which a config states by leaving `events` out.
29
+ if (webhook.events.length === 0) {
30
+ if (stated.events !== undefined)
31
+ assignments.push({ path: ["events"], text: null });
32
+ } else if (!sameEvents(stated.events, webhook.events))
30
33
  assignments.push({
31
34
  path: ["events"],
32
35
  text: `[${webhook.events.map((event) => JSON.stringify(event)).join(", ")}]`,
@@ -44,8 +47,8 @@ const fieldAssignments = ({
44
47
  return assignments;
45
48
  };
46
49
 
47
- /** Rule 3's wording: what the server holds against what the code evaluates to. */
48
- export const nonLiteralUrlWarning = ({
50
+ /** What the server holds against what the code evaluates to. */
51
+ const nonLiteralUrlWarning = ({
49
52
  id,
50
53
  envKey,
51
54
  server,
@@ -53,22 +56,21 @@ export const nonLiteralUrlWarning = ({
53
56
  }: {
54
57
  id: string;
55
58
  envKey: string;
56
- server: string | undefined;
59
+ server: string;
57
60
  config: unknown;
58
61
  }): string => {
59
62
  const pad = " ".repeat(id.length + 4);
60
63
  return [
61
- `⚠ ${id} url.${envKey} isn't a plain string, so pull left it untouched`,
62
- `${pad}server: ${server ?? "(not registered)"}`,
64
+ `⚠ ${id} (${envKey}) url isn't a plain string, so pull left it untouched`,
65
+ `${pad}server: ${server}`,
63
66
  `${pad}your config: ${typeof config === "string" ? config : "(no value)"}`,
64
67
  `${pad}Update it by hand, or make it a string and pull will manage it.`,
65
68
  ].join("\n");
66
69
  };
67
70
 
68
71
  /**
69
- * Rules 2 and 3: set or replace `url[envKey]` when it is a string literal (or
70
- * absent), and bring the other fields in line. Code is never rewritten; a
71
- * value that evaluates differently from the server earns a warning instead.
72
+ * Set a string-literal url and bring the other fields in line with the server.
73
+ * Code is never rewritten; a value that evaluates differently earns a warning.
72
74
  */
73
75
  export const updateWebhook = ({
74
76
  pull,
@@ -82,57 +84,50 @@ export const updateWebhook = ({
82
84
  envKey: string;
83
85
  }): WebhookEditResult => {
84
86
  const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
85
- if (webhook.events.length === 0 && Array.isArray(stated.events))
86
- result.warnings.push(
87
- `⚠ ${webhook.id} the dashboard webhook at this url receives every event; your next push narrows it to ${stated.events.join(", ")}`,
88
- );
89
- const located = locateWebhook({ pull, id: webhook.id });
90
- if (located === null) {
91
- result.unlocated.push({
92
- id: webhook.id,
93
- action: "update from the server by hand",
94
- });
95
- return result;
96
- }
97
- const patched = patchFixturePaths({
98
- source: located.source,
99
- builder: SPEC.builder,
100
- idField: SPEC.idField,
101
- id: webhook.id,
102
- assignments: [
103
- { path: ["url", envKey], text: JSON.stringify(webhook.url) },
104
- ...fieldAssignments({ webhook, stated }),
105
- ],
106
- bareKeys: true,
107
- });
108
- if (patched === null) {
87
+ const label = `${webhook.id} (${envKey})`;
88
+ const located = locateWebhook({ pull, id: webhook.id, envKey });
89
+ const patched =
90
+ located === null
91
+ ? null
92
+ : patchFixturePaths({
93
+ source: located.source,
94
+ builder: SPEC.builder,
95
+ idField: SPEC.idField,
96
+ id: webhook.id,
97
+ where: webhookWhere({ envKey }),
98
+ assignments: [
99
+ { path: ["url"], text: JSON.stringify(webhook.url) },
100
+ ...fieldAssignments({ webhook, stated }),
101
+ ],
102
+ bareKeys: true,
103
+ });
104
+ if (located === null || patched === null) {
109
105
  result.unlocated.push({
110
106
  id: webhook.id,
111
- action: "update from the server by hand",
107
+ action: `update the ${envKey} webhook from the server by hand`,
112
108
  });
113
109
  return result;
114
110
  }
115
111
  for (const { path } of patched.skipped) {
116
112
  const field = path.join(".");
117
- if (field !== `url.${envKey}`) {
113
+ if (field !== "url") {
118
114
  result.warnings.push(
119
- `⚠ ${webhook.id} ${field} isn't a plain literal, so pull left it untouched`,
115
+ `⚠ ${label} ${field} isn't a plain literal, so pull left it untouched`,
120
116
  );
121
117
  continue;
122
118
  }
123
- const config = stated.url?.[envKey];
124
- if (config === webhook.url) continue;
119
+ if (stated.url === webhook.url) continue;
125
120
  result.warnings.push(
126
121
  nonLiteralUrlWarning({
127
122
  id: webhook.id,
128
123
  envKey,
129
124
  server: webhook.url,
130
- config,
125
+ config: stated.url,
131
126
  }),
132
127
  );
133
128
  }
134
129
  if (patched.source === located.source) return result;
135
130
  pull.files.set(located.file, patched.source);
136
- result.lines.push(`~ webhook ${webhook.id}`);
131
+ result.lines.push(`~ webhook ${label}`);
137
132
  return result;
138
133
  };