chore: merge develop into hare/develop

# Conflicts:
#	crates/client/src/lib.rs
#	web/workspace/deno.json
This commit is contained in:
2026-09-01 09:15:10 +09:00
47 changed files with 4128 additions and 1035 deletions
@@ -0,0 +1,133 @@
import {
parseRepositoryAccessProjection,
parseRepositorySshCredentials,
parseRepositorySshHostTrusts,
RepositoryAccessSchemaError,
} from "../../src/lib/workspace/api/repository-access.ts";
function assertEquals(actual: unknown, expected: unknown): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
);
}
}
function assertSchemaError(body: () => unknown, path: string): void {
try {
body();
} catch (error) {
if (!(error instanceof RepositoryAccessSchemaError)) {
throw error;
}
if (!error.message.includes(path)) {
throw new Error(
`expected schema error path ${path}, got ${error.message}`,
);
}
return;
}
throw new Error(`expected RepositoryAccessSchemaError for ${path}`);
}
const credential = {
credential_id: "deploy-key",
workspace_id: "workspace-1",
name: "Deploy key",
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:credential",
current_revision: 2,
status: "active",
created_at: "2026-09-01T00:00:00Z",
rotated_at: null,
referenced_repositories: ["main"],
};
const hostTrust = {
host_trust_id: "gitea",
workspace_id: "workspace-1",
hostname: "gitea.example.test",
port: 22,
key_algorithm: "ssh-ed25519",
host_key: "ssh-ed25519 AAAA",
fingerprint: "SHA256:host",
current_revision: 3,
created_at: "2026-09-01T00:00:00Z",
updated_at: "2026-09-02T00:00:00Z",
referenced_repositories: ["main"],
};
Deno.test("Repository Access parsers accept generated response contracts", () => {
assertEquals(parseRepositorySshCredentials([credential]), [credential]);
assertEquals(parseRepositorySshHostTrusts([hostTrust]), [hostTrust]);
assertEquals(
parseRepositoryAccessProjection({
workspace_id: "workspace-1",
config_revision: 4,
projection_digest: "sha256:projection",
bindings: [{
repository_id: "main",
credential_id: "deploy-key",
host_trust_id: "gitea",
access: "read_only",
}],
}),
{
workspace_id: "workspace-1",
config_revision: 4,
projection_digest: "sha256:projection",
bindings: [{
repository_id: "main",
credential_id: "deploy-key",
host_trust_id: "gitea",
access: "read_only",
}],
},
);
});
Deno.test("Repository Access parsers reject malformed list responses", () => {
assertSchemaError(
() => parseRepositorySshCredentials({ credentials: [credential] }),
"credentials",
);
assertSchemaError(
() => parseRepositorySshHostTrusts({ host_trusts: [hostTrust] }),
"host_trusts",
);
});
Deno.test("Repository Access parsers reject missing and wrong-typed fields", () => {
const { current_revision: _revision, ...missingRevision } = credential;
assertSchemaError(
() => parseRepositorySshCredentials([missingRevision]),
"credentials[0].current_revision",
);
assertSchemaError(
() => parseRepositorySshHostTrusts([{ ...hostTrust, port: "22" }]),
"host_trusts[0].port",
);
assertSchemaError(
() =>
parseRepositoryAccessProjection({
workspace_id: "workspace-1",
config_revision: 4,
projection_digest: "sha256:projection",
bindings: [{
repository_id: "main",
credential_id: "deploy-key",
host_trust_id: "gitea",
access: "admin",
}],
}),
"access_projection.bindings[0].access",
);
});
Deno.test("Repository Access parsers reject unknown response fields", () => {
assertSchemaError(
() =>
parseRepositorySshCredentials([{ ...credential, private_key: "secret" }]),
"credentials[0].private_key",
);
});
@@ -0,0 +1,114 @@
import { loadRepositoryAccessJson } from "../../src/lib/workspace/api/repository-access-loader.ts";
import { RepositoryAccessSchemaError } from "../../src/lib/workspace/api/repository-access.ts";
type HttpFailure = { status?: number; body?: { message?: string } };
async function captureHttpFailure(
run: () => Promise<unknown>,
expectedStatus: number,
expectedMessage: string,
): Promise<HttpFailure> {
try {
await run();
} catch (error) {
const failure = error as HttpFailure;
if (failure.status !== expectedStatus) {
throw new Error(
`expected bounded ${expectedStatus}, got ${String(failure.status)}`,
);
}
if (failure.body?.message !== expectedMessage) {
throw new Error(
`unexpected bounded error: ${JSON.stringify(failure.body)}`,
);
}
return failure;
}
throw new Error(`expected bounded ${expectedStatus} error`);
}
for (const status of [401, 403]) {
Deno.test(`Repository Access loader maps ${status} to bounded permission unavailable`, async () => {
let requests = 0;
await captureHttpFailure(
() =>
loadRepositoryAccessJson(
() => {
requests += 1;
return Promise.resolve(new Response(null, { status }));
},
"/api/w/workspace-1/settings/repository-access",
(value) => value,
),
403,
"Repository Access is unavailable for this account.",
);
if (requests !== 1) {
throw new Error(`expected one bounded request, got ${requests}`);
}
});
}
Deno.test("Repository Access loader maps invalid JSON to safe bounded 502", async () => {
const upstreamSecret = "private-key-must-not-leak";
const failure = await captureHttpFailure(
() =>
loadRepositoryAccessJson(
() =>
Promise.resolve(
new Response(upstreamSecret, {
status: 200,
headers: { "content-type": "application/json" },
}),
),
"/api/w/workspace-1/settings/repository-access/credentials",
(value) => value,
),
502,
"Repository Access returned an invalid JSON response.",
);
if (JSON.stringify(failure.body).includes(upstreamSecret)) {
throw new Error("invalid JSON error exposed upstream response content");
}
});
Deno.test("Repository Access loader maps schema mismatch to explicit bounded 502", async () => {
const failure = await captureHttpFailure(
() =>
loadRepositoryAccessJson(
() => Promise.resolve(Response.json({ stale: true })),
"/api/w/workspace-1/settings/repository-access/credentials",
() => {
throw new RepositoryAccessSchemaError("credentials", "an array");
},
),
502,
"Repository Access response schema mismatch at credentials: expected an array",
);
if (!failure.body?.message?.includes("credentials")) {
throw new Error("schema mismatch error omitted the failing response path");
}
});
Deno.test("Repository Access loader never exposes failed upstream response bodies", async () => {
const upstreamSecret = "secret-ref-must-not-leak";
const failure = await captureHttpFailure(
() =>
loadRepositoryAccessJson(
() =>
Promise.resolve(
Response.json(
{ message: upstreamSecret, secret_ref: upstreamSecret },
{ status: 500 },
),
),
"/api/w/workspace-1/settings/repository-access/host-trusts",
(value) => value,
),
502,
"Repository Access request failed with status 500.",
);
if (JSON.stringify(failure.body).includes(upstreamSecret)) {
throw new Error("bounded upstream error exposed response content");
}
});
@@ -13,6 +13,58 @@ const source = await Deno.readTextFile(
import.meta.url,
),
);
const loaderSource = await Deno.readTextFile(
new URL(
"../../src/routes/w/[workspaceId]/settings/repository-access/+page.ts",
import.meta.url,
),
);
test("Repository Access Web code consumes workspace-api generated DTOs", () => {
assert(
source.includes("$lib/generated/repository-access-api"),
"mutation code should import generated request and response contracts",
);
assert(
loaderSource.includes("parseRepositorySshCredentials") &&
loaderSource.includes("parseRepositorySshHostTrusts") &&
loaderSource.includes("parseRepositoryAccessProjection"),
"loader should validate unknown JSON before exposing generated DTOs to Svelte",
);
assert(
loaderSource.indexOf('"/settings/repository-access"') <
loaderSource.indexOf("Promise.all"),
"loader should check Repository Access permission before starting list preloads",
);
for (
const duplicate of [
"interface RepositorySshCredential",
"interface RepositorySshHostTrust",
"interface RepositoryAccessProjection",
]
) {
assert(
!loaderSource.includes(duplicate) && !source.includes(duplicate),
`Web code must not redeclare ${duplicate}`,
);
}
});
test("Repository Access renders the shared access projection fields", () => {
for (
const field of [
"accessProjection.config_revision",
"accessProjection.projection_digest",
"accessProjection.bindings",
"binding.repository_id",
"binding.credential_id",
"binding.host_trust_id",
"binding.access",
]
) {
assert(source.includes(field), `missing access projection field ${field}`);
}
});
test("Repository credential submissions clear write-only fields in finally blocks", () => {
const createStart = source.indexOf("async function createCredential()");