Skip to content

Commit e9effc3

Browse files
committed
chore: merge main into csv surrogate fix
2 parents 68892c0 + e5909e5 commit e9effc3

859 files changed

Lines changed: 39899 additions & 4949 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/mobile-eas-production.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ jobs:
215215
git config user.name "${APP_SLUG}[bot]"
216216
git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com"
217217
git add apps/mobile/app.config.ts
218-
git commit \
218+
git commit --no-verify \
219219
-m "chore(mobile): bump app version to $RELEASE_VERSION" \
220220
-m "Co-authored-by: codex <[email protected]>"
221221
git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}"

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ jobs:
13881388
git config user.email "${{ steps.app_bot.outputs.email }}"
13891389
13901390
git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml
1391-
git commit -m "chore(release): prepare $RELEASE_TAG"
1391+
git commit --no-verify -m "chore(release): prepare $RELEASE_TAG"
13921392
git push origin HEAD:main
13931393
13941394
announce_discord:

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ The most common defect in this repo is a change that works on the path you teste
9191
An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state:
9292

9393
- The first `vp run dev` in a worktree does this for you when its database is missing and the source is at most 500 MiB. Larger sources warn and dev starts with an empty database. `--no-seed` or `T3CODE_DEV_SEED=0` skips seeding. Manual migration still accepts larger sources and may use substantial disk space before pruning.
94-
- Run `vp run migrate-dev-db` with your dev server stopped to refresh. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, and auth sessions, so your dev server never runs the developer's agents. Raise `--projects` and `--threads-per-project` for more data.
94+
- Run `vp run migrate-dev-db` with your dev server stopped to refresh. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, auth sessions, and links to other environments, so your dev server never runs the developer's agents or reaches their other machines. Raise `--projects` and `--threads-per-project` for more data.
9595
- Refresh `statev2.sqlite`, not `state.sqlite`. The server copies the V1 `state.sqlite` only when `statev2.sqlite` is missing.
9696
- Bring `secrets` and `settings.json` only if the flow under test needs them.
9797
- Copy in, never symlink. Data flows one way: into your sandbox, never back out.

