@proveanything/smartlinks 1.15.8 → 1.15.11

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,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,32 @@ 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
+ * `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. `isRescan` is
131
+ * authentic but a duplicate/replayed tap (`status === 'rescan'`) — e.g.
132
+ * a page refresh or the back button — for suppressing "first scan"
133
+ * celebration UX without treating the tag as fake.
112
134
  */
113
135
  export interface ItemStatusCondition extends BaseCondition {
114
136
  type: 'itemStatus';
115
- statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual';
137
+ statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual' | 'isAuthentic' | 'notAuthentic' | 'invalidProof' | 'isRescan';
116
138
  }
117
139
  /**
118
140
  * Facet-based condition — gates on the facet values assigned to the current product.
@@ -263,6 +285,8 @@ export interface ConditionParams {
263
285
  proof?: ProofInfo;
264
286
  /** Collection information */
265
287
  collection?: CollectionInfo;
288
+ /** Authenticity context for the item (proof) the URL points at, if any */
289
+ itemContext?: ItemContext;
266
290
  /** Statistics/tracking information */
267
291
  stats?: StatsInfo;
268
292
  /** Function to fetch conditions by ID (optional) */
@@ -300,7 +324,9 @@ export interface ConditionDebugOptions {
300
324
  * - **date** - Time-based conditions (before, after, between dates)
301
325
  * - **geofence** - Location-based restrictions
302
326
  * - **value** - Custom field comparisons
303
- * - **itemStatus** - Proof/item status checks (claimable, virtual, etc.)
327
+ * - **itemStatus** - Proof/item status checks: claimable, virtual, presence
328
+ * (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
329
+ * `invalidProof`/`isRescan`)
304
330
  * - **condition** - Nested condition references
305
331
  *
306
332
  * Conditions can be combined with AND or OR logic.
@@ -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 (claimable, virtual, etc.)
176
+ * - **itemStatus** - Proof/item status checks: claimable, virtual, presence
177
+ * (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
178
+ * `invalidProof`/`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;
820
823
  switch (condition.statusType) {
821
824
  case 'isClaimable':
822
825
  return {
@@ -848,6 +851,30 @@ 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 'isRescan':
873
+ return {
874
+ passed: ((_g = params.itemContext) === null || _g === void 0 ? void 0 : _g.status) === 'rescan' || !!((_h = params.itemContext) === null || _h === void 0 ? void 0 : _h.isRescan),
875
+ detail: 'Checked that itemContext is authentic but a duplicate/replayed tap (status === rescan).',
876
+ context: { itemContextStatus: (_j = params.itemContext) === null || _j === void 0 ? void 0 : _j.status },
877
+ };
851
878
  default:
852
879
  return {
853
880
  passed: false,
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.8 | Generated: 2026-07-19T09:55:23.623Z
3
+ Version: 1.15.11 | Generated: 2026-07-20T13:46:09.781Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -1419,6 +1419,11 @@ interface AppliedOverridesSummary {
1419
1419
  ```typescript
1420
1420
  interface SystemBlock {
1421
1421
  basePlanId?: string
1422
+ * Stable capability tier microapps should branch on instead of
1423
+ * `basePlanId` — see docs/appConfig.md §4.1. Known tiers are `ProductMode`;
1424
+ * an unrecognised value is a future tier your code doesn't know about yet
1425
+ * — fail closed to the nearest tier you do understand rather than erroring.
1426
+ productMode?: ProductMode | (string & {})
1422
1427
  addOnKeys?: string[]
1423
1428
  apps?: string[]
1424
1429
  * Explicit overrides only — an absent key is NOT "off". Resolve with
@@ -1451,6 +1456,8 @@ interface AppConfigSettings {
1451
1456
  }
1452
1457
  ```
1453
1458
 
1459
+ **ProductMode** = `'simple_redirect' | 'rich_redirect' | 'inform' | 'engage'`
1460
+
1454
1461
  ### appManifest
1455
1462
 
1456
1463
  **AppBundle** (interface)
@@ -5878,6 +5885,23 @@ interface InteractionEventContext {
5878
5885
 
5879
5886
  **EffectConfig** = ``
5880
5887
 
5888
+ ### itemContext
5889
+
5890
+ **ItemContext** (interface)
5891
+ ```typescript
5892
+ interface ItemContext {
5893
+ isAuthentic: boolean
5894
+ status: ItemContextStatus
5895
+ source: 'nfc' | 'serial'
5896
+ errorMessage?: string
5897
+ isRescan?: boolean
5898
+ tag?: TagContext
5899
+ checkedAt: number
5900
+ }
5901
+ ```
5902
+
5903
+ **ItemContextStatus** = ``
5904
+
5881
5905
  ### jobs
5882
5906
 
5883
5907
  **Job** (interface)
@@ -6408,6 +6432,24 @@ interface NfcClaimTagRequest {
6408
6432
  }
6409
6433
  ```
6410
6434
 
6435
+ **TagContext** (interface)
6436
+ ```typescript
6437
+ interface TagContext {
6438
+ status: TagStatus
6439
+ tagId?: string
6440
+ claimSetId?: string
6441
+ codeId?: string
6442
+ count?: number
6443
+ previousCount?: number
6444
+ data?: Record<string, unknown>
6445
+ source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none'
6446
+ validatedAt: number
6447
+ errorMessage?: string
6448
+ }
6449
+ ```
6450
+
6451
+ **TagStatus** = `'valid' | 'rescan' | 'invalid' | 'error'`
6452
+
6411
6453
  ### order
6412
6454
 
6413
6455
  **OrderItem** (interface)
@@ -7692,6 +7734,15 @@ interface SmartLinksWidgetProps {
7692
7734
  * accepted for backward compatibility.
7693
7735
  onNavigate?: (request: NavigationRequest | string) => void
7694
7736
  publicPortalUrl?: string
7737
+ * Authenticity context for the specific item (proof) the URL points at,
7738
+ * resolved via an NFC tap or a serial proof URL. `undefined` for
7739
+ * collection- and product-only URLs, where there's no item to verify.
7740
+ * See docs/item-context.md.
7741
+ itemContext?: ItemContext
7742
+ * @deprecated Use `itemContext.tag` instead. Kept for one release for
7743
+ * backward compatibility with scanner-aware apps that read raw NFC/SUN
7744
+ * data directly. See docs/item-context.md.
7745
+ tag?: TagContext
7695
7746
  size?: 'compact' | 'standard' | 'large'
7696
7747
  lang?: string
7697
7748
  translations?: Record<string, string>
@@ -7861,6 +7912,7 @@ interface ConditionParams {
7861
7912
  product?: ProductInfo
7862
7913
  proof?: ProofInfo
7863
7914
  collection?: CollectionInfo
7915
+ itemContext?: ItemContext
7864
7916
  stats?: StatsInfo
7865
7917
  fetchCondition?: (collectionId: string, conditionId: string) => Promise<ConditionSet | null>
7866
7918
  getLocation?: () => Promise<{ latitude: number; longitude: number }>
package/docs/appConfig.md CHANGED
@@ -60,9 +60,15 @@ export interface AppEntry {
60
60
  [k: string]: unknown; // per-module inline settings preserved
61
61
  }
62
62
 
63
+ /** Known `productMode` tiers, low → high — see §4.1. Not exhaustive; unrecognised values are future tiers. */
64
+ export type ProductMode = 'simple_redirect' | 'rich_redirect' | 'inform' | 'engage';
65
+
63
66
  /** Resolved entitlements — safe for every client to read. */
64
67
  export interface SystemBlock {
68
+ /** Internal billing identifier — not a capability signal, see §4. */
65
69
  basePlanId?: string;
70
+ /** Stable capability tier — see §4.1. Prefer this over basePlanId. */
71
+ productMode?: ProductMode | (string & {});
66
72
  addOnKeys?: string[];
67
73
  apps?: string[]; // apps unlocked by add-ons
68
74
  features?: Record<string, boolean>; // explicit overrides only — see §4.4 for absent-key resolution
@@ -184,12 +190,46 @@ Written only by `entitlements-reconcile`. Represents the union of:
184
190
  - every add-on line item on their Stripe subscription
185
191
  - `systemPrivate.overrides` (applied last, always wins)
186
192
 
187
- ### 4.1 `basePlanId`
193
+ `basePlanId` is an internal Stripe billing/plan identifier (string,
194
+ sourced from subscription metadata) — a billing concern, not a capability
195
+ signal. Don't branch app behavior on its literal value or its internal
196
+ plan names; that's what `productMode` is for.
197
+
198
+ ### 4.1 `productMode`
199
+
200
+ Stable capability tier the app should branch on instead of `basePlanId`
201
+ (a billing identity that can change independently of capability). Written
202
+ by `entitlements-reconcile` on every sync. Typed as `ProductMode` in the
203
+ SDK (`src/types/appConfiguration.ts`) — a union of the known tiers below,
204
+ plus any other string, since future tiers will show up as new values
205
+ before the SDK type is updated to know their names.
206
+
207
+ Ladder, low → high:
208
+
209
+ | `productMode` | Unlocks (roughly) |
210
+ | ------------------ | --------------------------------------------------------------------------------- |
211
+ | `simple_redirect` | Redirect-only: scanning a tag/QR sends the user straight to a URL — no product/proof page, no catalog UI. |
212
+ | `rich_redirect` | Adds a rendered product/proof page (image, description, spec data) — still no deeper apps. |
213
+ | `inform` | Adds informational surfaces beyond the basic page — documentation, provenance, facts-style apps. |
214
+ | `engage` | Adds engagement/ownership capabilities — claim, CRM, loyalty, interaction tracking. Top of the ladder today. |
215
+
216
+ > These four one-liners are inferred from the tier names and ladder
217
+ > ordering, not copied from an authoritative source — worth a sanity check
218
+ > against the actual capability gating before treating them as precise.
219
+
220
+ ```ts
221
+ const mode = cfg.system?.productMode;
222
+ if (mode === 'simple_redirect' || mode === 'rich_redirect') {
223
+ // redirect-only surface — hide catalog UI
224
+ }
225
+ ```
226
+
227
+ Unknown values (future tiers your code predates) should fail closed to
228
+ the nearest tier you understand, not throw or assume the top tier.
188
229
 
189
- String id of the tier. Sourced from Stripe subscription metadata
190
- (`basePlanId` or legacy `planId`). Examples: `simple_redirect`,
191
- `rich_redirect`, `inform`, `enrich`, `engage`, `hub_inform`,
192
- `hub_enrich`, `hub_engage`.
230
+ Not a feature flag and not a billing key — per-capability gating still
231
+ goes through `features` (§4.4); Stripe price selection still goes
232
+ through `basePlanId` + add-ons.
193
233
 
194
234
  ### 4.2 `addOnKeys[]`
195
235
 
@@ -253,6 +293,7 @@ about:
253
293
  | Flag | Gates |
254
294
  | -------------- | ---------------------------------------------------------------------- |
255
295
  | `analytics` | Analytics surfaces (dashboards, reporting, event exploration). |
296
+ | `authenticity` | Live authenticity tracking — flags genuine vs. counterfeit items and enables auditing/tracing on them (e.g. impossible-travel detection). |
256
297
  | `crm` | Customer interaction / CRM — contact records, broadcasts, segments. Sub-features hang off the `crm.*` dotted prefix, e.g. `crm.broadcasts`, `crm.segments`. |
257
298
  | `hub` | The Hub module. |
258
299
  | `portal` | QR code scan portals. |
@@ -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,183 @@
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
+ - `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 a fresh tap and a rescan
167
+
168
+ ## Deprecated
169
+
170
+ The old top-level `tag` prop (`TagContext`) is still forwarded for one
171
+ release for backward compatibility with scanner-aware apps that read raw
172
+ SUN data. New code should read `itemContext.tag` and treat `itemContext`
173
+ as the source of truth.
174
+
175
+ ## Note on terminology
176
+
177
+ Today the SDK still uses `proof` for a specific item record (a serialised
178
+ instance of a product). "Item record" is under consideration as a clearer
179
+ name — no API changes are planned imminently, but a future SDK version may
180
+ support both `proof`- and `item record`-shaped names for the same
181
+ functions/types side by side. `itemContext` is deliberately future-proof:
182
+ it describes context about whatever the URL points at, regardless of what
183
+ 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: `'isClaimable'`, `'notClaimable'`, `'noProof'`, `'hasProof'`, `'isVirtual'`, `'notVirtual'`
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'`, `'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 `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
package/docs/widgets.md CHANGED
@@ -159,6 +159,13 @@ interface SmartLinksWidgetProps {
159
159
  // Base URL to the full public portal for deep linking
160
160
  publicPortalUrl?: string;
161
161
 
162
+ // Authenticity context for the specific item (proof) the URL points at.
163
+ // undefined for collection- and product-only URLs.
164
+ itemContext?: ItemContext;
165
+
166
+ // @deprecated Use itemContext.tag instead — kept for one release
167
+ tag?: TagContext;
168
+
162
169
  // Size hint for responsive rendering
163
170
  size?: 'compact' | 'standard' | 'large';
164
171
 
@@ -180,6 +187,8 @@ interface SmartLinksWidgetProps {
180
187
  | `SL` | `typeof SL` | Pre-initialized SmartLinks SDK |
181
188
  | `onNavigate` | `function?` | Callback to navigate within parent app (accepts `NavigationRequest` or legacy string) |
182
189
  | `publicPortalUrl` | `string?` | Base URL to full portal for deep links |
190
+ | `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) |
191
+ | `tag` | `TagContext?` | **Deprecated** — use `itemContext.tag` instead. Kept for one release. |
183
192
  | `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
184
193
  | `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
185
194
  | `translations` | `object?` | Translation overrides |