@ssheleg/sheleg-dev 0.11.8 → 0.12.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.
Files changed (24) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +4 -3
  3. package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
  4. package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +24 -8
  5. package/plugins/sheleg-dev/skills/ad-tracking/references/consent-mode.md +57 -12
  6. package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +24 -26
  7. package/plugins/sheleg-dev/skills/crypto-payments/references/callback-route-hardening.md +52 -0
  8. package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +42 -23
  9. package/plugins/sheleg-dev/skills/error-tracking/SKILL.md +34 -6
  10. package/plugins/sheleg-dev/skills/error-tracking/references/scrubbing.md +65 -2
  11. package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +51 -23
  12. package/plugins/sheleg-dev/skills/google-auth/SKILL.md +13 -1
  13. package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +13 -3
  14. package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +150 -64
  15. package/plugins/sheleg-dev/skills/google-auth/references/sign-in-with-google.md +36 -19
  16. package/plugins/sheleg-dev/skills/google-signin/SKILL.md +55 -23
  17. package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +150 -49
  18. package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +9 -7
  19. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/README.md +17 -1
  20. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/assert-money-invariants.mjs +171 -5
  21. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/manifest.json +333 -198
  22. package/plugins/sheleg-dev/skills/stripe-billing/fixtures/reference-handler.mjs +259 -35
  23. package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +149 -28
  24. package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +76 -5
@@ -220,18 +220,40 @@ cannot mask the defect — which is the only reason this fixture measures the or
220
220
 
221
221
  ## Idempotency store
222
222
 
223
+ A claim records **receipt**, and only completion records **completion**. A row that
224
+ cannot tell the two apart turns every crash between them into a swallowed payment: the
225
+ retry is told "duplicate" about work that never finished. So the row carries a state —
226
+ `'processing'` on claim, `'completed'` exactly once, in the same transaction as the
227
+ grant — and a claim on an existing row answers by that state, never by mere existence.
228
+
223
229
  ```sql
224
230
  CREATE TABLE processed_webhook_events (
225
231
  id TEXT PRIMARY KEY, -- Stripe's evt_… id
226
232
  source TEXT NOT NULL, -- 'stripe' — the table serves every provider
227
- processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
233
+ state TEXT NOT NULL DEFAULT 'processing', -- 'processing' → 'completed', nothing else
234
+ claimed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
235
+ completed_at TIMESTAMPTZ -- set once, in the transaction that granted
228
236
  );
229
- CREATE INDEX ON processed_webhook_events (processed_at);
237
+ CREATE INDEX ON processed_webhook_events (state, claimed_at);
230
238
  ```
231
239
 
232
- - `claimEvent` = `INSERT`; unique violation means duplicate.
233
- - `releaseEventClaim` = `DELETE`, called only when processing threw.
234
- - Prune older than ~30 days on a schedule. Stripe stops retrying after three
240
+ - `claimEvent` = `INSERT`; a unique violation is not yet an answer — read the row's
241
+ state. `'completed'` means duplicate: answer 200 and stop. `'processing'` younger
242
+ than the claim expiry means another worker is (or may still be) on it: answer 5xx so
243
+ this delivery retries too. `'processing'` older than the expiry belonged to a worker
244
+ that died between receipt and completion — take it over with an `UPDATE … SET
245
+ claimed_at = now() WHERE id = … AND state = 'processing' AND claimed_at < now() -
246
+ <expiry>`, one row updated means the takeover is yours, zero means somebody beat you.
247
+ - `completeEvent` = `UPDATE … SET state = 'completed', completed_at = now()` — inside
248
+ the transaction that writes the grant, so a row that says completed is one whose
249
+ business write committed.
250
+ - `releaseEventClaim` = `DELETE … WHERE state = 'processing'`, called only when
251
+ processing threw and the route answered 5xx. A crashed worker never reaches this
252
+ line — that is what the expiry exists for.
253
+ - The claim expiry is comfortably longer than your slowest handler run (say, twice the
254
+ route timeout). Too short reprocesses live work; there is no "too long" that loses an
255
+ event, only one that delays its recovery.
256
+ - Prune completed rows older than ~30 days on a schedule. Stripe stops retrying after three
235
257
  days, so anything older is dead weight — but do not prune to a window shorter
