Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .kilo/skills/kilo-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ to send platform feedback and bug reports rather than a support channel.
The first path segment names an area. Search to learn an area's vocabulary:
each row's `summary` and `tags` are what the search matches.

**863 procedures** — **370 queries**, **493 mutations** — under **53 prefixes**.
**860 procedures** — **369 queries**, **491 mutations** — under **53 prefixes**.

| Prefix | Procedures | Queries | Mutations |
|---|---:|---:|---:|
Expand All @@ -61,9 +61,9 @@ each row's `summary` and `tags` are what the search matches.
| `cloudAgentNext` | 31 | 12 | 19 |
| `agentProfiles` | 30 | 4 | 26 |
| `securityAgent` | 26 | 15 | 11 |
| `kiloPass` | 21 | 11 | 10 |
| `githubPrReview` | 20 | 6 | 14 |
| `githubApps` | 19 | 9 | 10 |
| `kiloPass` | 18 | 10 | 8 |
| `cliSessionsV2` | 17 | 9 | 8 |
| `appBuilder` | 16 | 6 | 10 |
| `cliSessions` | 16 | 7 | 9 |
Expand Down
59 changes: 30 additions & 29 deletions .specs/kilo-pass.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ Kilo Pass exchanges a recurring payment for monthly base credits and a usage-tri
and yearly subscriptions. The mobile app does not expose native Kilo Pass purchases, restores, or store management.
Its Profile card and `/kilo-pass` route render the backend purchase presentation: `unavailable` has no purchase controls,
while `web_management` exposes Manage, which opens the returned `webUrl`. Native credit-pack purchases are unchanged.
Backend store verification, purchase completion, provider notifications, and expiry reconciliation remain available
for existing Kilo Pass store subscriptions independently of the mobile presentation.
The backend no longer verifies or completes Kilo Pass store purchases, reconciles their expiry, or applies their
notification lifecycle. Authenticated store notifications are recorded for deduplication and acknowledged without
subscription mutations, credits, refunds, or provider API calls. The notification endpoints remain for credit-pack
refunds and restorations; historical store subscriptions and purchase rows remain readable without a data migration.

Store subscription notifications, including Play renewals for blocked accounts, MUST be acknowledged and ignored.

A successful base-credit grant writes one threshold on the user row. When cumulative user usage reaches the effective
threshold, bonus logic acts on the selected effective active subscription. Monthly subscriptions use the tenure ramp
Expand All @@ -108,12 +112,12 @@ with welcome-promo overrides. Yearly subscriptions use a flat 50% monthly bonus.
| Capability | Stripe | App Store | Google Play |
|---|---|---|---|
| Persisted provider representation | Yes | Yes | Yes |
| Web state reads | Yes | Yes | Existing rows only |
| Monthly subscription entrypoint | Yes | No native mobile UI | No native mobile UI |
| Web state reads | Yes | Historical rows only | Historical rows only |
| Monthly subscription entrypoint | Yes | Removed | Removed |
| Yearly subscription entrypoint | Yes | Not exposed | Not exposed |
| Verified purchase completion ingress | Invoice-paid webhook | Signed transaction completion | Not exposed |
| Provider notification handling | Stripe events | App Store server notifications | Not exposed |
| Store-expiry reconciliation | N/A | Yes | Existing rows only |
| Verified purchase completion ingress | Invoice-paid webhook | Removed | Removed |
| Provider subscription notifications | Stripe events | Acknowledge and ignore | Acknowledge and ignore |
| Persisted store-expiry reconciliation | N/A | Removed; derive expiry on reads | Removed; derive expiry on reads |
| Duplicate-card gate | Yes | No | No |
| Scheduled tier/cadence changes | Yes | No | No |

Expand All @@ -133,8 +137,8 @@ with welcome-promo overrides. Yearly subscriptions use a flat 50% monthly bonus.
subscription row.
5. KiloClaw pending-balance reads MUST apply the same persisted-row ranking and MUST derive an open pause after
selection. That path does not derive store expiration.
6. Store purchase completion MUST use its own active-row check: the first user subscription row with a null ended
marker. It does not reuse the general effective-subscription selector or apply an explicit ordering.
6. Store subscription purchase completion MUST NOT be exposed. Historical rows MUST remain readable using the existing
effective-subscription selector and expiry derivation.
7. Persisted subscription rows MUST contain one provider shape at a time: Stripe rows use matching provider and Stripe
subscription identifiers; store rows use a provider subscription identifier and no Stripe subscription identifier.

Expand All @@ -143,8 +147,8 @@ with welcome-promo overrides. Yearly subscriptions use a flat 50% monthly bonus.
8. A handled Stripe `invoice.paid` event MUST issue base credits equal to the tier's configured monthly price,
independent of charged amount, tax, discount, or proration. A handled zero-dollar invoice still qualifies for base
credits.
9. An accepted store purchase MUST issue base credits equal to the tier's configured monthly price, except for App Store
same-period tier upgrades, which replace the current-period base grant through the upgrade-adjustment path.
9. Store subscription notifications MUST NOT grant, claw back, or restore Kilo Pass credits. One-off credit-pack
purchase, refund, and restoration handling MUST remain unchanged.
10. Stripe yearly subscriptions MUST receive an initial monthly base grant from invoice handling and later monthly base
grants from the yearly monthly-base cron. The cron processes Stripe rows only.
11. Base credits for a subscription and issue month MUST be issued at most once through the normal issuance path.
Expand Down Expand Up @@ -279,12 +283,8 @@ with welcome-promo overrides. Yearly subscriptions use a flat 50% monthly bonus.
47. Pause months consume scan budget. Monthly streak MUST NOT be described as an unbounded lifetime tenure count.
48. Stripe monthly invoice handling MUST reset streak to `1` when previous persisted provider status was ended. Recovery
from a non-ended transitional status such as `past_due` does not, by itself, reset streak.
49. Store monthly completion MUST recompute streak from the capped issuance-and-pause scan. It does not separately reset
streak because a prior store row was ended. Reactivation under the same provider subscription MAY reconnect
historical streak when issue months remain contiguous.
50. Stripe yearly invoice handling MUST store streak `0` and track the next monthly issue cursor instead. Generic store
purchase completion accepts yearly input internally but sets initial streak `1`; exposed store products are monthly
only and no store-yearly monthly-base cron exists.
49. Historical store streak values and issuance rows MUST remain unchanged; store notifications no longer recompute them.
50. Stripe yearly invoice handling MUST store streak `0` and track the next monthly issue cursor instead.

### Duplicate-Card Gate

Expand Down Expand Up @@ -348,8 +348,8 @@ Rules 51-55 protect against rapid cross-account reuse of a recently first-used c
pending cancellation overlap, current renewal-row UI gives active-until display precedence.
65. On the web read path, a selected store subscription whose latest purchase expired at or before now MUST be returned
as derived `canceled`, even if provider end notification was not received.
66. Store-expiry reconciliation MUST scan non-canceled App Store and Google Play rows, skip rows without purchases, and
persist `canceled`, clear pending cancellation, and set ended marker when latest purchase expired.
66. Store-expiry reconciliation MUST NOT run or persist cancellation. Store notifications MUST be acknowledged and
ignored, including purchases, renewals, expiry, cancellation, refund, revocation, and refund reversal.

### Bonus Expiry

Expand All @@ -368,15 +368,14 @@ Rules 51-55 protect against rapid cross-account reuse of a recently first-used c
attempt a separate failed audit write after rollback and report audit-write failure operationally.
73. Yearly monthly-base cron MUST append run and subscription audit entries. A per-subscription issuance failure MUST
append a failed audit entry and rethrow.
74. Store-expiry reconciliation MUST append success audit after persisted cancellation. App Store expiry notifications
also append success audit after persisting ended state.
74. Ignored store subscription notifications MUST retain store-event deduplication records but MUST NOT append
subscription lifecycle audit entries or change historical subscription rows.
75. Duplicate-card blocking MUST commit before provider enforcement and remain authoritative replay evidence. The block
audit MUST identify the matched first fingerprint claim and fingerprint digest without recording the raw fingerprint.
Cancellation and refund outcomes MUST be recorded separately in audit payloads. Cancellation failure MUST throw for
webhook retry; refund failure MUST remain observable without reversing the block or granting credits.
76. Repeated base or bonus issuance handled by normal issuance helpers MUST append skipped-idempotent audit entries.
Store-transaction replay and usage-triggered prechecks that find an existing bonus-like item return without an
equivalent skipped-idempotent audit entry.
Usage-triggered prechecks that find an existing bonus-like item return without an equivalent skipped-idempotent audit entry.
77. Usage-triggered bonus call sites do not share one failure-audit wrapper. Some callers append a failed audit entry,
while others log or propagate the error only.

