@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.
- package/CHANGELOG.md +16 -0
- package/package.json +4 -3
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +24 -8
- package/plugins/sheleg-dev/skills/ad-tracking/references/consent-mode.md +57 -12
- package/plugins/sheleg-dev/skills/crypto-payments/SKILL.md +24 -26
- package/plugins/sheleg-dev/skills/crypto-payments/references/callback-route-hardening.md +52 -0
- package/plugins/sheleg-dev/skills/crypto-payments/references/heleket-provider.md +42 -23
- package/plugins/sheleg-dev/skills/error-tracking/SKILL.md +34 -6
- package/plugins/sheleg-dev/skills/error-tracking/references/scrubbing.md +65 -2
- package/plugins/sheleg-dev/skills/frontend-performance/SKILL.md +51 -23
- package/plugins/sheleg-dev/skills/google-auth/SKILL.md +13 -1
- package/plugins/sheleg-dev/skills/google-auth/references/adc-and-service-accounts.md +13 -3
- package/plugins/sheleg-dev/skills/google-auth/references/oauth2-web-server.md +150 -64
- package/plugins/sheleg-dev/skills/google-auth/references/sign-in-with-google.md +36 -19
- package/plugins/sheleg-dev/skills/google-signin/SKILL.md +55 -23
- package/plugins/sheleg-dev/skills/google-signin/references/full-guide.md +150 -49
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +9 -7
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/README.md +17 -1
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/assert-money-invariants.mjs +171 -5
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/manifest.json +333 -198
- package/plugins/sheleg-dev/skills/stripe-billing/fixtures/reference-handler.mjs +259 -35
- package/plugins/sheleg-dev/skills/stripe-billing/references/subscription-lifecycle.md +149 -28
- package/plugins/sheleg-dev/skills/stripe-billing/references/webhook-events.md +76 -5
|
@@ -28,7 +28,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import assert from 'node:assert/strict';
|
|
30
30
|
import {
|
|
31
|
-
createStore, createHandler, subscriptionIdOf, RULES, CREDITS_PER_PERIOD,
|
|
31
|
+
createStore, createHandler, subscriptionIdOf, RULES, CREDITS_PER_PERIOD, CLAIM_TTL_MS,
|
|
32
|
+
TX_RETRIES,
|
|
32
33
|
} from './reference-handler.mjs';
|
|
33
34
|
|
|
34
35
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -117,7 +118,9 @@ export const INVARIANTS = [
|
|
|
117
118
|
id: 'sequential-redelivery-grants-once',
|
|
118
119
|
fixtures: [CYCLE_JAN, CYCLE_JAN_REDELIVERY],
|
|
119
120
|
states: 'the same evt_ delivered twice grants once AND runs its side effects once',
|
|
120
|
-
|
|
121
|
+
// `claim-completion` is here because "duplicate" is an answer about a COMPLETED row:
|
|
122
|
+
// a handler that never marks completion answers the redelivery in-flight instead.
|
|
123
|
+
breaks: ['claim', 'claim-completion', 'grant-on-renewal'],
|
|
121
124
|
async run({ store, handler, assert }) {
|
|
122
125
|
const first = await handler.deliver(fixture(CYCLE_JAN));
|
|
123
126
|
const second = await handler.deliver(fixture(CYCLE_JAN_REDELIVERY));
|
|
@@ -143,13 +146,176 @@ export const INVARIANTS = [
|
|
|
143
146
|
handler.deliver(fixture(CYCLE_JAN)),
|
|
144
147
|
handler.deliver(fixture(CYCLE_JAN_REDELIVERY)),
|
|
145
148
|
]);
|
|
146
|
-
const bodies = [a.body, b.body];
|
|
147
149
|
assert.equal(store.grants.length, 1, 'both deliveries granted');
|
|
148
150
|
assert.equal(store.credits.get('usr_PLACEHOLDER_alice'), CREDITS_PER_PERIOD,
|
|
149
151
|
'the user was credited twice for one renewal');
|
|
150
152
|
assert.equal(store.conversions.length, 1, 'the conversion fired twice');
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
// "duplicate" is an answer about a COMPLETED row. Mid-flight the loser answers 5xx
|
|
154
|
+
// so its own retry keeps the event alive if the winner dies before completing.
|
|
155
|
+
const winners = [a, b].filter((r) => r.status === 200 && r.body.duplicate !== true);
|
|
156
|
+
const loser = [a, b].find((r) => !winners.includes(r));
|
|
157
|
+
assert.ok(winners.length === 1 && loser
|
|
158
|
+
&& ((loser.status === 200 && loser.body.duplicate === true)
|
|
159
|
+
|| (loser.status === 500 && loser.body.error === 'in flight')),
|
|
160
|
+
'exactly one delivery processes; the other answers duplicate or in-flight, never grants');
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'crash-after-receipt-is-retryable',
|
|
165
|
+
fixtures: [CYCLE_JAN, CYCLE_JAN_REDELIVERY],
|
|
166
|
+
states: 'a worker that dies after claiming leaves a claim a later retry takes over — receipt is not completion',
|
|
167
|
+
breaks: ['claim-expiry', 'grant-on-renewal'],
|
|
168
|
+
async run({ store, handler, assert }) {
|
|
169
|
+
const dead = await handler.deliver(fixture(CYCLE_JAN), { crashAfterClaim: true });
|
|
170
|
+
assert.unmutated.equal(dead.status, 0, 'the crash simulation must not answer'); // the worker is gone
|
|
171
|
+
store.advanceClock(CLAIM_TTL_MS + 1);
|
|
172
|
+
const late = await handler.deliver(fixture(CYCLE_JAN_REDELIVERY));
|
|
173
|
+
assert.deepEqual(late.body, { received: true },
|
|
174
|
+
'the retry after the claim expiry was not let in — the payment is swallowed forever');
|
|
175
|
+
assert.equal(store.grants.length, 1, 'the recovered event did not grant');
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'in-flight-claim-answers-retry-later',
|
|
180
|
+
fixtures: [CYCLE_JAN, CYCLE_JAN_REDELIVERY],
|
|
181
|
+
states: 'a retry inside the claim window is told to come back — "duplicate" is an answer about a completed row',
|
|
182
|
+
breaks: ['claim'],
|
|
183
|
+
async run({ store, handler, assert }) {
|
|
184
|
+
await handler.deliver(fixture(CYCLE_JAN), { crashAfterClaim: true });
|
|
185
|
+
const early = await handler.deliver(fixture(CYCLE_JAN_REDELIVERY));
|
|
186
|
+
assert.ok(early.status === 500 && store.grants.length === 0,
|
|
187
|
+
'a retry inside the claim window must wait out a possibly live worker, not process or be told duplicate');
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'completion-is-recorded-with-the-grant',
|
|
192
|
+
fixtures: [CYCLE_JAN],
|
|
193
|
+
states: 'the claim row leaves processing the moment the grant commits — received and completed are different states',
|
|
194
|
+
breaks: ['claim-completion'],
|
|
195
|
+
async run({ store, handler, assert }) {
|
|
196
|
+
const event = fixture(CYCLE_JAN);
|
|
197
|
+
await handler.deliver(event);
|
|
198
|
+
const row = store.processedEvents.get(event.id);
|
|
199
|
+
// Conditional on the row existing so this isolates the completion mark: with no
|
|
200
|
+
// claim at all there is no row to leave in the wrong state.
|
|
201
|
+
assert.ok(!row || row.state === 'completed',
|
|
202
|
+
'the delivery answered done and its claim row still says processing — a crash-shaped lie');
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'duplicate-of-completed-never-regrants',
|
|
207
|
+
fixtures: [CYCLE_JAN, CYCLE_JAN_REDELIVERY],
|
|
208
|
+
states: 'a completed row stays completed: a retry arriving after the claim expiry answers duplicate and grants nothing',
|
|
209
|
+
breaks: ['claim', 'claim-completion', 'grant-on-renewal'],
|
|
210
|
+
async run({ store, handler, assert }) {
|
|
211
|
+
const first = await handler.deliver(fixture(CYCLE_JAN));
|
|
212
|
+
assert.unmutated.deepEqual(first.body, { received: true });
|
|
213
|
+
store.advanceClock(CLAIM_TTL_MS + 1);
|
|
214
|
+
const late = await handler.deliver(fixture(CYCLE_JAN_REDELIVERY));
|
|
215
|
+
assert.deepEqual(late.body, { received: true, duplicate: true },
|
|
216
|
+
'a completed event aged past the claim expiry was reprocessed — completion was never recorded');
|
|
217
|
+
assert.equal(store.grants.length, 1, 'a completed event was granted again');
|
|
218
|
+
assert.equal(store.notifications.length, 1, '"your renewal" was sent twice');
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'transaction-rolls-back-whole',
|
|
223
|
+
fixtures: [CYCLE_JAN],
|
|
224
|
+
states: 'a crash inside the transaction leaves nothing: no grant, no marker, no outbox row, no completion',
|
|
225
|
+
breaks: ['atomic-application'],
|
|
226
|
+
async run({ store, handler, assert }) {
|
|
227
|
+
const dead = await handler.deliver(fixture(CYCLE_JAN), { crashBeforeCommit: true });
|
|
228
|
+
assert.unmutated.equal(dead.status, 0, 'the crash simulation must not answer');
|
|
229
|
+
assert.ok(store.grants.length === 0 && store.grantedPeriods.size === 0
|
|
230
|
+
&& store.outbox.length === 0
|
|
231
|
+
&& ![...store.processedEvents.values()].some((r) => r.state === 'completed'),
|
|
232
|
+
'a crash before the commit left part of the application behind — half a payment no retry can see');
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'crash-before-commit-applies-nothing',
|
|
237
|
+
fixtures: [CYCLE_JAN, CYCLE_JAN_REDELIVERY],
|
|
238
|
+
states: 'after a mid-transaction crash, the retry applies everything exactly once',
|
|
239
|
+
// `claim-expiry` is here because the retry enters through the expired-claim door.
|
|
240
|
+
breaks: ['atomic-application', 'claim-expiry', 'grant-on-renewal'],
|
|
241
|
+
async run({ store, handler, assert }) {
|
|
242
|
+
const dead = await handler.deliver(fixture(CYCLE_JAN), { crashBeforeCommit: true });
|
|
243
|
+
assert.unmutated.equal(dead.status, 0, 'the crash simulation must not answer');
|
|
244
|
+
store.advanceClock(CLAIM_TTL_MS + 1);
|
|
245
|
+
const retry = await handler.deliver(fixture(CYCLE_JAN_REDELIVERY));
|
|
246
|
+
assert.deepEqual(retry.body, { received: true },
|
|
247
|
+
'the retry after the crash was not let in');
|
|
248
|
+
assert.equal(store.grants.length, 1, 'the retry did not apply the payment exactly once');
|
|
249
|
+
assert.equal(store.notifications.length, 1, 'the renewal notice did not go out exactly once');
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'committed-retry-sends-once',
|
|
254
|
+
fixtures: [CYCLE_JAN],
|
|
255
|
+
states: 'after the commit, a redelivered outbox row sends nothing twice — the consumer holds its own key',
|
|
256
|
+
breaks: ['outbox-consumer-key', 'grant-on-renewal'],
|
|
257
|
+
async run({ store, handler, assert }) {
|
|
258
|
+
await handler.deliver(fixture(CYCLE_JAN));
|
|
259
|
+
assert.equal(store.notifications.length, 1, 'the first drain did not send');
|
|
260
|
+
// The queue redelivers: every sent row comes back pending. At-least-once is
|
|
261
|
+
// the outbox's contract, so this is the normal case, not the weird one.
|
|
262
|
+
for (const row of store.outbox) row.state = 'pending';
|
|
263
|
+
handler.drainOutbox();
|
|
264
|
+
assert.equal(store.notifications.length, 1, '"your renewal" was sent twice');
|
|
265
|
+
assert.equal(store.conversions.length, 1,
|
|
266
|
+
'the conversion fired twice — the revenue is counted twice');
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'concurrent-entry-points-grant-once',
|
|
271
|
+
fixtures: [CYCLE_JAN],
|
|
272
|
+
states: 'a webhook and the reconciler racing for one period leave exactly one keyed ledger row — the claim is an INSERT, and a read is a round trip',
|
|
273
|
+
breaks: ['grant-key-atomic'],
|
|
274
|
+
async run({ store, handler, assert }) {
|
|
275
|
+
const event = fixture(CYCLE_JAN);
|
|
276
|
+
const subId = subscriptionIdOf(event.data.object);
|
|
277
|
+
await Promise.all([
|
|
278
|
+
handler.deliver(event),
|
|
279
|
+
handler.reconcile(subId, periodOfCycle(event), { userId: 'usr_PLACEHOLDER_alice' }),
|
|
280
|
+
]);
|
|
281
|
+
// Judged on the LEDGER, deliberately: the keyed row is the atomic claim's own
|
|
282
|
+
// axis. Grant counts belong to the marker invariants; measuring them here would
|
|
283
|
+
// blur the two rules this pack keeps isolated.
|
|
284
|
+
assert.equal(store.grantLedger.length, 1,
|
|
285
|
+
`the race left ${store.grantLedger.length} keyed ledger rows for one period`);
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'serialization-conflict-does-not-lose-the-renewal',
|
|
290
|
+
fixtures: [CYCLE_JAN],
|
|
291
|
+
states: 'a conflicted transaction retries inside the same claim and the renewal lands — a conflict delays a grant, never loses it',
|
|
292
|
+
breaks: ['tx-retry-bounded', 'grant-on-renewal'],
|
|
293
|
+
async run({ store, handler, assert }) {
|
|
294
|
+
store.failNextTransactions = 1;
|
|
295
|
+
const r = await handler.deliver(fixture(CYCLE_JAN));
|
|
296
|
+
assert.unmutated.equal(r.status, 200);
|
|
297
|
+
assert.equal(store.grants.length, 1,
|
|
298
|
+
'the conflicted transaction was dropped and the renewal never landed');
|
|
299
|
+
assert.equal(store.txAttempts, 2, 'the retry did not actually run a second attempt');
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'exhausted-retries-answer-5xx-and-the-redelivery-lands',
|
|
304
|
+
fixtures: [CYCLE_JAN, CYCLE_JAN_REDELIVERY],
|
|
305
|
+
states: 'past the bound the route answers 5xx with the claim released, and the redelivery grants exactly once',
|
|
306
|
+
// atomic-application is here because without the rollback, the conflicted
|
|
307
|
+
// attempts leave their halves behind and every count below drifts.
|
|
308
|
+
breaks: ['atomic-application', 'tx-retry-bounded', 'grant-on-renewal'],
|
|
309
|
+
async run({ store, handler, assert }) {
|
|
310
|
+
store.failNextTransactions = TX_RETRIES + 1;
|
|
311
|
+
const first = await handler.deliver(fixture(CYCLE_JAN));
|
|
312
|
+
assert.equal(first.status, 500,
|
|
313
|
+
`exhausted retries answered ${first.status} — a swallowed conflict lies "done"`);
|
|
314
|
+
assert.equal(store.grants.length, 0,
|
|
315
|
+
'grants landed from attempts that were supposed to roll back');
|
|
316
|
+
const retry = await handler.deliver(fixture(CYCLE_JAN_REDELIVERY));
|
|
317
|
+
assert.equal(retry.status, 200, 'the redelivery was not let back in');
|
|
318
|
+
assert.equal(store.grants.length, 1, 'the redelivery did not land the renewal exactly once');
|
|
153
319
|
},
|
|
154
320
|
},
|
|
155
321
|
{
|