236
258
  than the retry window, or a late retry reprocesses.
237
259
  - A failure to *claim* (database down) is a 503, never an optimistic "probably
@@ -244,6 +266,55 @@ delivery to answer `duplicate` and the renewal notice and the conversion to fire
244
266
  that separates this claim from the per-period marker, because the marker reads before it
245
267
  writes and the claim does not.
246
268
 
269
+ ## One transaction, then the outbox
270
+
271
+ The business application is ONE transaction: the entitlement, the business
272
+ dedup marker (the per-period grant row), the completion mark and the **outbox
273
+ rows** for every side effect commit together, or none of them exist. A crash
274
+ before the commit applies nothing — the claim still says `processing`, and the
275
+ retry runs the whole application again. A crash after the commit changes
276
+ nothing either: the work is durable, the retry reads `completed`, and the side
277
+ effects wait in the outbox.
278
+
279
+ The outbox delivers **at least once**, so the consumer carries its **own
280
+ dedup key** — event id + effect kind — remembered across rows. A redelivered
281
+ outbox row must send nothing twice: the email provider and the ad platform do
282
+ not participate in your transaction, and the consumer key is the only thing
283
+ standing between a queue hiccup and a second "your renewal" email with a
284
+ double-counted conversion.
285
+
286
+ **The mirror is state; the ledger is history — and conflicts only delay.**
287
+ The subscription mirror (current period, status, quantity) lives apart from
288
+ the grant ledger: the mirror moves FORWARD only (a late-arriving older event
289
+ never rewinds a confirmed period — the ordering rule), while the ledger keeps
290
+ one row per granted period forever. And a database serialization conflict
291
+ retries inside the same claim, bounded (three attempts in the reference);
292
+ past the bound the route answers 5xx with the claim released, so the
293
+ redelivery brings the renewal back — a conflict may delay a grant, never
294
+ lose it, and a swallowed conflict answered 200 is a renewal that silently
295
+ never happened.
296
+
297
+ **The crash between receipt and completion is proved too.**
298
+ `crash-after-receipt-is-retryable` kills the worker after the claim and requires the
299
+ retry arriving past the claim expiry to be let in and to grant;
300
+ `in-flight-claim-answers-retry-later` requires the retry arriving *inside* the window
301
+ to be told 5xx come back — not "duplicate", which is an answer about a completed row;
302
+ `completion-is-recorded-with-the-grant` requires the claim row to leave `'processing'`
303
+ the moment the grant commits; and `duplicate-of-completed-never-regrants` ages a
304
+ completed row past the expiry and requires the late retry to answer duplicate and
305
+ grant nothing. All four run against `fixtures/reference-handler.mjs`.
306
+
307
+ **And the transaction boundary is proved.** `transaction-rolls-back-whole`
308
+ crashes inside the transaction and requires no grant, no marker, no outbox row
309
+ and no completion to survive; `crash-before-commit-applies-nothing` lets the
310
+ retry in through the expired claim and requires everything applied exactly
311
+ once; `committed-retry-sends-once` redelivers every outbox row and requires
312
+ the notice and the conversion to fire once — the consumer key at work.
313
+ `serialization-conflict-does-not-lose-the-renewal` injects a conflict and
314
+ requires the retried transaction to land the grant;
315
+ `exhausted-retries-answer-5xx-and-the-redelivery-lands` exhausts the bound
316
+ and requires an honest 5xx followed by exactly one grant on redelivery.
317
+
247
318
  ## What to log
248
319
 
249
320
  Structured, and enough to answer "what happened to this payment" without the