1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
import assert from 'node:assert/strict';
import { finalizeEvent, generateSecretKey, getPublicKey, verifyEvent } from 'nostr-tools/pure';
import { mediaUploadSigner, uploadPublicMedia, uploadStream, MediaUploadError } from '../src/media-upload.js';
const walletKey = generateSecretKey(), loginKey = generateSecretKey();
let walletSigns = 0, loginSigns = 0, resumes = 0;
const wallet = { nostrSign: event => { walletSigns++; return finalizeEvent(event, walletKey); } };
const signer = { signEvent(event) { loginSigns++; return finalizeEvent(event, loginKey); } };
const login = {pubkey: getPublicKey(loginKey), signer};
let id = login, resumed = signer;
const hook = name => name === 'nostrLoginIdentity' ? id : (resumes++, resumed);
const file = new File(['image'], 'test.png', {type: 'image/png'});
const seen = [];
// Haven's owner/allowlist rule: only this login key can upload.
const fetcher = async (url, init) => {
const event = JSON.parse(atob(init.headers.authorization.slice(6)));
assert(verifyEvent(event));
seen.push(event.pubkey);
if (event.pubkey !== login.pubkey) return new Response('', {status:403, headers:{'x-reason':'only media signed by whitelisted pubkeys are allowed'}});
return Response.json({url:'https://haven.example/' + event.tags.find(t => t[0] === 'x')[1]});
};
const options = {servers:['https://haven.example'], fetcher};
await assert.rejects(uploadPublicMedia(file, event => wallet.nostrSign(event), options), e => e instanceof MediaUploadError && e.code === 'unauthorized');
const walletBefore = walletSigns;
await uploadPublicMedia(file, await mediaUploadSigner(wallet, hook), options);
assert.equal(walletSigns, walletBefore);
assert.equal(loginSigns, 1);
assert.equal(resumes, 0);
id = {...login, signer:null};
await uploadPublicMedia(file, await mediaUploadSigner(wallet, hook), options);
assert.equal(resumes, 1);
resumed = null;
await assert.rejects(mediaUploadSigner(wallet, hook), e => e.code === 'no-identity');
assert.equal(walletSigns, walletBefore, 'an unavailable login signer must never fall back to the wallet');
id = null;
const nativeEvent = await (await mediaUploadSigner(wallet, hook))({kind:24242, created_at:1, tags:[], content:''});
assert.equal(nativeEvent.pubkey, getPublicKey(walletKey));
id = login;
await uploadStream({master:'master.m3u8', files:new Map([
['clip.mp4', new File(['clip'], 'clip.mp4', {type:'video/mp4'})],
['master.m3u8', new File(['#EXTM3U\nclip.mp4'], 'master.m3u8', {type:'application/vnd.apple.mpegurl'})],
])}, await mediaUploadSigner(wallet, hook), options);
assert(seen.slice(1).every(pk => pk === login.pubkey));
// Exercise the app's actual routing for both regular files and HLS videos.
const app = await Bun.file('src/app.js').text();
const start = app.indexOf('async function publicMediaUpload(file) {');
const end = app.indexOf('// Whether the wizard', start);
const body = app.slice(start, end).replace("await import('./video-compress.js')", 'video');
const stream = {dim:'10x10',seconds:2};
const signs = [];
const ui = {};
const upload = new Function('wallet','featureHook','mediaUploadSigner','uploadPublicMedia','uploadStream','MediaUploadError','mediaProgress','ui','t','video', body + '\nreturn publicMediaUpload;')(
wallet, hook, mediaUploadSigner,
async (file, sign) => { signs.push(await sign({kind:24242, created_at:1, tags:[], content:''})); return 'photo'; },
async (stream, sign) => { signs.push(await sign({kind:24242, created_at:1, tags:[], content:''})); return 'video'; },
MediaUploadError, () => {}, ui, key => key,
{streamVideo: async () => stream});
assert.equal(await upload(file), 'photo');
assert.equal((await upload(new File(['video'], 'clip.mp4', {type:'video/mp4'}))).url, 'video');
assert(signs.every(e => e.pubkey === login.pubkey));
assert.equal(ui.mediaProgress, null);
console.log('✓ Haven-style allowlist: login identity, signer resume, no wallet fallback, native identity and app file/HLS routing');
|