@mearl/preset 0.0.0-stage → 3.0.0-beta.1
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 +201 -0
- package/README.md +57 -2
- package/dist/browserPreset.d.ts +22 -0
- package/dist/browserPreset.js +52 -0
- package/dist/browserSandbox.d.ts +47 -0
- package/dist/browserSandbox.js +141 -0
- package/dist/browserTransfer.d.ts +42 -0
- package/dist/browserTransfer.js +102 -0
- package/dist/browserTransport.d.ts +8 -0
- package/dist/browserTransport.js +64 -0
- package/dist/browserViews.d.ts +44 -0
- package/dist/browserViews.js +204 -0
- package/dist/bundle.d.ts +59 -0
- package/dist/bundle.js +219 -0
- package/dist/bundleFetch.d.ts +6 -0
- package/dist/bundleFetch.js +42 -0
- package/dist/bundleStore.d.ts +70 -0
- package/dist/bundleStore.js +478 -0
- package/dist/bundles.d.ts +22 -0
- package/dist/bundles.js +12 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/installation.d.ts +24 -0
- package/dist/installation.js +116 -0
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.js +156 -0
- package/dist/nodeSnapshot.d.ts +38 -0
- package/dist/nodeSnapshot.js +136 -0
- package/dist/pageRuntime.d.ts +19 -0
- package/dist/pageRuntime.js +47 -0
- package/dist/prepare.d.ts +34 -0
- package/dist/prepare.js +112 -0
- package/dist/preset.d.ts +24 -0
- package/dist/preset.js +53 -0
- package/dist/requestUi.d.ts +59 -0
- package/dist/requestUi.js +102 -0
- package/dist/resolve.d.ts +5 -0
- package/dist/resolve.js +134 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/package.json +40 -3
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { satisfies, valid, validRange } from 'semver';
|
|
2
|
+
import { clonePresetDefaults } from './manifest.js';
|
|
3
|
+
export const MAX_BUNDLE_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
export const MAX_BUNDLE_MANIFEST_BYTES = 64 * 1024;
|
|
5
|
+
export const MAX_BROWSER_STORAGE_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
export const MAX_PAGE_RUNTIME_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
export const MAX_REQUEST_UI_BYTES = 64 * 1024;
|
|
8
|
+
export class BundleError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(`${code}: ${message}`);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = 'BundleError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function invalid(message) {
|
|
17
|
+
throw new BundleError('BUNDLE_INVALID', message);
|
|
18
|
+
}
|
|
19
|
+
function fields(value, names) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
21
|
+
Object.keys(value).some(key => !names.includes(key)))
|
|
22
|
+
invalid('Unexpected manifest structure');
|
|
23
|
+
}
|
|
24
|
+
function path(value) {
|
|
25
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_./-]{0,255}$/.test(value) ||
|
|
26
|
+
value.split('/').some(segment => !segment || segment === '.' || segment === '..')) {
|
|
27
|
+
invalid('Resource paths must stay inside the bundle directory');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function bundleSourceUrl(source) {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(source);
|
|
33
|
+
if (!['https:', 'http:', 'file:'].includes(url.protocol) || url.username || url.password)
|
|
34
|
+
invalid('Invalid bundle source');
|
|
35
|
+
url.hash = '';
|
|
36
|
+
return url.href;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
invalid('Expected an absolute HTTP(S)/file bundle source without credentials');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function validateBundleManifest(value, mearlApiVersion) {
|
|
43
|
+
let json;
|
|
44
|
+
try {
|
|
45
|
+
json = clonePresetDefaults(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
invalid('Expected a JSON manifest');
|
|
49
|
+
}
|
|
50
|
+
fields(json, ['schemaVersion', 'id', 'version', 'requires', 'resources', 'contributions']);
|
|
51
|
+
if (json.schemaVersion !== 1 || typeof json.id !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(json.id))
|
|
52
|
+
invalid('Invalid bundle identity');
|
|
53
|
+
if (typeof json.version !== 'string' || valid(json.version) !== json.version)
|
|
54
|
+
invalid('Expected an exact bundle version');
|
|
55
|
+
if (valid(mearlApiVersion) !== mearlApiVersion)
|
|
56
|
+
invalid('Expected an exact host API version');
|
|
57
|
+
if (json.requires !== undefined) {
|
|
58
|
+
fields(json.requires, ['mearlApi']);
|
|
59
|
+
const requirement = json.requires.mearlApi;
|
|
60
|
+
if (typeof requirement !== 'string' || !requirement.trim() || validRange(requirement) === null)
|
|
61
|
+
invalid('Invalid host API requirement');
|
|
62
|
+
if (!satisfies(mearlApiVersion, requirement))
|
|
63
|
+
throw new BundleError('BUNDLE_INCOMPATIBLE', `Requires host API ${requirement}`);
|
|
64
|
+
}
|
|
65
|
+
if (!json.resources || typeof json.resources !== 'object' || Array.isArray(json.resources))
|
|
66
|
+
invalid('Expected a resource map');
|
|
67
|
+
const resources = Object.entries(json.resources);
|
|
68
|
+
if (!resources.length || resources.length > 64)
|
|
69
|
+
invalid('A bundle requires 1–64 resources');
|
|
70
|
+
let total = 0;
|
|
71
|
+
for (const [name, resource] of resources) {
|
|
72
|
+
path(name);
|
|
73
|
+
fields(resource, ['sha256', 'byteLength']);
|
|
74
|
+
if (typeof resource.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(resource.sha256) ||
|
|
75
|
+
typeof resource.byteLength !== 'number' || !Number.isSafeInteger(resource.byteLength) || resource.byteLength < 0)
|
|
76
|
+
invalid('Invalid resource metadata');
|
|
77
|
+
total += resource.byteLength;
|
|
78
|
+
if (total > MAX_BUNDLE_BYTES)
|
|
79
|
+
throw new BundleError('BUNDLE_QUOTA', 'Bundle resources exceed 16 MiB');
|
|
80
|
+
}
|
|
81
|
+
if (json.contributions !== undefined) {
|
|
82
|
+
fields(json.contributions, ['appProfiles', 'pageRuntimes', 'nodeRuntimes', 'browserRuntimes', 'requestUi', 'browserViews']);
|
|
83
|
+
if (json.contributions.requestUi !== undefined && (typeof json.contributions.requestUi !== 'string' || !Object.hasOwn(json.resources, json.contributions.requestUi)))
|
|
84
|
+
invalid('Request UI must reference a declared resource');
|
|
85
|
+
if (typeof json.contributions.requestUi === 'string' && json.resources[json.contributions.requestUi].byteLength > MAX_REQUEST_UI_BYTES)
|
|
86
|
+
throw new BundleError('BUNDLE_QUOTA', 'Request UI exceeds 64 KiB');
|
|
87
|
+
if (json.contributions.browserViews !== undefined) {
|
|
88
|
+
fields(json.contributions.browserViews, ['requestImport', 'requestCases', 'captureLauncher', 'captureController']);
|
|
89
|
+
for (const resource of Object.values(json.contributions.browserViews)) {
|
|
90
|
+
if (typeof resource !== 'string' || !Object.hasOwn(json.resources, resource))
|
|
91
|
+
invalid('Browser views must reference declared resources');
|
|
92
|
+
if (json.resources[resource].byteLength > MAX_REQUEST_UI_BYTES)
|
|
93
|
+
throw new BundleError('BUNDLE_QUOTA', 'Browser view definition exceeds 64 KiB');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const profiles = json.contributions.appProfiles;
|
|
97
|
+
if (!Array.isArray(profiles) || profiles.some(name => typeof name !== 'string' || !Object.hasOwn(json.resources, name)) ||
|
|
98
|
+
new Set(profiles).size !== profiles.length)
|
|
99
|
+
invalid('App profiles must reference distinct declared resources');
|
|
100
|
+
const runtimes = json.contributions.pageRuntimes;
|
|
101
|
+
if (runtimes !== undefined) {
|
|
102
|
+
if (!runtimes || typeof runtimes !== 'object' || Array.isArray(runtimes) || Object.entries(runtimes).some(([id, name]) => !/^[a-z][a-z0-9-]{0,79}$/.test(id) || typeof name !== 'string' || !Object.hasOwn(json.resources, name)))
|
|
103
|
+
invalid('Page runtimes must reference declared resources');
|
|
104
|
+
for (const name of Object.values(runtimes)) {
|
|
105
|
+
const resource = json.resources[name];
|
|
106
|
+
if (resource.byteLength > MAX_PAGE_RUNTIME_BYTES)
|
|
107
|
+
throw new BundleError('BUNDLE_QUOTA', 'Page runtime exceeds 2 MiB');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const kind of ['nodeRuntimes', 'browserRuntimes']) {
|
|
111
|
+
const runtimes = json.contributions[kind];
|
|
112
|
+
const label = kind === 'nodeRuntimes' ? 'Node' : 'Browser';
|
|
113
|
+
if (runtimes === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
if (!runtimes || typeof runtimes !== 'object' || Array.isArray(runtimes) || Object.keys(runtimes).length > 16)
|
|
116
|
+
invalid(`Invalid ${label} runtime declarations`);
|
|
117
|
+
for (const [host, declaration] of Object.entries(runtimes)) {
|
|
118
|
+
if (!/^[a-z][a-z0-9-]{0,79}$/.test(host))
|
|
119
|
+
invalid(`Invalid ${label} runtime host`);
|
|
120
|
+
fields(declaration, ['entry', 'bindings']);
|
|
121
|
+
if (typeof declaration.entry !== 'string' || !Object.hasOwn(json.resources, declaration.entry) || !declaration.entry.endsWith('.cjs'))
|
|
122
|
+
invalid(`${label} runtime entry must reference a declared CommonJS resource`);
|
|
123
|
+
if (!declaration.bindings || typeof declaration.bindings !== 'object' || Array.isArray(declaration.bindings) || Object.keys(declaration.bindings).length > 256 || Object.entries(declaration.bindings).some(([hook, handler]) => !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(hook) || typeof handler !== 'string' || !/^[a-z][a-zA-Z0-9_.-]{0,127}$/.test(handler)))
|
|
124
|
+
invalid(`Invalid ${label} runtime bindings`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (new TextEncoder().encode(JSON.stringify(json)).byteLength > MAX_BUNDLE_MANIFEST_BYTES)
|
|
129
|
+
throw new BundleError('BUNDLE_QUOTA', 'Manifest exceeds 64 KiB');
|
|
130
|
+
return json;
|
|
131
|
+
}
|
|
132
|
+
export async function bundleDigest(bytes) {
|
|
133
|
+
const digest = await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes).buffer);
|
|
134
|
+
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
}
|
|
136
|
+
async function manifestRevision(source, manifest) {
|
|
137
|
+
return bundleDigest(new TextEncoder().encode(JSON.stringify({ source, manifest })));
|
|
138
|
+
}
|
|
139
|
+
export function readBundleJson(bundle, name) {
|
|
140
|
+
if (!Object.hasOwn(bundle.resources, name))
|
|
141
|
+
invalid('Bundle resource is unavailable');
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bundle.resources[name]));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
invalid('Expected a UTF-8 JSON resource');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function readBundleText(bundle, name) {
|
|
150
|
+
if (!Object.hasOwn(bundle.resources, name))
|
|
151
|
+
invalid('Bundle resource is unavailable');
|
|
152
|
+
try {
|
|
153
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bundle.resources[name]);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
invalid('Expected a UTF-8 text resource');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export async function prepareBundle(source, options) {
|
|
160
|
+
options.signal?.throwIfAborted();
|
|
161
|
+
source = bundleSourceUrl(source);
|
|
162
|
+
if (options.version !== undefined && (!options.version.trim() || validRange(options.version) === null))
|
|
163
|
+
invalid('Invalid requested bundle version');
|
|
164
|
+
const bytes = Uint8Array.from(await options.load(source, { maxBytes: MAX_BUNDLE_MANIFEST_BYTES, signal: options.signal }));
|
|
165
|
+
options.signal?.throwIfAborted();
|
|
166
|
+
if (bytes.byteLength > MAX_BUNDLE_MANIFEST_BYTES)
|
|
167
|
+
throw new BundleError('BUNDLE_QUOTA', 'Manifest exceeds 64 KiB');
|
|
168
|
+
let value;
|
|
169
|
+
try {
|
|
170
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
invalid('Expected a UTF-8 JSON manifest');
|
|
174
|
+
}
|
|
175
|
+
const manifest = validateBundleManifest(value, options.mearlApiVersion);
|
|
176
|
+
if (manifest.id !== options.expectedId)
|
|
177
|
+
invalid('Bundle identity does not match the requested package');
|
|
178
|
+
if (options.version && !satisfies(manifest.version, options.version))
|
|
179
|
+
throw new BundleError('BUNDLE_INCOMPATIBLE', 'Bundle version does not satisfy the requested range');
|
|
180
|
+
const resources = {};
|
|
181
|
+
for (const [name, expected] of Object.entries(manifest.resources)) {
|
|
182
|
+
options.signal?.throwIfAborted();
|
|
183
|
+
const resource = Uint8Array.from(await options.load(new URL(name, source).href, { maxBytes: expected.byteLength, signal: options.signal }));
|
|
184
|
+
options.signal?.throwIfAborted();
|
|
185
|
+
if (resource.byteLength !== expected.byteLength || await bundleDigest(resource) !== expected.sha256) {
|
|
186
|
+
throw new BundleError('BUNDLE_INTEGRITY', `Resource integrity check failed: ${name}`);
|
|
187
|
+
}
|
|
188
|
+
resources[name] = resource;
|
|
189
|
+
}
|
|
190
|
+
options.signal?.throwIfAborted();
|
|
191
|
+
return { source, manifest, revision: await manifestRevision(source, manifest), resources };
|
|
192
|
+
}
|
|
193
|
+
/** Revalidates persisted snapshots without network access and returns independent resource buffers. */
|
|
194
|
+
export async function verifyBundle(bundle, mearlApiVersion) {
|
|
195
|
+
const source = bundleSourceUrl(bundle.source);
|
|
196
|
+
const manifest = validateBundleManifest(bundle.manifest, mearlApiVersion);
|
|
197
|
+
const revision = bundle.revision;
|
|
198
|
+
if (!bundle.resources || typeof bundle.resources !== 'object' ||
|
|
199
|
+
Object.keys(bundle.resources).length !== Object.keys(manifest.resources).length)
|
|
200
|
+
invalid('Stored resource set does not match the manifest');
|
|
201
|
+
const resources = {};
|
|
202
|
+
for (const [name, expected] of Object.entries(manifest.resources)) {
|
|
203
|
+
const original = bundle.resources[name];
|
|
204
|
+
if (!(original instanceof Uint8Array))
|
|
205
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Stored resource is not a byte buffer');
|
|
206
|
+
const bytes = Uint8Array.from(original);
|
|
207
|
+
if (bytes.byteLength !== expected.byteLength) {
|
|
208
|
+
throw new BundleError('BUNDLE_INTEGRITY', `Stored resource integrity check failed: ${name}`);
|
|
209
|
+
}
|
|
210
|
+
resources[name] = bytes;
|
|
211
|
+
}
|
|
212
|
+
if (await manifestRevision(source, manifest) !== revision)
|
|
213
|
+
throw new BundleError('BUNDLE_INTEGRITY', 'Stored manifest revision does not match');
|
|
214
|
+
for (const [name, expected] of Object.entries(manifest.resources)) {
|
|
215
|
+
if (await bundleDigest(resources[name]) !== expected.sha256)
|
|
216
|
+
throw new BundleError('BUNDLE_INTEGRITY', `Stored resource integrity check failed: ${name}`);
|
|
217
|
+
}
|
|
218
|
+
return { source, manifest, revision, resources };
|
|
219
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BundleLoader } from './bundle.js';
|
|
2
|
+
/** Source trust and browser host permission checks belong to the caller's fetch implementation. */
|
|
3
|
+
export declare function createBundleFetchLoader(options?: {
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
credentials?: RequestCredentials;
|
|
6
|
+
}): BundleLoader;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BundleError } from './bundle.js';
|
|
2
|
+
/** Source trust and browser host permission checks belong to the caller's fetch implementation. */
|
|
3
|
+
export function createBundleFetchLoader(options = {}) {
|
|
4
|
+
return async (source, { maxBytes, signal }) => {
|
|
5
|
+
const response = await (options.fetch ?? fetch)(source, {
|
|
6
|
+
credentials: options.credentials ?? 'omit', redirect: 'error', cache: 'no-store', signal,
|
|
7
|
+
});
|
|
8
|
+
if (!response.ok || response.redirected)
|
|
9
|
+
throw new BundleError('BUNDLE_INVALID', 'Bundle download failed or redirected');
|
|
10
|
+
if (!response.body)
|
|
11
|
+
return new Uint8Array();
|
|
12
|
+
const reader = response.body.getReader();
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let total = 0;
|
|
15
|
+
try {
|
|
16
|
+
while (true) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
const { done, value } = await reader.read();
|
|
19
|
+
if (done)
|
|
20
|
+
break;
|
|
21
|
+
total += value.byteLength;
|
|
22
|
+
if (total > maxBytes)
|
|
23
|
+
throw new BundleError('BUNDLE_QUOTA', 'Download exceeds its declared size limit');
|
|
24
|
+
chunks.push(value.slice());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
await reader.cancel().catch(() => { });
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
const bytes = new Uint8Array(total);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (const chunk of chunks) {
|
|
37
|
+
bytes.set(chunk, offset);
|
|
38
|
+
offset += chunk.byteLength;
|
|
39
|
+
}
|
|
40
|
+
return bytes;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type PreparedBundle } from './bundle.js';
|
|
2
|
+
import { type BrowserPresetSnapshot } from './browserPreset.js';
|
|
3
|
+
import type { JsonObject } from './types.js';
|
|
4
|
+
export interface BrowserPresetInstallation {
|
|
5
|
+
revision: string;
|
|
6
|
+
browserRevision: string;
|
|
7
|
+
overrides: JsonObject;
|
|
8
|
+
}
|
|
9
|
+
export declare class BrowserBundleStore {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private database?;
|
|
12
|
+
constructor(options: {
|
|
13
|
+
mearlApiVersion: string;
|
|
14
|
+
databaseName?: string;
|
|
15
|
+
indexedDB?: IDBFactory;
|
|
16
|
+
});
|
|
17
|
+
private open;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
private records;
|
|
20
|
+
get(id: string): Promise<PreparedBundle | undefined>;
|
|
21
|
+
private verified;
|
|
22
|
+
list(): Promise<{
|
|
23
|
+
bundles: PreparedBundle[];
|
|
24
|
+
errors: {
|
|
25
|
+
id: string;
|
|
26
|
+
error: string;
|
|
27
|
+
}[];
|
|
28
|
+
activation: {
|
|
29
|
+
snapshot: BrowserPresetSnapshot;
|
|
30
|
+
preset: import("./types.js").ResolvedPreset;
|
|
31
|
+
} | undefined;
|
|
32
|
+
activationError: string | undefined;
|
|
33
|
+
installation: BrowserPresetInstallation | undefined;
|
|
34
|
+
installationError: string | undefined;
|
|
35
|
+
installationToken: string | null;
|
|
36
|
+
activeRevision: string | null;
|
|
37
|
+
revisions: {
|
|
38
|
+
[k: string]: string;
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
41
|
+
activePreset(): Promise<{
|
|
42
|
+
snapshot: BrowserPresetSnapshot;
|
|
43
|
+
preset: import("./types.js").ResolvedPreset;
|
|
44
|
+
} | undefined>;
|
|
45
|
+
/** Verified browser modules remain addressable while a bound document retains them. */
|
|
46
|
+
browserRuntimeBytes(sha256: string): Promise<Uint8Array>;
|
|
47
|
+
/** Content-addressed code survives package updates so pinned documents can still navigate. */
|
|
48
|
+
pageRuntimeSource(sha256: string): Promise<string>;
|
|
49
|
+
pinPageRuntimes(patch: Record<string, string | null>): Promise<void>;
|
|
50
|
+
/** Hosts rebuild these references from their authoritative session state after restart. */
|
|
51
|
+
synchronizePageRuntimePins(pins: Record<string, string>): Promise<void>;
|
|
52
|
+
private updateRuntimePins;
|
|
53
|
+
/** Pins the verified selection while reading its package from the same IndexedDB snapshot. */
|
|
54
|
+
profileBinding(name: string): Promise<{
|
|
55
|
+
target: string;
|
|
56
|
+
bundle: PreparedBundle;
|
|
57
|
+
} | undefined>;
|
|
58
|
+
/** Packages and the exact preset graph become visible together, after host contribution checks. */
|
|
59
|
+
activatePreset(input: BrowserPresetSnapshot, replacements: Array<{
|
|
60
|
+
bundle: PreparedBundle;
|
|
61
|
+
expectedRevision: string | null;
|
|
62
|
+
}>, expectedRevision: string | null, association?: {
|
|
63
|
+
installation?: BrowserPresetInstallation;
|
|
64
|
+
expectedInstallationToken: string | null;
|
|
65
|
+
}, expectedBundleRevisions?: Record<string, string>): Promise<void>;
|
|
66
|
+
/** Verifies all resources before a compare-and-swap transaction replaces the active snapshot. */
|
|
67
|
+
install(bundle: PreparedBundle, expectedRevision: string | null, expectedBundleRevisions?: Record<string, string>): Promise<void>;
|
|
68
|
+
remove(id: string, expectedRevision: string): Promise<void>;
|
|
69
|
+
private change;
|
|
70
|
+
}
|