|
| 1 | +# Gatekeeper Salesforce |
| 2 | + |
| 3 | +Semantic search over your Salesforce org, available to Cloudflare OS agents as an ambient |
| 4 | +`SALESFORCE` binding — no connection or OAuth flow required. |
| 5 | + |
| 6 | +This auto-provisioned gatekeeper mirrors read-only Salesforce data (Accounts, Contacts, Campaigns, |
| 7 | +Opportunities, Tasks, Events, Leads, and Touchless custom objects) into Cloudflare **Vectorize** + |
| 8 | +**D1**, embedding each record with Workers AI `@cf/qwen/qwen3-embedding-0.6b` (1024-dim). Agents |
| 9 | +can then ask natural-language questions like "find Toyota dealers in Wisconsin with stalled demo |
| 10 | +activity" and get semantically-ranked records back — no SOQL needed, in <500 ms. |
| 11 | + |
| 12 | +## How it works |
| 13 | + |
| 14 | +``` |
| 15 | +Salesforce (REST /query, JWT) ──► SalesforceSyncWorkflow (Cron + resync RPC) |
| 16 | + │ |
| 17 | + ├─ chunk/embed (@cf/qwen/qwen3-embedding-0.6b) |
| 18 | + ├─ D1 records (full JSON content, sync cursors) |
| 19 | + └─ Vectorize (1024-dim cosine, indexed metadata) |
| 20 | + │ |
| 21 | +Agent ──► SalesforceIndex.search(query, opts) ──► Vectorize query → D1 fetch → observation |
| 22 | +``` |
| 23 | + |
| 24 | +- **Sync**: a durable Cloudflare Workflow pulls records modified since the last |
| 25 | + composite `SystemModstamp|Id` cursor (see `migrations/0001_init.sql`), every 2 hours |
| 26 | + (configurable). It only re-embeds records whose `search_text` actually changed, skips unchanged |
| 27 | + content, and reconciles deletions from Salesforce **getDeleted** (recycle-bin window) on every |
| 28 | + incremental run — including empty deltas. Full syncs may optionally reconcile against a complete |
| 29 | + ID inventory; incremental deltas are never treated as a full inventory. Removals delete from |
| 30 | + **D1 and Vectorize together**. |
| 31 | +- **Read path**: `search()` embeds the query, queries Vectorize (optionally filtered by |
| 32 | + objectType/owner/campaign/status), then hydrates the snippets from D1. Every read is authorized |
| 33 | + as an observation; reads never touch the approval queue. |
| 34 | + |
| 35 | +## Agent API |
| 36 | + |
| 37 | +The ambient binding (`SALESFORCE`, ts type `SalesforceIndex`) exposes: |
| 38 | + |
| 39 | +```ts |
| 40 | +interface SalesforceIndex { |
| 41 | + search(query: string, opts?: { |
| 42 | + objectType?: string; // "Account" | "Contact" | "Campaign" | ... |
| 43 | + ownerId?: string; // filter to a Salesforce user Id |
| 44 | + campaignId?: string; |
| 45 | + status?: string; |
| 46 | + limit?: number; // default 20, max 50 |
| 47 | + }): Promise<SalesforceSearchResult[]>; |
| 48 | + |
| 49 | + getRecord(id: string): Promise<SalesforceRecord | null>; |
| 50 | + listObjects(): Promise<SalesforceObjectInfo[]>; |
| 51 | + resync(opts?: { objectType?: string }): Promise<{ started: boolean; objectType?: string }>; |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +See `src/salesforce-gatekeeper.ts` (`SALESFORCE_TYPES`) for the exact agent-facing declarations. |
| 56 | + |
| 57 | +## Configuration |
| 58 | + |
| 59 | +### 1. Salesforce connected app (JWT) |
| 60 | + |
| 61 | +Reuse (or create) an External Client App in your Salesforce org for the JWT bearer flow: |
| 62 | + |
| 63 | +1. In Salesforce Setup, create an External Client App. |
| 64 | +2. Enable OAuth, add the JWT bearer flow, and register the integration user. |
| 65 | +3. Upload the **certificate** whose private key you'll configure as the Worker secret. |
| 66 | +4. Allowlist the integration user (e.g. `remi@touchless.io`) on the app. |
| 67 | + |
| 68 | +### 2. Worker secrets |
| 69 | + |
| 70 | +Set these on the deployed `gatekeeper-salesforce` Worker (in the starter, also via |
| 71 | +`packages/gatekeeper-salesforce/deploy-inputs.json`): |
| 72 | + |
| 73 | +| Secret | Value | |
| 74 | +| -------------------- | ------------------------------------------------------------------ | |
| 75 | +| `SF_CLIENT_ID` | The External Client App consumer key | |
| 76 | +| `SF_USERNAME` | The integration user, e.g. `remi@touchless.io` | |
| 77 | +| `SF_PRIVATE_KEY` | The RSA **PKCS#8** PEM private key (always stored in the vault) | |
| 78 | +| `SF_LOGIN_URL` (opt) | Defaults to `https://login.salesforce.com`; use `https://test.salesforce.com` for a sandbox | |
| 79 | + |
| 80 | +### 3. Cloudflare resources |
| 81 | + |
| 82 | +```sh |
| 83 | +# Vectorize index (1024 dims to match the embedding model) |
| 84 | +npx wrangler vectorize create salesforce-index --dimensions=1024 --metric=cosine |
| 85 | + |
| 86 | +# Enable metadata filtering on the fields used by search() options |
| 87 | +npx wrangler vectorize create-metadata-index salesforce-index --property-name='objectType' --type='string' |
| 88 | +npx wrangler vectorize create-metadata-index salesforce-index --property-name='ownerId' --type='string' |
| 89 | +npx wrangler vectorize create-metadata-index salesforce-index --property-name='campaignId'--type='string' |
| 90 | +npx wrangler vectorize create-metadata-index salesforce-index --property-name='status' --type='string' |
| 91 | + |
| 92 | +# D1 database + schema |
| 93 | +npx wrangler d1 create salesforce-vector-store |
| 94 | +npx wrangler d1 migrations apply salesforce-vector-store --remote |
| 95 | +``` |
| 96 | + |
| 97 | +Then point `wrangler.jsonc` `d1_databases[0].database_id` and `vectorize[0].index_name` at the |
| 98 | +created resources (the committed file uses `$D1_SF_DB_ID` / `$VECTORIZE_SF_INDEX_NAME` |
| 99 | +placeholders that the release manifest / deploy service resolves). |
| 100 | + |
| 101 | +### 4. Deploy + first sync |
| 102 | + |
| 103 | +```sh |
| 104 | +cd packages/gatekeeper-salesforce |
| 105 | +npx wrangler deploy # (secrets must be set first) |
| 106 | +npx wrangler workflows trigger sf-sync '{}' # full initial load |
| 107 | +``` |
| 108 | + |
| 109 | +The initial sync of ~100K records costs roughly **$0.30** in Workers AI embedding (the whole org |
| 110 | +fits in a single Vectorize index well below the 20M-vector limit) and completes in ~30–60 minutes |
| 111 | +thanks to Workflows' durable steps. |
| 112 | + |
| 113 | +## Object coverage |
| 114 | + |
| 115 | +The default registry lives in `src/sf-objects.ts` (`OBJECT_TYPE_CONFIGS`). It covers: |
| 116 | + |
| 117 | +Account, Contact, Campaign, CampaignMember, CampaignMemberStatus, Opportunity, Task, Event, Lead, |
| 118 | +`Campaign_Account__c`, `Cadence_Step_Snapshot__c`, `Outreach_Scorecard_Entry__c`. |
| 119 | + |
| 120 | +To add an object, insert an `object_config` row (SOQL fields + embedding fields) or extend the |
| 121 | +registry and re-run the seed step — no code change is needed for standard dosing. |
| 122 | + |
| 123 | +## Development |
| 124 | + |
| 125 | +```sh |
| 126 | +pnpm --filter @gadgets/gatekeeper-salesforce... install |
| 127 | +pnpm --filter @gadgets/gatekeeper-salesforce test # unit + D1 integration tests |
| 128 | +pnpm --filter @gadgets/gatekeeper-salesforce build # typecheck |
| 129 | +``` |
| 130 | + |
| 131 | +The dev server (`pnpm dev-server`) auto-discovers the gatekeeper and binds `GATEKEEPER_SALESFORCE`. |
| 132 | +For local dev, `wrangler.jsonc` uses `preview_database_id: "salesforce-vector-store"` and the same |
| 133 | +index name, so `wrangler dev` works against local D1 + Vectorize once you create them. |
| 134 | + |
| 135 | +## Security notes |
| 136 | + |
| 137 | +- Read-only: no Salesforce writes, no approval-queue actions. All reads are observations. |
| 138 | +- PII is kept inside Cloudflare (D1 + Vectorize) exactly as indexed; the sync never logs record |
| 139 | + bodies or the private key. Secrets live only in Worker secrets / the vault. |
| 140 | +- Metadata index values are capped at 64 bytes to stay within Vectorize limits. |
| 141 | + |
| 142 | +## Releasing |
| 143 | + |
| 144 | +Adding this package touched the release manifest: |
| 145 | + |
| 146 | +- `scripts/release/manifest-lib.mjs` now understands `d1_databases`, `vectorize`, `ai`, and |
| 147 | + `workflows` config keys, emitting `$D1_<BINDING>_ID`, `$VECTORIZE_<BINDING>_NAME`, `ai`, and |
| 148 | + `workflow` binding templates. `gatekeeper-salesforce` is in `NO_DEFAULT_CRED_INPUTS` (it takes |
| 149 | + `SF_*` secrets via `deploy-inputs.json`). |
| 150 | +- `scripts/testdata/golden-manifest.json` was regenerated with the new package entry. |
0 commit comments