@rel-time/mcp-server 1.2.4 → 1.2.5

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 (2) hide show
  1. package/index.js +103 -19
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -17,7 +17,7 @@ const os = require("os");
17
17
  const path = require("path");
18
18
  const readline = require("readline");
19
19
  const { exec } = require("child_process");
20
- const { PDFDocument } = require("pdf-lib");
20
+ const { PDFDocument, PDFName, PDFString } = require("pdf-lib");
21
21
  const QRCode = require("qrcode");
22
22
  const sharp = require("sharp");
23
23
 
@@ -282,10 +282,48 @@ function launchBrowserPasskeyBridge(timeoutMs = 300000) {
282
282
  }
283
283
 
284
284
  /**
285
- * Stamps the Option A (pure crisp 64-ray Sun-Stamp, 0% glare, no pill) onto a local Image or PDF.
285
+ * Injects a standard PNG tEXt chunk ("RelTimeVerify\0<verifyUrl>") right before IEND
286
+ * so Stamped PNG copies can be instantaneously verified when dropped into /verify.
287
+ */
288
+ function injectPngVerifyBuffer(pngBuffer, verifyUrl) {
289
+ if (!verifyUrl || !Buffer.isBuffer(pngBuffer) || pngBuffer.length < 24) {
290
+ return pngBuffer;
291
+ }
292
+ const iendOffset = pngBuffer.length - 12;
293
+ if (pngBuffer.slice(iendOffset + 4, iendOffset + 8).toString("ascii") !== "IEND") {
294
+ return pngBuffer;
295
+ }
296
+ const payload = Buffer.from(`RelTimeVerify\0${verifyUrl}`, "utf8");
297
+ const typeBuf = Buffer.from("tEXt", "ascii");
298
+ const crcInput = Buffer.concat([typeBuf, payload]);
299
+
300
+ let crc = 0xffffffff;
301
+ for (let i = 0; i < crcInput.length; i++) {
302
+ crc ^= crcInput[i];
303
+ for (let b = 0; b < 8; b++) {
304
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
305
+ }
306
+ }
307
+ crc = (crc ^ 0xffffffff) >>> 0;
308
+
309
+ const lenBuf = Buffer.alloc(4);
310
+ lenBuf.writeUInt32BE(payload.length, 0);
311
+ const crcBuf = Buffer.alloc(4);
312
+ crcBuf.writeUInt32BE(crc, 0);
313
+
314
+ const chunk = Buffer.concat([lenBuf, crcInput, crcBuf]);
315
+ return Buffer.concat([
316
+ pngBuffer.slice(0, iendOffset),
317
+ chunk,
318
+ pngBuffer.slice(iendOffset),
319
+ ]);
320
+ }
321
+
322
+ /**
323
+ * Stamps the Option 1A (+14° Bitcoin Tilt, pure crisp 64-ray Sun-Stamp, 0% glare, no pill) onto a local Image or PDF.
286
324
  * Returns { stamped: true, output_path, format } or { stamped: false, reason }.
287
325
  */
288
- async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = null) {
326
+ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = null, verifyHash = null) {
289
327
  const ext = path.extname(filePath).toLowerCase().replace(/^\./, "");
290
328
  const dir = path.dirname(filePath);
291
329
  const baseName = path.basename(filePath, path.extname(filePath));
@@ -300,18 +338,32 @@ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = nu
300
338
  };
301
339
  }
302
340
 
