@proveanything/smartlinks 1.15.19 → 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.
@@ -1,4 +1,4 @@
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";
1
+ import type { AuthLoginResponse, AppleLoginOptions, RefreshResponse, LogoutResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, AuthKitConfig, AuthKitConfigInput, 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*).
@@ -231,10 +231,24 @@ export declare namespace authKit {
231
231
  }>;
232
232
  /** Revoke a single trusted device by id (authenticated). */
233
233
  function revokeTrustedDevice(clientId: string, id: string): Promise<SuccessResponse>;
234
+ /**
235
+ * Load the **public** AuthKit config for a client (no auth). Returns branding +
236
+ * the public security subset (`security.passwordPolicy` + `security.session`);
237
+ * `security.lockout` is admin-only and never included here. Use this in the login
238
+ * UI to render password checklists and drive idle sign-out.
239
+ */
234
240
  function load(authKitId: string): Promise<AuthKitConfig>;
241
+ /** Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth). */
235
242
  function get(collectionId: string, authKitId: string): Promise<AuthKitConfig>;
236
243
  function list(collectionId: string, admin?: boolean): Promise<AuthKitConfig[]>;
237
- function create(collectionId: string, data: any): Promise<AuthKitConfig>;
238
- function update(collectionId: string, authKitId: string, data: any): Promise<AuthKitConfig>;
244
+ /** Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}. */
245
+ function create(collectionId: string, data: AuthKitConfigInput): Promise<AuthKitConfig>;
246
+ /**
247
+ * Update an AuthKit client config (admin). This is how the account **security
248
+ * policy** is written — pass a `security` block ({@link AuthKitSecurityConfig}).
249
+ * The server validates it and enforces it; the login UI reads the public subset
250
+ * back via {@link load}.
251
+ */
252
+ function update(collectionId: string, authKitId: string, data: AuthKitConfigInput): Promise<AuthKitConfig>;
239
253
  function remove(collectionId: string, authKitId: string): Promise<void>;
240
254
  }
@@ -444,11 +444,18 @@ export var authKit;
444
444
  /* ===================================
445
445
  * Collection-based AuthKit
446
446
  * =================================== */
447
+ /**
448
+ * Load the **public** AuthKit config for a client (no auth). Returns branding +
449
+ * the public security subset (`security.passwordPolicy` + `security.session`);
450
+ * `security.lockout` is admin-only and never included here. Use this in the login
451
+ * UI to render password checklists and drive idle sign-out.
452
+ */
447
453
  async function load(authKitId) {
448
454
  const path = `/authKit/${encodeURIComponent(authKitId)}/config`;
449
455
  return request(path);
450
456
  }
451
457
  authKit.load = load;
458
+ /** Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth). */
452
459
  async function get(collectionId, authKitId) {
453
460
  const path = `/admin/collection/${encodeURIComponent(collectionId)}/authKit/${encodeURIComponent(authKitId)}`;
454
461
  return request(path);
@@ -460,11 +467,18 @@ export var authKit;
460
467
  return request(path);
461
468
  }
462
469
  authKit.list = list;
470
+ /** Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}. */
463
471
  async function create(collectionId, data) {
464
472
  const path = `/admin/collection/${encodeURIComponent(collectionId)}/authKit`;
465
473
  return post(path, data);
466
474
  }
467
475
  authKit.create = create;
