@knowttl/checkpoint-axi 0.0.0 → 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 -3
  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 +48 -5
  41. package/skills/checkpoint-axi/SKILL.md +102 -0
@@ -0,0 +1,97 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { createHash } from "node:crypto";
3
+ import { isAbsolute } from "node:path";
4
+ import { z } from "zod";
5
+ import { exceptionFamilies } from "./exceptions.js";
6
+ import { readPrivateFile } from "./private-file.js";
7
+ import { gateway, READ_LIMITS } from "./transport.js";
8
+ import { coordinateWrite } from "./writes.js";
9
+ import { sectoolsWriteCommand } from "./write-sectools.js";
10
+ // Request parameter tables, not the contradictory string-boolean samples: guide pp.62-66, 76-80.
11
+ const criteria = { attachmentMd5: z.string(), senderEmail: z.string(), senderName: z.string(), recipient: z.string(),
12
+ senderClientIp: z.string(), senderDomain: z.string(), senderIp: z.string(), linkDomains: z.string(), subject: z.string() };
13
+ const fields = { ...criteria, comment: z.string(), actionNeeded: z.string(), ignoringSpfCheck: z.boolean(),
14
+ subjectMatching: z.string(), linkDomainMatching: z.string(), senderNameMatching: z.string(),
15
+ senderDomainMatching: z.string(), senderEmailMatching: z.string(), recipientMatching: z.string() };
16
+ const payload = z.strictObject({ ...fields, entityId: z.string().regex(/^[A-Za-z0-9_-]+$/) }).partial();
17
+ const record = z.object({ entityId: z.string().regex(/^[A-Za-z0-9_-]+$/),
18
+ ...Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, value.nullish()])) }).passthrough();
19
+ const fail = (code, message) => { throw new AxiError(message, code, ["Run checkpoint-axi exception create --help; inspect the preview before execution"]); };
20
+ export async function exceptionWriteCommand(name, kind, id, flags, store, transport) {
21
+ const action = name.slice("exception ".length);
22
+ if (kind === "anomaly" || kind === "click-time" || kind === "malware" || kind === "url" || kind === "dlp" || action === "remove-items" || action === "clear") {
23
+ return sectoolsWriteCommand(action, kind ?? "", id, flags, store, transport);
24
+ }
25
+ if (flags.has("--specific"))
26
+ return fail("VALIDATION_ERROR", "--specific is only valid for malware, url or dlp create");
27
+ if (kind !== "phishing-allow" && kind !== "phishing-block" && kind !== "spam-allow") {
28
+ return fail("VALIDATION_ERROR", "Choose phishing-allow, phishing-block, spam-allow, anomaly or click-time for exception writes");
29
+ }
30
+ if (action !== "create" && (!id || !/^[A-Za-z0-9_-]{1,256}$/.test(id)))
31
+ return fail("VALIDATION_ERROR", "Supply one exception ID containing letters, digits, underscores or hyphens");
32
+ if (flags.has("--execute") && flags.has("--dry-run"))
33
+ return fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
34
+ let data;
35
+ if (action !== "remove") {
36
+ const path = flags.get("--input");
37
+ if (!path || !isAbsolute(path))
38
+ return fail("VALIDATION_ERROR", "--input requires an absolute private JSON file path");
39
+ let value;
40
+ try {
41
+ value = JSON.parse(readPrivateFile(path, true, true));
42
+ }
43
+ catch (error) {
44
+ if (error instanceof AxiError)
45
+ throw error;
46
+ return fail("VALIDATION_ERROR", "Cannot read a valid private JSON input file");
47
+ }
48
+ const parsed = payload.safeParse(value);
49
+ if (!parsed.success || !Object.keys(criteria).some((key) => typeof parsed.data[key] === "string"
50
+ && String(parsed.data[key]).trim())) {
51
+ return fail("VALIDATION_ERROR", "Input must contain documented requestData fields and at least one nonempty matching criterion; ignoringSpfCheck is boolean");
52
+ }
53
+ data = parsed.data;
54
+ if (action === "update" && data.entityId !== undefined && data.entityId !== id)
55
+ return fail("VALIDATION_ERROR", "Input entityId must match the exception ID");
56
+ }
57
+ // Canonical payload identity binds a new rule's confirmation without putting its contents in the journal.
58
+ const canonical = data === undefined ? "" : JSON.stringify(Object.fromEntries(Object.entries(data).sort(([a], [b]) => a.localeCompare(b))));
59
+ const target = action === "create" ? `sha256:${createHash("sha256").update(canonical).digest("hex")}` : id;
60
+ const operation = `exception.${kind}.${action}`;
61
+ store.readOnly();
62
+ const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
63
+ const checkScope = () => {
64
+ if (store.readOnly() || !profile.writes.allowWrites)
65
+ fail("WRITES_DISABLED", "Writes are disabled for the selected profile");
66
+ if (!profile.writes.operations.includes(operation))
67
+ fail("OPERATION_NOT_WRITABLE", "Operation is outside the selected profile's exact write scope");
68
+ const fresh = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
69
+ if (JSON.stringify(fresh) !== JSON.stringify(profile))
70
+ fail("CONFIG_CHANGED", "Profile changed during the write; nothing was sent");
71
+ };
72
+ checkScope();
73
+ const session = gateway(profile, store.credentials(profile, true), store.redactor, transport, true);
74
+ const path = exceptionFamilies[kind].path;
75
+ const normalize = (value) => value === "" ? undefined : value ?? undefined;
76
+ const same = (item) => Object.keys(fields).every((key) => normalize(item[key]) === normalize(data[key]))
77
+ && (data.entityId === undefined || item.entityId === data.entityId);
78
+ const readState = async () => {
79
+ const page = await session.read({ path: (action === "create" ? path : `${path}/${id}`) });
80
+ const parsed = z.array(record).safeParse(page.records);
81
+ if (!parsed.success || page.scrollId || page.total !== undefined && page.total > page.records.length
82
+ || page.records.length > (action === "create" ? READ_LIMITS.rows : 1)
83
+ || action !== "create" && parsed.data.some((item) => item.entityId !== id)
84
+ || new Set(parsed.data.map((item) => item.entityId)).size !== parsed.data.length) {
85
+ return fail("RESPONSE_INVALID", "Exception preflight returned incomplete, mismatched or malformed state");
86
+ }
87
+ if (action === "update" && !parsed.data.length)
88
+ return fail("NOT_FOUND", "Exception to update was not found");
89
+ const noop = action === "remove" ? parsed.data.length === 0 : parsed.data.some(same);
90
+ return { noop, state: { existing: action === "create" ? parsed.data.filter(same) : parsed.data, ...(data ? { requested: data } : {}) } };
91
+ };
92
+ return coordinateWrite({ operation, targets: [target], flags, store, scope: { profile: profile.name, region: profile.region }, checkScope, readState,
93
+ send: async (authorization, checkScopeBeforeSend) => {
94
+ await session.exceptionWrite(authorization, kind, action, target, checkScopeBeforeSend, data);
95
+ return { help: [`Run checkpoint-axi exception ${action === "create" ? `list ${kind}` : `show ${kind} ${id}`} to reconcile the accepted request; live behavior is uncommissioned`] };
96
+ } });
97
+ }
@@ -0,0 +1,5 @@
1
+ import type { credentialStore } from "./profiles.js";
2
+ import { gateway } from "./transport.js";
3
+ export type SectoolsAction = "create" | "update" | "remove" | "remove-items" | "clear";
4
+ export type SectoolKind = "malware" | "url" | "dlp";
5
+ export declare function sectoolsWriteCommand(action: SectoolsAction, kind: string, id: string | undefined, flags: Map<string, string>, store: ReturnType<typeof credentialStore>, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
@@ -0,0 +1,107 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { createHash } from "node:crypto";
3
+ import { isAbsolute } from "node:path";
4
+ import { z } from "zod";
5
+ import { readPrivateFile } from "./private-file.js";
6
+ import { gateway } from "./transport.js";
7
+ import { coordinateWrite } from "./writes.js";
8
+ import { exceptionFamilies } from "./exceptions.js";
9
+ const identifier = z.string().regex(/^[A-Za-z0-9_-]{1,256}$/);
10
+ const text = z.string().refine((value) => value.trim().length > 0);
11
+ // Request bodies only: guide pp.84,86,92-95,97-99 publishes no response schemas.
12
+ const anomalyInput = z.strictObject({ addedBy: z.string(), requestJson: z.strictObject({
13
+ "whitelist-option:new_country": identifier, "apply-to-past": z.enum(["Yes", "No"]),
14
+ "anomaly-comment": z.string(), event_id: identifier,
15
+ }) });
16
+ const clickInput = z.strictObject({ listItemName: text, listId: identifier, createdBy: z.string() });
17
+ const clickUpdate = clickInput.extend({ listItemId: identifier });
18
+ const condition = z.strictObject({ condition: text });
19
+ // Guide pp.103-106,113-117,123-127 supplies request examples, but no parameter tables.
20
+ const sectoolFields = { exception_type: text, exception_str: text, comment: z.string() };
21
+ const sectoolCreate = z.strictObject({ ...sectoolFields, entity_type: text, entity_id: text,
22
+ file_name: z.string(), created_by_email: z.string(), is_exclusive: z.boolean() });
23
+ const malwareCreate = sectoolCreate.extend({ exception_payload: condition });
24
+ const sectoolUpdate = z.strictObject({ ...sectoolFields, exception_payload: condition });
25
+ const fail = (code, message) => {
26
+ throw new AxiError(message, code, ["Run checkpoint-axi exception create --help; live response identities remain unverified until commissioning through Artium"]);
27
+ };
28
+ export async function sectoolsWriteCommand(action, kind, id, flags, store, transport) {
29
+ const sectool = kind === "malware" || kind === "url" || kind === "dlp";
30
+ if (sectool && (action === "remove" || action === "clear")) {
31
+ return fail("ENDPOINT_UNVERIFIED", "Malware, URL and DLP deletes duplicate create requests in the guide (pages 102-109, 113-119, 123-129); commissioning must establish the real requests");
32
+ }
33
+ if (sectool ? action !== "create" && action !== "update"
34
+ : kind !== "anomaly" && kind !== "click-time" || kind === "anomaly" && action !== "create" && action !== "remove") {
35
+ return fail("VALIDATION_ERROR", "Anomaly supports create/remove; click-time supports create/update/remove/remove-items/clear; malware/url/dlp support create/update");
36
+ }
37
+ if (flags.has("--specific") && (!sectool || action !== "create"))
38
+ return fail("VALIDATION_ERROR", "--specific is only valid for malware, url or dlp create");
39
+ if (sectool && id !== undefined)
40
+ return fail("VALIDATION_ERROR", "Malware, URL and DLP updates identify the exception in --input; omit the positional ID");
41
+ if (flags.has("--execute") && flags.has("--dry-run"))
42
+ return fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
43
+ if (action === "clear")
44
+ return fail("ENDPOINT_UNVERIFIED", "Click-time clear requires a trustworthy pre-read count; the response schema is undocumented (guide pages 87-88, 96-97)");
45
+ if (action !== "remove-items" && flags.has("--ids"))
46
+ return fail("VALIDATION_ERROR", "--ids is only valid for click-time remove-items");
47
+ if (!sectool && (action === "update" || action === "remove") && !identifier.safeParse(id).success)
48
+ return fail("VALIDATION_ERROR", "Supply one exception ID containing letters, digits, underscores or hyphens");
49
+ let data;
50
+ if (action === "create" || action === "update") {
51
+ const path = flags.get("--input");
52
+ if (!path || !isAbsolute(path))
53
+ return fail("VALIDATION_ERROR", "--input requires an absolute private JSON file path");
54
+ let value;
55
+ try {
56
+ value = JSON.parse(readPrivateFile(path, true, true));
57
+ }
58
+ catch (error) {
59
+ if (error instanceof AxiError)
60
+ throw error;
61
+ return fail("VALIDATION_ERROR", "Cannot read a valid private JSON input file");
62
+ }
63
+ const parsed = (sectool ? action === "update" ? sectoolUpdate : kind === "malware" ? malwareCreate : sectoolCreate
64
+ : kind === "anomaly" ? anomalyInput : action === "update" ? clickUpdate : clickInput).safeParse(value);
65
+ if (!parsed.success)
66
+ return fail("VALIDATION_ERROR", "Input must contain only the documented requestData fields for this operation");
67
+ data = parsed.data;
68
+ if (sectool && !exceptionFamilies[kind].types.includes(String(data.exception_type)))
69
+ return fail("VALIDATION_ERROR", "Input exception_type must be a documented type for the selected kind");
70
+ if (!sectool && action === "update" && data.listItemId !== id)
71
+ return fail("VALIDATION_ERROR", "Input listItemId must match the positional item ID");
72
+ }
73
+ const targets = action === "remove-items" ? flags.get("--ids")?.split(",") ?? [] : action === "create" || sectool
74
+ ? [`sha256:${createHash("sha256").update(JSON.stringify(sectool ? { specific: flags.has("--specific"), data } : data)).digest("hex")}`] : [id];
75
+ if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length
76
+ || action === "remove-items" && targets.some((target) => !identifier.safeParse(target).success)) {
77
+ return fail("VALIDATION_ERROR", "Supply --ids with 1-20 unique comma-separated item IDs");
78
+ }
79
+ if (action === "remove-items")
80
+ data = { listItemIds: targets };
81
+ if (kind === "anomaly" && action === "remove")
82
+ data = { ruleId: id };
83
+ const operation = `exception.${kind}.${action}`;
84
+ store.readOnly();
85
+ const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
86
+ const checkScope = () => {
87
+ if (store.readOnly() || !profile.writes.allowWrites)
88
+ fail("WRITES_DISABLED", "Writes are disabled for the selected profile");
89
+ if (!profile.writes.operations.includes(operation))
90
+ fail("OPERATION_NOT_WRITABLE", "Operation is outside the selected profile's exact write scope");
91
+ const fresh = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
92
+ if (JSON.stringify(fresh) !== JSON.stringify(profile))
93
+ fail("CONFIG_CHANGED", "Profile changed during the write; nothing was sent");
94
+ };
95
+ checkScope();
96
+ const session = gateway(profile, store.credentials(profile, true), store.redactor, transport, true);
97
+ // No existence/no-op inference is safe without documented response identities.
98
+ // Both coordinator checks retain explicit uncertainty instead of inventing a list preflight.
99
+ return coordinateWrite({ operation, targets, flags, store, scope: { profile: profile.name, region: profile.region }, checkScope,
100
+ readState: async () => ({ noop: false, state: { targetState: "unverified", responseSchema: "unverified",
101
+ reason: "Existence and no-op detection require live response identity commissioning", ...(data ? { requested: data } : {}) } }),
102
+ send: async (authorization, checkScopeBeforeSend) => ({
103
+ response: await session.sectoolsWrite(authorization, kind, action, targets, checkScopeBeforeSend, data, flags.has("--specific")),
104
+ responseSchema: "unverified",
105
+ help: [`Run checkpoint-axi exception list ${kind}${sectool ? ` --type ${String(data.exception_type)}` : kind === "click-time" ? " --items" : ""} to inspect unverified JSON and reconcile; never replay automatically`],
106
+ }) });
107
+ }
@@ -0,0 +1,30 @@
1
+ import { credentialStore } from "./profiles.js";
2
+ import { gateway } from "./transport.js";
3
+ type Store = ReturnType<typeof credentialStore>;
4
+ type Flags = Map<string, string>;
5
+ export type WriteAuthorization = Readonly<{
6
+ operation: string;
7
+ targets: readonly string[];
8
+ profile: string;
9
+ region: string;
10
+ }>;
11
+ export declare function consumeWriteAuthorization(authorization: WriteAuthorization, operation: string, targets: readonly string[], profile: string, region: string): boolean;
12
+ export declare function coordinateWrite(args: {
13
+ operation: string;
14
+ targets: readonly string[];
15
+ flags: Flags;
16
+ store: Store;
17
+ scope: {
18
+ profile: string;
19
+ region: string;
20
+ };
21
+ checkScope: () => void;
22
+ readState: () => Promise<{
23
+ noop: boolean;
24
+ state: unknown;
25
+ }>;
26
+ send: (authorization: WriteAuthorization, checkScopeBeforeSend: () => void) => Promise<Record<string, unknown>>;
27
+ }): Promise<Record<string, unknown>>;
28
+ export declare function eventWriteCommand(flags: Flags, store: Store, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
29
+ export declare function emailWriteCommand(name: "email quarantine" | "email restore", flags: Flags, store: Store, transport?: Parameters<typeof gateway>[3]): Promise<Record<string, unknown>>;
30
+ export {};
@@ -0,0 +1,295 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+ import { z } from "zod";
6
+ import { gateway } from "./transport.js";
7
+ import { protectWindowsFile, windowsAcl } from "./private-file.js";
8
+ const fail = (code, message, help = []) => { throw new AxiError(message, code, help); };
9
+ const authorizations = new WeakSet();
10
+ export function consumeWriteAuthorization(authorization, operation, targets, profile, region) {
11
+ if (!authorizations.delete(authorization))
12
+ return false;
13
+ return authorization.operation === operation && authorization.profile === profile && authorization.region === region
14
+ && targets.length > 0 && targets.length <= 20 && new Set(targets).size === targets.length
15
+ && targets.every((target) => authorization.targets.includes(target));
16
+ }
17
+ // Metadata only, never message payloads, headers, credentials or API bodies.
18
+ function journal(store) {
19
+ const path = store.writeLogPath();
20
+ function append(record) {
21
+ let lock;
22
+ let fd;
23
+ try {
24
+ const directory = dirname(path);
25
+ const created = mkdirSync(directory, { recursive: true, mode: 0o700 });
26
+ if (process.platform === "win32" && created !== undefined)
27
+ protectWindowsFile(directory);
28
+ const parent = lstatSync(directory);
29
+ if (!parent.isDirectory() || parent.isSymbolicLink() || process.platform !== "win32"
30
+ && (parent.uid !== process.getuid() || (parent.mode & 0o077) !== 0))
31
+ throw new Error("Insecure journal directory");
32
+ if (process.platform === "win32")
33
+ windowsAcl(directory, true);
34
+ lock = openSync(`${path}.lock`, "wx", 0o600);
35
+ if (process.platform === "win32")
36
+ protectWindowsFile(`${path}.lock`);
37
+ try {
38
+ fd = openSync(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_EXCL, 0o600);
39
+ if (process.platform === "win32")
40
+ protectWindowsFile(path);
41
+ }
42
+ catch (error) {
43
+ if (error.code !== "EEXIST")
44
+ throw error;
45
+ const existing = lstatSync(path);
46
+ if (!existing.isFile() || existing.isSymbolicLink() || existing.nlink !== 1 || process.platform !== "win32"
47
+ && (existing.uid !== process.getuid() || (existing.mode & 0o077) !== 0))
48
+ throw new Error("Insecure journal file");
49
+ if (process.platform === "win32")
50
+ windowsAcl(path, true);
51
+ fd = openSync(path, constants.O_RDWR | constants.O_APPEND | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW));
52
+ }
53
+ const stat = fstatSync(fd);
54
+ if (!stat.isFile() || stat.nlink !== 1 || process.platform !== "win32"
55
+ && (stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0))
56
+ throw new Error("Insecure journal file");
57
+ if (process.platform === "win32")
58
+ windowsAcl(path, true);
59
+ const contents = readFileSync(fd, "utf8");
60
+ if (contents && !contents.endsWith("\n"))
61
+ throw new Error("Incomplete journal");
62
+ for (const line of contents.split("\n").filter(Boolean)) {
63
+ const entry = JSON.parse(line);
64
+ if (!entry || !["intent", "outcome"].includes(String(entry.kind)) || typeof entry.auditId !== "string")
65
+ throw new Error("Invalid journal");
66
+ if (record.kind === "intent" && entry.kind === "intent" && entry.auditId === record.auditId)
67
+ throw new Error("Duplicate intent");
68
+ }
69
+ const line = Buffer.from(JSON.stringify(store.redactor.value({ ...record, time: new Date().toISOString() })) + "\n");
70
+ let offset = 0;
71
+ while (offset < line.length) {
72
+ const written = writeSync(fd, line, offset, line.length - offset);
73
+ if (!written)
74
+ throw new Error("Incomplete journal write");
75
+ offset += written;
76
+ }
77
+ fsyncSync(fd);
78
+ // The directory entry must survive a crash too, before sending anything.
79
+ if (process.platform !== "win32") {
80
+ const stop = created === undefined ? directory : dirname(created);
81
+ let current = directory;
82
+ for (;;) {
83
+ const directoryFd = openSync(current, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
84
+ try {
85
+ fsyncSync(directoryFd);
86
+ }
87
+ finally {
88
+ closeSync(directoryFd);
89
+ }
90
+ if (current === stop)
91
+ break;
92
+ current = dirname(current);
93
+ }
94
+ }
95
+ }
96
+ catch {
97
+ fail(record.kind === "intent" ? "INTENT_NOT_RECORDED" : "OUTCOME_NOT_RECORDED", record.kind === "intent" ? "Durable write intent could not be recorded; nothing was sent" : "Write outcome could not be recorded; reconcile before any further action", [`Audit ID: ${String(record.auditId)}`, "Check CHECKPOINT_AXI_WRITE_LOG and its private directory permissions; never replay an ambiguous action"]);
98
+ }
99
+ finally {
100
+ if (fd !== undefined)
101
+ closeSync(fd);
102
+ if (lock !== undefined) {
103
+ closeSync(lock);
104
+ unlinkSync(`${path}.lock`);
105
+ }
106
+ }
107
+ }
108
+ return append;
109
+ }
110
+ // Domain callers bind the operation and single send; flags cannot supply routes.
111
+ export async function coordinateWrite(args) {
112
+ const { operation, targets, flags, store, checkScope, readState, send } = args;
113
+ if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length)
114
+ fail("VALIDATION_ERROR", "Supply 1-20 unique targets");
115
+ if (flags.has("--execute") && flags.has("--dry-run"))
116
+ fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
117
+ checkScope();
118
+ const current = await readState();
119
+ const confirmation = `${operation} ${targets.join(",")}`;
120
+ const preview = { operation, effect: "disruptive", targets, noop: current.noop, currentState: current.state, confirm: confirmation };
121
+ if (!flags.has("--execute"))
122
+ return { outcome: "preview", preview };
123
+ if (current.noop)
124
+ return { outcome: "noop", preview };
125
+ if (!flags.has("--confirm"))
126
+ fail("CONFIRM_REQUIRED", "Exact target confirmation is required", [`Repeat with --confirm '${confirmation}'`]);
127
+ if (flags.get("--confirm") !== confirmation)
128
+ fail("CONFIRM_MISMATCH", "Confirmation does not exactly match the operation and every target", [`Repeat with --confirm '${confirmation}'`]);
129
+ checkScope();
130
+ const auditId = randomUUID();
131
+ const record = journal(store);
132
+ const meta = { auditId, ...args.scope, operation, targets, effect: "disruptive" };
133
+ record({ ...meta, kind: "intent" });
134
+ let fresh;
135
+ try {
136
+ fresh = await readState();
137
+ checkScope();
138
+ }
139
+ catch (error) {
140
+ record({ ...meta, kind: "outcome", outcome: "NOT_SENT" });
141
+ throw error;
142
+ }
143
+ if (fresh.noop) {
144
+ record({ ...meta, kind: "outcome", outcome: "NOT_SENT" });
145
+ return { outcome: "noop", preview, auditId };
146
+ }
147
+ const authorization = Object.freeze({ operation, targets: Object.freeze([...targets]), ...args.scope });
148
+ authorizations.add(authorization);
149
+ let result;
150
+ let scopeRevoked = false;
151
+ try {
152
+ result = await send(authorization, () => {
153
+ try {
154
+ checkScope();
155
+ }
156
+ catch (error) {
157
+ scopeRevoked = true;
158
+ throw error;
159
+ }
160
+ });
161
+ }
162
+ catch (error) {
163
+ if (scopeRevoked) {
164
+ record({ ...meta, kind: "outcome", outcome: "NOT_SENT" });
165
+ throw error;
166
+ }
167
+ record({ ...meta, kind: "outcome", outcome: "OUTCOME_UNKNOWN" });
168
+ return fail("OUTCOME_UNKNOWN", "Action outcome is unknown; never replay automatically", [
169
+ `Audit ID: ${auditId}`, "Inspect the write journal and read back every target before deciding what to do next",
170
+ ]);
171
+ }
172
+ finally {
173
+ authorizations.delete(authorization);
174
+ }
175
+ record({ ...meta, kind: "outcome", outcome: "accepted" });
176
+ return { outcome: "accepted", auditId, ...result };
177
+ }
178
+ const entity = z.object({ entityInfo: z.object({ entityId: z.string(), saasEntityType: z.string() }),
179
+ entityPayload: z.object({ isQuarantined: z.boolean().optional(), isRestored: z.boolean().optional() }),
180
+ entityAvailableActions: z.array(z.object({ entityActionName: z.string() })) });
181
+ const task = z.object({ entityId: z.string(), taskId: z.union([z.string().regex(/^[A-Za-z0-9_-]+$/), z.number().int().nonnegative()]) });
182
+ const event = z.object({ eventId: z.string(), entityId: z.string(), state: z.string(),
183
+ availableEventActions: z.array(z.object({ actionName: z.string(), actionParameter: z.record(z.string(), z.unknown()) })) });
184
+ const eventTask = task.extend({ eventId: z.string() });
185
+ export async function eventWriteCommand(flags, store, transport) {
186
+ const targets = flags.get("--ids")?.split(",") ?? [];
187
+ const action = flags.get("--name");
188
+ if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length
189
+ || targets.some((id) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,255}$/.test(id))
190
+ || !action || !/^[a-z][A-Za-z0-9_-]{0,127}$/.test(action)) {
191
+ return fail("VALIDATION_ERROR", "Supply --name <action> and --ids with 1-20 unique comma-separated event IDs");
192
+ }
193
+ if (flags.has("--execute") && flags.has("--dry-run"))
194
+ return fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
195
+ store.readOnly();
196
+ const profile = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
197
+ const operation = `event.action.${action}`;
198
+ const checkScope = () => {
199
+ if (store.readOnly() || !profile.writes.allowWrites)
200
+ fail("WRITES_DISABLED", "Writes are disabled for the selected profile");
201
+ if (!profile.writes.operations.includes(operation))
202
+ fail("OPERATION_NOT_WRITABLE", "Operation is outside the selected profile's exact write scope");
203
+ const fresh = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
204
+ if (JSON.stringify(fresh) !== JSON.stringify(profile))
205
+ fail("CONFIG_CHANGED", "Profile changed during the write; nothing was sent");
206
+ };
207
+ checkScope();
208
+ const session = gateway(profile, store.credentials(profile, true), store.redactor, transport, true);
209
+ const entities = new Map();
210
+ const readState = async () => {
211
+ const states = [];
212
+ for (const id of targets) {
213
+ const page = await session.read({ path: `/event/${encodeURIComponent(id)}` });
214
+ const parsed = event.safeParse(page.records[0]);
215
+ if (page.records.length !== 1 || !parsed.success || parsed.data.eventId !== id) {
216
+ return fail("RESPONSE_INVALID", "Event preflight returned a missing, mismatched or malformed event");
217
+ }
218
+ const value = parsed.data;
219
+ const offered = value.availableEventActions.filter((item) => item.actionName === action);
220
+ if (!offered.length)
221
+ fail("ACTION_UNAVAILABLE", "Requested action is unavailable for an event target");
222
+ // Guide pp.36-37 uses objects, but p.43 requires strings; never guess serialization.
223
+ if (offered.length !== 1 || Object.keys(offered[0].actionParameter).length) {
224
+ fail("ENDPOINT_UNVERIFIED", "Parameterized event actions require live commissioning through Artium", ["Run checkpoint-axi event show <id> to inspect available actions; parameter serialization is unverified (guide pages 36-37, 43)"]);
225
+ }
226
+ entities.set(id, value.entityId);
227
+ states.push({ id, state: value.state, action });
228
+ }
229
+ return { noop: false, state: states };
230
+ };
231
+ return coordinateWrite({ operation, targets, flags, store, scope: { profile: profile.name, region: profile.region }, checkScope, readState, send: async (authorization) => {
232
+ const page = await session.eventAction(authorization, action, targets);
233
+ const parsed = z.array(eventTask).safeParse(page.records);
234
+ if (!parsed.success || parsed.data.length !== targets.length || new Set(parsed.data.map((item) => item.eventId)).size !== targets.length
235
+ || parsed.data.some((item) => !targets.includes(item.eventId) || entities.get(item.eventId) !== item.entityId
236
+ || store.redactor.text(String(item.taskId)) !== String(item.taskId))) {
237
+ return fail("OUTCOME_UNKNOWN", "Action response did not identify a task for every event target");
238
+ }
239
+ return { tasks: parsed.data.map((item) => ({ ...item, outcome: "accepted" })), help: ["Run checkpoint-axi task show <taskId> to inspect asynchronous execution; accepted does not mean completed"] };
240
+ } });
241
+ }
242
+ export async function emailWriteCommand(name, flags, store, transport) {
243
+ const targets = flags.get("--ids")?.split(",") ?? [];
244
+ if (!targets.length || targets.length > 20 || new Set(targets).size !== targets.length
245
+ || targets.some((id) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,255}$/.test(id))) {
246
+ return fail("VALIDATION_ERROR", "--ids must name 1-20 unique comma-separated entity IDs");
247
+ }
248
+ if (flags.has("--execute") && flags.has("--dry-run"))
249
+ return fail("VALIDATION_ERROR", "--execute cannot combine with --dry-run");
250
+ store.readOnly();
251
+ const loaded = store.load(flags.get("--config"), true);
252
+ const profile = store.select(loaded.config, flags.get("--profile"));
253
+ const operation = name.replace(" ", ".");
254
+ const checkScope = () => {
255
+ if (store.readOnly() || !profile.writes.allowWrites)
256
+ fail("WRITES_DISABLED", "Writes are disabled for the selected profile");
257
+ if (!profile.writes.operations.includes(operation))
258
+ fail("OPERATION_NOT_WRITABLE", "Operation is outside the selected profile's exact write scope");
259
+ const fresh = store.select(store.load(flags.get("--config"), true).config, flags.get("--profile"));
260
+ if (JSON.stringify(fresh) !== JSON.stringify(profile))
261
+ fail("CONFIG_CHANGED", "Profile changed during the write; nothing was sent");
262
+ };
263
+ checkScope();
264
+ const session = gateway(profile, store.credentials(profile, true), store.redactor, transport, true);
265
+ const action = name === "email quarantine" ? "quarantine" : "restore";
266
+ let pending = targets;
267
+ const readState = async () => {
268
+ const states = [];
269
+ for (const id of targets) {
270
+ const page = await session.read({ path: `/search/entity/${encodeURIComponent(id)}` });
271
+ const parsed = entity.safeParse(page.records[0]);
272
+ if (page.records.length !== 1 || !parsed.success || parsed.data.entityInfo.entityId !== id
273
+ || !["office365_emails_email", "google_mail_email"].includes(parsed.data.entityInfo.saasEntityType)) {
274
+ return fail("RESPONSE_INVALID", "Email preflight returned a missing, mismatched or malformed entity");
275
+ }
276
+ const value = parsed.data;
277
+ const noop = action === "quarantine" ? value.entityPayload.isQuarantined === true
278
+ : value.entityPayload.isRestored === true && value.entityPayload.isQuarantined === false;
279
+ if (!noop && !value.entityAvailableActions.some((item) => item.entityActionName === action))
280
+ fail("ACTION_UNAVAILABLE", "Requested action is unavailable for an email target");
281
+ states.push({ id, noop, isQuarantined: value.entityPayload.isQuarantined, isRestored: value.entityPayload.isRestored });
282
+ }
283
+ pending = states.filter((state) => !state.noop).map((state) => state.id);
284
+ return { noop: pending.length === 0, state: states };
285
+ };
286
+ return coordinateWrite({ operation, targets, flags, store, scope: { profile: profile.name, region: profile.region }, checkScope, readState, send: async (authorization) => {
287
+ const page = await session.emailAction(authorization, action, pending);
288
+ const parsed = z.array(task).safeParse(page.records);
289
+ if (!parsed.success || parsed.data.length !== pending.length || new Set(parsed.data.map((item) => item.entityId)).size !== pending.length
290
+ || parsed.data.some((item) => !pending.includes(item.entityId) || store.redactor.text(String(item.taskId)) !== String(item.taskId))) {
291
+ return fail("OUTCOME_UNKNOWN", "Action response did not identify a task for every target");
292
+ }
293
+ return { tasks: parsed.data.map((item) => ({ ...item, outcome: "accepted" })), help: ["Run checkpoint-axi task show <taskId> to inspect asynchronous execution; accepted does not mean completed"] };
294
+ } });
295
+ }