Skip to content

Commit 09ee36c

Browse files
committed
Merge branch 'codex/bugs-gh-267-sdk-limits'
2 parents b695aac + 5b17ab1 commit 09ee36c

7 files changed

Lines changed: 139 additions & 3 deletions

File tree

sdk/src/namespaces/billing.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { describe, it } from "node:test";
88
import assert from "node:assert/strict";
99

1010
import { Run402 } from "../index.js";
11+
import { LocalError } from "../errors.js";
1112
import type { CredentialsProvider } from "../credentials.js";
1213

1314
interface FetchCall {
@@ -189,6 +190,36 @@ describe("billing.history", () => {
189190
assert.equal(calls[0]!.url, "https://api.example.test/billing/v1/accounts/0xabc/history?limit=50");
190191
});
191192

193+
it("appends ?limit=1 when the minimum valid limit is provided", async () => {
194+
const { fetch, calls } = mockFetch(() =>
195+
jsonResponse({ identifier: "0xabc", identifier_type: "wallet", entries: [] }),
196+
);
197+
const sdk = makeSdk(fetch);
198+
await sdk.billing.history("0xABC", 1);
199+
200+
assert.equal(calls[0]!.url, "https://api.example.test/billing/v1/accounts/0xabc/history?limit=1");
201+
});
202+
203+
it("throws LocalError and does not request for invalid limits", async () => {
204+
const invalidLimits = [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1];
205+
206+
for (const limit of invalidLimits) {
207+
const { fetch, calls } = mockFetch(() => {
208+
throw new Error(`unexpected fetch for limit ${String(limit)}`);
209+
});
210+
const sdk = makeSdk(fetch);
211+
212+
await assert.rejects(
213+
sdk.billing.history("0xABC", limit),
214+
(err: unknown) =>
215+
err instanceof LocalError &&
216+
err.context === "fetching billing history" &&
217+
/limit.*positive safe integer/i.test(err.message),
218+
);
219+
assert.equal(calls.length, 0, `limit ${String(limit)} should not request`);
220+
}
221+
});
222+
192223
it("offers getHistory as a generic identifier alias", async () => {
193224
const { fetch, calls } = mockFetch(() =>
194225
jsonResponse({ identifier: "user@example.com", identifier_type: "email", entries: [] }),

sdk/src/namespaces/billing.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import type { Client } from "../kernel.js";
88
import { LocalError } from "../errors.js";
9+
import { assertPositiveSafeInteger } from "../validation.js";
910
import type { ProjectTier } from "./projects.types.js";
1011

1112
export interface BillingBalance {
@@ -102,7 +103,10 @@ export class Billing {
102103
/** Fetch billing history by wallet or email identifier. */
103104
async getHistory(identifier: BillingAccountIdentifier, limit?: number): Promise<BillingHistoryResult> {
104105
const encoded = encodeBillingIdentifier(identifier);
105-
const path = limit
106+
if (limit !== undefined) {
107+
assertPositiveSafeInteger(limit, "limit", "fetching billing history");
108+
}
109+
const path = limit !== undefined
106110
? `/billing/v1/accounts/${encoded}/history?limit=${encodeURIComponent(String(limit))}`
107111
: `/billing/v1/accounts/${encoded}/history`;
108112
return this.client.request<BillingHistoryResult>(path, {

sdk/src/namespaces/blobs.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { describe, it } from "node:test";
88
import assert from "node:assert/strict";
99

1010
import { Run402 } from "../index.js";
11-
import { ProjectNotFound, ApiError } from "../errors.js";
11+
import { ProjectNotFound, ApiError, LocalError } from "../errors.js";
1212
import type { CredentialsProvider } from "../credentials.js";
1313

1414
interface FetchCall {
@@ -403,6 +403,26 @@ describe("blobs.ls", () => {
403403
await sdk.blobs.ls("prj_known");
404404
assert.equal(calls[0]!.url, "https://api.example.test/storage/v1/blobs");
405405
});
406+
407+
it("throws LocalError and does not request for invalid limits", async () => {
408+
const invalidLimits = [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1];
409+
410+
for (const limit of invalidLimits) {
411+
const { fetch, calls } = mockFetch(() => {
412+
throw new Error(`unexpected fetch for limit ${String(limit)}`);
413+
});
414+
const sdk = makeSdk(fetch);
415+
416+
await assert.rejects(
417+
sdk.blobs.ls("prj_known", { limit }),
418+
(err: unknown) =>
419+
err instanceof LocalError &&
420+
err.context === "listing blobs" &&
421+
/limit.*positive safe integer/i.test(err.message),
422+
);
423+
assert.equal(calls.length, 0, `limit ${String(limit)} should not request`);
424+
}
425+
});
406426
});
407427

408428
describe("blobs.rm", () => {
@@ -923,4 +943,28 @@ describe("blobs.waitFresh", () => {
923943
ProjectNotFound,
924944
);
925945
});
946+
947+
it("throws LocalError and does not diagnose for invalid timeoutMs", async () => {
948+
const invalidTimeouts = [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1];
949+
950+
for (const timeoutMs of invalidTimeouts) {
951+
const { fetch, calls } = mockFetch(() => {
952+
throw new Error(`unexpected fetch for timeoutMs ${String(timeoutMs)}`);
953+
});
954+
const sdk = makeSdk(fetch);
955+
956+
await assert.rejects(
957+
sdk.blobs.waitFresh("prj_known", {
958+
url: "https://app.run402.com/_blob/k",
959+
sha256: "ff",
960+
timeoutMs,
961+
}),
962+
(err: unknown) =>
963+
err instanceof LocalError &&
964+
err.context === "waiting for CDN freshness" &&
965+
/timeoutMs.*positive safe integer/i.test(err.message),
966+
);
967+
assert.equal(calls.length, 0, `timeoutMs ${String(timeoutMs)} should not request`);
968+
}
969+
});
926970
});

sdk/src/namespaces/blobs.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import type { Client } from "../kernel.js";
1111
import { ApiError, LocalError, ProjectNotFound } from "../errors.js";
12+
import { assertPositiveSafeInteger } from "../validation.js";
1213
import type {
1314
BlobCacheKind,
1415
BlobCdnEnvelope,
@@ -488,11 +489,13 @@ export class Blobs {
488489
projectId: string,
489490
opts: BlobWaitFreshOptions,
490491
): Promise<BlobWaitFreshResult> {
492+
const timeoutMs = opts.timeoutMs ?? 60_000;
493+
assertPositiveSafeInteger(timeoutMs, "timeoutMs", "waiting for CDN freshness");
494+
491495
const project = await this.client.getProject(projectId);
492496
if (!project) throw new ProjectNotFound(projectId, "waiting for CDN freshness");
493497

494498
const expected = opts.sha256.toLowerCase();
495-
const timeoutMs = opts.timeoutMs ?? 60_000;
496499
const start = Date.now();
497500

498501
let attempts = 0;
@@ -563,6 +566,10 @@ export class Blobs {
563566

564567
/** List blobs with optional prefix + pagination. */
565568
async ls(projectId: string, opts: BlobLsOptions = {}): Promise<BlobLsResult> {
569+
if (opts.limit !== undefined) {
570+
assertPositiveSafeInteger(opts.limit, "limit", "listing blobs");
571+
}
572+
566573
const project = await this.client.getProject(projectId);
567574
if (!project) throw new ProjectNotFound(projectId, "listing blobs");
568575

sdk/src/namespaces/email.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { describe, it } from "node:test";
77
import assert from "node:assert/strict";
88

99
import { Run402 } from "../index.js";
10+
import { LocalError } from "../errors.js";
1011
import type { CredentialsProvider } from "../credentials.js";
1112

1213
interface FetchCall {
@@ -181,3 +182,36 @@ describe("email.send", () => {
181182
assert.equal(result.sent_at, "2026-05-01T18:30:14.904Z");
182183
});
183184
});
185+
186+
describe("email.list", () => {
187+
it("GETs mailbox messages with limit and after query params", async () => {
188+
const { fetch, calls } = mockFetch(() => jsonResponse([]));
189+
const sdk = makeSdk(makeCreds(), fetch);
190+
await sdk.email.list("prj_known", { limit: 1, after: "msg_prev" });
191+
192+
const url = new URL(calls[0]!.url);
193+
assert.equal(url.pathname, "/mailboxes/v1/mbx_known/messages");
194+
assert.equal(url.searchParams.get("limit"), "1");
195+
assert.equal(url.searchParams.get("after"), "msg_prev");
196+
});
197+
198+
it("throws LocalError and does not request for invalid limits", async () => {
199+
const invalidLimits = [0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1];
200+
201+
for (const limit of invalidLimits) {
202+
const { fetch, calls } = mockFetch(() => {
203+
throw new Error(`unexpected fetch for limit ${String(limit)}`);
204+
});
205+
const sdk = makeSdk(makeCreds(), fetch);
206+
207+
await assert.rejects(
208+
sdk.email.list("prj_known", { limit }),
209+
(err: unknown) =>
210+
err instanceof LocalError &&
211+
err.context === "listing emails" &&
212+
/limit.*positive safe integer/i.test(err.message),
213+
);
214+
assert.equal(calls.length, 0, `limit ${String(limit)} should not request`);
215+
}
216+
});
217+
});

sdk/src/namespaces/email.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import type { Client } from "../kernel.js";
88
import { ApiError, LocalError, ProjectNotFound } from "../errors.js";
9+
import { assertPositiveSafeInteger } from "../validation.js";
910

1011
const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
1112

@@ -353,6 +354,10 @@ export class Email {
353354

354355
/** List messages in the project's mailbox. */
355356
async list(projectId: string, opts: ListEmailsOptions = {}): Promise<EmailSummary[]> {
357+
if (opts.limit !== undefined) {
358+
assertPositiveSafeInteger(opts.limit, "limit", "listing emails");
359+
}
360+
356361
const { id, serviceKey } = await this.resolveMailbox(projectId);
357362
const qs = new URLSearchParams();
358363
if (opts.limit !== undefined) qs.set("limit", String(opts.limit));

sdk/src/validation.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { LocalError } from "./errors.js";
2+
3+
export function assertPositiveSafeInteger(
4+
value: number,
5+
name: string,
6+
context: string,
7+
): void {
8+
if (!Number.isSafeInteger(value) || value <= 0) {
9+
throw new LocalError(`${name} must be a positive safe integer.`, context);
10+
}
11+
}

0 commit comments

Comments
 (0)