fullstackgtm 0.48.0 → 0.50.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 (98) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/CONTRIBUTING.md +23 -4
  3. package/DATA-FLOWS.md +8 -2
  4. package/INSTALL_FOR_AGENTS.md +13 -4
  5. package/README.md +53 -1
  6. package/SECURITY.md +27 -3
  7. package/dist/acquireCheckpoint.d.ts +33 -0
  8. package/dist/acquireCheckpoint.js +149 -0
  9. package/dist/acquireLinkedIn.d.ts +2 -0
  10. package/dist/acquireLinkedIn.js +1 -1
  11. package/dist/cli/audit.js +10 -7
  12. package/dist/cli/auth.js +8 -4
  13. package/dist/cli/enrich.js +283 -79
  14. package/dist/cli/fix.js +20 -7
  15. package/dist/cli/help.js +23 -15
  16. package/dist/cli/market.js +180 -2
  17. package/dist/cli/plans.js +154 -9
  18. package/dist/cli/shared.d.ts +3 -1
  19. package/dist/cli/shared.js +2 -2
  20. package/dist/cli/ui.d.ts +10 -5
  21. package/dist/cli/ui.js +18 -5
  22. package/dist/cli.js +1 -1
  23. package/dist/config.d.ts +13 -1
  24. package/dist/config.js +23 -2
  25. package/dist/connectors/linkedin.d.ts +2 -0
  26. package/dist/connectors/linkedin.js +5 -0
  27. package/dist/connectors/prospectSources.d.ts +23 -0
  28. package/dist/connectors/prospectSources.js +25 -14
  29. package/dist/connectors/salesforce.js +12 -5
  30. package/dist/connectors/salesforceAuth.d.ts +9 -0
  31. package/dist/connectors/salesforceAuth.js +47 -5
  32. package/dist/connectors/theirstack.d.ts +0 -19
  33. package/dist/connectors/theirstack.js +3 -10
  34. package/dist/credentials.js +36 -26
  35. package/dist/enrich.d.ts +44 -1
  36. package/dist/hostedAcquireCheckpoint.d.ts +43 -0
  37. package/dist/hostedAcquireCheckpoint.js +129 -0
  38. package/dist/hostedPatchPlan.d.ts +87 -0
  39. package/dist/hostedPatchPlan.js +270 -0
  40. package/dist/icp.js +13 -2
  41. package/dist/index.d.ts +7 -3
  42. package/dist/index.js +6 -2
  43. package/dist/market.d.ts +1 -1
  44. package/dist/market.js +7 -127
  45. package/dist/marketClassify.d.ts +10 -0
  46. package/dist/marketClassify.js +52 -1
  47. package/dist/marketSourcing.js +7 -45
  48. package/dist/mcp.js +67 -115
  49. package/dist/planStore.d.ts +42 -1
  50. package/dist/planStore.js +268 -49
  51. package/dist/progress.d.ts +2 -2
  52. package/dist/progress.js +2 -2
  53. package/dist/providerError.d.ts +21 -0
  54. package/dist/providerError.js +30 -0
  55. package/dist/publicHttp.d.ts +28 -0
  56. package/dist/publicHttp.js +143 -0
  57. package/dist/secureFile.d.ts +15 -0
  58. package/dist/secureFile.js +164 -0
  59. package/dist/types.d.ts +1 -1
  60. package/docs/api.md +53 -2
  61. package/docs/architecture.md +22 -2
  62. package/llms.txt +7 -0
  63. package/package.json +5 -3
  64. package/skills/fullstackgtm/SKILL.md +8 -2
  65. package/src/acquireCheckpoint.ts +186 -0
  66. package/src/acquireLinkedIn.ts +3 -1
  67. package/src/cli/audit.ts +10 -7
  68. package/src/cli/auth.ts +8 -4
  69. package/src/cli/enrich.ts +322 -78
  70. package/src/cli/fix.ts +28 -7
  71. package/src/cli/help.ts +24 -15
  72. package/src/cli/market.ts +181 -2
  73. package/src/cli/plans.ts +159 -9
  74. package/src/cli/shared.ts +2 -1
  75. package/src/cli/ui.ts +19 -9
  76. package/src/cli.ts +1 -1
  77. package/src/config.ts +39 -1
  78. package/src/connectors/linkedin.ts +6 -0
  79. package/src/connectors/prospectSources.ts +50 -15
  80. package/src/connectors/salesforce.ts +12 -5
  81. package/src/connectors/salesforceAuth.ts +46 -6
  82. package/src/connectors/theirstack.ts +3 -10
  83. package/src/credentials.ts +47 -28
  84. package/src/enrich.ts +47 -1
  85. package/src/hostedAcquireCheckpoint.ts +156 -0
  86. package/src/hostedPatchPlan.ts +286 -0
  87. package/src/icp.ts +14 -2
  88. package/src/index.ts +24 -0
  89. package/src/market.ts +7 -111
  90. package/src/marketClassify.ts +61 -1
  91. package/src/marketSourcing.ts +7 -43
  92. package/src/mcp.ts +93 -136
  93. package/src/planStore.ts +322 -57
  94. package/src/progress.ts +2 -2
  95. package/src/providerError.ts +37 -0
  96. package/src/publicHttp.ts +147 -0
  97. package/src/secureFile.ts +169 -0
  98. package/src/types.ts +1 -0
@@ -1,10 +1,11 @@
1
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync, } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { refreshHubspotToken } from "./connectors/hubspotAuth.js";
6
- import { refreshSalesforceToken } from "./connectors/salesforceAuth.js";
6
+ import { refreshSalesforceToken, validateSalesforceOrigin } from "./connectors/salesforceAuth.js";
7
7
  import { detectKeychainBackend } from "./keychain.js";
8
+ import { assertNoSymlinkComponents, readSecureRegularFile, UnsafeManagedPathError, writeSecureFileAtomic, } from "./secureFile.js";
8
9
  /**
9
10
  * Local CLI credential store: ~/.fullstackgtm/credentials.json (0600), or
10
11
  * $FSGTM_HOME/credentials.json when set. Environment tokens always win over
@@ -80,7 +81,9 @@ export function ensureSecureHomeDir() {
80
81
  // umask) only to directories it creates, and never to pre-existing ones.
81
82
  const levels = dir === baseHomeDir() ? [dir] : [baseHomeDir(), join(baseHomeDir(), "profiles"), dir];
82
83
  for (const level of levels) {
84
+ assertNoSymlinkComponents(level);
83
85
  mkdirSync(level, { recursive: true, mode: 0o700 });
86
+ assertNoSymlinkComponents(level);
84
87
  try {
85
88
  chmodSync(level, 0o700);
86
89
  }
@@ -92,13 +95,7 @@ export function ensureSecureHomeDir() {
92
95
  }
93
96
  /** Write a 0600 file under the home, enforcing the mode even on rewrite. */
94
97
  export function writeSecureFile(path, contents) {
95
- writeFileSync(path, contents, { mode: 0o600 });
96
- try {
97
- chmodSync(path, 0o600);
98
- }
99
- catch {
100
- // Non-POSIX filesystems ignore chmod.
101
- }
98
+ writeSecureFileAtomic(path, contents);
102
99
  }
103
100
  /**
104
101
  * The 0600/0700 guarantee was write-only: a credentials.json inherited at
@@ -107,18 +104,12 @@ export function writeSecureFile(path, contents) {
107
104
  * mode on read too — re-tighten to 0600 and warn once — so a world-readable
108
105
  * credential store can't sit there silently leaking the token to other users.
109
106
  */
