@proveanything/smartlinks 1.15.10 → 1.15.13
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/api/authKit.d.ts +58 -3
- package/dist/api/authKit.js +116 -4
- package/dist/docs/API_SUMMARY.md +125 -9
- package/dist/docs/analytics-metadata-conventions.md +4 -0
- package/dist/docs/analytics.md +37 -3
- package/dist/docs/appConfig.md +46 -5
- package/dist/docs/auth-kit.md +110 -0
- package/dist/docs/item-context.md +1 -2
- package/dist/docs/utils.md +2 -2
- package/dist/openapi.yaml +415 -1
- package/dist/types/analytics.d.ts +37 -2
- package/dist/types/appConfiguration.d.ts +16 -0
- package/dist/types/authKit.d.ts +90 -0
- package/dist/utils/conditions.d.ts +7 -11
- package/dist/utils/conditions.js +4 -10
- package/docs/API_SUMMARY.md +125 -9
- package/docs/analytics-metadata-conventions.md +4 -0
- package/docs/analytics.md +37 -3
- package/docs/appConfig.md +46 -5
- package/docs/auth-kit.md +110 -0
- package/docs/item-context.md +1 -2
- package/docs/utils.md +2 -2
- package/openapi.yaml +415 -1
- package/package.json +1 -1
|
@@ -69,9 +69,25 @@ export interface AppliedOverridesSummary {
|
|
|
69
69
|
accountType?: boolean;
|
|
70
70
|
note?: string;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Capability ladder for `system.productMode`, low → high. See
|
|
74
|
+
* docs/appConfig.md §4.1 for what each tier unlocks. Not exhaustive going
|
|
75
|
+
* forward — `SystemBlock.productMode` accepts this union for autocomplete
|
|
76
|
+
* but also any other string, since future tiers will show up as new values
|
|
77
|
+
* before this type is updated to know about them.
|
|
78
|
+
*/
|
|
79
|
+
export type ProductMode = 'simple_redirect' | 'rich_redirect' | 'inform' | 'engage';
|
|
72
80
|
/** Resolved entitlements — written only by `entitlements-reconcile`, safe for every client to read. */
|
|
73
81
|
export interface SystemBlock {
|
|
82
|
+
/** Internal billing/plan identifier — see docs/appConfig.md §4.1. Not a capability signal; use `productMode` for that. */
|
|
74
83
|
basePlanId?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Stable capability tier microapps should branch on instead of
|
|
86
|
+
* `basePlanId` — see docs/appConfig.md §4.1. Known tiers are `ProductMode`;
|
|
87
|
+
* an unrecognised value is a future tier your code doesn't know about yet
|
|
88
|
+
* — fail closed to the nearest tier you do understand rather than erroring.
|
|
89
|
+
*/
|
|
90
|
+
productMode?: ProductMode | (string & {});
|
|
75
91
|
addOnKeys?: string[];
|
|
76
92
|
/** Apps unlocked by add-ons. */
|
|
77
93
|
apps?: string[];
|
package/dist/types/authKit.d.ts
CHANGED
|
@@ -120,6 +120,96 @@ export interface AppleLoginOptions {
|
|
|
120
120
|
* in `SmartlinksApiError.details`.
|
|
121
121
|
*/
|
|
122
122
|
export type AuthKitErrorCode = 'MISSING_APPLE_TOKEN' | 'APPLE_AUTH_NOT_CONFIGURED' | 'INVALID_APPLE_TOKEN' | 'ACCOUNT_EXISTS_UNVERIFIED' | 'APPLE_AUTH_FAILED';
|
|
123
|
+
/**
|
|
124
|
+
* Details carried on the `MFA_REQUIRED` error thrown by {@link authKit.login} when the
|
|
125
|
+
* client's MFA policy requires a step-up. Surfaced as `err.details` (or
|
|
126
|
+
* `err.errorResponse.details`) on the `SmartlinksApiError` — `login()` never resolves in
|
|
127
|
+
* this case, it throws a 403 instead.
|
|
128
|
+
*/
|
|
129
|
+
export interface MfaRequiredDetails {
|
|
130
|
+
/** Short-lived (10 min) and single-use. Pass it to every challenge/finalize call below. */
|
|
131
|
+
mfaSessionToken: string;
|
|
132
|
+
/** Only factors the user actually enrolled. */
|
|
133
|
+
availableFactors: Array<'email' | 'sms'>;
|
|
134
|
+
/** `sms` is preferred over `email` when both are enrolled. */
|
|
135
|
+
preferredFactor: 'email' | 'sms';
|
|
136
|
+
maskedDestinations: {
|
|
137
|
+
email?: string;
|
|
138
|
+
sms?: string;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Response from {@link authKit.mfaChallengeSend}. */
|
|
142
|
+
export interface MfaChallengeSendResponse {
|
|
143
|
+
success: true;
|
|
144
|
+
factor: 'email' | 'sms';
|
|
145
|
+
/** Masked, e.g. `"j***@x.com"` or `"+1******1234"`. */
|
|
146
|
+
destination: string;
|
|
147
|
+
/** ISO timestamp, 10 minutes from send. */
|
|
148
|
+
expiresAt: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Response from {@link authKit.mfaChallengeVerify} / {@link authKit.mfaRecoveryCode}.
|
|
152
|
+
* Same shape as a normal login response, plus trusted-device fields when the caller sent
|
|
153
|
+
* `trustDevice: true`.
|
|
154
|
+
*/
|
|
155
|
+
export interface MfaFinalizeResponse extends AuthLoginResponse {
|
|
156
|
+
/**
|
|
157
|
+
* Present only when the challenge was completed with `trustDevice: true`. Persist it
|
|
158
|
+
* alongside the refresh token (same secure storage) and send it as `trustedDeviceToken`
|
|
159
|
+
* on future {@link authKit.login} calls to skip the challenge on this device.
|
|
160
|
+
*/
|
|
161
|
+
trustedDeviceToken?: string;
|
|
162
|
+
trustedDeviceExpiresAt?: string;
|
|
163
|
+
}
|
|
164
|
+
/** Response from {@link authKit.enrollEmailMfa} / {@link authKit.enrollSmsMfa}. */
|
|
165
|
+
export interface MfaEnrollSendResponse {
|
|
166
|
+
mfaSessionToken: string;
|
|
167
|
+
destination: string;
|
|
168
|
+
expiresAt: string;
|
|
169
|
+
}
|
|
170
|
+
/** Response from {@link authKit.confirmEmailMfa} / {@link authKit.confirmSmsMfa}. */
|
|
171
|
+
export interface MfaEnrolledResponse {
|
|
172
|
+
enrolled: true;
|
|
173
|
+
factor: 'email' | 'sms';
|
|
174
|
+
}
|
|
175
|
+
/** Response from {@link authKit.getMfaFactors}. */
|
|
176
|
+
export interface MfaFactorsResponse {
|
|
177
|
+
enrolledFactors: {
|
|
178
|
+
email?: {
|
|
179
|
+
enrolledAt: string;
|
|
180
|
+
destination: string;
|
|
181
|
+
};
|
|
182
|
+
sms?: {
|
|
183
|
+
enrolledAt: string;
|
|
184
|
+
destination: string;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
recoveryCodesRemaining: number;
|
|
188
|
+
mfaEnabledForClient: boolean;
|
|
189
|
+
}
|
|
190
|
+
/** One entry from {@link authKit.listTrustedDevices}. */
|
|
191
|
+
export interface TrustedDevice {
|
|
192
|
+
id: string;
|
|
193
|
+
label: string | null;
|
|
194
|
+
createdAtMs: number;
|
|
195
|
+
lastUsedAtMs: number;
|
|
196
|
+
expiresAtMs: number;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Server-defined error codes for the step-up MFA flow, surfaced via
|
|
200
|
+
* `SmartlinksApiError.errorCode`.
|
|
201
|
+
*
|
|
202
|
+
* - `MFA_REQUIRED` (403) — from {@link authKit.login} only; see {@link MfaRequiredDetails}.
|
|
203
|
+
* - `INVALID_MFA_CODE` (401) — wrong code; let the user retry (attempts are capped, see next).
|
|
204
|
+
* - `MFA_TOO_MANY_ATTEMPTS` (429) — 5 wrong attempts; the challenge is burned, restart from `login()`.
|
|
205
|
+
* - `MFA_FACTOR_NOT_ENROLLED` (400) — requested factor isn't enrolled (or disabled for the
|
|
206
|
+
* client) — a programming error if the UI only offers `availableFactors`.
|
|
207
|
+
* - `MFA_SESSION_EXPIRED` (401) — `mfaSessionToken` past its 10-minute TTL; restart from `login()`.
|
|
208
|
+
* - `MFA_SESSION_INVALID` (401) — unknown/wrong-client/already-consumed token, or (on enroll
|
|
209
|
+
* confirm) mismatched user; restart the enroll/challenge flow.
|
|
210
|
+
* - `RECOVERY_CODE_INVALID` (401) — wrong or already-used recovery code.
|
|
211
|
+
*/
|
|
212
|
+
export type MfaErrorCode = 'MFA_REQUIRED' | 'INVALID_MFA_CODE' | 'MFA_TOO_MANY_ATTEMPTS' | 'MFA_FACTOR_NOT_ENROLLED' | 'MFA_SESSION_EXPIRED' | 'MFA_SESSION_INVALID' | 'RECOVERY_CODE_INVALID';
|
|
123
213
|
export interface MagicLinkSendResponse {
|
|
124
214
|
success: boolean;
|
|
125
215
|
message: string;
|
|
@@ -122,23 +122,19 @@ export interface ValueCondition extends BaseCondition {
|
|
|
122
122
|
* passed but didn't resolve" — see `invalidProof` below for that case.
|
|
123
123
|
* - **Authenticity** (reads `params.itemContext` — see `ItemContext`,
|
|
124
124
|
* docs/item-context.md): `isAuthentic`, `notAuthentic`, `invalidProof`,
|
|
125
|
-
* `
|
|
125
|
+
* `isRescan`.
|
|
126
126
|
* - `invalidProof` is specifically "an identifier was passed and resolution
|
|
127
127
|
* was attempted, but it came back invalid or not-found" — the "someone
|
|
128
128
|
* scanned a fake tag / typed a bad serial" case, as opposed to `noProof`
|
|
129
129
|
* ("nothing was on the URL to check at all").
|
|
130
|
-
* - `isAuthentic` is true for both a fresh tap and a rescan
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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.
|
|
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.
|
|
138
134
|
*/
|
|
139
135
|
export interface ItemStatusCondition extends BaseCondition {
|
|
140
136
|
type: 'itemStatus';
|
|
141
|
-
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual' | 'isAuthentic' | 'notAuthentic' | 'invalidProof' | '
|
|
137
|
+
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual' | 'isAuthentic' | 'notAuthentic' | 'invalidProof' | 'isRescan';
|
|
142
138
|
}
|
|
143
139
|
/**
|
|
144
140
|
* Facet-based condition — gates on the facet values assigned to the current product.
|
|
@@ -330,7 +326,7 @@ export interface ConditionDebugOptions {
|
|
|
330
326
|
* - **value** - Custom field comparisons
|
|
331
327
|
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
332
328
|
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
333
|
-
* `invalidProof`/`
|
|
329
|
+
* `invalidProof`/`isRescan`)
|
|
334
330
|
* - **condition** - Nested condition references
|
|
335
331
|
*
|
|
336
332
|
* Conditions can be combined with AND or OR logic.
|
package/dist/utils/conditions.js
CHANGED
|
@@ -175,7 +175,7 @@ async function evaluateConditionEntry(condition, params) {
|
|
|
175
175
|
* - **value** - Custom field comparisons
|
|
176
176
|
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
177
177
|
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
178
|
-
* `invalidProof`/`
|
|
178
|
+
* `invalidProof`/`isRescan`)
|
|
179
179
|
* - **condition** - Nested condition references
|
|
180
180
|
*
|
|
181
181
|
* Conditions can be combined with AND or OR logic.
|
|
@@ -819,7 +819,7 @@ async function validateFacet(condition, params) {
|
|
|
819
819
|
* Validate item status condition
|
|
820
820
|
*/
|
|
821
821
|
async function validateItemStatus(condition, params) {
|
|
822
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
822
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
823
823
|
switch (condition.statusType) {
|
|
824
824
|
case 'isClaimable':
|
|
825
825
|
return {
|
|
@@ -869,17 +869,11 @@ async function validateItemStatus(condition, params) {
|
|
|
869
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
870
|
context: { itemContextStatus: (_f = params.itemContext) === null || _f === void 0 ? void 0 : _f.status },
|
|
871
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
872
|
case 'isRescan':
|
|
879
873
|
return {
|
|
880
|
-
passed: ((
|
|
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),
|
|
881
875
|
detail: 'Checked that itemContext is authentic but a duplicate/replayed tap (status === rescan).',
|
|
882
|
-
context: { itemContextStatus: (
|
|
876
|
+
context: { itemContextStatus: (_j = params.itemContext) === null || _j === void 0 ? void 0 : _j.status },
|
|
883
877
|
};
|
|
884
878
|
default:
|
|
885
879
|
return {
|
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.13 | Generated: 2026-07-27T08:21:16.504Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -1139,6 +1139,17 @@ interface AnalyticsFilterRequest {
|
|
|
1139
1139
|
claimIds?: string[]
|
|
1140
1140
|
isAdmin?: boolean
|
|
1141
1141
|
hasLocation?: boolean
|
|
1142
|
+
* Filter web-events rows by the `source` column (list-match). Web-events
|
|
1143
|
+
* only - has no effect on `source: 'tag'` queries.
|
|
1144
|
+
*
|
|
1145
|
+
* There is deliberately no singular `source` filter: the request's own
|
|
1146
|
+
* top-level `source` field (`'events'` vs `'tag'`) already owns that name
|
|
1147
|
+
* as the table selector and predates this column - same word, two
|
|
1148
|
+
* different things. Use a single-element array (`sources: ['portal']`)
|
|
1149
|
+
* for an exact-match filter.
|
|
1150
|
+
sources?: string[]
|
|
1151
|
+
redirectMode?: string
|
|
1152
|
+
redirectModes?: string[]
|
|
1142
1153
|
}
|
|
1143
1154
|
```
|
|
1144
1155
|
|
|
@@ -1419,6 +1430,11 @@ interface AppliedOverridesSummary {
|
|
|
1419
1430
|
```typescript
|
|
1420
1431
|
interface SystemBlock {
|
|
1421
1432
|
basePlanId?: string
|
|
1433
|
+
* Stable capability tier microapps should branch on instead of
|
|
1434
|
+
* `basePlanId` — see docs/appConfig.md §4.1. Known tiers are `ProductMode`;
|
|
1435
|
+
* an unrecognised value is a future tier your code doesn't know about yet
|
|
1436
|
+
* — fail closed to the nearest tier you do understand rather than erroring.
|
|
1437
|
+
productMode?: ProductMode | (string & {})
|
|
1422
1438
|
addOnKeys?: string[]
|
|
1423
1439
|
apps?: string[]
|
|
1424
1440
|
* Explicit overrides only — an absent key is NOT "off". Resolve with
|
|
@@ -1451,6 +1467,8 @@ interface AppConfigSettings {
|
|
|
1451
1467
|
}
|
|
1452
1468
|
```
|
|
1453
1469
|
|
|
1470
|
+
**ProductMode** = `'simple_redirect' | 'rich_redirect' | 'inform' | 'engage'`
|
|
1471
|
+
|
|
1454
1472
|
### appManifest
|
|
1455
1473
|
|
|
1456
1474
|
**AppBundle** (interface)
|
|
@@ -3089,6 +3107,66 @@ interface AppleLoginOptions {
|
|
|
3089
3107
|
}
|
|
3090
3108
|
```
|
|
3091
3109
|
|
|
3110
|
+
**MfaRequiredDetails** (interface)
|
|
3111
|
+
```typescript
|
|
3112
|
+
interface MfaRequiredDetails {
|
|
3113
|
+
mfaSessionToken: string
|
|
3114
|
+
availableFactors: Array<'email' | 'sms'>
|
|
3115
|
+
preferredFactor: 'email' | 'sms'
|
|
3116
|
+
maskedDestinations: { email?: string; sms?: string }
|
|
3117
|
+
}
|
|
3118
|
+
```
|
|
3119
|
+
|
|
3120
|
+
**MfaChallengeSendResponse** (interface)
|
|
3121
|
+
```typescript
|
|
3122
|
+
interface MfaChallengeSendResponse {
|
|
3123
|
+
success: true
|
|
3124
|
+
factor: 'email' | 'sms'
|
|
3125
|
+
destination: string
|
|
3126
|
+
expiresAt: string
|
|
3127
|
+
}
|
|
3128
|
+
```
|
|
3129
|
+
|
|
3130
|
+
**MfaEnrollSendResponse** (interface)
|
|
3131
|
+
```typescript
|
|
3132
|
+
interface MfaEnrollSendResponse {
|
|
3133
|
+
mfaSessionToken: string
|
|
3134
|
+
destination: string
|
|
3135
|
+
expiresAt: string
|
|
3136
|
+
}
|
|
3137
|
+
```
|
|
3138
|
+
|
|
3139
|
+
**MfaEnrolledResponse** (interface)
|
|
3140
|
+
```typescript
|
|
3141
|
+
interface MfaEnrolledResponse {
|
|
3142
|
+
enrolled: true
|
|
3143
|
+
factor: 'email' | 'sms'
|
|
3144
|
+
}
|
|
3145
|
+
```
|
|
3146
|
+
|
|
3147
|
+
**MfaFactorsResponse** (interface)
|
|
3148
|
+
```typescript
|
|
3149
|
+
interface MfaFactorsResponse {
|
|
3150
|
+
enrolledFactors: {
|
|
3151
|
+
email?: { enrolledAt: string; destination: string }
|
|
3152
|
+
sms?: { enrolledAt: string; destination: string }
|
|
3153
|
+
}
|
|
3154
|
+
recoveryCodesRemaining: number
|
|
3155
|
+
mfaEnabledForClient: boolean
|
|
3156
|
+
}
|
|
3157
|
+
```
|
|
3158
|
+
|
|
3159
|
+
**TrustedDevice** (interface)
|
|
3160
|
+
```typescript
|
|
3161
|
+
interface TrustedDevice {
|
|
3162
|
+
id: string
|
|
3163
|
+
label: string | null
|
|
3164
|
+
createdAtMs: number
|
|
3165
|
+
lastUsedAtMs: number
|
|
3166
|
+
expiresAtMs: number
|
|
3167
|
+
}
|
|
3168
|
+
```
|
|
3169
|
+
|
|
3092
3170
|
**MagicLinkSendResponse** (interface)
|
|
3093
3171
|
```typescript
|
|
3094
3172
|
interface MagicLinkSendResponse {
|
|
@@ -3338,6 +3416,8 @@ interface AuthKitConfig {
|
|
|
3338
3416
|
|
|
3339
3417
|
**AuthKitErrorCode** = ``
|
|
3340
3418
|
|
|
3419
|
+
**MfaErrorCode** = ``
|
|
3420
|
+
|
|
3341
3421
|
**VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
|
|
3342
3422
|
|
|
3343
3423
|
### batch
|
|
@@ -8536,8 +8616,8 @@ Gets current account information for the logged in user. Returns user, owner, ac
|
|
|
8536
8616
|
|
|
8537
8617
|
### authKit
|
|
8538
8618
|
|
|
8539
|
-
**login**(clientId: string, email: string, password: string) → `Promise<AuthLoginResponse>`
|
|
8540
|
-
Login with email + password (public).
|
|
8619
|
+
**login**(clientId: string, email: string, password: string, trustedDeviceToken?: string) → `Promise<AuthLoginResponse>`
|
|
8620
|
+
Login with email + password (public). When the client's MFA policy requires a step-up, the server returns **403 `MFA_REQUIRED`** instead of a session — `login()` throws a `SmartlinksApiError` with `err.errorResponse?.errorCode === 'MFA_REQUIRED'` and the challenge details in `err.details` (see {@link MfaRequiredDetails}). Route the caller to {@link mfaChallengeSend} on that error; this method's return type is unchanged. returned one (via {@link mfaChallengeVerify}/{@link mfaRecoveryCode} with `trustDevice: true`), pass it here to skip the challenge entirely as long as it's still valid. If it's revoked/expired, the server silently falls back to requiring a fresh challenge — `login()` just returns `MFA_REQUIRED` again, no special handling.
|
|
8541
8621
|
|
|
8542
8622
|
**register**(clientId: string, data: { email: string; password: string; displayName?: string; accountData?: Record<string, any> }) → `Promise<AuthLoginResponse>`
|
|
8543
8623
|
Register a new user (public).
|
|
@@ -8629,23 +8709,59 @@ Update the authenticated user's profile and replace the bearer token when refres
|
|
|
8629
8709
|
**deleteAccount**(clientId: string, password: string, confirmText: string) → `Promise<SuccessResponse>`
|
|
8630
8710
|
Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
|
|
8631
8711
|
|
|
8712
|
+
**mfaChallengeSend**(clientId: string, mfaSessionToken: string, factor: 'email' | 'sms') → `Promise<MfaChallengeSendResponse>`
|
|
8713
|
+
Send (or resend) an MFA challenge code to the given factor (public).
|
|
8714
|
+
|
|
8715
|
+
**mfaChallengeVerify**(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string) → `Promise<MfaFinalizeResponse>`
|
|
8716
|
+
Verify an MFA challenge code and finalize the login (public). On success this behaves like {@link login} — the bearer token is adopted and the cache invalidated. it and pass it to future {@link login} calls to skip the challenge on this device.
|
|
8717
|
+
|
|
8718
|
+
**mfaRecoveryCode**(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string) → `Promise<MfaFinalizeResponse>`
|
|
8719
|
+
Finalize a challenged login using a single-use recovery code instead of a sent code (public). Same finalize-session behaviour as {@link mfaChallengeVerify}.
|
|
8720
|
+
|
|
8721
|
+
**getMfaFactors**(clientId: string) → `Promise<MfaFactorsResponse>`
|
|
8722
|
+
List enrolled factors and recovery-code count for the current user (authenticated).
|
|
8723
|
+
|
|
8724
|
+
**enrollEmailMfa**(clientId: string) → `Promise<MfaEnrollSendResponse>`
|
|
8725
|
+
Begin email-factor enrollment; sends a code to the account's existing email (authenticated).
|
|
8726
|
+
|
|
8727
|
+
**confirmEmailMfa**(clientId: string, mfaSessionToken: string, code: string) → `Promise<MfaEnrolledResponse>`
|
|
8728
|
+
Confirm email-factor enrollment with the code sent by {@link enrollEmailMfa} (authenticated).
|
|
8729
|
+
|
|
8730
|
+
**enrollSmsMfa**(clientId: string, phoneNumber: string) → `Promise<MfaEnrollSendResponse>`
|
|
8731
|
+
Begin SMS-factor enrollment; sends a code to the given phone number (authenticated).
|
|
8732
|
+
|
|
8733
|
+
**confirmSmsMfa**(clientId: string, mfaSessionToken: string, code: string) → `Promise<MfaEnrolledResponse>`
|
|
8734
|
+
Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated).
|
|
8735
|
+
|
|
8736
|
+
**generateMfaRecoveryCodes**(clientId: string, password: string) → `Promise<`
|
|
8737
|
+
Generate a fresh set of recovery codes, invalidating any previous set (authenticated). Returned **in plaintext, exactly once** — nothing else in the API ever returns them again, so the caller must display/export them immediately.
|
|
8738
|
+
|
|
8739
|
+
**removeMfaFactor**(clientId: string, factor: 'email' | 'sms', password: string) → `Promise<SuccessResponse>`
|
|
8740
|
+
Remove an enrolled MFA factor (authenticated). Server route is DELETE with a body.
|
|
8741
|
+
|
|
8742
|
+
**listTrustedDevices**(clientId: string) → `Promise<`
|
|
8743
|
+
List devices trusted to skip MFA challenges for the current user (authenticated).
|
|
8744
|
+
|
|
8745
|
+
**revokeTrustedDevice**(clientId: string, id: string) → `Promise<SuccessResponse>`
|
|
8746
|
+
Revoke a single trusted device by id (authenticated).
|
|
8747
|
+
|
|
8632
8748
|
**load**(authKitId: string) → `Promise<AuthKitConfig>`
|
|
8633
|
-
|
|
8749
|
+
Revoke a single trusted device by id (authenticated).
|
|
8634
8750
|
|
|
8635
8751
|
**get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
|
|
8636
|
-
|
|
8752
|
+
Revoke a single trusted device by id (authenticated).
|
|
8637
8753
|
|
|
8638
8754
|
**list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
|
|
8639
|
-
|
|
8755
|
+
Revoke a single trusted device by id (authenticated).
|
|
8640
8756
|
|
|
8641
8757
|
**create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
|
|
8642
|
-
|
|
8758
|
+
Revoke a single trusted device by id (authenticated).
|
|
8643
8759
|
|
|
8644
8760
|
**update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
|
|
8645
|
-
|
|
8761
|
+
Revoke a single trusted device by id (authenticated).
|
|
8646
8762
|
|
|
8647
8763
|
**remove**(collectionId: string, authKitId: string) → `Promise<void>`
|
|
8648
|
-
|
|
8764
|
+
Revoke a single trusted device by id (authenticated).
|
|
8649
8765
|
|
|
8650
8766
|
### batch
|
|
8651
8767
|
|
|
@@ -15,6 +15,8 @@ Some of these are now promoted top-level analytics fields. Others remain good me
|
|
|
15
15
|
- `entryType`
|
|
16
16
|
- `pageId`
|
|
17
17
|
- `scanMethod`
|
|
18
|
+
- `source` - collection/web-events only. Free-form client app identifier, e.g. `'portal'`, `'hub'`. No enum/whitelist. Not available on tag events - see the `analytics.tag.track(event)` section in [docs/analytics.md](analytics.md) for why.
|
|
19
|
+
- `redirectMode` - tag-events only. Usually server-written; only set this yourself if you're logging a redirect-style event.
|
|
18
20
|
|
|
19
21
|
These should be sent as top-level analytics fields, not inside `metadata`.
|
|
20
22
|
|
|
@@ -59,6 +61,7 @@ These keys give teams a shared vocabulary for:
|
|
|
59
61
|
- Send promoted fields at top level.
|
|
60
62
|
- Keep values flat and scalar where possible so they are easier to filter and break down later.
|
|
61
63
|
- Promote a field to a first-class backend column only when it becomes a hot platform-wide dimension.
|
|
64
|
+
- Note: `source` (the event column) and the query-time `source` parameter (`'events' | 'tag'`, which table to query) are unrelated fields that happen to share a name. When filtering by the `source` column, use the plural `sources` array - there is no singular `source` filter, precisely to avoid colliding with the table selector.
|
|
62
65
|
|
|
63
66
|
---
|
|
64
67
|
|
|
@@ -77,6 +80,7 @@ analytics.collection.track({
|
|
|
77
80
|
campaign: 'summer-launch',
|
|
78
81
|
utmSource: 'email',
|
|
79
82
|
pageId: 'QR123',
|
|
83
|
+
source: 'portal',
|
|
80
84
|
metadata: {
|
|
81
85
|
pagePath: '/c/demo-collection',
|
|
82
86
|
},
|
package/docs/analytics.md
CHANGED
|
@@ -48,6 +48,12 @@ The backend stores custom analytics dimensions in `metadata`, but promoted analy
|
|
|
48
48
|
|
|
49
49
|
See [docs/analytics-metadata-conventions.md](analytics-metadata-conventions.md) for the recommended key set.
|
|
50
50
|
|
|
51
|
+
### A note on server-written events
|
|
52
|
+
|
|
53
|
+
Not every row in these tables is written by your app. As of 2026-07-23, the server itself logs a `scan_redirect` event on the tag-events table at the moment a GS1 digital-link scan, claim short-link scan, or NFC tap decides on a redirect destination - before the client ever loads anything. This is separate from `scan_tag`, which your app still writes on landing exactly as before. `scan_blank_tag` remains excluded from the analytics pipeline.
|
|
54
|
+
|
|
55
|
+
Server-issued redirects also append `?sid=<value>` to the destination URL. If your landing page reads `sid` off the URL and reuses it as its own `sessionId` on the next `analytics.collection.track(...)` or `analytics.tag.track(...)` call, "did the redirect actually land" becomes a free `sessionId` join - nothing new to send, just reuse the value if it's present.
|
|
56
|
+
|
|
51
57
|
---
|
|
52
58
|
|
|
53
59
|
## Quick Start
|
|
@@ -67,6 +73,7 @@ analytics.collection.track({
|
|
|
67
73
|
appId: 'homepage',
|
|
68
74
|
path: '/c/demo-collection',
|
|
69
75
|
deviceType: 'mobile',
|
|
76
|
+
source: 'portal',
|
|
70
77
|
})
|
|
71
78
|
```
|
|
72
79
|
|
|
@@ -298,7 +305,9 @@ Tracks generic collection analytics events such as:
|
|
|
298
305
|
- internal navigation
|
|
299
306
|
- outbound link activity
|
|
300
307
|
|
|
301
|
-
Supported top-level fields include the core event fields plus promoted analytics columns such as `visitorId`, `referrerHost`, `pageId`, and `
|
|
308
|
+
Supported top-level fields include the core event fields plus promoted analytics columns such as `visitorId`, `referrerHost`, `pageId`, `entryType`, and `source`, along with custom metadata dimensions like `group`, `placement`, `pagePath`, and `qrCodeId`.
|
|
309
|
+
|
|
310
|
+
`source` is a free-form string identifying which client app logged the event, e.g. `'portal'`, `'hub'` - there's no enum or whitelist, send whatever identifies your app. It's web-events only; there is no equivalent field on tag events (see the `analytics.tag.track(event)` section below for why).
|
|
302
311
|
|
|
303
312
|
Example:
|
|
304
313
|
|
|
@@ -321,6 +330,7 @@ analytics.collection.track({
|
|
|
321
330
|
group: 'summer-launch',
|
|
322
331
|
placement: 'hero',
|
|
323
332
|
pageId: 'QR123',
|
|
333
|
+
source: 'portal',
|
|
324
334
|
metadata: { pagePath: '/c/demo-collection?pageId=QR123' },
|
|
325
335
|
})
|
|
326
336
|
```
|
|
@@ -334,7 +344,11 @@ Tracks physical scan analytics such as:
|
|
|
334
344
|
- claim/code activity
|
|
335
345
|
- admin vs customer scan behavior
|
|
336
346
|
|
|
337
|
-
Supported top-level fields include the core scan fields plus promoted analytics columns such as `visitorId`, `entryType`, and `
|
|
347
|
+
Supported top-level fields include the core scan fields plus promoted analytics columns such as `visitorId`, `entryType`, `scanMethod`, and `redirectMode`, along with custom metadata dimensions like `group`, `tag`, and campaign extras.
|
|
348
|
+
|
|
349
|
+
`redirectMode` is only relevant if you're logging a redirect-style event yourself - in practice it's mostly written by the server automatically (see [A note on server-written events](#a-note-on-server-written-events) above).
|
|
350
|
+
|
|
351
|
+
There is deliberately no `source` field on tag events, even though one exists on collection events. `eventType` already tells you who wrote a tag-events row - `scan_redirect` is always server-written, `scan_tag`/`scan_blank_tag` are always client-written - so a `source` column here would just duplicate that. To segment tag scans by NFC vs. QR, use `entryType`/`scanMethod` instead.
|
|
338
352
|
|
|
339
353
|
Example:
|
|
340
354
|
|
|
@@ -541,6 +555,24 @@ const countries = await analytics.admin.breakdown('demo-collection', {
|
|
|
541
555
|
})
|
|
542
556
|
```
|
|
543
557
|
|
|
558
|
+
Breaking down or filtering by the new `source` *column* (web-events only) works the same way, just watch the naming: the request's own top-level `source: 'events'` is the pre-existing table selector, and it sits alongside a `sources` array that filters by the column's value. They are unrelated fields that happen to share a name.
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
const appSources = await analytics.admin.breakdown('demo-collection', {
|
|
562
|
+
source: 'events', // table selector: query web-events
|
|
563
|
+
dimension: 'source', // break down by the `source` column
|
|
564
|
+
metric: 'count',
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
const portalOnly = await analytics.admin.events('demo-collection', {
|
|
568
|
+
source: 'events', // table selector
|
|
569
|
+
sources: ['portal'], // column filter - single-element array for exact match
|
|
570
|
+
limit: 100,
|
|
571
|
+
})
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
There is no singular `source` filter field for this reason - use `sources: ['portal']` for an exact match.
|
|
575
|
+
|
|
544
576
|
### Raw events
|
|
545
577
|
|
|
546
578
|
```typescript
|
|
@@ -588,7 +620,7 @@ For metrics, generic queries support:
|
|
|
588
620
|
- `uniqueSessions`
|
|
589
621
|
- `uniqueVisitors`
|
|
590
622
|
|
|
591
|
-
Extra collection-event filters include:
|
|
623
|
+
Extra collection-event (web-events) filters include:
|
|
592
624
|
|
|
593
625
|
- `appId`, `appIds[]`
|
|
594
626
|
- `destinationAppId`, `destinationAppIds[]`
|
|
@@ -596,6 +628,7 @@ Extra collection-event filters include:
|
|
|
596
628
|
- `href`, `path`
|
|
597
629
|
- `hrefContains`, `pathContains`
|
|
598
630
|
- `isExternal`
|
|
631
|
+
- `sources[]` - filter by the `source` column (list-match only, no singular form - see [Breakdown](#breakdown))
|
|
599
632
|
|
|
600
633
|
Extra tag-event filters include:
|
|
601
634
|
|
|
@@ -603,6 +636,7 @@ Extra tag-event filters include:
|
|
|
603
636
|
- `claimId`, `claimIds[]`
|
|
604
637
|
- `isAdmin`
|
|
605
638
|
- `hasLocation`
|
|
639
|
+
- `redirectMode`, `redirectModes[]`
|
|
606
640
|
|
|
607
641
|
---
|
|
608
642
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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. |
|