backpack-viewer 0.7.14 → 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,25 +1,25 @@
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;
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
4
  let m = null;
5
- async function M(t) {
5
+ async function q(t) {
6
6
  const e = await t.settings.get("relay_url");
7
- e && (f = e, T = `${f}/.well-known/oauth-authorization-server`), t.registerTaskbarIcon({
7
+ e && (g = e, L = `${g}/.well-known/oauth-authorization-server`), t.registerTaskbarIcon({
8
8
  label: "Share",
9
9
  iconText: "↗",
10
10
  position: "bottom-center",
11
- onClick: () => $(t)
11
+ onClick: () => B(t)
12
12
  });
13
13
  }
14
- function $(t) {
14
+ function B(t) {
15
15
  if (m && m.isVisible()) {
16
16
  m.setVisible(!1);
17
17
  return;
18
18
  }
19
19
  const e = t.getGraphName();
20
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, {
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
23
  title: `Share "${e}"`,
24
24
  defaultPosition: { left: Math.max(100, (window.innerWidth - 380) / 2), top: Math.max(80, (window.innerHeight - 400) / 2) },
25
25
  persistKey: "share-v2",
@@ -27,122 +27,122 @@ function $(t) {
27
27
  onClose: () => {
28
28
  m = null;
29
29
  }
30
- })), C(t, a);
30
+ })), C(t, s);
31
31
  }
32
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);
33
+ const s = await t.settings.get("relay_token");
34
+ e.replaceChildren(), s ? V(t, e, s) : O(t, e);
35
35
  }
36
- function I(t, e) {
37
- const a = document.createElement("div");
38
- a.className = "share-upsell";
36
+ function O(t, e) {
37
+ const s = document.createElement("div");
38
+ s.className = "share-upsell";
39
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);
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
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);
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
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);
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);
49
49
  }
50
- function B(t, e) {
51
- const a = document.createElement("div");
52
- a.className = "share-token-input";
50
+ function D(t, e) {
51
+ const s = document.createElement("div");
52
+ s.className = "share-token-input";
53
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);
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
57
  const o = document.createElement("div");
58
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();
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
62
  i && (await t.settings.set("relay_token", i), C(t, e));
63
- }), o.appendChild(r);
63
+ }), o.appendChild(a);
64
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);
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
66
  }
67
- async function O(t) {
67
+ async function F(t) {
68
68
  try {
69
- const e = await fetch(`${f}/api/graphs`, {
69
+ const e = await fetch(`${g}/api/graphs`, {
70
70
  headers: { Authorization: `Bearer ${t}` }
71
71
  });
72
72
  if (e.status === 401) return { graphs: [], error: "unauthorized" };
73
73
  if (!e.ok) return { graphs: [], error: `status ${e.status}` };
74
- const a = await e.json();
75
- return { graphs: Array.isArray(a) ? a : [] };
74
+ const s = await e.json();
75
+ return { graphs: Array.isArray(s) ? s : [] };
76
76
  } catch (e) {
77
77
  return { graphs: [], error: e.message };
78
78
  }
79
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);
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
84
  if (o.error === "unauthorized") {
85
85
  await t.settings.remove("relay_token"), C(t, e);
86
86
  return;
87
87
  }
88
88
  e.replaceChildren();
89
- const r = o.graphs.find((c) => c.name === n);
90
- r ? F(t, e, a, r) : V(t, e, a);
89
+ const a = o.graphs.find((c) => c.name === n);
90
+ a ? z(t, e, s, a) : Y(t, e, s);
91
91
  }
92
- function F(t, e, a, n) {
93
- const s = document.createElement("div");
94
- s.className = "share-synced";
92
+ function z(t, e, s, n) {
93
+ const r = document.createElement("div");
94
+ r.className = "share-synced";
95
95
  const o = document.createElement("h4");
96
- if (o.textContent = "Synced to your account", s.appendChild(o), n.syncedAt) {
96
+ if (o.textContent = "Synced to your account", r.appendChild(o), n.syncedAt) {
97
97
  const d = document.createElement("p");
98
- d.className = "share-note", d.textContent = `Last synced: ${new Date(n.syncedAt).toLocaleString()}`, s.appendChild(d);
98
+ d.className = "share-note", d.textContent = `Last synced: ${new Date(n.syncedAt).toLocaleString()}`, r.appendChild(d);
99
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);
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
102
  const c = document.createElement("button");
103
103
  c.className = "share-cta-btn", c.textContent = "Update & Share", c.addEventListener("click", async () => {
104
104
  c.disabled = !0, c.textContent = n.encrypted ? "Encrypting…" : "Syncing…";
105
105
  try {
106
- await x(t, e, a, n.encrypted);
106
+ await A(t, e, s, n.encrypted);
107
107
  } catch (d) {
108
- c.disabled = !1, c.textContent = "Update & Share", S(s), b(s, d.message);
108
+ c.disabled = !1, c.textContent = "Update & Share", N(r), E(r, d.message);
109
109
  }
110
- }), s.appendChild(c);
110
+ }), r.appendChild(c);
111
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);
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);
113
113
  }
