@forestrie/merklelog 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { Massif } from "./massifs/massif.js";
11
11
  export type { MassifStart, LeafComponent, LeafEnumeratorSpec, LeafEntry, } from "./massifs/types.js";
12
12
  export { MassifStartFmt } from "./massifs/types.js";
13
13
  export { LogFormat, Urkle, Bloom, IndexV2, leafCountForMassifHeight, leafTableBytes, nodeCountMax, nodeStoreBytes, bloomMBits, bloomBitsetBytes, bloomRegionBytes, indexDataBytesV2, peakStackEnd, massifLogEntries, urkleLeafTableStartFieldIndex, urkleLeafTableStartByteOffset, createLeafEnumerator, leafComponentByteOffset, leafComponentSize, computeLastMMRIndex, isMassifFull, } from "./massifs/types.js";
14
- export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
14
+ export { formatMassifObjectIndex, MASSIF_OBJECT_INDEX_DIGITS, MAX_MASSIF_INDEX, parseV2StorageObjectPath, v2CheckpointObjectKey, v2MassifObjectKey, } from "./massifs/v2storagepaths.js";
15
15
  export { openMassifNodeStore } from "./massifs/nodestore.js";
16
16
  export type { MassifNodeStore } from "./massifs/nodestore.js";
17
17
  export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,OAAO,EACP,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EACL,WAAW,EACX,MAAM,EACN,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,EAC5B,6BAA6B,EAC7B,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,OAAO,EACP,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EACL,WAAW,EACX,MAAM,EACN,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,EAC5B,6BAA6B,EAC7B,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { Uint64 } from "./uint64/index.js";
12
12
  export { Massif } from "./massifs/massif.js";
13
13
  export { MassifStartFmt } from "./massifs/types.js";
14
14
  export { LogFormat, Urkle, Bloom, IndexV2, leafCountForMassifHeight, leafTableBytes, nodeCountMax, nodeStoreBytes, bloomMBits, bloomBitsetBytes, bloomRegionBytes, indexDataBytesV2, peakStackEnd, massifLogEntries, urkleLeafTableStartFieldIndex, urkleLeafTableStartByteOffset, createLeafEnumerator, leafComponentByteOffset, leafComponentSize, computeLastMMRIndex, isMassifFull, } from "./massifs/types.js";
15
- export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
15
+ export { formatMassifObjectIndex, MASSIF_OBJECT_INDEX_DIGITS, MAX_MASSIF_INDEX, parseV2StorageObjectPath, v2CheckpointObjectKey, v2MassifObjectKey, } from "./massifs/v2storagepaths.js";
16
16
  // Massif blob node access for MMR proof building (plan-2607-15 §4)
17
17
  export { openMassifNodeStore } from "./massifs/nodestore.js";
18
18
  // Content-hash leaf lookup over the massif index region (FOR-373)
@@ -1,17 +1,57 @@
1
1
  /**
2
- * Parse an R2 object path to extract log ID, massif height, and massif index.
2
+ * Merklelog v2 R2 object layout:
3
3
  *
4
4
  * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
5
  * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
6
  *
7
- * @param path - The object path path from the R2 notification
7
+ * `{massifIndex}` is a zero-padded, 16-digit DECIMAL integer. The Go writer
8
+ * owns this convention: go-merklelog
9
+ * [massifs/storage/const.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/const.go)
10
+ * (`V1MMRBlobNameFmt = "%016d.log"`, `V1MMRSignedTreeHeadBlobNameFmt =
11
+ * "%016d.sth"`) and its reader
12
+ * [objectindexfrompath.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/objectindexfrompath.go)
13
+ * (`strconv.Atoi`). canopy-api builds its R2 keys through the functions
14
+ * here (FOR-621: an earlier reader parsed hex, which agrees with the writer
15
+ * only for indices 0-9). Code outside this package that spells a key by hand
16
+ * (delegation-coordinator `seal-hint.ts`, test fixtures, demos) uses only
17
+ * index 0, which reads the same in any radix; anything that needs another
18
+ * index must use these helpers.
19
+ */
20
+ /** Width of the massif index segment in an object key. */
21
+ export declare const MASSIF_OBJECT_INDEX_DIGITS = 16;
22
+ /**
23
+ * Largest massif index the Go side can represent. go-merklelog
24
+ * `FmtMassifPath(prefix, massifIndex uint32)` and arbor's publisher
25
+ * (`strconv.ParseUint(base, 10, 32)`) use `uint32`; a larger index would be
26
+ * silently truncated by `ObjectIndexFromPath`, so it is rejected here.
27
+ */
28
+ export declare const MAX_MASSIF_INDEX = 4294967295n;
29
+ /**
30
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
31
+ *
32
+ * @param path - The object path from the R2 notification
8
33
  * @returns Parsed components including logId, massifHeight, and massifIndex
9
34
  * @throws Error if the path doesn't match expected format or if parsing fails
10
35
  */
