standdown 0.2.1 → 0.2.3
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.
- package/ADOPTING.md +298 -0
- package/README.md +9 -0
- package/package.json +3 -2
package/ADOPTING.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Adopting `standdown` in an extension that already stands down
|
|
2
|
+
|
|
3
|
+
This is the **brownfield** migration guide. It is for an extension that *already*
|
|
4
|
+
has homegrown stand-down / affiliate-suppression logic and wants to move that
|
|
5
|
+
logic onto the [`standdown`](https://www.npmjs.com/package/standdown) library
|
|
6
|
+
without ever losing revenue in the switch.
|
|
7
|
+
|
|
8
|
+
> If you are adding stand-down to an extension that has **none** today, you want
|
|
9
|
+
> the greenfield "install standdown" skill instead — that is a clean install.
|
|
10
|
+
> This guide is the harder case: there is a working, revenue-critical decision
|
|
11
|
+
> path in production, and the job is to replace it *provably* rather than *hopefully*.
|
|
12
|
+
|
|
13
|
+
It is tool-agnostic. Point any coding agent (Claude Code, Cursor, Copilot, etc.)
|
|
14
|
+
at it. The one rule that overrides everything below: **you are migrating logic
|
|
15
|
+
that decides whether a partner keeps a commission. A wrong "activate" hijacks a
|
|
16
|
+
sale that someone else already earned. Treat every step as revenue-critical and
|
|
17
|
+
default to the existing behavior whenever the new behavior is uncertain.**
|
|
18
|
+
|
|
19
|
+
The migration runs in five phases:
|
|
20
|
+
|
|
21
|
+
1. **DETECT** — find the existing stand-down logic.
|
|
22
|
+
2. **MAP** — translate each homegrown construct to a `standdown` API concept.
|
|
23
|
+
3. **SHADOW** — run `standdown` in observe-only mode beside the real path, reconcile every divergence, *then* cut over behind a flag.
|
|
24
|
+
4. **GUARD** — hold the library's invariants as hard constraints the whole way.
|
|
25
|
+
5. **VERIFY** — characterization tests, the audit grade, and no-fail-open assertions before you delete anything.
|
|
26
|
+
|
|
27
|
+
Throughout, the worked example is a real migration: a browser
|
|
28
|
+
extension whose server-driven stand-down policy covered CJ, Rakuten, Impact,
|
|
29
|
+
eBay, and a handful of merchant blocks (Home Depot, AliExpress, Shein), with
|
|
30
|
+
`ignore_param` self-exemption and a whole-cookie-string matcher.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Phase 1 — DETECT the existing stand-down logic
|
|
35
|
+
|
|
36
|
+
You cannot migrate what you have not found. Stand-down logic is rarely in one
|
|
37
|
+
file called `standdown.ts`; it hides in redirect gates, cookie sniffers,
|
|
38
|
+
"disable on these domains" lists, and param allowlists. Sweep the codebase for
|
|
39
|
+
all of it before mapping anything.
|
|
40
|
+
|
|
41
|
+
**Grep for these signals** (case-insensitive, whole repo, including the
|
|
42
|
+
background worker, content scripts, and any server that ships policy to the
|
|
43
|
+
extension):
|
|
44
|
+
|
|
45
|
+
| What you're looking for | Grep patterns |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Affiliate **click-id params** | `cjevent`, `cjdata`, `irclickid`, `irgwc`, `awc`, `ranSiteID`, `ranEAID`, `ranMID`, `sscid`, `clickid`, `afsrc` |
|
|
48
|
+
| **Network redirect domains** | `linksynergy`, `anrdoezrs`, `dpbolvw`, `jdoqocy`, `kqzyfj`, `qksrv`, `awin1`, `shareasale`, `commission-junction` |
|
|
49
|
+
| **Cookie checks** | `document.cookie`, `lsclick_mid`, `linkshare`, `cje`, `cjevent_dc`, `im_ref`, cookie-name/`includes(` scans |
|
|
50
|
+
| **Self-exemption / own attribution** | `ignore_param`, `ignore-stand-down`, `self`, your own `PID=`, `SID=`, `siteID`, publisher-owned click ids |
|
|
51
|
+
| **Merchant / host blocks** | `disable_domains`, `disableHosts`, `blocklist`, `safari_popup_disable_domains`, allowlist/denylist of hosts |
|
|
52
|
+
| **The decision itself** | `stand down`, `standdown`, `stand-down`, `suppress`, `shouldActivate`, `shouldStandDown`, `allowlist`, `redirect` gates around cookie writes |
|
|
53
|
+
|
|
54
|
+
**Produce a detection inventory** — one row per distinct rule, capturing:
|
|
55
|
+
|
|
56
|
+
- the **network or merchant** it concerns,
|
|
57
|
+
- the **exact current behavior** (what makes it fire, what it does when it fires),
|
|
58
|
+
- **where** it lives (file:line), and
|
|
59
|
+
- any **TTL / persistence** (session flag? cookie duration? nothing?).
|
|
60
|
+
|
|
61
|
+
Worked example (an adopting extension): the sweep surfaced a server-fetched policy plus a
|
|
62
|
+
hand-copied `FALLBACK_POLICY` in the background worker, a `standDownHelper.ts`
|
|
63
|
+
that lowercased the **entire** `document.cookie` string and did `.includes()`,
|
|
64
|
+
an `ignore_param` self-exemption per network, a `standDownCookieDuration` of 60
|
|
65
|
+
minutes for CJ, and `disable_domains` lists for eBay, Home Depot, AliExpress,
|
|
66
|
+
and Shein. Note especially the two things that are easy to miss: the **cookie
|
|
67
|
+
matcher looked at values, not just names**, and `disable_domains` used
|
|
68
|
+
**substring** `includes()` (so `'ebay.'` also matched `rebay.com`).
|
|
69
|
+
|
|
70
|
+
Do not skip anything as "obviously equivalent." The divergences that cost
|
|
71
|
+
revenue are exactly the ones that look equivalent at a glance.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 2 — MAP homegrown constructs to the `standdown` API
|
|
76
|
+
|
|
77
|
+
`standdown` is a **data-driven policy engine**: you describe each network as a
|
|
78
|
+
`StanddownPolicy` (detection rules + stand-down behavior), hand the array to an
|
|
79
|
+
adapter, and it returns a `Decision`. Your job here is a translation table, not
|
|
80
|
+
new logic. Map each inventory row to exactly one library concept.
|
|
81
|
+
|
|
82
|
+
### The mapping table
|
|
83
|
+
|
|
84
|
+
| Homegrown construct | `standdown` concept | Notes |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| Affiliate params that trigger stand-down (`cjevent`, `ranSiteID`, …) | `detection.landingParams` | Grouped `anyOf` / `allOf`; `{ name }` alone = presence, add `value` + `match:'equals'` for value checks. |
|
|
87
|
+
| Network redirect/rotator hostnames (`linksynergy`, `anrdoezrs`) | `detection.redirectDomains` | `{ pattern, kind:'suffix' }`. Observed only by the webext adapter's `webRequest` plane. |
|
|
88
|
+
| Cookie checks | `detection.cookiePatterns` | **NAME-ONLY.** `{ name, match:'exact'\|'substring' }`. Values are never inspected — see the divergence note below. |
|
|
89
|
+
| Your **own** attribution params (`ignore_param`) | `selfPatterns` + `selfExemptionScope` | `{ name, value?, match?, networkId }`. Scope controls how long the exemption sticks — this is the highest-risk mapping. |
|
|
90
|
+
| "Never operate on this host" merchant blocks (`disable_domains`) | `detection.disableHosts` | `{ pattern, kind:'suffix'\|'regex' }`. Unconditional, strongest match, **not liftable** by any self-exemption. |
|
|
91
|
+
| Per-network minimum stand-down window (`standDownCookieDuration`) | `standdown.minDurationMs` with `sessionRule:'session-or-min'` | Calibrate to the production number. |
|
|
92
|
+
| Session vs persisted stand-down | adapter `storage: 'session'` \| `'local-ttl'` | `local-ttl` survives a `sessionStorage` clear within a sliding 24h envelope. |
|
|
93
|
+
|
|
94
|
+
### Worked example: an adopting extension's config
|
|
95
|
+
|
|
96
|
+
The inventory mapped cleanly onto the bundled packs plus one custom
|
|
97
|
+
merchant-block policy:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createContentStanddown } from 'standdown/content';
|
|
101
|
+
import { cjPolicy, impactPolicy, rakutenPolicy, ebayEpnPolicy } from 'standdown/policies';
|
|
102
|
+
|
|
103
|
+
// disable_domains -> a custom policy carrying ONLY detection.disableHosts.
|
|
104
|
+
// (...id, network, standdown, activation, metadata omitted for brevity...)
|
|
105
|
+
const hostMerchantBlocks = {
|
|
106
|
+
// id: 'host-merchant-blocks', network: {...}, standdown: {...}, activation: {...}, metadata: {...},
|
|
107
|
+
detection: {
|
|
108
|
+
disableHosts: [
|
|
109
|
+
{ pattern: '(^|\\.)ebay\\.[a-z.]+$', kind: 'regex' }, // replaces disable_domains ['ebay.com','ebay.']
|
|
110
|
+
{ pattern: 'homedepot.com', kind: 'suffix' },
|
|
111
|
+
{ pattern: 'aliexpress.com', kind: 'suffix' },
|
|
112
|
+
{ pattern: 'aliexpress.co.uk', kind: 'suffix' },
|
|
113
|
+
{ pattern: 'shein.com', kind: 'suffix' }, // suffix also covers m.shein.com
|
|
114
|
+
{ pattern: 'shein.co.uk', kind: 'suffix' }, // and m.shein.co.uk
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
const standdown = createContentStanddown({
|
|
120
|
+
policies: [cjPolicy, impactPolicy, rakutenPolicy, ebayEpnPolicy, hostMerchantBlocks],
|
|
121
|
+
selfPatterns: [
|
|
122
|
+
{ name: 'ranSiteID', value: 'EXAMPLESITEID', match: 'contains', networkId: 'rakuten' },
|
|
123
|
+
{ name: 'cp', value: '_examplebrand', match: 'contains', networkId: 'cj' },
|
|
124
|
+
{ name: 'PID', value: 'CJ0000000001', match: 'equals', networkId: 'cj' },
|
|
125
|
+
{ name: 'PID', value: 'CJ0000000002', match: 'equals', networkId: 'cj' },
|
|
126
|
+
],
|
|
127
|
+
selfExemptionScope: 'policy', // per-navigation, faithful to the adopter's ignore_param — NEVER 'session' here
|
|
128
|
+
publisherSites: ['example.com'],
|
|
129
|
+
storage: 'session', // or 'local-ttl' to honor CJ's 60-minute minDurationMs across a sessionStorage clear
|
|
130
|
+
auditLog: true,
|
|
131
|
+
onDecision: (d) => {/* namespaced shadow key / analytics only — see Phase 3 */},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Notes that generalize to any migration:
|
|
136
|
+
|
|
137
|
+
- **`disable_domains` is not a network** — do not invent a policy pack for it.
|
|
138
|
+
Merchant blocks are `detection.disableHosts` on a custom policy. Home Depot,
|
|
139
|
+
AliExpress, and Shein each become a `suffix` rule; a suffix rule already
|
|
140
|
+
matches subdomains, so `shein.com` covers `m.shein.com` with no extra entry.
|
|
141
|
+
- **`ignore_param` maps to `selfPatterns`, and the scope is the whole ballgame.**
|
|
142
|
+
The adopter's `ignore_param` was *per-navigation*, so `selfExemptionScope: 'policy'`
|
|
143
|
+
(the default) is faithful. `'session'` would add self-click stickiness the adopter
|
|
144
|
+
never had and could let the extension **activate** on a later param-less visit
|
|
145
|
+
where the adopter stood down — a more-permissive change that hijacks a sale. Only use
|
|
146
|
+
`'session'` if the homegrown code actually persisted the exemption.
|
|
147
|
+
- **Match the current fleet, not the "complete" library.** The bundled
|
|
148
|
+
`amazonPolicy` always stands down on Amazon; if the extension currently stays
|
|
149
|
+
*active* on Amazon (`ALLOW_AMAZON=true`), **exclude** it. Same for any host the
|
|
150
|
+
extension deliberately still operates on (an adopter's retired Wayfair block): omit
|
|
151
|
+
the `disableHosts` entry until the business decides to reinstate it. Broader
|
|
152
|
+
bundled packs (`universal`, `awin`, `shareasale`) are safe-stricter but will
|
|
153
|
+
disagree with current behavior a lot — keep them **opt-in** for the first cut.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Phase 3 — SHADOW mode before cutover (this is the whole point)
|
|
158
|
+
|
|
159
|
+
**Never big-bang-replace revenue logic.** The existing decision path is the
|
|
160
|
+
ground truth for money that is already being earned; the new one is a hypothesis
|
|
161
|
+
until proven. A direct swap bets real commissions on that hypothesis being
|
|
162
|
+
correct on the first try, across every network, merchant, and edge case you
|
|
163
|
+
found in Phase 1 — and the failure mode is silent (a hijacked sale looks like a
|
|
164
|
+
normal activation). So you run the new engine in the dark first and only promote
|
|
165
|
+
it once it agrees with reality on purpose.
|
|
166
|
+
|
|
167
|
+
### The protocol
|
|
168
|
+
|
|
169
|
+
1. **Baseline grade.** Run the audit grader against the *current* extension and
|
|
170
|
+
record its letter grade. This is the bar the migration must **meet or beat** —
|
|
171
|
+
never regress it.
|
|
172
|
+
|
|
173
|
+
2. **Shadow observe.** Wire `standdown` in alongside the existing path, computing
|
|
174
|
+
a `Decision` for every navigation but **taking no action on it**. The real
|
|
175
|
+
path still decides. Emit each shadow decision to a **namespaced** shadow key /
|
|
176
|
+
analytics channel (via `onDecision` or your own logging) next to what the real
|
|
177
|
+
path did. Nothing the user experiences changes.
|
|
178
|
+
|
|
179
|
+
3. **Reconcile divergences.** Compare shadow vs. real on live traffic and triage
|
|
180
|
+
every disagreement into one of:
|
|
181
|
+
- **safe-stricter** — `standdown` stands down where the old path activated.
|
|
182
|
+
No revenue risk (you can only *lose a competing activation*, never hijack).
|
|
183
|
+
Accept, or note as expected.
|
|
184
|
+
- **dangerous-more-permissive** — `standdown` would activate where the old
|
|
185
|
+
path stood down. **This is the failure class that hijacks sales. It must be
|
|
186
|
+
driven to zero before cutover**, usually by adding a `disableHosts` entry or
|
|
187
|
+
tightening a `selfPattern`.
|
|
188
|
+
- **needs-human-decision** — a genuine behavior change with a business
|
|
189
|
+
tradeoff (see the adopter cases below). Escalate; do not silently pick.
|
|
190
|
+
|
|
191
|
+
4. **Flagged cutover.** Only once dangerous-more-permissive divergences are zero
|
|
192
|
+
and the grade is ≥ baseline: move the real decision behind an
|
|
193
|
+
**off-by-default flag** that swaps the old path for `standdown`. Roll it
|
|
194
|
+
forward gradually. Keep the shadow comparison running so a regression is
|
|
195
|
+
visible immediately.
|
|
196
|
+
|
|
197
|
+
5. **Delete old code — last.** Only after the flag has been fully on in
|
|
198
|
+
production and stable do you remove the homegrown logic. Until then it stays
|
|
199
|
+
as the instant rollback.
|
|
200
|
+
|
|
201
|
+
### Worked example: the divergences an adopter had to reconcile
|
|
202
|
+
|
|
203
|
+
- **Cookie name-vs-value (safe-stricter, accepted).** The old matcher hit on a
|
|
204
|
+
cookie **name or value**; `standdown` matches **names only**. Both live adopter
|
|
205
|
+
tokens (`lsclick_mid`, `linkshare`) are real cookie *names*, so name-only still
|
|
206
|
+
catches them — name-only merely drops the old code's value-substring
|
|
207
|
+
over-matches. No fix.
|
|
208
|
+
- **eBay unconditional block (dangerous-more-permissive, MUST FIX).** The old
|
|
209
|
+
`disable_domains ['ebay.com','ebay.']` stood down on *every* eBay host;
|
|
210
|
+
`ebayEpnPolicy` alone only stands down on eBay tracking params/referrers, so a
|
|
211
|
+
param-less eBay page would **activate**. Fix: add the eBay `disableHosts` regex
|
|
212
|
+
shown in Phase 2. That restores the unconditional block and makes it unliftable.
|
|
213
|
+
- **`ignore_param` scope (dangerous-more-permissive if mis-set).** Covered above:
|
|
214
|
+
pin `selfExemptionScope: 'policy'`.
|
|
215
|
+
- **Self-click lift gap (needs-human-decision).** The old `ignore_param` actively
|
|
216
|
+
*cleared* an already-active CJ 60-minute stand-down so an adopter self-click could
|
|
217
|
+
re-win attribution. `standdown` is **monotone** — it never lifts an active
|
|
218
|
+
stand-down (Invariant, Phase 4). This is safe for never-hijacking but costs
|
|
219
|
+
adopter self-attribution in the CJ overlap window. Config cannot close it; a human
|
|
220
|
+
must accept the loss or build an out-of-library special case.
|
|
221
|
+
- **Amazon / Wayfair (needs-human-decision).** Both resolved by *matching current
|
|
222
|
+
behavior*: exclude `amazonPolicy`, omit the Wayfair block.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Phase 4 — GUARD the invariants (agent constraints)
|
|
227
|
+
|
|
228
|
+
These are the library's public commitments. In a migration they double as **hard
|
|
229
|
+
constraints on you, the agent**. If any change you are about to make would
|
|
230
|
+
violate one, stop and flag it instead.
|
|
231
|
+
|
|
232
|
+
- **No network call in the decision path (I1).** The decision must be local and
|
|
233
|
+
synchronous. The old code may have *fetched* its policy (the adopter fetched
|
|
234
|
+
`/api/stand-down-policy`); the migrated path takes a **statically imported**
|
|
235
|
+
`policies` array. Any policy freshness mechanism (signed refresh, a separate
|
|
236
|
+
shadow fetch) must run **outside** the decision path and must never edit the
|
|
237
|
+
live decision inline. Adding `webRequest`/`declarativeNetRequest` capture is a
|
|
238
|
+
manifest change requiring separate sign-off — do not slip it in.
|
|
239
|
+
- **Fail toward standing down (I3).** Unknown, ambiguous, malformed, or storage-
|
|
240
|
+
error states must **suppress activation**, never activate. This is the opposite
|
|
241
|
+
of the old code's fail-**open** to a stale `FALLBACK_POLICY`. When wiring the
|
|
242
|
+
content adapter, treat a `degraded` non-stand-down as a stand-down
|
|
243
|
+
(`decision.standDown || decision.degraded`) — partial signal coverage fails closed.
|
|
244
|
+
- **Monotone updates only (I4).** Stand-downs only broaden/lengthen; an active
|
|
245
|
+
stand-down is never lifted, and policy refresh may not edit activation rules.
|
|
246
|
+
Do not reintroduce "clear the stand-down" behavior to match old self-click code
|
|
247
|
+
— that is the human-decision gap above, not a bug to patch.
|
|
248
|
+
- **Cookie NAMES only (I2).** Never port a rule that depends on a cookie *value*.
|
|
249
|
+
If a genuine signal lives *only* in a value and never a name, `standdown`
|
|
250
|
+
cannot express it — flag it; do not try to smuggle the value into `Signals`.
|
|
251
|
+
- **No user profiling / no remote code (I2, I6).** Signals exclude user identity;
|
|
252
|
+
policies are data, never `eval`'d or fetched-and-executed.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Phase 5 — VERIFY before you delete
|
|
257
|
+
|
|
258
|
+
Do not remove the homegrown path on faith. Prove equivalence-or-better first.
|
|
259
|
+
|
|
260
|
+
- [ ] **Characterization tests.** Before touching anything, write tests that pin
|
|
261
|
+
the *current* extension's decision on a representative URL/cookie/referrer for
|
|
262
|
+
every network and merchant in the Phase 1 inventory. These are the executable
|
|
263
|
+
spec of "what we do today." The migrated path must pass them (or the delta must
|
|
264
|
+
be a signed-off human decision, not an accident).
|
|
265
|
+
- [ ] **Shadow divergence report is clean.** Zero **dangerous-more-permissive**
|
|
266
|
+
divergences on live traffic. Every remaining disagreement is classified
|
|
267
|
+
safe-stricter or an approved human decision.
|
|
268
|
+
- [ ] **Audit grade ≥ baseline.** Run the `audit/` grader (`npx tsx
|
|
269
|
+
grade/grade.ts /path/to/unpacked-extension`) against the built extension and
|
|
270
|
+
confirm the letter grade meets or beats the Phase-3 baseline. An **A / A+**
|
|
271
|
+
means it respects existing attribution on every tested network *and* still
|
|
272
|
+
activates when allowed. A **C (inert cap)** means it stopped activating at all —
|
|
273
|
+
over-suppression, which is safe for revenue but means you've shipped dead code;
|
|
274
|
+
investigate. An **F** means it hijacked attribution somewhere — the flag must
|
|
275
|
+
not go on.
|
|
276
|
+
- [ ] **No fail-open assertions.** Add tests asserting that malformed input,
|
|
277
|
+
storage errors, and unknown networks resolve to `standDown: true` (or a
|
|
278
|
+
suppressed activation), and that a `degraded` decision is treated as
|
|
279
|
+
stand-down. The migration must not have reintroduced any fail-open path.
|
|
280
|
+
- [ ] **Flag is off by default** and the old code still present as rollback until
|
|
281
|
+
the flag has been fully on and stable in production.
|
|
282
|
+
|
|
283
|
+
Only when every box is checked and the flag has soaked at 100% do you return to
|
|
284
|
+
Phase 3, step 5 and delete the homegrown logic.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Reference
|
|
289
|
+
|
|
290
|
+
- `README.md` — full API: `selfPatterns` / `selfExemptionScope`,
|
|
291
|
+
`detection.disableHosts`, `landingParams` / `redirectDomains` /
|
|
292
|
+
`cookiePatterns`, degraded decisions, signed policy refresh, and the public
|
|
293
|
+
invariants (I1–I7).
|
|
294
|
+
- `POLICIES.md` — bundled network packs and citations. `allPolicies` is the
|
|
295
|
+
verified set; `experimentalPolicies` are opt-in.
|
|
296
|
+
- `audit/README.md` — the conformance grader (the letter grade used in Phase 5).
|
|
297
|
+
- The greenfield "install standdown" skill — for extensions with no existing
|
|
298
|
+
stand-down logic.
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
+
<img src="https://img.shields.io/badge/status-alpha-E5484D?labelColor=1C1917" alt="project status: alpha">
|
|
6
7
|
<a href="https://www.npmjs.com/package/standdown"><img src="https://img.shields.io/npm/v/standdown?color=F5A623&label=npm&labelColor=1C1917" alt="npm version"></a>
|
|
7
8
|
<a href="https://github.com/dupe-com/standdown/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/dupe-com/standdown/ci.yml?branch=main&label=CI&labelColor=1C1917" alt="CI status"></a>
|
|
8
9
|
<img src="https://img.shields.io/badge/dependencies-0-2ea043?labelColor=1C1917" alt="zero runtime dependencies">
|
|
@@ -11,6 +12,14 @@
|
|
|
11
12
|
<a href="https://affiliatecoc.org"><img src="https://img.shields.io/badge/aligned-Affiliate%20CoC-F5A623?labelColor=1C1917" alt="aligned with the Affiliate Code of Conduct"></a>
|
|
12
13
|
</p>
|
|
13
14
|
|
|
15
|
+
> [!WARNING]
|
|
16
|
+
> **Alpha — expect bugs and breaking changes.** standdown is pre-1.0 and under
|
|
17
|
+
> active development: the API may shift between minor versions and edge cases are
|
|
18
|
+
> still being found. Because it makes revenue-affecting decisions, pin your
|
|
19
|
+
> version, verify it against your own integration with the
|
|
20
|
+
> [conformance grader](./audit), and please
|
|
21
|
+
> [report anything that misbehaves](https://github.com/dupe-com/standdown/issues).
|
|
22
|
+
|
|
14
23
|
> **Your extension shouldn't steal the sale.** `standdown` detects existing
|
|
15
24
|
> affiliate attribution, suppresses competing activation, and proves the
|
|
16
25
|
> decision was made locally — never on a server.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "standdown",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Affiliate stand-down, done right, for browser extensions.",
|
|
5
5
|
"author": "Dupe (https://dupe.com)",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"dist",
|
|
28
28
|
"README.md",
|
|
29
29
|
"LICENSE",
|
|
30
|
-
"POLICIES.md"
|
|
30
|
+
"POLICIES.md",
|
|
31
|
+
"ADOPTING.md"
|
|
31
32
|
],
|
|
32
33
|
"exports": {
|
|
33
34
|
".": {
|