@blamejs/core 0.4.1

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.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
package/lib/forms.js ADDED
@@ -0,0 +1,378 @@
1
+ "use strict";
2
+ /**
3
+ * Forms — CSRF tokens, HTML rendering, server-side validation.
4
+ *
5
+ * Three concerns the framework owns at the form layer:
6
+ *
7
+ * 1. CSRF tokens. Generation is a 32-byte random hex string;
8
+ * verification is constant-time. The middleware in
9
+ * lib/middleware/csrf-protect.js does the actual gating —
10
+ * this module provides the primitives the operator (or
11
+ * template) calls to issue a token and embed it.
12
+ *
13
+ * 2. Form HTML rendering. forms.render({ action, fields, csrfToken })
14
+ * produces a complete <form> element with auto-escaped attributes,
15
+ * a hidden CSRF input, and field-type dispatch (text / email /
16
+ * password / number / checkbox / radio / textarea / select /
17
+ * hidden / submit). All values pass through escapeAttribute so
18
+ * the form can't be hijacked by user-supplied attribute payloads.
19
+ *
20
+ * 3. Server-side validation. forms.validate(spec, body) walks the
21
+ * same field spec the renderer accepts and returns
22
+ * { valid, errors, values } — coerced types, required-field
23
+ * checks, length bounds, regex pattern, enum membership.
24
+ *
25
+ * The renderer + validator share their field spec on purpose: an
26
+ * operator's "this is what the form looks like" is also "this is
27
+ * what the form expects." A change to the spec adjusts both.
28
+ *
29
+ * Public API:
30
+ *
31
+ * forms.generateCsrfToken() → "<64 hex chars>"
32
+ * forms.verifyCsrfToken(submitted, expected) → boolean (timing-safe)
33
+ * forms.render(spec) → string (complete <form>…</form>)
34
+ * forms.validate(spec, body) → { valid, errors, values }
35
+ * forms.escapeAttribute(value) → string (double-quoted-attr context)
36
+ * forms.escapeHtml = template.escapeHtml (re-export for convenience)
37
+ */
38
+ var nodeCrypto = require("crypto");
39
+ var template = require("./template");
40
+
41
+ // ============================================================
42
+ // CSRF token primitives
43
+ // ============================================================
44
+
45
+ var CSRF_TOKEN_BYTES = 32;
46
+
47
+ function generateCsrfToken() {
48
+ return nodeCrypto.randomBytes(CSRF_TOKEN_BYTES).toString("hex");
49
+ }
50
+
51
+ function verifyCsrfToken(submitted, expected) {
52
+ if (typeof submitted !== "string" || typeof expected !== "string") return false;
53
+ if (submitted.length === 0 || submitted.length !== expected.length) return false;
54
+ var a = Buffer.from(submitted, "utf8");
55
+ var b = Buffer.from(expected, "utf8");
56
+ if (a.length !== b.length) return false;
57
+ return nodeCrypto.timingSafeEqual(a, b);
58
+ }
59
+
60
+ // ============================================================
61
+ // Attribute escaping
62
+ // ============================================================
63
+ //
64
+ // More strict than template.escapeHtml: escapes the same set plus
65
+ // backtick (some browsers treat ` as an attribute delimiter under
66
+ // quirks-mode parsing) and = (defense-in-depth for unquoted-attribute
67
+ // contexts the renderer happens to produce). Forms render attributes
68
+ // in double-quoted contexts only, so this is belt-and-suspenders.
69
+
70
+ var ATTR_ESCAPE_MAP = {
71
+ "&": "&amp;", "<": "&lt;", ">": "&gt;",
72
+ '"': "&quot;", "'": "&#x27;",
73
+ "`": "&#x60;", "=": "&#x3D;",
74
+ };
75
+ var ATTR_ESCAPE_RE = /[&<>"'`=]/g;
76
+
77
+ function escapeAttribute(value) {
78
+ if (value === null || value === undefined) return "";
79
+ var s = typeof value === "string" ? value : String(value);
80
+ return s.replace(ATTR_ESCAPE_RE, function (c) { return ATTR_ESCAPE_MAP[c]; });
81
+ }
82
+
83
+ // ============================================================
84
+ // Form rendering
85
+ // ============================================================
86
+ //
87
+ // spec = {
88
+ // action: string — form action URL (required)
89
+ // method: "POST" (default) | "GET" | …
90
+ // csrfToken: string — embedded as <input name="_csrf">
91
+ // csrfFieldName: string (default "_csrf") — match what csrfProtect expects
92
+ // id, className: string — top-level form attributes
93
+ // fields: FieldSpec[]
94
+ // submitLabel: string (default "Submit") — auto-appended submit button
95
+ // if no field of type=submit present
96
+ // enctype: string — multipart/form-data etc.
97
+ // autocomplete: string — "off" / "on"
98
+ // }
99
+ //
100
+ // FieldSpec common keys:
101
+ // name: string (required)
102
+ // type: "text" | "email" | "password" | "number" | "tel" | "url" |
103
+ // "search" | "date" | "time" | "datetime-local" | "checkbox" |
104
+ // "radio" | "textarea" | "select" | "hidden" | "submit"
105
+ // value: initial value (auto-escaped)
106
+ // label: string — wraps a <label>
107
+ // placeholder, required, readonly, disabled, autocomplete
108
+ // min, max, step, pattern, minlength, maxlength
109
+ // options: [{ value, label, selected? }] — for select / radio
110
+ // rows, cols — for textarea
111
+
112
+ var INPUT_TYPES = {
113
+ "text": 1, "email": 1, "password": 1, "number": 1, "tel": 1, "url": 1,
114
+ "search": 1, "date": 1, "time": 1, "datetime-local": 1, "month": 1, "week": 1,
115
+ "checkbox": 1, "radio": 1, "hidden": 1, "submit": 1, "color": 1,
116
+ "file": 1, "range": 1, "image": 1, "reset": 1, "button": 1,
117
+ };
118
+
119
+ function _renderInput(field) {
120
+ var attrs = [
121
+ 'type="' + escapeAttribute(field.type) + '"',
122
+ 'name="' + escapeAttribute(field.name) + '"',
123
+ ];
124
+ if (field.value !== undefined && field.value !== null) {
125
+ attrs.push('value="' + escapeAttribute(field.value) + '"');
126
+ }
127
+ if (field.placeholder) attrs.push('placeholder="' + escapeAttribute(field.placeholder) + '"');
128
+ if (field.required) attrs.push("required");
129
+ if (field.readonly) attrs.push("readonly");
130
+ if (field.disabled) attrs.push("disabled");
131
+ if (field.checked) attrs.push("checked");
132
+ if (field.autocomplete) attrs.push('autocomplete="' + escapeAttribute(field.autocomplete) + '"');
133
+ if (field.pattern) attrs.push('pattern="' + escapeAttribute(field.pattern) + '"');
134
+ if (field.min !== undefined) attrs.push('min="' + escapeAttribute(field.min) + '"');
135
+ if (field.max !== undefined) attrs.push('max="' + escapeAttribute(field.max) + '"');
136
+ if (field.step !== undefined) attrs.push('step="' + escapeAttribute(field.step) + '"');
137
+ if (field.minlength !== undefined) attrs.push('minlength="' + escapeAttribute(field.minlength) + '"');
138
+ if (field.maxlength !== undefined) attrs.push('maxlength="' + escapeAttribute(field.maxlength) + '"');
139
+ if (field.id) attrs.push('id="' + escapeAttribute(field.id) + '"');
140
+ if (field.className) attrs.push('class="' + escapeAttribute(field.className) + '"');
141
+ return "<input " + attrs.join(" ") + ">";
142
+ }
143
+
144
+ function _renderTextarea(field) {
145
+ var attrs = ['name="' + escapeAttribute(field.name) + '"'];
146
+ if (field.placeholder) attrs.push('placeholder="' + escapeAttribute(field.placeholder) + '"');
147
+ if (field.required) attrs.push("required");
148
+ if (field.readonly) attrs.push("readonly");
149
+ if (field.disabled) attrs.push("disabled");
150
+ if (field.rows !== undefined) attrs.push('rows="' + escapeAttribute(field.rows) + '"');
151
+ if (field.cols !== undefined) attrs.push('cols="' + escapeAttribute(field.cols) + '"');
152
+ if (field.minlength !== undefined) attrs.push('minlength="' + escapeAttribute(field.minlength) + '"');
153
+ if (field.maxlength !== undefined) attrs.push('maxlength="' + escapeAttribute(field.maxlength) + '"');
154
+ if (field.id) attrs.push('id="' + escapeAttribute(field.id) + '"');
155
+ if (field.className) attrs.push('class="' + escapeAttribute(field.className) + '"');
156
+ var body = field.value !== undefined && field.value !== null ? template.escapeHtml(field.value) : "";
157
+ return "<textarea " + attrs.join(" ") + ">" + body + "</textarea>";
158
+ }
159
+
160
+ function _renderSelect(field) {
161
+ var attrs = ['name="' + escapeAttribute(field.name) + '"'];
162
+ if (field.required) attrs.push("required");
163
+ if (field.disabled) attrs.push("disabled");
164
+ if (field.multiple) attrs.push("multiple");
165
+ if (field.id) attrs.push('id="' + escapeAttribute(field.id) + '"');
166
+ if (field.className) attrs.push('class="' + escapeAttribute(field.className) + '"');
167
+ var options = field.options || [];
168
+ var optionsHtml = options.map(function (o) {
169
+ var optAttrs = ['value="' + escapeAttribute(o.value) + '"'];
170
+ var selected = (o.selected) || (field.value !== undefined && String(field.value) === String(o.value));
171
+ if (selected) optAttrs.push("selected");
172
+ if (o.disabled) optAttrs.push("disabled");
173
+ return "<option " + optAttrs.join(" ") + ">" + template.escapeHtml(o.label === undefined ? o.value : o.label) + "</option>";
174
+ }).join("");
175
+ return "<select " + attrs.join(" ") + ">" + optionsHtml + "</select>";
176
+ }
177
+
178
+ function _renderField(field) {
179
+ if (!field || !field.name || typeof field.name !== "string") {
180
+ throw new Error("forms.render: each field requires a name");
181
+ }
182
+ var type = field.type || "text";
183
+ var control;
184
+ if (type === "textarea") control = _renderTextarea(field);
185
+ else if (type === "select") control = _renderSelect(field);
186
+ else if (INPUT_TYPES[type]) control = _renderInput(field);
187
+ else throw new Error("forms.render: unsupported field type: " + type);
188
+
189
+ // Hidden + submit fields don't need a label wrapper
190
+ if (type === "hidden" || type === "submit") return control;
191
+
192
+ if (field.label) {
193
+ return "<label>" + template.escapeHtml(field.label) + " " + control + "</label>";
194
+ }
195
+ return control;
196
+ }
197
+
198
+ function render(spec) {
199
+ if (!spec || typeof spec.action !== "string" || spec.action.length === 0) {
200
+ throw new Error("forms.render: spec.action is required");
201
+ }
202
+ if (!Array.isArray(spec.fields)) {
203
+ throw new Error("forms.render: spec.fields must be an array");
204
+ }
205
+ var method = (spec.method || "POST").toUpperCase();
206
+ var attrs = [
207
+ 'method="' + escapeAttribute(method) + '"',
208
+ 'action="' + escapeAttribute(spec.action) + '"',
209
+ ];
210
+ if (spec.id) attrs.push('id="' + escapeAttribute(spec.id) + '"');
211
+ if (spec.className) attrs.push('class="' + escapeAttribute(spec.className) + '"');
212
+ if (spec.enctype) attrs.push('enctype="' + escapeAttribute(spec.enctype) + '"');
213
+ if (spec.autocomplete) attrs.push('autocomplete="' + escapeAttribute(spec.autocomplete) + '"');
214
+ if (spec.target) attrs.push('target="' + escapeAttribute(spec.target) + '"');
215
+
216
+ var inner = "";
217
+
218
+ // CSRF hidden input. State-changing methods need it; safe methods
219
+ // (GET/HEAD) don't. We still render it on POST etc. when csrfToken
220
+ // is provided; the middleware decides whether it's required.
221
+ if (spec.csrfToken && method !== "GET" && method !== "HEAD") {
222
+ var csrfFieldName = spec.csrfFieldName || "_csrf";
223
+ inner += '<input type="hidden" name="' + escapeAttribute(csrfFieldName) +
224
+ '" value="' + escapeAttribute(spec.csrfToken) + '">';
225
+ }
226
+
227
+ for (var i = 0; i < spec.fields.length; i++) {
228
+ inner += _renderField(spec.fields[i]);
229
+ }
230
+
231
+ // Auto-submit button if the operator didn't include one
232
+ var hasSubmit = spec.fields.some(function (f) { return f.type === "submit"; });
233
+ if (!hasSubmit) {
234
+ inner += '<button type="submit">' + template.escapeHtml(spec.submitLabel || "Submit") + "</button>";
235
+ }
236
+
237
+ return "<form " + attrs.join(" ") + ">" + inner + "</form>";
238
+ }
239
+
240
+ // ============================================================
241
+ // Server-side validation
242
+ // ============================================================
243
+ //
244
+ // validate(spec, body) → { valid, errors, values }
245
+ //
246
+ // errors is a map fieldName → message (only failing fields). values is
247
+ // the coerced data: number/checkbox/etc. converted to their canonical
248
+ // types. body is whatever the operator passes — typically an object
249
+ // from URLSearchParams or JSON parse.
250
+
251
+ function _coerce(field, raw) {
252
+ // Checkbox special-case: browsers omit unchecked boxes from the
253
+ // submission entirely, so `raw === undefined` means "unchecked"
254
+ // (false), not "not provided." Coerce to false BEFORE the generic
255
+ // undefined-passthrough below.
256
+ if (field.type === "checkbox") {
257
+ if (raw === undefined || raw === null || raw === "" || raw === "false" || raw === "0") return false;
258
+ return true;
259
+ }
260
+ if (raw === undefined) return undefined;
261
+ switch (field.type) {
262
+ case "number":
263
+ case "range":
264
+ if (raw === "" || raw === null) return null;
265
+ var n = Number(raw);
266
+ return Number.isFinite(n) ? n : NaN;
267
+ case "date":
268
+ case "datetime-local":
269
+ case "time":
270
+ return typeof raw === "string" ? raw : String(raw);
271
+ default:
272
+ return typeof raw === "string" ? raw : String(raw);
273
+ }
274
+ }
275
+
276
+ function _isEmpty(v) {
277
+ return v === undefined || v === null || v === "";
278
+ }
279
+
280
+ function validate(spec, body) {
281
+ if (!spec || !Array.isArray(spec.fields)) {
282
+ throw new Error("forms.validate: spec.fields must be an array");
283
+ }
284
+ body = body || {};
285
+ var errors = {};
286
+ var values = {};
287
+
288
+ for (var i = 0; i < spec.fields.length; i++) {
289
+ var f = spec.fields[i];
290
+ if (!f.name) continue;
291
+ if (f.type === "submit") continue; // submit buttons aren't data
292
+
293
+ var raw = body[f.name];
294
+ var coerced = _coerce(f, raw);
295
+ values[f.name] = coerced;
296
+
297
+ if (f.required && _isEmpty(coerced) && coerced !== false) {
298
+ errors[f.name] = f.errorMessages && f.errorMessages.required
299
+ ? f.errorMessages.required
300
+ : (f.label || f.name) + " is required";
301
+ continue;
302
+ }
303
+ if (_isEmpty(coerced)) continue;
304
+
305
+ // Type-specific checks
306
+ if (f.type === "number" || f.type === "range") {
307
+ if (Number.isNaN(coerced)) {
308
+ errors[f.name] = (f.label || f.name) + " must be a number";
309
+ continue;
310
+ }
311
+ if (f.min !== undefined && coerced < Number(f.min)) {
312
+ errors[f.name] = (f.label || f.name) + " must be ≥ " + f.min;
313
+ continue;
314
+ }
315
+ if (f.max !== undefined && coerced > Number(f.max)) {
316
+ errors[f.name] = (f.label || f.name) + " must be ≤ " + f.max;
317
+ continue;
318
+ }
319
+ }
320
+ if (f.type === "email" && typeof coerced === "string") {
321
+ // Pragmatic email check — RFC 5322 is impractical to regex
322
+ // correctly; this catches obvious nonsense ("foo", "foo@",
323
+ // "@bar") without over-engineering.
324
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(coerced)) {
325
+ errors[f.name] = (f.label || f.name) + " must be a valid email address";
326
+ continue;
327
+ }
328
+ }
329
+ if (f.type === "url" && typeof coerced === "string") {
330
+ try { new URL(coerced); }
331
+ catch (_e) { errors[f.name] = (f.label || f.name) + " must be a valid URL"; continue; }
332
+ }
333
+ if (typeof coerced === "string") {
334
+ if (f.minlength !== undefined && coerced.length < Number(f.minlength)) {
335
+ errors[f.name] = (f.label || f.name) + " must be at least " + f.minlength + " characters";
336
+ continue;
337
+ }
338
+ if (f.maxlength !== undefined && coerced.length > Number(f.maxlength)) {
339
+ errors[f.name] = (f.label || f.name) + " must be at most " + f.maxlength + " characters";
340
+ continue;
341
+ }
342
+ if (f.pattern) {
343
+ var re;
344
+ try { re = new RegExp("^(?:" + f.pattern + ")$"); }
345
+ catch (_e) { /* invalid pattern in spec — operator bug; skip */ continue; }
346
+ if (!re.test(coerced)) {
347
+ errors[f.name] = f.errorMessages && f.errorMessages.pattern
348
+ ? f.errorMessages.pattern
349
+ : (f.label || f.name) + " has an invalid format";
350
+ continue;
351
+ }
352
+ }
353
+ }
354
+ if ((f.type === "select" || f.type === "radio") && Array.isArray(f.options)) {
355
+ var allowed = f.options.map(function (o) { return String(o.value); });
356
+ if (allowed.indexOf(String(coerced)) === -1) {
357
+ errors[f.name] = (f.label || f.name) + " has an invalid value";
358
+ continue;
359
+ }
360
+ }
361
+ }
362
+
363
+ return {
364
+ valid: Object.keys(errors).length === 0,
365
+ errors: errors,
366
+ values: values,
367
+ };
368
+ }
369
+
370
+ module.exports = {
371
+ generateCsrfToken: generateCsrfToken,
372
+ verifyCsrfToken: verifyCsrfToken,
373
+ render: render,
374
+ validate: validate,
375
+ escapeAttribute: escapeAttribute,
376
+ escapeHtml: template.escapeHtml,
377
+ CSRF_TOKEN_BYTES: CSRF_TOKEN_BYTES,
378
+ };
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var observability = require("./observability");
3
+
4
+ /**
5
+ * Framework error base class + cross-module operational error classes.
6
+ *
7
+ * Two scopes live here:
8
+ *
9
+ * 1. FrameworkError — base class every framework error class extends.
10
+ * Provides a single `instanceof FrameworkError` check (replacing the
11
+ * scattered `isXxxError` boolean flags) plus a stable shape: { name,
12
+ * code, message, isFrameworkError: true }.
13
+ *
14
+ * 2. Cross-module operational error classes — errors raised by more
15
+ * than one module that share a logical domain (e.g. ObjectStoreError
16
+ * raised by the 5 object-store adapters + the umbrella). These can't
17
+ * live in the umbrella module because adapters would need a circular
18
+ * require to access them. They live here, where every adapter can
19
+ * import from the same place.
20
+ *
21
+ * 3. defineClass(name, opts) — factory that produces a FrameworkError
22
+ * subclass with the standard shape. Eliminates the boilerplate that
23
+ * every per-domain error class was duplicating across lib/.
24
+ *
25
+ * Per-domain VALIDATION errors (SafeSqlError, SafeJsonError, SafeBufferError,
26
+ * SafeAsyncError, AtomicFileError, ChainWriterError, ClusterStorageError,
27
+ * NotLeaderError, FrameworkSchemaError, *SafeError parser families) stay
28
+ * co-located with their primitive module — they're single-owner, single-
29
+ * domain, and the *-safe filename convention already declares ownership.
30
+ * They extend FrameworkError so the unified `instanceof` check works.
31
+ *
32
+ * Operational error classes here all share:
33
+ * { name, code, message, permanent: bool, isFrameworkError: true }
34
+ * Adapters that talk over HTTP also carry `statusCode` for retry
35
+ * classification.
36
+ */
37
+
38
+ class FrameworkError extends Error {
39
+ constructor(message, code) {
40
+ super(message);
41
+ this.name = "FrameworkError";
42
+ this.code = code || "framework/invalid";
43
+ this.isFrameworkError = true;
44
+ }
45
+ }
46
+
47
+ // defineClass — factory for the standard FrameworkError-subclass shape
48
+ // every per-domain error followed by hand. Variants the factory covers:
49
+ //
50
+ // defineClass("MyError")
51
+ // constructor: (code, message, permanent)
52
+ // fields: name, permanent, isMyError
53
+ //
54
+ // defineClass("MyError", { withStatusCode: true })
55
+ // constructor: (code, message, permanent, statusCode)
56
+ // fields: + statusCode (HTTP-shaped operational errors)
57
+ //
58
+ // defineClass("MyError", { alwaysPermanent: true })
59
+ // constructor: (code, message)
60
+ // fields: permanent always true (auth failures, validation)
61
+ //
62
+ // defineClass("MyError", { withCause: true })
63
+ // constructor: (code, message, cause)
64
+ // fields: + cause (errors that wrap an upstream cause)
65
+ //
66
+ // Returns the constructor. Operators can attach extra static helpers
67
+ // to it after creation if they need to.
68
+ function defineClass(name, opts) {
69
+ if (typeof name !== "string" || name.length === 0) {
70
+ throw new Error("defineClass: name must be a non-empty string");
71
+ }
72
+ opts = opts || {};
73
+ var alwaysPermanent = !!opts.alwaysPermanent;
74
+ var withStatusCode = !!opts.withStatusCode;
75
+ var withCause = !!opts.withCause;
76
+ if (alwaysPermanent && (withStatusCode || withCause)) {
77
+ throw new Error("defineClass: alwaysPermanent is mutually exclusive with withStatusCode / withCause");
78
+ }
79
+ var flagKey = "is" + name;
80
+
81
+ // Generated class — uses an anonymous class expression so we can set
82
+ // the constructor name explicitly via Object.defineProperty (matters
83
+ // for stack traces and instanceof debugging).
84
+ var GeneratedError = class extends FrameworkError {
85
+ constructor(code, message, arg3, arg4) {
86
+ super(message, code);
87
+ this.name = name;
88
+ this[flagKey] = true;
89
+ if (alwaysPermanent) {
90
+ this.permanent = true;
91
+ } else if (withCause) {
92
+ this.cause = arg3;
93
+ } else {
94
+ this.permanent = !!arg3;
95
+ if (withStatusCode) this.statusCode = arg4;
96
+ }
97
+ // Framework-error class counter — routed into framework_errors_total
98
+ // when a metrics registry is active. observability.event is safe to
99
+ // call here even during framework-error's own load: observability's
100
+ // dependencies on metrics + tracing are themselves lazy-required
101
+ // and only resolve at first call (post-load).
102
+ try { observability.event("error.construct", 1, { class: name }); }
103
+ catch (_e) { /* defensive — no observability sink should ever break a constructor */ }
104
+ }
105
+ };
106
+ Object.defineProperty(GeneratedError, "name", { value: name, configurable: true });
107
+ // Per-class factory — collapses the boilerplate every module used to
108
+ // write as `function _err(code, msg, perm) { return new XxxError(...); }`.
109
+ // Now: `var _err = XxxError.factory;` (one line, same call shape).
110
+ GeneratedError.factory = function (code, message, arg3, arg4) {
111
+ return new GeneratedError(code, message, arg3, arg4);
112
+ };
113
+ return GeneratedError;
114
+ }
115
+
116
+ // ---- Cross-module operational classes (defined via the factory) ----
117
+
118
+ var ObjectStoreError = defineClass("ObjectStoreError", { withStatusCode: true });
119
+ var LogStreamError = defineClass("LogStreamError", { withStatusCode: true });
120
+ var QueueError = defineClass("QueueError");
121
+ var ExternalDbError = defineClass("ExternalDbError");
122
+ var ClusterError = defineClass("ClusterError");
123
+ var ClusterProviderError = defineClass("ClusterProviderError");
124
+ var HandlerError = defineClass("HandlerError", { withCause: true });
125
+ var StorageError = defineClass("StorageError");
126
+ // AuthError covers password / passkey / TOTP failures at the framework
127
+ // layer (lib/auth/*). Always permanent — auth failures are not transient
128
+ // ("retry might work"); they're "this credential doesn't match" or
129
+ // "this input was malformed".
130
+ var AuthError = defineClass("AuthError", { alwaysPermanent: true });
131
+ var JobsError = defineClass("JobsError");
132
+ var SchedulerError = defineClass("SchedulerError");
133
+ var SessionError = defineClass("SessionError");
134
+ var SlugError = defineClass("SlugError", { alwaysPermanent: true });
135
+ var WebhookError = defineClass("WebhookError", { alwaysPermanent: true });
136
+ var ApiKeyError = defineClass("ApiKeyError", { alwaysPermanent: true });
137
+ var PermissionsError = defineClass("PermissionsError", { alwaysPermanent: true });
138
+ // CacheError is alwaysPermanent: bad opts / missing key / closed-state
139
+ // errors are programming bugs, not transient. Backend-level transient
140
+ // failures (cluster DB unavailable mid-fetch) become observability +
141
+ // audit signals; they don't escape as exceptions to the caller.
142
+ var CacheError = defineClass("CacheError", { alwaysPermanent: true });
143
+ // SeederError is alwaysPermanent: load failures, bad-shape seed files,
144
+ // missing deps, and cycle errors are programming bugs. Per-seed runtime
145
+ // failures get wrapped in this class with the seed name in the message
146
+ // — operators see "seeders/run-failed: 0042-x.js: <cause>" not a raw
147
+ // driver exception.
148
+ var SeederError = defineClass("SeederError", { alwaysPermanent: true });
149
+ // I18nError is alwaysPermanent: bad locale tags, malformed translation
150
+ // trees, missing-key in throw mode, and bad input to formatters are
151
+ // programming bugs. Missing keys in default ("return-key") mode return
152
+ // the key without throwing — runtime hot-path semantics, not error.
153
+ var I18nError = defineClass("I18nError", { alwaysPermanent: true });
154
+ // NotifyError is alwaysPermanent: bad opts, unknown channels, transport
155
+ // contract violations are programming bugs. Per-send transient failures
156
+ // (the kind retry can recover) are surfaced from the underlying transport
157
+ // with their own shape; only after retry exhaustion does notify wrap
158
+ // them into NotifyError SEND_FAILED — at that point they ARE permanent.
159
+ var NotifyError = defineClass("NotifyError", { alwaysPermanent: true });
160
+ // TestingError is alwaysPermanent: bad inputs to test helpers
161
+ // (NaN clock, non-fn predicate, path-traversal tempDir prefix) and
162
+ // waitFor timeouts are programming bugs at test-write time.
163
+ var TestingError = defineClass("TestingError", { alwaysPermanent: true });
164
+
165
+ module.exports = {
166
+ FrameworkError: FrameworkError,
167
+ defineClass: defineClass,
168
+ ObjectStoreError: ObjectStoreError,
169
+ LogStreamError: LogStreamError,
170
+ QueueError: QueueError,
171
+ ExternalDbError: ExternalDbError,
172
+ ClusterError: ClusterError,
173
+ ClusterProviderError: ClusterProviderError,
174
+ HandlerError: HandlerError,
175
+ StorageError: StorageError,
176
+ AuthError: AuthError,
177
+ JobsError: JobsError,
178
+ SchedulerError: SchedulerError,
179
+ SessionError: SessionError,
180
+ SlugError: SlugError,
181
+ WebhookError: WebhookError,
182
+ ApiKeyError: ApiKeyError,
183
+ PermissionsError: PermissionsError,
184
+ CacheError: CacheError,
185
+ SeederError: SeederError,
186
+ I18nError: I18nError,
187
+ NotifyError: NotifyError,
188
+ TestingError: TestingError,
189
+ };