Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
da0add7
fix(issues): honor native actions and shared rate limits
Bil0000 Oct 14, 2026
8f8c677
feat(issues): add Jira Cloud and complete tracker workflows
Bil0000 Oct 14, 2026
da435c7
test(issues): include verified host migration in upgrade checks
Bil0000 Oct 14, 2026
0bcaacf
fix(issues): preserve bindings only for stable account IDs
Bil0000 Oct 14, 2026
dd2f147
fix(jira): preserve ordered list start values
Bil0000 Oct 14, 2026
fcba260
fix(jira): page older comments after the newest embedded page
Bil0000 Oct 14, 2026
211699e
fix(github): report truncated issue history
Bil0000 Oct 14, 2026
30e679b
fix(mcp): unlink saved issue refs without tracker reads
Bil0000 Oct 14, 2026
27f083f
fix(issues): keep tracker refreshes from changing thread activity
Bil0000 Oct 14, 2026
2ef51ec
fix(issues): preserve unknown comment totals and pull request states
Bil0000 Oct 14, 2026
553e2db
fix(issues): resolve native standalone tracker URLs
Bil0000 Oct 14, 2026
0d9db1d
fix(web): scope issue project filters to their environment
Bil0000 Oct 14, 2026
fb9d99c
fix(issues): key comment requests by their full identity
Bil0000 Oct 14, 2026
4c6f8d3
fix(issues): preserve native workspace and project scope identity
Bil0000 Oct 14, 2026
b5fec09
fix(issues): lend Linear reads within the bound workspace
Bil0000 Oct 14, 2026
03c6675
feat(mcp): page older issue comments with native cursors
Bil0000 Oct 14, 2026
91c4246
fix(azure): report partial history from raw update counts
Bil0000 Oct 14, 2026
735764e
fix(github): decode issue template config as YAML
Bil0000 Oct 14, 2026
4be8aad
fix(linear): scope quota pauses to native users and endpoints
Bil0000 Oct 14, 2026
fe6cc2a
fix(linear): retain archived labels and prefer native assignee IDs
Bil0000 Oct 14, 2026
e439e0e
fix(issues): preserve custom ports in issue URL identity
Bil0000 Oct 14, 2026
2016594
fix(linear): handle duplicate states and exclude label groups
Bil0000 Oct 14, 2026
30685a9
fix(gitlab): keep HTTP locator ports in issue source hosts
Bil0000 Oct 14, 2026
8ce9aef
fix(azure): decode legacy native work item URLs
Bil0000 Oct 14, 2026
5f979dd
fix(forgejo): retain and discover organization issue labels
Bil0000 Oct 14, 2026
aa2cf39
fix(jira): read foreign project URLs on the bound site
Bil0000 Oct 14, 2026
7a47fc9
fix(linear): reject ambiguous assignee fallbacks
Bil0000 Oct 14, 2026
70a64e1
fix(forgejo): check ownership and bound reaction reads
Bil0000 Oct 14, 2026
91761d3
fix(web): keep issue refreshes within filters
Bil0000 Oct 14, 2026
aae7fdc
fix(web): retain the correct issue comment window
Bil0000 Oct 14, 2026
5754e94
fix(issues): honor native comment reaction limits
Bil0000 Oct 14, 2026
97c395d
fix(web): retain confirmed issue reactions
Bil0000 Oct 14, 2026
0293ad0
fix(issues): resolve chat URLs through the server
Bil0000 Oct 14, 2026
81a63fd
fix(issues): stop empty snapshot refresh loops
Bil0000 Oct 14, 2026
ae7488a
fix(issues): preserve full native label sets
Bil0000 Oct 14, 2026
9b05a88
fix(forgejo): preserve issue and comment paging
Bil0000 Oct 14, 2026
cf7ba6b
fix(github): verify issue metadata writes
Bil0000 Oct 14, 2026
f8b7158
fix(gitlab): pin issue requests to the selected host
Bil0000 Oct 14, 2026
731471b
fix(jira): read full native issue label sets
Bil0000 Oct 14, 2026
10f3abd
fix(issues): clear bindings after failed credential rollback
Bil0000 Oct 14, 2026
7755b9f
test(web): supply thread shell in markdown mocks
Bil0000 Oct 14, 2026
6ed754e
fix(web): resolve issue panels in their owning environment
Bil0000 Oct 14, 2026
b1ec3c0
fix(auth): publish new secrets atomically
Bil0000 Oct 14, 2026
e4ef905
fix(issues): preserve selected native Linear IDs
Bil0000 Oct 14, 2026
054612d
fix(issues): qualify list keys by tracker host
Bil0000 Oct 14, 2026
4b971c4
fix(web): retain confirmed issue picker sets
Bil0000 Oct 14, 2026
ef7c52f
fix(linear): avoid reads for static issue permissions
Bil0000 Oct 14, 2026
03b42d7
fix(issues): retain cursors for legacy server errors
Bil0000 Oct 14, 2026
0302e21
fix(linear): retain inherited team labels
Bil0000 Oct 14, 2026
53f87bc
fix(issues): reset pages when source owner changes
Bil0000 Oct 14, 2026
f24ef0e
fix(issues): refresh source lists after each write
Bil0000 Oct 14, 2026
4d76aec
fix(web): match issue create sources by tracker
Bil0000 Oct 14, 2026
376e7ab
fix(linear): skip unused ancestor label reads
Bil0000 Oct 14, 2026
65a711a
fix(web): show standalone issue links in issues panel
Bil0000 Oct 14, 2026
6de241f
fix(issues): rebind continued rows to current project
Bil0000 Oct 14, 2026
e06f26c
fix(github): reserve issue quota for interactive writes
Bil0000 Oct 14, 2026
e639191
fix(contracts): accept optional issue comment viewers
Bil0000 Oct 14, 2026
bb64956
fix(issues): guard linked snapshots by generation
Bil0000 Oct 14, 2026
0d3324d
fix(issues): reuse known viewers for comment pages
Bil0000 Oct 14, 2026
14bae9b
fix(forgejo): continue after headerless comment slices
Bil0000 Oct 14, 2026
ddf6ee2
fix(github): allow native app metadata authorization
Bil0000 Oct 14, 2026
b5463ee
fix(web): keep unresolved issue links available
Bil0000 Oct 14, 2026
0a44102
fix(issues): restore cursors after exhausted refresh
Bil0000 Oct 14, 2026
3328082
fix(jira): use native endpoint for assignee changes
Bil0000 Oct 14, 2026
b26973b
fix(forgejo): keep linked pull request mount identity
Bil0000 Oct 14, 2026
9949a4b
fix(web): create issues through current source owners
Bil0000 Oct 14, 2026
f348df2
test(mcp): include viewer in issue schema fixture
Bil0000 Oct 14, 2026
470b371
fix(mcp): retain native issue label details
Bil0000 Oct 14, 2026
4c082ef
fix(web): retain the chosen issue create destination
Bil0000 Oct 14, 2026
ae0d9e6
fix(web): reset issue reactions when viewer changes
Bil0000 Oct 14, 2026
1ce727a
fix(github): report incomplete issue label lists
Bil0000 Oct 14, 2026
7ac49ed
fix(github): browse issues with native repository pages
Bil0000 Oct 14, 2026
ddaccfc
fix(github): enforce the native issue assignee limit
Bil0000 Oct 14, 2026
2be896d
fix(web): show partial issue environment failures
Bil0000 Oct 14, 2026
33b05af
fix(web): keep project inventory across host filters
Bil0000 Oct 14, 2026
3a2ca9f
fix(web): resolve issue links in their source environment
Bil0000 Oct 14, 2026
733453a
fix(web): use supported linked pull request targets
Bil0000 Oct 14, 2026
790a647
fix(web): hide state category actions that do nothing
Bil0000 Oct 14, 2026
a271488
fix(issues): load GitLab template bodies on selection
Bil0000 Oct 14, 2026
936f42a
fix(web): bind held issue comments to their viewer
Bil0000 Oct 14, 2026
a416146
fix(forgejo): match native label names by case fold
Bil0000 Oct 14, 2026
dec0405
fix(issues): compare Jira and Linear keys by native case
Bil0000 Oct 15, 2026
eaaa127
fix(web): load selected issue templates and keep typed drafts
Bil0000 Oct 15, 2026
62c1feb
fix(mcp): unlink saved native issue keys by their case rules
Bil0000 Oct 15, 2026
7eb3007
fix(issues): bind held list pages to each source viewer
Bil0000 Oct 15, 2026
566948c
fix(forgejo): match canonical comment repository names
Bil0000 Oct 15, 2026
22846d5
test(web): add viewers to the issue list fixture
Bil0000 Oct 15, 2026
916e1d3
fix(github): offer the native issue template chooser
Bil0000 Oct 15, 2026
23adc28
fix(linear): keep issue key searches within their team
Bil0000 Oct 15, 2026
81e55fe
fix(web): check issue panel support in its environment
Bil0000 Oct 15, 2026
6eec87a
fix(web): bind confirmed issue picker sets to the viewer
Bil0000 Oct 15, 2026
8c67513
fix(gitlab): read native issue awards and newest notes
Bil0000 Oct 15, 2026
03ab662
fix(web): check pull request support in its environment
Bil0000 Oct 15, 2026
60683ab
fix(issues): restart changed viewers from the first page
Bil0000 Oct 15, 2026
f2fdcba
fix(github): honor archived repositories and locked issues
Bil0000 Oct 15, 2026
6359833
fix(gitlab): use native issue permissions and verify metadata
Bil0000 Oct 15, 2026
f2bfdd2
fix(web): refresh issues over the latest held list
Bil0000 Oct 15, 2026
69254a5
fix(issues): isolate Forgejo installations and accounts
Bil0000 Oct 15, 2026
09dcbe1
fix(web): preserve issue link fallback settings
Bil0000 Oct 15, 2026
5191583
fix(gitlab): report omitted create assignees
Bil0000 Oct 15, 2026
3faaeec
fix(issues): honor native body and state metadata
Bil0000 Oct 15, 2026
746a8cc
fix(jira): protect rich descriptions from text edits
Bil0000 Oct 15, 2026
21ac56f
fix(azure): use native work item workflows
Bil0000 Oct 15, 2026
be2702e
fix(azure): write description format with its body
Bil0000 Oct 15, 2026
fe7d5a2
fix(azure): classify issue lists by native workflows
Bil0000 Oct 15, 2026
edbbadf
fix(web): show saved issues outside Git folders
Bil0000 Oct 15, 2026
fdba1bc
fix(web): keep title Escape on the issues page
Bil0000 Oct 15, 2026
7c30147
fix(web): key create choices by full issue scope
Bil0000 Oct 15, 2026
22edec1
fix(web): handle the advertised issues panel shortcut
Bil0000 Oct 15, 2026
b3dc931
fix(web): restore reopened issue selection in the URL
Bil0000 Oct 15, 2026
87ecf9a
fix(web): keep comment drafts on Escape
Bil0000 Oct 15, 2026
9040e3c
fix(web): key issue rows and errors by identity
Bil0000 Oct 15, 2026
776c2f5
fix(web): close the active issue tab with its shortcut
Bil0000 Oct 15, 2026
e0f9443
fix(web): use native issue labels in panel tabs
Bil0000 Oct 15, 2026
3b39adc
fix(web): keep issue updates out of navigation history
Bil0000 Oct 15, 2026
bfa365b
fix(web): place saved issue rows in Workspace
Bil0000 Oct 15, 2026
2ac8a5b
fix(web): key PR panels by their surface identity
Bil0000 Oct 15, 2026
cd6627d
fix(web): recognize resolved Forgejo web hosts
Bil0000 Oct 15, 2026
1a93376
fix(web): trust fresh issue reads with unknown viewers
Bil0000 Oct 15, 2026
606ba96
fix(web): use source environment for PR links
Bil0000 Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@napi-rs/keyring": "^1.3.0",
"@opencode-ai/sdk": "^1.3.15",
"@t3tools/issue-tracker-core": "workspace:*",
"@t3tools/issue-tracker-jira": "workspace:*",
"@t3tools/issue-tracker-linear": "workspace:*",
"@t3tools/provider-acp": "workspace:*",
"@t3tools/provider-acp-registry": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe("RPC authorization scopes", () => {
for (const method of [
WS_METHODS.issuesList,
WS_METHODS.issuesDetail,
WS_METHODS.issuesResolveUrl,
WS_METHODS.issuesComments,
WS_METHODS.issuesTemplates,
WS_METHODS.issuesCandidates,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.pullRequestsDetail]: AuthOrchestrationReadScope,
[WS_METHODS.issuesList]: AuthOrchestrationReadScope,
[WS_METHODS.issuesDetail]: AuthOrchestrationReadScope,
[WS_METHODS.issuesResolveUrl]: AuthOrchestrationReadScope,
[WS_METHODS.issuesComments]: AuthOrchestrationReadScope,
[WS_METHODS.issuesTemplates]: AuthOrchestrationReadScope,
[WS_METHODS.issuesCandidates]: AuthOrchestrationReadScope,
Expand Down
121 changes: 64 additions & 57 deletions apps/server/src/auth/ServerSecretStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";

import * as ServerConfig from "../config.ts";
Expand Down Expand Up @@ -98,52 +98,6 @@ const layerRemoveFailureSecretStore = () =>
Layer.provideMerge(layerRemoveFailureFileSystem),
);

const layerConcurrentReadMissFileSystem = Layer.effect(
FileSystem.FileSystem,
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const readCountRef = yield* Ref.make(0);
const readBarrier = yield* Deferred.make<void>();

return {
...fileSystem,
readFile: (path) =>
/[\\/]session-signing-key\.bin$/.test(String(path))
? Ref.updateAndGet(readCountRef, (count) => count + 1).pipe(
Effect.flatMap((count) => {
if (count > 2) {
return fileSystem.readFile(path);
}
return Effect.gen(function* () {
if (count === 2) {
yield* Deferred.succeed(readBarrier, void 0);
}
yield* Deferred.await(readBarrier);
return yield* Effect.failCause(
Cause.fail(
PlatformError.systemError({
_tag: "NotFound",
module: "FileSystem",
method: "readFile",
pathOrDescriptor: String(path),
description: "Secret file does not exist yet.",
}),
),
);
});
}),
)
: fileSystem.readFile(path),
} satisfies FileSystem.FileSystem;
}),
).pipe(Layer.provide(NodeServices.layer));

