atmn 2.0.42 → 2.0.44

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 (38) hide show
  1. package/dist/bin.js +360 -368
  2. package/dist/index.js +21 -19
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/package.json +2 -2
  5. package/src/actions/pull/webhooks/appendWebhook.ts +4 -3
  6. package/src/actions/pull/webhooks/applyWebhooksPull.ts +27 -98
  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 +32 -36
  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/emit.ts +4 -4
  26. package/src/generated/emitRuntime.ts +3 -3
  27. package/src/generated/lintRules.ts +17 -18
  28. package/src/generated/lintRuntime.ts +10 -0
  29. package/src/generated/skills.ts +10 -10
  30. package/src/generated/webhooks.ts +5 -7
  31. package/src/generated/wire.ts +0 -1
  32. package/src/render/renderPreview.ts +25 -9
  33. package/src/render/renderWebhooks.ts +8 -2
  34. package/src/actions/pull/webhooks/removeWebhookEnv.ts +0 -141
  35. package/src/actions/webhooks/isDashboardWebhook.ts +0 -9
  36. package/src/actions/webhooks/newWebhookId.ts +0 -4
  37. package/src/actions/webhooks/previewWebhooks.ts +0 -40
  38. package/src/actions/webhooks/resolveWebhookEnv.ts +0 -34
package/dist/index.js CHANGED
@@ -2234,25 +2234,38 @@ var LINT_RULES = {
2234
2234
  webhooks: {
2235
2235
  label: "webhook",
2236
2236
  idField: "id",
2237
- required: ["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
+ },
2244
+ env: {
2245
+ pattern: "^[a-z0-9_-]+$"
2246
+ },
2247
+ url: {
2248
+ pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
2243
2249
  }
2244
2250
  },
2245
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
+ },
2246
2257
  {
2247
2258
  kind: "unique",
2248
2259
  field: "id",
2249
- 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."
2250
2262
  },
2251
2263
  {
2252
2264
  kind: "unique",
2253
2265
  field: "id",
2266
+ alongside: "env",
2254
2267
  asEnvName: true,
2255
- 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."
2256
2269
  },
2257
2270
  {
2258
2271
  kind: "rejects",
@@ -2266,12 +2279,6 @@ var LINT_RULES = {
2266
2279
  prefix: "vercel.",
2267
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."
2268
2281
  },
2269
- {
2270
- kind: "nonEmpty",
2271
- field: "url",
2272
- warning: true,
2273
- because: "With no environment key the webhook is registered nowhere. Add `live`, `sandbox` or a sandbox's slug."
2274
- },
2275
2282
  {
2276
2283
  kind: "knownValues",
2277
2284
  field: "events",
@@ -2293,14 +2300,6 @@ var LINT_RULES = {
2293
2300
  because: "isn't known to this atmn version; the server will check it."
2294
2301
  }
2295
2302
  ]
2296
- },
2297
- "webhooks.url": {
2298
- keys: {
2299
- pattern: "^[a-z0-9_-]+$"
2300
- },
2301
- values: {
2302
- pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
2303
- }
2304
2303
  }
2305
2304
  };
2306
2305
 
