@markii/bundle 0.1.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 +21 -0
- package/README.md +5 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.js +49 -0
- package/dist/fs.d.ts +25 -0
- package/dist/fs.js +249 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/manifest.d.ts +56 -0
- package/dist/manifest.js +146 -0
- package/dist/paths.d.ts +54 -0
- package/dist/paths.js +84 -0
- package/dist/script-view.d.ts +57 -0
- package/dist/script-view.js +71 -0
- package/dist/storage.d.ts +31 -0
- package/dist/storage.js +14 -0
- package/dist/zip.d.ts +80 -0
- package/dist/zip.js +329 -0
- package/package.json +53 -0
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundle path-jail (spec §11): pure functions, zero dependencies (not
|
|
3
|
+
* even on Node builtins), so they're trivially unit-testable and reusable
|
|
4
|
+
* from both the browser-safe zip storage and the Node-only directory
|
|
5
|
+
* storage without either pulling in the other's runtime.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: percent-decoding is deliberately NOT applied anywhere in this
|
|
8
|
+
* module. A path segment like `..%2F` is treated as a literal filename
|
|
9
|
+
* (the three characters `.`, `.`, `%2F`... i.e. the six characters
|
|
10
|
+
* `..%2F`), never decoded into `../`. Bundle paths are opaque strings, not
|
|
11
|
+
* URLs — decoding them would let a percent-encoded traversal sequence slip
|
|
12
|
+
* past the `..`-segment check below, which is exactly the class of bug this
|
|
13
|
+
* module exists to prevent.
|
|
14
|
+
*/
|
|
15
|
+
/** The two write grants a manifest may declare under `permissions.bundle`. */
|
|
16
|
+
export type BundleFsGrant = 'read' | 'write:cache/';
|
|
17
|
+
export type NormalizePathResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
path: string;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Normalizes a bundle-relative path and rejects anything that could escape
|
|
26
|
+
* the bundle root or address the filesystem outside it. This is the single
|
|
27
|
+
* choke point every `BundleStorage` implementation must route through
|
|
28
|
+
* before touching disk or an in-memory archive.
|
|
29
|
+
*
|
|
30
|
+
* Accepts: relative paths using `/` separators, with `.` segments and
|
|
31
|
+
* repeated `/` collapsed, and a leading `./` stripped.
|
|
32
|
+
*
|
|
33
|
+
* Rejects: empty paths, null bytes, any backslash (so a literal `\` is
|
|
34
|
+
* never mistaken for a path separator on a platform that treats it as one),
|
|
35
|
+
* absolute paths (leading `/`), Windows drive-letter paths (`C:...`), and
|
|
36
|
+
* any `..` segment, wherever it appears (start, middle, or end).
|
|
37
|
+
*/
|
|
38
|
+
export declare function normalizeBundlePath(path: string): NormalizePathResult;
|
|
39
|
+
/** The write-access policy an untrusted script is evaluated against. */
|
|
40
|
+
export interface BundleWritePolicy {
|
|
41
|
+
/** The `permissions.bundle` grants declared in the note's manifest. */
|
|
42
|
+
grants: readonly BundleFsGrant[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Implements the write half of spec §11's bundle-scoped filesystem.
|
|
46
|
+
*
|
|
47
|
+
* `note.mk.md` and `manifest.json` are denied unconditionally, regardless of
|
|
48
|
+
* `policy` — this is load-bearing, not a default: a script that could edit
|
|
49
|
+
* the manifest could grant itself further permissions, and a script that
|
|
50
|
+
* could edit `note.mk.md` would make the document self-modifying, which §8
|
|
51
|
+
* explicitly rules out. Every other path requires the `write:cache/` grant
|
|
52
|
+
* and must normalize to a path under `cache/`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isWriteAllowed(path: string, policy: BundleWritePolicy): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundle path-jail (spec §11): pure functions, zero dependencies (not
|
|
3
|
+
* even on Node builtins), so they're trivially unit-testable and reusable
|
|
4
|
+
* from both the browser-safe zip storage and the Node-only directory
|
|
5
|
+
* storage without either pulling in the other's runtime.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: percent-decoding is deliberately NOT applied anywhere in this
|
|
8
|
+
* module. A path segment like `..%2F` is treated as a literal filename
|
|
9
|
+
* (the three characters `.`, `.`, `%2F`... i.e. the six characters
|
|
10
|
+
* `..%2F`), never decoded into `../`. Bundle paths are opaque strings, not
|
|
11
|
+
* URLs — decoding them would let a percent-encoded traversal sequence slip
|
|
12
|
+
* past the `..`-segment check below, which is exactly the class of bug this
|
|
13
|
+
* module exists to prevent.
|
|
14
|
+
*/
|
|
15
|
+
/** Matches a leading Windows drive letter, e.g. `C:` at the start of a path. */
|
|
16
|
+
const DRIVE_LETTER_RE = /^[A-Za-z]:/;
|
|
17
|
+
/**
|
|
18
|
+
* Normalizes a bundle-relative path and rejects anything that could escape
|
|
19
|
+
* the bundle root or address the filesystem outside it. This is the single
|
|
20
|
+
* choke point every `BundleStorage` implementation must route through
|
|
21
|
+
* before touching disk or an in-memory archive.
|
|
22
|
+
*
|
|
23
|
+
* Accepts: relative paths using `/` separators, with `.` segments and
|
|
24
|
+
* repeated `/` collapsed, and a leading `./` stripped.
|
|
25
|
+
*
|
|
26
|
+
* Rejects: empty paths, null bytes, any backslash (so a literal `\` is
|
|
27
|
+
* never mistaken for a path separator on a platform that treats it as one),
|
|
28
|
+
* absolute paths (leading `/`), Windows drive-letter paths (`C:...`), and
|
|
29
|
+
* any `..` segment, wherever it appears (start, middle, or end).
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeBundlePath(path) {
|
|
32
|
+
if (path.length === 0) {
|
|
33
|
+
return { ok: false, reason: 'path is empty' };
|
|
34
|
+
}
|
|
35
|
+
if (path.includes('\0')) {
|
|
36
|
+
return { ok: false, reason: 'path contains a null byte' };
|
|
37
|
+
}
|
|
38
|
+
if (path.includes('\\')) {
|
|
39
|
+
return { ok: false, reason: 'backslashes are not allowed in bundle paths' };
|
|
40
|
+
}
|
|
41
|
+
if (path.startsWith('/')) {
|
|
42
|
+
return { ok: false, reason: 'absolute paths are not allowed' };
|
|
43
|
+
}
|
|
44
|
+
if (DRIVE_LETTER_RE.test(path)) {
|
|
45
|
+
return { ok: false, reason: 'drive-letter paths are not allowed' };
|
|
46
|
+
}
|
|
47
|
+
const segments = [];
|
|
48
|
+
for (const segment of path.split('/')) {
|
|
49
|
+
// Empty segments (repeated `/` or a trailing `/`) and `.` segments
|
|
50
|
+
// collapse away silently — they carry no traversal meaning.
|
|
51
|
+
if (segment === '' || segment === '.')
|
|
52
|
+
continue;
|
|
53
|
+
if (segment === '..') {
|
|
54
|
+
return { ok: false, reason: '".." path segments are not allowed' };
|
|
55
|
+
}
|
|
56
|
+
segments.push(segment);
|
|
57
|
+
}
|
|
58
|
+
if (segments.length === 0) {
|
|
59
|
+
return { ok: false, reason: 'path has no meaningful segments' };
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, path: segments.join('/') };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Implements the write half of spec §11's bundle-scoped filesystem.
|
|
65
|
+
*
|
|
66
|
+
* `note.mk.md` and `manifest.json` are denied unconditionally, regardless of
|
|
67
|
+
* `policy` — this is load-bearing, not a default: a script that could edit
|
|
68
|
+
* the manifest could grant itself further permissions, and a script that
|
|
69
|
+
* could edit `note.mk.md` would make the document self-modifying, which §8
|
|
70
|
+
* explicitly rules out. Every other path requires the `write:cache/` grant
|
|
71
|
+
* and must normalize to a path under `cache/`.
|
|
72
|
+
*/
|
|
73
|
+
export function isWriteAllowed(path, policy) {
|
|
74
|
+
const normalized = normalizeBundlePath(path);
|
|
75
|
+
if (!normalized.ok)
|
|
76
|
+
return false;
|
|
77
|
+
const { path: p } = normalized;
|
|
78
|
+
// Unconditional denial: no policy input can override this.
|
|
79
|
+
if (p === 'note.mk.md' || p === 'manifest.json')
|
|
80
|
+
return false;
|
|
81
|
+
if (!policy.grants.includes('write:cache/'))
|
|
82
|
+
return false;
|
|
83
|
+
return p.startsWith('cache/') && p.length > 'cache/'.length;
|
|
84
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BundleManifest, BundlePermissions } from './manifest.js';
|
|
2
|
+
import type { BundleStorage } from './storage.js';
|
|
3
|
+
/**
|
|
4
|
+
* The capability-restricted view of a bundle a future Lua runtime (§8, §10,
|
|
5
|
+
* §11) will actually receive — never the raw `BundleStorage`. Deliberately
|
|
6
|
+
* exposes only `read` / `write` / `exists`, no `list`: directory
|
|
7
|
+
* enumeration stays host-side. An untrusted script that can already read
|
|
8
|
+
* `assets/photo.png` by name doesn't need the ability to *discover* every
|
|
9
|
+
* other file in the bundle; keeping `list` off the script-facing surface
|
|
10
|
+
* minimizes what a script can learn about a bundle it wasn't specifically
|
|
11
|
+
* pointed at (e.g. other cached datasets, other scripts' outputs).
|
|
12
|
+
*/
|
|
13
|
+
export interface ScriptView {
|
|
14
|
+
read(path: string): Promise<Uint8Array | undefined>;
|
|
15
|
+
write(path: string, data: Uint8Array): Promise<void>;
|
|
16
|
+
exists(path: string): Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The fully-trusted convenience case: grants a `ScriptView` exactly what
|
|
20
|
+
* `manifest.permissions` *declares*, nothing more. This is a legitimate
|
|
21
|
+
* pattern for a note the user has already explicitly decided to fully
|
|
22
|
+
* trust (their own note, a dev/test harness) — but it must always be an
|
|
23
|
+
* explicit, named opt-in at the call site, never `createScriptView`'s
|
|
24
|
+
* default. See the DEFECT-10 note on `createScriptView` for why: an
|
|
25
|
+
* untrusted `.mkbundle` opened from elsewhere must never be able to grant
|
|
26
|
+
* itself capabilities merely by declaring them in its own manifest.
|
|
27
|
+
*/
|
|
28
|
+
export declare function grantAllDeclaredPermissions(manifest: BundleManifest): BundlePermissions;
|
|
29
|
+
/**
|
|
30
|
+
* Builds a `ScriptView` over `storage`.
|
|
31
|
+
*
|
|
32
|
+
* DEFECT 10 / spec §10: "Capabilities are declared in the manifest, granted
|
|
33
|
+
* by the user" — declaring is not granting. `manifest.permissions` is
|
|
34
|
+
* whatever the (possibly untrusted) `.mkbundle` *asks for*; `grantedPermissions`
|
|
35
|
+
* is whatever the user has actually *approved* for this note (e.g. via a
|
|
36
|
+
* permission-prompt UI, remembered per note and re-prompted if scripts
|
|
37
|
+
* change — see §10). The capability this view actually exposes is the
|
|
38
|
+
* INTERSECTION of the two: the manifest can only ever narrow what the user
|
|
39
|
+
* granted, never expand it, and the user's grant can only ever narrow what
|
|
40
|
+
* the manifest declared wanting. Neither side alone is authoritative.
|
|
41
|
+
*
|
|
42
|
+
* `grantedPermissions` defaults to `{}` (zero grants) — deliberately not to
|
|
43
|
+
* "everything the manifest asks for". An untrusted note opened from
|
|
44
|
+
* elsewhere must start with zero grants and still render fully (§10); a
|
|
45
|
+
* caller that wants the old fully-trusted behavior must opt in explicitly,
|
|
46
|
+
* e.g. `createScriptView(storage, manifest, grantAllDeclaredPermissions(manifest))`.
|
|
47
|
+
*
|
|
48
|
+
* - No effective bundle grants at all: every call throws `ScriptCapabilityError`.
|
|
49
|
+
* - `'read'` in the intersection: `read`/`exists` work bundle-wide; `write` still fails.
|
|
50
|
+
* - `'write:cache/'` in the intersection: `write` works, but only for paths
|
|
51
|
+
* `isWriteAllowed` accepts — `cache/` only. Critically, this holds even
|
|
52
|
+
* if both the manifest and the granted set include `'write:cache/'` and
|
|
53
|
+
* the script asks for `manifest.json` or `note.mk.md`: `isWriteAllowed`
|
|
54
|
+
* denies those two paths unconditionally, regardless of what's granted
|
|
55
|
+
* or declared (see `./paths`).
|
|
56
|
+
*/
|
|
57
|
+
export declare function createScriptView(storage: BundleStorage, manifest: BundleManifest, grantedPermissions?: BundlePermissions): ScriptView;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ScriptCapabilityError } from './errors.js';
|
|
2
|
+
import { isWriteAllowed } from './paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* The fully-trusted convenience case: grants a `ScriptView` exactly what
|
|
5
|
+
* `manifest.permissions` *declares*, nothing more. This is a legitimate
|
|
6
|
+
* pattern for a note the user has already explicitly decided to fully
|
|
7
|
+
* trust (their own note, a dev/test harness) — but it must always be an
|
|
8
|
+
* explicit, named opt-in at the call site, never `createScriptView`'s
|
|
9
|
+
* default. See the DEFECT-10 note on `createScriptView` for why: an
|
|
10
|
+
* untrusted `.mkbundle` opened from elsewhere must never be able to grant
|
|
11
|
+
* itself capabilities merely by declaring them in its own manifest.
|
|
12
|
+
*/
|
|
13
|
+
export function grantAllDeclaredPermissions(manifest) {
|
|
14
|
+
return manifest.permissions ?? {};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds a `ScriptView` over `storage`.
|
|
18
|
+
*
|
|
19
|
+
* DEFECT 10 / spec §10: "Capabilities are declared in the manifest, granted
|
|
20
|
+
* by the user" — declaring is not granting. `manifest.permissions` is
|
|
21
|
+
* whatever the (possibly untrusted) `.mkbundle` *asks for*; `grantedPermissions`
|
|
22
|
+
* is whatever the user has actually *approved* for this note (e.g. via a
|
|
23
|
+
* permission-prompt UI, remembered per note and re-prompted if scripts
|
|
24
|
+
* change — see §10). The capability this view actually exposes is the
|
|
25
|
+
* INTERSECTION of the two: the manifest can only ever narrow what the user
|
|
26
|
+
* granted, never expand it, and the user's grant can only ever narrow what
|
|
27
|
+
* the manifest declared wanting. Neither side alone is authoritative.
|
|
28
|
+
*
|
|
29
|
+
* `grantedPermissions` defaults to `{}` (zero grants) — deliberately not to
|
|
30
|
+
* "everything the manifest asks for". An untrusted note opened from
|
|
31
|
+
* elsewhere must start with zero grants and still render fully (§10); a
|
|
32
|
+
* caller that wants the old fully-trusted behavior must opt in explicitly,
|
|
33
|
+
* e.g. `createScriptView(storage, manifest, grantAllDeclaredPermissions(manifest))`.
|
|
34
|
+
*
|
|
35
|
+
* - No effective bundle grants at all: every call throws `ScriptCapabilityError`.
|
|
36
|
+
* - `'read'` in the intersection: `read`/`exists` work bundle-wide; `write` still fails.
|
|
37
|
+
* - `'write:cache/'` in the intersection: `write` works, but only for paths
|
|
38
|
+
* `isWriteAllowed` accepts — `cache/` only. Critically, this holds even
|
|
39
|
+
* if both the manifest and the granted set include `'write:cache/'` and
|
|
40
|
+
* the script asks for `manifest.json` or `note.mk.md`: `isWriteAllowed`
|
|
41
|
+
* denies those two paths unconditionally, regardless of what's granted
|
|
42
|
+
* or declared (see `./paths`).
|
|
43
|
+
*/
|
|
44
|
+
export function createScriptView(storage, manifest, grantedPermissions = {}) {
|
|
45
|
+
const declared = new Set(manifest.permissions?.bundle ?? []);
|
|
46
|
+
const granted = grantedPermissions.bundle ?? [];
|
|
47
|
+
// Effective capability = declared ∩ granted. See the doc comment above —
|
|
48
|
+
// this is the load-bearing line for DEFECT 10.
|
|
49
|
+
const grants = granted.filter((grant) => declared.has(grant));
|
|
50
|
+
const canRead = grants.includes('read');
|
|
51
|
+
return {
|
|
52
|
+
async read(path) {
|
|
53
|
+
if (!canRead) {
|
|
54
|
+
throw new ScriptCapabilityError(`script has no "read" bundle permission (requested "${path}")`);
|
|
55
|
+
}
|
|
56
|
+
return storage.read(path);
|
|
57
|
+
},
|
|
58
|
+
async write(path, data) {
|
|
59
|
+
if (!isWriteAllowed(path, { grants })) {
|
|
60
|
+
throw new ScriptCapabilityError(`script may not write "${path}"`);
|
|
61
|
+
}
|
|
62
|
+
await storage.write(path, data);
|
|
63
|
+
},
|
|
64
|
+
async exists(path) {
|
|
65
|
+
if (!canRead) {
|
|
66
|
+
throw new ScriptCapabilityError(`script has no "read" bundle permission (requested "${path}")`);
|
|
67
|
+
}
|
|
68
|
+
return storage.exists(path);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common shape both bundle forms (zip, directory) implement. Every method
|
|
3
|
+
* takes/returns bundle-relative paths and routes through
|
|
4
|
+
* `normalizeBundlePath` before touching storage — see `normalizeOrThrow`.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT for implementers: every method MUST route its `path` argument
|
|
7
|
+
* through `normalizeOrThrow` (or an equivalent check) before touching disk
|
|
8
|
+
* or an in-memory archive. `ScriptView` (`./script-view`) delegates *all*
|
|
9
|
+
* path validation to whatever `BundleStorage` it's given — it does not
|
|
10
|
+
* re-normalize paths itself. A storage implementation that skips this
|
|
11
|
+
* choke point unjails every `ScriptView` built on top of it, no matter how
|
|
12
|
+
* carefully `isWriteAllowed`/`normalizeBundlePath` are enforced elsewhere.
|
|
13
|
+
* The directory form (`./fs`) additionally must not follow symlinks or
|
|
14
|
+
* hard links when resolving a path to a physical file — see
|
|
15
|
+
* `resolveInsideRoot` and `writeExistingFileNoHardlink` there for why a
|
|
16
|
+
* *logical* path-jail alone is not sufficient once real files are involved.
|
|
17
|
+
*/
|
|
18
|
+
export interface BundleStorage {
|
|
19
|
+
/** Returns the file's bytes, or `undefined` if no such path exists. */
|
|
20
|
+
read(path: string): Promise<Uint8Array | undefined>;
|
|
21
|
+
write(path: string, data: Uint8Array): Promise<void>;
|
|
22
|
+
/** All file paths currently in the bundle, bundle-relative, sorted. */
|
|
23
|
+
list(): Promise<string[]>;
|
|
24
|
+
exists(path: string): Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The shared choke point every `BundleStorage` implementation calls before
|
|
28
|
+
* touching an archive or the filesystem: normalizes `path` or throws a
|
|
29
|
+
* `BundlePathError` describing why it was rejected.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeOrThrow(path: string): string;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BundlePathError } from './errors.js';
|
|
2
|
+
import { normalizeBundlePath } from './paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shared choke point every `BundleStorage` implementation calls before
|
|
5
|
+
* touching an archive or the filesystem: normalizes `path` or throws a
|
|
6
|
+
* `BundlePathError` describing why it was rejected.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeOrThrow(path) {
|
|
9
|
+
const result = normalizeBundlePath(path);
|
|
10
|
+
if (!result.ok) {
|
|
11
|
+
throw new BundlePathError(path, result.reason);
|
|
12
|
+
}
|
|
13
|
+
return result.path;
|
|
14
|
+
}
|
package/dist/zip.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BundleStorage } from './storage.js';
|
|
2
|
+
/** Options for `openZipBundle`'s decompression-bomb guard (DEFECT 4). */
|
|
3
|
+
export interface OpenZipBundleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Reject an entry whose *declared* (header) uncompressed size exceeds
|
|
6
|
+
* this many bytes, before ever allocating a decompression buffer for it.
|
|
7
|
+
* Defaults to `DEFAULT_MAX_ZIP_ENTRY_BYTES`.
|
|
8
|
+
*/
|
|
9
|
+
maxEntryBytes?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Reject the archive once the running sum of every processed entry's
|
|
12
|
+
* declared uncompressed size exceeds this many bytes. Defaults to
|
|
13
|
+
* `DEFAULT_MAX_ZIP_TOTAL_BYTES`.
|
|
14
|
+
*/
|
|
15
|
+
maxTotalBytes?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Default per-entry decompressed-size cap: 256MB. */
|
|
18
|
+
export declare const DEFAULT_MAX_ZIP_ENTRY_BYTES: number;
|
|
19
|
+
/** Default total (summed across all entries) decompressed-size cap: 256MB. */
|
|
20
|
+
export declare const DEFAULT_MAX_ZIP_TOTAL_BYTES: number;
|
|
21
|
+
/**
|
|
22
|
+
* Opens the zip form of a bundle (browser-safe: `fflate` has no Node
|
|
23
|
+
* dependency; the ZIP container parsing here is our own and is also
|
|
24
|
+
* dependency-free). Directory entries (names ending in `/`) carry no data
|
|
25
|
+
* and are skipped — but only *after* the name has passed
|
|
26
|
+
* `normalizeBundlePath` (DEFECT 8: validating before the directory-entry
|
|
27
|
+
* skip means a malformed directory entry like `../evil/` is rejected loudly,
|
|
28
|
+
* the same as its non-directory counterpart `../evil`, instead of being
|
|
29
|
+
* silently dropped).
|
|
30
|
+
*
|
|
31
|
+
* Zip-slip protection: every file entry's name is run through
|
|
32
|
+
* `normalizeBundlePath`. Any entry that fails (`../`, an absolute path, a
|
|
33
|
+
* backslash path, a drive-letter path) is collected and, if any exist,
|
|
34
|
+
* the whole open is rejected with a `BundleZipError` listing every
|
|
35
|
+
* offending name — a tampered bundle must be loud, not silently pruned
|
|
36
|
+
* down to "the entries that happened to be safe."
|
|
37
|
+
*
|
|
38
|
+
* Collision protection (DEFECT 5): two distinct raw entry names that
|
|
39
|
+
* *normalize* to the same bundle path (e.g. `manifest.json` and
|
|
40
|
+
* `./manifest.json`, or `cache/x` and `cache//x`) are rejected outright
|
|
41
|
+
* rather than silently last-wins — a bundle could otherwise show a benign
|
|
42
|
+
* file to one reader and a hostile one to another depending on which
|
|
43
|
+
* implementation's normalization/iteration order "wins".
|
|
44
|
+
*
|
|
45
|
+
* Decompression-bomb protection (DEFECT 4): every entry's declared
|
|
46
|
+
* uncompressed size (read from the central directory, before any inflation
|
|
47
|
+
* happens) is checked against `options.maxEntryBytes`, and the running
|
|
48
|
+
* total across all entries against `options.maxTotalBytes`. A crafted
|
|
49
|
+
* high-ratio archive is rejected before its claimed size is ever allocated.
|
|
50
|
+
*
|
|
51
|
+
* CRC-32 verification (DEFECT 6): every entry's decompressed bytes are
|
|
52
|
+
* checked against the CRC-32 recorded in the central directory; a mismatch
|
|
53
|
+
* (corrupt archive, flipped bit) throws `BundleZipError` instead of
|
|
54
|
+
* silently returning wrong data.
|
|
55
|
+
*/
|
|
56
|
+
export declare function openZipBundle(bytes: Uint8Array, options?: OpenZipBundleOptions): BundleStorage;
|
|
57
|
+
/**
|
|
58
|
+
* Serializes a `BundleStorage` to zip bytes. Zip metadata (unix
|
|
59
|
+
* permission/symlink bits) is never written — `fflate`'s `zipSync` writes
|
|
60
|
+
* plain file entries, so re-extracting a bundle produced here can never
|
|
61
|
+
* materialize a symlink.
|
|
62
|
+
*
|
|
63
|
+
* The accumulator is an `Object.create(null)` dict (DEFECT 7), not a plain
|
|
64
|
+
* `{}` — `fflate`'s `zipSync` flattens its input with a bracket-assignment
|
|
65
|
+
* loop (`t[key] = ...`) that inherits the same `__proto__` special-case
|
|
66
|
+
* problem when `key` is nested under a directory prefix; giving it a
|
|
67
|
+
* null-prototype object here means a bundle path like `cache/__proto__`
|
|
68
|
+
* (a *nested* `__proto__`, i.e. everything except a bare top-level path)
|
|
69
|
+
* round-trips correctly instead of silently corrupting our own dict.
|
|
70
|
+
*
|
|
71
|
+
* A bundle path that is *exactly* `__proto__` (or `constructor` /
|
|
72
|
+
* `prototype`) at the top level — no directory prefix — cannot be
|
|
73
|
+
* represented at all: this is a hard limitation inside `fflate` 0.8.3
|
|
74
|
+
* itself (its internal flattening step, `fltn`, does `t[name] = [...]` on
|
|
75
|
+
* its *own* plain `{}` accumulator, which we cannot reach or fix), verified
|
|
76
|
+
* empirically to throw a raw `TypeError` from deep inside `zipSync` rather
|
|
77
|
+
* than silently corrupting. We turn that into a clear, typed
|
|
78
|
+
* `BundleZipError` instead of letting the raw `TypeError` escape.
|
|
79
|
+
*/
|
|
80
|
+
export declare function exportZipBundle(storage: BundleStorage): Promise<Uint8Array>;
|