fullstackgtm 0.10.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 +381 -0
- package/INSTALL_FOR_AGENTS.md +87 -0
- package/LICENSE +202 -0
- package/README.md +230 -0
- package/dist/audit.d.ts +7 -0
- package/dist/audit.js +202 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +6 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +915 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +85 -0
- package/dist/connector.d.ts +30 -0
- package/dist/connector.js +94 -0
- package/dist/connectors/hubspot.d.ts +20 -0
- package/dist/connectors/hubspot.js +409 -0
- package/dist/connectors/hubspotAuth.d.ts +42 -0
- package/dist/connectors/hubspotAuth.js +189 -0
- package/dist/connectors/salesforce.d.ts +26 -0
- package/dist/connectors/salesforce.js +318 -0
- package/dist/connectors/salesforceAuth.d.ts +44 -0
- package/dist/connectors/salesforceAuth.js +120 -0
- package/dist/connectors/stripe.d.ts +27 -0
- package/dist/connectors/stripe.js +176 -0
- package/dist/credentials.d.ts +75 -0
- package/dist/credentials.js +197 -0
- package/dist/demo.d.ts +20 -0
- package/dist/demo.js +169 -0
- package/dist/diff.d.ts +46 -0
- package/dist/diff.js +107 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +109 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/mappings.d.ts +8 -0
- package/dist/mappings.js +123 -0
- package/dist/mcp-bin.d.ts +2 -0
- package/dist/mcp-bin.js +33 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +140 -0
- package/dist/merge.d.ts +48 -0
- package/dist/merge.js +145 -0
- package/dist/planStore.d.ts +31 -0
- package/dist/planStore.js +116 -0
- package/dist/rules.d.ts +24 -0
- package/dist/rules.js +512 -0
- package/dist/sampleData.d.ts +2 -0
- package/dist/sampleData.js +115 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +8 -0
- package/docs/api.md +72 -0
- package/docs/roadmap-to-1.0.md +121 -0
- package/llms.txt +25 -0
- package/package.json +76 -0
- package/src/audit.ts +242 -0
- package/src/bin.ts +7 -0
- package/src/cli.ts +1042 -0
- package/src/config.ts +113 -0
- package/src/connector.ts +140 -0
- package/src/connectors/hubspot.ts +528 -0
- package/src/connectors/hubspotAuth.ts +246 -0
- package/src/connectors/salesforce.ts +420 -0
- package/src/connectors/salesforceAuth.ts +167 -0
- package/src/connectors/stripe.ts +215 -0
- package/src/credentials.ts +282 -0
- package/src/demo.ts +200 -0
- package/src/diff.ts +158 -0
- package/src/format.ts +162 -0
- package/src/index.ts +129 -0
- package/src/mappings.ts +157 -0
- package/src/mcp-bin.ts +32 -0
- package/src/mcp.ts +185 -0
- package/src/merge.ts +235 -0
- package/src/planStore.ts +155 -0
- package/src/rules.ts +539 -0
- package/src/sampleData.ts +117 -0
- package/src/types.ts +372 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { builtinAuditRules } from "./rules.ts";
|
|
5
|
+
import type { GtmAuditRule, GtmPolicy } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Project-level configuration: `fullstackgtm.config.json` in the working
|
|
9
|
+
* directory (or an explicit `--config` path). Policy thresholds, rule
|
|
10
|
+
* selection, and rule packages — third-party modules exporting additional
|
|
11
|
+
* `GtmAuditRule`s — all live here so a team's audit standard is reviewable
|
|
12
|
+
* in version control.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const CONFIG_FILE_NAME = "fullstackgtm.config.json";
|
|
16
|
+
|
|
17
|
+
export type FullstackgtmConfig = {
|
|
18
|
+
policy?: Partial<GtmPolicy>;
|
|
19
|
+
rules?: {
|
|
20
|
+
/** If set, only these rule ids run (after rule packages are loaded). */
|
|
21
|
+
enabled?: string[];
|
|
22
|
+
/** Removed from the effective set after `enabled` is applied. */
|
|
23
|
+
disabled?: string[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Module specifiers resolved relative to the config file. Each module must
|
|
27
|
+
* export `rules: GtmAuditRule[]`. Use compiled .js/.mjs modules so they
|
|
28
|
+
* load under plain Node.
|
|
29
|
+
*/
|
|
30
|
+
rulePackages?: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type LoadedConfig = {
|
|
34
|
+
config: FullstackgtmConfig;
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function loadConfig(
|
|
39
|
+
explicitPath?: string,
|
|
40
|
+
cwd = process.cwd(),
|
|
41
|
+
): LoadedConfig | null {
|
|
42
|
+
const path = explicitPath ? resolve(cwd, explicitPath) : resolve(cwd, CONFIG_FILE_NAME);
|
|
43
|
+
let raw: string;
|
|
44
|
+
try {
|
|
45
|
+
raw = readFileSync(path, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
if (explicitPath) throw new Error(`Could not read config file: ${path}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const config = JSON.parse(raw) as FullstackgtmConfig;
|
|
51
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
52
|
+
throw new Error(`${path} must contain a JSON object.`);
|
|
53
|
+
}
|
|
54
|
+
return { config, path };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Overlay config policy values onto a base policy; defined values win. */
|
|
58
|
+
export function mergePolicy(base: GtmPolicy, config?: FullstackgtmConfig): GtmPolicy {
|
|
59
|
+
if (!config?.policy) return base;
|
|
60
|
+
const merged = { ...base };
|
|
61
|
+
for (const [key, value] of Object.entries(config.policy)) {
|
|
62
|
+
if (value !== undefined) {
|
|
63
|
+
(merged as Record<string, unknown>)[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the effective rule set: built-ins plus rule-package exports, then
|
|
71
|
+
* `enabled` (allow-list) and `disabled` filters.
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveConfiguredRules(
|
|
74
|
+
loaded?: LoadedConfig | null,
|
|
75
|
+
baseRules: GtmAuditRule[] = builtinAuditRules,
|
|
76
|
+
): Promise<GtmAuditRule[]> {
|
|
77
|
+
let rules = [...baseRules];
|
|
78
|
+
|
|
79
|
+
for (const specifier of loaded?.config.rulePackages ?? []) {
|
|
80
|
+
const resolvedSpecifier =
|
|
81
|
+
specifier.startsWith(".") || isAbsolute(specifier)
|
|
82
|
+
? pathToFileURL(resolve(dirname(loaded!.path), specifier)).href
|
|
83
|
+
: specifier;
|
|
84
|
+
const rulePackage = await import(resolvedSpecifier);
|
|
85
|
+
const imported = rulePackage.rules ?? rulePackage.default?.rules;
|
|
86
|
+
if (!Array.isArray(imported)) {
|
|
87
|
+
throw new Error(`Rule package ${specifier} must export \`rules: GtmAuditRule[]\`.`);
|
|
88
|
+
}
|
|
89
|
+
for (const rule of imported) {
|
|
90
|
+
if (!rule?.id || typeof rule.evaluate !== "function") {
|
|
91
|
+
throw new Error(`Rule package ${specifier} exported an invalid rule.`);
|
|
92
|
+
}
|
|
93
|
+
rules.push(rule as GtmAuditRule);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const enabled = loaded?.config.rules?.enabled;
|
|
98
|
+
if (enabled?.length) {
|
|
99
|
+
const known = new Map(rules.map((rule) => [rule.id, rule]));
|
|
100
|
+
rules = enabled.map((id) => {
|
|
101
|
+
const rule = known.get(id);
|
|
102
|
+
if (!rule) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Config enables unknown rule: ${id}. Known rules: ${Array.from(known.keys()).join(", ")}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return rule;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const disabled = new Set(loaded?.config.rules?.disabled ?? []);
|
|
112
|
+
return rules.filter((rule) => !disabled.has(rule.id));
|
|
113
|
+
}
|
package/src/connector.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { requiresHumanInput } from "./rules.ts";
|
|
2
|
+
import type {
|
|
3
|
+
GtmConnector,
|
|
4
|
+
PatchOperation,
|
|
5
|
+
PatchOperationResult,
|
|
6
|
+
PatchPlan,
|
|
7
|
+
PatchPlanRun,
|
|
8
|
+
PatchPlanRunStatus,
|
|
9
|
+
} from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export type ApplyPatchPlanOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Explicit allow-list of operation ids the human approved. Operations not
|
|
14
|
+
* listed are skipped without any provider call. An empty list rejects the
|
|
15
|
+
* whole run.
|
|
16
|
+
*/
|
|
17
|
+
approvedOperationIds: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Concrete values supplied at approval time for operations whose
|
|
20
|
+
* `afterValue` is a `requires_human_*` placeholder, keyed by operation id.
|
|
21
|
+
*/
|
|
22
|
+
valueOverrides?: Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* Compare-and-set: before each field write, read the live provider value
|
|
25
|
+
* and refuse the write (`conflict` result) if it no longer matches the
|
|
26
|
+
* plan's `beforeValue`. Defaults to on when the connector supports
|
|
27
|
+
* `readField`.
|
|
28
|
+
*/
|
|
29
|
+
checkConflicts?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const FIELD_WRITE_OPERATIONS = new Set(["set_field", "clear_field", "link_record"]);
|
|
33
|
+
|
|
34
|
+
function normalizeForComparison(value: unknown): string | null {
|
|
35
|
+
if (value === undefined || value === null || value === "") return null;
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply an approved subset of a patch plan through a connector.
|
|
41
|
+
*
|
|
42
|
+
* The safety contract lives here, not in connectors:
|
|
43
|
+
* - nothing is written unless the operation id was explicitly approved
|
|
44
|
+
* - placeholder values are never written; they require an override
|
|
45
|
+
* - every operation produces a result, and the run is the audit record
|
|
46
|
+
*/
|
|
47
|
+
export async function applyPatchPlan(
|
|
48
|
+
connector: GtmConnector,
|
|
49
|
+
plan: PatchPlan,
|
|
50
|
+
options: ApplyPatchPlanOptions,
|
|
51
|
+
): Promise<PatchPlanRun> {
|
|
52
|
+
if (!connector.applyOperation) {
|
|
53
|
+
throw new Error(`The ${connector.provider} connector is read-only.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const startedAt = new Date().toISOString();
|
|
57
|
+
const approved = new Set(options.approvedOperationIds);
|
|
58
|
+
const checkConflicts =
|
|
59
|
+
options.checkConflicts ?? typeof connector.readField === "function";
|
|
60
|
+
const results: PatchOperationResult[] = [];
|
|
61
|
+
let attempted = 0;
|
|
62
|
+
let applied = 0;
|
|
63
|
+
|
|
64
|
+
for (const operation of plan.operations) {
|
|
65
|
+
if (!approved.has(operation.id)) {
|
|
66
|
+
results.push({
|
|
67
|
+
operationId: operation.id,
|
|
68
|
+
status: "skipped",
|
|
69
|
+
detail: "Operation was not approved.",
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const override = options.valueOverrides?.[operation.id];
|
|
75
|
+
if (requiresHumanInput(operation.afterValue) && override === undefined) {
|
|
76
|
+
results.push({
|
|
77
|
+
operationId: operation.id,
|
|
78
|
+
status: "skipped",
|
|
79
|
+
detail: "Operation needs a concrete value; supply a value override.",
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
checkConflicts &&
|
|
86
|
+
connector.readField &&
|
|
87
|
+
operation.field &&
|
|
88
|
+
FIELD_WRITE_OPERATIONS.has(operation.operation)
|
|
89
|
+
) {
|
|
90
|
+
const current = await connector.readField(
|
|
91
|
+
operation.objectType,
|
|
92
|
+
operation.objectId,
|
|
93
|
+
operation.field,
|
|
94
|
+
);
|
|
95
|
+
const expected = normalizeForComparison(operation.beforeValue);
|
|
96
|
+
const found = normalizeForComparison(current);
|
|
97
|
+
if (expected !== found) {
|
|
98
|
+
results.push({
|
|
99
|
+
operationId: operation.id,
|
|
100
|
+
status: "conflict",
|
|
101
|
+
detail: `Value drifted since the plan was proposed: expected ${expected ?? "∅"}, found ${found ?? "∅"}. Re-run the audit.`,
|
|
102
|
+
providerData: { currentValue: current ?? null },
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const resolved: PatchOperation =
|
|
109
|
+
override === undefined ? operation : { ...operation, afterValue: override };
|
|
110
|
+
|
|
111
|
+
attempted += 1;
|
|
112
|
+
try {
|
|
113
|
+
const result = await connector.applyOperation(resolved);
|
|
114
|
+
results.push(result);
|
|
115
|
+
if (result.status === "applied") applied += 1;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
results.push({
|
|
118
|
+
operationId: operation.id,
|
|
119
|
+
status: "failed",
|
|
120
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
planId: plan.id,
|
|
127
|
+
provider: connector.provider,
|
|
128
|
+
startedAt,
|
|
129
|
+
finishedAt: new Date().toISOString(),
|
|
130
|
+
status: runStatus(attempted, applied),
|
|
131
|
+
results,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runStatus(attempted: number, applied: number): PatchPlanRunStatus {
|
|
136
|
+
if (attempted === 0) return "rejected";
|
|
137
|
+
if (applied === attempted) return "applied";
|
|
138
|
+
if (applied > 0) return "partial";
|
|
139
|
+
return "failed";
|
|
140
|
+
}
|