@chainpatrol/cli 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e2b28a5: Expose `contactUrl` (main communication channel) on `orgs list` and `orgs get` output. Add `organization.missing-contact-url` healthcheck. Server-side work in @chainpatrol/external-trpc and @chainpatrol/validation supports the new field and healthcheck; both are private packages and don't need version bumps.
8
+ - b1e35f0: Surface pending Wallet Blocking / Takedown approvals on `orgs list` and `orgs get`. Each org now reports `pendingServiceApprovals` — open requests by lower-level members to enable an approval-gated service (Wallet Blocking / Takedowns) that a higher-up at the org has not yet approved. Adds `--pending-approval-active`, `--pending-approval-inactive`, and `--pending-approval-service <protection|takedowns>` filters to `orgs list` so you can track which organizations are awaiting sign-off, distinct from which have the service turned off. Server-side work in @chainpatrol/external-trpc and @chainpatrol/validation backs the new field and filters; both are private packages and don't need version bumps.
9
+
3
10
  ## 0.21.0
4
11
 
5
12
  ### Minor Changes
@@ -205,6 +205,12 @@ description: |
205
205
  "orgs that require admin approval", "orgs requiring approval",
206
206
  "which orgs require approval for twitter", "approval scope",
207
207
  "admin approval asset types", "approval per asset type",
208
+ "pending approval", "pending approvals", "pending service approval",
209
+ "orgs with pending approval", "pending wallet blocking approval",
210
+ "pending takedown approval", "awaiting approval", "waiting for approval",
211
+ "is protection active approval", "wallet blocking approval",
212
+ "who needs to approve wallet blocking", "service change request",
213
+ "track orgs with pending approval", "orgs awaiting sign-off",
208
214
  "is this URL blocked", "is this domain blocked", "is this address blocked",
209
215
  "check this asset", "asset check", "lookup asset status",
210
216
  "what is ARCHIVE_ORG", "what does PAGE mean", "list asset types",
@@ -735,10 +741,48 @@ So per-org JSON looks like:
735
741
  "detection": { "active": true },
736
742
  "darkWebMonitoring": { "active": false }
737
743
  },
738
- "obligatoryAdminApproval": { "active": true, "assetTypes": ["TWITTER"] }
744
+ "obligatoryAdminApproval": { "active": true, "assetTypes": ["TWITTER"] },
745
+ "pendingServiceApprovals": [
746
+ {
747
+ "service": "protection",
748
+ "automated": false,
749
+ "serviceType": "isProtectionActive",
750
+ "serviceName": "Wallet Blocking",
751
+ "requestedAt": "2026-05-30T18:04:11.000Z"
752
+ }
753
+ ]
739
754
  }
