@proveanything/smartlinks 1.9.0 → 1.9.1

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.9.0 | Generated: 2026-03-22T15:51:25.814Z
3
+ Version: 1.9.1 | Generated: 2026-03-23T16:13:10.706Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -280,6 +280,8 @@ const path = utils.buildPortalPath(params)
280
280
 
281
281
  The `validateCondition` function helps determine if content should be shown or hidden based on various criteria like geography, device type, user status, dates, and more.
282
282
 
283
+ Enable verbose tracing per invocation with `debugConditions`, or set `globalThis.SMARTLINKS_CONDITION_DEBUG = true` in a browser/devtools session to trace every evaluation.
284
+
283
285
  ### Basic Usage
284
286
 
285
287
  ```typescript
@@ -291,7 +293,6 @@ const canShow = await utils.validateCondition({
291
293
  type: 'and',
292
294
  conditions: [{
293
295
  type: 'country',
294
- useRegions: true,
295
296
  regions: ['eu'],
296
297
  contains: true
297
298
  }]
@@ -328,7 +329,6 @@ await utils.validateCondition({
328
329
  type: 'and',
329
330
  conditions: [{
330
331
  type: 'country',
331
- useRegions: true,
332
332
  regions: ['eu', 'uk'],
333
333
  contains: true // true = show IN these regions, false = hide IN these regions
334
334
  }]
@@ -336,6 +336,20 @@ await utils.validateCondition({
336
336
  user: { valid: true, location: { country: 'FR' } }
337
337
  })
338
338
 
339
+ // Regions and explicit countries can be combined
340
+ await utils.validateCondition({
341
+ condition: {
342
+ type: 'and',
343
+ conditions: [{
344
+ type: 'country',
345
+ regions: ['eu'],
346
+ countries: ['CH'],
347
+ contains: true
348
+ }]
349
+ },
350
+ user: { valid: true, location: { country: 'CH' } }
351
+ })
352
+
339
353
  // Or specific countries
340
354
  await utils.validateCondition({
341
355
  condition: {
@@ -582,7 +596,7 @@ await utils.validateCondition({
582
596
  conditions: [
583
597
  { type: 'user', userType: 'valid' },
584
598
  { type: 'device', displays: ['mobile'], contains: true },
585
- { type: 'country', useRegions: true, regions: ['eu'], contains: true }
599
+ { type: 'country', regions: ['eu'], contains: true }
586
600
  ]
587
601
  },
588
602
  user: { valid: true, location: { country: 'FR' } },
@@ -630,12 +644,49 @@ const showNewFeature = await utils.validateCondition({
630
644
  condition: {
631
645
  type: 'and',
632
646
  conditions: [
633
- { type: 'country', useRegions: true, regions: ['northamerica'], contains: true },
647
+ { type: 'country', regions: ['northamerica'], contains: true },
634
648
  { type: 'date', dateTest: 'after', afterDate: '2026-03-01' }
635
649
  ]
636
650
  },
637
651
  user: { valid: true, location: { country: 'US' } }
638
652
  })
653
+
654
+ ### Debug Logging
655
+
656
+ Trace which condition is being evaluated, whether it passed, and why:
657
+
658
+ ```typescript
659
+ await utils.validateCondition({
660
+ condition: {
661
+ type: 'and',
662
+ conditions: [
663
+ { type: 'user', userType: 'valid' },
664
+ { type: 'country', regions: ['eu'], contains: true }
665
+ ]
666
+ },
667
+ user: { valid: true, location: { country: 'DE' } },
668
+ debugConditions: true
669
+ })
670
+
671
+ // Or enable globally in the browser console/devtools
672
+ globalThis.SMARTLINKS_CONDITION_DEBUG = true
673
+ ```
674
+
675
+ If you want to route logs somewhere specific, pass a logger:
676
+
677
+ ```typescript
678
+ await utils.validateCondition({
679
+ condition: {
680
+ type: 'and',
681
+ conditions: [{ type: 'user', userType: 'valid' }]
682
+ },
683
+ user: { valid: true },
684
+ debugConditions: {
685
+ label: 'checkout-gate',
686
+ logger: (...args) => console.log(...args)
687
+ }
688
+ })
689
+ ```
639
690
  ```
640
691
 
641
692
  #### Mobile-Only Features
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { iframe } from "./iframe";
5
5
  export * as cache from './cache';
6
6
  export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
7
7
  export * as utils from './utils';
8
- export type { PortalPathParams, ConditionParams, ConditionSet, Condition, UserInfo, ProductInfo, ProofInfo, CollectionInfo, } from './utils';
8
+ export type { PortalPathParams, ConditionParams, ConditionDebugOptions, ConditionDebugLogger, ConditionSet, Condition, UserInfo, ProductInfo, ProofInfo, CollectionInfo, } from './utils';
9
9
  export type { LoginResponse, VerifyTokenResponse, AccountInfoResponse, AuthLocation, } from "./api/auth";
10
10
  export type { UserAccountRegistrationRequest, } from "./types/auth";
11
11
  export type { CommunicationEvent, CommsQueryByUser, CommsRecipientIdsQuery, CommsRecipientsWithoutActionQuery, CommsRecipientsWithActionQuery, RecipientId, RecipientWithOutcome, LogCommunicationEventBody, LogBulkCommunicationEventsBody, AppendResult, AppendBulkResult, CommsSettings, TopicConfig, CommsSettingsGetResponse, CommsSettingsPatchBody, CommsPublicTopicsResponse, UnsubscribeQuery, UnsubscribeResponse, CommsConsentUpsertRequest, CommsPreferencesUpsertRequest, CommsSubscribeRequest, CommsSubscribeResponse, CommsSubscriptionCheckQuery, CommsSubscriptionCheckResponse, CommsListMethodsQuery, CommsListMethodsResponse, RegisterEmailMethodRequest, RegisterSmsMethodRequest, RegisterMethodResponse, SubscriptionsResolveRequest, SubscriptionsResolveResponse, } from "./types/comms";
