@le-space/browser 0.1.25 → 0.1.27

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 (4) hide show
  1. package/README.md +20 -0
  2. package/index.d.ts +75 -1
  3. package/index.js +176 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,25 @@ The package should stay UI-neutral. It should provide browser-safe helpers, but
13
13
  it should not own app-specific Svelte state, prepaid product logic, or wallet
14
14
  UX.
15
15
 
16
+ ## Client Surface
17
+
18
+ The preferred public entrypoint is a typed browser client factory:
19
+
20
+ - `createAlephBrowserClient({ apiHost?, crnListUrl? })`
21
+
22
+ That client should remain small and stable. It currently owns:
23
+
24
+ - balance lookup
25
+ - CRN listing
26
+ - instance listing
27
+ - message envelope lookup
28
+ - deployment result inspection and polling
29
+ - Aleph message broadcast helpers
30
+
31
+ Lower-level helper functions remain exported too, but new extractions should
32
+ prefer hanging reusable behavior off the client surface unless there is a good
33
+ reason to keep them as standalone utilities.
34
+
16
35
  ## Planned v1 Scope
17
36
 
18
37
  The first real extraction wave should cover:
@@ -23,6 +42,7 @@ The first real extraction wave should cover:
23
42
  - balance fetch
24
43
  - CRN fetch
25
44
  - instance listing
45
+ - typed browser client factory
26
46
  - Aleph message broadcast helpers
27
47
  - deployment polling and result inspection
28
48
  - runtime detail inspection
