@drawbridge/drawbridge-agents 0.1.36 → 0.1.38
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.
|
@@ -9,6 +9,11 @@ skill) so it stops living only in tribal memory.
|
|
|
9
9
|
|
|
10
10
|
- `OAUTH_TOKEN_HMAC_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
|
+
- `UNSUBSCRIBE_TOKEN_HMAC_KEY` must be identical in **drawbridge-api** and **drawbridge-sync** —
|
|
13
|
+
both `assertEnv` it at boot. Sync signs the unsubscribe tokens it builds into notification
|
|
14
|
+
emails (`lib/suppression.js buildToken`); api verifies them on the public `/unsubscribe/:token`
|
|
15
|
+
routes (`lib/unsubscribe.js`). A mismatch makes every unsubscribe link 404 as "invalid or
|
|
16
|
+
expired" — one-click unsubscribes silently stop landing, which mail providers punish.
|
|
12
17
|
- reCAPTCHA on auth is a key pair: `GOOGLE_RECAPTCHA_SECRET` (**drawbridge-api**) + `NEXT_PUBLIC_GOOGLE_RECAPTCHA_KEY`
|
|
13
18
|
(**drawbridge-app-web**), both `assertEnv`'d at boot. The api verifies the v3 token app-web's
|
|
14
19
|
`/auth` forms attach and enforces it **unconditionally** on `/oauth/{signup,signin}/request`
|
|
@@ -48,6 +53,28 @@ skill) so it stops living only in tribal memory.
|
|
|
48
53
|
`ads-republishing.<connectionId>`). Never unique-index `coalesce` (superseding requires the
|
|
49
54
|
second insert to land), and writes that set `key` need the writer-side E11000 handler or a
|
|
50
55
|
replayed event logs a false error.
|
|
56
|
+
- **The opt-out floor: `suppression` is the single consent gate** (api ↔ sync ↔ webhooks).
|
|
57
|
+
Writers: api's `/unsubscribe` routes and draw preflight, sync's twilio buffer drain
|
|
58
|
+
(`lib/buffer.js 'twilio.message.inbound'`). The ONLY reader on any send path is sync
|
|
59
|
+
`lib/suppression.js canSend()` — a new lead-facing send path must call it, or opted-out
|
|
60
|
+
recipients get contacted (TCPA/CAN-SPAM exposure). Lead-facing = the notification carries
|
|
61
|
+
`lead`/`workflow`/`connection`; those sends also REQUIRE an org connection (no system-sender
|
|
62
|
+
fallback), while org-less system mail (OTC codes, member invites, security notices) keeps the
|
|
63
|
+
system sender and is never suppressed. Inbound STOP flows webhooks
|
|
64
|
+
(`POST /connection/:id/twilio`, signature-verified against the URL built from
|
|
65
|
+
`APP_CLIENT_WEBHOOKS_URI`) → `buffer` doc → sync drain → platform-global suppression row;
|
|
66
|
+
api registers the number's SmsUrl at twilio connect (`lib/twilio.js`) and `scripts.js
|
|
67
|
+
registerTwilioWebhooks` backfills — a deployed webhooks route with unregistered numbers hears
|
|
68
|
+
nothing, silently.
|
|
69
|
+
- **Adding an email/SMS vendor is a two-repo change** (api ↔ sync). A connection is deliverable
|
|
70
|
+
only if sync implements a sender for its slug — `lib/email.js` (`{ mailchimp, sendgrid }`) and
|
|
71
|
+
`lib/sms.js` (`{ twilio }`), which `queue/notification.js` reads **directly** as its guard and
|
|
72
|
+
org-scoped lookup; never re-list the slugs as literals, or the guard drifts from the senders
|
|
73
|
+
that exist. Adding a vendor to drawbridge-api `lib/connections.js` with `group : 'email'`/`'sms'`
|
|
74
|
+
makes it selectable throughout api and app-web, but with no sync sender every send is
|
|
75
|
+
cancelled, not delivered — merchant sees a workflow that ran and no message. The miss is
|
|
76
|
+
detectable, not silent: `notification.reason = 'connection-slug-mismatch'` plus an
|
|
77
|
+
Error-instance `logger.error` (a string never reaches Sentry Issues — see `sentry-sdk.md`).
|
|
51
78
|
- Schema-gated fields ship **api-first, booted**: a sibling writer setting a field new to a
|
|
52
79
|
drawbridge-api `$jsonSchema` (validators are strict + `additionalProperties: false`) must not
|
|
53
80
|
go live until the api has **booted** with the schema change on that environment — creating the
|
|
@@ -208,6 +235,33 @@ skill) so it stops living only in tribal memory.
|
|
|
208
235
|
drawbridge-utils (15 min). The prize-draw branch is a genuinely different 5 min
|
|
209
236
|
(api `route/otc.js` CODE_TTL_MINUTES) — the two are not interchangeable.
|
|
210
237
|
|
|
238
|
+
## Submission answers are immutable (api ↔ sync ↔ app-web ↔ share)
|
|
239
|
+
|
|
240
|
+
- **Nothing rewrites `submission.fields[]` after the write.** Renaming a field or one of its
|
|
241
|
+
options, and deleting a field, all used to reach back into every stored submission (`$set` on
|
|
242
|
+
`fields.$[element].label` / `.value`, `$pull` by `reference`). An answer is the record of what a
|
|
243
|
+
person was asked and said; a definition change must not touch it. The link forward is
|
|
244
|
+
`fields[].reference` (the field id) and, for choice answers, `fields[].option` (the option id
|
|
245
|
+
captured at submission time) — text is display, ids are identity.
|
|
246
|
+
- **Deleting a field is a tombstone**, `deleted : true`, never a `delete`. Every live read must
|
|
247
|
+
filter `deleted : { $ne : true }` — api's builder field list, create position count, delete
|
|
248
|
+
position decrement, and the share `campaign-fields` cache aggregate; sync's leads exporter and
|
|
249
|
+
the nightly `totals.fields` recount. The **lead-detail endpoint deliberately does not filter**:
|
|
250
|
+
app-web pivots `fieldsById[reference]`, so returning tombstoned definitions is the only thing
|
|
251
|
+
keeping a removed field's historical answers displayable (it labels them `(removed)`).
|
|
252
|
+
- Because the doc survives, **`stream/field.js`'s `delete` handler never fires for a removal** —
|
|
253
|
+
its `update` handler watches `updateDescription.updatedFields.deleted === true` for the
|
|
254
|
+
`campaign.totals.fields` decrement and cache-chain invalidation. A new consumer of field
|
|
255
|
+
deletion must hook the transition, not the delete.
|
|
256
|
+
- **Field validators live in `@drawbridge/drawbridge-utils/fields-validate`**, consumed by BOTH
|
|
257
|
+
drawbridge-share's form and drawbridge-api's submission POST — `yup` is injected so each keeps
|
|
258
|
+
its own copy. A rule added on one side only is client-side theater: the submission endpoint is
|
|
259
|
+
publicly reachable. `select` is checked in-list against the field's own options; `required()`
|
|
260
|
+
honours `attributes.required === false`.
|
|
261
|
+
- **`scripts/backfill-submission-option-ids.js` must run before any further option rename lands.**
|
|
262
|
+
It matches legacy answers to options by text one last time; once a definition and its stored
|
|
263
|
+
text diverge, nothing can match them again.
|
|
264
|
+
|
|
211
265
|
## Growth attribution tags (growth ↔ api)
|
|
212
266
|
|
|
213
267
|
- drawbridge-growth `lib/tags.js` MIRRORS drawbridge-api `lib/utm.js` — allow-listed keys, trim,
|
package/package.json
CHANGED