110
- function enforceCredentialFileMode(path) {
111
- try {
112
- const mode = statSync(path).mode & 0o777;
113
- if ((mode & 0o077) !== 0) {
114
- chmodSync(path, 0o600);
115
- console.error(`fullstackgtm: tightened ${path} from ${mode.toString(8).padStart(3, "0")} to 600 ` +
116
- "(it was readable or writable by other users).");
117
- }
118
- }
119
- catch {
120
- // Missing file or non-POSIX filesystem: nothing to enforce.
121
- }
107
+ function readCredentialFile(path) {
108
+ return readSecureRegularFile(path, {
109
+ tightenMode: 0o600,
110
+ onModeTightened: (mode) => console.error(`fullstackgtm: tightened ${path} from ${mode.toString(8).padStart(3, "0")} to 600 ` +
111
+ "(it was readable or writable by other users)."),
112
+ });
122
113
  }
123
114
  /**
124
115
  * Persistence backend for the credential blob: the OS keychain when
@@ -144,7 +135,7 @@ function migratePlaintextToKeychain(keychain) {
144
135
  if (!existsSync(credentialsPath()))
145
136
  return;
146
137
  try {
147
- const fileParsed = JSON.parse(readFileSync(credentialsPath(), "utf8"));
138
+ const fileParsed = JSON.parse(readCredentialFile(credentialsPath()));
148
139
  if (fileParsed && fileParsed.version === 1 && fileParsed.providers) {
149
140
  const current = keychain.backend.get(keychain.account);
150
141
  const existing = current ? (JSON.parse(current).providers ?? {}) : {};
@@ -164,7 +155,7 @@ function readFile() {
164
155
  if (keychain)
165
156
  migratePlaintextToKeychain(keychain);
166
157
  try {
167
- const raw = keychain ? keychain.backend.get(keychain.account) : (enforceCredentialFileMode(credentialsPath()), readFileSync(credentialsPath(), "utf8"));
158
+ const raw = keychain ? keychain.backend.get(keychain.account) : readCredentialFile(credentialsPath());
168
159
  if (raw) {
169
160
  const parsed = JSON.parse(raw);
170
161
  if (parsed && typeof parsed === "object" && parsed.version === 1 && parsed.providers) {
@@ -172,7 +163,9 @@ function readFile() {
172
163
  }
173
164
  }
174
165
  }
175
- catch {
166
+ catch (error) {
167
+ if (error instanceof UnsafeManagedPathError)
168
+ throw error;
176
169
  // Missing or unreadable store falls through to an empty one.
177
170
  }
178
171
  return { version: 1, providers: {} };
@@ -192,6 +185,18 @@ export function getCredential(provider) {
192
185
  return readFile().providers[provider] ?? null;
193
186
  }
194
187
  export function storeCredential(provider, credential) {
188
+ if (provider === "salesforce") {
189
+ if (!credential.instanceUrl) {
190
+ throw new Error("Salesforce credentials require an instance URL.");
191
+ }
192
+ credential = {
193
+ ...credential,
194
+ instanceUrl: validateSalesforceOrigin(credential.instanceUrl, "Salesforce credential instance URL"),
195
+ ...(credential.loginUrl
196
+ ? { loginUrl: validateSalesforceOrigin(credential.loginUrl, "Salesforce credential login URL") }
197
+ : {}),
198
+ };
199
+ }
195
200
  const file = readFile();
196
201
  file.providers[provider] = credential;
197
202
  persist(file);
@@ -248,11 +253,15 @@ export async function resolveSalesforceConnection(options = {}) {
248
253
  if (!credential.instanceUrl) {
249
254
  throw new Error("Stored Salesforce credential has no instance URL. Run `fullstackgtm login salesforce` again.");
250
255
  }
256
+ const instanceUrl = validateSalesforceOrigin(credential.instanceUrl, "Stored Salesforce credential instance URL");
257
+ if (credential.loginUrl) {
258
+ validateSalesforceOrigin(credential.loginUrl, "Stored Salesforce credential login URL");
259
+ }
251
260
  const needsRefresh = credential.kind === "oauth" &&
252
261
  credential.expiresAt !== undefined &&
253
262
  Date.now() > credential.expiresAt - REFRESH_SKEW_MS;
254
263
  if (!needsRefresh) {
255
- return { accessToken: credential.accessToken, instanceUrl: credential.instanceUrl };
264
+ return { accessToken: credential.accessToken, instanceUrl };
256
265
  }
257
266
  if (!credential.refreshToken || !credential.clientId) {
258
267
  throw new Error("Stored Salesforce token is expired and cannot be refreshed. Run `fullstackgtm login salesforce` again.");
@@ -279,9 +288,10 @@ export async function resolveSalesforceConnection(options = {}) {
279
288
  if (!minted.instanceUrl) {
280
289
  throw new Error("The hosted deployment returned no Salesforce instance URL.");
281
290
  }
291
+ const instanceUrl = validateSalesforceOrigin(minted.instanceUrl, "Hosted Salesforce credential instance URL");
282
292
  return {
283
293
  accessToken: minted.accessToken,
284
- instanceUrl: minted.instanceUrl,
294
+ instanceUrl,
285
295
  fieldMappings: minted.fieldMappings,
286
296
  };
287
297
  }
package/dist/enrich.d.ts CHANGED
@@ -86,6 +86,8 @@ export type AcquireDiscoveryConfig = {
86
86
  provider: "explorium" | "pipe0" | "linkedin" | "heyreach";
87
87
  filters?: Record<string, unknown>;
88
88
  size?: number;
89
+ /** Maximum raw provider candidates scanned per run while seeking fresh leads. */
90
+ scanLimit?: number;
89
91
  resolveEmailsWith?: "pipe0";
90
92
  /** LinkedIn sources only: the provider-native list id to read (HeyReach lead-list id). */
91
93
  listId?: string;
@@ -286,7 +288,46 @@ export declare function selectStaleWork(config: EnrichConfig, runs: EnrichRun[],
286
288
  now?: () => Date;
287
289
  staleDaysOverride?: number;
288
290
  }): EnrichWorkItem[];
289
- export type EnrichRunMode = EnrichMode | "ingest";
291
+ export type EnrichRunMode = EnrichMode | "ingest" | "acquire";
292
+ /**
293
+ * Provider continuation state for an acquisition query. The fingerprint binds
294
+ * a cursor/offset to the ICP + provider filters that produced it, preventing a
295
+ * changed query from accidentally resuming in the middle of the old audience.
296
+ */
297
+ export type AcquireDiscoveryCheckpoint = {
298
+ queryFingerprint: string;
299
+ /** Opaque provider cursor (Pipe0 and other cursor-based sources). */
300
+ cursor?: string | null;
301
+ /** Zero-based provider offset (LinkedIn/HeyReach and other offset sources). */
302
+ offset?: number | null;
303
+ /** True only when the provider has positively reported no next page. */
304
+ exhausted: boolean;
305
+ };
306
+ /**
307
+ * Truthful acquisition funnel for one run. Each field counts candidates, not
308
+ * provider requests, making duplicate saturation and resolution loss visible
309
+ * instead of reporting every run as zero.
310
+ */
311
+ export type AcquireRunFunnel = {
312
+ /** Raw candidates returned across all discovery pages scanned this run. */
313
+ discovered: number;
314
+ /** Candidates that met the configured ICP threshold. */
315
+ qualified: number;
316
+ /** Qualified candidates removed by the CRM pre-email dedupe. */
317
+ skippedCrm: number;
318
+ /** Qualified candidates removed by the cross-run seen ledger. */
319
+ skippedSeen: number;
320
+ /** Fresh candidates carrying the configured resolve-first key. */
321
+ resolved: number;
322
+ /** Governed create operations emitted into the saved/dry-run plan. */
323
+ proposed: number;
324
+ /** Otherwise-proposable candidates held back by the acquire meter. */
325
+ withheldByMeter: number;
326
+ };
327
+ export type AcquireRunTelemetry = {
328
+ funnel: AcquireRunFunnel;
329
+ discovery: AcquireDiscoveryCheckpoint;
330
+ };
290
331
  export type EnrichRun = {
291
332
  id: string;
292
333
  runLabel: string;
@@ -298,6 +339,8 @@ export type EnrichRun = {
298
339
  /** Resume point for an interrupted pull (last processed pull key). */
299
340
  cursor: string | null;
300
341
  counts: EnrichCounts;
342
+ /** Acquisition-only funnel + provider continuation state (absent on legacy runs). */
343
+ acquireTelemetry?: AcquireRunTelemetry;
301
344
  planIds: string[];
302
345
  stamps: EnrichStamp[];
303
346
  /** Staged source rows (ingest mode only), consumed by append/refresh. */
@@ -0,0 +1,43 @@
1
+ import type { AcquireDiscoveryCheckpoint } from "./enrich.ts";
2
+ export type HostedAcquireCheckpoint = {
3
+ checkpoint: AcquireDiscoveryCheckpoint;
4
+ /** Opaque server revision used only for compare-and-swap. */
5
+ version: number;
6
+ updatedAt: number;
7
+ };
8
+ export type HostedCheckpointReadResult = {
9
+ status: "unpaired";
10
+ } | {
11
+ status: "missing";
12
+ } | {
13
+ status: "found";
14
+ record: HostedAcquireCheckpoint;
15
+ } | {
16
+ status: "unavailable";
17
+ reason: string;
18
+ };
19
+ export type HostedCheckpointWriteResult = {
20
+ status: "unpaired";
21
+ } | {
22
+ status: "saved";
23
+ record: HostedAcquireCheckpoint;
24
+ } | {
25
+ status: "conflict";
26
+ current: HostedAcquireCheckpoint | null;
27
+ } | {
28
+ status: "unavailable";
29
+ reason: string;
30
+ };
31
+ type Options = {
32
+ fetchImpl?: typeof fetch;
33
+ timeoutMs?: number;
34
+ };
35
+ /** Read a query/list checkpoint. Inert unless this profile is paired. */
36
+ export declare function readHostedAcquireCheckpoint(key: string, options?: Options): Promise<HostedCheckpointReadResult>;
37
+ /**
38
+ * Mirror a local checkpoint using compare-and-swap. Pass null only when the
39
+ * caller observed no hosted record (sent as version zero); pass the exact
40
+ * positive version returned by read for an update.
41
+ */
42
+ export declare function writeHostedAcquireCheckpoint(key: string, checkpoint: AcquireDiscoveryCheckpoint, expectedVersion: number | null, options?: Options): Promise<HostedCheckpointWriteResult>;
43
+ export {};
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Authenticated acquisition-checkpoint transport for paired CLI installs.
3
+ *
4
+ * The local checkpoint remains authoritative for running a job. This module
5
+ * only mirrors/query-scoped continuation state to the paired hosted app so a
6
+ * different authenticated worker can resume it. Writes use compare-and-swap:
7
+ * a stale client receives a conflict and must explicitly reconcile instead of
8
+ * silently moving a provider cursor backwards or overwriting another worker.
9
+ */
10
+ import { getCredential } from "./credentials.js";
11
+ const ENDPOINT = "/api/cli/acquisition-checkpoint";
12
+ const DEFAULT_TIMEOUT_MS = 4000;
13
+ const CHECKPOINT_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
14
+ function validKey(key) {
15
+ if (!CHECKPOINT_KEY.test(key)) {
16
+ throw new Error("Acquisition checkpoint key must be 1-256 URL-safe characters.");
17
+ }
18
+ return key;
19
+ }
20
+ function broker() {
21
+ const credential = getCredential("broker");
22
+ if (!credential?.baseUrl || !credential.accessToken)
23
+ return null;
24
+ let url;
25
+ try {
26
+ url = new URL(credential.baseUrl);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
32
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
33
+ return null;
34
+ return { baseUrl: url.href.replace(/\/+$/, ""), accessToken: credential.accessToken };
35
+ }
36
+ function isCheckpoint(value) {
37
+ if (!value || typeof value !== "object")
38
+ return false;
39
+ const item = value;
40
+ return (typeof item.queryFingerprint === "string" &&
41
+ item.queryFingerprint.length > 0 &&
42
+ typeof item.exhausted === "boolean" &&
43
+ (item.cursor === undefined || item.cursor === null || typeof item.cursor === "string") &&
44
+ (item.offset === undefined || item.offset === null ||
45
+ (typeof item.offset === "number" && Number.isSafeInteger(item.offset) && item.offset >= 0)));
46
+ }
47
+ function parseRecord(value) {
48
+ if (!value || typeof value !== "object")
49
+ return null;
50
+ const body = value;
51
+ if (!isCheckpoint(body.checkpoint) || !Number.isSafeInteger(body.version) || body.version < 1)
52
+ return null;
53
+ if (!Number.isSafeInteger(body.updatedAt) || body.updatedAt < 0)
54
+ return null;
55
+ return {
56
+ checkpoint: body.checkpoint,
57
+ version: body.version,
58
+ updatedAt: body.updatedAt,
59
+ };
60
+ }
61
+ function reasonFor(status) {
62
+ if (status === 401 || status === 403)
63
+ return "hosted authentication was rejected; pair the CLI again";
64
+ return `hosted checkpoint request failed (HTTP ${status})`;
65
+ }
66
+ /** Read a query/list checkpoint. Inert unless this profile is paired. */
67
+ export async function readHostedAcquireCheckpoint(key, options = {}) {
68
+ validKey(key);
69
+ const paired = broker();
70
+ if (!paired)
71
+ return { status: "unpaired" };
72
+ try {
73
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}?key=${encodeURIComponent(key)}`, {
74
+ headers: { Authorization: `Bearer ${paired.accessToken}`, Accept: "application/json" },
75
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
76
+ });
77
+ if (response.status === 404)
78
+ return { status: "missing" };
79
+ if (!response.ok)
80
+ return { status: "unavailable", reason: reasonFor(response.status) };
81
+ const record = parseRecord(await response.json());
82
+ return record
83
+ ? { status: "found", record }
84
+ : { status: "unavailable", reason: "hosted checkpoint response was invalid" };
85
+ }
86
+ catch {
87
+ return { status: "unavailable", reason: "hosted checkpoint request was unavailable" };
88
+ }
89
+ }
90
+ /**
91
+ * Mirror a local checkpoint using compare-and-swap. Pass null only when the
92
+ * caller observed no hosted record (sent as version zero); pass the exact
93
+ * positive version returned by read for an update.
94
+ */
95
+ export async function writeHostedAcquireCheckpoint(key, checkpoint, expectedVersion, options = {}) {
96
+ validKey(key);
97
+ if (!isCheckpoint(checkpoint))
98
+ throw new Error("Refusing to sync an invalid acquisition checkpoint.");
99
+ if (expectedVersion !== null && (!Number.isSafeInteger(expectedVersion) || expectedVersion < 1)) {
100
+ throw new Error("expectedVersion must be null or a positive integer.");
101
+ }
102
+ const paired = broker();
103
+ if (!paired)
104
+ return { status: "unpaired" };
105
+ try {
106
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
107
+ method: "POST",
108
+ headers: { Authorization: `Bearer ${paired.accessToken}`, "Content-Type": "application/json" },
109
+ body: JSON.stringify({ key, checkpoint, expectedVersion: expectedVersion ?? 0 }),
110
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
111
+ });
112
+ if (response.status === 409) {
113
+ const body = await response.json().catch(() => null);
114
+ const current = body && typeof body === "object"
115
+ ? parseRecord(body.current)
116
+ : null;
117
+ return { status: "conflict", current };
118
+ }
119
+ if (!response.ok)
120
+ return { status: "unavailable", reason: reasonFor(response.status) };
121
+ const record = parseRecord(await response.json());
122
+ return record
123
+ ? { status: "saved", record }
124
+ : { status: "unavailable", reason: "hosted checkpoint response was invalid" };
125
+ }
126
+ catch {
127
+ return { status: "unavailable", reason: "hosted checkpoint request was unavailable" };
128
+ }
129
+ }
@@ -0,0 +1,87 @@
1
+ import type { PlanStore, StoredPlan } from "./planStore.ts";
2
+ import type { PatchOperation, PatchPlan, PatchPlanRun } from "./types.ts";
3
+ type Options = {
4
+ fetchImpl?: typeof fetch;
5
+ timeoutMs?: number;
6
+ };
7
+ export type HostedPlanState = {
8
+ patchPlanId: string;
9
+ externalPlanId: string;
10
+ documentSha256: string;
11
+ status: string;
12
+ approvedOperationIds: string[];
13
+ valueOverrides: Record<string, unknown>;
14
+ updatedAt: number;
15
+ runRecord?: PatchPlanRun;
16
+ url: string;
17
+ };
18
+ export type HostedPlanResult = {
19
+ status: "unpaired";
20
+ } | {
21
+ status: "missing";
22
+ } | {
23
+ status: "found" | "saved";
24
+ state: HostedPlanState;
25
+ } | {
26
+ status: "conflict";
27
+ reason: string;
28
+ } | {
29
+ status: "unavailable";
30
+ reason: string;
31
+ };
32
+ /** Review document intentionally omits evidence/findings and all mutable store security state. */
33
+ export declare function hostedReviewDocument(plan: PatchPlan): {
34
+ guards?: import("./types.ts").PlanGuard[] | undefined;
35
+ filter?: {
36
+ objectType: "account" | "contact" | "deal";
37
+ where: string[];
38
+ today?: string;
39
+ } | undefined;
40
+ id: string;
41
+ title: string;
42
+ createdAt: string;
43
+ status: string;
44
+ dryRun: boolean;
45
+ summary: string;
46
+ operations: PatchOperation[];
47
+ };
48
+ export declare function hostedPlanDigest(plan: PatchPlan): string;
49
+ export declare function uploadHostedPatchPlan(stored: StoredPlan, options?: Options): Promise<HostedPlanResult>;
50
+ export declare function readHostedPatchPlan(planId: string, options?: Options): Promise<HostedPlanResult>;
51
+ export type HostedReconcileResult = {
52
+ status: "unchanged" | "unpaired" | "missing";
53
+ } | {
54
+ status: "updated";
55
+ stored: StoredPlan;
56
+ remoteStatus: string;
57
+ } | {
58
+ status: "conflict" | "unavailable";
59
+ reason: string;
60
+ };
61
+ /** Pull a peer's newer lifecycle into the local replica without weakening local integrity gates. */
62
+ export declare function reconcileHostedPatchPlan(store: PlanStore, stored: StoredPlan, options?: Options): Promise<HostedReconcileResult>;
63
+ export declare function reportHostedPlanLifecycle(stored: StoredPlan, options?: Options & {
64
+ claimId?: string;
65
+ }): Promise<HostedPlanResult>;
66
+ export type HostedApplyClaimResult = {
67
+ status: "unpaired";
68
+ } | {
69
+ status: "claimed";
70
+ claimId: string;
71
+ expiresAt?: number;
72
+ } | {
73
+ status: "applied";
74
+ } | {
75
+ status: "conflict" | "unavailable";
76
+ reason: string;
77
+ };
78
+ /** Coordinate online replicas before provider I/O; provider idempotency remains the offline fallback. */
79
+ export declare function claimHostedPlanApply(stored: StoredPlan, options?: Options): Promise<HostedApplyClaimResult>;
80
+ /** Release a shared CLI claim only when preflight aborts before provider I/O. */
81
+ export declare function releaseHostedPlanApply(stored: StoredPlan, claimId: string, reason: string, options?: Options): Promise<{
82
+ status: "released" | "unpaired";
83
+ } | {
84
+ status: "conflict" | "unavailable";
85
+ reason: string;
86
+ }>;
87
+ export {};