pixelkiln 0.12.0 → 0.12.1

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/CONTRIBUTING.md CHANGED
@@ -15,6 +15,7 @@ npm ci
15
15
  npm run test:release
16
16
  npm run typecheck
17
17
  npm run test:docs
18
+ npm run test:security
18
19
  npm test
19
20
  npm run build
20
21
  npm run test:package
@@ -72,6 +73,9 @@ wire contracts.
72
73
  workflow detail in `docs/`, and link every guide from `docs/README.md`.
73
74
  - Run `npm run test:docs` after changing Markdown or the CLI command/flag
74
75
  surface. It checks local links, the docs index, README size, and CLI coverage.
76
+ - Run `npm run test:security` after changing provider source handling, lockfile
77
+ fixtures, or benchmark state. It scans tracked JSON without printing a
78
+ credential-bearing value.
75
79
  - Run the website lint and build checks when changing `website/` or the
76
80
  canonical Markdown it renders.
77
81
  - Run the package smoke test when changing exports, build configuration, the
@@ -85,6 +89,8 @@ wire contracts.
85
89
  I/O and estimates must carry their cost unit.
86
90
  - Provider responses are untrusted input and need runtime validation before
87
91
  entering the lockfile.
92
+ - Temporary signed URLs must not settle in a committed lockfile. Persist a
93
+ refreshable provider reference and keep storage credentials transient.
88
94
  - Lockfile writes must remain atomic and resumable. Additive defaults should
89
95
  preserve valid v2 files when extending lock entries.
90
96
  - Unknown flags and ambiguous output selection are errors. Silent widening is
package/PROVIDERS.md CHANGED
@@ -26,6 +26,8 @@ them.
26
26
  Live tests now cover single-candidate RD Fast and RD Plus stills from cost quote
27
27
  through submit, poll, PNG download, lockfile provenance, and cache validation.
28
28
  The RD Plus run covered isometric-asset, top-down-asset, and environment styles.
29
+ Lockfiles retain refreshable `retrodiffusion://` result references rather than
30
+ temporary signed storage URLs.
29
31
  Retro Diffusion's multi-candidate review, tileset, GIF, and spritesheet paths
30
32
  have mocked integration coverage but still need representative paid live runs.
31
33
 
package/README.md CHANGED
@@ -57,7 +57,7 @@ PixelKiln keeps the missing record:
57
57
  | Plan and budget | Offline manifest/lock/disk diff, provider-unit estimates, hard `--budget` ceiling, JSON/CI gate. |
58
58
  | Generate and review | Resumable submit/poll/pick/fetch pipeline with a fast local candidate sheet. |
59
59
  | Existing-art onboarding | Manifest scaffolding, exact-hash account adoption, and prompt recovery. |
60
- | Recovery | Validated local content cache, provider URL restore, account object-hash cache, and resumable jobs. |
60
+ | Recovery | Validated local content cache, durable provider-reference restore, account object-hash cache, and resumable jobs. |
61
61
  | Shared-account safety | Cross-project claim files or a registered workspace catalog, sibling-style exclusion, reviewed salvage, keep/discard tags, separate confirmed purge. |
62
62
  | Quality control | Palette distance, transparency, color-count, relative outlier, cache-integrity, and doctor gates. |
63
63
  | Sprite packaging | Deterministic RGBA packing, stable-cell mounting, explicit external input lists, structural output roles. |
package/SECURITY.md CHANGED
@@ -29,10 +29,19 @@ reporter.
29
29
 
30
30
  PixelKiln handles provider credentials, paid API actions, remote object deletion,
31
31
  local output paths, a localhost review server, and generated HTML containing
32
- provider data. Reports involving authentication leakage, path traversal,
32
+ provider data. Temporary provider result URLs can also contain signing
33
+ credentials in their query string. Reports involving authentication leakage, path traversal,
33
34
  cross-origin review actions, HTML/script injection, unsafe overwrite/delete
34
35
  behavior, lockfile corruption, or budget bypass are security relevant.
35
36
 
36
37
  Never attach a real `PIXELLAB_API_KEY`, `RD_API_KEY`, `.env` file, private
