@markii/lua 0.12.1 → 0.13.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/dist/capabilities.d.ts +1 -1
- package/dist/capabilities.js +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/json-table.d.ts +88 -0
- package/dist/json-table.js +151 -0
- package/dist/sandbox.d.ts +4 -1
- package/dist/sandbox.js +33 -2
- package/package.json +3 -3
package/dist/capabilities.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface CacheEntry {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Host-provided cache primitive backing `cache.get(key, ttl, fn)`. Real
|
|
27
|
-
* persistence (bundle
|
|
27
|
+
* persistence (bundle `.cache/`, IndexedDB, whatever the host uses) is the
|
|
28
28
|
* host's concern; this package only defines the read-if-fresh-else-run-fn
|
|
29
29
|
* contract.
|
|
30
30
|
*/
|
package/dist/capabilities.js
CHANGED
|
@@ -422,7 +422,7 @@ net.patch = function(url, body) return __smd_net_patch_blocked(url, body):await(
|
|
|
422
422
|
// JSON-safe result is handed to `config.cache!.set` — so
|
|
423
423
|
// `CacheEntry.value`'s STORAGE shape is unchanged (still whatever
|
|
424
424
|
// plain value the host's `CacheProvider` already expects; e.g. a
|
|
425
|
-
// bundle's
|
|
425
|
+
// bundle's `.cache/*.json` file), and a script's own scalar values
|
|
426
426
|
// (numbers, strings, booleans) round-trip exactly as before.
|
|
427
427
|
// - Either enforcement failing raises the existing, already-classified
|
|
428
428
|
// `MARSHAL_ERROR_TAG` error (`sandbox.ts` already recognizes it as
|
|
@@ -438,7 +438,7 @@ net.patch = function(url, body) return __smd_net_patch_blocked(url, body):await(
|
|
|
438
438
|
// to text and handed to Lua — mirrors `net.fetch_json`'s own
|
|
439
439
|
// pre-check exactly (adversarial finding B2). A host-stored value is
|
|
440
440
|
// exactly as untrusted as a remote fetch body — a bundle's
|
|
441
|
-
//
|
|
441
|
+
// `.cache/*.json` file, for instance, can be edited by anything with
|
|
442
442
|
// write access to the bundle, not just this sandbox's own WRITE side
|
|
443
443
|
// below — so without this check, a 300k-element cached array reached
|
|
444
444
|
// the script completely uncapped even though the FETCH path was
|
|
@@ -557,7 +557,7 @@ end
|
|
|
557
557
|
}
|
|
558
558
|
// --- bundle -------------------------------------------------------------
|
|
559
559
|
// Delegates entirely to the injected `ScriptView` (`@markii/bundle`), which
|
|
560
|
-
// already enforces the path-jail and the read/write
|
|
560
|
+
// already enforces the path-jail and the read/write:.cache/ split (spec
|
|
561
561
|
// §11). This module adds nothing on top except the tier gate for
|
|
562
562
|
// `bundle.write` (a tier-blocked stub under 'auto' — read-only tier) and
|
|
563
563
|
// the byte<->Lua-string conversion.
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type { CacheEntry, CacheProvider, CapabilityConfig, CapabilityDenial, Cap
|
|
|
8
8
|
export { DEFAULT_MAX_FETCH_BYTES, bytesToLuaString, buildCapabilities, isNetProviderDenial, luaStringToBytes, netProviderDenial, } from './capabilities.js';
|
|
9
9
|
export type { DocConfig } from './doc.js';
|
|
10
10
|
export { buildDoc } from './doc.js';
|
|
11
|
+
export type { JsonTableConfig } from './json-table.js';
|
|
12
|
+
export { buildJsonTable } from './json-table.js';
|
|
11
13
|
export type { MarshalLimits } from './marshal.js';
|
|
12
14
|
export { DEFAULT_MARSHAL_LIMITS, buildMarshalPrelude, checkJsonWithinLimits, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
13
15
|
export type { PackModuleResolver, RequireConfig } from './require.js';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { ALLOWED_GLOBALS, DENIED_GLOBALS, createEmptyLuaEngine, } from './global
|
|
|
8
8
|
export { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
9
9
|
export { DEFAULT_MAX_FETCH_BYTES, bytesToLuaString, buildCapabilities, isNetProviderDenial, luaStringToBytes, netProviderDenial, } from './capabilities.js';
|
|
10
10
|
export { buildDoc } from './doc.js';
|
|
11
|
+
export { buildJsonTable } from './json-table.js';
|
|
11
12
|
export { DEFAULT_MARSHAL_LIMITS, buildMarshalPrelude, checkJsonWithinLimits, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
12
13
|
export { buildRequire } from './require.js';
|
|
13
14
|
export { runScript } from './sandbox.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type MarshalLimits } from './marshal.js';
|
|
2
|
+
export interface JsonTableConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Depth/node budget applied to a `json.decode` input and, via
|
|
5
|
+
* `__smd_marshal_root`, to a `json.encode` value. The SAME `MarshalLimits`
|
|
6
|
+
* `./sandbox` already uses for the marshal prelude and `./capabilities`
|
|
7
|
+
* uses for `net.fetch_json` -- never a second, independently-tuned cap.
|
|
8
|
+
*/
|
|
9
|
+
limits: MarshalLimits;
|
|
10
|
+
/**
|
|
11
|
+
* Byte-size cap on the TEXT handed to `json.decode`, checked before the
|
|
12
|
+
* text is JSON-parsed at all. The SAME cap `net.fetch_json` applies to a
|
|
13
|
+
* fetched response body (`./capabilities`' `maxFetchBytes`), reused here
|
|
14
|
+
* rather than forked.
|
|
15
|
+
*/
|
|
16
|
+
maxFetchBytes: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Builds the raw JS globals and trusted Lua prelude for the `json` table:
|
|
20
|
+
* `json.decode(text)` and `json.encode(value)` (GitHub issue #40, slice 3a).
|
|
21
|
+
*
|
|
22
|
+
* `json` is pure computation with no I/O -- it never touches the network,
|
|
23
|
+
* the bundle filesystem, or the clock -- so `./sandbox` injects this
|
|
24
|
+
* unconditionally, for every tier, with no capability grant required. This
|
|
25
|
+
* mirrors how `./marshal`'s and `./doc`'s preludes are already injected
|
|
26
|
+
* unconditionally; `json` joins that same "always present" set rather than
|
|
27
|
+
* being wired through `./capabilities`' tier/grant machinery, which exists
|
|
28
|
+
* specifically to gate effectful or ambient-authority operations this
|
|
29
|
+
* table has none of.
|
|
30
|
+
*
|
|
31
|
+
* ## `json.decode`: reuses the EXISTING decoder and EXISTING budget
|
|
32
|
+
*
|
|
33
|
+
* The actual parse is `./json-decode`'s `__smd_json_decode` -- the same
|
|
34
|
+
* trusted, in-Lua JSON decoder `net.fetch_json`/`cache.get` already use in
|
|
35
|
+
* `./capabilities` (see that module's doc comment for why decoding must
|
|
36
|
+
* happen entirely in Lua rather than trusting wasmoon's own JS-object
|
|
37
|
+
* conversion). This function never writes a second decoder.
|
|
38
|
+
*
|
|
39
|
+
* Before that decoder ever sees the text, a JS-side precheck applies the
|
|
40
|
+
* exact same two-part budget `net.fetch_json` applies to a fetched body:
|
|
41
|
+
* the `maxFetchBytes` byte-size cap (an ordinary, untagged `error()`,
|
|
42
|
+
* `kind: 'runtime'` -- no `ScriptMarshalReason` value fits "too many
|
|
43
|
+
* bytes"), and `./marshal`'s `checkJsonWithinLimits` (the very function
|
|
44
|
+
* `./capabilities` calls) against the `JSON.parse`d value, tagged
|
|
45
|
+
* `MARK_MARSHAL:depth`/`MARK_MARSHAL:nodes` so `./sandbox`'s
|
|
46
|
+
* `classifyRuntimeError` reports it as `kind: 'marshal'`, `reason: 'depth'
|
|
47
|
+
* | 'nodes'` -- the same classification a `json.encode` budget violation
|
|
48
|
+
* gets (see below), so a script sees one consistent failure shape for "too
|
|
49
|
+
* deep or too wide" regardless of which direction it happened. Text that
|
|
50
|
+
* fails to `JSON.parse` at all is NOT diagnosed here in JS: it is
|
|
51
|
+
* handed straight to `__smd_json_decode`, which raises its own "malformed"
|
|
52
|
+
* error -- duplicating that diagnosis in two places risks the two
|
|
53
|
+
* disagreeing, and the Lua decoder's message is the one already documented.
|
|
54
|
+
*
|
|
55
|
+
* ## `json.encode`: reuses the EXISTING marshal walk
|
|
56
|
+
*
|
|
57
|
+
* `json.encode(value)` runs `value` through `__smd_marshal_root`
|
|
58
|
+
* (`./marshal`'s `buildMarshalPrelude`, already injected once per engine
|
|
59
|
+
* before this prelude runs) -- the SAME depth/node-capped, cycle-detecting,
|
|
60
|
+
* type-checking walk a script's own top-level return value already goes
|
|
61
|
+
* through. No second walk is written. The capped, marker-tagged result then
|
|
62
|
+
* crosses to JS as a single bounded function argument (safe by construction:
|
|
63
|
+
* the walk has already capped it, exactly as `./capabilities`' `cache.get`
|
|
64
|
+
* write path already relies on for the same reason), where
|
|
65
|
+
* `finalizeMarshaledValue` -- the same JS-side pass `./sandbox` runs on a
|
|
66
|
+
* script's return value -- strips the array marker and rejects a
|
|
67
|
+
* non-finite number, before the result is `JSON.stringify`d. A cycle,
|
|
68
|
+
* excess depth/nodes, a non-string table key, or a function/userdata/thread
|
|
69
|
+
* all raise the walk's existing `MARK_MARSHAL:<reason>` tag, so `json.encode`
|
|
70
|
+
* failures classify exactly like a script's own return-value marshal
|
|
71
|
+
* failures: `kind: 'marshal'`, with `reason` one of `./marshal`'s existing
|
|
72
|
+
* `ScriptMarshalReason` values.
|
|
73
|
+
*
|
|
74
|
+
* ## Rebinding safety
|
|
75
|
+
*
|
|
76
|
+
* `json.decode`/`json.encode` themselves close over `type`, `error`,
|
|
77
|
+
* `__smd_json_decode`, and `__smd_marshal_root` captured into locals AT
|
|
78
|
+
* PRELUDE-DEFINITION TIME -- before any untrusted script code runs -- so a
|
|
79
|
+
* later script reassigning any of those globals (or `json` itself) cannot
|
|
80
|
+
* neuter the type check or reach past the reused decoder/marshal walk. This
|
|
81
|
+
* is the same discipline `./json-decode` (finding A1) and `./capabilities`
|
|
82
|
+
* (findings A2/D1) already apply to their own internal calls into these
|
|
83
|
+
* exact two functions.
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildJsonTable(config: JsonTableConfig): {
|
|
86
|
+
rawGlobals: Record<string, (...args: never[]) => Promise<unknown>>;
|
|
87
|
+
preludeLua: string;
|
|
88
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { MARSHAL_ERROR_TAG } from './errors.js';
|
|
2
|
+
import { buildJsonDecodePrelude } from './json-decode.js';
|
|
3
|
+
import { checkJsonWithinLimits, finalizeMarshaledValue, } from './marshal.js';
|
|
4
|
+
/**
|
|
5
|
+
* Builds the raw JS globals and trusted Lua prelude for the `json` table:
|
|
6
|
+
* `json.decode(text)` and `json.encode(value)` (GitHub issue #40, slice 3a).
|
|
7
|
+
*
|
|
8
|
+
* `json` is pure computation with no I/O -- it never touches the network,
|
|
9
|
+
* the bundle filesystem, or the clock -- so `./sandbox` injects this
|
|
10
|
+
* unconditionally, for every tier, with no capability grant required. This
|
|
11
|
+
* mirrors how `./marshal`'s and `./doc`'s preludes are already injected
|
|
12
|
+
* unconditionally; `json` joins that same "always present" set rather than
|
|
13
|
+
* being wired through `./capabilities`' tier/grant machinery, which exists
|
|
14
|
+
* specifically to gate effectful or ambient-authority operations this
|
|
15
|
+
* table has none of.
|
|
16
|
+
*
|
|
17
|
+
* ## `json.decode`: reuses the EXISTING decoder and EXISTING budget
|
|
18
|
+
*
|
|
19
|
+
* The actual parse is `./json-decode`'s `__smd_json_decode` -- the same
|
|
20
|
+
* trusted, in-Lua JSON decoder `net.fetch_json`/`cache.get` already use in
|
|
21
|
+
* `./capabilities` (see that module's doc comment for why decoding must
|
|
22
|
+
* happen entirely in Lua rather than trusting wasmoon's own JS-object
|
|
23
|
+
* conversion). This function never writes a second decoder.
|
|
24
|
+
*
|
|
25
|
+
* Before that decoder ever sees the text, a JS-side precheck applies the
|
|
26
|
+
* exact same two-part budget `net.fetch_json` applies to a fetched body:
|
|
27
|
+
* the `maxFetchBytes` byte-size cap (an ordinary, untagged `error()`,
|
|
28
|
+
* `kind: 'runtime'` -- no `ScriptMarshalReason` value fits "too many
|
|
29
|
+
* bytes"), and `./marshal`'s `checkJsonWithinLimits` (the very function
|
|
30
|
+
* `./capabilities` calls) against the `JSON.parse`d value, tagged
|
|
31
|
+
* `MARK_MARSHAL:depth`/`MARK_MARSHAL:nodes` so `./sandbox`'s
|
|
32
|
+
* `classifyRuntimeError` reports it as `kind: 'marshal'`, `reason: 'depth'
|
|
33
|
+
* | 'nodes'` -- the same classification a `json.encode` budget violation
|
|
34
|
+
* gets (see below), so a script sees one consistent failure shape for "too
|
|
35
|
+
* deep or too wide" regardless of which direction it happened. Text that
|
|
36
|
+
* fails to `JSON.parse` at all is NOT diagnosed here in JS: it is
|
|
37
|
+
* handed straight to `__smd_json_decode`, which raises its own "malformed"
|
|
38
|
+
* error -- duplicating that diagnosis in two places risks the two
|
|
39
|
+
* disagreeing, and the Lua decoder's message is the one already documented.
|
|
40
|
+
*
|
|
41
|
+
* ## `json.encode`: reuses the EXISTING marshal walk
|
|
42
|
+
*
|
|
43
|
+
* `json.encode(value)` runs `value` through `__smd_marshal_root`
|
|
44
|
+
* (`./marshal`'s `buildMarshalPrelude`, already injected once per engine
|
|
45
|
+
* before this prelude runs) -- the SAME depth/node-capped, cycle-detecting,
|
|
46
|
+
* type-checking walk a script's own top-level return value already goes
|
|
47
|
+
* through. No second walk is written. The capped, marker-tagged result then
|
|
48
|
+
* crosses to JS as a single bounded function argument (safe by construction:
|
|
49
|
+
* the walk has already capped it, exactly as `./capabilities`' `cache.get`
|
|
50
|
+
* write path already relies on for the same reason), where
|
|
51
|
+
* `finalizeMarshaledValue` -- the same JS-side pass `./sandbox` runs on a
|
|
52
|
+
* script's return value -- strips the array marker and rejects a
|
|
53
|
+
* non-finite number, before the result is `JSON.stringify`d. A cycle,
|
|
54
|
+
* excess depth/nodes, a non-string table key, or a function/userdata/thread
|
|
55
|
+
* all raise the walk's existing `MARK_MARSHAL:<reason>` tag, so `json.encode`
|
|
56
|
+
* failures classify exactly like a script's own return-value marshal
|
|
57
|
+
* failures: `kind: 'marshal'`, with `reason` one of `./marshal`'s existing
|
|
58
|
+
* `ScriptMarshalReason` values.
|
|
59
|
+
*
|
|
60
|
+
* ## Rebinding safety
|
|
61
|
+
*
|
|
62
|
+
* `json.decode`/`json.encode` themselves close over `type`, `error`,
|
|
63
|
+
* `__smd_json_decode`, and `__smd_marshal_root` captured into locals AT
|
|
64
|
+
* PRELUDE-DEFINITION TIME -- before any untrusted script code runs -- so a
|
|
65
|
+
* later script reassigning any of those globals (or `json` itself) cannot
|
|
66
|
+
* neuter the type check or reach past the reused decoder/marshal walk. This
|
|
67
|
+
* is the same discipline `./json-decode` (finding A1) and `./capabilities`
|
|
68
|
+
* (findings A2/D1) already apply to their own internal calls into these
|
|
69
|
+
* exact two functions.
|
|
70
|
+
*/
|
|
71
|
+
export function buildJsonTable(config) {
|
|
72
|
+
const { limits, maxFetchBytes } = config;
|
|
73
|
+
const rawGlobals = {};
|
|
74
|
+
rawGlobals.__smd_json_decode_precheck_raw = (async (text) => {
|
|
75
|
+
if (typeof text === 'string' && text.length > maxFetchBytes) {
|
|
76
|
+
// No `ScriptMarshalReason` value fits "too many bytes" (that taxonomy
|
|
77
|
+
// is depth/nodes/cycle/type/key-type/non-finite-number/nul-byte, none
|
|
78
|
+
// of which is a byte-size cap), so this is left untagged: an ordinary
|
|
79
|
+
// Lua error, classified `kind: 'runtime'` by `./sandbox` like any
|
|
80
|
+
// other `error()` call. It is still a clean, bounded, descriptive
|
|
81
|
+
// failure -- never a hang, never a crash -- which is what matters.
|
|
82
|
+
throw new Error(`json.decode: input exceeds the ${maxFetchBytes}-byte limit`);
|
|
83
|
+
}
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not valid JSON text: let `__smd_json_decode` (the Lua decoder)
|
|
90
|
+
// raise its own "malformed" error rather than diagnosing this twice,
|
|
91
|
+
// in two places, with two possibly-inconsistent messages.
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const check = checkJsonWithinLimits(parsed, limits);
|
|
95
|
+
if (!check.ok) {
|
|
96
|
+
throw new Error(`${MARSHAL_ERROR_TAG}:${check.reason}: json.decode input ${check.message}`);
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
rawGlobals.__smd_json_encode_raw = (async (marshaledValue) => {
|
|
101
|
+
const finalized = finalizeMarshaledValue(marshaledValue);
|
|
102
|
+
if (!finalized.ok) {
|
|
103
|
+
throw new Error(`${MARSHAL_ERROR_TAG}:${finalized.reason}`);
|
|
104
|
+
}
|
|
105
|
+
const text = JSON.stringify(finalized.value);
|
|
106
|
+
// `JSON.stringify` only returns `undefined` for a value it cannot
|
|
107
|
+
// represent -- already excluded by `finalizeMarshaledValue` above -- so
|
|
108
|
+
// this is a defensive fallback that is never expected to trigger.
|
|
109
|
+
return text === undefined ? 'null' : text;
|
|
110
|
+
});
|
|
111
|
+
const preludeLua = `
|
|
112
|
+
${buildJsonDecodePrelude(limits)}
|
|
113
|
+
|
|
114
|
+
-- Captured into locals HERE, at prelude-definition time -- see the module
|
|
115
|
+
-- doc comment's "Rebinding safety" section.
|
|
116
|
+
local __smd_json_type = type
|
|
117
|
+
local __smd_json_error = error
|
|
118
|
+
local __smd_json_decode_precheck = __smd_json_decode_precheck_raw
|
|
119
|
+
local __smd_json_encode_finish = __smd_json_encode_raw
|
|
120
|
+
local __smd_json_decode_fn = __smd_json_decode
|
|
121
|
+
local __smd_json_marshal_root = __smd_marshal_root
|
|
122
|
+
__smd_json_decode_precheck_raw = nil
|
|
123
|
+
__smd_json_encode_raw = nil
|
|
124
|
+
-- \`__smd_json_decode\` (defined by the injected \`./json-decode\` prelude
|
|
125
|
+
-- above) is only ever needed as a GLOBAL long enough for callers to capture
|
|
126
|
+
-- it into their own local -- exactly like \`__smd_net_get_json_decode\` and
|
|
127
|
+
-- \`__smd_cache_json_decode\` already do in \`./capabilities\`. This prelude
|
|
128
|
+
-- runs AFTER any of those (see \`./sandbox\`'s injection order), so every
|
|
129
|
+
-- earlier consumer has already captured its own reference by this point;
|
|
130
|
+
-- nilling the global here keeps \`json\` from being the run that leaves a
|
|
131
|
+
-- private \`__smd_\` name reachable on every single run (it is now injected
|
|
132
|
+
-- UNCONDITIONALLY, unlike \`net\`/\`cache\`, which only define this global
|
|
133
|
+
-- when actually configured) -- see the pass-3 residue probes
|
|
134
|
+
-- (\`require-pass3.probe.test.ts\`, \`doc.probe.test.ts\`), which assert NO
|
|
135
|
+
-- \`__smd_\`-prefixed global survives except \`__smd_marshal_root\`.
|
|
136
|
+
__smd_json_decode = nil
|
|
137
|
+
|
|
138
|
+
json = json or {}
|
|
139
|
+
json.decode = function(text)
|
|
140
|
+
if __smd_json_type(text) ~= "string" then
|
|
141
|
+
__smd_json_error("json.decode expects a string argument")
|
|
142
|
+
end
|
|
143
|
+
__smd_json_decode_precheck(text):await()
|
|
144
|
+
return __smd_json_decode_fn(text)
|
|
145
|
+
end
|
|
146
|
+
json.encode = function(value)
|
|
147
|
+
return __smd_json_encode_finish(__smd_json_marshal_root(value)):await()
|
|
148
|
+
end
|
|
149
|
+
`;
|
|
150
|
+
return { rawGlobals, preludeLua };
|
|
151
|
+
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -63,7 +63,10 @@ export type RunScriptResult = {
|
|
|
63
63
|
* the real `require` global, sharing the same `bundle`/denial-recording
|
|
64
64
|
* wiring (§8's bundle-local and pack-namespaced module sources).
|
|
65
65
|
* 4. `./marshal` — inject the trusted node/depth-capped marshal walk that
|
|
66
|
-
* the wrapped user code's return value is piped through
|
|
66
|
+
* the wrapped user code's return value is piped through, then
|
|
67
|
+
* `./json-table` — the `json.decode`/`json.encode` table, unconditional
|
|
68
|
+
* and ungated (pure computation, no capability grant), reusing that
|
|
69
|
+
* same marshal walk and `./json-decode`'s existing decoder.
|
|
67
70
|
* 5. A dedicated child thread (NOT `engine.doString`, which creates its
|
|
68
71
|
* own internal thread we'd have no handle to — see `./limits`'s "hooks
|
|
69
72
|
* are per-thread" note) gets the instruction/wall-clock hook installed,
|
package/dist/sandbox.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { LuaReturn } from 'wasmoon';
|
|
2
|
-
import { buildCapabilities, } from './capabilities.js';
|
|
2
|
+
import { buildCapabilities, DEFAULT_MAX_FETCH_BYTES, } from './capabilities.js';
|
|
3
3
|
import { buildDoc } from './doc.js';
|
|
4
4
|
import { MARSHAL_ERROR_TAG, ScriptLimitError } from './errors.js';
|
|
5
5
|
import { createEmptyLuaEngine } from './globals.js';
|
|
6
|
+
import { buildJsonTable } from './json-table.js';
|
|
6
7
|
import { DEFAULT_LIMITS, installLimits } from './limits.js';
|
|
7
8
|
import { buildMarshalPrelude, DEFAULT_MARSHAL_LIMITS, finalizeMarshaledValue, wrapUserCode, } from './marshal.js';
|
|
8
9
|
import { buildRequire } from './require.js';
|
|
@@ -109,6 +110,20 @@ function extractMarshalReason(message) {
|
|
|
109
110
|
return 'key-type';
|
|
110
111
|
case 'nul-byte':
|
|
111
112
|
return 'nul-byte';
|
|
113
|
+
case 'non-finite-number':
|
|
114
|
+
// Unlike the other cases above (all raised from INSIDE the Lua-side
|
|
115
|
+
// `__smd_marshal` walk itself), a non-finite number is normally
|
|
116
|
+
// caught by `finalizeMarshaledValue` running directly in JS on a
|
|
117
|
+
// successful run's already-returned value -- it never crosses back
|
|
118
|
+
// through Lua as a thrown, tagged error on that path, so this case
|
|
119
|
+
// was unreached before `./json-table` existed. `json.encode` reuses
|
|
120
|
+
// `finalizeMarshaledValue` itself (the same JS-side check, the same
|
|
121
|
+
// reason) but calls it from inside a host function invoked FROM Lua,
|
|
122
|
+
// so its rejection DOES round-trip through a thrown Lua error and
|
|
123
|
+
// land here -- this case keeps that reused reason classified
|
|
124
|
+
// identically to the return-value path, instead of falling through
|
|
125
|
+
// to the generic 'type' default.
|
|
126
|
+
return 'non-finite-number';
|
|
112
127
|
case 'type':
|
|
113
128
|
return 'type';
|
|
114
129
|
default:
|
|
@@ -179,7 +194,10 @@ function classifyRuntimeError(err) {
|
|
|
179
194
|
* the real `require` global, sharing the same `bundle`/denial-recording
|
|
180
195
|
* wiring (§8's bundle-local and pack-namespaced module sources).
|
|
181
196
|
* 4. `./marshal` — inject the trusted node/depth-capped marshal walk that
|
|
182
|
-
* the wrapped user code's return value is piped through
|
|
197
|
+
* the wrapped user code's return value is piped through, then
|
|
198
|
+
* `./json-table` — the `json.decode`/`json.encode` table, unconditional
|
|
199
|
+
* and ungated (pure computation, no capability grant), reusing that
|
|
200
|
+
* same marshal walk and `./json-decode`'s existing decoder.
|
|
183
201
|
* 5. A dedicated child thread (NOT `engine.doString`, which creates its
|
|
184
202
|
* own internal thread we'd have no handle to — see `./limits`'s "hooks
|
|
185
203
|
* are per-thread" note) gets the instruction/wall-clock hook installed,
|
|
@@ -261,6 +279,19 @@ export async function runScript(options) {
|
|
|
261
279
|
throw new Error(`sandbox assembly left a code-loading primitive reachable (${String(loadResidue)}); refusing to run`);
|
|
262
280
|
}
|
|
263
281
|
await engine.doString(buildMarshalPrelude(marshalLimits));
|
|
282
|
+
// ./json-table: the `json` table (GitHub issue #40, slice 3a). Pure
|
|
283
|
+
// computation, no I/O -- injected unconditionally, for every tier, with
|
|
284
|
+
// no capability grant, same as the marshal/doc preludes below and
|
|
285
|
+
// above it. Wired AFTER the marshal prelude because `json.encode`
|
|
286
|
+
// reuses `__smd_marshal_root`, defined by that prelude.
|
|
287
|
+
const jsonTable = buildJsonTable({
|
|
288
|
+
limits: marshalLimits,
|
|
289
|
+
maxFetchBytes: options.maxFetchBytes ?? DEFAULT_MAX_FETCH_BYTES,
|
|
290
|
+
});
|
|
291
|
+
for (const [name, fn] of Object.entries(jsonTable.rawGlobals)) {
|
|
292
|
+
engine.global.set(name, fn);
|
|
293
|
+
}
|
|
294
|
+
await engine.doString(jsonTable.preludeLua);
|
|
264
295
|
// ./doc: the note-scoped read-only view (GitHub issue #33). Wired
|
|
265
296
|
// AFTER the marshal prelude and, like `require`, wired unconditionally
|
|
266
297
|
// — with no `options.doc` it is an empty listing, never an absent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/lua",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"lint": "eslint ."
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@markii/bundle": "^0.
|
|
48
|
-
"@markii/runtime": "^0.
|
|
47
|
+
"@markii/bundle": "^0.13.0",
|
|
48
|
+
"@markii/runtime": "^0.13.0",
|
|
49
49
|
"wasmoon": "^1.16.0"
|
|
50
50
|
}
|
|
51
51
|
}
|