476
+ /**
477
+ * Update an AuthKit client config (admin). This is how the account **security
478
+ * policy** is written — pass a `security` block ({@link AuthKitSecurityConfig}).
479
+ * The server validates it and enforces it; the login UI reads the public subset
480
+ * back via {@link load}.
481
+ */
468
482
  async function update(collectionId, authKitId, data) {
469
483
  const path = `/admin/collection/${encodeURIComponent(collectionId)}/authKit/${encodeURIComponent(authKitId)}`;
470
484
  return put(path, data);
@@ -1,4 +1,4 @@
1
- import { ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, ProofGrant, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult } from "../types/proof";
1
+ import { ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, ProofGrant, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult, ProofTransfer, TransferProofOptions, TransferProofResult } from "../types/proof";
2
2
  export declare namespace proof {
3
3
  /**
4
4
  * Retrieves a single Proof by Collection ID, Product ID, and Proof ID.
@@ -145,4 +145,37 @@ export declare namespace proof {
145
145
  * {@link setGrantToken} so subsequent data requests carry the token.
146
146
  */
147
147
  function redeemGrant(collectionId: string, productId: string, proofId: string, token: string, options?: RedeemGrantOptions): Promise<RedeemGrantResult>;
148
+ /**
149
+ * Start a push transfer of a proof (current owner / collection admin only).
150
+ *
151
+ * Directed — hand it to a named recipient who then calls {@link acceptTransfer}:
152
+ * ```ts
153
+ * await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' })
154
+ * ```
155
+ * Open release — make the proof claimable by anyone:
156
+ * ```ts
157
+ * await proof.transfer(collectionId, productId, proofId, { release: true })
158
+ * ```
159
+ */
160
+ function transfer(collectionId: string, productId: string, proofId: string, options: TransferProofOptions): Promise<TransferProofResult>;
161
+ /**
162
+ * Accept a directed transfer (the named recipient only). Completes the ownership
163
+ * move — the proof's `userId` becomes the caller and the previous owner's private
164
+ * data and share grants are cleared/voided.
165
+ */
166
+ function acceptTransfer(collectionId: string, productId: string, proofId: string): Promise<{
167
+ ok: boolean;
168
+ proof: ProofResponse;
169
+ }>;
170
+ /** Cancel a pending push transfer (current owner / collection admin only). */
171
+ function cancelTransfer(collectionId: string, productId: string, proofId: string): Promise<{
172
+ ok: boolean;
173
+ }>;
174
+ /**
175
+ * Get the active transfer/status for a proof (owner, collection admin, or the
176
+ * named recipient). Returns `{ transfer: null }` when nothing is in flight.
177
+ */
178
+ function getTransfer(collectionId: string, productId: string, proofId: string): Promise<{
179
+ transfer: ProofTransfer | null;
180
+ }>;
148
181
  }
package/dist/api/proof.js CHANGED
@@ -223,4 +223,53 @@ export var proof;
223
223
  return post(`${grantBase(collectionId, productId, proofId)}/redeem`, body);
224
224
  }
225
225
  proof.redeemGrant = redeemGrant;
