@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {