@le-space/browser 0.1.22 → 0.1.24

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 (3) hide show
  1. package/index.d.ts +136 -3
  2. package/index.js +250 -7
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -4,13 +4,146 @@ interface BrowserPackagePlan {
4
4
  modules: string[];
5
5
  }
6
6
  declare const BROWSER_PACKAGE_PLAN: BrowserPackagePlan;
7
+ type MessageStatus = 'processed' | 'pending' | 'rejected' | 'unknown';
8
+ interface BalanceResponse {
9
+ address: string;
10
+ balance: string;
11
+ locked_amount: string;
12
+ details?: Record<string, string>;
13
+ credit_balance: number;
14
+ }
15
+ interface CrnUsage {
16
+ cpu?: {
17
+ count?: number;
18
+ };
19
+ mem?: {
20
+ available_kB?: number;
21
+ };
22
+ disk?: {
23
+ available_kB?: number;
24
+ };
25
+ active?: boolean;
26
+ }
27
+ interface CrnLocation {
28
+ city?: string | null;
29
+ region?: string | null;
30
+ country?: string | null;
31
+ country_code?: string | null;
32
+ }
33
+ interface Crn {
34
+ hash: string;
35
+ name: string;
36
+ address: string;
37
+ score?: number | string | null;
38
+ performance?: number | string | null;
39
+ decentralization?: number | string | null;
40
+ qemu_support?: boolean;
41
+ confidential_support?: boolean;
42
+ gpu_support?: boolean;
43
+ system_usage?: CrnUsage | null;
44
+ payment_receiver_address?: string | null;
45
+ version?: string | null;
46
+ city?: string | null;
47
+ region?: string | null;
48
+ country?: string | null;
49
+ country_code?: string | null;
50
+ location?: CrnLocation | string | null;
51
+ resolved_ip?: string | null;
52
+ geo_source?: string | null;
53
+ }
54
+ interface CrnListResponse {
55
+ crns: Crn[];
56
+ }
57
+ type PaymentMode = 'hold' | 'credit';
58
+ interface InstanceMessage {
59
+ item_hash: string;
60
+ sender: string;
61
+ chain: string;
62
+ type: 'INSTANCE';
63
+ channel?: string;
64
+ content?: {
65
+ metadata?: {
66
+ name?: string;
67
+ };
68
+ payment?: {
69
+ type?: PaymentMode;
70
+ chain?: string;
71
+ };
72
+ rootfs?: {
73
+ parent?: {
74
+ ref?: string;
75
+ };
76
+ size_mib?: number;
77
+ };
78
+ requirements?: {
79
+ node?: {
80
+ node_hash?: string;
81
+ };
82
+ };
83
+ };
84
+ time?: string | number;
85
+ reception_time?: string;
86
+ confirmed?: boolean;
87
+ status?: string;
88
+ }
89
+ interface RootfsRequiredPortForward {
90
+ port: number;
91
+ tcp?: boolean;
92
+ udp?: boolean;
93
+ purpose?: string;
94
+ }
95
+ interface RootfsManifest {
96
+ profile?: string;
97
+ version: string;
98
+ rootfsInstallStrategy?: 'thin' | 'prebaked' | string;
99
+ requiresBootstrapNetwork?: boolean;
100
+ bootstrapSummary?: string;
101
+ requiredPortForwards?: RootfsRequiredPortForward[];
102
+ rootfsItemHash: string;
103
+ rootfsSizeMiB: number;
104
+ rootfsSourceSizeBytes?: number;
105
+ createdAt: string;
106
+ notes?: string;
107
+ }
108
+ interface RootfsManifestState {
109
+ manifest: RootfsManifest | null;
110
+ valid: boolean;
111
+ errors: string[];
112
+ }
113
+ type GatewayProbeStatus = 'reachable' | 'timeout' | 'error' | 'unavailable' | 'unknown';
114
+ interface RootfsResolution {
115
+ itemHash: string;
116
+ messageStatus: MessageStatus;
117
+ messageType: string | null;
118
+ cid: string | null;
119
+ receptionTime?: string | null;
120
+ rejectionErrorCode?: number | null;
121
+ rejectionReason?: string | null;
122
+ gatewayUrl: string | null;
123
+ gatewayStatus: GatewayProbeStatus;
124
+ gatewayError?: string | null;
125
+ }
7
126
 
