@proveanything/smartlinks 1.15.18 → 1.15.20

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.
@@ -502,6 +502,113 @@ await authKit.completePasswordReset(clientId, tokenFromUrl, 'newSecurePassword')
502
502
 
503
503
  ---
504
504
 
505
+ ## Account security policy
506
+
507
+ Each collection can configure account-security rules. **The API enforces all of it**; your
508
+ login UI reads the policy for UX only (a live password checklist, idle sign-out). There are
509
+ two sides: an **admin** writes the policy, and the **login UI** reads the public subset.
510
+
511
+ **Read (login UI, public — no auth):**
512
+
513
+ ```ts
514
+ const config = await authKit.load(clientId);
515
+ const policy = config.security?.passwordPolicy; // min length, char classes, block-common
516
+ const session = config.security?.session; // inactivity + absolute timeouts, rememberMe
517
+ ```
518
+
519
+ `lockout` values are admin-only and never returned by `load`.
520
+
521
+ **Write (admin):** set the whole `security` block with `authKit.update` (or `authKit.create`).
522
+ The shape is {@link AuthKitSecurityConfig}; see {@link AuthKitConfigInput}.
523
+
524
+ ```ts
525
+ await authKit.update(collectionId, clientId, {
526
+ security: {
527
+ passwordPolicy: {
528
+ minLength: 8, // hard floor enforced server-side
529
+ blockCommonPasswords: true, // reject the common/breached list
530
+ requireUppercase: false, requireLowercase: false,
531
+ requireNumber: false, requireSymbol: false,
532
+ expiryDays: 0, // 0 = never expires
533
+ historyCount: 0, // 0 = reuse allowed
534
+ },
535
+ lockout: { // admin-only; enforced server-side
536
+ enabled: true, maxFailedAttempts: 5,
537
+ attemptWindowMinutes: 15, lockoutMinutes: 15,
538
+ notifyUserOnLockout: true,
539
+ },
540
+ session: {
541
+ inactivityTimeoutMinutes: 0, // 0 = disabled (client-enforced when set)
542
+ inactivityWarningSeconds: 60,
543
+ absoluteTimeoutHours: 0, // 0 = use token lifetime (server-enforced)
544
+ rememberMe: true,
545
+ },
546
+ },
547
+ });
548
+ ```
549
+
550
+ Defaults if a collection has never set a policy: 8-char minimum + block-common for everyone,
551
+ lockout disabled, no expiry/history, no idle/absolute timeout. Omit any field to take its default.
552
+
553
+ ### Password policy
554
+
555
+ `register`, `completePasswordReset`, and `changePassword` validate the new password
556
+ server-side and throw a `SmartlinksApiError` with a {@link PasswordPolicyErrorCode}:
557
+
558
+ | `errorCode` (400) | Meaning |
559
+ |---|---|
560
+ | `PASSWORD_TOO_SHORT` | below `minLength` |
561
+ | `PASSWORD_REQUIREMENTS_NOT_MET` | missing a required character class |
562
+ | `PASSWORD_TOO_COMMON` | on the common/breached list |
563
+ | `PASSWORD_RECENTLY_USED` | matched one of the last `historyCount` passwords |
564
+
565
+ Render a live checklist from `policy` so users see the rules before submitting.
566
+
567
+ ### Lockout
568
+
569
+ After too many failed logins the account is temporarily locked. `login` throws:
570
+
571
+ ```ts
572
+ try {
573
+ await authKit.login(clientId, email, password);
574
+ } catch (err) {
575
+ if (err.errorCode === 'ACCOUNT_TEMPORARILY_LOCKED') {
576
+ const mins = Math.ceil(err.details.retryAfterSeconds / 60);
577
+ show(`Too many attempts. Try again in ${mins} minute(s).`);
578
+ }
579
+ }
580
+ ```
581
+
582
+ Failed MFA challenges count toward the same lock. Locking responds identically for unknown
583
+ accounts (no enumeration).
584
+
585
+ ### Password expiry
586
+
587
+ If a password is older than `passwordPolicy.expiryDays`, a valid login is refused with
588
+ **403 `PASSWORD_EXPIRED`** carrying a short-lived `resetToken` — send the user straight into
589
+ the reset form to change it in place:
590
+
591
+ ```ts
592
+ catch (err) {
593
+ if (err.errorCode === 'PASSWORD_EXPIRED') {
594
+ await authKit.completePasswordReset(clientId, err.details.resetToken, newPassword);
595
+ }
596
+ }
597
+ ```
598
+
599
+ ### Session lifetime
600
+
601
+ - **Absolute timeout** (`session.absoluteTimeoutHours`) is enforced server-side on the native
602
+ refresh path: once the session is too old, `refreshToken` throws **401 `SESSION_EXPIRED`**
603
+ (see {@link RefreshErrorCode}) — clear storage and route to login. Web sessions use the
604
+ stateless bearer token and rely on inactivity sign-out below.
605
+ - **Inactivity timeout** (`session.inactivityTimeoutMinutes` / `inactivityWarningSeconds`) is
606
+ **client-enforced** — sign the user out after idle, warning first. Sync across tabs.
607
+ - **`session.rememberMe: false`** → don't persist tokens to durable storage; treat the session
608
+ as browser-scoped.
609
+
610
+ ---
611
+
505
612
  ## Relationship to other parts of the SDK