‎apps/desktop/resources/installer.nsh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
!include "StrFunc.nsh"
2+
!ifdef BUILD_UNINSTALLER
3+
${UnStrRep}
4+
!define T3StrRep '${UnStrRep}'
5+
!else
6+
${StrRep}
7+
!define T3StrRep '${StrRep}'
8+
!endif
9+
10+
!macro customAppRunningMessages
11+
Push $0
12+
Push $1
13+
${If} $IsPowerShellAvailable == 0
14+
System::Call 'kernel32::SetEnvironmentVariable(t "T3CODE_INSTALL_DIR", t "$INSTDIR")'
15+
nsExec::ExecToStack `"$PowerShellPath" -NoProfile -NonInteractive -C "Get-CimInstance -ClassName Win32_Process | Where-Object { $$_.ExecutablePath -and [IO.Path]::GetDirectoryName($$_.ExecutablePath) -eq $$env:T3CODE_INSTALL_DIR } | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($$_.ExecutablePath) } | Where-Object { $$_ -in 'T3 Code (Alpha)', 'T3 Code (Beta)', 'T3 Code (Nightly)' } | Select-Object -First 1 | ForEach-Object { [Console]::Write($$_) }"`
16+
Pop $0
17+
Pop $1
18+
System::Call 'kernel32::SetEnvironmentVariable(t "T3CODE_INSTALL_DIR", p 0)'
19+
${If} $0 == 0
20+
${If} $1 == "T3 Code (Alpha)"
21+
${OrIf} $1 == "T3 Code (Beta)"
22+
${OrIf} $1 == "T3 Code (Nightly)"
23+
${T3StrRep} $appRunningMessage $appRunningMessage "${PRODUCT_NAME}" $1
24+
${T3StrRep} $appClosingMessage $appClosingMessage "${PRODUCT_NAME}" $1
25+
${T3StrRep} $appCannotBeClosedMessage $appCannotBeClosedMessage "${PRODUCT_NAME}" $1
26+
${EndIf}
27+
${EndIf}
28+
${EndIf}
29+
Pop $1
30+
Pop $0
31+
!macroend
32+
133
!macro customInstall
234
!ifndef DO_NOT_CREATE_START_MENU_SHORTCUT
335
${IfNot} ${FileExists} "$newStartMenuLink"
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// @effect-diagnostics nodeBuiltinImport:off -- These tests assert real POSIX modes on a temporary directory.
2+
import * as NodeFS from "node:fs";
3+
import * as NodeOS from "node:os";
4+
import * as NodePath from "node:path";
5+
import { assert, describe, it } from "@effect/vitest";
6+
import { vi } from "vite-plus/test";
7+
8+
vi.mock("node:fs", { spy: true });
9+
10+
import { openClerkTokenStorage, type ClerkTokenStorage } from "./ClerkTokenStore.ts";
11+
12+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Unix mode bits are the assertion; Windows reports them through ACLs.
13+
const describePosix = describe.skipIf(process.platform === "win32");
14+
15+
const modeOf = (path: string): number => NodeFS.statSync(path).mode & 0o777;
16+
17+
function writeLoose(path: string, contents: string): void {
18+
NodeFS.writeFileSync(path, contents, { mode: 0o666 });
19+
NodeFS.chmodSync(path, 0o666);
20+
}
21+
22+
async function withStateDir(run: (stateDir: string) => Promise<void> | void): Promise<void> {
23+
const stateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-clerk-tokens-"));
24+
try {
25+
await run(stateDir);
26+
} finally {
27+
NodeFS.rmSync(stateDir, { recursive: true, force: true });
28+
}
29+
}
30+
31+
const idleStorage = (): ClerkTokenStorage => ({
32+
getItem: async () => null,
33+
setItem: async () => undefined,
34+
removeItem: () => undefined,
35+
});
36+
37+
describePosix("ClerkTokenStore", () => {
38+
it("keeps clerk tokens at 0600 inside a 0700 directory", async () => {
39+
await withStateDir(async (stateDir) => {
40+
NodeFS.chmodSync(stateDir, 0o755);
41+
const directory = NodePath.join(stateDir, "clerk");
42+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
43+
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
44+
NodeFS.mkdirSync(directory, { mode: 0o755 });
45+
NodeFS.chmodSync(directory, 0o755);
46+
writeLoose(legacyPath, '{"__clerk_client_jwt":"enc:existing"}\n');
47+
48+
const opened: string[] = [];
49+
const storage = openClerkTokenStorage(stateDir, (cwd) => {
50+
opened.push(cwd);
51+
return {
52+
getItem: async () => {
53+
writeLoose(tokenPath, "from-get");
54+
return "from-get";
55+
},
56+
setItem: (_key, value) => {
57+
writeLoose(tokenPath, value);
58+
},
59+
removeItem: () => {
60+
NodeFS.rmSync(tokenPath, { force: true });
61+
},
62+
};
63+
});
64+
65+
assert.deepEqual(opened, [directory]);
66+
assert.equal(modeOf(stateDir), 0o755);
67+
assert.equal(modeOf(directory), 0o700);
68+
assert.equal(modeOf(tokenPath), 0o600);
69+
assert.equal(NodeFS.existsSync(legacyPath), false);
70+
assert.equal(
71+
NodeFS.readFileSync(tokenPath, "utf8"),
72+
'{"__clerk_client_jwt":"enc:existing"}\n',
73+
);
74+
75+
assert.equal(await storage.getItem("client"), "from-get");
76+
assert.equal(modeOf(tokenPath), 0o600);
77+
await storage.setItem("client", "from-set");
78+
assert.equal(modeOf(tokenPath), 0o600);
79+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "from-set");
80+
storage.removeItem("client");
81+
assert.equal(NodeFS.existsSync(tokenPath), false);
82+
83+
writeLoose(tokenPath, "kept");
84+
writeLoose(legacyPath, "stale");
85+
NodeFS.chmodSync(directory, 0o755);
86+
openClerkTokenStorage(stateDir, () => idleStorage());
87+
assert.equal(modeOf(directory), 0o700);
88+
assert.equal(modeOf(tokenPath), 0o600);
89+
assert.equal(NodeFS.existsSync(legacyPath), false);
90+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept");
91+
});
92+
});
93+
94+
it("removes the legacy token file when the private copy already exists", async () => {
95+
await withStateDir((stateDir) => {
96+
const directory = NodePath.join(stateDir, "clerk");
97+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
98+
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
99+
NodeFS.mkdirSync(directory, { mode: 0o755 });
100+
writeLoose(legacyPath, "stale-credential");
101+
writeLoose(tokenPath, "kept-credential");
102+
103+
openClerkTokenStorage(stateDir, () => idleStorage());
104+
105+
assert.equal(NodeFS.existsSync(legacyPath), false);
106+
assert.equal(modeOf(directory), 0o700);
107+
assert.equal(modeOf(tokenPath), 0o600);
108+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept-credential");
109+
});
110+
});
111+
112+
it("resolves a relative state directory before opening storage", async () => {
113+
await withStateDir(async (stateDir) => {
114+
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
115+
const opened: string[] = [];
116+
openClerkTokenStorage(relativeStateDir, (directory) => {
117+
opened.push(directory);
118+
return idleStorage();
119+
});
120+
121+
const directory = NodePath.join(stateDir, "clerk");
122+
assert.equal(NodePath.isAbsolute(relativeStateDir), false);
123+
assert.deepEqual(opened, [directory]);
124+
assert.equal(modeOf(directory), 0o700);
125+
});
126+
});
127+
128+
it("migrates relative state paths from Electron userData, not the working directory", async () => {
129+
await withStateDir((stateDir) => {
130+
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
131+
const userData = NodePath.join(
132+
stateDir,
133+
"electron",
134+
...relativeStateDir.split(NodePath.sep).map(() => "profile"),
135+
);
136+
const legacyStateDir = NodePath.join(userData, relativeStateDir);
137+
const legacyPath = NodePath.join(legacyStateDir, "clerk-tokens.json");
138+
NodeFS.mkdirSync(legacyStateDir, { recursive: true });
139+
writeLoose(legacyPath, "existing-session");
140+
const unrelatedPath = NodePath.join(stateDir, "clerk-tokens.json");
141+
writeLoose(unrelatedPath, "not-the-electron-session");
142+
143+
openClerkTokenStorage(relativeStateDir, idleStorage, userData);
144+
145+
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
146+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
147+
assert.equal(modeOf(tokenPath), 0o600);
148+
assert.equal(NodeFS.existsSync(legacyPath), false);
149+
assert.equal(NodeFS.readFileSync(unrelatedPath, "utf8"), "not-the-electron-session");
150+
});
151+
});
152+
153+
it("migrates tokens when the legacy directory is on another filesystem", async () => {
154+
await withStateDir((stateDir) => {
155+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
156+
writeLoose(legacyPath, "existing-session");
157+
using _rename = vi.spyOn(NodeFS, "renameSync").mockImplementation(() => {
158+
throw Object.assign(new Error("cross-device link"), { code: "EXDEV" });
159+
});
160+
161+
openClerkTokenStorage(stateDir, idleStorage);
162+
163+
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
164+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
165+
assert.equal(modeOf(tokenPath), 0o600);
166+
assert.equal(NodeFS.existsSync(legacyPath), false);
167+
});
168+
});
169+
170+
it("does not follow a symlinked clerk directory or token file", async () => {
171+
await withStateDir(async (stateDir) => {
172+
const outsideDirectory = NodePath.join(stateDir, "outside-dir");
173+
NodeFS.mkdirSync(outsideDirectory, { mode: 0o755 });
174+
NodeFS.chmodSync(outsideDirectory, 0o755);
175+
NodeFS.symlinkSync(outsideDirectory, NodePath.join(stateDir, "clerk"));
176+
177+
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a real directory/);
178+
assert.equal(modeOf(outsideDirectory), 0o755);
179+
NodeFS.unlinkSync(NodePath.join(stateDir, "clerk"));
180+
181+
const outsideFile = NodePath.join(stateDir, "outside.json");
182+
NodeFS.writeFileSync(outsideFile, "secret", { mode: 0o644 });
183+
NodeFS.chmodSync(outsideFile, 0o644);
184+
NodeFS.symlinkSync(outsideFile, NodePath.join(stateDir, "clerk-tokens.json"));
185+
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a regular file/);
186+
assert.equal(modeOf(outsideFile), 0o644);
187+
});
188+
});
189+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// @effect-diagnostics nodeBuiltinImport:off -- The Clerk bridge must lock the token file down before startup can yield to Electron's ready event.
2+
import * as NodeFS from "node:fs";
3+
import * as NodePath from "node:path";
4+
5+
// electron-store chmods every write to 0666; the private directory protects tokens during writes.
6+
const CLERK_TOKEN_DIRECTORY_MODE = 0o700;
7+
const CLERK_TOKEN_FILE_MODE = 0o600;
8+
const CLERK_TOKEN_DIRECTORY_NAME = "clerk";
9+
const CLERK_TOKEN_FILE_NAME = "clerk-tokens.json";
10+
11+
type Awaitable<T> = T | Promise<T>;
12+
13+
export interface ClerkTokenStorage {
14+
readonly getItem: (key: string) => Awaitable<string | null>;
15+
readonly setItem: (key: string, value: string) => Awaitable<void>;
16+
readonly removeItem: (key: string) => Awaitable<void>;
17+
}
18+
19+
function isErrno(error: unknown, code: string): boolean {
20+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
21+
}
22+
23+
function lstatIfExists(path: string): NodeFS.Stats | undefined {
24+
try {
25+
return NodeFS.lstatSync(path);
26+
} catch (error) {
27+
if (isErrno(error, "ENOENT")) return undefined;
28+
throw error;
29+
}
30+
}
31+
32+
function assertRegularFile(path: string, stat: NodeFS.Stats): void {
33+
if (stat.isSymbolicLink() || !stat.isFile()) {
34+
throw new Error(`Clerk token file "${path}" must be a regular file.`);
35+
}
36+
}
37+
38+
function restrictClerkTokenFile(path: string): void {
39+
const stat = lstatIfExists(path);
40+
if (stat === undefined) return;
41+
assertRegularFile(path, stat);
42+
if ((stat.mode & 0o777) === CLERK_TOKEN_FILE_MODE) return;
43+
NodeFS.chmodSync(path, CLERK_TOKEN_FILE_MODE);
44+
}
45+
46+
function migrateLegacyClerkTokenFile(stateDir: string, directory: string): void {
47+
const legacyPath = NodePath.join(stateDir, CLERK_TOKEN_FILE_NAME);
48+
const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
49+
const legacy = lstatIfExists(legacyPath);
50+
if (legacy !== undefined) {
51+
assertRegularFile(legacyPath, legacy);
52+
NodeFS.chmodSync(legacyPath, CLERK_TOKEN_FILE_MODE);
53+
}
54+
55+
const destination = lstatIfExists(tokenPath);
56+
if (destination === undefined) {
57+
if (legacy === undefined) return;
58+
try {
59+
NodeFS.copyFileSync(legacyPath, tokenPath, NodeFS.constants.COPYFILE_EXCL);
60+
NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
61+
NodeFS.unlinkSync(legacyPath);
62+
} catch (error) {
63+
if (!isErrno(error, "ENOENT")) throw error;
64+
}
65+
return;
66+
}
67+
68+
assertRegularFile(tokenPath, destination);
69+
NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
70+
if (legacy === undefined) return;
71+
try {
72+
NodeFS.unlinkSync(legacyPath);
73+
} catch (error) {
74+
if (!isErrno(error, "ENOENT")) throw error;
75+
}
76+
}
77+
78+
function restrictAfter<T>(run: () => Awaitable<T>, tighten: () => void): Awaitable<T> {
79+
let result: Awaitable<T>;
80+
try {
81+
result = run();
82+
} catch (error) {
83+
tighten();
84+
throw error;
85+
}
86+
if (result instanceof Promise) return result.finally(tighten);
87+
tighten();
88+
return result;
89+
}
90+
91+
/** Opens Clerk token storage in a private directory and keeps the file at `0600`. */
92+
export function openClerkTokenStorage(
93+
stateDir: string,
94+
createStorage: (directory: string) => ClerkTokenStorage,
95+
userDataPath = ".",
96+
): ClerkTokenStorage {
97+
const absoluteStateDir = NodePath.resolve(stateDir);
98+
const directory = NodePath.join(absoluteStateDir, CLERK_TOKEN_DIRECTORY_NAME);
99+
NodeFS.mkdirSync(directory, { recursive: true, mode: CLERK_TOKEN_DIRECTORY_MODE });
100+
const stat = NodeFS.lstatSync(directory);
101+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
102+
throw new Error(`Clerk token directory "${directory}" must be a real directory.`);
103+
}
104+
NodeFS.chmodSync(directory, CLERK_TOKEN_DIRECTORY_MODE);
105+
// electron-store rebases relative paths under Electron userData, not the working directory.
106+
migrateLegacyClerkTokenFile(NodePath.resolve(userDataPath, stateDir), directory);
107+
const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
108+
restrictClerkTokenFile(tokenPath);
109+
110+
const storage = createStorage(directory);
111+
const tighten = () => restrictClerkTokenFile(tokenPath);
112+
return {
113+
getItem: (key) => restrictAfter(() => storage.getItem(key), tighten),
114+
setItem: (key, value) => restrictAfter(() => storage.setItem(key, value), tighten),
115+
removeItem: (key) => restrictAfter(() => storage.removeItem(key), tighten),
116+
};
117+
}

0 commit comments

Comments
 (0)