@le-space/browser 0.1.24 → 0.1.26

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 +62 -2
  2. package/index.js +126 -3
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -5,6 +5,7 @@ interface BrowserPackagePlan {
5
5
  }
6
6
  declare const BROWSER_PACKAGE_PLAN: BrowserPackagePlan;
7
7
  type MessageStatus = 'processed' | 'pending' | 'rejected' | 'unknown';
8
+ type ReferenceStatus = MessageStatus | 'missing';
8
9
  interface BalanceResponse {
9
10
  address: string;
10
11
  balance: string;
@@ -12,6 +13,35 @@ interface BalanceResponse {
12
13
  details?: Record<string, string>;
13
14
  credit_balance: number;
14
15
  }
16
+ interface Price {
17
+ payg?: string | number | null;
18
+ holding?: string | number | null;
19
+ fixed?: string | number | null;
20
+ credit?: string | number | null;
21
+ }
22
+ interface ComputeUnit {
23
+ vcpus: number;
24
+ memory_mib: number;
25
+ disk_mib: number;
26
+ }
27
+ interface Tier {
28
+ id: string;
29
+ compute_units: number;
30
+ vram?: number | null;
31
+ model?: string | null;
32
+ }
33
+ interface InstancePricing {
34
+ price: {
35
+ storage?: Price;
36
+ compute_unit?: Price;
37
+ };
38
+ compute_unit: ComputeUnit;
39
+ tiers: Tier[];
40
+ }
41
+ interface PricingState {
42
+ pricing: InstancePricing | null;
43
+ fetchedAt: number | null;
44
+ }
15
45
  interface CrnUsage {
16
46
  cpu?: {
17
47
  count?: number;
@@ -86,6 +116,31 @@ interface InstanceMessage {
86
116
  confirmed?: boolean;
87
117
  status?: string;
88
118
  }
119
+ interface AlephMessageEnvelope {
120
+ status?: unknown;
121
+ type?: unknown;
122
+ error_code?: unknown;
123
+ details?: unknown;
124
+ message?: {
125
+ type?: unknown;
126
+ } | null;
127
+ messages?: Array<{
128
+ type?: unknown;
129
+ }> | null;
130
+ [key: string]: unknown;
131
+ }
132
+ interface MessageReference {
133
+ itemHash: string;
134
+ status: ReferenceStatus;
135
+ type: string | null;
136
+ }
137
+ interface DeploymentInspectionResult {
138
+ status: MessageStatus;
139
+ errorCode: number | null;
140
+ details: Record<string, unknown> | null;
141
+ rejectionReason: string | null;
142
+ references: MessageReference[];
143
+ }
89
144
  interface RootfsRequiredPortForward {
90
145
  port: number;
91
146
  tcp?: boolean;
@@ -132,6 +187,9 @@ declare function normalizeMessageStatus(status: unknown): MessageStatus;
132
187
  declare function fetchBalance(address: string, apiHost?: string): Promise<BalanceResponse>;
133
188
  declare function fetchCrns(url?: string): Promise<Crn[]>;
134
189
  declare function fetchInstances(address: string, apiHost?: string): Promise<InstanceMessage[]>;
190
+ declare function fetchMessageEnvelope(itemHash: string, apiHost?: string): Promise<AlephMessageEnvelope | null>;
191
+ declare function inspectDeploymentResult(itemHash: string, rootfsRef?: string, apiHost?: string): Promise<DeploymentInspectionResult>;
192
+ declare function waitForDeploymentResult(itemHash: string, rootfsRef?: string, apiHost?: string, attempts?: number, delayMs?: number): Promise<DeploymentInspectionResult>;
135
193
 
136
194
  declare const ITEM_HASH_RE: RegExp;
137
195
  declare const DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
@@ -144,6 +202,8 @@ declare function loadRootfsManifest(url?: string | URL, options?: LoadRootfsMani
144
202
  declare function verifyRootfsExists(itemHash: string, apiHost?: string): Promise<boolean>;
145
203
  declare function resolveRootfsReference(itemHash: string, apiHost?: string, gatewayBaseUrl?: string): Promise<RootfsResolution | null>;
146
204
 
147
- declare const BROWSER_PRICING_MODULE = "planned";
205
+ declare const DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
206
+ declare function parseInstancePricing(payload: unknown): InstancePricing;
207
+ declare function fetchInstancePricing(apiHost?: string, aggregateAddress?: string): Promise<PricingState>;
148
208
 
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 };
209
+ export { type AlephMessageEnvelope, 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 DeploymentInspectionResult, type GatewayProbeStatus, ITEM_HASH_RE, type InstanceMessage, type InstancePricing, type LoadRootfsManifestOptions, type MessageReference, type MessageStatus, type PaymentMode, type Price, type PricingState, type ReferenceStatus, type RootfsManifest, type RootfsManifestState, type RootfsRequiredPortForward, type RootfsResolution, type Tier, fetchBalance, fetchCrns, fetchInstancePricing, fetchInstances, fetchMessageEnvelope, fetchWithTimeout, inspectDeploymentResult, loadRootfsManifest, normalizeMessageStatus, parseInstancePricing, resolveRootfsReference, validateRootfsManifest, verifyRootfsExists, waitForDeploymentResult };
package/index.js CHANGED
@@ -89,6 +89,98 @@ async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
89
89
  status: typeof message.status === "string" && message.status.trim() ? message.status : message.confirmed ? "processed" : message.status
90
90
  }));
91
91
  }
