@graffiti-garden/wrapper-data-guard 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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\nlet instance: GraffitiGuarded | undefined;\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 destroyed = false;\n\n constructor(options: GraffitiGuardedOptions = {}) {\n super();\n if (instance) return instance;\n\n const hostUrl = new URL(\n options.hostUrl?.toString() ?? \"https://guard.graffiti.garden/\",\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 height: \"100vh\",\n inset: \"0\",\n position: \"fixed\",\n width: \"100vw\",\n zIndex: \"2147483647\",\n });\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({ type: \"graffiti-guard:connect\" }, hostUrl.origin);\n });\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 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 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 } else if (\n event.data.type === \"graffiti-guard:open-audit\" &&\n typeof event.data.actor === \"string\" &&\n Array.isArray(event.data.source)\n ) {\n this.audit(\n event.data,\n event.data.view === \"permissions\" ? \"permissions\" : \"activity\",\n );\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\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 loginUrl.searchParams.set(\"guardLogin\", \"1\");\n loginUrl.searchParams.set(\"redirectUrl\", window.location.href);\n if (actor) loginUrl.searchParams.set(\"suggestedActor\", actor);\n window.location.assign(loginUrl.href);\n return Promise.resolve();\n };\n\n /** Open the host-owned audit panel without exposing audit data to this app. */\n audit(\n session?: GraffitiGuardSession,\n view: \"activity\" | \"permissions\" = \"activity\",\n ) {\n const auditUrl = new URL(this.hostUrl);\n auditUrl.searchParams.set(\"redirectUrl\", window.location.href);\n auditUrl.searchParams.set(\"source\", JSON.stringify(session?.source ?? []));\n if (session?.actor) auditUrl.searchParams.set(\"actor\", session.actor);\n auditUrl.searchParams.set(\"view\", view);\n window.location.assign(auditUrl.href);\n }\n\n destroy() {\n if (this.destroyed) return;\n this.destroyed = true;\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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,28 @@
1
+ import { Graffiti, type GraffitiSession } from "@graffiti-garden/api";
2
+ export interface GraffitiGuardedOptions {
3
+ hostUrl?: string | URL;
4
+ channel?: string;
5
+ timeout?: number;
6
+ }
7
+ export interface GraffitiGuardSourceSegment {
8
+ id: string;
9
+ name: string;
10
+ }
11
+ export type GraffitiGuardSession = GraffitiSession & {
12
+ source?: GraffitiGuardSourceSegment[];
13
+ };
14
+ /** A Graffiti implementation backed by a sandboxed guard iframe. */
15
+ export declare class GraffitiGuarded extends Graffiti {
16
+ readonly sessionEvents: EventTarget;
17
+ private readonly hostUrl;
18
+ private readonly iframe;
19
+ private readonly rpc;
20
+ private readonly onMessage;
21
+ private destroyed;
22
+ constructor(options?: GraffitiGuardedOptions);
23
+ login: Graffiti["login"];
24
+ /** Open the host-owned audit panel without exposing audit data to this app. */
25
+ audit(session?: GraffitiGuardSession, view?: "activity" | "permissions"): void;
26
+ destroy(): void;
27
+ }
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;AAKtE,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,SAAS,CAAS;IAE1B,YAAY,OAAO,GAAE,sBAA2B,EA+F/C;IAED,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAOtB;IAEF,+EAA+E;IAC/E,KAAK,CACH,OAAO,CAAC,EAAE,oBAAoB,EAC9B,IAAI,GAAE,UAAU,GAAG,aAA0B,QAQ9C;IAED,OAAO,SAON;CACF"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@graffiti-garden/wrapper-data-guard",
3
+ "version": "0.1.0",
4
+ "description": "Ask for permission before a site accesses Graffiti",
5
+ "types": "./dist/index.d.ts",
6
+ "module": "./dist/esm/index.js",
7
+ "main": "./dist/cjs/index.js",
8
+ "browser": "./dist/browser/index.js",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/esm/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/cjs/index.js"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "src",
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build:types": "tsc --declaration --emitDeclarationOnly",
29
+ "build:js": "tsx esbuild.config.mts",
30
+ "build": "rm -rf dist && npm run build:types && npm run build:js",
31
+ "check": "tsc --noEmit",
32
+ "prepublishOnly": "npm run check && npm run build"
33
+ },
34
+ "author": "Theia Henderson",
35
+ "license": "GPL-3.0-or-later",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/graffiti-garden/graffiti.git",
39
+ "directory": "wrappers/data-guard/client"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/graffiti-garden/graffiti/issues"
43
+ },
44
+ "dependencies": {
45
+ "@graffiti-garden/api": "^1.2.0",
46
+ "@graffiti-garden/wrapper-iframe-rpc": "^0.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "esbuild": "^0.28.1",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^7.0.2"
52
+ }
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,158 @@
1
+ import { Graffiti, type GraffitiSession } from "@graffiti-garden/api";
2
+ import { GraffitiRpcClient } from "@graffiti-garden/wrapper-iframe-rpc/client";
3
+
4
+ let instance: GraffitiGuarded | undefined;
5
+
6
+ export interface GraffitiGuardedOptions {
7
+ hostUrl?: string | URL;
8
+ channel?: string;
9
+ timeout?: number;
10
+ }
11
+
12
+ export interface GraffitiGuardSourceSegment {
13
+ id: string;
14
+ name: string;
15
+ }
16
+
17
+ export type GraffitiGuardSession = GraffitiSession & {
18
+ source?: GraffitiGuardSourceSegment[];
19
+ };
20
+
21
+ /** A Graffiti implementation backed by a sandboxed guard iframe. */
22
+ // @ts-ignore Graffiti methods are delegated through the constructor proxy.
23
+ export class GraffitiGuarded extends Graffiti {
24
+ readonly sessionEvents!: EventTarget;
25
+ private readonly hostUrl!: URL;
26
+ private readonly iframe!: HTMLIFrameElement;
27
+ private readonly rpc!: GraffitiRpcClient;
28
+ private readonly onMessage!: (event: MessageEvent) => void;
29
+ private destroyed = false;
30
+
31
+ constructor(options: GraffitiGuardedOptions = {}) {
32
+ super();
33
+ if (instance) return instance;
34
+
35
+ const hostUrl = new URL(
36
+ options.hostUrl?.toString() ?? "https://guard.graffiti.garden/",
37
+ document.baseURI,
38
+ );
39
+ const iframe = document.createElement("iframe");
40
+ iframe.title = "Graffiti Guard";
41
+ iframe.allow = "storage-access";
42
+ iframe.sandbox.add(
43
+ "allow-scripts",
44
+ "allow-same-origin",
45
+ "allow-storage-access-by-user-activation",
46
+ "allow-downloads",
47
+ "allow-forms",
48
+ "allow-popups",
49
+ "allow-top-navigation-by-user-activation",
50
+ );
51
+ iframe.src = hostUrl.href;
52
+ iframe.setAttribute("aria-hidden", "true");
53
+ Object.assign(iframe.style, {
54
+ background: "transparent",
55
+ border: "0",
56
+ display: "none",
57
+ height: "100vh",
58
+ inset: "0",
59
+ position: "fixed",
60
+ width: "100vw",
61
+ zIndex: "2147483647",
62
+ });
63
+ iframe.addEventListener("load", () => {
64
+ // Bootstrap the guard with the browser-reported embedding origin, which
65
+ // the RPC handshake verifies internally but does not expose to the host.
66
+ iframe.contentWindow?.postMessage({ type: "graffiti-guard:connect" }, hostUrl.origin);
67
+ });
68
+ document.body.append(iframe);
69
+
70
+ const remoteWindow = iframe.contentWindow;
71
+ if (!remoteWindow) {
72
+ iframe.remove();
73
+ throw new Error("Graffiti Guard iframe did not create a content window.");
74
+ }
75
+
76
+ const rpc = new GraffitiRpcClient({
77
+ remoteWindow,
78
+ allowedOrigins: [hostUrl.origin],
79
+ channel: options.channel,
80
+ timeout: options.timeout,
81
+ });
82
+ const onMessage = (event: MessageEvent) => {
83
+ if (
84
+ event.source !== remoteWindow ||
85
+ event.origin !== hostUrl.origin ||
86
+ !event.data ||
87
+ typeof event.data !== "object"
88
+ ) {
89
+ return;
90
+ }
91
+ if (
92
+ event.data.type === "graffiti-guard:set-visible" &&
93
+ typeof event.data.visible === "boolean"
94
+ ) {
95
+ iframe.style.display = event.data.visible ? "block" : "none";
96
+ iframe.setAttribute("aria-hidden", String(!event.data.visible));
97
+ } else if (
98
+ event.data.type === "graffiti-guard:open-audit" &&
99
+ typeof event.data.actor === "string" &&
100
+ Array.isArray(event.data.source)
101
+ ) {
102
+ this.audit(
103
+ event.data,
104
+ event.data.view === "permissions" ? "permissions" : "activity",
105
+ );
106
+ }
107
+ };
108
+ window.addEventListener("message", onMessage);
109
+
110
+ this.hostUrl = hostUrl;
111
+ this.iframe = iframe;
112
+ this.rpc = rpc;
113
+ this.sessionEvents = rpc.sessionEvents;
114
+ this.onMessage = onMessage;
115
+
116
+ instance = new Proxy(this, {
117
+ get(target, property, receiver) {
118
+ if (Reflect.has(target, property)) {
119
+ return Reflect.get(target, property, receiver);
120
+ }
121
+ const value = Reflect.get(rpc, property, rpc);
122
+ return typeof value === "function" ? value.bind(rpc) : value;
123
+ },
124
+ });
125
+ return instance;
126
+ }
127
+
128
+ login: Graffiti["login"] = (actor) => {
129
+ const loginUrl = new URL(this.hostUrl);
130
+ loginUrl.searchParams.set("guardLogin", "1");
131
+ loginUrl.searchParams.set("redirectUrl", window.location.href);
132
+ if (actor) loginUrl.searchParams.set("suggestedActor", actor);
133
+ window.location.assign(loginUrl.href);
134
+ return Promise.resolve();
135
+ };
136
+
137
+ /** Open the host-owned audit panel without exposing audit data to this app. */
138
+ audit(
139
+ session?: GraffitiGuardSession,
140
+ view: "activity" | "permissions" = "activity",
141
+ ) {
142
+ const auditUrl = new URL(this.hostUrl);
143
+ auditUrl.searchParams.set("redirectUrl", window.location.href);
144
+ auditUrl.searchParams.set("source", JSON.stringify(session?.source ?? []));
145
+ if (session?.actor) auditUrl.searchParams.set("actor", session.actor);
146
+ auditUrl.searchParams.set("view", view);
147
+ window.location.assign(auditUrl.href);
148
+ }
149
+
150
+ destroy() {
151
+ if (this.destroyed) return;
152
+ this.destroyed = true;
153
+ window.removeEventListener("message", this.onMessage);
154
+ this.rpc.destroy();
155
+ this.iframe.remove();
156
+ if (instance === this) instance = undefined;
157
+ }
158
+ }