altweb-context 1.0.2 → 1.1.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.
@@ -6920,12 +6920,33 @@ if (typeof globalThis.window === "undefined") {
6920
6920
  }
6921
6921
 
6922
6922
  // ../core/src/crypto/encoding.ts
6923
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
6923
6924
  function base64urlDecode(str) {
6925
+ if (!/^[A-Za-z0-9_-]*$/.test(str)) {
6926
+ throw new Error("invalid base64url charset");
6927
+ }
6928
+ const rem = str.length % 4;
6929
+ if (rem === 1) {
6930
+ throw new Error("invalid base64url length");
6931
+ }
6932
+ if (rem !== 0) {
6933
+ const lastValue = BASE64URL_ALPHABET.indexOf(str[str.length - 1]);
6934
+ const unusedBits = rem === 2 ? 15 : 3;
6935
+ if ((lastValue & unusedBits) !== 0) {
6936
+ throw new Error("non-canonical base64url encoding");
6937
+ }
6938
+ }
6924
6939
  let base644 = str.replace(/-/g, "+").replace(/_/g, "/");
6925
6940
  while (base644.length % 4) base644 += "=";
6926
6941
  const binary = atob(base644);
6927
6942
  return Uint8Array.from(binary, (c) => c.charCodeAt(0));
6928
6943
  }
6944
+ function concatBytes(a, b2) {
6945
+ const out = new Uint8Array(a.length + b2.length);
6946
+ out.set(a, 0);
6947
+ out.set(b2, a.length);
6948
+ return out;
6949
+ }
6929
6950
 
6930
6951
  // ../core/src/crypto/key-derivation.ts
6931
6952
  var PBKDF2_ITERATIONS = 6e5;
@@ -6966,10 +6987,25 @@ async function decrypt(payload, password) {
6966
6987
  }
6967
6988
 
6968
6989
  // ../core/src/crypto/signing.ts
