@totemsdk/spatial-proof 0.1.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/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/canonical.d.ts +53 -0
- package/dist/canonical.js +97 -0
- package/dist/claim.d.ts +34 -0
- package/dist/claim.js +75 -0
- package/dist/geometry.d.ts +91 -0
- package/dist/geometry.js +334 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +68 -0
- package/dist/proof.d.ts +54 -0
- package/dist/proof.js +189 -0
- package/dist/proofgraph.d.ts +44 -0
- package/dist/proofgraph.js +98 -0
- package/dist/relations.d.ts +21 -0
- package/dist/relations.js +270 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.js +13 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Totem SDK Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# @totemsdk/spatial-proof
|
|
2
|
+
|
|
3
|
+
Generic spatial relationship proof primitives for Totem Edge — geometry hashes, geofence relations, route checks, and proof envelope integration.
|
|
4
|
+
|
|
5
|
+
No network. No storage. No map rendering. No GDAL. No PostGIS. No external API. Pure schema, calculation, and proof primitives.
|
|
6
|
+
|
|
7
|
+
## What this package does
|
|
8
|
+
|
|
9
|
+
`@totemsdk/spatial-proof` answers one question:
|
|
10
|
+
|
|
11
|
+
> "How does this proof, device, asset, observation, raster, image, route, or event relate to a known spatial object?"
|
|
12
|
+
|
|
13
|
+
For example:
|
|
14
|
+
|
|
15
|
+
- a location claim is **inside** a site boundary
|
|
16
|
+
- a drone flight **covered** a polygon
|
|
17
|
+
- a ship **entered** a maritime zone
|
|
18
|
+
- a robot **stayed** inside its permitted work area
|
|
19
|
+
- a vehicle **followed** an approved route
|
|
20
|
+
- a weather sensor is **within distance** of a construction-site geofence
|
|
21
|
+
- a satellite scene footprint **covers** a project boundary
|
|
22
|
+
- a derived flood mask **overlaps** a work zone
|
|
23
|
+
|
|
24
|
+
It is not ZEDD-specific. ZEDD, Wajibika, AgriPulse, Guardbase, logistics, robotics, maritime, drones, and autonomous vehicles can all use it.
|
|
25
|
+
|
|
26
|
+
## What this package does NOT prove
|
|
27
|
+
|
|
28
|
+
This package produces deterministic geospatial relation claims, not absolute geodetic truth. Where a calculation is approximate, the claim's `result.uncertainty` says so explicitly.
|
|
29
|
+
|
|
30
|
+
- **Bounding-box relations** (`intersects`, `overlaps`, `covers`, `covered_by`) are computed on axis-aligned bounding boxes and are **not** exact polygon intersections or coverages — the claim always carries a bounding-box uncertainty note.
|
|
31
|
+
- **Planar geometry** — point-in-polygon, boundary distance, and route distance use planar/equirectangular math on lon/lat. Fine for typical geofences; not geodesic.
|
|
32
|
+
- **No GIS engine parity** — no projections, topology, topology repair, or full polygon boolean operations.
|
|
33
|
+
|
|
34
|
+
Out of scope (intentionally):
|
|
35
|
+
|
|
36
|
+
- GPS hardware drivers, satellite APIs, map services, drone telemetry → those live in transport/device packages
|
|
37
|
+
- Raster/image processing → future `@totemsdk/raster-proof`
|
|
38
|
+
- Map rendering, database storage, network calls, legal claims engines
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @totemsdk/spatial-proof
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Coordinate order (READ FIRST)
|
|
47
|
+
|
|
48
|
+
**All coordinates are `[lon, lat]`** — GeoJSON order (WGS84 / EPSG:4326), NOT `[lat, lon]`. This is the opposite of the `{ lat, lon }` shape used by `@totemsdk/location-proof`.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { pointInPolygon } from '@totemsdk/spatial-proof';
|
|
52
|
+
|
|
53
|
+
const site = {
|
|
54
|
+
type: 'Polygon' as const,
|
|
55
|
+
coordinates: [[
|
|
56
|
+
[0, 0], [0, 1], [1, 1], [1, 0], [0, 0], // [lon, lat]
|
|
57
|
+
]],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
pointInPolygon([0.5, 0.5], site); // true — lon 0.5, lat 0.5
|
|
61
|
+
pointInPolygon([5, 5], site); // false
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Package scope
|
|
65
|
+
|
|
66
|
+
- GeoJSON-like `Point`, `LineString`, `Polygon`, `MultiPolygon` in `[lon, lat]`
|
|
67
|
+
- Axis-aligned bounding boxes and bbox intersection/coverage
|
|
68
|
+
- WGS84 / EPSG:4326 metadata (any other CRS produces a validation warning)
|
|
69
|
+
- Geometry hashing (`totem:geo:…`)
|
|
70
|
+
- Spatial object IDs (`totem:spatial:…`)
|
|
71
|
+
- Point-in-polygon, point distance, point-near-boundary, point-on-route
|
|
72
|
+
- Polygon overlap via bbox precheck plus polygon vertex probing (approximate)
|
|
73
|
+
- `covers` for scene footprints over site boundaries
|
|
74
|
+
- Deterministic relation claims with explicit uncertainty notes
|
|
75
|
+
- `@totemsdk/proof` evidence refs, unsigned proof creation, WOTS signing, and end-to-end verification
|
|
76
|
+
- `@totemsdk/proofgraph` node/edge helpers
|
|
77
|
+
- `@totemsdk/location-proof` integration (one-directional dependency)
|
|
78
|
+
|
|
79
|
+
## API table
|
|
80
|
+
|
|
81
|
+
### IDs and hashing
|
|
82
|
+
|
|
83
|
+
| Export | Description |
|
|
84
|
+
|--------|-------------|
|
|
85
|
+
| `canonicalJson(value)` | Deterministic canonical JSON — recursively sorted keys |
|
|
86
|
+
| `toHex(bytes)` | Uint8Array → lowercase hex (no `0x`) |
|
|
87
|
+
| `computeGeometryHash(geometry)` | Stable geometry ID `totem:geo:<sha3-256-hex>` |
|
|
88
|
+
| `computeSpatialObjectId(input)` | Stable object ID `totem:spatial:<sha3-256-hex>` |
|
|
89
|
+
| `computeSpatialRelationId(input)` | Stable relation ID `totem:spatial:<sha3-256-hex>` |
|
|
90
|
+
| `hashSpatialObject(obj)` | SHA3-256 hex of an object's stable fields |
|
|
91
|
+
| `hashSpatialRelationClaim(claim)` | SHA3-256 hex of a claim's stable fields |
|
|
92
|
+
|
|
93
|
+
### Geometry
|
|
94
|
+
|
|
95
|
+
| Export | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| `validateCoordinate(coord)` | Range-check a `[lon, lat]` coordinate |
|
|
98
|
+
| `validateGeometry(geometry)` | Structural validation (ring closure, min points, ranges) |
|
|
99
|
+
| `validateSpatialObject(obj)` | Object validation (geometry + CRS warnings) |
|
|
100
|
+
| `getBoundingBox(geometry)` | Axis-aligned bounding box of any geometry |
|
|
101
|
+
| `bboxIntersects(a, b)` | Bbox overlap test |
|
|
102
|
+
| `bboxCovers(a, b)` | Bbox containment test |
|
|
103
|
+
| `distanceMeters(a, b)` | Haversine great-circle distance in meters |
|
|
104
|
+
| `distancePointToSegmentMeters(p, a, b)` | Perpendicular distance point → segment |
|
|
105
|
+
| `distancePointToLineStringMeters(p, line)` | Minimum distance point → LineString |
|
|
106
|
+
| `pointInPolygon(p, polygon)` | Ray-casting point-in-polygon (outer ring) |
|
|
107
|
+
| `pointInMultiPolygon(p, multi)` | Point in any polygon of a MultiPolygon |
|
|
108
|
+
| `isPointNearBoundary(p, polygon, thresholdM)` | Point within `thresholdM` of a boundary ring |
|
|
109
|
+
| `isRingClosed(ring)` | Check ring closure |
|
|
110
|
+
| `normalizePolygonRing(ring)` | Deterministically close a ring (immutable) |
|
|
111
|
+
| `normalizePolygon(polygon)` | Deterministically close all rings (immutable) |
|
|
112
|
+
|
|
113
|
+
### Relation evaluation
|
|
114
|
+
|
|
115
|
+
| Export | Description |
|
|
116
|
+
|--------|-------------|
|
|
117
|
+
| `evaluateSpatialRelation(params)` | Deterministic relation claim from subject + spatial object |
|
|
118
|
+
| `validateSpatialRelationClaim(claim)` | Structural validation of a relation claim |
|
|
119
|
+
|
|
120
|
+
### Location-proof integration
|
|
121
|
+
|
|
122
|
+
| Export | Description |
|
|
123
|
+
|--------|-------------|
|
|
124
|
+
| `spatialRelationFromLocationClaim(params)` | Derive a relation claim from a `LocationClaim` |
|
|
125
|
+
|
|
126
|
+
### Proof integration
|
|
127
|
+
|
|
128
|
+
| Export | Description |
|
|
129
|
+
|--------|-------------|
|
|
130
|
+
| `spatialObjectToEvidenceRef(obj)` | Object → `EvidenceRef` |
|
|
131
|
+
| `spatialRelationToEvidenceRef(claim)` | Claim → `EvidenceRef` |
|
|
132
|
+
| `spatialClaimEvidenceRefs(claim, obj, subjectGeometry?)` | Full evidence ref list for a spatial proof |
|
|
133
|
+
| `createUnsignedSpatialProof(params)` | Build an unsigned `attestation` proof |
|
|
134
|
+
| `signSpatialProof(unsigned, seed, keyIndex)` | WOTS-sign the proof |
|
|
135
|
+
| `verifySpatialProof(signed)` | Full end-to-end verification |
|
|
136
|
+
|
|
137
|
+
### Proofgraph integration
|
|
138
|
+
|
|
139
|
+
| Export | Description |
|
|
140
|
+
|--------|-------------|
|
|
141
|
+
| `spatialObjectToProofGraphNode(obj)` | Object → `custom` `ProofGraphNode` |
|
|
142
|
+
| `spatialRelationToProofGraphNode(claim)` | Claim → `custom` `ProofGraphNode` |
|
|
143
|
+
| `spatialRelationToGraphEdges(claim)` | Claim → `about` / `references` / `derived_from` edges |
|
|
144
|
+
| `addSpatialRelationToGraph(graph, claim)` | Immutably add a claim node |
|
|
145
|
+
|
|
146
|
+
## Type reference
|
|
147
|
+
|
|
148
|
+
### `Coordinate` and `GeoGeometry`
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
type Coordinate = [number, number]; // [lon, lat]
|
|
152
|
+
|
|
153
|
+
type GeoGeometry =
|
|
154
|
+
| { type: 'Point'; coordinates: Coordinate }
|
|
155
|
+
| { type: 'LineString'; coordinates: Coordinate[] }
|
|
156
|
+
| { type: 'Polygon'; coordinates: Coordinate[][] } // rings (outer + holes), closed
|
|
157
|
+
| { type: 'MultiPolygon'; coordinates: Coordinate[][][] };
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `SpatialObject`
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
interface SpatialObject {
|
|
164
|
+
spatialId: string; // totem:spatial:<sha3-256-hex>
|
|
165
|
+
kind: 'site-boundary' | 'zone' | 'route' | 'scene-footprint'
|
|
166
|
+
| 'asset-footprint' | 'restricted-area' | 'inspection-area' | 'custom';
|
|
167
|
+
geometry: GeoGeometry;
|
|
168
|
+
crs?: 'EPSG:4326' | string; // defaults to EPSG:4326
|
|
169
|
+
name?: string;
|
|
170
|
+
metadata?: Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `SpatialRelationClaim`
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface SpatialRelationClaim {
|
|
178
|
+
relationId: string; // totem:spatial:<sha3-256-hex>
|
|
179
|
+
subjectId: string;
|
|
180
|
+
subjectKind: string;
|
|
181
|
+
spatialObjectId: string;
|
|
182
|
+
relation: 'inside' | 'outside' | 'intersects' | 'overlaps' | 'covers'
|
|
183
|
+
| 'covered_by' | 'within_distance' | 'near_boundary' | 'on_route'
|
|
184
|
+
| 'entered_zone' | 'exited_zone' | 'unknown';
|
|
185
|
+
computedAt: number; // ms since epoch
|
|
186
|
+
engine: { name: string; version: string; algorithm: string };
|
|
187
|
+
inputs: {
|
|
188
|
+
subjectGeometryHash?: string;
|
|
189
|
+
spatialGeometryHash: string;
|
|
190
|
+
subjectProofId?: string;
|
|
191
|
+
locationClaimId?: string;
|
|
192
|
+
rasterManifestId?: string;
|
|
193
|
+
};
|
|
194
|
+
result: {
|
|
195
|
+
matched: boolean;
|
|
196
|
+
distanceM?: number;
|
|
197
|
+
confidenceScore?: number;
|
|
198
|
+
uncertainty?: string[]; // explicit notes where math is approximate
|
|
199
|
+
};
|
|
200
|
+
metadata?: Record<string, unknown>;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Stable IDs exclude only mutable fields: `relationId` and `metadata` for claims; `spatialId` and `metadata` for objects. `computedAt` and `result` are content — a re-evaluation at a different time is a new observation of a relationship.
|
|
205
|
+
|
|
206
|
+
## Usage example — point inside polygon → proof → sign → verify
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import {
|
|
210
|
+
computeSpatialObjectId,
|
|
211
|
+
evaluateSpatialRelation,
|
|
212
|
+
createUnsignedSpatialProof,
|
|
213
|
+
signSpatialProof,
|
|
214
|
+
verifySpatialProof,
|
|
215
|
+
} from '@totemsdk/spatial-proof';
|
|
216
|
+
|
|
217
|
+
const site = {
|
|
218
|
+
spatialId: computeSpatialObjectId({
|
|
219
|
+
kind: 'site-boundary',
|
|
220
|
+
name: 'ZEDD site A',
|
|
221
|
+
geometry: {
|
|
222
|
+
type: 'Polygon',
|
|
223
|
+
coordinates: [[
|
|
224
|
+
[36.80, -1.28], [36.82, -1.28], [36.82, -1.30], [36.80, -1.30], [36.80, -1.28],
|
|
225
|
+
]],
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
kind: 'site-boundary' as const,
|
|
229
|
+
name: 'ZEDD site A',
|
|
230
|
+
geometry: {
|
|
231
|
+
type: 'Polygon',
|
|
232
|
+
coordinates: [[
|
|
233
|
+
[36.80, -1.28], [36.82, -1.28], [36.82, -1.30], [36.80, -1.30], [36.80, -1.28],
|
|
234
|
+
]],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// drone GPS fix: lon 36.81, lat -1.29 (GeoJSON [lon, lat] order)
|
|
239
|
+
const claim = evaluateSpatialRelation({
|
|
240
|
+
subjectId: 'device:drone-007',
|
|
241
|
+
subjectKind: 'device',
|
|
242
|
+
spatialObject: site,
|
|
243
|
+
relation: 'inside',
|
|
244
|
+
subjectGeometry: { type: 'Point', coordinates: [36.81, -1.29] },
|
|
245
|
+
computedAt: 1_700_000_000_000,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log(claim.result.matched); // true
|
|
249
|
+
console.log(claim.result.uncertainty); // ['Point-in-polygon test is planar (lon/lat); not a geodesic test.']
|
|
250
|
+
|
|
251
|
+
const unsigned = createUnsignedSpatialProof({ claim, spatialObject: site, issuedAt: 1_700_000_000_000 });
|
|
252
|
+
const signed = signSpatialProof(unsigned, seedBytes, 7); // reserve the WOTS key index first!
|
|
253
|
+
|
|
254
|
+
const result = verifySpatialProof(signed);
|
|
255
|
+
console.log(result.valid, result.relationId);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Location-proof integration example
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { createLocationClaim } from '@totemsdk/location-proof';
|
|
262
|
+
import { spatialRelationFromLocationClaim } from '@totemsdk/spatial-proof';
|
|
263
|
+
|
|
264
|
+
const locationClaim = createLocationClaim({
|
|
265
|
+
subjectId: 'device:drone-007',
|
|
266
|
+
deviceId: 'drone-007',
|
|
267
|
+
observedAt: 1_700_000_000_000,
|
|
268
|
+
location: { lat: -1.29, lon: 36.81, accuracyM: 2 }, // location-proof uses { lat, lon }
|
|
269
|
+
source: { type: 'rtk', satellitesUsed: 16, hdop: 0.7 },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const relation = spatialRelationFromLocationClaim({
|
|
273
|
+
locationClaim,
|
|
274
|
+
spatialObject: site,
|
|
275
|
+
relation: 'inside',
|
|
276
|
+
computedAt: 1_700_000_000_000,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
console.log(relation.result.matched); // true
|
|
280
|
+
console.log(relation.inputs.locationClaimId); // totem:location:…
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Proofgraph example
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { createProofGraph, addEdge } from '@totemsdk/proofgraph';
|
|
287
|
+
import {
|
|
288
|
+
spatialRelationToProofGraphNode,
|
|
289
|
+
spatialRelationToGraphEdges,
|
|
290
|
+
} from '@totemsdk/spatial-proof';
|
|
291
|
+
|
|
292
|
+
let graph = createProofGraph();
|
|
293
|
+
const node = spatialRelationToProofGraphNode(claim); // custom:<relationId>
|
|
294
|
+
for (const edge of spatialRelationToGraphEdges(claim)) {
|
|
295
|
+
graph = addEdge(graph, edge); // about / references / derived_from
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Drone / ship / robot / vehicle examples
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// Drone flight covered a polygon
|
|
303
|
+
const covered = evaluateSpatialRelation({
|
|
304
|
+
subjectId: 'device:drone-007',
|
|
305
|
+
subjectKind: 'device',
|
|
306
|
+
spatialObject: { ...site, kind: 'zone' },
|
|
307
|
+
relation: 'covers',
|
|
308
|
+
subjectGeometry: { type: 'Polygon', coordinates: [[...flightBbox]] }, // approx — bbox only
|
|
309
|
+
computedAt: 1_700_000_000_000,
|
|
310
|
+
});
|
|
311
|
+
// covered.result.uncertainty includes the bounding-box note.
|
|
312
|
+
|
|
313
|
+
// Ship entered a maritime zone
|
|
314
|
+
const entered = evaluateSpatialRelation({
|
|
315
|
+
subjectId: 'vessel:ms-heron',
|
|
316
|
+
subjectKind: 'vessel',
|
|
317
|
+
spatialObject: { ...site, kind: 'zone', name: 'EEZ maritime zone' },
|
|
318
|
+
relation: 'entered_zone',
|
|
319
|
+
subjectGeometry: { type: 'Point', coordinates: [40.0, 35.0] },
|
|
320
|
+
computedAt: 1_700_000_000_000,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Robot stayed inside a permitted work area
|
|
324
|
+
const stayed = evaluateSpatialRelation({
|
|
325
|
+
subjectId: 'robot:arm-01',
|
|
326
|
+
subjectKind: 'robot',
|
|
327
|
+
spatialObject: { ...site, kind: 'restricted-area' },
|
|
328
|
+
relation: 'inside',
|
|
329
|
+
subjectGeometry: { type: 'Point', coordinates: [36.81, -1.29] },
|
|
330
|
+
computedAt: 1_700_000_000_000,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Vehicle followed an approved route (LineString, corridor tolerance)
|
|
334
|
+
const onRoute = evaluateSpatialRelation({
|
|
335
|
+
subjectId: 'vehicle:bus-12',
|
|
336
|
+
subjectKind: 'vehicle',
|
|
337
|
+
spatialObject: { ...site, kind: 'route', geometry: approvedRouteLineString },
|
|
338
|
+
relation: 'on_route',
|
|
339
|
+
subjectGeometry: { type: 'Point', coordinates: [36.81, -1.29] },
|
|
340
|
+
maxDistanceM: 50,
|
|
341
|
+
computedAt: 1_700_000_000_000,
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Limitations
|
|
346
|
+
|
|
347
|
+
- `intersects` / `overlaps` / `covers` / `covered_by` are **bounding-box approximations** — never treated as exact polygon intersections or coverages. The claim always says so.
|
|
348
|
+
- Polygon "overlap" uses a vertex-probing heuristic when both sides are polygons; polygons that cross edges without shared vertices may be misjudged. Full polygon intersection is out of scope.
|
|
349
|
+
- All math is planar/equirectangular on WGS84 lon/lat. Suitable for geofences and local routes; not a geodetic engine.
|
|
350
|
+
- `MultiPolygon` holes are validated but the hole semantics of `pointInPolygon` are not applied (outer ring only).
|
|
351
|
+
|
|
352
|
+
## Security notes
|
|
353
|
+
|
|
354
|
+
- **WOTS one-time keys.** Each WOTS key index can be used exactly once. Never sign two different proofs with the same key index. Reserve indices through [`@totemsdk/wots-lease`](https://www.npmjs.com/package/@totemsdk/wots-lease) before signing.
|
|
355
|
+
- **Relations are not absolute truth.** A bbox-only relation is a deliberate approximation — trust it only where bbox precision is acceptable.
|
|
356
|
+
- **Input integrity.** The subject geometry is hashed into the claim. To verify that a claim's `result` matches its inputs, re-evaluate with the same subject geometry and compare `matched`.
|
|
357
|
+
- **Proof verification** recomputes the relation ID, evidence hashes, and WOTS signature; anchored records are optional and not required.
|
|
358
|
+
|
|
359
|
+
## Related packages
|
|
360
|
+
|
|
361
|
+
- [`@totemsdk/core`](https://www.npmjs.com/package/@totemsdk/core) — SHA3-256, WOTS signing, script derivation
|
|
362
|
+
- [`@totemsdk/proof`](https://www.npmjs.com/package/@totemsdk/proof) — proof envelopes, signing, verification
|
|
363
|
+
- [`@totemsdk/proofgraph`](https://www.npmjs.com/package/@totemsdk/proofgraph) — content-addressed proof relationship graph
|
|
364
|
+
- [`@totemsdk/location-proof`](https://www.npmjs.com/package/@totemsdk/location-proof) — device-neutral location claims and confidence scoring
|
|
365
|
+
- [`@totemsdk/identity`](https://www.npmjs.com/package/@totemsdk/identity) — device/agent identities
|
|
366
|
+
- [`@totemsdk/manifest`](https://www.npmjs.com/package/@totemsdk/manifest) — signed entity declarations
|
|
367
|
+
- [`@totemsdk/wots-lease`](https://www.npmjs.com/package/@totemsdk/wots-lease) — one-time key safety
|
|
368
|
+
- [`@totemsdk/edge`](https://www.npmjs.com/package/@totemsdk/edge) — unified edge runtime
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON, hashing, and stable ID rules for @totemsdk/spatial-proof.
|
|
3
|
+
*
|
|
4
|
+
* canonicalJson and toHex are re-exported from @totemsdk/proof, which is the
|
|
5
|
+
* canonical implementation across the Totem SDK (deterministic canonical JSON
|
|
6
|
+
* with recursively sorted keys, lowercase hex without 0x prefix).
|
|
7
|
+
*
|
|
8
|
+
* ID rules:
|
|
9
|
+
* Geometry: "totem:geo:" + sha3_256("totem-geometry" + canonicalJson(geometry))
|
|
10
|
+
* Spatial object:"totem:spatial:" + sha3_256("totem-spatial-object" + canonicalJson(stableObject))
|
|
11
|
+
* Relation claim:"totem:spatial:" + sha3_256("totem-spatial-relation" + canonicalJson(stableRelation))
|
|
12
|
+
*
|
|
13
|
+
* Stable IDs deliberately exclude mutable / non-content fields so equivalent
|
|
14
|
+
* logical content always produces the same identifier:
|
|
15
|
+
* spatial object: spatialId, metadata
|
|
16
|
+
* relation claim: relationId, metadata (result and computedAt are content —
|
|
17
|
+
* a re-evaluation is a new observation of a relationship).
|
|
18
|
+
*
|
|
19
|
+
* COORDINATE ORDER is [lon, lat] (GeoJSON / EPSG:4326).
|
|
20
|
+
*/
|
|
21
|
+
import { canonicalJson, toHex } from '@totemsdk/proof';
|
|
22
|
+
import type { GeoGeometry, SpatialObject, SpatialRelationClaim } from './types.js';
|
|
23
|
+
export { canonicalJson, toHex };
|
|
24
|
+
/**
|
|
25
|
+
* Compute the stable geometry identifier: "totem:geo:<sha3-256-hex>".
|
|
26
|
+
* Deterministic over the exact geometry — the same geometry always hashes
|
|
27
|
+
* to the same identifier.
|
|
28
|
+
*/
|
|
29
|
+
export declare function computeGeometryHash(geometry: GeoGeometry): string;
|
|
30
|
+
/**
|
|
31
|
+
* Compute a stable URI-style spatial object ID: "totem:spatial:<sha3-256-hex>".
|
|
32
|
+
* Callers pass the object minus spatialId; metadata is excluded internally.
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeSpatialObjectId(input: Omit<SpatialObject, 'spatialId'>): string;
|
|
35
|
+
/**
|
|
36
|
+
* Compute a stable URI-style spatial relation claim ID using the same
|
|
37
|
+
* "totem:spatial:" namespace as spatial objects. Relation and object hashes
|
|
38
|
+
* are distinguishable by their content-derived preimage, so the shared prefix
|
|
39
|
+
* cannot cause an ID collision for equivalent logical content.
|
|
40
|
+
*/
|
|
41
|
+
export declare function computeSpatialRelationId(input: Omit<SpatialRelationClaim, 'relationId'>): string;
|
|
42
|
+
/**
|
|
43
|
+
* Hash a complete SpatialObject (excluding spatialId and metadata)
|
|
44
|
+
* to lowercase SHA3-256 hex without a 0x prefix — the value used in
|
|
45
|
+
* EvidenceRef.hash.
|
|
46
|
+
*/
|
|
47
|
+
export declare function hashSpatialObject(obj: SpatialObject): string;
|
|
48
|
+
/**
|
|
49
|
+
* Hash a complete SpatialRelationClaim (excluding relationId and metadata)
|
|
50
|
+
* to lowercase SHA3-256 hex without a 0x prefix — the value used in
|
|
51
|
+
* EvidenceRef.hash.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hashSpatialRelationClaim(claim: SpatialRelationClaim): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical JSON, hashing, and stable ID rules for @totemsdk/spatial-proof.
|
|
4
|
+
*
|
|
5
|
+
* canonicalJson and toHex are re-exported from @totemsdk/proof, which is the
|
|
6
|
+
* canonical implementation across the Totem SDK (deterministic canonical JSON
|
|
7
|
+
* with recursively sorted keys, lowercase hex without 0x prefix).
|
|
8
|
+
*
|
|
9
|
+
* ID rules:
|
|
10
|
+
* Geometry: "totem:geo:" + sha3_256("totem-geometry" + canonicalJson(geometry))
|
|
11
|
+
* Spatial object:"totem:spatial:" + sha3_256("totem-spatial-object" + canonicalJson(stableObject))
|
|
12
|
+
* Relation claim:"totem:spatial:" + sha3_256("totem-spatial-relation" + canonicalJson(stableRelation))
|
|
13
|
+
*
|
|
14
|
+
* Stable IDs deliberately exclude mutable / non-content fields so equivalent
|
|
15
|
+
* logical content always produces the same identifier:
|
|
16
|
+
* spatial object: spatialId, metadata
|
|
17
|
+
* relation claim: relationId, metadata (result and computedAt are content —
|
|
18
|
+
* a re-evaluation is a new observation of a relationship).
|
|
19
|
+
*
|
|
20
|
+
* COORDINATE ORDER is [lon, lat] (GeoJSON / EPSG:4326).
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.toHex = exports.canonicalJson = void 0;
|
|
24
|
+
exports.computeGeometryHash = computeGeometryHash;
|
|
25
|
+
exports.computeSpatialObjectId = computeSpatialObjectId;
|
|
26
|
+
exports.computeSpatialRelationId = computeSpatialRelationId;
|
|
27
|
+
exports.hashSpatialObject = hashSpatialObject;
|
|
28
|
+
exports.hashSpatialRelationClaim = hashSpatialRelationClaim;
|
|
29
|
+
const core_1 = require("@totemsdk/core");
|
|
30
|
+
const proof_1 = require("@totemsdk/proof");
|
|
31
|
+
Object.defineProperty(exports, "canonicalJson", { enumerable: true, get: function () { return proof_1.canonicalJson; } });
|
|
32
|
+
Object.defineProperty(exports, "toHex", { enumerable: true, get: function () { return proof_1.toHex; } });
|
|
33
|
+
const GEOMETRY_PREFIX = 'totem-geometry';
|
|
34
|
+
const SPATIAL_OBJECT_PREFIX = 'totem-spatial-object';
|
|
35
|
+
const SPATIAL_RELATION_PREFIX = 'totem-spatial-relation';
|
|
36
|
+
function stableSpatialObjectInput(input) {
|
|
37
|
+
const { spatialId: _spatialId, metadata: _metadata, ...stable } = input;
|
|
38
|
+
return stable;
|
|
39
|
+
}
|
|
40
|
+
function stableRelationInput(input) {
|
|
41
|
+
const { relationId: _relationId, metadata: _metadata, ...stable } = input;
|
|
42
|
+
return stable;
|
|
43
|
+
}
|
|
44
|
+
function hashStableGeometry(geometry) {
|
|
45
|
+
const digest = (0, core_1.sha3_256)(new TextEncoder().encode(GEOMETRY_PREFIX + (0, proof_1.canonicalJson)(geometry)));
|
|
46
|
+
return (0, proof_1.toHex)(digest);
|
|
47
|
+
}
|
|
48
|
+
function hashStableSpatialObject(input) {
|
|
49
|
+
const digest = (0, core_1.sha3_256)(new TextEncoder().encode(SPATIAL_OBJECT_PREFIX + (0, proof_1.canonicalJson)(stableSpatialObjectInput(input))));
|
|
50
|
+
return (0, proof_1.toHex)(digest);
|
|
51
|
+
}
|
|
52
|
+
function hashStableRelation(input) {
|
|
53
|
+
const digest = (0, core_1.sha3_256)(new TextEncoder().encode(SPATIAL_RELATION_PREFIX + (0, proof_1.canonicalJson)(stableRelationInput(input))));
|
|
54
|
+
return (0, proof_1.toHex)(digest);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute the stable geometry identifier: "totem:geo:<sha3-256-hex>".
|
|
58
|
+
* Deterministic over the exact geometry — the same geometry always hashes
|
|
59
|
+
* to the same identifier.
|
|
60
|
+
*/
|
|
61
|
+
function computeGeometryHash(geometry) {
|
|
62
|
+
return 'totem:geo:' + hashStableGeometry(geometry);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Compute a stable URI-style spatial object ID: "totem:spatial:<sha3-256-hex>".
|
|
66
|
+
* Callers pass the object minus spatialId; metadata is excluded internally.
|
|
67
|
+
*/
|
|
68
|
+
function computeSpatialObjectId(input) {
|
|
69
|
+
return 'totem:spatial:' + hashStableSpatialObject(input);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compute a stable URI-style spatial relation claim ID using the same
|
|
73
|
+
* "totem:spatial:" namespace as spatial objects. Relation and object hashes
|
|
74
|
+
* are distinguishable by their content-derived preimage, so the shared prefix
|
|
75
|
+
* cannot cause an ID collision for equivalent logical content.
|
|
76
|
+
*/
|
|
77
|
+
function computeSpatialRelationId(input) {
|
|
78
|
+
return 'totem:spatial:' + hashStableRelation(input);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Hash a complete SpatialObject (excluding spatialId and metadata)
|
|
82
|
+
* to lowercase SHA3-256 hex without a 0x prefix — the value used in
|
|
83
|
+
* EvidenceRef.hash.
|
|
84
|
+
*/
|
|
85
|
+
function hashSpatialObject(obj) {
|
|
86
|
+
const { spatialId: _spatialId, ...rest } = obj;
|
|
87
|
+
return hashStableSpatialObject(rest);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hash a complete SpatialRelationClaim (excluding relationId and metadata)
|
|
91
|
+
* to lowercase SHA3-256 hex without a 0x prefix — the value used in
|
|
92
|
+
* EvidenceRef.hash.
|
|
93
|
+
*/
|
|
94
|
+
function hashSpatialRelationClaim(claim) {
|
|
95
|
+
const { relationId: _relationId, ...rest } = claim;
|
|
96
|
+
return hashStableRelation(rest);
|
|
97
|
+
}
|
package/dist/claim.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spatial relation claim construction for @totemsdk/spatial-proof.
|
|
3
|
+
*
|
|
4
|
+
* Includes spatialRelationFromLocationClaim, which bridges an
|
|
5
|
+
* @totemsdk/location-proof LocationClaim into a spatial relation claim
|
|
6
|
+
* without any circular dependency (spatial-proof → location-proof only).
|
|
7
|
+
*/
|
|
8
|
+
import type { LocationClaim } from '@totemsdk/location-proof';
|
|
9
|
+
import type { SpatialObject, SpatialRelationClaim, SpatialRelationType } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Derive a spatial relation claim from a location claim.
|
|
12
|
+
*
|
|
13
|
+
* The location claim's [lon, lat] is used as the subject Point geometry and
|
|
14
|
+
* its claimId is recorded as the locationClaimId input, so the resulting
|
|
15
|
+
* spatial claim provably references the location claim it was computed from.
|
|
16
|
+
*/
|
|
17
|
+
export declare function spatialRelationFromLocationClaim(params: {
|
|
18
|
+
locationClaim: LocationClaim;
|
|
19
|
+
spatialObject: SpatialObject;
|
|
20
|
+
relation: SpatialRelationType;
|
|
21
|
+
computedAt?: number;
|
|
22
|
+
maxDistanceM?: number;
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
}): SpatialRelationClaim;
|
|
25
|
+
/**
|
|
26
|
+
* Validate the structure of a spatial relation claim (does not re-evaluate the
|
|
27
|
+
* geometry — verification of the proof envelope also checks the claim ID and
|
|
28
|
+
* evidence hashes).
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateSpatialRelationClaim(claim: SpatialRelationClaim): {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
errors: string[];
|
|
33
|
+
warnings: string[];
|
|
34
|
+
};
|
package/dist/claim.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Spatial relation claim construction for @totemsdk/spatial-proof.
|
|
4
|
+
*
|
|
5
|
+
* Includes spatialRelationFromLocationClaim, which bridges an
|
|
6
|
+
* @totemsdk/location-proof LocationClaim into a spatial relation claim
|
|
7
|
+
* without any circular dependency (spatial-proof → location-proof only).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.spatialRelationFromLocationClaim = spatialRelationFromLocationClaim;
|
|
11
|
+
exports.validateSpatialRelationClaim = validateSpatialRelationClaim;
|
|
12
|
+
const relations_js_1 = require("./relations.js");
|
|
13
|
+
/**
|
|
14
|
+
* Derive a spatial relation claim from a location claim.
|
|
15
|
+
*
|
|
16
|
+
* The location claim's [lon, lat] is used as the subject Point geometry and
|
|
17
|
+
* its claimId is recorded as the locationClaimId input, so the resulting
|
|
18
|
+
* spatial claim provably references the location claim it was computed from.
|
|
19
|
+
*/
|
|
20
|
+
function spatialRelationFromLocationClaim(params) {
|
|
21
|
+
const { locationClaim, spatialObject, relation } = params;
|
|
22
|
+
const location = locationClaim.location;
|
|
23
|
+
const evalParams = {
|
|
24
|
+
subjectId: locationClaim.subjectId,
|
|
25
|
+
subjectKind: 'location-claim',
|
|
26
|
+
spatialObject,
|
|
27
|
+
relation,
|
|
28
|
+
subjectGeometry: {
|
|
29
|
+
type: 'Point',
|
|
30
|
+
coordinates: [location.lon, location.lat],
|
|
31
|
+
},
|
|
32
|
+
...(params.computedAt !== undefined ? { computedAt: params.computedAt } : {}),
|
|
33
|
+
...(params.maxDistanceM !== undefined ? { maxDistanceM: params.maxDistanceM } : {}),
|
|
34
|
+
...(params.metadata !== undefined ? { metadata: params.metadata } : {}),
|
|
35
|
+
locationClaimId: locationClaim.claimId,
|
|
36
|
+
};
|
|
37
|
+
return (0, relations_js_1.evaluateSpatialRelation)(evalParams);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate the structure of a spatial relation claim (does not re-evaluate the
|
|
41
|
+
* geometry — verification of the proof envelope also checks the claim ID and
|
|
42
|
+
* evidence hashes).
|
|
43
|
+
*/
|
|
44
|
+
function validateSpatialRelationClaim(claim) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
const warnings = [];
|
|
47
|
+
if (typeof claim.relationId !== 'string' || claim.relationId.length === 0) {
|
|
48
|
+
errors.push('relationId is required');
|
|
49
|
+
}
|
|
50
|
+
if (typeof claim.subjectId !== 'string' || claim.subjectId.length === 0) {
|
|
51
|
+
errors.push('subjectId is required');
|
|
52
|
+
}
|
|
53
|
+
if (typeof claim.spatialObjectId !== 'string' || claim.spatialObjectId.length === 0) {
|
|
54
|
+
errors.push('spatialObjectId is required');
|
|
55
|
+
}
|
|
56
|
+
if (!claim.relation || typeof claim.relation !== 'string') {
|
|
57
|
+
errors.push('relation is required');
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isFinite(claim.computedAt) || claim.computedAt <= 0) {
|
|
60
|
+
errors.push('computedAt must be finite and positive');
|
|
61
|
+
}
|
|
62
|
+
if (!claim.engine || typeof claim.engine.name !== 'string' || typeof claim.engine.algorithm !== 'string') {
|
|
63
|
+
errors.push('engine (name + algorithm) is required');
|
|
64
|
+
}
|
|
65
|
+
if (!claim.inputs || typeof claim.inputs.spatialGeometryHash !== 'string') {
|
|
66
|
+
errors.push('inputs.spatialGeometryHash is required');
|
|
67
|
+
}
|
|
68
|
+
if (!claim.result || typeof claim.result.matched !== 'boolean') {
|
|
69
|
+
errors.push('result.matched is required');
|
|
70
|
+
}
|
|
71
|
+
if (claim.result.distanceM !== undefined && !Number.isFinite(claim.result.distanceM)) {
|
|
72
|
+
errors.push('result.distanceM must be finite');
|
|
73
|
+
}
|
|
74
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
75
|
+
}
|