| 13 | const base = "https://skills.example.test/catalog/" |
| 14 | |
| 15 | async function pull(skills: unknown[], files: Record<string, string> = {}, cache?: Awaited<ReturnType<typeof tmpdir>>) { |
| 16 | const tmp = cache ?? (await tmpdir()) |
| 17 | const requests: string[] = [] |
| 18 | const http = Layer.succeed( |
| 19 | HttpClient.HttpClient, |
| 20 | HttpClient.make((request) => |
| 21 | Effect.sync(() => requests.push(request.url)).pipe( |
| 22 | Effect.map(() => { |
| 23 | const body = request.url === `${base}index.json` ? JSON.stringify({ skills }) : files[request.url] |
| 24 | return HttpClientResponse.fromWeb( |
| 25 | request, |
| 26 | new Response(body ?? "Not Found", { status: body === undefined ? 404 : 200 }), |
| 27 | ) |
| 28 | }), |
| 29 | ), |
| 30 | ), |
| 31 | ) |
| 32 | const skillDiscoveryLayer = AppNodeBuilder.build(SkillDiscovery.node, [ |
| 33 | [LayerNodePlatform.httpClient, http], |
| 34 | [Global.node, Global.layerWith({ cache: tmp.path })], |
| 35 | ]) |
| 36 | const directories = await Effect.runPromise( |
| 37 | Effect.gen(function* () { |
| 38 | return yield* (yield* SkillDiscovery.Service).pull(base) |
| 39 | }).pipe(Effect.provide(skillDiscoveryLayer)), |
| 40 | ) |
| 41 | return { tmp, requests, directories } |
| 42 | } |
| 43 | |
| 44 | describe("SkillDiscovery.pull", () => { |
| 45 | test("rejects skill name traversal without fetching files", async () => { |