gridstamp 1.0.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/.cursorrules +74 -0
- package/CLAUDE.md +61 -0
- package/LICENSE +190 -0
- package/README.md +107 -0
- package/dist/index.js +194 -0
- package/package.json +84 -0
- package/src/antispoofing/detector.ts +509 -0
- package/src/antispoofing/index.ts +7 -0
- package/src/gamification/badges.ts +429 -0
- package/src/gamification/fleet-leaderboard.ts +293 -0
- package/src/gamification/index.ts +44 -0
- package/src/gamification/streaks.ts +243 -0
- package/src/gamification/trust-tiers.ts +393 -0
- package/src/gamification/zone-mastery.ts +256 -0
- package/src/index.ts +341 -0
- package/src/memory/index.ts +9 -0
- package/src/memory/place-cells.ts +279 -0
- package/src/memory/spatial-memory.ts +375 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/pathfinding.ts +403 -0
- package/src/perception/camera.ts +249 -0
- package/src/perception/index.ts +2 -0
- package/src/types/index.ts +416 -0
- package/src/utils/crypto.ts +94 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/math.ts +204 -0
- package/src/verification/index.ts +9 -0
- package/src/verification/spatial-proof.ts +442 -0
- package/tests/antispoofing/detector.test.ts +196 -0
- package/tests/gamification/badges.test.ts +163 -0
- package/tests/gamification/fleet-leaderboard.test.ts +181 -0
- package/tests/gamification/streaks.test.ts +158 -0
- package/tests/gamification/trust-tiers.test.ts +165 -0
- package/tests/gamification/zone-mastery.test.ts +143 -0
- package/tests/memory/place-cells.test.ts +128 -0
- package/tests/stress/load.test.ts +499 -0
- package/tests/stress/security.test.ts +378 -0
- package/tests/stress/simulation.test.ts +361 -0
- package/tests/utils/crypto.test.ts +115 -0
- package/tests/utils/math.test.ts +195 -0
- package/tests/verification/spatial-proof.test.ts +299 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
computeSSIM,
|
|
4
|
+
rgbToGrayscale,
|
|
5
|
+
approximateLPIPS,
|
|
6
|
+
computeSpatialMetrics,
|
|
7
|
+
generateSpatialProof,
|
|
8
|
+
verifySpatialProofIntegrity,
|
|
9
|
+
createSettlement,
|
|
10
|
+
} from '../../src/verification/spatial-proof.js';
|
|
11
|
+
import { signFrame, generateNonce } from '../../src/utils/crypto.js';
|
|
12
|
+
import type { CameraFrame, RenderedView, Pose, VerificationThresholds } from '../../src/types/index.js';
|
|
13
|
+
// RenderedView also used in settlement test
|
|
14
|
+
|
|
15
|
+
const TEST_SECRET = 'a'.repeat(32);
|
|
16
|
+
const TEST_POSE: Pose = {
|
|
17
|
+
position: { x: 5, y: 10, z: 0 },
|
|
18
|
+
orientation: { w: 1, x: 0, y: 0, z: 0 },
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function makeTestImage(width: number, height: number, seed: number = 42): Uint8Array {
|
|
23
|
+
const rgb = new Uint8Array(width * height * 3);
|
|
24
|
+
for (let i = 0; i < rgb.length; i++) {
|
|
25
|
+
rgb[i] = (i * 17 + seed * 31) % 256;
|
|
26
|
+
}
|
|
27
|
+
return rgb;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeFrame(width: number, height: number, seed: number = 42): CameraFrame {
|
|
31
|
+
const rgb = makeTestImage(width, height, seed);
|
|
32
|
+
const depth = new Float32Array(width * height);
|
|
33
|
+
for (let i = 0; i < depth.length; i++) {
|
|
34
|
+
depth[i] = 1 + Math.random() * 4;
|
|
35
|
+
}
|
|
36
|
+
const timestamp = Date.now();
|
|
37
|
+
const seq = 1;
|
|
38
|
+
return {
|
|
39
|
+
id: generateNonce(8),
|
|
40
|
+
timestamp,
|
|
41
|
+
rgb,
|
|
42
|
+
width,
|
|
43
|
+
height,
|
|
44
|
+
depth,
|
|
45
|
+
pose: TEST_POSE,
|
|
46
|
+
hmac: signFrame(rgb, timestamp, seq, TEST_SECRET),
|
|
47
|
+
sequenceNumber: seq,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeRender(width: number, height: number, seed: number = 42): RenderedView {
|
|
52
|
+
const rgb = makeTestImage(width, height, seed);
|
|
53
|
+
const depth = new Float32Array(width * height);
|
|
54
|
+
for (let i = 0; i < depth.length; i++) {
|
|
55
|
+
depth[i] = 1 + Math.random() * 4;
|
|
56
|
+
}
|
|
57
|
+
return { rgb, depth, width, height, pose: TEST_POSE, renderTimeMs: 5 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('SSIM', () => {
|
|
61
|
+
it('returns 1.0 for identical images', () => {
|
|
62
|
+
const img = new Uint8Array(64 * 64);
|
|
63
|
+
for (let i = 0; i < img.length; i++) img[i] = (i * 7) % 256;
|
|
64
|
+
const ssim = computeSSIM(img, img, 64, 64);
|
|
65
|
+
expect(ssim).toBeCloseTo(1.0, 2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns low SSIM for different images', () => {
|
|
69
|
+
const a = new Uint8Array(64 * 64);
|
|
70
|
+
const b = new Uint8Array(64 * 64);
|
|
71
|
+
for (let i = 0; i < a.length; i++) {
|
|
72
|
+
a[i] = (i * 7) % 256;
|
|
73
|
+
b[i] = (i * 13 + 128) % 256;
|
|
74
|
+
}
|
|
75
|
+
const ssim = computeSSIM(a, b, 64, 64);
|
|
76
|
+
expect(ssim).toBeLessThan(0.5);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws on dimension mismatch', () => {
|
|
80
|
+
expect(() => computeSSIM(new Uint8Array(100), new Uint8Array(200), 10, 10)).toThrow('dimensions');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('RGB to Grayscale', () => {
|
|
85
|
+
it('converts correctly', () => {
|
|
86
|
+
// Pure red pixel
|
|
87
|
+
const rgb = new Uint8Array([255, 0, 0]);
|
|
88
|
+
const gray = rgbToGrayscale(rgb, 1, 1);
|
|
89
|
+
expect(gray[0]).toBe(Math.round(0.299 * 255)); // ~76
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('white pixel becomes 255', () => {
|
|
93
|
+
const rgb = new Uint8Array([255, 255, 255]);
|
|
94
|
+
const gray = rgbToGrayscale(rgb, 1, 1);
|
|
95
|
+
expect(gray[0]).toBe(255);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('LPIPS Approximation', () => {
|
|
100
|
+
it('returns 0 for identical images', () => {
|
|
101
|
+
const img = new Uint8Array(64 * 64);
|
|
102
|
+
for (let i = 0; i < img.length; i++) img[i] = (i * 7) % 256;
|
|
103
|
+
const lpips = approximateLPIPS(img, img, 64, 64);
|
|
104
|
+
expect(lpips).toBeCloseTo(0, 1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns positive value for different images', () => {
|
|
108
|
+
const a = new Uint8Array(64 * 64);
|
|
109
|
+
const b = new Uint8Array(64 * 64);
|
|
110
|
+
for (let i = 0; i < a.length; i++) {
|
|
111
|
+
a[i] = (i * 7) % 256;
|
|
112
|
+
b[i] = (i * 13 + 128) % 256;
|
|
113
|
+
}
|
|
114
|
+
const lpips = approximateLPIPS(a, b, 64, 64);
|
|
115
|
+
expect(lpips).toBeGreaterThan(0);
|
|
116
|
+
expect(lpips).toBeLessThanOrEqual(1);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('Spatial Metrics', () => {
|
|
121
|
+
it('computes metrics for matching views', () => {
|
|
122
|
+
const frame = makeFrame(64, 64, 42);
|
|
123
|
+
const render = makeRender(64, 64, 42);
|
|
124
|
+
const thresholds: VerificationThresholds = {
|
|
125
|
+
minSSIM: 0.75,
|
|
126
|
+
maxLPIPS: 0.25,
|
|
127
|
+
maxDepthMAE: 0.5,
|
|
128
|
+
minComposite: 0.75,
|
|
129
|
+
};
|
|
130
|
+
const metrics = computeSpatialMetrics(render, frame, thresholds);
|
|
131
|
+
expect(metrics.ssim).toBeDefined();
|
|
132
|
+
expect(metrics.lpips).toBeDefined();
|
|
133
|
+
expect(metrics.depthMAE).toBeDefined();
|
|
134
|
+
expect(metrics.composite).toBeDefined();
|
|
135
|
+
expect(metrics.composite).toBeGreaterThanOrEqual(0);
|
|
136
|
+
expect(metrics.composite).toBeLessThanOrEqual(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws on dimension mismatch', () => {
|
|
140
|
+
const frame = makeFrame(64, 64);
|
|
141
|
+
const render = makeRender(32, 32);
|
|
142
|
+
expect(() => computeSpatialMetrics(render, frame, {
|
|
143
|
+
minSSIM: 0.75, maxLPIPS: 0.25, maxDepthMAE: 0.5, minComposite: 0.75,
|
|
144
|
+
})).toThrow('mismatch');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Spatial Proof', () => {
|
|
149
|
+
it('generates a valid proof', () => {
|
|
150
|
+
const frame = makeFrame(64, 64);
|
|
151
|
+
const render = makeRender(64, 64);
|
|
152
|
+
const proof = generateSpatialProof(
|
|
153
|
+
'robot-001',
|
|
154
|
+
TEST_POSE,
|
|
155
|
+
frame,
|
|
156
|
+
render,
|
|
157
|
+
'merkle-root-abc',
|
|
158
|
+
['proof1', 'proof2'],
|
|
159
|
+
TEST_SECRET,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(proof.id).toBeTruthy();
|
|
163
|
+
expect(proof.robotId).toBe('robot-001');
|
|
164
|
+
expect(proof.signature).toHaveLength(64);
|
|
165
|
+
expect(proof.nonce).toHaveLength(64);
|
|
166
|
+
expect(proof.metrics).toBeDefined();
|
|
167
|
+
expect(typeof proof.passed).toBe('boolean');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('proof integrity check passes for valid proof', () => {
|
|
171
|
+
const frame = makeFrame(64, 64);
|
|
172
|
+
const render = makeRender(64, 64);
|
|
173
|
+
const proof = generateSpatialProof(
|
|
174
|
+
'robot-001', TEST_POSE, frame, render,
|
|
175
|
+
'merkle-root', [], TEST_SECRET,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const result = verifySpatialProofIntegrity(proof, TEST_SECRET);
|
|
179
|
+
expect(result.valid).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('proof integrity fails with wrong secret', () => {
|
|
183
|
+
const frame = makeFrame(64, 64);
|
|
184
|
+
const render = makeRender(64, 64);
|
|
185
|
+
const proof = generateSpatialProof(
|
|
186
|
+
'robot-001', TEST_POSE, frame, render,
|
|
187
|
+
'merkle-root', [], TEST_SECRET,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const result = verifySpatialProofIntegrity(proof, 'b'.repeat(32));
|
|
191
|
+
expect(result.valid).toBe(false);
|
|
192
|
+
expect(result.reason).toContain('signature');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('proof integrity fails if expired', () => {
|
|
196
|
+
const frame = makeFrame(64, 64);
|
|
197
|
+
const render = makeRender(64, 64);
|
|
198
|
+
const proof = generateSpatialProof(
|
|
199
|
+
'robot-001', TEST_POSE, frame, render,
|
|
200
|
+
'merkle-root', [], TEST_SECRET,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Fake an old proof by mutating timestamp (proof is already signed so sig will fail too)
|
|
204
|
+
const oldProof = { ...proof, timestamp: Date.now() - 600_000 }; // 10 min old
|
|
205
|
+
const result = verifySpatialProofIntegrity(oldProof, TEST_SECRET, 300_000); // 5 min max
|
|
206
|
+
expect(result.valid).toBe(false);
|
|
207
|
+
// Either expired or signature mismatch (since we changed timestamp)
|
|
208
|
+
expect(result.reason).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('rejects unsigned frames', () => {
|
|
212
|
+
const frame = makeFrame(64, 64);
|
|
213
|
+
const unsignedFrame = { ...frame, hmac: undefined };
|
|
214
|
+
const render = makeRender(64, 64);
|
|
215
|
+
expect(() => generateSpatialProof(
|
|
216
|
+
'robot-001', TEST_POSE, unsignedFrame, render,
|
|
217
|
+
'merkle-root', [], TEST_SECRET,
|
|
218
|
+
)).toThrow('HMAC-signed');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('rejects empty robotId', () => {
|
|
222
|
+
const frame = makeFrame(64, 64);
|
|
223
|
+
const render = makeRender(64, 64);
|
|
224
|
+
expect(() => generateSpatialProof(
|
|
225
|
+
'', TEST_POSE, frame, render,
|
|
226
|
+
'merkle-root', [], TEST_SECRET,
|
|
227
|
+
)).toThrow('robotId');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rejects short HMAC secret', () => {
|
|
231
|
+
const frame = makeFrame(64, 64);
|
|
232
|
+
const render = makeRender(64, 64);
|
|
233
|
+
expect(() => generateSpatialProof(
|
|
234
|
+
'robot-001', TEST_POSE, frame, render,
|
|
235
|
+
'merkle-root', [], 'short',
|
|
236
|
+
)).toThrow('32 chars');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Settlement', () => {
|
|
241
|
+
it('creates verified settlement for passing proof', () => {
|
|
242
|
+
const frame = makeFrame(64, 64);
|
|
243
|
+
const render = makeRender(64, 64);
|
|
244
|
+
const proof = generateSpatialProof(
|
|
245
|
+
'robot-001', TEST_POSE, frame, render,
|
|
246
|
+
'merkle-root', [], TEST_SECRET,
|
|
247
|
+
);
|
|
248
|
+
// Generate a proof where same image is used for both expected and actual (will pass SSIM=1.0)
|
|
249
|
+
const identicalRender: RenderedView = {
|
|
250
|
+
rgb: frame.rgb,
|
|
251
|
+
depth: frame.depth!,
|
|
252
|
+
width: frame.width,
|
|
253
|
+
height: frame.height,
|
|
254
|
+
pose: TEST_POSE,
|
|
255
|
+
renderTimeMs: 1,
|
|
256
|
+
};
|
|
257
|
+
const passingProof = generateSpatialProof(
|
|
258
|
+
'robot-001', TEST_POSE, frame, identicalRender,
|
|
259
|
+
'merkle-root', [], TEST_SECRET,
|
|
260
|
+
);
|
|
261
|
+
expect(passingProof.passed).toBe(true);
|
|
262
|
+
const settlement = createSettlement(passingProof, 10.00, 'USD', 'merchant-001', TEST_SECRET);
|
|
263
|
+
expect(settlement.amount).toBe(10.00);
|
|
264
|
+
expect(settlement.currency).toBe('USD');
|
|
265
|
+
expect(settlement.payeeId).toBe('merchant-001');
|
|
266
|
+
expect(settlement.status).toBe('verified');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('fails settlement for failing proof', () => {
|
|
270
|
+
const frame = makeFrame(64, 64);
|
|
271
|
+
const render = makeRender(64, 64);
|
|
272
|
+
const proof = generateSpatialProof(
|
|
273
|
+
'robot-001', TEST_POSE, frame, render,
|
|
274
|
+
'merkle-root', [], TEST_SECRET,
|
|
275
|
+
);
|
|
276
|
+
const failedProof = { ...proof, passed: false };
|
|
277
|
+
const settlement = createSettlement(failedProof, 10.00, 'USD', 'merchant-001', TEST_SECRET);
|
|
278
|
+
expect(settlement.status).toBe('failed');
|
|
279
|
+
expect(settlement.failureReason).toContain('Spatial verification failed');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('rejects zero amount', () => {
|
|
283
|
+
const frame = makeFrame(64, 64);
|
|
284
|
+
const render = makeRender(64, 64);
|
|
285
|
+
const proof = generateSpatialProof(
|
|
286
|
+
'robot-001', TEST_POSE, frame, render, 'mr', [], TEST_SECRET,
|
|
287
|
+
);
|
|
288
|
+
expect(() => createSettlement(proof, 0, 'USD', 'merchant', TEST_SECRET)).toThrow('positive');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('rejects negative amount', () => {
|
|
292
|
+
const frame = makeFrame(64, 64);
|
|
293
|
+
const render = makeRender(64, 64);
|
|
294
|
+
const proof = generateSpatialProof(
|
|
295
|
+
'robot-001', TEST_POSE, frame, render, 'mr', [], TEST_SECRET,
|
|
296
|
+
);
|
|
297
|
+
expect(() => createSettlement(proof, -5, 'USD', 'merchant', TEST_SECRET)).toThrow('positive');
|
|
298
|
+
});
|
|
299
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"noUnusedLocals": false,
|
|
18
|
+
"noUnusedParameters": false,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"exactOptionalPropertyTypes": false,
|
|
22
|
+
"noUncheckedIndexedAccess": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
26
|
+
}
|