diff --git a/.gitignore b/.gitignore index f2dc7b6..55dde68 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.pkg *.msi .DS_Store +node_modules/ diff --git a/internal/job/job.go b/internal/job/job.go index 4034178..16796e4 100644 --- a/internal/job/job.go +++ b/internal/job/job.go @@ -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, @@ -148,18 +159,19 @@ func Run(opt Options) (Result, error) { } sidecar := map[string]any{ - "helper_version": HelperVersion, - "document_name": stem + ".pdf", - "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), + "helper_version": HelperVersion, + "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_file": filepath.Base(origPath), "certificate_pdf": filepath.Base(certPath), - "byte_length": len(srcBytes), - "saved_at": time.Now().UTC().Format(time.RFC3339), + "byte_length": len(srcBytes), + "status": "ok", + "saved_at": time.Now().UTC().Format(time.RFC3339), } jsonPath := filepath.Join(outDir, stem+".verae.json") jb, err := json.MarshalIndent(sidecar, "", " ") diff --git a/internal/job/job_test.go b/internal/job/job_test.go index 2012b2e..ce39a14 100644 --- a/internal/job/job_test.go +++ b/internal/job/job_test.go @@ -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 }, } diff --git a/internal/pdfcert/fallback.go b/internal/pdfcert/fallback.go new file mode 100644 index 0000000..e4584d6 --- /dev/null +++ b/internal/pdfcert/fallback.go @@ -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 +} diff --git a/internal/pdfcert/pdfcert.go b/internal/pdfcert/pdfcert.go index 5d99749..72e55f5 100644 --- a/internal/pdfcert/pdfcert.go +++ b/internal/pdfcert/pdfcert.go @@ -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) +} + +func buildWithNode(row map[string]any) ([]byte, error) { + script := findBuildScript() + if script == "" { + return nil, fmt.Errorf("registration-pdf build.mjs not found") } - - 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 { + tmp, err := os.MkdirTemp("", "verae-regpdf-*") + if err != nil { return nil, err } - return buf.Bytes(), nil -} - -func orDash(s string) string { - if strings.TrimSpace(s) == "" { - return "—" + 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 } - return s + 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 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 diff --git a/internal/veraeapi/client.go b/internal/veraeapi/client.go index 1763489..789803a 100644 --- a/internal/veraeapi/client.go +++ b/internal/veraeapi/client.go @@ -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{ diff --git a/internal/veraeapi/client_test.go b/internal/veraeapi/client_test.go index 327656c..4ae0f54 100644 --- a/internal/veraeapi/client_test.go +++ b/internal/veraeapi/client_test.go @@ -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) { diff --git a/platform/registration-pdf/build.mjs b/platform/registration-pdf/build.mjs new file mode 100644 index 0000000..4b1748f --- /dev/null +++ b/platform/registration-pdf/build.mjs @@ -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: { 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 "); + process.exit(2); +} +const row = JSON.parse(readFileSync(inPath, "utf8")); +const pdf = await buildPdf(row); +writeFileSync(outPath, pdf); +console.log(outPath); diff --git a/platform/registration-pdf/package-lock.json b/platform/registration-pdf/package-lock.json new file mode 100644 index 0000000..8ee1b4e --- /dev/null +++ b/platform/registration-pdf/package-lock.json @@ -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" + } + } + } +} diff --git a/platform/registration-pdf/package.json b/platform/registration-pdf/package.json new file mode 100644 index 0000000..764b63e --- /dev/null +++ b/platform/registration-pdf/package.json @@ -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" + } +} \ No newline at end of file