@nodii/grpc-interceptors 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compose.d.ts +7 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +20 -0
- package/dist/compose.js.map +1 -0
- package/dist/configure.d.ts +66 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +169 -0
- package/dist/configure.js.map +1 -0
- package/dist/factory.d.ts +7 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +100 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/dist/interceptors/audit-context.d.ts +3 -0
- package/dist/interceptors/audit-context.d.ts.map +1 -0
- package/dist/interceptors/audit-context.js +59 -0
- package/dist/interceptors/audit-context.js.map +1 -0
- package/dist/interceptors/audit.d.ts +7 -0
- package/dist/interceptors/audit.d.ts.map +1 -0
- package/dist/interceptors/audit.js +111 -0
- package/dist/interceptors/audit.js.map +1 -0
- package/dist/interceptors/cancellation-guard.d.ts +3 -0
- package/dist/interceptors/cancellation-guard.d.ts.map +1 -0
- package/dist/interceptors/cancellation-guard.js +54 -0
- package/dist/interceptors/cancellation-guard.js.map +1 -0
- package/dist/interceptors/deadline-guard.d.ts +3 -0
- package/dist/interceptors/deadline-guard.d.ts.map +1 -0
- package/dist/interceptors/deadline-guard.js +90 -0
- package/dist/interceptors/deadline-guard.js.map +1 -0
- package/dist/interceptors/enrich-auth.d.ts +3 -0
- package/dist/interceptors/enrich-auth.d.ts.map +1 -0
- package/dist/interceptors/enrich-auth.js +85 -0
- package/dist/interceptors/enrich-auth.js.map +1 -0
- package/dist/interceptors/error-map.d.ts +4 -0
- package/dist/interceptors/error-map.d.ts.map +1 -0
- package/dist/interceptors/error-map.js +94 -0
- package/dist/interceptors/error-map.js.map +1 -0
- package/dist/interceptors/logging.d.ts +7 -0
- package/dist/interceptors/logging.d.ts.map +1 -0
- package/dist/interceptors/logging.js +66 -0
- package/dist/interceptors/logging.js.map +1 -0
- package/dist/interceptors/saga-headers.d.ts +23 -0
- package/dist/interceptors/saga-headers.d.ts.map +1 -0
- package/dist/interceptors/saga-headers.js +40 -0
- package/dist/interceptors/saga-headers.js.map +1 -0
- package/dist/interceptors/signal-binder.d.ts +3 -0
- package/dist/interceptors/signal-binder.d.ts.map +1 -0
- package/dist/interceptors/signal-binder.js +83 -0
- package/dist/interceptors/signal-binder.js.map +1 -0
- package/dist/interceptors/tenant-context.d.ts +3 -0
- package/dist/interceptors/tenant-context.d.ts.map +1 -0
- package/dist/interceptors/tenant-context.js +40 -0
- package/dist/interceptors/tenant-context.js.map +1 -0
- package/dist/interceptors-exports.d.ts +9 -0
- package/dist/interceptors-exports.d.ts.map +1 -0
- package/dist/interceptors-exports.js +12 -0
- package/dist/interceptors-exports.js.map +1 -0
- package/dist/reexports.d.ts +10 -0
- package/dist/reexports.d.ts.map +1 -0
- package/dist/reexports.js +27 -0
- package/dist/reexports.js.map +1 -0
- package/dist/telemetry-shim.d.ts +12 -0
- package/dist/telemetry-shim.d.ts.map +1 -0
- package/dist/telemetry-shim.js +94 -0
- package/dist/telemetry-shim.js.map +1 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +36 -0
- package/dist/types.js.map +1 -0
- package/package.json +14 -7
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// withAuditUnary — spec § 5.6.
|
|
2
|
+
//
|
|
3
|
+
// Emits the *framing* row on every gRPC call (mutator + audited reads,
|
|
4
|
+
// including rejections). The handler is still responsible for the
|
|
5
|
+
// state-change row inside its own tx via `audit.emit(tx, ...)`. The
|
|
6
|
+
// framing + state-change row join via `correlation_id`.
|
|
7
|
+
//
|
|
8
|
+
// Routes through `@nodii/telemetry.audit.emit` (Pattern C) by default;
|
|
9
|
+
// best-effort — emitter failures log and proceed per audit doctrine § 6.2.
|
|
10
|
+
import { resolveTelemetry } from "../telemetry-shim";
|
|
11
|
+
import { GrpcStatusError } from "../types";
|
|
12
|
+
function mapOutcome(code, detail) {
|
|
13
|
+
switch (code) {
|
|
14
|
+
case "OK":
|
|
15
|
+
case "INVALID_ARGUMENT":
|
|
16
|
+
case "NOT_FOUND":
|
|
17
|
+
case "ALREADY_EXISTS":
|
|
18
|
+
return "success";
|
|
19
|
+
case "UNAUTHENTICATED":
|
|
20
|
+
case "PERMISSION_DENIED":
|
|
21
|
+
return "denied_by_authz";
|
|
22
|
+
case "FAILED_PRECONDITION":
|
|
23
|
+
if (detail && /mfa(?:_|-)required|mfa(?:_|-)stale/i.test(detail)) {
|
|
24
|
+
return "denied_by_mfa";
|
|
25
|
+
}
|
|
26
|
+
return "success";
|
|
27
|
+
default:
|
|
28
|
+
return "failure";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function classify(err) {
|
|
32
|
+
if (err instanceof GrpcStatusError)
|
|
33
|
+
return { code: err.code, detail: err.detail };
|
|
34
|
+
return { code: "INTERNAL" };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the audit interceptor. Sits between logging and auth in the
|
|
38
|
+
* locked composition.
|
|
39
|
+
*/
|
|
40
|
+
export function withAuditUnary(config = {}) {
|
|
41
|
+
const auditOnRead = config.auditOnReadMethods ?? new Set();
|
|
42
|
+
const skip = config.skipMethods ?? new Set();
|
|
43
|
+
return (methodName, handler) => {
|
|
44
|
+
return async (call) => {
|
|
45
|
+
if (skip.has(methodName))
|
|
46
|
+
return handler(call);
|
|
47
|
+
const t = resolveTelemetry();
|
|
48
|
+
const emitter = config.emitter ?? t.audit;
|
|
49
|
+
const log = t.logger;
|
|
50
|
+
const isAuditOnRead = auditOnRead.has(methodName) || isLikelyMutator(methodName);
|
|
51
|
+
let resultErr;
|
|
52
|
+
try {
|
|
53
|
+
return await handler(call);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
resultErr = err;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (isAuditOnRead) {
|
|
61
|
+
const code = resultErr
|
|
62
|
+
? classify(resultErr).code
|
|
63
|
+
: "OK";
|
|
64
|
+
const detail = resultErr ? classify(resultErr).detail : undefined;
|
|
65
|
+
const outcome = mapOutcome(code, detail);
|
|
66
|
+
try {
|
|
67
|
+
await emitter.emit({
|
|
68
|
+
action: `rpc.${methodName.replace(/^\/+/, "")}`,
|
|
69
|
+
target_kind: "rpc",
|
|
70
|
+
target_id: null,
|
|
71
|
+
payload: {
|
|
72
|
+
outcome,
|
|
73
|
+
code,
|
|
74
|
+
detail,
|
|
75
|
+
correlation_id: call.auth?.requestContext?.correlationId,
|
|
76
|
+
actor_kind: deriveActorKind(call),
|
|
77
|
+
tenant_id: call.auth?.requestContext?.tenant_id ?? null,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (emitErr) {
|
|
82
|
+
log.error("audit.emit_failed", {
|
|
83
|
+
method: methodName,
|
|
84
|
+
error: emitErr instanceof Error ? emitErr.message : String(emitErr),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function deriveActorKind(call) {
|
|
93
|
+
if (call.auth?.userActor?.user_id)
|
|
94
|
+
return "user";
|
|
95
|
+
if (call.auth?.serviceActor?.service_id || call.auth?.serviceActor?.serviceId)
|
|
96
|
+
return "service";
|
|
97
|
+
return "unknown";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Heuristic: at v0.1.0 we emit a framing row for every method unless the
|
|
101
|
+
* service explicitly opts out via `skipMethods` (e.g. health probes).
|
|
102
|
+
* Reads opt-in via `auditOnReadMethods`. We err on the side of emitting
|
|
103
|
+
* more rows; the audit doctrine § 6.6 default is no-audit on reads, but
|
|
104
|
+
* the spec § 5.6 says "every gRPC call including rejections" gets a
|
|
105
|
+
* framing row — so this returns `true` by default. Services can suppress
|
|
106
|
+
* via `skipMethods`.
|
|
107
|
+
*/
|
|
108
|
+
function isLikelyMutator(_methodName) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/interceptors/audit.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,EAAE;AACF,uEAAuE;AACvE,kEAAkE;AAClE,oEAAoE;AACpE,wDAAwD;AACxD,EAAE;AACF,uEAAuE;AACvE,2EAA2E;AAE3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQrD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAI3C,SAAS,UAAU,CAAC,IAAoB,EAAE,MAAe;IACvD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,IAAI,CAAC;QACV,KAAK,kBAAkB,CAAC;QACxB,KAAK,WAAW,CAAC;QACjB,KAAK,gBAAgB;YACnB,OAAO,SAAS,CAAC;QACnB,KAAK,iBAAiB,CAAC;QACvB,KAAK,mBAAmB;YACtB,OAAO,iBAAiB,CAAC;QAC3B,KAAK,qBAAqB;YACxB,IAAI,MAAM,IAAI,qCAAqC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACjE,OAAO,eAAe,CAAC;YACzB,CAAC;YACD,OAAO,SAAS,CAAC;QACnB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,IAAI,GAAG,YAAY,eAAe;QAChC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IAChD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAC9B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAAsB,EAAE;IACrD,MAAM,WAAW,GAAG,MAAM,CAAC,kBAAkB,IAAI,IAAI,GAAG,EAAU,CAAC;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,IAAI,IAAI,GAAG,EAAU,CAAC;IAErD,OAAO,CAAC,UAAkB,EAAE,OAAqB,EAAgB,EAAE;QACjE,OAAO,KAAK,EAAE,IAAe,EAAoB,EAAE;YACjD,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;YAE/C,MAAM,CAAC,GAAG,gBAAgB,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,KAAK,CAAC;YAC1C,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;YACrB,MAAM,aAAa,GACjB,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,eAAe,CAAC,UAAU,CAAC,CAAC;YAE7D,IAAI,SAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,SAAS,GAAG,GAAG,CAAC;gBAChB,MAAM,GAAG,CAAC;YACZ,CAAC;oBAAS,CAAC;gBACT,IAAI,aAAa,EAAE,CAAC;oBAClB,MAAM,IAAI,GAAmB,SAAS;wBACpC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI;wBAC1B,CAAC,CAAC,IAAI,CAAC;oBACT,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;oBAClE,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;oBACzC,IAAI,CAAC;wBACH,MAAM,OAAO,CAAC,IAAI,CAAC;4BACjB,MAAM,EAAE,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE;4BAC/C,WAAW,EAAE,KAAK;4BAClB,SAAS,EAAE,IAAI;4BACf,OAAO,EAAE;gCACP,OAAO;gCACP,IAAI;gCACJ,MAAM;gCACN,cAAc,EAAE,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,aAAa;gCACxD,UAAU,EAAE,eAAe,CAAC,IAAI,CAAC;gCACjC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,IAAI,IAAI;6BACxD;yBACF,CAAC,CAAC;oBACL,CAAC;oBAAC,OAAO,OAAO,EAAE,CAAC;wBACjB,GAAG,CAAC,KAAK,CAAC,mBAAmB,EAAE;4BAC7B,MAAM,EAAE,UAAU;4BAClB,KAAK,EACH,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;yBAC/D,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,IAAe;IACtC,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO;QAAE,OAAO,MAAM,CAAC;IACjD,IAAI,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,IAAI,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS;QAC3E,OAAO,SAAS,CAAC;IACnB,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,WAAmB;IAC1C,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellation-guard.d.ts","sourceRoot":"","sources":["../../src/interceptors/cancellation-guard.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,kBAAkB,EAGlB,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAKlB,wBAAgB,iBAAiB,CAC/B,MAAM,GAAE,kBAAuB,GAC9B,gBAAgB,CAgDlB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// cancellationGuard — spec § 5.11.
|
|
2
|
+
//
|
|
3
|
+
// Innermost interceptor before the handler. Registers a cancel callback
|
|
4
|
+
// on the call; on cancellation it runs service-registered cancel
|
|
5
|
+
// handlers + races them against `gracePeriodMs` (default 250ms). After
|
|
6
|
+
// the grace period (or if all handlers resolve sooner) the call short-
|
|
7
|
+
// circuits with `RpcCancelledError` (gRPC `CANCELLED context_cancelled`).
|
|
8
|
+
import { RpcCancelledError } from "../types";
|
|
9
|
+
const DEFAULT_GRACE_MS = 250;
|
|
10
|
+
export function cancellationGuard(config = {}) {
|
|
11
|
+
const graceMs = config.gracePeriodMs ?? DEFAULT_GRACE_MS;
|
|
12
|
+
const onCancel = config.onCancel ?? [];
|
|
13
|
+
return (_methodName, handler) => {
|
|
14
|
+
return async (call) => {
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
let cancellationPromise = null;
|
|
17
|
+
const onCancelled = () => {
|
|
18
|
+
cancelled = true;
|
|
19
|
+
// Fire handlers but cap waits at graceMs.
|
|
20
|
+
const handlers = Promise.allSettled(onCancel.map(async (h) => {
|
|
21
|
+
try {
|
|
22
|
+
await h();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* swallow */
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
const grace = new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
29
|
+
return Promise.race([handlers, grace]).then(() => {
|
|
30
|
+
throw new RpcCancelledError();
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
if (typeof call.on === "function") {
|
|
34
|
+
call.on("cancelled", () => {
|
|
35
|
+
if (!cancellationPromise)
|
|
36
|
+
cancellationPromise = onCancelled();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Fixture path: tests set `call.cancelled = true` before calling.
|
|
40
|
+
if (call.cancelled && !cancellationPromise) {
|
|
41
|
+
cancellationPromise = onCancelled();
|
|
42
|
+
}
|
|
43
|
+
const handlerPromise = handler(call);
|
|
44
|
+
if (cancellationPromise) {
|
|
45
|
+
return Promise.race([handlerPromise, cancellationPromise]);
|
|
46
|
+
}
|
|
47
|
+
const result = await handlerPromise;
|
|
48
|
+
if (cancelled)
|
|
49
|
+
throw new RpcCancelledError();
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=cancellation-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellation-guard.js","sourceRoot":"","sources":["../../src/interceptors/cancellation-guard.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,EAAE;AACF,wEAAwE;AACxE,iEAAiE;AACjE,uEAAuE;AACvE,uEAAuE;AACvE,0EAA0E;AAQ1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,MAAM,UAAU,iBAAiB,CAC/B,SAA6B,EAAE;IAE/B,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,IAAI,gBAAgB,CAAC;IACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;IAEvC,OAAO,CAAC,WAAmB,EAAE,OAAqB,EAAgB,EAAE;QAClE,OAAO,KAAK,EAAE,IAAe,EAAoB,EAAE;YACjD,IAAI,SAAS,GAAG,KAAK,CAAC;YACtB,IAAI,mBAAmB,GAA0B,IAAI,CAAC;YAEtD,MAAM,WAAW,GAAG,GAAmB,EAAE;gBACvC,SAAS,GAAG,IAAI,CAAC;gBACjB,0CAA0C;gBAC1C,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CACjC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;oBACvB,IAAI,CAAC;wBACH,MAAM,CAAC,EAAE,CAAC;oBACZ,CAAC;oBAAC,MAAM,CAAC;wBACP,aAAa;oBACf,CAAC;gBACH,CAAC,CAAC,CACH,CAAC;gBACF,MAAM,KAAK,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAC1C,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAC7B,CAAC;gBACF,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;oBAC/C,MAAM,IAAI,iBAAiB,EAAE,CAAC;gBAChC,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;YAEF,IAAI,OAAO,IAAI,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;gBAClC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;oBACxB,IAAI,CAAC,mBAAmB;wBAAE,mBAAmB,GAAG,WAAW,EAAE,CAAC;gBAChE,CAAC,CAAC,CAAC;YACL,CAAC;YACD,kEAAkE;YAClE,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBAC3C,mBAAmB,GAAG,WAAW,EAAE,CAAC;YACtC,CAAC;YAED,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,mBAAmB,EAAE,CAAC;gBACxB,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC,CAAC;YAC7D,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC;YACpC,IAAI,SAAS;gBAAE,MAAM,IAAI,iBAAiB,EAAE,CAAC;YAC7C,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadline-guard.d.ts","sourceRoot":"","sources":["../../src/interceptors/deadline-guard.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,cAAc,EAGd,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAoDlB,wBAAgB,aAAa,CAAC,MAAM,GAAE,cAAmB,GAAG,gBAAgB,CAgC3E"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// deadlineGuard — spec § 5.10.
|
|
2
|
+
//
|
|
3
|
+
// Reads inbound gRPC deadline from `call.metadata.grpc-timeout` (or
|
|
4
|
+
// `deadline` if the consumer's runtime exposes it). Reserves a `budgetMs`
|
|
5
|
+
// (default 50ms) at the boundary so outbound calls inherit a *shorter*
|
|
6
|
+
// deadline through the consumer's own client interceptor wiring.
|
|
7
|
+
//
|
|
8
|
+
// `call.auth.requestContext.deadlineMs` is set on the call so downstream
|
|
9
|
+
// interceptors / the handler can read the propagated deadline; the
|
|
10
|
+
// consumer's `createClientAuthInterceptor` reads it back to stamp on
|
|
11
|
+
// outbound metadata.
|
|
12
|
+
import { GrpcStatusError } from "../types";
|
|
13
|
+
const DEFAULT_BUDGET_MS = 50;
|
|
14
|
+
function readDeadlineMs(call) {
|
|
15
|
+
// Public field set by transport adapters (or by callers constructing
|
|
16
|
+
// a call directly — see `UnaryCall.deadlineMs`).
|
|
17
|
+
if (typeof call.deadlineMs === "number")
|
|
18
|
+
return call.deadlineMs;
|
|
19
|
+
// Honor a numeric metadata value next.
|
|
20
|
+
const values = call.metadata?.get("grpc-timeout");
|
|
21
|
+
if (values && values.length > 0) {
|
|
22
|
+
const v = values[0];
|
|
23
|
+
if (typeof v === "string") {
|
|
24
|
+
const parsed = parseGrpcTimeout(v);
|
|
25
|
+
if (parsed !== undefined)
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
else if (typeof v === "number") {
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Back-compat: pre-public-API fixtures may still inject `__deadlineMs`.
|
|
33
|
+
// DEPRECATED — scheduled for removal in @nodii/grpc-interceptors
|
|
34
|
+
// v0.3.0. Use the public `call.deadlineMs` field instead.
|
|
35
|
+
const dl = call.__deadlineMs;
|
|
36
|
+
if (typeof dl === "number")
|
|
37
|
+
return dl;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Parse the gRPC `grpc-timeout` metadata header (e.g. `100m`, `5S`). */
|
|
41
|
+
function parseGrpcTimeout(v) {
|
|
42
|
+
const m = /^([0-9]+)([HMSmun])$/.exec(v);
|
|
43
|
+
if (!m)
|
|
44
|
+
return undefined;
|
|
45
|
+
const n = Number(m[1]);
|
|
46
|
+
const unit = m[2];
|
|
47
|
+
switch (unit) {
|
|
48
|
+
case "H":
|
|
49
|
+
return n * 3600_000;
|
|
50
|
+
case "M":
|
|
51
|
+
return n * 60_000;
|
|
52
|
+
case "S":
|
|
53
|
+
return n * 1000;
|
|
54
|
+
case "m":
|
|
55
|
+
return n;
|
|
56
|
+
case "u":
|
|
57
|
+
return n / 1000;
|
|
58
|
+
case "n":
|
|
59
|
+
return n / 1_000_000;
|
|
60
|
+
default:
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function deadlineGuard(config = {}) {
|
|
65
|
+
const defaultBudget = config.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
66
|
+
if (defaultBudget < 0) {
|
|
67
|
+
throw new GrpcStatusError("INTERNAL", "deadline_budget_invalid");
|
|
68
|
+
}
|
|
69
|
+
const perMethod = config.perMethodBudgetMs ?? {};
|
|
70
|
+
return (methodName, handler) => {
|
|
71
|
+
return async (call) => {
|
|
72
|
+
const inboundMs = readDeadlineMs(call);
|
|
73
|
+
const budget = perMethod[methodName] ?? defaultBudget;
|
|
74
|
+
if (inboundMs !== undefined && inboundMs <= 0) {
|
|
75
|
+
throw new GrpcStatusError("DEADLINE_EXCEEDED", "deadline_already_expired");
|
|
76
|
+
}
|
|
77
|
+
if (inboundMs !== undefined) {
|
|
78
|
+
const propagated = Math.max(0, inboundMs - budget);
|
|
79
|
+
const auth = call.auth ?? {};
|
|
80
|
+
auth.requestContext = {
|
|
81
|
+
...auth.requestContext,
|
|
82
|
+
...{ deadlineMs: propagated },
|
|
83
|
+
};
|
|
84
|
+
call.auth = auth;
|
|
85
|
+
}
|
|
86
|
+
return handler(call);
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=deadline-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadline-guard.js","sourceRoot":"","sources":["../../src/interceptors/deadline-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrich-auth.d.ts","sourceRoot":"","sources":["../../src/interceptors/enrich-auth.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,gBAAgB,EAGhB,gBAAgB,EACjB,MAAM,UAAU,CAAC;AA6ClB,wBAAgB,iBAAiB,CAC/B,MAAM,GAAE,gBAAqB,GAC5B,gBAAgB,CA6ClB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// enrichAuthContext — spec § 5.7.
|
|
2
|
+
//
|
|
3
|
+
// Pulls `tenant_id` / `intent_id` / `actor_type` off the inbound proto +
|
|
4
|
+
// metadata and binds them under `call.auth.requestContext`. Mints a
|
|
5
|
+
// per-call `correlationId` used as the join key between the framing row
|
|
6
|
+
// and the handler-emitted state-change row (spec § 5.6).
|
|
7
|
+
import { GrpcStatusError } from "../types";
|
|
8
|
+
const DEFAULT_TENANT_FIELD = "tenant_id";
|
|
9
|
+
function readMetadata(call, key) {
|
|
10
|
+
const values = call.metadata?.get(key);
|
|
11
|
+
if (!values || values.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const first = values[0];
|
|
14
|
+
if (typeof first === "string")
|
|
15
|
+
return first;
|
|
16
|
+
if (first instanceof Uint8Array || Buffer.isBuffer?.(first)) {
|
|
17
|
+
try {
|
|
18
|
+
return Buffer.from(first).toString("utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function readProtoField(req, field) {
|
|
27
|
+
if (!req || typeof req !== "object")
|
|
28
|
+
return undefined;
|
|
29
|
+
const r = req;
|
|
30
|
+
const v = r[field];
|
|
31
|
+
if (typeof v === "string" && v.length > 0)
|
|
32
|
+
return v;
|
|
33
|
+
// proto camelCase fallback
|
|
34
|
+
const camel = field.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
|
35
|
+
if (camel !== field) {
|
|
36
|
+
const v2 = r[camel];
|
|
37
|
+
if (typeof v2 === "string" && v2.length > 0)
|
|
38
|
+
return v2;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function defaultMintCorrelationId() {
|
|
43
|
+
// Deterministic + unique enough for v0.1.0 — production wiring uses
|
|
44
|
+
// `@nodii/telemetry.uuidv7` once telemetry is bootstrapped.
|
|
45
|
+
const bytes = new Uint8Array(16);
|
|
46
|
+
for (let i = 0; i < 16; i++)
|
|
47
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
48
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
49
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
50
|
+
}
|
|
51
|
+
export function enrichAuthContext(config = {}) {
|
|
52
|
+
const tenantField = config.tenantField ?? DEFAULT_TENANT_FIELD;
|
|
53
|
+
const tenantRequiredByMethod = config.tenantRequiredByMethod ?? {};
|
|
54
|
+
const mintCorrelationId = config.mintCorrelationId ?? defaultMintCorrelationId;
|
|
55
|
+
return (methodName, handler) => {
|
|
56
|
+
return async (call) => {
|
|
57
|
+
const tenantFromProto = readProtoField(call.request, tenantField);
|
|
58
|
+
const tenantFromUserActor = call.auth?.userActor?.tenant_id;
|
|
59
|
+
const tenantFromServiceActor = call.auth?.serviceActor?.tenant_id;
|
|
60
|
+
const tenant_id = tenantFromProto ?? tenantFromUserActor ?? tenantFromServiceActor;
|
|
61
|
+
const intent_id = readMetadata(call, "x-nodii-intent-id");
|
|
62
|
+
const actor_type_raw = readMetadata(call, "x-nodii-actor-type");
|
|
63
|
+
const actor_type = actor_type_raw === "user" ||
|
|
64
|
+
actor_type_raw === "agent" ||
|
|
65
|
+
actor_type_raw === "system"
|
|
66
|
+
? actor_type_raw
|
|
67
|
+
: undefined;
|
|
68
|
+
const required = tenantRequiredByMethod[methodName] ?? true;
|
|
69
|
+
if (required && !tenant_id) {
|
|
70
|
+
throw new GrpcStatusError("FAILED_PRECONDITION", "tenant_context_required");
|
|
71
|
+
}
|
|
72
|
+
const auth = call.auth ?? {};
|
|
73
|
+
auth.requestContext = {
|
|
74
|
+
...auth.requestContext,
|
|
75
|
+
tenant_id,
|
|
76
|
+
intent_id,
|
|
77
|
+
actor_type,
|
|
78
|
+
correlationId: auth.requestContext?.correlationId ?? mintCorrelationId(),
|
|
79
|
+
};
|
|
80
|
+
call.auth = auth;
|
|
81
|
+
return handler(call);
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=enrich-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrich-auth.js","sourceRoot":"","sources":["../../src/interceptors/enrich-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ErrorMapEntry, UnaryInterceptor } from "../types";
|
|
2
|
+
export declare const STANDARD_ERROR_CATALOG: Record<string, ErrorMapEntry>;
|
|
3
|
+
export declare function errorMap(catalog: Record<string, ErrorMapEntry>): UnaryInterceptor;
|
|
4
|
+
//# sourceMappingURL=error-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-map.d.ts","sourceRoot":"","sources":["../../src/interceptors/error-map.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAgB,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG9E,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAgDhE,CAAC;AAWF,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GACrC,gBAAgB,CAgBlB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// errorMap — spec § 5.12.
|
|
2
|
+
//
|
|
3
|
+
// Catches every exception thrown by the inner handler/interceptors. Maps
|
|
4
|
+
// via `errorCatalog`:
|
|
5
|
+
// - Catalog hit by class name OR by static `errorName` property →
|
|
6
|
+
// wraps as a typed `GrpcStatusError` with the configured code/detail.
|
|
7
|
+
// - Already a `GrpcStatusError` → pass through.
|
|
8
|
+
// - Anything else → `INTERNAL internal_error` (no stack-trace leak on
|
|
9
|
+
// the wire; the logging interceptor records the full stack).
|
|
10
|
+
//
|
|
11
|
+
// The standard error catalog (`STANDARD_ERROR_CATALOG`) covers the
|
|
12
|
+
// cross-cutting library errors (TenantContextRequiredError,
|
|
13
|
+
// IdempotencyKeyMissingError, ...). Services spread it into their own
|
|
14
|
+
// catalog: `{ ...STANDARD_ERROR_CATALOG, ...myDomainErrors }`.
|
|
15
|
+
import { GrpcStatusError } from "../types";
|
|
16
|
+
export const STANDARD_ERROR_CATALOG = {
|
|
17
|
+
TenantContextRequiredError: {
|
|
18
|
+
code: "FAILED_PRECONDITION",
|
|
19
|
+
detail: "tenant_context_required",
|
|
20
|
+
},
|
|
21
|
+
IdempotencyKeyMissingError: {
|
|
22
|
+
code: "INVALID_ARGUMENT",
|
|
23
|
+
detail: "idempotency_key_missing",
|
|
24
|
+
},
|
|
25
|
+
// @nodii/saga ships `SagaContextRequired` (NOT `SagaContextRequiredError`)
|
|
26
|
+
// — catalog both for back-compat.
|
|
27
|
+
SagaContextRequired: {
|
|
28
|
+
code: "FAILED_PRECONDITION",
|
|
29
|
+
detail: "saga_context_required",
|
|
30
|
+
},
|
|
31
|
+
SagaContextRequiredError: {
|
|
32
|
+
code: "FAILED_PRECONDITION",
|
|
33
|
+
detail: "saga_context_required",
|
|
34
|
+
},
|
|
35
|
+
// @nodii/idempotency ships `IdempotencyServiceError` carrying a numeric
|
|
36
|
+
// code — when it bubbles through errorMap we map by name + the error-map
|
|
37
|
+
// interceptor lets the inner GrpcStatusError-or-IdempotencyServiceError
|
|
38
|
+
// shape pass through. Catalog only fires when the inbound error matches
|
|
39
|
+
// by class name AND is NOT already a GrpcStatusError.
|
|
40
|
+
IdempotencyServiceError: {
|
|
41
|
+
code: "ABORTED",
|
|
42
|
+
detail: "idempotency_in_flight",
|
|
43
|
+
},
|
|
44
|
+
MfaRequiredError: {
|
|
45
|
+
code: "FAILED_PRECONDITION",
|
|
46
|
+
detail: "mfa_required",
|
|
47
|
+
},
|
|
48
|
+
MfaStaleError: {
|
|
49
|
+
code: "FAILED_PRECONDITION",
|
|
50
|
+
detail: "mfa_stale",
|
|
51
|
+
},
|
|
52
|
+
ScopeMissingError: {
|
|
53
|
+
code: "PERMISSION_DENIED",
|
|
54
|
+
detail: "auth_scope_missing",
|
|
55
|
+
},
|
|
56
|
+
TokenInvalidError: {
|
|
57
|
+
code: "UNAUTHENTICATED",
|
|
58
|
+
detail: "auth_token_invalid",
|
|
59
|
+
},
|
|
60
|
+
TokenExpiredError: {
|
|
61
|
+
code: "UNAUTHENTICATED",
|
|
62
|
+
detail: "auth_token_expired",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
function nameOf(err) {
|
|
66
|
+
if (err instanceof Error)
|
|
67
|
+
return err.name;
|
|
68
|
+
if (err && typeof err === "object" && "name" in err) {
|
|
69
|
+
const v = err.name;
|
|
70
|
+
if (typeof v === "string")
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
export function errorMap(catalog) {
|
|
76
|
+
return (_methodName, handler) => {
|
|
77
|
+
return async (call) => {
|
|
78
|
+
try {
|
|
79
|
+
return await handler(call);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof GrpcStatusError)
|
|
83
|
+
throw err;
|
|
84
|
+
const name = nameOf(err);
|
|
85
|
+
if (name && catalog[name]) {
|
|
86
|
+
const entry = catalog[name];
|
|
87
|
+
throw new GrpcStatusError(entry.code, entry.detail, err);
|
|
88
|
+
}
|
|
89
|
+
throw new GrpcStatusError("INTERNAL", "internal_error", err);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=error-map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-map.js","sourceRoot":"","sources":["../../src/interceptors/error-map.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,EAAE;AACF,yEAAyE;AACzE,sBAAsB;AACtB,oEAAoE;AACpE,0EAA0E;AAC1E,kDAAkD;AAClD,wEAAwE;AACxE,iEAAiE;AACjE,EAAE;AACF,mEAAmE;AACnE,4DAA4D;AAC5D,sEAAsE;AACtE,+DAA+D;AAG/D,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,CAAC,MAAM,sBAAsB,GAAkC;IACnE,0BAA0B,EAAE;QAC1B,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,yBAAyB;KAClC;IACD,0BAA0B,EAAE;QAC1B,IAAI,EAAE,kBAAkB;QACxB,MAAM,EAAE,yBAAyB;KAClC;IACD,2EAA2E;IAC3E,kCAAkC;IAClC,mBAAmB,EAAE;QACnB,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,uBAAuB;KAChC;IACD,wBAAwB,EAAE;QACxB,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,uBAAuB;KAChC;IACD,wEAAwE;IACxE,yEAAyE;IACzE,wEAAwE;IACxE,wEAAwE;IACxE,sDAAsD;IACtD,uBAAuB,EAAE;QACvB,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,uBAAuB;KAChC;IACD,gBAAgB,EAAE;QAChB,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,cAAc;KACvB;IACD,aAAa,EAAE;QACb,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,WAAW;KACpB;IACD,iBAAiB,EAAE;QACjB,IAAI,EAAE,mBAAmB;QACzB,MAAM,EAAE,oBAAoB;KAC7B;IACD,iBAAiB,EAAE;QACjB,IAAI,EAAE,iBAAiB;QACvB,MAAM,EAAE,oBAAoB;KAC7B;IACD,iBAAiB,EAAE;QACjB,IAAI,EAAE,iBAAiB;QACvB,MAAM,EAAE,oBAAoB;KAC7B;CACF,CAAC;AAEF,SAAS,MAAM,CAAC,GAAY;IAC1B,IAAI,GAAG,YAAY,KAAK;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IAC1C,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QACpD,MAAM,CAAC,GAAI,GAA0B,CAAC,IAAI,CAAC;QAC3C,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,OAAsC;IAEtC,OAAO,CAAC,WAAmB,EAAE,OAAqB,EAAgB,EAAE;QAClE,OAAO,KAAK,EAAE,IAAI,EAAE,EAAE;YACpB,IAAI,CAAC;gBACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,eAAe;oBAAE,MAAM,GAAG,CAAC;gBAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;gBACzB,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;oBAC5B,MAAM,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBAC3D,CAAC;gBACD,MAAM,IAAI,eAAe,CAAC,UAAU,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAC;YAC/D,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LoggingConfig, UnaryInterceptor } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Build the logging interceptor factory. Outermost in the locked stack
|
|
4
|
+
* so every call — including auth failures — is logged.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withLoggingUnary(config?: LoggingConfig): UnaryInterceptor;
|
|
7
|
+
//# sourceMappingURL=logging.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["../../src/interceptors/logging.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,aAAa,EAGb,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAkBlB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,aAAkB,GAAG,gBAAgB,CAyC7E"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// withLoggingUnary — spec § 5.5.
|
|
2
|
+
//
|
|
3
|
+
// Always logs failures (codes in `alwaysEmit`); samples successes via
|
|
4
|
+
// `successSample`. Wire never carries auth headers / request body /
|
|
5
|
+
// response body — pino-style redaction baked in.
|
|
6
|
+
import { resolveTelemetry } from "../telemetry-shim";
|
|
7
|
+
import { GrpcStatusError } from "../types";
|
|
8
|
+
const DEFAULT_ALWAYS_EMIT = [
|
|
9
|
+
"UNAUTHENTICATED",
|
|
10
|
+
"PERMISSION_DENIED",
|
|
11
|
+
"FAILED_PRECONDITION",
|
|
12
|
+
"INVALID_ARGUMENT",
|
|
13
|
+
"INTERNAL",
|
|
14
|
+
"UNAVAILABLE",
|
|
15
|
+
"UNKNOWN",
|
|
16
|
+
];
|
|
17
|
+
function classify(err) {
|
|
18
|
+
if (err instanceof GrpcStatusError)
|
|
19
|
+
return err.code;
|
|
20
|
+
return "INTERNAL";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the logging interceptor factory. Outermost in the locked stack
|
|
24
|
+
* so every call — including auth failures — is logged.
|
|
25
|
+
*/
|
|
26
|
+
export function withLoggingUnary(config = {}) {
|
|
27
|
+
const successSample = config.successSample ?? 0.01;
|
|
28
|
+
const alwaysEmit = new Set(config.alwaysEmit ?? DEFAULT_ALWAYS_EMIT);
|
|
29
|
+
const random = config.random ?? Math.random;
|
|
30
|
+
return (methodName, handler) => {
|
|
31
|
+
return async (call) => {
|
|
32
|
+
const log = config.logger ?? resolveTelemetry().logger;
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
try {
|
|
35
|
+
const result = await handler(call);
|
|
36
|
+
const latency = Date.now() - start;
|
|
37
|
+
if (alwaysEmit.has("OK") || random() < successSample) {
|
|
38
|
+
log.info("rpc.ok", {
|
|
39
|
+
rpc: {
|
|
40
|
+
method: methodName,
|
|
41
|
+
code: "OK",
|
|
42
|
+
latency_ms: latency,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const code = classify(err);
|
|
50
|
+
const latency = Date.now() - start;
|
|
51
|
+
if (alwaysEmit.has(code)) {
|
|
52
|
+
log.warn("rpc.fail", {
|
|
53
|
+
rpc: {
|
|
54
|
+
method: methodName,
|
|
55
|
+
code,
|
|
56
|
+
latency_ms: latency,
|
|
57
|
+
detail: err instanceof GrpcStatusError ? err.detail : undefined,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=logging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging.js","sourceRoot":"","sources":["../../src/interceptors/logging.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,iDAAiD;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQrD,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,mBAAmB,GAA8B;IACrD,iBAAiB;IACjB,mBAAmB;IACnB,qBAAqB;IACrB,kBAAkB;IAClB,UAAU;IACV,aAAa;IACb,SAAS;CACV,CAAC;AAEF,SAAS,QAAQ,CAAC,GAAY;IAC5B,IAAI,GAAG,YAAY,eAAe;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IACpD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE;IACzD,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,IAAI,CAAC;IACnD,MAAM,UAAU,GAAG,IAAI,GAAG,CACxB,MAAM,CAAC,UAAU,IAAI,mBAAmB,CACzC,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;IAE5C,OAAO,CAAC,UAAkB,EAAE,OAAqB,EAAgB,EAAE;QACjE,OAAO,KAAK,EAAE,IAAe,EAAoB,EAAE;YACjD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC,MAAM,CAAC;YACvD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;gBACnC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;gBACnC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;oBACrD,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE;wBACjB,GAAG,EAAE;4BACH,MAAM,EAAE,UAAU;4BAClB,IAAI,EAAE,IAAI;4BACV,UAAU,EAAE,OAAO;yBACpB;qBACF,CAAC,CAAC;gBACL,CAAC;gBACD,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;gBACnC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBACzB,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE;wBACnB,GAAG,EAAE;4BACH,MAAM,EAAE,UAAU;4BAClB,IAAI;4BACJ,UAAU,EAAE,OAAO;4BACnB,MAAM,EAAE,GAAG,YAAY,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;yBAChE;qBACF,CAAC,CAAC;gBACL,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal client interceptor surface — matches `@grpc/grpc-js`'s
|
|
3
|
+
* `Interceptor` signature without taking a hard dep on @grpc/grpc-js
|
|
4
|
+
* types at the type level. Real consumers pass the returned factory
|
|
5
|
+
* directly to `client.makeUnaryRequest({interceptors: [withSagaHeaders()]})`.
|
|
6
|
+
*/
|
|
7
|
+
type GrpcMetadata = {
|
|
8
|
+
set(key: string, value: string): void;
|
|
9
|
+
add?(key: string, value: string): void;
|
|
10
|
+
};
|
|
11
|
+
interface ClientCallOptions {
|
|
12
|
+
metadata?: GrpcMetadata;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Lightweight client interceptor: a function that mutates the outbound
|
|
16
|
+
* metadata in place. Consumers wire this into their @grpc/grpc-js client
|
|
17
|
+
* config via `grpc.credentials.createFromMetadataGenerator`, or via the
|
|
18
|
+
* built-in `interceptors: [...]` array on the client constructor.
|
|
19
|
+
*/
|
|
20
|
+
export type SagaHeaderInjector = (options: ClientCallOptions) => void;
|
|
21
|
+
export declare function withSagaHeaders(): SagaHeaderInjector;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=saga-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saga-headers.d.ts","sourceRoot":"","sources":["../../src/interceptors/saga-headers.ts"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,KAAK,YAAY,GAAG;IAClB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC,CAAC;AAEF,UAAU,iBAAiB;IACzB,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAEtE,wBAAgB,eAAe,IAAI,kBAAkB,CAkBpD"}
|