740
755
  \`\`\`
741
756
 
757
+ #### Pending service approvals (Wallet Blocking / Takedowns)
758
+
759
+ Turning on **Wallet Blocking** (\`protection\`, the "is protection active"
760
+ toggle) or **Takedowns** is approval-gated: when a lower-level org member
761
+ tries to enable one, ChainPatrol records a pending request that a higher-up
762
+ at the org \u2014 an org OWNER, or ChainPatrol staff acting as owner \u2014 must
763
+ approve before the service actually turns on. \`pendingServiceApprovals\`
764
+ lists those open, awaiting-sign-off requests per org.
765
+
766
+ This is the answer to "which organizations have a pending approval for
767
+ Wallet blocking or Takedown services?" \u2014 it is NOT the same as "which orgs
768
+ have Wallet Blocking turned off". An org can have \`protection.active=false\`
769
+ with **no** pending approval (nobody has asked) OR **with** a pending
770
+ approval (someone asked and it's waiting on an owner). Only
771
+ \`pendingServiceApprovals\` distinguishes the two.
772
+
773
+ Each entry:
774
+
775
+ - \`service\` \u2014 \`protection\` (Wallet Blocking) or \`takedowns\`, matching the
776
+ \`services\` keys.
777
+ - \`automated\` \u2014 \`true\` when the request is for the automated variant
778
+ (e.g. "Automated Wallet Blocking").
779
+ - \`serviceType\` \u2014 raw enum, e.g. \`isProtectionActive\`.
780
+ - \`serviceName\` \u2014 human label, e.g. \`Wallet Blocking\`.
781
+ - \`requestedAt\` \u2014 when the enable request was submitted (ISO 8601, UTC).
782
+
783
+ An **empty array means nothing is awaiting approval** \u2014 read \`services\` for
784
+ the current on/off state, never infer it from this field.
785
+
742
786
  When a user asks about "automated reporting / reviewing / protection",
743
787
  explain that the flag exists in the DB but has no operational effect and
744
788
  isn't exposed by the public API \u2014 only \`takedowns.automated\` carries a
@@ -767,6 +811,15 @@ Filter flags (all optional, all comma-separated lists where noted):
767
811
  \`--obligatory-approval-inactive\`. Use the canonical asset-type enum
768
812
  names (\`TWITTER\`, \`URL\`, \`PAGE\`, \u2026); run \`chainpatrol asset types\`
769
813
  to see them all.
814
+ - \`--pending-approval-active\` \u2014 only orgs with at least one open
815
+ service-enable approval awaiting a higher-up's sign-off.
816
+ - \`--pending-approval-inactive\` \u2014 only orgs with no pending approvals.
817
+ - \`--pending-approval-service <list>\` \u2014 only orgs with a pending approval
818
+ for one of the listed services. Accepts \`protection\` (Wallet Blocking)
819
+ and/or \`takedowns\` \u2014 those are the only approval-gated services.
820
+ Matches both the manual and automated variant. Passing this flag implies
821
+ a pending approval exists, so it can't be combined with
822
+ \`--pending-approval-inactive\`.
770
823
 
771
824
  Service names: \`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\`,
772
825
  \`detection\`, \`darkWebMonitoring\`.
@@ -794,17 +847,39 @@ chainpatrol --json orgs list --obligatory-approval-active \\
794
847
  | jq '.organizations[] | select(.obligatoryAdminApproval.assetTypes | length == 0) | .slug'
795
848
  \`\`\`
796
849
 
850
+ #### Use case: tracking orgs with a pending Wallet Blocking / Takedown approval
851
+
852
+ When a user asks "which organizations have a pending approval for Wallet
853
+ blocking or Takedown services?", do NOT reach for \`--service-inactive
854
+ protection\` \u2014 that lists orgs with the service turned OFF, which is a
855
+ different question. Use the pending-approval filter, which surfaces
856
+ staff-initiated enable requests still waiting on a higher-up:
857
+
858
+ \`\`\`bash
859
+ # Every org with a pending Wallet Blocking OR Takedown approval:
860
+ chainpatrol --json orgs list --pending-approval-service protection,takedowns
861
+
862
+ # Just Wallet Blocking:
863
+ chainpatrol --json orgs list --pending-approval-service protection
864
+
865
+ # Any pending approval at all, then read each org's pendingServiceApprovals:
866
+ chainpatrol --json orgs list --pending-approval-active \\
867
+ | jq '.organizations[] | {slug, pending: [.pendingServiceApprovals[].serviceName]}'
868
+ \`\`\`
869
+
797
870
  ### \`orgs get\` \u2014 Get a single organization by slug
798
871
 
799
872
  Look up one organization the caller has access to. Returns the same per-org
800
873
  shape as a single row from \`orgs list\` \u2014 \`active\` for every service,
801
874
  plus \`automated\` on \`takedowns\` only (see the note above on why other
802
875
  services don't expose an automation flag), plus \`obligatoryAdminApproval\`
803
- (\`active\` + \`assetTypes\`). Anything you could read from \`orgs list\` you
804
- can also read here without paging or filtering. Use it when the user
805
- names a specific customer ("show me acme's setup", "is takedowns
806
- automation on for morpho?", "does this org require admin approval for
807
- Twitter?") and you don't need the rest of the catalogue.
876
+ (\`active\` + \`assetTypes\`) and \`pendingServiceApprovals\` (open Wallet
877
+ Blocking / Takedown enable requests awaiting a higher-up's sign-off).
878
+ Anything you could read from \`orgs list\` you can also read here without
879
+ paging or filtering. Use it when the user names a specific customer ("show
880
+ me acme's setup", "is takedowns automation on for morpho?", "does this org
881
+ require admin approval for Twitter?", "is morpho waiting on a Wallet
882
+ Blocking approval?") and you don't need the rest of the catalogue.
808
883
 
809
884
  \`\`\`bash
810
885
  chainpatrol orgs get <slug>
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-4N2YKTJ5.js";
16
+ } from "./chunk-LKITFBIG.js";
17
17
  import "./chunk-6VTWWGNV.js";
18
18
  import {
19
19
  DateTime
@@ -532,12 +532,16 @@ var HELP = {
532
532
  "--service-active <l> Comma-separated services that must be active",
533
533
  "--service-inactive <l> Comma-separated services that must be inactive",
534
534
  "--service-automated <l> Services whose automation must be ON (only `takedowns` is accepted \u2014 the other services have no meaningful automation toggle)",
535
- "--service-manual <l> Services whose automation must be OFF (only `takedowns` is accepted)"
535
+ "--service-manual <l> Services whose automation must be OFF (only `takedowns` is accepted)",
536
+ "--pending-approval-active Only orgs with an open service-enable approval awaiting a higher-up's sign-off",
537
+ "--pending-approval-inactive Only orgs with no pending service-enable approvals",
538
+ "--pending-approval-service <l> Only orgs with a pending approval for the listed service(s) \u2014 `protection` (Wallet Blocking) and/or `takedowns`"
536
539
  ],
537
540
  examples: [
538
541
  "chainpatrol orgs list --subscription-status ACTIVE",
539
542
  "chainpatrol orgs list --service-active takedowns --service-manual takedowns",
540
- "chainpatrol --json orgs list --subscription-status ACTIVE,TRIAL --service-active detection"
543
+ "chainpatrol --json orgs list --subscription-status ACTIVE,TRIAL --service-active detection",
544
+ "chainpatrol --json orgs list --pending-approval-service protection,takedowns"
541
545
  ]
542
546
  },
543
547
  "orgs get": {
@@ -919,6 +923,9 @@ ${getTopLevelHelp()}
919
923
  obligatoryApprovalActive: { type: "boolean" },
920
924
  obligatoryApprovalInactive: { type: "boolean" },
921
925
  obligatoryApprovalAssetType: { type: "string" },
926
+ pendingApprovalActive: { type: "boolean" },
927
+ pendingApprovalInactive: { type: "boolean" },
928
+ pendingApprovalService: { type: "string" },
922
929
  query: { type: "string" },
923
930
  assetType: { type: "string" },
924
931
  takedownStatus: { type: "string" },
@@ -1561,7 +1568,7 @@ async function main() {
1561
1568
  }
1562
1569
  case "orgs": {
1563
1570
  if (subcommand === "list") {
1564
- const { runOrgsList } = await import("./list-MHICJ2TK.js");
1571
+ const { runOrgsList } = await import("./list-JK4SRMPL.js");
1565
1572
  await runOrgsList({
1566
1573
  query: cli.flags.query,
1567
1574
  subscriptionStatus: cli.flags.subscriptionStatus,
@@ -1572,6 +1579,9 @@ async function main() {
1572
1579
  obligatoryApprovalActive: cli.flags.obligatoryApprovalActive,
1573
1580
  obligatoryApprovalInactive: cli.flags.obligatoryApprovalInactive,
1574
1581
  obligatoryApprovalAssetType: cli.flags.obligatoryApprovalAssetType,
1582
+ pendingApprovalActive: cli.flags.pendingApprovalActive,
1583
+ pendingApprovalInactive: cli.flags.pendingApprovalInactive,
1584
+ pendingApprovalService: cli.flags.pendingApprovalService,
1575
1585
  json: jsonMode,
1576
1586
  outputFormat: cliContext.outputFormat
1577
1587
  });
@@ -1580,7 +1590,7 @@ async function main() {
1580
1590
  if (subcommand === "get") {
1581
1591
  const positionalSlug = action;
1582
1592
  const slug = positionalSlug ?? await tryResolveOrg();
1583
- const { runOrgsGet } = await import("./get-XN752KSL.js");
1593
+ const { runOrgsGet } = await import("./get-GTACVCLE.js");
1584
1594
  await runOrgsGet({
1585
1595
  slug,
1586
1596
  json: jsonMode,
@@ -1711,12 +1721,12 @@ async function main() {
1711
1721
  case "setup":
1712
1722
  case "install":
1713
1723
  case "i": {
1714
- const { setupSkill } = await import("./setup-skill-UEOROLCF.js");
1724
+ const { setupSkill } = await import("./setup-skill-V3HDWN7T.js");
1715
1725
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1716
1726
  break;
1717
1727
  }
1718
1728
  case "uninstall": {
1719
- const { uninstallSkill } = await import("./setup-skill-UEOROLCF.js");
1729
+ const { uninstallSkill } = await import("./setup-skill-V3HDWN7T.js");
1720
1730
  uninstallSkill({ json: jsonMode });
1721
1731
  break;
1722
1732
  }
@@ -31,6 +31,13 @@ function summarizeObligatoryAdminApproval(org) {
31
31
  const types = org.obligatoryAdminApproval.assetTypes;
32
32
  return types.length === 0 ? "on (all asset types)" : `on (${types.join(", ")})`;
33
33
  }
34
+ function summarizePendingApprovals(org) {
35
+ if (org.pendingServiceApprovals.length === 0) return "none";
36
+ const labels = Array.from(
37
+ new Set(org.pendingServiceApprovals.map((approval) => approval.serviceName))
38
+ );
39
+ return labels.join(", ");
40
+ }
34
41
  async function runOrgsGet(options) {
35
42
  const slug = options.slug?.trim();
36
43
  if (!slug) {
@@ -49,14 +56,17 @@ async function runOrgsGet(options) {
49
56
  markdown: [
50
57
  `# ${organization.name} (\`${organization.slug}\`)`,
