@composed-di/core 0.5.2-alpha → 0.5.3-alpha

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.
@@ -1,35 +1,97 @@
1
1
  import type { DisposeContext, EventSpan, InitializeContext, MethodCallContext, ServiceModuleListener } from './serviceModuleListener';
2
2
  import type { ServiceKey } from './serviceKey';
3
3
  /**
4
- * The placeholder that replaces redacted values in event contexts and
5
- * outcomes.
4
+ * The placeholder that replaces a redacted value when no custom
5
+ * transform is given for it.
6
6
  */
7
- export declare const REDACTED_VALUE = "[redacted]";
7
+ export declare const REDACTED_VALUE = "[REDACTED]";
8
+ /**
9
+ * Custom masking for one included property, narrowed to the exact
10
+ * method named when {@link RedactionRuleBuilder.redact} is called.
11
+ * Omitting a side fully blanks it with {@link REDACTED_VALUE}; providing
12
+ * one lets you report a partial mask instead (e.g. the last 4 digits of
13
+ * a card number) — both always return a `string`, the masked
14
+ * representation to report in place of the real value.
15
+ */
16
+ export type Mask<T, K extends Extract<keyof T, string>> = T[K] extends (...args: infer A) => infer R ? {
17
+ maskArgs?: (...args: A) => string;
18
+ maskResult?: (result: R) => string;
19
+ } : never;
8
20
  /**
9
21
  * Marks a service — or specific properties of it — as sensitive, so the
10
- * values flowing through its events are replaced with {@link REDACTED_VALUE}.
22
+ * values flowing through its events are redacted. Returned by
23
+ * {@link RedactionRuleBuilder.build}, which is the only way to construct
24
+ * one — the `redactAll`/per-property merge logic lives entirely inside
25
+ * `maskArgs`/`maskResult`, so callers never need to know how a rule
26
+ * reached its decision, only what to do with it.
11
27
  */
12
28
  export interface RedactionRule<T> {
29
+ readonly key: ServiceKey<T>;
30
+ /**
31
+ * The args to report for a call to `functionName`: unchanged if not
32
+ * redacted, otherwise blanked or run through a custom `maskArgs`.
33
+ */
34
+ maskArgs(functionName: string, args: readonly unknown[]): readonly unknown[];
35
+ /**
36
+ * The value to report for a success outcome: unchanged if not
37
+ * redacted, otherwise blanked or run through a custom `maskResult`.
38
+ * Omit `functionName` to ask about the initialize result (the service
39
+ * instance itself), which only the rule's `redactAll` default touches.
40
+ */
41
+ maskResult(functionName: string | undefined, result: unknown): unknown;
42
+ }
43
+ /**
44
+ * Fluent, single-key rule builder returned by {@link redactionRule}.
45
+ * `redactAll`, `redact`, and `exclude` all merge into the same rule —
46
+ * call them in any combination, in any order; the more specific
47
+ * per-property calls (`redact`/`exclude`) always win over the blanket
48
+ * `redactAll` default for the properties they name.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const rules = [
53
+ * redactionRule(SecretClientKey)
54
+ * .redactAll()
55
+ * .build(), // whole service is sensitive
56
+ * redactionRule(BillingKey)
57
+ * .redactAll()
58
+ * .redact('chargeCard', { maskResult: (card) => `card ending in ${card.number.slice(-4)}` })
59
+ * .exclude('ping') // redact everything except this, with one custom mask
60
+ * .build(),
61
+ * redactionRule(VaultKey)
62
+ * .redact('getSecret')
63
+ * .build(), // only this call, nothing else
64
+ * ];
65
+ * ```
66
+ */
67
+ export declare class RedactionRuleBuilder<T> {
68
+ private readonly key;
69
+ private redactAllFlag;
70
+ private readonly overrides;
71
+ constructor(key: ServiceKey<T>);
72
+ /** Redacts every property, plus the initialize result, by default. */
73
+ redactAll(): this;
13
74
  /**
14
- * The service key whose values must be redacted. Matched by key
15
- * identity, like everywhere else in the container.
75
+ * Marks one property (method) as redacted, with optional custom
76
+ * masking. Call repeatedly for several properties. Overrides
77
+ * `redactAll`/`exclude` for this specific property.
16
78
  */
17
- key: ServiceKey<T>;
79
+ redact<K extends Extract<keyof T, string>>(name: K, mask?: Mask<T, K>): this;
18
80
  /**
19
- * Property names to redact. Optional: when omitted, ALL properties of
20
- * the service are redacted, along with its initialize result (the
21
- * service instance itself may carry credentials or config).
81
+ * Marks one or more properties as explicitly NOT redacted, overriding
82
+ * `redactAll` for just these.
22
83
  */
23
- properties?: Extract<keyof T, string>[];
84
+ exclude(...names: Extract<keyof T, string>[]): this;
85
+ build(): RedactionRule<any>;
24
86
  }
25
- export declare function redactionRule<T>(key: ServiceKey<T>, properties?: Extract<keyof T, string>[]): RedactionRule<T>;
87
+ export declare function redactionRule<T>(key: ServiceKey<T>): RedactionRuleBuilder<T>;
26
88
  /**
27
89
  * A ServiceEventListener decorator that redacts sensitive values before
28
90
  * they reach the wrapped listener. Works with any implementation via
29
91
  * delegation: arguments in MethodCallContext and success values in
30
- * EventOutcome are replaced with {@link REDACTED_VALUE}, so the delegate
31
- * never sees the sensitive data — whatever it captures or exports is
32
- * already scrubbed.
92
+ * EventOutcome are replaced (wholesale, or via a custom transform)
93
+ * before the delegate ever sees them — whatever it captures or exports
94
+ * is already scrubbed.
33
95
  *
34
96
  * Failure outcomes are passed through unchanged so error reporting keeps
35
97
  * working; keep secrets out of error messages at the throwing site.
@@ -39,8 +101,9 @@ export declare function redactionRule<T>(key: ServiceKey<T>, properties?: Extrac
39
101
  * const listener = new RedactingEventListener(
40
102
  * new OTELEventListener({ captureArguments: true, captureResults: true }),
41
103
  * [
42
- * { key: SecretClientKey }, // whole service is sensitive
43
- * { key: VaultKey, properties: ['getSecret'] }, // only these calls
104
+ * redactionRule(SecretClientKey).redactAll().build(), // whole service is sensitive
105
+ * redactionRule(VaultKey).redact('getSecret').build(), // only this call
106
+ * redactionRule(HealthKey).redactAll().exclude('ping').build(), // everything but this call
44
107
  * ],
45
108
  * );
46
109
  * ```
@@ -52,6 +115,5 @@ export declare class RedactingEventListener implements ServiceModuleListener {
52
115
  onInitialize(context: InitializeContext): EventSpan | void;
53
116
  onDispose(context: DisposeContext): EventSpan | void;
54
117
  onMethodCall(context: MethodCallContext): EventSpan | void;
55
- private isRedacted;
56
118
  }
57
119
  //# sourceMappingURL=redactingEventListener.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redactingEventListener.d.ts","sourceRoot":"","sources":["../src/redactingEventListener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,cAAc,eAAe,CAAC;AAE3C;;;GAGG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B;;;OAGG;IACH,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAEnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;CACzC;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAE9G;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,sBAAuB,YAAW,qBAAqB;IAEhE,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,QAAQ,EAAE,qBAAqB,EAC/B,KAAK,EAAE,SAAS,aAAa,CAAC,GAAG,CAAC,EAAE;IAGvD,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI;IAU1D,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI;IAKpD,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI;IAc1D,OAAO,CAAC,UAAU;CAYnB"}
1
+ {"version":3,"file":"redactingEventListener.d.ts","sourceRoot":"","sources":["../src/redactingEventListener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,cAAc,eAAe,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CACrE,GAAG,IAAI,EAAE,MAAM,CAAC,KACb,MAAM,CAAC,GACR;IACE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;CACpC,GACD,KAAK,CAAC;AAeV;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAE5B;;;OAGG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,OAAO,EAAE,CAAC;IAE7E;;;;;OAKG;IACH,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;CACxE;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,oBAAoB,CAAC,CAAC;IAIrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAHhC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;gBAErC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC;IAE/C,sEAAsE;IACtE,SAAS,IAAI,IAAI;IAKjB;;;;OAIG;IACH,MAAM,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI;IAK5E;;;OAGG;IACH,OAAO,CAAC,GAAG,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,GAAG,IAAI;IAOnD,KAAK,IAAI,aAAa,CAAC,GAAG,CAAC;CAwC5B;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAE5E;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,YAAW,qBAAqB;IAEhE,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,QAAQ,EAAE,qBAAqB,EAC/B,KAAK,EAAE,SAAS,aAAa,CAAC,GAAG,CAAC,EAAE;IAGvD,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI;IAK1D,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI;IAKpD,YAAY,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI;CAY3D"}
@@ -1,22 +1,113 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RedactingEventListener = exports.REDACTED_VALUE = void 0;
3
+ exports.RedactingEventListener = exports.RedactionRuleBuilder = exports.REDACTED_VALUE = void 0;
4
4
  exports.redactionRule = redactionRule;
5
5
  /**
6
- * The placeholder that replaces redacted values in event contexts and
7
- * outcomes.
6
+ * The placeholder that replaces a redacted value when no custom
7
+ * transform is given for it.
8
8
  */
9
- exports.REDACTED_VALUE = '[redacted]';
10
- function redactionRule(key, properties) {
11
- return { key, properties };
9
+ exports.REDACTED_VALUE = '[REDACTED]';
10
+ /**
11
+ * Fluent, single-key rule builder returned by {@link redactionRule}.
12
+ * `redactAll`, `redact`, and `exclude` all merge into the same rule —
13
+ * call them in any combination, in any order; the more specific
14
+ * per-property calls (`redact`/`exclude`) always win over the blanket
15
+ * `redactAll` default for the properties they name.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const rules = [
20
+ * redactionRule(SecretClientKey)
21
+ * .redactAll()
22
+ * .build(), // whole service is sensitive
23
+ * redactionRule(BillingKey)
24
+ * .redactAll()
25
+ * .redact('chargeCard', { maskResult: (card) => `card ending in ${card.number.slice(-4)}` })
26
+ * .exclude('ping') // redact everything except this, with one custom mask
27
+ * .build(),
28
+ * redactionRule(VaultKey)
29
+ * .redact('getSecret')
30
+ * .build(), // only this call, nothing else
31
+ * ];
32
+ * ```
33
+ */
34
+ class RedactionRuleBuilder {
35
+ constructor(key) {
36
+ this.key = key;
37
+ this.redactAllFlag = false;
38
+ this.overrides = {};
39
+ }
40
+ /** Redacts every property, plus the initialize result, by default. */
41
+ redactAll() {
42
+ this.redactAllFlag = true;
43
+ return this;
44
+ }
45
+ /**
46
+ * Marks one property (method) as redacted, with optional custom
47
+ * masking. Call repeatedly for several properties. Overrides
48
+ * `redactAll`/`exclude` for this specific property.
49
+ */
50
+ redact(name, mask) {
51
+ this.overrides[name] = Object.assign({ redacted: true }, mask);
52
+ return this;
53
+ }
54
+ /**
55
+ * Marks one or more properties as explicitly NOT redacted, overriding
56
+ * `redactAll` for just these.
57
+ */
58
+ exclude(...names) {
59
+ for (const name of names) {
60
+ this.overrides[name] = { redacted: false };
61
+ }
62
+ return this;
63
+ }
64
+ build() {
65
+ if (!this.redactAllFlag && Object.keys(this.overrides).length === 0) {
66
+ throw new Error(`redactionRule(${this.key.name}) has no effect: call .redactAll() and/or .redact(...) ` +
67
+ 'before .build(), otherwise this rule never redacts anything.');
68
+ }
69
+ const redactAllFlag = this.redactAllFlag;
70
+ const overrides = this.overrides;
71
+ return {
72
+ key: this.key,
73
+ maskArgs(functionName, args) {
74
+ const override = overrides[functionName];
75
+ const redacted = override ? override.redacted : redactAllFlag;
76
+ if (!redacted) {
77
+ return args;
78
+ }
79
+ return (override === null || override === void 0 ? void 0 : override.maskArgs)
80
+ ? // A custom mask reports one string for the whole call,
81
+ // rather than a value per argument.
82
+ [override.maskArgs(...args)]
83
+ : // Replace each argument rather than the whole array, so the
84
+ // delegate still sees the call's arity.
85
+ args.map(() => exports.REDACTED_VALUE);
86
+ },
87
+ maskResult(functionName, result) {
88
+ const override = functionName === undefined ? undefined : overrides[functionName];
89
+ const redacted = override ? override.redacted : redactAllFlag;
90
+ if (!redacted) {
91
+ return result;
92
+ }
93
+ return (override === null || override === void 0 ? void 0 : override.maskResult)
94
+ ? override.maskResult(result)
95
+ : exports.REDACTED_VALUE;
96
+ },
97
+ };
98
+ }
99
+ }
100
+ exports.RedactionRuleBuilder = RedactionRuleBuilder;
101
+ function redactionRule(key) {
102
+ return new RedactionRuleBuilder(key);
12
103
  }
13
104
  /**
14
105
  * A ServiceEventListener decorator that redacts sensitive values before
15
106
  * they reach the wrapped listener. Works with any implementation via
16
107
  * delegation: arguments in MethodCallContext and success values in
17
- * EventOutcome are replaced with {@link REDACTED_VALUE}, so the delegate
18
- * never sees the sensitive data — whatever it captures or exports is
19
- * already scrubbed.
108
+ * EventOutcome are replaced (wholesale, or via a custom transform)
109
+ * before the delegate ever sees them — whatever it captures or exports
110
+ * is already scrubbed.
20
111
  *
21
112
  * Failure outcomes are passed through unchanged so error reporting keeps
22
113
  * working; keep secrets out of error messages at the throwing site.
@@ -26,8 +117,9 @@ function redactionRule(key, properties) {
26
117
  * const listener = new RedactingEventListener(
27
118
  * new OTELEventListener({ captureArguments: true, captureResults: true }),
28
119
  * [
29
- * { key: SecretClientKey }, // whole service is sensitive
30
- * { key: VaultKey, properties: ['getSecret'] }, // only these calls
120
+ * redactionRule(SecretClientKey).redactAll().build(), // whole service is sensitive
121
+ * redactionRule(VaultKey).redact('getSecret').build(), // only this call
122
+ * redactionRule(HealthKey).redactAll().exclude('ping').build(), // everything but this call
31
123
  * ],
32
124
  * );
33
125
  * ```
@@ -39,10 +131,8 @@ class RedactingEventListener {
39
131
  }
40
132
  onInitialize(context) {
41
133
  var _a, _b;
42
- // The initialize result is the service instance itself, so it is
43
- // redacted only when the whole service is sensitive a rule without
44
- // `properties`.
45
- return redactSpan((_b = (_a = this.delegate).onInitialize) === null || _b === void 0 ? void 0 : _b.call(_a, context), this.isRedacted(context.key));
134
+ const rule = this.rules.find((r) => r.key === context.key);
135
+ return redactSpan((_b = (_a = this.delegate).onInitialize) === null || _b === void 0 ? void 0 : _b.call(_a, context), rule);
46
136
  }
47
137
  onDispose(context) {
48
138
  var _a, _b;
@@ -51,39 +141,29 @@ class RedactingEventListener {
51
141
  }
52
142
  onMethodCall(context) {
53
143
  var _a, _b;
54
- const redacted = this.isRedacted(context.key, context.functionName);
55
- const span = (_b = (_a = this.delegate).onMethodCall) === null || _b === void 0 ? void 0 : _b.call(_a, redacted
56
- ? // Replace each argument rather than the whole array, so the
57
- Object.assign(Object.assign({}, context), { args: context.args.map(() => exports.REDACTED_VALUE) }) : context);
58
- return redactSpan(span, redacted);
59
- }
60
- // 1. Si expiro el dbr `EXPIRED` el cron debe mandar a `FACE_MISMATCH`?
61
- // 2. Si hubo un matchLevel intermediario el cron debe mandar a `FACE_MISMATCH`? (Probablemente no)
62
- isRedacted(key, propertyName) {
63
- return this.rules.some((rule) => {
64
- var _a;
65
- return rule.key === key &&
66
- // A rule without `properties` redacts everything on the key,
67
- // including initialize — where no property name is passed —
68
- // while a rule with `properties` matches only those calls.
69
- (rule.properties === undefined ||
70
- (propertyName !== undefined &&
71
- ((_a = rule.properties) === null || _a === void 0 ? void 0 : _a.includes(propertyName))));
72
- });
144
+ const rule = this.rules.find((r) => r.key === context.key);
145
+ const span = (_b = (_a = this.delegate).onMethodCall) === null || _b === void 0 ? void 0 : _b.call(_a, rule
146
+ ? Object.assign(Object.assign({}, context), { args: rule.maskArgs(context.functionName, context.args) }) : context);
147
+ return redactSpan(span, rule, context.functionName);
73
148
  }
74
149
  }
75
150
  exports.RedactingEventListener = RedactingEventListener;
76
151
  /**
77
152
  * Wraps the delegate's EventSpan so success values are redacted before
78
- * `end` sees them. `run` (and any future fields) pass through untouched.
153
+ * `end` sees them, by delegating to the rule's `maskResult`. `run` (and
154
+ * any future fields) pass through untouched. `functionName` is omitted
155
+ * for the initialize result (the instance itself).
79
156
  */
80
- function redactSpan(span, redacted) {
81
- if (!redacted || !(span === null || span === void 0 ? void 0 : span.end)) {
157
+ function redactSpan(span, rule, functionName) {
158
+ if (!rule || !(span === null || span === void 0 ? void 0 : span.end)) {
82
159
  return span;
83
160
  }
84
161
  const end = span.end.bind(span);
85
162
  return Object.assign(Object.assign({}, span), { end: (outcome) => end(outcome.type === 'success'
86
- ? { type: 'success', value: exports.REDACTED_VALUE }
163
+ ? {
164
+ type: 'success',
165
+ value: rule.maskResult(functionName, outcome.value),
166
+ }
87
167
  : outcome) });
88
168
  }
89
169
  //# sourceMappingURL=redactingEventListener.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redactingEventListener.js","sourceRoot":"","sources":["../src/redactingEventListener.ts"],"names":[],"mappings":";;;AAmCA,sCAEC;AA3BD;;;GAGG;AACU,QAAA,cAAc,GAAG,YAAY,CAAC;AAqB3C,SAAgB,aAAa,CAAI,GAAkB,EAAE,UAAuC;IAC1F,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,sBAAsB;IACjC,YACmB,QAA+B,EAC/B,KAAoC;QADpC,aAAQ,GAAR,QAAQ,CAAuB;QAC/B,UAAK,GAAL,KAAK,CAA+B;IACpD,CAAC;IAEJ,YAAY,CAAC,OAA0B;;QACrC,iEAAiE;QACjE,qEAAqE;QACrE,gBAAgB;QAChB,OAAO,UAAU,CACf,MAAA,MAAA,IAAI,CAAC,QAAQ,EAAC,YAAY,mDAAG,OAAO,CAAC,EACrC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAC7B,CAAC;IACJ,CAAC;IAED,SAAS,CAAC,OAAuB;;QAC/B,uEAAuE;QACvE,OAAO,MAAA,MAAA,IAAI,CAAC,QAAQ,EAAC,SAAS,mDAAG,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED,YAAY,CAAC,OAA0B;;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,MAAA,MAAA,IAAI,CAAC,QAAQ,EAAC,YAAY,mDACrC,QAAQ;YACN,CAAC,CAAC,4DAA4D;6CAEvD,OAAO,KAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,sBAAc,CAAC,IAC5D,CAAC,CAAC,OAAO,CACZ,CAAC;QACF,OAAO,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACpC,CAAC;IAED,uEAAuE;IACvE,mGAAmG;IAC3F,UAAU,CAAC,GAAoB,EAAE,YAAqB;QAC5D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CACpB,CAAC,IAAI,EAAE,EAAE;;YACP,OAAA,IAAI,CAAC,GAAG,KAAK,GAAG;gBAChB,6DAA6D;gBAC7D,4DAA4D;gBAC5D,2DAA2D;gBAC3D,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS;oBAC5B,CAAC,YAAY,KAAK,SAAS;yBACzB,MAAA,IAAI,CAAC,UAAU,0CAAE,QAAQ,CAAC,YAAY,CAAC,CAAA,CAAC,CAAC,CAAA;SAAA,CAChD,CAAC;IACJ,CAAC;CACF;AA/CD,wDA+CC;AAED;;;GAGG;AACH,SAAS,UAAU,CACjB,IAAsB,EACtB,QAAiB;IAEjB,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,GAAG,CAAA,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,uCACK,IAAI,KACP,GAAG,EAAE,CAAC,OAAqB,EAAE,EAAE,CAC7B,GAAG,CACD,OAAO,CAAC,IAAI,KAAK,SAAS;YACxB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,sBAAc,EAAE;YAC5C,CAAC,CAAC,OAAO,CACZ,IACH;AACJ,CAAC"}
1
+ {"version":3,"file":"redactingEventListener.js","sourceRoot":"","sources":["../src/redactingEventListener.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@composed-di/core",
3
3
  "private": false,
4
- "version": "0.5.2-alpha",
4
+ "version": "0.5.3-alpha",
5
5
  "author": "Juan Herrera juanhr454@gmail.com",
6
6
  "type": "commonjs",
7
7
  "main": "./dist/index.js",
@@ -9,41 +9,177 @@ import type {
9
9
  import type { ServiceKey } from './serviceKey';
10
10
 
11
11
  /**
12
- * The placeholder that replaces redacted values in event contexts and
13
- * outcomes.
12
+ * The placeholder that replaces a redacted value when no custom
13
+ * transform is given for it.
14
14
  */
15
- export const REDACTED_VALUE = '[redacted]';
15
+ export const REDACTED_VALUE = '[REDACTED]';
16
+
17
+ /**
18
+ * Custom masking for one included property, narrowed to the exact
19
+ * method named when {@link RedactionRuleBuilder.redact} is called.
20
+ * Omitting a side fully blanks it with {@link REDACTED_VALUE}; providing
21
+ * one lets you report a partial mask instead (e.g. the last 4 digits of
22
+ * a card number) — both always return a `string`, the masked
23
+ * representation to report in place of the real value.
24
+ */
25
+ export type Mask<T, K extends Extract<keyof T, string>> = T[K] extends (
26
+ ...args: infer A
27
+ ) => infer R
28
+ ? {
29
+ maskArgs?: (...args: A) => string;
30
+ maskResult?: (result: R) => string;
31
+ }
32
+ : never;
33
+
34
+ /**
35
+ * Per-property override, layered on top of a rule's `redactAll` default.
36
+ * `redacted: true` (from {@link RedactionRuleBuilder.redact}) redacts
37
+ * that property, optionally with a custom mask; `redacted: false` (from
38
+ * {@link RedactionRuleBuilder.exclude}) forces it to be left untouched
39
+ * regardless of `redactAll`.
40
+ */
41
+ interface PropertyOverride {
42
+ redacted: boolean;
43
+ maskArgs?: (...args: any[]) => string;
44
+ maskResult?: (result: any) => string;
45
+ }
16
46
 
17
47
  /**
18
48
  * Marks a service — or specific properties of it — as sensitive, so the
19
- * values flowing through its events are replaced with {@link REDACTED_VALUE}.
49
+ * values flowing through its events are redacted. Returned by
50
+ * {@link RedactionRuleBuilder.build}, which is the only way to construct
51
+ * one — the `redactAll`/per-property merge logic lives entirely inside
52
+ * `maskArgs`/`maskResult`, so callers never need to know how a rule
53
+ * reached its decision, only what to do with it.
20
54
  */
21
55
  export interface RedactionRule<T> {
56
+ readonly key: ServiceKey<T>;
57
+
22
58
  /**
23
- * The service key whose values must be redacted. Matched by key
24
- * identity, like everywhere else in the container.
59
+ * The args to report for a call to `functionName`: unchanged if not
60
+ * redacted, otherwise blanked or run through a custom `maskArgs`.
25
61
  */
26
- key: ServiceKey<T>;
62
+ maskArgs(functionName: string, args: readonly unknown[]): readonly unknown[];
27
63
 
28
64
  /**
29
- * Property names to redact. Optional: when omitted, ALL properties of
30
- * the service are redacted, along with its initialize result (the
31
- * service instance itself may carry credentials or config).
65
+ * The value to report for a success outcome: unchanged if not
66
+ * redacted, otherwise blanked or run through a custom `maskResult`.
67
+ * Omit `functionName` to ask about the initialize result (the service
68
+ * instance itself), which only the rule's `redactAll` default touches.
32
69
  */
33
- properties?: Extract<keyof T, string>[];
70
+ maskResult(functionName: string | undefined, result: unknown): unknown;
34
71
  }
35
72
 
36
- export function redactionRule<T>(key: ServiceKey<T>, properties?: Extract<keyof T, string>[]): RedactionRule<T> {
37
- return { key, properties };
73
+ /**
74
+ * Fluent, single-key rule builder returned by {@link redactionRule}.
75
+ * `redactAll`, `redact`, and `exclude` all merge into the same rule —
76
+ * call them in any combination, in any order; the more specific
77
+ * per-property calls (`redact`/`exclude`) always win over the blanket
78
+ * `redactAll` default for the properties they name.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const rules = [
83
+ * redactionRule(SecretClientKey)
84
+ * .redactAll()
85
+ * .build(), // whole service is sensitive
86
+ * redactionRule(BillingKey)
87
+ * .redactAll()
88
+ * .redact('chargeCard', { maskResult: (card) => `card ending in ${card.number.slice(-4)}` })
89
+ * .exclude('ping') // redact everything except this, with one custom mask
90
+ * .build(),
91
+ * redactionRule(VaultKey)
92
+ * .redact('getSecret')
93
+ * .build(), // only this call, nothing else
94
+ * ];
95
+ * ```
96
+ */
97
+ export class RedactionRuleBuilder<T> {
98
+ private redactAllFlag = false;
99
+ private readonly overrides: Record<string, PropertyOverride> = {};
100
+
101
+ constructor(private readonly key: ServiceKey<T>) {}
102
+
103
+ /** Redacts every property, plus the initialize result, by default. */
104
+ redactAll(): this {
105
+ this.redactAllFlag = true;
106
+ return this;
107
+ }
108
+
109
+ /**
110
+ * Marks one property (method) as redacted, with optional custom
111
+ * masking. Call repeatedly for several properties. Overrides
112
+ * `redactAll`/`exclude` for this specific property.
113
+ */
114
+ redact<K extends Extract<keyof T, string>>(name: K, mask?: Mask<T, K>): this {
115
+ this.overrides[name] = { redacted: true, ...mask };
116
+ return this;
117
+ }
118
+
119
+ /**
120
+ * Marks one or more properties as explicitly NOT redacted, overriding
121
+ * `redactAll` for just these.
122
+ */
123
+ exclude(...names: Extract<keyof T, string>[]): this {
124
+ for (const name of names) {
125
+ this.overrides[name] = { redacted: false };
126
+ }
127
+ return this;
128
+ }
129
+
130
+ build(): RedactionRule<any> {
131
+ if (!this.redactAllFlag && Object.keys(this.overrides).length === 0) {
132
+ throw new Error(
133
+ `redactionRule(${this.key.name}) has no effect: call .redactAll() and/or .redact(...) ` +
134
+ 'before .build(), otherwise this rule never redacts anything.',
135
+ );
136
+ }
137
+
138
+ const redactAllFlag = this.redactAllFlag;
139
+ const overrides = this.overrides;
140
+
141
+ return {
142
+ key: this.key,
143
+ maskArgs(functionName, args) {
144
+ const override = overrides[functionName];
145
+ const redacted = override ? override.redacted : redactAllFlag;
146
+ if (!redacted) {
147
+ return args;
148
+ }
149
+ return override?.maskArgs
150
+ ? // A custom mask reports one string for the whole call,
151
+ // rather than a value per argument.
152
+ [override.maskArgs(...args)]
153
+ : // Replace each argument rather than the whole array, so the
154
+ // delegate still sees the call's arity.
155
+ args.map(() => REDACTED_VALUE);
156
+ },
157
+ maskResult(functionName, result) {
158
+ const override =
159
+ functionName === undefined ? undefined : overrides[functionName];
160
+ const redacted = override ? override.redacted : redactAllFlag;
161
+ if (!redacted) {
162
+ return result;
163
+ }
164
+ return override?.maskResult
165
+ ? override.maskResult(result)
166
+ : REDACTED_VALUE;
167
+ },
168
+ } as RedactionRule<any>;
169
+ }
170
+ }
171
+
172
+ export function redactionRule<T>(key: ServiceKey<T>): RedactionRuleBuilder<T> {
173
+ return new RedactionRuleBuilder(key);
38
174
  }
39
175
 
40
176
  /**
41
177
  * A ServiceEventListener decorator that redacts sensitive values before
42
178
  * they reach the wrapped listener. Works with any implementation via
43
179
  * delegation: arguments in MethodCallContext and success values in
44
- * EventOutcome are replaced with {@link REDACTED_VALUE}, so the delegate
45
- * never sees the sensitive data — whatever it captures or exports is
46
- * already scrubbed.
180
+ * EventOutcome are replaced (wholesale, or via a custom transform)
181
+ * before the delegate ever sees them — whatever it captures or exports
182
+ * is already scrubbed.
47
183
  *
48
184
  * Failure outcomes are passed through unchanged so error reporting keeps
49
185
  * working; keep secrets out of error messages at the throwing site.
@@ -53,8 +189,9 @@ export function redactionRule<T>(key: ServiceKey<T>, properties?: Extract<keyof
53
189
  * const listener = new RedactingEventListener(
54
190
  * new OTELEventListener({ captureArguments: true, captureResults: true }),
55
191
  * [
56
- * { key: SecretClientKey }, // whole service is sensitive
57
- * { key: VaultKey, properties: ['getSecret'] }, // only these calls
192
+ * redactionRule(SecretClientKey).redactAll().build(), // whole service is sensitive
193
+ * redactionRule(VaultKey).redact('getSecret').build(), // only this call
194
+ * redactionRule(HealthKey).redactAll().exclude('ping').build(), // everything but this call
58
195
  * ],
59
196
  * );
60
197
  * ```
@@ -66,13 +203,8 @@ export class RedactingEventListener implements ServiceModuleListener {
66
203
  ) {}
67
204
 
68
205
  onInitialize(context: InitializeContext): EventSpan | void {
69
- // The initialize result is the service instance itself, so it is
70
- // redacted only when the whole service is sensitive — a rule without
71
- // `properties`.
72
- return redactSpan(
73
- this.delegate.onInitialize?.(context),
74
- this.isRedacted(context.key),
75
- );
206
+ const rule = this.rules.find((r) => r.key === context.key);
207
+ return redactSpan(this.delegate.onInitialize?.(context), rule);
76
208
  }
77
209
 
78
210
  onDispose(context: DisposeContext): EventSpan | void {
@@ -81,42 +213,31 @@ export class RedactingEventListener implements ServiceModuleListener {
81
213
  }
82
214
 
83
215
  onMethodCall(context: MethodCallContext): EventSpan | void {
84
- const redacted = this.isRedacted(context.key, context.functionName);
216
+ const rule = this.rules.find((r) => r.key === context.key);
85
217
  const span = this.delegate.onMethodCall?.(
86
- redacted
87
- ? // Replace each argument rather than the whole array, so the
88
- // delegate still sees the call's arity.
89
- { ...context, args: context.args.map(() => REDACTED_VALUE) }
218
+ rule
219
+ ? {
220
+ ...context,
221
+ args: rule.maskArgs(context.functionName, context.args),
222
+ }
90
223
  : context,
91
224
  );
92
- return redactSpan(span, redacted);
93
- }
94
-
95
- // 1. Si expiro el dbr `EXPIRED` el cron debe mandar a `FACE_MISMATCH`?
96
- // 2. Si hubo un matchLevel intermediario el cron debe mandar a `FACE_MISMATCH`? (Probablemente no)
97
- private isRedacted(key: ServiceKey<any>, propertyName?: string): boolean {
98
- return this.rules.some(
99
- (rule) =>
100
- rule.key === key &&
101
- // A rule without `properties` redacts everything on the key,
102
- // including initialize — where no property name is passed —
103
- // while a rule with `properties` matches only those calls.
104
- (rule.properties === undefined ||
105
- (propertyName !== undefined &&
106
- rule.properties?.includes(propertyName))),
107
- );
225
+ return redactSpan(span, rule, context.functionName);
108
226
  }
109
227
  }
110
228
 
111
229
  /**
112
230
  * Wraps the delegate's EventSpan so success values are redacted before
113
- * `end` sees them. `run` (and any future fields) pass through untouched.
231
+ * `end` sees them, by delegating to the rule's `maskResult`. `run` (and
232
+ * any future fields) pass through untouched. `functionName` is omitted
233
+ * for the initialize result (the instance itself).
114
234
  */
115
235
  function redactSpan(
116
236
  span: EventSpan | void,
117
- redacted: boolean,
237
+ rule: RedactionRule<any> | undefined,
238
+ functionName?: string,
118
239
  ): EventSpan | void {
119
- if (!redacted || !span?.end) {
240
+ if (!rule || !span?.end) {
120
241
  return span;
121
242
  }
122
243
  const end = span.end.bind(span);
@@ -125,7 +246,10 @@ function redactSpan(
125
246
  end: (outcome: EventOutcome) =>
126
247
  end(
127
248
  outcome.type === 'success'
128
- ? { type: 'success', value: REDACTED_VALUE }
249
+ ? {
250
+ type: 'success',
251
+ value: rule.maskResult(functionName, outcome.value),
252
+ }
129
253
  : outcome,
130
254
  ),
131
255
  };