backpack-viewer 0.7.11 → 0.7.13
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/extensions/share/src/index.js +520 -295
- package/package.json +5 -3
- package/dist/app/assets/index-BbX2AsyK.css +0 -1
- package/dist/app/assets/index-Dz__sU13.js +0 -12
- package/dist/app/assets/index-F9MHq10h.js +0 -6
- package/dist/app/assets/layout-worker-4xak23M6.js +0 -1
- package/dist/app/index.html +0 -18
- package/dist/extensions/share/index-B8_hkT8R.js +0 -6277
|
@@ -1,323 +1,548 @@
|
|
|
1
|
-
const
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
}
|
|
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;
|
|
1
|
+
const DEFAULT_RELAY_URL = "https://app.backpackontology.com";
|
|
2
|
+
let RELAY_URL = DEFAULT_RELAY_URL;
|
|
3
|
+
let OAUTH_METADATA_URL = `${RELAY_URL}/.well-known/oauth-authorization-server`;
|
|
4
|
+
const BPAK_MAGIC = new Uint8Array([0x42, 0x50, 0x41, 0x4b]);
|
|
5
|
+
const BPAK_VERSION = 0x01;
|
|
6
|
+
let panel = null;
|
|
7
|
+
export async function activate(viewer) {
|
|
8
|
+
const customRelay = await viewer.settings.get("relay_url");
|
|
9
|
+
if (customRelay) {
|
|
10
|
+
RELAY_URL = customRelay;
|
|
11
|
+
OAUTH_METADATA_URL = `${RELAY_URL}/.well-known/oauth-authorization-server`;
|
|
29
12
|
}
|
|
30
|
-
|
|
13
|
+
viewer.registerTaskbarIcon({
|
|
14
|
+
label: "Share",
|
|
15
|
+
iconText: "\u2197",
|
|
16
|
+
position: "bottom-center",
|
|
17
|
+
onClick: () => toggleSharePanel(viewer),
|
|
18
|
+
});
|
|
31
19
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
function toggleSharePanel(viewer) {
|
|
21
|
+
if (panel && panel.isVisible()) {
|
|
22
|
+
panel.setVisible(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const graphName = viewer.getGraphName();
|
|
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);
|
|
35
48
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
49
|
+
async function renderSharePanel(viewer, container) {
|
|
50
|
+
const token = await viewer.settings.get("relay_token");
|
|
51
|
+
container.replaceChildren();
|
|
52
|
+
if (!token) {
|
|
53
|
+
renderUpsell(viewer, container);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
renderSyncView(viewer, container, token);
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
// --- Pre-auth: sign-in upsell ---
|
|
60
|
+
function renderUpsell(viewer, container) {
|
|
61
|
+
const w = document.createElement("div");
|
|
62
|
+
w.className = "share-upsell";
|
|
63
|
+
const h = document.createElement("h4");
|
|
64
|
+
h.textContent = "Share this graph with anyone";
|
|
65
|
+
w.appendChild(h);
|
|
66
|
+
const p = document.createElement("p");
|
|
67
|
+
p.textContent = "Encrypt your graph and get a shareable link. Recipients open it in their browser \u2014 no install needed.";
|
|
68
|
+
w.appendChild(p);
|
|
69
|
+
const cta = document.createElement("button");
|
|
70
|
+
cta.className = "share-cta-btn";
|
|
71
|
+
cta.textContent = "Sign in to share";
|
|
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);
|
|
66
84
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
function renderTokenInput(viewer, container) {
|
|
86
|
+
const w = document.createElement("div");
|
|
87
|
+
w.className = "share-token-input";
|
|
88
|
+
const label = document.createElement("p");
|
|
89
|
+
label.textContent = "Paste your API token from your account settings:";
|
|
90
|
+
w.appendChild(label);
|
|
91
|
+
const input = document.createElement("input");
|
|
92
|
+
input.type = "password";
|
|
93
|
+
input.placeholder = "Token";
|
|
94
|
+
input.className = "share-input";
|
|
95
|
+
w.appendChild(input);
|
|
96
|
+
const row = document.createElement("div");
|
|
97
|
+
row.className = "share-btn-row";
|
|
98
|
+
const saveBtn = document.createElement("button");
|
|
99
|
+
saveBtn.className = "share-btn-primary";
|
|
100
|
+
saveBtn.textContent = "Save";
|
|
101
|
+
saveBtn.addEventListener("click", async () => {
|
|
102
|
+
const val = input.value.trim();
|
|
103
|
+
if (!val)
|
|
104
|
+
return;
|
|
105
|
+
await viewer.settings.set("relay_token", val);
|
|
106
|
+
renderSharePanel(viewer, container);
|
|
71
107
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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);
|
|
108
|
+
row.appendChild(saveBtn);
|
|
109
|
+
const backBtn = document.createElement("button");
|
|
110
|
+
backBtn.className = "share-btn-secondary";
|
|
111
|
+
backBtn.textContent = "Back";
|
|
112
|
+
backBtn.addEventListener("click", () => renderSharePanel(viewer, container));
|
|
113
|
+
row.appendChild(backBtn);
|
|
114
|
+
w.appendChild(row);
|
|
115
|
+
container.replaceChildren(w);
|
|
91
116
|
}
|
|
92
|
-
function
|
|
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…";
|
|
117
|
+
async function fetchGraphs(token) {
|
|
105
118
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
const res = await fetch(`${RELAY_URL}/api/graphs`, {
|
|
120
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
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 };
|
|
109
131
|
}
|
|
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);
|
|
113
132
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
133
|
+
async function renderSyncView(viewer, container, token) {
|
|
134
|
+
const graphName = viewer.getGraphName();
|
|
135
|
+
const loading = document.createElement("div");
|
|
136
|
+
loading.className = "share-loading";
|
|
137
|
+
loading.textContent = "Checking account\u2026";
|
|
138
|
+
container.replaceChildren(loading);
|
|
139
|
+
const result = await fetchGraphs(token);
|
|
140
|
+
if (result.error === "unauthorized") {
|
|
141
|
+
await viewer.settings.remove("relay_token");
|
|
142
|
+
renderSharePanel(viewer, container);
|
|
129
143
|
return;
|
|
130
|
-
}
|
|
131
|
-
S(n), b(n, i);
|
|
132
144
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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);
|
|
145
|
+
container.replaceChildren();
|
|
146
|
+
const existing = result.graphs.find(g => g.name === graphName);
|
|
147
|
+
if (existing) {
|
|
148
|
+
renderAlreadySynced(viewer, container, token, existing);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
renderSyncForm(viewer, container, token);
|
|
167
152
|
}
|
|
168
|
-
}), n.appendChild(h), _(t, e, n);
|
|
169
|
-
}
|
|
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);
|
|
177
153
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
154
|
+
function renderAlreadySynced(viewer, container, token, graph) {
|
|
155
|
+
const w = document.createElement("div");
|
|
156
|
+
w.className = "share-synced";
|
|
157
|
+
const h = document.createElement("h4");
|
|
158
|
+
h.textContent = "Synced to your account";
|
|
159
|
+
w.appendChild(h);
|
|
160
|
+
if (graph.syncedAt) {
|
|
161
|
+
const time = document.createElement("p");
|
|
162
|
+
time.className = "share-note";
|
|
163
|
+
time.textContent = `Last synced: ${new Date(graph.syncedAt).toLocaleString()}`;
|
|
164
|
+
w.appendChild(time);
|
|
165
|
+
}
|
|
166
|
+
const badge = document.createElement("p");
|
|
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);
|
|
180
195
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
function renderSyncForm(viewer, container, token) {
|
|
197
|
+
const w = document.createElement("div");
|
|
198
|
+
w.className = "share-form";
|
|
199
|
+
const desc = document.createElement("p");
|
|
200
|
+
desc.className = "share-description";
|
|
201
|
+
desc.textContent = "Your graph will be encrypted and synced to your Backpack account. You\u2019ll get a shareable link.";
|
|
202
|
+
w.appendChild(desc);
|
|
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);
|
|
184
230
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
function renderQuotaExceeded(viewer, container, token) {
|
|
232
|
+
const w = document.createElement("div");
|
|
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";
|
|
196
278
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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}`);
|
|
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);
|
|
209
286
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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);
|
|
304
|
+
}
|
|
305
|
+
function clearErrors(parent) {
|
|
306
|
+
for (const el of parent.querySelectorAll(".share-error"))
|
|
307
|
+
el.remove();
|
|
308
|
+
}
|
|
309
|
+
function appendError(parent, msg) {
|
|
310
|
+
const el = document.createElement("p");
|
|
311
|
+
el.className = "share-error";
|
|
312
|
+
el.textContent = msg;
|
|
313
|
+
parent.appendChild(el);
|
|
216
314
|
}
|
|
217
|
-
|
|
218
|
-
|
|
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})`;
|
|
315
|
+
// --- OAuth PKCE flow ---
|
|
316
|
+
async function startOAuthFlow(viewer, container) {
|
|
236
317
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
318
|
+
const metaRes = await fetch(OAUTH_METADATA_URL);
|
|
319
|
+
const meta = (await metaRes.json());
|
|
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";
|
|
240
401
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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})`;
|
|
417
|
+
try {
|
|
418
|
+
const body = await syncRes.json();
|
|
419
|
+
if (body.error)
|
|
420
|
+
errorMsg = body.error;
|
|
421
|
+
}
|
|
422
|
+
catch { /* ignore */ }
|
|
423
|
+
throw new Error(errorMsg);
|
|
424
|
+
}
|
|
425
|
+
// Step 3: Create share link
|
|
426
|
+
const shareRes = await relayFetch(token, `${RELAY_URL}/api/graphs/${encodeURIComponent(graphName)}/share`, {
|
|
427
|
+
method: "POST",
|
|
428
|
+
});
|
|
429
|
+
if (!shareRes.ok) {
|
|
430
|
+
// Sync succeeded but share link creation failed — still show success without link
|
|
431
|
+
renderSuccess(container, null, encrypted, undefined);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const shareData = (await shareRes.json());
|
|
435
|
+
const shareLink = fragmentKey ? `${shareData.url}#k=${fragmentKey}` : shareData.url;
|
|
436
|
+
renderSuccess(container, shareLink, encrypted, shareData.expires_at);
|
|
252
437
|
}
|
|
253
|
-
async function
|
|
254
|
-
|
|
255
|
-
|
|
438
|
+
async function relayFetch(token, url, init = {}) {
|
|
439
|
+
const headers = new Headers(init.headers);
|
|
440
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
441
|
+
return fetch(url, { ...init, headers });
|
|
256
442
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
443
|
+
// --- BPAK envelope builder ---
|
|
444
|
+
async function buildEnvelope(name, payload, format, graph) {
|
|
445
|
+
const payloadCopy = new Uint8Array(payload).buffer;
|
|
446
|
+
const hash = await crypto.subtle.digest("SHA-256", payloadCopy);
|
|
447
|
+
const checksum = "sha256:" + Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
448
|
+
// Collect node type names for dashboard stats
|
|
449
|
+
const typeSet = new Set();
|
|
450
|
+
for (const node of graph.nodes)
|
|
451
|
+
typeSet.add(node.type);
|
|
452
|
+
const header = JSON.stringify({
|
|
453
|
+
format,
|
|
454
|
+
created_at: new Date().toISOString(),
|
|
455
|
+
backpack_name: name,
|
|
456
|
+
graph_count: 1,
|
|
457
|
+
checksum,
|
|
458
|
+
node_count: graph.nodes.length,
|
|
459
|
+
edge_count: graph.edges.length,
|
|
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;
|
|
274
477
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
478
|
+
// --- Success state ---
|
|
479
|
+
function renderSuccess(container, shareLink, encrypted, expiresAt) {
|
|
480
|
+
const w = document.createElement("div");
|
|
481
|
+
w.className = "share-success";
|
|
482
|
+
const h = document.createElement("h4");
|
|
483
|
+
h.textContent = shareLink ? "Synced & shared!" : "Synced!";
|
|
484
|
+
w.appendChild(h);
|
|
485
|
+
if (shareLink) {
|
|
486
|
+
const row = document.createElement("div");
|
|
487
|
+
row.className = "share-link-row";
|
|
488
|
+
const input = document.createElement("input");
|
|
489
|
+
input.type = "text";
|
|
490
|
+
input.readOnly = true;
|
|
491
|
+
input.value = shareLink;
|
|
492
|
+
input.className = "share-link-input";
|
|
493
|
+
row.appendChild(input);
|
|
494
|
+
const copyBtn = document.createElement("button");
|
|
495
|
+
copyBtn.className = "share-btn-primary";
|
|
496
|
+
copyBtn.textContent = "Copy";
|
|
497
|
+
copyBtn.addEventListener("click", () => {
|
|
498
|
+
navigator.clipboard.writeText(shareLink).then(() => {
|
|
499
|
+
copyBtn.textContent = "Copied!";
|
|
500
|
+
setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000);
|
|
501
|
+
}).catch(() => {
|
|
502
|
+
copyBtn.textContent = "Failed";
|
|
503
|
+
setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
row.appendChild(copyBtn);
|
|
507
|
+
w.appendChild(row);
|
|
508
|
+
}
|
|
509
|
+
if (encrypted) {
|
|
510
|
+
const note = document.createElement("p");
|
|
511
|
+
note.className = "share-note";
|
|
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);
|
|
309
528
|
}
|
|
310
|
-
|
|
311
|
-
|
|
529
|
+
// --- OAuth session helpers ---
|
|
530
|
+
function clearOAuthSessionStorage() {
|
|
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");
|
|
312
536
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
537
|
+
// --- PKCE helpers ---
|
|
538
|
+
function generateCodeVerifier() {
|
|
539
|
+
const arr = new Uint8Array(32);
|
|
540
|
+
crypto.getRandomValues(arr);
|
|
541
|
+
return btoa(String.fromCharCode(...arr)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
316
542
|
}
|
|
317
|
-
async function
|
|
318
|
-
|
|
319
|
-
|
|
543
|
+
async function generateCodeChallenge(verifier) {
|
|
544
|
+
const data = new TextEncoder().encode(verifier);
|
|
545
|
+
const dataCopy = new Uint8Array(data).buffer;
|
|
546
|
+
const digest = await crypto.subtle.digest("SHA-256", dataCopy);
|
|
547
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
320
548
|
}
|
|
321
|
-
export {
|
|
322
|
-
M as activate
|
|
323
|
-
};
|