primitive-admin 1.0.61 → 1.0.63
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/README.md +125 -69
- package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
- package/dist/bin/primitive.js +25 -14
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/analytics.js +36 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +52 -188
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +13 -153
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +14 -100
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +47 -8
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +479 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.js +12 -7
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +84 -176
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +171 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.js +4 -4
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +63 -0
- package/dist/src/commands/databases.js +649 -87
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +869 -12
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +21 -128
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.d.ts +12 -1
- package/dist/src/commands/env.js +115 -26
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +116 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +14 -109
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/init.d.ts +24 -1
- package/dist/src/commands/init.js +1194 -279
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +166 -435
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +112 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.js +33 -141
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +206 -563
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -1
- package/dist/src/commands/rule-sets.js +16 -132
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +10 -0
- package/dist/src/commands/scripts.js +203 -241
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sessions.js +17 -5
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +113 -70
- package/dist/src/commands/sync-app-settings.js +193 -242
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +1904 -51
- package/dist/src/commands/sync.js +9468 -1790
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +8 -2
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +92 -1
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +44 -58
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/webhooks.js +318 -156
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +49 -12
- package/dist/src/commands/workflows.js +652 -1086
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/access-rule-display.d.ts +21 -0
- package/dist/src/lib/access-rule-display.js +34 -0
- package/dist/src/lib/access-rule-display.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +541 -61
- package/dist/src/lib/api-client.js +559 -102
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
- package/dist/src/lib/app-settings-descriptor.js +403 -82
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +8 -0
- package/dist/src/lib/cli-manifest.js +1 -0
- package/dist/src/lib/cli-manifest.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +658 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +85 -0
- package/dist/src/lib/config-payload.js +116 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +130 -0
- package/dist/src/lib/config-surface.js +300 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +10 -0
- package/dist/src/lib/credentials-store.js +23 -0
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +187 -0
- package/dist/src/lib/env-resolver-core.js +325 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +28 -6
- package/dist/src/lib/env-resolver.js +44 -32
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +12 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
- package/dist/src/lib/generated-config-surfaces.js +4058 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-client-platforms.d.ts +14 -0
- package/dist/src/lib/init-client-platforms.js +71 -0
- package/dist/src/lib/init-client-platforms.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +44 -5
- package/dist/src/lib/init-config.js +80 -7
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
- package/dist/src/lib/init-email-redirect-uris.js +46 -0
- package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
- package/dist/src/lib/init-ios-links.d.ts +90 -0
- package/dist/src/lib/init-ios-links.js +218 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +33 -0
- package/dist/src/lib/init-xcode.js +114 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/integration-selector.d.ts +42 -0
- package/dist/src/lib/integration-selector.js +46 -0
- package/dist/src/lib/integration-selector.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/local-test-cases.d.ts +63 -0
- package/dist/src/lib/local-test-cases.js +135 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +2 -2
- package/dist/src/lib/migration-nag.js +3 -3
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +24 -1
- package/dist/src/lib/output.js +42 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +50 -0
- package/dist/src/lib/paginate.js +53 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/platform-owned.d.ts +29 -3
- package/dist/src/lib/platform-owned.js +42 -3
- package/dist/src/lib/platform-owned.js.map +1 -1
- package/dist/src/lib/project-config.d.ts +8 -0
- package/dist/src/lib/project-config.js +30 -5
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/query-operators.d.ts +1 -1
- package/dist/src/lib/query-operators.js +1 -1
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +43 -14
- package/dist/src/lib/resolve-platform.js +74 -12
- package/dist/src/lib/resolve-platform.js.map +1 -1
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.js +6 -3
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshots.d.ts +2 -2
- package/dist/src/lib/snapshots.js +2 -2
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
- package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
- package/dist/src/lib/swift-codegen/generator.js +181 -7
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
- package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +39 -0
- package/dist/src/lib/sync-paths.js +70 -2
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/sync-resource-types.d.ts +138 -31
- package/dist/src/lib/sync-resource-types.js +241 -47
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +95 -0
- package/dist/src/lib/sync-selectors.js +228 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +62 -4
- package/dist/src/lib/template.js +147 -39
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-file-names.d.ts +40 -0
- package/dist/src/lib/test-case-file-names.js +91 -0
- package/dist/src/lib/test-case-file-names.js.map +1 -0
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +15 -0
- package/dist/src/lib/test-case-variables.js +29 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +4 -4
- package/dist/src/lib/toml-database-config.js +27 -34
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +9 -5
- package/dist/src/lib/toml-metadata-config.js +40 -45
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +1 -1
- package/dist/src/lib/toml-native-form.js +1 -1
- package/dist/src/lib/toml-params-validator.d.ts +1 -1
- package/dist/src/lib/toml-params-validator.js +1 -1
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +25 -0
- package/dist/src/lib/version-check.js +77 -8
- package/dist/src/lib/version-check.js.map +1 -1
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/web-url.d.ts +40 -0
- package/dist/src/lib/web-url.js +76 -0
- package/dist/src/lib/web-url.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +69 -45
- package/dist/src/lib/workflow-apply.js +99 -72
- package/dist/src/lib/workflow-apply.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.js +4 -4
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +31 -20
- package/dist/src/lib/workflow-payload.js +100 -110
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
- package/dist/src/lib/workflow-toml-validator.js +141 -16
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +78 -22
- package/dist/src/validators.d.ts +7 -7
- package/dist/src/validators.js +6 -6
- package/package.json +11 -6
- package/dist/src/commands/settings.d.ts +0 -15
- package/dist/src/commands/settings.js +0 -102
- package/dist/src/commands/settings.js.map +0 -1
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "fs";
|
|
1
2
|
import { ApiClient } from "../lib/api-client.js";
|
|
2
3
|
import { resolveAppId } from "../lib/config.js";
|
|
3
|
-
import { success, error, info, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, json, divider, } from "../lib/output.js";
|
|
4
|
+
import { success, error, info, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, json, divider, dim, warn, } from "../lib/output.js";
|
|
5
|
+
import { assertSupportedFlags, buildWebhookEnvelope, } from "../lib/log-inspection.js";
|
|
4
6
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
7
|
+
import { parseStatusFilter } from "../lib/object-status-filter.js";
|
|
8
|
+
import { signingSecretStatusWarning, signingSecretHealthWarning, previousSigningSecretWarning, formatGracePeriodMs, } from "../lib/signing-secret-status.js";
|
|
9
|
+
import { fetchAll } from "./sync.js";
|
|
10
|
+
/**
|
|
11
|
+
* Platform ceiling on a webhook body (`MAX_WEBHOOK_BODY_BYTES_LIMIT`, 25 MiB).
|
|
12
|
+
* A per-webhook cap can be lower — that one is the server's call and comes back
|
|
13
|
+
* as a `body_too_large` verdict. This bound only stops the CLI from reading and
|
|
14
|
+
* encoding a file no webhook could ever accept.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_VERIFY_BODY_BYTES = 25 * 1024 * 1024;
|
|
5
17
|
export function registerWebhooksCommands(program) {
|
|
6
18
|
const webhooks = program
|
|
7
19
|
.command("webhooks")
|
|
@@ -9,11 +21,23 @@ export function registerWebhooksCommands(program) {
|
|
|
9
21
|
.addHelpText("after", `
|
|
10
22
|
Examples:
|
|
11
23
|
$ primitive webhooks list
|
|
12
|
-
$ primitive
|
|
24
|
+
$ primitive secrets set STRIPE_WEBHOOK_SECRET --value whsec_...
|
|
25
|
+
$ primitive config create webhook stripe-events # then edit webhooks/stripe-events.toml and: primitive config push
|
|
13
26
|
$ primitive webhooks get 01HXY...
|
|
14
|
-
$ primitive webhooks rotate-secret 01HXY... --secret
|
|
27
|
+
$ primitive webhooks rotate-secret 01HXY... --secret "{{secrets.STRIPE_WEBHOOK_SECRET_NEXT}}"
|
|
15
28
|
$ primitive webhooks events 01HXY...
|
|
29
|
+
$ primitive webhooks archive 01HXY... # retire it (soft delete)
|
|
16
30
|
$ primitive webhooks test 01HXY... --payload '{"event":"test"}'
|
|
31
|
+
$ primitive webhooks verify 01HXY... --header 'Stripe-Signature: t=...,v1=...' --body-file captured.json
|
|
32
|
+
`);
|
|
33
|
+
// Server-side configuration is authored in TOML and applied with `config push`
|
|
34
|
+
// (issue #2645) — this group no longer carries config-setting flags.
|
|
35
|
+
webhooks.addHelpText("after", `
|
|
36
|
+
Configuration lives in webhooks/<key>.toml:
|
|
37
|
+
$ primitive config fields webhook # the file's keys, types and defaults
|
|
38
|
+
$ primitive config create webhook <key> # scaffold one locally
|
|
39
|
+
$ primitive config set webhook/<key> <path>=<value>
|
|
40
|
+
$ primitive config push --only webhook/<key>
|
|
17
41
|
`);
|
|
18
42
|
// List webhooks
|
|
19
43
|
webhooks
|
|
@@ -21,15 +45,27 @@ Examples:
|
|
|
21
45
|
.description("List webhooks")
|
|
22
46
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
23
47
|
.option("--app <app-id>", "App ID")
|
|
24
|
-
.option("--status <status>", "Filter by status: active,
|
|
48
|
+
.option("--status <status>", "Filter by status: active, inactive, archived")
|
|
25
49
|
.option("--json", "Output as JSON")
|
|
26
50
|
.action(async (appId, options) => {
|
|
27
51
|
const resolvedAppId = resolveAppId(appId, options);
|
|
28
52
|
const client = new ApiClient();
|
|
29
53
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
// #2803 — `paused` is gone; refuse it by name rather than returning the
|
|
55
|
+
// empty list a canonical filter would produce.
|
|
56
|
+
const status = parseStatusFilter("webhooks", options.status, [
|
|
57
|
+
"active",
|
|
58
|
+
"inactive",
|
|
59
|
+
"archived",
|
|
60
|
+
]);
|
|
61
|
+
// Drain every page (#2232). The admin list endpoint pages at 25 by
|
|
62
|
+
// default, so an app anywhere near the per-app webhook cap of 50 was
|
|
63
|
+
// silently truncated here — and this is the command an operator uses to
|
|
64
|
+
// find a webhook to hard-delete when a create is refused at the cap.
|
|
65
|
+
const items = await fetchAll((params) => client.listWebhooks(resolvedAppId, {
|
|
66
|
+
...params,
|
|
67
|
+
status,
|
|
68
|
+
}));
|
|
33
69
|
if (options.json) {
|
|
34
70
|
json(items);
|
|
35
71
|
return;
|
|
@@ -52,81 +88,6 @@ Examples:
|
|
|
52
88
|
process.exit(1);
|
|
53
89
|
}
|
|
54
90
|
});
|
|
55
|
-
// Create webhook
|
|
56
|
-
webhooks
|
|
57
|
-
.command("create")
|
|
58
|
-
.description("Create a new webhook")
|
|
59
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
60
|
-
.option("--app <app-id>", "App ID")
|
|
61
|
-
.option("--key <key>", "Webhook key (unique identifier)")
|
|
62
|
-
.option("--name <name>", "Display name")
|
|
63
|
-
.option("--workflow-key <wfKey>", "Workflow key to trigger")
|
|
64
|
-
.option("--scheme <scheme>", "Verification scheme (e.g., hmac-sha256, none)")
|
|
65
|
-
.option("--secret <secret>", "Signing secret")
|
|
66
|
-
.option("--status <status>", "Status: active, paused (default: active)")
|
|
67
|
-
.option("--description <desc>", "Description")
|
|
68
|
-
.option("--tolerance-seconds <n>", "Timestamp tolerance in seconds")
|
|
69
|
-
.option("--no-dedup", "Disable deduplication")
|
|
70
|
-
.option("--dedup-window-ms <n>", "Deduplication window in milliseconds")
|
|
71
|
-
.option("--allowed-ips <cidrs...>", "Allowed IP CIDRs")
|
|
72
|
-
.option("--input-mapping <json>", "Input mapping as JSON")
|
|
73
|
-
.option("--json", "Output as JSON")
|
|
74
|
-
.action(async (appId, options) => {
|
|
75
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
76
|
-
const client = new ApiClient();
|
|
77
|
-
if (!options.key || !options.name || !options.workflowKey || !options.scheme) {
|
|
78
|
-
error("Required: --key, --name, --workflow-key, --scheme");
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
const payload = {
|
|
82
|
-
webhookKey: options.key,
|
|
83
|
-
displayName: options.name,
|
|
84
|
-
workflowKey: options.workflowKey,
|
|
85
|
-
verificationScheme: options.scheme,
|
|
86
|
-
// Default new webhooks to active so they accept inbound deliveries
|
|
87
|
-
// immediately. The server also defaults to active, but sending an
|
|
88
|
-
// explicit status keeps create self-describing.
|
|
89
|
-
status: options.status || "active",
|
|
90
|
-
};
|
|
91
|
-
if (options.secret)
|
|
92
|
-
payload.signingSecret = options.secret;
|
|
93
|
-
if (options.description)
|
|
94
|
-
payload.description = options.description;
|
|
95
|
-
if (options.toleranceSeconds)
|
|
96
|
-
payload.toleranceSeconds = parseInt(options.toleranceSeconds);
|
|
97
|
-
if (options.dedup === false)
|
|
98
|
-
payload.deduplicationEnabled = false;
|
|
99
|
-
if (options.dedupWindowMs)
|
|
100
|
-
payload.deduplicationWindowMs = parseInt(options.dedupWindowMs);
|
|
101
|
-
if (options.allowedIps)
|
|
102
|
-
payload.allowedIpCidrs = options.allowedIps;
|
|
103
|
-
if (options.inputMapping) {
|
|
104
|
-
try {
|
|
105
|
-
payload.inputMapping = JSON.parse(options.inputMapping);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
error("Invalid JSON in --input-mapping");
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
const result = await client.createWebhook(resolvedAppId, payload);
|
|
114
|
-
if (options.json) {
|
|
115
|
-
json(result);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
success(`Webhook created: ${result.displayName}`);
|
|
119
|
-
keyValue("Webhook ID", result.webhookId);
|
|
120
|
-
keyValue("Key", result.webhookKey);
|
|
121
|
-
keyValue("Workflow", result.workflowKey);
|
|
122
|
-
keyValue("Scheme", result.verificationScheme);
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
error(err.message);
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
// Get webhook
|
|
130
91
|
webhooks
|
|
131
92
|
.command("get")
|
|
132
93
|
.description("Get webhook details")
|
|
@@ -149,13 +110,44 @@ Examples:
|
|
|
149
110
|
printResult("Status", formatStatus(result.status));
|
|
150
111
|
printResult("Workflow Key", result.workflowKey);
|
|
151
112
|
printResult("Verification Scheme", result.verificationScheme);
|
|
152
|
-
printResult("Signing Secret", result.
|
|
113
|
+
printResult("Signing Secret", result.signingSecret || "-");
|
|
114
|
+
printResult("Previous Signing Secret", result.previousSigningSecret || "-");
|
|
115
|
+
// The grace slot's own status, so an operator can tell "no previous
|
|
116
|
+
// key" from "a raw value is still live there" — both print the previous
|
|
117
|
+
// secret as `-`, because a withheld value is never echoed (#2383).
|
|
118
|
+
printResult("Previous Signing Secret Status", result.previousSigningSecretStatus || "-");
|
|
119
|
+
// A stored value that is not a reference is never echoed, so say what
|
|
120
|
+
// it is here rather than showing an empty field: a legacy literal from
|
|
121
|
+
// before reference-only still verifies deliveries, a malformed
|
|
122
|
+
// reference does not (#2332). Same wording as `config pull`.
|
|
123
|
+
const signingSecretWarning = signingSecretStatusWarning(result.signingSecretStatus, {
|
|
124
|
+
subject: "Signing secret",
|
|
125
|
+
blocks: "this webhook cannot be updated or rotated",
|
|
126
|
+
});
|
|
127
|
+
if (signingSecretWarning)
|
|
128
|
+
warn(signingSecretWarning);
|
|
129
|
+
// The grace slot gets its own line: a raw value there keeps verifying
|
|
130
|
+
// deliveries even though the row's current slot reads as migrated
|
|
131
|
+
// (#2383). The lever is the grace period, not a migration.
|
|
132
|
+
const previousWarning = previousSigningSecretWarning(result.previousSigningSecretStatus, { subject: "Previous signing secret" });
|
|
133
|
+
if (previousWarning)
|
|
134
|
+
warn(previousWarning);
|
|
135
|
+
// The health axis, per slot — anything that is not `ok`.
|
|
136
|
+
const healthWarning = signingSecretHealthWarning(result.signingSecretHealth, { subject: "Signing secret" });
|
|
137
|
+
if (healthWarning)
|
|
138
|
+
warn(healthWarning);
|
|
139
|
+
const previousHealthWarning = signingSecretHealthWarning(result.previousSigningSecretHealth, { subject: "Previous signing secret" });
|
|
140
|
+
if (previousHealthWarning)
|
|
141
|
+
warn(previousHealthWarning);
|
|
153
142
|
divider();
|
|
154
143
|
info("Configuration:");
|
|
155
144
|
printResult(" Tolerance Seconds", String(result.toleranceSeconds ?? "-"));
|
|
156
145
|
printResult(" Deduplication", result.deduplicationEnabled ? "enabled" : "disabled");
|
|
157
146
|
printResult(" Dedup Window", result.deduplicationWindowMs ? `${result.deduplicationWindowMs}ms` : "-");
|
|
158
|
-
printResult("
|
|
147
|
+
printResult(" Max Body Bytes", result.maxBodyBytes ? `${result.maxBodyBytes}` : "default (5 MiB)");
|
|
148
|
+
// Not a truthiness test: a stored `0` means immediate cutover, which is
|
|
149
|
+
// the opposite of the unset case's 24 h default (#2383).
|
|
150
|
+
printResult(" Grace Period", formatGracePeriodMs(result.secretGracePeriodMs));
|
|
159
151
|
printResult(" Allowed IPs", result.allowedIpCidrs || "-");
|
|
160
152
|
printResult(" Input Mapping", result.inputMapping || "-");
|
|
161
153
|
divider();
|
|
@@ -168,113 +160,121 @@ Examples:
|
|
|
168
160
|
process.exit(1);
|
|
169
161
|
}
|
|
170
162
|
});
|
|
171
|
-
//
|
|
163
|
+
// #2645 criterion 9 / #2803 — the operational verbs. They are the only
|
|
164
|
+
// writers of `status`, which is server-owned and no longer a `[webhook]`
|
|
165
|
+
// TOML key: `config push` can neither set nor clear it, so pushing a file
|
|
166
|
+
// pulled before a pause no longer puts the endpoint back in service.
|
|
172
167
|
webhooks
|
|
173
|
-
.command("
|
|
174
|
-
.description("
|
|
168
|
+
.command("disable")
|
|
169
|
+
.description("Take a webhook out of service now. Inbound deliveries are refused with a 410; the committed configuration is unchanged.")
|
|
175
170
|
.argument("<webhook-id>", "Webhook ID")
|
|
176
171
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
177
|
-
.option("--name <name>", "Display name")
|
|
178
|
-
.option("--description <desc>", "Description")
|
|
179
|
-
.option("--status <status>", "Status: active, paused")
|
|
180
|
-
.option("--scheme <scheme>", "Verification scheme")
|
|
181
|
-
.option("--workflow-key <wfKey>", "Workflow key to trigger")
|
|
182
|
-
.option("--tolerance-seconds <n>", "Timestamp tolerance in seconds")
|
|
183
|
-
.option("--no-dedup", "Disable deduplication")
|
|
184
|
-
.option("--dedup-window-ms <n>", "Deduplication window in milliseconds")
|
|
185
|
-
.option("--allowed-ips <cidrs...>", "Allowed IP CIDRs")
|
|
186
|
-
.option("--grace-period-ms <n>", "Secret grace period in milliseconds")
|
|
187
|
-
.option("--input-mapping <json>", "Input mapping as JSON")
|
|
188
172
|
.option("--json", "Output as JSON")
|
|
189
173
|
.action(async (webhookId, options) => {
|
|
190
174
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
payload.status = options.status;
|
|
198
|
-
if (options.scheme)
|
|
199
|
-
payload.verificationScheme = options.scheme;
|
|
200
|
-
if (options.workflowKey)
|
|
201
|
-
payload.workflowKey = options.workflowKey;
|
|
202
|
-
if (options.toleranceSeconds)
|
|
203
|
-
payload.toleranceSeconds = parseInt(options.toleranceSeconds);
|
|
204
|
-
if (options.dedup === false)
|
|
205
|
-
payload.deduplicationEnabled = false;
|
|
206
|
-
if (options.dedupWindowMs)
|
|
207
|
-
payload.deduplicationWindowMs = parseInt(options.dedupWindowMs);
|
|
208
|
-
if (options.allowedIps)
|
|
209
|
-
payload.allowedIpCidrs = options.allowedIps;
|
|
210
|
-
if (options.gracePeriodMs)
|
|
211
|
-
payload.secretGracePeriodMs = parseInt(options.gracePeriodMs);
|
|
212
|
-
if (options.inputMapping) {
|
|
213
|
-
try {
|
|
214
|
-
payload.inputMapping = JSON.parse(options.inputMapping);
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
error("Invalid JSON in --input-mapping");
|
|
218
|
-
process.exit(1);
|
|
175
|
+
const client = new ApiClient();
|
|
176
|
+
try {
|
|
177
|
+
const result = await client.setWebhookAvailability(resolvedAppId, webhookId, "disable");
|
|
178
|
+
if (options.json) {
|
|
179
|
+
json(result);
|
|
180
|
+
return;
|
|
219
181
|
}
|
|
182
|
+
success(`Webhook disabled: ${result.webhookKey || webhookId}`);
|
|
183
|
+
info("Deliveries are refused with a 410 — `primitive webhooks enable` puts it back.");
|
|
220
184
|
}
|
|
221
|
-
|
|
222
|
-
error(
|
|
185
|
+
catch (err) {
|
|
186
|
+
error(err.message);
|
|
223
187
|
process.exit(1);
|
|
224
188
|
}
|
|
189
|
+
});
|
|
190
|
+
webhooks
|
|
191
|
+
.command("enable")
|
|
192
|
+
.description("Put a webhook back in service.")
|
|
193
|
+
.argument("<webhook-id>", "Webhook ID")
|
|
194
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
195
|
+
.option("--json", "Output as JSON")
|
|
196
|
+
.action(async (webhookId, options) => {
|
|
197
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
225
198
|
const client = new ApiClient();
|
|
226
199
|
try {
|
|
227
|
-
const result = await client.
|
|
200
|
+
const result = await client.setWebhookAvailability(resolvedAppId, webhookId, "enable");
|
|
228
201
|
if (options.json) {
|
|
229
202
|
json(result);
|
|
230
203
|
return;
|
|
231
204
|
}
|
|
232
|
-
success(
|
|
205
|
+
success(`Webhook enabled: ${result.webhookKey || webhookId}`);
|
|
233
206
|
}
|
|
234
207
|
catch (err) {
|
|
235
208
|
error(err.message);
|
|
236
209
|
process.exit(1);
|
|
237
210
|
}
|
|
238
211
|
});
|
|
239
|
-
//
|
|
212
|
+
// #2907 — the delete flow's CLI spelling. Not a second writer of `status`:
|
|
213
|
+
// this is the plain `DELETE` the server has always answered by writing the
|
|
214
|
+
// tombstone, which until now only the admin API and the console could reach.
|
|
215
|
+
// Named `archive`, not `delete`, because a configuration noun gets no
|
|
216
|
+
// `delete` verb (docs/cli-design.md) and prune-by-push stays the only CLI
|
|
217
|
+
// hard-delete path.
|
|
240
218
|
webhooks
|
|
241
|
-
.command("
|
|
242
|
-
.description("
|
|
219
|
+
.command("archive")
|
|
220
|
+
.description("Retire a webhook (soft delete). Delete-lifecycle, not `disable`: the row is kept, keeps its key and its capped slot, and there is no un-archive.")
|
|
243
221
|
.argument("<webhook-id>", "Webhook ID")
|
|
244
222
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
245
223
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
224
|
+
.option("--json", "Output as JSON")
|
|
225
|
+
.addHelpText("after", `
|
|
226
|
+
What archiving a webhook costs:
|
|
227
|
+
Inbound deliveries are refused, as they are for a disabled webhook.
|
|
228
|
+
The archived row still holds its webhookKey AND its slot under the per-app
|
|
229
|
+
webhook cap (50) — archiving frees neither, which is why an archived row a
|
|
230
|
+
delivery still reaches is not free.
|
|
231
|
+
\`primitive webhooks enable\` refuses an archived webhook; there is no
|
|
232
|
+
un-archive.
|
|
233
|
+
|
|
234
|
+
Reclaiming the key and the slot means hard-deleting the row:
|
|
235
|
+
$ rm webhooks/<key>.toml
|
|
236
|
+
$ primitive config push --prune # confirmed; hard-deletes the row
|
|
237
|
+
# or hard-delete it in the console, then re-add the file and push.
|
|
238
|
+
`)
|
|
246
239
|
.action(async (webhookId, options) => {
|
|
247
240
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
248
241
|
if (!options.yes) {
|
|
249
|
-
let
|
|
242
|
+
let confirmed;
|
|
250
243
|
try {
|
|
251
|
-
|
|
244
|
+
confirmed = await confirmPrompt(`Archive webhook ${webhookId}? Deliveries stop, the row keeps its key ` +
|
|
245
|
+
`and its capped slot, and there is no un-archive.`);
|
|
252
246
|
}
|
|
253
247
|
catch (err) {
|
|
254
248
|
error(err.message);
|
|
255
249
|
process.exit(1);
|
|
256
250
|
}
|
|
257
|
-
if (!
|
|
251
|
+
if (!confirmed) {
|
|
258
252
|
info("Cancelled.");
|
|
259
253
|
return;
|
|
260
254
|
}
|
|
261
255
|
}
|
|
262
256
|
const client = new ApiClient();
|
|
263
257
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
// The soft delete: no `hard`, so the server writes the tombstone. The
|
|
259
|
+
// `--hard` path stays out of the CLI surface — prune-by-push owns it.
|
|
260
|
+
const result = await client.deleteWebhook(resolvedAppId, webhookId);
|
|
261
|
+
if (options.json) {
|
|
262
|
+
json(result);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
success(`Webhook archived: ${webhookId}`);
|
|
266
|
+
info("Deliveries are refused. The row still holds its key and its capped slot — reclaim both by removing its file and running a confirmed `primitive config push --prune`.");
|
|
266
267
|
}
|
|
267
268
|
catch (err) {
|
|
268
269
|
error(err.message);
|
|
269
270
|
process.exit(1);
|
|
270
271
|
}
|
|
271
272
|
});
|
|
272
|
-
// Rotate secret
|
|
273
273
|
webhooks
|
|
274
274
|
.command("rotate-secret")
|
|
275
275
|
.description("Rotate the signing secret for a webhook")
|
|
276
276
|
.argument("<webhook-id>", "Webhook ID")
|
|
277
|
-
.requiredOption("--secret <
|
|
277
|
+
.requiredOption("--secret <ref>", "New signing secret, as a {{secrets.KEY}} reference to an app secret")
|
|
278
278
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
279
279
|
.option("--json", "Output as JSON")
|
|
280
280
|
.action(async (webhookId, options) => {
|
|
@@ -288,9 +288,15 @@ Examples:
|
|
|
288
288
|
json(result);
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
|
+
// Rotation now returns the serialized webhook on both surfaces, and
|
|
292
|
+
// both references are plain text (the secret values live only in the
|
|
293
|
+
// secret store), so print them rather than a masked value.
|
|
291
294
|
success("Signing secret rotated.");
|
|
292
|
-
if (result.
|
|
293
|
-
keyValue("Secret
|
|
295
|
+
if (result.signingSecret) {
|
|
296
|
+
keyValue("Signing Secret", result.signingSecret);
|
|
297
|
+
}
|
|
298
|
+
if (result.previousSigningSecret) {
|
|
299
|
+
keyValue("Previous Signing Secret", result.previousSigningSecret);
|
|
294
300
|
}
|
|
295
301
|
}
|
|
296
302
|
catch (err) {
|
|
@@ -305,24 +311,41 @@ Examples:
|
|
|
305
311
|
.argument("<webhook-id>", "Webhook ID")
|
|
306
312
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
307
313
|
.option("--limit <n>", "Number of events to show", "20")
|
|
314
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page")
|
|
315
|
+
// Declared so they can be rejected with a clear message instead of being
|
|
316
|
+
// silently ignored — status/time filtering on webhook events is deferred to
|
|
317
|
+
// Phase B (issue #1967). See assertSupportedFlags below.
|
|
318
|
+
.option("--status <status>", "(not supported yet — deferred to Phase B)")
|
|
319
|
+
.option("--from <iso>", "(not supported yet — deferred to Phase B)")
|
|
320
|
+
.option("--to <iso>", "(not supported yet — deferred to Phase B)")
|
|
308
321
|
.option("--json", "Output as JSON")
|
|
309
322
|
.action(async (webhookId, options) => {
|
|
310
323
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
311
324
|
const client = new ApiClient();
|
|
312
325
|
try {
|
|
313
|
-
|
|
326
|
+
// The webhook-events route serves limit + cursor only; reject the
|
|
327
|
+
// Phase B filters cleanly rather than dropping them silently.
|
|
328
|
+
assertSupportedFlags("webhook events", { status: options.status, from: options.from, to: options.to }, ["limit", "cursor"]);
|
|
329
|
+
const page = await client.listWebhookEvents(resolvedAppId, webhookId, {
|
|
314
330
|
limit: parseInt(options.limit),
|
|
331
|
+
...(options.cursor && { cursor: options.cursor }),
|
|
315
332
|
});
|
|
316
|
-
const events = result?.items ?? result ?? [];
|
|
317
333
|
if (options.json) {
|
|
318
|
-
json(
|
|
334
|
+
json(buildWebhookEnvelope(page.items, page));
|
|
319
335
|
return;
|
|
320
336
|
}
|
|
321
|
-
|
|
337
|
+
const printCursorHint = () => {
|
|
338
|
+
if (!page.nextCursor)
|
|
339
|
+
return;
|
|
340
|
+
console.log();
|
|
341
|
+
dim(`More events. Use --cursor ${page.nextCursor} for the next page.`);
|
|
342
|
+
};
|
|
343
|
+
if (!page.items || page.items.length === 0) {
|
|
322
344
|
info("No events found.");
|
|
345
|
+
printCursorHint();
|
|
323
346
|
return;
|
|
324
347
|
}
|
|
325
|
-
console.log(formatTable(
|
|
348
|
+
console.log(formatTable(page.items, [
|
|
326
349
|
{ header: "EVENT ID", key: "eventId", format: formatId },
|
|
327
350
|
{ header: "EXTERNAL ID", key: "externalEventId" },
|
|
328
351
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
@@ -330,6 +353,7 @@ Examples:
|
|
|
330
353
|
{ header: "REASON", key: "rejectionReason" },
|
|
331
354
|
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
332
355
|
]));
|
|
356
|
+
printCursorHint();
|
|
333
357
|
}
|
|
334
358
|
catch (err) {
|
|
335
359
|
error(err.message);
|
|
@@ -339,18 +363,23 @@ Examples:
|
|
|
339
363
|
// Test webhook
|
|
340
364
|
webhooks
|
|
341
365
|
.command("test")
|
|
342
|
-
.description("
|
|
366
|
+
.description("Sign a test payload for a webhook (returns body + headers)")
|
|
343
367
|
.argument("<webhook-id>", "Webhook ID")
|
|
344
368
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
345
|
-
.option("--payload <json>", "Test payload as JSON,
|
|
369
|
+
.option("--payload <json>", "Test payload as JSON, signed verbatim. Pass the event " +
|
|
346
370
|
"object directly (e.g. '{\"type\":\"event\",\"data\":{…}}'); it is used " +
|
|
347
|
-
"as-is. Omit it to
|
|
371
|
+
"as-is. Omit it to sign a canned webhook.test ping.")
|
|
348
372
|
.option("--json", "Output as JSON")
|
|
349
373
|
.addHelpText("after", `
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
374
|
+
This command signs a payload and prints the signature headers and the signed
|
|
375
|
+
body. It does NOT post them to the webhook's receive endpoint, so no workflow
|
|
376
|
+
runs and no delivery is recorded — send the returned body with the returned
|
|
377
|
+
headers to POST /app/{appId}/webhook/{webhookKey} yourself to do that.
|
|
378
|
+
|
|
379
|
+
The value of --payload is signed exactly as given (WYSIWYG). A JSON object is
|
|
380
|
+
used as the test body verbatim, so a wrapped payload such as '{"payload":{…}}'
|
|
381
|
+
signs the literal wrapper — do not wrap the event. Omitting --payload (or
|
|
382
|
+
passing a non-object such as an array) signs the canned
|
|
354
383
|
{"type":"webhook.test", …} ping.
|
|
355
384
|
`)
|
|
356
385
|
.action(async (webhookId, options) => {
|
|
@@ -373,7 +402,9 @@ Omitting --payload (or passing a non-object such as an array) sends the canned
|
|
|
373
402
|
return;
|
|
374
403
|
}
|
|
375
404
|
if (result.success) {
|
|
376
|
-
success("Test
|
|
405
|
+
success("Test payload signed. Nothing was delivered — post the signed body " +
|
|
406
|
+
"(--json shows the exact rawBody) with these headers to the " +
|
|
407
|
+
"receive endpoint to trigger the workflow.");
|
|
377
408
|
if (result.signatureHeaders) {
|
|
378
409
|
divider();
|
|
379
410
|
info("Generated Signature Headers:");
|
|
@@ -392,7 +423,138 @@ Omitting --payload (or passing a non-object such as an array) sends the canned
|
|
|
392
423
|
}
|
|
393
424
|
}
|
|
394
425
|
catch (err) {
|
|
395
|
-
|
|
426
|
+
// The server refuses a preview it cannot sign with a coded 400 — the
|
|
427
|
+
// scheme holds only public key material, the row still stores a raw
|
|
428
|
+
// secret, the signing secret is unset or unresolvable (#2445). Print
|
|
429
|
+
// the machine-readable code alongside the message, the established
|
|
430
|
+
// convention here: the prose explains, the code is what an operator
|
|
431
|
+
// searches for or reports.
|
|
432
|
+
error(err?.code ? `${err.message} (${err.code})` : err.message);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
// Verify a captured delivery's signature
|
|
437
|
+
webhooks
|
|
438
|
+
.command("verify")
|
|
439
|
+
.description("Check whether signature headers you captured verify against a webhook")
|
|
440
|
+
.argument("<webhook-id>", "Webhook ID")
|
|
441
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
442
|
+
.option("--header <name: value>", "A header from the captured delivery. Repeatable.", (value, previous) => [...(previous || []), value], [])
|
|
443
|
+
.option("--body <text>", "The captured delivery body, as text")
|
|
444
|
+
.option("--body-file <path>", "Read the captured delivery body from a file")
|
|
445
|
+
.option("--json", "Output as JSON")
|
|
446
|
+
.addHelpText("after", `
|
|
447
|
+
Runs the webhook's configured verifier over the body and headers you supply and
|
|
448
|
+
reports whether that signature checks out — the useful question when a real
|
|
449
|
+
delivery was rejected 401, and the only way to check a scheme the platform holds
|
|
450
|
+
only public key material for (discord, jwt, plaid), which \`webhooks test\`
|
|
451
|
+
cannot preview.
|
|
452
|
+
|
|
453
|
+
Nothing is delivered: no workflow runs, and no delivery is recorded (this check
|
|
454
|
+
never appears in \`primitive webhooks events\`).
|
|
455
|
+
|
|
456
|
+
It answers SIGNATURE verification only. A real delivery is also subject to the
|
|
457
|
+
webhook's status and IP allowlist before verification, and to handshake rules
|
|
458
|
+
and deduplication after it.
|
|
459
|
+
|
|
460
|
+
Exits 0 when the signature verifies and non-zero otherwise, so it composes in a
|
|
461
|
+
script.
|
|
462
|
+
|
|
463
|
+
$ primitive webhooks verify 01HXY... \\
|
|
464
|
+
--header 'Stripe-Signature: t=1754...,v1=abc...' \\
|
|
465
|
+
--body-file captured-delivery.json
|
|
466
|
+
`)
|
|
467
|
+
.action(async (webhookId, options) => {
|
|
468
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
469
|
+
if (options.body !== undefined && options.bodyFile !== undefined) {
|
|
470
|
+
error("Pass --body or --body-file, not both.");
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
let rawBody;
|
|
474
|
+
let bodyBase64;
|
|
475
|
+
if (options.bodyFile !== undefined) {
|
|
476
|
+
let bytes;
|
|
477
|
+
try {
|
|
478
|
+
// Size first: a mistakenly-passed large file should be a clear CLI
|
|
479
|
+
// error rather than a buffer the command tries to base64 and ship.
|
|
480
|
+
const { size } = statSync(options.bodyFile);
|
|
481
|
+
if (size > MAX_VERIFY_BODY_BYTES) {
|
|
482
|
+
error(`--body-file is ${size} bytes, over the ${MAX_VERIFY_BODY_BYTES}-byte platform maximum for a webhook body. A delivery that large is refused before its signature is checked.`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
bytes = readFileSync(options.bodyFile);
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
error(`Could not read --body-file: ${err.message}`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// A signature is over BYTES. Decoding a capture that is not valid
|
|
494
|
+
// UTF-8 replaces each bad byte with U+FFFD, so sending it as `rawBody`
|
|
495
|
+
// would check the signature against bytes the provider never sent and
|
|
496
|
+
// report a confident `sig_invalid` — the wrong answer in exactly the
|
|
497
|
+
// case this command exists for. `bodyBase64` carries those bytes
|
|
498
|
+
// intact, so only a body that round-trips losslessly goes as text.
|
|
499
|
+
const asText = bytes.toString("utf-8");
|
|
500
|
+
if (Buffer.from(asText, "utf-8").equals(bytes)) {
|
|
501
|
+
rawBody = asText;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
bodyBase64 = bytes.toString("base64");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
// An empty body is a legitimate delivery, so an omitted --body means
|
|
509
|
+
// "the delivery carried no body" rather than "no body was supplied".
|
|
510
|
+
rawBody = options.body ?? "";
|
|
511
|
+
}
|
|
512
|
+
// Ordered pairs, not an object: a real capture can carry the same field
|
|
513
|
+
// name on more than one line, which an object cannot represent.
|
|
514
|
+
const headers = [];
|
|
515
|
+
for (const entry of (options.header || [])) {
|
|
516
|
+
const separator = entry.indexOf(":");
|
|
517
|
+
if (separator <= 0) {
|
|
518
|
+
error(`Invalid --header '${entry}'. Use 'Name: value'.`);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
headers.push([
|
|
522
|
+
entry.slice(0, separator).trim(),
|
|
523
|
+
entry.slice(separator + 1).trim(),
|
|
524
|
+
]);
|
|
525
|
+
}
|
|
526
|
+
const client = new ApiClient();
|
|
527
|
+
try {
|
|
528
|
+
const result = await client.verifyWebhook(resolvedAppId, webhookId, {
|
|
529
|
+
headers,
|
|
530
|
+
// Exactly one body form — the server refuses a request naming both.
|
|
531
|
+
...(bodyBase64 !== undefined ? { bodyBase64 } : { rawBody }),
|
|
532
|
+
});
|
|
533
|
+
if (options.json) {
|
|
534
|
+
json(result);
|
|
535
|
+
}
|
|
536
|
+
else if (result.verified) {
|
|
537
|
+
success("Signature verified.");
|
|
538
|
+
keyValue(" Scheme", String(result.verificationScheme));
|
|
539
|
+
if (result.dedupKey)
|
|
540
|
+
keyValue(" Dedup key", String(result.dedupKey));
|
|
541
|
+
if (result.externalEventId) {
|
|
542
|
+
keyValue(" Event ID", String(result.externalEventId));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
error("Signature did NOT verify.");
|
|
547
|
+
keyValue(" Scheme", String(result.verificationScheme));
|
|
548
|
+
keyValue(" Reason", String(result.rejectionReason || "unknown"));
|
|
549
|
+
}
|
|
550
|
+
if (result.webhookStatus && result.webhookStatus !== "active") {
|
|
551
|
+
warn(`This webhook is ${result.webhookStatus} — a live delivery would be refused before its signature was checked.`);
|
|
552
|
+
}
|
|
553
|
+
if (!result.verified)
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
error(err?.code ? `${err.message} (${err.code})` : err.message);
|
|
396
558
|
process.exit(1);
|
|
397
559
|
}
|
|
398
560
|
});
|