@fizzyflow/endless-vector 0.0.8 → 0.0.10

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.
@@ -1,8 +1,7 @@
1
1
  import EndlessVectorHistory from './EndlessVectorHistory.js';
2
2
 
3
3
  /**
4
- * @typedef {import('@mysten/sui/client').SuiClient} SuiClient
5
- * @typedef {import('@mysten/sui/client').GetDynamicFieldsParams} GetDynamicFieldsParams
4
+ * @typedef {import('@mysten/sui/grpc').SuiGrpcClient} SuiGrpcClient
6
5
  * @typedef {import('./EndlessVector.js').default} EndlessVector
7
6
  */
8
7
 
@@ -10,14 +9,14 @@ export default class EndlessVectorArchive {
10
9
  /**
11
10
  * Creates a new EndlessVectorArchive instance.
12
11
  * @param {Object} params - Configuration parameters
13
- * @param {SuiClient} [params.suiClient] - Sui client instance for blockchain interactions
12
+ * @param {SuiGrpcClient} [params.suiClient] - Sui gRPC client instance for blockchain interactions
14
13
  * @param {string} [params.id] - ID or address of the EndlessVectorArchive on the Sui blockchain
15
14
  * @param {number} [params.index=0] - Index position of this archive item in the sequence
16
15
  * @param {EndlessVector} [params.endlessVector] - Reference to the parent EndlessVector instance
17
16
  * @param {Object} [params.fields] - Raw field data from the blockchain object
18
17
  */
19
18
  constructor(params = {}) {
20
- /** @type {SuiClient} */
19
+ /** @type {SuiGrpcClient} */
21
20
  this.suiClient = params.suiClient;
22
21
  /** @type {string} */
23
22
  this.id = params.id;
@@ -47,8 +46,8 @@ export default class EndlessVectorArchive {
47
46
  */
48
47
  setFields(fields) {
49
48
  this._fields = fields;
50
- this.historyTableId = fields?.history?.fields?.id?.id;
51
- this.historyItemsCount = parseInt(fields?.history?.fields?.size || '0');
49
+ this.historyTableId = fields?.history?.id;
50
+ this.historyItemsCount = parseInt(fields?.history?.size || '0');
52
51
  }
53
52
 
54
53
  /**
@@ -75,7 +74,7 @@ export default class EndlessVectorArchive {
75
74
  * @returns {number} The starting index (inclusive)
76
75
  */
77
76
  get startsAt() {
78
- return this.endsAt - this.length;
77
+ return this.endsAt - this.length + 1;
79
78
  }
80
79
 
81
80
  /**
@@ -134,40 +133,30 @@ export default class EndlessVectorArchive {
134
133
  throw new Error('historyTableId is not set');
135
134
  }
136
135
 
137
- /** @type {GetDynamicFieldsParams} */
138
- const getDynamicFieldsParams = {
139
- parentId: this.historyTableId,
140
- options: {
141
- showContent: true,
142
- showType: true,
143
- },
144
- };
145
-
146
- let resp = null;
136
+ let cursor = undefined;
147
137
  let haveToLookMore = true;
148
138
 
149
139
  do {
150
- resp = await this.suiClient.getDynamicFields(getDynamicFieldsParams);
151
- if (resp && resp.data && resp.data.length) {
152
- for (const df of resp.data) {
153
- if (df?.objectId) {
154
- const itemHistoryIndex = parseInt(df.name.value);
155
- const endlessVectorHistory = new EndlessVectorHistory({
156
- suiClient: this.suiClient,
157
- id: df.objectId,
158
- index: itemHistoryIndex,
159
- endlessVector: this._endlessVector,
160
- endlessVectorArchive: this,
161
- });
162
- this._history[itemHistoryIndex] = endlessVectorHistory;
163
- if (itemHistoryIndex === historyIndexInt) {
164
- haveToLookMore = false;
165
- }
140
+ const resp = await this.suiClient.listDynamicFields({ parentId: this.historyTableId, cursor });
141
+ for (const df of resp.dynamicFields ?? []) {
142
+ if (df?.fieldId) {
143
+ const itemHistoryIndex = EndlessVectorArchive._decodeBcsU64(df.name.bcs);
144
+ const endlessVectorHistory = new EndlessVectorHistory({
145
+ suiClient: this.suiClient,
146
+ id: df.fieldId,
147
+ index: itemHistoryIndex,
148
+ endlessVector: this._endlessVector,
149
+ endlessVectorArchive: this,
150
+ });
151
+ this._history[itemHistoryIndex] = endlessVectorHistory;
152
+ if (itemHistoryIndex === historyIndexInt) {
153
+ haveToLookMore = false;
166
154
  }
167
155
  }
168
- getDynamicFieldsParams.cursor = resp.nextCursor;
169
156
  }
170
- } while (resp?.hasNextPage && haveToLookMore);
157
+ cursor = resp.cursor;
158
+ if (!resp.hasNextPage) break;
159
+ } while (haveToLookMore);
171
160
 
172
161
  if (!this._history[historyIndexInt]) {
173
162
  throw new Error(`History not found for index ${historyIndexInt}`);
@@ -206,11 +195,17 @@ export default class EndlessVectorArchive {
206
195
  * @returns {Promise<Uint8Array>} The suffix bytes from the history item
207
196
  * @throws {Error} If the index is out of range for this archive item
208
197
  */
198
+ static _decodeBcsU64(bcsBytes) {
199
+ const b = bcsBytes instanceof Uint8Array ? bcsBytes : new Uint8Array(bcsBytes);
200
+ const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
201
+ return Number(dv.getBigUint64(0, true));
202
+ }
203
+
209
204
  async getSuffixFromHistoryItemOfIndex(i) {
210
205
  if (i < this.historyItemsCount) {
211
206
  const historyItem = await this.getHistory(i);
212
207
  if (historyItem) {
213
- return historyItem.getSuffixStoredBytes();
208
+ return await historyItem.getSuffixStoredBytes();
214
209
  }
215
210
  }
216
211
 
@@ -1,6 +1,8 @@
1
1
 
2
+ import EndlessVectorItem from './EndlessVectorItem.js';
3
+
2
4
  /**
3
- * @typedef {import('@mysten/sui/client').SuiClient} SuiClient
5
+ * @typedef {import('@mysten/sui/grpc').SuiGrpcClient} SuiGrpcClient
4
6
  * @typedef {import('./EndlessVector.js').default} EndlessVector
5
7
  * @typedef {import('./EndlessVectorArchive.js').default} EndlessVectorArchive
6
8
  */
@@ -14,7 +16,7 @@ export default class EndlessVectorHistory {
14
16
  /**
15
17
  * Creates a new EndlessVectorHistory instance.
16
18
  * @param {Object} params - Configuration parameters
17
- * @param {SuiClient} [params.suiClient] - Sui client instance for blockchain interactions
19
+ * @param {SuiGrpcClient} [params.suiClient] - Sui gRPC client instance for blockchain interactions
18
20
  * @param {string} [params.id] - Unique identifier for this history item
19
21
  * @param {number} [params.index=0] - Index position of this history item in the sequence
20
22
  * @param {?Object} [params.fields] - Raw field data from the blockchain object
@@ -22,7 +24,7 @@ export default class EndlessVectorHistory {
22
24
  * @param {?EndlessVectorArchive} [params.endlessVectorArchive] - Reference to the parent EndlessVectorArchive instance
23
25
  */
24
26
  constructor(params = {}) {
25
- /** @type {SuiClient} */
27
+ /** @type {SuiGrpcClient} */
26
28
  this.suiClient = params.suiClient;
27
29
  /** @type {string} */
28
30
  this.id = params.id;
@@ -146,12 +148,14 @@ export default class EndlessVectorHistory {
146
148
  indexInItems = i - this.startsAt + 1;
147
149
  }
148
150
 
151
+ const context = { endlessVector: this._endlessVector, endlessVectorHistory: this };
152
+
149
153
  if (indexInItems < (this._fields.items.length - 1)) {
150
- return new Uint8Array(this._fields.items[indexInItems]);
154
+ return await EndlessVectorItem.fromGrpcJson(this._fields.items[indexInItems], context).bytes();
151
155
  } else if (indexInItems === (this._fields.items.length - 1)) {
152
156
  if (this.followedByNextBytes) {
153
157
  // if this item is child of archive, get suffix from next item of archive, otherwise from endless vector
154
- const suffix = this._endlessVectorArchive ?
158
+ const suffix = this._endlessVectorArchive ?
155
159
  (await this._endlessVectorArchive.getSuffixFromHistoryItemOfIndex(this.index + 1)) :
156
160
  (await this._endlessVector.getSuffixFromHistoryItemOfIndex(this.index + 1));
157
161
 
@@ -159,13 +163,11 @@ export default class EndlessVectorHistory {
159
163
  throw new Error('suffix bytes length mismatch');
160
164
  }
161
165
 
162
- const current = new Uint8Array(this._fields.items[indexInItems]);
163
- const combined = new Uint8Array(current.length + suffix.length);
164
- combined.set(current);
165
- combined.set(suffix, current.length);
166
- return combined;
166
+ const head = EndlessVectorItem.fromGrpcJson(this._fields.items[indexInItems], context);
167
+ const tail = new EndlessVectorItem({ type: 'bytes', bytes: suffix });
168
+ return EndlessVectorItem.concatBytes(head, tail);
167
169
  } else {
168
- return new Uint8Array(this._fields.items[indexInItems]);
170
+ return await EndlessVectorItem.fromGrpcJson(this._fields.items[indexInItems], context).bytes();
169
171
  }
170
172
  }
171
173
  }
@@ -178,10 +180,12 @@ export default class EndlessVectorHistory {
178
180
  * to the last item of the previous history segment.
179
181
  * @returns {Uint8Array} The suffix bytes, or empty array if none stored
180
182
  */
181
- getSuffixStoredBytes() {
183
+ async getSuffixStoredBytes() {
182
184
  if (this.firstItemIsFromPreviousHistory) {
183
- return new Uint8Array(this._fields.items[0]);
185
+ const context = { endlessVector: this._endlessVector, endlessVectorHistory: this };
186
+ return await EndlessVectorItem.fromGrpcJson(this._fields.items[0], context).bytes();
184
187
  }
185
188
  return new Uint8Array();
186
189
  }
190
+
187
191
  }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Represents a single item stored in an EndlessVector.
3
+ *
4
+ * Currently only bytes items are supported. Blob items (Walrus-stored payloads)
5
+ * are recognised and preserved but cannot yet be read — calling bytes() on a
6
+ * blob item throws. This design lets the rest of the SDK work today while
7
+ * leaving a clear extension point for blob support.
8
+ *
9
+ * gRPC JSON shape of EndlessWalrusItem:
10
+ * bytes item → { bytes: "<base64>", blob: null, meta: "<base64>" }
11
+ * blob item → { bytes: null, blob: {...}, meta: "<base64>" }
12
+ * empty item → { bytes: null, blob: null, meta: "<base64>" }
13
+ */
14
+ function decodeBase64(value) {
15
+ if (typeof Buffer !== 'undefined') {
16
+ return new Uint8Array(Buffer.from(value, 'base64'));
17
+ }
18
+ const binary = atob(value);
19
+ const bytes = new Uint8Array(binary.length);
20
+ for (let i = 0; i < binary.length; i += 1) {
21
+ bytes[i] = binary.charCodeAt(i);
22
+ }
23
+ return bytes;
24
+ }
25
+
26
+ export default class EndlessVectorItem {
27
+ /**
28
+ * @param {Object} params
29
+ * @param {'bytes'|'blob'|'empty'} params.type
30
+ * @param {Uint8Array|null} [params.bytes]
31
+ * @param {Object|null} [params.blobData] - raw gRPC blob fields, preserved for future use
32
+ * @param {Uint8Array} [params.meta]
33
+ * @param {import('./EndlessVector.js').default|null} [params.endlessVector] - parent EndlessVector instance
34
+ * @param {import('./EndlessVectorHistory.js').default|null} [params.endlessVectorHistory] - parent EndlessVectorHistory instance
35
+ */
36
+ constructor(params = {}) {
37
+ /** @type {'bytes'|'blob'|'empty'} */
38
+ this.type = params.type || 'empty';
39
+ /** @type {Uint8Array|null} */
40
+ this._bytes = params.bytes || null;
41
+ /** @type {Object|null} */
42
+ this._blobData = params.blobData || null;
43
+ /** @type {Uint8Array} */
44
+ this.meta = params.meta || new Uint8Array();
45
+ /** @type {import('./EndlessVector.js').default|null} */
46
+ this._endlessVector = params.endlessVector || null;
47
+ /** @type {import('./EndlessVectorHistory.js').default|null} */
48
+ this._endlessVectorHistory = params.endlessVectorHistory || null;
49
+ }
50
+
51
+ /** @returns {boolean} */
52
+ get isBytes() { return this.type === 'bytes'; }
53
+ /** @returns {boolean} */
54
+ get isBlob() { return this.type === 'blob'; }
55
+ /** @returns {boolean} */
56
+ get isEmpty() { return this.type === 'empty'; }
57
+
58
+ /**
59
+ * Returns the binary size of this item in bytes.
60
+ * For bytes items, returns the byte array length.
61
+ * For blob items, returns the size from on-chain Blob object data.
62
+ * @returns {number}
63
+ */
64
+ get size() {
65
+ if (this.type === 'bytes') return this._bytes?.length || 0;
66
+ if (this.type === 'blob') return parseInt(this._blobData?.size || 0);
67
+ return 0;
68
+ }
69
+
70
+ /**
71
+ * Returns the raw bytes payload.
72
+ * @returns {Uint8Array}
73
+ * @throws {Error} If the item is a blob (not yet supported) or empty
74
+ */
75
+ async bytes() {
76
+ if (this.type === 'bytes') return this._bytes;
77
+ if (this.type === 'blob') {
78
+ if (this._endlessVector?.walrus?.readBlobBytes) {
79
+ return await this._endlessVector.walrus.readBlobBytes(this._blobData);
80
+ }
81
+ throw new Error('Blob items require walrusClient or aggregatorUrl to be configured on the EndlessVector');
82
+ }
83
+ throw new Error('Item is empty');
84
+ }
85
+
86
+ /**
87
+ * Returns the raw gRPC blob fields for blob items (future Walrus support).
88
+ * @returns {Object|null}
89
+ */
90
+ blobData() {
91
+ return this._blobData;
92
+ }
93
+
94
+ /**
95
+ * Parses an EndlessWalrusItem from its gRPC JSON representation.
96
+ *
97
+ * Handles three historical wire formats:
98
+ * - gRPC JSON struct { bytes: "<base64>"|null, blob: {...}|null, meta: "<base64>" }
99
+ * - Legacy base64 string "<base64>"
100
+ * - Legacy plain number array [1, 2, 3]
101
+ *
102
+ * @param {Object|string|number[]|null} raw
103
+ * @param {Object} [context={}]
104
+ * @param {import('./EndlessVector.js').default|null} [context.endlessVector]
105
+ * @param {import('./EndlessVectorHistory.js').default|null} [context.endlessVectorHistory]
106
+ * @returns {EndlessVectorItem}
107
+ */
108
+ static fromGrpcJson(raw, context = {}) {
109
+ if (raw == null) {
110
+ return new EndlessVectorItem({ type: 'empty', ...context });
111
+ }
112
+
113
+ // Legacy: plain number array
114
+ if (Array.isArray(raw)) {
115
+ return new EndlessVectorItem({ type: 'bytes', bytes: new Uint8Array(raw), ...context });
116
+ }
117
+
118
+ // Legacy: bare base64 string (whole item serialised as base64)
119
+ if (typeof raw === 'string') {
120
+ return new EndlessVectorItem({ type: 'bytes', bytes: decodeBase64(raw), ...context });
121
+ }
122
+
123
+ // gRPC JSON struct
124
+ const meta = raw.meta
125
+ ? decodeBase64(raw.meta)
126
+ : new Uint8Array();
127
+
128
+ if (raw.blob != null) {
129
+ return new EndlessVectorItem({ type: 'blob', blobData: raw.blob, meta, ...context });
130
+ }
131
+
132
+ if (raw.bytes != null) {
133
+ const bytes = typeof raw.bytes === 'string'
134
+ ? decodeBase64(raw.bytes)
135
+ : new Uint8Array(raw.bytes);
136
+ return new EndlessVectorItem({ type: 'bytes', bytes, meta, ...context });
137
+ }
138
+
139
+ return new EndlessVectorItem({ type: 'empty', meta, ...context });
140
+ }
141
+
142
+ /**
143
+ * Concatenates two bytes items into one. Used when a split item spans two
144
+ * history segments (followed_by_next_bytes / firstItemIsFromPreviousHistory).
145
+ *
146
+ * @param {EndlessVectorItem} head
147
+ * @param {EndlessVectorItem} tail
148
+ * @returns {Uint8Array}
149
+ * @throws {Error} If either item is not a bytes item
150
+ */
151
+ static concatBytes(head, tail) {
152
+ if (head.type !== 'bytes' || tail.type !== 'bytes') {
153
+ throw new Error('concatBytes requires two bytes items');
154
+ }
155
+ const combined = new Uint8Array(head._bytes.length + tail._bytes.length);
156
+ combined.set(head._bytes);
157
+ combined.set(tail._bytes, head._bytes.length);
158
+ return combined;
159
+ }
160
+ }
@@ -0,0 +1,192 @@
1
+ import { Transaction } from '@mysten/sui/transactions';
2
+ import { fromHex } from '@mysten/sui/utils';
3
+ import { SessionKey } from '@mysten/seal';
4
+ import { gcm } from '@noble/ciphers/aes.js';
5
+ import { randomBytes } from '@noble/ciphers/utils.js';
6
+
7
+ /**
8
+ * @typedef {import('@mysten/seal').SealClient} SealClient
9
+ * @typedef {import('@mysten/seal').SessionKey} SessionKey_T
10
+ * @typedef {import('./EndlessVector.js').default} EndlessVector
11
+ */
12
+
13
+ const AES_KEY_BYTES = 32;
14
+ const AES_NONCE_BYTES = 12;
15
+ const AES_TAG_BYTES = 16;
16
+ const DEFAULT_TTL_MIN = 5;
17
+
18
+ /**
19
+ * Seal-layered encryption companion for EndlessVector.
20
+ * Attached as `endlessVector.seal` on every EndlessVector instance; only "enabled"
21
+ * when a sealClient is supplied at construction time.
22
+ *
23
+ * When enabled, the parent EndlessVector encrypts every pushed item with a per-vector
24
+ * AES-256-GCM key and decrypts on read. The AES key itself is Seal-encrypted scoped
25
+ * to the vector's object id and stored on-chain as `EndlessWalrusVector.seal_encrypted_key`.
26
+ * Access policy: `seal_approve_endless_vector_owner` (only the vector owner can decrypt
27
+ * the AES key — anyone else gets ciphertext only).
28
+ *
29
+ * AES payload layout: `nonce(12B) || ciphertext || tag(16B)` (28B overhead per item).
30
+ */
31
+ export default class EndlessVectorSeal {
32
+ /**
33
+ * @param {Object} params
34
+ * @param {EndlessVector} params.endlessVector - parent EndlessVector
35
+ * @param {SealClient} [params.sealClient]
36
+ * @param {SessionKey_T} [params.sessionKey] - optional pre-built SessionKey
37
+ * @param {any} [params.signer] - keypair/signer to mint a SessionKey when needed
38
+ * @param {number} [params.sealTtlMin=5] - SessionKey ttl in minutes
39
+ */
40
+ constructor(params = {}) {
41
+ /** @type {EndlessVector} */
42
+ this._endlessVector = params.endlessVector || null;
43
+ /** @type {?SealClient} */
44
+ this._sealClient = params.sealClient || null;
45
+ /** @type {?SessionKey_T} */
46
+ this._sessionKey = params.sessionKey || null;
47
+ /** @type {?any} */
48
+ this._signer = params.signer || null;
49
+ /** @type {number} */
50
+ this._ttlMin = params.sealTtlMin || DEFAULT_TTL_MIN;
51
+
52
+ /** @type {?Uint8Array} - cached plaintext AES key (after key unwrap) */
53
+ this._aesKey = null;
54
+ }
55
+
56
+ /** True iff a sealClient was supplied. Callers gate behavior on this. */
57
+ get isEnabled() {
58
+ return !!this._sealClient;
59
+ }
60
+
61
+ /** Generate a fresh AES-256 key — used at `create()` time when sealing a new vector. */
62
+ static generateAesKey() {
63
+ return randomBytes(AES_KEY_BYTES);
64
+ }
65
+
66
+ /** Cache a plaintext AES key (e.g. immediately after `create()` so the first push needn't unwrap). */
67
+ setAesKey(key) {
68
+ if (!(key instanceof Uint8Array) || key.length !== AES_KEY_BYTES) {
69
+ throw new Error(`seal: key must be a ${AES_KEY_BYTES}-byte Uint8Array`);
70
+ }
71
+ this._aesKey = key;
72
+ }
73
+
74
+ /**
75
+ * Seal-encrypt the AES key scoped to the vector's object id.
76
+ * Caller is responsible for storing the returned bytes on-chain via
77
+ * `set_seal_encrypted_key` on the vector.
78
+ *
79
+ * @param {Uint8Array} aesKey
80
+ * @returns {Promise<Uint8Array>} the Seal-encrypted (wrapped) key
81
+ */
82
+ async wrapAesKey(aesKey) {
83
+ this._assertEnabled();
84
+ const ev = this._endlessVector;
85
+ if (!ev._packageId) throw new Error('seal.wrapAesKey requires packageId on the vector');
86
+ if (!ev.id) throw new Error('seal.wrapAesKey requires the vector id (call after create())');
87
+
88
+ const idHex = EndlessVectorSeal._objectIdToHex(ev.id);
89
+ const { encryptedObject } = await this._sealClient.encrypt({
90
+ threshold: 1,
91
+ packageId: ev._packageId,
92
+ id: idHex,
93
+ data: aesKey,
94
+ });
95
+ return new Uint8Array(encryptedObject);
96
+ }
97
+
98
+ /** Encrypt a single item before push. */
99
+ async encryptItem(plaintext) {
100
+ this._assertEnabled();
101
+ const key = await this._ensureAesKey();
102
+ const nonce = randomBytes(AES_NONCE_BYTES);
103
+ const ct = gcm(key, nonce).encrypt(plaintext);
104
+ const out = new Uint8Array(AES_NONCE_BYTES + ct.length);
105
+ out.set(nonce, 0);
106
+ out.set(ct, AES_NONCE_BYTES);
107
+ return out;
108
+ }
109
+
110
+ /** Decrypt a single item after read. */
111
+ async decryptItem(payload) {
112
+ this._assertEnabled();
113
+ if (payload.length < AES_NONCE_BYTES + AES_TAG_BYTES) {
114
+ throw new Error(`seal.decryptItem: payload too short (${payload.length})`);
115
+ }
116
+ const key = await this._ensureAesKey();
117
+ const nonce = payload.subarray(0, AES_NONCE_BYTES);
118
+ const ct = payload.subarray(AES_NONCE_BYTES);
119
+ return gcm(key, nonce).decrypt(ct);
120
+ }
121
+
122
+ /**
123
+ * Resolve the plaintext AES key. If it's already cached, return it; otherwise fetch
124
+ * the wrapped key from the vector's on-chain state, build a PTB proving ownership via
125
+ * `seal_approve_endless_vector_owner`, and run `sealClient.decrypt`.
126
+ */
127
+ async _ensureAesKey() {
128
+ if (this._aesKey) return this._aesKey;
129
+
130
+ const ev = this._endlessVector;
131
+ await ev.initialize();
132
+ const wrapped = ev.sealEncryptedKey;
133
+ if (!wrapped) throw new Error('seal: vector has no seal_encrypted_key on-chain');
134
+
135
+ const idHex = EndlessVectorSeal._objectIdToHex(ev.id);
136
+ const tx = new Transaction();
137
+ tx.moveCall({
138
+ target: `${ev._packageId}::endless_walrus::seal_approve_endless_vector_owner`,
139
+ arguments: [
140
+ tx.pure.vector('u8', Array.from(fromHex(idHex))),
141
+ tx.object(ev.id),
142
+ ],
143
+ });
144
+
145
+ const senderAddress = this._senderAddress();
146
+ if (senderAddress) tx.setSender(senderAddress);
147
+ const txBytes = await tx.build({ client: ev.suiClient, onlyTransactionKind: true });
148
+
149
+ const sessionKey = await this._ensureSessionKey();
150
+ const aesKey = await this._sealClient.decrypt({
151
+ data: wrapped,
152
+ sessionKey,
153
+ txBytes,
154
+ });
155
+ this._aesKey = new Uint8Array(aesKey);
156
+ return this._aesKey;
157
+ }
158
+
159
+ async _ensureSessionKey() {
160
+ if (this._sessionKey && !this._sessionKey.isExpired?.()) return this._sessionKey;
161
+ if (!this._signer) throw new Error('seal: signer or sessionKey is required to mint a SessionKey');
162
+
163
+ const ev = this._endlessVector;
164
+ const senderAddress = this._senderAddress();
165
+ if (!senderAddress) throw new Error('seal: senderAddress is required to mint a SessionKey');
166
+
167
+ this._sessionKey = await SessionKey.create({
168
+ address: senderAddress,
169
+ packageId: ev._packageId,
170
+ ttlMin: this._ttlMin,
171
+ signer: this._signer,
172
+ suiClient: ev.suiClient,
173
+ });
174
+ return this._sessionKey;
175
+ }
176
+
177
+ _senderAddress() {
178
+ // Prefer an explicit walrus.senderAddress (already plumbed for blob writes);
179
+ // fall back to the suiClient address if present.
180
+ return this._endlessVector?.walrus?._senderAddress
181
+ ?? this._endlessVector?.suiClient?.address
182
+ ?? null;
183
+ }
184
+
185
+ _assertEnabled() {
186
+ if (!this.isEnabled) throw new Error('seal: sealClient not configured on this EndlessVector');
187
+ }
188
+
189
+ static _objectIdToHex(objectId) {
190
+ return String(objectId).replace(/^0x/, '').padStart(64, '0');
191
+ }
192
+ }