Expand Down Expand Up @@ -410,18 +409,14 @@ The following behavior or stronger guarantees are not implemented by current cod

1. Deterministic identifier tiebreak for equal-recency subscription rows.
2. Effective-subscription reselection after late-derived pause or store expiration.
3. Exposed verified Google Play purchase completion and provider notification handling.
4. Mobile-store yearly products and store-yearly monthly issuance lifecycle.
5. Dedicated durable workflow state for duplicate-card cancellation or refund partial failures beyond existing audits and
Stripe idempotency keys.
6. One canonical projection path that matches issuance eligibility, existing bonus-like item checks, scheduled-change
target behavior, and stored Stripe promo reason across every consumer.
7. Unbounded or explicitly durable streak accounting beyond the 36-month scan cap.
8. Explicit ended-state streak reset for reactivated store subscriptions.
9. Guaranteed expiry for every granted bonus credit.
10. Independent durable audit recording for every failed provider or issuance operation.
11. Store-provider welcome-promo anti-abuse signals equivalent to Stripe fingerprint claims.
12. Atomic prevention of concurrent or repeated active Kilo Pass purchases by the same user. Duplicate-card Rules 51-55
11. Atomic prevention of concurrent or repeated active Kilo Pass purchases by the same user. Duplicate-card Rules 51-55
intentionally exclude same-user purchases.

## Adjacent Spec Compatibility
Expand All @@ -436,6 +431,13 @@ promo or Kilo Pass referral conversion.

## Changelog

### 2026-11-09 -- Store subscription backend retirement

- Removed App Store and Play Kilo Pass catalogs, verification, completion, and expiry reconciliation.
- Store subscription notifications are acknowledged and ignored, while credit-pack refund and restoration paths remain.
- Shared Apple receipt decoding remains in the store SDK for credit-pack verification; subscription-only verifiers are removed.
- Retained historical store data, expiry-derived reads, provider-management guards, and Stripe Kilo Pass.

### 2026-10-08 -- Timestamp-based monthly welcome bonus cutoff

- Eligible first-time monthly starts strictly before October 9, 2026 at midnight UTC receive 50%, 10%, then 15%.
Expand All @@ -448,7 +450,6 @@ promo or Kilo Pass referral conversion.
- Kept the selected welcome month in ramp totals after the welcome callout disappears.
- Pending referral rewards skip the welcome-month issuance and apply to the next eligible issuance
(`.specs/impact-referrals.md` Rule 123).

### 2026-06-08 -- Yearly duplicate-card enforcement

- First paid yearly purchases are now subject to the same 24-hour cross-account card-reuse restriction as first paid
Expand Down
18 changes: 9 additions & 9 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,15 @@ The connection-role migration preserves a sole eligible connection, prefers an u
- `CHURNKEY_API_SECRET` - Secret for Churnkey (cancellation flows). `[SECRET]`
- `NEXT_PUBLIC_CHURNKEY_APP_ID` - Public app ID for Churnkey widget. [PUBLIC]

### Apple / In-App Purchases

- `APPLE_APP_APPLE_ID` - Apple App ID for IAP verification. `[SECRET]`
- `APPLE_IAP_ENVIRONMENT` - Apple IAP environment (`Sandbox` or `Production`). [SERVER]
- `APPLE_IAP_KEY_ID` - Apple IAP key identifier. `[SECRET]`
- `APPLE_IAP_ISSUER_ID` - Apple IAP issuer (team) ID. `[SECRET]`
- `APPLE_IAP_PRIVATE_KEY` - Apple IAP private key (PEM/ES256) for receipt validation. `[SECRET]`
- `APPLE_ROOT_CERTIFICATES_PEM` - Apple root CA certs (PEM) for validating IAP receipts. [SERVER]
- `GOOGLE_PLAY_PUBLISHER_SERVICE_ACCOUNT_JSON` - Service account JSON for the Android Publisher API (subscriptions v2 get). `[SECRET]`
### Apple / Google Play Credit-Pack Purchases

- `APPLE_APP_APPLE_ID` - Apple App ID for credit-pack receipt and server-notification verification. `[SECRET]`
- `APPLE_IAP_ENVIRONMENT` - Apple credit-pack IAP environment (`Sandbox` or `Production`). [SERVER]
- `APPLE_IAP_KEY_ID` - Apple server API key identifier, retained for credit-pack consumption requests. `[SECRET]`
- `APPLE_IAP_ISSUER_ID` - Apple server API issuer (team) ID, retained for credit-pack consumption requests. `[SECRET]`
- `APPLE_IAP_PRIVATE_KEY` - Apple server API private key (PEM/ES256), retained for credit-pack consumption requests. `[SECRET]`
- `APPLE_ROOT_CERTIFICATES_PEM` - Apple root CA certs (PEM) for validating credit-pack receipts and server notifications. [SERVER]
- `GOOGLE_PLAY_PUBLISHER_SERVICE_ACCOUNT_JSON` - Android Publisher service account JSON for credit-pack purchase verification, consumption, and refund order lookup. Kilo Pass subscription API access is no longer used. `[SECRET]`
- `GOOGLE_PLAY_RTDN_PUSH_AUDIENCE` - Expected OIDC audience for Play Real-time Developer Notification Pub/Sub push (the HTTPS URL of POST /api/kilo-pass/play/notifications). `[SERVER]`
- `GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL` - Expected service-account email for the Play RTDN Pub/Sub push OIDC token; the token `email` claim must match it. [SERVER]
- `APPLE_APP_BUNDLE_ID` - iOS app bundle ID for Apple App Attest and Sign In verification. [SERVER]
Expand Down

This file was deleted.

10 changes: 4 additions & 6 deletions apps/web/src/lib/credits/store-verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { createHash } from 'node:crypto';
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import type { androidpublisher_v3 } from '@googleapis/androidpublisher';

import type { AppleStoreDecodedTransaction } from '@/lib/kilo-pass/apple-store-verifier';
import type * as AppleStoreVerifier from '@/lib/kilo-pass/apple-store-verifier';
import type { AppleStoreDecodedTransaction } from '@/lib/kilo-pass/apple-store-sdk';
import type * as AppleStoreVerifier from '@/lib/kilo-pass/apple-store-sdk';
import { KiloPassPaymentProvider } from '@kilocode/web-shared/lib/kilo-pass/enums';
import {
STORE_PURCHASE_PENDING_MESSAGE,
Expand Down Expand Up @@ -34,10 +34,8 @@ jest.mock('@/lib/kilo-pass/google-play-sdk', () => ({
mockConsumeGooglePlayProductPurchase(...args),
}));

jest.mock('@/lib/kilo-pass/apple-store-verifier', () => {
const actual = jest.requireActual<typeof AppleStoreVerifier>(
'@/lib/kilo-pass/apple-store-verifier'
);
jest.mock('@/lib/kilo-pass/apple-store-sdk', () => {
const actual = jest.requireActual<typeof AppleStoreVerifier>('@/lib/kilo-pass/apple-store-sdk');
return {
...actual,
decodeAppleStoreTransactionJws: (...args: [string]) =>
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/lib/credits/store-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { createHash } from 'node:crypto';
import type { androidpublisher_v3 } from '@googleapis/androidpublisher';

import { KiloPassPaymentProvider } from '@kilocode/web-shared/lib/kilo-pass/enums';
import { APPLE_STORE_BUNDLE_ID } from '@/lib/kilo-pass/apple-store-sdk';
import {
APPLE_STORE_BUNDLE_ID,
decodeAppleStoreTransactionJws,
normalizeEnvironment,
type AppleStoreDecodedTransaction,
type AppleStoreEnvironment,
} from '@/lib/kilo-pass/apple-store-verifier';
} from '@/lib/kilo-pass/apple-store-sdk';
import {
consumeGooglePlayProductPurchase,
getGooglePlayProductPurchase,
Expand Down Expand Up @@ -230,10 +230,8 @@ export async function verifyGooglePlayCreditPurchase(params: {
// A test (license-tester) purchase is credited like a paid one on purpose,
// and this is the documented exception: the stores offer no other path to
// exercise a real charge end to end, the amount always comes from the
// catalog and never from the caller, and the grant is idempotent. It
// mirrors the Kilo Pass store flow, which also grants on a Sandbox
// purchase. Refusing Sandbox here would make the only testable store path
// uncreditable.
// catalog and never from the caller, and the grant is idempotent.
// Refusing Sandbox here would make the only testable store path uncreditable.
environment: apiData.purchaseType === 0 ? 'Sandbox' : 'Production',
rawPayload: apiData,
};
Expand Down
Loading
Loading