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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ Clients can bypass the cache by sending a `Cache-Control: no-cache` request head
| Endpoint | Cache key format | TTL |
|-----------------------------------------|------------------------------------------------------------|----------|
| `GET /api/marketplace` | `marketplace:<tenantId>:<originalUrl>` | 15s |
| `GET /api/investor/locks` | `investor:locks:<tenantId>:<principalScope>:<originalUrl>` | 15s |
| `GET /api/investor/locks/:invoiceId` | `investor:lock:<tenantId>:<principalScope>:<invoiceId>:<funderAddress>` | 15s |
| `GET /api/investor/locks` | `investor:locks:<tenantId>:sha256(<principalScope>):<path>?<normalizedQuery>` | 15s |
| `GET /api/investor/locks/:invoiceId` | `investor:lock:<tenantId>:sha256(<principalScope>):<invoiceId>:sha256(<funderAddress>)` | 15s |

For investor-lock responses, `<principalScope>` is `admin:<role>` for tenant-wide admin or owner reads, or `funder:<boundAddress>` for non-admin investor reads. This prevents one authenticated principal from receiving another principal's cached lock response when the URL is otherwise identical.
For investor-lock responses, `<principalScope>` is `admin:<role>` for tenant-wide admin or owner reads, or `funder:<boundAddress>` for non-admin investor reads. The cache key hashes principal scope and `funderAddress` query values, and normalizes query parameter ordering. This prevents one authenticated principal from receiving another principal's cached lock response when the URL is otherwise identical without storing raw funder addresses in cache keys.

### Tenant isolation

Expand Down
76 changes: 28 additions & 48 deletions src/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* - Standardized log levels
* - Request correlation via request IDs
* - Automatic enrichment from the AsyncLocalStorage request context
* (requestId, correlationId, tenantId, userId) no manual threading needed.
* (requestId, correlationId, tenantId, userId) with no manual threading.
*
* @module logger
*/
Expand Down Expand Up @@ -51,57 +51,37 @@ const _base = pino(
);

/**
* Build a merged bindings object from the ambient context plus any
* caller-supplied overrides. Explicit values always win.
*
* @param {Record<string, unknown>} [overrides] - Per-call bindings.
* @returns {Record<string, unknown>} Merged bindings.
*/
function _mergeContext(overrides) {
const ctx = getContext();
// Ambient context first so caller overrides take precedence.
return Object.keys(ctx).length === 0 && !overrides
? {}
: { ...ctx, ...overrides };
}

/**
* Thin proxy that enriches every log call with the ambient request context.
* Explicit per-call fields passed to `logger.info({ … }, msg)` override the
* ambient values for that call only.
* Logger instance with stable own level methods. Keeping the wrappers as
* assignable properties lets Jest spy on `logger.warn` without recursing back
* through the wrapped Pino method.
*
* @type {import('pino').Logger}
*/
const logger = new Proxy(_base, {
get(target, prop) {
const LEVEL_METHODS = new Set(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
if (typeof prop === 'string' && LEVEL_METHODS.has(prop)) {
return function enrichedLog(objOrMsg, ...rest) {
const ctx = getContext();
const hasCtx = Object.keys(ctx).length > 0;

if (!hasCtx) {
// No ambient context — call through unchanged (background jobs).
return target[prop](objOrMsg, ...rest);
}

if (typeof objOrMsg === 'string') {
// Signature: logger.info('message')
return target[prop]({ ...ctx }, objOrMsg, ...rest);
}

if (objOrMsg && typeof objOrMsg === 'object') {
// Signature: logger.info({ key: val }, 'message')
// Explicit fields override ambient.
return target[prop]({ ...ctx, ...objOrMsg }, ...rest);
}

return target[prop](objOrMsg, ...rest);
};
const logger = _base;
const LEVEL_METHODS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];

for (const level of LEVEL_METHODS) {
const write = _base[level].bind(_base);

logger[level] = function enrichedLog(objOrMsg, ...rest) {
const ctx = getContext();
const hasCtx = Object.keys(ctx).length > 0;

if (!hasCtx) {
return write(objOrMsg, ...rest);
}
return target[prop];
},
});

if (typeof objOrMsg === 'string') {
return write({ ...ctx }, objOrMsg, ...rest);
}

if (objOrMsg && typeof objOrMsg === 'object') {
return write({ ...ctx, ...objOrMsg }, ...rest);
}

return write(objOrMsg, ...rest);
};
}

/**
* Create a per-request child logger bound only with safe correlation fields.
Expand Down
Loading
Loading