8
127
  declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
9
128
 
10
- declare const BROWSER_ALEPH_API_MODULE = "planned";
129
+ declare const DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
130
+ declare const DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
131
+ declare function normalizeMessageStatus(status: unknown): MessageStatus;
132
+ declare function fetchBalance(address: string, apiHost?: string): Promise<BalanceResponse>;
133
+ declare function fetchCrns(url?: string): Promise<Crn[]>;
134
+ declare function fetchInstances(address: string, apiHost?: string): Promise<InstanceMessage[]>;
11
135
 
12
- declare const BROWSER_ROOTFS_MODULE = "planned";
136
+ declare const ITEM_HASH_RE: RegExp;
137
+ declare const DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
138
+ declare const DEFAULT_IPFS_GATEWAY_BASE_URL = "https://ipfs.aleph.cloud/ipfs/";
139
+ interface LoadRootfsManifestOptions {
140
+ baseUrl?: string | URL;
141
+ }
142
+ declare function validateRootfsManifest(manifest: RootfsManifest | null): RootfsManifestState;
143
+ declare function loadRootfsManifest(url?: string | URL, options?: LoadRootfsManifestOptions): Promise<RootfsManifestState>;
144
+ declare function verifyRootfsExists(itemHash: string, apiHost?: string): Promise<boolean>;
145
+ declare function resolveRootfsReference(itemHash: string, apiHost?: string, gatewayBaseUrl?: string): Promise<RootfsResolution | null>;
13
146
 
14
147
  declare const BROWSER_PRICING_MODULE = "planned";
15
148
 
16
- export { BROWSER_ALEPH_API_MODULE, BROWSER_PACKAGE_PLAN, BROWSER_PRICING_MODULE, BROWSER_ROOTFS_MODULE, type BrowserExtractionPhase, type BrowserPackagePlan, fetchWithTimeout };
149
+ export { BROWSER_PACKAGE_PLAN, BROWSER_PRICING_MODULE, type BalanceResponse, type BrowserExtractionPhase, type BrowserPackagePlan, type Crn, type CrnListResponse, type CrnLocation, type CrnUsage, DEFAULT_ALEPH_API_HOST, DEFAULT_CRN_LIST_URL, DEFAULT_IPFS_GATEWAY_BASE_URL, DEFAULT_ROOTFS_MANIFEST_URL, type GatewayProbeStatus, ITEM_HASH_RE, type InstanceMessage, type LoadRootfsManifestOptions, type MessageStatus, type PaymentMode, type RootfsManifest, type RootfsManifestState, type RootfsRequiredPortForward, type RootfsResolution, fetchBalance, fetchCrns, fetchInstances, fetchWithTimeout, loadRootfsManifest, normalizeMessageStatus, resolveRootfsReference, validateRootfsManifest, verifyRootfsExists };
package/index.js CHANGED
@@ -9,8 +9,8 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
9
9
  const controller = new AbortController();
10
10
  const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
