Skip to content

typescript: hand-written fetch runtime — M6 (D-160) - #47

Merged
unofficialbox merged 2 commits into
mainfrom
claude/engine-rewrite-review-vibdbd
Jul 16, 2026
Merged

typescript: hand-written fetch runtime — M6 (D-160)#47
unofficialbox merged 2 commits into
mainfrom
claude/engine-rewrite-review-vibdbd

Conversation

@unofficialbox

@unofficialbox unofficialbox commented Jul 16, 2026

Copy link
Copy Markdown
Owner

What

Adds runtimes/typescript/gantryruntime — the real runtime the generated Box TypeScript SDK ships against (TR-TS.5), the implementation the rendered runtime.ts stubs (D-158) stand in for. A standalone package, like runtimes/go/gantryruntime and runtimes/rust/gantryruntime.

The fourth M6 slice, after the model layer (#44), the runtime-contract stubs (#45), and the managers/client (#46).

The runtime

src/runtime.ts implements the V1 contract with matching names/signatures:

  • Request / Response / Stream envelopes (bodies buffered so retries can replay them).
  • Client session — baseUrl, newRequest, accessToken, and a retrying fetch: exponential backoff + full jitter, a single 401 token refresh, and Retry-After on 429/503, cancellable via AbortSignal.
  • The with* request builders (withQuery/withHeader/withJsonBody/withFormBody/withStreamBody/withMultipartBody) and the response accessors (responseBytes/responseStream/responseHeader/statusCode).

src/errors.ts holds BoxApiError extends Error (the exceptions model, TR-TS.3).

It depends only on the platform fetch/Headers/URLSearchParamsno Node-only APIs — so it runs on any modern JavaScript runtime.

Auth

src/auth.ts provides three fetch-only flows:

  • developerToken — a fixed console token.
  • clientCredentials (CCG) — server-to-server, enterpriseId or userId subject.
  • OAuth 2.0 authorization-code — authorizeUrl to build the redirect, exchangeCode to turn a code into an Auth, oauth to resume from a stored refresh token.

CCG and OAuth cache the access token behind a single-flight refresh (concurrent callers share one in-flight exchange) and refresh a margin before expiry; OAuth rotates the refresh token Box returns.

JWT server auth is deferred to a follow-up slice — it is the only flow needing an RSA signing key (node:crypto + @types/node), so keeping it separate preserves this slice's platform-neutral, dependency-free tsc gate.

Verification (TR-TS.5)

  • Swap test (the_generated_sdk_compiles_against_the_real_runtime): generates the SDK, replaces the stub runtime.ts with the real runtime's source, adds a smoke module that constructs the client from an auth flow, and tsc --noEmits the whole package — proving the runtime satisfies the exact contract signatures the managers call, with no drift (FR-5.2). This is the TypeScript analogue of the Rust backend's cargo check --examples gate.
  • Standalone: the runtime package type-checks under strict TS7, wired as a CI step alongside the Go/Rust runtime jobs.

Not in this slice

JWT server auth, then reference docs + generated tests + the conform --target typescript shape — later M6 slices. A live smoke test against a real Box account (as Go/Rust have) is also a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_016kjN3CzbXhJrnUmUHJJqpW


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added a hand-written TypeScript runtime that powers SDK request/response handling, streaming and multipart uploads, configurable base URLs, and rich response helpers.
    • Implemented authentication for developer tokens, client credentials, and OAuth authorization-code (with cached tokens and refresh-token rotation).
    • Added retry logic with exponential backoff (full jitter), Retry-After-aware delays, idempotency-aware retries, and forced token refresh on 401.
  • Bug Fixes
    • Improved reliability for transient 5xx/transport failures and reduced failures due to expired access tokens.
  • Tests
    • Added a type-checking test ensuring the generated TypeScript SDK compiles against the real runtime.
    • Added CI TypeScript strict type-check coverage.
  • Documentation
    • Updated decision and progress notes for the TypeScript backend runtime work.

Add runtimes/typescript/gantryruntime, the real runtime the generated Box
TypeScript SDK ships against (TR-TS.5) — the implementation the rendered
runtime.ts stubs (D-158) stand in for. A standalone package, like the Go
and Rust runtimes.

src/runtime.ts implements the V1 contract with matching names/signatures:
Request/Response/Stream envelopes, a Client session (baseUrl, newRequest,
accessToken, and a retrying fetch — exponential backoff + full jitter, a
single 401 token refresh, Retry-After on 429/503), the with* request
builders, and the response accessors. src/errors.ts holds BoxApiError
(the exceptions model, TR-TS.3). It depends only on the platform
fetch/Headers/URLSearchParams — no Node-only APIs — so it runs on any
modern JavaScript runtime.

src/auth.ts provides three fetch-only auth flows: developerToken (fixed
token), clientCredentials (CCG), and the OAuth 2.0 authorization-code flow
(authorizeUrl / exchangeCode / oauth resume). CCG and OAuth cache the
access token behind a single-flight refresh and refresh a margin before
expiry; OAuth rotates the refresh token Box returns. JWT server auth is
deferred to a follow-up slice — it is the only flow needing an RSA signing
key (node:crypto + @types/node), so keeping it separate preserves this
slice's platform-neutral, dependency-free tsc gate.

Gated: a backend test generates the SDK, swaps the stub runtime.ts for the
real runtime's source, adds a smoke module constructing the client from an
auth flow, and tsc --noEmit's the whole package — proving the runtime
satisfies the contract signatures the managers call, no drift (FR-5.2),
exactly as the Rust backend's cargo check --examples gate. The runtime
package also type-checks standalone under strict TS7, wired as a CI step
alongside the Go/Rust runtime jobs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kjN3CzbXhJrnUmUHJJqpW
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f1d67f0c-975a-4776-a3c9-92e308bec57d

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd5874 and 91c6a2c.

📒 Files selected for processing (5)
  • DECISIONS.md
  • PROGRESS.md
  • runtimes/typescript/gantryruntime/src/auth.ts
  • runtimes/typescript/gantryruntime/src/errors.ts
  • runtimes/typescript/gantryruntime/src/runtime.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • DECISIONS.md
  • PROGRESS.md
  • runtimes/typescript/gantryruntime/src/runtime.ts

📝 Walkthrough

Walkthrough

The PR adds a standalone handwritten TypeScript runtime with authentication flows, HTTP request and response handling, retries, package metadata, strict type-checking, CI validation, and generated SDK compatibility testing.

Changes

TypeScript runtime implementation

Layer / File(s) Summary
Authentication and error contracts
runtimes/typescript/gantryruntime/src/auth.ts, runtimes/typescript/gantryruntime/src/errors.ts
Adds fixed-token, client-credentials, and OAuth authorization-code flows with cached refresh behavior, plus the BoxApiError type.
HTTP runtime and request execution
runtimes/typescript/gantryruntime/src/runtime.ts
Adds runtime envelopes, request builders, response accessors, base URLs, bearer-token handling, retries, backoff, jitter, and abort-aware delays.
Package wiring and generated SDK validation
runtimes/typescript/gantryruntime/package.json, runtimes/typescript/gantryruntime/tsconfig.json, crates/gantry-backend-typescript/tests/compile_output.rs, .github/workflows/ci.yml
Exposes the runtime package, configures strict TypeScript checking, validates generated SDK compilation against the real runtime, and adds CI type-checking.
Decision and progress records
DECISIONS.md, PROGRESS.md
Documents the TypeScript runtime slice and updates progress and next-step records.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GeneratedSDK
  participant Client
  participant Auth
  participant BoxAPI
  GeneratedSDK->>Client: create request
  Client->>Auth: accessToken()
  Auth-->>Client: access token
  Client->>BoxAPI: fetch authenticated request
  BoxAPI-->>Client: response
  Client-->>GeneratedSDK: runtime response
Loading

Possibly related PRs

  • unofficialbox/box-gantry#46: Adds generated promise-based managers and client usage that this PR type-checks against the real TypeScript runtime.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding the hand-written TypeScript fetch runtime for M6/D-160.
Docstring Coverage ✅ Passed Docstring coverage is 91.30% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/engine-rewrite-review-vibdbd

Comment @coderabbitai help to get the list of available commands.

@unofficialbox
unofficialbox marked this pull request as ready for review July 16, 2026 19:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PROGRESS.md`:
- Around line 33-34: Synchronize the progress percentages in PROGRESS.md:
reconcile the TypeScript entries around the v4 summary and M6 milestone, and
align the Total ~90% figure with the ~88% overall figure. Update the affected
figures consistently, or document the distinct measurement basis where the
values intentionally differ.

In `@runtimes/typescript/gantryruntime/package.json`:
- Around line 5-9: Update the `@box/gantryruntime` package configuration to
publish compiled artifacts: configure the build to emit JavaScript and
declaration files into dist/, then change the exports entry from src/runtime.ts
to the generated JavaScript and types from src/runtime.ts to the generated
declaration file. Ensure the published package includes the dist/ artifacts.

In `@runtimes/typescript/gantryruntime/src/auth.ts`:
- Around line 61-78: Update accessToken to start the shared refresh without
binding it to any caller’s AbortSignal, while preserving single-flight behavior
and token state updates. For each caller awaiting this.inflight, race the shared
promise against that caller’s signal so cancellation affects only that waiter;
callers without a signal should await the shared refresh directly. Keep refresh
errors clearing this.inflight.
- Around line 128-155: Update CcgConfig and clientCredentials to require exactly
one subject: reject configurations providing both userId and enterpriseId or
neither, using a discriminated union for the TypeScript API plus a runtime
validation for JavaScript callers. Preserve subject selection only after
validation, and never send an empty enterprise subject.
- Around line 86-112: Update postTokenForm to catch fetch transport failures,
including network and abort errors, and rethrow them as BoxApiError so token
acquisition preserves the runtime error contract. Keep HTTP response handling
and JSON parsing unchanged, and attach the original failure as a cause if
BoxApiError supports it.
- Around line 189-220: The refreshTokenExchange flow updates the rotated refresh
token only in local state, so oauth callers cannot persist it. Expose each new
token through the oauth/auth API or add a persistence callback that receives the
updated refresh token, and wire it through refreshTokenExchange and oauth while
preserving the existing token exchange behavior.

In `@runtimes/typescript/gantryruntime/src/runtime.ts`:
- Around line 139-143: Update the 401 retry branch in the runtime request flow
to invalidate the cached credential or request a forced refresh before calling
auth.accessToken(signal). Extend the Auth contract and its implementations with
the required invalidation or force-refresh semantics, then use them when
refreshed is false so the retry sends a newly obtained token.
- Around line 80-83: Validate options.maxRetries in the constructor before
assigning this.maxRetries: accept only finite, non-negative integers, and reject
Infinity, NaN, negative values, and fractional values with an appropriate error.
Preserve the default of 5 when maxRetries is not provided.
- Around line 168-179: Update retryAfter to support both delay-seconds and
HTTP-date Retry-After values: retain numeric parsing for seconds, otherwise
parse the header as an HTTP date and calculate the delay relative to the current
time. Clamp any resulting negative delay to zero, while continuing to use
backoff(attempt) only when the header is invalid or absent.
- Around line 110-149: Update the retry loop in the request method containing
the attempt counter, transport-error handling, and retriable-status branch so
retries occur only for idempotent HTTP methods. Preserve the initial request and
existing token-refresh behavior, but prevent non-idempotent writes from
repeating after transport failures or 429/5xx responses unless the existing
request contract provides idempotency protection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 92f12233-2a87-4a98-b24a-22766fb0a608

📥 Commits

Reviewing files that changed from the base of the PR and between d9e5723 and 0fd5874.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • DECISIONS.md
  • PROGRESS.md
  • crates/gantry-backend-typescript/tests/compile_output.rs
  • runtimes/typescript/gantryruntime/package.json
  • runtimes/typescript/gantryruntime/src/auth.ts
  • runtimes/typescript/gantryruntime/src/errors.ts
  • runtimes/typescript/gantryruntime/src/runtime.ts
  • runtimes/typescript/gantryruntime/tsconfig.json

Comment thread PROGRESS.md
Comment on lines +5 to +9
"type": "module",
"types": "./src/runtime.ts",
"exports": {
".": "./src/runtime.ts"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

git ls-files runtimes/typescript/gantryruntime package.json tsconfig*.json runtimes/typescript/gantryruntime/src

Repository: unofficialbox/box-gantry

Length of output: 400


🏁 Script executed:

cat -n runtimes/typescript/gantryruntime/package.json
printf '\n--- tsconfig ---\n'
cat -n runtimes/typescript/gantryruntime/tsconfig.json
printf '\n--- runtime.ts ---\n'
cat -n runtimes/typescript/gantryruntime/src/runtime.ts

Repository: unofficialbox/box-gantry

Length of output: 13436


🏁 Script executed:

rg -n "`@box/gantryruntime`|gantryruntime" -S .
printf '\n--- package files ---\n'
git ls-files '*package.json' '*pnpm-workspace.yaml' '*turbo.json' '*nx.json' '*vite.config.*' '*rollup.config.*' '*tsup.config.*'

Repository: unofficialbox/box-gantry

Length of output: 22721


🏁 Script executed:

cat -n crates/gantry-backend-typescript/tests/compile_output.rs | sed -n '150,240p'

Repository: unofficialbox/box-gantry

Length of output: 3110


🏁 Script executed:

sed -n '2390,2425p' DECISIONS.md
printf '\n---\n'
sed -n '150,210p' ARCHITECTURE.md

Repository: unofficialbox/box-gantry

Length of output: 5725


🌐 Web query:

Node.js TypeScript stripping node_modules imported TypeScript package.json exports source .ts node_modules support

💡 Result:

As of July 2026, Node.js natively supports TypeScript via type stripping [1], but it explicitly restricts this functionality within the node_modules directory to discourage package authors from publishing raw TypeScript source code [1][2]. Key details regarding this behavior include: Restriction: By default, Node.js throws the error ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING when it encounters a.ts,.mts, or.cts file inside a node_modules folder [1][2][3]. This safeguard exists to protect editor and TypeScript compiler (tsc) performance, as consuming raw TypeScript dependencies without type declarations can lead to significantly slower type inference [3]. Recent Developments: As of mid-2026, there is active development in the Node.js project to refine or lift this restriction to better support monorepos and specific deployment scenarios [4][3]. For example, pull requests have been proposed to allow type stripping in node_modules if co-located declaration files (.d.ts) are present [3], or to remove the restriction entirely to enable zero-build workflows for workspace packages [4]. Alternative Solutions: For projects that require TypeScript resolution in node_modules (such as monorepos), developers often use third-party tools or experimental loaders (e.g., node-ts-resolver) that hook into Node.js's internal type-stripping mechanisms to bypass the default node_modules restriction [5]. Package authors are currently expected to publish transpiled JavaScript files along with declaration files (.d.ts) [3], utilizing the package.json exports field to point to these built assets, rather than shipping raw.ts sources [4]. You can track the ongoing status of this feature through official Node.js GitHub discussions, such as PR #63853 and PR #63936, which detail the evolving support for type stripping in dependencies [4][3].

Citations:


Publish built JS from @box/gantryruntime

"exports": "./src/runtime.ts" points consumers at raw TypeScript, but installed packages under node_modules can’t rely on Node’s TypeScript stripping. Emit JS and declarations to dist/, then update exports and types to those build artifacts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtimes/typescript/gantryruntime/package.json` around lines 5 - 9, Update
the `@box/gantryruntime` package configuration to publish compiled artifacts:
configure the build to emit JavaScript and declaration files into dist/, then
change the exports entry from src/runtime.ts to the generated JavaScript and
types from src/runtime.ts to the generated declaration file. Ensure the
published package includes the dist/ artifacts.

Comment on lines +61 to +78
async accessToken(signal?: AbortSignal): Promise<string> {
if (this.token && this.expiry - Date.now() > REFRESH_MARGIN_MS) {
return this.token;
}
if (!this.inflight) {
this.inflight = this.refresh(signal)
.then((token) => {
this.token = token.accessToken;
this.expiry = Date.now() + token.ttlMs;
this.inflight = undefined;
return token.accessToken;
})
.catch((err: unknown) => {
this.inflight = undefined;
throw err;
});
}
return this.inflight;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Decouple shared refreshes from each caller’s abort signal.

The first caller’s signal owns the shared refresh, so its cancellation fails every waiter. Conversely, later callers cannot cancel while awaiting this.inflight. Start an independent single-flight refresh and race each caller against its own signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtimes/typescript/gantryruntime/src/auth.ts` around lines 61 - 78, Update
accessToken to start the shared refresh without binding it to any caller’s
AbortSignal, while preserving single-flight behavior and token state updates.
For each caller awaiting this.inflight, race the shared promise against that
caller’s signal so cancellation affects only that waiter; callers without a
signal should await the shared refresh directly. Keep refresh errors clearing
this.inflight.

Comment thread runtimes/typescript/gantryruntime/src/auth.ts
Comment on lines +128 to +155
export interface CcgConfig {
clientId: string;
clientSecret: string;
enterpriseId?: string;
userId?: string;
/** Optional; defaults to Box's token endpoint. */
tokenUrl?: string;
}

/** Build a CCG `Auth`. */
export function clientCredentials(config: CcgConfig): Auth {
const tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
const subject = config.userId
? { type: 'user', id: config.userId }
: { type: 'enterprise', id: config.enterpriseId ?? '' };
return new CachedToken((signal) =>
postTokenForm(
tokenUrl,
{
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
box_subject_type: subject.type,
box_subject_id: subject.id,
},
signal,
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n### auth.ts outline\n'
ast-grep outline runtimes/typescript/gantryruntime/src/auth.ts --view expanded || true

printf '\n### auth.ts relevant lines\n'
sed -n '1,260p' runtimes/typescript/gantryruntime/src/auth.ts | cat -n

printf '\n### search for related validation/usages\n'
rg -n "clientCredentials|CcgConfig|enterpriseId|userId|box_subject_type|box_subject_id|DEFAULT_TOKEN_URL|CachedToken" runtimes/typescript/gantryruntime/src -S

Repository: unofficialbox/box-gantry

Length of output: 12829


Require exactly one CCG subject. runtimes/typescript/gantryruntime/src/auth.ts should reject configs that set both userId and enterpriseId, or neither; clientCredentials() currently prefers userId and otherwise sends an empty enterprise subject. A discriminated union plus a runtime check would keep the TS API and JS callers aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtimes/typescript/gantryruntime/src/auth.ts` around lines 128 - 155, Update
CcgConfig and clientCredentials to require exactly one subject: reject
configurations providing both userId and enterpriseId or neither, using a
discriminated union for the TypeScript API plus a runtime validation for
JavaScript callers. Preserve subject selection only after validation, and never
send an empty enterprise subject.

Comment thread runtimes/typescript/gantryruntime/src/auth.ts
Comment thread runtimes/typescript/gantryruntime/src/runtime.ts
Comment on lines +110 to +149
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
const headers = new Headers(request.headers);
headers.set('Authorization', `Bearer ${token}`);

// `httpResponse` is the platform `Response` (via inference — the name is
// shadowed by this module's `Response` class, so it is never annotated).
let httpResponse;
try {
httpResponse = await fetch(url, {
method: request.method,
headers,
// A `Uint8Array` is a valid `BodyInit` at runtime; the cast bridges
// the `Uint8Array<ArrayBufferLike>` vs `BufferSource` generic gap in
// the DOM lib without narrowing the contract's plain-`Uint8Array` body.
body: request.body as BodyInit | undefined,
signal,
});
} catch (err) {
lastError = err;
if (attempt === this.maxRetries) {
break;
}
await sleep(backoff(attempt), signal);
continue;
}

const bodyBytes = new Uint8Array(await httpResponse.arrayBuffer());
const response = new Response(httpResponse.status, httpResponse.headers, bodyBytes);

// A single token refresh on 401.
if (response.status === 401 && !refreshed) {
refreshed = true;
token = await this.auth.accessToken(signal);
continue;
}
// Backoff on rate-limit / server errors, honoring Retry-After.
if (retriable(response.status) && attempt < this.maxRetries) {
await sleep(retryAfter(response, attempt), signal);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and surrounding context.
FILE="runtimes/typescript/gantryruntime/src/runtime.ts"
wc -l "$FILE"
sed -n '1,220p' "$FILE"

Repository: unofficialbox/box-gantry

Length of output: 7929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="runtimes/typescript/gantryruntime/src/runtime.ts"
wc -l "$FILE"
sed -n '1,220p' "$FILE"

Repository: unofficialbox/box-gantry

Length of output: 7929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where requests are created and executed to see whether the retry policy
# can distinguish safe/idempotent operations.
rg -n "newRequest\(|\.fetch\(request|client\.fetch|await this\.client\.fetch|request\.method" runtimes/typescript/gantryruntime/src -g '!**/dist/**'

Repository: unofficialbox/box-gantry

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "newRequest\(|\.fetch\(request|client\.fetch|await this\.client\.fetch|request\.method" runtimes/typescript/gantryruntime/src -g '!**/dist/**'

Repository: unofficialbox/box-gantry

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the TypeScript runtime tree and search for request construction/HTTP verbs
# outside the runtime implementation.
find runtimes/typescript -maxdepth 4 -type f \( -name '*.ts' -o -name '*.mts' -o -name '*.cts' \) | sort | sed -n '1,200p'
echo '---'
rg -n "\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b|withJsonBody\(|withQuery\(|withHeader\(|newRequest\(" runtimes/typescript -g '!**/dist/**'

Repository: unofficialbox/box-gantry

Length of output: 1015


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "idempot|retry|retriabl|safe method|X-Request-Id|Idempotency-Key|operation metadata|mutat" runtimes/typescript -g '!**/dist/**'

Repository: unofficialbox/box-gantry

Length of output: 1093


Do not retry non-idempotent writes. This loop retries every method on transport failures and 429/5xx responses, so a mutating request can be replayed after a partial commit and duplicate the operation. Restrict retries to idempotent methods or add idempotency protection for writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtimes/typescript/gantryruntime/src/runtime.ts` around lines 110 - 149,
Update the retry loop in the request method containing the attempt counter,
transport-error handling, and retriable-status branch so retries occur only for
idempotent HTTP methods. Preserve the initial request and existing token-refresh
behavior, but prevent non-idempotent writes from repeating after transport
failures or 429/5xx responses unless the existing request contract provides
idempotency protection.

Comment thread runtimes/typescript/gantryruntime/src/runtime.ts Outdated
Comment on lines +168 to +179
/** The delay before a retry: the response's Retry-After seconds when present,
* else the backoff schedule. */
function retryAfter(response: Response, attempt: number): number {
const header = response.headers.get('Retry-After');
if (header) {
const seconds = Number.parseInt(header, 10);
if (Number.isFinite(seconds)) {
return seconds * 1000;
}
}
return backoff(attempt);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle HTTP-date Retry-After values. Retry-After can be either delay-seconds or an HTTP date; the current parseInt path ignores dates, so the backoff fallback can retry too early. Parse both forms and clamp negative delays to zero.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtimes/typescript/gantryruntime/src/runtime.ts` around lines 168 - 179,
Update retryAfter to support both delay-seconds and HTTP-date Retry-After
values: retain numeric parsing for seconds, otherwise parse the header as an
HTTP date and calculate the delay relative to the current time. Clamp any
resulting negative delay to zero, while continuing to use backoff(attempt) only
when the header is invalid or absent.

Bring the TS runtime to parity with the Rust runtime's retry/auth semantics
and tighten the auth error contract, per CodeRabbit's review of #47.

runtime.ts:
- Idempotency-gated retries (shouldRetry): a 429 (never processed) retries
  for any method; a transport error or 5xx (which may have committed a
  write) retries only for idempotent methods — matching Rust's should_retry.
- Force-refresh on 401 (auth.forceRefresh) re-acquires past the token cache,
  so the retry no longer resends the rejected token — matching Rust's
  force_refresh. Previously the 401 refresh was a no-op when the token was
  still cached.
- Retry-After parsed as a floor over backoff (delay-seconds only, like
  Rust) and clamped to a 30s ceiling so a hostile header can't stall.
- maxRetries validated as a non-negative integer at construction.

auth.ts:
- Auth gains forceRefresh(stale); CachedToken returns a concurrently
  refreshed token without a network round-trip, else runs a single-flight
  refresh. Token acquisition is no longer bound to a caller's AbortSignal,
  so one caller's cancellation can't fail every waiter.
- postTokenForm wraps transport failures as BoxApiError (with cause), so
  auth acquisition never escapes the runtime's error contract.
- clientCredentials requires a subject (enterpriseId or userId) rather than
  silently sending an empty enterprise subject.
- OAuth reports each rotated refresh token through an optional onRefresh
  hook, so a resume doesn't start from an already-invalidated token.

errors.ts: BoxApiError carries an optional cause.

Docs: sync the stale M6/overall progress figures (~90% / M6 ~42%).

Deferred with rationale: publishing @box/gantryruntime as built dist/ JS +
d.ts is a ship concern (like the Rust NF-8 crate) — the runtime is consumed
as source by the swap test today, not installed from node_modules. HTTP-date
Retry-After is unhandled in both the Rust and TS runtimes (a shared gap).

Runtime tsc-clean; the generated SDK still type-checks against the real
runtime (swap gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kjN3CzbXhJrnUmUHJJqpW

Copy link
Copy Markdown
Owner Author

Thanks — worked through the review in 91c6a2c. Most findings were genuine gaps where this runtime diverged from the Rust runtime's established behavior, so I brought it to parity:

Fixed

  • Idempotency-gated retries — 429 (never processed) retries any method; transport errors and 5xx (which may have committed a write) retry only idempotent methods. Mirrors Rust's should_retry.
  • Force-refresh on 401 — the 401 branch now calls Auth.forceRefresh(stale), which re-acquires past the token cache (the TS analogue of Rust's force_refresh). Previously the refresh was a no-op when the token was still cached, so the retry resent the rejected token.
  • Single-flight not bound to a caller's signal — token acquisition no longer threads any one request's AbortSignal, so one caller's cancellation can't fail every waiter (matches Rust, which doesn't thread a per-request signal into token acquisition).
  • Auth transport errors wrapped as BoxApiError (with cause); BoxApiError now carries an optional cause.
  • CCG subject required (enterpriseId or userId) — no more empty enterprise subject.
  • Refresh-token rotation exposed via an optional onRefresh persistence hook on OAuthConfig.
  • maxRetries validated as a non-negative integer.
  • Retry-After as a floor over backoff (delay-seconds, like Rust), clamped to a 30s ceiling.
  • Synced the stale progress figures.

Deferred, with rationale

  • Publish built dist/ JS + .d.ts — this is a ship/packaging concern (the analogue of the Rust NF-8 crate-publish, also deferred). The runtime is consumed as source by the swap test today, not installed from node_modules, so raw-.ts exports is fine for the current use. It'll get a real build/publish setup in the ship slice.
  • HTTP-date Retry-After — unhandled in both the Rust and TS runtimes (both parse delay-seconds only); a shared gap to close uniformly rather than a TS-only divergence.

Runtime is tsc-clean and the generated SDK still type-checks against the real runtime (swap gate).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants