@chainpatrol/cli 0.19.0 → 0.21.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,54 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b3e77eb: `orgs list` and `orgs get` now surface each organization's "Obligatory
8
+ Organization Admin Approval" state (`Organization.requiresCustomerReview`)
9
+ alongside its per-asset-type scope. The new response field is
10
+ `obligatoryAdminApproval: { active, assetTypes }` — an empty `assetTypes`
11
+ list with `active: true` means approval applies to every asset type
12
+ (matches the in-app UI's behaviour when no narrowing has been configured).
13
+
14
+ `orgs list` also accepts three new filters that compose with the
15
+ existing ones via AND:
16
+
17
+ - `--obligatory-approval-active` / `--obligatory-approval-inactive` —
18
+ filter on the toggle itself.
19
+ - `--obligatory-approval-asset-type <csv>` — return only orgs whose
20
+ approval scope covers every listed asset type (an org's empty
21
+ per-asset-type list still counts as "all types"). Passing this flag
22
+ implies the feature is on, so it can't be combined with
23
+ `--obligatory-approval-inactive`.
24
+
25
+ Server-side work in `@chainpatrol/external-trpc` and `@chainpatrol/validation`
26
+ extends the `/user/orgs` and `/user/orgs/{slug}` schemas accordingly; both
27
+ are private packages and don't need version bumps.
28
+
29
+ ## 0.20.0
30
+
31
+ ### Minor Changes
32
+
33
+ - e63d269: Add `chainpatrol asset types` — list every supported `AssetType` enum
34
+ value with its human-readable label and a short description.
35
+
36
+ The mapping (e.g. `ARCHIVE_ORG` → "Archive.org" / Wayback snapshot,
37
+ `PAGE` → "Page" / standalone web page tracked independently of its
38
+ parent URL, `FIVE_HUNDRED_PX` → "500px") ships bundled with the CLI as
39
+ a static map, so the command works without auth or a network round
40
+ trip. Output is available in human, JSON, markdown, and CSV formats —
41
+ useful when an agent or script needs to translate enum ↔ display name,
42
+ validate a user-supplied `--asset-type` value, or enumerate the
43
+ available types before constructing a filter for `asset list`,
44
+ `threats list`, `takedowns list`, `detections list`, `reports list`,
45
+ or `orgs assets list`.
46
+
47
+ The Claude Code skill is updated to point at the new command whenever
48
+ a user asks "what is `ARCHIVE_ORG`?", "what does `PAGE` mean?", or
49
+ "what asset types does ChainPatrol support?". Shell completions and
50
+ top-level/per-command help text are updated accordingly.
51
+
3
52
  ## 0.19.0
4
53
 
5
54
  ### Minor Changes
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  installCompletions,
3
3
  uninstallCompletions
4
- } from "./chunk-NQT77IB7.js";
4
+ } from "./chunk-6VTWWGNV.js";
5
5
 
6
6
  // src/commands/setup-skill.ts
7
7
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
@@ -199,8 +199,17 @@ description: |
199
199
  "fetch org details", "org info",
200
200
  "orgs with takedowns off", "automation off across orgs",
201
201
  "which customers have X enabled", "service toggles by org",
202
+ "obligatory admin approval", "obligatory organization admin approval",
203
+ "obligatory approval", "admin approval enabled", "admin approval orgs",
204
+ "requires customer review", "requires admin approval",
205
+ "orgs that require admin approval", "orgs requiring approval",
206
+ "which orgs require approval for twitter", "approval scope",
207
+ "admin approval asset types", "approval per asset type",
202
208
  "is this URL blocked", "is this domain blocked", "is this address blocked",
203
209
  "check this asset", "asset check", "lookup asset status",
210
+ "what is ARCHIVE_ORG", "what does PAGE mean", "list asset types",
211
+ "supported asset types", "asset type mapping", "asset type enum",
212
+ "what asset types are there", "human readable asset type",
204
213
  "how many takedowns", "takedowns in the last", "threats taken down",
205
214
  "across all clients", "across all customers", "across all orgs",
206
215
  "across all brands", "company-wide", "total takedowns", "total threats",
@@ -383,6 +392,29 @@ If any individual lookup fails, the CLI still prints results for the
383
392
  successful ones, then exits non-zero so failures aren't silently
384
393
  swallowed.
385
394
 
