@ssheleg/sheleg-dev 0.5.2 → 0.7.0
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/CHANGELOG.md +347 -0
- package/README.md +101 -1
- package/SECURITY.md +190 -52
- package/bin/sheleg-dev.js +18 -0
- package/package.json +2 -2
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/sheleg-dev/hooks/hooks.json +29 -0
- package/plugins/sheleg-dev/hooks/lib/moneygate.js +587 -0
- package/plugins/sheleg-dev/hooks/money-gate.js +68 -0
- package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +24 -75
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/README.md +76 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/assert-dedup-contract.mjs +249 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/manifest.json +75 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-capi-from-thank-you-page.json +32 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-capi-server.json +26 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-pixel-browser.json +12 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/reference-emitter.mjs +129 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +42 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +159 -2
- package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +68 -0
- package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +349 -10
- package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +31 -13
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +48 -104
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/README.md +110 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/assert-money-invariants.mjs +404 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/charge-refunded-partial.json +52 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/charge-refunded-remainder.json +63 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-async-payment-failed.json +43 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-completed-async-unpaid.json +43 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-completed-paid.json +42 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle-next-period.json +70 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle-redelivery.json +70 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle.json +61 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-update-proration.json +90 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/manifest.json +172 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/reference-handler.mjs +315 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +27 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +38 -2
- package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +43 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// A purchase emitter that satisfies the deduplication contract this skill states, and the
|
|
2
|
+
// same emitter with one rule removed at a time.
|
|
3
|
+
//
|
|
4
|
+
// Two call sites, and which one may report a purchase is the whole invariant:
|
|
5
|
+
//
|
|
6
|
+
// onChargeCleared(purchase) <- the webhook handler, the only place that knows the money
|
|
7
|
+
// moved. Sends the Conversions API event.
|
|
8
|
+
// onThankYouPage(purchase) <- the browser. Fires the pixel with the id the server will
|
|
9
|
+
// reuse, and reports nothing on its own authority.
|
|
10
|
+
//
|
|
11
|
+
// `without: ['shared-event-id']` and friends delete the lines a generated integration
|
|
12
|
+
// routinely omits; `fixtures/README.md` shows the code each flag removes. Nothing here
|
|
13
|
+
// reaches the network — the sink collects what WOULD have been sent, which is what the
|
|
14
|
+
// assertions read. No access token appears in this directory at all.
|
|
15
|
+
|
|
16
|
+
export const RULES = Object.freeze([
|
|
17
|
+
'shared-event-id',
|
|
18
|
+
'exact-event-name',
|
|
19
|
+
'webhook-sourced',
|
|
20
|
+
'keep-browser-event',
|
|
21
|
+
'hashed-identifiers',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** Meta compares `event_name` as well as the id, and it is the half that gets missed. */
|
|
25
|
+
export const EVENT_NAME = 'Purchase';
|
|
26
|
+
|
|
27
|
+
export function createSink() {
|
|
28
|
+
return { pixel: [], capi: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function customData(purchase) {
|
|
32
|
+
return {
|
|
33
|
+
value: purchase.value,
|
|
34
|
+
currency: purchase.currency,
|
|
35
|
+
content_ids: purchase.contentIds,
|
|
36
|
+
content_type: 'product',
|
|
37
|
+
num_items: purchase.numItems,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createEmitter(sink, options = {}) {
|
|
42
|
+
const without = new Set(options.without || []);
|
|
43
|
+
for (const rule of without) {
|
|
44
|
+
if (!RULES.includes(rule)) throw new Error(`unknown rule: ${rule}`);
|
|
45
|
+
}
|
|
46
|
+
const has = (rule) => !without.has(rule);
|
|
47
|
+
let generated = 0;
|
|
48
|
+
|
|
49
|
+
/** One id, generated once at session creation, carried by both sides. */
|
|
50
|
+
function eventIdFor(purchase) {
|
|
51
|
+
if (has('shared-event-id')) return purchase.conversionEventId;
|
|
52
|
+
generated += 1;
|
|
53
|
+
return `evtid_generated_at_emission_${generated}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function eventNameFor(side) {
|
|
57
|
+
if (has('exact-event-name')) return EVENT_NAME;
|
|
58
|
+
// The trap: an id that matches and a name that does not deduplicates nothing.
|
|
59
|
+
return side === 'server' ? EVENT_NAME.toLowerCase() : EVENT_NAME;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function userData(purchase) {
|
|
63
|
+
const identifiers = has('hashed-identifiers')
|
|
64
|
+
? { em: [purchase.emailSha256], external_id: [purchase.externalId] }
|
|
65
|
+
: { em: [purchase.email], external_id: [purchase.externalId] };
|
|
66
|
+
return {
|
|
67
|
+
...identifiers,
|
|
68
|
+
client_ip_address: purchase.clientIpAddress,
|
|
69
|
+
client_user_agent: purchase.clientUserAgent,
|
|
70
|
+
fbp: purchase.fbp,
|
|
71
|
+
fbc: purchase.fbc,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function capiBody(purchase) {
|
|
76
|
+
return {
|
|
77
|
+
data: [{
|
|
78
|
+
event_name: eventNameFor('server'),
|
|
79
|
+
event_time: purchase.eventTime,
|
|
80
|
+
event_id: eventIdFor(purchase),
|
|
81
|
+
event_source_url: purchase.eventSourceUrl,
|
|
82
|
+
action_source: 'website',
|
|
83
|
+
user_data: userData(purchase),
|
|
84
|
+
custom_data: customData(purchase),
|
|
85
|
+
}],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
/** The webhook handler. `cleared` is the provider's word, not the browser's. */
|
|
91
|
+
onChargeCleared(purchase) {
|
|
92
|
+
if (purchase.cleared !== true) {
|
|
93
|
+
// Reached only by a caller that guessed. The gate below is the one that matters.
|
|
94
|
+
return { sent: false, why: 'the charge has not cleared' };
|
|
95
|
+
}
|
|
96
|
+
sink.capi.push(capiBody(purchase));
|
|
97
|
+
return { sent: true };
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The thank-you page. It fires the pixel and NOTHING ELSE: a browser cannot know
|
|
102
|
+
* whether the charge cleared, and for an async method it usually has not.
|
|
103
|
+
*/
|
|
104
|
+
onThankYouPage(purchase) {
|
|
105
|
+
if (!has('webhook-sourced')) {
|
|
106
|
+
// The defect: reporting the purchase from the one place with no idea whether the
|
|
107
|
+
// money moved. `purchase-capi-from-thank-you-page.json` is what this sends.
|
|
108
|
+
sink.capi.push(capiBody(purchase));
|
|
109
|
+
}
|
|
110
|
+
if (has('webhook-sourced') && purchase.cleared !== true) {
|
|
111
|
+
// Not even the pixel: a Purchase fired here exists for every session that reached
|
|
112
|
+
// the page, including the ones that never paid.
|
|
113
|
+
return { fired: false, why: 'the charge has not cleared' };
|
|
114
|
+
}
|
|
115
|
+
if (!has('keep-browser-event')) {
|
|
116
|
+
// The other direction: dropping the browser event loses the click ids, the consent
|
|
117
|
+
// state and the session that only the browser carries.
|
|
118
|
+
return { fired: false, why: 'browser event removed' };
|
|
119
|
+
}
|
|
120
|
+
sink.pixel.push([
|
|
121
|
+
'track',
|
|
122
|
+
eventNameFor('browser'),
|
|
123
|
+
customData(purchase),
|
|
124
|
+
{ eventID: eventIdFor(purchase) },
|
|
125
|
+
]);
|
|
126
|
+
return { fired: true };
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -201,3 +201,45 @@ function trackEvent(name, params) {
|
|
|
201
201
|
gtag('event', name, params);
|
|
202
202
|
}
|
|
203
203
|
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## User identification
|
|
208
|
+
|
|
209
|
+
### Cross-Platform Identification Strategy
|
|
210
|
+
|
|
211
|
+
| Platform | Method | When | What it enables |
|
|
212
|
+
|---|---|---|---|
|
|
213
|
+
| GA4 | `user_id` via `gtag('config', TAG_ID, { user_id })` | After login/onboarding | Cross-device tracking, audience building |
|
|
214
|
+
| GA4 | Enhanced Conversions via `gtag('set', 'user_data', { email })` | After login/onboarding | Better conversion attribution |
|
|
215
|
+
| Meta Pixel | Advanced Matching via `fbq('init', PIXEL_ID, { em, fn })` | After login/onboarding | Better conversion attribution, larger custom audiences |
|
|
216
|
+
| Mixpanel | `alias()` + `identify()` | After login/onboarding | Merge anonymous → identified user profiles |
|
|
217
|
+
|
|
218
|
+
**All identification calls should be placed in a single identification component** that runs in the dashboard/authenticated layout. This ensures:
|
|
219
|
+
- User data is sent once per session
|
|
220
|
+
- All platforms receive identification data at the same time
|
|
221
|
+
- Pre-login anonymous events merge into the identified profile
|
|
222
|
+
|
|
223
|
+
### Alias vs Identify (Mixpanel)
|
|
224
|
+
|
|
225
|
+
- `alias(userId, anonymousId)` — creates a permanent link between the anonymous and real user ID. Must be called **exactly once** per user, **before** `identify()`. Use a localStorage flag to prevent duplicate calls.
|
|
226
|
+
- `identify(userId)` — sets the user ID for all subsequent events.
|
|
227
|
+
|
|
228
|
+
### Timing
|
|
229
|
+
|
|
230
|
+
Fire identification in a `useEffect` in the authenticated layout:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
// 1. Mixpanel alias + identify
|
|
235
|
+
// 2. GA4 user_id + Enhanced Conversions
|
|
236
|
+
// 3. Meta Advanced Matching
|
|
237
|
+
}, [userId, email, name]);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
Moved out of `SKILL.md` on 2026-08-16: the body was 5273 tokens against a
|
|
243
|
+
< 5000 budget, and identity is the other half of the question this file already
|
|
244
|
+
owns — an event name says what happened, identity says who it happened to.
|
|
245
|
+
|
|
@@ -9,9 +9,13 @@ setup snippet and the traps.
|
|
|
9
9
|
## Contents
|
|
10
10
|
|
|
11
11
|
- [Parameter object properties](#parameter-object-properties) — what Meta accepts per event
|
|
12
|
+
- [Standard events](#standard-events) — the seventeen, and the four that carry a funnel
|
|
12
13
|
- [Firing events](#firing-events) — the wrapper and its consent gate
|
|
13
|
-
- [Advanced matching](#advanced-matching) — hashed identifiers
|
|
14
|
-
- [
|
|
14
|
+
- [Advanced matching](#advanced-matching) — hashed identifiers
|
|
15
|
+
- [What must never be sent](#what-must-never-be-sent) — the categories that are a terms breach, not a tuning choice
|
|
16
|
+
- [Deduplication with the Conversions API](#deduplication-with-the-conversions-api) — the contract that stops one purchase counting twice
|
|
17
|
+
- [LinkedIn conversion tracking](#linkedin-conversion-tracking) — Insight Tag, Campaign Manager rules, `lintrk`
|
|
18
|
+
- [Meta Events Manager verification](#meta-events-manager-verification) — reading it back
|
|
15
19
|
|
|
16
20
|
### Parameter Object Properties
|
|
17
21
|
|
|
@@ -28,6 +32,22 @@ setup snippet and the traps.
|
|
|
28
32
|
| `predicted_ltv` | float | Predicted lifetime value (for Subscribe) |
|
|
29
33
|
| `status` | boolean | Registration status (for CompleteRegistration) |
|
|
30
34
|
|
|
35
|
+
### Standard Events
|
|
36
|
+
|
|
37
|
+
Meta defines 17 standard events. The most important for SaaS conversion funnels:
|
|
38
|
+
|
|
39
|
+
| Event | When | Parameters | Notes |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| `PageView` | Every page load | (automatic from base code) | Fires from the init script |
|
|
42
|
+
| `CompleteRegistration` | User signs up | `content_name` (method), `status: true` | Separate from GA4 `sign_up` |
|
|
43
|
+
| `InitiateCheckout` | Checkout started | `value`, `currency`, `content_name`, `content_category`, `num_items` | |
|
|
44
|
+
| `Purchase` | Purchase completed | `value` (required), `currency` (required), `content_name`, `content_category`, `content_type`, `contents[]` | `value` + `currency` are **mandatory** for optimisation |
|
|
45
|
+
| `Subscribe` | Recurring subscription | `value`, `currency`, `predicted_ltv` | Distinct from one-time Purchase |
|
|
46
|
+
| `Lead` | Lead form submitted | (optional params) | |
|
|
47
|
+
| `ViewContent` | Key content viewed | `content_name`, `content_category` | |
|
|
48
|
+
| `AddToCart` | Item added to cart | `content_name`, `value`, `currency` | |
|
|
49
|
+
| `Search` | Search performed | `search_string` | |
|
|
50
|
+
|
|
31
51
|
### Firing Events
|
|
32
52
|
|
|
33
53
|
```javascript
|
|
@@ -75,6 +95,143 @@ export function setFbAdvancedMatching(data: { em?: string; fn?: string }) {
|
|
|
75
95
|
}
|
|
76
96
|
```
|
|
77
97
|
|
|
98
|
+
### What must never be sent
|
|
99
|
+
|
|
100
|
+
Advanced Matching is the one place a tracking integration can breach Meta's terms
|
|
101
|
+
by accident, because the wrapper accepts whatever you hand it and hashes it
|
|
102
|
+
without judgement. SHA-256 does **not** make a value safe to send: hashing is a
|
|
103
|
+
matching mechanism, not a permission. That the wrapper receives a hash at all is checkable:
|
|
104
|
+
`identifiers-reach-the-server-hashed` in `fixtures/assert-dedup-contract.mjs` requires every
|
|
105
|
+
`user_data.em` entry to be 64 hex characters and refuses anything with an `@` in it. What may
|
|
106
|
+
be hashed in the first place is the table below, and no assertion decides it.
|
|
107
|
+
|
|
108
|
+
Meta's Business Tools Terms prohibit sharing data that "includes or is based on"
|
|
109
|
+
any of these, and the prohibition covers the Pixel, the Conversions API, the
|
|
110
|
+
Facebook SDK for App Events, Offline Conversions and the App Events API alike:
|
|
111
|
+
|
|
112
|
+
| Never send | Why it is not a judgement call |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Health information | Named in the terms. Meta also runs a signals-filtering mechanism that drops data it categorises as potentially health-related before it reaches ads ranking — so the data is both a breach and useless |
|
|
115
|
+
| Financial information | Named in the terms |
|
|
116
|
+
| Consumer report information | Named in the terms |
|
|
117
|
+
| Social security numbers, credit card numbers | Named as identifiers Meta does not permit |
|
|
118
|
+
| Data from or about children under 13 | Named in the terms |
|
|
119
|
+
| Anything sensitive under applicable law or industry guidance | The terms defer to local definitions, so the list above is a floor and not a ceiling |
|
|
120
|
+
|
|
121
|
+
**The event NAME is covered too**, and this is the half integrations miss: "the
|
|
122
|
+
names you choose and criteria you establish for your events, conversions, and any
|
|
123
|
+
custom audiences you create must not reflect, imply or be based on any category of
|
|
124
|
+
sensitive information." A `trackCustom('DiabetesPlanPurchase')` is a breach with
|
|
125
|
+
an empty parameter object.
|
|
126
|
+
|
|
127
|
+
**The practical rule:** the eleven Advanced Matching keys — `em`, `fn`, `ln`,
|
|
128
|
+
`ph`, `external_id`, `ge`, `db`, `ct`, `st`, `zp`, `country` — are the whole
|
|
129
|
+
allowed surface. Anything you were about to add beside them is the thing to stop
|
|
130
|
+
and check.
|
|
131
|
+
|
|
132
|
+
Source: Meta Business Tools Terms and the Sensitive Health Information help
|
|
133
|
+
centre, read 2026-08-16. Re-read before quoting: the categories move.
|
|
134
|
+
|
|
135
|
+
### Deduplication with the Conversions API
|
|
136
|
+
|
|
137
|
+
**Send both, and let Meta discard one.** Browser and server events are not an
|
|
138
|
+
either/or — the pixel is blocked often enough that server-side is the reliable
|
|
139
|
+
source, and the browser event carries signals the server cannot see. The whole
|
|
140
|
+
job is making sure the pair collapses into one conversion instead of two.
|
|
141
|
+
|
|
142
|
+
**Two fields must match, not one.** Meta compares:
|
|
143
|
+
|
|
144
|
+
| Pixel | Conversions API |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `eventID` | `event_id` |
|
|
147
|
+
| `event` (the event name) | `event_name` |
|
|
148
|
+
|
|
149
|
+
Both. An integration that generates a shared `event_id` and lets the two sides
|
|
150
|
+
disagree on the name — `Purchase` in the browser, `purchase` on the server — has
|
|
151
|
+
two events that will never deduplicate, and the revenue is counted twice. This is
|
|
152
|
+
the single most common way this goes wrong, because the `event_id` half is
|
|
153
|
+
obvious and the `event_name` half is not.
|
|
154
|
+
|
|
155
|
+
**The window is 48 hours**, and it runs from when Meta receives the *first* event
|
|
156
|
+
carrying a given `event_id` — not from when the purchase happened. A server event
|
|
157
|
+
retried out of a dead-letter queue two days later is a second conversion.
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// One id, generated once, used by both sides.
|
|
161
|
+
const eventId = crypto.randomUUID();
|
|
162
|
+
|
|
163
|
+
// Browser
|
|
164
|
+
fbq('track', 'Purchase', { value: 30.00, currency: 'USD' }, { eventID: eventId });
|
|
165
|
+
|
|
166
|
+
// Server — the SAME event_name string, not a variant
|
|
167
|
+
await sendCapi({
|
|
168
|
+
event_name: 'Purchase', // must equal the pixel's 'Purchase' exactly
|
|
169
|
+
event_id: eventId,
|
|
170
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
171
|
+
action_source: 'website',
|
|
172
|
+
user_data: { /* hashed */ },
|
|
173
|
+
custom_data: { value: 30.00, currency: 'USD' },
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**The alternative method, and its trap.** Where you cannot thread an id through,
|
|
178
|
+
Meta will also deduplicate on `event_name` plus `fbp` and/or `external_id`, held
|
|
179
|
+
consistent across both sources. It is weaker in a specific direction: *"server
|
|
180
|
+
events will not be discarded if a browser event has not been received in the past
|
|
181
|
+
48 hours, even if an identical browser event arrives after the server event."*
|
|
182
|
+
So the browser event must arrive **first**. For a purchase confirmed by a webhook
|
|
183
|
+
— which is the pattern this skill teaches, because the webhook is the payment —
|
|
184
|
+
the server event usually arrives first, and this method then does nothing. Use
|
|
185
|
+
`event_id`.
|
|
186
|
+
|
|
187
|
+
Neither method deduplicates within a single source: two pixel fires of the same
|
|
188
|
+
purchase are two conversions no matter what.
|
|
189
|
+
|
|
190
|
+
**Proved, not asserted.** `fixtures/purchase-pixel-browser.json` and
|
|
191
|
+
`fixtures/purchase-capi-server.json` are the two payloads one purchase produces, and
|
|
192
|
+
`fixtures/assert-dedup-contract.mjs` compares them: `pixel-and-capi-carry-one-event-name`
|
|
193
|
+
fails an emitter that sends `Purchase` from the browser and a lowercased variant from the
|
|
194
|
+
server, which is the half of this contract that gets missed. The shape itself is a ratchet —
|
|
195
|
+
`the-shipped-capi-fixture-is-what-a-correct-emitter-sends` requires the emitted body to equal
|
|
196
|
+
the fixture byte for byte, so **the fixtures are the reference output rather than an
|
|
197
|
+
illustration**.
|
|
198
|
+
|
|
199
|
+
Source: [Deduplicate pixel and Conversions API
|
|
200
|
+
events](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events),
|
|
201
|
+
read 2026-08-16.
|
|
202
|
+
|
|
203
|
+
### LinkedIn conversion tracking
|
|
204
|
+
|
|
205
|
+
**Env var:** `NEXT_PUBLIC_LINKEDIN_PARTNER_ID` (numeric string).
|
|
206
|
+
|
|
207
|
+
The Insight Tag is consent-gated on the same wrapper as the Meta Pixel — it loads
|
|
208
|
+
`insight.min.js` and ships a `noscript` pixel fallback.
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
_linkedin_partner_id = "PARTNER_ID";
|
|
212
|
+
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
|
|
213
|
+
window._linkedin_data_partner_ids.push(_linkedin_partner_id);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Conversions are configured in Campaign Manager, not in code.** The Insight Tag
|
|
217
|
+
tracks page views by itself, and URL rules match those page loads to conversions:
|
|
218
|
+
|
|
219
|
+
1. Campaign Manager → Analyze → Conversion tracking → Create conversion
|
|
220
|
+
2. Define the URL rules (`/thank-you`, `/onboarding`)
|
|
221
|
+
3. The tag matches page loads against them
|
|
222
|
+
|
|
223
|
+
For an event-based conversion rather than a URL one:
|
|
224
|
+
`window.lintrk('track', { conversion_id: 123456 })`.
|
|
225
|
+
|
|
226
|
+
**LinkedIn has no equivalent of the `event_id` contract above.** There is no
|
|
227
|
+
browser/server pair to deduplicate here, which is why a URL rule that matches a
|
|
228
|
+
page a user can refresh counts every refresh — put the rule on a page reached once,
|
|
229
|
+
or use `lintrk` from the code path that actually completed the action.
|
|
230
|
+
|
|
231
|
+
**Docs:**
|
|
232
|
+
- [LinkedIn Insight Tag setup](https://www.linkedin.com/help/lms/answer/a418880)
|
|
233
|
+
- [LinkedIn conversion tracking](https://www.linkedin.com/help/lms/answer/a423304)
|
|
234
|
+
|
|
78
235
|
### Meta Events Manager Verification
|
|
79
236
|
|
|
80
237
|
1. Install the **Meta Pixel Helper** Chrome extension
|
|
@@ -45,6 +45,7 @@ and anything else that issues an invoice and calls you back.
|
|
|
45
45
|
- [Reconciliation fields](#reconciliation-fields)
|
|
46
46
|
- [Crediting: the amount waterfall](#crediting-the-amount-waterfall)
|
|
47
47
|
- [Refunds and AML holds](#refunds-and-aml-holds)
|
|
48
|
+
- [The test/live credential boundary](#the-testlive-credential-boundary)
|
|
48
49
|
- [Local development](#local-development)
|
|
49
50
|
- [Test matrix](#test-matrix)
|
|
50
51
|
- [Security checklist](#security-checklist)
|
|
@@ -309,6 +310,68 @@ Crypto has no chargeback, which people mistake for "no reversals".
|
|
|
309
310
|
- **Never auto-refund from the webhook.** Route holds and refunds to a queue a
|
|
310
311
|
human can see.
|
|
311
312
|
|
|
313
|
+
**And make it a precondition, not a paragraph.** The three lines above were advice for
|
|
314
|
+
four releases: an agent that never read them, and a shell nobody read, were unaffected by
|
|
315
|
+
all three. This pack now ships a `PreToolUse` gate —
|
|
316
|
+
`plugins/sheleg-dev/hooks/money-gate.js` — that refuses a refund, a payout, closing a
|
|
317
|
+
dispute, an export of `HELEKET_API_KEY` in a run declaring `test`, a live `sk_live_…` key
|
|
318
|
+
and `SKIP_BILLING=true` in production, until the authorised person has signed that
|
|
319
|
+
category off for the session. The pack's `README.md` carries the categories and how one is
|
|
320
|
+
authorised. In your application the same rule is a queue and a human; in the agent's
|
|
321
|
+
shell it is the hook.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## The test/live credential boundary
|
|
326
|
+
|
|
327
|
+
**Establish what the provider actually offers before designing around it.** Card
|
|
328
|
+
processors hand you two parallel worlds — Stripe gives a whole second account and stamps
|
|
329
|
+
the environment into the key itself (`sk_test_` / `sk_live_`). Most crypto gateways do
|
|
330
|
+
not. Answer these three, from the provider's documentation, and write the answers down:
|
|
331
|
+
|
|
332
|
+
1. **Is there a separate test credential?** Not "a test mode" — a *different secret*.
|
|
333
|
+
2. **Can the environment be read from the key?** A prefix, a scope, anything.
|
|
334
|
+
3. **Does the key also sign webhooks?** If yes, it cannot be scoped down: everything that
|
|
335
|
+
verifies a callback can also create a charge.
|
|
336
|
+
|
|
337
|
+
The answers decide the design, and the worst combination is common: one key, no marker,
|
|
338
|
+
"test mode" as a **toggle on the merchant account**. Then the same secret creates play
|
|
339
|
+
invoices before somebody moves a dashboard switch and real ones after — with no deploy and
|
|
340
|
+
no signal to any machine holding it.
|
|
341
|
+
|
|
342
|
+
**Build the strongest control the provider allows, in this order:**
|
|
343
|
+
|
|
344
|
+
1. **A separate sandbox account**, so the credential on a dev machine *cannot* reach
|
|
345
|
+
production. This is the only control that still works after everyone has forgotten why
|
|
346
|
+
it is there — which is the whole argument for preferring it to a warning in a README.
|
|
347
|
+
2. **A declared environment plus a boot assertion**, when there is no second credential.
|
|
348
|
+
Keep the declaration in a variable **separate from the secret** (`<PROVIDER>_ENV=test|
|
|
349
|
+
production`): one variable that both carries the key and names its environment cannot
|
|
350
|
+
be checked against itself, two can. Pin the production identity with something
|
|
351
|
+
non-secret — the merchant id, or a truncated hash of the live key — and refuse
|
|
352
|
+
**both** mismatches at **startup**:
|
|
353
|
+
|
|
354
|
+
- a live credential declared test — the obvious loss;
|
|
355
|
+
- a test credential declared live — the quiet one, where invoices settle to a merchant
|
|
356
|
+
nobody reconciles and nothing errors until revenue is missing.
|
|
357
|
+
|
|
358
|
+
Assert at module load, not in the payment handler: a run that merely *holds* the key
|
|
359
|
+
must fail too, and it is agent and CI runs that hold keys without ever charging
|
|
360
|
+
anything. And when the check cannot tell — nothing pinned — refuse on the test side.
|
|
361
|
+
"Could not prove it was safe" must never read as "it was safe".
|
|
362
|
+
3. **Write down what is left.** Where the provider offers no test credential, a developer
|
|
363
|
+
holds a production one and the assertion narrows that window without closing it. An
|
|
364
|
+
unavoidable exposure that is named is a different object from one that is silent: the
|
|
365
|
+
first gets rotated, monitored and revisited, the second gets rediscovered by an
|
|
366
|
+
incident.
|
|
367
|
+
|
|
368
|
+
The same rule applies to the mock path below: `SKIP_BILLING=true` in production is not a
|
|
369
|
+
shortcut, it is a free-money path, and the assertion is what refuses it.
|
|
370
|
+
|
|
371
|
+
Worked end to end for one gateway with no test credential at all —
|
|
372
|
+
[`references/heleket-provider.md`](references/heleket-provider.md), *The test/live
|
|
373
|
+
boundary* and *Residual exposure*.
|
|
374
|
+
|
|
312
375
|
---
|
|
313
376
|
|
|
314
377
|
## Local development
|
|
@@ -363,6 +426,9 @@ Cover these, and plant a defect against each before believing the green:
|
|
|
363
426
|
- [ ] CSRF exempted for exactly one path, by exact match
|
|
364
427
|
- [ ] Entitlement never granted from a client-side redirect
|
|
365
428
|
- [ ] API key and merchant id from environment, never in the repository
|
|
429
|
+
- [ ] A sandbox account is used where the provider offers one, or the reason it cannot be is written down
|
|
430
|
+
- [ ] The declared environment is a variable separate from the secret, and a boot assertion refuses **both** mismatches
|
|
431
|
+
- [ ] The assertion runs at module load, so a run that only holds the key fails too
|
|
366
432
|
- [ ] Callback URL is HTTPS in every environment that is not a local tunnel
|
|
367
433
|
- [ ] `SKIP_BILLING` asserted impossible in production
|
|
368
434
|
- [ ] Reconciliation fields written at settlement time
|
|
@@ -382,3 +448,5 @@ Cover these, and plant a defect against each before believing the green:
|
|
|
382
448
|
| "Paid" users with no product | credited from the browser redirect |
|
|
383
449
|
| Half the payments land as `wrong_amount` | no conversion buffer |
|
|
384
450
|
| Cannot answer "how much did we net" | commission and `merchantAmount` never stored |
|
|
451
|
+
| A dev machine, a CI job or an agent run is holding the production key | no separate credential, and no assertion binding the key to a declared environment |
|
|
452
|
+
| Real invoices in a merchant dashboard nobody watches | a staging credential declared live — the mismatch nothing refused |
|