@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/docs/auth-kit.md CHANGED
@@ -304,6 +304,116 @@ clearPersistedTokens();
304
304
 
305
305
  ---
306
306
 
307
+ ## Step-up MFA (Phase 1)
308
+
309
+ Backend-only for now — **no admin-console UI and no challenge UI ship with this pass.**
310
+ Factors: **email OTP, SMS OTP, recovery codes**. WhatsApp OTP, TOTP, and passkeys are
311
+ deferred to a later phase; don't build against them yet.
312
+
313
+ The step-up gate is wired into **`login()` only** — `register`, `verifyPhoneCode`, and
314
+ `verifyMagicLink` never return `MFA_REQUIRED`, regardless of client config.
315
+
316
+ ### Handling `MFA_REQUIRED` from `login()`
317
+
318
+ `login()`'s return type is unchanged. When the client's MFA policy requires a step-up,
319
+ the server returns 403 instead of a session, and `login()` throws:
320
+
321
+ ```ts
322
+ import { authKit, SmartlinksApiError } from '@proveanything/smartlinks';
323
+
324
+ try {
325
+ const session = await authKit.login(clientId, email, password);
326
+ // logged in, no MFA required
327
+ } catch (err) {
328
+ if (err instanceof SmartlinksApiError && err.errorCode === 'MFA_REQUIRED') {
329
+ const { mfaSessionToken, availableFactors, preferredFactor, maskedDestinations } = err.details!;
330
+ // → route to a challenge UI; call authKit.mfaChallengeSend(clientId, mfaSessionToken, preferredFactor)
331
+ } else {
332
+ throw err;
333
+ }
334
+ }
335
+ ```
336
+
337
+ ### Completing the challenge
338
+
339
+ ```ts
340
+ // Send (or resend, or switch factor) a code on the same mfaSessionToken
341
+ const sent = await authKit.mfaChallengeSend(clientId, mfaSessionToken, 'sms');
342
+ // sent.destination is masked, e.g. "+1******1234"
343
+
344
+ // Verify the code — behaves like login() on success: bearer token is adopted automatically
345
+ const session = await authKit.mfaChallengeVerify(clientId, mfaSessionToken, '123456', /* trustDevice */ true, 'My Laptop');
346
+
347
+ // Or finalize with a single-use recovery code instead
348
+ const session = await authKit.mfaRecoveryCode(clientId, mfaSessionToken, recoveryCode);
349
+ ```
350
+
351
+ `mfaSessionToken` is short-lived (10 min) and single-use — burned on 5 failed attempts
352
+ (`MFA_TOO_MANY_ATTEMPTS`) or on success. Once burned/expired, there's no "resend within the
353
+ same session" — the caller must restart from `login()`.
354
+
355
+ ### Trusted devices — "don't ask again on this device"
356
+
357
+ There's no fingerprinting involved: a "recognized device" is purely "the caller presented a
358
+ valid, unexpired, unrevoked `trustedDeviceToken`". Handle it like the native refresh token
359
+ above — same secure storage, same persist-before-next-call discipline:
360
+
361
+ ```ts
362
+ // 1) Persist trustedDeviceToken from a successful challenge (trustDevice: true)
363
+ persistTrustedDeviceToken(session.trustedDeviceToken, session.trustedDeviceExpiresAt);
364
+
365
+ // 2) Send it on every subsequent login() — if still valid, the challenge is skipped entirely
366
+ const session = await authKit.login(clientId, email, password, storedTrustedDeviceToken);
367
+ // If it's revoked/expired, the server silently falls back to requiring a fresh challenge —
368
+ // login() just throws MFA_REQUIRED again, no special-case handling needed.
369
+
370
+ // 3) Let users audit/revoke devices from a Settings screen
371
+ const { devices } = await authKit.listTrustedDevices(clientId);
372
+ await authKit.revokeTrustedDevice(clientId, deviceId);
373
+ ```
374
+
375
+ ### Factor management (Settings → Security)
376
+
377
+ Authenticated (bearer token) — same `mfaSessionToken` + code mechanism as login challenges,
378
+ just against a `purpose: 'enroll'` challenge instead of `'login'`.
379
+
380
+ ```ts
381
+ // Enroll email (uses the account's existing email)
382
+ const { mfaSessionToken } = await authKit.enrollEmailMfa(clientId);
383
+ await authKit.confirmEmailMfa(clientId, mfaSessionToken, code);
384
+
385
+ // Enroll SMS
386
+ const { mfaSessionToken: smsToken } = await authKit.enrollSmsMfa(clientId, '+61400000000');
387
+ await authKit.confirmSmsMfa(clientId, smsToken, code);
388
+
389
+ // Recovery codes — plaintext, shown exactly once; nothing else in the API returns them again
390
+ const { recoveryCodes } = await authKit.generateMfaRecoveryCodes(clientId, password);
391
+
392
+ // Inspect / remove
393
+ const factors = await authKit.getMfaFactors(clientId);
394
+ // factors: { enrolledFactors: { email?, sms? }, recoveryCodesRemaining, mfaEnabledForClient }
395
+ await authKit.removeMfaFactor(clientId, 'sms', password);
396
+ ```
397
+
398
+ > Enrollment endpoints don't currently check the client's `mfa.mode` — enrolling is possible
399
+ > even when MFA is configured `'off'` for a client. Those factors simply won't be challenged
400
+ > at login until an admin turns the mode on (via the existing admin AuthKit config update,
401
+ > no new admin route in this pass).
402
+
403
+ ### MFA error codes
404
+
405
+ | `errorCode` | HTTP | Meaning / client action |
406
+ |---|---|---|
407
+ | `MFA_REQUIRED` | 403 | From `login()` only — see above |
408
+ | `INVALID_MFA_CODE` | 401 | Wrong code — let the user retry (attempts are capped) |
409
+ | `MFA_TOO_MANY_ATTEMPTS` | 429 | 5 wrong attempts — the challenge is burned; restart from `login()` |
410
+ | `MFA_FACTOR_NOT_ENROLLED` | 400 | Requested factor isn't enrolled — programming error if the UI only offers `availableFactors` |
411
+ | `MFA_SESSION_EXPIRED` | 401 | `mfaSessionToken` past its 10-minute TTL — restart from `login()` |
412
+ | `MFA_SESSION_INVALID` | 401 | Unknown/wrong-client/already-consumed token, or (on enroll confirm) mismatched user |
413
+ | `RECOVERY_CODE_INVALID` | 401 | Wrong or already-used recovery code |
414
+
415
+ ---
416
+
307
417
  ## Profile management
308
418
 
309
419
  ```ts
@@ -163,8 +163,7 @@ const showFakeWarning = await validateCondition({
163
163
  - `isAuthentic` — passes when `itemContext.isAuthentic` is true (`valid`/`rescan`)
164
164
  - `notAuthentic` — passes when an `itemContext` was resolved but isn't authentic (`invalid`/`not-found`/`error`)
165
165
  - `invalidProof` — passes specifically when resolution was attempted and came back `invalid` or `not-found` — the "someone scanned a fake tag / typed a bad serial" case, as distinct from `noProof` ("nothing was on the URL to check at all")
166
- - `isFirstScan` — the single "this is good, show the full experience" check: authentic AND this is the first time it's been seen (`status === 'valid'`)
167
- - `isRescan` — authentic but a duplicate/replayed tap (`status === 'rescan'`) — use this to suppress "first scan" celebration UX without treating the tag as fake; `isAuthentic` alone is true for both `isFirstScan` and `isRescan` cases
166
+ - `isRescan` — authentic but a duplicate/replayed tap (`status === 'rescan'`) — use this to suppress "first scan" celebration UX without treating the tag as fake; `isAuthentic` alone is true for both a fresh tap and a rescan
168
167
 
169
168
  ## Deprecated
170
169
 
package/docs/utils.md CHANGED
@@ -530,9 +530,9 @@ await utils.validateCondition({
530
530
  Status types:
531
531
  - Claim/ownership (reads `proof`): `'isClaimable'`, `'notClaimable'`, `'isVirtual'`, `'notVirtual'`
532
532
  - Presence (reads `proof`): `'hasProof'`, `'noProof'` — `noProof` only means *nothing was attempted*, not that an attempt failed
533
- - Authenticity (reads `itemContext`): `'isAuthentic'`, `'notAuthentic'`, `'invalidProof'`, `'isFirstScan'`, `'isRescan'`
533
+ - Authenticity (reads `itemContext`): `'isAuthentic'`, `'notAuthentic'`, `'invalidProof'`, `'isRescan'`
534
534
  - `invalidProof` is specifically "an identifier was passed and resolution came back `invalid`/`not-found`", distinct from `noProof`'s "nothing on the URL to check at all"
535
- - `isAuthentic` is true for both a fresh tap and a rescan. Use `isFirstScan` (authentic AND `status === 'valid'`) as the single "this is good, show the full experience" check, and `isRescan` (authentic but `status === 'rescan'`) to suppress "first scan" celebration UX without treating the tag as fake
535
+ - `isAuthentic` is true for both a fresh tap and a rescan. Use `isRescan` (authentic but `status === 'rescan'`) to suppress "first scan" celebration UX without treating the tag as fake
536
536
 
537
537
  #### Version Conditions
538
538
  For A/B testing or versioned content:
package/openapi.yaml CHANGED
@@ -8077,7 +8077,7 @@ paths:
8077
8077
  get:
8078
8078
  tags:
8079
8079
  - authKit
8080
- summary: authKit.load
8080
+ summary: Revoke a single trusted device by id (authenticated).
8081
8081
  operationId: authKit_load
8082
8082
  security: []
8083
8083
  parameters:
@@ -8970,6 +8970,285 @@ paths:
8970
8970
  application/json:
8971
8971
  schema:
8972
8972
  $ref: "#/components/schemas/UpsertContactRequest"
8973
+ /authkit/{clientId}/mfa/challenge/recovery-code:
8974
+ post:
8975
+ tags:
8976
+ - authKit
8977
+ summary: Finalize a challenged login using a single-use recovery code instead of a sent code (public).
8978
+ operationId: authKit_mfaRecoveryCode
8979
+ security: []
8980
+ parameters:
8981
+ - name: clientId
8982
+ in: path
8983
+ required: true
8984
+ schema:
8985
+ type: string
8986
+ responses:
8987
+ 200:
8988
+ description: Success
8989
+ content:
8990
+ application/json:
8991
+ schema:
8992
+ $ref: "#/components/schemas/MfaFinalizeResponse"
8993
+ 400:
8994
+ description: Bad request
8995
+ 401:
8996
+ description: Unauthorized
8997
+ 404:
8998
+ description: Not found
8999
+ /authkit/{clientId}/mfa/challenge/send:
9000
+ post:
9001
+ tags:
9002
+ - authKit
9003
+ summary: Send (or resend) an MFA challenge code to the given factor (public).
9004
+ operationId: authKit_mfaChallengeSend
9005
+ security: []
9006
+ parameters:
9007
+ - name: clientId
9008
+ in: path
9009
+ required: true
9010
+ schema:
9011
+ type: string
9012
+ responses:
9013
+ 200:
9014
+ description: Success
9015
+ content:
9016
+ application/json:
9017
+ schema:
9018
+ $ref: "#/components/schemas/MfaChallengeSendResponse"
9019
+ 400:
9020
+ description: Bad request
9021
+ 401:
9022
+ description: Unauthorized
9023
+ 404:
9024
+ description: Not found
9025
+ requestBody:
9026
+ required: true
9027
+ content:
9028
+ application/json:
9029
+ schema:
9030
+ type: string
9031
+ enum:
9032
+ - email
9033
+ - sms
9034
+ /authkit/{clientId}/mfa/challenge/verify:
9035
+ post:
9036
+ tags:
9037
+ - authKit
9038
+ summary: Verify an MFA challenge code and finalize the login (public).
9039
+ operationId: authKit_mfaChallengeVerify
9040
+ security: []
9041
+ parameters:
9042
+ - name: clientId
9043
+ in: path
9044
+ required: true
9045
+ schema:
9046
+ type: string
9047
+ responses:
9048
+ 200:
9049
+ description: Success
9050
+ content:
9051
+ application/json:
9052
+ schema:
9053
+ $ref: "#/components/schemas/MfaFinalizeResponse"
9054
+ 400:
9055
+ description: Bad request
9056
+ 401:
9057
+ description: Unauthorized
9058
+ 404:
9059
+ description: Not found
9060
+ /authkit/{clientId}/mfa/factors:
9061
+ get:
9062
+ tags:
9063
+ - authKit
9064
+ summary: List enrolled factors and recovery-code count for the current user (authenticated).
9065
+ operationId: authKit_getMfaFactors
9066
+ security: []
9067
+ parameters:
9068
+ - name: clientId
9069
+ in: path
9070
+ required: true
9071
+ schema:
9072
+ type: string
9073
+ responses:
9074
+ 200:
9075
+ description: Success
9076
+ content:
9077
+ application/json:
9078
+ schema:
9079
+ $ref: "#/components/schemas/MfaFactorsResponse"
9080
+ 400:
9081
+ description: Bad request
9082
+ 401:
9083
+ description: Unauthorized
9084
+ 404:
9085
+ description: Not found
9086
+ /authkit/{clientId}/mfa/factors/email/confirm:
9087
+ post:
9088
+ tags:
9089
+ - authKit
9090
+ summary: "Confirm email-factor enrollment with the code sent by {@link enrollEmailMfa} (authenticated)."
9091
+ operationId: authKit_confirmEmailMfa
9092
+ security: []
9093
+ parameters:
9094
+ - name: clientId
9095
+ in: path
9096
+ required: true
9097
+ schema:
9098
+ type: string
9099
+ responses:
9100
+ 200:
9101
+ description: Success
9102
+ content:
9103
+ application/json:
9104
+ schema:
9105
+ $ref: "#/components/schemas/MfaEnrolledResponse"
9106
+ 400:
9107
+ description: Bad request
9108
+ 401:
9109
+ description: Unauthorized
9110
+ 404:
9111
+ description: Not found
9112
+ /authkit/{clientId}/mfa/factors/email/enroll:
9113
+ post:
9114
+ tags:
9115
+ - authKit
9116
+ summary: "Begin email-factor enrollment; sends a code to the account's existing email (authenticated)."
9117
+ operationId: authKit_enrollEmailMfa
9118
+ security: []
9119
+ parameters:
9120
+ - name: clientId
9121
+ in: path
9122
+ required: true
9123
+ schema:
9124
+ type: string
9125
+ responses:
9126
+ 200:
9127
+ description: Success
9128
+ content:
9129
+ application/json:
9130
+ schema:
9131
+ $ref: "#/components/schemas/MfaEnrollSendResponse"
9132
+ 400:
9133
+ description: Bad request
9134
+ 401:
9135
+ description: Unauthorized
9136
+ 404:
9137
+ description: Not found
9138
+ /authkit/{clientId}/mfa/factors/sms/confirm:
9139
+ post:
9140
+ tags:
9141
+ - authKit
9142
+ summary: "Confirm SMS-factor enrollment with the code sent by {@link enrollSmsMfa} (authenticated)."
9143
+ operationId: authKit_confirmSmsMfa
9144
+ security: []
9145
+ parameters:
9146
+ - name: clientId
9147
+ in: path
9148
+ required: true
9149
+ schema:
9150
+ type: string
9151
+ responses:
9152
+ 200:
9153
+ description: Success
9154
+ content:
9155
+ application/json:
9156
+ schema:
9157
+ $ref: "#/components/schemas/MfaEnrolledResponse"
9158
+ 400:
9159
+ description: Bad request
9160
+ 401:
9161
+ description: Unauthorized
9162
+ 404:
9163
+ description: Not found
9164
+ /authkit/{clientId}/mfa/factors/sms/enroll:
9165
+ post:
9166
+ tags:
9167
+ - authKit
9168
+ summary: Begin SMS-factor enrollment; sends a code to the given phone number (authenticated).
9169
+ operationId: authKit_enrollSmsMfa
9170
+ security: []
9171
+ parameters:
9172
+ - name: clientId
9173
+ in: path
9174
+ required: true
9175
+ schema:
9176
+ type: string
9177
+ responses:
9178
+ 200:
9179
+ description: Success
9180
+ content:
9181
+ application/json:
9182
+ schema:
9183
+ $ref: "#/components/schemas/MfaEnrollSendResponse"
9184
+ 400:
9185
+ description: Bad request
9186
+ 401:
9187
+ description: Unauthorized
9188
+ 404:
9189
+ description: Not found
9190
+ /authkit/{clientId}/mfa/factors/{factor}:
9191
+ get:
9192
+ tags:
9193
+ - authKit
9194
+ summary: Remove an enrolled MFA factor (authenticated).
9195
+ operationId: authKit_removeMfaFactor
9196
+ security: []
9197
+ parameters:
9198
+ - name: clientId
9199
+ in: path
9200
+ required: true
9201
+ schema:
9202
+ type: string
9203
+ - name: factor
9204
+ in: path
9205
+ required: true
9206
+ schema:
9207
+ type: string
9208
+ responses:
9209
+ 200:
9210
+ description: Success
9211
+ content:
9212
+ application/json:
9213
+ schema:
9214
+ $ref: "#/components/schemas/SuccessResponse"
9215
+ 400:
9216
+ description: Bad request
9217
+ 401:
9218
+ description: Unauthorized
9219
+ 404:
9220
+ description: Not found
9221
+ /authkit/{clientId}/mfa/trusted-devices/{id}:
9222
+ delete:
9223
+ tags:
9224
+ - authKit
9225
+ summary: Revoke a single trusted device by id (authenticated).
9226
+ operationId: authKit_revokeTrustedDevice
9227
+ security: []
9228
+ parameters:
9229
+ - name: clientId
9230
+ in: path
9231
+ required: true
9232
+ schema:
9233
+ type: string
9234
+ - name: id
9235
+ in: path
9236
+ required: true
9237
+ schema:
9238
+ type: string
9239
+ responses:
9240
+ 200:
9241
+ description: Success
9242
+ content:
9243
+ application/json:
9244
+ schema:
9245
+ $ref: "#/components/schemas/SuccessResponse"
9246
+ 400:
9247
+ description: Bad request
9248
+ 401:
9249
+ description: Unauthorized
9250
+ 404:
9251
+ description: Not found
8973
9252
  /platform/location:
8974
9253
  post:
8975
9254
  tags:
@@ -14979,6 +15258,8 @@ components:
14979
15258
  type: boolean
14980
15259
  location:
14981
15260
  $ref: "#/components/schemas/AnalyticsLocation"
15261
+ source:
15262
+ type: string
14982
15263
  metadata:
14983
15264
  type: object
14984
15265
  additionalProperties: true
@@ -15014,6 +15295,8 @@ components:
15014
15295
  $ref: "#/components/schemas/AnalyticsLocation"
15015
15296
  isAdmin:
15016
15297
  type: boolean
15298
+ redirectMode:
15299
+ type: string
15017
15300
  metadata:
15018
15301
  type: object
15019
15302
  additionalProperties: true
@@ -15222,6 +15505,16 @@ components:
15222
15505
  type: boolean
15223
15506
  hasLocation:
15224
15507
  type: boolean
15508
+ sources:
15509
+ type: array
15510
+ items:
15511
+ type: string
15512
+ redirectMode:
15513
+ type: string
15514
+ redirectModes:
15515
+ type: array
15516
+ items:
15517
+ type: string
15225
15518
  AnalyticsSummaryRequest:
15226
15519
  type: object
15227
15520
  properties:
@@ -15666,6 +15959,9 @@ components:
15666
15959
  properties:
15667
15960
  basePlanId:
15668
15961
  type: string
15962
+ productMode:
15963
+ type: object
15964
+ additionalProperties: true
15669
15965
  addOnKeys:
15670
15966
  type: array
15671
15967
  items:
@@ -18413,6 +18709,124 @@ components:
18413
18709
  userInfo:
18414
18710
  type: object
18415
18711
  additionalProperties: true
18712
+ MfaRequiredDetails:
18713
+ type: object
18714
+ properties:
18715
+ mfaSessionToken:
18716
+ type: string
18717
+ availableFactors:
18718
+ type: array
18719
+ items:
18720
+ type: string
18721
+ enum:
18722
+ - email
18723
+ - sms
18724
+ preferredFactor:
18725
+ type: string
18726
+ enum:
18727
+ - email
18728
+ - sms
18729
+ maskedDestinations:
18730
+ type: object
18731
+ additionalProperties: true
18732
+ required:
18733
+ - mfaSessionToken
18734
+ - availableFactors
18735
+ - preferredFactor
18736
+ - maskedDestinations
18737
+ MfaChallengeSendResponse:
18738
+ type: object
18739
+ properties:
18740
+ success:
18741
+ type: object
18742
+ additionalProperties: true
18743
+ factor:
18744
+ type: string
18745
+ enum:
18746
+ - email
18747
+ - sms
18748
+ destination:
18749
+ type: string
18750
+ expiresAt:
18751
+ type: string
18752
+ required:
18753
+ - success
18754
+ - factor
18755
+ - destination
18756
+ - expiresAt
18757
+ MfaFinalizeResponse:
18758
+ type: object
18759
+ properties:
18760
+ trustedDeviceToken:
18761
+ type: string
18762
+ trustedDeviceExpiresAt:
18763
+ type: string
18764
+ MfaEnrollSendResponse:
18765
+ type: object
18766
+ properties:
18767
+ mfaSessionToken:
18768
+ type: string
18769
+ destination:
18770
+ type: string
18771
+ expiresAt:
18772
+ type: string
18773
+ required:
18774
+ - mfaSessionToken
18775
+ - destination
18776
+ - expiresAt
18777
+ MfaEnrolledResponse:
18778
+ type: object
18779
+ properties:
18780
+ enrolled:
18781
+ type: object
18782
+ additionalProperties: true
18783
+ factor:
18784
+ type: string
18785
+ enum:
18786
+ - email
18787
+ - sms
18788
+ required:
18789
+ - enrolled
18790
+ - factor
18791
+ MfaFactorsResponse:
18792
+ type: object
18793
+ properties:
18794
+ enrolledFactors:
18795
+ type: object
18796
+ additionalProperties: true
18797
+ email:
18798
+ type: object
18799
+ additionalProperties: true
18800
+ sms:
18801
+ type: object
18802
+ additionalProperties: true
18803
+ recoveryCodesRemaining:
18804
+ type: number
18805
+ mfaEnabledForClient:
18806
+ type: boolean
18807
+ required:
18808
+ - enrolledFactors
18809
+ - recoveryCodesRemaining
18810
+ - mfaEnabledForClient
18811
+ TrustedDevice:
18812
+ type: object
18813
+ properties:
18814
+ id:
18815
+ type: string
18816
+ label:
18817
+ type: string
18818
+ createdAtMs:
18819
+ type: number
18820
+ lastUsedAtMs:
18821
+ type: number
18822
+ expiresAtMs:
18823
+ type: number
18824
+ required:
18825
+ - id
18826
+ - label
18827
+ - createdAtMs
18828
+ - lastUsedAtMs
18829
+ - expiresAtMs
18416
18830
  MagicLinkSendResponse:
18417
18831
  type: object
18418
18832
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.15.10",
3
+ "version": "1.15.13",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",