@drawbridge/drawbridge-agents 0.1.37 → 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,19 @@ 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.
51
69
  - **Adding an email/SMS vendor is a two-repo change** (api ↔ sync). A connection is deliverable
52
70
  only if sync implements a sender for its slug — `lib/email.js` (`{ mailchimp, sendgrid }`) and
53
71
  `lib/sms.js` (`{ twilio }`), which `queue/notification.js` reads **directly** as its guard and
@@ -217,6 +235,33 @@ skill) so it stops living only in tribal memory.
217
235
  drawbridge-utils (15 min). The prize-draw branch is a genuinely different 5 min
218
236
  (api `route/otc.js` CODE_TTL_MINUTES) — the two are not interchangeable.
219
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
+
220
265
  ## Growth attribution tags (growth ↔ api)
221
266
 
222
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.37",
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": {