Files
homarr/apps/workshop/pb_hooks/widget-packages.pb.js
T

116 lines
5.2 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
routerAdd("POST", "/api/workshop/package-releases", (event) => {
const info = event.requestInfo();
if (!info.auth) throw new UnauthorizedError("Sign in to publish widget packages");
const body = info.body;
const source = body.source;
if (!source || source.$schema !== "homarr-widget-package-v3" || !source.manifest || !source.files) {
throw new BadRequestError("Expected a v3 widget source package");
}
const manifest = source.manifest;
if (
!/^[A-Za-z][A-Za-z0-9._-]{0,127}$/.test(manifest.id) ||
!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?$/.test(manifest.version) ||
manifest.sdkVersion !== "1"
) {
throw new BadRequestError("Invalid widget package identity or SDK version");
}
if (!manifest.entrypoints || !Object.prototype.hasOwnProperty.call(source.files, manifest.entrypoints.tile)) {
throw new BadRequestError("The package must contain its tile entrypoint");
}
const canonical = (value) => {
if (Array.isArray(value)) return value.map(canonical);
if (value !== null && typeof value === "object") {
const result = {};
for (const key of Object.keys(value).sort()) result[key] = canonical(value[key]);
return result;
}
return value;
};
const content = JSON.stringify(canonical(source));
if (content.length > 20_000_000) throw new BadRequestError("Widget source exceeds 20 MB");
const sourceDigest = $security.sha256(content);
let artifact = "";
let artifactDigest = "";
if (body.artifact) {
const supplied = body.artifact;
if (supplied.format !== "homarr-widget-artifact-v3" || supplied.sourceDigest !== sourceDigest) {
throw new BadRequestError("Artifact does not identify the supplied source package");
}
const contents = {};
for (const key of Object.keys(supplied)) if (key !== "digest") contents[key] = supplied[key];
artifactDigest = $security.sha256(JSON.stringify(canonical(contents)));
if (artifactDigest !== supplied.digest) throw new BadRequestError("Artifact checksum mismatch");
artifact = JSON.stringify(supplied);
if (artifact.length > 80_000_000) throw new BadRequestError("Artifact exceeds 80 MB");
}
let created;
event.app.runInTransaction((app) => {
let submission;
if (body.submissionId) {
submission = app.findRecordById("submissions", String(body.submissionId));
if (submission.getString("author") !== info.auth.id)
throw new ForbiddenError("Only the author can publish a release");
if (submission.getString("widgetSchema") !== source.$schema)
throw new BadRequestError("Create a separate package submission for v3");
if (Number(body.expectedRevision) !== submission.getInt("revision"))
throw new BadRequestError("Submission changed since it was read");
const previous = JSON.parse(submission.getString("content"));
if (previous.manifest.id !== manifest.id) throw new BadRequestError("A release must retain its package identity");
const current = submission.getInt("revision");
submission.set("expectedRevision", current);
submission.set("revision", current + 1);
} else {
submission = new Record(app.findCollectionByNameOrId("submissions"));
submission.set("type", "customWidget");
submission.set("widgetSchema", source.$schema);
submission.set("author", info.auth.id);
submission.set("revision", 1);
submission.set("expectedRevision", 0);
}
submission.set("title", String(body.title || manifest.name || manifest.id));
submission.set("description", String(body.description ?? manifest.description ?? ""));
submission.set("content", content);
submission.set("changelog", String(body.changelog || ""));
submission.set("outdated", false);
app.save(submission);
const release = new Record(app.findCollectionByNameOrId("widget_releases"));
release.set("submission", submission.id);
release.set("author", info.auth.id);
release.set("packageId", manifest.id);
release.set("version", manifest.version);
release.set("sdkVersion", manifest.sdkVersion);
release.set("content", content);
release.set("sourceDigest", sourceDigest);
release.set("artifact", artifact);
release.set("artifactDigest", artifactDigest);
release.set("changelog", String(body.changelog || ""));
if (body.forkedFrom) {
const origin = app.findRecordById("widget_releases", String(body.forkedFrom));
if (origin.getString("packageId") === manifest.id) throw new BadRequestError("A fork needs its own package ID");
release.set("forkedFrom", origin.id);
}
app.save(release);
created = release;
});
return event.json(201, created);
});
onRecordCreateRequest((event) => {
if (event.record.getString("widgetSchema") === "homarr-widget-package-v3") {
throw new BadRequestError("Publish v3 packages through the package release endpoint");
}
event.next();
}, "submissions");
onRecordUpdateRequest((event) => {
if (
event.record.original().getString("widgetSchema") === "homarr-widget-package-v3" &&
event.record.getString("content") !== event.record.original().getString("content")
) {
throw new BadRequestError("Published package source is immutable; publish a new release");
}
event.next();
}, "submissions");