92
+ function messageTypeFromEnvelope(payload) {
93
+ if (!payload) return null;
94
+ const type = payload.type ?? payload.message?.type ?? (Array.isArray(payload.messages) ? payload.messages[0]?.type : void 0);
95
+ return typeof type === "string" ? type.toUpperCase() : null;
96
+ }
97
+ function extractReferenceHashes(details) {
98
+ if (!details || typeof details !== "object" || !("errors" in details)) return [];
99
+ const errors = details.errors;
100
+ if (!Array.isArray(errors)) return [];
101
+ return errors.filter((value) => typeof value === "string");
102
+ }
103
+ function describeRejectedDeployment(payload, references, rootfsRef) {
104
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
105
+ const pendingReferences = references.filter((reference) => reference.status === "pending");
106
+ const missingReferences = references.filter((reference) => reference.status === "missing");
107
+ const rootfsReference = references.find((reference) => reference.itemHash === rootfsRef);
108
+ if (rootfsReference?.status === "pending") {
109
+ return `Aleph rejected this deployment because the referenced rootfs STORE message ${rootfsReference.itemHash} is still pending and cannot yet be used by an instance. Wait for that STORE message to process, then deploy again.`;
110
+ }
111
+ if (pendingReferences.length > 0) {
112
+ return `Aleph rejected this deployment because referenced message(s) are still pending: ${pendingReferences.map((reference) => reference.itemHash).join(", ")}.`;
113
+ }
114
+ if (missingReferences.length > 0) {
115
+ return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
116
+ }
117
+ const referencedHashes = extractReferenceHashes(payload.details);
118
+ if (referencedHashes.length > 0) {
119
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
120
+ }
121
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
122
+ }
123
+ async function fetchMessageEnvelope(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
124
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
125
+ cache: "no-cache"
126
+ });
127
+ if (response.status === 404) return null;
128
+ if (!response.ok) throw new Error(`Message lookup failed: ${response.status}`);
129
+ return await response.json();
130
+ }
131
+ async function fetchReference(itemHash, apiHost) {
132
+ const payload = await fetchMessageEnvelope(itemHash, apiHost);
133
+ if (!payload) {
134
+ return {
135
+ itemHash,
136
+ status: "missing",
137
+ type: null
138
+ };
139
+ }
140
+ return {
141
+ itemHash,
142
+ status: normalizeMessageStatus(payload.status),
143
+ type: messageTypeFromEnvelope(payload)
144
+ };
145
+ }
146
+ async function inspectDeploymentResult(itemHash, rootfsRef, apiHost = DEFAULT_ALEPH_API_HOST) {
147
+ const payload = await fetchMessageEnvelope(itemHash, apiHost);
148
+ if (!payload) {
149
+ return {
150
+ status: "unknown",
151
+ errorCode: null,
152
+ details: null,
153
+ rejectionReason: `Deployment message ${itemHash} was not found on Aleph.`,
154
+ references: []
155
+ };
156
+ }
157
+ const relatedHashes = new Set(rootfsRef ? [rootfsRef] : []);
158
+ for (const referenceHash of extractReferenceHashes(payload.details)) {
159
+ relatedHashes.add(referenceHash);
160
+ }
161
+ const references = await Promise.all(Array.from(relatedHashes).map((hash) => fetchReference(hash, apiHost)));
162
+ const status = normalizeMessageStatus(payload.status);
163
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
164
+ const details = payload.details && typeof payload.details === "object" ? payload.details : null;
165
+ return {
166
+ status,
167
+ errorCode,
168
+ details,
169
+ rejectionReason: status === "rejected" ? describeRejectedDeployment(payload, references, rootfsRef) : null,
170
+ references
171
+ };
172
+ }
173
+ async function waitForDeploymentResult(itemHash, rootfsRef, apiHost = DEFAULT_ALEPH_API_HOST, attempts = 15, delayMs = 2e3) {
174
+ let lastResult = await inspectDeploymentResult(itemHash, rootfsRef, apiHost);
175
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
176
+ if (lastResult.status === "processed" || lastResult.status === "rejected") {
177
+ return lastResult;
178
+ }
179
+ await new Promise((resolve) => globalThis.setTimeout(resolve, delayMs));
180
+ lastResult = await inspectDeploymentResult(itemHash, rootfsRef, apiHost);
181
+ }
182
+ return lastResult;
183
+ }
92
184
 
