@namiml/sdk-core 3.4.12-dev.202609252349 → 3.4.12-dev.202609281859

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.mjs CHANGED
@@ -104,7 +104,7 @@ const {
104
104
  // version — stamped by scripts/version.sh
105
105
  NAMI_SDK_VERSION = "3.4.12",
106
106
  // full package version including dev suffix — stamped by scripts/version.sh
107
- NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609252349",
107
+ NAMI_SDK_PACKAGE_VERSION = "3.4.12-dev.202609281859",
108
108
  // environments
109
109
  PRODUCTION = "production", DEVELOPMENT = "development",
110
110
  // error messages
@@ -117,7 +117,13 @@ API_VERSION = "v3", BASE_URL_PATH = `sdk/${API_VERSION}/platform`, BASE_URL = "h
117
117
  EXTENDED_CLIENT_INFO_PREFIX = "extendedClientInfo", EXTENDED_CLIENT_INFO_DELIMITER = ":", VALIDATE_PRODUCT_GROUPS = "validateProductGroups", LOG_HTTP_REQUESTS = "logHTTPRequests", LOG_HTTP_TRAFFIC = "logHTTPTraffic", API_TRAFFIC_OBSERVER = "apiTrafficObserver", STARTUP_TELEMETRY = "startupTelemetry",
118
118
  // Test hook: every platform-config fetch attempt fails before the request leaves.
119
119
  // Same namiCommand as Apple's and Android's, so the shared first-run flows drive it.
120
- SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure", EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT = 2000,
120
+ SIMULATE_CONFIG_FETCH_FAILURE = "simulateConfigFetchFailure",
121
+ // Test hook: every per-paywall body fetch fails before the request leaves, so the body cache's
122
+ // failure rows can be exercised without a proxy. Same namiCommand as Apple's and Android's (NAM-4357).
123
+ SIMULATE_PAYWALL_FETCH_FAILURE = "simulatePaywallFetchFailure",
124
+ // `campaignCacheTTL=<seconds>` overrides the config's campaign_cache_ttl — Apple's command name.
125
+ // It lets the paywall-cache e2e suite pin TTL 3600 / 1 / 0 per load (NAM-4357).
126
+ CAMPAIGN_CACHE_TTL_PREFIX = "campaignCacheTTL=", EXTENDED_PLATFORM = "extended-platform", EXTENDED_PLATFORM_VERSION = "extended-platform-version", API_MAX_CALLS_LIMIT = 2, API_RETRY_DELAY_SEC = 2, API_TIMEOUT_LIMIT = 20000, DEVICE_API_TIMEOUT_LIMIT = 2000,
121
127
  // Platform-config attempts when no initial config was supplied (Android's
122
128
  // CONFIG_MAX_ATTEMPTS; Apple's retry(maxAttempts: 3)).
123
129
  CONFIG_MAX_ATTEMPTS = 3,
@@ -3057,6 +3063,206 @@ function uniqBy(array, iteratee) {
3057
3063
  return (array && array.length) ? baseUniq(array, baseIteratee(iteratee)) : [];
3058
3064
  }
3059
3065
 
3066
+ /** Storage key of an entry's raw body; the exact URL follows the prefix. */
3067
+ const PAYWALL_BODY_KEY_PREFIX = "api_paywall_body:";
3068
+ /**
3069
+ * Storage key of an entry's metadata. Written AFTER the body, so it is the commit point: an entry
3070
+ * is visible only once both are durable, and a body with no metadata is an interrupted write.
3071
+ */
3072
+ const PAYWALL_BODY_META_KEY_PREFIX = "api_paywall_body_meta:";
3073
+ const DEFAULT_MAX_BODY_ENTRIES = 500;
3074
+ class PaywallBodyCache {
3075
+ constructor(storage = () => getPlatformAdapters().storage, maxEntries = DEFAULT_MAX_BODY_ENTRIES) {
3076
+ this.storage = storage;
3077
+ this.maxEntries = maxEntries;
3078
+ /** Set under `campaign_cache_ttl == 0`: entries live here, in this process only. */
3079
+ this.memory = null;
3080
+ /** Metadata of every persisted entry, loaded once from the storage adapter. */
3081
+ this.index = null;
3082
+ }
3083
+ get count() {
3084
+ return this.memory ? this.memory.size : this.loadIndex().size;
3085
+ }
3086
+ /**
3087
+ * Applies `campaign_cache_ttl` at the start of a resolution. `null` keeps everything, `n > 0`
3088
+ * purges entries whose own age is `>= n` seconds, and `0` purges the persistent store and keeps
3089
+ * this process's entries in memory only.
3090
+ */
3091
+ evaluateTtl(ttl, now = Date.now()) {
3092
+ if (ttl === 0) {
3093
+ if (this.memory)
3094
+ return;
3095
+ this.dropPersisted([...this.loadIndex().keys()]);
3096
+ this.memory = new Map();
3097
+ return;
3098
+ }
3099
+ // Leaving TTL 0 goes back to the persistent store; the process-local entries are dropped.
3100
+ this.memory = null;
3101
+ if (ttl === null)
3102
+ return;
3103
+ const expired = [...this.loadIndex().values()].filter((meta) => isExpired(meta, ttl, now));
3104
+ this.dropPersisted(expired.map((meta) => meta.url));
3105
+ }
3106
+ /**
3107
+ * The entry stored under this exact URL, or null when there is none, it is past `ttl`, or its
3108
+ * body is missing (a half-written entry, which is deleted here).
3109
+ */
3110
+ validEntry(url, ttl, now = Date.now()) {
3111
+ if (this.memory)
3112
+ return this.memory.get(url) ?? null;
3113
+ const meta = this.loadIndex().get(url);
3114
+ if (!meta)
3115
+ return null;
3116
+ if (ttl !== null && ttl > 0 && isExpired(meta, ttl, now)) {
3117
+ this.dropPersisted([url]);
3118
+ return null;
3119
+ }
3120
+ const body = this.read(PAYWALL_BODY_KEY_PREFIX + url);
3121
+ if (body === null) {
3122
+ this.dropPersisted([url]);
3123
+ return null;
3124
+ }
3125
+ return { meta, body };
3126
+ }
3127
+ /**
3128
+ * The stored body for this exact URL, read straight from storage rather than through the index,
3129
+ * with no TTL check. This is what rebuilds the aggregate `api_paywalls`, so it also sees an entry
3130
+ * a host wrote after the index was loaded (the web test apps inject local test paywalls that way).
3131
+ */
3132
+ bodyFor(url) {
3133
+ if (this.memory)
3134
+ return this.memory.get(url)?.body ?? null;
3135
+ return this.read(PAYWALL_BODY_KEY_PREFIX + url);
3136
+ }
3137
+ /** Stores a body under its exact URL. Returns false when the write did not land (quota, I/O). */
3138
+ store(url, contentHash, body, now = Date.now()) {
3139
+ const meta = { url, savedAt: now, ...(contentHash ? { contentHash } : {}) };
3140
+ if (this.memory) {
3141
+ this.memory.set(url, { meta, body });
3142
+ return true;
3143
+ }
3144
+ const index = this.loadIndex();
3145
+ const metaJson = JSON.stringify(meta);
3146
+ // Body first, metadata last. Each write is verified by reading it back: the web adapter
3147
+ // swallows a quota error rather than throwing, so a silent no-op is the common failure.
3148
+ if (this.write(PAYWALL_BODY_KEY_PREFIX + url, body) &&
3149
+ this.write(PAYWALL_BODY_META_KEY_PREFIX + url, metaJson)) {
3150
+ index.set(url, meta);
3151
+ return true;
3152
+ }
3153
+ logger.warn(`Paywall body cache: could not store ${url}; it will be fetched next time`);
3154
+ this.dropPersisted([url]);
3155
+ return false;
3156
+ }
3157
+ /** Stores a bundled body unless an entry already exists — a body the SDK fetched always wins. */
3158
+ seedIfAbsent(url, contentHash, body, now = Date.now()) {
3159
+ const exists = this.memory ? this.memory.has(url) : this.loadIndex().has(url);
3160
+ return exists ? false : this.store(url, contentHash, body, now);
3161
+ }
3162
+ /** Deletes one entry — used for an undecodable body. Not eviction. */
3163
+ remove(url) {
3164
+ if (this.memory)
3165
+ this.memory.delete(url);
3166
+ else
3167
+ this.dropPersisted([url]);
3168
+ }
3169
+ /**
3170
+ * Eviction, run once per resolution after a complete `campaign_rules` fetch: delete every entry
3171
+ * whose URL this resolution did not name, then cap the store at `maxEntries`, oldest first.
3172
+ */
3173
+ reconcile(currentUrls) {
3174
+ const metas = this.memory
3175
+ ? [...this.memory.values()].map((entry) => entry.meta)
3176
+ : [...this.loadIndex().values()];
3177
+ const unnamed = metas.filter((meta) => !currentUrls.has(meta.url)).map((meta) => meta.url);
3178
+ const kept = metas.filter((meta) => currentUrls.has(meta.url)).sort((a, b) => a.savedAt - b.savedAt);
3179
+ const overCap = kept.slice(0, Math.max(0, kept.length - this.maxEntries)).map((meta) => meta.url);
3180
+ [...unnamed, ...overCap].forEach((url) => this.remove(url));
3181
+ }
3182
+ /** Empties the store — persisted and in-memory entries alike. Called by `Nami.reset()`. */
3183
+ purgeAll() {
3184
+ this.dropPersisted([...this.loadIndex().keys()]);
3185
+ this.memory?.clear();
3186
+ }
3187
+ /** Builds the index from the metadata keys, clearing away half-written and unreadable entries. */
3188
+ loadIndex() {
3189
+ if (this.index)
3190
+ return this.index;
3191
+ this.index = new Map();
3192
+ let keys;
3193
+ try {
3194
+ keys = this.storage().getAllKeys();
3195
+ }
3196
+ catch (error) {
3197
+ logger.warn(`Paywall body cache: storage unavailable, caching nothing (${error})`);
3198
+ return this.index;
3199
+ }
3200
+ const bodies = new Set(keys.filter((key) => key.startsWith(PAYWALL_BODY_KEY_PREFIX)).map((key) => key.slice(PAYWALL_BODY_KEY_PREFIX.length)));
3201
+ const stale = [];
3202
+ for (const key of keys) {
3203
+ if (!key.startsWith(PAYWALL_BODY_META_KEY_PREFIX))
3204
+ continue;
3205
+ const url = key.slice(PAYWALL_BODY_META_KEY_PREFIX.length);
3206
+ const meta = parseMeta(this.read(key));
3207
+ if (meta?.url === url && bodies.has(url))
3208
+ this.index.set(url, meta);
3209
+ else
3210
+ stale.push(url);
3211
+ bodies.delete(url);
3212
+ }
3213
+ // Bodies whose metadata never landed: interrupted writes.
3214
+ this.dropPersisted([...stale, ...bodies]);
3215
+ return this.index;
3216
+ }
3217
+ dropPersisted(urls) {
3218
+ for (const url of urls) {
3219
+ this.index?.delete(url);
3220
+ this.removeKey(PAYWALL_BODY_META_KEY_PREFIX + url);
3221
+ this.removeKey(PAYWALL_BODY_KEY_PREFIX + url);
3222
+ }
3223
+ }
3224
+ read(key) {
3225
+ try {
3226
+ return this.storage().getItem(key);
3227
+ }
3228
+ catch {
3229
+ return null;
3230
+ }
3231
+ }
3232
+ write(key, value) {
3233
+ try {
3234
+ this.storage().setItem(key, value);
3235
+ }
3236
+ catch {
3237
+ return false;
3238
+ }
3239
+ return this.read(key) === value;
3240
+ }
3241
+ removeKey(key) {
3242
+ try {
3243
+ this.storage().removeItem(key);
3244
+ }
3245
+ catch {
3246
+ // Nothing more to do: an entry that cannot be deleted is re-validated on the next read.
3247
+ }
3248
+ }
3249
+ }
3250
+ function isExpired(meta, ttlSeconds, now) {
3251
+ return (now - meta.savedAt) / 1000 >= ttlSeconds;
3252
+ }
3253
+ function parseMeta(raw) {
3254
+ if (raw === null)
3255
+ return null;
3256
+ try {
3257
+ const meta = JSON.parse(raw);
3258
+ return typeof meta?.url === "string" && typeof meta?.savedAt === "number" ? meta : null;
3259
+ }
3260
+ catch {
3261
+ return null;
3262
+ }
3263
+ }
3264
+ const paywallBodyCache = new PaywallBodyCache();
3265
+
3060
3266
  class StorageService {
3061
3267
  constructor() {
3062
3268
  this.memoryStore = {};
@@ -3139,6 +3345,20 @@ class StorageService {
3139
3345
  }
3140
3346
  this.setItem(key, paywalls);
3141
3347
  }
3348
+ /**
3349
+ * Writes the aggregate `api_paywalls` as the list of URLs a per-URL resolution produced, rather
3350
+ * than a second copy of their bodies (NAM-4357): the bodies already live in the per-URL body
3351
+ * cache, and `getPaywalls(API_PAYWALLS)` rebuilds the paywall objects from it. The legacy bulk
3352
+ * path still writes whole objects through `setPaywalls`.
3353
+ */
3354
+ setApiPaywallsFromBodyCache(urls) {
3355
+ const index = { bodyCacheUrls: urls };
3356
+ if (this.getCampaignCacheTtl() === 0) {
3357
+ this.memoryStore[API_PAYWALLS] = JSON.stringify(index);
3358
+ return;
3359
+ }
3360
+ this.setItem(API_PAYWALLS, index);
3361
+ }
3142
3362
  getPaywalls(key) {
3143
3363
  if (key === INITIAL_PAYWALLS) {
3144
3364
  return this.memoryStore[INITIAL_PAYWALLS]
@@ -3147,8 +3367,10 @@ class StorageService {
3147
3367
  }
3148
3368
  if (key === API_PAYWALLS) {
3149
3369
  const memoryValue = this.memoryStore[API_PAYWALLS];
3150
- if (memoryValue != null)
3151
- return JSON.parse(memoryValue);
3370
+ if (memoryValue != null) {
3371
+ const parsed = JSON.parse(memoryValue);
3372
+ return isBodyCacheAggregate(parsed) ? paywallsFromBodyCache(parsed.bodyCacheUrls) : parsed;
3373
+ }
3152
3374
  let stored = this.getItem(API_PAYWALLS);
3153
3375
  if (stored === null)
3154
3376
  return null;
@@ -3160,6 +3382,8 @@ class StorageService {
3160
3382
  return stored;
3161
3383
  }
3162
3384
  }
3385
+ if (isBodyCacheAggregate(stored))
3386
+ return paywallsFromBodyCache(stored.bodyCacheUrls);
3163
3387
  return this.isLegacyTimestampedCache(stored) ? stored.data : stored;
3164
3388
  }
3165
3389
  return this.getItem(key);
@@ -3328,7 +3552,18 @@ class StorageService {
3328
3552
  this.resetItem(API_CAMPAIGN_RULES);
3329
3553
  this.resetItem(API_CAMPAIGN_SESSION_TIMESTAMP);
3330
3554
  }
3555
+ /**
3556
+ * The effective `campaign_cache_ttl`: a `campaignCacheTTL=<n>` namiCommand first, then the API
3557
+ * config, then the initial config. Lives here rather than in `utils/config` because this service
3558
+ * needs it to decide where the aggregate caches go, and `utils/config` already imports this one.
3559
+ */
3331
3560
  getCampaignCacheTtl() {
3561
+ const override = this.getNamiConfig()?.namiCommands
3562
+ ?.filter((cmd) => cmd.startsWith(CAMPAIGN_CACHE_TTL_PREFIX))
3563
+ .map((cmd) => cmd.slice(CAMPAIGN_CACHE_TTL_PREFIX.length))
3564
+ .find((value) => /^\d+$/.test(value));
3565
+ if (override !== undefined)
3566
+ return parseInt(override, 10);
3332
3567
  const config = this.getAppConfig(API_CONFIG)
3333
3568
  || this.getAppConfig(INITIAL_APP_CONFIG);
3334
3569
  return config?.campaign_cache_ttl ?? null;
@@ -3420,6 +3655,28 @@ class StorageService {
3420
3655
  this.memoryStore = {};
3421
3656
  }
3422
3657
  }
3658
+ function isBodyCacheAggregate(value) {
3659
+ return typeof value === "object" && value !== null && Array.isArray(value.bodyCacheUrls);
3660
+ }
3661
+ /**
3662
+ * The paywalls a per-URL resolution produced, decoded from the body cache. A URL whose entry has
3663
+ * since gone (evicted, purged, or a write that never landed) is simply absent.
3664
+ */
3665
+ function paywallsFromBodyCache(urls) {
3666
+ const paywalls = [];
3667
+ for (const url of urls) {
3668
+ const body = paywallBodyCache.bodyFor(url);
3669
+ if (body === null)
3670
+ continue;
3671
+ try {
3672
+ paywalls.push(JSON.parse(body));
3673
+ }
3674
+ catch {
3675
+ paywallBodyCache.remove(url);
3676
+ }
3677
+ }
3678
+ return paywalls;
3679
+ }
3423
3680
  const storageService = new StorageService();
3424
3681
 
3425
3682
  class RetryLimitExceededError extends Error {
@@ -6797,11 +7054,8 @@ const hasCapability = (capability) => {
6797
7054
  || storageService.getAppConfig(INITIAL_APP_CONFIG);
6798
7055
  return appConfig?.capabilities?.includes(capability) ?? false;
6799
7056
  };
6800
- const getCampaignCacheTtl = () => {
6801
- const config = storageService.getAppConfig(API_CONFIG)
6802
- || storageService.getAppConfig(INITIAL_APP_CONFIG);
6803
- return config?.campaign_cache_ttl ?? null;
6804
- };
7057
+ /** The effective `campaign_cache_ttl`, `campaignCacheTTL=<n>` namiCommand first. */
7058
+ const getCampaignCacheTtl = () => storageService.getCampaignCacheTtl();
6805
7059
  /**
6806
7060
  * The effective global journey settings from the current config, coalescing a missing
6807
7061
  * `journey_settings` object — or any missing field — to {@link DEFAULT_JOURNEY_SETTINGS}.
@@ -7114,211 +7368,6 @@ async function timeoutRequest$1(url, options = {}, timeout) {
7114
7368
  return response;
7115
7369
  }
7116
7370
 
7117
- function requirePlatformID(target, propertyKey, descriptor) {
7118
- const originalMethod = descriptor.value;
7119
- descriptor.value = function (...args) {
7120
- if (!this.platformID) {
7121
- throw new PlatformIDRequiredError();
7122
- }
7123
- return originalMethod.apply(this, args);
7124
- };
7125
- return descriptor;
7126
- }
7127
- function requireDeviceID(target, propertyKey, descriptor) {
7128
- const originalMethod = descriptor.value;
7129
- descriptor.value = function (...args) {
7130
- const device = storageService.getDevice();
7131
- if (!device?.id) {
7132
- throw new DeviceIDRequiredError();
7133
- }
7134
- return originalMethod.apply(this, args);
7135
- };
7136
- return descriptor;
7137
- }
7138
-
7139
- class NamiAPI {
7140
- get deviceID() {
7141
- return storageService.getDevice()?.id;
7142
- }
7143
- constructor() {
7144
- const config = storageService.getNamiConfig();
7145
- if (!this.platformID && config) {
7146
- this.configure(config);
7147
- }
7148
- }
7149
- static configure(config) {
7150
- this.instance.configure(config);
7151
- }
7152
- configure(config) {
7153
- this.baseURL = getBaseUrl(config.namiCommands);
7154
- this.platformID = config.appPlatformID;
7155
- }
7156
- /**
7157
- * Clear cached baseURL and platformID so the next `configure()` call
7158
- * re-derives them from fresh storage. Called from `Nami.reset()`.
7159
- *
7160
- * Without this, the NamiAPI singleton holds the previous lifecycle's
7161
- * platform/base URL even after storage has been wiped — any in-flight
7162
- * or queued request after reset would hit the old endpoint.
7163
- */
7164
- static reset() {
7165
- this.instance.reset();
7166
- }
7167
- reset() {
7168
- this.baseURL = undefined;
7169
- this.platformID = undefined;
7170
- }
7171
- async login(externalId) {
7172
- if (!externalId) {
7173
- throw new ExternalIDRequiredError();
7174
- }
7175
- const path = `device/${this.deviceID}/login/`;
7176
- const data = { external_id: externalId };
7177
- return await this.requestBodyAPI(path, data, 'PUT');
7178
- }
7179
- async logout() {
7180
- const path = `device/${this.deviceID}/logout/`;
7181
- return await this.requestBodyAPI(path, {}, 'PUT');
7182
- }
7183
- async startSession(sessionStartTime) {
7184
- const path = `device/${this.deviceID}/session/`;
7185
- const resp = await this.requestBodyAPI(path, {
7186
- start_time: sessionStartTime,
7187
- app_env: "production",
7188
- });
7189
- storageService.setSessionCounter();
7190
- if (resp.id) {
7191
- storageService.setSessionId(resp.id);
7192
- }
7193
- }
7194
- async postImpression(options) {
7195
- const path = `device/${this.deviceID}/impression/`;
7196
- const resp = await this.requestBodyAPI(path, options, "POST", true);
7197
- if (resp.id) {
7198
- storageService.setLastImpressionId(resp.id);
7199
- // Notify the flow-completion engine (no-op unless a flow is open) so it
7200
- // can bind this impression to the current screen's completion context.
7201
- NamiAPI.impressionListener?.(resp.id);
7202
- }
7203
- }
7204
- /**
7205
- * Reports a flow completion result against a prior impression
7206
- * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
7207
- * impression must already exist. `keepalive` so a dismiss-triggered PATCH
7208
- * survives page unload on web.
7209
- */
7210
- async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
7211
- const path = `device/${this.deviceID}/impression/${impressionId}/`;
7212
- await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7213
- }
7214
- async postConversion(options) {
7215
- const path = `device/${this.deviceID}/transaction/`;
7216
- await this.requestBodyAPI(path, options, "POST", true);
7217
- }
7218
- async purchaseValidation(options) {
7219
- const path = `device/${this.deviceID}/purchase_event/`;
7220
- await this.requestBodyAPI(path, options, "PUT");
7221
- }
7222
- async fetchAPI(path, timeout, retries) {
7223
- const url = `${this.baseURL}/${this.platformID}/` + path;
7224
- return await withRetry(url, undefined, timeout, retries);
7225
- }
7226
- async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
7227
- const url = `${this.baseURL}/${this.platformID}/` + path;
7228
- const options = {
7229
- method,
7230
- headers: {
7231
- "Content-Type": "application/json",
7232
- },
7233
- body: JSON.stringify(bodyData),
7234
- keepalive,
7235
- };
7236
- return await withRetry(url, options, timeout, retries);
7237
- }
7238
- }
7239
- NamiAPI.instance = new NamiAPI();
7240
- __decorate([
7241
- requireDeviceID
7242
- ], NamiAPI.prototype, "login", null);
7243
- __decorate([
7244
- requireDeviceID
7245
- ], NamiAPI.prototype, "logout", null);
7246
- __decorate([
7247
- requireDeviceID
7248
- ], NamiAPI.prototype, "startSession", null);
7249
- __decorate([
7250
- requireDeviceID
7251
- ], NamiAPI.prototype, "postImpression", null);
7252
- __decorate([
7253
- requireDeviceID
7254
- ], NamiAPI.prototype, "patchImpressionCompletion", null);
7255
- __decorate([
7256
- requireDeviceID
7257
- ], NamiAPI.prototype, "postConversion", null);
7258
- __decorate([
7259
- requireDeviceID
7260
- ], NamiAPI.prototype, "purchaseValidation", null);
7261
- __decorate([
7262
- requirePlatformID
7263
- ], NamiAPI.prototype, "fetchAPI", null);
7264
- __decorate([
7265
- requirePlatformID
7266
- ], NamiAPI.prototype, "requestBodyAPI", null);
7267
-
7268
- function isNamiFlowCampaign(campaign) {
7269
- return campaign?.flow != null;
7270
- }
7271
- /**
7272
- * @enum NamiCampaignRuleType
7273
- * Types of Nami campaigns
7274
- */
7275
- var NamiCampaignRuleType;
7276
- (function (NamiCampaignRuleType) {
7277
- NamiCampaignRuleType["DEFAULT"] = "default";
7278
- NamiCampaignRuleType["LABEL"] = "label";
7279
- NamiCampaignRuleType["UNKNOWN"] = "unknown";
7280
- NamiCampaignRuleType["URL"] = "url";
7281
- })(NamiCampaignRuleType || (NamiCampaignRuleType = {}));
7282
- // NamiCampaignManagerStatic interface + const proxy live in
7283
- // `src/managers/campaign.ts`. They must stay out of this file: PaywallState
7284
- // type-imports from here, and pulling in the manager class triggers a
7285
- // constructor chain that runs before PaywallState finishes loading. See NAM-1207.
7286
-
7287
- /**
7288
- * Get initial paywalls from storage service.
7289
- * @returns An array of initial paywalls from storage service.
7290
- */
7291
- const getInitialPaywalls = () => {
7292
- return storageService.getPaywalls(INITIAL_PAYWALLS) ?? [];
7293
- };
7294
- /**
7295
- * Get all API paywalls from storage service.
7296
- * @returns An array of API paywalls from storage service.
7297
- */
7298
- const getApiPaywalls = () => {
7299
- return storageService.getPaywalls(API_PAYWALLS) ?? [];
7300
- };
7301
- /**
7302
- * Get all paywalls from both API and Initial sources, ensuring uniqueness.
7303
- * This function combines paywalls from both sources and filters out duplicates.
7304
- * API Data takes precedence over Initial Data.
7305
- *
7306
- * @returns A combined list of unique paywalls based on both API and Initial data.
7307
- */
7308
- const allPaywalls = () => {
7309
- const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
7310
- return uniqBy(paywalls, "id");
7311
- };
7312
- const getPaywall = (paywallId) => {
7313
- const paywalls = allPaywalls();
7314
- return paywalls.find((paywall) => paywall.id === paywallId);
7315
- };
7316
- const hasAllPaywalls = (paywallIds) => {
7317
- const paywalls = allPaywalls();
7318
- const paywallIdSet = new Set(paywalls.map(p => p.id));
7319
- return paywallIds.every(id => paywallIdSet.has(id));
7320
- };
7321
-
7322
7371
  const globalScope = typeof globalThis !== 'undefined' ? globalThis :
7323
7372
  typeof self !== 'undefined' ? self :
7324
7373
  typeof global !== 'undefined' ? global :
@@ -7542,6 +7591,227 @@ function uuidFromSplitPosition(namiCommands) {
7542
7591
  return value && !isNaN(numericalValue) ? generateUuidForSplitPosition(numericalValue) : undefined;
7543
7592
  }
7544
7593
 
7594
+ function requirePlatformID(target, propertyKey, descriptor) {
7595
+ const originalMethod = descriptor.value;
7596
+ descriptor.value = function (...args) {
7597
+ if (!this.platformID) {
7598
+ throw new PlatformIDRequiredError();
7599
+ }
7600
+ return originalMethod.apply(this, args);
7601
+ };
7602
+ return descriptor;
7603
+ }
7604
+ function requireDeviceID(target, propertyKey, descriptor) {
7605
+ const originalMethod = descriptor.value;
7606
+ descriptor.value = function (...args) {
7607
+ const device = storageService.getDevice();
7608
+ if (!device?.id) {
7609
+ throw new DeviceIDRequiredError();
7610
+ }
7611
+ return originalMethod.apply(this, args);
7612
+ };
7613
+ return descriptor;
7614
+ }
7615
+
7616
+ class NamiAPI {
7617
+ get deviceID() {
7618
+ return storageService.getDevice()?.id;
7619
+ }
7620
+ constructor() {
7621
+ const config = storageService.getNamiConfig();
7622
+ if (!this.platformID && config) {
7623
+ this.configure(config);
7624
+ }
7625
+ }
7626
+ static configure(config) {
7627
+ this.instance.configure(config);
7628
+ }
7629
+ configure(config) {
7630
+ this.baseURL = getBaseUrl(config.namiCommands);
7631
+ this.platformID = config.appPlatformID;
7632
+ }
7633
+ /**
7634
+ * Clear cached baseURL and platformID so the next `configure()` call
7635
+ * re-derives them from fresh storage. Called from `Nami.reset()`.
7636
+ *
7637
+ * Without this, the NamiAPI singleton holds the previous lifecycle's
7638
+ * platform/base URL even after storage has been wiped — any in-flight
7639
+ * or queued request after reset would hit the old endpoint.
7640
+ */
7641
+ static reset() {
7642
+ this.instance.reset();
7643
+ }
7644
+ reset() {
7645
+ this.baseURL = undefined;
7646
+ this.platformID = undefined;
7647
+ }
7648
+ async login(externalId) {
7649
+ if (!externalId) {
7650
+ throw new ExternalIDRequiredError();
7651
+ }
7652
+ const path = `device/${this.deviceID}/login/`;
7653
+ const data = { external_id: externalId };
7654
+ return await this.requestBodyAPI(path, data, 'PUT');
7655
+ }
7656
+ async logout() {
7657
+ const path = `device/${this.deviceID}/logout/`;
7658
+ return await this.requestBodyAPI(path, {}, 'PUT');
7659
+ }
7660
+ async startSession(sessionStartTime) {
7661
+ const path = `device/${this.deviceID}/session/`;
7662
+ const resp = await this.requestBodyAPI(path, {
7663
+ start_time: sessionStartTime,
7664
+ app_env: "production",
7665
+ });
7666
+ storageService.setSessionCounter();
7667
+ if (resp.id) {
7668
+ storageService.setSessionId(resp.id);
7669
+ }
7670
+ }
7671
+ async postImpression(options) {
7672
+ const path = `device/${this.deviceID}/impression/`;
7673
+ // Declare the id rather than taking it from the response. Three things depended on that
7674
+ // response before NAM-4421:
7675
+ //
7676
+ // 1. Retries were not idempotent. `withRetry` re-sends on any non-ok response, so a backend
7677
+ // (or a proxy) that created the impression and then returned 5xx got a *second* impression
7678
+ // per retry — up to three rows for one page view, inflating the denominator of paywall
7679
+ // conversion rate.
7680
+ // 2. Until the response landed, `getLastImpressionId()` still held the previous page's id, so
7681
+ // anything snapshotting in that window — including `setPurchaseImpression()` on a buy tap —
7682
+ // copied the wrong page.
7683
+ // 3. When every retry failed, the id was never published at all, so storage kept the previous
7684
+ // page's id and purchases here were attributed to the page before this one.
7685
+ const impressionId = options.id ?? generateUUID();
7686
+ const body = {
7687
+ ...options,
7688
+ id: impressionId,
7689
+ impression_date: options.impression_date ?? new Date().toISOString(),
7690
+ };
7691
+ storageService.setLastImpressionId(impressionId);
7692
+ // Notify the flow-completion engine (no-op unless a flow is open) so it
7693
+ // can bind this impression to the current screen's completion context.
7694
+ NamiAPI.impressionListener?.(impressionId);
7695
+ await this.requestBodyAPI(path, body, "POST", true);
7696
+ }
7697
+ /**
7698
+ * Reports a flow completion result against a prior impression
7699
+ * (NAM-1545 / Flow Completion Step). UPDATE-only on the server; the
7700
+ * impression must already exist. `keepalive` so a dismiss-triggered PATCH
7701
+ * survives page unload on web.
7702
+ */
7703
+ async patchImpressionCompletion(impressionId, completion_result, completion_result_date) {
7704
+ const path = `device/${this.deviceID}/impression/${impressionId}/`;
7705
+ await this.requestBodyAPI(path, { completion_result, completion_result_date }, "PATCH", true);
7706
+ }
7707
+ async postConversion(options) {
7708
+ const path = `device/${this.deviceID}/transaction/`;
7709
+ await this.requestBodyAPI(path, options, "POST", true);
7710
+ }
7711
+ async purchaseValidation(options) {
7712
+ const path = `device/${this.deviceID}/purchase_event/`;
7713
+ await this.requestBodyAPI(path, options, "PUT");
7714
+ }
7715
+ async fetchAPI(path, timeout, retries) {
7716
+ const url = `${this.baseURL}/${this.platformID}/` + path;
7717
+ return await withRetry(url, undefined, timeout, retries);
7718
+ }
7719
+ async requestBodyAPI(path, bodyData, method = "POST", keepalive = false, timeout, retries) {
7720
+ const url = `${this.baseURL}/${this.platformID}/` + path;
7721
+ const options = {
7722
+ method,
7723
+ headers: {
7724
+ "Content-Type": "application/json",
7725
+ },
7726
+ body: JSON.stringify(bodyData),
7727
+ keepalive,
7728
+ };
7729
+ return await withRetry(url, options, timeout, retries);
7730
+ }
7731
+ }
7732
+ NamiAPI.instance = new NamiAPI();
7733
+ __decorate([
7734
+ requireDeviceID
7735
+ ], NamiAPI.prototype, "login", null);
7736
+ __decorate([
7737
+ requireDeviceID
7738
+ ], NamiAPI.prototype, "logout", null);
7739
+ __decorate([
7740
+ requireDeviceID
7741
+ ], NamiAPI.prototype, "startSession", null);
7742
+ __decorate([
7743
+ requireDeviceID
7744
+ ], NamiAPI.prototype, "postImpression", null);
7745
+ __decorate([
7746
+ requireDeviceID
7747
+ ], NamiAPI.prototype, "patchImpressionCompletion", null);
7748
+ __decorate([
7749
+ requireDeviceID
7750
+ ], NamiAPI.prototype, "postConversion", null);
7751
+ __decorate([
7752
+ requireDeviceID
7753
+ ], NamiAPI.prototype, "purchaseValidation", null);
7754
+ __decorate([
7755
+ requirePlatformID
7756
+ ], NamiAPI.prototype, "fetchAPI", null);
7757
+ __decorate([
7758
+ requirePlatformID
7759
+ ], NamiAPI.prototype, "requestBodyAPI", null);
7760
+
7761
+ function isNamiFlowCampaign(campaign) {
7762
+ return campaign?.flow != null;
7763
+ }
7764
+ /**
7765
+ * @enum NamiCampaignRuleType
7766
+ * Types of Nami campaigns
7767
+ */
7768
+ var NamiCampaignRuleType;
7769
+ (function (NamiCampaignRuleType) {
7770
+ NamiCampaignRuleType["DEFAULT"] = "default";
7771
+ NamiCampaignRuleType["LABEL"] = "label";
7772
+ NamiCampaignRuleType["UNKNOWN"] = "unknown";
7773
+ NamiCampaignRuleType["URL"] = "url";
7774
+ })(NamiCampaignRuleType || (NamiCampaignRuleType = {}));
7775
+ // NamiCampaignManagerStatic interface + const proxy live in
7776
+ // `src/managers/campaign.ts`. They must stay out of this file: PaywallState
7777
+ // type-imports from here, and pulling in the manager class triggers a
7778
+ // constructor chain that runs before PaywallState finishes loading. See NAM-1207.
7779
+
7780
+ /**
7781
+ * Get initial paywalls from storage service.
7782
+ * @returns An array of initial paywalls from storage service.
7783
+ */
7784
+ const getInitialPaywalls = () => {
7785
+ return storageService.getPaywalls(INITIAL_PAYWALLS) ?? [];
7786
+ };
7787
+ /**
7788
+ * Get all API paywalls from storage service.
7789
+ * @returns An array of API paywalls from storage service.
7790
+ */
7791
+ const getApiPaywalls = () => {
7792
+ return storageService.getPaywalls(API_PAYWALLS) ?? [];
7793
+ };
7794
+ /**
7795
+ * Get all paywalls from both API and Initial sources, ensuring uniqueness.
7796
+ * This function combines paywalls from both sources and filters out duplicates.
7797
+ * API Data takes precedence over Initial Data.
7798
+ *
7799
+ * @returns A combined list of unique paywalls based on both API and Initial data.
7800
+ */
7801
+ const allPaywalls = () => {
7802
+ const paywalls = compact([...getApiPaywalls(), ...getInitialPaywalls()]);
7803
+ return uniqBy(paywalls, "id");
7804
+ };
7805
+ const getPaywall = (paywallId) => {
7806
+ const paywalls = allPaywalls();
7807
+ return paywalls.find((paywall) => paywall.id === paywallId);
7808
+ };
7809
+ const hasAllPaywalls = (paywallIds) => {
7810
+ const paywalls = allPaywalls();
7811
+ const paywallIdSet = new Set(paywalls.map(p => p.id));
7812
+ return paywallIds.every(id => paywallIdSet.has(id));
7813
+ };
7814
+
7545
7815
  const formatDistanceLocale$1j = {
7546
7816
  lessThanXSeconds: {
7547
7817
  one: "minder as 'n sekonde",
@@ -57508,22 +57778,23 @@ class CampaignRuleRepository {
57508
57778
  static hasPaywallUrls(campaigns) {
57509
57779
  return campaigns.some((c) => c.paywall_url || c.page_urls || c.flow?.page_urls);
57510
57780
  }
57781
+ /**
57782
+ * The campaign rules for this form factor, unvalidated, plus whether they came from the network.
57783
+ * `fromNetwork` is false when the stored rules stood in for a failed fetch (or updates are
57784
+ * disabled): the per-URL body cache must then neither resolve nor evict (NAM-4357).
57785
+ */
57511
57786
  async fetchCampaignRulesRaw() {
57512
- const authDevice = storageService.getDevice();
57513
57787
  if (this.disableCampaignUpdates) {
57514
57788
  logger.info("Campaign updates disabled by configuration. Skipping fetch.");
57515
- return [];
57789
+ return { campaigns: [], fromNetwork: false };
57516
57790
  }
57517
- let campaignRules;
57518
- if (!authDevice?.id) {
57519
- campaignRules = await this.getAnonymousCampaigns();
57520
- }
57521
- else {
57522
- campaignRules = await this.getCampaigns(authDevice.id);
57523
- }
57524
- campaignRules = campaignRules?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
57791
+ const authDevice = storageService.getDevice();
57792
+ const fetched = authDevice?.id
57793
+ ? await this.fetchCampaigns(authDevice.id)
57794
+ : await this.fetchAnonymousCampaigns();
57795
+ const campaigns = fetched.campaigns?.filter((cRule) => (cRule.paywall && cRule.form_factors.some((f) => f.form_factor === this.currentFormFactor)) ||
57525
57796
  (cRule.flow && cRule.form_factors?.some((f) => f.form_factor === this.currentFormFactor)));
57526
- return campaignRules;
57797
+ return { campaigns, fromNetwork: fetched.fromNetwork };
57527
57798
  }
57528
57799
  finalizeCampaignRules(campaignRules, paywalls) {
57529
57800
  // NAM-2457: a server flow rule is kept only when BOTH assets are satisfied —
@@ -57559,10 +57830,19 @@ class CampaignRuleRepository {
57559
57830
  listeners.forEach((handler) => handler(campaigns));
57560
57831
  }
57561
57832
  async getAnonymousCampaigns(sdkVersion = NAMI_SDK_VERSION) {
57833
+ return (await this.fetchAnonymousCampaigns(sdkVersion)).campaigns;
57834
+ }
57835
+ async getCampaigns(deviceId) {
57836
+ return (await this.fetchCampaigns(deviceId)).campaigns;
57837
+ }
57838
+ async fetchAnonymousCampaigns(sdkVersion = NAMI_SDK_VERSION) {
57562
57839
  const path = `campaign_rules/?sdk_version=${sdkVersion}`;
57563
57840
  try {
57564
57841
  const resp = await NamiAPI.instance.fetchAPI(path);
57565
- return mapAnonymousCampaigns(resp.results ?? [], this.splitPosition, this.currentFormFactor);
57842
+ return {
57843
+ campaigns: mapAnonymousCampaigns(resp.results ?? [], this.splitPosition, this.currentFormFactor),
57844
+ fromNetwork: true,
57845
+ };
57566
57846
  }
57567
57847
  catch (error) {
57568
57848
  const data = this.fallbackData();
@@ -57570,23 +57850,23 @@ class CampaignRuleRepository {
57570
57850
  logger.error(handleErrors(error.status, path));
57571
57851
  throw error;
57572
57852
  }
57573
- return data.results;
57853
+ return { campaigns: data.results, fromNetwork: false };
57574
57854
  }
57575
57855
  }
57576
- async getCampaigns(deviceId) {
57856
+ async fetchCampaigns(deviceId) {
57577
57857
  const path = `device/${deviceId}/campaign_rules/`;
57578
- let data;
57579
57858
  try {
57580
- data = await NamiAPI.instance.fetchAPI(path);
57859
+ const data = await NamiAPI.instance.fetchAPI(path);
57860
+ return { campaigns: data?.results || [], fromNetwork: true };
57581
57861
  }
57582
57862
  catch (error) {
57583
- data = this.fallbackData();
57863
+ const data = this.fallbackData();
57584
57864
  if (!data) {
57585
57865
  logger.error(handleErrors(error.status, path));
57586
57866
  throw error;
57587
57867
  }
57868
+ return { campaigns: data.results || [], fromNetwork: false };
57588
57869
  }
57589
- return data?.results || [];
57590
57870
  }
57591
57871
  fallbackData() {
57592
57872
  const storedData = storageService.getCampaignRules(API_CAMPAIGN_RULES)
@@ -57869,6 +58149,15 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
57869
58149
  * RetryExhaustedError when all attempts and/or the budget are spent.
57870
58150
  */
57871
58151
  async function fetchWithCdnRetry(url, opts = {}) {
58152
+ const { data, retryCount } = await fetchTextWithCdnRetry(url, opts);
58153
+ return { data: JSON.parse(data), retryCount };
58154
+ }
58155
+ /**
58156
+ * {@link fetchWithCdnRetry}'s retry loop, returning the raw 2xx body text instead of parsed JSON.
58157
+ * The per-URL paywall body cache stores those exact bytes, so a cached body is decoded by the
58158
+ * same `JSON.parse` a fetched one is (NAM-4357).
58159
+ */
58160
+ async function fetchTextWithCdnRetry(url, opts = {}) {
57872
58161
  const maxAttempts = opts.maxAttempts ?? DEFAULTS.maxAttempts;
57873
58162
  const baseDelayMs = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
57874
58163
  const maxDelayMs = opts.maxDelayMs ?? DEFAULTS.maxDelayMs;
@@ -57918,8 +58207,7 @@ async function fetchWithCdnRetry(url, opts = {}) {
57918
58207
  const responseBody = await response.clone().text();
57919
58208
  logger.debug(`[HTTP] Response ${response.status}: ${responseBody}`);
57920
58209
  }
57921
- const data = (await response.json());
57922
- return { data, retryCount: attempt };
58210
+ return { data: await response.text(), retryCount: attempt };
57923
58211
  }
57924
58212
  catch (err) {
57925
58213
  if (err instanceof NonRetryableHttpError)
@@ -57940,7 +58228,43 @@ async function fetchWithCdnRetry(url, opts = {}) {
57940
58228
  throw new RetryExhaustedError(url, attempt, lastStatus);
57941
58229
  }
57942
58230
 
58231
+ /**
58232
+ * Reads the parts of a `campaign_rules` paywall URL the per-URL body cache needs (NAM-4357).
58233
+ *
58234
+ * Both helpers scan the string rather than build a `URL`: the cache is keyed on the exact bytes
58235
+ * the server sent, so nothing here may normalise, re-encode or reject a URL — a malformed one is
58236
+ * simply un-hashed.
58237
+ */
58238
+ /**
58239
+ * The `content_hash` query value, or undefined when it is missing or empty. When the item appears
58240
+ * more than once the first value wins. The value is never parsed or validated: it is a
58241
+ * cache-validity token, not a content selector.
58242
+ */
58243
+ function contentHashOf(url) {
58244
+ const query = url.split("#")[0].split("?")[1];
58245
+ if (!query)
58246
+ return undefined;
58247
+ for (const item of query.split("&")) {
58248
+ const eq = item.indexOf("=");
58249
+ const name = eq === -1 ? item : item.slice(0, eq);
58250
+ if (name !== "content_hash")
58251
+ continue;
58252
+ const value = eq === -1 ? "" : item.slice(eq + 1);
58253
+ return value || undefined;
58254
+ }
58255
+ return undefined;
58256
+ }
58257
+ /** The path segment after `/paywall/` — the paywall id the URL names — or undefined. */
58258
+ function paywallIdOf(url) {
58259
+ const path = url.split(/[?#]/)[0];
58260
+ const match = /\/paywall\/([^/]+)/.exec(path);
58261
+ return match ? match[1] : undefined;
58262
+ }
58263
+
57943
58264
  class PaywallRepository {
58265
+ constructor(bodyCache = paywallBodyCache) {
58266
+ this.bodyCache = bodyCache;
58267
+ }
57944
58268
  async fetchPaywalls() {
57945
58269
  const authDevice = storageService.getDevice();
57946
58270
  let paywalls;
@@ -57989,34 +58313,131 @@ class PaywallRepository {
57989
58313
  }
57990
58314
  return data?.results || [];
57991
58315
  }
57992
- async fetchPaywallByUrl(url) {
57993
- return fetchWithCdnRetry(url);
57994
- }
57995
- async fetchPaywallsByUrls(urls) {
58316
+ /**
58317
+ * Resolves the URL set named by `campaign_rules` into paywall objects — the decision table of
58318
+ * `api-resilience-and-caching.md` § 2.2, run identically at startup and on `refresh()`:
58319
+ *
58320
+ * | URL | entry for the exact URL within TTL | action | provenance |
58321
+ * |-----------|------------------------------------|-------------------------------|--------------------------|
58322
+ * | hashed | yes | serve it, no request | `cacheHit` |
58323
+ * | hashed | no | fetch; store the 2xx body | `fetched` |
58324
+ * | un-hashed | not consulted | fetch; store the 2xx body | `fetched` |
58325
+ * | either | fetch failed, entry within TTL | serve it, keep it | `cacheAfterFetchFailure` |
58326
+ * | either | fetch failed, no entry | no body | counts `failed` |
58327
+ *
58328
+ * Writes the aggregate `api_paywalls` — as the URLs of every body this resolution produced, since
58329
+ * the bodies themselves are already in the body cache — unless it produced none, so an outage
58330
+ * cannot overwrite a good aggregate with an empty one. Eviction is the caller's: it runs only
58331
+ * after a complete `campaign_rules` fetch.
58332
+ */
58333
+ async resolvePaywallsByUrls(urls) {
57996
58334
  const uniqueUrls = [...new Set(urls)];
57997
- const results = await Promise.allSettled(uniqueUrls.map((url) => this.fetchPaywallByUrl(url)));
57998
- const paywalls = [];
57999
- let retryCount = 0;
58000
- for (const result of results) {
58001
- if (result.status === "fulfilled") {
58002
- paywalls.push(result.value.data);
58003
- retryCount += result.value.retryCount;
58004
- }
58005
- else {
58006
- // Note: an exhausted-failure path contributed retries we can't see
58007
- // from the rejected promise, so retryCount under-reports failures.
58008
- // Acceptable for the telemetry signal; revisit if full accounting matters.
58009
- logger.error(`Failed to fetch individual paywall: ${result.reason}`);
58335
+ const ttl = getCampaignCacheTtl();
58336
+ this.bodyCache.evaluateTtl(ttl);
58337
+ const resolutions = await Promise.all(uniqueUrls.map((url) => this.resolvePaywall(url, ttl)));
58338
+ const outcome = {
58339
+ paywalls: [],
58340
+ urls: uniqueUrls.length,
58341
+ hashed: uniqueUrls.filter((url) => contentHashOf(url) !== undefined).length,
58342
+ cacheHits: 0,
58343
+ fetched: 0,
58344
+ cacheAfterFetchFailure: 0,
58345
+ failed: 0,
58346
+ retryCount: 0,
58347
+ };
58348
+ const resolvedUrls = new Map();
58349
+ resolutions.forEach((resolution, i) => {
58350
+ outcome.retryCount += resolution.retryCount;
58351
+ if (!resolution.paywall) {
58352
+ outcome.failed += 1;
58353
+ return;
58010
58354
  }
58355
+ resolvedUrls.set(resolution.paywall, uniqueUrls[i]);
58356
+ if (resolution.paywall.fetchSource === "cacheHit")
58357
+ outcome.cacheHits += 1;
58358
+ else if (resolution.paywall.fetchSource === "cacheAfterFetchFailure")
58359
+ outcome.cacheAfterFetchFailure += 1;
58360
+ else
58361
+ outcome.fetched += 1;
58362
+ });
58363
+ outcome.paywalls = this.validatePaywalls([...resolvedUrls.keys()]);
58364
+ if (outcome.paywalls.length > 0) {
58365
+ storageService.setApiPaywallsFromBodyCache(outcome.paywalls.map((paywall) => resolvedUrls.get(paywall)));
58011
58366
  }
58012
- if (paywalls.length > 0) {
58013
- const valid = this.validatePaywalls(paywalls);
58014
- if (valid.length > 0) {
58015
- storageService.setPaywalls(API_PAYWALLS, valid);
58367
+ return outcome;
58368
+ }
58369
+ /**
58370
+ * Seeds the body cache from the initial-config bundle (§ 2.2 "Seeding"): every **hashed** bundled
58371
+ * URL whose paywall id matches a bundled body is stored under that exact URL unless an entry
58372
+ * already exists. A hash in a bundled URL was minted against the bundled body, so the pair is
58373
+ * exact. Runs at configure, before the first resolution.
58374
+ */
58375
+ seedFromInitialConfig(campaignRules, paywalls) {
58376
+ const bundled = new Map(paywalls.map((paywall) => [paywall.id, paywall]));
58377
+ this.bodyCache.evaluateTtl(getCampaignCacheTtl());
58378
+ let candidates = 0;
58379
+ let seeded = 0;
58380
+ for (const url of new Set(bundledPaywallUrls(campaignRules))) {
58381
+ const contentHash = contentHashOf(url);
58382
+ const id = paywallIdOf(url);
58383
+ const paywall = id ? bundled.get(id) : undefined;
58384
+ if (!contentHash || !paywall)
58385
+ continue;
58386
+ candidates += 1;
58387
+ if (this.bodyCache.seedIfAbsent(url, contentHash, JSON.stringify(paywall)))
58388
+ seeded += 1;
58389
+ }
58390
+ logger.info(`Paywall body cache seeded from initial config: seeded=${seeded}, candidates=${candidates}`);
58391
+ }
58392
+ /** One row of the decision table. Never throws. */
58393
+ async resolvePaywall(url, ttl) {
58394
+ const contentHash = contentHashOf(url);
58395
+ if (contentHash) {
58396
+ const hit = this.decodeCached(url, ttl);
58397
+ if (hit)
58398
+ return { paywall: withProvenance(hit, "cacheHit", contentHash), retryCount: 0 };
58399
+ }
58400
+ let retryCount = 0;
58401
+ try {
58402
+ const response = await this.fetchPaywallText(url);
58403
+ retryCount = response.retryCount;
58404
+ const paywall = decodePaywall(response.data);
58405
+ if (paywall) {
58406
+ this.bodyCache.store(url, contentHash, response.data);
58407
+ return { paywall: withProvenance(paywall, "fetched", contentHash), retryCount };
58016
58408
  }
58017
- return { paywalls: valid, retryCount };
58409
+ logger.error(`Paywall body from ${url} is not a decodable paywall`);
58018
58410
  }
58019
- return { paywalls, retryCount };
58411
+ catch (error) {
58412
+ // Note: an exhausted-failure path contributed retries we can't see from the thrown error,
58413
+ // so retry_count under-reports failures. Acceptable for the telemetry signal.
58414
+ logger.error(`Failed to fetch individual paywall: ${error}`);
58415
+ }
58416
+ const fallback = this.decodeCached(url, ttl);
58417
+ if (fallback) {
58418
+ logger.warn(`Serving the cached body for ${url} after fetch failure (within campaign_cache_ttl)`);
58419
+ return { paywall: withProvenance(fallback, "cacheAfterFetchFailure", contentHash), retryCount };
58420
+ }
58421
+ return { retryCount };
58422
+ }
58423
+ /** The stored body for this exact URL, decoded; an undecodable one is deleted (not eviction). */
58424
+ decodeCached(url, ttl) {
58425
+ const entry = this.bodyCache.validEntry(url, ttl);
58426
+ if (!entry)
58427
+ return undefined;
58428
+ const paywall = decodePaywall(entry.body);
58429
+ if (!paywall) {
58430
+ logger.warn(`Deleting an undecodable cached paywall body for ${url}`);
58431
+ this.bodyCache.remove(url);
58432
+ }
58433
+ return paywall;
58434
+ }
58435
+ fetchPaywallText(url) {
58436
+ if (storageService.getNamiConfig()?.namiCommands?.includes(SIMULATE_PAYWALL_FETCH_FAILURE)) {
58437
+ logger.warn(`${SIMULATE_PAYWALL_FETCH_FAILURE}: failing the fetch for ${url} before the network`);
58438
+ return Promise.reject(new RetryExhaustedError(url, 0));
58439
+ }
58440
+ return fetchTextWithCdnRetry(url);
58020
58441
  }
58021
58442
  validatePaywalls(paywalls) {
58022
58443
  return paywalls.filter((paywall) => {
@@ -58041,6 +58462,33 @@ class PaywallRepository {
58041
58462
  }
58042
58463
  }
58043
58464
  PaywallRepository.instance = new PaywallRepository();
58465
+ function decodePaywall(text) {
58466
+ try {
58467
+ const value = JSON.parse(text);
58468
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
58469
+ }
58470
+ catch {
58471
+ return undefined;
58472
+ }
58473
+ }
58474
+ function withProvenance(paywall, fetchSource, contentHash) {
58475
+ return { ...paywall, fetchSource, ...(contentHash ? { contentHash } : {}) };
58476
+ }
58477
+ /** Every paywall URL a bundled (anonymous-format) rule names, across all of its segments. */
58478
+ function bundledPaywallUrls(campaignRules) {
58479
+ const urls = [];
58480
+ for (const rule of campaignRules) {
58481
+ for (const segment of rule.segments ?? []) {
58482
+ if (segment.paywall_url)
58483
+ urls.push(segment.paywall_url);
58484
+ if (segment.page_urls)
58485
+ urls.push(...Object.values(segment.page_urls));
58486
+ if (segment.flow?.page_urls)
58487
+ urls.push(...Object.values(segment.flow.page_urls));
58488
+ }
58489
+ }
58490
+ return urls;
58491
+ }
58044
58492
 
58045
58493
  class ConfigRepository {
58046
58494
  /**
@@ -61841,7 +62289,10 @@ class NamiRefs {
61841
62289
  }
61842
62290
  }
61843
62291
  if (initconfig?.paywalls) {
61844
- storageService.setPaywalls(INITIAL_PAYWALLS, initconfig.paywalls);
62292
+ storageService.setPaywalls(INITIAL_PAYWALLS, initconfig.paywalls.map((paywall) => ({ ...paywall, fetchSource: "initialConfig" })));
62293
+ if (initconfig.campaign_rules?.length) {
62294
+ PaywallRepository.instance.seedFromInitialConfig(initconfig.campaign_rules, initconfig.paywalls);
62295
+ }
61845
62296
  }
61846
62297
  if (initconfig?.products) {
61847
62298
  storageService.setProducts(INITIAL_PRODUCTS, initconfig.products);
@@ -61908,7 +62359,7 @@ class NamiRefs {
61908
62359
  const paywallRepo = PaywallRepository.instance;
61909
62360
  const startTime = Date.now();
61910
62361
  const campaignRulesStartTime = Date.now();
61911
- const rawCampaigns = await campaignRepo.fetchCampaignRulesRaw();
62362
+ const { campaigns: rawCampaigns, fromNetwork } = await campaignRepo.fetchCampaignRulesRaw();
61912
62363
  const campaignRulesDuration = Date.now() - campaignRulesStartTime;
61913
62364
  // Hydrate flow objects: fetch flow JSON from flow.url for campaigns that
61914
62365
  // use the URL-based format (4.0.0+ since NAM-3493) instead of inline flow.object
@@ -61929,24 +62380,35 @@ class NamiRefs {
61929
62380
  const useIndividual = !campaignRepo.useLegacyPaywallFetch &&
61930
62381
  CampaignRuleRepository.hasPaywallUrls(rawCampaigns);
61931
62382
  let paywalls;
61932
- let retryCount = 0;
62383
+ let outcome = null;
61933
62384
  const paywallsStartTime = Date.now();
61934
- if (useIndividual) {
62385
+ if (useIndividual && !fromNetwork) {
62386
+ // campaign_rules came from the stored copy, so this is not a complete page set: no
62387
+ // resolution and no eviction (api-resilience-and-caching.md § 2.2). The aggregate serves.
62388
+ logger.info("Paywall resolution skipped: campaign_rules were served from the stored copy");
62389
+ paywalls = getApiPaywalls();
62390
+ }
62391
+ else if (useIndividual) {
61935
62392
  const urls = CampaignRuleRepository.extractPaywallUrls(rawCampaigns);
61936
- const result = await paywallRepo.fetchPaywallsByUrls(urls);
61937
- paywalls = result.paywalls;
61938
- retryCount = result.retryCount;
62393
+ outcome = await paywallRepo.resolvePaywallsByUrls(urls);
62394
+ paywalls = outcome.paywalls;
62395
+ paywallBodyCache.reconcile(new Set(urls));
61939
62396
  }
61940
62397
  else {
61941
62398
  paywalls = await paywallRepo.fetchPaywalls();
61942
62399
  }
61943
62400
  const paywallsDuration = Date.now() - paywallsStartTime;
61944
62401
  const totalDuration = Date.now() - startTime;
61945
- if (Nami.instance.maxLogging) {
61946
- logger.info(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
61947
- `count=${paywalls.length}, total=${totalDuration}ms, ` +
62402
+ if (!useIndividual || outcome) {
62403
+ // The line every SDK emits once per resolution; the paywall-cache e2e suites parse it.
62404
+ const cacheFields = outcome
62405
+ ? `, urls=${outcome.urls}, hashed=${outcome.hashed}, cache_hits=${outcome.cacheHits}, ` +
62406
+ `fetched=${outcome.fetched}, cache_after_fetch_failure=${outcome.cacheAfterFetchFailure}, failed=${outcome.failed}`
62407
+ : "";
62408
+ logger.debug(`Paywall fetch telemetry: strategy=${useIndividual ? "individual" : "bulk"}, ` +
62409
+ `count=${paywalls.length}${cacheFields}, total=${totalDuration}ms, ` +
61948
62410
  `campaigns=${campaignRulesDuration}ms, paywalls=${paywallsDuration}ms` +
61949
- `${useIndividual ? `, retry_count=${retryCount}` : ""}`);
62411
+ `${outcome ? `, retry_count=${outcome.retryCount}` : ""}`);
61950
62412
  }
61951
62413
  return campaignRepo.finalizeCampaignRules(rawCampaigns, paywalls);
61952
62414
  }
@@ -62120,6 +62582,8 @@ class Nami {
62120
62582
  static async reset() {
62121
62583
  var _a;
62122
62584
  storageService.clearAll();
62585
+ // The per-URL paywall body cache keeps its own prefixed keys and an in-memory index (NAM-4357).
62586
+ paywallBodyCache.purgeAll();
62123
62587
  clearInMemoryAnonymousMode();
62124
62588
  __classPrivateFieldSet(Nami.instance, _Nami_isInitialized, false, "f");
62125
62589
  __classPrivateFieldGet((_a = Nami.instance), _Nami_removeForegroundListener, "f")?.call(_a);
@@ -66221,4 +66685,4 @@ var internal = /*#__PURE__*/Object.freeze({
66221
66685
  NamiPurchaseManager: NamiPurchaseManager
66222
66686
  });
66223
66687
 
66224
- export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_TRAFFIC_OBSERVER, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_NOT_AVAILABLE, CONFIG_API_TIMEOUT_LIMIT, CONFIG_MAX_ATTEMPTS, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEFAULT_JOURNEY_SETTINGS, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$1 as NamiPurchaseManager, NamiRefs, NamiReservedActions, NotFoundError, PAYWALL_ACTION_EVENT, PLATFORM_ID_REQUIRED, PRODUCTION, PaywallManagerEvents, PaywallRepository, PaywallState, PlacementLabelResolver, PlatformIDRequiredError, ProductRepository, RECONFIG_SUCCESS, RESUME_THROTTLE_MS, RetryLimitExceededError, SDKNotInitializedError, SDK_NOT_INITIALIZED, SERVER_NAMI_ENTITLEMENTS, SESSION_REQUIRED, SHOULD_SHOW_LOADING_INDICATOR, SIMULATE_CONFIG_FETCH_FAILURE, SKU_TEXT_REGEX, SMART_TEXT_PATTERN, STARTUP_TELEMETRY, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, TRANSPORT_FAILURE_STATUS, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, NamiProfileManager$1 as _NamiProfileManager, internal as _internal, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, didReceiveResponse, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getJourneySettings, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerAPITrafficObserver, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };
66688
+ export { ALREADY_CONFIGURED, ANONYMOUS_MODE, ANONYMOUS_MODE_ALREADY_OFF, ANONYMOUS_MODE_ALREADY_ON, ANONYMOUS_MODE_LOGIN_NOT_ALLOWED, ANONYMOUS_UUID, APIError, API_ACTIVE_ENTITLEMENTS, API_CAMPAIGN_RULES, API_CAMPAIGN_SESSION_TIMESTAMP, API_CONFIG, API_MAX_CALLS_LIMIT, API_PAYWALLS, API_PRODUCTS, API_RETRY_DELAY_SEC, API_TIMEOUT_LIMIT, API_TRAFFIC_OBSERVER, API_VERSION, AUTH_DEVICE, AVAILABLE_ACTIVE_ENTITLEMENTS_CHANGED, AVAILABLE_CAMPAIGNS_CHANGED, AccountStateAction, AnonymousCDPError, AnonymousLoginError, AnonymousModeAlreadyOffError, AnonymousModeAlreadyOnError, BASE_STAGING_URL, BASE_URL, BASE_URL_PATH, BadRequestError, BasicNamiFlow, BorderMap, BorderSideMap, CAMPAIGN_CACHE_TTL_PREFIX, CAMPAIGN_NOT_AVAILABLE, CONFIG_API_TIMEOUT_LIMIT, CONFIG_MAX_ATTEMPTS, CUSTOMER_ATTRIBUTES_KEY_PREFIX, CUSTOMER_JOURNEY_STATE_CHANGED, CUSTOM_HOST_PREFIX, CampaignNotAvailableError, CampaignRuleConversionEventType, CampaignRuleRepository, Capabilities, ClientError, ConfigRepository, ConflictError, CustomerJourneyRepository, DEFAULT_JOURNEY_SETTINGS, DEVELOPMENT, DEVICE_API_TIMEOUT_LIMIT, DEVICE_ID_NOT_SET, DEVICE_ID_REQUIRED, DISABLE_ASYNC_LOGIN_LOGOUT, DeviceIDRequiredError, DeviceRepository, EXTENDED_CLIENT_INFO_DELIMITER, EXTENDED_CLIENT_INFO_PREFIX, EXTENDED_PLATFORM, EXTENDED_PLATFORM_VERSION, EXTERNAL_ID_REQUIRED, EntitlementRepository, EntitlementUtils, ExternalIDRequiredError, FLOW_SCREENS_NOT_AVAILABLE, FlowScreensNotAvailableError, HTML_REGEX, INITIAL_APP_CONFIG, INITIAL_CAMPAIGN_RULES, INITIAL_PAYWALLS, INITIAL_PRODUCTS, INITIAL_SESSION_COUNTER_VALUE, INITIAL_SUCCESS, InternalServerError, KEY_SESSION_COUNTER, LIQUID_VARIABLE_REGEX, LOCAL_NAMI_ENTITLEMENTS, LOG_HTTP_REQUESTS, LOG_HTTP_TRAFFIC, LaunchCampaignError, LaunchContextResolver, LogLevel, NAMI_CONFIGURATION, NAMI_CUSTOMER_JOURNEY_STATE, NAMI_LANGUAGE_CODE, NAMI_LAST_IMPRESSION_ID, NAMI_LAUNCH_ID, NAMI_PROFILE, NAMI_PURCHASE_CHANNEL, NAMI_PURCHASE_IMPRESSION_ID, NAMI_SDK_PACKAGE_VERSION, NAMI_SDK_VERSION, NAMI_SESSION_ID, NAMI_STORAGE_KEYS, Nami, NamiAPI, NamiAnimationType, NamiCampaignManager$1 as NamiCampaignManager, NamiCampaignRuleType, NamiConditionEvaluator, NamiCustomerManager$1 as NamiCustomerManager, NamiEntitlementManager$1 as NamiEntitlementManager, NamiEventEmitter, NamiFlow, NamiFlowActionFunction, NamiFlowManager$1 as NamiFlowManager, NamiFlowStepType, NamiHandoffTag, NamiPaywallAction, NamiPaywallManager$1 as NamiPaywallManager, PaywallManagerEvents as NamiPaywallManagerEvents, NamiPermissionOutcome, NamiPurchaseManager$1 as NamiPurchaseManager, NamiRefs, NamiReservedActions, NotFoundError, PAYWALL_ACTION_EVENT, PLATFORM_ID_REQUIRED, PRODUCTION, PaywallManagerEvents, PaywallRepository, PaywallState, PlacementLabelResolver, PlatformIDRequiredError, ProductRepository, RECONFIG_SUCCESS, RESUME_THROTTLE_MS, RetryLimitExceededError, SDKNotInitializedError, SDK_NOT_INITIALIZED, SERVER_NAMI_ENTITLEMENTS, SESSION_REQUIRED, SHOULD_SHOW_LOADING_INDICATOR, SIMULATE_CONFIG_FETCH_FAILURE, SIMULATE_PAYWALL_FETCH_FAILURE, SKU_TEXT_REGEX, SMART_TEXT_PATTERN, STARTUP_TELEMETRY, STATUS_BAD_REQUEST, STATUS_CONFLICT, STATUS_INTERNAL_SERVER_ERROR, STATUS_NOT_FOUND, STATUS_SUCCESS, SessionService, SimpleEventTarget, StorageService, TRANSPORT_FAILURE_STATUS, UNABLE_TO_UPDATE_CDP_ID, USE_STAGING_API, VALIDATE_PRODUCT_GROUPS, VAR_REGEX, NamiProfileManager$1 as _NamiProfileManager, internal as _internal, activateEntitlementByPurchase, activeEntitlements, aggregateScreenreaderText, allCampaigns, allPaywalls, applyEntitlementActivation, audienceSplitPosition, bestUrlCampaignMatch, bigintToUuid, checkAnySkuHasPromoOffer, checkAnySkuHasTrialOffer, convertISO8601PeriodToText, convertLocale, convertOfferToPricingPhase, createNamiEntitlements, currentSku, didReceiveResponse, empty, extractStandardPricingPhases, formatDate, formattedPrice, generateUUID, getApiCampaigns, getApiPaywalls, getBaseUrl, getBillingPeriodNumber, getCurrencyFormat, getDeviceData, getDeviceFormFactor, getDeviceScaleFactor, getEffectiveWebStyle, getEntitlementRefIdsForSku, getExtendedClientInfo, getFreeTrialPeriod, getInitialCampaigns, getInitialPaywalls, getJourneySettings, getPaywall, getPaywallDataFromLabel, getPercentagePriceDifference, getPeriodNumberInDays, getPeriodNumberInWeeks, getPlatformAdapters, getPriceDifference, getPricePerMonth, getProductDetail, getPurchaseAdapter, getReferenceSku, getSkuProductDetailKeys, getSkuSmartTextValue, getSlideSmartTextValue, getStandardBillingPeriod, getTranslate, getUrlParams, handleErrors, hasAllPaywalls, hasCapability, hasPurchaseManagement, initialState, invokeHandler, isAnonymousMode, isInitialConfigCompressed, isNamiFlowCampaign, isSubscription, isValidISODate, isValidUrl, logger, mapAnonymousCampaigns, namiBuySKU, normalizeLaunchContext, parseToSemver, postConversion, productDetail, registerAPITrafficObserver, registerPlatformAdapters, registerPurchaseAdapter, resolveLoopSource, selectSegment, setActiveNamiEntitlements, shouldValidateProductGroups, skuItems, skuMapFromEntitlements, storageService, toDouble, toNamiEntitlements, toNamiSKU, tryParseB64Gzip, tryParseJson, updateRelatedSKUsForNamiEntitlement, uuidFromSplitPosition, validateForm, validateMinSDKVersion, validateTextInput };