@feasibleone/blong-gogo 1.35.0 → 1.37.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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * The framework's two progress-point handles (PRD R11/R26).
3
+ *
4
+ * A checkpoint reports a moment; a decision selects a branch. That difference is the whole
5
+ * reason one is optional and the other is not, so it is pinned here: a checkpoint exists
6
+ * only where points are recorded, a decision exists in every mode, and both reach the log
7
+ * through the attachable vocabulary — degrading to nothing and to a plain selection where
8
+ * no emitter is attached, because a dispatch path must never reach the emitter's ambient
9
+ * scope (F-197).
10
+ */
11
+ import type {
12
+ IMeta,
13
+ IProgressEntry,
14
+ IProgressPoint,
15
+ IProgressRegion,
16
+ } from '@feasibleone/blong/types';
17
+ import {
18
+ attachSemanticVocabulary,
19
+ detachSemanticVocabulary,
20
+ } from '@feasibleone/semantic-log/attachable';
21
+ import * as semantic from '@feasibleone/semantic-log/emitter';
22
+ import t from 'tap';
23
+ import {createAttachCheckpoint} from './checkpoint.ts';
24
+
25
+ const MODES = ['production', 'debug', 'test'] as const;
26
+
27
+ /** The points an invocation recorded, in the order it announced them. */
28
+ const pointsOf = (progress: IProgressEntry[] | undefined): IProgressPoint[] =>
29
+ (progress ?? []).filter((entry): entry is IProgressPoint => entry.kind === 'point');
30
+
31
+ /** The branches an invocation took, in the order it took them. */
32
+ const regionsOf = (progress: IProgressEntry[] | undefined): IProgressRegion[] =>
33
+ (progress ?? []).filter((entry): entry is IProgressRegion => entry.kind === 'region');
34
+
35
+ t.test('a branch is taken and its rationale kept, in every mode', t => {
36
+ for (const mode of MODES) {
37
+ const meta: IMeta = {};
38
+ createAttachCheckpoint(mode)(meta);
39
+ const result = meta.decide?.('rate-within-limit', {rate: 2}, [
40
+ {name: 'decline', when: values => (values.rate as number) > 1, run: () => 'declined'},
41
+ {name: 'accept', when: () => true, run: () => 'accepted'},
42
+ ]);
43
+ t.equal(result, 'declined', `${mode}: the branch is taken`);
44
+ t.same(
45
+ meta.progress,
46
+ [
47
+ {
48
+ kind: 'region',
49
+ discriminator: 'rate-within-limit',
50
+ candidates: ['decline', 'accept'],
51
+ chosen: 'decline',
52
+ values: {rate: 2},
53
+ },
54
+ ],
55
+ `${mode}: and the invocation keeps which one it was`,
56
+ );
57
+ }
58
+ t.end();
59
+ });
60
+
61
+ t.test('a checkpoint exists only where points are recorded', t => {
62
+ const production: IMeta = {};
63
+ createAttachCheckpoint('production')(production);
64
+ t.notOk(
65
+ production.checkpoint,
66
+ 'absent in production, which is what makes the `?.` call cost nothing',
67
+ );
68
+ t.ok(production.decide, 'while the branch helper is there, because a branch cannot be skipped');
69
+
70
+ const debug: IMeta = {};
71
+ createAttachCheckpoint('debug')(debug);
72
+ debug.checkpoint?.('total-calculated', {total: 200});
73
+ const [point] = pointsOf(debug.progress);
74
+ t.equal(debug.progress?.length, 1, 'recorded where points are');
75
+ t.equal(point?.name, 'total-calculated');
76
+ t.same(point?.data, {total: 200});
77
+ t.type(point?.timestamp, 'number', 'and stamped when it happened');
78
+ t.end();
79
+ });
80
+
81
+ t.test('a point announced inside a branch names the branch it sat in', t => {
82
+ attachSemanticVocabulary(semantic);
83
+ t.teardown(() => detachSemanticVocabulary());
84
+ const meta: IMeta = {};
85
+ createAttachCheckpoint('test')(meta);
86
+ meta.checkpoint?.('before-the-branch');
87
+ meta.decide?.('rate-within-limit', {rate: 2}, [
88
+ {
89
+ name: 'decline',
90
+ when: () => true,
91
+ run: () => {
92
+ meta.checkpoint?.('declined');
93
+ return 'declined';
94
+ },
95
+ },
96
+ {name: 'accept', when: () => true, run: () => 'accepted'},
97
+ ]);
98
+
99
+ t.same(
100
+ meta.progress?.map(entry => entry.kind),
101
+ ['point', 'region', 'point'],
102
+ 'the branch is announced where it was taken, between the points on either side of it',
103
+ );
104
+ t.same(
105
+ pointsOf(meta.progress).map(point => point.regions?.length ?? 0),
106
+ [0, 1],
107
+ 'and only the point announced inside the branch names it, which is what nests a report',
108
+ );
109
+ t.same(
110
+ pointsOf(meta.progress)[1]?.regions,
111
+ [
112
+ {
113
+ discriminator: 'rate-within-limit',
114
+ candidates: ['decline', 'accept'],
115
+ chosen: 'decline',
116
+ },
117
+ ],
118
+ 'by the names of the branch, not by a position the log mints for itself',
119
+ );
120
+ t.same(
121
+ regionsOf(meta.progress)[0]?.regions,
122
+ undefined,
123
+ 'while the branch itself names nothing above it',
124
+ );
125
+ t.end();
126
+ });
127
+
128
+ t.test('a branch taken inside a branch names the one it sits in', t => {
129
+ attachSemanticVocabulary(semantic);
130
+ t.teardown(() => detachSemanticVocabulary());
131
+ const meta: IMeta = {};
132
+ createAttachCheckpoint('test')(meta);
133
+ meta.decide?.('outer', {}, [
134
+ {
135
+ name: 'taken',
136
+ when: () => true,
137
+ run: () =>
138
+ meta.decide?.('inner', {}, [{name: 'only', when: () => true, run: () => 'inner'}]),
139
+ },
140
+ ]);
141
+
142
+ t.same(
143
+ regionsOf(meta.progress).map(region => region.discriminator),
144
+ ['outer', 'inner'],
145
+ 'both are announced, outermost first',
146
+ );
147
+ t.same(
148
+ regionsOf(meta.progress)[1]?.regions?.map(region => region.discriminator),
149
+ ['outer'],
150
+ 'and the nested one is in the chain of the one that encloses it',
151
+ );
152
+ t.end();
153
+ });
154
+
155
+ t.test('a checkpoint is announced to the log, and announces nothing without one', t => {
156
+ detachSemanticVocabulary();
157
+ const detached: IMeta = {};
158
+ createAttachCheckpoint('test')(detached);
159
+ detached.checkpoint?.('no-log');
160
+ t.equal(
161
+ semantic.takePoints(),
162
+ undefined,
163
+ 'nothing is announced where no emitter is attached: the degradation is the whole point',
164
+ );
165
+
166
+ attachSemanticVocabulary(semantic);
167
+ t.teardown(() => detachSemanticVocabulary());
168
+ const attached: IMeta = {};
169
+ createAttachCheckpoint('test')(attached);
170
+ attached.checkpoint?.('total-calculated', {total: 200});
171
+ t.same(
172
+ semantic.takePoints(),
173
+ [{name: 'total-calculated', data: {total: 200}}],
174
+ 'PRD R26: the milestone reaches the log, so a diagram can draw it as a note',
175
+ );
176
+ t.equal(semantic.takePoints(), undefined, 'and is taken once, by the record that carries it');
177
+ t.end();
178
+ });
179
+
180
+ t.test('a decision is announced to the log too, and still selects without one', t => {
181
+ detachSemanticVocabulary();
182
+ const detached: IMeta = {};
183
+ createAttachCheckpoint('test')(detached);
184
+ t.equal(
185
+ detached.decide?.('d', {}, [{name: 'only', when: () => true, run: () => 7}]),
186
+ 7,
187
+ 'the branch is taken with nothing attached',
188
+ );
189
+ t.equal(semantic.takeDecision(), undefined, 'and no rationale is staged to record');
190
+
191
+ attachSemanticVocabulary(semantic);
192
+ t.teardown(() => detachSemanticVocabulary());
193
+ const attached: IMeta = {};
194
+ createAttachCheckpoint('test')(attached);
195
+ attached.decide?.('route-selection', {}, [
196
+ {name: 'hubB', when: () => true, run: () => 'hubB'},
197
+ {name: 'hold', when: () => true, run: () => undefined},
198
+ ]);
199
+ t.same(
200
+ semantic.takeDecision()?.candidates,
201
+ ['hubB', 'hold'],
202
+ 'the rationale is staged for the next record, in evaluation order (PRD R11)',
203
+ );
204
+ t.end();
205
+ });
206
+
207
+ t.test('no branch matching is a recorded non-choice', t => {
208
+ const meta: IMeta = {};
209
+ createAttachCheckpoint('test')(meta);
210
+ t.equal(meta.decide?.('never', {}, [{name: 'a', when: () => false, run: () => 1}]), undefined);
211
+ t.equal(regionsOf(meta.progress)[0]?.chosen, 'none', 'recorded as `none` rather than lost');
212
+ t.end();
213
+ });
214
+
215
+ t.test('each predicate is evaluated once', t => {
216
+ const meta: IMeta = {};
217
+ createAttachCheckpoint('test')(meta);
218
+ let evaluations = 0;
219
+ meta.decide?.('counting', {}, [
220
+ {
221
+ name: 'only',
222
+ when: () => {
223
+ evaluations++;
224
+ return true;
225
+ },
226
+ run: () => 'taken',
227
+ },
228
+ ]);
229
+ t.equal(evaluations, 1, 'the `run` is wrapped, not the predicate re-evaluated');
230
+ t.end();
231
+ });
232
+
233
+ t.test('a branch that throws records nothing', t => {
234
+ const meta: IMeta = {};
235
+ createAttachCheckpoint('test')(meta);
236
+ t.throws(
237
+ () =>
238
+ meta.decide?.('boom', {}, [
239
+ {
240
+ name: 'a',
241
+ when: () => true,
242
+ run: () => {
243
+ throw new Error('handler failed');
244
+ },
245
+ },
246
+ ]),
247
+ /handler failed/,
248
+ 'the failure is not swallowed',
249
+ );
250
+ t.notOk(
251
+ meta.progress,
252
+ 'a decision that never completed is not a decision, so nothing is recorded',
253
+ );
254
+ t.end();
255
+ });
256
+
257
+ t.test('attaching twice does not replace a handle already there', t => {
258
+ const meta: IMeta = {};
259
+ const attach = createAttachCheckpoint('test');
260
+ attach(meta);
261
+ const first = meta.decide;
262
+ attach(meta);
263
+ t.equal(meta.decide, first, 'a dispatch that was already prepared is left alone');
264
+ t.end();
265
+ });
package/src/checkpoint.ts CHANGED
@@ -1,32 +1,133 @@
1
- import type {CheckpointFn, IMeta} from '@feasibleone/blong/types';
1
+ import type {
2
+ CheckpointFn,
3
+ DecideFn,
4
+ IMeta,
5
+ IProgressPoint,
6
+ IProgressRegion,
7
+ IRegionMark,
8
+ } from '@feasibleone/blong/types';
9
+ import {vocabulary} from '@feasibleone/semantic-log/attachable';
2
10
 
3
11
  /**
4
- * Records a checkpoint in the $meta.checkpoints array.
12
+ * The branches the calling scope is running inside, named the way `$meta` names them.
13
+ *
14
+ * Names only, and never the log's own mark: the mark carries the position it was minted at,
15
+ * which describes a place in *this* process' execution. An entry that travelled here from
16
+ * another process over a call would bring a position that means nothing here, while the three
17
+ * names still say exactly which branches it sat in — which is all a report groups by.
18
+ */
19
+ const regionMarks = (): IRegionMark[] | undefined => {
20
+ const chain = vocabulary.currentRegions();
21
+ if (chain === undefined || chain.length === 0) {
22
+ return undefined;
23
+ }
24
+ return chain.map(region => ({
25
+ discriminator: region.discriminator,
26
+ candidates: region.candidates,
27
+ chosen: region.chosen,
28
+ }));
29
+ };
30
+
31
+ /**
32
+ * Reports a checkpoint into `$meta.progress`, and announces it as a progress point
33
+ * (PRD R26).
34
+ *
5
35
  * Uses `this` binding — works correctly when called as
6
36
  * $meta.checkpoint?.('name', data) with optional chaining.
7
37
  */
8
38
  const checkpoint: CheckpointFn = function (this: IMeta, name: string, data?: unknown): void {
9
- (this.checkpoints ??= []).push({
39
+ const regions = regionMarks();
40
+ const entry: IProgressPoint = {
41
+ kind: 'point',
10
42
  name,
11
- data,
43
+ ...(data === undefined ? {} : {data}),
12
44
  timestamp: Date.now(),
13
- });
45
+ ...(regions === undefined ? {} : {regions}),
46
+ };
47
+ (this.progress ??= []).push(entry);
48
+ // The same moment is announced to the log, so a checkpoint reaches the record the
49
+ // sequence diagram is drawn from rather than only the list a test asserts on. The
50
+ // vocabulary degrades to nothing where no emitter is attached, so this is one call
51
+ // and never a way for a dispatch path to import the emitter's ambient scope (F-197).
52
+ vocabulary.point(name, data);
14
53
  };
15
54
 
16
55
  /**
17
- * Attaches the checkpoint function to $meta if not already present.
18
- * The function uses `this` to record into the $meta it's called on.
56
+ * Takes a branch, and keeps what it was and which one was taken on the invocation that took
57
+ * it (PRD R11/R26).
58
+ *
59
+ * `this`-bound like {@link checkpoint}. The branch's `run` is *wrapped* rather than the
60
+ * predicates re-evaluated: a predicate with a side effect must run exactly once, and the
61
+ * decision recorded has to be the one the code actually took. A branch whose `run` throws
62
+ * records nothing — the entry is withdrawn before the throw propagates, because a decision
63
+ * that never completed is not a decision.
19
64
  */
20
- export function attachCheckpoint($meta: IMeta): void {
21
- $meta.checkpoint ??= checkpoint;
22
- }
65
+ const decide: DecideFn = function <T>(
66
+ this: IMeta,
67
+ discriminator: string,
68
+ values: Record<string, unknown>,
69
+ branches: ReadonlyArray<{
70
+ name: string;
71
+ when: (values: Record<string, unknown>) => boolean;
72
+ run: () => T;
73
+ }>,
74
+ ): T | undefined {
75
+ let chosen: string | undefined;
76
+ const wrapped = branches.map(branch => ({
77
+ ...branch,
78
+ run: () => {
79
+ chosen = branch.name;
80
+ return branch.run();
81
+ },
82
+ }));
83
+ // The entry is pushed *before* the branch runs, which is what puts it where the
84
+ // announcement order wants it: the points the branch announces come after it in the
85
+ // array, and a report that groups by those points therefore draws them inside the
86
+ // branch. Nothing else in the entry is known yet, so `chosen` is filled in below.
87
+ const outer = regionMarks();
88
+ const entry: IProgressRegion = {
89
+ kind: 'region',
90
+ discriminator,
91
+ candidates: branches.map(branch => branch.name),
92
+ chosen: '',
93
+ values,
94
+ ...(outer === undefined ? {} : {regions: outer}),
95
+ };
96
+ const held = this.progress;
97
+ const progress = (this.progress ??= []);
98
+ progress.push(entry);
99
+ try {
100
+ const result = vocabulary.decide<T>(discriminator, values, wrapped);
101
+ entry.chosen = chosen ?? 'none';
102
+ return result;
103
+ } catch (error) {
104
+ // A branch whose `run` throws records nothing, which is the rule this recorder has
105
+ // always kept: a decision that never completed is not a decision. The entry was
106
+ // pushed early, so it is withdrawn again — and, where the array was this call's own,
107
+ // the slot goes with it, so a throwing branch leaves `$meta.progress` exactly as
108
+ // absent as it found it. The throw still propagates.
109
+ const at = progress.lastIndexOf(entry);
110
+ if (at !== -1) progress.splice(at, 1);
111
+ if (held === undefined && progress.length === 0) delete this.progress;
112
+ throw error;
113
+ }
114
+ };
23
115
 
24
116
  /**
25
- * Returns an attachCheckpoint function when checkpoint mode is enabled,
26
- * or undefined for production (zero overhead via optional chaining).
117
+ * Attach the framework's progress-point handles to a dispatch's `$meta`.
118
+ *
119
+ * `decide` is attached in **every** mode: it selects a branch, so a missing one would skip
120
+ * the work rather than cost a note. `checkpoint` is attached only where points are
121
+ * recorded, which is what keeps its `?.` call free in production.
27
122
  */
28
123
  export function createAttachCheckpoint(
29
124
  mode: 'test' | 'debug' | 'production',
30
- ): ((meta: IMeta) => void) | undefined {
31
- return mode === 'production' ? undefined : attachCheckpoint;
125
+ ): (meta: IMeta) => void {
126
+ const recording = mode !== 'production';
127
+ return (meta: IMeta): void => {
128
+ meta.decide ??= decide;
129
+ if (recording) {
130
+ meta.checkpoint ??= checkpoint;
131
+ }
132
+ };
32
133
  }
@@ -0,0 +1,94 @@
1
+ import {server} from '@feasibleone/blong';
2
+ import {test} from 'tap';
3
+
4
+ import load from './loadServer.ts';
5
+
6
+ /**
7
+ * The `ci` block is a captured run's configuration — colours off, Allure results
8
+ * written — and a CI run is one by definition, so `activeConfigs` activates the
9
+ * block for it whether or not the entry named the intent.
10
+ *
11
+ * What made this worth a test: only the entries that remembered to pass `ci` in CI
12
+ * wrote Allure results, so a package whose only runner is tap produced no report and
13
+ * the CI summary had nothing to link for it. The observable here is the same trick
14
+ * `exit.intent.test.ts` uses — a config key that only the `ci` block sets — because
15
+ * the resolved `watch.allure.enabled` is not exposed on the registry.
16
+ *
17
+ * Only `load` is exercised, never `start`: the assertions are about which config
18
+ * blocks merged. Every load is stopped, since the `integration` intent opens the
19
+ * semantic-log cluster service and a registry that is never stopped leaves the suite
20
+ * reporting `timeout!` instead of passing (`exit.intent.test.ts` explains it at
21
+ * length).
22
+ */
23
+
24
+ /**
25
+ * The suite the load is built from. `pkg` is supplied so the loader skips its
26
+ * `createRequire(mod.url)('./package.json')` lookup, and `parentConfig` is an object
27
+ * rather than a name so the test does not depend on the working directory.
28
+ */
29
+ const suite = (config: object) =>
30
+ server(
31
+ () =>
32
+ ({
33
+ url: import.meta.url,
34
+ pkg: {name: 'ci-intent-test', version: '0.0.0'},
35
+ children: [],
36
+ config: {default: {}, ...config},
37
+ }) as never,
38
+ );
39
+
40
+ /** The `exit` the run resolves, with `ci` as the only thing that sets it. */
41
+ async function exitWithCiBlock(intents: string[]): Promise<boolean | undefined> {
42
+ const registry = await load(
43
+ suite({ci: {exit: true}}) as unknown as Parameters<typeof load>[0],
44
+ 'ci-intent-test',
45
+ {},
46
+ intents,
47
+ );
48
+ try {
49
+ return registry.exit;
50
+ } finally {
51
+ await registry.stop();
52
+ }
53
+ }
54
+
55
+ /** Run `fn` with `CI` set the way a runner sets it, and restore it afterwards. */
56
+ async function withCi<T>(value: string | undefined, fn: () => Promise<T>): Promise<T> {
57
+ const previous = process.env['CI'];
58
+ if (value === undefined) delete process.env['CI'];
59
+ else process.env['CI'] = value;
60
+ try {
61
+ return await fn();
62
+ } finally {
63
+ if (previous === undefined) delete process.env['CI'];
64
+ else process.env['CI'] = previous;
65
+ }
66
+ }
67
+
68
+ test('a CI run activates the ci block without being asked', async t => {
69
+ t.equal(
70
+ await withCi('true', () => exitWithCiBlock([])),
71
+ true,
72
+ 'the block merged although no intent named it',
73
+ );
74
+ t.equal(
75
+ await withCi('true', () => exitWithCiBlock(['dev', 'integration'])),
76
+ true,
77
+ 'the intents a run normally passes do not take it away',
78
+ );
79
+ });
80
+
81
+ test('a run that is not CI needs the intent', async t => {
82
+ // `false`, not `undefined`: the registry always carries a boolean once loaded.
83
+ // Deleting `CI` rather than inheriting it, because this suite runs in CI itself.
84
+ t.equal(
85
+ await withCi(undefined, () => exitWithCiBlock([])),
86
+ false,
87
+ 'the block stays out of a run whose output is watched',
88
+ );
89
+ t.equal(
90
+ await withCi(undefined, () => exitWithCiBlock(['ci'])),
91
+ true,
92
+ 'and naming it still does what it always did',
93
+ );
94
+ });
@@ -1,4 +1,4 @@
1
- import {handler, type IMeta, type ITypedError} from '@feasibleone/blong/types';
1
+ import {handler, type IMeta, type IProgressEntry, type ITypedError} from '@feasibleone/blong/types';
2
2
  import {type Response} from 'got';
