@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.
@@ -1,11 +1,25 @@
1
- import type { AuthLoginResponse, AppleLoginOptions, RefreshResponse, LogoutResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, AuthKitConfig, MagicLinkSendResponse, MagicLinkVerifyResponse, UserProfile, UpdateProfileResponse, ProfileUpdateData, SuccessResponse, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse } from "../types/authKit";
1
+ import type { AuthLoginResponse, AppleLoginOptions, RefreshResponse, LogoutResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, AuthKitConfig, MagicLinkSendResponse, MagicLinkVerifyResponse, UserProfile, UpdateProfileResponse, ProfileUpdateData, SuccessResponse, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse, MfaChallengeSendResponse, MfaFinalizeResponse, MfaEnrollSendResponse, MfaEnrolledResponse, MfaFactorsResponse, TrustedDevice } from "../types/authKit";
2
2
  /**
3
3
  * Namespace containing helper functions for the new AuthKit API.
4
4
  * Legacy collection-based authKit helpers retained (marked as *Legacy*).
5
5
  */
6
6
  export declare namespace authKit {
7
- /** Login with email + password (public). */
8
- function login(clientId: string, email: string, password: string): Promise<AuthLoginResponse>;
7
+ /**
8
+ * Login with email + password (public).
9
+ *
10
+ * When the client's MFA policy requires a step-up, the server returns **403
11
+ * `MFA_REQUIRED`** instead of a session — `login()` throws a `SmartlinksApiError` with
12
+ * `err.errorResponse?.errorCode === 'MFA_REQUIRED'` and the challenge details in
13
+ * `err.details` (see {@link MfaRequiredDetails}). Route the caller to
14
+ * {@link mfaChallengeSend} on that error; this method's return type is unchanged.
15
+ *
16
+ * @param trustedDeviceToken - Optional. If a previous MFA challenge on this device
17
+ * returned one (via {@link mfaChallengeVerify}/{@link mfaRecoveryCode} with
18
+ * `trustDevice: true`), pass it here to skip the challenge entirely as long as it's
19
+ * still valid. If it's revoked/expired, the server silently falls back to requiring a
20
+ * fresh challenge — `login()` just returns `MFA_REQUIRED` again, no special handling.
21
+ */
22
+ function login(clientId: string, email: string, password: string, trustedDeviceToken?: string): Promise<AuthLoginResponse>;
9
23
  /** Register a new user (public). */
10
24
  function register(clientId: string, data: {
11
25
  email: string;
@@ -124,6 +138,47 @@ export declare namespace authKit {
124
138
  function verifyEmailChange(clientId: string, token: string): Promise<SuccessResponse>;
125
139
  function updatePhone(clientId: string, phoneNumber: string, verificationCode: string): Promise<SuccessResponse>;
126
140
  function deleteAccount(clientId: string, password: string, confirmText: string): Promise<SuccessResponse>;
141
+ /** Send (or resend) an MFA challenge code to the given factor (public). */
142
+ function mfaChallengeSend(clientId: string, mfaSessionToken: string, factor: 'email' | 'sms'): Promise<MfaChallengeSendResponse>;
143
+ /**
144
+ * Verify an MFA challenge code and finalize the login (public). On success this behaves
145
+ * like {@link login} — the bearer token is adopted and the cache invalidated.
146
+ *
147
+ * @param trustDevice - When `true`, the response includes `trustedDeviceToken` — persist
148
+ * it and pass it to future {@link login} calls to skip the challenge on this device.
149
+ */
150
+ function mfaChallengeVerify(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string): Promise<MfaFinalizeResponse>;
151
+ /**
152
+ * Finalize a challenged login using a single-use recovery code instead of a sent code
153
+ * (public). Same finalize-session behaviour as {@link mfaChallengeVerify}.
154
+ */
155
+ function mfaRecoveryCode(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string): Promise<MfaFinalizeResponse>;
156
+ /** List enrolled factors and recovery-code count for the current user (authenticated). */
157
+ function getMfaFactors(clientId: string): Promise<MfaFactorsResponse>;
158
+ /** Begin email-factor enrollment; sends a code to the account's existing email (authenticated). */
159
+ function enrollEmailMfa(clientId: string): Promise<MfaEnrollSendResponse>;
160
+ /** Confirm email-factor enrollment with the code sent by {@link enrollEmailMfa} (authenticated). */
161
+ function confirmEmailMfa(clientId: string, mfaSessionToken: string, code: string): Promise<MfaEnrolledResponse>;
162
+ /** Begin SMS-factor enrollment; sends a code to the given phone number (authenticated). */
163
+ function enrollSmsMfa(clientId: string, phoneNumber: string): Promise<MfaEnrollSendResponse>;
164
+ /** Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated). */
165
+ function confirmSmsMfa(clientId: string, mfaSessionToken: string, code: string): Promise<MfaEnrolledResponse>;
166
+ /**
167
+ * Generate a fresh set of recovery codes, invalidating any previous set (authenticated).
168
+ * Returned **in plaintext, exactly once** — nothing else in the API ever returns them
169
+ * again, so the caller must display/export them immediately.
170
+ */
171
+ function generateMfaRecoveryCodes(clientId: string, password: string): Promise<{
172
+ recoveryCodes: string[];
173
+ }>;
174
+ /** Remove an enrolled MFA factor (authenticated). Server route is DELETE with a body. */
175
+ function removeMfaFactor(clientId: string, factor: 'email' | 'sms', password: string): Promise<SuccessResponse>;
176
+ /** List devices trusted to skip MFA challenges for the current user (authenticated). */
177
+ function listTrustedDevices(clientId: string): Promise<{
178
+ devices: TrustedDevice[];
179
+ }>;
180
+ /** Revoke a single trusted device by id (authenticated). */
181
+ function revokeTrustedDevice(clientId: string, id: string): Promise<SuccessResponse>;
127
182
  function load(authKitId: string): Promise<AuthKitConfig>;
128
183
  function get(collectionId: string, authKitId: string): Promise<AuthKitConfig>;
129
184
  function list(collectionId: string, admin?: boolean): Promise<AuthKitConfig[]>;
@@ -1,4 +1,4 @@
1
- import { request, post, put, del, setBearerToken, invalidateCache } from "../http";
1
+ import { request, post, put, del, requestWithOptions, setBearerToken, invalidateCache } from "../http";
2
2
  /**
3
3
  * Namespace containing helper functions for the new AuthKit API.
4
4
  * Legacy collection-based authKit helpers retained (marked as *Legacy*).
@@ -8,9 +8,26 @@ export var authKit;
8
8
  /* ===================================
9
9
  * Authentication (Per client)
10
10
  * =================================== */
11
- /** Login with email + password (public). */
12
- async function login(clientId, email, password) {
13
- const res = await post(`/authkit/${encodeURIComponent(clientId)}/auth/login`, { email, password });
11
+ /**
12
+ * Login with email + password (public).
13
+ *
14
+ * When the client's MFA policy requires a step-up, the server returns **403
15
+ * `MFA_REQUIRED`** instead of a session — `login()` throws a `SmartlinksApiError` with
16
+ * `err.errorResponse?.errorCode === 'MFA_REQUIRED'` and the challenge details in
17
+ * `err.details` (see {@link MfaRequiredDetails}). Route the caller to
18
+ * {@link mfaChallengeSend} on that error; this method's return type is unchanged.
19
+ *
20
+ * @param trustedDeviceToken - Optional. If a previous MFA challenge on this device
21
+ * returned one (via {@link mfaChallengeVerify}/{@link mfaRecoveryCode} with
22
+ * `trustDevice: true`), pass it here to skip the challenge entirely as long as it's
23
+ * still valid. If it's revoked/expired, the server silently falls back to requiring a
24
+ * fresh challenge — `login()` just returns `MFA_REQUIRED` again, no special handling.
25
+ */
26
+ async function login(clientId, email, password, trustedDeviceToken) {
27
+ const body = { email, password };
28
+ if (trustedDeviceToken)
29
+ body.trustedDeviceToken = trustedDeviceToken;
30
+ const res = await post(`/authkit/${encodeURIComponent(clientId)}/auth/login`, body);
14
31
  if (res.token) {
15
32
  setBearerToken(res.token);
16
33
  invalidateCache();
@@ -277,6 +294,101 @@ export var authKit;
277
294
  return res;
278
295
  }
279
296
  authKit.deleteAccount = deleteAccount;
297
+ /* ===================================
298
+ * Step-up MFA — completing a challenged login (public)
299
+ *
300
+ * Called after login() throws MFA_REQUIRED. mfaSessionToken is short-lived (10 min) and
301
+ * single-use — burned on 5 failed attempts (MFA_TOO_MANY_ATTEMPTS) or on success.
302
+ * challenge/send can be called again on the same token (while unexpired/unburned) to
303
+ * switch factors or resend.
304
+ * =================================== */
305
+ /** Send (or resend) an MFA challenge code to the given factor (public). */
306
+ async function mfaChallengeSend(clientId, mfaSessionToken, factor) {
307
+ return post(`/authkit/${encodeURIComponent(clientId)}/mfa/challenge/send`, { mfaSessionToken, factor });
308
+ }
309
+ authKit.mfaChallengeSend = mfaChallengeSend;
310
+ /**
311
+ * Verify an MFA challenge code and finalize the login (public). On success this behaves
312
+ * like {@link login} — the bearer token is adopted and the cache invalidated.
313
+ *
314
+ * @param trustDevice - When `true`, the response includes `trustedDeviceToken` — persist
315
+ * it and pass it to future {@link login} calls to skip the challenge on this device.
316
+ */
317
+ async function mfaChallengeVerify(clientId, mfaSessionToken, code, trustDevice, deviceLabel) {
318
+ const res = await post(`/authkit/${encodeURIComponent(clientId)}/mfa/challenge/verify`, { mfaSessionToken, code, trustDevice, deviceLabel });
319
+ if (res.token) {
320
+ setBearerToken(res.token);
321
+ invalidateCache();
322
+ }
323
+ return res;
324
+ }
325
+ authKit.mfaChallengeVerify = mfaChallengeVerify;
326
+ /**
327
+ * Finalize a challenged login using a single-use recovery code instead of a sent code
328
+ * (public). Same finalize-session behaviour as {@link mfaChallengeVerify}.
329
+ */
330
+ async function mfaRecoveryCode(clientId, mfaSessionToken, code, trustDevice, deviceLabel) {
331
+ const res = await post(`/authkit/${encodeURIComponent(clientId)}/mfa/challenge/recovery-code`, { mfaSessionToken, code, trustDevice, deviceLabel });
332
+ if (res.token) {
333
+ setBearerToken(res.token);
334
+ invalidateCache();
335
+ }
336
+ return res;
337
+ }
338
+ authKit.mfaRecoveryCode = mfaRecoveryCode;
339
+ /* ===================================
340
+ * Step-up MFA — factor management (Authenticated)
341
+ * =================================== */
342
+ /** List enrolled factors and recovery-code count for the current user (authenticated). */
343
+ async function getMfaFactors(clientId) {
344
+ return request(`/authkit/${encodeURIComponent(clientId)}/mfa/factors`);
345
+ }
346
+ authKit.getMfaFactors = getMfaFactors;
347
+ /** Begin email-factor enrollment; sends a code to the account's existing email (authenticated). */
348
+ async function enrollEmailMfa(clientId) {
349
+ return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/email/enroll`, {});
350
+ }
351
+ authKit.enrollEmailMfa = enrollEmailMfa;
352
+ /** Confirm email-factor enrollment with the code sent by {@link enrollEmailMfa} (authenticated). */
353
+ async function confirmEmailMfa(clientId, mfaSessionToken, code) {
354
+ return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/email/confirm`, { mfaSessionToken, code });
355
+ }
356
+ authKit.confirmEmailMfa = confirmEmailMfa;
357
+ /** Begin SMS-factor enrollment; sends a code to the given phone number (authenticated). */
358
+ async function enrollSmsMfa(clientId, phoneNumber) {
359
+ return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/sms/enroll`, { phoneNumber });
360
+ }
361
+ authKit.enrollSmsMfa = enrollSmsMfa;
362
+ /** Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated). */
363
+ async function confirmSmsMfa(clientId, mfaSessionToken, code) {
364
+ return post(`/authkit/${encodeURIComponent(clientId)}/mfa/factors/sms/confirm`, { mfaSessionToken, code });
365
+ }
366
+ authKit.confirmSmsMfa = confirmSmsMfa;
367
+ /**
368
+ * Generate a fresh set of recovery codes, invalidating any previous set (authenticated).
369
+ * Returned **in plaintext, exactly once** — nothing else in the API ever returns them
370
+ * again, so the caller must display/export them immediately.
371
+ */
372
+ async function generateMfaRecoveryCodes(clientId, password) {
373
+ return post(`/authkit/${encodeURIComponent(clientId)}/mfa/recovery-codes/generate`, { password });
374
+ }
375
+ authKit.generateMfaRecoveryCodes = generateMfaRecoveryCodes;
376
+ /** Remove an enrolled MFA factor (authenticated). Server route is DELETE with a body. */
377
+ async function removeMfaFactor(clientId, factor, password) {
378
+ const path = `/authkit/${encodeURIComponent(clientId)}/mfa/factors/${factor}`;
379
+ return requestWithOptions(path, { method: 'DELETE', body: JSON.stringify({ password }) });
380
+ }
381
+ authKit.removeMfaFactor = removeMfaFactor;
382
+ /** List devices trusted to skip MFA challenges for the current user (authenticated). */
383
+ async function listTrustedDevices(clientId) {
384
+ return request(`/authkit/${encodeURIComponent(clientId)}/mfa/trusted-devices`);
385
+ }
386
+ authKit.listTrustedDevices = listTrustedDevices;
387
+ /** Revoke a single trusted device by id (authenticated). */
388
+ async function revokeTrustedDevice(clientId, id) {
389
+ return del(`/authkit/${encodeURIComponent(clientId)}/mfa/trusted-devices/${encodeURIComponent(id)}`);
390
+ }
391
+ authKit.revokeTrustedDevice = revokeTrustedDevice;
280
392
  /* ===================================
281
393
  * Collection-based AuthKit
282
394
  * =================================== */
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.10 | Generated: 2026-07-19T13:35:07.171Z
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
- Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
8749
+ Revoke a single trusted device by id (authenticated).
8634
8750
 
8635
8751
  **get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
8636
- Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
8752
+ Revoke a single trusted device by id (authenticated).
8637
8753
 
8638
8754
  **list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
8639
- Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
8755
+ Revoke a single trusted device by id (authenticated).
8640
8756
 
8641
8757
  **create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
8642
- Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
8758
+ Revoke a single trusted device by id (authenticated).
8643
8759
 
8644
8760
  **update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
8645
- Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
8761
+ Revoke a single trusted device by id (authenticated).
8646
8762
 
8647
8763
  **remove**(collectionId: string, authKitId: string) → `Promise<void>`
8648
- Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
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
  },
@@ -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 `entryType`, along with custom metadata dimensions like `group`, `placement`, `pagePath`, and `qrCodeId`.
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 `scanMethod`, along with custom metadata dimensions like `group`, `tag`, and campaign extras.
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
 
@@ -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. |