93
185
  // src/rootfs.ts
94
186
  var ITEM_HASH_RE = /^[a-fA-F0-9]{64}$/u;
@@ -272,10 +364,36 @@ async function resolveRootfsReference(itemHash, apiHost = DEFAULT_ALEPH_API_HOST
272
364
  }
273
365
 
274
366
  // src/pricing.ts
275
- var BROWSER_PRICING_MODULE = "planned";
367
+ var DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
368
+ function parseInstancePricing(payload) {
369
+ const data = payload;
370
+ const pricing = data.data?.pricing ?? data.pricing;
371
+ const instance = pricing?.instance;
372
+ if (!instance?.price?.compute_unit || !instance.compute_unit || !Array.isArray(instance.tiers)) {
373
+ throw new Error("Aleph pricing aggregate does not contain instance pricing.");
374
+ }
375
+ return instance;
376
+ }
377
+ async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateAddress = DEFAULT_ALEPH_AGGREGATE_ADDRESS) {
378
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/aggregates/${aggregateAddress}.json?keys=pricing`, {
379
+ cache: "no-cache"
380
+ });
381
+ if (!response.ok) {
382
+ throw new Error(`Pricing aggregate request failed: ${response.status}`);
383
+ }
384
+ const payload = await response.json();
385
+ const pricingAggregate = payload.data?.pricing;
386
+ if (!pricingAggregate) {
387
+ throw new Error("Pricing aggregate response did not include a pricing key.");
388
+ }
389
+ return {
390
+ pricing: parseInstancePricing({ pricing: pricingAggregate }),
391
+ fetchedAt: Date.now()
392
+ };
393
+ }
276
394
  export {
277
395
  BROWSER_PACKAGE_PLAN,
278
- BROWSER_PRICING_MODULE,
396
+ DEFAULT_ALEPH_AGGREGATE_ADDRESS,
279
397
  DEFAULT_ALEPH_API_HOST,
280
398
  DEFAULT_CRN_LIST_URL,
281
399
  DEFAULT_IPFS_GATEWAY_BASE_URL,
@@ -283,11 +401,16 @@ export {
283
401
  ITEM_HASH_RE,
284
402
  fetchBalance,
285
403
  fetchCrns,
404
+ fetchInstancePricing,
286
405
  fetchInstances,
406
+ fetchMessageEnvelope,
287
407
  fetchWithTimeout,
408
+ inspectDeploymentResult,
288
409
  loadRootfsManifest,
289
410
  normalizeMessageStatus,
411
+ parseInstancePricing,
290
412
  resolveRootfsReference,
291
413
  validateRootfsManifest,
292
- verifyRootfsExists
414
+ verifyRootfsExists,
415
+ waitForDeploymentResult
293
416
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/browser",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Shared browser-safe Aleph deployment and polling helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",