@proveanything/smartlinks 1.15.19 → 1.15.21

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.21 | Generated: 2026-08-25T08:34:52.897Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -33,6 +33,8 @@ For detailed guides on specific features:
33
33
  - **[Theme System](theme.system.md)** - Theme configuration and customization
34
34
  - **[Theme Defaults](theme-defaults.md)** - Default theme values and presets
35
35
  - **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
36
+ - **[Proof Share Grants](proof-share-grants.md)** - Delegated, scoped, revocable bearer access to a single proof (read/comment/verify-owner links)
37
+ - **[Proof Ownership Transfer](proof-ownership-transfer.md)** - Moving a proof to a new owner: directed transfer, open release, accept/cancel, and the state machine
36
38
  - **[Product Facets SDK](PRODUCT_FACETS_SDK.md)** - Admin and public product facet endpoints and TypeScript interfaces
37
39
  - **[Attestations](attestations.md)** - Append-only fact log with cryptographic chain integrity, time-series analytics, and public/owner/admin visibility
38
40
  - **[Auth Kit](auth-kit.md)** - End-user authentication flows (email/password, magic link, OTP, OAuth) for microapps
@@ -3524,6 +3526,8 @@ interface AuthKitLockoutPolicy {
3524
3526
 
3525
3527
  **VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
3526
3528
 
3529
+ **AuthKitConfigInput** = ``
3530
+
3527
3531
  **PasswordPolicyErrorCode** = ``
3528
3532
 
3529
3533
  **LoginSecurityErrorCode** = ``
@@ -7416,6 +7420,48 @@ interface RedeemGrantOptions {
7416
7420
  }
7417
7421
  ```
7418
7422
 
7423
+ **ProofTransfer** (interface)
7424
+ ```typescript
7425
+ interface ProofTransfer {
7426
+ id: string
7427
+ proofId: string
7428
+ productId?: string | null
7429
+ type: ProofTransferType
7430
+ state: ProofTransferState
7431
+ fromUserId?: string | null
7432
+ toUserId?: string | null
7433
+ toEmail?: string | null
7434
+ initiatedByUserId?: string | null
7435
+ initiatedByRole?: 'owner' | 'claimant' | 'admin' | null
7436
+ disputeReason?: string | null
7437
+ disputeDeadline?: string | null
7438
+ completedAt?: string | null
7439
+ createdAt: string
7440
+ updatedAt: string
7441
+ }
7442
+ ```
7443
+
7444
+ **TransferProofOptions** (interface)
7445
+ ```typescript
7446
+ interface TransferProofOptions {
7447
+ toEmail?: string
7448
+ toUserId?: string
7449
+ toName?: string
7450
+ release?: boolean
7451
+ message?: string
7452
+ notify?: boolean
7453
+ }
7454
+ ```
7455
+
7456
+ **TransferProofResult** (interface)
7457
+ ```typescript
7458
+ interface TransferProofResult {
7459
+ ok: boolean
7460
+ mode: 'directed' | 'open_release'
7461
+ transfer: ProofTransfer
7462
+ }
7463
+ ```
7464
+
7419
7465
  **ProofResponse** = `Proof`
7420
7466
 
7421
7467
  **ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
@@ -7430,6 +7476,10 @@ interface RedeemGrantOptions {
7430
7476
 
7431
7477
  **RedeemGrantResult** = ``
7432
7478
 
7479
+ **ProofTransferType** = `'directed' | 'open_release' | 'contested'`
7480
+
7481
+ **ProofTransferState** = ``
7482
+
7433
7483
  ### qr
7434
7484
 
7435
7485
  **QrShortCodeLookupResponse** (interface)
@@ -8952,22 +9002,22 @@ List devices trusted to skip MFA challenges for the current user (authenticated)
8952
9002
  Revoke a single trusted device by id (authenticated).
8953
9003
 
8954
9004
  **load**(authKitId: string) → `Promise<AuthKitConfig>`
8955
- Revoke a single trusted device by id (authenticated).
9005
+ 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
9006
 
8957
9007
  **get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
8958
- Revoke a single trusted device by id (authenticated).
9008
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8959
9009
 
8960
9010
  **list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
8961
- Revoke a single trusted device by id (authenticated).
9011
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8962
9012
 
8963
- **create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
8964
- Revoke a single trusted device by id (authenticated).
9013
+ **create**(collectionId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9014
+ Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}.
8965
9015
 
8966
- **update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
8967
- Revoke a single trusted device by id (authenticated).
9016
+ **update**(collectionId: string, authKitId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9017
+ 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
9018
 
8969
9019
  **remove**(collectionId: string, authKitId: string) → `Promise<void>`
8970
- Revoke a single trusted device by id (authenticated).
9020
+ 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
9021
 
8972
9022
  ### batch
8973
9023
 
@@ -10101,6 +10151,27 @@ Revoke a grant by id (owner / collection admin only). Takes effect immediately.
10101
10151
  options?: RedeemGrantOptions) → `Promise<RedeemGrantResult>`
10102
10152
  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
10153
 
10154
+ **transfer**(collectionId: string,
10155
+ productId: string,
10156
+ proofId: string,
10157
+ options: TransferProofOptions) → `Promise<TransferProofResult>`
10158
+ 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 }) ```
10159
+
10160
+ **acceptTransfer**(collectionId: string,
10161
+ productId: string,
10162
+ proofId: string) → `Promise<`
10163
+ 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.
10164
+
10165
+ **cancelTransfer**(collectionId: string,
10166
+ productId: string,
10167
+ proofId: string) → `Promise<`
10168
+ Cancel a pending push transfer (current owner / collection admin only).
10169
+
10170
+ **getTransfer**(collectionId: string,
10171
+ productId: string,
10172
+ proofId: string) → `Promise<`
10173
+ Get the active transfer/status for a proof (owner, collection admin, or the named recipient). Returns `{ transfer: null }` when nothing is in flight.
10174
+
10104
10175
  ### publicClient
10105
10176
 
10106
10177
  **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.21 | Generated: 2026-08-25T08:34:52.897Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -33,6 +33,8 @@ For detailed guides on specific features:
33
33
  - **[Theme System](theme.system.md)** - Theme configuration and customization
34
34
  - **[Theme Defaults](theme-defaults.md)** - Default theme values and presets
35
35
  - **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
36
+ - **[Proof Share Grants](proof-share-grants.md)** - Delegated, scoped, revocable bearer access to a single proof (read/comment/verify-owner links)
37
+ - **[Proof Ownership Transfer](proof-ownership-transfer.md)** - Moving a proof to a new owner: directed transfer, open release, accept/cancel, and the state machine
36
38
  - **[Product Facets SDK](PRODUCT_FACETS_SDK.md)** - Admin and public product facet endpoints and TypeScript interfaces
37
39
  - **[Attestations](attestations.md)** - Append-only fact log with cryptographic chain integrity, time-series analytics, and public/owner/admin visibility
38
40
  - **[Auth Kit](auth-kit.md)** - End-user authentication flows (email/password, magic link, OTP, OAuth) for microapps
@@ -3524,6 +3526,8 @@ interface AuthKitLockoutPolicy {
3524
3526
 
3525
3527
  **VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
3526
3528
 
3529
+ **AuthKitConfigInput** = ``
3530
+
3527
3531
  **PasswordPolicyErrorCode** = ``
3528
3532
 
3529
3533
  **LoginSecurityErrorCode** = ``
@@ -7416,6 +7420,48 @@ interface RedeemGrantOptions {
7416
7420
  }
7417
7421
  ```
7418
7422
 
7423
+ **ProofTransfer** (interface)
7424
+ ```typescript
7425
+ interface ProofTransfer {
7426
+ id: string
7427
+ proofId: string
7428
+ productId?: string | null
7429
+ type: ProofTransferType
7430
+ state: ProofTransferState
7431
+ fromUserId?: string | null
7432
+ toUserId?: string | null
7433
+ toEmail?: string | null
7434
+ initiatedByUserId?: string | null
7435
+ initiatedByRole?: 'owner' | 'claimant' | 'admin' | null
7436
+ disputeReason?: string | null
7437
+ disputeDeadline?: string | null
7438
+ completedAt?: string | null
7439
+ createdAt: string
7440
+ updatedAt: string
7441
+ }
7442
+ ```
7443
+
7444
+ **TransferProofOptions** (interface)
7445
+ ```typescript
7446
+ interface TransferProofOptions {
7447
+ toEmail?: string
7448
+ toUserId?: string
7449
+ toName?: string
7450
+ release?: boolean
7451
+ message?: string
7452
+ notify?: boolean
7453
+ }
7454
+ ```
7455
+
7456
+ **TransferProofResult** (interface)
7457
+ ```typescript
7458
+ interface TransferProofResult {
7459
+ ok: boolean
7460
+ mode: 'directed' | 'open_release'
7461
+ transfer: ProofTransfer
7462
+ }
7463
+ ```
7464
+
7419
7465
  **ProofResponse** = `Proof`
7420
7466
 
7421
7467
  **ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
@@ -7430,6 +7476,10 @@ interface RedeemGrantOptions {
7430
7476
 
7431
7477
  **RedeemGrantResult** = ``
7432
7478
 
7479
+ **ProofTransferType** = `'directed' | 'open_release' | 'contested'`
7480
+
7481
+ **ProofTransferState** = ``
7482
+
7433
7483
  ### qr
7434
7484
 
7435
7485
  **QrShortCodeLookupResponse** (interface)
@@ -8952,22 +9002,22 @@ List devices trusted to skip MFA challenges for the current user (authenticated)
8952
9002
  Revoke a single trusted device by id (authenticated).
8953
9003
 
8954
9004
  **load**(authKitId: string) → `Promise<AuthKitConfig>`
8955
- Revoke a single trusted device by id (authenticated).
9005
+ 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
9006
 
8957
9007
  **get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
8958
- Revoke a single trusted device by id (authenticated).
9008
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8959
9009
 
8960
9010
  **list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
8961
- Revoke a single trusted device by id (authenticated).
9011
+ Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
8962
9012
 
8963
- **create**(collectionId: string, data: any) → `Promise<AuthKitConfig>`
8964
- Revoke a single trusted device by id (authenticated).
9013
+ **create**(collectionId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9014
+ Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}.
8965
9015
 
8966
- **update**(collectionId: string, authKitId: string, data: any) → `Promise<AuthKitConfig>`
8967
- Revoke a single trusted device by id (authenticated).
9016
+ **update**(collectionId: string, authKitId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
9017
+ 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
9018
 
8969
9019
  **remove**(collectionId: string, authKitId: string) → `Promise<void>`
8970
- Revoke a single trusted device by id (authenticated).
9020
+ 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
9021
 
8972
9022
  ### batch
8973
9023
 
@@ -10101,6 +10151,27 @@ Revoke a grant by id (owner / collection admin only). Takes effect immediately.
10101
10151
  options?: RedeemGrantOptions) → `Promise<RedeemGrantResult>`
10102
10152
  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
10153
 
10154
+ **transfer**(collectionId: string,
10155
+ productId: string,
10156
+ proofId: string,
10157
+ options: TransferProofOptions) → `Promise<TransferProofResult>`
10158
+ 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 }) ```
10159
+
10160
+ **acceptTransfer**(collectionId: string,
10161
+ productId: string,
10162
+ proofId: string) → `Promise<`
10163
+ 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.
10164
+
10165
+ **cancelTransfer**(collectionId: string,
10166
+ productId: string,
10167
+ proofId: string) → `Promise<`
10168
+ Cancel a pending push transfer (current owner / collection admin only).
10169
+
10170
+ **getTransfer**(collectionId: string,
10171
+ productId: string,
10172
+ proofId: string) → `Promise<`
10173
+ Get the active transfer/status for a proof (owner, collection admin, or the named recipient). Returns `{ transfer: null }` when nothing is in flight.
10174
+
10104
10175
  ### publicClient
10105
10176
 
10106
10177
  **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.21",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",