6990
+ var P256_N = BigInt(
6991
+ "0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
6992
+ );
6993
+ var P256_HALF_N = P256_N >> 1n;
6994
+ function isLowS(signature) {
6995
+ if (signature.length !== 64) return true;
6996
+ let s = 0n;
6997
+ for (let i = 32; i < 64; i++) {
6998
+ s = s << 8n | BigInt(signature[i]);
6999
+ }
7000
+ return s <= P256_HALF_N;
7001
+ }
6969
7002
  async function verify(data, signatureBase64, publicKeyBase64) {
6970
7003
  try {
6971
7004
  const signature = base64urlDecode(signatureBase64);
6972
7005
  const publicKeyData = base64urlDecode(publicKeyBase64);
7006
+ if (!isLowS(signature)) {
7007
+ return false;
7008
+ }
6973
7009
  const publicKey = await crypto.subtle.importKey(
6974
7010
  "spki",
6975
7011
  publicKeyData,
@@ -11190,30 +11226,39 @@ var pako = {
11190
11226
 
11191
11227
  // ../core/src/compression/compress.ts
11192
11228
  var MAX_DECOMPRESSED_BYTES = 16 * 1024 * 1024;
11229
+ var MAX_COMPRESSED_BYTES = 17 * 1024 * 1024;
11230
+ var INFLATE_SLICE_BYTES = 64 * 1024;
11193
11231
  function decompress(data) {
11232
+ if (data.length > MAX_COMPRESSED_BYTES) {
11233
+ throw new Error(
11234
+ `Compressed payload exceeds the ${MAX_COMPRESSED_BYTES} byte limit`
11235
+ );
11236
+ }
11194
11237
  const inflator = new pako.Inflate();
11195
11238
  const chunks = [];
11196
11239
  let total = 0;
11197
- let bombed = false;
11198
11240
  inflator.onData = (chunk) => {
11199
11241
  const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
11200
11242
  total += bytes.length;
11201
- if (total > MAX_DECOMPRESSED_BYTES) {
11202
- bombed = true;
11203
- inflator.err = -2;
11204
- return;
11205
- }
11206
11243
  chunks.push(bytes);
11207
11244
  };
11208
- inflator.onEnd = () => {
11209
- };
11210
- inflator.push(data, true);
11211
- if (bombed) {
11212
- throw new Error(
11213
- `Decompressed payload exceeds the ${MAX_DECOMPRESSED_BYTES} byte limit`
11214
- );
11245
+ if (data.length === 0) {
11246
+ inflator.push(data, true);
11215
11247
  }
11216
- if (inflator.err) {
11248
+ for (let offset2 = 0; offset2 < data.length; offset2 += INFLATE_SLICE_BYTES) {
11249
+ const last = offset2 + INFLATE_SLICE_BYTES >= data.length;
11250
+ inflator.push(data.subarray(offset2, offset2 + INFLATE_SLICE_BYTES), last);
11251
+ if (total > MAX_DECOMPRESSED_BYTES) {
11252
+ throw new Error(
11253
+ `Decompressed payload exceeds the ${MAX_DECOMPRESSED_BYTES} byte limit`
11254
+ );
11255
+ }
11256
+ if (inflator.err) {
11257
+ throw new Error(inflator.msg || "invalid deflate stream");
11258
+ }
11259
+ if (inflator.ended) break;
11260
+ }
11261
+ if (inflator.err || !inflator.ended) {
11217
11262
  throw new Error(inflator.msg || "invalid deflate stream");
11218
11263
  }
11219
11264
  const result = new Uint8Array(total);
@@ -12769,12 +12814,45 @@ function sanitizePage(page) {
12769
12814
  ...page.meta,
12770
12815
  title: purify.sanitize(page.meta.title, { ALLOWED_TAGS: [] }),
12771
12816
  description: page.meta.description ? purify.sanitize(page.meta.description, { ALLOWED_TAGS: [] }) : void 0,
12772
- author: page.meta.author ? purify.sanitize(page.meta.author, { ALLOWED_TAGS: [] }) : void 0
12817
+ author: page.meta.author ? purify.sanitize(page.meta.author, { ALLOWED_TAGS: [] }) : void 0,
12818
+ header: page.meta.header ? sanitizeHeader(page.meta.header) : void 0,
12819
+ footer: page.meta.footer ? sanitizeFooter(page.meta.footer) : void 0
12773
12820
  },
12774
12821
  blocks: page.blocks.map(sanitizeBlock),
12775
12822
  style: sanitizeStyle(page.style)
12776
12823
  };
12777
12824
  }
12825
+ function sanitizeHeader(header) {
12826
+ return {
12827
+ ...header,
12828
+ logo: header.logo ? sanitizeImageSrc(header.logo) : void 0,
12829
+ customText: header.customText ? purify.sanitize(header.customText, { ALLOWED_TAGS: [] }) : void 0
12830
+ };
12831
+ }
12832
+ function sanitizeFooter(footer) {
12833
+ return {
12834
+ ...footer,
12835
+ copyright: footer.copyright ? purify.sanitize(footer.copyright, { ALLOWED_TAGS: [] }) : void 0,
12836
+ customText: footer.customText ? purify.sanitize(footer.customText, { ALLOWED_TAGS: [] }) : void 0,
12837
+ links: footer.links ? footer.links.map((link) => ({
12838
+ label: purify.sanitize(link.label, { ALLOWED_TAGS: [] }),
12839
+ url: sanitizeUrl(link.url)
12840
+ })) : void 0
12841
+ };
12842
+ }
12843
+ function sanitizeImageSrc(src) {
12844
+ if (src.startsWith("data:")) {
12845
+ return validateDataUri(src);
12846
+ }
12847
+ try {
12848
+ const parsed = new URL(src);
12849
+ if (["http:", "https:"].includes(parsed.protocol)) {
12850
+ return src;
12851
+ }
12852
+ } catch {
12853
+ }
12854
+ return "";
12855
+ }
12778
12856
  function sanitizeBlock(block) {
12779
12857
  switch (block.t) {
12780
12858
  case "h":
@@ -27584,6 +27662,14 @@ function validatePageStructure(data) {
27584
27662
  }
27585
27663
 
27586
27664
  // ../core/src/codec/decoder.ts
27665
+ var MAX_ENVELOPE_CHARS = 24 * 1024 * 1024;
27666
+ function assertEnvelopeSize(hash2) {
27667
+ if (hash2.length > MAX_ENVELOPE_CHARS) {
27668
+ throw new ValidationError(
27669
+ `Envelope exceeds the ${MAX_ENVELOPE_CHARS} character limit`
27670
+ );
27671
+ }
27672
+ }
27587
27673
  var DecryptionError = class extends Error {
27588
27674
  constructor(message) {
27589
27675
  super(message);
@@ -27597,6 +27683,7 @@ var ValidationError = class extends Error {
27597
27683
  }
27598
27684
  };
27599
27685
  async function decodePage(hash2, password) {
27686
+ assertEnvelopeSize(hash2);
27600
27687
  const envelopeBytes = base64urlDecode(hash2);
27601
27688
  const envelopeJson = new TextDecoder().decode(envelopeBytes);
27602
27689
  let envelope;
@@ -27632,7 +27719,8 @@ async function decodePage(hash2, password) {
27632
27719
  let verified2 = false;
27633
27720
  let fingerprint2;
27634
27721
  if (envelope.s && envelope.pk) {
27635
- verified2 = await verify(blocksCompressed, envelope.s, envelope.pk);
27722
+ const signedBytes = concatBytes(metaCompressed, blocksCompressed);
27723
+ verified2 = await verify(signedBytes, envelope.s, envelope.pk);
27636
27724
  if (verified2) {
27637
27725
  fingerprint2 = await computeFingerprint(envelope.pk);
27638
27726
  }
@@ -27706,6 +27794,7 @@ async function decodePage(hash2, password) {
27706
27794
  }
27707
27795
  function isEncryptedContent(hash2) {
27708
27796
  try {
27797
+ assertEnvelopeSize(hash2);
27709
27798
  const envelopeBytes = base64urlDecode(hash2);
27710
27799
  const envelopeJson = new TextDecoder().decode(envelopeBytes);
27711
27800
  const envelope = JSON.parse(envelopeJson);
@@ -27717,6 +27806,9 @@ function isEncryptedContent(hash2) {
27717
27806
 
27718
27807
  // ../core/src/codec/inspect.ts
27719
27808
  async function inspectArtifact(hash2) {
27809
+ if (hash2.length > MAX_ENVELOPE_CHARS) {
27810
+ throw new ValidationError("Invalid artifact format");
27811
+ }
27720
27812
  let envelope;
27721
27813
  try {
27722
27814
  envelope = JSON.parse(new TextDecoder().decode(base64urlDecode(hash2)));
@@ -29082,10 +29174,28 @@ var HTML_ENTITIES = {
29082
29174
  function decodeHtmlEntities(text2) {
29083
29175
  return text2.replace(/&(?:amp|lt|gt|quot|#39|apos|nbsp);/g, (match) => HTML_ENTITIES[match] || match);
29084
29176
  }
29177
+ function stripTags(html2) {
29178
+ let out = "";
29179
+ let i = 0;
29180
+ while (i < html2.length) {
29181
+ const lt = html2.indexOf("<", i);
29182
+ if (lt === -1) {
29183
+ out += html2.slice(i);
29184
+ break;
29185
+ }
29186
+ out += html2.slice(i, lt);
29187
+ const gt = html2.indexOf(">", lt + 1);
29188
+ if (gt === -1) {
29189
+ out += html2.slice(lt);
29190
+ break;
29191
+ }
29192
+ i = gt + 1;
29193
+ }
29194
+ return out;
29195
+ }
29085
29196
  function stripHtml(html2) {
29086
29197
  if (!html2) return "";
29087
- const stripped = html2.replace(/<[^>]*>/g, "");
29088
- return decodeHtmlEntities(stripped);
29198
+ return decodeHtmlEntities(stripTags(html2));
29089
29199
  }
29090
29200
  function htmlToMarkdown(html2) {
29091
29201
  if (!html2) return "";
@@ -29094,7 +29204,7 @@ function htmlToMarkdown(html2) {
29094
29204
  md = md.replace(/<(?:em|i)>(.*?)<\/(?:em|i)>/gi, "*$1*");
29095
29205
  md = md.replace(/<code>(.*?)<\/code>/gi, "`$1`");
29096
29206
  md = md.replace(/<a\s+href="([^"]*)"[^>]*>(.*?)<\/a>/gi, "[$2]($1)");
29097
- md = md.replace(/<[^>]*>/g, "");
29207
+ md = stripTags(md);
29098
29208
  return decodeHtmlEntities(md);
29099
29209
  }
29100
29210
 
@@ -47378,16 +47488,52 @@ function isPrivateV4(addr) {
47378
47488
  a === 169 && b2 === 254 || // link-local / cloud metadata
47379
47489
  a === 172 && b2 >= 16 && b2 <= 31 || a === 192 && b2 === 168;
47380
47490
  }
47491
+ function parseHextets(addr) {
47492
+ let s = addr;
47493
+ const lastColon = s.lastIndexOf(":");
47494
+ const tail = s.slice(lastColon + 1);
47495
+ if (tail.includes(".")) {
47496
+ if (isIP(tail) !== 4) return null;
47497
+ const [a, b2, c, d] = tail.split(".").map(Number);
47498
+ s = s.slice(0, lastColon + 1) + (a << 8 | b2).toString(16) + ":" + (c << 8 | d).toString(16);
47499
+ }
47500
+ const doubleColons = s.split("::");
47501
+ if (doubleColons.length > 2) return null;
47502
+ const parseGroups = (part) => {
47503
+ if (part === "") return [];
47504
+ const groups = [];
47505
+ for (const g2 of part.split(":")) {
47506
+ if (!/^[0-9a-fA-F]{1,4}$/.test(g2)) return null;
47507
+ groups.push(parseInt(g2, 16));
47508
+ }
47509
+ return groups;
47510
+ };
47511
+ const head = parseGroups(doubleColons[0]);
47512
+ const tailGroups = doubleColons.length === 2 ? parseGroups(doubleColons[1]) : [];
47513
+ if (head === null || tailGroups === null) return null;
47514
+ if (doubleColons.length === 2) {
47515
+ const fill = 8 - head.length - tailGroups.length;
47516
+ if (fill < 1) return null;
47517
+ return [...head, ...new Array(fill).fill(0), ...tailGroups];
47518
+ }
47519
+ return head.length === 8 ? head : null;
47520
+ }
47521
+ function embeddedV4(hi, lo) {
47522
+ return `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
47523
+ }
47381
47524
  function isPrivateAddress(addr) {
47382
- const lower = addr.toLowerCase();
47383
- if (lower.startsWith("::ffff:")) {
47384
- const embedded = lower.slice(7);
47385
- return isIP(embedded) === 4 ? isPrivateV4(embedded) : true;
47386
- }
47387
47525
  if (isIP(addr) === 4) return isPrivateV4(addr);
47388
- return lower === "::1" || lower === "::" || lower.startsWith("fe80:") || // link-local
47389
- lower.startsWith("fc") || // ULA fc00::/7
47390
- lower.startsWith("fd");
47526
+ const h = parseHextets(addr);
47527
+ if (h === null) return true;
47528
+ if (h.slice(0, 6).every((x2) => x2 === 0)) return true;
47529
+ if (h.slice(0, 5).every((x2) => x2 === 0) && h[5] === 65535) {
47530
+ return isPrivateV4(embeddedV4(h[6], h[7]));
47531
+ }
47532
+ if ((h[0] & 65472) === 65152) return true;
47533
+ if ((h[0] & 65024) === 64512) return true;
47534
+ if (h[0] === 100 && h[1] === 65435) return true;
47535
+ if (h[0] === 8194) return true;
47536
+ return false;
47391
47537
  }
47392
47538
  function guardedLookup(hostname4, options, callback) {
47393
47539
  lookup(hostname4, { ...options, all: true }, (err2, addresses) => {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "altweb-context",
3
3
  "mcpName": "software.altweb/altweb-context",
4
- "version": "1.0.2",
5
- "description": "MCP server that loads ALTWEB signed context capsules into AI agents verifies first, refuses unsigned, tampered, or untrusted context by default. Verify before you inject.",
4
+ "version": "1.1.0",
5
+ "description": "MCP server that loads ALTWEB signed context capsules into AI agents \u2014 verifies first, refuses unsigned, tampered, or untrusted context by default. Verify before you inject.",
6
6
  "type": "module",
7
7
  "license": "AGPL-3.0-or-later",
8
- "author": "Daniel C. ȘOIMU",
8
+ "author": "Daniel C. \u0218OIMU",
9
9
  "homepage": "https://altweb.software",
10
10
  "repository": {
11
11
  "type": "git",