11
36
  export declare function parseV2StorageObjectPath(path: string): {
12
37
  logId: string;
13
38
  massifHeight: number;
14
- massifIndex: number;
39
+ massifIndex: bigint;
15
40
  type: string;
16
41
  };
42
+ /**
43
+ * Build the massif `.log` object key for a log.
44
+ */
45
+ export declare function v2MassifObjectKey(massifHeight: number, logId: string, massifIndex: bigint | number): string;
46
+ /**
47
+ * Build the signed checkpoint `.sth` object key for a log.
48
+ */
49
+ export declare function v2CheckpointObjectKey(massifHeight: number, logId: string, massifIndex: bigint | number): string;
50
+ /**
51
+ * Format a massif index as the 16-digit decimal key segment (Go `%016d`).
52
+ *
53
+ * @throws Error if the index is negative, not an integer, not a safe integer
54
+ * (number input), or exceeds {@link MAX_MASSIF_INDEX}.
55
+ */
56
+ export declare function formatMassifObjectIndex(massifIndex: bigint | number): string;
17
57
  //# sourceMappingURL=v2storagepaths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v2storagepaths.d.ts","sourceRoot":"","sources":["../../src/massifs/v2storagepaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM;;;;;EAkDpD"}
1
+ {"version":3,"file":"v2storagepaths.d.ts","sourceRoot":"","sources":["../../src/massifs/v2storagepaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAiB5C;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM;;;;;EAyDpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,MAAM,GAC3B,MAAM,CAGR;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,MAAM,GAC3B,MAAM,CAGR;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAW5E"}
@@ -1,42 +1,84 @@
1
1
  /**
2
- * Parse an R2 object path to extract log ID, massif height, and massif index.
2
+ * Merklelog v2 R2 object layout:
3
3
  *
4
4
  * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
5
  * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
6
  *
7
- * @param path - The object path path from the R2 notification
7
+ * `{massifIndex}` is a zero-padded, 16-digit DECIMAL integer. The Go writer
8
+ * owns this convention: go-merklelog
9
+ * [massifs/storage/const.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/const.go)
10
+ * (`V1MMRBlobNameFmt = "%016d.log"`, `V1MMRSignedTreeHeadBlobNameFmt =
11
+ * "%016d.sth"`) and its reader
12
+ * [objectindexfrompath.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/objectindexfrompath.go)
13
+ * (`strconv.Atoi`). canopy-api builds its R2 keys through the functions
14
+ * here (FOR-621: an earlier reader parsed hex, which agrees with the writer
15
+ * only for indices 0-9). Code outside this package that spells a key by hand
16
+ * (delegation-coordinator `seal-hint.ts`, test fixtures, demos) uses only
17
+ * index 0, which reads the same in any radix; anything that needs another
18
+ * index must use these helpers.
19
+ */
20
+ /** Width of the massif index segment in an object key. */
21
+ export const MASSIF_OBJECT_INDEX_DIGITS = 16;
22
+ /**
23
+ * Largest massif index the Go side can represent. go-merklelog
24
+ * `FmtMassifPath(prefix, massifIndex uint32)` and arbor's publisher
25
+ * (`strconv.ParseUint(base, 10, 32)`) use `uint32`; a larger index would be
26
+ * silently truncated by `ObjectIndexFromPath`, so it is rejected here.
27
+ */
28
+ export const MAX_MASSIF_INDEX = 0xffffffffn;
29
+ /**
30
+ * Massif height bounds. Go writes height as `%d` of a `uint8` and arbor's
31
+ * publisher `ParseCheckpointKey` rejects 0, so valid heights are 1..255 with
32
+ * no leading zeros.
33
+ */
34
+ const MIN_MASSIF_HEIGHT = 1;
35
+ const MAX_MASSIF_HEIGHT = 255;
36
+ const MASSIF_OBJECT_INDEX_RE = /^[0-9]{16}$/;
37
+ const MASSIF_HEIGHT_RE = /^[1-9][0-9]{0,2}$/;
38
+ const OBJECT_EXTENSIONS = {
39
+ massifs: ".log",
40
+ checkpoints: ".sth",
41
+ };
42
+ /**
43
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
44
+ *
45
+ * @param path - The object path from the R2 notification
8
46
  * @returns Parsed components including logId, massifHeight, and massifIndex
9
47
  * @throws Error if the path doesn't match expected format or if parsing fails
10
48
  */
11
49
  export function parseV2StorageObjectPath(path) {
12
50
  const parts = path.split("/");
13
- if (parts.length < 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
51
+ if (parts.length !== 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
14
52
  throw new Error(`Unrecognized path format: ${path}`);
15
- // Check for new v2 format: v2/merklelog/massifs/{massifHeight}/{logId}/{index}.log
16
- // or v2/merklelog/checkpoints/{massifHeight}/{logId}/{index}.sth
17
- const typePart = parts[2]; // "massifs" or "checkpoints"
53
+ const typePart = parts[2];
18
54
  const massifHeightStr = parts[3];
19
55
  const logId = parts[4];
20
56
  const filename = parts[5];
21
- // Validate extension
22
- const expectedExt = typePart === "massifs" ? ".log" : ".sth";
57
+ const expectedExt = Object.hasOwn(OBJECT_EXTENSIONS, typePart)
58
+ ? OBJECT_EXTENSIONS[typePart]
59
+ : undefined;
60
+ if (expectedExt === undefined) {
61
+ throw new Error(`Object type must be massifs or checkpoints, got: ${typePart}`);
62
+ }
63
+ if (logId.length === 0) {
64
+ throw new Error(`Log id segment is empty: ${path}`);
65
+ }
23
66
  if (!filename.endsWith(expectedExt)) {
24
67
  throw new Error(`Expected ${expectedExt} extension for ${typePart}, got: ${filename}`);
25
68
  }
26
- // Parse massif index (hex, 16 digits)
27
69
  const massifIndexStr = filename.slice(0, -expectedExt.length);
28
- if (massifIndexStr.length !== 16) {
29
- throw new Error(`Massif index must be 16 hex digits, got ${massifIndexStr.length}: ${massifIndexStr}`);
70
+ if (!MASSIF_OBJECT_INDEX_RE.test(massifIndexStr)) {
71
+ throw new Error(`Massif index must be 16 decimal digits, got: ${massifIndexStr}`);
30
72
  }
31
- const massifIndex = Number.parseInt(massifIndexStr, 16);
32
- if (!Number.isFinite(massifIndex)) {
33
- throw new Error(`Failed to parse massif index as hex number: ${massifIndexStr}`);
73
+ const massifIndex = BigInt(massifIndexStr);
74
+ if (massifIndex > MAX_MASSIF_INDEX) {
75
+ throw new Error(`Massif index must be 0..${MAX_MASSIF_INDEX}, got ${massifIndex}`);
34
76
  }
35
- // Parse massifHeight
36
- const massifHeight = Number.parseInt(massifHeightStr, 10);
37
- if (!Number.isFinite(massifHeight)) {
77
+ if (!MASSIF_HEIGHT_RE.test(massifHeightStr) ||
78
+ Number(massifHeightStr) > MAX_MASSIF_HEIGHT) {
38
79
  throw new Error(`Failed to parse massif height as number: ${massifHeightStr}`);
39
80
  }
81
+ const massifHeight = Number(massifHeightStr);
40
82
  return {
41
83
  logId,
42
84
  massifHeight,
@@ -44,3 +86,48 @@ export function parseV2StorageObjectPath(path) {
44
86
  type: typePart,
45
87
  };
46
88
  }
89
+ /**
90
+ * Build the massif `.log` object key for a log.
91
+ */
92
+ export function v2MassifObjectKey(massifHeight, logId, massifIndex) {
93
+ assertKeyHeightAndLogId(massifHeight, logId);
94
+ return `v2/merklelog/massifs/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.log`;
95
+ }
96
+ /**
97
+ * Build the signed checkpoint `.sth` object key for a log.
98
+ */
99
+ export function v2CheckpointObjectKey(massifHeight, logId, massifIndex) {
100
+ assertKeyHeightAndLogId(massifHeight, logId);
101
+ return `v2/merklelog/checkpoints/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.sth`;
102
+ }
103
+ /**
104
+ * Format a massif index as the 16-digit decimal key segment (Go `%016d`).
105
+ *
106
+ * @throws Error if the index is negative, not an integer, not a safe integer
107
+ * (number input), or exceeds {@link MAX_MASSIF_INDEX}.
108
+ */
109
+ export function formatMassifObjectIndex(massifIndex) {
110
+ if (typeof massifIndex === "number" && !Number.isSafeInteger(massifIndex)) {
111
+ throw new Error(`Massif index must be a safe integer, got ${massifIndex}`);
112
+ }
113
+ const index = BigInt(massifIndex);
114
+ if (index < 0n || index > MAX_MASSIF_INDEX) {
115
+ throw new Error(`Massif index must be 0..${MAX_MASSIF_INDEX}, got ${index}`);
116
+ }
117
+ return index.toString(10).padStart(MASSIF_OBJECT_INDEX_DIGITS, "0");
118
+ }
119
+ /**
120
+ * Reject a massif height or log id that would build a key Go cannot read:
121
+ * height outside 1..255 or non-integer, or a log id that is empty or would
122
+ * add path segments.
123
+ */
124
+ function assertKeyHeightAndLogId(massifHeight, logId) {
125
+ if (!Number.isInteger(massifHeight) ||
126
+ massifHeight < MIN_MASSIF_HEIGHT ||
127
+ massifHeight > MAX_MASSIF_HEIGHT) {
128
+ throw new Error(`Massif height must be an integer ${MIN_MASSIF_HEIGHT}..${MAX_MASSIF_HEIGHT}, got ${massifHeight}`);
129
+ }
130
+ if (logId.length === 0 || logId.includes("/")) {
131
+ throw new Error(`Log id must be a non-empty path segment, got: ${JSON.stringify(logId)}`);
132
+ }
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/merklelog",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "MMR (merkle mountain range) proofs and massif storage format for Forestrie transparency logs",
package/src/index.ts CHANGED
@@ -43,7 +43,14 @@ export {
43
43
  isMassifFull,
44
44
  } from "./massifs/types.js";
45
45
 
46
- export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
46
+ export {
47
+ formatMassifObjectIndex,
48
+ MASSIF_OBJECT_INDEX_DIGITS,
49
+ MAX_MASSIF_INDEX,
50
+ parseV2StorageObjectPath,
51
+ v2CheckpointObjectKey,
52
+ v2MassifObjectKey,
53
+ } from "./massifs/v2storagepaths.js";
47
54
 
48
55
  // Massif blob node access for MMR proof building (plan-2607-15 §4)
49
56
  export { openMassifNodeStore } from "./massifs/nodestore.js";
@@ -1,56 +1,106 @@
1
1
  /**
2
- * Parse an R2 object path to extract log ID, massif height, and massif index.
2
+ * Merklelog v2 R2 object layout:
3
3
  *
4
4
  * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
5
  * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
6
  *
7
- * @param path - The object path path from the R2 notification
7
+ * `{massifIndex}` is a zero-padded, 16-digit DECIMAL integer. The Go writer
8
+ * owns this convention: go-merklelog
9
+ * [massifs/storage/const.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/const.go)
10
+ * (`V1MMRBlobNameFmt = "%016d.log"`, `V1MMRSignedTreeHeadBlobNameFmt =
11
+ * "%016d.sth"`) and its reader
12
+ * [objectindexfrompath.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/objectindexfrompath.go)
13
+ * (`strconv.Atoi`). canopy-api builds its R2 keys through the functions
14
+ * here (FOR-621: an earlier reader parsed hex, which agrees with the writer
15
+ * only for indices 0-9). Code outside this package that spells a key by hand
16
+ * (delegation-coordinator `seal-hint.ts`, test fixtures, demos) uses only
17
+ * index 0, which reads the same in any radix; anything that needs another
18
+ * index must use these helpers.
19
+ */
20
+
21
+ /** Width of the massif index segment in an object key. */
22
+ export const MASSIF_OBJECT_INDEX_DIGITS = 16;
23
+
24
+ /**
25
+ * Largest massif index the Go side can represent. go-merklelog
26
+ * `FmtMassifPath(prefix, massifIndex uint32)` and arbor's publisher
27
+ * (`strconv.ParseUint(base, 10, 32)`) use `uint32`; a larger index would be
28
+ * silently truncated by `ObjectIndexFromPath`, so it is rejected here.
29
+ */
30
+ export const MAX_MASSIF_INDEX = 0xffffffffn;
31
+
32
+ /**
33
+ * Massif height bounds. Go writes height as `%d` of a `uint8` and arbor's
34
+ * publisher `ParseCheckpointKey` rejects 0, so valid heights are 1..255 with
35
+ * no leading zeros.
36
+ */
37
+ const MIN_MASSIF_HEIGHT = 1;
38
+ const MAX_MASSIF_HEIGHT = 255;
39
+
40
+ const MASSIF_OBJECT_INDEX_RE = /^[0-9]{16}$/;
41
+ const MASSIF_HEIGHT_RE = /^[1-9][0-9]{0,2}$/;
42
+ const OBJECT_EXTENSIONS: Readonly<Record<string, string>> = {
43
+ massifs: ".log",
44
+ checkpoints: ".sth",
45
+ };
46
+
47
+ /**
48
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
49
+ *
50
+ * @param path - The object path from the R2 notification
8
51
  * @returns Parsed components including logId, massifHeight, and massifIndex
9
52
  * @throws Error if the path doesn't match expected format or if parsing fails
10
53
  */
11
54
  export function parseV2StorageObjectPath(path: string) {
12
55
  const parts = path.split("/");
13
56
 
14
- if (parts.length < 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
57
+ if (parts.length !== 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
15
58
  throw new Error(`Unrecognized path format: ${path}`);
16
59
 
17
- // Check for new v2 format: v2/merklelog/massifs/{massifHeight}/{logId}/{index}.log
18
- // or v2/merklelog/checkpoints/{massifHeight}/{logId}/{index}.sth
19
- const typePart = parts[2]; // "massifs" or "checkpoints"
60
+ const typePart = parts[2];
20
61
  const massifHeightStr = parts[3];
21
62
  const logId = parts[4];
22
63
  const filename = parts[5];
23
64
 
24
- // Validate extension
25
- const expectedExt = typePart === "massifs" ? ".log" : ".sth";
65
+ const expectedExt = Object.hasOwn(OBJECT_EXTENSIONS, typePart)
66
+ ? OBJECT_EXTENSIONS[typePart]
67
+ : undefined;
68
+ if (expectedExt === undefined) {
69
+ throw new Error(
70
+ `Object type must be massifs or checkpoints, got: ${typePart}`,
71
+ );
72
+ }
73
+ if (logId.length === 0) {
74
+ throw new Error(`Log id segment is empty: ${path}`);
75
+ }
26
76
  if (!filename.endsWith(expectedExt)) {
27
77
  throw new Error(
28
78
  `Expected ${expectedExt} extension for ${typePart}, got: ${filename}`,
29
79
  );
30
80
  }
31
81
 
32
- // Parse massif index (hex, 16 digits)
33
82
  const massifIndexStr = filename.slice(0, -expectedExt.length);
34
- if (massifIndexStr.length !== 16) {
83
+ if (!MASSIF_OBJECT_INDEX_RE.test(massifIndexStr)) {
35
84
  throw new Error(
36
- `Massif index must be 16 hex digits, got ${massifIndexStr.length}: ${massifIndexStr}`,
85
+ `Massif index must be 16 decimal digits, got: ${massifIndexStr}`,
37
86
  );
38
87
  }
39
-
40
- const massifIndex = Number.parseInt(massifIndexStr, 16);
41
- if (!Number.isFinite(massifIndex)) {
88
+ const massifIndex = BigInt(massifIndexStr);
89
+ if (massifIndex > MAX_MASSIF_INDEX) {
42
90
  throw new Error(
43
- `Failed to parse massif index as hex number: ${massifIndexStr}`,
91
+ `Massif index must be 0..${MAX_MASSIF_INDEX}, got ${massifIndex}`,
44
92
  );
45
93
  }
46
94
 
47
- // Parse massifHeight
48
- const massifHeight = Number.parseInt(massifHeightStr, 10);
49
- if (!Number.isFinite(massifHeight)) {
95
+ if (
96
+ !MASSIF_HEIGHT_RE.test(massifHeightStr) ||
97
+ Number(massifHeightStr) > MAX_MASSIF_HEIGHT
98
+ ) {
50
99
  throw new Error(
51
100
  `Failed to parse massif height as number: ${massifHeightStr}`,
52
101
  );
53
102
  }
103
+ const massifHeight = Number(massifHeightStr);
54
104
 
55
105
  return {
56
106
  logId,
@@ -59,3 +109,68 @@ export function parseV2StorageObjectPath(path: string) {
59
109
  type: typePart,
60
110
  };
61
111
  }
112
+
113
+ /**
114
+ * Build the massif `.log` object key for a log.
115
+ */
116
+ export function v2MassifObjectKey(
117
+ massifHeight: number,
118
+ logId: string,
119
+ massifIndex: bigint | number,
120
+ ): string {
121
+ assertKeyHeightAndLogId(massifHeight, logId);
122
+ return `v2/merklelog/massifs/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.log`;
123
+ }
124
+
125
+ /**
126
+ * Build the signed checkpoint `.sth` object key for a log.
127
+ */
128
+ export function v2CheckpointObjectKey(
129
+ massifHeight: number,
130
+ logId: string,
131
+ massifIndex: bigint | number,
132
+ ): string {
133
+ assertKeyHeightAndLogId(massifHeight, logId);
134
+ return `v2/merklelog/checkpoints/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.sth`;
135
+ }
136
+
137
+ /**
138
+ * Format a massif index as the 16-digit decimal key segment (Go `%016d`).
139
+ *
140
+ * @throws Error if the index is negative, not an integer, not a safe integer
141
+ * (number input), or exceeds {@link MAX_MASSIF_INDEX}.
142
+ */
143
+ export function formatMassifObjectIndex(massifIndex: bigint | number): string {
144
+ if (typeof massifIndex === "number" && !Number.isSafeInteger(massifIndex)) {
145
+ throw new Error(`Massif index must be a safe integer, got ${massifIndex}`);
146
+ }
147
+ const index = BigInt(massifIndex);
148
+ if (index < 0n || index > MAX_MASSIF_INDEX) {
149
+ throw new Error(
150
+ `Massif index must be 0..${MAX_MASSIF_INDEX}, got ${index}`,
151
+ );
152
+ }
153
+ return index.toString(10).padStart(MASSIF_OBJECT_INDEX_DIGITS, "0");
154
+ }
155
+
156
+ /**
157
+ * Reject a massif height or log id that would build a key Go cannot read:
158
+ * height outside 1..255 or non-integer, or a log id that is empty or would
159
+ * add path segments.
160
+ */
161
+ function assertKeyHeightAndLogId(massifHeight: number, logId: string): void {
162
+ if (
163
+ !Number.isInteger(massifHeight) ||
164
+ massifHeight < MIN_MASSIF_HEIGHT ||
165
+ massifHeight > MAX_MASSIF_HEIGHT
166
+ ) {
167
+ throw new Error(
168
+ `Massif height must be an integer ${MIN_MASSIF_HEIGHT}..${MAX_MASSIF_HEIGHT}, got ${massifHeight}`,
169
+ );
170
+ }
171
+ if (logId.length === 0 || logId.includes("/")) {
172
+ throw new Error(
173
+ `Log id must be a non-empty path segment, got: ${JSON.stringify(logId)}`,
174
+ );
175
+ }
176
+ }