@proveanything/smartlinks 1.16.6 → 1.16.7

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 { Attestation, CreateAttestationInput, ListAttestationsParams, ListAttestationsResponse, AttestationSummaryParams, AttestationSummaryResponse, PublicAttestationSummaryResponse, AttestationLatestParams, AttestationLatestResponse, PublicAttestationLatestResponse, AttestationVerifyParams, ChainVerifyResult, AttestationTreeSummaryParams, AttestationTreeSummaryResponse, PublicAttestationTreeSummaryResponse, AttestationTreeLatestParams, AttestationTreeLatestResponse, PublicAttestationTreeLatestResponse, PublicListAttestationsResponse, OwnerAttestationInput, CreateOwnerAttestationResponse } from "../types/attestations";
1
+ import type { Attestation, CreateAttestationInput, ListAttestationsParams, ListAttestationsResponse, AttestationSummaryParams, AttestationSummaryResponse, PublicAttestationSummaryResponse, AttestationLatestParams, AttestationLatestResponse, PublicAttestationLatestResponse, AttestationVerifyParams, ChainVerifyResult, AttestationTreeSummaryParams, AttestationTreeSummaryResponse, PublicAttestationTreeSummaryResponse, AttestationTreeLatestParams, AttestationTreeLatestResponse, PublicAttestationTreeLatestResponse, PublicListAttestationsResponse, OwnerAttestationInput, CreateOwnerAttestationResponse, ModerateAttestationInput, ModerateAttestationResponse } from "../types/attestations";
2
2
  /**
3
3
  * Postgres-backed Attestations API (v2).
4
4
  *
@@ -13,9 +13,11 @@ import type { Attestation, CreateAttestationInput, ListAttestationsParams, ListA
13
13
  * admin session or bearer token. All three data zones are returned, and the
14
14
  * business can write any zone/visibility ({@link create}).
15
15
  * - **Public** endpoints (`/public/collection/:id/attestations`) are read-only,
16
- * EXCEPT {@link publicCreate}: the proof OWNER may author an attestation on
17
- * their own item (value + ownerData, visibility public|owner never the admin
18
- * zone). Owner elevation is via `Authorization: Bearer <Firebase ID token>`.
16
+ * EXCEPT {@link publicCreate} (the proof OWNER, or a `contribute`-grant holder,
17
+ * authors an attestation value + ownerData, visibility public|owner, never the
18
+ * admin zone) and {@link moderate} (owner/admin approves or rejects a contributed
19
+ * record). Owner elevation is via `Authorization: Bearer <Firebase ID token>`;
20
+ * contributor access via a grant token set with `setGrantToken`.
19
21
  *
20
22
  * @see docs/attestations.md
21
23
  */
@@ -211,22 +213,49 @@ export declare namespace attestations {
211
213
  */
212
214
  function publicList(collectionId: string, params: ListAttestationsParams): Promise<PublicListAttestationsResponse>;
213
215
  /**
214
- * Create an OWNER-authored attestation (public write) — the counterpart to the
215
- * admin {@link create}. The authenticated caller must OWN the linked proof
216
- * (identity, not a read grant). Guardrails enforced server-side: they may write
217
- * `value` + `ownerData` only (`adminData` is dropped), `visibility` is clamped
218
- * to `'public' | 'owner'`, and `authorId` is forced to the caller. The record
219
- * joins the same tamper-evident hash chain.
216
+ * Create a public attestation — the counterpart to the admin {@link create}.
217
+ * Authorised two ways, same call (the server decides from the request):
218
+ * 1. the proof OWNER (identity, via `Authorization: Bearer <Firebase ID token>`)
219
+ * adds an attestation to their own item; or
220
+ * 2. a holder of a `contribute`-scope grant adds one call
221
+ * {@link setGrantToken} with the grant token first; for a public-link
222
+ * (anonymous) grant, pass `guestName` for attribution.
223
+ *
224
+ * Guardrails (server-enforced): `value` + `ownerData` only (`adminData` dropped),
225
+ * `visibility` clamped to `'public' | 'owner'`, `authorId`/`grantId` server-stamped.
226
+ * If the contribute grant was issued with `moderate: true`, the returned record
227
+ * has `moderationStatus: 'pending'` — held to the owner until {@link moderate}.
228
+ * The record joins the same tamper-evident hash chain.
220
229
  * POST /public/collection/:collectionId/attestations
221
230
  * ```ts
231
+ * // Owner:
222
232
  * await attestations.publicCreate('coll_123', {
223
233
  * subjectType: 'proof', subjectId: 'proof_1',
224
234
  * attestationType: 'condition-report',
225
235
  * value: { grade: 'excellent' }, visibility: 'public',
226
236
  * })
237
+ * // Contributor on a shared link:
238
+ * setGrantToken(shareToken)
239
+ * await attestations.publicCreate('coll_123', {
240
+ * subjectType: 'proof', subjectId: 'proof_1',
241
+ * attestationType: 'photo', value: { url }, visibility: 'public',
242
+ * guestName: 'Sam',
243
+ * })
227
244
  * ```
228
245
  */
229
246
  function publicCreate(collectionId: string, data: OwnerAttestationInput): Promise<CreateOwnerAttestationResponse>;
247
+ /**
248
+ * Moderate a contributed attestation (proof OWNER by identity, or collection
249
+ * admin). `'approve'` releases it to its declared visibility; `'reject'` keeps
250
+ * it author + admin only. Only the `moderationStatus` changes — the hashed fact
251
+ * and its chain are untouched. Find pending items with
252
+ * {@link publicList} + `moderationStatus: 'pending'`.
253
+ * POST /public/collection/:collectionId/attestations/:attestationId/moderate
254
+ * ```ts
255
+ * await attestations.moderate('coll_123', 'att_uuid', { decision: 'approve' })
256
+ * ```
257
+ */
258
+ function moderate(collectionId: string, attestationId: string, input: ModerateAttestationInput): Promise<ModerateAttestationResponse>;
230
259
  /**
231
260
  * Time-series summary (public).
232
261
  *
@@ -26,9 +26,11 @@ function buildAttestationQuery(params) {
26
26
  * admin session or bearer token. All three data zones are returned, and the
27
27
  * business can write any zone/visibility ({@link create}).
28
28
  * - **Public** endpoints (`/public/collection/:id/attestations`) are read-only,
29
- * EXCEPT {@link publicCreate}: the proof OWNER may author an attestation on
30
- * their own item (value + ownerData, visibility public|owner never the admin
31
- * zone). Owner elevation is via `Authorization: Bearer <Firebase ID token>`.
29
+ * EXCEPT {@link publicCreate} (the proof OWNER, or a `contribute`-grant holder,
30
+ * authors an attestation value + ownerData, visibility public|owner, never the
31
+ * admin zone) and {@link moderate} (owner/admin approves or rejects a contributed
32
+ * record). Owner elevation is via `Authorization: Bearer <Firebase ID token>`;
33
+ * contributor access via a grant token set with `setGrantToken`.
32
34
  *
33
35
  * @see docs/attestations.md
34
36
  */
@@ -277,19 +279,34 @@ export var attestations;
277
279
  }
278
280
  attestations.publicList = publicList;
