@bcts/hubert 1.0.0-alpha.17

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.
Files changed (104) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +18 -0
  3. package/dist/arid-derivation-1CJuU-kZ.cjs +150 -0
  4. package/dist/arid-derivation-1CJuU-kZ.cjs.map +1 -0
  5. package/dist/arid-derivation-CbqACjdg.mjs +126 -0
  6. package/dist/arid-derivation-CbqACjdg.mjs.map +1 -0
  7. package/dist/bin/hubert.cjs +384 -0
  8. package/dist/bin/hubert.cjs.map +1 -0
  9. package/dist/bin/hubert.d.cts +1 -0
  10. package/dist/bin/hubert.d.mts +1 -0
  11. package/dist/bin/hubert.mjs +383 -0
  12. package/dist/bin/hubert.mjs.map +1 -0
  13. package/dist/chunk-CbDLau6x.cjs +34 -0
  14. package/dist/hybrid/index.cjs +14 -0
  15. package/dist/hybrid/index.d.cts +3 -0
  16. package/dist/hybrid/index.d.mts +3 -0
  17. package/dist/hybrid/index.mjs +6 -0
  18. package/dist/hybrid-BZhumygj.mjs +356 -0
  19. package/dist/hybrid-BZhumygj.mjs.map +1 -0
  20. package/dist/hybrid-dX5JLumO.cjs +410 -0
  21. package/dist/hybrid-dX5JLumO.cjs.map +1 -0
  22. package/dist/index-BEzpUC7r.d.mts +380 -0
  23. package/dist/index-BEzpUC7r.d.mts.map +1 -0
  24. package/dist/index-C2F6ugLL.d.mts +210 -0
  25. package/dist/index-C2F6ugLL.d.mts.map +1 -0
  26. package/dist/index-CUnDouMb.d.mts +215 -0
  27. package/dist/index-CUnDouMb.d.mts.map +1 -0
  28. package/dist/index-CV6lZJqY.d.cts +380 -0
  29. package/dist/index-CV6lZJqY.d.cts.map +1 -0
  30. package/dist/index-CY3TCzIm.d.cts +217 -0
  31. package/dist/index-CY3TCzIm.d.cts.map +1 -0
  32. package/dist/index-DEr4SR1J.d.cts +215 -0
  33. package/dist/index-DEr4SR1J.d.cts.map +1 -0
  34. package/dist/index-T1LHanIb.d.mts +217 -0
  35. package/dist/index-T1LHanIb.d.mts.map +1 -0
  36. package/dist/index-jyzuOhFB.d.cts +210 -0
  37. package/dist/index-jyzuOhFB.d.cts.map +1 -0
  38. package/dist/index.cjs +60 -0
  39. package/dist/index.d.cts +161 -0
  40. package/dist/index.d.cts.map +1 -0
  41. package/dist/index.d.mts +161 -0
  42. package/dist/index.d.mts.map +1 -0
  43. package/dist/index.mjs +10 -0
  44. package/dist/ipfs/index.cjs +13 -0
  45. package/dist/ipfs/index.d.cts +3 -0
  46. package/dist/ipfs/index.d.mts +3 -0
  47. package/dist/ipfs/index.mjs +5 -0
  48. package/dist/ipfs-BRMMCBjv.mjs +1 -0
  49. package/dist/ipfs-CetOVQcO.cjs +0 -0
  50. package/dist/kv-BAmhmMOo.cjs +425 -0
  51. package/dist/kv-BAmhmMOo.cjs.map +1 -0
  52. package/dist/kv-C-emxv0w.mjs +375 -0
  53. package/dist/kv-C-emxv0w.mjs.map +1 -0
  54. package/dist/kv-DJiKvypY.mjs +403 -0
  55. package/dist/kv-DJiKvypY.mjs.map +1 -0
  56. package/dist/kv-store-DmngWWuw.d.mts +183 -0
  57. package/dist/kv-store-DmngWWuw.d.mts.map +1 -0
  58. package/dist/kv-store-ww-AUyLd.d.cts +183 -0
  59. package/dist/kv-store-ww-AUyLd.d.cts.map +1 -0
  60. package/dist/kv-yjvQa_LH.cjs +457 -0
  61. package/dist/kv-yjvQa_LH.cjs.map +1 -0
  62. package/dist/logging-hmzNzifq.mjs +158 -0
  63. package/dist/logging-hmzNzifq.mjs.map +1 -0
  64. package/dist/logging-qc9uMgil.cjs +212 -0
  65. package/dist/logging-qc9uMgil.cjs.map +1 -0
  66. package/dist/mainline/index.cjs +12 -0
  67. package/dist/mainline/index.d.cts +3 -0
  68. package/dist/mainline/index.d.mts +3 -0
  69. package/dist/mainline/index.mjs +5 -0
  70. package/dist/mainline-D_jfeFMh.cjs +0 -0
  71. package/dist/mainline-cFIuXbo-.mjs +1 -0
  72. package/dist/server/index.cjs +14 -0
  73. package/dist/server/index.d.cts +3 -0
  74. package/dist/server/index.d.mts +3 -0
  75. package/dist/server/index.mjs +3 -0
  76. package/dist/server-BBNRZ30D.cjs +912 -0
  77. package/dist/server-BBNRZ30D.cjs.map +1 -0
  78. package/dist/server-DVyk9gqU.mjs +836 -0
  79. package/dist/server-DVyk9gqU.mjs.map +1 -0
  80. package/package.json +125 -0
  81. package/src/arid-derivation.ts +155 -0
  82. package/src/bin/hubert.ts +667 -0
  83. package/src/error.ts +89 -0
  84. package/src/hybrid/error.ts +77 -0
  85. package/src/hybrid/index.ts +24 -0
  86. package/src/hybrid/kv.ts +236 -0
  87. package/src/hybrid/reference.ts +176 -0
  88. package/src/index.ts +145 -0
  89. package/src/ipfs/error.ts +83 -0
  90. package/src/ipfs/index.ts +24 -0
  91. package/src/ipfs/kv.ts +476 -0
  92. package/src/ipfs/value.ts +85 -0
  93. package/src/kv-store.ts +128 -0
  94. package/src/logging.ts +88 -0
  95. package/src/mainline/error.ts +108 -0
  96. package/src/mainline/index.ts +23 -0
  97. package/src/mainline/kv.ts +411 -0
  98. package/src/server/error.ts +83 -0
  99. package/src/server/index.ts +29 -0
  100. package/src/server/kv.ts +211 -0
  101. package/src/server/memory-kv.ts +191 -0
  102. package/src/server/server-kv.ts +92 -0
  103. package/src/server/server.ts +369 -0
  104. package/src/server/sqlite-kv.ts +295 -0
