Skip to content
Open
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
858 changes: 858 additions & 0 deletions docs/openapi.yaml

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions frontend/app/api/docs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { readFileSync } from "node:fs";
import { join } from "node:path";

// Serve the raw OpenAPI spec as JSON (for tooling consumption).
export async function GET() {
const specPath = join(process.cwd(), "..", "docs", "openapi.yaml");
let specYaml: string;
try {
specYaml = readFileSync(specPath, "utf-8");
} catch {
return NextResponse.json({ error: "OpenAPI spec not found" }, { status: 404 });
}
Comment on lines +8 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Spec file read is unused — dead I/O on every request

specYaml holds the parsed YAML but is never referenced again; the HTML response hard-codes spec-url="/api/docs/spec" and fetches it from the sibling endpoint. The read serves only as a file-existence guard, meaning the entire YAML (~858 lines) is loaded into memory and discarded on every Redoc page load. A stat check (fs.existsSync or statSync) would achieve the same guard without reading the file contents.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/app/api/docs/route.ts
Line: 8-13

Comment:
**Spec file read is unused — dead I/O on every request**

`specYaml` holds the parsed YAML but is never referenced again; the HTML response hard-codes `spec-url="/api/docs/spec"` and fetches it from the sibling endpoint. The read serves only as a file-existence guard, meaning the entire YAML (~858 lines) is loaded into memory and discarded on every Redoc page load. A stat check (`fs.existsSync` or `statSync`) would achieve the same guard without reading the file contents.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code Fix in Cursor


// Serve the Redoc HTML page — loads spec from /api/docs/spec.yaml
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>StellarCred API Reference</title>
<!-- Redoc standalone bundle (no React dependency needed) -->
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Unpinned CDN script without subresource integrity

The Redoc bundle is fetched from cdn.redoc.ly/redoc/latest with no integrity= attribute. Two problems compound here: (1) latest is a moving pointer — a breaking release silently breaks the docs UI; (2) without SRI the browser will execute whatever the CDN serves, so a compromised or hijacked CDN delivers arbitrary JavaScript in your app's origin. Because /api/docs is served from the same origin as the API, any injected script can read document.cookie, localStorage, and make credentialed same-origin requests. Pin to a specific version and add a crossorigin="anonymous" integrity="sha384-..." attribute.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/app/api/docs/route.ts
Line: 23

Comment:
**Unpinned CDN script without subresource integrity**

The Redoc bundle is fetched from `cdn.redoc.ly/redoc/latest` with no `integrity=` attribute. Two problems compound here: (1) `latest` is a moving pointer — a breaking release silently breaks the docs UI; (2) without SRI the browser will execute whatever the CDN serves, so a compromised or hijacked CDN delivers arbitrary JavaScript in your app's origin. Because `/api/docs` is served from the same origin as the API, any injected script can read `document.cookie`, `localStorage`, and make credentialed same-origin requests. Pin to a specific version and add a `crossorigin="anonymous" integrity="sha384-..."` attribute.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code Fix in Cursor

<style>
body { margin: 0; }
</style>
</head>
<body>
<redoc spec-url="/api/docs/spec"></redoc>
<script>
Redoc.init('/api/docs/spec', {
theme: {
colors: { primary: { main: '#4f46e5' } },
typography: { fontFamily: 'Inter, system-ui, sans-serif' }
}
});
</script>
</body>
</html>`;

return new NextResponse(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
26 changes: 26 additions & 0 deletions frontend/app/api/docs/spec/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { readFileSync } from "node:fs";
import { join } from "node:path";

/**
* Serves the raw OpenAPI spec as YAML at GET /api/docs/spec.
* Used by the Redoc page at /api/docs to load the spec.
* Also useful for tooling (e.g. `openapi-typescript`, Postman import).
*/
export async function GET() {
const specPath = join(process.cwd(), "..", "docs", "openapi.yaml");
let specYaml: string;
try {
specYaml = readFileSync(specPath, "utf-8");
} catch {
return NextResponse.json({ error: "OpenAPI spec not found" }, { status: 404 });
}

return new NextResponse(specYaml, {
headers: {
"Content-Type": "application/yaml; charset=utf-8",
// Allow cross-origin fetch so browser-based tooling can consume it.
"Access-Control-Allow-Origin": "*",
},
});
}
17 changes: 2 additions & 15 deletions frontend/app/api/issue/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sha256 } from "@noble/hashes/sha2.js";
import { IssuerClient, CREDENTIAL_TYPES, type CredentialType, type ClaimParams } from "@stellarcred/issuer";
import { fetchIssuerPubkey } from "@/lib/issuer-registry";
import { logger, stripSensitiveFields, resolveRequestId } from "../../../lib/logger";
import type { IssueRequest, IssueResponse200, IssueResponse202 } from "../../../types/index.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unused type imports

IssueResponse200 and IssueResponse202 are imported but not used anywhere in the file — no function return type is annotated with them. They were likely intended to type the NextResponse.json(...) call sites, but that wiring was not completed.

Suggested change
import type { IssueRequest, IssueResponse200, IssueResponse202 } from "../../../types/index.js";
import type { IssueRequest } from "../../../types/index.js";
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/app/api/issue/route.ts
Line: 6

Comment:
**Unused type imports**

`IssueResponse200` and `IssueResponse202` are imported but not used anywhere in the file — no function return type is annotated with them. They were likely intended to type the `NextResponse.json(...)` call sites, but that wiring was not completed.

```suggestion
import type { IssueRequest } from "../../../types/index.js";
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor


// Server-side only — never shipped to the browser.
// Set ISSUER_PRIVATE_KEY in .env.local to the 64-char hex secp256k1 private
Expand Down Expand Up @@ -222,21 +223,7 @@ export async function POST(req: NextRequest) {
return response;
};

let body: {
credential_types?: string[];
// Legacy single-type shape — still accepted for backward compatibility.
type?: string;
holder?: string;
issuerId?: string;
issuerName?: string;
expiry?: string;
attributes?: Record<string, string>;
attribute?: string;
claimParams?: ClaimParams;
// Set by the frontend after the user returns from Persona's hosted flow.
persona_inquiry_id?: string;
returnUrl?: string;
};
let body: IssueRequest;

try {
body = await req.json();
Expand Down
3 changes: 2 additions & 1 deletion frontend/app/api/issuers/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { NextResponse } from "next/server";
import { fetchRegisteredIssuers } from "@/lib/issuer-registry";
import type { RegisteredIssuer } from "../../../types/index.js";

// Any existing account works for read-only Soroban simulation.
const SIM_ACCOUNT =
process.env.NEXT_PUBLIC_ISSUER_ADDRESS ??
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";

export async function GET() {
export async function GET(): Promise<NextResponse<{ issuers: RegisteredIssuer[] } | { error: string; issuers: [] }>> {
try {
const issuers = await fetchRegisteredIssuers(SIM_ACCOUNT);
return NextResponse.json({ issuers });
Expand Down
5 changes: 3 additions & 2 deletions frontend/app/api/plaid-balance/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { logger, stripSensitiveFields, resolveRequestId } from "../../../lib/logger";
import type { PlaidBalanceResponse } from "../../../types/index.js";

export async function GET(req: NextRequest) {
export async function GET(req: NextRequest): Promise<NextResponse<PlaidBalanceResponse | { error: string }>> {
const requestId = resolveRequestId(req.headers.get("x-request-id"));

const sendResponse = (response: NextResponse) => {
const sendResponse = <T>(response: NextResponse<T>): NextResponse<T> => {
response.headers.set("x-request-id", requestId);
return response;
};
Expand Down
19 changes: 1 addition & 18 deletions frontend/app/api/ready/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
import { NextResponse } from "next/server";
import { RPC_URL, CONTRACTS } from "../../../lib/stellar";
import type { ReadyResponse, SignerStatus, DependencyStatus } from "../../../types/index.js";

export const dynamic = "force-dynamic";

interface DependencyStatus {
status: "ok" | "error";
message?: string;
}

interface SignerStatus extends DependencyStatus {
/** "demo" = signing with the public demo issuer key; "configured" = ISSUER_PRIVATE_KEY set. */
issuer: "demo" | "configured";
}

interface ReadyResponse {
ready: boolean;
signer: SignerStatus;
contracts: DependencyStatus;
rpc: DependencyStatus;
persona: DependencyStatus;
}

async function checkRpc(): Promise<DependencyStatus> {
try {
const res = await fetch(RPC_URL, {
Expand Down
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"predev": "node scripts/copy-bb.mjs",
"prebuild": "node scripts/copy-bb.mjs",
"postinstall": "pnpm --dir packages/issuer build",
"generate-types": "openapi-typescript ../docs/openapi.yaml -o types/api.d.ts",
"dev": "next dev",
"build": "next build",
"start": "next start",
Expand All @@ -31,13 +32,15 @@
"react-dom": "^18.3.1"
},
"devDependencies": {
"@redocly/cli": "^1.34.17",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^25.0.1",
"openapi-typescript": "^7.13.0",
"typescript": "^5",
"vitest": "^2.1.9"
}
Expand Down
Loading
Loading