51
58
  "",
59
+ `- Contact URL: ${organization.contactUrl ?? "(not set)"}`,
52
60
  `- Subscription status: ${organization.subscriptionStatus}`,
53
61
  `- Services: ${summarizeServices(organization)}`,
54
- `- Obligatory admin approval: ${summarizeObligatoryAdminApproval(organization)}`
62
+ `- Obligatory admin approval: ${summarizeObligatoryAdminApproval(organization)}`,
63
+ `- Pending service approvals: ${summarizePendingApprovals(organization)}`
55
64
  ].join("\n"),
56
65
  csv: toCsvRows([
57
66
  {
58
67
  slug: organization.slug,
59
68
  name: organization.name,
69
+ contactUrl: organization.contactUrl ?? "",
60
70
  subscriptionStatus: organization.subscriptionStatus,
61
71
  reportingActive: organization.services.reporting.active,
62
72
  reviewingActive: organization.services.reviewing.active,
@@ -69,16 +79,28 @@ async function runOrgsGet(options) {
69
79
  obligatoryAdminApprovalActive: organization.obligatoryAdminApproval.active,
70
80
  // Empty list + active=true means "applies to all asset types" — read
71
81
  // the active column alongside this one to interpret.
72
- obligatoryAdminApprovalAssetTypes: organization.obligatoryAdminApproval.assetTypes.join("|")
82
+ obligatoryAdminApprovalAssetTypes: organization.obligatoryAdminApproval.assetTypes.join("|"),
83
+ // Pending staff requests to enable Wallet Blocking / Takedowns that a
84
+ // higher-up has not yet approved. Raw `serviceType` enum values,
85
+ // de-duplicated; an empty cell means nothing is awaiting approval.
86
+ pendingServiceApprovals: Array.from(
87
+ new Set(
88
+ organization.pendingServiceApprovals.map((approval) => approval.serviceType)
89
+ )
90
+ ).join("|")
73
91
  }
74
92
  ]),
75
93
  human: () => {
76
94
  console.log(`${organization.name} (slug: ${organization.slug})`);
95
+ console.log(` contactUrl: ${organization.contactUrl ?? "(not set)"}`);
77
96
  console.log(` subscriptionStatus: ${organization.subscriptionStatus}`);
78
97
  console.log(` services: ${summarizeServices(organization)}`);
79
98
  console.log(
80
99
  ` obligatoryAdminApproval: ${summarizeObligatoryAdminApproval(organization)}`
81
100
  );
101
+ console.log(
102
+ ` pendingServiceApprovals: ${summarizePendingApprovals(organization)}`
103
+ );
82
104
  }
83
105
  });
