@blamejs/pki 0.5.5 → 0.5.7

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/lib/tsp-sign.js CHANGED
@@ -27,18 +27,40 @@ var guard = require("./guard-all");
27
27
  var frameworkError = require("./framework-error");
28
28
 
29
29
  var pkix = require("./schema-pkix");
30
+ var pkiBuild = require("./pki-build");
30
31
  var TspError = frameworkError.TspError;
31
32
  var _NS = pkix.makeNS("tsp", TspError, oid);
32
33
  var b = asn1.build;
33
34
  function _err(code, message, cause) { return new TspError(code, message, cause); }
34
35
  function O(name) { return oid.byName(name); }
35
36
 
37
+ // The shared authoring-side builder, for the same pre-encoded-Extension validation every other
38
+ // request builder uses (a CSR's extensionRequest, a CRMF CertTemplate, a CRL's entry extensions).
39
+ var _b = pkiBuild.makeBuilder({
40
+ ErrorClass: TspError, prefix: "tsp", O: O, NS: _NS,
41
+ NAME_SCHEMA: pkix.name(_NS), SPKI_SCHEMA: pkix.spki(_NS),
42
+ });
43
+
36
44
  // Digest names whose imprint / certHash this producer supports (the SHA-2 family).
37
45
  var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
38
46
  // The message-imprint hash MUST be exactly the digest algorithm's output length (RFC 3161 sec.
39
47
  // 2.4.1 -- hashedMessage is "the hash of the datum to be time-stamped").
40
48
  var HASH_LEN = { sha256: 32, sha384: 48, sha512: 64 };
41
49
 
50
+ // How much a `pki.path.validate` revocationChecked outcome ESTABLISHED, ordered. `false` is "no
51
+ // checker ran at all"; "undetermined" is "one ran and could not answer"; "waived" is an
52
+ // undetermined one a caller chose to pass; "determined" is an explicit good-or-revoked answer.
53
+ // Ranking them lets a caller trying several candidate paths keep the most established outcome any
54
+ // of them reached, rather than whichever happened to run last.
55
+ var _REVOCATION_RANK = { "false": 0, "undetermined": 1, "waived": 2, "determined": 3 };
56
+ function _rankRevocation(v) {
57
+ var r = _REVOCATION_RANK[String(v)];
58
+ return r === undefined ? 0 : r; // an unrecognized value establishes nothing
59
+ }
60
+ function _moreEstablished(candidate, current) {
61
+ return _rankRevocation(candidate) > _rankRevocation(current);
62
+ }
63
+
42
64
  // A hash AlgorithmIdentifier { OID, NULL } -- messageImprint and ESSCertIDv2 hash algorithms
43
65
  // carry an explicit NULL parameter (the form RFC 3161 / RFC 5035 producers emit).