279
281
  /**
280
- * Create an OWNER-authored attestation (public write) — the counterpart to the
281
- * admin {@link create}. The authenticated caller must OWN the linked proof
282
- * (identity, not a read grant). Guardrails enforced server-side: they may write
283
- * `value` + `ownerData` only (`adminData` is dropped), `visibility` is clamped
284
- * to `'public' | 'owner'`, and `authorId` is forced to the caller. The record
285
- * joins the same tamper-evident hash chain.
282
+ * Create a public attestation — the counterpart to the admin {@link create}.
283
+ * Authorised two ways, same call (the server decides from the request):
284
+ * 1. the proof OWNER (identity, via `Authorization: Bearer <Firebase ID token>`)
285
+ * adds an attestation to their own item; or
286
+ * 2. a holder of a `contribute`-scope grant adds one call
287
+ * {@link setGrantToken} with the grant token first; for a public-link
288
+ * (anonymous) grant, pass `guestName` for attribution.
289
+ *
290
+ * Guardrails (server-enforced): `value` + `ownerData` only (`adminData` dropped),
291
+ * `visibility` clamped to `'public' | 'owner'`, `authorId`/`grantId` server-stamped.
292
+ * If the contribute grant was issued with `moderate: true`, the returned record
293
+ * has `moderationStatus: 'pending'` — held to the owner until {@link moderate}.
294
+ * The record joins the same tamper-evident hash chain.
286
295
  * POST /public/collection/:collectionId/attestations
287
296
  * ```ts
297
+ * // Owner:
288
298
  * await attestations.publicCreate('coll_123', {
289
299
  * subjectType: 'proof', subjectId: 'proof_1',
290
300
  * attestationType: 'condition-report',
291
301
  * value: { grade: 'excellent' }, visibility: 'public',
292
302
  * })
303
+ * // Contributor on a shared link:
304
+ * setGrantToken(shareToken)
305
+ * await attestations.publicCreate('coll_123', {
306
+ * subjectType: 'proof', subjectId: 'proof_1',
307
+ * attestationType: 'photo', value: { url }, visibility: 'public',
308
+ * guestName: 'Sam',
309
+ * })
293
310
  * ```
294
311
  */
295
312
  async function publicCreate(collectionId, data) {
@@ -297,6 +314,22 @@ export var attestations;
297
314
  return post(path, data);
298
315
  }
299
316
  attestations.publicCreate = publicCreate;
