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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Opt-in SDK diagnostics (`src/diagnostics/`): redacted lifecycle hooks, `buildDiagnosticsReport`, and support guide (`docs/diagnostics.md`) for configuration, network, transaction, wallet, and vault observability without leaking secrets
- Initial SDK release with wallet management, XLM payments, and transaction history
- Soroban savings-vault helpers (`depositToVault`, `withdrawFromVault`, `getVaultBalance`)
- Network error handling with retry guidance
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ npm install @axionvera/pocketpay-sdk
- [Safe Retry Policy](./docs/retry-policy.md) - Classifying submission outcomes, safe retry rules, and the `withRetryPolicy`API
- [Error Handling](./docs/error-handling.md) - SDK error handling overview
- [Logging Guidance](./docs/logging.md) - Safe logging practices for SDK applications
- [SDK Diagnostics](./docs/diagnostics.md) - Opt-in redacted lifecycle hooks and support-safe reports
- [Logging: Transaction Payloads & Debug Mode](./docs/logging-payloads-and-debug.md) - Safely logging signed transaction XDR, memos, and debug output
- [Security Best Practices](./docs/security.md) - Key management and transaction safety
- [Dependency Review](./docs/dependency-review.md) - How SDK dependencies are evaluated, added, updated, and justified
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ Horizon server factory (`setHorizonServerFactory`, `resetHorizonServerFactory`)
lets tests swap the network layer. Every module that touches the network reads
its endpoints from here rather than hardcoding them.

### diagnostics

Opt-in observability for support and local debugging. The module owns the
redacted report model (`buildDiagnosticsReport`), lifecycle hook registry
(`enableDiagnostics` / `emitDiagnosticsEvent`), and deny-list redaction
(`redactDiagnosticsValue`). Feature modules may emit events; diagnostics must
not import wallet/payments/soroban implementation details beyond types already
available through config and the capability registry. Hooks are off by default.
See [diagnostics.md](./diagnostics.md).

### utils

Cross-cutting helpers used by every feature module. Two groups live here. First,
Expand Down
87 changes: 87 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# SDK Diagnostics and Safe Support Workflows

Opt-in diagnostics help apps and support engineers debug configuration, network
state, transaction lifecycle, wallet capability, and vault readiness **without**
leaking secret keys, seed phrases, signed XDR, or other sensitive material.

> [!CAUTION]
> Diagnostics are **off by default**. Never enable event hooks in production
> log pipelines that are not already scrubbed. Even with redaction, prefer the
> structured `buildDiagnosticsReport()` snapshot for support tickets over
> dumping raw console output.

## Quick start

```ts
import {
enableDiagnostics,
disableDiagnostics,
buildDiagnosticsReport,
createWallet,
type DiagnosticsEvent,
} from 'stellar-pocketpay-sdk';

const events: DiagnosticsEvent[] = [];

enableDiagnostics({
hooks: {
onEvent: (event) => {
// Already redacted — safe to forward to your logger
console.debug('[pocketpay]', event.domain, event.type, event.data);
events.push(event);
},
},
});

createWallet(); // emits wallet.created with publicKey only (no secretKey value)

const report = buildDiagnosticsReport({ network: 'testnet' });
// Attach `report` to a support ticket — it contains URLs, capability status,
// and vault readiness flags, never signing material.

disableDiagnostics(); // clear hooks when done
```

Environment note: `POCKETPAY_DEBUG=true` alone does **not** start emitting
events. You must still call `enableDiagnostics` / `setDiagnosticsHooks`. Debug
mode expectations for application loggers remain documented in
[logging-payloads-and-debug.md](./logging-payloads-and-debug.md).

## What is safe vs never shared

| Value | In events / report? |
| --- | --- |
| Public key (`G…`), tx hash, ledger, network name, Horizon/Soroban URLs | Yes |
| Capability status, vault readiness, timeout | Yes |
| Contract id (`C…`) when configured | Yes (on-chain public) |
| Secret key, mnemonic, seed, signed XDR, signatures, `sourceSecret` | **Never** — replaced with `[REDACTED]` |
| Memo text | Not included in diagnostics events by default |

Redaction is implemented by `redactDiagnosticsValue` using the deny-list in
`DIAGNOSTICS_SENSITIVE_KEYS`, plus string scrubbing for embedded `S…` keys.

## Lifecycle events (when enabled)

| Domain | Example `type` | Typical `data` |
| --- | --- | --- |
| `config` | `config.resolved` | network, URLs, timeout, `contractIdConfigured` |
| `wallet` | `wallet.created` / `wallet.imported` | `publicKey`, `hasSecretKey: true` |
| `transaction` | `transaction.submit.*` / `transaction.history.fetched` | txHash, counts — not envelopes |
| `network` | `network.retry.attempt` | attempt, outcome kind, delayMs, txHash |
| `vault` | `vault.readiness` | `ready`, operation, configuration flags |

## Support workflow

1. Reproduce with diagnostics enabled in a **non-production** environment.
2. Call `buildDiagnosticsReport()` and save the JSON (no secrets).
3. Collect redacted event traces for the failing operation (`type` + `data`).
4. Share the report + event types with support — **never** paste wallet backups,
seed phrases, or signed XDR.
5. Call `disableDiagnostics()` when finished.

## Related docs

- [Logging Guidance](./logging.md)
- [Logging: payloads and debug mode](./logging-payloads-and-debug.md)
- [Security Best Practices](./security.md)
- [Support Policy](./support-policy.md)
1 change: 1 addition & 0 deletions docs/logging-payloads-and-debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ console.debug(message, redactSensitive(context));

## See also

- [SDK Diagnostics](./diagnostics.md) — Opt-in lifecycle hooks and support-safe reports with strict redaction.
- [Logging Guidance](./logging.md) - Base safe-logging practices and the safe-identifier table.
- [Error Handling](./error-handling.md) - Logging `PocketPayError` without leaking context.
- [Security Best Practices](./security.md) - Key management and transaction safety.
13 changes: 13 additions & 0 deletions docs/support-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ non-Node runtimes are tracked as future work.

CI runs on **Node 22** via GitHub Actions. See `.github/workflows/ci.yml`.

## Safe support diagnostics

When filing or answering support requests about SDK behaviour, use the opt-in
diagnostics APIs documented in [diagnostics.md](./diagnostics.md):

- `buildDiagnosticsReport()` — shareable JSON snapshot (config, network,
capabilities, vault readiness) with secrets redacted.
- `enableDiagnostics({ hooks })` — lifecycle events that never include secret
keys, mnemonics, or signed XDR.

Do **not** ask users for secret keys, seed phrases, or signed transaction
envelopes.

## Maintenance status

| Aspect | Status |
Expand Down
14 changes: 13 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PocketPayError,
} from '../types';
import { redactSensitive } from '../utils';
import { emitDiagnosticsEvent } from '../diagnostics/hooks';
// ─── Default URLs ───────────────────────────────────────────────────────────
const HORIZON_URLS: Record<StellarNetwork, string> = {
testnet: 'https://horizon-testnet.stellar.org',
Expand Down Expand Up @@ -249,7 +250,18 @@ export function resolveConfig(overrides?: Partial<SDKConfig>): SDKConfig {
validateContractId(contractId);
}

return { network, horizonUrl, sorobanRpcUrl, timeout, contractId };
const resolved = { network, horizonUrl, sorobanRpcUrl, timeout, contractId };

emitDiagnosticsEvent('config', 'config.resolved', {
network: resolved.network,
horizonUrl: resolved.horizonUrl,
sorobanRpcUrl: resolved.sorobanRpcUrl,
timeoutMs: resolved.timeout,
contractIdConfigured:
typeof resolved.contractId === 'string' && resolved.contractId.length > 0,
});

return resolved;
}

