@drawbridge/drawbridge-agents 0.1.50 → 0.1.55
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.
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: drawbridge-add-connection
|
|
3
|
-
description: Use when adding a
|
|
3
|
+
description: Use when adding a vendor connection (a new manifest in drawbridge-utils), changing one's auth type or scopes, or adding a hook, link, or step to an existing manifest — and when asked to check an existing connection against the connection checklist.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add a connection
|
|
7
7
|
|
|
8
|
-
A connection is one manifest
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from whichever auth mechanism is easiest to wire.
|
|
8
|
+
A connection is one manifest in `drawbridge-utils/lib/connections/providers/`.
|
|
9
|
+
The manifest is the only place vendor facts live, and it carries its own
|
|
10
|
+
proof: a `review` key the contract test refuses to accept incomplete. This
|
|
11
|
+
skill is the order of work that fills it honestly.
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## When to use
|
|
21
|
-
|
|
22
|
-
Any new vendor manifest, and any auth-type change to an existing one (keys →
|
|
23
|
-
OAuth is a rebuild of the auth surface, not a tweak).
|
|
13
|
+
**A manifest ships complete: every hook the vendor can serve, every link the
|
|
14
|
+
dashboard exposes, every scope those need, copy humanized, walked on dev.
|
|
15
|
+
Nothing is deferred for a deadline. Darren, 2026-09-11: "Don't skip shit, do
|
|
16
|
+
this right, our production launch deadline is somewhat flexible." If the work
|
|
17
|
+
does not fit the time, the time moves.**
|
|
24
18
|
|
|
25
19
|
## Steps
|
|
26
20
|
|
|
27
|
-
1. **
|
|
28
|
-
|
|
29
|
-
url was once written from memory, pointed at their API host, and stalled
|
|
21
|
+
1. **Read the vendor twice, and write down where.** Fetch, never recall — a
|
|
22
|
+
consent url once written from memory pointed at the api host and stalled
|
|
30
23
|
every merchant on a blank page.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
- **API reference**: auth mechanisms and who the vendor says each is for
|
|
25
|
+
(your own account vs accounts outside your organization — Drawbridge is
|
|
26
|
+
always the second), partner or public-app program, OAuth urls and
|
|
27
|
+
transport, token lifetime, refresh, every endpoint a hook will call.
|
|
28
|
+
- **Dashboard docs** (help centre, app UI): the page url for each object
|
|
29
|
+
we will hold an id for (list, segment, tag, contact, order), where the
|
|
30
|
+
vendor says the id appears, what the vendor calls each thing, and what a
|
|
31
|
+
disconnect actually does on their side. This read fills `urls.*` and the
|
|
32
|
+
merchant copy. A vendor whose UI urls are not documented gets an
|
|
33
|
+
observed form, marked observed, confirmed on dev before promote.
|
|
34
|
+
`review.api` and `review.dashboard` take the urls read.
|
|
35
|
+
|
|
36
|
+
2. **Scope for the whole manifest, not today's hooks.** List every scope the
|
|
37
|
+
vendor offers with its url in `review.scopes`, then request every scope any
|
|
38
|
+
hook in the contract could need for this vendor — contacts, segments,
|
|
39
|
+
tags, custom fields, lists, webhooks — whether or not that hook ships
|
|
40
|
+
now. Scopes are app-level at Klaviyo and Attentive: adding one later costs
|
|
41
|
+
every connected merchant a re-consent. "Minimum for what I built today" is
|
|
42
|
+
the wrong minimum.
|
|
43
|
+
|
|
44
|
+
3. **Pick `auth.type` from the vendor's own framing:**
|
|
43
45
|
|
|
44
46
|
| vendor's shape | type |
|
|
45
47
|
|---|---|
|
|
@@ -48,32 +50,53 @@ OAuth is a rebuild of the auth surface, not a tweak).
|
|
|
48
50
|
| the vendor offers ONLY per-account credentials | `keys` |
|
|
49
51
|
| no third party behind it at all | `generated` |
|
|
50
52
|
|
|
51
|
-
`keys`
|
|
52
|
-
|
|
53
|
-
the manifest
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
does
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
`keys` is only for a vendor that offers nothing else to third parties.
|
|
54
|
+
Where OAuth exists, build OAuth; a registration that has not landed ships
|
|
55
|
+
the manifest dormant (`requires` names the client vars), never as keys.
|
|
56
|
+
|
|
57
|
+
4. **Implement every hook the vendor can serve.** The only `false` is "the
|
|
58
|
+
vendor has no endpoint for this", with the one-line reason and the page
|
|
59
|
+
that shows the absence. Time is never a reason. "Flip later" is how segment
|
|
60
|
+
labels shipped as tags with no id kept. Every `urls` entry the dashboard
|
|
61
|
+
read found is implemented too.
|
|
62
|
+
|
|
63
|
+
5. **Write the merchant copy from the two reads, then run the humanizer.**
|
|
64
|
+
Excerpt, description, guide, confirm, error copy: the vendor's own names
|
|
65
|
+
for things, the guide's navigation path from the help centre, and what
|
|
66
|
+
disconnect really does. Then invoke `humanizer:humanizer` on the copy
|
|
67
|
+
before commit and put the date in `review.content`. Not "if time".
|
|
68
|
+
|
|
69
|
+
6. **Build against the checklist** in
|
|
70
|
+
`drawbridge-docs/reference/connection-hooks.md` → "Adding a connection".
|
|
71
|
+
Its guards are the enforcement: directory registration, feature grant,
|
|
72
|
+
`build()`, the `review` contract test. Trust a named failure over your
|
|
70
73
|
memory of the steps.
|
|
71
74
|
|
|
75
|
+
7. **Walk it on dev, then date `review.verified`.** Connect a real account,
|
|
76
|
+
pick the resource, sync one contact and one opted-out contact, open each
|
|
77
|
+
`urls.*` link. `verified` is `null` on the publish that first carries the
|
|
78
|
+
manifest (the walk needs it deployed) and dated on the next; a manifest
|
|
79
|
+
still `null` at promote does not promote. A green suite is not the walk.
|
|
80
|
+
|
|
81
|
+
## Checking an existing connection
|
|
82
|
+
|
|
83
|
+
Same steps, read as questions: is each `review` entry a real url or date, do
|
|
84
|
+
the requested scopes cover every hook the contract lists, does each `urls.*`
|
|
85
|
+
entry exist and open, was the copy humanized. A gap is a manifest fix, not a
|
|
86
|
+
note.
|
|
87
|
+
|
|
88
|
+
## Red flags — stop
|
|
89
|
+
|
|
90
|
+
- "Minimum scopes for now" — re-consent for every merchant later.
|
|
91
|
+
- "Help centre only for the guide" — `urls.*` will be empty.
|
|
92
|
+
- "Humanizer if time" — it is a step with a date, not a polish.
|
|
93
|
+
- "Verified in the header comment" — `review.verified` is the record.
|
|
94
|
+
- "Flip that hook later", "skipped because of the deadline" — nothing is
|
|
95
|
+
skipped. Say the time is short and keep going.
|
|
96
|
+
- "Keys as an interim, OAuth after the demo" — the OAuth build is the work.
|
|
97
|
+
|
|
72
98
|
## Notes
|
|
73
99
|
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
endpoint filters by name server-side, the field must NOT declare
|
|
78
|
-
`search : false` — Attentive was the first vendor that could, and a test pins
|
|
79
|
-
that it stays searchable.
|
|
100
|
+
- A fact that must hold across repos goes through `drawbridge-record-contract`.
|
|
101
|
+
- If the vendor's list endpoint filters by name server-side, the field must
|
|
102
|
+
NOT declare `search : false`; a test pins that for Attentive.
|
|
@@ -7,7 +7,7 @@ skill) so it stops living only in tribal memory.
|
|
|
7
7
|
|
|
8
8
|
## Shared secrets must match across services
|
|
9
9
|
|
|
10
|
-
- `
|
|
10
|
+
- `HMAC_OAUTH_TOKEN_KEY` must be identical in **drawbridge-api** and **drawbridge-sync** — both
|
|
11
11
|
`assertEnv` it at boot. A mismatch silently breaks realtime token verification.
|
|
12
12
|
- `HMAC_UNSUBSCRIBE_TOKEN_KEY` must be identical in **drawbridge-api** and **drawbridge-sync** —
|
|
13
13
|
both `assertEnv` it at boot. Sync signs the unsubscribe tokens it builds into notification
|
|
@@ -68,13 +68,27 @@ skill) so it stops living only in tribal memory.
|
|
|
68
68
|
`lib/suppression.js canSend()` — a new lead-facing send path must call it, or opted-out
|
|
69
69
|
recipients get contacted (TCPA/CAN-SPAM exposure). Lead-facing = the notification carries
|
|
70
70
|
`lead`/`workflow`/`connection`; those sends also REQUIRE an org connection (no system-sender
|
|
71
|
-
fallback), while
|
|
72
|
-
|
|
71
|
+
fallback), while system mail (OTC codes, member invites, security notices) keeps the system
|
|
72
|
+
sender and is never suppressed. **System mail is identified by `audience`, not by whether an
|
|
73
|
+
organization is present** — `queue/identity.js` sends member mail WITH an org, and
|
|
74
|
+
`stream/transaction.js` sends AI-balance notices with none. Inbound STOP flows webhooks
|
|
73
75
|
(`POST /connection/:id/twilio`, signature-verified against the URL built from
|
|
74
76
|
`APP_CLIENT_WEBHOOKS_URI`) → `buffer` doc → sync drain → platform-global suppression row;
|
|
75
77
|
api registers the number's SmsUrl at twilio connect (`lib/twilio.js`) and `scripts.js
|
|
76
78
|
registerTwilioWebhooks` backfills — a deployed webhooks route with unregistered numbers hears
|
|
77
79
|
nothing, silently.
|
|
80
|
+
- **`audience` picks the FROM-address; `intent` picks the opt-out machinery** (utils ↔ sync ↔
|
|
81
|
+
api). `resolveEmailSender` (sync `queue/notification.js`) resolves verified `identity` row →
|
|
82
|
+
`subAddress( leadSender, organization.shortId )` for lead-facing mail → `accountSender` for
|
|
83
|
+
member and platform mail. The tag is `shortId`, never `subdomain` — a rename and a custom-plan
|
|
84
|
+
downgrade both rewrite subdomain, and the from-address is frozen into delivered mail; api's
|
|
85
|
+
`identityDefaults.from` must tag with the SAME field or the settings screen shows a merchant an
|
|
86
|
+
address their leads never see. Drop the `leadFacing` branch and the fallback is `leadSender || accountSender`
|
|
87
|
+
for everything, so the moment a lead address is configured, Drawbridge's own mail leaves from
|
|
88
|
+
the merchant-facing lead domain — silently, with nothing raised. `subAddress` lives in utils
|
|
89
|
+
`lib/email.js` and is the inverse of `toCanonicalEmail`, which suppression uses to strip the
|
|
90
|
+
same tag; break the round-trip and consent lookups stop matching. api mirrors ONLY the
|
|
91
|
+
lead-facing branch (`identityDefaults.from`). Detail: drawbridge-docs `reference/providers.md`.
|
|
78
92
|
- **Adding an email/SMS vendor is a two-repo change** (api ↔ sync). A connection is deliverable
|
|
79
93
|
only if sync implements a sender for its slug — `lib/email.js` (`{ mailchimp, sendgrid }`) and
|
|
80
94
|
`lib/sms.js` (`{ twilio }`), which `queue/notification.js` reads **directly** as its guard and
|
|
@@ -108,6 +122,14 @@ skill) so it stops living only in tribal memory.
|
|
|
108
122
|
missing until 2026-08-13 (Shopify renames took 5+ min to reach share pages). Product scope is
|
|
109
123
|
special: the doc carries no campaign/org refs, so the worker resolves featuring campaigns via
|
|
110
124
|
`campaign.products[]`, per-page org (canonical product docs can span orgs).
|
|
125
|
+
Since 2026-09-09 share's HTML cache also stores a **404 render** under the same html key,
|
|
126
|
+
for the soft TTL only and never background-revalidated: every miss leaves share's single
|
|
127
|
+
server IP and lands in the api's global per-IP limiter, so a scanner walking a subdomain
|
|
128
|
+
(Sentry DRAWBRIDGE-SHARE-FQ) or a burst on an unpublished slug used to cost an api call per
|
|
129
|
+
request. The seam therefore MUST bust on page **insert** as well as update/delete (sync
|
|
130
|
+
`stream/page.js`), or a page created already published answers "not found" until the entry
|
|
131
|
+
expires. A slug can never carry a dot (`slugify` strips them), which is what lets share's
|
|
132
|
+
proxy refuse extension paths before they reach the api at all.
|
|
111
133
|
- Scraped-asset file reuse keys on `organization + meta.origin + meta.element` **plus
|
|
112
134
|
`meta.render` for pipeline-rendered marks** (sync `resolveAsset` writes/queries it; the field
|
|
113
135
|
and its index live in drawbridge-api `schema/file.js`). `origin` identifies the source asset,
|
|
@@ -153,17 +175,34 @@ skill) so it stops living only in tribal memory.
|
|
|
153
175
|
would have zeroed every invoice once the products were archived.
|
|
154
176
|
- **The v2 pricing-plan layer (`bpp_*`, billing cadences) is never touched by script.** Canceling a
|
|
155
177
|
billing cadence is terminal and unrecoverable. Dashboard, by hand, only.
|
|
156
|
-
- **The Shopify meter handle is
|
|
157
|
-
|
|
158
|
-
(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
- **The Shopify meter handle is manifest data, provider-overridable, and sender-verified
|
|
179
|
+
(≥ utils 0.0.151)** (utils ↔ sync ↔ api ↔ Partner Dashboard). The utils shopify manifest
|
|
180
|
+
publishes `events.order.handle` = `slugify('drawbridge-orders')`; the provider row's
|
|
181
|
+
`orderEventHandle` field (admin-editable, `format:'slug'` — normalized on save AND read)
|
|
182
|
+
overrides it at mint time. The order hook puts the effective handle on the billing job and
|
|
183
|
+
mints `billed.transaction` = `<handle>.<orderId>` (also the event's `reference` attribute —
|
|
184
|
+
the shared trace key). `sendAppEvent` has NO default handle any more, and sync's sender
|
|
185
|
+
REFUSES to send when the job carries none (`shopify.usage.billing.handle.missing`) or when
|
|
186
|
+
the transaction prefix disagrees (`…handle.mismatch`) — both before any provider read, both
|
|
187
|
+
page through the abandoned alert. The handle must still exactly match — case-sensitive,
|
|
188
|
+
handle not display name — a usage meter on the plan version each merchant APPROVED: meters
|
|
189
|
+
attach only at approval and are never retroactive, and a pre-meter approval 202s every event
|
|
190
|
+
and bills nothing (the 19/99 write-off; also every pre-2026-09 production approval).
|
|
191
|
+
- **Shopify billing proof comes only from the Partner ledger; absence is never a verdict**
|
|
192
|
+
(utils ↔ sync ↔ api ↔ app-web). No Admin API version materializes a managed-pricing meter
|
|
193
|
+
(measured 2026-09-04), so `connection.source.metered` is a billing-PROOF reference: the
|
|
194
|
+
latest positive `CHARGE_USAGE` charge id from the Partner API (provider row fields
|
|
195
|
+
`partnerOrgId`/`partnerAppId`/`partnerToken`), `null` only from measured unmetered evidence,
|
|
196
|
+
absent = never proven — and every consumer (task card, checklist item, billing Issue,
|
|
197
|
+
presented-error status, admin badges, App Home error row) gates on the explicit null, never
|
|
198
|
+
on silence. Writers: the daily health run, the approval webhook's instant restamp, and the
|
|
199
|
+
reselect flow's stale-null lift (a completed reselect returns a null-stamped store to
|
|
200
|
+
no-verdict — the null described the approval that was just replaced). Charge rows exist only
|
|
201
|
+
at billing boundaries (usage accrues invisibly between them); each row is reconciled once
|
|
202
|
+
against its window's orders — balanced stamps `billed.receipt`, a mismatch pages
|
|
203
|
+
`shopify.usage.reconciliation.mismatch`, and admin-voided orders (`billed.void`, the
|
|
204
|
+
write-off for pre-meter losses) leave both the sums and the dashboard's unreconciled count.
|
|
205
|
+
Detail: drawbridge-docs `reference/shopify-app.md` (Billing).
|
|
167
206
|
- **A subscription must open with the plan item.** It used to open with the metered actions price
|
|
168
207
|
and attach the plan afterwards with `always_invoice`. With no metered item, Stripe rejects an
|
|
169
208
|
itemless subscription, so the plan goes in at creation — changing the first invoice's shape.
|
|
@@ -190,6 +229,14 @@ skill) so it stops living only in tribal memory.
|
|
|
190
229
|
sync upserts the same invoice from three concurrent webhooks (finalized/payment_succeeded/voided)
|
|
191
230
|
and relies on the index to reject the losing insert so the catch can redo it as an update. Drop
|
|
192
231
|
the index → silent duplicate invoice rows; remove the catch → concurrent events throw + retry.
|
|
232
|
+
- The `buffer` collection's **unique partial index on Stripe `provider.id`** (drawbridge-api
|
|
233
|
+
`schema/buffer.js`, Stripe rows with a string id only) and webhooks' E11000 catch in
|
|
234
|
+
`route/stripe.js` are one contract: the route dedupes a Stripe redelivery with a read before its
|
|
235
|
+
insert, two retries arriving together both pass that read, and the index refuses the second
|
|
236
|
+
insert, which the route answers 200 without touching the first row. Drop the index → the race
|
|
237
|
+
reopens silently and sync processes one event twice; remove the catch → a concurrent retry 500s
|
|
238
|
+
and Stripe keeps retrying. The index is manual (the resetIndexes block is the runbook) and its
|
|
239
|
+
creation fails while duplicates exist — dedupe first.
|
|
193
240
|
- The `contact` collection's **unique multikey index on `leads`** (drawbridge-api
|
|
194
241
|
`schema/contact.js`) and sync's contact resolution (`lib/contact.js` + `queue/contact.js`) are
|
|
195
242
|
one contract: a lead belongs to at most one contact. One submission enqueues two resolve jobs
|
|
@@ -438,6 +485,18 @@ sweep (`queue/identity.js`), which re-points any number whose Twilio config drif
|
|
|
438
485
|
the path in one place and the sweep "repairs" every number to the wrong URL — inbound SMS
|
|
439
486
|
(STOP included) goes dark with all rows reading healthy.
|
|
440
487
|
|
|
488
|
+
**Provider rows are keyed by VENDOR, and no manifest owns a vendor (utils ↔ api ↔ sync ↔
|
|
489
|
+
webhooks, since utils 0.0.153).** A manifest declares only the fields it spends under
|
|
490
|
+
`provider.vendors.<vendor>`; `mergeVendors()` in utils unions every manifest's slices into
|
|
491
|
+
one entry per vendor, and the `provider` collection holds one row per key of that merge —
|
|
492
|
+
`sendgrid`, `twilio`, `hubspot` for the private `drawbridge` connection, one `google` row for
|
|
493
|
+
however many Google connections come. Build refuses a vendor drawn two ways, a shared field
|
|
494
|
+
declared differently, and a credential name claimed twice. A connection reads its vendors
|
|
495
|
+
merged through `vendorSettings({ controller, slug })` (sync's `drawbridgeProvider()`, the
|
|
496
|
+
api's identity/organization routes); the merged keys keep the old names. Reading
|
|
497
|
+
`providerSettings({ slug : 'drawbridge' })` anywhere finds no row — it was split on dev
|
|
498
|
+
2026-09-08 and prod is seeded per-vendor. Detail: `drawbridge-docs/reference/providers.md`.
|
|
499
|
+
|
|
441
500
|
## Import surfaces
|
|
442
501
|
|
|
443
502
|
- Import names against a package's **actual exports** — a missing export resolves to `undefined`
|
package/package.json
CHANGED