317
+ /**
318
+ * Moderate a contributed attestation (proof OWNER by identity, or collection
319
+ * admin). `'approve'` releases it to its declared visibility; `'reject'` keeps
320
+ * it author + admin only. Only the `moderationStatus` changes — the hashed fact
321
+ * and its chain are untouched. Find pending items with
322
+ * {@link publicList} + `moderationStatus: 'pending'`.
323
+ * POST /public/collection/:collectionId/attestations/:attestationId/moderate
324
+ * ```ts
325
+ * await attestations.moderate('coll_123', 'att_uuid', { decision: 'approve' })
326
+ * ```
327
+ */
328
+ async function moderate(collectionId, attestationId, input) {
329
+ const path = `/public/collection/${encodeURIComponent(collectionId)}/attestations/${encodeURIComponent(attestationId)}/moderate`;
330
+ return post(path, input);
331
+ }
332
+ attestations.moderate = moderate;
300
333
  /**
301
334
  * Time-series summary (public).
302
335
  *
package/dist/api/proof.js CHANGED
@@ -222,6 +222,9 @@ export var proof;
222
222
  body.audience = options.audience;
223
223
  if (options.expiresAt)
224
224
  body.expiresAt = options.expiresAt instanceof Date ? options.expiresAt.toISOString() : options.expiresAt;
225
+ // Only applied server-side when scope includes 'contribute'.
226
+ if (options.moderate !== undefined)
227
+ body.moderate = options.moderate;
225
228
  return post(grantBase(collectionId, productId, proofId), body);
226
229
  }
227
230
  proof.createGrant = createGrant;
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.16.6 | Generated: 2026-09-06T07:49:21.444Z
3
+ Version: 1.16.7 | Generated: 2026-09-09T11:29:21.917Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2745,6 +2745,15 @@ interface Attestation {
2745
2745
  unit?: string
2746
2746
  source?: string
2747
2747
  authorId?: string
2748
+ * When authored under a `contribute` grant (rather than by identity), the id of
2749
+ * the granting token — provenance for a contributed record. `null`/absent for
2750
+ * owner/admin/identity writes.
2751
+ grantId?: string | null
2752
+ * Moderation gate, orthogonal to {@link visibility} and excluded from the hash
2753
+ * chain. `'approved'` (default) is live; `'pending'` is held for owner review
2754
+ * (visible only to its author and owner/admin audiences); `'rejected'` was
2755
+ * declined. Contributions under a `moderate` grant start `'pending'`.
2756
+ moderationStatus?: AttestationModerationStatus
2748
2757
  metadata?: Record<string, any>
2749
2758
  contentHash: string
2750
2759
  prevHash?: string
@@ -2810,6 +2819,23 @@ interface OwnerAttestationInput {
2810
2819
  unit?: string
2811
2820
  source?: string
2812
2821
  metadata?: Record<string, any>
2822
+ * Attribution for an anonymous (public-link) contribute-grant write. Ignored
2823
+ * for owner writes and for named-grant writes (attributed to the signed-in uid).
2824
+ guestName?: string
2825
+ }
2826
+ ```
2827
+
2828
+ **ModerateAttestationInput** (interface)
2829
+ ```typescript
2830
+ interface ModerateAttestationInput {
2831
+ decision: 'approve' | 'reject'
2832
+ }
2833
+ ```
2834
+
2835
+ **ModerateAttestationResponse** (interface)
2836
+ ```typescript
2837
+ interface ModerateAttestationResponse {
2838
+ attestation: Attestation
2813
2839
  }
2814
2840
  ```
2815
2841
 
@@ -2905,6 +2931,10 @@ interface ListAttestationsParams {
2905
2931
  subjectType: AttestationSubjectType
2906
2932
  subjectId: string
2907
2933
  attestationType?: string
2934
+ * Filter by moderation state. Primarily for the owner review queue
2935
+ * (`moderationStatus: 'pending'`). ANDs with the server's audience gate, so a
2936
+ * public caller can never use it to widen access.
2937
+ moderationStatus?: AttestationModerationStatus
2908
2938
  recordedAfter?: string
2909
2939
  recordedBefore?: string
2910
2940
  limit?: number
@@ -2969,6 +2999,8 @@ interface AttestationTreeLatestParams {
2969
2999
 
2970
3000
  **AttestationVisibility** = `'public' | 'owner' | 'admin'`
2971
3001
 
3002
+ **AttestationModerationStatus** = `'approved' | 'pending' | 'rejected'`
3003
+
2972
3004
  **AttestationAudience** = `'public' | 'owner' | 'admin'`
2973
3005
 
2974
3006
  **AttestationGroupBy** = `'hour' | 'day' | 'week' | 'month'`
@@ -7564,6 +7596,9 @@ interface ProofGrant {
7564
7596
  proofId: string
7565
7597
  productId?: string | null
7566
7598
  scope: GrantScope[]
7599
+ * `contribute` grants only: when true, records/attestations added under this
7600
+ * grant land `pending` (owner-only) until the owner approves them.
7601
+ moderate?: boolean
7567
7602
  audience: GrantAudience
7568
7603
  createdBy: string
7569
7604
  expiresAt?: string | null
@@ -7582,6 +7617,11 @@ interface CreateGrantOptions {
7582
7617
  scope: GrantScope[]
7583
7618
  audience?: GrantAudience
7584
7619
  expiresAt?: Date | string
7620
+ * Only meaningful with the `contribute` scope: hold contributions made under
7621
+ * this grant for owner review (they start `pending` and are owner-only until
7622
+ * approved). Ignored for other scopes. Defaults to `false` (contributions live
7623
+ * on write).
7624
+ moderate?: boolean
7585
7625
  }
7586
7626
  ```
7587
7627
 
@@ -7679,7 +7719,7 @@ interface CancelTransferOptions {
7679
7719
 
7680
7720
  **ProofFieldDef** = `ScopedFieldDef & { scope?: ProofFieldScope }`
7681
7721
 
7682
- **GrantScope** = `'read' | 'comment' | 'admin' | 'verify_owner'`
7722
+ **GrantScope** = `'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner'`
7683
7723
 
7684
7724
  **RedeemGrantResult** = ``
7685
7725
 
@@ -9088,7 +9128,12 @@ List attestations for a subject (public). Records with `visibility='admin'` are
9088
9128
 
9089
9129
  **publicCreate**(collectionId: string,
9090
9130
  data: OwnerAttestationInput) → `Promise<CreateOwnerAttestationResponse>`
9091
- Create an OWNER-authored attestation (public write) — the counterpart to the admin {@link create}. The authenticated caller must OWN the linked proof (identity, not a read grant). Guardrails enforced server-side: they may write `value` + `ownerData` only (`adminData` is dropped), `visibility` is clamped to `'public' | 'owner'`, and `authorId` is forced to the caller. The record joins the same tamper-evident hash chain. POST /public/collection/:collectionId/attestations ```ts await attestations.publicCreate('coll_123', { subjectType: 'proof', subjectId: 'proof_1', attestationType: 'condition-report', value: { grade: 'excellent' }, visibility: 'public', }) ```
9131
+ Create a public attestation — the counterpart to the admin {@link create}. Authorised two ways, same call (the server decides from the request): 1. the proof OWNER (identity, via `Authorization: Bearer <Firebase ID token>`) adds an attestation to their own item; or 2. a holder of a `contribute`-scope grant adds one — call {@link setGrantToken} with the grant token first; for a public-link (anonymous) grant, pass `guestName` for attribution. Guardrails (server-enforced): `value` + `ownerData` only (`adminData` dropped), `visibility` clamped to `'public' | 'owner'`, `authorId`/`grantId` server-stamped. If the contribute grant was issued with `moderate: true`, the returned record has `moderationStatus: 'pending'` — held to the owner until {@link moderate}. The record joins the same tamper-evident hash chain. POST /public/collection/:collectionId/attestations ```ts // Owner: await attestations.publicCreate('coll_123', { subjectType: 'proof', subjectId: 'proof_1', attestationType: 'condition-report', value: { grade: 'excellent' }, visibility: 'public', }) // Contributor on a shared link: setGrantToken(shareToken) await attestations.publicCreate('coll_123', { subjectType: 'proof', subjectId: 'proof_1', attestationType: 'photo', value: { url }, visibility: 'public', guestName: 'Sam', }) ```
9132
+
9133
+ **moderate**(collectionId: string,
9134
+ attestationId: string,
9135
+ input: ModerateAttestationInput) → `Promise<ModerateAttestationResponse>`
9136
+ Moderate a contributed attestation (proof OWNER by identity, or collection admin). `'approve'` releases it to its declared visibility; `'reject'` keeps it author + admin only. Only the `moderationStatus` changes — the hashed fact and its chain are untouched. Find pending items with {@link publicList} + `moderationStatus: 'pending'`. POST /public/collection/:collectionId/attestations/:attestationId/moderate ```ts await attestations.moderate('coll_123', 'att_uuid', { decision: 'approve' }) ```
9092
9137
 
9093
9138
  **publicSummary**(collectionId: string,
9094
9139
  params: AttestationSummaryParams) → `Promise<PublicAttestationSummaryResponse>`
@@ -166,6 +166,25 @@ When ownership is confirmed the request is served at `audience='owner'`, which i
166
166
  - `visibility` is clamped to `'public' | 'owner'` (never `'admin'`), defaulting to `'owner'`;
167
167
  - `authorId` is forced to the caller and `metadata.authorType = 'owner'` is stamped.
168
168
 
169
+ **Contribute write (grant)** — the same endpoint also accepts a holder of a
170
+ `contribute`-scope share grant (set the token with `setGrantToken` first), so a
171
+ third party can add an attestation without owning the proof. Named grant →
172
+ attributed to the signed-in uid; public-link grant → anonymous, with `guestName`
173
+ for attribution; `grantId` is stamped either way. If the grant was issued with
174
+ `moderate: true`, the record is created with `moderationStatus: 'pending'` and is
175
+ held to the owner until approved.
176
+
177
+ **Moderation** — `moderationStatus` (`approved` | `pending` | `rejected`) is a gate
178
+ **orthogonal to `visibility`** and excluded from the hash chain. A `pending` record
179
+ is returned only to its author and to owner/admin audiences — never to the public,
180
+ whatever its target visibility — and is excluded from public analytics. The owner
181
+ (identity) or a collection admin resolves it with `attestations.moderate(collectionId,
182
+ attestationId, { decision: 'approve' | 'reject' })`; approve releases it to its
183
+ declared visibility, reject keeps it author+admin-only. The review queue is
184
+ `attestations.publicList(..., { moderationStatus: 'pending' })`. Flipping the status
185
+ leaves the hashed fact and its chain untouched. See
186
+ [Proof Share Grants → Contribute access](proof-share-grants.md#contribute-access--let-someone-add-to-a-proof).
187
+
169
188
  The record joins the same append-only, hash-chained log as business/system writes. Business writes (any zone/visibility) remain admin-only.
170
189
 
171
190
  ### Visibility vs audience
@@ -22,11 +22,12 @@ re-checks the grant **server-side** against the database, so revocation is immed
22
22
  |-------|--------------------|
23
23
  | `read` | read owner-tier data on the proof (attestations, threads, records, cases) |
24
24
  | `comment` | create threads/replies on the proof (guest comments) |
25
+ | `contribute` | add attestations / records to the proof for the life of the grant (temporary contribute access) — optionally held for owner review via `moderate` |
25
26
  | `admin` | read owner-tier data (reserved for elevated share cases; never exposes the platform admin zone) |
26
27
  | `verify_owner` | redeem a shareable ownership **assertion** (not the account) |
27
28
 
28
29
  A grant can carry several scopes, e.g. `['read', 'comment']` for a shareable,
29
- commentable album.
30
+ commentable album, or `['read', 'contribute']` to let someone add photos.
30
31
 
31
32
  ### Security & lifecycle
32
33
 
@@ -124,6 +125,65 @@ Other grant holders (and the owner) see these comments because a `read` grant re
124
125
 
125
126
  ---
126
127
 
128
+ ## Contribute access — let someone add to a proof
129
+
130
+ A `contribute` scope grant is **temporary write access**: the bearer can add
131
+ attestations (a photo, a video reference, a check-in) and app records to the proof
132
+ for the life of the grant — no account or proof claim required. It's the successor
133
+ to ad-hoc "claim windows": properly time-boxed (`expiresAt`), revocable, and voided
134
+ on ownership transfer like every other grant.
135
+
136
+ ```typescript
137
+ // Owner issues a contribute grant that expires in 48h and holds contributions
138
+ // for review before they go public.
139
+ const grant = await proof.createGrant(collectionId, productId, proofId, {
140
+ scope: ['read', 'contribute'],
141
+ moderate: true, // contributions land 'pending'
142
+ expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
143
+ })
144
+
145
+ // Contributor (on the shared link) adds a photo attestation.
146
+ setGrantToken(shareToken)
147
+ await attestation.publicCreate(collectionId, {
148
+ subjectType: 'proof', subjectId: proofId,
149
+ attestationType: 'photo',
150
+ value: { url: 'https://…/photo.jpg' },
151
+ visibility: 'public',
152
+ guestName: 'Sam', // attribution for a public-link bearer
153
+ })
154
+ ```
155
+
156
+ ### Moderation — the two-step review
157
+
158
+ When the grant is issued with `moderate: true`, each contribution is created with
159
+ `moderationStatus: 'pending'` and is **held to the owner**: it is returned only to
160
+ its own author and to owner/admin audiences — never to the public, whatever its
161
+ target `visibility`. The owner reviews and approves (or rejects) it:
162
+
163
+ ```typescript
164
+ // Owner lists what's waiting (owner-tier read = identity or an owner session):
165
+ const { attestations } = await attestation.publicList(collectionId, {
166
+ subjectType: 'proof', subjectId: proofId,
167
+ moderationStatus: 'pending',
168
+ })
169
+
170
+ // Approve → the record goes live at its declared visibility; reject → author+admin only.
171
+ await attestation.moderate(collectionId, attestations[0].id, { decision: 'approve' })
172
+ ```
173
+
174
+ The moderation gate is enforced **server-side** — a front end may badge a pending
175
+ item, but it is the server that withholds it from other viewers. Only
176
+ `moderationStatus` changes on approve/reject; the hashed fact and its chain are
177
+ untouched. Contributions under a grant issued with `moderate: false` (the default)
178
+ go live immediately.
179
+
180
+ > **Media note.** A contributed photo/video is best modelled as an attestation
181
+ > (`attestationType: 'photo'`, `value: { url }`) so it rides this moderation model.
182
+ > The legacy per-proof asset upload path is unchanged and is not grant- or
183
+ > moderation-aware.
184
+
185
+ ---
186
+
127
187
  ## Proof of ownership — `verify_owner`
128
188
 
129
189
  Ownership itself is **not** a grant — it is `proof.ownerId`, established via the
@@ -154,6 +214,7 @@ granted proof (and only that proof):
154
214
  - **Attestations** — `attestation.publicList({ subjectType: 'proof', subjectId })`
155
215
  - **Threads / Records / Cases** — `app.threads.list`, `app.records.*`, `app.cases.list`, and the single-item GETs, filtered to the granted proof
156
216
  - **Thread creation / replies** — with a `comment` scope grant (see below)
217
+ - **Attestation / record creation** — with a `contribute` scope grant (see [Contribute access](#contribute-access--let-someone-add-to-a-proof))
157
218
 
158
219
  The token never exposes the platform `admin` zone, and only reveals `owner`-visibility
159
220
  rows for the granted `proofId`.
@@ -175,14 +236,24 @@ at `sites/{collectionId}/apps/{appId}` — a `grant` branch alongside
175
236
  "requireScope": "comment",
176
237
  "enforce": { "visibility": "owner", "status": "open" }
177
238
  }
239
+ },
240
+ "records": {
241
+ "grant": {
242
+ "allow": true,
243
+ "requireScope": "contribute",
244
+ "enforce": { "visibility": "owner" } // held to the owner until promoted
245
+ }
178
246
  }
179
247
  }
180
248
  }
181
249
  ```
182
250
 
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).
251
+ This enables grant-scoped commenting and contribution **without** opening up
252
+ anonymous creation. The `enforce.visibility: "owner"` keeps the created object
253
+ private to the proof (visible to the owner and other grant holders, not the wider
254
+ public). Attestation contribution is gated the same way, by the `contribute` scope;
255
+ whether those contributions are held for review is set per-grant with `moderate`,
256
+ not in app config.
186
257
 
187
258
  ---
188
259
 
@@ -200,12 +271,13 @@ namespace proof {
200
271
  function setGrantToken(token: string | undefined): void
201
272
  function getGrantToken(): string | undefined
202
273
 
203
- type GrantScope = 'read' | 'comment' | 'admin' | 'verify_owner'
274
+ type GrantScope = 'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner'
204
275
 
205
276
  interface CreateGrantOptions {
206
277
  scope: GrantScope[] // at least one
207
278
  audience?: { kind: 'public_link' } | { kind: 'named'; email?: string; userId?: string }
208
279
  expiresAt?: Date | string
280
+ moderate?: boolean // 'contribute' only — hold for review
209
281
  }
210
282
 
211
283
  interface RedeemGrantOptions { guestName?: string }
@@ -219,6 +291,7 @@ interface ProofGrant {
219
291
  proofId: string
220
292
  productId?: string | null
221
293
  scope: GrantScope[]
294
+ moderate?: boolean // 'contribute' grants: contributions held for review when true
222
295
  audience: { kind: 'public_link' | 'named'; email?: string; userId?: string }
223
296
  createdBy: string
224
297
  expiresAt?: string | null
package/dist/openapi.yaml CHANGED
@@ -1261,6 +1261,11 @@ paths:
1261
1261
  required: false
1262
1262
  schema:
1263
1263
  type: string
1264
+ - name: moderationStatus
1265
+ in: query
1266
+ required: false
1267
+ schema:
1268
+ $ref: "#/components/schemas/AttestationModerationStatus"
1264
1269
  - name: recordedAfter
1265
1270
  in: query
1266
1271
  required: false
@@ -10237,6 +10242,11 @@ paths:
10237
10242
  required: false
10238
10243
  schema:
10239
10244
  type: string
10245
+ - name: moderationStatus
10246
+ in: query
10247
+ required: false
10248
+ schema:
10249
+ $ref: "#/components/schemas/AttestationModerationStatus"
10240
10250
  - name: recordedAfter
10241
10251
  in: query
10242
10252
  required: false
@@ -10273,7 +10283,7 @@ paths:
10273
10283
  post:
10274
10284
  tags:
10275
10285
  - attestations
10276
- summary: "Create an OWNER-authored attestation (public write) — the counterpart to the admin {@link create}."
10286
+ summary: attestations.publicCreate
10277
10287
  operationId: attestations_publicCreate
10278
10288
  security: []
10279
10289
  parameters:
@@ -10505,6 +10515,43 @@ paths:
10505
10515
  description: Unauthorized
10506
10516
  404:
10507
10517
  description: Not found
10518
+ /public/collection/{collectionId}/attestations/{attestationId}/moderate:
10519
+ post:
10520
+ tags:
10521
+ - attestations
10522
+ summary: Moderate a contributed attestation (proof OWNER by identity, or collection admin).
10523
+ operationId: attestations_moderate
10524
+ security: []
10525
+ parameters:
10526
+ - name: collectionId
10527
+ in: path
10528
+ required: true
10529
+ schema:
10530
+ type: string
10531
+ - name: attestationId
10532
+ in: path
10533
+ required: true
10534
+ schema:
10535
+ type: string
10536
+ responses:
10537
+ 200:
10538
+ description: Success
10539
+ content:
10540
+ application/json:
10541
+ schema:
10542
+ $ref: "#/components/schemas/ModerateAttestationResponse"
10543
+ 400:
10544
+ description: Bad request
10545
+ 401:
10546
+ description: Unauthorized
10547
+ 404:
10548
+ description: Not found
10549
+ requestBody:
10550
+ required: true
10551
+ content:
10552
+ application/json:
10553
+ schema:
10554
+ $ref: "#/components/schemas/ModerateAttestationInput"
10508
10555
  /public/collection/{collectionId}/comm/email/register:
10509
10556
  post:
10510
10557
  tags:
@@ -11115,6 +11162,11 @@ paths:
11115
11162
  required: false
11116
11163
  schema:
11117
11164
  type: string
11165
+ - name: moderationStatus
11166
+ in: query
11167
+ required: false
11168
+ schema:
11169
+ $ref: "#/components/schemas/AttestationModerationStatus"
11118
11170
  - name: recordedAfter
11119
11171
  in: query
11120
11172
  required: false
@@ -18720,6 +18772,10 @@ components:
18720
18772
  type: string
18721
18773
  authorId:
18722
18774
  type: string
18775
+ grantId:
18776
+ type: string
18777
+ moderationStatus:
18778
+ $ref: "#/components/schemas/AttestationModerationStatus"
18723
18779
  metadata:
18724
18780
  type: object
18725
18781
  additionalProperties: true
@@ -18842,10 +18898,29 @@ components:
18842
18898
  metadata:
18843
18899
  type: object
18844
18900
  additionalProperties: true
18901
+ guestName:
18902
+ type: string
18845
18903
  required:
18846
18904
  - subjectType
18847
18905
  - subjectId
18848
18906
  - attestationType
18907
+ ModerateAttestationInput:
18908
+ type: object
18909
+ properties:
18910
+ decision:
18911
+ type: string
18912
+ enum:
18913
+ - approve
18914
+ - reject
18915
+ required:
18916
+ - decision
18917
+ ModerateAttestationResponse:
18918
+ type: object
18919
+ properties:
18920
+ attestation:
18921
+ $ref: "#/components/schemas/Attestation"
18922
+ required:
18923
+ - attestation
18849
18924
  ListAttestationsResponse:
18850
18925
  type: object
18851
18926
  properties:
@@ -18985,6 +19060,8 @@ components:
18985
19060
  type: string
18986
19061
  attestationType:
18987
19062
  type: string
19063
+ moderationStatus:
19064
+ $ref: "#/components/schemas/AttestationModerationStatus"
18988
19065
  recordedAfter:
18989
19066
  type: string
18990
19067
  recordedBefore:
@@ -25832,6 +25909,8 @@ components:
25832
25909
  type: array
25833
25910
  items:
25834
25911
  $ref: "#/components/schemas/GrantScope"
25912
+ moderate:
25913
+ type: boolean
25835
25914
  audience:
25836
25915
  $ref: "#/components/schemas/GrantAudience"
25837
25916
  createdBy:
@@ -25872,6 +25951,8 @@ components:
25872
25951
  expiresAt:
25873
25952
  type: object
25874
25953
  additionalProperties: true
25954
+ moderate:
25955
+ type: boolean
25875
25956
  required:
25876
25957
  - scope
25877
25958
  RedeemGrantOptions:
@@ -17,6 +17,14 @@ export type AttestationSubjectType = 'container' | 'proof' | 'product' | 'tag' |
17
17
  * - `'admin'` — visible to admin callers only
18
18
  */
