@proveanything/smartlinks 1.16.6 → 1.17.0

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,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.17.0 | Generated: 2026-09-13T07:38:31.861Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -134,6 +134,7 @@ The Smartlinks SDK is organized into the following namespaces:
134
134
  - **containers** - Functions for containers operations
135
135
  - **facets** - Functions for facets operations
136
136
  - **http** - Functions for http operations
137
+ - **integrations** - Functions for integrations operations
137
138
  - **jobs** - Functions for jobs operations
138
139
  - **journeysAnalytics** - Functions for journeysAnalytics operations
139
140
  - **location** - Functions for location operations
@@ -142,6 +143,7 @@ The Smartlinks SDK is organized into the following namespaces:
142
143
  - **order** - Functions for order operations
143
144
  - **products** - Functions for products operations
144
145
  - **realtime** - Functions for realtime operations
146
+ - **secrets** - Functions for secrets operations
145
147
  - **tags** - Functions for tags operations
146
148
  - **template** - Functions for template operations
147
149
  - **translations** - Functions for translations operations
@@ -2745,6 +2747,15 @@ interface Attestation {
2745
2747
  unit?: string
2746
2748
  source?: string
2747
2749
  authorId?: string
2750
+ * When authored under a `contribute` grant (rather than by identity), the id of
2751
+ * the granting token — provenance for a contributed record. `null`/absent for
2752
+ * owner/admin/identity writes.
2753
+ grantId?: string | null
2754
+ * Moderation gate, orthogonal to {@link visibility} and excluded from the hash
2755
+ * chain. `'approved'` (default) is live; `'pending'` is held for owner review
2756
+ * (visible only to its author and owner/admin audiences); `'rejected'` was
2757
+ * declined. Contributions under a `moderate` grant start `'pending'`.
2758
+ moderationStatus?: AttestationModerationStatus
2748
2759
  metadata?: Record<string, any>
2749
2760
  contentHash: string
2750
2761
  prevHash?: string
@@ -2810,6 +2821,23 @@ interface OwnerAttestationInput {
2810
2821
  unit?: string
2811
2822
  source?: string
2812
2823
  metadata?: Record<string, any>
2824
+ * Attribution for an anonymous (public-link) contribute-grant write. Ignored
2825
+ * for owner writes and for named-grant writes (attributed to the signed-in uid).
2826
+ guestName?: string
2827
+ }
2828
+ ```
2829
+
2830
+ **ModerateAttestationInput** (interface)
2831
+ ```typescript
2832
+ interface ModerateAttestationInput {
2833
+ decision: 'approve' | 'reject'
2834
+ }
2835
+ ```
2836
+
2837
+ **ModerateAttestationResponse** (interface)
2838
+ ```typescript
2839
+ interface ModerateAttestationResponse {
2840
+ attestation: Attestation
2813
2841
  }
2814
2842
  ```
2815
2843
 
@@ -2905,6 +2933,10 @@ interface ListAttestationsParams {
2905
2933
  subjectType: AttestationSubjectType
2906
2934
  subjectId: string
2907
2935
  attestationType?: string
2936
+ * Filter by moderation state. Primarily for the owner review queue
2937
+ * (`moderationStatus: 'pending'`). ANDs with the server's audience gate, so a
2938
+ * public caller can never use it to widen access.
2939
+ moderationStatus?: AttestationModerationStatus
2908
2940
  recordedAfter?: string
2909
2941
  recordedBefore?: string
2910
2942
  limit?: number
@@ -2969,6 +3001,8 @@ interface AttestationTreeLatestParams {
2969
3001
 
2970
3002
  **AttestationVisibility** = `'public' | 'owner' | 'admin'`
2971
3003
 
3004
+ **AttestationModerationStatus** = `'approved' | 'pending' | 'rejected'`
3005
+
2972
3006
  **AttestationAudience** = `'public' | 'owner' | 'admin'`
2973
3007
 
2974
3008
  **AttestationGroupBy** = `'hour' | 'day' | 'week' | 'month'`
@@ -5736,6 +5770,195 @@ interface UploadDoneMessage {
5736
5770
 
5737
5771
  **UploadMessage** = ``
5738
5772
 
5773
+ ### integrations
5774
+
5775
+ **FieldMapping** (interface)
5776
+ ```typescript
5777
+ interface FieldMapping {
5778
+ targetPath: string
5779
+ sourcePath?: string
5780
+ transformType: TransformType
5781
+ transformExpression?: string
5782
+ }
5783
+ ```
5784
+
5785
+ **FlowConnectionAuth** (interface)
5786
+ ```typescript
5787
+ interface FlowConnectionAuth {
5788
+ method: FlowAuthMethod
5789
+ headerName?: string
5790
+ credentialRef?: string
5791
+ }
5792
+ ```
5793
+
5794
+ **FlowConnection** (interface)
5795
+ ```typescript
5796
+ interface FlowConnection {
5797
+ baseUrl?: string
5798
+ sendEndpoint?: string
5799
+ fetchEndpoint?: string
5800
+ defaultHeaders?: Record<string, string>
5801
+ auth?: FlowConnectionAuth
5802
+ }
5803
+ ```
5804
+
5805
+ **IntegrationFlowConfig** (interface)
5806
+ ```typescript
5807
+ interface IntegrationFlowConfig {
5808
+ connection?: FlowConnection
5809
+ fieldMappings?: FieldMapping[]
5810
+ [key: string]: any
5811
+ }
5812
+ ```
5813
+
5814
+ **IntegrationFlow** (interface)
5815
+ ```typescript
5816
+ interface IntegrationFlow {
5817
+ id: string
5818
+ orgId: string
5819
+ collectionId: string
5820
+ appId: string
5821
+ direction: FlowDirection
5822
+ name: string
5823
+ status: FlowStatus
5824
+ eventTypes: string[]
5825
+ schedule: string | null
5826
+ sourceEntity: string | null
5827
+ targetEntity: string | null
5828
+ config: IntegrationFlowConfig
5829
+ createdBy: string | null
5830
+ createdAt: string
5831
+ updatedAt: string
5832
+ deletedAt?: string | null
5833
+ lastRunAt?: string | null
5834
+ lastRunStatus?: string | null
5835
+ lastRunError?: string | null
5836
+ lastRunCount?: number | null
5837
+ lastPollAt?: string | null
5838
+ lastCursor?: string | null
5839
+ totalSynced?: number | null
5840
+ }
5841
+ ```
5842
+
5843
+ **CreateFlowInput** (interface)
5844
+ ```typescript
5845
+ interface CreateFlowInput {
5846
+ appId: string
5847
+ direction: FlowDirection
5848
+ name: string
5849
+ status?: FlowStatus
5850
+ eventTypes?: string[]
5851
+ schedule?: string | null
5852
+ sourceEntity?: string | null
5853
+ targetEntity?: string | null
5854
+ config?: IntegrationFlowConfig
5855
+ }
5856
+ ```
5857
+
5858
+ **ListFlowsQuery** (interface)
5859
+ ```typescript
5860
+ interface ListFlowsQuery {
5861
+ direction?: FlowDirection
5862
+ status?: FlowStatus
5863
+ appId?: string
5864
+ }
5865
+ ```
5866
+
5867
+ **FlowList** (interface)
5868
+ ```typescript
5869
+ interface FlowList {
5870
+ flows: IntegrationFlow[]
5871
+ }
5872
+ ```
5873
+
5874
+ **RunFlowInput** (interface)
5875
+ ```typescript
5876
+ interface RunFlowInput {
5877
+ entityId?: string
5878
+ }
5879
+ ```
5880
+
5881
+ **RunFlowSummary** (interface)
5882
+ ```typescript
5883
+ interface RunFlowSummary {
5884
+ flowId: string
5885
+ direction: FlowDirection
5886
+ records: number
5887
+ sent: number
5888
+ failed: number
5889
+ status: RunStatus
5890
+ }
5891
+ ```
5892
+
5893
+ **RunFlowEnqueued** (interface)
5894
+ ```typescript
5895
+ interface RunFlowEnqueued {
5896
+ enqueued: true
5897
+ flowId: string
5898
+ entityId: string | null
5899
+ }
5900
+ ```
5901
+
5902
+ **SecretMeta** (interface)
5903
+ ```typescript
5904
+ interface SecretMeta {
5905
+ ref: string
5906
+ name: string | null
5907
+ purpose: string
5908
+ hint: string
5909
+ keyVersion: number
5910
+ createdBy: string | null
5911
+ createdAt: string
5912
+ updatedAt: string
5913
+ rotatedAt?: string | null
5914
+ }
5915
+ ```
5916
+
5917
+ **SecretList** (interface)
5918
+ ```typescript
5919
+ interface SecretList {
5920
+ secrets: SecretMeta[]
5921
+ }
5922
+ ```
5923
+
5924
+ **SetSecretInput** (interface)
5925
+ ```typescript
5926
+ interface SetSecretInput {
5927
+ value: string
5928
+ name?: string
5929
+ purpose?: string
5930
+ }
5931
+ ```
5932
+
5933
+ **SetSecretResult** (interface)
5934
+ ```typescript
5935
+ interface SetSecretResult {
5936
+ ref: string
5937
+ hint: string
5938
+ }
5939
+ ```
5940
+
5941
+ **ListSecretsQuery** (interface)
5942
+ ```typescript
5943
+ interface ListSecretsQuery {
5944
+ purpose?: string
5945
+ }
5946
+ ```
5947
+
5948
+ **FlowDirection** = `'inbound' | 'outbound'`
5949
+
5950
+ **FlowStatus** = `'draft' | 'active' | 'paused' | 'error'`
5951
+
5952
+ **RunStatus** = `'success' | 'partial' | 'error'`
5953
+
5954
+ **TransformType** = `'direct' | 'static' | 'template' | 'jsonata' | 'ai'`
5955
+
5956
+ **FlowAuthMethod** = `'api_key' | 'bearer' | 'basic' | 'webhook' | 'oauth2' | 'none'`
5957
+
5958
+ **UpdateFlowInput** = `Partial<Omit<CreateFlowInput, 'direction'>> & {`
5959
+
5960
+ **RunFlowResult** = `RunFlowSummary | RunFlowEnqueued`
5961
+
5739
5962
  ### interaction
5740
5963
 
5741
5964
  **AdminInteractionsQueryRequest** (interface)
@@ -7564,6 +7787,9 @@ interface ProofGrant {
7564
7787
  proofId: string
7565
7788
  productId?: string | null
7566
7789
  scope: GrantScope[]
7790
+ * `contribute` grants only: when true, records/attestations added under this
7791
+ * grant land `pending` (owner-only) until the owner approves them.
7792
+ moderate?: boolean
7567
7793
  audience: GrantAudience
7568
7794
  createdBy: string
7569
7795
  expiresAt?: string | null
@@ -7582,6 +7808,11 @@ interface CreateGrantOptions {
7582
7808
  scope: GrantScope[]
7583
7809
  audience?: GrantAudience
7584
7810
  expiresAt?: Date | string
7811
+ * Only meaningful with the `contribute` scope: hold contributions made under
7812
+ * this grant for owner review (they start `pending` and are owner-only until
7813
+ * approved). Ignored for other scopes. Defaults to `false` (contributions live
7814
+ * on write).
7815
+ moderate?: boolean
7585
7816
  }
7586
7817
  ```
7587
7818
 
@@ -7679,7 +7910,7 @@ interface CancelTransferOptions {
7679
7910
 
7680
7911
  **ProofFieldDef** = `ScopedFieldDef & { scope?: ProofFieldScope }`
7681
7912
 
7682
- **GrantScope** = `'read' | 'comment' | 'admin' | 'verify_owner'`
7913
+ **GrantScope** = `'read' | 'comment' | 'contribute' | 'admin' | 'verify_owner'`
7683
7914
 
7684
7915
  **RedeemGrantResult** = ``
7685
7916
 
@@ -9088,7 +9319,12 @@ List attestations for a subject (public). Records with `visibility='admin'` are
9088
9319
 
9089
9320
  **publicCreate**(collectionId: string,
9090
9321
  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', }) ```
9322
+ 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', }) ```
9323
+
9324
+ **moderate**(collectionId: string,
9325
+ attestationId: string,
9326
+ input: ModerateAttestationInput) → `Promise<ModerateAttestationResponse>`
9327
+ 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
9328
 
9093
9329
  **publicSummary**(collectionId: string,
9094
9330
  params: AttestationSummaryParams) → `Promise<PublicAttestationSummaryResponse>`
@@ -9876,6 +10112,34 @@ Perform a PATCH request to any API endpoint.
9876
10112
  **del**(path: string) → `Promise<T>`
9877
10113
  Perform a DELETE request to any API endpoint.
9878
10114
 
10115
+ ### integrations
10116
+
10117
+ **listFlows**(collectionId: string, query: ListFlowsQuery = {}) → `Promise<FlowList>`
10118
+ List flows in a collection. GET /integrations/flows
10119
+
10120
+ **createFlow**(collectionId: string, input: CreateFlowInput) → `Promise<IntegrationFlow>`
10121
+ Create a flow. POST /integrations/flows
10122
+
10123
+ **getFlow**(collectionId: string, id: string) → `Promise<IntegrationFlow>`
10124
+ Get one flow. GET /integrations/flows/:id
10125
+
10126
+ **updateFlow**(collectionId: string, id: string, input: UpdateFlowInput) → `Promise<IntegrationFlow>`
10127
+ Update whitelisted fields. PUT /integrations/flows/:id
10128
+
10129
+ **deleteFlow**(collectionId: string, id: string) → `Promise<`
10130
+ Soft-delete a flow. DELETE /integrations/flows/:id
10131
+
10132
+ **runFlow**(collectionId: string,
10133
+ id: string,
10134
+ options: RunFlowInput & { async?: boolean } = {}) → `Promise<RunFlowResult>`
10135
+ Run a flow now. POST /integrations/flows/:id/run - inline (default): resolves and returns the run summary. - options.async: enqueue on the worker, returns { enqueued: true }. Pass options.entityId to run for a single source entity.
10136
+
10137
+ **isRunSummary**(r: RunFlowResult) → `r is RunFlowSummary`
10138
+ Type guard: the run executed inline and returned a summary.
10139
+
10140
+ **isRunEnqueued**(r: RunFlowResult) → `r is RunFlowEnqueued`
10141
+ Type guard: the run was enqueued (async).
10142
+
9879
10143
  ### interactions
9880
10144
 
9881
10145
  **query**(collectionId: string,
@@ -10568,6 +10832,23 @@ Get an Ably token for public (user-scoped) real-time communication. This endpoin
10568
10832
  **getAdminToken**() → `Promise<AblyTokenRequest>`
10569
10833
  Get an Ably token for admin real-time communication. This endpoint returns an Ably TokenRequest that can be used to initialize an Ably client with admin permissions to receive system notifications and alerts. Admin users get subscribe-only (read-only) access to the interaction:{userId} channel pattern. Requires admin authentication (Bearer token). ```ts const tokenRequest = await realtime.getAdminToken() // Use with Ably const ably = new Ably.Realtime.Promise({ authCallback: async (data, callback) => { callback(null, tokenRequest) } }) // Subscribe to admin interaction channel const userId = 'my-user-id' const channel = ably.channels.get(`interaction:${userId}`) await channel.subscribe((message) => { console.log('Admin notification:', message.data) }) ```
10570
10834
 
10835
+ ### secrets
10836
+
10837
+ **list**(collectionId: string, query: ListSecretsQuery = {}) → `Promise<SecretList>`
10838
+ List secrets as refs + masked hints + metadata (never values). GET /secrets
10839
+
10840
+ **set**(collectionId: string, input: SetSecretInput) → `Promise<SetSecretResult>`
10841
+ Create a secret. POST /secrets → { ref, hint }. Store the ref on a flow.
10842
+
10843
+ **get**(collectionId: string, ref: string) → `Promise<SecretMeta>`
10844
+ Metadata for one secret (never the value). GET /secrets/:ref
10845
+
10846
+ **rotate**(collectionId: string, ref: string, input: SetSecretInput) → `Promise<SetSecretResult>`
10847
+ Rotate/update a secret's value (and optionally name/purpose). PUT /secrets/:ref → { ref, hint }
10848
+
10849
+ **remove**(collectionId: string, ref: string) → `Promise<`
10850
+ Soft-delete a secret. DELETE /secrets/:ref
10851
+
10571
10852
  ### segments
10572
10853
 
10573
10854
  **create**(collectionId: string,
@@ -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
@@ -0,0 +1,141 @@
1
+ # Integrations
2
+
3
+ An **integration flow** is one input/output pipeline between SmartLinks and an external
4
+ system. There are two directions:
5
+
6
+ - **outbound** — read a SmartLinks entity (v1: a product), transform it with field
7
+ mappings, and send it to an external endpoint.
8
+ - **inbound** — fetch from an external system and write a SmartLinks entity. *(Executor is
9
+ outbound-first; inbound lands in a later increment.)*
10
+
11
+ Flows are triggered three ways, all converging on the same executor:
12
+
13
+ - **manual** — `integrations.runFlow(...)`, inline (returns a run summary) or enqueued.
14
+ - **event** — an outbound flow subscribed to an event type (e.g. `product.updated`) fires
15
+ automatically when that entity changes.
16
+ - **schedule** — a flow carrying a cron/interval `schedule` is run by the scan job. *(next)*
17
+
18
+ Credentials are **never** stored on the flow. The connection holds an opaque
19
+ `credentialRef` into the **sealed-secret store** (`secrets` namespace); the value is sealed
20
+ at rest and resolved server-side only, at execution.
21
+
22
+ ---
23
+
24
+ ## The flow model
25
+
26
+ ```ts
27
+ interface IntegrationFlow {
28
+ id: string
29
+ direction: 'inbound' | 'outbound'
30
+ name: string
31
+ status: 'draft' | 'active' | 'paused' | 'error' // only 'active' flows fire on events/schedule
32
+ eventTypes: string[] // e.g. ['product.updated']
33
+ schedule: string | null // cron/interval for scheduled flows
34
+ sourceEntity: string | null // outbound source, v1: 'product'
35
+ targetEntity: string | null // inbound target
36
+ config: {
37
+ connection?: {
38
+ baseUrl?: string
39
+ sendEndpoint?: string // outbound: appended to baseUrl
40
+ defaultHeaders?: Record<string, string>
41
+ auth?: { method: 'api_key' | 'bearer' | 'basic' | ..., headerName?: string, credentialRef?: string }
42
+ }
43
+ fieldMappings?: FieldMapping[]
44
+ }
45
+ // ...run watermark/telemetry: lastRunAt, lastRunStatus, lastRunCount, totalSynced
46
+ }
47
+ ```
48
+
49
+ ### Field mappings (transform)
50
+
51
+ Each mapping produces one field on the target payload:
52
+
53
+ | transformType | uses | meaning |
54
+ |---|---|---|
55
+ | `direct` | `sourcePath` | copy the value at that dot-path |
56
+ | `static` | `transformExpression` | a constant |
57
+ | `template` | `transformExpression` | a Liquid template rendered against the source record |
58
+ | `jsonata` / `ai` | — | recognised but not yet executed; reported as a per-field error |
59
+
60
+ A single field's failure is collected and the rest continue (partial success) — it never
61
+ aborts the whole record.
62
+
63
+ ---
64
+
65
+ ## Secrets (write-only)
66
+
67
+ The secret store is **write-only from the client**: you can set, rotate, list (refs +
68
+ masked hints + metadata) and delete — but a value never comes back over the API.
69
+
70
+ ```ts
71
+ import { secrets, integrations } from '@proveanything/smartlinks'
72
+
73
+ // 1. Store the destination credential — keep the returned ref.
74
+ const { ref } = await secrets.set(collectionId, {
75
+ name: 'Acme API key',
76
+ purpose: 'integration',
77
+ value: 'sk_live_…', // sent once; never retrievable
78
+ })
79
+
80
+ // list shows refs + masked hints only (safe to render)
81
+ const { secrets: list } = await secrets.list(collectionId)
82
+ // → [{ ref, name: 'Acme API key', hint: '…live_1a2b', purpose, createdAt, ... }]
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Creating and running a flow
88
+
89
+ ```ts
90
+ // 2. Create an outbound flow that pushes products to Acme, authed by the secret above.
91
+ const flow = await integrations.createFlow(collectionId, {
92
+ appId: 'my-integration-app',
93
+ direction: 'outbound',
94
+ name: 'Push products to Acme',
95
+ status: 'active',
96
+ eventTypes: ['product.updated'], // fire whenever a product changes
97
+ sourceEntity: 'product',
98
+ config: {
99
+ connection: {
100
+ baseUrl: 'https://api.acme.example',
101
+ sendEndpoint: '/v1/products',
102
+ auth: { method: 'api_key', headerName: 'X-API-Key', credentialRef: ref },
103
+ },
104
+ fieldMappings: [
105
+ { targetPath: 'sku', sourcePath: 'sku', transformType: 'direct' },
106
+ { targetPath: 'name', sourcePath: 'name', transformType: 'direct' },
107
+ { targetPath: 'label', transformType: 'template', transformExpression: '{{name}} ({{sku}})' },
108
+ ],
109
+ },
110
+ })
111
+
112
+ // 3a. Test it now against one product — inline, returns a summary.
113
+ const result = await integrations.runFlow(collectionId, flow.id, { entityId: 'P1045716' })
114
+ if (integrations.isRunSummary(result)) {
115
+ console.log(result) // { records: 1, sent: 1, failed: 0, status: 'success' }
116
+ }
117
+
118
+ // 3b. Or enqueue on the worker (returns immediately).
119
+ await integrations.runFlow(collectionId, flow.id, { entityId: 'P1045716', async: true })
120
+ ```
121
+
122
+ Once `status: 'active'` with `eventTypes: ['product.updated']`, editing that product in the
123
+ admin API fires the flow automatically — no manual run needed.
124
+
125
+ ---
126
+
127
+ ## Reference
128
+
129
+ | Function | HTTP |
130
+ |---|---|
131
+ | `integrations.listFlows(collectionId, query?)` | `GET /integrations/flows` |
132
+ | `integrations.createFlow(collectionId, input)` | `POST /integrations/flows` |
133
+ | `integrations.getFlow(collectionId, id)` | `GET /integrations/flows/:id` |
134
+ | `integrations.updateFlow(collectionId, id, input)` | `PUT /integrations/flows/:id` |
135
+ | `integrations.deleteFlow(collectionId, id)` | `DELETE /integrations/flows/:id` |
136
+ | `integrations.runFlow(collectionId, id, opts?)` | `POST /integrations/flows/:id/run` |
137
+ | `secrets.list(collectionId, query?)` | `GET /secrets` |
138
+ | `secrets.set(collectionId, input)` | `POST /secrets` |
139
+ | `secrets.get(collectionId, ref)` | `GET /secrets/:ref` |
140
+ | `secrets.rotate(collectionId, ref, input)` | `PUT /secrets/:ref` |
141
+ | `secrets.remove(collectionId, ref)` | `DELETE /secrets/:ref` |
@@ -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