@proveanything/smartlinks 1.16.5 → 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 } 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
  *
@@ -10,9 +10,14 @@ import type { Attestation, CreateAttestationInput, ListAttestationsParams, ListA
10
10
  *
11
11
  * ### Admin vs Public
12
12
  * - **Admin** endpoints (`/admin/collection/:id/attestations`) require a valid
13
- * admin session or bearer token. All three data zones are returned.
14
- * - **Public** endpoints (`/public/collection/:id/attestations`) are read-only.
15
- * Owner elevation is available via `Authorization: Bearer <Firebase ID token>`.
13
+ * admin session or bearer token. All three data zones are returned, and the
14
+ * business can write any zone/visibility ({@link create}).
15
+ * - **Public** endpoints (`/public/collection/:id/attestations`) are read-only,
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`.
16
21
  *
17
22
  * @see docs/attestations.md
18
23
  */
@@ -207,6 +212,50 @@ export declare namespace attestations {
207
212
  * ```
208
213
  */
209
214
  function publicList(collectionId: string, params: ListAttestationsParams): Promise<PublicListAttestationsResponse>;
215
+ /**
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.
229
+ * POST /public/collection/:collectionId/attestations
230
+ * ```ts
231
+ * // Owner:
232
+ * await attestations.publicCreate('coll_123', {
233
+ * subjectType: 'proof', subjectId: 'proof_1',
234
+ * attestationType: 'condition-report',
235
+ * value: { grade: 'excellent' }, visibility: 'public',
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
+ * })
244
+ * ```
245
+ */
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>;
210
259
  /**
211
260
  * Time-series summary (public).
212
261
  *
@@ -23,9 +23,14 @@ function buildAttestationQuery(params) {
23
23
  *
24
24
  * ### Admin vs Public
25
25
  * - **Admin** endpoints (`/admin/collection/:id/attestations`) require a valid
26
- * admin session or bearer token. All three data zones are returned.
27
- * - **Public** endpoints (`/public/collection/:id/attestations`) are read-only.
28
- * Owner elevation is available via `Authorization: Bearer <Firebase ID token>`.
26
+ * admin session or bearer token. All three data zones are returned, and the
27
+ * business can write any zone/visibility ({@link create}).
28
+ * - **Public** endpoints (`/public/collection/:id/attestations`) are read-only,
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`.
29
34
  *
30
35
  * @see docs/attestations.md
31
36
  */
@@ -273,6 +278,58 @@ export var attestations;
273
278
  return request(path);
274
279
  }
275
280
  attestations.publicList = publicList;