19
19
  export type AttestationVisibility = 'public' | 'owner' | 'admin';
20
+ /**
21
+ * Moderation state of an attestation (separate from {@link AttestationVisibility}).
22
+ * - `'approved'` — live; follows its declared visibility (the default)
23
+ * - `'pending'` — awaiting owner review; returned only to its author and to
24
+ * owner/admin audiences, never to the public, whatever its target visibility
25
+ * - `'rejected'` — owner declined; author + admin only
26
+ */
27
+ export type AttestationModerationStatus = 'approved' | 'pending' | 'rejected';
20
28
  /**
21
29
  * Resolved audience tier returned by public endpoints.
22
30
  * Tells the client which data zones are populated in the response.
@@ -59,6 +67,19 @@ export interface Attestation {
59
67
  source?: string;
60
68
  /** User ID or service account that recorded the fact */
61
69
  authorId?: string;
70
+ /**
71
+ * When authored under a `contribute` grant (rather than by identity), the id of
72
+ * the granting token — provenance for a contributed record. `null`/absent for
73
+ * owner/admin/identity writes.
74
+ */
75
+ grantId?: string | null;
76
+ /**
77
+ * Moderation gate, orthogonal to {@link visibility} and excluded from the hash
78
+ * chain. `'approved'` (default) is live; `'pending'` is held for owner review
79
+ * (visible only to its author and owner/admin audiences); `'rejected'` was
80
+ * declined. Contributions under a `moderate` grant start `'pending'`.
81
+ */
82
+ moderationStatus?: AttestationModerationStatus;
62
83
  /** Arbitrary extra metadata */
