Add media verification addon with C2PA/ProofMode support
Introduces a new zammad-addon-media-verify package that uses the proofmode Ruby gem (built from proofmode-rust) to verify media attachments on tickets for C2PA content credentials and ProofMode cryptographic proofs. The addon runs as a Zammad scheduled job that: - Scans incoming ticket articles for media attachments (images, video, audio, PDFs, ZIPs) - Calls proofmode check_files() to verify C2PA manifests, PGP signatures, OpenTimestamps, and EXIF metadata - Posts a human-readable verification report as an internal note on the ticket - Tracks checked articles via preferences to avoid duplicate processing Also restores the zammad-addon-common package (previously removed in repo cleanup) to share build tooling (ZPM builder and migration generator) between addon packages, keeping things DRY. The link addon now imports from common instead of inlining these. Docker integration: - Dockerfile updated to install proofmode gem from docker/zammad/gems/ - setup.rb updated to handle MediaVerify package lifecycle https://claude.ai/code/session_01GJYbRCFFJCJDAEcEVbD36N
This commit is contained in:
parent
c40d7d056e
commit
33375c9221
22 changed files with 761 additions and 2821 deletions
19
packages/zammad-addon-common/package.json
Normal file
19
packages/zammad-addon-common/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@link-stack/zammad-addon-common",
|
||||
"version": "3.5.0-beta.1",
|
||||
"description": "Shared build tooling for Zammad addon packages.",
|
||||
"exports": {
|
||||
"./build": "./src/build.ts",
|
||||
"./migrate": "./src/migrate.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.0",
|
||||
"glob": "^11.0.3",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3"
|
||||
},
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-or-later"
|
||||
}
|
||||
88
packages/zammad-addon-common/src/build.ts
Normal file
88
packages/zammad-addon-common/src/build.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { promises as fs } from "fs";
|
||||
import { glob } from "glob";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const log = (msg: string, data?: Record<string, any>) => {
|
||||
console.log(JSON.stringify({ msg, ...data, timestamp: new Date().toISOString() }));
|
||||
};
|
||||
|
||||
const packageFile = async (actualPath: string): Promise<any> => {
|
||||
log('Packaging file', { actualPath });
|
||||
const packagePath = actualPath.slice(4);
|
||||
const data = await fs.readFile(actualPath, "utf-8");
|
||||
const content = Buffer.from(data, "utf-8").toString("base64");
|
||||
const fileStats = await fs.stat(actualPath);
|
||||
const permission = parseInt(
|
||||
(fileStats.mode & 0o777).toString(8).slice(-3),
|
||||
10,
|
||||
);
|
||||
return {
|
||||
location: packagePath,
|
||||
permission,
|
||||
encode: "base64",
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
const packageFiles = async () => {
|
||||
const packagedFiles: any[] = [];
|
||||
const ignoredPatterns = [
|
||||
/\.gitkeep/,
|
||||
/Gemfile/,
|
||||
/Gemfile.lock/,
|
||||
/\.ruby-version/,
|
||||
];
|
||||
|
||||
const processDir = async (dir: string) => {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await processDir(entryPath);
|
||||
} else if (entry.isFile()) {
|
||||
if (!ignoredPatterns.some((pattern) => pattern.test(entry.name))) {
|
||||
packagedFiles.push(await packageFile(entryPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await processDir("./src/");
|
||||
return packagedFiles;
|
||||
};
|
||||
|
||||
export const createZPM = async ({
|
||||
name,
|
||||
displayName,
|
||||
version,
|
||||
}: Record<string, string>) => {
|
||||
const files = await packageFiles();
|
||||
const skeleton = {
|
||||
name: displayName,
|
||||
version,
|
||||
vendor: "Center for Digital Resilience",
|
||||
license: "AGPL-v3+",
|
||||
url: `https://gitlab.com/digiresilience/link/link-stack/packages/${name}`,
|
||||
buildhost: os.hostname(),
|
||||
builddate: new Date().toISOString(),
|
||||
files,
|
||||
};
|
||||
const pkg = JSON.stringify(skeleton, null, 2);
|
||||
|
||||
try {
|
||||
const oldFiles = await glob(`../../docker/zammad/addons/${name}-v*.zpm`, {});
|
||||
|
||||
for (const file of oldFiles) {
|
||||
await fs.unlink(file);
|
||||
log('File was deleted', { file });
|
||||
}
|
||||
} catch (err) {
|
||||
log('Error removing old addon files', { error: String(err) });
|
||||
}
|
||||
await fs.writeFile(
|
||||
`../../docker/zammad/addons/${name}-v${version}.zpm`,
|
||||
pkg,
|
||||
"utf-8",
|
||||
);
|
||||
};
|
||||
43
packages/zammad-addon-common/src/migrate.ts
Normal file
43
packages/zammad-addon-common/src/migrate.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const underscore = (str: string) => {
|
||||
return str
|
||||
.replace(/([a-z\d])([A-Z])/g, "$1_$2")
|
||||
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, "$1_$2")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
const camelize = (str: string): string => {
|
||||
const camelizedStr = str.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
||||
|
||||
return camelizedStr.charAt(0).toUpperCase() + camelizedStr.slice(1);
|
||||
}
|
||||
|
||||
export const createMigration = async ({ displayName }: Record<string, string>) => {
|
||||
const rawName: string = await new Promise((resolve) => {
|
||||
process.stdin.setEncoding("utf-8");
|
||||
process.stdout.write("Enter migration name: ");
|
||||
process.stdin.once("data", (data: string) => {
|
||||
resolve(data.trim());
|
||||
});
|
||||
});
|
||||
|
||||
const migrationBaseName = `${displayName}_${underscore(rawName)}`;
|
||||
const migrationName = camelize(migrationBaseName);
|
||||
const migrationTemplate = `class MIGRATION_NAME < ActiveRecord::Migration[5.2]
|
||||
def self.up
|
||||
# add your code here
|
||||
end
|
||||
|
||||
def self.down
|
||||
# add your code here
|
||||
end
|
||||
end`;
|
||||
const contents = migrationTemplate.replace("MIGRATION_NAME", migrationName);
|
||||
const time = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14);
|
||||
const migrationFileName = `${time}_${migrationBaseName}.rb`;
|
||||
const addonDir = path.join("src", "db", "addon", displayName);
|
||||
await fs.mkdir(addonDir, { recursive: true });
|
||||
await fs.writeFile(path.join(addonDir, migrationFileName), contents);
|
||||
}
|
||||
13
packages/zammad-addon-common/tsconfig.json
Normal file
13
packages/zammad-addon-common/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue