@blamejs/core 0.18.47 → 0.18.49

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 (44) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/lib/ai-model-manifest.js +2 -2
  3. package/lib/audit-sign.js +3 -3
  4. package/lib/auth/passkey.js +1 -1
  5. package/lib/backup/index.js +2 -2
  6. package/lib/client-hints.js +1 -2
  7. package/lib/codepoint-class.js +6 -5
  8. package/lib/gate-contract.js +147 -1
  9. package/lib/guard-archive.js +24 -5
  10. package/lib/guard-auth.js +28 -0
  11. package/lib/guard-cidr.js +19 -0
  12. package/lib/guard-country.js +4 -4
  13. package/lib/guard-csv.js +31 -2
  14. package/lib/guard-domain.js +22 -0
  15. package/lib/guard-email.js +17 -0
  16. package/lib/guard-filename.js +45 -11
  17. package/lib/guard-graphql.js +16 -0
  18. package/lib/guard-html.js +17 -0
  19. package/lib/guard-image.js +19 -0
  20. package/lib/guard-json.js +23 -0
  21. package/lib/guard-jsonpath.js +14 -1
  22. package/lib/guard-jwt.js +20 -2
  23. package/lib/guard-markdown.js +25 -0
  24. package/lib/guard-mime.js +15 -5
  25. package/lib/guard-oauth.js +23 -0
  26. package/lib/guard-pdf.js +19 -1
  27. package/lib/guard-regex.js +31 -15
  28. package/lib/guard-shell.js +14 -1
  29. package/lib/guard-svg.js +17 -0
  30. package/lib/guard-template.js +14 -1
  31. package/lib/guard-text.js +16 -1
  32. package/lib/guard-time.js +20 -4
  33. package/lib/guard-uuid.js +28 -4
  34. package/lib/guard-xml.js +19 -0
  35. package/lib/guard-yaml.js +22 -2
  36. package/lib/mail-auth.js +3 -6
  37. package/lib/metrics.js +4 -4
  38. package/lib/mtls-ca.js +4 -4
  39. package/lib/network-dns.js +2 -2
  40. package/lib/session-device-binding.js +2 -2
  41. package/lib/vault/index.js +2 -2
  42. package/lib/ws-client.js +2 -2
  43. package/package.json +1 -1
  44. package/sbom.cdx.json +6 -6
package/lib/guard-text.js CHANGED
@@ -118,6 +118,20 @@ var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256
118
118
  // so this guard's hand-bound resolver holds a caller to the same rules the
119
119
  // generated validate() does. maxRuntimeMs is deliberately absent: zero there
120
120
  // means no runtime budget. See guard-image for the full reasoning.
121
+ // Each policy's vocabulary, so a misspelling is a boot error rather than a
122
+ // runtime surprise. Read leniently, a typo takes whichever branch is not the
123
+ // strict one: `encodingPolicy: "rejct"` is not "allow", so the check runs, and
124
+ // it is not "reject" either, so malformed UTF-8 drops to a warning.
125
+ var POLICY_ENUM = gateContract.policyVocabulary(
126
+ ["confusablePolicy", "encodingPolicy"],
127
+ gateContract.POLICY_VALUES.rejectAuditAllow);
128
+
129
+ // The same table for the entry points that bind their own resolver, with the
130
+ // family's character policies folded in — defineGuard adds those itself, a
131
+ // hand-bound resolver has to be given them.
132
+ var RESOLVER_ENUMS = Object.freeze(Object.assign({},
133
+ gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }), POLICY_ENUM));
134
+
121
135
  var INT_OPTS = ["maxBytes"];
122
136
 
123
137
  // ---- Internal helpers ----
@@ -135,7 +149,7 @@ function _resolveOpts(opts) {
135
149
  // generated path refuses it.
136
150
  intOpts: INT_OPTS,
137
151
  nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
138
- enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
152
+ enumOpts: RESOLVER_ENUMS,
139
153
  });
140
154
  }
141
155
 
