fix: match Verae Timestamping.app registration PDF and receipt fields

Parse completedAt/certificate/result from Verae API; enrich merkle via
POST /api/verify (blockIndex). Generate certificate with the same jsPDF
layout as register.html export. Preserve original filename (e.g. .html).
This commit is contained in:
George Lambert 2026-10-03 04:26:45 -04:00
commit c6d998a16d
10 changed files with 1110 additions and 198 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
*.pkg
*.msi
.DS_Store
node_modules/

View file

@ -100,7 +100,18 @@ func Run(opt Options) (Result, error) {
}
stem := StemFromPath(opt.SourcePDF, opt.DocumentName)
origPath := filepath.Join(outDir, stem+".pdf")
// Preserve original basename (e.g. .html) for the proven artifact + certificate label.
origBase := filepath.Base(opt.SourcePDF)
if opt.DocumentName != "" {
origBase = filepath.Base(opt.DocumentName)
if filepath.Ext(origBase) == "" {
origBase = origBase + filepath.Ext(opt.SourcePDF)
}
}
if origBase == "" || origBase == "." {
origBase = stem + ".pdf"
}
origPath := filepath.Join(outDir, origBase)
srcBytes, err := os.ReadFile(opt.SourcePDF)
if err != nil {
return Result{}, err
@ -116,7 +127,7 @@ func Run(opt Options) (Result, error) {
client := *opt.Client
client.Channel = opt.Channel
rec, err := client.Stamp(digest, stem+".pdf")
rec, err := client.Stamp(digest, origBase)
if err != nil {
return Result{}, err
}
@ -130,7 +141,7 @@ func Run(opt Options) (Result, error) {
// Always prefer the branded Registration Document (digests-only wording,
// no mcp.verae.ai). Edge attachPdf is optional and may still say MCP.
pdfBytes, err := pdfcert.Build(pdfcert.Input{
DocumentName: stem + ".pdf",
DocumentName: origBase,
SHA256: digest,
ReceiptID: rec.ReceiptID,
CreatedAt: rec.CreatedAt,
@ -149,16 +160,17 @@ func Run(opt Options) (Result, error) {
sidecar := map[string]any{
"helper_version": HelperVersion,
"document_name": stem + ".pdf",
"document_name": origBase,
"sha256": digest,
"receipt_id": rec.ReceiptID,
"created_at": rec.CreatedAt,
"merkle": rec.Merkle,
"channel": string(opt.Channel),
"base_url": rec.BaseURL,
"original_pdf": filepath.Base(origPath),
"original_file": filepath.Base(origPath),
"certificate_pdf": filepath.Base(certPath),
"byte_length": len(srcBytes),
"status": "ok",
"saved_at": time.Now().UTC().Format(time.RFC3339),
}
jsonPath := filepath.Join(outDir, stem+".verae.json")

View file

@ -84,7 +84,10 @@ func TestRunVeraeRendersLocalCert(t *testing.T) {
VeraeBase: "https://api.test",
VeraeToken: "jwt",
HTTPDo: func(req *http.Request) (*http.Response, error) {
body := `{"certificate":"cidLocal1","timestamp":"2026-10-03T12:00:00Z","existing":true}`
body := `{"existing":true,"certificate":"cidLocal1","receipts":[{"certificate":"cidLocal1","completedAt":"2026-10-03T12:00:00Z","blockIndex":"99"}]}`
if strings.HasPrefix(req.URL.Path, "/api/hash/") {
body = `{"receipts":[{"certificate":"cidLocal1","completedAt":"2026-10-03T12:00:00Z","blockIndex":"99"}]}`
}
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
},
}

View file

@ -0,0 +1,158 @@
package pdfcert
import (
"bytes"
"fmt"
"os"
"path/filepath"
"github.com/phpdave11/gofpdf"
qrcode "github.com/skip2/go-qrcode"
)
// Minimal fallback when Node/jsPDF is unavailable.
func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup string) ([]byte, error) {
pdf := gofpdf.New("P", "pt", "Letter", "")
pdf.SetCompression(false)
pdf.SetMargins(48, 48, 48)
pdf.SetAutoPageBreak(false, 0)
pdf.AddPage()
pageW, pageH := pdf.GetPageSize()
margin := 48.0
pdf.SetDrawColor(42, 24, 56)
pdf.SetLineWidth(2)
pdf.Rect(28, 28, pageW-56, pageH-56, "D")
pdf.SetDrawColor(196, 168, 90)
pdf.SetLineWidth(0.6)
pdf.Rect(34, 34, pageW-68, pageH-68, "D")
if p, cleanup, err := materializeLogo(); err == nil {
pdf.ImageOptions(p, margin, 48, 120, 36, false, gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: true}, 0, "")
if cleanup != nil {
defer cleanup()
}
}
pdf.SetTextColor(42, 24, 56)
pdf.SetFont("Times", "B", 18)
pdf.SetXY(margin, 100)
pdf.CellFormat(pageW-margin*2, 20, "Verae Timestamp Registration Document", "", 0, "C", false, 0, "")
pdf.SetTextColor(90, 80, 100)
pdf.SetFont("Helvetica", "", 10)
pdf.SetXY(margin, 122)
pdf.CellFormat(pageW-margin*2, 12, "Official certificate of digital content registration on the VeraeTime global ledger", "", 0, "C", false, 0, "")
pdf.SetDrawColor(196, 168, 90)
pdf.SetLineWidth(1)
pdf.Line(margin, 142, pageW-margin, 142)
if created == "" {
created = "—"
}
fields := [][2]string{
{"Document name", docName},
{"Content SHA-256", in.SHA256},
{"Registration scope", scope},
{"Original timestamp (UTC)", created},
{"Receipt ID (CID)", orDash(in.ReceiptID)},
{"Merkle / block hash", merkle},
{"Ledger location", "VeraeTime global ledger - api.veraetime.net"},
{"Blockchain explorer", "https://explorer.verae.ai/"},
{"Public lookup URL", lookup},
}
if in.ByteLength > 0 {
fields = append([][2]string{
{"Document name", docName},
{"Document size", fmt.Sprintf("%d bytes", in.ByteLength)},
}, fields[1:]...)
}
y := 168.0
for _, fv := range fields {
pdf.SetFont("Helvetica", "B", 10)
pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, y)
pdf.Cell(pageW-margin*2, 12, fv[0])
y += 14
pdf.SetFont("Helvetica", "", 10)
pdf.SetTextColor(40, 40, 50)
for _, ln := range pdf.SplitLines([]byte(fv[1]), pageW-margin*2-10) {
pdf.SetXY(margin, y)
pdf.Cell(pageW-margin*2-10, 12, string(ln))
y += 12
}
y += 10
if y > pageH-220 {
break
}
}
if qrPNG, err := qrcode.Encode(lookup, qrcode.Medium, 280); err == nil {
if qrFile, qclean, qerr := writeTempPNG(qrPNG); qerr == nil {
qrSize := 110.0
qrX := pageW - margin - qrSize
qrY := pageH - margin - qrSize - 36
pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "")
pdf.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100)
pdf.SetXY(qrX, qrY+qrSize+4)
pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, "")
if qclean != nil {
defer qclean()
}
}
}
pdf.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100)
pdf.SetXY(margin, pageH-64)
pdf.MultiCell(pageW-margin*2-130, 10, "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.", "", "L", false)
pdf.SetFont("Helvetica", "B", 9)
pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, pageH-36)
pdf.Cell(pageW-margin*2, 12, "Verae | verae.com | explorer.verae.ai")
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func orDash(s string) string {
if s == "" {
return "—"
}
return s
}
func materializeLogo() (path string, cleanup func(), err error) {
for _, c := range []string{
filepath.Join("assets", "VeraeFullLogo.png"),
"/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png",
} {
if st, e := os.Stat(c); e == nil && st.Size() > 0 {
return c, nil, nil
}
}
if len(embeddedLogo) == 0 {
return "", nil, fmt.Errorf("logo not found")
}
return writeTempPNG(embeddedLogo)
}
func writeTempPNG(b []byte) (path string, cleanup func(), err error) {
f, err := os.CreateTemp("", "verae-png-*.png")
if err != nil {
return "", nil, err
}
if _, err := f.Write(b); err != nil {
f.Close()
os.Remove(f.Name())
return "", nil, err
}
f.Close()
return f.Name(), func() { _ = os.Remove(f.Name()) }, nil
}

View file

@ -3,14 +3,13 @@ package pdfcert
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/phpdave11/gofpdf"
qrcode "github.com/skip2/go-qrcode"
)
//go:embed logo.png
@ -24,7 +23,7 @@ type Input struct {
CreatedAt string
Merkle string
Scope string
VerifyURL string // optional override; must not be mcp.verae.ai
VerifyURL string
Channel string
ByteLength int64
}
@ -33,7 +32,6 @@ type Input struct {
func LookupURL(sha256, receiptID string) string {
sha256 = strings.TrimSpace(sha256)
receiptID = strings.TrimSpace(receiptID)
// Explorer is the public surface; include digest (+ receipt when present).
u := "https://explorer.verae.ai/?sha256=" + sha256
if receiptID != "" {
u += "&receipt=" + receiptID
@ -41,11 +39,13 @@ func LookupURL(sha256, receiptID string) string {
return u
}
// Build returns the branded one-page registration PDF (US Letter).
// Build returns the branded registration PDF using the same jsPDF layout as
// Verae Timestamping.app / register.html export when Node is available.
func Build(in Input) ([]byte, error) {
created := in.CreatedAt
if created == "" {
created = time.Now().UTC().Format(time.RFC3339Nano)
// Do not invent a ledger time — leave blank for the PDF field.
created = ""
}
scope := strings.ToUpper(strings.TrimSpace(in.Scope))
if scope == "" {
@ -64,155 +64,74 @@ func Build(in Input) ([]byte, error) {
docName = "(digest only)"
}
pdf := gofpdf.New("P", "pt", "Letter", "")
pdf.SetCompression(false) // keep certificate text extractable / greppable
pdf.SetMargins(48, 48, 48)
pdf.SetAutoPageBreak(false, 0)
pdf.AddPage()
pageW, pageH := pdf.GetPageSize()
margin := 48.0
// Outer frames (purple + gold) matching register.html / app.html
pdf.SetDrawColor(42, 24, 56)
pdf.SetLineWidth(2)
pdf.Rect(28, 28, pageW-56, pageH-56, "D")
pdf.SetDrawColor(196, 168, 90)
pdf.SetLineWidth(0.6)
pdf.Rect(34, 34, pageW-68, pageH-68, "D")
// Logo
logoPath, cleanup, err := materializeLogo()
if err == nil {
pdf.ImageOptions(logoPath, margin, 48, 120, 36, false, gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: true}, 0, "")
if cleanup != nil {
defer cleanup()
}
row := map[string]any{
"file": docName,
"sha256": in.SHA256,
"scope": scope,
"receipt_id": in.ReceiptID,
"created_at": created,
"merkle": merkle,
"lookup_url": lookup,
"size_bytes": in.ByteLength,
}
pdf.SetTextColor(42, 24, 56)
pdf.SetFont("Times", "B", 18)
pdf.SetXY(margin, 100)
pdf.CellFormat(pageW-margin*2, 20, "Verae Timestamp Registration Document", "", 0, "C", false, 0, "")
pdf.SetTextColor(90, 80, 100)
pdf.SetFont("Helvetica", "", 10)
pdf.SetXY(margin, 122)
pdf.CellFormat(pageW-margin*2, 12, "Official certificate of digital content registration on the VeraeTime global ledger", "", 0, "C", false, 0, "")
pdf.SetDrawColor(196, 168, 90)
pdf.SetLineWidth(1)
pdf.Line(margin, 142, pageW-margin, 142)
fields := [][2]string{
{"Document name", docName},
}
if in.ByteLength > 0 {
fields = append(fields, [2]string{"Document size", fmt.Sprintf("%d bytes", in.ByteLength)})
}
fields = append(fields,
[2]string{"Content SHA-256", in.SHA256},
[2]string{"Registration scope", scope},
[2]string{"Original timestamp (UTC)", created},
[2]string{"Receipt ID (CID)", orDash(in.ReceiptID)},
[2]string{"Merkle / block hash", merkle},
[2]string{"Ledger location", "VeraeTime global ledger - api.veraetime.net"},
[2]string{"Blockchain explorer", "https://explorer.verae.ai/"},
[2]string{"Public lookup URL", lookup},
)
y := 168.0
for _, fv := range fields {
label, value := fv[0], fv[1]
pdf.SetFont("Helvetica", "B", 10)
pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, y)
pdf.Cell(pageW-margin*2, 12, label)
y += 14
pdf.SetFont("Helvetica", "", 10)
pdf.SetTextColor(40, 40, 50)
lines := pdf.SplitLines([]byte(value), pageW-margin*2-10)
for _, ln := range lines {
pdf.SetXY(margin, y)
pdf.Cell(pageW-margin*2-10, 12, string(ln))
y += 12
}
y += 10
if y > pageH-220 {
break
}
if b, err := buildWithNode(row); err == nil && len(b) > 1000 {
return b, nil
}
// QR code → explorer lookup (not mcp.verae.ai)
qrPNG, err := qrcode.Encode(lookup, qrcode.Medium, 280)
if err == nil {
qrFile, qclean, qerr := writeTempPNG(qrPNG)
if qerr == nil {
qrSize := 110.0
qrX := pageW - margin - qrSize
qrY := pageH - margin - qrSize - 36
pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "")
pdf.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100)
pdf.SetXY(qrX, qrY+qrSize+4)
pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, "")
if qclean != nil {
defer qclean()
}
}
return buildFallbackGofpdf(in, docName, created, scope, merkle, lookup)
}
disclaimer := "This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted."
pdf.SetFont("Helvetica", "", 8)
pdf.SetTextColor(90, 80, 100)
pdf.SetXY(margin, pageH-64)
pdf.MultiCell(pageW-margin*2-130, 10, disclaimer, "", "L", false)
pdf.SetFont("Helvetica", "B", 9)
pdf.SetTextColor(42, 24, 56)
pdf.SetXY(margin, pageH-36)
pdf.Cell(pageW-margin*2, 12, "Verae | verae.com | explorer.verae.ai")
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
func buildWithNode(row map[string]any) ([]byte, error) {
script := findBuildScript()
if script == "" {
return nil, fmt.Errorf("registration-pdf build.mjs not found")
}
tmp, err := os.MkdirTemp("", "verae-regpdf-*")
if err != nil {
return nil, err
}
return buf.Bytes(), nil
defer os.RemoveAll(tmp)
inPath := filepath.Join(tmp, "in.json")
outPath := filepath.Join(tmp, "out.pdf")
jb, err := json.Marshal(row)
if err != nil {
return nil, err
}
if err := os.WriteFile(inPath, jb, 0o600); err != nil {
return nil, err
}
cmd := exec.Command("node", script, inPath, outPath)
cmd.Dir = filepath.Dir(script)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("node build.mjs: %w (%s)", err, stderr.String())
}
return os.ReadFile(outPath)
}
func orDash(s string) string {
if strings.TrimSpace(s) == "" {
return "—"
}
return s
}
func materializeLogo() (path string, cleanup func(), err error) {
// Prefer module assets path next to source; fall back to embed
func findBuildScript() string {
candidates := []string{
filepath.Join("assets", "VeraeFullLogo.png"),
"/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png",
"/Users/marchon/research/PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs",
filepath.Join("platform", "registration-pdf", "build.mjs"),
}
// Walk up from executable
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
filepath.Join(dir, "platform", "registration-pdf", "build.mjs"),
filepath.Join(dir, "..", "platform", "registration-pdf", "build.mjs"),
)
}
for _, c := range candidates {
if st, e := os.Stat(c); e == nil && st.Size() > 0 {
return c, nil, nil
if st, err := os.Stat(c); err == nil && !st.IsDir() {
abs, _ := filepath.Abs(c)
return abs
}
}
if len(embeddedLogo) == 0 {
return "", nil, fmt.Errorf("logo not found")
}
return writeTempPNG(embeddedLogo)
return ""
}
func writeTempPNG(b []byte) (path string, cleanup func(), err error) {
f, err := os.CreateTemp("", "verae-logo-*.png")
if err != nil {
return "", nil, err
}
if _, err := f.Write(b); err != nil {
f.Close()
os.Remove(f.Name())
return "", nil, err
}
f.Close()
return f.Name(), func() { _ = os.Remove(f.Name()) }, nil
}
// Ensure created stays empty when unknown — used by tests.
var _ = time.Now

View file

@ -104,31 +104,27 @@ func (c *Client) stampVerae(digest string) (Receipt, error) {
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
if res.StatusCode == http.StatusOK {
cid := str(parsed["certificate"])
if cid == "" {
cid = str(parsed["receipt_id"])
}
if cid != "" || truthy(parsed["existing"]) {
if cid == "" {
cid = str(parsed["certificate"])
}
return Receipt{
ReceiptID: cid,
SHA256: digest,
CreatedAt: str(parsed["timestamp"]),
Merkle: str(parsed["blockHash"]),
Channel: auth.ChannelVerae,
BaseURL: base,
RawResponse: raw,
}, nil
}
}
if res.StatusCode >= 300 {
return Receipt{}, fmt.Errorf("verae stamp HTTP %d: %s", res.StatusCode, truncate(raw, 400))
}
cid, created, merkle := extractVeraeReceipt(parsed)
jobID := str(parsed["jobId"])
// Existing digest (or immediate certificate): prefer receipts[].completedAt
if cid != "" && (truthy(parsed["existing"]) || str(parsed["certificate"]) != "") {
rec := Receipt{
ReceiptID: cid,
SHA256: digest,
CreatedAt: created,
Merkle: merkle,
Channel: auth.ChannelVerae,
BaseURL: base,
RawResponse: raw,
}
return c.enrichFromHash(base, rec)
}
if jobID == "" {
return Receipt{}, fmt.Errorf("verae stamp: missing jobId in %s", truncate(raw, 400))
}
@ -171,36 +167,123 @@ func (c *Client) pollVerae(base, digest, jobID string, firstRaw []byte) (Receipt
if st == "failed" || st == "error" {
return Receipt{}, fmt.Errorf("verae job %s failed: %s", jobID, truncate(raw, 400))
}
cid := str(parsed["receipt_id"])
// Status shape: { status:"completed", result:"Qm…", completedAt:"…" }
cid := str(parsed["result"])
if cid == "" {
cid = str(parsed["certificate"])
cid, _, _ = extractVeraeReceipt(parsed)
}
if cid == "" {
if r, ok := parsed["result"].(map[string]any); ok {
cid = str(r["receipt_id"])
if cid == "" {
cid = str(r["certificate"])
if (cid != "" && (st == "completed" || st == "done")) || (st == "completed" && cid != "") {
created := firstNonEmpty(str(parsed["completedAt"]), str(parsed["timestamp"]), str(parsed["created_at"]))
merkle := firstNonEmpty(str(parsed["blockHash"]), str(parsed["merkle"]), str(parsed["blockIndex"]))
if meta, ok := parsed["metadata"].(map[string]any); ok {
created = firstNonEmpty(str(meta["timestamp"]), str(meta["completedAt"]), created)
merkle = firstNonEmpty(str(meta["blockIndex"]), str(meta["blockHash"]), merkle)
}
}
}
if cid != "" || st == "completed" || st == "done" {
if cid == "" {
cid = jobID
}
return Receipt{
rec := Receipt{
ReceiptID: cid,
SHA256: digest,
CreatedAt: firstNonEmpty(str(parsed["timestamp"]), str(parsed["created_at"]), time.Now().UTC().Format(time.RFC3339)),
Merkle: firstNonEmpty(str(parsed["blockHash"]), str(parsed["merkle"])),
CreatedAt: created,
Merkle: merkle,
Channel: auth.ChannelVerae,
BaseURL: base,
RawResponse: last,
}, nil
}
return c.enrichFromHash(base, rec)
}
if st == "completed" || st == "done" {
// completed but empty result — still try hash lookup
rec := Receipt{SHA256: digest, Channel: auth.ChannelVerae, BaseURL: base, RawResponse: last, CreatedAt: str(parsed["completedAt"])}
rec, err := c.enrichFromHash(base, rec)
if err == nil && rec.ReceiptID != "" {
return rec, nil
}
}
}
return Receipt{}, fmt.Errorf("verae job %s timed out; last=%s", jobID, truncate(last, 400))
}
// extractVeraeReceipt pulls CID + completedAt from stamp / receipts[] payloads.
func extractVeraeReceipt(parsed map[string]any) (cid, created, merkle string) {
cid = firstNonEmpty(str(parsed["certificate"]), str(parsed["receipt_id"]))
created = firstNonEmpty(str(parsed["completedAt"]), str(parsed["timestamp"]), str(parsed["created_at"]))
merkle = firstNonEmpty(str(parsed["blockHash"]), str(parsed["merkle"]), str(parsed["blockIndex"]))
for _, item := range asSlice(parsed["receipts"]) {
m, ok := item.(map[string]any)
if !ok {
continue
}
cid = firstNonEmpty(cid, str(m["certificate"]), str(m["receipt_id"]))
created = firstNonEmpty(created, str(m["completedAt"]), str(m["created_at"]), str(m["timestamp"]))
merkle = firstNonEmpty(merkle, str(m["blockHash"]), str(m["merkle"]), str(m["blockIndex"]))
}
return cid, created, merkle
}
func asSlice(v any) []any {
switch t := v.(type) {
case []any:
return t
default:
return nil
}
}
// enrichFromHash fills missing created_at / merkle / receipt via GET /api/hash/{digest}
// and POST /api/verify {certificate} (blockIndex → merkle).
func (c *Client) enrichFromHash(base string, rec Receipt) (Receipt, error) {
if rec.CreatedAt != "" && rec.Merkle != "" && rec.ReceiptID != "" {
return rec, nil
}
req, err := http.NewRequest(http.MethodGet, base+"/api/hash/"+rec.SHA256, nil)
if err == nil {
if c.VeraeToken != "" {
req.Header.Set("Authorization", "Bearer "+c.VeraeToken)
}
if res, err := c.do(req); err == nil {
raw, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode < 300 {
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
cid, created, merkle := extractVeraeReceipt(parsed)
rec.ReceiptID = firstNonEmpty(rec.ReceiptID, cid)
rec.CreatedAt = firstNonEmpty(rec.CreatedAt, created)
rec.Merkle = firstNonEmpty(rec.Merkle, merkle)
}
}
}
if rec.ReceiptID != "" && rec.Merkle == "" {
body, _ := json.Marshal(map[string]string{"certificate": rec.ReceiptID})
vreq, err := http.NewRequest(http.MethodPost, base+"/api/verify", bytes.NewReader(body))
if err == nil {
vreq.Header.Set("Content-Type", "application/json")
if c.VeraeToken != "" {
vreq.Header.Set("Authorization", "Bearer "+c.VeraeToken)
}
if res, err := c.do(vreq); err == nil {
raw, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode < 300 {
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
rec.CreatedAt = firstNonEmpty(rec.CreatedAt, str(parsed["timestamp"]), str(parsed["completedAt"]))
// blockIndex may be a number
rec.Merkle = firstNonEmpty(rec.Merkle, str(parsed["blockIndex"]), str(parsed["blockHash"]), str(parsed["merkle"]))
if rec.Merkle == "" {
if n, ok := parsed["blockIndex"].(float64); ok {
rec.Merkle = fmt.Sprintf("%.0f", n)
}
}
}
}
}
}
return rec, nil
}
func (c *Client) stampZapier(digest, documentName string) (Receipt, error) {
base := c.zapierBase()
body, _ := json.Marshal(map[string]any{

View file

@ -18,24 +18,32 @@ func TestStampVeraeImmediateCertificate(t *testing.T) {
VeraeBase: "https://api.example.test",
VeraeToken: "tok",
HTTPDo: func(req *http.Request) (*http.Response, error) {
if req.URL.Path != "/api/timestamp" {
switch {
case req.URL.Path == "/api/timestamp":
body := `{"existing":true,"certificate":"QmTestReceipt","receipts":[{"certificate":"QmTestReceipt","digest":"` + digest64 + `","completedAt":"2026-10-03T07:03:17.669231143Z","blockIndex":"113198"}]}`
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
case strings.HasPrefix(req.URL.Path, "/api/hash/"):
body := `{"receipts":[{"certificate":"QmTestReceipt","completedAt":"2026-10-03T07:03:17.669231143Z"}]}`
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
case req.URL.Path == "/api/verify":
body := `{"valid":true,"timestamp":"2026-10-03T07:03:17.669231143Z","blockIndex":113198}`
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
default:
t.Fatalf("path %s", req.URL.Path)
return nil, nil
}
body := `{"certificate":"bafyTestReceipt","timestamp":"2026-10-03T00:00:00Z","existing":true}`
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(body)),
Header: make(http.Header),
}, nil
},
}
r, err := c.Stamp(digest64, "Doc.pdf")
if err != nil {
t.Fatal(err)
}
if r.ReceiptID != "bafyTestReceipt" {
if r.ReceiptID != "QmTestReceipt" {
t.Fatalf("receipt %q", r.ReceiptID)
}
if r.CreatedAt != "2026-10-03T07:03:17.669231143Z" {
t.Fatalf("created_at %q", r.CreatedAt)
}
}
func TestStampZapierAttachPdf(t *testing.T) {

View file

@ -0,0 +1,175 @@
#!/usr/bin/env node
/**
* Official Verae Timestamp Registration Document — same layout as
* mcp.verae.ai register.html / app.html jsPDF export.
*
* Wording overrides (print helper):
* - no mcp.verae.ai URLs
* - "only the digest was submitted" (no "via MCP")
* - footer: Verae | verae.com | explorer.verae.ai
*
* Usage: node build.mjs <input.json> <output.pdf>
* input.json: { file, size_bytes, sha256, scope, receipt_id, created_at, merkle, lookup_url? }
*/
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const __dirname = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
async function loadJsPDF() {
try {
const { jsPDF } = await import("jspdf");
return jsPDF;
} catch {
// npx / global fallbacks not used — deps installed beside this script
throw new Error("jspdf not installed; run: npm install --prefix platform/registration-pdf");
}
}
async function loadQR() {
const QRCode = await import("qrcode");
return QRCode.default || QRCode;
}
function lookupURL(sha, receipt) {
let u = `https://explorer.verae.ai/?sha256=${encodeURIComponent(sha || "")}`;
if (receipt) u += `&receipt=${encodeURIComponent(receipt)}`;
return u;
}
function findLogo() {
const candidates = [
join(__dirname, "../../assets/VeraeFullLogo.png"),
join(__dirname, "VeraeFullLogo.png"),
"/Users/marchon/research/PrintToVeraeTimeStamp/assets/VeraeFullLogo.png",
"/Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server/site/assets/VeraeFullLogo.png",
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return null;
}
async function buildPdf(row) {
const jsPDF = await loadJsPDF();
const QRCode = await loadQR();
const doc = new jsPDF({ unit: "pt", format: "letter" });
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = 48;
const logoPath = findLogo();
let logoData = null;
if (logoPath) {
const b64 = readFileSync(logoPath).toString("base64");
logoData = "data:image/png;base64," + b64;
}
const lookup =
row.lookup_url && !String(row.lookup_url).includes("mcp.verae.ai")
? row.lookup_url
: lookupURL(row.sha256, row.receipt_id);
doc.setDrawColor(42, 24, 56);
doc.setLineWidth(2);
doc.rect(28, 28, pageW - 56, pageH - 56);
doc.setLineWidth(0.6);
doc.setDrawColor(196, 168, 90);
doc.rect(34, 34, pageW - 68, pageH - 68);
if (logoData) {
try {
doc.addImage(logoData, "PNG", margin, 48, 120, 36);
} catch (_) {}
}
doc.setFont("times", "bold");
doc.setFontSize(18);
doc.setTextColor(42, 24, 56);
doc.text("Verae Timestamp Registration Document", pageW / 2, 110, { align: "center" });
doc.setFont("helvetica", "normal");
doc.setFontSize(10);
doc.setTextColor(90, 80, 100);
doc.text(
"Official certificate of digital content registration on the VeraeTime global ledger",
pageW / 2,
128,
{ align: "center" },
);
doc.setDrawColor(196, 168, 90);
doc.setLineWidth(1);
doc.line(margin, 142, pageW - margin, 142);
const fields = [
["Document name", row.file || row.document_name || "(digest only)"],
["Content SHA-256", row.sha256 || ""],
["Registration scope", String(row.scope || "global").toUpperCase()],
["Original timestamp (UTC)", row.created_at || "—"],
["Receipt ID (CID)", row.receipt_id || "—"],
["Merkle / block hash", row.merkle || "See ledger lookup via receipt CID"],
["Ledger location", "VeraeTime global ledger · api.veraetime.net"],
["Blockchain explorer", "https://explorer.verae.ai/"],
["Public lookup URL", lookup],
];
if (row.size_bytes != null && row.size_bytes !== "") {
fields.splice(1, 0, ["Document size", `${row.size_bytes} bytes`]);
}
let y = 168;
doc.setFontSize(10);
for (const [label, value] of fields) {
doc.setFont("helvetica", "bold");
doc.setTextColor(42, 24, 56);
doc.text(label, margin, y);
doc.setFont("helvetica", "normal");
doc.setTextColor(40, 40, 50);
const lines = doc.splitTextToSize(String(value ?? ""), pageW - margin * 2 - 10);
doc.text(lines, margin, y + 14);
y += 14 + lines.length * 12 + 10;
if (y > pageH - 220) break;
}
const qr = await QRCode.toDataURL(lookup, {
errorCorrectionLevel: "M",
margin: 1,
width: 280,
color: { dark: "#2a1838", light: "#ffffff" },
});
const qrSize = 110;
const qrX = pageW - margin - qrSize;
const qrY = pageH - margin - qrSize - 36;
doc.addImage(qr, "PNG", qrX, qrY, qrSize, qrSize);
doc.setFontSize(8);
doc.setTextColor(90, 80, 100);
doc.text("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, {
align: "center",
});
doc.setFontSize(8);
doc.text(
"This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.",
margin,
pageH - 52,
{ maxWidth: pageW - margin * 2 - qrSize - 16 },
);
doc.setFont("helvetica", "bold");
doc.setTextColor(42, 24, 56);
doc.text("Verae · verae.com · explorer.verae.ai", margin, pageH - 36);
return Buffer.from(doc.output("arraybuffer"));
}
const [,, inPath, outPath] = process.argv;
if (!inPath || !outPath) {
console.error("Usage: node build.mjs <input.json> <output.pdf>");
process.exit(2);
}
const row = JSON.parse(readFileSync(inPath, "utf8"));
const pdf = await buildPdf(row);
writeFileSync(outPath, pdf);
console.log(outPath);

View file

@ -0,0 +1,536 @@
{
"name": "registration-pdf",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "registration-pdf",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"jspdf": "^2.5.2",
"qrcode": "^1.5.4"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.50.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"engines": {
"node": "*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dompurify": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
"integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jspdf": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
"integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.5.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
}
}
}

View file

@ -0,0 +1,17 @@
{
"name": "registration-pdf",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"jspdf": "^2.5.2",
"qrcode": "^1.5.4"
}
}