pixelkiln 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -164,6 +164,7 @@ __export(index_exports, {
164
164
  fontOutputPaths: () => fontOutputPaths,
165
165
  formatCost: () => formatCost,
166
166
  frameSetFps: () => frameSetFps,
167
+ galleryContentSecurityPolicy: () => galleryContentSecurityPolicy,
167
168
  galleryMediaId: () => galleryMediaId,
168
169
  galleryMediaRoute: () => galleryMediaRoute,
169
170
  generateFont: () => generateFont,
@@ -15946,6 +15947,15 @@ var SkeletonAnimationRequestSchema = import_zod10.z.object({
15946
15947
  /** Replace an existing `keypointsFile` with `set`. */
15947
15948
  overwrite: import_zod10.z.boolean().optional()
15948
15949
  }).strict();
15950
+ var SkeletonKeypointsUpdateSchema = import_zod10.z.object({
15951
+ action: import_zod10.z.literal("update-skeleton-keypoints"),
15952
+ project: import_zod10.z.string().min(1).optional(),
15953
+ styleId: import_zod10.z.string().min(1),
15954
+ assetId: import_zod10.z.string().min(1),
15955
+ /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
15956
+ expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
15957
+ set: SkeletonSetSchema
15958
+ }).strict();
15949
15959
  async function createSkeletonAnimation(manifestPath, request) {
15950
15960
  const root = import_node_path29.default.dirname(import_node_path29.default.resolve(manifestPath));
15951
15961
  const file = import_node_path29.default.resolve(root, request.keypointsFile);
@@ -15991,6 +16001,26 @@ async function createSkeletonAnimation(manifestPath, request) {
15991
16001
  throw error;
15992
16002
  }
15993
16003
  }
16004
+ async function updateSkeletonKeypoints(ctx, request) {
16005
+ const key = lockKey(request.styleId, request.assetId);
16006
+ const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
16007
+ if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16008
+ const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16009
+ if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16010
+ const relative = import_node_path29.default.relative(ctx.loaded.root, file);
16011
+ if (relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
16012
+ throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16013
+ }
16014
+ const current = (0, import_node_fs25.existsSync)(file) ? sha256(await (0, import_promises22.readFile)(file)) : null;
16015
+ if (current !== request.expectedSha256) {
16016
+ throw Object.assign(
16017
+ new Error(`${relative.split(import_node_path29.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16018
+ { status: 409 }
16019
+ );
16020
+ }
16021
+ await (0, import_promises22.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16022
+ return relative.split(import_node_path29.default.sep).join("/");
16023
+ }
15994
16024
 
15995
16025
  // src/gallery/edit.ts
15996
16026
  var Base64 = import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
@@ -16067,6 +16097,18 @@ function createGalleryEditHandler(opts) {
16067
16097
  }
16068
16098
  return opts.reload();
16069
16099
  }
16100
+ const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
16101
+ if (poses.success) {
16102
+ if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
16103
+ const written = await updateSkeletonKeypoints(await opts.loadProject(poses.data.project), poses.data);
16104
+ log(` poses saved: ${written} (${poses.data.styleId}/${poses.data.assetId} is stale until generated again)`);
16105
+ return opts.reload();
16106
+ }
16107
+ if (body?.action === "update-skeleton-keypoints") {
16108
+ throw new ManifestEditError(
16109
+ "invalid pose edit: " + poses.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16110
+ );
16111
+ }
16070
16112
  const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
16071
16113
  if (skeleton.success) {
16072
16114
  let manifestPath2;
@@ -16440,7 +16482,8 @@ async function buildGallerySnapshot(opts) {
16440
16482
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
16441
16483
  skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
16442
16484
  keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
16443
- set: spec.revision.skeleton ?? null
16485
+ set: spec.revision.skeleton ?? null,
16486
+ sha256: spec.revision.keypointsSha256 ?? null
16444
16487
  } : null,
16445
16488
  character: describeCharacter(spec, entry),
16446
16489
  outputs,
@@ -16728,12 +16771,47 @@ async function buildWorkspaceGallerySnapshot(opts) {
16728
16771
 
16729
16772
  // src/gallery/page.ts
16730
16773
  var import_node_fs27 = require("fs");
16774
+
16775
+ // src/gallery/client-bundle.ts
16776
+ var import_node_module = require("module");
16777
+ var import_node_url2 = require("url");
16778
+ function bundleGalleryClient(entry = (0, import_node_url2.fileURLToPath)(new URL("./client/src/main.ts", importMetaUrl))) {
16779
+ const esbuild = (0, import_node_module.createRequire)(importMetaUrl)("esbuild");
16780
+ const result = esbuild.buildSync({
16781
+ entryPoints: [entry],
16782
+ bundle: true,
16783
+ format: "iife",
16784
+ target: "es2020",
16785
+ platform: "browser",
16786
+ write: false,
16787
+ legalComments: "none",
16788
+ charset: "utf8"
16789
+ });
16790
+ return result.outputFiles[0].text;
16791
+ }
16792
+
16793
+ // src/gallery/page.ts
16794
+ function galleryContentSecurityPolicy(nonce) {
16795
+ return [
16796
+ "default-src 'self'",
16797
+ `script-src 'nonce-${nonce}'`,
16798
+ `style-src 'nonce-${nonce}'`,
16799
+ "img-src 'self' data: blob:",
16800
+ "frame-src 'self'",
16801
+ "connect-src 'self'",
16802
+ "object-src 'none'",
16803
+ "base-uri 'none'",
16804
+ "form-action 'none'",
16805
+ "frame-ancestors 'none'"
16806
+ ].join("; ");
16807
+ }
16731
16808
  var CLIENT_DIR = new URL("./client/", importMetaUrl);
16732
16809
  var client;
16733
16810
  function clientAssets() {
16811
+ const prebuilt = new URL("gallery.js", CLIENT_DIR);
16734
16812
  client ??= {
16735
16813
  css: (0, import_node_fs27.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
16736
- js: (0, import_node_fs27.readFileSync)(new URL("gallery.js", CLIENT_DIR), "utf8")
16814
+ js: (0, import_node_fs27.existsSync)(prebuilt) ? (0, import_node_fs27.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
16737
16815
  };
16738
16816
  return client;
16739
16817
  }
@@ -16745,6 +16823,7 @@ function renderGallery(snapshot, opts = {}) {
16745
16823
  const editor = JSON.stringify(Boolean(opts.editor && opts.session));
16746
16824
  const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
16747
16825
  const { css, js } = clientAssets();
16826
+ const nonce = opts.nonce ? ` nonce="${escapeHtml2(opts.nonce)}"` : "";
16748
16827
  return `<!doctype html>
16749
16828
  <html lang="en">
16750
16829
  <head>
@@ -16752,7 +16831,7 @@ function renderGallery(snapshot, opts = {}) {
16752
16831
  <meta name="viewport" content="width=device-width, initial-scale=1">
16753
16832
  <title>${escapeHtml2(title)}</title>
16754
16833
  <link rel="icon" href="data:,">
16755
- <style>
16834
+ <style${nonce}>
16756
16835
  ${css}</style>
16757
16836
  </head>
16758
16837
  <body>
@@ -16794,7 +16873,7 @@ ${css}</style>
16794
16873
  <div id="drawer-host"></div>
16795
16874
  <div id="dialog-host"></div>
16796
16875
  <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
16797
- <script>
16876
+ <script${nonce}>
16798
16877
  const INITIAL = ${data};
16799
16878
  const SESSION = ${session};
16800
16879
  const EDITABLE = ${editable};
@@ -17287,6 +17366,10 @@ async function serveGallery(opts) {
17287
17366
  }
17288
17367
  }
17289
17368
  if (req.method !== "GET" && req.method !== "HEAD") return fail(405, "the gallery is read-only");
17369
+ if (url2.pathname === "/api/skeleton") {
17370
+ if (!opts.skeleton) return fail(404, "skeleton estimates need the gallery started with --edit");
17371
+ return json(200, opts.skeleton.status());
17372
+ }
17290
17373
  if (url2.pathname === "/api/jobs") {
17291
17374
  if (!opts.generate) return fail(404, "generation is off");
17292
17375
  return json(200, opts.generate.status());
@@ -17320,12 +17403,15 @@ async function serveGallery(opts) {
17320
17403
  try {
17321
17404
  if (url2.pathname === "/") {
17322
17405
  const { snapshot } = await load();
17406
+ const nonce = (0, import_node_crypto8.randomBytes)(16).toString("base64");
17323
17407
  res.writeHead(200, {
17324
17408
  "Content-Type": "text/html; charset=utf-8",
17325
17409
  "Cache-Control": "no-store",
17326
- "X-Content-Type-Options": "nosniff"
17410
+ "X-Content-Type-Options": "nosniff",
17411
+ "Content-Security-Policy": galleryContentSecurityPolicy(nonce)
17327
17412
  });
17328
17413
  res.end(renderGallery(snapshot, {
17414
+ nonce,
17329
17415
  ...session ? { session } : {},
17330
17416
  editable: Boolean(opts.edit),
17331
17417
  generation: Boolean(opts.generate),
@@ -18499,6 +18585,7 @@ async function workspaceStatus(ws, dir) {
18499
18585
  fontOutputPaths,
18500
18586
  formatCost,
18501
18587
  frameSetFps,
18588
+ galleryContentSecurityPolicy,
18502
18589
  galleryMediaId,
18503
18590
  galleryMediaRoute,
18504
18591
  generateFont,