@@ -575,6 +589,7 @@ var INTEGRATION_FIXTURES = Object.freeze({
575
589
  // inspection surface (validate / sanitize / gate). The bespoke `gate` carries
576
590
  // the text sanitize-then-refuse chain (confusables are non-repairable).
577
591
  module.exports = gateContract.defineGuard({
592
+ enumOpts: POLICY_ENUM,
578
593
  name: "text",
579
594
  kind: "content",
580
595
  charRepair: true,
package/lib/guard-time.js CHANGED
@@ -299,10 +299,10 @@ function _detectIssues(input, opts) {
299
299
  * @opts
300
300
  * profile: "strict"|"balanced"|"permissive",
301
301
  * compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
302
- * bidiPolicy: "reject"|"strip"|"audit"|"allow",
303
- * controlPolicy: "reject"|"strip"|"allow",
304
- * nullBytePolicy: "reject"|"strip"|"allow",
305
- * zeroWidthPolicy: "reject"|"strip"|"allow",
302
+ * bidiPolicy: "reject"|"audit"|"allow",
303
+ * controlPolicy: "reject"|"audit"|"allow",
304
+ * nullBytePolicy: "reject"|"audit"|"allow",
305
+ * zeroWidthPolicy: "reject"|"audit"|"allow",
306
306
  * naiveDatetimePolicy: "reject"|"audit"|"allow",
307
307
  * nonUtcOffsetPolicy: "reject"|"audit"|"allow",
308
308
  * leapSecondPolicy: "reject"|"audit"|"allow",
@@ -393,7 +393,23 @@ var INTEGRATION_FIXTURES = gateContract.identifierFixtures("2026-05-05T12:34:56Z
393
393
  // surface (validate / sanitize). The gate is the factory default — the
394
394
  // standard serve -> audit-only -> refuse chain — reading
395
395
  // ctx.identifier / ctx.timestamp / ctx.time via ctxFields.
396
+ // Each policy's vocabulary, so a misspelling is a boot error rather than a
397
+ // runtime surprise. Read leniently, a typo takes whichever branch is not the
398
+ // strict one: `naiveDatetimePolicy: "rejct"` is not "allow", so the check
399
+ // runs, and it is not "reject" either, so a zone-less timestamp drops to a
400
+ // warning.
401
+ //
402
+ // `fractionalDigitsPolicy` carries "truncate" because over-precise fractional
403
+ // seconds are the one finding here that can be repaired rather than reported.
404
+ var POLICY_ENUM = gateContract.policyVocabulary([
405
+ "naiveDatetimePolicy", "nonUtcOffsetPolicy", "leapSecondPolicy",
406
+ "dateOnlyPolicy", "timeOnlyPolicy",
407
+ ], gateContract.POLICY_VALUES.rejectAuditAllow, {
408
+ fractionalDigitsPolicy: ["reject", "truncate", "audit", "audit-only", "allow"],
409
+ });
410
+
396
411
  module.exports = gateContract.defineGuard({
412
+ enumOpts: POLICY_ENUM,
397
413
  name: "time",
398
414
  kind: "identifier",
399
415
  errorClass: GuardTimeError,
package/lib/guard-uuid.js CHANGED
@@ -286,10 +286,10 @@ function _detectIssues(input, opts) {
286
286
  * @opts
287
287
  * profile: "strict"|"balanced"|"permissive",
288
288
  * compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
289
- * bidiPolicy: "reject"|"strip"|"audit"|"allow",
290
- * controlPolicy: "reject"|"strip"|"allow",
291
- * nullBytePolicy: "reject"|"strip"|"allow",
292
- * zeroWidthPolicy: "reject"|"strip"|"allow",
289
+ * bidiPolicy: "reject"|"audit"|"allow",
290
+ * controlPolicy: "reject"|"audit"|"allow",
291
+ * nullBytePolicy: "reject"|"audit"|"allow",
292
+ * zeroWidthPolicy: "reject"|"audit"|"allow",
293
293
  * formatPolicy: "hyphenated"|"hyphenless"|"braced"|"urn"|"hyphenated-only"|"any",
294
294
  * versionPolicy: "reject-unassigned"|"audit"|"allow",
295
295
  * variantPolicy: "reject-non-rfc"|"audit"|"allow",
@@ -371,10 +371,34 @@ var INTEGRATION_FIXTURES = gateContract.identifierFixtures("550e8400-e29b-41d4-a
371
371
  // surface (validate / sanitize). The gate is the factory default — the
372
372
  // standard serve -> audit-only -> refuse chain — reading ctx.identifier ||
373
373
  // ctx.uuid via ctxFields.
374
+ // Each policy's vocabulary, so a misspelling is a boot error rather than a
375
+ // runtime surprise. Read leniently, a typo takes whichever branch is not the
376
+ // strict one: `versionPolicy: "reject-unassinged"` is not "allow", so the check
377
+ // runs, and it is not the reject spelling either, so the finding drops to a
378
+ // warning — the operator asked to refuse an unassigned version and silently got
379
+ // an audit. The values are the ones written beside each profile entry above,
380
+ // confirmed against what the code compares.
381
+ //
382
+ // `audit-only` rides with `audit` because it behaves identically here and the
383
+ // framework treats them as synonyms everywhere else; refusing it here while
384
+ // accepting it elsewhere would be a fresh disagreement between two spellings of
385
+ // one setting.
386
+ var POLICY_ENUM = gateContract.policyVocabulary([
387
+ "nilPolicy", "maxPolicy", "urnPolicy", "bracedPolicy",
388
+ ], gateContract.POLICY_VALUES.rejectAuditAllow, {
389
+ // These three name their own values because their findings are not "this
390
+ // construct appeared" but "the identifier is the wrong shape", and the
391
+ // refusal has to say which shape it wanted.
392
+ formatPolicy: ["hyphenated", "hyphenless", "braced", "urn", "hyphenated-only", "any"],
393
+ versionPolicy: ["reject-unassigned", "audit", "audit-only", "allow"],
394
+ variantPolicy: ["reject-non-rfc", "audit", "audit-only", "allow"],
395
+ });
396
+
374
397
  module.exports = gateContract.defineGuard({
375
398
  name: "uuid",
376
399
  kind: "identifier",
377
400
  errorClass: GuardUuidError,
401
+ enumOpts: POLICY_ENUM,
378
402
  profiles: PROFILES,
379
403
  defaults: DEFAULTS,
380
404
  postures: COMPLIANCE_POSTURES,
package/lib/guard-xml.js CHANGED
@@ -733,7 +733,26 @@ var INTEGRATION_FIXTURES = Object.freeze({
733
733
  // ENTITY / external / parameter-entity throwing while strip-able char-class
734
734
  // threats are repaired. The bespoke `gate` carries XML's per-policy
735
735
  // canSanitize matrix unchanged.
736
+ // Each policy's vocabulary, so a misspelling is a boot error rather than a
737
+ // runtime surprise. Read leniently, a typo takes whichever branch is not the
738
+ // strict one: `externalEntityPolicy: "rejct"` is not "allow", so the check
739
+ // runs, and it is not "reject" either, so an XXE vector drops to a warning.
740
+ //
741
+ // `xmlDsigPolicy` has no "reject": a signature block is a legitimate part of a
742
+ // signed document, so the guard reports its presence and leaves verification
743
+ // to the caller rather than refusing the document for carrying one.
744
+ //
745
+ // The character policies are absent on purpose — they are derived for the
746
+ // whole guard family, and an entry here would shadow that derivation.
747
+ var POLICY_ENUM = gateContract.policyVocabulary([
748
+ "doctypePolicy", "entityPolicy", "externalEntityPolicy", "xincludePolicy",
749
+ "processingInstrPolicy", "cdataPolicy", "schemaLocationPolicy",
750
+ ], gateContract.POLICY_VALUES.rejectAuditAllow, {
751
+ xmlDsigPolicy: ["audit", "audit-only", "allow"],
752
+ });
753
+
736
754
  var _guard = module.exports = gateContract.defineGuard({
755
+ enumOpts: POLICY_ENUM,
737
756
  name: "xml",
738
757
  kind: "content",
739
758
  charRepair: true,
package/lib/guard-yaml.js CHANGED
@@ -262,6 +262,25 @@ var COMPLIANCE_POSTURES = gateContract.compliancePostures(PROFILES, { base: 256
262
262
  // The options that are genuinely CAPS, where zero is not a setting — named once
263
263
  // so this guard's hand-bound parse() and gate() hold a caller to the same rules
264
264
  // the generated validate() does. See guard-image for the full reasoning.
265
+ // Each policy's vocabulary, so a misspelling is a boot error rather than a
266
+ // runtime surprise. Read leniently, a typo takes whichever branch is not the
267
+ // strict one: `aliasPolicy: "rejct"` is not "allow", so the check runs, and it
268
+ // is not "reject" either, so a billion-laughs alias drops to a warning.
269
+ //
270
+ // `audit-only` is absent: tagPolicy tests for "audit" exactly, and a guard
271
+ // whose policies do not all take the same values is worse than one that takes
272
+ // fewer.
273
+ var POLICY_ENUM = gateContract.policyVocabulary([
274
+ "tagPolicy", "aliasPolicy", "multiDocPolicy", "norwayPolicy",
275
+ "leadingZeroPolicy", "duplicateKeyPolicy", "mergeKeyPolicy",
276
+ ], ["reject", "audit", "allow"]);
277
+
278
+ // The same table for the entry points that bind their own resolver, with the
279
+ // family's character policies folded in — defineGuard adds those itself, a
280
+ // hand-bound resolver has to be given them.
281
+ var RESOLVER_ENUMS = Object.freeze(Object.assign({},
282
+ gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }), POLICY_ENUM));
283
+
265
284
  var INT_OPTS = ["maxBytes", "maxDepth", "maxAnchors", "maxAliasDepth",
266
285
  "maxDocuments", "maxNodes", "maxScalarLength"];
267
286
 
@@ -653,7 +672,7 @@ function parse(input, opts) {
653
672
  // `bidiPolicy: "rejct"` here while the generated path refuses it.
654
673
  intOpts: INT_OPTS,
655
674
  nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
656
- enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
675
+ enumOpts: RESOLVER_ENUMS,
657
676
  });
658
677
  if (typeof input !== "string") {
659
678
  throw _err("yaml.bad-input", "parse requires string input");
@@ -805,7 +824,7 @@ function gate(opts) {
805
824
  // `bidiPolicy: "rejct"` here while the generated path refuses it.
806
825
  intOpts: INT_OPTS,
807
826
  nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
808
- enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
827
+ enumOpts: RESOLVER_ENUMS,
809
828
  });
810
829
  return gateContract.buildContentGate({
811
830
  name: opts.name || "guardYaml:" + (opts.profile || "default"),
@@ -817,6 +836,7 @@ function gate(opts) {
817
836
  }
818
837
 
819
838
  module.exports = gateContract.defineGuard({
839
+ enumOpts: POLICY_ENUM,
820
840
  name: "yaml",
821
841
  kind: "content",
822
842
  charRepair: true,
package/lib/mail-auth.js CHANGED
@@ -1227,12 +1227,9 @@ function _dmarcAuthorDomainLabels(domain) {
1227
1227
  if (!d) return null;
1228
1228
  // Both RFC 1035 §2.3.4 bounds — 63 octets per label and 253 for the whole
1229
1229
  // name — are enforced by `canonicalDomain` itself, so an over-long or empty
1230
- // label has already returned "" above. This function used to re-check the
1231
- // label bound because canonicalDomain enforced only the total; the comment
1232
- // here argued a label cap was a DNS wire rule rather than a naming one, which
1233
- // did not survive the observation that the 253 cap is equally a wire rule and
1234
- // was enforced there anyway. Fixing it at the definition means every caller
1235
- // gets it, not just this one.
1230
+ // label has already returned "" above. No re-check here: both are wire rules
1231
+ // of the same kind, and enforcing them where the name is defined gives them
1232
+ // to every caller rather than to whichever ones remembered.
1236
1233
  return d.split(".");
1237
1234
  }
1238
1235
 
package/lib/metrics.js CHANGED
@@ -1024,7 +1024,7 @@ function _serializeRegistry(registry) {
1024
1024
  // a JSON-friendly structured shape. Histograms get full buckets +
1025
1025
  // bucket counts so downstream consumers compose
1026
1026
  // `histogram_quantile()` against the snapshot without a separate
1027
- // exposition endpoint (issue #100).
1027
+ // exposition endpoint.
1028
1028
  var out = {};
1029
1029
  var names = registry.metrics instanceof Map
1030
1030
  ? Array.from(registry.metrics.keys()).sort()
@@ -1073,8 +1073,8 @@ function snapshotStartWriter(opts) {
1073
1073
  throw new MetricsError("metrics-snapshot/bad-fields",
1074
1074
  "metrics.snapshot.startWriter: opts.fields must be a function returning the snapshot object");
1075
1075
  }
1076
- // Issue #100 — optional `registry` handle pulls every registered
1077
- // metric into a structured `metrics` field in the JSON snapshot:
1076
+ // The optional `registry` handle pulls every registered metric into a
1077
+ // structured `metrics` field in the JSON snapshot:
1078
1078
  // counters / gauges as `{ value }` per label set, histograms as
1079
1079
  // `{ buckets, observations }` with bucket counts + sum + count.
1080
1080
  // Sidecar readers compose `histogram_quantile()` against the
@@ -1420,7 +1420,7 @@ function snapshotRender(snap, opts) {
1420
1420
  "metrics.snapshot.render: snap must be a startWriter-produced object (got " + typeof snap + ")");
1421
1421
  }
1422
1422
  var fields = snap.fields;
1423
- // Labeled registry families (issue #430) — a snapshot written with
1423
+ // Labeled registry families — a snapshot written with
1424
1424
  // startWriter's `registry` option carries every registered counter /
1425
1425
  // gauge / histogram under `metrics`. Both formats render them so a
1426
1426
  // sidecar consuming a snapshot written by another process gets the
package/lib/mtls-ca.js CHANGED
@@ -25,9 +25,9 @@
25
25
  * `ca.crl` (signed CRL derived from the registry).
26
26
  *
27
27
  * `caKeySealedMode` defaults to "required" — sealed file required,
28
- * plaintext refused. The legacy "auto" fallback was removed; it
29
- * defaulted to writing plaintext on a fresh install, which is the
30
- * inverse of the framework's security-defaults-on posture for
28
+ * plaintext refused. There is no "auto" mode, because deciding
29
+ * for the operator means writing plaintext on a fresh install,
30
+ * which is the inverse of the framework's security-defaults-on posture for
31
31
  * at-rest key material. The "disabled" mode is a dev-only opt-out
32
32
  * (operator must justify with audited reason).
33
33
  *
@@ -2554,7 +2554,7 @@ function create(opts) {
2554
2554
  path: paths.crl };
2555
2555
  }
2556
2556
 
2557
- // ---- Algorithm migration (issue #532) ----
2557
+ // ---- Algorithm migration ----
2558
2558
 
2559
2559
  // Serialize rotations on this handle. Two concurrent rotate() calls must not
2560
2560
  // both read the same current generation, both mint the next one, and clobber
@@ -535,8 +535,8 @@ function _dnsQueryLabels(host, primitive) {
535
535
  // A delimiter is the one that bites, because `domainToASCII` TRUNCATES at
536
536
  // one, so `example.com/evil` can masquerade as a trusted prefix of itself.
537
537
  //
538
- // Mirroring the rule was tried first, and the list of near-misses above is
539
- // what that produced. Asking the owner is the version that cannot drift.
538
+ // Mirroring the rule locally produces the list of near-misses above. Asking
539
+ // the owner is the version that cannot drift.
540
540
  var canonical = publicSuffix.canonicalDomain(h);
541
541
  if (!canonical) {
542
542
  throw new DnsError("dns/bad-host",
@@ -301,8 +301,8 @@ function create(opts) {
301
301
  // A no-store instance is still useful: the stateless fingerprint() reads no
302
302
  // store and is the soft device-binding building block for self-validating
303
303
  // tokens (a sealed cookie / JWT carrying the fingerprint inside). Rather than
304
- // refuse to construct (issue #330 fingerprint() unreachable without a
305
- // store), build the instance and let the persisted bind()/verify() lifecycle
304
+ // refuse to construct, which would put fingerprint() out of reach for want of
305
+ // a store, build the instance and let the persisted bind()/verify() lifecycle
306
306
  // throw a clear "no store configured" when actually called. Operators wanting
307
307
  // ONLY the stateless digest can also use the static
308
308
  // b.sessionDeviceBinding.fingerprint(req, opts) with no create() at all.
@@ -472,8 +472,8 @@ async function initFirstRunWrapped() {
472
472
  "failed to wrap new vault key: " + e.message);
473
473
  }
474
474
 
475
- // Atomic write via the framework's atomic-file primitive (temp + fsync +
476
- // rename + dir fsync — same flow this code used to inline manually).
475
+ // Atomic write via the framework's atomic-file primitive: temp + fsync +
476
+ // rename + dir fsync.
477
477
  atomicFile.writeSync(paths.sealed, sealed, { fileMode: 0o600 });
478
478
 
479
479
  log("generated and sealed new vault keypair (ML-KEM-1024 + P-384 hybrid)");
package/lib/ws-client.js CHANGED
@@ -453,8 +453,8 @@ class WsClient extends EventEmitter {
453
453
  if (lookup) tlsOpts.lookup = lookup;
454
454
  // The group preference arrives with the shared posture above, already
455
455
  // reflecting a runtime setKeyShares(). It lands as `ecdhCurve`: node:tls
456
- // has no `curves` option — it accepts that key and ignores it, which is
457
- // how this preference used to be dropped from the handshake in silence,
456
+ // has no `curves` option — it accepts that key and ignores it, so a
457
+ // preference sent under that name leaves the handshake in silence,
458
458
  // whereas a bad `ecdhCurve` throws. An operator value in dialTlsOpts
459
459
  // still wins, since it is merged last.
460
460
  socket = tls.connect(tlsOpts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.47",
3
+ "version": "0.18.49",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:2b85824f-8c87-4b4e-9ee7-abfe05e1b852",
5
+ "serialNumber": "urn:uuid:a6344adf-71aa-4268-ba0f-0073a6ead16a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-22T12:32:06.262Z",
8
+ "timestamp": "2026-08-22T18:41:08.842Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.18.47",
22
+ "bom-ref": "@blamejs/core@0.18.49",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.47",
25
+ "version": "0.18.49",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.18.47",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.49",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.18.47",
57
+ "ref": "@blamejs/core@0.18.49",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]