@proveanything/smartlinks 1.9.17 → 1.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { AppCase, CreateCaseInput, UpdateCaseInput, AppendHistoryInput, CaseSummaryRequest, CaseSummaryResponse, CaseListQueryParams, AppThread, CreateThreadInput, UpdateThreadInput, ReplyInput, ThreadListQueryParams, AppRecord, CreateRecordInput, UpdateRecordInput, RecordListQueryParams, PaginatedResponse, AggregateRequest, AggregateResponse, RelatedResponse } from '../types/appObjects';
1
+ import type { AppCase, CreateCaseInput, UpdateCaseInput, AppendHistoryInput, CaseSummaryRequest, CaseSummaryResponse, CaseListQueryParams, AppThread, CreateThreadInput, UpdateThreadInput, ReplyInput, ThreadListQueryParams, AppRecord, CreateRecordInput, CreateRecordResponse, UpdateRecordInput, RecordListQueryParams, PaginatedResponse, AggregateRequest, AggregateResponse, RelatedResponse } from '../types/appObjects';
2
2
  export declare namespace app {
3
3
  namespace cases {
4
4
  /**
@@ -95,8 +95,15 @@ export declare namespace app {
95
95
  /**
96
96
  * Create a new record
97
97
  * POST /records
98
+ *
99
+ * When called on the public endpoint (admin = false) with an anonymous
100
+ * caller, and the app's `publicCreate.records.anonymous.edit.editToken`
101
+ * policy is enabled, the response includes a one-time `editToken` string.
102
+ * Store it immediately — it is never returned again.
103
+ *
104
+ * @see {@link updateWithToken} — use the edit token for a follow-up amendment
98
105
  */
99
- function create(collectionId: string, appId: string, input: CreateRecordInput, admin?: boolean): Promise<AppRecord>;
106
+ function create(collectionId: string, appId: string, input: CreateRecordInput, admin?: boolean): Promise<CreateRecordResponse>;
100
107
  /**
101
108
  * List records with optional query parameters
102
109
  * GET /records
@@ -113,6 +120,53 @@ export declare namespace app {
113
120
  * Admin can update any field, public (owner) can only update data and owner
114
121
  */
115
122
  function update(collectionId: string, appId: string, recordId: string, input: UpdateRecordInput, admin?: boolean): Promise<AppRecord>;
123
+ /**
124
+ * Amend the `data` zone of a record using an anonymous edit token.
125
+ * PATCH /records/:recordId (public endpoint, no auth)
126
+ *
127
+ * This is the follow-up call after an anonymous `create()` that returned an
128
+ * `editToken`. Present the token via `X-Edit-Token` — the server validates
129
+ * it with a constant-time comparison and, if `windowMinutes` is configured
130
+ * in the policy, checks that the token has not expired.
131
+ *
132
+ * **Scope:** only the `data` zone may be modified via this path.
133
+ * `owner`, `admin`, `status`, `visibility`, and indexed fields are
134
+ * immutable to anonymous token holders.
135
+ *
136
+ * @param collectionId - Collection the record belongs to
137
+ * @param appId - App the record belongs to
138
+ * @param recordId - ID of the record to amend
139
+ * @param data - New (full replacement) value for the `data` zone
140
+ * @param editToken - Token received from the original `create()` response
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * const record = await app.records.create(collectionId, appId, {
145
+ * recordType: 'payment',
146
+ * visibility: 'public',
147
+ * data: { amount: 9900, currency: 'USD' },
148
+ * })
149
+ * const { editToken } = record // store this immediately!
150
+ *
151
+ * // Later, once the payment gateway confirms:
152
+ * const updated = await app.records.updateWithToken(
153
+ * collectionId,
154
+ * appId,
155
+ * record.id,
156
+ * { amount: 9900, currency: 'USD', transactionId: 'txn_abc123' },
157
+ * editToken,
158
+ * )
159
+ * ```
160
+ *
161
+ * ### Error codes
162
+ * | HTTP | `errorCode` | Meaning |
163
+ * |------|-----------------------|---------------------------------------------------|
164
+ * | 401 | `UNAUTHORIZED` | No auth token and no `X-Edit-Token` header |
165
+ * | 403 | `FORBIDDEN` | Policy not enabled, or token does not match |
166
+ * | 403 | `EDIT_WINDOW_EXPIRED` | `windowMinutes` elapsed since record creation |
167
+ * | 404 | `NOT_FOUND` | Record does not exist |
168
+ */
169
+ function updateWithToken(collectionId: string, appId: string, recordId: string, data: Record<string, unknown>, editToken: string): Promise<AppRecord>;
116
170
  /**
117
171
  * Soft delete a record
118
172
  * DELETE /records/:recordId
@@ -187,6 +187,13 @@ export var app;
187
187
  /**
188
188
  * Create a new record
189
189
  * POST /records
190
+ *
191
+ * When called on the public endpoint (admin = false) with an anonymous
192
+ * caller, and the app's `publicCreate.records.anonymous.edit.editToken`
193
+ * policy is enabled, the response includes a one-time `editToken` string.
194
+ * Store it immediately — it is never returned again.
195
+ *
196
+ * @see {@link updateWithToken} — use the edit token for a follow-up amendment
190
197
  */
191
198
  async function create(collectionId, appId, input, admin = false) {
192
199
  const path = basePath(collectionId, appId, admin);
@@ -222,6 +229,57 @@ export var app;
222
229
  return patch(path, input);
223
230
  }
224
231
  records.update = update;
232
+ /**
233
+ * Amend the `data` zone of a record using an anonymous edit token.
234
+ * PATCH /records/:recordId (public endpoint, no auth)
235
+ *
236
+ * This is the follow-up call after an anonymous `create()` that returned an
237
+ * `editToken`. Present the token via `X-Edit-Token` — the server validates
238
+ * it with a constant-time comparison and, if `windowMinutes` is configured
239
+ * in the policy, checks that the token has not expired.
240
+ *
241
+ * **Scope:** only the `data` zone may be modified via this path.
242
+ * `owner`, `admin`, `status`, `visibility`, and indexed fields are
243
+ * immutable to anonymous token holders.
244
+ *
245
+ * @param collectionId - Collection the record belongs to
246
+ * @param appId - App the record belongs to
247
+ * @param recordId - ID of the record to amend
248
+ * @param data - New (full replacement) value for the `data` zone
249
+ * @param editToken - Token received from the original `create()` response
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * const record = await app.records.create(collectionId, appId, {
254
+ * recordType: 'payment',
255
+ * visibility: 'public',
256
+ * data: { amount: 9900, currency: 'USD' },
257
+ * })
258
+ * const { editToken } = record // store this immediately!
259
+ *
260
+ * // Later, once the payment gateway confirms:
261
+ * const updated = await app.records.updateWithToken(
262
+ * collectionId,
263
+ * appId,
264
+ * record.id,
265
+ * { amount: 9900, currency: 'USD', transactionId: 'txn_abc123' },
266
+ * editToken,
267
+ * )
268
+ * ```
269
+ *
270
+ * ### Error codes
271
+ * | HTTP | `errorCode` | Meaning |
272
+ * |------|-----------------------|---------------------------------------------------|
273
+ * | 401 | `UNAUTHORIZED` | No auth token and no `X-Edit-Token` header |
274
+ * | 403 | `FORBIDDEN` | Policy not enabled, or token does not match |
275
+ * | 403 | `EDIT_WINDOW_EXPIRED` | `windowMinutes` elapsed since record creation |
276
+ * | 404 | `NOT_FOUND` | Record does not exist |
277
+ */
278
+ async function updateWithToken(collectionId, appId, recordId, data, editToken) {
279
+ const path = `${basePath(collectionId, appId, false)}/${encodeURIComponent(recordId)}`;
280
+ return patch(path, { data }, { 'X-Edit-Token': editToken });
281
+ }
282
+ records.updateWithToken = updateWithToken;
225
283
  /**
226
284
  * Soft delete a record
227
285
  * DELETE /records/:recordId
@@ -0,0 +1,77 @@
1
+ import type { SmartLinksWidgetProps } from '../types';
2
+ /**
3
+ * Props for a SmartLinks container component.
4
+ *
5
+ * Extends {@link SmartLinksWidgetProps} with container-specific additions.
6
+ *
7
+ * ## Rendering modes
8
+ *
9
+ * Containers run in two modes, and props are the sole source of context in
10
+ * both:
11
+ *
12
+ * | Mode | How context arrives |
13
+ * |-------------------|--------------------------------------------------------|
14
+ * | Direct component | All props are passed directly by the parent platform |
15
+ * | Iframe fallback | Core params arrive via URL; `PublicPage` reads them |
16
+ * from search params when the equivalent prop is absent |
17
+ *
18
+ * ## Deep-link parameters (`dlParams`) in container mode
19
+ *
20
+ * When the parent platform resolves a deep link (e.g. from a scanned QR code,
21
+ * NFC tap, or shared URL), it **decodes** the link's `dlParams` payload and
22
+ * forwards each parameter as an explicit React prop — it does **not** append
23
+ * them to the browser URL.
24
+ *
25
+ * App-specific params (e.g. `preselectedAmount`, `voucherId`, `campaignSlug`)
26
+ * should therefore be declared as optional props directly on this interface.
27
+ * The parent guarantees that any param present in the `dlParams` payload will
28
+ * arrive as the corresponding named prop.
29
+ *
30
+ * ### PublicPage priority order
31
+ *
32
+ * `PublicPage` (and pages derived from it) **must** resolve app-specific params
33
+ * in the following priority order so the component works correctly in both
34
+ * rendering modes:
35
+ *
36
+ * ```
37
+ * 1. Prop value (set by parent in container / direct-component mode)
38
+ * 2. React Router search param (useSearchParams) in MemoryRouter
39
+ * 3. Raw URL search param (window.location) — iframe-mode fallback
40
+ * ```
41
+ *
42
+ * Example hook:
43
+ *
44
+ * ```ts
45
+ * function usePreselectedAmount(
46
+ * propValue: string | undefined,
47
+ * ): string | undefined {
48
+ * const [searchParams] = useSearchParams()
49
+ * return (
50
+ * propValue ??
51
+ * searchParams.get('preselectedAmount') ??
52
+ * new URLSearchParams(window.location.search).get('preselectedAmount') ??
53
+ * undefined
54
+ * )
55
+ * }
56
+ * ```
57
+ *
58
+ * @see docs/smartlinks/routing-guide.md — full pattern documentation
59
+ * @see docs/containers.md — container architecture overview
60
+ */
61
+ export interface SmartLinksContainerProps extends SmartLinksWidgetProps {
62
+ /**
63
+ * Optional CSS class applied to the outermost wrapper element rendered by
64
+ * the framework around this container.
65
+ */
66
+ className?: string;
67
+ /**
68
+ * Initial route path to navigate to inside the container's MemoryRouter.
69
+ *
70
+ * The framework sets the MemoryRouter's `initialEntries` from this value
71
+ * when present. Useful for deep links that target a specific page within
72
+ * the container (e.g. `'/loyalty/redeem'`).
73
+ *
74
+ * In iframe mode the equivalent is the `pageId` URL search parameter.
75
+ */
76
+ initialPath?: string;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.9.17 | Generated: 2026-04-15T16:54:07.573Z
3
+ Version: 1.9.20 | Generated: 2026-04-17T10:13:30.466Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -1957,22 +1957,51 @@ interface RelatedResponse {
1957
1957
  **PublicCreatePolicy** (interface)
1958
1958
  ```typescript
1959
1959
  interface PublicCreatePolicy {
1960
- cases?: PublicCreateRule
1961
- threads?: PublicCreateRule
1962
- records?: PublicCreateRule
1960
+ cases?: PublicCreateObjectRule
1961
+ threads?: PublicCreateObjectRule
1962
+ records?: PublicCreateObjectRule
1963
1963
  }
1964
1964
  ```
1965
1965
 
1966
- **PublicCreateRule** (interface)
1966
+ **PublicCreateObjectRule** (interface)
1967
1967
  ```typescript
1968
- interface PublicCreateRule {
1969
- allow: {
1970
- anonymous?: boolean
1971
- authenticated?: boolean
1972
- }
1968
+ interface PublicCreateObjectRule {
1969
+ anonymous?: PublicCreateBranch
1970
+ authenticated?: PublicCreateBranch
1971
+ }
1972
+ ```
1973
+
1974
+ **PublicCreateBranch** (interface)
1975
+ ```typescript
1976
+ interface PublicCreateBranch {
1977
+ allow: boolean
1978
+ * Field values merged **over** the caller's request body before writing.
1979
+ * Use this to lock down `visibility` and `status` regardless of what the
1980
+ * client sends.
1973
1981
  enforce?: {
1974
- anonymous?: Partial<CreateCaseInput | CreateThreadInput | CreateRecordInput>
1975
- authenticated?: Partial<CreateCaseInput | CreateThreadInput | CreateRecordInput>
1982
+ visibility?: 'public' | 'owner' | 'admin'
1983
+ status?: string
1984
+ }
1985
+ * Anonymous edit-token configuration.
1986
+ * **Records only** — ignored for cases and threads.
1987
+ *
1988
+ * When `editToken: true`, the server generates a one-time 256-bit hex token
1989
+ * on anonymous record creation, stores it in `admin.editToken` (never
1990
+ * exposed to public / owner responses), and returns it **once** in the
1991
+ * creation response under the `editToken` key.
1992
+ *
1993
+ * The client can then pass that token as the `X-Edit-Token` header on
1994
+ * `PATCH /records/:recordId` to amend the `data` zone without
1995
+ * authentication.
1996
+ *
1997
+ * @see {@link CreateRecordResponse} — creation response shape
1998
+ * @see {@link records.updateWithToken} — SDK method for the amendment call
1999
+ edit?: {
2000
+ editToken: boolean
2001
+ * Optional expiry window in minutes from `createdAt`.
2002
+ * After this many minutes the token is rejected with HTTP 403
2003
+ * `EDIT_WINDOW_EXPIRED`. Omit for no expiry.
2004
+ windowMinutes?: number
1976
2005
  }
1977
2006
  }
1978
2007
  ```
@@ -6654,6 +6683,46 @@ interface TranslationUpdateRequest {
6654
6683
 
6655
6684
  **VariantUpdateRequest** = `any`
6656
6685
 
6686
+ ### widgets
6687
+
6688
+ **NavigationRequest** (interface)
6689
+ ```typescript
6690
+ interface NavigationRequest {
6691
+ appId: string
6692
+ deepLink?: string
6693
+ params?: Record<string, string>
6694
+ productId?: string
6695
+ proofId?: string
6696
+ }
6697
+ ```
6698
+
6699
+ **SmartLinksWidgetProps** (interface)
6700
+ ```typescript
6701
+ interface SmartLinksWidgetProps {
6702
+ collectionId: string
6703
+ appId: string
6704
+ productId?: string
6705
+ proofId?: string
6706
+ user?: {
6707
+ id?: string
6708
+ email?: string
6709
+ name?: string
6710
+ admin?: boolean
6711
+ }
6712
+ * Pre-initialised SmartLinks SDK instance provided by the parent platform.
6713
+ * At runtime this is `typeof import('@proveanything/smartlinks')`.
6714
+ SL: Record<string, unknown>
6715
+ * Navigation callback. Emit a `NavigationRequest` to ask the parent
6716
+ * platform to navigate to another app. A legacy plain-string path is also
6717
+ * accepted for backward compatibility.
6718
+ onNavigate?: (request: NavigationRequest | string) => void
6719
+ publicPortalUrl?: string
6720
+ size?: 'compact' | 'standard' | 'large'
6721
+ lang?: string
6722
+ translations?: Record<string, string>
6723
+ }
6724
+ ```
6725
+
6657
6726
  ### appConfiguration (api)
6658
6727
 
6659
6728
  **AppConfigOptions** (type)
@@ -6778,6 +6847,18 @@ interface UserInfo {
6778
6847
  interface ProductInfo {
6779
6848
  id: string
6780
6849
  tags?: Record<string, any>
6850
+ * Facet values assigned to this product.
6851
+ * Shape mirrors `ProductFacetMap`: a map of facet key → array of value objects.
6852
+ * Each value object must have at minimum a `key` string property.
6853
+ *
6854
+ * @example
6855
+ * ```ts
6856
+ * {
6857
+ * material: [{ key: 'cotton', name: 'Cotton' }],
6858
+ * certifications: [{ key: 'organic', name: 'Organic' }, { key: 'recycled', name: 'Recycled' }]
6859
+ * }
6860
+ * ```
6861
+ facets?: Record<string, Array<{ key: string; [k: string]: unknown }>>
6781
6862
  }
6782
6863
  ```
6783
6864
 
@@ -7024,8 +7105,8 @@ General-purpose structured app objects. Use these when a simple scoped data item
7024
7105
  **create**(collectionId: string,
7025
7106
  appId: string,
7026
7107
  input: CreateRecordInput,
7027
- admin: boolean = false) → `Promise<AppRecord>`
7028
- Create a new record POST /records
7108
+ admin: boolean = false) → `Promise<CreateRecordResponse>`
7109
+ Create a new record POST /records When called on the public endpoint (admin = false) with an anonymous caller, and the app's `publicCreate.records.anonymous.edit.editToken` policy is enabled, the response includes a one-time `editToken` string. Store it immediately — it is never returned again.
7029
7110
 
7030
7111
  **list**(collectionId: string,
7031
7112
  appId: string,
@@ -7046,6 +7127,13 @@ Get a single record by ID GET /records/:recordId
7046
7127
  admin: boolean = false) → `Promise<AppRecord>`
7047
7128
  Update a record PATCH /records/:recordId Admin can update any field, public (owner) can only update data and owner
7048
7129
 
7130
+ **updateWithToken**(collectionId: string,
7131
+ appId: string,
7132
+ recordId: string,
7133
+ data: Record<string, unknown>,
7134
+ editToken: string) → `Promise<AppRecord>`
7135
+ Amend the `data` zone of a record using an anonymous edit token. PATCH /records/:recordId (public endpoint, no auth) This is the follow-up call after an anonymous `create()` that returned an `editToken`. Present the token via `X-Edit-Token` — the server validates it with a constant-time comparison and, if `windowMinutes` is configured in the policy, checks that the token has not expired. **Scope:** only the `data` zone may be modified via this path. `owner`, `admin`, `status`, `visibility`, and indexed fields are immutable to anonymous token holders. ```ts const record = await app.records.create(collectionId, appId, { recordType: 'payment', visibility: 'public', data: { amount: 9900, currency: 'USD' }, }) const { editToken } = record // store this immediately! // Later, once the payment gateway confirms: const updated = await app.records.updateWithToken( collectionId, appId, record.id, { amount: 9900, currency: 'USD', transactionId: 'txn_abc123' }, editToken, ) ``` ### Error codes | HTTP | `errorCode` | Meaning | |------|-----------------------|---------------------------------------------------| | 401 | `UNAUTHORIZED` | No auth token and no `X-Edit-Token` header | | 403 | `FORBIDDEN` | Policy not enabled, or token does not match | | 403 | `EDIT_WINDOW_EXPIRED` | `windowMinutes` elapsed since record creation | | 404 | `NOT_FOUND` | Record does not exist |
7136
+
7049
7137
  **remove**(collectionId: string,
7050
7138
  appId: string,
7051
7139
  recordId: string,
@@ -576,47 +576,81 @@ const usageStats = await app.records.aggregate(collectionId, appId, {
576
576
 
577
577
  ## Public Create Policies
578
578
 
579
- Control who can create objects on **public endpoints** by setting a `publicCreate` policy on your app's config. This is a `publicCreate` field inside your app config object (identified by `appId` within your collection).
579
+ Control who can create objects on **public endpoints** by setting a `publicCreate` policy on your app's config document (identified by `appId` within your collection).
580
+
581
+ Set the policy via:
582
+ ```
583
+ POST /api/v1/admin/collection/:collectionId/apps/:appId
584
+ ```
585
+
586
+ The server reads this document at request time — no cache invalidation or service restart is required.
580
587
 
581
588
  ### Policy Structure
582
589
 
590
+ Each object type (`cases`, `threads`, `records`) has **independent branches** for anonymous and authenticated callers. Each branch carries its own `allow` flag, optional field overrides (`enforce`), and — for records — optional edit-token config (`edit`).
591
+
583
592
  ```typescript
584
593
  interface PublicCreatePolicy {
585
- cases?: {
586
- allow: {
587
- anonymous?: boolean // allow unauthenticated users
588
- authenticated?: boolean // allow authenticated contacts
589
- }
590
- enforce?: {
591
- anonymous?: Partial<CreateCaseInput> // force these values for anon
592
- authenticated?: Partial<CreateCaseInput> // force these values for auth
593
- }
594
+ cases?: PublicCreateObjectRule
595
+ threads?: PublicCreateObjectRule
596
+ records?: PublicCreateObjectRule
597
+ }
598
+
599
+ interface PublicCreateObjectRule {
600
+ anonymous?: PublicCreateBranch
601
+ authenticated?: PublicCreateBranch
602
+ }
603
+
604
+ interface PublicCreateBranch {
605
+ /** Whether creation is permitted for this caller class */
606
+ allow: boolean
607
+
608
+ /**
609
+ * Hard overrides merged over the caller's body before writing.
610
+ * Lock down visibility and status regardless of what clients send.
611
+ */
612
+ enforce?: {
613
+ visibility?: 'public' | 'owner' | 'admin'
614
+ status?: string
615
+ }
616
+
617
+ /**
618
+ * Anonymous edit-token config — records only.
619
+ * See "Anonymous Edit Tokens" section below.
620
+ */
621
+ edit?: {
622
+ editToken: boolean
623
+ windowMinutes?: number // omit for no expiry
594
624
  }
595
- threads?: { /* same structure */ }
596
- records?: { /* same structure */ }
597
625
  }
598
626
  ```
599
627
 
628
+ #### Visibility enforcement guard-rails
629
+
630
+ The server silently corrects misconfigured visibility values:
631
+
632
+ | Caller type | `enforce.visibility` supplied | Server overrides to |
633
+ |-----------------|-------------------------------|---------------------|
634
+ | `anonymous` | `'owner'` | `'admin'` |
635
+ | `authenticated` | `'public'` | `'owner'` |
636
+
637
+ These guards exist because anonymous callers have no identity to own a record, and `'public'` visibility on authenticated-only objects would be a misconfiguration.
638
+
600
639
  ### Example Policies
601
640
 
602
641
  **Support tickets from anyone:**
603
642
 
604
643
  ```json
605
644
  {
606
- "cases": {
607
- "allow": {
608
- "anonymous": true,
609
- "authenticated": true
610
- },
611
- "enforce": {
645
+ "publicCreate": {
646
+ "cases": {
612
647
  "anonymous": {
613
- "visibility": "owner",
614
- "status": "open",
615
- "category": "support"
648
+ "allow": true,
649
+ "enforce": { "visibility": "public", "status": "open" }
616
650
  },
617
651
  "authenticated": {
618
- "visibility": "owner",
619
- "status": "open"
652
+ "allow": true,
653
+ "enforce": { "visibility": "owner", "status": "open" }
620
654
  }
621
655
  }
622
656
  }
@@ -627,15 +661,35 @@ interface PublicCreatePolicy {
627
661
 
628
662
  ```json
629
663
  {
630
- "threads": {
631
- "allow": {
632
- "anonymous": false,
633
- "authenticated": true
634
- },
635
- "enforce": {
664
+ "publicCreate": {
665
+ "threads": {
666
+ "anonymous": { "allow": false },
636
667
  "authenticated": {
637
- "visibility": "public",
638
- "status": "open"
668
+ "allow": true,
669
+ "enforce": { "visibility": "public", "status": "open" }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ ```
675
+
676
+ **Anonymous record creation with edit token (30-minute window):**
677
+
678
+ ```json
679
+ {
680
+ "publicCreate": {
681
+ "records": {
682
+ "anonymous": {
683
+ "allow": true,
684
+ "enforce": { "visibility": "public", "status": "pending" },
685
+ "edit": {
686
+ "editToken": true,
687
+ "windowMinutes": 30
688
+ }
689
+ },
690
+ "authenticated": {
691
+ "allow": true,
692
+ "enforce": { "visibility": "owner", "status": "pending" }
639
693
  }
640
694
  }
641
695
  }
@@ -646,16 +700,113 @@ interface PublicCreatePolicy {
646
700
 
647
701
  ```json
648
702
  {
649
- "records": {
650
- "allow": {
651
- "anonymous": false,
652
- "authenticated": false
703
+ "publicCreate": {
704
+ "records": {
705
+ "anonymous": { "allow": false },
706
+ "authenticated": { "allow": false }
653
707
  }
654
708
  }
655
709
  }
656
710
  ```
657
711
 
658
- The `enforce` values are **merged over** the caller's request body, so you can lock down fields like `visibility`, `status`, or `category` regardless of what clients send.
712
+ The `enforce` values are **merged over** the caller's request body, so you can lock down fields like `visibility` and `status` regardless of what clients send.
713
+
714
+ ---
715
+
716
+ ## Anonymous Edit Tokens
717
+
718
+ Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
719
+
720
+ Designed for flows where a client needs to make a follow-up update before a server-side process locks the record. Common examples: payment + confirmation, multi-step forms, IoT device registration.
721
+
722
+ ### How It Works
723
+
724
+ ```
725
+ 1. Configure — set publicCreate.records.anonymous.edit.editToken: true in app config
726
+ 2. Create — anonymous POST /records returns { ...record, editToken: "3f8a2c1e..." }
727
+ Token is stored in record's admin zone; never visible again
728
+ 3. Amend — PATCH /records/:recordId with X-Edit-Token header
729
+ Only the data zone may be modified
730
+ 4. Expiry — if windowMinutes is set, token is rejected after that many minutes
731
+ ```
732
+
733
+ ### SDK Usage
734
+
735
+ ```typescript
736
+ import { app } from '@proveanything/smartlinks';
737
+
738
+ // Step 1: Create the record (anonymous caller — no auth token)
739
+ const response = await app.records.create(collectionId, appId, {
740
+ recordType: 'payment',
741
+ visibility: 'public',
742
+ data: { amount: 9900, currency: 'USD' },
743
+ })
744
+
745
+ // editToken is present only when the policy has editToken: true
746
+ const { editToken } = response // ⚠️ store immediately — returned once only
747
+
748
+ // Step 2: After external confirmation (e.g. payment gateway callback)
749
+ const updated = await app.records.updateWithToken(
750
+ collectionId,
751
+ appId,
752
+ response.id,
753
+ { amount: 9900, currency: 'USD', transactionId: 'txn_abc123' },
754
+ editToken,
755
+ )
756
+ ```
757
+
758
+ `app.records.updateWithToken()` sends the token as the `X-Edit-Token` request header on the public PATCH endpoint — no auth token needed.
759
+
760
+ ### Creation Response Shape
761
+
762
+ ```typescript
763
+ interface CreateRecordResponse extends AppRecord {
764
+ /**
765
+ * Present only on anonymous creation when editToken policy is enabled.
766
+ * Returned ONCE — store it client-side immediately.
767
+ */
768
+ editToken?: string
769
+ }
770
+ ```
771
+
772
+ Example creation response:
773
+
774
+ ```json
775
+ {
776
+ "id": "a1b2c3d4-...",
777
+ "recordType": "payment",
778
+ "status": "pending",
779
+ "visibility": "public",
780
+ "data": { "amount": 9900, "currency": "USD" },
781
+ "createdAt": "2026-04-16T12:00:00.000Z",
782
+ "editToken": "3f8a2c1e..."
783
+ }
784
+ ```
785
+
786
+ ### Amendment Scope
787
+
788
+ Anonymous token updates may only modify the **`data` zone**. The following are immutable via this path:
789
+
790
+ - `owner`, `admin` zones
791
+ - `status`, `visibility`
792
+ - All indexed fields (`recordType`, `ref`, `startsAt`, `expiresAt`, etc.)
793
+
794
+ ### Error Codes
795
+
796
+ | HTTP | `errorCode` | Meaning |
797
+ |------|------------------------|--------------------------------------------------|
798
+ | 401 | `UNAUTHORIZED` | No auth token and no `X-Edit-Token` header |
799
+ | 403 | `FORBIDDEN` | `editToken` policy not enabled for this app |
800
+ | 403 | `FORBIDDEN` | Token does not match |
801
+ | 403 | `EDIT_WINDOW_EXPIRED` | `windowMinutes` elapsed since record creation |
802
+ | 404 | `NOT_FOUND` | Record does not exist |
803
+
804
+ ### Security Notes
805
+
806
+ - The token is stored in `admin.editToken` and is **always stripped** from public and owner responses — it cannot be read back after creation.
807
+ - Token comparison uses `crypto.timingSafeEqual` to prevent timing-based oracle attacks.
808
+ - The token is a 32-byte (`crypto.randomBytes(32)`) hex string — 256 bits of entropy.
809
+ - For sensitive flows, combine `windowMinutes` with a server-side process that removes or overwrites the token once the record is confirmed.
659
810
 
660
811
  ---
661
812