summaryrefslogtreecommitdiffstats
path: root/internal/assets
diff options
context:
space:
mode:
Diffstat (limited to 'internal/assets')
-rw-r--r--internal/assets/assets.go6
-rw-r--r--internal/assets/web/static/Nym.icobin0 -> 1406 bytes
-rw-r--r--internal/assets/web/static/nacl-util.min.js1
-rw-r--r--internal/assets/web/static/nacl.min.js1
-rw-r--r--internal/assets/web/static/powWorker.js57
-rw-r--r--internal/assets/web/static/style.css150
-rw-r--r--internal/assets/web/templates/index.html1577
7 files changed, 1792 insertions, 0 deletions
diff --git a/internal/assets/assets.go b/internal/assets/assets.go
new file mode 100644
index 0000000..827f15c
--- /dev/null
+++ b/internal/assets/assets.go
@@ -0,0 +1,6 @@
+package assets
+
+import "embed"
+
+//go:embed web/templates/index.html web/static/*
+var FS embed.FS
diff --git a/internal/assets/web/static/Nym.ico b/internal/assets/web/static/Nym.ico
new file mode 100644
index 0000000..26a2943
--- /dev/null
+++ b/internal/assets/web/static/Nym.ico
Binary files differ
diff --git a/internal/assets/web/static/nacl-util.min.js b/internal/assets/web/static/nacl-util.min.js
new file mode 100644
index 0000000..0426742
--- /dev/null
+++ b/internal/assets/web/static/nacl-util.min.js
@@ -0,0 +1 @@
+!function(e,n){"use strict";"undefined"!=typeof module&&module.exports?module.exports=n():(e.nacl||(e.nacl={}),e.nacl.util=n())}(this,function(){"use strict";var e={};function o(e){if(!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(e))throw new TypeError("invalid encoding")}return e.decodeUTF8=function(e){if("string"!=typeof e)throw new TypeError("expected string");var n,r=unescape(encodeURIComponent(e)),t=new Uint8Array(r.length);for(n=0;n<r.length;n++)t[n]=r.charCodeAt(n);return t},e.encodeUTF8=function(e){var n,r=[];for(n=0;n<e.length;n++)r.push(String.fromCharCode(e[n]));return decodeURIComponent(escape(r.join("")))},"undefined"==typeof atob?void 0!==Buffer.from?(e.encodeBase64=function(e){return Buffer.from(e).toString("base64")},e.decodeBase64=function(e){return o(e),new Uint8Array(Array.prototype.slice.call(Buffer.from(e,"base64"),0))}):(e.encodeBase64=function(e){return new Buffer(e).toString("base64")},e.decodeBase64=function(e){return o(e),new Uint8Array(Array.prototype.slice.call(new Buffer(e,"base64"),0))}):(e.encodeBase64=function(e){var n,r=[],t=e.length;for(n=0;n<t;n++)r.push(String.fromCharCode(e[n]));return btoa(r.join(""))},e.decodeBase64=function(e){o(e);var n,r=atob(e),t=new Uint8Array(r.length);for(n=0;n<r.length;n++)t[n]=r.charCodeAt(n);return t}),e}); \ No newline at end of file
diff --git a/internal/assets/web/static/nacl.min.js b/internal/assets/web/static/nacl.min.js
new file mode 100644
index 0000000..65340cc
--- /dev/null
+++ b/internal/assets/web/static/nacl.min.js
@@ -0,0 +1 @@
+!function(i){"use strict";var m=function(r,n){this.hi=0|r,this.lo=0|n},v=function(r){var n,e=new Float64Array(16);if(r)for(n=0;n<r.length;n++)e[n]=r[n];return e},a=function(){throw new Error("no PRNG")},o=new Uint8Array(16),e=new Uint8Array(32);e[0]=9;var c=v(),w=v([1]),g=v([56129,1]),y=v([30883,4953,19914,30187,55467,16705,2637,112,59544,30585,16505,36039,65139,11119,27886,20995]),l=v([61785,9906,39828,60374,45398,33411,5274,224,53552,61171,33010,6542,64743,22239,55772,9222]),t=v([54554,36645,11616,51542,42930,38181,51040,26924,56412,64982,57905,49316,21502,52590,14035,8553]),f=v([26200,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214]),s=v([41136,18958,6951,50414,58488,44335,6150,12099,55207,15867,153,11085,57099,20417,9344,11139]);function h(r,n){return r<<n|r>>>32-n}function b(r,n){var e=255&r[n+3];return(e=(e=e<<8|255&r[n+2])<<8|255&r[n+1])<<8|255&r[n+0]}function B(r,n){var e=r[n]<<24|r[n+1]<<16|r[n+2]<<8|r[n+3],t=r[n+4]<<24|r[n+5]<<16|r[n+6]<<8|r[n+7];return new m(e,t)}function p(r,n,e){var t;for(t=0;t<4;t++)r[n+t]=255&e,e>>>=8}function S(r,n,e){r[n]=e.hi>>24&255,r[n+1]=e.hi>>16&255,r[n+2]=e.hi>>8&255,r[n+3]=255&e.hi,r[n+4]=e.lo>>24&255,r[n+5]=e.lo>>16&255,r[n+6]=e.lo>>8&255,r[n+7]=255&e.lo}function u(r,n,e,t,o){var i,a=0;for(i=0;i<o;i++)a|=r[n+i]^e[t+i];return(1&a-1>>>8)-1}function A(r,n,e,t){return u(r,n,e,t,16)}function _(r,n,e,t){return u(r,n,e,t,32)}function U(r,n,e,t,o){var i,a,f,u=new Uint32Array(16),c=new Uint32Array(16),w=new Uint32Array(16),y=new Uint32Array(4);for(i=0;i<4;i++)c[5*i]=b(t,4*i),c[1+i]=b(e,4*i),c[6+i]=b(n,4*i),c[11+i]=b(e,16+4*i);for(i=0;i<16;i++)w[i]=c[i];for(i=0;i<20;i++){for(a=0;a<4;a++){for(f=0;f<4;f++)y[f]=c[(5*a+4*f)%16];for(y[1]^=h(y[0]+y[3]|0,7),y[2]^=h(y[1]+y[0]|0,9),y[3]^=h(y[2]+y[1]|0,13),y[0]^=h(y[3]+y[2]|0,18),f=0;f<4;f++)u[4*a+(a+f)%4]=y[f]}for(f=0;f<16;f++)c[f]=u[f]}if(o){for(i=0;i<16;i++)c[i]=c[i]+w[i]|0;for(i=0;i<4;i++)c[5*i]=c[5*i]-b(t,4*i)|0,c[6+i]=c[6+i]-b(n,4*i)|0;for(i=0;i<4;i++)p(r,4*i,c[5*i]),p(r,16+4*i,c[6+i])}else for(i=0;i<16;i++)p(r,4*i,c[i]+w[i]|0)}function E(r,n,e,t){U(r,n,e,t,!1)}function x(r,n,e,t){return U(r,n,e,t,!0),0}var d=new Uint8Array([101,120,112,97,110,100,32,51,50,45,98,121,116,101,32,107]);function K(r,n,e,t,o,i,a){var f,u,c=new Uint8Array(16),w=new Uint8Array(64);if(!o)return 0;for(u=0;u<16;u++)c[u]=0;for(u=0;u<8;u++)c[u]=i[u];for(;64<=o;){for(E(w,c,a,d),u=0;u<64;u++)r[n+u]=(e?e[t+u]:0)^w[u];for(f=1,u=8;u<16;u++)f=f+(255&c[u])|0,c[u]=255&f,f>>>=8;o-=64,n+=64,e&&(t+=64)}if(0<o)for(E(w,c,a,d),u=0;u<o;u++)r[n+u]=(e?e[t+u]:0)^w[u];return 0}function Y(r,n,e,t,o){return K(r,n,null,0,e,t,o)}function L(r,n,e,t,o){var i=new Uint8Array(32);return x(i,t,o,d),Y(r,n,e,t.subarray(16),i)}function T(r,n,e,t,o,i,a){var f=new Uint8Array(32);return x(f,i,a,d),K(r,n,e,t,o,i.subarray(16),f)}function k(r,n){var e,t=0;for(e=0;e<17;e++)t=t+(r[e]+n[e]|0)|0,r[e]=255&t,t>>>=8}var z=new Uint32Array([5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,252]);function R(r,n,e,t,o,i){var a,f,u,c,w=new Uint32Array(17),y=new Uint32Array(17),l=new Uint32Array(17),s=new Uint32Array(17),h=new Uint32Array(17);for(u=0;u<17;u++)y[u]=l[u]=0;for(u=0;u<16;u++)y[u]=i[u];for(y[3]&=15,y[4]&=252,y[7]&=15,y[8]&=252,y[11]&=15,y[12]&=252,y[15]&=15;0<o;){for(u=0;u<17;u++)s[u]=0;for(u=0;u<16&&u<o;++u)s[u]=e[t+u];for(s[u]=1,t+=u,o-=u,k(l,s),f=0;f<17;f++)for(u=w[f]=0;u<17;u++)w[f]=w[f]+l[u]*(u<=f?y[f-u]:320*y[f+17-u]|0)|0;for(f=0;f<17;f++)l[f]=w[f];for(u=c=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;for(c=c+l[16]|0,l[16]=3&c,c=5*(c>>>2)|0,u=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;c=c+l[16]|0,l[16]=c}for(u=0;u<17;u++)h[u]=l[u];for(k(l,z),a=0|-(l[16]>>>7),u=0;u<17;u++)l[u]^=a&(h[u]^l[u]);for(u=0;u<16;u++)s[u]=i[u+16];for(s[16]=0,k(l,s),u=0;u<16;u++)r[n+u]=l[u];return 0}function P(r,n,e,t,o,i){var a=new Uint8Array(16);return R(a,0,e,t,o,i),A(r,n,a,0)}function M(r,n,e,t,o){var i;if(e<32)return-1;for(T(r,0,n,0,e,t,o),R(r,16,r,32,e-32,r),i=0;i<16;i++)r[i]=0;return 0}function N(r,n,e,t,o){var i,a=new Uint8Array(32);if(e<32)return-1;if(L(a,0,32,t,o),0!==P(n,16,n,32,e-32,a))return-1;for(T(r,0,n,0,e,t,o),i=0;i<32;i++)r[i]=0;return 0}function O(r,n){var e;for(e=0;e<16;e++)r[e]=0|n[e]}function C(r){var n,e;for(e=0;e<16;e++)r[e]+=65536,n=Math.floor(r[e]/65536),r[(e+1)*(e<15?1:0)]+=n-1+37*(n-1)*(15===e?1:0),r[e]-=65536*n}function F(r,n,e){for(var t,o=~(e-1),i=0;i<16;i++)t=o&(r[i]^n[i]),r[i]^=t,n[i]^=t}function Z(r,n){var e,t,o,i=v(),a=v();for(e=0;e<16;e++)a[e]=n[e];for(C(a),C(a),C(a),t=0;t<2;t++){for(i[0]=a[0]-65517,e=1;e<15;e++)i[e]=a[e]-65535-(i[e-1]>>16&1),i[e-1]&=65535;i[15]=a[15]-32767-(i[14]>>16&1),o=i[15]>>16&1,i[14]&=65535,F(a,i,1-o)}for(e=0;e<16;e++)r[2*e]=255&a[e],r[2*e+1]=a[e]>>8}function G(r,n){var e=new Uint8Array(32),t=new Uint8Array(32);return Z(e,r),Z(t,n),_(e,0,t,0)}function q(r){var n=new Uint8Array(32);return Z(n,r),1&n[0]}function D(r,n){var e;for(e=0;e<16;e++)r[e]=n[2*e]+(n[2*e+1]<<8);r[15]&=32767}function I(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]+e[t]|0}function V(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]-e[t]|0}function X(r,n,e){var t,o,i=new Float64Array(31);for(t=0;t<31;t++)i[t]=0;for(t=0;t<16;t++)for(o=0;o<16;o++)i[t+o]+=n[t]*e[o];for(t=0;t<15;t++)i[t]+=38*i[t+16];for(t=0;t<16;t++)r[t]=i[t];C(r),C(r)}function j(r,n){X(r,n,n)}function H(r,n){var e,t=v();for(e=0;e<16;e++)t[e]=n[e];for(e=253;0<=e;e--)j(t,t),2!==e&&4!==e&&X(t,t,n);for(e=0;e<16;e++)r[e]=t[e]}function J(r,n){var e,t=v();for(e=0;e<16;e++)t[e]=n[e];for(e=250;0<=e;e--)j(t,t),1!==e&&X(t,t,n);for(e=0;e<16;e++)r[e]=t[e]}function Q(r,n,e){var t,o,i=new Uint8Array(32),a=new Float64Array(80),f=v(),u=v(),c=v(),w=v(),y=v(),l=v();for(o=0;o<31;o++)i[o]=n[o];for(i[31]=127&n[31]|64,i[0]&=248,D(a,e),o=0;o<16;o++)u[o]=a[o],w[o]=f[o]=c[o]=0;for(f[0]=w[0]=1,o=254;0<=o;--o)F(f,u,t=i[o>>>3]>>>(7&o)&1),F(c,w,t),I(y,f,c),V(f,f,c),I(c,u,w),V(u,u,w),j(w,y),j(l,f),X(f,c,f),X(c,u,y),I(y,f,c),V(f,f,c),j(u,f),V(c,w,l),X(f,c,g),I(f,f,w),X(c,c,f),X(f,w,l),X(w,u,a),j(u,y),F(f,u,t),F(c,w,t);for(o=0;o<16;o++)a[o+16]=f[o],a[o+32]=c[o],a[o+48]=u[o],a[o+64]=w[o];var s=a.subarray(32),h=a.subarray(16);return H(s,s),X(h,h,s),Z(r,h),0}function W(r,n){return Q(r,n,e)}function $(r,n){return a(n,32),W(r,n)}function rr(r,n,e){var t=new Uint8Array(32);return Q(t,e,n),x(r,o,t,d)}var nr=M,er=N;function tr(){var r,n,e,t=0,o=0,i=0,a=0,f=65535;for(e=0;e<arguments.length;e++)t+=(r=arguments[e].lo)&f,o+=r>>>16,i+=(n=arguments[e].hi)&f,a+=n>>>16;return new m((i+=(o+=t>>>16)>>>16)&f|(a+=i>>>16)<<16,t&f|o<<16)}function or(r,n){return new m(r.hi>>>n,r.lo>>>n|r.hi<<32-n)}function ir(){var r,n=0,e=0;for(r=0;r<arguments.length;r++)n^=arguments[r].lo,e^=arguments[r].hi;return new m(e,n)}function ar(r,n){var e,t,o=32-n;return n<32?(e=r.hi>>>n|r.lo<<o,t=r.lo>>>n|r.hi<<o):n<64&&(e=r.lo>>>n|r.hi<<o,t=r.hi>>>n|r.lo<<o),new m(e,t)}var fr=[new m(1116352408,3609767458),new m(1899447441,602891725),new m(3049323471,3964484399),new m(3921009573,2173295548),new m(961987163,4081628472),new m(1508970993,3053834265),new m(2453635748,2937671579),new m(2870763221,3664609560),new m(3624381080,2734883394),new m(310598401,1164996542),new m(607225278,1323610764),new m(1426881987,3590304994),new m(1925078388,4068182383),new m(2162078206,991336113),new m(2614888103,633803317),new m(3248222580,3479774868),new m(3835390401,2666613458),new m(4022224774,944711139),new m(264347078,2341262773),new m(604807628,2007800933),new m(770255983,1495990901),new m(1249150122,1856431235),new m(1555081692,3175218132),new m(1996064986,2198950837),new m(2554220882,3999719339),new m(2821834349,766784016),new m(2952996808,2566594879),new m(3210313671,3203337956),new m(3336571891,1034457026),new m(3584528711,2466948901),new m(113926993,3758326383),new m(338241895,168717936),new m(666307205,1188179964),new m(773529912,1546045734),new m(1294757372,1522805485),new m(1396182291,2643833823),new m(1695183700,2343527390),new m(1986661051,1014477480),new m(2177026350,1206759142),new m(2456956037,344077627),new m(2730485921,1290863460),new m(2820302411,3158454273),new m(3259730800,3505952657),new m(3345764771,106217008),new m(3516065817,3606008344),new m(3600352804,1432725776),new m(4094571909,1467031594),new m(275423344,851169720),new m(430227734,3100823752),new m(506948616,1363258195),new m(659060556,3750685593),new m(883997877,3785050280),new m(958139571,3318307427),new m(1322822218,3812723403),new m(1537002063,2003034995),new m(1747873779,3602036899),new m(1955562222,1575990012),new m(2024104815,1125592928),new m(2227730452,2716904306),new m(2361852424,442776044),new m(2428436474,593698344),new m(2756734187,3733110249),new m(3204031479,2999351573),new m(3329325298,3815920427),new m(3391569614,3928383900),new m(3515267271,566280711),new m(3940187606,3454069534),new m(4118630271,4000239992),new m(116418474,1914138554),new m(174292421,2731055270),new m(289380356,3203993006),new m(460393269,320620315),new m(685471733,587496836),new m(852142971,1086792851),new m(1017036298,365543100),new m(1126000580,2618297676),new m(1288033470,3409855158),new m(1501505948,4234509866),new m(1607167915,987167468),new m(1816402316,1246189591)];function ur(r,n,e){var t,o,i,a=[],f=[],u=[],c=[];for(o=0;o<8;o++)a[o]=u[o]=B(r,8*o);for(var w,y,l,s,h,v,g,b,p,A,_,U,E,x,d=0;128<=e;){for(o=0;o<16;o++)c[o]=B(n,8*o+d);for(o=0;o<80;o++){for(i=0;i<8;i++)f[i]=u[i];for(t=tr(u[7],ir(ar(x=u[4],14),ar(x,18),ar(x,41)),(p=u[4],A=u[5],_=u[6],0,U=p.hi&A.hi^~p.hi&_.hi,E=p.lo&A.lo^~p.lo&_.lo,new m(U,E)),fr[o],c[o%16]),f[7]=tr(t,ir(ar(b=u[0],28),ar(b,34),ar(b,39)),(l=u[0],s=u[1],h=u[2],0,v=l.hi&s.hi^l.hi&h.hi^s.hi&h.hi,g=l.lo&s.lo^l.lo&h.lo^s.lo&h.lo,new m(v,g))),f[3]=tr(f[3],t),i=0;i<8;i++)u[(i+1)%8]=f[i];if(o%16==15)for(i=0;i<16;i++)c[i]=tr(c[i],c[(i+9)%16],ir(ar(y=c[(i+1)%16],1),ar(y,8),or(y,7)),ir(ar(w=c[(i+14)%16],19),ar(w,61),or(w,6)))}for(o=0;o<8;o++)u[o]=tr(u[o],a[o]),a[o]=u[o];d+=128,e-=128}for(o=0;o<8;o++)S(r,8*o,a[o]);return e}var cr=new Uint8Array([106,9,230,103,243,188,201,8,187,103,174,133,132,202,167,59,60,110,243,114,254,148,248,43,165,79,245,58,95,29,54,241,81,14,82,127,173,230,130,209,155,5,104,140,43,62,108,31,31,131,217,171,251,65,189,107,91,224,205,25,19,126,33,121]);function wr(r,n,e){var t,o=new Uint8Array(64),i=new Uint8Array(256),a=e;for(t=0;t<64;t++)o[t]=cr[t];for(ur(o,n,e),e%=128,t=0;t<256;t++)i[t]=0;for(t=0;t<e;t++)i[t]=n[a-e+t];for(i[e]=128,i[(e=256-128*(e<112?1:0))-9]=0,S(i,e-8,new m(a/536870912|0,a<<3)),ur(o,i,e),t=0;t<64;t++)r[t]=o[t];return 0}function yr(r,n){var e=v(),t=v(),o=v(),i=v(),a=v(),f=v(),u=v(),c=v(),w=v();V(e,r[1],r[0]),V(w,n[1],n[0]),X(e,e,w),I(t,r[0],r[1]),I(w,n[0],n[1]),X(t,t,w),X(o,r[3],n[3]),X(o,o,l),X(i,r[2],n[2]),I(i,i,i),V(a,t,e),V(f,i,o),I(u,i,o),I(c,t,e),X(r[0],a,f),X(r[1],c,u),X(r[2],u,f),X(r[3],a,c)}function lr(r,n,e){var t;for(t=0;t<4;t++)F(r[t],n[t],e)}function sr(r,n){var e=v(),t=v(),o=v();H(o,n[2]),X(e,n[0],o),X(t,n[1],o),Z(r,t),r[31]^=q(e)<<7}function hr(r,n,e){var t,o;for(O(r[0],c),O(r[1],w),O(r[2],w),O(r[3],c),o=255;0<=o;--o)lr(r,n,t=e[o/8|0]>>(7&o)&1),yr(n,r),yr(r,r),lr(r,n,t)}function vr(r,n){var e=[v(),v(),v(),v()];O(e[0],t),O(e[1],f),O(e[2],w),X(e[3],t,f),hr(r,e,n)}function gr(r,n,e){var t,o=new Uint8Array(64),i=[v(),v(),v(),v()];for(e||a(n,32),wr(o,n,32),o[0]&=248,o[31]&=127,o[31]|=64,vr(i,o),sr(r,i),t=0;t<32;t++)n[t+32]=r[t];return 0}var br=new Float64Array([237,211,245,92,26,99,18,88,214,156,247,162,222,249,222,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16]);function pr(r,n){var e,t,o,i;for(t=63;32<=t;--t){for(e=0,o=t-32,i=t-12;o<i;++o)n[o]+=e-16*n[t]*br[o-(t-32)],e=Math.floor((n[o]+128)/256),n[o]-=256*e;n[o]+=e,n[t]=0}for(o=e=0;o<32;o++)n[o]+=e-(n[31]>>4)*br[o],e=n[o]>>8,n[o]&=255;for(o=0;o<32;o++)n[o]-=e*br[o];for(t=0;t<32;t++)n[t+1]+=n[t]>>8,r[t]=255&n[t]}function Ar(r){var n,e=new Float64Array(64);for(n=0;n<64;n++)e[n]=r[n];for(n=0;n<64;n++)r[n]=0;pr(r,e)}function _r(r,n,e,t){var o,i,a=new Uint8Array(64),f=new Uint8Array(64),u=new Uint8Array(64),c=new Float64Array(64),w=[v(),v(),v(),v()];wr(a,t,32),a[0]&=248,a[31]&=127,a[31]|=64;var y=e+64;for(o=0;o<e;o++)r[64+o]=n[o];for(o=0;o<32;o++)r[32+o]=a[32+o];for(wr(u,r.subarray(32),e+32),Ar(u),vr(w,u),sr(r,w),o=32;o<64;o++)r[o]=t[o];for(wr(f,r,e+64),Ar(f),o=0;o<64;o++)c[o]=0;for(o=0;o<32;o++)c[o]=u[o];for(o=0;o<32;o++)for(i=0;i<32;i++)c[o+i]+=f[o]*a[i];return pr(r.subarray(32),c),y}function Ur(r,n,e,t){var o,i=new Uint8Array(32),a=new Uint8Array(64),f=[v(),v(),v(),v()],u=[v(),v(),v(),v()];if(e<64)return-1;if(function(r,n){var e=v(),t=v(),o=v(),i=v(),a=v(),f=v(),u=v();if(O(r[2],w),D(r[1],n),j(o,r[1]),X(i,o,y),V(o,o,r[2]),I(i,r[2],i),j(a,i),j(f,a),X(u,f,a),X(e,u,o),X(e,e,i),J(e,e),X(e,e,o),X(e,e,i),X(e,e,i),X(r[0],e,i),j(t,r[0]),X(t,t,i),G(t,o)&&X(r[0],r[0],s),j(t,r[0]),X(t,t,i),G(t,o))return 1;q(r[0])===n[31]>>7&&V(r[0],c,r[0]),X(r[3],r[0],r[1])}(u,t))return-1;for(o=0;o<e;o++)r[o]=n[o];for(o=0;o<32;o++)r[o+32]=t[o];if(wr(a,r,e),Ar(a),hr(f,u,a),vr(u,n.subarray(32)),yr(f,u),sr(i,f),e-=64,_(n,0,i,0)){for(o=0;o<e;o++)r[o]=0;return-1}for(o=0;o<e;o++)r[o]=n[o+64];return e}function Er(r,n){if(32!==r.length)throw new Error("bad key size");if(24!==n.length)throw new Error("bad nonce size")}function xr(){for(var r=0;r<arguments.length;r++)if(!(arguments[r]instanceof Uint8Array))throw new TypeError("unexpected type, use Uint8Array")}function dr(r){for(var n=0;n<r.length;n++)r[n]=0}i.lowlevel={crypto_core_hsalsa20:x,crypto_stream_xor:T,crypto_stream:L,crypto_stream_salsa20_xor:K,crypto_stream_salsa20:Y,crypto_onetimeauth:R,crypto_onetimeauth_verify:P,crypto_verify_16:A,crypto_verify_32:_,crypto_secretbox:M,crypto_secretbox_open:N,crypto_scalarmult:Q,crypto_scalarmult_base:W,crypto_box_beforenm:rr,crypto_box_afternm:nr,crypto_box:function(r,n,e,t,o,i){var a=new Uint8Array(32);return rr(a,o,i),nr(r,n,e,t,a)},crypto_box_open:function(r,n,e,t,o,i){var a=new Uint8Array(32);return rr(a,o,i),er(r,n,e,t,a)},crypto_box_keypair:$,crypto_hash:wr,crypto_sign:_r,crypto_sign_keypair:gr,crypto_sign_open:Ur,crypto_secretbox_KEYBYTES:32,crypto_secretbox_NONCEBYTES:24,crypto_secretbox_ZEROBYTES:32,crypto_secretbox_BOXZEROBYTES:16,crypto_scalarmult_BYTES:32,crypto_scalarmult_SCALARBYTES:32,crypto_box_PUBLICKEYBYTES:32,crypto_box_SECRETKEYBYTES:32,crypto_box_BEFORENMBYTES:32,crypto_box_NONCEBYTES:24,crypto_box_ZEROBYTES:32,crypto_box_BOXZEROBYTES:16,crypto_sign_BYTES:64,crypto_sign_PUBLICKEYBYTES:32,crypto_sign_SECRETKEYBYTES:64,crypto_sign_SEEDBYTES:32,crypto_hash_BYTES:64,gf:v,D:y,L:br,pack25519:Z,unpack25519:D,M:X,A:I,S:j,Z:V,pow2523:J,add:yr,set25519:O,modL:pr,scalarmult:hr,scalarbase:vr},i.randomBytes=function(r){var n=new Uint8Array(r);return a(n,r),n},i.secretbox=function(r,n,e){xr(r,n,e),Er(e,n);for(var t=new Uint8Array(32+r.length),o=new Uint8Array(t.length),i=0;i<r.length;i++)t[i+32]=r[i];return M(o,t,t.length,n,e),o.subarray(16)},i.secretbox.open=function(r,n,e){xr(r,n,e),Er(e,n);for(var t=new Uint8Array(16+r.length),o=new Uint8Array(t.length),i=0;i<r.length;i++)t[i+16]=r[i];return t.length<32||0!==N(o,t,t.length,n,e)?null:o.subarray(32)},i.secretbox.keyLength=32,i.secretbox.nonceLength=24,i.secretbox.overheadLength=16,i.scalarMult=function(r,n){if(xr(r,n),32!==r.length)throw new Error("bad n size");if(32!==n.length)throw new Error("bad p size");var e=new Uint8Array(32);return Q(e,r,n),e},i.scalarMult.base=function(r){if(xr(r),32!==r.length)throw new Error("bad n size");var n=new Uint8Array(32);return W(n,r),n},i.scalarMult.scalarLength=32,i.scalarMult.groupElementLength=32,i.box=function(r,n,e,t){var o=i.box.before(e,t);return i.secretbox(r,n,o)},i.box.before=function(r,n){xr(r,n),function(r,n){if(32!==r.length)throw new Error("bad public key size");if(32!==n.length)throw new Error("bad secret key size")}(r,n);var e=new Uint8Array(32);return rr(e,r,n),e},i.box.after=i.secretbox,i.box.open=function(r,n,e,t){var o=i.box.before(e,t);return i.secretbox.open(r,n,o)},i.box.open.after=i.secretbox.open,i.box.keyPair=function(){var r=new Uint8Array(32),n=new Uint8Array(32);return $(r,n),{publicKey:r,secretKey:n}},i.box.keyPair.fromSecretKey=function(r){if(xr(r),32!==r.length)throw new Error("bad secret key size");var n=new Uint8Array(32);return W(n,r),{publicKey:n,secretKey:new Uint8Array(r)}},i.box.publicKeyLength=32,i.box.secretKeyLength=32,i.box.sharedKeyLength=32,i.box.nonceLength=24,i.box.overheadLength=i.secretbox.overheadLength,i.sign=function(r,n){if(xr(r,n),64!==n.length)throw new Error("bad secret key size");var e=new Uint8Array(64+r.length);return _r(e,r,r.length,n),e},i.sign.open=function(r,n){if(xr(r,n),32!==n.length)throw new Error("bad public key size");var e=new Uint8Array(r.length),t=Ur(e,r,r.length,n);if(t<0)return null;for(var o=new Uint8Array(t),i=0;i<o.length;i++)o[i]=e[i];return o},i.sign.detached=function(r,n){for(var e=i.sign(r,n),t=new Uint8Array(64),o=0;o<t.length;o++)t[o]=e[o];return t},i.sign.detached.verify=function(r,n,e){if(xr(r,n,e),64!==n.length)throw new Error("bad signature size");if(32!==e.length)throw new Error("bad public key size");var t,o=new Uint8Array(64+r.length),i=new Uint8Array(64+r.length);for(t=0;t<64;t++)o[t]=n[t];for(t=0;t<r.length;t++)o[t+64]=r[t];return 0<=Ur(i,o,o.length,e)},i.sign.keyPair=function(){var r=new Uint8Array(32),n=new Uint8Array(64);return gr(r,n),{publicKey:r,secretKey:n}},i.sign.keyPair.fromSecretKey=function(r){if(xr(r),64!==r.length)throw new Error("bad secret key size");for(var n=new Uint8Array(32),e=0;e<n.length;e++)n[e]=r[32+e];return{publicKey:n,secretKey:new Uint8Array(r)}},i.sign.keyPair.fromSeed=function(r){if(xr(r),32!==r.length)throw new Error("bad seed size");for(var n=new Uint8Array(32),e=new Uint8Array(64),t=0;t<32;t++)e[t]=r[t];return gr(n,e,!0),{publicKey:n,secretKey:e}},i.sign.publicKeyLength=32,i.sign.secretKeyLength=64,i.sign.seedLength=32,i.sign.signatureLength=64,i.hash=function(r){xr(r);var n=new Uint8Array(64);return wr(n,r,r.length),n},i.hash.hashLength=64,i.verify=function(r,n){return xr(r,n),0!==r.length&&0!==n.length&&(r.length===n.length&&0===u(r,0,n,0,r.length))},i.setPRNG=function(r){a=r},function(){var o="undefined"!=typeof self?self.crypto||self.msCrypto:null;if(o&&o.getRandomValues){i.setPRNG(function(r,n){var e,t=new Uint8Array(n);for(e=0;e<n;e+=65536)o.getRandomValues(t.subarray(e,e+Math.min(n-e,65536)));for(e=0;e<n;e++)r[e]=t[e];dr(t)})}else"undefined"!=typeof require&&(o=require("crypto"))&&o.randomBytes&&i.setPRNG(function(r,n){var e,t=o.randomBytes(n);for(e=0;e<n;e++)r[e]=t[e];dr(t)})}()}("undefined"!=typeof module&&module.exports?module.exports:self.nacl=self.nacl||{}); \ No newline at end of file
diff --git a/internal/assets/web/static/powWorker.js b/internal/assets/web/static/powWorker.js
new file mode 100644
index 0000000..fa668a1
--- /dev/null
+++ b/internal/assets/web/static/powWorker.js
@@ -0,0 +1,57 @@
+self.onmessage = async ({ data }) => {
+ const { prefix, targetZeros, startNonce, step } = data;
+ const zeroStr = '0'.repeat(targetZeros);
+ let nonce = startNonce;
+ let checked = 0;
+
+ // Batch size: controllare piΓΉ nonce per ogni aggiornamento di progresso
+ const BATCH_SIZE = 500;
+
+ // Parallelizzazione del lavoro utilizzando piΓΉ promise contemporaneamente
+ const CONCURRENT_PROMISES = 12;
+
+ // Funzione per verificare un singolo nonce
+ const checkNonce = async (nonceToCheck) => {
+ const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(prefix + nonceToCheck));
+ const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
+ return { nonce: nonceToCheck, hex };
+ };
+
+ // Funzione per verificare un batch di nonce in parallelo
+ const processBatch = async () => {
+ const promises = [];
+ for (let i = 0; i < CONCURRENT_PROMISES; i++) {
+ let batchPromises = [];
+ for (let j = 0; j < BATCH_SIZE; j++) {
+ const currentNonce = nonce;
+ batchPromises.push(checkNonce(currentNonce));
+ nonce += step;
+ }
+ promises.push(Promise.all(batchPromises));
+ }
+
+ // Attende il completamento di tutti i batch
+ const results = await Promise.all(promises);
+
+ // Appiattisce i risultati e controlla se c'Γ¨ una corrispondenza
+ const allResults = results.flat();
+ for (const result of allResults) {
+ if (result.hex.startsWith(zeroStr)) {
+ return { found: true, nonce: result.nonce };
+ }
+ }
+
+ checked += CONCURRENT_PROMISES * BATCH_SIZE;
+ self.postMessage({ type: 'progress', checked });
+ return { found: false };
+ };
+
+ // Loop principale
+ while (true) {
+ const { found, nonce: foundNonce } = await processBatch();
+ if (found) {
+ self.postMessage({ type: 'found', nonce: foundNonce });
+ break;
+ }
+ }
+};
diff --git a/internal/assets/web/static/style.css b/internal/assets/web/static/style.css
new file mode 100644
index 0000000..c016b5d
--- /dev/null
+++ b/internal/assets/web/static/style.css
@@ -0,0 +1,150 @@
+:root {
+ color-scheme: light dark;
+ --bg: #f4f5f7;
+ --fg: #171a1f;
+ --muted: #657083;
+ --panel: #ffffff;
+ --border: #cbd3df;
+ --accent: #0f766e;
+ --accent-dark: #0b5f59;
+ --danger: #b42318;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #121417;
+ --fg: #f2f4f7;
+ --muted: #a5adba;
+ --panel: #1b1f26;
+ --border: #3a4351;
+ --accent: #2dd4bf;
+ --accent-dark: #14b8a6;
+ }
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--fg);
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ line-height: 1.5;
+}
+
+.shell {
+ width: min(960px, calc(100vw - 32px));
+ margin: 0 auto;
+ padding: 24px 0 40px;
+}
+
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 18px;
+}
+
+h1, h2, p { margin-top: 0; }
+h1 { margin-bottom: 4px; font-size: 2rem; }
+h2 { font-size: 1.1rem; }
+p, footer { color: var(--muted); }
+
+.health {
+ color: var(--accent);
+ text-decoration: none;
+ font-size: 0.95rem;
+}
+
+.panel {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 18px;
+ margin-bottom: 16px;
+}
+
+.grid {
+ display: grid;
+ gap: 14px;
+}
+
+.grid.two {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+label {
+ display: block;
+ color: var(--muted);
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+input, textarea, select {
+ width: 100%;
+ margin-top: 6px;
+ padding: 10px 11px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+}
+
+textarea {
+ resize: vertical;
+ min-height: 130px;
+}
+
+input[readonly], textarea[readonly] {
+ color: var(--muted);
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin: 14px 0;
+}
+
+button, .fileButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 40px;
+ padding: 0 14px;
+ border: 0;
+ border-radius: 6px;
+ background: var(--accent);
+ color: #fff;
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+button:hover, .fileButton:hover { background: var(--accent-dark); }
+button:disabled { opacity: 0.5; cursor: not-allowed; }
+.fileButton input { display: none; }
+
+.final {
+ justify-content: flex-end;
+}
+
+footer {
+ padding-top: 8px;
+ font-size: 0.85rem;
+}
+
+.errorText {
+ color: var(--danger);
+ font-weight: 700;
+}
+
+@media (max-width: 700px) {
+ .grid.two { grid-template-columns: 1fr; }
+ .topbar { align-items: flex-start; flex-direction: column; }
+ .actions.final { justify-content: stretch; }
+ .actions.final button { width: 100%; }
+}
+
diff --git a/internal/assets/web/templates/index.html b/internal/assets/web/templates/index.html
new file mode 100644
index 0000000..0d2d820
--- /dev/null
+++ b/internal/assets/web/templates/index.html
@@ -0,0 +1,1577 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>N2Usenet Gateway v2.7 Nym</title>
+ <link rel="icon" href="/favicon.ico" sizes="any">
+ <link rel="shortcut icon" href="/favicon.ico">
+ <style>
+ :root {
+ --background: #f9f9f9;
+ --card-bg: #fff;
+ --text: #333;
+ --border: #ccc;
+ --primary: #4caf50;
+ --primary-hover: #45a049;
+ --tab-bg: #eee;
+ --input-bg: #fff;
+ --input-readonly: #eee;
+ --progress-bg: #ddd;
+ --success: #28a745;
+ --warning: #ffc107;
+ --error: #dc3545;
+ }
+
+ .dark-theme {
+ --background: #1a1a1a;
+ --card-bg: #2c2c2c;
+ --text: #e0e0e0;
+ --border: #444;
+ --primary: #5cbb60;
+ --primary-hover: #4caf50;
+ --tab-bg: #333;
+ --input-bg: #3c3c3c;
+ --input-readonly: #2a2a2a;
+ --progress-bg: #444;
+ }
+
+ body {
+ background: var(--background);
+ color: var(--text);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ margin: 0;
+ padding: 0;
+ transition: background 0.3s ease;
+ }
+
+ .container {
+ max-width: 800px;
+ margin: auto;
+ padding: 20px;
+ }
+
+ header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ }
+
+ .theme-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .toggle {
+ position: relative;
+ width: 60px;
+ height: 30px;
+ background: #ccc;
+ border-radius: 30px;
+ padding: 4px;
+ cursor: pointer;
+ transition: 0.3s;
+ }
+
+ .toggle:before {
+ content: '';
+ position: absolute;
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ background: white;
+ top: 2px;
+ left: 2px;
+ transition: 0.3s;
+ }
+
+ input[type="checkbox"]:checked + .toggle {
+ background: #4caf50;
+ }
+
+ input[type="checkbox"]:checked + .toggle:before {
+ transform: translateX(30px);
+ }
+
+ input[type="checkbox"] {
+ display: none;
+ }
+
+ .notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ max-width: 400px;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ z-index: 10000;
+ animation: slideIn 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .notification.success { background: var(--success); color: white; }
+ .notification.error { background: var(--error); color: white; }
+ .notification.warning { background: var(--warning); color: #333; }
+ .notification.info { background: #007bff; color: white; }
+
+ @keyframes slideIn {
+ from { transform: translateX(400px); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+
+ @keyframes pulse {
+ 0% { box-shadow: 0 0 0 0 rgba(156, 39, 176, 0.7); }
+ 50% { box-shadow: 0 0 0 15px rgba(156, 39, 176, 0); }
+ 100% { box-shadow: 0 0 0 0 rgba(156, 39, 176, 0); }
+ }
+
+ .notification-icon { font-size: 1.5em; }
+ .notification-close { margin-left: auto; cursor: pointer; font-size: 1.2em; opacity: 0.8; }
+ .notification-close:hover { opacity: 1; }
+
+ .section-info {
+ background: var(--card-bg);
+ border-left: 4px solid var(--primary);
+ padding: 10px 15px;
+ margin-bottom: 15px;
+ border-radius: 0 4px 4px 0;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ }
+
+ .tabs {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 15px;
+ }
+
+ .tabs button {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 12px 15px;
+ border: none;
+ background: var(--tab-bg);
+ color: var(--text);
+ cursor: pointer;
+ border-radius: 4px;
+ font-weight: bold;
+ transition: background 0.2s, transform 0.1s;
+ position: relative;
+ }
+
+ .tabs button:hover { background: var(--primary); color: white; }
+ .tabs button:active { transform: scale(0.98); }
+
+ .tabs button.completed::after {
+ content: 'βœ“';
+ position: absolute;
+ right: 15px;
+ background: var(--success);
+ color: white;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.9em;
+ }
+
+ .tab-content {
+ display: none !important;
+ padding: 20px;
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ margin-bottom: 20px;
+ }
+
+ .tab-content.active {
+ display: block !important;
+ }
+
+ label {
+ display: block;
+ margin-top: 15px;
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+
+ input, textarea, select {
+ width: 100%;
+ padding: 10px;
+ margin-top: 5px;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--input-bg);
+ color: var(--text);
+ font-family: inherit;
+ }
+
+ input:focus, textarea:focus, select:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
+ }
+
+ input.error, textarea.error { border-color: var(--error); }
+
+ input[readonly], textarea[readonly] {
+ background: var(--input-readonly);
+ cursor: not-allowed;
+ border: 2px solid #999;
+ }
+
+ input.prefilled {
+ background: #e8f5e9;
+ border: 2px solid var(--primary);
+ }
+
+ .dark-theme input.prefilled {
+ background: #1b3d1b;
+ }
+
+ button {
+ margin-top: 15px;
+ padding: 12px 20px;
+ background: var(--primary);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: bold;
+ transition: background 0.2s, opacity 0.2s;
+ font-family: inherit;
+ }
+
+ button:hover:not(:disabled) { background: var(--primary-hover); }
+ button:disabled { opacity: 0.6; cursor: not-allowed; }
+
+ .btn-secondary {
+ background: #6c757d;
+ }
+ .btn-secondary:hover:not(:disabled) { background: #5a6268; }
+
+ .progress-bar {
+ width: 100%;
+ background: var(--progress-bg);
+ border-radius: 5px;
+ overflow: hidden;
+ height: 20px;
+ margin-top: 10px;
+ }
+
+ .progress-bar-inner {
+ height: 100%;
+ width: 0;
+ background: var(--primary);
+ text-align: center;
+ color: white;
+ line-height: 20px;
+ transition: width 0.3s;
+ font-size: 0.85em;
+ }
+
+ .output-field {
+ margin-top: 15px;
+ padding: 10px;
+ background: var(--input-readonly);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ word-break: break-all;
+ min-height: 30px;
+ font-family: monospace;
+ font-size: 0.9em;
+ }
+
+ .output-field.empty { color: #999; font-style: italic; }
+
+ .gateway-info {
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ padding: 15px;
+ margin: 15px 0;
+ border-radius: 4px;
+ border-left: 4px solid #007bff;
+ }
+
+ .gateway-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .gateway-item:last-child { border-bottom: none; }
+
+ .gateway-priority {
+ background: #007bff;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.8em;
+ margin-right: 10px;
+ min-width: 70px;
+ text-align: center;
+ }
+
+ .gateway-address {
+ font-family: monospace;
+ background: var(--input-readonly);
+ padding: 4px 8px;
+ border-radius: 4px;
+ flex: 1;
+ font-size: 0.85em;
+ }
+
+ .prefill-banner {
+ background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
+ color: white;
+ padding: 12px 15px;
+ border-radius: 4px;
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
+ }
+
+ .prefill-banner.reply {
+ background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
+ }
+
+ .prefill-banner-icon { font-size: 1.5em; }
+ .prefill-banner-text { flex: 1; }
+ .prefill-banner-text strong { display: block; margin-bottom: 2px; }
+ .prefill-banner-text small { opacity: 0.9; }
+
+ .keypair-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-top: 15px;
+ }
+
+ .keypair-actions button {
+ flex: 1;
+ min-width: 150px;
+ }
+
+ .identity-loaded {
+ background: #d4edda;
+ border: 1px solid #c3e6cb;
+ padding: 10px 15px;
+ border-radius: 4px;
+ margin-top: 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .dark-theme .identity-loaded {
+ background: #1e3d1e;
+ border-color: #2e5d2e;
+ }
+
+ .identity-loaded-icon { font-size: 1.5em; }
+ .identity-loaded-text { flex: 1; }
+
+ .identicon-preview {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 10px;
+ }
+
+ .identicon-preview img {
+ width: 32px;
+ height: 32px;
+ image-rendering: pixelated;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ }
+
+ .identicon-section {
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-left: 4px solid #9c27b0;
+ padding: 15px;
+ margin: 15px 0;
+ border-radius: 0 4px 4px 0;
+ text-align: center;
+ }
+
+ .identicon-section h4 {
+ margin: 0 0 15px 0;
+ color: #9c27b0;
+ }
+
+ .identicon-large-preview {
+ margin: 15px 0;
+ }
+
+ .identicon-hash {
+ font-size: 0.8em;
+ color: #666;
+ word-break: break-all;
+ margin: 10px 0;
+ }
+
+ .identicon-hash code {
+ background: var(--input-readonly);
+ padding: 4px 8px;
+ border-radius: 4px;
+ display: inline-block;
+ max-width: 100%;
+ overflow-x: auto;
+ }
+
+ .identicon-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin: 15px 0;
+ }
+
+ .identicon-actions button {
+ margin: 0;
+ padding: 8px 15px;
+ font-size: 0.9em;
+ }
+
+ .identicon-tip {
+ background: #f3e5f5;
+ padding: 10px;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+
+ .dark-theme .identicon-tip {
+ background: #2d1f30;
+ }
+
+ footer {
+ text-align: center;
+ margin-top: 40px;
+ padding-top: 20px;
+ border-top: 1px solid var(--border);
+ font-size: 0.9em;
+ color: #888;
+ }
+
+ .footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 10px;
+ }
+
+ .footer-links a {
+ color: var(--primary);
+ text-decoration: none;
+ transition: color 0.2s;
+ }
+
+ .footer-links a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+ }
+
+ @media (max-width: 600px) {
+ .container { padding: 10px; }
+ .tab-content { padding: 15px 10px; }
+ .footer-links { flex-direction: column; gap: 10px; }
+ .gateway-item { flex-direction: column; align-items: flex-start; gap: 5px; }
+ .gateway-address { width: 100%; }
+ .notification { right: 10px; left: 10px; max-width: none; }
+ .keypair-actions { flex-direction: column; }
+ .keypair-actions button { min-width: 100%; }
+ }
+ </style>
+ <script src="/static/nacl.min.js"></script>
+ <script src="/static/nacl-util.min.js"></script>
+</head>
+<body>
+<div class="container">
+ <header>
+ <h1>N2Usenet Gateway v2.7 Nym</h1>
+ <div class="theme-toggle">
+ <span>🌞</span>
+ <input type="checkbox" id="themeToggle">
+ <label for="themeToggle" class="toggle"></label>
+ <span>πŸŒ™</span>
+ </div>
+ </header>
+
+ <div id="prefillBanner" class="prefill-banner" style="display: none;">
+ <span class="prefill-banner-icon">πŸ“</span>
+ <div class="prefill-banner-text">
+ <strong id="prefillTitle">New Post</strong>
+ <small id="prefillDetails">Fields pre-filled from Onion Newsreader</small>
+ </div>
+ </div>
+
+ <div class="tabs">
+ <button id="tabBtn1" onclick="showTab('pow')">
+ <span>1. Generate Hashcash Token</span>
+ </button>
+ <button id="tabBtn2" onclick="showTab('sign')">
+ <span>2. Sign Message (Identity)</span>
+ </button>
+ <button id="tabBtn3" onclick="showTab('send')">
+ <span>3. Send Message</span>
+ </button>
+ </div>
+
+ <div id="pow" class="tab-content active">
+ <h2>1. Load Identity or Start Fresh</h2>
+
+ <div class="identity-loader" style="background: var(--card-bg); border: 2px dashed var(--primary); border-radius: 8px; padding: 20px; margin-bottom: 20px; text-align: center;">
+ <p style="margin: 0 0 15px 0;"><strong>πŸ”‘ Have a saved identity?</strong> Load it to auto-fill all fields.</p>
+ <button id="loadIdentityBtn" class="btn-primary" style="font-size: 1.1em; padding: 12px 30px;">πŸ“‚ Load Identity File</button>
+ <input type="file" id="identityFileInput" accept=".json" style="display:none;">
+ <p style="margin: 15px 0 0 0; font-size: 0.9em; color: var(--text); opacity: 0.7;">Or fill the fields below to create a new identity.</p>
+ </div>
+
+ <div id="identityLoadedPow" class="identity-loaded" style="display:none; margin-bottom: 20px;">
+ <span class="identity-loaded-icon">βœ…</span>
+ <div class="identity-loaded-text">
+ <strong id="loadedIdentityName">Identity Loaded</strong>
+ <small id="loadedIdentityEmail">email@example.com</small>
+ </div>
+ <div class="identicon-preview">
+ <img id="identiconPreviewPow" alt="Your identicon" style="width: 48px; height: 48px; image-rendering: pixelated; border-radius: 4px;">
+ </div>
+ </div>
+
+ <div class="section-info">
+ <p><strong>About N2Usenet:</strong> A privacy-focused standalone gateway that posts to Usenet newsgroups over Nym to the TCPReset Mail2News and NNTP infrastructure. No access logs are kept. To post you must first create, or load, a <strong>VFACE pseudonymous identity</strong>: an Ed25519 keypair whose public key deterministically generates your identicon. Same key &rarr; same identicon &rarr; same person over time.</p>
+ <p><strong>What is Proof-of-Work?</strong> This step generates a hashcash token that prevents spam by requiring your computer to perform some calculations.</p>
+ </div>
+
+ <label>Username (your pseudonym): <input type="text" id="fromName" placeholder="YourPseudonym" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required></label>
+ <label>Email (for identity hash): <input type="email" id="hcEmail" placeholder="your@email.example" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required></label>
+ <label>Difficulty (bits):
+ <select id="hcBits">
+ <option value="16">16 bits (very fast, ~instant - recommended for hardened browsers)</option>
+ <option value="20" selected>20 bits (fast, ~few seconds)</option>
+ <option value="24">24 bits (medium, ~30-60 seconds)</option>
+ <option value="28">28 bits (slow, ~several minutes)</option>
+ </select>
+ </label>
+ <button id="genTokenBtn">Generate Token</button>
+ <div class="progress-bar"><div id="tokenProgress" class="progress-bar-inner">Ready</div></div>
+ <label>Generated Token:</label>
+ <div id="tokenOutput" class="output-field empty">Token will appear here after generation</div>
+ </div>
+
+ <div id="sign" class="tab-content">
+ <h2>2. Sign Your Message</h2>
+
+ <div id="identiconSection" class="identicon-section" style="display:none;">
+ <h4>🎨 Your Visual Identity</h4>
+ <div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;">
+ <div class="identicon-large-preview">
+ <img id="identiconPreviewLarge" alt="Your identicon" style="width:96px;height:96px;image-rendering:pixelated;border:2px solid var(--border);border-radius:8px;">
+ </div>
+ <div style="flex: 1; min-width: 200px;">
+ <p style="margin: 0 0 5px 0;"><strong id="identityUsername">Username</strong></p>
+ <p style="margin: 0 0 10px 0; font-size: 0.9em; color: var(--text); opacity: 0.8;" id="identityEmail">email@example.com</p>
+ <p class="identicon-hash" style="margin: 0;">Hash: <code id="identityHashDisplay"></code></p>
+ </div>
+ </div>
+ <p style="margin-top: 15px; text-align: center; font-size: 0.95em;">Identity material stays only in browser memory for this tab and is wiped after a successful send.</p>
+ <p style="margin-top: 15px; text-align: center; font-size: 0.9em;">
+ <a href="https://identicons.virebent.art" target="_blank" rel="noopener noreferrer" style="color: #9c27b0; text-decoration: none;">πŸ” VFACE Verifier - Verify an identity</a>
+ </p>
+ </div>
+
+ <div class="section-info">
+ <p><strong>What is this?</strong> Your keypair is generated and your message is signed <strong>locally in your browser</strong> with the self-hosted TweetNaCl (<code>nacl</code>) library, and your Ed25519 secret key never leaves your device. Only the public identity (username, email, public key) is sent, to render the identicon. The signature proves you authored this post.</p>
+ <p><strong>VFACE / Face Header:</strong> Your identicon is produced by the original <code>identicons-cli</code> engine, the same backend as <code>identicons.virebent.art</code>, so it is fully deterministic: the same key always yields the same identicon. It is embedded as a folded <code>Face:</code> header (RFC&nbsp;4021/2822) alongside <code>X-Ed25519-Pub</code> and <code>X-Ed25519-Sig</code>, visible in newsreaders like Newsgrouper and re-verifiable by any reader from your public key.</p>
+ </div>
+
+ <label>Email (from PoW):</label>
+ <input type="text" id="readonlyEmailSign" readonly>
+
+ <label>Message to Sign:</label>
+ <textarea id="messageToSign" rows="6" placeholder="Write your message here..." autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required></textarea>
+
+ <div class="keypair-actions">
+ <button id="genKeyBtn">πŸ”‘ Generate New Keypair</button>
+ </div>
+ <input type="file" id="keyFileInput" accept=".json" style="display:none;">
+
+ <button id="signMsgBtn" disabled>✍️ Sign Message</button>
+
+ <label>Public Key:</label>
+ <div id="pubKeyOutput" class="output-field empty">Public key will appear here</div>
+ <label>Signature:</label>
+ <div id="signatureOutput" class="output-field empty">Signature will appear here</div>
+ </div>
+
+ <div id="send" class="tab-content">
+ <h2>Send Message</h2>
+ <div class="section-info">
+ <p><strong>What is this?</strong> This final step sends your signed message to Usenet via Nym, Mail2News, and the <code>news.tcpreset.net</code> NNTP service.</p>
+ <div class="gateway-info">
+ <h4>πŸ” Nym to TCPReset Posting Path</h4>
+ <p style="margin-bottom: 15px; font-size: 0.95em;">Your message is submitted over HTTPS, then relayed from this server through Nym to the Mail2News gateway and finally into <code>news.tcpreset.net</code> NNTP:</p>
+
+ <div class="gateway-item">
+ <span class="gateway-priority">Step 1</span>
+ <div style="flex: 1;">
+ <strong>Nym SMTP transport</strong>
+ <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Nym SOCKS5/requester transport to the TCPReset Mail2News SMTP gateway.</p>
+ </div>
+ </div>
+ <details style="margin-top: 8px; font-size: 0.8em;">
+ <summary style="cursor: pointer; color: #007bff;">Show relay nodes</summary>
+ <ul style="margin: 8px 0; padding-left: 20px; font-family: monospace; color: #555;">
+ <li><strong>SMTP:</strong> mail2news.tcpreset.net:587 STARTTLS via Nym SOCKS5</li>
+ </ul>
+ </details>
+
+ <div class="gateway-item">
+ <span class="gateway-priority">Step 2</span>
+ <div style="flex: 1;">
+ <strong>Mail2News Gateway</strong>
+ <div class="gateway-address" style="margin-top: 5px;">mail2news@mail2news.tcpreset.net</div>
+ <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Converts the signed email payload into a Usenet post for the TCPReset news service.</p>
+ </div>
+ </div>
+
+ <div class="gateway-item">
+ <span class="gateway-priority">Step 3</span>
+ <div style="flex: 1;">
+ <strong>TCPReset NNTP</strong>
+ <div class="gateway-address" style="margin-top: 5px;">nntp://news.tcpreset.net</div>
+ <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Final posting target for the standalone N2Usenet path.</p>
+ </div>
+ </div>
+
+ <p style="margin-top: 15px; padding: 10px; background: rgba(0,123,255,0.1); border-radius: 4px; font-size: 0.9em;">
+ <strong>πŸ”’ Privacy Guarantee:</strong> The browser-to-server hop is HTTPS. The server-to-Mail2News hop uses Nym plus SMTP TLS. Mail2News then posts into <code>news.tcpreset.net</code> NNTP.
+ </p>
+ </div>
+ </div>
+
+ <label>Email used for PoW:</label>
+ <input type="text" id="readonlyEmailSend" readonly>
+ <form id="sendForm" method="POST" action="/submit" autocomplete="off">
+ <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
+ <label>From (Name):</label>
+ <input type="text" id="readonlyFromName" readonly>
+ <input type="hidden" name="from" id="fromFull">
+ <label>Newsgroups (max 3, comma separated):
+ <input type="text" name="newsgroups" id="newsgroups" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required placeholder="e.g. alt.privacy, comp.security">
+ </label>
+ <label>Subject: <input type="text" name="subject" id="subject" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required placeholder="Message Subject"></label>
+ <label>References (optional): <input type="text" name="references" id="references" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Message-ID of post you're replying to"></label>
+ <label>X-Hashcash Token: <input type="text" name="xhashcash" id="hcToken" required readonly></label>
+ <label>Message:</label>
+ <textarea name="message" id="messageContent" rows="8" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" required placeholder="Your message will appear here after signing..."></textarea>
+ <input type="hidden" name="x-ed25519-pub" id="x-ed25519-pub">
+ <input type="hidden" name="x-ed25519-sig" id="x-ed25519-sig">
+ <button type="submit" id="sendBtn">Send Message</button>
+ </form>
+ </div>
+
+ <footer>
+ <div>N2Usenet Gateway v2.7.0 Β© 2025 - Privacy-focused Usenet posting via Nym</div>
+ <div class="footer-links">
+ <a href="https://yamn.virebent.art">Home</a>
+ <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%69%6E%66%6F%40%76%69%72%65%62%65%6E%74%2E%61%72%74">Contact</a>
+ <a href="https://github.com/gabrix73/m2usenet-go">Code</a>
+ </div>
+ </footer>
+</div>
+
+<script>
+// ============================================================================
+// GLOBAL FUNCTIONS - MUST BE DEFINED FIRST
+// ============================================================================
+
+function showTab(id) {
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+}
+
+function showNotification(message, type, duration) {
+ type = type || 'info';
+ duration = duration === undefined ? 5000 : duration;
+ const icons = { success: 'βœ“', error: 'βœ—', warning: '⚠', info: 'β„Ή' };
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+ notification.innerHTML = `
+ <span class="notification-icon">${icons[type] || icons.info}</span>
+ <span class="notification-message">${message}</span>
+ <span class="notification-close" onclick="this.parentElement.remove()">Γ—</span>
+ `;
+ document.body.appendChild(notification);
+ if (duration > 0) {
+ setTimeout(() => { if (notification.parentElement) notification.remove(); }, duration);
+ }
+}
+
+// ============================================================================
+// STATE MANAGEMENT
+// ============================================================================
+
+const appState = {
+ step1Complete: false,
+ step2Complete: false,
+ step3Complete: false,
+ prefillMode: null
+};
+
+function updateTabIndicators() {
+ document.getElementById('tabBtn1').classList.toggle('completed', appState.step1Complete);
+ document.getElementById('tabBtn2').classList.toggle('completed', appState.step2Complete);
+ document.getElementById('tabBtn3').classList.toggle('completed', appState.step3Complete);
+}
+
+let keyPair = null;
+let workersSupported = true;
+
+function setOutputField(id, value, emptyText) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ if (value) {
+ el.innerText = value;
+ el.classList.remove('empty');
+ return;
+ }
+ el.innerText = emptyText;
+ el.classList.add('empty');
+}
+
+function resetEphemeralState(messageID) {
+ if (keyPair) {
+ if (keyPair.secretKey && typeof keyPair.secretKey.fill === 'function') keyPair.secretKey.fill(0);
+ if (keyPair.publicKey && typeof keyPair.publicKey.fill === 'function') keyPair.publicKey.fill(0);
+ }
+ keyPair = null;
+ currentIdenticonData = null;
+
+ appState.step1Complete = false;
+ appState.step2Complete = false;
+ appState.step3Complete = false;
+ updateTabIndicators();
+
+ [
+ 'fromName', 'hcEmail', 'readonlyEmailSign', 'readonlyEmailSend',
+ 'readonlyFromName', 'fromFull', 'newsgroups', 'subject', 'references',
+ 'hcToken', 'messageToSign', 'messageContent', 'x-ed25519-pub',
+ 'x-ed25519-sig'
+ ].forEach((id) => {
+ const el = document.getElementById(id);
+ if (el) el.value = '';
+ });
+
+ ['identityFileInput', 'keyFileInput'].forEach((id) => {
+ const el = document.getElementById(id);
+ if (el) el.value = '';
+ });
+
+ document.querySelectorAll('input, textarea').forEach((el) => {
+ el.classList.remove('error', 'prefilled');
+ });
+
+ const hcBits = document.getElementById('hcBits');
+ if (hcBits) hcBits.value = '20';
+
+ setOutputField('tokenOutput', '', 'Token will appear here after generation');
+ setOutputField('pubKeyOutput', '', 'Public key will appear here');
+ setOutputField('signatureOutput', '', 'Signature will appear here');
+
+ const tokenProgress = document.getElementById('tokenProgress');
+ if (tokenProgress) {
+ tokenProgress.style.width = '0';
+ tokenProgress.textContent = 'Ready';
+ }
+
+ const identityLoaded = document.getElementById('identityLoadedPow');
+ if (identityLoaded) identityLoaded.style.display = 'none';
+ const identiconSection = document.getElementById('identiconSection');
+ if (identiconSection) identiconSection.style.display = 'none';
+ const prefillBanner = document.getElementById('prefillBanner');
+ if (prefillBanner) {
+ prefillBanner.style.display = 'none';
+ prefillBanner.classList.remove('reply');
+ }
+
+ ['identiconPreviewPow', 'identiconPreviewLarge'].forEach((id) => {
+ const img = document.getElementById(id);
+ if (img) img.removeAttribute('src');
+ });
+ const identityHash = document.getElementById('identityHashDisplay');
+ if (identityHash) identityHash.textContent = '';
+ const loadedName = document.getElementById('loadedIdentityName');
+ if (loadedName) loadedName.textContent = 'Identity Loaded';
+ const loadedEmail = document.getElementById('loadedIdentityEmail');
+ if (loadedEmail) loadedEmail.textContent = 'Identity kept only in memory';
+ const identityUsername = document.getElementById('identityUsername');
+ if (identityUsername) identityUsername.textContent = 'Username';
+ const identityEmail = document.getElementById('identityEmail');
+ if (identityEmail) identityEmail.textContent = 'email@example.com';
+
+ const genKeyBtn = document.getElementById('genKeyBtn');
+ if (genKeyBtn) {
+ genKeyBtn.disabled = false;
+ genKeyBtn.textContent = 'πŸ”‘ Generate New Keypair';
+ }
+ const signBtn = document.getElementById('signMsgBtn');
+ if (signBtn) signBtn.disabled = true;
+ const sendBtn = document.getElementById('sendBtn');
+ if (sendBtn) {
+ sendBtn.disabled = false;
+ sendBtn.textContent = 'Send Message';
+ sendBtn.style.opacity = '';
+ sendBtn.style.cursor = '';
+ }
+ const progressDiv = document.getElementById('sending-progress');
+ if (progressDiv) progressDiv.remove();
+
+ if (window.history && window.history.replaceState) {
+ window.history.replaceState({}, document.title, window.location.pathname);
+ }
+
+ showTab('pow');
+
+ if (messageID) {
+ showNotification(`βœ“ Message sent. Message-ID: ${messageID}. Identity and form data cleared from memory.`, 'success', 12000);
+ }
+}
+
+try {
+ if (typeof Worker === 'undefined') {
+ workersSupported = false;
+ }
+} catch (e) {
+ workersSupported = false;
+}
+
+// ============================================================================
+// THEME TOGGLE
+// ============================================================================
+
+document.getElementById('themeToggle').addEventListener('change', function() {
+ document.body.classList.toggle('dark-theme', this.checked);
+});
+
+// ============================================================================
+// IDENTICON GENERATION (client-side)
+// ============================================================================
+
+let currentIdenticonData = null;
+
+async function generateIdenticon(username, email, pubkeyBase64) {
+ // Generate the identicon with the ORIGINAL identicons-cli engine (server-side),
+ // the same backend used by identicons.virebent.art (Ch1ffr3punk algorithm).
+ // Only PUBLIC data leaves the browser (username|email|ed25519-pubkey).
+ const body = new URLSearchParams({ username: username, email: email, pubkey: pubkeyBase64 });
+ const resp = await fetch('identicon.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: body.toString()
+ });
+ if (!resp.ok) throw new Error('identicon engine error ' + resp.status);
+ const r = await resp.json();
+ if (!r.face48 || !r.preview) throw new Error('identicon engine returned no data');
+
+ currentIdenticonData = {
+ dataUrl: r.preview, // 256x256 PNG dataurl (display/download)
+ hash: r.hash, // sha256(username|email|pubkey)
+ face48: r.face48, // raw base64 PNG (48x48) -> Face: header
+ faceHeader: r.faceHeader, // folded "Face: ..." header (RFC 2822)
+ username: username,
+ email: email,
+ pubkey: pubkeyBase64,
+ claim: JSON.stringify({
+ version: 2,
+ type: 'n2usenet-identity',
+ username: username,
+ email: email,
+ pubkey: pubkeyBase64,
+ identityHash: r.hash,
+ created: new Date().toISOString(),
+ algorithm: 'identicons-cli sha256(username|email|pubkey) [VFACE]'
+ }, null, 2)
+ };
+ return r.preview;
+}
+
+async function updateIdenticonPreview() {
+ if (!keyPair) return;
+
+ const pubB64 = nacl.util.encodeBase64(keyPair.publicKey);
+ const username = document.getElementById('fromName').value || 'Anonymous';
+ const email = document.getElementById('hcEmail').value || 'anonymous@n2usenet.local';
+
+ try {
+ const identiconUrl = await generateIdenticon(username, email, pubB64);
+
+ document.getElementById('identiconPreviewPow').src = identiconUrl;
+ document.getElementById('identiconPreviewLarge').src = identiconUrl;
+ document.getElementById('identityHashDisplay').textContent = currentIdenticonData.hash;
+ document.getElementById('identityUsername').textContent = username;
+ document.getElementById('identityEmail').textContent = email;
+ document.getElementById('identiconSection').style.display = 'block';
+
+ } catch (e) {
+ console.error('Identicon generation failed:', e);
+ }
+}
+
+// ============================================================================
+// INITIALIZATION
+// ============================================================================
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (typeof nacl === 'undefined') {
+ showNotification('Cryptography library failed to load. Please refresh the page.', 'error', 0);
+ return;
+ }
+
+ if (typeof nacl.util === 'undefined') {
+ nacl.util = {
+ decodeUTF8: function(str) { return new TextEncoder().encode(str); },
+ encodeUTF8: function(arr) { return new TextDecoder().decode(arr); },
+ encodeBase64: function(arr) {
+ return btoa(Array.from(new Uint8Array(arr)).map(byte => String.fromCharCode(byte)).join(''));
+ },
+ decodeBase64: function(b64) {
+ const bin = atob(b64);
+ const arr = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
+ return arr;
+ }
+ };
+ }
+
+ showTab('pow');
+ showNotification('N2Usenet Gateway v2.7.0 ready. No identity or message data is persisted.', 'success', 4000);
+});
+
+// ============================================================================
+// SHA-1 IMPLEMENTATION
+// ============================================================================
+
+function sha1(str) {
+ function rotate_left(n, s) { return (n << s) | (n >>> (32 - s)); }
+ function cvt_hex(val) {
+ let str = '';
+ for (let i = 7; i >= 0; i--) {
+ const v = (val >>> (i * 4)) & 0x0f;
+ str += v.toString(16);
+ }
+ return str;
+ }
+ function utf8Encode(str) { return unescape(encodeURIComponent(str)); }
+
+ let blockstart, i, j;
+ const W = new Array(80);
+ let H0 = 0x67452301, H1 = 0xEFCDAB89, H2 = 0x98BADCFE, H3 = 0x10325476, H4 = 0xC3D2E1F0;
+ let A, B, C, D, E, temp;
+
+ str = utf8Encode(str);
+ const str_len = str.length;
+ const word_array = [];
+
+ for (i = 0; i < str_len - 3; i += 4) {
+ j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3);
+ word_array.push(j);
+ }
+
+ switch (str_len % 4) {
+ case 0: i = 0x080000000; break;
+ case 1: i = str.charCodeAt(str_len - 1) << 24 | 0x0800000; break;
+ case 2: i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000; break;
+ case 3: i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) << 8 | 0x80; break;
+ }
+
+ word_array.push(i);
+ while ((word_array.length % 16) != 14) word_array.push(0);
+ word_array.push(str_len >>> 29);
+ word_array.push((str_len << 3) & 0x0ffffffff);
+
+ for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
+ for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
+ for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
+
+ A = H0; B = H1; C = H2; D = H3; E = H4;
+
+ for (i = 0; i <= 19; i++) {
+ temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+ for (i = 20; i <= 39; i++) {
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+ for (i = 40; i <= 59; i++) {
+ temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+ for (i = 60; i <= 79; i++) {
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+
+ H0 = (H0 + A) & 0x0ffffffff;
+ H1 = (H1 + B) & 0x0ffffffff;
+ H2 = (H2 + C) & 0x0ffffffff;
+ H3 = (H3 + D) & 0x0ffffffff;
+ H4 = (H4 + E) & 0x0ffffffff;
+ }
+
+ return cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
+}
+
+function mineSingleThread(prefix, targetZeros, progressCallback, foundCallback) {
+ const target = '0'.repeat(targetZeros);
+ let nonce = 0, checked = 0;
+ const batchSize = 100;
+
+ function mineNextBatch() {
+ const endNonce = nonce + batchSize;
+ while (nonce < endNonce) {
+ const token = prefix + nonce;
+ const hash = sha1(token);
+ checked++;
+ if (hash.startsWith(target)) {
+ foundCallback(nonce, checked);
+ return;
+ }
+ nonce++;
+ }
+ progressCallback(checked);
+ setTimeout(mineNextBatch, 0);
+ }
+ mineNextBatch();
+}
+
+// ============================================================================
+// STEP 1: HASHCASH TOKEN GENERATION
+// ============================================================================
+
+document.getElementById('genTokenBtn').onclick = () => {
+ const email = document.getElementById('hcEmail').value.trim();
+ const bits = parseInt(document.getElementById('hcBits').value);
+
+ if (!email) {
+ showNotification('⚠ Please enter your email address first!', 'warning');
+ document.getElementById('hcEmail').classList.add('error');
+ document.getElementById('hcEmail').focus();
+ return;
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ showNotification('⚠ Please enter a valid email address!', 'warning');
+ document.getElementById('hcEmail').classList.add('error');
+ document.getElementById('hcEmail').focus();
+ return;
+ }
+
+ document.getElementById('hcEmail').classList.remove('error');
+ showNotification(`πŸ”¨ Starting token generation (${bits} bits)...`, 'info');
+
+ const btn = document.getElementById('genTokenBtn');
+ btn.disabled = true;
+ btn.textContent = 'Mining Token...';
+
+ const now = new Date();
+ const timestamp = now.getUTCFullYear().toString().slice(2) +
+ ("0"+(now.getUTCMonth()+1)).slice(-2) +
+ ("0"+now.getUTCDate()).slice(-2) +
+ ("0"+now.getUTCHours()).slice(-2) +
+ ("0"+now.getUTCMinutes()).slice(-2) +
+ ("0"+now.getUTCSeconds()).slice(-2);
+ const ext = "";
+ const rand = Math.floor(Math.random()*1e6);
+ const prefix = `1:${bits}:${timestamp}:${email}:${ext}:${rand}:`;
+ const zeros = bits/4;
+
+ const progBar = document.getElementById('tokenProgress');
+ progBar.style.width = '0%';
+ progBar.innerText = 'Mining...';
+
+ let totalChecked = 0;
+ let startTime = Date.now();
+
+ function updateProgress(checked) {
+ totalChecked += checked;
+ const elapsed = (Date.now() - startTime) / 1000;
+ const hashRate = Math.round(totalChecked / elapsed);
+ progBar.style.width = Math.min(95, (totalChecked / 10000)) + '%';
+ progBar.innerText = `${totalChecked} hashes (${hashRate}/s)`;
+ }
+
+ function onFound(nonce, totalHashes) {
+ const token = prefix + nonce;
+ const outputField = document.getElementById('tokenOutput');
+ outputField.innerText = token;
+ outputField.classList.remove('empty');
+ document.getElementById('hcToken').value = token;
+
+ const fromName = document.getElementById('fromName').value || 'Anonymous';
+ document.getElementById('fromFull').value = `${fromName} <${email}>`;
+ document.getElementById('readonlyFromName').value = fromName;
+ document.getElementById('readonlyEmailSign').value = email;
+ document.getElementById('readonlyEmailSend').value = email;
+
+ btn.disabled = false;
+ btn.textContent = 'Generate Token';
+ progBar.style.width = '100%';
+ progBar.innerText = `Complete! (${totalHashes} hashes)`;
+
+ appState.step1Complete = true;
+ updateTabIndicators();
+
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ showNotification(`βœ“ Token generated in ${elapsed}s!`, 'success', 8000);
+
+ setTimeout(() => showTab('sign'), 2000);
+ }
+
+ if (workersSupported) {
+ try {
+ const cores = navigator.hardwareConcurrency || 2;
+ let found = false;
+ let coreChecked = Array(cores).fill(0);
+ const workers = [];
+
+ function updateCombinedProgress() {
+ const sum = coreChecked.reduce((a,b)=>a+b,0);
+ updateProgress(sum - totalChecked);
+ }
+
+ for (let i = 0; i < cores; i++) {
+ try {
+ const w = new Worker('/static/powWorker.js');
+ w.postMessage({ prefix, targetZeros: zeros, startNonce: i, step: cores });
+
+ w.onmessage = e => {
+ if (found) return;
+ if (e.data.type === 'found') {
+ found = true;
+ workers.forEach(x => x.terminate());
+ onFound(e.data.nonce, e.data.checked);
+ }
+ if (e.data.type === 'progress') {
+ coreChecked[i] = e.data.checked;
+ updateCombinedProgress();
+ }
+ };
+
+ w.onerror = (err) => {
+ if (i === 0) {
+ workers.forEach(x => x.terminate());
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ }
+ };
+
+ workers.push(w);
+ } catch (err) {
+ if (i === 0) {
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ return;
+ }
+ }
+ }
+
+ } catch (err) {
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ }
+ } else {
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ }
+};
+
+// ============================================================================
+// STEP 2: ED25519 SIGNATURE + KEYPAIR MANAGEMENT
+// ============================================================================
+
+document.getElementById('genKeyBtn').onclick = async function() {
+ try {
+ showNotification('πŸ” Generating Ed25519 key pair...', 'info');
+ keyPair = nacl.sign.keyPair();
+
+ const pubB64 = nacl.util.encodeBase64(keyPair.publicKey);
+
+ const pubOutput = document.getElementById('pubKeyOutput');
+ pubOutput.innerText = pubB64;
+ pubOutput.classList.remove('empty');
+ document.getElementById('x-ed25519-pub').value = pubB64;
+ document.getElementById('signMsgBtn').disabled = false;
+
+ const username = document.getElementById('fromName').value || 'Anonymous';
+ const email = document.getElementById('hcEmail').value || '';
+ document.getElementById('identityLoadedPow').style.display = 'flex';
+ document.getElementById('loadedIdentityName').textContent = username;
+ document.getElementById('loadedIdentityEmail').textContent = email || 'Identity active only in memory';
+
+ await updateIdenticonPreview();
+ showNotification('βœ“ Identity generated in memory. Now write and sign your message.', 'success', 6000);
+ } catch (error) {
+ showNotification('βœ— Error: ' + error.message, 'error');
+ }
+};
+
+document.getElementById('loadIdentityBtn').onclick = function() {
+ document.getElementById('identityFileInput').click();
+};
+
+document.getElementById('identityFileInput').onchange = async function(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ try {
+ const text = await file.text();
+ const data = JSON.parse(text);
+
+ if (!data.publicKey || !data.secretKey) {
+ throw new Error('Invalid identity file format');
+ }
+
+ const pubKey = nacl.util.decodeBase64(data.publicKey);
+ const secKey = nacl.util.decodeBase64(data.secretKey);
+
+ if (pubKey.length !== 32 || secKey.length !== 64) {
+ throw new Error('Invalid key lengths');
+ }
+
+ keyPair = { publicKey: pubKey, secretKey: secKey };
+
+ const pubB64 = data.publicKey;
+ const pubOutput = document.getElementById('pubKeyOutput');
+ pubOutput.innerText = pubB64;
+ pubOutput.classList.remove('empty');
+ document.getElementById('x-ed25519-pub').value = pubB64;
+ document.getElementById('signMsgBtn').disabled = false;
+
+ if (data.username) {
+ document.getElementById('fromName').value = data.username;
+ document.getElementById('readonlyFromName').value = data.username;
+ }
+ if (data.email) {
+ document.getElementById('hcEmail').value = data.email;
+ const fromName = document.getElementById('fromName').value || 'Anonymous';
+ document.getElementById('fromFull').value = `${fromName} <${data.email}>`;
+ document.getElementById('readonlyEmailSign').value = data.email;
+ document.getElementById('readonlyEmailSend').value = data.email;
+ }
+
+ const username = data.username || 'Anonymous';
+ const email = data.email || '';
+ document.getElementById('identityLoadedPow').style.display = 'flex';
+ document.getElementById('loadedIdentityName').textContent = username;
+ document.getElementById('loadedIdentityEmail').textContent = email;
+
+ await updateIdenticonPreview();
+
+ if (currentIdenticonData && currentIdenticonData.dataUrl) {
+ document.getElementById('identiconPreviewPow').src = currentIdenticonData.dataUrl;
+ }
+
+ document.getElementById('genKeyBtn').textContent = 'πŸ”‘ Replace Keypair';
+ showNotification('βœ“ Identity loaded into memory only. Now generate PoW token.', 'success', 6000);
+ } catch (error) {
+ showNotification('βœ— Failed to load identity: ' + error.message, 'error');
+ }
+
+ e.target.value = '';
+};
+
+// Usenet line-wrap convention (RFC 1855 Netiquette Guidelines): wrap plain-text
+// body lines at ~72 columns for compatibility with old-style newsreaders.
+// Must run BEFORE signing, not after, so the Ed25519 signature covers the
+// exact bytes that end up posted to Usenet.
+function wrapUsenetText(text, width) {
+ width = width || 72;
+ return text.split('\n').map(function(line) {
+ if (line.length === 0 || line.startsWith('>')) return line;
+ const words = line.split(' ');
+ const wrapped = [];
+ let current = '';
+ for (const word of words) {
+ if (current === '') {
+ current = word;
+ } else if ((current + ' ' + word).length <= width) {
+ current += ' ' + word;
+ } else {
+ wrapped.push(current);
+ current = word;
+ }
+ }
+ wrapped.push(current);
+ return wrapped.join('\n');
+ }).join('\n');
+}
+
+document.getElementById('signMsgBtn').onclick = function() {
+ try {
+ if (!keyPair) {
+ showNotification('⚠ Please generate or load a key pair first!', 'warning');
+ return;
+ }
+
+ const msg = wrapUsenetText(document.getElementById('messageToSign').value.trim());
+ if (!msg) {
+ showNotification('⚠ Please write a message!', 'warning');
+ document.getElementById('messageToSign').classList.add('error');
+ document.getElementById('messageToSign').focus();
+ return;
+ }
+
+ if (msg.length < 10) {
+ showNotification('⚠ Message too short (min 10 characters)', 'warning');
+ return;
+ }
+
+ document.getElementById('messageToSign').classList.remove('error');
+ showNotification('✍ Signing message...', 'info');
+
+ const msgBytes = nacl.util.decodeUTF8(msg);
+ const sig = nacl.sign.detached(msgBytes, keyPair.secretKey);
+ const sigB64 = nacl.util.encodeBase64(sig);
+
+ const sigOutput = document.getElementById('signatureOutput');
+ sigOutput.innerText = sigB64;
+ sigOutput.classList.remove('empty');
+ document.getElementById('x-ed25519-sig').value = sigB64;
+
+ document.getElementById('messageContent').value = msg + "\n\n--- Digital Signature ---\n" + sigB64;
+
+ appState.step2Complete = true;
+ updateTabIndicators();
+ showNotification('βœ“ Message signed with your in-memory identity.', 'success', 8000);
+ setTimeout(() => showTab('send'), 1200);
+ } catch (error) {
+ showNotification('βœ— Error: ' + error.message, 'error');
+ }
+};
+
+document.getElementById('fromName').addEventListener('input', function() {
+ const email = document.getElementById('hcEmail').value;
+ document.getElementById('readonlyFromName').value = this.value;
+ if (email) {
+ document.getElementById('fromFull').value = `${this.value} <${email}>`;
+ }
+ if (keyPair) {
+ updateIdenticonPreview();
+ }
+});
+
+// ============================================================================
+// STEP 3: SEND FORM
+// ============================================================================
+
+document.getElementById('sendForm').addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ const requiredFields = [
+ {id: 'fromFull', name: 'From'},
+ {id: 'newsgroups', name: 'Newsgroups'},
+ {id: 'subject', name: 'Subject'},
+ {id: 'hcToken', name: 'Hashcash Token'},
+ {id: 'messageContent', name: 'Message'},
+ {id: 'x-ed25519-pub', name: 'Public Key'},
+ {id: 'x-ed25519-sig', name: 'Signature'}
+ ];
+
+ const missing = [];
+ requiredFields.forEach(field => {
+ const el = document.getElementById(field.id);
+ if (!el.value.trim()) {
+ missing.push(field.name);
+ el.classList.add('error');
+ } else {
+ el.classList.remove('error');
+ }
+ });
+
+ if (missing.length > 0) {
+ showNotification(`⚠ Missing: ${missing.join(', ')}`, 'warning', 8000);
+ return;
+ }
+
+ const messageLen = document.getElementById('messageContent').value.length;
+ if (messageLen < 10) {
+ showNotification('⚠ Message too short', 'warning');
+ return;
+ }
+
+ const newsgroups = document.getElementById('newsgroups').value.split(',').map(g => g.trim()).filter(g => g);
+ if (newsgroups.length === 0) {
+ showNotification('⚠ Enter at least one newsgroup!', 'warning');
+ return;
+ }
+
+ if (newsgroups.length > 3) {
+ showNotification('⚠ Max 3 newsgroups!', 'warning');
+ return;
+ }
+
+ const confirmMsg = `Send message?\n\nNewsgroups: ${newsgroups.join(', ')}\nSubject: ${document.getElementById('subject').value}\nLength: ${messageLen} chars\n\nDelivery via Nym may take up to 120 seconds.`;
+
+ if (!confirm(confirmMsg)) {
+ return;
+ }
+
+ showNotification('πŸ“€ Sending message...', 'info', 0);
+ const sendBtn = document.getElementById('sendBtn');
+ sendBtn.disabled = true;
+ sendBtn.textContent = 'Sending...';
+ sendBtn.style.opacity = '0.6';
+ sendBtn.style.cursor = 'not-allowed';
+
+ const progressDiv = document.createElement('div');
+ progressDiv.id = 'sending-progress';
+ progressDiv.style.cssText = 'margin-top: 10px; padding: 10px; background: #fff3cd; border-left: 4px solid #ffc107; color: #856404;';
+ progressDiv.innerHTML = '<strong>⏳ Sending message...</strong><br>This may take up to 120 seconds via Nym. Please wait.';
+ sendBtn.parentNode.insertBefore(progressDiv, sendBtn.nextSibling);
+
+ try {
+ const resp = await fetch('/submit', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
+ },
+ body: new URLSearchParams(new FormData(this)).toString(),
+ credentials: 'same-origin',
+ cache: 'no-store'
+ });
+ let data = {};
+ try {
+ data = await resp.json();
+ } catch (err) {}
+ if (!resp.ok) {
+ throw new Error(data.error || 'Delivery failed.');
+ }
+ appState.step3Complete = true;
+ updateTabIndicators();
+ resetEphemeralState(data.messageId || '');
+ } catch (error) {
+ if (progressDiv.parentNode) progressDiv.remove();
+ sendBtn.disabled = false;
+ sendBtn.textContent = 'Send Message';
+ sendBtn.style.opacity = '';
+ sendBtn.style.cursor = '';
+ showNotification('βœ— ' + error.message, 'error', 10000);
+ }
+});
+
+// ============================================================================
+// ONION-NEWSREADER INTEGRATION
+// ============================================================================
+
+(function() {
+ function decode(s) {
+ if (!s) return '';
+ try {
+ return decodeURIComponent(s.replace(/\+/g, ' '));
+ } catch(e) {
+ return s;
+ }
+ }
+
+ function fillField(id, value, highlight) {
+ highlight = highlight !== false;
+ const field = document.getElementById(id);
+ if (field && value) {
+ field.value = value;
+ if (highlight) field.classList.add('prefilled');
+ return true;
+ }
+ return false;
+ }
+
+ function showPrefillBanner(isReply, details) {
+ const banner = document.getElementById('prefillBanner');
+ const title = document.getElementById('prefillTitle');
+ const detailsEl = document.getElementById('prefillDetails');
+
+ if (banner) {
+ banner.style.display = 'flex';
+ if (isReply) {
+ banner.classList.add('reply');
+ title.textContent = 'πŸ’¬ Reply Mode';
+ } else {
+ title.textContent = 'πŸ“ New Post';
+ }
+ detailsEl.textContent = details;
+ }
+ }
+
+ let prefillData = null;
+ let prefillSource = null;
+ const params = new URLSearchParams(window.location.search);
+ const action = params.get('action');
+
+ if (action === 'new' || action === 'reply' || params.has('newsgroups')) {
+ prefillData = {
+ newsgroups: decode(params.get('newsgroups') || ''),
+ subject: decode(params.get('subject') || ''),
+ references: decode(params.get('references') || ''),
+ body: decode(params.get('body') || '')
+ };
+ prefillSource = 'URL';
+ }
+
+ if (prefillData) {
+ let filledFields = [];
+
+ if (prefillData.newsgroups) {
+ fillField('newsgroups', prefillData.newsgroups);
+ filledFields.push('Newsgroups');
+ }
+
+ if (prefillData.subject) {
+ fillField('subject', prefillData.subject);
+ filledFields.push('Subject');
+ }
+
+ if (prefillData.references) {
+ fillField('references', prefillData.references);
+ filledFields.push('References');
+ }
+
+ if (prefillData.body) {
+ fillField('messageToSign', prefillData.body);
+ filledFields.push('Quoted message');
+ }
+
+ const isReply = !!(prefillData.references ||
+ (prefillData.subject && prefillData.subject.toLowerCase().startsWith('re:')));
+
+ if (filledFields.length > 0) {
+ const details = `Pre-filled: ${filledFields.join(', ')} (from ${prefillSource})`;
+ showPrefillBanner(isReply, details);
+
+ if (isReply) {
+ showNotification(`πŸ’¬ Reply mode: ${prefillData.newsgroups}`, 'info', 5000);
+ } else {
+ showNotification(`πŸ“ New post to: ${prefillData.newsgroups}`, 'success', 5000);
+ }
+ }
+
+ setTimeout(() => showTab('pow'), 300);
+ }
+})();
+</script>
+
+</body>
+</html>