@ssheleg/sheleg-dev 0.4.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 (32) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +132 -0
  4. package/SECURITY.md +63 -0
  5. package/bin/sheleg-dev.js +99 -0
  6. package/package.json +45 -0
  7. package/plugins/sheleg-dev/.claude-plugin/plugin.json +28 -0
  8. package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +442 -0
  9. package/plugins/sheleg-dev/skills/ad-tracking/references/consent-mode.md +257 -0
  10. package/plugins/sheleg-dev/skills/ad-tracking/references/event-tracking.md +201 -0
  11. package/plugins/sheleg-dev/skills/ad-tracking/references/gtag-api.md +389 -0
  12. package/plugins/sheleg-dev/skills/ad-tracking/references/meta-linkedin.md +103 -0
  13. package/plugins/sheleg-dev/skills/ad-tracking/references/performance-security.md +288 -0
  14. package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +384 -0
  15. package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +1357 -0
  16. package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +147 -0
  17. package/plugins/sheleg-dev/skills/frontend-performance/references/accessibility-perf.md +172 -0
  18. package/plugins/sheleg-dev/skills/frontend-performance/references/css-performance.md +180 -0
  19. package/plugins/sheleg-dev/skills/frontend-performance/references/nextjs.md +271 -0
  20. package/plugins/sheleg-dev/skills/google-auth/SKILL.md +275 -0
  21. package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +464 -0
  22. package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +615 -0
  23. package/plugins/sheleg-dev/skills/google-auth/references/sign-in-with-google.md +593 -0
  24. package/plugins/sheleg-dev/skills/google-auth/references/workload-identity.md +376 -0
  25. package/plugins/sheleg-dev/skills/google-signin/SKILL.md +121 -0
  26. package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +421 -0
  27. package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +458 -0
  28. package/plugins/sheleg-dev/skills/stripe-billing/references/price-integrity.md +201 -0
  29. package/plugins/sheleg-dev/skills/stripe-billing/references/stripe-agent-toolchain.md +173 -0
  30. package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +294 -0
  31. package/plugins/sheleg-dev/skills/stripe-billing/references/testing-and-local-dev.md +214 -0
  32. package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +211 -0