11
11
  try {
12
- if (typeof input === "string" || input instanceof URL) {
13
- const url = new URL(String(input), globalThis.location?.href);
12
+ if (input instanceof URL) {
13
+ const url = new URL(input.toString());
14
14
  url.searchParams.set("_ts", String(Date.now()));
15
15
  return await fetch(url, {
16
16
  ...init,
@@ -18,6 +18,20 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
18
18
  signal: init.signal ?? controller.signal
19
19
  });
20
20
  }
21
+ if (typeof input === "string") {
22
+ let requestInput = input;
23
+ try {
24
+ const url = new URL(input, globalThis.location?.href);
25
+ url.searchParams.set("_ts", String(Date.now()));
26
+ requestInput = url;
27
+ } catch {
28
+ }
29
+ return await fetch(requestInput, {
30
+ ...init,
31
+ cache: init.cache ?? "no-store",
32
+ signal: init.signal ?? controller.signal
33
+ });
34
+ }
21
35
  return await fetch(input, {
22
36
  ...init,
23
37
  cache: init.cache ?? "no-store",
@@ -34,17 +48,246 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
34
48
  }
35
49
 
36
50
  // src/aleph-api.ts
37
- var BROWSER_ALEPH_API_MODULE = "planned";
51
+ var DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
52
+ var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
53
+ function normalizeMessageStatus(status) {
54
+ if (typeof status !== "string") return "unknown";
55
+ const normalized = status.toLowerCase();
56
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
57
+ return normalized;
58
+ }
59
+ return "unknown";
60
+ }
61
+ async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
62
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/addresses/${address}/balance`, {
63
+ cache: "no-cache"
64
+ });
65
+ if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
66
+ return await response.json();
67
+ }
68
+ async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
69
+ const requestUrl = new URL(url);
70
+ requestUrl.searchParams.set("filter_inactive", "true");
71
+ const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
72
+ if (!response.ok) throw new Error(`CRN list request failed: ${response.status}`);
73
+ const payload = await response.json();
74
+ return payload.crns ?? [];
75
+ }
76
+ async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
77
+ const url = new URL("/api/v0/messages.json", apiHost);
78
+ url.searchParams.set("msgTypes", "INSTANCE");
79
+ url.searchParams.set("addresses", address);
80
+ url.searchParams.set("message_statuses", "processed,pending,rejected,removing");
81
+ url.searchParams.set("pagination", "100");
82
+ url.searchParams.set("page", "1");
83
+ url.searchParams.set("sortOrder", "-1");
84
+ const response = await fetchWithTimeout(url, { cache: "no-cache" });
85
+ if (!response.ok) throw new Error(`Instance list request failed: ${response.status}`);
86
+ const payload = await response.json();
87
+ return (payload.messages ?? []).map((message) => ({
88
+ ...message,
89
+ status: typeof message.status === "string" && message.status.trim() ? message.status : message.confirmed ? "processed" : message.status
90
+ }));
91
+ }
38
92
 
39
93
  // src/rootfs.ts
40
- var BROWSER_ROOTFS_MODULE = "planned";
94
+ var ITEM_HASH_RE = /^[a-fA-F0-9]{64}$/u;
95
+ var DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
96
+ var DEFAULT_IPFS_GATEWAY_BASE_URL = "https://ipfs.aleph.cloud/ipfs/";
97
+ function resolveManifestUrl(input, baseUrl) {
98
+ if (input instanceof URL) return input;
99
+ try {
100
+ return new URL(input, baseUrl ? String(baseUrl) : globalThis.location?.href);
101
+ } catch {
102
+ return input;
103
+ }
104
+ }
105
+ function validateRootfsManifest(manifest) {
106
+ const errors = [];
107
+ if (!manifest) {
108
+ return { manifest, valid: false, errors: ["Rootfs manifest is missing."] };
109
+ }
110
+ if (!manifest.version) errors.push("Rootfs manifest version is missing.");
111
+ if (manifest.rootfsInstallStrategy != null && manifest.rootfsInstallStrategy !== "thin" && manifest.rootfsInstallStrategy !== "prebaked") {
112
+ errors.push('Rootfs install strategy must be "thin" or "prebaked" when provided.');
113
+ }
114
+ if (manifest.requiresBootstrapNetwork != null && typeof manifest.requiresBootstrapNetwork !== "boolean") {
115
+ errors.push("Rootfs bootstrap network flag must be a boolean when provided.");
116
+ }
117
+ if (manifest.bootstrapSummary != null && !manifest.bootstrapSummary.trim()) {
118
+ errors.push("Rootfs bootstrap summary must be non-empty when provided.");
119
+ }
120
+ if (manifest.requiredPortForwards != null) {
121
+ if (!Array.isArray(manifest.requiredPortForwards)) {
122
+ errors.push("Rootfs required port forwards must be an array when provided.");
123
+ } else {
124
+ manifest.requiredPortForwards.forEach((entry, index) => {
125
+ if (!entry || typeof entry !== "object") {
126
+ errors.push(`Rootfs required port forward #${index + 1} must be an object.`);
127
+ return;
128
+ }
129
+ if (!Number.isInteger(entry.port) || entry.port < 1 || entry.port > 65535) {
130
+ errors.push(`Rootfs required port forward #${index + 1} must use a TCP/UDP port between 1 and 65535.`);
131
+ }
132
+ if (entry.tcp !== true && entry.udp !== true) {
133
+ errors.push(`Rootfs required port forward #${index + 1} must enable TCP or UDP.`);
134
+ }
135
+ if (entry.purpose != null && (typeof entry.purpose !== "string" || !entry.purpose.trim())) {
136
+ errors.push(`Rootfs required port forward #${index + 1} purpose must be non-empty when provided.`);
137
+ }
138
+ });
139
+ }
140
+ }
141
+ if (!ITEM_HASH_RE.test(manifest.rootfsItemHash || "")) {
142
+ errors.push("Rootfs ItemHash must be a 64 character hex value.");
143
+ }
144
+ if (!Number.isInteger(manifest.rootfsSizeMiB) || manifest.rootfsSizeMiB <= 0) {
145
+ errors.push("Rootfs size must be a positive MiB integer.");
146
+ }
147
+ if (manifest.rootfsSourceSizeBytes != null && (!Number.isInteger(manifest.rootfsSourceSizeBytes) || manifest.rootfsSourceSizeBytes <= 0)) {
148
+ errors.push("Rootfs source size must be a positive byte integer when provided.");
149
+ }
150
+ if (!manifest.createdAt || Number.isNaN(new Date(manifest.createdAt).getTime())) {
151
+ errors.push("Rootfs creation date is missing or invalid.");
152
+ }
153
+ return { manifest, valid: errors.length === 0, errors };
154
+ }
155
+ async function loadRootfsManifest(url = DEFAULT_ROOTFS_MANIFEST_URL, options = {}) {
156
+ const response = await fetchWithTimeout(resolveManifestUrl(url, options.baseUrl), { cache: "no-cache" });
157
+ if (!response.ok) {
158
+ throw new Error(`Rootfs manifest request failed: ${response.status}`);
159
+ }
160
+ return validateRootfsManifest(await response.json());
161
+ }
162
+ async function verifyRootfsExists(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
163
+ if (!ITEM_HASH_RE.test(itemHash)) return false;
164
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
165
+ method: "GET",
166
+ cache: "no-cache"
167
+ });
168
+ if (response.status === 404) return false;
169
+ if (!response.ok) throw new Error(`Rootfs lookup failed: ${response.status}`);
170
+ const payload = await response.json();
171
+ const firstMessage = Array.isArray(payload.messages) ? payload.messages[0] : void 0;
172
+ const type = String(payload.type || payload.message?.type || firstMessage?.type || "").toUpperCase();
173
+ return type === "STORE";
174
+ }
175
+ function normalizeStatus(status) {
176
+ if (typeof status !== "string") return "unknown";
177
+ const normalized = status.toLowerCase();
178
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
179
+ return normalized;
180
+ }
181
+ return "unknown";
182
+ }
183
+ function parseCidFromPayload(payload) {
184
+ const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
185
+ const directContent = firstMessage?.content && typeof firstMessage.content === "object" ? firstMessage.content : null;
186
+ if (typeof directContent?.item_hash === "string") {
187
+ return directContent.item_hash;
188
+ }
189
+ if (typeof firstMessage?.item_content === "string") {
190
+ try {
191
+ const itemContent = JSON.parse(firstMessage.item_content);
192
+ if (typeof itemContent.item_hash === "string") {
193
+ return itemContent.item_hash;
194
+ }
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ function parseRejectionReason(payload) {
202
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
203
+ const details = payload.details && typeof payload.details === "object" ? payload.details : null;
204
+ const rawErrors = Array.isArray(details?.errors) ? details.errors : [];
205
+ const firstError = rawErrors[0] && typeof rawErrors[0] === "object" ? rawErrors[0] : null;
206
+ if (firstError) {
207
+ const accountBalance = Number(firstError.account_balance);
208
+ const requiredBalance = Number(firstError.required_balance);
209
+ if (Number.isFinite(accountBalance) && Number.isFinite(requiredBalance)) {
210
+ const shortfall = requiredBalance - accountBalance;
211
+ return {
212
+ rejectionErrorCode: errorCode,
213
+ rejectionReason: shortfall > 0 ? `Rejected by Aleph for insufficient hold balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short.` : `Rejected by Aleph for insufficient hold balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required.`
214
+ };
215
+ }
216
+ }
217
+ return {
218
+ rejectionErrorCode: errorCode,
219
+ rejectionReason: errorCode != null ? `Rejected by Aleph (error code ${errorCode}).` : null
220
+ };
221
+ }
222
+ async function probeGateway(cid, gatewayBaseUrl = DEFAULT_IPFS_GATEWAY_BASE_URL) {
223
+ const gatewayUrl = new URL(cid, gatewayBaseUrl).toString();
224
+ try {
225
+ const response = await fetchWithTimeout(gatewayUrl, { method: "HEAD", cache: "no-store" }, 5e3);
226
+ return {
227
+ gatewayUrl,
228
+ gatewayStatus: response.ok ? "reachable" : "error",
229
+ gatewayError: response.ok ? null : `Gateway responded with ${response.status}.`
230
+ };
231
+ } catch (error) {
232
+ if (error instanceof Error && error.message.includes("timed out")) {
233
+ return {
234
+ gatewayUrl,
235
+ gatewayStatus: "timeout",
236
+ gatewayError: error.message
237
+ };
238
+ }
239
+ return {
240
+ gatewayUrl,
241
+ gatewayStatus: "unavailable",
242
+ gatewayError: error instanceof Error ? error.message : String(error)
243
+ };
244
+ }
245
+ }
246
+ async function resolveRootfsReference(itemHash, apiHost = DEFAULT_ALEPH_API_HOST, gatewayBaseUrl = DEFAULT_IPFS_GATEWAY_BASE_URL) {
247
+ if (!ITEM_HASH_RE.test(itemHash)) return null;
248
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
249
+ method: "GET",
250
+ cache: "no-cache"
251
+ });
252
+ if (response.status === 404) return null;
253
+ if (!response.ok) throw new Error(`Rootfs lookup failed: ${response.status}`);
254
+ const payload = await response.json();
255
+ const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
256
+ const messageObject = payload.message && typeof payload.message === "object" ? payload.message : null;
257
+ const cid = parseCidFromPayload(payload);
258
+ const rejection = normalizeStatus(payload.status) === "rejected" ? parseRejectionReason(payload) : { rejectionErrorCode: null, rejectionReason: null };
259
+ const gateway = cid ? await probeGateway(cid, gatewayBaseUrl) : { gatewayUrl: null, gatewayStatus: "unknown", gatewayError: null };
260
+ return {
261
+ itemHash,
262
+ messageStatus: normalizeStatus(payload.status),
263
+ messageType: String(payload.type || messageObject?.type || firstMessage?.type || "").toUpperCase() || null,
264
+ cid,
265
+ receptionTime: typeof payload.reception_time === "string" ? payload.reception_time : null,
266
+ rejectionErrorCode: rejection.rejectionErrorCode,
267
+ rejectionReason: rejection.rejectionReason,
268
+ gatewayUrl: gateway.gatewayUrl,
269
+ gatewayStatus: gateway.gatewayStatus,
270
+ gatewayError: gateway.gatewayError
271
+ };
272
+ }
41
273
 
42
274
  // src/pricing.ts
43
275
  var BROWSER_PRICING_MODULE = "planned";
44
276
  export {
45
- BROWSER_ALEPH_API_MODULE,
46
277
  BROWSER_PACKAGE_PLAN,
47
278
  BROWSER_PRICING_MODULE,
48
- BROWSER_ROOTFS_MODULE,
49
- fetchWithTimeout
279
+ DEFAULT_ALEPH_API_HOST,
280
+ DEFAULT_CRN_LIST_URL,
281
+ DEFAULT_IPFS_GATEWAY_BASE_URL,
282
+ DEFAULT_ROOTFS_MANIFEST_URL,
283
+ ITEM_HASH_RE,
284
+ fetchBalance,
285
+ fetchCrns,
286
+ fetchInstances,
287
+ fetchWithTimeout,
288
+ loadRootfsManifest,
289
+ normalizeMessageStatus,
290
+ resolveRootfsReference,
291
+ validateRootfsManifest,
292
+ verifyRootfsExists
50
293
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/browser",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Shared browser-safe Aleph deployment and polling helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",