@drawbridge/drawbridge-agents 0.1.24 → 0.1.26

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.
@@ -54,6 +54,7 @@ skill) so it stops living only in tribal memory.
54
54
  index manually is not enough; until the boot collMod lands, every insert carrying the field
55
55
  fails Mongo 121 (not E11000), so keyed-idempotency writers silently drop their writes
56
56
  (2026-07-30 `notification.key` dev outage, Sentry DRAWBRIDGE-SYNC-32).
57
+ - Scraped-asset file reuse keys on `organization + meta.origin + meta.element` **plus
57
58
  `meta.render` for pipeline-rendered marks** (sync `resolveAsset` writes/queries it; the field
58
59
  and its index live in drawbridge-api `schema/file.js`). `origin` identifies the source asset,
59
60
  `render` content-hashes the rendered output — drop `render` from either side and a rendering
@@ -62,17 +63,23 @@ skill) so it stops living only in tribal memory.
62
63
 
63
64
  ## Billing / metering
64
65
 
65
- - Meter events are **single-writer**: true-ups are `billing.trueup` ledger rows, never meter
66
- events. A true-up written as a meter event in-window corrupts the reflection poll and strands
67
- units unbillable.
68
- - **Upgrade mechanics and the billing path are one contract**: drawbridge-stripe's upgrade
69
- (`subscription.change`) swaps the metered actions price in place, and classic-mode Stripe then
70
- ignores meter events on that closing cycle invoice entirely so drawbridge-sync's
71
- `settleCycleUsage` bills upgrade-truncated invoices (actions line starts >60s after
72
- `period_start`) as plain invoice items priced from plans.js instead of the meter. Changing the
73
- upgrade mechanic (schedules, cycle-anchor reset, `billing_mode=flexible`) or the metering path
74
- without re-checking the other side silently re-zeroes upgrade-cycle invoices. Detail:
75
- drawbridge-docs `reference/billing.md` § Stripe meter emission.
66
+ - **Actions never touch Stripe's meter.** Every cycle invoice bills the period's ledger count as
67
+ invoice items priced from `plans.js` (drawbridge-sync `settleCycleUsage`). The meter was removed
68
+ after it produced five distinct defects, ending in a 60-second line-window overlap that billed
69
+ one correctly-delivered event twice. Do not reintroduce meter events, and do not "fix" a billing
70
+ discrepancy by reporting usage to Stripe corrections are `billing.trueup` ledger rows.
71
+ - **The actions PRODUCT is load-bearing; the meter and metered price are not.** drawbridge-stripe
72
+ (≥0.1.42) provisions only a product for actions, and drawbridge-sync prices its invoice lines
73
+ with `price_data.product` read from `items.actions.product`. Deactivating or dropping that
74
+ product breaks every future invoice which is why api's `scripts.js removeActionsMeters` strips
75
+ the item, price and meter but never the product, and does not reuse
76
+ `stripeSubscription.deactivatableItems` (that includes it).
77
+ - **A subscription must open with the plan item.** It used to open with the metered actions price
78
+ and attach the plan afterwards with `always_invoice`. With no metered item, Stripe rejects an
79
+ itemless subscription, so the plan goes in at creation — changing the first invoice's shape.
80
+ - **Invoice line descriptions must keep the word "actions":** `isActionsLine` matches `/actions/i`
81
+ to count billed quantity for the billed-vs-ledger reconcile. A copy edit that drops it silently
82
+ breaks the audit. Detail: drawbridge-docs `reference/billing.md`.
76
83
  - Every `$inc` on a `totals.*` counter needs a matching entry in `drawbridge-sync/lib/totals.js`
77
84
  `SOURCES` — that map is the only drift backstop.
78
85
  - The `invoice` collection's **unique partial index on `stripeInvoiceId`** (drawbridge-api
@@ -99,6 +106,32 @@ skill) so it stops living only in tribal memory.
99
106
  `createdAt`. Re-adding a TTL shorter than any billing interval silently under-bills long cycles
100
107
  and blanks reporting past the window — any retention change must clear billing + reporting.
101
108
 
109
+ ## Query params + the OAuth resume target (utils ↔ app-web ↔ api ↔ shopify-app)
110
+
111
+ - `request()` in `@drawbridge/drawbridge-utils` (`lib/fetch.js`) builds EVERY family GET query and
112
+ MUST percent-encode param values — `qs.stringify( params, { encodeValuesOnly : true } )`, never
113
+ `encode : false` (≥0.0.90; pinned by `test/fetch.test.js`). Param values routinely carry user input
114
+ and whole URLs, and with encoding off the first `&` inside a value ends that value while its
115
+ remainder arrives as sibling params. This silently truncated every list search (`search.value` of
116
+ `Smith & Sons` reached the api as `Smith `) and cost the Shopify account link its `redirect_uri`.
117
+ `encodeValuesOnly` — not full encoding — keeps the bracket notation (`search[keys][0]=`) so every
118
+ query the api already parses is byte-identical whenever values needed no escaping.
119
+ - The OAuth resume target (`redirectTo`) is **a URL carrying its own query string**, not a scalar:
120
+ `/api/oauth/authorize?client_id=…&redirect_uri=…&state=…&code_challenge=…`. Every hop must preserve
121
+ it whole — app-web's authorize BFF requires BOTH `client_id` and `redirect_uri` and otherwise falls
122
+ back to `/organizations`, which reads to a merchant as "connecting silently did nothing" rather
123
+ than as an error. Hops that carry it: the `/auth` ↔ `/auth/signup` links, `POST /api/auth/signin`,
124
+ `/oauth/google/start` → the api's `oauth.state` token row → `/api/auth/google`, `/api/switch/:id`,
125
+ and app-web's `proxy.js` auth gate.
126
+ - app-web's `/api/oauth/authorize` stamps `resumed=1` on the resume url when it bounces to `/auth`.
127
+ `getSession()` returns null both for "no session" and for "the api session lookup failed", so a
128
+ blip sends the browser to `/auth`, which resolves the session itself and sends it straight back;
129
+ the marker makes a second unresolved arrival stop instead of trading redirects until the browser
130
+ gives up.
131
+ - The api's `POST /oauth/google/callback` returns `redirectTo` in its 400 body when it read the state
132
+ row before failing, so app-web can keep the resume target across `/auth?error=oauth`. A
133
+ `oauth.state.invalid` failure cannot — that row is what carried the target.
134
+
102
135
  ## Reporting (api ↔ app-web)
103
136
 
104
137
  - Every reporting request (usage/revenue/redemptions/analytics, org + admin) carries the viewer's
@@ -145,6 +178,12 @@ skill) so it stops living only in tribal memory.
145
178
  - The poster worker `$set`s `poster : 0` on every run and APPENDS to `frames[]`. Any script that
146
179
  generates a frame must snapshot and restore `poster`/`posterTimestamp`, **including on the
147
180
  failure path**, or a retry silently discards the user's thumbnail choice.
181
+ - **Never compare frame timestamps as strings.** drawbridge-api writes `'HH:MM:SS'` (upload
182
+ default `'00:00:00'`) while app-web's scrubber writes `'HH:MM:SS.mmm'`, so `=== '00:00:00'`
183
+ misses the starting frame. That single mistake defeated the api's duplicate guard AND app-web's
184
+ own zero-check on the same day, letting the picker create a second starting frame. Compare
185
+ parsed milliseconds — `frameMilliseconds` in api `route/organization-file.js`, `lib/frames.js`
186
+ in app-web.
148
187
 
149
188
  ## Import surfaces
150
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {