@knowttl/checkpoint-axi 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -2
  3. package/axi-credential.schema.json +35 -0
  4. package/bin/checkpoint-axi-hook.js +9 -0
  5. package/bin/checkpoint-axi.js +8 -0
  6. package/bin/version.js +3 -0
  7. package/dist/src/catalogue.d.ts +317 -0
  8. package/dist/src/catalogue.js +103 -0
  9. package/dist/src/cli.d.ts +9 -0
  10. package/dist/src/cli.js +22 -0
  11. package/dist/src/doctor.d.ts +3 -0
  12. package/dist/src/doctor.js +37 -0
  13. package/dist/src/downloads.d.ts +3 -0
  14. package/dist/src/downloads.js +52 -0
  15. package/dist/src/exceptions.d.ts +37 -0
  16. package/dist/src/exceptions.js +81 -0
  17. package/dist/src/local-commands.d.ts +3 -0
  18. package/dist/src/local-commands.js +123 -0
  19. package/dist/src/private-file.d.ts +10 -0
  20. package/dist/src/private-file.js +65 -0
  21. package/dist/src/profiles.d.ts +122 -0
  22. package/dist/src/profiles.js +137 -0
  23. package/dist/src/reads.d.ts +3 -0
  24. package/dist/src/reads.js +169 -0
  25. package/dist/src/redact.d.ts +6 -0
  26. package/dist/src/redact.js +25 -0
  27. package/dist/src/regions.d.ts +9 -0
  28. package/dist/src/regions.js +10 -0
  29. package/dist/src/transport.d.ts +86 -0
  30. package/dist/src/transport.js +306 -0
  31. package/dist/src/write-exceptions.d.ts +4 -0
  32. package/dist/src/write-exceptions.js +97 -0
  33. package/dist/src/write-sectools.d.ts +5 -0
  34. package/dist/src/write-sectools.js +107 -0
  35. package/dist/src/writes.d.ts +30 -0
  36. package/dist/src/writes.js +295 -0
  37. package/docs/commands.md +461 -0
  38. package/inventory/capabilities.json +217 -0
  39. package/inventory/specs.json +12 -0
  40. package/package.json +54 -4
  41. package/skills/checkpoint-axi/SKILL.md +102 -0
@@ -0,0 +1,81 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { z } from "zod";
3
+ import { gateway, READ_LIMITS } from "./transport.js";
4
+ // Read routes and selectors: guide pages 49-60, 69-74, 83, 87-91, 100-102, 110-112 and 120-123.
5
+ export const exceptionFamilies = {
6
+ "phishing-allow": { path: "/exceptions/whitelist", types: [] },
7
+ "phishing-block": { path: "/exceptions/blacklist", types: [] },
8
+ "spam-allow": { path: "/exceptions/spam_whitelist", types: [] },
9
+ anomaly: { path: "/sectools/anomaly/exceptions", types: [] },
10
+ "click-time": { path: "/sectools/click_time_protection/exceptions", types: [] },
11
+ malware: { path: "/sectool-exceptions/checkpoint2/exceptions", types: ["hash", "micro_hash", "file_type", "ppat_sender_name", "allow_file_type", "sender_email"] },
12
+ url: { path: "/sectool-exceptions/avanan_url/exceptions", types: ["allow-url", "allow-domain", "block-url", "block-domain"] },
13
+ dlp: { path: "/sectool-exceptions/avanan_dlp/exceptions", types: ["hash", "text_content", "sender_email", "recipient_email"] },
14
+ };
15
+ // Only these families have published response fields (pages 50, 56-60 and 70-74).
16
+ const exception = z.object({ entityId: z.string().min(1), senderEmail: z.string().nullish(),
17
+ subject: z.string().nullish(), comment: z.string().nullish() }).passthrough();
18
+ export async function exceptionCommand(name, kind, value, flags, store, transport) {
19
+ const invalid = (message) => { throw new AxiError(message, "VALIDATION_ERROR", [`Run checkpoint-axi ${name} --help`]); };
20
+ if (!kind || !Object.hasOwn(exceptionFamilies, kind))
21
+ invalid(`Choose an exception kind: ${Object.keys(exceptionFamilies).join(", ")}`);
22
+ const family = exceptionFamilies[kind];
23
+ const show = name === "exception show";
24
+ const documented = family.path.startsWith("/exceptions/");
25
+ const type = flags.get("--type");
26
+ if (family.types.length ? !type || !family.types.includes(type) : type !== undefined) {
27
+ invalid(family.types.length ? `--type must be one of: ${family.types.join(", ")}` : "--type is only valid for malware, url and dlp");
28
+ }
29
+ if (flags.has("--items") && kind !== "click-time")
30
+ invalid("--items is only valid for click-time");
31
+ if (show && (!value?.trim() || /[\u0000-\u001f\u007f]/.test(value) || value === "." || value === ".."))
32
+ invalid("show requires one nonempty exception ID or value");
33
+ if (show && documented && !/^[A-Za-z0-9_-]+$/.test(value))
34
+ invalid("Exception ID must contain letters, digits, underscores or hyphens");
35
+ const limitText = flags.get("--limit") ?? "50";
36
+ if (!/^[1-9][0-9]*$/.test(limitText) || Number(limitText) > READ_LIMITS.rows)
37
+ invalid("--limit must be an integer from 1 to 1000");
38
+ if (flags.has("--all") && flags.has("--limit"))
39
+ invalid("Use either --all or --limit");
40
+ if (show && kind === "anomaly")
41
+ throw new AxiError("Anomaly detail endpoint and response identity are undocumented (guide pages 83-84)", "ENDPOINT_UNVERIFIED", ["Use checkpoint-axi exception list anomaly; owner commissioning happens later through Artium"]);
42
+ let path = family.path;
43
+ if (type)
44
+ path += `/${type}`;
45
+ if (flags.has("--items"))
46
+ path += "/items";
47
+ if (show) {
48
+ try {
49
+ path += `/${encodeURIComponent(value)}`;
50
+ }
51
+ catch {
52
+ invalid("Exception value must be valid Unicode");
53
+ }
54
+ }
55
+ store.readOnly();
56
+ const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
57
+ const api = gateway(profile, store.credentials(profile, true), store.redactor, transport);
58
+ const page = await api.read({ path: path });
59
+ if (documented && !z.array(exception).safeParse(page.records).success)
60
+ throw new AxiError("Invalid exception response", "RESPONSE_INVALID", []);
61
+ const unverified = documented ? {} : { responseSchema: "unverified" };
62
+ if (show) {
63
+ if (!page.records.length)
64
+ throw new AxiError("Exception was not found", "NOT_FOUND", []);
65
+ if (documented && (page.records.length !== 1 || exception.parse(page.records[0]).entityId !== value)) {
66
+ throw new AxiError("Invalid exception detail response", "RESPONSE_INVALID", []);
67
+ }
68
+ return { exception: documented ? page.records[0] : page.records, ...unverified };
69
+ }
70
+ const limit = flags.has("--all") ? READ_LIMITS.rows : Number(limitText);
71
+ const rows = page.records.slice(0, limit);
72
+ const complete = rows.length === page.records.length && !page.scrollId && (page.total === undefined || page.total <= rows.length);
73
+ return { exceptions: documented ? rows.map((row) => {
74
+ const record = exception.parse(row);
75
+ return { id: record.entityId, sender: record.senderEmail ?? "", subject: record.subject ?? "", comment: record.comment ?? "" };
76
+ }) : rows, count: rows.length, ...(page.total === undefined ? {} : { total: page.total }), complete, ...unverified,
77
+ ...(!complete ? { reason: "No continuation request parameter is documented for this exception route" } : {}),
78
+ help: [rows.length ? kind === "anomaly" ? "Anomaly detail remains unavailable pending response identity commissioning"
79
+ : `Run checkpoint-axi exception show ${kind} <id-or-value>${type ? ` --type ${type}` : ""}${flags.has("--items") ? " --items" : ""} for details`
80
+ : `No ${kind} exceptions found`, ...(!complete && limit < READ_LIMITS.rows ? ["Repeat with --all for up to 1000 rows; server-side continuation remains undocumented"] : [])] };
81
+ }
@@ -0,0 +1,3 @@
1
+ import { credentialStore } from "./profiles.js";
2
+ import type { gateway } from "./transport.js";
3
+ export declare function localCommand(argv: string[], store: ReturnType<typeof credentialStore>, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
@@ -0,0 +1,123 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { homedir } from "node:os";
3
+ import { resolve } from "node:path";
4
+ import { catalogue, DESCRIPTION, help, SETUP_GUIDANCE } from "./catalogue.js";
5
+ import { doctor } from "./doctor.js";
6
+ import { readCommand } from "./reads.js";
7
+ import { exceptionCommand } from "./exceptions.js";
8
+ import { emailWriteCommand, eventWriteCommand } from "./writes.js";
9
+ import { exceptionWriteCommand } from "./write-exceptions.js";
10
+ import { downloadCommand } from "./downloads.js";
11
+ export async function localCommand(argv, store, transport) {
12
+ if (argv.length === 1 && argv[0] === "--help")
13
+ return help();
14
+ const name = !argv.length || argv[0]?.startsWith("--") ? "home" : argv[0] === "home" || argv[0] === "doctor" ? argv[0] : argv.slice(0, 2).join(" ");
15
+ const entry = Object.hasOwn(catalogue, name) ? catalogue[name] : undefined;
16
+ const usage = (message = "Unknown command, argument, duplicate or invalid flag") => {
17
+ throw new AxiError(message, "VALIDATION_ERROR", [
18
+ `Valid commands: ${Object.keys(catalogue).join(", ")}; valid flags: ${entry ? Object.keys(entry.flags).map((flag) => `--${flag}`).join(", ") : "run checkpoint-axi --help"}`,
19
+ ]);
20
+ };
21
+ if (!entry)
22
+ return usage();
23
+ const flags = new Map();
24
+ const isExceptionWrite = name === "exception create" || name === "exception update" || name === "exception remove" || name === "exception remove-items" || name === "exception clear";
25
+ const isException = name === "exception list" || name === "exception show" || isExceptionWrite;
26
+ const isDownload = name === "email download" || name === "attachment download" || name === "attachment download-all";
27
+ const kind = isException && !argv[2]?.startsWith("--") ? argv[2] : undefined;
28
+ const idIndex = isException ? 3 : 2;
29
+ const entityId = (isDownload || name.endsWith(" show") || name === "exception update" || name === "exception remove") && !argv[idIndex]?.startsWith("--") ? argv[idIndex] : undefined;
30
+ const attachmentId = name === "attachment download" && entityId !== undefined && !argv[3]?.startsWith("--") ? argv[3] : undefined;
31
+ const start = name === "home" ? (argv[0] === "home" ? 1 : 0) : name === "doctor" ? 1
32
+ : isException ? 2 + Number(kind !== undefined) + Number(entityId !== undefined) : entityId === undefined ? 2 : 3 + Number(attachmentId !== undefined);
33
+ for (let index = start; index < argv.length; index++) {
34
+ const flag = argv[index];
35
+ if (!flag.startsWith("--"))
36
+ usage();
37
+ if (!Object.hasOwn(entry.flags, flag.slice(2)) || flags.has(flag)) {
38
+ const label = flag.split("=")[0];
39
+ usage(/^--[A-Za-z][A-Za-z0-9-]*$/.test(label) ? `Unknown or duplicate flag ${label}` : undefined);
40
+ }
41
+ if (["--help", "--full", "--all", "--items", "--specific", "--original", "--execute", "--dry-run"].includes(flag))
42
+ flags.set(flag, "true");
43
+ else {
44
+ const value = argv[++index];
45
+ if (!value || value.startsWith("--"))
46
+ usage();
47
+ flags.set(flag, value);
48
+ }
49
+ }
50
+ if (flags.has("--help"))
51
+ return { command: name, ...entry };
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]]
54
+ : name === "doctor" ? ["region", "saas", "checks", "pass", "help"] : name === "auth status" ? ["profile", "source", "region", "origin", "saas", "clientSecret", "readOnly"]
55
+ : name === "profile list" ? ["profiles", "count", "config"] : name === "config path" ? ["config", "state"]
56
+ : ["bin", "description", "status", "profiles", "count", "help"];
57
+ const isWrite = name === "email quarantine" || name === "email restore" || name === "event action" || isExceptionWrite;
58
+ if (isWrite)
59
+ available.splice(0, available.length, "outcome", "preview", "auditId", "tasks", "response", "responseSchema", "help");
60
+ if (isDownload)
61
+ available.splice(0, available.length, "path", "size", "sha256");
62
+ const fields = flags.get("--fields")?.split(",") ?? available;
63
+ if (fields.some((field) => !available.includes(field)) || new Set(fields).size !== fields.length)
64
+ usage();
65
+ const config = flags.get("--config");
66
+ let result;
67
+ if (isDownload)
68
+ result = await downloadCommand(name, entityId, attachmentId, flags, store, transport);
69
+ else if (name === "exception create" || name === "exception update" || name === "exception remove" || name === "exception remove-items" || name === "exception clear")
70
+ result = await exceptionWriteCommand(name, kind, entityId, flags, store, transport);
71
+ else if (name === "event action")
72
+ result = await eventWriteCommand(flags, store, transport);
73
+ else if (name === "email quarantine" || name === "email restore")
74
+ result = await emailWriteCommand(name, flags, store, transport);
75
+ else if (name === "exception list" || name === "exception show")
76
+ result = await exceptionCommand(name, kind, entityId, flags, store, transport);
77
+ else if (name === "email search" || name === "email show" || name === "event list" || name === "event show" || name === "task show")
78
+ result = await readCommand(name, entityId, flags, store, transport);
79
+ else if (name === "doctor")
80
+ result = await doctor(store, config, flags.get("--profile"), flags.get("--saas"), transport);
81
+ else if (name === "auth status")
82
+ result = store.status(config, flags.get("--profile"));
83
+ else if (name === "profile list")
84
+ result = store.list(config);
85
+ else if (name === "config path")
86
+ result = { config: store.configPath(config), state: store.statePath() };
87
+ else {
88
+ const listed = store.list(config);
89
+ // Selection errors matter when context exists, but a new installation has a useful empty home.
90
+ const loaded = store.load(config);
91
+ let status = "not configured";
92
+ try {
93
+ const selected = store.select(loaded.config, flags.get("--profile"));
94
+ status = `configured: ${selected.name} (${selected.region}); not verified with the API`;
95
+ }
96
+ catch (error) {
97
+ if (!(error instanceof AxiError) || error.code !== "PROFILE_REQUIRED")
98
+ throw error;
99
+ }
100
+ const executable = resolve(process.argv[1] ?? "bin/checkpoint-axi.js");
101
+ const home = homedir();
102
+ result = { bin: executable.startsWith(`${home}/`) ? `~${executable.slice(home.length)}` : executable,
103
+ description: DESCRIPTION, status, profiles: listed.profiles, count: listed.count,
104
+ help: [SETUP_GUIDANCE, "Run checkpoint-axi auth status to validate local credentials", "Run checkpoint-axi profile list to inspect configured profiles"] };
105
+ }
106
+ let truncated = false;
107
+ const preview = (value) => {
108
+ if (typeof value === "string" && value.length > 500 && !flags.has("--full")) {
109
+ truncated = true;
110
+ return `${value.slice(0, 500)}... (truncated, ${value.length} chars total)`;
111
+ }
112
+ if (Array.isArray(value))
113
+ return value.map(preview);
114
+ if (value && typeof value === "object")
115
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, preview(child)]));
116
+ return value;
117
+ };
118
+ const projected = Object.fromEntries(fields.filter((field) => result[field] !== undefined).map((field) => [field,
119
+ field === "cursor" || isDownload ? store.redactor.value(result[field]) : preview(store.redactor.value(result[field]))]));
120
+ if (truncated && !isWrite)
121
+ projected.help = [...(Array.isArray(projected.help) ? projected.help : []), "Repeat the command with --full to see complete strings"];
122
+ return projected;
123
+ }
@@ -0,0 +1,10 @@
1
+ export type WindowsAcl = (path: string, verify: boolean) => void;
2
+ export declare const windowsAcl: WindowsAcl;
3
+ export declare function protectWindowsFile(path: string): void;
4
+ export declare function toolDirectory(kind: "config" | "state", env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, home?: string): string;
5
+ export type PrivateFileOptions = {
6
+ platform?: NodeJS.Platform;
7
+ acl?: WindowsAcl;
8
+ uid?: number;
9
+ };
10
+ export declare function readPrivateFile(path: string, secret: boolean, verifyAcl: boolean, options?: PrivateFileOptions): string;
@@ -0,0 +1,65 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, win32 } from "node:path";
5
+ import { AxiError } from "axi-sdk-js";
6
+ export const windowsAcl = (path, verify) => {
7
+ const quoted = `'${path.replaceAll("'", "''")}'`;
8
+ const script = `$ErrorActionPreference='Stop'; $p=${quoted}; $item=Get-Item -LiteralPath $p -Force; `
9
+ + `if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse point' }; `
10
+ + (verify ? `$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User; $allowed=@($sid.Value,'S-1-5-18','S-1-5-32-544'); `
11
+ + `$acl=Get-Acl -LiteralPath $p; if (!$acl.AreAccessRulesProtected -or ($acl.Owner -ne $sid.Translate([Security.Principal.NTAccount]).Value -and $acl.Owner -ne $sid.Value)) { throw 'Unprotected ACL' }; `
12
+ + `$seen=@(); foreach ($r in $acl.Access) { $s=$r.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value; if ($s -notin $allowed -or $r.IsInherited -or $r.AccessControlType -ne 'Allow' -or $r.FileSystemRights -ne [Security.AccessControl.FileSystemRights]::FullControl) { throw 'Unexpected ACL' }; $seen+=$s }; foreach ($s in $allowed) { if ($s -notin $seen) { throw 'Missing ACL' } }` : "");
13
+ execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "pipe" });
14
+ };
15
+ export function protectWindowsFile(path) {
16
+ const quoted = `'${path.replaceAll("'", "''")}'`;
17
+ const script = `$ErrorActionPreference='Stop'; $p=${quoted}; $item=Get-Item -LiteralPath $p -Force; `
18
+ + `if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Reparse point' }; `
19
+ + `$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User; $acl=Get-Acl -LiteralPath $p; `
20
+ + `$acl.SetAccessRuleProtection($true,$false); foreach ($r in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($r) }; $acl.SetOwner($sid); `
21
+ + `foreach ($s in @($sid.Value,'S-1-5-18','S-1-5-32-544')) { $id=New-Object Security.Principal.SecurityIdentifier($s); `
22
+ + `$rule=New-Object Security.AccessControl.FileSystemAccessRule($id,'FullControl','Allow'); $acl.AddAccessRule($rule) }; Set-Acl -LiteralPath $p -AclObject $acl`;
23
+ execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "pipe" });
24
+ windowsAcl(path, true);
25
+ }
26
+ export function toolDirectory(kind, env = process.env, platform = process.platform, home = homedir()) {
27
+ if (platform === "win32") {
28
+ const base = env[kind === "config" ? "APPDATA" : "LOCALAPPDATA"];
29
+ return win32.join(base && win32.isAbsolute(base) ? base : win32.join(home, "AppData", kind === "config" ? "Roaming" : "Local"), "checkpoint-axi");
30
+ }
31
+ const xdg = env[kind === "config" ? "XDG_CONFIG_HOME" : "XDG_STATE_HOME"];
32
+ return join(xdg && isAbsolute(xdg) ? xdg : join(home, kind === "config" ? ".config" : ".local/state"), "checkpoint-axi");
33
+ }
34
+ export function readPrivateFile(path, secret, verifyAcl, options = {}) {
35
+ const platform = options.platform ?? process.platform;
36
+ const insecure = () => {
37
+ throw new AxiError("Insecure private file", "CONFIG_INSECURE", [
38
+ platform === "win32" ? "Apply a protected DACL granting FullControl only to the current user, SYSTEM and Administrators"
39
+ : "Ensure the file is regular, owned by the current user and has mode 600",
40
+ ]);
41
+ };
42
+ function validate(stat) {
43
+ if (!stat.isFile() || stat.isSymbolicLink())
44
+ insecure();
45
+ if (platform !== "win32" && (stat.uid !== (options.uid ?? process.getuid()) || (stat.mode & (secret ? 0o077 : 0o022)) !== 0))
46
+ insecure();
47
+ }
48
+ validate(lstatSync(path));
49
+ if (platform === "win32") {
50
+ try {
51
+ (options.acl ?? windowsAcl)(path, verifyAcl);
52
+ }
53
+ catch {
54
+ insecure();
55
+ }
56
+ }
57
+ const fd = openSync(path, constants.O_RDONLY | (platform === "win32" ? 0 : constants.O_NOFOLLOW));
58
+ try {
59
+ validate(fstatSync(fd));
60
+ return readFileSync(fd, "utf8");
61
+ }
62
+ finally {
63
+ closeSync(fd);
64
+ }
65
+ }
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ import { type PrivateFileOptions } from "./private-file.js";
3
+ import { SecretRedactor } from "./redact.js";
4
+ export type CredentialContext = PrivateFileOptions & {
5
+ env?: NodeJS.ProcessEnv;
6
+ home?: string;
7
+ };
8
+ declare function schema(platform: NodeJS.Platform): z.ZodObject<{
9
+ version: z.ZodLiteral<1>;
10
+ defaultProfile: z.ZodOptional<z.ZodString>;
11
+ profiles: z.ZodRecord<z.ZodString, z.ZodObject<{
12
+ region: z.ZodEnum<{
13
+ us: "us";
14
+ eu: "eu";
15
+ ca: "ca";
16
+ au: "au";
17
+ uk: "uk";
18
+ uae: "uae";
19
+ in: "in";
20
+ }>;
21
+ clientId: z.ZodString;
22
+ clientSecret: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
23
+ env: z.ZodString;
24
+ }, z.core.$strict>, z.ZodObject<{
25
+ file: z.ZodString;
26
+ }, z.core.$strict>]>>;
27
+ saas: z.ZodDefault<z.ZodString>;
28
+ scope: z.ZodOptional<z.ZodString>;
29
+ downloads: z.ZodDefault<z.ZodObject<{
30
+ allow: z.ZodBoolean;
31
+ }, z.core.$strict>>;
32
+ writes: z.ZodDefault<z.ZodObject<{
33
+ allowWrites: z.ZodBoolean;
34
+ operations: z.ZodArray<z.ZodString>;
35
+ }, z.core.$strict>>;
36
+ }, z.core.$strict>>;
37
+ }, z.core.$strict>;
38
+ export type Profile = z.infer<ReturnType<typeof schema>>["profiles"][string];
39
+ export type SelectedProfile = Profile & {
40
+ name: string;
41
+ source: "flag" | "env" | "env-profile" | "config-default" | "sole";
42
+ };
43
+ export declare function credentialStore(options?: CredentialContext, redactor?: SecretRedactor): {
44
+ configPath: (explicit?: string) => string;
45
+ statePath: () => string;
46
+ writeLogPath: () => string;
47
+ readOnly: () => boolean;
48
+ load: (explicit?: string, verifyAcl?: boolean) => {
49
+ path: string;
50
+ config: {
51
+ version: 1;
52
+ profiles: Record<string, {
53
+ region: "us" | "eu" | "ca" | "au" | "uk" | "uae" | "in";
54
+ clientId: string;
55
+ clientSecret: {
56
+ env: string;
57
+ } | {
58
+ file: string;
59
+ };
60
+ saas: string;
61
+ downloads: {
62
+ allow: boolean;
63
+ };
64
+ writes: {
65
+ allowWrites: boolean;
66
+ operations: string[];
67
+ };
68
+ scope?: string | undefined;
69
+ }>;
70
+ defaultProfile?: string | undefined;
71
+ };
72
+ };
73
+ select: (config: ReturnType<(explicit?: string, verifyAcl?: boolean) => {
74
+ path: string;
75
+ config: {
76
+ version: 1;
77
+ profiles: Record<string, {
78
+ region: "us" | "eu" | "ca" | "au" | "uk" | "uae" | "in";
79
+ clientId: string;
80
+ clientSecret: {
81
+ env: string;
82
+ } | {
83
+ file: string;
84
+ };
85
+ saas: string;
86
+ downloads: {
87
+ allow: boolean;
88
+ };
89
+ writes: {
90
+ allowWrites: boolean;
91
+ operations: string[];
92
+ };
93
+ scope?: string | undefined;
94
+ }>;
95
+ defaultProfile?: string | undefined;
96
+ };
97
+ }>["config"], flag?: string) => SelectedProfile;
98
+ credentials: (profile: SelectedProfile, verifyAcl?: boolean) => {
99
+ clientId: string;
100
+ clientSecret: string;
101
+ };
102
+ status: (explicit?: string, flag?: string) => {
103
+ profile: string;
104
+ source: "env" | "flag" | "env-profile" | "config-default" | "sole";
105
+ region: "us" | "eu" | "ca" | "au" | "uk" | "uae" | "in";
106
+ origin: "https://cloudinfra-gw-us.portal.checkpoint.com" | "https://cloudinfra-gw.portal.checkpoint.com" | "https://cloudinfra-gw.ca.portal.checkpoint.com" | "https://cloudinfra-gw.ap.portal.checkpoint.com" | "https://cloudinfra-gw.uk.portal.checkpoint.com" | "https://cloudinfra-gw.me.portal.checkpoint.com" | "https://cloudinfra-gw.in.portal.checkpoint.com";
107
+ saas: string;
108
+ clientSecret: string;
109
+ readOnly: boolean;
110
+ };
111
+ list: (explicit?: string) => {
112
+ profiles: {
113
+ name: string;
114
+ region: "us" | "eu" | "ca" | "au" | "uk" | "uae" | "in";
115
+ default: boolean;
116
+ }[];
117
+ count: number;
118
+ config: string;
119
+ };
120
+ redactor: SecretRedactor;
121
+ };
122
+ export {};
@@ -0,0 +1,137 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { z } from "zod";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, resolve, win32 } from "node:path";
5
+ import { readPrivateFile, toolDirectory } from "./private-file.js";
6
+ import { REGIONS } from "./regions.js";
7
+ import { SecretRedactor } from "./redact.js";
8
+ const profileName = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/);
9
+ const region = z.enum(Object.keys(REGIONS));
10
+ const nonempty = z.string().trim().min(1);
11
+ function schema(platform) {
12
+ const secretRef = z.union([
13
+ z.strictObject({ env: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/) }),
14
+ z.strictObject({ file: z.string().refine((path) => platform === "win32" ? /^[A-Za-z]:[\\/]/.test(path) : isAbsolute(path)) }),
15
+ ]);
16
+ const profile = z.strictObject({
17
+ region,
18
+ clientId: nonempty,
19
+ clientSecret: secretRef.default({ env: "CHECKPOINT_AXI_CLIENT_SECRET" }),
20
+ saas: nonempty.default("office365_emails"),
21
+ scope: nonempty.optional(),
22
+ downloads: z.strictObject({ allow: z.boolean() }).default({ allow: false }),
23
+ writes: z.strictObject({ allowWrites: z.boolean(), operations: z.array(z.string().regex(/^[a-z][A-Za-z0-9_.-]*$/)) })
24
+ .default({ allowWrites: false, operations: [] }),
25
+ });
26
+ return z.strictObject({ version: z.literal(1), defaultProfile: profileName.optional(), profiles: z.record(profileName, profile) })
27
+ .refine((config) => config.defaultProfile === undefined || Object.hasOwn(config.profiles, config.defaultProfile));
28
+ }
29
+ export function credentialStore(options = {}, redactor = new SecretRedactor()) {
30
+ const env = options.env ?? process.env;
31
+ const platform = options.platform ?? process.platform;
32
+ const home = options.home ?? homedir();
33
+ const invalid = (message) => {
34
+ throw new AxiError(message, "CONFIG_INVALID", [
35
+ "Use version: 1, named profiles, a supported region, clientId and a clientSecret env/file SecretRef; run checkpoint-axi auth status --help",
36
+ ]);
37
+ };
38
+ redactor.add(env.CHECKPOINT_AXI_CLIENT_SECRET);
39
+ function configPath(explicit) {
40
+ const selected = explicit ?? env.CHECKPOINT_AXI_CONFIG;
41
+ if (selected !== undefined && !selected.trim())
42
+ invalid("Config path must not be blank");
43
+ return selected !== undefined ? resolve(selected) : (platform === "win32" ? win32.join : join)(toolDirectory("config", env, platform, home), "config.json");
44
+ }
45
+ function readOnly() {
46
+ const value = env.CHECKPOINT_AXI_READ_ONLY?.toLowerCase();
47
+ if (value === undefined || value === "0" || value === "false")
48
+ return false;
49
+ if (value === "1" || value === "true")
50
+ return true;
51
+ return invalid("Read-only override must be 1, true, 0 or false");
52
+ }
53
+ function load(explicit, verifyAcl = false) {
54
+ const path = configPath(explicit);
55
+ let raw;
56
+ try {
57
+ raw = JSON.parse(readPrivateFile(path, false, verifyAcl, options));
58
+ }
59
+ catch (error) {
60
+ if (error instanceof AxiError)
61
+ throw error;
62
+ if (explicit === undefined && env.CHECKPOINT_AXI_CONFIG === undefined && error.code === "ENOENT") {
63
+ return { path, config: { version: 1, profiles: {}, defaultProfile: undefined } };
64
+ }
65
+ return invalid("Cannot read profile configuration as JSON; check its path and syntax");
66
+ }
67
+ // Register malformed inline secrets too, before any validation result is rendered.
68
+ if (raw && typeof raw === "object" && "profiles" in raw && raw.profiles && typeof raw.profiles === "object") {
69
+ for (const profile of Object.values(raw.profiles)) {
70
+ if (!profile || typeof profile !== "object" || !("clientSecret" in profile))
71
+ continue;
72
+ const ref = profile.clientSecret;
73
+ if (typeof ref === "string")
74
+ redactor.add(ref);
75
+ if (ref && typeof ref === "object" && "env" in ref && typeof ref.env === "string")
76
+ redactor.add(env[ref.env]);
77
+ }
78
+ }
79
+ const parsed = schema(platform).safeParse(raw);
80
+ if (!parsed.success)
81
+ return invalid("Invalid configuration: check profile names, region, defaultProfile, unknown fields and clientSecret SecretRef");
82
+ return { path, config: parsed.data };
83
+ }
84
+ function select(config, flag) {
85
+ const selected = flag ?? env.CHECKPOINT_AXI_PROFILE;
86
+ if (selected !== undefined && !profileName.safeParse(selected).success)
87
+ invalid("Invalid selected profile name");
88
+ const envFields = ["REGION", "CLIENT_ID", "SAAS", "SCOPE"];
89
+ if (selected === undefined && envFields.some((field) => env[`CHECKPOINT_AXI_${field}`] !== undefined)) {
90
+ 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())) {
92
+ return invalid("Incomplete or invalid env profile; set REGION, CLIENT_ID and CLIENT_SECRET together");
93
+ }
94
+ return { region: env.CHECKPOINT_AXI_REGION, clientId: env.CHECKPOINT_AXI_CLIENT_ID.trim(),
95
+ clientSecret: { env: "CHECKPOINT_AXI_CLIENT_SECRET" }, saas: env.CHECKPOINT_AXI_SAAS?.trim() ?? "office365_emails", scope: env.CHECKPOINT_AXI_SCOPE?.trim(),
96
+ downloads: { allow: false }, writes: { allowWrites: false, operations: [] }, name: "env", source: "env-profile" };
97
+ }
98
+ const names = Object.keys(config.profiles);
99
+ const name = selected ?? config.defaultProfile ?? (names.length === 1 ? names[0] : undefined);
100
+ if (name === undefined)
101
+ throw new AxiError(names.length ? "Select one of the configured profiles" : "No profiles are configured", names.length ? "PROFILE_AMBIGUOUS" : "PROFILE_REQUIRED", ["Pass --profile <name> or configure defaultProfile; run checkpoint-axi config path"]);
102
+ if (!Object.hasOwn(config.profiles, name))
103
+ throw new AxiError("Selected profile is not configured", "PROFILE_NOT_FOUND", ["Run checkpoint-axi profile list"]);
104
+ return { ...config.profiles[name], name, source: flag !== undefined ? "flag" : env.CHECKPOINT_AXI_PROFILE !== undefined ? "env" : config.defaultProfile !== undefined ? "config-default" : "sole" };
105
+ }
106
+ function credentials(profile, verifyAcl = false) {
107
+ const ref = profile.clientSecret;
108
+ let value;
109
+ try {
110
+ value = "env" in ref ? env[ref.env] : readPrivateFile(ref.file, true, verifyAcl, options).replace(/^\uFEFF/, "").trimEnd();
111
+ }
112
+ catch (error) {
113
+ if (error instanceof AxiError)
114
+ throw error;
115
+ throw new AxiError("Cannot read clientSecret SecretRef file", "AUTH_REQUIRED", ["Check the SecretRef file exists and is readable"]);
116
+ }
117
+ redactor.add(value);
118
+ if (!value?.trim())
119
+ throw new AxiError("clientSecret is required", "AUTH_REQUIRED", ["Set the selected clientSecret SecretRef source; run checkpoint-axi auth status --help"]);
120
+ return { clientId: profile.clientId, clientSecret: value };
121
+ }
122
+ function status(explicit, flag) {
123
+ const forced = readOnly();
124
+ const loaded = load(explicit, true);
125
+ const profile = select(loaded.config, flag);
126
+ credentials(profile, true);
127
+ return { profile: profile.name, source: profile.source, region: profile.region, origin: REGIONS[profile.region], saas: profile.saas,
128
+ clientSecret: "env" in profile.clientSecret ? "env" : "file", readOnly: forced || !profile.writes.allowWrites || profile.writes.operations.length === 0 };
129
+ }
130
+ function list(explicit) {
131
+ readOnly();
132
+ const loaded = load(explicit);
133
+ return { profiles: Object.entries(loaded.config.profiles).map(([name, profile]) => ({ name, region: profile.region, default: name === loaded.config.defaultProfile })),
134
+ count: Object.keys(loaded.config.profiles).length, config: loaded.path };
135
+ }
136
+ return { configPath, statePath: () => toolDirectory("state", env, platform, home), writeLogPath: () => env.CHECKPOINT_AXI_WRITE_LOG?.trim() || join(toolDirectory("state", env, platform, home), "writes.log"), readOnly, load, select, credentials, status, list, redactor };
137
+ }
@@ -0,0 +1,3 @@
1
+ import type { credentialStore } from "./profiles.js";
2
+ import { gateway } from "./transport.js";
3
+ export declare function readCommand(name: "email search" | "email show" | "event list" | "event show" | "task show", id: string | undefined, flags: Map<string, string>, store: ReturnType<typeof credentialStore>, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;