114
- function V(t, e, a) {
114
+ function Y(t, e, s) {
115
115
  const n = document.createElement("div");
116
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);
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
119
  const o = document.createElement("button");
120
120
  o.className = "share-cta-btn", o.textContent = "Sync & Share", o.addEventListener("click", async () => {
121
121
  o.disabled = !0, o.textContent = "Encrypting…";
122
122
  try {
123
- await x(t, e, a, !0);
123
+ await A(t, e, s, !0);
124
124
  } catch (c) {
125
125
  o.disabled = !1, o.textContent = "Sync & Share";
126
126
  const i = c.message;
127
127
  if (i.includes("encrypted") && i.includes("limit")) {
128
- z(t, e, a);
128
+ j(t, e, s);
129
129
  return;
130
130
  }
131
- S(n), b(n, i);
131
+ N(n), E(n, i);
132
132
  }
133
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);
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);
136
136
  }
137
- function z(t, e, a) {
137
+ function j(t, e, s) {
138
138
  const n = document.createElement("div");
139
139
  n.className = "share-quota";
140
- const s = document.createElement("h4");
141
- s.textContent = "Encrypted limit reached", n.appendChild(s);
140
+ const r = document.createElement("h4");
141
+ r.textContent = "Encrypted limit reached", n.appendChild(r);
142
142
  const o = document.createElement("p");
143
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);
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
146
  const c = document.createElement("div");
147
147
  c.className = "share-divider", n.appendChild(c);
148
148
  const i = document.createElement("p");
@@ -161,70 +161,73 @@ function z(t, e, a) {
161
161
  }), h.addEventListener("click", async () => {
162
162
  h.disabled = !0, h.textContent = "Syncing…";
163
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);
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);
167
167
  }
168
- }), n.appendChild(h), _(t, e, n);
168
+ }), n.appendChild(h), x(t, e, n);
169
169
  }
170
- function _(t, e, a) {
170
+ function x(t, e, s) {
171
171
  const n = document.createElement("div");
172
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 () => {
173
+ const r = document.createElement("button");
174
+ r.className = "share-token-link", r.textContent = "Sign out", r.addEventListener("click", async () => {
175
175
  await t.settings.remove("relay_token"), C(t, e);
176
- }), n.appendChild(s), a.appendChild(n), e.replaceChildren(a);
176
+ }), n.appendChild(r), s.appendChild(n), e.replaceChildren(s);
177
177
  }
