@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 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
+ }
@@ -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
+ }