@banou/ripple 0.0.4 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +141 -3
  2. package/_headers +14 -0
  3. package/api-Jq4OSTp1-B4uxQLLk.js +338 -0
  4. package/app.webmanifest +50 -0
  5. package/assets/empty-BvRk9kiK.js +0 -0
  6. package/assets/libtorrent-DS_9Yo8M.js +6 -0
  7. package/assets/sintel.torrent +0 -0
  8. package/assets/worker-SJZDGuG-.js +23 -0
  9. package/build-DEOZ3A7n.js +1846 -0
  10. package/index.js +44658 -0
  11. package/jassub/default.woff2 +0 -0
  12. package/jassub/fonts.js +1 -0
  13. package/jassub/jassub-assets.js +0 -0
  14. package/jassub/jassub-worker-modern.wasm +0 -0
  15. package/jassub/jassub-worker.js +1 -0
  16. package/jassub/jassub-worker.wasm +0 -0
  17. package/jassub/worker.js +148 -0
  18. package/libav-jspi.wasm +0 -0
  19. package/libav-worker.js +4824 -0
  20. package/libav.wasm +0 -0
  21. package/package.json +14 -81
  22. package/remote-BLaEXLm6.js +209 -0
  23. package/sw.js +192 -0
  24. package/build/app.webmanifest +0 -65
  25. package/build/assets/index-be95001c.js +0 -211
  26. package/build/assets/index-d5b720ec.js +0 -109
  27. package/build/assets/io-worker-bde41815.js +0 -17681
  28. package/build/index-3576f032.js +0 -14881
  29. package/build/index-65b2197a.js +0 -116
  30. package/build/index-9a320571.js +0 -107
  31. package/build/index.js +0 -33826
  32. package/build/io-worker-190cc8d7.js +0 -2862
  33. package/build/jassub-worker-modern.wasm +0 -0
  34. package/build/jassub-worker.js +0 -15
  35. package/build/libav-worker.js +0 -1565
  36. package/build/libav.wasm +0 -0
  37. package/build/shared-worker.js +0 -13
  38. package/build/worker.js +0 -38
  39. /package/{build/assets → assets}/logo.png +0 -0
  40. /package/{build/assets → assets}/logo.svg +0 -0
  41. /package/{build/index.html → index.html} +0 -0
