@nextera.one/tps-standard 0.5.0 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextera.one/tps-standard",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "The Universal Protocol for Space-Time Coordinates. A standard URI scheme (tps://) combining WGS84 spatial data with hierarchical, multi-calendar temporal coordinates. Includes TPS-UID: time-first, reversible event identifiers.",
5
5
  "keywords": [
6
6
  "tps",
@@ -24,7 +24,7 @@
24
24
  "type": "git",
25
25
  "url": "git+https://github.com/nextera-one/tps.git"
26
26
  },
27
- "license": "MIT",
27
+ "license": "Apache-2.0",
28
28
  "author": "Mohammed Ayesh",
29
29
  "types": "dist/index.d.ts",
30
30
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -2,9 +2,15 @@
2
2
  * TPS: Temporal Positioning System
3
3
  * The Universal Protocol for Space-Time Coordinates.
4
4
  * @packageDocumentation
5
- * @version 0.4.2
6
- * @license MIT
5
+ * @version 0.5.0
6
+ * @license Apache-2.0
7
7
  * @copyright 2026 TPS Standards Working Group
8
+ *
9
+ * v0.5.0 Changes:
10
+ * - Added Actor anchor (A:) for provenance tracking
11
+ * - Added Signature (!) for cryptographic verification
12
+ * - Added structural anchors (bldg, floor, room, zone)
13
+ * - Added geospatial cell systems (S2, H3, Plus Code, what3words)
8
14
  */
9
15
 
10
16
  export type CalendarCode = 'greg' | 'hij' | 'jul' | 'holo' | 'unix';
@@ -22,11 +28,32 @@ export interface TPSComponents {
22
28
  second?: number;
23
29
  unixSeconds?: number;
24
30
 
25
- // --- SPATIAL ---
31
+ // --- SPATIAL: GPS Coordinates ---
26
32
  latitude?: number;
27
33
  longitude?: number;
28
34
  altitude?: number;
29
35
 
36
+ // --- SPATIAL: Geospatial Cells ---
37
+ /** Google S2 cell ID (hierarchical, prefix-searchable) */
38
+ s2Cell?: string;
39
+ /** Uber H3 cell ID (hexagonal grid) */
40
+ h3Cell?: string;
41
+ /** Open Location Code / Plus Code */
42
+ plusCode?: string;
43
+ /** what3words address (e.g. "filled.count.soap") */
44
+ what3words?: string;
45
+
46
+ // --- SPATIAL: Structural Anchors ---
47
+ /** Physical building identifier */
48
+ building?: string;
49
+ /** Vertical division (level) */
50
+ floor?: string;
51
+ /** Enclosed space identifier */
52
+ room?: string;
53
+ /** Logical area within building */
54
+ zone?: string;
55
+
56
+ // --- SPATIAL: Privacy Markers ---
30
57
  /** Technical missing data (e.g. server log without GPS) */
31
58
  isUnknownLocation?: boolean;
32
59
  /** Removed for legal/security reasons (e.g. GDPR) */
@@ -34,6 +61,12 @@ export interface TPSComponents {
34
61
  /** Masked by user preference (e.g. "Don't show my location") */
35
62
  isHiddenLocation?: boolean;
36
63
 
64
+ // --- PROVENANCE ---
65
+ /** Actor anchor - identifies observer/witness (e.g. "did:web:sensor.example.com", "node:gateway-01") */
66
+ actor?: string;
67
+ /** Verification hash appended to time (e.g. "sha256:8f3e2a...") */
68
+ signature?: string;
69
+
37
70
  // --- CONTEXT ---
38
71
  extensions?: Record<string, string>;
39
72
  }
@@ -229,12 +262,40 @@ export class TPS {
229
262
  }
230
263
 
231
264
  // --- REGEX ---
265
+ // Updated for v0.5.0: supports L: anchors, A: actor, ! signature, structural & geospatial anchors
266
+ // Note: Complex regex - carefully balanced parentheses
232
267
  private static readonly REGEX_URI = new RegExp(
233
- '^tps://(?<space>unknown|redacted|hidden|(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?)@T:(?<calendar>[a-z]{3,4})\\.(?:(?<unix>s\\d+(?:\\.\\d+)?)|m(?<millennium>-?\\d+)(?:\\.c(?<century>\\d+)(?:\\.y(?<year>\\d+)(?:\\.M(?<month>\\d{1,2})(?:\\.d(?<day>\\d{1,2})(?:\\.h(?<hour>\\d{1,2})(?:\\.n(?<minute>\\d{1,2})(?:\\.s(?<second>\\d{1,2}(?:\\.\\d+)?))?)?)?)?)?)?)?)?(?:;(?<extensions>[a-z0-9\\.\\-\\_]+))?$',
268
+ '^tps://' +
269
+ // Location part (L: prefix optional for backward compat)
270
+ '(?:L:)?(?<space>' +
271
+ '~|-|unknown|redacted|hidden|' + // Privacy markers
272
+ 's2=(?<s2>[a-fA-F0-9]+)|' + // S2 cell
273
+ 'h3=(?<h3>[a-fA-F0-9]+)|' + // H3 cell
274
+ 'plus=(?<plus>[A-Z0-9+]+)|' + // Plus Code
275
+ 'w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|' + // what3words
276
+ 'bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|' + // Structural
277
+ '(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?' + // GPS
278
+ ')' +
279
+ // Optional Actor anchor
280
+ '(?:/A:(?<actor>[^/@]+))?' +
281
+ // Time part separator
282
+ '[/@]T:(?<calendar>[a-z]{3,4})\\.' +
283
+ // Time components
284
+ '(?:(?<unix>s\\d+(?:\\.\\d+)?)|m(?<millennium>-?\\d+)(?:\\.c(?<century>\\d+)(?:\\.y(?<year>\\d+)(?:\\.M(?<month>\\d{1,2})(?:\\.d(?<day>\\d{1,2})(?:\\.h(?<hour>\\d{1,2})(?:\\.n(?<minute>\\d{1,2})(?:\\.s(?<second>\\d{1,2}(?:\\.\\d+)?))?)?)?)?)?)?)?)' +
285
+ // Optional signature
286
+ '(?:!(?<signature>[^;?#]+))?' +
287
+ // Optional extensions
288
+ '(?:;(?<extensions>[a-z0-9.\\-_=]+))?' +
289
+ // Optional query params
290
+ '(?:\\?(?<params>[^#]+))?' +
291
+ // Optional context
292
+ '(?:#(?<context>.+))?$',
234
293
  );
235
294
 
236
295
  private static readonly REGEX_TIME = new RegExp(
237
- '^T:(?<calendar>[a-z]{3,4})\\.(?:(?<unix>s\\d+(?:\\.\\d+)?)|m(?<millennium>-?\\d+)(?:\\.c(?<century>\\d+)(?:\\.y(?<year>\\d+)(?:\\.M(?<month>\\d{1,2})(?:\\.d(?<day>\\d{1,2})(?:\\.h(?<hour>\\d{1,2})(?:\\.n(?<minute>\\d{1,2})(?:\\.s(?<second>\\d{1,2}(?:\\.\\d+)?))?)?)?)?)?)?)?)?$',
296
+ '^T:(?<calendar>[a-z]{3,4})\\.' +
297
+ '(?:(?<unix>s\\d+(?:\\.\\d+)?)|m(?<millennium>-?\\d+)(?:\\.c(?<century>\\d+)(?:\\.y(?<year>\\d+)(?:\\.M(?<month>\\d{1,2})(?:\\.d(?<day>\\d{1,2})(?:\\.h(?<hour>\\d{1,2})(?:\\.n(?<minute>\\d{1,2})(?:\\.s(?<second>\\d{1,2}(?:\\.\\d+)?))?)?)?)?)?)?)?)' +
298
+ '(?:!(?<signature>[^;?#]+))?$',
238
299
  );
239
300
 
240
301
  // --- CORE METHODS ---
@@ -261,23 +322,42 @@ export class TPS {
261
322
  * @returns Full URI string (e.g. "tps://...").
262
323
  */
263
324
  static toURI(comp: TPSComponents): string {
264
- // 1. Build Space Part
265
- let spacePart = 'unknown'; // Default safe fallback
325
+ // 1. Build Space Part (L: anchor)
326
+ let spacePart = 'L:-'; // Default: unknown
266
327
 
267
328
  if (comp.isHiddenLocation) {
268
- spacePart = 'hidden';
329
+ spacePart = 'L:~';
269
330
  } else if (comp.isRedactedLocation) {
270
- spacePart = 'redacted';
331
+ spacePart = 'L:redacted';
271
332
  } else if (comp.isUnknownLocation) {
272
- spacePart = 'unknown';
333
+ spacePart = 'L:-';
334
+ } else if (comp.s2Cell) {
335
+ spacePart = `L:s2=${comp.s2Cell}`;
336
+ } else if (comp.h3Cell) {
337
+ spacePart = `L:h3=${comp.h3Cell}`;
338
+ } else if (comp.plusCode) {
339
+ spacePart = `L:plus=${comp.plusCode}`;
340
+ } else if (comp.what3words) {
341
+ spacePart = `L:w3w=${comp.what3words}`;
342
+ } else if (comp.building) {
343
+ spacePart = `L:bldg=${comp.building}`;
344
+ if (comp.floor) spacePart += `.floor=${comp.floor}`;
345
+ if (comp.room) spacePart += `.room=${comp.room}`;
346
+ if (comp.zone) spacePart += `.zone=${comp.zone}`;
273
347
  } else if (comp.latitude !== undefined && comp.longitude !== undefined) {
274
- spacePart = `${comp.latitude},${comp.longitude}`;
348
+ spacePart = `L:${comp.latitude},${comp.longitude}`;
275
349
  if (comp.altitude !== undefined) {
276
350
  spacePart += `,${comp.altitude}m`;
277
351
  }
278
352
  }
279
353
 
280
- // 2. Build Time Part
354
+ // 2. Build Actor Part (A: anchor) - optional
355
+ let actorPart = '';
356
+ if (comp.actor) {
357
+ actorPart = `/A:${comp.actor}`;
358
+ }
359
+
360
+ // 3. Build Time Part
281
361
  let timePart = `T:${comp.calendar}`;
282
362
 
283
363
  if (comp.calendar === 'unix' && comp.unixSeconds !== undefined) {
@@ -293,16 +373,21 @@ export class TPS {
293
373
  if (comp.second !== undefined) timePart += `.s${this.pad(comp.second)}`;
294
374
  }
295
375
 
296
- // 3. Build Extensions
376
+ // 4. Add Signature (!) - optional
377
+ if (comp.signature) {
378
+ timePart += `!${comp.signature}`;
379
+ }
380
+
381
+ // 5. Build Extensions
297
382
  let extPart = '';
298
383
  if (comp.extensions && Object.keys(comp.extensions).length > 0) {
299
384
  const extStrings = Object.entries(comp.extensions).map(
300
- ([k, v]) => `${k}${v}`,
385
+ ([k, v]) => `${k}=${v}`,
301
386
  );
302
387
  extPart = `;${extStrings.join('.')}`;
303
388
  }
304
389
 
305
- return `tps://${spacePart}@${timePart}${extPart}`;
390
+ return `tps://${spacePart}${actorPart}/${timePart}${extPart}`;
306
391
  }
307
392
 
308
393
  /**
@@ -339,7 +424,9 @@ export class TPS {
339
424
  const n = date.getUTCMinutes();
340
425
  const s = date.getUTCSeconds();
341
426
 
342
- return `T:greg.m${m}.c${c}.y${y}.M${this.pad(M)}.d${this.pad(d)}.h${this.pad(h)}.n${this.pad(n)}.s${this.pad(s)}`;
427
+ return `T:greg.m${m}.c${c}.y${y}.M${this.pad(M)}.d${this.pad(
428
+ d,
429
+ )}.h${this.pad(h)}.n${this.pad(n)}.s${this.pad(s)}`;
343
430
  }
344
431
 
345
432
  throw new Error(
@@ -547,11 +634,44 @@ export class TPS {
547
634
  if (g.second) components.second = parseFloat(g.second);
548
635
  }
549
636
 
637
+ // Signature Mapping
638
+ if (g.signature) {
639
+ components.signature = g.signature;
640
+ }
641
+
642
+ // Actor Mapping
643
+ if (g.actor) {
644
+ components.actor = g.actor;
645
+ }
646
+
550
647
  // Space Mapping
551
648
  if (g.space) {
552
- if (g.space === 'unknown') components.isUnknownLocation = true;
553
- else if (g.space === 'redacted') components.isRedactedLocation = true;
554
- else if (g.space === 'hidden') components.isHiddenLocation = true;
649
+ // Privacy markers
650
+ if (g.space === 'unknown' || g.space === '-') {
651
+ components.isUnknownLocation = true;
652
+ } else if (g.space === 'redacted') {
653
+ components.isRedactedLocation = true;
654
+ } else if (g.space === 'hidden' || g.space === '~') {
655
+ components.isHiddenLocation = true;
656
+ }
657
+ // Geospatial cells
658
+ else if (g.s2) {
659
+ components.s2Cell = g.s2;
660
+ } else if (g.h3) {
661
+ components.h3Cell = g.h3;
662
+ } else if (g.plus) {
663
+ components.plusCode = g.plus;
664
+ } else if (g.w3w) {
665
+ components.what3words = g.w3w;
666
+ }
667
+ // Structural anchors
668
+ else if (g.bldg) {
669
+ components.building = g.bldg;
670
+ if (g.floor) components.floor = g.floor;
671
+ if (g.room) components.room = g.room;
672
+ if (g.zone) components.zone = g.zone;
673
+ }
674
+ // GPS coordinates
555
675
  else {
556
676
  if (g.lat) components.latitude = parseFloat(g.lat);
557
677
  if (g.lon) components.longitude = parseFloat(g.lon);
@@ -564,9 +684,17 @@ export class TPS {
564
684
  const extObj: any = {};
565
685
  const parts = g.extensions.split('.');
566
686
  parts.forEach((p: string) => {
567
- const key = p.charAt(0);
568
- const val = p.substring(1);
569
- if (key && val) extObj[key] = val;
687
+ const eqIdx = p.indexOf('=');
688
+ if (eqIdx > 0) {
689
+ const key = p.substring(0, eqIdx);
690
+ const val = p.substring(eqIdx + 1);
691
+ if (key && val) extObj[key] = val;
692
+ } else {
693
+ // Legacy format: first char is key
694
+ const key = p.charAt(0);
695
+ const val = p.substring(1);
696
+ if (key && val) extObj[key] = val;
697
+ }
570
698
  });
571
699
  components.extensions = extObj;
572
700
  }
@@ -871,7 +999,9 @@ export class TPSUID7RB {
871
999
 
872
1000
  const pad = (num: number) => num.toString().padStart(2, '0');
873
1001
 
874
- const timePart = `T:greg.m${m}.c${c}.y${y}.M${pad(M)}.d${pad(d)}.h${pad(h)}.n${pad(n)}.s${pad(s)}`;
1002
+ const timePart = `T:greg.m${m}.c${c}.y${y}.M${pad(M)}.d${pad(d)}.h${pad(
1003
+ h,
1004
+ )}.n${pad(n)}.s${pad(s)}`;
875
1005
 
876
1006
  let spacePart = 'unknown';
877
1007
  if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
@@ -1160,12 +1290,16 @@ export class TPSUID7RB {
1160
1290
  const content = new Uint8Array(contentLen);
1161
1291
  let offset = 0;
1162
1292
 
1163
- content.set(this.MAGIC, offset); offset += 4;
1293
+ content.set(this.MAGIC, offset);
1294
+ offset += 4;
1164
1295
  content[offset++] = this.VER;
1165
1296
  content[offset++] = flags;
1166
- content.set(this.writeU48(epochMs), offset); offset += 6;
1167
- content.set(nonceBuf, offset); offset += 4;
1168
- content.set(lenVar, offset); offset += lenVar.length;
1297
+ content.set(this.writeU48(epochMs), offset);
1298
+ offset += 6;
1299
+ content.set(nonceBuf, offset);
1300
+ offset += 4;
1301
+ content.set(lenVar, offset);
1302
+ offset += lenVar.length;
1169
1303
  content.set(payload, offset);
1170
1304
 
1171
1305
  // Sign the content
@@ -1215,7 +1349,10 @@ export class TPSUID7RB {
1215
1349
  // We need to parse LEN and Payload to find the split point
1216
1350
  let offset = 16; // Start of LEN
1217
1351
  // Decode LEN
1218
- const { value: tpsLen, bytesRead } = this.uvarintDecode(sealedBytes, offset);
1352
+ const { value: tpsLen, bytesRead } = this.uvarintDecode(
1353
+ sealedBytes,
1354
+ offset,
1355
+ );
1219
1356
  offset += bytesRead;
1220
1357
  const payloadEnd = offset + tpsLen;
1221
1358
 
@@ -1233,12 +1370,16 @@ export class TPSUID7RB {
1233
1370
 
1234
1371
  const sealType = sealedBytes[payloadEnd];
1235
1372
  if (sealType !== 0x01) {
1236
- throw new Error(`TPSUID7RB: unsupported seal type 0x${sealType.toString(16)}`);
1373
+ throw new Error(
1374
+ `TPSUID7RB: unsupported seal type 0x${sealType.toString(16)}`,
1375
+ );
1237
1376
  }
1238
1377
 
1239
1378
  const signature = sealedBytes.slice(payloadEnd + 1);
1240
1379
  if (signature.length !== 64) {
1241
- throw new Error(`TPSUID7RB: invalid Ed25519 signature length ${signature.length}`);
1380
+ throw new Error(
1381
+ `TPSUID7RB: invalid Ed25519 signature length ${signature.length}`,
1382
+ );
1242
1383
  }
1243
1384
 
1244
1385
  // Verify
@@ -1270,20 +1411,20 @@ export class TPSUID7RB {
1270
1411
  // or ensure key is properly formatted.
1271
1412
  // For simplicity in Node 20+, crypto.sign(null, data, privateKey) works if key is KeyObject.
1272
1413
  // If raw bytes: establish KeyObject.
1273
-
1414
+
1274
1415
  let keyObj;
1275
1416
  if (Buffer.isBuffer(privateKey) || privateKey instanceof Uint8Array) {
1276
- // Assuming raw 64-byte private key (or 32-byte seed properly expanded by crypto)
1277
- // Node < 16 is tricky with raw keys.
1278
- // Let's assume standard Ed25519 standard implementation pattern logic:
1279
- keyObj = crypto.createPrivateKey({
1280
- key: Buffer.from(privateKey),
1281
- format: 'der', // or 'pem' - strict.
1282
- type: 'pkcs8'
1283
- });
1284
- // Actually, simpler: construct key object from raw bytes if possible?
1285
- // Node's crypto is strict. Let's try the simplest:
1286
- // If hex string provided, convert to buffer.
1417
+ // Assuming raw 64-byte private key (or 32-byte seed properly expanded by crypto)
1418
+ // Node < 16 is tricky with raw keys.
1419
+ // Let's assume standard Ed25519 standard implementation pattern logic:
1420
+ keyObj = crypto.createPrivateKey({
1421
+ key: Buffer.from(privateKey),
1422
+ format: 'der', // or 'pem' - strict.
1423
+ type: 'pkcs8',
1424
+ });
1425
+ // Actually, simpler: construct key object from raw bytes if possible?
1426
+ // Node's crypto is strict. Let's try the simplest:
1427
+ // If hex string provided, convert to buffer.
1287
1428
  }
1288
1429
 
1289
1430
  // Simpler fallback: If user passed a PEM string, great.
@@ -1292,14 +1433,18 @@ export class TPSUID7RB {
1292
1433
  // and assume the user provides a VALID key object or compatible format (PEM/DER).
1293
1434
  // Handling RAW Ed25519 keys in Node requires specific 'crypto.createPrivateKey' with 'raw' format (Node 11.6+).
1294
1435
 
1295
- const key = typeof privateKey === 'string' && !privateKey.includes('PRIVATE KEY')
1296
- ? crypto.createPrivateKey({ key: Buffer.from(privateKey, 'hex'), format: 'pem', type: 'pkcs8' }) // Fallback guess
1297
- : privateKey;
1436
+ const key =
1437
+ typeof privateKey === 'string' && !privateKey.includes('PRIVATE KEY')
1438
+ ? crypto.createPrivateKey({
1439
+ key: Buffer.from(privateKey, 'hex'),
1440
+ format: 'pem',
1441
+ type: 'pkcs8',
1442
+ }) // Fallback guess
1443
+ : privateKey;
1298
1444
 
1299
1445
  // Note: Raw Ed25519 key support in Node.js 'crypto' acts via 'generateKeyPair' or KeyObject.
1300
1446
  // Direct raw signing is via crypto.sign(null, data, key).
1301
1447
  return new Uint8Array(crypto.sign(null, data, key));
1302
-
1303
1448
  } catch (e) {
1304
1449
  // If standard crypto fails (e.g. key format issue), throw
1305
1450
  throw new Error('TPSUID7RB: signing failed (check key format)');
@@ -1350,4 +1495,3 @@ export class TPSUID7RB {
1350
1495
  throw new Error('TPSUID7RB: no crypto available');
1351
1496
  }
1352
1497
  }
1353
-