@proveanything/smartlinks 1.15.6 → 1.15.10
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/docs/API_SUMMARY.md +46 -4
- package/dist/docs/appConfig.md +33 -54
- package/dist/docs/containers.md +2 -0
- package/dist/docs/item-context.md +184 -0
- package/dist/docs/utils.md +35 -2
- package/dist/docs/widgets.md +9 -0
- package/dist/openapi.yaml +62 -8
- package/dist/types/appConfiguration.d.ts +0 -1
- package/dist/types/collection.d.ts +0 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/itemContext.d.ts +30 -0
- package/dist/types/itemContext.js +2 -0
- package/dist/types/nfc.d.ts +38 -0
- package/dist/types/widgets.d.ts +15 -0
- package/dist/utils/conditions.d.ts +33 -3
- package/dist/utils/conditions.js +34 -1
- package/docs/API_SUMMARY.md +46 -4
- package/docs/appConfig.md +33 -54
- package/docs/containers.md +2 -0
- package/docs/item-context.md +184 -0
- package/docs/utils.md +35 -2
- package/docs/widgets.md +9 -0
- package/openapi.yaml +62 -8
- package/package.json +1 -1
package/dist/types/nfc.d.ts
CHANGED
|
@@ -39,3 +39,41 @@ export interface NfcClaimTagRequest {
|
|
|
39
39
|
codeId: string;
|
|
40
40
|
data: Record<string, any>;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Outcome of validating the NFC/QR tag that brought the visitor to a
|
|
44
|
+
* SmartLinks app.
|
|
45
|
+
*
|
|
46
|
+
* - `valid` — fresh, genuine tap. SUN counter advanced (or a non-consuming
|
|
47
|
+
* lookup succeeded).
|
|
48
|
+
* - `rescan` — genuine tag, but this exact tap was already seen (refresh,
|
|
49
|
+
* back button, or a non-advancing SUN counter).
|
|
50
|
+
* - `invalid` — validation rejected the payload outright (bad crypto,
|
|
51
|
+
* unknown tag, tampered URL) — i.e. a fake/counterfeit tag or serial.
|
|
52
|
+
* - `error` — network/transport failure; authenticity could not be
|
|
53
|
+
* confirmed either way.
|
|
54
|
+
*/
|
|
55
|
+
export type TagStatus = 'valid' | 'rescan' | 'invalid' | 'error';
|
|
56
|
+
/**
|
|
57
|
+
* Result of resolving the tag identifiers on the entry URL, handed to
|
|
58
|
+
* sub-apps so they can render status-appropriate UX instead of a generic
|
|
59
|
+
* error. See docs/tag-context.md for the full contract.
|
|
60
|
+
*/
|
|
61
|
+
export interface TagContext {
|
|
62
|
+
/** Always present when a `TagContext` is delivered at all. */
|
|
63
|
+
status: TagStatus;
|
|
64
|
+
/** Identifiers, when known. */
|
|
65
|
+
tagId?: string;
|
|
66
|
+
claimSetId?: string;
|
|
67
|
+
codeId?: string;
|
|
68
|
+
/** SUN counter values, present when validation returned them. */
|
|
69
|
+
count?: number;
|
|
70
|
+
previousCount?: number;
|
|
71
|
+
/** Arbitrary tag-level payload returned by validation/lookup. */
|
|
72
|
+
data?: Record<string, unknown>;
|
|
73
|
+
/** How the identifiers were resolved. */
|
|
74
|
+
source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none';
|
|
75
|
+
/** Epoch ms at resolution time. */
|
|
76
|
+
validatedAt: number;
|
|
77
|
+
/** Human-readable message for `invalid` / `error`. */
|
|
78
|
+
errorMessage?: string;
|
|
79
|
+
}
|
package/dist/types/widgets.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TagContext } from './nfc';
|
|
2
|
+
import type { ItemContext } from './itemContext';
|
|
1
3
|
/**
|
|
2
4
|
* Structured navigation request emitted via the `onNavigate` prop when a
|
|
3
5
|
* widget or container needs to navigate the parent platform shell to another
|
|
@@ -62,6 +64,19 @@ export interface SmartLinksWidgetProps {
|
|
|
62
64
|
onNavigate?: (request: NavigationRequest | string) => void;
|
|
63
65
|
/** Base URL of the full public portal, used for constructing deep links */
|
|
64
66
|
publicPortalUrl?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Authenticity context for the specific item (proof) the URL points at,
|
|
69
|
+
* resolved via an NFC tap or a serial proof URL. `undefined` for
|
|
70
|
+
* collection- and product-only URLs, where there's no item to verify.
|
|
71
|
+
* See docs/item-context.md.
|
|
72
|
+
*/
|
|
73
|
+
itemContext?: ItemContext;
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated Use `itemContext.tag` instead. Kept for one release for
|
|
76
|
+
* backward compatibility with scanner-aware apps that read raw NFC/SUN
|
|
77
|
+
* data directly. See docs/item-context.md.
|
|
78
|
+
*/
|
|
79
|
+
tag?: TagContext;
|
|
65
80
|
/** Responsive size hint */
|
|
66
81
|
size?: 'compact' | 'standard' | 'large';
|
|
67
82
|
/** BCP-47 language code (e.g. `'en'`, `'fr'`) */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ItemContext } from '../types/itemContext';
|
|
1
2
|
/**
|
|
2
3
|
* Geographic region definitions for country-based conditions
|
|
3
4
|
*/
|
|
@@ -108,11 +109,36 @@ export interface ValueCondition extends BaseCondition {
|
|
|
108
109
|
value: string | number | boolean;
|
|
109
110
|
}
|
|
110
111
|
/**
|
|
111
|
-
* Item status condition
|
|
112
|
+
* Item status condition — one condition type for everything conditions
|
|
113
|
+
* need to know about "the item," so sub-apps don't have to juggle a
|
|
114
|
+
* separate authenticity condition alongside this one.
|
|
115
|
+
*
|
|
116
|
+
* Two families of `statusType`, kept in one enum on purpose:
|
|
117
|
+
* - **Claim/ownership** (unchanged): `isClaimable`, `notClaimable`,
|
|
118
|
+
* `isVirtual`, `notVirtual`, `hasProof`, `noProof`. These read
|
|
119
|
+
* `params.proof` — whether a resolved proof record was supplied at all,
|
|
120
|
+
* and its claim/virtual flags. `noProof` only means "nothing was even
|
|
121
|
+
* attempted"; it does NOT distinguish that from "an identifier was
|
|
122
|
+
* passed but didn't resolve" — see `invalidProof` below for that case.
|
|
123
|
+
* - **Authenticity** (reads `params.itemContext` — see `ItemContext`,
|
|
124
|
+
* docs/item-context.md): `isAuthentic`, `notAuthentic`, `invalidProof`,
|
|
125
|
+
* `isFirstScan`, `isRescan`.
|
|
126
|
+
* - `invalidProof` is specifically "an identifier was passed and resolution
|
|
127
|
+
* was attempted, but it came back invalid or not-found" — the "someone
|
|
128
|
+
* scanned a fake tag / typed a bad serial" case, as opposed to `noProof`
|
|
129
|
+
* ("nothing was on the URL to check at all").
|
|
130
|
+
* - `isAuthentic` is true for both a fresh tap and a rescan — use
|
|
131
|
+
* `isFirstScan` / `isRescan` when the fresh-vs-duplicate distinction
|
|
132
|
+
* matters. `isFirstScan` is the common "this is good, show the full
|
|
133
|
+
* experience" check (authentic AND not seen before, `status === 'valid'`).
|
|
134
|
+
* `isRescan` is authentic but a duplicate/replayed tap
|
|
135
|
+
* (`status === 'rescan'`) — e.g. a page refresh or the back button —
|
|
136
|
+
* for suppressing "first scan" celebration UX without treating the tag
|
|
137
|
+
* as fake.
|
|
112
138
|
*/
|
|
113
139
|
export interface ItemStatusCondition extends BaseCondition {
|
|
114
140
|
type: 'itemStatus';
|
|
115
|
-
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual';
|
|
141
|
+
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual' | 'isAuthentic' | 'notAuthentic' | 'invalidProof' | 'isFirstScan' | 'isRescan';
|
|
116
142
|
}
|
|
117
143
|
/**
|
|
118
144
|
* Facet-based condition — gates on the facet values assigned to the current product.
|
|
@@ -263,6 +289,8 @@ export interface ConditionParams {
|
|
|
263
289
|
proof?: ProofInfo;
|
|
264
290
|
/** Collection information */
|
|
265
291
|
collection?: CollectionInfo;
|
|
292
|
+
/** Authenticity context for the item (proof) the URL points at, if any */
|
|
293
|
+
itemContext?: ItemContext;
|
|
266
294
|
/** Statistics/tracking information */
|
|
267
295
|
stats?: StatsInfo;
|
|
268
296
|
/** Function to fetch conditions by ID (optional) */
|
|
@@ -300,7 +328,9 @@ export interface ConditionDebugOptions {
|
|
|
300
328
|
* - **date** - Time-based conditions (before, after, between dates)
|
|
301
329
|
* - **geofence** - Location-based restrictions
|
|
302
330
|
* - **value** - Custom field comparisons
|
|
303
|
-
* - **itemStatus** - Proof/item status checks
|
|
331
|
+
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
332
|
+
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
333
|
+
* `invalidProof`/`isFirstScan`/`isRescan`)
|
|
304
334
|
* - **condition** - Nested condition references
|
|
305
335
|
*
|
|
306
336
|
* Conditions can be combined with AND or OR logic.
|
package/dist/utils/conditions.js
CHANGED
|
@@ -173,7 +173,9 @@ async function evaluateConditionEntry(condition, params) {
|
|
|
173
173
|
* - **date** - Time-based conditions (before, after, between dates)
|
|
174
174
|
* - **geofence** - Location-based restrictions
|
|
175
175
|
* - **value** - Custom field comparisons
|
|
176
|
-
* - **itemStatus** - Proof/item status checks
|
|
176
|
+
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
177
|
+
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
178
|
+
* `invalidProof`/`isFirstScan`/`isRescan`)
|
|
177
179
|
* - **condition** - Nested condition references
|
|
178
180
|
*
|
|
179
181
|
* Conditions can be combined with AND or OR logic.
|
|
@@ -817,6 +819,7 @@ async function validateFacet(condition, params) {
|
|
|
817
819
|
* Validate item status condition
|
|
818
820
|
*/
|
|
819
821
|
async function validateItemStatus(condition, params) {
|
|
822
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
820
823
|
switch (condition.statusType) {
|
|
821
824
|
case 'isClaimable':
|
|
822
825
|
return {
|
|
@@ -848,6 +851,36 @@ async function validateItemStatus(condition, params) {
|
|
|
848
851
|
passed: !!(params.proof && !params.proof.virtual),
|
|
849
852
|
detail: 'Checked proof.virtual for falsiness.',
|
|
850
853
|
};
|
|
854
|
+
case 'isAuthentic':
|
|
855
|
+
return {
|
|
856
|
+
passed: !!((_a = params.itemContext) === null || _a === void 0 ? void 0 : _a.isAuthentic),
|
|
857
|
+
detail: 'Checked itemContext.isAuthentic for truthiness.',
|
|
858
|
+
context: { itemContextStatus: (_b = params.itemContext) === null || _b === void 0 ? void 0 : _b.status },
|
|
859
|
+
};
|
|
860
|
+
case 'notAuthentic':
|
|
861
|
+
return {
|
|
862
|
+
passed: !!(params.itemContext && !params.itemContext.isAuthentic),
|
|
863
|
+
detail: 'Checked that an itemContext was resolved and isAuthentic is false.',
|
|
864
|
+
context: { itemContextStatus: (_c = params.itemContext) === null || _c === void 0 ? void 0 : _c.status },
|
|
865
|
+
};
|
|
866
|
+
case 'invalidProof':
|
|
867
|
+
return {
|
|
868
|
+
passed: ((_d = params.itemContext) === null || _d === void 0 ? void 0 : _d.status) === 'invalid' || ((_e = params.itemContext) === null || _e === void 0 ? void 0 : _e.status) === 'not-found',
|
|
869
|
+
detail: 'Checked that resolution was attempted and came back invalid or not-found (as opposed to noProof, where nothing was attempted at all).',
|
|
870
|
+
context: { itemContextStatus: (_f = params.itemContext) === null || _f === void 0 ? void 0 : _f.status },
|
|
871
|
+
};
|
|
872
|
+
case 'isFirstScan':
|
|
873
|
+
return {
|
|
874
|
+
passed: ((_g = params.itemContext) === null || _g === void 0 ? void 0 : _g.status) === 'valid',
|
|
875
|
+
detail: 'Checked that itemContext is authentic and this is the first time it has been seen (status === valid, not rescan).',
|
|
876
|
+
context: { itemContextStatus: (_h = params.itemContext) === null || _h === void 0 ? void 0 : _h.status },
|
|
877
|
+
};
|
|
878
|
+
case 'isRescan':
|
|
879
|
+
return {
|
|
880
|
+
passed: ((_j = params.itemContext) === null || _j === void 0 ? void 0 : _j.status) === 'rescan' || !!((_k = params.itemContext) === null || _k === void 0 ? void 0 : _k.isRescan),
|
|
881
|
+
detail: 'Checked that itemContext is authentic but a duplicate/replayed tap (status === rescan).',
|
|
882
|
+
context: { itemContextStatus: (_l = params.itemContext) === null || _l === void 0 ? void 0 : _l.status },
|
|
883
|
+
};
|
|
851
884
|
default:
|
|
852
885
|
return {
|
|
853
886
|
passed: false,
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.15.
|
|
3
|
+
Version: 1.15.10 | Generated: 2026-07-19T13:35:07.171Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -1447,7 +1447,6 @@ interface AppConfigSettings {
|
|
|
1447
1447
|
addOns?: string[]
|
|
1448
1448
|
requestedBasePlanId?: string
|
|
1449
1449
|
itemRecordMode?: 'registered' | 'owned' | null
|
|
1450
|
-
virtualItemsEnabled?: boolean
|
|
1451
1450
|
system?: SystemBlock
|
|
1452
1451
|
}
|
|
1453
1452
|
```
|
|
@@ -3698,8 +3697,6 @@ interface Collection {
|
|
|
3698
3697
|
secondaryColor?: string
|
|
3699
3698
|
portalUrl?: string // URL for the collection's portal (if applicable)
|
|
3700
3699
|
allowAutoGenerateClaims?: boolean
|
|
3701
|
-
variants: boolean // does this collection support variants?
|
|
3702
|
-
batches: boolean // does this collection support batches?
|
|
3703
3700
|
defaultAuthKitId: string // default auth kit for this collection, used for auth
|
|
3704
3701
|
}
|
|
3705
3702
|
```
|
|
@@ -5881,6 +5878,23 @@ interface InteractionEventContext {
|
|
|
5881
5878
|
|
|
5882
5879
|
**EffectConfig** = ``
|
|
5883
5880
|
|
|
5881
|
+
### itemContext
|
|
5882
|
+
|
|
5883
|
+
**ItemContext** (interface)
|
|
5884
|
+
```typescript
|
|
5885
|
+
interface ItemContext {
|
|
5886
|
+
isAuthentic: boolean
|
|
5887
|
+
status: ItemContextStatus
|
|
5888
|
+
source: 'nfc' | 'serial'
|
|
5889
|
+
errorMessage?: string
|
|
5890
|
+
isRescan?: boolean
|
|
5891
|
+
tag?: TagContext
|
|
5892
|
+
checkedAt: number
|
|
5893
|
+
}
|
|
5894
|
+
```
|
|
5895
|
+
|
|
5896
|
+
**ItemContextStatus** = ``
|
|
5897
|
+
|
|
5884
5898
|
### jobs
|
|
5885
5899
|
|
|
5886
5900
|
**Job** (interface)
|
|
@@ -6411,6 +6425,24 @@ interface NfcClaimTagRequest {
|
|
|
6411
6425
|
}
|
|
6412
6426
|
```
|
|
6413
6427
|
|
|
6428
|
+
**TagContext** (interface)
|
|
6429
|
+
```typescript
|
|
6430
|
+
interface TagContext {
|
|
6431
|
+
status: TagStatus
|
|
6432
|
+
tagId?: string
|
|
6433
|
+
claimSetId?: string
|
|
6434
|
+
codeId?: string
|
|
6435
|
+
count?: number
|
|
6436
|
+
previousCount?: number
|
|
6437
|
+
data?: Record<string, unknown>
|
|
6438
|
+
source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none'
|
|
6439
|
+
validatedAt: number
|
|
6440
|
+
errorMessage?: string
|
|
6441
|
+
}
|
|
6442
|
+
```
|
|
6443
|
+
|
|
6444
|
+
**TagStatus** = `'valid' | 'rescan' | 'invalid' | 'error'`
|
|
6445
|
+
|
|
6414
6446
|
### order
|
|
6415
6447
|
|
|
6416
6448
|
**OrderItem** (interface)
|
|
@@ -7695,6 +7727,15 @@ interface SmartLinksWidgetProps {
|
|
|
7695
7727
|
* accepted for backward compatibility.
|
|
7696
7728
|
onNavigate?: (request: NavigationRequest | string) => void
|
|
7697
7729
|
publicPortalUrl?: string
|
|
7730
|
+
* Authenticity context for the specific item (proof) the URL points at,
|
|
7731
|
+
* resolved via an NFC tap or a serial proof URL. `undefined` for
|
|
7732
|
+
* collection- and product-only URLs, where there's no item to verify.
|
|
7733
|
+
* See docs/item-context.md.
|
|
7734
|
+
itemContext?: ItemContext
|
|
7735
|
+
* @deprecated Use `itemContext.tag` instead. Kept for one release for
|
|
7736
|
+
* backward compatibility with scanner-aware apps that read raw NFC/SUN
|
|
7737
|
+
* data directly. See docs/item-context.md.
|
|
7738
|
+
tag?: TagContext
|
|
7698
7739
|
size?: 'compact' | 'standard' | 'large'
|
|
7699
7740
|
lang?: string
|
|
7700
7741
|
translations?: Record<string, string>
|
|
@@ -7864,6 +7905,7 @@ interface ConditionParams {
|
|
|
7864
7905
|
product?: ProductInfo
|
|
7865
7906
|
proof?: ProofInfo
|
|
7866
7907
|
collection?: CollectionInfo
|
|
7908
|
+
itemContext?: ItemContext
|
|
7867
7909
|
stats?: StatsInfo
|
|
7868
7910
|
fetchCondition?: (collectionId: string, conditionId: string) => Promise<ConditionSet | null>
|
|
7869
7911
|
getLocation?: () => Promise<{ latitude: number; longitude: number }>
|
package/docs/appConfig.md
CHANGED
|
@@ -36,7 +36,6 @@ export interface AppConfigSettings {
|
|
|
36
36
|
|
|
37
37
|
/** How the account handles individual physical items. */
|
|
38
38
|
itemRecordMode?: 'registered' | 'owned' | null;
|
|
39
|
-
virtualItemsEnabled?: boolean;
|
|
40
39
|
|
|
41
40
|
// ── SYSTEM-OWNED, PUBLIC (reconcile writes; client read-only) ─────
|
|
42
41
|
/** Resolved entitlement truth. Read this to gate features. */
|
|
@@ -245,6 +244,25 @@ Always resolve flags through `appConfiguration.isFeatureEnabled(collectionId, fl
|
|
|
245
244
|
if you already have a `system` block from elsewhere — never read
|
|
246
245
|
`cfg.system.features[flag]` directly.
|
|
247
246
|
|
|
247
|
+
#### 4.4.1 Common feature flags
|
|
248
|
+
|
|
249
|
+
`features` is an open map — any microapp can define its own key — but
|
|
250
|
+
these are the major, cross-cutting flags most integrations will care
|
|
251
|
+
about:
|
|
252
|
+
|
|
253
|
+
| Flag | Gates |
|
|
254
|
+
| -------------- | ---------------------------------------------------------------------- |
|
|
255
|
+
| `analytics` | Analytics surfaces (dashboards, reporting, event exploration). |
|
|
256
|
+
| `crm` | Customer interaction / CRM — contact records, broadcasts, segments. Sub-features hang off the `crm.*` dotted prefix, e.g. `crm.broadcasts`, `crm.segments`. |
|
|
257
|
+
| `hub` | The Hub module. |
|
|
258
|
+
| `portal` | QR code scan portals. |
|
|
259
|
+
| `virtualItems` | Virtual items — algorithmic per-item IDs with no persistent row (battery serials, scan-to-collect points, bulk QR sheets). Replaces the old root-level `virtualItemsEnabled` boolean, which has been removed (§8) — resolve this like any other feature flag, not as a separate config key. Independent of `itemRecordMode`. |
|
|
260
|
+
| `batches` | Batch support. Used to be the `Collection.batches` boolean; that field has been removed — this flag is now the only source of truth. |
|
|
261
|
+
| `variants` | Variant support. Used to be the `Collection.variants` boolean; that field has been removed — this flag is now the only source of truth. |
|
|
262
|
+
|
|
263
|
+
Same resolution rule as any flag (§4.4) — don't read these off
|
|
264
|
+
`cfg.system.features` directly, always go through `isFeatureEnabled()`.
|
|
265
|
+
|
|
248
266
|
### 4.5 `meters`
|
|
249
267
|
|
|
250
268
|
Map keyed by add-on key. Apps reporting usage should call the
|
|
@@ -407,18 +425,14 @@ with a stale local copy.
|
|
|
407
425
|
Clients MUST NOT write directly to `system` or `systemPrivate`.
|
|
408
426
|
`entitlements-reconcile` merges user-owned fields through untouched.
|
|
409
427
|
|
|
410
|
-
## 8. Item handling (`itemRecordMode
|
|
428
|
+
## 8. Item handling (`itemRecordMode`)
|
|
411
429
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
them.
|
|
430
|
+
A root-level, user-owned key describing how the account treats
|
|
431
|
+
individual physical items. Reconcile never touches it.
|
|
415
432
|
|
|
416
|
-
|
|
433
|
+
Two axes (do NOT conflate):
|
|
417
434
|
|
|
418
|
-
1. **
|
|
419
|
-
IDs, no per-item row. Battery serials, scan-to-collect points,
|
|
420
|
-
bulk QR sheets. Can be on with `itemRecordMode` off.
|
|
421
|
-
2. **Item records** — `itemRecordMode`. Materialise a persistent row
|
|
435
|
+
1. **Item records** — `itemRecordMode`. Materialise a persistent row
|
|
422
436
|
per physical item. `null` / missing = **disabled** (no separate
|
|
423
437
|
`"none"` enum — reads collapse to `mode === "off"` via the
|
|
424
438
|
`useItemRecordMode()` hook).
|
|
@@ -426,7 +440,15 @@ Three axes (do NOT conflate):
|
|
|
426
440
|
provenance without a customer relationship.
|
|
427
441
|
- `"owned"` — tag row + owner + interaction history. Requires
|
|
428
442
|
`basePlanId >= "engage"`. Enables claim / transfer / CRM.
|
|
429
|
-
|
|
443
|
+
2. **CRM** — downstream consequence of `owned`; not a peer flag.
|
|
444
|
+
|
|
445
|
+
**Virtual items are not part of this key.** Algorithmic IDs with no
|
|
446
|
+
per-item row (battery serials, scan-to-collect points, bulk QR sheets)
|
|
447
|
+
used to be a separate root-level `virtualItemsEnabled: boolean` — that
|
|
448
|
+
field has been removed. Virtual items are now gated by the `virtualItems`
|
|
449
|
+
feature flag under `system.features` (§4.4.1), same as every other
|
|
450
|
+
feature — resolve it with `isFeatureEnabled(collectionId, 'virtualItems')`.
|
|
451
|
+
It's independent of `itemRecordMode` and can be on with item records off.
|
|
430
452
|
|
|
431
453
|
**Billing:** the usage ledger stamps `tier` at event time from
|
|
432
454
|
`itemRecordMode`. Writes with `mode === null` MUST be rejected. Mode
|
|
@@ -447,46 +469,3 @@ CRM visibility), not just a capability flag.
|
|
|
447
469
|
features yet.
|
|
448
470
|
- `systemOverrides` (old root-level key) → `systemPrivate.overrides`.
|
|
449
471
|
One-off backfill moves the block and re-runs reconcile.
|
|
450
|
-
|
|
451
|
-
## 10. Change log
|
|
452
|
-
|
|
453
|
-
- **2026-07-17**: Initial contract published; `appConfig.system` becomes
|
|
454
|
-
canonical entitlements source of truth. `entitlements` group deprecated.
|
|
455
|
-
- **2026-07-17**: Added `systemOverrides` for additive comps; reconcile
|
|
456
|
-
records `system.appliedOverrides`.
|
|
457
|
-
- **2026-07-18**: Renamed `systemOverrides` → `systemPrivate.overrides`
|
|
458
|
-
and formalised the three-tier access model (user / system /
|
|
459
|
-
systemPrivate). API strips `systemPrivate` for non-admin readers.
|
|
460
|
-
Added `system.accountType` derived from
|
|
461
|
-
`systemPrivate.overrides.accountType`. Added top-of-doc TypeScript
|
|
462
|
-
definition.
|
|
463
|
-
- **2026-07-18**: Added SDK helpers `appConfiguration.getAppConfig()`
|
|
464
|
-
(typed + cached), `isFeatureEnabled()` (async, cache-backed feature
|
|
465
|
-
gate), and `isFeatureEnabledSync()` (cache-only, for callers that can't
|
|
466
|
-
await). Renamed the exported TypeScript type to `AppConfigSettings`
|
|
467
|
-
to avoid a name collision with the existing per-module `AppConfig`
|
|
468
|
-
type in `src/types/collection.ts`.
|
|
469
|
-
- **2026-07-18**: `/public/collection/:id/app/config` (`collection.getAppsConfig()`)
|
|
470
|
-
now returns the `appConfig` entitlements data (`system`, `addOns`,
|
|
471
|
-
`requestedBasePlanId`, `itemRecordMode`, `virtualItemsEnabled`) merged
|
|
472
|
-
alongside its existing app-catalog `apps[]`. `appConfiguration.getAppConfig()`
|
|
473
|
-
was repointed at this endpoint (dropping its `admin` option — this endpoint
|
|
474
|
-
is public-only); the settings-group endpoint (`appConfiguration.getConfig({ appId: 'appConfig' })`)
|
|
475
|
-
is still the only way to reach `systemPrivate`. See §3.1 for why the two
|
|
476
|
-
endpoints' `apps[]` fields are shaped differently and shouldn't be conflated.
|
|
477
|
-
- **2026-07-18**: `system.features` changed from "only truthy flags appear"
|
|
478
|
-
to explicit `Record<string, boolean>` overrides. Added the `accountType`
|
|
479
|
-
default rule (§4.4): enterprise accounts default every flag to on unless
|
|
480
|
-
explicitly `false`; standard accounts default off unless explicitly `true`.
|
|
481
|
-
Added `appConfiguration.resolveFeature(system, flag)` — the pure resolver
|
|
482
|
-
behind `isFeatureEnabled()`/`isFeatureEnabledSync()`, exposed only for the
|
|
483
|
-
rare case of already holding a `system` block from somewhere other than
|
|
484
|
-
`getAppConfig()`. There is no separate admin feature-check path — `system`
|
|
485
|
-
is public data on every read, so admin surfaces should call
|
|
486
|
-
`isFeatureEnabled()` like everything else. Reading `cfg.system.features[flag]`
|
|
487
|
-
directly is no longer correct for enterprise accounts — always resolve
|
|
488
|
-
through one of these three.
|
|
489
|
-
- **2026-07-19**: Corrected flag naming convention (§4.4): flags are
|
|
490
|
-
camelCase (e.g. `customDomain`), not snake_case, and may use a dotted
|
|
491
|
-
grouping prefix for a topic/module (e.g. `crm.broadcasts`). Updated all
|
|
492
|
-
examples in this doc accordingly.
|
package/docs/containers.md
CHANGED
|
@@ -231,6 +231,8 @@ All standard widget props apply:
|
|
|
231
231
|
| `productId` | string | ❌ | Product context |
|
|
232
232
|
| `proofId` | string | ❌ | Proof context |
|
|
233
233
|
| `user` | object | ❌ | Current user info |
|
|
234
|
+
| `itemContext` | `ItemContext` | ❌ | Authenticity context for the specific item (proof) the URL points at; `undefined` for collection/product-only URLs — see [item-context.md](item-context.md) |
|
|
235
|
+
| `tag` | `TagContext` | ❌ | **Deprecated** — use `itemContext.tag` instead. Kept for one release. |
|
|
234
236
|
| `onNavigate` | function | ❌ | Navigation callback (accepts `NavigationRequest` or legacy string) |
|
|
235
237
|
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
|
|
236
238
|
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Item Context (container prop)
|
|
2
|
+
|
|
3
|
+
> **Copy this file into `node_modules/@proveanything/smartlinks/docs/item-context.md`** in the published SDK package.
|
|
4
|
+
|
|
5
|
+
When the URL points at a specific item — either a **serial proof URL** or an
|
|
6
|
+
**NFC tap** — the portal derives an `ItemContext` describing what it found
|
|
7
|
+
and hands it to the container as the **`itemContext`** prop.
|
|
8
|
+
|
|
9
|
+
For collection- and product-only URLs there is no item to describe, so
|
|
10
|
+
`itemContext` is `undefined`. Sub-apps that don't care about authenticity
|
|
11
|
+
can simply ignore the prop.
|
|
12
|
+
|
|
13
|
+
The prop is named `itemContext` (not `item`) to make clear it's *context
|
|
14
|
+
about the item*, not the item record itself. The underlying record is
|
|
15
|
+
passed separately via `proof`, `product` and `collection` props.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
export default function MyContainer({ itemContext, collection, product, proof }) {
|
|
19
|
+
if (!itemContext) {
|
|
20
|
+
// Collection or product view — nothing to verify.
|
|
21
|
+
return <BrowseView collection={collection} product={product} />;
|
|
22
|
+
}
|
|
23
|
+
if (itemContext.isAuthentic) {
|
|
24
|
+
return <RealContent proof={proof} />;
|
|
25
|
+
}
|
|
26
|
+
if (itemContext.status === 'not-found' || itemContext.status === 'invalid') {
|
|
27
|
+
return <NotGenuine message={itemContext.errorMessage} />;
|
|
28
|
+
}
|
|
29
|
+
// status === 'error' — transport failure
|
|
30
|
+
return <TransientError message={itemContext.errorMessage} />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Shape
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface ItemContext {
|
|
40
|
+
/** True only for `valid` and `rescan`. Read this for the simple case. */
|
|
41
|
+
isAuthentic: boolean;
|
|
42
|
+
|
|
43
|
+
status:
|
|
44
|
+
| 'valid' // genuine
|
|
45
|
+
| 'rescan' // genuine, but this exact NFC tap has been seen before
|
|
46
|
+
| 'invalid' // SDK rejected the NFC payload (bad crypto)
|
|
47
|
+
| 'not-found' // identifier well-formed but 404
|
|
48
|
+
| 'error'; // transport / network failure — cannot tell
|
|
49
|
+
|
|
50
|
+
source: 'nfc' | 'serial';
|
|
51
|
+
|
|
52
|
+
/** Set for `invalid` / `not-found` / `error`. Safe to display. */
|
|
53
|
+
errorMessage?: string;
|
|
54
|
+
|
|
55
|
+
/** True when SDK reports this exact NFC tap was seen before. */
|
|
56
|
+
isRescan?: boolean;
|
|
57
|
+
|
|
58
|
+
/** Full NFC payload (SUN counter, tagId, claimSetId, codeId, mirror…). Present only when source === 'nfc'. */
|
|
59
|
+
tag?: TagContext;
|
|
60
|
+
|
|
61
|
+
/** Epoch ms when the check was derived. */
|
|
62
|
+
checkedAt: number;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Also exported from `@proveanything/smartlinks` as `ItemContext` /
|
|
67
|
+
`ItemContextStatus` (`src/types/itemContext.ts`). The nested `TagContext`
|
|
68
|
+
shape is unchanged (`src/types/nfc.ts`).
|
|
69
|
+
|
|
70
|
+
## When `itemContext` is present
|
|
71
|
+
|
|
72
|
+
| URL points at | itemContext |
|
|
73
|
+
| ---------------------------------------------- | -------------------- |
|
|
74
|
+
| Collection root | `undefined` |
|
|
75
|
+
| Product page (no proof) | `undefined` |
|
|
76
|
+
| Serial proof URL (any outcome) | present, source `serial` |
|
|
77
|
+
| NFC tap (any outcome) | present, source `nfc` |
|
|
78
|
+
|
|
79
|
+
## Status matrix (when present)
|
|
80
|
+
|
|
81
|
+
| Situation | source | status | isAuthentic |
|
|
82
|
+
| ----------------------------------------------------- | -------- | ------------- | :---------: |
|
|
83
|
+
| Fresh NFC tap, SUN counter validated | `nfc` | `valid` | ✅ |
|
|
84
|
+
| Same NFC tap replayed / refresh | `nfc` | `rescan` | ✅ |
|
|
85
|
+
| NFC payload rejected (bad crypto / bad params) | `nfc` | `invalid` | ❌ |
|
|
86
|
+
| Serial proof URL, proof fetched OK | `serial` | `valid` | ✅ |
|
|
87
|
+
| Serial proof URL, proof returned 404 | `serial` | `not-found` | ❌ |
|
|
88
|
+
| Network / 5xx while resolving proof or NFC | (either) | `error` | ❌ |
|
|
89
|
+
|
|
90
|
+
`isAuthentic === (status === 'valid' || status === 'rescan')`.
|
|
91
|
+
|
|
92
|
+
## How it's delivered
|
|
93
|
+
|
|
94
|
+
**Direct-mounted containers** (default public mode):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
type Props = {
|
|
98
|
+
collection: Collection;
|
|
99
|
+
product?: Product;
|
|
100
|
+
proof?: Proof;
|
|
101
|
+
itemContext?: ItemContext; // ← only when URL targets a specific item
|
|
102
|
+
// …plus the normal navigation / theme / auth props
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
NFC-aware apps that need the raw SUN / tag payload read `itemContext.tag`.
|
|
107
|
+
|
|
108
|
+
**Iframe containers**: the portal serialises `ItemContext` to JSON, base64-
|
|
109
|
+
encodes it (UTF-8 safe) and appends it as an `itemContext` query param on
|
|
110
|
+
the iframe URL — omitted entirely when there is no item context:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
function readItemContext(): ItemContext | undefined {
|
|
114
|
+
const raw = new URLSearchParams(location.search).get('itemContext');
|
|
115
|
+
if (!raw) return undefined;
|
|
116
|
+
try { return JSON.parse(decodeURIComponent(escape(atob(raw)))); }
|
|
117
|
+
catch { return undefined; }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Guarantees
|
|
122
|
+
|
|
123
|
+
- `itemContext` is present on every container mount whose URL targeted an
|
|
124
|
+
item (proof or NFC tap). Absent otherwise.
|
|
125
|
+
- `SL.nfc.validate` is called at most once per `(claimSetId, codeId)` per
|
|
126
|
+
browser session — SUN counters are never double-consumed on refresh.
|
|
127
|
+
- SSR uses `SL.nfc.lookupTag` (non-consuming) only.
|
|
128
|
+
- `isAuthentic === true` implies the SDK either validated an NFC payload
|
|
129
|
+
or returned a full proof document. It is never inferred from URL shape.
|
|
130
|
+
- The portal will **not** render its full-page error surface for a 404 on
|
|
131
|
+
a proof — that surfaces as `status: 'not-found'` on `itemContext`, and
|
|
132
|
+
the container is still mounted so it can present the message its own way.
|
|
133
|
+
|
|
134
|
+
## What containers should do
|
|
135
|
+
|
|
136
|
+
| Status | Recommended UI |
|
|
137
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
138
|
+
| `valid` | Full experience. Show ownership, actions, etc. |
|
|
139
|
+
| `rescan` | Same as `valid`, optionally with a subtle "seen before" hint. |
|
|
140
|
+
| `invalid` | Clear "this doesn't look genuine" message. Do NOT reveal data. |
|
|
141
|
+
| `not-found` | "We don't recognise this item." Offer a link home. |
|
|
142
|
+
| `error` | Neutral "couldn't verify right now." Offer retry. |
|
|
143
|
+
| _absent_ | Standard browse UI. Don't mention authenticity at all. |
|
|
144
|
+
|
|
145
|
+
## Gating UX with conditions
|
|
146
|
+
|
|
147
|
+
Sub-apps should standardize on passing `itemContext` into `validateCondition()`
|
|
148
|
+
and gating with `itemStatus` — the same condition type already used for
|
|
149
|
+
claim/virtual checks, extended with authenticity `statusType`s that read
|
|
150
|
+
`itemContext` instead of `proof`:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { validateCondition } from '@proveanything/smartlinks';
|
|
154
|
+
|
|
155
|
+
const showFakeWarning = await validateCondition({
|
|
156
|
+
condition: {
|
|
157
|
+
conditions: [{ type: 'itemStatus', statusType: 'invalidProof' }],
|
|
158
|
+
},
|
|
159
|
+
itemContext, // the prop delivered to the container
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- `isAuthentic` — passes when `itemContext.isAuthentic` is true (`valid`/`rescan`)
|
|
164
|
+
- `notAuthentic` — passes when an `itemContext` was resolved but isn't authentic (`invalid`/`not-found`/`error`)
|
|
165
|
+
- `invalidProof` — passes specifically when resolution was attempted and came back `invalid` or `not-found` — the "someone scanned a fake tag / typed a bad serial" case, as distinct from `noProof` ("nothing was on the URL to check at all")
|
|
166
|
+
- `isFirstScan` — the single "this is good, show the full experience" check: authentic AND this is the first time it's been seen (`status === 'valid'`)
|
|
167
|
+
- `isRescan` — authentic but a duplicate/replayed tap (`status === 'rescan'`) — use this to suppress "first scan" celebration UX without treating the tag as fake; `isAuthentic` alone is true for both `isFirstScan` and `isRescan` cases
|
|
168
|
+
|
|
169
|
+
## Deprecated
|
|
170
|
+
|
|
171
|
+
The old top-level `tag` prop (`TagContext`) is still forwarded for one
|
|
172
|
+
release for backward compatibility with scanner-aware apps that read raw
|
|
173
|
+
SUN data. New code should read `itemContext.tag` and treat `itemContext`
|
|
174
|
+
as the source of truth.
|
|
175
|
+
|
|
176
|
+
## Note on terminology
|
|
177
|
+
|
|
178
|
+
Today the SDK still uses `proof` for a specific item record (a serialised
|
|
179
|
+
instance of a product). "Item record" is under consideration as a clearer
|
|
180
|
+
name — no API changes are planned imminently, but a future SDK version may
|
|
181
|
+
support both `proof`- and `item record`-shaped names for the same
|
|
182
|
+
functions/types side by side. `itemContext` is deliberately future-proof:
|
|
183
|
+
it describes context about whatever the URL points at, regardless of what
|
|
184
|
+
we ultimately call the record itself.
|
package/docs/utils.md
CHANGED
|
@@ -485,9 +485,12 @@ await utils.validateCondition({
|
|
|
485
485
|
```
|
|
486
486
|
|
|
487
487
|
#### Item Status Conditions
|
|
488
|
-
Check proof/item status
|
|
488
|
+
Check proof/item status — claim/ownership state, and authenticity of the
|
|
489
|
+
item (proof) the URL points at (via NFC tap or serial proof URL — see
|
|
490
|
+
[item-context.md](item-context.md)):
|
|
489
491
|
|
|
490
492
|
```typescript
|
|
493
|
+
// Claim/ownership — unchanged, reads `proof`
|
|
491
494
|
await utils.validateCondition({
|
|
492
495
|
condition: {
|
|
493
496
|
type: 'and',
|
|
@@ -498,9 +501,38 @@ await utils.validateCondition({
|
|
|
498
501
|
},
|
|
499
502
|
proof: { claimable: true }
|
|
500
503
|
})
|
|
504
|
+
|
|
505
|
+
// Authenticity — reads `itemContext`
|
|
506
|
+
await utils.validateCondition({
|
|
507
|
+
condition: {
|
|
508
|
+
type: 'and',
|
|
509
|
+
conditions: [{
|
|
510
|
+
type: 'itemStatus',
|
|
511
|
+
statusType: 'invalidProof'
|
|
512
|
+
}]
|
|
513
|
+
},
|
|
514
|
+
itemContext: { isAuthentic: false, status: 'invalid', source: 'nfc', checkedAt: Date.now() }
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// Fresh scan vs. duplicate/replayed scan — both authentic, different UX
|
|
518
|
+
await utils.validateCondition({
|
|
519
|
+
condition: {
|
|
520
|
+
type: 'and',
|
|
521
|
+
conditions: [{
|
|
522
|
+
type: 'itemStatus',
|
|
523
|
+
statusType: 'isRescan'
|
|
524
|
+
}]
|
|
525
|
+
},
|
|
526
|
+
itemContext: { isAuthentic: true, status: 'rescan', source: 'nfc', checkedAt: Date.now() }
|
|
527
|
+
})
|
|
501
528
|
```
|
|
502
529
|
|
|
503
|
-
Status types:
|
|
530
|
+
Status types:
|
|
531
|
+
- Claim/ownership (reads `proof`): `'isClaimable'`, `'notClaimable'`, `'isVirtual'`, `'notVirtual'`
|
|
532
|
+
- Presence (reads `proof`): `'hasProof'`, `'noProof'` — `noProof` only means *nothing was attempted*, not that an attempt failed
|
|
533
|
+
- Authenticity (reads `itemContext`): `'isAuthentic'`, `'notAuthentic'`, `'invalidProof'`, `'isFirstScan'`, `'isRescan'`
|
|
534
|
+
- `invalidProof` is specifically "an identifier was passed and resolution came back `invalid`/`not-found`", distinct from `noProof`'s "nothing on the URL to check at all"
|
|
535
|
+
- `isAuthentic` is true for both a fresh tap and a rescan. Use `isFirstScan` (authentic AND `status === 'valid'`) as the single "this is good, show the full experience" check, and `isRescan` (authentic but `status === 'rescan'`) to suppress "first scan" celebration UX without treating the tag as fake
|
|
504
536
|
|
|
505
537
|
#### Version Conditions
|
|
506
538
|
For A/B testing or versioned content:
|
|
@@ -720,6 +752,7 @@ See [examples/utils-demo.ts](../examples/utils-demo.ts) for comprehensive exampl
|
|
|
720
752
|
- **[API Summary](API_SUMMARY.md)** - Complete SDK reference with all namespaces and functions
|
|
721
753
|
- **[QR Codes](API_SUMMARY.md#qr)** - QR code lookup functions that work with generated paths
|
|
722
754
|
- **[NFC](API_SUMMARY.md#nfc)** - NFC tag claiming and validation
|
|
755
|
+
- **[Item Context](item-context.md)** - `itemContext` prop delivered to containers describing the item (proof) the URL points at and whether it's authentic
|
|
723
756
|
- **[Collections](API_SUMMARY.md#collection)** - Collection management functions
|
|
724
757
|
- **[Products](API_SUMMARY.md#product)** - Product CRUD operations
|
|
725
758
|
- **[Batches](API_SUMMARY.md#batch)** - Batch management
|