@knowttl/checkpoint-axi 0.1.1 → 0.1.2

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
@@ -74,6 +74,14 @@ Use the [command reference](docs/commands.md) for the full catalogue.
74
74
  | home | Show local profile context without resolving secrets | Local read | [Guide](docs/configuration.md) |
75
75
  | profile | List named file profiles | Local read | [Guide](docs/configuration.md) |
76
76
  | task | Inspect asynchronous action status without polling | Read | [Guide](docs/reads.md#event-and-task-reads) |
77
+
78
+ Email search --filter accepts a JSON array of {saasAttrName, saasAttrOp, saasAttrValue}.
79
+ saasAttrName is a field path, not a closed enum (guide pages 19-20).
80
+ Documented examples: entityPayload.subject, entityPayload.fromEmail, entityPayload.attachmentCount.
81
+ 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.
82
+ saasAttrOp: is, contains, startsWith, isEmpty, isNot, notContains, isNotEmpty, greaterThan, lessThan.
83
+ The guide defines operators globally, without a per-field compatibility matrix; examples pair fromEmail with is and attachmentCount with greaterThan.
84
+ saasAttrValue is a string.
77
85
  <!-- command-registry:end -->
78
86
 
79
87
  The command catalogue generates this overview and the full references.
@@ -114,8 +122,8 @@ File metadata never mixes with environment-profile fields; SecretRefs may explic
114
122
  | `CHECKPOINT_AXI_REGION` | Environment-profile region | Required with client ID/secret |
115
123
  | `CHECKPOINT_AXI_CLIENT_ID` | Environment-profile client ID | Required with region/secret |
116
124
  | `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 |
125
+ | `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) |
126
+ | `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
127
  | `CHECKPOINT_AXI_READ_ONLY` | Force read-only: `1`/`true`; `0`/`false` releases override | Unset |
120
128
  | `CHECKPOINT_AXI_WRITE_LOG` | Write journal path | State directory's `writes.log`; blank uses default |
121
129
  | `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,7 @@
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 EMAIL_PAYLOAD_ATTRIBUTES: string[];
4
+ export declare const EMAIL_FILTER_GUIDANCE: string;
3
5
  export declare const catalogue: {
4
6
  readonly "email download": {
5
7
  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)";
@@ -230,7 +232,7 @@ export declare const catalogue: {
230
232
  readonly "email search": {
231
233
  readonly description: "Search emails with a bounded date window and scroll continuation (API guide pages 18-27)";
232
234
  readonly flags: {
233
- readonly filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)";
235
+ readonly filter: `JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. ${string}`;
234
236
  readonly "start-date": string;
235
237
  readonly "end-date": string;
236
238
  readonly limit: string;
@@ -1,6 +1,11 @@
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 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}`);
5
+ export const EMAIL_FILTER_GUIDANCE = "saasAttrName is a field path, not a closed enum (guide pages 19-20). Documented examples: "
6
+ + EMAIL_PAYLOAD_ATTRIBUTES.slice(0, 3).join(", ") + ". Other documented email payload paths (pages 15-16, 24-25): "
7
+ + EMAIL_PAYLOAD_ATTRIBUTES.slice(3).join(", ")
8
+ + ". 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
9
  const common = {
5
10
  config: "Config path; default CHECKPOINT_AXI_CONFIG or platform path",
6
11
  profile: "Profile name; default CHECKPOINT_AXI_PROFILE, env profile, config default, or sole profile",
@@ -82,7 +87,7 @@ export const catalogue = {
82
87
  saas: "SaaS override; default selected profile saas or office365_emails",
83
88
  "saas-entity": "Optional SaaS entity type selector",
84
89
  ...paging,
85
- filter: "JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters (guide pages 19-20)",
90
+ filter: `JSON array of {saasAttrName, saasAttrOp, saasAttrValue} filters. ${EMAIL_FILTER_GUIDANCE}`,
86
91
  }, examples: ["checkpoint-axi email search", "checkpoint-axi email search --all --saas office365_emails", "checkpoint-axi email search --cursor <cursor>"] },
87
92
  "email show": { description: "Show one email with verdicts, actions and available actions (API guide pages 10-18)", flags: common,
88
93
  examples: ["checkpoint-axi email show <id>", "checkpoint-axi email show <id> --full"] },
@@ -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
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import { REGIONS } from "./regions.js";
5
5
  import { gateway, READ_LIMITS } from "./transport.js";
6
+ import { responseInvalid } from "./response-invalid.js";
6
7
  const day = 86_400_000;
7
8
  const timestamp = z.iso.datetime({ offset: true });
8
9
  const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
@@ -10,10 +11,12 @@ const filters = z.array(z.strictObject({ saasAttrName: z.string().min(1),
10
11
  saasAttrValue: z.string() }));
11
12
  const continuation = z.strictObject({ version: z.literal(1), binding: z.string(), start: timestamp, end: timestamp,
12
13
  scroll: z.string().optional(), offset: z.number().int().nonnegative(), replayRisk: z.literal(true).optional() });
14
+ // The guide's prose/table (pp.11,14) says security-result array; its JSON examples use an object.
15
+ const securityResults = z.object({ combinedVerdict: z.unknown().optional() }).passthrough();
13
16
  // Entity schemas and SaaS-specific payloads: guide pages 11-15 and 21-24.
14
17
  const entity = z.object({ entityInfo: z.object({ entityId: z.string().min(1), entityActionState: z.string().optional() }).passthrough(),
15
18
  entityPayload: z.object({ subject: z.string().optional() }).passthrough(),
16
- entitySecurityResults: z.object({ combinedVerdict: z.unknown().optional() }).passthrough().optional(),
19
+ entitySecurityResults: z.union([securityResults, z.array(z.object({}).passthrough())]).optional(),
17
20
  }).passthrough();
18
21
  // Event and task fields: guide pages 31-32, 35-36 and 40-41.
19
22
  const event = z.object({ eventId: z.string().min(1), type: z.string(), state: z.string(), severity: z.string() }).passthrough();
@@ -101,15 +104,19 @@ export async function readCommand(name, id, flags, store, transport) {
101
104
  if (name.endsWith(" show")) {
102
105
  const kind = name.split(" ")[0];
103
106
  const page = await api.read({ path: name === "task show" ? `/task/${id}` : isEvent ? `/event/${id}` : `/search/entity/${id}` });
107
+ const response = { responseData: page.dataIsArray ? page.records : page.records[0] };
108
+ const recordPath = page.dataIsArray ? ["responseData", 0] : ["responseData"];
104
109
  if (page.records.length === 0)
105
110
  throw new AxiError(`${kind} was not found`, "NOT_FOUND", []);
106
111
  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", []);
112
+ if (page.records.length !== 1)
113
+ throw responseInvalid(`Invalid ${kind} detail response`, response, [["responseData"]]);
114
+ if (!parsed.success)
115
+ throw responseInvalid(`Invalid ${kind} detail response`, response, parsed.error.issues.map((issue) => [...recordPath, ...issue.path]));
109
116
  const record = page.records[0];
110
117
  const returnedId = name === "task show" ? String(record.id) : isEvent ? record.eventId : record.entityInfo?.entityId;
111
118
  if (returnedId !== id)
112
- throw new AxiError(`Invalid ${kind} detail response`, "RESPONSE_INVALID", []);
119
+ throw responseInvalid(`Invalid ${kind} detail response`, response, [[...recordPath, ...(name === "task show" ? ["id"] : isEvent ? ["eventId"] : ["entityInfo", "entityId"])]]);
113
120
  return { [kind]: parsed.data };
114
121
  }
115
122
  const results = [];
@@ -131,11 +138,14 @@ export async function readCommand(name, id, flags, store, transport) {
131
138
  throw error;
132
139
  }
133
140
  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", []);
141
+ const response = { responseData: page.dataIsArray ? page.records : page.records[0] };
142
+ if (!parsed.success)
143
+ throw responseInvalid("Invalid search response", response, parsed.error.issues.map((issue) => ["responseData", ...(page.dataIsArray ? issue.path : issue.path.slice(1))]));
144
+ if (offset > page.records.length)
145
+ throw responseInvalid("Invalid search response", response, [["responseData"]]);
136
146
  // Never wrap an echoed credential in a reversible continuation cursor.
137
147
  if (page.scrollId && store.redactor.text(page.scrollId) !== page.scrollId)
138
- throw new AxiError("Unsafe API scrollId", "RESPONSE_INVALID", []);
148
+ throw responseInvalid("Unsafe API scrollId", { responseEnvelope: { scrollId: page.scrollId } }, [["responseEnvelope", "scrollId"]]);
139
149
  total = page.total ?? total;
140
150
  const rows = parsed.data.slice(offset, offset + limit - results.length);
141
151
  results.push(...rows.map((record) => {
@@ -145,7 +155,7 @@ export async function readCommand(name, id, flags, store, transport) {
145
155
  }
146
156
  const value = entity.parse(record);
147
157
  return { id: value.entityInfo.entityId, subject: value.entityPayload.subject ?? "",
148
- state: value.entityInfo.entityActionState ?? "", verdict: value.entitySecurityResults?.combinedVerdict ?? null };
158
+ state: value.entityInfo.entityActionState ?? "", verdict: Array.isArray(value.entitySecurityResults) ? null : value.entitySecurityResults?.combinedVerdict ?? null };
149
159
  }));
150
160
  offset += rows.length;
151
161
  if (offset < page.records.length) {
@@ -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
  };
@@ -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).
@@ -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,6 +36,12 @@ 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
46
  Event detail includes available actions; task detail reads status and per-action results once, without waiting or polling:
41
47
 
package/docs/commands.md CHANGED
@@ -197,7 +197,7 @@ 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
+ | `--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
201
 
202
202
  ```sh
203
203
  checkpoint-axi email search
@@ -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,6 +42,13 @@ 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).
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.2",
4
4
  "description": "Inspect Check Point Email Security through compact, guarded AXI commands",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,6 +41,12 @@ 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).
46
52
  Use --full for strings truncated at 500 characters and --fields for top-level output projection.