diff --git a/components/attestify/README.md b/components/attestify/README.md new file mode 100644 index 0000000000000..f23b3cd9fb9de --- /dev/null +++ b/components/attestify/README.md @@ -0,0 +1,7 @@ +# Attestify + +Attestify issues tamper-evident, cryptographically-verifiable certificates. Each certificate gets a +permanent public verify page anyone can check — Ed25519-signed, so it can't be forged after issuance. +Free, no signup required. + +Learn more: https://attestify.novadyne.ai diff --git a/components/attestify/actions/issue-certificate/README.md b/components/attestify/actions/issue-certificate/README.md new file mode 100644 index 0000000000000..fc0ee499de8c2 --- /dev/null +++ b/components/attestify/actions/issue-certificate/README.md @@ -0,0 +1,18 @@ +# Issue Certificate + +Issue a tamper-evident, cryptographically-verifiable certificate with Attestify. Each certificate +gets a permanent public verify page anyone can check — Ed25519-signed, so it can't be forged after +issuance. Free, no signup. + +**Props** +- **Organization / Issuer** (required) — shown on the certificate and the public verify page. +- **Course / Credential** (required) — what the certificate is for. +- **Recipient Name** (required) — name on the certificate. +- **Recipient Email** (optional) — echoed back so you can join it to the verify URL for a mail-merge + or LMS step. Never stored in the signed record and never shown on the public verify page. +- **Completion Date** (optional, `YYYY-MM-DD`) — defaults to today. + +**Returns** the issued certificate: `cert_id`, `verify_url` (the permanent public page), +`signed_record_url` (the Ed25519-signed JSON), `cert_image_url`, plus the echoed fields. + +Docs: https://attestify.novadyne.ai diff --git a/components/attestify/actions/issue-certificate/issue-certificate.mjs b/components/attestify/actions/issue-certificate/issue-certificate.mjs new file mode 100644 index 0000000000000..71c4cfe873831 --- /dev/null +++ b/components/attestify/actions/issue-certificate/issue-certificate.mjs @@ -0,0 +1,72 @@ +import app from "../../attestify.app.mjs"; + +export default { + key: "attestify-issue-certificate", + name: "Issue Certificate", + description: + "Issue a tamper-evident, cryptographically-verifiable certificate. Each certificate gets a permanent public verify page anyone can check — Ed25519-signed, so it can't be forged after issuance. Free, no signup. [See the documentation](https://attestify.novadyne.ai)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + props: { + app, + issuer: { propDefinition: [app, "issuer"] }, + course: { propDefinition: [app, "course"] }, + recipientName: { propDefinition: [app, "recipientName"] }, + recipientEmail: { propDefinition: [app, "recipientEmail"] }, + completionDate: { propDefinition: [app, "completionDate"] }, + }, + async run({ $ }) { + if (this.completionDate && !/^\d{4}-\d{2}-\d{2}$/.test(this.completionDate)) { + throw new Error( + `Completion Date must be in YYYY-MM-DD format (got "${this.completionDate}")`, + ); + } + + const recipient = { name: this.recipientName }; + if (this.recipientEmail) recipient.email = this.recipientEmail; + + const body = { + issuer: this.issuer, + course: this.course, + recipients: [recipient], + }; + if (this.completionDate) body.date = this.completionDate; + + const resp = await this.app.issueCertificate($, body); + + if (!resp || resp.ok !== true) { + const err = resp?.error ?? "unknown"; + const detail = resp?.detail ? ` — ${resp.detail}` : ""; + throw new Error(`Attestify error: ${err}${detail}`); + } + + const cert = resp.certs && resp.certs[0]; + if (!cert) { + throw new Error( + "Attestify returned no certificate (the recipient may have been empty, or the request was treated as an automated crawler).", + ); + } + + $.export( + "$summary", + `Issued certificate ${cert.cert_id} for ${cert.recipient_name} — verify at ${cert.verify_url}`, + ); + + return { + cert_id: cert.cert_id, + recipient_name: cert.recipient_name, + recipient_email: cert.recipient_email ?? (this.recipientEmail || undefined), + course: cert.course, + issuer: cert.issuer, + completion_date: this.completionDate || undefined, + verify_url: cert.verify_url, + cert_image_url: cert.cert_url, + signed_record_url: cert.json_url, + }; + }, +}; diff --git a/components/attestify/attestify.app.mjs b/components/attestify/attestify.app.mjs new file mode 100644 index 0000000000000..a33f76f10bef0 --- /dev/null +++ b/components/attestify/attestify.app.mjs @@ -0,0 +1,64 @@ +import { axios } from "@pipedream/platform"; + +// Attestify needs no credentials (free, no signup), so this app declares no auth. +// It exists as the branded namespace + shared HTTP method/prop definitions for the actions. +export default { + type: "app", + app: "attestify", + propDefinitions: { + issuer: { + type: "string", + label: "Organization / Issuer", + description: + "The organization issuing the certificate (shown on the certificate and the public verify page). Example: `Acme Real Estate Academy`.", + }, + course: { + type: "string", + label: "Course / Credential", + description: + "The course or credential being certified. Example: `4-Hour Continuing Education — Ethics`.", + }, + recipientName: { + type: "string", + label: "Recipient Name", + description: "Name of the certificate recipient.", + }, + recipientEmail: { + type: "string", + label: "Recipient Email", + description: + "Optional. Echoed back so you can join it to the verify URL for your mail-merge or LMS. It is **never** stored in the signed record and **never** shown on the public verify page — recipient PII stays on your side.", + optional: true, + }, + completionDate: { + type: "string", + label: "Completion Date", + description: + "Optional. The date the credential was earned, shown on the certificate (format `YYYY-MM-DD`, for example `2026-06-21`). Leave empty to stamp today.", + optional: true, + }, + }, + methods: { + _baseUrl() { + return "https://attestify.novadyne.ai"; + }, + async _request($, { path, headers, ...opts } = {}) { + return axios($, { + ...opts, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + // identify Pipedream-sourced issuances in telemetry (= the demand signal) + "User-Agent": "pipedream-attestify/0.0.1", + }, + }); + }, + async issueCertificate($, data) { + return this._request($, { + method: "POST", + path: "/cert/issue", + data, + }); + }, + }, +}; diff --git a/components/attestify/package.json b/components/attestify/package.json new file mode 100644 index 0000000000000..c162db7a3401f --- /dev/null +++ b/components/attestify/package.json @@ -0,0 +1,21 @@ +{ + "name": "@pipedream/attestify", + "version": "0.0.1", + "description": "Pipedream Attestify Components — issue tamper-evident, cryptographically-verifiable certificates with a permanent public verify page.", + "main": "attestify.app.mjs", + "type": "module", + "keywords": [ + "pipedream", + "attestify", + "certificates", + "verifiable-credentials" + ], + "homepage": "https://pipedream.com/apps/attestify", + "author": "Novadyne", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" + } +}