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 +6 -0
- package/PROVIDERS.md +2 -0
- package/README.md +1 -1
- package/SECURITY.md +10 -1
- package/dist/cli.js +88 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +93 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +91 -12
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +10 -2
- package/docs/GETTING_STARTED.md +3 -2
- package/docs/LIBRARY.md +6 -0
- package/docs/QUALITY.md +3 -0
- package/docs/RECOVERY.md +11 -4
- package/docs/RETRO_DIFFUSION.md +7 -0
- package/package.json +2 -1
- package/skills/pixelkiln/references/retro-diffusion.md +4 -0
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
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
3452
|
-
//
|
|
3453
|
-
//
|
|
3454
|
-
|
|
3455
|
-
|
|
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
|
});
|