Repository navigation
feat(analytics): report connected client platforms #8481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
2ea6add
feat(analytics): report connected client platforms
t3dotgg 07279ce
docs(analytics): clarify telemetry identity precedence
t3dotgg 2e59a19
fix(desktop): remove unsupported preload imports
t3dotgg 72aa508
fix(ci): harden preload bundle checks
t3dotgg 8070b24
fix(desktop): reject commented dynamic preload imports
t3dotgg 2ef40db
fix(desktop): detect commented preload requires
t3dotgg 1cc4229
fix(auth): omit telemetry sentinels from client labels
t3dotgg 9ced832
fix(desktop): detect optional preload requires
t3dotgg 24c162b
fix(desktop): parse preload runtime imports
t3dotgg eb42690
fix(desktop): verify preload bridge APIs
t3dotgg 8dc1cfc
fix(desktop): verify callable preload bridge APIs
t3dotgg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import * as NodeFSP from "node:fs/promises"; | ||
| import * as NodeURL from "node:url"; | ||
| import { parse } from "acorn"; | ||
|
|
||
| const expectedDesktopBridgeApis = [ | ||
| "getClientPlatform", | ||
| "getLocalEnvironmentBootstraps", | ||
| "pickFolder", | ||
| ]; | ||
| const clerkPasskeysGlobal = "__clerk_internal_electron_passkeys"; | ||
|
|
||
| const isSyntaxNode = (value) => | ||
| typeof value === "object" && value !== null && "type" in value && typeof value.type === "string"; | ||
|
|
||
| const readStaticName = (node) => { | ||
| if (node.type === "Identifier") return node.name; | ||
| if (node.type === "Literal" && typeof node.value === "string") return node.value; | ||
| return undefined; | ||
| }; | ||
|
|
||
| const readMemberName = (node) => { | ||
| if (node.type !== "MemberExpression") return undefined; | ||
| if (!node.computed && node.property.type === "Identifier") return node.property.name; | ||
| return readStaticName(node.property); | ||
| }; | ||
|
|
||
| const isContextBridge = (node) => | ||
| (node.type === "Identifier" && node.name === "contextBridge") || | ||
| (node.type === "MemberExpression" && readMemberName(node) === "contextBridge"); | ||
|
|
||
| const inspectBundle = (source) => { | ||
| const runtimeImports = []; | ||
| const exposedGlobals = new Set(); | ||
| const desktopBridgeApis = new Set(); | ||
| const visit = (node) => { | ||
| if (node.type === "ImportExpression") { | ||
| throw new Error("Desktop preload bundle contains a dynamic import() call"); | ||
| } | ||
|
|
||
| if (node.type === "CallExpression" && node.callee.type === "Identifier") { | ||
|
t3dotgg marked this conversation as resolved.
|
||
| if (node.callee.name === "require") { | ||
| const [argument] = node.arguments; | ||
| if (node.arguments.length !== 1 || argument?.type !== "Literal") { | ||
| throw new Error("Desktop preload bundle contains a dynamic require() call"); | ||
| } | ||
| if (typeof argument.value !== "string") { | ||
| throw new Error("Desktop preload bundle contains a dynamic require() call"); | ||
| } | ||
| runtimeImports.push(argument.value); | ||
| } | ||
| } | ||
|
|
||
| if ( | ||
| node.type === "CallExpression" && | ||
| node.callee.type === "MemberExpression" && | ||
| readMemberName(node.callee) === "exposeInMainWorld" && | ||
| isContextBridge(node.callee.object) | ||
| ) { | ||
| const [globalName, api] = node.arguments; | ||
| const name = globalName === undefined ? undefined : readStaticName(globalName); | ||
| if (name !== undefined) exposedGlobals.add(name); | ||
|
|
||
| if (name === "desktopBridge" && api?.type === "ObjectExpression") { | ||
| for (const property of api.properties) { | ||
| if (property.type !== "Property") continue; | ||
| const propertyName = readStaticName(property.key); | ||
| if (propertyName !== undefined) desktopBridgeApis.add(propertyName); | ||
|
macroscopeapp[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| for (const child of Object.values(node)) { | ||
| if (Array.isArray(child)) { | ||
| for (const item of child) { | ||
| if (isSyntaxNode(item)) visit(item); | ||
| } | ||
| } else if (isSyntaxNode(child)) { | ||
| visit(child); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| visit(parse(source, { ecmaVersion: "latest", sourceType: "script" })); | ||
| return { desktopBridgeApis, exposedGlobals, runtimeImports }; | ||
| }; | ||
|
|
||
| export const verifyPreloadBundle = (source) => { | ||
| const { desktopBridgeApis, exposedGlobals, runtimeImports } = inspectBundle(source); | ||
| const missingApis = expectedDesktopBridgeApis.filter((api) => !desktopBridgeApis.has(api)); | ||
| if (!exposedGlobals.has("desktopBridge")) missingApis.unshift("desktopBridge exposure"); | ||
| if (!exposedGlobals.has(clerkPasskeysGlobal)) missingApis.push(`${clerkPasskeysGlobal} exposure`); | ||
|
|
||
| if (missingApis.length > 0) { | ||
| throw new Error(`Desktop preload bundle is missing executable APIs: ${missingApis.join(", ")}`); | ||
| } | ||
|
|
||
| const sandboxModules = new Set(["electron", "events", "timers", "url"]); | ||
| const unsupportedImports = [...new Set(runtimeImports)] | ||
| .filter((moduleName) => !sandboxModules.has(moduleName)) | ||
| .toSorted(); | ||
|
|
||
| if (unsupportedImports.length > 0) { | ||
| throw new Error( | ||
| `Desktop preload bundle contains unsupported sandbox imports: ${unsupportedImports.join(", ")}`, | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| if (process.argv[1] && NodeURL.pathToFileURL(process.argv[1]).href === import.meta.url) { | ||
| const preloadUrl = new URL("../dist-electron/preload.cjs", import.meta.url); | ||
| const source = await NodeFSP.readFile(preloadUrl, "utf8"); | ||
| verifyPreloadBundle(source); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { assert, describe, it } from "vite-plus/test"; | ||
|
|
||
| import { verifyPreloadBundle } from "./verify-preload-bundle.mjs"; | ||
|
|
||
| const validPreload = ` | ||
| const electron = require("electron"); | ||
| const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; | ||
| electron.contextBridge.exposeInMainWorld("__clerk_internal_electron_passkeys", {}); | ||
| electron.contextBridge.exposeInMainWorld("desktopBridge", { | ||
| getClientPlatform: () => process.platform, | ||
| getLocalEnvironmentBootstraps: () => [], | ||
| pickFolder: (options) => electron.ipcRenderer.invoke(PICK_FOLDER_CHANNEL, options), | ||
| }); | ||
| `; | ||
|
|
||
| describe("desktop preload bundle verifier", () => { | ||
| it("rejects required API names that only appear in strings", () => { | ||
| assert.throws( | ||
| () => | ||
| verifyPreloadBundle(` | ||
| "desktopBridge getClientPlatform getLocalEnvironmentBootstraps pickFolder"; | ||
| "__clerk_internal_electron_passkeys"; | ||
| require("electron"); | ||
| `), | ||
| /missing executable APIs/, | ||
| ); | ||
| }); | ||
|
|
||
| it("rejects dynamic imports with comments before the opening parenthesis", () => { | ||
| assert.throws( | ||
| () => | ||
| verifyPreloadBundle(`${validPreload}\nimport /* @vite-ignore */("unsupported-module");`), | ||
| /dynamic import\(\)/, | ||
| ); | ||
| }); | ||
|
|
||
| it("ignores import-like text in strings", () => { | ||
| assert.doesNotThrow(() => | ||
| verifyPreloadBundle(`${validPreload}\nconst message = 'import /* comment */("module")';`), | ||
| ); | ||
| }); | ||
|
|
||
| it("rejects unsupported require calls with comments before the opening parenthesis", () => { | ||
| assert.throws( | ||
| () => verifyPreloadBundle(`${validPreload}\nrequire /* @__PURE__ */ ("node:fs");`), | ||
| /unsupported sandbox imports: node:fs/, | ||
| ); | ||
| }); | ||
|
|
||
| it("rejects unsupported optional require calls", () => { | ||
| assert.throws( | ||
| () => verifyPreloadBundle(`${validPreload}\nrequire?.("node:fs");`), | ||
| /unsupported sandbox imports: node:fs/, | ||
| ); | ||
| }); | ||
|
|
||
| it("ignores require-like text in strings and comments", () => { | ||
| assert.doesNotThrow(() => | ||
| verifyPreloadBundle(` | ||
| ${validPreload} | ||
| const message = 'require("node:fs")'; | ||
| // require("node:path") | ||
| `), | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.