63
84
  metadata?: Record<string, any>;
64
85
  /** SHA-256 digest of this record (includes `prevHash`) */
@@ -129,10 +150,18 @@ export interface CreateAttestationInput {
129
150
  metadata?: Record<string, any>;
130
151
  }
131
152
  /**
132
- * Owner-authored attestation input (public write). The proof OWNER adds an
133
- * attestation to their own item. Restricted vs {@link CreateAttestationInput}:
134
- * no `adminData` (business-only zone), `visibility` limited to `'public' | 'owner'`,
135
- * and `authorId` is forced to the caller by the server (so it's omitted here).
153
+ * Public attestation write input, used by {@link attestations.publicCreate}.
154
+ *
155
+ * Authorised two ways, same input shape (the server decides from the request):
156
+ * 1. the proof OWNER (Firebase ID token) adds an attestation to their own item;
157
+ * 2. a holder of a `contribute`-scope grant adds one — set the grant token with
158
+ * `setGrantToken(token)` first; for a public-link (anonymous) grant, pass
159
+ * `guestName` for attribution.
160
+ *
161
+ * Restricted vs {@link CreateAttestationInput}: no `adminData` (business-only
162
+ * zone), `visibility` limited to `'public' | 'owner'`, and `authorId` / `grantId`
163
+ * are server-stamped (so they're omitted here). If the contribute grant was issued
164
+ * with `moderate: true`, the created record comes back `moderationStatus: 'pending'`.
136
165
  */