@@ -2521,6 +2520,10 @@ var entryRuleFailures = ({
2521
2520
  return [];
2522
2521
  return value.filter((item) => typeof item === "string" && !rule.values.includes(item)).map((item) => `\`${item}\` ${rule.because}`);
2523
2522
  }
2523
+ case "notMap": {
2524
+ const value = entry[rule.field];
2525
+ return isEntry(value) ? [`${rule.field} is a map. ${rule.because}`] : [];
2526
+ }
2524
2527
  case "uniformPrefix": {
2525
2528
  const value = entry[rule.field];
2526
2529
  if (!Array.isArray(value))
@@ -3011,8 +3014,7 @@ var CATALOG_HINTS = hintsOf({
3011
3014
  "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers",
3012
3015
  "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
3013
3016
  "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.modelMarkups",
3014
- "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups",
3015
- "webhooks.url"
3017
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups"
3016
3018
  ],
3017
3019
  frozenPaths: [
3018
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","../test/webhooks/webhooksRender.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.42",
3
+ "version": "2.0.44",
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": "5dd16854ac3848701a74f14bddf6fe15a1c19ef6"
52
+ "gitHead": "d55d450eb83ed6ab89420f720e58f06e35a2b52b"
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,14 +22,15 @@ const fixtureRowOf = ({
22
22
  envKey: string;
23
23
  }): Record<string, unknown> => ({
24
24
  id: webhook.id,
25
- url: { [envKey]: webhook.url },
25
+ [SPEC.envField]: envKey,
26
+ url: webhook.url,
26
27
  // No list is every event; `events` is left out rather than written as `[]`.
27
28
  ...(webhook.events.length > 0 ? { events: webhook.events } : {}),
28
29
  ...(webhook.description ? { description: webhook.description } : {}),
29
30
  ...(webhook.disabled ? { disabled: true } : {}),
30
31
  });
31
32
 
32
- /** 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. */
33
34
  export const appendWebhook = ({
34
35
  pull,
35
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,26 +8,17 @@ 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 },
20
+ env: envKey,
21
+ url: webhook.url,
33
22
  events: webhook.events.length > 0 ? webhook.events : undefined,
34
23
  description: webhook.description,
35
24
  disabled: webhook.disabled,
@@ -42,112 +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
- const id = newId();
114
- const failure = appendWebhook({
115
- pull,
116
- webhook: { ...webhook, id },
117
- envKey,
118
- });
119
- if (failure === null) {
120
- next.set(
121
- id,
122
- statedFrom({ row: undefined, webhook: { ...webhook, id }, envKey }),
123
- );
124
- result.lines.push(
125
- `+ webhook ${id} (made in the dashboard; push adopts it by URL)`,
126
- );
127
- } else result.unlocated.push({ id, action: failure });
128
- continue;
129
- }
130
- present.add(webhook.id);
131
- const row = statedById.get(webhook.id);
132
- next.set(webhook.id, statedFrom({ row, webhook, envKey }));
58
+ const row = inEnv.get(webhook.id);
133
59
  if (row !== undefined) {
134
60
  merge(result, updateWebhook({ pull, webhook, stated: row, envKey }));
61
+ next.push(statedFrom({ webhook, envKey }));
135
62
  continue;
136
63
  }
137
64
  const failure = appendWebhook({ pull, webhook, envKey });
138
- if (failure === null) result.lines.push(`+ webhook ${webhook.id}`);
139
- 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 }));
140
71
  }
141
72
 
142
- for (const row of stated ?? []) {
143
- if (present.has(row.id) || row.url?.[envKey] === undefined) continue;
144
- 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 });
145
77
  merge(result, removed);
146
- // A key left in the source (code, or unlocated) is still the config's.
147
- if (removed.lines.length === 0) continue;
148
- const { [envKey]: _removed, ...url } = row.url;
149
- if (Object.keys(url).length === 0) next.delete(row.id);
150
- 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);
151
80
  }