44
66
  function _hashAlgId(name) {
@@ -109,7 +131,17 @@ function _signingCertV2(certDer, hashName) {
109
131
  * var token = await pki.tsp.sign(imprint, { cert: signerCertDer, key: signerKeyPkcs8 }, { policy: "1.3.6.1.4.1.1", serialNumber: 1 });
110
132
  * (await pki.cms.verify(token)).valid; // true
111
133
  */
134
+ // Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async); the checks stay
135
+ // synchronous because they read the caller's mutable imprint, TSA material and options.
112
136
  function sign(messageImprint, tsa, opts) {
137
+ // Every caller-owned argument copied at entry and released when the call settles -- see the note
138
+ // on the same call in x509-sign. The imprint matters most: it is the digest the token attests to.
139
+ return guard.bytes.fixedCall(TspError, "tsp/bad-input", [
140
+ [messageImprint, "the messageImprint"], [tsa, "the TSA"], [opts, "pki.tsp.sign options"],
141
+ ], _sign);
142
+ }
143
+
144
+ function _sign(messageImprint, tsa, opts) {
113
145
  opts = opts || {};
114
146
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
115
147
  var mi = messageImprint || {};
@@ -130,10 +162,14 @@ function sign(messageImprint, tsa, opts) {
130
162
  // non-Date or an Invalid Date that would encode a garbage GeneralizedTime).
131
163
  if (opts.genTime != null) guard.time.assertValid(opts.genTime, _err, "tsp/bad-input", "genTime");
132
164
  var genTime = opts.genTime instanceof Date ? opts.genTime : new Date();
133
- var fields = [b.integer(1n), _policy(opts.policy), imprint, b.integer(BigInt(opts.serialNumber)), b.generalizedTime(genTime)];
165
+ // A caller-authored INTEGER is coerced through the shared guard, so a value that is not one is a
166
+ // typed tsp/bad-input rather than a raw SyntaxError or RangeError out of BigInt() -- an untyped
167
+ // fault escaping a public verb, which a caller cannot catch by code.
168
+ var serial = guard.range.authoredInteger(opts.serialNumber, _err, "tsp/bad-input", "serialNumber");
169
+ var fields = [b.integer(1n), _policy(opts.policy), imprint, b.integer(serial), b.generalizedTime(genTime)];
134
170
  if (opts.accuracy) fields.push(_accuracy(opts.accuracy));
135
171
  if (opts.ordering === true) fields.push(b.boolean(true));
136
- if (opts.nonce != null) fields.push(b.integer(BigInt(opts.nonce)));
172
+ if (opts.nonce != null) fields.push(b.integer(guard.range.authoredInteger(opts.nonce, _err, "tsp/bad-input", "nonce")));
137
173
  var tstInfo = b.sequence(fields);
138
174
 
139
175
  var signCert = { type: "signingCertificateV2", values: [_signingCertV2(certDer, certHashAlg)] };
@@ -237,12 +273,26 @@ function request(messageImprint, opts) {
237
273
  // extensions in schema order. certReq DEFAULT FALSE -> only an explicit TRUE is encoded (DER).
238
274
  var fields = [b.integer(1n), imprint];
239
275
  if (opts.reqPolicy != null) fields.push(_policy(opts.reqPolicy));
240
- if (opts.nonce != null) fields.push(b.integer(BigInt(opts.nonce)));
276
+ if (opts.nonce != null) fields.push(b.integer(guard.range.authoredInteger(opts.nonce, _err, "tsp/bad-input", "nonce")));
241
277
  if (opts.certReq != null && typeof opts.certReq !== "boolean") throw _err("tsp/bad-input", "certReq must be a boolean");
242
278
  if (opts.certReq === true) fields.push(b.boolean(true));
243
279
  if (opts.extensions != null) {
244
280
  if (!Array.isArray(opts.extensions) || !opts.extensions.every(function (e) { return Buffer.isBuffer(e) || e instanceof Uint8Array; })) throw _err("tsp/bad-input", "extensions must be an array of encoded Extension DER buffers");
245
- if (opts.extensions.length) fields.push(b.contextConstructed(0, Buffer.concat(opts.extensions.map(function (e) { return Buffer.from(e); }))));
281
+ // Each pre-encoded Extension is validated as one, and its extnID must not repeat -- the same
282
+ // gate every other request builder applies. Spliced in unchecked, a caller relaying a blob it
283
+ // did not author put fully chosen bytes inside [0] and this encoder emitted DER its own
284
+ // parseRequest refuses: an undecodable value, a duplicate extnID, an explicit critical=FALSE
285
+ // the DER DEFAULT rule forbids.
286
+ var seenExt = {};
287
+ var encoded = opts.extensions.map(function (e, i) {
288
+ var der = Buffer.from(e);
289
+ _b.assertValidExtension(der, i);
290
+ var extnId = asn1.read.oid(asn1.decode(der).children[0]);
291
+ if (seenExt[extnId]) throw _err("tsp/bad-input", "duplicate request extension " + extnId + " (RFC 5280 sec. 4.2)");
292
+ seenExt[extnId] = true;
293
+ return der;
294
+ });
295
+ if (encoded.length) fields.push(b.contextConstructed(0, Buffer.concat(encoded)));
246
296
  }
247
297
  var der = b.sequence(fields);
248
298
  return opts.pem ? schemaTsp.pemEncode(der, "TIMESTAMP REQUEST") : der;
@@ -564,7 +614,8 @@ function _buildTsaChains(leaf, pool) {
564
614
  * PEM (never a parsed object -- every checked field is read from the CMS-verified eContent, so a
565
615
  * mutated parsed structure cannot desynchronize the checks from the signed bytes). `data` is the
566
616
  * original bytes (hashed under the token's messageImprint algorithm) or a precomputed
567
- * `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, trusted, genTime, accuracy, serialNumber,
617
+ * `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, trusted, revocationChecked,
618
+ * anchorConstraints, genTime, accuracy, serialNumber,
568
619
  * serialNumberHex, policy, nonce, tsa, tstInfo, signer, code?, reason? }`. `valid` is true only
569
620
  * when the CMS signature, the imprint match, the eContentType, the ESSCertID(V2) binding, the
570
621
  * RFC 3161 sec. 2.3 critical single-`timeStamping` extendedKeyUsage rule, the requested nonce (when
@@ -579,11 +630,19 @@ function _buildTsaChains(leaf, pool) {
579
630
  * accepting one. A timestamp is archived precisely to be re-read years later, and one boolean
580
631
  * cannot answer both questions then.
581
632
  *
633
+ * `revocationChecked` is the third claim, for the same reason. Revocation runs only when a
634
+ * `revocationChecker` is supplied, so a `trusted` token whose TSA was never checked against a CRL
635
+ * or an OCSP responder reads identically to one established un-revoked -- unless the verdict says
636
+ * which. It is `false` whenever no path ran at all. `anchorConstraints` carries whatever the anchor
637
+ * itself constrained, from `pki.path.validate`.
638
+ *
582
639
  * @opts trustAnchor Anchor `{ name, publicKey, algorithm }` -- runs `pki.path.validate` on the
583
640
  * TSA certificate chain ordered from the token's embedded certificates
584
- * (validity at genTime, requiredEku timeStamping, revocation), so a TSA under
585
- * an intermediate CA validates, not only one directly under the anchor. Omit
586
- * to verify signature + imprint + binding + EKU only and anchor the cert yourself.
641
+ * (validity at genTime, requiredEku timeStamping, and revocation when a
642
+ * `revocationChecker` is supplied -- `revocationChecked` reports which), so a
643
+ * TSA under an intermediate CA validates, not only one directly under the
644
+ * anchor. Omit to verify signature + imprint + binding + EKU only and anchor
645
+ * the cert yourself.
587
646
  * @opts nonce Require the token's TSTInfo.nonce to equal this (a number/BigInt).
588
647
  * @opts reqPolicy Require the token's policy to equal this (an OID name or dotted string).
589
648
  * @opts certs Out-of-band TSA certificates (an array of DER `Buffer`s) added to the signer
@@ -630,8 +689,15 @@ async function verify(token, data, opts) {
630
689
  // `res.trusted` must get an answer on both branches -- an undefined on the failure path is the
631
690
  // same "cannot tell what was checked" the field was added to remove, and `!res.trusted` reading
632
691
  // true by accident is not the same as its reading true because nothing anchored the TSA.
692
+ // What a path established travels with the REFUSAL as well. Hardcoding these to false/null here
693
+ // would misreport the one case the field matters most for: a path that ran, established the TSA
694
+ // REVOKED, and refused on that basis did check revocation, and saying it did not is the same
695
+ // "cannot tell what was checked" the field was added to remove. They stay false/null until a path
696
+ // actually produces them, which is the honest answer before one runs.
697
+ var revocationChecked = false;
698
+ var anchorConstraints = null;
633
699
  function fail(code, reason) {
634
- return { valid: false, trusted: false, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
700
+ return { valid: false, trusted: false, revocationChecked: revocationChecked, anchorConstraints: anchorConstraints, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
635
701
  }
636
702
  // M12 -- the CMS signature over the exact RFC 5652 sec. 5.4 preimage (message-digest bound to the
637
703
  // authenticated eContent). cms.verify re-parses the same bytes; a failure is a fail-closed verdict.
@@ -647,7 +713,7 @@ async function verify(token, data, opts) {
647
713
  if (mi !== true) return fail(mi);
648
714
  // M14 -- if a request nonce is supplied, the token MUST echo it (BigInt-exact).
649
715
  if (opts.nonce != null) {
650
- var wantNonce = BigInt(opts.nonce);
716
+ var wantNonce = guard.range.authoredInteger(opts.nonce, _err, "tsp/bad-input", "opts.nonce");
651
717
  if (tst.nonce == null || tst.nonce !== wantNonce) return fail("tsp/nonce-mismatch");
652
718
  }
653
719
  // M15 -- if the requested policy is supplied, the token's policy MUST equal it.
@@ -713,23 +779,54 @@ async function verify(token, data, opts) {
713
779
  // end of the chain and not the other, and a root explicitly distrusted for timestamping would
714
780
  // still answer trusted. The purpose is not a caller choice here: this verb validates timestamp
715
781
  // tokens and nothing else, so there is exactly one purpose its anchors can be judged under.
782
+ // EVERY path result goes through this one function, so none can be produced without being
783
+ // accumulated. The alternative -- validating in several places and accumulating afterwards --
784
+ // is what let a result be overwritten before it was counted, first across candidate chains and
785
+ // then across the two endpoints of one candidate. There is now one door and it always counts.
786
+ //
787
+ // The two fields accumulate INDEPENDENTLY because they are independent facts. Whether
788
+ // revocation was established and which anchor constraints were consulted do not imply each
789
+ // other: an anchor that rejects the TSA on its own purposes or distrustAfter metadata, with no
790
+ // revocationChecker configured, establishes constraints while establishing nothing about
791
+ // revocation.
792
+ async function validateAt(chain, when) {
793
+ var res = await pathValidate.validate(chain, {
794
+ time: when, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
795
+ });
796
+ // The most any attempt ESTABLISHED, kept across every attempt: backtracking tries several
797
+ // chains for one TSA certificate and a fractional genTime validates each at two endpoints,
798
+ // so an attempt that fails earlier than another must not erase what that other established.
799
+ if (_moreEstablished(res.revocationChecked, revocationChecked)) revocationChecked = res.revocationChecked;
800
+ // Every attempt validates against the SAME opts.trustAnchor under the same purpose, so any
801
+ // non-null value describes that one anchor; the first to report them is as good as the last.
802
+ if (anchorConstraints == null && res.anchorConstraints != null) anchorConstraints = res.anchorConstraints;
803
+ return res;
804
+ }
716
805
  var chains = _buildTsaChains(pkiX509.parse(tsaCertDer), pool);
717
806
  for (var ci = 0; ci < chains.length && !(pathRes && pathRes.valid); ci++) {
718
- pathRes = await pathValidate.validate(chains[ci], {
719
- time: floorT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
720
- });
721
- if (pathRes.valid && ceilT !== floorT) {
722
- pathRes = await pathValidate.validate(chains[ci], {
723
- time: ceilT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
724
- });
725
- }
807
+ pathRes = await validateAt(chains[ci], floorT);
808
+ if (pathRes.valid && ceilT !== floorT) pathRes = await validateAt(chains[ci], ceilT);
726
809
  }
727
810
  } catch (e) { return fail("tsp/untrusted-tsa", (e && e.message) || String(e)); }
811
+ // On a REFUSAL the question is whether ANYTHING established the TSA's revocation status, so the
812
+ // accumulated best answers it: a candidate that reached the checker and was rejected as revoked
813
+ // did check revocation, and saying otherwise would misdescribe the reason for the refusal.
728
814
  if (!pathRes || !pathRes.valid) return fail("tsp/untrusted-tsa", "the TSA certificate did not validate to the trust anchor at genTime");
815
+ // On ACCEPTANCE the question is narrower and the accumulated best would OVERCLAIM: what matters
816
+ // is what was checked on the path actually accepted, not what some rejected candidate managed.
817
+ // The loop stops at the first valid path, so `pathRes` is that path.
818
+ revocationChecked = pathRes.revocationChecked;
819
+ anchorConstraints = pathRes.anchorConstraints;
820
+ // Reduced to a bare `trusted: true`, this answered the same way whether the TSA was established
821
+ // un-revoked or revocation was never consulted at all -- and revocation only runs when a
822
+ // revocationChecker is supplied, so the second case is the DEFAULT. A caller archiving a
823
+ // timestamp verdict could not tell the two apart later, which is the distinction the
824
+ // valid/trusted split exists to keep.
729
825
  trusted = true;
730
826
  }
731
827
  return {
732
- valid: true, trusted: trusted, genTime: tst.genTime, accuracy: tst.accuracy,
828
+ valid: true, trusted: trusted, revocationChecked: revocationChecked, anchorConstraints: anchorConstraints,
829
+ genTime: tst.genTime, accuracy: tst.accuracy,
733
830
  serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex,
734
831
  policy: tst.policy, policyName: tst.policyName, nonce: tst.nonce, tsa: tst.tsa,
735
832
  tstInfo: tst, signer: { cert: tsaCertDer, sid: signer.sid },
@@ -225,7 +225,7 @@ function normalizeObjectAttributePolicy(policy, E, code) {
225
225
  // canonical round-trip -- the same three checks written here by hand, and now written
226
226
  // once. It also decodes, so the hex path cannot validate one string and decode another.
227
227
  allow = ap.allow.map(function (entry, i) {
228
- if (Buffer.isBuffer(entry)) return entry;
228
+ if (Buffer.isBuffer(entry)) return guard.bytes.snapshot(entry, E, code, "opts.tpmPolicy.authPolicy.allow[" + i + "]");
229
229
  var label = "opts.tpmPolicy.authPolicy.allow[" + i + "]";
230
230
  if (typeof entry !== "string" || entry.length === 0) {
231
231
  throw new E(code, label + " must be a Buffer or an even-length hex string");
@@ -233,7 +233,7 @@ function _asCert(v, label) {
233
233
  // with this toolkit: which metadata authority to trust is the operator's choice, and a verifier
234
234
  // that bundled its own would be deciding trust on the caller's behalf.
235
235
  function verifyMetadataBlob(blob, opts) {
236
- return Promise.resolve().then(function () { return _verifyMetadataBlob(blob, opts); });
236
+ return guard.async.deferred(function () { return _verifyMetadataBlob(blob, opts); });
237
237
  }
238
238
 
239
239
  function _verifyMetadataBlob(blob, opts) {
package/lib/webauthn.js CHANGED
@@ -531,7 +531,7 @@ var _FORMAT_SCOPED_BOOLEAN_OPTS = ["verifySafetyNetJws", "requireCtsProfileMatch
531
531
  // object cannot recurse without end.
532
532
  function _cloneParsed(v, depth) {
533
533
  if (depth > 64) throw _err("webauthn/bad-input", "opts.rootCertificates[] is nested too deeply to be a parsed certificate");
534
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v);
534
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, _err, "webauthn/bad-input", "a byte field of opts.rootCertificates[]");
535
535
  if (Array.isArray(v)) return v.map(function (x) { return _cloneParsed(x, depth + 1); });
536
536
  if (v instanceof Date) return new Date(v.getTime());
537
537
  if (v && typeof v === "object") {
package/lib/x509-sign.js CHANGED
@@ -78,7 +78,7 @@ function _skiValueOf(caCert) {
78
78
  return null;
79
79
  }
80
80
  function _akiKeyId(val, ctx) {
81
- if (Buffer.isBuffer(val)) return val;
81
+ if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, CertificateError, "x509/bad-input", "the authorityKeyIdentifier keyIdentifier");
82
82
  if (val === true) {
83
83
  if (ctx.issuerCert) { var ski = _skiValueOf(ctx.issuerCert); if (ski) return ski; }
84
84
  return _spkiKeyId(ctx.issuerSpki);
@@ -265,7 +265,16 @@ function _hasCriticalSan(extSpec) {
265
265
  * pki.schema.x509.parse(root).subject.dn; // "CN=Example Root CA"
266
266
  */
267
267
  function sign(spec, issuer, opts) {
268
- return Promise.resolve().then(function () { return _sign(spec, issuer, opts); });
268
+ // EVERY caller-owned argument is copied before a field of any of them is read, and every copy is
269
+ // cleared when the call settles. The checks below run NOW, and the values they approved are
270
+ // encoded, signed and emitted several promise turns afterwards, while the caller still owns all
271
+ // three objects -- so a spec, an option or a signer read again after the first turn need not be
272
+ // the one that passed. Copying only some of them leaves the rest reachable; copying without the
273
+ // release leaves a duplicate of whatever secret was nested in them. guard.bytes.fixArguments is
274
+ // both halves, and is what every producing verb in the toolkit opens with.
275
+ return guard.bytes.fixedCall(CertificateError, "x509/bad-input", [
276
+ [spec, "the certificate spec"], [issuer, "the issuer"], [opts, "pki.x509.sign options"],
277
+ ], _sign);
269
278
  }
270
279
 
271
280
  function _sign(spec, issuer, opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
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:e4afed62-75b0-4b79-b65c-4d16c17ee70b",
5
+ "serialNumber": "urn:uuid:a8c014a2-3631-443a-b33d-f10501d992fb",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-15T23:54:15.184Z",
8
+ "timestamp": "2026-08-16T09:48:05.398Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.5.5",
22
+ "bom-ref": "@blamejs/pki@0.5.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.5",
25
+ "version": "0.5.7",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.5.5",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.5.7",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.5.5",
57
+ "ref": "@blamejs/pki@0.5.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]