281
+ /**
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.
295
+ * POST /public/collection/:collectionId/attestations
296
+ * ```ts
297
+ * // Owner:
298
+ * await attestations.publicCreate('coll_123', {
299
+ * subjectType: 'proof', subjectId: 'proof_1',
300
+ * attestationType: 'condition-report',
301
+ * value: { grade: 'excellent' }, visibility: 'public',
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
+ * })
310
+ * ```
311
+ */
312
+ async function publicCreate(collectionId, data) {
313
+ const path = `/public/collection/${encodeURIComponent(collectionId)}/attestations`;
314
+ return post(path, data);
315
+ }
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;
276
333
  /**
277
334
  * Time-series summary (public).
278
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.5 | Generated: 2026-09-05T15:46:49.017Z
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
@@ -2797,6 +2806,39 @@ interface CreateAttestationInput {
2797
2806
  }
2798
2807
  ```
2799
2808
 
2809
+ **OwnerAttestationInput** (interface)
2810
+ ```typescript
2811
+ interface OwnerAttestationInput {
2812
+ subjectType: AttestationSubjectType
2813
+ subjectId: string
2814
+ attestationType: string
2815
+ recordedAt?: string
2816
+ visibility?: 'public' | 'owner'
2817
+ value?: Record<string, any>
2818
+ ownerData?: Record<string, any>
2819
+ unit?: string
2820
+ source?: string
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
2839
+ }
2840
+ ```
2841
+
2800
2842
  **ListAttestationsResponse** (interface)
2801
2843
  ```typescript
2802
2844
  interface ListAttestationsResponse {
@@ -2804,6 +2846,13 @@ interface ListAttestationsResponse {
2804
2846
  }
2805
2847
  ```
2806
2848
 
2849
+ **CreateOwnerAttestationResponse** (interface)
2850
+ ```typescript
2851
+ interface CreateOwnerAttestationResponse {
2852
+ attestation: Attestation
2853
+ }
2854
+ ```
2855
+
2807
2856
  **PublicListAttestationsResponse** (interface)
2808
2857
  ```typescript
2809
2858
  interface PublicListAttestationsResponse {
@@ -2882,6 +2931,10 @@ interface ListAttestationsParams {
2882
2931
  subjectType: AttestationSubjectType
2883
2932
  subjectId: string
2884
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
2885
2938
  recordedAfter?: string
2886
2939
  recordedBefore?: string
2887
2940
  limit?: number
@@ -2946,6 +2999,8 @@ interface AttestationTreeLatestParams {
2946
2999
 
2947
3000
  **AttestationVisibility** = `'public' | 'owner' | 'admin'`
2948
3001
 
3002
+ **AttestationModerationStatus** = `'approved' | 'pending' | 'rejected'`
3003
+
2949
3004
  **AttestationAudience** = `'public' | 'owner' | 'admin'`
2950
3005
 
2951
3006
  **AttestationGroupBy** = `'hour' | 'day' | 'week' | 'month'`
@@ -7541,6 +7596,9 @@ interface ProofGrant {
7541
7596
  proofId: string
7542
7597
  productId?: string | null
7543
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
7544
7602
  audience: GrantAudience
7545
7603
  createdBy: string
7546
7604
  expiresAt?: string | null
@@ -7559,6 +7617,11 @@ interface CreateGrantOptions {
7559
7617
  scope: GrantScope[]
7560
7618
  audience?: GrantAudience
7561
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
7562
7625
  }
7563
7626
  ```
7564
7627
 
@@ -7656,7 +7719,7 @@ interface CancelTransferOptions {
7656
7719
 
7657
7720
  **ProofFieldDef** = `ScopedFieldDef & { scope?: ProofFieldScope }`
7658
7721
 
7659
- **GrantScope** = `'read' | 'comment' | 'admin' | 'verify_owner'`
7722
+ **GrantScope** = `'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner'`
7660
7723
 
7661
7724
  **RedeemGrantResult** = ``
7662
7725
 
@@ -9063,6 +9126,15 @@ Tree latest snapshot — most-recent record per type across a container subtree
9063
9126
  params: ListAttestationsParams) → `Promise<PublicListAttestationsResponse>`
9064
9127
  List attestations for a subject (public). Records with `visibility='admin'` are always excluded. Records with `visibility='owner'` are included only when the caller provides a valid Firebase ID token that resolves to the subject owner. The `audience` field in the response indicates the tier that was served. ```typescript const { attestations: records, audience } = await attestations.publicList('coll_123', { subjectType: 'proof', subjectId: 'proof-uuid', }) ```
9065
9128
 
9129
+ **publicCreate**(collectionId: string,
9130
+ data: OwnerAttestationInput) → `Promise<CreateOwnerAttestationResponse>`
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' }) ```
9137
+
9066
9138
  **publicSummary**(collectionId: string,
9067
9139
  params: AttestationSummaryParams) → `Promise<PublicAttestationSummaryResponse>`
9068
9140
  Time-series summary (public). Always served at `audience='public'`. Same parameters as the admin version. `attestationType` are required
@@ -159,6 +159,34 @@ The middleware resolves the UID and checks ownership:
159
159
 
160
160
  When ownership is confirmed the request is served at `audience='owner'`, which includes `ownerData`.
161
161
 
162
+ **Owner write** — the proof owner may also *author* an attestation on their own item via `POST /api/v1/public/collection/:collectionId/attestations` (`attestations.publicCreate`). It is the public counterpart to the admin write, with tight guardrails:
163
+
164
+ - the caller must be authenticated and **own the linked proof** (identity — a read grant is not enough); a subject with no linked proof is rejected;
165
+ - they may write `value` and `ownerData` only — **`adminData` is dropped** (the business zone);
166
+ - `visibility` is clamped to `'public' | 'owner'` (never `'admin'`), defaulting to `'owner'`;
167
+ - `authorId` is forced to the caller and `metadata.authorType = 'owner'` is stamped.
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
+
188
+ The record joins the same append-only, hash-chained log as business/system writes. Business writes (any zone/visibility) remain admin-only.
189
+
162
190
  ### Visibility vs audience
163
191
 
164
192
  `visibility` is a property of an **individual record** set at write time. `audience` describes the **caller's tier** resolved at read time. The server applies:
@@ -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