226
+ // ---------------------------------------------------------------------------
227
+ // Ownership transfer — moving a proof's single owner from A to B
228
+ //
229
+ // A proof always has exactly one owner (`proof.userId`). A transfer moves that
230
+ // owner with the current owner's consent: either DIRECTED to a named recipient
231
+ // (who accepts) or an OPEN RELEASE (the proof becomes claimable by anyone).
232
+ // Contested pull-claims + dispute resolution are a later addition.
233
+ // ---------------------------------------------------------------------------
234
+ function transferBase(collectionId, productId, proofId) {
235
+ return `/public/collection/${encodeURIComponent(collectionId)}/product/${encodeURIComponent(productId)}/proof/${encodeURIComponent(proofId)}/transfer`;
236
+ }
237
+ /**
238
+ * Start a push transfer of a proof (current owner / collection admin only).
239
+ *
240
+ * Directed — hand it to a named recipient who then calls {@link acceptTransfer}:
241
+ * ```ts
242
+ * await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' })
243
+ * ```
244
+ * Open release — make the proof claimable by anyone:
245
+ * ```ts
246
+ * await proof.transfer(collectionId, productId, proofId, { release: true })
247
+ * ```
248
+ */
249
+ async function transfer(collectionId, productId, proofId, options) {
250
+ return post(transferBase(collectionId, productId, proofId), Object.assign({}, options));
251
+ }
252
+ proof.transfer = transfer;
253
+ /**
254
+ * Accept a directed transfer (the named recipient only). Completes the ownership
255
+ * move — the proof's `userId` becomes the caller and the previous owner's private
256
+ * data and share grants are cleared/voided.
257
+ */
258
+ async function acceptTransfer(collectionId, productId, proofId) {
259
+ return post(`${transferBase(collectionId, productId, proofId)}/accept`, {});
260
+ }
261
+ proof.acceptTransfer = acceptTransfer;
262
+ /** Cancel a pending push transfer (current owner / collection admin only). */
263
+ async function cancelTransfer(collectionId, productId, proofId) {
264
+ return post(`${transferBase(collectionId, productId, proofId)}/cancel`, {});
265
+ }
266
+ proof.cancelTransfer = cancelTransfer;
267
+ /**
268
+ * Get the active transfer/status for a proof (owner, collection admin, or the
269
+ * named recipient). Returns `{ transfer: null }` when nothing is in flight.
270
+ */
271
+ async function getTransfer(collectionId, productId, proofId) {
272
+ return request(transferBase(collectionId, productId, proofId));
273
+ }
274
+ proof.getTransfer = getTransfer;
226
275
  })(proof || (proof = {}));
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.19 | Generated: 2026-08-20T18:06:08.134Z
3
+ Version: 1.15.20 | Generated: 2026-08-25T07:03:45.544Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -3524,6 +3524,8 @@ interface AuthKitLockoutPolicy {
3524
3524
 
3525
3525
  **VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
3526
3526
 
3527
+ **AuthKitConfigInput** = ``
3528
+
3527
3529
  **PasswordPolicyErrorCode** = ``
3528
3530
 
3529
3531
  **LoginSecurityErrorCode** = ``
@@ -7416,6 +7418,48 @@ interface RedeemGrantOptions {
7416
7418
  }
7417
7419
  ```
7418
7420
 
7421
+ **ProofTransfer** (interface)
7422
+ ```typescript
7423
+ interface ProofTransfer {
7424
+ id: string
7425
+ proofId: string
7426
+ productId?: string | null
7427
+ type: ProofTransferType
7428
+ state: ProofTransferState
7429
+ fromUserId?: string | null
7430
+ toUserId?: string | null
7431
+ toEmail?: string | null
7432
+ initiatedByUserId?: string | null
7433
+ initiatedByRole?: 'owner' | 'claimant' | 'admin' | null
7434
+ disputeReason?: string | null
7435
+ disputeDeadline?: string | null
7436
+ completedAt?: string | null
7437
+ createdAt: string
7438
+ updatedAt: string
7439
+ }
7440
+ ```
7441
+
7442
+ **TransferProofOptions** (interface)
7443
+ ```typescript
7444
+ interface TransferProofOptions {
7445
+ toEmail?: string
7446
+ toUserId?: string
7447
+ toName?: string
7448
+ release?: boolean
7449
+ message?: string
7450
+ notify?: boolean
7451
+ }
7452
+ ```
7453
+
7454
+ **TransferProofResult** (interface)
7455
+ ```typescript
7456
+ interface TransferProofResult {
7457
+ ok: boolean
7458
+ mode: 'directed' | 'open_release'
7459
+ transfer: ProofTransfer
7460
+ }
7461
+ ```
7462
+
7419
7463
  **ProofResponse** = `Proof`
7420
7464
 
7421
7465
  **ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
@@ -7430,6 +7474,10 @@ interface RedeemGrantOptions {
7430
7474
 
7431
7475
  **RedeemGrantResult** = ``
7432
7476
 
7477
+ **ProofTransferType** = `'directed' | 'open_release' | 'contested'`
7478
+
7479
+ **ProofTransferState** = ``
7480
+
7433
7481
  ### qr
7434
7482
 
7435
7483
  **QrShortCodeLookupResponse** (interface)
@@ -8952,22 +9000,22 @@ List devices trusted to skip MFA challenges for the current user (authenticated)
8952
9000
  Revoke a single trusted device by id (authenticated).
8953
9001
 
8954
9002
  **load**(authKitId: string) → `Promise<AuthKitConfig>`
8955
- Revoke a single trusted device by id (authenticated).
9003
+ Load the **public** AuthKit config for a client (no auth). Returns branding + the public security subset (`security.passwordPolicy` + `security.session`); `security.lockout` is admin-only and never included here. Use this in the login UI to render password checklists and drive idle sign-out.
8956
9004
 
8957
9005
  **get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
8958
- Revoke a single trusted device by id (authenticated).
9006
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8959
9007
 
8960
9008
  **list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
8961
- Revoke a single trusted device by id (authenticated).
9009
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8962
9010
 
8963
- **create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
8964
- Revoke a single trusted device by id (authenticated).
9011
+ **create**(collectionId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9012
+ Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}.
8965
9013
 
8966
- **update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
8967
- Revoke a single trusted device by id (authenticated).
9014
+ **update**(collectionId: string, authKitId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9015
+ Update an AuthKit client config (admin). This is how the account **security policy** is written — pass a `security` block ({@link AuthKitSecurityConfig}). The server validates it and enforces it; the login UI reads the public subset back via {@link load}.
8968
9016
 
8969
9017
  **remove**(collectionId: string, authKitId: string) → `Promise<void>`
8970
- Revoke a single trusted device by id (authenticated).
9018
+ Update an AuthKit client config (admin). This is how the account **security policy** is written — pass a `security` block ({@link AuthKitSecurityConfig}). The server validates it and enforces it; the login UI reads the public subset back via {@link load}.
8971
9019
 
8972
9020
  ### batch
8973
9021
 
@@ -10101,6 +10149,27 @@ Revoke a grant by id (owner / collection admin only). Takes effect immediately.
10101
10149
  options?: RedeemGrantOptions) → `Promise<RedeemGrantResult>`
10102
10150
  Redeem a grant token (anonymous or signed-in). Records the redemption and returns the granted scope, or — for a `verify_owner` grant — an ownership assertion (never the account). After redeeming, call {@link setGrantToken} so subsequent data requests carry the token.
10103
10151
 
10152
+ **transfer**(collectionId: string,
10153
+ productId: string,
10154
+ proofId: string,
10155
+ options: TransferProofOptions) → `Promise<TransferProofResult>`
10156
+ Start a push transfer of a proof (current owner / collection admin only). Directed — hand it to a named recipient who then calls {@link acceptTransfer}: ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' }) ``` Open release — make the proof claimable by anyone: ```ts await proof.transfer(collectionId, productId, proofId, { release: true }) ```
10157
+
10158
+ **acceptTransfer**(collectionId: string,
10159
+ productId: string,
10160
+ proofId: string) → `Promise<`
10161
+ Accept a directed transfer (the named recipient only). Completes the ownership move — the proof's `userId` becomes the caller and the previous owner's private data and share grants are cleared/voided.
10162
+
10163
+ **cancelTransfer**(collectionId: string,
10164
+ productId: string,
10165
+ proofId: string) → `Promise<`
10166
+ Cancel a pending push transfer (current owner / collection admin only).
10167
+
10168
+ **getTransfer**(collectionId: string,
10169
+ productId: string,
10170
+ proofId: string) → `Promise<`
10171
+ Get the active transfer/status for a proof (owner, collection admin, or the named recipient). Returns `{ transfer: null }` when nothing is in flight.
10172
+
10104
10173
  ### publicClient
10105
10174
 
10106
10175
  **chat**(collectionId: string,
@@ -504,9 +504,11 @@ await authKit.completePasswordReset(clientId, tokenFromUrl, 'newSecurePassword')
504
504
 
505
505
  ## Account security policy
506
506
 
507
- Each collection can configure account-security rules (via the admin console). **The API
508
- enforces all of it**; your login UI reads the policy for UX only (a live password checklist,
509
- idle sign-out). Read it from the config:
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):**
510
512
 
511
513
  ```ts
512
514
  const config = await authKit.load(clientId);
@@ -514,7 +516,39 @@ const policy = config.security?.passwordPolicy; // min length, char classes, bl
514
516
  const session = config.security?.session; // inactivity + absolute timeouts, rememberMe
515
517
  ```
516
518
 
517
- `lockout` values are admin-only and never returned here.
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.
518
552
 
519
553
  ### Password policy
520
554
 
@@ -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, ProofWrite, 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, AuthKitSecurityConfig, AuthKitPasswordPolicy, AuthKitSessionPolicy, AuthKitLockoutPolicy, PasswordPolicyErrorCode, LoginSecurityErrorCode, } 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:
@@ -19437,6 +19449,11 @@ components:
19437
19449
  type: number
