@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 +100 -0
- package/README.md +7 -1
- package/SECURITY.md +27 -11
- package/bin/sheleg-dev.js +86 -0
- package/package.json +3 -3
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +18 -27
- package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +39 -0
- package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +14 -1
- package/plugins/sheleg-dev/skills/google-auth/SKILL.md +19 -31
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +3 -1
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.
|
|
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
|
-
|
|
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
|
|
73
|
-
whole surface of both files in
|
|
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:
|
|
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:
|
|
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:
|
|
202
|
-
# built-in requires, the copy/mkdir/remove calls, homedir,
|
|
203
|
-
# rm -rf + cp -R. No process, no socket, no
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
Application Default Credentials,
|
|
7
|
-
|
|
8
|
-
verification and security best practices. Triggers - "google auth", "
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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**
|