@le-space/browser 0.1.23 → 0.1.25

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 +80 -3
  2. package/index.js +233 -7
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -12,6 +12,35 @@ interface BalanceResponse {
12
12
  details?: Record<string, string>;
13
13
  credit_balance: number;
14
14
  }
15
+ interface Price {
16
+ payg?: string | number | null;
17
+ holding?: string | number | null;
18
+ fixed?: string | number | null;
19
+ credit?: string | number | null;
20
+ }
21
+ interface ComputeUnit {
22
+ vcpus: number;
23
+ memory_mib: number;
24
+ disk_mib: number;
25
+ }
26
+ interface Tier {
27
+ id: string;
28
+ compute_units: number;
29
+ vram?: number | null;
30
+ model?: string | null;
31
+ }
32
+ interface InstancePricing {
33
+ price: {
34
+ storage?: Price;
35
+ compute_unit?: Price;
36
+ };
37
+ compute_unit: ComputeUnit;
38
+ tiers: Tier[];
39
+ }
40
+ interface PricingState {
41
+ pricing: InstancePricing | null;
42
+ fetchedAt: number | null;
43
+ }
15
44
  interface CrnUsage {
16
45
  cpu?: {
17
46
  count?: number;
@@ -86,6 +115,43 @@ interface InstanceMessage {
86
115
  confirmed?: boolean;
87
116
  status?: string;
88
117
  }
118
+ interface RootfsRequiredPortForward {
119
+ port: number;
120
+ tcp?: boolean;
121
+ udp?: boolean;
122
+ purpose?: string;
123
+ }
124
+ interface RootfsManifest {
125
+ profile?: string;
126
+ version: string;
127
+ rootfsInstallStrategy?: 'thin' | 'prebaked' | string;
128
+ requiresBootstrapNetwork?: boolean;
129
+ bootstrapSummary?: string;
130
+ requiredPortForwards?: RootfsRequiredPortForward[];
131
+ rootfsItemHash: string;
132
+ rootfsSizeMiB: number;
133
+ rootfsSourceSizeBytes?: number;
134
+ createdAt: string;
135
+ notes?: string;
136
+ }
137
+ interface RootfsManifestState {
138
+ manifest: RootfsManifest | null;
139
+ valid: boolean;
140
+ errors: string[];
141
+ }
142
+ type GatewayProbeStatus = 'reachable' | 'timeout' | 'error' | 'unavailable' | 'unknown';
143
+ interface RootfsResolution {
144
+ itemHash: string;
145
+ messageStatus: MessageStatus;
146
+ messageType: string | null;
147
+ cid: string | null;
148
+ receptionTime?: string | null;
149
+ rejectionErrorCode?: number | null;
150
+ rejectionReason?: string | null;
151
+ gatewayUrl: string | null;
152
+ gatewayStatus: GatewayProbeStatus;
153
+ gatewayError?: string | null;
154
+ }
89
155
 
90
156
  declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, timeoutMs?: number): Promise<Response>;
91
157
 
@@ -96,8 +162,19 @@ declare function fetchBalance(address: string, apiHost?: string): Promise<Balanc
96
162
  declare function fetchCrns(url?: string): Promise<Crn[]>;
97
163
  declare function fetchInstances(address: string, apiHost?: string): Promise<InstanceMessage[]>;
98
164
 
99
- declare const BROWSER_ROOTFS_MODULE = "planned";
165
+ declare const ITEM_HASH_RE: RegExp;
166
+ declare const DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
167
+ declare const DEFAULT_IPFS_GATEWAY_BASE_URL = "https://ipfs.aleph.cloud/ipfs/";
168
+ interface LoadRootfsManifestOptions {
169
+ baseUrl?: string | URL;
170
+ }
171
+ declare function validateRootfsManifest(manifest: RootfsManifest | null): RootfsManifestState;
172
+ declare function loadRootfsManifest(url?: string | URL, options?: LoadRootfsManifestOptions): Promise<RootfsManifestState>;
173
+ declare function verifyRootfsExists(itemHash: string, apiHost?: string): Promise<boolean>;
174
+ declare function resolveRootfsReference(itemHash: string, apiHost?: string, gatewayBaseUrl?: string): Promise<RootfsResolution | null>;
100
175
 
101
- declare const BROWSER_PRICING_MODULE = "planned";
176
+ declare const DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
177
+ declare function parseInstancePricing(payload: unknown): InstancePricing;
178
+ declare function fetchInstancePricing(apiHost?: string, aggregateAddress?: string): Promise<PricingState>;
102
179
 
103
- export { BROWSER_PACKAGE_PLAN, BROWSER_PRICING_MODULE, BROWSER_ROOTFS_MODULE, type BalanceResponse, type BrowserExtractionPhase, type BrowserPackagePlan, type Crn, type CrnListResponse, type CrnLocation, type CrnUsage, DEFAULT_ALEPH_API_HOST, DEFAULT_CRN_LIST_URL, type InstanceMessage, type MessageStatus, type PaymentMode, fetchBalance, fetchCrns, fetchInstances, fetchWithTimeout, normalizeMessageStatus };
180
+ export { BROWSER_PACKAGE_PLAN, type BalanceResponse, type BrowserExtractionPhase, type BrowserPackagePlan, type ComputeUnit, type Crn, type CrnListResponse, type CrnLocation, type CrnUsage, DEFAULT_ALEPH_AGGREGATE_ADDRESS, 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 InstancePricing, type LoadRootfsManifestOptions, type MessageStatus, type PaymentMode, type Price, type PricingState, type RootfsManifest, type RootfsManifestState, type RootfsRequiredPortForward, type RootfsResolution, type Tier, fetchBalance, fetchCrns, fetchInstancePricing, fetchInstances, fetchWithTimeout, loadRootfsManifest, normalizeMessageStatus, parseInstancePricing, 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",
@@ -77,19 +91,231 @@ async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
77
91
  }
78
92
 
79
93
  // src/rootfs.ts
80
- 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
+ }
81
273
 
