@drawbridge/drawbridge-agents 0.1.51 → 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.
|
|
@@ -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,
|
|
@@ -207,6 +229,14 @@ skill) so it stops living only in tribal memory.
|
|
|
207
229
|
sync upserts the same invoice from three concurrent webhooks (finalized/payment_succeeded/voided)
|
|
208
230
|
and relies on the index to reject the losing insert so the catch can redo it as an update. Drop
|
|
209
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.
|
|
210
240
|
- The `contact` collection's **unique multikey index on `leads`** (drawbridge-api
|
|
211
241
|
`schema/contact.js`) and sync's contact resolution (`lib/contact.js` + `queue/contact.js`) are
|
|
212
242
|
one contract: a lead belongs to at most one contact. One submission enqueues two resolve jobs
|
|
@@ -455,6 +485,18 @@ sweep (`queue/identity.js`), which re-points any number whose Twilio config drif
|
|
|
455
485
|
the path in one place and the sweep "repairs" every number to the wrong URL — inbound SMS
|
|
456
486
|
(STOP included) goes dark with all rows reading healthy.
|
|
457
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
|
+
|
|
458
500
|
## Import surfaces
|
|
459
501
|
|
|
460
502
|
- Import names against a package's **actual exports** — a missing export resolves to `undefined`
|
package/package.json
CHANGED