19438
19450
  notifyUserOnLockout:
19439
19451
  type: boolean
19452
+ AuthKitConfigInput:
19453
+ type: string
19454
+ enum:
19455
+ - id
19456
+ - updatedAt
19440
19457
  PasswordPolicyErrorCode:
19441
19458
  type: string
19442
19459
  enum:
@@ -25180,6 +25197,87 @@ components:
25180
25197
  properties:
25181
25198
  guestName:
25182
25199
  type: string
25200
+ ProofTransfer:
25201
+ type: object
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
25183
25281
  QrShortCodeLookupResponse:
25184
25282
  type: object
25185
25283
  properties:
@@ -408,6 +408,23 @@ export interface AuthKitConfig {
408
408
  */
409
409
  security?: AuthKitSecurityConfig;
410
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>;
411
428
  /**
412
429
  * Per-collection account-security policy. The API **enforces** all of this; the
413
430
  * client uses `passwordPolicy` (live checklist) and `session` (idle sign-out) for UX.
@@ -175,3 +175,60 @@ export type RedeemGrantResult = {
175
175
  issuedAt?: string;
176
176
  expiresAt?: string;
177
177
  };
178
+ /**
179
+ * How a transfer was initiated.
180
+ * - `directed` — the owner pushed it to a named recipient, who must accept.
181
+ * - `open_release` — the owner released it; the proof is `claimable` by anyone.
182
+ * - `contested` — a holder claimed ownership the owner did not release (Phase 2).
183
+ */
184
+ export type ProofTransferType = 'directed' | 'open_release' | 'contested';
185
+ /**
186
+ * Where a transfer is in its lifecycle. `pending` is in-flight; `completed`
187
+ * means ownership moved. `disputed`/`escalated` are Phase 2 (contested claims).
188
+ */
189
+ export type ProofTransferState = 'pending' | 'completed' | 'cancelled' | 'expired' | 'disputed' | 'escalated' | 'rejected';
190
+ /** An ownership-transfer record on a proof. */
191
+ export interface ProofTransfer {
192
+ id: string;
193
+ proofId: string;
194
+ productId?: string | null;
195
+ type: ProofTransferType;
196
+ state: ProofTransferState;
197
+ /** Owner at the time the transfer was initiated. */
198
+ fromUserId?: string | null;
199
+ /** Intended recipient (directed) / claimant (contested). */
200
+ toUserId?: string | null;
201
+ toEmail?: string | null;
202
+ initiatedByUserId?: string | null;
203
+ initiatedByRole?: 'owner' | 'claimant' | 'admin' | null;
204
+ disputeReason?: string | null;
205
+ disputeDeadline?: string | null;
206
+ completedAt?: string | null;
207
+ createdAt: string;
208
+ updatedAt: string;
209
+ }
210
+ /**
211
+ * Start a push transfer. Provide **one** of:
212
+ * - `toEmail` / `toUserId` — a directed transfer to a named recipient (they accept).
213
+ * - `release: true` — an open release (the proof becomes claimable by anyone).
214
+ */
215
+ export interface TransferProofOptions {
216
+ /** Directed: recipient email (created/looked up if needed). */
217
+ toEmail?: string;
218
+ /** Directed: recipient user id, if already known. */
219
+ toUserId?: string;
220
+ /** Directed: display name for a newly-created recipient. */
221
+ toName?: string;
222
+ /** Open release: mark the proof claimable instead of directing it. */
223
+ release?: boolean;
224
+ /** Optional note included in the recipient email. */
225
+ message?: string;
226
+ /** Set `false` to skip the recipient notification email (directed only). */
227
+ notify?: boolean;
228
+ }
229
+ /** Result of initiating a push transfer. */
230
+ export interface TransferProofResult {
231
+ ok: boolean;
232
+ mode: 'directed' | 'open_release';
233
+ transfer: ProofTransfer;
234
+ }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.19 | Generated: 2026-08-20T18:06:08.134Z
3
+ Version: 1.15.20 | Generated: 2026-08-25T07:03:45.544Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -3524,6 +3524,8 @@ interface AuthKitLockoutPolicy {
3524
3524
 
3525
3525
  **VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
3526
3526
 
3527
+ **AuthKitConfigInput** = ``
3528
+
3527
3529
  **PasswordPolicyErrorCode** = ``
3528
3530
 
3529
3531
  **LoginSecurityErrorCode** = ``
@@ -7416,6 +7418,48 @@ interface RedeemGrantOptions {
7416
7418
  }
7417
7419
  ```
7418
7420
 
7421
+ **ProofTransfer** (interface)
7422
+ ```typescript
7423
+ interface ProofTransfer {
7424
+ id: string
7425
+ proofId: string
7426
+ productId?: string | null
7427
+ type: ProofTransferType
7428
+ state: ProofTransferState
7429
+ fromUserId?: string | null
7430
+ toUserId?: string | null
7431
+ toEmail?: string | null
7432
+ initiatedByUserId?: string | null
7433
+ initiatedByRole?: 'owner' | 'claimant' | 'admin' | null
7434
+ disputeReason?: string | null
7435
+ disputeDeadline?: string | null
7436
+ completedAt?: string | null
7437
+ createdAt: string
7438
+ updatedAt: string
7439
+ }
7440
+ ```
7441
+
7442
+ **TransferProofOptions** (interface)
7443
+ ```typescript
7444
+ interface TransferProofOptions {
7445
+ toEmail?: string
7446
+ toUserId?: string
7447
+ toName?: string
7448
+ release?: boolean
7449
+ message?: string
7450
+ notify?: boolean
7451
+ }
7452
+ ```
7453
+
7454
+ **TransferProofResult** (interface)
7455
+ ```typescript
7456
+ interface TransferProofResult {
7457
+ ok: boolean
7458
+ mode: 'directed' | 'open_release'
7459
+ transfer: ProofTransfer
7460
+ }
7461
+ ```
7462
+
7419
7463
  **ProofResponse** = `Proof`
7420
7464
 
7421
7465
  **ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
@@ -7430,6 +7474,10 @@ interface RedeemGrantOptions {
7430
7474
 
7431
7475
  **RedeemGrantResult** = ``
7432
7476
 
7477
+ **ProofTransferType** = `'directed' | 'open_release' | 'contested'`
7478
+
7479
+ **ProofTransferState** = ``
7480
+
7433
7481
  ### qr
7434
7482
 
7435
7483
  **QrShortCodeLookupResponse** (interface)
@@ -8952,22 +9000,22 @@ List devices trusted to skip MFA challenges for the current user (authenticated)
8952
9000
  Revoke a single trusted device by id (authenticated).
8953
9001
 
8954
9002
  **load**(authKitId: string) → `Promise<AuthKitConfig>`
8955
- Revoke a single trusted device by id (authenticated).
9003
+ Load the **public** AuthKit config for a client (no auth). Returns branding + the public security subset (`security.passwordPolicy` + `security.session`); `security.lockout` is admin-only and never included here. Use this in the login UI to render password checklists and drive idle sign-out.
8956
9004
 
8957
9005
  **get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
8958
- Revoke a single trusted device by id (authenticated).
9006
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8959
9007
 
8960
9008
  **list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
8961
- Revoke a single trusted device by id (authenticated).
9009
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8962
9010
 
8963
- **create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
8964
- Revoke a single trusted device by id (authenticated).
9011
+ **create**(collectionId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9012
+ Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}.
8965
9013
 
8966
- **update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
8967
- Revoke a single trusted device by id (authenticated).
9014
+ **update**(collectionId: string, authKitId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9015
+ Update an AuthKit client config (admin). This is how the account **security policy** is written — pass a `security` block ({@link AuthKitSecurityConfig}). The server validates it and enforces it; the login UI reads the public subset back via {@link load}.
8968
9016
 
8969
9017
  **remove**(collectionId: string, authKitId: string) → `Promise<void>`
8970
- Revoke a single trusted device by id (authenticated).
9018
+ Update an AuthKit client config (admin). This is how the account **security policy** is written — pass a `security` block ({@link AuthKitSecurityConfig}). The server validates it and enforces it; the login UI reads the public subset back via {@link load}.
8971
9019
 
8972
9020
  ### batch
8973
9021
 
@@ -10101,6 +10149,27 @@ Revoke a grant by id (owner / collection admin only). Takes effect immediately.
10101
10149
  options?: RedeemGrantOptions) → `Promise<RedeemGrantResult>`
10102
10150
  Redeem a grant token (anonymous or signed-in). Records the redemption and returns the granted scope, or — for a `verify_owner` grant — an ownership assertion (never the account). After redeeming, call {@link setGrantToken} so subsequent data requests carry the token.
10103
10151
 
10152
+ **transfer**(collectionId: string,
10153
+ productId: string,
10154
+ proofId: string,
10155
+ options: TransferProofOptions) → `Promise<TransferProofResult>`
10156
+ Start a push transfer of a proof (current owner / collection admin only). Directed — hand it to a named recipient who then calls {@link acceptTransfer}: ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' }) ``` Open release — make the proof claimable by anyone: ```ts await proof.transfer(collectionId, productId, proofId, { release: true }) ```
10157
+
10158
+ **acceptTransfer**(collectionId: string,
10159
+ productId: string,
10160
+ proofId: string) → `Promise<`
10161
+ Accept a directed transfer (the named recipient only). Completes the ownership move — the proof's `userId` becomes the caller and the previous owner's private data and share grants are cleared/voided.
10162
+
10163
+ **cancelTransfer**(collectionId: string,
10164
+ productId: string,
10165
+ proofId: string) → `Promise<`
10166
+ Cancel a pending push transfer (current owner / collection admin only).
10167
+
10168
+ **getTransfer**(collectionId: string,
10169
+ productId: string,
10170
+ proofId: string) → `Promise<`
10171
+ Get the active transfer/status for a proof (owner, collection admin, or the named recipient). Returns `{ transfer: null }` when nothing is in flight.
10172
+
10104
10173
  ### publicClient
10105
10174
 
10106
10175
  **chat**(collectionId: string,
package/docs/auth-kit.md CHANGED
@@ -504,9 +504,11 @@ await authKit.completePasswordReset(clientId, tokenFromUrl, 'newSecurePassword')
504
504
 
505
505
  ## Account security policy
506
506
 
507
- Each collection can configure account-security rules (via the admin console). **The API
508
- enforces all of it**; your login UI reads the policy for UX only (a live password checklist,
509
- idle sign-out). Read it from the config:
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):**
510
512
 
511
513
  ```ts
512
514
  const config = await authKit.load(clientId);
@@ -514,7 +516,39 @@ const policy = config.security?.passwordPolicy; // min length, char classes, bl
514
516
  const session = config.security?.session; // inactivity + absolute timeouts, rememberMe
515
517
  ```
516
518
 
517
- `lockout` values are admin-only and never returned here.
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.
518
552
 
519
553
  ### Password policy
520
554
 
package/docs/overview.md CHANGED
@@ -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/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:
@@ -19437,6 +19449,11 @@ components:
19437
19449
  type: number
19438
19450
  notifyUserOnLockout:
19439
19451
  type: boolean
19452
+ AuthKitConfigInput:
19453
+ type: string
19454
+ enum:
19455
+ - id
19456
+ - updatedAt
19440
19457
  PasswordPolicyErrorCode:
19441
19458
  type: string
19442
19459
  enum:
@@ -25180,6 +25197,87 @@ components:
25180
25197
  properties:
25181
25198
  guestName:
25182
25199
  type: string
25200
+ ProofTransfer:
25201
+ type: object
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
25183
25281
  QrShortCodeLookupResponse:
25184
25282
  type: object
25185
25283
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.15.19",
3
+ "version": "1.15.20",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",