openbriefing 0.1.1 → 0.3.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/dist/obs.js CHANGED
@@ -15,6 +15,9 @@ export const SIZES = {
15
15
  keep: 40,
16
16
  attribution: 12,
17
17
  keepMax: 3,
18
+ highlight: 60,
19
+ highlightLabel: 6,
20
+ highlightMax: 5,
18
21
  tiers: {
19
22
  short: { target: [40, 80] },
20
23
  medium: { target: [200, 350] },
@@ -36,6 +39,7 @@ export function countWords(text) {
36
39
  const t = text.trim();
37
40
  return t === "" ? 0 : t.split(/\s+/u).length;
38
41
  }
42
+ const newBag = () => ({ props: new Map(), highlights: [] });
39
43
  const KNOWN = new Set([
40
44
  "obs:modified",
41
45
  "obs:summary",
@@ -45,6 +49,8 @@ const KNOWN = new Set([
45
49
  "obs:title",
46
50
  "obs:headline",
47
51
  "obs:keep",
52
+ "obs:highlight",
53
+ "obs:highlight:label",
48
54
  "obs:derive",
49
55
  "obs:derive:translate",
50
56
  "obs:derive:combine",
@@ -55,14 +61,33 @@ const KNOWN = new Set([
55
61
  "obs:version",
56
62
  ]);
57
63
  function put(bag, key, value) {
58
- const list = bag.get(key);
64
+ const list = bag.props.get(key);
59
65
  if (list)
60
66
  list.push(value);
61
67
  else
62
- bag.set(key, [value]);
68
+ bag.props.set(key, [value]);
63
69
  }
70
+ /** Files one property, routing highlights and their labels to the ordered list (§3.1). */
71
+ function collect(bag, key, value, warnings) {
72
+ if (key === "obs:highlight") {
73
+ bag.highlights.push({ text: value });
74
+ }
75
+ else if (key === "obs:highlight:label") {
76
+ const last = bag.highlights[bag.highlights.length - 1];
77
+ if (last === undefined)
78
+ warnings.push("obs:highlight:label before any obs:highlight; it is ignored (§3.1)");
79
+ else if (last.label === undefined)
80
+ last.label = value;
81
+ else
82
+ warnings.push(`obs:highlight #${bag.highlights.length} has two labels; the first is used (§3.1)`);
83
+ }
84
+ else {
85
+ put(bag, key, value);
86
+ }
87
+ }
88
+ const isEmpty = (bag) => bag.props.size === 0 && bag.highlights.length === 0;
64
89
  function first(bag, key, warnings) {
65
- const list = bag.get(key);
90
+ const list = bag.props.get(key);
66
91
  if (!list || list.length === 0)
67
92
  return undefined;
68
93
  if (list.length > 1 && key !== "obs:keep") {
@@ -77,14 +102,14 @@ function cleanText(key, raw, warnings) {
77
102
  let text = raw;
78
103
  if (TAG_RE.test(text)) {
79
104
  text = text.replace(TAG_RE, "");
80
- warnings.push(`${key}: HTML removed; values are plain text (§8)`);
105
+ warnings.push(`${key}: HTML removed; values are plain text (§9)`);
81
106
  }
82
107
  TAG_RE.lastIndex = 0;
83
108
  text = text.replace(/\r\n?/gu, "\n").trim();
84
109
  if (text === "")
85
110
  return undefined;
86
111
  if (URL_RE.test(text))
87
- warnings.push(`${key}: contains a URL; summaries should not (§8)`);
112
+ warnings.push(`${key}: contains a URL; summaries should not (§9)`);
88
113
  return text;
89
114
  }
90
115
  function enumValue(key, raw, allowed, fallback, warnings) {
@@ -102,7 +127,7 @@ function validModified(v) {
102
127
  return ISO_RE.test(v) && !Number.isNaN(Date.parse(v));
103
128
  }
104
129
  const TRACKING = /^(?:utm_.*|fbclid|gclid)$/iu;
105
- /** URL comparison of §11: scheme and host case, trailing slash, tracking parameters. */
130
+ /** URL comparison of §12: scheme and host case, trailing slash, tracking parameters. */
106
131
  export function normalizeUrl(input, base) {
107
132
  let u;
108
133
  try {
@@ -130,11 +155,13 @@ function sizeWarning(key, words, max, warnings) {
130
155
  function build(bag, ctx, extraWarnings = []) {
131
156
  const errors = [];
132
157
  const warnings = [...extraWarnings];
133
- for (const key of bag.keys()) {
158
+ for (const key of bag.props.keys()) {
134
159
  if (KNOWN.has(key))
135
160
  continue;
136
161
  if (key.startsWith("obs:audio:") || key === "obs:audio")
137
- continue; // reserved, §14
162
+ continue; // reserved, §15
163
+ if (key === "obs:highlight:start" || key === "obs:highlight:end")
164
+ continue; // reserved, §15
138
165
  warnings.push(`${key} is not an OBS v1 property and is ignored (§3.1)`);
139
166
  }
140
167
  // Text values, cleaned once.
@@ -156,13 +183,22 @@ function build(bag, ctx, extraWarnings = []) {
156
183
  warnings.push(`obs:version ${n} is newer than this reader (v1); unknown tags are ignored`);
157
184
  }
158
185
  }
159
- // obs:modified (required)
160
- const modified = first(bag, "obs:modified", warnings)?.trim();
161
- if (modified === undefined || modified === "") {
162
- errors.push("obs:modified is missing; it is required (§3)");
186
+ // The effective modified time (§13): obs:modified, else the carrier's own date, else none.
187
+ let modified;
188
+ let modifiedFrom = "none";
189
+ const rawModified = first(bag, "obs:modified", warnings)?.trim();
190
+ if (rawModified !== undefined && rawModified !== "") {
191
+ if (validModified(rawModified)) {
192
+ modified = rawModified;
193
+ modifiedFrom = "obs";
194
+ }
195
+ else {
196
+ warnings.push(`obs:modified: "${rawModified}" is not an ISO 8601 date and time; it is ignored (§13)`);
197
+ }
163
198
  }
164
- else if (!validModified(modified)) {
165
- errors.push(`obs:modified: "${modified}" is not an ISO 8601 date and time (§3)`);
199
+ if (modified === undefined && ctx.alt !== undefined && validModified(ctx.alt.value)) {
200
+ modified = ctx.alt.value;
201
+ modifiedFrom = ctx.alt.from;
166
202
  }
167
203
  // Summaries (§4)
168
204
  const tiered = new Map();
@@ -199,7 +235,7 @@ function build(bag, ctx, extraWarnings = []) {
199
235
  if (headline !== undefined)
200
236
  sizeWarning("obs:headline", countWords(headline), SIZES.headline, warnings);
201
237
  // Keep lines (§6)
202
- const keepAll = (bag.get("obs:keep") ?? [])
238
+ const keepAll = (bag.props.get("obs:keep") ?? [])
203
239
  .map((raw) => cleanText("obs:keep", raw, warnings))
204
240
  .filter((v) => v !== undefined);
205
241
  if (keepAll.length > SIZES.keepMax) {
@@ -207,6 +243,31 @@ function build(bag, ctx, extraWarnings = []) {
207
243
  }
208
244
  const keep = keepAll.slice(0, SIZES.keepMax);
209
245
  keep.forEach((k, i) => sizeWarning(`obs:keep #${i + 1}`, countWords(k), SIZES.keep, warnings));
246
+ // Highlights (§7)
247
+ const highlightsAll = [];
248
+ for (const raw of bag.highlights) {
249
+ const t = cleanText("obs:highlight", raw.text, warnings);
250
+ if (t === undefined)
251
+ continue;
252
+ const h = { text: t, words: countWords(t) };
253
+ if (raw.label !== undefined) {
254
+ const label = cleanText("obs:highlight:label", raw.label, warnings);
255
+ if (label !== undefined)
256
+ h.label = label;
257
+ }
258
+ if (typeof raw.inContent === "boolean")
259
+ h.inContent = raw.inContent;
260
+ highlightsAll.push(h);
261
+ }
262
+ if (highlightsAll.length > SIZES.highlightMax) {
263
+ warnings.push(`obs:highlight appears ${highlightsAll.length} times; only the first ${SIZES.highlightMax} are used (§3)`);
264
+ }
265
+ const highlights = highlightsAll.slice(0, SIZES.highlightMax);
266
+ highlights.forEach((h, i) => {
267
+ sizeWarning(`obs:highlight #${i + 1}`, h.words, SIZES.highlight, warnings);
268
+ if (h.label !== undefined)
269
+ sizeWarning(`obs:highlight:label #${i + 1}`, countWords(h.label), SIZES.highlightLabel, warnings);
270
+ });
210
271
  // Derivation (§5)
211
272
  const mode = enumValue("obs:derive", first(bag, "obs:derive", warnings), ["verbatim", "anchor", "open"], "anchor", warnings);
212
273
  const translate = enumValue("obs:derive:translate", first(bag, "obs:derive:translate", warnings), ["yes", "no"], "yes", warnings) === "yes";
@@ -216,7 +277,7 @@ function build(bag, ctx, extraWarnings = []) {
216
277
  sizeWarning("obs:attribution", countWords(attribution), SIZES.attribution, warnings);
217
278
  const preference = enumValue("obs:preference", first(bag, "obs:preference", warnings), ["summary", "full"], "summary", warnings);
218
279
  const lang = first(bag, "obs:lang", warnings)?.trim() || ctx.lang || undefined;
219
- // Identity (§11)
280
+ // Identity (§12)
220
281
  let url;
221
282
  let describesThisPage = true;
222
283
  const rawUrl = first(bag, "obs:url", warnings)?.trim();
@@ -231,10 +292,12 @@ function build(bag, ctx, extraWarnings = []) {
231
292
  }
232
293
  const brief = {
233
294
  version,
234
- modified: modified,
295
+ ...(modified !== undefined ? { modified } : {}),
296
+ modifiedFrom,
235
297
  describesThisPage,
236
298
  summaries,
237
299
  keep,
300
+ highlights,
238
301
  derive: { mode, translate, combine },
239
302
  preference,
240
303
  };
@@ -250,6 +313,34 @@ function build(bag, ctx, extraWarnings = []) {
250
313
  brief.lang = lang;
251
314
  return { brief, errors, warnings };
252
315
  }
316
+ /**
317
+ * The loose comparison of §7: compatibility forms, straight quotes for curly,
318
+ * one kind of dash, one kind of space, one case. Words themselves must match.
319
+ */
320
+ export function looseText(s) {
321
+ return s
322
+ .normalize("NFKC")
323
+ .replace(/[\u2018\u2019\u201a\u201b\u2032]/gu, "'")
324
+ .replace(/[\u201c\u201d\u201e\u201f\u2033]/gu, '"')
325
+ .replace(/[\u2010-\u2015\u2212]/gu, "-")
326
+ .replace(/\s*-\s*/gu, "-")
327
+ .replace(/\s+/gu, " ")
328
+ .trim()
329
+ .toLowerCase();
330
+ }
331
+ /** Sets inContent on each highlight from the page's text; skipped when the body is empty (§7). */
332
+ function checkContent(brief, bodyText, warnings) {
333
+ const body = bodyText === undefined ? "" : looseText(bodyText);
334
+ if (body === "")
335
+ return;
336
+ for (const h of brief.highlights) {
337
+ h.inContent = body.includes(looseText(h.text));
338
+ if (!h.inContent) {
339
+ const head = h.text.split(/\s+/u).slice(0, 6).join(" ");
340
+ warnings.push(`obs:highlight "${head}…" is not in the page's text (§7)`);
341
+ }
342
+ }
343
+ }
253
344
  function guard(fn) {
254
345
  try {
255
346
  return fn();
@@ -260,13 +351,24 @@ function guard(fn) {
260
351
  }
261
352
  // ---------------------------------------------------------------------------
262
353
  // Pages (§3.1)
354
+ function metaContent(doc, key) {
355
+ for (const meta of Array.from(doc.getElementsByTagName("meta"))) {
356
+ const k = (meta.getAttribute("property") ?? meta.getAttribute("name") ?? "").trim().toLowerCase();
357
+ if (k !== key)
358
+ continue;
359
+ const v = meta.getAttribute("content")?.trim();
360
+ if (v)
361
+ return v;
362
+ }
363
+ return undefined;
364
+ }
263
365
  /**
264
366
  * Reads a page's obs: meta tags. `pageUrl` is where the page was fetched
265
367
  * from; the page's <link rel="canonical"> takes precedence for §11.
266
368
  */
267
369
  export function parseObs(doc, pageUrl) {
268
370
  return guard(() => {
269
- const bag = new Map();
371
+ const bag = newBag();
270
372
  const warnings = [];
271
373
  let inBody = false;
272
374
  for (const meta of Array.from(doc.getElementsByTagName("meta"))) {
@@ -280,9 +382,13 @@ export function parseObs(doc, pageUrl) {
280
382
  continue;
281
383
  if (!inBody && doc.body && doc.body.contains(meta))
282
384
  inBody = true;
283
- put(bag, key, content);
385
+ // An unescaped double quote ends the attribute early; the rest becomes attributes with a quote in their names.
386
+ if (Array.from(meta.attributes).some((a) => a.name.includes('"'))) {
387
+ warnings.push(`${key}: the value is cut short at a double quote; write a double quote inside a value as &quot; (§9)`);
388
+ }
389
+ collect(bag, key, content, warnings);
284
390
  }
285
- if (bag.size === 0)
391
+ if (isEmpty(bag))
286
392
  return { errors: ["No obs: tags on the page"], warnings };
287
393
  if (inBody)
288
394
  warnings.push("Some obs: tags are in <body>; they belong in <head> (§3.1)");
@@ -294,11 +400,17 @@ export function parseObs(doc, pageUrl) {
294
400
  ctx.canonical = canonical;
295
401
  if (lang !== undefined)
296
402
  ctx.lang = lang;
297
- return build(bag, ctx, warnings);
403
+ const articleModified = metaContent(doc, "article:modified_time");
404
+ if (articleModified !== undefined)
405
+ ctx.alt = { value: articleModified, from: "article" };
406
+ const result = build(bag, ctx, warnings);
407
+ if (result.brief)
408
+ checkContent(result.brief, doc.body?.textContent ?? undefined, result.warnings);
409
+ return result;
298
410
  });
299
411
  }
300
412
  // ---------------------------------------------------------------------------
301
- // Feeds (§13)
413
+ // Feeds (§14)
302
414
  function obsChildren(item) {
303
415
  return Array.from(item.children).filter((el) => {
304
416
  if (el.namespaceURI === OBS_NS)
@@ -329,18 +441,42 @@ function itemLink(item) {
329
441
  }
330
442
  return undefined;
331
443
  }
444
+ /** The item's own date (§13): Atom <updated>, or RSS <pubDate> turned into ISO 8601. */
445
+ function itemDate(item) {
446
+ for (const el of Array.from(item.children)) {
447
+ if (el.namespaceURI === OBS_NS)
448
+ continue;
449
+ const text = el.textContent?.trim();
450
+ if (!text)
451
+ continue;
452
+ if (el.localName === "updated" && validModified(text))
453
+ return text;
454
+ if (el.localName === "pubDate") {
455
+ const ms = Date.parse(text);
456
+ if (!Number.isNaN(ms))
457
+ return new Date(ms).toISOString();
458
+ }
459
+ }
460
+ return undefined;
461
+ }
332
462
  /**
333
463
  * Reads the OBS elements of one RSS <item> or Atom <entry>. `itemUrl`
334
464
  * overrides the item's own link when given.
335
465
  */
336
466
  export function parseFeedObs(item, itemUrl) {
337
467
  return guard(() => {
338
- const bag = new Map();
468
+ const bag = newBag();
339
469
  const warnings = [];
340
470
  for (const el of obsChildren(item)) {
341
471
  const name = localName(el);
342
472
  const value = el.textContent ?? "";
343
- if (name === "summary") {
473
+ if (name === "highlight") {
474
+ collect(bag, "obs:highlight", value, warnings);
475
+ const label = el.getAttribute("label");
476
+ if (label !== null)
477
+ collect(bag, "obs:highlight:label", label, warnings);
478
+ }
479
+ else if (name === "summary") {
344
480
  const length = el.getAttribute("length")?.trim().toLowerCase();
345
481
  if (length === undefined || length === "")
346
482
  put(bag, "obs:summary", value);
@@ -363,13 +499,16 @@ export function parseFeedObs(item, itemUrl) {
363
499
  put(bag, `obs:${name}`, value);
364
500
  }
365
501
  }
366
- if (bag.size === 0)
502
+ if (isEmpty(bag))
367
503
  return { errors: ["No OBS elements on the item"], warnings };
368
504
  const link = itemUrl ?? itemLink(item);
369
505
  const canonical = link ? normalizeUrl(link) : undefined;
370
506
  const ctx = {};
371
507
  if (canonical !== undefined)
372
508
  ctx.canonical = canonical;
509
+ const date = itemDate(item);
510
+ if (date !== undefined)
511
+ ctx.alt = { value: date, from: "feed" };
373
512
  return build(bag, ctx, warnings);
374
513
  });
375
514
  }
@@ -382,7 +521,7 @@ export function validateObs(input) {
382
521
  return { errors: ["Expected an object in the ObsBrief shape"], warnings: [] };
383
522
  }
384
523
  const o = input;
385
- const bag = new Map();
524
+ const bag = newBag();
386
525
  const warnings = [];
387
526
  const str = (v) => (typeof v === "string" ? v : undefined);
388
527
  const set = (key, v) => {
@@ -392,7 +531,14 @@ export function validateObs(input) {
392
531
  else if (v !== undefined && v !== null)
393
532
  warnings.push(`${key} is not a string; it is ignored`);
394
533
  };
395
- set("obs:modified", o.modified);
534
+ // A modified time that came from the carrier is carried through with its source.
535
+ const ctx = {};
536
+ if ((o.modifiedFrom === "article" || o.modifiedFrom === "feed") && typeof o.modified === "string") {
537
+ ctx.alt = { value: o.modified, from: o.modifiedFrom };
538
+ }
539
+ else {
540
+ set("obs:modified", o.modified);
541
+ }
396
542
  if (typeof o.version === "number")
397
543
  put(bag, "obs:version", String(o.version));
398
544
  set("obs:url", o.url);
@@ -422,6 +568,26 @@ export function validateObs(input) {
422
568
  if (Array.isArray(o.keep))
423
569
  for (const k of o.keep)
424
570
  set("obs:keep", k);
571
+ if (Array.isArray(o.highlights)) {
572
+ for (const h of o.highlights) {
573
+ if (!h || typeof h !== "object")
574
+ continue;
575
+ const rec = h;
576
+ const t = str(rec.text);
577
+ if (t === undefined) {
578
+ warnings.push("A highlight has no text; it is ignored");
579
+ continue;
580
+ }
581
+ const raw = { text: t };
582
+ const label = str(rec.label);
583
+ if (label !== undefined)
584
+ raw.label = label;
585
+ // The producer saw the page; its verdict is carried through.
586
+ if (typeof rec.inContent === "boolean")
587
+ raw.inContent = rec.inContent;
588
+ bag.highlights.push(raw);
589
+ }
590
+ }
425
591
  if (o.derive && typeof o.derive === "object") {
426
592
  const d = o.derive;
427
593
  set("obs:derive", d.mode);
@@ -430,7 +596,9 @@ export function validateObs(input) {
430
596
  if (typeof d.combine === "boolean")
431
597
  put(bag, "obs:derive:combine", d.combine ? "yes" : "no");
432
598
  }
433
- const result = build(bag, {}, warnings);
599
+ if (isEmpty(bag))
600
+ return { errors: ["No summary: obs:summary or a tiered summary is required (§3)"], warnings };
601
+ const result = build(bag, ctx, warnings);
434
602
  if (result.brief && typeof o.describesThisPage === "boolean") {
435
603
  result.brief.describesThisPage = o.describesThisPage;
436
604
  }
@@ -438,12 +606,122 @@ export function validateObs(input) {
438
606
  });
439
607
  }
440
608
  // ---------------------------------------------------------------------------
441
- // Feed against page (§13)
442
- /** The later obs:modified stands; on a tie, the page stands. */
609
+ // Feed against page (§14)
610
+ /**
611
+ * The later effective modified time stands. When the two are equal, or either
612
+ * side has none, the page stands.
613
+ */
443
614
  export function mergeObs(page, feed) {
444
615
  if (!page)
445
616
  return feed;
446
617
  if (!feed)
447
618
  return page;
619
+ if (page.modified === undefined || feed.modified === undefined)
620
+ return page;
448
621
  return Date.parse(feed.modified) > Date.parse(page.modified) ? feed : page;
449
622
  }
623
+ // ---------------------------------------------------------------------------
624
+ // Change detection (§13)
625
+ /**
626
+ * A stable hash of what a brief says: every text value and every setting, in a
627
+ * fixed order. Two briefs with the same fingerprint present the same. It leaves
628
+ * out the modified time, and everything that depends on where the tags were
629
+ * read rather than on what they say (the language fallback, describesThisPage,
630
+ * a highlight's inContent), so a page and its feed item agree. Readers compare
631
+ * fingerprints to tell a change, above all when there is no modified time.
632
+ *
633
+ * SHA-256 of the canonical JSON below, in hex, first 16 characters.
634
+ */
635
+ export function fingerprint(brief) {
636
+ const canonical = JSON.stringify([
637
+ "obs1",
638
+ brief.version,
639
+ brief.url ?? null,
640
+ brief.title ?? null,
641
+ brief.headline ?? null,
642
+ brief.summaries.map((s) => [s.tier, s.text]),
643
+ brief.keep,
644
+ brief.highlights.map((h) => [h.text, h.label ?? null]),
645
+ [brief.derive.mode, brief.derive.translate, brief.derive.combine],
646
+ brief.attribution ?? null,
647
+ brief.preference,
648
+ ]);
649
+ return sha256Hex(utf8(canonical)).slice(0, 16);
650
+ }
651
+ /** UTF-8 bytes, without TextEncoder so the module needs no platform API. */
652
+ function utf8(text) {
653
+ const out = [];
654
+ for (const ch of text) {
655
+ const c = ch.codePointAt(0);
656
+ if (c < 0x80)
657
+ out.push(c);
658
+ else if (c < 0x800)
659
+ out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
660
+ else if (c < 0x10000)
661
+ out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
662
+ else
663
+ out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
664
+ }
665
+ return out;
666
+ }
667
+ const SHA_K = [
668
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
669
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
670
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
671
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
672
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
673
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
674
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
675
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
676
+ ];
677
+ /** SHA-256 (FIPS 180-4), synchronous, so fingerprint() is too. */
678
+ function sha256Hex(bytes) {
679
+ const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
680
+ const bits = bytes.length * 8;
681
+ const msg = bytes.slice();
682
+ msg.push(0x80);
683
+ while (msg.length % 64 !== 56)
684
+ msg.push(0);
685
+ const hi = Math.floor(bits / 0x100000000);
686
+ const lo = bits >>> 0;
687
+ msg.push((hi >>> 24) & 255, (hi >>> 16) & 255, (hi >>> 8) & 255, hi & 255, (lo >>> 24) & 255, (lo >>> 16) & 255, (lo >>> 8) & 255, lo & 255);
688
+ const w = new Array(64).fill(0);
689
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
690
+ for (let i = 0; i < msg.length; i += 64) {
691
+ for (let t = 0; t < 16; t++)
692
+ w[t] = ((msg[i + t * 4] << 24) | (msg[i + t * 4 + 1] << 16) | (msg[i + t * 4 + 2] << 8) | msg[i + t * 4 + 3]) >>> 0;
693
+ for (let t = 16; t < 64; t++) {
694
+ const a = w[t - 15];
695
+ const b = w[t - 2];
696
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
697
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
698
+ w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
699
+ }
700
+ let [a, b, c, d, e, f, g, k] = h;
701
+ for (let t = 0; t < 64; t++) {
702
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
703
+ const ch = (e & f) ^ (~e & g);
704
+ const t1 = (k + S1 + ch + SHA_K[t] + w[t]) >>> 0;
705
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
706
+ const maj = (a & b) ^ (a & c) ^ (b & c);
707
+ const t2 = (S0 + maj) >>> 0;
708
+ k = g;
709
+ g = f;
710
+ f = e;
711
+ e = (d + t1) >>> 0;
712
+ d = c;
713
+ c = b;
714
+ b = a;
715
+ a = (t1 + t2) >>> 0;
716
+ }
717
+ h[0] = (h[0] + a) >>> 0;
718
+ h[1] = (h[1] + b) >>> 0;
719
+ h[2] = (h[2] + c) >>> 0;
720
+ h[3] = (h[3] + d) >>> 0;
721
+ h[4] = (h[4] + e) >>> 0;
722
+ h[5] = (h[5] + f) >>> 0;
723
+ h[6] = (h[6] + g) >>> 0;
724
+ h[7] = (h[7] + k) >>> 0;
725
+ }
726
+ return h.map((x) => x.toString(16).padStart(8, "0")).join("");
727
+ }