82
274
  // src/pricing.ts
83
- var BROWSER_PRICING_MODULE = "planned";
275
+ var DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
276
+ function parseInstancePricing(payload) {
277
+ const data = payload;
278
+ const pricing = data.data?.pricing ?? data.pricing;
279
+ const instance = pricing?.instance;
280
+ if (!instance?.price?.compute_unit || !instance.compute_unit || !Array.isArray(instance.tiers)) {
281
+ throw new Error("Aleph pricing aggregate does not contain instance pricing.");
282
+ }
283
+ return instance;
284
+ }
285
+ async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateAddress = DEFAULT_ALEPH_AGGREGATE_ADDRESS) {
286
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/aggregates/${aggregateAddress}.json?keys=pricing`, {
287
+ cache: "no-cache"
288
+ });
289
+ if (!response.ok) {
290
+ throw new Error(`Pricing aggregate request failed: ${response.status}`);
291
+ }
292
+ const payload = await response.json();
293
+ const pricingAggregate = payload.data?.pricing;
294
+ if (!pricingAggregate) {
295
+ throw new Error("Pricing aggregate response did not include a pricing key.");
296
+ }
297
+ return {
298
+ pricing: parseInstancePricing({ pricing: pricingAggregate }),
299
+ fetchedAt: Date.now()
300
+ };
301
+ }
84
302
  export {
85
303
  BROWSER_PACKAGE_PLAN,
86
- BROWSER_PRICING_MODULE,
87
- BROWSER_ROOTFS_MODULE,
304
+ DEFAULT_ALEPH_AGGREGATE_ADDRESS,
88
305
  DEFAULT_ALEPH_API_HOST,
89
306
  DEFAULT_CRN_LIST_URL,
307
+ DEFAULT_IPFS_GATEWAY_BASE_URL,
308
+ DEFAULT_ROOTFS_MANIFEST_URL,
309
+ ITEM_HASH_RE,
90
310
  fetchBalance,
91
311
  fetchCrns,
312
+ fetchInstancePricing,
92
313
  fetchInstances,
93
314
  fetchWithTimeout,
94
- normalizeMessageStatus
315
+ loadRootfsManifest,
316
+ normalizeMessageStatus,
317
+ parseInstancePricing,
318
+ resolveRootfsReference,
319
+ validateRootfsManifest,
320
+ verifyRootfsExists
95
321
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/browser",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Shared browser-safe Aleph deployment and polling helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",