395
+ ### \`asset types\` \u2014 List every supported asset type and what it means
396
+
397
+ When a user asks "what is \`ARCHIVE_ORG\`?", "what does \`PAGE\` mean?",
398
+ or "what asset types does ChainPatrol support?" \u2014 don't guess. Run:
399
+
400
+ \`\`\`bash
401
+ chainpatrol asset types
402
+ chainpatrol --json asset types
403
+ \`\`\`
404
+
405
+ The mapping is bundled with the CLI (no API or auth required), so this
406
+ is safe to run anywhere. Each row is \`{ type, label, description }\`:
407
+ \`type\` is the canonical enum value used by every \`--asset-type\` flag
408
+ (\`asset list\`, \`threats list\`, \`takedowns list\`, \`detections list\`,
409
+ \`reports list\`, \`orgs assets list\`); \`label\` is the human-friendly
410
+ display name (\`ARCHIVE_ORG\` \u2192 \`Archive.org\`, \`PAGE\` \u2192 \`Page\`,
411
+ \`FIVE_HUNDRED_PX\` \u2192 \`500px\`); \`description\` is a one-line note for
412
+ unobvious entries.
413
+
414
+ Use this whenever you need to translate between enum and display name,
415
+ validate that a type the user mentioned is real, or enumerate options
416
+ before constructing a filter.
417
+
386
418
  ### \`configs list\` \u2014 List detection configurations
387
419
 
388
420
  Requires authentication and an organization slug.
@@ -630,14 +662,17 @@ Guide. Key signals in the response:
630
662
 
631
663
  Use \`--all\` to snapshot every org you have access to instead of a single slug.
632
664
 
633
- ### \`orgs list\` \u2014 List organizations with subscription status and service toggles
665
+ ### \`orgs list\` \u2014 List organizations with subscription status, service toggles, and admin-approval scope
634
666
 
635
667
  Returns every organization the caller can see, with each org's
636
- subscription status (\`PROSPECT\`, \`TRIAL\`, \`ACTIVE\`, \`INTEGRATION\`) and
637
- which services are active. Use it to answer questions like "which
638
- customers have takedowns enabled but automation off?" or "which prospects
639
- don't have detection turned on yet?" \u2014 filters compose with AND and are
640
- applied server-side, so one call returns the final list.
668
+ subscription status (\`PROSPECT\`, \`TRIAL\`, \`ACTIVE\`, \`INTEGRATION\`),
669
+ which services are active, and the per-org "Obligatory Organization
670
+ Admin Approval" toggle (with its asset-type scope). Use it to answer
671
+ questions like "which customers have takedowns enabled but automation
672
+ off?", "which prospects don't have detection turned on yet?", or "which
673
+ orgs require admin approval before adding Twitter assets to the
674
+ blocklist?" \u2014 filters compose with AND and are applied server-side, so
675
+ one call returns the final list.
641
676
 
642
677
  \`\`\`bash
643
678
  chainpatrol --json orgs list \\
@@ -660,6 +695,34 @@ misleading filters like "reporting.automated=true" that don't mean
660
695
  anything. \`detection\` and \`darkWebMonitoring\` have always been
661
696
  single-flag services.
662
697
 
698
+ #### Obligatory Organization Admin Approval
699
+
700
+ Each org also has a separate "Obligatory Organization Admin Approval"
701
+ toggle (\`Organization.requiresCustomerReview\` in the schema, surfaced
702
+ on the Services settings page in the app) that gates customer-side
703
+ review of proposals ChainPatrol staff have already approved before
704
+ they're added to the blocklist. It is NOT one of the operational
705
+ services above \u2014 it is an org-policy toggle with its own per-asset-type
706
+ scope.
707
+
708
+ Each org's response includes:
709
+
710
+ \`\`\`json
711
+ {
712
+ "obligatoryAdminApproval": {
713
+ "active": true,
714
+ "assetTypes": ["TWITTER", "URL"]
715
+ }
716
+ }
717
+ \`\`\`
718
+
719
+ - \`active=true\` + empty \`assetTypes\` means approval applies to
720
+ **all** asset types \u2014 the org has not narrowed the scope.
721
+ - \`active=true\` + a non-empty \`assetTypes\` list means approval is
722
+ required only for those asset types.
723
+ - \`active=false\` means no approval is required; \`assetTypes\` is
724
+ always empty in that case.
725
+
663
726
  So per-org JSON looks like:
664
727
 
665
728
  \`\`\`json
@@ -671,7 +734,8 @@ So per-org JSON looks like:
671
734
  "takedowns": { "active": true, "automated": false },
672
735
  "detection": { "active": true },
673
736
  "darkWebMonitoring": { "active": false }
674
- }
737
+ },
738
+ "obligatoryAdminApproval": { "active": true, "assetTypes": ["TWITTER"] }
675
739
  }
676
740
  \`\`\`
677
741
 
@@ -680,7 +744,7 @@ explain that the flag exists in the DB but has no operational effect and
680
744
  isn't exposed by the public API \u2014 only \`takedowns.automated\` carries a
681
745
  real meaning.
682
746
 
683
- Filter flags (all optional, all comma-separated lists):
747
+ Filter flags (all optional, all comma-separated lists where noted):
684
748
 
685
749
  - \`--query <text>\` partial name match (substring, case-insensitive)
686
750
  - \`--subscription-status <list>\` one or more of \`PROSPECT\`, \`TRIAL\`,
@@ -693,6 +757,16 @@ Filter flags (all optional, all comma-separated lists):
693
757
  meaningful automation toggle. Passing any other service name errors out.
694
758
  - \`--service-manual <list>\` services whose automation must be OFF.
695
759
  Same restriction \u2014 only \`takedowns\`.
760
+ - \`--obligatory-approval-active\` \u2014 only orgs with admin approval on
761
+ - \`--obligatory-approval-inactive\` \u2014 only orgs with admin approval off
762
+ - \`--obligatory-approval-asset-type <list>\` \u2014 only orgs whose admin
763
+ approval scope covers EVERY listed asset type. The match treats an
764
+ org's empty per-asset-type list as "applies to all asset types", so a
765
+ fully-broad org matches every value passed here. Passing this flag
766
+ implies the feature is on, so it can't be combined with
767
+ \`--obligatory-approval-inactive\`. Use the canonical asset-type enum
768
+ names (\`TWITTER\`, \`URL\`, \`PAGE\`, \u2026); run \`chainpatrol asset types\`
769
+ to see them all.
696
770
 
697
771
  Service names: \`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\`,
698
772
  \`detection\`, \`darkWebMonitoring\`.
@@ -701,16 +775,36 @@ Customers see only orgs they're a member of. Staff/superuser sessions see
701
775
  every matching org. The response is the same in both cases; visibility is
702
776
  enforced server-side.
703
777
 
778
+ #### Use case: finding which orgs require admin approval for an asset type
779
+
780
+ When a user asks "which orgs require admin approval before blocking
781
+ Twitter assets?", reach straight for the filter \u2014 no client-side
782
+ post-processing needed:
783
+
784
+ \`\`\`bash
785
+ chainpatrol --json orgs list --obligatory-approval-asset-type TWITTER
786
+ \`\`\`
787
+
788
+ To audit just the broad opt-ins ("which orgs require approval for
789
+ everything?"), filter on the toggle and inspect \`assetTypes\` in the
790
+ output \u2014 an empty array means "all":
791
+
792
+ \`\`\`bash
793
+ chainpatrol --json orgs list --obligatory-approval-active \\
794
+ | jq '.organizations[] | select(.obligatoryAdminApproval.assetTypes | length == 0) | .slug'
795
+ \`\`\`
796
+
704
797
  ### \`orgs get\` \u2014 Get a single organization by slug
705
798
 
706
799
  Look up one organization the caller has access to. Returns the same per-org
707
800
  shape as a single row from \`orgs list\` \u2014 \`active\` for every service,
708
801
  plus \`automated\` on \`takedowns\` only (see the note above on why other
709
- services don't expose an automation flag). Anything you could read from
710
- \`orgs list\` you can also read here without paging or filtering. Use it
711
- when the user names a specific customer ("show me acme's setup", "is
712
- takedowns automation on for morpho?") and you don't need the rest of the
713
- catalogue.
802
+ 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.
714
808
 
715
809
  \`\`\`bash
716
810
  chainpatrol orgs get <slug>
@@ -66,6 +66,7 @@ _chainpatrol() {
66
66
  asset_subcommands=(
67
67
  'check:Check asset status against ChainPatrol'
68
68
  'list:List assets in the global ChainPatrol blocklist'
69
+ 'types:List supported asset types and their human-readable labels'
69
70
  )
70
71
  _describe 'subcommand' asset_subcommands
71
72
  ;;
@@ -161,7 +162,7 @@ var BASH_COMPLETION = `_chainpatrol() {
161
162
  return 0
162
163
  ;;
163
164
  asset)
