@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/browser/index.js +1 -1
- package/dist/browser/index.js.map +3 -3
- package/dist/cjs/index.js +64 -8
- package/dist/cjs/index.js.map +2 -2
- package/dist/esm/index.js +67 -9
- package/dist/esm/index.js.map +2 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +75 -9
package/src/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Graffiti,
|
|
3
|
+
type GraffitiSession,
|
|
4
|
+
} from "@graffiti-garden/api";
|
|
2
5
|
import { GraffitiRpcClient } from "@graffiti-garden/wrapper-iframe-rpc/client";
|
|
3
6
|
|
|
4
7
|
declare const DATA_GUARD_DEFAULT_HOST_URL: string;
|
|
5
8
|
|
|
6
9
|
let instance: GraffitiGuarded | undefined;
|
|
7
10
|
const defaultConnectionTimeout = 10_000;
|
|
11
|
+
const pendingLoginKey = "graffiti-guard-login:";
|
|
8
12
|
|
|
9
13
|
export interface GraffitiGuardedOptions {
|
|
10
14
|
hostUrl?: string | URL;
|
|
@@ -24,13 +28,16 @@ export type GraffitiGuardSession = GraffitiSession & {
|
|
|
24
28
|
/** A Graffiti implementation backed by a sandboxed guard iframe. */
|
|
25
29
|
// @ts-ignore Graffiti methods are delegated through the constructor proxy.
|
|
26
30
|
export class GraffitiGuarded extends Graffiti {
|
|
27
|
-
readonly sessionEvents!:
|
|
31
|
+
readonly sessionEvents!: Graffiti["sessionEvents"];
|
|
28
32
|
private readonly hostUrl!: URL;
|
|
29
33
|
private readonly iframe!: HTMLIFrameElement;
|
|
30
34
|
private readonly rpc!: GraffitiRpcClient;
|
|
31
35
|
private readonly onMessage!: (event: MessageEvent) => void;
|
|
32
36
|
private readonly connectionTimeout!: number;
|
|
33
37
|
private destroyed = false;
|
|
38
|
+
private returnFocus: HTMLElement | null = null;
|
|
39
|
+
private readonly backgroundInert = new Map<HTMLElement, boolean>();
|
|
40
|
+
private backgroundObserver: MutationObserver | undefined;
|
|
34
41
|
|
|
35
42
|
constructor(options: GraffitiGuardedOptions = {}) {
|
|
36
43
|
super();
|
|
@@ -40,6 +47,8 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
40
47
|
options.hostUrl?.toString() ?? DATA_GUARD_DEFAULT_HOST_URL,
|
|
41
48
|
document.baseURI,
|
|
42
49
|
);
|
|
50
|
+
const loginKey = pendingLoginKey + hostUrl.origin;
|
|
51
|
+
const loginNonce = sessionStorage.getItem(loginKey);
|
|
43
52
|
const iframe = document.createElement("iframe");
|
|
44
53
|
iframe.title = "Graffiti Guard";
|
|
45
54
|
iframe.allow = "storage-access";
|
|
@@ -58,7 +67,7 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
58
67
|
background: "transparent",
|
|
59
68
|
border: "0",
|
|
60
69
|
display: "none",
|
|
61
|
-
//
|
|
70
|
+
// Keep the overlay within the visible viewport when browser controls move.
|
|
62
71
|
height: CSS.supports("height", "100svh") ? "100svh" : "100vh",
|
|
63
72
|
inset: "0",
|
|
64
73
|
position: "fixed",
|
|
@@ -74,6 +83,7 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
74
83
|
{
|
|
75
84
|
type: "graffiti-guard:connect",
|
|
76
85
|
pageUrl: window.location.href,
|
|
86
|
+
...(loginNonce ? { loginNonce } : {}),
|
|
77
87
|
},
|
|
78
88
|
hostUrl.origin,
|
|
79
89
|
);
|
|
@@ -102,6 +112,10 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
102
112
|
channel: options.channel,
|
|
103
113
|
timeout: options.timeout,
|
|
104
114
|
});
|
|
115
|
+
this.sessionEvents = rpc.sessionEvents;
|
|
116
|
+
this.sessionEvents.addEventListener("initialized", () => {
|
|
117
|
+
if (sessionStorage.getItem(loginKey) === loginNonce) sessionStorage.removeItem(loginKey);
|
|
118
|
+
}, { once: true });
|
|
105
119
|
const connectionTimeout = window.setTimeout(
|
|
106
120
|
showConnectionError,
|
|
107
121
|
options.timeout ?? defaultConnectionTimeout,
|
|
@@ -128,10 +142,8 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
128
142
|
event.data.type === "graffiti-guard:set-visible" &&
|
|
129
143
|
typeof event.data.visible === "boolean"
|
|
130
144
|
) {
|
|
131
|
-
|
|
132
|
-
iframe.setAttribute("aria-hidden", String(!event.data.visible));
|
|
145
|
+
this.setGuardVisible(event.data.visible);
|
|
133
146
|
if (event.data.visible) {
|
|
134
|
-
iframe.focus();
|
|
135
147
|
remoteWindow.postMessage(
|
|
136
148
|
{ type: "graffiti-guard:shown" },
|
|
137
149
|
hostUrl.origin,
|
|
@@ -166,7 +178,6 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
166
178
|
this.hostUrl = hostUrl;
|
|
167
179
|
this.iframe = iframe;
|
|
168
180
|
this.rpc = rpc;
|
|
169
|
-
this.sessionEvents = rpc.sessionEvents;
|
|
170
181
|
this.onMessage = onMessage;
|
|
171
182
|
this.connectionTimeout = connectionTimeout;
|
|
172
183
|
|
|
@@ -182,7 +193,7 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
182
193
|
return instance;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
|
-
login: Graffiti["login"] = (
|
|
196
|
+
login: Graffiti["login"] = (options) => {
|
|
186
197
|
const loginUrl = new URL(this.hostUrl);
|
|
187
198
|
// The guard reads this state in the browser; keep it out of the HTTP request.
|
|
188
199
|
// This allows for very long redirectUrls
|
|
@@ -190,7 +201,11 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
190
201
|
guardLogin: "1",
|
|
191
202
|
redirectUrl: window.location.href,
|
|
192
203
|
});
|
|
193
|
-
if (actor) params.set("suggestedActor", actor);
|
|
204
|
+
if (options?.actor) params.set("suggestedActor", options.actor);
|
|
205
|
+
const nonce = crypto.randomUUID();
|
|
206
|
+
sessionStorage.setItem(pendingLoginKey + this.hostUrl.origin, nonce);
|
|
207
|
+
params.set("loginNonce", nonce);
|
|
208
|
+
if (options?.state !== undefined) params.set("state", options.state);
|
|
194
209
|
loginUrl.hash = params.toString();
|
|
195
210
|
window.location.assign(loginUrl.href);
|
|
196
211
|
return Promise.resolve();
|
|
@@ -211,11 +226,62 @@ export class GraffitiGuarded extends Graffiti {
|
|
|
211
226
|
return auditUrl.href;
|
|
212
227
|
}
|
|
213
228
|
|
|
229
|
+
private setGuardVisible(visible: boolean) {
|
|
230
|
+
if (visible) {
|
|
231
|
+
if (this.iframe.style.display === "block") return;
|
|
232
|
+
const focused = document.activeElement;
|
|
233
|
+
this.returnFocus =
|
|
234
|
+
focused instanceof HTMLElement && focused !== this.iframe
|
|
235
|
+
? focused
|
|
236
|
+
: null;
|
|
237
|
+
this.inertBackground();
|
|
238
|
+
this.backgroundObserver = new MutationObserver(() => this.inertBackground());
|
|
239
|
+
this.backgroundObserver.observe(document.body, { childList: true });
|
|
240
|
+
this.iframe.style.display = "block";
|
|
241
|
+
this.iframe.setAttribute("aria-hidden", "false");
|
|
242
|
+
this.iframe.focus();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (this.iframe.style.display === "none") return;
|
|
247
|
+
|
|
248
|
+
this.iframe.style.display = "none";
|
|
249
|
+
this.iframe.setAttribute("aria-hidden", "true");
|
|
250
|
+
this.backgroundObserver?.disconnect();
|
|
251
|
+
this.backgroundObserver = undefined;
|
|
252
|
+
for (const [element, wasInert] of this.backgroundInert) {
|
|
253
|
+
element.inert = wasInert;
|
|
254
|
+
}
|
|
255
|
+
this.backgroundInert.clear();
|
|
256
|
+
const target = this.returnFocus;
|
|
257
|
+
this.returnFocus = null;
|
|
258
|
+
if (target?.isConnected && !target.inert && target !== document.body) {
|
|
259
|
+
target.focus({ preventScroll: true });
|
|
260
|
+
} else {
|
|
261
|
+
const previousTabindex = document.body.getAttribute("tabindex");
|
|
262
|
+
document.body.tabIndex = -1;
|
|
263
|
+
document.body.focus({ preventScroll: true });
|
|
264
|
+
if (previousTabindex === null) document.body.removeAttribute("tabindex");
|
|
265
|
+
else document.body.setAttribute("tabindex", previousTabindex);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private inertBackground() {
|
|
270
|
+
for (const child of Array.from(document.body.children)) {
|
|
271
|
+
if (child === this.iframe || !(child instanceof HTMLElement)) continue;
|
|
272
|
+
if (!this.backgroundInert.has(child)) {
|
|
273
|
+
this.backgroundInert.set(child, child.inert);
|
|
274
|
+
}
|
|
275
|
+
child.inert = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
214
279
|
destroy() {
|
|
215
280
|
if (this.destroyed) return;
|
|
216
281
|
this.destroyed = true;
|
|
217
282
|
window.clearTimeout(this.connectionTimeout);
|
|
218
283
|
window.removeEventListener("message", this.onMessage);
|
|
284
|
+
if (this.iframe.style.display === "block") this.setGuardVisible(false);
|
|
219
285
|
this.rpc.destroy();
|
|
220
286
|
this.iframe.remove();
|
|
221
287
|
if (instance === this) instance = undefined;
|