@@ -23,6 +23,7 @@ export interface BaseCondition {
23
23
  export interface CountryCondition extends BaseCondition {
24
24
  type: 'country';
25
25
  countries?: string[];
26
+ /** @deprecated Regions are applied automatically when regions is provided. */
26
27
  useRegions?: boolean;
27
28
  regions?: RegionKey[];
28
29
  contains: boolean;
@@ -209,9 +210,17 @@ export interface ConditionParams {
209
210
  latitude: number;
210
211
  longitude: number;
211
212
  }>;
213
+ /** Enable verbose condition evaluation logging for this invocation */
214
+ debugConditions?: boolean | ConditionDebugOptions;
212
215
  /** Any additional custom fields for value-based conditions */
213
216
  [key: string]: any;
214
217
  }
218
+ export type ConditionDebugLogger = (...args: any[]) => void;
219
+ export interface ConditionDebugOptions {
220
+ enabled?: boolean;
221
+ logger?: ConditionDebugLogger;
222
+ label?: string;
223
+ }
215
224
  /**
216
225
  * Validates if a condition set passes based on the provided parameters.
217
226
  *
@@ -246,7 +255,6 @@ export interface ConditionParams {
246
255
  * type: 'and',
247
256
  * conditions: [{
248
257
  * type: 'country',
249
- * useRegions: true,
250
258
  * regions: ['eu'],
251
259
  * contains: true
252
260
  * }]
@@ -16,6 +16,139 @@ export const REGION_COUNTRIES = {
16
16
  };
17
17
  // Condition cache
18
18
  const conditionCache = {};
19
+ const CONDITION_DEBUG_GLOBAL_KEY = 'SMARTLINKS_CONDITION_DEBUG';
20
+ function defaultConditionDebugLogger(...args) {
21
+ if (typeof console === 'undefined')
22
+ return;
23
+ if (typeof console.debug === 'function') {
24
+ console.debug(...args);
25
+ return;
26
+ }
27
+ if (typeof console.log === 'function') {
28
+ console.log(...args);
29
+ }
30
+ }
31
+ function getGlobalConditionDebugOptions() {
32
+ if (typeof globalThis === 'undefined') {
33
+ return undefined;
34
+ }
35
+ const value = globalThis[CONDITION_DEBUG_GLOBAL_KEY];
36
+ if (typeof value === 'boolean') {
37
+ return value;
38
+ }
39
+ if (value && typeof value === 'object') {
40
+ return value;
41
+ }
42
+ return undefined;
43
+ }
44
+ function resolveConditionDebugState(params) {
45
+ var _a, _b, _c;
46
+ const globalOptions = getGlobalConditionDebugOptions();
47
+ const localOptions = params.debugConditions;
48
+ const mergedOptions = Object.assign(Object.assign({}, (typeof globalOptions === 'object' ? globalOptions : {})), (typeof localOptions === 'object' ? localOptions : {}));
49
+ let enabled = false;
50
+ if (typeof globalOptions === 'boolean') {
51
+ enabled = globalOptions;
52
+ }
53
+ else if (typeof globalOptions === 'object' && globalOptions) {
54
+ enabled = (_a = globalOptions.enabled) !== null && _a !== void 0 ? _a : true;
55
+ }
56
+ if (typeof localOptions === 'boolean') {
57
+ enabled = localOptions;
58
+ }
59
+ else if (typeof localOptions === 'object' && localOptions) {
60
+ enabled = (_b = localOptions.enabled) !== null && _b !== void 0 ? _b : true;
61
+ }
62
+ if (!enabled) {
63
+ return undefined;
64
+ }
65
+ return {
66
+ depth: 0,
67
+ label: mergedOptions.label,
68
+ logger: (_c = mergedOptions.logger) !== null && _c !== void 0 ? _c : defaultConditionDebugLogger,
69
+ };
70
+ }
71
+ function createChildDebugState(state) {
72
+ if (!state) {
73
+ return undefined;
74
+ }
75
+ return Object.assign(Object.assign({}, state), { depth: state.depth + 1 });
76
+ }
77
+ function logConditionDebug(state, message, context) {
78
+ if (!state) {
79
+ return;
80
+ }
81
+ const indent = ' '.repeat(state.depth);
82
+ const prefix = state.label
83
+ ? `[smartlinks:conditions:${state.label}]`
84
+ : '[smartlinks:conditions]';
85
+ if (context) {
86
+ state.logger(`${prefix} ${indent}${message}`, context);
87
+ return;
88
+ }
89
+ state.logger(`${prefix} ${indent}${message}`);
90
+ }
91
+ function summarizeConditionSet(condition) {
92
+ var _a, _b, _c;
93
+ return `${(_a = condition.type) !== null && _a !== void 0 ? _a : 'and'} (${(_c = (_b = condition.conditions) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0} conditions)`;
94
+ }
95
+ function summarizeCondition(condition) {
96
+ var _a, _b;
97
+ switch (condition.type) {
98
+ case 'country':
99
+ return `country regions=${((_a = condition.regions) === null || _a === void 0 ? void 0 : _a.join(',')) || 'none'} countries=${((_b = condition.countries) === null || _b === void 0 ? void 0 : _b.join(',')) || 'none'} contains=${condition.contains}`;
100
+ case 'version':
101
+ return `version [${condition.versions.join(', ')}] contains=${condition.contains}`;
102
+ case 'device':
103
+ return `device [${condition.displays.join(', ')}] contains=${condition.contains}`;
104
+ case 'condition':
105
+ return `condition ref=${condition.conditionId} passes=${condition.passes}`;
106
+ case 'user':
107
+ return `user type=${condition.userType}`;
108
+ case 'product':
109
+ return `product ids=${condition.productIds.join(', ')} contains=${condition.contains}`;
110
+ case 'tag':
111
+ return `tag tags=${condition.tags.join(', ')} contains=${condition.contains}`;
112
+ case 'date':
113
+ return `date test=${condition.dateTest}`;
114
+ case 'geofence':
115
+ return `geofence contains=${condition.contains}`;
116
+ case 'value':
117
+ return `value field=${condition.field} ${condition.validationType} ${String(condition.value)}`;
118
+ case 'itemStatus':
119
+ return `itemStatus ${condition.statusType}`;
120
+ default:
121
+ return 'unknown condition';
122
+ }
123
+ }
124
+ async function evaluateConditionEntry(condition, params) {
125
+ switch (condition.type) {
126
+ case 'country':
127
+ return validateCountry(condition, params);
128
+ case 'version':
129
+ return validateVersion(condition, params);
130
+ case 'device':
131
+ return validateDevice(condition, params);
132
+ case 'condition':
133
+ return validateNestedCondition(condition, params);
134
+ case 'user':
135
+ return validateUser(condition, params);
136
+ case 'product':
137
+ return validateProduct(condition, params);
138
+ case 'tag':
139
+ return validateTag(condition, params);
140
+ case 'date':
141
+ return validateDate(condition, params);
142
+ case 'geofence':
143
+ return validateGeofence(condition, params);
144
+ case 'value':
145
+ return validateValue(condition, params);
146
+ case 'itemStatus':
147
+ return validateItemStatus(condition, params);
148
+ default:
149
+ return undefined;
150
+ }
151
+ }
19
152
  /**
20
153
  * Validates if a condition set passes based on the provided parameters.
21
154
  *
@@ -50,7 +183,6 @@ const conditionCache = {};
50
183
  * type: 'and',
51
184
  * conditions: [{
52
185
  * type: 'country',
53
- * useRegions: true,
54
186
  * regions: ['eu'],
55
187
  * contains: true
56
188
  * }]
@@ -85,9 +217,13 @@ const conditionCache = {};
85
217
  * ```
86
218
  */
87
219
  export async function validateCondition(params) {
88
- var _a, _b;
220
+ var _a, _b, _c;
221
+ const internalParams = params;
222
+ (_a = internalParams.__conditionDebugState) !== null && _a !== void 0 ? _a : (internalParams.__conditionDebugState = resolveConditionDebugState(params));
223
+ const debugState = internalParams.__conditionDebugState;
89
224
  // If no condition specified, pass by default
90
225
  if (!params.conditionId && !params.condition) {
226
+ logConditionDebug(debugState, 'No condition supplied; passing by default.');
91
227
  return true;
92
228
  }
93
229
  let cond = params.condition;
@@ -95,86 +231,78 @@ export async function validateCondition(params) {
95
231
  if (params.conditionId) {
96
232
  // Check cache first
97
233
  if (!conditionCache[params.conditionId]) {
234
+ logConditionDebug(debugState, 'Condition cache miss.', { conditionId: params.conditionId });
98
235
  // Try to fetch if function provided
99
236
  if (params.fetchCondition && params.collection) {
100
237
  const fetchedCond = await params.fetchCondition(params.collection.id, params.conditionId);
101
238
  if (fetchedCond) {
102
239
  conditionCache[params.conditionId] = fetchedCond;
240
+ logConditionDebug(debugState, 'Fetched condition into cache.', {
241
+ conditionId: params.conditionId,
242
+ conditionSet: summarizeConditionSet(fetchedCond),
243
+ });
103
244
  }
104
245
  }
105
246
  }
106
247
  if (!conditionCache[params.conditionId]) {
248
+ logConditionDebug(debugState, 'Condition not found; passing by default.', { conditionId: params.conditionId });
107
249
  return true;
108
250
  }
109
251
  // Prevent infinite recursion
110
- (_a = params.conditionStack) !== null && _a !== void 0 ? _a : (params.conditionStack = []);
252
+ (_b = params.conditionStack) !== null && _b !== void 0 ? _b : (params.conditionStack = []);
111
253
  params.conditionStack.push(params.conditionId);
112
254
  cond = conditionCache[params.conditionId];
113
255
  }
256
+ else {
257
+ logConditionDebug(debugState, 'Evaluating inline condition set.', {
258
+ conditionSet: cond ? summarizeConditionSet(cond) : 'none',
259
+ });
260
+ }
114
261
  if (!cond) {
262
+ logConditionDebug(debugState, 'Resolved condition set is empty; passing by default.');
115
263
  return true;
116
264
  }
117
265
  // Default to AND logic
118
- (_b = cond.type) !== null && _b !== void 0 ? _b : (cond.type = 'and');
266
+ const conditionType = (_c = cond.type) !== null && _c !== void 0 ? _c : 'and';
119
267
  // Empty condition set passes
120
268
  if (!cond.conditions || cond.conditions.length === 0) {
269
+ logConditionDebug(debugState, 'Condition set has no entries; passing by default.', {
270
+ conditionSet: summarizeConditionSet(cond),
271
+ });
121
272
  return true;
122
273
  }
274
+ logConditionDebug(debugState, 'Condition set start.', {
275
+ logic: conditionType,
276
+ conditionCount: cond.conditions.length,
277
+ conditionId: params.conditionId,
278
+ });
123
279
  // Evaluate each condition
124
- for (const c of cond.conditions) {
125
- let result = false;
126
- switch (c.type) {
127
- case 'country':
128
- result = await validateCountry(c, params);
129
- break;
130
- case 'version':
131
- result = await validateVersion(c, params);
132
- break;
133
- case 'device':
134
- result = await validateDevice(c, params);
135
- break;
136
- case 'condition':
137
- result = await validateNestedCondition(c, params);
138
- break;
139
- case 'user':
140
- result = await validateUser(c, params);
141
- break;
142
- case 'product':
143
- result = await validateProduct(c, params);
144
- break;
145
- case 'tag':
146
- result = await validateTag(c, params);
147
- break;
148
- case 'date':
149
- result = await validateDate(c, params);
150
- break;
151
- case 'geofence':
152
- result = await validateGeofence(c, params);
153
- break;
154
- case 'value':
155
- result = await validateValue(c, params);
156
- break;
157
- case 'itemStatus':
158
- result = await validateItemStatus(c, params);
159
- break;
160
- default:
161
- // Unknown condition type, skip
162
- continue;
280
+ for (const [index, c] of cond.conditions.entries()) {
281
+ logConditionDebug(debugState, `Condition ${index + 1}/${cond.conditions.length} start: ${summarizeCondition(c)}`);
282
+ const evaluation = await evaluateConditionEntry(c, internalParams);
283
+ if (!evaluation) {
284
+ logConditionDebug(debugState, `Condition ${index + 1} skipped: unknown type.`, { type: c.type });
285
+ continue;
163
286
  }
287
+ logConditionDebug(debugState, `Condition ${index + 1} result: ${evaluation.passed ? 'PASS' : 'FAIL'}`, Object.assign({ type: c.type, detail: evaluation.detail }, evaluation.context));
164
288
  // AND logic: if any condition fails, entire set fails
165
- if (!result && cond.type === 'and') {
289
+ if (!evaluation.passed && conditionType === 'and') {
290
+ logConditionDebug(debugState, 'Condition set short-circuited to FAIL (AND logic).');
166
291
  return false;
167
292
  }
168
293
  // OR logic: if any condition passes, entire set passes
169
- if (result && cond.type === 'or') {
294
+ if (evaluation.passed && conditionType === 'or') {
295
+ logConditionDebug(debugState, 'Condition set short-circuited to PASS (OR logic).');
170
296
  return true;
171
297
  }
172
298
  }
173
299
  // AND: all passed
174
- if (cond.type === 'and') {
300
+ if (conditionType === 'and') {
301
+ logConditionDebug(debugState, 'Condition set result: PASS.');
175
302
  return true;
176
303
  }
177
304
  // OR: all failed
305
+ logConditionDebug(debugState, 'Condition set result: FAIL.');
178
306
  return false;
179
307
  }
180
308
  /**
@@ -184,27 +312,42 @@ async function validateCountry(condition, params) {
184
312
  var _a, _b, _c;
185
313
  const country = (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.location) === null || _b === void 0 ? void 0 : _b.country;
186
314
  if (!country) {
187
- return false;
315
+ return {
316
+ passed: false,
317
+ detail: 'User country was not available.',
318
+ };
188
319
  }
189
- // Build country list from regions or direct country list
190
- let countryList = condition.countries || [];
191
- if (condition.useRegions && ((_c = condition.regions) === null || _c === void 0 ? void 0 : _c.length)) {
192
- countryList = [];
320
+ // Build country list from direct countries and any configured regions.
321
+ const countryList = [...(condition.countries || [])];
322
+ if ((_c = condition.regions) === null || _c === void 0 ? void 0 : _c.length) {
193
323
  for (const region of condition.regions) {
194
324
  if (REGION_COUNTRIES[region]) {
195
325
  countryList.push(...REGION_COUNTRIES[region]);
196
326
  }
197
327
  }
198
- // Remove duplicates
199
- countryList = [...new Set(countryList)];
200
328
  }
201
- if (!countryList.length) {
202
- return false;
329
+ const normalizedCountryList = [...new Set(countryList)];
330
+ if (!normalizedCountryList.length) {
331
+ return {
332
+ passed: false,
333
+ detail: 'No countries or regions were configured on the condition.',
334
+ };
203
335
  }
204
- const inList = countryList.includes(country);
336
+ const inList = normalizedCountryList.includes(country);
205
337
  // contains=true: pass if country IS in list
206
338
  // contains=false: pass if country is NOT in list
207
- return condition.contains ? inList : !inList;
339
+ return {
340
+ passed: condition.contains ? inList : !inList,
341
+ detail: condition.contains
342
+ ? `Country ${country} ${inList ? 'matched' : 'did not match'} the allowed list.`
343
+ : `Country ${country} ${inList ? 'matched' : 'did not match'} the blocked list.`,
344
+ context: {
345
+ country,
346
+ regions: condition.regions,
347
+ countryList: normalizedCountryList,
348
+ contains: condition.contains,
349
+ },
350
+ };
208
351
  }
209
352
  /**
210
353
  * Validate version-based condition
@@ -213,7 +356,15 @@ async function validateVersion(condition, params) {
213
356
  var _a, _b;
214
357
  const version = (_b = (_a = params.stats) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : null;
215
358
  const inList = condition.versions.includes(version);
216
- return condition.contains ? inList : !inList;
359
+ return {
360
+ passed: condition.contains ? inList : !inList,
361
+ detail: `Version ${version !== null && version !== void 0 ? version : 'null'} ${inList ? 'matched' : 'did not match'} the configured list.`,
362
+ context: {
363
+ version,
364
+ versions: condition.versions,
365
+ contains: condition.contains,
366
+ },
367
+ };
217
368
  }
218
369
  /**
219
370
  * Validate device/platform condition
@@ -225,25 +376,53 @@ async function validateDevice(condition, params) {
225
376
  const mobile = (_b = params.stats) === null || _b === void 0 ? void 0 : _b.mobile;
226
377
  for (const display of displays) {
227
378
  if (display === 'android' && (platform === null || platform === void 0 ? void 0 : platform.android)) {
228
- return condition.contains;
379
+ return {
380
+ passed: condition.contains,
381
+ detail: 'Matched android platform.',
382
+ context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
383
+ };
229
384
  }
230
385
  if (display === 'ios' && (platform === null || platform === void 0 ? void 0 : platform.ios)) {
231
- return condition.contains;
386
+ return {
387
+ passed: condition.contains,
388
+ detail: 'Matched ios platform.',
389
+ context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
390
+ };
232
391
  }
233
392
  if (display === 'win' && (platform === null || platform === void 0 ? void 0 : platform.win)) {
234
- return condition.contains;
393
+ return {
394
+ passed: condition.contains,
395
+ detail: 'Matched win platform.',
396
+ context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
397
+ };
235
398
  }
236
399
  if (display === 'mac' && (platform === null || platform === void 0 ? void 0 : platform.mac)) {
237
- return condition.contains;
400
+ return {
401
+ passed: condition.contains,
402
+ detail: 'Matched mac platform.',
403
+ context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
404
+ };
238
405
  }
239
406
  if (display === 'desktop' && !mobile) {
240
- return condition.contains;
407
+ return {
408
+ passed: condition.contains,
409
+ detail: 'Matched desktop device mode.',
410
+ context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
411
+ };
241
412
  }
242
413
  if (display === 'mobile' && mobile) {
243
- return condition.contains;
414
+ return {
415
+ passed: condition.contains,
416
+ detail: 'Matched mobile device mode.',
417
+ context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
418
+ };
244
419
  }
245
420
  }
246
- return !condition.contains;
421
+ return {
422
+ passed: !condition.contains,
423
+ detail: 'No configured display matched the current platform/device.',
424
+ context: { displays, contains: condition.contains, platform, mobile },
425
+ };
247
426
  }
248
427
  /**
249
428
  * Validate nested condition reference
@@ -251,48 +430,90 @@ async function validateDevice(condition, params) {
251
430
  async function validateNestedCondition(condition, params) {
252
431
  const newParams = Object.assign({}, params);
253
432
  newParams.conditionId = condition.conditionId;
433
+ newParams.__conditionDebugState = createChildDebugState(params.__conditionDebugState);
254
434
  // Prevent infinite recursion
255
435
  newParams.conditionStack = [...(newParams.conditionStack || [])];
256
436
  if (newParams.conditionStack.includes(condition.conditionId)) {
257
- return true;
437
+ return {
438
+ passed: true,
439
+ detail: `Nested condition ${condition.conditionId} skipped to avoid recursion.`,
440
+ context: { conditionId: condition.conditionId, conditionStack: newParams.conditionStack },
441
+ };
258
442
  }
259
443
  const result = await validateCondition(newParams);
260
- return condition.passes ? result : !result;
444
+ return {
445
+ passed: condition.passes ? result : !result,
446
+ detail: `Nested condition ${condition.conditionId} ${result ? 'passed' : 'failed'} and passes=${condition.passes}.`,
447
+ context: { conditionId: condition.conditionId, nestedResult: result, passes: condition.passes },
448
+ };
261
449
  }
262
450
  /**
263
451
  * Validate user-based condition
264
452
  */
265
453
  async function validateUser(condition, params) {
266
- var _a, _b, _c, _d, _e, _f, _g;
454
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
267
455
  const userType = condition.userType;
268
456
  switch (userType) {
269
457
  case 'valid':
270
458
  // User is logged in
271
- return (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.valid) !== null && _b !== void 0 ? _b : false;
459
+ return {
460
+ passed: (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.valid) !== null && _b !== void 0 ? _b : false,
461
+ detail: `User valid flag is ${(_d = (_c = params.user) === null || _c === void 0 ? void 0 : _c.valid) !== null && _d !== void 0 ? _d : false}.`,
462
+ };
272
463
  case 'invalid':
273
464
  // User is not logged in
274
- return !((_d = (_c = params.user) === null || _c === void 0 ? void 0 : _c.valid) !== null && _d !== void 0 ? _d : false);
465
+ return {
466
+ passed: !((_f = (_e = params.user) === null || _e === void 0 ? void 0 : _e.valid) !== null && _f !== void 0 ? _f : false),
467
+ detail: `User valid flag is ${(_h = (_g = params.user) === null || _g === void 0 ? void 0 : _g.valid) !== null && _h !== void 0 ? _h : false}.`,
468
+ };
275
469
  case 'owner':
276
470
  // User owns the proof
277
- return !!(params.proof &&
278
- ((_e = params.user) === null || _e === void 0 ? void 0 : _e.valid) &&
279
- params.user.uid &&
280
- params.user.uid === params.proof.userId);
471
+ return {
472
+ passed: !!(params.proof &&
473
+ ((_j = params.user) === null || _j === void 0 ? void 0 : _j.valid) &&
474
+ params.user.uid &&
475
+ params.user.uid === params.proof.userId),
476
+ detail: 'Owner check compares user.uid with proof.userId.',
477
+ context: {
478
+ userId: (_k = params.user) === null || _k === void 0 ? void 0 : _k.uid,
479
+ proofUserId: (_l = params.proof) === null || _l === void 0 ? void 0 : _l.userId,
480
+ userValid: (_o = (_m = params.user) === null || _m === void 0 ? void 0 : _m.valid) !== null && _o !== void 0 ? _o : false,
481
+ },
482
+ };
281
483
  case 'admin':
282
484
  // User is admin of the collection
283
- return !!(params.collection &&
284
- ((_f = params.user) === null || _f === void 0 ? void 0 : _f.valid) &&
285
- params.user.uid &&
286
- params.collection.roles &&
287
- params.collection.roles[params.user.uid]);
485
+ return {
486
+ passed: !!(params.collection &&
487
+ ((_p = params.user) === null || _p === void 0 ? void 0 : _p.valid) &&
488
+ params.user.uid &&
489
+ params.collection.roles &&
490
+ params.collection.roles[params.user.uid]),
491
+ detail: 'Admin check looks for a role entry on collection.roles.',
492
+ context: {
493
+ userId: (_q = params.user) === null || _q === void 0 ? void 0 : _q.uid,
494
+ userValid: (_s = (_r = params.user) === null || _r === void 0 ? void 0 : _r.valid) !== null && _s !== void 0 ? _s : false,
495
+ hasRoles: !!((_t = params.collection) === null || _t === void 0 ? void 0 : _t.roles),
496
+ },
497
+ };
288
498
  case 'group':
289
499
  // User is member of specific group
290
500
  // TODO: Implement group membership check
291
- return !!(params.collection &&
292
- ((_g = params.user) === null || _g === void 0 ? void 0 : _g.valid) &&
293
- params.user.groups);
501
+ return {
502
+ passed: !!(params.collection &&
503
+ ((_u = params.user) === null || _u === void 0 ? void 0 : _u.valid) &&
504
+ params.user.groups),
505
+ detail: 'Group condition currently checks only that groups are present on the user.',
506
+ context: {
507
+ groupId: condition.groupId,
508
+ groups: (_v = params.user) === null || _v === void 0 ? void 0 : _v.groups,
509
+ userValid: (_x = (_w = params.user) === null || _w === void 0 ? void 0 : _w.valid) !== null && _x !== void 0 ? _x : false,
510
+ },
511
+ };
294
512
  default:
295
- return true;
513
+ return {
514
+ passed: true,
515
+ detail: `Unknown userType ${userType}; passing by default.`,
516
+ };
296
517
  }
297
518
  }
298
519
  /**
@@ -303,10 +524,18 @@ async function validateProduct(condition, params) {
303
524
  const productId = (_a = params.product) === null || _a === void 0 ? void 0 : _a.id;
304
525
  // No product ID available
305
526
  if (!productId) {
306
- return !condition.contains;
527
+ return {
528
+ passed: !condition.contains,
529
+ detail: 'Product ID was not available.',
530
+ context: { contains: condition.contains, productIds: condition.productIds },
531
+ };
307
532
  }
308
533
  const inList = condition.productIds.includes(productId);
309
- return condition.contains ? inList : !inList;
534
+ return {
535
+ passed: condition.contains ? inList : !inList,
536
+ detail: `Product ${productId} ${inList ? 'matched' : 'did not match'} the configured list.`,
537
+ context: { productId, productIds: condition.productIds, contains: condition.contains },
538
+ };
310
539
  }
311
540
  /**
312
541
  * Validate tag-based condition
@@ -316,19 +545,35 @@ async function validateTag(condition, params) {
316
545
  const productId = (_a = params.product) === null || _a === void 0 ? void 0 : _a.id;
317
546
  // No product
318
547
  if (!productId) {
319
- return !condition.contains;
548
+ return {
549
+ passed: !condition.contains,
550
+ detail: 'Product ID was not available.',
551
+ context: { contains: condition.contains, tags: condition.tags },
552
+ };
320
553
  }
321
554
  // No tags on product
322
555
  if (!((_b = params.product) === null || _b === void 0 ? void 0 : _b.tags)) {
323
- return !condition.contains;
556
+ return {
557
+ passed: !condition.contains,
558
+ detail: 'Product tags were not available.',
559
+ context: { productId, contains: condition.contains, tags: condition.tags },
560
+ };
324
561
  }
325
562
  // Check if any condition tag exists on product
326
563
  for (const tag of condition.tags) {
327
564
  if (params.product.tags[tag]) {
328
- return condition.contains;
565
+ return {
566
+ passed: condition.contains,
567
+ detail: `Product ${productId} matched tag ${tag}.`,
568
+ context: { productId, matchedTag: tag, contains: condition.contains },
569
+ };
329
570
  }
330
571
  }
331
- return !condition.contains;
572
+ return {
573
+ passed: !condition.contains,
574
+ detail: `Product ${productId} did not match any configured tag.`,
575
+ context: { productId, tags: condition.tags, contains: condition.contains },
576
+ };
332
577
  }
333
578
  /**
334
579
  * Validate date-based condition
@@ -337,21 +582,39 @@ async function validateDate(condition, params) {
337
582
  const now = Date.now();
338
583
  switch (condition.dateTest) {
339
584
  case 'before':
340
- if (!condition.beforeDate)
341
- return false;
342
- return now < Date.parse(condition.beforeDate);
585
+ if (!condition.beforeDate) {
586
+ return { passed: false, detail: 'beforeDate was not provided.' };
587
+ }
588
+ return {
589
+ passed: now < Date.parse(condition.beforeDate),
590
+ detail: `Current time is ${now < Date.parse(condition.beforeDate) ? 'before' : 'not before'} ${condition.beforeDate}.`,
591
+ context: { now, beforeDate: condition.beforeDate },
592
+ };
343
593
  case 'after':
344
- if (!condition.afterDate)
345
- return false;
346
- return now > Date.parse(condition.afterDate);
594
+ if (!condition.afterDate) {
595
+ return { passed: false, detail: 'afterDate was not provided.' };
596
+ }
597
+ return {
598
+ passed: now > Date.parse(condition.afterDate),
599
+ detail: `Current time is ${now > Date.parse(condition.afterDate) ? 'after' : 'not after'} ${condition.afterDate}.`,
600
+ context: { now, afterDate: condition.afterDate },
601
+ };
347
602
  case 'between':
348
- if (!condition.rangeDate || condition.rangeDate.length !== 2)
349
- return false;
603
+ if (!condition.rangeDate || condition.rangeDate.length !== 2) {
604
+ return { passed: false, detail: 'rangeDate must contain exactly two entries.' };
605
+ }
350
606
  const start = Date.parse(condition.rangeDate[0]);
351
607
  const end = Date.parse(condition.rangeDate[1]);
352
- return now > start && now < end;
608
+ return {
609
+ passed: now > start && now < end,
610
+ detail: `Current time is ${now > start && now < end ? 'within' : 'outside'} the configured date range.`,
611
+ context: { now, start, end, rangeDate: condition.rangeDate },
612
+ };
353
613
  default:
354
- return false;
614
+ return {
615
+ passed: false,
616
+ detail: `Unsupported dateTest ${condition.dateTest}.`,
617
+ };
355
618
  }
356
619
  }
357
620
  /**
@@ -374,18 +637,39 @@ async function validateGeofence(condition, params) {
374
637
  lng = location.longitude;
375
638
  }
376
639
  catch (error) {
377
- return false;
640
+ return {
641
+ passed: false,
642
+ detail: 'getLocation threw while resolving user coordinates.',
643
+ context: { error },
644
+ };
378
645
  }
379
646
  }
380
647
  if (lat === undefined || lng === undefined) {
381
- return false;
648
+ return {
649
+ passed: false,
650
+ detail: 'Latitude/longitude were not available.',
651
+ };
382
652
  }
383
653
  // Check if outside bounding box
384
654
  const outside = lat > condition.top ||
385
655
  lat < condition.bottom ||
386
656
  lng < condition.left ||
387
657
  lng > condition.right;
388
- return condition.contains ? !outside : outside;
658
+ return {
659
+ passed: condition.contains ? !outside : outside,
660
+ detail: `Coordinates are ${outside ? 'outside' : 'inside'} the configured geofence.`,
661
+ context: {
662
+ latitude: lat,
663
+ longitude: lng,
664
+ contains: condition.contains,
665
+ bounds: {
666
+ top: condition.top,
667
+ bottom: condition.bottom,
668
+ left: condition.left,
669
+ right: condition.right,
670
+ },
671
+ },
672
+ };
389
673
  }
390
674
  /**
391
675
  * Validate value comparison condition
@@ -399,7 +683,10 @@ async function validateValue(condition, params) {
399
683
  base = base[field];
400
684
  }
401
685
  else {
402
- return false;
686
+ return {
687
+ passed: false,
688
+ detail: `Field ${condition.field} was not found in the provided params.`,
689
+ };
403
690
  }
404
691
  }
405
692
  // Convert value to correct type
@@ -411,18 +698,37 @@ async function validateValue(condition, params) {
411
698
  val = parseInt(val, 10);
412
699
  }
413
700
  // Perform comparison
701
+ let passed = false;
414
702
  switch (condition.validationType) {
415
703
  case 'equal':
416
- return base == val;
704
+ passed = base == val;
705
+ break;
417
706
  case 'not':
418
- return base != val;
707
+ passed = base != val;
708
+ break;
419
709
  case 'greater':
420
- return base > val;
710
+ passed = base > val;
711
+ break;
421
712
  case 'less':
422
- return base < val;
713
+ passed = base < val;
714
+ break;
423
715
  default:
424
- return false;
716
+ return {
717
+ passed: false,
718
+ detail: `Unsupported validationType ${condition.validationType}.`,
719
+ };
425
720
  }
721
+ return {
722
+ passed,
723
+ detail: `Compared field ${condition.field} (${String(base)}) with ${String(val)} using ${condition.validationType}.`,
724
+ context: {
725
+ field: condition.field,
726
+ fieldValue: base,
727
+ comparisonValue: val,
728
+ fieldType: condition.fieldType,
729
+ validationType: condition.validationType,
730
+ },
731
+ };
426
732
  }
427
733
  /**
428
734
  * Validate item status condition
@@ -430,19 +736,40 @@ async function validateValue(condition, params) {
430
736
  async function validateItemStatus(condition, params) {
431
737
  switch (condition.statusType) {
432
738
  case 'isClaimable':
433
- return !!(params.proof && params.proof.claimable);
739
+ return {
740
+ passed: !!(params.proof && params.proof.claimable),
741
+ detail: 'Checked proof.claimable for truthiness.',
742
+ };
434
743
  case 'notClaimable':
435
- return !!(params.proof && !params.proof.claimable);
744
+ return {
745
+ passed: !!(params.proof && !params.proof.claimable),
746
+ detail: 'Checked proof.claimable for falsiness.',
747
+ };
436
748
  case 'noProof':
437
- return !params.proof;
749
+ return {
750
+ passed: !params.proof,
751
+ detail: 'Checked that no proof object was provided.',
752
+ };
438
753
  case 'hasProof':
439
- return !!params.proof;
754
+ return {
755
+ passed: !!params.proof,
756
+ detail: 'Checked that a proof object was provided.',
757
+ };
440
758
  case 'isVirtual':
441
- return !!(params.proof && params.proof.virtual);
759
+ return {
760
+ passed: !!(params.proof && params.proof.virtual),
761
+ detail: 'Checked proof.virtual for truthiness.',
762
+ };
442
763
  case 'notVirtual':
443
- return !!(params.proof && !params.proof.virtual);
764
+ return {
765
+ passed: !!(params.proof && !params.proof.virtual),
766
+ detail: 'Checked proof.virtual for falsiness.',
767
+ };
444
768
  default:
445
- return false;
769
+ return {
770
+ passed: false,
771
+ detail: `Unsupported statusType ${condition.statusType}.`,
772
+ };
446
773
  }
447
774
  }
448
775
  /**
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.9.0 | Generated: 2026-03-22T15:51:25.814Z
3
+ Version: 1.9.1 | Generated: 2026-03-23T16:13:10.706Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
package/docs/utils.md CHANGED
@@ -280,6 +280,8 @@ const path = utils.buildPortalPath(params)
280
280
 
281
281
  The `validateCondition` function helps determine if content should be shown or hidden based on various criteria like geography, device type, user status, dates, and more.
282
282
 
283
+ Enable verbose tracing per invocation with `debugConditions`, or set `globalThis.SMARTLINKS_CONDITION_DEBUG = true` in a browser/devtools session to trace every evaluation.
284
+
283
285
  ### Basic Usage
284
286
 
285
287
  ```typescript
@@ -291,7 +293,6 @@ const canShow = await utils.validateCondition({
291
293
  type: 'and',
292
294
  conditions: [{
293
295
  type: 'country',
294
- useRegions: true,
295
296
  regions: ['eu'],
296
297
  contains: true
297
298
  }]
@@ -328,7 +329,6 @@ await utils.validateCondition({
328
329
  type: 'and',
329
330
  conditions: [{
330
331
  type: 'country',
331
- useRegions: true,
332
332
  regions: ['eu', 'uk'],
333
333
  contains: true // true = show IN these regions, false = hide IN these regions
334
334
  }]
@@ -336,6 +336,20 @@ await utils.validateCondition({
336
336
  user: { valid: true, location: { country: 'FR' } }
337
337
  })
338
338
 
339
+ // Regions and explicit countries can be combined
340
+ await utils.validateCondition({
341
+ condition: {
342
+ type: 'and',
343
+ conditions: [{
344
+ type: 'country',
345
+ regions: ['eu'],
346
+ countries: ['CH'],
347
+ contains: true
348
+ }]
349
+ },
350
+ user: { valid: true, location: { country: 'CH' } }
351
+ })
352
+
339
353
  // Or specific countries
340
354
  await utils.validateCondition({
341
355
  condition: {
@@ -582,7 +596,7 @@ await utils.validateCondition({
582
596
  conditions: [
583
597
  { type: 'user', userType: 'valid' },
584
598
  { type: 'device', displays: ['mobile'], contains: true },
585
- { type: 'country', useRegions: true, regions: ['eu'], contains: true }
599
+ { type: 'country', regions: ['eu'], contains: true }
586
600
  ]
587
601
  },
588
602
  user: { valid: true, location: { country: 'FR' } },
@@ -630,12 +644,49 @@ const showNewFeature = await utils.validateCondition({
630
644
  condition: {
631
645
  type: 'and',
632
646
  conditions: [
633
- { type: 'country', useRegions: true, regions: ['northamerica'], contains: true },
647
+ { type: 'country', regions: ['northamerica'], contains: true },
634
648
  { type: 'date', dateTest: 'after', afterDate: '2026-03-01' }
635
649
  ]
636
650
  },
637
651
  user: { valid: true, location: { country: 'US' } }
638
652
  })
653
+
654
+ ### Debug Logging
655
+
656
+ Trace which condition is being evaluated, whether it passed, and why:
657
+
658
+ ```typescript
659
+ await utils.validateCondition({
660
+ condition: {
661
+ type: 'and',
662
+ conditions: [
663
+ { type: 'user', userType: 'valid' },
664
+ { type: 'country', regions: ['eu'], contains: true }
665
+ ]
666
+ },
667
+ user: { valid: true, location: { country: 'DE' } },
668
+ debugConditions: true
669
+ })
670
+
671
+ // Or enable globally in the browser console/devtools
672
+ globalThis.SMARTLINKS_CONDITION_DEBUG = true
673
+ ```
674
+
675
+ If you want to route logs somewhere specific, pass a logger:
676
+
677
+ ```typescript
678
+ await utils.validateCondition({
679
+ condition: {
680
+ type: 'and',
681
+ conditions: [{ type: 'user', userType: 'valid' }]
682
+ },
683
+ user: { valid: true },
684
+ debugConditions: {
685
+ label: 'checkout-gate',
686
+ logger: (...args) => console.log(...args)
687
+ }
688
+ })
689
+ ```
639
690
  ```
640
691
 
641
692
  #### Mobile-Only Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",