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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown,wasm32v1-none
targets: wasm32-unknown-unknown


- name: Install Stellar CLI
run: |
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ source code or block explorer.

---

### Log Redaction

Coordinator, resolver, and relayer logs use `@oversync/sdk/logging` for
log-safe payloads. Redaction matches sensitive keys case-insensitively
after removing separators, including `secret`, `preimage`, `privateKey`,
`token`, `authorization`, `signedXdr`, and `mnemonic`. Safe debugging
context such as order IDs, chain names, addresses, statuses, transaction
hashes, and failure codes should remain visible.

---

## How a swap actually works (60-second tour)

```
Expand Down
1 change: 1 addition & 0 deletions coordinator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"replay": "tsx src/replay.ts"
},
"dependencies": {
"@oversync/sdk": "workspace:*",
"@stellar/stellar-sdk": "^13.0.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
Expand Down
3 changes: 2 additions & 1 deletion coordinator/src/listeners/ethereum-listener.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createPublicClient, http, parseAbiItem, type PublicClient } from "viem";
import { sepolia, mainnet } from "viem/chains";
import type { Logger } from "pino";
import { redactLogValue } from "@oversync/sdk/logging";
import type { CoordinatorConfig } from "../config.js";
import type { OrderService } from "../services/order-service.js";
import { listenerLastBlock } from "../metrics.js";
Expand Down Expand Up @@ -86,7 +87,7 @@ export class EthereumListener {
listenerLastBlock.set({ chain: "ethereum" }, Number(log.blockNumber));
}
this.log.info(
{ orderId: log.args.orderId!.toString(), preimage: log.args.preimage },
redactLogValue({ orderId: log.args.orderId!.toString(), preimage: log.args.preimage }),
"ETH order claimed"
);
// Secret reveal is recorded by SecretService when a client posts
Expand Down
9 changes: 8 additions & 1 deletion coordinator/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import pino, { type Logger } from "pino";
import { redactLogObject } from "@oversync/sdk/logging";

let cached: Logger | null = null;

export function getLogger(level: string = "info"): Logger {
if (!cached) {
cached = pino({ level, base: { service: "oversync-coordinator" } });
cached = pino({
level,
base: { service: "oversync-coordinator" },
formatters: {
log: (object) => redactLogObject(object)
}
});
}
return cached;
}
13 changes: 13 additions & 0 deletions coordinator/src/server/routes/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ export function ordersRoutes(orders: OrderService): Router {
});

// Parameterized routes come AFTER specific routes

// GET /orders/:id/transitions — must be declared before GET /orders/:id
// so Express matches the more specific path first.
router.get("/orders/:id/transitions", async (req, res, next) => {
const id = req.params.id;
try {
const transitions = await orders.getTransitions(id);
res.json({ transitions });
} catch (err) {
next(err);
}
});

router.get("/orders/:id", async (req, res, next) => {
const id = req.params.id;
try {
Expand Down
10 changes: 10 additions & 0 deletions coordinator/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { defineConfig } from "vitest/config";
import path from "node:path";
import { fileURLToPath } from "node:url";

const here = path.dirname(fileURLToPath(import.meta.url));
const sdkSrc = path.resolve(here, "../packages/sdk/src");

export default defineConfig({
test: {
Expand All @@ -10,5 +15,10 @@ export default defineConfig({
external: [/^node:/]
}
}
},
resolve: {
alias: [
{ find: /^@oversync\/sdk\/logging$/, replacement: path.join(sdkSrc, "logging/index.ts") }
]
}
});
4 changes: 4 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"import": "./dist/secrets/index.js",
"types": "./dist/secrets/index.d.ts"
},
"./logging": {
"import": "./dist/logging/index.js",
"types": "./dist/logging/index.d.ts"
},
"./state-machine": {
"import": "./dist/state-machine/index.js",
"types": "./dist/state-machine/index.d.ts"
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./types/index.js";
export * from "./secrets/index.js";
export * from "./state-machine/index.js";
export * from "./assets/index.js";
export * from "./logging/index.js";
export * from "./errors/index.js";
export * from "./explorers/index.js";
export {
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/src/logging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
isSensitiveLogKey,
redactLogObject,
redactLogString,
redactLogValue
} from "./redaction.js";
66 changes: 66 additions & 0 deletions packages/sdk/src/logging/redaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const REDACTED = "[REDACTED]";

const SENSITIVE_KEYS = new Set([
"authorization",
"mnemonic",
"preimage",
"privatekey",
"resolverprivatekey",
"resolversecret",
"secret",
"secretkey",
"signedxdr",
"token"
]);

function normaliseKey(key: string): string {
return key.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}

export function isSensitiveLogKey(key: string): boolean {
return SENSITIVE_KEYS.has(normaliseKey(key));
}

export function redactLogString(value: string): string {
return value
.replace(/\bBearer\s+[-._~+/A-Za-z0-9]+=*/gi, `Bearer ${REDACTED}`)
.replace(/\b0x[0-9a-fA-F]{64,}\b/g, REDACTED)
.replace(/\bS[A-Z2-7]{55}\b/g, REDACTED);
}

export function redactLogValue(value: unknown, seen = new WeakSet<object>()): unknown {
if (typeof value === "string") {
return redactLogString(value);
}

if (typeof value !== "object" || value === null) {
return value;
}

if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);

if (Array.isArray(value)) {
return value.map((item) => redactLogValue(item, seen));
}

if (value instanceof Error) {
return {
name: value.name,
message: redactLogString(value.message),
stack: value.stack ? redactLogString(value.stack) : undefined
};
}

const redacted: Record<string, unknown> = {};
for (const [key, nested] of Object.entries(value)) {
redacted[key] = isSensitiveLogKey(key) ? REDACTED : redactLogValue(nested, seen);
}
return redacted;
}

export function redactLogObject<T extends Record<string, unknown>>(value: T): Record<string, unknown> {
return redactLogValue(value) as Record<string, unknown>;
}
56 changes: 56 additions & 0 deletions packages/sdk/test/log-redaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { isSensitiveLogKey, redactLogString, redactLogValue } from "../src/logging/index.js";

describe("log redaction", () => {
it("redacts nested sensitive fields while preserving safe debugging context", () => {
const value = {
publicId: "ord_123",
status: "src_locked",
srcChain: "ethereum",
address: "0x1111111111111111111111111111111111111111",
secret: "plain-secret",
nested: {
privateKey: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
signedXdr: "AAAAAgAAAAA..."
}
};

expect(redactLogValue(value)).toEqual({
publicId: "ord_123",
status: "src_locked",
srcChain: "ethereum",
address: "0x1111111111111111111111111111111111111111",
secret: "[REDACTED]",
nested: {
privateKey: "[REDACTED]",
signedXdr: "[REDACTED]"
}
});
});

it("redacts arrays with mixed safe and sensitive fields", () => {
const value = [
{ orderId: "42", token: "bearer-token", chain: "stellar" },
{ resolver: "0x2222222222222222222222222222222222222222", failureCode: "timeout" }
];

expect(redactLogValue(value)).toEqual([
{ orderId: "42", token: "[REDACTED]", chain: "stellar" },
{ resolver: "0x2222222222222222222222222222222222222222", failureCode: "timeout" }
]);
});

it("matches sensitive keys with unknown casing and separators", () => {
expect(isSensitiveLogKey("Authorization")).toBe(true);
expect(isSensitiveLogKey("authorization")).toBe(true);
expect(isSensitiveLogKey("signed_xdr")).toBe(true);
expect(isSensitiveLogKey("private-key")).toBe(true);
});

it("redacts secret-looking substrings from plain strings", () => {
const preimage = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
expect(redactLogString(`claim failed for ${preimage} with Bearer abc.def`)).toBe(
"claim failed for [REDACTED] with Bearer [REDACTED]"
);
});
});
Loading
Loading