@markii/lua 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/capabilities.d.ts +124 -0
- package/dist/capabilities.js +281 -0
- package/dist/errors.d.ts +63 -0
- package/dist/errors.js +43 -0
- package/dist/executor.d.ts +39 -0
- package/dist/executor.js +26 -0
- package/dist/globals.d.ts +32 -0
- package/dist/globals.js +284 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +13 -0
- package/dist/limits.d.ts +146 -0
- package/dist/limits.js +172 -0
- package/dist/marshal.d.ts +126 -0
- package/dist/marshal.js +260 -0
- package/dist/require.d.ts +43 -0
- package/dist/require.js +49 -0
- package/dist/sandbox.d.ts +71 -0
- package/dist/sandbox.js +336 -0
- package/package.json +51 -0
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { LuaReturn } from 'wasmoon';
|
|
2
|
+
import { buildCapabilities, } from './capabilities.js';
|
|
3
|
+
import { CAPABILITY_ERROR_TAG, MARSHAL_ERROR_TAG, ScriptLimitError, } from './errors.js';
|
|
4
|
+
import { createEmptyLuaEngine } from './globals.js';
|
|
5
|
+
import { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
6
|
+
import { buildMarshalPrelude, DEFAULT_MARSHAL_LIMITS, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
7
|
+
/**
|
|
8
|
+
* The wall-clock hard-kill in `./limits` only fires between Lua VM
|
|
9
|
+
* instructions — it cannot observe a script suspended on `:await()`-ing a
|
|
10
|
+
* host-provided async capability call that never resolves (no instructions
|
|
11
|
+
* execute during that wait, so the hook never gets scheduled). This extra
|
|
12
|
+
* margin over `limits.wallClockMs` before the outer race guard below fires
|
|
13
|
+
* gives the IN-VM hook first right of way for the (far more common)
|
|
14
|
+
* compute-bound case, so the two mechanisms don't race each other for the
|
|
15
|
+
* `breachKind` attribution; this guard exists purely as the backstop for
|
|
16
|
+
* the async-hang case the in-VM hook structurally cannot see.
|
|
17
|
+
*/
|
|
18
|
+
const WALL_CLOCK_GUARD_SLACK_MS = 250;
|
|
19
|
+
function describeError(err) {
|
|
20
|
+
if (err instanceof Error)
|
|
21
|
+
return err.message;
|
|
22
|
+
return String(err);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wraps `thread.assertOk` (public wasmoon API, `thread.d.ts`) to capture the
|
|
26
|
+
* raw C-API status code (`LuaReturn`) that `lua_resume`/`lua_pcall` returned
|
|
27
|
+
* for the run, before wasmoon collapses it into a generic `Error`. Returns a
|
|
28
|
+
* getter for that last-seen code, plus a restore function.
|
|
29
|
+
*
|
|
30
|
+
* ## Why this exists: memory-cap breaches were misclassified as `'runtime'`
|
|
31
|
+
*
|
|
32
|
+
* `thread.run()` calls `this.assertOk(resumeResult.result)` exactly once,
|
|
33
|
+
* with the terminal status of the run. When the Lua allocator (the
|
|
34
|
+
* `traceAllocations`-backed custom allocator `./globals` installs, capped by
|
|
35
|
+
* `engine.global.setMemoryMax`) returns null past the cap, the VM raises
|
|
36
|
+
* `LUA_ERRMEM` — `assertOk` sees `LuaReturn.ErrorMem` (4) and throws a plain
|
|
37
|
+
* `Error` whose `.message` is set directly from `lua_tolstring` (skipping
|
|
38
|
+
* the traceback step, since traceback generation itself needs allocation
|
|
39
|
+
* that could also fail under OOM). That message is INDISTINGUISHABLE from
|
|
40
|
+
* an ordinary runtime error's message on the far side — both are plain
|
|
41
|
+
* strings — so without this hook, a genuine memory-cap breach and a
|
|
42
|
+
* script's own `error("not enough memory")` collapse to the exact same
|
|
43
|
+
* shape and there is no reliable way to tell them apart from the message
|
|
44
|
+
* alone.
|
|
45
|
+
*
|
|
46
|
+
* ## Why the status code is non-spoofable (unlike message matching)
|
|
47
|
+
*
|
|
48
|
+
* `LuaReturn.ErrorMem` is the literal C-API return code from
|
|
49
|
+
* `lua_resume`/`lua_pcall` — it is set by the Lua VM's own error-throwing
|
|
50
|
+
* path (`luaD_throw` with `LUA_ERRMEM`) when the allocator fails, and by
|
|
51
|
+
* nothing else. A script calling `error("not enough memory")` raises an
|
|
52
|
+
* ORDINARY Lua error (`LUA_ERRRUN`, code 2) — verified empirically in a
|
|
53
|
+
* throwaway harness: `error('not enough memory')` yields status 2, while an
|
|
54
|
+
* actual allocator-capped allocation yields status 4, with output messages
|
|
55
|
+
* that are otherwise identical strings. A script has no way to make Lua's
|
|
56
|
+
* own C `lua_resume` return `LUA_ERRMEM` other than genuinely exhausting the
|
|
57
|
+
* capped allocator.
|
|
58
|
+
*
|
|
59
|
+
* ## Why this does NOT reclassify a script's own `pcall`-caught OOM
|
|
60
|
+
*
|
|
61
|
+
* When a script wraps the failing allocation in its OWN `pcall`
|
|
62
|
+
* (`pcall(function() return string.rep(...) end)`), the `LUA_ERRMEM` is
|
|
63
|
+
* raised and caught entirely INSIDE that inner `lua_pcall`, at the Lua
|
|
64
|
+
* level — the outer `lua_resume` that `thread.run()` drives still completes
|
|
65
|
+
* with status `LuaReturn.Ok` (the script's own `pcall` returned `false,
|
|
66
|
+
* "not enough memory"` as an ordinary value). `assertOk` is therefore never
|
|
67
|
+
* called with `ErrorMem` in that case, so this hook correctly leaves that
|
|
68
|
+
* case alone — matching the existing (and intentional) behavior asserted in
|
|
69
|
+
* `sandbox.test.ts`'s "memory cap stops a string.rep balloon ... without
|
|
70
|
+
* OOM-ing the process" test, which expects that case to come back as an
|
|
71
|
+
* ordinary successful run (`ok: true, value: 'false'`), not a `'limit'`
|
|
72
|
+
* failure.
|
|
73
|
+
*/
|
|
74
|
+
function captureAssertOkStatus(thread) {
|
|
75
|
+
const original = thread.assertOk.bind(thread);
|
|
76
|
+
let lastStatus;
|
|
77
|
+
thread.assertOk = (result) => {
|
|
78
|
+
lastStatus = result;
|
|
79
|
+
original(result);
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
lastStatus: () => lastStatus,
|
|
83
|
+
restore: () => {
|
|
84
|
+
thread.assertOk = original;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function extractMarshalReason(message) {
|
|
89
|
+
// The tagged reason is always on the SAME LINE as the tag (Lua's
|
|
90
|
+
// `error()` produces "chunkname:line: MARK_MARSHAL:<reason>[:extra]");
|
|
91
|
+
// wasmoon appends a "\nstack traceback:\n..." block after it, which
|
|
92
|
+
// itself contains further colons (e.g. "[string \"...\"]:10:") — take
|
|
93
|
+
// only the first line before splitting on ":", or those traceback
|
|
94
|
+
// colons get mistaken for part of the tag.
|
|
95
|
+
const afterTag = message
|
|
96
|
+
.slice(message.indexOf(MARSHAL_ERROR_TAG) + MARSHAL_ERROR_TAG.length)
|
|
97
|
+
.split('\n')[0];
|
|
98
|
+
const tag = (afterTag ?? '').replace(/^:/, '').split(':')[0]?.trim();
|
|
99
|
+
switch (tag) {
|
|
100
|
+
case 'nodes':
|
|
101
|
+
return 'nodes';
|
|
102
|
+
case 'depth':
|
|
103
|
+
return 'depth';
|
|
104
|
+
case 'cycle':
|
|
105
|
+
return 'cycle';
|
|
106
|
+
case 'key-type':
|
|
107
|
+
return 'key-type';
|
|
108
|
+
case 'type':
|
|
109
|
+
return 'type';
|
|
110
|
+
default:
|
|
111
|
+
return 'type';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Classifies an error thrown out of `thread.run()` into the discriminated
|
|
116
|
+
* `ScriptFailure` shape. Message-prefix matching (`CAPABILITY_ERROR_TAG`,
|
|
117
|
+
* `MARSHAL_ERROR_TAG`) is used rather than `instanceof` because wasmoon
|
|
118
|
+
* does not preserve JS `Error` subclass identity across the Lua round trip
|
|
119
|
+
* — see the doc comment on those tags in `./errors` for the empirical
|
|
120
|
+
* evidence. Resource-limit breaches are ALL classified separately, BEFORE
|
|
121
|
+
* this function is ever called, via non-spoofable out-of-band signals —
|
|
122
|
+
* never through this message-based path, since a script can trivially
|
|
123
|
+
* forge any message string (e.g. `error("MARK_LIMIT: ...")` or
|
|
124
|
+
* `error("not enough memory")`) but cannot forge these:
|
|
125
|
+
* - instruction/wall-clock breaches: the JS closure flag from
|
|
126
|
+
* `./limits`' hook (see `runScript` below);
|
|
127
|
+
* - the async-hang backstop: `instanceof ScriptLimitError` on the
|
|
128
|
+
* `Promise.race` guard's own sentinel, which never crosses the Lua
|
|
129
|
+
* boundary (see the guard's construction in `runScript`);
|
|
130
|
+
* - memory-cap breaches: the raw `LuaReturn.ErrorMem` C-API status code
|
|
131
|
+
* captured by `captureAssertOkStatus` (see its doc comment).
|
|
132
|
+
*/
|
|
133
|
+
function classifyRuntimeError(err) {
|
|
134
|
+
const message = describeError(err);
|
|
135
|
+
if (message.includes(CAPABILITY_ERROR_TAG)) {
|
|
136
|
+
return {
|
|
137
|
+
kind: 'capability',
|
|
138
|
+
message: message.replace(`${CAPABILITY_ERROR_TAG}: `, ''),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (message.includes(MARSHAL_ERROR_TAG)) {
|
|
142
|
+
return {
|
|
143
|
+
kind: 'marshal',
|
|
144
|
+
reason: extractMarshalReason(message),
|
|
145
|
+
message,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { kind: 'runtime', message };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Runs one Lua script in a fresh, fully isolated sandbox and always tears
|
|
152
|
+
* the engine down before returning — a run never leaves state (globals,
|
|
153
|
+
* memory, hooks) for a later run to inherit. Never throws: every way
|
|
154
|
+
* hostile code can fail comes back as `{ ok: false, error }`, never a raw
|
|
155
|
+
* exception (see `./errors`).
|
|
156
|
+
*
|
|
157
|
+
* Orchestration, in order:
|
|
158
|
+
* 1. `./globals` — fresh engine, curated empty environment (no `os`/`io`/
|
|
159
|
+
* `require`/etc; see that module for exactly what's kept and why).
|
|
160
|
+
* 2. Memory cap (`engine.global.setMemoryMax`, backed by the
|
|
161
|
+
* `traceAllocations: true` custom allocator `./globals` requests).
|
|
162
|
+
* 3. `./capabilities` — build the `net`/`cache`/`bundle` Lua tables from
|
|
163
|
+
* whatever providers/grants/tier this call was given.
|
|
164
|
+
* 4. `./marshal` — inject the trusted node/depth-capped marshal walk that
|
|
165
|
+
* the wrapped user code's return value is piped through.
|
|
166
|
+
* 5. A dedicated child thread (NOT `engine.doString`, which creates its
|
|
167
|
+
* own internal thread we'd have no handle to — see `./limits`'s "hooks
|
|
168
|
+
* are per-thread" note) gets the instruction/wall-clock hook installed,
|
|
169
|
+
* then runs the wrapped user code.
|
|
170
|
+
* 6. The out-of-band breach flag from step 5's hook is checked
|
|
171
|
+
* UNCONDITIONALLY and, if set, wins over whatever the run otherwise
|
|
172
|
+
* reported — see `./limits`'s doc comment for why this is the actual
|
|
173
|
+
* enforcement point for "not swallowed by the script's own `pcall`".
|
|
174
|
+
* 7. Otherwise, a thrown error is classified by three non-spoofable
|
|
175
|
+
* out-of-band signals, in order, before ever falling back to
|
|
176
|
+
* `classifyRuntimeError`'s message-based path: the wall-clock guard's
|
|
177
|
+
* own `ScriptLimitError` sentinel (`instanceof`, Defect 3), then the
|
|
178
|
+
* raw `LuaReturn.ErrorMem` status code (Defect 2). A successful return
|
|
179
|
+
* goes through `finalizeMarshaledValue` for the final NaN/Infinity
|
|
180
|
+
* check and marker cleanup.
|
|
181
|
+
* 8. `finally`: hook removed, thread popped, engine closed — every path,
|
|
182
|
+
* including every early return above.
|
|
183
|
+
*/
|
|
184
|
+
export async function runScript(options) {
|
|
185
|
+
const limits = { ...DEFAULT_LIMITS, ...options.limits };
|
|
186
|
+
const marshalLimits = {
|
|
187
|
+
...DEFAULT_MARSHAL_LIMITS,
|
|
188
|
+
...options.marshalLimits,
|
|
189
|
+
};
|
|
190
|
+
const engine = await createEmptyLuaEngine({ wasmUri: options.wasmUri });
|
|
191
|
+
engine.global.setMemoryMax(limits.maxMemoryBytes);
|
|
192
|
+
let thread;
|
|
193
|
+
let threadStackIndex;
|
|
194
|
+
let limitHandle;
|
|
195
|
+
let guardTimer;
|
|
196
|
+
try {
|
|
197
|
+
const { rawGlobals, preludeLua } = buildCapabilities({
|
|
198
|
+
tier: options.tier,
|
|
199
|
+
net: options.net,
|
|
200
|
+
netGrants: options.netGrants,
|
|
201
|
+
cache: options.cache,
|
|
202
|
+
bundle: options.bundle,
|
|
203
|
+
maxFetchBytes: options.maxFetchBytes,
|
|
204
|
+
});
|
|
205
|
+
for (const [name, fn] of Object.entries(rawGlobals)) {
|
|
206
|
+
engine.global.set(name, fn);
|
|
207
|
+
}
|
|
208
|
+
if (preludeLua.trim().length > 0) {
|
|
209
|
+
await engine.doString(preludeLua);
|
|
210
|
+
}
|
|
211
|
+
await engine.doString(buildMarshalPrelude(marshalLimits));
|
|
212
|
+
thread = engine.global.newThread();
|
|
213
|
+
threadStackIndex = engine.global.getTop();
|
|
214
|
+
limitHandle = installLimits(thread, limits);
|
|
215
|
+
try {
|
|
216
|
+
thread.loadString(wrapUserCode(options.code));
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
error: { kind: 'runtime', message: describeError(err) },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// Identity-based sentinel (Defect 3): this rejects with a
|
|
225
|
+
// `ScriptLimitError` — a class defined and thrown entirely within this
|
|
226
|
+
// module, never round-tripped through Lua — so the `instanceof` check
|
|
227
|
+
// below cannot be spoofed by a script's own `error("...")` call, even
|
|
228
|
+
// one using this exact message text (see the test asserting that
|
|
229
|
+
// distinction). Message-string matching would be spoofable; `instanceof`
|
|
230
|
+
// is not, because unlike the `CAPABILITY_ERROR_TAG`/`MARSHAL_ERROR_TAG`
|
|
231
|
+
// cases in `classifyRuntimeError` (which DO cross the Lua boundary and
|
|
232
|
+
// so lose subclass identity — see `./errors`'s doc comment), this
|
|
233
|
+
// guard's reject() and its catch below are the same JS scope: the error
|
|
234
|
+
// never enters the Lua VM at all.
|
|
235
|
+
const guard = new Promise((_resolve, reject) => {
|
|
236
|
+
guardTimer = setTimeout(() => {
|
|
237
|
+
reject(new ScriptLimitError('timeout', 'wall-clock timeout exceeded (external async guard: a host capability call never resolved)'));
|
|
238
|
+
}, limits.wallClockMs + WALL_CLOCK_GUARD_SLACK_MS);
|
|
239
|
+
guardTimer.unref?.();
|
|
240
|
+
});
|
|
241
|
+
// Defect 2: capture the raw LuaReturn status code for this run so a
|
|
242
|
+
// genuine memory-cap breach can be told apart, non-spoofably, from an
|
|
243
|
+
// ordinary Lua runtime error whose message happens to say "not enough
|
|
244
|
+
// memory" — see `captureAssertOkStatus`'s doc comment.
|
|
245
|
+
const statusCapture = captureAssertOkStatus(thread);
|
|
246
|
+
let runResult;
|
|
247
|
+
try {
|
|
248
|
+
const values = await Promise.race([thread.run(0), guard]);
|
|
249
|
+
runResult = {
|
|
250
|
+
kind: 'ok',
|
|
251
|
+
value: values.length > 0 ? values[0] : undefined,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
runResult = { kind: 'error', err };
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (guardTimer)
|
|
259
|
+
clearTimeout(guardTimer);
|
|
260
|
+
statusCapture.restore();
|
|
261
|
+
}
|
|
262
|
+
// Authoritative check: a resource-limit breach always wins, regardless
|
|
263
|
+
// of whether Lua-level execution otherwise appears to have "succeeded"
|
|
264
|
+
// (a script's own `pcall` can catch and survive the in-VM interrupt
|
|
265
|
+
// Lua-side; it can never see or clear this JS-side flag). See
|
|
266
|
+
// `./limits` for the full reasoning and the empirical evidence.
|
|
267
|
+
if (limitHandle.isBreached()) {
|
|
268
|
+
const kind = limitHandle.breachKind();
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: {
|
|
272
|
+
kind: 'limit',
|
|
273
|
+
limit: kind,
|
|
274
|
+
message: `script exceeded its ${kind ?? 'resource'} limit`,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (runResult.kind === 'error') {
|
|
279
|
+
// Defect 3: the external wall-clock guard's own sentinel error,
|
|
280
|
+
// identified by class identity (never by message) — see the guard's
|
|
281
|
+
// construction above.
|
|
282
|
+
if (runResult.err instanceof ScriptLimitError) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
error: {
|
|
286
|
+
kind: 'limit',
|
|
287
|
+
limit: runResult.err.limitKind,
|
|
288
|
+
message: `script exceeded its ${runResult.err.limitKind} limit`,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Defect 2: a genuine, uncaught memory-cap breach, identified by the
|
|
293
|
+
// non-spoofable raw LuaReturn status code — see
|
|
294
|
+
// `captureAssertOkStatus`'s doc comment.
|
|
295
|
+
if (statusCapture.lastStatus() === LuaReturn.ErrorMem) {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
error: {
|
|
299
|
+
kind: 'limit',
|
|
300
|
+
limit: 'memory',
|
|
301
|
+
message: 'script exceeded its memory limit',
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return { ok: false, error: classifyRuntimeError(runResult.err) };
|
|
306
|
+
}
|
|
307
|
+
const finalized = finalizeMarshaledValue(runResult.value);
|
|
308
|
+
if (!finalized.ok) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: {
|
|
312
|
+
kind: 'marshal',
|
|
313
|
+
reason: finalized.reason,
|
|
314
|
+
message: finalized.message,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return { ok: true, value: finalized.value };
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
limitHandle?.dispose();
|
|
322
|
+
if (thread !== undefined && threadStackIndex !== undefined) {
|
|
323
|
+
try {
|
|
324
|
+
if (!engine.global.isClosed()) {
|
|
325
|
+
engine.global.remove(threadStackIndex);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Best-effort cleanup only; the engine is closed unconditionally next.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!engine.global.isClosed()) {
|
|
333
|
+
engine.global.close();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markii/lua",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sandboxed Lua 5.4 (wasmoon) execution runtime for Mark's document scripting: an empty-env global whitelist, two-tier capability-gated net/cache/bundle access, instruction-count/wall-clock/memory limits, and depth/size-capped Lua<->JS marshaling.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"mark",
|
|
8
|
+
"mk.md",
|
|
9
|
+
"lua",
|
|
10
|
+
"wasmoon",
|
|
11
|
+
"sandbox",
|
|
12
|
+
"scripting"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "sadigaxund",
|
|
16
|
+
"homepage": "https://github.com/sadigaxund/markii#readme",
|
|
17
|
+
"bugs": "https://github.com/sadigaxund/markii/issues",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/sadigaxund/markii.git",
|
|
21
|
+
"directory": "packages/markii-lua"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"build": "tsc --noEmit -p tsconfig.json",
|
|
43
|
+
"build:dist": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
44
|
+
"lint": "eslint ."
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@markii/bundle": "0.1.0",
|
|
48
|
+
"@markii/runtime": "0.1.0",
|
|
49
|
+
"wasmoon": "^1.16.0"
|
|
50
|
+
}
|
|
51
|
+
}
|