@proveanything/smartlinks 1.15.17 → 1.15.18

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 CHANGED
@@ -22,6 +22,9 @@ If you're new to the SDK, this is the easiest path:
22
22
 
23
23
  - [docs/ai.md](docs/ai.md) — AI responses, chat, RAG, voice, streaming, and product assistants
24
24
  - [docs/analytics.md](docs/analytics.md) — fire-and-forget web analytics, tag scan telemetry, and dashboard queries
25
+ - [docs/proof-share-grants.md](docs/proof-share-grants.md) — delegated, scoped, revocable share links for a proof (album sharing, guest comments, proof-of-ownership)
26
+ - [docs/assets.md](docs/assets.md) — asset uploads, including resumable uploads for large files (video)
27
+ - [docs/app-objects.md](docs/app-objects.md) — cases, threads (comments), and records
25
28
  - [docs/translations.md](docs/translations.md) — runtime translation lookup, browser-side caching, and translation admin flows
26
29
  - [docs/widgets.md](docs/widgets.md) — embeddable React components
27
30
  - [docs/realtime.md](docs/realtime.md) — subscriptions and live updates
@@ -33,6 +36,8 @@ If you're new to the SDK, this is the easiest path:
33
36
  - **Build an AI assistant** → start with [docs/ai.md](docs/ai.md)
34
37
  - **Track page views, clicks, or tag scans** → start with [docs/analytics.md](docs/analytics.md)
35
38
  - **Translate dynamic content with local browser caching** → start with [docs/translations.md](docs/translations.md)
