@proveanything/smartlinks 1.3.25 → 1.3.27

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.
@@ -0,0 +1,453 @@
1
+ // src/utils/conditions.ts
2
+ /**
3
+ * Geographic region definitions for country-based conditions
4
+ */
5
+ export const REGION_COUNTRIES = {
6
+ // European Union (27 member states as of 2026)
7
+ eu: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'],
8
+ // European Economic Area (EU + EFTA countries in EEA)
9
+ eea: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', 'LI', 'NO'],
10
+ // United Kingdom
11
+ uk: ['GB'],
12
+ // North America
13
+ northamerica: ['US', 'CA', 'MX'],
14
+ // Asia Pacific (major markets)
15
+ asiapacific: ['AU', 'NZ', 'JP', 'KR', 'SG', 'HK', 'TW', 'TH', 'MY', 'PH', 'ID', 'VN', 'IN']
16
+ };
17
+ // Condition cache
18
+ const conditionCache = {};
19
+ /**
20
+ * Validates if a condition set passes based on the provided parameters.
21
+ *
22
+ * Conditions are commonly used for controlling page rendering, content visibility,
23
+ * and feature access based on various criteria like geography, device type, user status, etc.
24
+ *
25
+ * Supports multiple condition types:
26
+ * - **country** - Geographic restrictions (countries or regions like EU, EEA)
27
+ * - **version** - Version-based display (e.g., A/B testing)
28
+ * - **device** - Platform/device targeting (iOS, Android, desktop, mobile)
29
+ * - **user** - User authentication status (logged in, owner, admin)
30
+ * - **product** - Product-specific conditions
31
+ * - **tag** - Product tag-based conditions
32
+ * - **date** - Time-based conditions (before, after, between dates)
33
+ * - **geofence** - Location-based restrictions
34
+ * - **value** - Custom field comparisons
35
+ * - **itemStatus** - Proof/item status checks (claimable, virtual, etc.)
36
+ * - **condition** - Nested condition references
37
+ *
38
+ * Conditions can be combined with AND or OR logic.
39
+ *
40
+ * @param params - Validation parameters including condition and context
41
+ * @returns Promise that resolves to true if condition passes, false otherwise
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { validateCondition } from '@proveanything/smartlinks'
46
+ *
47
+ * // Simple country check
48
+ * const canShow = await validateCondition({
49
+ * condition: {
50
+ * type: 'and',
51
+ * conditions: [{
52
+ * type: 'country',
53
+ * useRegions: true,
54
+ * regions: ['eu'],
55
+ * contains: true
56
+ * }]
57
+ * },
58
+ * user: { valid: true, location: { country: 'DE' } }
59
+ * })
60
+ *
61
+ * // Multiple conditions with AND logic
62
+ * const result = await validateCondition({
63
+ * condition: {
64
+ * type: 'and',
65
+ * conditions: [
66
+ * { type: 'user', userType: 'valid' },
67
+ * { type: 'device', displays: ['mobile'], contains: true }
68
+ * ]
69
+ * },
70
+ * user: { valid: true },
71
+ * stats: { mobile: true }
72
+ * })
73
+ *
74
+ * // Date-based condition
75
+ * const isActive = await validateCondition({
76
+ * condition: {
77
+ * type: 'and',
78
+ * conditions: [{
79
+ * type: 'date',
80
+ * dateTest: 'between',
81
+ * rangeDate: ['2026-01-01', '2026-12-31']
82
+ * }]
83
+ * }
84
+ * })
85
+ * ```
86
+ */
87
+ export async function validateCondition(params) {
88
+ var _a, _b;
89
+ // If no condition specified, pass by default
90
+ if (!params.conditionId && !params.condition) {
91
+ return true;
92
+ }
93
+ let cond = params.condition;
94
+ // Load condition by ID if specified
95
+ if (params.conditionId) {
96
+ // Check cache first
97
+ if (!conditionCache[params.conditionId]) {
98
+ // Try to fetch if function provided
99
+ if (params.fetchCondition && params.collection) {
100
+ const fetchedCond = await params.fetchCondition(params.collection.id, params.conditionId);
101
+ if (fetchedCond) {
102
+ conditionCache[params.conditionId] = fetchedCond;
103
+ }
104
+ }
105
+ }
106
+ if (!conditionCache[params.conditionId]) {
107
+ return true;
108
+ }
109
+ // Prevent infinite recursion
110
+ (_a = params.conditionStack) !== null && _a !== void 0 ? _a : (params.conditionStack = []);
111
+ params.conditionStack.push(params.conditionId);
112
+ cond = conditionCache[params.conditionId];
113
+ }
114
+ if (!cond) {
115
+ return true;
116
+ }
117
+ // Default to AND logic
118
+ (_b = cond.type) !== null && _b !== void 0 ? _b : (cond.type = 'and');
119
+ // Empty condition set passes
120
+ if (!cond.conditions || cond.conditions.length === 0) {
121
+ return true;
122
+ }
123
+ // 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;
163
+ }
164
+ // AND logic: if any condition fails, entire set fails
165
+ if (!result && cond.type === 'and') {
166
+ return false;
167
+ }
168
+ // OR logic: if any condition passes, entire set passes
169
+ if (result && cond.type === 'or') {
170
+ return true;
171
+ }
172
+ }
173
+ // AND: all passed
174
+ if (cond.type === 'and') {
175
+ return true;
176
+ }
177
+ // OR: all failed
178
+ return false;
179
+ }
180
+ /**
181
+ * Validate country-based condition
182
+ */
183
+ async function validateCountry(condition, params) {
184
+ var _a, _b, _c;
185
+ const country = (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.location) === null || _b === void 0 ? void 0 : _b.country;
186
+ if (!country) {
187
+ return false;
188
+ }
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 = [];
193
+ for (const region of condition.regions) {
194
+ if (REGION_COUNTRIES[region]) {
195
+ countryList.push(...REGION_COUNTRIES[region]);
196
+ }
197
+ }
198
+ // Remove duplicates
199
+ countryList = [...new Set(countryList)];
200
+ }
201
+ if (!countryList.length) {
202
+ return false;
203
+ }
204
+ const inList = countryList.includes(country);
205
+ // contains=true: pass if country IS in list
206
+ // contains=false: pass if country is NOT in list
207
+ return condition.contains ? inList : !inList;
208
+ }
209
+ /**
210
+ * Validate version-based condition
211
+ */
212
+ async function validateVersion(condition, params) {
213
+ var _a, _b;
214
+ const version = (_b = (_a = params.stats) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : null;
215
+ const inList = condition.versions.includes(version);
216
+ return condition.contains ? inList : !inList;
217
+ }
218
+ /**
219
+ * Validate device/platform condition
220
+ */
221
+ async function validateDevice(condition, params) {
222
+ var _a, _b;
223
+ const displays = condition.displays;
224
+ const platform = (_a = params.stats) === null || _a === void 0 ? void 0 : _a.platform;
225
+ const mobile = (_b = params.stats) === null || _b === void 0 ? void 0 : _b.mobile;
226
+ for (const display of displays) {
227
+ if (display === 'android' && (platform === null || platform === void 0 ? void 0 : platform.android)) {
228
+ return condition.contains;
229
+ }
230
+ if (display === 'ios' && (platform === null || platform === void 0 ? void 0 : platform.ios)) {
231
+ return condition.contains;
232
+ }
233
+ if (display === 'win' && (platform === null || platform === void 0 ? void 0 : platform.win)) {
234
+ return condition.contains;
235
+ }
236
+ if (display === 'mac' && (platform === null || platform === void 0 ? void 0 : platform.mac)) {
237
+ return condition.contains;
238
+ }
239
+ if (display === 'desktop' && !mobile) {
240
+ return condition.contains;
241
+ }
242
+ if (display === 'mobile' && mobile) {
243
+ return condition.contains;
244
+ }
245
+ }
246
+ return !condition.contains;
247
+ }
248
+ /**
249
+ * Validate nested condition reference
250
+ */
251
+ async function validateNestedCondition(condition, params) {
252
+ const newParams = Object.assign({}, params);
253
+ newParams.conditionId = condition.conditionId;
254
+ // Prevent infinite recursion
255
+ newParams.conditionStack = [...(newParams.conditionStack || [])];
256
+ if (newParams.conditionStack.includes(condition.conditionId)) {
257
+ return true;
258
+ }
259
+ const result = await validateCondition(newParams);
260
+ return condition.passes ? result : !result;
261
+ }
262
+ /**
263
+ * Validate user-based condition
264
+ */
265
+ async function validateUser(condition, params) {
266
+ var _a, _b, _c, _d, _e, _f, _g;
267
+ const userType = condition.userType;
268
+ switch (userType) {
269
+ case 'valid':
270
+ // User is logged in
271
+ return (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.valid) !== null && _b !== void 0 ? _b : false;
272
+ case 'invalid':
273
+ // 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);
275
+ case 'owner':
276
+ // 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);
281
+ case 'admin':
282
+ // 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]);
288
+ case 'group':
289
+ // User is member of specific group
290
+ // 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);
294
+ default:
295
+ return true;
296
+ }
297
+ }
298
+ /**
299
+ * Validate product-based condition
300
+ */
301
+ async function validateProduct(condition, params) {
302
+ var _a;
303
+ const productId = (_a = params.product) === null || _a === void 0 ? void 0 : _a.id;
304
+ // No product ID available
305
+ if (!productId) {
306
+ return !condition.contains;
307
+ }
308
+ const inList = condition.productIds.includes(productId);
309
+ return condition.contains ? inList : !inList;
310
+ }
311
+ /**
312
+ * Validate tag-based condition
313
+ */
314
+ async function validateTag(condition, params) {
315
+ var _a, _b;
316
+ const productId = (_a = params.product) === null || _a === void 0 ? void 0 : _a.id;
317
+ // No product
318
+ if (!productId) {
319
+ return !condition.contains;
320
+ }
321
+ // No tags on product
322
+ if (!((_b = params.product) === null || _b === void 0 ? void 0 : _b.tags)) {
323
+ return !condition.contains;
324
+ }
325
+ // Check if any condition tag exists on product
326
+ for (const tag of condition.tags) {
327
+ if (params.product.tags[tag]) {
328
+ return condition.contains;
329
+ }
330
+ }
331
+ return !condition.contains;
332
+ }
333
+ /**
334
+ * Validate date-based condition
335
+ */
336
+ async function validateDate(condition, params) {
337
+ const now = Date.now();
338
+ switch (condition.dateTest) {
339
+ case 'before':
340
+ if (!condition.beforeDate)
341
+ return false;
342
+ return now < Date.parse(condition.beforeDate);
343
+ case 'after':
344
+ if (!condition.afterDate)
345
+ return false;
346
+ return now > Date.parse(condition.afterDate);
347
+ case 'between':
348
+ if (!condition.rangeDate || condition.rangeDate.length !== 2)
349
+ return false;
350
+ const start = Date.parse(condition.rangeDate[0]);
351
+ const end = Date.parse(condition.rangeDate[1]);
352
+ return now > start && now < end;
353
+ default:
354
+ return false;
355
+ }
356
+ }
357
+ /**
358
+ * Validate geofence-based condition
359
+ */
360
+ async function validateGeofence(condition, params) {
361
+ var _a;
362
+ let lat;
363
+ let lng;
364
+ // Try to get location from params first
365
+ if ((_a = params.user) === null || _a === void 0 ? void 0 : _a.location) {
366
+ lat = params.user.location.latitude;
367
+ lng = params.user.location.longitude;
368
+ }
369
+ // If not available and getLocation function provided, fetch it
370
+ if ((lat === undefined || lng === undefined) && params.getLocation) {
371
+ try {
372
+ const location = await params.getLocation();
373
+ lat = location.latitude;
374
+ lng = location.longitude;
375
+ }
376
+ catch (error) {
377
+ return false;
378
+ }
379
+ }
380
+ if (lat === undefined || lng === undefined) {
381
+ return false;
382
+ }
383
+ // Check if outside bounding box
384
+ const outside = lat > condition.top ||
385
+ lat < condition.bottom ||
386
+ lng < condition.left ||
387
+ lng > condition.right;
388
+ return condition.contains ? !outside : outside;
389
+ }
390
+ /**
391
+ * Validate value comparison condition
392
+ */
393
+ async function validateValue(condition, params) {
394
+ // Navigate to field value using dot notation
395
+ const fieldPath = condition.field.split('.');
396
+ let base = params;
397
+ for (const field of fieldPath) {
398
+ if (base && typeof base === 'object' && field in base && typeof base[field] !== 'undefined') {
399
+ base = base[field];
400
+ }
401
+ else {
402
+ return false;
403
+ }
404
+ }
405
+ // Convert value to correct type
406
+ let val = condition.value;
407
+ if (typeof val === 'string' && condition.fieldType === 'boolean') {
408
+ val = val.toLowerCase() === 'true';
409
+ }
410
+ else if (typeof val === 'string' && condition.fieldType === 'integer') {
411
+ val = parseInt(val, 10);
412
+ }
413
+ // Perform comparison
414
+ switch (condition.validationType) {
415
+ case 'equal':
416
+ return base == val;
417
+ case 'not':
418
+ return base != val;
419
+ case 'greater':
420
+ return base > val;
421
+ case 'less':
422
+ return base < val;
423
+ default:
424
+ return false;
425
+ }
426
+ }
427
+ /**
428
+ * Validate item status condition
429
+ */
430
+ async function validateItemStatus(condition, params) {
431
+ switch (condition.statusType) {
432
+ case 'isClaimable':
433
+ return !!(params.proof && params.proof.claimable);
434
+ case 'notClaimable':
435
+ return !!(params.proof && !params.proof.claimable);
436
+ case 'noProof':
437
+ return !params.proof;
438
+ case 'hasProof':
439
+ return !!params.proof;
440
+ case 'isVirtual':
441
+ return !!(params.proof && params.proof.virtual);
442
+ case 'notVirtual':
443
+ return !!(params.proof && !params.proof.virtual);
444
+ default:
445
+ return false;
446
+ }
447
+ }
448
+ /**
449
+ * Clear the condition cache
450
+ */
451
+ export function clearConditionCache() {
452
+ Object.keys(conditionCache).forEach(key => delete conditionCache[key]);
453
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utility functions and helpers for working with smartlinks data.
3
+ * @module utils
4
+ */
5
+ export * from './paths';
6
+ export * from './conditions';
@@ -0,0 +1,7 @@
1
+ // src/utils/index.ts
2
+ /**
3
+ * Utility functions and helpers for working with smartlinks data.
4
+ * @module utils
5
+ */
6
+ export * from './paths';
7
+ export * from './conditions';
@@ -0,0 +1,82 @@
1
+ import type { Product } from '../types/product';
2
+ import type { Collection } from '../types/collection';
3
+ import type { BatchResponse } from '../types/batch';
4
+ import type { Proof } from '../types/proof';
5
+ /**
6
+ * Parameters for building a portal path.
7
+ * Pass in objects where available - the function will extract the needed properties.
8
+ */
9
+ export interface PortalPathParams {
10
+ /** Collection object (required) - provides shortId and optional portalUrl */
11
+ collection: Collection | {
12
+ shortId: string;
13
+ portalUrl?: string;
14
+ };
15
+ /** Full product object (extracts id, gtin, and ownGtin from the product) */
16
+ product?: Product;
17
+ /** Just a product ID (if you don't have the full product) */
18
+ productId?: string;
19
+ /** Batch object (extracts id and expiryDate) */
20
+ batch?: BatchResponse;
21
+ /** Just a batch ID string (if you don't have the full batch object) */
22
+ batchId?: string;
23
+ /** Variant object OR just a variant ID string */
24
+ variant?: {
25
+ id: string;
26
+ } | string;
27
+ /** Proof object OR just a proof ID string */
28
+ proof?: Proof | string;
29
+ /** Additional query parameters */
30
+ queryParams?: Record<string, string>;
31
+ }
32
+ /**
33
+ * Builds a portal path/URL based on the provided parameters.
34
+ *
35
+ * Pass in objects where available (collection, product, batch, etc.) and the function
36
+ * will extract the needed properties. You can also pass just IDs if you don't have the full objects.
37
+ *
38
+ * Supports multiple path formats:
39
+ * - Basic product: `/c/{shortId}/{productId}`
40
+ * - With proof: `/c/{shortId}/{productId}/{proofId}`
41
+ * - GTIN (own): `/01/{gtin}` - ownGtin is read from the product object
42
+ * - GTIN (not own): `/gc/{shortId}/01/{gtin}`
43
+ * - With batch: adds `/10/{batchId}` and optionally `?17={expiryDate}`
44
+ * - With variant: adds `/22/{variantId}`
45
+ *
46
+ * @param params - Path parameters
47
+ * @returns The built portal path or URL
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // Basic product path (pass objects)
52
+ * buildPortalPath({
53
+ * collection: myCollection,
54
+ * product: myProduct // reads ownGtin from product if present
55
+ * })
56
+ * // Returns: https://portal.smartlinks.io/c/abc123/prod1
57
+ *
58
+ * // GTIN path (ownGtin read from product)
59
+ * buildPortalPath({
60
+ * collection: myCollection,
61
+ * product: myProduct // if product.ownGtin is true, uses /01/ path
62
+ * })
63
+ * // Returns: https://portal.smartlinks.io/01/1234567890123
64
+ *
65
+ * // With batch object (includes expiry date)
66
+ * buildPortalPath({
67
+ * collection: myCollection,
68
+ * product: myProduct,
69
+ * batch: myBatch // extracts id and expiryDate
70
+ * })
71
+ * // Returns: /01/1234567890123/10/batch1?17=260630
72
+ *
73
+ * // Or just pass IDs
74
+ * buildPortalPath({
75
+ * collection: { shortId: 'abc123' },
76
+ * productId: 'prod1',
77
+ * batchId: 'batch1' // just the ID, no expiry
78
+ * })
79
+ * // Returns: /c/abc123/prod1
80
+ * ```
81
+ */
82
+ export declare function buildPortalPath(params: PortalPathParams): string;