164
- COMPREPLY=( $(compgen -W "check list" -- "\${cur}") )
165
+ COMPREPLY=( $(compgen -W "check list types" -- "\${cur}") )
165
166
  return 0
166
167
  ;;
167
168
  configs)
package/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-UDGJEN4M.js";
17
- import "./chunk-NQT77IB7.js";
16
+ } from "./chunk-4N2YKTJ5.js";
17
+ import "./chunk-6VTWWGNV.js";
18
18
  import {
19
19
  DateTime
20
20
  } from "./chunk-TFCNKBRC.js";
@@ -92,14 +92,25 @@ var HELP = {
92
92
  usage: "chainpatrol logout"
93
93
  },
94
94
  asset: {
95
- description: "Check asset status or list blocked/allowed assets across the global ChainPatrol blocklist.",
96
- usage: "chainpatrol asset <check|list>",
95
+ description: "Check asset status, list blocked/allowed assets across the global ChainPatrol blocklist, or list every supported asset type.",
96
+ usage: "chainpatrol asset <check|list|types>",
97
97
  examples: [
98
98
  "chainpatrol asset check https://phish.example",
99
99
  "chainpatrol asset list --asset-type URL --from 2026-05-01",
100
+ "chainpatrol asset types",
100
101
  "chainpatrol --json asset check 0xabc123\u2026"
101
102
  ]
102
103
  },
