@nosslabs/iap 0.3.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 ADDED
@@ -0,0 +1,114 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@nosslabs/iap` will be documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.3.0] — 2026-05-06
9
+
10
+ ### Changed (BREAKING)
11
+
12
+ - **Package renamed: `@nossdev/iap` → `@nosslabs/iap`.** Update your install
13
+ and import sites:
14
+ ```
15
+ npm uninstall @nossdev/iap
16
+ npm install @nosslabs/iap
17
+ ```
18
+ ```ts
19
+ // before
20
+ import { createIAP } from '@nossdev/iap';
21
+ // after
22
+ import { createIAP } from '@nosslabs/iap';
23
+ ```
24
+ Behavior is unchanged. The rename disambiguates registry routing for
25
+ consumers that point `@nossdev:*` at a private registry — previously,
26
+ any `.npmrc` mapping `@nossdev` to a private feed would also intercept
27
+ the public `@nossdev/iap` lookup. The `@nossdev/iap` package on npm
28
+ remains installable at its existing versions but receives no further
29
+ updates under that name.
30
+
31
+ - **Default storage namespace: `nossdev_iap` → `nosslabs_iap`.** On
32
+ upgrade, prior cached entitlements are not read. The library refetches
33
+ from the backend on first `getEntitlements()` / `restorePurchases()`
34
+ call, so no manual migration is required. If you depend on cache
35
+ continuity across the upgrade, set
36
+ `storage: { namespace: 'nossdev_iap' }` explicitly in your IAP config
37
+ to keep reading the old key.
38
+
39
+ - **Logger console prefix: `[@nossdev/iap]` → `[@nosslabs/iap]`.** Update
40
+ any log-grep dashboards or filters keyed on the old prefix.
41
+
42
+ ### Migration
43
+
44
+ No API changes. Drop-in once the package name and (optionally) the
45
+ storage namespace override are updated.
46
+
47
+ ## [0.2.0] — 2026-05-06
48
+
49
+ ### Changed (BREAKING)
50
+
51
+ - **`purchase()` signature is now an options object.** Replace `iap.purchase('premium_monthly')` with `iap.purchase({ productId: 'premium_monthly' })`. The new shape is required for the additive `appUserId` field below and any future per-purchase options. Search-and-replace migration; one mechanical edit per call site. See [Migration § v0.1 → v0.2](https://iap.nossdev.com/migration#v0-1-v0-2-breaking-purchase-signature).
52
+
53
+ ### Added
54
+
55
+ - **Pre-attached `appUserId` for the verify/webhook user-mapping path.** New optional `appUserId` field on `PurchaseOptions` accepts either a UUID v4 string or an async fetcher (`() => Promise<string>`). When supplied, the resolved value is validated as a UUID v4 and forwarded to StoreKit's `appAccountToken` (iOS) / Play Billing's `obfuscatedAccountId` (Android) — making it available to the consumer's backend on Attesto's verify response and outbound webhook payload as a top-level `appUserId` field. Eliminates the verify/webhook race for purchases where the user is signed in. Fetcher is invoked fresh per purchase; no iap-side caching (backend owns the mint-or-lookup idempotency). See [Getting started § Pre-attaching a user identifier](https://iap.nossdev.com/guide/getting-started#pre-attaching-a-user-identifier-optional).
56
+ - **`AppUserId` and `PurchaseOptions` types** exported from the package root for consumers who type their own helpers around `purchase(...)`.
57
+ - **Two new error codes**:
58
+ - `INVALID_APP_USER_ID` — supplied value (literal or fetcher-returned) isn't a valid UUID v4. Thrown synchronously / via Promise rejection, before reaching the native adapter.
59
+ - `APP_USER_ID_FETCH_FAILED` — async fetcher threw or rejected. Original error is attached as `cause` for introspection.
60
+
61
+ ## [0.1.3] — 2026-05-06
62
+
63
+ ### Fixed
64
+
65
+ - **Restore response no longer requires a `transaction` envelope.** `HttpBackendAdapter.restore()` previously validated against the same schema as `verifyApple` / `verifyGoogle`, which required `transaction: { id, productId, ... }` on success. The orchestrator never reads `response.transaction` on the restore path — `iap.restorePurchases()` returns `{ restored, entitlements }` and the field was never surfaced. Backends may now respond with `{ valid: true, entitlements: [...] }` and the library accepts it. Backends that include `transaction` aren't broken — the field is preserved (passthrough) but no longer validated.
66
+ - **Top-level response envelopes now passthrough unknown keys.** Every backend response schema (`verifyResponseSchema`, the new `restoreResponseSchema`, `entitlementsResponseSchema`, `productManifestResponseSchema`) used `z.object()`'s strip-unknown default, silently dropping consumer-defined extras (analytics ids, debug fields, server timestamps, custom flags). Inner schemas (`passthroughEntitlementSchema`, `verifiedTransactionSchema`) already passed through; this patch closes the top-level gap so backend metadata rides through end-to-end. Consumer code can read these extras via a runtime cast — the library validates only the named fields it owns.
67
+
68
+ ### Changed
69
+
70
+ - **`BackendAdapter.restore()` return type** is now `RestoreResponse<T>` rather than `VerifyResponse<T>`. The success branch omits the `transaction` field; the failure branch is unchanged. Existing custom adapters returning `VerifyResponse` from `restore()` remain structurally compatible — `{ valid: true; entitlements; transaction }` is assignable to `{ valid: true; entitlements }`. Update your typings opportunistically.
71
+ - **`transaction.verifiedAt` no longer validated** in the runtime schema. The library never read it; consumers that send it still see it preserved via the existing `verifiedTransactionSchema.passthrough()`.
72
+
73
+ ## [0.1.2] — 2026-05-05
74
+
75
+ ### Fixed
76
+
77
+ - **`androidPlanId` no longer required for subscription products** — the schema previously enforced `androidPlanId` cross-platform via a `.refine()` on `configuredProductSchema`, blocking iOS-only consumers and single-plan Android subscriptions from validating their config or backend manifest. The field is now consistently optional. The Android native adapter already falls back to `native.getOffer()` (the default offer) when it's missing, so the runtime is unaffected. Set `androidPlanId` explicitly only when an Android subscription has multiple base plans and you need to disambiguate. iOS ignores it.
78
+ - **`verifyApple` / `verifyGoogle` are individually optional** — previously both were required at the schema level even for single-platform builds. Now at least one of them must be set; the other can be omitted. iOS-only consumers can drop `verifyGoogle`, Android-only consumers can drop `verifyApple`. The HTTP adapter throws `IAPError(INVALID_CONFIG)` with a clear message if the runtime ever dispatches to a missing endpoint — but in practice the orchestrator only calls the endpoint matching the active native transaction's platform.
79
+
80
+ ## [0.1.1] — 2026-05-05
81
+
82
+ ### Fixed
83
+
84
+ - **HTTP client URL normalization** — `HttpClient` now forgives mismatched slashes between `backend.baseUrl` and `backend.endpoints.*`. Previously, only a trailing slash on `baseUrl` was stripped; an endpoint path without a leading slash silently produced a malformed URL (`https://api.example.comiap/verify`). Both sides are now normalized: `baseUrl` trailing slashes (including `//`) are stripped and a leading `/` is added to the endpoint path if missing. No behavior change for correctly-configured consumers.
85
+
86
+ ## [0.1.0] — 2026-04-29
87
+
88
+ First public release. Capacitor 5 IAP orchestrator that defers acknowledgement to a backend you control.
89
+
90
+ ### Added
91
+
92
+ - **`createIAP({ products, backend })` factory** — Promise-based public API. Returns an instance with `initialize()`, `purchase()`, `restore()`, `refresh()`, `getEntitlements()`, `hasEntitlement()`, and a typed event emitter.
93
+ - **Capacitor 5 native adapter** — wraps [`cordova-plugin-purchase`](https://github.com/j3k0/cordova-plugin-purchase) `^13.x`. Defers `Transaction.finish()` until backend verification succeeds (the "never grant before backend confirms" guarantee). Web stub no-ops gracefully.
94
+ - **Backend HTTP client** — fetch wrapper with timeout, stepped retry on 5xx (1 s / 2 s / 4 s, no retry on 4xx), pluggable `getAuthHeaders`, request/response transforms, and structured error mapping to `IAPError` with `IAPErrorCode` enum.
95
+ - **Backend abstraction** — `BackendAdapter` interface with optional methods (`verifyApple`, `verifyGoogle`, `entitlements`, `restore`, `listProducts`). Bring your own adapter (Firebase, Supabase, GraphQL) or use the built-in HTTP adapter.
96
+ - **Backend-driven product manifest** — `createIAP({ products })` is optional. Set `backend.endpoints.products` (HTTP) or implement `listProducts()` on a custom adapter to have the backend curate the SKU list. Hard caveat: every SKU must still be pre-registered in App Store Connect / Google Play Console.
97
+ - **Purchase flow orchestration** — captures the cdv `Transaction` in `pendingFinish`, calls backend `verifyApple`/`verifyGoogle`, only then triggers `nativeAdapter.acknowledge()`. Concurrency lock prevents double-purchase. Emits `purchase-started`, `purchase-success`, `purchase-failed`, `purchase-cancelled`, `purchase-pending`, `verification-failed`.
98
+ - **Restore flow** — `iap.restore()` re-fetches owned items, surfaces newly granted entitlements, deduplicates against the local cache.
99
+ - **Refresh + recovery** — `iap.refresh()` reconciles `unfinished_transactions` storage on app resume / launch; recovery on `initialize()` re-attempts verification for transactions that died between native success and ack.
100
+ - **Entitlement cache** — Capacitor Preferences-backed (with in-memory fallback for tests/web). Survives app restarts. Sync reads via `getEntitlements()` / `hasEntitlement()` for fast UI.
101
+ - **Configurable logger** — `Logger` interface with a default console-backed implementation. Inject a structured logger (Sentry, Datadog) for production observability.
102
+ - **Documentation site** — VitePress at [iap.nossdev.com](https://iap.nossdev.com): installation, configuration, backend contract, API reference, and recipes for Vue + Quasar, React, and Pinia store.
103
+ - **CI** — typecheck + lint + test + build on Node 20 and 22, matrix run on every PR and push to `main`.
104
+
105
+ ### Notes for early adopters
106
+
107
+ - API may have breaking changes through the 0.x line as production usage exposes rough edges. Pin the minor (`^0.1.0`) and watch this CHANGELOG.
108
+ - Capacitor 7 migration is preserved in git history (commit `f1d20ed`); the v7 native adapter ships as a separate major (`1.x`) when the consumer ecosystem catches up.
109
+
110
+ ### Future
111
+
112
+ - Upgrade `zod` from 3 to 4 once the wider ecosystem catches up.
113
+ - Migrate from `NPM_TOKEN` to npm OIDC trusted publishers.
114
+ - Capacitor 7 + `@capgo/native-purchases v7.x` adapter.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nossdev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @nosslabs/iap
2
+
3
+ > Thin Capacitor IAP orchestrator. Server-side validation via [Attesto](https://attesto.nossdev.com).
4
+
5
+ **Status: 0.2.0 — published.** API may have breaking changes through the 0.x line as it's exercised in production apps. Pin the minor version (`^0.2.0`) and watch the [CHANGELOG](./CHANGELOG.md).
6
+
7
+ ```bash
8
+ npm install @nosslabs/iap cordova-plugin-purchase
9
+ npx cap sync
10
+ ```
11
+
12
+ ```typescript
13
+ import { createIAP } from '@nosslabs/iap';
14
+
15
+ const iap = createIAP({
16
+ products: [
17
+ { id: 'premium_monthly', type: 'subscription', androidPlanId: 'monthly-plan' },
18
+ ],
19
+ backend: {
20
+ baseUrl: 'https://api.your-app.com',
21
+ endpoints: {
22
+ verifyApple: '/api/iap/verify/apple',
23
+ verifyGoogle: '/api/iap/verify/google',
24
+ entitlements: '/api/iap/entitlements',
25
+ restore: '/api/iap/restore',
26
+ },
27
+ getAuthHeaders: async () => ({
28
+ Authorization: `Bearer ${await getAuthToken()}`,
29
+ }),
30
+ },
31
+ });
32
+
33
+ await iap.initialize();
34
+
35
+ const result = await iap.purchase({ productId: 'premium_monthly' });
36
+ if (result.status === 'success') {
37
+ // backend has validated; entitlements are cached
38
+ }
39
+
40
+ // (optional) Pre-attach a UUID so it travels through StoreKit/Play Billing
41
+ // and reaches your backend on both the verify response and the eventual
42
+ // webhook — eliminates the verify/webhook race for purchases where the
43
+ // user is signed in. Either pass a string you already have or an async
44
+ // fetcher that hits your backend (which mints+saves on first call,
45
+ // returns the existing UUID on subsequent calls).
46
+ await iap.purchase({
47
+ productId: 'premium_monthly',
48
+ appUserId: async () => {
49
+ const r = await fetch('/api/iap/uuid', { headers: authHeaders() });
50
+ return (await r.json()).uuid;
51
+ },
52
+ });
53
+ ```
54
+
55
+ ## Documentation
56
+
57
+ **📘 [iap.nossdev.com](https://iap.nossdev.com)** — installation, configuration, framework recipes, API reference.
58
+
59
+ - [Getting started](https://iap.nossdev.com/guide/getting-started) — first purchase in 30 minutes
60
+ - [Backend contract](https://iap.nossdev.com/guide/backend-contract) — four endpoints your backend implements
61
+ - [Architecture](https://iap.nossdev.com/guide/architecture) — three-tier model
62
+ - [Vue + Quasar recipe](https://iap.nossdev.com/recipes/vue-quasar) / [React recipe](https://iap.nossdev.com/recipes/react)
63
+
64
+ ## Why this library
65
+
66
+ `@nosslabs/iap` does **one thing**: orchestrate the purchase flow on the client. It
67
+
68
+ - wraps `cordova-plugin-purchase` for native purchase + restore,
69
+ - POSTs to **your** backend (which calls Attesto) for receipt validation,
70
+ - acknowledges native transactions only **after** the backend confirms (no phantom grants),
71
+ - caches entitlements locally for instant, reactive UI reads,
72
+ - recovers unfinished transactions across app launches.
73
+
74
+ It does **not**: talk to Attesto directly, define entitlement business logic, manage user auth, or ship paywall UI. Those belong to your app and your backend.
75
+
76
+ ## Capacitor support matrix
77
+
78
+ | `@nosslabs/iap` | Capacitor | Plugin | Status |
79
+ |---|---|---|---|
80
+ | 0.x | 5.x | `cordova-plugin-purchase ^13.x` | **Current** |
81
+ | 1.x | 7.x | TBD (Capacitor-native plugin) | Roadmap |
82
+
83
+ ## Optional peer dependency
84
+
85
+ If you want auto-refresh on app resume (default behavior):
86
+
87
+ ```bash
88
+ npm install @capacitor/app
89
+ npx cap sync
90
+ ```
91
+
92
+ Or disable the listener with `options.refreshOnResume: false`. See [installation guide](https://iap.nossdev.com/guide/installation#optional-app-resume-listener).
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ mise install # Node 22 + npm 10
98
+ npm install
99
+ npm run typecheck # tsc --noEmit
100
+ npm run lint # biome check
101
+ npm test # vitest run
102
+ npm run build # tsup → dist/index.{js,cjs,d.ts}
103
+ npm run docs:dev # vitepress dev (http://localhost:5173)
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT — see [LICENSE](./LICENSE).