@oxyhq/core 10.1.1 → 10.1.3
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/session/accountDialogController.js +25 -7
- package/dist/cjs/utils/textNormalization.js +26 -13
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/session/accountDialogController.js +25 -7
- package/dist/esm/utils/textNormalization.js +26 -13
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/models/interfaces.d.ts +0 -1
- package/dist/types/utils/textNormalization.d.ts +11 -8
- package/package.json +1 -1
- package/src/models/interfaces.ts +0 -1
- package/src/session/__tests__/accountDialogController.test.ts +36 -0
- package/src/session/accountDialogController.ts +23 -7
- package/src/utils/__tests__/textNormalization.test.ts +11 -2
- package/src/utils/textNormalization.ts +27 -13
|
@@ -78,20 +78,23 @@ export declare function normalizeInlineText(value: string): string;
|
|
|
78
78
|
* 2. Unify every line-break form (CRLF, lone CR, U+2028, U+2029) to `\n`.
|
|
79
79
|
* 3. Collapse runs of HORIZONTAL whitespace (spaces, tabs, NBSP and friends)
|
|
80
80
|
* to a single space. Line breaks are untouched.
|
|
81
|
-
* 4. Strip the horizontal whitespace at
|
|
81
|
+
* 4. Strip the horizontal whitespace at BOTH ends of every line.
|
|
82
82
|
* 5. Collapse three or more line breaks to exactly one blank line (`\n\n`).
|
|
83
|
-
* 6. Trim both ends.
|
|
83
|
+
* 6. Trim both ends of the value.
|
|
84
84
|
*
|
|
85
85
|
* STEP 4 MUST PRECEDE STEP 5 — this is the whole point of the function. A
|
|
86
86
|
* "blank" line that actually contains spaces (`"a\n \n \nb"`) breaks the
|
|
87
87
|
* run of `\n` characters, so a bare `\n{3,}` collapse (step 5 alone) never sees
|
|
88
88
|
* it and the extra blank lines survive into the UI. That is exactly the bug in
|
|
89
|
-
* federated post bodies.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
89
|
+
* federated post bodies. Trimming each line first turns those lines into real,
|
|
90
|
+
* empty lines, which step 5 then collapses.
|
|
91
|
+
*
|
|
92
|
+
* Every line is trimmed on BOTH sides, so a leading indent is removed outright
|
|
93
|
+
* rather than reduced to one space. Step 3 has already destroyed whatever indent
|
|
94
|
+
* the author wrote (`" Mundo"` → `" Mundo"`), so a surviving space would not
|
|
95
|
+
* be the author's intent — it would be an arbitrary remnant of exactly the
|
|
96
|
+
* source-markup indentation this function exists to erase, and `pre-wrap` renders
|
|
97
|
+
* it. Indentation is invisible in HTML by spec; it must be invisible here too.
|
|
95
98
|
*
|
|
96
99
|
* A value that is empty or whitespace-only returns `''`.
|
|
97
100
|
*
|
package/package.json
CHANGED
package/src/models/interfaces.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { User } from '../../models/interfaces';
|
|
|
4
4
|
import type { SessionLoginResponse, MinimalUserData } from '../../models/session';
|
|
5
5
|
import type { AccountNode } from '../../mixins/OxyServices.accounts';
|
|
6
6
|
import { SessionClient, type SessionClientHost } from '../SessionClient';
|
|
7
|
+
import { logger } from '../../utils/loggerUtils';
|
|
7
8
|
import {
|
|
8
9
|
AccountDialogController,
|
|
9
10
|
createAccountDialogController,
|
|
@@ -206,6 +207,7 @@ describe('AccountDialogController — account list', () => {
|
|
|
206
207
|
});
|
|
207
208
|
|
|
208
209
|
it('keeps device rows and surfaces the error when listAccounts fails', async () => {
|
|
210
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
209
211
|
const { controller, oxy, sc } = makeHarness();
|
|
210
212
|
sc.set(state([{ accountId: 'a1', sessionId: 's1' }], 'a1'));
|
|
211
213
|
oxy.getUsersByIds.mockResolvedValue([user('a1')]);
|
|
@@ -216,6 +218,40 @@ describe('AccountDialogController — account list', () => {
|
|
|
216
218
|
const snap = controller.getSnapshot();
|
|
217
219
|
expect(snap.error).toBe('graph boom');
|
|
218
220
|
expect(snap.accounts.map((r) => r.accountId)).toEqual(['a1']);
|
|
221
|
+
// A genuine (non-401) failure STILL warns.
|
|
222
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
223
|
+
'[AccountDialogController] listAccounts failed',
|
|
224
|
+
{ component: 'AccountDialogController' },
|
|
225
|
+
expect.any(Error),
|
|
226
|
+
);
|
|
227
|
+
warnSpy.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('treats a 401 from listAccounts as the signed-out edge — debug, no surfaced error, no warn', async () => {
|
|
231
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
232
|
+
const debugSpy = jest.spyOn(logger, 'debug').mockImplementation(() => undefined);
|
|
233
|
+
const { controller, oxy, sc } = makeHarness();
|
|
234
|
+
sc.set(state([{ accountId: 'a1', sessionId: 's1' }], 'a1'));
|
|
235
|
+
oxy.getUsersByIds.mockResolvedValue([user('a1')]);
|
|
236
|
+
// A stale/revoked bearer 401s: an EXPECTED signed-out outcome, not a failure.
|
|
237
|
+
oxy.listAccounts.mockRejectedValue(
|
|
238
|
+
Object.assign(new Error('Invalid or missing authorization header'), { status: 401 }),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
await controller.refresh();
|
|
242
|
+
|
|
243
|
+
const snap = controller.getSnapshot();
|
|
244
|
+
// Never surface an error for a normal signed-out state; device rows still render.
|
|
245
|
+
expect(snap.error).toBeNull();
|
|
246
|
+
expect(snap.accounts.map((r) => r.accountId)).toEqual(['a1']);
|
|
247
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
248
|
+
expect(debugSpy).toHaveBeenCalledWith(
|
|
249
|
+
'[AccountDialogController] listAccounts unauthorized (signed out)',
|
|
250
|
+
{ component: 'AccountDialogController' },
|
|
251
|
+
expect.objectContaining({ status: 401 }),
|
|
252
|
+
);
|
|
253
|
+
warnSpy.mockRestore();
|
|
254
|
+
debugSpy.mockRestore();
|
|
219
255
|
});
|
|
220
256
|
});
|
|
221
257
|
|
|
@@ -30,6 +30,7 @@ import type { OxyServices } from '../OxyServices';
|
|
|
30
30
|
import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
31
31
|
import type { User } from '../models/interfaces';
|
|
32
32
|
import { logger } from '../utils/loggerUtils';
|
|
33
|
+
import { extractErrorStatus } from '../utils/errorUtils';
|
|
33
34
|
import { CENTRAL_IDP_APEX } from '../utils/authWebUrl';
|
|
34
35
|
import {
|
|
35
36
|
generateOAuthState,
|
|
@@ -392,10 +393,19 @@ export class AccountDialogController {
|
|
|
392
393
|
try {
|
|
393
394
|
graph = await this.oxyServices.listAccounts();
|
|
394
395
|
} catch (error) {
|
|
395
|
-
// A
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
// A 401 here is the EXPECTED signed-out edge, not a failure: the bearer was
|
|
397
|
+
// stale/revoked, so `HttpService` already cleared it and emitted
|
|
398
|
+
// `onTokensChanged(null)`, which drops the graph via `reconcileAuth`. Log at
|
|
399
|
+
// debug and leave the dialog error-free — a signed-out device with zero
|
|
400
|
+
// accounts is a normal state, not a warning. Any other error (network, 5xx,
|
|
401
|
+
// malformed) IS unexpected: surface it and warn while keeping the prior graph
|
|
402
|
+
// so device rows still render.
|
|
403
|
+
if (extractErrorStatus(error) === 401) {
|
|
404
|
+
logger.debug('[AccountDialogController] listAccounts unauthorized (signed out)', { component: 'AccountDialogController' }, error);
|
|
405
|
+
} else {
|
|
406
|
+
this.error = errorMessage(error);
|
|
407
|
+
logger.warn('[AccountDialogController] listAccounts failed', { component: 'AccountDialogController' }, error);
|
|
408
|
+
}
|
|
399
409
|
}
|
|
400
410
|
if (seq !== this.refreshSeq) return; // superseded by a newer refresh
|
|
401
411
|
|
|
@@ -432,9 +442,15 @@ export class AccountDialogController {
|
|
|
432
442
|
try {
|
|
433
443
|
profiles = await this.oxyServices.getUsersByIds(ids);
|
|
434
444
|
} catch (error) {
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
445
|
+
// A 401 is the EXPECTED signed-out edge (stale/cleared bearer) — log at debug.
|
|
446
|
+
// `getUsersByIds` already swallows per-chunk failures and returns `[]`, so any
|
|
447
|
+
// OTHER error here is an unexpected total failure worth a warn. Either way keep
|
|
448
|
+
// the prior profile map.
|
|
449
|
+
if (extractErrorStatus(error) === 401) {
|
|
450
|
+
logger.debug('[AccountDialogController] getUsersByIds unauthorized (signed out)', { component: 'AccountDialogController' }, error);
|
|
451
|
+
} else {
|
|
452
|
+
logger.warn('[AccountDialogController] getUsersByIds failed', { component: 'AccountDialogController' }, error);
|
|
453
|
+
}
|
|
438
454
|
return;
|
|
439
455
|
}
|
|
440
456
|
if (seq !== this.refreshSeq) return; // superseded
|
|
@@ -124,8 +124,16 @@ describe('normalizeMultilineText', () => {
|
|
|
124
124
|
);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
it('
|
|
128
|
-
expect(normalizeMultilineText('a\n b')).toBe('a\
|
|
127
|
+
it('strips leading horizontal whitespace from every line', () => {
|
|
128
|
+
expect(normalizeMultilineText('a\n b')).toBe('a\nb');
|
|
129
|
+
expect(normalizeMultilineText('one\n\ttwo\n three')).toBe('one\ntwo\nthree');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('leaves no indent residue after a space-filled blank line', () => {
|
|
133
|
+
// The blank line collapses AND the six-space indent goes away entirely —
|
|
134
|
+
// collapsing it to a single space would leave a visible artifact under
|
|
135
|
+
// `white-space: pre-wrap`.
|
|
136
|
+
expect(normalizeMultilineText('Hola\n \n\n Mundo')).toBe('Hola\n\nMundo');
|
|
129
137
|
});
|
|
130
138
|
|
|
131
139
|
it('normalizes CRLF and lone CR to \\n', () => {
|
|
@@ -176,6 +184,7 @@ describe('normalizeMultilineText', () => {
|
|
|
176
184
|
'First paragraph.\n\nSecond paragraph.',
|
|
177
185
|
`a${NBSP}b`,
|
|
178
186
|
'a\n b',
|
|
187
|
+
'Hola\n \n\n Mundo',
|
|
179
188
|
'',
|
|
180
189
|
' ',
|
|
181
190
|
`Caf${DECOMPOSED_E_ACUTE}`,
|
|
@@ -61,12 +61,11 @@ const INLINE_NEEDS_NORMALIZATION = /[^\x20-\x7E]|^ | $| {2}/;
|
|
|
61
61
|
/**
|
|
62
62
|
* Same idea as {@link INLINE_NEEDS_NORMALIZATION}, for MULTILINE values: `\n`
|
|
63
63
|
* joins the printable-ASCII fast-path alphabet, and the additional shapes a
|
|
64
|
-
* normalized body can never contain are a space
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* the author's, and normalization deliberately preserves it.
|
|
64
|
+
* normalized body can never contain are a space adjacent to a line break — on
|
|
65
|
+
* either side, since every line is trimmed — and a run of three line breaks
|
|
66
|
+
* (more than one blank line).
|
|
68
67
|
*/
|
|
69
|
-
const MULTILINE_NEEDS_NORMALIZATION = /[^\x20-\x7E\n]|^[ \n]|[ \n]$| {2}| \n|\n{3}/;
|
|
68
|
+
const MULTILINE_NEEDS_NORMALIZATION = /[^\x20-\x7E\n]|^[ \n]|[ \n]$| {2}| \n|\n |\n{3}/;
|
|
70
69
|
|
|
71
70
|
/** Any run of whitespace, including tabs, line breaks and Unicode spaces. */
|
|
72
71
|
const ANY_WHITESPACE_RUN = /\s+/g;
|
|
@@ -92,9 +91,20 @@ const LINE_BREAK_FORMS = /\r\n|\r|\p{Zl}|\p{Zp}/gu;
|
|
|
92
91
|
*/
|
|
93
92
|
const HORIZONTAL_WHITESPACE_RUN = /[^\S\n]+/g;
|
|
94
93
|
|
|
95
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Horizontal whitespace at the END of a line — the blank-line spoiler: it is
|
|
96
|
+
* what makes an "empty" line non-empty and hides it from {@link EXCESS_BLANK_LINES}.
|
|
97
|
+
*/
|
|
96
98
|
const TRAILING_HORIZONTAL_WHITESPACE = / +\n/g;
|
|
97
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Horizontal whitespace at the START of a line: source-markup indentation. HTML
|
|
102
|
+
* collapses it by spec, so it is invisible where the text came from and carries
|
|
103
|
+
* no meaning — it only becomes visible once a client renders the value with
|
|
104
|
+
* `white-space: pre-wrap`.
|
|
105
|
+
*/
|
|
106
|
+
const LEADING_HORIZONTAL_WHITESPACE = /\n +/g;
|
|
107
|
+
|
|
98
108
|
/** Three or more line breaks: more than one blank line between paragraphs. */
|
|
99
109
|
const EXCESS_BLANK_LINES = /\n{3,}/g;
|
|
100
110
|
|
|
@@ -140,20 +150,23 @@ export function normalizeInlineText(value: string): string {
|
|
|
140
150
|
* 2. Unify every line-break form (CRLF, lone CR, U+2028, U+2029) to `\n`.
|
|
141
151
|
* 3. Collapse runs of HORIZONTAL whitespace (spaces, tabs, NBSP and friends)
|
|
142
152
|
* to a single space. Line breaks are untouched.
|
|
143
|
-
* 4. Strip the horizontal whitespace at
|
|
153
|
+
* 4. Strip the horizontal whitespace at BOTH ends of every line.
|
|
144
154
|
* 5. Collapse three or more line breaks to exactly one blank line (`\n\n`).
|
|
145
|
-
* 6. Trim both ends.
|
|
155
|
+
* 6. Trim both ends of the value.
|
|
146
156
|
*
|
|
147
157
|
* STEP 4 MUST PRECEDE STEP 5 — this is the whole point of the function. A
|
|
148
158
|
* "blank" line that actually contains spaces (`"a\n \n \nb"`) breaks the
|
|
149
159
|
* run of `\n` characters, so a bare `\n{3,}` collapse (step 5 alone) never sees
|
|
150
160
|
* it and the extra blank lines survive into the UI. That is exactly the bug in
|
|
151
|
-
* federated post bodies.
|
|
152
|
-
*
|
|
161
|
+
* federated post bodies. Trimming each line first turns those lines into real,
|
|
162
|
+
* empty lines, which step 5 then collapses.
|
|
153
163
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
164
|
+
* Every line is trimmed on BOTH sides, so a leading indent is removed outright
|
|
165
|
+
* rather than reduced to one space. Step 3 has already destroyed whatever indent
|
|
166
|
+
* the author wrote (`" Mundo"` → `" Mundo"`), so a surviving space would not
|
|
167
|
+
* be the author's intent — it would be an arbitrary remnant of exactly the
|
|
168
|
+
* source-markup indentation this function exists to erase, and `pre-wrap` renders
|
|
169
|
+
* it. Indentation is invisible in HTML by spec; it must be invisible here too.
|
|
157
170
|
*
|
|
158
171
|
* A value that is empty or whitespace-only returns `''`.
|
|
159
172
|
*
|
|
@@ -168,6 +181,7 @@ export function normalizeMultilineText(value: string): string {
|
|
|
168
181
|
.replace(LINE_BREAK_FORMS, '\n')
|
|
169
182
|
.replace(HORIZONTAL_WHITESPACE_RUN, ' ')
|
|
170
183
|
.replace(TRAILING_HORIZONTAL_WHITESPACE, '\n')
|
|
184
|
+
.replace(LEADING_HORIZONTAL_WHITESPACE, '\n')
|
|
171
185
|
.replace(EXCESS_BLANK_LINES, '\n\n')
|
|
172
186
|
.trim();
|
|
173
187
|
}
|