@ssheleg/sheleg-dev 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +347 -0
- package/README.md +101 -1
- package/SECURITY.md +190 -52
- package/bin/sheleg-dev.js +18 -0
- package/package.json +2 -2
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/sheleg-dev/hooks/hooks.json +29 -0
- package/plugins/sheleg-dev/hooks/lib/moneygate.js +587 -0
- package/plugins/sheleg-dev/hooks/money-gate.js +68 -0
- package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +24 -75
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/README.md +76 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/assert-dedup-contract.mjs +249 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/manifest.json +75 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-capi-from-thank-you-page.json +32 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-capi-server.json +26 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/purchase-pixel-browser.json +12 -0
- package/plugins/sheleg-dev/skills/ad-tracking/fixtures/reference-emitter.mjs +129 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +42 -0
- package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +159 -2
- package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +68 -0
- package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +349 -10
- package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +31 -13
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +48 -104
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/README.md +110 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/assert-money-invariants.mjs +404 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/charge-refunded-partial.json +52 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/charge-refunded-remainder.json +63 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-async-payment-failed.json +43 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-completed-async-unpaid.json +43 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/checkout-session-completed-paid.json +42 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle-next-period.json +70 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle-redelivery.json +70 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-cycle.json +61 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/invoice-paid-subscription-update-proration.json +90 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/manifest.json +172 -0
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/reference-handler.mjs +315 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +27 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +38 -2
- package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +43 -1
|
@@ -9,6 +9,9 @@ one provider's wire format looks like when you apply them.
|
|
|
9
9
|
- [1. What is Heleket and when to use it](#1-what-is-heleket-and-when-to-use-it)
|
|
10
10
|
- [2. Architecture overview](#2-architecture-overview)
|
|
11
11
|
- [3. Environment variables](#3-environment-variables)
|
|
12
|
+
- [The test/live boundary — and why it cannot be a sentence](#the-testlive-boundary--and-why-it-cannot-be-a-sentence)
|
|
13
|
+
- [The boot assertion](#the-boot-assertion)
|
|
14
|
+
- [Residual exposure](#residual-exposure)
|
|
12
15
|
- [4. Database schema (Prisma)](#4-database-schema-prisma)
|
|
13
16
|
- [5. Heleket REST API reference](#5-heleket-rest-api-reference)
|
|
14
17
|
- [6. Webhook (IPN) payload reference](#6-webhook-ipn-payload-reference)
|
|
@@ -61,6 +64,7 @@ spot rate and settles to your merchant wallet in your chosen target coin (typica
|
|
|
61
64
|
| Default webhook IP | `31.133.220.8` (single static IP) |
|
|
62
65
|
| Auth | Per-merchant API key + `merchant` UUID header |
|
|
63
66
|
| Signature | MD5 over base64 of JSON body, suffixed with API key |
|
|
67
|
+
| **Test credential** | **none.** One key per merchant, which is *also* the webhook signing secret; no sandbox host, no restricted scope, no `test`/`live` marker in the key itself. "Test mode" is a toggle on the merchant account — see [Residual exposure](#residual-exposure) |
|
|
64
68
|
| Dashboard | https://heleket.com → Merchant settings |
|
|
65
69
|
| Settles to | USDT, USDC, BTC, ETH, BNB, TRX, etc. (configurable per invoice) |
|
|
66
70
|
| Pricing currency | Any fiat ISO code (USD recommended) |
|
|
@@ -123,27 +127,245 @@ is the only authoritative source for status changes; client polling is for UX on
|
|
|
123
127
|
|
|
124
128
|
| Variable | Required | Default | Source / Notes |
|
|
125
129
|
|----------|----------|---------|----------------|
|
|
126
|
-
| `
|
|
130
|
+
| `HELEKET_ENV` | yes | **none, deliberately** | `test` \| `production`. The environment this run *declares*. Separate from the key on purpose — one variable that both carries the secret and names its environment cannot be checked against itself. No default: a default is the control disappearing the first time somebody copies a `.env`. |
|
|
131
|
+
| `HELEKET_API_KEY` | yes | — | Merchant settings → Payment API. Trim whitespace on read. **Never log this value**, it is also the webhook signing secret. There is no test variant of it — see [Residual exposure](#residual-exposure). |
|
|
127
132
|
| `HELEKET_MERCHANT_ID` | yes | — | Merchant settings → Merchant UUID. Sent as `merchant` header on every API call. |
|
|
133
|
+
| `HELEKET_LIVE_MERCHANT_ID` | yes | — | The **production** merchant UUID, pinned. Not a secret (it travels in a header on every call); it is the thing that lets a boot assertion tell a live credential from a sandbox one. |
|
|
134
|
+
| `HELEKET_LIVE_KEY_FINGERPRINT` | no | — | First 12 hex of `sha256(live key)`. The second discriminator, for when the merchant UUID is shared or unset. A one-way 48-bit prefix over a provider-issued high-entropy secret — see the caveat under [the boot assertion](#the-boot-assertion). |
|
|
128
135
|
| `HELEKET_WEBHOOK_IP` | no | `31.133.220.8` | Override only if Heleket changes their callback IP. Localhost (`127.0.0.1` / `::1`) is always allowed for tests. |
|
|
129
|
-
| `SKIP_BILLING` | no | `false` | When `true` the checkout endpoint short-circuits, marks the payment paid immediately, and credits the balance — for local dev / staging without hitting Heleket. |
|
|
136
|
+
| `SKIP_BILLING` | no | `false` | When `true` the checkout endpoint short-circuits, marks the payment paid immediately, and credits the balance — for local dev / staging without hitting Heleket. **Refused when `HELEKET_ENV=production`**: there it is not a shortcut, it is a free-money path. |
|
|
130
137
|
| `NEXTAUTH_URL` or `NEXT_PUBLIC_APP_URL` | yes | — | Public origin used to build `url_callback`, `url_success`, `url_return`. Must be HTTPS in production. |
|
|
131
138
|
|
|
132
139
|
`.env.example` snippet:
|
|
133
140
|
|
|
134
141
|
```bash
|
|
135
142
|
# ── Heleket (crypto payments) ─────────────────────────────────
|
|
143
|
+
# Which environment this run declares. No default; the boot assertion refuses an
|
|
144
|
+
# unset value rather than guessing, and refuses a credential that disagrees with it.
|
|
145
|
+
HELEKET_ENV="test"
|
|
136
146
|
# Payment API key and merchant UUID from Heleket merchant settings (https://heleket.com).
|
|
147
|
+
# In a test run these MUST be the sandbox merchant's, not production's.
|
|
137
148
|
HELEKET_API_KEY=""
|
|
138
149
|
HELEKET_MERCHANT_ID=""
|
|
150
|
+
# The production merchant, pinned so a test run holding it can be refused at startup.
|
|
151
|
+
# Not a secret. Without it, a test run cannot prove it is not live — and is refused.
|
|
152
|
+
HELEKET_LIVE_MERCHANT_ID=""
|
|
153
|
+
# Optional second discriminator: printf %s "$LIVE_KEY" | shasum -a 256 | cut -c1-12
|
|
154
|
+
HELEKET_LIVE_KEY_FINGERPRINT=""
|
|
139
155
|
# Whitelist IP for webhook verification (Heleket callback origin).
|
|
140
156
|
HELEKET_WEBHOOK_IP="31.133.220.8"
|
|
141
157
|
```
|
|
142
158
|
|
|
143
|
-
**Security**: rotate the API key quarterly, store in a secret manager (Doppler,
|
|
159
|
+
**Security**: rotate the API key quarterly, store in a secret manager (Doppler,
|
|
144
160
|
AWS Secrets Manager, Cloudflare Workers Secret, K8s Secret), never commit `.env`,
|
|
145
161
|
never expose to the client bundle (no `NEXT_PUBLIC_` prefix).
|
|
146
162
|
|
|
163
|
+
### The test/live boundary — and why it cannot be a sentence
|
|
164
|
+
|
|
165
|
+
Read the provider's credential model before designing around it, because it is worse
|
|
166
|
+
than Stripe's and the difference decides the design:
|
|
167
|
+
|
|
168
|
+
| | Stripe | Heleket |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| Separate test credential | yes, a whole second account | **no** |
|
|
171
|
+
| Environment readable from the key | yes, `sk_test_` / `sk_live_` prefix | **no** — an opaque string with no marker |
|
|
172
|
+
| Sandbox host | no, same host | no, same host (`api.heleket.com`) |
|
|
173
|
+
| What "test mode" is | a property of the key | **a toggle on the merchant account** |
|
|
174
|
+
| Key also signs webhooks | no, a separate `whsec_` | **yes, the same value** |
|
|
175
|
+
|
|
176
|
+
So the house pattern — declare the environment in a variable separate from the secret,
|
|
177
|
+
then assert at boot that the two agree (the `stripe-billing` skill, `references/price-integrity.md`,
|
|
178
|
+
*Test and live mode*) — applies, but its Stripe implementation does not: there is no key
|
|
179
|
+
prefix to read. The comparison has to be made against something else, and Heleket gives
|
|
180
|
+
exactly two candidates, both non-secret:
|
|
181
|
+
|
|
182
|
+
- **the merchant UUID** (`HELEKET_MERCHANT_ID`), which is what distinguishes one merchant
|
|
183
|
+
account from another and travels in a header on every call;
|
|
184
|
+
- **a fingerprint of the key**, for when the UUID is shared or absent.
|
|
185
|
+
|
|
186
|
+
Pin the production values and the assertion below can refuse a mismatch in **both**
|
|
187
|
+
directions. One direction is half a boundary: a production key in a dev run is the
|
|
188
|
+
obvious loss, and a sandbox key in production is the quiet one — invoices created
|
|
189
|
+
against a merchant nobody reconciles, money settling to the wrong wallet, and no error
|
|
190
|
+
until somebody asks where the week's revenue went.
|
|
191
|
+
|
|
192
|
+
**Strongest control first.** The assertion is the *second* line. The first is a
|
|
193
|
+
**separate sandbox merchant account**, so the credential a dev machine holds authorises
|
|
194
|
+
nothing on the live merchant — M-06's point exactly: a credential that cannot reach
|
|
195
|
+
production beats a sentence saying not to use it there, because it still works after
|
|
196
|
+
everyone has forgotten the sentence. Check whether your Heleket account permits a second
|
|
197
|
+
merchant before assuming it does; this document cannot confirm it for you, and if it does
|
|
198
|
+
not, the exposure below is what you are left holding.
|
|
199
|
+
|
|
200
|
+
### The boot assertion
|
|
201
|
+
|
|
202
|
+
Fails at **startup**, not at the first charge. Copy it whole:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// src/lib/heleket-env.ts
|
|
206
|
+
import { createHash } from "node:crypto";
|
|
207
|
+
|
|
208
|
+
export type HeleketEnv = "test" | "production";
|
|
209
|
+
|
|
210
|
+
/** Codes, not sentences. A code survives a rewording, greps out of a log, and is what
|
|
211
|
+
* an alert should fire on. */
|
|
212
|
+
export type HeleketEnvErrorCode =
|
|
213
|
+
| "HELEKET_ENV_MISSING"
|
|
214
|
+
| "HELEKET_ENV_INVALID"
|
|
215
|
+
| "HELEKET_ENV_UNPINNED"
|
|
216
|
+
| "HELEKET_ENV_TEST_HOLDS_LIVE_CREDENTIAL"
|
|
217
|
+
| "HELEKET_ENV_LIVE_HOLDS_TEST_CREDENTIAL"
|
|
218
|
+
| "HELEKET_ENV_LIVE_WITH_SKIP_BILLING";
|
|
219
|
+
|
|
220
|
+
export class HeleketEnvError extends Error {
|
|
221
|
+
constructor(readonly code: HeleketEnvErrorCode, message: string) {
|
|
222
|
+
super(`${code}: ${message}`);
|
|
223
|
+
this.name = "HeleketEnvError";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 48-bit prefix of SHA-256 over the key.
|
|
229
|
+
*
|
|
230
|
+
* Safe to keep in configuration ONLY because a Heleket API key is a long
|
|
231
|
+
* provider-issued random string: the digest is one-way and 12 hex characters carry
|
|
232
|
+
* nothing recoverable. Never fingerprint a value a human chose — for a guessable
|
|
233
|
+
* secret this publishes a confirmation oracle. If in doubt, pin the merchant UUID
|
|
234
|
+
* instead and leave this unset.
|
|
235
|
+
*/
|
|
236
|
+
export function keyFingerprint(key: string): string {
|
|
237
|
+
return createHash("sha256").update(key.trim()).digest("hex").slice(0, 12);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type Identity = "live" | "not-live" | "unknown";
|
|
241
|
+
|
|
242
|
+
/** Which credential is this, judged only against pinned non-secret values. */
|
|
243
|
+
function identify(env: NodeJS.ProcessEnv): Identity {
|
|
244
|
+
const key = env.HELEKET_API_KEY?.trim();
|
|
245
|
+
const merchant = env.HELEKET_MERCHANT_ID?.trim();
|
|
246
|
+
const liveMerchant = env.HELEKET_LIVE_MERCHANT_ID?.trim();
|
|
247
|
+
const liveFinger = env.HELEKET_LIVE_KEY_FINGERPRINT?.trim().toLowerCase();
|
|
248
|
+
|
|
249
|
+
if (!liveMerchant && !liveFinger) return "unknown";
|
|
250
|
+
if (liveMerchant && merchant && merchant === liveMerchant) return "live";
|
|
251
|
+
if (liveFinger && key && keyFingerprint(key) === liveFinger) return "live";
|
|
252
|
+
if ((liveMerchant && merchant) || (liveFinger && key)) return "not-live";
|
|
253
|
+
return "unknown";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function assertHeleketEnv(env: NodeJS.ProcessEnv = process.env): HeleketEnv {
|
|
257
|
+
const declared = env.HELEKET_ENV?.trim();
|
|
258
|
+
|
|
259
|
+
if (!declared) {
|
|
260
|
+
throw new HeleketEnvError(
|
|
261
|
+
"HELEKET_ENV_MISSING",
|
|
262
|
+
"HELEKET_ENV is unset. It has no default on purpose: a default is the control " +
|
|
263
|
+
'disappearing the first time somebody copies a .env. Set "test" or "production".',
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (declared !== "test" && declared !== "production") {
|
|
267
|
+
throw new HeleketEnvError(
|
|
268
|
+
"HELEKET_ENV_INVALID",
|
|
269
|
+
`HELEKET_ENV="${declared}" — expected "test" or "production".`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (declared === "production" && env.SKIP_BILLING === "true") {
|
|
274
|
+
throw new HeleketEnvError(
|
|
275
|
+
"HELEKET_ENV_LIVE_WITH_SKIP_BILLING",
|
|
276
|
+
"SKIP_BILLING=true credits balances with no payment behind them. In production " +
|
|
277
|
+
"that is not a shortcut, it is a free-money path reachable by anyone who can " +
|
|
278
|
+
"read the checkout URL.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const identity = identify(env);
|
|
283
|
+
|
|
284
|
+
// Direction 1 — a live credential declared test. The dev machine, the CI job and the
|
|
285
|
+
// agent run all hold a key that creates real invoices the moment the dashboard
|
|
286
|
+
// toggle moves.
|
|
287
|
+
if (declared === "test" && identity === "live") {
|
|
288
|
+
throw new HeleketEnvError(
|
|
289
|
+
"HELEKET_ENV_TEST_HOLDS_LIVE_CREDENTIAL",
|
|
290
|
+
"HELEKET_ENV=test but this is the pinned production credential. Heleket has no " +
|
|
291
|
+
"test key: the same value that signs your webhooks creates real invoices as " +
|
|
292
|
+
"soon as merchant Test mode is off. Use the sandbox merchant account.",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Cannot tell is refused, not assumed — on the test side, which is where the control
|
|
297
|
+
// has to hold. "We could not prove it was safe" must never read as "it was safe".
|
|
298
|
+
if (declared === "test" && identity === "unknown") {
|
|
299
|
+
throw new HeleketEnvError(
|
|
300
|
+
"HELEKET_ENV_UNPINNED",
|
|
301
|
+
"HELEKET_ENV=test but neither HELEKET_LIVE_MERCHANT_ID nor " +
|
|
302
|
+
"HELEKET_LIVE_KEY_FINGERPRINT is pinned, so nothing here can prove this " +
|
|
303
|
+
"credential is not the production one.",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Direction 2 — a test credential declared live. The quiet one: invoices settle to a
|
|
308
|
+
// merchant nobody reconciles and no error surfaces until revenue is missing.
|
|
309
|
+
if (declared === "production" && identity === "not-live") {
|
|
310
|
+
throw new HeleketEnvError(
|
|
311
|
+
"HELEKET_ENV_LIVE_HOLDS_TEST_CREDENTIAL",
|
|
312
|
+
"HELEKET_ENV=production but this credential is not the pinned live merchant's. " +
|
|
313
|
+
"Invoices would be created against the wrong merchant and settle to a wallet " +
|
|
314
|
+
"your reconciliation never reads.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (declared === "production" && identity === "unknown") {
|
|
319
|
+
console.warn(
|
|
320
|
+
"[heleket] HELEKET_ENV=production with no live pin. Set HELEKET_LIVE_MERCHANT_ID " +
|
|
321
|
+
"— it is what lets a test run be refused.",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return declared;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Wire it at module load of the client library, above every export, so the process cannot
|
|
330
|
+
reach a request handler in a state the assertion would have rejected:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
// src/lib/heleket.ts — first executable line
|
|
334
|
+
import { assertHeleketEnv } from "./heleket-env";
|
|
335
|
+
|
|
336
|
+
export const HELEKET_ENV = assertHeleketEnv();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Calling it from the checkout handler instead is a control that only fires once a user is
|
|
340
|
+
already trying to pay, and never at all in the run that merely holds the key.
|
|
341
|
+
|
|
342
|
+
### Residual exposure
|
|
343
|
+
|
|
344
|
+
**Named because it does not go away, and an unwritten risk reads as an absent one.**
|
|
345
|
+
|
|
346
|
+
Heleket issues **one credential per merchant** and it is also the webhook signing secret.
|
|
347
|
+
There is no test key, no sandbox host and no read-only scope. "Test mode" is a toggle in
|
|
348
|
+
merchant settings (Section 15), which means the *same* key creates $1 test invoices before
|
|
349
|
+
the toggle moves and real ones after it — and it moves in a dashboard, by anyone with
|
|
350
|
+
dashboard access, with no deploy and no signal to the machines holding that key.
|
|
351
|
+
|
|
352
|
+
Three consequences, stated rather than implied:
|
|
353
|
+
|
|
354
|
+
1. **Unless you hold a second merchant account, a developer following Section 15 Option B
|
|
355
|
+
holds a production credential.** The assertion above narrows that window — it refuses
|
|
356
|
+
the run that declares the wrong environment — but it cannot make the key harmless. It
|
|
357
|
+
is a check on the declaration, not a limit on the credential.
|
|
358
|
+
2. **The key cannot be scoped down.** Because it doubles as the signing secret, you cannot
|
|
359
|
+
issue a verify-only credential for a service that merely validates callbacks; every
|
|
360
|
+
holder of the verifier can also create invoices.
|
|
361
|
+
3. **Rotation is the only real revocation.** There is no per-environment key to revoke, so
|
|
362
|
+
a leak from a dev machine is a production incident: rotate in merchant settings, and
|
|
363
|
+
re-pin `HELEKET_LIVE_KEY_FINGERPRINT` in the same change or the assertion starts
|
|
364
|
+
answering about a key that no longer exists.
|
|
365
|
+
|
|
366
|
+
If the compliance posture in the banner at the top of this document matters to you, this
|
|
367
|
+
credential model is part of the same assessment.
|
|
368
|
+
|
|
147
369
|
---
|
|
148
370
|
|
|
149
371
|
## 4. Database schema (Prisma)
|
|
@@ -1131,7 +1353,9 @@ balance updated, never touching Heleket. Perfect for E2E tests, demos, and featu
|
|
|
1131
1353
|
|
|
1132
1354
|
### Option B — Real Heleket + ngrok / cloudflared
|
|
1133
1355
|
|
|
1134
|
-
For end-to-end testing against the real provider
|
|
1356
|
+
For end-to-end testing against the real provider. **Use a sandbox merchant account if your
|
|
1357
|
+
Heleket account permits one** — that is the control that survives context loss; everything
|
|
1358
|
+
below is what to do because the provider gives you nothing stronger.
|
|
1135
1359
|
|
|
1136
1360
|
```bash
|
|
1137
1361
|
# Terminal 1
|
|
@@ -1143,15 +1367,30 @@ cloudflared tunnel --url http://localhost:3000
|
|
|
1143
1367
|
|
|
1144
1368
|
# Set in .env.local:
|
|
1145
1369
|
NEXTAUTH_URL=https://random-name.trycloudflare.com
|
|
1146
|
-
|
|
1147
|
-
|
|
1370
|
+
# Declared first, and never "production" on a developer machine.
|
|
1371
|
+
HELEKET_ENV=test
|
|
1372
|
+
# The SANDBOX merchant's key and UUID. Not production's.
|
|
1373
|
+
HELEKET_API_KEY=<sandbox merchant key>
|
|
1374
|
+
HELEKET_MERCHANT_ID=<sandbox merchant uuid>
|
|
1375
|
+
# The production merchant, pinned. This is what makes the line above checkable:
|
|
1376
|
+
# paste the live UUID here and `assertHeleketEnv()` refuses to boot if the two
|
|
1377
|
+
# credentials above ever turn out to be it.
|
|
1378
|
+
HELEKET_LIVE_MERCHANT_ID=<production merchant uuid>
|
|
1148
1379
|
HELEKET_WEBHOOK_IP=31.133.220.8
|
|
1149
1380
|
|
|
1150
1381
|
# Restart Next.js so it picks up the public URL
|
|
1151
1382
|
```
|
|
1152
1383
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1384
|
+
Heleket's "Test mode" (toggle in merchant settings) gives free $1 invoices, payable with
|
|
1385
|
+
any spare USDT. **It is a property of the merchant account, not of the key** — the same
|
|
1386
|
+
credential creates real invoices the moment somebody moves that toggle, so it is not a
|
|
1387
|
+
substitute for a separate merchant and it is not what makes this safe. What makes it
|
|
1388
|
+
checkable is the pin above plus
|
|
1389
|
+
[the boot assertion](#the-boot-assertion); what remains after both is
|
|
1390
|
+
[Residual exposure](#residual-exposure).
|
|
1391
|
+
|
|
1392
|
+
If your account has only one merchant, prefer **Option A** for everything except the one
|
|
1393
|
+
thing that genuinely needs the wire: a real callback arriving from Heleket's IP.
|
|
1155
1394
|
|
|
1156
1395
|
---
|
|
1157
1396
|
|
|
@@ -1251,8 +1490,87 @@ expect(res2.status).toBe(200);
|
|
|
1251
1490
|
// User's balance must NOT have increased again.
|
|
1252
1491
|
```
|
|
1253
1492
|
|
|
1493
|
+
### Credential-boundary tests (`heleket-env.ts`)
|
|
1494
|
+
|
|
1495
|
+
Both directions, and a defect planted against each. A boundary nobody has watched refuse
|
|
1496
|
+
is not a boundary — and the direction people skip is the second one.
|
|
1497
|
+
|
|
1498
|
+
```ts
|
|
1499
|
+
import { describe, it, expect } from "vitest";
|
|
1500
|
+
import { assertHeleketEnv, keyFingerprint, HeleketEnvError } from "./heleket-env";
|
|
1501
|
+
|
|
1502
|
+
const LIVE_MERCHANT = "11111111-1111-1111-1111-111111111111";
|
|
1503
|
+
const SANDBOX_MERCHANT = "22222222-2222-2222-2222-222222222222";
|
|
1504
|
+
const LIVE_KEY = "placeholder-live-key-not-a-real-credential";
|
|
1505
|
+
|
|
1506
|
+
const base = {
|
|
1507
|
+
HELEKET_LIVE_MERCHANT_ID: LIVE_MERCHANT,
|
|
1508
|
+
HELEKET_LIVE_KEY_FINGERPRINT: keyFingerprint(LIVE_KEY),
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
function code(env: Record<string, string>): string {
|
|
1512
|
+
try {
|
|
1513
|
+
assertHeleketEnv(env as NodeJS.ProcessEnv);
|
|
1514
|
+
return "PASSED";
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
return (e as HeleketEnvError).code;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
describe("assertHeleketEnv", () => {
|
|
1521
|
+
it("refuses a live credential declared test — by merchant uuid", () => {
|
|
1522
|
+
expect(code({ ...base, HELEKET_ENV: "test", HELEKET_MERCHANT_ID: LIVE_MERCHANT }))
|
|
1523
|
+
.toBe("HELEKET_ENV_TEST_HOLDS_LIVE_CREDENTIAL");
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("refuses a live credential declared test — by key fingerprint alone", () => {
|
|
1527
|
+
// the UUID is the sandbox one, so only the fingerprint can catch this
|
|
1528
|
+
expect(code({ ...base, HELEKET_ENV: "test",
|
|
1529
|
+
HELEKET_MERCHANT_ID: SANDBOX_MERCHANT, HELEKET_API_KEY: LIVE_KEY }))
|
|
1530
|
+
.toBe("HELEKET_ENV_TEST_HOLDS_LIVE_CREDENTIAL");
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
it("refuses a test credential declared live", () => {
|
|
1534
|
+
expect(code({ ...base, HELEKET_ENV: "production",
|
|
1535
|
+
HELEKET_MERCHANT_ID: SANDBOX_MERCHANT }))
|
|
1536
|
+
.toBe("HELEKET_ENV_LIVE_HOLDS_TEST_CREDENTIAL");
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
it("refuses an unset environment rather than defaulting", () => {
|
|
1540
|
+
expect(code({ ...base, HELEKET_MERCHANT_ID: SANDBOX_MERCHANT }))
|
|
1541
|
+
.toBe("HELEKET_ENV_MISSING");
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it("refuses a test run that cannot prove it is not live", () => {
|
|
1545
|
+
expect(code({ HELEKET_ENV: "test", HELEKET_MERCHANT_ID: SANDBOX_MERCHANT }))
|
|
1546
|
+
.toBe("HELEKET_ENV_UNPINNED");
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
it("refuses SKIP_BILLING in production", () => {
|
|
1550
|
+
expect(code({ ...base, HELEKET_ENV: "production",
|
|
1551
|
+
HELEKET_MERCHANT_ID: LIVE_MERCHANT, SKIP_BILLING: "true" }))
|
|
1552
|
+
.toBe("HELEKET_ENV_LIVE_WITH_SKIP_BILLING");
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
it("admits the two configurations that are actually correct", () => {
|
|
1556
|
+
expect(code({ ...base, HELEKET_ENV: "production",
|
|
1557
|
+
HELEKET_MERCHANT_ID: LIVE_MERCHANT })).toBe("PASSED");
|
|
1558
|
+
expect(code({ ...base, HELEKET_ENV: "test",
|
|
1559
|
+
HELEKET_MERCHANT_ID: SANDBOX_MERCHANT })).toBe("PASSED");
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
The last case is not padding. A boundary that refuses everything passes every negative
|
|
1565
|
+
test and stops anyone deploying, so it gets switched off within a day — which is the
|
|
1566
|
+
failure mode a refusal with no working configuration always ends in.
|
|
1567
|
+
|
|
1254
1568
|
### Test matrix to cover
|
|
1255
1569
|
|
|
1570
|
+
- `HELEKET_ENV=test` holding the live merchant credential → refused at boot
|
|
1571
|
+
- `HELEKET_ENV=production` holding a non-live credential → refused at boot
|
|
1572
|
+
- `HELEKET_ENV` unset → refused, not defaulted
|
|
1573
|
+
- `HELEKET_ENV=production` with `SKIP_BILLING=true` → refused at boot
|
|
1256
1574
|
- Unauthorized checkout → 401
|
|
1257
1575
|
- Amount below `MIN_AMOUNT` → 400
|
|
1258
1576
|
- Amount above `MAX_AMOUNT` → 400
|
|
@@ -1275,6 +1593,20 @@ Before shipping to production, verify each item:
|
|
|
1275
1593
|
|
|
1276
1594
|
- [ ] `HELEKET_API_KEY` and `HELEKET_MERCHANT_ID` stored in a secret manager, not in
|
|
1277
1595
|
`.env` committed to git.
|
|
1596
|
+
- [ ] A **separate sandbox merchant account** exists, or the reason it does not is
|
|
1597
|
+
written down where the next person will read it — the credential a dev machine
|
|
1598
|
+
holds is the whole boundary, and `assertHeleketEnv()` only checks the declaration.
|
|
1599
|
+
- [ ] `HELEKET_ENV` is set explicitly in every environment, and `HELEKET_LIVE_MERCHANT_ID`
|
|
1600
|
+
is pinned so a test run can be refused.
|
|
1601
|
+
- [ ] `assertHeleketEnv()` runs at **module load** of the client library, not inside the
|
|
1602
|
+
checkout handler — a run that merely holds the key must fail too.
|
|
1603
|
+
- [ ] Both refusal directions have been watched failing:
|
|
1604
|
+
`HELEKET_ENV_TEST_HOLDS_LIVE_CREDENTIAL` and
|
|
1605
|
+
`HELEKET_ENV_LIVE_HOLDS_TEST_CREDENTIAL`.
|
|
1606
|
+
- [ ] `SKIP_BILLING` is refused when `HELEKET_ENV=production`, by the assertion and not
|
|
1607
|
+
by a comment.
|
|
1608
|
+
- [ ] The key-rotation runbook re-pins `HELEKET_LIVE_KEY_FINGERPRINT` in the same change,
|
|
1609
|
+
or the assertion begins answering about a key that no longer exists.
|
|
1278
1610
|
- [ ] Webhook route is in the **CSRF exempt list** of your middleware.
|
|
1279
1611
|
- [ ] Webhook IP allowlist active (`isAllowedWebhookIp`), proxy-aware
|
|
1280
1612
|
(`do-connecting-ip` / `cf-connecting-ip` / last hop of `x-forwarded-for`).
|
|
@@ -1302,6 +1634,10 @@ Before shipping to production, verify each item:
|
|
|
1302
1634
|
|
|
1303
1635
|
| Symptom | Likely cause | Fix |
|
|
1304
1636
|
|---------|-------------|-----|
|
|
1637
|
+
| App refuses to boot with `HELEKET_ENV_TEST_HOLDS_LIVE_CREDENTIAL` | The dev machine is holding the production merchant's key — working as designed | Switch to the sandbox merchant. Do **not** widen the assertion; it caught the thing it exists for. |
|
|
1638
|
+
| App refuses to boot with `HELEKET_ENV_LIVE_HOLDS_TEST_CREDENTIAL` in production | A staging key reached the production environment | Fix the secret, not the check: this is the direction that loses money silently. |
|
|
1639
|
+
| App refuses to boot with `HELEKET_ENV_UNPINNED` | `HELEKET_LIVE_MERCHANT_ID` is not set, so a test run cannot prove it is not live | Pin the production merchant UUID. It is not a secret. |
|
|
1640
|
+
| A real payment arrived but no invoice exists in the dashboard you are looking at | Two merchant accounts; you are reading the sandbox one | Check `HELEKET_MERCHANT_ID` against `HELEKET_LIVE_MERCHANT_ID`. |
|
|
1305
1641
|
| Every webhook fails signature | Forgot the PHP-escape form | Use the dual `verifyWebhookSignature`. |
|
|
1306
1642
|
| Webhook returns 403 in production but works locally | Behind a CDN (Cloudflare / DO / Vercel), real IP is in `cf-connecting-ip` / `do-connecting-ip` / `x-forwarded-for` | Implement proxy-aware `extractIp`. |
|
|
1307
1643
|
| User pays $30, balance shows $29.97 | Crediting `paymentAmountUsd` without buffer | Add 1% buffer in checkout, credit `tokenAmount` (or use the waterfall `paymentAmountUsd ?? tokenAmount ?? amountUsd`). |
|
|
@@ -1324,8 +1660,11 @@ When the user says "add crypto payments to this project":
|
|
|
1324
1660
|
framework, an existing user model with a balance field of some kind, an authenticated
|
|
1325
1661
|
session helper.
|
|
1326
1662
|
2. **Add env vars** to `.env.example` and the secret manager:
|
|
1327
|
-
`HELEKET_API_KEY`, `HELEKET_MERCHANT_ID`, `
|
|
1328
|
-
`SKIP_BILLING=false`.
|
|
1663
|
+
`HELEKET_ENV`, `HELEKET_API_KEY`, `HELEKET_MERCHANT_ID`, `HELEKET_LIVE_MERCHANT_ID`,
|
|
1664
|
+
`HELEKET_WEBHOOK_IP=31.133.220.8`, `SKIP_BILLING=false`. Then create
|
|
1665
|
+
`src/lib/heleket-env.ts` (Section 3, copy verbatim) and call `assertHeleketEnv()` at
|
|
1666
|
+
the top of the client library — **before** step 4, so the first run of anything is
|
|
1667
|
+
already inside the boundary rather than being retrofitted into it later.
|
|
1329
1668
|
3. **Add Prisma model + enum** (Section 4) and run `npx prisma migrate dev --name
|
|
1330
1669
|
add_crypto_payments`.
|
|
1331
1670
|
4. **Create the client library** at `src/lib/heleket.ts` (Section 7, copy verbatim).
|
|
@@ -3,8 +3,8 @@ name: frontend-performance
|
|
|
3
3
|
description: >-
|
|
4
4
|
Use when building or auditing web pages for performance — running PageSpeed Insights,
|
|
5
5
|
optimizing Lighthouse scores, fixing render-blocking resources, reducing bundle size,
|
|
6
|
-
improving load times, or diagnosing slow page rendering. Covers Core Web Vitals (LCP,
|
|
7
|
-
CLS
|
|
6
|
+
improving load times, or diagnosing slow page rendering. Covers Core Web Vitals (LCP, INP,
|
|
7
|
+
CLS) and the Lighthouse diagnostics beside them (FCP, TBT, Speed Index), font loading strategies, CSS animation compositing, JavaScript
|
|
8
8
|
bundle optimization, cache headers, code splitting, Content Security Policy, image
|
|
9
9
|
optimization, and the contrast and heading issues that move a Lighthouse score. Triggers -
|
|
10
10
|
"performance audit", "PageSpeed", "Lighthouse", "Core Web Vitals", "LCP", "CLS", "INP",
|
|
@@ -15,7 +15,7 @@ description: >-
|
|
|
15
15
|
|
|
16
16
|
# Frontend Performance
|
|
17
17
|
|
|
18
|
-
Optimize frontend applications for maximum Lighthouse scores and real-user performance. This skill covers the technical performance layer -- for
|
|
18
|
+
Optimize frontend applications for maximum Lighthouse scores and real-user performance. This skill covers the technical performance layer -- for how it looks, see `sheleg-design`; for what the interface must do, see `super-ux`.
|
|
19
19
|
|
|
20
20
|
## Audit Workflow
|
|
21
21
|
|
|
@@ -25,16 +25,29 @@ Optimize frontend applications for maximum Lighthouse scores and real-user perfo
|
|
|
25
25
|
4. **Implement** -- Apply fixes per the reference guides.
|
|
26
26
|
5. **Validate** -- `next build` (or framework equivalent) + re-run Lighthouse. Verify no regressions.
|
|
27
27
|
|
|
28
|
-
## Core Web Vitals
|
|
28
|
+
## Core Web Vitals, and the diagnostics beside them
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**Three metrics are Core Web Vitals. The other three are not**, and the difference
|
|
31
|
+
is not pedantry: only the first three are what Google reports and ranks on, and
|
|
32
|
+
only they are field-measurable. TBT is a *lab* metric — web.dev says it "is not
|
|
33
|
+
part of the Core Web Vitals set because they are not field-measurable" — and it
|
|
34
|
+
stands in for INP when you have no field data. Telling a client their Speed Index
|
|
35
|
+
is a failing Core Web Vital is telling them about a thing Google does not measure.
|
|
36
|
+
|
|
37
|
+
| Core Web Vital | Good | Needs Work | Poor |
|
|
31
38
|
|--------|------|------------|------|
|
|
32
39
|
| LCP (Largest Contentful Paint) | < 2.5s | 2.5-4.0s | > 4.0s |
|
|
33
40
|
| INP (Interaction to Next Paint) | < 200ms | 200-500ms | > 500ms |
|
|
34
41
|
| CLS (Cumulative Layout Shift) | < 0.1 | 0.1-0.25 | > 0.25 |
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
42
|
+
|
|
43
|
+
| Lab diagnostic | Good | Needs Work | Poor | Stands in for |
|
|
44
|
+
|--------|------|------------|------|---|
|
|
45
|
+
| FCP (First Contentful Paint) | < 1.8s | 1.8-3.0s | > 3.0s | early LCP signal |
|
|
46
|
+
| TBT (Total Blocking Time) | < 200ms | 200-600ms | > 600ms | INP, in the lab |
|
|
47
|
+
| SI (Speed Index) | < 3.4s | 3.4-5.8s | > 5.8s | perceived load |
|
|
48
|
+
|
|
49
|
+
Thresholds and the classification verified against `web.dev/articles/vitals`,
|
|
50
|
+
2026-08-16.
|
|
38
51
|
|
|
39
52
|
## Performance Checklist
|
|
40
53
|
|
|
@@ -140,8 +153,13 @@ Set these limits for landing pages:
|
|
|
140
153
|
|
|
141
154
|
## Related Skills
|
|
142
155
|
|
|
143
|
-
- `
|
|
144
|
-
|
|
145
|
-
- `
|
|
146
|
-
- `seo-audit` --
|
|
147
|
-
|
|
156
|
+
- `sheleg-design` -- how it looks and moves, including the motion budget this page's
|
|
157
|
+
numbers constrain
|
|
158
|
+
- `super-ux` -- what the interface must do, and the flows a slow page loses
|
|
159
|
+
- `seo-aeo-audit` -- technical SEO and crawlability, which reads the same Core Web
|
|
160
|
+
Vitals from the field rather than the lab
|
|
161
|
+
|
|
162
|
+
Four names that used to sit here — `frontend-design`, `landing-page-design`,
|
|
163
|
+
`next-best-practices`, `responsive-design` — resolved to nothing installable, and
|
|
164
|
+
`seo-audit` was one character-class away from the real `seo-aeo-audit`, which is
|
|
165
|
+
how a typo survives review.
|