@blamejs/core 0.13.15 → 0.13.17
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/CHANGELOG.md +4 -0
- package/lib/mail-agent.js +47 -37
- package/lib/template.js +159 -23
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/simplewebauthn-server.cjs +31 -28
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.17 (2026-05-27) — **Template engine can render from a string with no views directory — for serverless / read-only filesystems.** b.template.create previously required a viewsDir that exists on disk, and rendering always read the template (and its layout/partials) from that directory — unusable on a read-only or ephemeral serverless filesystem where the templates aren't on disk. The engine now accepts a source string directly: viewsDir is optional, and the returned engine exposes renderString(source, data?, opts?) and compileString(source, opts?) that compile and render from a string with no disk read. {% extends %} and {{> partial}} in a string source resolve through an operator-supplied opts.resolve(name) -> string callback (without it, an extends throws a clear error and a missing partial inlines empty, matching the file path). The same HTML-escaping, expression grammar, and extends/partial-depth caps apply. The file-backed render / compile / precompileAll still work exactly as before when a viewsDir is configured, and now refuse with a clear error when one isn't. **Added:** *`engine.renderString` / `engine.compileString` — render templates from a string, no viewsDir* — `b.template.create({})` (no `viewsDir`) returns a string-only engine; `renderString(source, data?, { resolve })` and `compileString(source, { resolve })` compile and render from a source string with zero filesystem access — the read-only / serverless path. `{% extends %}` and `{{> partial}}` resolve through `opts.resolve(name) -> string`. The HTML escaping, grammar, and depth caps are identical to the file path. When a `viewsDir` IS configured, `render`/`compile`/`precompileAll` behave exactly as before; without one they refuse with `viewsDir not configured`. `renderString(source, { resolve })` may omit the data argument — an opts object carrying a function `resolve` is recognized as opts, not data. **Security:** *Vendored `@simplewebauthn/server` refreshed 13.3.0 → 13.3.1* — The vendored WebAuthn server bundle (`b.auth.passkey`'s registration/authentication verification) is refreshed to the latest upstream patch, with the MANIFEST version, CPE, and SHA-256 integrity hashes updated and the bundle re-verified.
|
|
12
|
+
|
|
13
|
+
- v0.13.16 (2026-05-27) — **`b.mail.agent` docs now describe the facade accurately, and not-yet-wired verbs point to the primitive to use.** b.mail.agent's module documentation claimed it was "the standardization contract for every mail protocol" that JMAP / IMAP / POP3 all route through — but no protocol server actually dispatches through the agent (the framework's own JMAP EmailSubmission handler composes b.mail.send.deliver directly), and the compose / send / reply / forward, sieve.list / sieve.activate, identity / vacation / mdn.* and export / job / import verbs throw mail-agent/not-implemented. The docs are corrected to describe what the agent is: a mailbox-access facade (RBAC + posture + audit + dispatch around a mail store) whose read surface plus the mailbox-mutation and Sieve-upload methods are wired, with the remaining verbs not yet routed through it. Those verbs' error message now names the underlying primitive to compose directly (b.mail.send.deliver, b.mail.sieve, b.mailMdn, …) instead of citing a version tag that had long passed. The public WIRED_AT export (a method→version map that no longer reflected reality) is replaced by COMPOSE_HINT (a method→primitive-to-compose map). No behaviour change: the same methods are wired or throw exactly as before. **Changed:** *`b.mail.agent` documentation corrected; not-implemented errors point to the primitive to compose* — The `@module` / `@card` no longer claim the agent is the universal protocol-dispatch contract — it's documented as a mailbox-access facade with a wired read + mutation + Sieve-upload surface, and the compose/send/identity/vacation/MDN/export verbs documented as not yet routed through it (compose the underlying primitive directly until a protocol server adopts the agent). The `mail-agent/not-implemented` error now names that primitive (e.g. `b.mail.send.deliver`) rather than a passed version tag. **Removed:** *`b.mail.agent.WIRED_AT` export replaced by `COMPOSE_HINT`* — The `WIRED_AT` export mapped each method to a framework version that was supposed to "light it up" — versions that have all shipped without the wiring, so the map was misleading. It is replaced by `COMPOSE_HINT`, mapping each not-yet-wired method to the primitive an operator composes directly. Operators reading `b.mail.agent.WIRED_AT` should read `b.mail.agent.COMPOSE_HINT` instead (pre-1.0: no compatibility shim).
|
|
14
|
+
|
|
11
15
|
- v0.13.15 (2026-05-27) — **Corrected more source citations and made deferred/reserved options honest in their docs.** A second accuracy pass over source threat-annotations and option docs. Three citation corrections: the base64url strict-decode guard cited CVE-2022-0235 (which is actually a node-fetch cookie-leak, unrelated) — it now names the weakness class it defends (CWE-347 / CWE-1286 signature canonicalization); the glob consecutive-wildcard ReDoS cap cited the wrong library (the CVE-2026-26996 ReDoS is minimatch, not picomatch — the adjacent picomatch one is CVE-2026-33671); and CVE-2026-32178 is reframed to the CWE-138 header-injection-spoofing class the public record actually documents (and dropped from the end-of-data SMTP-smuggling list, which is a different class). Several options/statuses are now honest about not-yet-implemented surface: b.archive.read.zip.fromTrustedStream is marked experimental (its methods throw and its options aren't honored yet — the example now shows the supported buffer-then-random-access path); b.acme revokeCert's useCertKey / certPrivateKey are marked reserved (the cert-key path throws; account-key signing is the supported default); and a stale message claiming passkey break-glass factors were a future feature is removed (passkeys are a live allowed factor). No runtime behaviour changes beyond message/doc text. **Changed:** *Deferred / reserved surface now documented honestly* — `b.archive.read.zip.fromTrustedStream` is marked `experimental` — its `inspect`/`entries`/`extract` throw and its `bombPolicy`/`audit` options aren't honored yet; the documented example now shows the supported path (buffer the stream, then use the random-access reader). `b.acme` `revokeCert`'s `useCertKey` / `certPrivateKey` options are marked reserved (the cert-key-signed-revocation path throws; account-key signing, the default, covers mainstream CAs). A `b.breakGlass` policy error and comment that called passkey factors a future feature are corrected — passkeys are a live allowed factor. **Fixed:** *Corrected misattributed CVE citations in source threat-annotations* — `b.crypto.fromBase64Url`'s strict-decode guard cited CVE-2022-0235 (a node-fetch header-leak, unrelated to base64/JWT decoding); it now cites the weakness class it actually defends — CWE-347 / CWE-1286 signature canonicalization. `b.guardRegex`'s consecutive-`*` cap attributed CVE-2026-26996 to picomatch; that ReDoS is in minimatch (the picomatch ReDoS it also defends is CVE-2026-33671) — the library name is corrected. CVE-2026-32178 is reframed to the CWE-138 header-injection spoofing class the public advisory documents, and removed from the end-of-data SMTP-smuggling trio (a distinct class). No behaviour change — the defenses are unchanged.
|
|
12
16
|
|
|
13
17
|
- v0.13.14 (2026-05-27) — **DNSSEC chain validation now bounds KeyTrap (CVE-2023-50387) amplification with hard caps.** b.network.dns.dnssec.verifyChain tried every DNSKEY whose 16-bit key tag matched an RRSIG, with no cap on how many candidates or total signature verifications a single response could drive. A hostile zone publishing many DNSKEYs sharing one key tag (plus matching RRSIGs) could force O(keys x signatures) full public-key verifications from one query — the KeyTrap denial-of-service (CVE-2023-50387). Validation is now bounded by non-configurable caps that match the BIND / Unbound mitigations: at most 4 same-tag candidate keys are tried per RRSIG, at most 64 DNSKEYs per zone link and 16 DS records per delegation are accepted, the chain is at most 128 links deep, and the whole response is held to a signature-validation budget that scales with chain depth (so a legitimate deep delegation is never false-rejected while bounded collisions stay bounded); exceeding any of these refuses the response rather than performing the work. Separately, a domain name that encodes to more than 255 octets is now refused at canonicalization (RFC 1035 §2.3.4), which also bounds the NSEC3 closest-encloser label enumeration, and the NSEC3 iteration ceiling is lowered from 500 to 150 to match the BIND 9.16.33+ / Unbound 1.17.1 fix for the sibling CVE-2023-50868. **Security:** *`verifyChain` caps colliding-key fan-out and total signature validations (KeyTrap / CVE-2023-50387)* — A zone advertising many same-key-tag DNSKEYs and RRSIGs can no longer drive unbounded public-key verifications. New refusals: `dnssec/too-many-colliding-keys` (>4 same-tag candidates per RRSIG), `dnssec/too-many-dnskeys` (>64 DNSKEYs per zone link), `dnssec/too-many-ds` (>16 DS records per delegation), `dnssec/too-many-links` (chain deeper than 128), and `dnssec/validation-budget-exceeded` (signature validations beyond the depth-scaled budget). The caps are intentionally non-configurable — they sit well above any legitimate zone, and the budget scales with chain depth so deep delegations validate normally. · *Domain-name octet cap + lower NSEC3 iteration ceiling* — A name that canonicalizes to more than 255 octets is refused (`dnssec/bad-name`, RFC 1035 §2.3.4), which bounds the per-label NSEC3 closest-encloser enumeration (CVE-2023-50868 class). The default NSEC3 iteration ceiling drops from 500 to 150, matching the BIND 9.16.33+ / Unbound 1.17.1 post-CVE defaults (RFC 9276 recommends 0).
|
package/lib/mail-agent.js
CHANGED
|
@@ -7,19 +7,26 @@
|
|
|
7
7
|
* @featured true
|
|
8
8
|
*
|
|
9
9
|
* @intro
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* A mailbox-access facade that owns RBAC, posture enforcement, audit
|
|
11
|
+
* emission, dispatch (local / worker-pool / queue), and worker
|
|
12
|
+
* isolation around a mail store, so a protocol server built on top
|
|
13
|
+
* can stay a thin shell. It is designed to be the shared dispatch
|
|
14
|
+
* layer mail-protocol servers route through; today the read surface
|
|
15
|
+
* and the mailbox-mutation + Sieve-upload methods are wired, while the
|
|
16
|
+
* compose/send and identity/vacation/MDN/export verbs are not yet
|
|
17
|
+
* wired into the facade (see below).
|
|
17
18
|
*
|
|
18
|
-
* `agent.create()` returns the facade. Methods backed by
|
|
19
|
-
* `b.mailStore`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* `agent.create()` returns the facade. Methods backed by
|
|
20
|
+
* `b.mailStore` (folders / fetch / search / move / flag / delete /
|
|
21
|
+
* expunge, plus `sieve.put`) run immediately. The remaining verbs —
|
|
22
|
+
* compose / send / reply / forward, sieve.list / sieve.activate,
|
|
23
|
+
* identity / vacation / mdn.*, export / job / import — throw
|
|
24
|
+
* `mail-agent/not-implemented`: they are not yet routed through the
|
|
25
|
+
* agent. Until they are, compose the underlying primitive directly
|
|
26
|
+
* (`b.mail.send.deliver` for outbound, `b.mail.sieve` for Sieve,
|
|
27
|
+
* `b.mailMdn` for MDN, etc.) — which is what the framework's own JMAP
|
|
28
|
+
* `emailSubmissionSet` handler does. They wire into the facade when a
|
|
29
|
+
* protocol server adopts the agent as its dispatch layer.
|
|
23
30
|
*
|
|
24
31
|
* ```js
|
|
25
32
|
* var agent = b.mail.agent.create({
|
|
@@ -58,9 +65,11 @@
|
|
|
58
65
|
* on every entrypoint.
|
|
59
66
|
*
|
|
60
67
|
* @card
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
68
|
+
* Mailbox-access facade — RBAC + posture + audit + dispatch around a
|
|
69
|
+
* mail store, so a protocol server on top stays a thin shell. Read +
|
|
70
|
+
* mailbox-mutation + Sieve-upload methods are wired; compose/send and
|
|
71
|
+
* identity/vacation/MDN/export verbs compose the underlying primitive
|
|
72
|
+
* directly until a protocol server routes them through the agent.
|
|
64
73
|
*/
|
|
65
74
|
|
|
66
75
|
var lazyRequire = require("./lazy-require");
|
|
@@ -118,25 +127,25 @@ var SCOPE_FOR_METHOD = Object.freeze({
|
|
|
118
127
|
import: "mail:import",
|
|
119
128
|
});
|
|
120
129
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"sieve.
|
|
131
|
-
"sieve.activate": "
|
|
132
|
-
"identity.set": "
|
|
133
|
-
"vacation.set": "
|
|
134
|
-
"mdn.send": "
|
|
135
|
-
"mdn.parse": "
|
|
136
|
-
"mdn.allowList": "
|
|
137
|
-
export: "
|
|
138
|
-
job: "
|
|
139
|
-
import: "
|
|
130
|
+
// Verbs not yet routed through the agent facade. The error points the
|
|
131
|
+
// operator at the underlying primitive to compose directly (the
|
|
132
|
+
// escape hatch) — defer-with-condition: these wire into the agent when
|
|
133
|
+
// a protocol server adopts it as its dispatch layer.
|
|
134
|
+
var COMPOSE_HINT = Object.freeze({
|
|
135
|
+
compose: "b.mail.send.deliver",
|
|
136
|
+
send: "b.mail.send.deliver",
|
|
137
|
+
reply: "b.mail.send.deliver",
|
|
138
|
+
forward: "b.mail.send.deliver",
|
|
139
|
+
"sieve.list": "b.mail.sieve",
|
|
140
|
+
"sieve.activate": "b.mail.sieve",
|
|
141
|
+
"identity.set": "your identity store + b.mail.sieve",
|
|
142
|
+
"vacation.set": "b.mail.sieve (vacation extension)",
|
|
143
|
+
"mdn.send": "b.mailMdn",
|
|
144
|
+
"mdn.parse": "b.mailMdn",
|
|
145
|
+
"mdn.allowList": "b.mailMdn",
|
|
146
|
+
export: "b.mailStore / b.auditTools",
|
|
147
|
+
job: "the dispatch queue directly",
|
|
148
|
+
import: "b.mailStore",
|
|
140
149
|
});
|
|
141
150
|
|
|
142
151
|
/**
|
|
@@ -653,9 +662,10 @@ function _notImplemented(ctx, method, args) {
|
|
|
653
662
|
// the slice lights up.
|
|
654
663
|
if (ctx.posture) guardMailQuery.validateActor(args && args.actor, ctx.posture);
|
|
655
664
|
_checkPermission(ctx, method, args);
|
|
656
|
-
ctx.auditEmit("mail.agent.not_implemented", args && args.actor, { method: method,
|
|
665
|
+
ctx.auditEmit("mail.agent.not_implemented", args && args.actor, { method: method, composeDirectly: COMPOSE_HINT[method] });
|
|
657
666
|
return Promise.reject(new MailAgentError("mail-agent/not-implemented",
|
|
658
|
-
"agent." + method + "
|
|
667
|
+
"agent." + method + " is not yet routed through the agent facade — compose " +
|
|
668
|
+
COMPOSE_HINT[method] + " directly"));
|
|
659
669
|
}
|
|
660
670
|
|
|
661
671
|
// ---- Internals ------------------------------------------------------------
|
|
@@ -771,7 +781,7 @@ module.exports = {
|
|
|
771
781
|
consumer: consumer,
|
|
772
782
|
MailAgentError: MailAgentError,
|
|
773
783
|
SCOPE_FOR_METHOD: SCOPE_FOR_METHOD,
|
|
774
|
-
|
|
784
|
+
COMPOSE_HINT: COMPOSE_HINT,
|
|
775
785
|
HEAVY_METHODS: HEAVY_METHODS,
|
|
776
786
|
// Re-export the guard family so callers can introspect without
|
|
777
787
|
// separate requires.
|
package/lib/template.js
CHANGED
|
@@ -105,6 +105,13 @@ var sandboxModule = lazyRequire(function () { return require("./sandbox"); });
|
|
|
105
105
|
// never hits it, low enough to bound a malicious / misconfigured cycle.
|
|
106
106
|
var MAX_TEMPLATE_DEPTH = 0x10;
|
|
107
107
|
|
|
108
|
+
// Byte cap for STRING-sourced templates (compileString / renderString),
|
|
109
|
+
// which accept operator-supplied — potentially untrusted — source. The
|
|
110
|
+
// file path renders trusted files on disk and is uncapped. The cap bounds
|
|
111
|
+
// the tokenizer / parser cost (and any pathological tag stream) on hostile
|
|
112
|
+
// string input; operators override per call with `opts.maxBytes`.
|
|
113
|
+
var DEFAULT_STRING_TEMPLATE_BYTES = require("./constants").BYTES.kib(256);
|
|
114
|
+
|
|
108
115
|
// ============================================================
|
|
109
116
|
// HTML escape (exported)
|
|
110
117
|
// ============================================================
|
|
@@ -176,11 +183,44 @@ function _resolvePartialPath(viewsDir, partialName) {
|
|
|
176
183
|
// ============================================================
|
|
177
184
|
|
|
178
185
|
var EXTENDS_RE = /^\s*\{%\s*extends\s+"([^"]+)"\s*%\}\s*/;
|
|
179
|
-
|
|
186
|
+
// Block open / endblock as one alternation of two fixed-shape tags (no
|
|
187
|
+
// nested quantifiers, disjoint character classes → linear). The prior
|
|
188
|
+
// single `{% block %}…{% endblock %}` regex used a lazy `[\s\S]*?` span
|
|
189
|
+
// under the global flag, which is polynomial (O(n^2)) on input with many
|
|
190
|
+
// unclosed block-opens — a ReDoS now that `renderString` feeds untrusted
|
|
191
|
+
// string templates through here. Group 1 (the block name) is present only
|
|
192
|
+
// on the open branch, which distinguishes open from close in the walk.
|
|
193
|
+
var BLOCK_TAG_RE = /\{%\s*block\s+([A-Za-z_][A-Za-z0-9_-]*)\s*%\}|\{%\s*endblock\s*%\}/g;
|
|
194
|
+
|
|
195
|
+
// Single linear left-to-right pass: pair each {% block NAME %} with the
|
|
196
|
+
// next {% endblock %} (no nesting — the first endblock closes, matching
|
|
197
|
+
// the prior lazy semantics) and replace the span with replacer(name,
|
|
198
|
+
// content). `matchAll` walks the tag stream once; no backtracking.
|
|
199
|
+
function _replaceBlocks(source, replacer) {
|
|
200
|
+
var out = "";
|
|
201
|
+
var pos = 0;
|
|
202
|
+
var openMatch = null;
|
|
203
|
+
var iter = source.matchAll(BLOCK_TAG_RE);
|
|
204
|
+
var m = iter.next();
|
|
205
|
+
while (!m.done) {
|
|
206
|
+
var tag = m.value;
|
|
207
|
+
if (tag[1] !== undefined) { // open tag (block name captured)
|
|
208
|
+
if (!openMatch) openMatch = tag; // ignore nested opens until the close
|
|
209
|
+
} else if (openMatch) { // close tag with an open pending
|
|
210
|
+
var contentStart = openMatch.index + openMatch[0].length;
|
|
211
|
+
out += source.slice(pos, openMatch.index) +
|
|
212
|
+
replacer(openMatch[1], source.slice(contentStart, tag.index));
|
|
213
|
+
pos = tag.index + tag[0].length;
|
|
214
|
+
openMatch = null;
|
|
215
|
+
}
|
|
216
|
+
m = iter.next();
|
|
217
|
+
}
|
|
218
|
+
return out + source.slice(pos);
|
|
219
|
+
}
|
|
180
220
|
|
|
181
221
|
function _extractBlocks(source) {
|
|
182
222
|
var blocks = {};
|
|
183
|
-
var rest = source
|
|
223
|
+
var rest = _replaceBlocks(source, function (name, content) {
|
|
184
224
|
blocks[name] = content;
|
|
185
225
|
return "";
|
|
186
226
|
});
|
|
@@ -188,14 +228,16 @@ function _extractBlocks(source) {
|
|
|
188
228
|
}
|
|
189
229
|
|
|
190
230
|
function _substituteBlocks(parentSource, childBlocks) {
|
|
191
|
-
return parentSource
|
|
231
|
+
return _replaceBlocks(parentSource, function (name, defaultContent) {
|
|
192
232
|
return Object.prototype.hasOwnProperty.call(childBlocks, name)
|
|
193
233
|
? childBlocks[name]
|
|
194
234
|
: defaultContent;
|
|
195
235
|
});
|
|
196
236
|
}
|
|
197
237
|
|
|
198
|
-
|
|
238
|
+
// `loadView(name)` returns the source string for a parent layout (the
|
|
239
|
+
// file path reads it from viewsDir; the string path calls opts.resolve).
|
|
240
|
+
function _resolveExtends(loadView, source) {
|
|
199
241
|
// Walk UP the extends chain accumulating block overrides. Closer-to-
|
|
200
242
|
// leaf overrides win; each parent's blocks fill in only the names the
|
|
201
243
|
// chain hasn't already set. When the chain hits a template with no
|
|
@@ -226,8 +268,10 @@ function _resolveExtends(viewsDir, source) {
|
|
|
226
268
|
allOverrides[k] = extracted.blocks[k];
|
|
227
269
|
}
|
|
228
270
|
}
|
|
229
|
-
|
|
230
|
-
current
|
|
271
|
+
current = loadView(parentName);
|
|
272
|
+
if (typeof current !== "string") {
|
|
273
|
+
throw new Error("template: {% extends \"" + parentName + "\" %} could not be resolved");
|
|
274
|
+
}
|
|
231
275
|
depth++;
|
|
232
276
|
}
|
|
233
277
|
return _substituteBlocks(current, allOverrides);
|
|
@@ -237,15 +281,18 @@ function _resolveExtends(viewsDir, source) {
|
|
|
237
281
|
// Partial inlining (post-extends, pre-tokenize)
|
|
238
282
|
// ============================================================
|
|
239
283
|
|
|
240
|
-
|
|
284
|
+
// `loadPartial(name)` returns the partial source string, or null/undefined
|
|
285
|
+
// when the partial is absent (the file path resolves <viewsDir>/partials;
|
|
286
|
+
// the string path calls opts.resolve).
|
|
287
|
+
function _inlinePartials(loadPartial, source, depth) {
|
|
241
288
|
if (depth > MAX_TEMPLATE_DEPTH) {
|
|
242
289
|
throw new Error("template: partial recursion depth exceeded " + MAX_TEMPLATE_DEPTH +
|
|
243
290
|
" — possible cycle");
|
|
244
291
|
}
|
|
245
292
|
return source.replace(/\{\{>\s*([A-Za-z_][A-Za-z0-9_-]*)\s*\}\}/g, function (_, name) {
|
|
246
|
-
var
|
|
247
|
-
if (
|
|
248
|
-
return _inlinePartials(
|
|
293
|
+
var sub = loadPartial(name);
|
|
294
|
+
if (typeof sub !== "string") return ""; // missing partial → silent empty so a stale `{{> name}}` reference doesn't crash the render
|
|
295
|
+
return _inlinePartials(loadPartial, sub, depth + 1);
|
|
249
296
|
});
|
|
250
297
|
}
|
|
251
298
|
|
|
@@ -739,20 +786,28 @@ function _evalBlock(nodes, scopes, escFn) {
|
|
|
739
786
|
* @since 0.1.0
|
|
740
787
|
* @related b.template.render, b.template.escapeHtml
|
|
741
788
|
*
|
|
742
|
-
* Builds an engine instance
|
|
743
|
-
*
|
|
789
|
+
* Builds an engine instance. With `opts.viewsDir` the returned object
|
|
790
|
+
* exposes `render(viewName, data?)` for one-shot rendering,
|
|
744
791
|
* `compile(viewName)` for AST-only access (caches under viewName),
|
|
745
792
|
* `precompileAll()` for boot-time validation of every `.html` file
|
|
746
793
|
* under `viewsDir`, and `reset()` to drop the AST cache (useful in
|
|
747
794
|
* live-reload workflows).
|
|
748
795
|
*
|
|
796
|
+
* `viewsDir` is optional: an engine created without it serves from a
|
|
797
|
+
* source STRING via `renderString(source, data?, opts?)` and
|
|
798
|
+
* `compileString(source, opts?)` — the read-only / serverless path with
|
|
799
|
+
* no disk read. `{% extends %}` and `{{> partial}}` in a string source
|
|
800
|
+
* resolve through `opts.resolve(name) -> string` (without it, an extends
|
|
801
|
+
* throws and a missing partial inlines empty). The file-backed
|
|
802
|
+
* render/compile/precompileAll refuse when no `viewsDir` is configured.
|
|
803
|
+
*
|
|
749
804
|
* View names are resolved against `viewsDir`; names containing `..`
|
|
750
805
|
* or NUL are refused, and resolved paths outside `viewsDir` throw.
|
|
751
806
|
* Layout-extends and partial-inclusion recursion are bounded at
|
|
752
807
|
* depth 16 to defend against accidental cycles.
|
|
753
808
|
*
|
|
754
809
|
* @opts
|
|
755
|
-
* viewsDir: string, //
|
|
810
|
+
* viewsDir: string, // optional — directory of .html templates; omit for string-only (renderString) use
|
|
756
811
|
* cache: boolean, // default true; set false for live-reload
|
|
757
812
|
* escapeHtml: function (value) → string, // override the default 5-character HTML escape
|
|
758
813
|
* sandbox: boolean, // when true, sandboxHelpers run through b.sandbox.run
|
|
@@ -768,13 +823,16 @@ function _evalBlock(nodes, scopes, escFn) {
|
|
|
768
823
|
function create(opts) {
|
|
769
824
|
opts = opts || {};
|
|
770
825
|
validateOpts(opts, ["viewsDir", "cache", "escapeHtml", "sandbox", "sandboxHelpers", "sandboxOpts"], "b.template");
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
826
|
+
// viewsDir is optional: an engine created without it serves string
|
|
827
|
+
// sources via renderString / compileString (the serverless / read-only-FS
|
|
828
|
+
// path) — the file-backed render / compile / precompileAll then refuse.
|
|
829
|
+
var viewsDir = null;
|
|
830
|
+
if (opts.viewsDir) {
|
|
831
|
+
if (!nodeFs.existsSync(opts.viewsDir)) {
|
|
832
|
+
throw new Error("template: viewsDir does not exist: " + opts.viewsDir);
|
|
833
|
+
}
|
|
834
|
+
viewsDir = nodePath.resolve(opts.viewsDir);
|
|
776
835
|
}
|
|
777
|
-
var viewsDir = nodePath.resolve(opts.viewsDir);
|
|
778
836
|
var cacheOn = opts.cache !== false;
|
|
779
837
|
var customEscape = typeof opts.escapeHtml === "function" ? opts.escapeHtml : escapeHtml;
|
|
780
838
|
var astCache = {};
|
|
@@ -820,12 +878,23 @@ function create(opts) {
|
|
|
820
878
|
}
|
|
821
879
|
}
|
|
822
880
|
|
|
881
|
+
// File-backed load callbacks for the extends/partial resolvers.
|
|
882
|
+
function _loadViewFile(name) {
|
|
883
|
+
return nodeFs.readFileSync(_resolveViewPath(viewsDir, name), "utf8");
|
|
884
|
+
}
|
|
885
|
+
function _loadPartialFile(name) {
|
|
886
|
+
var p = _resolvePartialPath(viewsDir, name);
|
|
887
|
+
return p ? nodeFs.readFileSync(p, "utf8") : null;
|
|
888
|
+
}
|
|
889
|
+
|
|
823
890
|
function compile(viewName) {
|
|
891
|
+
if (!viewsDir) {
|
|
892
|
+
throw new Error("template: viewsDir not configured — use renderString/compileString for string sources");
|
|
893
|
+
}
|
|
824
894
|
if (cacheOn && astCache[viewName]) return astCache[viewName];
|
|
825
|
-
var
|
|
826
|
-
|
|
827
|
-
source =
|
|
828
|
-
source = _inlinePartials(viewsDir, source, 0);
|
|
895
|
+
var source = nodeFs.readFileSync(_resolveViewPath(viewsDir, viewName), "utf8");
|
|
896
|
+
source = _resolveExtends(_loadViewFile, source);
|
|
897
|
+
source = _inlinePartials(_loadPartialFile, source, 0);
|
|
829
898
|
var tokens = _tokenize(source);
|
|
830
899
|
var ast = _parseTokens(tokens);
|
|
831
900
|
if (cacheOn) astCache[viewName] = ast;
|
|
@@ -837,6 +906,68 @@ function create(opts) {
|
|
|
837
906
|
return _evalBlock(ast.body, [data || {}], customEscape);
|
|
838
907
|
}
|
|
839
908
|
|
|
909
|
+
// ---- String-source variants (serverless / read-only FS): compile and
|
|
910
|
+
// render from a source STRING with no viewsDir disk read. `{% extends %}`
|
|
911
|
+
// and `{{> partial}}` resolve through an operator-supplied
|
|
912
|
+
// `sopts.resolve(name) -> string` callback; without it, an extends in
|
|
913
|
+
// the source throws and a missing partial inlines empty (same as the
|
|
914
|
+
// file path). No caching — string sources are ad-hoc.
|
|
915
|
+
function _stringLoaders(sopts, maxBytes) {
|
|
916
|
+
var resolve = sopts && sopts.resolve;
|
|
917
|
+
if (resolve !== undefined && typeof resolve !== "function") {
|
|
918
|
+
throw new Error("template.compileString: opts.resolve must be a function (name) => string");
|
|
919
|
+
}
|
|
920
|
+
function _capped(s, what) {
|
|
921
|
+
if (typeof s === "string" && Buffer.byteLength(s, "utf8") > maxBytes) {
|
|
922
|
+
throw new Error("template.compileString: " + what + " exceeds maxBytes=" + maxBytes);
|
|
923
|
+
}
|
|
924
|
+
return s;
|
|
925
|
+
}
|
|
926
|
+
var loadView = function (name) {
|
|
927
|
+
var s = resolve ? resolve(name) : undefined;
|
|
928
|
+
if (typeof s !== "string") {
|
|
929
|
+
throw new Error("template.compileString: {% extends \"" + name +
|
|
930
|
+
"\" %} needs opts.resolve(name) to return the layout source");
|
|
931
|
+
}
|
|
932
|
+
return _capped(s, "resolved layout '" + name + "'");
|
|
933
|
+
};
|
|
934
|
+
var loadPartial = function (name) {
|
|
935
|
+
var s = resolve ? resolve(name) : null;
|
|
936
|
+
return typeof s === "string" ? _capped(s, "resolved partial '" + name + "'") : null;
|
|
937
|
+
};
|
|
938
|
+
return { loadView: loadView, loadPartial: loadPartial };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function compileString(source, sopts) {
|
|
942
|
+
if (typeof source !== "string") {
|
|
943
|
+
throw new Error("template.compileString(source): source must be a string");
|
|
944
|
+
}
|
|
945
|
+
var maxBytes = (sopts && typeof sopts.maxBytes === "number") ? sopts.maxBytes : DEFAULT_STRING_TEMPLATE_BYTES;
|
|
946
|
+
if (Buffer.byteLength(source, "utf8") > maxBytes) {
|
|
947
|
+
throw new Error("template.compileString: source exceeds maxBytes=" + maxBytes +
|
|
948
|
+
" — string templates are bounded against hostile input; raise opts.maxBytes if intentional");
|
|
949
|
+
}
|
|
950
|
+
var ld = _stringLoaders(sopts, maxBytes);
|
|
951
|
+
var resolved = _resolveExtends(ld.loadView, source);
|
|
952
|
+
resolved = _inlinePartials(ld.loadPartial, resolved, 0);
|
|
953
|
+
return _parseTokens(_tokenize(resolved));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function renderString(source, data, sopts) {
|
|
957
|
+
// Disambiguate the optional middle arg: `renderString(source, { resolve })`
|
|
958
|
+
// — a 2nd arg carrying a function-valued `resolve` and no 3rd arg is the
|
|
959
|
+
// opts object, not render data (template data values are rendered, not
|
|
960
|
+
// called, so a function `resolve` is unambiguously the resolver). This
|
|
961
|
+
// lets a layout/partial template with no data omit the data placeholder.
|
|
962
|
+
if (sopts === undefined && data && typeof data === "object" &&
|
|
963
|
+
typeof data.resolve === "function") {
|
|
964
|
+
sopts = data;
|
|
965
|
+
data = undefined;
|
|
966
|
+
}
|
|
967
|
+
var ast = compileString(source, sopts);
|
|
968
|
+
return _evalBlock(ast.body, [data || {}], customEscape);
|
|
969
|
+
}
|
|
970
|
+
|
|
840
971
|
function reset() { astCache = {}; }
|
|
841
972
|
|
|
842
973
|
// Walk viewsDir, compile every .html file. Surfaces parse errors at
|
|
@@ -845,6 +976,9 @@ function create(opts) {
|
|
|
845
976
|
// a typo like `{% if not foo %}` fails the deploy, not the user.
|
|
846
977
|
// Returns the list of view names compiled.
|
|
847
978
|
function precompileAll() {
|
|
979
|
+
if (!viewsDir) {
|
|
980
|
+
throw new Error("template: viewsDir not configured — precompileAll requires a views directory");
|
|
981
|
+
}
|
|
848
982
|
var compiled = [];
|
|
849
983
|
function walk(dir, prefix) {
|
|
850
984
|
var entries = nodeFs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -878,6 +1012,8 @@ function create(opts) {
|
|
|
878
1012
|
return {
|
|
879
1013
|
compile: compile,
|
|
880
1014
|
render: render,
|
|
1015
|
+
compileString: compileString,
|
|
1016
|
+
renderString: renderString,
|
|
881
1017
|
reset: reset,
|
|
882
1018
|
precompileAll: precompileAll,
|
|
883
1019
|
viewsDir: viewsDir,
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"hashes": {
|
|
19
19
|
"server": "sha256:5d539dfc9ef47121d4c09bd7256d76448a1f5ac47ee09ac44c78ff6a062af9ab"
|
|
20
20
|
},
|
|
21
|
-
"refreshedAt": "2026-05-
|
|
21
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
22
22
|
},
|
|
23
23
|
"@noble/curves": {
|
|
24
24
|
"version": "2.2.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"hashes": {
|
|
41
41
|
"server": "sha256:ebf254d5eb56aef8705a1c4af9603f47987b4870a9bb5e657e06907b701e2731"
|
|
42
42
|
},
|
|
43
|
-
"refreshedAt": "2026-05-
|
|
43
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
44
44
|
},
|
|
45
45
|
"@noble/post-quantum": {
|
|
46
46
|
"version": "0.6.1",
|
|
@@ -71,10 +71,10 @@
|
|
|
71
71
|
"hashes": {
|
|
72
72
|
"server": "sha256:f9190309daadca4c2e2cc2b76beaa6b96e463429cc3c390bd9f0ceaf7b588c68"
|
|
73
73
|
},
|
|
74
|
-
"refreshedAt": "2026-05-
|
|
74
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
75
75
|
},
|
|
76
76
|
"@simplewebauthn/server": {
|
|
77
|
-
"version": "13.3.
|
|
77
|
+
"version": "13.3.1",
|
|
78
78
|
"license": "MIT",
|
|
79
79
|
"author": "Matthew Miller",
|
|
80
80
|
"source": "https://github.com/MasterKale/SimpleWebAuthn",
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"server": "lib/vendor/simplewebauthn-server.cjs"
|
|
90
90
|
},
|
|
91
91
|
"bundler": "esbuild --format=cjs --minify --platform=node --external:crypto --external:node:crypto",
|
|
92
|
-
"bundledAt": "2026-
|
|
93
|
-
"cpe": "cpe:2.3:a:simplewebauthn:server:13.3.
|
|
92
|
+
"bundledAt": "2026-05-27T00:00:00Z",
|
|
93
|
+
"cpe": "cpe:2.3:a:simplewebauthn:server:13.3.1:*:*:*:*:node.js:*:*",
|
|
94
94
|
"hashes": {
|
|
95
|
-
"server": "sha256:
|
|
95
|
+
"server": "sha256:f359a782ac57e3ff56ac71083d17f5c082f88ab49d645fc2bede398b47adebdb"
|
|
96
96
|
},
|
|
97
|
-
"refreshedAt": "2026-05-
|
|
97
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
98
98
|
},
|
|
99
99
|
"SecLists-common-passwords-top-10000": {
|
|
100
100
|
"version": "10k-most-common (master)",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
|
|
116
116
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
117
|
-
"refreshedAt": "2026-05-
|
|
117
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
118
118
|
},
|
|
119
119
|
"bimi-trust-anchors": {
|
|
120
120
|
"version": "operator-managed",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
|
|
141
141
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
142
|
-
"refreshedAt": "2026-05-
|
|
142
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
143
143
|
},
|
|
144
144
|
"publicsuffix-list": {
|
|
145
145
|
"version": "master",
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
},
|
|
160
160
|
"runtime_artifact": "lib/vendor/public-suffix-list.data.js",
|
|
161
161
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
162
|
-
"refreshedAt": "2026-05-
|
|
162
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
163
163
|
},
|
|
164
164
|
"peculiar-pki": {
|
|
165
165
|
"version": "2.0.0+pkijs-3.4.0",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"hashes": {
|
|
191
191
|
"server": "sha256:9bbc191afaaa2b1e5757f00480457c08134cdc2c55d541df18d9155bba9cbf77"
|
|
192
192
|
},
|
|
193
|
-
"refreshedAt": "2026-05-
|
|
193
|
+
"refreshedAt": "2026-05-27T17:51:05.993Z"
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|