@@ -0,0 +1,211 @@
1
+ # Webhook events — the catalogue and the contract
2
+
3
+ **Load this when** writing or reviewing the webhook handler: which events to
4
+ subscribe to, what each payload actually carries, and the ordering and failure
5
+ rules that decide whether a payment becomes an entitlement exactly once.
6
+
7
+ ## Contents
8
+
9
+ - [The endpoint contract](#the-endpoint-contract)
10
+ - [Which events to subscribe to](#which-events-to-subscribe-to)
11
+ - [checkout.session.completed](#checkoutsessioncompleted)
12
+ - [Asynchronous payment methods](#asynchronous-payment-methods)
13
+ - [invoice.paid](#invoicepaid)
14
+ - [invoice.payment_failed and dunning](#invoicepayment_failed-and-dunning)
15
+ - [customer.subscription.updated](#customersubscriptionupdated)
16
+ - [customer.subscription.deleted](#customersubscriptiondeleted)
17
+ - [charge.refunded and charge.dispute.created](#chargerefunded-and-chargedisputecreated)
18
+ - [Ordering is not guaranteed](#ordering-is-not-guaranteed)
19
+ - [Idempotency store](#idempotency-store)
20
+ - [What to log](#what-to-log)
21
+
22
+ ## The endpoint contract
23
+
24
+ | Situation | Status | Why |
25
+ |---|---|---|
26
+ | handled successfully | 200 | |
27
+ | duplicate delivery | 200 | a non-2xx makes Stripe retry forever |
28
+ | event type you do not handle | 200 | subscribing to more than you handle is normal |
29
+ | missing or invalid signature | 400 | and no detail in the body — it is an oracle |
30
+ | webhook secret not configured | 500 | a misconfigured endpoint must not look healthy |
31
+ | your idempotency store is down | 503 | you cannot prove this is not a duplicate |
32
+ | handler threw | 500 | after releasing the claim, so the retry can work |
33
+
34
+ Stripe retries with exponential backoff for up to three days, then disables the
35
+ endpoint and emails the account. A handler that answers 200 on failure gets no
36
+ retry and no email — the payment is simply gone from your side.
37
+
38
+ Respond fast. Heavy work belongs in a queue: Stripe times out at 30 seconds and
39
+ counts the timeout as a failure.
40
+
41
+ ## Which events to subscribe to
42
+
43
+ A subscription product needs seven; anything else is noise you will have to read
44
+ through during an incident.
45
+
46
+ | Event | Why |
47
+ |---|---|
48
+ | `checkout.session.completed` | the first grant |
49
+ | `checkout.session.async_payment_succeeded` | delayed methods that later clear |
50
+ | `checkout.session.async_payment_failed` | delayed methods that later fail |
51
+ | `invoice.paid` | renewals, and the proration invoices that must *not* grant |
52
+ | `invoice.payment_failed` | dunning starts; mark past due |
53
+ | `customer.subscription.updated` | plan change, quantity change, cancel scheduled, status change |
54
+ | `customer.subscription.deleted` | the teardown, from any source |
55
+ | `charge.refunded` | claw back what was granted |
56
+
57
+ Add `charge.dispute.created` if disputes are a real risk for your business, and
58
+ `customer.subscription.trial_will_end` if you run trials.
59
+
60
+ ## checkout.session.completed
61
+
62
+ ```ts
63
+ if (session.payment_status === "unpaid") return; // an async method: wait for the async event
64
+ const userId = session.metadata?.userId; // set at session creation
65
+ if (!userId) { log.error("session without userId"); return; } // 200, but loudly
66
+ ```
67
+
68
+ - `mode` is `"subscription"` or `"payment"` — branch on it before anything else.
69
+ - The session's `subscription` field is an **id**, not an object, unless you
70
+ expanded it. Retrieve it to read items, quantity and period.
71
+ - Uniqueness key: the subscription id for subscriptions, the payment intent id
72
+ for one-off payments. Put a unique constraint on the column and let the
73
+ database arbitrate — the `SELECT` that precedes the `INSERT` is a race.
74
+ - A user that no longer exists is a 200 with an error log, not a 500. Retrying
75
+ will not bring them back.
76
+
77
+ ## Asynchronous payment methods
78
+
79
+ Bank debits, vouchers and bank transfers complete the session before the money
80
+ moves. `payment_status: "unpaid"` at `checkout.session.completed` means the
81
+ grant belongs to `async_payment_succeeded`, which may arrive days later — and
82
+ `async_payment_failed` means it never will. Both must be handled or the product
83
+ is either never granted or granted for a payment that failed.
84
+
85
+ ## invoice.paid
86
+
87
+ The single most consequential field is `billing_reason`:
88
+
89
+ | Value | Meaning | Grant? |
90
+ |---|---|---|
91
+ | `subscription_create` | first invoice for a new subscription | **no** — checkout did it |
92
+ | `subscription_cycle` | the renewal | **yes** |
93
+ | `subscription_update` | mid-cycle proration (quantity or plan change) | **no** |
94
+ | `subscription_threshold` | a usage threshold was crossed | explicitly, if you use them |
95
+ | `manual` | an invoice you created | explicitly |
96
+
97
+ Reading the subscription id off an invoice depends on the API version. On
98
+ current versions it hangs off `invoice.parent.subscription_details.subscription`
99
+ and may be a string or an object:
100
+
101
+ ```ts
102
+ function subscriptionIdOf(invoice: Stripe.Invoice): string | null {
103
+ const ref = invoice.parent?.subscription_details?.subscription;
104
+ if (typeof ref === "string") return ref;
105
+ if (ref && typeof ref === "object") return ref.id;
106
+ return null; // not a subscription invoice
107
+ }
108
+ ```
109
+
110
+ Period dates live on the **subscription item**
111
+ (`sub.items.data[0].current_period_start` / `current_period_end`), not on the
112
+ subscription. Code that reads the old top-level fields gets `undefined` and
113
+ stores an epoch date — no error, wrong renewal date forever.
114
+
115
+ Guard the grant with a marker that survives a replay: a `lastGrantedPeriodStart`
116
+ compared against the new period start, or an audit row keyed by `invoice.id`.
117
+ Check it **inside** the same transaction as the grant.
118
+
119
+ ## invoice.payment_failed and dunning
120
+
121
+ Mark the subscription `past_due` and tell the user, with a link to the customer
122
+ portal — this is the highest-value notification in the whole integration.
123
+
124
+ Do not cancel anything here. Stripe's dunning settings decide how many retries
125
+ happen over how many days and what the terminal state is (`canceled` or
126
+ `unpaid`); when it ends you receive `customer.subscription.deleted` or an
127
+ `updated` with the final status. Cancelling early cancels customers whose second
128
+ attempt would have succeeded.
129
+
130
+ ## customer.subscription.updated
131
+
132
+ One event covers plan change, quantity change, `cancel_at_period_end` flipping,
133
+ trial ending, and status transitions. Diff against your stored row and act only
134
+ on what actually changed.
135
+
136
+ The `previous_attributes` object on the event tells you what changed — but it is
137
+ on the **event**, not the subscription object, so a handler typed as
138
+ `(sub: Stripe.Subscription)` cannot see it. Pass the event through if you need
139
+ it.
140
+
141
+ For seat and metered products, quantity changes are usually driven by your own
142
+ endpoints, which already wrote the new quantity. Make this handler idempotent
143
+ against its own writes rather than trying to detect "who did this".
144
+
145
+ ## customer.subscription.deleted
146
+
147
+ The teardown, and the only place it should live: it fires whether the
148
+ cancellation came from your UI, the customer portal, dunning, or an admin in the
149
+ Dashboard.
150
+
151
+ Order matters:
152
+
153
+ 1. external calls that can fail (reclaim balances, disable provisioned
154
+ resources) — outside the transaction, each individually caught;
155
+ 2. one transaction: reclaim credit, unlink resources, mark canceled;
156
+ 3. after commit: stop containers, send notifications, emit analytics.
157
+
158
+ Doing (3) inside the transaction sends "your subscription ended" for a
159
+ transaction that then rolls back.
160
+
161
+ ## charge.refunded and charge.dispute.created
162
+
163
+ `amount_refunded` is **cumulative**. Compute the increment against your stored
164
+ total under a compare-and-swap; see the `SKILL.md` section. Resolve the target
165
+ two ways: by `payment_intent` for one-off purchases, and by `charge.invoice` →
166
+ invoice → subscription for subscription refunds. A refund handler that only
167
+ knows the first path silently ignores every subscription refund.
168
+
169
+ `charge.dispute.created` is money already gone plus a fee. Treat it as a refund
170
+ for entitlement purposes and route it to a human — evidence has a deadline.
171
+
172
+ ## Ordering is not guaranteed
173
+
174
+ Events can arrive out of order, and Stripe's own docs say so. Two consequences:
175
+
176
+ - **Never derive state from arrival order.** Derive it from the object in the
177
+ payload, or re-retrieve the object and use that.
178
+ - **Guard on state, not on time.** "Only apply if the stored period is older"
179
+ beats "apply if this event is newer than the last one I saw", because the
180
+ second one trusts a clock you do not own.
181
+
182
+ For a subscription whose state you cannot reconstruct from one event, retrieve
183
+ the subscription fresh inside the handler. One extra API call is cheaper than a
184
+ class of ordering bugs.
185
+
186
+ ## Idempotency store
187
+
188
+ ```sql
189
+ CREATE TABLE processed_webhook_events (
190
+ id TEXT PRIMARY KEY, -- Stripe's evt_… id
191
+ source TEXT NOT NULL, -- 'stripe' — the table serves every provider
192
+ processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
193
+ );
194
+ CREATE INDEX ON processed_webhook_events (processed_at);
195
+ ```
196
+
197
+ - `claimEvent` = `INSERT`; unique violation means duplicate.
198
+ - `releaseEventClaim` = `DELETE`, called only when processing threw.
199
+ - Prune older than ~30 days on a schedule. Stripe stops retrying after three
200
+ days, so anything older is dead weight — but do not prune to a window shorter
201
+ than the retry window, or a late retry reprocesses.
202
+ - A failure to *claim* (database down) is a 503, never an optimistic "probably
203
+ new".
204
+
205
+ ## What to log
206
+
207
+ Structured, and enough to answer "what happened to this payment" without the
208
+ Dashboard: event id, event type, subscription or payment intent id, the user id
209
+ from metadata, the decision taken ("granted", "skipped: subscription_update",
210
+ "duplicate"), and the outcome. Never the raw payload — it contains customer
211
+ data — and never anything key-shaped.