137
166
  export interface OwnerAttestationInput {
138
167
  subjectType: AttestationSubjectType;
@@ -146,6 +175,18 @@ export interface OwnerAttestationInput {
146
175
  unit?: string;
147
176
  source?: string;
148
177
  metadata?: Record<string, any>;
178
+ /**
179
+ * Attribution for an anonymous (public-link) contribute-grant write. Ignored
180
+ * for owner writes and for named-grant writes (attributed to the signed-in uid).
181
+ */
182
+ guestName?: string;
183
+ }
184
+ /** Owner/admin decision on a pending contributed attestation. */
185
+ export interface ModerateAttestationInput {
186
+ decision: 'approve' | 'reject';
187
+ }
188
+ export interface ModerateAttestationResponse {
189
+ attestation: Attestation;
149
190
  }
150
191
  export interface ListAttestationsResponse {
151
192
  attestations: Attestation[];
@@ -197,6 +238,12 @@ export interface ListAttestationsParams {
197
238
  /** Required */
198
239
  subjectId: string;
199
240
  attestationType?: string;
241
+ /**
242
+ * Filter by moderation state. Primarily for the owner review queue
243
+ * (`moderationStatus: 'pending'`). ANDs with the server's audience gate, so a
244
+ * public caller can never use it to widen access.
245
+ */
246
+ moderationStatus?: AttestationModerationStatus;
200
247
  /** ISO 8601 lower bound (inclusive) */
201
248
  recordedAfter?: string;
202
249
  /** ISO 8601 upper bound (inclusive) */
@@ -164,8 +164,16 @@ export type ProofFieldDef = ScopedFieldDef & {
164
164
  export interface ProofFieldsConfig {
165
165
  fields: ProofFieldDef[];
166
166
  }
167
- /** What a grant authorises the bearer to do on the proof. */
168
- export type GrantScope = 'read' | 'comment' | 'admin' | 'verify_owner';
167
+ /**
168
+ * What a grant authorises the bearer to do on the proof.
169
+ * - `read` — see owner-tier data
170
+ * - `comment` — post app threads/comments
171
+ * - `contribute` — add records / attestations (temporary contribute access); pair
172
+ * with `moderate` on {@link CreateGrantOptions} to hold contributions for review
173
+ * - `admin` — owner-tier read across the proof
174
+ * - `verify_owner` — redeem an ownership assertion (never the account)
175
+ */
176
+ export type GrantScope = 'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner';
169
177
  /** Who may redeem a grant. */
170
178
  export interface GrantAudience {
171
179
  kind: 'public_link' | 'named';
@@ -178,6 +186,11 @@ export interface ProofGrant {
178
186
  proofId: string;
179
187
  productId?: string | null;
180
188
  scope: GrantScope[];
189
+ /**
190
+ * `contribute` grants only: when true, records/attestations added under this
191
+ * grant land `pending` (owner-only) until the owner approves them.
192
+ */
193
+ moderate?: boolean;
181
194
  audience: GrantAudience;
182
195
  createdBy: string;
183
196
  expiresAt?: string | null;
@@ -200,6 +213,13 @@ export interface CreateGrantOptions {
200
213
  audience?: GrantAudience;
201
214
  /** Optional expiry — a `Date` or ISO string. */
202
215
  expiresAt?: Date | string;
216
+ /**
217
+ * Only meaningful with the `contribute` scope: hold contributions made under
218
+ * this grant for owner review (they start `pending` and are owner-only until
219
+ * approved). Ignored for other scopes. Defaults to `false` (contributions live
220
+ * on write).
221
+ */
222
+ moderate?: boolean;
203
223
  }
204
224
  export interface RedeemGrantOptions {
205
225
  /** Display name to stamp on guest activity when the redeemer is not signed in. */
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.16.6 | Generated: 2026-09-06T07:49:21.444Z
3
+ Version: 1.16.7 | Generated: 2026-09-09T11:29:21.917Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -2745,6 +2745,15 @@ interface Attestation {
2745
2745
  unit?: string
2746
2746
  source?: string
2747
2747
  authorId?: string
2748
+ * When authored under a `contribute` grant (rather than by identity), the id of
2749
+ * the granting token — provenance for a contributed record. `null`/absent for
2750
+ * owner/admin/identity writes.
2751
+ grantId?: string | null
2752
+ * Moderation gate, orthogonal to {@link visibility} and excluded from the hash
2753
+ * chain. `'approved'` (default) is live; `'pending'` is held for owner review
2754
+ * (visible only to its author and owner/admin audiences); `'rejected'` was
2755
+ * declined. Contributions under a `moderate` grant start `'pending'`.
2756
+ moderationStatus?: AttestationModerationStatus
2748
2757
  metadata?: Record<string, any>
2749
2758
  contentHash: string
2750
2759
  prevHash?: string
@@ -2810,6 +2819,23 @@ interface OwnerAttestationInput {
2810
2819
  unit?: string
2811
2820
  source?: string
2812
2821
  metadata?: Record<string, any>
2822
+ * Attribution for an anonymous (public-link) contribute-grant write. Ignored
2823
+ * for owner writes and for named-grant writes (attributed to the signed-in uid).
2824
+ guestName?: string
2825
+ }
2826
+ ```
2827
+
2828
+ **ModerateAttestationInput** (interface)
2829
+ ```typescript
2830
+ interface ModerateAttestationInput {
2831
+ decision: 'approve' | 'reject'
2832
+ }
2833
+ ```
2834
+
2835
+ **ModerateAttestationResponse** (interface)
2836
+ ```typescript
2837
+ interface ModerateAttestationResponse {
2838
+ attestation: Attestation
2813
2839
  }
2814
2840
  ```
2815
2841
 
@@ -2905,6 +2931,10 @@ interface ListAttestationsParams {
2905
2931
  subjectType: AttestationSubjectType
2906
2932
  subjectId: string
2907
2933
  attestationType?: string
2934
+ * Filter by moderation state. Primarily for the owner review queue
2935
+ * (`moderationStatus: 'pending'`). ANDs with the server's audience gate, so a
2936
+ * public caller can never use it to widen access.
2937
+ moderationStatus?: AttestationModerationStatus
2908
2938
  recordedAfter?: string
2909
2939
  recordedBefore?: string
2910
2940
  limit?: number
@@ -2969,6 +2999,8 @@ interface AttestationTreeLatestParams {
2969
2999
 
2970
3000
  **AttestationVisibility** = `'public' | 'owner' | 'admin'`
2971
3001
 
3002
+ **AttestationModerationStatus** = `'approved' | 'pending' | 'rejected'`
3003
+
2972
3004
  **AttestationAudience** = `'public' | 'owner' | 'admin'`
2973
3005
 
2974
3006
  **AttestationGroupBy** = `'hour' | 'day' | 'week' | 'month'`
@@ -7564,6 +7596,9 @@ interface ProofGrant {
7564
7596
  proofId: string
7565
7597
  productId?: string | null
7566
7598
  scope: GrantScope[]
7599
+ * `contribute` grants only: when true, records/attestations added under this
7600
+ * grant land `pending` (owner-only) until the owner approves them.
7601
+ moderate?: boolean
7567
7602
  audience: GrantAudience
7568
7603
  createdBy: string
7569
7604
  expiresAt?: string | null
@@ -7582,6 +7617,11 @@ interface CreateGrantOptions {
7582
7617
  scope: GrantScope[]
7583
7618
  audience?: GrantAudience
7584
7619
  expiresAt?: Date | string
7620
+ * Only meaningful with the `contribute` scope: hold contributions made under
7621
+ * this grant for owner review (they start `pending` and are owner-only until
7622
+ * approved). Ignored for other scopes. Defaults to `false` (contributions live
7623
+ * on write).
7624
+ moderate?: boolean
7585
7625
  }
7586
7626
  ```
7587
7627
 
@@ -7679,7 +7719,7 @@ interface CancelTransferOptions {
7679
7719
 
7680
7720
  **ProofFieldDef** = `ScopedFieldDef & { scope?: ProofFieldScope }`
7681
7721
 
7682
- **GrantScope** = `'read' | 'comment' | 'admin' | 'verify_owner'`
7722
+ **GrantScope** = `'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner'`
7683
7723
 
7684
7724
  **RedeemGrantResult** = ``
7685
7725
 
@@ -9088,7 +9128,12 @@ List attestations for a subject (public). Records with `visibility='admin'` are
9088
9128
 
9089
9129
  **publicCreate**(collectionId: string,
9090
9130
  data: OwnerAttestationInput) → `Promise<CreateOwnerAttestationResponse>`
9091
- Create an OWNER-authored attestation (public write) — the counterpart to the admin {@link create}. The authenticated caller must OWN the linked proof (identity, not a read grant). Guardrails enforced server-side: they may write `value` + `ownerData` only (`adminData` is dropped), `visibility` is clamped to `'public' | 'owner'`, and `authorId` is forced to the caller. The record joins the same tamper-evident hash chain. POST /public/collection/:collectionId/attestations ```ts await attestations.publicCreate('coll_123', { subjectType: 'proof', subjectId: 'proof_1', attestationType: 'condition-report', value: { grade: 'excellent' }, visibility: 'public', }) ```
9131
+ Create a public attestation — the counterpart to the admin {@link create}. Authorised two ways, same call (the server decides from the request): 1. the proof OWNER (identity, via `Authorization: Bearer <Firebase ID token>`) adds an attestation to their own item; or 2. a holder of a `contribute`-scope grant adds one — call {@link setGrantToken} with the grant token first; for a public-link (anonymous) grant, pass `guestName` for attribution. Guardrails (server-enforced): `value` + `ownerData` only (`adminData` dropped), `visibility` clamped to `'public' | 'owner'`, `authorId`/`grantId` server-stamped. If the contribute grant was issued with `moderate: true`, the returned record has `moderationStatus: 'pending'` — held to the owner until {@link moderate}. The record joins the same tamper-evident hash chain. POST /public/collection/:collectionId/attestations ```ts // Owner: await attestations.publicCreate('coll_123', { subjectType: 'proof', subjectId: 'proof_1', attestationType: 'condition-report', value: { grade: 'excellent' }, visibility: 'public', }) // Contributor on a shared link: setGrantToken(shareToken) await attestations.publicCreate('coll_123', { subjectType: 'proof', subjectId: 'proof_1', attestationType: 'photo', value: { url }, visibility: 'public', guestName: 'Sam', }) ```
9132
+
9133
+ **moderate**(collectionId: string,
9134
+ attestationId: string,
9135
+ input: ModerateAttestationInput) → `Promise<ModerateAttestationResponse>`
9136
+ Moderate a contributed attestation (proof OWNER by identity, or collection admin). `'approve'` releases it to its declared visibility; `'reject'` keeps it author + admin only. Only the `moderationStatus` changes — the hashed fact and its chain are untouched. Find pending items with {@link publicList} + `moderationStatus: 'pending'`. POST /public/collection/:collectionId/attestations/:attestationId/moderate ```ts await attestations.moderate('coll_123', 'att_uuid', { decision: 'approve' }) ```
9092
9137
 
9093
9138
  **publicSummary**(collectionId: string,
9094
9139
  params: AttestationSummaryParams) → `Promise<PublicAttestationSummaryResponse>`
@@ -166,6 +166,25 @@ When ownership is confirmed the request is served at `audience='owner'`, which i
166
166
  - `visibility` is clamped to `'public' | 'owner'` (never `'admin'`), defaulting to `'owner'`;
167
167
  - `authorId` is forced to the caller and `metadata.authorType = 'owner'` is stamped.
168
168
 
169
+ **Contribute write (grant)** — the same endpoint also accepts a holder of a
170
+ `contribute`-scope share grant (set the token with `setGrantToken` first), so a
171
+ third party can add an attestation without owning the proof. Named grant →
172
+ attributed to the signed-in uid; public-link grant → anonymous, with `guestName`
173
+ for attribution; `grantId` is stamped either way. If the grant was issued with
174
+ `moderate: true`, the record is created with `moderationStatus: 'pending'` and is
175
+ held to the owner until approved.
176
+
177
+ **Moderation** — `moderationStatus` (`approved` | `pending` | `rejected`) is a gate
178
+ **orthogonal to `visibility`** and excluded from the hash chain. A `pending` record
179
+ is returned only to its author and to owner/admin audiences — never to the public,
180
+ whatever its target visibility — and is excluded from public analytics. The owner
181
+ (identity) or a collection admin resolves it with `attestations.moderate(collectionId,
182
+ attestationId, { decision: 'approve' | 'reject' })`; approve releases it to its
183
+ declared visibility, reject keeps it author+admin-only. The review queue is
184
+ `attestations.publicList(..., { moderationStatus: 'pending' })`. Flipping the status
185
+ leaves the hashed fact and its chain untouched. See
186
+ [Proof Share Grants → Contribute access](proof-share-grants.md#contribute-access--let-someone-add-to-a-proof).
187
+
169
188
  The record joins the same append-only, hash-chained log as business/system writes. Business writes (any zone/visibility) remain admin-only.
170
189
 
171
190
  ### Visibility vs audience
@@ -22,11 +22,12 @@ re-checks the grant **server-side** against the database, so revocation is immed
22
22
  |-------|--------------------|
23
23
  | `read` | read owner-tier data on the proof (attestations, threads, records, cases) |
24
24
  | `comment` | create threads/replies on the proof (guest comments) |
25
+ | `contribute` | add attestations / records to the proof for the life of the grant (temporary contribute access) — optionally held for owner review via `moderate` |
25
26
  | `admin` | read owner-tier data (reserved for elevated share cases; never exposes the platform admin zone) |
26
27
  | `verify_owner` | redeem a shareable ownership **assertion** (not the account) |
27
28
 
28
29
  A grant can carry several scopes, e.g. `['read', 'comment']` for a shareable,
29
- commentable album.
30
+ commentable album, or `['read', 'contribute']` to let someone add photos.
30
31
 
31
32
  ### Security & lifecycle
32
33
 
@@ -124,6 +125,65 @@ Other grant holders (and the owner) see these comments because a `read` grant re
124
125
 
125
126
  ---
126
127
 
128
+ ## Contribute access — let someone add to a proof
129
+
130
+ A `contribute` scope grant is **temporary write access**: the bearer can add
131
+ attestations (a photo, a video reference, a check-in) and app records to the proof
132
+ for the life of the grant — no account or proof claim required. It's the successor
133
+ to ad-hoc "claim windows": properly time-boxed (`expiresAt`), revocable, and voided
134
+ on ownership transfer like every other grant.
135
+
136
+ ```typescript
137
+ // Owner issues a contribute grant that expires in 48h and holds contributions
138
+ // for review before they go public.
139
+ const grant = await proof.createGrant(collectionId, productId, proofId, {
140
+ scope: ['read', 'contribute'],
141
+ moderate: true, // contributions land 'pending'
142
+ expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
143
+ })
144
+
145
+ // Contributor (on the shared link) adds a photo attestation.
146
+ setGrantToken(shareToken)
147
+ await attestation.publicCreate(collectionId, {
148
+ subjectType: 'proof', subjectId: proofId,
149
+ attestationType: 'photo',
150
+ value: { url: 'https://…/photo.jpg' },
151
+ visibility: 'public',
152
+ guestName: 'Sam', // attribution for a public-link bearer
153
+ })
154
+ ```
155
+
156
+ ### Moderation — the two-step review
157
+
158
+ When the grant is issued with `moderate: true`, each contribution is created with
159
+ `moderationStatus: 'pending'` and is **held to the owner**: it is returned only to
160
+ its own author and to owner/admin audiences — never to the public, whatever its
161
+ target `visibility`. The owner reviews and approves (or rejects) it:
162
+
163
+ ```typescript
164
+ // Owner lists what's waiting (owner-tier read = identity or an owner session):
165
+ const { attestations } = await attestation.publicList(collectionId, {
166
+ subjectType: 'proof', subjectId: proofId,
167
+ moderationStatus: 'pending',
168
+ })
169
+
170
+ // Approve → the record goes live at its declared visibility; reject → author+admin only.
171
+ await attestation.moderate(collectionId, attestations[0].id, { decision: 'approve' })
172
+ ```
173
+
174
+ The moderation gate is enforced **server-side** — a front end may badge a pending
175
+ item, but it is the server that withholds it from other viewers. Only
176
+ `moderationStatus` changes on approve/reject; the hashed fact and its chain are
177
+ untouched. Contributions under a grant issued with `moderate: false` (the default)
178
+ go live immediately.
179
+
180
+ > **Media note.** A contributed photo/video is best modelled as an attestation
181
+ > (`attestationType: 'photo'`, `value: { url }`) so it rides this moderation model.
182
+ > The legacy per-proof asset upload path is unchanged and is not grant- or
183
+ > moderation-aware.
184
+
185
+ ---
186
+
127
187
  ## Proof of ownership — `verify_owner`
128
188
 
129
189
  Ownership itself is **not** a grant — it is `proof.ownerId`, established via the
@@ -154,6 +214,7 @@ granted proof (and only that proof):
154
214
  - **Attestations** — `attestation.publicList({ subjectType: 'proof', subjectId })`
155
215
  - **Threads / Records / Cases** — `app.threads.list`, `app.records.*`, `app.cases.list`, and the single-item GETs, filtered to the granted proof
156
216
  - **Thread creation / replies** — with a `comment` scope grant (see below)
217
+ - **Attestation / record creation** — with a `contribute` scope grant (see [Contribute access](#contribute-access--let-someone-add-to-a-proof))
157
218
 
158
219
  The token never exposes the platform `admin` zone, and only reveals `owner`-visibility
159
220
  rows for the granted `proofId`.
@@ -175,14 +236,24 @@ at `sites/{collectionId}/apps/{appId}` — a `grant` branch alongside
175
236
  "requireScope": "comment",
176
237
  "enforce": { "visibility": "owner", "status": "open" }
177
238
  }
239
+ },
240
+ "records": {
241
+ "grant": {
242
+ "allow": true,
243
+ "requireScope": "contribute",
244
+ "enforce": { "visibility": "owner" } // held to the owner until promoted
245
+ }
178
246
  }
179
247
  }
180
248
  }
181
249
  ```
182
250
 
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).
251
+ This enables grant-scoped commenting and contribution **without** opening up
252
+ anonymous creation. The `enforce.visibility: "owner"` keeps the created object
253
+ private to the proof (visible to the owner and other grant holders, not the wider
254
+ public). Attestation contribution is gated the same way, by the `contribute` scope;
255
+ whether those contributions are held for review is set per-grant with `moderate`,
256
+ not in app config.
186
257
 
187
258
  ---
188
259
 
@@ -200,12 +271,13 @@ namespace proof {
200
271
  function setGrantToken(token: string | undefined): void
201
272
  function getGrantToken(): string | undefined
202
273
 
203
- type GrantScope = 'read' | 'comment' | 'admin' | 'verify_owner'
274
+ type GrantScope = 'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner'
204
275
 
205
276
  interface CreateGrantOptions {
206
277
  scope: GrantScope[] // at least one
207
278
  audience?: { kind: 'public_link' } | { kind: 'named'; email?: string; userId?: string }
208
279
  expiresAt?: Date | string
280
+ moderate?: boolean // 'contribute' only — hold for review
209
281
  }
210
282
 
211
283
  interface RedeemGrantOptions { guestName?: string }
@@ -219,6 +291,7 @@ interface ProofGrant {
219
291
  proofId: string
220
292
  productId?: string | null
221
293
  scope: GrantScope[]
294
+ moderate?: boolean // 'contribute' grants: contributions held for review when true
222
295
  audience: { kind: 'public_link' | 'named'; email?: string; userId?: string }
223
296
  createdBy: string
224
297
  expiresAt?: string | null
package/openapi.yaml CHANGED
@@ -1261,6 +1261,11 @@ paths:
1261
1261
  required: false
1262
1262
  schema:
1263
1263
  type: string
1264
+ - name: moderationStatus
1265
+ in: query
1266
+ required: false
1267
+ schema:
1268
+ $ref: "#/components/schemas/AttestationModerationStatus"
1264
1269
  - name: recordedAfter
1265
1270
  in: query
1266
1271
  required: false
@@ -10237,6 +10242,11 @@ paths:
10237
10242
  required: false
10238
10243
  schema:
10239
10244
  type: string
10245
+ - name: moderationStatus
10246
+ in: query
10247
+ required: false
10248
+ schema:
10249
+ $ref: "#/components/schemas/AttestationModerationStatus"
10240
10250
  - name: recordedAfter
10241
10251
  in: query
10242
10252
  required: false
@@ -10273,7 +10283,7 @@ paths:
10273
10283
  post:
10274
10284
  tags:
10275
10285
  - attestations
10276
- summary: "Create an OWNER-authored attestation (public write) — the counterpart to the admin {@link create}."
10286
+ summary: attestations.publicCreate
10277
10287
  operationId: attestations_publicCreate
10278
10288
  security: []
10279
10289
  parameters:
@@ -10505,6 +10515,43 @@ paths:
10505
10515
  description: Unauthorized
10506
10516
  404:
10507
10517
  description: Not found
10518
+ /public/collection/{collectionId}/attestations/{attestationId}/moderate:
10519
+ post:
10520
+ tags:
10521
+ - attestations
10522
+ summary: Moderate a contributed attestation (proof OWNER by identity, or collection admin).
10523
+ operationId: attestations_moderate
10524
+ security: []
10525
+ parameters:
10526
+ - name: collectionId
10527
+ in: path
10528
+ required: true
10529
+ schema:
10530
+ type: string
10531
+ - name: attestationId
10532
+ in: path
10533
+ required: true
10534
+ schema:
10535
+ type: string
10536
+ responses:
10537
+ 200:
10538
+ description: Success
10539
+ content:
10540
+ application/json:
10541
+ schema:
10542
+ $ref: "#/components/schemas/ModerateAttestationResponse"
10543
+ 400:
10544
+ description: Bad request
10545
+ 401:
10546
+ description: Unauthorized
10547
+ 404:
10548
+ description: Not found
10549
+ requestBody:
10550
+ required: true
10551
+ content:
10552
+ application/json:
10553
+ schema:
10554
+ $ref: "#/components/schemas/ModerateAttestationInput"
10508
10555
  /public/collection/{collectionId}/comm/email/register:
10509
10556
  post:
10510
10557
  tags:
@@ -11115,6 +11162,11 @@ paths:
11115
11162
  required: false
11116
11163
  schema:
11117
11164
  type: string
11165
+ - name: moderationStatus
11166
+ in: query
11167
+ required: false
11168
+ schema:
11169
+ $ref: "#/components/schemas/AttestationModerationStatus"
11118
11170
  - name: recordedAfter
11119
11171
  in: query
11120
11172
  required: false
@@ -18720,6 +18772,10 @@ components:
18720
18772
  type: string
18721
18773
  authorId:
18722
18774
  type: string
18775
+ grantId:
18776
+ type: string
18777
+ moderationStatus:
18778
+ $ref: "#/components/schemas/AttestationModerationStatus"
18723
18779
  metadata:
18724
18780
  type: object
18725
18781
  additionalProperties: true
@@ -18842,10 +18898,29 @@ components:
18842
18898
  metadata:
18843
18899
  type: object
18844
18900
  additionalProperties: true
18901
+ guestName:
18902
+ type: string
18845
18903
  required:
18846
18904
  - subjectType
18847
18905
  - subjectId
18848
18906
  - attestationType
18907
+ ModerateAttestationInput:
18908
+ type: object
18909
+ properties:
18910
+ decision:
18911
+ type: string
18912
+ enum:
18913
+ - approve
18914
+ - reject
18915
+ required:
18916
+ - decision
18917
+ ModerateAttestationResponse:
18918
+ type: object
18919
+ properties:
18920
+ attestation:
18921
+ $ref: "#/components/schemas/Attestation"
18922
+ required:
18923
+ - attestation
18849
18924
  ListAttestationsResponse:
18850
18925
  type: object
18851
18926
  properties:
@@ -18985,6 +19060,8 @@ components:
18985
19060
  type: string
18986
19061
  attestationType:
18987
19062
  type: string
19063
+ moderationStatus:
19064
+ $ref: "#/components/schemas/AttestationModerationStatus"
18988
19065
  recordedAfter:
18989
19066
  type: string
18990
19067
  recordedBefore:
@@ -25832,6 +25909,8 @@ components:
25832
25909
  type: array
25833
25910
  items:
25834
25911
  $ref: "#/components/schemas/GrantScope"
25912
+ moderate:
25913
+ type: boolean
25835
25914
  audience:
25836
25915
  $ref: "#/components/schemas/GrantAudience"
25837
25916
  createdBy:
@@ -25872,6 +25951,8 @@ components:
25872
25951
  expiresAt:
25873
25952
  type: object
25874
25953
  additionalProperties: true
25954
+ moderate:
25955
+ type: boolean
25875
25956
  required:
25876
25957
  - scope
25877
25958
  RedeemGrantOptions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.16.6",
3
+ "version": "1.16.7",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",