From 6e8c1d2fd71e5e7cca43f07753472cd50dfefa35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Mui=CC=81n=CC=83o=20Garci=CC=81a?= Date: Tue, 28 Apr 2026 13:31:46 +0200 Subject: [PATCH 1/2] Add createSignedUrl to YepCodeApi and YepCodeStorage Wraps the new POST /storage/signed-urls endpoint that returns a short-lived GCS V4 signed read URL for an existing file. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/api/types.ts | 11 +++++ src/api/yepcodeApi.ts | 6 +++ src/storage/yepcodeStorage.ts | 9 +++- tests/api/yepcodeApi.storage.test.ts | 68 +++++++++++++++++++++++++++- tests/storage/yepcodeStorage.test.ts | 54 +++++++++++++++++++++- 5 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/api/types.ts b/src/api/types.ts index 5efe0e9..129d314 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -463,6 +463,17 @@ export type CreateStorageObjectInput = { file: File | Blob | Readable; }; +export type CreateSignedUrlInput = { + path: string; + expiresInSeconds?: number; +}; + +export type SignedUrl = { + url: string; + path: string; + expiresAt: string; +}; + /** * Auth */ diff --git a/src/api/yepcodeApi.ts b/src/api/yepcodeApi.ts index cf90f2f..71bfb7b 100644 --- a/src/api/yepcodeApi.ts +++ b/src/api/yepcodeApi.ts @@ -34,6 +34,8 @@ import { VersionedModuleAliasesPaginatedResult, StorageObject, CreateStorageObjectInput, + CreateSignedUrlInput, + SignedUrl, Token, Sandbox, CreateSandboxInput, @@ -796,6 +798,10 @@ export class YepCodeApi { ); } + async createSignedUrl(data: CreateSignedUrlInput): Promise { + return this.request("POST", "/storage/signed-urls", { data }); + } + // Auth endpoints async getToken(apiToken: string): Promise { return this.request("POST", "/auth/token", { diff --git a/src/storage/yepcodeStorage.ts b/src/storage/yepcodeStorage.ts index ad6a80a..ddcdb63 100644 --- a/src/storage/yepcodeStorage.ts +++ b/src/storage/yepcodeStorage.ts @@ -1,5 +1,5 @@ import { YepCodeApi } from "../api"; -import { StorageObject, YepCodeApiConfig } from "../api/types"; +import { SignedUrl, StorageObject, YepCodeApiConfig } from "../api/types"; import { Readable } from "stream"; export class YepCodeStorage { @@ -27,4 +27,11 @@ export class YepCodeStorage { async delete(filename: string): Promise { await this.api.deleteObject(filename); } + + async createSignedUrl( + filename: string, + options: { expiresInSeconds?: number } = {} + ): Promise { + return this.api.createSignedUrl({ path: filename, ...options }); + } } diff --git a/tests/api/yepcodeApi.storage.test.ts b/tests/api/yepcodeApi.storage.test.ts index c7bbae1..04f0c07 100644 --- a/tests/api/yepcodeApi.storage.test.ts +++ b/tests/api/yepcodeApi.storage.test.ts @@ -1,5 +1,5 @@ -import { YepCodeApi } from "../../src/api/yepcodeApi"; -import { StorageObject } from "../../src/api/types"; +import { YepCodeApi, YepCodeApiError } from "../../src/api/yepcodeApi"; +import { SignedUrl, StorageObject } from "../../src/api/types"; import fs, { createWriteStream, readFileSync } from "fs"; import path from "path"; import { Readable } from "stream"; @@ -113,4 +113,68 @@ describe.skip("YepCodeApi", () => { await verifyDownloadedFile(result, downloadedFile, testFilePath); }); }); + + describe("createSignedUrl", () => { + it("should return a signed url with the default expiry", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await api.createObject({ name: testName, file }); + + const result: SignedUrl = await api.createSignedUrl({ path: testName }); + + expect(typeof result.url).toBe("string"); + expect(result.url.length).toBeGreaterThan(0); + expect(result.path).toBe(testName); + + const expiresAt = new Date(result.expiresAt).getTime(); + const expectedExpiry = Date.now() + 3600 * 1000; + expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000); + }); + + it("should return a signed url with a custom expiry", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await api.createObject({ name: testName, file }); + + const result: SignedUrl = await api.createSignedUrl({ + path: testName, + expiresInSeconds: 60, + }); + + const expiresAt = new Date(result.expiresAt).getTime(); + const expectedExpiry = Date.now() + 60 * 1000; + expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000); + }); + + it("should return content matching the original file when fetched", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await api.createObject({ name: testName, file }); + + const { url } = await api.createSignedUrl({ path: testName }); + + const response = await fetch(url); + expect(response.ok).toBe(true); + const body = await response.text(); + expect(body).toBe(readFileSync(testFilePath, "utf8")); + }); + + it("should throw a 404 when the file does not exist", async () => { + await expect( + api.createSignedUrl({ path: "does-not-exist.txt" }) + ).rejects.toMatchObject({ + name: "YepCodeApiError", + status: 404, + } as Partial); + }); + + it("should throw a 400 when expiresInSeconds is out of range", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await api.createObject({ name: testName, file }); + + await expect( + api.createSignedUrl({ path: testName, expiresInSeconds: 999999 }) + ).rejects.toMatchObject({ + name: "YepCodeApiError", + status: 400, + } as Partial); + }); + }); }); diff --git a/tests/storage/yepcodeStorage.test.ts b/tests/storage/yepcodeStorage.test.ts index 0bc05dd..f1d8cc0 100644 --- a/tests/storage/yepcodeStorage.test.ts +++ b/tests/storage/yepcodeStorage.test.ts @@ -1,5 +1,6 @@ import { YepCodeStorage } from "../../src/storage"; -import { StorageObject } from "../../src/api/types"; +import { YepCodeApiError } from "../../src/api/yepcodeApi"; +import { SignedUrl, StorageObject } from "../../src/api/types"; import fs, { createWriteStream, readFileSync } from "fs"; import path from "path"; import { Readable } from "stream"; @@ -117,4 +118,55 @@ describe.skip("YepCodeStorage", () => { await verifyDownloadedFile(result, downloadedFile, testFilePath); }); }); + + describe("createSignedUrl", () => { + it("should return a signed url with the default expiry", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await storage.upload(testName, file); + + const result: SignedUrl = await storage.createSignedUrl(testName); + + expect(typeof result.url).toBe("string"); + expect(result.url.length).toBeGreaterThan(0); + expect(result.path).toBe(testName); + + const expiresAt = new Date(result.expiresAt).getTime(); + const expectedExpiry = Date.now() + 3600 * 1000; + expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000); + }); + + it("should return a signed url with a custom expiry", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await storage.upload(testName, file); + + const result: SignedUrl = await storage.createSignedUrl(testName, { + expiresInSeconds: 60, + }); + + const expiresAt = new Date(result.expiresAt).getTime(); + const expectedExpiry = Date.now() + 60 * 1000; + expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000); + }); + + it("should throw a 404 when the file does not exist", async () => { + await expect( + storage.createSignedUrl("does-not-exist.txt") + ).rejects.toMatchObject({ + name: "YepCodeApiError", + status: 404, + } as Partial); + }); + + it("should throw a 400 when expiresInSeconds is out of range", async () => { + const file: File = new File([readFileSync(testFilePath)], testName); + await storage.upload(testName, file); + + await expect( + storage.createSignedUrl(testName, { expiresInSeconds: 999999 }) + ).rejects.toMatchObject({ + name: "YepCodeApiError", + status: 400, + } as Partial); + }); + }); }); From a88e80deefe1633b111999456b4cd65ec6f7a492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Mui=CC=81n=CC=83o=20Garci=CC=81a?= Date: Tue, 28 Apr 2026 14:47:09 +0200 Subject: [PATCH 2/2] Document storage signed URL support in README. Add quick-start and API reference docs for createSignedUrl in YepCodeApi and YepCodeStorage so the new storage capability is clearly discoverable. Made-with: Cursor --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 575fe1e..43a263d 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ const processes = await api.getProcesses(); ### 6. Storage Objects -You can manage files in your YepCode workspace using the `YepCodeStorage` class. This allows you to upload, list, download, and delete files easily. +You can manage files in your YepCode workspace using the `YepCodeStorage` class. This allows you to upload, list, download, delete, and generate signed URLs for files easily. ```js const { YepCodeStorage } = require('@yepcode/run'); @@ -126,6 +126,12 @@ console.log(files); const stream = await storage.download('path/myfile.txt'); stream.pipe(fs.createWriteStream('./downloaded.txt')); +// Create a temporary signed URL (1 hour by default) +const signedUrl = await storage.createSignedUrl('path/myfile.txt', { + expiresInSeconds: 300 // Optional +}); +console.log(signedUrl.url, signedUrl.expiresAt); + // Delete a file await storage.delete('myfile.txt'); ``` @@ -392,9 +398,33 @@ interface Process { } ``` +##### `createSignedUrl(data: CreateSignedUrlInput): Promise` + +Creates a temporary signed URL for a stored file. + +**Parameters:** + +- `data.path`: Storage path (filename) to generate a signed URL for +- `data.expiresInSeconds`: Optional expiry in seconds + +**Returns:** Promise + +```typescript +interface CreateSignedUrlInput { + path: string; + expiresInSeconds?: number; +} + +interface SignedUrl { + url: string; + path: string; + expiresAt: string; +} +``` + ### YepCodeStorage -Manages file storage in your YepCode workspace. Allows you to upload, list, download, and delete files using the YepCode API. +Manages file storage in your YepCode workspace. Allows you to upload, list, download, delete, and generate signed URLs using the YepCode API. #### Constructor @@ -434,6 +464,14 @@ Deletes a file from YepCode storage. - `filename`: Name of the file to delete - **Returns:** Promise +##### `createSignedUrl(filename: string, options?: { expiresInSeconds?: number }): Promise` + +Creates a temporary signed URL for a file in storage. + +- `filename`: Name of the file to generate a signed URL for +- `options.expiresInSeconds`: Optional expiry in seconds +- **Returns:** Promise + ##### `StorageObject` ```typescript @@ -446,6 +484,16 @@ interface StorageObject { } ``` +##### `SignedUrl` + +```typescript +interface SignedUrl { + url: string; + path: string; + expiresAt: string; +} +``` + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.