@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
@@ -0,0 +1,977 @@
1
+ "use strict";
2
+ /**
3
+ * Security-focused YAML parser (safe subset of YAML 1.2).
4
+ *
5
+ * YAML's full grammar is large and historically the source of many CVEs:
6
+ * `!!python/object` deserialization in PyYAML, anchor-cycle billion-laughs
7
+ * DoS, tag-based RCE chains. This parser implements only the JSON-shaped
8
+ * subset that real-world config files actually use, and REJECTS the rest
9
+ * with explicit errors.
10
+ *
11
+ * Supported:
12
+ * - Block mappings: `key: value`, indented children
13
+ * - Block sequences: `- item`, indented multi-line items
14
+ * - Flow mappings: `{ key: value, ... }`
15
+ * - Flow sequences: `[a, b, c]`
16
+ * - Plain scalars with YAML 1.2 *core schema* type inference:
17
+ * null / Null / NULL / ~ / empty null
18
+ * true / True / TRUE / false / False boolean
19
+ * (only these forms — NOT yes/no/on/off, fixing the
20
+ * "Norway problem" where country: NO became country: false)
21
+ * integers: 0, -17, 0o755, 0x1F number
22
+ * floats: 3.14, 5e+22, .inf, -.inf, .nan number
23
+ * everything else string
24
+ * - Single-quoted strings (no escapes; `''` for literal apostrophe)
25
+ * - Double-quoted strings (\n, \t, \\, \", \uXXXX, \UXXXXXXXX)
26
+ * - Block scalars: literal `|` and folded `>` with chomp indicators
27
+ * (-, +, or default)
28
+ * - `#` comments (line-rest)
29
+ *
30
+ * REJECTED with explicit errors:
31
+ * - Anchors `&name` / aliases `*name` (billion-laughs / cycles)
32
+ * - Tags `!tag`, `!!tag`, `!<uri>` (tag-driven deserialization)
33
+ * - YAML directives `%YAML`, `%TAG`
34
+ * - Multi-document streams (`---` / `...` separators)
35
+ * - Complex keys (`? mapping_key`)
36
+ * - Merge keys `<<` (anchor-using feature)
37
+ * - Tabs in indentation (per YAML 1.2 spec)
38
+ * - __proto__ / constructor / prototype as map keys (prototype pollution)
39
+ *
40
+ * Output shape:
41
+ * - mappings plain {} object
42
+ * - sequences JS array
43
+ * - scalars string / number / boolean / null per core schema
44
+ *
45
+ * Public API:
46
+ * yaml.parse(input, opts?) value | throws SafeYamlError
47
+ * yaml.SafeYamlError error class
48
+ *
49
+ * Defaults:
50
+ * maxBytes: 1 MiB
51
+ * maxDepth: 100
52
+ * maxKeys: 50000
53
+ */
54
+
55
+ var C = require("../constants");
56
+ var safeBuffer = require("../safe-buffer");
57
+ var { FrameworkError } = require("../framework-error");
58
+
59
+ class SafeYamlError extends FrameworkError {
60
+ constructor(message, code, line, col) {
61
+ super(line != null ? message + " at line " + line + ":" + col : message);
62
+ this.name = "SafeYamlError";
63
+ this.code = code || "yaml/invalid";
64
+ this.line = line == null ? null : line;
65
+ this.col = col == null ? null : col;
66
+ this.isSafeYamlError = true;
67
+ }
68
+ }
69
+
70
+ var DEFAULTS = {
71
+ maxBytes: C.BYTES.mib(1),
72
+ maxDepth: 100,
73
+ maxKeys: 50000,
74
+ };
75
+
76
+ var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
77
+
78
+ // YAML 1.2 core-schema scalar resolution. Order matters: null first
79
+ // (covers ~ and empty), then bool, then int (with base prefixes), then
80
+ // float, then string fallback.
81
+ var NULL_RE = /^(null|Null|NULL|~|)$/;
82
+ var BOOL_RE = /^(true|True|TRUE|false|False|FALSE)$/;
83
+ var INT_RE = /^[-+]?(0|[1-9][0-9]*)$/;
84
+ var INT_OCT = /^0o[0-7]+$/;
85
+ var INT_HEX = /^0x[0-9a-fA-F]+$/;
86
+ var FLOAT_RE = /^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$/;
87
+ var FLOAT_INF = /^[-+]?\.(inf|Inf|INF)$/;
88
+ var FLOAT_NAN = /^\.(nan|NaN|NAN)$/;
89
+
90
+ function _resolveScalar(s) {
91
+ if (NULL_RE.test(s)) return null;
92
+ if (BOOL_RE.test(s)) return s.toLowerCase() === "true";
93
+ if (INT_RE.test(s)) {
94
+ var n = parseInt(s, 10);
95
+ if (Number.isSafeInteger(n)) return n;
96
+ return s; // fallback to string for huge ints (don't lose precision silently)
97
+ }
98
+ if (INT_OCT.test(s)) {
99
+ var oct = parseInt(s.substring(2), 8);
100
+ if (Number.isSafeInteger(oct)) return oct;
101
+ return s;
102
+ }
103
+ if (INT_HEX.test(s)) {
104
+ var hex = parseInt(s.substring(2), 16);
105
+ if (Number.isSafeInteger(hex)) return hex;
106
+ return s;
107
+ }
108
+ if (FLOAT_INF.test(s)) return s.charAt(0) === "-" ? -Infinity : Infinity;
109
+ if (FLOAT_NAN.test(s)) return NaN;
110
+ if (FLOAT_RE.test(s)) {
111
+ var f = parseFloat(s);
112
+ if (!isNaN(f)) return f;
113
+ }
114
+ return s;
115
+ }
116
+
117
+ function parse(input, opts) {
118
+ opts = opts || {};
119
+ var maxBytes = (typeof opts.maxBytes === "number" && opts.maxBytes > 0)
120
+ ? Math.min(opts.maxBytes, C.BYTES.mib(64)) : DEFAULTS.maxBytes;
121
+ var maxDepth = (typeof opts.maxDepth === "number" && opts.maxDepth > 0)
122
+ ? Math.min(opts.maxDepth, 1000) : DEFAULTS.maxDepth;
123
+ var maxKeys = (typeof opts.maxKeys === "number" && opts.maxKeys > 0)
124
+ ? Math.min(opts.maxKeys, 1000000) : DEFAULTS.maxKeys;
125
+
126
+ input = safeBuffer.normalizeText(input, {
127
+ maxBytes: maxBytes,
128
+ errorClass: SafeYamlError,
129
+ typeCode: "yaml/wrong-input-type",
130
+ sizeCode: "yaml/too-large",
131
+ });
132
+
133
+ // Pre-validate: scan for banned constructs ANYWHERE in the input. Cheap
134
+ // up-front rejection beats partial-parse-then-throw, and it surfaces a
135
+ // clearer diagnostic than "unexpected & at line 47".
136
+ // Skip bans that appear inside quoted strings — those are legitimate
137
+ // string content. Block-scalar content also exempt.
138
+ _preValidate(input);
139
+
140
+ // Normalize line endings: CRLF / CR → LF for consistent line-based work.
141
+ input = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
142
+
143
+ // Split into raw lines preserving line numbers.
144
+ var rawLines = input.split("\n");
145
+ var lines = [];
146
+ for (var i = 0; i < rawLines.length; i++) {
147
+ var raw = rawLines[i];
148
+ // Strip trailing whitespace from the line for analysis (block-scalar
149
+ // content paths preserve their own internal spacing separately).
150
+ var trimmed = raw.replace(/[ \t]+$/, "");
151
+ var indent = 0;
152
+ while (indent < raw.length && raw.charAt(indent) === " ") indent += 1;
153
+ if (indent < raw.length && raw.charAt(indent) === "\t") {
154
+ throw new SafeYamlError("tab in indentation (YAML 1.2 forbids)", "yaml/tab-indent", i + 1, indent + 1);
155
+ }
156
+ var content = raw.substring(indent);
157
+ var isBlank = content.length === 0;
158
+ var isComment = content.charAt(0) === "#";
159
+ lines.push({
160
+ lineNumber: i + 1,
161
+ raw: raw,
162
+ indent: indent,
163
+ content: content,
164
+ trimmed: trimmed,
165
+ isBlank: isBlank,
166
+ isComment: isComment,
167
+ });
168
+ }
169
+
170
+ // Skip leading blank/comment lines and an optional `---` document marker.
171
+ // Reject any later `---` or `...` (multi-document streams not supported).
172
+ var idx = 0;
173
+ while (idx < lines.length && (lines[idx].isBlank || lines[idx].isComment)) idx += 1;
174
+ if (idx < lines.length && /^---(\s|$)/.test(lines[idx].content)) idx += 1;
175
+ // Subsequent doc markers anywhere = reject.
176
+ for (var j = idx; j < lines.length; j++) {
177
+ var c = lines[j].content;
178
+ if (/^---(\s|$)/.test(c) || /^\.\.\.(\s|$)/.test(c)) {
179
+ throw new SafeYamlError(
180
+ "multi-document YAML streams are not supported",
181
+ "yaml/multi-document", lines[j].lineNumber, 1
182
+ );
183
+ }
184
+ }
185
+
186
+ var keyCount = 0;
187
+
188
+ function _bumpKeys(line) {
189
+ keyCount += 1;
190
+ if (keyCount > maxKeys) {
191
+ throw new SafeYamlError("input exceeds maxKeys", "yaml/too-many-keys", line, 1);
192
+ }
193
+ }
194
+
195
+ // ---- Recursive parse ----
196
+ // parseValue takes a starting line index and the *minimum* indent the
197
+ // value occupies. Returns { value, nextLine }.
198
+
199
+ function parseValueAtLine(startIdx, parentIndent, depth) {
200
+ if (depth > maxDepth) {
201
+ throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep",
202
+ startIdx < lines.length ? lines[startIdx].lineNumber : null, 1);
203
+ }
204
+
205
+ // Skip blank + comment lines.
206
+ var k = startIdx;
207
+ while (k < lines.length && (lines[k].isBlank || lines[k].isComment)) k += 1;
208
+ if (k >= lines.length) return { value: null, nextLine: lines.length };
209
+
210
+ var firstLine = lines[k];
211
+ if (firstLine.indent <= parentIndent) {
212
+ // No content for this value — it's an empty value (e.g. `key:` with nothing after)
213
+ return { value: null, nextLine: k };
214
+ }
215
+
216
+ var content = firstLine.content;
217
+ var indent = firstLine.indent;
218
+
219
+ // Block sequence: line starts with `- ` or just `-` (then empty / value)
220
+ if (content === "-" || content.startsWith("- ")) {
221
+ return _parseBlockSequence(k, indent, depth);
222
+ }
223
+
224
+ // Block mapping starts with a key (bare or quoted) followed by `:`.
225
+ var keyRange = _scanKeyRange(content, firstLine.lineNumber, indent);
226
+ if (keyRange) {
227
+ return _parseBlockMapping(k, indent, depth);
228
+ }
229
+
230
+ // Block scalar header: `|` or `>` (with optional chomp/indent indicator)
231
+ if (/^[|>][-+]?[0-9]?\s*(#.*)?$/.test(content)) {
232
+ return _parseBlockScalar(k, indent, content);
233
+ }
234
+
235
+ // Otherwise: a single scalar (possibly continued via flow style on
236
+ // following lines, but for the safe subset we restrict scalars to
237
+ // single-line plain or quoted, or wrapped flow on a single line).
238
+ return _parseScalarOrFlow(k, indent);
239
+ }
240
+
241
+ // _scanKeyRange returns { keyLiteral, valueStart } if the line starts
242
+ // with a bare-or-quoted key followed by ":" + (space|EOL), else null.
243
+ function _scanKeyRange(content, lineNumber, indent) {
244
+ var p = 0;
245
+ var len = content.length;
246
+ if (len === 0) return null;
247
+ if (content.charAt(0) === "?") {
248
+ throw new SafeYamlError("complex keys (`? key`) are not supported",
249
+ "yaml/complex-key-banned", lineNumber, indent + 1);
250
+ }
251
+ if (content.charAt(0) === '"') {
252
+ // Find closing quote
253
+ var i = 1;
254
+ while (i < len) {
255
+ var ch = content.charAt(i);
256
+ if (ch === "\\") { i += 2; continue; }
257
+ if (ch === '"') break;
258
+ i += 1;
259
+ }
260
+ if (i >= len) return null; // unterminated → not a key
261
+ p = i + 1;
262
+ } else if (content.charAt(0) === "'") {
263
+ var j = 1;
264
+ while (j < len) {
265
+ if (content.charAt(j) === "'") {
266
+ if (content.charAt(j + 1) === "'") { j += 2; continue; }
267
+ break;
268
+ }
269
+ j += 1;
270
+ }
271
+ if (j >= len) return null;
272
+ p = j + 1;
273
+ } else {
274
+ // Plain scalar key: read up to ':' followed by space or EOL.
275
+ while (p < len) {
276
+ if (content.charAt(p) === ":" &&
277
+ (p + 1 === len || content.charAt(p + 1) === " ")) {
278
+ break;
279
+ }
280
+ p += 1;
281
+ }
282
+ if (p >= len) return null;
283
+ }
284
+ if (content.charAt(p) !== ":") return null;
285
+ var afterColon = p + 1;
286
+ if (afterColon !== len && content.charAt(afterColon) !== " ") return null;
287
+ return { keyEnd: p, valueStart: afterColon };
288
+ }
289
+
290
+ function _decodeKeyLiteral(raw, lineNumber, col) {
291
+ if (raw.charAt(0) === '"') {
292
+ return _decodeDoubleQuoted(raw, lineNumber, col);
293
+ }
294
+ if (raw.charAt(0) === "'") {
295
+ return _decodeSingleQuoted(raw, lineNumber, col);
296
+ }
297
+ var trimmed = raw.replace(/[ \t]+$/, "");
298
+ if (POISONED_KEYS.has(trimmed)) {
299
+ throw new SafeYamlError("forbidden key '" + trimmed + "'",
300
+ "yaml/poisoned-key", lineNumber, col);
301
+ }
302
+ return trimmed;
303
+ }
304
+
305
+ function _parseBlockMapping(startIdx, indent, depth) {
306
+ var result = Object.create(null);
307
+ var seen = new Set();
308
+ var k = startIdx;
309
+ while (k < lines.length) {
310
+ var ln = lines[k];
311
+ if (ln.isBlank || ln.isComment) { k += 1; continue; }
312
+ if (ln.indent < indent) break;
313
+ if (ln.indent > indent) {
314
+ // Shouldn't happen if our caller dispatched correctly
315
+ throw new SafeYamlError("unexpected indent", "yaml/bad-indent", ln.lineNumber, ln.indent + 1);
316
+ }
317
+ // Reject a block sequence at the same indent — that's not a mapping.
318
+ if (ln.content === "-" || ln.content.startsWith("- ")) {
319
+ throw new SafeYamlError("sequence item where mapping key expected",
320
+ "yaml/expected-key", ln.lineNumber, ln.indent + 1);
321
+ }
322
+ var keyRange = _scanKeyRange(ln.content, ln.lineNumber, ln.indent);
323
+ if (!keyRange) {
324
+ throw new SafeYamlError("expected mapping key 'name:'",
325
+ "yaml/expected-key", ln.lineNumber, ln.indent + 1);
326
+ }
327
+ var keyLiteral = ln.content.substring(0, keyRange.keyEnd);
328
+ var key = _decodeKeyLiteral(keyLiteral, ln.lineNumber, ln.indent + 1);
329
+ if (typeof key !== "string") {
330
+ throw new SafeYamlError("non-string mapping key not supported",
331
+ "yaml/bad-key", ln.lineNumber, ln.indent + 1);
332
+ }
333
+ if (POISONED_KEYS.has(key)) {
334
+ throw new SafeYamlError("forbidden key '" + key + "'",
335
+ "yaml/poisoned-key", ln.lineNumber, ln.indent + 1);
336
+ }
337
+ if (key === "<<") {
338
+ throw new SafeYamlError("merge key '<<' not supported (anchor-using feature)",
339
+ "yaml/merge-key-banned", ln.lineNumber, ln.indent + 1);
340
+ }
341
+ if (seen.has(key)) {
342
+ throw new SafeYamlError("duplicate mapping key '" + key + "'",
343
+ "yaml/duplicate-key", ln.lineNumber, ln.indent + 1);
344
+ }
345
+ seen.add(key);
346
+ _bumpKeys(ln.lineNumber);
347
+
348
+ // Determine the value:
349
+ // 1. if there's content after the colon, that's a flow value or
350
+ // a plain scalar continuation on the same line.
351
+ // 2. otherwise the value lives on subsequent more-indented lines.
352
+ var afterColon = ln.content.substring(keyRange.valueStart).replace(/^[ \t]+/, "");
353
+ // Strip end-of-line comment
354
+ afterColon = _stripEolComment(afterColon);
355
+ var value;
356
+ if (afterColon.length > 0) {
357
+ // Inline value on this line.
358
+ if (afterColon.charAt(0) === "|" || afterColon.charAt(0) === ">") {
359
+ // Block scalar header inline with key
360
+ if (!/^[|>][-+]?[0-9]?\s*$/.test(afterColon)) {
361
+ throw new SafeYamlError("malformed block scalar header",
362
+ "yaml/bad-block-scalar", ln.lineNumber, ln.indent + 1);
363
+ }
364
+ var bs = _parseBlockScalar(k, indent, afterColon);
365
+ // Block scalar header was on this line; content is on the
366
+ // following lines. _parseBlockScalar handles the header
367
+ // tokenisation when given the header content directly.
368
+ value = bs.value;
369
+ k = bs.nextLine;
370
+ result[key] = value;
371
+ continue;
372
+ }
373
+ value = _parseInlineValue(afterColon, ln.lineNumber, ln.indent + keyRange.valueStart);
374
+ k += 1;
375
+ } else {
376
+ // Value is on more-indented lines below.
377
+ var nested = parseValueAtLine(k + 1, indent, depth + 1);
378
+ value = nested.value;
379
+ k = nested.nextLine;
380
+ }
381
+ result[key] = value;
382
+ }
383
+ // Convert null-prototype to plain {} below in _normalize.
384
+ return { value: result, nextLine: k };
385
+ }
386
+
387
+ function _parseBlockSequence(startIdx, indent, depth) {
388
+ var arr = [];
389
+ var k = startIdx;
390
+ while (k < lines.length) {
391
+ var ln = lines[k];
392
+ if (ln.isBlank || ln.isComment) { k += 1; continue; }
393
+ if (ln.indent < indent) break;
394
+ if (ln.indent !== indent) {
395
+ throw new SafeYamlError("unexpected indent in sequence",
396
+ "yaml/bad-indent", ln.lineNumber, ln.indent + 1);
397
+ }
398
+ if (ln.content !== "-" && !ln.content.startsWith("- ")) break;
399
+ _bumpKeys(ln.lineNumber);
400
+
401
+ // Item value:
402
+ // - "- " followed by content → inline value (could be flow, scalar,
403
+ // or the start of a nested mapping if "key: value" pattern)
404
+ // - "-" alone → value on more-indented lines below
405
+ var afterDash = ln.content === "-" ? "" : ln.content.substring(2);
406
+ afterDash = _stripEolComment(afterDash);
407
+ var item;
408
+ if (afterDash.length === 0) {
409
+ var nested = parseValueAtLine(k + 1, indent, depth + 1);
410
+ item = nested.value;
411
+ k = nested.nextLine;
412
+ } else {
413
+ // Could be a nested mapping: `- key: value` (compact mapping)
414
+ var mapKey = _scanKeyRange(afterDash, ln.lineNumber, ln.indent + 2);
415
+ if (mapKey) {
416
+ // Treat as a single-line block mapping at indent+2 by
417
+ // synthesising a sub-region. The mapping's "indent" is the
418
+ // column where the key starts (ln.indent + 2).
419
+ // We need to feed _parseBlockMapping starting at this line but
420
+ // with the mapping treating its own indent as ln.indent + 2.
421
+ // Simplest: clone the line with the dash stripped, then continue
422
+ // with subsequent more-indented lines as the mapping's body.
423
+ var synthetic = {
424
+ lineNumber: ln.lineNumber,
425
+ raw: ln.raw,
426
+ indent: ln.indent + 2,
427
+ content: afterDash,
428
+ trimmed: afterDash,
429
+ isBlank: false,
430
+ isComment: false,
431
+ };
432
+ var saved = lines[k];
433
+ lines[k] = synthetic;
434
+ try {
435
+ var sub = _parseBlockMapping(k, ln.indent + 2, depth + 1);
436
+ item = sub.value;
437
+ k = sub.nextLine;
438
+ } finally {
439
+ lines[saved.lineNumber - 1] = saved;
440
+ }
441
+ } else {
442
+ item = _parseInlineValue(afterDash, ln.lineNumber, ln.indent + 2);
443
+ k += 1;
444
+ }
445
+ }
446
+ arr.push(item);
447
+ }
448
+ return { value: arr, nextLine: k };
449
+ }
450
+
451
+ function _parseInlineValue(text, lineNumber, col) {
452
+ // Strip trailing whitespace/comment already done by caller (mostly).
453
+ // Handle: flow [...] / {...}, quoted strings, plain scalars.
454
+ var t = text;
455
+ if (t.charAt(0) === "[") return _parseFlowSequence(t, lineNumber, col, 0);
456
+ if (t.charAt(0) === "{") return _parseFlowMapping(t, lineNumber, col, 0);
457
+ if (t.charAt(0) === '"') {
458
+ var dq = _decodeDoubleQuoted(t, lineNumber, col);
459
+ var afterDq = _trailingAfterQuoted(t, '"');
460
+ if (afterDq.length > 0 && afterDq.replace(/^\s+/, "") !== "") {
461
+ throw new SafeYamlError("unexpected content after quoted string",
462
+ "yaml/trailing-content", lineNumber, col);
463
+ }
464
+ return dq;
465
+ }
466
+ if (t.charAt(0) === "'") {
467
+ var sq = _decodeSingleQuoted(t, lineNumber, col);
468
+ return sq;
469
+ }
470
+ // Plain scalar — strip trailing space, resolve type.
471
+ return _resolveScalar(t.replace(/[ \t]+$/, ""));
472
+ }
473
+
474
+ // For quoted-string termination check on a single-line value.
475
+ function _trailingAfterQuoted(text, quote) {
476
+ if (text.charAt(0) !== quote) return text;
477
+ var i = 1;
478
+ while (i < text.length) {
479
+ var ch = text.charAt(i);
480
+ if (quote === '"' && ch === "\\") { i += 2; continue; }
481
+ if (ch === quote) {
482
+ if (quote === "'" && text.charAt(i + 1) === "'") { i += 2; continue; }
483
+ return text.substring(i + 1);
484
+ }
485
+ i += 1;
486
+ }
487
+ return "";
488
+ }
489
+
490
+ // ---- Flow style ----
491
+
492
+ function _parseFlowSequence(text, lineNumber, col, depthIncoming) {
493
+ var p = 1; // skip [
494
+ var arr = [];
495
+ while (p < text.length) {
496
+ _flowSkipWs(text, p);
497
+ p = _flowSkipWsIndex(text, p);
498
+ if (text.charAt(p) === "]") return arr;
499
+ var v = _parseFlowValue(text, p, lineNumber, col, depthIncoming + 1);
500
+ arr.push(v.value);
501
+ p = v.nextPos;
502
+ p = _flowSkipWsIndex(text, p);
503
+ if (text.charAt(p) === ",") { p += 1; continue; }
504
+ if (text.charAt(p) === "]") { return arr; }
505
+ throw new SafeYamlError("expected ',' or ']' in flow sequence",
506
+ "yaml/bad-flow", lineNumber, col + p);
507
+ }
508
+ throw new SafeYamlError("unterminated flow sequence",
509
+ "yaml/unterminated-flow", lineNumber, col);
510
+ }
511
+
512
+ function _parseFlowMapping(text, lineNumber, col, depthIncoming) {
513
+ if (depthIncoming > maxDepth) {
514
+ throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep", lineNumber, col);
515
+ }
516
+ var p = 1;
517
+ var result = Object.create(null);
518
+ while (p < text.length) {
519
+ p = _flowSkipWsIndex(text, p);
520
+ if (text.charAt(p) === "}") { return result; }
521
+ // Read key
522
+ var keyVal = _parseFlowKey(text, p, lineNumber, col);
523
+ var key = keyVal.key;
524
+ if (typeof key !== "string") {
525
+ throw new SafeYamlError("non-string flow-mapping key",
526
+ "yaml/bad-key", lineNumber, col + p);
527
+ }
528
+ if (POISONED_KEYS.has(key)) {
529
+ throw new SafeYamlError("forbidden key '" + key + "'",
530
+ "yaml/poisoned-key", lineNumber, col + p);
531
+ }
532
+ _bumpKeys(lineNumber);
533
+ p = keyVal.nextPos;
534
+ p = _flowSkipWsIndex(text, p);
535
+ if (text.charAt(p) !== ":") {
536
+ throw new SafeYamlError("expected ':' in flow mapping",
537
+ "yaml/bad-flow", lineNumber, col + p);
538
+ }
539
+ p += 1;
540
+ p = _flowSkipWsIndex(text, p);
541
+ var valRes = _parseFlowValue(text, p, lineNumber, col, depthIncoming + 1);
542
+ result[key] = valRes.value;
543
+ p = valRes.nextPos;
544
+ p = _flowSkipWsIndex(text, p);
545
+ if (text.charAt(p) === ",") { p += 1; continue; }
546
+ if (text.charAt(p) === "}") { return result; }
547
+ throw new SafeYamlError("expected ',' or '}' in flow mapping",
548
+ "yaml/bad-flow", lineNumber, col + p);
549
+ }
550
+ throw new SafeYamlError("unterminated flow mapping",
551
+ "yaml/unterminated-flow", lineNumber, col);
552
+ }
553
+
554
+ function _parseFlowValue(text, p, lineNumber, col, depthIncoming) {
555
+ if (depthIncoming > maxDepth) {
556
+ throw new SafeYamlError("input exceeds maxDepth", "yaml/too-deep", lineNumber, col + p);
557
+ }
558
+ var ch = text.charAt(p);
559
+ if (ch === "[") {
560
+ var sub = _parseFlowSequence(text.substring(p), lineNumber, col + p, depthIncoming);
561
+ // We need to find where the matching ] closes — re-scan
562
+ var endP = _findMatchingBracket(text, p, "[", "]", lineNumber, col);
563
+ return { value: sub, nextPos: endP + 1 };
564
+ }
565
+ if (ch === "{") {
566
+ var subM = _parseFlowMapping(text.substring(p), lineNumber, col + p, depthIncoming);
567
+ var endM = _findMatchingBracket(text, p, "{", "}", lineNumber, col);
568
+ return { value: subM, nextPos: endM + 1 };
569
+ }
570
+ if (ch === '"') {
571
+ var dq = _decodeDoubleQuoted(text.substring(p), lineNumber, col + p);
572
+ var endQ = _findClosingQuote(text, p, '"', lineNumber, col);
573
+ return { value: dq, nextPos: endQ + 1 };
574
+ }
575
+ if (ch === "'") {
576
+ var sq = _decodeSingleQuoted(text.substring(p), lineNumber, col + p);
577
+ var endSQ = _findClosingQuote(text, p, "'", lineNumber, col);
578
+ return { value: sq, nextPos: endSQ + 1 };
579
+ }
580
+ // Plain scalar — read until , } ] or EOL
581
+ var start = p;
582
+ while (p < text.length) {
583
+ var c = text.charAt(p);
584
+ if (c === "," || c === "}" || c === "]") break;
585
+ p += 1;
586
+ }
587
+ var raw = text.substring(start, p).replace(/[ \t]+$/, "");
588
+ return { value: _resolveScalar(raw), nextPos: p };
589
+ }
590
+
591
+ function _parseFlowKey(text, p, lineNumber, col) {
592
+ var ch = text.charAt(p);
593
+ if (ch === '"') {
594
+ var dq = _decodeDoubleQuoted(text.substring(p), lineNumber, col + p);
595
+ var endQ = _findClosingQuote(text, p, '"', lineNumber, col);
596
+ return { key: dq, nextPos: endQ + 1 };
597
+ }
598
+ if (ch === "'") {
599
+ var sq = _decodeSingleQuoted(text.substring(p), lineNumber, col + p);
600
+ var endSQ = _findClosingQuote(text, p, "'", lineNumber, col);
601
+ return { key: sq, nextPos: endSQ + 1 };
602
+ }
603
+ // Plain key — read until ':' (followed by space/comma/end) or comma/end
604
+ var start = p;
605
+ while (p < text.length) {
606
+ var c = text.charAt(p);
607
+ if (c === ":" || c === "," || c === "}" || c === "]") break;
608
+ p += 1;
609
+ }
610
+ return { key: text.substring(start, p).replace(/[ \t]+$/, ""), nextPos: p };
611
+ }
612
+
613
+ function _findMatchingBracket(text, start, open, close, lineNumber, col) {
614
+ var depth = 0;
615
+ var i = start;
616
+ while (i < text.length) {
617
+ var c = text.charAt(i);
618
+ if (c === '"') { i = _findClosingQuote(text, i, '"', lineNumber, col) + 1; continue; }
619
+ if (c === "'") { i = _findClosingQuote(text, i, "'", lineNumber, col) + 1; continue; }
620
+ if (c === open) depth += 1;
621
+ else if (c === close) {
622
+ depth -= 1;
623
+ if (depth === 0) return i;
624
+ }
625
+ i += 1;
626
+ }
627
+ throw new SafeYamlError("unterminated flow brackets",
628
+ "yaml/unterminated-flow", lineNumber, col + start);
629
+ }
630
+
631
+ function _findClosingQuote(text, start, quote, lineNumber, col) {
632
+ var i = start + 1;
633
+ while (i < text.length) {
634
+ var c = text.charAt(i);
635
+ if (quote === '"' && c === "\\") { i += 2; continue; }
636
+ if (c === quote) {
637
+ if (quote === "'" && text.charAt(i + 1) === "'") { i += 2; continue; }
638
+ return i;
639
+ }
640
+ i += 1;
641
+ }
642
+ throw new SafeYamlError("unterminated quoted string",
643
+ "yaml/unterminated-string", lineNumber, col + start);
644
+ }
645
+
646
+ function _flowSkipWs(_text, _p) { /* no-op shim — real version below */ }
647
+ function _flowSkipWsIndex(text, p) {
648
+ while (p < text.length) {
649
+ var c = text.charAt(p);
650
+ if (c === " " || c === "\t") p += 1;
651
+ else break;
652
+ }
653
+ return p;
654
+ }
655
+
656
+ // ---- Scalar decoding ----
657
+
658
+ function _decodeDoubleQuoted(raw, lineNumber, col) {
659
+ if (raw.charAt(0) !== '"') {
660
+ throw new SafeYamlError("expected '\"'", "yaml/bad-string", lineNumber, col);
661
+ }
662
+ var i = 1;
663
+ var out = "";
664
+ while (i < raw.length) {
665
+ var ch = raw.charAt(i);
666
+ if (ch === '"') return out;
667
+ if (ch === "\\") {
668
+ var esc = raw.charAt(i + 1);
669
+ switch (esc) {
670
+ case '"': out += '"'; i += 2; break;
671
+ case "\\": out += "\\"; i += 2; break;
672
+ case "/": out += "/"; i += 2; break;
673
+ case "n": out += "\n"; i += 2; break;
674
+ case "t": out += "\t"; i += 2; break;
675
+ case "r": out += "\r"; i += 2; break;
676
+ case "b": out += "\b"; i += 2; break;
677
+ case "f": out += "\f"; i += 2; break;
678
+ case "0": out += "\0"; i += 2; break;
679
+ case "u": {
680
+ var hex = raw.substring(i + 2, i + 6);
681
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
682
+ throw new SafeYamlError("bad \\u escape", "yaml/bad-escape", lineNumber, col + i);
683
+ }
684
+ out += String.fromCharCode(parseInt(hex, 16));
685
+ i += 6;
686
+ break;
687
+ }
688
+ case "U": {
689
+ var hex8 = raw.substring(i + 2, i + 10);
690
+ if (!/^[0-9a-fA-F]{8}$/.test(hex8)) {
691
+ throw new SafeYamlError("bad \\U escape", "yaml/bad-escape", lineNumber, col + i);
692
+ }
693
+ var code = parseInt(hex8, 16);
694
+ if (code > 0x10FFFF) {
695
+ throw new SafeYamlError("\\U code point > U+10FFFF",
696
+ "yaml/bad-escape", lineNumber, col + i);
697
+ }
698
+ out += String.fromCodePoint(code);
699
+ i += 10;
700
+ break;
701
+ }
702
+ default:
703
+ throw new SafeYamlError("unknown escape '\\" + esc + "'",
704
+ "yaml/bad-escape", lineNumber, col + i);
705
+ }
706
+ continue;
707
+ }
708
+ out += ch;
709
+ i += 1;
710
+ }
711
+ throw new SafeYamlError("unterminated double-quoted string",
712
+ "yaml/unterminated-string", lineNumber, col);
713
+ }
714
+
715
+ function _decodeSingleQuoted(raw, lineNumber, col) {
716
+ if (raw.charAt(0) !== "'") {
717
+ throw new SafeYamlError("expected \"'\"", "yaml/bad-string", lineNumber, col);
718
+ }
719
+ var i = 1;
720
+ var out = "";
721
+ while (i < raw.length) {
722
+ var ch = raw.charAt(i);
723
+ if (ch === "'") {
724
+ if (raw.charAt(i + 1) === "'") { out += "'"; i += 2; continue; }
725
+ return out;
726
+ }
727
+ out += ch;
728
+ i += 1;
729
+ }
730
+ throw new SafeYamlError("unterminated single-quoted string",
731
+ "yaml/unterminated-string", lineNumber, col);
732
+ }
733
+
734
+ function _stripEolComment(text) {
735
+ // Strip ` #...` comments (must be preceded by whitespace) at end of line.
736
+ var match = text.match(/^(.*?)(\s+#.*)?$/);
737
+ return (match && match[1] != null ? match[1] : text).replace(/[ \t]+$/, "");
738
+ }
739
+
740
+ // ---- Block scalars (| literal, > folded) ----
741
+
742
+ function _parseBlockScalar(startIdx, parentIndent, headerContent) {
743
+ var headerLine = lines[startIdx];
744
+ var header = headerContent.trim();
745
+ var style = header.charAt(0); // '|' or '>'
746
+ var rest = header.substring(1);
747
+ var chomp = ""; // "-" strip, "+" keep, "" clip (default)
748
+ var explicitIndent = null;
749
+ for (var i = 0; i < rest.length; i++) {
750
+ var ch = rest.charAt(i);
751
+ if (ch === "-" || ch === "+") {
752
+ if (chomp) throw new SafeYamlError("multiple chomping indicators",
753
+ "yaml/bad-block-scalar", headerLine.lineNumber, parentIndent + 1);
754
+ chomp = ch;
755
+ } else if (ch >= "1" && ch <= "9") {
756
+ if (explicitIndent != null) throw new SafeYamlError("multiple indentation indicators",
757
+ "yaml/bad-block-scalar", headerLine.lineNumber, parentIndent + 1);
758
+ explicitIndent = parseInt(ch, 10);
759
+ } else if (ch === " " || ch === "\t" || ch === "#") {
760
+ // comment / trailing whitespace — stop reading indicators
761
+ break;
762
+ }
763
+ }
764
+
765
+ // Collect content lines: anything more-indented than parentIndent.
766
+ // The first non-blank content line establishes the block's indent.
767
+ var k = startIdx + 1;
768
+ var blockIndent = null;
769
+ var contentLines = [];
770
+ while (k < lines.length) {
771
+ var ln = lines[k];
772
+ if (ln.isBlank) { contentLines.push(""); k += 1; continue; }
773
+ if (ln.indent <= parentIndent) break;
774
+ if (blockIndent === null) {
775
+ blockIndent = explicitIndent != null ? parentIndent + explicitIndent : ln.indent;
776
+ }
777
+ if (ln.indent < blockIndent) break;
778
+ var trimmedToIndent = ln.raw.substring(blockIndent);
779
+ contentLines.push(trimmedToIndent);
780
+ k += 1;
781
+ }
782
+
783
+ // Trim trailing blank lines based on chomp.
784
+ var trailingBlanks = 0;
785
+ while (contentLines.length > 0 && contentLines[contentLines.length - 1] === "") {
786
+ contentLines.pop();
787
+ trailingBlanks += 1;
788
+ }
789
+
790
+ var body;
791
+ if (style === "|") {
792
+ // Literal: each content line + a newline; preserve internal blanks
793
+ // (we just popped trailing ones for chomp handling).
794
+ body = contentLines.join("\n");
795
+ if (contentLines.length > 0) body += "\n";
796
+ } else {
797
+ // Folded: blank line separates paragraphs (becomes a newline);
798
+ // adjacent non-blank lines fold to single space.
799
+ var folded = "";
800
+ for (var n = 0; n < contentLines.length; n++) {
801
+ var cl = contentLines[n];
802
+ if (cl === "") {
803
+ folded += "\n";
804
+ continue;
805
+ }
806
+ if (folded.length > 0 && !folded.endsWith("\n")) {
807
+ folded += " ";
808
+ }
809
+ folded += cl;
810
+ }
811
+ body = folded;
812
+ if (contentLines.length > 0) body += "\n";
813
+ }
814
+
815
+ if (chomp === "-") {
816
+ // strip — remove trailing newline(s)
817
+ body = body.replace(/\n+$/, "");
818
+ } else if (chomp === "+") {
819
+ // keep — restore trailing blanks we popped
820
+ body += "\n".repeat(trailingBlanks);
821
+ } /* else "clip" — keep one trailing newline (already there) */
822
+
823
+ return { value: body, nextLine: k };
824
+ }
825
+
826
+ function _parseScalarOrFlow(startIdx, indent) {
827
+ var ln = lines[startIdx];
828
+ var content = _stripEolComment(ln.content);
829
+ var v = _parseInlineValue(content, ln.lineNumber, ln.indent + 1);
830
+ return { value: v, nextLine: startIdx + 1 };
831
+ }
832
+
833
+ // ---- Top-level dispatch ----
834
+ var top = parseValueAtLine(idx, -1, 1);
835
+
836
+ // Normalize null-prototype objects to plain {} so JSON.stringify and
837
+ // for-in behave naturally for the operator.
838
+ function _normalize(value) {
839
+ if (Array.isArray(value)) return value.map(_normalize);
840
+ if (value && typeof value === "object" && !(value instanceof Date)) {
841
+ var out = {};
842
+ for (var k in value) {
843
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
844
+ out[k] = _normalize(value[k]);
845
+ }
846
+ }
847
+ return out;
848
+ }
849
+ return value;
850
+ }
851
+ return _normalize(top.value);
852
+ }
853
+
854
+ // ---- Pre-validation: scan input for banned constructs ----
855
+ //
856
+ // We mask out string content (single-quoted, double-quoted) and block-
857
+ // scalar bodies before scanning, so legitimate string content with `&`
858
+ // or `!` characters doesn't false-positive.
859
+ function _preValidate(input) {
860
+ // Build a "scan-safe" copy where quoted-string bodies and block-scalar
861
+ // bodies are replaced with same-length whitespace. Simple state machine.
862
+ var len = input.length;
863
+ var line = 1;
864
+ var col = 1;
865
+ var i = 0;
866
+ var safe = "";
867
+
868
+ function advance(n) {
869
+ n = n == null ? 1 : n;
870
+ for (var z = 0; z < n; z++) {
871
+ if (i < len && input.charCodeAt(i) === 0x0A) { line += 1; col = 1; }
872
+ else col += 1;
873
+ i += 1;
874
+ }
875
+ }
876
+
877
+ while (i < len) {
878
+ var c = input.charAt(i);
879
+ // Comments — pass through
880
+ if (c === "#") {
881
+ while (i < len && input.charAt(i) !== "\n") {
882
+ safe += input.charAt(i);
883
+ i += 1;
884
+ col += 1;
885
+ }
886
+ continue;
887
+ }
888
+ // Quoted strings — mask body
889
+ if (c === '"' || c === "'") {
890
+ var quote = c;
891
+ var startLine = line;
892
+ var startCol = col;
893
+ safe += c;
894
+ advance();
895
+ while (i < len) {
896
+ var ch = input.charAt(i);
897
+ if (quote === '"' && ch === "\\" && i + 1 < len) {
898
+ // mask both
899
+ safe += " ";
900
+ advance(2);
901
+ continue;
902
+ }
903
+ if (ch === quote) {
904
+ if (quote === "'" && input.charAt(i + 1) === "'") {
905
+ safe += " ";
906
+ advance(2);
907
+ continue;
908
+ }
909
+ safe += ch;
910
+ advance();
911
+ break;
912
+ }
913
+ // Mask content (preserve newlines for line counting)
914
+ safe += (ch === "\n") ? "\n" : " ";
915
+ advance();
916
+ }
917
+ if (i > len) {
918
+ throw new SafeYamlError("unterminated quoted string",
919
+ "yaml/unterminated-string", startLine, startCol);
920
+ }
921
+ continue;
922
+ }
923
+ // Pass through everything else
924
+ safe += c;
925
+ advance();
926
+ }
927
+
928
+ // Now scan `safe` for banned constructs.
929
+ // Banned tokens (must be at line-start or after whitespace, not in keys):
930
+ // &name anchor
931
+ // *name alias
932
+ // !tag, !!tag
933
+ // %YAML / %TAG directive (only at column 0)
934
+
935
+ // Anchors: `&` followed by a name char, with whitespace or `-`/`?` before
936
+ // (or start of value position). A simple heuristic: any unescaped `&`
937
+ // that's followed by an identifier char and is preceded by space or
938
+ // line start is an anchor. Same for `*`.
939
+ var anchorOrAliasRe = /(^|\s)([&*])([A-Za-z0-9_][A-Za-z0-9_-]*)/;
940
+ var m = safe.match(anchorOrAliasRe);
941
+ if (m) {
942
+ var posIdx = safe.indexOf(m[0]);
943
+ var lineCount = safe.substring(0, posIdx).split("\n").length;
944
+ throw new SafeYamlError(
945
+ m[2] === "&" ? "anchors are not supported" : "aliases are not supported",
946
+ m[2] === "&" ? "yaml/anchors-banned" : "yaml/aliases-banned",
947
+ lineCount, 1
948
+ );
949
+ }
950
+
951
+ // Tags: `!` at start of value or after `: ` / `- `. False-positive risk:
952
+ // "key: !something" vs "key: ![bracket". We match `!` followed by
953
+ // alphanumeric or `<`.
954
+ var tagRe = /(^|[\s-])(!{1,2}[A-Za-z<])/;
955
+ var mt = safe.match(tagRe);
956
+ if (mt) {
957
+ var tagIdx = safe.indexOf(mt[0]);
958
+ var tagLine = safe.substring(0, tagIdx).split("\n").length;
959
+ throw new SafeYamlError("tags are not supported",
960
+ "yaml/tags-banned", tagLine, 1);
961
+ }
962
+
963
+ // Directives: `%YAML` or `%TAG` at column 0
964
+ var dirRe = /(^|\n)%(YAML|TAG)\b/;
965
+ var md = safe.match(dirRe);
966
+ if (md) {
967
+ var dirIdx = safe.indexOf(md[0]);
968
+ var dirLine = safe.substring(0, dirIdx).split("\n").length + (md[1] === "\n" ? 1 : 0);
969
+ throw new SafeYamlError("directives are not supported",
970
+ "yaml/directives-banned", dirLine, 1);
971
+ }
972
+ }
973
+
974
+ module.exports = {
975
+ parse: parse,
976
+ SafeYamlError: SafeYamlError,
977
+ };