104
+ "asset types": {
105
+ description: "List every asset type supported by ChainPatrol and what each enum value means (e.g. ARCHIVE_ORG \u2192 Archive.org Wayback snapshot, PAGE \u2192 standalone web page). Reads from a static map bundled with the CLI \u2014 no API or auth required. Useful for picking the right --asset-type value for `asset list`, `threats list`, `takedowns list`, etc.",
106
+ usage: "chainpatrol asset types",
107
+ examples: [
108
+ "chainpatrol asset types",
109
+ "chainpatrol --json asset types",
110
+ "chainpatrol asset types --output markdown",
111
+ "chainpatrol asset types --output csv > asset-types.csv"
112
+ ]
113
+ },
103
114
  "asset list": {
104
115
  description: "List assets in the global ChainPatrol blocklist for a given asset type. Defaults to BLOCKED assets updated in the last day. Use --from and --to to widen the window.",
105
116
  usage: "chainpatrol asset list --asset-type <type> [--from ISO --to ISO] [filters]",
@@ -636,7 +647,7 @@ function getTopLevelHelp() {
636
647
  " login Authenticate with ChainPatrol",
637
648
  " logout Clear stored credentials",
638
649
  " whoami Show the current authenticated identity",
639
- " asset Check or list assets in the global ChainPatrol blocklist",
650
+ " asset Check or list assets, or list supported asset types",
640
651
  " configs Manage detection configs",
641
652
  " detections Validate, run, list, update detection configs and results",
642
653
  " healthchecks List and run organization healthchecks",
@@ -905,6 +916,9 @@ ${getTopLevelHelp()}
905
916
  serviceInactive: { type: "string" },
906
917
  serviceAutomated: { type: "string" },
907
918
  serviceManual: { type: "string" },
919
+ obligatoryApprovalActive: { type: "boolean" },
920
+ obligatoryApprovalInactive: { type: "boolean" },
921
+ obligatoryApprovalAssetType: { type: "string" },
908
922
  query: { type: "string" },
909
923
  assetType: { type: "string" },
910
924
  takedownStatus: { type: "string" },
@@ -1183,9 +1197,17 @@ async function main() {
1183
1197
  });
1184
1198
  break;
1185
1199
  }
1186
- const hint = subcommand ? suggest(subcommand, ["check", "list"]) : null;
1200
+ if (subcommand === "types") {
1201
+ const { runAssetTypes } = await import("./types-PTMFLYUV.js");
1202
+ runAssetTypes({
1203
+ json: jsonMode,
1204
+ outputFormat: cliContext.outputFormat
1205
+ });
1206
+ break;
1207
+ }
1208
+ const hint = subcommand ? suggest(subcommand, ["check", "list", "types"]) : null;
1187
1209
  throw new Error(
1188
- subcommand ? `Unknown subcommand: asset ${subcommand}${hint ? `. Did you mean "asset ${hint}"?` : ""}` : "Usage: chainpatrol asset <check|list>"
1210
+ subcommand ? `Unknown subcommand: asset ${subcommand}${hint ? `. Did you mean "asset ${hint}"?` : ""}` : "Usage: chainpatrol asset <check|list|types>"
1189
1211
  );
1190
1212
  }
