@knowttl/checkpoint-axi 0.1.1 → 0.1.3

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 CHANGED
@@ -59,6 +59,9 @@ Use the hook, the skill or both; neither is required for ordinary CLI use.
59
59
  Start investigations with the compact [agent guide](docs/agent-guide.md), linked from [SKILL.md](skills/checkpoint-axi/SKILL.md).
60
60
  Use the [command reference](docs/commands.md) for the full catalogue.
61
61
 
62
+ Email search and event list support opt-in read-only detail expansion with `--expand`.
63
+ See [bounded detail expansion](docs/reads.md#bounded-detail-expansion) for limits, shared budgets, partial results and examples.
64
+
62
65
  ## What it covers
63
66
 
64
67
  <!-- command-registry:start -->
@@ -73,7 +76,23 @@ Use the [command reference](docs/commands.md) for the full catalogue.
73
76
  | exception | Inspect eight kinds; preview or request supported rule changes | Read / gated write | [Guide](docs/writes.md) |
74
77
  | home | Show local profile context without resolving secrets | Local read | [Guide](docs/configuration.md) |
75
78
  | profile | List named file profiles | Local read | [Guide](docs/configuration.md) |
76
- | task | Inspect asynchronous action status without polling | Read | [Guide](docs/reads.md#event-and-task-reads) |
79
+ | task | Inspect asynchronous action status once or with bounded read-only waiting | Read | [Guide](docs/reads.md#event-and-task-reads) |
80
+
81
+ Task show reads once by default; use task show <id> --wait --timeout 30 for bounded read-only polling.
82
+ --timeout is required with --wait (integer seconds, range 1-60); --poll-interval defaults to 2 seconds (range 1-10), and --wait-requests defaults to 10 total HTTP requests (range 2-10), including auth and retries.
83
+ Wait stops on completed, failed or stopped; init, inprogress and paused remain non-terminal.
84
+ The task snapshot contains the final status; wait reports terminal, polls (validated status snapshots) and reason (terminal, timeout or budget).
85
+ A non-terminal stop exits 0 with the last snapshot and a read-only next-step hint; inspect wait even on exit 0 and include task,wait,help with --fields.
86
+ Timeout covers auth, requests, retries and delays within the shared 8 MiB response cap; failures before any snapshot remain errors.
87
+ Waiting never resubmits an action or changes remote state; a failed or stopped task is terminal, not successful execution.
88
+
89
+ Email search --filter accepts a JSON array of {saasAttrName, saasAttrOp, saasAttrValue}.
90
+ saasAttrName is a field path, not a closed enum (guide pages 19-20).
91
+ Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount.
92
+ Other documented email payload paths (pages 15-16, 24-25): entityPayload.internetMessageId, entityPayload.received, entityPayload.size, entityPayload.emailLinks, entityPayload.attachments, entityPayload.mode, entityPayload.recipients, entityPayload.fromDomain, entityPayload.fromUser, entityPayload.fromName, entityPayload.to, entityPayload.toUser, entityPayload.cc, entityPayload.ccUser, entityPayload.bcc, entityPayload.bccUser, entityPayload.replyToEmail, entityPayload.replyToNickname, entityPayload.isRead, entityPayload.isDeleted, entityPayload.isIncoming, entityPayload.isInternal, entityPayload.isOutgoing, entityPayload.isQuarantined, entityPayload.isQuarantineNotification, entityPayload.isRestored, entityPayload.isRestoreRequested, entityPayload.isRestoreDeclined, entityPayload.saasSpamVerdict, entityPayload.SpfResult.
93
+ saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan.
94
+ The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan.
95
+ saasAttrValue is a string.
77
96
  <!-- command-registry:end -->
78
97
 
79
98
  The command catalogue generates this overview and the full references.
@@ -114,8 +133,8 @@ File metadata never mixes with environment-profile fields; SecretRefs may explic
114
133
  | `CHECKPOINT_AXI_REGION` | Environment-profile region | Required with client ID/secret |
115
134
  | `CHECKPOINT_AXI_CLIENT_ID` | Environment-profile client ID | Required with region/secret |
116
135
  | `CHECKPOINT_AXI_CLIENT_SECRET` | Environment secret source | Default SecretRef; required for env profile |
117
- | `CHECKPOINT_AXI_SAAS` | Environment-profile SaaS | `office365_emails` |
118
- | `CHECKPOINT_AXI_SCOPE` | Environment-profile download scope | Unset |
136
+ | `CHECKPOINT_AXI_SAAS` | Environment-profile SaaS application name, not a URL; see [SaaS and scope](docs/configuration.md#saas-and-scope) for selection and format | `office365_emails` (Microsoft 365 email) |
137
+ | `CHECKPOINT_AXI_SCOPE` | Environment-profile download scope; see [SaaS and scope](docs/configuration.md#saas-and-scope) for values and [download requirements](docs/downloads.md) for usage and permission limits | Unset |
119
138
  | `CHECKPOINT_AXI_READ_ONLY` | Force read-only: `1`/`true`; `0`/`false` releases override | Unset |
120
139
  | `CHECKPOINT_AXI_WRITE_LOG` | Write journal path | State directory's `writes.log`; blank uses default |
121
140
  | `XDG_CONFIG_HOME` | Absolute config base on Linux/macOS | `~/.config` |
@@ -6,29 +6,48 @@
6
6
  "additionalProperties": false,
7
7
  "required": ["region", "clientId"],
8
8
  "properties": {
9
- "region": { "type": "string", "enum": ["us", "eu", "ca", "au", "uk", "uae", "in"] },
10
- "clientId": { "type": "string", "minLength": 1, "pattern": "\\S" },
11
- "clientSecret": { "$ref": "#/$defs/secretRef", "x-secret": true, "default": { "env": "CHECKPOINT_AXI_CLIENT_SECRET" } },
12
- "saas": { "type": "string", "minLength": 1, "pattern": "\\S", "default": "office365_emails" },
13
- "scope": { "type": "string", "minLength": 1, "pattern": "\\S" },
9
+ "region": {
10
+ "title": "Region", "description": "Required region code for your tenant's regional gateway: us, eu, ca, au, uk, uae or in. Obtain it from your tenant administrator. There is no default; URLs are not accepted.",
11
+ "type": "string", "enum": ["us", "eu", "ca", "au", "uk", "uae", "in"]
12
+ },
13
+ "clientId": {
14
+ "title": "Client ID", "description": "Required non-blank API client ID from your tenant's API credentials. There is no default.",
15
+ "type": "string", "minLength": 1, "pattern": "\\S"
16
+ },
17
+ "clientSecret": {
18
+ "title": "Client secret reference", "description": "Reference the environment variable or absolute private file containing your API client secret, never the secret itself. If omitted, reads CHECKPOINT_AXI_CLIENT_SECRET; a missing or blank secret prevents authentication.",
19
+ "$ref": "#/$defs/secretRef", "x-secret": true, "default": { "env": "CHECKPOINT_AXI_CLIENT_SECRET" }
20
+ },
21
+ "saas": {
22
+ "title": "SaaS application name",
23
+ "description": "One non-blank SaaS application name for the tenant, not a gateway, host or authentication URL; slash (/) is not allowed. Obtain the exact API application name from your tenant administrator or Check Point support. If omitted, defaults to office365_emails, selecting Microsoft 365 email; supply the appropriate name for another application. A profile takes one value, while --saas on event list accepts a comma-separated list.",
24
+ "type": "string", "minLength": 1, "pattern": "\\S", "not": { "pattern": "/" }, "default": "office365_emails"
25
+ },
26
+ "scope": {
27
+ "title": "Download scope",
28
+ "description": "Download scope value supplied by your tenant administrator or Check Point support for the intended download. Use printable ASCII with no spaces (characters ! through ~); comma-separated values are sent verbatim. There is no default: omitted means unset, so email download fails because it requires the profile scope as its scopes header. Attachment download uses this only as the default scope query; --scope overrides it, and neither supplied means no scope query. Tenant scope authorization and the relationship between these selectors remain unverified; do not guess a value.",
29
+ "type": "string", "minLength": 1, "pattern": "^[!-~]+$"
30
+ },
14
31
  "downloads": {
32
+ "title": "Download permission", "description": "Separate file-profile download opt-in. If omitted, downloads are disabled. Environment profiles cannot enable downloads.",
15
33
  "type": "object", "additionalProperties": false, "required": ["allow"], "default": { "allow": false },
16
- "properties": { "allow": { "type": "boolean" } }
34
+ "properties": { "allow": { "title": "Allow downloads", "description": "Set true to permit download commands; each command still requires a new private output file.", "type": "boolean" } }
17
35
  },
18
36
  "writes": {
37
+ "title": "Write permissions", "description": "File-profile write opt-in and exact operation allow-list. If omitted, writes are disabled with no allowed operations. Forced read-only always wins.",
19
38
  "type": "object", "additionalProperties": false, "required": ["allowWrites", "operations"],
20
39
  "default": { "allowWrites": false, "operations": [] },
21
40
  "properties": {
22
- "allowWrites": { "type": "boolean" },
23
- "operations": { "type": "array", "items": { "type": "string", "pattern": "^[a-z][A-Za-z0-9_.-]*$" } }
41
+ "allowWrites": { "title": "Allow writes", "description": "Set true to permit listed operations, subject to preview, exact confirmation and durable redacted intent.", "type": "boolean" },
42
+ "operations": { "title": "Allowed write operations", "description": "Exact operation names to allow, such as email.quarantine. An empty list permits no writes.", "type": "array", "items": { "type": "string", "pattern": "^[a-z][A-Za-z0-9_.-]*$" } }
24
43
  }
25
44
  }
26
45
  },
27
46
  "$defs": {
28
47
  "secretRef": {
29
48
  "oneOf": [
30
- { "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
31
- { "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "type": "string", "pattern": "^(?:/|[A-Za-z]:[\\\\/])" } } }
49
+ { "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "title": "Secret environment variable", "description": "Name of the environment variable holding the secret, such as CHECKPOINT_AXI_CLIENT_SECRET. Use letters, digits and underscores, starting with a letter or underscore.", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
50
+ { "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "title": "Secret file path", "description": "Absolute path to a private UTF-8 file holding the secret. Symlinks and insecure permissions are rejected; a leading BOM and trailing whitespace are removed from its content.", "type": "string", "pattern": "^(?:/|[A-Za-z]:[\\\\/])" } } }
32
51
  ]
33
52
  }
34
53
  }
@@ -1,5 +1,9 @@
1
1
  export declare const DESCRIPTION = "Inspect Check Point Email Security through compact, guarded AXI commands";
2
2
  export declare const SETUP_GUIDANCE = "Run checkpoint-axi config path; configure a region, clientId and clientSecret env/file SecretRef";
3
+ export declare const TASK_WAIT_GUIDANCE: string;
4
+ export declare const DETAIL_EXPANSION_GUIDANCE: string;
5
+ export declare const EMAIL_PAYLOAD_ATTRIBUTES: string[];
6
+ export declare const EMAIL_FILTER_GUIDANCE: string;
3
7
  export declare const catalogue: {
4
8
  readonly "email download": {
5
9
  readonly description: "Download an email to a new private file with profile opt-in, a 50 MiB cap and mandatory SHA-256 verification; scopes and email hash availability are unverified (guide pages 131-132)";
@@ -192,6 +196,9 @@ export declare const catalogue: {
192
196
  readonly "event-ids": "Comma-separated event IDs; default unfiltered";
193
197
  readonly "confidence-indicator": "Confidence indicator selector; default unfiltered";
194
198
  readonly description: "Description selector; default unfiltered";
199
+ readonly expand: string;
200
+ readonly "expand-limit": string;
201
+ readonly "expand-requests": string;
195
202
  readonly "start-date": string;
196
203
  readonly "end-date": string;
197
204
  readonly limit: string;
@@ -203,7 +210,7 @@ export declare const catalogue: {
203
210
  readonly full: string;
204
211
  readonly help: string;
205
212
  };
206
- readonly examples: readonly ["checkpoint-axi event list", "checkpoint-axi event list --all --event-types phishing", "checkpoint-axi event list --cursor <cursor>"];
213
+ readonly examples: readonly ["checkpoint-axi event list", "checkpoint-axi event list --limit 5 --expand --event-types phishing", "checkpoint-axi event list --cursor <cursor>"];
207
214
  };
208
215
  readonly "event show": {
209
216
  readonly description: "Show one event, actions taken and available actions (API guide pages 34-37)";
@@ -217,20 +224,27 @@ export declare const catalogue: {
217
224
  readonly examples: readonly ["checkpoint-axi event show <id>", "checkpoint-axi event show <id> --full"];
218
225
  };
219
226
  readonly "task show": {
220
- readonly description: "Show asynchronous task status and per-action details (API guide pages 30-33)";
227
+ readonly description: "Show asynchronous task status and per-action details, optionally waiting within explicit bounds (API guide pages 30-33)";
221
228
  readonly flags: {
222
- config: string;
223
- profile: string;
224
- fields: string;
225
- full: string;
226
- help: string;
229
+ readonly wait: "Poll existing task status only; default false; requires --timeout; terminal: completed, failed, stopped";
230
+ readonly timeout: "Total wait timeout in integer seconds, range 1-60; required with --wait; includes auth and retries";
231
+ readonly "poll-interval": "Seconds between status reads; default 2, range 1-10; requires --wait";
232
+ readonly "wait-requests": "Total HTTP request cap including auth and retries; default 10, range 2-10; requires --wait";
233
+ readonly config: string;
234
+ readonly profile: string;
235
+ readonly fields: string;
236
+ readonly full: string;
237
+ readonly help: string;
227
238
  };
228
- readonly examples: readonly ["checkpoint-axi task show <id>", "checkpoint-axi task show <id> --full"];
239
+ readonly examples: readonly ["checkpoint-axi task show <id>", "checkpoint-axi task show <id> --wait --timeout 30 --fields task,wait,help", "checkpoint-axi task show <id> --full"];
229
240
  };
230
241
  readonly "email search": {
231
242
  readonly description: "Search emails with a bounded date window and scroll continuation (API guide pages 18-27)";
232
243
  readonly flags: {
233
- readonly filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)";
244
+ readonly filter: `JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. ${string}`;
245
+ readonly expand: string;
246
+ readonly "expand-limit": string;
247
+ readonly "expand-requests": string;
234
248
  readonly "start-date": string;
235
249
  readonly "end-date": string;
236
250
  readonly limit: string;
@@ -244,7 +258,7 @@ export declare const catalogue: {
244
258
  readonly full: string;
245
259
  readonly help: string;
246
260
  };
247
- readonly examples: readonly ["checkpoint-axi email search", "checkpoint-axi email search --all --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"];
261
+ readonly examples: readonly ["checkpoint-axi email search", "checkpoint-axi email search --limit 5 --expand --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"];
248
262
  };
249
263
  readonly "email show": {
250
264
  readonly description: "Show one email with verdicts, actions and available actions (API guide pages 10-18)";
@@ -1,6 +1,26 @@
1
1
  import { exceptionFamilies } from "./exceptions.js";
2
2
  export const DESCRIPTION = "Inspect Check Point Email Security through compact, guarded AXI commands";
3
3
  export const SETUP_GUIDANCE = "Run checkpoint-axi config path; configure a region, clientId and clientSecret env/file SecretRef";
4
+ export const TASK_WAIT_GUIDANCE = "Task show reads once by default; use task show <id> --wait --timeout 30 for bounded read-only polling.\n"
5
+ + "--timeout is required with --wait (integer seconds, range 1-60); --poll-interval defaults to 2 seconds (range 1-10), and --wait-requests defaults to 10 total HTTP requests (range 2-10), including auth and retries.\n"
6
+ + "Wait stops on completed, failed or stopped; init, inprogress and paused remain non-terminal.\n"
7
+ + "The task snapshot contains the final status; wait reports terminal, polls (validated status snapshots) and reason (terminal, timeout or budget).\n"
8
+ + "A non-terminal stop exits 0 with the last snapshot and a read-only next-step hint; inspect wait even on exit 0 and include task,wait,help with --fields.\n"
9
+ + "Timeout covers auth, requests, retries and delays within the shared 8 MiB response cap; failures before any snapshot remain errors.\n"
10
+ + "Waiting never resubmits an action or changes remote state; a failed or stopped task is terminal, not successful execution.";
11
+ export const DETAIL_EXPANSION_GUIDANCE = "Email search and event list accept --expand for read-only show details in each successful row's detail field.\n"
12
+ + "Expansion is off by default; --expand-limit caps detail attempts (default 5, range 1-8), and --expand-requests caps total HTTP requests (default 10, range 2-10), including auth, list pages and retries.\n"
13
+ + "Both limit flags require --expand; listing runs first and shares the 60-second and 8 MiB transport budgets with detail reads.\n"
14
+ + "Inspect the expansion object's expanded, complete and notExpanded fields for every unexpanded ID and its reason: item-cap-reached, request-cap-reached, budget-exhausted or per-item-error (with an error code).\n"
15
+ + "Partial expansion keeps every list row and successful detail; list complete/cursor describe pagination separately.\n"
16
+ + "Use --fields emails,expansion or --fields events,expansion to retain expansion status; --full restores full strings after redaction.\n"
17
+ + "With --all, list pages can consume the entire request budget before any detail is expanded; reduce the list limit or use individual show reads for remaining IDs.\n"
18
+ + "Expansion never downloads files, invokes available actions or changes remote state.";
19
+ export const EMAIL_PAYLOAD_ATTRIBUTES = ["subject", "fromEmail", "attachmentCount", "internetMessageId", "received", "size", "emailLinks", "attachments", "mode", "recipients", "fromDomain", "fromUser", "fromName", "to", "toUser", "cc", "ccUser", "bcc", "bccUser", "replyToEmail", "replyToNickname", "isRead", "isDeleted", "isIncoming", "isInternal", "isOutgoing", "isQuarantined", "isQuarantineNotification", "isRestored", "isRestoreRequested", "isRestoreDeclined", "saasSpamVerdict", "SpfResult"].map((name) => `entityPayload.${name}`);
20
+ export const EMAIL_FILTER_GUIDANCE = "saasAttrName is a field path, not a closed enum (guide pages 19-20). Documented examples: "
21
+ + EMAIL_PAYLOAD_ATTRIBUTES.slice(0, 3).join(", ") + ". Other documented email payload paths (pages 15-16, 24-25): "
22
+ + EMAIL_PAYLOAD_ATTRIBUTES.slice(3).join(", ")
23
+ + ". saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan. The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan. saasAttrValue is a string.";
4
24
  const common = {
5
25
  config: "Config path; default CHECKPOINT_AXI_CONFIG or platform path",
6
26
  profile: "Profile name; default CHECKPOINT_AXI_PROFILE, env profile, config default, or sole profile",
@@ -16,6 +36,11 @@ const paging = {
16
36
  cursor: "Opaque continuation bound to profile, region, operation and selectors; repeat original selectors",
17
37
  };
18
38
  const exceptionType = `Required for ${["malware", "url", "dlp"].map((kind) => `${kind}: ${exceptionFamilies[kind].types.join(",")}`).join("; ")}`;
39
+ const expansion = {
40
+ expand: "Opt in to read-only show details on returned rows; default false; partial expansion is reported separately",
41
+ "expand-limit": "Maximum detail attempts; default 5, range 1-8; requires --expand",
42
+ "expand-requests": "Total HTTP request cap including auth, list pages, detail and retries; default 10, range 2-10; requires --expand",
43
+ };
19
44
  const writeFlags = { ...common,
20
45
  ids: "Required comma-separated unique entity IDs; maximum 20",
21
46
  execute: "Send once after all write gates; default false (preview)",
@@ -65,7 +90,7 @@ export const catalogue = {
65
90
  "exception show": { description: "Show an exception by kind and ID/value; anomaly refuses ENDPOINT_UNVERIFIED pending commissioning (guide pages 52-123)",
66
91
  flags: { ...common, type: exceptionType, items: "Click-time item route instead of list route; default false" },
67
92
  examples: ["checkpoint-axi exception show phishing-block <id>", "checkpoint-axi exception show url <value> --type allow-url", "checkpoint-axi exception show click-time <id> --items --full"] },
68
- "event list": { description: "List security events with bounded scroll continuation (API guide pages 37-42)", flags: { ...common, ...paging,
93
+ "event list": { description: "List security events with bounded scroll continuation (API guide pages 37-42)", flags: { ...common, ...paging, ...expansion,
69
94
  saas: "Comma-separated SaaS overrides; default selected profile saas or office365_emails",
70
95
  "event-types": "Comma-separated event types; default unfiltered",
71
96
  "event-states": "Comma-separated event states; default unfiltered",
@@ -73,17 +98,21 @@ export const catalogue = {
73
98
  "event-ids": "Comma-separated event IDs; default unfiltered",
74
99
  "confidence-indicator": "Confidence indicator selector; default unfiltered",
75
100
  description: "Description selector; default unfiltered",
76
- }, examples: ["checkpoint-axi event list", "checkpoint-axi event list --all --event-types phishing", "checkpoint-axi event list --cursor <cursor>"] },
101
+ }, examples: ["checkpoint-axi event list", "checkpoint-axi event list --limit 5 --expand --event-types phishing", "checkpoint-axi event list --cursor <cursor>"] },
77
102
  "event show": { description: "Show one event, actions taken and available actions (API guide pages 34-37)", flags: common,
78
103
  examples: ["checkpoint-axi event show <id>", "checkpoint-axi event show <id> --full"] },
79
- "task show": { description: "Show asynchronous task status and per-action details (API guide pages 30-33)", flags: common,
80
- examples: ["checkpoint-axi task show <id>", "checkpoint-axi task show <id> --full"] },
104
+ "task show": { description: "Show asynchronous task status and per-action details, optionally waiting within explicit bounds (API guide pages 30-33)", flags: { ...common,
105
+ wait: "Poll existing task status only; default false; requires --timeout; terminal: completed, failed, stopped",
106
+ timeout: "Total wait timeout in integer seconds, range 1-60; required with --wait; includes auth and retries",
107
+ "poll-interval": "Seconds between status reads; default 2, range 1-10; requires --wait",
108
+ "wait-requests": "Total HTTP request cap including auth and retries; default 10, range 2-10; requires --wait",
109
+ }, examples: ["checkpoint-axi task show <id>", "checkpoint-axi task show <id> --wait --timeout 30 --fields task,wait,help", "checkpoint-axi task show <id> --full"] },
81
110
  "email search": { description: "Search emails with a bounded date window and scroll continuation (API guide pages 18-27)", flags: { ...common,
82
111
  saas: "SaaS override; default selected profile saas or office365_emails",
83
112
  "saas-entity": "Optional SaaS entity type selector",
84
- ...paging,
85
- filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)",
86
- }, examples: ["checkpoint-axi email search", "checkpoint-axi email search --all --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"] },
113
+ ...paging, ...expansion,
114
+ filter: `JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. ${EMAIL_FILTER_GUIDANCE}`,
115
+ }, examples: ["checkpoint-axi email search", "checkpoint-axi email search --limit 5 --expand --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"] },
87
116
  "email show": { description: "Show one email with verdicts, actions and available actions (API guide pages 10-18)", flags: common,
88
117
  examples: ["checkpoint-axi email show <id>", "checkpoint-axi email show <id> --full"] },
89
118
  doctor: { description: "Check regional API authentication and one bounded email search without printing tenant data", flags: { ...common, saas: "SaaS override; default selected profile saas or office365_emails" },
@@ -38,7 +38,7 @@ export async function localCommand(argv, store, transport) {
38
38
  const label = flag.split("=")[0];
39
39
  usage(/^--[A-Za-z][A-Za-z0-9-]*$/.test(label) ? `Unknown or duplicate flag ${label}` : undefined);
40
40
  }
41
- if (["--help", "--full", "--all", "--items", "--specific", "--original", "--execute", "--dry-run"].includes(flag))
41
+ if (["--help", "--full", "--all", "--expand", "--wait", "--items", "--specific", "--original", "--execute", "--dry-run"].includes(flag))
42
42
  flags.set(flag, "true");
43
43
  else {
44
44
  const value = argv[++index];
@@ -50,11 +50,13 @@ export async function localCommand(argv, store, transport) {
50
50
  if (flags.has("--help"))
51
51
  return { command: name, ...entry };
52
52
  const available = isException ? name === "exception list" ? ["exceptions", "count", "total", "complete", "reason", "responseSchema", "help"] : ["exception", "responseSchema"]
53
- : name === "email search" || name === "event list" ? [name === "event list" ? "events" : "emails", "count", "total", "complete", "reason", "cursor", "help"] : name.endsWith(" show") ? [name.split(" ")[0]]
53
+ : name === "email search" || name === "event list" ? [name === "event list" ? "events" : "emails", "count", "total", "complete", "reason", "cursor", "expansion", "help"] : name.endsWith(" show") ? [name.split(" ")[0]]
54
54
  : name === "doctor" ? ["region", "saas", "checks", "pass", "help"] : name === "auth status" ? ["profile", "source", "region", "origin", "saas", "clientSecret", "readOnly"]
55
55
  : name === "profile list" ? ["profiles", "count", "config"] : name === "config path" ? ["config", "state"]
56
56
  : ["bin", "description", "status", "profiles", "count", "help"];
57
57
  const isWrite = name === "email quarantine" || name === "email restore" || name === "event action" || isExceptionWrite;
58
+ if (name === "task show")
59
+ available.push("wait", "help");
58
60
  if (isWrite)
59
61
  available.splice(0, available.length, "outcome", "preview", "auditId", "tasks", "response", "responseSchema", "help");
60
62
  if (isDownload)
@@ -8,6 +8,7 @@ import { SecretRedactor } from "./redact.js";
8
8
  const profileName = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/);
9
9
  const region = z.enum(Object.keys(REGIONS));
10
10
  const nonempty = z.string().trim().min(1);
11
+ const saasName = nonempty.refine((value) => !value.includes("/"));
11
12
  function schema(platform) {
12
13
  const secretRef = z.union([
13
14
  z.strictObject({ env: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/) }),
@@ -17,7 +18,7 @@ function schema(platform) {
17
18
  region,
18
19
  clientId: nonempty,
19
20
  clientSecret: secretRef.default({ env: "CHECKPOINT_AXI_CLIENT_SECRET" }),
20
- saas: nonempty.default("office365_emails"),
21
+ saas: saasName.default("office365_emails"),
21
22
  scope: nonempty.optional(),
22
23
  downloads: z.strictObject({ allow: z.boolean() }).default({ allow: false }),
23
24
  writes: z.strictObject({ allowWrites: z.boolean(), operations: z.array(z.string().regex(/^[a-z][A-Za-z0-9_.-]*$/)) })
@@ -78,7 +79,7 @@ export function credentialStore(options = {}, redactor = new SecretRedactor()) {
78
79
  }
79
80
  const parsed = schema(platform).safeParse(raw);
80
81
  if (!parsed.success)
81
- return invalid("Invalid configuration: check profile names, region, defaultProfile, unknown fields and clientSecret SecretRef");
82
+ return invalid("Invalid configuration: check profile names, region, defaultProfile, unknown fields, clientSecret SecretRef and saas (use a SaaS application name, not a URL)");
82
83
  return { path, config: parsed.data };
83
84
  }
84
85
  function select(config, flag) {
@@ -87,8 +88,11 @@ export function credentialStore(options = {}, redactor = new SecretRedactor()) {
87
88
  invalid("Invalid selected profile name");
88
89
  const envFields = ["REGION", "CLIENT_ID", "SAAS", "SCOPE"];
89
90
  if (selected === undefined && envFields.some((field) => env[`CHECKPOINT_AXI_${field}`] !== undefined)) {
91
+ if (env.CHECKPOINT_AXI_SAAS !== undefined && !saasName.safeParse(env.CHECKPOINT_AXI_SAAS).success) {
92
+ return invalid("saas must be a SaaS application name, not a URL");
93
+ }
90
94
  if (!region.safeParse(env.CHECKPOINT_AXI_REGION).success || !env.CHECKPOINT_AXI_CLIENT_ID?.trim() || !env.CHECKPOINT_AXI_CLIENT_SECRET?.trim()
91
- || (env.CHECKPOINT_AXI_SAAS !== undefined && !env.CHECKPOINT_AXI_SAAS.trim()) || (env.CHECKPOINT_AXI_SCOPE !== undefined && !env.CHECKPOINT_AXI_SCOPE.trim())) {
95
+ || (env.CHECKPOINT_AXI_SCOPE !== undefined && !env.CHECKPOINT_AXI_SCOPE.trim())) {
92
96
  return invalid("Incomplete or invalid env profile; set REGION, CLIENT_ID and CLIENT_SECRET together");
93
97
  }
94
98
  return { region: env.CHECKPOINT_AXI_REGION, clientId: env.CHECKPOINT_AXI_CLIENT_ID.trim(),
package/dist/src/reads.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
2
  import { createHash } from "node:crypto";
3
+ import { setTimeout as delay } from "node:timers/promises";
3
4
  import { z } from "zod";
4
5
  import { REGIONS } from "./regions.js";
5
6
  import { gateway, READ_LIMITS } from "./transport.js";
7
+ import { responseInvalid } from "./response-invalid.js";
6
8
  const day = 86_400_000;
7
9
  const timestamp = z.iso.datetime({ offset: true });
8
10
  const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
@@ -10,10 +12,12 @@ const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
10
12
  saasAttrValue: z.string() }));
11
13
  const continuation = z.strictObject({ version: z.literal(1), binding: z.string(), start: timestamp, end: timestamp,
12
14
  scroll: z.string().optional(), offset: z.number().int().nonnegative(), replayRisk: z.literal(true).optional() });
15
+ // The guide's prose/table (pp.11,14) says security-result array; its JSON examples use an object.
16
+ const securityResults = z.object({ combinedVerdict: z.unknown().optional() }).passthrough();
13
17
  // Entity schemas and SaaS-specific payloads: guide pages 11-15 and 21-24.
14
18
  const entity = z.object({ entityInfo: z.object({ entityId: z.string().min(1), entityActionState: z.string().optional() }).passthrough(),
15
19
  entityPayload: z.object({ subject: z.string().optional() }).passthrough(),
16
- entitySecurityResults: z.object({ combinedVerdict: z.unknown().optional() }).passthrough().optional(),
20
+ entitySecurityResults: z.union([securityResults, z.array(z.object({}).passthrough())]).optional(),
17
21
  }).passthrough();
18
22
  // Event and task fields: guide pages 31-32, 35-36 and 40-41.
19
23
  const event = z.object({ eventId: z.string().min(1), type: z.string(), state: z.string(), severity: z.string() }).passthrough();
@@ -27,12 +31,32 @@ export async function readCommand(name, id, flags, store, transport) {
27
31
  const isEvent = name.startsWith("event ");
28
32
  if (name.endsWith(" show") && (!id || !/^[A-Za-z0-9_-]+$/.test(id)))
29
33
  invalid(`${name} requires one ID containing letters, digits, underscores or hyphens`);
34
+ const waiting = flags.has("--wait");
35
+ if (!waiting && ["--timeout", "--poll-interval", "--wait-requests"].some((flag) => flags.has(flag)))
36
+ invalid("Wait limits require --wait");
37
+ const timeoutText = flags.get("--timeout") ?? "";
38
+ const intervalText = flags.get("--poll-interval") ?? "2";
39
+ const waitRequestsText = flags.get("--wait-requests") ?? "10";
40
+ if (waiting && (!/^[1-9][0-9]*$/.test(timeoutText) || Number(timeoutText) > 60))
41
+ invalid("--wait requires --timeout in integer seconds from 1 to 60");
42
+ if (!/^(?:[1-9]|10)$/.test(intervalText))
43
+ invalid("--poll-interval must be integer seconds from 1 to 10");
44
+ if (!/^(?:[2-9]|10)$/.test(waitRequestsText))
45
+ invalid("--wait-requests must be an integer from 2 to 10");
30
46
  const limitText = flags.get("--limit") ?? "50";
31
47
  if (!/^[1-9][0-9]*$/.test(limitText) || Number(limitText) > READ_LIMITS.rows)
32
48
  invalid("--limit must be an integer from 1 to 1000");
33
49
  if (flags.has("--all") && flags.has("--limit"))
34
50
  invalid("Use either --all or --limit");
35
51
  const limit = flags.has("--all") ? READ_LIMITS.rows : Number(limitText);
52
+ if (!flags.has("--expand") && (flags.has("--expand-limit") || flags.has("--expand-requests")))
53
+ invalid("Expansion limits require --expand");
54
+ const expandLimitText = flags.get("--expand-limit") ?? "5";
55
+ const expandRequestsText = flags.get("--expand-requests") ?? "10";
56
+ if (!/^[1-8]$/.test(expandLimitText))
57
+ invalid("--expand-limit must be an integer from 1 to 8");
58
+ if (!/^(?:[2-9]|10)$/.test(expandRequestsText))
59
+ invalid("--expand-requests must be an integer from 2 to 10");
36
60
  let extended;
37
61
  if (flags.has("--filter")) {
38
62
  let raw;
@@ -97,21 +121,66 @@ export async function readCommand(name, id, flags, store, transport) {
97
121
  origin: REGIONS[profile.region], operation: name, ...(isEvent ? { selectors } : { entityFilter, extended }) })).digest("hex");
98
122
  if (cursor && cursor.binding !== binding)
99
123
  invalid("Cursor does not match this profile, region or search selectors; repeat the original selectors");
100
- const api = gateway(profile, store.credentials(profile, true), store.redactor, transport);
101
- if (name.endsWith(" show")) {
124
+ const now = transport?.now ?? Date.now;
125
+ const wait = transport?.wait ?? delay;
126
+ const timeout = waiting ? Number(timeoutText) * 1_000 : READ_LIMITS.milliseconds;
127
+ const deadline = now() + timeout;
128
+ const api = gateway(profile, store.credentials(profile, true), store.redactor, transport, false, Number(waiting ? waitRequestsText : expandRequestsText), timeout);
129
+ async function detail(id) {
130
+ if (!/^[A-Za-z0-9_-]+$/.test(id))
131
+ throw new AxiError("Invalid detail ID", "RESPONSE_INVALID", []);
102
132
  const kind = name.split(" ")[0];
103
133
  const page = await api.read({ path: name === "task show" ? `/task/${id}` : isEvent ? `/event/${id}` : `/search/entity/${id}` });
134
+ const response = { responseData: page.dataIsArray ? page.records : page.records[0] };
135
+ const recordPath = page.dataIsArray ? ["responseData", 0] : ["responseData"];
104
136
  if (page.records.length === 0)
105
137
  throw new AxiError(`${kind} was not found`, "NOT_FOUND", []);
106
138
  const parsed = (name === "task show" ? task : isEvent ? event : entity).safeParse(page.records[0]);
107
- if (page.records.length !== 1 || !parsed.success)
108
- throw new AxiError(`Invalid ${kind} detail response`, "RESPONSE_INVALID", []);
139
+ if (page.records.length !== 1)
140
+ throw responseInvalid(`Invalid ${kind} detail response`, response, [["responseData"]]);
141
+ if (!parsed.success)
142
+ throw responseInvalid(`Invalid ${kind} detail response`, response, parsed.error.issues.map((issue) => [...recordPath, ...issue.path]));
109
143
  const record = page.records[0];
110
144
  const returnedId = name === "task show" ? String(record.id) : isEvent ? record.eventId : record.entityInfo?.entityId;
111
145
  if (returnedId !== id)
112
- throw new AxiError(`Invalid ${kind} detail response`, "RESPONSE_INVALID", []);
113
- return { [kind]: parsed.data };
146
+ throw responseInvalid(`Invalid ${kind} detail response`, response, [[...recordPath, ...(name === "task show" ? ["id"] : isEvent ? ["eventId"] : ["entityInfo", "entityId"])]]);
147
+ return parsed.data;
114
148
  }
149
+ if (name === "task show" && waiting) {
150
+ let snapshot;
151
+ let polls = 0;
152
+ let reason;
153
+ for (;;) {
154
+ try {
155
+ snapshot = await detail(id);
156
+ polls++;
157
+ }
158
+ catch (error) {
159
+ if (!snapshot || !(error instanceof AxiError) || !["REQUEST_BUDGET", "TIME_BUDGET", "RESPONSE_TOO_LARGE"].includes(error.code))
160
+ throw error;
161
+ reason = error.code === "TIME_BUDGET" ? "timeout" : "budget";
162
+ break;
163
+ }
164
+ if (["completed", "failed", "stopped"].includes(snapshot.status)) {
165
+ reason = "terminal";
166
+ break;
167
+ }
168
+ const remaining = deadline - now();
169
+ if (remaining <= 0) {
170
+ reason = "timeout";
171
+ break;
172
+ }
173
+ await wait(Math.min(Number(intervalText) * 1_000, remaining));
174
+ if (now() >= deadline) {
175
+ reason = "timeout";
176
+ break;
177
+ }
178
+ }
179
+ return { task: snapshot, wait: { terminal: reason === "terminal", polls, reason },
180
+ ...(reason === "terminal" ? {} : { help: [`Run checkpoint-axi task show ${id}${flags.has("--config") ? " --config <absolute-path>" : ""}${flags.has("--profile") ? " --profile <name>" : ""} to read status again; the task may still run, never resubmit its action to obtain status`] }) };
181
+ }
182
+ if (name.endsWith(" show"))
183
+ return { [name.split(" ")[0]]: await detail(id) };
115
184
  const results = [];
116
185
  let scroll = cursor?.scroll;
117
186
  let offset = cursor?.offset ?? 0;
@@ -131,11 +200,14 @@ export async function readCommand(name, id, flags, store, transport) {
131
200
  throw error;
132
201
  }
133
202
  const parsed = z.array(isEvent ? event : entity).safeParse(page.records);
134
- if (!parsed.success || offset > page.records.length)
135
- throw new AxiError("Invalid search response", "RESPONSE_INVALID", []);
203
+ const response = { responseData: page.dataIsArray ? page.records : page.records[0] };
204
+ if (!parsed.success)
205
+ throw responseInvalid("Invalid search response", response, parsed.error.issues.map((issue) => ["responseData", ...(page.dataIsArray ? issue.path : issue.path.slice(1))]));
206
+ if (offset > page.records.length)
207
+ throw responseInvalid("Invalid search response", response, [["responseData"]]);
136
208
  // Never wrap an echoed credential in a reversible continuation cursor.
137
209
  if (page.scrollId && store.redactor.text(page.scrollId) !== page.scrollId)
138
- throw new AxiError("Unsafe API scrollId", "RESPONSE_INVALID", []);
210
+ throw responseInvalid("Unsafe API scrollId", { responseEnvelope: { scrollId: page.scrollId } }, [["responseEnvelope", "scrollId"]]);
139
211
  total = page.total ?? total;
140
212
  const rows = parsed.data.slice(offset, offset + limit - results.length);
141
213
  results.push(...rows.map((record) => {
@@ -145,7 +217,7 @@ export async function readCommand(name, id, flags, store, transport) {
145
217
  }
146
218
  const value = entity.parse(record);
147
219
  return { id: value.entityInfo.entityId, subject: value.entityPayload.subject ?? "",
148
- state: value.entityInfo.entityActionState ?? "", verdict: value.entitySecurityResults?.combinedVerdict ?? null };
220
+ state: value.entityInfo.entityActionState ?? "", verdict: Array.isArray(value.entitySecurityResults) ? null : value.entitySecurityResults?.combinedVerdict ?? null };
149
221
  }));
150
222
  offset += rows.length;
151
223
  if (offset < page.records.length) {
@@ -161,7 +233,43 @@ export async function readCommand(name, id, flags, store, transport) {
161
233
  }
162
234
  const next = exhausted ? undefined : Buffer.from(JSON.stringify({ version: 1, binding, start, end, scroll, offset,
163
235
  ...(replayRisk ? { replayRisk: true } : {}) })).toString("base64url");
236
+ let expansion;
237
+ if (flags.has("--expand")) {
238
+ let attempted = 0;
239
+ let expanded = 0;
240
+ let stopped;
241
+ const notExpanded = [];
242
+ for (const row of results) {
243
+ const itemId = row.id;
244
+ if (stopped) {
245
+ notExpanded.push({ id: itemId, ...stopped });
246
+ continue;
247
+ }
248
+ if (attempted >= Number(expandLimitText)) {
249
+ notExpanded.push({ id: itemId, reason: "item-cap-reached" });
250
+ continue;
251
+ }
252
+ attempted++;
253
+ try {
254
+ row.detail = await detail(itemId);
255
+ expanded++;
256
+ }
257
+ catch (error) {
258
+ if (!(error instanceof AxiError))
259
+ throw error;
260
+ const code = error.code;
261
+ if (["REQUEST_BUDGET", "TIME_BUDGET", "RESPONSE_TOO_LARGE"].includes(code)) {
262
+ stopped = { reason: code === "REQUEST_BUDGET" ? "request-cap-reached" : "budget-exhausted", code };
263
+ notExpanded.push({ id: itemId, ...stopped });
264
+ }
265
+ else
266
+ notExpanded.push({ id: itemId, reason: "per-item-error", code });
267
+ }
268
+ }
269
+ expansion = { expanded, attempted, itemLimit: Number(expandLimitText), requestLimit: Number(expandRequestsText), complete: notExpanded.length === 0, notExpanded };
270
+ }
164
271
  return { [isEvent ? "events" : "emails"]: results, count: results.length, ...(total === undefined ? {} : { total }), complete: exhausted && !replayRisk,
272
+ ...(expansion ? { expansion } : {}),
165
273
  ...(replayRisk ? { reason: "replay-may-skip-or-repeat" } : {}), ...(next ? { cursor: next } : {}),
166
274
  help: [...(results.length ? [isEvent ? "Run checkpoint-axi event show <id> for event details and available actions" : "Run checkpoint-axi email show <id> for verdicts, actions and available actions"] : [`No ${isEvent ? "events" : "emails"} matched this search window and selectors`]),
167
275
  ...(next ? ["Repeat this search with --cursor <cursor> and the original selectors to continue"] : []),
@@ -0,0 +1,2 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ export declare function responseInvalid(message: string, value: unknown, paths: readonly (readonly PropertyKey[])[]): AxiError;
@@ -0,0 +1,17 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ // Never format validator messages or received values: both may contain tenant data.
3
+ export function responseInvalid(message, value, paths) {
4
+ const failures = paths.map((path) => {
5
+ let received = value;
6
+ let location = "$";
7
+ for (const key of path) {
8
+ location += typeof key === "number" ? `[${key}]`
9
+ : `.${typeof key === "string" && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : "<key>"}`;
10
+ received = received !== null && typeof received === "object" && Object.hasOwn(received, key)
11
+ ? received[key] : undefined;
12
+ }
13
+ const type = received === undefined ? "missing" : received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
14
+ return `${location}: ${type}`;
15
+ });
16
+ return new AxiError(`${message} (${[...new Set(failures)].join("; ")})`, "RESPONSE_INVALID", []);
17
+ }
@@ -59,6 +59,7 @@ export type ReadRequest = {
59
59
  };
60
60
  export type Page = {
61
61
  records: unknown[];
62
+ dataIsArray: boolean;
62
63
  scrollId?: string;
63
64
  total?: number;
64
65
  };
@@ -71,7 +72,7 @@ export declare function decodePage(value: unknown, unverified?: boolean): Page;
71
72
  export declare function gateway(profile: SelectedProfile, credentials: {
72
73
  clientId: string;
73
74
  clientSecret: string;
74
- }, redactor: SecretRedactor, dependencies?: Dependencies, mutation?: boolean): {
75
+ }, redactor: SecretRedactor, dependencies?: Dependencies, mutation?: boolean, requestLimit?: number, milliseconds?: number): {
75
76
  authenticate: () => Promise<void>;
76
77
  read: (query: ReadRequest) => Promise<Page>;
77
78
  download: (query: DownloadRequest) => Promise<{
@@ -4,6 +4,7 @@ import { request } from "node:https";
4
4
  import { setTimeout as delay } from "node:timers/promises";
5
5
  import { REGIONS } from "./regions.js";
6
6
  import { consumeWriteAuthorization } from "./writes.js";
7
+ import { responseInvalid } from "./response-invalid.js";
7
8
  export const READ_LIMITS = { requests: 10, rows: 1_000, milliseconds: 60_000, bytes: 8 * 1024 * 1024 };
8
9
  export const DOWNLOAD_LIMIT = 50 * 1024 * 1024;
9
10
  const failure = (code, message) => new AxiError(message, code, ["Check the selected region and credentials; run checkpoint-axi doctor"]);
@@ -12,24 +13,26 @@ const object = (value) => value !== null && typeof value === "object" && !Array.
12
13
  export function decodePage(value, unverified = false) {
13
14
  // These exception families publish no response schema (guide pages 83-123).
14
15
  if (unverified && (!object(value) || !Object.hasOwn(value, "responseEnvelope"))) {
15
- return { records: Array.isArray(value) ? value : [value] };
16
+ return { records: Array.isArray(value) ? value : [value], dataIsArray: Array.isArray(value) };
16
17
  }
17
- if (!object(value) || !object(value.responseEnvelope))
18
- throw failure("RESPONSE_INVALID", "Missing API response envelope");
18
+ if (!object(value))
19
+ throw responseInvalid("Missing API response envelope", value, [[]]);
20
+ if (!object(value.responseEnvelope))
21
+ throw responseInvalid("Missing API response envelope", value, [["responseEnvelope"]]);
19
22
  const envelope = value.responseEnvelope;
20
23
  if (envelope.responseCode !== 0 && envelope.responseCode !== "0" && envelope.responseCode !== 200 && envelope.responseCode !== "200") {
21
24
  throw failure("API_ERROR", "API response envelope did not report success");
22
25
  }
23
26
  const data = value.responseData;
24
27
  if ((!unverified && !Array.isArray(data) && !object(data)) || data === undefined)
25
- throw failure("RESPONSE_INVALID", "Expected responseData");
28
+ throw responseInvalid("Expected responseData", value, [["responseData"]]);
26
29
  if (envelope.scrollId !== undefined && envelope.scrollId !== null && typeof envelope.scrollId !== "string") {
27
- throw failure("RESPONSE_INVALID", "Invalid API scrollId");
30
+ throw responseInvalid("Invalid API scrollId", value, [["responseEnvelope", "scrollId"]]);
28
31
  }
29
32
  if (envelope.totalRecordsNumber !== undefined && (!Number.isSafeInteger(envelope.totalRecordsNumber) || Number(envelope.totalRecordsNumber) < 0)) {
30
- throw failure("RESPONSE_INVALID", "Invalid API totalRecordsNumber");
33
+ throw responseInvalid("Invalid API totalRecordsNumber", value, [["responseEnvelope", "totalRecordsNumber"]]);
31
34
  }
32
- return { records: Array.isArray(data) ? data : [data], scrollId: typeof envelope.scrollId === "string" && envelope.scrollId ? envelope.scrollId : undefined,
35
+ return { records: Array.isArray(data) ? data : [data], dataIsArray: Array.isArray(data), scrollId: typeof envelope.scrollId === "string" && envelope.scrollId ? envelope.scrollId : undefined,
33
36
  total: envelope.totalRecordsNumber };
34
37
  }
35
38
  // No field name or lifetime is commissioned yet (guide page 8).
@@ -63,10 +66,10 @@ function tokenFrom(value, redactor) {
63
66
  throw failure("AUTH_SHAPE_UNVERIFIED", "Unverified auth token response shape; owner commissioning is required (API guide page 8 does not specify the shape)");
64
67
  return [...tokens][0];
65
68
  }
66
- export function gateway(profile, credentials, redactor, dependencies = {}, mutation = false) {
69
+ export function gateway(profile, credentials, redactor, dependencies = {}, mutation = false, requestLimit = READ_LIMITS.requests, milliseconds = READ_LIMITS.milliseconds) {
67
70
  const now = dependencies.now ?? Date.now;
68
71
  const wait = dependencies.wait ?? delay;
69
- const deadline = now() + READ_LIMITS.milliseconds;
72
+ const deadline = now() + Math.min(milliseconds, READ_LIMITS.milliseconds);
70
73
  let bytes = 0;
71
74
  let requests = 0;
72
75
  let token;
@@ -83,7 +86,7 @@ export function gateway(profile, credentials, redactor, dependencies = {}, mutat
83
86
  const remaining = deadline - now();
84
87
  if (remaining <= 0)
85
88
  throw failure("TIME_BUDGET", "Read time budget exhausted");
86
- if (++requests > (mutation ? 44 : READ_LIMITS.requests))
89
+ if (++requests > (mutation ? 44 : Math.min(requestLimit, READ_LIMITS.requests)))
87
90
  throw failure("REQUEST_BUDGET", "Request budget exhausted");
88
91
  const payload = body === undefined ? undefined : JSON.stringify(body);
89
92
  if (bearer)
@@ -125,7 +128,7 @@ export function gateway(profile, credentials, redactor, dependencies = {}, mutat
125
128
  resolve({ status, value: JSON.parse(json) });
126
129
  }
127
130
  catch {
128
- reject(failure("RESPONSE_INVALID", "API returned invalid JSON"));
131
+ reject(responseInvalid("API returned invalid JSON", json, [[]]));
129
132
  }
130
133
  });
131
134
  });
@@ -36,13 +36,20 @@ checkpoint-axi email show synthetic-email-1 --fields email
36
36
  ```
37
37
 
38
38
  Search flags: --saas (default profile SaaS or office365_emails), --saas-entity, and --filter (JSON array of {saasAttrName, saasAttrOp, saasAttrValue}).
39
+ saasAttrName is a field path, not a closed enum (guide pages 19-20).
40
+ Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount.
41
+ Other documented email payload paths (pages 15-16, 24-25): entityPayload.internetMessageId, entityPayload.received, entityPayload.size, entityPayload.emailLinks, entityPayload.attachments, entityPayload.mode, entityPayload.recipients, entityPayload.fromDomain, entityPayload.fromUser, entityPayload.fromName, entityPayload.to, entityPayload.toUser, entityPayload.cc, entityPayload.ccUser, entityPayload.bcc, entityPayload.bccUser, entityPayload.replyToEmail, entityPayload.replyToNickname, entityPayload.isRead, entityPayload.isDeleted, entityPayload.isIncoming, entityPayload.isInternal, entityPayload.isOutgoing, entityPayload.isQuarantined, entityPayload.isQuarantineNotification, entityPayload.isRestored, entityPayload.isRestoreRequested, entityPayload.isRestoreDeclined, entityPayload.saasSpamVerdict, entityPayload.SpfResult.
42
+ saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan.
43
+ The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan.
44
+ saasAttrValue is a string.
39
45
  Event selectors include --event-types, --event-states, --severities, --event-ids (comma-separated), --confidence-indicator and --description; --saas accepts comma-separated SaaS values here.
40
- Event detail includes available actions; task detail reads status and per-action results once, without waiting or polling:
46
+ Event detail includes available actions; task detail reads status and per-action results once by default:
41
47
 
42
48
  ```sh
43
49
  checkpoint-axi event list --all --event-types phishing --fields events,count,total,complete,reason,cursor
44
50
  checkpoint-axi event show synthetic-event-1 --fields event
45
51
  checkpoint-axi task show synthetic-task-1 --fields task
52
+ checkpoint-axi task show synthetic-task-1 --wait --timeout 30 --fields task,wait,help
46
53
  ```
47
54
 
48
55
  Inspect exception rules with exception list <kind> or exception show <kind> <id-or-value>:
@@ -68,7 +75,27 @@ Inspect complete, reason and cursor even on exit 0; limits and page replay can l
68
75
  Resume with --cursor <returned-cursor> and the original selectors/profile; see [continuation limitations](reads.md#email-reads).
69
76
  Exception --all bounds returned rows at 1000 but cannot fetch additional pages: no continuation parameter is documented.
70
77
  Email/event detail already includes available actions, avoiding a separate action lookup.
71
- There is no bulk detail expansion, cross-resource pivot or task wait flag; use returned IDs for individual reads and never replay an action to obtain its status.
78
+ Email search and event list accept --expand for read-only show details in each successful row's detail field.
79
+ Expansion is off by default; --expand-limit caps detail attempts (default 5, range 1-8), and --expand-requests caps total HTTP requests (default 10, range 2-10), including auth, list pages and retries.
80
+ Both limit flags require --expand; listing runs first and shares the 60-second and 8 MiB transport budgets with detail reads.
81
+ Inspect the expansion object's expanded, complete and notExpanded fields for every unexpanded ID and its reason: item-cap-reached, request-cap-reached, budget-exhausted or per-item-error (with an error code).
82
+ Partial expansion keeps every list row and successful detail; list complete/cursor describe pagination separately.
83
+ Use --fields emails,expansion or --fields events,expansion to retain expansion status; --full restores full strings after redaction.
84
+ With --all, list pages can consume the entire request budget before any detail is expanded; reduce the list limit or use individual show reads for remaining IDs.
85
+ Expansion never downloads files, invokes available actions or changes remote state.
86
+ Task show reads once by default; use task show <id> --wait --timeout 30 for bounded read-only polling.
87
+ --timeout is required with --wait (integer seconds, range 1-60); --poll-interval defaults to 2 seconds (range 1-10), and --wait-requests defaults to 10 total HTTP requests (range 2-10), including auth and retries.
88
+ Wait stops on completed, failed or stopped; init, inprogress and paused remain non-terminal.
89
+ The task snapshot contains the final status; wait reports terminal, polls (validated status snapshots) and reason (terminal, timeout or budget).
90
+ A non-terminal stop exits 0 with the last snapshot and a read-only next-step hint; inspect wait even on exit 0 and include task,wait,help with --fields.
91
+ Timeout covers auth, requests, retries and delays within the shared 8 MiB response cap; failures before any snapshot remain errors.
92
+ Waiting never resubmits an action or changes remote state; a failed or stopped task is terminal, not successful execution.
93
+ There is no cross-resource pivot; use returned IDs for individual reads and never replay an action to obtain its status.
94
+
95
+ ```sh
96
+ checkpoint-axi email search --limit 5 --expand --expand-limit 3 --expand-requests 6 --fields emails,count,complete,cursor,expansion
97
+ checkpoint-axi event list --limit 5 --expand --fields events,count,complete,cursor,expansion
98
+ ```
72
99
 
73
100
  --fields selects top-level output keys, not nested row columns: request emails or events together with count,total,complete,reason,cursor for lists, and email,event,task or exception for details.
74
101
  Email rows contain id,subject,state,verdict; event rows contain id,type,state,severity; documented exception rows contain id,sender,subject,comment.
package/docs/commands.md CHANGED
@@ -32,7 +32,7 @@ Run each command with `--help` for concise help.
32
32
  | `checkpoint-axi exception update` | Preview or replace one exception; phishing/spam/click-time require an ID; malware/url/dlp identify it in --input without an ID; anomaly unsupported (guide pages 64-67, 78-81, 94-96, 105-107, 116-117, 126-127) |
33
33
  | `checkpoint-axi home` | Show local profile context without resolving secrets or making API calls |
34
34
  | `checkpoint-axi profile list` | List file profiles without resolving secrets; --profile does not filter this list |
35
- | `checkpoint-axi task show` | Show asynchronous task status and per-action details (API guide pages 30-33) |
35
+ | `checkpoint-axi task show` | Show asynchronous task status and per-action details, optionally waiting within explicit bounds (API guide pages 30-33) |
36
36
 
37
37
  ## attachment download
38
38
 
@@ -197,11 +197,14 @@ Search emails with a bounded date window and scroll continuation (API guide page
197
197
  | `--limit` | Maximum rows; default 50, range 1-1000; cannot combine with --all |
198
198
  | `--all` | Read up to 1000 rows / 10 HTTP requests / 60 seconds / 8 MiB; default false |
199
199
  | `--cursor` | Opaque continuation bound to profile, region, operation and selectors; repeat original selectors |
200
- | `--filter` | JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20) |
200
+ | `--expand` | Opt in to read-only show details on returned rows; default false; partial expansion is reported separately |
201
+ | `--expand-limit` | Maximum detail attempts; default 5, range 1-8; requires --expand |
202
+ | `--expand-requests` | Total HTTP request cap including auth, list pages, detail and retries; default 10, range 2-10; requires --expand |
203
+ | `--filter` | JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. saasAttrName is a field path, not a closed enum (guide pages 19-20). Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount. Other documented email payload paths (pages 15-16, 24-25): entityPayload.internetMessageId, entityPayload.received, entityPayload.size, entityPayload.emailLinks, entityPayload.attachments, entityPayload.mode, entityPayload.recipients, entityPayload.fromDomain, entityPayload.fromUser, entityPayload.fromName, entityPayload.to, entityPayload.toUser, entityPayload.cc, entityPayload.ccUser, entityPayload.bcc, entityPayload.bccUser, entityPayload.replyToEmail, entityPayload.replyToNickname, entityPayload.isRead, entityPayload.isDeleted, entityPayload.isIncoming, entityPayload.isInternal, entityPayload.isOutgoing, entityPayload.isQuarantined, entityPayload.isQuarantineNotification, entityPayload.isRestored, entityPayload.isRestoreRequested, entityPayload.isRestoreDeclined, entityPayload.saasSpamVerdict, entityPayload.SpfResult. saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan. The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan. saasAttrValue is a string. |
201
204
 
202
205
  ```sh
203
206
  checkpoint-axi email search
204
- checkpoint-axi email search --all --saas office365_emails
207
+ checkpoint-axi email search --limit 5 --expand --saas office365_emails
205
208
  checkpoint-axi email search --cursor <cursor>
206
209
  ```
207
210
 
@@ -260,6 +263,9 @@ List security events with bounded scroll continuation (API guide pages 37-42)
260
263
  | `--limit` | Maximum rows; default 50, range 1-1000; cannot combine with --all |
261
264
  | `--all` | Read up to 1000 rows / 10 HTTP requests / 60 seconds / 8 MiB; default false |
262
265
  | `--cursor` | Opaque continuation bound to profile, region, operation and selectors; repeat original selectors |
266
+ | `--expand` | Opt in to read-only show details on returned rows; default false; partial expansion is reported separately |
267
+ | `--expand-limit` | Maximum detail attempts; default 5, range 1-8; requires --expand |
268
+ | `--expand-requests` | Total HTTP request cap including auth, list pages, detail and retries; default 10, range 2-10; requires --expand |
263
269
  | `--saas` | Comma-separated SaaS overrides; default selected profile saas or office365_emails |
264
270
  | `--event-types` | Comma-separated event types; default unfiltered |
265
271
  | `--event-states` | Comma-separated event states; default unfiltered |
@@ -270,7 +276,7 @@ List security events with bounded scroll continuation (API guide pages 37-42)
270
276
 
271
277
  ```sh
272
278
  checkpoint-axi event list
273
- checkpoint-axi event list --all --event-types phishing
279
+ checkpoint-axi event list --limit 5 --expand --event-types phishing
274
280
  checkpoint-axi event list --cursor <cursor>
275
281
  ```
276
282
 
@@ -473,7 +479,7 @@ checkpoint-axi profile list --config /example/config.json
473
479
 
474
480
  ## task show
475
481
 
476
- Show asynchronous task status and per-action details (API guide pages 30-33)
482
+ Show asynchronous task status and per-action details, optionally waiting within explicit bounds (API guide pages 30-33)
477
483
 
478
484
  | Flag | Meaning |
479
485
  |---|---|
@@ -482,8 +488,13 @@ Show asynchronous task status and per-action details (API guide pages 30-33)
482
488
  | `--fields` | Comma-separated output fields; default all |
483
489
  | `--full` | Show full strings instead of 500-character previews; default false |
484
490
  | `--help` | Show concise command help; default false |
491
+ | `--wait` | Poll existing task status only; default false; requires --timeout; terminal: completed, failed, stopped |
492
+ | `--timeout` | Total wait timeout in integer seconds, range 1-60; required with --wait; includes auth and retries |
493
+ | `--poll-interval` | Seconds between status reads; default 2, range 1-10; requires --wait |
494
+ | `--wait-requests` | Total HTTP request cap including auth and retries; default 10, range 2-10; requires --wait |
485
495
 
486
496
  ```sh
487
497
  checkpoint-axi task show <id>
498
+ checkpoint-axi task show <id> --wait --timeout 30 --fields task,wait,help
488
499
  checkpoint-axi task show <id> --full
489
500
  ```
@@ -35,8 +35,6 @@ Omitting `clientSecret` defaults its reference to `CHECKPOINT_AXI_CLIENT_SECRET`
35
35
  It confirms local readiness, not API authentication.
36
36
  `profile list` and home never resolve secret files.
37
37
  Supported regions are `us`, `eu`, `ca`, `au`, `uk`, `uae` and `in`; there is no origin override.
38
- Optional `saas` defaults to `office365_emails`; `doctor`, `email search` and `event list` accept `--saas <name>` to override it for that call.
39
- Optional `scope` supplies the required email download `scopes` header and the default optional attachment `scope` query.
40
38
  The host-facing [credential schema](../axi-credential.schema.json) describes one strict profile.
41
39
 
42
40
  Config precedence is `--config`, then `CHECKPOINT_AXI_CONFIG`, then the platform path.
@@ -52,6 +50,22 @@ POSIX reads reject symlinks, foreign ownership, group/other-writable config and
52
50
  Secret file content is UTF-8 with a leading BOM and trailing whitespace removed.
53
51
  On Windows, regular files and reparse points are checked without interpreting mode bits; `auth status`, `doctor` and email, event, task and exception API commands also verify a protected DACL for the current user, SYSTEM and Administrators.
54
52
 
53
+ ## SaaS and scope
54
+
55
+ `saas` is one non-blank SaaS application name, not a gateway, host or authentication URL.
56
+ Obtain the exact API application name for your tenant from its administrator or Check Point support.
57
+ If omitted, it defaults to `office365_emails`, selecting Microsoft 365 email; choose the appropriate name when targeting another application.
58
+ `office365_emails` is valid; `https://example.test/auth` and `example.test/auth` are invalid because slash (`/`) is not allowed.
59
+ A profile takes one value, not a comma-separated list.
60
+ `doctor` and `email search` accept `--saas <name>` for a single-call override; `event list --saas <names>` accepts a comma-separated list.
61
+
62
+ `scope` is a download scope value supplied by your tenant administrator or Check Point support for the intended download.
63
+ Use printable ASCII with no spaces (characters `!` through `~`); comma-separated values are sent verbatim.
64
+ For example, `synthetic:scope` has a valid form, while `synthetic scope` does not; the example is not a tenant-authorized value.
65
+ There is no default: omitted means unset.
66
+ Do not guess a tenant-authorized value.
67
+ See [download requirements](downloads.md) for how email and attachment commands use scope, the remaining verification limits, and the separate download opt-in and output checks.
68
+
55
69
  ## Write and download permissions
56
70
 
57
71
  File profiles are read-only by default.
package/docs/downloads.md CHANGED
@@ -17,6 +17,8 @@ checkpoint-axi attachment download-all <entity-id> --out /absolute/attachments
17
17
  ```
18
18
 
19
19
  Email downloads require profile `scope`, sent verbatim as the comma-separated `scopes` header required on page 131.
20
+ Without it, email download fails before authentication.
21
+ See [SaaS and scope](configuration.md#saas-and-scope) for the profile value's source and format.
20
22
  `--original` selects the unmodified email; default false includes the service's processing headers.
21
23
  Attachment commands use optional `--scope` as a query selector, defaulting to profile `scope` when configured; no scope query is sent when both are absent.
22
24
  Scope authorization and the relationship between the email `scopes` header and attachment `scope` query require manual verification.
package/docs/reads.md CHANGED
@@ -30,6 +30,8 @@ Search defaults to the past 24 hours and 50 rows; `--start-date` and `--end-date
30
30
  `--saas` overrides the selected profile and `--saas-entity` optionally selects the entity type.
31
31
  `--filter` accepts a JSON array of the documented `saasAttrName`, `saasAttrOp` and `saasAttrValue` filters (guide pages 19-20).
32
32
  For example, `--filter '[{"saasAttrName":"entityPayload.subject","saasAttrOp":"contains","saasAttrValue":"Synthetic"}]'` selects a subject substring.
33
+ The [generated search reference](commands.md#email-search) lists documented field paths and operators, also available in `email search --help`.
34
+ The guide uses field paths rather than a closed attribute enum and does not specify a per-field operator compatibility matrix.
33
35
  Use `--limit <1-1000>` or `--all`, which stops at 1,000 rows, 10 HTTP requests, 60 seconds or 8 MiB, including authentication and retries.
34
36
  Incomplete searches return `complete: false`; when an opaque `cursor` is present, repeat with `--cursor <cursor>` and the same original selectors to continue.
35
37
  Cursors preserve the original window, bind profile/client identity, regional origin, operation and selectors, and retain an offset when a row limit splits a server page.
@@ -40,12 +42,33 @@ Treat cursors as sensitive search state and do not edit them.
40
42
  Cursor strings are never truncated; email text is redacted before 500-character previews, with `--full` for complete text.
41
43
  See [downloads](downloads.md) for private file output.
42
44
 
45
+ The vendor PDF describes `entitySecurityResults` as an array in its prose/table (pages 11 and 14), while its JSON examples use an object.
46
+ Both documented forms are accepted; detail preserves them, and search reports `verdict: null` for an array because no combined verdict object is documented there.
47
+ Other response constraints remain unchanged.
48
+ `RESPONSE_INVALID` diagnostics for these reads name failing JSON paths and received types, including `missing`, without response values.
49
+ Non-identifier path keys appear as `<key>`.
50
+ These diagnostics support owner commissioning; the actual failing fields behind TD-049's live unfiltered-search and spam-detail failures remain unverified.
51
+
43
52
  ## Event and task reads
44
53
 
45
54
  `checkpoint-axi event list` returns compact ID, type, state and severity rows (guide pages 37-42).
46
55
  `checkpoint-axi event show <id>` preserves event details, action history and available actions (guide pages 34-37).
47
56
  `checkpoint-axi task show <id>` returns task status, progress and per-action details for asynchronous actions (guide pages 30-33).
48
- Task states are `init`, `inprogress`, `completed`, `failed`, `stopped` and `paused`; task show performs one read without polling.
57
+ Task states are `init`, `inprogress`, `completed`, `failed`, `stopped` and `paused`; task show performs one read by default.
58
+ Use `checkpoint-axi task show <id> --wait --timeout 30` to poll an already submitted task without resubmitting its action.
59
+ `--wait` requires an explicit `--timeout` in integer seconds, from 1 to 60.
60
+ `--poll-interval` defaults to 2 seconds, from 1 to 10; `--wait-requests` defaults to 10 total HTTP requests, from 2 to 10, including authentication, re-exchange and retries.
61
+ These limits require `--wait` and invalid bounds fail before configuration or network access.
62
+ The timeout includes authentication, requests, retries and delays, and the existing 8 MiB response cap remains shared across polls.
63
+ The first read starts immediately; subsequent reads wait the configured interval after the preceding read, without starting another read at or after the deadline.
64
+ `completed`, `failed` and `stopped` are terminal; `init`, `inprogress` and `paused` remain non-terminal.
65
+ The final `task` snapshot retains status, progress and per-action details; the `wait` object reports `terminal`, `polls` (validated status snapshots, excluding authentication and retries), and `reason`: `terminal`, `timeout` or `budget`.
66
+ Request or response-byte exhaustion reports `budget`; an exhausted deadline or a retry delay that cannot fit reports `timeout`.
67
+ A non-terminal stop exits 0, preserves the last snapshot and includes a `help` hint for another status read.
68
+ Inspect `wait` even on exit 0; terminal failure or stopping does not mean the submitted action succeeded.
69
+ Failures before any snapshot and non-budget read failures remain structured errors rather than reporting stale data as current.
70
+ Use `--fields task,wait,help` to retain wait metadata and recovery guidance when projecting output.
71
+ Waiting only repeats the existing task status GET; it never invokes an available action, downloads a file or resubmits the original mutation.
49
72
  Event list accepts comma-separated `--event-types`, `--event-states`, `--severities`, `--event-ids` and `--saas`, plus `--confidence-indicator` and `--description` text selectors.
50
73
  SaaS defaults to the selected profile's value or `office365_emails`; other event selectors default to unfiltered.
51
74
  Event list uses the same date windows, row and transport budgets, bound continuation and honest page replay flags as [email search](reads.md#email-reads).
@@ -54,6 +77,34 @@ Event and task text is redacted before 500-character previews; use `--full` for
54
77
  For writes, see [event actions](writes.md#event-actions).
55
78
  These reads are verified offline with synthetic fixtures; authentication, token lifetime and live API behavior require manual verification.
56
79
 
80
+ ## Bounded detail expansion
81
+
82
+ Email search and event list accept `--expand` to inspect returned items through the same validated read-only path as their individual show commands.
83
+ Expansion is off by default; available actions are returned as data and are never invoked.
84
+
85
+ ```sh
86
+ checkpoint-axi email search --limit 5 --expand --expand-limit 3 --expand-requests 6
87
+ checkpoint-axi event list --limit 5 --expand --fields events,count,complete,cursor,expansion
88
+ ```
89
+
90
+ `--expand-limit` caps detail attempts, including failed attempts: default 5, minimum 1, maximum 8.
91
+ `--expand-requests` caps total HTTP requests for the invocation: default 10, minimum 2, maximum 10.
92
+ Both limit flags require `--expand`; invalid bounds are rejected before configuration or network access.
93
+ Authentication, token re-exchange, list pages, detail requests and read retries all consume this request cap.
94
+ Listing runs first; detail reads reuse its token, deadline and response-byte accounting without resetting the 60-second or 8 MiB budgets.
95
+ Using `--all` can therefore consume the entire budget before any detail is fetched.
96
+ Use a smaller list limit to leave room for detail, or inspect remaining IDs with individual show commands.
97
+
98
+ Each successfully expanded row gains `detail`, containing the same validated payload and actions as show, with normal secret redaction and 500-character string previews (`--full` restores full strings).
99
+ Every list row and successful detail survives expansion failures; partial expansion exits 0.
100
+ The `expansion` object reports `expanded`, `attempted`, `itemLimit`, `requestLimit`, `complete` and `notExpanded`.
101
+ Each `notExpanded` entry includes the item `id` and `reason`: `item-cap-reached`, `request-cap-reached`, `budget-exhausted` or `per-item-error`.
102
+ Budget and per-item failures also include a safe error `code`; raw upstream error bodies are never printed.
103
+ Budget exhaustion stops further detail attempts; an individual HTTP, response-validation or other read failure records that item and continues within the caps.
104
+ List `complete`, `reason` and `cursor` still describe pagination; `complete` inside `expansion` independently describes whether every returned row was expanded.
105
+ With `--fields`, include `expansion` alongside `emails` or `events` so omissions remain visible.
106
+ Expansion performs no writes, actions, downloads, cross-resource pivots or task waiting.
107
+
57
108
  ## Exception reads
58
109
 
59
110
  `checkpoint-axi exception list <kind>` and `exception show <kind> <id-or-value>` cover `phishing-allow`, `phishing-block`, `spam-allow`, `anomaly`, `click-time`, `malware`, `url` and `dlp` (guide pages 49-123).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/checkpoint-axi",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Inspect Check Point Email Security through compact, guarded AXI commands",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,8 +41,22 @@ Both are bounded by 8 MiB of JSON and 60 seconds including auth and read retries
41
41
  Email search defaults to 50 compact rows and a 24-hour window; --start-date and --end-date require ISO timestamps with timezones and a positive window of at most 31 days (guide pages 18-27).
42
42
  Use --limit <1-1000> or --all, which caps output at 1000 rows within the shared transport budget.
43
43
  Use --saas-entity for an entity type or --filter for a JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20).
44
+ saasAttrName is a field path, not a closed enum (guide pages 19-20).
45
+ Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount.
46
+ Other documented email payload paths (pages 15-16, 24-25): entityPayload.internetMessageId, entityPayload.received, entityPayload.size, entityPayload.emailLinks, entityPayload.attachments, entityPayload.mode, entityPayload.recipients, entityPayload.fromDomain, entityPayload.fromUser, entityPayload.fromName, entityPayload.to, entityPayload.toUser, entityPayload.cc, entityPayload.ccUser, entityPayload.bcc, entityPayload.bccUser, entityPayload.replyToEmail, entityPayload.replyToNickname, entityPayload.isRead, entityPayload.isDeleted, entityPayload.isIncoming, entityPayload.isInternal, entityPayload.isOutgoing, entityPayload.isQuarantined, entityPayload.isQuarantineNotification, entityPayload.isRestored, entityPayload.isRestoreRequested, entityPayload.isRestoreDeclined, entityPayload.saasSpamVerdict, entityPayload.SpfResult.
47
+ saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan.
48
+ The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan.
49
+ saasAttrValue is a string.
44
50
  See [email-read guidance](../../docs/reads.md#email-reads) for continuation, completion status, cursor safety and page replay limitations.
45
51
  Email show <id> includes payload, verdicts, actions taken and available actions (guide pages 10-18).
52
+ Email search and event list accept --expand for read-only show details in each successful row's detail field.
53
+ Expansion is off by default; --expand-limit caps detail attempts (default 5, range 1-8), and --expand-requests caps total HTTP requests (default 10, range 2-10), including auth, list pages and retries.
54
+ Both limit flags require --expand; listing runs first and shares the 60-second and 8 MiB transport budgets with detail reads.
55
+ Inspect the expansion object's expanded, complete and notExpanded fields for every unexpanded ID and its reason: item-cap-reached, request-cap-reached, budget-exhausted or per-item-error (with an error code).
56
+ Partial expansion keeps every list row and successful detail; list complete/cursor describe pagination separately.
57
+ Use --fields emails,expansion or --fields events,expansion to retain expansion status; --full restores full strings after redaction.
58
+ With --all, list pages can consume the entire request budget before any detail is expanded; reduce the list limit or use individual show reads for remaining IDs.
59
+ Expansion never downloads files, invokes available actions or changes remote state.
46
60
  Use --full for strings truncated at 500 characters and --fields for top-level output projection.
47
61
  Cursor strings are never truncated; secret-derived strings are redacted before previews.
48
62
  Downloads require file-profile downloads.allow and an absolute new --out file, created exclusively with mode 0600 or a protected Windows DACL.
@@ -58,7 +72,14 @@ Event action --ids <ids> --name <action> previews by default and requires the ex
58
72
  Every target must offer the action on both reads; nonempty actionParameter objects or repeated action names refuse ENDPOINT_UNVERIFIED until live commissioning resolves the object/string contradiction (guide pages 36-37, 43).
59
73
  Use event show <id> to inspect all available actions, including deferred parameterized actions.
60
74
  Accepted actions return task IDs; use task show <taskId> to inspect asynchronous execution without replaying ambiguous outcomes.
61
- Event list supports documented selectors and shares email search bounds and continuation; event show preserves actions and task show reads asynchronous status without polling (guide pages 30-42).
75
+ Event list supports documented selectors and shares email search bounds and continuation; event show preserves actions and task show reads asynchronous status (guide pages 30-42).
76
+ Task show reads once by default; use task show <id> --wait --timeout 30 for bounded read-only polling.
77
+ --timeout is required with --wait (integer seconds, range 1-60); --poll-interval defaults to 2 seconds (range 1-10), and --wait-requests defaults to 10 total HTTP requests (range 2-10), including auth and retries.
78
+ Wait stops on completed, failed or stopped; init, inprogress and paused remain non-terminal.
79
+ The task snapshot contains the final status; wait reports terminal, polls (validated status snapshots) and reason (terminal, timeout or budget).
80
+ A non-terminal stop exits 0 with the last snapshot and a read-only next-step hint; inspect wait even on exit 0 and include task,wait,help with --fields.
81
+ Timeout covers auth, requests, retries and delays within the shared 8 MiB response cap; failures before any snapshot remain errors.
82
+ Waiting never resubmits an action or changes remote state; a failed or stopped task is terminal, not successful execution.
62
83
  Exception list/show accept phishing-allow, phishing-block, spam-allow, anomaly, click-time, malware, url and dlp (guide pages 49-123).
63
84
  For malware, url and dlp, --type is required; run exception list --help for the documented values.
64
85
  Click-time --items selects item routes; by default list routes are used.
@@ -99,4 +120,4 @@ All live-only facts remain unverified, including token response shape and lifeti
99
120
  | `npx -y @knowttl/checkpoint-axi exception update` | Preview or replace one exception; phishing/spam/click-time require an ID; malware/url/dlp identify it in --input without an ID; anomaly unsupported (guide pages 64-67, 78-81, 94-96, 105-107, 116-117, 126-127) |
100
121
  | `npx -y @knowttl/checkpoint-axi home` | Show local profile context without resolving secrets or making API calls |
101
122
  | `npx -y @knowttl/checkpoint-axi profile list` | List file profiles without resolving secrets; --profile does not filter this list |
102
- | `npx -y @knowttl/checkpoint-axi task show` | Show asynchronous task status and per-action details (API guide pages 30-33) |
123
+ | `npx -y @knowttl/checkpoint-axi task show` | Show asynchronous task status and per-action details, optionally waiting within explicit bounds (API guide pages 30-33) |