@ssheleg/sheleg-dev 0.10.4 → 0.11.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 CHANGED
@@ -4,6 +4,106 @@ All notable changes to this project are documented here.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.11.0 — a trigger literal gets one home, and four facts get their dates
8
+
9
+ The 2026-08-29 skill audit found the pack's two Google skills advertising the same
10
+ end-user sign-in triggers, one skill re-teaching another's contract more weakly, and
11
+ three external facts stated without the date they were true on. All of it shipped green
12
+ through a 23-check validator, because none of it was a check yet.
13
+
14
+ - **`google-auth` and `google-signin` no longer compete for the same prompt** (audit
15
+ DEV-01, HIGH). Both front-matter descriptions carried `"google login"`, `"GIS"`,
16
+ `"Sign In with Google"` and «вход через Google» — four literal collisions between the
17
+ skill that owns end-user sign-in and the skill that owns the server library surface,
18
+ with which one loads left to the host's guess. The end-user set is stripped from
19
+ `google-auth`, whose description now speaks the server vocabulary (ADC, service
20
+ accounts and their keys, Workload Identity Federation, `verifyIdToken`); the
21
+ cross-deferral sentence stays. A new validator check,
22
+ `check_trigger_literals_have_one_home`, compares the quoted trigger literals of every
23
+ skill pair and refuses a collision — watched failing in both directions (a re-planted
24
+ `"google login"`, and a stale entry in `TRIGGERS_SHARED_BY_DESIGN`, the enumerated
25
+ exemption that carries the one deliberate share: `webhook signature`, which the
26
+ umbrella routes to `stripe-billing`). One new negative self-test in CI; the local
27
+ floor moves 42 → 43. The verdict line moves 23 → 24 checks.
28
+ - **`google-auth` stops re-teaching ID-token verification with a weaker contract**
29
+ (DEV-02). Its section 3 taught the sign-in verification omitting the `nonce` binding
30
+ and `email_verified` — the two checks `google-signin`'s checklist adds above the
31
+ library call, drifting live under a section that duplicated the other skill's core
32
+ job. Cut to the bare `verifyIdToken` / `verify_oauth2_token` call plus an explicit
33
+ statement of what the call does NOT check and where the sign-in contract lives. The
34
+ partial `g_csrf_token` bullet in Security Best Practices is likewise now a pointer.
35
+ - **The Stripe `apiVersion` pins carry their date** (DEV-03). `2026-07-29.dahlia` was
36
+ pinned undated in a document that itself says the API moves monthly — and it was
37
+ already one release behind: checked 2026-08-30 against `docs.stripe.com/changelog`,
38
+ where `2026-08-26.dahlia` is current. Both pin sites now say when they were true and
39
+ send the reader to the changelog before pinning.
40
+ - **The crypto `sign()` block is labeled as ONE provider's scheme** (DEV-04). The
41
+ SKILL taught `md5(base64(json) + apiKey)` under a provider-neutral heading two
42
+ screens after claiming the section holds for four providers — it is Heleket's scheme
43
+ alone. Checked 2026-08-30: Coinbase Commerce signs HMAC-SHA256
44
+ (`X-CC-Webhook-Signature`), NOWPayments HMAC-SHA512 (`x-nowpayments-sig`), BTCPay
45
+ HMAC-SHA256 (`BTCPay-Sig`). The pattern transfers; the algorithm does not, and the
46
+ intro now says which is which.
47
+ - **`allow_enhanced_conversions` moves to the tag it belongs on** (DEV-05). The setup
48
+ step put the flag on the GA4 `G-` config, where it enables nothing for Google Ads;
49
+ where it appears at all it belongs on the `AW-` config command. Checked 2026-08-30:
50
+ Google's current setup docs (`support.google.com/google-ads/answer/9888145`,
51
+ `…/13258081`) no longer document the flag — the live mechanism is the account-level
52
+ toggle plus `gtag('set', 'user_data', …)` — and the step now states both facts.
53
+ - **`ad-tracking`'s UTM Attribution section moved into
54
+ `references/event-tracking.md`.** The DEV-05 fix pushed the body past the 4750-token
55
+ house working limit (23 tokens of headroom going in), and the house answer at the
56
+ limit is a split, not a trim — same seam as v0.10.0's cancellation split.
57
+ Body ~4753 tokens after the move; `stripe-billing`'s recomputed numbers in
58
+ `docs/evals/stripe-billing.md` moved with its dated pins (4737 tokens, 383 lines).
59
+ - **The social card now names all five trades** (umbrella deferral, 2026-08-29). The
60
+ committed `docs/assets/social-preview.png` is compared pixel-for-pixel by the
61
+ umbrella's site test against the card it generates from its `skills.json` role cell,
62
+ and the role grows `errors` for the seventh skill. Regenerated with the umbrella's
63
+ own generator from the exact cell it will adopt — `integrations: money in, tracking,
64
+ errors, sign-in, speed` — verified by reproducing the previous committed card
65
+ byte-for-byte from the previous cell first. README and `marketplace.json` carry the
66
+ same five-trade phrasing. Measured while regenerating: at scale 3 the new eyebrow
67
+ paints to 1 px of the canvas edge, because the umbrella's `fitScale` measures without
68
+ the tracking `drawText` adds — nothing is truncated, and the fix (if wanted) belongs
69
+ in `og-card.js`, not here.
70
+
71
+ ## v0.10.5 — the installers refuse the shadow they used to write
72
+
73
+ Both install channels — `npx @ssheleg/sheleg-dev` and `install.sh` — now consult the
74
+ target home's `~/.claude/plugins/installed_plugins.json` before writing anything under
75
+ `~/.claude/skills/`. If the sheleg-dev **plugin** is installed there, the install is
76
+ **refused with exit 3**: a plain copy beside a plugin shadows the plugin's skill of the
77
+ same name and serves the frozen version forever, and until this release neither channel
78
+ looked at all. Reproduced live in the family on 2026-08-29: a bare
79
+ `npx @ssheleg/telegram-dev` shipped three shadows while the plugin was enabled.
80
+
81
+ - `installed_plugins.json` is the signal, because it is the record of what is actually
82
+ installed. A check keyed on the `plugins/marketplaces/<name>` directory alone — the
83
+ shape other family members carried — is the fail-open class: a marketplace added from
84
+ a local `directory` source has no dir there, and plugin names differ from marketplace
85
+ names. The directory is kept only as a fallback signal.
86
+ - The refusal names its remedy with the **real spec read from the JSON**
87
+ (`claude plugin marketplace update sheleg-dev`, `claude plugin update <spec>`), plus
88
+ the family launcher line, and offers `--force` as the explicit override for running
89
+ two channels deliberately.
90
+ - An absent or corrupt `installed_plugins.json` reads as "no plugin": the check fails
91
+ open and never crashes an install — the fresh HOME is the common case.
92
+ - New suite `test/installer_test.js` (11 cases, both channels, throwaway HOMEs), wired
93
+ into `npm test` and CI. **Watched failing first**: run against the pre-refusal
94
+ installers it went red on 7 of 11 cases, each showing the shadow landing at exit 0.
95
+ - Both success paths now end by saying how the next version arrives — the update line —
96
+ and `install.sh` gained `--force`/usage parsing (unknown argument exits 2) to carry
97
+ the same contract as the npm channel.
98
+ - `.claude-plugin/marketplace.json` still said "Six integration skills" while seven
99
+ ship (the validator counts 7; README and SECURITY say seven). It now says seven and
100
+ names error tracking — the second hand-written count this repository has caught after
101
+ the fact, which is why the new suite derives the roster from the tree.
102
+ - `SECURITY.md`'s installer contract moved with the behaviour: the read surface now
103
+ includes the two read-only looks at the target home, and the I/O-surface grep line
104
+ count was re-measured (twelve, and it was already twelve before this change — the
105
+ "eleven" had rotted when a comment naming `rm -rf` arrived).
106
+
7
107
  ## v0.10.4 — the channel that sends the installs, on npm too