341
+ // Resolve verification URL from verifyHash or from <a href="..."> inside svgContent
342
+ let verifyUrl = verifyHash ? `https://rel-time.com/verify?t=${verifyHash.toLowerCase()}` : null;
343
+ if (!verifyUrl && typeof svgContent === "string") {
344
+ const match = svgContent.match(/https?:\/\/rel-time\.com\/verify\?[tThH]=([0-9a-fA-F]{64})/);
345
+ if (match) {
346
+ verifyUrl = `https://rel-time.com/verify?t=${match[1].toLowerCase()}`;
347
+ }
348
+ }
349
+
350
+ // Strip outer <a> wrapper before rasterizing with sharp/librsvg
351
+ const cleanSvg = String(svgContent || "")
352
+ .replace(/<a\b[^>]*>/gi, "")
353
+ .replace(/<\/a>/gi, "");
354
+
303
355
  if (isImage) {
304
356
  const outPath = customOutputPath ? path.resolve(customOutputPath) : path.join(dir, `reltime-stamped-${baseName}.png`);
305
357
  const img = sharp(filePath);
306
358
  const meta = await img.metadata();
307
- const w = meta.width || 1200;
308
- const h = meta.height || 800;
359
+ const w = (meta.autoOrient && meta.autoOrient.width) || meta.width || 1200;
360
+ const h = (meta.autoOrient && meta.autoOrient.height) || meta.height || 800;
309
361
 
310
362
  const minDim = Math.min(w, h);
311
- const badgeSize = Math.max(130, Math.min(420, Math.round(minDim * 0.22)));
312
- const margin = Math.max(12, Math.round(minDim * 0.025));
363
+ const badgeSize = Math.max(180, Math.round(minDim * 0.22));
364
+ const margin = Math.max(14, Math.round(minDim * 0.025));
313
365
 
314
- const stampPngBuffer = await sharp(Buffer.from(svgContent, "utf8"))
366
+ const stampPngBuffer = await sharp(Buffer.from(cleanSvg, "utf8"))
315
367
  .resize(badgeSize, badgeSize)
316
368
  .png()
317
369
  .toBuffer();
@@ -319,10 +371,14 @@ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = nu
319
371
  const left = Math.max(0, w - badgeSize - margin);
320
372
  const top = Math.max(0, h - badgeSize - margin);
321
373
 
322
- await img
374
+ const rawOutBuf = await sharp(filePath)
375
+ .rotate()
323
376
  .composite([{ input: stampPngBuffer, left, top }])
324
377
  .png()
325
- .toFile(outPath);
378
+ .toBuffer();
379
+
380
+ const finalPngBuf = verifyUrl ? injectPngVerifyBuffer(rawOutBuf, verifyUrl) : rawOutBuf;
381
+ fs.writeFileSync(outPath, finalPngBuf);
326
382
 
327
383
  return {
328
384
  stamped: true,
@@ -343,24 +399,52 @@ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = nu
343
399
  const firstPage = pages[0];
344
400
  const { width, height } = firstPage.getSize();
345
401
 
346
- // Render high-DPI 480x480 Option A Sun-Stamp PNG (pure crisp SVG, 0% glare, no pill)
347
- const stampPngBuffer = await sharp(Buffer.from(svgContent, "utf8"))
348
- .resize(480, 480)
402
+ // Render high-DPI 1024x1024 (300+ DPI) Option 1A Sun-Stamp PNG
403
+ const stampPngBuffer = await sharp(Buffer.from(cleanSvg, "utf8"))
404
+ .resize(1024, 1024)
349
405
  .png()
350
406
  .toBuffer();
351
407
 
352
408
  const pngImage = await pdfDoc.embedPng(stampPngBuffer);
353
- const targetSize = Math.min(135, Math.max(85, Math.min(width, height) * 0.2));
409
+ const targetSize = Math.min(148, Math.max(105, Math.min(width, height) * 0.22));
354
410
  const margin = 18;
411
+ const drawX = Math.max(margin, width - targetSize - margin);
412
+ const drawY = margin;
355
413
 
356
414
  firstPage.drawImage(pngImage, {
357
- x: Math.max(margin, width - targetSize - margin),
358
- y: margin,
415
+ x: drawX,
416
+ y: drawY,
359
417
  width: targetSize,
360
418
  height: targetSize,
361
419
  });
362
420
 
363
- const modifiedPdfBytes = await pdfDoc.save();
421
+ // Attach clickable PDF Link Annotation over the Sun-Stamp rectangle
422
+ if (verifyUrl) {
423
+ try {
424
+ const linkAnnotation = pdfDoc.context.obj({
425
+ Type: "Annot",
426
+ Subtype: "Link",
427
+ Rect: [drawX, drawY, drawX + targetSize, drawY + targetSize],
428
+ Border: [0, 0, 0],
429
+ F: 4,
430
+ A: {
431
+ Type: "Action",
432
+ S: "URI",
433
+ URI: PDFString.of(verifyUrl),
434
+ },
435
+ });
436
+ const linkRef = pdfDoc.context.register(linkAnnotation);
437
+ if (typeof firstPage.node.addAnnot === "function") {
438
+ firstPage.node.addAnnot(linkRef);
439
+ } else {
440
+ firstPage.node.set(PDFName.of("Annots"), pdfDoc.context.obj([linkRef]));
441
+ }
442
+ } catch (e) {
443
+ // Ignore annotation error on restricted PDFs
444
+ }
445
+ }
446
+
447
+ const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false });
364
448
  fs.writeFileSync(outPath, Buffer.from(modifiedPdfBytes));
365
449
 
366
450
  return {
@@ -701,7 +785,7 @@ async function executeTool(name, args = {}) {
701
785
  // 3. Automatically generate Stamped Copy (.PDF or .PNG) for PDFs and Images
702
786
  let stampedCopyResult = null;
703
787
  if (args.save_stamped_copy !== false) {
704
- stampedCopyResult = await stampLocalFileOptionA(filePath, svgContent);
788
+ stampedCopyResult = await stampLocalFileOptionA(filePath, svgContent, null, clientHash);
705
789
  }
706
790
 
707
791
  return {
@@ -743,7 +827,7 @@ async function executeTool(name, args = {}) {
743
827
  throw new Error(`Failed to fetch Sun-Stamp (${stampRes.status}): ${await stampRes.text()}`);
744
828
  }
745
829
  const svgContent = await stampRes.text();
746
- const result = await stampLocalFileOptionA(filePath, svgContent, args.output_path || null);
830
+ const result = await stampLocalFileOptionA(filePath, svgContent, args.output_path || null, targetHash);
747
831
  return {
748
832
  file_path: filePath,
749
833
  hash: targetHash,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rel-time/mcp-server",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Official Model Context Protocol (MCP) server for Rel-Time (rel-time.com) — Zero-Knowledge Bitcoin & VDF Provenance, Local File Hashing, and Sun-Stamp Generation for Claude, Cursor, Windsurf, and Antigravity.",
5
5
  "main": "index.js",
6
6
  "bin": {