@proveanything/smartlinks 1.15.18 → 1.15.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api/authKit.d.ts +27 -3
- package/dist/api/authKit.js +24 -0
- package/dist/api/proof.d.ts +58 -2
- package/dist/api/proof.js +74 -2
- package/dist/docs/API_SUMMARY.md +162 -18
- package/dist/docs/auth-kit.md +107 -0
- package/dist/docs/overview.md +3 -0
- package/dist/docs/proof-ownership-transfer.md +114 -0
- package/dist/index.d.ts +2 -2
- package/dist/openapi.yaml +189 -10
- package/dist/types/authKit.d.ts +84 -1
- package/dist/types/proof.d.ts +117 -5
- package/docs/API_SUMMARY.md +162 -18
- package/docs/auth-kit.md +107 -0
- package/docs/overview.md +3 -0
- package/docs/proof-ownership-transfer.md +114 -0
- package/openapi.yaml +189 -10
- package/package.json +1 -1
package/dist/types/proof.d.ts
CHANGED
|
@@ -40,16 +40,71 @@ export interface Proof {
|
|
|
40
40
|
values: ProofValues;
|
|
41
41
|
}
|
|
42
42
|
export type ProofResponse = Proof;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The proof's writable content, addressed by zone. Its keys mirror the proof
|
|
45
|
+
* document, so what you pass is what the proof looks like. Each zone has its own
|
|
46
|
+
* read/write visibility:
|
|
47
|
+
*
|
|
48
|
+
* | Zone | Stored at | Readable by | Writable by |
|
|
49
|
+
* |----------|----------------|------------------------|---------------|
|
|
50
|
+
* | `values` | `proof.values` | public + owner + admin | owner + admin |
|
|
51
|
+
* | `data` | `proof.data` | public + owner + admin | admin only |
|
|
52
|
+
* | `admin` | `proof.admin` | admin only | admin only |
|
|
53
|
+
* | `owner` | `proof.owner` | admin only | admin only |
|
|
54
|
+
*
|
|
55
|
+
* Use `data` for business fields everyone should *see* but only the business
|
|
56
|
+
* should *set* (e.g. a serial number). Use `admin` for fields only the business
|
|
57
|
+
* should see at all. On update, object zones deep-merge.
|
|
58
|
+
*/
|
|
59
|
+
export interface ProofWrite {
|
|
60
|
+
/**
|
|
61
|
+
* Choose the proof's ID (serial, NFC id, etc.). Honoured **on create only** —
|
|
62
|
+
* the ledger doc becomes `{productId}-{id}`. Omit to auto-generate. Ignored on
|
|
63
|
+
* update (a proof's ID is immutable).
|
|
64
|
+
*/
|
|
65
|
+
id?: string;
|
|
66
|
+
/** Owner + business-writable consumer data. Public + owner readable. → `proof.values` */
|
|
67
|
+
values?: ProofValues;
|
|
68
|
+
/** Business spec data (e.g. serialNo). Public + owner readable, admin-only writable. → `proof.data` */
|
|
46
69
|
data?: Record<string, JsonValue>;
|
|
47
|
-
/** Business-only spec data. */
|
|
70
|
+
/** Business-only spec data — admin-only read & write (stripped from public + owner). → `proof.admin` */
|
|
48
71
|
admin?: Record<string, JsonValue>;
|
|
72
|
+
/** Business-only root data — admin-only. → `proof.owner` */
|
|
73
|
+
owner?: Record<string, JsonValue>;
|
|
74
|
+
/** Is this proof available to be claimed. */
|
|
75
|
+
claimable?: boolean;
|
|
76
|
+
/** Any other named root field. */
|
|
77
|
+
[key: string]: JsonValue | Record<string, JsonValue> | ProofValues | undefined;
|
|
78
|
+
}
|
|
79
|
+
export interface ProofCreateRequest {
|
|
80
|
+
/**
|
|
81
|
+
* The proof to create, by zone (mirrors the proof document). This is the clear,
|
|
82
|
+
* recommended shape — `create(collectionId, productId, { proof: {...} })`.
|
|
83
|
+
*/
|
|
84
|
+
proof?: ProofWrite;
|
|
85
|
+
/** Owner + business-writable consumer data (→ `proof.values`). Same as `proof.values`. */
|
|
86
|
+
values?: ProofValues;
|
|
87
|
+
/** Canonical root `claimable` flag (also accepted as `proof.claimable`). */
|
|
49
88
|
claimable?: boolean;
|
|
50
89
|
virtual?: boolean;
|
|
90
|
+
/** @deprecated Legacy alias for `proof`. Use `proof`. */
|
|
91
|
+
core?: ProofWrite;
|
|
92
|
+
/**
|
|
93
|
+
* @deprecated On the request body this is folded into the **values bag**
|
|
94
|
+
* (public + owner-writable) — NOT `proof.data`. Use `proof.data`.
|
|
95
|
+
*/
|
|
96
|
+
data?: Record<string, JsonValue>;
|
|
97
|
+
/** @deprecated Not routed to `proof.admin` on create — use `proof.admin`. */
|
|
98
|
+
admin?: Record<string, JsonValue>;
|
|
51
99
|
}
|
|
52
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Update passes the proof's fields **at the root** — `update(c, p, id, { data: {...} })`
|
|
102
|
+
* sets `proof.data`, `{ values: {...} }` sets `proof.values`, etc. (A `proof` block is
|
|
103
|
+
* also accepted and routed the same way.) Object zones deep-merge.
|
|
104
|
+
*/
|
|
105
|
+
export type ProofUpdateRequest = Partial<ProofWrite> & {
|
|
106
|
+
proof?: ProofWrite;
|
|
107
|
+
};
|
|
53
108
|
export type ProofClaimRequest = Record<string, any>;
|
|
54
109
|
/**
|
|
55
110
|
* `'public'` (default, omitted) reads/writes `proof.values[key]`.
|
|
@@ -120,3 +175,60 @@ export type RedeemGrantResult = {
|
|
|
120
175
|
issuedAt?: string;
|
|
121
176
|
expiresAt?: string;
|
|
122
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
|
+
}
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.15.
|
|
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
|
|
|
@@ -3464,6 +3464,55 @@ interface AuthKitConfig {
|
|
|
3464
3464
|
supportEmail?: string
|
|
3465
3465
|
redirectUrl?: string
|
|
3466
3466
|
updatedAt?: string
|
|
3467
|
+
* Per-collection security policy. On the public config endpoint only
|
|
3468
|
+
* `passwordPolicy` + `session` are returned (the client renders password
|
|
3469
|
+
* checklists / idle sign-out from them); `lockout` is admin-only and enforced
|
|
3470
|
+
* server-side. See {@link AuthKitSecurityConfig}.
|
|
3471
|
+
security?: AuthKitSecurityConfig
|
|
3472
|
+
}
|
|
3473
|
+
```
|
|
3474
|
+
|
|
3475
|
+
**AuthKitSecurityConfig** (interface)
|
|
3476
|
+
```typescript
|
|
3477
|
+
interface AuthKitSecurityConfig {
|
|
3478
|
+
passwordPolicy?: AuthKitPasswordPolicy
|
|
3479
|
+
session?: AuthKitSessionPolicy
|
|
3480
|
+
lockout?: AuthKitLockoutPolicy
|
|
3481
|
+
}
|
|
3482
|
+
```
|
|
3483
|
+
|
|
3484
|
+
**AuthKitPasswordPolicy** (interface)
|
|
3485
|
+
```typescript
|
|
3486
|
+
interface AuthKitPasswordPolicy {
|
|
3487
|
+
minLength?: number
|
|
3488
|
+
requireUppercase?: boolean
|
|
3489
|
+
requireLowercase?: boolean
|
|
3490
|
+
requireNumber?: boolean
|
|
3491
|
+
requireSymbol?: boolean
|
|
3492
|
+
blockCommonPasswords?: boolean
|
|
3493
|
+
expiryDays?: number
|
|
3494
|
+
historyCount?: number
|
|
3495
|
+
}
|
|
3496
|
+
```
|
|
3497
|
+
|
|
3498
|
+
**AuthKitSessionPolicy** (interface)
|
|
3499
|
+
```typescript
|
|
3500
|
+
interface AuthKitSessionPolicy {
|
|
3501
|
+
inactivityTimeoutMinutes?: number
|
|
3502
|
+
inactivityWarningSeconds?: number
|
|
3503
|
+
absoluteTimeoutHours?: number
|
|
3504
|
+
rememberMe?: boolean
|
|
3505
|
+
}
|
|
3506
|
+
```
|
|
3507
|
+
|
|
3508
|
+
**AuthKitLockoutPolicy** (interface)
|
|
3509
|
+
```typescript
|
|
3510
|
+
interface AuthKitLockoutPolicy {
|
|
3511
|
+
enabled?: boolean
|
|
3512
|
+
maxFailedAttempts?: number
|
|
3513
|
+
attemptWindowMinutes?: number
|
|
3514
|
+
lockoutMinutes?: number
|
|
3515
|
+
notifyUserOnLockout?: boolean
|
|
3467
3516
|
}
|
|
3468
3517
|
```
|
|
3469
3518
|
|
|
@@ -3475,6 +3524,12 @@ interface AuthKitConfig {
|
|
|
3475
3524
|
|
|
3476
3525
|
**VerifyStatus** = `'pending' | 'verified' | 'failed' | 'expired' | 'unknown'`
|
|
3477
3526
|
|
|
3527
|
+
**AuthKitConfigInput** = ``
|
|
3528
|
+
|
|
3529
|
+
**PasswordPolicyErrorCode** = ``
|
|
3530
|
+
|
|
3531
|
+
**LoginSecurityErrorCode** = ``
|
|
3532
|
+
|
|
3478
3533
|
### batch
|
|
3479
3534
|
|
|
3480
3535
|
**FirebaseTimestamp** (interface)
|
|
@@ -7279,14 +7334,36 @@ interface Proof {
|
|
|
7279
7334
|
}
|
|
7280
7335
|
```
|
|
7281
7336
|
|
|
7282
|
-
**
|
|
7337
|
+
**ProofWrite** (interface)
|
|
7283
7338
|
```typescript
|
|
7284
|
-
interface
|
|
7285
|
-
|
|
7339
|
+
interface ProofWrite {
|
|
7340
|
+
* Choose the proof's ID (serial, NFC id, etc.). Honoured **on create only** —
|
|
7341
|
+
* the ledger doc becomes `{productId}-{id}`. Omit to auto-generate. Ignored on
|
|
7342
|
+
* update (a proof's ID is immutable).
|
|
7343
|
+
id?: string
|
|
7344
|
+
values?: ProofValues
|
|
7286
7345
|
data?: Record<string, JsonValue>
|
|
7287
7346
|
admin?: Record<string, JsonValue>
|
|
7347
|
+
owner?: Record<string, JsonValue>
|
|
7348
|
+
claimable?: boolean
|
|
7349
|
+
[key: string]: JsonValue | Record<string, JsonValue> | ProofValues | undefined
|
|
7350
|
+
}
|
|
7351
|
+
```
|
|
7352
|
+
|
|
7353
|
+
**ProofCreateRequest** (interface)
|
|
7354
|
+
```typescript
|
|
7355
|
+
interface ProofCreateRequest {
|
|
7356
|
+
* The proof to create, by zone (mirrors the proof document). This is the clear,
|
|
7357
|
+
* recommended shape — `create(collectionId, productId, { proof: {...} })`.
|
|
7358
|
+
proof?: ProofWrite
|
|
7359
|
+
values?: ProofValues
|
|
7288
7360
|
claimable?: boolean
|
|
7289
7361
|
virtual?: boolean
|
|
7362
|
+
core?: ProofWrite
|
|
7363
|
+
* @deprecated On the request body this is folded into the **values bag**
|
|
7364
|
+
* (public + owner-writable) — NOT `proof.data`. Use `proof.data`.
|
|
7365
|
+
data?: Record<string, JsonValue>
|
|
7366
|
+
admin?: Record<string, JsonValue>
|
|
7290
7367
|
}
|
|
7291
7368
|
```
|
|
7292
7369
|
|
|
@@ -7341,9 +7418,51 @@ interface RedeemGrantOptions {
|
|
|
7341
7418
|
}
|
|
7342
7419
|
```
|
|
7343
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
|
+
|
|
7344
7463
|
**ProofResponse** = `Proof`
|
|
7345
7464
|
|
|
7346
|
-
**ProofUpdateRequest** = `Partial<
|
|
7465
|
+
**ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
|
|
7347
7466
|
|
|
7348
7467
|
**ProofClaimRequest** = `Record<string, any>`
|
|
7349
7468
|
|
|
@@ -7355,6 +7474,10 @@ interface RedeemGrantOptions {
|
|
|
7355
7474
|
|
|
7356
7475
|
**RedeemGrantResult** = ``
|
|
7357
7476
|
|
|
7477
|
+
**ProofTransferType** = `'directed' | 'open_release' | 'contested'`
|
|
7478
|
+
|
|
7479
|
+
**ProofTransferState** = ``
|
|
7480
|
+
|
|
7358
7481
|
### qr
|
|
7359
7482
|
|
|
7360
7483
|
**QrShortCodeLookupResponse** (interface)
|
|
@@ -8748,10 +8871,10 @@ Gets current account information for the logged in user. Returns user, owner, ac
|
|
|
8748
8871
|
### authKit
|
|
8749
8872
|
|
|
8750
8873
|
**login**(clientId: string, email: string, password: string, trustedDeviceToken?: string) → `Promise<AuthLoginResponse>`
|
|
8751
|
-
Login with email + password (public). When the client's MFA policy requires a step-up, the server returns **403 `MFA_REQUIRED`** instead of a session — `login()` throws a `SmartlinksApiError` with `err.errorResponse?.errorCode === 'MFA_REQUIRED'` and the challenge details in `err.details` (see {@link MfaRequiredDetails}). Route the caller to {@link mfaChallengeSend} on that error; this method's return type is unchanged. returned one (via {@link mfaChallengeVerify}/{@link mfaRecoveryCode} with `trustDevice: true`), pass it here to skip the challenge entirely as long as it's still valid. If it's revoked/expired, the server silently falls back to requiring a fresh challenge — `login()` just returns `MFA_REQUIRED` again, no special handling.
|
|
8874
|
+
Login with email + password (public). When the client's MFA policy requires a step-up, the server returns **403 `MFA_REQUIRED`** instead of a session — `login()` throws a `SmartlinksApiError` with `err.errorResponse?.errorCode === 'MFA_REQUIRED'` and the challenge details in `err.details` (see {@link MfaRequiredDetails}). Route the caller to {@link mfaChallengeSend} on that error; this method's return type is unchanged. returned one (via {@link mfaChallengeVerify}/{@link mfaRecoveryCode} with `trustDevice: true`), pass it here to skip the challenge entirely as long as it's still valid. If it's revoked/expired, the server silently falls back to requiring a fresh challenge — `login()` just returns `MFA_REQUIRED` again, no special handling. Security errors (thrown as `SmartlinksApiError`, see {@link LoginSecurityErrorCode}): - `ACCOUNT_TEMPORARILY_LOCKED` (429) — `err.details.retryAfterSeconds` says how long to wait. - `PASSWORD_EXPIRED` (403) — `err.details.resetToken` is short-lived; route into {@link completePasswordReset} to change the password in place.
|
|
8752
8875
|
|
|
8753
8876
|
**register**(clientId: string, data: { email: string; password: string; displayName?: string; accountData?: Record<string, any> }) → `Promise<AuthLoginResponse>`
|
|
8754
|
-
Register a new user (public). Not gated by step-up MFA — a brand-new user has no enrolled factors yet, so there's nothing to challenge against.
|
|
8877
|
+
Register a new user (public). Not gated by step-up MFA — a brand-new user has no enrolled factors yet, so there's nothing to challenge against. The new password is validated against the collection's `passwordPolicy` — may throw a {@link PasswordPolicyErrorCode} (400). The same validation applies to {@link completePasswordReset} and {@link changePassword}. Read the policy for a live checklist from `authKit.load(clientId)` → `config.security.passwordPolicy`.
|
|
8755
8878
|
|
|
8756
8879
|
**googleLogin**(clientId: string, idToken: string, trustedDeviceToken?: string) → `Promise<AuthLoginResponse>`
|
|
8757
8880
|
Google OAuth login via ID token (public). Gated by step-up MFA — see {@link login} for the `MFA_REQUIRED` error shape. {@link mfaChallengeVerify}/{@link mfaRecoveryCode} (with `trustDevice: true`) to skip the challenge on this device, same as {@link login}.
|
|
@@ -8877,22 +9000,22 @@ List devices trusted to skip MFA challenges for the current user (authenticated)
|
|
|
8877
9000
|
Revoke a single trusted device by id (authenticated).
|
|
8878
9001
|
|
|
8879
9002
|
**load**(authKitId: string) → `Promise<AuthKitConfig>`
|
|
8880
|
-
|
|
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.
|
|
8881
9004
|
|
|
8882
9005
|
**get**(collectionId: string, authKitId: string) → `Promise<AuthKitConfig>`
|
|
8883
|
-
|
|
9006
|
+
Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
|
|
8884
9007
|
|
|
8885
9008
|
**list**(collectionId: string, admin?: boolean) → `Promise<AuthKitConfig[]>`
|
|
8886
|
-
|
|
9009
|
+
Get the full AuthKit config, including admin-only fields like `security.lockout` (admin auth).
|
|
8887
9010
|
|
|
8888
|
-
**create**(collectionId: string, data:
|
|
8889
|
-
|
|
9011
|
+
**create**(collectionId: string, data: AuthKitConfigInput) → `Promise<AuthKitConfig>`
|
|
9012
|
+
Create an AuthKit client config (admin). Accepts the account `security` policy — see {@link AuthKitConfigInput}.
|
|
8890
9013
|
|
|
8891
|
-
**update**(collectionId: string, authKitId: string, data:
|
|
8892
|
-
|
|
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}.
|
|
8893
9016
|
|
|
8894
9017
|
**remove**(collectionId: string, authKitId: string) → `Promise<void>`
|
|
8895
|
-
|
|
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}.
|
|
8896
9019
|
|
|
8897
9020
|
### batch
|
|
8898
9021
|
|
|
@@ -9952,14 +10075,14 @@ List all Proofs for a Collection.
|
|
|
9952
10075
|
|
|
9953
10076
|
**create**(collectionId: string,
|
|
9954
10077
|
productId: string,
|
|
9955
|
-
|
|
9956
|
-
Create a proof for a product (admin only). POST /admin/collection/:collectionId/product/:productId/proof
|
|
10078
|
+
request: ProofCreateRequest) → `Promise<ProofResponse>`
|
|
10079
|
+
Create a proof for a product (admin only). POST /admin/collection/:collectionId/product/:productId/proof Pass the proof's content in a `proof` block, keyed by zone (see {@link ProofWrite}): ```ts proof.create(collectionId, productId, { proof: { values: { colour: 'red' }, // public + owner readable, owner + admin writable data: { serialNo: 1001 }, // public + owner readable, ADMIN-only writable admin: { costPrice: 4.20 }, // admin-only }, claimable: true, }) ``` Note: a top-level `data`/`admin` on the request body is legacy — top-level `data` gets folded into the values bag, so use `proof.data` for `proof.data`.
|
|
9957
10080
|
|
|
9958
10081
|
**update**(collectionId: string,
|
|
9959
10082
|
productId: string,
|
|
9960
10083
|
proofId: string,
|
|
9961
10084
|
values: ProofUpdateRequest) → `Promise<ProofResponse>`
|
|
9962
|
-
Update a proof for a product (admin only). PUT /admin/collection/:collectionId/product/:productId/proof/:proofId
|
|
10085
|
+
Update a proof for a product (admin only). PUT /admin/collection/:collectionId/product/:productId/proof/:proofId Pass the fields to change **at the root**, keyed by zone (see {@link ProofWrite}): ```ts proof.update(collectionId, productId, proofId, { data: { serialNo: 1002 }, // → proof.data (admin-only writable) values: { colour: 'blue' }, // → proof.values }) ``` Object zones deep-merge, so you can change one field without wiping the rest.
|
|
9963
10086
|
|
|
9964
10087
|
**claim**(collectionId: string,
|
|
9965
10088
|
productId: string,
|
|
@@ -10026,6 +10149,27 @@ Revoke a grant by id (owner / collection admin only). Takes effect immediately.
|
|
|
10026
10149
|
options?: RedeemGrantOptions) → `Promise<RedeemGrantResult>`
|
|
10027
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.
|
|
10028
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
|
+
|
|
10029
10173
|
### publicClient
|
|
10030
10174
|
|
|
10031
10175
|
**chat**(collectionId: string,
|
package/docs/auth-kit.md
CHANGED
|
@@ -502,6 +502,113 @@ await authKit.completePasswordReset(clientId, tokenFromUrl, 'newSecurePassword')
|
|
|
502
502
|
|
|
503
503
|
---
|
|
504
504
|
|
|
505
|
+
## Account security policy
|
|
506
|
+
|
|
507
|
+
Each collection can configure account-security rules. **The API enforces all of it**; your
|
|
508
|
+
login UI reads the policy for UX only (a live password checklist, idle sign-out). There are
|
|
509
|
+
two sides: an **admin** writes the policy, and the **login UI** reads the public subset.
|
|
510
|
+
|
|
511
|
+
**Read (login UI, public — no auth):**
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
const config = await authKit.load(clientId);
|
|
515
|
+
const policy = config.security?.passwordPolicy; // min length, char classes, block-common
|
|
516
|
+
const session = config.security?.session; // inactivity + absolute timeouts, rememberMe
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
`lockout` values are admin-only and never returned by `load`.
|
|
520
|
+
|
|
521
|
+
**Write (admin):** set the whole `security` block with `authKit.update` (or `authKit.create`).
|
|
522
|
+
The shape is {@link AuthKitSecurityConfig}; see {@link AuthKitConfigInput}.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
await authKit.update(collectionId, clientId, {
|
|
526
|
+
security: {
|
|
527
|
+
passwordPolicy: {
|
|
528
|
+
minLength: 8, // hard floor enforced server-side
|
|
529
|
+
blockCommonPasswords: true, // reject the common/breached list
|
|
530
|
+
requireUppercase: false, requireLowercase: false,
|
|
531
|
+
requireNumber: false, requireSymbol: false,
|
|
532
|
+
expiryDays: 0, // 0 = never expires
|
|
533
|
+
historyCount: 0, // 0 = reuse allowed
|
|
534
|
+
},
|
|
535
|
+
lockout: { // admin-only; enforced server-side
|
|
536
|
+
enabled: true, maxFailedAttempts: 5,
|
|
537
|
+
attemptWindowMinutes: 15, lockoutMinutes: 15,
|
|
538
|
+
notifyUserOnLockout: true,
|
|
539
|
+
},
|
|
540
|
+
session: {
|
|
541
|
+
inactivityTimeoutMinutes: 0, // 0 = disabled (client-enforced when set)
|
|
542
|
+
inactivityWarningSeconds: 60,
|
|
543
|
+
absoluteTimeoutHours: 0, // 0 = use token lifetime (server-enforced)
|
|
544
|
+
rememberMe: true,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Defaults if a collection has never set a policy: 8-char minimum + block-common for everyone,
|
|
551
|
+
lockout disabled, no expiry/history, no idle/absolute timeout. Omit any field to take its default.
|
|
552
|
+
|
|
553
|
+
### Password policy
|
|
554
|
+
|
|
555
|
+
`register`, `completePasswordReset`, and `changePassword` validate the new password
|
|
556
|
+
server-side and throw a `SmartlinksApiError` with a {@link PasswordPolicyErrorCode}:
|
|
557
|
+
|
|
558
|
+
| `errorCode` (400) | Meaning |
|
|
559
|
+
|---|---|
|
|
560
|
+
| `PASSWORD_TOO_SHORT` | below `minLength` |
|
|
561
|
+
| `PASSWORD_REQUIREMENTS_NOT_MET` | missing a required character class |
|
|
562
|
+
| `PASSWORD_TOO_COMMON` | on the common/breached list |
|
|
563
|
+
| `PASSWORD_RECENTLY_USED` | matched one of the last `historyCount` passwords |
|
|
564
|
+
|
|
565
|
+
Render a live checklist from `policy` so users see the rules before submitting.
|
|
566
|
+
|
|
567
|
+
### Lockout
|
|
568
|
+
|
|
569
|
+
After too many failed logins the account is temporarily locked. `login` throws:
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
try {
|
|
573
|
+
await authKit.login(clientId, email, password);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err.errorCode === 'ACCOUNT_TEMPORARILY_LOCKED') {
|
|
576
|
+
const mins = Math.ceil(err.details.retryAfterSeconds / 60);
|
|
577
|
+
show(`Too many attempts. Try again in ${mins} minute(s).`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Failed MFA challenges count toward the same lock. Locking responds identically for unknown
|
|
583
|
+
accounts (no enumeration).
|
|
584
|
+
|
|
585
|
+
### Password expiry
|
|
586
|
+
|
|
587
|
+
If a password is older than `passwordPolicy.expiryDays`, a valid login is refused with
|
|
588
|
+
**403 `PASSWORD_EXPIRED`** carrying a short-lived `resetToken` — send the user straight into
|
|
589
|
+
the reset form to change it in place:
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
catch (err) {
|
|
593
|
+
if (err.errorCode === 'PASSWORD_EXPIRED') {
|
|
594
|
+
await authKit.completePasswordReset(clientId, err.details.resetToken, newPassword);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Session lifetime
|
|
600
|
+
|
|
601
|
+
- **Absolute timeout** (`session.absoluteTimeoutHours`) is enforced server-side on the native
|
|
602
|
+
refresh path: once the session is too old, `refreshToken` throws **401 `SESSION_EXPIRED`**
|
|
603
|
+
(see {@link RefreshErrorCode}) — clear storage and route to login. Web sessions use the
|
|
604
|
+
stateless bearer token and rely on inactivity sign-out below.
|
|
605
|
+
- **Inactivity timeout** (`session.inactivityTimeoutMinutes` / `inactivityWarningSeconds`) is
|
|
606
|
+
**client-enforced** — sign the user out after idle, warning first. Sync across tabs.
|
|
607
|
+
- **`session.rememberMe: false`** → don't persist tokens to durable storage; treat the session
|
|
608
|
+
as browser-scoped.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
505
612
|
## Relationship to other parts of the SDK
|
|
506
613
|
|
|
507
614
|
| Concern | Where it lives |
|
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).
|