@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.
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
@@ -10270,6 +10280,37 @@ paths:
10270
10280
  description: Unauthorized
10271
10281
  404:
10272
10282
  description: Not found
10283
+ post:
10284
+ tags:
10285
+ - attestations
10286
+ summary: attestations.publicCreate
10287
+ operationId: attestations_publicCreate
10288
+ security: []
10289
+ parameters:
10290
+ - name: collectionId
10291
+ in: path
10292
+ required: true
10293
+ schema:
10294
+ type: string
10295
+ responses:
10296
+ 200:
10297
+ description: Success
10298
+ content:
10299
+ application/json:
10300
+ schema:
10301
+ $ref: "#/components/schemas/CreateOwnerAttestationResponse"
10302
+ 400:
10303
+ description: Bad request
10304
+ 401:
10305
+ description: Unauthorized
10306
+ 404:
10307
+ description: Not found
10308
+ requestBody:
10309
+ required: true
10310
+ content:
10311
+ application/json:
10312
+ schema:
10313
+ $ref: "#/components/schemas/OwnerAttestationInput"
10273
10314
  /public/collection/{collectionId}/attestations/latest:
10274
10315
  get:
10275
10316
  tags:
@@ -10474,6 +10515,43 @@ paths:
10474
10515
  description: Unauthorized
10475
10516
  404:
10476
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"
10477
10555
  /public/collection/{collectionId}/comm/email/register:
10478
10556
  post:
10479
10557
  tags:
@@ -11084,6 +11162,11 @@ paths:
11084
11162
  required: false
11085
11163
  schema:
11086
11164
  type: string
11165
+ - name: moderationStatus
11166
+ in: query
11167
+ required: false
11168
+ schema:
11169
+ $ref: "#/components/schemas/AttestationModerationStatus"
11087
11170
  - name: recordedAfter
11088
11171
  in: query
11089
11172
  required: false
@@ -18689,6 +18772,10 @@ components:
18689
18772
  type: string
18690
18773
  authorId:
18691
18774
  type: string
18775
+ grantId:
18776
+ type: string
18777
+ moderationStatus:
18778
+ $ref: "#/components/schemas/AttestationModerationStatus"
18692
18779
  metadata:
18693
18780
  type: object
18694
18781
  additionalProperties: true
@@ -18782,6 +18869,58 @@ components:
18782
18869
  - subjectType
18783
18870
  - subjectId
18784
18871
  - attestationType
18872
+ OwnerAttestationInput:
18873
+ type: object
18874
+ properties:
18875
+ subjectType:
18876
+ $ref: "#/components/schemas/AttestationSubjectType"
18877
+ subjectId:
18878
+ type: string
18879
+ attestationType:
18880
+ type: string
18881
+ recordedAt:
18882
+ type: string
18883
+ visibility:
18884
+ type: string
18885
+ enum:
18886
+ - public
18887
+ - owner
18888
+ value:
18889
+ type: object
18890
+ additionalProperties: true
18891
+ ownerData:
18892
+ type: object
18893
+ additionalProperties: true
18894
+ unit:
18895
+ type: string
18896
+ source:
18897
+ type: string
18898
+ metadata:
18899
+ type: object
18900
+ additionalProperties: true
18901
+ guestName:
18902
+ type: string
18903
+ required:
18904
+ - subjectType
18905
+ - subjectId
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
18785
18924
  ListAttestationsResponse:
18786
18925
  type: object
18787
18926
  properties:
@@ -18791,6 +18930,13 @@ components:
18791
18930
  $ref: "#/components/schemas/Attestation"
18792
18931
  required:
18793
18932
  - attestations
18933
+ CreateOwnerAttestationResponse:
18934
+ type: object
18935
+ properties:
18936
+ attestation:
18937
+ $ref: "#/components/schemas/Attestation"
18938
+ required:
18939
+ - attestation
18794
18940
  PublicListAttestationsResponse:
18795
18941
  type: object
18796
18942
  properties:
@@ -18914,6 +19060,8 @@ components:
18914
19060
  type: string
18915
19061
  attestationType:
18916
19062
  type: string
19063
+ moderationStatus:
19064
+ $ref: "#/components/schemas/AttestationModerationStatus"
18917
19065
  recordedAfter:
18918
19066
  type: string
18919
19067
  recordedBefore:
@@ -25761,6 +25909,8 @@ components:
25761
25909
  type: array
25762
25910
  items:
25763
25911
  $ref: "#/components/schemas/GrantScope"
25912
+ moderate:
25913
+ type: boolean
25764
25914
  audience:
25765
25915
  $ref: "#/components/schemas/GrantAudience"
25766
25916
  createdBy:
@@ -25801,6 +25951,8 @@ components:
25801
25951
  expiresAt:
25802
25952
  type: object
25803
25953
  additionalProperties: true
25954
+ moderate:
25955
+ type: boolean
25804
25956
  required:
25805
25957
  - scope
25806
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`) */
@@ -128,9 +149,51 @@ export interface CreateAttestationInput {
128
149
  authorId?: string;
129
150
  metadata?: Record<string, any>;
130
151
  }
152
+ /**
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'`.
165
+ */
166
+ export interface OwnerAttestationInput {
167
+ subjectType: AttestationSubjectType;
168
+ subjectId: string;
169
+ attestationType: string;
170
+ recordedAt?: string;
171
+ /** `'public'` | `'owner'` only — defaults to `'owner'`. */
172
+ visibility?: 'public' | 'owner';
173
+ value?: Record<string, any>;
174
+ ownerData?: Record<string, any>;
175
+ unit?: string;
176
+ source?: string;
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;
190
+ }
131
191
  export interface ListAttestationsResponse {
132
192
  attestations: Attestation[];
133
193
  }
194
+ export interface CreateOwnerAttestationResponse {
195
+ attestation: Attestation;
196
+ }
134
197
  export interface PublicListAttestationsResponse {
135
198
  attestations: Attestation[];
136
199
  /** Resolved audience tier; governs which data zones are populated */
@@ -175,6 +238,12 @@ export interface ListAttestationsParams {
175
238
  /** Required */
176
239
  subjectId: string;
177
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;
178
247
  /** ISO 8601 lower bound (inclusive) */
179
248
  recordedAfter?: string;
180
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.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