@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.
- package/index.js +103 -19
- 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
|
-
*
|
|
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(
|
|
312
|
-
const margin = Math.max(
|
|
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(
|
|
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
|
|
374
|
+
const rawOutBuf = await sharp(filePath)
|
|
375
|
+
.rotate()
|
|
323
376
|
.composite([{ input: stampPngBuffer, left, top }])
|
|
324
377
|
.png()
|
|
325
|
-
.
|
|
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
|
|
347
|
-
const stampPngBuffer = await sharp(Buffer.from(
|
|
348
|
-
.resize(
|
|
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(
|
|
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:
|
|
358
|
-
y:
|
|
415
|
+
x: drawX,
|
|
416
|
+
y: drawY,
|
|
359
417
|
width: targetSize,
|
|
360
418
|
height: targetSize,
|
|
361
419
|
});
|
|
362
420
|
|
|
363
|
-
|
|
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.
|
|
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": {
|