@artblocks/abx-token-api 0.1.0-alpha.3 → 0.1.0-alpha.5
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/control-plane.d.ts +78 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +522 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +14 -12
- package/dist/dashboard.js.map +1 -1
- package/dist/server.d.ts +0 -26
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +59 -357
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/dist/dashboard.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAsBlE;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ/F;AAED;wFACwF;AACxF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAe9F"}
|
package/dist/dashboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { explorerUrl } from '@artblocks/abx-sdk';
|
|
2
2
|
/** Wrap page content in the shared, dependency-free HTML shell. */
|
|
3
3
|
function page(title, baseUrl, body) {
|
|
4
4
|
return `<!doctype html>
|
|
@@ -67,16 +67,16 @@ function renderProject(s, baseUrl, chainId) {
|
|
|
67
67
|
<div class="info-col">
|
|
68
68
|
<h2>${esc(s.name ?? s.address)} ${s.symbol ? `<span class="sym">${esc(s.symbol)}</span>` : ''}</h2>
|
|
69
69
|
<div class="chip-row">
|
|
70
|
-
<a class="chip" href="${
|
|
70
|
+
<a class="chip" href="${explorerUrl(chainId)}/address/${s.address}" target="_blank" title="View on the block explorer">${shorten(s.address)} ↗</a>
|
|
71
71
|
${canonical}
|
|
72
72
|
</div>
|
|
73
73
|
|
|
74
74
|
<div class="facts">
|
|
75
75
|
${fact('Token #0', tok?.minted ? `<span class="badge ok">minted</span>` : `<span class="badge warn">not yet minted</span>`)}
|
|
76
|
-
${fact('Owner', tok?.minted && tok.owner ? linkAddr(tok.owner) : s.owner ? `${linkAddr(s.owner)} <span class="muted">(admin)</span>` : '—')}
|
|
76
|
+
${fact('Owner', tok?.minted && tok.owner ? linkAddr(tok.owner, chainId) : s.owner ? `${linkAddr(s.owner, chainId)} <span class="muted">(admin)</span>` : '—')}
|
|
77
77
|
${fact('Royalty', s.royalty ? `${(s.royalty.bps / 100).toFixed(2)}% → ${shorten(s.royalty.receiver)}` : 'none')}
|
|
78
78
|
${fact('ABX core version', s.abxVersion !== null ? `v${s.abxVersion}` : '—')}
|
|
79
|
-
${fact('Implementation', s.implementation ? linkAddr(s.implementation) : '—')}
|
|
79
|
+
${fact('Implementation', s.implementation ? linkAddr(s.implementation, chainId) : '—')}
|
|
80
80
|
</div>
|
|
81
81
|
|
|
82
82
|
<div class="exts">
|
|
@@ -104,7 +104,7 @@ function renderProject(s, baseUrl, chainId) {
|
|
|
104
104
|
<table>
|
|
105
105
|
<thead><tr><th></th><th>event</th><th>what it told us</th><th>block</th><th>tx</th></tr></thead>
|
|
106
106
|
<tbody>
|
|
107
|
-
${s.events.map(eventRow).join('')}
|
|
107
|
+
${s.events.map((e) => eventRow(e, chainId)).join('')}
|
|
108
108
|
</tbody>
|
|
109
109
|
</table>
|
|
110
110
|
</section>`;
|
|
@@ -122,21 +122,21 @@ function renderCommitments(s) {
|
|
|
122
122
|
.join('')}
|
|
123
123
|
</div>`;
|
|
124
124
|
}
|
|
125
|
-
function eventRow(e) {
|
|
125
|
+
function eventRow(e, chainId) {
|
|
126
126
|
const reg = e.register === 2 ? `<span class="reg r2" title="Native ABX event">ABX</span>` : `<span class="reg r1" title="Standard ERC event">ERC</span>`;
|
|
127
127
|
return `<tr>
|
|
128
128
|
<td>${reg}</td>
|
|
129
129
|
<td class="evt">${esc(e.name)}</td>
|
|
130
130
|
<td class="what">${esc(e.what)}</td>
|
|
131
131
|
<td class="num">${esc(e.blockNumber)}</td>
|
|
132
|
-
<td><a href="${
|
|
132
|
+
<td><a href="${explorerUrl(chainId)}/tx/${e.txHash}" target="_blank">${shorten(e.txHash)} ↗</a></td>
|
|
133
133
|
</tr>`;
|
|
134
134
|
}
|
|
135
135
|
function fact(label, value) {
|
|
136
136
|
return `<div class="f"><span class="fl">${esc(label)}</span><span class="fv">${value}</span></div>`;
|
|
137
137
|
}
|
|
138
|
-
function linkAddr(a) {
|
|
139
|
-
return `<a href="${
|
|
138
|
+
function linkAddr(a, chainId) {
|
|
139
|
+
return `<a href="${explorerUrl(chainId)}/address/${a}" target="_blank">${shorten(a)} ↗</a>`;
|
|
140
140
|
}
|
|
141
141
|
function friendly(name) {
|
|
142
142
|
if (name === 'abx.extension.royalty')
|
|
@@ -160,11 +160,13 @@ function timeago(iso) {
|
|
|
160
160
|
function esc(s) {
|
|
161
161
|
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
162
162
|
}
|
|
163
|
+
// `abx`, never `pnpm abx` — this page is served to a published user who has no repo and no pnpm
|
|
164
|
+
// (the contributor invocation is `pnpm abx`, and leaking it here is an uncopyable command).
|
|
163
165
|
const EMPTY_STATE = `<section class="empty">
|
|
164
166
|
<h2>No project indexed yet</h2>
|
|
165
|
-
<p>Deploy a 1/1
|
|
166
|
-
<pre>
|
|
167
|
-
<p class="muted">Then refresh this page.</p>
|
|
167
|
+
<p>Deploy a 1/1 and index it in one command:</p>
|
|
168
|
+
<pre>abx demo</pre>
|
|
169
|
+
<p class="muted">Already deployed elsewhere? Register it with this node: <code>abx add <address> --remote</code>. Then refresh this page.</p>
|
|
168
170
|
</section>`;
|
|
169
171
|
const CSS = `
|
|
170
172
|
:root{--bg:#0a0a0f;--raised:#12121a;--card:#16161f;--ink:#e8e8ef;--dim:#9a9aad;--faint:#5b5b70;--accent:#7df9c6;--accent2:#8b7dff;--accent3:#ffb86b;--line:#26263a;--mono:"SF Mono","Fira Code",ui-monospace,Menlo,monospace;--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif}
|
package/dist/dashboard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -62,30 +62,4 @@ export declare function verifyProject(state: ProjectState | null, storage: Stora
|
|
|
62
62
|
}[];
|
|
63
63
|
error?: undefined;
|
|
64
64
|
}>;
|
|
65
|
-
/**
|
|
66
|
-
* `POST /admin/projects {address, fromBlock?, factory?, label?, description?, externalUrl?,
|
|
67
|
-
* attributes?, contentLocators?}` — attributes are off-chain operator traits; contentLocators
|
|
68
|
-
* bridge `{ "0x<keccak>": "ipfs://<cid>" }` so the image points at IPFS without holding bytes.
|
|
69
|
-
* → register the contract with THIS node and replay it from chain. Idempotent: a
|
|
70
|
-
* re-POST is the post-deploy "nudge" that pulls events that landed since.
|
|
71
|
-
* `DELETE /admin/projects/:address` → stop indexing it (drops the projection).
|
|
72
|
-
*
|
|
73
|
-
* We deliberately do NOT factory-scope here: if you own this resolver and you tell it
|
|
74
|
-
* a contract, it does its best to index it. (Allowlisting by factory is a platform
|
|
75
|
-
* concern — see docs/10-backlog.md B6 — not a self-hosting one.)
|
|
76
|
-
*/
|
|
77
|
-
/**
|
|
78
|
-
* Decide the scan floor + whether a full replay is needed when registering a project via the
|
|
79
|
-
* admin control plane. Pure (the discovery/refusal fallback for the null case is the caller's):
|
|
80
|
-
* - explicit `bodyFromBlock` wins; else the `existingFromBlock` already stored.
|
|
81
|
-
* - `null` ⇒ NEITHER supplied nor stored — the caller must derive the deploy block or refuse
|
|
82
|
-
* (never default to genesis: a range-capped RPC would sweep millions of blocks).
|
|
83
|
-
* - `full` is true only when forced, on a first registration (no existing floor), or when the
|
|
84
|
-
* floor actually CHANGED — so re-sending the SAME floor (the CLI now always forwards the deploy
|
|
85
|
-
* block, even on a nudge) stays incremental: `abx add --remote` twice ≠ two full scans.
|
|
86
|
-
*/
|
|
87
|
-
export declare function planRegistrationFloor(bodyFromBlock: string | undefined, existingFromBlock: string | undefined, forceFull?: boolean): {
|
|
88
|
-
fromBlock: string;
|
|
89
|
-
full: boolean;
|
|
90
|
-
} | null;
|
|
91
65
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,MAAM,EAAsB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,MAAM,EAAsB,MAAM,WAAW,CAAC;AAC/F,OAAO,EAaL,KAAK,YAAY,EAEjB,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAiB,KAAK,cAAc,EAAC,MAAM,wBAAwB,CAAC;AAmC3E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yGAAyG;IACzG,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAAA;CAAC,CAAC,CAsB3D;AAaD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAYxF;AAED,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,SAAe,GAAG,MAAM,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAqB3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAwBhE;AAYD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,CAAC,CAO/F;AAsZD,wBAAsB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,EAAE,OAAO,EAAE,cAAc;;;;;;;;;;;;;;;GAetF"}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import {
|
|
3
|
-
import { discoverDeployBlock, fieldOf, makePublicClient, normalizeAttributes, renderArtifactKey, resolveChain, DEFAULT_CHAIN_KEY, verifyAgainstHash, METADATA_FIELD as F, METADATA_REPRESENTATION as R, } from '@artblocks/abx-sdk';
|
|
2
|
+
import { fieldOf, makePublicClient, normalizeAttributes, renderArtifactKey, resolveChain, DEFAULT_CHAIN_KEY, verifyAgainstHash, METADATA_FIELD as F, METADATA_REPRESENTATION as R, } from '@artblocks/abx-sdk';
|
|
4
3
|
import { resolveBackend } from '@artblocks/abx-storage';
|
|
5
4
|
import { fallbackImageSvg, IMAGE_MEDIA_TYPE } from './art.js';
|
|
6
5
|
import { buildContractMetadata, buildTokenMetadata, fieldMimeType } from './metadata.js';
|
|
@@ -9,7 +8,8 @@ import { depStatusReport } from './deps.js';
|
|
|
9
8
|
import { ABX_JS } from './abxjs.js';
|
|
10
9
|
import { resolveFieldBytes, resolveFieldRendered, COLLECTION_TOKEN_ID } from './resolve.js';
|
|
11
10
|
import { renderDashboard, renderIndex } from './dashboard.js';
|
|
12
|
-
import {
|
|
11
|
+
import { watchIntervalMs } from './watcher.js';
|
|
12
|
+
import { requireBearer, routeControlPlane, safeLocators, sendJson, serviceDescriptor, summarize, } from './control-plane.js';
|
|
13
13
|
// A read-only client for resolving on-chain `reader`-represented content (eth_call).
|
|
14
14
|
const chainClient = makePublicClient();
|
|
15
15
|
// The chain this resolver serves. The path grammar carries the chainId
|
|
@@ -17,8 +17,12 @@ const chainClient = makePublicClient();
|
|
|
17
17
|
// paths for chains it doesn't index. Today one resolver = one chain; this gates that.
|
|
18
18
|
const SERVER_CHAIN_ID = resolveChain(process.env.ABX_CHAIN).id;
|
|
19
19
|
// The chain *key* ('sepolia', …) the indexer registers projects under — the string
|
|
20
|
-
// form of the same chain SERVER_CHAIN_ID identifies. Used by the
|
|
20
|
+
// form of the same chain SERVER_CHAIN_ID identifies. Used by the control plane.
|
|
21
21
|
const SERVER_CHAIN_KEY = process.env.ABX_CHAIN ?? DEFAULT_CHAIN_KEY; // MUST match SERVER_CHAIN_ID's default (base-sepolia) — a stale 'sepolia' desynced the key from the id
|
|
22
|
+
/** The context the /v1 control plane + descriptor run against (control-plane.ts owns the routes). */
|
|
23
|
+
function controlPlaneCtx(indexer, storage, baseUrl) {
|
|
24
|
+
return { indexer, storage, chainId: SERVER_CHAIN_ID, chainKey: SERVER_CHAIN_KEY, baseUrl };
|
|
25
|
+
}
|
|
22
26
|
/**
|
|
23
27
|
* Resolve a token's image bytes by dispatching on the `image` field's single active
|
|
24
28
|
* representation (token scope first, else the collection-wide field — the same fallback the
|
|
@@ -130,10 +134,32 @@ export function createTokenApiServer(opts) {
|
|
|
130
134
|
await route(req, res, indexer, baseUrl, storage);
|
|
131
135
|
}
|
|
132
136
|
catch (err) {
|
|
133
|
-
|
|
137
|
+
// NEVER hand a raw internal error to the client. An unexpected failure here is usually an
|
|
138
|
+
// upstream RPC error, and viem's message embeds the full endpoint URL — which for a keyed
|
|
139
|
+
// endpoint IS a credential. On a multi-tenant provider that would leak the operator's RPC key
|
|
140
|
+
// to any tenant who can trigger a 500 (see specs/self-host-toolkit/remote-services.md). So:
|
|
141
|
+
// full detail to the operator's log, a generic + redacted message on the wire.
|
|
142
|
+
const detail = err.message ?? 'unknown error';
|
|
143
|
+
console.error(`[server] ${req.method} ${req.url} failed: ${detail}`);
|
|
144
|
+
sendJson(res, 500, {
|
|
145
|
+
error: `internal error serving ${req.url ?? '/'} — see the node's logs for detail${upstreamHint(detail)}`,
|
|
146
|
+
code: 'internal_error',
|
|
147
|
+
});
|
|
134
148
|
}
|
|
135
149
|
});
|
|
136
150
|
}
|
|
151
|
+
/** A safe, credential-free hint about WHAT class of thing broke, so a caller isn't left blind by the
|
|
152
|
+
* generic 500. Recognizes the common upstream-RPC failures by status text only — never echoes the
|
|
153
|
+
* message (which can embed a keyed endpoint URL). */
|
|
154
|
+
function upstreamHint(detail) {
|
|
155
|
+
if (/\b429\b|Too Many Requests|rate limit/i.test(detail))
|
|
156
|
+
return ' (upstream RPC rate-limited this node — its operator needs a higher-capacity endpoint)';
|
|
157
|
+
if (/\b(401|403)\b|Unauthorized|Forbidden/i.test(detail))
|
|
158
|
+
return " (this node's upstream RPC rejected its credentials)";
|
|
159
|
+
if (/\b5\d\d\b|ECONNREFUSED|ETIMEDOUT|fetch failed/i.test(detail))
|
|
160
|
+
return " (this node's upstream RPC is unreachable or erroring)";
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
137
163
|
export function startTokenApiServer(opts) {
|
|
138
164
|
const port = opts.port ?? Number(process.env.ABX_PORT ?? DEFAULT_PORT);
|
|
139
165
|
const baseUrl = opts.baseUrl ?? resolveBaseUrl(port);
|
|
@@ -147,6 +173,17 @@ async function route(req, res, indexer, baseUrl, storage) {
|
|
|
147
173
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
148
174
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
149
175
|
const method = req.method ?? 'GET';
|
|
176
|
+
// CORS preflight — without this, a browser client can never send Authorization to the
|
|
177
|
+
// control plane (the wildcard origin above covers simple GETs only).
|
|
178
|
+
if (method === 'OPTIONS') {
|
|
179
|
+
res.writeHead(204, {
|
|
180
|
+
'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS',
|
|
181
|
+
'access-control-allow-headers': 'authorization,content-type',
|
|
182
|
+
'access-control-max-age': '86400',
|
|
183
|
+
});
|
|
184
|
+
res.end();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
150
187
|
// GET / — read-only node index: which contracts this resolver serves (no actions).
|
|
151
188
|
if (parts.length === 0) {
|
|
152
189
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
@@ -168,6 +205,12 @@ async function route(req, res, indexer, baseUrl, storage) {
|
|
|
168
205
|
// GET /health
|
|
169
206
|
if (parts[0] === 'health')
|
|
170
207
|
return sendJson(res, 200, { ok: true, baseUrl });
|
|
208
|
+
// GET /.well-known/abx-service — the service descriptor (public): what this node supports,
|
|
209
|
+
// agent-readably, so a client can match a project's needs to this service BEFORE trusting it
|
|
210
|
+
// with a registration. See specs/self-host-toolkit/remote-services.md.
|
|
211
|
+
if (parts[0] === '.well-known' && parts[1] === 'abx-service') {
|
|
212
|
+
return sendJson(res, 200, await serviceDescriptor(controlPlaneCtx(indexer, storage, baseUrl)));
|
|
213
|
+
}
|
|
171
214
|
// GET /abx.js — the runtime companion (directory builds include it; the generator inlines it).
|
|
172
215
|
if (parts[0] === 'abx.js') {
|
|
173
216
|
res.writeHead(200, {
|
|
@@ -203,25 +246,14 @@ async function route(req, res, indexer, baseUrl, storage) {
|
|
|
203
246
|
res.end(view.html);
|
|
204
247
|
return;
|
|
205
248
|
}
|
|
206
|
-
// /
|
|
207
|
-
// it's a *remote `abx add`*: it
|
|
208
|
-
// any local one) which contracts to index.
|
|
209
|
-
// "no signing key on the host" rule is intact; the
|
|
210
|
-
// control only. Disabled unless ABX_RESOLVER_ADMIN_TOKEN is
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// POST /admin/effect-artifacts — a conforming effect runner publishes a render output (a durable
|
|
215
|
-
// locator, or raw bytes for small must-inline outputs like traits) so a resolver that does NOT share
|
|
216
|
-
// the runner's storage disk can still serve it. Same bearer as the control plane; never signs on-chain.
|
|
217
|
-
if (parts[0] === 'admin' && parts[1] === 'effect-artifacts') {
|
|
218
|
-
return adminRenderArtifacts(req, res, indexer, storage);
|
|
219
|
-
}
|
|
220
|
-
// POST /admin/effect-status — a runner reports a run's transient state ('rendering' /
|
|
221
|
-
// 'failed{error}' / 'done'). Pure observability: correctness stays artifact-presence at the
|
|
222
|
-
// settled inputsHash; these rows are rebuildable and 'done' simply clears one. Admin-gated.
|
|
223
|
-
if (parts[0] === 'admin' && parts[1] === 'effect-status') {
|
|
224
|
-
return adminEffectStatus(req, res, indexer);
|
|
249
|
+
// /v1/* — the control plane (register/list/deregister/reindex/status + the effect-publish
|
|
250
|
+
// lane). This is the ONE write surface on the resolver, and it's a *remote `abx add`*: it
|
|
251
|
+
// tells THIS node (a different projection store from any local one) which contracts to index.
|
|
252
|
+
// It never signs anything on-chain — the "no signing key on the host" rule is intact; the
|
|
253
|
+
// bearer token authorizes indexing control only. Disabled unless ABX_RESOLVER_ADMIN_TOKEN is
|
|
254
|
+
// set on the resolver. Routes + auth live in control-plane.ts.
|
|
255
|
+
if (parts[0] === 'v1') {
|
|
256
|
+
return routeControlPlane(req, res, controlPlaneCtx(indexer, storage, baseUrl), parts.slice(1));
|
|
225
257
|
}
|
|
226
258
|
// /api/*
|
|
227
259
|
if (parts[0] === 'api') {
|
|
@@ -247,21 +279,12 @@ async function route(req, res, indexer, baseUrl, storage) {
|
|
|
247
279
|
}
|
|
248
280
|
if (parts[1] === 'project' && parts[2]) {
|
|
249
281
|
const address = parts[2];
|
|
250
|
-
// POST|GET /api/project/:addr/reindex — full replay from chain. ADMIN-ONLY: a full replay
|
|
251
|
-
// is expensive + mutating, so it must never be a public action (it's `abx index --remote`
|
|
252
|
-
// from the CLI). Gated by the same bearer as the control plane.
|
|
253
|
-
if (parts[3] === 'reindex') {
|
|
254
|
-
if (!requireAdmin(req, res))
|
|
255
|
-
return;
|
|
256
|
-
const { state, elapsedMs } = await indexer.reindex(address, { full: true });
|
|
257
|
-
notifyEffects(address);
|
|
258
|
-
return sendJson(res, 200, { elapsedMs, state });
|
|
259
|
-
}
|
|
260
282
|
// GET /api/project/:addr/verify — re-hashes served bytes against the on-chain anchor.
|
|
261
|
-
//
|
|
283
|
+
// BEARER-ONLY: it triggers outbound chain + gateway fetches, so it isn't a public endpoint
|
|
262
284
|
// (anyone can still verify independently via `abx verify` — no need for this node to do it).
|
|
285
|
+
// Reindex moved to the control plane: POST /v1/projects/{chainId}/{address}/reindex.
|
|
263
286
|
if (parts[3] === 'verify') {
|
|
264
|
-
if (!
|
|
287
|
+
if (!requireBearer(req, res))
|
|
265
288
|
return;
|
|
266
289
|
return sendJson(res, 200, await verifyProject(indexer.getProject(address), storage));
|
|
267
290
|
}
|
|
@@ -523,32 +546,6 @@ function safeTokenAttributes(json) {
|
|
|
523
546
|
return undefined;
|
|
524
547
|
}
|
|
525
548
|
}
|
|
526
|
-
/** Parse a stored content-locators JSON column → `{hash: locator}` (lowercased keys, never throws). */
|
|
527
|
-
function safeLocators(json) {
|
|
528
|
-
try {
|
|
529
|
-
const obj = JSON.parse(json);
|
|
530
|
-
const out = {};
|
|
531
|
-
for (const [k, v] of Object.entries(obj))
|
|
532
|
-
if (typeof v === 'string')
|
|
533
|
-
out[k.toLowerCase()] = v;
|
|
534
|
-
return Object.keys(out).length ? out : undefined;
|
|
535
|
-
}
|
|
536
|
-
catch {
|
|
537
|
-
return undefined;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
/** Merge incoming content locators over the stored ones (additive — a re-add can bring new hashes
|
|
541
|
-
* without dropping known ones). Returns a JSON string for the column, or undefined if empty. */
|
|
542
|
-
function mergeLocators(existingJson, incoming) {
|
|
543
|
-
const base = existingJson ? safeLocators(existingJson) ?? {} : {};
|
|
544
|
-
if (incoming && typeof incoming === 'object') {
|
|
545
|
-
for (const [k, v] of Object.entries(incoming)) {
|
|
546
|
-
if (typeof v === 'string' && v)
|
|
547
|
-
base[k.toLowerCase()] = v;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return Object.keys(base).length ? JSON.stringify(base) : undefined;
|
|
551
|
-
}
|
|
552
549
|
export async function verifyProject(state, storage) {
|
|
553
550
|
if (!state)
|
|
554
551
|
return { error: 'unknown project' };
|
|
@@ -564,301 +561,6 @@ export async function verifyProject(state, storage) {
|
|
|
564
561
|
}));
|
|
565
562
|
return { address: state.address, tokens };
|
|
566
563
|
}
|
|
567
|
-
function summarize(s) {
|
|
568
|
-
return {
|
|
569
|
-
address: s.address,
|
|
570
|
-
name: s.name,
|
|
571
|
-
symbol: s.symbol,
|
|
572
|
-
owner: s.owner,
|
|
573
|
-
abxVersion: s.abxVersion,
|
|
574
|
-
isCanonical: s.isCanonical,
|
|
575
|
-
extensions: s.extensions.map((e) => e.name),
|
|
576
|
-
eventCount: s.eventCount,
|
|
577
|
-
tokenCount: s.tokens.length,
|
|
578
|
-
mintedCount: s.tokens.filter((t) => t.minted).length,
|
|
579
|
-
reconstructedAt: s.reconstructedAt,
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
function sendJson(res, status, body) {
|
|
583
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
584
|
-
res.end(JSON.stringify(body, null, 2));
|
|
585
|
-
}
|
|
586
|
-
// ── admin control plane (remote `abx add`) ───────────────────────────────────--
|
|
587
|
-
/** Guard an admin-only API action (reindex/verify): 404 when no admin token is configured on the
|
|
588
|
-
* host (the action surface is disabled), 401 on a missing/bad bearer. Returns false if it handled
|
|
589
|
-
* the response (caller must stop), true when the request is authorized to proceed. */
|
|
590
|
-
function requireAdmin(req, res) {
|
|
591
|
-
if (!process.env.ABX_RESOLVER_ADMIN_TOKEN) {
|
|
592
|
-
sendJson(res, 404, { error: 'admin action disabled — set ABX_RESOLVER_ADMIN_TOKEN on the resolver (operate via the abx CLI)' });
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
if (!adminAuthorized(req)) {
|
|
596
|
-
sendJson(res, 401, { error: 'unauthorized — this action needs Authorization: Bearer <ABX_RESOLVER_ADMIN_TOKEN>' });
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
return true;
|
|
600
|
-
}
|
|
601
|
-
/** Constant-time bearer check against ABX_RESOLVER_ADMIN_TOKEN. */
|
|
602
|
-
function adminAuthorized(req) {
|
|
603
|
-
const token = process.env.ABX_RESOLVER_ADMIN_TOKEN;
|
|
604
|
-
if (!token)
|
|
605
|
-
return false;
|
|
606
|
-
const header = req.headers['authorization'];
|
|
607
|
-
const raw = (Array.isArray(header) ? header[0] : header) ?? '';
|
|
608
|
-
const m = /^Bearer\s+(.+)$/i.exec(raw.trim());
|
|
609
|
-
if (!m)
|
|
610
|
-
return false;
|
|
611
|
-
const got = Buffer.from(m[1]);
|
|
612
|
-
const want = Buffer.from(token);
|
|
613
|
-
return got.length === want.length && timingSafeEqual(got, want);
|
|
614
|
-
}
|
|
615
|
-
/** Read + JSON-parse a request body, capped so a bad caller can't exhaust memory. */
|
|
616
|
-
async function readJsonBody(req, maxBytes = 64 * 1024) {
|
|
617
|
-
const chunks = [];
|
|
618
|
-
let size = 0;
|
|
619
|
-
for await (const chunk of req) {
|
|
620
|
-
size += chunk.length;
|
|
621
|
-
if (size > maxBytes)
|
|
622
|
-
throw new Error('request body too large');
|
|
623
|
-
chunks.push(chunk);
|
|
624
|
-
}
|
|
625
|
-
if (chunks.length === 0)
|
|
626
|
-
return {};
|
|
627
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
628
|
-
}
|
|
629
|
-
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
630
|
-
/**
|
|
631
|
-
* `POST /admin/projects {address, fromBlock?, factory?, label?, description?, externalUrl?,
|
|
632
|
-
* attributes?, contentLocators?}` — attributes are off-chain operator traits; contentLocators
|
|
633
|
-
* bridge `{ "0x<keccak>": "ipfs://<cid>" }` so the image points at IPFS without holding bytes.
|
|
634
|
-
* → register the contract with THIS node and replay it from chain. Idempotent: a
|
|
635
|
-
* re-POST is the post-deploy "nudge" that pulls events that landed since.
|
|
636
|
-
* `DELETE /admin/projects/:address` → stop indexing it (drops the projection).
|
|
637
|
-
*
|
|
638
|
-
* We deliberately do NOT factory-scope here: if you own this resolver and you tell it
|
|
639
|
-
* a contract, it does its best to index it. (Allowlisting by factory is a platform
|
|
640
|
-
* concern — see docs/10-backlog.md B6 — not a self-hosting one.)
|
|
641
|
-
*/
|
|
642
|
-
/**
|
|
643
|
-
* Decide the scan floor + whether a full replay is needed when registering a project via the
|
|
644
|
-
* admin control plane. Pure (the discovery/refusal fallback for the null case is the caller's):
|
|
645
|
-
* - explicit `bodyFromBlock` wins; else the `existingFromBlock` already stored.
|
|
646
|
-
* - `null` ⇒ NEITHER supplied nor stored — the caller must derive the deploy block or refuse
|
|
647
|
-
* (never default to genesis: a range-capped RPC would sweep millions of blocks).
|
|
648
|
-
* - `full` is true only when forced, on a first registration (no existing floor), or when the
|
|
649
|
-
* floor actually CHANGED — so re-sending the SAME floor (the CLI now always forwards the deploy
|
|
650
|
-
* block, even on a nudge) stays incremental: `abx add --remote` twice ≠ two full scans.
|
|
651
|
-
*/
|
|
652
|
-
export function planRegistrationFloor(bodyFromBlock, existingFromBlock, forceFull = false) {
|
|
653
|
-
const fromBlock = bodyFromBlock !== undefined ? String(bodyFromBlock) : existingFromBlock;
|
|
654
|
-
if (fromBlock === undefined)
|
|
655
|
-
return null;
|
|
656
|
-
const full = forceFull || existingFromBlock === undefined || fromBlock !== existingFromBlock;
|
|
657
|
-
return { fromBlock, full };
|
|
658
|
-
}
|
|
659
|
-
async function adminProjects(req, res, indexer, addrSeg) {
|
|
660
|
-
if (!process.env.ABX_RESOLVER_ADMIN_TOKEN) {
|
|
661
|
-
return sendJson(res, 404, {
|
|
662
|
-
error: 'admin API disabled — set ABX_RESOLVER_ADMIN_TOKEN on the resolver to enable remote add/remove',
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
if (!adminAuthorized(req)) {
|
|
666
|
-
return sendJson(res, 401, { error: 'unauthorized — send Authorization: Bearer <ABX_RESOLVER_ADMIN_TOKEN>' });
|
|
667
|
-
}
|
|
668
|
-
const method = req.method ?? 'GET';
|
|
669
|
-
if (method === 'POST' && addrSeg === undefined) {
|
|
670
|
-
let body;
|
|
671
|
-
try {
|
|
672
|
-
body = await readJsonBody(req);
|
|
673
|
-
}
|
|
674
|
-
catch (err) {
|
|
675
|
-
return sendJson(res, 400, { error: err.message });
|
|
676
|
-
}
|
|
677
|
-
const address = body.address;
|
|
678
|
-
if (!address || !ADDRESS_RE.test(address)) {
|
|
679
|
-
return sendJson(res, 400, { error: 'body.address must be a 0x-prefixed 20-byte address' });
|
|
680
|
-
}
|
|
681
|
-
// A re-POST is the post-deploy nudge: preserve the existing scan floor + metadata
|
|
682
|
-
// (don't reset fromBlock and re-scan), and re-index incrementally. A first add — or one
|
|
683
|
-
// that supplies a *new* fromBlock — replays fully from that floor. (See planRegistrationFloor.)
|
|
684
|
-
const existing = indexer.store.getRegistration(address);
|
|
685
|
-
let plan = planRegistrationFloor(body.fromBlock, existing?.fromBlock, body.full === true);
|
|
686
|
-
if (!plan) {
|
|
687
|
-
// No floor supplied and none stored. NEVER default to genesis — a range-capped RPC would
|
|
688
|
-
// grind millions of blocks (the "resolver won't index" trap). Derive the deploy block from
|
|
689
|
-
// chain; refuse if we can't (archive getCode unavailable) rather than guess a bad floor.
|
|
690
|
-
const discovered = await discoverDeployBlock(indexer.publicClient(SERVER_CHAIN_KEY), address);
|
|
691
|
-
if (discovered === null) {
|
|
692
|
-
return sendJson(res, 400, {
|
|
693
|
-
error: 'first registration needs fromBlock (the contract deploy block) — refusing a from-genesis scan. ' +
|
|
694
|
-
'The abx CLI derives it automatically; if calling the API directly, pass fromBlock, or point the resolver at an archive RPC that serves historical eth_getCode.',
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
plan = { fromBlock: discovered.toString(), full: true };
|
|
698
|
-
}
|
|
699
|
-
const { fromBlock, full } = plan;
|
|
700
|
-
// Attributes / locators arrive as JSON values; store them as text. Normalize attributes so a
|
|
701
|
-
// bad payload can't poison the served traits. Each preserves the existing value when omitted.
|
|
702
|
-
let attributes = existing?.attributes;
|
|
703
|
-
if (body.attributes !== undefined) {
|
|
704
|
-
attributes = body.attributes === null ? undefined : JSON.stringify(normalizeAttributes(body.attributes));
|
|
705
|
-
}
|
|
706
|
-
// Per-token off-chain traits (a Series' editable attributes): a `{ "<tokenId>": attrs }` object,
|
|
707
|
-
// each value normalized. Same preserve-on-omit / clear-on-null semantics as `attributes`.
|
|
708
|
-
let tokenAttributes = existing?.tokenAttributes;
|
|
709
|
-
if (body.tokenAttributes !== undefined) {
|
|
710
|
-
if (body.tokenAttributes === null)
|
|
711
|
-
tokenAttributes = undefined;
|
|
712
|
-
else {
|
|
713
|
-
const norm = {};
|
|
714
|
-
for (const [tokenId, v] of Object.entries(body.tokenAttributes)) {
|
|
715
|
-
const a = normalizeAttributes(v);
|
|
716
|
-
if (a.length)
|
|
717
|
-
norm[tokenId] = a;
|
|
718
|
-
}
|
|
719
|
-
tokenAttributes = Object.keys(norm).length ? JSON.stringify(norm) : undefined;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
let contentLocators = existing?.contentLocators;
|
|
723
|
-
if (body.contentLocators !== undefined) {
|
|
724
|
-
contentLocators = mergeLocators(existing?.contentLocators, body.contentLocators);
|
|
725
|
-
}
|
|
726
|
-
indexer.register({
|
|
727
|
-
address: address,
|
|
728
|
-
chainKey: SERVER_CHAIN_KEY,
|
|
729
|
-
fromBlock,
|
|
730
|
-
factory: body.factory ?? existing?.factory ?? process.env.ABX_FACTORY ?? null,
|
|
731
|
-
label: body.label ?? existing?.label,
|
|
732
|
-
description: body.description ?? existing?.description,
|
|
733
|
-
externalUrl: body.externalUrl ?? existing?.externalUrl,
|
|
734
|
-
attributes,
|
|
735
|
-
tokenAttributes,
|
|
736
|
-
contentLocators,
|
|
737
|
-
});
|
|
738
|
-
const { state, elapsedMs, mode } = await indexer.reindex(address, { full });
|
|
739
|
-
notifyEffects(address);
|
|
740
|
-
return sendJson(res, 200, { ok: true, mode, elapsedMs, project: summarize(state) });
|
|
741
|
-
}
|
|
742
|
-
if (method === 'DELETE' && addrSeg) {
|
|
743
|
-
if (!ADDRESS_RE.test(addrSeg))
|
|
744
|
-
return sendJson(res, 400, { error: 'address path segment must be a 0x address' });
|
|
745
|
-
const existed = !!indexer.store.getRegistration(addrSeg);
|
|
746
|
-
indexer.store.deregister(addrSeg);
|
|
747
|
-
return sendJson(res, existed ? 200 : 404, existed ? { ok: true, address: addrSeg } : { error: 'not registered' });
|
|
748
|
-
}
|
|
749
|
-
return sendJson(res, 405, { error: 'use POST /admin/projects or DELETE /admin/projects/<address>' });
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
|
-
* `POST /admin/effect-artifacts {address, tokenId, inputsHash, output?, effectKey?, locator? |
|
|
753
|
-
* bytes_base64?, contentType?}` — a conforming effect runner publishes a render output so a
|
|
754
|
-
* resolver that does NOT share the runner's storage disk can serve it. Two modes:
|
|
755
|
-
* - `locator` (ipfs://<cid> | ar://<txid> | https://…): stored as a pointer; `/image` 302-redirects.
|
|
756
|
-
* - `bytes_base64`: stored in this node's own byte custody (what traits use — they inline into JSON).
|
|
757
|
-
* The artifact key is computed from the runner-supplied `inputsHash` — NOT recomputed from current
|
|
758
|
-
* state — so a render is never re-addressed to a state it doesn't depict (a param change instead makes
|
|
759
|
-
* it unreachable, the correct self-invalidation). Admin-token gated; never signs on-chain.
|
|
760
|
-
*/
|
|
761
|
-
async function adminRenderArtifacts(req, res, indexer, storage) {
|
|
762
|
-
if (!process.env.ABX_RESOLVER_ADMIN_TOKEN) {
|
|
763
|
-
return sendJson(res, 404, { error: 'admin API disabled — set ABX_RESOLVER_ADMIN_TOKEN on the resolver' });
|
|
764
|
-
}
|
|
765
|
-
if (!adminAuthorized(req)) {
|
|
766
|
-
return sendJson(res, 401, { error: 'unauthorized — send Authorization: Bearer <ABX_RESOLVER_ADMIN_TOKEN>' });
|
|
767
|
-
}
|
|
768
|
-
if ((req.method ?? 'GET') !== 'POST') {
|
|
769
|
-
return sendJson(res, 405, { error: 'use POST /admin/effect-artifacts' });
|
|
770
|
-
}
|
|
771
|
-
let body;
|
|
772
|
-
try {
|
|
773
|
-
body = await readJsonBody(req, 8 * 1024 * 1024); // a thumbnail pushed as bytes can exceed the 64KB default
|
|
774
|
-
}
|
|
775
|
-
catch (err) {
|
|
776
|
-
return sendJson(res, 400, { error: err.message });
|
|
777
|
-
}
|
|
778
|
-
const address = body.address;
|
|
779
|
-
if (!address || !ADDRESS_RE.test(address)) {
|
|
780
|
-
return sendJson(res, 400, { error: 'body.address must be a 0x-prefixed 20-byte address' });
|
|
781
|
-
}
|
|
782
|
-
if (body.tokenId === undefined || body.tokenId === null) {
|
|
783
|
-
return sendJson(res, 400, { error: 'body.tokenId required' });
|
|
784
|
-
}
|
|
785
|
-
const tokenId = String(body.tokenId);
|
|
786
|
-
const inputsHashHex = body.inputsHash;
|
|
787
|
-
if (!inputsHashHex || !/^0x[0-9a-fA-F]{64}$/.test(inputsHashHex)) {
|
|
788
|
-
return sendJson(res, 400, { error: 'body.inputsHash must be the 0x 32-byte hash the runner rendered (this node does NOT recompute it)' });
|
|
789
|
-
}
|
|
790
|
-
// Any declared output key (the data plane's generality) — 'image'/'traits' are just the
|
|
791
|
-
// reference render effect's two.
|
|
792
|
-
const output = typeof body.output === 'string' && body.output ? body.output : 'image';
|
|
793
|
-
const effectKey = typeof body.effectKey === 'string' && body.effectKey ? body.effectKey : 'render';
|
|
794
|
-
const contentType = body.contentType ?? (output === 'traits' ? 'application/json' : 'image/png');
|
|
795
|
-
const key = renderArtifactKey(SERVER_CHAIN_ID, address, tokenId, inputsHashHex, output, effectKey);
|
|
796
|
-
// BOTH modes register a row — the row is the `artifacts` manifest's enumeration surface;
|
|
797
|
-
// locator NULL on the bytes mode means "the bytes live in this node's custody at the key".
|
|
798
|
-
const row = { key, address, tokenId, effectKey, outputKey: output, inputsHash: inputsHashHex, contentType };
|
|
799
|
-
const locator = typeof body.locator === 'string' && body.locator ? body.locator : undefined;
|
|
800
|
-
if (locator) {
|
|
801
|
-
indexer.store.putEffectArtifact({ ...row, locator });
|
|
802
|
-
return sendJson(res, 200, { ok: true, mode: 'locator', key, output, locator });
|
|
803
|
-
}
|
|
804
|
-
const bytesB64 = typeof body.bytes_base64 === 'string' && body.bytes_base64 ? body.bytes_base64 : undefined;
|
|
805
|
-
if (bytesB64) {
|
|
806
|
-
const bytes = new Uint8Array(Buffer.from(bytesB64, 'base64'));
|
|
807
|
-
await storage.put(key, { bytes, contentType });
|
|
808
|
-
indexer.store.putEffectArtifact({ ...row, locator: null });
|
|
809
|
-
return sendJson(res, 200, { ok: true, mode: 'bytes', key, output, bytes: bytes.length });
|
|
810
|
-
}
|
|
811
|
-
return sendJson(res, 400, { error: 'provide body.locator (a durable ipfs://ar://https URL) or body.bytes_base64' });
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* `POST /admin/effect-status {key, address, tokenId, effectKey, status, error?, attempts?}` — a
|
|
815
|
-
* runner reports one run's transient state for the artifact `key` it is producing (the runner
|
|
816
|
-
* computes the key; this node never re-derives it, mirroring /admin/effect-artifacts). `status`
|
|
817
|
-
* 'done' clears the row (artifact presence takes over as truth); 'rendering'/'failed' upsert.
|
|
818
|
-
*/
|
|
819
|
-
async function adminEffectStatus(req, res, indexer) {
|
|
820
|
-
if (!process.env.ABX_RESOLVER_ADMIN_TOKEN) {
|
|
821
|
-
return sendJson(res, 404, { error: 'admin API disabled — set ABX_RESOLVER_ADMIN_TOKEN on the resolver' });
|
|
822
|
-
}
|
|
823
|
-
if (!adminAuthorized(req)) {
|
|
824
|
-
return sendJson(res, 401, { error: 'unauthorized — send Authorization: Bearer <ABX_RESOLVER_ADMIN_TOKEN>' });
|
|
825
|
-
}
|
|
826
|
-
if ((req.method ?? 'GET') !== 'POST')
|
|
827
|
-
return sendJson(res, 405, { error: 'use POST /admin/effect-status' });
|
|
828
|
-
let body;
|
|
829
|
-
try {
|
|
830
|
-
body = await readJsonBody(req);
|
|
831
|
-
}
|
|
832
|
-
catch (err) {
|
|
833
|
-
return sendJson(res, 400, { error: err.message });
|
|
834
|
-
}
|
|
835
|
-
const key = body.key;
|
|
836
|
-
if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
|
|
837
|
-
return sendJson(res, 400, { error: 'body.key must be the 0x 32-byte artifact key this run produces' });
|
|
838
|
-
}
|
|
839
|
-
const address = body.address;
|
|
840
|
-
if (!address || !ADDRESS_RE.test(address)) {
|
|
841
|
-
return sendJson(res, 400, { error: 'body.address must be a 0x-prefixed 20-byte address' });
|
|
842
|
-
}
|
|
843
|
-
const status = body.status;
|
|
844
|
-
if (status === 'done') {
|
|
845
|
-
indexer.store.clearEffectStatus(key);
|
|
846
|
-
return sendJson(res, 200, { ok: true, cleared: key });
|
|
847
|
-
}
|
|
848
|
-
if (status !== 'rendering' && status !== 'failed') {
|
|
849
|
-
return sendJson(res, 400, { error: "body.status must be 'rendering' | 'failed' | 'done'" });
|
|
850
|
-
}
|
|
851
|
-
indexer.store.putEffectStatus({
|
|
852
|
-
key,
|
|
853
|
-
address,
|
|
854
|
-
tokenId: String(body.tokenId ?? ''),
|
|
855
|
-
effectKey: typeof body.effectKey === 'string' && body.effectKey ? body.effectKey : 'render',
|
|
856
|
-
status,
|
|
857
|
-
error: typeof body.error === 'string' ? body.error.slice(0, 2000) : null,
|
|
858
|
-
attempts: typeof body.attempts === 'number' ? body.attempts : undefined,
|
|
859
|
-
});
|
|
860
|
-
return sendJson(res, 200, { ok: true, key, status });
|
|
861
|
-
}
|
|
862
564
|
/**
|
|
863
565
|
* Per-token effect status for a project — the "did every thumbnail land?" surface behind
|
|
864
566
|
* `GET /api/project/:addr/effects` and `abx verify`. Status is DERIVED, in precedence order:
|