/**
Expand Down
99 changes: 99 additions & 0 deletions src/diagnostics/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Opt-in diagnostics hook registry.
*
* Mirrors the Horizon server factory pattern: disabled by default, process-local,
* and resettable for tests. Emit is a no-op unless diagnostics are enabled.
*/

import { redactDiagnosticsValue } from './redact';
import type {
DiagnosticsDomain,
DiagnosticsEvent,
DiagnosticsHooks,
EnableDiagnosticsOptions,
} from './types';

let hooks: DiagnosticsHooks | null = null;
let explicitlyEnabled = false;

function envDebugEnabled(): boolean {
return process.env.POCKETPAY_DEBUG === 'true';
}

/**
* Enable diagnostics and optionally register lifecycle hooks.
* Off by default — call this explicitly from app bootstrap or support tooling.
*/
export function enableDiagnostics(options: EnableDiagnosticsOptions = {}): void {
explicitlyEnabled = true;
if (options.hooks) {
hooks = { ...options.hooks };
}
}

/**
* Replace the active hooks without toggling the enabled flag.
* Prefer {@link enableDiagnostics} for first-time setup.
*/
export function setDiagnosticsHooks(next: DiagnosticsHooks): void {
hooks = { ...next };
explicitlyEnabled = true;
}

/**
* Disable diagnostics and clear hooks.
*/
export function disableDiagnostics(): void {
explicitlyEnabled = false;
hooks = null;
}

/**
* Alias for {@link disableDiagnostics} — matches `resetHorizonServerFactory`.
*/
export function resetDiagnosticsHooks(): void {
disableDiagnostics();
}

/**
* True when diagnostics are explicitly enabled, or when `POCKETPAY_DEBUG=true`
* and hooks have been registered (env alone does not fire events with no hooks).
*/
export function isDiagnosticsEnabled(): boolean {
if (explicitlyEnabled) return true;
return envDebugEnabled() && hooks !== null;
}

/** Current hooks (or null). */
export function getDiagnosticsHooks(): DiagnosticsHooks | null {
return hooks;
}

/**
* Emit a lifecycle event to the registered hook when diagnostics are enabled.
* Always deep-redacts `data` before delivery. Hook errors are swallowed so
* observability never breaks SDK call paths.
*/
export function emitDiagnosticsEvent(
domain: DiagnosticsDomain,
type: string,
data: Record<string, unknown> = {},
): void {
if (!isDiagnosticsEnabled()) return;
const onEvent = hooks?.onEvent;
if (!onEvent) return;

const event: DiagnosticsEvent = {
schemaVersion: 1,
domain,
type,
timestamp: new Date().toISOString(),
data: redactDiagnosticsValue(data) as Record<string, unknown>,
};

try {
onEvent(event);
} catch {
// Never let a broken consumer hook fail the SDK operation.
}
}
40 changes: 40 additions & 0 deletions src/diagnostics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* SDK diagnostics and observability (opt-in, redacted).
*
* @packageDocumentation
*/

export type {
DiagnosticsDomain,
DiagnosticsHooks,
EnableDiagnosticsOptions,
SafeConfigSnapshot,
SafeNetworkSnapshot,
CapabilityDiagnosticsEntry,
WalletCapabilitySnapshot,
VaultReadinessSnapshot,
DiagnosticsReport,
DiagnosticsEvent,
DiagnosticsSensitiveKey,
} from './types';

export { DIAGNOSTICS_SENSITIVE_KEYS } from './types';

export {
redactDiagnosticsValue,
redactDiagnosticsString,
isDiagnosticsSensitiveKey,
DIAGNOSTICS_REDACTED_PLACEHOLDER,
} from './redact';

export {
enableDiagnostics,
disableDiagnostics,
setDiagnosticsHooks,
resetDiagnosticsHooks,
isDiagnosticsEnabled,
getDiagnosticsHooks,
emitDiagnosticsEvent,
} from './hooks';

export { buildDiagnosticsReport } from './report';
Loading