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.
- package/dist/bin.js +405 -392
- package/dist/index.js +20 -21
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions/pull/webhooks/appendWebhook.ts +6 -4
- package/src/actions/pull/webhooks/applyWebhooksPull.ts +28 -105
- package/src/actions/pull/webhooks/deleteWebhook.ts +59 -0
- package/src/actions/pull/webhooks/locateWebhook.ts +9 -1
- package/src/actions/pull/webhooks/readWebhookEnvs.ts +7 -12
- package/src/actions/pull/webhooks/types.ts +3 -2
- package/src/actions/pull/webhooks/updateWebhook.ts +37 -42
- package/src/actions/pull/webhooks/webhookEnvSkipWarning.ts +29 -0
- package/src/actions/pull/webhooks/webhookPullEnvs.ts +5 -5
- package/src/actions/pull.ts +1 -1
- package/src/actions/push.ts +30 -12
- package/src/actions/webhooks/applyWebhooks.ts +41 -10
- package/src/actions/webhooks/previewWebhookEnvs.ts +213 -0
- package/src/actions/webhooks/resolveWebhooksForEnv.ts +5 -16
- package/src/actions/webhooks/types/webhookEnv.ts +1 -3
- package/src/actions/webhooks/webhookEnvFailures.ts +27 -0
- package/src/actions/webhooks/webhookSecretName.ts +10 -9
- package/src/actions/webhooks/writeWebhookSecrets.ts +5 -2
- package/src/auth/runOAuthFlow.ts +1 -1
- package/src/cli.ts +23 -25
- package/src/generated/apiRoutes.ts +43 -12
- package/src/generated/client.ts +10 -10
- package/src/generated/emit.ts +4 -4
- package/src/generated/emitRuntime.ts +3 -3
- package/src/generated/lintRules.ts +16 -20
- package/src/generated/lintRuntime.ts +10 -0
- package/src/generated/skills.ts +10 -10
- package/src/generated/webhooks.ts +7 -9
- package/src/generated/wire.ts +0 -1
- package/src/render/renderPreview.ts +25 -9
- package/src/render/renderWebhooks.ts +9 -3
- package/src/actions/pull/webhooks/removeWebhookEnv.ts +0 -141
- package/src/actions/webhooks/isDashboardWebhook.ts +0 -9
- package/src/actions/webhooks/newWebhookId.ts +0 -4
- package/src/actions/webhooks/previewWebhooks.ts +0 -40
- package/src/actions/webhooks/resolveWebhookEnv.ts +0 -34
package/dist/bin.js
CHANGED
|
@@ -985,7 +985,7 @@ var API_ROUTES = [
|
|
|
985
985
|
name: "proration_behavior",
|
|
986
986
|
type: "string",
|
|
987
987
|
required: false,
|
|
988
|
-
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
|
|
988
|
+
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
989
989
|
},
|
|
990
990
|
{
|
|
991
991
|
name: "redirect_mode",
|
|
@@ -1192,7 +1192,7 @@ var API_ROUTES = [
|
|
|
1192
1192
|
name: "billing_behavior",
|
|
1193
1193
|
type: "string",
|
|
1194
1194
|
required: false,
|
|
1195
|
-
description: "Whether to prorate the immediate phase. 'none' skips proration charges and credits."
|
|
1195
|
+
description: "Whether to prorate the immediate phase. 'none' skips proration charges and credits, 'bill_difference' charges/credits the full-period price difference."
|
|
1196
1196
|
},
|
|
1197
1197
|
{
|
|
1198
1198
|
name: "no_billing_changes",
|
|
@@ -1291,7 +1291,7 @@ var API_ROUTES = [
|
|
|
1291
1291
|
name: "billing_behavior",
|
|
1292
1292
|
type: "string",
|
|
1293
1293
|
required: false,
|
|
1294
|
-
description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything."
|
|
1294
|
+
description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
1295
1295
|
},
|
|
1296
1296
|
{
|
|
1297
1297
|
name: "billing_cycle_anchor",
|
|
@@ -1400,7 +1400,7 @@ var API_ROUTES = [
|
|
|
1400
1400
|
name: "proration_behavior",
|
|
1401
1401
|
type: "string",
|
|
1402
1402
|
required: false,
|
|
1403
|
-
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
|
|
1403
|
+
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
1404
1404
|
},
|
|
1405
1405
|
{
|
|
1406
1406
|
name: "redirect_mode",
|
|
@@ -1601,7 +1601,7 @@ var API_ROUTES = [
|
|
|
1601
1601
|
name: "billing_behavior",
|
|
1602
1602
|
type: "string",
|
|
1603
1603
|
required: false,
|
|
1604
|
-
description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything."
|
|
1604
|
+
description: "How to handle billing. 'prorate_immediately' charges/credits prorated amounts now, 'none' does not charge/credit anything, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
1605
1605
|
},
|
|
1606
1606
|
{
|
|
1607
1607
|
name: "billing_cycle_anchor",
|
|
@@ -1710,7 +1710,7 @@ var API_ROUTES = [
|
|
|
1710
1710
|
name: "proration_behavior",
|
|
1711
1711
|
type: "string",
|
|
1712
1712
|
required: false,
|
|
1713
|
-
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
|
|
1713
|
+
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
1714
1714
|
},
|
|
1715
1715
|
{
|
|
1716
1716
|
name: "redirect_mode",
|
|
@@ -1851,7 +1851,7 @@ var API_ROUTES = [
|
|
|
1851
1851
|
name: "proration_behavior",
|
|
1852
1852
|
type: "string",
|
|
1853
1853
|
required: false,
|
|
1854
|
-
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
|
|
1854
|
+
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
1855
1855
|
},
|
|
1856
1856
|
{
|
|
1857
1857
|
name: "redirect_mode",
|
|
@@ -2091,7 +2091,7 @@ var API_ROUTES = [
|
|
|
2091
2091
|
name: "proration_behavior",
|
|
2092
2092
|
type: "string",
|
|
2093
2093
|
required: false,
|
|
2094
|
-
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges."
|
|
2094
|
+
description: "How to handle proration when updating an existing subscription. 'prorate_immediately' charges/credits prorated amounts now, 'none' skips creating any charges, 'bill_difference' charges/credits the full-period price difference now without changing the billing cycle."
|
|
2095
2095
|
},
|
|
2096
2096
|
{
|
|
2097
2097
|
name: "subscription_id",
|
|
@@ -2209,6 +2209,33 @@ var API_ROUTES = [
|
|
|
2209
2209
|
}
|
|
2210
2210
|
]
|
|
2211
2211
|
},
|
|
2212
|
+
{
|
|
2213
|
+
group: "billing",
|
|
2214
|
+
method: "verify",
|
|
2215
|
+
path: "/v1/billing.verify",
|
|
2216
|
+
description: "Checks a customer's Stripe subscriptions against Autumn's record of their plans and reports any drift. Read-only: it never changes Autumn or Stripe.",
|
|
2217
|
+
body: "object",
|
|
2218
|
+
fields: [
|
|
2219
|
+
{
|
|
2220
|
+
name: "customer_id",
|
|
2221
|
+
type: "string",
|
|
2222
|
+
required: true,
|
|
2223
|
+
description: "Autumn customer whose Stripe subscriptions should be checked against Autumn's customer_products."
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
name: "subscription_ids",
|
|
2227
|
+
type: "json",
|
|
2228
|
+
required: false,
|
|
2229
|
+
description: "Optional whitelist of Stripe subscription IDs to verify. Defaults to every subscription linked to the customer's plans, plus any other active Stripe subscription on the customer, which is reported as not linked to Autumn."
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
name: "strict",
|
|
2233
|
+
type: "boolean",
|
|
2234
|
+
required: false,
|
|
2235
|
+
description: "When true, report missing usage-based items and unexpected metered Stripe items. Defaults to false."
|
|
2236
|
+
}
|
|
2237
|
+
]
|
|
2238
|
+
},
|
|
2212
2239
|
{
|
|
2213
2240
|
group: "billing",
|
|
2214
2241
|
method: "import",
|
|
@@ -2698,7 +2725,7 @@ var API_ROUTES = [
|
|
|
2698
2725
|
group: "balances",
|
|
2699
2726
|
method: "batch_track",
|
|
2700
2727
|
path: "/v1/balances.batch_track",
|
|
2701
|
-
description: "Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 202 immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 202 and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.",
|
|
2728
|
+
description: "Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 200 (or 202) immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 200 (or 202) and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.",
|
|
2702
2729
|
body: "array",
|
|
2703
2730
|
fields: []
|
|
2704
2731
|
},
|
|
@@ -3876,8 +3903,8 @@ var API_ROUTES = [
|
|
|
3876
3903
|
{
|
|
3877
3904
|
name: "events",
|
|
3878
3905
|
type: "json",
|
|
3879
|
-
required:
|
|
3880
|
-
description: "The events sent to this webhook.
|
|
3906
|
+
required: false,
|
|
3907
|
+
description: "The events sent to this webhook. Leave it out to send every event. `vercel.*` events can't be mixed with other events."
|
|
3881
3908
|
},
|
|
3882
3909
|
{
|
|
3883
3910
|
name: "description",
|
|
@@ -3939,7 +3966,7 @@ var API_ROUTES = [
|
|
|
3939
3966
|
name: "events",
|
|
3940
3967
|
type: "json",
|
|
3941
3968
|
required: false,
|
|
3942
|
-
description: "The events sent to this webhook.
|
|
3969
|
+
description: "The events sent to this webhook. Pass `[]` to send every event; omit it to keep the current list. `vercel.*` events can't be mixed with other events."
|
|
3943
3970
|
},
|
|
3944
3971
|
{
|
|
3945
3972
|
name: "description",
|
|
@@ -6218,28 +6245,38 @@ var LINT_RULES = {
|
|
|
6218
6245
|
webhooks: {
|
|
6219
6246
|
label: "webhook",
|
|
6220
6247
|
idField: "id",
|
|
6221
|
-
required: ["
|
|
6248
|
+
required: ["env", "id", "url"],
|
|
6222
6249
|
fields: {
|
|
6223
6250
|
id: {
|
|
6224
6251
|
minLength: 1,
|
|
6225
6252
|
maxLength: 256,
|
|
6226
6253
|
pattern: "^[a-zA-Z0-9_-]+$"
|
|
6227
6254
|
},
|
|
6228
|
-
|
|
6229
|
-
|
|
6255
|
+
env: {
|
|
6256
|
+
pattern: "^[a-z0-9_-]+$"
|
|
6257
|
+
},
|
|
6258
|
+
url: {
|
|
6259
|
+
pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
|
|
6230
6260
|
}
|
|
6231
6261
|
},
|
|
6232
6262
|
rules: [
|
|
6263
|
+
{
|
|
6264
|
+
kind: "notMap",
|
|
6265
|
+
field: "url",
|
|
6266
|
+
because: 'Each webhook() is one endpoint in one env: split into one webhook() per env, e.g. webhook({ id, env: "live", url }).'
|
|
6267
|
+
},
|
|
6233
6268
|
{
|
|
6234
6269
|
kind: "unique",
|
|
6235
6270
|
field: "id",
|
|
6236
|
-
|
|
6271
|
+
alongside: "env",
|
|
6272
|
+
because: "Two webhooks claiming one id in one env race to define the same endpoint."
|
|
6237
6273
|
},
|
|
6238
6274
|
{
|
|
6239
6275
|
kind: "unique",
|
|
6240
6276
|
field: "id",
|
|
6277
|
+
alongside: "env",
|
|
6241
6278
|
asEnvName: true,
|
|
6242
|
-
because: "Each webhook's signing secret is saved
|
|
6279
|
+
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."
|
|
6243
6280
|
},
|
|
6244
6281
|
{
|
|
6245
6282
|
kind: "rejects",
|
|
@@ -6253,12 +6290,6 @@ var LINT_RULES = {
|
|
|
6253
6290
|
prefix: "vercel.",
|
|
6254
6291
|
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."
|
|
6255
6292
|
},
|
|
6256
|
-
{
|
|
6257
|
-
kind: "nonEmpty",
|
|
6258
|
-
field: "url",
|
|
6259
|
-
warning: true,
|
|
6260
|
-
because: "With no environment key the webhook is registered nowhere. Add `live`, `sandbox` or a sandbox's slug."
|
|
6261
|
-
},
|
|
6262
6293
|
{
|
|
6263
6294
|
kind: "knownValues",
|
|
6264
6295
|
field: "events",
|
|
@@ -6280,14 +6311,6 @@ var LINT_RULES = {
|
|
|
6280
6311
|
because: "isn't known to this atmn version; the server will check it."
|
|
6281
6312
|
}
|
|
6282
6313
|
]
|
|
6283
|
-
},
|
|
6284
|
-
"webhooks.url": {
|
|
6285
|
-
keys: {
|
|
6286
|
-
pattern: "^[a-z0-9_-]+$"
|
|
6287
|
-
},
|
|
6288
|
-
values: {
|
|
6289
|
-
pattern: "^[Hh][Tt][Tt][Pp][Ss]:\\/\\/"
|
|
6290
|
-
}
|
|
6291
6314
|
}
|
|
6292
6315
|
};
|
|
6293
6316
|
|
|
@@ -6508,6 +6531,10 @@ var entryRuleFailures = ({
|
|
|
6508
6531
|
return [];
|
|
6509
6532
|
return value.filter((item) => typeof item === "string" && !rule.values.includes(item)).map((item) => `\`${item}\` ${rule.because}`);
|
|
6510
6533
|
}
|
|
6534
|
+
case "notMap": {
|
|
6535
|
+
const value = entry[rule.field];
|
|
6536
|
+
return isEntry(value) ? [`${rule.field} is a map. ${rule.because}`] : [];
|
|
6537
|
+
}
|
|
6511
6538
|
case "uniformPrefix": {
|
|
6512
6539
|
const value = entry[rule.field];
|
|
6513
6540
|
if (!Array.isArray(value))
|
|
@@ -6998,8 +7025,7 @@ var CATALOG_HINTS = hintsOf({
|
|
|
6998
7025
|
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers",
|
|
6999
7026
|
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
|
|
7000
7027
|
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.modelMarkups",
|
|
7001
|
-
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups"
|
|
7002
|
-
"webhooks.url"
|
|
7028
|
+
"plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups"
|
|
7003
7029
|
],
|
|
7004
7030
|
frozenPaths: [
|
|
7005
7031
|
"plans.licenses.metadata",
|
|
@@ -7297,7 +7323,7 @@ var createClient = (options) => ({
|
|
|
7297
7323
|
});
|
|
7298
7324
|
|
|
7299
7325
|
// src/version.ts
|
|
7300
|
-
var version = "2.0.
|
|
7326
|
+
var version = "2.0.43";
|
|
7301
7327
|
|
|
7302
7328
|
// src/http/autumnFetch.ts
|
|
7303
7329
|
var runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.versions.node}`;
|
|
@@ -9435,11 +9461,11 @@ var SYNCED_LISTS = {
|
|
|
9435
9461
|
builder: "webhook",
|
|
9436
9462
|
idField: "id",
|
|
9437
9463
|
responseIdField: "id",
|
|
9438
|
-
keys: ["id", "url", "events", "description", "disabled"],
|
|
9439
|
-
required: ["
|
|
9440
|
-
paths: ["description", "disabled", "events", "id", "url"],
|
|
9464
|
+
keys: ["id", "env", "url", "events", "description", "disabled"],
|
|
9465
|
+
required: ["env", "id", "url"],
|
|
9466
|
+
paths: ["description", "disabled", "env", "events", "id", "url"],
|
|
9441
9467
|
pull: true,
|
|
9442
|
-
|
|
9468
|
+
envField: "env"
|
|
9443
9469
|
}
|
|
9444
9470
|
};
|
|
9445
9471
|
var NESTED_FIXTURES = {
|
|
@@ -9554,16 +9580,16 @@ import { dirname as dirname4, join as join7 } from "node:path";
|
|
|
9554
9580
|
import chalk3 from "chalk";
|
|
9555
9581
|
|
|
9556
9582
|
// src/generated/skills.ts
|
|
9557
|
-
var SKILLS_VERSION = "2.0.
|
|
9583
|
+
var SKILLS_VERSION = "2.0.43";
|
|
9558
9584
|
var SKILLS = [
|
|
9559
9585
|
{
|
|
9560
9586
|
name: "autumn-setup",
|
|
9561
9587
|
description: "First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.",
|
|
9562
|
-
version: "2.0.
|
|
9588
|
+
version: "2.0.43",
|
|
9563
9589
|
markdown: `---
|
|
9564
9590
|
name: autumn-setup
|
|
9565
9591
|
description: First-time Autumn setup — install the atmn CLI, connect to an org (sign in, or keyless with no account), turn the user's pricing into autumn.config.ts, and push it to a sandbox org. Use when the user is new to Autumn, pastes an Autumn setup prompt, or asks to set up Autumn, add billing, monetization, subscriptions, usage-based billing, pricing, or plans. If Autumn is already set up, use autumn-catalog instead.
|
|
9566
|
-
version: 2.0.
|
|
9592
|
+
version: 2.0.43
|
|
9567
9593
|
---
|
|
9568
9594
|
|
|
9569
9595
|
# Setup
|
|
@@ -9721,11 +9747,11 @@ Then stop. Don't keep building, don't tour the dashboard, don't deploy anything.
|
|
|
9721
9747
|
{
|
|
9722
9748
|
name: "autumn-catalog",
|
|
9723
9749
|
description: "Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.",
|
|
9724
|
-
version: "2.0.
|
|
9750
|
+
version: "2.0.43",
|
|
9725
9751
|
markdown: `---
|
|
9726
9752
|
name: autumn-catalog
|
|
9727
9753
|
description: Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.
|
|
9728
|
-
version: 2.0.
|
|
9754
|
+
version: 2.0.43
|
|
9729
9755
|
---
|
|
9730
9756
|
|
|
9731
9757
|
# Catalog
|
|
@@ -13592,7 +13618,7 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
|
|
|
13592
13618
|
},
|
|
13593
13619
|
{
|
|
13594
13620
|
path: "references/atmn.md",
|
|
13595
|
-
contents: "# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog's state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog's state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill's: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan's only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server's stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant's customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn's:\n\n- A variant is an entry in its base row's `variants` array and a license link an entry in the parent row's `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from \"atmn\";\n\nexport const messages = feature({\n featureId: \"messages\",\n name: \"Messages\",\n type: \"metered\",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v2\",\n name: \"Pro Annual\",\n customize: { price: { amount: 250, interval: \"year\" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v1\",\n name: \"Pro Annual\",\n customize: { price: { amount: 200, interval: \"year\" } },\n});\n\nexport const proV2 = plan({\n planId: \"pro\",\n versionSlug: \"v2\",\n active: true,\n name: \"Pro\",\n price: { amount: 25, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: \"month\" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: \"prod_…\", // written back by push --yes / pull\n planId: \"pro\",\n versionSlug: \"v1\",\n active: false,\n name: \"Pro\",\n price: { amount: 20, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: \"month\" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: \"month\" },\n price: {\n amount: 0.9,\n billingMethod: \"usage_based\",\n billingUnits: 1000,\n interval: \"month\",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server's catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org's catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages, keyed by `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\n`url` is a map keyed by environment, never one string. A missing key means \"don't register this webhook there\", which is not an error:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nexport default atmn({\n webhooks: [\n webhook({\n id: \"billing\",\n events: [\"billing.updated\", \"balances.limit_reached\"],\n url: {\n live: \"https://myapp.com/api/autumn\", // atmn push -p\n sandbox: \"https://staging.myapp.com/api/autumn\", // the default sandbox\n \"qa-team\": \"https://qa.myapp.com/api/autumn\", // atmn sandbox use qa-team\n },\n }),\n ],\n});\n```\n\n- Keys: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). Never copy a sandbox URL into `live`: ask the user for the production URL, or leave `live` out.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work. `events` needs at least one entry.\n- `id` is permanent: changing it registers a new webhook. Two ids that differ only by case or `-`/`_` are refused.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook's signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<ORG4>_SECRET` in `.env.local` (or `.env`) for a sandbox. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target; a key that's rejected or belongs to another org skips its env with a warning and leaves that env's keys alone. Per env it adds webhooks it finds, sets a changed URL, removes the env's key when the server no longer has that webhook, and deletes a webhook whose map ends up empty. A URL written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server. Webhooks already in the dashboard will be pulled and be editable in the config once you pull as usual. Since the dashboard doesn't let you set an ID, a permanent ID will be assigned to this for future reference.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don't have an account.` — hand that to the setup flow, don't ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything."
|
|
13621
|
+
contents: "# atmn catalog flows\n\nContents: commands · when to use it · the config is the catalog's state (versions, renames, drafts) · variants and licenses · config shapes · splitting the config · update loop · pull · webhooks · sandboxes and keys · what to show the user.\n\nUse `atmn` when a project has or should have an `autumn.config.ts` source of truth.\n\nCommands — these two, not `atmn preview` (that does not exist):\n\n```sh\natmn push # lints, then previews the diff; applies nothing\natmn push --yes # apply exactly what the preview showed\n```\n\n`push` without `--yes` is always a dry run, on a clean org too, and it never asks anything: `--yes` is the only gate. The commands that do ask (`init`, `login --keyless`, `sandbox use`, `skills install`) print the flag to pass instead whenever there is no TTY, which is every agent run; `--headless` forces that in a terminal.\n\n## When to use it\n\n- New project: run `atmn init`. It connects (sign in, or keyless), places the config, pulls whatever the org already holds, and installs these skills beside it.\n- Existing project: if `autumn.config.ts` exists, run `atmn pull` first so every row carries its server ids, then edit and push. Never rewrite a config that predates you from scratch.\n- Use MCP/API directly when the user wants dashboard/API-first changes or there is no local config workflow.\n\n## The config is the catalog's state\n\nThe document is the whole desired catalog for every collection it states. A plan or feature missing from a stated `plans` / `features` is a deletion (archived when customers depend on it). A collection left out entirely is not managed. That rule reaches down to versions: **a version row missing from `plans` is deleted too**.\n\nEvery version of a plan is a row in `plans`. Rows of one plan share a `planId`; each names its version with `versionSlug`; exactly one is `active: true` and the rest `active: false`. There is no history collection and no timeline — array order means nothing. What a version *is* (a group of customers, not a step in time) is the `autumn-concepts` skill's: read `references/plan.md` in the `autumn-concepts` skill. This file owns how rows express it.\n\nTwo fields make a row addressable:\n\n- `planId` + `versionSlug` — what you write. The pair names exactly one version. `versionSlug` is required on every plan row and every variant row, including a plan's only version; an omitted slug is a lint error, never an implicit `v1`.\n- `internalId` — the server's stable id. It is minted on the first `push --yes` and written back into the fixture; `pull` writes it for every row that lacks one. A fixture carrying it can be renamed (`planId`, `featureId`, `versionSlug`) and the server treats that as a rename, not a delete plus create.\n\n**This is where atmn differs from the API and the dashboard.** Those send one plan entry plus flags — `versioning`, `active`, `propagate` — as `references/catalog-update.md` describes. A config has none of those flags. The intent is read off the rows — which rows exist, which one is active, which carry an `internalId` — and the preview reports what the server derived. If the preview says something other than what you meant, change the rows and preview again; never look for a flag.\n\n| You want | Row edit | Preview shows |\n|---|---|---|\n| Change a version in place (active or history) | edit that row; keep its `versionSlug` and `internalId` | `~ pro@v1`, a migration for its customers |\n| The same change on every version | make the edit on every row of the plan | one `~` per row |\n| A new version; customers stay on the old one | add a row: same `planId`, new `versionSlug`, `active: true`, **no `internalId`**; flip the old row to `active: false` | `+ pro@v2`, and `active: true -> false` on the old row |\n| A draft nobody can buy yet | add the row with `active: false` beside the active one | `+` with no pointer move |\n| Rename the plan id | change `planId` on **every** row of the plan (all carry `internalId`) | a rename, not `-` + `+` |\n| Rename a version | change `versionSlug` on a row that carries `internalId` | `Version slug` rename |\n| Retire a version | remove its row | `-`; refused while customers are on it (it names both exits: migrate them, or archive the whole plan) |\n\nGuards the lint applies before anything is sent: a plan with no active row or two active rows (`mark the one customers can buy active: true and the rest active: false`), a missing `versionSlug`, the same `planId` + `versionSlug` twice. A plan whose only row is `active: false` is refused — a draft needs an active sibling.\n\nNumbers vs slugs: the server numbers versions in creation order and the preview labels rows with that number (`pro@v2`). The config never states numbers, only slugs. Push a `v1` row after `v2` already exists and the server numbers it higher; the slug still says `v1`. Never read a preview number back as the slug.\n\n## Variants and licenses\n\nWhat a variant's customize can change, when a base edit reaches a variant, and how a license link anchors to a child version are catalog-wide: `references/catalog-update.md`. What is atmn's:\n\n- A variant is an entry in its base row's `variants` array and a license link an entry in the parent row's `licenses` array: `variant({...})` and `license({...})` fixtures, inline or imported from their own files, edited in place there. Pull writes new ones in that form. Every entry the config lists is a declared overlay; there is no `propagate` in a config, so a base edit reaches a variant through the entry you write, not a follow flag.\n- Minting a base version means listing the variant entries again under the new base row, each with the new `versionSlug` and no `internalId`. The old entries stay under the old base row. One variant version cannot serve two base rows; the lint names both rows and says to version and relink the variant.\n- To retire a variant, set `archived: true` on its entry. A variant left out of the array is a deletion, refused while customers hold it.\n- Every `license({...})` states `versionSlug` — the lint refuses one without it — because the link is pinned to that child version and a config that names it links the same version in every environment. Pull writes it back. Minting a child version moves no parent; relinking a parent is editing that slug.\n\n## Config shapes\n\n`autumn.config.ts` uses the atmn package types, not raw API JSON. Field names are camelCase: `featureId`, `planId`, `billingMethod`, `billingUnits`, `freeTrial`, `intervalCount`, `versionSlug`. Follow the exported types from the package when editing config. Amounts are plain dollars: $20 is `20`, never `2000`.\n\nBuilders — `feature`, `plan`, `variant`, `license`. Items are plain objects on the plan; there is no `item()` builder and fixtures expose `featureId` / `planId`, never `.id`. A plan with an active `v2`, its `v1` kept for the customers still on it, each with an annual variant:\n\n```ts\nimport { atmn, feature, plan, variant } from \"atmn\";\n\nexport const messages = feature({\n featureId: \"messages\",\n name: \"Messages\",\n type: \"metered\",\n consumable: true,\n});\n\nexport const proAnnualV2 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v2\",\n name: \"Pro Annual\",\n customize: { price: { amount: 250, interval: \"year\" } },\n});\n\nexport const proAnnualV1 = variant({\n variantPlanId: \"pro_annual\",\n versionSlug: \"v1\",\n name: \"Pro Annual\",\n customize: { price: { amount: 200, interval: \"year\" } },\n});\n\nexport const proV2 = plan({\n planId: \"pro\",\n versionSlug: \"v2\",\n active: true,\n name: \"Pro\",\n price: { amount: 25, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 10000, reset: { interval: \"month\" } }],\n variants: [proAnnualV2],\n});\n\nexport const proV1 = plan({\n internalId: \"prod_…\", // written back by push --yes / pull\n planId: \"pro\",\n versionSlug: \"v1\",\n active: false,\n name: \"Pro\",\n price: { amount: 20, interval: \"month\" },\n items: [{ featureId: messages.featureId, included: 5000, reset: { interval: \"month\" } }],\n variants: [proAnnualV1],\n});\n\nexport default atmn({ features: [messages], plans: [proV2, proV1] });\n```\n\nBefore `proV2` existed, `proV1` was the active row; minting `v2` was adding `proV2` without an `internalId` and flipping `proV1` to `active: false`.\n\nUsage-priced item:\n\n```ts\n{\n featureId: messages.featureId,\n included: 10000,\n reset: { interval: \"month\" },\n price: {\n amount: 0.9,\n billingMethod: \"usage_based\",\n billingUnits: 1000,\n interval: \"month\",\n },\n}\n```\n\n## Splitting the config\n\n`atmn init` scaffolds four files: `autumn.config.ts` (the root, `export default atmn({...})`), `features.ts`, `plans.ts`, `rewards.ts`. Every version of every plan lives in the `plans` array — active and history rows side by side. The config is ordinary TypeScript, so a row can be lifted into its own file under any export name and referenced from the array; `pull` follows imports and edits each fixture where it lives, and appends rows it has to add to the imported array. What it cannot do is edit a fixture that is not a plain literal — a spread, a helper call, a `.map()` — and it says so and writes nothing rather than guess.\n\n## Update loop\n\n1. Inspect or create `autumn.config.ts`.\n2. Edit the config to represent the desired catalog.\n3. Run `atmn push` to preview changes.\n4. Show the user the plan diffs, the customer impact, which plans mint a new version, and the draft migrations it would create. If the versioning is not what they meant, change the rows (table above) and preview again.\n5. Rerun `atmn push --yes` to apply the same preview.\n6. Report created/updated/deleted/archived features and plans, and the draft migrations the output lists. `push --yes` also writes `internalId` (and any `versionSlug` the server assigned) back into the fixtures — say so; those edits are expected.\n\nTwo notes push prints that are worth relaying: a plan removed while an id-less plan appears looks like a rename — pull first so the fixture carries its id; and a config still stating a deprecated field (`entityFeatureId`) gets a note naming the replacement.\n\n## Pull\n\n`atmn pull` writes the server's catalog back into the config in place: it flips `active` where the dashboard promoted a version, appends versions the config never mentioned, and backfills `internalId` and `versionSlug`. Run it after anyone touches the dashboard, and before editing a config you did not write. With no config yet, `pull` asks which folder to create it in; headless, it prints the `-c <dir>` hint and stops, so run `atmn init` or pass `-c` instead.\n\n`atmn pull --overwrite` is different: it rewrites `autumn.config.ts` and the `features.ts`, `plans.ts` and `rewards.ts` beside it from the server. It never deletes a file, and it leaves alone any file that does not import the package. It needs `--yes`, and it is the right move only when the config describes a different org than the key — the tell is `Your config no longer matches this org's catalog`. Anywhere else, a plain `pull` is what you want.\n\n## Webhooks\n\n`webhooks` in the config lists the Autumn webhooks it manages. Each `webhook()` is one endpoint in one environment, keyed by `env` and `id`. Unlike the catalog it is never a deletion list: `push` creates or updates the webhooks it states and leaves every other webhook alone (the preview shows them as `unmanaged`).\n\nTo register a webhook in several environments, write one `webhook()` per environment. The same `id` may repeat across environments:\n\n```ts\nimport { atmn, webhook } from \"atmn\";\n\nconst events = [\"billing.updated\", \"balances.limit_reached\"];\n\nexport default atmn({\n webhooks: [\n webhook({ id: \"billing\", env: \"live\", url: \"https://myapp.com/api/autumn\", events }), // atmn push -p\n webhook({ id: \"billing\", env: \"sandbox\", url: \"https://staging.myapp.com/api/autumn\", events }), // the default sandbox\n webhook({ id: \"billing\", env: \"qa-team\", url: \"https://qa.myapp.com/api/autumn\", events }), // the sandbox named qa-team\n ],\n});\n```\n\n- `env`: `live` for `-p`, `sandbox` for the default sandbox, or a named sandbox's slug (its name lowercased; no spaces). An environment with no entry has nothing registered. Never copy a sandbox URL into a `live` entry: ask the user for the production URL, or leave `live` out.\n- One `id` may appear once per `env`; a repeat in the same env is refused.\n- Configs from atmn 2.0.36–2.0.42 stated `url` as a map (`url: { live, sandbox }`). That shape is refused: split it into one `webhook()` per env.\n- Every URL must be https and publicly reachable; localhost and private networks are refused, tunnels such as ngrok work.\n- Leave `events` out to receive every event. Don't write `events: []`.\n- Vercel webhooks require their own distinct webhook and are a mutually exclusive variant to the rest. It can only listen to Vercel events and cannot later be changed to listen to non-Vercel events.\n- `id` is permanent: changing it registers a new webhook. In one env, two ids that differ only by case or `-`/`_` are refused.\n- A webhook made in the dashboard has no id of its own, so its entry uses the `ep_…` id `pull` writes. Keep that id: push updates that endpoint by it.\n- `push` syncs webhooks to every sandbox with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), each through its own key, whichever sandbox is pinned. The preview shows one `Webhooks · <env>` block per env with changes. A key that's rejected or belongs to another org skips its env with a warning. The catalog and settings still go to the targeted env only.\n- Production webhooks change only with `-p`, which syncs `live` and no sandbox. When `AUTUMN_PROD_SECRET_KEY` is set, a plain push also previews `live` read-only, and if production would change it prints `Production webhooks differ from your config (<ids>). Run atmn push -p to update production.` Relay that line, and run `-p` only when the user asks.\n- The preview gives a changed URL its own line (`billing url: old → new`). Read it out to the user before `--yes`.\n- `push --yes` writes each new webhook's signing secret once and prints exactly where: `AUTUMN_WEBHOOK_<ID>_SECRET` in `.env.prod` for `-p`, `AUTUMN_WEBHOOK_<ID>_<SANDBOX_SLUG>_SECRET` in `.env.local` (or `.env`) for each sandbox (`AUTUMN_WEBHOOK_BILLING_SANDBOX_SECRET` for the default sandbox, `AUTUMN_WEBHOOK_BILLING_QA_TEAM_SECRET` for `qa-team`). Secrets saved earlier keep their existing names. If two sandboxes' names differ only by punctuation (`qa-team`, `qa_team`), they would share a variable, so push refuses before writing; ask the user to rename one. Relay the variable name and file; never print or read the secret.\n- `pull` reads webhooks from every env with a key in your env files (`AUTUMN_SECRET_KEY` → `sandbox`, `AUTUMN_PROD_SECRET_KEY` → `live`, each `AUTUMN_SANDBOX_<id>_SECRET_KEY` → its slug), whatever the target. A key that's rejected, belongs to another org, or whose sandbox is gone skips its env with a warning, and that env's entries stay. For each env it read, pull writes one entry per endpoint: it adds new ones (dashboard ones under their `ep_…` id), updates changed fields, and deletes an entry the server no longer has in that env. A value written as code (`process.env.X`) is never rewritten; pull warns when it differs from the server.\n\n## Sandboxes and keys\n\n- `atmn sandbox use <name>` pins a named sandbox; every command after it targets that sandbox until `atmn sandbox use --clear`.\n- `atmn reset --yes` empties the pinned sandbox; `atmn push --yes` rebuilds it from the config.\n- `atmn env --json` says which org, sandbox and key a command would hit, with `notes` on what to do when something is off.\n- A missing key fails fast and names the fix: `AUTUMN_SECRET_KEY is not set. Run atmn login, or atmn login --keyless if you don't have an account.` — hand that to the setup flow, don't ask the user to paste a key.\n\n## What to show the user\n\n- Which plans mint a new version, and the draft migrations that come with them.\n- Feature/plan/version deletions that will archive instead because dependencies or customers exist.\n- Which sandbox is pinned before applying anything."
|
|
13596
13622
|
},
|
|
13597
13623
|
{
|
|
13598
13624
|
path: "references/catalog-update.md",
|
|
@@ -13607,11 +13633,11 @@ Pitch: "Pro $20/mo or $200/yr — 1,000 messages per month either way."
|
|
|
13607
13633
|
{
|
|
13608
13634
|
name: "autumn-integrate",
|
|
13609
13635
|
description: "Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.",
|
|
13610
|
-
version: "2.0.
|
|
13636
|
+
version: "2.0.43",
|
|
13611
13637
|
markdown: `---
|
|
13612
13638
|
name: autumn-integrate
|
|
13613
13639
|
description: Integrating Autumn into an app's backend — creating customers, billing flows (attach, checkout, upgrade, cancel), gating features with check, recording usage with track, and exposing billing data to the frontend. Use when the user asks to integrate Autumn, add billing/payments to their app, gate features, meter usage, or build checkout/upgrade flows. Requires a pushed catalog; use autumn-catalog first if plans don't exist yet.
|
|
13614
|
-
version: 2.0.
|
|
13640
|
+
version: 2.0.43
|
|
13615
13641
|
---
|
|
13616
13642
|
|
|
13617
13643
|
# Integrate
|
|
@@ -18213,11 +18239,11 @@ Monitor your logs for this message. If you see it, check [status.useautumn.com](
|
|
|
18213
18239
|
{
|
|
18214
18240
|
name: "autumn-concepts",
|
|
18215
18241
|
description: "Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.",
|
|
18216
|
-
version: "2.0.
|
|
18242
|
+
version: "2.0.43",
|
|
18217
18243
|
markdown: `---
|
|
18218
18244
|
name: autumn-concepts
|
|
18219
18245
|
description: Understanding Autumn's billing objects before acting on them — what a plan version is and what active means, when a trial starts or ends billing, how entity- and seat-scoped subscriptions differ from the customer's own, what a customize diff actually changes, and how caps, overage, and top-ups behave. Load before versioning, editing, or drafting a plan, or when a customer's state does not match the plain reading of their plan.
|
|
18220
|
-
version: 2.0.
|
|
18246
|
+
version: 2.0.43
|
|
18221
18247
|
---
|
|
18222
18248
|
|
|
18223
18249
|
# Concepts
|
|
@@ -19405,7 +19431,7 @@ var runOAuthFlow = async ({
|
|
|
19405
19431
|
if (outcome)
|
|
19406
19432
|
return outcome;
|
|
19407
19433
|
}
|
|
19408
|
-
throw new Error(`All OAuth callback ports (${OAUTH_PORTS
|
|
19434
|
+
throw new Error(`All OAuth callback ports (${OAUTH_PORTS[0]}-${OAUTH_PORTS[OAUTH_PORTS.length - 1]}) are in use.`);
|
|
19409
19435
|
};
|
|
19410
19436
|
|
|
19411
19437
|
// src/actions/login.ts
|
|
@@ -22469,65 +22495,6 @@ var rewriteConfig = ({
|
|
|
22469
22495
|
return { rewritten: [configPath, ...rewritten], kept };
|
|
22470
22496
|
};
|
|
22471
22497
|
|
|
22472
|
-
// src/actions/webhooks/isDashboardWebhook.ts
|
|
22473
|
-
var SVIX_ENDPOINT_ID = /^ep_[0-9A-Za-z]{27}$/;
|
|
22474
|
-
var isDashboardWebhook = ({ id }) => SVIX_ENDPOINT_ID.test(id);
|
|
22475
|
-
|
|
22476
|
-
// ../ksuid/src/base62.ts
|
|
22477
|
-
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
22478
|
-
var base62Encode = ({
|
|
22479
|
-
bytes,
|
|
22480
|
-
padTo
|
|
22481
|
-
}) => {
|
|
22482
|
-
const digits = [];
|
|
22483
|
-
const working = new Uint8Array(bytes);
|
|
22484
|
-
let allZero = false;
|
|
22485
|
-
while (!allZero) {
|
|
22486
|
-
allZero = true;
|
|
22487
|
-
let remainder = 0;
|
|
22488
|
-
for (let i = 0;i < working.length; i++) {
|
|
22489
|
-
const accumulator = remainder * 256 + working[i];
|
|
22490
|
-
working[i] = Math.floor(accumulator / 62);
|
|
22491
|
-
remainder = accumulator % 62;
|
|
22492
|
-
if (working[i] !== 0)
|
|
22493
|
-
allZero = false;
|
|
22494
|
-
}
|
|
22495
|
-
digits.push(remainder);
|
|
22496
|
-
}
|
|
22497
|
-
digits.reverse();
|
|
22498
|
-
while (digits.length < padTo) {
|
|
22499
|
-
digits.unshift(0);
|
|
22500
|
-
}
|
|
22501
|
-
return digits.map((d) => ALPHABET[d]).join("");
|
|
22502
|
-
};
|
|
22503
|
-
|
|
22504
|
-
// ../ksuid/src/random.ts
|
|
22505
|
-
var getRandomBytes = ({ size }) => {
|
|
22506
|
-
const buffer = new Uint8Array(size);
|
|
22507
|
-
globalThis.crypto.getRandomValues(buffer);
|
|
22508
|
-
return buffer;
|
|
22509
|
-
};
|
|
22510
|
-
|
|
22511
|
-
// ../ksuid/src/index.ts
|
|
22512
|
-
var KSUID_EPOCH = 1400000000;
|
|
22513
|
-
var PAYLOAD_BYTES = 20;
|
|
22514
|
-
var ENCODED_LENGTH = 27;
|
|
22515
|
-
var generateKsuid = ({ prefix = "" } = {}) => {
|
|
22516
|
-
const payload = new Uint8Array(PAYLOAD_BYTES);
|
|
22517
|
-
const timestamp = Math.floor(Date.now() / 1000) - KSUID_EPOCH;
|
|
22518
|
-
payload[0] = timestamp >>> 24 & 255;
|
|
22519
|
-
payload[1] = timestamp >>> 16 & 255;
|
|
22520
|
-
payload[2] = timestamp >>> 8 & 255;
|
|
22521
|
-
payload[3] = timestamp & 255;
|
|
22522
|
-
const randomBytes = getRandomBytes({ size: 16 });
|
|
22523
|
-
payload.set(randomBytes, 4);
|
|
22524
|
-
const encoded = base62Encode({ bytes: payload, padTo: ENCODED_LENGTH });
|
|
22525
|
-
return `${prefix}${encoded}`;
|
|
22526
|
-
};
|
|
22527
|
-
|
|
22528
|
-
// src/actions/webhooks/newWebhookId.ts
|
|
22529
|
-
var newWebhookId = () => generateKsuid({ prefix: "wh_" });
|
|
22530
|
-
|
|
22531
22498
|
// src/actions/pull/webhooks/appendWebhook.ts
|
|
22532
22499
|
var SPEC = SYNCED_LISTS.webhooks;
|
|
22533
22500
|
var COLLECTION = "webhooks";
|
|
@@ -22536,8 +22503,9 @@ var fixtureRowOf = ({
|
|
|
22536
22503
|
envKey
|
|
22537
22504
|
}) => ({
|
|
22538
22505
|
id: webhook.id,
|
|
22539
|
-
|
|
22540
|
-
|
|
22506
|
+
[SPEC.envField]: envKey,
|
|
22507
|
+
url: webhook.url,
|
|
22508
|
+
...webhook.events.length > 0 ? { events: webhook.events } : {},
|
|
22541
22509
|
...webhook.description ? { description: webhook.description } : {},
|
|
22542
22510
|
...webhook.disabled ? { disabled: true } : {}
|
|
22543
22511
|
});
|
|
@@ -22587,26 +22555,77 @@ var appendWebhook = ({
|
|
|
22587
22555
|
var SPEC2 = SYNCED_LISTS.webhooks;
|
|
22588
22556
|
var locateWebhook = ({
|
|
22589
22557
|
pull,
|
|
22590
|
-
id
|
|
22558
|
+
id,
|
|
22559
|
+
envKey
|
|
22591
22560
|
}) => locateFixture({
|
|
22592
22561
|
configPath: pull.configPath,
|
|
22593
22562
|
files: pull.files,
|
|
22594
22563
|
builder: SPEC2.builder,
|
|
22595
22564
|
idField: SPEC2.idField,
|
|
22596
22565
|
id,
|
|
22566
|
+
where: webhookWhere({ envKey }),
|
|
22597
22567
|
allowDynamic: true
|
|
22598
22568
|
});
|
|
22569
|
+
var webhookWhere = ({ envKey }) => [
|
|
22570
|
+
{ field: SPEC2.envField, equals: envKey }
|
|
22571
|
+
];
|
|
22572
|
+
|
|
22573
|
+
// src/actions/pull/webhooks/deleteWebhook.ts
|
|
22574
|
+
var SPEC3 = SYNCED_LISTS.webhooks;
|
|
22575
|
+
var deleteWebhook = ({
|
|
22576
|
+
pull,
|
|
22577
|
+
stated: stated2,
|
|
22578
|
+
envKey
|
|
22579
|
+
}) => {
|
|
22580
|
+
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22581
|
+
const located = locateWebhook({ pull, id: stated2.id, envKey });
|
|
22582
|
+
const removed = located === null ? null : deleteFixtureLiteral({
|
|
22583
|
+
source: located.source,
|
|
22584
|
+
builder: SPEC3.builder,
|
|
22585
|
+
idField: SPEC3.idField,
|
|
22586
|
+
id: stated2.id,
|
|
22587
|
+
where: webhookWhere({ envKey })
|
|
22588
|
+
});
|
|
22589
|
+
if (located === null || removed === null) {
|
|
22590
|
+
result.unlocated.push({
|
|
22591
|
+
id: stated2.id,
|
|
22592
|
+
action: `delete the ${envKey} webhook by hand`
|
|
22593
|
+
});
|
|
22594
|
+
return result;
|
|
22595
|
+
}
|
|
22596
|
+
pull.files.set(located.file, removed.source);
|
|
22597
|
+
if (removed.exportedName !== undefined) {
|
|
22598
|
+
const referenceFiles = new Set([pull.configPath]);
|
|
22599
|
+
const target = resolveCollectionTarget({
|
|
22600
|
+
configPath: pull.configPath,
|
|
22601
|
+
files: pull.files,
|
|
22602
|
+
collection: "webhooks"
|
|
22603
|
+
});
|
|
22604
|
+
if (target !== null)
|
|
22605
|
+
referenceFiles.add(target.file);
|
|
22606
|
+
for (const file of referenceFiles)
|
|
22607
|
+
pull.files.set(file, deleteReference({
|
|
22608
|
+
source: pull.files.get(file) ?? "",
|
|
22609
|
+
name: removed.exportedName
|
|
22610
|
+
}));
|
|
22611
|
+
}
|
|
22612
|
+
result.lines.push(`- webhook ${stated2.id} (${envKey})`);
|
|
22613
|
+
return result;
|
|
22614
|
+
};
|
|
22599
22615
|
|
|
22600
22616
|
// src/actions/pull/webhooks/updateWebhook.ts
|
|
22601
22617
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
22602
|
-
var
|
|
22618
|
+
var SPEC4 = SYNCED_LISTS.webhooks;
|
|
22603
22619
|
var sameEvents = (left, right) => Array.isArray(left) && isDeepStrictEqual2([...left].sort(), [...right].sort());
|
|
22604
22620
|
var fieldAssignments = ({
|
|
22605
22621
|
webhook,
|
|
22606
22622
|
stated: stated2
|
|
22607
22623
|
}) => {
|
|
22608
22624
|
const assignments = [];
|
|
22609
|
-
if (webhook.events.length
|
|
22625
|
+
if (webhook.events.length === 0) {
|
|
22626
|
+
if (stated2.events !== undefined)
|
|
22627
|
+
assignments.push({ path: ["events"], text: null });
|
|
22628
|
+
} else if (!sameEvents(stated2.events, webhook.events))
|
|
22610
22629
|
assignments.push({
|
|
22611
22630
|
path: ["events"],
|
|
22612
22631
|
text: `[${webhook.events.map((event) => JSON.stringify(event)).join(", ")}]`
|
|
@@ -22631,8 +22650,8 @@ var nonLiteralUrlWarning = ({
|
|
|
22631
22650
|
}) => {
|
|
22632
22651
|
const pad = " ".repeat(id.length + 4);
|
|
22633
22652
|
return [
|
|
22634
|
-
`⚠ ${id}
|
|
22635
|
-
`${pad}server: ${server
|
|
22653
|
+
`⚠ ${id} (${envKey}) url isn't a plain string, so pull left it untouched`,
|
|
22654
|
+
`${pad}server: ${server}`,
|
|
22636
22655
|
`${pad}your config: ${typeof config === "string" ? config : "(no value)"}`,
|
|
22637
22656
|
`${pad}Update it by hand, or make it a string and pull will manage it.`
|
|
22638
22657
|
].join(`
|
|
@@ -22645,164 +22664,58 @@ var updateWebhook = ({
|
|
|
22645
22664
|
envKey
|
|
22646
22665
|
}) => {
|
|
22647
22666
|
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
const
|
|
22651
|
-
if (located === null) {
|
|
22652
|
-
result.unlocated.push({
|
|
22653
|
-
id: webhook.id,
|
|
22654
|
-
action: "update from the server by hand"
|
|
22655
|
-
});
|
|
22656
|
-
return result;
|
|
22657
|
-
}
|
|
22658
|
-
const patched = patchFixturePaths({
|
|
22667
|
+
const label = `${webhook.id} (${envKey})`;
|
|
22668
|
+
const located = locateWebhook({ pull, id: webhook.id, envKey });
|
|
22669
|
+
const patched = located === null ? null : patchFixturePaths({
|
|
22659
22670
|
source: located.source,
|
|
22660
|
-
builder:
|
|
22661
|
-
idField:
|
|
22671
|
+
builder: SPEC4.builder,
|
|
22672
|
+
idField: SPEC4.idField,
|
|
22662
22673
|
id: webhook.id,
|
|
22674
|
+
where: webhookWhere({ envKey }),
|
|
22663
22675
|
assignments: [
|
|
22664
|
-
{ path: ["url"
|
|
22676
|
+
{ path: ["url"], text: JSON.stringify(webhook.url) },
|
|
22665
22677
|
...fieldAssignments({ webhook, stated: stated2 })
|
|
22666
22678
|
],
|
|
22667
22679
|
bareKeys: true
|
|
22668
22680
|
});
|
|
22669
|
-
if (patched === null) {
|
|
22681
|
+
if (located === null || patched === null) {
|
|
22670
22682
|
result.unlocated.push({
|
|
22671
22683
|
id: webhook.id,
|
|
22672
|
-
action:
|
|
22684
|
+
action: `update the ${envKey} webhook from the server by hand`
|
|
22673
22685
|
});
|
|
22674
22686
|
return result;
|
|
22675
22687
|
}
|
|
22676
22688
|
for (const { path } of patched.skipped) {
|
|
22677
22689
|
const field = path.join(".");
|
|
22678
|
-
if (field !==
|
|
22679
|
-
result.warnings.push(`⚠ ${
|
|
22690
|
+
if (field !== "url") {
|
|
22691
|
+
result.warnings.push(`⚠ ${label} ${field} isn't a plain literal, so pull left it untouched`);
|
|
22680
22692
|
continue;
|
|
22681
22693
|
}
|
|
22682
|
-
|
|
22683
|
-
if (config === webhook.url)
|
|
22694
|
+
if (stated2.url === webhook.url)
|
|
22684
22695
|
continue;
|
|
22685
22696
|
result.warnings.push(nonLiteralUrlWarning({
|
|
22686
22697
|
id: webhook.id,
|
|
22687
22698
|
envKey,
|
|
22688
22699
|
server: webhook.url,
|
|
22689
|
-
config
|
|
22700
|
+
config: stated2.url
|
|
22690
22701
|
}));
|
|
22691
22702
|
}
|
|
22692
22703
|
if (patched.source === located.source)
|
|
22693
22704
|
return result;
|
|
22694
22705
|
pull.files.set(located.file, patched.source);
|
|
22695
|
-
result.lines.push(`~ webhook ${
|
|
22696
|
-
return result;
|
|
22697
|
-
};
|
|
22698
|
-
|
|
22699
|
-
// src/actions/pull/webhooks/removeWebhookEnv.ts
|
|
22700
|
-
var SPEC4 = SYNCED_LISTS.webhooks;
|
|
22701
|
-
var urlMapIsEmpty = ({
|
|
22702
|
-
pull,
|
|
22703
|
-
id
|
|
22704
|
-
}) => {
|
|
22705
|
-
const located = locateWebhook({ pull, id });
|
|
22706
|
-
const object = located === null ? null : fixtureObjectOf(located.node);
|
|
22707
|
-
const member = object?.namedChildren().find((child) => child.kind() === "pair" && child.field("key") !== null && fixtureKeyOf({ node: child.field("key") }) === "url");
|
|
22708
|
-
const value = member?.field("value");
|
|
22709
|
-
if (value === null || value === undefined)
|
|
22710
|
-
return false;
|
|
22711
|
-
const read = readFixtureValue({ node: value });
|
|
22712
|
-
return typeof read === "object" && read !== null && Object.keys(read).length === 0;
|
|
22713
|
-
};
|
|
22714
|
-
var deleteWebhook = ({
|
|
22715
|
-
pull,
|
|
22716
|
-
id
|
|
22717
|
-
}) => {
|
|
22718
|
-
const located = locateWebhook({ pull, id });
|
|
22719
|
-
if (located === null)
|
|
22720
|
-
return false;
|
|
22721
|
-
const removed = deleteFixtureLiteral({
|
|
22722
|
-
source: located.source,
|
|
22723
|
-
builder: SPEC4.builder,
|
|
22724
|
-
idField: SPEC4.idField,
|
|
22725
|
-
id
|
|
22726
|
-
});
|
|
22727
|
-
if (removed === null)
|
|
22728
|
-
return false;
|
|
22729
|
-
pull.files.set(located.file, removed.source);
|
|
22730
|
-
if (removed.exportedName !== undefined) {
|
|
22731
|
-
const referenceFiles = new Set([pull.configPath]);
|
|
22732
|
-
const target = resolveCollectionTarget({
|
|
22733
|
-
configPath: pull.configPath,
|
|
22734
|
-
files: pull.files,
|
|
22735
|
-
collection: "webhooks"
|
|
22736
|
-
});
|
|
22737
|
-
if (target !== null)
|
|
22738
|
-
referenceFiles.add(target.file);
|
|
22739
|
-
for (const file of referenceFiles)
|
|
22740
|
-
pull.files.set(file, deleteReference({
|
|
22741
|
-
source: pull.files.get(file) ?? "",
|
|
22742
|
-
name: removed.exportedName
|
|
22743
|
-
}));
|
|
22744
|
-
}
|
|
22745
|
-
return true;
|
|
22746
|
-
};
|
|
22747
|
-
var removeWebhookEnv = ({
|
|
22748
|
-
pull,
|
|
22749
|
-
stated: stated2,
|
|
22750
|
-
envKey
|
|
22751
|
-
}) => {
|
|
22752
|
-
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22753
|
-
const located = locateWebhook({ pull, id: stated2.id });
|
|
22754
|
-
const patched = located === null ? null : patchFixturePaths({
|
|
22755
|
-
source: located.source,
|
|
22756
|
-
builder: SPEC4.builder,
|
|
22757
|
-
idField: SPEC4.idField,
|
|
22758
|
-
id: stated2.id,
|
|
22759
|
-
assignments: [{ path: ["url", envKey], text: null }]
|
|
22760
|
-
});
|
|
22761
|
-
if (located === null || patched === null) {
|
|
22762
|
-
result.unlocated.push({
|
|
22763
|
-
id: stated2.id,
|
|
22764
|
-
action: `remove url.${envKey} by hand`
|
|
22765
|
-
});
|
|
22766
|
-
return result;
|
|
22767
|
-
}
|
|
22768
|
-
if (patched.skipped.length > 0) {
|
|
22769
|
-
result.warnings.push(nonLiteralUrlWarning({
|
|
22770
|
-
id: stated2.id,
|
|
22771
|
-
envKey,
|
|
22772
|
-
server: undefined,
|
|
22773
|
-
config: stated2.url?.[envKey]
|
|
22774
|
-
}));
|
|
22775
|
-
return result;
|
|
22776
|
-
}
|
|
22777
|
-
if (patched.source === located.source)
|
|
22778
|
-
return result;
|
|
22779
|
-
pull.files.set(located.file, patched.source);
|
|
22780
|
-
if (urlMapIsEmpty({ pull, id: stated2.id })) {
|
|
22781
|
-
if (deleteWebhook({ pull, id: stated2.id })) {
|
|
22782
|
-
result.lines.push(`- webhook ${stated2.id}`);
|
|
22783
|
-
return result;
|
|
22784
|
-
}
|
|
22785
|
-
result.unlocated.push({
|
|
22786
|
-
id: stated2.id,
|
|
22787
|
-
action: "delete the webhook by hand: its url map is now empty"
|
|
22788
|
-
});
|
|
22789
|
-
return result;
|
|
22790
|
-
}
|
|
22791
|
-
result.lines.push(`- webhook ${stated2.id} url.${envKey}`);
|
|
22706
|
+
result.lines.push(`~ webhook ${label}`);
|
|
22792
22707
|
return result;
|
|
22793
22708
|
};
|
|
22794
22709
|
|
|
22795
22710
|
// src/actions/pull/webhooks/applyWebhooksPull.ts
|
|
22796
|
-
var isVercelWebhook = (events) => Array.isArray(events) && events.length > 0 && events.every((event) => typeof event === "string" && event.startsWith("vercel."));
|
|
22797
22711
|
var statedFrom = ({
|
|
22798
|
-
row,
|
|
22799
22712
|
webhook,
|
|
22800
22713
|
envKey
|
|
22801
22714
|
}) => ({
|
|
22802
|
-
...row,
|
|
22803
22715
|
id: webhook.id,
|
|
22804
|
-
|
|
22805
|
-
|
|
22716
|
+
env: envKey,
|
|
22717
|
+
url: webhook.url,
|
|
22718
|
+
events: webhook.events.length > 0 ? webhook.events : undefined,
|
|
22806
22719
|
description: webhook.description,
|
|
22807
22720
|
disabled: webhook.disabled
|
|
22808
22721
|
});
|
|
@@ -22815,77 +22728,37 @@ var applyWebhooksPull = ({
|
|
|
22815
22728
|
pull,
|
|
22816
22729
|
remote,
|
|
22817
22730
|
stated: stated2,
|
|
22818
|
-
envKey
|
|
22819
|
-
newId = newWebhookId
|
|
22731
|
+
envKey
|
|
22820
22732
|
}) => {
|
|
22821
22733
|
const result = { lines: [], warnings: [], unlocated: [] };
|
|
22822
|
-
const
|
|
22823
|
-
const
|
|
22824
|
-
const
|
|
22825
|
-
const present = new Set;
|
|
22734
|
+
const rows = stated2 ?? [];
|
|
22735
|
+
const inEnv = new Map(rows.filter((row) => row.env === envKey).map((row) => [row.id, row]));
|
|
22736
|
+
const next = rows.filter((row) => row.env !== envKey);
|
|
22826
22737
|
for (const webhook of remote) {
|
|
22827
|
-
|
|
22828
|
-
const sameApp = (row2) => isVercelWebhook(row2.events) === isVercelWebhook(webhook.events);
|
|
22829
|
-
const represented = (stated2 ?? []).find((row2) => row2.url?.[envKey] === webhook.url && sameApp(row2)) ?? (stated2 ?? []).find((row2) => row2.url?.[envKey] === undefined && Object.values(row2.url ?? {}).includes(webhook.url) && sameApp(row2));
|
|
22830
|
-
if (represented !== undefined) {
|
|
22831
|
-
present.add(represented.id);
|
|
22832
|
-
next.set(represented.id, statedFrom({
|
|
22833
|
-
row: represented,
|
|
22834
|
-
webhook: { ...webhook, id: represented.id },
|
|
22835
|
-
envKey
|
|
22836
|
-
}));
|
|
22837
|
-
merge(result, updateWebhook({
|
|
22838
|
-
pull,
|
|
22839
|
-
webhook: { ...webhook, id: represented.id },
|
|
22840
|
-
stated: represented,
|
|
22841
|
-
envKey
|
|
22842
|
-
}));
|
|
22843
|
-
continue;
|
|
22844
|
-
}
|
|
22845
|
-
if (webhook.events.length === 0) {
|
|
22846
|
-
result.lines.push(`· webhook ${webhook.id} receives every event; give it an event list in the dashboard, or add it to your config, to manage it here`);
|
|
22847
|
-
continue;
|
|
22848
|
-
}
|
|
22849
|
-
const id = newId();
|
|
22850
|
-
const failure2 = appendWebhook({
|
|
22851
|
-
pull,
|
|
22852
|
-
webhook: { ...webhook, id },
|
|
22853
|
-
envKey
|
|
22854
|
-
});
|
|
22855
|
-
if (failure2 === null) {
|
|
22856
|
-
next.set(id, statedFrom({ row: undefined, webhook: { ...webhook, id }, envKey }));
|
|
22857
|
-
result.lines.push(`+ webhook ${id} (made in the dashboard; push adopts it by URL)`);
|
|
22858
|
-
} else
|
|
22859
|
-
result.unlocated.push({ id, action: failure2 });
|
|
22860
|
-
continue;
|
|
22861
|
-
}
|
|
22862
|
-
present.add(webhook.id);
|
|
22863
|
-
const row = statedById.get(webhook.id);
|
|
22864
|
-
next.set(webhook.id, statedFrom({ row, webhook, envKey }));
|
|
22738
|
+
const row = inEnv.get(webhook.id);
|
|
22865
22739
|
if (row !== undefined) {
|
|
22866
22740
|
merge(result, updateWebhook({ pull, webhook, stated: row, envKey }));
|
|
22741
|
+
next.push(statedFrom({ webhook, envKey }));
|
|
22867
22742
|
continue;
|
|
22868
22743
|
}
|
|
22869
22744
|
const failure = appendWebhook({ pull, webhook, envKey });
|
|
22870
|
-
if (failure
|
|
22871
|
-
result.lines.push(`+ webhook ${webhook.id}`);
|
|
22872
|
-
else
|
|
22745
|
+
if (failure !== null) {
|
|
22873
22746
|
result.unlocated.push({ id: webhook.id, action: failure });
|
|
22747
|
+
continue;
|
|
22748
|
+
}
|
|
22749
|
+
result.lines.push(`+ webhook ${webhook.id} (${envKey})`);
|
|
22750
|
+
next.push(statedFrom({ webhook, envKey }));
|
|
22874
22751
|
}
|
|
22875
|
-
|
|
22876
|
-
|
|
22752
|
+
const remoteIds = new Set(remote.map(({ id }) => id));
|
|
22753
|
+
for (const row of inEnv.values()) {
|
|
22754
|
+
if (remoteIds.has(row.id))
|
|
22877
22755
|
continue;
|
|
22878
|
-
const removed =
|
|
22756
|
+
const removed = deleteWebhook({ pull, stated: row, envKey });
|
|
22879
22757
|
merge(result, removed);
|
|
22880
22758
|
if (removed.lines.length === 0)
|
|
22881
|
-
|
|
22882
|
-
const { [envKey]: _removed, ...url } = row.url;
|
|
22883
|
-
if (Object.keys(url).length === 0)
|
|
22884
|
-
next.delete(row.id);
|
|
22885
|
-
else
|
|
22886
|
-
next.set(row.id, { ...row, url });
|
|
22759
|
+
next.push(row);
|
|
22887
22760
|
}
|
|
22888
|
-
return { ...result, stated:
|
|
22761
|
+
return { ...result, stated: next };
|
|
22889
22762
|
};
|
|
22890
22763
|
|
|
22891
22764
|
// src/generated/sandboxName.ts
|
|
@@ -22953,6 +22826,7 @@ var webhookPullEnvs = ({
|
|
|
22953
22826
|
{
|
|
22954
22827
|
keyName,
|
|
22955
22828
|
label,
|
|
22829
|
+
secretKey,
|
|
22956
22830
|
envKey,
|
|
22957
22831
|
listWebhooks: () => listWebhooks({ secretKey })
|
|
22958
22832
|
}
|
|
@@ -22960,8 +22834,28 @@ var webhookPullEnvs = ({
|
|
|
22960
22834
|
});
|
|
22961
22835
|
};
|
|
22962
22836
|
|
|
22837
|
+
// src/actions/pull/webhooks/webhookEnvSkipWarning.ts
|
|
22838
|
+
var skipReason = (error) => {
|
|
22839
|
+
if (error instanceof ForeignOrgKeyError)
|
|
22840
|
+
return "belongs to another org";
|
|
22841
|
+
if (!(error instanceof AutumnApiError))
|
|
22842
|
+
return null;
|
|
22843
|
+
if (error.status === 401 || error.status === 403)
|
|
22844
|
+
return "was rejected";
|
|
22845
|
+
if (error.status !== 404)
|
|
22846
|
+
return null;
|
|
22847
|
+
const { message } = error.body ?? {};
|
|
22848
|
+
return `failed: ${typeof message === "string" ? message : error.status}`;
|
|
22849
|
+
};
|
|
22850
|
+
var webhookEnvSkipWarning = ({
|
|
22851
|
+
env,
|
|
22852
|
+
error
|
|
22853
|
+
}) => {
|
|
22854
|
+
const reason = skipReason(error);
|
|
22855
|
+
return reason === null ? undefined : `⚠ webhooks: skipped ${env.label} (${env.keyName} ${reason})`;
|
|
22856
|
+
};
|
|
22857
|
+
|
|
22963
22858
|
// src/actions/pull/webhooks/readWebhookEnvs.ts
|
|
22964
|
-
var isRejectedKey = (error) => error instanceof AutumnApiError && (error.status === 401 || error.status === 403);
|
|
22965
22859
|
var readWebhookEnvs = async ({
|
|
22966
22860
|
envs
|
|
22967
22861
|
}) => {
|
|
@@ -22973,11 +22867,10 @@ var readWebhookEnvs = async ({
|
|
|
22973
22867
|
]);
|
|
22974
22868
|
return { envKey, list: list2 };
|
|
22975
22869
|
} catch (error) {
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
if (!isRejectedKey(error))
|
|
22870
|
+
const warning = webhookEnvSkipWarning({ env, error });
|
|
22871
|
+
if (warning === undefined)
|
|
22979
22872
|
throw error;
|
|
22980
|
-
return
|
|
22873
|
+
return warning;
|
|
22981
22874
|
}
|
|
22982
22875
|
}));
|
|
22983
22876
|
return {
|
|
@@ -23021,7 +22914,7 @@ var webhooksHaveWork = ({
|
|
|
23021
22914
|
webhooks
|
|
23022
22915
|
}) => (webhooks?.changes ?? []).some((change) => change.action !== "unmanaged");
|
|
23023
22916
|
var DETAIL_INDENT = " ";
|
|
23024
|
-
var list2 = (values) => values.length === 0 ? "
|
|
22917
|
+
var list2 = (values) => values.length === 0 ? "every event" : values.join(", ");
|
|
23025
22918
|
var updateDetails = ({
|
|
23026
22919
|
id,
|
|
23027
22920
|
before,
|
|
@@ -23074,7 +22967,8 @@ var renderWebhooks = ({
|
|
|
23074
22967
|
if (rows.length === 0)
|
|
23075
22968
|
return null;
|
|
23076
22969
|
const count = webhooks.changes.filter((change) => change.action !== "unmanaged").length;
|
|
23077
|
-
|
|
22970
|
+
const heading = webhooks.env === undefined ? `Webhooks (${count})` : `Webhooks · ${webhooks.env} (${count})`;
|
|
22971
|
+
return [chalk4.bold(heading), ...rows].join(`
|
|
23078
22972
|
`);
|
|
23079
22973
|
};
|
|
23080
22974
|
|
|
@@ -23501,8 +23395,10 @@ var renderPreview = ({
|
|
|
23501
23395
|
const referralPrograms = (preview.referralPrograms ?? []).filter(rowHasWork);
|
|
23502
23396
|
const migrations = preview.migrations ?? [];
|
|
23503
23397
|
const settings = settingChanges(preview.settings);
|
|
23504
|
-
const webhooks = preview.webhooks
|
|
23505
|
-
|
|
23398
|
+
const webhooks = (preview.webhooks ?? []).map((lane) => renderWebhooks({ webhooks: lane })).filter((block) => block !== null);
|
|
23399
|
+
const productionWebhooks = preview.productionWebhooks ?? [];
|
|
23400
|
+
const productionHint = productionWebhooks.length > 0 ? chalk5.yellow(`Production webhooks differ from your config (${productionWebhooks.join(", ")}). Run atmn push -p to update production.`) : preview.productionWebhooksUnchecked === true ? chalk5.yellow("Production webhooks weren't checked, so this push can't say they match your config.") : null;
|
|
23401
|
+
if (features.length === 0 && plans.length === 0 && rewards.length === 0 && referralPrograms.length === 0 && settings.length === 0 && webhooks.length === 0 && productionHint === null) {
|
|
23506
23402
|
return chalk5.dim("No changes. Your catalog matches your config.");
|
|
23507
23403
|
}
|
|
23508
23404
|
const sections = [];
|
|
@@ -23571,8 +23467,9 @@ var renderPreview = ({
|
|
|
23571
23467
|
].join(`
|
|
23572
23468
|
`));
|
|
23573
23469
|
}
|
|
23574
|
-
|
|
23575
|
-
|
|
23470
|
+
sections.push(...webhooks);
|
|
23471
|
+
if (productionHint !== null)
|
|
23472
|
+
sections.push(productionHint);
|
|
23576
23473
|
if (migrations.length > 0) {
|
|
23577
23474
|
sections.push(renderPlannedMigrations({ migrations, plans }));
|
|
23578
23475
|
}
|
|
@@ -23582,7 +23479,7 @@ var renderPreview = ({
|
|
|
23582
23479
|
};
|
|
23583
23480
|
var previewIsEmpty = ({
|
|
23584
23481
|
preview
|
|
23585
|
-
}) => !(preview.features ?? []).some(rowHasWork) && !(preview.plans ?? []).some(rowHasWork) && !(preview.rewards ?? []).some(rowHasWork) && !(preview.referralPrograms ?? []).some(rowHasWork) && !settingsHaveWork({ settings: preview.settings }) && !webhooksHaveWork({ webhooks
|
|
23482
|
+
}) => !(preview.features ?? []).some(rowHasWork) && !(preview.plans ?? []).some(rowHasWork) && !(preview.rewards ?? []).some(rowHasWork) && !(preview.referralPrograms ?? []).some(rowHasWork) && !settingsHaveWork({ settings: preview.settings }) && !(preview.webhooks ?? []).some((webhooks) => webhooksHaveWork({ webhooks }));
|
|
23586
23483
|
|
|
23587
23484
|
// src/actions/push/backfillInternalIds.ts
|
|
23588
23485
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
@@ -24038,18 +23935,35 @@ var withScopeHint = ({
|
|
|
24038
23935
|
hint: hint2
|
|
24039
23936
|
}) => error instanceof AutumnApiError && error.message.includes(SCOPE_REFUSAL) ? new Error(hint2, { cause: error }) : error;
|
|
24040
23937
|
|
|
23938
|
+
// src/actions/webhooks/webhookEnvFailures.ts
|
|
23939
|
+
var throwWebhookEnvFailures = ({
|
|
23940
|
+
failures,
|
|
23941
|
+
envCount,
|
|
23942
|
+
warnings = []
|
|
23943
|
+
}) => {
|
|
23944
|
+
if (failures.length === 0)
|
|
23945
|
+
return;
|
|
23946
|
+
throw new Error([
|
|
23947
|
+
...failures.map(({ env, message }) => envCount > 1 ? message.replace(/^/gm, `${env}: `) : message),
|
|
23948
|
+
...warnings
|
|
23949
|
+
].join(`
|
|
23950
|
+
`));
|
|
23951
|
+
};
|
|
23952
|
+
var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
|
|
23953
|
+
|
|
24041
23954
|
// src/actions/webhooks/writeWebhookSecrets.ts
|
|
24042
23955
|
import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
24043
23956
|
import { dirname as dirname11, join as join13, relative as relative6 } from "node:path";
|
|
24044
23957
|
|
|
24045
23958
|
// src/actions/webhooks/webhookSecretName.ts
|
|
24046
23959
|
var envNameSegment = (value) => value.toUpperCase().replace(/-/g, "_");
|
|
23960
|
+
var envKeySegment = (envKey) => envKey.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
24047
23961
|
var webhookSecretName = ({
|
|
24048
23962
|
id,
|
|
24049
|
-
|
|
23963
|
+
envKey
|
|
24050
23964
|
}) => {
|
|
24051
|
-
const
|
|
24052
|
-
return `AUTUMN_WEBHOOK_${envNameSegment(id)}${
|
|
23965
|
+
const suffix = envKey === undefined ? "" : `_${envKeySegment(envKey)}`;
|
|
23966
|
+
return `AUTUMN_WEBHOOK_${envNameSegment(id)}${suffix}_SECRET`;
|
|
24053
23967
|
};
|
|
24054
23968
|
|
|
24055
23969
|
// src/actions/webhooks/writeWebhookSecrets.ts
|
|
@@ -24066,8 +23980,11 @@ var writeWebhookSecrets = async ({
|
|
|
24066
23980
|
}) => {
|
|
24067
23981
|
if (secrets.length === 0)
|
|
24068
23982
|
return [];
|
|
24069
|
-
const
|
|
24070
|
-
const values = Object.fromEntries(secrets.map(({ id, secret }) => [
|
|
23983
|
+
const envKey = env.live ? undefined : env.key;
|
|
23984
|
+
const values = Object.fromEntries(secrets.map(({ id, secret }) => [
|
|
23985
|
+
webhookSecretName({ id, envKey }),
|
|
23986
|
+
secret
|
|
23987
|
+
]));
|
|
24071
23988
|
let path;
|
|
24072
23989
|
if (env.live) {
|
|
24073
23990
|
path = prodEnvPath({ envDirs });
|
|
@@ -24081,16 +23998,13 @@ var writeWebhookSecrets = async ({
|
|
|
24081
23998
|
};
|
|
24082
23999
|
|
|
24083
24000
|
// src/actions/webhooks/applyWebhooks.ts
|
|
24084
|
-
var
|
|
24085
|
-
client,
|
|
24001
|
+
var applyLane = async ({
|
|
24086
24002
|
lane,
|
|
24087
24003
|
envDirs,
|
|
24088
24004
|
cwd,
|
|
24089
24005
|
write
|
|
24090
24006
|
}) => {
|
|
24091
|
-
|
|
24092
|
-
return;
|
|
24093
|
-
const result = await client.syncWebhooks(lane.body);
|
|
24007
|
+
const result = await lane.client.syncWebhooks(lane.body);
|
|
24094
24008
|
const saved = await writeWebhookSecrets({
|
|
24095
24009
|
secrets: result.secrets,
|
|
24096
24010
|
env: lane.env,
|
|
@@ -24103,54 +24017,168 @@ var applyWebhooks = async ({
|
|
|
24103
24017
|
]);
|
|
24104
24018
|
const adopted = lane.preview.changes.filter((change) => change.action === "adopt" && !failed.has(change.id)).map((change) => `Adopted ${change.id} (existing dashboard webhook); its signing secret is unchanged, nothing written`);
|
|
24105
24019
|
write(`
|
|
24106
|
-
Applied webhooks.
|
|
24020
|
+
Applied webhooks to ${lane.env.key}.
|
|
24107
24021
|
${[...adopted, ...saved].map((line2) => `${line2}
|
|
24108
24022
|
`).join("")}`);
|
|
24109
24023
|
if (result.errors.length > 0)
|
|
24110
24024
|
throw new Error(result.errors.map(({ id, message }) => `${id}: ${message}`).join(`
|
|
24111
24025
|
`));
|
|
24112
24026
|
};
|
|
24027
|
+
var applyWebhooks = async ({
|
|
24028
|
+
lanes,
|
|
24029
|
+
envDirs,
|
|
24030
|
+
cwd,
|
|
24031
|
+
write
|
|
24032
|
+
}) => {
|
|
24033
|
+
const withWork = lanes.filter((lane) => webhooksHaveWork({ webhooks: lane.preview }));
|
|
24034
|
+
const results = await Promise.allSettled(withWork.map((lane) => applyLane({ lane, envDirs, cwd, write })));
|
|
24035
|
+
const failures = results.flatMap((result, index) => result.status === "rejected" ? [
|
|
24036
|
+
{
|
|
24037
|
+
env: withWork[index]?.env.key ?? "",
|
|
24038
|
+
message: messageOf2(result.reason)
|
|
24039
|
+
}
|
|
24040
|
+
] : []);
|
|
24041
|
+
throwWebhookEnvFailures({ failures, envCount: withWork.length });
|
|
24042
|
+
};
|
|
24113
24043
|
|
|
24114
24044
|
// src/actions/webhooks/resolveWebhooksForEnv.ts
|
|
24115
24045
|
var resolveWebhooksForEnv = ({
|
|
24116
24046
|
rows,
|
|
24117
24047
|
envKey
|
|
24118
24048
|
}) => {
|
|
24119
|
-
const {
|
|
24120
|
-
|
|
24121
|
-
for (const row of rows) {
|
|
24122
|
-
const values = envKeyed.map((field) => row[field]?.[envKey]);
|
|
24123
|
-
if (values.some((value) => typeof value !== "string"))
|
|
24124
|
-
continue;
|
|
24125
|
-
webhooks.push({
|
|
24126
|
-
...row,
|
|
24127
|
-
...Object.fromEntries(envKeyed.map((field, index) => [field, values[index]]))
|
|
24128
|
-
});
|
|
24129
|
-
}
|
|
24130
|
-
return webhooks;
|
|
24049
|
+
const { envField } = SYNCED_LISTS.webhooks;
|
|
24050
|
+
return rows.filter((row) => row[envField] === envKey).map(({ [envField]: _env, ...row }) => row);
|
|
24131
24051
|
};
|
|
24132
24052
|
|
|
24133
|
-
// src/actions/webhooks/
|
|
24134
|
-
var
|
|
24135
|
-
|
|
24053
|
+
// src/actions/webhooks/previewWebhookEnvs.ts
|
|
24054
|
+
var isLive = (env) => env.keyName === "AUTUMN_PROD_SECRET_KEY";
|
|
24055
|
+
var assertDistinctSecretNames = ({
|
|
24056
|
+
lanes
|
|
24057
|
+
}) => {
|
|
24058
|
+
const secretName = ({ lane, id }) => webhookSecretName({
|
|
24059
|
+
id,
|
|
24060
|
+
...lane.env.live ? {} : { envKey: lane.env.key }
|
|
24061
|
+
});
|
|
24062
|
+
const clashes = new Map;
|
|
24063
|
+
for (const lane of lanes) {
|
|
24064
|
+
for (const change of lane.preview.changes) {
|
|
24065
|
+
if (change.action !== "create")
|
|
24066
|
+
continue;
|
|
24067
|
+
const name = secretName({ lane, id: change.id });
|
|
24068
|
+
for (const other of lanes) {
|
|
24069
|
+
if (other === lane)
|
|
24070
|
+
continue;
|
|
24071
|
+
const shares = other.body.webhooks.some((webhook) => secretName({ lane: other, id: webhook.id }) === name);
|
|
24072
|
+
if (!shares)
|
|
24073
|
+
continue;
|
|
24074
|
+
const [first, second] = [other.env.key, lane.env.key].sort();
|
|
24075
|
+
clashes.set(`${first}|${second}|${name}`, `${first} and ${second} would both save ${change.id}'s signing secret as ${name}. Rename one of those sandboxes so their names differ by more than punctuation.`);
|
|
24076
|
+
}
|
|
24077
|
+
}
|
|
24078
|
+
}
|
|
24079
|
+
if (clashes.size > 0)
|
|
24080
|
+
throw new Error([...clashes.values()].join(`
|
|
24081
|
+
`));
|
|
24082
|
+
};
|
|
24083
|
+
var previewEnv = async ({
|
|
24084
|
+
env,
|
|
24136
24085
|
rows,
|
|
24137
|
-
|
|
24086
|
+
clientFor
|
|
24087
|
+
}) => {
|
|
24088
|
+
try {
|
|
24089
|
+
const key = await env.envKey();
|
|
24090
|
+
const webhooks = resolveWebhooksForEnv({ rows, envKey: key });
|
|
24091
|
+
if (webhooks.length === 0)
|
|
24092
|
+
return;
|
|
24093
|
+
const client = clientFor({ secretKey: env.secretKey });
|
|
24094
|
+
const body = { webhooks };
|
|
24095
|
+
const { changes, errors } = await client.previewSyncWebhooks(body);
|
|
24096
|
+
return {
|
|
24097
|
+
lane: {
|
|
24098
|
+
preview: { env: key, changes },
|
|
24099
|
+
body,
|
|
24100
|
+
env: { key, live: isLive(env) },
|
|
24101
|
+
client
|
|
24102
|
+
},
|
|
24103
|
+
errors
|
|
24104
|
+
};
|
|
24105
|
+
} catch (error) {
|
|
24106
|
+
const skipped = webhookEnvSkipWarning({ env, error });
|
|
24107
|
+
if (skipped === undefined)
|
|
24108
|
+
throw error;
|
|
24109
|
+
return { skipped };
|
|
24110
|
+
}
|
|
24111
|
+
};
|
|
24112
|
+
var previewWebhookEnvs = async ({
|
|
24113
|
+
rows,
|
|
24114
|
+
envs,
|
|
24115
|
+
prod,
|
|
24116
|
+
clientFor
|
|
24138
24117
|
}) => {
|
|
24139
24118
|
if (rows === undefined)
|
|
24140
24119
|
return;
|
|
24141
|
-
if (
|
|
24142
|
-
throw new Error("webhooks need the
|
|
24143
|
-
const
|
|
24144
|
-
const
|
|
24145
|
-
|
|
24146
|
-
|
|
24120
|
+
if (envs === undefined || clientFor === undefined)
|
|
24121
|
+
throw new Error("webhooks need the env keys to pick each url.");
|
|
24122
|
+
const all = envs();
|
|
24123
|
+
const synced = all.filter((env) => isLive(env) === prod);
|
|
24124
|
+
const probed = prod ? [] : all.filter(isLive);
|
|
24125
|
+
const [syncedResults, probedResults] = await Promise.all([
|
|
24126
|
+
Promise.allSettled(synced.map((env) => previewEnv({ env, rows, clientFor }))),
|
|
24127
|
+
Promise.allSettled(probed.map((env) => previewEnv({ env, rows, clientFor })))
|
|
24128
|
+
]);
|
|
24129
|
+
const lanes = [];
|
|
24130
|
+
const skipped = [];
|
|
24131
|
+
probedResults.forEach((result, index) => {
|
|
24132
|
+
if (result.status === "rejected")
|
|
24133
|
+
skipped.push(`⚠ webhooks: couldn't check ${probed[index]?.label ?? "live"} for changes (${messageOf2(result.reason)})`);
|
|
24147
24134
|
});
|
|
24148
|
-
const
|
|
24149
|
-
const
|
|
24150
|
-
|
|
24151
|
-
|
|
24152
|
-
|
|
24153
|
-
|
|
24135
|
+
const productionUnchecked = probedResults.some((result) => result.status === "rejected" || result.value !== undefined && ("skipped" in result.value));
|
|
24136
|
+
const failures = [];
|
|
24137
|
+
syncedResults.forEach((result, index) => {
|
|
24138
|
+
const label = synced[index]?.label ?? "";
|
|
24139
|
+
if (result.status === "rejected") {
|
|
24140
|
+
failures.push({ env: label, message: messageOf2(result.reason) });
|
|
24141
|
+
return;
|
|
24142
|
+
}
|
|
24143
|
+
const preview = result.value;
|
|
24144
|
+
if (preview === undefined)
|
|
24145
|
+
return;
|
|
24146
|
+
if ("skipped" in preview) {
|
|
24147
|
+
skipped.push(preview.skipped);
|
|
24148
|
+
return;
|
|
24149
|
+
}
|
|
24150
|
+
lanes.push(preview.lane);
|
|
24151
|
+
if (preview.errors.length > 0)
|
|
24152
|
+
failures.push({
|
|
24153
|
+
env: preview.lane.env.key,
|
|
24154
|
+
message: preview.errors.map(({ id, message }) => `${id}: ${message}`).join(`
|
|
24155
|
+
`)
|
|
24156
|
+
});
|
|
24157
|
+
});
|
|
24158
|
+
throwWebhookEnvFailures({
|
|
24159
|
+
failures,
|
|
24160
|
+
envCount: synced.length,
|
|
24161
|
+
warnings: skipped
|
|
24162
|
+
});
|
|
24163
|
+
assertDistinctSecretNames({ lanes });
|
|
24164
|
+
const productionDiffers = [];
|
|
24165
|
+
for (const result of probedResults) {
|
|
24166
|
+
if (result.status === "rejected")
|
|
24167
|
+
continue;
|
|
24168
|
+
const preview = result.value;
|
|
24169
|
+
if (preview === undefined)
|
|
24170
|
+
continue;
|
|
24171
|
+
if ("skipped" in preview) {
|
|
24172
|
+
skipped.push(preview.skipped);
|
|
24173
|
+
continue;
|
|
24174
|
+
}
|
|
24175
|
+
const changed = preview.lane.preview.changes.filter((change) => change.action !== "unmanaged");
|
|
24176
|
+
productionDiffers.push(...new Set([
|
|
24177
|
+
...changed.map((change) => change.id),
|
|
24178
|
+
...preview.errors.map((error) => error.id)
|
|
24179
|
+
]));
|
|
24180
|
+
}
|
|
24181
|
+
return { lanes, productionDiffers, productionUnchecked, skipped };
|
|
24154
24182
|
};
|
|
24155
24183
|
|
|
24156
24184
|
// src/actions/push.ts
|
|
@@ -24316,7 +24344,9 @@ var runPush = async ({
|
|
|
24316
24344
|
dryRun = false,
|
|
24317
24345
|
write = (text) => process.stdout.write(text),
|
|
24318
24346
|
migrationLinkBase,
|
|
24319
|
-
|
|
24347
|
+
prod = false,
|
|
24348
|
+
webhookEnvs,
|
|
24349
|
+
webhookClientFor
|
|
24320
24350
|
}) => {
|
|
24321
24351
|
const project = resolveProject({ cwd, configFlag });
|
|
24322
24352
|
const dirs = configSearchDirs({ cwd, configPath: configFlag });
|
|
@@ -24339,10 +24369,11 @@ var runPush = async ({
|
|
|
24339
24369
|
const previews = await settleLanes({
|
|
24340
24370
|
settings: previewSettings({ client, body: settingsBody }),
|
|
24341
24371
|
catalog: client.previewUpdate(wire),
|
|
24342
|
-
webhooks:
|
|
24343
|
-
client,
|
|
24372
|
+
webhooks: previewWebhookEnvs({
|
|
24344
24373
|
rows: lists.webhooks,
|
|
24345
|
-
|
|
24374
|
+
envs: webhookEnvs,
|
|
24375
|
+
prod,
|
|
24376
|
+
clientFor: webhookClientFor
|
|
24346
24377
|
})
|
|
24347
24378
|
});
|
|
24348
24379
|
const { settings, webhooks } = previews.values;
|
|
@@ -24354,8 +24385,17 @@ var runPush = async ({
|
|
|
24354
24385
|
const preview = {
|
|
24355
24386
|
...catalogPreview,
|
|
24356
24387
|
settings,
|
|
24357
|
-
...webhooks === undefined ? {} : {
|
|
24388
|
+
...webhooks === undefined ? {} : {
|
|
24389
|
+
webhooks: webhooks.lanes.map((lane) => lane.preview),
|
|
24390
|
+
productionWebhooks: webhooks.productionDiffers,
|
|
24391
|
+
productionWebhooksUnchecked: webhooks.productionUnchecked
|
|
24392
|
+
}
|
|
24358
24393
|
};
|
|
24394
|
+
if (webhooks !== undefined && webhooks.skipped.length > 0)
|
|
24395
|
+
write(`${webhooks.skipped.join(`
|
|
24396
|
+
`)}
|
|
24397
|
+
|
|
24398
|
+
`);
|
|
24359
24399
|
write(`${renderPreview({ preview, migrationLinkBase })}
|
|
24360
24400
|
`);
|
|
24361
24401
|
if (catalogDeferral !== undefined)
|
|
@@ -24392,8 +24432,7 @@ ${renderCatalogDeferral({ error: catalogDeferral.error })}
|
|
|
24392
24432
|
const applied = await settleLanes({
|
|
24393
24433
|
catalog: settled === undefined ? settingsStep().then(catalogStep) : catalogStep(settled),
|
|
24394
24434
|
webhooks: applyWebhooks({
|
|
24395
|
-
|
|
24396
|
-
lane: webhooks,
|
|
24435
|
+
lanes: webhooks?.lanes ?? [],
|
|
24397
24436
|
envDirs: project.envDirs,
|
|
24398
24437
|
cwd,
|
|
24399
24438
|
write
|
|
@@ -25142,28 +25181,6 @@ var runSandboxUse = async ({
|
|
|
25142
25181
|
return result;
|
|
25143
25182
|
};
|
|
25144
25183
|
|
|
25145
|
-
// src/actions/webhooks/resolveWebhookEnv.ts
|
|
25146
|
-
var memoized = (fetchOrgInfo2) => {
|
|
25147
|
-
let pending;
|
|
25148
|
-
return () => {
|
|
25149
|
-
pending ??= fetchOrgInfo2();
|
|
25150
|
-
return pending;
|
|
25151
|
-
};
|
|
25152
|
-
};
|
|
25153
|
-
var resolveWebhookEnv = async ({
|
|
25154
|
-
target,
|
|
25155
|
-
prod,
|
|
25156
|
-
fetchOrgInfo: fetchOrgInfo2
|
|
25157
|
-
}) => {
|
|
25158
|
-
const orgInfo = memoized(fetchOrgInfo2);
|
|
25159
|
-
const orgId = async () => (await orgInfo()).id;
|
|
25160
|
-
if (prod)
|
|
25161
|
-
return { key: "live", live: true, orgId };
|
|
25162
|
-
if (target.sandboxId === undefined)
|
|
25163
|
-
return { key: "sandbox", live: false, orgId };
|
|
25164
|
-
return { key: sandboxSlug((await orgInfo()).name), live: false, orgId };
|
|
25165
|
-
};
|
|
25166
|
-
|
|
25167
25184
|
// src/cli.ts
|
|
25168
25185
|
var normalizeVersionFlag = ({ argv }) => argv.map((token) => token === "-v" ? "--version" : token);
|
|
25169
25186
|
var withTargetFlags = (program) => program.option("-p, --prod", "target production instead of sandbox").option("--sandbox <sandboxId>", "target a specific sandbox").option("-l, --local", "target a local server (default port 8080)").option("--port <port>", "port of a local server (implies --local)").option("-b, --base-url <url>", "send to this URL instead").option("--client-id <id>", "OAuth client id the CLI identifies as").option("-c, --config <path>", "autumn.config.ts, or the folder holding it (default: found from cwd or the root package.json)").option("--headless", "never prompt: print what a command needs and stop (default when not a TTY)");
|
|
@@ -25255,13 +25272,15 @@ var pinnedSandboxName = async ({
|
|
|
25255
25272
|
return {};
|
|
25256
25273
|
}
|
|
25257
25274
|
};
|
|
25258
|
-
var
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25275
|
+
var webhookClientFor = ({ target }) => ({ secretKey }) => createClient({
|
|
25276
|
+
secretKey,
|
|
25277
|
+
fetch: autumnFetch,
|
|
25278
|
+
...target.baseUrl ? { baseUrl: target.baseUrl } : {}
|
|
25279
|
+
});
|
|
25280
|
+
var webhookEnvsFor = ({ target }) => () => webhookPullEnvs({
|
|
25281
|
+
targetKeyName: target.secretKeyName,
|
|
25282
|
+
listWebhooks: ({ secretKey }) => webhookClientFor({ target })({ secretKey }).listWebhooks({}),
|
|
25283
|
+
fetchOrgInfo: ({ secretKey }) => fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey })
|
|
25265
25284
|
});
|
|
25266
25285
|
var mainOrgInfo = ({ target }) => {
|
|
25267
25286
|
const main = managementTarget({ target });
|
|
@@ -25361,7 +25380,9 @@ Linking a keyless org to an account:
|
|
|
25361
25380
|
client: clientFor({ target }),
|
|
25362
25381
|
configPath: configFlagOf({ command }),
|
|
25363
25382
|
dryRun: !apply,
|
|
25364
|
-
|
|
25383
|
+
prod: command.optsWithGlobals().prod === true,
|
|
25384
|
+
webhookEnvs: webhookEnvsFor({ target }),
|
|
25385
|
+
webhookClientFor: webhookClientFor({ target })
|
|
25365
25386
|
});
|
|
25366
25387
|
process.exitCode = pushExitCode({ apply, result });
|
|
25367
25388
|
if (!apply && !previewIsEmpty({ preview: result.preview }))
|
|
@@ -25411,15 +25432,7 @@ Nothing applied. Re-run with --yes to apply these changes.
|
|
|
25411
25432
|
overwrite: options.overwrite === true,
|
|
25412
25433
|
yes: options.yes === true,
|
|
25413
25434
|
prompter: prompterFor({ command }),
|
|
25414
|
-
webhookEnvs: (
|
|
25415
|
-
targetKeyName: target.secretKeyName,
|
|
25416
|
-
listWebhooks: ({ secretKey }) => createClient({
|
|
25417
|
-
secretKey,
|
|
25418
|
-
fetch: autumnFetch,
|
|
25419
|
-
...target.baseUrl ? { baseUrl: target.baseUrl } : {}
|
|
25420
|
-
}).listWebhooks({}),
|
|
25421
|
-
fetchOrgInfo: ({ secretKey }) => fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey })
|
|
25422
|
-
})
|
|
25435
|
+
webhookEnvs: webhookEnvsFor({ target })
|
|
25423
25436
|
});
|
|
25424
25437
|
writeStaleSkillsHint({ command });
|
|
25425
25438
|
});
|