From eeacdc51ffb21dbe5ad3c01d426146a3fa4a3994 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 07:01:20 -0700 Subject: [PATCH] refactor(assets): add structured access context Co-authored-by: codex --- apps/server/src/assets/AssetAccess.test.ts | 16 ++++++++ apps/server/src/assets/AssetAccess.ts | 44 ++++++++++++++++++++-- apps/server/src/ws.ts | 8 ++++ packages/contracts/src/assets.ts | 18 +++++++++ 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index 836d7a29ea1..7df2e3361c8 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -87,6 +87,14 @@ describe("AssetAccess", () => { workspaceRoot: root, }).pipe(Effect.flip); expect(error.message).toBe("Workspace file path must be relative to the project root."); + expect(error).toMatchObject({ + operation: "validate-workspace-path", + resource: { + _tag: "workspace-file", + threadId: "thread-1", + path: htmlPath, + }, + }); expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError); }).pipe(Effect.provide(testLayer)), ); @@ -121,6 +129,14 @@ describe("AssetAccess", () => { }).pipe(Effect.provideService(FileSystem.FileSystem, failingFileSystem), Effect.flip); expect(error.message).toBe("Failed to inspect the workspace asset."); + expect(error).toMatchObject({ + operation: "inspect-workspace-asset", + resource: { + _tag: "workspace-file", + threadId: "thread-1", + path: htmlPath, + }, + }); expect(error.cause).toBe(cause); }).pipe(Effect.provide(testLayer)), ); diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index 22449be862e..f7be262b41a 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -165,12 +165,18 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i switch (input.resource._tag) { case "workspace-file": { if (!input.workspaceRoot) { - return yield* new AssetAccessError({ message: "Workspace context was not found." }); + return yield* new AssetAccessError({ + operation: "resolve-workspace-context", + resource: input.resource, + message: "Workspace context was not found.", + }); } const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.workspaceRoot).pipe( Effect.mapError( (cause) => new AssetAccessError({ + operation: "normalize-workspace-root", + resource: input.resource, message: "Failed to normalize the workspace root.", cause, }), @@ -185,6 +191,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "validate-workspace-path", + resource: input.resource, message: "Workspace file path must be relative to the project root.", cause, }), @@ -192,6 +200,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i ); if (!isWorkspacePreviewEntryPath(resolved.relativePath)) { return yield* new AssetAccessError({ + operation: "validate-preview-type", + resource: input.resource, message: "Only browser documents and images can be previewed.", }); } @@ -202,18 +212,26 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "inspect-workspace-asset", + resource: input.resource, message: "Failed to inspect the workspace asset.", cause, }), ), ); if (!canonicalFile) { - return yield* new AssetAccessError({ message: "Workspace asset was not found." }); + return yield* new AssetAccessError({ + operation: "locate-workspace-asset", + resource: input.resource, + message: "Workspace asset was not found.", + }); } const canonicalWorkspaceRoot = yield* fileSystem.realPath(workspaceRoot).pipe( Effect.mapError( (cause) => new AssetAccessError({ + operation: "resolve-workspace", + resource: input.resource, message: "Failed to resolve workspace.", cause, }), @@ -244,7 +262,11 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i attachmentId: input.resource.attachmentId, }); if (!attachmentPath) { - return yield* new AssetAccessError({ message: "Attachment was not found." }); + return yield* new AssetAccessError({ + operation: "locate-attachment", + resource: input.resource, + message: "Attachment was not found.", + }); } claims = { version: 1, @@ -260,6 +282,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "normalize-workspace-root", + resource: input.resource, message: "Failed to normalize the workspace root.", cause, }), @@ -270,6 +294,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "resolve-project-favicon", + resource: input.resource, message: "Failed to resolve project favicon.", cause, }), @@ -282,13 +308,19 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "inspect-project-favicon", + resource: input.resource, message: "Failed to inspect the project favicon.", cause, }), ), )) ) { - return yield* new AssetAccessError({ message: "Project favicon was not found." }); + return yield* new AssetAccessError({ + operation: "locate-project-favicon", + resource: input.resource, + message: "Project favicon was not found.", + }); } claims = { version: 1, @@ -297,6 +329,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "resolve-workspace", + resource: input.resource, message: "Failed to resolve workspace.", cause, }), @@ -315,6 +349,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i Effect.mapError( (cause) => new AssetAccessError({ + operation: "load-signing-key", + resource: input.resource, message: "Failed to load the asset signing key.", cause, }), diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 03b609ddcfe..40755bb1244 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1341,6 +1341,8 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => Effect.mapError( (cause) => new AssetAccessError({ + operation: "resolve-workspace-context", + resource: input.resource, message: "Failed to resolve workspace context.", cause, }), @@ -1348,6 +1350,8 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => ); if (Option.isNone(thread)) { return yield* new AssetAccessError({ + operation: "resolve-workspace-context", + resource: input.resource, message: "Workspace context was not found.", }); } @@ -1357,6 +1361,8 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => Effect.mapError( (cause) => new AssetAccessError({ + operation: "resolve-workspace-context", + resource: input.resource, message: "Failed to resolve workspace context.", cause, }), @@ -1364,6 +1370,8 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => ); if (Option.isNone(project)) { return yield* new AssetAccessError({ + operation: "resolve-workspace-context", + resource: input.resource, message: "Workspace context was not found.", }); } diff --git a/packages/contracts/src/assets.ts b/packages/contracts/src/assets.ts index bd1ac0a53ec..fdfbe64246e 100644 --- a/packages/contracts/src/assets.ts +++ b/packages/contracts/src/assets.ts @@ -29,9 +29,27 @@ export const AssetCreateUrlResult = Schema.Struct({ }); export type AssetCreateUrlResult = typeof AssetCreateUrlResult.Type; +export const AssetAccessOperation = Schema.Literals([ + "resolve-workspace-context", + "normalize-workspace-root", + "validate-workspace-path", + "validate-preview-type", + "inspect-workspace-asset", + "locate-workspace-asset", + "resolve-workspace", + "locate-attachment", + "resolve-project-favicon", + "inspect-project-favicon", + "locate-project-favicon", + "load-signing-key", +]); +export type AssetAccessOperation = typeof AssetAccessOperation.Type; + export class AssetAccessError extends Schema.TaggedErrorClass()( "AssetAccessError", { + operation: AssetAccessOperation, + resource: AssetResource, message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), },