@graffiti-garden/wrapper-data-guard 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -25,6 +25,7 @@ var import_api = require("@graffiti-garden/api");
25
25
  var import_client = require("@graffiti-garden/wrapper-iframe-rpc/client");
26
26
  let instance;
27
27
  const defaultConnectionTimeout = 1e4;
28
+ const pendingLoginKey = "graffiti-guard-login:";
28
29
  class GraffitiGuarded extends import_api.Graffiti {
29
30
  sessionEvents;
30
31
  hostUrl;
@@ -33,6 +34,9 @@ class GraffitiGuarded extends import_api.Graffiti {
33
34
  onMessage;
34
35
  connectionTimeout;
35
36
  destroyed = false;
37
+ returnFocus = null;
38
+ backgroundInert = /* @__PURE__ */ new Map();
39
+ backgroundObserver;
36
40
  constructor(options = {}) {
37
41
  super();
38
42
  if (instance) return instance;
@@ -40,6 +44,8 @@ class GraffitiGuarded extends import_api.Graffiti {
40
44
  options.hostUrl?.toString() ?? "https://guard.graffiti.garden/",
41
45
  document.baseURI
42
46
  );
47
+ const loginKey = pendingLoginKey + hostUrl.origin;
48
+ const loginNonce = sessionStorage.getItem(loginKey);
43
49
  const iframe = document.createElement("iframe");
44
50
  iframe.title = "Graffiti Guard";
45
51
  iframe.allow = "storage-access";
@@ -58,7 +64,7 @@ class GraffitiGuarded extends import_api.Graffiti {
58
64
  background: "transparent",
59
65
  border: "0",
60
66
  display: "none",
61
- // Prompts scroll inside this frame, so size it for visible browser controls.
67
+ // Keep the overlay within the visible viewport when browser controls move.
62
68
  height: CSS.supports("height", "100svh") ? "100svh" : "100vh",
63
69
  inset: "0",
64
70
  position: "fixed",
@@ -71,7 +77,8 @@ class GraffitiGuarded extends import_api.Graffiti {
71
77
  iframe.contentWindow?.postMessage(
72
78
  {
73
79
  type: "graffiti-guard:connect",
74
- pageUrl: window.location.href
80
+ pageUrl: window.location.href,
81
+ ...loginNonce ? { loginNonce } : {}
75
82
  },
76
83
  hostUrl.origin
77
84
  );
@@ -96,6 +103,10 @@ class GraffitiGuarded extends import_api.Graffiti {
96
103
  channel: options.channel,
97
104
  timeout: options.timeout
98
105
  });
106
+ this.sessionEvents = rpc.sessionEvents;
107
+ this.sessionEvents.addEventListener("initialized", () => {
108
+ if (sessionStorage.getItem(loginKey) === loginNonce) sessionStorage.removeItem(loginKey);
109
+ }, { once: true });
99
110
  const connectionTimeout = window.setTimeout(
100
111
  showConnectionError,
101
112
  options.timeout ?? defaultConnectionTimeout
@@ -112,10 +123,8 @@ class GraffitiGuarded extends import_api.Graffiti {
112
123
  if (event.data.type === "graffiti-guard:connected") {
113
124
  return;
114
125
  } else if (event.data.type === "graffiti-guard:set-visible" && typeof event.data.visible === "boolean") {
115
- iframe.style.display = event.data.visible ? "block" : "none";
116
- iframe.setAttribute("aria-hidden", String(!event.data.visible));
126
+ this.setGuardVisible(event.data.visible);
117
127
  if (event.data.visible) {
118
- iframe.focus();
119
128
  remoteWindow.postMessage(
120
129
  { type: "graffiti-guard:shown" },
121
130
  hostUrl.origin
@@ -139,7 +148,6 @@ class GraffitiGuarded extends import_api.Graffiti {
139
148
  this.hostUrl = hostUrl;
140
149
  this.iframe = iframe;
141
150
  this.rpc = rpc;
142
- this.sessionEvents = rpc.sessionEvents;
143
151
  this.onMessage = onMessage;
144
152
  this.connectionTimeout = connectionTimeout;
145
153
  instance = new Proxy(this, {
@@ -153,13 +161,17 @@ class GraffitiGuarded extends import_api.Graffiti {
153
161
  });
154
162
  return instance;
155
163
  }
156
- login = (actor) => {
164
+ login = (options) => {
157
165
  const loginUrl = new URL(this.hostUrl);
158
166
  const params = new URLSearchParams({
159
167
  guardLogin: "1",
160
168
  redirectUrl: window.location.href
161
169
  });
162
- if (actor) params.set("suggestedActor", actor);
170
+ if (options?.actor) params.set("suggestedActor", options.actor);
171
+ const nonce = crypto.randomUUID();
172
+ sessionStorage.setItem(pendingLoginKey + this.hostUrl.origin, nonce);
173
+ params.set("loginNonce", nonce);
174
+ if (options?.state !== void 0) params.set("state", options.state);
163
175
  loginUrl.hash = params.toString();
164
176
  window.location.assign(loginUrl.href);
165
177
  return Promise.resolve();
@@ -175,11 +187,55 @@ class GraffitiGuarded extends import_api.Graffiti {
175
187
  auditUrl.hash = params.toString();
176
188
  return auditUrl.href;
177
189
  }
190
+ setGuardVisible(visible) {
191
+ if (visible) {
192
+ if (this.iframe.style.display === "block") return;
193
+ const focused = document.activeElement;
194
+ this.returnFocus = focused instanceof HTMLElement && focused !== this.iframe ? focused : null;
195
+ this.inertBackground();
196
+ this.backgroundObserver = new MutationObserver(() => this.inertBackground());
197
+ this.backgroundObserver.observe(document.body, { childList: true });
198
+ this.iframe.style.display = "block";
199
+ this.iframe.setAttribute("aria-hidden", "false");
200
+ this.iframe.focus();
201
+ return;
202
+ }
203
+ if (this.iframe.style.display === "none") return;
204
+ this.iframe.style.display = "none";
205
+ this.iframe.setAttribute("aria-hidden", "true");
206
+ this.backgroundObserver?.disconnect();
207
+ this.backgroundObserver = void 0;
208
+ for (const [element, wasInert] of this.backgroundInert) {
209
+ element.inert = wasInert;
210
+ }
211
+ this.backgroundInert.clear();
212
+ const target = this.returnFocus;
213
+ this.returnFocus = null;
214
+ if (target?.isConnected && !target.inert && target !== document.body) {
215
+ target.focus({ preventScroll: true });
216
+ } else {
217
+ const previousTabindex = document.body.getAttribute("tabindex");
218
+ document.body.tabIndex = -1;
219
+ document.body.focus({ preventScroll: true });
220
+ if (previousTabindex === null) document.body.removeAttribute("tabindex");
221
+ else document.body.setAttribute("tabindex", previousTabindex);
222
+ }
223
+ }
224
+ inertBackground() {
225
+ for (const child of Array.from(document.body.children)) {
226
+ if (child === this.iframe || !(child instanceof HTMLElement)) continue;
227
+ if (!this.backgroundInert.has(child)) {
228
+ this.backgroundInert.set(child, child.inert);
229
+ }
230
+ child.inert = true;
231
+ }
232
+ }
178
233
  destroy() {
179
234
  if (this.destroyed) return;
180
235
  this.destroyed = true;
181
236
  window.clearTimeout(this.connectionTimeout);
182
237
  window.removeEventListener("message", this.onMessage);
238
+ if (this.iframe.style.display === "block") this.setGuardVisible(false);
183
239
  this.rpc.destroy();
184
240
  this.iframe.remove();
185
241
  if (instance === this) instance = void 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import { Graffiti, type GraffitiSession } from \"@graffiti-garden/api\";\nimport { GraffitiRpcClient } from \"@graffiti-garden/wrapper-iframe-rpc/client\";\n\ndeclare const DATA_GUARD_DEFAULT_HOST_URL: string;\n\nlet instance: GraffitiGuarded | undefined;\nconst defaultConnectionTimeout = 10_000;\n\nexport interface GraffitiGuardedOptions {\n hostUrl?: string | URL;\n channel?: string;\n timeout?: number;\n}\n\nexport interface GraffitiGuardSourceSegment {\n id: string;\n name: string;\n}\n\nexport type GraffitiGuardSession = GraffitiSession & {\n source?: GraffitiGuardSourceSegment[];\n};\n\n/** A Graffiti implementation backed by a sandboxed guard iframe. */\n// @ts-ignore Graffiti methods are delegated through the constructor proxy.\nexport class GraffitiGuarded extends Graffiti {\n readonly sessionEvents!: EventTarget;\n private readonly hostUrl!: URL;\n private readonly iframe!: HTMLIFrameElement;\n private readonly rpc!: GraffitiRpcClient;\n private readonly onMessage!: (event: MessageEvent) => void;\n private readonly connectionTimeout!: number;\n private destroyed = false;\n\n constructor(options: GraffitiGuardedOptions = {}) {\n super();\n if (instance) return instance;\n\n const hostUrl = new URL(\n options.hostUrl?.toString() ?? DATA_GUARD_DEFAULT_HOST_URL,\n document.baseURI,\n );\n const iframe = document.createElement(\"iframe\");\n iframe.title = \"Graffiti Guard\";\n iframe.allow = \"storage-access\";\n iframe.sandbox.add(\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-storage-access-by-user-activation\",\n \"allow-downloads\",\n \"allow-forms\",\n \"allow-popups\",\n \"allow-top-navigation-by-user-activation\",\n );\n iframe.src = hostUrl.href;\n iframe.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(iframe.style, {\n background: \"transparent\",\n border: \"0\",\n display: \"none\",\n // Prompts scroll inside this frame, so size it for visible browser controls.\n height: CSS.supports(\"height\", \"100svh\") ? \"100svh\" : \"100vh\",\n inset: \"0\",\n position: \"fixed\",\n width: \"100%\",\n zIndex: \"2147483647\",\n });\n let connected = false;\n let connectionErrorShown = false;\n iframe.addEventListener(\"load\", () => {\n // Bootstrap the guard with the browser-reported embedding origin, which\n // the RPC handshake verifies internally but does not expose to the host.\n iframe.contentWindow?.postMessage(\n {\n type: \"graffiti-guard:connect\",\n pageUrl: window.location.href,\n },\n hostUrl.origin,\n );\n });\n const showConnectionError = () => {\n if (connected || connectionErrorShown) return;\n connectionErrorShown = true;\n window.alert(\n `This app could not connect to its data service at ${hostUrl.origin}. ` +\n \"The service may be offline, or your browser may be blocking its security certificate. \" +\n \"Open that address directly, then reload this page.\",\n );\n };\n iframe.addEventListener(\"error\", showConnectionError);\n document.body.append(iframe);\n\n const remoteWindow = iframe.contentWindow;\n if (!remoteWindow) {\n iframe.remove();\n throw new Error(\"Graffiti Guard iframe did not create a content window.\");\n }\n\n const rpc = new GraffitiRpcClient({\n remoteWindow,\n allowedOrigins: [hostUrl.origin],\n channel: options.channel,\n timeout: options.timeout,\n });\n const connectionTimeout = window.setTimeout(\n showConnectionError,\n options.timeout ?? defaultConnectionTimeout,\n );\n const markConnected = () => {\n connected = true;\n window.clearTimeout(connectionTimeout);\n };\n const onMessage = (event: MessageEvent) => {\n if (\n event.source !== remoteWindow ||\n event.origin !== hostUrl.origin ||\n !event.data ||\n typeof event.data !== \"object\"\n ) {\n return;\n }\n markConnected();\n if (\n event.data.type === \"graffiti-guard:connected\"\n ) {\n return;\n } else if (\n event.data.type === \"graffiti-guard:set-visible\" &&\n typeof event.data.visible === \"boolean\"\n ) {\n iframe.style.display = event.data.visible ? \"block\" : \"none\";\n iframe.setAttribute(\"aria-hidden\", String(!event.data.visible));\n if (event.data.visible) {\n iframe.focus();\n remoteWindow.postMessage(\n { type: \"graffiti-guard:shown\" },\n hostUrl.origin,\n );\n }\n } else if (\n event.data.type === \"graffiti-guard:open-storage-setup\" &&\n typeof event.data.url === \"string\"\n ) {\n try {\n const setupUrl = new URL(event.data.url);\n const setupParams = new URLSearchParams(setupUrl.hash.slice(1));\n if (\n setupUrl.origin !== hostUrl.origin ||\n (setupParams.get(\"guardStorageSetup\") !== \"1\" &&\n setupUrl.searchParams.get(\"guardStorageSetup\") !== \"1\")\n ) {\n return;\n }\n window.location.assign(setupUrl);\n } catch {}\n } else if (\n event.data.type === \"graffiti-guard:open-audit\" &&\n typeof event.data.actor === \"string\" &&\n Array.isArray(event.data.source)\n ) {\n window.location.assign(this.auditUrl(event.data));\n }\n };\n window.addEventListener(\"message\", onMessage);\n\n this.hostUrl = hostUrl;\n this.iframe = iframe;\n this.rpc = rpc;\n this.sessionEvents = rpc.sessionEvents;\n this.onMessage = onMessage;\n this.connectionTimeout = connectionTimeout;\n\n instance = new Proxy(this, {\n get(target, property, receiver) {\n if (Reflect.has(target, property)) {\n return Reflect.get(target, property, receiver);\n }\n const value = Reflect.get(rpc, property, rpc);\n return typeof value === \"function\" ? value.bind(rpc) : value;\n },\n });\n return instance;\n }\n\n login: Graffiti[\"login\"] = (actor) => {\n const loginUrl = new URL(this.hostUrl);\n // The guard reads this state in the browser; keep it out of the HTTP request.\n // This allows for very long redirectUrls\n const params = new URLSearchParams({\n guardLogin: \"1\",\n redirectUrl: window.location.href,\n });\n if (actor) params.set(\"suggestedActor\", actor);\n loginUrl.hash = params.toString();\n window.location.assign(loginUrl.href);\n return Promise.resolve();\n };\n\n /** Link to the host-owned audit panel without exposing audit data to this app. */\n auditUrl(scope?: {\n actor?: string;\n source?: GraffitiGuardSourceSegment[];\n }) {\n const auditUrl = new URL(this.hostUrl);\n const params = new URLSearchParams({\n redirectUrl: window.location.href,\n source: JSON.stringify(scope?.source ?? []),\n });\n if (scope?.actor) params.set(\"actor\", scope.actor);\n auditUrl.hash = params.toString();\n return auditUrl.href;\n }\n\n destroy() {\n if (this.destroyed) return;\n this.destroyed = true;\n window.clearTimeout(this.connectionTimeout);\n window.removeEventListener(\"message\", this.onMessage);\n this.rpc.destroy();\n this.iframe.remove();\n if (instance === this) instance = undefined;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n Graffiti,\n type GraffitiSession,\n} from \"@graffiti-garden/api\";\nimport { GraffitiRpcClient } from \"@graffiti-garden/wrapper-iframe-rpc/client\";\n\ndeclare const DATA_GUARD_DEFAULT_HOST_URL: string;\n\nlet instance: GraffitiGuarded | undefined;\nconst defaultConnectionTimeout = 10_000;\nconst pendingLoginKey = \"graffiti-guard-login:\";\n\nexport interface GraffitiGuardedOptions {\n hostUrl?: string | URL;\n channel?: string;\n timeout?: number;\n}\n\nexport interface GraffitiGuardSourceSegment {\n id: string;\n name: string;\n}\n\nexport type GraffitiGuardSession = GraffitiSession & {\n source?: GraffitiGuardSourceSegment[];\n};\n\n/** A Graffiti implementation backed by a sandboxed guard iframe. */\n// @ts-ignore Graffiti methods are delegated through the constructor proxy.\nexport class GraffitiGuarded extends Graffiti {\n readonly sessionEvents!: Graffiti[\"sessionEvents\"];\n private readonly hostUrl!: URL;\n private readonly iframe!: HTMLIFrameElement;\n private readonly rpc!: GraffitiRpcClient;\n private readonly onMessage!: (event: MessageEvent) => void;\n private readonly connectionTimeout!: number;\n private destroyed = false;\n private returnFocus: HTMLElement | null = null;\n private readonly backgroundInert = new Map<HTMLElement, boolean>();\n private backgroundObserver: MutationObserver | undefined;\n\n constructor(options: GraffitiGuardedOptions = {}) {\n super();\n if (instance) return instance;\n\n const hostUrl = new URL(\n options.hostUrl?.toString() ?? DATA_GUARD_DEFAULT_HOST_URL,\n document.baseURI,\n );\n const loginKey = pendingLoginKey + hostUrl.origin;\n const loginNonce = sessionStorage.getItem(loginKey);\n const iframe = document.createElement(\"iframe\");\n iframe.title = \"Graffiti Guard\";\n iframe.allow = \"storage-access\";\n iframe.sandbox.add(\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-storage-access-by-user-activation\",\n \"allow-downloads\",\n \"allow-forms\",\n \"allow-popups\",\n \"allow-top-navigation-by-user-activation\",\n );\n iframe.src = hostUrl.href;\n iframe.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(iframe.style, {\n background: \"transparent\",\n border: \"0\",\n display: \"none\",\n // Keep the overlay within the visible viewport when browser controls move.\n height: CSS.supports(\"height\", \"100svh\") ? \"100svh\" : \"100vh\",\n inset: \"0\",\n position: \"fixed\",\n width: \"100%\",\n zIndex: \"2147483647\",\n });\n let connected = false;\n let connectionErrorShown = false;\n iframe.addEventListener(\"load\", () => {\n // Bootstrap the guard with the browser-reported embedding origin, which\n // the RPC handshake verifies internally but does not expose to the host.\n iframe.contentWindow?.postMessage(\n {\n type: \"graffiti-guard:connect\",\n pageUrl: window.location.href,\n ...(loginNonce ? { loginNonce } : {}),\n },\n hostUrl.origin,\n );\n });\n const showConnectionError = () => {\n if (connected || connectionErrorShown) return;\n connectionErrorShown = true;\n window.alert(\n `This app could not connect to its data service at ${hostUrl.origin}. ` +\n \"The service may be offline, or your browser may be blocking its security certificate. \" +\n \"Open that address directly, then reload this page.\",\n );\n };\n iframe.addEventListener(\"error\", showConnectionError);\n document.body.append(iframe);\n\n const remoteWindow = iframe.contentWindow;\n if (!remoteWindow) {\n iframe.remove();\n throw new Error(\"Graffiti Guard iframe did not create a content window.\");\n }\n\n const rpc = new GraffitiRpcClient({\n remoteWindow,\n allowedOrigins: [hostUrl.origin],\n channel: options.channel,\n timeout: options.timeout,\n });\n this.sessionEvents = rpc.sessionEvents;\n this.sessionEvents.addEventListener(\"initialized\", () => {\n if (sessionStorage.getItem(loginKey) === loginNonce) sessionStorage.removeItem(loginKey);\n }, { once: true });\n const connectionTimeout = window.setTimeout(\n showConnectionError,\n options.timeout ?? defaultConnectionTimeout,\n );\n const markConnected = () => {\n connected = true;\n window.clearTimeout(connectionTimeout);\n };\n const onMessage = (event: MessageEvent) => {\n if (\n event.source !== remoteWindow ||\n event.origin !== hostUrl.origin ||\n !event.data ||\n typeof event.data !== \"object\"\n ) {\n return;\n }\n markConnected();\n if (\n event.data.type === \"graffiti-guard:connected\"\n ) {\n return;\n } else if (\n event.data.type === \"graffiti-guard:set-visible\" &&\n typeof event.data.visible === \"boolean\"\n ) {\n this.setGuardVisible(event.data.visible);\n if (event.data.visible) {\n remoteWindow.postMessage(\n { type: \"graffiti-guard:shown\" },\n hostUrl.origin,\n );\n }\n } else if (\n event.data.type === \"graffiti-guard:open-storage-setup\" &&\n typeof event.data.url === \"string\"\n ) {\n try {\n const setupUrl = new URL(event.data.url);\n const setupParams = new URLSearchParams(setupUrl.hash.slice(1));\n if (\n setupUrl.origin !== hostUrl.origin ||\n (setupParams.get(\"guardStorageSetup\") !== \"1\" &&\n setupUrl.searchParams.get(\"guardStorageSetup\") !== \"1\")\n ) {\n return;\n }\n window.location.assign(setupUrl);\n } catch {}\n } else if (\n event.data.type === \"graffiti-guard:open-audit\" &&\n typeof event.data.actor === \"string\" &&\n Array.isArray(event.data.source)\n ) {\n window.location.assign(this.auditUrl(event.data));\n }\n };\n window.addEventListener(\"message\", onMessage);\n\n this.hostUrl = hostUrl;\n this.iframe = iframe;\n this.rpc = rpc;\n this.onMessage = onMessage;\n this.connectionTimeout = connectionTimeout;\n\n instance = new Proxy(this, {\n get(target, property, receiver) {\n if (Reflect.has(target, property)) {\n return Reflect.get(target, property, receiver);\n }\n const value = Reflect.get(rpc, property, rpc);\n return typeof value === \"function\" ? value.bind(rpc) : value;\n },\n });\n return instance;\n }\n\n login: Graffiti[\"login\"] = (options) => {\n const loginUrl = new URL(this.hostUrl);\n // The guard reads this state in the browser; keep it out of the HTTP request.\n // This allows for very long redirectUrls\n const params = new URLSearchParams({\n guardLogin: \"1\",\n redirectUrl: window.location.href,\n });\n if (options?.actor) params.set(\"suggestedActor\", options.actor);\n const nonce = crypto.randomUUID();\n sessionStorage.setItem(pendingLoginKey + this.hostUrl.origin, nonce);\n params.set(\"loginNonce\", nonce);\n if (options?.state !== undefined) params.set(\"state\", options.state);\n loginUrl.hash = params.toString();\n window.location.assign(loginUrl.href);\n return Promise.resolve();\n };\n\n /** Link to the host-owned audit panel without exposing audit data to this app. */\n auditUrl(scope?: {\n actor?: string;\n source?: GraffitiGuardSourceSegment[];\n }) {\n const auditUrl = new URL(this.hostUrl);\n const params = new URLSearchParams({\n redirectUrl: window.location.href,\n source: JSON.stringify(scope?.source ?? []),\n });\n if (scope?.actor) params.set(\"actor\", scope.actor);\n auditUrl.hash = params.toString();\n return auditUrl.href;\n }\n\n private setGuardVisible(visible: boolean) {\n if (visible) {\n if (this.iframe.style.display === \"block\") return;\n const focused = document.activeElement;\n this.returnFocus =\n focused instanceof HTMLElement && focused !== this.iframe\n ? focused\n : null;\n this.inertBackground();\n this.backgroundObserver = new MutationObserver(() => this.inertBackground());\n this.backgroundObserver.observe(document.body, { childList: true });\n this.iframe.style.display = \"block\";\n this.iframe.setAttribute(\"aria-hidden\", \"false\");\n this.iframe.focus();\n return;\n }\n\n if (this.iframe.style.display === \"none\") return;\n\n this.iframe.style.display = \"none\";\n this.iframe.setAttribute(\"aria-hidden\", \"true\");\n this.backgroundObserver?.disconnect();\n this.backgroundObserver = undefined;\n for (const [element, wasInert] of this.backgroundInert) {\n element.inert = wasInert;\n }\n this.backgroundInert.clear();\n const target = this.returnFocus;\n this.returnFocus = null;\n if (target?.isConnected && !target.inert && target !== document.body) {\n target.focus({ preventScroll: true });\n } else {\n const previousTabindex = document.body.getAttribute(\"tabindex\");\n document.body.tabIndex = -1;\n document.body.focus({ preventScroll: true });\n if (previousTabindex === null) document.body.removeAttribute(\"tabindex\");\n else document.body.setAttribute(\"tabindex\", previousTabindex);\n }\n }\n\n private inertBackground() {\n for (const child of Array.from(document.body.children)) {\n if (child === this.iframe || !(child instanceof HTMLElement)) continue;\n if (!this.backgroundInert.has(child)) {\n this.backgroundInert.set(child, child.inert);\n }\n child.inert = true;\n }\n }\n\n destroy() {\n if (this.destroyed) return;\n this.destroyed = true;\n window.clearTimeout(this.connectionTimeout);\n window.removeEventListener(\"message\", this.onMessage);\n if (this.iframe.style.display === \"block\") this.setGuardVisible(false);\n this.rpc.destroy();\n this.iframe.remove();\n if (instance === this) instance = undefined;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/esm/index.js CHANGED
@@ -1,7 +1,10 @@
1
- import { Graffiti } from "@graffiti-garden/api";
1
+ import {
2
+ Graffiti
3
+ } from "@graffiti-garden/api";
2
4
  import { GraffitiRpcClient } from "@graffiti-garden/wrapper-iframe-rpc/client";
3
5
  let instance;
4
6
  const defaultConnectionTimeout = 1e4;
7
+ const pendingLoginKey = "graffiti-guard-login:";
5
8
  class GraffitiGuarded extends Graffiti {
6
9
  sessionEvents;
7
10
  hostUrl;
@@ -10,6 +13,9 @@ class GraffitiGuarded extends Graffiti {
10
13
  onMessage;
11
14
  connectionTimeout;
12
15
  destroyed = false;
16
+ returnFocus = null;
17
+ backgroundInert = /* @__PURE__ */ new Map();
18
+ backgroundObserver;
13
19
  constructor(options = {}) {
14
20
  super();
15
21
  if (instance) return instance;
@@ -17,6 +23,8 @@ class GraffitiGuarded extends Graffiti {
17
23
  options.hostUrl?.toString() ?? "https://guard.graffiti.garden/",
18
24
  document.baseURI
19
25
  );
26
+ const loginKey = pendingLoginKey + hostUrl.origin;
27
+ const loginNonce = sessionStorage.getItem(loginKey);
20
28
  const iframe = document.createElement("iframe");
21
29
  iframe.title = "Graffiti Guard";
22
30
  iframe.allow = "storage-access";
@@ -35,7 +43,7 @@ class GraffitiGuarded extends Graffiti {
35
43
  background: "transparent",
36
44
  border: "0",
37
45
  display: "none",
38
- // Prompts scroll inside this frame, so size it for visible browser controls.
46
+ // Keep the overlay within the visible viewport when browser controls move.
39
47
  height: CSS.supports("height", "100svh") ? "100svh" : "100vh",
40
48
  inset: "0",
41
49
  position: "fixed",
@@ -48,7 +56,8 @@ class GraffitiGuarded extends Graffiti {
48
56
  iframe.contentWindow?.postMessage(
49
57
  {
50
58
  type: "graffiti-guard:connect",
51
- pageUrl: window.location.href
59
+ pageUrl: window.location.href,
60
+ ...loginNonce ? { loginNonce } : {}
52
61
  },
53
62
  hostUrl.origin
54
63
  );
@@ -73,6 +82,10 @@ class GraffitiGuarded extends Graffiti {
73
82
  channel: options.channel,
74
83
  timeout: options.timeout
75
84
  });
85
+ this.sessionEvents = rpc.sessionEvents;
86
+ this.sessionEvents.addEventListener("initialized", () => {
87
+ if (sessionStorage.getItem(loginKey) === loginNonce) sessionStorage.removeItem(loginKey);
88
+ }, { once: true });
76
89
  const connectionTimeout = window.setTimeout(
77
90
  showConnectionError,
78
91
  options.timeout ?? defaultConnectionTimeout
@@ -89,10 +102,8 @@ class GraffitiGuarded extends Graffiti {
89
102
  if (event.data.type === "graffiti-guard:connected") {
90
103
  return;
91
104
  } else if (event.data.type === "graffiti-guard:set-visible" && typeof event.data.visible === "boolean") {
92
- iframe.style.display = event.data.visible ? "block" : "none";
93
- iframe.setAttribute("aria-hidden", String(!event.data.visible));
105
+ this.setGuardVisible(event.data.visible);
94
106
  if (event.data.visible) {
95
- iframe.focus();
96
107
  remoteWindow.postMessage(
97
108
  { type: "graffiti-guard:shown" },
98
109
  hostUrl.origin
@@ -116,7 +127,6 @@ class GraffitiGuarded extends Graffiti {
116
127
  this.hostUrl = hostUrl;
117
128
  this.iframe = iframe;
118
129
  this.rpc = rpc;
119
- this.sessionEvents = rpc.sessionEvents;
120
130
  this.onMessage = onMessage;
121
131
  this.connectionTimeout = connectionTimeout;
122
132
  instance = new Proxy(this, {
@@ -130,13 +140,17 @@ class GraffitiGuarded extends Graffiti {
130
140
  });
131
141
  return instance;
132
142
  }
133
- login = (actor) => {
143
+ login = (options) => {
134
144
  const loginUrl = new URL(this.hostUrl);
135
145
  const params = new URLSearchParams({
136
146
  guardLogin: "1",
137
147
  redirectUrl: window.location.href
138
148
  });
139
- if (actor) params.set("suggestedActor", actor);
149
+ if (options?.actor) params.set("suggestedActor", options.actor);
150
+ const nonce = crypto.randomUUID();
151
+ sessionStorage.setItem(pendingLoginKey + this.hostUrl.origin, nonce);
152
+ params.set("loginNonce", nonce);
153
+ if (options?.state !== void 0) params.set("state", options.state);
140
154
  loginUrl.hash = params.toString();
141
155
  window.location.assign(loginUrl.href);
142
156
  return Promise.resolve();
@@ -152,11 +166,55 @@ class GraffitiGuarded extends Graffiti {
152
166
  auditUrl.hash = params.toString();
153
167
  return auditUrl.href;
154
168
  }
169
+ setGuardVisible(visible) {
170
+ if (visible) {
171
+ if (this.iframe.style.display === "block") return;
172
+ const focused = document.activeElement;
173
+ this.returnFocus = focused instanceof HTMLElement && focused !== this.iframe ? focused : null;
174
+ this.inertBackground();
175
+ this.backgroundObserver = new MutationObserver(() => this.inertBackground());
176
+ this.backgroundObserver.observe(document.body, { childList: true });
177
+ this.iframe.style.display = "block";
178
+ this.iframe.setAttribute("aria-hidden", "false");
179
+ this.iframe.focus();
180
+ return;
181
+ }
182
+ if (this.iframe.style.display === "none") return;
183
+ this.iframe.style.display = "none";
184
+ this.iframe.setAttribute("aria-hidden", "true");
185
+ this.backgroundObserver?.disconnect();
186
+ this.backgroundObserver = void 0;
187
+ for (const [element, wasInert] of this.backgroundInert) {
188
+ element.inert = wasInert;
189
+ }
190
+ this.backgroundInert.clear();
191
+ const target = this.returnFocus;
192
+ this.returnFocus = null;
193
+ if (target?.isConnected && !target.inert && target !== document.body) {
194
+ target.focus({ preventScroll: true });
195
+ } else {
196
+ const previousTabindex = document.body.getAttribute("tabindex");
197
+ document.body.tabIndex = -1;
198
+ document.body.focus({ preventScroll: true });
199
+ if (previousTabindex === null) document.body.removeAttribute("tabindex");
200
+ else document.body.setAttribute("tabindex", previousTabindex);
201
+ }
202
+ }
203
+ inertBackground() {
204
+ for (const child of Array.from(document.body.children)) {
205
+ if (child === this.iframe || !(child instanceof HTMLElement)) continue;
206
+ if (!this.backgroundInert.has(child)) {
207
+ this.backgroundInert.set(child, child.inert);
208
+ }
209
+ child.inert = true;
210
+ }
211
+ }
155
212
  destroy() {
156
213
  if (this.destroyed) return;
157
214
  this.destroyed = true;
158
215
  window.clearTimeout(this.connectionTimeout);
159
216
  window.removeEventListener("message", this.onMessage);
217
+ if (this.iframe.style.display === "block") this.setGuardVisible(false);
160
218
  this.rpc.destroy();
161
219
  this.iframe.remove();
162
220
  if (instance === this) instance = void 0;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import { Graffiti, type GraffitiSession } from \"@graffiti-garden/api\";\nimport { GraffitiRpcClient } from \"@graffiti-garden/wrapper-iframe-rpc/client\";\n\ndeclare const DATA_GUARD_DEFAULT_HOST_URL: string;\n\nlet instance: GraffitiGuarded | undefined;\nconst defaultConnectionTimeout = 10_000;\n\nexport interface GraffitiGuardedOptions {\n hostUrl?: string | URL;\n channel?: string;\n timeout?: number;\n}\n\nexport interface GraffitiGuardSourceSegment {\n id: string;\n name: string;\n}\n\nexport type GraffitiGuardSession = GraffitiSession & {\n source?: GraffitiGuardSourceSegment[];\n};\n\n/** A Graffiti implementation backed by a sandboxed guard iframe. */\n// @ts-ignore Graffiti methods are delegated through the constructor proxy.\nexport class GraffitiGuarded extends Graffiti {\n readonly sessionEvents!: EventTarget;\n private readonly hostUrl!: URL;\n private readonly iframe!: HTMLIFrameElement;\n private readonly rpc!: GraffitiRpcClient;\n private readonly onMessage!: (event: MessageEvent) => void;\n private readonly connectionTimeout!: number;\n private destroyed = false;\n\n constructor(options: GraffitiGuardedOptions = {}) {\n super();\n if (instance) return instance;\n\n const hostUrl = new URL(\n options.hostUrl?.toString() ?? DATA_GUARD_DEFAULT_HOST_URL,\n document.baseURI,\n );\n const iframe = document.createElement(\"iframe\");\n iframe.title = \"Graffiti Guard\";\n iframe.allow = \"storage-access\";\n iframe.sandbox.add(\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-storage-access-by-user-activation\",\n \"allow-downloads\",\n \"allow-forms\",\n \"allow-popups\",\n \"allow-top-navigation-by-user-activation\",\n );\n iframe.src = hostUrl.href;\n iframe.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(iframe.style, {\n background: \"transparent\",\n border: \"0\",\n display: \"none\",\n // Prompts scroll inside this frame, so size it for visible browser controls.\n height: CSS.supports(\"height\", \"100svh\") ? \"100svh\" : \"100vh\",\n inset: \"0\",\n position: \"fixed\",\n width: \"100%\",\n zIndex: \"2147483647\",\n });\n let connected = false;\n let connectionErrorShown = false;\n iframe.addEventListener(\"load\", () => {\n // Bootstrap the guard with the browser-reported embedding origin, which\n // the RPC handshake verifies internally but does not expose to the host.\n iframe.contentWindow?.postMessage(\n {\n type: \"graffiti-guard:connect\",\n pageUrl: window.location.href,\n },\n hostUrl.origin,\n );\n });\n const showConnectionError = () => {\n if (connected || connectionErrorShown) return;\n connectionErrorShown = true;\n window.alert(\n `This app could not connect to its data service at ${hostUrl.origin}. ` +\n \"The service may be offline, or your browser may be blocking its security certificate. \" +\n \"Open that address directly, then reload this page.\",\n );\n };\n iframe.addEventListener(\"error\", showConnectionError);\n document.body.append(iframe);\n\n const remoteWindow = iframe.contentWindow;\n if (!remoteWindow) {\n iframe.remove();\n throw new Error(\"Graffiti Guard iframe did not create a content window.\");\n }\n\n const rpc = new GraffitiRpcClient({\n remoteWindow,\n allowedOrigins: [hostUrl.origin],\n channel: options.channel,\n timeout: options.timeout,\n });\n const connectionTimeout = window.setTimeout(\n showConnectionError,\n options.timeout ?? defaultConnectionTimeout,\n );\n const markConnected = () => {\n connected = true;\n window.clearTimeout(connectionTimeout);\n };\n const onMessage = (event: MessageEvent) => {\n if (\n event.source !== remoteWindow ||\n event.origin !== hostUrl.origin ||\n !event.data ||\n typeof event.data !== \"object\"\n ) {\n return;\n }\n markConnected();\n if (\n event.data.type === \"graffiti-guard:connected\"\n ) {\n return;\n } else if (\n event.data.type === \"graffiti-guard:set-visible\" &&\n typeof event.data.visible === \"boolean\"\n ) {\n iframe.style.display = event.data.visible ? \"block\" : \"none\";\n iframe.setAttribute(\"aria-hidden\", String(!event.data.visible));\n if (event.data.visible) {\n iframe.focus();\n remoteWindow.postMessage(\n { type: \"graffiti-guard:shown\" },\n hostUrl.origin,\n );\n }\n } else if (\n event.data.type === \"graffiti-guard:open-storage-setup\" &&\n typeof event.data.url === \"string\"\n ) {\n try {\n const setupUrl = new URL(event.data.url);\n const setupParams = new URLSearchParams(setupUrl.hash.slice(1));\n if (\n setupUrl.origin !== hostUrl.origin ||\n (setupParams.get(\"guardStorageSetup\") !== \"1\" &&\n setupUrl.searchParams.get(\"guardStorageSetup\") !== \"1\")\n ) {\n return;\n }\n window.location.assign(setupUrl);\n } catch {}\n } else if (\n event.data.type === \"graffiti-guard:open-audit\" &&\n typeof event.data.actor === \"string\" &&\n Array.isArray(event.data.source)\n ) {\n window.location.assign(this.auditUrl(event.data));\n }\n };\n window.addEventListener(\"message\", onMessage);\n\n this.hostUrl = hostUrl;\n this.iframe = iframe;\n this.rpc = rpc;\n this.sessionEvents = rpc.sessionEvents;\n this.onMessage = onMessage;\n this.connectionTimeout = connectionTimeout;\n\n instance = new Proxy(this, {\n get(target, property, receiver) {\n if (Reflect.has(target, property)) {\n return Reflect.get(target, property, receiver);\n }\n const value = Reflect.get(rpc, property, rpc);\n return typeof value === \"function\" ? value.bind(rpc) : value;\n },\n });\n return instance;\n }\n\n login: Graffiti[\"login\"] = (actor) => {\n const loginUrl = new URL(this.hostUrl);\n // The guard reads this state in the browser; keep it out of the HTTP request.\n // This allows for very long redirectUrls\n const params = new URLSearchParams({\n guardLogin: \"1\",\n redirectUrl: window.location.href,\n });\n if (actor) params.set(\"suggestedActor\", actor);\n loginUrl.hash = params.toString();\n window.location.assign(loginUrl.href);\n return Promise.resolve();\n };\n\n /** Link to the host-owned audit panel without exposing audit data to this app. */\n auditUrl(scope?: {\n actor?: string;\n source?: GraffitiGuardSourceSegment[];\n }) {\n const auditUrl = new URL(this.hostUrl);\n const params = new URLSearchParams({\n redirectUrl: window.location.href,\n source: JSON.stringify(scope?.source ?? []),\n });\n if (scope?.actor) params.set(\"actor\", scope.actor);\n auditUrl.hash = params.toString();\n return auditUrl.href;\n }\n\n destroy() {\n if (this.destroyed) return;\n this.destroyed = true;\n window.clearTimeout(this.connectionTimeout);\n window.removeEventListener(\"message\", this.onMessage);\n this.rpc.destroy();\n this.iframe.remove();\n if (instance === this) instance = undefined;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n Graffiti,\n type GraffitiSession,\n} from \"@graffiti-garden/api\";\nimport { GraffitiRpcClient } from \"@graffiti-garden/wrapper-iframe-rpc/client\";\n\ndeclare const DATA_GUARD_DEFAULT_HOST_URL: string;\n\nlet instance: GraffitiGuarded | undefined;\nconst defaultConnectionTimeout = 10_000;\nconst pendingLoginKey = \"graffiti-guard-login:\";\n\nexport interface GraffitiGuardedOptions {\n hostUrl?: string | URL;\n channel?: string;\n timeout?: number;\n}\n\nexport interface GraffitiGuardSourceSegment {\n id: string;\n name: string;\n}\n\nexport type GraffitiGuardSession = GraffitiSession & {\n source?: GraffitiGuardSourceSegment[];\n};\n\n/** A Graffiti implementation backed by a sandboxed guard iframe. */\n// @ts-ignore Graffiti methods are delegated through the constructor proxy.\nexport class GraffitiGuarded extends Graffiti {\n readonly sessionEvents!: Graffiti[\"sessionEvents\"];\n private readonly hostUrl!: URL;\n private readonly iframe!: HTMLIFrameElement;\n private readonly rpc!: GraffitiRpcClient;\n private readonly onMessage!: (event: MessageEvent) => void;\n private readonly connectionTimeout!: number;\n private destroyed = false;\n private returnFocus: HTMLElement | null = null;\n private readonly backgroundInert = new Map<HTMLElement, boolean>();\n private backgroundObserver: MutationObserver | undefined;\n\n constructor(options: GraffitiGuardedOptions = {}) {\n super();\n if (instance) return instance;\n\n const hostUrl = new URL(\n options.hostUrl?.toString() ?? DATA_GUARD_DEFAULT_HOST_URL,\n document.baseURI,\n );\n const loginKey = pendingLoginKey + hostUrl.origin;\n const loginNonce = sessionStorage.getItem(loginKey);\n const iframe = document.createElement(\"iframe\");\n iframe.title = \"Graffiti Guard\";\n iframe.allow = \"storage-access\";\n iframe.sandbox.add(\n \"allow-scripts\",\n \"allow-same-origin\",\n \"allow-storage-access-by-user-activation\",\n \"allow-downloads\",\n \"allow-forms\",\n \"allow-popups\",\n \"allow-top-navigation-by-user-activation\",\n );\n iframe.src = hostUrl.href;\n iframe.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(iframe.style, {\n background: \"transparent\",\n border: \"0\",\n display: \"none\",\n // Keep the overlay within the visible viewport when browser controls move.\n height: CSS.supports(\"height\", \"100svh\") ? \"100svh\" : \"100vh\",\n inset: \"0\",\n position: \"fixed\",\n width: \"100%\",\n zIndex: \"2147483647\",\n });\n let connected = false;\n let connectionErrorShown = false;\n iframe.addEventListener(\"load\", () => {\n // Bootstrap the guard with the browser-reported embedding origin, which\n // the RPC handshake verifies internally but does not expose to the host.\n iframe.contentWindow?.postMessage(\n {\n type: \"graffiti-guard:connect\",\n pageUrl: window.location.href,\n ...(loginNonce ? { loginNonce } : {}),\n },\n hostUrl.origin,\n );\n });\n const showConnectionError = () => {\n if (connected || connectionErrorShown) return;\n connectionErrorShown = true;\n window.alert(\n `This app could not connect to its data service at ${hostUrl.origin}. ` +\n \"The service may be offline, or your browser may be blocking its security certificate. \" +\n \"Open that address directly, then reload this page.\",\n );\n };\n iframe.addEventListener(\"error\", showConnectionError);\n document.body.append(iframe);\n\n const remoteWindow = iframe.contentWindow;\n if (!remoteWindow) {\n iframe.remove();\n throw new Error(\"Graffiti Guard iframe did not create a content window.\");\n }\n\n const rpc = new GraffitiRpcClient({\n remoteWindow,\n allowedOrigins: [hostUrl.origin],\n channel: options.channel,\n timeout: options.timeout,\n });\n this.sessionEvents = rpc.sessionEvents;\n this.sessionEvents.addEventListener(\"initialized\", () => {\n if (sessionStorage.getItem(loginKey) === loginNonce) sessionStorage.removeItem(loginKey);\n }, { once: true });\n const connectionTimeout = window.setTimeout(\n showConnectionError,\n options.timeout ?? defaultConnectionTimeout,\n );\n const markConnected = () => {\n connected = true;\n window.clearTimeout(connectionTimeout);\n };\n const onMessage = (event: MessageEvent) => {\n if (\n event.source !== remoteWindow ||\n event.origin !== hostUrl.origin ||\n !event.data ||\n typeof event.data !== \"object\"\n ) {\n return;\n }\n markConnected();\n if (\n event.data.type === \"graffiti-guard:connected\"\n ) {\n return;\n } else if (\n event.data.type === \"graffiti-guard:set-visible\" &&\n typeof event.data.visible === \"boolean\"\n ) {\n this.setGuardVisible(event.data.visible);\n if (event.data.visible) {\n remoteWindow.postMessage(\n { type: \"graffiti-guard:shown\" },\n hostUrl.origin,\n );\n }\n } else if (\n event.data.type === \"graffiti-guard:open-storage-setup\" &&\n typeof event.data.url === \"string\"\n ) {\n try {\n const setupUrl = new URL(event.data.url);\n const setupParams = new URLSearchParams(setupUrl.hash.slice(1));\n if (\n setupUrl.origin !== hostUrl.origin ||\n (setupParams.get(\"guardStorageSetup\") !== \"1\" &&\n setupUrl.searchParams.get(\"guardStorageSetup\") !== \"1\")\n ) {\n return;\n }\n window.location.assign(setupUrl);\n } catch {}\n } else if (\n event.data.type === \"graffiti-guard:open-audit\" &&\n typeof event.data.actor === \"string\" &&\n Array.isArray(event.data.source)\n ) {\n window.location.assign(this.auditUrl(event.data));\n }\n };\n window.addEventListener(\"message\", onMessage);\n\n this.hostUrl = hostUrl;\n this.iframe = iframe;\n this.rpc = rpc;\n this.onMessage = onMessage;\n this.connectionTimeout = connectionTimeout;\n\n instance = new Proxy(this, {\n get(target, property, receiver) {\n if (Reflect.has(target, property)) {\n return Reflect.get(target, property, receiver);\n }\n const value = Reflect.get(rpc, property, rpc);\n return typeof value === \"function\" ? value.bind(rpc) : value;\n },\n });\n return instance;\n }\n\n login: Graffiti[\"login\"] = (options) => {\n const loginUrl = new URL(this.hostUrl);\n // The guard reads this state in the browser; keep it out of the HTTP request.\n // This allows for very long redirectUrls\n const params = new URLSearchParams({\n guardLogin: \"1\",\n redirectUrl: window.location.href,\n });\n if (options?.actor) params.set(\"suggestedActor\", options.actor);\n const nonce = crypto.randomUUID();\n sessionStorage.setItem(pendingLoginKey + this.hostUrl.origin, nonce);\n params.set(\"loginNonce\", nonce);\n if (options?.state !== undefined) params.set(\"state\", options.state);\n loginUrl.hash = params.toString();\n window.location.assign(loginUrl.href);\n return Promise.resolve();\n };\n\n /** Link to the host-owned audit panel without exposing audit data to this app. */\n auditUrl(scope?: {\n actor?: string;\n source?: GraffitiGuardSourceSegment[];\n }) {\n const auditUrl = new URL(this.hostUrl);\n const params = new URLSearchParams({\n redirectUrl: window.location.href,\n source: JSON.stringify(scope?.source ?? []),\n });\n if (scope?.actor) params.set(\"actor\", scope.actor);\n auditUrl.hash = params.toString();\n return auditUrl.href;\n }\n\n private setGuardVisible(visible: boolean) {\n if (visible) {\n if (this.iframe.style.display === \"block\") return;\n const focused = document.activeElement;\n this.returnFocus =\n focused instanceof HTMLElement && focused !== this.iframe\n ? focused\n : null;\n this.inertBackground();\n this.backgroundObserver = new MutationObserver(() => this.inertBackground());\n this.backgroundObserver.observe(document.body, { childList: true });\n this.iframe.style.display = \"block\";\n this.iframe.setAttribute(\"aria-hidden\", \"false\");\n this.iframe.focus();\n return;\n }\n\n if (this.iframe.style.display === \"none\") return;\n\n this.iframe.style.display = \"none\";\n this.iframe.setAttribute(\"aria-hidden\", \"true\");\n this.backgroundObserver?.disconnect();\n this.backgroundObserver = undefined;\n for (const [element, wasInert] of this.backgroundInert) {\n element.inert = wasInert;\n }\n this.backgroundInert.clear();\n const target = this.returnFocus;\n this.returnFocus = null;\n if (target?.isConnected && !target.inert && target !== document.body) {\n target.focus({ preventScroll: true });\n } else {\n const previousTabindex = document.body.getAttribute(\"tabindex\");\n document.body.tabIndex = -1;\n document.body.focus({ preventScroll: true });\n if (previousTabindex === null) document.body.removeAttribute(\"tabindex\");\n else document.body.setAttribute(\"tabindex\", previousTabindex);\n }\n }\n\n private inertBackground() {\n for (const child of Array.from(document.body.children)) {\n if (child === this.iframe || !(child instanceof HTMLElement)) continue;\n if (!this.backgroundInert.has(child)) {\n this.backgroundInert.set(child, child.inert);\n }\n child.inert = true;\n }\n }\n\n destroy() {\n if (this.destroyed) return;\n this.destroyed = true;\n window.clearTimeout(this.connectionTimeout);\n window.removeEventListener(\"message\", this.onMessage);\n if (this.iframe.style.display === \"block\") this.setGuardVisible(false);\n this.rpc.destroy();\n this.iframe.remove();\n if (instance === this) instance = undefined;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/index.d.ts CHANGED
@@ -13,13 +13,16 @@ export type GraffitiGuardSession = GraffitiSession & {
13
13
  };
14
14
  /** A Graffiti implementation backed by a sandboxed guard iframe. */
15
15
  export declare class GraffitiGuarded extends Graffiti {
16
- readonly sessionEvents: EventTarget;
16
+ readonly sessionEvents: Graffiti["sessionEvents"];
17
17
  private readonly hostUrl;
18
18
  private readonly iframe;
19
19
  private readonly rpc;
20
20
  private readonly onMessage;
21
21
  private readonly connectionTimeout;
22
22
  private destroyed;
23
+ private returnFocus;
24
+ private readonly backgroundInert;
25
+ private backgroundObserver;
23
26
  constructor(options?: GraffitiGuardedOptions);
24
27
  login: Graffiti["login"];
25
28
  /** Link to the host-owned audit panel without exposing audit data to this app. */
@@ -27,6 +30,8 @@ export declare class GraffitiGuarded extends Graffiti {
27
30
  actor?: string;
28
31
  source?: GraffitiGuardSourceSegment[];
29
32
  }): string;
33
+ private setGuardVisible;
34
+ private inertBackground;
30
35
  destroy(): void;
31
36
  }
32
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQtE,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,oBAAoB,GAAG,eAAe,GAAG;IACnD,MAAM,CAAC,EAAE,0BAA0B,EAAE,CAAC;CACvC,CAAC;AAEF,oEAAoE;AAEpE,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,QAAQ,CAAC,aAAa,EAAG,WAAW,CAAC;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAO;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,OAAO,GAAE,sBAA2B,EAoJ/C;IAED,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAYtB;IAEF,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,0BAA0B,EAAE,CAAC;KACvC,UASA;IAED,OAAO,SAQN;CACF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAS9B,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,oBAAoB,GAAG,eAAe,GAAG;IACnD,MAAM,CAAC,EAAE,0BAA0B,EAAE,CAAC;CACvC,CAAC;AAEF,oEAAoE;AAEpE,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,QAAQ,CAAC,aAAa,EAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAO;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmC;IACnE,OAAO,CAAC,kBAAkB,CAA+B;IAEzD,YAAY,OAAO,GAAE,sBAA2B,EAwJ/C;IAED,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAgBtB;IAEF,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,0BAA0B,EAAE,CAAC;KACvC,UASA;IAED,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,eAAe;IAUvB,OAAO,SASN;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/wrapper-data-guard",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Ask for permission before a site accesses Graffiti",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -43,8 +43,8 @@
43
43
  "url": "https://github.com/graffiti-garden/graffiti/issues"
44
44
  },
45
45
  "dependencies": {
46
- "@graffiti-garden/api": "^1.2.0",
47
- "@graffiti-garden/wrapper-iframe-rpc": "^0.1.0"
46
+ "@graffiti-garden/api": "^1.4.0",
47
+ "@graffiti-garden/wrapper-iframe-rpc": "^0.2.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^26.6.2",