diff --git a/package-lock.json b/package-lock.json index f9995a9..dae74da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@smtpfast/sdk", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@smtpfast/sdk", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "@hey-api/openapi-ts": "0.99.0", diff --git a/package.json b/package.json index 16fb136..2853e84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@smtpfast/sdk", - "version": "0.1.0", + "version": "0.1.1", "description": "The official SMTPfast SDK for Node.js and TypeScript.", "license": "MIT", "type": "module", diff --git a/src/core.ts b/src/core.ts index 049b07f..1941be0 100644 --- a/src/core.ts +++ b/src/core.ts @@ -81,6 +81,7 @@ export class Core { /** Make one API call and return the parsed body, or throw an APIError. */ async call(fn: SdkFn, args: object, options: RequestOptions = {}, idempotent = false): Promise { + checkPath((args as { path?: Record }).path); const maxRetries = options.maxRetries ?? this.#maxRetries; const headers: Record = { ...options.headers }; if (idempotent) { @@ -142,6 +143,17 @@ export class Core { } /** How long to wait before retrying this response, or null for no retry. */ +// An id of "", "." or ".." would become a dot-segment that the URL parser +// resolves, so the request would reach a different endpoint. +function checkPath(path: Record | undefined): void { + for (const [name, value] of Object.entries(path ?? {})) { + const text = typeof value === "string" || (typeof value === "number" && Number.isFinite(value)) ? String(value) : null; + if (text === null || text === "" || text === "." || text === "..") { + throw new SMTPfastError(`Invalid path parameter ${name}: ${JSON.stringify(value) ?? String(value)}`); + } + } +} + function retryDelay(response: Response, attempt: number, retryable: boolean): number | null { if (!RETRY_STATUS.has(response.status)) return null; const header = response.headers.get("Retry-After"); diff --git a/src/version.ts b/src/version.ts index 52c905c..6fa18df 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = "0.1.0"; +export const VERSION = "0.1.1"; diff --git a/test/client.test.ts b/test/client.test.ts index 57453f4..1c5b630 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -33,6 +33,14 @@ describe("requests", () => { expect(calls[0].url.pathname).toBe("/api/v1/inboxes/in%201/threads/th_2/emails/em%2F3/reply"); }); + it("rejects an empty or dot path parameter before any request", async () => { + const { client, calls } = mockClient([]); + for (const id of ["", ".", "..", [".."], { toString: () => ".." }, null, undefined]) { + await expect(client.emails.get(id as string)).rejects.toThrow(SMTPfastError); + } + expect(calls).toHaveLength(0); + }); + it("sends query parameters", async () => { const { client, calls } = mockClient([json({ data: [], has_more: false })]); await client.emails.list({ limit: 5, after: "em_9" });