| author | |
| committer | |
| log | 0eba548282b96ee420ad0b1a3f5d9d4487376b90 |
| tree | d831a177719d88b2ce1fd2c161b870ad90f7199c |
| parent | 68c0dd3b7095cae054a9db5e48157e89f65c0664 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>4 files changed, 200 insertions(+), 79 deletions(-)
apps/server/src/assets/AssetAccess.test.ts+3-2| ... | ... | @@ -88,7 +88,7 @@ describe("AssetAccess", () => { |
| 88 | 88 | }).pipe(Effect.flip); |
| 89 | 89 | expect(error.message).toBe("Workspace file path must be relative to the project root."); |
| 90 | 90 | expect(error).toMatchObject({ |
| 91 | operation: "validate-workspace-path", | |
| 91 | _tag: "AssetWorkspacePathValidationError", | |
| 92 | 92 | resource: { |
| 93 | 93 | _tag: "workspace-file", |
| 94 | 94 | threadId: "thread-1", |
| ... | ... | @@ -130,7 +130,7 @@ describe("AssetAccess", () => { |
| 130 | 130 | |
| 131 | 131 | expect(error.message).toBe("Failed to inspect the workspace asset."); |
| 132 | 132 | expect(error).toMatchObject({ |
| 133 | operation: "inspect-workspace-asset", | |
| 133 | _tag: "AssetWorkspaceAssetInspectionError", | |
| 134 | 134 | resource: { |
| 135 | 135 | _tag: "workspace-file", |
| 136 | 136 | threadId: "thread-1", |
| ... | ... | @@ -268,6 +268,7 @@ describe("AssetAccess", () => { |
| 268 | 268 | ); |
| 269 | 269 | |
| 270 | 270 | expect(error.message).toBe("Failed to resolve project favicon."); |
| 271 | expect(error._tag).toBe("AssetProjectFaviconResolutionError"); | |
| 271 | 272 | expect(error.cause).toBe(resolutionCause); |
| 272 | 273 | }).pipe(Effect.provide(testLayer)), |
| 273 | 274 | ); |
apps/server/src/assets/AssetAccess.ts+28-43| ... | ... | @@ -1,5 +1,18 @@ |
| 1 | 1 | import type { AssetResource } from "@t3tools/contracts"; |
| 2 | import { AssetAccessError } from "@t3tools/contracts"; | |
| 2 | import { | |
| 3 | AssetAttachmentNotFoundError, | |
| 4 | AssetPreviewTypeValidationError, | |
| 5 | AssetProjectFaviconInspectionError, | |
| 6 | AssetProjectFaviconNotFoundError, | |
| 7 | AssetProjectFaviconResolutionError, | |
| 8 | AssetSigningKeyLoadError, | |
| 9 | AssetWorkspaceAssetInspectionError, | |
| 10 | AssetWorkspaceAssetNotFoundError, | |
| 11 | AssetWorkspaceContextNotFoundError, | |
| 12 | AssetWorkspacePathValidationError, | |
| 13 | AssetWorkspaceResolutionError, | |
| 14 | AssetWorkspaceRootNormalizationError, | |
| 15 | } from "@t3tools/contracts"; | |
| 3 | 16 | import { |
| 4 | 17 | isWorkspaceImagePreviewPath, |
| 5 | 18 | isWorkspacePreviewEntryPath, |
| ... | ... | @@ -165,19 +178,15 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 165 | 178 | switch (input.resource._tag) { |
| 166 | 179 | case "workspace-file": { |
| 167 | 180 | if (!input.workspaceRoot) { |
| 168 | return yield* new AssetAccessError({ | |
| 169 | operation: "resolve-workspace-context", | |
| 181 | return yield* new AssetWorkspaceContextNotFoundError({ | |
| 170 | 182 | resource: input.resource, |
| 171 | message: "Workspace context was not found.", | |
| 172 | 183 | }); |
| 173 | 184 | } |
| 174 | 185 | const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.workspaceRoot).pipe( |
| 175 | 186 | Effect.mapError( |
| 176 | 187 | (cause) => |
| 177 | new AssetAccessError({ | |
| 178 | operation: "normalize-workspace-root", | |
| 188 | new AssetWorkspaceRootNormalizationError({ | |
| 179 | 189 | resource: input.resource, |
| 180 | message: "Failed to normalize the workspace root.", | |
| 181 | 190 | cause, |
| 182 | 191 | }), |
| 183 | 192 | ), |
| ... | ... | @@ -190,19 +199,15 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 190 | 199 | .pipe( |
| 191 | 200 | Effect.mapError( |
| 192 | 201 | (cause) => |
| 193 | new AssetAccessError({ | |
| 194 | operation: "validate-workspace-path", | |
| 202 | new AssetWorkspacePathValidationError({ | |
| 195 | 203 | resource: input.resource, |
| 196 | message: "Workspace file path must be relative to the project root.", | |
| 197 | 204 | cause, |
| 198 | 205 | }), |
| 199 | 206 | ), |
| 200 | 207 | ); |
| 201 | 208 | if (!isWorkspacePreviewEntryPath(resolved.relativePath)) { |
| 202 | return yield* new AssetAccessError({ | |
| 203 | operation: "validate-preview-type", | |
| 209 | return yield* new AssetPreviewTypeValidationError({ | |
| 204 | 210 | resource: input.resource, |
| 205 | message: "Only browser documents and images can be previewed.", | |
| 206 | 211 | }); |
| 207 | 212 | } |
| 208 | 213 | const canonicalFile = yield* resolveCanonicalWorkspaceFile({ |
| ... | ... | @@ -211,28 +216,22 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 211 | 216 | }).pipe( |
| 212 | 217 | Effect.mapError( |
| 213 | 218 | (cause) => |
| 214 | new AssetAccessError({ | |
| 215 | operation: "inspect-workspace-asset", | |
| 219 | new AssetWorkspaceAssetInspectionError({ | |
| 216 | 220 | resource: input.resource, |
| 217 | message: "Failed to inspect the workspace asset.", | |
| 218 | 221 | cause, |
| 219 | 222 | }), |
| 220 | 223 | ), |
| 221 | 224 | ); |
| 222 | 225 | if (!canonicalFile) { |
| 223 | return yield* new AssetAccessError({ | |
| 224 | operation: "locate-workspace-asset", | |
| 226 | return yield* new AssetWorkspaceAssetNotFoundError({ | |
| 225 | 227 | resource: input.resource, |
| 226 | message: "Workspace asset was not found.", | |
| 227 | 228 | }); |
| 228 | 229 | } |
| 229 | 230 | const canonicalWorkspaceRoot = yield* fileSystem.realPath(workspaceRoot).pipe( |
| 230 | 231 | Effect.mapError( |
| 231 | 232 | (cause) => |
| 232 | new AssetAccessError({ | |
| 233 | operation: "resolve-workspace", | |
| 233 | new AssetWorkspaceResolutionError({ | |
| 234 | 234 | resource: input.resource, |
| 235 | message: "Failed to resolve workspace.", | |
| 236 | 235 | cause, |
| 237 | 236 | }), |
| 238 | 237 | ), |
| ... | ... | @@ -262,10 +261,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 262 | 261 | attachmentId: input.resource.attachmentId, |
| 263 | 262 | }); |
| 264 | 263 | if (!attachmentPath) { |
| 265 | return yield* new AssetAccessError({ | |
| 266 | operation: "locate-attachment", | |
| 264 | return yield* new AssetAttachmentNotFoundError({ | |
| 267 | 265 | resource: input.resource, |
| 268 | message: "Attachment was not found.", | |
| 269 | 266 | }); |
| 270 | 267 | } |
| 271 | 268 | claims = { |
| ... | ... | @@ -281,10 +278,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 281 | 278 | const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.resource.cwd).pipe( |
| 282 | 279 | Effect.mapError( |
| 283 | 280 | (cause) => |
| 284 | new AssetAccessError({ | |
| 285 | operation: "normalize-workspace-root", | |
| 281 | new AssetWorkspaceRootNormalizationError({ | |
| 286 | 282 | resource: input.resource, |
| 287 | message: "Failed to normalize the workspace root.", | |
| 288 | 283 | cause, |
| 289 | 284 | }), |
| 290 | 285 | ), |
| ... | ... | @@ -293,10 +288,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 293 | 288 | const faviconPath = yield* faviconResolver.resolvePath(workspaceRoot).pipe( |
| 294 | 289 | Effect.mapError( |
| 295 | 290 | (cause) => |
| 296 | new AssetAccessError({ | |
| 297 | operation: "resolve-project-favicon", | |
| 291 | new AssetProjectFaviconResolutionError({ | |
| 298 | 292 | resource: input.resource, |
| 299 | message: "Failed to resolve project favicon.", | |
| 300 | 293 | cause, |
| 301 | 294 | }), |
| 302 | 295 | ), |
| ... | ... | @@ -307,19 +300,15 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 307 | 300 | !(yield* resolveCanonicalWorkspaceFile({ workspaceRoot, relativePath }).pipe( |
| 308 | 301 | Effect.mapError( |
| 309 | 302 | (cause) => |
| 310 | new AssetAccessError({ | |
| 311 | operation: "inspect-project-favicon", | |
| 303 | new AssetProjectFaviconInspectionError({ | |
| 312 | 304 | resource: input.resource, |
| 313 | message: "Failed to inspect the project favicon.", | |
| 314 | 305 | cause, |
| 315 | 306 | }), |
| 316 | 307 | ), |
| 317 | 308 | )) |
| 318 | 309 | ) { |
| 319 | return yield* new AssetAccessError({ | |
| 320 | operation: "locate-project-favicon", | |
| 310 | return yield* new AssetProjectFaviconNotFoundError({ | |
| 321 | 311 | resource: input.resource, |
| 322 | message: "Project favicon was not found.", | |
| 323 | 312 | }); |
| 324 | 313 | } |
| 325 | 314 | claims = { |
| ... | ... | @@ -328,10 +317,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 328 | 317 | workspaceRoot: yield* fileSystem.realPath(workspaceRoot).pipe( |
| 329 | 318 | Effect.mapError( |
| 330 | 319 | (cause) => |
| 331 | new AssetAccessError({ | |
| 332 | operation: "resolve-workspace", | |
| 320 | new AssetWorkspaceResolutionError({ | |
| 333 | 321 | resource: input.resource, |
| 334 | message: "Failed to resolve workspace.", | |
| 335 | 322 | cause, |
| 336 | 323 | }), |
| 337 | 324 | ), |
| ... | ... | @@ -348,10 +335,8 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i |
| 348 | 335 | const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32).pipe( |
| 349 | 336 | Effect.mapError( |
| 350 | 337 | (cause) => |
| 351 | new AssetAccessError({ | |
| 352 | operation: "load-signing-key", | |
| 338 | new AssetSigningKeyLoadError({ | |
| 353 | 339 | resource: input.resource, |
| 354 | message: "Failed to load the asset signing key.", | |
| 355 | 340 | cause, |
| 356 | 341 | }), |
| 357 | 342 | ), |
apps/server/src/ws.ts+6-13| ... | ... | @@ -46,7 +46,8 @@ import { |
| 46 | 46 | OrchestrationReplayEventsError, |
| 47 | 47 | type FilesystemBrowseFailure, |
| 48 | 48 | FilesystemBrowseError, |
| 49 | AssetAccessError, | |
| 49 | AssetWorkspaceContextNotFoundError, | |
| 50 | AssetWorkspaceContextResolutionError, | |
| 50 | 51 | EnvironmentAuthorizationError, |
| 51 | 52 | ThreadId, |
| 52 | 53 | type TerminalAttachStreamEvent, |
| ... | ... | @@ -1413,19 +1414,15 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => |
| 1413 | 1414 | .pipe( |
| 1414 | 1415 | Effect.mapError( |
| 1415 | 1416 | (cause) => |
| 1416 | new AssetAccessError({ | |
| 1417 | operation: "resolve-workspace-context", | |
| 1417 | new AssetWorkspaceContextResolutionError({ | |
| 1418 | 1418 | resource: input.resource, |
| 1419 | message: "Failed to resolve workspace context.", | |
| 1420 | 1419 | cause, |
| 1421 | 1420 | }), |
| 1422 | 1421 | ), |
| 1423 | 1422 | ); |
| 1424 | 1423 | if (Option.isNone(thread)) { |
| 1425 | return yield* new AssetAccessError({ | |
| 1426 | operation: "resolve-workspace-context", | |
| 1424 | return yield* new AssetWorkspaceContextNotFoundError({ | |
| 1427 | 1425 | resource: input.resource, |
| 1428 | message: "Workspace context was not found.", | |
| 1429 | 1426 | }); |
| 1430 | 1427 | } |
| 1431 | 1428 | const project = yield* projectionSnapshotQuery |
| ... | ... | @@ -1433,19 +1430,15 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => |
| 1433 | 1430 | .pipe( |
| 1434 | 1431 | Effect.mapError( |
| 1435 | 1432 | (cause) => |
| 1436 | new AssetAccessError({ | |
| 1437 | operation: "resolve-workspace-context", | |
| 1433 | new AssetWorkspaceContextResolutionError({ | |
| 1438 | 1434 | resource: input.resource, |
| 1439 | message: "Failed to resolve workspace context.", | |
| 1440 | 1435 | cause, |
| 1441 | 1436 | }), |
| 1442 | 1437 | ), |
| 1443 | 1438 | ); |
| 1444 | 1439 | if (Option.isNone(project)) { |
| 1445 | return yield* new AssetAccessError({ | |
| 1446 | operation: "resolve-workspace-context", | |
| 1440 | return yield* new AssetWorkspaceContextNotFoundError({ | |
| 1447 | 1441 | resource: input.resource, |
| 1448 | message: "Workspace context was not found.", | |
| 1449 | 1442 | }); |
| 1450 | 1443 | } |
| 1451 | 1444 | return yield* issueAssetUrl({ |
packages/contracts/src/assets.ts+163-21| ... | ... | @@ -29,28 +29,170 @@ export const AssetCreateUrlResult = Schema.Struct({ |
| 29 | 29 | }); |
| 30 | 30 | export type AssetCreateUrlResult = typeof AssetCreateUrlResult.Type; |
| 31 | 31 | |
| 32 | export const AssetAccessOperation = Schema.Literals([ | |
| 33 | "resolve-workspace-context", | |
| 34 | "normalize-workspace-root", | |
| 35 | "validate-workspace-path", | |
| 36 | "validate-preview-type", | |
| 37 | "inspect-workspace-asset", | |
| 38 | "locate-workspace-asset", | |
| 39 | "resolve-workspace", | |
| 40 | "locate-attachment", | |
| 41 | "resolve-project-favicon", | |
| 42 | "inspect-project-favicon", | |
| 43 | "locate-project-favicon", | |
| 44 | "load-signing-key", | |
| 45 | ]); | |
| 46 | export type AssetAccessOperation = typeof AssetAccessOperation.Type; | |
| 32 | export class AssetWorkspaceContextNotFoundError extends Schema.TaggedErrorClass<AssetWorkspaceContextNotFoundError>()( | |
| 33 | "AssetWorkspaceContextNotFoundError", | |
| 34 | { | |
| 35 | resource: AssetResource, | |
| 36 | }, | |
| 37 | ) { | |
| 38 | override get message(): string { | |
| 39 | return "Workspace context was not found."; | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | export class AssetWorkspaceContextResolutionError extends Schema.TaggedErrorClass<AssetWorkspaceContextResolutionError>()( | |
| 44 | "AssetWorkspaceContextResolutionError", | |
| 45 | { | |
| 46 | resource: AssetResource, | |
| 47 | cause: Schema.Defect(), | |
| 48 | }, | |
| 49 | ) { | |
| 50 | override get message(): string { | |
| 51 | return "Failed to resolve workspace context."; | |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | export class AssetWorkspaceRootNormalizationError extends Schema.TaggedErrorClass<AssetWorkspaceRootNormalizationError>()( | |
| 56 | "AssetWorkspaceRootNormalizationError", | |
| 57 | { | |
| 58 | resource: AssetResource, | |
| 59 | cause: Schema.Defect(), | |
| 60 | }, | |
| 61 | ) { | |
| 62 | override get message(): string { | |
| 63 | return "Failed to normalize the workspace root."; | |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | export class AssetWorkspacePathValidationError extends Schema.TaggedErrorClass<AssetWorkspacePathValidationError>()( | |
| 68 | "AssetWorkspacePathValidationError", | |
| 69 | { | |
| 70 | resource: AssetResource, | |
| 71 | cause: Schema.Defect(), | |
| 72 | }, | |
| 73 | ) { | |
| 74 | override get message(): string { | |
| 75 | return "Workspace file path must be relative to the project root."; | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | export class AssetPreviewTypeValidationError extends Schema.TaggedErrorClass<AssetPreviewTypeValidationError>()( | |
| 80 | "AssetPreviewTypeValidationError", | |
| 81 | { | |
| 82 | resource: AssetResource, | |
| 83 | }, | |
| 84 | ) { | |
| 85 | override get message(): string { | |
| 86 | return "Only browser documents and images can be previewed."; | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | export class AssetWorkspaceAssetInspectionError extends Schema.TaggedErrorClass<AssetWorkspaceAssetInspectionError>()( | |
| 91 | "AssetWorkspaceAssetInspectionError", | |
| 92 | { | |
| 93 | resource: AssetResource, | |
| 94 | cause: Schema.Defect(), | |
| 95 | }, | |
| 96 | ) { | |
| 97 | override get message(): string { | |
| 98 | return "Failed to inspect the workspace asset."; | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | export class AssetWorkspaceAssetNotFoundError extends Schema.TaggedErrorClass<AssetWorkspaceAssetNotFoundError>()( | |
| 103 | "AssetWorkspaceAssetNotFoundError", | |
| 104 | { | |
| 105 | resource: AssetResource, | |
| 106 | }, | |
| 107 | ) { | |
| 108 | override get message(): string { | |
| 109 | return "Workspace asset was not found."; | |
| 110 | } | |
| 111 | } | |
| 112 | ||
| 113 | export class AssetWorkspaceResolutionError extends Schema.TaggedErrorClass<AssetWorkspaceResolutionError>()( | |
| 114 | "AssetWorkspaceResolutionError", | |
| 115 | { | |
| 116 | resource: AssetResource, | |
| 117 | cause: Schema.Defect(), | |
| 118 | }, | |
| 119 | ) { | |
| 120 | override get message(): string { | |
| 121 | return "Failed to resolve workspace."; | |
| 122 | } | |
| 123 | } | |
| 47 | 124 | |
| 48 | export class AssetAccessError extends Schema.TaggedErrorClass<AssetAccessError>()( | |
| 49 | "AssetAccessError", | |
| 125 | export class AssetAttachmentNotFoundError extends Schema.TaggedErrorClass<AssetAttachmentNotFoundError>()( | |
| 126 | "AssetAttachmentNotFoundError", | |
| 50 | 127 | { |
| 51 | operation: AssetAccessOperation, | |
| 52 | 128 | resource: AssetResource, |
| 53 | message: TrimmedNonEmptyString, | |
| 54 | cause: Schema.optional(Schema.Defect()), | |
| 55 | 129 | }, |
| 56 | ) {} | |
| 130 | ) { | |
| 131 | override get message(): string { | |
| 132 | return "Attachment was not found."; | |
| 133 | } | |
| 134 | } | |
| 135 | ||
| 136 | export class AssetProjectFaviconResolutionError extends Schema.TaggedErrorClass<AssetProjectFaviconResolutionError>()( | |
| 137 | "AssetProjectFaviconResolutionError", | |
| 138 | { | |
| 139 | resource: AssetResource, | |
| 140 | cause: Schema.Defect(), | |
| 141 | }, | |
| 142 | ) { | |
| 143 | override get message(): string { | |
| 144 | return "Failed to resolve project favicon."; | |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | export class AssetProjectFaviconInspectionError extends Schema.TaggedErrorClass<AssetProjectFaviconInspectionError>()( | |
| 149 | "AssetProjectFaviconInspectionError", | |
| 150 | { | |
| 151 | resource: AssetResource, | |
| 152 | cause: Schema.Defect(), | |
| 153 | }, | |
| 154 | ) { | |
| 155 | override get message(): string { | |
| 156 | return "Failed to inspect the project favicon."; | |
| 157 | } | |
| 158 | } | |
| 159 | ||
| 160 | export class AssetProjectFaviconNotFoundError extends Schema.TaggedErrorClass<AssetProjectFaviconNotFoundError>()( | |
| 161 | "AssetProjectFaviconNotFoundError", | |
| 162 | { | |
| 163 | resource: AssetResource, | |
| 164 | }, | |
| 165 | ) { | |
| 166 | override get message(): string { | |
| 167 | return "Project favicon was not found."; | |
| 168 | } | |
| 169 | } | |
| 170 | ||
| 171 | export class AssetSigningKeyLoadError extends Schema.TaggedErrorClass<AssetSigningKeyLoadError>()( | |
| 172 | "AssetSigningKeyLoadError", | |
| 173 | { | |
| 174 | resource: AssetResource, | |
| 175 | cause: Schema.Defect(), | |
| 176 | }, | |
| 177 | ) { | |
| 178 | override get message(): string { | |
| 179 | return "Failed to load the asset signing key."; | |
| 180 | } | |
| 181 | } | |
| 182 | ||
| 183 | export const AssetAccessError = Schema.Union([ | |
| 184 | AssetWorkspaceContextNotFoundError, | |
| 185 | AssetWorkspaceContextResolutionError, | |
| 186 | AssetWorkspaceRootNormalizationError, | |
| 187 | AssetWorkspacePathValidationError, | |
| 188 | AssetPreviewTypeValidationError, | |
| 189 | AssetWorkspaceAssetInspectionError, | |
| 190 | AssetWorkspaceAssetNotFoundError, | |
| 191 | AssetWorkspaceResolutionError, | |
| 192 | AssetAttachmentNotFoundError, | |
| 193 | AssetProjectFaviconResolutionError, | |
| 194 | AssetProjectFaviconInspectionError, | |
| 195 | AssetProjectFaviconNotFoundError, | |
| 196 | AssetSigningKeyLoadError, | |
| 197 | ]); | |
| 198 | export type AssetAccessError = typeof AssetAccessError.Type; |