package/src/error.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Top-level error types for the hubert library.
3
+ *
4
+ * Port of error.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ /**
10
+ * Base error class for all Hubert errors.
11
+ *
12
+ * @category Errors
13
+ */
14
+ export class HubertError extends Error {
15
+ constructor(message: string) {
16
+ super(message);
17
+ this.name = "HubertError";
18
+ // Maintains proper stack trace for where error was thrown (V8 only)
19
+ if (Error.captureStackTrace) {
20
+ Error.captureStackTrace(this, this.constructor);
21
+ }
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Error thrown when attempting to store at an ARID that already exists.
27
+ *
28
+ * Port of `Error::AlreadyExists { arid }` from error.rs line 6.
29
+ *
30
+ * @category Errors
31
+ */
32
+ export class AlreadyExistsError extends HubertError {
33
+ /** The ARID that already exists */
34
+ readonly arid: string;
35
+
36
+ constructor(arid: string) {
37
+ super(`${arid} already exists`);
38
+ this.name = "AlreadyExistsError";
39
+ this.arid = arid;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Error thrown when an ARID is not found.
45
+ *
46
+ * Port of `Error::NotFound` from error.rs line 8.
47
+ *
48
+ * @category Errors
49
+ */
50
+ export class NotFoundError extends HubertError {
51
+ constructor() {
52
+ super("Not found");
53
+ this.name = "NotFoundError";
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Error thrown when an ARID format is invalid.
59
+ *
60
+ * Port of `Error::InvalidArid` from error.rs line 11.
61
+ *
62
+ * @category Errors
63
+ */
64
+ export class InvalidAridError extends HubertError {
65
+ constructor() {
66
+ super("Invalid ARID format");
67
+ this.name = "InvalidAridError";
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Error thrown for I/O operations.
73
+ *
74
+ * Port of `Error::Io(e)` from error.rs line 35.
75
+ *
76
+ * @category Errors
77
+ */
78
+ export class IoError extends HubertError {
79
+ /** The underlying error */
80
+ override readonly cause?: Error;
81
+
82
+ constructor(message: string, cause?: Error) {
83
+ super(`IO error: ${message}`);
84
+ this.name = "IoError";
85
+ if (cause !== undefined) {
86
+ this.cause = cause;
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Hybrid-specific errors.
3
+ *
4
+ * Port of hybrid/error.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { HubertError } from "../error.js";
10
+
11
+ /**
12
+ * Base class for Hybrid-specific errors.
13
+ *
14
+ * @category Hybrid
15
+ */
16
+ export class HybridError extends HubertError {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = "HybridError";
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Referenced IPFS content not found.
25
+ *
26
+ * Port of `Error::ContentNotFound` from hybrid/error.rs line 4-5.
27
+ *
28
+ * @category Hybrid
29
+ */
30
+ export class ContentNotFoundError extends HybridError {
31
+ constructor() {
32
+ super("Referenced IPFS content not found");
33
+ this.name = "ContentNotFoundError";
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Not a reference envelope.
39
+ *
40
+ * Port of `Error::NotReferenceEnvelope` from hybrid/error.rs line 7-8.
41
+ *
42
+ * @category Hybrid
43
+ */
44
+ export class NotReferenceEnvelopeError extends HybridError {
45
+ constructor() {
46
+ super("Not a reference envelope");
47
+ this.name = "NotReferenceEnvelopeError";
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Invalid ARID in reference envelope.
53
+ *
54
+ * Port of `Error::InvalidReferenceArid` from hybrid/error.rs line 10-11.
55
+ *
56
+ * @category Hybrid
57
+ */
58
+ export class InvalidReferenceAridError extends HybridError {
59
+ constructor() {
60
+ super("Invalid ARID in reference envelope");
61
+ this.name = "InvalidReferenceAridError";
62
+ }
63
+ }
64
+
65
+ /**
66
+ * No id assertion found in reference envelope.
67
+ *
68
+ * Port of `Error::NoIdAssertion` from hybrid/error.rs line 13-14.
69
+ *
70
+ * @category Hybrid
71
+ */
72
+ export class NoIdAssertionError extends HybridError {
73
+ constructor() {
74
+ super("No id assertion found in reference envelope");
75
+ this.name = "NoIdAssertionError";
76
+ }
77
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Hybrid module for Hubert distributed storage.
3
+ *
4
+ * This module combines Mainline DHT and IPFS for optimal storage.
5
+ *
6
+ * Port of hybrid/mod.rs from hubert-rust.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ // Error types
12
+ export {
13
+ HybridError,
14
+ ContentNotFoundError,
15
+ NotReferenceEnvelopeError,
16
+ InvalidReferenceAridError,
17
+ NoIdAssertionError,
18
+ } from "./error.js";
19
+
20
+ // Reference envelope utilities
21
+ export { createReferenceEnvelope, isReferenceEnvelope, extractReferenceArid } from "./reference.js";
22
+
23
+ // Hybrid KvStore implementation
24
+ export { HybridKv } from "./kv.js";
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Hybrid storage layer combining Mainline DHT and IPFS.
3
+ *
4
+ * Port of hybrid/kv.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { ARID } from "@bcts/components";
10
+ import { type Envelope } from "@bcts/envelope";
11
+
12
+ import { type KvStore } from "../kv-store.js";
13
+ import { verbosePrintln } from "../logging.js";
14
+ import { IpfsKv } from "../ipfs/kv.js";
15
+ import { MainlineDhtKv } from "../mainline/kv.js";
16
+ import { ContentNotFoundError } from "./error.js";
17
+ import { createReferenceEnvelope, extractReferenceArid, isReferenceEnvelope } from "./reference.js";
18
+
19
+ /**
20
+ * Hybrid storage layer combining Mainline DHT and IPFS.
21
+ *
22
+ * Automatically optimizes storage based on envelope size:
23
+ * - **Small envelopes (≤1000 bytes)**: Stored directly in DHT
24
+ * - **Large envelopes (>1000 bytes)**: Reference in DHT → actual envelope in IPFS
25
+ *
26
+ * This provides the best of both worlds:
27
+ * - Fast lookups for small messages via DHT
28
+ * - Large capacity for big messages via IPFS
29
+ * - Transparent indirection handled automatically
30
+ *
31
+ * Port of `struct HybridKv` from hybrid/kv.rs lines 59-63.
32
+ *
33
+ * # Requirements
34
+ *
35
+ * - No external daemon for DHT (embedded client)
36
+ * - Requires Kubo daemon for IPFS (http://127.0.0.1:5001)
37
+ *
38
+ * @category Hybrid Backend
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const store = await HybridKv.create("http://127.0.0.1:5001");
43
+ *
44
+ * // Small envelope → DHT only
45
+ * const arid1 = ARID.new();
46
+ * const small = Envelope.new("Small message");
47
+ * await store.put(arid1, small);
48
+ *
49
+ * // Large envelope → DHT reference + IPFS
50
+ * const arid2 = ARID.new();
51
+ * const large = Envelope.new("x".repeat(2000));
52
+ * await store.put(arid2, large);
53
+ *
54
+ * // Get works the same for both
55
+ * const retrieved1 = await store.get(arid1);
56
+ * const retrieved2 = await store.get(arid2);
57
+ * ```
58
+ */
59
+ export class HybridKv implements KvStore {
60
+ private readonly dht: MainlineDhtKv;
61
+ private ipfs: IpfsKv;
62
+ private dhtSizeLimit: number;
63
+
64
+ /**
65
+ * Private constructor - use `create()` factory method.
66
+ */
67
+ private constructor(dht: MainlineDhtKv, ipfs: IpfsKv) {
68
+ this.dht = dht;
69
+ this.ipfs = ipfs;
70
+ this.dhtSizeLimit = 1000; // Conservative DHT limit
71
+ }
72
+
73
+ /**
74
+ * Create a new Hybrid KV store with default settings.
75
+ *
76
+ * Port of `HybridKv::new()` from hybrid/kv.rs lines 75-84.
77
+ *
78
+ * @param ipfsRpcUrl - IPFS RPC endpoint (e.g., "http://127.0.0.1:5001")
79
+ */
80
+ static async create(ipfsRpcUrl: string): Promise<HybridKv> {
81
+ const dht = await MainlineDhtKv.create();
82
+ const ipfs = new IpfsKv(ipfsRpcUrl);
83
+
84
+ return new HybridKv(dht, ipfs);
85
+ }
86
+
87
+ /**
88
+ * Set custom DHT size limit (default: 1000 bytes).
89
+ *
90
+ * Envelopes larger than this will use IPFS indirection.
91
+ *
92
+ * Port of `HybridKv::with_dht_size_limit()` from hybrid/kv.rs lines 89-92.
93
+ */
94
+ withDhtSizeLimit(limit: number): this {
95
+ this.dhtSizeLimit = limit;
96
+ return this;
97
+ }
98
+
99
+ /**
100
+ * Set whether to pin content in IPFS (default: false).
101
+ *
102
+ * Only affects envelopes stored in IPFS (when larger than DHT limit).
103
+ *
104
+ * Port of `HybridKv::with_pin_content()` from hybrid/kv.rs lines 97-100.
105
+ */
106
+ withPinContent(pin: boolean): this {
107
+ this.ipfs = this.ipfs.withPinContent(pin);
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Check if an envelope fits in the DHT.
113
+ *
114
+ * Port of `HybridKv::fits_in_dht()` from hybrid/kv.rs lines 103-106.
115
+ *
116
+ * @internal
117
+ */
118
+ private fitsInDht(envelope: Envelope): boolean {
119
+ const serialized = envelope.taggedCborData;
120
+ return serialized.length <= this.dhtSizeLimit;
121
+ }
122
+
123
+ /**
124
+ * Store an envelope at the given ARID.
125
+ *
126
+ * Port of `KvStore::put()` implementation from hybrid/kv.rs lines 109-168.
127
+ */
128
+ async put(
129
+ arid: ARID,
130
+ envelope: Envelope,
131
+ ttlSeconds?: number,
132
+ verbose?: boolean,
133
+ ): Promise<string> {
134
+ // Check if it fits in DHT
135
+ if (this.fitsInDht(envelope)) {
136
+ // Store directly in DHT (DHT handles obfuscation)
137
+ if (verbose) {
138
+ verbosePrintln(`Storing envelope in DHT (size ≤ ${this.dhtSizeLimit} bytes)`);
139
+ }
140
+ await this.dht.put(arid, envelope, ttlSeconds, verbose);
141
+ return `Stored in DHT at ARID: ${arid.urString()}`;
142
+ } else {
143
+ // Use IPFS with DHT reference
144
+ if (verbose) {
145
+ verbosePrintln("Envelope too large for DHT, using IPFS indirection");
146
+ }
147
+
148
+ // 1. Store actual envelope in IPFS with a new ARID
149
+ // (IPFS handles obfuscation with reference_arid)
150
+ const referenceArid = ARID.new();
151
+ if (verbose) {
152
+ verbosePrintln(
153
+ `Storing actual envelope in IPFS with reference ARID: ${referenceArid.urString()}`,
154
+ );
155
+ }
156
+ await this.ipfs.put(referenceArid, envelope, ttlSeconds, verbose);
157
+
158
+ // 2. Create reference envelope
159
+ const envelopeSize = envelope.taggedCborData.length;
160
+ const reference = createReferenceEnvelope(referenceArid, envelopeSize);
161
+
162
+ // 3. Store reference envelope in DHT at original ARID
163
+ // (DHT handles obfuscation with original arid)
164
+ if (verbose) {
165
+ verbosePrintln("Storing reference envelope in DHT at original ARID");
166
+ }
167
+ await this.dht.put(arid, reference, ttlSeconds, verbose);
168
+
169
+ return `Stored in IPFS (ref: ${referenceArid.urString()}) via DHT at ARID: ${arid.urString()}`;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Retrieve an envelope for the given ARID.
175
+ *
176
+ * Port of `KvStore::get()` implementation from hybrid/kv.rs lines 171-230.
177
+ */
178
+ async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {
179
+ // 1. Try to get from DHT (DHT handles deobfuscation)
180
+ const dhtEnvelope = await this.dht.get(arid, timeoutSeconds, verbose);
181
+
182
+ if (dhtEnvelope === null) {
183
+ return null;
184
+ }
185
+
186
+ // 2. Check if the envelope is a reference envelope
187
+ if (isReferenceEnvelope(dhtEnvelope)) {
188
+ if (verbose) {
189
+ verbosePrintln("Found reference envelope, fetching actual envelope from IPFS");
190
+ }
191
+
192
+ // 3. Extract reference ARID
193
+ const referenceArid = extractReferenceArid(dhtEnvelope);
194
+
195
+ if (verbose) {
196
+ verbosePrintln(`Reference ARID: ${referenceArid.urString()}`);
197
+ }
198
+
199
+ // 4. Retrieve actual envelope from IPFS
200
+ // (IPFS handles deobfuscation with reference_arid)
201
+ const ipfsEnvelope = await this.ipfs.get(referenceArid, timeoutSeconds, verbose);
202
+
203
+ if (ipfsEnvelope === null) {
204
+ throw new ContentNotFoundError();
205
+ }
206
+
207
+ if (verbose) {
208
+ verbosePrintln("Successfully retrieved actual envelope from IPFS");
209
+ }
210
+ return ipfsEnvelope;
211
+ } else {
212
+ // Not a reference envelope, return it directly
213
+ if (verbose) {
214
+ verbosePrintln("Envelope is not a reference, treating as direct payload");
215
+ }
216
+ return dhtEnvelope;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Check if an envelope exists at the given ARID.
222
+ *
223
+ * Port of `KvStore::exists()` implementation from hybrid/kv.rs lines 254-257.
224
+ */
225
+ async exists(arid: ARID): Promise<boolean> {
226
+ // Check DHT only (references count as existing)
227
+ return await this.dht.exists(arid);
228
+ }
229
+
230
+ /**
231
+ * Destroy the hybrid store and release resources.
232
+ */
233
+ async destroy(): Promise<void> {
234
+ await this.dht.destroy();
235
+ }
236
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Reference envelope utilities for hybrid storage.
3
+ *
4
+ * Port of hybrid/reference.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { ARID } from "@bcts/components";
10
+ import { Envelope } from "@bcts/envelope";
11
+ import { KnownValue } from "@bcts/known-values";
12
+
13
+ import {
14
+ InvalidReferenceAridError,
15
+ NoIdAssertionError,
16
+ NotReferenceEnvelopeError,
17
+ } from "./error.js";
18
+
19
+ /**
20
+ * Known value for dereferenceVia predicate.
21
+ * @internal
22
+ */
23
+ const DEREFERENCE_VIA = new KnownValue(8);
24
+
25
+ /**
26
+ * Known value for id predicate.
27
+ * @internal
28
+ */
29
+ const ID = new KnownValue(2);
30
+
31
+ /**
32
+ * Creates a reference envelope that points to content stored in IPFS.
33
+ *
34
+ * Reference envelopes are small envelopes stored in the DHT that contain
35
+ * a pointer to the actual envelope stored in IPFS. This allows the hybrid
36
+ * storage layer to transparently handle large envelopes that exceed the
37
+ * DHT size limit.
38
+ *
39
+ * Port of `create_reference_envelope()` from hybrid/reference.rs lines 31-39.
40
+ *
41
+ * # Format
42
+ *
43
+ * ```text
44
+ * '' [
45
+ * 'dereferenceVia': "ipfs",
46
+ * 'id': <ARID>,
47
+ * "size": <number>
48
+ * ]
49
+ * ```
50
+ *
51
+ * @param referenceArid - The ARID used to look up the actual envelope in IPFS
52
+ * @param actualSize - Size of the actual envelope in bytes (for diagnostics)
53
+ * @returns A reference envelope that can be stored in the DHT
54
+ *
55
+ * @category Hybrid
56
+ */
57
+ export function createReferenceEnvelope(referenceArid: ARID, actualSize: number): Envelope {
58
+ return Envelope.unit()
59
+ .addAssertion(DEREFERENCE_VIA, "ipfs")
60
+ .addAssertion(ID, referenceArid)
61
+ .addAssertion("size", actualSize);
62
+ }
63
+
64
+ /**
65
+ * Checks if an envelope is a reference envelope.
66
+ *
67
+ * A reference envelope contains `dereferenceVia: "ipfs"` and an `id` assertion.
68
+ *
69
+ * Port of `is_reference_envelope()` from hybrid/reference.rs lines 53-91.
70
+ *
71
+ * @param envelope - The envelope to check
72
+ * @returns `true` if this is a reference envelope, `false` otherwise
73
+ *
74
+ * @category Hybrid
75
+ */
76
+ export function isReferenceEnvelope(envelope: Envelope): boolean {
77
+ // Check if subject is the unit value
78
+ if (!envelope.isSubjectUnit) {
79
+ return false;
80
+ }
81
+
82
+ const assertions = envelope.assertions();
83
+
84
+ // Check for dereferenceVia: "ipfs" assertion
85
+ let hasDereferenceVia = false;
86
+ let hasId = false;
87
+
88
+ for (const assertion of assertions) {
89
+ try {
90
+ const predicate = assertion.predicate();
91
+
92
+ // Check if predicate is a known value
93
+ const predicateSubject = predicate.subject();
94
+ if (predicateSubject instanceof KnownValue) {
95
+ const kv = predicateSubject;
96
+
97
+ // Check for dereferenceVia
98
+ if (kv.value === DEREFERENCE_VIA.value) {
99
+ const object = assertion.object();
100
+ const objectSubject = object.subject();
101
+ if (typeof objectSubject === "string" && objectSubject === "ipfs") {
102
+ hasDereferenceVia = true;
103
+ }
104
+ }
105
+
106
+ // Check for id
107
+ if (kv.value === ID.value) {
108
+ hasId = true;
109
+ }
110
+ }
111
+ } catch {
112
+ // Skip assertions that can't be parsed
113
+ continue;
114
+ }
115
+ }
116
+
117
+ return hasDereferenceVia && hasId;
118
+ }
119
+
120
+ /**
121
+ * Extracts the reference ARID from a reference envelope.
122
+ *
123
+ * Port of `extract_reference_arid()` from hybrid/reference.rs lines 104-129.
124
+ *
125
+ * @param envelope - The reference envelope
126
+ * @returns The reference ARID
127
+ * @throws {NotReferenceEnvelopeError} If the envelope is not a reference envelope
128
+ * @throws {InvalidReferenceAridError} If the ARID cannot be extracted
129
+ * @throws {NoIdAssertionError} If no id assertion is found
130
+ *
131
+ * @category Hybrid
132
+ */
133
+ export function extractReferenceArid(envelope: Envelope): ARID {
134
+ if (!isReferenceEnvelope(envelope)) {
135
+ throw new NotReferenceEnvelopeError();
136
+ }
137
+
138
+ const assertions = envelope.assertions();
139
+
140
+ // Find the id assertion and extract the ARID
141
+ for (const assertion of assertions) {
142
+ try {
143
+ const predicate = assertion.predicate();
144
+ const predicateSubject = predicate.subject();
145
+
146
+ if (predicateSubject instanceof KnownValue) {
147
+ const kv = predicateSubject;
148
+
149
+ // Check for id
150
+ if (kv.value === ID.value) {
151
+ const object = assertion.object();
152
+ const objectSubject = object.subject();
153
+
154
+ if (objectSubject instanceof ARID) {
155
+ return objectSubject;
156
+ }
157
+
158
+ // Try to extract ARID from CBOR
159
+ throw new InvalidReferenceAridError();
160
+ }
161
+ }
162
+ } catch (error) {
163
+ if (
164
+ error instanceof NotReferenceEnvelopeError ||
165
+ error instanceof InvalidReferenceAridError ||
166
+ error instanceof NoIdAssertionError
167
+ ) {
168
+ throw error;
169
+ }
170
+ // Continue searching
171
+ continue;
172
+ }
173
+ }
174
+
175
+ throw new NoIdAssertionError();
176
+ }