@fizzyflow/endless-vector 0.0.7 → 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.
- package/EndlessVector.js +401 -142
- package/EndlessVectorArchive.js +31 -36
- package/EndlessVectorHistory.js +17 -13
- package/EndlessVectorItem.js +160 -0
- package/EndlessVectorSeal.js +192 -0
- package/EndlessVectorWalrus.js +467 -0
- package/README.md +253 -198
- package/ids.js +4 -4
- package/index.d.ts +181 -157
- package/index.js +5 -1
- package/package.json +16 -6
- package/test/base.test.js +388 -427
- package/test/base.test.txt +499 -0
- package/test/fixture.js +93 -0
- package/test/grpc_json_browser.test.js +22 -0
- package/test/helpers.js +2 -2
- package/test/helpers.txt +32 -0
- package/test/seal.test.js +319 -0
- package/test/walrus_blobs.test.js +178 -0
- package/test/walrus_blobs_extend.test.js +115 -0
- package/test/walrus_blobs_history.test.js +301 -0
- package/test/walrus_blobs_sdk.test.js +148 -0
- package/tsconfig.json +13 -0
- package/vitest.config.js +16 -0
package/EndlessVectorArchive.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import EndlessVectorHistory from './EndlessVectorHistory.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @typedef {import('@mysten/sui/
|
|
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 {
|
|
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 {
|
|
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?.
|
|
51
|
-
this.historyItemsCount = parseInt(fields?.history?.
|
|
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
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
package/EndlessVectorHistory.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
|
+
import EndlessVectorItem from './EndlessVectorItem.js';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
|
-
* @typedef {import('@mysten/sui/
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|