84
106
  }
@@ -29,6 +29,7 @@ var SINGLE_SERVICES = [
29
29
  "darkWebMonitoring"
30
30
  ];
31
31
  var ALL_SERVICES = [...DUAL_SERVICES, ...SINGLE_SERVICES];
32
+ var PENDING_APPROVAL_SERVICES = ["protection", "takedowns"];
32
33
  function splitCsv(value) {
33
34
  if (!value) return [];
34
35
  return value.split(",").map((part) => part.trim()).filter(Boolean);
@@ -155,6 +156,47 @@ function buildObligatoryAdminApprovalFilter(options) {
155
156
  if (assetTypeStrings.length > 0) filter.assetTypes = assetTypeStrings;
156
157
  return Object.keys(filter).length > 0 ? filter : void 0;
157
158
  }
159
+ function parsePendingApprovalServices(raw) {
160
+ const parts = splitCsv(raw);
161
+ const result = [];
162
+ for (const part of parts) {
163
+ if (!PENDING_APPROVAL_SERVICES.includes(part)) {
164
+ throw new CliExitError(
165
+ `Unknown service '${part}' in --pending-approval-service. Only ${PENDING_APPROVAL_SERVICES.join(", ")} can require approval.`,
166
+ ExitCode.USAGE
167
+ );
168
+ }
169
+ result.push(part);
170
+ }
171
+ return result;
172
+ }
173
+ function buildPendingServiceApprovalFilter(options) {
174
+ if (options.pendingApprovalActive && options.pendingApprovalInactive) {
175
+ throw new CliExitError(
176
+ "Conflicting flags: --pending-approval-active and --pending-approval-inactive can't both be passed.",
177
+ ExitCode.USAGE
178
+ );
179
+ }
180
+ const services = parsePendingApprovalServices(options.pendingApprovalService);
181
+ if (services.length > 0 && options.pendingApprovalInactive) {
182
+ throw new CliExitError(
183
+ "Conflicting flags: --pending-approval-service implies a pending approval exists, so it can't be combined with --pending-approval-inactive.",
184
+ ExitCode.USAGE
185
+ );
186
+ }
187
+ const filter = {};
188
+ if (options.pendingApprovalActive) filter.active = true;
189
+ if (options.pendingApprovalInactive) filter.active = false;
190
+ if (services.length > 0) filter.services = services;
191
+ return Object.keys(filter).length > 0 ? filter : void 0;
192
+ }
193
+ function summarizePendingApprovals(org) {
194
+ if (org.pendingServiceApprovals.length === 0) return "none";
195
+ const labels = Array.from(
196
+ new Set(org.pendingServiceApprovals.map((approval) => approval.serviceName))
197
+ );
198
+ return labels.join(", ");
199
+ }
158
200
  async function runOrgsList(options) {
159
201
  const client = options.apiClient ?? createApiClient();
160
202
  const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
@@ -162,7 +204,8 @@ async function runOrgsList(options) {
162
204
  query: options.query ?? "",
163
205
  subscriptionStatus: parseSubscriptionStatuses(options.subscriptionStatus),
164
206
  services: buildServicesFilter(options),
165
- obligatoryAdminApproval: buildObligatoryAdminApprovalFilter(options)
207
+ obligatoryAdminApproval: buildObligatoryAdminApprovalFilter(options),
208
+ pendingServiceApproval: buildPendingServiceApprovalFilter(options)
166
209
  };
167
210
  const result = await client.getUserOrgs(input);
168
211
  const organizations = result.organizations;
@@ -173,13 +216,14 @@ async function runOrgsList(options) {
173
216
  `# Organizations (${organizations.length})`,
174
217
  "",
175
218
  ...organizations.map(
176
- (org) => `- \`${org.slug}\` \u2014 ${org.name} \u2014 ${org.subscriptionStatus} \u2014 ${summarizeServices(org)} \u2014 admin-approval: ${summarizeObligatoryAdminApproval(org)}`
219
+ (org) => `- \`${org.slug}\` \u2014 ${org.name} \u2014 ${org.subscriptionStatus} \u2014 contact: ${org.contactUrl ?? "(not set)"} \u2014 ${summarizeServices(org)} \u2014 admin-approval: ${summarizeObligatoryAdminApproval(org)} \u2014 pending-approval: ${summarizePendingApprovals(org)}`
177
220
  )
178
221
  ].join("\n"),
179
222
  csv: toCsvRows(
180
223
  organizations.map((org) => ({
181
224
  slug: org.slug,
182
225
  name: org.name,
226
+ contactUrl: org.contactUrl ?? "",
183
227
  subscriptionStatus: org.subscriptionStatus,
184
228
  reportingActive: org.services.reporting.active,
185
229
  reviewingActive: org.services.reviewing.active,
@@ -193,7 +237,14 @@ async function runOrgsList(options) {
193
237
  // Empty list + active means "applies to all asset types"; we keep
194
238
  // the cell empty in CSV so spreadsheets render it as the natural
195
239
  // "no narrowing" value — pair it with the active column.
196
- obligatoryAdminApprovalAssetTypes: org.obligatoryAdminApproval.assetTypes.join("|")
240
+ obligatoryAdminApprovalAssetTypes: org.obligatoryAdminApproval.assetTypes.join("|"),
241
+ // Pending staff requests to enable Wallet Blocking / Takedowns that
242
+ // a higher-up has not yet approved. We emit the raw `serviceType`
243
+ // enum values (de-duplicated) so the column is machine-filterable;
244
+ // an empty cell means nothing is awaiting approval.
245
+ pendingServiceApprovals: Array.from(
246
+ new Set(org.pendingServiceApprovals.map((approval) => approval.serviceType))
247
+ ).join("|")
197
248
  }))
198
249
  ),
199
250
  human: () => {
@@ -205,8 +256,9 @@ async function runOrgsList(options) {
205
256
  const slugWidth = Math.max(...organizations.map((org) => org.slug.length), 4);
206
257
  const nameWidth = Math.max(...organizations.map((org) => org.name.length), 4);
207
258
  for (const org of organizations) {
259
+ const contact = org.contactUrl ?? "(no contact)";
208
260
  console.log(
209
- ` ${org.slug.padEnd(slugWidth)} ${org.name.padEnd(nameWidth)} ${org.subscriptionStatus.padEnd(11)} ${summarizeServices(org)} admin-approval=${summarizeObligatoryAdminApproval(org)}`
261
+ ` ${org.slug.padEnd(slugWidth)} ${org.name.padEnd(nameWidth)} ${org.subscriptionStatus.padEnd(11)} ${contact} ${summarizeServices(org)} admin-approval=${summarizeObligatoryAdminApproval(org)} pending-approval=${summarizePendingApprovals(org)}`
210
262
  );
211
263
  }
212
264
  }
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-4N2YKTJ5.js";
9
+ } from "./chunk-LKITFBIG.js";
10
10
  import "./chunk-6VTWWGNV.js";
11
11
  export {
12
12
  getBundledSkillContent,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@chainpatrol/cli",
3
3
  "description": "The official ChainPatrol CLI — terminal interface for threat detection",
4
4
  "author": "Umar Ahmed <umar@chainpatrol.io>",
5
- "version": "0.21.0",
5
+ "version": "0.22.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [