@feasibleone/blong-gogo 1.22.0 → 1.23.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/AdapterBase.ts +6 -4
- package/src/Gateway.ts +30 -6
- package/src/expected-errors.test.ts +291 -0
- package/src/lib.ts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.23.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.22.0...blong-gogo-v1.23.0) (2026-05-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* expected errors ([57ab05e](https://github.com/feasibleone/blong/commit/57ab05ed66b86405561f519e395e30f43b4fffa2))
|
|
9
|
+
|
|
3
10
|
## [1.22.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.21.0...blong-gogo-v1.22.0) (2026-05-14)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
package/src/AdapterBase.ts
CHANGED
|
@@ -14,6 +14,7 @@ import PQueue from 'p-queue';
|
|
|
14
14
|
import merge from 'ut-function.merge';
|
|
15
15
|
|
|
16
16
|
import ConfigRuntime from './ConfigRuntime.ts';
|
|
17
|
+
import {isExpectedError} from './lib.ts';
|
|
17
18
|
import loop from './loop.ts';
|
|
18
19
|
|
|
19
20
|
const errorMap: IErrorMap = {
|
|
@@ -177,11 +178,12 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
error(error: ITypedError, $meta: IMeta): void {
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
if ($meta) error.method = $meta.method;
|
|
182
|
+
if (isExpectedError(error.type, $meta?.expect)) {
|
|
183
|
+
(this.log as {debug?: (...args: unknown[]) => void})?.debug?.(error);
|
|
184
|
+
return;
|
|
184
185
|
}
|
|
186
|
+
(this.log as {error?: (...args: unknown[]) => void})?.error?.(error);
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
findValidation(): unknown {
|
package/src/Gateway.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {v4} from 'uuid';
|
|
|
20
20
|
import type {IResolution} from './Resolution.ts';
|
|
21
21
|
import type {IRpcClient} from './RpcClient.ts';
|
|
22
22
|
import jwt from './jwt.ts';
|
|
23
|
-
import {methodParts, snakeToCamel} from './lib.ts';
|
|
23
|
+
import {isExpectedError, methodParts, snakeToCamel} from './lib.ts';
|
|
24
24
|
import type {IConfig as IConfigMLE} from './mle.ts';
|
|
25
25
|
import swagger from './swagger.ts';
|
|
26
26
|
|
|
@@ -60,6 +60,15 @@ interface IConfig extends IConfigMLE {
|
|
|
60
60
|
logLevel?: LevelWithSilent;
|
|
61
61
|
cors?: unknown;
|
|
62
62
|
debug?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* When true, the gateway accepts the `expect` field from JSON-RPC request
|
|
65
|
+
* bodies and uses it to suppress error-level log entries for declared
|
|
66
|
+
* expected errors (demoting them to `debug` level).
|
|
67
|
+
*
|
|
68
|
+
* Set to `true` in the `dev` intent. Must be `false` (the default) in
|
|
69
|
+
* production to prevent callers from suppressing audit-level error logs.
|
|
70
|
+
*/
|
|
71
|
+
expectedErrors?: boolean;
|
|
63
72
|
errorFields: unknown[];
|
|
64
73
|
jwt: {
|
|
65
74
|
cache: object;
|
|
@@ -147,6 +156,7 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
147
156
|
encrypt: undefined,
|
|
148
157
|
},
|
|
149
158
|
debug: false,
|
|
159
|
+
expectedErrors: false,
|
|
150
160
|
errorFields: [],
|
|
151
161
|
jwt: {
|
|
152
162
|
cache: {},
|
|
@@ -418,6 +428,13 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
418
428
|
timeout: false,
|
|
419
429
|
expect: undefined,
|
|
420
430
|
};
|
|
431
|
+
// Only honour `expect` from the request body when the gateway
|
|
432
|
+
// is configured to allow it. This prevents external callers
|
|
433
|
+
// from suppressing error-level logs in production.
|
|
434
|
+
const resolvedExpect =
|
|
435
|
+
this.#config.expectedErrors && expect
|
|
436
|
+
? ([] as string[]).concat(expect)
|
|
437
|
+
: undefined;
|
|
421
438
|
const methodName = isWildcard
|
|
422
439
|
? new URL(request.url, 'http://localhost').pathname
|
|
423
440
|
.slice(5)
|
|
@@ -432,7 +449,7 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
432
449
|
...(timeout && {
|
|
433
450
|
timeout: this.#platform.timing.after(timeout as number),
|
|
434
451
|
}),
|
|
435
|
-
...(
|
|
452
|
+
...(resolvedExpect && {expect: resolvedExpect}),
|
|
436
453
|
...this._meta(request, pkg?.version, methodName.split('.')[0]),
|
|
437
454
|
};
|
|
438
455
|
const notfound = (): unknown =>
|
|
@@ -483,10 +500,17 @@ export default class Gateway extends Internal implements IGateway {
|
|
|
483
500
|
httpResponse?: unknown;
|
|
484
501
|
[key: string]: unknown;
|
|
485
502
|
};
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
503
|
+
if (isExpectedError(typedError.type as string | undefined, resolvedExpect)) {
|
|
504
|
+
request.log.debug(
|
|
505
|
+
{err: error, method: methodName},
|
|
506
|
+
'gateway expected error',
|
|
507
|
+
);
|
|
508
|
+
} else {
|
|
509
|
+
request.log.error(
|
|
510
|
+
{err: error, method: methodName},
|
|
511
|
+
'gateway handler error',
|
|
512
|
+
);
|
|
513
|
+
}
|
|
490
514
|
this._applyMeta(
|
|
491
515
|
reply
|
|
492
516
|
.header('x-envoy-decorator-operation', methodName)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the "expected errors" feature.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - isExpectedError() matching rules (exact, array, wildcard)
|
|
6
|
+
* - AdapterBase.error() log-level demotion for expected errors
|
|
7
|
+
* - Gateway respects `expectedErrors` config flag
|
|
8
|
+
* - Propagation: expect travels with $meta through the handler chain
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert';
|
|
12
|
+
import {describe, it} from 'node:test';
|
|
13
|
+
|
|
14
|
+
import {isExpectedError} from './lib.ts';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// isExpectedError() — matching rules
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
describe('isExpectedError — matching rules', () => {
|
|
21
|
+
it('returns false when error type is undefined', () => {
|
|
22
|
+
assert.strictEqual(isExpectedError(undefined, 'foo.bar'), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns false when expect is undefined', () => {
|
|
26
|
+
assert.strictEqual(isExpectedError('foo.bar', undefined), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns false when expect is an empty array', () => {
|
|
30
|
+
assert.strictEqual(isExpectedError('foo.bar', []), false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('exact string — matches identical type', () => {
|
|
34
|
+
assert.strictEqual(isExpectedError('parking.invalidZone', 'parking.invalidZone'), true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('exact string — does not match different type', () => {
|
|
38
|
+
assert.strictEqual(isExpectedError('parking.rateLimit', 'parking.invalidZone'), false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('array — matches when type is in the list', () => {
|
|
42
|
+
assert.strictEqual(
|
|
43
|
+
isExpectedError('auth.unauthorized', ['parking.invalidZone', 'auth.unauthorized']),
|
|
44
|
+
true,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('array — does not match when type is absent from the list', () => {
|
|
49
|
+
assert.strictEqual(
|
|
50
|
+
isExpectedError('payment.failed', ['parking.invalidZone', 'auth.unauthorized']),
|
|
51
|
+
false,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('wildcard — matches any type with the given prefix', () => {
|
|
56
|
+
assert.strictEqual(isExpectedError('parking.invalidZone', 'parking.*'), true);
|
|
57
|
+
assert.strictEqual(isExpectedError('parking.rateLimit', 'parking.*'), true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('wildcard — does not match a different namespace', () => {
|
|
61
|
+
assert.strictEqual(isExpectedError('auth.unauthorized', 'parking.*'), false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('wildcard — does not match the prefix segment itself (no dot)', () => {
|
|
65
|
+
// 'parking' alone should NOT match 'parking.*' (requires the dot)
|
|
66
|
+
assert.strictEqual(isExpectedError('parking', 'parking.*'), false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('array with wildcard — matches when a wildcard entry covers the type', () => {
|
|
70
|
+
assert.strictEqual(
|
|
71
|
+
isExpectedError('parking.invalidZone', ['auth.*', 'parking.*']),
|
|
72
|
+
true,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('wildcard at top of namespace — auth.*', () => {
|
|
77
|
+
assert.strictEqual(isExpectedError('auth.unauthorized', 'auth.*'), true);
|
|
78
|
+
assert.strictEqual(isExpectedError('auth.tokenExpired', 'auth.*'), true);
|
|
79
|
+
assert.strictEqual(isExpectedError('gateway.jwtMissingHeader', 'auth.*'), false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// AdapterBase.error() — log-level demotion
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
describe('AdapterBase.error() — log-level demotion', () => {
|
|
88
|
+
/**
|
|
89
|
+
* Minimal stand-in for AdapterBase that exposes only the `error()` method
|
|
90
|
+
* under test. We copy the implementation verbatim so we are testing the
|
|
91
|
+
* real logic without spinning up a full adapter.
|
|
92
|
+
*/
|
|
93
|
+
function makeAdapter(log: Record<string, (...a: unknown[]) => void>) {
|
|
94
|
+
return {
|
|
95
|
+
log,
|
|
96
|
+
error(
|
|
97
|
+
error: {type?: string; method?: string},
|
|
98
|
+
$meta: {method?: string; expect?: string | string[]},
|
|
99
|
+
) {
|
|
100
|
+
if ($meta) error.method = $meta.method;
|
|
101
|
+
if (isExpectedError(error.type, $meta?.expect)) {
|
|
102
|
+
(this.log as Record<string, (...a: unknown[]) => void>).debug?.(error);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
(this.log as Record<string, (...a: unknown[]) => void>).error?.(error);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
it('logs at error level when expect is not set', () => {
|
|
111
|
+
const logged: {level: string; error: unknown}[] = [];
|
|
112
|
+
const adapter = makeAdapter({
|
|
113
|
+
error: e => logged.push({level: 'error', error: e}),
|
|
114
|
+
debug: e => logged.push({level: 'debug', error: e}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
adapter.error({type: 'parking.invalidZone'}, {method: 'parkingTest'});
|
|
118
|
+
|
|
119
|
+
assert.strictEqual(logged.length, 1);
|
|
120
|
+
assert.strictEqual(logged[0].level, 'error');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('logs at debug level and does not log at error when error is expected', () => {
|
|
124
|
+
const logged: {level: string; error: unknown}[] = [];
|
|
125
|
+
const adapter = makeAdapter({
|
|
126
|
+
error: e => logged.push({level: 'error', error: e}),
|
|
127
|
+
debug: e => logged.push({level: 'debug', error: e}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
adapter.error(
|
|
131
|
+
{type: 'parking.invalidZone'},
|
|
132
|
+
{method: 'parkingTest', expect: 'parking.invalidZone'},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
assert.strictEqual(logged.length, 1);
|
|
136
|
+
assert.strictEqual(logged[0].level, 'debug');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('logs at debug level for wildcard match', () => {
|
|
140
|
+
const logged: {level: string; error: unknown}[] = [];
|
|
141
|
+
const adapter = makeAdapter({
|
|
142
|
+
error: e => logged.push({level: 'error', error: e}),
|
|
143
|
+
debug: e => logged.push({level: 'debug', error: e}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
adapter.error(
|
|
147
|
+
{type: 'parking.rateLimit'},
|
|
148
|
+
{method: 'parkingTest', expect: 'parking.*'},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
assert.strictEqual(logged.length, 1);
|
|
152
|
+
assert.strictEqual(logged[0].level, 'debug');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('logs at error level when the error type does not match expect', () => {
|
|
156
|
+
const logged: {level: string; error: unknown}[] = [];
|
|
157
|
+
const adapter = makeAdapter({
|
|
158
|
+
error: e => logged.push({level: 'error', error: e}),
|
|
159
|
+
debug: e => logged.push({level: 'debug', error: e}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
adapter.error(
|
|
163
|
+
{type: 'auth.unauthorized'},
|
|
164
|
+
{method: 'someMethod', expect: 'parking.invalidZone'},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
assert.strictEqual(logged.length, 1);
|
|
168
|
+
assert.strictEqual(logged[0].level, 'error');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('attaches $meta.method to the error object', () => {
|
|
172
|
+
const captured: unknown[] = [];
|
|
173
|
+
const adapter = makeAdapter({
|
|
174
|
+
error: e => captured.push(e),
|
|
175
|
+
debug: () => {},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const err: {type: string; method?: string} = {type: 'foo.bar'};
|
|
179
|
+
adapter.error(err, {method: 'someMethod'});
|
|
180
|
+
|
|
181
|
+
assert.strictEqual((captured[0] as typeof err).method, 'someMethod');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('attaches $meta.method even when the error is expected', () => {
|
|
185
|
+
const captured: unknown[] = [];
|
|
186
|
+
const adapter = makeAdapter({
|
|
187
|
+
error: () => {},
|
|
188
|
+
debug: e => captured.push(e),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const err: {type: string; method?: string} = {type: 'parking.invalidZone'};
|
|
192
|
+
adapter.error(err, {method: 'parkingTest', expect: 'parking.invalidZone'});
|
|
193
|
+
|
|
194
|
+
assert.strictEqual((captured[0] as typeof err).method, 'parkingTest');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Gateway expectedErrors flag
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
describe('Gateway — expectedErrors config flag', () => {
|
|
203
|
+
/**
|
|
204
|
+
* Simulate the gateway route-handler logic that builds `resolvedExpect`.
|
|
205
|
+
* Returns the value of `resolvedExpect` that the gateway would compute.
|
|
206
|
+
*/
|
|
207
|
+
function buildResolvedExpect(
|
|
208
|
+
expectedErrorsEnabled: boolean,
|
|
209
|
+
expectFromBody: string | string[] | undefined,
|
|
210
|
+
): string[] | undefined {
|
|
211
|
+
return expectedErrorsEnabled && expectFromBody
|
|
212
|
+
? ([] as string[]).concat(expectFromBody)
|
|
213
|
+
: undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
it('resolves expect when expectedErrors is true and expect is provided', () => {
|
|
217
|
+
const result = buildResolvedExpect(true, 'parking.invalidZone');
|
|
218
|
+
assert.deepStrictEqual(result, ['parking.invalidZone']);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('resolves expect array when expectedErrors is true', () => {
|
|
222
|
+
const result = buildResolvedExpect(true, ['parking.invalidZone', 'auth.unauthorized']);
|
|
223
|
+
assert.deepStrictEqual(result, ['parking.invalidZone', 'auth.unauthorized']);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('returns undefined when expectedErrors is false (production)', () => {
|
|
227
|
+
const result = buildResolvedExpect(false, 'parking.invalidZone');
|
|
228
|
+
assert.strictEqual(result, undefined);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('returns undefined when expect is not provided even if expectedErrors is true', () => {
|
|
232
|
+
const result = buildResolvedExpect(true, undefined);
|
|
233
|
+
assert.strictEqual(result, undefined);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('with resolvedExpect=undefined, isExpectedError always returns false', () => {
|
|
237
|
+
assert.strictEqual(isExpectedError('parking.invalidZone', undefined), false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Meta propagation — $meta.expect travels through handler chain
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
describe('$meta.expect propagation', () => {
|
|
246
|
+
/**
|
|
247
|
+
* Simulate a two-hop dispatch: caller → first handler → second handler.
|
|
248
|
+
* The second handler logs an error and the first handler propagates it.
|
|
249
|
+
* Verify that $meta.expect set by the caller controls log level at every hop.
|
|
250
|
+
*/
|
|
251
|
+
it('expect set by caller controls log level at every handler in the chain', () => {
|
|
252
|
+
const logged: {level: string; type: string}[] = [];
|
|
253
|
+
|
|
254
|
+
function logError(error: {type: string}, $meta: {expect?: string | string[]}) {
|
|
255
|
+
if (isExpectedError(error.type, $meta?.expect)) {
|
|
256
|
+
logged.push({level: 'debug', type: error.type});
|
|
257
|
+
} else {
|
|
258
|
+
logged.push({level: 'error', type: error.type});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const callerMeta = {method: 'callerMethod', expect: 'foo.someError'};
|
|
263
|
+
|
|
264
|
+
// Simulate two handler hops both receiving the same $meta
|
|
265
|
+
logError({type: 'foo.someError'}, callerMeta); // hop 1
|
|
266
|
+
logError({type: 'foo.someError'}, callerMeta); // hop 2
|
|
267
|
+
|
|
268
|
+
assert.strictEqual(logged.length, 2);
|
|
269
|
+
assert.ok(logged.every(l => l.level === 'debug'), 'all hops should log at debug');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('an error type not in expect is still logged at error level on all hops', () => {
|
|
273
|
+
const logged: {level: string; type: string}[] = [];
|
|
274
|
+
|
|
275
|
+
function logError(error: {type: string}, $meta: {expect?: string | string[]}) {
|
|
276
|
+
if (isExpectedError(error.type, $meta?.expect)) {
|
|
277
|
+
logged.push({level: 'debug', type: error.type});
|
|
278
|
+
} else {
|
|
279
|
+
logged.push({level: 'error', type: error.type});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const callerMeta = {method: 'callerMethod', expect: 'foo.someError'};
|
|
284
|
+
|
|
285
|
+
logError({type: 'bar.otherError'}, callerMeta); // unexpected on hop 1
|
|
286
|
+
logError({type: 'bar.otherError'}, callerMeta); // unexpected on hop 2
|
|
287
|
+
|
|
288
|
+
assert.strictEqual(logged.length, 2);
|
|
289
|
+
assert.ok(logged.every(l => l.level === 'error'), 'unexpected errors stay at error level');
|
|
290
|
+
});
|
|
291
|
+
});
|
package/src/lib.ts
CHANGED
|
@@ -66,6 +66,28 @@ export function parseAnnotatedKey(key: string): {
|
|
|
66
66
|
return {annotations, handlerName};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Returns true when an error type matches the `expect` declaration on `$meta`.
|
|
71
|
+
*
|
|
72
|
+
* Matching rules:
|
|
73
|
+
* - Exact string: `'foo.bar'` matches only `foo.bar`
|
|
74
|
+
* - Array of strings: any element that matches (exact or wildcard)
|
|
75
|
+
* - Wildcard prefix: `'foo.*'` matches any type starting with `foo.`
|
|
76
|
+
*/
|
|
77
|
+
export function isExpectedError(
|
|
78
|
+
errorType: string | undefined,
|
|
79
|
+
expect: string | string[] | undefined,
|
|
80
|
+
): boolean {
|
|
81
|
+
if (!errorType || !expect) return false;
|
|
82
|
+
const patterns = ([] as string[]).concat(expect);
|
|
83
|
+
return patterns.some(pattern => {
|
|
84
|
+
if (pattern.endsWith('.*')) {
|
|
85
|
+
return errorType.startsWith(pattern.slice(0, -1));
|
|
86
|
+
}
|
|
87
|
+
return errorType === pattern;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
let loginCache: Promise<{protocol: string; hostname: string; port: number}> | null = null;
|
|
70
92
|
export async function loginService(
|
|
71
93
|
discovery: (service: string) => Promise<{protocol: string; hostname: string; port: number}>,
|