package/README.md CHANGED
@@ -1,3 +1,141 @@
1
- # Ripple, the torrent app that respect your privacy
2
-
3
- THE app that allows you to download torrents and stream video files from the safety of your browser!
1
+ # Ripple, the torrent app that respect your privacy
2
+
3
+ THE app that allows you to download torrents and stream video files from the safety of your browser!
4
+
5
+ ## Embedding
6
+
7
+ `/watch` and `/download` are the pages another site puts in an iframe. Both take a magnet and the
8
+ same query; the path chooses which one renders. `/watch` plays the file, `/download` delivers it.
9
+
10
+ | param | value | meaning |
11
+ | --- | --- | --- |
12
+ | `m` | the packed magnet, base64url | what Ripple writes today |
13
+ | `magnet` | base64 of the magnet URI | the original form, read forever |
14
+ | `fileIndex` | a file index | the file `/watch` plays; the fallback `/download` uses |
15
+ | `files` | `all`, `3`, `0-4`, `0,2,5` | what `/download` delivers |
16
+ | `f` | a packed file list | optional preview, `/download` only |
17
+
18
+ One of `m` or `magnet` is required. `m` wins if both are present.
19
+
20
+ There is no `mode` parameter. It used to choose the page and is now neither written nor read, so a
21
+ link still carrying one gets the page its PATH names and the parameter is ignored.
22
+
23
+ ### The two magnet forms
24
+
25
+ `magnet=<base64 of the magnet URI>` is the original, and it is **permanent**. Every link ever handed
26
+ out with it keeps working, and an embedder that finds it easier to write can keep writing it. Nothing
27
+ about it has changed.
28
+
29
+ `m=` is the same torrent packed smaller: the infohash as raw bytes rather than 40 hex characters, and
30
+ the rest of the query deflated against a fixed table of the announce URLs that public magnets
31
+ overwhelmingly share. It is base64url, so a query string carries it without escaping anything. Across
32
+ a corpus covering every magnet form Ripple accepts, the median link is **69% shorter**; a five-tracker
33
+ release goes from a 549-character URL to 148. A magnet using only private trackers has nothing to
34
+ match against and still comes out around 42% shorter, on the strength of the packed infohash alone.
35
+
36
+ Ripple writes whichever of the two is shorter, which is almost always `m`. The digit is a version:
37
+ the table it compresses against can never be edited, so if it ever needs to change the parameter
38
+ becomes `m2` and `m` keeps decoding the way it always did.
39
+
40
+ ### Parameter order
41
+
42
+ Order never matters when READING a link. When writing one, Ripple puts the plainest parts first:
43
+
44
+ ```
45
+ /watch?fileIndex=3&m=AQAIraWnphg6rh4J2DHfZ0jVZglaEAMWq8GZeSWpORSmTXxFBwA
46
+ /download?files=2&m=AQAIraWnphg6rh4J2DHfZ0jVZglaEAMWq8GZeSWpORSmTXxFBwA
47
+ /download?m=AQAIraWnphg6rh4J2DHfZ0jVZglaEAMWq8GZeSWpORSmTXxFBwA&f=AWNSC87MK0nN0YdQ
48
+ ```
49
+
50
+ The path, then which files, then the packed torrent, then `f`. The torrent used to lead, from when
51
+ it was the only thing in the link; now that everything around it is base64url or an index, leading
52
+ with it buried the one part a person can read behind forty characters of noise. A URL is read from
53
+ the left and truncated from the right, so `f`, the longest parameter and the one a reader cares
54
+ least about, goes last.
55
+
56
+ ### `f`, the file list
57
+
58
+ A magnet names a torrent and nothing else, so a download link normally opens on "Reading the torrent
59
+ from the network" for as long as metadata takes. `f` closes that gap by carrying the file list the
60
+ sender already had, and the page shows it immediately, marked **from the link**.
61
+
62
+ It is a preview and Ripple treats it as one. It never decides what gets downloaded: the button stays
63
+ disabled until real metadata arrives, the download itself is resolved against that metadata, and the
64
+ per-file buttons are not rendered at all while the list is only the link's claim. A link that
65
+ describes a torrent inaccurately therefore costs a reader a wrong line on screen for a few seconds,
66
+ and can never cost them the wrong file on disk.
67
+
68
+ Ripple writes it only on `/download`, only when it has the list, and only when it fits: a
69
+ 12-episode season costs about 172 characters and a 48-file season about 416. Past a budget it is
70
+ left off entirely rather than pushing the link past what a chat message will carry. Absent, `f`
71
+ changes nothing, so a link without it behaves exactly as it always has.
72
+
73
+ Putting the whole `.torrent` in the URL instead does not work, and the reason is arithmetic rather
74
+ than encoding: piece hashes are around 94% of a torrent and are 20 bytes of SHA1 per piece, so they
75
+ are incompressible. A 12-episode season is a 28,512-character URL and a 40 GB remux is 68,676. The
76
+ file list is the part that is both small and worth having.
77
+
78
+ ### `/watch`
79
+
80
+ Plays `fileIndex` (0 if absent) in the media player, with the filename, peer count and transfer
81
+ rates drawn over the video.
82
+
83
+ The mode used to be a parameter, and for a while Ripple wrote `mode=watch` out in full so that a
84
+ link said what it did rather than being told apart by an absence. The path says it for free and in
85
+ the part of a URL that is read first, so the parameter is gone from both halves and a link still
86
+ carrying one gets the page its path names.
87
+
88
+ **This route was `/embed` until 2026-09-09 and the old path is gone rather than redirected.** A link
89
+ written against it 404s to the SPA shell; rewrite it as `/watch` with the query untouched.
90
+
91
+ ### `/download`
92
+
93
+ A download page: the release name, the size of the selection, and one button. One file is delivered
94
+ as that file; anything more is delivered as a single `.zip`, written straight through to the
95
+ browser's own downloader without ever being held in memory.
96
+
97
+ ```
98
+ /download?magnet=<base64> the whole torrent, as a zip
99
+ /download?magnet=<base64>&files=3 just file 3
100
+ /download?magnet=<base64>&files=0-4 files 0 to 4 inclusive, as a zip
101
+ /download?magnet=<base64>&files=0,2,5 those three, as a zip
102
+ /download?magnet=<base64>&fileIndex=3 same as files=3, so a watch URL becomes a download by
103
+ swapping /watch for /download and nothing else
104
+ ```
105
+
106
+ `files` outranks `fileIndex`. Indices the torrent does not have are dropped rather than clamped, and
107
+ a selection that resolves to nothing says so instead of quietly downloading the whole torrent.
108
+
109
+ ### What an embedder has to grant
110
+
111
+ **The frame needs `allow-downloads` if it is sandboxed at all.** Sandbox flags on a nested browsing
112
+ context are the union of the parent's set and the child's, so a flag the embedder withheld cannot be
113
+ restored from inside, and Chrome then refuses the download *silently*: the navigation is dropped, no
114
+ event fires and nothing throws. There is no `downloads` feature in Permissions-Policy, so `allow=`
115
+ is not a lever here; the `sandbox` attribute is the only one.
116
+
117
+ ```html
118
+ <iframe src="https://torrent.fkn.app/download?magnet=..."
119
+ sandbox="allow-scripts allow-same-origin allow-downloads"></iframe>
120
+ ```
121
+
122
+ Omitting `sandbox` entirely works too. When the page detects it is framed by another origin it
123
+ offers a link to open itself top-level, which is the way out if the embedder cannot grant the flag.
124
+
125
+ Two other consequences of being framed, both handled: Chrome refuses `showSaveFilePicker` in a
126
+ cross-origin frame, so the page does not ask for one, and delivery goes through the service worker
127
+ instead.
128
+
129
+ ## Tests
130
+
131
+ | command | what it covers | cost |
132
+ | --- | --- | --- |
133
+ | `npm test` | pure logic in node | ~1s |
134
+ | `npm run test:browser` | components in real Chrome | ~2s |
135
+ | `npx tsc --noEmit` | types | ~1s |
136
+ | `npx vp lint` | oxlint, type aware | ~1s |
137
+ | `npm run test:download` | the download page against a real torrent, end to end | ~30s |
138
+ | `npm run test:e2e` | the engine against real swarms | minutes, headful |
139
+
140
+ The playwright suites run headful on purpose: headless Chromium stalls the engine at a flat 0 B/s in
141
+ every topology, which makes anything torrent-shaped unmeasurable rather than merely slow.
package/_headers ADDED
@@ -0,0 +1,14 @@
1
+ # Cloudflare Pages serves this verbatim. Path order matters: the first matching block wins.
2
+
3
+ # /add asks a person to agree to something on behalf of whatever site linked them here, so it must
4
+ # never render inside a frame. A page that can size and position an invisible iframe can put the Add
5
+ # button under the visitor's cursor while they believe they are clicking something else, and nothing
6
+ # written inside the frame helps, because the surrounding page chooses what is visible.
7
+ #
8
+ # The page checks `window.top !== window.self` as well. This header is the half that works before any
9
+ # script runs, and the check is the half that survives a host serving these files without it.
10
+ /add
11
+ X-Frame-Options: DENY
12
+ Content-Security-Policy: frame-ancestors 'none'
13
+
14
+ # NOT /watch or /download, deliberately: being framed by another site is the entire point of both.
@@ -0,0 +1,338 @@
1
+ import { a as e } from "./build-DEOZ3A7n.js";
2
+ //#region \0rolldown/runtime.js
3
+ var t = Object.create, n = Object.defineProperty, r = Object.getOwnPropertyDescriptor, i = Object.getOwnPropertyNames, a = Object.getPrototypeOf, o = Object.prototype.hasOwnProperty, s = (e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t.exports), c = (e, t) => {
4
+ let r = {};
5
+ for (var i in e) n(r, i, {
6
+ get: e[i],
7
+ enumerable: !0
8
+ });
9
+ return t || n(r, Symbol.toStringTag, { value: "Module" }), r;
10
+ }, l = (e, t, a, s) => {
11
+ if (t && typeof t == "object" || typeof t == "function") for (var c = i(t), l = 0, u = c.length, d; l < u; l++) d = c[l], !o.call(e, d) && d !== a && n(e, d, {
12
+ get: ((e) => t[e]).bind(null, d),
13
+ enumerable: !(s = r(t, d)) || s.enumerable
14
+ });
15
+ return e;
16
+ }, u = (e, r, i) => (i = e == null ? {} : t(a(e)), l(r || !e || !e.__esModule ? n(i, "default", {
17
+ value: e,
18
+ enumerable: !0
19
+ }) : i, e)), d = Object.defineProperty, f = (e, t) => {
20
+ let n = {};
21
+ for (var r in e) d(n, r, {
22
+ get: e[r],
23
+ enumerable: !0
24
+ });
25
+ return t || d(n, Symbol.toStringTag, { value: "Module" }), n;
26
+ }, ee = /* @__PURE__ */ c({
27
+ a: () => Q,
28
+ i: () => Y,
29
+ n: () => Ce,
30
+ o: () => J,
31
+ r: () => $,
32
+ t: () => Z
33
+ }), te = 1e4, ne = 16, p = (e) => {
34
+ let t;
35
+ try {
36
+ t = e.contentWindow;
37
+ } catch {
38
+ return !0;
39
+ }
40
+ if (!t) return !0;
41
+ try {
42
+ let e = t.location.href;
43
+ return e !== "" && e !== "about:blank";
44
+ } catch {
45
+ return !0;
46
+ }
47
+ }, m = (e, t = te) => new Promise((n) => {
48
+ if (p(e)) return n();
49
+ let r = () => {
50
+ clearInterval(i), clearTimeout(a), e.removeEventListener("load", r), n();
51
+ }, i = setInterval(() => {
52
+ p(e) && r();
53
+ }, ne), a = setTimeout(r, t);
54
+ e.addEventListener("load", r);
55
+ }), re = 32, ie = 8, ae = 120, h = typeof CSS < "u" && CSS.supports("clip-path", "path('M0 0H1V1H0Z')"), g = typeof CSS < "u" && CSS.supports("clip-path", "polygon(nonzero, 0px 0px, 1px 0px, 1px 1px)"), oe = (e) => {
56
+ let t = e.style;
57
+ t.setProperty("display", "block", "important"), t.setProperty("position", "fixed", "important"), t.setProperty("top", "0", "important"), t.setProperty("left", "0", "important"), t.setProperty("width", "100%", "important"), t.setProperty("height", "100%", "important"), t.setProperty("border", "none", "important"), t.setProperty("padding", "0", "important"), t.setProperty("box-sizing", "border-box", "important"), t.setProperty("clip-path", "inset(100%)", "important"), t.setProperty("background", "transparent"), t.setProperty("color-scheme", "normal"), t.setProperty("z-index", "2147483647"), e.setAttribute("title", "FKN");
58
+ }, se = ({ x: e, y: t, width: n, height: r, radius: i }) => {
59
+ if (!i) return `M${e} ${t}h${n}v${r}h${-n}Z`;
60
+ let a = Math.min(n, r) / 2, [o, s, c, l] = i.map((e) => Math.max(0, Math.min(e, a)));
61
+ return `M${e + o} ${t}h${n - o - s}a${s} ${s} 0 0 1 ${s} ${s}v${r - s - c}a${c} ${c} 0 0 1 ${-c} ${c}h${-(n - c - l)}a${l} ${l} 0 0 1 ${-l} ${-l}v${-(r - l - o)}a${o} ${o} 0 0 1 ${o} ${-o}Z`;
62
+ }, ce = () => h && g, _ = ({ x: e, y: t, width: n, height: r }) => [
63
+ [e, t],
64
+ [e + n, t],
65
+ [e + n, t + r],
66
+ [e, t + r],
67
+ [e, t]
68
+ ], le = (e) => {
69
+ let t = e[0];
70
+ if (!t) return [];
71
+ let n = _(t)[0], r = [];
72
+ return e.forEach((t, i) => {
73
+ r.push(..._(t)), i > 0 && i < e.length - 1 && r.push(n);
74
+ }), r;
75
+ }, v = (e) => `polygon(nonzero, ${le(e).map(([e, t]) => `${e}px ${t}px`).join(", ")})`, ue = ({ x: e, y: t, width: n, height: r, radius: i }, a) => {
76
+ let o = `${t}px ${a.width - e - n}px ${a.height - t - r}px ${e}px`;
77
+ return i ? `inset(${o} round ${i.map((e) => `${e}px`).join(" ")})` : `inset(${o})`;
78
+ }, y = (e, t, n) => {
79
+ if (t) return "none";
80
+ if (e.length === 0) return "inset(100%)";
81
+ let r = e.length === 1 ? e[0] : void 0;
82
+ return r ? ue(r, n) : g && e.length <= 5 && !e.some((e) => e.radius) ? v(e) : `path('${e.map(se).join("")}')`;
83
+ }, de = (e) => {
84
+ if (e.length < 2) return null;
85
+ let t = Math.min(...e.map((e) => e.x)), n = Math.min(...e.map((e) => e.y));
86
+ return {
87
+ x: t,
88
+ y: n,
89
+ width: Math.max(...e.map((e) => e.x + e.width)) - t,
90
+ height: Math.max(...e.map((e) => e.y + e.height)) - n
91
+ };
92
+ }, b = (e, t, n) => t >= e.x && t < e.x + e.width && n >= e.y && n < e.y + e.height, x = 16, S = (e, t, n) => t === 1 ? e * x : t === 2 ? e * n : e, C = (e, t, n) => {
93
+ let r = getComputedStyle(e);
94
+ return n !== 0 && /auto|scroll/.test(r.overflowY) && e.scrollHeight > e.clientHeight ? !0 : t !== 0 && /auto|scroll/.test(r.overflowX) && e.scrollWidth > e.clientWidth;
95
+ }, fe = (e, t, n) => {
96
+ for (let r = e; r; r = r.parentElement) if (C(r, t, n)) return r;
97
+ return document.scrollingElement;
98
+ }, w = null, T = null, E = (e) => ({
99
+ width: e.offsetWidth || window.innerWidth,
100
+ height: e.offsetHeight || window.innerHeight
101
+ }), D = (e) => {
102
+ if (!T || !w || !b(T, e.clientX, e.clientY)) return;
103
+ let t = document.elementFromPoint(e.clientX, e.clientY);
104
+ if (!t || t === w) return;
105
+ let n = S(e.deltaX, e.deltaMode, window.innerWidth), r = S(e.deltaY, e.deltaMode, window.innerHeight), i = fe(t, n, r);
106
+ i && (i.scrollBy({
107
+ left: n,
108
+ top: r,
109
+ behavior: "instant"
110
+ }), e.cancelable && e.preventDefault());
111
+ }, pe = (e) => {
112
+ let t = T !== null;
113
+ T = e, t !== (e !== null) && (e ? window.addEventListener("wheel", D, {
114
+ capture: !0,
115
+ passive: !1
116
+ }) : window.removeEventListener("wheel", D, { capture: !0 }));
117
+ }, O = 0, k = (e) => {
118
+ if (e === O) return;
119
+ O = e;
120
+ let t = document.documentElement.style;
121
+ e > 0 ? (t.setProperty("margin-top", `${e}px`, "important"), t.setProperty("--fkn-inset-top", `${e}px`)) : (t.removeProperty("margin-top"), t.removeProperty("--fkn-inset-top"));
122
+ }, A = (e, { modal: t, rects: n, inset: r }, i) => {
123
+ if (k(r), !h) {
124
+ let r = t || n.length > 0;
125
+ e.style.setProperty("pointer-events", r ? "auto" : "none", "important"), e.style.setProperty("visibility", r ? "visible" : "hidden", "important");
126
+ return;
127
+ }
128
+ let a = y(n, t, i), o = a === "none" || CSS.supports("clip-path", a) ? a : "inset(100%)";
129
+ e.style.setProperty("clip-path", o, "important"), pe(t || o !== a ? null : de(n));
130
+ }, j = (e) => {
131
+ if (typeof e != "object" || !e) return 0;
132
+ let { top: t } = e;
133
+ return typeof t != "number" || !Number.isFinite(t) || t <= 0 ? 0 : Math.min(Math.round(t), ae);
134
+ }, M = (e) => {
135
+ if (!(!Array.isArray(e) || e.length !== 4) && e.every((e) => typeof e == "number" && Number.isFinite(e) && e >= 0)) return e.some((e) => e > 0) ? e : void 0;
136
+ }, N = (e, t) => {
137
+ if (typeof e != "object" || !e) return null;
138
+ let { x: n, y: r, width: i, height: a } = e;
139
+ if (![
140
+ n,
141
+ r,
142
+ i,
143
+ a
144
+ ].every((e) => typeof e == "number" && Number.isFinite(e))) return null;
145
+ let o = t.width, s = t.height, c = Math.max(0, Math.min(Math.round(n), o)), l = Math.max(0, Math.min(Math.round(r), s)), u = Math.max(c, Math.min(Math.round(n + i), o)), d = Math.max(l, Math.min(Math.round(r + a), s)), f = M(e.radius);
146
+ return {
147
+ x: c,
148
+ y: l,
149
+ width: u - c,
150
+ height: d - l,
151
+ ...f ? { radius: f } : {}
152
+ };
153
+ }, P = {
154
+ "not-rendered": "this page is not painting it at all, so it or an ancestor is display:none, or the frame was moved out of the layout",
155
+ clipped: "it is clipped away or positioned outside the viewport",
156
+ "not-shown": "it is rendered but would not be seen, so either something is drawn over it, or it or an ancestor has opacity below 1, visibility:hidden, or a filter"
157
+ }, F = (e) => Array.isArray(e) ? e.slice(0, ie).flatMap((e) => {
158
+ if (typeof e != "object" || !e) return [];
159
+ let { kind: t, cause: n, level: r } = e;
160
+ return typeof t != "string" || t.length > 40 || typeof n != "string" || !(n in P) || r !== "full" && r !== "liveness" ? [] : [{
161
+ kind: t,
162
+ cause: n,
163
+ level: r
164
+ }];
165
+ }) : [], I = /* @__PURE__ */ new Set(), L = (e) => {
166
+ for (let { kind: t, cause: n, level: r } of e) I.has(t) || (I.add(t), console.error(`[FKN] The FKN ${JSON.stringify(t)} is not visible on this page: ${P[n]}. Nobody using this page can see or answer it.
167
+ @fkn/lib draws its UI inside <iframe title="FKN"> and sets display, position, top, left, width, height, border, padding, box-sizing, clip-path and z-index on that element. Leave those as they are, then check for an ancestor with opacity, visibility, display or a clipping transform, and for an element stacked above it.` + (r === "liveness" ? "\nThis browser reports limited detail, so the cause above is a best guess." : "")));
168
+ }, R = (e, t) => {
169
+ if (typeof e != "object" || !e) return null;
170
+ let n = e, r = (Array.isArray(n.rects) ? n.rects.slice(0, re) : []).map((e) => N(e, t)).filter((e) => e !== null && e.width > 0 && e.height > 0);
171
+ return {
172
+ modal: n.modal === !0,
173
+ rects: r,
174
+ hidden: F(n.hidden),
175
+ inset: j(n.inset)
176
+ };
177
+ }, z = () => w !== null, B = null, V = null, H = () => {
178
+ if (!w || B === null) return;
179
+ let e = E(w), t = R(B, e);
180
+ t && A(w, t, e);
181
+ }, me = (e) => {
182
+ typeof ResizeObserver > "u" || (V ??= new ResizeObserver(H), V.disconnect(), V.observe(e));
183
+ }, he = (e) => {
184
+ oe(e), A(e, {
185
+ modal: !1,
186
+ rects: [],
187
+ hidden: [],
188
+ inset: 0
189
+ }, E(e)), w = e, me(e);
190
+ }, ge = (e) => {
191
+ if (!w) return;
192
+ let t = E(w), n = R(e, t);
193
+ n && (B = e, A(w, n, t), L(n.hidden));
194
+ }, U = "https://fkn.app", W = typeof globalThis < "u" && globalThis.document ? globalThis.document : void 0, G = typeof globalThis < "u" && globalThis.crossOriginIsolated === !0, K = `${U}/api${G ? "?coi=1" : ""}`, _e = "fkn-api-port", ve = "fkn-api-port-ack", ye = 2e3, be = () => {
195
+ if (!W || typeof window > "u" || window.parent === window) return Promise.resolve(void 0);
196
+ let e;
197
+ try {
198
+ e = window.parent;
199
+ } catch {
200
+ return Promise.resolve(void 0);
201
+ }
202
+ let t = new MessageChannel();
203
+ return new Promise((n) => {
204
+ let r = (e) => {
205
+ if (clearTimeout(a), t.port1.removeEventListener("message", i), !e) try {
206
+ t.port1.close();
207
+ } catch {}
208
+ n(e);
209
+ }, i = (e) => {
210
+ e.data?.type === ve && r(t.port1);
211
+ }, a = setTimeout(() => r(void 0), ye);
212
+ t.port1.addEventListener("message", i), t.port1.start();
213
+ try {
214
+ e.postMessage({ type: _e }, U, [t.port2]);
215
+ } catch {
216
+ r(void 0);
217
+ }
218
+ });
219
+ }, q, xe = () => {
220
+ if (q || !W) return q;
221
+ let e = W.querySelector(`iframe[src="${K}"]`);
222
+ if (e) q = e;
223
+ else {
224
+ let e = W.createElement("iframe");
225
+ G && e.setAttribute("allow", "cross-origin-isolated"), e.src = K, W.body.appendChild(e), q = e;
226
+ }
227
+ return he(q), q;
228
+ }, J = (async () => {
229
+ if (!W) return;
230
+ let e = await be();
231
+ if (e) return {
232
+ receive: e,
233
+ emit: e
234
+ };
235
+ let t = xe();
236
+ if (!t) return;
237
+ await m(t);
238
+ let n = t.contentWindow;
239
+ return n ? {
240
+ receive: window,
241
+ emit: n
242
+ } : void 0;
243
+ })(), Se = (e) => {
244
+ let t, n = () => {}, r = new Promise((e) => {
245
+ n = e;
246
+ }), i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Set(), o = 0, s = 0;
247
+ (async () => {
248
+ for await (let r of e) {
249
+ let e = (t?.epoch ?? 0) + 1;
250
+ t = {
251
+ remote: r,
252
+ epoch: e
253
+ }, e === 1 && n(r);
254
+ let o = [...a];
255
+ a.clear();
256
+ let s = /* @__PURE__ */ Error("FKN: the broker was replaced while this call was pending; retry it");
257
+ for (let e of o) try {
258
+ e(s);
259
+ } catch {}
260
+ for (let t of [...i]) try {
261
+ t(r, e);
262
+ } catch {}
263
+ }
264
+ })().catch(() => {});
265
+ let c = async (e, n) => {
266
+ let i = t?.remote ?? await r;
267
+ for (let t of e.slice(0, -1)) i = i[t];
268
+ let c = i[e[e.length - 1] ?? ""];
269
+ o = Date.now(), s += 1;
270
+ let l = () => {}, u = new Promise((e, t) => {
271
+ l = t;
272
+ });
273
+ a.add(l);
274
+ try {
275
+ return await Promise.race([Promise.resolve(c.apply(i, n)), u]);
276
+ } finally {
277
+ --s, a.delete(l), o = Date.now();
278
+ }
279
+ }, l = /* @__PURE__ */ new Map(), u = (e) => {
280
+ let n = e.join("\0"), r = l.get(n);
281
+ if (r) return r;
282
+ let i = new Proxy(function() {}, {
283
+ get: (n, r) => {
284
+ if (typeof r != "string" || r === "then") return;
285
+ let i = t;
286
+ if (i) {
287
+ let t = i.remote;
288
+ for (let n of e) t = t?.[n];
289
+ if (t?.[r] === void 0) return;
290
+ }
291
+ return u([...e, r]);
292
+ },
293
+ apply: (t, n, r) => c(e, r)
294
+ });
295
+ return l.set(n, i), i;
296
+ };
297
+ return {
298
+ facade: u([]),
299
+ first: r.then(() => {}),
300
+ current: () => t,
301
+ onEpoch: (e) => {
302
+ if (i.add(e), t) try {
303
+ e(t.remote, t.epoch);
304
+ } catch {}
305
+ return () => {
306
+ i.delete(e);
307
+ };
308
+ },
309
+ lastCall: () => o,
310
+ inFlight: () => s
311
+ };
312
+ }, Ce = /* @__PURE__ */ f({
313
+ apiPromise: () => Z,
314
+ currentApiEpoch: () => $,
315
+ hasTransport: () => Y,
316
+ onApiEpoch: () => Q
317
+ }), Y = typeof window < "u" || typeof self < "u", we = async () => {
318
+ if (typeof window < "u") return J;
319
+ if (typeof self < "u") return {
320
+ receive: self,
321
+ emit: self
322
+ };
323
+ }, X = Se({ async *[Symbol.asyncIterator]() {
324
+ let t = await we();
325
+ if (!t) {
326
+ await new Promise(() => {});
327
+ return;
328
+ }
329
+ yield* e({}, {
330
+ key: "fkn-api",
331
+ transport: t
332
+ });
333
+ } }), Z = X.first.then(() => X.facade), Q = X.onEpoch, $ = X.current;
334
+ X.lastCall, X.inFlight, Q((e) => {
335
+ z() && e.overlay.setHost(ge, { exactClips: ce() }).catch(() => {});
336
+ });
337
+ //#endregion
338
+ export { Z as a, s as c, Y as i, c as l, J as n, ee as o, Q as r, f as s, $ as t, u };
@@ -0,0 +1,50 @@
1
+ {
2
+ "short_name": "Ripple",
3
+ "name": "Ripple",
4
+ "description": "Download and stream torrents in your browser",
5
+ "icons": [
6
+ {
7
+ "src": "./assets/logo.svg",
8
+ "sizes": "any",
9
+ "type": "image/svg+xml",
10
+ "purpose": "any"
11
+ },
12
+ {
13
+ "src": "./assets/logo.png",
14
+ "sizes": "554x561",
15
+ "type": "image/png",
16
+ "purpose": "any"
17
+ }
18
+ ],
19
+ "id": "/?source=pwa",
20
+ "start_url": "/?source=pwa",
21
+ "background_color": "#0f0f0f",
22
+ "display": "standalone",
23
+ "scope": "/",
24
+ "theme_color": "#0f0f0f",
25
+ "launch_handler": {
26
+ "client_mode": "focus-existing"
27
+ },
28
+ "file_handlers": [
29
+ {
30
+ "action": "/",
31
+ "accept": {
32
+ "application/x-bittorrent": [".torrent"]
33
+ },
34
+ "icons": [
35
+ {
36
+ "src": "./assets/logo.png",
37
+ "sizes": "554x561",
38
+ "type": "image/png"
39
+ }
40
+ ],
41
+ "launch_type": "single-client"
42
+ }
43
+ ],
44
+ "protocol_handlers": [
45
+ {
46
+ "protocol": "magnet",
47
+ "url": "/?magnet=%s"
48
+ }
49
+ ]
50
+ }
File without changes