@nosslabs/iap 0.3.0 → 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.
- package/CHANGELOG.md +102 -0
- package/README.md +1 -1
- package/dist/index.cjs +69 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -11
- package/dist/index.d.ts +102 -11
- package/dist/index.js +69 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -224,6 +224,19 @@ declare const optionsConfigSchema: z.ZodObject<{
|
|
|
224
224
|
* Excess entries stay in storage and are processed on subsequent launches.
|
|
225
225
|
*/
|
|
226
226
|
recoveryMaxBatch: z.ZodDefault<z.ZodNumber>;
|
|
227
|
+
/**
|
|
228
|
+
* List of backend `valid:false` error codes that recovery should treat as
|
|
229
|
+
* permanent — entries with a matching error are removed from storage
|
|
230
|
+
* instead of retried on every launch. When omitted (the default), iap
|
|
231
|
+
* uses `DEFAULT_PERMANENT_ERROR_CODES` (`['TRANSACTION_NOT_FOUND',
|
|
232
|
+
* 'PRODUCT_MISMATCH']`).
|
|
233
|
+
*
|
|
234
|
+
* REPLACES the default when provided — pass `[...DEFAULT_PERMANENT_ERROR_CODES,
|
|
235
|
+
* 'YOUR_CODE']` to extend, or `[]` to disable the feature entirely
|
|
236
|
+
* (revert to retry-forever behavior). Export the constant from
|
|
237
|
+
* `@nosslabs/iap` for the spread form.
|
|
238
|
+
*/
|
|
239
|
+
permanentErrorCodes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
227
240
|
productPriceCacheTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
228
241
|
logLevel: z.ZodDefault<z.ZodEnum<["silent", "error", "warn", "info", "debug"]>>;
|
|
229
242
|
logger: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -234,12 +247,14 @@ declare const optionsConfigSchema: z.ZodObject<{
|
|
|
234
247
|
recoveryMaxBatch: number;
|
|
235
248
|
productPriceCacheTtlMs: number;
|
|
236
249
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
250
|
+
permanentErrorCodes?: string[] | undefined;
|
|
237
251
|
logger?: unknown;
|
|
238
252
|
}, {
|
|
239
253
|
refreshOnResume?: boolean | undefined;
|
|
240
254
|
entitlementCacheTtlMs?: number | undefined;
|
|
241
255
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
242
256
|
recoveryMaxBatch?: number | undefined;
|
|
257
|
+
permanentErrorCodes?: string[] | undefined;
|
|
243
258
|
productPriceCacheTtlMs?: number | undefined;
|
|
244
259
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
245
260
|
logger?: unknown;
|
|
@@ -434,6 +449,19 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
434
449
|
* Excess entries stay in storage and are processed on subsequent launches.
|
|
435
450
|
*/
|
|
436
451
|
recoveryMaxBatch: z.ZodDefault<z.ZodNumber>;
|
|
452
|
+
/**
|
|
453
|
+
* List of backend `valid:false` error codes that recovery should treat as
|
|
454
|
+
* permanent — entries with a matching error are removed from storage
|
|
455
|
+
* instead of retried on every launch. When omitted (the default), iap
|
|
456
|
+
* uses `DEFAULT_PERMANENT_ERROR_CODES` (`['TRANSACTION_NOT_FOUND',
|
|
457
|
+
* 'PRODUCT_MISMATCH']`).
|
|
458
|
+
*
|
|
459
|
+
* REPLACES the default when provided — pass `[...DEFAULT_PERMANENT_ERROR_CODES,
|
|
460
|
+
* 'YOUR_CODE']` to extend, or `[]` to disable the feature entirely
|
|
461
|
+
* (revert to retry-forever behavior). Export the constant from
|
|
462
|
+
* `@nosslabs/iap` for the spread form.
|
|
463
|
+
*/
|
|
464
|
+
permanentErrorCodes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
437
465
|
productPriceCacheTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
438
466
|
logLevel: z.ZodDefault<z.ZodEnum<["silent", "error", "warn", "info", "debug"]>>;
|
|
439
467
|
logger: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -444,12 +472,14 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
444
472
|
recoveryMaxBatch: number;
|
|
445
473
|
productPriceCacheTtlMs: number;
|
|
446
474
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
475
|
+
permanentErrorCodes?: string[] | undefined;
|
|
447
476
|
logger?: unknown;
|
|
448
477
|
}, {
|
|
449
478
|
refreshOnResume?: boolean | undefined;
|
|
450
479
|
entitlementCacheTtlMs?: number | undefined;
|
|
451
480
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
452
481
|
recoveryMaxBatch?: number | undefined;
|
|
482
|
+
permanentErrorCodes?: string[] | undefined;
|
|
453
483
|
productPriceCacheTtlMs?: number | undefined;
|
|
454
484
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
455
485
|
logger?: unknown;
|
|
@@ -462,6 +492,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
462
492
|
recoveryMaxBatch: number;
|
|
463
493
|
productPriceCacheTtlMs: number;
|
|
464
494
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
495
|
+
permanentErrorCodes?: string[] | undefined;
|
|
465
496
|
logger?: unknown;
|
|
466
497
|
};
|
|
467
498
|
backend: {
|
|
@@ -514,6 +545,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
514
545
|
entitlementCacheTtlMs?: number | undefined;
|
|
515
546
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
516
547
|
recoveryMaxBatch?: number | undefined;
|
|
548
|
+
permanentErrorCodes?: string[] | undefined;
|
|
517
549
|
productPriceCacheTtlMs?: number | undefined;
|
|
518
550
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
519
551
|
logger?: unknown;
|
|
@@ -536,6 +568,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
536
568
|
recoveryMaxBatch: number;
|
|
537
569
|
productPriceCacheTtlMs: number;
|
|
538
570
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
571
|
+
permanentErrorCodes?: string[] | undefined;
|
|
539
572
|
logger?: unknown;
|
|
540
573
|
};
|
|
541
574
|
backend: {
|
|
@@ -588,6 +621,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
588
621
|
entitlementCacheTtlMs?: number | undefined;
|
|
589
622
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
590
623
|
recoveryMaxBatch?: number | undefined;
|
|
624
|
+
permanentErrorCodes?: string[] | undefined;
|
|
591
625
|
productPriceCacheTtlMs?: number | undefined;
|
|
592
626
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
593
627
|
logger?: unknown;
|
|
@@ -814,6 +848,24 @@ interface EventMap<TEntitlement extends EntitlementBase = EntitlementBase> {
|
|
|
814
848
|
productId: string;
|
|
815
849
|
lastFetchedAt: number;
|
|
816
850
|
};
|
|
851
|
+
/**
|
|
852
|
+
* Recovery classified an `unfinished_transactions` entry as permanently
|
|
853
|
+
* invalid (per `options.permanentErrorCodes`) and removed it from
|
|
854
|
+
* storage. Will not be retried on subsequent launches. Useful for ops
|
|
855
|
+
* logging / alerting on stuck-loop self-heal events.
|
|
856
|
+
*
|
|
857
|
+
* **Token is unmasked.** Receipt tokens (Apple `transactionId` /
|
|
858
|
+
* Google `purchaseToken`) are useful for correlation in debugging
|
|
859
|
+
* but are receipts you don't want to leak — treat as sensitive. Mask
|
|
860
|
+
* before forwarding to external analytics / logging services. iap's
|
|
861
|
+
* own internal logs use a masked form (see `lib/redact.ts`).
|
|
862
|
+
*/
|
|
863
|
+
'recovery-dropped-permanent': {
|
|
864
|
+
productId: string;
|
|
865
|
+
token: string;
|
|
866
|
+
error: string;
|
|
867
|
+
message?: string;
|
|
868
|
+
};
|
|
817
869
|
error: {
|
|
818
870
|
error: IAPError;
|
|
819
871
|
};
|
|
@@ -822,6 +874,24 @@ type EventName<TEntitlement extends EntitlementBase = EntitlementBase> = keyof E
|
|
|
822
874
|
type EventPayload<K extends EventName<TEntitlement>, TEntitlement extends EntitlementBase = EntitlementBase> = EventMap<TEntitlement>[K];
|
|
823
875
|
type Unsubscribe = () => void;
|
|
824
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Convenience context the library passes to a function-form
|
|
879
|
+
* {@link AppUserId} fetcher. `authHeaders` is the result of awaiting
|
|
880
|
+
* `backend.getAuthHeaders()` — the same headers the library will use
|
|
881
|
+
* for its own backend requests. Resolved fresh per purchase so token
|
|
882
|
+
* refresh keeps working.
|
|
883
|
+
*
|
|
884
|
+
* It's a convenience, not a contract: fetchers may legitimately ignore
|
|
885
|
+
* the parameter. Use it when your UUID-minting endpoint shares auth
|
|
886
|
+
* with your IAP backend; ignore it (and close over your own auth
|
|
887
|
+
* state) when it doesn't.
|
|
888
|
+
*
|
|
889
|
+
* For consumers using a custom `BackendAdapter` (no `getAuthHeaders`
|
|
890
|
+
* configured), `authHeaders` is `{}`.
|
|
891
|
+
*/
|
|
892
|
+
interface AppUserIdFetcherContext {
|
|
893
|
+
authHeaders: Record<string, string>;
|
|
894
|
+
}
|
|
825
895
|
/**
|
|
826
896
|
* Value supplied to `iap.purchase({ appUserId })`. Either a UUID v4
|
|
827
897
|
* string the caller already has (e.g. from local cache / app state) or
|
|
@@ -830,9 +900,14 @@ type Unsubscribe = () => void;
|
|
|
830
900
|
* persists on first call, returns the existing UUID on later calls).
|
|
831
901
|
*
|
|
832
902
|
* The fetcher is invoked **fresh on every purchase** — iap caches
|
|
833
|
-
* nothing. The backend owns the mint-or-lookup idempotency.
|
|
834
|
-
*
|
|
835
|
-
*
|
|
903
|
+
* nothing. The backend owns the mint-or-lookup idempotency.
|
|
904
|
+
*
|
|
905
|
+
* Two fetcher shapes are supported:
|
|
906
|
+
* - `() => Promise<string>` — closes over its own auth state.
|
|
907
|
+
* - `(ctx) => Promise<string>` — receives `ctx.authHeaders` populated
|
|
908
|
+
* from `backend.getAuthHeaders()` so the auth wired up for IAP
|
|
909
|
+
* requests can be reused without redefining a helper. See
|
|
910
|
+
* {@link AppUserIdFetcherContext}.
|
|
836
911
|
*
|
|
837
912
|
* Either form is validated as UUID v4 before being forwarded to
|
|
838
913
|
* StoreKit's `appAccountToken` (iOS) / Play Billing's
|
|
@@ -840,7 +915,7 @@ type Unsubscribe = () => void;
|
|
|
840
915
|
* `IAPError(INVALID_APP_USER_ID)`. A throwing/rejecting fetcher
|
|
841
916
|
* surfaces as `IAPError(APP_USER_ID_FETCH_FAILED, cause: <original>)`.
|
|
842
917
|
*/
|
|
843
|
-
type AppUserId = string | (() => Promise<string>);
|
|
918
|
+
type AppUserId = string | (() => Promise<string>) | ((ctx: AppUserIdFetcherContext) => Promise<string>);
|
|
844
919
|
/**
|
|
845
920
|
* Options accepted by `iap.purchase(...)`. `productId` is required;
|
|
846
921
|
* `appUserId` is optional — when omitted, no `applicationUsername` is
|
|
@@ -972,12 +1047,6 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
|
|
|
972
1047
|
}
|
|
973
1048
|
declare function createIAP<TEntitlement extends EntitlementBase = EntitlementBase>(input: IAPConfigInput): IAP<TEntitlement>;
|
|
974
1049
|
|
|
975
|
-
/**
|
|
976
|
-
* Library version. Updated by the publish workflow to match `package.json`.
|
|
977
|
-
* Read at runtime by the logger so error reports include the version.
|
|
978
|
-
*/
|
|
979
|
-
declare const VERSION = "0.1.0";
|
|
980
|
-
|
|
981
1050
|
interface VerifyAppleRequest {
|
|
982
1051
|
productId: string;
|
|
983
1052
|
/** Apple StoreKit transaction id (numeric string). */
|
|
@@ -1075,6 +1144,28 @@ interface BackendAdapter<TEntitlement extends EntitlementBase = EntitlementBase>
|
|
|
1075
1144
|
listProducts?(): Promise<ConfiguredProduct[]>;
|
|
1076
1145
|
}
|
|
1077
1146
|
|
|
1147
|
+
/**
|
|
1148
|
+
* Backend `valid:false` error codes that are treated as permanent by default
|
|
1149
|
+
* (entry removed from `unfinished_transactions` instead of retried forever).
|
|
1150
|
+
*
|
|
1151
|
+
* Conservative on purpose: only the two codes the documented recipe
|
|
1152
|
+
* contract identifies as unambiguous "domain-not-found" answers. Consumers
|
|
1153
|
+
* with custom backend error vocabularies extend via
|
|
1154
|
+
* `options.permanentErrorCodes`.
|
|
1155
|
+
*
|
|
1156
|
+
* Distinct from the `RECOVERABLE_CODES` set in `lib/errors.ts`: that one
|
|
1157
|
+
* classifies iap-internal `IAPErrorCode`s for transport/storage retry
|
|
1158
|
+
* semantics; this one classifies opaque error strings the consumer's
|
|
1159
|
+
* backend returns in `valid:false` responses.
|
|
1160
|
+
*/
|
|
1161
|
+
declare const DEFAULT_PERMANENT_ERROR_CODES: readonly string[];
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Library version. Updated by the publish workflow to match `package.json`.
|
|
1165
|
+
* Read at runtime by the logger so error reports include the version.
|
|
1166
|
+
*/
|
|
1167
|
+
declare const VERSION = "0.1.0";
|
|
1168
|
+
|
|
1078
1169
|
interface HttpBackendAdapterOptions {
|
|
1079
1170
|
baseUrl: string;
|
|
1080
1171
|
endpoints: {
|
|
@@ -1131,4 +1222,4 @@ declare class HttpBackendAdapter<TEntitlement extends EntitlementBase = Entitlem
|
|
|
1131
1222
|
listProducts(): Promise<ConfiguredProduct[]>;
|
|
1132
1223
|
}
|
|
1133
1224
|
|
|
1134
|
-
export { type AppUserId, type BackendAdapter, type BackendConfig, type BackendConfigInput, type ConfiguredProduct, type DefaultEntitlement, type EntitlementBase, type EventMap, type EventName, type EventPayload, HttpBackendAdapter, HttpClient, type HttpRequest, type IAP, type IAPConfig, type IAPConfigInput, IAPError, IAPErrorCode, type LogLevel, type Logger, type NativeTransaction, type OptionsConfig, type Platform, type Product, type ProductType, type PurchaseOptions, type PurchaseResult, type RestoreRequest, type RestoreRequestTransaction, type RestoreResponse, type RestoreResult, type StorageConfig, type Unsubscribe, VERSION, type VerifiedTransaction, type VerifyAppleRequest, type VerifyGoogleRequest, type VerifyResponse, createIAP, errorHint, isIAPError };
|
|
1225
|
+
export { type AppUserId, type BackendAdapter, type BackendConfig, type BackendConfigInput, type ConfiguredProduct, DEFAULT_PERMANENT_ERROR_CODES, type DefaultEntitlement, type EntitlementBase, type EventMap, type EventName, type EventPayload, HttpBackendAdapter, HttpClient, type HttpRequest, type IAP, type IAPConfig, type IAPConfigInput, IAPError, IAPErrorCode, type LogLevel, type Logger, type NativeTransaction, type OptionsConfig, type Platform, type Product, type ProductType, type PurchaseOptions, type PurchaseResult, type RestoreRequest, type RestoreRequestTransaction, type RestoreResponse, type RestoreResult, type StorageConfig, type Unsubscribe, VERSION, type VerifiedTransaction, type VerifyAppleRequest, type VerifyGoogleRequest, type VerifyResponse, createIAP, errorHint, isIAPError };
|
package/dist/index.d.ts
CHANGED
|
@@ -224,6 +224,19 @@ declare const optionsConfigSchema: z.ZodObject<{
|
|
|
224
224
|
* Excess entries stay in storage and are processed on subsequent launches.
|
|
225
225
|
*/
|
|
226
226
|
recoveryMaxBatch: z.ZodDefault<z.ZodNumber>;
|
|
227
|
+
/**
|
|
228
|
+
* List of backend `valid:false` error codes that recovery should treat as
|
|
229
|
+
* permanent — entries with a matching error are removed from storage
|
|
230
|
+
* instead of retried on every launch. When omitted (the default), iap
|
|
231
|
+
* uses `DEFAULT_PERMANENT_ERROR_CODES` (`['TRANSACTION_NOT_FOUND',
|
|
232
|
+
* 'PRODUCT_MISMATCH']`).
|
|
233
|
+
*
|
|
234
|
+
* REPLACES the default when provided — pass `[...DEFAULT_PERMANENT_ERROR_CODES,
|
|
235
|
+
* 'YOUR_CODE']` to extend, or `[]` to disable the feature entirely
|
|
236
|
+
* (revert to retry-forever behavior). Export the constant from
|
|
237
|
+
* `@nosslabs/iap` for the spread form.
|
|
238
|
+
*/
|
|
239
|
+
permanentErrorCodes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
227
240
|
productPriceCacheTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
228
241
|
logLevel: z.ZodDefault<z.ZodEnum<["silent", "error", "warn", "info", "debug"]>>;
|
|
229
242
|
logger: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -234,12 +247,14 @@ declare const optionsConfigSchema: z.ZodObject<{
|
|
|
234
247
|
recoveryMaxBatch: number;
|
|
235
248
|
productPriceCacheTtlMs: number;
|
|
236
249
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
250
|
+
permanentErrorCodes?: string[] | undefined;
|
|
237
251
|
logger?: unknown;
|
|
238
252
|
}, {
|
|
239
253
|
refreshOnResume?: boolean | undefined;
|
|
240
254
|
entitlementCacheTtlMs?: number | undefined;
|
|
241
255
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
242
256
|
recoveryMaxBatch?: number | undefined;
|
|
257
|
+
permanentErrorCodes?: string[] | undefined;
|
|
243
258
|
productPriceCacheTtlMs?: number | undefined;
|
|
244
259
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
245
260
|
logger?: unknown;
|
|
@@ -434,6 +449,19 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
434
449
|
* Excess entries stay in storage and are processed on subsequent launches.
|
|
435
450
|
*/
|
|
436
451
|
recoveryMaxBatch: z.ZodDefault<z.ZodNumber>;
|
|
452
|
+
/**
|
|
453
|
+
* List of backend `valid:false` error codes that recovery should treat as
|
|
454
|
+
* permanent — entries with a matching error are removed from storage
|
|
455
|
+
* instead of retried on every launch. When omitted (the default), iap
|
|
456
|
+
* uses `DEFAULT_PERMANENT_ERROR_CODES` (`['TRANSACTION_NOT_FOUND',
|
|
457
|
+
* 'PRODUCT_MISMATCH']`).
|
|
458
|
+
*
|
|
459
|
+
* REPLACES the default when provided — pass `[...DEFAULT_PERMANENT_ERROR_CODES,
|
|
460
|
+
* 'YOUR_CODE']` to extend, or `[]` to disable the feature entirely
|
|
461
|
+
* (revert to retry-forever behavior). Export the constant from
|
|
462
|
+
* `@nosslabs/iap` for the spread form.
|
|
463
|
+
*/
|
|
464
|
+
permanentErrorCodes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
437
465
|
productPriceCacheTtlMs: z.ZodDefault<z.ZodNumber>;
|
|
438
466
|
logLevel: z.ZodDefault<z.ZodEnum<["silent", "error", "warn", "info", "debug"]>>;
|
|
439
467
|
logger: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -444,12 +472,14 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
444
472
|
recoveryMaxBatch: number;
|
|
445
473
|
productPriceCacheTtlMs: number;
|
|
446
474
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
475
|
+
permanentErrorCodes?: string[] | undefined;
|
|
447
476
|
logger?: unknown;
|
|
448
477
|
}, {
|
|
449
478
|
refreshOnResume?: boolean | undefined;
|
|
450
479
|
entitlementCacheTtlMs?: number | undefined;
|
|
451
480
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
452
481
|
recoveryMaxBatch?: number | undefined;
|
|
482
|
+
permanentErrorCodes?: string[] | undefined;
|
|
453
483
|
productPriceCacheTtlMs?: number | undefined;
|
|
454
484
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
455
485
|
logger?: unknown;
|
|
@@ -462,6 +492,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
462
492
|
recoveryMaxBatch: number;
|
|
463
493
|
productPriceCacheTtlMs: number;
|
|
464
494
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
495
|
+
permanentErrorCodes?: string[] | undefined;
|
|
465
496
|
logger?: unknown;
|
|
466
497
|
};
|
|
467
498
|
backend: {
|
|
@@ -514,6 +545,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
514
545
|
entitlementCacheTtlMs?: number | undefined;
|
|
515
546
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
516
547
|
recoveryMaxBatch?: number | undefined;
|
|
548
|
+
permanentErrorCodes?: string[] | undefined;
|
|
517
549
|
productPriceCacheTtlMs?: number | undefined;
|
|
518
550
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
519
551
|
logger?: unknown;
|
|
@@ -536,6 +568,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
536
568
|
recoveryMaxBatch: number;
|
|
537
569
|
productPriceCacheTtlMs: number;
|
|
538
570
|
logLevel: "silent" | "error" | "warn" | "info" | "debug";
|
|
571
|
+
permanentErrorCodes?: string[] | undefined;
|
|
539
572
|
logger?: unknown;
|
|
540
573
|
};
|
|
541
574
|
backend: {
|
|
@@ -588,6 +621,7 @@ declare const iapConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
588
621
|
entitlementCacheTtlMs?: number | undefined;
|
|
589
622
|
recoverUnfinishedTransactions?: boolean | undefined;
|
|
590
623
|
recoveryMaxBatch?: number | undefined;
|
|
624
|
+
permanentErrorCodes?: string[] | undefined;
|
|
591
625
|
productPriceCacheTtlMs?: number | undefined;
|
|
592
626
|
logLevel?: "silent" | "error" | "warn" | "info" | "debug" | undefined;
|
|
593
627
|
logger?: unknown;
|
|
@@ -814,6 +848,24 @@ interface EventMap<TEntitlement extends EntitlementBase = EntitlementBase> {
|
|
|
814
848
|
productId: string;
|
|
815
849
|
lastFetchedAt: number;
|
|
816
850
|
};
|
|
851
|
+
/**
|
|
852
|
+
* Recovery classified an `unfinished_transactions` entry as permanently
|
|
853
|
+
* invalid (per `options.permanentErrorCodes`) and removed it from
|
|
854
|
+
* storage. Will not be retried on subsequent launches. Useful for ops
|
|
855
|
+
* logging / alerting on stuck-loop self-heal events.
|
|
856
|
+
*
|
|
857
|
+
* **Token is unmasked.** Receipt tokens (Apple `transactionId` /
|
|
858
|
+
* Google `purchaseToken`) are useful for correlation in debugging
|
|
859
|
+
* but are receipts you don't want to leak — treat as sensitive. Mask
|
|
860
|
+
* before forwarding to external analytics / logging services. iap's
|
|
861
|
+
* own internal logs use a masked form (see `lib/redact.ts`).
|
|
862
|
+
*/
|
|
863
|
+
'recovery-dropped-permanent': {
|
|
864
|
+
productId: string;
|
|
865
|
+
token: string;
|
|
866
|
+
error: string;
|
|
867
|
+
message?: string;
|
|
868
|
+
};
|
|
817
869
|
error: {
|
|
818
870
|
error: IAPError;
|
|
819
871
|
};
|
|
@@ -822,6 +874,24 @@ type EventName<TEntitlement extends EntitlementBase = EntitlementBase> = keyof E
|
|
|
822
874
|
type EventPayload<K extends EventName<TEntitlement>, TEntitlement extends EntitlementBase = EntitlementBase> = EventMap<TEntitlement>[K];
|
|
823
875
|
type Unsubscribe = () => void;
|
|
824
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Convenience context the library passes to a function-form
|
|
879
|
+
* {@link AppUserId} fetcher. `authHeaders` is the result of awaiting
|
|
880
|
+
* `backend.getAuthHeaders()` — the same headers the library will use
|
|
881
|
+
* for its own backend requests. Resolved fresh per purchase so token
|
|
882
|
+
* refresh keeps working.
|
|
883
|
+
*
|
|
884
|
+
* It's a convenience, not a contract: fetchers may legitimately ignore
|
|
885
|
+
* the parameter. Use it when your UUID-minting endpoint shares auth
|
|
886
|
+
* with your IAP backend; ignore it (and close over your own auth
|
|
887
|
+
* state) when it doesn't.
|
|
888
|
+
*
|
|
889
|
+
* For consumers using a custom `BackendAdapter` (no `getAuthHeaders`
|
|
890
|
+
* configured), `authHeaders` is `{}`.
|
|
891
|
+
*/
|
|
892
|
+
interface AppUserIdFetcherContext {
|
|
893
|
+
authHeaders: Record<string, string>;
|
|
894
|
+
}
|
|
825
895
|
/**
|
|
826
896
|
* Value supplied to `iap.purchase({ appUserId })`. Either a UUID v4
|
|
827
897
|
* string the caller already has (e.g. from local cache / app state) or
|
|
@@ -830,9 +900,14 @@ type Unsubscribe = () => void;
|
|
|
830
900
|
* persists on first call, returns the existing UUID on later calls).
|
|
831
901
|
*
|
|
832
902
|
* The fetcher is invoked **fresh on every purchase** — iap caches
|
|
833
|
-
* nothing. The backend owns the mint-or-lookup idempotency.
|
|
834
|
-
*
|
|
835
|
-
*
|
|
903
|
+
* nothing. The backend owns the mint-or-lookup idempotency.
|
|
904
|
+
*
|
|
905
|
+
* Two fetcher shapes are supported:
|
|
906
|
+
* - `() => Promise<string>` — closes over its own auth state.
|
|
907
|
+
* - `(ctx) => Promise<string>` — receives `ctx.authHeaders` populated
|
|
908
|
+
* from `backend.getAuthHeaders()` so the auth wired up for IAP
|
|
909
|
+
* requests can be reused without redefining a helper. See
|
|
910
|
+
* {@link AppUserIdFetcherContext}.
|
|
836
911
|
*
|
|
837
912
|
* Either form is validated as UUID v4 before being forwarded to
|
|
838
913
|
* StoreKit's `appAccountToken` (iOS) / Play Billing's
|
|
@@ -840,7 +915,7 @@ type Unsubscribe = () => void;
|
|
|
840
915
|
* `IAPError(INVALID_APP_USER_ID)`. A throwing/rejecting fetcher
|
|
841
916
|
* surfaces as `IAPError(APP_USER_ID_FETCH_FAILED, cause: <original>)`.
|
|
842
917
|
*/
|
|
843
|
-
type AppUserId = string | (() => Promise<string>);
|
|
918
|
+
type AppUserId = string | (() => Promise<string>) | ((ctx: AppUserIdFetcherContext) => Promise<string>);
|
|
844
919
|
/**
|
|
845
920
|
* Options accepted by `iap.purchase(...)`. `productId` is required;
|
|
846
921
|
* `appUserId` is optional — when omitted, no `applicationUsername` is
|
|
@@ -972,12 +1047,6 @@ interface IAP<TEntitlement extends EntitlementBase = EntitlementBase> {
|
|
|
972
1047
|
}
|
|
973
1048
|
declare function createIAP<TEntitlement extends EntitlementBase = EntitlementBase>(input: IAPConfigInput): IAP<TEntitlement>;
|
|
974
1049
|
|
|
975
|
-
/**
|
|
976
|
-
* Library version. Updated by the publish workflow to match `package.json`.
|
|
977
|
-
* Read at runtime by the logger so error reports include the version.
|
|
978
|
-
*/
|
|
979
|
-
declare const VERSION = "0.1.0";
|
|
980
|
-
|
|
981
1050
|
interface VerifyAppleRequest {
|
|
982
1051
|
productId: string;
|
|
983
1052
|
/** Apple StoreKit transaction id (numeric string). */
|
|
@@ -1075,6 +1144,28 @@ interface BackendAdapter<TEntitlement extends EntitlementBase = EntitlementBase>
|
|
|
1075
1144
|
listProducts?(): Promise<ConfiguredProduct[]>;
|
|
1076
1145
|
}
|
|
1077
1146
|
|
|
1147
|
+
/**
|
|
1148
|
+
* Backend `valid:false` error codes that are treated as permanent by default
|
|
1149
|
+
* (entry removed from `unfinished_transactions` instead of retried forever).
|
|
1150
|
+
*
|
|
1151
|
+
* Conservative on purpose: only the two codes the documented recipe
|
|
1152
|
+
* contract identifies as unambiguous "domain-not-found" answers. Consumers
|
|
1153
|
+
* with custom backend error vocabularies extend via
|
|
1154
|
+
* `options.permanentErrorCodes`.
|
|
1155
|
+
*
|
|
1156
|
+
* Distinct from the `RECOVERABLE_CODES` set in `lib/errors.ts`: that one
|
|
1157
|
+
* classifies iap-internal `IAPErrorCode`s for transport/storage retry
|
|
1158
|
+
* semantics; this one classifies opaque error strings the consumer's
|
|
1159
|
+
* backend returns in `valid:false` responses.
|
|
1160
|
+
*/
|
|
1161
|
+
declare const DEFAULT_PERMANENT_ERROR_CODES: readonly string[];
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Library version. Updated by the publish workflow to match `package.json`.
|
|
1165
|
+
* Read at runtime by the logger so error reports include the version.
|
|
1166
|
+
*/
|
|
1167
|
+
declare const VERSION = "0.1.0";
|
|
1168
|
+
|
|
1078
1169
|
interface HttpBackendAdapterOptions {
|
|
1079
1170
|
baseUrl: string;
|
|
1080
1171
|
endpoints: {
|
|
@@ -1131,4 +1222,4 @@ declare class HttpBackendAdapter<TEntitlement extends EntitlementBase = Entitlem
|
|
|
1131
1222
|
listProducts(): Promise<ConfiguredProduct[]>;
|
|
1132
1223
|
}
|
|
1133
1224
|
|
|
1134
|
-
export { type AppUserId, type BackendAdapter, type BackendConfig, type BackendConfigInput, type ConfiguredProduct, type DefaultEntitlement, type EntitlementBase, type EventMap, type EventName, type EventPayload, HttpBackendAdapter, HttpClient, type HttpRequest, type IAP, type IAPConfig, type IAPConfigInput, IAPError, IAPErrorCode, type LogLevel, type Logger, type NativeTransaction, type OptionsConfig, type Platform, type Product, type ProductType, type PurchaseOptions, type PurchaseResult, type RestoreRequest, type RestoreRequestTransaction, type RestoreResponse, type RestoreResult, type StorageConfig, type Unsubscribe, VERSION, type VerifiedTransaction, type VerifyAppleRequest, type VerifyGoogleRequest, type VerifyResponse, createIAP, errorHint, isIAPError };
|
|
1225
|
+
export { type AppUserId, type BackendAdapter, type BackendConfig, type BackendConfigInput, type ConfiguredProduct, DEFAULT_PERMANENT_ERROR_CODES, type DefaultEntitlement, type EntitlementBase, type EventMap, type EventName, type EventPayload, HttpBackendAdapter, HttpClient, type HttpRequest, type IAP, type IAPConfig, type IAPConfigInput, IAPError, IAPErrorCode, type LogLevel, type Logger, type NativeTransaction, type OptionsConfig, type Platform, type Product, type ProductType, type PurchaseOptions, type PurchaseResult, type RestoreRequest, type RestoreRequestTransaction, type RestoreResponse, type RestoreResult, type StorageConfig, type Unsubscribe, VERSION, type VerifiedTransaction, type VerifyAppleRequest, type VerifyGoogleRequest, type VerifyResponse, createIAP, errorHint, isIAPError };
|
package/dist/index.js
CHANGED
|
@@ -728,6 +728,19 @@ var optionsConfigSchema = z.object({
|
|
|
728
728
|
* Excess entries stay in storage and are processed on subsequent launches.
|
|
729
729
|
*/
|
|
730
730
|
recoveryMaxBatch: z.number().int().positive().default(50),
|
|
731
|
+
/**
|
|
732
|
+
* List of backend `valid:false` error codes that recovery should treat as
|
|
733
|
+
* permanent — entries with a matching error are removed from storage
|
|
734
|
+
* instead of retried on every launch. When omitted (the default), iap
|
|
735
|
+
* uses `DEFAULT_PERMANENT_ERROR_CODES` (`['TRANSACTION_NOT_FOUND',
|
|
736
|
+
* 'PRODUCT_MISMATCH']`).
|
|
737
|
+
*
|
|
738
|
+
* REPLACES the default when provided — pass `[...DEFAULT_PERMANENT_ERROR_CODES,
|
|
739
|
+
* 'YOUR_CODE']` to extend, or `[]` to disable the feature entirely
|
|
740
|
+
* (revert to retry-forever behavior). Export the constant from
|
|
741
|
+
* `@nosslabs/iap` for the spread form.
|
|
742
|
+
*/
|
|
743
|
+
permanentErrorCodes: z.array(z.string()).optional(),
|
|
731
744
|
productPriceCacheTtlMs: z.number().int().positive().default(24 * 60 * 60 * 1e3),
|
|
732
745
|
logLevel: z.enum(["silent", "error", "warn", "info", "debug"]).default("info"),
|
|
733
746
|
logger: z.unknown().optional()
|
|
@@ -1230,7 +1243,11 @@ var PurchaseOrchestrator = class {
|
|
|
1230
1243
|
message: `Product "${productId}" is not in the configured catalog.`
|
|
1231
1244
|
});
|
|
1232
1245
|
}
|
|
1233
|
-
|
|
1246
|
+
let resolvedAppUserId;
|
|
1247
|
+
if (appUserId !== void 0) {
|
|
1248
|
+
const ctx = typeof appUserId === "function" ? { authHeaders: await this.deps.getAuthHeaders() } : { authHeaders: {} };
|
|
1249
|
+
resolvedAppUserId = await resolveAppUserId(appUserId, ctx);
|
|
1250
|
+
}
|
|
1234
1251
|
this.inFlight.add(productId);
|
|
1235
1252
|
this.deps.emitter.emit("purchase-started", { productId });
|
|
1236
1253
|
try {
|
|
@@ -1347,11 +1364,11 @@ var PurchaseOrchestrator = class {
|
|
|
1347
1364
|
return { status: "verification_failed", productId, error: iapError };
|
|
1348
1365
|
}
|
|
1349
1366
|
};
|
|
1350
|
-
async function resolveAppUserId(supply) {
|
|
1367
|
+
async function resolveAppUserId(supply, ctx) {
|
|
1351
1368
|
let resolved;
|
|
1352
1369
|
if (typeof supply === "function") {
|
|
1353
1370
|
try {
|
|
1354
|
-
resolved = await supply();
|
|
1371
|
+
resolved = await supply(ctx);
|
|
1355
1372
|
} catch (cause) {
|
|
1356
1373
|
throw new IAPError({
|
|
1357
1374
|
code: IAPErrorCode.APP_USER_ID_FETCH_FAILED,
|
|
@@ -1372,6 +1389,10 @@ async function resolveAppUserId(supply) {
|
|
|
1372
1389
|
}
|
|
1373
1390
|
|
|
1374
1391
|
// src/core/recovery-flow.ts
|
|
1392
|
+
var DEFAULT_PERMANENT_ERROR_CODES = [
|
|
1393
|
+
"TRANSACTION_NOT_FOUND",
|
|
1394
|
+
"PRODUCT_MISMATCH"
|
|
1395
|
+
];
|
|
1375
1396
|
var RecoveryOrchestrator = class {
|
|
1376
1397
|
constructor(deps) {
|
|
1377
1398
|
this.deps = deps;
|
|
@@ -1381,7 +1402,7 @@ var RecoveryOrchestrator = class {
|
|
|
1381
1402
|
const { unfinished, logger, maxBatch } = this.deps;
|
|
1382
1403
|
const allEntries = await unfinished.list();
|
|
1383
1404
|
if (allEntries.length === 0) {
|
|
1384
|
-
return { recovered: 0, failures: 0, inspected: 0 };
|
|
1405
|
+
return { recovered: 0, failures: 0, droppedPermanent: 0, inspected: 0 };
|
|
1385
1406
|
}
|
|
1386
1407
|
const entries = allEntries.slice(0, maxBatch);
|
|
1387
1408
|
if (allEntries.length > maxBatch) {
|
|
@@ -1394,6 +1415,7 @@ var RecoveryOrchestrator = class {
|
|
|
1394
1415
|
const settled = await Promise.allSettled(entries.map((entry) => this.processEntry(entry)));
|
|
1395
1416
|
let recovered = 0;
|
|
1396
1417
|
let failures = 0;
|
|
1418
|
+
let droppedPermanent = 0;
|
|
1397
1419
|
let latestEntitlements = null;
|
|
1398
1420
|
for (const result of settled) {
|
|
1399
1421
|
if (result.status === "rejected") {
|
|
@@ -1403,6 +1425,8 @@ var RecoveryOrchestrator = class {
|
|
|
1403
1425
|
if (result.value.kind === "recovered") {
|
|
1404
1426
|
recovered += 1;
|
|
1405
1427
|
latestEntitlements = result.value.entitlements;
|
|
1428
|
+
} else if (result.value.kind === "dropped-permanent") {
|
|
1429
|
+
droppedPermanent += 1;
|
|
1406
1430
|
} else {
|
|
1407
1431
|
failures += 1;
|
|
1408
1432
|
}
|
|
@@ -1411,19 +1435,47 @@ var RecoveryOrchestrator = class {
|
|
|
1411
1435
|
await this.applyEntitlements(latestEntitlements);
|
|
1412
1436
|
}
|
|
1413
1437
|
logger.debug(
|
|
1414
|
-
`Recovery: ${recovered} recovered, ${failures} left in list (will retry next launch).`
|
|
1438
|
+
`Recovery: ${recovered} recovered, ${droppedPermanent} dropped (permanent), ${failures} left in list (will retry next launch).`
|
|
1415
1439
|
);
|
|
1416
|
-
return { recovered, failures, inspected: entries.length };
|
|
1440
|
+
return { recovered, failures, droppedPermanent, inspected: entries.length };
|
|
1417
1441
|
}
|
|
1418
1442
|
async processEntry(entry) {
|
|
1419
|
-
const { nativeAdapter, unfinished, logger } = this.deps;
|
|
1443
|
+
const { nativeAdapter, unfinished, logger, emitter, permanentErrorCodes } = this.deps;
|
|
1420
1444
|
const tx = entryToNativeTransaction(entry);
|
|
1421
1445
|
const tokenLabel = maskToken(entry.token);
|
|
1422
1446
|
try {
|
|
1423
1447
|
const response = await verifyNativeTransaction(this.deps.backend, tx);
|
|
1424
1448
|
if (!response.valid) {
|
|
1449
|
+
if (permanentErrorCodes.has(response.error)) {
|
|
1450
|
+
logger.info(
|
|
1451
|
+
`Recovery: dropping permanently-invalid token=${tokenLabel} productId=${entry.productId} (${response.error}).`
|
|
1452
|
+
);
|
|
1453
|
+
try {
|
|
1454
|
+
await nativeAdapter.acknowledge(tx);
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
logger.warn(
|
|
1457
|
+
`Recovery: best-effort acknowledge() failed for productId=${entry.productId}; proceeding with removal anyway.`,
|
|
1458
|
+
error
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
await unfinished.remove(entry.token);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
logger.warn(
|
|
1465
|
+
`Recovery: unfinished.remove() failed for productId=${entry.productId} after permanent classification; will dedupe on next launch.`,
|
|
1466
|
+
error
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
emitter.emit("recovery-dropped-permanent", {
|
|
1470
|
+
productId: entry.productId,
|
|
1471
|
+
token: entry.token,
|
|
1472
|
+
error: response.error,
|
|
1473
|
+
...response.message !== void 0 ? { message: response.message } : {}
|
|
1474
|
+
});
|
|
1475
|
+
return { kind: "dropped-permanent" };
|
|
1476
|
+
}
|
|
1425
1477
|
logger.debug(
|
|
1426
|
-
`Recovery: backend rejected token=${tokenLabel} productId=${entry.productId} (${response.error}); leaving in list.`
|
|
1478
|
+
`Recovery: backend rejected token=${tokenLabel} productId=${entry.productId} (${response.error}); leaving in list for retry.`
|
|
1427
1479
|
);
|
|
1428
1480
|
return { kind: "failed" };
|
|
1429
1481
|
}
|
|
@@ -1849,6 +1901,8 @@ function createIAP(input) {
|
|
|
1849
1901
|
state.logger.debug(`Resolved ${validated.data.length} product(s) from backend manifest.`);
|
|
1850
1902
|
}
|
|
1851
1903
|
state.adapter = await selectNativeAdapter({ products: state.products });
|
|
1904
|
+
const configGetAuthHeaders = state.config.backend.getAuthHeaders;
|
|
1905
|
+
const getAuthHeaders = configGetAuthHeaders ? async () => configGetAuthHeaders() : async () => ({});
|
|
1852
1906
|
const sharedDeps = {
|
|
1853
1907
|
nativeAdapter: state.adapter,
|
|
1854
1908
|
backend: state.backend,
|
|
@@ -1862,7 +1916,8 @@ function createIAP(input) {
|
|
|
1862
1916
|
},
|
|
1863
1917
|
setCachePersisted: (cachedAt) => {
|
|
1864
1918
|
state.cachedAt = cachedAt;
|
|
1865
|
-
}
|
|
1919
|
+
},
|
|
1920
|
+
getAuthHeaders
|
|
1866
1921
|
};
|
|
1867
1922
|
state.orchestrator = new PurchaseOrchestrator({
|
|
1868
1923
|
...sharedDeps,
|
|
@@ -1871,7 +1926,10 @@ function createIAP(input) {
|
|
|
1871
1926
|
state.restorer = new RestoreOrchestrator(sharedDeps);
|
|
1872
1927
|
state.recoverer = new RecoveryOrchestrator({
|
|
1873
1928
|
...sharedDeps,
|
|
1874
|
-
maxBatch: state.config.options.recoveryMaxBatch
|
|
1929
|
+
maxBatch: state.config.options.recoveryMaxBatch,
|
|
1930
|
+
permanentErrorCodes: new Set(
|
|
1931
|
+
state.config.options.permanentErrorCodes ?? DEFAULT_PERMANENT_ERROR_CODES
|
|
1932
|
+
)
|
|
1875
1933
|
});
|
|
1876
1934
|
try {
|
|
1877
1935
|
await state.adapter.isAvailable();
|
|
@@ -2062,6 +2120,6 @@ init_errors();
|
|
|
2062
2120
|
// src/version.ts
|
|
2063
2121
|
var VERSION = "0.1.0";
|
|
2064
2122
|
|
|
2065
|
-
export { HttpBackendAdapter, HttpClient, IAPError, IAPErrorCode, VERSION, createIAP, errorHint, isIAPError };
|
|
2123
|
+
export { DEFAULT_PERMANENT_ERROR_CODES, HttpBackendAdapter, HttpClient, IAPError, IAPErrorCode, VERSION, createIAP, errorHint, isIAPError };
|
|
2066
2124
|
//# sourceMappingURL=index.js.map
|
|
2067
2125
|
//# sourceMappingURL=index.js.map
|