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.
- package/CHANGELOG.md +91 -0
- package/CONTRIBUTING.md +23 -4
- package/DATA-FLOWS.md +8 -2
- package/INSTALL_FOR_AGENTS.md +13 -4
- package/README.md +53 -1
- package/SECURITY.md +27 -3
- package/dist/acquireCheckpoint.d.ts +33 -0
- package/dist/acquireCheckpoint.js +149 -0
- package/dist/acquireLinkedIn.d.ts +2 -0
- package/dist/acquireLinkedIn.js +1 -1
- package/dist/cli/audit.js +10 -7
- package/dist/cli/auth.js +8 -4
- package/dist/cli/enrich.js +283 -79
- package/dist/cli/fix.js +20 -7
- package/dist/cli/help.js +23 -15
- package/dist/cli/market.js +180 -2
- package/dist/cli/plans.js +154 -9
- package/dist/cli/shared.d.ts +3 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli/ui.d.ts +10 -5
- package/dist/cli/ui.js +18 -5
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +23 -2
- package/dist/connectors/linkedin.d.ts +2 -0
- package/dist/connectors/linkedin.js +5 -0
- package/dist/connectors/prospectSources.d.ts +23 -0
- package/dist/connectors/prospectSources.js +25 -14
- 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/theirstack.d.ts +0 -19
- package/dist/connectors/theirstack.js +3 -10
- package/dist/credentials.js +36 -26
- package/dist/enrich.d.ts +44 -1
- package/dist/hostedAcquireCheckpoint.d.ts +43 -0
- package/dist/hostedAcquireCheckpoint.js +129 -0
- package/dist/hostedPatchPlan.d.ts +87 -0
- package/dist/hostedPatchPlan.js +270 -0
- package/dist/icp.js +13 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -2
- 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 +67 -115
- package/dist/planStore.d.ts +42 -1
- package/dist/planStore.js +268 -49
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +2 -2
- 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/secureFile.d.ts +15 -0
- package/dist/secureFile.js +164 -0
- package/dist/types.d.ts +1 -1
- package/docs/api.md +53 -2
- package/docs/architecture.md +22 -2
- package/llms.txt +7 -0
- package/package.json +5 -3
- package/skills/fullstackgtm/SKILL.md +8 -2
- package/src/acquireCheckpoint.ts +186 -0
- package/src/acquireLinkedIn.ts +3 -1
- package/src/cli/audit.ts +10 -7
- package/src/cli/auth.ts +8 -4
- package/src/cli/enrich.ts +322 -78
- package/src/cli/fix.ts +28 -7
- package/src/cli/help.ts +24 -15
- package/src/cli/market.ts +181 -2
- package/src/cli/plans.ts +159 -9
- package/src/cli/shared.ts +2 -1
- package/src/cli/ui.ts +19 -9
- package/src/cli.ts +1 -1
- package/src/config.ts +39 -1
- package/src/connectors/linkedin.ts +6 -0
- package/src/connectors/prospectSources.ts +50 -15
- package/src/connectors/salesforce.ts +12 -5
- package/src/connectors/salesforceAuth.ts +46 -6
- package/src/connectors/theirstack.ts +3 -10
- package/src/credentials.ts +47 -28
- package/src/enrich.ts +47 -1
- package/src/hostedAcquireCheckpoint.ts +156 -0
- package/src/hostedPatchPlan.ts +286 -0
- package/src/icp.ts +14 -2
- package/src/index.ts +24 -0
- package/src/market.ts +7 -111
- package/src/marketClassify.ts +61 -1
- package/src/marketSourcing.ts +7 -43
- package/src/mcp.ts +93 -136
- package/src/planStore.ts +322 -57
- package/src/progress.ts +2 -2
- package/src/providerError.ts +37 -0
- package/src/publicHttp.ts +147 -0
- package/src/secureFile.ts +169 -0
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,97 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.50.0] — 2026-07-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- CLI and hosted now act as local-first replicas of one immutable patch plan.
|
|
15
|
+
Either surface may execute when it has a compatible CRM connection; online
|
|
16
|
+
execution uses a shared claim, while offline CLI work remains available and
|
|
17
|
+
reconciles later through stable provider idempotency and exact receipts.
|
|
18
|
+
- `fullstackgtm plans sync` explicitly exchanges approval revisions, terminal
|
|
19
|
+
state, and per-operation execution receipts. Plan list, show, and apply also
|
|
20
|
+
perform best-effort ambient check-ins.
|
|
21
|
+
- Hosted can execute fully synchronized CLI-origin plans through its connected
|
|
22
|
+
HubSpot or Salesforce connector. The CLI imports hosted runs without replaying
|
|
23
|
+
provider writes, and hosted imports exact CLI outcomes rather than inferring
|
|
24
|
+
success for every approved row.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- Shared execution claims are bound to the immutable plan, exact approved
|
|
29
|
+
subset, replica, and paired CLI token. Approval edits freeze during apply;
|
|
30
|
+
stranded claims require an explicit audited recovery, and preflight release
|
|
31
|
+
is permitted only before provider I/O by the owning replica.
|
|
32
|
+
- Hosted ingestion recomputes the immutable document digest and verifies an
|
|
33
|
+
exact one-to-one operation set. Terminal transitions require complete,
|
|
34
|
+
duplicate-free receipts for the approved subset and cannot widen authority.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Paired CLIs now mirror saved plans into the hosted Patch Plans queue and
|
|
39
|
+
print an exact review deep-link. Mirrors are org-scoped, immutable and
|
|
40
|
+
idempotent; hosted approvals are hash/operation-verified and locally
|
|
41
|
+
HMAC-signed before CLI execution. Security state and evidence/findings stay
|
|
42
|
+
local, while bounded provider receipts replicate between capable executors.
|
|
43
|
+
- `enrich acquire` now reports discovery, resolution, and plan-building progress
|
|
44
|
+
across the previously silent provider/dedupe middle. Paired hosted runs render
|
|
45
|
+
the same live stage, counters, and generic progress notes on their detail page.
|
|
46
|
+
- Interactive acquisition runs retain both the CRM snapshot and acquisition
|
|
47
|
+
checklists in terminal history, then print a compact plan summary, copyable
|
|
48
|
+
review/approve/apply commands, and the paired hosted Runs deep-link.
|
|
49
|
+
- Persistent progress completion freezes the already-painted final frame rather
|
|
50
|
+
than repainting it, preventing duplicate live/final checklists in scrollback.
|
|
51
|
+
- Acquisition now persists query-bound Pipe0 cursors and HeyReach/Explorium
|
|
52
|
+
offsets so scheduled runs traverse the full audience instead of repeatedly
|
|
53
|
+
exhausting the first 25 results. `--max` controls desired new leads and the
|
|
54
|
+
new `--scan-limit` bounds raw duplicate-heavy scanning. Run history records
|
|
55
|
+
the complete discovery funnel and provider-confirmed exhaustion state.
|
|
56
|
+
- Checkpoints now live in a dedicated profile-scoped store keyed independently
|
|
57
|
+
by provider/source/list/query. Paired CLIs synchronize opaque continuation to
|
|
58
|
+
an organization-scoped hosted record using broker authentication and
|
|
59
|
+
compare-and-swap revisions; conflicts retain the newer hosted checkpoint.
|
|
60
|
+
- The documented LinkedIn `--list <id>` flag is accepted by strict CLI parsing,
|
|
61
|
+
and NAICS-only software ICPs retain their industry constraint on Pipe0.
|
|
62
|
+
|
|
63
|
+
## [0.49.0] — 2026-07-10
|
|
64
|
+
|
|
65
|
+
### Security
|
|
66
|
+
|
|
67
|
+
- MCP apply now accepts only store-backed, HMAC-approved plan ids; external
|
|
68
|
+
plan paths and caller-supplied approvals/value overrides are rejected.
|
|
69
|
+
- Apply uses an atomic single-owner claim and durable attempt journal.
|
|
70
|
+
Uncertain attempts remain fail-closed until an operator acknowledges provider
|
|
71
|
+
state; recovery clears every approval and never replays writes automatically.
|
|
72
|
+
- Salesforce credential-bearing endpoints are restricted to canonical HTTPS
|
|
73
|
+
Salesforce origins and redirects are handled without forwarding credentials.
|
|
74
|
+
- Executable rule packages require explicit CLI trust and are disabled entirely
|
|
75
|
+
in MCP. Implicit repository configuration cannot activate plugins.
|
|
76
|
+
- Sensitive local files use atomic no-follow writes and verified no-follow
|
|
77
|
+
reads; symlinked credential, plan, and managed-home paths are refused.
|
|
78
|
+
- Market capture/sourcing uses DNS-pinned public-only HTTP with per-hop redirect
|
|
79
|
+
validation, bounded responses, and cross-origin credential stripping.
|
|
80
|
+
- Provider HTTP failures expose typed status metadata without raw response
|
|
81
|
+
bodies, reflected secrets, or PII.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- **Breaking pre-1.0 plan-store contract:** `PlanStore` implementations now
|
|
86
|
+
participate in apply claims, attempt recovery, and claim-bound run recording;
|
|
87
|
+
`ApprovalStatus` includes `applying`. Custom stores must implement the new
|
|
88
|
+
lifecycle before upgrading.
|
|
89
|
+
- Public mirror generation is commit-derived, allowlisted, secret-scanned, and
|
|
90
|
+
fail-closed. npm publishing uses pinned actions/npm plus a protected release
|
|
91
|
+
environment and provenance verification.
|
|
92
|
+
- Package CI now validates Node 20/22, Linux/macOS/Windows compiled runtimes,
|
|
93
|
+
and actual installed tarballs with and without optional MCP peers.
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- `bulk-update --help` and `-h` now return focused help before parsing an object
|
|
98
|
+
type or touching configuration, credentials, disk, or network.
|
|
99
|
+
- Installation guidance no longer pins a brittle exact demo finding count.
|
|
100
|
+
|
|
10
101
|
## [0.48.0] — 2026-07-08
|
|
11
102
|
|
|
12
103
|
### Added
|
package/CONTRIBUTING.md
CHANGED
|
@@ -44,6 +44,21 @@ node --experimental-strip-types --test tests/fullstackgtm*.test.ts # from the
|
|
|
44
44
|
node packages/fullstackgtm/src/bin.ts doctor # run the CLI locally
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Before a release-facing change, verify the artifact rather than only its source
|
|
48
|
+
tree:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd packages/fullstackgtm
|
|
52
|
+
npm run build
|
|
53
|
+
node scripts/verify-packed-install.mjs # isolated install, optional peers absent
|
|
54
|
+
node scripts/verify-packed-install.mjs --with-peers # isolated install, MCP peers present
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The verifier creates the real npm tarball, enforces required/prohibited paths,
|
|
58
|
+
imports the public export, checks its declaration entrypoint, and runs both
|
|
59
|
+
installed binaries. CI repeats compiled-runtime checks on Node 20 and 22 across
|
|
60
|
+
Linux, macOS, and Windows; source-level tests remain on Node 22.6+.
|
|
61
|
+
|
|
47
62
|
> **Tests live at the monorepo root `tests/`, not in the package.** Running
|
|
48
63
|
> `npm test` *inside* `packages/fullstackgtm` deliberately fails with a pointer
|
|
49
64
|
> (a `pretest` guard) rather than silently passing with zero tests. On the
|
|
@@ -78,13 +93,17 @@ the development monorepo). From the monorepo:
|
|
|
78
93
|
|
|
79
94
|
1. Bump `packages/fullstackgtm/package.json` version + add a `CHANGELOG.md`
|
|
80
95
|
entry; merge to `main`.
|
|
81
|
-
2. `scripts/sync-oss.sh
|
|
82
|
-
|
|
96
|
+
2. Run `scripts/sync-oss.sh` to export an allowlisted mirror from the full
|
|
97
|
+
source commit, build `dist/`, scan for secrets, and inspect the complete
|
|
98
|
+
staged diff. Then rerun with `--push` after review. Clone failures stop the
|
|
99
|
+
release; only a genuine first publication may use the separately confirmed
|
|
100
|
+
`--initialize-public-repo` mode.
|
|
83
101
|
3. Re-check the mirror's `package.json` version, then tag `vX.Y.Z` on the mirror
|
|
84
102
|
and push the tag.
|
|
85
103
|
4. GitHub Actions (`release.yml`) publishes to npm via **OIDC trusted
|
|
86
|
-
publishing** — no tokens.
|
|
87
|
-
|
|
104
|
+
publishing** — no tokens. The protected `npm-production` environment must
|
|
105
|
+
require an independent reviewer. The workflow rebuilds from source and
|
|
106
|
+
refuses to publish if committed `dist/` differs (supply-chain gate).
|
|
88
107
|
|
|
89
108
|
**Access a new co-maintainer needs to cut a release:** write access to
|
|
90
109
|
`github.com/fullstackgtm/core` (for `sync-oss.sh --push` and the tag push). No
|
package/DATA-FLOWS.md
CHANGED
|
@@ -10,7 +10,8 @@ is no fullstackgtm-operated server in the data path for the open package.**
|
|
|
10
10
|
|
|
11
11
|
- CRM snapshots, patch plans, approvals, apply-run records, market captures and
|
|
12
12
|
observations, enrich run state, and the signing/credential stores all live
|
|
13
|
-
under `$FSGTM_HOME` (default `~/.fullstackgtm`), `0600`/`0700
|
|
13
|
+
under `$FSGTM_HOME` (default `~/.fullstackgtm`), `0600`/`0700`, using atomic
|
|
14
|
+
no-follow file I/O. Nothing is
|
|
14
15
|
uploaded to Full Stack GTM.
|
|
15
16
|
- No telemetry, analytics, or phone-home. The core package has zero runtime
|
|
16
17
|
dependencies; the only network calls are the ones listed below, all to
|
|
@@ -23,12 +24,17 @@ is no fullstackgtm-operated server in the data path for the open package.**
|
|
|
23
24
|
| `snapshot`, `audit`, `apply`, `resolve`, `bulk-update`, `dedupe`, `reassign`, `fix`, `enrich` (writeback) | **Your CRM** (HubSpot / Salesforce / Stripe API) | Reads: your CRM records. Writes: only approved patch operations. | Your CRM token (env / stored / broker) |
|
|
24
25
|
| `call parse`, `call score`, `market classify`, `market refresh` | **Your LLM provider** (api.anthropic.com or api.openai.com) | The call transcript / captured competitor page text you point at, plus the extraction prompt | Your `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (BYO) |
|
|
25
26
|
| `enrich append --source apollo`, `enrich refresh` | **Apollo** (api.apollo.io) | The company domain / contact email being enriched | Your `APOLLO_API_KEY` (BYO) |
|
|
26
|
-
| `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` |
|
|
27
|
+
| `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` | A bounded HTTP GET; DNS answers are validated and pinned, mixed/private/reserved answers are refused, and every redirect is revalidated | none |
|
|
27
28
|
| `login --via <url>` (optional) | **Your hosted deployment's broker** | A pairing handshake; the broker mints short-lived CRM tokens | broker pairing token |
|
|
29
|
+
| paired `enrich acquire` checkpoint sync (optional) | **Your hosted deployment** | Opaque query fingerprint plus provider cursor/list offset, exhaustion flag, and CAS revision. No prospect records, emails, names, or provider credentials. Scoped to the broker token's organization. | broker pairing token |
|
|
28
30
|
|
|
29
31
|
Commands not listed (`plans`, `rules`, `doctor`, `schedule`, `audit-log`,
|
|
30
32
|
`diff`, `merge`, report rendering) make **no network calls**.
|
|
31
33
|
|
|
34
|
+
Provider HTTP failures persist only typed provider/operation/status/retryability
|
|
35
|
+
metadata. Raw third-party response bodies—which may reflect credentials,
|
|
36
|
+
filters, or PII—are not written to run records or normal CLI/MCP errors.
|
|
37
|
+
|
|
32
38
|
## Avoiding third-party data egress
|
|
33
39
|
|
|
34
40
|
- **LLM verbs are optional.** `call parse --deterministic` uses a free,
|
package/INSTALL_FOR_AGENTS.md
CHANGED
|
@@ -33,9 +33,11 @@ machine — that is normal and does not block the next step.
|
|
|
33
33
|
fullstackgtm audit --demo --json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Expect a JSON patch plan with `dryRun: true
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
Expect a JSON patch plan with `dryRun: true`, a non-empty `findings` array, and
|
|
37
|
+
matching proposed operations over a generated, deliberately messy CRM. The
|
|
38
|
+
exact count can grow as built-in rules are added; the stable contract is
|
|
39
|
+
determinism per release and seed. `--seed 7` is the default, so two runs on the
|
|
40
|
+
same version produce identical finding and operation ids. Exit code 0.
|
|
39
41
|
|
|
40
42
|
This proves the whole pipeline (snapshot → audit → plan) without any account.
|
|
41
43
|
|
|
@@ -133,10 +135,16 @@ works from inside existing projects too.
|
|
|
133
135
|
Tools exposed over stdio — read-only: `fullstackgtm_audit`,
|
|
134
136
|
`fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`,
|
|
135
137
|
`fullstackgtm_call_parse`, `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
|
|
136
|
-
`fullstackgtm_apply` (
|
|
138
|
+
`fullstackgtm_apply` (stored `planId` only; approvals and values must already be
|
|
139
|
+
human-approved and HMAC-signed in the local plan store),
|
|
137
140
|
`fullstackgtm_market_observe` (every quoted span is verified against the
|
|
138
141
|
stored captures before anything is appended).
|
|
139
142
|
|
|
143
|
+
MCP cannot execute rule packages, apply external plan files, or supply its own
|
|
144
|
+
approvals/value overrides. For an interrupted apply, reconcile provider state,
|
|
145
|
+
then run `fullstackgtm plans recover <id> --acknowledge-uncertain-writes`; this
|
|
146
|
+
replays nothing and clears approvals so a fresh review is required.
|
|
147
|
+
|
|
140
148
|
## Troubleshooting
|
|
141
149
|
|
|
142
150
|
| Symptom | Fix |
|
|
@@ -145,3 +153,4 @@ stored captures before anything is appended).
|
|
|
145
153
|
| `No HubSpot credentials` (exit 1) | Set `HUBSPOT_ACCESS_TOKEN` or have a human run `fullstackgtm login hubspot --hosted` |
|
|
146
154
|
| MCP server prints peer-dependency help | Install `@modelcontextprotocol/sdk` and `zod` (see step 6) |
|
|
147
155
|
| Need machine state for orchestration | `fullstackgtm doctor --json` |
|
|
156
|
+
| Plan is `applying` after an interrupted request | Reconcile provider state, then run `fullstackgtm plans recover <id> --acknowledge-uncertain-writes`; review and approve again |
|
package/README.md
CHANGED
|
@@ -192,6 +192,7 @@ echo "$PIPE0_API_KEY" | fullstackgtm login pipe0 # discovery + work-e
|
|
|
192
192
|
|
|
193
193
|
fullstackgtm enrich acquire --provider hubspot # zero-config: ICP → discover → score → dedup → dry-run diff
|
|
194
194
|
fullstackgtm enrich acquire --provider hubspot --max 25 --save # cap the batch, persist the needs_approval plan
|
|
195
|
+
fullstackgtm enrich acquire --provider hubspot --max 25 --scan-limit 500 --save # scan through duplicate-heavy pages for 25 fresh leads
|
|
195
196
|
fullstackgtm plans approve <id> --operations all
|
|
196
197
|
fullstackgtm apply --plan-id <id> --provider hubspot # the only step that creates contacts
|
|
197
198
|
```
|
|
@@ -206,6 +207,46 @@ The **ICP** (`icp.json`) is the single targeting artifact: it generates each pro
|
|
|
206
207
|
|
|
207
208
|
Every run is **metered**: a per-profile budget caps both record count and provider spend (per-day + per-month, whichever binds first), charged only for creates that actually land. `enrich acquire` runs with just an `icp.json` and a `login` — sensible defaults for budget, provider, and create-mapping ship in the box.
|
|
208
209
|
|
|
210
|
+
Discovery continuation is profile-scoped and independently keyed by the exact
|
|
211
|
+
provider, source, list, and ICP query. Scheduled runs resume the saved Pipe0
|
|
212
|
+
cursor or HeyReach offset instead of rereading page one, even when several
|
|
213
|
+
lists alternate. When the CLI is paired, this non-PII checkpoint is mirrored
|
|
214
|
+
to the hosted organization with compare-and-swap so another authenticated
|
|
215
|
+
worker can resume without moving a newer cursor backwards. `--max` is the desired net-new plan size;
|
|
216
|
+
`--scan-limit` independently bounds raw candidates scanned after ICP filtering
|
|
217
|
+
and dedupe. `enrich status --runs` exposes the full funnel and whether the
|
|
218
|
+
audience is continuing or provider-confirmed exhausted.
|
|
219
|
+
|
|
220
|
+
When paired, every saved plan is also mirrored into the hosted Patch Plans
|
|
221
|
+
review queue and the CLI prints its exact deep-link. The mirror is immutable
|
|
222
|
+
and organization-scoped: replaying the same plan is idempotent, while the same
|
|
223
|
+
id with different content is rejected. Hosted reviewers can approve or reject;
|
|
224
|
+
the plan is executable from any synchronized surface with a compatible CRM
|
|
225
|
+
connection. A CLI-created plan can be applied in hosted, and a hosted approval
|
|
226
|
+
can be applied from the CLI—the plan's origin does not permanently own it.
|
|
227
|
+
|
|
228
|
+
This is local-first replication, not remote control. The local plan store works
|
|
229
|
+
offline, and plan commands perform a best-effort hosted check-in. Run
|
|
230
|
+
`fullstackgtm plans sync` to explicitly exchange every plan's latest approval,
|
|
231
|
+
execution claim, status, and receipts. If hosted applies while the CLI is
|
|
232
|
+
offline, the next check-in imports the exact per-operation results and marks
|
|
233
|
+
the local plan applied. If the CLI applies while hosted is unavailable, it
|
|
234
|
+
keeps the result locally and uploads the receipt on a later check-in.
|
|
235
|
+
|
|
236
|
+
Before either surface writes, it verifies the immutable plan hash, selected
|
|
237
|
+
operation ids, current approval revision, connector capability, and provider
|
|
238
|
+
state. Online replicas use a short-lived execution claim to coordinate; stable
|
|
239
|
+
operation ids, resolve-before-create, compare-and-set guards, and provider
|
|
240
|
+
readback protect offline retries. Receipts record each operation as `applied`,
|
|
241
|
+
`skipped`, or `failed`, plus provider record ids when available, so another
|
|
242
|
+
replica learns what happened rather than inferring it from plan status. A
|
|
243
|
+
conflicting plan body or approval revision is surfaced for review and is never
|
|
244
|
+
merged by expanding authority.
|
|
245
|
+
|
|
246
|
+
Local signing keys and HMAC digests never upload. Operation before/after values
|
|
247
|
+
do upload to the paired organization because they are required for informed
|
|
248
|
+
review and hosted execution.
|
|
249
|
+
|
|
209
250
|
**Discovery sources** are zero-config presets behind `--source`: `explorium` and `pipe0` run ICP-driven people search, while `linkedin` (Phase 1 — discovery + dry-run) reads a pre-built **HeyReach** lead list:
|
|
210
251
|
|
|
211
252
|
```bash
|
|
@@ -373,6 +414,12 @@ fullstackgtm logout hubspot # or: salesforce | broker
|
|
|
373
414
|
|
|
374
415
|
BYO direct credentials always win over a hosted broker pairing, so an operator can override the team default. First-party app secrets never ship in the npm package; hosted login stores a local broker token and mints provider tokens server-side. HubSpot does not support the device-authorization grant or secretless public clients, which is why the bring-your-own-app OAuth path requires client credentials; they are stored locally for silent refresh, the same model as `gcloud` and `aws` CLI profiles.
|
|
375
416
|
|
|
417
|
+
Salesforce credential-bearing URLs are restricted to canonical HTTPS
|
|
418
|
+
Salesforce-owned production, sandbox, and My Domain origins. Lookalike hosts,
|
|
419
|
+
userinfo, paths/query/fragment, nonstandard ports, unsafe token-response
|
|
420
|
+
instance URLs, and credential-forwarding redirects are refused both when the
|
|
421
|
+
credential is stored and every time it is used.
|
|
422
|
+
|
|
376
423
|
## Connect your CRM
|
|
377
424
|
|
|
378
425
|
What each provider actually requires before `audit --provider <name>` works on your data.
|
|
@@ -539,10 +586,15 @@ Nine tools are exposed over stdio.
|
|
|
539
586
|
|
|
540
587
|
**Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_capabilities` (server/tool capability manifest), `fullstackgtm_rules` (rule discovery), `fullstackgtm_suggest` (deterministic placeholder values with confidence + reasons), `fullstackgtm_call_parse` (transcripts → provenance-marked segments, insights, and evidence), `fullstackgtm_resolve` (the create gate: exists / ambiguous / safe_to_create), and `fullstackgtm_market_worksheet` (the classification packet for one vendor: claims, judging rules, captured page texts).
|
|
541
588
|
|
|
542
|
-
**Gated:** `fullstackgtm_apply` (
|
|
589
|
+
**Gated:** `fullstackgtm_apply` (accepts only a stored plan id; approvals and values must already be human-approved and HMAC-signed in the plan store) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
|
|
543
590
|
|
|
544
591
|
Tokens stored via `fullstackgtm login` are picked up automatically — the env var is only needed when no stored login exists.
|
|
545
592
|
|
|
593
|
+
Rule packages are executable JavaScript and are disabled by default. In the CLI,
|
|
594
|
+
review the modules and use `--config <path> --allow-plugins`; use `--no-plugins`
|
|
595
|
+
to apply only declarative config. MCP never executes rule packages: mutable
|
|
596
|
+
tool-call paths are not a durable code-trust boundary.
|
|
597
|
+
|
|
546
598
|
## Safety model
|
|
547
599
|
|
|
548
600
|
1. Reads are safe by default; audits never mutate anything.
|
package/SECURITY.md
CHANGED
|
@@ -21,7 +21,10 @@ newest version, not backported (the project is pre-1.0).
|
|
|
21
21
|
**Credentials.** API tokens are never accepted as command-line arguments
|
|
22
22
|
(they would leak into the process table and shell history); they come from an
|
|
23
23
|
environment variable or stdin only, and are stored `0600` under a `0700` home
|
|
24
|
-
(`$FSGTM_HOME`, default `~/.fullstackgtm`)
|
|
24
|
+
(`$FSGTM_HOME`, default `~/.fullstackgtm`). Writes use exclusive temporary
|
|
25
|
+
files, fsync, and atomic rename; reads require regular files opened without
|
|
26
|
+
following symlinks. Managed-home, credential, and plan-store symlink paths are
|
|
27
|
+
refused before chmod/read/write. This is the
|
|
25
28
|
same custody model as the `gcloud`/`aws` CLIs. The hosted broker
|
|
26
29
|
(`login --via`) exists so a team can connect a CRM once, server-side, and hand
|
|
27
30
|
laptops only a revocable pairing token instead of a long-lived super-admin key;
|
|
@@ -57,13 +60,34 @@ approved on one machine cannot be applied on another (the key does not travel).
|
|
|
57
60
|
Documented boundary: this defends the plan file, not an attacker who already
|
|
58
61
|
holds the signing key (same directory and permissions as the credential store).
|
|
59
62
|
|
|
63
|
+
**Single-owner apply and recovery.** Store-backed CLI and MCP writes atomically
|
|
64
|
+
claim an approved plan and journal the provider, caller surface, and attempt
|
|
65
|
+
state before provider I/O. A second process cannot apply the same approval.
|
|
66
|
+
Failures after provider I/O remain `applying`/uncertain because a timeout cannot
|
|
67
|
+
prove no remote write landed. Recovery never auto-replays: after reconciling CRM
|
|
68
|
+
state, `plans recover <id> --acknowledge-uncertain-writes` clears every approval
|
|
69
|
+
and signature and requires a fresh human review.
|
|
70
|
+
|
|
71
|
+
**Executable configuration.** `rulePackages` are arbitrary JavaScript. An
|
|
72
|
+
implicitly discovered repository config cannot activate them; CLI execution
|
|
73
|
+
requires explicit `--config <path> --allow-plugins` (`--no-plugins` disables
|
|
74
|
+
them). MCP never executes rule packages or accepts external plans/caller
|
|
75
|
+
approvals.
|
|
76
|
+
|
|
77
|
+
**Credential origins.** Salesforce access and refresh tokens are sent only to
|
|
78
|
+
canonical HTTPS Salesforce-owned origins. Userinfo, paths/query/fragment,
|
|
79
|
+
nonstandard ports, lookalike domains, unsafe token-response instance URLs, and
|
|
80
|
+
credential-preserving redirects are refused at input, storage, and use.
|
|
81
|
+
|
|
60
82
|
**Scheduling never auto-approves.** Scheduled (cron) runs are restricted to a
|
|
61
83
|
read/plan-side allowlist plus `apply --plan-id` whose approved status and
|
|
62
84
|
signatures are re-checked at every firing. Arbitrary shell is not schedulable.
|
|
63
85
|
|
|
64
86
|
**Untrusted input.** Competitor pages fetched by `market capture` are guarded
|
|
65
|
-
against SSRF
|
|
66
|
-
|
|
87
|
+
against SSRF: every DNS answer must be globally routable, validated answers are
|
|
88
|
+
pinned into socket creation, mixed/private/reserved answers are refused, every
|
|
89
|
+
redirect is revalidated, sensitive headers are stripped cross-origin, and
|
|
90
|
+
time/body/redirect limits are enforced. LLM-extracted call insights and market
|
|
67
91
|
classifications are mechanically verified verbatim against the source text
|
|
68
92
|
before they can drive a writeback, so a prompt-injected transcript or page
|
|
69
93
|
cannot fabricate a grounded-looking change. CSV/formula-injection in ingested
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The complete identity of one independently traversable provider audience. */
|
|
2
|
+
export type AcquireCheckpointKey = {
|
|
3
|
+
provider: string;
|
|
4
|
+
source: string;
|
|
5
|
+
listId: string | null;
|
|
6
|
+
queryFingerprint: string;
|
|
7
|
+
};
|
|
8
|
+
export type AcquireContinuation = {
|
|
9
|
+
cursor?: string | null;
|
|
10
|
+
offset?: number | null;
|
|
11
|
+
exhausted: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** Versioned wire format shared by local persistence and hosted sync. */
|
|
14
|
+
export type AcquireCheckpoint = {
|
|
15
|
+
version: 1;
|
|
16
|
+
id: string;
|
|
17
|
+
key: AcquireCheckpointKey;
|
|
18
|
+
continuation: AcquireContinuation;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function validateAcquireCheckpointKey(value: unknown): AcquireCheckpointKey;
|
|
22
|
+
/** Stable, non-secret filename/key suitable for both filesystem and hosted records. */
|
|
23
|
+
export declare function acquireCheckpointId(key: AcquireCheckpointKey): string;
|
|
24
|
+
export declare function validateAcquireCheckpoint(value: unknown): AcquireCheckpoint;
|
|
25
|
+
export declare function acquireCheckpointsDir(baseDir?: string): string;
|
|
26
|
+
export interface AcquireCheckpointStore {
|
|
27
|
+
get(key: AcquireCheckpointKey): Promise<AcquireCheckpoint | null>;
|
|
28
|
+
put(key: AcquireCheckpointKey, continuation: AcquireContinuation, updatedAt?: Date): Promise<AcquireCheckpoint>;
|
|
29
|
+
/** Import a validated local/hosted record without changing its timestamp. */
|
|
30
|
+
putRecord(record: AcquireCheckpoint): Promise<AcquireCheckpoint>;
|
|
31
|
+
list(): Promise<AcquireCheckpoint[]>;
|
|
32
|
+
}
|
|
33
|
+
export declare function createFileAcquireCheckpointStore(baseDir?: string): AcquireCheckpointStore;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { credentialsDir, ensureSecureHomeDir } from "./credentials.js";
|
|
5
|
+
import { assertNoSymlinkComponents, readSecureRegularFile, writeSecureFileAtomic, } from "./secureFile.js";
|
|
6
|
+
const COMPONENT_MAX = 512;
|
|
7
|
+
function requireComponent(value, name) {
|
|
8
|
+
if (typeof value !== "string" || value.length === 0 || value.length > COMPONENT_MAX || /[\u0000-\u001f]/.test(value)) {
|
|
9
|
+
throw new Error(`Invalid acquisition checkpoint ${name}`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function validateAcquireCheckpointKey(value) {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15
|
+
throw new Error("Invalid acquisition checkpoint key");
|
|
16
|
+
}
|
|
17
|
+
const candidate = value;
|
|
18
|
+
const listId = candidate.listId;
|
|
19
|
+
if (listId !== null && listId !== undefined)
|
|
20
|
+
requireComponent(listId, "listId");
|
|
21
|
+
return {
|
|
22
|
+
provider: requireComponent(candidate.provider, "provider"),
|
|
23
|
+
source: requireComponent(candidate.source, "source"),
|
|
24
|
+
listId: listId ?? null,
|
|
25
|
+
queryFingerprint: requireComponent(candidate.queryFingerprint, "queryFingerprint"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Stable, non-secret filename/key suitable for both filesystem and hosted records. */
|
|
29
|
+
export function acquireCheckpointId(key) {
|
|
30
|
+
const valid = validateAcquireCheckpointKey(key);
|
|
31
|
+
const canonical = JSON.stringify([valid.provider, valid.source, valid.listId, valid.queryFingerprint]);
|
|
32
|
+
return `acqcp_${createHash("sha256").update(canonical).digest("hex")}`;
|
|
33
|
+
}
|
|
34
|
+
function validateContinuation(value) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
36
|
+
throw new Error("Invalid acquisition checkpoint continuation");
|
|
37
|
+
}
|
|
38
|
+
const candidate = value;
|
|
39
|
+
if (typeof candidate.exhausted !== "boolean")
|
|
40
|
+
throw new Error("Invalid acquisition checkpoint exhausted flag");
|
|
41
|
+
if (candidate.cursor !== undefined && candidate.cursor !== null)
|
|
42
|
+
requireComponent(candidate.cursor, "cursor");
|
|
43
|
+
if (candidate.offset !== undefined && candidate.offset !== null &&
|
|
44
|
+
(!Number.isSafeInteger(candidate.offset) || candidate.offset < 0)) {
|
|
45
|
+
throw new Error("Invalid acquisition checkpoint offset");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...(candidate.cursor !== undefined ? { cursor: candidate.cursor } : {}),
|
|
49
|
+
...(candidate.offset !== undefined ? { offset: candidate.offset } : {}),
|
|
50
|
+
exhausted: candidate.exhausted,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function validateAcquireCheckpoint(value) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
throw new Error("Invalid acquisition checkpoint");
|
|
56
|
+
}
|
|
57
|
+
const candidate = value;
|
|
58
|
+
if (candidate.version !== 1)
|
|
59
|
+
throw new Error(`Unsupported acquisition checkpoint version: ${String(candidate.version)}`);
|
|
60
|
+
const key = validateAcquireCheckpointKey(candidate.key);
|
|
61
|
+
const id = acquireCheckpointId(key);
|
|
62
|
+
if (candidate.id !== id)
|
|
63
|
+
throw new Error("Acquisition checkpoint id does not match its key");
|
|
64
|
+
if (typeof candidate.updatedAt !== "string" || !Number.isFinite(Date.parse(candidate.updatedAt))) {
|
|
65
|
+
throw new Error("Invalid acquisition checkpoint updatedAt");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
id,
|
|
70
|
+
key,
|
|
71
|
+
continuation: validateContinuation(candidate.continuation),
|
|
72
|
+
updatedAt: candidate.updatedAt,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function acquireCheckpointsDir(baseDir) {
|
|
76
|
+
return join(baseDir ?? credentialsDir(), "acquire", "checkpoints");
|
|
77
|
+
}
|
|
78
|
+
export function createFileAcquireCheckpointStore(baseDir) {
|
|
79
|
+
const directory = acquireCheckpointsDir(baseDir);
|
|
80
|
+
function fileForId(id) {
|
|
81
|
+
return join(directory, `${id}.json`);
|
|
82
|
+
}
|
|
83
|
+
function readId(id) {
|
|
84
|
+
try {
|
|
85
|
+
return validateAcquireCheckpoint(JSON.parse(readSecureRegularFile(fileForId(id), { tightenMode: 0o600 })));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === "ENOENT")
|
|
89
|
+
return null;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function ensureDirectory() {
|
|
94
|
+
if (baseDir === undefined)
|
|
95
|
+
ensureSecureHomeDir();
|
|
96
|
+
// Tighten both managed levels. mkdir's mode does not affect a directory
|
|
97
|
+
// restored or pre-created with broader permissions.
|
|
98
|
+
const levels = [join(baseDir ?? credentialsDir(), "acquire"), directory];
|
|
99
|
+
for (const level of levels) {
|
|
100
|
+
assertNoSymlinkComponents(level);
|
|
101
|
+
mkdirSync(level, { recursive: true, mode: 0o700 });
|
|
102
|
+
assertNoSymlinkComponents(level);
|
|
103
|
+
try {
|
|
104
|
+
chmodSync(level, 0o700);
|
|
105
|
+
}
|
|
106
|
+
catch { /* chmod is unavailable on some filesystems. */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function writeRecord(value) {
|
|
110
|
+
const record = validateAcquireCheckpoint(value);
|
|
111
|
+
ensureDirectory();
|
|
112
|
+
writeSecureFileAtomic(fileForId(record.id), `${JSON.stringify(record, null, 2)}\n`);
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
async get(key) {
|
|
117
|
+
return readId(acquireCheckpointId(key));
|
|
118
|
+
},
|
|
119
|
+
async put(key, continuation, updatedAt = new Date()) {
|
|
120
|
+
const validKey = validateAcquireCheckpointKey(key);
|
|
121
|
+
return writeRecord({
|
|
122
|
+
version: 1,
|
|
123
|
+
id: acquireCheckpointId(validKey),
|
|
124
|
+
key: validKey,
|
|
125
|
+
continuation: validateContinuation(continuation),
|
|
126
|
+
updatedAt: updatedAt.toISOString(),
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
async putRecord(record) {
|
|
130
|
+
return writeRecord(record);
|
|
131
|
+
},
|
|
132
|
+
async list() {
|
|
133
|
+
let names;
|
|
134
|
+
try {
|
|
135
|
+
assertNoSymlinkComponents(directory);
|
|
136
|
+
names = readdirSync(directory).filter((name) => /^acqcp_[a-f0-9]{64}\.json$/.test(name));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.code === "ENOENT")
|
|
140
|
+
return [];
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return names
|
|
144
|
+
.map((name) => readId(name.slice(0, -5)))
|
|
145
|
+
.filter((record) => record !== null)
|
|
146
|
+
.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -25,6 +25,8 @@ export type DiscoverLinkedInOptions = {
|
|
|
25
25
|
sourceId?: string;
|
|
26
26
|
/** Hard cap on prospects pulled. */
|
|
27
27
|
max?: number;
|
|
28
|
+
/** Opaque provider continuation cursor. */
|
|
29
|
+
cursor?: string;
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* Discovery branch for `acquireFromApi`: read prospects from a LinkedIn source
|
package/dist/acquireLinkedIn.js
CHANGED
|
@@ -41,7 +41,7 @@ export function linkedInProspectToProspect(lp) {
|
|
|
41
41
|
* provider is injected so this is fully testable against the fake (no key).
|
|
42
42
|
*/
|
|
43
43
|
export async function discoverLinkedInProspects(provider, options = {}) {
|
|
44
|
-
const raw = await provider.fetchProspects({ sourceId: options.sourceId, max: options.max });
|
|
44
|
+
const raw = await provider.fetchProspects({ sourceId: options.sourceId, max: options.max, cursor: options.cursor });
|
|
45
45
|
return raw.map(linkedInProspectToProspect);
|
|
46
46
|
}
|
|
47
47
|
/**
|
package/dist/cli/audit.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { auditSnapshot, defaultPolicy } from "../audit.js";
|
|
5
|
-
import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
|
|
5
|
+
import { loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli } from "../config.js";
|
|
6
6
|
import { getCredential, resolveHubspotConnection } from "../credentials.js";
|
|
7
7
|
import { patchPlanToMarkdown } from "../format.js";
|
|
8
8
|
import { appendHealthEntry, computeHealth, healthToMarkdown, readHealthTimeline, saveWorkspaceSnapshot, summarizeHealth, activeWorkspaceProfile } from "../health.js";
|
|
@@ -169,8 +169,9 @@ async function registerHubspotWithBroker() {
|
|
|
169
169
|
}
|
|
170
170
|
export async function audit(args) {
|
|
171
171
|
const threshold = failOnThreshold(args);
|
|
172
|
-
const
|
|
173
|
-
const
|
|
172
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
173
|
+
const loaded = loadConfig(explicitConfig);
|
|
174
|
+
const rules = selectedRules(args, await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig)));
|
|
174
175
|
const snapshot = await readSnapshot(args);
|
|
175
176
|
const policy = mergePolicy(defaultPolicy(), loaded?.config);
|
|
176
177
|
const today = option(args, "--today");
|
|
@@ -326,8 +327,9 @@ function shortDay(at) {
|
|
|
326
327
|
* re-fetching (useful for a plan produced earlier or by another machine).
|
|
327
328
|
*/
|
|
328
329
|
export async function reportCommand(args) {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
330
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
331
|
+
const loaded = loadConfig(explicitConfig);
|
|
332
|
+
const configuredRules = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
|
|
331
333
|
let plan;
|
|
332
334
|
let snapshot;
|
|
333
335
|
const planPath = option(args, "--plan");
|
|
@@ -375,8 +377,9 @@ export async function reportCommand(args) {
|
|
|
375
377
|
}
|
|
376
378
|
}
|
|
377
379
|
export async function rulesCommand(args) {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
381
|
+
const loaded = loadConfig(explicitConfig);
|
|
382
|
+
const rules = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
|
|
380
383
|
if (args.includes("--json")) {
|
|
381
384
|
console.log(JSON.stringify(rules.map(({ id, title, description, category }) => ({
|
|
382
385
|
id,
|
package/dist/cli/auth.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { DEFAULT_LOOPBACK_PORT, openInBrowser, runHubspotLoopbackLogin, validateHubspotToken } from "../connectors/hubspotAuth.js";
|
|
5
|
-
import { pollSalesforceDeviceLogin, startSalesforceDeviceLogin, validateSalesforceToken } from "../connectors/salesforceAuth.js";
|
|
5
|
+
import { pollSalesforceDeviceLogin, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken } from "../connectors/salesforceAuth.js";
|
|
6
6
|
import { activeProfile, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, storeCredential } from "../credentials.js";
|
|
7
7
|
import { activeWorkspaceProfile, readHealthTimeline, summarizeHealth } from "../health.js";
|
|
8
8
|
import { resolveLlmCredential, validateLlmKey } from "../llm.js";
|
|
@@ -229,7 +229,10 @@ async function salesforceLogin(args) {
|
|
|
229
229
|
await guidedProviderLogin("salesforce", args, "--device requires --client-id (the consumer key of a Connected App with device flow enabled).");
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
|
-
const
|
|
232
|
+
const loginUrlInput = option(args, "--login-url") ?? undefined;
|
|
233
|
+
const loginUrl = loginUrlInput
|
|
234
|
+
? validateSalesforceOrigin(loginUrlInput, "Salesforce --login-url")
|
|
235
|
+
: undefined;
|
|
233
236
|
const authorization = await startSalesforceDeviceLogin({ clientId, loginUrl });
|
|
234
237
|
console.error(`\nPairing code: ${authorization.userCode}\n\nConfirm this code at:\n\n ${authorization.verificationUri}\n`);
|
|
235
238
|
void openInBrowser(authorization.verificationUri);
|
|
@@ -254,11 +257,12 @@ async function salesforceLogin(args) {
|
|
|
254
257
|
console.log("Tokens refresh silently; no further browser interaction is needed.");
|
|
255
258
|
return;
|
|
256
259
|
}
|
|
257
|
-
const
|
|
258
|
-
if (!
|
|
260
|
+
const instanceUrlInput = option(args, "--instance-url");
|
|
261
|
+
if (!instanceUrlInput) {
|
|
259
262
|
await guidedProviderLogin("salesforce", args, "Salesforce login needs hosted OAuth, --device --client-id <consumer key>, or --instance-url <https://yourorg.my.salesforce.com> with the access token piped on stdin.");
|
|
260
263
|
return;
|
|
261
264
|
}
|
|
265
|
+
const instanceUrl = validateSalesforceOrigin(instanceUrlInput, "Salesforce --instance-url");
|
|
262
266
|
const token = await readSecret("Salesforce access token");
|
|
263
267
|
if (!token)
|
|
264
268
|
throw new Error("No access token provided.");
|