506
613
 
507
614
  | Concern | Where it lives |
@@ -81,6 +81,9 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
81
81
  | **App Records Pattern** | `docs/app-records-pattern.md` | Standard pattern for per-product/facet/variant/batch admin + public widget UIs |
82
82
  | **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
83
83
  | **Product/Proof Data Scoping** | `docs/proof-product-data-scoping.md` | Canonical spec for `product.data`/`.admin` and `proof.data`/`.admin`/`.values` (owner/personal) — who can read and write each bucket |
84
+ | **Proof Claiming** | `docs/proof-claiming-methods.md` | The ways a user claims a proof (serial, NFC, claim-set, auto-generate) |
85
+ | **Proof Share Grants** | `docs/proof-share-grants.md` | Delegated, scoped, revocable bearer access to a single proof (read/comment/verify-owner) |
86
+ | **Proof Ownership Transfer** | `docs/proof-ownership-transfer.md` | Moving a proof's single owner (directed transfer / open release), accept/cancel, and the state machine |
84
87
  | **appConfig / Feature Flags** | `docs/appConfig.md` | `appConfig` settings contract — installed apps, `system.features`/`entitledAppGroups`/`meters`, `isFeatureEnabled()` helper |
85
88
 
86
89
  ---
@@ -0,0 +1,114 @@
1
+ # Proof Ownership Transfer
2
+
3
+ Every proof has exactly **one owner** (`proof.userId`). A *transfer* moves that
4
+ owner from A to B — for a resale, a gift, or handing an item on. Ownership carries
5
+ the owner-scoped data with it and voids the previous owner's private access.
6
+
7
+ This covers **push (owner-initiated) transfers** — the current owner consents to the
8
+ move. Contested pull-claims and dispute resolution (where a holder claims ownership
9
+ the owner never released) are a later addition; the state machine below already
10
+ reserves states for them.
11
+
12
+ ---
13
+
14
+ ## Two ways to push
15
+
16
+ | Mode | How | Who completes it |
17
+ |------|-----|------------------|
18
+ | **Directed** | Owner names a recipient (`toEmail` / `toUserId`). The proof is earmarked for them (`claimUserId`) and they're emailed a link. | The named recipient calls `acceptTransfer`. |
19
+ | **Open release** | Owner marks the proof `claimable`. | Anyone claims it via the normal claim flow. |
20
+
21
+ Only the current owner (or a collection admin) can start a transfer. A proof can
22
+ have **one active transfer at a time**.
23
+
24
+ ### What completion does
25
+
26
+ When a directed transfer is accepted (or an admin resolves one), the move is
27
+ authoritative and atomic:
28
+
29
+ - `proof.userId` becomes the new owner; `claimable` is cleared.
30
+ - The **previous owner's private zones are removed** — `values.owner`,
31
+ `values.personal[oldOwner]`, and their `roles` entry.
32
+ - Every **share grant auto-voids** (grants are bound to the owner at issue), so
33
+ stale "I own this" links stop resolving.
34
+ - An append-only **`ownership_transfer` attestation** records `from → to`, so the
35
+ chain of custody is verifiable.
36
+
37
+ ---
38
+
39
+ ## Owner flow
40
+
41
+ ```ts
42
+ import { proof } from '@proveanything/smartlinks'
43
+
44
+ // Directed — hand it to a named buyer (they must accept)
45
+ const { transfer } = await proof.transfer(collectionId, productId, proofId, {
46
+ toEmail: 'buyer@example.com',
47
+ message: 'Enjoy the watch!',
48
+ })
49
+
50
+ // …or an open release — anyone can now claim it
51
+ await proof.transfer(collectionId, productId, proofId, { release: true })
52
+
53
+ // Check status at any time
54
+ const { transfer: active } = await proof.getTransfer(collectionId, productId, proofId)
55
+
56
+ // Change your mind before it's accepted
57
+ await proof.cancelTransfer(collectionId, productId, proofId)
58
+ ```
59
+
60
+ ## Recipient flow (directed)
61
+
62
+ ```ts
63
+ // Only the named recipient can accept — a third party is rejected.
64
+ const { proof: mine } = await proof.acceptTransfer(collectionId, productId, proofId)
65
+ ```
66
+
67
+ The recipient sees the pending transfer on their account (a pending claim) and via
68
+ the email link. Until they accept, the proof still belongs to the seller.
69
+
70
+ ---
71
+
72
+ ## State machine
73
+
74
+ ```
75
+ transfer({toEmail}) accept
76
+ (owned) ────────────────────────▶ pending ─────────────▶ completed
77
+ │ │
78
+ │ transfer({release}) │ cancel
79
+ └────────────────────────────────▶├─────────────────▶ cancelled
80
+
81
+ │ (later: dispute / escalation)
82
+ └─────────▶ disputed / escalated / rejected
83
+ ```
84
+
85
+ - `pending` — a transfer is in flight (directed earmark or open release).
86
+ - `completed` — ownership moved.
87
+ - `cancelled` — the owner withdrew it before acceptance.
88
+ - `expired` / `disputed` / `escalated` / `rejected` — reserved for contested
89
+ claims + dispute resolution (later).
90
+
91
+ ---
92
+
93
+ ## Security notes
94
+
95
+ - **Directed accept is access-controlled.** Only the earmarked recipient
96
+ (`transfer.toUserId`) can accept; the claim endpoint also rejects anyone whose
97
+ id ≠ `claimUserId` while a directed transfer is pending. A directed transfer
98
+ can't be grabbed by a bystander.
99
+ - **Ownership only moves one way** — through completion. There is no path that
100
+ silently strips an active owner without their consent (contested claims, when
101
+ added, are dispute-protected and never auto-transfer by default).
102
+ - **Every state change is audited** as an append-only attestation on the proof.
103
+
104
+ ## API
105
+
106
+ | Method | Endpoint |
107
+ |--------|----------|
108
+ | `proof.transfer(c, p, id, opts)` | `POST …/proof/:id/transfer` |
109
+ | `proof.acceptTransfer(c, p, id)` | `POST …/proof/:id/transfer/accept` |
110
+ | `proof.cancelTransfer(c, p, id)` | `POST …/proof/:id/transfer/cancel` |
111
+ | `proof.getTransfer(c, p, id)` | `GET …/proof/:id/transfer` |
112
+
113
+ See also [Proof Share Grants](./proof-share-grants.md) and
114
+ [Proof Claiming Methods](./proof-claiming-methods.md).
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export type { AdditionalGtin, ISODateString, JsonPrimitive, JsonValue, ProductCr
17
17
  export type { TranslationLookupMode, TranslationContentType, TranslationQuality, TranslationItemStatus, TranslationContextValue, TranslationContext, TranslationLookupRequestBase, TranslationLookupSingleRequest, TranslationLookupBatchRequest, TranslationLookupRequest, TranslationLookupItem, TranslationLookupResponse, ResolvedTranslationItem, ResolvedTranslationResponse, TranslationHashOptions, TranslationResolveOptions, TranslationRecord, TranslationListParams, TranslationListResponse, TranslationUpdateRequest, } from "./types/translations";
18
18
  export type { FacetBucket, FacetDefinition, FacetDefinitionWriteInput, FacetGetParams, FacetListParams, FacetListResponse, FacetNamespaceListResponse, FacetQueryRequest, FacetQueryResponse, FacetValue, FacetValueDefinition, FacetValueGetParams, FacetValueListParams, FacetValueListResponse, FacetValueResponse, FacetValueWriteInput, PublicFacetListParams, } from "./types/facets";
19
19
  export type { Collection, CollectionResponse, CollectionCreateRequest, CollectionUpdateRequest, DomainTarget, HubAvailabilityResponse, } from "./types/collection";
20
- export type { Proof, ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, ProofGrant, GrantScope, GrantAudience, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult, } from "./types/proof";
20
+ export type { Proof, ProofResponse, ProofWrite, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, ProofGrant, GrantScope, GrantAudience, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult, ProofTransfer, ProofTransferType, ProofTransferState, TransferProofOptions, TransferProofResult, } from "./types/proof";
21
21
  export type { QrShortCodeLookupResponse, } from "./types/qr";
22
22
  export type { ReverseTagLookupParams, ReverseTagLookupResponse, } from "./types/tags";
23
23
  export type { AdminMobileCapability, ActionableCapability, AdminMobileHostId, AdminMobileEvent, AdminMobileEventCallback, AdminMobileEventSubscriber, ScannerEventSubscriber, // @deprecated — use AdminMobileEventCallback
@@ -25,4 +25,4 @@ AdminMobileHostContext, AdminMobileComponentManifest, AdminMobileBundleManifest,
25
25
  MobileAdminBundleManifest, } from './mobile-admin/types';
26
26
  export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors';
27
27
  export type { NativeCapability, NativeFacade, ShareFacade, ClipboardFacade, HapticImpactStyle, HapticNotificationStyle, HapticsFacade, NetworkStatus, NetworkFacade, DeviceInfo, DeviceFacade, StorageFacade, QrScanOptions, QrFacade, AuthFacade, NfcReadResult, NfcFacade, RfidScanOptions, RfidFacade, EventsFacade, WebSourceMode, WebSourceConfig, WebSourceFacade, } from './native/types';
28
- export type { AuthKitUser, UserProfile, ProfileUpdateData, UpdateProfileResponse, SuccessResponse, AuthLoginResponse, AppleLoginOptions, AuthKitErrorCode, RefreshResponse, LogoutResponse, RefreshErrorCode, MagicLinkSendResponse, MagicLinkVerifyResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, VerifyStatus, WhatsAppReplyCta, WhatsAppReplyOptions, WhatsAppContactData, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse, AuthKitBrandingConfig, AuthKitConfig, } from './types/authKit';
28
+ export type { AuthKitUser, UserProfile, ProfileUpdateData, UpdateProfileResponse, SuccessResponse, AuthLoginResponse, AppleLoginOptions, AuthKitErrorCode, RefreshResponse, LogoutResponse, RefreshErrorCode, MagicLinkSendResponse, MagicLinkVerifyResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, VerifyStatus, WhatsAppReplyCta, WhatsAppReplyOptions, WhatsAppContactData, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse, AuthKitBrandingConfig, AuthKitConfig, AuthKitConfigInput, AuthKitSecurityConfig, AuthKitPasswordPolicy, AuthKitSessionPolicy, AuthKitLockoutPolicy, PasswordPolicyErrorCode, LoginSecurityErrorCode, } from './types/authKit';
package/dist/openapi.yaml CHANGED
@@ -1579,7 +1579,7 @@ paths:
1579
1579
  post:
1580
1580
  tags:
1581
1581
  - authKit
1582
- summary: authKit.create
1582
+ summary: Create an AuthKit client config (admin).
1583
1583
  operationId: authKit_create
1584
1584
  security:
1585
1585
  - bearerAuth: []
@@ -1602,11 +1602,17 @@ paths:
1602
1602
  description: Unauthorized
1603
1603
  404:
1604
1604
  description: Not found
1605
+ requestBody:
1606
+ required: true
1607
+ content:
1608
+ application/json:
1609
+ schema:
1610
+ $ref: "#/components/schemas/AuthKitConfigInput"
1605
1611
  /admin/collection/{collectionId}/authKit/{authKitId}:
1606
1612
  get:
1607
1613
  tags:
1608
1614
  - authKit
1609
- summary: authKit.get
1615
+ summary: "Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth)."
1610
1616
  operationId: authKit_get
1611
1617
  security:
1612
1618
  - bearerAuth: []
@@ -1637,7 +1643,7 @@ paths:
1637
1643
  put:
1638
1644
  tags:
1639
1645
  - authKit
1640
- summary: authKit.update
1646
+ summary: Update an AuthKit client config (admin).
1641
1647
  operationId: authKit_update
1642
1648
  security:
1643
1649
  - bearerAuth: []
@@ -1665,6 +1671,12 @@ paths:
1665
1671
  description: Unauthorized
1666
1672
  404:
1667
1673
  description: Not found
1674
+ requestBody:
1675
+ required: true
1676
+ content:
1677
+ application/json:
1678
+ schema:
1679
+ $ref: "#/components/schemas/AuthKitConfigInput"
1668
1680
  delete:
1669
1681
  tags:
1670
1682
  - authKit
@@ -8077,7 +8089,7 @@ paths:
8077
8089
  get:
8078
8090
  tags:
8079
8091
  - authKit
8080
- summary: Revoke a single trusted device by id (authenticated).
8092
+ summary: "Load the **public** AuthKit config for a client (no auth)."
8081
8093
  operationId: authKit_load
8082
8094
  security: []
8083
8095
  parameters:
@@ -8401,7 +8413,7 @@ paths:
8401
8413
  post:
8402
8414
  tags:
8403
8415
  - authKit
8404
- summary: Login with email + password (public).
8416
+ summary: authKit.login
8405
8417
  operationId: authKit_login
8406
8418
  security: []
8407
8419
  parameters:
@@ -19381,8 +19393,74 @@ components:
19381
19393
  type: string
19382
19394
  updatedAt:
19383
19395
  type: string
19396
+ security:
19397
+ $ref: "#/components/schemas/AuthKitSecurityConfig"
19384
19398
  required:
19385
19399
  - id
19400
+ AuthKitSecurityConfig:
19401
+ type: object
19402
+ properties:
19403
+ passwordPolicy:
19404
+ $ref: "#/components/schemas/AuthKitPasswordPolicy"
19405
+ session:
19406
+ $ref: "#/components/schemas/AuthKitSessionPolicy"
19407
+ lockout:
19408
+ $ref: "#/components/schemas/AuthKitLockoutPolicy"
19409
+ AuthKitPasswordPolicy:
19410
+ type: object
19411
+ properties:
19412
+ minLength:
19413
+ type: number
19414
+ requireUppercase:
19415
+ type: boolean
19416
+ requireLowercase:
19417
+ type: boolean
19418
+ requireNumber:
19419
+ type: boolean
19420
+ requireSymbol:
19421
+ type: boolean
19422
+ blockCommonPasswords:
19423
+ type: boolean
19424
+ expiryDays:
19425
+ type: number
19426
+ historyCount:
19427
+ type: number
19428
+ AuthKitSessionPolicy:
19429
+ type: object
19430
+ properties:
19431
+ inactivityTimeoutMinutes:
19432
+ type: number
19433
+ inactivityWarningSeconds:
19434
+ type: number
19435
+ absoluteTimeoutHours:
19436
+ type: number
19437
+ rememberMe:
19438
+ type: boolean
19439
+ AuthKitLockoutPolicy:
19440
+ type: object
19441
+ properties:
19442
+ enabled:
19443
+ type: boolean
19444
+ maxFailedAttempts:
19445
+ type: number
19446
+ attemptWindowMinutes:
19447
+ type: number
19448
+ lockoutMinutes:
19449
+ type: number
19450
+ notifyUserOnLockout:
19451
+ type: boolean
19452
+ AuthKitConfigInput:
19453
+ type: string
19454
+ enum:
19455
+ - id
19456
+ - updatedAt
19457
+ PasswordPolicyErrorCode:
19458
+ type: string
19459
+ enum:
19460
+ - PASSWORD_TOO_SHORT
19461
+ - PASSWORD_REQUIREMENTS_NOT_MET
19462
+ - PASSWORD_TOO_COMMON
19463
+ - PASSWORD_RECENTLY_USED
19386
19464
  FirebaseTimestamp:
19387
19465
  type: object
19388
19466
  properties:
@@ -24994,9 +25072,11 @@ components:
24994
25072
  - tokenId
24995
25073
  - userId
24996
25074
  - values
24997
- ProofCreateRequest:
25075
+ ProofWrite:
24998
25076
  type: object
24999
25077
  properties:
25078
+ id:
25079
+ type: string
25000
25080
  values:
25001
25081
  $ref: "#/components/schemas/ProofValues"
25002
25082
  data:
@@ -25007,12 +25087,33 @@ components:
25007
25087
  type: object
25008
25088
  additionalProperties:
25009
25089
  $ref: "#/components/schemas/JsonValue"
25090
+ owner:
25091
+ type: object
25092
+ additionalProperties:
25093
+ $ref: "#/components/schemas/JsonValue"
25094
+ claimable:
25095
+ type: boolean
25096
+ ProofCreateRequest:
25097
+ type: object
25098
+ properties:
25099
+ proof:
25100
+ $ref: "#/components/schemas/ProofWrite"
25101
+ values:
25102
+ $ref: "#/components/schemas/ProofValues"
25010
25103
  claimable:
25011
25104
  type: boolean
25012
25105
  virtual:
25013
25106
  type: boolean
25014
- required:
25015
- - values
25107
+ core:
25108
+ $ref: "#/components/schemas/ProofWrite"
25109
+ data:
25110
+ type: object
25111
+ additionalProperties:
25112
+ $ref: "#/components/schemas/JsonValue"
25113
+ admin:
25114
+ type: object
25115
+ additionalProperties:
25116
+ $ref: "#/components/schemas/JsonValue"
25016
25117
  ProofFieldsConfig:
25017
25118
  type: object
25018
25119
  properties:
@@ -25096,9 +25197,87 @@ components:
25096
25197
  properties:
25097
25198
  guestName:
25098
25199
  type: string
25099
- ProofResponse:
25200
+ ProofTransfer:
25100
25201
  type: object
25101
- additionalProperties: true
25202
+ properties:
25203
+ id:
25204
+ type: string
25205
+ proofId:
25206
+ type: string
25207
+ productId:
25208
+ type: string
25209
+ type:
25210
+ $ref: "#/components/schemas/ProofTransferType"
25211
+ state:
25212
+ $ref: "#/components/schemas/ProofTransferState"
25213
+ fromUserId:
25214
+ type: string
25215
+ toUserId:
25216
+ type: string
25217
+ toEmail:
25218
+ type: string
25219
+ initiatedByUserId:
25220
+ type: string
25221
+ initiatedByRole:
25222
+ type: string
25223
+ enum:
25224
+ - owner
25225
+ - claimant
25226
+ - admin
25227
+ disputeReason:
25228
+ type: string
25229
+ disputeDeadline:
25230
+ type: string
25231
+ completedAt:
25232
+ type: string
25233
+ createdAt:
25234
+ type: string
25235
+ updatedAt:
25236
+ type: string
25237
+ required:
25238
+ - id
25239
+ - proofId
25240
+ - type
25241
+ - state
25242
+ - createdAt
25243
+ - updatedAt
25244
+ TransferProofOptions:
25245
+ type: object
25246
+ properties:
25247
+ toEmail:
25248
+ type: string
25249
+ toUserId:
25250
+ type: string
25251
+ toName:
25252
+ type: string
25253
+ release:
25254
+ type: boolean
25255
+ message:
25256
+ type: string
25257
+ notify:
25258
+ type: boolean
25259
+ TransferProofResult:
25260
+ type: object
25261
+ properties:
25262
+ ok:
25263
+ type: boolean
25264
+ mode:
25265
+ type: string
25266
+ enum:
25267
+ - directed
25268
+ - open_release
25269
+ transfer:
25270
+ $ref: "#/components/schemas/ProofTransfer"
25271
+ required:
25272
+ - ok
25273
+ - mode
25274
+ - transfer
25275
+ ProofTransferType:
25276
+ type: string
25277
+ enum:
25278
+ - directed
25279
+ - open_release
25280
+ - contested
25102
25281
  QrShortCodeLookupResponse:
25103
25282
  type: object
25104
25283
  properties:
@@ -89,8 +89,11 @@ export interface LogoutResponse {
89
89
  * - `INVALID_REFRESH_TOKEN` (401) — unknown / expired / revoked / wrong client → `logout()` + route to login.
90
90
  * - `REFRESH_TOKEN_REUSE_DETECTED` (401) — a consumed token was replayed; the **entire session
91
91
  * family was revoked server-side**. Hard logout: clear storage, force re-login.
92
+ * - `SESSION_EXPIRED` (401) — the session hit the collection's absolute session timeout
93
+ * (`security.session.absoluteTimeoutHours`). Clear storage and route to login. Distinct from
94
+ * `INVALID_REFRESH_TOKEN` so the UI can message "your session expired" rather than an error.
92
95
  */
93
- export type RefreshErrorCode = 'MISSING_REFRESH_TOKEN' | 'INVALID_REFRESH_TOKEN' | 'REFRESH_TOKEN_REUSE_DETECTED';
96
+ export type RefreshErrorCode = 'MISSING_REFRESH_TOKEN' | 'INVALID_REFRESH_TOKEN' | 'REFRESH_TOKEN_REUSE_DETECTED' | 'SESSION_EXPIRED';
94
97
  /**
95
98
  * Options for {@link authKit.appleLogin}. All fields are optional — only the
96
99
  * `identityToken` (passed as a positional argument) is required by the server.
@@ -397,4 +400,84 @@ export interface AuthKitConfig {
397
400
  supportEmail?: string;
398
401
  redirectUrl?: string;
399
402
  updatedAt?: string;
403
+ /**
404
+ * Per-collection security policy. On the public config endpoint only
405
+ * `passwordPolicy` + `session` are returned (the client renders password
406
+ * checklists / idle sign-out from them); `lockout` is admin-only and enforced
407
+ * server-side. See {@link AuthKitSecurityConfig}.
408
+ */
409
+ security?: AuthKitSecurityConfig;
410
+ }
411
+ /**
412
+ * Writable shape for `authKit.create` / `authKit.update` (admin). Known fields are
413
+ * typed — importantly `security?: AuthKitSecurityConfig` for the account security
414
+ * policy — while additional admin-console fields are still permitted. `id` and
415
+ * `updatedAt` are server-owned and omitted.
416
+ *
417
+ * ```ts
418
+ * await authKit.update(collectionId, authKitId, {
419
+ * security: {
420
+ * passwordPolicy: { minLength: 10, blockCommonPasswords: true },
421
+ * lockout: { enabled: true, maxFailedAttempts: 5, lockoutMinutes: 15 },
422
+ * session: { inactivityTimeoutMinutes: 30, absoluteTimeoutHours: 12 },
423
+ * },
424
+ * })
425
+ * ```
426
+ */
427
+ export type AuthKitConfigInput = Partial<Omit<AuthKitConfig, 'id' | 'updatedAt'>> & Record<string, any>;
428
+ /**
429
+ * Per-collection account-security policy. The API **enforces** all of this; the
430
+ * client uses `passwordPolicy` (live checklist) and `session` (idle sign-out) for UX.
431
+ */
432
+ export interface AuthKitSecurityConfig {
433
+ passwordPolicy?: AuthKitPasswordPolicy;
434
+ session?: AuthKitSessionPolicy;
435
+ /** Admin-only; never returned on the public config endpoint. */
436
+ lockout?: AuthKitLockoutPolicy;
437
+ }
438
+ export interface AuthKitPasswordPolicy {
439
+ minLength?: number;
440
+ requireUppercase?: boolean;
441
+ requireLowercase?: boolean;
442
+ requireNumber?: boolean;
443
+ requireSymbol?: boolean;
444
+ blockCommonPasswords?: boolean;
445
+ /** 0 = never expires. */
446
+ expiryDays?: number;
447
+ /** 0 = reuse allowed. */
448
+ historyCount?: number;
449
+ }
450
+ export interface AuthKitSessionPolicy {
451
+ /** 0 = never (client-enforced idle sign-out). */
452
+ inactivityTimeoutMinutes?: number;
453
+ inactivityWarningSeconds?: number;
454
+ /** 0 = use token lifetime. Enforced server-side on the native refresh path (→ `SESSION_EXPIRED`). */
455
+ absoluteTimeoutHours?: number;
456
+ rememberMe?: boolean;
457
+ }
458
+ /** Admin-only lockout policy (operational; not exposed publicly). */
459
+ export interface AuthKitLockoutPolicy {
460
+ enabled?: boolean;
461
+ maxFailedAttempts?: number;
462
+ attemptWindowMinutes?: number;
463
+ lockoutMinutes?: number;
464
+ notifyUserOnLockout?: boolean;
400
465
  }
466
+ /**
467
+ * Password-policy validation errors (400) returned by `register`, `completePasswordReset`,
468
+ * and `changePassword`. Surfaced via `SmartlinksApiError.errorCode`.
469
+ * - `PASSWORD_TOO_SHORT` — below `passwordPolicy.minLength`.
470
+ * - `PASSWORD_REQUIREMENTS_NOT_MET` — missing a required character class.
471
+ * - `PASSWORD_TOO_COMMON` — matched the common/breached list.
472
+ * - `PASSWORD_RECENTLY_USED` — matched one of the last `historyCount` passwords.
473
+ */
474
+ export type PasswordPolicyErrorCode = 'PASSWORD_TOO_SHORT' | 'PASSWORD_REQUIREMENTS_NOT_MET' | 'PASSWORD_TOO_COMMON' | 'PASSWORD_RECENTLY_USED';
475
+ /**
476
+ * Security errors returned by `login`. Surfaced via `SmartlinksApiError.errorCode`,
477
+ * with extra fields in `SmartlinksApiError.details`:
478
+ * - `ACCOUNT_TEMPORARILY_LOCKED` (429) — too many failed attempts; `details.retryAfterSeconds`
479
+ * says how long to wait. Show a "try again in N minutes" message.
480
+ * - `PASSWORD_EXPIRED` (403) — password older than `passwordPolicy.expiryDays`; `details.resetToken`
481
+ * is a short-lived token — send the user straight into `completePasswordReset()` to change it in place.
482
+ */
483
+ export type LoginSecurityErrorCode = 'ACCOUNT_TEMPORARILY_LOCKED' | 'PASSWORD_EXPIRED';