178
- function S(t) {
178
+ function N(t) {
179
179
  for (const e of t.querySelectorAll(".share-error")) e.remove();
180
180
  }
181
- function b(t, e) {
182
- const a = document.createElement("p");
183
- a.className = "share-error", a.textContent = e, t.appendChild(a);
181
+ function E(t, e) {
182
+ const s = document.createElement("p");
183
+ s.className = "share-error", s.textContent = e, t.appendChild(s);
184
184
  }
185
- async function Y(t, e) {
185
+ async function G(t, e) {
186
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);
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
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;
190
+ var w;
191
+ if (((w = h.data) == null ? void 0 : w.type) !== "backpack-oauth-callback") return;
192
192
  window.removeEventListener("message", p);
193
- const { code: w, returnedState: E } = h.data;
194
- if (E === d) {
195
- G();
193
+ const { code: f, returnedState: _ } = h.data;
194
+ if (_ === d) {
195
+ H();
196
196
  try {
197
- const g = await (await fetch(n.token_endpoint, {
197
+ const y = await (await fetch(n.token_endpoint, {
198
198
  method: "POST",
199
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()
200
+ body: new URLSearchParams({ grant_type: "authorization_code", code: f, redirect_uri: i, client_id: o.client_id, code_verifier: a }).toString()
201
201
  })).json();
202
- if (!g.access_token) {
203
- b(e, "Token exchange failed: no access token returned.");
202
+ if (!y.access_token) {
203
+ E(e, "Token exchange failed: no access token returned.");
204
204
  return;
205
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}`);
206
+ await t.settings.set("relay_token", y.access_token), C(t, e);
207
+ } catch (S) {
208
+ E(e, `Token exchange failed: ${S.message}`);
209
209
  }
210
210
  }
211
211
  };
212
212
  window.addEventListener("message", p), (!u || u.closed) && (window.location.href = l.toString());
213
- } catch (a) {
214
- b(e, `Auth failed: ${a.message}`);
213
+ } catch (s) {
214
+ E(e, `Auth failed: ${s.message}`);
215
215
  }
216
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");
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
220
  const c = new TextEncoder().encode(JSON.stringify(o));
221
221
  let i, d, l = "";
222
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(/=+$/, "");
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(/=+$/, "");
225
228
  } else
226
229
  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, {
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, {
228
231
  method: "PUT",
229
232
  headers: { "Content-Type": "application/octet-stream" },
230
233
  body: u
@@ -234,35 +237,37 @@ async function x(t, e, a, n, s = "private") {
234
237
  throw await t.settings.remove("relay_token"), C(t, e), new Error("Session expired. Please sign in again.");
235
238
  let y = `Sync failed (${h.status})`;
236
239
  try {
237
- const g = await h.json();
238
- g.error && (y = g.error);
240
+ const b = await h.json();
241
+ b.error && (y = b.error);
239
242
  } catch {
240
243
  }
241
244
  throw new Error(y);
242
245
  }
243
- const w = await v(a, `${f}/api/graphs/${encodeURIComponent(r)}/share`, {
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`, {
244
249
  method: "POST"
245
250
  });
246
- if (!w.ok) {
247
- A(e, null, n, void 0);
251
+ if (!_.ok) {
252
+ U(e, null, n, void 0);
248
253
  return;
249
254
  }
250
- const E = await w.json(), k = l ? `${E.url}#k=${l}` : E.url;
251
- A(e, k, n, E.expires_at);
255
+ const w = await _.json(), S = l ? `${w.url}#k=${l}` : w.url;
256
+ U(e, S, n, w.expires_at);
252
257
  }
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 });
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 });
256
261
  }
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();
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();
259
264
  for (const h of n.nodes) c.add(h.type);
260
265
  const i = JSON.stringify({
261
- format: a,
266
+ format: s,
262
267
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
263
268
  backpack_name: t,
264
269
  graph_count: 1,
265
- checksum: r,
270
+ checksum: a,
266
271
  node_count: n.nodes.length,
267
272
  edge_count: n.edges.length,
268
273
  node_types: Array.from(c)
@@ -270,17 +275,17 @@ async function j(t, e, a, n) {
270
275
  new DataView(l).setUint32(0, d.length, !1);
271
276
  const u = new Uint8Array(9 + d.length + e.length);
272
277
  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;
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;
274
279
  }
275
- function A(t, e, a, n) {
276
- const s = document.createElement("div");
277
- s.className = "share-success";
280
+ function U(t, e, s, n) {
281
+ const r = document.createElement("div");
282
+ r.className = "share-success";
278
283
  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";
284
+ if (o.textContent = e ? "Synced & shared!" : "Synced!", r.appendChild(o), e) {
285
+ const a = document.createElement("div");
286
+ a.className = "share-link-row";
282
287
  const c = document.createElement("input");
283
- c.type = "text", c.readOnly = !0, c.value = e, c.className = "share-link-input", r.appendChild(c);
288
+ c.type = "text", c.readOnly = !0, c.value = e, c.className = "share-link-input", a.appendChild(c);
284
289
  const i = document.createElement("button");
285
290
  i.className = "share-btn-primary", i.textContent = "Copy", i.addEventListener("click", () => {
286
291
  navigator.clipboard.writeText(e).then(() => {
@@ -292,32 +297,32 @@ function A(t, e, a, n) {
292
297
  i.textContent = "Copy";
293
298
  }, 2e3);
294
299
  });
295
- }), r.appendChild(i), s.appendChild(r);
300
+ }), a.appendChild(i), r.appendChild(a);
296
301
  }
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);
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);
300
305
  } 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);
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);
303
308
  }
304
309
  if (n) {
305
- const r = document.createElement("p");
306
- r.className = "share-note", r.textContent = `Expires: ${new Date(n).toLocaleDateString()}`, s.appendChild(r);
310
+ const a = document.createElement("p");
311
+ a.className = "share-note", a.textContent = `Expires: ${new Date(n).toLocaleDateString()}`, r.appendChild(a);
307
312
  }
308
- t.replaceChildren(s);
313
+ t.replaceChildren(r);
309
314
  }