39
+ - **Share a proof / album by link, or add guest comments** → start with [docs/proof-share-grants.md](docs/proof-share-grants.md)
40
+ - **Upload large video with resume-on-reconnect** → see [Resumable uploads](docs/assets.md#resumable-uploads-large-files-eg-video)
36
41
  - **Fetch collections/products** → see [Quick start](README.md#quick-start)
37
42
  - **Authenticate admins or end users** → see [Authentication](README.md#authentication)
38
43
  - **Upload and manage files** → see [Assets](README.md#assets)
@@ -44,6 +49,9 @@ For the full list of functions and types, see the API summary:
44
49
  **Documentation:**
45
50
  - [AI & Chat Completions](docs/ai.md) - Chat completions, RAG, voice integration
46
51
  - [Analytics](docs/analytics.md) - Fire-and-forget analytics tracking, tag scans, and dashboard queries
52
+ - [Proof Share Grants](docs/proof-share-grants.md) - Delegated, revocable share links; guest comments; proof-of-ownership
53
+ - [Assets](docs/assets.md) - Uploads, resumable large-file uploads, token uploads, replace/versioning
54
+ - [App Objects](docs/app-objects.md) - Cases, threads (comments), records
47
55
  - [Translations](docs/translations.md) - Runtime translation lookup, browser-side IndexedDB caching, and admin translation management
48
56
  - [Widgets](docs/widgets.md) - Embeddable React components
49
57
  - [Realtime](docs/realtime.md) - Realtime data updates
@@ -85,6 +85,12 @@ export declare namespace app {
85
85
  * Atomically appends to replies array, increments replyCount, updates lastReplyAt
86
86
  */
87
87
  function reply(collectionId: string, appId: string, threadId: string, input: ReplyInput, admin?: boolean): Promise<AppThread>;
88
+ /**
89
+ * Delete a single reply from a thread by its reply id (moderation).
90
+ * DELETE /threads/:threadId/reply/:replyId
91
+ * Authorised for the reply's author, the proof owner, or a collection admin.
92
+ */
93
+ function deleteReply(collectionId: string, appId: string, threadId: string, replyId: string, admin?: boolean): Promise<AppThread>;
88
94
  /**
89
95
  * Get aggregate statistics for threads
90
96
  * POST /threads/aggregate
@@ -164,6 +164,16 @@ export var app;
164
164
  return post(path, input);
165
165
  }
166
166
  threads.reply = reply;
167
+ /**
168
+ * Delete a single reply from a thread by its reply id (moderation).
169
+ * DELETE /threads/:threadId/reply/:replyId
170
+ * Authorised for the reply's author, the proof owner, or a collection admin.
171
+ */
172
+ async function deleteReply(collectionId, appId, threadId, replyId, admin = false) {
173
+ const path = `${basePath(collectionId, appId, admin)}/${encodeURIComponent(threadId)}/reply/${encodeURIComponent(replyId)}`;
174
+ return del(path);
175
+ }
176
+ threads.deleteReply = deleteReply;
167
177
  /**
168
178
  * Get aggregate statistics for threads
169
179
  * POST /threads/aggregate
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.17 | Generated: 2026-08-18T08:10:50.608Z
3
+ Version: 1.15.18 | Generated: 2026-08-19T06:40:47.505Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -1993,6 +1993,10 @@ interface CreateThreadInput {
1993
1993
  data?: Record<string, unknown>
1994
1994
  owner?: Record<string, unknown>
1995
1995
  admin?: Record<string, unknown> // admin only
1996
+ * Optional atomic first reply. Posting a comment no longer needs a separate
1997
+ * create-thread-then-reply round trip (which could orphan an empty thread on
1998
+ * partial failure). The reply is stored with a generated `id` and timestamp.
1999
+ firstReply?: ReplyInput
1996
2000
  }
1997
2001
  ```
1998
2002
 
@@ -8435,6 +8439,13 @@ Soft delete a thread DELETE /threads/:threadId
8435
8439
  admin: boolean = false) → `Promise<AppThread>`
8436
8440
  Add a reply to a thread POST /threads/:threadId/reply Atomically appends to replies array, increments replyCount, updates lastReplyAt
8437
8441
 
8442
+ **deleteReply**(collectionId: string,
8443
+ appId: string,
8444
+ threadId: string,
8445
+ replyId: string,
8446
+ admin: boolean = false) → `Promise<AppThread>`
8447
+ Delete a single reply from a thread by its reply id (moderation). DELETE /threads/:threadId/reply/:replyId Authorised for the reply's author, the proof owner, or a collection admin.
8448
+
8438
8449
  **aggregate**(collectionId: string,
8439
8450
  appId: string,
8440
8451
  request: AggregateRequest,
@@ -429,6 +429,71 @@ const productComments = await app.threads.list(collectionId, appId, {
429
429
  });
430
430
  ```
431
431
 
432
+ ### Anchoring to app entities and proofs
433
+
434
+ Anchor a thread to your own entity with `parentType` + `parentId`. `parentId` is a
435
+ free-form string (SmartLinks short ids, not just UUIDs), so you can use your app's
436
+ native ids directly — no need to stash them in `body` or a tag.
437
+
438
+ ```typescript
439
+ await app.threads.create(collectionId, appId, {
440
+ parentType: 'memory',
441
+ parentId: memoryId, // e.g. "dVdthBQLAjQEnitU7aWy" — text, not a UUID
442
+ proofId, // anchor to a proof (enables grant-tier reads)
443
+ body: { text: 'Lovely photo' },
444
+ });
445
+ ```
446
+
447
+ List filters that pair with this (see `ThreadListQueryParams`):
448
+
449
+ | Param | Purpose |
450
+ |-------|---------|
451
+ | `parentType` / `parentId` | one app entity's threads |
452
+ | `parentIds` | **batch** — every thread for many entities in one call (e.g. a feed) |
453
+ | `proofId` / `productId` | threads anchored to a proof / product |
454
+
455
+ ```typescript
456
+ // One request for the 30 memories currently on screen:
457
+ const feed = await app.threads.list(collectionId, appId, {
458
+ parentType: 'memory',
459
+ parentIds: visibleMemoryIds,
460
+ sort: 'createdAt:desc',
461
+ });
462
+ ```
463
+
464
+ ### Atomic first comment (`firstReply`)
465
+
466
+ Posting the first comment no longer needs a create-thread-then-reply two-step (which
467
+ could leave an orphan empty thread if the reply failed). Pass `firstReply` to create
468
+ the thread and its first reply in one atomic call:
469
+
470
+ ```typescript
471
+ await app.threads.create(collectionId, appId, {
472
+ parentType: 'memory', parentId: memoryId, proofId,
473
+ firstReply: { text: 'Lovely photo', authorName: 'Sam' },
474
+ });
475
+ ```
476
+
477
+ ### Deleting a reply (moderation)
478
+
479
+ Replies carry a stable `id`. Remove a single one — authorised for the reply's author,
480
+ the proof owner, or a collection admin:
481
+
482
+ ```typescript
483
+ await app.threads.deleteReply(collectionId, appId, threadId, replyId);
484
+ ```
485
+
486
+ ### Grant-tier access (private, shareable comments)
487
+
488
+ A comment thread on a shared album should be `visibility: 'owner'` (private to the
489
+ proof), with access coming from a **share grant** rather than opening the data to the
490
+ world. A holder of an active `read`/`comment` grant on the proof is treated as
491
+ owner-tier **for that proof only**. Enable it with the `publicCreate.threads.grant`
492
+ policy branch and carry the token via `setGrantToken`.
493
+
494
+ See **[Proof Share Grants](proof-share-grants.md)** for the full flow (create/redeem
495
+ grants, guest commenting, revocation, and the app config).
496
+
432
497
  ---
433
498
 
434
499
  ## Records
@@ -243,6 +243,87 @@ await Api.asset.bulkDelete({
243
243
 
244
244
  ---
245
245
 
246
+ ## Resumable uploads (large files, e.g. video)
247
+
248
+ `asset.upload()` is a single request — if the connection drops, the whole file
249
+ restarts. For large files on flaky connections (e.g. phone video), use a
250
+ **resumable** upload: the file is chunked directly to storage and can be paused,
251
+ resumed, and — crucially — **continued after a page reload or app restart**.
252
+
253
+ ```typescript
254
+ import { asset } from '@proveanything/smartlinks'
255
+
256
+ // 1. Open a resumable upload.
257
+ const handle = await asset.createResumableUpload({
258
+ file, // a File (input[type=file] / drag-drop)
259
+ scope: { type: 'proof', collectionId, productId, proofId },
260
+ appId: 'photo-memory',
261
+ // token: uploadToken, // for public/token uploads (see below)
262
+ })
263
+
264
+ // 2. Persist handle.id so the upload survives a reload.
265
+ localStorage.setItem('pendingUpload', handle.id)
266
+
267
+ // 3. Upload. Resumes automatically from the offset storage already holds.
268
+ const uploaded = await handle.start({
269
+ onProgress: (pct) => setProgress(pct), // 0–100
270
+ signal: abortController.signal, // optional: cancel a stalled upload
271
+ })
272
+ localStorage.removeItem('pendingUpload')
273
+ ```
274
+
275
+ ### Pause / resume, and resume after a reload
276
+
277
+ ```typescript
278
+ handle.pause() // stop after the current chunk
279
+ await handle.resume({ onProgress }) // continue
280
+
281
+ // After a reload / app kill — rehydrate from the persisted id and the same file:
282
+ const saved = localStorage.getItem('pendingUpload')
283
+ if (saved) {
284
+ const handle = await asset.resumeUpload(saved, file)
285
+ await handle.start({ onProgress }) // continues, does not restart
286
+ }
287
+ ```
288
+
289
+ ### API
290
+
291
+ ```typescript
292
+ namespace asset {
293
+ createResumableUpload(options: CreateResumableUploadOptions): Promise<ResumableUploadHandle>
294
+ resumeUpload(handleId: string, file: File): Promise<ResumableUploadHandle>
295
+ }
296
+
297
+ interface CreateResumableUploadOptions {
298
+ file: File
299
+ scope: { type: 'collection'; collectionId: string }
300
+ | { type: 'product'; collectionId: string; productId: string }
301
+ | { type: 'proof'; collectionId: string; productId: string; proofId: string }
302
+ name?: string
303
+ metadata?: Record<string, any>
304
+ appId?: string
305
+ admin?: boolean // admin route (default is the public route)
306
+ token?: string // upload token for public/unauthenticated uploads
307
+ }
308
+
309
+ interface ResumableUploadHandle {
310
+ readonly id: string // durable, persistable — pass to resumeUpload() after a reload
311
+ readonly size: number // total bytes
312
+ start(opts?: { onProgress?: (pct: number) => void; signal?: AbortSignal }): Promise<Asset>
313
+ pause(): void
314
+ resume(opts?: { onProgress?: (pct: number) => void; signal?: AbortSignal }): Promise<Asset>
315
+ }
316
+ ```
317
+
318
+ **Notes**
319
+
320
+ - `handle.id` is an opaque string that carries everything needed to resume — persist it as-is.
321
+ - On completion, `start()`/`resume()` resolves to the finalized `Asset` record.
322
+ - `pause()` causes the in-flight `start()`/`resume()` promise to reject with an `UploadPausedError`; call `resume()` to continue.
323
+ - `asset.upload()` also now accepts an `AbortSignal` (`upload({ ..., signal })`) for cancelling a stalled single-shot upload.
324
+
325
+ ---
326
+
246
327
  ## Public (token-based) uploads
247
328
 
248
329
  For anonymous or contact-initiated uploads from the portal — no admin auth required.
@@ -0,0 +1,232 @@
1
+ # Proof Share Grants
2
+
3
+ Delegated, scoped, **revocable** bearer access to a single proof — the middle tier
4
+ between "public" (everyone) and "owner" (only the signed-in owner).
5
+
6
+ A grant lets an owner hand out a link that lets specific recipients **see or do
7
+ specific things on one proof for a limited time**, without those recipients needing
8
+ a SmartLinks account or a proof claim. Typical uses: sharing a private photo album,
9
+ letting guests comment on it, or publishing a verifiable "I own this" assertion.
10
+
11
+ ---
12
+
13
+ ## Concepts
14
+
15
+ A **grant** is a row issued by the proof owner (or a collection admin) and redeemed
16
+ by a bearer holding an opaque token. Every data request that touches the proof
17
+ re-checks the grant **server-side** against the database, so revocation is immediate.
18
+
19
+ ### Scopes — what a grant authorises
20
+
21
+ | Scope | Grants the bearer… |
22
+ |-------|--------------------|
23
+ | `read` | read owner-tier data on the proof (attestations, threads, records, cases) |
24
+ | `comment` | create threads/replies on the proof (guest comments) |
25
+ | `admin` | read owner-tier data (reserved for elevated share cases; never exposes the platform admin zone) |
26
+ | `verify_owner` | redeem a shareable ownership **assertion** (not the account) |
27
+
28
+ A grant can carry several scopes, e.g. `['read', 'comment']` for a shareable,
29
+ commentable album.
30
+
31
+ ### Security & lifecycle
32
+
33
+ - The token is opaque, unguessable, and returned to the issuer **exactly once** (on `createGrant`). It is never returned by `listGrants`.
34
+ - **Revocation is immediate** — the grant is re-checked on every request, so `revokeGrant` invalidates a token across all clients at once.
35
+ - **Auto-invalidation on transfer** — every grant is voided the moment the proof's `ownerId` changes (e.g. a resale/re-claim), so a stale "I own this" link cannot keep resolving.
36
+ - A grant is scoped to **one proof**; it can never widen access to other proofs or collection-level data.
37
+
38
+ ---
39
+
40
+ ## Owner flow — create, list, revoke
41
+
42
+ ```typescript
43
+ import { proof } from '@proveanything/smartlinks'
44
+
45
+ // Create a read+comment grant that expires in 7 days.
46
+ const grant = await proof.createGrant(collectionId, productId, proofId, {
47
+ scope: ['read', 'comment'],
48
+ audience: { kind: 'public_link' },
49
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
50
+ })
51
+
52
+ // `grant.token` is available ONLY here — embed it in your share link now.
53
+ const shareUrl = `https://app.example.com/album?proofId=${proofId}&shareToken=${grant.token}`
54
+
55
+ // List active + past grants (tokens are never included).
56
+ const grants = await proof.listGrants(collectionId, productId, proofId)
57
+
58
+ // Stop sharing — takes effect on the very next request from any client.
59
+ await proof.revokeGrant(collectionId, productId, proofId, grant.grantId)
60
+ ```
61
+
62
+ `createGrant` / `listGrants` / `revokeGrant` require the caller to be the **proof
63
+ owner** (or a collection admin) — i.e. a signed-in user whose `bearerToken` is set.
64
+
65
+ ---
66
+
67
+ ## Recipient flow — redeem, then carry the token
68
+
69
+ A recipient opens the share link, redeems the token once, then sets it as the
70
+ active grant token. From then on **every** SDK request carries the token
71
+ (`X-Grant-Token`), so all proof reads/writes are evaluated against the grant.
72
+
73
+ ```typescript
74
+ import { proof, setGrantToken } from '@proveanything/smartlinks'
75
+
76
+ const shareToken = new URLSearchParams(location.search).get('shareToken')!
77
+
78
+ // Redeem once (anonymous or signed-in). Records the redemption; optionally names the guest.
79
+ await proof.redeemGrant(collectionId, productId, proofId, shareToken, {
80
+ guestName: 'Sam', // stamped on guest activity when not signed in
81
+ })
82
+
83
+ // Attach the token to every subsequent request.
84
+ setGrantToken(shareToken)
85
+
86
+ // Now grant-tier reads succeed — e.g. owner-visibility memories on the proof:
87
+ const { attestations } = await attestation.publicList(collectionId, {
88
+ subjectType: 'proof', subjectId: proofId,
89
+ })
90
+
91
+ // Clear it when leaving the shared view:
92
+ setGrantToken(undefined)
93
+ ```
94
+
95
+ > **Persisting across reloads.** `setGrantToken` holds the token in memory. To keep a
96
+ > shared session across reloads, persist `shareToken` yourself (e.g. in `localStorage`)
97
+ > and call `setGrantToken` again on load.
98
+
99
+ ---
100
+
101
+ ## Guest commenting
102
+
103
+ With a `comment` scope grant, a bearer can post comments even when they are not
104
+ signed in. The app must enable the **grant branch** of the thread create policy
105
+ (see [App config](#app-config)); comments are then created at `visibility: 'owner'`
106
+ (private to the proof) and stamped `authorType: 'guest'`.
107
+
108
+ ```typescript
109
+ import { app } from '@proveanything/smartlinks'
110
+ // setGrantToken(shareToken) has already been called.
111
+
112
+ // One atomic call — no separate create-then-reply round trip.
113
+ await app.threads.create(collectionId, 'photo-memory', {
114
+ parentType: 'memory',
115
+ parentId: memoryId, // text — SmartLinks short ids are fine (not just UUIDs)
116
+ proofId, // anchor to the proof so grant readers see it
117
+ firstReply: { text: 'Lovely photo', authorName: 'Sam' },
118
+ })
119
+ ```
120
+
121
+ Other grant holders (and the owner) see these comments because a `read` grant reveals
122
+ `owner`-visibility threads for the proof. See
123
+ [App Objects → Threads](app-objects.md#threads) for the full threads API.
124
+
125
+ ---
126
+
127
+ ## Proof of ownership — `verify_owner`
128
+
129
+ Ownership itself is **not** a grant — it is `proof.ownerId`, established via the
130
+ existing [claim flow](proof-claiming-methods.md). A `verify_owner` grant only
131
+ publishes a shareable, verifiable **assertion** derived from that ownership, without
132
+ handing over the account:
133
+
134
+ ```typescript
135
+ const grant = await proof.createGrant(collectionId, productId, proofId, {
136
+ scope: ['verify_owner'],
137
+ })
138
+
139
+ // The recipient redeems it and gets the assertion — never the account:
140
+ const result = await proof.redeemGrant(collectionId, productId, proofId, grant.token)
141
+ // { proofId, assertsOwnership: true, ownerDisplayName?, issuedAt, expiresAt }
142
+ ```
143
+
144
+ Because grants auto-invalidate on transfer, a resale cannot leave a stale
145
+ "I own this" link in circulation.
146
+
147
+ ---
148
+
149
+ ## What a grant gates
150
+
151
+ When a valid grant token is present, these public reads elevate to owner-tier for the
152
+ granted proof (and only that proof):
153
+
154
+ - **Attestations** — `attestation.publicList({ subjectType: 'proof', subjectId })`
155
+ - **Threads / Records / Cases** — `app.threads.list`, `app.records.*`, `app.cases.list`, and the single-item GETs, filtered to the granted proof
156
+ - **Thread creation / replies** — with a `comment` scope grant (see below)
157
+
158
+ The token never exposes the platform `admin` zone, and only reveals `owner`-visibility
159
+ rows for the granted `proofId`.
160
+
161
+ ---
162
+
163
+ ## App config
164
+
165
+ Grant-based commenting is opt-in per app, configured on the app's Firestore config
166
+ at `sites/{collectionId}/apps/{appId}` — a `grant` branch alongside
167
+ `anonymous` / `authenticated`:
168
+
169
+ ```jsonc
170
+ {
171
+ "publicCreate": {
172
+ "threads": {
173
+ "grant": {
174
+ "allow": true,
175
+ "requireScope": "comment",
176
+ "enforce": { "visibility": "owner", "status": "open" }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ This enables grant-scoped commenting **without** opening up anonymous creation. The
184
+ `enforce.visibility: "owner"` keeps comments private to the proof (visible to the
185
+ owner and other grant holders, not the wider public).
186
+
187
+ ---
188
+
189
+ ## API reference
190
+
191
+ ```typescript
192
+ namespace proof {
193
+ createGrant(collectionId, productId, proofId, options: CreateGrantOptions): Promise<ProofGrant>
194
+ listGrants(collectionId, productId, proofId): Promise<ProofGrant[]>
195
+ revokeGrant(collectionId, productId, proofId, grantId): Promise<void>
196
+ redeemGrant(collectionId, productId, proofId, token, options?: RedeemGrantOptions): Promise<RedeemGrantResult>
197
+ }
198
+
199
+ // Attach / clear the active grant token (sent as X-Grant-Token on every request).
200
+ function setGrantToken(token: string | undefined): void
201
+ function getGrantToken(): string | undefined
202
+
203
+ type GrantScope = 'read' | 'comment' | 'admin' | 'verify_owner'
204
+
205
+ interface CreateGrantOptions {
206
+ scope: GrantScope[] // at least one
207
+ audience?: { kind: 'public_link' } | { kind: 'named'; email?: string; userId?: string }
208
+ expiresAt?: Date | string
209
+ }
210
+
211
+ interface RedeemGrantOptions { guestName?: string }
212
+
213
+ type RedeemGrantResult =
214
+ | { scope: GrantScope[]; redeemedAt: string }
215
+ | { proofId: string; assertsOwnership: true; ownerDisplayName?: string; issuedAt?: string; expiresAt?: string }
216
+
217
+ interface ProofGrant {
218
+ grantId: string
219
+ proofId: string
220
+ productId?: string | null
221
+ scope: GrantScope[]
222
+ audience: { kind: 'public_link' | 'named'; email?: string; userId?: string }
223
+ createdBy: string
224
+ expiresAt?: string | null
225
+ revokedAt?: string | null
226
+ redeemedBy?: { userId?: string; guestName?: string; redeemedAt: string }
227
+ redeemCount: number
228
+ createdAt: string
229
+ updatedAt: string
230
+ token?: string // present ONLY on the createGrant response
231
+ }
232
+ ```
package/dist/openapi.yaml CHANGED
@@ -13941,6 +13941,23 @@ paths:
13941
13941
  required: false
13942
13942
  schema:
13943
13943
  type: string
13944
+ - name: parentIds
13945
+ in: query
13946
+ required: false
13947
+ schema:
13948
+ type: array
13949
+ items:
13950
+ type: string
13951
+ - name: proofId
13952
+ in: query
13953
+ required: false
13954
+ schema:
13955
+ type: string
13956
+ - name: productId
13957
+ in: query
13958
+ required: false
13959
+ schema:
13960
+ type: string
13944
13961
  - name: tag
13945
13962
  in: query
13946
13963
  required: false
@@ -14182,6 +14199,52 @@ paths:
14182
14199
  application/json:
14183
14200
  schema:
14184
14201
  $ref: "#/components/schemas/ReplyInput"
14202
+ /{zone}/collection/{collectionId}/app/{appId}/threads/{threadId}/reply/{replyId}:
14203
+ delete:
14204
+ tags:
14205
+ - threads
14206
+ summary: Delete a single reply from a thread by its reply id (moderation).
14207
+ operationId: threads_deleteReply
14208
+ security: []
14209
+ parameters:
14210
+ - name: zone
14211
+ in: path
14212
+ required: true
14213
+ schema:
14214
+ type: string
14215
+ - name: collectionId
14216
+ in: path
14217
+ required: true
14218
+ schema:
14219
+ type: string
14220
+ - name: appId
14221
+ in: path
14222
+ required: true
14223
+ schema:
14224
+ type: string
14225
+ - name: threadId
14226
+ in: path
14227
+ required: true
14228
+ schema:
14229
+ type: string
14230
+ - name: replyId
14231
+ in: path
14232
+ required: true
14233
+ schema:
14234
+ type: string
14235
+ responses:
14236
+ 200:
14237
+ description: Success
14238
+ content:
14239
+ application/json:
14240
+ schema:
14241
+ $ref: "#/components/schemas/AppThread"
14242
+ 400:
14243
+ description: Bad request
14244
+ 401:
14245
+ description: Unauthorized
14246
+ 404:
14247
+ description: Not found
14185
14248
  components:
14186
14249
  securitySchemes:
14187
14250
  bearerAuth:
@@ -16967,6 +17030,8 @@ components:
16967
17030
  admin:
16968
17031
  type: object
16969
17032
  additionalProperties: true
17033
+ firstReply:
17034
+ $ref: "#/components/schemas/ReplyInput"
16970
17035
  UpdateThreadInput:
16971
17036
  type: object
16972
17037
  properties:
@@ -17012,6 +17077,14 @@ components:
17012
17077
  type: string
17013
17078
  parentId:
17014
17079
  type: string
17080
+ parentIds:
17081
+ type: array
17082
+ items:
17083
+ type: string
17084
+ proofId:
17085
+ type: string
17086
+ productId:
17087
+ type: string
17015
17088
  tag:
17016
17089
  type: string
17017
17090
  contactId:
@@ -250,6 +250,12 @@ export interface CreateThreadInput {
250
250
  data?: Record<string, unknown>;
251
251
  owner?: Record<string, unknown>;
252
252
  admin?: Record<string, unknown>;
253
+ /**
254
+ * Optional atomic first reply. Posting a comment no longer needs a separate
255
+ * create-thread-then-reply round trip (which could orphan an empty thread on
256
+ * partial failure). The reply is stored with a generated `id` and timestamp.
257
+ */
258
+ firstReply?: ReplyInput;
253
259
  }
254
260
  /**
255
261
  * Input for updating a thread
@@ -279,8 +285,16 @@ export interface ReplyInput {
279
285
  export interface ThreadListQueryParams extends ListQueryParams {
280
286
  slug?: string;
281
287
  authorId?: string;
288
+ /** Disambiguates the parent entity kind (e.g. "memory", "case", "proof"). */
282
289
  parentType?: string;
290
+ /** Anchor to a single app entity id (text — SmartLinks short ids, not UUIDs). */
283
291
  parentId?: string;
292
+ /** Batch-fetch threads for many app entities in one call (e.g. a memory feed). */
293
+ parentIds?: string[];
294
+ /** Anchor threads to a proof. For grant-token callers this is the enforced filter. */
295
+ proofId?: string;
296
+ /** Anchor threads to a product (one tier up from proof). */
297
+ productId?: string;
284
298
  tag?: string;
285
299
  contactId?: string;
286
300
  }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.17 | Generated: 2026-08-18T08:10:50.608Z
3
+ Version: 1.15.18 | Generated: 2026-08-19T06:40:47.505Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -1993,6 +1993,10 @@ interface CreateThreadInput {
1993
1993
  data?: Record<string, unknown>
1994
1994
  owner?: Record<string, unknown>
1995
1995
  admin?: Record<string, unknown> // admin only
1996
+ * Optional atomic first reply. Posting a comment no longer needs a separate
1997
+ * create-thread-then-reply round trip (which could orphan an empty thread on
1998
+ * partial failure). The reply is stored with a generated `id` and timestamp.
1999
+ firstReply?: ReplyInput
1996
2000
  }
1997
2001
  ```
1998
2002
 
@@ -8435,6 +8439,13 @@ Soft delete a thread DELETE /threads/:threadId
8435
8439
  admin: boolean = false) → `Promise<AppThread>`
8436
8440
  Add a reply to a thread POST /threads/:threadId/reply Atomically appends to replies array, increments replyCount, updates lastReplyAt
8437
8441
 
8442
+ **deleteReply**(collectionId: string,
8443
+ appId: string,
8444
+ threadId: string,
8445
+ replyId: string,
8446
+ admin: boolean = false) → `Promise<AppThread>`
8447
+ Delete a single reply from a thread by its reply id (moderation). DELETE /threads/:threadId/reply/:replyId Authorised for the reply's author, the proof owner, or a collection admin.
8448
+
8438
8449
  **aggregate**(collectionId: string,
8439
8450
  appId: string,
8440
8451
  request: AggregateRequest,
@@ -429,6 +429,71 @@ const productComments = await app.threads.list(collectionId, appId, {
429
429
  });
430
430
  ```
431
431
 
432
+ ### Anchoring to app entities and proofs
433
+
434
+ Anchor a thread to your own entity with `parentType` + `parentId`. `parentId` is a
435
+ free-form string (SmartLinks short ids, not just UUIDs), so you can use your app's
436
+ native ids directly — no need to stash them in `body` or a tag.
437
+
438
+ ```typescript
439
+ await app.threads.create(collectionId, appId, {
440
+ parentType: 'memory',
441
+ parentId: memoryId, // e.g. "dVdthBQLAjQEnitU7aWy" — text, not a UUID
442
+ proofId, // anchor to a proof (enables grant-tier reads)
443
+ body: { text: 'Lovely photo' },
444
+ });
445
+ ```
446
+
447
+ List filters that pair with this (see `ThreadListQueryParams`):
448
+
449
+ | Param | Purpose |
450
+ |-------|---------|
451
+ | `parentType` / `parentId` | one app entity's threads |
452
+ | `parentIds` | **batch** — every thread for many entities in one call (e.g. a feed) |
453
+ | `proofId` / `productId` | threads anchored to a proof / product |
454
+
455
+ ```typescript
456
+ // One request for the 30 memories currently on screen:
457
+ const feed = await app.threads.list(collectionId, appId, {
458
+ parentType: 'memory',
459
+ parentIds: visibleMemoryIds,
460
+ sort: 'createdAt:desc',
461
+ });
462
+ ```
463
+
464
+ ### Atomic first comment (`firstReply`)
465
+
466
+ Posting the first comment no longer needs a create-thread-then-reply two-step (which
467
+ could leave an orphan empty thread if the reply failed). Pass `firstReply` to create
468
+ the thread and its first reply in one atomic call:
469
+
470
+ ```typescript
471
+ await app.threads.create(collectionId, appId, {
472
+ parentType: 'memory', parentId: memoryId, proofId,
473
+ firstReply: { text: 'Lovely photo', authorName: 'Sam' },
474
+ });
475
+ ```
476
+
477
+ ### Deleting a reply (moderation)
478
+
479
+ Replies carry a stable `id`. Remove a single one — authorised for the reply's author,
480
+ the proof owner, or a collection admin:
481
+
482
+ ```typescript
483
+ await app.threads.deleteReply(collectionId, appId, threadId, replyId);
484
+ ```
485
+
486
+ ### Grant-tier access (private, shareable comments)
487
+
488
+ A comment thread on a shared album should be `visibility: 'owner'` (private to the
489
+ proof), with access coming from a **share grant** rather than opening the data to the
490
+ world. A holder of an active `read`/`comment` grant on the proof is treated as
491
+ owner-tier **for that proof only**. Enable it with the `publicCreate.threads.grant`
492
+ policy branch and carry the token via `setGrantToken`.
493
+
494
+ See **[Proof Share Grants](proof-share-grants.md)** for the full flow (create/redeem
495
+ grants, guest commenting, revocation, and the app config).
496
+
432
497
  ---
433
498
 
434
499
  ## Records
package/docs/assets.md CHANGED
@@ -243,6 +243,87 @@ await Api.asset.bulkDelete({
243
243
 
244
244
  ---
245
245
 
246
+ ## Resumable uploads (large files, e.g. video)
247
+
248
+ `asset.upload()` is a single request — if the connection drops, the whole file
249
+ restarts. For large files on flaky connections (e.g. phone video), use a
250
+ **resumable** upload: the file is chunked directly to storage and can be paused,
251
+ resumed, and — crucially — **continued after a page reload or app restart**.
252
+
253
+ ```typescript
254
+ import { asset } from '@proveanything/smartlinks'
255
+
256
+ // 1. Open a resumable upload.
257
+ const handle = await asset.createResumableUpload({
258
+ file, // a File (input[type=file] / drag-drop)
259
+ scope: { type: 'proof', collectionId, productId, proofId },
260
+ appId: 'photo-memory',
261
+ // token: uploadToken, // for public/token uploads (see below)
262
+ })
263
+
264
+ // 2. Persist handle.id so the upload survives a reload.
265
+ localStorage.setItem('pendingUpload', handle.id)
266
+
267
+ // 3. Upload. Resumes automatically from the offset storage already holds.
268
+ const uploaded = await handle.start({
269
+ onProgress: (pct) => setProgress(pct), // 0–100
270
+ signal: abortController.signal, // optional: cancel a stalled upload
271
+ })
272
+ localStorage.removeItem('pendingUpload')
273
+ ```
274
+
275
+ ### Pause / resume, and resume after a reload
276
+
277
+ ```typescript
278
+ handle.pause() // stop after the current chunk
279
+ await handle.resume({ onProgress }) // continue
280
+
281
+ // After a reload / app kill — rehydrate from the persisted id and the same file:
282
+ const saved = localStorage.getItem('pendingUpload')
283
+ if (saved) {
284
+ const handle = await asset.resumeUpload(saved, file)
285
+ await handle.start({ onProgress }) // continues, does not restart
286
+ }
287
+ ```
288
+
289
+ ### API
290
+
291
+ ```typescript
292
+ namespace asset {
293
+ createResumableUpload(options: CreateResumableUploadOptions): Promise<ResumableUploadHandle>
294
+ resumeUpload(handleId: string, file: File): Promise<ResumableUploadHandle>
295
+ }
296
+
297
+ interface CreateResumableUploadOptions {
298
+ file: File
299
+ scope: { type: 'collection'; collectionId: string }
300
+ | { type: 'product'; collectionId: string; productId: string }
301
+ | { type: 'proof'; collectionId: string; productId: string; proofId: string }
302
+ name?: string
303
+ metadata?: Record<string, any>
304
+ appId?: string
305
+ admin?: boolean // admin route (default is the public route)
306
+ token?: string // upload token for public/unauthenticated uploads
307
+ }
308
+
309
+ interface ResumableUploadHandle {
310
+ readonly id: string // durable, persistable — pass to resumeUpload() after a reload
311
+ readonly size: number // total bytes
312
+ start(opts?: { onProgress?: (pct: number) => void; signal?: AbortSignal }): Promise<Asset>
313
+ pause(): void
314
+ resume(opts?: { onProgress?: (pct: number) => void; signal?: AbortSignal }): Promise<Asset>
315
+ }
316
+ ```
317
+
318
+ **Notes**
319
+
320
+ - `handle.id` is an opaque string that carries everything needed to resume — persist it as-is.
321
+ - On completion, `start()`/`resume()` resolves to the finalized `Asset` record.
322
+ - `pause()` causes the in-flight `start()`/`resume()` promise to reject with an `UploadPausedError`; call `resume()` to continue.
323
+ - `asset.upload()` also now accepts an `AbortSignal` (`upload({ ..., signal })`) for cancelling a stalled single-shot upload.
324
+
325
+ ---
326
+
246
327
  ## Public (token-based) uploads
247
328
 
248
329
  For anonymous or contact-initiated uploads from the portal — no admin auth required.
@@ -0,0 +1,232 @@
1
+ # Proof Share Grants
2
+
3
+ Delegated, scoped, **revocable** bearer access to a single proof — the middle tier
4
+ between "public" (everyone) and "owner" (only the signed-in owner).
5
+
6
+ A grant lets an owner hand out a link that lets specific recipients **see or do
7
+ specific things on one proof for a limited time**, without those recipients needing
8
+ a SmartLinks account or a proof claim. Typical uses: sharing a private photo album,
9
+ letting guests comment on it, or publishing a verifiable "I own this" assertion.
10
+
11
+ ---
12
+
13
+ ## Concepts
14
+
15
+ A **grant** is a row issued by the proof owner (or a collection admin) and redeemed
16
+ by a bearer holding an opaque token. Every data request that touches the proof
17
+ re-checks the grant **server-side** against the database, so revocation is immediate.
18
+
19
+ ### Scopes — what a grant authorises
20
+
21
+ | Scope | Grants the bearer… |
22
+ |-------|--------------------|
23
+ | `read` | read owner-tier data on the proof (attestations, threads, records, cases) |
24
+ | `comment` | create threads/replies on the proof (guest comments) |
25
+ | `admin` | read owner-tier data (reserved for elevated share cases; never exposes the platform admin zone) |
26
+ | `verify_owner` | redeem a shareable ownership **assertion** (not the account) |
27
+
28
+ A grant can carry several scopes, e.g. `['read', 'comment']` for a shareable,
29
+ commentable album.
30
+
31
+ ### Security & lifecycle
32
+
33
+ - The token is opaque, unguessable, and returned to the issuer **exactly once** (on `createGrant`). It is never returned by `listGrants`.
34
+ - **Revocation is immediate** — the grant is re-checked on every request, so `revokeGrant` invalidates a token across all clients at once.
35
+ - **Auto-invalidation on transfer** — every grant is voided the moment the proof's `ownerId` changes (e.g. a resale/re-claim), so a stale "I own this" link cannot keep resolving.
36
+ - A grant is scoped to **one proof**; it can never widen access to other proofs or collection-level data.
37
+
38
+ ---
39
+
40
+ ## Owner flow — create, list, revoke
41
+
42
+ ```typescript
43
+ import { proof } from '@proveanything/smartlinks'
44
+
45
+ // Create a read+comment grant that expires in 7 days.
46
+ const grant = await proof.createGrant(collectionId, productId, proofId, {
47
+ scope: ['read', 'comment'],
48
+ audience: { kind: 'public_link' },
49
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
50
+ })
51
+
52
+ // `grant.token` is available ONLY here — embed it in your share link now.
53
+ const shareUrl = `https://app.example.com/album?proofId=${proofId}&shareToken=${grant.token}`
54
+
55
+ // List active + past grants (tokens are never included).
56
+ const grants = await proof.listGrants(collectionId, productId, proofId)
57
+
58
+ // Stop sharing — takes effect on the very next request from any client.
59
+ await proof.revokeGrant(collectionId, productId, proofId, grant.grantId)
60
+ ```
61
+
62
+ `createGrant` / `listGrants` / `revokeGrant` require the caller to be the **proof
63
+ owner** (or a collection admin) — i.e. a signed-in user whose `bearerToken` is set.
64
+
65
+ ---
66
+
67
+ ## Recipient flow — redeem, then carry the token
68
+
69
+ A recipient opens the share link, redeems the token once, then sets it as the
70
+ active grant token. From then on **every** SDK request carries the token
71
+ (`X-Grant-Token`), so all proof reads/writes are evaluated against the grant.
72
+
73
+ ```typescript
74
+ import { proof, setGrantToken } from '@proveanything/smartlinks'
75
+
76
+ const shareToken = new URLSearchParams(location.search).get('shareToken')!
77
+
78
+ // Redeem once (anonymous or signed-in). Records the redemption; optionally names the guest.
79
+ await proof.redeemGrant(collectionId, productId, proofId, shareToken, {
80
+ guestName: 'Sam', // stamped on guest activity when not signed in
81
+ })
82
+
83
+ // Attach the token to every subsequent request.
84
+ setGrantToken(shareToken)
85
+
86
+ // Now grant-tier reads succeed — e.g. owner-visibility memories on the proof:
87
+ const { attestations } = await attestation.publicList(collectionId, {
88
+ subjectType: 'proof', subjectId: proofId,
89
+ })
90
+
91
+ // Clear it when leaving the shared view:
92
+ setGrantToken(undefined)
93
+ ```
94
+
95
+ > **Persisting across reloads.** `setGrantToken` holds the token in memory. To keep a
96
+ > shared session across reloads, persist `shareToken` yourself (e.g. in `localStorage`)
97
+ > and call `setGrantToken` again on load.
98
+
99
+ ---
100
+
101
+ ## Guest commenting
102
+
103
+ With a `comment` scope grant, a bearer can post comments even when they are not
104
+ signed in. The app must enable the **grant branch** of the thread create policy
105
+ (see [App config](#app-config)); comments are then created at `visibility: 'owner'`
106
+ (private to the proof) and stamped `authorType: 'guest'`.
107
+
108
+ ```typescript
109
+ import { app } from '@proveanything/smartlinks'
110
+ // setGrantToken(shareToken) has already been called.
111
+
112
+ // One atomic call — no separate create-then-reply round trip.
113
+ await app.threads.create(collectionId, 'photo-memory', {
114
+ parentType: 'memory',
115
+ parentId: memoryId, // text — SmartLinks short ids are fine (not just UUIDs)
116
+ proofId, // anchor to the proof so grant readers see it
117
+ firstReply: { text: 'Lovely photo', authorName: 'Sam' },
118
+ })
119
+ ```
120
+
121
+ Other grant holders (and the owner) see these comments because a `read` grant reveals
122
+ `owner`-visibility threads for the proof. See
123
+ [App Objects → Threads](app-objects.md#threads) for the full threads API.
124
+
125
+ ---
126
+
127
+ ## Proof of ownership — `verify_owner`
128
+
129
+ Ownership itself is **not** a grant — it is `proof.ownerId`, established via the
130
+ existing [claim flow](proof-claiming-methods.md). A `verify_owner` grant only
131
+ publishes a shareable, verifiable **assertion** derived from that ownership, without
132
+ handing over the account:
133
+
134
+ ```typescript
135
+ const grant = await proof.createGrant(collectionId, productId, proofId, {
136
+ scope: ['verify_owner'],
137
+ })
138
+
139
+ // The recipient redeems it and gets the assertion — never the account:
140
+ const result = await proof.redeemGrant(collectionId, productId, proofId, grant.token)
141
+ // { proofId, assertsOwnership: true, ownerDisplayName?, issuedAt, expiresAt }
142
+ ```
143
+
144
+ Because grants auto-invalidate on transfer, a resale cannot leave a stale
145
+ "I own this" link in circulation.
146
+
147
+ ---
148
+
149
+ ## What a grant gates
150
+
151
+ When a valid grant token is present, these public reads elevate to owner-tier for the
152
+ granted proof (and only that proof):
153
+
154
+ - **Attestations** — `attestation.publicList({ subjectType: 'proof', subjectId })`
155
+ - **Threads / Records / Cases** — `app.threads.list`, `app.records.*`, `app.cases.list`, and the single-item GETs, filtered to the granted proof
156
+ - **Thread creation / replies** — with a `comment` scope grant (see below)
157
+
158
+ The token never exposes the platform `admin` zone, and only reveals `owner`-visibility
159
+ rows for the granted `proofId`.
160
+
161
+ ---
162
+
163
+ ## App config
164
+
165
+ Grant-based commenting is opt-in per app, configured on the app's Firestore config
166
+ at `sites/{collectionId}/apps/{appId}` — a `grant` branch alongside
167
+ `anonymous` / `authenticated`:
168
+
169
+ ```jsonc
170
+ {
171
+ "publicCreate": {
172
+ "threads": {
173
+ "grant": {
174
+ "allow": true,
175
+ "requireScope": "comment",
176
+ "enforce": { "visibility": "owner", "status": "open" }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ This enables grant-scoped commenting **without** opening up anonymous creation. The
184
+ `enforce.visibility: "owner"` keeps comments private to the proof (visible to the
185
+ owner and other grant holders, not the wider public).
186
+
187
+ ---
188
+
189
+ ## API reference
190
+
191
+ ```typescript
192
+ namespace proof {
193
+ createGrant(collectionId, productId, proofId, options: CreateGrantOptions): Promise<ProofGrant>
194
+ listGrants(collectionId, productId, proofId): Promise<ProofGrant[]>
195
+ revokeGrant(collectionId, productId, proofId, grantId): Promise<void>
196
+ redeemGrant(collectionId, productId, proofId, token, options?: RedeemGrantOptions): Promise<RedeemGrantResult>
197
+ }
198
+
199
+ // Attach / clear the active grant token (sent as X-Grant-Token on every request).
200
+ function setGrantToken(token: string | undefined): void
201
+ function getGrantToken(): string | undefined
202
+
203
+ type GrantScope = 'read' | 'comment' | 'admin' | 'verify_owner'
204
+
205
+ interface CreateGrantOptions {
206
+ scope: GrantScope[] // at least one
207
+ audience?: { kind: 'public_link' } | { kind: 'named'; email?: string; userId?: string }
208
+ expiresAt?: Date | string
209
+ }
210
+
211
+ interface RedeemGrantOptions { guestName?: string }
212
+
213
+ type RedeemGrantResult =
214
+ | { scope: GrantScope[]; redeemedAt: string }
215
+ | { proofId: string; assertsOwnership: true; ownerDisplayName?: string; issuedAt?: string; expiresAt?: string }
216
+
217
+ interface ProofGrant {
218
+ grantId: string
219
+ proofId: string
220
+ productId?: string | null
221
+ scope: GrantScope[]
222
+ audience: { kind: 'public_link' | 'named'; email?: string; userId?: string }
223
+ createdBy: string
224
+ expiresAt?: string | null
225
+ revokedAt?: string | null
226
+ redeemedBy?: { userId?: string; guestName?: string; redeemedAt: string }
227
+ redeemCount: number
228
+ createdAt: string
229
+ updatedAt: string
230
+ token?: string // present ONLY on the createGrant response
231
+ }
232
+ ```
package/openapi.yaml CHANGED
@@ -13941,6 +13941,23 @@ paths:
13941
13941
  required: false
13942
13942
  schema:
13943
13943
  type: string
13944
+ - name: parentIds
13945
+ in: query
13946
+ required: false
13947
+ schema:
13948
+ type: array
13949
+ items:
13950
+ type: string
13951
+ - name: proofId
13952
+ in: query
13953
+ required: false
13954
+ schema:
13955
+ type: string
13956
+ - name: productId
13957
+ in: query
13958
+ required: false
13959
+ schema:
13960
+ type: string
13944
13961
  - name: tag
13945
13962
  in: query
13946
13963
  required: false
@@ -14182,6 +14199,52 @@ paths:
14182
14199
  application/json:
14183
14200
  schema:
14184
14201
  $ref: "#/components/schemas/ReplyInput"
14202
+ /{zone}/collection/{collectionId}/app/{appId}/threads/{threadId}/reply/{replyId}:
14203
+ delete:
14204
+ tags:
14205
+ - threads
14206
+ summary: Delete a single reply from a thread by its reply id (moderation).
14207
+ operationId: threads_deleteReply
14208
+ security: []
14209
+ parameters:
14210
+ - name: zone
14211
+ in: path
14212
+ required: true
14213
+ schema:
14214
+ type: string
14215
+ - name: collectionId
14216
+ in: path
14217
+ required: true
14218
+ schema:
14219
+ type: string
14220
+ - name: appId
14221
+ in: path
14222
+ required: true
14223
+ schema:
14224
+ type: string
14225
+ - name: threadId
14226
+ in: path
14227
+ required: true
14228
+ schema:
14229
+ type: string
14230
+ - name: replyId
14231
+ in: path
14232
+ required: true
14233
+ schema:
14234
+ type: string
14235
+ responses:
14236
+ 200:
14237
+ description: Success
14238
+ content:
14239
+ application/json:
14240
+ schema:
14241
+ $ref: "#/components/schemas/AppThread"
14242
+ 400:
14243
+ description: Bad request
14244
+ 401:
14245
+ description: Unauthorized
14246
+ 404:
14247
+ description: Not found
14185
14248
  components:
14186
14249
  securitySchemes:
14187
14250
  bearerAuth:
@@ -16967,6 +17030,8 @@ components:
16967
17030
  admin:
16968
17031
  type: object
16969
17032
  additionalProperties: true
17033
+ firstReply:
17034
+ $ref: "#/components/schemas/ReplyInput"
16970
17035
  UpdateThreadInput:
16971
17036
  type: object
16972
17037
  properties:
@@ -17012,6 +17077,14 @@ components:
17012
17077
  type: string
17013
17078
  parentId:
17014
17079
  type: string
17080
+ parentIds:
17081
+ type: array
17082
+ items:
17083
+ type: string
17084
+ proofId:
17085
+ type: string
17086
+ productId:
17087
+ type: string
17015
17088
  tag:
17016
17089
  type: string
17017
17090
  contactId:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.15.17",
3
+ "version": "1.15.18",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",