8
108
 
9
109
  - The `skills.sh` badge and the canonical `homepage` reached GitHub in the previous cycle and stopped
package/README.md CHANGED
@@ -23,7 +23,7 @@ Agent Skills standard directly, scanning `~/.agents/skills` — where `npx skill
23
23
  add` puts this pack — at rank 500.
24
24
 
25
25
  The integration layer a product reaches once it has users: **money in, tracking,
26
- sign-in, and speed.**
26
+ errors, sign-in, and speed.**
27
27
 
28
28
  Part of the [ssheleg skill family](https://github.com/ssheleg/sshlg-skills).
29
29
 
@@ -118,6 +118,12 @@ a Lighthouse score.
118
118
  npx @ssheleg/sheleg-dev
119
119
  ```
120
120
 
121
+ If the sheleg-dev **plugin** is already installed in that home, the npm installer
122
+ and `install.sh` refuse with exit 3 instead of writing plain copies — a plain
123
+ copy shadows the plugin's skill of the same name and serves the frozen version
124
+ forever. The refusal prints the plugin-channel update commands; `--force`
125
+ overrides it, deliberately.
126
+
121
127
  **Any of 70+ agents:**
122
128
 
123
129
  ```bash
package/SECURITY.md CHANGED
@@ -33,7 +33,7 @@ no socket; the greps below prove all three.
33
33
  | `plugins/sheleg-dev/hooks/money-gate.js` | 1 | Runs on a tool call, when the plugin is enabled. Requires `path` and its own decision module — nothing else. Reads stdin, writes one JSON line, exits 0 on every path. |
34
34
  | `plugins/sheleg-dev/hooks/lib/moneygate.js` | 1 | Pure. Payload and environment in, verdict out. No `require` at all, no filesystem, no clock. |
35
35
  | `bin/sheleg-dev.js` — the npm installer | 1 | Runs only when you invoke it. Node built-ins only: `fs`, `path`, `os`. |
36
- | `install.sh` — the shell installer | 1 | **Not in the tarball** — `files` ships `bin/` and `plugins/`, so this one reaches you only through a clone. Runs when you invoke it. Coreutils only: `cd`, `pwd`, `dirname`, `basename`, `mkdir`, `rm`, `cp`, `echo`. It is the destructive channel: `rm -rf` per skill, then `cp -R`. |
36
+ | `install.sh` — the shell installer | 1 | **Not in the tarball** — `files` ships `bin/` and `plugins/`, so this one reaches you only through a clone. Runs when you invoke it. Shell tools only: `cd`, `pwd`, `dirname`, `basename`, `mkdir`, `rm`, `cp`, `echo`, plus `sed` and `head` to read the plugin registry it refuses to shadow. It is the destructive channel: `rm -rf` per skill, then `cp -R`. |
37
37
  | `plugins/sheleg-dev/skills/*/fixtures/*.json` — webhook payloads | 16 | Data. Placeholder ids only; no key, token, signing secret or real customer id. |
38
38
  | `plugins/sheleg-dev/skills/*/fixtures/*.mjs` — the assertion packs and their reference implementations | 4 | Runs only when you invoke it. Reads the JSON beside it with `readFileSync` and nothing else: no `child_process`, no `fetch`, no write, no `process.env`. |
39
39
  | `plugins/sheleg-dev/skills/*/fixtures/manifest.json` and its `README.md` | 4 | Text. The invariant-to-fixture-to-document map, which `test/validate.py` reads. |
@@ -61,24 +61,32 @@ nothing but the CLI you typed.
61
61
  ## What the installers read and write
62
62
 
63
63
  Both do one thing: copy the seven skill directories out of the package into
64
- `~/.claude/skills/<name>`.
65
-
66
- - **Read:** only files inside the package.
64
+ `~/.claude/skills/<name>` — unless the sheleg-dev **plugin** is already installed
65
+ in that home, in which case both refuse with exit 3 rather than write plain
66
+ copies that would shadow the plugin's skills and serve this frozen version
67
+ forever. `--force` overrides the refusal, deliberately.
68
+
69
+ - **Read:** files inside the package, plus two read-only looks at the target
70
+ home to decide the refusal above: `~/.claude/plugins/installed_plugins.json`
71
+ (the record of installed plugins; a missing or unparsable file reads as "no
72
+ plugin", so the check fails open and never crashes an install) and whether
73
+ `~/.claude/plugins/marketplaces/sheleg-dev` exists, kept only as a fallback
74
+ signal.
67
75
  - **Write:** only `~/.claude/skills/` — seven directories, named for the seven skills.
68
76
  Nothing else on the filesystem, nothing outside `$HOME`.
69
77
  - **Network: none.** Neither file opens a socket or resolves a hostname.
70
78
  `bin/sheleg-dev.js` requires `fs`, `path` and `os` and nothing else — no
71
79
  `child_process`, no `fetch`, no `http` — so it spawns no process at all;
72
- `install.sh` runs only the coreutils named above. The grep below prints the
73
- whole surface of both files in eleven lines.
80
+ `install.sh` runs only the shell tools named above. The grep below prints the
81
+ whole surface of both files in twelve lines.
74
82
  - **No telemetry, no analytics, no phone-home.** Nothing here has anywhere to
75
83
  send anything.
76
84
 
77
85
  **One destructive difference between the two installers.** `bin/sheleg-dev.js`
78
86
  skips a skill that is already installed and says so, unless you pass `--force`
79
- (`bin/sheleg-dev.js:45-46`). `install.sh` does not ask: for each of the seven names
87
+ (`bin/sheleg-dev.js:89-90`). `install.sh` does not ask: for each of the seven names
80
88
  it runs `rm -rf` on the destination and re-copies, every time
81
- (`install.sh:21-22`). If you have hand-edited an installed skill, the shell
89
+ (`install.sh:62-63`). If you have hand-edited an installed skill, the shell
82
90
  installer deletes your edits and the node installer does not.
83
91
 
84
92
  ## Credentials
@@ -193,14 +201,22 @@ node test/moneygate_test.js
193
201
  # The command prints its own count.
194
202
  node test/fixtures_test.js
195
203
 
204
+ # Both installers against throwaway HOMEs: fresh install, rerun, --force, and the
205
+ # plugin-present refusal — exit 3, the remedy naming the real plugin spec, nothing
206
+ # written; a corrupt ~/.claude/plugins/installed_plugins.json fails open. The command
207
+ # prints its own count.
208
+ node test/installer_test.js
209
+
196
210
  # What the assertion packs cannot reach: NO OUTPUT, and grep exits 1 because it
197
211
  # matched nothing. `readFileSync` of the fixtures beside them is their whole I/O.
198
212
  grep -rnE "child_process|\bfetch\(|require\('(http|https|net|dns|tls)'\)|spawn\(|execFile|writeFileSync|appendFileSync|unlinkSync|process\.env\." \
199
213
  plugins/sheleg-dev/skills/*/fixtures/*.mjs
200
214
 
201
- # The entire I/O surface of the only two executable files: eleven lines. Three
202
- # built-in requires, the copy/mkdir/remove calls, homedir, and install.sh's
203
- # rm -rf + cp -R. No process, no socket, no network.
215
+ # The entire I/O surface of the only two executable files: twelve lines. Three
216
+ # built-in requires, the copy/mkdir/remove calls, homedir, install.sh's
217
+ # rm -rf + cp -R, and one comment naming rm -rf. No process, no socket, no
218
+ # network — the plugin-registry read that decides the shadow refusal is
219
+ # readFileSync in the node file and sed in the shell one.
204
220
  grep -nE "require|child_process|exec|spawn|fetch|socket|rm -rf|cp -R|copyFileSync|mkdirSync|rmSync|homedir" bin/sheleg-dev.js install.sh
205
221
 
206
222
  # Live-key shapes across the skill payload: one line, and it is a placeholder.
package/bin/sheleg-dev.js CHANGED
@@ -17,6 +17,44 @@ const os = require('os');
17
17
  const ROOT = path.resolve(__dirname, '..');
18
18
  const REPO = 'ssheleg/sheleg-dev';
19
19
 
20
+ // Exit codes are the contract: 0 installed or skipped, 1 corrupted package,
21
+ // 2 usage error, 3 refused — the plugin channel owns this agent (--force overrides).
22
+ const EXIT_PLUGIN_PRESENT = 3;
23
+
24
+ /**
25
+ * The plugin spec (`<name>@<marketplace>`) installed for `name` in this home,
26
+ * or null.
27
+ *
28
+ * `installed_plugins.json` is the record of what is actually installed. The
29
+ * `plugins/marketplaces/<name>` directory — the check other members of this
30
+ * family keyed on until 2026-08-29 — under-reports: a marketplace added from a
31
+ * local `directory` source has no dir there at all, and plugin names differ
32
+ * from marketplace names, so a check keyed on it stays green while the shadow
33
+ * lands. Absence and corruption both read as "no plugin": the fresh HOME is
34
+ * the common case, and an installer that crashes on a parse error refuses the
35
+ * machines that need it most.
36
+ */
37
+ function installedPluginSpec(home, name) {
38
+ try {
39
+ const raw = fs.readFileSync(
40
+ path.join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
41
+ const parsed = JSON.parse(raw);
42
+ const plugins =
43
+ parsed && typeof parsed === 'object' &&
44
+ parsed.plugins && typeof parsed.plugins === 'object'
45
+ ? parsed.plugins
46
+ : parsed;
47
+ if (!plugins || typeof plugins !== 'object') return null;
48
+ for (const spec of Object.keys(plugins)) {
49
+ if (spec === name) return `${name}@${name}`;
50
+ if (spec.startsWith(name + '@')) return spec;
51
+ }
52
+ } catch {
53
+ // missing or corrupt = no plugin — fail open on absence, never crash
54
+ }
55
+ return null;
56
+ }
57
+
20
58
  function usage() {
21
59
  console.log(`sheleg-dev installer
22
60
 
@@ -25,6 +63,12 @@ Usage:
25
63
  into ~/.claude (skip existing unless --force)
26
64
  npx @ssheleg/sheleg-dev --help
27
65
 
66
+ Exit codes:
67
+ 0 installed or skipped 2 usage error
68
+ 1 corrupted package 3 refused: the sheleg-dev PLUGIN is installed in
69
+ this home — plain copies would shadow its skills
70
+ (pass --force to write them anyway)
71
+
28
72
  Other install paths:
29
73
  Claude Code plugin: /plugin marketplace add ${REPO}
30
74
  /plugin install sheleg-dev@sheleg-dev
@@ -84,6 +128,39 @@ function main(argv) {
84
128
  }
85
129
 
86
130
  const home = os.homedir();
131
+
132
+ // One channel per agent. Plain copies under ~/.claude/skills/ beside the
133
+ // installed sheleg-dev plugin are a second listing of the same skills, and
134
+ // the stale copies win — the exact shadow this family's canon forbids.
135
+ // Refuse rather than create them, and refuse LOUDLY: a marketplaces/-dir
136
+ // check alone is the fail-open class (a directory-sourced marketplace has no
137
+ // dir there, plugin names differ from marketplace names), and a refusal that
138
+ // exits 0 reads as success to every script above it. Reproduced live
139
+ // 2026-08-29: a bare `npx @ssheleg/telegram-dev` shipped three shadows past
140
+ // exactly that hole while the plugin was enabled.
141
+ const spec = installedPluginSpec(home, 'sheleg-dev');
142
+ const marketplace = path.join(home, '.claude', 'plugins', 'marketplaces', 'sheleg-dev');
143
+ const viaMarketplaceDir = !spec && fs.existsSync(marketplace);
144
+ if ((spec || viaMarketplaceDir) && !force) {
145
+ const found = spec
146
+ ? `installed as the Claude Code plugin ${spec}\n` +
147
+ ' (declared in ~/.claude/plugins/installed_plugins.json)'
148
+ : `registered as a Claude Code marketplace\n (${marketplace})`;
149
+ console.error(
150
+ `refused: sheleg-dev is already ${found}.\n` +
151
+ ` Plain copies in ~/.claude/skills/ would shadow the plugin's\n` +
152
+ ` ${names.length} skills and serve this frozen version forever. Update the\n` +
153
+ ' plugin channel instead:\n' +
154
+ ' claude plugin marketplace update sheleg-dev\n' +
155
+ ` claude plugin update ${spec || 'sheleg-dev@sheleg-dev'}\n` +
156
+ ' Family launcher (updates every member, prunes shadow copies):\n' +
157
+ ' npx --yes sshlg-skills@latest update\n' +
158
+ ' Pass --force to write the plain copies anyway — a deliberate choice\n' +
159
+ ' to run two channels, where the stale one wins.'
160
+ );
161
+ return EXIT_PLUGIN_PRESENT;
162
+ }
163
+
87
164
  for (const name of names) {
88
165
  installOne(
89
166
  `${name} skill`,
@@ -111,6 +188,15 @@ function main(argv) {
111
188
  console.log('skills copy. To get it here, register it yourself — README.md, section');
112
189
  console.log('"The manual gate", has the settings snippet. Nothing enforces this step.');
113
190
  }
191
+ // The last line says how the next version arrives — "Installed" is not a
192
+ // complete sentence. Auto-update is off on purpose: this member composes
193
+ // with its family, and per-marketplace autoUpdate moves each member on its
194
+ // own clock, into combinations nobody tested together.
195
+ console.log(
196
+ '\nUpdates: rerun `npx @ssheleg/sheleg-dev@latest --force`, or refresh the\n' +
197
+ 'whole family with `npx --yes sshlg-skills@latest update` (every channel,\n' +
198
+ 'and it prunes plain copies that would shadow a plugin).'
199
+ );
114
200
  return 0;
115
201
  }
116
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/sheleg-dev",
3
- "version": "0.10.4",
3
+ "version": "0.11.0",
4
4
  "description": "Seven integration skills for the layer a product reaches once it has users: Stripe subscription billing and cancellation retention, crypto payments, ad and conversion tracking under Consent Mode v2, Google sign-in and the wider Google auth surface, error tracking, and Core Web Vitals. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "sheleg-dev": "bin/sheleg-dev.js"
@@ -43,8 +43,8 @@
43
43
  "skill"
44
44
  ],
45
45
  "scripts": {
46
- "test": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js",
46
+ "test": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js && node test/installer_test.js",
47
47
  "test:negatives": "python3 test/negatives.py",
48
- "test:all": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js && python3 test/negatives.py"
48
+ "test:all": "python3 test/validate.py && node test/moneygate_test.js && node test/fixtures_test.js && node test/installer_test.js && python3 test/negatives.py"
49
49
  }
50
50
  }
@@ -3,7 +3,7 @@
3
3
  "name": "sheleg-dev",
4
4
  "displayName": "Sheleg Dev",
5
5
  "description": "The integration layer a product needs once it has users: Stripe subscription billing reconciled into your own database, crypto payments that survive over-payment and duplicate webhooks, ad and conversion tracking under Consent Mode v2, Google sign-in with the account-linking guards, the wider Google auth surface, and Core Web Vitals work that moves the score, and Sentry error tracking wired so it does not forward your own credentials to a third party and so a stack trace names the commit that caused it.",
6
- "version": "0.10.4",
6
+ "version": "0.11.0",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -157,7 +157,20 @@ Both share consent state, first-party cookies, and the dataLayer pipeline.
157
157
 
158
158
  Enhanced Conversions improve attribution by matching hashed user data (email, phone) with Google accounts. Setup:
159
159
 
160
- 1. Enable in GA4 config: `allow_enhanced_conversions: true`
160
+ 1. Where the flag appears at all, it belongs on the **Google Ads config — the
161
+ `AW-` tag, never the GA4 `G-` tag** (an earlier revision of this skill put it
162
+ on the `G-` config, where it enables nothing for Ads):
163
+
164
+ ```javascript
165
+ gtag('config', 'AW-XXXXXXXXX', { allow_enhanced_conversions: true });
166
+ ```
167
+
168
+ Checked 2026-08-30: Google's current setup docs
169
+ (`support.google.com/google-ads/answer/9888145`, `…/13258081`) no longer
170
+ document the flag at all — the live mechanism is the account-level toggle
171
+ (step 3) plus `user_data` (step 2). Keep the flag if your tag already
172
+ carries it; do not copy it onto the `G-` config.
173
+
161
174
  2. Set user data before conversion events fire:
162
175
 
163
176
  ```javascript
@@ -323,32 +336,10 @@ Strategies** for `next/script` strategy choice and SPA route changes.
323
336
  the parent page's.
324
337
  ## UTM Attribution
325
338
 
326
- ### Capture Flow
327
-
328
- 1. `UtmCapture` component reads UTM params from URL on page load → stores in localStorage
329
- 2. On registration (credentials): read UTM from localStorage, send in POST body, save to DB
330
- 3. On registration (OAuth): UTM persists in localStorage through redirect → synced after return via API call
331
- 4. Mixpanel `people.set_once()` stores initial UTM as permanent user properties
332
- 5. Clear localStorage after successful DB sync
333
-
334
- ### UTM Parameters
335
-
336
- | Parameter | Purpose | Example |
337
- |---|---|---|
338
- | `utm_source` | Traffic source | `google`, `facebook`, `newsletter` |
339
- | `utm_medium` | Marketing medium | `cpc`, `social`, `email` |
340
- | `utm_campaign` | Campaign name | `spring_sale_2025` |
341
- | `utm_content` | Ad creative variant | `banner_a`, `text_link` |
342
- | `utm_term` | Paid search keyword | `ai+chatbot` |
343
-
344
- ### gclid / fbclid / li_fat_id
345
-
346
- Google, Meta, and LinkedIn append their own click IDs to URLs:
347
- - `gclid` — Google Ads click ID (stored in `_gcl_aw` cookie by gtag.js automatically)
348
- - `fbclid` — Meta click ID (stored in `_fbc` cookie by fbevents.js automatically)
349
- - `li_fat_id` — LinkedIn click ID (handled by Insight Tag)
350
-
351
- These are handled automatically by the respective SDKs. Do not strip them from URLs.
339
+ Read `references/event-tracking.md` → **UTM Attribution** for the capture flow
340
+ (localStorage through OAuth redirects into the DB), the five-parameter table,
341
+ and the platform click IDs (`gclid`/`fbclid`/`li_fat_id`) — which the SDKs
342
+ handle automatically; do not strip them from URLs.
352
343
 
353
344
  ---
354
345
 
@@ -13,6 +13,8 @@
13
13
  - [Ecommerce Events](#ecommerce-events)
14
14
  - [SaaS / Subscription Events](#saas--subscription-events)
15
15
  - [trackEvent Wrapper Pattern](#trackevent-wrapper-pattern)
16
+ - [User identification](#user-identification)
17
+ - [UTM Attribution](#utm-attribution)
16
18
 
17
19
  ## Table of Contents
18
20
 
@@ -23,6 +25,8 @@
23
25
  - [Ecommerce Events](#ecommerce-events)
24
26
  - [SaaS / Subscription Events](#saas--subscription-events)
25
27
  - [trackEvent Wrapper Pattern](#trackevent-wrapper-pattern)
28
+ - [User identification](#user-identification)
29
+ - [UTM Attribution](#utm-attribution)
26
30
 
27
31
  ## Event Types
28
32
 
@@ -243,3 +247,38 @@ Moved out of `SKILL.md` on 2026-08-16: the body was 5273 tokens against a
243
247
  < 5000 budget, and identity is the other half of the question this file already
244
248
  owns — an event name says what happened, identity says who it happened to.
245
249
 
250
+
251
+ ---
252
+
253
+ ## UTM Attribution
254
+
255
+ Moved out of `SKILL.md` on 2026-08-30: the body crossed the 4750-token house
256
+ working limit when the Enhanced Conversions fix landed, and attribution
257
+ parameters are schema — the question this file already owns.
258
+
259
+ ### Capture Flow
260
+
261
+ 1. `UtmCapture` component reads UTM params from URL on page load → stores in localStorage
262
+ 2. On registration (credentials): read UTM from localStorage, send in POST body, save to DB
263
+ 3. On registration (OAuth): UTM persists in localStorage through redirect → synced after return via API call
264
+ 4. Mixpanel `people.set_once()` stores initial UTM as permanent user properties
265
+ 5. Clear localStorage after successful DB sync
266
+
267
+ ### UTM Parameters
268
+
269
+ | Parameter | Purpose | Example |
270
+ |---|---|---|
271
+ | `utm_source` | Traffic source | `google`, `facebook`, `newsletter` |
272
+ | `utm_medium` | Marketing medium | `cpc`, `social`, `email` |
273
+ | `utm_campaign` | Campaign name | `spring_sale_2025` |
274
+ | `utm_content` | Ad creative variant | `banner_a`, `text_link` |
275
+ | `utm_term` | Paid search keyword | `ai+chatbot` |
276
+
277
+ ### gclid / fbclid / li_fat_id
278
+
279
+ Google, Meta, and LinkedIn append their own click IDs to URLs:
280
+ - `gclid` — Google Ads click ID (stored in `_gcl_aw` cookie by gtag.js automatically)
281
+ - `fbclid` — Meta click ID (stored in `_fbc` cookie by fbevents.js automatically)
282
+ - `li_fat_id` — LinkedIn click ID (handled by Insight Tag)
283
+
284
+ These are handled automatically by the respective SDKs. Do not strip them from URLs.
@@ -24,7 +24,9 @@ production.
24
24
  This skill is provider-neutral. Concrete request/response shapes for one gateway
25
25
  live in [`references/heleket-provider.md`](references/heleket-provider.md);
26
26
  the invariants below hold for Coinbase Commerce, NOWPayments, BTCPay, Heleket
27
- and anything else that issues an invoice and calls you back.
27
+ and anything else that issues an invoice and calls you back. One thing is NOT
28
+ provider-neutral and is labeled where it appears: the webhook signature
29
+ *algorithm*, which differs per gateway.
28
30
 
29
31
  > **Choosing a provider is a business and compliance decision, not a technical
30
32
  > one.** Crypto payment processors differ sharply in regulatory standing —
@@ -117,9 +119,20 @@ waterfall below.
117
119
 
118
120
  The callback is an unauthenticated public endpoint until you prove otherwise.
119
121
 
122
+ **The `sign()` below is ONE provider's scheme — Heleket's:
123
+ `md5(base64(json) + apiKey)`.** The *pattern* (constant-time compare, sign the
124
+ raw body, verify before parsing) transfers; the algorithm does not. Checked
125
+ 2026-08-30: Coinbase Commerce signs with **HMAC-SHA256** of the raw body
126
+ (`X-CC-Webhook-Signature`), NOWPayments with **HMAC-SHA512** of the
127
+ sorted-key JSON (`x-nowpayments-sig`), BTCPay with **HMAC-SHA256**
128
+ (`BTCPay-Sig`). Take the algorithm from your provider's own reference — for
129
+ Heleket's full shapes see
130
+ [`references/heleket-provider.md`](references/heleket-provider.md).
131
+
120
132
  ```ts
121
133
  import { createHash, timingSafeEqual } from 'node:crypto';
122
134
 
135
+ // Heleket's scheme. Swap the digest for your provider — see the note above.
123
136
  function sign(payload: unknown, apiKey: string): string {
124
137
  const json = JSON.stringify(payload);
125
138
  return createHash('md5')
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: google-auth
3
3
  description: >-
4
- Use when implementing Google authentication in a Node.js or Python web application — Google
5
- login, OAuth 2.0 flows, verifying Google ID tokens, service account authentication,
6
- Application Default Credentials, Google Identity Services, Workload Identity Federation, API
7
- keys, or working with google-auth-library (Node.js) or google-auth (Python). Covers ID token
8
- verification and security best practices. Triggers - "google auth", "google login", "google
9
- sign in", "OAuth 2.0 Google", "google-auth-library", "Sign In with Google", "Google Identity
10
- Services", "GIS", "ADC", "Application Default Credentials", "service account", "Google ID
11
- token", "verifyIdToken", "GOOGLE_APPLICATION_CREDENTIALS", "Google SSO", "вход через Google",
12
- "авторизация Google", "сервисный аккаунт", "проверить ID-токен", "гугл-логин". For end-user
13
- web sign-in only, use the google-signin skill instead.
4
+ Use when a server authenticates to Google in a Node.js or Python application — OAuth 2.0
5
+ flows, verifying Google ID tokens server-side, service account authentication and keys,
6
+ Application Default Credentials, Workload Identity Federation, API keys, or working with
7
+ google-auth-library (Node.js) or google-auth (Python). Covers server-side ID token
8
+ verification and security best practices. Triggers - "google auth", "OAuth 2.0 Google",
9
+ "google-auth-library", "ADC", "Application Default Credentials", "service account",
10
+ "Workload Identity Federation", "Google ID token", "verifyIdToken",
11
+ "GOOGLE_APPLICATION_CREDENTIALS", "Google SSO", "авторизация Google", "сервисный аккаунт",
12
+ "ключи сервисного аккаунта", "проверить ID-токен". For end-user web sign-in only, use the
13
+ google-signin skill instead.
14
14
  ---
15
15
 
16
16
  # Google Authentication for Node.js & Python
@@ -45,7 +45,7 @@ pip install google-api-python-client
45
45
  |--------|----------|-------------------|---------------------------|
46
46
  | **ADC** | Same identity for all users, server-to-server | `GoogleAuth` | `google.auth.default()` |
47
47
  | **OAuth 2.0** | Actions on behalf of end users | `OAuth2Client` | `google_auth_oauthlib.flow.Flow` |
48
- | **Sign In with Google (GIS)** | User sign-in/sign-up on websites | GIS JS SDK + `verifyIdToken()` | GIS JS SDK + `id_token.verify_oauth2_token()` |
48
+ | **Sign In with Google (GIS)** | User sign-in/sign-up on websites — the google-signin skill's ground | GIS JS SDK + `verifyIdToken()` | GIS JS SDK + `id_token.verify_oauth2_token()` |
49
49
  | **JWT / Service Account** | Server-to-server, single identity | `JWT` | `service_account.Credentials` |
50
50
  | **API Key** | Public data, no user context | `OAuth2Client({ apiKey })` | passed to `googleapiclient.discovery.build(developerKey=)` |
51
51
  | **Compute** | On GCP with attached service account | `Compute` | `google.auth.compute_engine.Credentials` |
@@ -134,7 +134,7 @@ credentials = flow.credentials
134
134
 
135
135
  For the complete OAuth 2.0 flow (parameters, token exchange, refresh, revocation, incremental auth), see [references/oauth2-web-server.md](references/oauth2-web-server.md).
136
136
 
137
- ### 3. Sign In with Google — ID Token Verification
137
+ ### 3. Verifying a Google ID Token — the library call
138
138
 
139
139
  **Node.js**
140
140
 
@@ -147,10 +147,6 @@ const ticket = await client.verifyIdToken({
147
147
  audience: WEB_CLIENT_ID,
148
148
  });
149
149
  const payload = ticket.getPayload();
150
- const userId = payload['sub'];
151
- const email = payload['email'];
152
- const name = payload['name'];
153
- const picture = payload['picture'];
154
150
  ```
155
151
 
156
152
  **Python**
@@ -159,22 +155,14 @@ const picture = payload['picture'];
159
155
  from google.oauth2 import id_token
160
156
  from google.auth.transport import requests
161
157
 
162
- request = requests.Request()
163
-
164
- payload = id_token.verify_oauth2_token(
165
- token,
166
- request,
167
- WEB_CLIENT_ID
168
- )
169
- user_id = payload['sub']
170
- email = payload['email']
171
- name = payload.get('name', '')
172
- picture = payload.get('picture')
158
+ payload = id_token.verify_oauth2_token(token, requests.Request(), WEB_CLIENT_ID)
173
159
  ```
174
160
 
175
- Verification checks: JWT signature, `aud`, `exp`, `iss` (accounts.google.com).
176
-
177
- For GIS integration, CSRF protection, and hosted domain validation, see [references/sign-in-with-google.md](references/sign-in-with-google.md).
161
+ The call checks signature, `aud`, `exp` and `iss` — **and nothing else**. That is
162
+ the library contract, not the web sign-in contract: for the sign-in security
163
+ checklist (`nonce` binding, `email_verified`, login-CSRF defense, account
164
+ linking) use the **google-signin** skill — it is the one home for that contract,
165
+ and this skill deliberately does not restate it.
178
166
 
179
167
  ### 4. JWT / Service Account
180
168
 
@@ -264,7 +252,7 @@ if credentials.expired and credentials.refresh_token:
264
252
  - Store `refresh_token` securely; it's only returned on first authorization
265
253
  - Validate external credential configurations before use (check `token_url`, `service_account_impersonation_url` point to googleapis.com)
266
254
  - Prefer Workload Identity Federation over service account keys for non-GCP environments
267
- - For GIS: verify `g_csrf_token` with double-submit-cookie pattern
255
+ - For end-user web sign-in, apply the google-signin skill's full checklist — a partial restatement here is how the two skills drifted apart once already
268
256
  - **Python-specific**: reuse a single `google.auth.transport.requests.Request()` instance across verifications for connection pooling; do not create a new one per call in hot paths
269
257
 
270
258
  ## Reference Files
@@ -90,6 +90,8 @@ export function getStripe(): Stripe {
90
90
  if (!client) {
91
91
  const key = process.env.STRIPE_SECRET_KEY;
92
92
  if (!key) throw new Error("STRIPE_SECRET_KEY is not set");
93
+ // Pin checked 2026-08-30 — 2026-08-26.dahlia was already newer. Confirm
94
+ // against Stripe's changelog before pinning; this line goes stale monthly.
93
95
  client = new Stripe(key, { apiVersion: "2026-07-29.dahlia", maxNetworkRetries: 2 });
94
96
  }
95
97
  return client;
@@ -171,7 +173,7 @@ await stripe.checkout.sessions.create({
171
173
 
172
174
  `{CHECKOUT_SESSION_ID}` is substituted by Stripe, not rendered by you. On
173
175
  `2026-03-25.dahlia`+ an `integration_identifier` label lets you compare flows in
174
- the Dashboard.
176
+ the Dashboard (floor checked 2026-08-30 against Stripe's changelog).
175
177
 
176
178
  Three decisions belong to `stripe-best-practices`: **usage-based billing**
177
179
  (Metronome for anything new; Billing Meters is a low-level primitive), **tax**