37
38
  provider URL, or unredacted lockfile from a confidential project to a public
38
39
  report.
40
+
41
+ PixelKiln keeps durable provider references in lockfiles when an adapter can
42
+ refresh an expiring result. After successful ingestion it removes signed URLs,
43
+ inline data URLs, and local file URLs from settled lock entries. A failed
44
+ download may retain its source locally so `fetch` can retry, but that in-flight
45
+ lockfile should not be committed. `npm run test:security` checks every tracked
46
+ JSON file for credential-bearing URLs and reports only the file and JSON path,
47
+ never the sensitive value.
package/dist/cli.js CHANGED
@@ -1963,6 +1963,7 @@ function firstUrl(urls) {
1963
1963
 
1964
1964
  // src/providers/retrodiffusion.ts
1965
1965
  var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
1966
+ var SOURCE_PROTOCOL = "retrodiffusion:";
1966
1967
  var RetroDiffusionClient = class {
1967
1968
  constructor(token, baseUrl = DEFAULT_BASE_URL2, request = fetch) {
1968
1969
  this.token = token;
@@ -2042,7 +2043,7 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
2042
2043
  return new _RetroDiffusionProvider(new RetroDiffusionClient(void 0));
2043
2044
  }
2044
2045
  static forDownloads() {
2045
- return _RetroDiffusionProvider.forOffline();
2046
+ return new _RetroDiffusionProvider(new RetroDiffusionClient(process.env.RD_API_KEY));
2046
2047
  }
2047
2048
  supports(generator) {
2048
2049
  return generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "animation";
@@ -2148,11 +2149,15 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
2148
2149
  const urls = sources.map((source) => source.url);
2149
2150
  if (!urls.length) return { status: "failed", error: "Retro Diffusion task returned no images" };
2150
2151
  if (urls.length > 1) return { status: "review", candidateUrls: urls };
2152
+ const durableSources = sources.map((source, index) => ({
2153
+ ...source,
2154
+ url: sourceRef2(jobId, index, mediaType)
2155
+ }));
2151
2156
  return {
2152
2157
  status: "ready",
2153
2158
  objectId: `${jobId}#0`,
2154
- sourceUrl: urls[0],
2155
- sources,
2159
+ sourceUrl: durableSources[0].url,
2160
+ sources: durableSources,
2156
2161
  metadata: {
2157
2162
  balanceCost: task.result?.balance_cost ?? null,
2158
2163
  remainingBalance: task.result?.remaining_balance ?? null,
@@ -2171,9 +2176,26 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
2171
2176
  if (task.status !== "succeeded") throw new Error(`Retro Diffusion task ${jobId} is not ready`);
2172
2177
  const url = resultSources(task.result, MediaType.PNG)[index]?.url;
2173
2178
  if (!url) throw new Error(`Retro Diffusion task ${jobId} has no candidate at index ${index}`);
2174
- return { objectId: `${jobId}#${index}`, sourceUrl: url };
2179
+ return { objectId: `${jobId}#${index}`, sourceUrl: sourceRef2(jobId, index, MediaType.PNG) };
2175
2180
  }
2176
2181
  async download(url) {
2182
+ if (url.startsWith(`${SOURCE_PROTOCOL}//`)) {
2183
+ const reference = parseSourceRef(url);
2184
+ const task = await this.client.task(reference.jobId);
2185
+ if (task.status !== "succeeded") {
2186
+ throw new Error(`Retro Diffusion task ${reference.jobId} is not ready`);
2187
+ }
2188
+ const source = resultSources(task.result, reference.mediaType)[reference.index]?.url;
2189
+ if (!source) {
2190
+ throw new Error(
2191
+ `Retro Diffusion task ${reference.jobId} has no output at index ${reference.index}`
2192
+ );
2193
+ }
2194
+ return this.downloadResolved(source);
2195
+ }
2196
+ return this.downloadResolved(url);
2197
+ }
2198
+ async downloadResolved(url) {
2177
2199
  const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
2178
2200
  if (data) return Buffer.from(data, "base64");
2179
2201
  const response = await fetch(url);
@@ -2184,6 +2206,23 @@ var RetroDiffusionProvider = class _RetroDiffusionProvider {
2184
2206
  return { unit: "usd", remaining: await this.client.balance() };
2185
2207
  }
2186
2208
  };
2209
+ function sourceRef2(jobId, index, mediaType) {
2210
+ const url = new URL("retrodiffusion://output");
2211
+ url.searchParams.set("job", jobId);
2212
+ url.searchParams.set("index", String(index));
2213
+ url.searchParams.set("mediaType", mediaType);
2214
+ return url.href;
2215
+ }
2216
+ function parseSourceRef(value) {
2217
+ const url = new URL(value);
2218
+ const jobId = url.searchParams.get("job");
2219
+ const index = Number(url.searchParams.get("index"));
2220
+ const rawMediaType = url.searchParams.get("mediaType");
2221
+ if (url.protocol !== SOURCE_PROTOCOL || url.hostname !== "output" || !jobId || !Number.isInteger(index) || index < 0 || rawMediaType !== MediaType.PNG && rawMediaType !== MediaType.GIF) {
2222
+ throw new Error("Invalid durable Retro Diffusion source reference");
2223
+ }
2224
+ return { jobId, index, mediaType: rawMediaType };
2225
+ }
2187
2226
  function retroOptions(spec) {
2188
2227
  return spec.providerOptions;
2189
2228
  }
@@ -3343,6 +3382,45 @@ async function poll(provider, lock, lockPath, opts = {}) {
3343
3382
  import { existsSync as existsSync6 } from "fs";
3344
3383
  import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3345
3384
  import path9 from "path";
3385
+
3386
+ // src/source-url.ts
3387
+ var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
3388
+ "access_token",
3389
+ "apikey",
3390
+ "api_key",
3391
+ "auth",
3392
+ "authorization",
3393
+ "awsaccesskeyid",
3394
+ "googleaccessid",
3395
+ "key",
3396
+ "sig"
3397
+ ]);
3398
+ function isSensitiveSourceUrl(value) {
3399
+ let parsed;
3400
+ try {
3401
+ parsed = new URL(value);
3402
+ } catch {
3403
+ return false;
3404
+ }
3405
+ if (parsed.username || parsed.password) return true;
3406
+ for (const key of parsed.searchParams.keys()) {
3407
+ const normalized = key.toLowerCase();
3408
+ if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
3409
+ }
3410
+ return false;
3411
+ }
3412
+ function shouldPersistSourceUrl(value) {
3413
+ let parsed;
3414
+ try {
3415
+ parsed = new URL(value);
3416
+ } catch {
3417
+ return true;
3418
+ }
3419
+ if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
3420
+ return !isSensitiveSourceUrl(value);
3421
+ }
3422
+
3423
+ // src/pipeline/fetch.ts
3346
3424
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
3347
3425
  const log2 = opts.onProgress ?? (() => {
3348
3426
  });
@@ -3444,15 +3522,16 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
3444
3522
  await rename2(tmp, target);
3445
3523
  log2(` wrote ${path9.relative(process.cwd(), target)}`);
3446
3524
  }
3525
+ const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
3447
3526
  upsert(lock, key, {
3448
3527
  status: "downloaded",
3449
3528
  provider: provider.id,
3450
3529
  outputs,
3451
- // Inline providers can hand the pipeline a machine-local temp file.
3452
- // Once its bytes are in the durable content cache, retaining that
3453
- // path makes a committed lockfile non-portable and falsely suggests
3454
- // the source still exists on another checkout.
3455
- ...cacheDir && sources.every((source) => source.url.startsWith("file://")) ? { sourceUrl: null, sourceUrls: [] } : {},
3530
+ // A finished lock may be committed. Inline bytes, machine-local
3531
+ // paths, and credential-bearing signed URLs are useful while a
3532
+ // download is retryable but must not survive successful ingestion.
3533
+ sourceUrl: persistentSources[0]?.url ?? null,
3534
+ sourceUrls: persistentSources,
3456
3535
  downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
3457
3536
  error: null
3458
3537
  });