fullstackgtm 0.47.0 → 0.49.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.
- package/CHANGELOG.md +76 -6
- package/CONTRIBUTING.md +23 -4
- package/DATA-FLOWS.md +7 -2
- package/INSTALL_FOR_AGENTS.md +15 -6
- package/README.md +28 -9
- package/SECURITY.md +27 -3
- package/dist/audit.js +7 -0
- package/dist/backfill.js +2 -16
- package/dist/cli/audit.js +10 -7
- package/dist/cli/auth.js +8 -4
- package/dist/cli/fix.d.ts +3 -0
- package/dist/cli/fix.js +90 -8
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.js +81 -3
- package/dist/cli/market.js +180 -2
- package/dist/cli/plans.js +56 -5
- package/dist/cli/suggest.d.ts +2 -1
- package/dist/cli/suggest.js +49 -3
- package/dist/cli/ui.js +2 -0
- package/dist/cli.js +41 -16
- package/dist/config.d.ts +13 -1
- package/dist/config.js +23 -2
- package/dist/connectors/hubspot.d.ts +2 -1
- package/dist/connectors/prospectSources.js +4 -11
- package/dist/connectors/salesforce.d.ts +2 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/salesforceAuth.d.ts +9 -0
- package/dist/connectors/salesforceAuth.js +47 -5
- package/dist/connectors/signalSources.js +2 -11
- package/dist/connectors/theirstack.d.ts +0 -19
- package/dist/connectors/theirstack.js +3 -10
- package/dist/credentials.js +36 -26
- package/dist/format.js +31 -5
- package/dist/freeEmailDomains.d.ts +1 -0
- package/dist/freeEmailDomains.js +14 -0
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +8 -3
- package/dist/market.d.ts +1 -1
- package/dist/market.js +7 -127
- package/dist/marketClassify.d.ts +10 -0
- package/dist/marketClassify.js +52 -1
- package/dist/marketSourcing.js +7 -45
- package/dist/mcp.js +86 -130
- package/dist/planStore.d.ts +28 -1
- package/dist/planStore.js +195 -49
- package/dist/providerError.d.ts +21 -0
- package/dist/providerError.js +30 -0
- package/dist/publicHttp.d.ts +28 -0
- package/dist/publicHttp.js +143 -0
- package/dist/relationships.d.ts +39 -0
- package/dist/relationships.js +101 -0
- package/dist/route.d.ts +46 -0
- package/dist/route.js +228 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +172 -12
- package/dist/secureFile.d.ts +15 -0
- package/dist/secureFile.js +164 -0
- package/dist/types.d.ts +16 -1
- package/docs/api.md +49 -5
- package/docs/architecture.md +18 -4
- package/llms.txt +7 -0
- package/package.json +5 -3
- package/skills/fullstackgtm/SKILL.md +9 -3
- package/src/audit.ts +7 -0
- package/src/backfill.ts +2 -17
- package/src/cli/audit.ts +10 -7
- package/src/cli/auth.ts +8 -4
- package/src/cli/fix.ts +96 -8
- package/src/cli/help.ts +88 -3
- package/src/cli/market.ts +181 -2
- package/src/cli/plans.ts +66 -5
- package/src/cli/suggest.ts +58 -4
- package/src/cli/ui.ts +1 -0
- package/src/cli.ts +39 -14
- package/src/config.ts +39 -1
- package/src/connectors/hubspot.ts +3 -1
- package/src/connectors/prospectSources.ts +4 -11
- package/src/connectors/salesforce.ts +15 -6
- package/src/connectors/salesforceAuth.ts +46 -6
- package/src/connectors/signalSources.ts +2 -12
- package/src/connectors/theirstack.ts +3 -10
- package/src/credentials.ts +47 -28
- package/src/format.ts +33 -5
- package/src/freeEmailDomains.ts +14 -0
- package/src/hierarchy.ts +185 -0
- package/src/index.ts +29 -0
- package/src/market.ts +7 -111
- package/src/marketClassify.ts +61 -1
- package/src/marketSourcing.ts +7 -43
- package/src/mcp.ts +115 -152
- package/src/planStore.ts +235 -57
- package/src/providerError.ts +37 -0
- package/src/publicHttp.ts +147 -0
- package/src/relationships.ts +115 -0
- package/src/route.ts +278 -0
- package/src/rules.ts +192 -12
- package/src/secureFile.ts +169 -0
- package/src/types.ts +18 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
fchmodSync,
|
|
8
|
+
fstatSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { dirname, isAbsolute, parse, resolve } from "node:path";
|
|
17
|
+
|
|
18
|
+
export class UnsafeManagedPathError extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "UnsafeManagedPathError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertNoSymlinkComponents(path: string): void {
|
|
26
|
+
const absolute = isAbsolute(path) ? path : resolve(path);
|
|
27
|
+
const root = parse(absolute).root;
|
|
28
|
+
const relative = absolute.slice(root.length);
|
|
29
|
+
let current = root;
|
|
30
|
+
|
|
31
|
+
for (const component of relative.split(/[\\/]+/).filter(Boolean)) {
|
|
32
|
+
current = resolve(current, component);
|
|
33
|
+
let stat;
|
|
34
|
+
try {
|
|
35
|
+
stat = lstatSync(current);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (stat.isSymbolicLink()) {
|
|
41
|
+
throw new UnsafeManagedPathError(`Refusing to use unsafe path containing a symbolic link: ${current}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read a managed file through a no-follow descriptor and require a regular file. */
|
|
47
|
+
export function readSecureRegularFile(
|
|
48
|
+
path: string,
|
|
49
|
+
options: { tightenMode?: number; onModeTightened?: (previousMode: number) => void } = {},
|
|
50
|
+
): string {
|
|
51
|
+
const absolute = resolve(path);
|
|
52
|
+
assertNoSymlinkComponents(dirname(absolute));
|
|
53
|
+
try {
|
|
54
|
+
if (lstatSync(absolute).isSymbolicLink()) {
|
|
55
|
+
throw new UnsafeManagedPathError(`Refusing to read symbolic link: ${absolute}`);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
62
|
+
let fd: number | undefined;
|
|
63
|
+
try {
|
|
64
|
+
fd = openSync(absolute, constants.O_RDONLY | noFollow);
|
|
65
|
+
const stat = fstatSync(fd);
|
|
66
|
+
if (!stat.isFile()) {
|
|
67
|
+
throw new UnsafeManagedPathError(`Refusing to read non-regular file: ${absolute}`);
|
|
68
|
+
}
|
|
69
|
+
if (options.tightenMode !== undefined) {
|
|
70
|
+
const previousMode = stat.mode & 0o777;
|
|
71
|
+
if ((previousMode & ~options.tightenMode) !== 0) {
|
|
72
|
+
try {
|
|
73
|
+
fchmodSync(fd, options.tightenMode);
|
|
74
|
+
options.onModeTightened?.(previousMode);
|
|
75
|
+
} catch {
|
|
76
|
+
// Windows and some non-POSIX filesystems do not implement chmod.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return readFileSync(fd, "utf8");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP") {
|
|
83
|
+
throw new UnsafeManagedPathError(`Refusing to read symbolic link: ${absolute}`);
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
} finally {
|
|
87
|
+
if (fd !== undefined) closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Atomically replace a private file without ever opening the destination.
|
|
93
|
+
* Existing destination and parent symlinks are rejected. The temporary file
|
|
94
|
+
* is created exclusively beside the destination so rename remains atomic.
|
|
95
|
+
*/
|
|
96
|
+
export function writeSecureFileAtomic(path: string, contents: string | Uint8Array): void {
|
|
97
|
+
const destination = resolve(path);
|
|
98
|
+
const parent = dirname(destination);
|
|
99
|
+
assertNoSymlinkComponents(parent);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (lstatSync(destination).isSymbolicLink()) {
|
|
103
|
+
throw new Error(`Refusing to replace symbolic link: ${destination}`);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let temporary = "";
|
|
110
|
+
let fd: number | undefined;
|
|
111
|
+
try {
|
|
112
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
113
|
+
temporary = `${destination}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
114
|
+
try {
|
|
115
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
116
|
+
fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 0o600);
|
|
117
|
+
break;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (fd === undefined) throw new Error(`Unable to create an exclusive temporary file for ${destination}`);
|
|
123
|
+
|
|
124
|
+
writeFileSync(fd, contents);
|
|
125
|
+
fsyncSync(fd);
|
|
126
|
+
closeSync(fd);
|
|
127
|
+
fd = undefined;
|
|
128
|
+
try {
|
|
129
|
+
chmodSync(temporary, 0o600);
|
|
130
|
+
} catch {
|
|
131
|
+
// Windows and some non-POSIX filesystems do not implement chmod.
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Recheck immediately before replacement. rename replaces a symlink rather
|
|
135
|
+
// than following it, but rejecting one makes managed-path policy explicit.
|
|
136
|
+
try {
|
|
137
|
+
if (lstatSync(destination).isSymbolicLink()) {
|
|
138
|
+
throw new Error(`Refusing to replace symbolic link: ${destination}`);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
142
|
+
}
|
|
143
|
+
assertNoSymlinkComponents(parent);
|
|
144
|
+
renameSync(temporary, destination);
|
|
145
|
+
temporary = "";
|
|
146
|
+
|
|
147
|
+
// Persist the directory entry where supported. Opening/fsyncing a
|
|
148
|
+
// directory is not available on Windows, so this durability enhancement is
|
|
149
|
+
// deliberately best-effort there.
|
|
150
|
+
let directoryFd: number | undefined;
|
|
151
|
+
try {
|
|
152
|
+
directoryFd = openSync(parent, constants.O_RDONLY);
|
|
153
|
+
fsyncSync(directoryFd);
|
|
154
|
+
} catch {
|
|
155
|
+
// Unsupported by the platform/filesystem.
|
|
156
|
+
} finally {
|
|
157
|
+
if (directoryFd !== undefined) closeSync(directoryFd);
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
if (fd !== undefined) closeSync(fd);
|
|
161
|
+
if (temporary) {
|
|
162
|
+
try {
|
|
163
|
+
unlinkSync(temporary);
|
|
164
|
+
} catch {
|
|
165
|
+
// Best-effort cleanup after a failed write.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type ApprovalStatus =
|
|
|
20
20
|
| "draft"
|
|
21
21
|
| "needs_approval"
|
|
22
22
|
| "approved"
|
|
23
|
+
| "applying"
|
|
23
24
|
| "rejected"
|
|
24
25
|
| "applied";
|
|
25
26
|
|
|
@@ -105,6 +106,15 @@ export type CreateRecordPayload = {
|
|
|
105
106
|
|
|
106
107
|
export type AuditFindingSeverity = "info" | "warning" | "critical";
|
|
107
108
|
|
|
109
|
+
export type PatchPlanAssumptionConfidence = "derived" | "heuristic" | "guess";
|
|
110
|
+
|
|
111
|
+
export type PatchPlanAssumption = {
|
|
112
|
+
id: string;
|
|
113
|
+
text: string;
|
|
114
|
+
source?: string;
|
|
115
|
+
confidence?: PatchPlanAssumptionConfidence;
|
|
116
|
+
};
|
|
117
|
+
|
|
108
118
|
/**
|
|
109
119
|
* One claim that a canonical record exists in an external system. A record
|
|
110
120
|
* merged from several providers carries one identity per provider.
|
|
@@ -397,6 +407,10 @@ export type PatchPlan = {
|
|
|
397
407
|
* Account/contact hygiene findings are NOT here; read `findings` for those. */
|
|
398
408
|
pipelineFindings?: PipelineFinding[];
|
|
399
409
|
evidence?: GtmEvidence[];
|
|
410
|
+
/** Assumptions used to turn audit observations into findings and operations. */
|
|
411
|
+
assumptions?: PatchPlanAssumption[];
|
|
412
|
+
/** Human decisions or unresolved questions that affect how the plan should be applied. */
|
|
413
|
+
openQuestions?: string[];
|
|
400
414
|
operations: PatchOperation[];
|
|
401
415
|
/**
|
|
402
416
|
* The filter this plan's operations were selected by. Re-evaluated per
|
|
@@ -454,6 +468,8 @@ export type GtmRuleContext = {
|
|
|
454
468
|
export type GtmRuleResult = {
|
|
455
469
|
findings: AuditFinding[];
|
|
456
470
|
operations: PatchOperation[];
|
|
471
|
+
/** Rule-level assumptions that explain the policy or heuristic behind returned findings. */
|
|
472
|
+
assumptions?: PatchPlanAssumption[];
|
|
457
473
|
};
|
|
458
474
|
|
|
459
475
|
/**
|
|
@@ -466,6 +482,8 @@ export type GtmAuditRule = {
|
|
|
466
482
|
description: string;
|
|
467
483
|
/** Grouping for docs and discovery, e.g. "hygiene", "forecast", "coverage". */
|
|
468
484
|
category?: string;
|
|
485
|
+
/** False for findings-only rules whose evaluator intentionally emits no patch operations. */
|
|
486
|
+
emitsOperations?: boolean;
|
|
469
487
|
evaluate: (context: GtmRuleContext) => GtmRuleResult;
|
|
470
488
|
};
|
|
471
489
|
|