310
- function G() {
315
+ function H() {
311
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");
312
317
  }
313
- function K() {
318
+ function M() {
314
319
  const t = new Uint8Array(32);
315
320
  return crypto.getRandomValues(t), btoa(String.fromCharCode(...t)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
316
321
  }
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);
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);
319
324
  return btoa(String.fromCharCode(...new Uint8Array(n))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
320
325
  }
321
326
  export {
322
- M as activate
327
+ q as activate
323
328
  };
@@ -1,4 +1,5 @@
1
1
  import { listBackpacks, getActiveBackpack, setActiveBackpack, registerBackpack, unregisterBackpack, } from "backpack-ontology";
2
+ import { readExtensionSettings } from "./server-extensions.js";
2
3
  // --- Small HTTP helpers (used only inside this module) ---
3
4
  function readBody(req) {
4
5
  return new Promise((resolve, reject) => {
@@ -375,6 +376,24 @@ export async function handleApiRequest(req, res, ctx) {
375
376
  }
376
377
  return true;
377
378
  }
379
+ // --- /api/sync-status ---
380
+ if (url === "/api/sync-status" && method === "GET") {
381
+ try {
382
+ const settings = await readExtensionSettings("share");
383
+ const syncedMap = settings.synced;
384
+ const synced = [];
385
+ if (syncedMap && typeof syncedMap === "object" && !Array.isArray(syncedMap)) {
386
+ for (const name of Object.keys(syncedMap)) {
387
+ synced.push(name);
388
+ }
389
+ }
390
+ sendJson(res, 200, { synced });
391
+ }
392
+ catch {
393
+ sendJson(res, 200, { synced: [] });
394
+ }
395
+ return true;
396
+ }
378
397
  // --- /api/ontologies/* ---
379
398
  if (url === "/api/ontologies" && method === "GET") {
380
399
  try {
package/dist/sidebar.js CHANGED
@@ -254,6 +254,10 @@ export function initSidebar(container, onSelectOrCallbacks) {
254
254
  const lockBatchPromise = fetch("/api/locks")
255
255
  .then((r) => r.json())
256
256
  .catch(() => ({}));
257
+ const syncStatusPromise = fetch("/api/sync-status")
258
+ .then((r) => r.json())
259
+ .then((d) => new Set(d.synced))
260
+ .catch(() => new Set());
257
261
  items = summaries.map((s) => {
258
262
  const li = document.createElement("li");
259
263
  li.className = "ontology-item";
@@ -273,8 +277,6 @@ export function initSidebar(container, onSelectOrCallbacks) {
273
277
  lockBadge.className = "sidebar-lock-badge";
274
278
  lockBadge.dataset.graph = s.name;
275
279
  lockBatchPromise.then((locks) => {
276
- // Bail if this badge has been detached from the DOM (sidebar
277
- // re-rendered before the batch resolved)
278
280
  if (!lockBadge.isConnected)
279
281
  return;
280
282
  const lock = locks[s.name];
@@ -284,9 +286,23 @@ export function initSidebar(container, onSelectOrCallbacks) {
284
286
  lockBadge.classList.add("active");
285
287
  }
286
288
  });
289
+ // Sync badge — shows a cloud icon for graphs that have been synced
290
+ const syncBadge = document.createElement("span");
291
+ syncBadge.className = "sidebar-sync-badge";
292
+ syncBadge.dataset.graph = s.name;
293
+ syncStatusPromise.then((syncedSet) => {
294
+ if (!syncBadge.isConnected)
295
+ return;
296
+ if (syncedSet.has(s.name)) {
297
+ syncBadge.textContent = "synced";
298
+ syncBadge.title = "This graph has been synced";
299
+ syncBadge.classList.add("active");
300
+ }
301
+ });
287
302
  li.appendChild(nameSpan);
288
303
  li.appendChild(statsSpan);
289
304
  li.appendChild(lockBadge);
305
+ li.appendChild(syncBadge);
290
306
  li.appendChild(branchSpan);
291
307
  if (cbs.onRename) {
292
308
  const editBtn = document.createElement("button");
package/dist/style.css CHANGED
@@ -506,6 +506,22 @@ body {
506
506
  color: #c08c00;
507
507
  }
508
508
 
509
+ .sidebar-sync-badge {
510
+ font-size: 10px;
511
+ color: #5a9fd4;
512
+ display: none;
513
+ margin-top: 2px;
514
+ }
515
+
516
+ .sidebar-sync-badge.active {
517
+ display: block;
518
+ }
519
+
520
+ .sidebar-sync-badge.active::before {
521
+ content: "☁ ";
522
+ color: #5a9fd4;
523
+ }
524
+
509
525
  .branch-picker {
510
526
  background: var(--bg-surface);
511
527
  border: 1px solid var(--border);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backpack-viewer",
3
- "version": "0.7.14",
3
+ "version": "0.7.15",
4
4
  "description": "Web-based graph visualizer for backpack-ontology — Canvas 2D, force-directed layout, live reload",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Noah Irzinger",