1191
1213
  case "detections": {
@@ -1539,7 +1561,7 @@ async function main() {
1539
1561
  }
1540
1562
  case "orgs": {
1541
1563
  if (subcommand === "list") {
1542
- const { runOrgsList } = await import("./list-QXA6GLCV.js");
1564
+ const { runOrgsList } = await import("./list-MHICJ2TK.js");
1543
1565
  await runOrgsList({
1544
1566
  query: cli.flags.query,
1545
1567
  subscriptionStatus: cli.flags.subscriptionStatus,
@@ -1547,6 +1569,9 @@ async function main() {
1547
1569
  serviceInactive: cli.flags.serviceInactive,
1548
1570
  serviceAutomated: cli.flags.serviceAutomated,
1549
1571
  serviceManual: cli.flags.serviceManual,
1572
+ obligatoryApprovalActive: cli.flags.obligatoryApprovalActive,
1573
+ obligatoryApprovalInactive: cli.flags.obligatoryApprovalInactive,
1574
+ obligatoryApprovalAssetType: cli.flags.obligatoryApprovalAssetType,
1550
1575
  json: jsonMode,
1551
1576
  outputFormat: cliContext.outputFormat
1552
1577
  });
@@ -1555,7 +1580,7 @@ async function main() {
1555
1580
  if (subcommand === "get") {
1556
1581
  const positionalSlug = action;
1557
1582
  const slug = positionalSlug ?? await tryResolveOrg();
1558
- const { runOrgsGet } = await import("./get-K7VHV5X2.js");
1583
+ const { runOrgsGet } = await import("./get-XN752KSL.js");
1559
1584
  await runOrgsGet({
1560
1585
  slug,
1561
1586
  json: jsonMode,
@@ -1686,17 +1711,17 @@ async function main() {
1686
1711
  case "setup":
1687
1712
  case "install":
1688
1713
  case "i": {
1689
- const { setupSkill } = await import("./setup-skill-F7437RT3.js");
1714
+ const { setupSkill } = await import("./setup-skill-UEOROLCF.js");
1690
1715
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1691
1716
  break;
1692
1717
  }
1693
1718
  case "uninstall": {
1694
- const { uninstallSkill } = await import("./setup-skill-F7437RT3.js");
1719
+ const { uninstallSkill } = await import("./setup-skill-UEOROLCF.js");
1695
1720
  uninstallSkill({ json: jsonMode });
1696
1721
  break;
1697
1722
  }
1698
1723
  case "completions": {
1699
- const { printCompletions } = await import("./completions-6C6KG5DH.js");
1724
+ const { printCompletions } = await import("./completions-ILC76OX5.js");
1700
1725
  printCompletions(subcommand);
1701
1726
  break;
1702
1727
  }
@@ -3,7 +3,7 @@ import {
3
3
  installCompletions,
4
4
  printCompletions,
5
5
  uninstallCompletions
6
- } from "./chunk-NQT77IB7.js";
6
+ } from "./chunk-6VTWWGNV.js";
7
7
  export {
8
8
  getCompletionScript,
9
9
  installCompletions,
@@ -26,6 +26,11 @@ function summarizeServices(org) {
26
26
  if (org.services.darkWebMonitoring.active) parts.push("darkWebMonitoring");
27
27
  return parts.length > 0 ? parts.join(" ") : "(no services enabled)";
28
28
  }
29
+ function summarizeObligatoryAdminApproval(org) {
30
+ if (!org.obligatoryAdminApproval.active) return "off";
31
+ const types = org.obligatoryAdminApproval.assetTypes;
32
+ return types.length === 0 ? "on (all asset types)" : `on (${types.join(", ")})`;
33
+ }
29
34
  async function runOrgsGet(options) {
30
35
  const slug = options.slug?.trim();
31
36
  if (!slug) {
@@ -45,7 +50,8 @@ async function runOrgsGet(options) {
45
50
  `# ${organization.name} (\`${organization.slug}\`)`,
46
51
  "",
47
52
  `- Subscription status: ${organization.subscriptionStatus}`,
48
- `- Services: ${summarizeServices(organization)}`
53
+ `- Services: ${summarizeServices(organization)}`,
54
+ `- Obligatory admin approval: ${summarizeObligatoryAdminApproval(organization)}`
49
55
  ].join("\n"),
50
56
  csv: toCsvRows([
51
57
  {
@@ -59,13 +65,20 @@ async function runOrgsGet(options) {
59
65
  // `takedowns` is the only service with a meaningful automation flag.
60
66
  takedownsAutomated: organization.services.takedowns.automated,
61
67
  detectionActive: organization.services.detection.active,
62
- darkWebMonitoringActive: organization.services.darkWebMonitoring.active
68
+ darkWebMonitoringActive: organization.services.darkWebMonitoring.active,
69
+ obligatoryAdminApprovalActive: organization.obligatoryAdminApproval.active,
70
+ // Empty list + active=true means "applies to all asset types" — read
71
+ // the active column alongside this one to interpret.
72
+ obligatoryAdminApprovalAssetTypes: organization.obligatoryAdminApproval.assetTypes.join("|")
63
73
  }
64
74
  ]),
65
75
  human: () => {
66
76
  console.log(`${organization.name} (slug: ${organization.slug})`);
67
77
  console.log(` subscriptionStatus: ${organization.subscriptionStatus}`);
68
78
  console.log(` services: ${summarizeServices(organization)}`);
79
+ console.log(
80
+ ` obligatoryAdminApproval: ${summarizeObligatoryAdminApproval(organization)}`
81
+ );
69
82
  }
70
83
  });
71
84
  }
@@ -128,13 +128,41 @@ function summarizeServices(org) {
128
128
  }
129
129
  return parts.length > 0 ? parts.join(" ") : "(no services enabled)";
130
130
  }
131
+ function summarizeObligatoryAdminApproval(org) {
132
+ if (!org.obligatoryAdminApproval.active) return "off";
133
+ const types = org.obligatoryAdminApproval.assetTypes;
134
+ return types.length === 0 ? "on (all asset types)" : `on (${types.join(", ")})`;
135
+ }
136
+ function buildObligatoryAdminApprovalFilter(options) {
137
+ if (options.obligatoryApprovalActive && options.obligatoryApprovalInactive) {
138
+ throw new CliExitError(
139
+ "Conflicting flags: --obligatory-approval-active and --obligatory-approval-inactive can't both be passed.",
140
+ ExitCode.USAGE
141
+ );
142
+ }
143
+ const assetTypeStrings = splitCsv(options.obligatoryApprovalAssetType).map(
144
+ (value) => value.toUpperCase()
145
+ );
146
+ if (assetTypeStrings.length > 0 && options.obligatoryApprovalInactive) {
147
+ throw new CliExitError(
148
+ "Conflicting flags: --obligatory-approval-asset-type implies the feature is on, so it can't be combined with --obligatory-approval-inactive.",
149
+ ExitCode.USAGE
150
+ );
151
+ }
152
+ const filter = {};
153
+ if (options.obligatoryApprovalActive) filter.active = true;
154
+ if (options.obligatoryApprovalInactive) filter.active = false;
155
+ if (assetTypeStrings.length > 0) filter.assetTypes = assetTypeStrings;
156
+ return Object.keys(filter).length > 0 ? filter : void 0;
157
+ }
131
158
  async function runOrgsList(options) {
132
159
  const client = options.apiClient ?? createApiClient();
133
160
  const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
134
161
  const input = {
135
162
  query: options.query ?? "",
136
163
  subscriptionStatus: parseSubscriptionStatuses(options.subscriptionStatus),
137
- services: buildServicesFilter(options)
164
+ services: buildServicesFilter(options),
165
+ obligatoryAdminApproval: buildObligatoryAdminApprovalFilter(options)
138
166
  };
139
167
  const result = await client.getUserOrgs(input);
140
168
  const organizations = result.organizations;
@@ -145,7 +173,7 @@ async function runOrgsList(options) {
145
173
  `# Organizations (${organizations.length})`,
146
174
  "",
147
175
  ...organizations.map(
148
- (org) => `- \`${org.slug}\` \u2014 ${org.name} \u2014 ${org.subscriptionStatus} \u2014 ${summarizeServices(org)}`
176
+ (org) => `- \`${org.slug}\` \u2014 ${org.name} \u2014 ${org.subscriptionStatus} \u2014 ${summarizeServices(org)} \u2014 admin-approval: ${summarizeObligatoryAdminApproval(org)}`
149
177
  )
150
178
  ].join("\n"),
151
179
  csv: toCsvRows(
@@ -160,7 +188,12 @@ async function runOrgsList(options) {
160
188
  // `takedowns` is the only service with a meaningful automation flag.
161
189
  takedownsAutomated: org.services.takedowns.automated,
162
190
  detectionActive: org.services.detection.active,
163
- darkWebMonitoringActive: org.services.darkWebMonitoring.active
191
+ darkWebMonitoringActive: org.services.darkWebMonitoring.active,
192
+ obligatoryAdminApprovalActive: org.obligatoryAdminApproval.active,
193
+ // Empty list + active means "applies to all asset types"; we keep
194
+ // the cell empty in CSV so spreadsheets render it as the natural
195
+ // "no narrowing" value — pair it with the active column.
196
+ obligatoryAdminApprovalAssetTypes: org.obligatoryAdminApproval.assetTypes.join("|")
164
197
  }))
165
198
  ),
166
199
  human: () => {
@@ -173,7 +206,7 @@ async function runOrgsList(options) {
173
206
  const nameWidth = Math.max(...organizations.map((org) => org.name.length), 4);
174
207
  for (const org of organizations) {
175
208
  console.log(
176
- ` ${org.slug.padEnd(slugWidth)} ${org.name.padEnd(nameWidth)} ${org.subscriptionStatus.padEnd(11)} ${summarizeServices(org)}`
209
+ ` ${org.slug.padEnd(slugWidth)} ${org.name.padEnd(nameWidth)} ${org.subscriptionStatus.padEnd(11)} ${summarizeServices(org)} admin-approval=${summarizeObligatoryAdminApproval(org)}`
177
210
  );
178
211
  }
179
212
  }
@@ -6,8 +6,8 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-UDGJEN4M.js";
10
- import "./chunk-NQT77IB7.js";
9
+ } from "./chunk-4N2YKTJ5.js";
10
+ import "./chunk-6VTWWGNV.js";
11
11
  export {
12
12
  getBundledSkillContent,
13
13
  getBundledSkillVersion,
@@ -0,0 +1,271 @@
1
+ import {
2
+ printOutput,
3
+ toCsvRows
4
+ } from "./chunk-VFT3TD3E.js";
5
+
6
+ // src/lib/asset-types.ts
7
+ var ASSET_TYPES = [
8
+ {
9
+ type: "URL",
10
+ label: "URL",
11
+ description: "A full URL or domain (web page). The most common asset type for phishing sites."
12
+ },
13
+ {
14
+ type: "PAGE",
15
+ label: "Page",
16
+ description: "A standalone web page tracked independently of its parent URL (e.g. a hosted phishing kit deployed on a shared platform)."
17
+ },
18
+ {
19
+ type: "ADDRESS",
20
+ label: "Address",
21
+ description: "A blockchain address (EVM, Bitcoin, Solana, etc.) used by a threat."
22
+ },
23
+ {
24
+ type: "DISCORD",
25
+ label: "Discord",
26
+ description: "A Discord server invite or guild."
27
+ },
28
+ { type: "LINKEDIN", label: "LinkedIn", description: "A LinkedIn profile or page." },
29
+ { type: "TWITTER", label: "Twitter", description: "An X (formerly Twitter) account." },
30
+ { type: "FACEBOOK", label: "Facebook", description: "A Facebook page or profile." },
31
+ { type: "YOUTUBE", label: "YouTube", description: "A YouTube channel or video." },
32
+ { type: "REDDIT", label: "Reddit", description: "A Reddit user or subreddit." },
33
+ {
34
+ type: "TELEGRAM",
35
+ label: "Telegram",
36
+ description: "A Telegram channel, group, or user."
37
+ },
38
+ {
39
+ type: "GOOGLE_APP_STORE",
40
+ label: "Google Play Store",
41
+ description: "An Android app on Google Play."
42
+ },
43
+ {
44
+ type: "APPLE_APP_STORE",
45
+ label: "Apple App Store",
46
+ description: "An iOS app on the Apple App Store."
47
+ },
48
+ {
49
+ type: "AMAZON_APP_STORE",
50
+ label: "Amazon App Store",
51
+ description: "An Android app on the Amazon Appstore."
52
+ },
53
+ {
54
+ type: "MICROSOFT_APP_STORE",
55
+ label: "Microsoft App Store",
56
+ description: "An app in the Microsoft Store."
57
+ },
58
+ { type: "TIKTOK", label: "TikTok", description: "A TikTok account or video." },
59
+ { type: "INSTAGRAM", label: "Instagram", description: "An Instagram account." },
60
+ { type: "THREADS", label: "Threads", description: "A Threads (Meta) account." },
61
+ { type: "MEDIUM", label: "Medium", description: "A Medium blog or post." },
62
+ {
63
+ type: "CHROME_WEB_STORE",
64
+ label: "Chrome Web Store",
65
+ description: "A Chrome browser extension."
66
+ },
67
+ {
68
+ type: "MOZILLA_ADDONS",
69
+ label: "Mozilla Firefox Addons",
70
+ description: "A Firefox browser add-on."
71
+ },
72
+ {
73
+ type: "OPERA_ADDONS",
74
+ label: "Opera Addons",
75
+ description: "An Opera browser add-on."
76
+ },
77
+ { type: "EMAIL", label: "Email", description: "An email address." },
78
+ { type: "PATREON", label: "Patreon", description: "A Patreon page." },
79
+ { type: "OPENSEA", label: "OpenSea", description: "An OpenSea collection or account." },
80
+ { type: "FARCASTER", label: "Farcaster", description: "A Farcaster account." },
81
+ {
82
+ type: "IPFS",
83
+ label: "IPFS",
84
+ description: "An IPFS-hosted resource (CID or gateway URL)."
85
+ },
86
+ { type: "GOOGLE_FORM", label: "Google Form", description: "A Google Forms form." },
87
+ {
88
+ type: "WHATSAPP",
89
+ label: "WhatsApp",
90
+ description: "A WhatsApp number, group, or channel."
91
+ },
92
+ { type: "DISCORD_USER", label: "Discord User", description: "A Discord user account." },
93
+ { type: "QUORA", label: "Quora", description: "A Quora user or space." },
94
+ {
95
+ type: "GITHUB",
96
+ label: "GitHub",
97
+ description: "A GitHub account, repository, or page."
98
+ },
99
+ { type: "TEACHABLE", label: "Teachable", description: "A Teachable course or school." },
100
+ { type: "SUBSTACK", label: "Substack", description: "A Substack publication." },
101
+ { type: "DEBANK", label: "Debank", description: "A Debank profile." },
102
+ { type: "TAWK_TO", label: "Tawk.to Chat", description: "A Tawk.to live-chat widget." },
103
+ { type: "JOTFORM", label: "Jotform", description: "A Jotform form." },
104
+ { type: "PRIMAL", label: "Primal", description: "A Primal (Nostr) profile." },
105
+ { type: "BLUESKY", label: "Bluesky", description: "A Bluesky account." },
106
+ { type: "SNAPCHAT", label: "Snapchat", description: "A Snapchat account." },
107
+ { type: "DESO", label: "DeSo", description: "A DeSo profile." },
108
+ { type: "PINTEREST", label: "Pinterest", description: "A Pinterest account or board." },
109
+ { type: "FLICKR", label: "Flickr", description: "A Flickr account." },
110
+ { type: "GALXE", label: "Galxe", description: "A Galxe campaign or space." },
111
+ { type: "VELOG", label: "Velog", description: "A Velog blog or post." },
112
+ { type: "NPM", label: "npm", description: "An npm package." },
113
+ { type: "PYPI", label: "PyPI", description: "A PyPI Python package." },
114
+ { type: "HEX", label: "Hex", description: "A Hex Erlang/Elixir package." },
115
+ { type: "DOCKER_HUB", label: "Docker Hub", description: "A Docker Hub image." },
116
+ {
117
+ type: "VOCAL_MEDIA",
118
+ label: "Vocal Media",
119
+ description: "A Vocal Media profile or story."
120
+ },
121
+ { type: "TECKFINE", label: "Teckfine", description: "A Teckfine listing." },
122
+ {
123
+ type: "TENDERLY",
124
+ label: "Tenderly",
125
+ description: "A Tenderly dashboard or simulation."
126
+ },
127
+ { type: "HACKMD", label: "HackMD", description: "A HackMD note." },
128
+ { type: "ETSY", label: "Etsy", description: "An Etsy shop or listing." },
129
+ { type: "ZAZZLE", label: "Zazzle", description: "A Zazzle shop or product." },
130
+ { type: "BASENAME", label: "Basename", description: "A Base ENS (basename) record." },
131
+ {
132
+ type: "BILIBILI_TV",
133
+ label: "Bilibili TV",
134
+ description: "A Bilibili TV channel or video."
135
+ },
136
+ { type: "VIMEO", label: "Vimeo", description: "A Vimeo video or channel." },
137
+ {
138
+ type: "DAILYMOTION",
139
+ label: "Dailymotion",
140
+ description: "A Dailymotion video or channel."
141
+ },
142
+ { type: "PHONE_NUMBER", label: "Phone Number", description: "A phone number." },
143
+ { type: "SLACK", label: "Slack", description: "A Slack workspace invite." },
144
+ { type: "CALENDLY", label: "Calendly", description: "A Calendly scheduling page." },
145
+ {
146
+ type: "NGROK",
147
+ label: "Ngrok",
148
+ description: "An ngrok tunnel (often used to host short-lived phishing endpoints)."
149
+ },
150
+ { type: "RARIBLE", label: "Rarible", description: "A Rarible collection or profile." },
151
+ {
152
+ type: "RUST_PACKAGE",
153
+ label: "Rust Package",
154
+ description: "A crates.io Rust package."
155
+ },
156
+ { type: "FLATHUB", label: "Flathub", description: "A Flathub Linux app listing." },
157
+ { type: "VIDLII", label: "VidLii", description: "A VidLii video or channel." },
158
+ { type: "VEVIOZ", label: "Vevioz", description: "A Vevioz listing." },
159
+ { type: "ISSUU", label: "Issuu", description: "An Issuu publication." },
160
+ {
161
+ type: "SOUNDCLOUD",
162
+ label: "SoundCloud",
163
+ description: "A SoundCloud account or track."
164
+ },
165
+ { type: "ZAPPER", label: "Zapper", description: "A Zapper profile or bundle." },
166
+ { type: "REDNOTE", label: "RedNote", description: "A RedNote (Xiaohongshu) account." },
167
+ {
168
+ type: "SAMSUNG_APP_STORE",
169
+ label: "Samsung Galaxy Store",
170
+ description: "An app on Samsung Galaxy Store."
171
+ },
172
+ {
173
+ type: "HUAWEI_APP_STORE",
174
+ label: "Huawei AppGallery",
175
+ description: "An app on Huawei AppGallery."
176
+ },
177
+ {
178
+ type: "XIAOMI_APP_STORE",
179
+ label: "Xiaomi GetApps",
180
+ description: "An app on Xiaomi GetApps."
181
+ },
182
+ {
183
+ type: "TENCENT_APP_STORE",
184
+ label: "Tencent App Store",
185
+ description: "An app on the Tencent App Store."
186
+ },
187
+ {
188
+ type: "OPPO_APP_STORE",
189
+ label: "OPPO App Market",
190
+ description: "An app on OPPO App Market."
191
+ },
192
+ {
193
+ type: "VIVO_APP_STORE",
194
+ label: "Vivo App Store",
195
+ description: "An app on Vivo App Store."
196
+ },
197
+ { type: "F_DROID", label: "F-Droid", description: "An app on F-Droid." },
198
+ { type: "GOOGLE_AD", label: "Google Ad", description: "A Google Ads advertisement." },
199
+ { type: "BING_AD", label: "Bing Ad", description: "A Microsoft Bing advertisement." },
200
+ { type: "TWITCH", label: "Twitch", description: "A Twitch channel." },
201
+ { type: "BEHANCE", label: "Behance", description: "A Behance profile or project." },
202
+ { type: "ZORA", label: "Zora", description: "A Zora collection or profile." },
203
+ {
204
+ type: "META_AD",
205
+ label: "Meta Ad",
206
+ description: "A Facebook/Instagram (Meta) advertisement."
207
+ },
208
+ { type: "SIGNAL", label: "Signal", description: "A Signal group invite or contact." },
209
+ { type: "DEVIANTART", label: "DeviantArt", description: "A DeviantArt profile." },
210
+ { type: "BANDCAMP", label: "Bandcamp", description: "A Bandcamp page." },
211
+ {
212
+ type: "ARCHIVE_ORG",
213
+ label: "Archive.org",
214
+ description: "An Internet Archive (archive.org) snapshot \u2014 typically a Wayback Machine URL preserving a phishing page."
215
+ },
216
+ {
217
+ type: "FIVE_HUNDRED_PX",
218
+ label: "500px",
219
+ description: "A 500px photographer profile."
220
+ }
221
+ ];
222
+ var ASSET_TYPE_LABELS = Object.freeze(
223
+ Object.fromEntries(ASSET_TYPES.map((entry) => [entry.type, entry.label]))
224
+ );
225
+
226
+ // src/commands/asset/types.ts
227
+ function toRow(entry) {
228
+ return {
229
+ type: entry.type,
230
+ label: entry.label,
231
+ description: entry.description ?? ""
232
+ };
233
+ }
234
+ function runAssetTypes(options) {
235
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
236
+ const rows = ASSET_TYPES.map(toRow);
237
+ const escapeMarkdownCell = (value) => value.replaceAll("|", "\\|");
238
+ printOutput({
239
+ outputFormat,
240
+ json: { assetTypes: ASSET_TYPES, count: ASSET_TYPES.length },
241
+ markdown: [
242
+ "# ChainPatrol asset types",
243
+ "",
244
+ `Total: ${ASSET_TYPES.length}`,
245
+ "",
246
+ "| Type | Label | Description |",
247
+ "| --- | --- | --- |",
248
+ ...rows.map(
249
+ (row) => `| \`${row.type}\` | ${escapeMarkdownCell(row.label)} | ${escapeMarkdownCell(row.description)} |`
250
+ )
251
+ ].join("\n"),
252
+ csv: toCsvRows(rows),
253
+ human: () => {
254
+ const typeColumnWidth = Math.max(...ASSET_TYPES.map((entry) => entry.type.length));
255
+ const labelColumnWidth = Math.max(
256
+ ...ASSET_TYPES.map((entry) => entry.label.length)
257
+ );
258
+ console.log(`ChainPatrol asset types (${ASSET_TYPES.length})`);
259
+ console.log("");
260
+ for (const entry of ASSET_TYPES) {
261
+ const type = entry.type.padEnd(typeColumnWidth);
262
+ const label = entry.label.padEnd(labelColumnWidth);
263
+ const description = entry.description ? ` ${entry.description}` : "";
264
+ console.log(`${type} ${label}${description}`);
265
+ }
266
+ }
267
+ });
268
+ }
269
+ export {
270
+ runAssetTypes
271
+ };
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.19.0",
5
+ "version": "0.21.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [