release(v4.0.0): Shade GA — V3.x consolidation + audit prep
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
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
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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@shade/files",
|
||||
"version": "0.3.0",
|
||||
"version": "4.0.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@shade/core": "workspace:*",
|
||||
"@shade/crypto-web": "workspace:*",
|
||||
"@shade/observability": "workspace:*",
|
||||
"@shade/proto": "workspace:*",
|
||||
"@shade/sdk": "workspace:*",
|
||||
"@shade/streams": "workspace:*",
|
||||
|
||||
@@ -80,9 +80,13 @@ export function createFilesNamespace(shade: Shade): FilesNamespace {
|
||||
if (state.serverBridge === null) {
|
||||
state.serverBridge = await createServerStreamsBridge(shade);
|
||||
}
|
||||
const inheritedObservability = shade.getObservability?.();
|
||||
const handler = createFileHandler(shade, {
|
||||
...handlerConfig,
|
||||
streamsBridge: state.serverBridge,
|
||||
...(handlerConfig.observability === undefined && inheritedObservability !== undefined
|
||||
? { observability: inheritedObservability }
|
||||
: {}),
|
||||
});
|
||||
const detach = attachFileHandler(state.channel, handler);
|
||||
state.serverHandler = handler;
|
||||
|
||||
@@ -79,6 +79,17 @@ import {
|
||||
NOOP_METRIC_SINK,
|
||||
type MetricSink,
|
||||
} from './metrics.js';
|
||||
import {
|
||||
ATTR_BYTES_BIN,
|
||||
ATTR_ERROR_CODE,
|
||||
ATTR_OP,
|
||||
ATTR_PEER_HASH,
|
||||
ATTR_RESULT,
|
||||
bytesBin,
|
||||
NOOP_HOOK,
|
||||
peerHash,
|
||||
type ObservabilityHook,
|
||||
} from '@shade/observability';
|
||||
import {
|
||||
CustomArgsSchema,
|
||||
CustomResultSchema,
|
||||
@@ -153,6 +164,12 @@ export interface FileHandlerConfig extends FileHandlerOps {
|
||||
isFingerprintVerified?: (sender: string) => boolean | Promise<boolean>;
|
||||
/** Vendor-neutral metrics sink. */
|
||||
onMetric?: MetricSink;
|
||||
/**
|
||||
* Optional OTel observability hook. When supplied, each op is wrapped in
|
||||
* a `shade.files.op` span with PII-safe attributes (peer.hash, op,
|
||||
* bytes.bin, result, error.code). Defaults to no-op when omitted.
|
||||
*/
|
||||
observability?: ObservabilityHook;
|
||||
/** Called BEFORE the handler runs. Throw to deny. */
|
||||
beforeOp?: (ctx: OpContext<unknown>) => void | Promise<void>;
|
||||
/** Called AFTER the handler returns. Result is the validated response. */
|
||||
@@ -207,12 +224,34 @@ export function createFileHandler(
|
||||
const defaultTimeoutMs = config.defaultTimeoutMs ?? 60_000;
|
||||
const ioTimeoutMs = config.ioTimeoutMs ?? 60_000;
|
||||
const metrics: MetricSink = config.onMetric ?? NOOP_METRIC_SINK;
|
||||
const observability: ObservabilityHook = config.observability ?? NOOP_HOOK;
|
||||
const customRegistrations = config.custom ?? {};
|
||||
const isCustomKind = (kind: string): boolean => kind === 'shade.fs.custom/v1';
|
||||
|
||||
async function handleRequest(
|
||||
from: string,
|
||||
request: RpcRequest,
|
||||
): Promise<RpcResponse | RpcError> {
|
||||
const span = observability.startSpan('shade.files.op', {
|
||||
[ATTR_PEER_HASH]: peerHash(from),
|
||||
});
|
||||
try {
|
||||
const out = await runHandleRequest(from, request, span);
|
||||
return out;
|
||||
} catch (err) {
|
||||
span.recordException(err);
|
||||
span.setAttribute(ATTR_ERROR_CODE, errorCodeOf(err));
|
||||
span.setStatus('error');
|
||||
throw err;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function runHandleRequest(
|
||||
from: string,
|
||||
request: RpcRequest,
|
||||
span: import('@shade/observability').Span,
|
||||
): Promise<RpcResponse | RpcError> {
|
||||
// 0. Replay-window check (independent of sig — defends against
|
||||
// intercept-and-resend even when sig verification is disabled).
|
||||
@@ -316,6 +355,7 @@ export function createFileHandler(
|
||||
// Replace payload with validated value (Zod may apply defaults).
|
||||
(parsedArgs as CustomArgs).payload = payloadParse.data;
|
||||
}
|
||||
span.setAttribute(ATTR_OP, resolvedOpKind);
|
||||
|
||||
// 3. Path validation (skip ops without a path)
|
||||
let primaryPath = '';
|
||||
@@ -488,6 +528,10 @@ export function createFileHandler(
|
||||
const durationMs = Date.now() - startedAt;
|
||||
metrics(METRIC_OP_DURATION_MS, durationMs, { op: resolvedOpKind, result: 'error' });
|
||||
metrics(METRIC_OP_TOTAL, 1, { op: resolvedOpKind, result: 'error' });
|
||||
span.setAttribute(ATTR_RESULT, 'error');
|
||||
span.setAttribute(ATTR_ERROR_CODE, errorCodeOf(err));
|
||||
span.recordException(err);
|
||||
span.setStatus('error');
|
||||
cleanup({ release: true });
|
||||
if (config.onError !== undefined) {
|
||||
try {
|
||||
@@ -543,6 +587,7 @@ export function createFileHandler(
|
||||
const durationMs = Date.now() - startedAt;
|
||||
metrics(METRIC_OP_DURATION_MS, durationMs, { op: resolvedOpKind, result: 'ok' });
|
||||
metrics(METRIC_OP_TOTAL, 1, { op: resolvedOpKind, result: 'ok' });
|
||||
span.setAttribute(ATTR_RESULT, 'ok');
|
||||
if (estimatedBytes > 0) {
|
||||
// Inbound bytes (write) vs outbound (read) — both reuse the same
|
||||
// pre-call `estimatedBytes`, since post-execution reconciliation
|
||||
@@ -551,7 +596,9 @@ export function createFileHandler(
|
||||
if (direction !== null) {
|
||||
metrics(direction, estimatedBytes, { op: resolvedOpKind });
|
||||
}
|
||||
span.setAttribute(ATTR_BYTES_BIN, bytesBin(estimatedBytes));
|
||||
}
|
||||
span.setStatus('ok');
|
||||
|
||||
return makeResponseEnvelope(request, resultParse.data);
|
||||
|
||||
@@ -654,6 +701,17 @@ function makeResponseEnvelope(req: RpcRequest, result: unknown): RpcResponse {
|
||||
};
|
||||
}
|
||||
|
||||
function errorCodeOf(err: unknown): string {
|
||||
if (err === null || err === undefined) return 'SHADE_UNKNOWN';
|
||||
if (typeof err === 'object') {
|
||||
const code = (err as { code?: unknown }).code;
|
||||
if (typeof code === 'string' && code.length > 0) return code;
|
||||
const name = (err as { name?: unknown }).name;
|
||||
if (typeof name === 'string' && name.length > 0) return name;
|
||||
}
|
||||
return 'SHADE_UNKNOWN';
|
||||
}
|
||||
|
||||
function makeErrorEnvelope(req: RpcRequest, err: unknown): RpcError {
|
||||
return {
|
||||
kind: 'shade.fs.error/v1',
|
||||
|
||||
Reference in New Issue
Block a user