Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@relayauth/server",
"version": "0.2.23",
"version": "0.2.24",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
35 changes: 35 additions & 0 deletions packages/server/src/__tests__/create-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import crypto from "node:crypto";
import test from "node:test";
import type { AgentIdentity, CreateIdentityInput, RelayAuthTokenClaims } from "@relayauth/types";
import { FixedWindowRateLimiter, RateLimitExceededError } from "../lib/rate-limit.js";
import { StorageCapacityExhaustedError } from "../lib/storage-retry.js";
import type { IdentityBudget, StoredIdentity } from "../storage/identity-types.js";
import {
assertJsonResponse,
Expand Down Expand Up @@ -398,3 +399,37 @@ test("the global error handler preserves rate-limit errors as 429 responses", as
assert.equal(response.headers.get("Retry-After"), "60");
assert.equal(response.headers.get("RateLimit-Remaining"), "0");
});

test("POST /v1/identities returns 503 with capacity envelope when storage.identities.create throws StorageCapacityExhaustedError", async () => {
// Verifies that StorageCapacityExhaustedError thrown from storage.identities.create()
// propagates correctly: the route catch block re-throws it (not swallowed as 500),
// and app.onError handles it via storageCapacityResponse → 503 with Retry-After.
// This is the server-side assertion that the 503 route mapping fix is correct.
const storage = createTestStorage();
storage.identities.create = async () => {
throw new StorageCapacityExhaustedError("post-auth mint capacity exhausted (status 429)");
};
const app = createTestApp({}, { storage });

const response = await app.request(
createTestRequest(
"POST",
"/v1/identities",
{ name: "capacity-probe", sponsorId: "user_sponsor_1" },
{ Authorization: `Bearer ${createAuthToken()}` },
),
undefined,
app.bindings,
);

const body = await assertJsonResponse<{
code?: string;
retryable?: boolean;
retryAfterMs?: number;
requestId?: string;
}>(response, 503);
assert.equal(body.code, "storage_capacity_exhausted", "capacity envelope code");
assert.equal(body.retryable, true, "capacity envelope must be retryable");
assert.ok(response.headers.get("Retry-After"), "Retry-After header must be present");
assert.equal(response.headers.get("Cache-Control"), "no-store", "Cache-Control must be no-store");
});
5 changes: 5 additions & 0 deletions packages/server/src/routes/identities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AppEnv } from "../env.js";
import { authenticateAndAuthorizeFromContext, authenticateBearerOrApiKey, authorizeClaims, decodeBase64UrlJson } from "../lib/auth.js";
import { emitObserverEvent, now as observerNow } from "../lib/events.js";
import {
isStorageCapacityExhausted,
isTransientStorageOverload,
isStorageOverloadedError,
StorageOverloadedError,
Expand Down Expand Up @@ -502,6 +503,10 @@ identities.post("/", async (c) => {
});
return c.json(createdIdentity, 201);
} catch (error) {
if (isStorageCapacityExhausted(error)) {
throw error;
}

if (isStorageOverloadedError(error)) {
return storageOverloadResponse(c, error);
}
Expand Down
Loading