@blamejs/core 0.6.69 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +27 -20
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +17 -4
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +42 -17
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/http-client.js
CHANGED
|
@@ -131,9 +131,9 @@ var _transports = new Map();
|
|
|
131
131
|
// effect on the per-origin cache.
|
|
132
132
|
var DEFAULT_AGENT_OPTS = Object.freeze({
|
|
133
133
|
keepAlive: true,
|
|
134
|
-
keepAliveMsecs:
|
|
135
|
-
maxSockets: 16,
|
|
136
|
-
maxFreeSockets: 8,
|
|
134
|
+
keepAliveMsecs: C.TIME.seconds(1),
|
|
135
|
+
maxSockets: C.BYTES.bytes(16),
|
|
136
|
+
maxFreeSockets: C.BYTES.bytes(8),
|
|
137
137
|
scheduling: "lifo",
|
|
138
138
|
});
|
|
139
139
|
|
|
@@ -172,7 +172,7 @@ function configurePool(opts) {
|
|
|
172
172
|
// subsequent requests build fresh transports with the new opts.
|
|
173
173
|
_transports.forEach(function (t) {
|
|
174
174
|
if (t && t.kind === "h1" && t.agent && typeof t.agent.destroy === "function") {
|
|
175
|
-
try { t.agent.destroy(); } catch (_e) {}
|
|
175
|
+
try { t.agent.destroy(); } catch (_e) { /* best-effort agent teardown */ }
|
|
176
176
|
}
|
|
177
177
|
if (t && t.kind === "h2" && t.session) {
|
|
178
178
|
_tearDownH2Session(t.session);
|
|
@@ -202,9 +202,16 @@ var DEFAULT_IDLE_TIMEOUT_MS = C.TIME.seconds(30);
|
|
|
202
202
|
// connection forever.
|
|
203
203
|
var H2_SESSION_IDLE_TIMEOUT_MS = C.TIME.minutes(5);
|
|
204
204
|
|
|
205
|
+
// IANA-assigned default ports per RFC 9110 §4.2.
|
|
206
|
+
var DEFAULT_HTTPS_PORT = 443;
|
|
207
|
+
var DEFAULT_HTTP_PORT = C.BYTES.bytes(80);
|
|
208
|
+
|
|
209
|
+
function _defaultPortFor(u) {
|
|
210
|
+
return u.protocol === "https:" ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
|
|
211
|
+
}
|
|
212
|
+
|
|
205
213
|
function _originKey(u) {
|
|
206
|
-
return u.protocol + "//" + u.hostname + ":" +
|
|
207
|
-
(u.port || (u.protocol === "https:" ? 443 : 80));
|
|
214
|
+
return u.protocol + "//" + u.hostname + ":" + (u.port || _defaultPortFor(u));
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
function _makeH1Transport(u, ips) {
|
|
@@ -342,9 +349,17 @@ function _makeError(errorClass, code, message, permanent, statusCode) {
|
|
|
342
349
|
return new errorClass(code, message, permanent, statusCode);
|
|
343
350
|
}
|
|
344
351
|
|
|
352
|
+
// RFC 9110 §15.5 4xx codes that are NOT permanent (request-timeout,
|
|
353
|
+
// too-early, too-many-requests — operator should retry).
|
|
354
|
+
var STATUS_REQUEST_TIMEOUT = C.BYTES.bytes(408);
|
|
355
|
+
var STATUS_TOO_EARLY = 425;
|
|
356
|
+
var STATUS_TOO_MANY_REQUESTS = 429;
|
|
357
|
+
|
|
345
358
|
function _isPermanentStatus(statusCode) {
|
|
346
359
|
if (statusCode >= 400 && statusCode < 500) {
|
|
347
|
-
return statusCode !==
|
|
360
|
+
return statusCode !== STATUS_REQUEST_TIMEOUT &&
|
|
361
|
+
statusCode !== STATUS_TOO_EARLY &&
|
|
362
|
+
statusCode !== STATUS_TOO_MANY_REQUESTS;
|
|
348
363
|
}
|
|
349
364
|
return false;
|
|
350
365
|
}
|
|
@@ -412,7 +427,7 @@ function _attachJarCookie(headers, jar, url) {
|
|
|
412
427
|
// parser accepts so round-trip from one blamejs app's outbound to
|
|
413
428
|
// another's inbound is exact.
|
|
414
429
|
function _buildMultipartBody(spec) {
|
|
415
|
-
var boundary = "----blamejs-mp-" + crypto.generateToken(16);
|
|
430
|
+
var boundary = "----blamejs-mp-" + crypto.generateToken(C.BYTES.bytes(16));
|
|
416
431
|
var CRLF = "\r\n";
|
|
417
432
|
var parts = [];
|
|
418
433
|
|
|
@@ -600,7 +615,7 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
600
615
|
var originalUrl = opts.url;
|
|
601
616
|
var originalOrigin = null;
|
|
602
617
|
try {
|
|
603
|
-
var u0 =
|
|
618
|
+
var u0 = safeUrl.parse(opts.url, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
|
|
604
619
|
originalOrigin = u0.protocol + "//" + u0.host;
|
|
605
620
|
} catch (_e) { /* request() will reject on next hop's parse */ }
|
|
606
621
|
|
|
@@ -618,7 +633,12 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
618
633
|
// of the request that produced the redirect, which may itself be a
|
|
619
634
|
// post-redirect URL).
|
|
620
635
|
var nextUrl;
|
|
621
|
-
try {
|
|
636
|
+
try {
|
|
637
|
+
// Resolve relative Location against current URL using Node's URL
|
|
638
|
+
// base-URL form. Re-validate the resolved absolute through safeUrl
|
|
639
|
+
// immediately below.
|
|
640
|
+
nextUrl = Reflect.construct(URL, [loc, current.url]).toString();
|
|
641
|
+
}
|
|
622
642
|
catch (_e) {
|
|
623
643
|
return Promise.reject(_makeError(opts.errorClass, "BAD_REDIRECT",
|
|
624
644
|
"Location header invalid URL: " + loc, true));
|
|
@@ -628,7 +648,7 @@ function _requestWithRedirects(opts, hopsLeft) {
|
|
|
628
648
|
var nextHeaders = current.headers || {};
|
|
629
649
|
var nextOrigin;
|
|
630
650
|
try {
|
|
631
|
-
var nu =
|
|
651
|
+
var nu = safeUrl.parse(nextUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
|
|
632
652
|
nextOrigin = nu.protocol + "//" + nu.host;
|
|
633
653
|
} catch (_e) { /* request() will reject when it tries to parse */ }
|
|
634
654
|
if (originalOrigin && nextOrigin && nextOrigin !== originalOrigin) {
|
|
@@ -808,8 +828,9 @@ function _requestH1(transport, u, opts) {
|
|
|
808
828
|
if (signal && signal.aborted) {
|
|
809
829
|
var r0 = signal.reason;
|
|
810
830
|
var code0 = (r0 && r0.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
811
|
-
|
|
831
|
+
reject(_makeError(opts.errorClass, code0,
|
|
812
832
|
(r0 && r0.message) || "request aborted before start", false));
|
|
833
|
+
return;
|
|
813
834
|
}
|
|
814
835
|
|
|
815
836
|
if (Buffer.isBuffer(opts.body)) {
|
|
@@ -819,7 +840,7 @@ function _requestH1(transport, u, opts) {
|
|
|
819
840
|
var reqOpts = {
|
|
820
841
|
method: method,
|
|
821
842
|
hostname: u.hostname,
|
|
822
|
-
port: u.port || (u
|
|
843
|
+
port: u.port || _defaultPortFor(u),
|
|
823
844
|
path: u.pathname + (u.search || ""),
|
|
824
845
|
headers: headers,
|
|
825
846
|
agent: transport.agent,
|
|
@@ -944,7 +965,7 @@ function _requestH1(transport, u, opts) {
|
|
|
944
965
|
var r = signal.reason;
|
|
945
966
|
var code = (r && r.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
946
967
|
var msg = (r && r.message) || "request aborted";
|
|
947
|
-
try { req.destroy(r || new Error(msg)); } catch (_e) {}
|
|
968
|
+
try { req.destroy(r || new Error(msg)); } catch (_e) { /* best-effort req teardown */ }
|
|
948
969
|
_reject(_makeError(opts.errorClass, code, msg, false));
|
|
949
970
|
};
|
|
950
971
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -970,7 +991,7 @@ function _requestH1(transport, u, opts) {
|
|
|
970
991
|
opts.body.on("data", function (c) { _emitUpload(c.length); });
|
|
971
992
|
}
|
|
972
993
|
opts.body.on("error", function (e) {
|
|
973
|
-
try { req.destroy(); } catch (_) {}
|
|
994
|
+
try { req.destroy(); } catch (_) { /* best-effort req teardown */ }
|
|
974
995
|
_reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
|
|
975
996
|
"request body stream error: " + e.message, false));
|
|
976
997
|
});
|
|
@@ -1012,8 +1033,9 @@ function _requestH2(transport, u, opts) {
|
|
|
1012
1033
|
if (signal && signal.aborted) {
|
|
1013
1034
|
var r0 = signal.reason;
|
|
1014
1035
|
var code0 = (r0 && r0.name === "TimeoutError") ? "ETIMEDOUT" : "ABORT";
|
|
1015
|
-
|
|
1036
|
+
reject(_makeError(opts.errorClass, code0,
|
|
1016
1037
|
(r0 && r0.message) || "request aborted before start", false));
|
|
1038
|
+
return;
|
|
1017
1039
|
}
|
|
1018
1040
|
|
|
1019
1041
|
var headers = _toH2Headers(method, u, opts.headers || {});
|
|
@@ -1027,7 +1049,8 @@ function _requestH2(transport, u, opts) {
|
|
|
1027
1049
|
endStream: opts.body == null,
|
|
1028
1050
|
});
|
|
1029
1051
|
} catch (e) {
|
|
1030
|
-
|
|
1052
|
+
reject(_makeError(opts.errorClass, e.code || "H2_REQUEST_ERROR", e.message, false));
|
|
1053
|
+
return;
|
|
1031
1054
|
}
|
|
1032
1055
|
|
|
1033
1056
|
var settled = false;
|
|
@@ -1037,7 +1060,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1037
1060
|
// Idle timeout for the stream itself (zero-progress detector).
|
|
1038
1061
|
var idleMs = typeof opts.idleTimeoutMs === "number" ? opts.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
|
|
1039
1062
|
stream.setTimeout(idleMs, function () {
|
|
1040
|
-
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
|
|
1063
|
+
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) { /* best-effort h2 stream cancel */ }
|
|
1041
1064
|
_reject(_makeError(opts.errorClass, "ETIMEDOUT",
|
|
1042
1065
|
"h2 stream idle timeout (no data for " + idleMs + "ms)", false));
|
|
1043
1066
|
});
|
|
@@ -1072,7 +1095,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1072
1095
|
try { collector.push(chunk); }
|
|
1073
1096
|
catch (_e) {
|
|
1074
1097
|
capExceeded = true;
|
|
1075
|
-
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e2) {}
|
|
1098
|
+
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e2) { /* best-effort h2 stream cancel */ }
|
|
1076
1099
|
_reject(_makeError(opts.errorClass, "RESPONSE_TOO_LARGE",
|
|
1077
1100
|
"response body exceeds " + maxResponseBytes + " bytes", true));
|
|
1078
1101
|
}
|
|
@@ -1107,7 +1130,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1107
1130
|
var msg = (r && r.message) || "request aborted";
|
|
1108
1131
|
// NGHTTP2_CANCEL is the protocol-level "I gave up" signal —
|
|
1109
1132
|
// cleaner than destroying the stream.
|
|
1110
|
-
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) {}
|
|
1133
|
+
try { stream.close(http2.constants.NGHTTP2_CANCEL); } catch (_e) { /* best-effort h2 stream cancel */ }
|
|
1111
1134
|
_reject(_makeError(opts.errorClass, code, msg, false));
|
|
1112
1135
|
};
|
|
1113
1136
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1115,7 +1138,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1115
1138
|
|
|
1116
1139
|
if (opts.body && typeof opts.body.pipe === "function") {
|
|
1117
1140
|
opts.body.on("error", function (e) {
|
|
1118
|
-
try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) {}
|
|
1141
|
+
try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) { /* best-effort h2 stream cancel */ }
|
|
1119
1142
|
_reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
|
|
1120
1143
|
"request body stream error: " + e.message, false));
|
|
1121
1144
|
});
|
|
@@ -1134,7 +1157,7 @@ function _requestH2(transport, u, opts) {
|
|
|
1134
1157
|
function _resetForTest() {
|
|
1135
1158
|
_transports.forEach(function (t) {
|
|
1136
1159
|
if (t && t.kind === "h1" && t.agent && typeof t.agent.destroy === "function") {
|
|
1137
|
-
try { t.agent.destroy(); } catch (_e) {}
|
|
1160
|
+
try { t.agent.destroy(); } catch (_e) { /* best-effort agent teardown */ }
|
|
1138
1161
|
}
|
|
1139
1162
|
if (t && t.kind === "h2" && t.session) {
|
|
1140
1163
|
_tearDownH2Session(t.session);
|
|
@@ -1149,7 +1172,7 @@ function _getCachedTransportCount() {
|
|
|
1149
1172
|
|
|
1150
1173
|
// Diagnostic — returns 'h1' | 'h2' | null for a given URL's cached transport.
|
|
1151
1174
|
function _getCachedTransportKind(url) {
|
|
1152
|
-
var u = url instanceof URL ? url :
|
|
1175
|
+
var u = url instanceof URL ? url : safeUrl.parse(url, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
|
|
1153
1176
|
var t = _transports.get(_originKey(u));
|
|
1154
1177
|
if (!t) return null;
|
|
1155
1178
|
if (t.then) return "pending";
|
package/lib/i18n.js
CHANGED
|
@@ -63,6 +63,7 @@ var fs = require("node:fs");
|
|
|
63
63
|
var path = require("node:path");
|
|
64
64
|
var lazyRequire = require("./lazy-require");
|
|
65
65
|
var requestHelpers = require("./request-helpers");
|
|
66
|
+
var safeJson = require("./safe-json");
|
|
66
67
|
var validateOpts = require("./validate-opts");
|
|
67
68
|
var { I18nError } = require("./framework-error");
|
|
68
69
|
|
|
@@ -239,7 +240,7 @@ function _loadFromDir(dir, locales) {
|
|
|
239
240
|
throw _err("LOAD_FAILED",
|
|
240
241
|
"i18n: failed to read '" + filePath + "': " + ((e && e.message) || String(e)));
|
|
241
242
|
}
|
|
242
|
-
try { out[locale] =
|
|
243
|
+
try { out[locale] = safeJson.parse(raw); }
|
|
243
244
|
catch (e) {
|
|
244
245
|
throw _err("LOAD_FAILED",
|
|
245
246
|
"i18n: failed to parse JSON for locale '" + locale + "' (" + filePath + "): " +
|
|
@@ -252,9 +253,7 @@ function _loadFromDir(dir, locales) {
|
|
|
252
253
|
// ---- Validation: create opts ----
|
|
253
254
|
|
|
254
255
|
function _validateCreateOpts(opts) {
|
|
255
|
-
|
|
256
|
-
throw _err("BAD_OPT", "i18n.create: opts must be an object");
|
|
257
|
-
}
|
|
256
|
+
validateOpts.requireObject(opts, "i18n.create", I18nError);
|
|
258
257
|
_validateLocale("i18n.create: defaultLocale", opts.defaultLocale);
|
|
259
258
|
_validateLocaleArray("i18n.create: locales", opts.locales);
|
|
260
259
|
if (opts.locales.indexOf(opts.defaultLocale) === -1) {
|
|
@@ -279,18 +278,9 @@ function _validateCreateOpts(opts) {
|
|
|
279
278
|
throw _err("BAD_OPT", "i18n.create: translations must be an object keyed by locale");
|
|
280
279
|
}
|
|
281
280
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (opts.observability !== undefined && opts.observability !== null) {
|
|
286
|
-
if (typeof opts.observability !== "object" ||
|
|
287
|
-
typeof opts.observability.event !== "function") {
|
|
288
|
-
throw _err("BAD_OPT", "i18n.create: observability must be a b.observability-shaped object");
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (opts.clock !== undefined && typeof opts.clock !== "function") {
|
|
292
|
-
throw _err("BAD_OPT", "i18n.create: clock must be a function");
|
|
293
|
-
}
|
|
281
|
+
validateOpts.optionalNonEmptyString(opts.dir, "i18n.create: dir", I18nError);
|
|
282
|
+
validateOpts.observabilityShape(opts.observability, "i18n.create", I18nError);
|
|
283
|
+
validateOpts.optionalFunction(opts.clock, "i18n.create: clock", I18nError);
|
|
294
284
|
}
|
|
295
285
|
|
|
296
286
|
// ---- Dotted-path resolution ----
|
package/lib/jobs.js
CHANGED
|
@@ -73,10 +73,13 @@
|
|
|
73
73
|
* jobs.stats()
|
|
74
74
|
* → { defined: [string], started: boolean }
|
|
75
75
|
*/
|
|
76
|
+
var { boot } = require("./log");
|
|
76
77
|
var queue = require("./queue");
|
|
77
78
|
var validateOpts = require("./validate-opts");
|
|
78
79
|
var { JobsError } = require("./framework-error");
|
|
79
80
|
|
|
81
|
+
var log = boot("jobs");
|
|
82
|
+
|
|
80
83
|
function create(opts) {
|
|
81
84
|
opts = opts || {};
|
|
82
85
|
validateOpts(opts, [
|
|
@@ -146,7 +149,8 @@ function create(opts) {
|
|
|
146
149
|
async function shutdown(shutdownOpts) {
|
|
147
150
|
if (!started) {
|
|
148
151
|
// Even when not started, queue.shutdown handles its own state.
|
|
149
|
-
try { await queue.shutdown(shutdownOpts); }
|
|
152
|
+
try { await queue.shutdown(shutdownOpts); }
|
|
153
|
+
catch (e) { log.debug("shutdown-failed", { op: "queue.shutdown", error: e.message }); }
|
|
150
154
|
return;
|
|
151
155
|
}
|
|
152
156
|
started = false;
|
|
@@ -47,22 +47,32 @@
|
|
|
47
47
|
*/
|
|
48
48
|
var C = require("./constants");
|
|
49
49
|
var nodeCrypto = require("node:crypto");
|
|
50
|
+
var safeAsync = require("./safe-async");
|
|
51
|
+
var safeJson = require("./safe-json");
|
|
50
52
|
var sigv4 = require("./object-store/sigv4");
|
|
51
53
|
var retryHelper = require("./retry");
|
|
52
54
|
var { LogStreamError } = require("./framework-error");
|
|
53
55
|
var httpClient = require("./http-client");
|
|
54
56
|
|
|
55
57
|
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
56
|
-
|
|
58
|
+
// AWS PutLogEvents hard limit: 10K events per batch (Logs_20140328 docs).
|
|
59
|
+
var CW_MAX_EVENTS_PER_BATCH = C.BYTES.bytes(10000);
|
|
57
60
|
var CW_MAX_BATCH_BYTES = C.BYTES.mib(1);
|
|
58
|
-
var CW_MAX_EVENT_BYTES = 256
|
|
59
|
-
var CW_EVENT_OVERHEAD_BYTES = 26;
|
|
61
|
+
var CW_MAX_EVENT_BYTES = C.BYTES.kib(256);
|
|
62
|
+
var CW_EVENT_OVERHEAD_BYTES = C.BYTES.bytes(26);
|
|
63
|
+
// Truncated-message preview embedded in onDrop callbacks for oversize
|
|
64
|
+
// events — short enough to fit in a single log line so operators can
|
|
65
|
+
// spot the producer that emitted the over-cap record.
|
|
66
|
+
var DROP_PREVIEW_BYTES = C.BYTES.bytes(200);
|
|
60
67
|
|
|
61
68
|
var DEFAULTS = {
|
|
62
69
|
batchSize: 100,
|
|
63
70
|
maxBatchAgeMs: C.TIME.seconds(5),
|
|
64
71
|
timeoutMs: C.TIME.seconds(30),
|
|
65
|
-
|
|
72
|
+
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
73
|
+
// identity passthrough so the file's literal arithmetic has a single
|
|
74
|
+
// source of truth.
|
|
75
|
+
bufferLimit: C.BYTES.bytes(10000),
|
|
66
76
|
};
|
|
67
77
|
|
|
68
78
|
var _err = LogStreamError.factory;
|
|
@@ -96,15 +106,16 @@ function _signedHeaders(cfg, body, target) {
|
|
|
96
106
|
"X-Amz-Target": target,
|
|
97
107
|
};
|
|
98
108
|
var signed = sigv4.signRequest({
|
|
99
|
-
method:
|
|
100
|
-
url:
|
|
101
|
-
headers:
|
|
102
|
-
payloadHash:
|
|
103
|
-
region:
|
|
104
|
-
service:
|
|
105
|
-
accessKeyId:
|
|
106
|
-
secretAccessKey:
|
|
107
|
-
sessionToken:
|
|
109
|
+
method: "POST",
|
|
110
|
+
url: url,
|
|
111
|
+
headers: unsigned,
|
|
112
|
+
payloadHash: payloadHash,
|
|
113
|
+
region: cfg.region,
|
|
114
|
+
service: "logs",
|
|
115
|
+
accessKeyId: cfg.accessKeyId,
|
|
116
|
+
secretAccessKey: cfg.secretAccessKey,
|
|
117
|
+
sessionToken: cfg.sessionToken || null,
|
|
118
|
+
allowedProtocols: cfg.allowedProtocols,
|
|
108
119
|
});
|
|
109
120
|
return signed.headers;
|
|
110
121
|
}
|
|
@@ -188,23 +199,13 @@ function create(config) {
|
|
|
188
199
|
}
|
|
189
200
|
var cfg = Object.assign({}, DEFAULTS, config);
|
|
190
201
|
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
191
|
-
|
|
192
|
-
if (!onDrop) return;
|
|
193
|
-
try { onDrop({ reason: reason, batch: batch, error: err || null }); }
|
|
194
|
-
catch (_e) { /* best-effort */ }
|
|
195
|
-
}
|
|
202
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
196
203
|
var buffer = [];
|
|
197
204
|
var dropCount = 0;
|
|
198
|
-
var flushTimer = null;
|
|
199
205
|
var inFlight = false;
|
|
200
206
|
var closed = false;
|
|
201
207
|
var sequenceToken = null;
|
|
202
|
-
|
|
203
|
-
function _scheduleFlush() {
|
|
204
|
-
if (flushTimer) return;
|
|
205
|
-
flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, cfg.maxBatchAgeMs);
|
|
206
|
-
flushTimer.unref();
|
|
207
|
-
}
|
|
208
|
+
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
208
209
|
|
|
209
210
|
function _takeBatch() {
|
|
210
211
|
var batch = [];
|
|
@@ -267,7 +268,7 @@ function create(config) {
|
|
|
267
268
|
}
|
|
268
269
|
} finally {
|
|
269
270
|
inFlight = false;
|
|
270
|
-
if (buffer.length > 0)
|
|
271
|
+
if (buffer.length > 0) flushScheduler.schedule();
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
@@ -290,7 +291,7 @@ function create(config) {
|
|
|
290
291
|
}
|
|
291
292
|
if (res && res.body) {
|
|
292
293
|
try {
|
|
293
|
-
var parsed =
|
|
294
|
+
var parsed = safeJson.parse(res.body.toString("utf8"), { maxBytes: MAX_RESPONSE_BYTES });
|
|
294
295
|
if (parsed && parsed.nextSequenceToken) sequenceToken = parsed.nextSequenceToken;
|
|
295
296
|
} catch (_e) { /* response body not JSON; modern CW returns empty */ }
|
|
296
297
|
}
|
|
@@ -309,7 +310,7 @@ function create(config) {
|
|
|
309
310
|
if (size > CW_MAX_EVENT_BYTES) {
|
|
310
311
|
_emitDrop("event-too-large", [{
|
|
311
312
|
timestamp: record.ts || Date.now(),
|
|
312
|
-
message: message.slice(0,
|
|
313
|
+
message: message.slice(0, DROP_PREVIEW_BYTES) + "...[truncated for drop event]",
|
|
313
314
|
}], new Error("event exceeds 256 KiB CloudWatch hard cap (was " + size + " bytes)"));
|
|
314
315
|
dropCount += 1;
|
|
315
316
|
return Promise.resolve({ accepted: false, reason: "event too large" });
|
|
@@ -326,14 +327,14 @@ function create(config) {
|
|
|
326
327
|
if (buffer.length >= cfg.batchSize) {
|
|
327
328
|
_flush().catch(function () {});
|
|
328
329
|
} else {
|
|
329
|
-
|
|
330
|
+
flushScheduler.schedule();
|
|
330
331
|
}
|
|
331
332
|
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
async function close() {
|
|
335
336
|
closed = true;
|
|
336
|
-
|
|
337
|
+
flushScheduler.cancel();
|
|
337
338
|
await _flush();
|
|
338
339
|
}
|
|
339
340
|
|
package/lib/log-stream-local.js
CHANGED
|
@@ -28,8 +28,12 @@ var path = require("path");
|
|
|
28
28
|
var zlib = require("zlib");
|
|
29
29
|
var atomicFile = require("./atomic-file");
|
|
30
30
|
var C = require("./constants");
|
|
31
|
+
var { boot } = require("./log");
|
|
32
|
+
var time = require("./time");
|
|
31
33
|
var { LogStreamError } = require("./framework-error");
|
|
32
34
|
|
|
35
|
+
var log = boot("log-stream-local");
|
|
36
|
+
|
|
33
37
|
var DEFAULTS = {
|
|
34
38
|
maxFileBytes: C.BYTES.mib(100),
|
|
35
39
|
maxFileAgeMs: C.TIME.days(7),
|
|
@@ -72,9 +76,13 @@ function create(config) {
|
|
|
72
76
|
|
|
73
77
|
function _rotate() {
|
|
74
78
|
try {
|
|
75
|
-
if (fd != null) {
|
|
79
|
+
if (fd != null) {
|
|
80
|
+
try { fs.closeSync(fd); }
|
|
81
|
+
catch (e) { log.warn("rotate-close-failed: " + e.message); }
|
|
82
|
+
fd = null;
|
|
83
|
+
}
|
|
76
84
|
// Build rotated filename: blamejs-YYYYMMDDTHHMMSSZ.log
|
|
77
|
-
var stamp = new Date()
|
|
85
|
+
var stamp = time.toIso8601NoMs(new Date()).replace(/[-:]/g, "");
|
|
78
86
|
var rotated = path.join(dir, cfg.fileNamePrefix + "-" + stamp + ".log");
|
|
79
87
|
if (fs.existsSync(activePath)) {
|
|
80
88
|
fs.renameSync(activePath, rotated);
|
|
@@ -117,7 +125,8 @@ function create(config) {
|
|
|
117
125
|
function close() {
|
|
118
126
|
if (fd != null) {
|
|
119
127
|
try { fs.fsyncSync(fd); } catch (_e) { /* best effort */ }
|
|
120
|
-
try { fs.closeSync(fd); }
|
|
128
|
+
try { fs.closeSync(fd); }
|
|
129
|
+
catch (e) { log.warn("close-failed: " + e.message); }
|
|
121
130
|
fd = null;
|
|
122
131
|
}
|
|
123
132
|
return Promise.resolve();
|
|
@@ -27,19 +27,25 @@
|
|
|
27
27
|
*/
|
|
28
28
|
var http2 = require("node:http2");
|
|
29
29
|
var C = require("./constants");
|
|
30
|
+
var { boot } = require("./log");
|
|
30
31
|
var pb = require("./protobuf-encoder");
|
|
32
|
+
var safeAsync = require("./safe-async");
|
|
31
33
|
var safeUrl = require("./safe-url");
|
|
32
34
|
var { tearDownH2Session } = require("./http2-teardown");
|
|
33
35
|
var { LogStreamError } = require("./framework-error");
|
|
34
36
|
|
|
35
37
|
var _err = LogStreamError.factory;
|
|
38
|
+
var _log = boot("log-stream-otlp-grpc");
|
|
36
39
|
|
|
37
40
|
var DEFAULTS = {
|
|
38
41
|
scopeName: "blamejs",
|
|
39
42
|
batchSize: 100,
|
|
40
43
|
maxBatchAgeMs: C.TIME.seconds(5),
|
|
41
44
|
timeoutMs: C.TIME.seconds(30),
|
|
42
|
-
|
|
45
|
+
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
46
|
+
// identity passthrough so the file's literal arithmetic has a single
|
|
47
|
+
// source of truth.
|
|
48
|
+
bufferLimit: C.BYTES.bytes(10000),
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
var SEVERITY = {
|
|
@@ -194,7 +200,10 @@ function _frame(messageBuf) {
|
|
|
194
200
|
// connection alive across many Export calls; we re-create on
|
|
195
201
|
// disconnect.
|
|
196
202
|
function _makeClient(cfg) {
|
|
197
|
-
var url =
|
|
203
|
+
var url = safeUrl.parse(cfg.url, {
|
|
204
|
+
allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
205
|
+
errorClass: LogStreamError,
|
|
206
|
+
});
|
|
198
207
|
var authority = url.protocol + "//" + url.host;
|
|
199
208
|
var sessionOpts = {};
|
|
200
209
|
if (cfg.ca) sessionOpts.ca = cfg.ca;
|
|
@@ -227,7 +236,11 @@ function _doExport(session, cfg, records) {
|
|
|
227
236
|
|
|
228
237
|
var timer = setTimeout(function () {
|
|
229
238
|
errored = true;
|
|
230
|
-
|
|
239
|
+
// Best-effort cancel of the in-flight HTTP/2 request — close()
|
|
240
|
+
// can throw on an already-torn-down stream; the timeout error
|
|
241
|
+
// below is the authoritative signal so we swallow + log only.
|
|
242
|
+
try { req.close(http2.constants.NGHTTP2_CANCEL); }
|
|
243
|
+
catch (e) { _log.debug("otlp-grpc-timer-cancel: " + (e && e.message || e)); }
|
|
231
244
|
reject(_err("ETIMEDOUT",
|
|
232
245
|
"otlp-grpc: request timed out after " + cfg.timeoutMs + "ms"));
|
|
233
246
|
}, cfg.timeoutMs);
|
|
@@ -290,18 +303,15 @@ function create(config) {
|
|
|
290
303
|
cfg.bufferLimit = Number(cfg.bufferLimit) || DEFAULTS.bufferLimit;
|
|
291
304
|
|
|
292
305
|
var onDrop = typeof config.onDrop === "function" ? config.onDrop : null;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try { onDrop({ reason: reason, batch: batch, error: err || null }); }
|
|
296
|
-
catch (_e) {}
|
|
297
|
-
}
|
|
306
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop,
|
|
307
|
+
function (e) { _log.debug("otlp-grpc-onDrop-threw: " + (e && e.message || e)); });
|
|
298
308
|
|
|
299
309
|
var buffer = [];
|
|
300
310
|
var inFlight = false;
|
|
301
311
|
var closed = false;
|
|
302
|
-
var flushTimer = null;
|
|
303
312
|
var session = null;
|
|
304
313
|
var inflightPromise = null;
|
|
314
|
+
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
305
315
|
|
|
306
316
|
function _ensureSession() {
|
|
307
317
|
if (session && !session.destroyed) return session;
|
|
@@ -309,15 +319,6 @@ function create(config) {
|
|
|
309
319
|
return session;
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
function _scheduleFlush() {
|
|
313
|
-
if (flushTimer || closed) return;
|
|
314
|
-
flushTimer = setTimeout(function () {
|
|
315
|
-
flushTimer = null;
|
|
316
|
-
_flush().catch(function () { /* drained internally */ });
|
|
317
|
-
}, cfg.maxBatchAgeMs);
|
|
318
|
-
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
322
|
async function _flush() {
|
|
322
323
|
if (inFlight || buffer.length === 0) return;
|
|
323
324
|
inFlight = true;
|
|
@@ -353,17 +354,22 @@ function create(config) {
|
|
|
353
354
|
// Drain immediately; don't await — emit is fire-and-forget on the
|
|
354
355
|
// hot path. Errors surface via onDrop.
|
|
355
356
|
_flush().catch(function () {});
|
|
356
|
-
} else {
|
|
357
|
-
|
|
357
|
+
} else if (!closed) {
|
|
358
|
+
flushScheduler.schedule();
|
|
358
359
|
}
|
|
359
360
|
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
async function close() {
|
|
363
364
|
closed = true;
|
|
364
|
-
|
|
365
|
-
// Drain
|
|
366
|
-
|
|
365
|
+
flushScheduler.cancel();
|
|
366
|
+
// Drain the in-flight Export before tearing down the HTTP/2 session.
|
|
367
|
+
// The promise rejection (if any) was already routed through onDrop
|
|
368
|
+
// by _flush — we only await here to avoid racing the teardown.
|
|
369
|
+
if (inflightPromise) {
|
|
370
|
+
try { await inflightPromise; }
|
|
371
|
+
catch (e) { _log.debug("otlp-grpc-close-drain: " + (e && e.message || e)); }
|
|
372
|
+
}
|
|
367
373
|
var pending = buffer.splice(0, buffer.length);
|
|
368
374
|
if (pending.length > 0) {
|
|
369
375
|
try {
|
package/lib/log-stream-otlp.js
CHANGED
|
@@ -51,13 +51,16 @@
|
|
|
51
51
|
* collector which forwards.
|
|
52
52
|
*/
|
|
53
53
|
var C = require("./constants");
|
|
54
|
+
var pkg = require("../package.json");
|
|
54
55
|
var retryHelper = require("./retry");
|
|
55
56
|
var { LogStreamError } = require("./framework-error");
|
|
56
57
|
var httpClient = require("./http-client");
|
|
58
|
+
var safeAsync = require("./safe-async");
|
|
57
59
|
var safeUrl = require("./safe-url");
|
|
58
60
|
var authHeader = require("./auth-header");
|
|
59
61
|
|
|
60
62
|
var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
|
|
63
|
+
var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
|
|
61
64
|
|
|
62
65
|
// OTel Logs Data Model severity numbers.
|
|
63
66
|
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
@@ -73,7 +76,10 @@ var DEFAULTS = {
|
|
|
73
76
|
batchSize: 100,
|
|
74
77
|
maxBatchAgeMs: C.TIME.seconds(5),
|
|
75
78
|
timeoutMs: C.TIME.seconds(30),
|
|
76
|
-
|
|
79
|
+
// Ring-buffer cap (event count, not bytes); routed through C.BYTES
|
|
80
|
+
// identity passthrough so the file's literal arithmetic has a single
|
|
81
|
+
// source of truth.
|
|
82
|
+
bufferLimit: C.BYTES.bytes(10000),
|
|
77
83
|
};
|
|
78
84
|
|
|
79
85
|
var _err = LogStreamError.factory;
|
|
@@ -193,30 +199,18 @@ function create(config) {
|
|
|
193
199
|
allowedProtocols: cfg.allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
194
200
|
errorClass: LogStreamError,
|
|
195
201
|
});
|
|
196
|
-
var scopeVersion = cfg.scopeVersion ||
|
|
197
|
-
try { return require("../package.json").version; } catch (_e) { return "unknown"; }
|
|
198
|
-
})();
|
|
202
|
+
var scopeVersion = cfg.scopeVersion || FRAMEWORK_VERSION;
|
|
199
203
|
var headers = Object.assign({
|
|
200
204
|
"Content-Type": "application/json",
|
|
201
205
|
"Accept": "application/json",
|
|
202
206
|
}, _authHeaders(cfg));
|
|
203
207
|
var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
|
|
204
|
-
|
|
205
|
-
if (!onDrop) return;
|
|
206
|
-
try { onDrop({ reason: reason, batch: batch, error: err || null }); }
|
|
207
|
-
catch (_e) { /* drop callback is best-effort */ }
|
|
208
|
-
}
|
|
208
|
+
var _emitDrop = safeAsync.makeDropCallback(onDrop);
|
|
209
209
|
var buffer = [];
|
|
210
210
|
var dropCount = 0;
|
|
211
|
-
var flushTimer = null;
|
|
212
211
|
var inFlight = false;
|
|
213
212
|
var closed = false;
|
|
214
|
-
|
|
215
|
-
function _scheduleFlush() {
|
|
216
|
-
if (flushTimer) return;
|
|
217
|
-
flushTimer = setTimeout(function () { flushTimer = null; _flush(); }, cfg.maxBatchAgeMs);
|
|
218
|
-
flushTimer.unref();
|
|
219
|
-
}
|
|
213
|
+
var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
|
|
220
214
|
|
|
221
215
|
async function _flush() {
|
|
222
216
|
if (inFlight) return;
|
|
@@ -238,7 +232,7 @@ function create(config) {
|
|
|
238
232
|
}
|
|
239
233
|
} finally {
|
|
240
234
|
inFlight = false;
|
|
241
|
-
if (buffer.length > 0)
|
|
235
|
+
if (buffer.length > 0) flushScheduler.schedule();
|
|
242
236
|
}
|
|
243
237
|
}
|
|
244
238
|
|
|
@@ -253,14 +247,14 @@ function create(config) {
|
|
|
253
247
|
if (buffer.length >= cfg.batchSize) {
|
|
254
248
|
_flush().catch(function () {});
|
|
255
249
|
} else {
|
|
256
|
-
|
|
250
|
+
flushScheduler.schedule();
|
|
257
251
|
}
|
|
258
252
|
return Promise.resolve({ accepted: true, queued: buffer.length });
|
|
259
253
|
}
|
|
260
254
|
|
|
261
255
|
async function close() {
|
|
262
256
|
closed = true;
|
|
263
|
-
|
|
257
|
+
flushScheduler.cancel();
|
|
264
258
|
await _flush();
|
|
265
259
|
}
|
|
266
260
|
|