backpack-viewer 0.7.13 → 0.7.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/assets/index-BbX2AsyK.css +1 -0
- package/dist/app/assets/index-CSbPTbYZ.js +6 -0
- package/dist/app/assets/index-Dz__sU13.js +12 -0
- package/dist/app/assets/layout-worker-4xak23M6.js +1 -0
- package/dist/app/index.html +18 -0
- package/dist/extensions/share/index-B8_hkT8R.js +6277 -0
- package/dist/extensions/share/src/index.js +295 -520
- package/package.json +3 -2
|
@@ -1,548 +1,323 @@
|
|
|
1
|
-
const
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
viewer.registerTaskbarIcon({
|
|
14
|
-
label: "Share",
|
|
15
|
-
iconText: "\u2197",
|
|
16
|
-
position: "bottom-center",
|
|
17
|
-
onClick: () => toggleSharePanel(viewer),
|
|
18
|
-
});
|
|
1
|
+
const L = "https://app.backpackontology.com";
|
|
2
|
+
let f = L, T = `${f}/.well-known/oauth-authorization-server`;
|
|
3
|
+
const P = new Uint8Array([66, 80, 65, 75]), R = 1;
|
|
4
|
+
let m = null;
|
|
5
|
+
async function M(t) {
|
|
6
|
+
const e = await t.settings.get("relay_url");
|
|
7
|
+
e && (f = e, T = `${f}/.well-known/oauth-authorization-server`), t.registerTaskbarIcon({
|
|
8
|
+
label: "Share",
|
|
9
|
+
iconText: "↗",
|
|
10
|
+
position: "bottom-center",
|
|
11
|
+
onClick: () => $(t)
|
|
12
|
+
});
|
|
19
13
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
function $(t) {
|
|
15
|
+
if (m && m.isVisible()) {
|
|
16
|
+
m.setVisible(!1);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const e = t.getGraphName();
|
|
20
|
+
if (!e) return;
|
|
21
|
+
const a = document.createElement("div");
|
|
22
|
+
a.className = "share-panel-body", m ? (m.element.replaceChildren(), m.element.appendChild(a), m.setTitle(`Share "${e}"`), m.setVisible(!0), m.bringToFront()) : (a.textContent = "Loading...", m = t.mountPanel(a, {
|
|
23
|
+
title: `Share "${e}"`,
|
|
24
|
+
defaultPosition: { left: Math.max(100, (window.innerWidth - 380) / 2), top: Math.max(80, (window.innerHeight - 400) / 2) },
|
|
25
|
+
persistKey: "share-v2",
|
|
26
|
+
showFullscreenButton: !1,
|
|
27
|
+
onClose: () => {
|
|
28
|
+
m = null;
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
if (!graphName)
|
|
27
|
-
return;
|
|
28
|
-
const body = document.createElement("div");
|
|
29
|
-
body.className = "share-panel-body";
|
|
30
|
-
if (panel) {
|
|
31
|
-
panel.element.replaceChildren();
|
|
32
|
-
panel.element.appendChild(body);
|
|
33
|
-
panel.setTitle(`Share "${graphName}"`);
|
|
34
|
-
panel.setVisible(true);
|
|
35
|
-
panel.bringToFront();
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
body.textContent = "Loading...";
|
|
39
|
-
panel = viewer.mountPanel(body, {
|
|
40
|
-
title: `Share "${graphName}"`,
|
|
41
|
-
defaultPosition: { left: Math.max(100, (window.innerWidth - 380) / 2), top: Math.max(80, (window.innerHeight - 400) / 2) },
|
|
42
|
-
persistKey: "share-v2",
|
|
43
|
-
showFullscreenButton: false,
|
|
44
|
-
onClose: () => { panel = null; },
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
renderSharePanel(viewer, body);
|
|
30
|
+
})), C(t, a);
|
|
48
31
|
}
|
|
49
|
-
async function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!token) {
|
|
53
|
-
renderUpsell(viewer, container);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
renderSyncView(viewer, container, token);
|
|
57
|
-
}
|
|
32
|
+
async function C(t, e) {
|
|
33
|
+
const a = await t.settings.get("relay_token");
|
|
34
|
+
e.replaceChildren(), a ? D(t, e, a) : I(t, e);
|
|
58
35
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
cta.addEventListener("click", () => startOAuthFlow(viewer, container));
|
|
73
|
-
w.appendChild(cta);
|
|
74
|
-
const tokenLink = document.createElement("button");
|
|
75
|
-
tokenLink.className = "share-token-link";
|
|
76
|
-
tokenLink.textContent = "Or paste an API token";
|
|
77
|
-
tokenLink.addEventListener("click", () => renderTokenInput(viewer, container));
|
|
78
|
-
w.appendChild(tokenLink);
|
|
79
|
-
const trust = document.createElement("p");
|
|
80
|
-
trust.className = "share-trust";
|
|
81
|
-
trust.textContent = "Free account. Your graph is encrypted before upload \u2014 we can\u2019t read it.";
|
|
82
|
-
w.appendChild(trust);
|
|
83
|
-
container.replaceChildren(w);
|
|
36
|
+
function I(t, e) {
|
|
37
|
+
const a = document.createElement("div");
|
|
38
|
+
a.className = "share-upsell";
|
|
39
|
+
const n = document.createElement("h4");
|
|
40
|
+
n.textContent = "Share this graph with anyone", a.appendChild(n);
|
|
41
|
+
const s = document.createElement("p");
|
|
42
|
+
s.textContent = "Encrypt your graph and get a shareable link. Recipients open it in their browser — no install needed.", a.appendChild(s);
|
|
43
|
+
const o = document.createElement("button");
|
|
44
|
+
o.className = "share-cta-btn", o.textContent = "Sign in to share", o.addEventListener("click", () => Y(t, e)), a.appendChild(o);
|
|
45
|
+
const r = document.createElement("button");
|
|
46
|
+
r.className = "share-token-link", r.textContent = "Or paste an API token", r.addEventListener("click", () => B(t, e)), a.appendChild(r);
|
|
47
|
+
const c = document.createElement("p");
|
|
48
|
+
c.className = "share-trust", c.textContent = "Free account. Your graph is encrypted before upload — we can’t read it.", a.appendChild(c), e.replaceChildren(a);
|
|
84
49
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
renderSharePanel(viewer, container);
|
|
50
|
+
function B(t, e) {
|
|
51
|
+
const a = document.createElement("div");
|
|
52
|
+
a.className = "share-token-input";
|
|
53
|
+
const n = document.createElement("p");
|
|
54
|
+
n.textContent = "Paste your API token from your account settings:", a.appendChild(n);
|
|
55
|
+
const s = document.createElement("input");
|
|
56
|
+
s.type = "password", s.placeholder = "Token", s.className = "share-input", a.appendChild(s);
|
|
57
|
+
const o = document.createElement("div");
|
|
58
|
+
o.className = "share-btn-row";
|
|
59
|
+
const r = document.createElement("button");
|
|
60
|
+
r.className = "share-btn-primary", r.textContent = "Save", r.addEventListener("click", async () => {
|
|
61
|
+
const i = s.value.trim();
|
|
62
|
+
i && (await t.settings.set("relay_token", i), C(t, e));
|
|
63
|
+
}), o.appendChild(r);
|
|
64
|
+
const c = document.createElement("button");
|
|
65
|
+
c.className = "share-btn-secondary", c.textContent = "Back", c.addEventListener("click", () => C(t, e)), o.appendChild(c), a.appendChild(o), e.replaceChildren(a);
|
|
66
|
+
}
|
|
67
|
+
async function O(t) {
|
|
68
|
+
try {
|
|
69
|
+
const e = await fetch(`${f}/api/graphs`, {
|
|
70
|
+
headers: { Authorization: `Bearer ${t}` }
|
|
107
71
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
72
|
+
if (e.status === 401) return { graphs: [], error: "unauthorized" };
|
|
73
|
+
if (!e.ok) return { graphs: [], error: `status ${e.status}` };
|
|
74
|
+
const a = await e.json();
|
|
75
|
+
return { graphs: Array.isArray(a) ? a : [] };
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { graphs: [], error: e.message };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function D(t, e, a) {
|
|
81
|
+
const n = t.getGraphName(), s = document.createElement("div");
|
|
82
|
+
s.className = "share-loading", s.textContent = "Checking account…", e.replaceChildren(s);
|
|
83
|
+
const o = await O(a);
|
|
84
|
+
if (o.error === "unauthorized") {
|
|
85
|
+
await t.settings.remove("relay_token"), C(t, e);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
e.replaceChildren();
|
|
89
|
+
const r = o.graphs.find((c) => c.name === n);
|
|
90
|
+
r ? F(t, e, a, r) : V(t, e, a);
|
|
116
91
|
}
|
|
117
|
-
|
|
92
|
+
function F(t, e, a, n) {
|
|
93
|
+
const s = document.createElement("div");
|
|
94
|
+
s.className = "share-synced";
|
|
95
|
+
const o = document.createElement("h4");
|
|
96
|
+
if (o.textContent = "Synced to your account", s.appendChild(o), n.syncedAt) {
|
|
97
|
+
const d = document.createElement("p");
|
|
98
|
+
d.className = "share-note", d.textContent = `Last synced: ${new Date(n.syncedAt).toLocaleString()}`, s.appendChild(d);
|
|
99
|
+
}
|
|
100
|
+
const r = document.createElement("p");
|
|
101
|
+
r.className = "share-note", r.textContent = n.encrypted ? "Encrypted — server cannot read your data." : "Public — stored unencrypted.", s.appendChild(r);
|
|
102
|
+
const c = document.createElement("button");
|
|
103
|
+
c.className = "share-cta-btn", c.textContent = "Update & Share", c.addEventListener("click", async () => {
|
|
104
|
+
c.disabled = !0, c.textContent = n.encrypted ? "Encrypting…" : "Syncing…";
|
|
118
105
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (res.status === 401)
|
|
123
|
-
return { graphs: [], error: "unauthorized" };
|
|
124
|
-
if (!res.ok)
|
|
125
|
-
return { graphs: [], error: `status ${res.status}` };
|
|
126
|
-
const data = await res.json();
|
|
127
|
-
return { graphs: Array.isArray(data) ? data : [] };
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
return { graphs: [], error: err.message };
|
|
106
|
+
await x(t, e, a, n.encrypted);
|
|
107
|
+
} catch (d) {
|
|
108
|
+
c.disabled = !1, c.textContent = "Update & Share", S(s), b(s, d.message);
|
|
131
109
|
}
|
|
110
|
+
}), s.appendChild(c);
|
|
111
|
+
const i = document.createElement("a");
|
|
112
|
+
i.href = f, i.target = "_blank", i.rel = "noopener", i.className = "share-token-link", i.textContent = "Open dashboard", s.appendChild(i), _(t, e, s);
|
|
132
113
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
114
|
+
function V(t, e, a) {
|
|
115
|
+
const n = document.createElement("div");
|
|
116
|
+
n.className = "share-form";
|
|
117
|
+
const s = document.createElement("p");
|
|
118
|
+
s.className = "share-description", s.textContent = "Your graph will be encrypted and synced to your Backpack account. You’ll get a shareable link.", n.appendChild(s);
|
|
119
|
+
const o = document.createElement("button");
|
|
120
|
+
o.className = "share-cta-btn", o.textContent = "Sync & Share", o.addEventListener("click", async () => {
|
|
121
|
+
o.disabled = !0, o.textContent = "Encrypting…";
|
|
122
|
+
try {
|
|
123
|
+
await x(t, e, a, !0);
|
|
124
|
+
} catch (c) {
|
|
125
|
+
o.disabled = !1, o.textContent = "Sync & Share";
|
|
126
|
+
const i = c.message;
|
|
127
|
+
if (i.includes("encrypted") && i.includes("limit")) {
|
|
128
|
+
z(t, e, a);
|
|
143
129
|
return;
|
|
130
|
+
}
|
|
131
|
+
S(n), b(n, i);
|
|
144
132
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
renderAlreadySynced(viewer, container, token, existing);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
renderSyncForm(viewer, container, token);
|
|
152
|
-
}
|
|
133
|
+
}), n.appendChild(o);
|
|
134
|
+
const r = document.createElement("p");
|
|
135
|
+
r.className = "share-trust", r.textContent = "Your first encrypted graph is free. Data is encrypted before upload — we can’t read it.", n.appendChild(r), _(t, e, n);
|
|
153
136
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
137
|
+
function z(t, e, a) {
|
|
138
|
+
const n = document.createElement("div");
|
|
139
|
+
n.className = "share-quota";
|
|
140
|
+
const s = document.createElement("h4");
|
|
141
|
+
s.textContent = "Encrypted limit reached", n.appendChild(s);
|
|
142
|
+
const o = document.createElement("p");
|
|
143
|
+
o.className = "share-description", o.textContent = "Your free account includes one encrypted graph, which is already in use.", n.appendChild(o);
|
|
144
|
+
const r = document.createElement("a");
|
|
145
|
+
r.href = `${f}/settings`, r.target = "_blank", r.rel = "noopener", r.className = "share-cta-btn share-btn-link", r.textContent = "Upgrade for unlimited encryption", n.appendChild(r);
|
|
146
|
+
const c = document.createElement("div");
|
|
147
|
+
c.className = "share-divider", n.appendChild(c);
|
|
148
|
+
const i = document.createElement("p");
|
|
149
|
+
i.className = "share-description", i.textContent = "Or share as a public graph:", n.appendChild(i);
|
|
150
|
+
const d = document.createElement("p");
|
|
151
|
+
d.className = "share-warning", d.textContent = "Your graph data will be stored unencrypted and visible to the server and anyone with the link.", n.appendChild(d);
|
|
152
|
+
const l = document.createElement("label");
|
|
153
|
+
l.className = "share-toggle-row";
|
|
154
|
+
const u = document.createElement("input");
|
|
155
|
+
u.type = "checkbox", l.appendChild(u);
|
|
156
|
+
const p = document.createElement("span");
|
|
157
|
+
p.textContent = "I understand this graph will not be encrypted", l.appendChild(p), n.appendChild(l);
|
|
158
|
+
const h = document.createElement("button");
|
|
159
|
+
h.className = "share-btn-secondary", h.textContent = "Share as public graph", h.disabled = !0, u.addEventListener("change", () => {
|
|
160
|
+
h.disabled = !u.checked;
|
|
161
|
+
}), h.addEventListener("click", async () => {
|
|
162
|
+
h.disabled = !0, h.textContent = "Syncing…";
|
|
163
|
+
try {
|
|
164
|
+
await x(t, e, a, !1, "public");
|
|
165
|
+
} catch (w) {
|
|
166
|
+
h.disabled = !1, h.textContent = "Share as public graph", S(n), b(n, w.message);
|
|
165
167
|
}
|
|
166
|
-
|
|
167
|
-
badge.className = "share-note";
|
|
168
|
-
badge.textContent = graph.encrypted ? "Encrypted \u2014 server cannot read your data." : "Public \u2014 stored unencrypted.";
|
|
169
|
-
w.appendChild(badge);
|
|
170
|
-
const updateBtn = document.createElement("button");
|
|
171
|
-
updateBtn.className = "share-cta-btn";
|
|
172
|
-
updateBtn.textContent = "Update & Share";
|
|
173
|
-
updateBtn.addEventListener("click", async () => {
|
|
174
|
-
updateBtn.disabled = true;
|
|
175
|
-
updateBtn.textContent = graph.encrypted ? "Encrypting\u2026" : "Syncing\u2026";
|
|
176
|
-
try {
|
|
177
|
-
await doSyncAndShare(viewer, container, token, graph.encrypted);
|
|
178
|
-
}
|
|
179
|
-
catch (err) {
|
|
180
|
-
updateBtn.disabled = false;
|
|
181
|
-
updateBtn.textContent = "Update & Share";
|
|
182
|
-
clearErrors(w);
|
|
183
|
-
appendError(w, err.message);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
w.appendChild(updateBtn);
|
|
187
|
-
const dashLink = document.createElement("a");
|
|
188
|
-
dashLink.href = RELAY_URL;
|
|
189
|
-
dashLink.target = "_blank";
|
|
190
|
-
dashLink.rel = "noopener";
|
|
191
|
-
dashLink.className = "share-token-link";
|
|
192
|
-
dashLink.textContent = "Open dashboard";
|
|
193
|
-
w.appendChild(dashLink);
|
|
194
|
-
appendFooter(viewer, container, w);
|
|
168
|
+
}), n.appendChild(h), _(t, e, n);
|
|
195
169
|
}
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const syncBtn = document.createElement("button");
|
|
204
|
-
syncBtn.className = "share-cta-btn";
|
|
205
|
-
syncBtn.textContent = "Sync & Share";
|
|
206
|
-
syncBtn.addEventListener("click", async () => {
|
|
207
|
-
syncBtn.disabled = true;
|
|
208
|
-
syncBtn.textContent = "Encrypting\u2026";
|
|
209
|
-
try {
|
|
210
|
-
await doSyncAndShare(viewer, container, token, true);
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
syncBtn.disabled = false;
|
|
214
|
-
syncBtn.textContent = "Sync & Share";
|
|
215
|
-
const msg = err.message;
|
|
216
|
-
if (msg.includes("encrypted") && msg.includes("limit")) {
|
|
217
|
-
renderQuotaExceeded(viewer, container, token);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
clearErrors(w);
|
|
221
|
-
appendError(w, msg);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
w.appendChild(syncBtn);
|
|
225
|
-
const freeNote = document.createElement("p");
|
|
226
|
-
freeNote.className = "share-trust";
|
|
227
|
-
freeNote.textContent = "Your first encrypted graph is free. Data is encrypted before upload\u200a\u2014\u200awe can\u2019t read it.";
|
|
228
|
-
w.appendChild(freeNote);
|
|
229
|
-
appendFooter(viewer, container, w);
|
|
170
|
+
function _(t, e, a) {
|
|
171
|
+
const n = document.createElement("div");
|
|
172
|
+
n.className = "share-footer";
|
|
173
|
+
const s = document.createElement("button");
|
|
174
|
+
s.className = "share-token-link", s.textContent = "Sign out", s.addEventListener("click", async () => {
|
|
175
|
+
await t.settings.remove("relay_token"), C(t, e);
|
|
176
|
+
}), n.appendChild(s), a.appendChild(n), e.replaceChildren(a);
|
|
230
177
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
w.className = "share-quota";
|
|
234
|
-
const h = document.createElement("h4");
|
|
235
|
-
h.textContent = "Encrypted limit reached";
|
|
236
|
-
w.appendChild(h);
|
|
237
|
-
const desc = document.createElement("p");
|
|
238
|
-
desc.className = "share-description";
|
|
239
|
-
desc.textContent = "Your free account includes one encrypted graph, which is already in use.";
|
|
240
|
-
w.appendChild(desc);
|
|
241
|
-
const upgradeBtn = document.createElement("a");
|
|
242
|
-
upgradeBtn.href = `${RELAY_URL}/settings`;
|
|
243
|
-
upgradeBtn.target = "_blank";
|
|
244
|
-
upgradeBtn.rel = "noopener";
|
|
245
|
-
upgradeBtn.className = "share-cta-btn share-btn-link";
|
|
246
|
-
upgradeBtn.textContent = "Upgrade for unlimited encryption";
|
|
247
|
-
w.appendChild(upgradeBtn);
|
|
248
|
-
const divider = document.createElement("div");
|
|
249
|
-
divider.className = "share-divider";
|
|
250
|
-
w.appendChild(divider);
|
|
251
|
-
const publicLabel = document.createElement("p");
|
|
252
|
-
publicLabel.className = "share-description";
|
|
253
|
-
publicLabel.textContent = "Or share as a public graph:";
|
|
254
|
-
w.appendChild(publicLabel);
|
|
255
|
-
const warning = document.createElement("p");
|
|
256
|
-
warning.className = "share-warning";
|
|
257
|
-
warning.textContent = "Your graph data will be stored unencrypted and visible to the server and anyone with the link.";
|
|
258
|
-
w.appendChild(warning);
|
|
259
|
-
const confirmRow = document.createElement("label");
|
|
260
|
-
confirmRow.className = "share-toggle-row";
|
|
261
|
-
const confirmCb = document.createElement("input");
|
|
262
|
-
confirmCb.type = "checkbox";
|
|
263
|
-
confirmRow.appendChild(confirmCb);
|
|
264
|
-
const confirmLabel = document.createElement("span");
|
|
265
|
-
confirmLabel.textContent = "I understand this graph will not be encrypted";
|
|
266
|
-
confirmRow.appendChild(confirmLabel);
|
|
267
|
-
w.appendChild(confirmRow);
|
|
268
|
-
const publicBtn = document.createElement("button");
|
|
269
|
-
publicBtn.className = "share-btn-secondary";
|
|
270
|
-
publicBtn.textContent = "Share as public graph";
|
|
271
|
-
publicBtn.disabled = true;
|
|
272
|
-
confirmCb.addEventListener("change", () => {
|
|
273
|
-
publicBtn.disabled = !confirmCb.checked;
|
|
274
|
-
});
|
|
275
|
-
publicBtn.addEventListener("click", async () => {
|
|
276
|
-
publicBtn.disabled = true;
|
|
277
|
-
publicBtn.textContent = "Syncing\u2026";
|
|
278
|
-
try {
|
|
279
|
-
await doSyncAndShare(viewer, container, token, false, "public");
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
publicBtn.disabled = false;
|
|
283
|
-
publicBtn.textContent = "Share as public graph";
|
|
284
|
-
clearErrors(w);
|
|
285
|
-
appendError(w, err.message);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
w.appendChild(publicBtn);
|
|
289
|
-
appendFooter(viewer, container, w);
|
|
290
|
-
}
|
|
291
|
-
function appendFooter(viewer, container, w) {
|
|
292
|
-
const footer = document.createElement("div");
|
|
293
|
-
footer.className = "share-footer";
|
|
294
|
-
const logoutBtn = document.createElement("button");
|
|
295
|
-
logoutBtn.className = "share-token-link";
|
|
296
|
-
logoutBtn.textContent = "Sign out";
|
|
297
|
-
logoutBtn.addEventListener("click", async () => {
|
|
298
|
-
await viewer.settings.remove("relay_token");
|
|
299
|
-
renderSharePanel(viewer, container);
|
|
300
|
-
});
|
|
301
|
-
footer.appendChild(logoutBtn);
|
|
302
|
-
w.appendChild(footer);
|
|
303
|
-
container.replaceChildren(w);
|
|
178
|
+
function S(t) {
|
|
179
|
+
for (const e of t.querySelectorAll(".share-error")) e.remove();
|
|
304
180
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
181
|
+
function b(t, e) {
|
|
182
|
+
const a = document.createElement("p");
|
|
183
|
+
a.className = "share-error", a.textContent = e, t.appendChild(a);
|
|
308
184
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const regRes = await fetch(meta.registration_endpoint, { method: "POST" });
|
|
321
|
-
const client = (await regRes.json());
|
|
322
|
-
const codeVerifier = generateCodeVerifier();
|
|
323
|
-
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
324
|
-
const redirectUri = window.location.origin + "/oauth/callback";
|
|
325
|
-
const state = crypto.randomUUID();
|
|
326
|
-
const authUrl = new URL(meta.authorization_endpoint);
|
|
327
|
-
authUrl.searchParams.set("client_id", client.client_id);
|
|
328
|
-
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
329
|
-
authUrl.searchParams.set("response_type", "code");
|
|
330
|
-
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
331
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
332
|
-
authUrl.searchParams.set("state", state);
|
|
333
|
-
if (!authUrl.searchParams.has("scope")) {
|
|
334
|
-
authUrl.searchParams.set("scope", "openid email profile offline_access");
|
|
335
|
-
}
|
|
336
|
-
sessionStorage.setItem("share_oauth_state", state);
|
|
337
|
-
sessionStorage.setItem("share_oauth_token_endpoint", meta.token_endpoint);
|
|
338
|
-
sessionStorage.setItem("share_oauth_client_id", client.client_id);
|
|
339
|
-
sessionStorage.setItem("share_oauth_code_verifier", codeVerifier);
|
|
340
|
-
sessionStorage.setItem("share_oauth_redirect_uri", redirectUri);
|
|
341
|
-
const popup = window.open(authUrl.toString(), "backpack-share-auth", "width=500,height=700");
|
|
342
|
-
const handler = async (event) => {
|
|
343
|
-
if (event.data?.type !== "backpack-oauth-callback")
|
|
344
|
-
return;
|
|
345
|
-
window.removeEventListener("message", handler);
|
|
346
|
-
const { code, returnedState } = event.data;
|
|
347
|
-
if (returnedState !== state)
|
|
348
|
-
return;
|
|
349
|
-
clearOAuthSessionStorage();
|
|
350
|
-
try {
|
|
351
|
-
const tokenRes = await fetch(meta.token_endpoint, {
|
|
352
|
-
method: "POST",
|
|
353
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
354
|
-
body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, client_id: client.client_id, code_verifier: codeVerifier }).toString(),
|
|
355
|
-
});
|
|
356
|
-
const tokenData = (await tokenRes.json());
|
|
357
|
-
if (!tokenData.access_token) {
|
|
358
|
-
appendError(container, "Token exchange failed: no access token returned.");
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
await viewer.settings.set("relay_token", tokenData.access_token);
|
|
362
|
-
renderSharePanel(viewer, container);
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
appendError(container, `Token exchange failed: ${err.message}`);
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
window.addEventListener("message", handler);
|
|
369
|
-
if (!popup || popup.closed) {
|
|
370
|
-
window.location.href = authUrl.toString();
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch (err) {
|
|
374
|
-
appendError(container, `Auth failed: ${err.message}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
// --- Sync & Share ---
|
|
378
|
-
async function doSyncAndShare(viewer, container, token, encrypted, visibility = "private") {
|
|
379
|
-
const graph = viewer.getGraph();
|
|
380
|
-
const graphName = viewer.getGraphName();
|
|
381
|
-
if (!graph || !graphName)
|
|
382
|
-
throw new Error("No graph loaded");
|
|
383
|
-
// Step 1: Build BPAK envelope
|
|
384
|
-
const graphJSON = new TextEncoder().encode(JSON.stringify(graph));
|
|
385
|
-
let payload;
|
|
386
|
-
let format;
|
|
387
|
-
let fragmentKey = "";
|
|
388
|
-
if (encrypted) {
|
|
389
|
-
const age = await import("age-encryption");
|
|
390
|
-
const secretKey = await age.generateX25519Identity();
|
|
391
|
-
const publicKey = await age.identityToRecipient(secretKey);
|
|
392
|
-
const e = new age.Encrypter();
|
|
393
|
-
e.addRecipient(publicKey);
|
|
394
|
-
payload = await e.encrypt(graphJSON);
|
|
395
|
-
format = "age-v1";
|
|
396
|
-
fragmentKey = btoa(secretKey).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
payload = graphJSON;
|
|
400
|
-
format = "plaintext";
|
|
401
|
-
}
|
|
402
|
-
const envelope = await buildEnvelope(graphName, payload, format, graph);
|
|
403
|
-
// Step 2: Sync to cloud
|
|
404
|
-
const syncUrl = `${RELAY_URL}/api/graphs/${encodeURIComponent(graphName)}/sync${visibility === "public" ? "?visibility=public" : ""}`;
|
|
405
|
-
const syncRes = await relayFetch(token, syncUrl, {
|
|
406
|
-
method: "PUT",
|
|
407
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
408
|
-
body: envelope,
|
|
409
|
-
});
|
|
410
|
-
if (!syncRes.ok) {
|
|
411
|
-
if (syncRes.status === 401) {
|
|
412
|
-
await viewer.settings.remove("relay_token");
|
|
413
|
-
renderSharePanel(viewer, container);
|
|
414
|
-
throw new Error("Session expired. Please sign in again.");
|
|
415
|
-
}
|
|
416
|
-
let errorMsg = `Sync failed (${syncRes.status})`;
|
|
185
|
+
async function Y(t, e) {
|
|
186
|
+
try {
|
|
187
|
+
const n = await (await fetch(T)).json(), o = await (await fetch(n.registration_endpoint, { method: "POST" })).json(), r = K(), c = await H(r), i = window.location.origin + "/oauth/callback", d = crypto.randomUUID(), l = new URL(n.authorization_endpoint);
|
|
188
|
+
l.searchParams.set("client_id", o.client_id), l.searchParams.set("redirect_uri", i), l.searchParams.set("response_type", "code"), l.searchParams.set("code_challenge", c), l.searchParams.set("code_challenge_method", "S256"), l.searchParams.set("state", d), l.searchParams.has("scope") || l.searchParams.set("scope", "openid email profile offline_access"), sessionStorage.setItem("share_oauth_state", d), sessionStorage.setItem("share_oauth_token_endpoint", n.token_endpoint), sessionStorage.setItem("share_oauth_client_id", o.client_id), sessionStorage.setItem("share_oauth_code_verifier", r), sessionStorage.setItem("share_oauth_redirect_uri", i);
|
|
189
|
+
const u = window.open(l.toString(), "backpack-share-auth", "width=500,height=700"), p = async (h) => {
|
|
190
|
+
var k;
|
|
191
|
+
if (((k = h.data) == null ? void 0 : k.type) !== "backpack-oauth-callback") return;
|
|
192
|
+
window.removeEventListener("message", p);
|
|
193
|
+
const { code: w, returnedState: E } = h.data;
|
|
194
|
+
if (E === d) {
|
|
195
|
+
G();
|
|
417
196
|
try {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
197
|
+
const g = await (await fetch(n.token_endpoint, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
200
|
+
body: new URLSearchParams({ grant_type: "authorization_code", code: w, redirect_uri: i, client_id: o.client_id, code_verifier: r }).toString()
|
|
201
|
+
})).json();
|
|
202
|
+
if (!g.access_token) {
|
|
203
|
+
b(e, "Token exchange failed: no access token returned.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await t.settings.set("relay_token", g.access_token), C(t, e);
|
|
207
|
+
} catch (y) {
|
|
208
|
+
b(e, `Token exchange failed: ${y.message}`);
|
|
421
209
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
window.addEventListener("message", p), (!u || u.closed) && (window.location.href = l.toString());
|
|
213
|
+
} catch (a) {
|
|
214
|
+
b(e, `Auth failed: ${a.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function x(t, e, a, n, s = "private") {
|
|
218
|
+
const o = t.getGraph(), r = t.getGraphName();
|
|
219
|
+
if (!o || !r) throw new Error("No graph loaded");
|
|
220
|
+
const c = new TextEncoder().encode(JSON.stringify(o));
|
|
221
|
+
let i, d, l = "";
|
|
222
|
+
if (n) {
|
|
223
|
+
const y = await import("../index-B8_hkT8R.js"), g = await y.generateX25519Identity(), U = await y.identityToRecipient(g), N = new y.Encrypter();
|
|
224
|
+
N.addRecipient(U), i = await N.encrypt(c), d = "age-v1", l = btoa(g).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
225
|
+
} else
|
|
226
|
+
i = c, d = "plaintext";
|
|
227
|
+
const u = await j(r, i, d, o), p = `${f}/api/graphs/${encodeURIComponent(r)}/sync${s === "public" ? "?visibility=public" : ""}`, h = await v(a, p, {
|
|
228
|
+
method: "PUT",
|
|
229
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
230
|
+
body: u
|
|
231
|
+
});
|
|
232
|
+
if (!h.ok) {
|
|
233
|
+
if (h.status === 401)
|
|
234
|
+
throw await t.settings.remove("relay_token"), C(t, e), new Error("Session expired. Please sign in again.");
|
|
235
|
+
let y = `Sync failed (${h.status})`;
|
|
236
|
+
try {
|
|
237
|
+
const g = await h.json();
|
|
238
|
+
g.error && (y = g.error);
|
|
239
|
+
} catch {
|
|
433
240
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
241
|
+
throw new Error(y);
|
|
242
|
+
}
|
|
243
|
+
const w = await v(a, `${f}/api/graphs/${encodeURIComponent(r)}/share`, {
|
|
244
|
+
method: "POST"
|
|
245
|
+
});
|
|
246
|
+
if (!w.ok) {
|
|
247
|
+
A(e, null, n, void 0);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const E = await w.json(), k = l ? `${E.url}#k=${l}` : E.url;
|
|
251
|
+
A(e, k, n, E.expires_at);
|
|
437
252
|
}
|
|
438
|
-
async function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return fetch(url, { ...init, headers });
|
|
253
|
+
async function v(t, e, a = {}) {
|
|
254
|
+
const n = new Headers(a.headers);
|
|
255
|
+
return n.set("Authorization", `Bearer ${t}`), fetch(e, { ...a, headers: n });
|
|
442
256
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
node_types: Array.from(typeSet),
|
|
461
|
-
});
|
|
462
|
-
const headerBytes = new TextEncoder().encode(header);
|
|
463
|
-
const headerLenBuf = new ArrayBuffer(4);
|
|
464
|
-
new DataView(headerLenBuf).setUint32(0, headerBytes.length, false);
|
|
465
|
-
const result = new Uint8Array(4 + 1 + 4 + headerBytes.length + payload.length);
|
|
466
|
-
let off = 0;
|
|
467
|
-
result.set(BPAK_MAGIC, off);
|
|
468
|
-
off += 4;
|
|
469
|
-
result[off] = BPAK_VERSION;
|
|
470
|
-
off += 1;
|
|
471
|
-
result.set(new Uint8Array(headerLenBuf), off);
|
|
472
|
-
off += 4;
|
|
473
|
-
result.set(headerBytes, off);
|
|
474
|
-
off += headerBytes.length;
|
|
475
|
-
result.set(payload, off);
|
|
476
|
-
return result;
|
|
257
|
+
async function j(t, e, a, n) {
|
|
258
|
+
const s = new Uint8Array(e).buffer, o = await crypto.subtle.digest("SHA-256", s), r = "sha256:" + Array.from(new Uint8Array(o)).map((h) => h.toString(16).padStart(2, "0")).join(""), c = /* @__PURE__ */ new Set();
|
|
259
|
+
for (const h of n.nodes) c.add(h.type);
|
|
260
|
+
const i = JSON.stringify({
|
|
261
|
+
format: a,
|
|
262
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
263
|
+
backpack_name: t,
|
|
264
|
+
graph_count: 1,
|
|
265
|
+
checksum: r,
|
|
266
|
+
node_count: n.nodes.length,
|
|
267
|
+
edge_count: n.edges.length,
|
|
268
|
+
node_types: Array.from(c)
|
|
269
|
+
}), d = new TextEncoder().encode(i), l = new ArrayBuffer(4);
|
|
270
|
+
new DataView(l).setUint32(0, d.length, !1);
|
|
271
|
+
const u = new Uint8Array(9 + d.length + e.length);
|
|
272
|
+
let p = 0;
|
|
273
|
+
return u.set(P, p), p += 4, u[p] = R, p += 1, u.set(new Uint8Array(l), p), p += 4, u.set(d, p), p += d.length, u.set(e, p), u;
|
|
477
274
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
note.textContent = "The decryption key is embedded in the link. Share the complete link \u2014 if the #k= part is removed, recipients won\u2019t be able to decrypt. The server cannot read your data.";
|
|
513
|
-
w.appendChild(note);
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
const note = document.createElement("p");
|
|
517
|
-
note.className = "share-note";
|
|
518
|
-
note.textContent = "This graph is stored unencrypted. Anyone with the link can view it.";
|
|
519
|
-
w.appendChild(note);
|
|
520
|
-
}
|
|
521
|
-
if (expiresAt) {
|
|
522
|
-
const exp = document.createElement("p");
|
|
523
|
-
exp.className = "share-note";
|
|
524
|
-
exp.textContent = `Expires: ${new Date(expiresAt).toLocaleDateString()}`;
|
|
525
|
-
w.appendChild(exp);
|
|
526
|
-
}
|
|
527
|
-
container.replaceChildren(w);
|
|
275
|
+
function A(t, e, a, n) {
|
|
276
|
+
const s = document.createElement("div");
|
|
277
|
+
s.className = "share-success";
|
|
278
|
+
const o = document.createElement("h4");
|
|
279
|
+
if (o.textContent = e ? "Synced & shared!" : "Synced!", s.appendChild(o), e) {
|
|
280
|
+
const r = document.createElement("div");
|
|
281
|
+
r.className = "share-link-row";
|
|
282
|
+
const c = document.createElement("input");
|
|
283
|
+
c.type = "text", c.readOnly = !0, c.value = e, c.className = "share-link-input", r.appendChild(c);
|
|
284
|
+
const i = document.createElement("button");
|
|
285
|
+
i.className = "share-btn-primary", i.textContent = "Copy", i.addEventListener("click", () => {
|
|
286
|
+
navigator.clipboard.writeText(e).then(() => {
|
|
287
|
+
i.textContent = "Copied!", setTimeout(() => {
|
|
288
|
+
i.textContent = "Copy";
|
|
289
|
+
}, 2e3);
|
|
290
|
+
}).catch(() => {
|
|
291
|
+
i.textContent = "Failed", setTimeout(() => {
|
|
292
|
+
i.textContent = "Copy";
|
|
293
|
+
}, 2e3);
|
|
294
|
+
});
|
|
295
|
+
}), r.appendChild(i), s.appendChild(r);
|
|
296
|
+
}
|
|
297
|
+
if (a) {
|
|
298
|
+
const r = document.createElement("p");
|
|
299
|
+
r.className = "share-note", r.textContent = "The decryption key is embedded in the link. Share the complete link — if the #k= part is removed, recipients won’t be able to decrypt. The server cannot read your data.", s.appendChild(r);
|
|
300
|
+
} else {
|
|
301
|
+
const r = document.createElement("p");
|
|
302
|
+
r.className = "share-note", r.textContent = "This graph is stored unencrypted. Anyone with the link can view it.", s.appendChild(r);
|
|
303
|
+
}
|
|
304
|
+
if (n) {
|
|
305
|
+
const r = document.createElement("p");
|
|
306
|
+
r.className = "share-note", r.textContent = `Expires: ${new Date(n).toLocaleDateString()}`, s.appendChild(r);
|
|
307
|
+
}
|
|
308
|
+
t.replaceChildren(s);
|
|
528
309
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
sessionStorage.removeItem("share_oauth_state");
|
|
532
|
-
sessionStorage.removeItem("share_oauth_token_endpoint");
|
|
533
|
-
sessionStorage.removeItem("share_oauth_client_id");
|
|
534
|
-
sessionStorage.removeItem("share_oauth_code_verifier");
|
|
535
|
-
sessionStorage.removeItem("share_oauth_redirect_uri");
|
|
310
|
+
function G() {
|
|
311
|
+
sessionStorage.removeItem("share_oauth_state"), sessionStorage.removeItem("share_oauth_token_endpoint"), sessionStorage.removeItem("share_oauth_client_id"), sessionStorage.removeItem("share_oauth_code_verifier"), sessionStorage.removeItem("share_oauth_redirect_uri");
|
|
536
312
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
crypto.getRandomValues(arr);
|
|
541
|
-
return btoa(String.fromCharCode(...arr)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
313
|
+
function K() {
|
|
314
|
+
const t = new Uint8Array(32);
|
|
315
|
+
return crypto.getRandomValues(t), btoa(String.fromCharCode(...t)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
542
316
|
}
|
|
543
|
-
async function
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const digest = await crypto.subtle.digest("SHA-256", dataCopy);
|
|
547
|
-
return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
317
|
+
async function H(t) {
|
|
318
|
+
const e = new TextEncoder().encode(t), a = new Uint8Array(e).buffer, n = await crypto.subtle.digest("SHA-256", a);
|
|
319
|
+
return btoa(String.fromCharCode(...new Uint8Array(n))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
548
320
|
}
|
|
321
|
+
export {
|
|
322
|
+
M as activate
|
|
323
|
+
};
|