Files
Shade/packages/shade-observability/src/attributes.ts
Sterister e6fdf31b49
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.

Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
  Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
  bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
  shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
  non-realtime stack.

Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
  version 4.0.0 on /health.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00

123 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Safe-attribute helpers — these are the ONLY attribute keys/values that
* Shade internals are allowed to put on spans. The PII-policy guarantees:
*
* - No plaintext peer addresses (use `peerHash`).
* - No plaintext payloads.
* - No exact byte counts for stream content (use `bytesBin`).
* - Counters/codes are fine.
*
* Custom-op authors who want to add their own attributes MUST go through
* `safeAttribute()`, which rejects keys/values that look like PII.
*/
import { sha256 } from '@noble/hashes/sha2.js';
// ─── Standard attribute keys ─────────────────────────────────
export const ATTR_PEER_HASH = 'shade.peer.hash';
export const ATTR_BYTES_BIN = 'shade.bytes.bin';
export const ATTR_LANE_COUNT = 'shade.lane.count';
export const ATTR_LANE_ID = 'shade.lane.id';
export const ATTR_RETRY_COUNT = 'shade.retry.count';
export const ATTR_ERROR_CODE = 'shade.error.code';
export const ATTR_OP = 'shade.op';
export const ATTR_ROUTE = 'shade.route';
export const ATTR_HTTP_STATUS = 'shade.http.status';
export const ATTR_DIRECTION = 'shade.direction';
export const ATTR_PARTITION = 'shade.partition';
export const ATTR_RESULT = 'shade.result';
/**
* Forbidden substrings — if these appear in attribute keys we refuse.
* Mirrors the intent of the PII policy doc: never log addresses or
* exact-byte sizes.
*/
const FORBIDDEN_KEY_FRAGMENTS = [
'peer.address',
'peer_address',
'plaintext',
'payload',
'bytes.exact',
'bytes_exact',
];
/** 8-byte stable pseudonym derived from a peer address. */
export function peerHash(address: string): string {
const enc = new TextEncoder().encode(address);
const digest = sha256(enc);
let hex = '';
for (let i = 0; i < 4; i++) hex += digest[i]!.toString(16).padStart(2, '0');
return hex;
}
/** Bin a byte count into a coarse PII-safe bucket. */
export function bytesBin(n: number): string {
if (!Number.isFinite(n) || n < 0) return 'unknown';
if (n <= 4 * 1024) return '≤4KB';
if (n <= 64 * 1024) return '464KB';
if (n <= 1024 * 1024) return '64KB1MB';
if (n <= 10 * 1024 * 1024) return '110MB';
if (n <= 100 * 1024 * 1024) return '10100MB';
if (n <= 1024 * 1024 * 1024) return '100MB1GB';
return '≥1GB';
}
/** Bin a lane count into a stable bucket. */
export function laneCountBin(n: number): number {
if (n <= 1) return 1;
if (n <= 4) return 4;
if (n <= 16) return 16;
if (n <= 64) return 64;
return 64;
}
export class UnsafeAttributeError extends Error {
override readonly name = 'UnsafeAttributeError';
constructor(reason: string) {
super(reason);
}
}
/**
* Validate a user-supplied custom attribute. Returns the key/value pair
* untouched on success, or throws `UnsafeAttributeError`. Use this in
* any code path that accepts attributes from outside Shade's own
* helpers (e.g. plugin-supplied tags).
*/
export function safeAttribute(
key: string,
value: string | number | boolean,
): { key: string; value: string | number | boolean } {
const lower = key.toLowerCase();
for (const frag of FORBIDDEN_KEY_FRAGMENTS) {
if (lower.includes(frag)) {
throw new UnsafeAttributeError(`attribute key "${key}" is PII-unsafe (contains "${frag}")`);
}
}
if (typeof value === 'string') {
if (value.length > 256) {
throw new UnsafeAttributeError(
`attribute "${key}" value too long (${value.length}B); cap at 256B to avoid embedded PII`,
);
}
if (looksLikeAddress(value)) {
throw new UnsafeAttributeError(
`attribute "${key}" value looks like a peer address — use peerHash() first`,
);
}
}
return { key, value };
}
function looksLikeAddress(s: string): boolean {
// Heuristic: emails, "device:UUID", and DID-style identifiers all look
// like PII to the grep tester. Hashes (8 hex chars) and small ints are
// fine.
if (/^[a-f0-9]{1,16}$/i.test(s)) return false;
if (/@/.test(s)) return true;
if (/^device:/i.test(s)) return true;
if (/^did:/i.test(s)) return true;
return false;
}