backpack-viewer 0.7.13 → 0.7.15

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.
@@ -1,548 +1,328 @@
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`;
12
- }
13
- viewer.registerTaskbarIcon({
14
- label: "Share",
15
- iconText: "\u2197",
16
- position: "bottom-center",
17
- onClick: () => toggleSharePanel(viewer),
18
- });
1
+ const R = "https://app.backpackontology.com";
2
+ let g = R, L = `${g}/.well-known/oauth-authorization-server`;
3
+ const $ = new Uint8Array([66, 80, 65, 75]), I = 1;
4
+ let m = null;
5
+ async function q(t) {
6
+ const e = await t.settings.get("relay_url");
7
+ e && (g = e, L = `${g}/.well-known/oauth-authorization-server`), t.registerTaskbarIcon({
8
+ label: "Share",
9
+ iconText: "↗",
10
+ position: "bottom-center",
11
+ onClick: () => B(t)
12
+ });
19
13
  }
20
- function toggleSharePanel(viewer) {
21
- if (panel && panel.isVisible()) {
22
- panel.setVisible(false);
23
- return;
14
+ function B(t) {
15
+ if (m && m.isVisible()) {
16
+ m.setVisible(!1);
17
+ return;
18
+ }
19
+ const e = t.getGraphName();
20
+ if (!e) return;
21
+ const s = document.createElement("div");
22
+ s.className = "share-panel-body", m ? (m.element.replaceChildren(), m.element.appendChild(s), m.setTitle(`Share "${e}"`), m.setVisible(!0), m.bringToFront()) : (s.textContent = "Loading...", m = t.mountPanel(s, {
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
- 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);
30
+ })), C(t, s);
48
31
  }
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
- }
32
+ async function C(t, e) {
33
+ const s = await t.settings.get("relay_token");
34
+ e.replaceChildren(), s ? V(t, e, s) : O(t, e);
58
35
  }
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);
36
+ function O(t, e) {
37
+ const s = document.createElement("div");
38
+ s.className = "share-upsell";
39
+ const n = document.createElement("h4");
40
+ n.textContent = "Share this graph with anyone", s.appendChild(n);
41
+ const r = document.createElement("p");
42
+ r.textContent = "Encrypt your graph and get a shareable link. Recipients open it in their browser — no install needed.", s.appendChild(r);
43
+ const o = document.createElement("button");
44
+ o.className = "share-cta-btn", o.textContent = "Sign in to share", o.addEventListener("click", () => G(t, e)), s.appendChild(o);
45
+ const a = document.createElement("button");
46
+ a.className = "share-token-link", a.textContent = "Or paste an API token", a.addEventListener("click", () => D(t, e)), s.appendChild(a);
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.", s.appendChild(c), e.replaceChildren(s);
84
49
  }
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);
50
+ function D(t, e) {
51
+ const s = document.createElement("div");
52
+ s.className = "share-token-input";
53
+ const n = document.createElement("p");
54
+ n.textContent = "Paste your API token from your account settings:", s.appendChild(n);
55
+ const r = document.createElement("input");
56
+ r.type = "password", r.placeholder = "Token", r.className = "share-input", s.appendChild(r);
57
+ const o = document.createElement("div");
58
+ o.className = "share-btn-row";
59
+ const a = document.createElement("button");
60
+ a.className = "share-btn-primary", a.textContent = "Save", a.addEventListener("click", async () => {
61
+ const i = r.value.trim();
62
+ i && (await t.settings.set("relay_token", i), C(t, e));
63
+ }), o.appendChild(a);
64
+ const c = document.createElement("button");
65
+ c.className = "share-btn-secondary", c.textContent = "Back", c.addEventListener("click", () => C(t, e)), o.appendChild(c), s.appendChild(o), e.replaceChildren(s);
66
+ }
67
+ async function F(t) {
68
+ try {
69
+ const e = await fetch(`${g}/api/graphs`, {
70
+ headers: { Authorization: `Bearer ${t}` }
107
71
  });
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);
72
+ if (e.status === 401) return { graphs: [], error: "unauthorized" };
73
+ if (!e.ok) return { graphs: [], error: `status ${e.status}` };
74
+ const s = await e.json();
75
+ return { graphs: Array.isArray(s) ? s : [] };
76
+ } catch (e) {
77
+ return { graphs: [], error: e.message };
78
+ }
79
+ }
80
+ async function V(t, e, s) {
81
+ const n = t.getGraphName(), r = document.createElement("div");
82
+ r.className = "share-loading", r.textContent = "Checking account…", e.replaceChildren(r);
83
+ const o = await F(s);
84
+ if (o.error === "unauthorized") {
85
+ await t.settings.remove("relay_token"), C(t, e);
86
+ return;
87
+ }
88
+ e.replaceChildren();
89
+ const a = o.graphs.find((c) => c.name === n);
90
+ a ? z(t, e, s, a) : Y(t, e, s);
116
91
  }
117
- async function fetchGraphs(token) {
92
+ function z(t, e, s, n) {
93
+ const r = document.createElement("div");
94
+ r.className = "share-synced";
95
+ const o = document.createElement("h4");
96
+ if (o.textContent = "Synced to your account", r.appendChild(o), n.syncedAt) {
97
+ const d = document.createElement("p");
98
+ d.className = "share-note", d.textContent = `Last synced: ${new Date(n.syncedAt).toLocaleString()}`, r.appendChild(d);
99
+ }
100
+ const a = document.createElement("p");
101
+ a.className = "share-note", a.textContent = n.encrypted ? "Encrypted — server cannot read your data." : "Public — stored unencrypted.", r.appendChild(a);
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
- 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 };
106
+ await A(t, e, s, n.encrypted);
107
+ } catch (d) {
108
+ c.disabled = !1, c.textContent = "Update & Share", N(r), E(r, d.message);
131
109
  }
110
+ }), r.appendChild(c);
111
+ const i = document.createElement("a");
112
+ i.href = g, i.target = "_blank", i.rel = "noopener", i.className = "share-token-link", i.textContent = "Open dashboard", r.appendChild(i), x(t, e, r);
132
113
  }
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);
114
+ function Y(t, e, s) {
115
+ const n = document.createElement("div");
116
+ n.className = "share-form";
117
+ const r = document.createElement("p");
118
+ r.className = "share-description", r.textContent = "Your graph will be encrypted and synced to your Backpack account. You’ll get a shareable link.", n.appendChild(r);
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 A(t, e, s, !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
+ j(t, e, s);
143
129
  return;
130
+ }
131
+ N(n), E(n, i);
144
132
  }
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);
152
- }
133
+ }), n.appendChild(o);
134
+ const a = document.createElement("p");
135
+ a.className = "share-trust", a.textContent = "Your first encrypted graph is free. Data is encrypted before upload — we can’t read it.", n.appendChild(a), x(t, e, n);
153
136
  }
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);
137
+ function j(t, e, s) {
138
+ const n = document.createElement("div");
139
+ n.className = "share-quota";
140
+ const r = document.createElement("h4");
141
+ r.textContent = "Encrypted limit reached", n.appendChild(r);
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 a = document.createElement("a");
145
+ a.href = `${g}/settings`, a.target = "_blank", a.rel = "noopener", a.className = "share-cta-btn share-btn-link", a.textContent = "Upgrade for unlimited encryption", n.appendChild(a);
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 A(t, e, s, !1, "public");
165
+ } catch (f) {
166
+ h.disabled = !1, h.textContent = "Share as public graph", N(n), E(n, f.message);
165
167
  }
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);
168
+ }), n.appendChild(h), x(t, e, n);
195
169
  }
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);
170
+ function x(t, e, s) {
171
+ const n = document.createElement("div");
172
+ n.className = "share-footer";
173
+ const r = document.createElement("button");
174
+ r.className = "share-token-link", r.textContent = "Sign out", r.addEventListener("click", async () => {
175
+ await t.settings.remove("relay_token"), C(t, e);
176
+ }), n.appendChild(r), s.appendChild(n), e.replaceChildren(s);
230
177
  }
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";
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 N(t) {
179
+ for (const e of t.querySelectorAll(".share-error")) e.remove();
304
180
  }
305
- function clearErrors(parent) {
306
- for (const el of parent.querySelectorAll(".share-error"))
307
- el.remove();
181
+ function E(t, e) {
182
+ const s = document.createElement("p");
183
+ s.className = "share-error", s.textContent = e, t.appendChild(s);
308
184
  }
309
- function appendError(parent, msg) {
310
- const el = document.createElement("p");
311
- el.className = "share-error";
312
- el.textContent = msg;
313
- parent.appendChild(el);
314
- }
315
- // --- OAuth PKCE flow ---
316
- async function startOAuthFlow(viewer, container) {
317
- try {
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";
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 G(t, e) {
186
+ try {
187
+ const n = await (await fetch(L)).json(), o = await (await fetch(n.registration_endpoint, { method: "POST" })).json(), a = M(), c = await J(a), 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", a), 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 w;
191
+ if (((w = h.data) == null ? void 0 : w.type) !== "backpack-oauth-callback") return;
192
+ window.removeEventListener("message", p);
193
+ const { code: f, returnedState: _ } = h.data;
194
+ if (_ === d) {
195
+ H();
417
196
  try {
418
- const body = await syncRes.json();
419
- if (body.error)
420
- errorMsg = body.error;
197
+ const y = 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: f, redirect_uri: i, client_id: o.client_id, code_verifier: a }).toString()
201
+ })).json();
202
+ if (!y.access_token) {
203
+ E(e, "Token exchange failed: no access token returned.");
204
+ return;
205
+ }
206
+ await t.settings.set("relay_token", y.access_token), C(t, e);
207
+ } catch (S) {
208
+ E(e, `Token exchange failed: ${S.message}`);
421
209
  }
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;
210
+ }
211
+ };
212
+ window.addEventListener("message", p), (!u || u.closed) && (window.location.href = l.toString());
213
+ } catch (s) {
214
+ E(e, `Auth failed: ${s.message}`);
215
+ }
216
+ }
217
+ async function A(t, e, s, n, r = "private") {
218
+ const o = t.getGraph(), a = t.getGraphName();
219
+ if (!o || !a) 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"), b = await t.settings.get("keys") || {};
224
+ let k = b[a];
225
+ k || (k = await y.generateX25519Identity(), b[a] = k, await t.settings.set("keys", b));
226
+ const P = await y.identityToRecipient(k), v = new y.Encrypter();
227
+ v.addRecipient(P), i = await v.encrypt(c), d = "age-v1", l = btoa(k).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
228
+ } else
229
+ i = c, d = "plaintext";
230
+ const u = await K(a, i, d, o), p = `${g}/api/graphs/${encodeURIComponent(a)}/sync${r === "public" ? "?visibility=public" : ""}`, h = await T(s, p, {
231
+ method: "PUT",
232
+ headers: { "Content-Type": "application/octet-stream" },
233
+ body: u
234
+ });
235
+ if (!h.ok) {
236
+ if (h.status === 401)
237
+ throw await t.settings.remove("relay_token"), C(t, e), new Error("Session expired. Please sign in again.");
238
+ let y = `Sync failed (${h.status})`;
239
+ try {
240
+ const b = await h.json();
241
+ b.error && (y = b.error);
242
+ } catch {
433
243
  }
434
- const shareData = (await shareRes.json());
435
- const shareLink = fragmentKey ? `${shareData.url}#k=${fragmentKey}` : shareData.url;
436
- renderSuccess(container, shareLink, encrypted, shareData.expires_at);
244
+ throw new Error(y);
245
+ }
246
+ const f = await t.settings.get("synced") || {};
247
+ f[a] || (f[a] = !0, await t.settings.set("synced", f));
248
+ const _ = await T(s, `${g}/api/graphs/${encodeURIComponent(a)}/share`, {
249
+ method: "POST"
250
+ });
251
+ if (!_.ok) {
252
+ U(e, null, n, void 0);
253
+ return;
254
+ }
255
+ const w = await _.json(), S = l ? `${w.url}#k=${l}` : w.url;
256
+ U(e, S, n, w.expires_at);
437
257
  }
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 });
258
+ async function T(t, e, s = {}) {
259
+ const n = new Headers(s.headers);
260
+ return n.set("Authorization", `Bearer ${t}`), fetch(e, { ...s, headers: n });
442
261
  }
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;
262
+ async function K(t, e, s, n) {
263
+ const r = new Uint8Array(e).buffer, o = await crypto.subtle.digest("SHA-256", r), a = "sha256:" + Array.from(new Uint8Array(o)).map((h) => h.toString(16).padStart(2, "0")).join(""), c = /* @__PURE__ */ new Set();
264
+ for (const h of n.nodes) c.add(h.type);
265
+ const i = JSON.stringify({
266
+ format: s,
267
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
268
+ backpack_name: t,
269
+ graph_count: 1,
270
+ checksum: a,
271
+ node_count: n.nodes.length,
272
+ edge_count: n.edges.length,
273
+ node_types: Array.from(c)
274
+ }), d = new TextEncoder().encode(i), l = new ArrayBuffer(4);
275
+ new DataView(l).setUint32(0, d.length, !1);
276
+ const u = new Uint8Array(9 + d.length + e.length);
277
+ let p = 0;
278
+ return u.set($, p), p += 4, u[p] = I, p += 1, u.set(new Uint8Array(l), p), p += 4, u.set(d, p), p += d.length, u.set(e, p), u;
477
279
  }
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);
280
+ function U(t, e, s, n) {
281
+ const r = document.createElement("div");
282
+ r.className = "share-success";
283
+ const o = document.createElement("h4");
284
+ if (o.textContent = e ? "Synced & shared!" : "Synced!", r.appendChild(o), e) {
285
+ const a = document.createElement("div");
286
+ a.className = "share-link-row";
287
+ const c = document.createElement("input");
288
+ c.type = "text", c.readOnly = !0, c.value = e, c.className = "share-link-input", a.appendChild(c);
289
+ const i = document.createElement("button");
290
+ i.className = "share-btn-primary", i.textContent = "Copy", i.addEventListener("click", () => {
291
+ navigator.clipboard.writeText(e).then(() => {
292
+ i.textContent = "Copied!", setTimeout(() => {
293
+ i.textContent = "Copy";
294
+ }, 2e3);
295
+ }).catch(() => {
296
+ i.textContent = "Failed", setTimeout(() => {
297
+ i.textContent = "Copy";
298
+ }, 2e3);
299
+ });
300
+ }), a.appendChild(i), r.appendChild(a);
301
+ }
302
+ if (s) {
303
+ const a = document.createElement("p");
304
+ a.className = "share-note", a.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.", r.appendChild(a);
305
+ } else {
306
+ const a = document.createElement("p");
307
+ a.className = "share-note", a.textContent = "This graph is stored unencrypted. Anyone with the link can view it.", r.appendChild(a);
308
+ }
309
+ if (n) {
310
+ const a = document.createElement("p");
311
+ a.className = "share-note", a.textContent = `Expires: ${new Date(n).toLocaleDateString()}`, r.appendChild(a);
312
+ }
313
+ t.replaceChildren(r);
528
314
  }
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");
315
+ function H() {
316
+ 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
317
  }
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(/=+$/, "");
318
+ function M() {
319
+ const t = new Uint8Array(32);
320
+ return crypto.getRandomValues(t), btoa(String.fromCharCode(...t)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
542
321
  }
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(/=+$/, "");
322
+ async function J(t) {
323
+ const e = new TextEncoder().encode(t), s = new Uint8Array(e).buffer, n = await crypto.subtle.digest("SHA-256", s);
324
+ return btoa(String.fromCharCode(...new Uint8Array(n))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
548
325
  }
326
+ export {
327
+ q as activate
328
+ };