@artblocks/abx-token-api 0.1.0-alpha.4 → 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/dashboard.js CHANGED
@@ -160,11 +160,13 @@ function timeago(iso) {
160
160
  function esc(s) {
161
161
  return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 to Sepolia and index it in one command:</p>
166
- <pre>pnpm abx demo</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 &lt;address&gt; --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}
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AAElE,mEAAmE;AACnE,SAAS,IAAI,CAAC,KAAa,EAAE,OAAe,EAAE,IAAY;IACxD,OAAO;;;;SAIA,GAAG,CAAC,KAAK,CAAC;SACV,GAAG;;;;kCAIsB,GAAG,CAAC,OAAO,CAAC;;QAEtC,IAAI;;;;eAIG,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAqB,EAAE,OAAe,EAAE,OAAe;IACrF,OAAO,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,QAAQ,EAAE,OAAO,EAAE;;;UAGzD,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,cAAc,CAAC;sDACS,OAAO;;IAEzD,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;AAChD,CAAC;AAED;wFACwF;AACxF,MAAM,UAAU,WAAW,CAAC,QAAwB,EAAE,OAAe,EAAE,OAAe;IACpF,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;IACjG,MAAM,IAAI,GAAG,OAAO;SACjB,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,mBAAmB,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,cAAc,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,cAAc,CACxN;SACA,IAAI,CAAC,EAAE,CAAC,CAAC;IACZ,OAAO,IAAI,CAAC,oBAAoB,EAAE,OAAO,EAAE;;;;;;IAMzC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,gDAAgD,IAAI,iBAAiB,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC5G,CAAC;AAED,SAAS,aAAa,CAAC,CAAe,EAAE,OAAe,EAAE,OAAe;IACtE,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,MAAM,OAAO,IAAI,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW;QAC7B,CAAC,CAAC,8DAA8D;QAChE,CAAC,CAAC,6DAA6D,CAAC;IAElE,OAAO;wCAC+B,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;;yBAE7B,GAAG,CAAC,CAAC,CAAC,aAAa,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,UAAU;;mBAErE,OAAO,MAAM,OAAO,IAAI,CAAC,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,IAAI,GAAG;mBACxD,OAAO,MAAM,OAAO,IAAI,CAAC,CAAC,OAAO;;;;;YAKxC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;gCAEnE,WAAW,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,wDAAwD,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;UACzI,SAAS;;;;UAIT,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,sCAAsC,CAAC,CAAC,CAAC,gDAAgD,CAAC;UACzH,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,qCAAqC,CAAC,CAAC,CAAC,GAAG,CAAC;UAC3J,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;UAC7G,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;UAC1E,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;;;;;UAKpF,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,iCAAiC;;;QAGpJ,iBAAiB,CAAC,CAAC,CAAC;;;;;sCAKU,GAAG,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,UAAU,4BAA4B,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;;;;4BAIpJ,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,iDAAiD,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;;;;;;;;;;;UAW/F,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;aAG/C,CAAC;AACd,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAe;IACxC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CACzC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,EAAC,CAAC,CAAC,CACxE,CAAC;IACF,MAAM,UAAU,GAAG,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,EAAC,CAAC,CAAC,CAAC;IAC/G,MAAM,GAAG,GAAG,CAAC,GAAG,WAAW,EAAE,GAAG,UAAU,CAAC,CAAC;IAC5C,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO;;MAEH,GAAG;SACF,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,iCAAiC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,8BAA8B,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,sCAAsC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAC9L;SACA,IAAI,CAAC,EAAE,CAAC;SACN,CAAC;AACV,CAAC;AAED,SAAS,QAAQ,CAAC,CAAqF,EAAE,OAAe;IACtH,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,0DAA0D,CAAC,CAAC,CAAC,4DAA4D,CAAC;IACzJ,OAAO;UACC,GAAG;sBACS,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;uBACV,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;sBACZ,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC;mBACrB,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,qBAAqB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;QACpF,CAAC;AACT,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,KAAa;IACxC,OAAO,mCAAmC,GAAG,CAAC,KAAK,CAAC,2BAA2B,KAAK,eAAe,CAAC;AACtG,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,OAAe;IAC1C,OAAO,YAAY,WAAW,CAAC,OAAO,CAAC,YAAY,CAAC,qBAAqB,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC9F,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,IAAI,KAAK,uBAAuB;QAAE,OAAO,SAAS,CAAC;IACvD,IAAI,IAAI,KAAK,kCAAkC;QAAE,OAAO,oBAAoB,CAAC;IAC7E,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACjE,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,OAAO,CAAC;IACrC,IAAI,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;IACxD,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;AAC3C,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC;AAC3H,CAAC;AAED,MAAM,WAAW,GAAG;;;;;WAKT,CAAC;AAEZ,MAAM,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgEX,CAAC"}
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
@@ -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;AAE/F,OAAO,EAcL,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;AAqB3E,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,CAchE;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,CAAC,CAO/F;AAgbD,wBAAsB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,EAAE,OAAO,EAAE,cAAc;;;;;;;;;;;;;;;GAetF;AAoED;;;;;;;;;;;GAWG;AACH;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,SAAS,UAAQ,GAChB;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAC,GAAG,IAAI,CAK3C"}
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 { timingSafeEqual } from 'node:crypto';
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 { notifyEffects, watchIntervalMs } from './watcher.js';
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 admin control plane.
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
- sendJson(res, 500, { error: err.message });
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
- // /admin/* — the control plane. This is the ONE write surface on the resolver, and
207
- // it's a *remote `abx add`*: it tells THIS node (a different projection store from
208
- // any local one) which contracts to index. It never signs anything on-chain — the
209
- // "no signing key on the host" rule is intact; the bearer token authorizes indexing
210
- // control only. Disabled unless ABX_RESOLVER_ADMIN_TOKEN is set on the resolver.
211
- if (parts[0] === 'admin' && parts[1] === 'projects') {
212
- return adminProjects(req, res, indexer, parts[2]);
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
- // ADMIN-ONLY: it triggers outbound chain + gateway fetches, so it isn't a public endpoint
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 (!requireAdmin(req, res))
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: