@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/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. The
834
- * fetcher closes over whatever auth state the caller needs (session
835
- * token, JWT, cookie); iap passes no implicit context.
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. The
834
- * fetcher closes over whatever auth state the caller needs (session
835
- * token, JWT, cookie); iap passes no implicit context.
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
- const resolvedAppUserId = appUserId !== void 0 ? await resolveAppUserId(appUserId) : void 0;
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