@proveanything/smartlinks 1.15.11 → 1.15.14
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 +110 -13
- package/dist/api/authKit.js +172 -18
- package/dist/docs/API_SUMMARY.md +148 -20
- package/dist/docs/analytics-metadata-conventions.md +4 -0
- package/dist/docs/analytics.md +37 -3
- package/dist/docs/auth-kit.md +132 -0
- package/dist/openapi.yaml +435 -2
- package/dist/types/analytics.d.ts +37 -2
- package/dist/types/authKit.d.ts +99 -0
- package/dist/types/collection.d.ts +23 -0
- package/docs/API_SUMMARY.md +148 -20
- package/docs/analytics-metadata-conventions.md +4 -0
- package/docs/analytics.md +37 -3
- package/docs/auth-kit.md +132 -0
- package/openapi.yaml +435 -2
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.15.
|
|
3
|
+
Version: 1.15.14 | Generated: 2026-07-29T07:45:17.540Z
|
|
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
|
|
|
@@ -3093,6 +3104,70 @@ interface AppleLoginOptions {
|
|
|
3093
3104
|
* these again, and never inside the token. Forwarded so the server can persist the
|
|
3094
3105
|
* display name on first account creation. Treated as untrusted (never used for identity).
|
|
3095
3106
|
userInfo?: { email?: string; name?: string }
|
|
3107
|
+
* A previously-issued trusted-device token (from a prior MFA `challenge/verify`
|
|
3108
|
+
* or `challenge/recovery-code` response). If still valid, the server skips any
|
|
3109
|
+
* step-up challenge for this login. See `SDK_AUTHKIT_MFA_UPDATE.md` §3.
|
|
3110
|
+
trustedDeviceToken?: string
|
|
3111
|
+
}
|
|
3112
|
+
```
|
|
3113
|
+
|
|
3114
|
+
**MfaRequiredDetails** (interface)
|
|
3115
|
+
```typescript
|
|
3116
|
+
interface MfaRequiredDetails {
|
|
3117
|
+
mfaSessionToken: string
|
|
3118
|
+
availableFactors: Array<'email' | 'sms'>
|
|
3119
|
+
preferredFactor: 'email' | 'sms'
|
|
3120
|
+
maskedDestinations: { email?: string; sms?: string }
|
|
3121
|
+
}
|
|
3122
|
+
```
|
|
3123
|
+
|
|
3124
|
+
**MfaChallengeSendResponse** (interface)
|
|
3125
|
+
```typescript
|
|
3126
|
+
interface MfaChallengeSendResponse {
|
|
3127
|
+
success: true
|
|
3128
|
+
factor: 'email' | 'sms'
|
|
3129
|
+
destination: string
|
|
3130
|
+
expiresAt: string
|
|
3131
|
+
}
|
|
3132
|
+
```
|
|
3133
|
+
|
|
3134
|
+
**MfaEnrollSendResponse** (interface)
|
|
3135
|
+
```typescript
|
|
3136
|
+
interface MfaEnrollSendResponse {
|
|
3137
|
+
mfaSessionToken: string
|
|
3138
|
+
destination: string
|
|
3139
|
+
expiresAt: string
|
|
3140
|
+
}
|
|
3141
|
+
```
|
|
3142
|
+
|
|
3143
|
+
**MfaEnrolledResponse** (interface)
|
|
3144
|
+
```typescript
|
|
3145
|
+
interface MfaEnrolledResponse {
|
|
3146
|
+
enrolled: true
|
|
3147
|
+
factor: 'email' | 'sms'
|
|
3148
|
+
}
|
|
3149
|
+
```
|
|
3150
|
+
|
|
3151
|
+
**MfaFactorsResponse** (interface)
|
|
3152
|
+
```typescript
|
|
3153
|
+
interface MfaFactorsResponse {
|
|
3154
|
+
enrolledFactors: {
|
|
3155
|
+
email?: { enrolledAt: string; destination: string }
|
|
3156
|
+
sms?: { enrolledAt: string; destination: string }
|
|
3157
|
+
}
|
|
3158
|
+
recoveryCodesRemaining: number
|
|
3159
|
+
mfaEnabledForClient: boolean
|
|
3160
|
+
}
|
|
3161
|
+
```
|
|
3162
|
+
|
|
3163
|
+
**TrustedDevice** (interface)
|
|
3164
|
+
```typescript
|
|
3165
|
+
interface TrustedDevice {
|
|
3166
|
+
id: string
|
|
3167
|
+
label: string | null
|
|
3168
|
+
createdAtMs: number
|
|
3169
|
+
lastUsedAtMs: number
|
|
3170
|
+
expiresAtMs: number
|
|
3096
3171
|
}
|
|
3097
3172
|
```
|
|
3098
3173
|
|
|
@@ -3345,6 +3420,8 @@ interface AuthKitConfig {
|
|
|
3345
3420
|
|
|
3346
3421
|
**AuthKitErrorCode** = ``
|
|
3347
3422
|
|
|
3423
|
+
**MfaErrorCode** = ``
|
|
3424
|
+
|
|
3348
3425
|
**VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
|
|
3349
3426
|
|
|
3350
3427
|
### batch
|
|
@@ -3705,6 +3782,21 @@ interface Collection {
|
|
|
3705
3782
|
portalUrl?: string // URL for the collection's portal (if applicable)
|
|
3706
3783
|
allowAutoGenerateClaims?: boolean
|
|
3707
3784
|
defaultAuthKitId: string // default auth kit for this collection, used for auth
|
|
3785
|
+
admin?: {
|
|
3786
|
+
* Redirect behavior for plain collection-level scans (a short link with
|
|
3787
|
+
* no product/serial code in the path, e.g. `https://.../c/shortId`).
|
|
3788
|
+
* Unset means such links always go to the normal collection page.
|
|
3789
|
+
redirect?: CollectionRedirectConfig
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
```
|
|
3793
|
+
|
|
3794
|
+
**CollectionRedirectConfig** (interface)
|
|
3795
|
+
```typescript
|
|
3796
|
+
interface CollectionRedirectConfig {
|
|
3797
|
+
mode: 'fixed' | 'dynamic' | 'deep'
|
|
3798
|
+
url?: string
|
|
3799
|
+
template?: string
|
|
3708
3800
|
}
|
|
3709
3801
|
```
|
|
3710
3802
|
|
|
@@ -8543,20 +8635,20 @@ Gets current account information for the logged in user. Returns user, owner, ac
|
|
|
8543
8635
|
|
|
8544
8636
|
### authKit
|
|
8545
8637
|
|
|
8546
|
-
**login**(clientId: string, email: string, password: string) → `Promise<AuthLoginResponse>`
|
|
8547
|
-
Login with email + password (public).
|
|
8638
|
+
**login**(clientId: string, email: string, password: string, trustedDeviceToken?: string) → `Promise<AuthLoginResponse>`
|
|
8639
|
+
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.
|
|
8548
8640
|
|
|
8549
8641
|
**register**(clientId: string, data: { email: string; password: string; displayName?: string; accountData?: Record<string, any> }) → `Promise<AuthLoginResponse>`
|
|
8550
|
-
Register a new user (public).
|
|
8642
|
+
Register a new user (public). Not gated by step-up MFA — a brand-new user has no enrolled factors yet, so there's nothing to challenge against.
|
|
8551
8643
|
|
|
8552
|
-
**googleLogin**(clientId: string, idToken: string) → `Promise<AuthLoginResponse>`
|
|
8553
|
-
Google OAuth login via ID token (public).
|
|
8644
|
+
**googleLogin**(clientId: string, idToken: string, trustedDeviceToken?: string) → `Promise<AuthLoginResponse>`
|
|
8645
|
+
Google OAuth login via ID token (public). Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape. {@link mfaChallengeVerify}/{@link mfaRecoveryCode} (with `trustDevice: true`) to skip the challenge on this device, same as {@link login}.
|
|
8554
8646
|
|
|
8555
8647
|
**googleCodeLogin**(clientId: string, code: string, redirectUri: string) → `Promise<AuthLoginResponse>`
|
|
8556
8648
|
Google OAuth login via server-side authorization code (public).
|
|
8557
8649
|
|
|
8558
8650
|
**appleLogin**(clientId: string, identityToken: string, opts?: AppleLoginOptions) → `Promise<AuthLoginResponse>`
|
|
8559
|
-
Sign in with Apple via an Apple identity token (public). Mirrors {@link googleLogin}. On success the returned bearer token is stored automatically and the cache is invalidated. Notable error codes (thrown as `SmartlinksApiError`, read via `err.errorCode`): - `MISSING_APPLE_TOKEN` (400), `APPLE_AUTH_NOT_CONFIGURED` (400), `INVALID_APPLE_TOKEN` (401), `APPLE_AUTH_FAILED` (500) - `ACCOUNT_EXISTS_UNVERIFIED` (409) — an unverified account already owns this email; the server refuses to silently link. `err.details.requiresEmailVerification` is `true`. Recoverable: the user should sign in with their password (or reset it), then link Apple from settings. **The same 409 can now come back from {@link googleLogin}** under the shared verified-to-verified linking policy.
|
|
8651
|
+
Sign in with Apple via an Apple identity token (public). Mirrors {@link googleLogin}. On success the returned bearer token is stored automatically and the cache is invalidated. Notable error codes (thrown as `SmartlinksApiError`, read via `err.errorCode`): - `MISSING_APPLE_TOKEN` (400), `APPLE_AUTH_NOT_CONFIGURED` (400), `INVALID_APPLE_TOKEN` (401), `APPLE_AUTH_FAILED` (500) - `ACCOUNT_EXISTS_UNVERIFIED` (409) — an unverified account already owns this email; the server refuses to silently link. `err.details.requiresEmailVerification` is `true`. Recoverable: the user should sign in with their password (or reset it), then link Apple from settings. **The same 409 can now come back from {@link googleLogin}** under the shared verified-to-verified linking policy. Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape. Pass `opts.trustedDeviceToken` to skip the challenge on a recognized device.
|
|
8560
8652
|
|
|
8561
8653
|
**refreshToken**(clientId: string, refreshToken: string) → `Promise<RefreshResponse>`
|
|
8562
8654
|
Exchange a refresh token for a fresh access token (public — the refresh token IS the credential). **Native sessions only**; refresh tokens are issued only when the host opted in via `initializeApi({ platform: 'native' })`. On success the new access token is stored automatically (`setBearerToken`). The returned `refreshToken` is **rotated** — the caller must persist it and discard the old one before refreshing again. ⚠️ **Single-use, no retry, serialize calls.** This method issues exactly one request and never retries: replaying a consumed refresh token triggers `REFRESH_TOKEN_REUSE_DETECTED` (the whole session family is revoked). The caller is responsible for ensuring only one refresh is in flight at a time (e.g. across tabs or resume events). Errors (thrown as `SmartlinksApiError`, read via `err.errorCode`): `MISSING_REFRESH_TOKEN` (400), `INVALID_REFRESH_TOKEN` (401), `REFRESH_TOKEN_REUSE_DETECTED` (401) — the last two mean a hard logout.
|
|
@@ -8567,26 +8659,26 @@ Revoke a refresh token's entire family server-side (that device's whole rotation
|
|
|
8567
8659
|
**sendMagicLink**(clientId: string, data: { email: string; redirectUrl: string; accountData?: Record<string, any> }) → `Promise<MagicLinkSendResponse>`
|
|
8568
8660
|
Send a magic link email to the user (public).
|
|
8569
8661
|
|
|
8570
|
-
**verifyMagicLink**(clientId: string, token: string) → `Promise<MagicLinkVerifyResponse>`
|
|
8571
|
-
Verify a magic link token and authenticate/create the user (public).
|
|
8662
|
+
**verifyMagicLink**(clientId: string, token: string, trustedDeviceToken?: string) → `Promise<MagicLinkVerifyResponse>`
|
|
8663
|
+
Verify a magic link token and authenticate/create the user (public). Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape.
|
|
8572
8664
|
|
|
8573
8665
|
**sendPhoneCode**(clientId: string, phoneNumber: string) → `Promise<PhoneSendCodeResponse>`
|
|
8574
8666
|
Send phone verification code (public).
|
|
8575
8667
|
|
|
8576
|
-
**verifyPhoneCode**(clientId: string, phoneNumber: string, code: string) → `Promise<PhoneVerifyResponse>`
|
|
8577
|
-
Verify phone verification code (public).
|
|
8668
|
+
**verifyPhoneCode**(clientId: string, phoneNumber: string, code: string, trustedDeviceToken?: string) → `Promise<PhoneVerifyResponse>`
|
|
8669
|
+
Verify phone verification code (public). Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape.
|
|
8578
8670
|
|
|
8579
8671
|
**sendWhatsApp**(clientId: string, body: SendWhatsAppRequest = {}) → `Promise<SendWhatsAppResponse>`
|
|
8580
8672
|
Send a WhatsApp verification deep-link (public).
|
|
8581
8673
|
|
|
8582
8674
|
**verifyWhatsApp**(clientId: string, token: string, phoneNumber: string) → `Promise<VerifyWhatsAppResponse>`
|
|
8583
|
-
Manually verify WhatsApp token if inbound webhook path is unavailable (legacy/public fallback).
|
|
8675
|
+
Manually verify WhatsApp token if inbound webhook path is unavailable (legacy/public fallback). Not gated by step-up MFA — this endpoint only confirms the code, it never issues a session/bearer token, so there is nothing to challenge. {@link exchangeWhatsAppSession} is the WhatsApp method that's gated.
|
|
8584
8676
|
|
|
8585
8677
|
**getWhatsAppStatus**(clientId: string, token: string) → `Promise<WhatsAppStatusResponse>`
|
|
8586
8678
|
Poll WhatsApp verification status for a token (public).
|
|
8587
8679
|
|
|
8588
|
-
**exchangeWhatsAppSession**(clientId: string, token: string, sessionKey: string) → `Promise<ExchangeWhatsAppSessionResponse>`
|
|
8589
|
-
Exchange a verified WhatsApp token for an Auth Kit session (public).
|
|
8680
|
+
**exchangeWhatsAppSession**(clientId: string, token: string, sessionKey: string, trustedDeviceToken?: string) → `Promise<ExchangeWhatsAppSessionResponse>`
|
|
8681
|
+
Exchange a verified WhatsApp token for an Auth Kit session (public). Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape. This is the WhatsApp method that needs `trustedDeviceToken`, not {@link verifyWhatsApp} (which never issues a session).
|
|
8590
8682
|
|
|
8591
8683
|
**sendSmsVerify**(clientId: string, body: SendSmsVerifyRequest) → `Promise<SendSmsVerifyResponse>`
|
|
8592
8684
|
Send an SMS click-to-verify link (public).
|
|
@@ -8636,23 +8728,59 @@ Update the authenticated user's profile and replace the bearer token when refres
|
|
|
8636
8728
|
**deleteAccount**(clientId: string, password: string, confirmText: string) → `Promise<SuccessResponse>`
|
|
8637
8729
|
Update the authenticated user's profile and replace the bearer token when refreshed claims are returned.
|
|
8638
8730
|
|
|
8731
|
+
**mfaChallengeSend**(clientId: string, mfaSessionToken: string, factor: 'email' | 'sms') → `Promise<MfaChallengeSendResponse>`
|
|
8732
|
+
Send (or resend) an MFA challenge code to the given factor (public).
|
|
8733
|
+
|
|
8734
|
+
**mfaChallengeVerify**(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string) → `Promise<MfaFinalizeResponse>`
|
|
8735
|
+
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.
|
|
8736
|
+
|
|
8737
|
+
**mfaRecoveryCode**(clientId: string, mfaSessionToken: string, code: string, trustDevice?: boolean, deviceLabel?: string) → `Promise<MfaFinalizeResponse>`
|
|
8738
|
+
Finalize a challenged login using a single-use recovery code instead of a sent code (public). Same finalize-session behaviour as {@link mfaChallengeVerify}.
|
|
8739
|
+
|
|
8740
|
+
**getMfaFactors**(clientId: string) → `Promise<MfaFactorsResponse>`
|
|
8741
|
+
List enrolled factors and recovery-code count for the current user (authenticated).
|
|
8742
|
+
|
|
8743
|
+
**enrollEmailMfa**(clientId: string) → `Promise<MfaEnrollSendResponse>`
|
|
8744
|
+
Begin email-factor enrollment; sends a code to the account's existing email (authenticated).
|
|
8745
|
+
|
|
8746
|
+
**confirmEmailMfa**(clientId: string, mfaSessionToken: string, code: string) → `Promise<MfaEnrolledResponse>`
|
|
8747
|
+
Confirm email-factor enrollment with the code sent by {@link enrollEmailMfa} (authenticated).
|
|
8748
|
+
|
|
8749
|
+
**enrollSmsMfa**(clientId: string, phoneNumber: string) → `Promise<MfaEnrollSendResponse>`
|
|
8750
|
+
Begin SMS-factor enrollment; sends a code to the given phone number (authenticated).
|
|
8751
|
+
|
|
8752
|
+
**confirmSmsMfa**(clientId: string, mfaSessionToken: string, code: string) → `Promise<MfaEnrolledResponse>`
|
|
8753
|
+
Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated).
|
|
8754
|
+
|
|
8755
|
+
**generateMfaRecoveryCodes**(clientId: string, password: string) → `Promise<`
|
|
8756
|
+
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.
|
|
8757
|
+
|
|
8758
|
+
**removeMfaFactor**(clientId: string, factor: 'email' | 'sms', password: string) → `Promise<SuccessResponse>`
|
|
8759
|
+
Remove an enrolled MFA factor (authenticated). Server route is DELETE with a body.
|
|
8760
|
+
|
|
8761
|
+
**listTrustedDevices**(clientId: string) → `Promise<`
|
|
8762
|
+
List devices trusted to skip MFA challenges for the current user (authenticated).
|
|
8763
|
+
|
|
8764
|
+
**revokeTrustedDevice**(clientId: string, id: string) → `Promise<SuccessResponse>`
|
|
8765
|
+
Revoke a single trusted device by id (authenticated).
|
|
8766
|
+
|
|
8639
8767
|
**load**(authKitId: string) → `Promise<AuthKitConfig>`
|
|
8640
|
-
|
|
8768
|
+
Revoke a single trusted device by id (authenticated).
|
|
8641
8769
|
|
|
8642
8770
|
**get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
|
|
8643
|
-
|
|
8771
|
+
Revoke a single trusted device by id (authenticated).
|
|
8644
8772
|
|
|
8645
8773
|
**list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
|
|
8646
|
-
|
|
8774
|
+
Revoke a single trusted device by id (authenticated).
|
|
8647
8775
|
|
|
8648
8776
|
**create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
|
|
8649
|
-
|
|
8777
|
+
Revoke a single trusted device by id (authenticated).
|
|
8650
8778
|
|
|
8651
8779
|
**update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
|
|
8652
|
-
|
|
8780
|
+
Revoke a single trusted device by id (authenticated).
|
|
8653
8781
|
|
|
8654
8782
|
**remove**(collectionId: string, authKitId: string) → `Promise<void>`
|
|
8655
|
-
|
|
8783
|
+
Revoke a single trusted device by id (authenticated).
|
|
8656
8784
|
|
|
8657
8785
|
### batch
|
|
8658
8786
|
|
|
@@ -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/dist/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/dist/docs/auth-kit.md
CHANGED
|
@@ -226,6 +226,7 @@ const session = await authKit.appleLogin(clientId, appleIdentityToken, {
|
|
|
226
226
|
// All optional:
|
|
227
227
|
nonce, // raw nonce, if you used nonce binding
|
|
228
228
|
userInfo: { name, email }, // first authorization callback ONLY — Apple never resends it
|
|
229
|
+
trustedDeviceToken, // skip an MFA step-up challenge on a recognized device — see "Step-up MFA" below
|
|
229
230
|
});
|
|
230
231
|
// session.isNewUser and session.expiresAt (ms epoch) are populated by this endpoint.
|
|
231
232
|
```
|
|
@@ -304,6 +305,137 @@ clearPersistedTokens();
|
|
|
304
305
|
|
|
305
306
|
---
|
|
306
307
|
|
|
308
|
+
## Step-up MFA (Phase 1)
|
|
309
|
+
|
|
310
|
+
Backend-only for now — **no admin-console UI and no challenge UI ship with this pass.**
|
|
311
|
+
Factors: **email OTP, SMS OTP, recovery codes**. WhatsApp OTP, TOTP, and passkeys as
|
|
312
|
+
*factors* are deferred to a later phase; don't build against them yet.
|
|
313
|
+
|
|
314
|
+
The step-up gate applies to **every login method that issues a session**:
|
|
315
|
+
|
|
316
|
+
| Method | Gated? |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `login()` | ✅ |
|
|
319
|
+
| `googleLogin()` | ✅ |
|
|
320
|
+
| `appleLogin()` | ✅ — pass `opts.trustedDeviceToken` |
|
|
321
|
+
| `verifyPhoneCode()` | ✅ |
|
|
322
|
+
| `verifyMagicLink()` | ✅ |
|
|
323
|
+
| `exchangeWhatsAppSession()` | ✅ — **this is the WhatsApp method that needs a trusted-device token, not `verifyWhatsApp()`** |
|
|
324
|
+
| `verifyWhatsApp()` | ❌ — only confirms the code, never issues a session, so there's nothing to gate |
|
|
325
|
+
| `register()` | ❌ — a brand-new user has no enrolled factors to challenge against |
|
|
326
|
+
| `googleCodeLogin()` | ❌ — no `/auth/google-code` route exists server-side |
|
|
327
|
+
|
|
328
|
+
All six gated methods accept an optional `trustedDeviceToken` param (the last positional
|
|
329
|
+
argument, or `opts.trustedDeviceToken` for `appleLogin()`) — same purpose everywhere: skip
|
|
330
|
+
the challenge on a device the user already verified.
|
|
331
|
+
|
|
332
|
+
### Handling `MFA_REQUIRED`
|
|
333
|
+
|
|
334
|
+
Every gated method's return type is unchanged. When the client's MFA policy requires a
|
|
335
|
+
step-up, the server returns 403 instead of a session, and the method throws — identically
|
|
336
|
+
for all six:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { authKit, SmartlinksApiError } from '@proveanything/smartlinks';
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const session = await authKit.login(clientId, email, password);
|
|
343
|
+
// logged in, no MFA required
|
|
344
|
+
} catch (err) {
|
|
345
|
+
if (err instanceof SmartlinksApiError && err.errorCode === 'MFA_REQUIRED') {
|
|
346
|
+
const { mfaSessionToken, availableFactors, preferredFactor, maskedDestinations } = err.details!;
|
|
347
|
+
// → route to a challenge UI; call authKit.mfaChallengeSend(clientId, mfaSessionToken, preferredFactor)
|
|
348
|
+
} else {
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Completing the challenge
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
// Send (or resend, or switch factor) a code on the same mfaSessionToken
|
|
358
|
+
const sent = await authKit.mfaChallengeSend(clientId, mfaSessionToken, 'sms');
|
|
359
|
+
// sent.destination is masked, e.g. "+1******1234"
|
|
360
|
+
|
|
361
|
+
// Verify the code — behaves like login() on success: bearer token is adopted automatically
|
|
362
|
+
const session = await authKit.mfaChallengeVerify(clientId, mfaSessionToken, '123456', /* trustDevice */ true, 'My Laptop');
|
|
363
|
+
|
|
364
|
+
// Or finalize with a single-use recovery code instead
|
|
365
|
+
const session = await authKit.mfaRecoveryCode(clientId, mfaSessionToken, recoveryCode);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`mfaSessionToken` is short-lived (10 min) and single-use — burned on 5 failed attempts
|
|
369
|
+
(`MFA_TOO_MANY_ATTEMPTS`) or on success. Once burned/expired, there's no "resend within the
|
|
370
|
+
same session" — the caller must restart from `login()`.
|
|
371
|
+
|
|
372
|
+
### Trusted devices — "don't ask again on this device"
|
|
373
|
+
|
|
374
|
+
There's no fingerprinting involved: a "recognized device" is purely "the caller presented a
|
|
375
|
+
valid, unexpired, unrevoked `trustedDeviceToken`". Handle it like the native refresh token
|
|
376
|
+
above — same secure storage, same persist-before-next-call discipline:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
// 1) Persist trustedDeviceToken from a successful challenge (trustDevice: true)
|
|
380
|
+
persistTrustedDeviceToken(session.trustedDeviceToken, session.trustedDeviceExpiresAt);
|
|
381
|
+
|
|
382
|
+
// 2) Send it on every subsequent call to ANY of the six gated methods — if still valid,
|
|
383
|
+
// the challenge is skipped entirely. It isn't tied to which method the user challenged
|
|
384
|
+
// through, so "remember this device" works no matter which login method they pick next.
|
|
385
|
+
const session = await authKit.login(clientId, email, password, storedTrustedDeviceToken);
|
|
386
|
+
const session = await authKit.googleLogin(clientId, idToken, storedTrustedDeviceToken);
|
|
387
|
+
const session = await authKit.exchangeWhatsAppSession(clientId, waToken, sessionKey, storedTrustedDeviceToken);
|
|
388
|
+
// ...and so on for appleLogin (via opts), verifyPhoneCode, verifyMagicLink.
|
|
389
|
+
// If it's revoked/expired, the server silently falls back to requiring a fresh challenge —
|
|
390
|
+
// the method just throws MFA_REQUIRED again, no special-case handling needed.
|
|
391
|
+
|
|
392
|
+
// 3) Let users audit/revoke devices from a Settings screen
|
|
393
|
+
const { devices } = await authKit.listTrustedDevices(clientId);
|
|
394
|
+
await authKit.revokeTrustedDevice(clientId, deviceId);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Factor management (Settings → Security)
|
|
398
|
+
|
|
399
|
+
Authenticated (bearer token) — same `mfaSessionToken` + code mechanism as login challenges,
|
|
400
|
+
just against a `purpose: 'enroll'` challenge instead of `'login'`.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// Enroll email (uses the account's existing email)
|
|
404
|
+
const { mfaSessionToken } = await authKit.enrollEmailMfa(clientId);
|
|
405
|
+
await authKit.confirmEmailMfa(clientId, mfaSessionToken, code);
|
|
406
|
+
|
|
407
|
+
// Enroll SMS
|
|
408
|
+
const { mfaSessionToken: smsToken } = await authKit.enrollSmsMfa(clientId, '+61400000000');
|
|
409
|
+
await authKit.confirmSmsMfa(clientId, smsToken, code);
|
|
410
|
+
|
|
411
|
+
// Recovery codes — plaintext, shown exactly once; nothing else in the API returns them again
|
|
412
|
+
const { recoveryCodes } = await authKit.generateMfaRecoveryCodes(clientId, password);
|
|
413
|
+
|
|
414
|
+
// Inspect / remove
|
|
415
|
+
const factors = await authKit.getMfaFactors(clientId);
|
|
416
|
+
// factors: { enrolledFactors: { email?, sms? }, recoveryCodesRemaining, mfaEnabledForClient }
|
|
417
|
+
await authKit.removeMfaFactor(clientId, 'sms', password);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
> Enrollment endpoints don't currently check the client's `mfa.mode` — enrolling is possible
|
|
421
|
+
> even when MFA is configured `'off'` for a client. Those factors simply won't be challenged
|
|
422
|
+
> at login until an admin turns the mode on (via the existing admin AuthKit config update,
|
|
423
|
+
> no new admin route in this pass).
|
|
424
|
+
|
|
425
|
+
### MFA error codes
|
|
426
|
+
|
|
427
|
+
| `errorCode` | HTTP | Meaning / client action |
|
|
428
|
+
|---|---|---|
|
|
429
|
+
| `MFA_REQUIRED` | 403 | From any of the six gated login methods — see above |
|
|
430
|
+
| `INVALID_MFA_CODE` | 401 | Wrong code — let the user retry (attempts are capped) |
|
|
431
|
+
| `MFA_TOO_MANY_ATTEMPTS` | 429 | 5 wrong attempts — the challenge is burned; restart from `login()` |
|
|
432
|
+
| `MFA_FACTOR_NOT_ENROLLED` | 400 | Requested factor isn't enrolled — programming error if the UI only offers `availableFactors` |
|
|
433
|
+
| `MFA_SESSION_EXPIRED` | 401 | `mfaSessionToken` past its 10-minute TTL — restart from `login()` |
|
|
434
|
+
| `MFA_SESSION_INVALID` | 401 | Unknown/wrong-client/already-consumed token, or (on enroll confirm) mismatched user |
|
|
435
|
+
| `RECOVERY_CODE_INVALID` | 401 | Wrong or already-used recovery code |
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
307
439
|
## Profile management
|
|
308
440
|
|
|
309
441
|
```ts
|