@markii/lua 0.13.0 → 0.15.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.js +123 -144
- package/package.json +3 -3
package/dist/capabilities.js
CHANGED
|
@@ -198,63 +198,72 @@ export function buildCapabilities(config) {
|
|
|
198
198
|
// rather than behind one combined condition — an earlier version of
|
|
199
199
|
// this function nested POST/PATCH wiring inside "if GET is granted",
|
|
200
200
|
// which silently produced no `net.post` at all for a POST-only grant.
|
|
201
|
+
//
|
|
202
|
+
// The `net` table itself, and every one of its three methods, is now
|
|
203
|
+
// ALWAYS wired, regardless of grants, provider wiring, or tier (GitHub
|
|
204
|
+
// capability-stub finding, batch 7 #47). Before this, an ungranted
|
|
205
|
+
// script saw a missing global (`net` itself omitted when nothing was
|
|
206
|
+
// granted, or a missing method when a specific host/verb wasn't) and
|
|
207
|
+
// failed with an ordinary Lua "attempt to index/call a nil value" —
|
|
208
|
+
// indistinguishable, from inside the script and from a host reading
|
|
209
|
+
// `runScript`'s outcome, from a plain typo. A denied capability and a
|
|
210
|
+
// typo must never look the same (see `./errors`'s module doc comment).
|
|
211
|
+
// Each method below performs its own grant/tier check and records a
|
|
212
|
+
// denial through `recordDenial` (the non-spoofable `CapabilityDenials`
|
|
213
|
+
// handle `sandbox.ts` reads) before throwing — never a typed error
|
|
214
|
+
// string, and never anything a script's own `error()` call could forge;
|
|
215
|
+
// see `./errors.ts`'s `CAPABILITY_ERROR_TAG` doc comment for why the
|
|
216
|
+
// message text itself must never become a classification signal. One
|
|
217
|
+
// side effect, expected and documented in CHANGELOG: a script that does
|
|
218
|
+
// `if net then ... end` to feature-detect now always sees a table.
|
|
201
219
|
const netGrants = config.netGrants ?? { get: [], post: [] };
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// breaks `type()`/`#`/marshaling a nested return value, and silently
|
|
250
|
-
// raises on a `null` field read). Strings, unlike objects/arrays, are
|
|
251
|
-
// scalars and cross the boundary cleanly with no proxy involved; the
|
|
252
|
-
// prelude below decodes this text into a genuine Lua table entirely
|
|
253
|
-
// in Lua (`__smd_json_decode`, `./json-decode`).
|
|
254
|
-
return res.body;
|
|
255
|
-
});
|
|
256
|
-
ensureJsonDecodePrelude();
|
|
257
|
-
preludeParts.push(`
|
|
220
|
+
preludeParts.push('net = net or {}\n');
|
|
221
|
+
rawGlobals.__smd_net_get_raw = (async (url) => {
|
|
222
|
+
const host = hostnameOf(url);
|
|
223
|
+
if (!config.net || !host || !netGrants.get.includes(host)) {
|
|
224
|
+
const message = `net access to host "${host ?? url}" not granted for GET`;
|
|
225
|
+
recordDenial('denied', message);
|
|
226
|
+
throw capabilityError(message);
|
|
227
|
+
}
|
|
228
|
+
const res = await callNetProvider(() => config.net.get(url));
|
|
229
|
+
if (res.body.length > maxFetchBytes) {
|
|
230
|
+
const message = `fetch response for "${url}" exceeds the ${maxFetchBytes}-byte cap`;
|
|
231
|
+
recordDenial('denied', message);
|
|
232
|
+
throw capabilityError(message);
|
|
233
|
+
}
|
|
234
|
+
let parsed;
|
|
235
|
+
try {
|
|
236
|
+
parsed = JSON.parse(res.body);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
const message = `fetch response for "${url}" was not valid JSON`;
|
|
240
|
+
recordDenial('denied', message);
|
|
241
|
+
throw capabilityError(message);
|
|
242
|
+
}
|
|
243
|
+
// Depth/node budget, checked HERE on the plain parsed JS value and
|
|
244
|
+
// BEFORE the raw text is ever handed to Lua — see `./json-decode`'s
|
|
245
|
+
// doc comment (GitHub issue #6) for why decoding happens entirely in
|
|
246
|
+
// Lua, and `MarshalLimits`' doc comment above for why this reuses the
|
|
247
|
+
// same budget the return-value marshal walk already enforces.
|
|
248
|
+
const budgetCheck = checkJsonWithinLimits(parsed, marshalLimits);
|
|
249
|
+
if (!budgetCheck.ok) {
|
|
250
|
+
const message = `fetch response for "${url}" ${budgetCheck.message}`;
|
|
251
|
+
recordDenial('denied', message);
|
|
252
|
+
throw capabilityError(message);
|
|
253
|
+
}
|
|
254
|
+
// Hand back the RAW JSON TEXT, not the parsed JS value: any object or
|
|
255
|
+
// array crossing this JS->Lua boundary as-is would arrive in Lua as a
|
|
256
|
+
// wasmoon `js_proxy` userdata, not a genuine table (see
|
|
257
|
+
// `./json-decode`'s doc comment for the full mechanism and why that
|
|
258
|
+
// breaks `type()`/`#`/marshaling a nested return value, and silently
|
|
259
|
+
// raises on a `null` field read). Strings, unlike objects/arrays, are
|
|
260
|
+
// scalars and cross the boundary cleanly with no proxy involved; the
|
|
261
|
+
// prelude below decodes this text into a genuine Lua table entirely
|
|
262
|
+
// in Lua (`__smd_json_decode`, `./json-decode`).
|
|
263
|
+
return res.body;
|
|
264
|
+
});
|
|
265
|
+
ensureJsonDecodePrelude();
|
|
266
|
+
preludeParts.push(`
|
|
258
267
|
local __smd_net_get = __smd_net_get_raw
|
|
259
268
|
-- Captured into a local HERE, at prelude-definition time (this whole
|
|
260
269
|
-- prelude runs once, before any untrusted script code) -- NOT resolved as
|
|
@@ -268,39 +277,43 @@ local __smd_net_get_json_decode = __smd_json_decode
|
|
|
268
277
|
__smd_net_get_raw = nil
|
|
269
278
|
net.fetch_json = function(url) return __smd_net_get_json_decode(__smd_net_get(url):await()) end
|
|
270
279
|
`);
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// never
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
280
|
+
// POST/PATCH are effectful. Wired unconditionally like GET above: the
|
|
281
|
+
// raw handler itself decides, in this order, whether the call is
|
|
282
|
+
// ungranted (`'denied'`), tier-forbidden (`'tier-blocked'`, only reached
|
|
283
|
+
// once the host/verb IS granted, under the read-only 'auto' tier — spec
|
|
284
|
+
// §8: "An effectful call under an auto trigger fails cleanly"), or has
|
|
285
|
+
// no provider wired for that verb at all (`'denied'`, since the host
|
|
286
|
+
// never offered it), before ever reaching `config.net.post`/`.patch`.
|
|
287
|
+
rawGlobals.__smd_net_post_raw = (async (url, body) => {
|
|
288
|
+
const host = hostnameOf(url);
|
|
289
|
+
if (!host || !netGrants.post.includes(host)) {
|
|
290
|
+
const message = `net access to host "${host ?? url}" not granted for POST`;
|
|
291
|
+
recordDenial('denied', message);
|
|
292
|
+
throw capabilityError(message);
|
|
293
|
+
}
|
|
294
|
+
if (config.tier === 'auto') {
|
|
295
|
+
const message = 'net.post is granted but not permitted under the read-only auto tier (requires a manual run)';
|
|
296
|
+
recordDenial('tier-blocked', message);
|
|
297
|
+
throw capabilityError(message);
|
|
298
|
+
}
|
|
299
|
+
if (!config.net?.post) {
|
|
300
|
+
const message = `net access to host "${host}" not granted for POST`;
|
|
301
|
+
recordDenial('denied', message);
|
|
302
|
+
throw capabilityError(message);
|
|
303
|
+
}
|
|
304
|
+
const res = await callNetProvider(() => config.net.post(url, body));
|
|
305
|
+
// As with `net.fetch_json` above (GitHub issue #6): a plain JS object
|
|
306
|
+
// (even one this shallow) crosses into Lua as a `js_proxy` userdata,
|
|
307
|
+
// not a genuine table. `status`/`body` are both scalars, so instead
|
|
308
|
+
// of proxying the whole response object, resolve with a
|
|
309
|
+
// `LuaMultiReturn` — `:await()` recognizes that and expands it into
|
|
310
|
+
// TWO separate Lua return values (see `wasmoon`'s promise
|
|
311
|
+
// `await`/`MultiReturn` handling) — and let the trusted prelude below
|
|
312
|
+
// rebuild a real `{status=..., body=...}` table out of ordinary Lua
|
|
313
|
+
// table-constructor syntax.
|
|
314
|
+
return LuaMultiReturn.of(res.status, res.body);
|
|
315
|
+
});
|
|
316
|
+
preludeParts.push(`
|
|
304
317
|
local __smd_net_post = __smd_net_post_raw
|
|
305
318
|
__smd_net_post_raw = nil
|
|
306
319
|
net.post = function(url, body)
|
|
@@ -308,46 +321,29 @@ net.post = function(url, body)
|
|
|
308
321
|
return { status = status, body = respBody }
|
|
309
322
|
end
|
|
310
323
|
`);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// (`if net.post then`) would read exactly backwards.
|
|
321
|
-
config.tier === 'auto' &&
|
|
322
|
-
config.net?.post &&
|
|
323
|
-
netGrants.post.length > 0) {
|
|
324
|
-
rawGlobals.__smd_net_post_tier_blocked_raw = (async () => {
|
|
325
|
-
const message = 'net.post is granted but not permitted under the read-only auto tier (requires a manual run)';
|
|
324
|
+
rawGlobals.__smd_net_patch_raw = (async (url, body) => {
|
|
325
|
+
const host = hostnameOf(url);
|
|
326
|
+
if (!host || !netGrants.post.includes(host)) {
|
|
327
|
+
const message = `net access to host "${host ?? url}" not granted for PATCH`;
|
|
328
|
+
recordDenial('denied', message);
|
|
329
|
+
throw capabilityError(message);
|
|
330
|
+
}
|
|
331
|
+
if (config.tier === 'auto') {
|
|
332
|
+
const message = 'net.patch is granted but not permitted under the read-only auto tier (requires a manual run)';
|
|
326
333
|
recordDenial('tier-blocked', message);
|
|
327
334
|
throw capabilityError(message);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!host || !netGrants.post.includes(host)) {
|
|
341
|
-
const message = `net access to host "${host ?? url}" not granted for PATCH`;
|
|
342
|
-
recordDenial('denied', message);
|
|
343
|
-
throw capabilityError(message);
|
|
344
|
-
}
|
|
345
|
-
const res = await callNetProvider(() => config.net.patch(url, body));
|
|
346
|
-
// Same fix as `net.post` above (GitHub issue #6) — see that block's
|
|
347
|
-
// comment for the full mechanism.
|
|
348
|
-
return LuaMultiReturn.of(res.status, res.body);
|
|
349
|
-
});
|
|
350
|
-
preludeParts.push(`
|
|
335
|
+
}
|
|
336
|
+
if (!config.net?.patch) {
|
|
337
|
+
const message = `net access to host "${host}" not granted for PATCH`;
|
|
338
|
+
recordDenial('denied', message);
|
|
339
|
+
throw capabilityError(message);
|
|
340
|
+
}
|
|
341
|
+
const res = await callNetProvider(() => config.net.patch(url, body));
|
|
342
|
+
// Same fix as `net.post` above (GitHub issue #6) — see that block's
|
|
343
|
+
// comment for the full mechanism.
|
|
344
|
+
return LuaMultiReturn.of(res.status, res.body);
|
|
345
|
+
});
|
|
346
|
+
preludeParts.push(`
|
|
351
347
|
local __smd_net_patch = __smd_net_patch_raw
|
|
352
348
|
__smd_net_patch_raw = nil
|
|
353
349
|
net.patch = function(url, body)
|
|
@@ -355,23 +351,6 @@ net.patch = function(url, body)
|
|
|
355
351
|
return { status = status, body = respBody }
|
|
356
352
|
end
|
|
357
353
|
`);
|
|
358
|
-
}
|
|
359
|
-
else if (
|
|
360
|
-
// Same mirroring as the POST stub above — see its comment.
|
|
361
|
-
config.tier === 'auto' &&
|
|
362
|
-
config.net?.patch &&
|
|
363
|
-
netGrants.post.length > 0) {
|
|
364
|
-
rawGlobals.__smd_net_patch_tier_blocked_raw = (async () => {
|
|
365
|
-
const message = 'net.patch is granted but not permitted under the read-only auto tier (requires a manual run)';
|
|
366
|
-
recordDenial('tier-blocked', message);
|
|
367
|
-
throw capabilityError(message);
|
|
368
|
-
});
|
|
369
|
-
preludeParts.push(`
|
|
370
|
-
local __smd_net_patch_blocked = __smd_net_patch_tier_blocked_raw
|
|
371
|
-
__smd_net_patch_tier_blocked_raw = nil
|
|
372
|
-
net.patch = function(url, body) return __smd_net_patch_blocked(url, body):await() end
|
|
373
|
-
`);
|
|
374
|
-
}
|
|
375
354
|
// --- cache --------------------------------------------------------------
|
|
376
355
|
// cache.get is implemented ENTIRELY IN LUA (see the prelude below),
|
|
377
356
|
// calling the script-provided `fn` as a normal Lua-to-Lua call. This is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/lua",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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.15.0",
|
|
48
|
+
"@markii/runtime": "^0.15.0",
|
|
49
49
|
"wasmoon": "^1.16.0"
|
|
50
50
|
}
|
|
51
51
|
}
|