@proveanything/smartlinks 1.6.7 → 1.7.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.
- package/dist/api/attestation.d.ts +22 -0
- package/dist/api/attestation.js +22 -0
- package/dist/api/attestations.d.ts +292 -0
- package/dist/api/attestations.js +405 -0
- package/dist/api/containers.d.ts +236 -0
- package/dist/api/containers.js +316 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/tags.d.ts +20 -1
- package/dist/api/tags.js +30 -0
- package/dist/docs/API_SUMMARY.md +701 -7
- package/dist/docs/app-manifest.md +430 -0
- package/dist/docs/attestations.md +498 -0
- package/dist/docs/container-tracking.md +437 -0
- package/dist/docs/deep-link-discovery.md +6 -6
- package/dist/docs/executor.md +554 -0
- package/dist/index.d.ts +3 -0
- package/dist/openapi.yaml +3110 -1323
- package/dist/types/appManifest.d.ts +152 -0
- package/dist/types/attestation.d.ts +12 -0
- package/dist/types/attestations.d.ts +237 -0
- package/dist/types/attestations.js +11 -0
- package/dist/types/containers.d.ts +186 -0
- package/dist/types/containers.js +10 -0
- package/dist/types/tags.d.ts +47 -3
- package/docs/API_SUMMARY.md +701 -7
- package/docs/app-manifest.md +430 -0
- package/docs/attestations.md +498 -0
- package/docs/container-tracking.md +437 -0
- package/docs/deep-link-discovery.md +6 -6
- package/docs/executor.md +554 -0
- package/openapi.yaml +3110 -1323
- package/package.json +1 -1
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# Attestations
|
|
2
|
+
|
|
3
|
+
> Postgres-backed, append-only fact log for any object in the system.
|
|
4
|
+
|
|
5
|
+
The new `attestations` API replaces the legacy Firestore attestation endpoints and adds polymorphic subjects, three-tier data visibility, cryptographic hash-chain integrity, and time-series analytics.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Migration from Legacy Attestations](#migration-from-legacy-attestations)
|
|
13
|
+
- [TypeScript Interfaces](#typescript-interfaces)
|
|
14
|
+
- [Security & Audience Tiers](#security--audience-tiers)
|
|
15
|
+
- [SDK Usage](#sdk-usage)
|
|
16
|
+
- [Admin — Write](#admin--write)
|
|
17
|
+
- [Admin — Read](#admin--read)
|
|
18
|
+
- [Public — Read](#public--read)
|
|
19
|
+
- [Container-Scoped Shortcuts](#container-scoped-shortcuts)
|
|
20
|
+
- [REST API Reference](#rest-api-reference)
|
|
21
|
+
- [Design Notes](#design-notes)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
An **attestation** is an immutable fact record attached to a subject — a container, a proof, a product, a tag, or any other typed entity. Records are append-only: once written they are never updated or deleted.
|
|
28
|
+
|
|
29
|
+
Key properties:
|
|
30
|
+
|
|
31
|
+
| Property | Description |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **Polymorphic** | One endpoint covers any `subjectType` |
|
|
34
|
+
| **Three data zones** | `value` (public), `ownerData` (owner-tier), `adminData` (admin-tier) |
|
|
35
|
+
| **Per-record visibility** | Each record carries its own `visibility` flag |
|
|
36
|
+
| **Hash-chain integrity** | SHA-256 `contentHash` chains to the previous record for the same `(subjectType, subjectId, attestationType)` tuple |
|
|
37
|
+
| **Tree analytics** | Rollup queries aggregate across an entire container subtree via BFS traversal |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Migration from Legacy Attestations
|
|
42
|
+
|
|
43
|
+
| | Legacy (Firestore) | New (Postgres) |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| **Namespace** | `attestation` | `attestations` |
|
|
46
|
+
| **Subject scope** | Proof only (`/product/:pid/proof/:id/attestation`) | Any type: `container`, `proof`, `product`, `tag`, … |
|
|
47
|
+
| **Mutability** | Supports update & delete | Append-only (by design) |
|
|
48
|
+
| **Data zones** | `public`, `private`, `proof` | `value` (public), `ownerData` (owner), `adminData` (admin) |
|
|
49
|
+
| **Integrity** | None | SHA-256 hash chain |
|
|
50
|
+
|
|
51
|
+
The legacy `attestation` namespace (and its types `AttestationResponse`, `AttestationCreateRequest`, `AttestationUpdateRequest`) are marked `@deprecated`. The underlying Firestore endpoints remain active for backward-compatibility, but all new integrations should use the `attestations` namespace.
|
|
52
|
+
|
|
53
|
+
**Migrating a proof attestation write:**
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Before (legacy)
|
|
57
|
+
await attestation.create(collectionId, productId, proofId, {
|
|
58
|
+
public: { note: 'Temperature OK' },
|
|
59
|
+
private: {},
|
|
60
|
+
proof: {},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// After (new)
|
|
64
|
+
await attestations.create(collectionId, {
|
|
65
|
+
subjectType: 'proof',
|
|
66
|
+
subjectId: proofId,
|
|
67
|
+
attestationType: 'temperature_check',
|
|
68
|
+
value: { note: 'Temperature OK', celsius: 4.1 },
|
|
69
|
+
unit: '°C',
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## TypeScript Interfaces
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
type AttestationSubjectType = 'container' | 'proof' | 'product' | 'tag' | 'serial' | 'order_item' | string
|
|
79
|
+
type AttestationVisibility = 'public' | 'owner' | 'admin'
|
|
80
|
+
type AttestationAudience = 'public' | 'owner' | 'admin'
|
|
81
|
+
type AttestationGroupBy = 'hour' | 'day' | 'week' | 'month'
|
|
82
|
+
|
|
83
|
+
interface Attestation {
|
|
84
|
+
id: string
|
|
85
|
+
orgId: string
|
|
86
|
+
collectionId: string
|
|
87
|
+
subjectType: AttestationSubjectType
|
|
88
|
+
subjectId: string
|
|
89
|
+
attestationType: string // e.g. 'temperature', 'abv', 'angel_share'
|
|
90
|
+
recordedAt: string // ISO 8601 — when the fact was true
|
|
91
|
+
visibility: AttestationVisibility
|
|
92
|
+
value?: Record<string, any> // Public data zone
|
|
93
|
+
ownerData?: Record<string, any> // Stripped unless audience >= 'owner'
|
|
94
|
+
adminData?: Record<string, any> // Stripped unless audience === 'admin'
|
|
95
|
+
unit?: string
|
|
96
|
+
source?: string
|
|
97
|
+
authorId?: string
|
|
98
|
+
metadata?: Record<string, any>
|
|
99
|
+
contentHash: string // SHA-256 of this record (incl. prevHash)
|
|
100
|
+
prevHash?: string // contentHash of the preceding record
|
|
101
|
+
createdAt: string // ISO 8601 — database insertion time
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface LatestAttestation {
|
|
105
|
+
attestationType: string
|
|
106
|
+
latest: Attestation
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface AttestationSummaryBucket {
|
|
110
|
+
period: string // e.g. "2025-04-01" for groupBy=day
|
|
111
|
+
count: number
|
|
112
|
+
values?: Record<string, any>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ChainVerifyResult {
|
|
116
|
+
valid: boolean
|
|
117
|
+
checkedCount: number
|
|
118
|
+
failedAt?: string
|
|
119
|
+
message: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface CreateAttestationInput {
|
|
123
|
+
subjectType: AttestationSubjectType // required
|
|
124
|
+
subjectId: string // required
|
|
125
|
+
attestationType: string // required
|
|
126
|
+
recordedAt?: string
|
|
127
|
+
visibility?: AttestationVisibility // default 'public'
|
|
128
|
+
value?: Record<string, any>
|
|
129
|
+
ownerData?: Record<string, any>
|
|
130
|
+
adminData?: Record<string, any>
|
|
131
|
+
unit?: string
|
|
132
|
+
source?: string
|
|
133
|
+
authorId?: string
|
|
134
|
+
metadata?: Record<string, any>
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Security & Audience Tiers
|
|
141
|
+
|
|
142
|
+
### Admin endpoints
|
|
143
|
+
|
|
144
|
+
All admin endpoints are mounted under `/api/v1/admin/collection/:collectionId/attestations` and require a valid admin session or bearer token. The caller always receives all three data zones.
|
|
145
|
+
|
|
146
|
+
### Public endpoints
|
|
147
|
+
|
|
148
|
+
Public endpoints at `/api/v1/public/collection/:collectionId/attestations` are read-only and require no credentials for `audience='public'` data.
|
|
149
|
+
|
|
150
|
+
**Owner elevation** — supply a Firebase ID token via `Authorization: Bearer <idToken>`:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Authorization: Bearer <Firebase ID Token>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The middleware resolves the UID and checks ownership:
|
|
157
|
+
- `subjectType=proof` → checks `proof.userId` in Firestore
|
|
158
|
+
- `subjectType=container` → checks `container.metadata.proofId`
|
|
159
|
+
|
|
160
|
+
When ownership is confirmed the request is served at `audience='owner'`, which includes `ownerData`.
|
|
161
|
+
|
|
162
|
+
### Visibility vs audience
|
|
163
|
+
|
|
164
|
+
`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:
|
|
165
|
+
|
|
166
|
+
| Audience | Included records | Data zones |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `'public'` | `visibility='public'` only | `value` only |
|
|
169
|
+
| `'owner'` | `'public'` and `'owner'` | `value`, `ownerData` |
|
|
170
|
+
| `'admin'` | All records | `value`, `ownerData`, `adminData` |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## SDK Usage
|
|
175
|
+
|
|
176
|
+
Import via the top-level SDK export:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { attestations } from '@proveanything/smartlinks'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Admin — Write
|
|
183
|
+
|
|
184
|
+
#### Create a single attestation
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const record = await attestations.create('coll_123', {
|
|
188
|
+
subjectType: 'container',
|
|
189
|
+
subjectId: 'cask-uuid',
|
|
190
|
+
attestationType: 'temperature',
|
|
191
|
+
recordedAt: '2025-04-15T14:30:00Z',
|
|
192
|
+
value: { celsius: 12.4 },
|
|
193
|
+
ownerData: { sensorId: 'TEMP-7' },
|
|
194
|
+
unit: '°C',
|
|
195
|
+
visibility: 'public',
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Batch-create attestations
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const records = await attestations.createBatch('coll_123', [
|
|
203
|
+
{ subjectType: 'container', subjectId: 'cask-uuid', attestationType: 'temperature', value: { celsius: 12.4 } },
|
|
204
|
+
{ subjectType: 'container', subjectId: 'cask-uuid', attestationType: 'humidity', value: { rh: 68 } },
|
|
205
|
+
])
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Admin — Read
|
|
209
|
+
|
|
210
|
+
#### List attestations
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const { attestations: records } = await attestations.list('coll_123', {
|
|
214
|
+
subjectType: 'container',
|
|
215
|
+
subjectId: 'cask-uuid',
|
|
216
|
+
attestationType: 'temperature',
|
|
217
|
+
recordedAfter: '2025-01-01T00:00:00Z',
|
|
218
|
+
limit: 50,
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Time-series summary
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const { summary } = await attestations.summary('coll_123', {
|
|
226
|
+
subjectType: 'container',
|
|
227
|
+
subjectId: 'cask-uuid',
|
|
228
|
+
attestationType: 'temperature',
|
|
229
|
+
valueField: 'celsius',
|
|
230
|
+
groupBy: 'day',
|
|
231
|
+
})
|
|
232
|
+
// summary[0] = { period: '2025-04-01', count: 24, values: { celsius_avg: 12.1 } }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Latest snapshot
|
|
236
|
+
|
|
237
|
+
Returns one entry per `attestationType` — the most recent record for each type:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const { latest } = await attestations.latest('coll_123', {
|
|
241
|
+
subjectType: 'container',
|
|
242
|
+
subjectId: 'fridge-uuid',
|
|
243
|
+
})
|
|
244
|
+
// latest[0].attestationType === 'temperature'
|
|
245
|
+
// latest[0].latest.value === { celsius: 4.1 }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Verify hash chain
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const result = await attestations.verify('coll_123', {
|
|
252
|
+
subjectType: 'container',
|
|
253
|
+
subjectId: 'cask-uuid',
|
|
254
|
+
attestationType: 'temperature',
|
|
255
|
+
})
|
|
256
|
+
if (!result.valid) {
|
|
257
|
+
console.warn('Chain integrity broken at record:', result.failedAt)
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Tree time-series summary
|
|
262
|
+
|
|
263
|
+
Aggregates attestations across an **entire container subtree** (BFS traversal from a root container):
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const { summary, subjectCount } = await attestations.treeSummary('coll_123', {
|
|
267
|
+
subjectId: 'warehouse-uuid',
|
|
268
|
+
attestationType: 'temperature',
|
|
269
|
+
valueField: 'celsius',
|
|
270
|
+
groupBy: 'hour',
|
|
271
|
+
includeItems: true,
|
|
272
|
+
})
|
|
273
|
+
console.log(`Aggregated over ${subjectCount} subjects`)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Tree latest snapshot
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const { latest, subjectCount } = await attestations.treeLatest('coll_123', {
|
|
280
|
+
subjectId: 'warehouse-uuid',
|
|
281
|
+
includeItems: true,
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Public — Read
|
|
286
|
+
|
|
287
|
+
#### List (with optional owner elevation)
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const { attestations: records, audience } = await attestations.publicList('coll_123', {
|
|
291
|
+
subjectType: 'proof',
|
|
292
|
+
subjectId: 'proof-uuid',
|
|
293
|
+
})
|
|
294
|
+
// audience === 'owner' when a valid Firebase token was provided
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Time-series summary (always public)
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const { summary } = await attestations.publicSummary('coll_123', {
|
|
301
|
+
subjectType: 'container',
|
|
302
|
+
subjectId: 'cask-uuid',
|
|
303
|
+
attestationType: 'temperature',
|
|
304
|
+
groupBy: 'week',
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### Latest snapshot
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const { latest, audience } = await attestations.publicLatest('coll_123', {
|
|
312
|
+
subjectType: 'container',
|
|
313
|
+
subjectId: 'cask-uuid',
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Tree variants
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const { summary, subjectCount } = await attestations.publicTreeSummary('coll_123', {
|
|
321
|
+
subjectId: 'warehouse-uuid',
|
|
322
|
+
attestationType: 'temperature',
|
|
323
|
+
groupBy: 'day',
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const { latest } = await attestations.publicTreeLatest('coll_123', {
|
|
327
|
+
subjectId: 'warehouse-uuid',
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Container-Scoped Shortcuts
|
|
332
|
+
|
|
333
|
+
The public containers router re-exposes attestation endpoints pre-scoped to `subjectType=container`. Use these when you already have a `containerId`:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// List attestations for a container
|
|
337
|
+
const { attestations: records } = await attestations.publicContainerList(
|
|
338
|
+
'coll_123',
|
|
339
|
+
'cask-uuid',
|
|
340
|
+
{ attestationType: 'temperature', limit: 10 }
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
// Latest snapshot
|
|
344
|
+
const { latest } = await attestations.publicContainerLatest('coll_123', 'cask-uuid')
|
|
345
|
+
|
|
346
|
+
// Time-series summary
|
|
347
|
+
const { summary } = await attestations.publicContainerSummary('coll_123', 'cask-uuid', {
|
|
348
|
+
attestationType: 'temperature',
|
|
349
|
+
valueField: 'celsius',
|
|
350
|
+
groupBy: 'day',
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// Tree summary rooted at this container
|
|
354
|
+
const { summary: treeSummary } = await attestations.publicContainerTreeSummary('coll_123', 'warehouse-uuid', {
|
|
355
|
+
attestationType: 'temperature',
|
|
356
|
+
groupBy: 'hour',
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Tree latest
|
|
360
|
+
const { latest: treeLatest } = await attestations.publicContainerTreeLatest('coll_123', 'warehouse-uuid')
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## REST API Reference
|
|
366
|
+
|
|
367
|
+
### Admin endpoints
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
POST /api/v1/admin/collection/:collectionId/attestations
|
|
371
|
+
GET /api/v1/admin/collection/:collectionId/attestations
|
|
372
|
+
GET /api/v1/admin/collection/:collectionId/attestations/summary
|
|
373
|
+
GET /api/v1/admin/collection/:collectionId/attestations/latest
|
|
374
|
+
GET /api/v1/admin/collection/:collectionId/attestations/verify
|
|
375
|
+
GET /api/v1/admin/collection/:collectionId/attestations/tree-summary
|
|
376
|
+
GET /api/v1/admin/collection/:collectionId/attestations/tree-latest
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### POST body — single record
|
|
380
|
+
|
|
381
|
+
```json
|
|
382
|
+
{
|
|
383
|
+
"subjectType": "container",
|
|
384
|
+
"subjectId": "uuid-of-cask",
|
|
385
|
+
"attestationType": "temperature",
|
|
386
|
+
"recordedAt": "2025-04-15T14:30:00Z",
|
|
387
|
+
"value": { "celsius": 12.4 },
|
|
388
|
+
"ownerData": { "sensorId": "TEMP-7" },
|
|
389
|
+
"unit": "°C",
|
|
390
|
+
"visibility": "public"
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### POST body — batch (array)
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
[
|
|
398
|
+
{ "subjectType": "container", "subjectId": "uuid1", "attestationType": "temperature", "value": { "celsius": 12.4 } },
|
|
399
|
+
{ "subjectType": "container", "subjectId": "uuid1", "attestationType": "humidity", "value": { "rh": 68 } }
|
|
400
|
+
]
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### GET query parameters (list)
|
|
404
|
+
|
|
405
|
+
| Parameter | Required | Description |
|
|
406
|
+
|---|---|---|
|
|
407
|
+
| `subjectType` | ✅ | Subject type |
|
|
408
|
+
| `subjectId` | ✅ | Subject UUID |
|
|
409
|
+
| `attestationType` | — | Filter by type |
|
|
410
|
+
| `recordedAfter` | — | ISO 8601 lower bound |
|
|
411
|
+
| `recordedBefore` | — | ISO 8601 upper bound |
|
|
412
|
+
| `limit` | — | Default `100` |
|
|
413
|
+
| `offset` | — | Default `0` |
|
|
414
|
+
|
|
415
|
+
#### GET query parameters (summary)
|
|
416
|
+
|
|
417
|
+
| Parameter | Required | Description |
|
|
418
|
+
|---|---|---|
|
|
419
|
+
| `subjectType` | ✅ | |
|
|
420
|
+
| `subjectId` | ✅ | |
|
|
421
|
+
| `attestationType` | ✅ | |
|
|
422
|
+
| `valueField` | — | Dot-path inside `value` to aggregate |
|
|
423
|
+
| `groupBy` | — | `hour` \| `day` \| `week` \| `month` (default `day`) |
|
|
424
|
+
| `recordedAfter` | — | ISO 8601 |
|
|
425
|
+
| `recordedBefore` | — | ISO 8601 |
|
|
426
|
+
| `limit` | — | Max buckets (default `200`) |
|
|
427
|
+
|
|
428
|
+
#### GET query parameters (tree-summary / tree-latest)
|
|
429
|
+
|
|
430
|
+
| Parameter | Required | Description |
|
|
431
|
+
|---|---|---|
|
|
432
|
+
| `subjectType` | ✅ | Must be `container` |
|
|
433
|
+
| `subjectId` | ✅ | Root container UUID |
|
|
434
|
+
| `attestationType` | ✅ (tree-summary) | |
|
|
435
|
+
| `valueField` | — | |
|
|
436
|
+
| `groupBy` | — | |
|
|
437
|
+
| `recordedAfter` | — | |
|
|
438
|
+
| `recordedBefore` | — | |
|
|
439
|
+
| `limit` | — | |
|
|
440
|
+
| `includeItems` | — | Include container items in BFS traversal (default `true`) |
|
|
441
|
+
|
|
442
|
+
### Public endpoints
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
GET /api/v1/public/collection/:collectionId/attestations
|
|
446
|
+
GET /api/v1/public/collection/:collectionId/attestations/summary
|
|
447
|
+
GET /api/v1/public/collection/:collectionId/attestations/latest
|
|
448
|
+
GET /api/v1/public/collection/:collectionId/attestations/tree-summary
|
|
449
|
+
GET /api/v1/public/collection/:collectionId/attestations/tree-latest
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Same query parameters as admin. Add `Authorization: Bearer <idToken>` for owner elevation.
|
|
453
|
+
|
|
454
|
+
### Container-scoped public shortcuts
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId/attestations
|
|
458
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId/attestations/summary
|
|
459
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId/attestations/latest
|
|
460
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId/attestations/tree-summary
|
|
461
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId/attestations/tree-latest
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Identical to the polymorphic routes with `subjectType=container&subjectId=:containerId` pre-filled.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Design Notes
|
|
469
|
+
|
|
470
|
+
### Append-only records
|
|
471
|
+
|
|
472
|
+
Attestations must **never** be updated or deleted. The hash chain exists to make tampering detectable. To correct a recorded fact, append a new attestation record with a note in `metadata`; run `/verify` to confirm chain integrity.
|
|
473
|
+
|
|
474
|
+
### Hash chain structure
|
|
475
|
+
|
|
476
|
+
Each record's `contentHash` is computed as:
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
SHA-256(subjectType + subjectId + attestationType + recordedAt + JSON(value) + prevHash)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`prevHash` is the `contentHash` of the immediately preceding record for the same `(subjectType, subjectId, attestationType)` tuple. The first record in a chain has no `prevHash`.
|
|
483
|
+
|
|
484
|
+
### Tree analytics use case
|
|
485
|
+
|
|
486
|
+
The tree endpoints enable queries like *"what was the average temperature inside the warehouse (and all sub-containers and their items) over the last 7 days?"*:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
const { summary } = await attestations.publicTreeSummary('coll_123', {
|
|
490
|
+
subjectId: 'warehouse-uuid',
|
|
491
|
+
attestationType: 'temperature',
|
|
492
|
+
valueField: 'celsius',
|
|
493
|
+
groupBy: 'day',
|
|
494
|
+
recordedAfter: sevenDaysAgo,
|
|
495
|
+
})
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
This also enables the cold-chain use-case: was a particular item inside a container that exceeded a temperature threshold during a given window? Cross-reference container attestations with container item membership timestamps using `ContainerItem.addedAt` and `ContainerItem.removedAt`.
|