const layerConcurrentCreateSecretStore = () =>
ServerSecretStore.layer.pipe(
Layer.provide(layerServerConfig()),
Layer.provideMerge(layerConcurrentReadMissFileSystem),
);

it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
it.effect("returns Option.none when a secret file does not exist", () =>
Effect.gen(function* () {
Expand All @@ -168,21 +122,74 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {

it.effect("returns the persisted secret when concurrent creators race", () =>
Effect.gen(function* () {
const secretStore = yield* ServerSecretStore.ServerSecretStore;

const [first, second] = yield* Effect.all(
[
secretStore.getOrCreateRandom("session-signing-key", 32),
secretStore.getOrCreateRandom("session-signing-key", 32),
],
{ concurrency: "unbounded" },
const config = yield* ServerConfig.ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const writeStarted = yield* Deferred.make<void>();
const releaseWrite = yield* Deferred.make<void>();
const secretPath = path.join(config.secretsDir, "session-signing-key.bin");
const tempPrefix = path.join(config.secretsDir, "session-signing-key.");
let pauseNextWrite = true;
const secretStore = yield* ServerSecretStore.make.pipe(
Effect.provideService(FileSystem.FileSystem, {
...fileSystem,
open: (filePath, options) =>
fileSystem.open(filePath, options).pipe(
Effect.map((file) =>
filePath === secretPath || filePath.startsWith(tempPrefix)
? {
...file,
sync: file.sync,
writeAll: (bytes) =>
Effect.gen(function* () {
if (pauseNextWrite) {
pauseNextWrite = false;
yield* Deferred.succeed(writeStarted, void 0);
yield* Deferred.await(releaseWrite);
}
yield* file.writeAll(bytes);
}),
}
: file,
),
),
}),
);
const firstCreator = yield* secretStore
.getOrCreateRandom("session-signing-key", 32)
.pipe(Effect.forkChild);
yield* Deferred.await(writeStarted);
const second = yield* secretStore.getOrCreateRandom("session-signing-key", 32);
yield* Deferred.succeed(releaseWrite, void 0);
const first = yield* Fiber.join(firstCreator);
const persisted = yield* secretStore.get("session-signing-key");
const persistedBytes = Option.getOrThrow(persisted);

assert.lengthOf(persistedBytes, 32);
assert.deepEqual(Array.from(first), Array.from(persistedBytes));
assert.deepEqual(Array.from(second), Array.from(persistedBytes));
}).pipe(Effect.provide(layerConcurrentCreateSecretStore())),
assert.deepEqual(yield* fileSystem.readDirectory(config.secretsDir), [
"session-signing-key.bin",
]);
}).pipe(Effect.provide(layerServerConfig())),
);

it.effect("keeps an existing secret and removes unpublished files after a create collision", () =>
Effect.gen(function* () {
const secretStore = yield* ServerSecretStore.ServerSecretStore;
const config = yield* ServerConfig.ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;
const value = Uint8Array.from([1, 2, 3]);
yield* secretStore.create("existing-secret", value);

const error = yield* secretStore
.create("existing-secret", Uint8Array.from([4]))
.pipe(Effect.flip);

assert.isTrue(ServerSecretStore.isSecretAlreadyExistsError(error));
assert.deepEqual(Option.getOrThrow(yield* secretStore.get("existing-secret")), value);
assert.deepEqual(yield* fileSystem.readDirectory(config.secretsDir), ["existing-secret.bin"]);
}).pipe(Effect.provide(ServerSecretStore.layer.pipe(Layer.provideMerge(layerServerConfig())))),
);

it.effect("uses restrictive permissions for the secret directory and files", () =>
Expand Down
11 changes: 8 additions & 3 deletions apps/server/src/auth/ServerSecretStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,18 @@ export const make = Effect.gen(function* () {
const secretPath = resolveSecretPath(name);
return Effect.scoped(
Effect.gen(function* () {
const file = yield* fileSystem.open(secretPath, {
flag: "wx",
const tempPath = yield* fileSystem.makeTempFileScoped({
directory: serverConfig.secretsDir,
prefix: `${name}.`,
});
const file = yield* fileSystem.open(tempPath, {
flag: "w",
mode: 0o600,
});
yield* fileSystem.chmod(tempPath, 0o600);
yield* file.writeAll(value);
yield* file.sync;
yield* fileSystem.chmod(secretPath, 0o600);
yield* fileSystem.link(tempPath, secretPath);
}),
).pipe(
Effect.mapError(
Expand Down
Loading
Loading