@ssheleg/sheleg-dev 0.13.3 → 0.14.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 +27 -0
- package/README.md +13 -1
- package/SECURITY.md +17 -16
- package/package.json +1 -1
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/sheleg-dev/hooks/lib/moneygate.js +14 -4
- package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +17 -0
- package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +25 -3
- package/plugins/sheleg-dev/skills/frontend-performance/references/accessibility-perf.md +1 -1
- package/plugins/sheleg-dev/skills/google-auth/SKILL.md +2 -1
- package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +18 -0
- package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +16 -0
- package/plugins/sheleg-dev/skills/google-signin/SKILL.md +6 -3
- package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +7 -3
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +3 -1
- package/plugins/sheleg-dev/skills/stripe-billing/references/price-integrity.md +25 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +1 -1
- package/plugins/sheleg-dev/skills/stripe-billing/scripts/assert-stripe-env.mjs +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## v0.14.0 — frontend performance is tested in WebKit (Safari) first
|
|
2
|
+
|
|
3
|
+
- `frontend-performance/SKILL.md` gains *Test browser — WebKit (Safari) first*:
|
|
4
|
+
Safari through `safaridriver`, Playwright `webkit` (`--browser webkit`,
|
|
5
|
+
`npx playwright install webkit`), Safari Web Inspector's Timelines, and
|
|
6
|
+
`open -a Safari <url>` on macOS rather than the system default browser.
|
|
7
|
+
- Lighthouse, PageSpeed Insights, CrUX, CDP traces and the chrome-devtools MCP stay
|
|
8
|
+
as the stated Chromium exception (Chromium by definition): Chrome Dev, a
|
|
9
|
+
temporary profile, every started process closed. A page checked only in Chromium
|
|
10
|
+
is reported as exactly that; a Lighthouse score is not a cross-browser verdict.
|
|
11
|
+
- The Measure and Validate steps run the WebKit pass too; *Degradation* names the
|
|
12
|
+
WebKit channels; the contrast check in `references/accessibility-perf.md` starts
|
|
13
|
+
in Safari Web Inspector.
|
|
14
|
+
- New regression `test/audit_regressions/safari-first.py` with drop mutations. Text
|
|
15
|
+
integrity only; no hook or script behaviour changed.
|
|
16
|
+
|
|
17
|
+
## v0.13.4 — FW03 credential boundaries
|
|
18
|
+
|
|
19
|
+
- Supply a copyable, locally tested Stripe mode assertion before client construction.
|
|
20
|
+
- Refuse explicit Google service-account JSON and OAuth client-secret assignments in
|
|
21
|
+
non-production or undeclared runs, while preserving public metadata assignments.
|
|
22
|
+
- Correct Google Testing advice for basic identity scopes and Workspace controls.
|
|
23
|
+
- Replace a Google access-token log with a value-free event. Document ambient ADC,
|
|
24
|
+
skills-only hooks and CAPI coverage limits. No provider operation was performed.
|
|
25
|
+
- Validate explicit repository paths inside skill Markdown without treating consumer
|
|
26
|
+
project paths as repository claims.
|
|
27
|
+
|
|
1
28
|
## v0.13.3 — a lightweight tag cannot publish a release
|
|
2
29
|
|
|
3
30
|
- **The release refuses a lightweight tag.** `release.yml` gains one step, right after
|
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ person has signed the category off **for this session**:
|
|
|
159
159
|
| Category | What it stops |
|
|
160
160
|
|---|---|
|
|
161
161
|
| `live-key` | a live-shaped `sk_live_…` / `rk_live_…` reaching a command |
|
|
162
|
-
| `credential` |
|
|
162
|
+
| `credential` | explicitly assigning `HELEKET_API_KEY`, `GOOGLE_CREDENTIALS` or `GOOGLE_CLIENT_SECRET` in a run that declares `test`, or declares nothing |
|
|
163
163
|
| `refund` | `stripe refunds create`, a POST to a `…/v1/refunds` URL, or a `create_refund` tool |
|
|
164
164
|
| `payout` | `stripe payouts\|transfers create`, a POST to `…/v1/payouts`, `…/v1/transfers` or Heleket's `…/v1/payout` |
|
|
165
165
|
| `dispute` | `stripe disputes close`, or `…/v1/disputes/…/close` — closing one accepts the loss |
|
|
@@ -167,6 +167,18 @@ person has signed the category off **for this session**:
|
|
|
167
167
|
| `self-authorisation` | a command that sets this gate's own switch. Never authorisable |
|
|
168
168
|
| `skip-billing` | `SKIP_BILLING=true` in a run declaring production — the free-money path. Never authorisable |
|
|
169
169
|
|
|
170
|
+
The Google names identify service-account JSON and an OAuth client secret. A Google
|
|
171
|
+
OAuth app in Testing still uses real credentials and can access permitted real data.
|
|
172
|
+
Public client IDs, project IDs, pixel IDs and ADC configuration paths are not treated
|
|
173
|
+
as secrets by name. This command-text guard does not inspect inherited credentials,
|
|
174
|
+
files or ambient ADC; skills-only installations do not activate it.
|
|
175
|
+
|
|
176
|
+
Stripe clients can copy the pure boot assertion from
|
|
177
|
+
[`assert-stripe-env.mjs`](plugins/sheleg-dev/skills/stripe-billing/scripts/assert-stripe-env.mjs).
|
|
178
|
+
It compares `STRIPE_PRODUCT_MODE` with a secret/restricted key prefix before client
|
|
179
|
+
construction and fails on unknown formats. It does not prove credential validity
|
|
180
|
+
or grant permission for a provider operation.
|
|
181
|
+
|
|
170
182
|
**Authorising one.** The authorised person exports the category — or `all` — in the
|
|
171
183
|
shell that starts the session, and starts a new one:
|
|
172
184
|
|
package/SECURITY.md
CHANGED
|
@@ -17,12 +17,11 @@ documents name that does not exist here.
|
|
|
17
17
|
|
|
18
18
|
## What ships, and what of it executes
|
|
19
19
|
|
|
20
|
-
`git ls-files plugins` returns **
|
|
21
|
-
files of the manual gate,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
no socket; the greps below prove all three.
|
|
20
|
+
`git ls-files plugins` returns **62 files: 35 markdown, one plugin manifest, three
|
|
21
|
+
files of the manual gate, 22 files of money fixtures, and one Stripe mode assertion.**
|
|
22
|
+
The fixture runners read their adjacent JSON only when invoked. The new Stripe
|
|
23
|
+
assertion is a pure function: caller-supplied mode and key in, mode or error code out;
|
|
24
|
+
no I/O, network or logging. Neither runs during installation.
|
|
26
25
|
|
|
27
26
|
| Component | Count | Runtime behavior |
|
|
28
27
|
|---|---|---|
|
|
@@ -32,6 +31,7 @@ no socket; the greps below prove all three.
|
|
|
32
31
|
| `plugins/sheleg-dev/hooks/hooks.json` | 1 | Hook manifest read by Claude Code. Declares one `PreToolUse` hook and no `if` filter. |
|
|
33
32
|
| `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
33
|
| `plugins/sheleg-dev/hooks/lib/moneygate.js` | 1 | Pure. Payload and environment in, verdict out. No `require` at all, no filesystem, no clock. |
|
|
34
|
+
| `plugins/sheleg-dev/skills/stripe-billing/scripts/assert-stripe-env.mjs` | 1 | Copyable boot assertion. Compares the declared mode with a secret/restricted key prefix. No I/O or logging; does not authenticate a credential. |
|
|
35
35
|
| `bin/sheleg-dev.js` — the npm installer | 1 | Runs only when you invoke it. Node built-ins only: `fs`, `path`, `os`. |
|
|
36
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. |
|
|
@@ -39,7 +39,7 @@ no socket; the greps below prove all three.
|
|
|
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. |
|
|
40
40
|
|
|
41
41
|
The published tarball is `bin/` and `plugins/` plus `README.md`, `CHANGELOG.md`,
|
|
42
|
-
`SECURITY.md`, `LICENSE` and `package.json` —
|
|
42
|
+
`SECURITY.md`, `LICENSE` and `package.json` — 68 files, listed by
|
|
43
43
|
`npm pack --dry-run`. `install.sh`, `test/`, `docs/` and `CONTRIBUTING.md` are **not**
|
|
44
44
|
in it; `test/validate.py` now refuses a path these shipped documents name that the
|
|
45
45
|
tarball does not carry, because resolving in a clone is the wrong question for a
|
|
@@ -54,8 +54,7 @@ in it can do is print nothing. It reads `process.env` for one variable pair
|
|
|
54
54
|
declaration a run already carries, and it never writes to any file. `README.md` →
|
|
55
55
|
*The manual gate* is its single home.
|
|
56
56
|
|
|
57
|
-
**There are no npm lifecycle scripts.** `package.json` declares
|
|
58
|
-
script, `test`. Installing this package, or reaching it through `npx`, runs
|
|
57
|
+
**There are no npm lifecycle scripts.** `package.json` declares development and test scripts only; none are install lifecycle hooks. Installing this package, or reaching it through `npx`, runs
|
|
59
58
|
nothing but the CLI you typed.
|
|
60
59
|
|
|
61
60
|
## What the installers read and write
|
|
@@ -92,8 +91,10 @@ installer deletes your edits and the node installer does not.
|
|
|
92
91
|
## Credentials
|
|
93
92
|
|
|
94
93
|
**This pack handles payment credentials as a subject and never as a value.** It
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
stores and transmits no credential and logs no credential value. Its hook examines
|
|
95
|
+
command text, including explicit credential assignments; the copyable Stripe
|
|
96
|
+
assertion accepts a caller-supplied key to check its mode prefix. Neither reads a
|
|
97
|
+
secret store. Knowledge examples describe credentials without shipping real ones.
|
|
97
98
|
|
|
98
99
|
What it contains is credential *names*, inside example code you adapt —
|
|
99
100
|
`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `HELEKET_API_KEY`,
|
|
@@ -177,11 +178,11 @@ git clone https://github.com/ssheleg/sheleg-dev && cd sheleg-dev
|
|
|
177
178
|
# self-describing documents name -- this file included.
|
|
178
179
|
python3 test/validate.py
|
|
179
180
|
|
|
180
|
-
# The shipped payload:
|
|
181
|
+
# The shipped payload: 62 files.
|
|
181
182
|
git ls-files plugins
|
|
182
183
|
|
|
183
|
-
#
|
|
184
|
-
# files of money fixtures. Everything else
|
|
184
|
+
# 27 lines: the plugin manifest, the three files of the manual gate, and the 22
|
|
185
|
+
# files of money fixtures, plus the Stripe mode assertion. Everything else is markdown.
|
|
185
186
|
git ls-files plugins | grep -v '\.md$'
|
|
186
187
|
|
|
187
188
|
# Every require the gate makes: two lines, both in the hook -- `path`, and its own
|
|
@@ -212,7 +213,7 @@ node test/installer_test.js
|
|
|
212
213
|
grep -rnE "child_process|\bfetch\(|require\('(http|https|net|dns|tls)'\)|spawn\(|execFile|writeFileSync|appendFileSync|unlinkSync|process\.env\." \
|
|
213
214
|
plugins/sheleg-dev/skills/*/fixtures/*.mjs
|
|
214
215
|
|
|
215
|
-
# The
|
|
216
|
+
# The I/O surface of the two installers: twelve lines. Three
|
|
216
217
|
# built-in requires, the copy/mkdir/remove calls, homedir, install.sh's
|
|
217
218
|
# rm -rf + cp -R, and one comment naming rm -rf. No process, no socket, no
|
|
218
219
|
# network — the plugin-registry read that decides the shadow refusal is
|
|
@@ -222,6 +223,6 @@ grep -nE "require|child_process|exec|spawn|fetch|socket|rm -rf|cp -R|copyFileSyn
|
|
|
222
223
|
# Live-key shapes across the skill payload: one line, and it is a placeholder.
|
|
223
224
|
grep -rnE "sk_live_[A-Za-z0-9]|rk_live_[A-Za-z0-9]|whsec_[A-Za-z0-9]{8}|BEGIN [A-Z ]*PRIVATE KEY" plugins
|
|
224
225
|
|
|
225
|
-
# What npm publishes, and nothing else:
|
|
226
|
+
# What npm publishes, and nothing else: 68 files.
|
|
226
227
|
npm pack --dry-run
|
|
227
228
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/sheleg-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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"
|
|
@@ -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.14.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -267,7 +267,18 @@ const READERS = new Set([
|
|
|
267
267
|
* environment into the key, so `STRIPE_SECRET_KEY=sk_test_…` is correct and common. The
|
|
268
268
|
* live half is caught by its shape (`LIVE_KEY`), not by the variable's name.
|
|
269
269
|
*/
|
|
270
|
+
// These named Google values are secrets, not client IDs or ADC configuration paths.
|
|
271
|
+
// Their authority comes from scopes/IAM and grants, never a test/live key prefix.
|
|
272
|
+
// Source and bounds: google-auth/references/adc-and-service-accounts.md and
|
|
273
|
+
// oauth2-web-server.md, Credential boundary sections (FW03).
|
|
270
274
|
const NO_TEST_VARIANT = new Map([
|
|
275
|
+
['GOOGLE_CREDENTIALS',
|
|
276
|
+
'Google service-account credential JSON carries authority from IAM, not a test-key ' +
|
|
277
|
+
'prefix. Use a credential-free mock for offline tests; an isolated test project is ' +
|
|
278
|
+
'still a real provider operation, with separately restricted principal and resources.'],
|
|
279
|
+
['GOOGLE_CLIENT_SECRET',
|
|
280
|
+
'Google OAuth client secrets have no declared test/live key prefix. OAuth Testing ' +
|
|
281
|
+
'status limits app access; it is not a fake credential or proof that user data is synthetic.'],
|
|
271
282
|
['HELEKET_API_KEY',
|
|
272
283
|
'Heleket issues one key per merchant, it is also the webhook signing secret, and it ' +
|
|
273
284
|
'carries no test/live marker — so there is no test value this can hold ' +
|
|
@@ -487,13 +498,12 @@ function decide(payload, env) {
|
|
|
487
498
|
if (allowedFor('credential', e, runEnv)) break;
|
|
488
499
|
if (runEnv === 'production') {
|
|
489
500
|
return deny('credential',
|
|
490
|
-
`this command exports ${name} — a
|
|
501
|
+
`this command exports ${name} — a provider credential`, why, runEnv);
|
|
491
502
|
}
|
|
492
503
|
return deny('credential',
|
|
493
504
|
`this command exports ${name} in a run declaring ${runEnv || 'nothing at all'}`,
|
|
494
|
-
`${why}
|
|
495
|
-
'
|
|
496
|
-
'that merely holds the key never reaches it.', runEnv);
|
|
505
|
+
`${why} This shell assignment is checked before the application starts; ` +
|
|
506
|
+
'the gate does not inspect credential values or infer a safe provider environment.', runEnv);
|
|
497
507
|
}
|
|
498
508
|
}
|
|
499
509
|
|
|
@@ -264,3 +264,20 @@ or use `lintrk` from the code path that actually completed the action.
|
|
|
264
264
|
|
|
265
265
|
---
|
|
266
266
|
|
|
267
|
+
|
|
268
|
+
## Credential boundary for the server adapter
|
|
269
|
+
|
|
270
|
+
The shipped `sendCapi` example and dedup fixtures specify event payloads; they do not
|
|
271
|
+
provision an access token or implement network authentication. Public pixel IDs belong
|
|
272
|
+
in browser configuration; provider access tokens belong only in the server's named
|
|
273
|
+
secret store. Before supplying a real token, bind the adapter to a separately identified
|
|
274
|
+
account/dataset and approved operation. Offline fixtures use the supplied emitter and
|
|
275
|
+
no provider token. A payload fixture passing is not credential isolation or successful
|
|
276
|
+
Meta delivery. This pack currently has no CAPI token-name rule in its shell gate.
|
|
277
|
+
|
|
278
|
+
Meta's [official Node SDK](https://github.com/facebook/facebook-nodejs-business-sdk/blob/main/README.md)
|
|
279
|
+
describes access tokens as opaque and requires one for the server client (checked
|
|
280
|
+
2026-10-13). Do not infer test/live authority from a guessed prefix. The current Meta
|
|
281
|
+
Test Events documentation could not be retrieved in this pass (HTTP 429); the exact
|
|
282
|
+
provider test-token/permission contract remains unverified, so no claim that a test-event
|
|
283
|
+
marker restricts the token is made here.
|
|
@@ -20,11 +20,33 @@ Optimize frontend applications for maximum Lighthouse scores and real-user perfo
|
|
|
20
20
|
|
|
21
21
|
## Audit Workflow
|
|
22
22
|
|
|
23
|
-
1. **Measure** -- Run PageSpeed Insights or Lighthouse. Record scores and
|
|
23
|
+
1. **Measure** -- Run PageSpeed Insights or Lighthouse (Chromium) and load the same scenario in WebKit first (see *Test browser* below). Record scores, failing audits and the engine of each number.
|
|
24
24
|
2. **Diagnose** -- Map each failing audit to a specific code-level cause (see checklist below).
|
|
25
25
|
3. **Prioritize** -- Fix by impact: P0 blocks rendering, P1 hurts metrics, P2 is informational.
|
|
26
26
|
4. **Implement** -- Apply fixes per the reference guides.
|
|
27
|
-
5. **Validate** -- `next build` (or framework equivalent) + re-run Lighthouse. Verify no regressions.
|
|
27
|
+
5. **Validate** -- `next build` (or framework equivalent) + re-run Lighthouse and the WebKit run. Verify no regressions in either engine.
|
|
28
|
+
|
|
29
|
+
## Test browser — WebKit (Safari) first
|
|
30
|
+
|
|
31
|
+
Load and profile the page in **WebKit first**: Safari through `safaridriver`
|
|
32
|
+
(WebDriver; `sudo safaridriver --enable` once) for a real browser, Playwright's
|
|
33
|
+
`webkit` (the `webkit` project, `--browser webkit`, installed with
|
|
34
|
+
`npx playwright install webkit`) for scripted runs and traces, Safari Web
|
|
35
|
+
Inspector's Timelines for debugging. On macOS open a page with
|
|
36
|
+
`open -a Safari <url>`, not the system default browser, which may be another
|
|
37
|
+
engine. An automation library (browser-use and the like) runs on its
|
|
38
|
+
WebKit/Safari backend where it has one. WebKit has the narrowest support
|
|
39
|
+
envelope of the big three — a regression that only iOS Safari users feel is
|
|
40
|
+
invisible to every Chrome tool below.
|
|
41
|
+
|
|
42
|
+
**Lighthouse, PageSpeed Insights, CrUX, a CDP trace and the chrome-devtools MCP
|
|
43
|
+
are Chromium by definition** — they stay in this workflow as the **stated
|
|
44
|
+
Chromium exception**: use them when the task needs Chrome itself, and say which
|
|
45
|
+
and why. Prefer the Chrome Dev build for a Chrome run you start; give it a
|
|
46
|
+
temporary profile (never the user's), record what you started, and close every
|
|
47
|
+
process you started before you finish. A page checked only in Chromium is
|
|
48
|
+
reported as "checked only in Chromium", never as "tested in a browser"; a
|
|
49
|
+
Lighthouse score is a Chrome lab number, not a cross-browser verdict.
|
|
28
50
|
|
|
29
51
|
## Core Web Vitals, and the diagnostics beside them
|
|
30
52
|
|
|
@@ -198,7 +220,7 @@ how a typo survives review.
|
|
|
198
220
|
- **Not Claude Code** (Cursor, Codex, the skills CLI, the API container): the pack's
|
|
199
221
|
`PreToolUse` gate does not run — every rule here still applies, enforced by a person
|
|
200
222
|
instead of a hook.
|
|
201
|
-
- **No browser channel** (no Chrome DevTools MCP
|
|
223
|
+
- **No browser channel** (no `safaridriver`, no Playwright `webkit`, no Chrome DevTools MCP): a page is judged from
|
|
202
224
|
the diff and the numbers are not measured on this machine. Record the weaker claim;
|
|
203
225
|
a Lighthouse score nobody ran is not a score.
|
|
204
226
|
- **No network**: PageSpeed Insights and field data are unavailable. Lab reasoning is
|
|
@@ -56,7 +56,7 @@ If you need visual hierarchy without reducing contrast below WCAG thresholds:
|
|
|
56
56
|
|
|
57
57
|
### Checking Contrast
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Inspect in Safari Web Inspector first: the Computed styles give the text colour and, on the element or the ancestor that paints it, the background colour; and the formula below gives the ratio. Chrome DevTools' color picker shows the contrast ratio directly — a Chromium tool, so name it as the engine you checked in.
|
|
60
60
|
|
|
61
61
|
Or calculate manually: `(L1 + 0.05) / (L2 + 0.05)` where L1 is the lighter relative luminance.
|
|
62
62
|
|
|
@@ -235,7 +235,8 @@ client.on('tokens', (tokens) => {
|
|
|
235
235
|
if (tokens.refresh_token) {
|
|
236
236
|
// Store refresh_token — only sent on first auth
|
|
237
237
|
}
|
|
238
|
-
|
|
238
|
+
// Record an event name only; token values never enter logs.
|
|
239
|
+
console.info("google_auth_tokens_refreshed");
|
|
239
240
|
});
|
|
240
241
|
```
|
|
241
242
|
|
|
@@ -459,3 +459,21 @@ decoded = id_token.verify_token(
|
|
|
459
459
|
| `GOOGLE_CLOUD_PROJECT` | Default project ID |
|
|
460
460
|
| `GCLOUD_PROJECT` | Alias for project ID |
|
|
461
461
|
| `HTTPS_PROXY` / `https_proxy` | Proxy for HTTPS requests |
|
|
462
|
+
|
|
463
|
+
## Credential boundary: real authority versus offline mocks
|
|
464
|
+
|
|
465
|
+
Checked 2026-10-13 against [Google's ADC documentation](https://docs.cloud.google.com/docs/authentication/application-default-credentials).
|
|
466
|
+
`GOOGLE_CREDENTIALS` in the JSON-loading examples carries a service-account private key.
|
|
467
|
+
It has no sandbox marker comparable to Stripe's mode prefix. A local or staging process
|
|
468
|
+
can still access real resources through its IAM grants: isolate the project, principal
|
|
469
|
+
and permissions before a real-provider test. Offline fixtures should inject an in-memory
|
|
470
|
+
auth adapter and no credentials.
|
|
471
|
+
|
|
472
|
+
The pack's Claude plugin gate refuses explicit shell assignments to `GOOGLE_CREDENTIALS`
|
|
473
|
+
in a test/undeclared run; a real-provider run uses the existing operator-authorised
|
|
474
|
+
credential category. A name in a grep pattern and `GOOGLE_CLOUD_PROJECT` are metadata,
|
|
475
|
+
not secret assignments. `GOOGLE_APPLICATION_CREDENTIALS` is deliberately not classified
|
|
476
|
+
as a secret by its name: it can point to either a key or keyless federation configuration.
|
|
477
|
+
Allowing that path does not prove its target safe. Verify the resolved principal and
|
|
478
|
+
source without reading a secret into logs; ambient ADC, metadata identities, files and
|
|
479
|
+
inherited environment are outside this lexical assignment check.
|
|
@@ -684,3 +684,19 @@ Common scopes:
|
|
|
684
684
|
- `https://www.googleapis.com/auth/drive.readonly` — Read-only Drive
|
|
685
685
|
- `https://www.googleapis.com/auth/calendar.readonly` — Read-only Calendar
|
|
686
686
|
- `https://www.googleapis.com/auth/cloud-platform` — Full GCP access
|
|
687
|
+
|
|
688
|
+
## Credential boundary: OAuth Testing is not a fake credential
|
|
689
|
+
|
|
690
|
+
Checked 2026-10-13 against [Google's OAuth app-state documentation](https://developers.google.com/identity/protocols/oauth2/production-readiness/overview).
|
|
691
|
+
The consent screen's Testing state governs access and token lifetime; it does not turn
|
|
692
|
+
a user's data or a client secret into synthetic fixtures. Use separate clients, test
|
|
693
|
+
accounts and least scopes for provider tests; an offline mock needs no Google secret.
|
|
694
|
+
The plugin gate refuses explicit `GOOGLE_CLIENT_SECRET` shell assignments in a test or
|
|
695
|
+
undeclared run. Existing operator authorisation is required for its real-provider path.
|
|
696
|
+
This does not classify inherited credentials, files or refresh/access tokens; those
|
|
697
|
+
still require the application's credential boundary. Errors and logs carry event names
|
|
698
|
+
or stable codes only, never a token value.
|
|
699
|
+
|
|
700
|
+
`GOOGLE_CLIENT_ID` is public metadata. The `google-signin` GIS ID-token flow does not use
|
|
701
|
+
a client secret at all; do not add one to make a test pass. See the `google-signin` skill
|
|
702
|
+
for its nonce, audience, origin and session checks.
|
|
@@ -110,8 +110,11 @@ attacker's account.
|
|
|
110
110
|
|
|
111
111
|
## Setup (GCP)
|
|
112
112
|
|
|
113
|
-
1. Cloud Console → APIs & Services → OAuth consent screen
|
|
114
|
-
|
|
113
|
+
1. Cloud Console → APIs & Services → OAuth consent screen: choose the audience.
|
|
114
|
+
External Testing normally limits access to test users, but basic identity
|
|
115
|
+
scopes (`openid`, `email`, `profile`) are exempt. Workspace policy can still
|
|
116
|
+
block access; publishing alone is not the remedy. See the dated setup note
|
|
117
|
+
in [the full guide](references/full-guide.md).
|
|
115
118
|
2. Credentials → Create OAuth client ID → **Web application** →
|
|
116
119
|
**Authorized JavaScript origins** = every origin the button renders on,
|
|
117
120
|
incl. `http://localhost:<port>` for dev (scheme+host+port, no path, no
|
|
@@ -148,7 +151,7 @@ attacker's account.
|
|
|
148
151
|
| Button never renders | GSI async race (add retry loop), empty client ID, or CSP blocks `accounts.google.com` |
|
|
149
152
|
| "Token used too early/expired" | Server clock skew → sync NTP |
|
|
150
153
|
| Audience mismatch | Frontend/backend client IDs differ → single env source of truth |
|
|
151
|
-
| `access_denied` on consent |
|
|
154
|
+
| `access_denied` on consent | Check requested scopes, audience and Workspace policy. Testing allowlisting applies beyond basic identity scopes; investigate before changing publishing status |
|
|
152
155
|
| Works locally, breaks in prod | Prod origin missing in GCP, or `Secure` cookie served over plain HTTP |
|
|
153
156
|
| One Tap missing / FedCM console warnings | FedCM is mandatory since Aug 2025 and the opt-out is gone (checked 2026-08-06). Load `gsi/client` from Google and keep it current — a vendored or pinned copy is the real break. The rendered button flow is unaffected; code branching on the old `isNotDisplayed()` moment callbacks is not |
|
|
154
157
|
|
|
@@ -219,8 +219,12 @@ cookie** — out of reach of JS/XSS, never sent cross-site.
|
|
|
219
219
|
1. <https://console.cloud.google.com/> → create/pick a project.
|
|
220
220
|
2. **APIs & Services → OAuth consent screen**: user type **External**, fill
|
|
221
221
|
app name / support email / developer email. Scopes: none needed beyond the
|
|
222
|
-
defaults (GIS only uses `openid email profile`).
|
|
223
|
-
|
|
222
|
+
defaults (GIS only uses `openid email profile`). External apps in Testing
|
|
223
|
+
normally require allowlisted users, but apps requesting only these basic
|
|
224
|
+
identity scopes are exempt. Workspace administrators can still block the app.
|
|
225
|
+
Choose publishing and verification status for the intended audience and actual
|
|
226
|
+
scopes, not as a blanket fix for a sign-in error. Checked 2026-10-13 against
|
|
227
|
+
[Google OAuth app states](https://developers.google.com/identity/protocols/oauth2/production-readiness/overview).
|
|
224
228
|
3. **APIs & Services → Credentials → Create credentials → OAuth client ID**:
|
|
225
229
|
- Application type: **Web application**.
|
|
226
230
|
- **Authorized JavaScript origins**: every origin the button renders on —
|
|
@@ -373,7 +377,7 @@ shape that flow never sends.
|
|
|
373
377
|
| `[GSI_LOGGER] The given origin is not allowed for the given client ID` | Current origin missing from **Authorized JavaScript origins** (check port and `www.` exactly). Changes take minutes to propagate |
|
|
374
378
|
| `Invalid Google token: Token used too early/expired` | Server clock skew → sync NTP |
|
|
375
379
|
| `Wrong recipient / audience mismatch` | Frontend and backend use different client IDs — single env source of truth |
|
|
376
|
-
| 403 `access_denied` at consent |
|
|
380
|
+
| 403 `access_denied` at consent | Check requested scopes, user type and Workspace controls. For an External Testing app requesting more than basic identity scopes, confirm the user is allowlisted. Publishing does not override administrator policy |
|
|
377
381
|
| Works locally, fails in prod | Prod origin not in Authorized origins; or cookie has `Secure` on plain HTTP (Prowl dev: `PROWL_INSECURE_COOKIES=1`) |
|
|
378
382
|
| One Tap doesn't show / FedCM warnings | Third-party-cookie phase-out: GIS auto-migrates to FedCM; keep `gsi/client` unmodified and current — the rendered button flow is unaffected |
|
|
379
383
|
| User "lost password" but signed up via Google | Expected: Google-only accounts have empty password hash. Recovery = sign in with Google using the same email |
|
|
@@ -85,12 +85,14 @@ customers. See [`references/price-integrity.md`](references/price-integrity.md).
|
|
|
85
85
|
## The client
|
|
86
86
|
|
|
87
87
|
```ts
|
|
88
|
+
// Copy the linked assert-stripe-env.mjs beside this client (see price-integrity.md).
|
|
89
|
+
import { assertStripeEnv } from "./assert-stripe-env.mjs";
|
|
88
90
|
let client: Stripe | null = null;
|
|
89
91
|
|
|
90
92
|
export function getStripe(): Stripe {
|
|
91
93
|
if (!client) {
|
|
92
94
|
const key = process.env.STRIPE_SECRET_KEY;
|
|
93
|
-
|
|
95
|
+
assertStripeEnv(process.env.STRIPE_PRODUCT_MODE, key);
|
|
94
96
|
// Pin checked 2026-08-30 — 2026-08-26.dahlia was already newer. Confirm
|
|
95
97
|
// against Stripe's changelog before pinning; this line goes stale monthly.
|
|
96
98
|
client = new Stripe(key, { apiVersion: "2026-07-29.dahlia", maxNetworkRetries: 2 });
|
|
@@ -78,6 +78,31 @@ throw new Error(
|
|
|
78
78
|
Left raw, that error reads as "somebody deleted the product" and sends people to
|
|
79
79
|
the Dashboard for an hour.
|
|
80
80
|
|
|
81
|
+
### Assert the credential mode before constructing the client
|
|
82
|
+
|
|
83
|
+
Copy [assert-stripe-env.mjs](../scripts/assert-stripe-env.mjs) beside your client,
|
|
84
|
+
and call `assertStripeEnv(process.env.STRIPE_PRODUCT_MODE, process.env.STRIPE_SECRET_KEY)`
|
|
85
|
+
before `new Stripe`. The function is shipped executable code, not a second prose
|
|
86
|
+
implementation. Supply the secret through your project's secret runner; the following
|
|
87
|
+
values describe the configuration contract, not credentials to paste:
|
|
88
|
+
|
|
89
|
+
```dotenv
|
|
90
|
+
STRIPE_PRODUCT_MODE=test
|
|
91
|
+
STRIPE_SECRET_KEY="<injected-at-runtime>"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`STRIPE_ENV_TEST_HOLDS_LIVE_CREDENTIAL` and
|
|
95
|
+
`STRIPE_ENV_LIVE_HOLDS_TEST_CREDENTIAL` refuse opposite mode mismatches. Missing or
|
|
96
|
+
unknown modes/keys fail closed. This bounded assertion recognises account secret and
|
|
97
|
+
restricted keys (`sk_`/`rk_` with test/live mode); organization and other unrecognised
|
|
98
|
+
keys need an explicit mode contract, not inferred safety. It never logs or returns the
|
|
99
|
+
key. Agreement proves neither authenticity, account ownership nor permissions.
|
|
100
|
+
Webhook signing secrets have a separate endpoint binding and are not classified here.
|
|
101
|
+
|
|
102
|
+
Source checked 2026-10-13: [Stripe API keys](https://docs.stripe.com/keys), key types
|
|
103
|
+
and sandbox versus live mode. The executable regression covers both supported key
|
|
104
|
+
classes, both mismatches, absent mode/key and unsupported shapes without provider calls.
|
|
105
|
+
|
|
81
106
|
## One source for every displayed number
|
|
82
107
|
|
|
83
108
|
Every surface that shows money reads from **one** module: the pricing page, the
|
|
@@ -244,7 +244,7 @@ Before the first real charge, on staging, with a real key in test mode:
|
|
|
244
244
|
- [ ] webhook endpoint registered for live mode, subscribed to exactly the events you handle
|
|
245
245
|
- [ ] `STRIPE_WEBHOOK_SECRET` is the **live endpoint's** secret, not `stripe listen`'s
|
|
246
246
|
- [ ] the live key is a restricted key with only the permissions the app uses
|
|
247
|
-
- [ ] `STRIPE_PRODUCT_MODE` and the key are both live
|
|
247
|
+
- [ ] `STRIPE_PRODUCT_MODE` and the key are both live; call `assertStripeEnv` from [price-integrity.md](price-integrity.md#assert-the-credential-mode-before-constructing-the-client) before constructing the client
|
|
248
248
|
- [ ] the price conformance job is scheduled and its findings reach a human
|
|
249
249
|
- [ ] the reconciliation job is scheduled
|
|
250
250
|
- [ ] alerts exist for: signature failures, handler 5xx, endpoint disabled by Stripe
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Copy beside the Stripe client and call before constructing it. No I/O or logging.
|
|
2
|
+
* Prefix agreement is not credential authenticity or permission validation.
|
|
3
|
+
*/
|
|
4
|
+
export function assertStripeEnv(mode, key) {
|
|
5
|
+
if (!['test', 'production'].includes(mode)) throw new Error('STRIPE_ENV_INVALID');
|
|
6
|
+
if (typeof key !== 'string' || !key) throw new Error('STRIPE_KEY_MISSING');
|
|
7
|
+
const match = /^(?:sk|rk)_(test|live)_[A-Za-z0-9]+$/.exec(key);
|
|
8
|
+
// Organization/unknown keys do not encode a mode this assertion can prove.
|
|
9
|
+
if (!match) throw new Error('STRIPE_KEY_MODE_UNVERIFIED');
|
|
10
|
+
if (mode === 'test' && match[1] === 'live') {
|
|
11
|
+
throw new Error('STRIPE_ENV_TEST_HOLDS_LIVE_CREDENTIAL');
|
|
12
|
+
}
|
|
13
|
+
if (mode === 'production' && match[1] === 'test') {
|
|
14
|
+
throw new Error('STRIPE_ENV_LIVE_HOLDS_TEST_CREDENTIAL');
|
|
15
|
+
}
|
|
16
|
+
return mode;
|
|
17
|
+
}
|