3
3
 
4
4
  export default handler(({errors}) => ({
@@ -8,7 +8,7 @@ export default handler(({errors}) => ({
8
8
  error?: unknown;
9
9
  validation?: unknown;
10
10
  debug?: unknown;
11
- checkpoints?: unknown[];
11
+ progress?: unknown[];
12
12
  }>,
13
13
  $meta?: IMeta,
14
14
  ) {
@@ -54,8 +54,8 @@ export default handler(({errors}) => ({
54
54
  }),
55
55
  });
56
56
  } else if (typeof body === 'object' && 'result' in body && !('error' in body)) {
57
- if ($meta && body.checkpoints?.length) {
58
- ($meta.checkpoints ??= []).push(...body.checkpoints);
57
+ if ($meta && body.progress?.length) {
58
+ ($meta.progress ??= []).push(...(body.progress as IProgressEntry[]));
59
59
  }
60
60
  return body.result;
61
61
  } else {
@@ -99,10 +99,13 @@ export default handler<{
99
99
  * expired session) the tokens are cleared so the next request surfaces a
100
100
  * clean 401 to the caller, which the UI turns into a login prompt.
101
101
  */
102
- async function refresh(this: {
103
- exec?(...params: unknown[]): Promise<unknown>;
104
- error?(error: unknown, $meta?: unknown): void;
105
- }, opts: {force?: boolean} = {}): Promise<void> {
102
+ async function refresh(
103
+ this: {
104
+ exec?(...params: unknown[]): Promise<unknown>;
105
+ error?(error: unknown, $meta?: unknown): void;
106
+ },
107
+ opts: {force?: boolean} = {},
108
+ ): Promise<void> {
106
109
  const now = Date.now();
107
110
  if (token && (opts.force || tokenExpire < now)) {
108
111
  if (refreshToken && refreshTokenExpire > now) {
@@ -126,7 +129,10 @@ export default handler<{
126
129
  {},
127
130
  )) as {
128
131
  statusCode?: number;
129
- body?: {result?: IToken; error?: {type?: string; message?: string; statusCode?: number}};
132
+ body?: {
133
+ result?: IToken;
134
+ error?: {type?: string; message?: string; statusCode?: number};
135
+ };
130
136
  };
131
137
  return result;
132
138
  })();
@@ -134,7 +140,10 @@ export default handler<{
134
140
  if (pending !== null) pending = null;
135
141
  const {body, statusCode} = result as {
136
142
  statusCode?: number;
137
- body?: {result?: IToken; error?: {type?: string; message?: string; statusCode?: number}};
143
+ body?: {
144
+ result?: IToken;
145
+ error?: {type?: string; message?: string; statusCode?: number};
146
+ };
138
147
  };
139
148
  // The gateway MLE-encrypts the response with the handshake
140
149
  // keys, so decrypt the result before reading the token.
@@ -263,7 +272,14 @@ export default handler<{
263
272
  },
264
273
  async loginTokenCreateResponseReceive(result: Response<{result: unknown}>, $meta: unknown) {
265
274
  await decrypt(result.body, 'result');
266
- if ((result.body as {error?: unknown})?.error) return super.receive(result, $meta);
275
+ if ((result.body as {error?: unknown})?.error) {
276
+ // The base `receive` reads an error from the *decrypted* body and this hook
277
+ // delegates to it, so the error has to be decrypted here: without this the
278
+ // encrypted envelope is what a failed login throws, and the reason for the
279
+ // failure is unreadable to the caller that has to act on it.
280
+ await decrypt(result.body, 'error');
281
+ return super.receive(result, $meta);
282
+ }
267
283
  readToken(result.body.result as IToken);
268
284
  return super.receive(result, $meta);
269
285
  },
@@ -272,9 +288,15 @@ export default handler<{
272
288
  async loginTokenRefreshRequestSend(params: {$http?: unknown}, $meta: unknown) {
273
289
  return super.send(await encryptPublic(params), $meta);
274
290
  },
275
- async loginTokenRefreshResponseReceive(result: Response<{result: unknown}>, $meta: unknown) {
291
+ async loginTokenRefreshResponseReceive(
292
+ result: Response<{result: unknown}>,
293
+ $meta: unknown,
294
+ ) {
276
295
  await decrypt(result.body, 'result');
277
- if ((result.body as {error?: unknown})?.error) return super.receive(result, $meta);
296
+ if ((result.body as {error?: unknown})?.error) {
297
+ await decrypt(result.body, 'error');
298
+ return super.receive(result, $meta);
299
+ }
278
300
  readToken(result.body.result as IToken);
279
301
  return super.receive(result, $meta);
280
302
  },
@@ -290,9 +312,15 @@ export default handler<{
290
312
  async loginTokenRestoreRequestSend(params: {$http?: unknown}, $meta: unknown) {
291
313
  return super.send(await encryptPublic(params), $meta);
292
314
  },
293
- async loginTokenRestoreResponseReceive(result: Response<{result: unknown}>, $meta: unknown) {
315
+ async loginTokenRestoreResponseReceive(
316
+ result: Response<{result: unknown}>,
317
+ $meta: unknown,
318
+ ) {
294
319
  await decrypt(result.body, 'result');
295
- if ((result.body as {error?: unknown})?.error) return super.receive(result, $meta);
320
+ if ((result.body as {error?: unknown})?.error) {
321
+ await decrypt(result.body, 'error');
322
+ return super.receive(result, $meta);
323
+ }
296
324
  readToken(result.body.result as IToken);
297
325
  return super.receive(result, $meta);
298
326
  },