@blastlauncher/web 0.0.0-stage → 3.0.0-alpha.0
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/LICENSE +7 -0
- package/README.md +85 -2
- package/assets/almostnode.mjs +735 -0
- package/assets/blast-sources.json +1 -0
- package/assets/blast-web.js +35 -0
- package/assets/esbuild.mjs +2695 -0
- package/assets/esbuild.wasm +0 -0
- package/dist/browser-bundle.d.ts +54 -0
- package/dist/browser-bundle.js +441 -0
- package/dist/browser-install.d.ts +66 -0
- package/dist/browser-install.js +326 -0
- package/dist/bundle-config.d.ts +10 -0
- package/dist/bundle-config.js +31 -0
- package/dist/bundles.d.ts +29 -0
- package/dist/bundles.js +126 -0
- package/dist/capabilities.d.ts +66 -0
- package/dist/capabilities.js +288 -0
- package/dist/custom-element.d.ts +6 -0
- package/dist/custom-element.js +36 -0
- package/dist/default-renderer.d.ts +27 -0
- package/dist/default-renderer.js +145 -0
- package/dist/default-styles.d.ts +2 -0
- package/dist/default-styles.js +28 -0
- package/dist/extension-files.d.ts +21 -0
- package/dist/extension-files.js +86 -0
- package/dist/handlers.d.ts +18 -0
- package/dist/handlers.js +22 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/launch.d.ts +11 -0
- package/dist/launch.js +29 -0
- package/dist/prebuilt.d.ts +24 -0
- package/dist/prebuilt.js +112 -0
- package/dist/render.d.ts +62 -0
- package/dist/render.js +288 -0
- package/dist/runtime-port.d.ts +5 -0
- package/dist/runtime-port.js +8 -0
- package/dist/sandbox.d.ts +25 -0
- package/dist/sandbox.js +71 -0
- package/dist/session.d.ts +77 -0
- package/dist/session.js +520 -0
- package/dist/source.d.ts +16 -0
- package/dist/source.js +42 -0
- package/package.json +33 -4
- package/sandbox/almostnode-full.mjs +19713 -0
- package/sandbox/index.html +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { maxSatisfying, satisfies } from "semver";
|
|
2
|
+
import { supplied } from "@blastlauncher/protocol/dependency-policy";
|
|
3
|
+
export class BrowserInstallError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "BrowserInstallError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class BrowserIntegrityError extends BrowserInstallError {
|
|
12
|
+
packageName;
|
|
13
|
+
expected;
|
|
14
|
+
actual;
|
|
15
|
+
constructor(packageName, expected, actual) {
|
|
16
|
+
super("BROWSER_INTEGRITY", `Integrity check failed for ${packageName}`);
|
|
17
|
+
this.name = "BrowserIntegrityError";
|
|
18
|
+
this.packageName = packageName;
|
|
19
|
+
this.expected = expected;
|
|
20
|
+
this.actual = actual;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class BrowserSizeLimitError extends BrowserInstallError {
|
|
24
|
+
limit;
|
|
25
|
+
actual;
|
|
26
|
+
constructor(limit, actual) {
|
|
27
|
+
super("BROWSER_SIZE_LIMIT", `Browser dependency size limit exceeded (${actual} > ${limit} bytes)`);
|
|
28
|
+
this.name = "BrowserSizeLimitError";
|
|
29
|
+
this.limit = limit;
|
|
30
|
+
this.actual = actual;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class BrowserPackageLimitError extends BrowserInstallError {
|
|
34
|
+
limit;
|
|
35
|
+
constructor(limit) {
|
|
36
|
+
super("BROWSER_PACKAGE_LIMIT", `Browser dependency package limit exceeded (${limit})`);
|
|
37
|
+
this.name = "BrowserPackageLimitError";
|
|
38
|
+
this.limit = limit;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class BrowserNativePackageError extends BrowserInstallError {
|
|
42
|
+
packageName;
|
|
43
|
+
constructor(packageName) {
|
|
44
|
+
super("BROWSER_NATIVE_MODULE", `Native package is not supported in the browser: ${packageName}`);
|
|
45
|
+
this.name = "BrowserNativePackageError";
|
|
46
|
+
this.packageName = packageName;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class BrowserResolutionError extends BrowserInstallError {
|
|
50
|
+
packageName;
|
|
51
|
+
constructor(packageName, range) {
|
|
52
|
+
super("BROWSER_RESOLUTION", `Unable to resolve ${packageName}@${range} from the npm registry`);
|
|
53
|
+
this.name = "BrowserResolutionError";
|
|
54
|
+
this.packageName = packageName;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const DEFAULT_MAX_BYTES = 32 * 1024 * 1024;
|
|
58
|
+
const DEFAULT_MAX_PACKAGES = 128;
|
|
59
|
+
function packagePath(name) {
|
|
60
|
+
return `node_modules/${name}`;
|
|
61
|
+
}
|
|
62
|
+
function ancestorCandidates(parentPath, name) {
|
|
63
|
+
if (!parentPath)
|
|
64
|
+
return [packagePath(name)];
|
|
65
|
+
// Nearest first, exactly the order Node and bundleForBrowser resolve in: the
|
|
66
|
+
// parent's own node_modules, then each enclosing node_modules up to the root.
|
|
67
|
+
const result = [`${parentPath}/node_modules/${name}`];
|
|
68
|
+
let packageDirectory = parentPath;
|
|
69
|
+
while (packageDirectory && packageDirectory !== ".") {
|
|
70
|
+
const nodeModulesDirectory = packageDirectory.slice(0, packageDirectory.lastIndexOf("/"));
|
|
71
|
+
if (!nodeModulesDirectory)
|
|
72
|
+
break;
|
|
73
|
+
result.push(`${nodeModulesDirectory}/${name}`);
|
|
74
|
+
const separator = nodeModulesDirectory.lastIndexOf("/");
|
|
75
|
+
if (separator < 0)
|
|
76
|
+
break;
|
|
77
|
+
packageDirectory = nodeModulesDirectory.slice(0, separator);
|
|
78
|
+
}
|
|
79
|
+
result.push(packagePath(name));
|
|
80
|
+
return [...new Set(result)];
|
|
81
|
+
}
|
|
82
|
+
function lockedVersion(lock, path) {
|
|
83
|
+
return lock?.packages?.[path]?.version;
|
|
84
|
+
}
|
|
85
|
+
/** npm v1 lockfiles only nest dependency records, so only the root has a path here. */
|
|
86
|
+
function legacyLockVersion(lock, name) {
|
|
87
|
+
return lock?.dependencies?.[name]?.version;
|
|
88
|
+
}
|
|
89
|
+
function parseLockfile(lockfile) {
|
|
90
|
+
if (lockfile === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (typeof lockfile === "string")
|
|
93
|
+
return JSON.parse(lockfile);
|
|
94
|
+
return lockfile;
|
|
95
|
+
}
|
|
96
|
+
function decode(bytes) {
|
|
97
|
+
return new TextDecoder().decode(bytes);
|
|
98
|
+
}
|
|
99
|
+
function readString(bytes) {
|
|
100
|
+
return decode(bytes).replace(/\0.*$/, "").trim();
|
|
101
|
+
}
|
|
102
|
+
function octal(bytes) {
|
|
103
|
+
const value = readString(bytes).replace(/[^0-7].*$/, "");
|
|
104
|
+
return value ? Number.parseInt(value, 8) : 0;
|
|
105
|
+
}
|
|
106
|
+
export class BrowserTarPathError extends BrowserInstallError {
|
|
107
|
+
path;
|
|
108
|
+
constructor(path) {
|
|
109
|
+
super("BROWSER_TAR_PATH", `Unsafe path in npm archive: ${path}`);
|
|
110
|
+
this.name = "BrowserTarPathError";
|
|
111
|
+
this.path = path;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function normalTarPath(path) {
|
|
115
|
+
if (path.includes("\\") || path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path))
|
|
116
|
+
throw new BrowserTarPathError(path);
|
|
117
|
+
const parts = path.split("/");
|
|
118
|
+
if (parts.some((part) => part === ".."))
|
|
119
|
+
throw new BrowserTarPathError(path);
|
|
120
|
+
return parts.filter((part) => part !== "" && part !== ".").join("/");
|
|
121
|
+
}
|
|
122
|
+
/** Parses the small subset of POSIX tar needed for npm archives, including pax and ustar names. */
|
|
123
|
+
export function unpackTar(archive) {
|
|
124
|
+
const files = new Map();
|
|
125
|
+
let offset = 0;
|
|
126
|
+
let paxPath;
|
|
127
|
+
while (offset + 512 <= archive.length) {
|
|
128
|
+
const header = archive.subarray(offset, offset + 512);
|
|
129
|
+
offset += 512;
|
|
130
|
+
if (header.every((byte) => byte === 0))
|
|
131
|
+
break;
|
|
132
|
+
const size = octal(header.subarray(124, 136));
|
|
133
|
+
const type = header[156];
|
|
134
|
+
let name = readString(header.subarray(0, 100));
|
|
135
|
+
const prefix = readString(header.subarray(345, 500));
|
|
136
|
+
if (prefix)
|
|
137
|
+
name = `${prefix}/${name}`;
|
|
138
|
+
const body = archive.subarray(offset, offset + size);
|
|
139
|
+
offset += Math.ceil(size / 512) * 512;
|
|
140
|
+
if (type === 120) {
|
|
141
|
+
for (const record of decode(body).split("\n")) {
|
|
142
|
+
const equals = record.indexOf("=");
|
|
143
|
+
if (equals > 0) {
|
|
144
|
+
const key = record.slice(record.indexOf(" ") + 1, equals);
|
|
145
|
+
if (key === "path")
|
|
146
|
+
paxPath = record.slice(equals + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
name = normalTarPath(paxPath ?? name);
|
|
152
|
+
paxPath = undefined;
|
|
153
|
+
if (!name || type === 53 || type === 1 || type === 2 || name.endsWith("/"))
|
|
154
|
+
continue;
|
|
155
|
+
if (type === 0 || type === 48 || type === undefined)
|
|
156
|
+
files.set(name, body.slice());
|
|
157
|
+
}
|
|
158
|
+
return files;
|
|
159
|
+
}
|
|
160
|
+
async function gunzip(bytes) {
|
|
161
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
162
|
+
copy.set(bytes);
|
|
163
|
+
const stream = new Blob([copy.buffer]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
164
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
165
|
+
}
|
|
166
|
+
async function integrity(bytes) {
|
|
167
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
168
|
+
copy.set(bytes);
|
|
169
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-512", copy.buffer);
|
|
170
|
+
let binary = "";
|
|
171
|
+
for (const byte of new Uint8Array(digest))
|
|
172
|
+
binary += String.fromCharCode(byte);
|
|
173
|
+
return `sha512-${btoa(binary)}`;
|
|
174
|
+
}
|
|
175
|
+
export async function installForBrowser(options) {
|
|
176
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
177
|
+
const maxPackages = options.maxPackages ?? DEFAULT_MAX_PACKAGES;
|
|
178
|
+
const lock = parseLockfile(options.lockfile);
|
|
179
|
+
const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/$/, "");
|
|
180
|
+
const packages = new Map();
|
|
181
|
+
const versions = new Map();
|
|
182
|
+
const records = new Map();
|
|
183
|
+
let bytes = 0;
|
|
184
|
+
const addBytes = (count) => {
|
|
185
|
+
bytes += count;
|
|
186
|
+
if (bytes > maxBytes)
|
|
187
|
+
throw new BrowserSizeLimitError(maxBytes, bytes);
|
|
188
|
+
};
|
|
189
|
+
// One registry request per package name, however many dependents ask for it.
|
|
190
|
+
const packuments = new Map();
|
|
191
|
+
const packumentFor = (name, range) => {
|
|
192
|
+
let pending = packuments.get(name);
|
|
193
|
+
if (!pending) {
|
|
194
|
+
pending = options
|
|
195
|
+
.fetch(`${registry}/${name}`, {
|
|
196
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
197
|
+
})
|
|
198
|
+
.then((response) => {
|
|
199
|
+
if (!response.ok)
|
|
200
|
+
throw new BrowserResolutionError(name, range);
|
|
201
|
+
return response.json();
|
|
202
|
+
});
|
|
203
|
+
packuments.set(name, pending);
|
|
204
|
+
}
|
|
205
|
+
return pending;
|
|
206
|
+
};
|
|
207
|
+
async function install(name, range, parentPath) {
|
|
208
|
+
if (supplied(name))
|
|
209
|
+
return;
|
|
210
|
+
const packument = (await packumentFor(name, range));
|
|
211
|
+
// The nearest existing copy is the one the importer will load, so it alone
|
|
212
|
+
// decides: reuse it when it satisfies, otherwise nest a copy under the parent.
|
|
213
|
+
// A farther matching copy must not count; it would be shadowed at runtime.
|
|
214
|
+
const candidates = ancestorCandidates(parentPath, name);
|
|
215
|
+
const shadow = candidates.find((candidate) => records.has(candidate));
|
|
216
|
+
if (shadow) {
|
|
217
|
+
const record = records.get(shadow);
|
|
218
|
+
if (satisfies(record.version, range)) {
|
|
219
|
+
if (!record.active)
|
|
220
|
+
await record.promise;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Nothing is loaded yet, so follow the layout npm resolved for this
|
|
225
|
+
// extension. The nearest locked copy that satisfies the range is where Node
|
|
226
|
+
// would find it, including a nested copy when the root holds another major.
|
|
227
|
+
let path = shadow
|
|
228
|
+
? undefined
|
|
229
|
+
: candidates.find((candidate) => {
|
|
230
|
+
const version = lockedVersion(lock, candidate);
|
|
231
|
+
return version !== undefined && satisfies(version, range) && !records.has(candidate);
|
|
232
|
+
});
|
|
233
|
+
if (!path) {
|
|
234
|
+
if (shadow) {
|
|
235
|
+
// A shadowing copy forces nesting below the importer; a top-level
|
|
236
|
+
// duplicate range conflict nests below the root copy itself.
|
|
237
|
+
path = `${parentPath ?? packagePath(name)}/node_modules/${name}`;
|
|
238
|
+
}
|
|
239
|
+
else if (parentPath === undefined) {
|
|
240
|
+
path = packagePath(name);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const root = packagePath(name);
|
|
244
|
+
const rootVersion = lockedVersion(lock, root);
|
|
245
|
+
// The lockfile's root version is the copy npm hoisted for this importer;
|
|
246
|
+
// when it does not satisfy the range, nest instead of overwriting it.
|
|
247
|
+
path =
|
|
248
|
+
rootVersion !== undefined && !satisfies(rootVersion, range)
|
|
249
|
+
? `${parentPath}/node_modules/${name}`
|
|
250
|
+
: root;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
while (records.has(path)) {
|
|
254
|
+
const record = records.get(path);
|
|
255
|
+
if (satisfies(record.version, range)) {
|
|
256
|
+
if (!record.active)
|
|
257
|
+
await record.promise;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
path = `${path}/node_modules/${name}`;
|
|
261
|
+
}
|
|
262
|
+
const locked = lockedVersion(lock, path) ??
|
|
263
|
+
(path === packagePath(name) ? legacyLockVersion(lock, name) : undefined);
|
|
264
|
+
const version = locked !== undefined && satisfies(locked, range)
|
|
265
|
+
? locked
|
|
266
|
+
: maxSatisfying(Object.keys(packument.versions ?? {}), range);
|
|
267
|
+
if (!version || !satisfies(version, range))
|
|
268
|
+
throw new BrowserResolutionError(name, range);
|
|
269
|
+
const metadata = packument.versions?.[version];
|
|
270
|
+
const tarball = metadata?.dist?.tarball;
|
|
271
|
+
if (!metadata || !tarball)
|
|
272
|
+
throw new BrowserResolutionError(name, range);
|
|
273
|
+
if (records.size >= maxPackages)
|
|
274
|
+
throw new BrowserPackageLimitError(maxPackages);
|
|
275
|
+
let finish;
|
|
276
|
+
let fail;
|
|
277
|
+
const promise = new Promise((resolve, reject) => {
|
|
278
|
+
finish = resolve;
|
|
279
|
+
fail = reject;
|
|
280
|
+
});
|
|
281
|
+
const record = { name, version, promise, active: true };
|
|
282
|
+
records.set(path, record);
|
|
283
|
+
const task = (async () => {
|
|
284
|
+
try {
|
|
285
|
+
const archiveResponse = await options.fetch(tarball);
|
|
286
|
+
if (!archiveResponse.ok)
|
|
287
|
+
throw new BrowserResolutionError(name, version);
|
|
288
|
+
const archive = new Uint8Array(await archiveResponse.arrayBuffer());
|
|
289
|
+
addBytes(archive.byteLength);
|
|
290
|
+
const actual = await integrity(archive);
|
|
291
|
+
const expected = metadata.dist?.integrity;
|
|
292
|
+
const expectedSha512 = expected?.split(/\s+/).find((entry) => entry.startsWith("sha512-"));
|
|
293
|
+
if (expectedSha512 && expectedSha512 !== actual)
|
|
294
|
+
throw new BrowserIntegrityError(name, expectedSha512, actual);
|
|
295
|
+
const files = unpackTar(await gunzip(archive));
|
|
296
|
+
if ([...files.keys()].some((file) => file.endsWith(".node") || file === "binding.gyp" || file.endsWith("/binding.gyp")))
|
|
297
|
+
throw new BrowserNativePackageError(name);
|
|
298
|
+
for (const [file, content] of files) {
|
|
299
|
+
const clean = file.replace(/^package\/?/, "");
|
|
300
|
+
if (clean)
|
|
301
|
+
packages.set(`${path}/${clean}`, content);
|
|
302
|
+
}
|
|
303
|
+
if (!packages.has(`${path}/package.json`))
|
|
304
|
+
packages.set(`${path}/package.json`, JSON.stringify({ name, version }));
|
|
305
|
+
if (!versions.has(name) || path === packagePath(name))
|
|
306
|
+
versions.set(name, version);
|
|
307
|
+
const optionalPeers = new Set(Object.entries(metadata.peerDependenciesMeta ?? {})
|
|
308
|
+
.filter(([, value]) => value?.optional === true)
|
|
309
|
+
.map(([dependency]) => dependency));
|
|
310
|
+
const dependencies = Object.fromEntries(Object.entries({ ...metadata.dependencies, ...metadata.peerDependencies }).filter(([dependency]) => !optionalPeers.has(dependency)));
|
|
311
|
+
await Promise.all(Object.entries(dependencies).map(([dependency, dependencyRange]) => install(dependency, dependencyRange, path)));
|
|
312
|
+
record.active = false;
|
|
313
|
+
finish();
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
record.active = false;
|
|
317
|
+
fail(error);
|
|
318
|
+
}
|
|
319
|
+
})();
|
|
320
|
+
// Keep the promise exposed by the record independent of the async executor's return value.
|
|
321
|
+
void task;
|
|
322
|
+
return promise;
|
|
323
|
+
}
|
|
324
|
+
await Promise.all(Object.entries(options.manifest.dependencies ?? {}).map(([name, range]) => install(name, range)));
|
|
325
|
+
return { packages, versions, bytes };
|
|
326
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser bundling configuration shared by the prebuild and the in-browser
|
|
3
|
+
* path. Both must resolve the same runtime modules to the same in-memory
|
|
4
|
+
* files; keeping one list means a prebuilt bundle and a bundle the browser
|
|
5
|
+
* builds on demand cannot disagree about what is external.
|
|
6
|
+
*/
|
|
7
|
+
/** Runtime modules supplied once and shared by every prebuilt command bundle. */
|
|
8
|
+
export declare const sharedModules: string[];
|
|
9
|
+
/** In-memory paths for the same runtime modules. */
|
|
10
|
+
export declare const aliases: Record<string, string>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser bundling configuration shared by the prebuild and the in-browser
|
|
3
|
+
* path. Both must resolve the same runtime modules to the same in-memory
|
|
4
|
+
* files; keeping one list means a prebuilt bundle and a bundle the browser
|
|
5
|
+
* builds on demand cannot disagree about what is external.
|
|
6
|
+
*/
|
|
7
|
+
/** Runtime modules supplied once and shared by every prebuilt command bundle. */
|
|
8
|
+
export const sharedModules = [
|
|
9
|
+
"react",
|
|
10
|
+
"react/jsx-runtime",
|
|
11
|
+
"react/jsx-dev-runtime",
|
|
12
|
+
"@raycast/api",
|
|
13
|
+
"react-reconciler",
|
|
14
|
+
"scheduler",
|
|
15
|
+
"@blastlauncher/protocol",
|
|
16
|
+
];
|
|
17
|
+
/** In-memory paths for the same runtime modules. */
|
|
18
|
+
export const aliases = {
|
|
19
|
+
"@raycast/api": "/packages/runtime/src/api/index.ts",
|
|
20
|
+
"@blastlauncher/protocol": "/packages/protocol/src/index.ts",
|
|
21
|
+
react: "/node_modules/react/index.js",
|
|
22
|
+
"react/jsx-runtime": "/node_modules/react/jsx-runtime.js",
|
|
23
|
+
"react/jsx-dev-runtime": "/node_modules/react/jsx-dev-runtime.js",
|
|
24
|
+
"react-reconciler": "/node_modules/react-reconciler/index.js",
|
|
25
|
+
"react-reconciler/constants.js": "/node_modules/react-reconciler/constants.js",
|
|
26
|
+
scheduler: "/node_modules/scheduler/index.js",
|
|
27
|
+
"stream/promises": "/packages/web/sandbox/stream-promises.ts",
|
|
28
|
+
"node:stream/promises": "/packages/web/sandbox/stream-promises.ts",
|
|
29
|
+
"util/types": "/packages/web/sandbox/util-types.ts",
|
|
30
|
+
"node:util/types": "/packages/web/sandbox/util-types.ts",
|
|
31
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Preference } from "@blastlauncher/protocol/preference-fields";
|
|
2
|
+
import type { CommandMode } from "@blastlauncher/protocol";
|
|
3
|
+
export type CatalogArgument = {
|
|
4
|
+
name: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type CatalogManifest = {
|
|
8
|
+
name: string;
|
|
9
|
+
preferences: Preference[];
|
|
10
|
+
commands: Record<string, Preference[]>;
|
|
11
|
+
arguments?: Record<string, CatalogArgument[]>;
|
|
12
|
+
modes?: Record<string, CommandMode>;
|
|
13
|
+
};
|
|
14
|
+
export type CatalogBundle = {
|
|
15
|
+
path: string;
|
|
16
|
+
bytes: number;
|
|
17
|
+
sha256: string;
|
|
18
|
+
files?: Record<string, CatalogBundle>;
|
|
19
|
+
manifest?: CatalogManifest;
|
|
20
|
+
};
|
|
21
|
+
export type BundleIndex = {
|
|
22
|
+
buildSha: string;
|
|
23
|
+
bundles: Record<string, CatalogBundle>;
|
|
24
|
+
runtime?: CatalogBundle;
|
|
25
|
+
};
|
|
26
|
+
export declare function bundleKey(extension: string, command: string): string;
|
|
27
|
+
export declare function parseBundleIndex(value: unknown): BundleIndex;
|
|
28
|
+
export declare function sha256Hex(value: ArrayBuffer): Promise<string>;
|
|
29
|
+
export declare function bundleMatches(index: BundleIndex | undefined, buildSha: string | undefined, key: string, bytes: ArrayBuffer): Promise<boolean>;
|
package/dist/bundles.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { parsePreferences } from "@blastlauncher/protocol/preference-fields";
|
|
2
|
+
export function bundleKey(extension, command) {
|
|
3
|
+
return `${extension}/${command}`;
|
|
4
|
+
}
|
|
5
|
+
export function parseBundleIndex(value) {
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
throw new Error("Invalid browser bundle index");
|
|
8
|
+
const input = value;
|
|
9
|
+
if (typeof input.buildSha !== "string" || !input.bundles || typeof input.bundles !== "object")
|
|
10
|
+
throw new Error("Invalid browser bundle index shape");
|
|
11
|
+
const runtimeValue = input.runtime;
|
|
12
|
+
let runtime;
|
|
13
|
+
if (runtimeValue !== undefined) {
|
|
14
|
+
if (!runtimeValue || typeof runtimeValue !== "object")
|
|
15
|
+
throw new Error("Invalid runtime bundle");
|
|
16
|
+
const item = runtimeValue;
|
|
17
|
+
if (item.path !== "bundles/runtime.js" ||
|
|
18
|
+
typeof item.bytes !== "number" ||
|
|
19
|
+
!Number.isInteger(item.bytes) ||
|
|
20
|
+
typeof item.sha256 !== "string" ||
|
|
21
|
+
!/^[0-9a-f]{64}$/.test(item.sha256))
|
|
22
|
+
throw new Error("Invalid runtime bundle");
|
|
23
|
+
runtime = { path: item.path, bytes: item.bytes, sha256: item.sha256 };
|
|
24
|
+
}
|
|
25
|
+
const bundles = {};
|
|
26
|
+
for (const [key, entryValue] of Object.entries(input.bundles)) {
|
|
27
|
+
if (!entryValue || typeof entryValue !== "object")
|
|
28
|
+
throw new Error(`Invalid bundle entry: ${key}`);
|
|
29
|
+
const item = entryValue;
|
|
30
|
+
if (typeof item.path !== "string" ||
|
|
31
|
+
!/^bundles\/[^/]+\/[^/]+\.js$/.test(item.path) ||
|
|
32
|
+
typeof item.bytes !== "number" ||
|
|
33
|
+
!Number.isInteger(item.bytes) ||
|
|
34
|
+
item.bytes < 0 ||
|
|
35
|
+
typeof item.sha256 !== "string" ||
|
|
36
|
+
!/^[0-9a-f]{64}$/.test(item.sha256))
|
|
37
|
+
throw new Error(`Invalid bundle entry shape: ${key}`);
|
|
38
|
+
const files = {};
|
|
39
|
+
if (item.files !== undefined) {
|
|
40
|
+
if (!item.files || typeof item.files !== "object")
|
|
41
|
+
throw new Error(`Invalid bundle files: ${key}`);
|
|
42
|
+
for (const [name, fileValue] of Object.entries(item.files)) {
|
|
43
|
+
if (!fileValue || typeof fileValue !== "object")
|
|
44
|
+
throw new Error(`Invalid bundle file: ${key}/${name}`);
|
|
45
|
+
const file = fileValue;
|
|
46
|
+
if (typeof file.path !== "string" ||
|
|
47
|
+
!file.path.startsWith("bundles/") ||
|
|
48
|
+
typeof file.bytes !== "number" ||
|
|
49
|
+
!Number.isInteger(file.bytes) ||
|
|
50
|
+
file.bytes < 0 ||
|
|
51
|
+
typeof file.sha256 !== "string" ||
|
|
52
|
+
!/^[0-9a-f]{64}$/.test(file.sha256))
|
|
53
|
+
throw new Error(`Invalid bundle file shape: ${key}/${name}`);
|
|
54
|
+
files[name] = { path: file.path, bytes: file.bytes, sha256: file.sha256 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let manifest;
|
|
58
|
+
if (item.manifest !== undefined) {
|
|
59
|
+
if (!item.manifest || typeof item.manifest !== "object")
|
|
60
|
+
throw new Error(`Invalid bundle manifest: ${key}`);
|
|
61
|
+
const declared = item.manifest;
|
|
62
|
+
if (typeof declared.name !== "string" ||
|
|
63
|
+
!declared.commands ||
|
|
64
|
+
typeof declared.commands !== "object")
|
|
65
|
+
throw new Error(`Invalid bundle manifest shape: ${key}`);
|
|
66
|
+
const commands = {};
|
|
67
|
+
const modes = {};
|
|
68
|
+
const declaredModes = declared.modes;
|
|
69
|
+
if (declaredModes !== undefined && (!declaredModes || typeof declaredModes !== "object"))
|
|
70
|
+
throw new Error(`Invalid bundle manifest modes: ${key}`);
|
|
71
|
+
for (const [name, mode] of Object.entries((declaredModes ?? {}))) {
|
|
72
|
+
if (mode !== "view" &&
|
|
73
|
+
mode !== "no-view" &&
|
|
74
|
+
mode !== "menu-bar" &&
|
|
75
|
+
mode !== "model-provider")
|
|
76
|
+
throw new Error(`Invalid bundle manifest mode: ${key}/${name}`);
|
|
77
|
+
modes[name] = mode;
|
|
78
|
+
}
|
|
79
|
+
for (const [name, preferences] of Object.entries(declared.commands))
|
|
80
|
+
commands[name] = parsePreferences(preferences, `Bundle ${key} command ${name} preferences`);
|
|
81
|
+
for (const name of Object.keys(commands))
|
|
82
|
+
modes[name] ??= "view";
|
|
83
|
+
const argumentsByCommand = {};
|
|
84
|
+
const declaredArguments = declared.arguments;
|
|
85
|
+
if (declaredArguments !== undefined &&
|
|
86
|
+
(!declaredArguments || typeof declaredArguments !== "object"))
|
|
87
|
+
throw new Error(`Invalid bundle manifest arguments: ${key}`);
|
|
88
|
+
for (const [name, argumentsFor] of Object.entries((declaredArguments ?? {}))) {
|
|
89
|
+
if (!Array.isArray(argumentsFor))
|
|
90
|
+
throw new Error(`Invalid bundle manifest arguments: ${key}/${name}`);
|
|
91
|
+
argumentsByCommand[name] = argumentsFor.map((argument) => {
|
|
92
|
+
const entry = argument;
|
|
93
|
+
if (!entry || typeof entry.name !== "string" || typeof entry.required !== "boolean")
|
|
94
|
+
throw new Error(`Invalid bundle manifest argument: ${key}/${name}`);
|
|
95
|
+
return { name: entry.name, required: entry.required };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
manifest = {
|
|
99
|
+
name: declared.name,
|
|
100
|
+
preferences: parsePreferences(declared.preferences, `Bundle ${key} preferences`),
|
|
101
|
+
commands,
|
|
102
|
+
...(Object.keys(argumentsByCommand).length ? { arguments: argumentsByCommand } : {}),
|
|
103
|
+
...(Object.keys(modes).length ? { modes } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
bundles[key] = {
|
|
107
|
+
path: item.path,
|
|
108
|
+
bytes: item.bytes,
|
|
109
|
+
sha256: item.sha256,
|
|
110
|
+
...(Object.keys(files).length ? { files } : {}),
|
|
111
|
+
...(manifest ? { manifest } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { buildSha: input.buildSha, bundles, ...(runtime ? { runtime } : {}) };
|
|
115
|
+
}
|
|
116
|
+
export async function sha256Hex(value) {
|
|
117
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", value);
|
|
118
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
119
|
+
}
|
|
120
|
+
export async function bundleMatches(index, buildSha, key, bytes) {
|
|
121
|
+
const entry = index?.bundles[key];
|
|
122
|
+
return Boolean(entry &&
|
|
123
|
+
buildSha === index?.buildSha &&
|
|
124
|
+
entry.bytes === bytes.byteLength &&
|
|
125
|
+
(await sha256Hex(bytes)) === entry.sha256);
|
|
126
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The slice of the host catalog the capability handlers consult. */
|
|
2
|
+
export type CapabilityCatalog = {
|
|
3
|
+
extensions: {
|
|
4
|
+
name: string;
|
|
5
|
+
commands: {
|
|
6
|
+
name: string;
|
|
7
|
+
}[];
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
export type BrowserStorage = {
|
|
11
|
+
getItem(key: string): string | null;
|
|
12
|
+
setItem(key: string, value: string): void;
|
|
13
|
+
removeItem(key: string): void;
|
|
14
|
+
key(index: number): string | null;
|
|
15
|
+
readonly length: number;
|
|
16
|
+
};
|
|
17
|
+
export type ClipboardBridge = {
|
|
18
|
+
readText?: () => Promise<string>;
|
|
19
|
+
writeText?: (text: string) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export type CapabilityRequest = {
|
|
22
|
+
name: string;
|
|
23
|
+
params: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type CapabilityResult = {
|
|
26
|
+
ok: true;
|
|
27
|
+
value: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
error: {
|
|
31
|
+
code: "DENIED";
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export type CapabilityContext = {
|
|
36
|
+
extensionName: string;
|
|
37
|
+
commandName: string;
|
|
38
|
+
source: string;
|
|
39
|
+
};
|
|
40
|
+
export type BrowserCapabilityOptions = {
|
|
41
|
+
catalog: CapabilityCatalog;
|
|
42
|
+
storage?: BrowserStorage;
|
|
43
|
+
clipboard?: ClipboardBridge;
|
|
44
|
+
openUrl?: (url: string) => void;
|
|
45
|
+
launchCommand?: (source: string, command: string) => Promise<void>;
|
|
46
|
+
status?: (message: string, error?: boolean) => void;
|
|
47
|
+
openPreferences?: (scope: "extension" | "command", context: CapabilityContext) => void;
|
|
48
|
+
};
|
|
49
|
+
export type BrowserPreferences = {
|
|
50
|
+
extension: Record<string, JsonValue>;
|
|
51
|
+
commands: Record<string, Record<string, JsonValue>>;
|
|
52
|
+
};
|
|
53
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
54
|
+
[key: string]: JsonValue;
|
|
55
|
+
};
|
|
56
|
+
export declare function createBrowserPreferenceStore(extension: string, storage?: BrowserStorage): {
|
|
57
|
+
read: () => BrowserPreferences;
|
|
58
|
+
save: (next: BrowserPreferences) => void;
|
|
59
|
+
values(command: string): {
|
|
60
|
+
[x: string]: JsonValue;
|
|
61
|
+
};
|
|
62
|
+
update(extensionValues: Record<string, JsonValue>, command: string, commandValues: Record<string, JsonValue>): void;
|
|
63
|
+
};
|
|
64
|
+
/** Browser-backed capability handlers. All browser effects are injected for unit tests. */
|
|
65
|
+
export declare function createBrowserCapabilities(options: BrowserCapabilityOptions): (request: CapabilityRequest, context: CapabilityContext) => Promise<CapabilityResult>;
|
|
66
|
+
export {};
|