Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8982299
Redact secrets from service logs
Jun 28, 2026
eda5f06
Add SCF testnet metrics snapshot
Jun 28, 2026
665bf6f
Resolve merge conflicts
Jul 6, 2026
82c7ba8
fix: use moduleResolution bundler to resolve sdk sub-path exports
Jul 6, 2026
62e2bbf
fix: use lowercase moduleResolution bundler in resolver
Jul 6, 2026
1840de9
fix(soroban): export and enable testutils feature for resolver-registry
Jul 6, 2026
11466eb
fix(coordinator): add missing GET /orders/:id/transitions route
Jul 7, 2026
b01f99c
fix: use moduleResolution bundler to resolve sdk sub-path exports
Jul 6, 2026
3f77f28
fix: use lowercase moduleResolution bundler in resolver
Jul 6, 2026
952b6d2
fix(soroban): export and enable testutils feature for resolver-registry
Jul 6, 2026
dabf2a1
fix(coordinator): add missing GET /orders/:id/transitions route
Jul 7, 2026
72f0268
fix(soroban): pin Rust 1.81.0 to avoid CryptoRng trait regression
Jul 7, 2026
9b995d2
fix(ci): remove wasm32v1-none target, incompatible with Rust 1.81.0
Jul 7, 2026
d59fae1
chore: remove 1.81 toolchain pin
Jul 7, 2026
0464871
fix(soroban): commit Cargo.lock to enforce ed25519-dalek downgrade in CI
Jul 7, 2026
ecb746d
fix(ci): restore rust stable toolchain
Jul 7, 2026
618c308
Merge branch 'fix/ci-all-checks'
Jul 7, 2026
83125df
fix(ci): use pre-built stellar-cli binary to prevent hanging
Jul 7, 2026
39ff45f
fix(ci): restore wasm32v1-none target and correct stellar-cli install
Jul 7, 2026
9c1e416
fix(ci): use stable rust to support edition2024 dependencies
Jul 7, 2026
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ jobs:

- name: Install Stellar CLI
run: |
cargo install --locked stellar-cli --version 22.8.1 || true
wget -q https://github.com/stellar/stellar-cli/releases/download/v22.8.1/stellar-cli-22.8.1-x86_64-unknown-linux-gnu.tar.gz
tar -xzf stellar-cli-22.8.1-x86_64-unknown-linux-gnu.tar.gz
sudo mv stellar /usr/local/bin/
stellar --version

- name: Cache cargo
Expand All @@ -108,7 +110,7 @@ jobs:
~/.cargo/registry
~/.cargo/git
soroban/target
key: soroban-${{ runner.os }}-${{ hashFiles('soroban/Cargo.toml', 'soroban/contracts/**/Cargo.toml') }}
key: soroban-${{ runner.os }}-${{ hashFiles('soroban/Cargo.lock', 'soroban/Cargo.toml', 'soroban/contracts/**/Cargo.toml') }}

- name: Build WASM
run: stellar contract build
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