package/index.d.ts CHANGED
@@ -5,6 +5,9 @@ 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';
9
+ type AlephSenderChain = 'ETH';
10
+ type AlephMessageType = 'INSTANCE' | 'FORGET' | 'AGGREGATE';
8
11
  interface BalanceResponse {
9
12
  address: string;
10
13
  balance: string;
@@ -115,6 +118,66 @@ interface InstanceMessage {
115
118
  confirmed?: boolean;
116
119
  status?: string;
117
120
  }
121
+ interface AlephMessageEnvelope {
122
+ status?: unknown;
123
+ type?: unknown;
124
+ error_code?: unknown;
125
+ details?: unknown;
126
+ message?: {
127
+ type?: unknown;
128
+ } | null;
129
+ messages?: Array<{
130
+ type?: unknown;
131
+ }> | null;
132
+ [key: string]: unknown;
133
+ }
134
+ interface MessageReference {
135
+ itemHash: string;
136
+ status: ReferenceStatus;
137
+ type: string | null;
138
+ }
139
+ interface DeploymentInspectionResult {
140
+ status: MessageStatus;
141
+ errorCode: number | null;
142
+ details: Record<string, unknown> | null;
143
+ rejectionReason: string | null;
144
+ references: MessageReference[];
145
+ }
146
+ interface AlephBroadcastMessage {
147
+ sender: string;
148
+ chain: AlephSenderChain;
149
+ signature: string;
150
+ type: AlephMessageType;
151
+ item_hash: string;
152
+ item_type: 'inline';
153
+ item_content: string;
154
+ time: number;
155
+ channel: string;
156
+ }
157
+ interface AlephBroadcastResponse {
158
+ publication_status?: {
159
+ status: string;
160
+ failed?: unknown[];
161
+ };
162
+ message_status?: MessageStatus;
163
+ [key: string]: unknown;
164
+ }
165
+ interface BroadcastResult {
166
+ response: AlephBroadcastResponse;
167
+ httpStatus: number;
168
+ }
169
+ interface AlephBrowserClient {
170
+ apiHost: string;
171
+ crnListUrl: string;
172
+ fetchBalance(address: string): Promise<BalanceResponse>;
173
+ fetchCrns(): Promise<Crn[]>;
174
+ fetchInstances(address: string): Promise<InstanceMessage[]>;
175
+ fetchMessageEnvelope(itemHash: string): Promise<AlephMessageEnvelope | null>;
176
+ inspectDeploymentResult(itemHash: string, rootfsRef?: string): Promise<DeploymentInspectionResult>;
177
+ waitForDeploymentResult(itemHash: string, rootfsRef?: string, attempts?: number, delayMs?: number): Promise<DeploymentInspectionResult>;
178
+ broadcastInstanceMessage(message: AlephBroadcastMessage, sync?: boolean): Promise<BroadcastResult>;
179
+ broadcastAlephMessage(message: AlephBroadcastMessage, sync?: boolean): Promise<BroadcastResult>;
180
+ }
118
181
  interface RootfsRequiredPortForward {
119
182
  port: number;
120
183
  tcp?: boolean;
@@ -161,6 +224,17 @@ declare function normalizeMessageStatus(status: unknown): MessageStatus;
161
224
  declare function fetchBalance(address: string, apiHost?: string): Promise<BalanceResponse>;
162
225
  declare function fetchCrns(url?: string): Promise<Crn[]>;
163
226
  declare function fetchInstances(address: string, apiHost?: string): Promise<InstanceMessage[]>;
227
+ declare function fetchMessageEnvelope(itemHash: string, apiHost?: string): Promise<AlephMessageEnvelope | null>;
228
+ declare function inspectDeploymentResult(itemHash: string, rootfsRef?: string, apiHost?: string): Promise<DeploymentInspectionResult>;
229
+ declare function waitForDeploymentResult(itemHash: string, rootfsRef?: string, apiHost?: string, attempts?: number, delayMs?: number): Promise<DeploymentInspectionResult>;
230
+ declare function broadcastInstanceMessage(message: AlephBroadcastMessage, apiHost?: string, sync?: boolean): Promise<BroadcastResult>;
231
+ declare function broadcastAlephMessage(message: AlephBroadcastMessage, apiHost?: string, sync?: boolean): Promise<BroadcastResult>;
232
+
233
+ interface CreateAlephBrowserClientOptions {
234
+ apiHost?: string;
235
+ crnListUrl?: string;
236
+ }
237
+ declare function createAlephBrowserClient(options?: CreateAlephBrowserClientOptions): AlephBrowserClient;
164
238
 
165
239
  declare const ITEM_HASH_RE: RegExp;
166
240
  declare const DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
@@ -177,4 +251,4 @@ declare const DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e
177
251
  declare function parseInstancePricing(payload: unknown): InstancePricing;
178
252
  declare function fetchInstancePricing(apiHost?: string, aggregateAddress?: string): Promise<PricingState>;
179
253
 
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 };
254
+ export { type AlephBroadcastMessage, type AlephBroadcastResponse, type AlephBrowserClient, type AlephMessageEnvelope, type AlephMessageType, type AlephSenderChain, BROWSER_PACKAGE_PLAN, type BalanceResponse, type BroadcastResult, type BrowserExtractionPhase, type BrowserPackagePlan, type ComputeUnit, type CreateAlephBrowserClientOptions, 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, broadcastAlephMessage, broadcastInstanceMessage, createAlephBrowserClient, fetchBalance, fetchCrns, fetchInstancePricing, fetchInstances, fetchMessageEnvelope, fetchWithTimeout, inspectDeploymentResult, loadRootfsManifest, normalizeMessageStatus, parseInstancePricing, resolveRootfsReference, validateRootfsManifest, verifyRootfsExists, waitForDeploymentResult };
package/index.js CHANGED
@@ -89,6 +89,175 @@ 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
+ }
184
+ function isInvalidMessageFormatResponse(response, payload) {
185
+ if (response.status !== 422) return false;
186
+ const details = payload.details;
187
+ if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
188
+ if (details && typeof details === "object") {
189
+ const detailMessage = details.message;
190
+ if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
191
+ }
192
+ return false;
193
+ }
194
+ async function postBroadcastPayload(body, apiHost) {
195
+ const rawResponse = await fetchWithTimeout(`${apiHost}/api/v0/messages`, {
196
+ method: "POST",
197
+ headers: { "content-type": "application/json" },
198
+ body: JSON.stringify(body)
199
+ });
200
+ const response = await rawResponse.json().catch(() => ({}));
201
+ return {
202
+ response,
203
+ httpStatus: rawResponse.status,
204
+ rawResponse
205
+ };
206
+ }
207
+ async function broadcastInstanceMessage(message, apiHost = DEFAULT_ALEPH_API_HOST, sync = false) {
208
+ const attempts = [{ sync, message }, { ...message, sync }, { ...message }];
209
+ for (let index = 0; index < attempts.length; index += 1) {
210
+ const result = await postBroadcastPayload(attempts[index], apiHost);
211
+ if (result.rawResponse.ok || result.httpStatus === 202) {
212
+ return {
213
+ response: result.response,
214
+ httpStatus: result.httpStatus
215
+ };
216
+ }
217
+ const canRetry = index < attempts.length - 1 && isInvalidMessageFormatResponse(result.rawResponse, result.response);
218
+ if (!canRetry) {
219
+ throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response)}`);
220
+ }
221
+ }
222
+ throw new Error("Broadcast failed: no compatible request format was accepted");
223
+ }
224
+ async function broadcastAlephMessage(message, apiHost = DEFAULT_ALEPH_API_HOST, sync = false) {
225
+ return broadcastInstanceMessage(message, apiHost, sync);
226
+ }
227
+
228
+ // src/client.ts
229
+ function createAlephBrowserClient(options = {}) {
230
+ const apiHost = options.apiHost ?? DEFAULT_ALEPH_API_HOST;
231
+ const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
232
+ return {
233
+ apiHost,
234
+ crnListUrl,
235
+ fetchBalance(address) {
236
+ return fetchBalance(address, apiHost);
237
+ },
238
+ fetchCrns() {
239
+ return fetchCrns(crnListUrl);
240
+ },
241
+ fetchInstances(address) {
242
+ return fetchInstances(address, apiHost);
243
+ },
244
+ fetchMessageEnvelope(itemHash) {
245
+ return fetchMessageEnvelope(itemHash, apiHost);
246
+ },
247
+ inspectDeploymentResult(itemHash, rootfsRef) {
248
+ return inspectDeploymentResult(itemHash, rootfsRef, apiHost);
249
+ },
250
+ waitForDeploymentResult(itemHash, rootfsRef, attempts, delayMs) {
251
+ return waitForDeploymentResult(itemHash, rootfsRef, apiHost, attempts, delayMs);
252
+ },
253
+ broadcastInstanceMessage(message, sync) {
254
+ return broadcastInstanceMessage(message, apiHost, sync);
255
+ },
256
+ broadcastAlephMessage(message, sync) {
257
+ return broadcastAlephMessage(message, apiHost, sync);
258
+ }
259
+ };
260
+ }
92
261
 
93
262
  // src/rootfs.ts
94
263
  var ITEM_HASH_RE = /^[a-fA-F0-9]{64}$/u;
@@ -307,15 +476,21 @@ export {
307
476
  DEFAULT_IPFS_GATEWAY_BASE_URL,
308
477
  DEFAULT_ROOTFS_MANIFEST_URL,
309
478
  ITEM_HASH_RE,
479
+ broadcastAlephMessage,
480
+ broadcastInstanceMessage,
481
+ createAlephBrowserClient,
310
482
  fetchBalance,
311
483
  fetchCrns,
312
484
  fetchInstancePricing,
313
485
  fetchInstances,
486
+ fetchMessageEnvelope,
314
487
  fetchWithTimeout,
488
+ inspectDeploymentResult,
315
489
  loadRootfsManifest,
316
490
  normalizeMessageStatus,
317
491
  parseInstancePricing,
318
492
  resolveRootfsReference,
319
493
  validateRootfsManifest,
320
- verifyRootfsExists
494
+ verifyRootfsExists,
495
+ waitForDeploymentResult
321
496
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/browser",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Shared browser-safe Aleph deployment and polling helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",