feat(storage): Blossom Phase 2 — BUD-12 delete/list primitives [LOCAL] (#30)

The management primitives, self-contained + unit-tested. Not yet wired into the
S3-shaped orphan-GC / delete-flow (that reconcile system assumes bucket/key
semantics — its own browser-tested increment, same pattern as the upload).

- Vendor entry: export createDeleteAuth (+ rebuilt bundle, still ~27KB).
- blossomService.deleteFile({ server, sha256, signer }) — BUD-12 DELETE /<sha256>
  with a signed kind-24242 delete auth (BUD-11).
- blossomService.listFiles({ server, pubkey, signer }) — BUD-12 GET /list/<pubkey>,
  optional list auth.

Tests: blossom-service 9/9 (added delete/list guards + happy paths). Lint + build
clean.

REMAINING Phase 2: wire deleteFile into video-delete cleanup (resolve
server+sha256 from the blossom storagePointer), orphan-GC parity, and the
kind-10063 discovery UI (import/publish server list — needs relay fetch/publish
plumbing).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-09 16:36:31 -04:00
1 parent c5e8c7f9f1
commit 5935702e75
4 files changed
+89 -1

No files matched your search

+49
View File
@@ -179,6 +179,55 @@ export class BlossomService {
return result;
}
/**
* Delete a blob from one Blossom server (BUD-12 `DELETE /<sha256>`), authorized
* by a signed kind-24242 delete auth (BUD-11). Returns true on success.
*/
async deleteFile({ server, sha256, signer } = {}) {
const url = typeof server === "string" ? server.trim() : "";
const hash = typeof sha256 === "string" ? sha256.trim() : "";
if (!url || !hash) {
throw new Error("Blossom delete requires a server and a sha256.");
}
if (typeof signer !== "function") {
throw new Error("Blossom delete requires a signer.");
}
const sdk = await this.loadSdk();
const { deleteBlob, createDeleteAuth } = sdk;
if (
typeof deleteBlob !== "function" ||
typeof createDeleteAuth !== "function"
) {
throw new Error("Blossom SDK is missing delete exports.");
}
return deleteBlob(url, hash, {
onAuth: () => createDeleteAuth(signer, hash),
});
}
/**
* List a user's blobs on a Blossom server (BUD-12 `GET /list/<pubkey>`). Some
* servers require a signed list auth; provide the signer to cover those.
* Returns the array of blob descriptors.
*/
async listFiles({ server, pubkey, signer } = {}) {
const url = typeof server === "string" ? server.trim() : "";
const pk = typeof pubkey === "string" ? pubkey.trim() : "";
if (!url || !pk) {
throw new Error("Blossom list requires a server and a pubkey.");
}
const sdk = await this.loadSdk();
const { listBlobs, createListAuth } = sdk;
if (typeof listBlobs !== "function") {
throw new Error("Blossom SDK is missing list exports.");
}
const opts =
typeof signer === "function" && typeof createListAuth === "function"
? { onAuth: () => createListAuth(signer) }
: {};
return listBlobs(url, pk, opts);
}
}
const blossomService = new BlossomService();
+1
View File
@@ -9,6 +9,7 @@ export {
createUploadAuth,
createMirrorAuth,
createListAuth,
createDeleteAuth,
encodeAuthorizationHeader,
} from "blossom-client-sdk/auth";
export {
+38
View File
@@ -121,3 +121,41 @@ test("uploadVideo with a torrent uploads the .torrent as a blob and builds a mag
assert.match(result.magnet, /xs=https%3A%2F%2Fa%2F/, "xs = blossom .torrent url");
assert.equal(result.torrentUrl, `https://a/${"b".repeat(64)}`);
});
// --- BUD-12 management primitives (Phase 2) ---
test("deleteFile guards + calls deleteBlob with a delete auth", async () => {
const svc = new BlossomService();
const calls = [];
svc.loadSdk = async () => ({
createDeleteAuth: async (s, hash) => ({ kind: 24242, t: "delete", hash }),
deleteBlob: async (server, hash, opts) => {
if (typeof opts?.onAuth === "function") await opts.onAuth();
calls.push({ server, hash });
return true;
},
});
await assert.rejects(() => svc.deleteFile({ sha256: SHA, signer }), /server and a sha256/);
await assert.rejects(() => svc.deleteFile({ server: "https://a", signer }), /server and a sha256/);
await assert.rejects(
() => svc.deleteFile({ server: "https://a", sha256: SHA, signer: null }),
/requires a signer/,
);
const ok = await svc.deleteFile({ server: "https://a", sha256: SHA, signer });
assert.equal(ok, true);
assert.deepEqual(calls[0], { server: "https://a", hash: SHA });
});
test("listFiles returns the server's blob descriptors for a pubkey", async () => {
const svc = new BlossomService();
svc.loadSdk = async () => ({
createListAuth: async () => ({ kind: 24242, t: "list" }),
listBlobs: async (server, pubkey) => [
{ url: `${server}/${SHA}`, sha256: SHA, size: 1, type: "video/mp4" },
],
});
await assert.rejects(() => svc.listFiles({ pubkey: "pk" }), /server and a pubkey/);
const blobs = await svc.listFiles({ server: "https://a", pubkey: "f".repeat(64), signer });
assert.equal(blobs.length, 1);
assert.equal(blobs[0].sha256, SHA);
});
+1 -1
View File
@@ -1,4 +1,4 @@
var _t=Object.defineProperty;var ue=(n,e,t)=>e in n?_t(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var $=(n,e)=>()=>(n&&(e=n(n=0)),e);var Et=(n,e)=>{for(var t in e)_t(n,t,{get:e[t],enumerable:!0})};var Q=(n,e,t)=>ue(n,typeof e!="symbol"?e+"":e,t);function he(n){return n instanceof Uint8Array||ArrayBuffer.isView(n)&&n.constructor.name==="Uint8Array"}function Y(n,...e){if(!he(n))throw new Error("Uint8Array expected");if(e.length>0&&!e.includes(n.length))throw new Error("Uint8Array expected of length "+e+", got length="+n.length)}function St(n,e=!0){if(n.destroyed)throw new Error("Hash instance has been destroyed");if(e&&n.finished)throw new Error("Hash#digest() has already been called")}function Mt(n,e){Y(n);let t=e.outputLen;if(n.length<t)throw new Error("digestInto() expects output buffer of length at least "+t)}function V(...n){for(let e=0;e<n.length;e++)n[e].fill(0)}function st(n){return new DataView(n.buffer,n.byteOffset,n.byteLength)}function R(n,e){return n<<32-e|n>>>e}function Ft(n){if(Y(n),fe)return n.toHex();let e="";for(let t=0;t<n.length;t++)e+=le[n[t]];return e}function de(n){if(typeof n!="string")throw new Error("string expected");return new Uint8Array(new TextEncoder().encode(n))}function Bt(n){return typeof n=="string"&&(n=de(n)),Y(n),n}function N(n){let e=r=>n().update(Bt(r)).digest(),t=n();return e.outputLen=t.outputLen,e.blockLen=t.blockLen,e.create=()=>n(),e}var fe,le,it,ct=$(()=>{fe=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",le=Array.from({length:256},(n,e)=>e.toString(16).padStart(2,"0"));it=class{}});function xe(n,e,t,r){if(typeof n.setBigUint64=="function")return n.setBigUint64(e,t,r);let o=BigInt(32),i=BigInt(4294967295),a=Number(t>>o&i),s=Number(t&i),u=r?4:0,h=r?0:4;n.setUint32(e+u,a,r),n.setUint32(e+h,s,r)}function kt(n,e,t){return n&e^~n&t}function Dt(n,e,t){return n&e^n&t^e&t}var Z,_,M,E,S,jt=$(()=>{ct();Z=class extends it{constructor(e,t,r,o){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=e,this.outputLen=t,this.padOffset=r,this.isLE=o,this.buffer=new Uint8Array(e),this.view=st(this.buffer)}update(e){St(this),e=Bt(e),Y(e);let{view:t,buffer:r,blockLen:o}=this,i=e.length;for(let a=0;a<i;){let s=Math.min(o-this.pos,i-a);if(s===o){let u=st(e);for(;o<=i-a;a+=o)this.process(u,a);continue}r.set(e.subarray(a,a+s),this.pos),this.pos+=s,a+=s,this.pos===o&&(this.process(t,0),this.pos=0)}return this.length+=e.length,this.roundClean(),this}digestInto(e){St(this),Mt(e,this),this.finished=!0;let{buffer:t,view:r,blockLen:o,isLE:i}=this,{pos:a}=this;t[a++]=128,V(this.buffer.subarray(a)),this.padOffset>o-a&&(this.process(r,0),a=0);for(let c=a;c<o;c++)t[c]=0;xe(r,o-8,BigInt(this.length*8),i),this.process(r,0);let s=st(e),u=this.outputLen;if(u%4)throw new Error("_sha2: outputLen should be aligned to 32bit");let h=u/4,d=this.get();if(h>d.length)throw new Error("_sha2: outputLen bigger than state");for(let c=0;c<h;c++)s.setUint32(4*c,d[c],i)}digest(){let{buffer:e,outputLen:t}=this;this.digestInto(e);let r=e.slice(0,t);return this.destroy(),r}_cloneInto(e){e||(e=new this.constructor),e.set(...this.get());let{blockLen:t,buffer:r,length:o,finished:i,destroyed:a,pos:s}=this;return e.destroyed=a,e.finished=i,e.length=o,e.pos=s,o%t&&e.buffer.set(r),e}clone(){return this._cloneInto()}},_=Uint32Array.from([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),M=Uint32Array.from([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428]),E=Uint32Array.from([3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428]),S=Uint32Array.from([1779033703,4089235720,3144134277,2227873595,1013904242,4271175723,2773480762,1595750129,1359893119,2917565137,2600822924,725511199,528734635,4215389547,1541459225,327033209])});function me(n,e=!1){return e?{h:Number(n&ut),l:Number(n>>Vt&ut)}:{h:Number(n>>Vt&ut)|0,l:Number(n&ut)|0}}function Nt(n,e=!1){let t=n.length,r=new Uint32Array(t),o=new Uint32Array(t);for(let i=0;i<t;i++){let{h:a,l:s}=me(n[i],e);[r[i],o[i]]=[a,s]}return[r,o]}function I(n,e,t,r){let o=(e>>>0)+(r>>>0);return{h:n+t+(o/2**32|0)|0,l:o|0}}var ut,Vt,Ut,Ht,G,W,tt,et,Gt,Wt,Ot,qt,Xt,Kt,$t=$(()=>{ut=BigInt(4294967295),Vt=BigInt(32);Ut=(n,e,t)=>n>>>t,Ht=(n,e,t)=>n<<32-t|e>>>t,G=(n,e,t)=>n>>>t|e<<32-t,W=(n,e,t)=>n<<32-t|e>>>t,tt=(n,e,t)=>n<<64-t|e>>>t-32,et=(n,e,t)=>n>>>t-32|e<<64-t;Gt=(n,e,t)=>(n>>>0)+(e>>>0)+(t>>>0),Wt=(n,e,t,r)=>e+t+r+(n/2**32|0)|0,Ot=(n,e,t,r)=>(n>>>0)+(e>>>0)+(t>>>0)+(r>>>0),qt=(n,e,t,r,o)=>e+t+r+o+(n/2**32|0)|0,Xt=(n,e,t,r,o)=>(n>>>0)+(e>>>0)+(t>>>0)+(r>>>0)+(o>>>0),Kt=(n,e,t,r,o,i)=>e+t+r+o+i+(n/2**32|0)|0});var Qt={};Et(Qt,{SHA224:()=>ht,SHA256:()=>nt,SHA384:()=>ft,SHA512:()=>O,SHA512_224:()=>lt,SHA512_256:()=>dt,sha224:()=>Ae,sha256:()=>ye,sha384:()=>Se,sha512:()=>Ee,sha512_224:()=>Ue,sha512_256:()=>Be});var ge,k,nt,ht,Jt,wLine truncated
var _t=Object.defineProperty;var ue=(n,e,t)=>e in n?_t(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var $=(n,e)=>()=>(n&&(e=n(n=0)),e);var St=(n,e)=>{for(var t in e)_t(n,t,{get:e[t],enumerable:!0})};var Q=(n,e,t)=>ue(n,typeof e!="symbol"?e+"":e,t);function he(n){return n instanceof Uint8Array||ArrayBuffer.isView(n)&&n.constructor.name==="Uint8Array"}function Y(n,...e){if(!he(n))throw new Error("Uint8Array expected");if(e.length>0&&!e.includes(n.length))throw new Error("Uint8Array expected of length "+e+", got length="+n.length)}function Bt(n,e=!0){if(n.destroyed)throw new Error("Hash instance has been destroyed");if(e&&n.finished)throw new Error("Hash#digest() has already been called")}function Mt(n,e){Y(n);let t=e.outputLen;if(n.length<t)throw new Error("digestInto() expects output buffer of length at least "+t)}function V(...n){for(let e=0;e<n.length;e++)n[e].fill(0)}function st(n){return new DataView(n.buffer,n.byteOffset,n.byteLength)}function R(n,e){return n<<32-e|n>>>e}function Ft(n){if(Y(n),fe)return n.toHex();let e="";for(let t=0;t<n.length;t++)e+=le[n[t]];return e}function de(n){if(typeof n!="string")throw new Error("string expected");return new Uint8Array(new TextEncoder().encode(n))}function Ut(n){return typeof n=="string"&&(n=de(n)),Y(n),n}function N(n){let e=r=>n().update(Ut(r)).digest(),t=n();return e.outputLen=t.outputLen,e.blockLen=t.blockLen,e.create=()=>n(),e}var fe,le,it,ct=$(()=>{fe=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",le=Array.from({length:256},(n,e)=>e.toString(16).padStart(2,"0"));it=class{}});function xe(n,e,t,r){if(typeof n.setBigUint64=="function")return n.setBigUint64(e,t,r);let o=BigInt(32),i=BigInt(4294967295),a=Number(t>>o&i),s=Number(t&i),u=r?4:0,h=r?0:4;n.setUint32(e+u,a,r),n.setUint32(e+h,s,r)}function kt(n,e,t){return n&e^~n&t}function Dt(n,e,t){return n&e^n&t^e&t}var Z,_,M,E,S,jt=$(()=>{ct();Z=class extends it{constructor(e,t,r,o){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=e,this.outputLen=t,this.padOffset=r,this.isLE=o,this.buffer=new Uint8Array(e),this.view=st(this.buffer)}update(e){Bt(this),e=Ut(e),Y(e);let{view:t,buffer:r,blockLen:o}=this,i=e.length;for(let a=0;a<i;){let s=Math.min(o-this.pos,i-a);if(s===o){let u=st(e);for(;o<=i-a;a+=o)this.process(u,a);continue}r.set(e.subarray(a,a+s),this.pos),this.pos+=s,a+=s,this.pos===o&&(this.process(t,0),this.pos=0)}return this.length+=e.length,this.roundClean(),this}digestInto(e){Bt(this),Mt(e,this),this.finished=!0;let{buffer:t,view:r,blockLen:o,isLE:i}=this,{pos:a}=this;t[a++]=128,V(this.buffer.subarray(a)),this.padOffset>o-a&&(this.process(r,0),a=0);for(let c=a;c<o;c++)t[c]=0;xe(r,o-8,BigInt(this.length*8),i),this.process(r,0);let s=st(e),u=this.outputLen;if(u%4)throw new Error("_sha2: outputLen should be aligned to 32bit");let h=u/4,d=this.get();if(h>d.length)throw new Error("_sha2: outputLen bigger than state");for(let c=0;c<h;c++)s.setUint32(4*c,d[c],i)}digest(){let{buffer:e,outputLen:t}=this;this.digestInto(e);let r=e.slice(0,t);return this.destroy(),r}_cloneInto(e){e||(e=new this.constructor),e.set(...this.get());let{blockLen:t,buffer:r,length:o,finished:i,destroyed:a,pos:s}=this;return e.destroyed=a,e.finished=i,e.length=o,e.pos=s,o%t&&e.buffer.set(r),e}clone(){return this._cloneInto()}},_=Uint32Array.from([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),M=Uint32Array.from([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428]),E=Uint32Array.from([3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428]),S=Uint32Array.from([1779033703,4089235720,3144134277,2227873595,1013904242,4271175723,2773480762,1595750129,1359893119,2917565137,2600822924,725511199,528734635,4215389547,1541459225,327033209])});function me(n,e=!1){return e?{h:Number(n&ut),l:Number(n>>Vt&ut)}:{h:Number(n>>Vt&ut)|0,l:Number(n&ut)|0}}function Nt(n,e=!1){let t=n.length,r=new Uint32Array(t),o=new Uint32Array(t);for(let i=0;i<t;i++){let{h:a,l:s}=me(n[i],e);[r[i],o[i]]=[a,s]}return[r,o]}function I(n,e,t,r){let o=(e>>>0)+(r>>>0);return{h:n+t+(o/2**32|0)|0,l:o|0}}var ut,Vt,Ht,Lt,G,W,tt,et,Gt,Wt,Ot,qt,Xt,Kt,$t=$(()=>{ut=BigInt(4294967295),Vt=BigInt(32);Ht=(n,e,t)=>n>>>t,Lt=(n,e,t)=>n<<32-t|e>>>t,G=(n,e,t)=>n>>>t|e<<32-t,W=(n,e,t)=>n<<32-t|e>>>t,tt=(n,e,t)=>n<<64-t|e>>>t-32,et=(n,e,t)=>n>>>t-32|e<<64-t;Gt=(n,e,t)=>(n>>>0)+(e>>>0)+(t>>>0),Wt=(n,e,t,r)=>e+t+r+(n/2**32|0)|0,Ot=(n,e,t,r)=>(n>>>0)+(e>>>0)+(t>>>0)+(r>>>0),qt=(n,e,t,r,o)=>e+t+r+o+(n/2**32|0)|0,Xt=(n,e,t,r,o)=>(n>>>0)+(e>>>0)+(t>>>0)+(r>>>0)+(o>>>0),Kt=(n,e,t,r,o,i)=>e+t+r+o+i+(n/2**32|0)|0});var Qt={};St(Qt,{SHA224:()=>ht,SHA256:()=>nt,SHA384:()=>ft,SHA512:()=>O,SHA512_224:()=>lt,SHA512_256:()=>dt,sha224:()=>Ae,sha256:()=>ye,sha384:()=>Se,sha512:()=>Ee,sha512_224:()=>Ue,sha512_256:()=>Be});var ge,k,nt,ht,Jt,wLine truncated
/*! Bundled license information:
@noble/hashes/esm/utils.js: