langcoach 0.1.0 → 0.3.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,140 @@
1
+ (function() {
2
+ //#region src/shared/dev-hosts.ts
3
+ /**
4
+ * Local hosts the e2e build (and unit tests) accept in place of labs.google. Production builds define
5
+ * `__E2E__` as false, so this folds to an empty list and the host names are compiled out of the bundle
6
+ * (the zip script refuses any bundle that still contains them).
7
+ */
8
+ var DEV_HOSTNAMES = [];
9
+ /** http(s) URL on one of the dev hosts (always false in production builds). */
10
+ function isDevHostUrl(url) {
11
+ if (DEV_HOSTNAMES.length === 0) return false;
12
+ try {
13
+ const u = new URL(url);
14
+ return (u.protocol === "http:" || u.protocol === "https:") && DEV_HOSTNAMES.includes(u.hostname);
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ //#endregion
20
+ //#region src/lib/parse.ts
21
+ var KINDS = [
22
+ "terms",
23
+ "grammar",
24
+ "conversation",
25
+ "object-descriptors"
26
+ ];
27
+ var PATH_RE = /^\/lll\/api\/insights\/([a-z-]+)$/;
28
+ function kindFromUrl(url, opts) {
29
+ try {
30
+ const u = new URL(url);
31
+ const prod = u.protocol === "https:" && u.hostname === "labs.google" && u.port === "";
32
+ const local = !!opts?.allowDevHosts && isDevHostUrl(url);
33
+ if (!prod && !local) return null;
34
+ const kind = PATH_RE.exec(u.pathname)?.[1];
35
+ return kind && KINDS.includes(kind) ? kind : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ //#endregion
41
+ //#region src/content/fetch-wrap.ts
42
+ function urlOf(input, base) {
43
+ const raw = typeof input === "string" ? input : input instanceof URL ? input.href : typeof Request !== "undefined" && input instanceof Request ? input.url : String(input);
44
+ return new URL(raw, typeof base === "function" ? base() : base).href;
45
+ }
46
+ function parseBody(body) {
47
+ if (typeof body !== "string") return void 0;
48
+ try {
49
+ return JSON.parse(body);
50
+ } catch {
51
+ return;
52
+ }
53
+ }
54
+ /**
55
+ * Wraps fetch so lesson responses can be observed. The page's call path is untouched: the original is
56
+ * called with identical `this` and arguments and its own promise is returned (no extra await, same
57
+ * Response object, same rejection). Only insights responses are cloned; the clone is read in a
58
+ * detached promise and every failure there is swallowed.
59
+ */
60
+ function wrapFetch(original, onCapture, opts = {}) {
61
+ return function langcoachFetch(...args) {
62
+ const p = original.apply(this, args);
63
+ try {
64
+ const url = urlOf(args[0], opts.base);
65
+ const kind = kindFromUrl(url, { allowDevHosts: opts.allowDevHosts });
66
+ if (kind) {
67
+ const body = args[1]?.body;
68
+ p.then((res) => {
69
+ if (!res.ok) return;
70
+ res.clone().json().then((response) => {
71
+ onCapture({
72
+ kind,
73
+ request: parseBody(body),
74
+ response,
75
+ url
76
+ });
77
+ }).catch(() => {});
78
+ }, () => {}).catch(() => {});
79
+ }
80
+ } catch {}
81
+ return p;
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/content/protocol.ts
86
+ /** Attribute the isolated script sets on <html> to hand the per-page nonce to the MAIN-world script. */
87
+ var NONCE_ATTR = "data-langcoach-nonce";
88
+ /** window.postMessage `source` tags between the MAIN-world and isolated content scripts. */
89
+ var CAPTURE_SOURCE = "langcoach-capture";
90
+ var ACK_SOURCE = "langcoach-ack";
91
+ //#endregion
92
+ //#region src/content/main.ts
93
+ var WRAPPED = Symbol.for("langcoach.fetchWrapped");
94
+ /**
95
+ * MAIN-world entry (document_start): wraps window.fetch and posts lesson captures to the isolated
96
+ * content script. Never throws into the page.
97
+ */
98
+ function startMain(win, opts) {
99
+ try {
100
+ const current = win.fetch;
101
+ if (typeof current !== "function" || current[WRAPPED]) return;
102
+ let nonce = null;
103
+ const takeNonce = () => {
104
+ if (nonce) return nonce;
105
+ const v = win.document.documentElement?.getAttribute(NONCE_ATTR);
106
+ if (!v) return null;
107
+ nonce = v;
108
+ try {
109
+ win.postMessage({
110
+ source: ACK_SOURCE,
111
+ nonce: v
112
+ }, win.location.origin);
113
+ } catch {}
114
+ return nonce;
115
+ };
116
+ takeNonce();
117
+ const onCapture = (c) => {
118
+ const n = takeNonce();
119
+ if (!n) return;
120
+ try {
121
+ win.postMessage({
122
+ source: CAPTURE_SOURCE,
123
+ nonce: n,
124
+ ...c
125
+ }, win.location.origin);
126
+ } catch {}
127
+ };
128
+ const wrapped = wrapFetch(current, onCapture, {
129
+ base: () => win.location.href,
130
+ allowDevHosts: opts.allowDevHosts
131
+ });
132
+ wrapped[WRAPPED] = true;
133
+ win.fetch = wrapped;
134
+ } catch {}
135
+ }
136
+ //#endregion
137
+ //#region src/content-main.ts
138
+ startMain(window, { allowDevHosts: false });
139
+ //#endregion
140
+ })();
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,63 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "__MSG_extName__",
4
+ "description": "__MSG_extDescription__",
5
+ "version": "0.3.0",
6
+ "default_locale": "en",
7
+ "minimum_chrome_version": "116",
8
+ "icons": {
9
+ "16": "icons/16.png",
10
+ "32": "icons/32.png",
11
+ "48": "icons/48.png",
12
+ "128": "icons/128.png"
13
+ },
14
+ "action": {
15
+ "default_title": "__MSG_extName__",
16
+ "default_icon": {
17
+ "16": "icons/16.png",
18
+ "32": "icons/32.png",
19
+ "48": "icons/48.png",
20
+ "128": "icons/128.png"
21
+ }
22
+ },
23
+ "side_panel": {
24
+ "default_path": "sidepanel.html"
25
+ },
26
+ "background": {
27
+ "service_worker": "background.js",
28
+ "type": "module"
29
+ },
30
+ "permissions": [
31
+ "storage",
32
+ "unlimitedStorage",
33
+ "sidePanel",
34
+ "contextMenus",
35
+ "alarms",
36
+ "notifications"
37
+ ],
38
+ "host_permissions": [
39
+ "https://labs.google/lll/*",
40
+ "http://127.0.0.1:47821/*"
41
+ ],
42
+ "content_scripts": [
43
+ {
44
+ "matches": [
45
+ "https://labs.google/lll/*"
46
+ ],
47
+ "js": [
48
+ "content-main.js"
49
+ ],
50
+ "run_at": "document_start",
51
+ "world": "MAIN"
52
+ },
53
+ {
54
+ "matches": [
55
+ "https://labs.google/lll/*"
56
+ ],
57
+ "js": [
58
+ "content-isolated.js"
59
+ ],
60
+ "run_at": "document_start"
61
+ }
62
+ ]
63
+ }
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>LangCoach</title>
7
+ <script type="module" crossorigin src="/sidepanel.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/chunks/translate-CM6Phtg_.js">
9
+ <link rel="stylesheet" crossorigin href="/assets/sidepanel-DMjVhmRR.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ </body>
14
+ </html>