152
- return { ...result, stated: [...next.values()] };
81
+ return { ...result, stated: next };
153
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,
@@ -47,8 +47,8 @@ const fieldAssignments = ({
47
47
  return assignments;
48
48
  };
49
49
 
50
- /** Rule 3's wording: what the server holds against what the code evaluates to. */
51
- export const nonLiteralUrlWarning = ({
50
+ /** What the server holds against what the code evaluates to. */
51
+ const nonLiteralUrlWarning = ({
52
52
  id,
53
53
  envKey,
54
54
  server,
@@ -56,22 +56,21 @@ export const nonLiteralUrlWarning = ({
56
56
  }: {
57
57
  id: string;
58
58
  envKey: string;
59
- server: string | undefined;
59
+ server: string;
60
60
  config: unknown;
61
61
  }): string => {
62
62
  const pad = " ".repeat(id.length + 4);
63
63
  return [
64
- `⚠ ${id} url.${envKey} isn't a plain string, so pull left it untouched`,
65
- `${pad}server: ${server ?? "(not registered)"}`,
64
+ `⚠ ${id} (${envKey}) url isn't a plain string, so pull left it untouched`,
65
+ `${pad}server: ${server}`,
66
66
  `${pad}your config: ${typeof config === "string" ? config : "(no value)"}`,
67
67
  `${pad}Update it by hand, or make it a string and pull will manage it.`,
68
68
  ].join("\n");
69
69
  };
70
70
 
71
71
  /**
72
- * Rules 2 and 3: set or replace `url[envKey]` when it is a string literal (or
73
- * absent), and bring the other fields in line. Code is never rewritten; a
74
- * 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.
75
74
  */
76
75
  export const updateWebhook = ({
77
76
  pull,
@@ -85,53 +84,50 @@ export const updateWebhook = ({
85
84
  envKey: string;
86
85
  }): WebhookEditResult => {
87
86
  const result: WebhookEditResult = { lines: [], warnings: [], unlocated: [] };
88
- const located = locateWebhook({ pull, id: webhook.id });
89
- if (located === 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) {
90
105
  result.unlocated.push({
91
106
  id: webhook.id,
92
- action: "update from the server by hand",
93
- });
94
- return result;
95
- }
96
- const patched = patchFixturePaths({
97
- source: located.source,
98
- builder: SPEC.builder,
99
- idField: SPEC.idField,
100
- id: webhook.id,
101
- assignments: [
102
- { path: ["url", envKey], text: JSON.stringify(webhook.url) },
103
- ...fieldAssignments({ webhook, stated }),
104
- ],
105
- bareKeys: true,
106
- });
107
- if (patched === null) {
108
- result.unlocated.push({
109
- id: webhook.id,
110
- action: "update from the server by hand",
107
+ action: `update the ${envKey} webhook from the server by hand`,
111
108
  });
112
109
  return result;
113
110
  }
114
111
  for (const { path } of patched.skipped) {
115
112
  const field = path.join(".");
116
- if (field !== `url.${envKey}`) {
113
+ if (field !== "url") {
117
114
  result.warnings.push(
118
- `⚠ ${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`,
119
116
  );
120
117
  continue;
121
118
  }
122
- const config = stated.url?.[envKey];
123
- if (config === webhook.url) continue;
119
+ if (stated.url === webhook.url) continue;
124
120
  result.warnings.push(
125
121
  nonLiteralUrlWarning({
126
122
  id: webhook.id,
127
123
  envKey,
128
124
  server: webhook.url,
129
- config,
125
+ config: stated.url,
130
126
  }),
131
127
  );
132
128
  }
133
129
  if (patched.source === located.source) return result;
134
130
  pull.files.set(located.file, patched.source);
135
- result.lines.push(`~ webhook ${webhook.id}`);
131
+ result.lines.push(`~ webhook ${label}`);
136
132
  return result;
137
133
  };
@@ -0,0 +1,29 @@
1
+ import { AutumnApiError } from "../../../generated/client";
2
+ import { ForeignOrgKeyError, type WebhookPullEnv } from "./webhookPullEnvs";
3
+
4
+ /** Why a failure skips its env; null when it should fail the command. */
5
+ const skipReason = (error: unknown): string | null => {
6
+ if (error instanceof ForeignOrgKeyError) return "belongs to another org";
7
+ if (!(error instanceof AutumnApiError)) return null;
8
+ if (error.status === 401 || error.status === 403) return "was rejected";
9
+ if (error.status !== 404) return null;
10
+ const { message } = (error.body ?? {}) as { message?: unknown };
11
+ return `failed: ${typeof message === "string" ? message : error.status}`;
12
+ };
13
+
14
+ /**
15
+ * A rejected key, one for another org, or a 404 (a deleted sandbox) skips its
16
+ * env with this warning; anything else is a real failure.
17
+ */
18
+ export const webhookEnvSkipWarning = ({
19
+ env,
20
+ error,
21
+ }: {
22
+ env: Pick<WebhookPullEnv, "label" | "keyName">;
23
+ error: unknown;
24
+ }): string | undefined => {
25
+ const reason = skipReason(error);
26
+ return reason === null
27
+ ? undefined
28
+ : `⚠ webhooks: skipped ${env.label} (${env.keyName} ${reason})`;
29
+ };