@docstack/abe 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 +10 -0
- package/README.md +66 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +63 -0
- package/lib/policy.d.ts +30 -0
- package/lib/policy.js +145 -0
- package/lib/runtime.d.ts +7 -0
- package/lib/runtime.js +26 -0
- package/lib/wasm/bytes.d.ts +1 -0
- package/lib/wasm/bytes.js +3 -0
- package/lib/wasm/glue.d.ts +50 -0
- package/lib/wasm/glue.js +426 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Onyx <hello@onyx.ac> (https://onyx.ac)
|
|
4
|
+
|
|
5
|
+
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
|
6
|
+
International License. You are free to share and adapt this work, including
|
|
7
|
+
commercially, provided you give appropriate attribution and distribute any
|
|
8
|
+
derivative works under the same license.
|
|
9
|
+
|
|
10
|
+
Full legal text: https://creativecommons.org/licenses/by-sa/4.0/legalcode
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@docstack/abe)
|
|
2
|
+
[](https://onyx-og.github.io/docstack/docs/guides/access-scopes)
|
|
3
|
+
[](https://github.com/onyx-og/docstack/blob/main/LICENSE.md)
|
|
4
|
+
|
|
5
|
+
# @docstack/abe
|
|
6
|
+
|
|
7
|
+
**The cryptography behind DocStack access scopes.**
|
|
8
|
+
|
|
9
|
+
CP-ABE, the AC17 scheme, from [rabe](https://github.com/Fraunhofer-AISEC/rabe) compiled to WASM, with the policy normaliser and the authority helpers that mint keys and seal scope content keys. `@docstack/client` depends on it and loads it lazily, only when a stack declares scopes; application code on a device never imports it.
|
|
10
|
+
|
|
11
|
+
An access scope is a named set of content whose encrypted fields seal under one 32-byte content key, and that key is sealed under an attribute policy such as `("role:manager" and "dept:sales") or "clearance:secret"`. A device whose attribute key satisfies the policy opens the scope; one whose key does not holds the same ciphertext and reads `null`. Denial is decryption failure, not a check, so it binds the device owner too.
|
|
12
|
+
|
|
13
|
+
## Two halves, one module
|
|
14
|
+
|
|
15
|
+
**Authority half**: runs where the application controls it, its server, an admin ceremony, a script. Master keys never belong on end-user devices.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { setup, keygen, wrapCek } from '@docstack/abe';
|
|
19
|
+
|
|
20
|
+
const { pk, msk } = await setup(); // once; keep msk secret, pk may be public
|
|
21
|
+
const aliceKey = await keygen(msk, ['role:hr', 'dept:people']);
|
|
22
|
+
const sealed = await wrapCek(pk, '"role:hr" or "clearance:exec"', contentKey); // 32 bytes in, opaque blob out
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
In practice `ClientStack.buildAccessScope({ scopeId, policyString, pk })` from `@docstack/client` calls `wrapCek` for you and returns a complete `~AccessScope` document: a fresh content key sealed under the policy, its key id, and a per-scope canary. Ship that document in an application patch and every device holds it.
|
|
26
|
+
|
|
27
|
+
**Client half**: the one call the engine makes.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { decryptCek } from '@docstack/abe';
|
|
31
|
+
|
|
32
|
+
const cek = await decryptCek(attributeKey, sealed); // Uint8Array, or null when the policy is not satisfied
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Policies
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { normalizePolicy, parsePolicy, policyAttributes } from '@docstack/abe';
|
|
39
|
+
|
|
40
|
+
normalizePolicy('"a" and "b" and "c" and "d"'); // '(("a" and "b") and ("c" and "d"))'
|
|
41
|
+
policyAttributes('("role:hr" or "clearance:exec")'); // ['role:hr', 'clearance:exec']
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Formulas are monotone: `and`, `or`, parentheses, `"kind:value"` literals. No negation, no code. The scheme's converter needs balanced parentheses and panics on long unbalanced `and` chains, so every sealing call normalises the formula first; `normalizePolicy` throws on `not`.
|
|
45
|
+
|
|
46
|
+
## Keys and blobs are opaque
|
|
47
|
+
|
|
48
|
+
Every key and ciphertext is a string. rabe serialises 64-bit limbs that do not survive a JavaScript JSON round-trip, so store and transport these blobs verbatim and never parse them.
|
|
49
|
+
|
|
50
|
+
## Status
|
|
51
|
+
|
|
52
|
+
Experimental cryptography, unaudited. rabe's BN254 backend carries roughly a 100-bit modern security margin; an audit gates any production claim. The reasoning, the alternatives measured and the residual risks are in [ADR-0045](https://github.com/onyx-og/docstack/blob/main/specs/adr/0045-access-control-is-cryptographic-cp-abe-scopes-beside-the-engine.md) and the [threat model](https://onyx-og.github.io/docstack/docs/concepts/access-control/threat-model).
|
|
53
|
+
|
|
54
|
+
## Building from source
|
|
55
|
+
|
|
56
|
+
The WASM under `src/wasm` is vendored and regenerated by `npm run build:wasm`, which needs the Rust toolchain with the `wasm32-unknown-unknown` target and `wasm-bindgen-cli` matching the version in `rust/Cargo.lock`. `npm run build` compiles the TypeScript and copies the vendored artifacts into `lib/`; `npm test` runs a smoke test through the WASM.
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
- [Scope your data](https://onyx-og.github.io/docstack/docs/guides/access-scopes), the guide
|
|
61
|
+
- [Access control](https://onyx-og.github.io/docstack/docs/concepts/access-control/), the model, its formula language and its limits
|
|
62
|
+
- [API reference](https://onyx-og.github.io/docstack/docs/api/abe/)
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[CC-BY-SA-4.0](https://github.com/onyx-og/docstack/blob/main/LICENSE.md) · © Onyx AC, LLC. rabe is licensed under the MIT license.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { normalizePolicy, parsePolicy, policyAttributes } from "./policy.js";
|
|
2
|
+
export type { PolicyNode } from "./policy.js";
|
|
3
|
+
/** Authority: mints the master key pair. Returns opaque `pk` (public) and `msk` (SECRET) blobs. */
|
|
4
|
+
export declare const setup: () => Promise<{
|
|
5
|
+
pk: string;
|
|
6
|
+
msk: string;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Authority: issues a user's attribute secret key — their attributes embedded
|
|
10
|
+
* in the key material itself. The returned blob is the `attributeKey` the
|
|
11
|
+
* consumer's infrastructure hands to that user's devices (spec 02 §4).
|
|
12
|
+
*/
|
|
13
|
+
export declare const keygen: (msk: string, attributes: string[]) => Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Authority: seals a scope's 32-byte CEK under a policy formula. The formula
|
|
16
|
+
* is normalized first (balanced parenthesization — rabe's converter panics on
|
|
17
|
+
* long unbalanced AND-chains), so authors write it naturally.
|
|
18
|
+
*/
|
|
19
|
+
export declare const wrapCek: (pk: string, policy: string, cek: Uint8Array) => Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Client: attempts a scope's CEK with the session's attribute key.
|
|
22
|
+
* `null` means the key does not satisfy the scope's policy — access denial as
|
|
23
|
+
* decryption failure, the whole point of the architecture.
|
|
24
|
+
*/
|
|
25
|
+
export declare const decryptCek: (attributeKey: string, wrappedCek: string) => Promise<Uint8Array | null>;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docstack/abe — CP-ABE (AC17, the FAME scheme) for DocStack access scopes.
|
|
3
|
+
*
|
|
4
|
+
* rabe (Fraunhofer AISEC) compiled to WASM; gate ruling ADR-0045. Every key
|
|
5
|
+
* and ciphertext is an OPAQUE string: rabe-bn serializes u64 limbs that do not
|
|
6
|
+
* survive a JavaScript JSON round-trip (found the hard way in the spike), so
|
|
7
|
+
* callers store and transport these blobs verbatim and never parse them.
|
|
8
|
+
*
|
|
9
|
+
* Two halves share the module because they share the WASM:
|
|
10
|
+
* - **Authority half** (`setup`, `keygen`, `wrapCek`): runs where the
|
|
11
|
+
* application controls it — its server, an admin ceremony, later the
|
|
12
|
+
* docstack-server package. Master keys never belong on end-user devices.
|
|
13
|
+
* - **Client half** (`decryptCek`): the only call @docstack/client makes.
|
|
14
|
+
* Denial is `null` — the mathematics failing IS the access decision.
|
|
15
|
+
*
|
|
16
|
+
* EXPERIMENTAL CRYPTOGRAPHY: unaudited (rabe-bn is a fork of an old zcash
|
|
17
|
+
* BN254 crate, ~100-bit modern margin). An audit gates any production claim.
|
|
18
|
+
*/
|
|
19
|
+
import { ensureAbe } from "./runtime.js";
|
|
20
|
+
import { normalizePolicy } from "./policy.js";
|
|
21
|
+
export { normalizePolicy, parsePolicy, policyAttributes } from "./policy.js";
|
|
22
|
+
/** Authority: mints the master key pair. Returns opaque `pk` (public) and `msk` (SECRET) blobs. */
|
|
23
|
+
export const setup = async () => {
|
|
24
|
+
const abe = await ensureAbe();
|
|
25
|
+
const bundle = JSON.parse(abe.setup());
|
|
26
|
+
return { pk: bundle.pk, msk: bundle.msk };
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Authority: issues a user's attribute secret key — their attributes embedded
|
|
30
|
+
* in the key material itself. The returned blob is the `attributeKey` the
|
|
31
|
+
* consumer's infrastructure hands to that user's devices (spec 02 §4).
|
|
32
|
+
*/
|
|
33
|
+
export const keygen = async (msk, attributes) => {
|
|
34
|
+
if (!attributes.length)
|
|
35
|
+
throw new Error("keygen requires at least one attribute.");
|
|
36
|
+
const abe = await ensureAbe();
|
|
37
|
+
return abe.cp_keygen(msk, JSON.stringify(attributes));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Authority: seals a scope's 32-byte CEK under a policy formula. The formula
|
|
41
|
+
* is normalized first (balanced parenthesization — rabe's converter panics on
|
|
42
|
+
* long unbalanced AND-chains), so authors write it naturally.
|
|
43
|
+
*/
|
|
44
|
+
export const wrapCek = async (pk, policy, cek) => {
|
|
45
|
+
if (cek.length !== 32)
|
|
46
|
+
throw new Error(`wrapCek seals 32-byte CEKs; got ${cek.length} bytes.`);
|
|
47
|
+
const abe = await ensureAbe();
|
|
48
|
+
return abe.cp_encrypt(pk, normalizePolicy(policy), cek);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Client: attempts a scope's CEK with the session's attribute key.
|
|
52
|
+
* `null` means the key does not satisfy the scope's policy — access denial as
|
|
53
|
+
* decryption failure, the whole point of the architecture.
|
|
54
|
+
*/
|
|
55
|
+
export const decryptCek = async (attributeKey, wrappedCek) => {
|
|
56
|
+
const abe = await ensureAbe();
|
|
57
|
+
try {
|
|
58
|
+
return abe.cp_decrypt(attributeKey, wrappedCek);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
package/lib/policy.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attribute-policy language and its normalizer.
|
|
3
|
+
*
|
|
4
|
+
* DocStack's one access-control language (ADR-0045, spec 02 §1): quoted
|
|
5
|
+
* attributes composed with `and`/`or` and parentheses — monotone by
|
|
6
|
+
* construction, no negation, no data-dependence. This module is the ONLY path
|
|
7
|
+
* a policy string takes into the ABE layer: rabe's MSP converter panics on
|
|
8
|
+
* unparenthesized AND-chains of length ≥ 3 (characterized in
|
|
9
|
+
* `spikes/abe/bench/rabe-probe.ts`), so every formula is parsed here and
|
|
10
|
+
* re-emitted as a fully parenthesized, BALANCED binary tree before it reaches
|
|
11
|
+
* the scheme. Authors write formulas naturally; the wire form is canonical.
|
|
12
|
+
*/
|
|
13
|
+
export type PolicyNode = {
|
|
14
|
+
kind: "attr";
|
|
15
|
+
name: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "op";
|
|
18
|
+
op: "and" | "or";
|
|
19
|
+
children: PolicyNode[];
|
|
20
|
+
};
|
|
21
|
+
/** Recursive-descent parse; `and` binds tighter than `or` (the conventional precedence). */
|
|
22
|
+
export declare const parsePolicy: (policy: string) => PolicyNode;
|
|
23
|
+
/**
|
|
24
|
+
* Parses and re-emits a policy in canonical form: attributes quoted, every
|
|
25
|
+
* operator application parenthesized, same-op chains balanced. Throws on
|
|
26
|
+
* anything outside the language — the loud refusal is the API.
|
|
27
|
+
*/
|
|
28
|
+
export declare const normalizePolicy: (policy: string) => string;
|
|
29
|
+
/** The attribute names a formula mentions, in first-appearance order. */
|
|
30
|
+
export declare const policyAttributes: (policy: string) => string[];
|
package/lib/policy.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attribute-policy language and its normalizer.
|
|
3
|
+
*
|
|
4
|
+
* DocStack's one access-control language (ADR-0045, spec 02 §1): quoted
|
|
5
|
+
* attributes composed with `and`/`or` and parentheses — monotone by
|
|
6
|
+
* construction, no negation, no data-dependence. This module is the ONLY path
|
|
7
|
+
* a policy string takes into the ABE layer: rabe's MSP converter panics on
|
|
8
|
+
* unparenthesized AND-chains of length ≥ 3 (characterized in
|
|
9
|
+
* `spikes/abe/bench/rabe-probe.ts`), so every formula is parsed here and
|
|
10
|
+
* re-emitted as a fully parenthesized, BALANCED binary tree before it reaches
|
|
11
|
+
* the scheme. Authors write formulas naturally; the wire form is canonical.
|
|
12
|
+
*/
|
|
13
|
+
const tokenize = (policy) => {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < policy.length) {
|
|
17
|
+
const ch = policy[i];
|
|
18
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (ch === "(") {
|
|
23
|
+
tokens.push({ kind: "open" });
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === ")") {
|
|
28
|
+
tokens.push({ kind: "close" });
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '"') {
|
|
33
|
+
const end = policy.indexOf('"', i + 1);
|
|
34
|
+
if (end === -1)
|
|
35
|
+
throw new Error(`Policy has an unterminated quoted attribute at position ${i}.`);
|
|
36
|
+
const name = policy.slice(i + 1, end);
|
|
37
|
+
if (!name.trim())
|
|
38
|
+
throw new Error("Policy contains an empty attribute name.");
|
|
39
|
+
tokens.push({ kind: "attr", name });
|
|
40
|
+
i = end + 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// A bare word: and / or, case-insensitive. Anything else is refused
|
|
44
|
+
// loudly — including every negation spelling, which the monotone
|
|
45
|
+
// language deliberately lacks.
|
|
46
|
+
const match = /^[A-Za-z_][A-Za-z0-9_:-]*/.exec(policy.slice(i));
|
|
47
|
+
if (!match)
|
|
48
|
+
throw new Error(`Unexpected character '${ch}' in policy at position ${i}.`);
|
|
49
|
+
const word = match[0];
|
|
50
|
+
const lower = word.toLowerCase();
|
|
51
|
+
if (lower === "and")
|
|
52
|
+
tokens.push({ kind: "and" });
|
|
53
|
+
else if (lower === "or")
|
|
54
|
+
tokens.push({ kind: "or" });
|
|
55
|
+
else if (lower === "not" || lower === "nand" || lower === "nor") {
|
|
56
|
+
throw new Error(`Policy operator '${word}' is not part of the monotone language: no negation exists. Model the positive attribute instead.`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new Error(`Unquoted word '${word}' in policy: attributes must be double-quoted, e.g. "role:manager".`);
|
|
60
|
+
}
|
|
61
|
+
i += word.length;
|
|
62
|
+
}
|
|
63
|
+
return tokens;
|
|
64
|
+
};
|
|
65
|
+
/** Recursive-descent parse; `and` binds tighter than `or` (the conventional precedence). */
|
|
66
|
+
export const parsePolicy = (policy) => {
|
|
67
|
+
const tokens = tokenize(policy);
|
|
68
|
+
let position = 0;
|
|
69
|
+
const peek = () => tokens[position];
|
|
70
|
+
const take = () => tokens[position++];
|
|
71
|
+
const primary = () => {
|
|
72
|
+
const token = take();
|
|
73
|
+
if (!token)
|
|
74
|
+
throw new Error("Policy ended where an attribute or '(' was expected.");
|
|
75
|
+
if (token.kind === "attr")
|
|
76
|
+
return { kind: "attr", name: token.name };
|
|
77
|
+
if (token.kind === "open") {
|
|
78
|
+
const node = orExpr();
|
|
79
|
+
const close = take();
|
|
80
|
+
if (!close || close.kind !== "close")
|
|
81
|
+
throw new Error("Policy has an unbalanced '('.");
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
throw new Error("Policy has an operator where an attribute or '(' was expected.");
|
|
85
|
+
};
|
|
86
|
+
const andExpr = () => {
|
|
87
|
+
const children = [primary()];
|
|
88
|
+
while (peek()?.kind === "and") {
|
|
89
|
+
take();
|
|
90
|
+
children.push(primary());
|
|
91
|
+
}
|
|
92
|
+
return children.length === 1 ? children[0] : { kind: "op", op: "and", children };
|
|
93
|
+
};
|
|
94
|
+
const orExpr = () => {
|
|
95
|
+
const children = [andExpr()];
|
|
96
|
+
while (peek()?.kind === "or") {
|
|
97
|
+
take();
|
|
98
|
+
children.push(andExpr());
|
|
99
|
+
}
|
|
100
|
+
return children.length === 1 ? children[0] : { kind: "op", op: "or", children };
|
|
101
|
+
};
|
|
102
|
+
const root = orExpr();
|
|
103
|
+
if (position !== tokens.length)
|
|
104
|
+
throw new Error("Policy has trailing content after a complete formula (an unbalanced ')' or a missing operator).");
|
|
105
|
+
return root;
|
|
106
|
+
};
|
|
107
|
+
/** Flattens nested same-op nodes so chains balance across author parenthesization. */
|
|
108
|
+
const flatten = (node) => {
|
|
109
|
+
if (node.kind === "attr")
|
|
110
|
+
return node;
|
|
111
|
+
const children = [];
|
|
112
|
+
for (const child of node.children.map(flatten)) {
|
|
113
|
+
if (child.kind === "op" && child.op === node.op)
|
|
114
|
+
children.push(...child.children);
|
|
115
|
+
else
|
|
116
|
+
children.push(child);
|
|
117
|
+
}
|
|
118
|
+
return { kind: "op", op: node.op, children };
|
|
119
|
+
};
|
|
120
|
+
/** Emits a chain as a balanced binary tree — the shape rabe's converter accepts at any length. */
|
|
121
|
+
const emitBalanced = (nodes, op) => {
|
|
122
|
+
if (nodes.length === 1)
|
|
123
|
+
return emit(nodes[0]);
|
|
124
|
+
const mid = Math.ceil(nodes.length / 2);
|
|
125
|
+
return `(${emitBalanced(nodes.slice(0, mid), op)} ${op} ${emitBalanced(nodes.slice(mid), op)})`;
|
|
126
|
+
};
|
|
127
|
+
const emit = (node) => node.kind === "attr" ? `"${node.name}"` : emitBalanced(node.children, node.op);
|
|
128
|
+
/**
|
|
129
|
+
* Parses and re-emits a policy in canonical form: attributes quoted, every
|
|
130
|
+
* operator application parenthesized, same-op chains balanced. Throws on
|
|
131
|
+
* anything outside the language — the loud refusal is the API.
|
|
132
|
+
*/
|
|
133
|
+
export const normalizePolicy = (policy) => emit(flatten(parsePolicy(policy)));
|
|
134
|
+
/** The attribute names a formula mentions, in first-appearance order. */
|
|
135
|
+
export const policyAttributes = (policy) => {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const walk = (node) => {
|
|
138
|
+
if (node.kind === "attr")
|
|
139
|
+
seen.add(node.name);
|
|
140
|
+
else
|
|
141
|
+
node.children.forEach(walk);
|
|
142
|
+
};
|
|
143
|
+
walk(parsePolicy(policy));
|
|
144
|
+
return [...seen];
|
|
145
|
+
};
|
package/lib/runtime.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type AbeApi = {
|
|
2
|
+
setup(): string;
|
|
3
|
+
cp_keygen(mskJson: string, attrsJson: string): string;
|
|
4
|
+
cp_encrypt(pkJson: string, policy: string, plaintext: Uint8Array): string;
|
|
5
|
+
cp_decrypt(skJson: string, ctJson: string): Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
export declare const ensureAbe: () => Promise<AbeApi>;
|
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy WASM instantiation. The module bytes ride the bundle base64-embedded
|
|
3
|
+
* (no fetch, no asset path — every bundler and the browser test harness see
|
|
4
|
+
* plain JS), but nothing compiles until the first ABE call: a stack that never
|
|
5
|
+
* touches scopes pays bytes, not startup. Instantiation is ASYNC on purpose —
|
|
6
|
+
* Chrome caps synchronous WebAssembly compilation on the main thread, so
|
|
7
|
+
* `initSync` over a 600 KB module is a browser refusal waiting to happen.
|
|
8
|
+
*/
|
|
9
|
+
import init, * as glue from "./wasm/glue.js";
|
|
10
|
+
import { WASM_BASE64 } from "./wasm/bytes.js";
|
|
11
|
+
let ready = null;
|
|
12
|
+
const decode = (b64) => {
|
|
13
|
+
if (typeof Buffer !== "undefined")
|
|
14
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
15
|
+
const binary = atob(b64);
|
|
16
|
+
const bytes = new Uint8Array(binary.length);
|
|
17
|
+
for (let i = 0; i < binary.length; i++)
|
|
18
|
+
bytes[i] = binary.charCodeAt(i);
|
|
19
|
+
return bytes;
|
|
20
|
+
};
|
|
21
|
+
export const ensureAbe = () => {
|
|
22
|
+
if (!ready) {
|
|
23
|
+
ready = init({ module_or_path: decode(WASM_BASE64) }).then(() => glue);
|
|
24
|
+
}
|
|
25
|
+
return ready;
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WASM_BASE64: string;
|