@oxy.so/core 1.9.0 → 1.10.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/README.md +4 -4
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +112 -163
- package/dist/cjs/OxyServices.base.js +4 -2
- package/dist/cjs/boot/sessionColdBoot.js +3 -0
- package/dist/cjs/mixins/OxyServices.user.js +2 -5
- package/dist/cjs/server/cors.js +0 -1
- package/dist/cjs/session/refresh.js +71 -7
- package/dist/cjs/utils/errorUtils.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +112 -163
- package/dist/esm/OxyServices.base.js +4 -2
- package/dist/esm/boot/sessionColdBoot.js +3 -0
- package/dist/esm/mixins/OxyServices.user.js +2 -5
- package/dist/esm/server/cors.js +0 -1
- package/dist/esm/session/refresh.js +70 -6
- package/dist/esm/utils/errorUtils.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +59 -7
- package/dist/types/OxyServices.base.d.ts +3 -1
- package/dist/types/mixins/OxyServices.civic.d.ts +11 -1
- package/dist/types/mixins/OxyServices.user.d.ts +1 -4
- package/dist/types/session/refresh.d.ts +21 -0
- package/dist/types/utils/errorUtils.d.ts +1 -1
- package/package.json +1 -1
- package/src/HttpService.ts +116 -187
- package/src/OxyServices.base.ts +4 -2
- package/src/__tests__/httpServiceFormEncoded.test.ts +2 -5
- package/src/__tests__/{httpServiceCsrf.test.ts → httpServiceWrites.test.ts} +49 -109
- package/src/__tests__/inSessionRefresh.test.ts +53 -0
- package/src/__tests__/linkedClient.test.ts +0 -7
- package/src/__tests__/nearExpiryMintStorm.test.ts +165 -0
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +1 -1
- package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +1 -1
- package/src/boot/__tests__/sessionColdBoot.test.ts +1 -1
- package/src/boot/sessionColdBoot.ts +3 -0
- package/src/mixins/OxyServices.user.ts +2 -5
- package/src/mixins/__tests__/backgroundCredential.httpIntegration.test.ts +5 -10
- package/src/mixins/__tests__/discoveryErrorHandling.test.ts +3 -4
- package/src/mixins/__tests__/topics.test.ts +2 -3
- package/src/server/cors.ts +0 -1
- package/src/session/__tests__/refresh.identityPin.test.ts +1 -1
- package/src/session/__tests__/refresh.test.ts +115 -2
- package/src/session/refresh.ts +77 -6
- package/src/utils/errorUtils.ts +1 -1
|
@@ -82,7 +82,7 @@ function makeOxy(overrides: OxyOverrides = {}): { oxy: OxyServices; setTokens: j
|
|
|
82
82
|
overrides.requestChallenge
|
|
83
83
|
?? (async () => ({ challenge: 'chal-1', expiresAt: '2030-01-01T00:00:00.000Z' })),
|
|
84
84
|
verifyChallenge: overrides.verifyChallenge ?? (async () => IDENTITY_SESSION),
|
|
85
|
-
httpService: { runSingleFlightDeviceSecretMint: makeMintSingleFlight() },
|
|
85
|
+
httpService: { runSingleFlightDeviceSecretMint: makeMintSingleFlight(), getSessionEpoch: () => 0 },
|
|
86
86
|
} as unknown as OxyServices;
|
|
87
87
|
return { oxy, setTokens };
|
|
88
88
|
}
|
|
@@ -59,8 +59,18 @@ interface RefreshMockOverrides {
|
|
|
59
59
|
|
|
60
60
|
function makeOxy(
|
|
61
61
|
overrides: RefreshMockOverrides = {},
|
|
62
|
-
): {
|
|
62
|
+
): {
|
|
63
|
+
oxy: OxyServices;
|
|
64
|
+
setTokens: jest.Mock;
|
|
65
|
+
noteRefreshRateLimited: jest.Mock;
|
|
66
|
+
/** `HttpService`'s session epoch; bump it to simulate a sign-out mid-refresh. */
|
|
67
|
+
epoch: { current: number };
|
|
68
|
+
/** `HttpService.hasSessionEnded()`: a sign-out with no token planted since. */
|
|
69
|
+
ended: { current: boolean };
|
|
70
|
+
} {
|
|
63
71
|
const setTokens = jest.fn();
|
|
72
|
+
const epoch = { current: 0 };
|
|
73
|
+
const ended = { current: false };
|
|
64
74
|
const noteRefreshRateLimited = jest.fn();
|
|
65
75
|
const oxy = {
|
|
66
76
|
setTokens,
|
|
@@ -71,9 +81,11 @@ function makeOxy(
|
|
|
71
81
|
httpService: {
|
|
72
82
|
runSingleFlightDeviceSecretMint: makeMintSingleFlight(),
|
|
73
83
|
noteRefreshRateLimited,
|
|
84
|
+
getSessionEpoch: () => epoch.current,
|
|
85
|
+
hasSessionEnded: () => ended.current,
|
|
74
86
|
},
|
|
75
87
|
} as unknown as OxyServices;
|
|
76
|
-
return { oxy, setTokens, noteRefreshRateLimited };
|
|
88
|
+
return { oxy, setTokens, noteRefreshRateLimited, epoch, ended };
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
describe('refreshPersistedSession — arm 1 (device-secret mint)', () => {
|
|
@@ -297,6 +309,107 @@ describe('refreshPersistedSession — arm 2 (native shared-key fallback)', () =>
|
|
|
297
309
|
});
|
|
298
310
|
});
|
|
299
311
|
|
|
312
|
+
describe('refreshPersistedSession — a sign-out mid-refresh', () => {
|
|
313
|
+
it('arm 1: plants nothing and leaves a cleared store cleared', async () => {
|
|
314
|
+
const store = createMemoryAuthStateStore();
|
|
315
|
+
await store.save(STORED);
|
|
316
|
+
let respond!: () => void;
|
|
317
|
+
const { oxy, setTokens, epoch } = makeOxy({
|
|
318
|
+
mintFromDeviceSecret: () =>
|
|
319
|
+
new Promise((resolve) => {
|
|
320
|
+
respond = () => resolve(MINT);
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const pending = refreshPersistedSession({ oxy, store, allowSharedKeyFallback: true });
|
|
325
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
326
|
+
// The sign-out: the credential is dropped and the session epoch moves.
|
|
327
|
+
await store.clear();
|
|
328
|
+
epoch.current += 1;
|
|
329
|
+
respond();
|
|
330
|
+
|
|
331
|
+
await expect(pending).resolves.toBeNull();
|
|
332
|
+
expect(setTokens).not.toHaveBeenCalled();
|
|
333
|
+
expect(await store.load()).toBeNull();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('arm 1: still persists the rotated secret when the store kept the credential', async () => {
|
|
337
|
+
const store = createMemoryAuthStateStore();
|
|
338
|
+
await store.save(STORED);
|
|
339
|
+
let respond!: () => void;
|
|
340
|
+
const { oxy, setTokens, epoch } = makeOxy({
|
|
341
|
+
mintFromDeviceSecret: () =>
|
|
342
|
+
new Promise((resolve) => {
|
|
343
|
+
respond = () => resolve(MINT);
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const pending = refreshPersistedSession({ oxy, store, allowSharedKeyFallback: false });
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
349
|
+
// A local teardown that keeps the store (so a reload can restore): the
|
|
350
|
+
// server rotated the secret, and the store must not be left holding the old one.
|
|
351
|
+
epoch.current += 1;
|
|
352
|
+
respond();
|
|
353
|
+
|
|
354
|
+
await expect(pending).resolves.toBeNull();
|
|
355
|
+
expect(setTokens).not.toHaveBeenCalled();
|
|
356
|
+
expect((await store.load())?.deviceSecret).toBe('ds-next-secret');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('arm 2: the shared keychain does not sign the user back in', async () => {
|
|
360
|
+
const store = createMemoryAuthStateStore();
|
|
361
|
+
let respond!: () => void;
|
|
362
|
+
const signInWithSharedIdentity = jest.fn(
|
|
363
|
+
(_opts?: { plantTokens?: boolean }) =>
|
|
364
|
+
new Promise<SessionLoginResponse>((resolve) => {
|
|
365
|
+
respond = () =>
|
|
366
|
+
resolve({
|
|
367
|
+
sessionId: 'sess-shared',
|
|
368
|
+
deviceId: 'dev-1',
|
|
369
|
+
deviceSecret: 'ds-shared',
|
|
370
|
+
expiresAt: '2030-01-01T00:00:00.000Z',
|
|
371
|
+
user: { id: 'user-1', username: 'u', name: {}, avatar: undefined },
|
|
372
|
+
accessToken: 'access-shared',
|
|
373
|
+
});
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
const { oxy, setTokens, epoch } = makeOxy({ signInWithSharedIdentity });
|
|
377
|
+
|
|
378
|
+
const pending = refreshPersistedSession({ oxy, store, allowSharedKeyFallback: true });
|
|
379
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
380
|
+
epoch.current += 1;
|
|
381
|
+
respond();
|
|
382
|
+
|
|
383
|
+
await expect(pending).resolves.toBeNull();
|
|
384
|
+
expect(signInWithSharedIdentity).toHaveBeenCalledWith({ plantTokens: false });
|
|
385
|
+
expect(setTokens).not.toHaveBeenCalled();
|
|
386
|
+
expect(await store.load()).toBeNull();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('refreshPersistedSession — after a sign-out', () => {
|
|
391
|
+
it('does not re-sign-in with the shared keychain', async () => {
|
|
392
|
+
const store = createMemoryAuthStateStore();
|
|
393
|
+
const signInWithSharedIdentity = jest.fn(async () => null);
|
|
394
|
+
const { oxy, setTokens, ended } = makeOxy({ signInWithSharedIdentity });
|
|
395
|
+
ended.current = true;
|
|
396
|
+
|
|
397
|
+
await expect(refreshPersistedSession({ oxy, store, allowSharedKeyFallback: true })).resolves.toBeNull();
|
|
398
|
+
expect(signInWithSharedIdentity).not.toHaveBeenCalled();
|
|
399
|
+
expect(setTokens).not.toHaveBeenCalled();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('still mints from a device credential the store holds (a sign-in made in another tab)', async () => {
|
|
403
|
+
const store = createMemoryAuthStateStore();
|
|
404
|
+
await store.save(STORED);
|
|
405
|
+
const { oxy, setTokens, ended } = makeOxy();
|
|
406
|
+
ended.current = true;
|
|
407
|
+
|
|
408
|
+
await expect(refreshPersistedSession({ oxy, store, allowSharedKeyFallback: true })).resolves.toBe('access-new');
|
|
409
|
+
expect(setTokens).toHaveBeenCalledWith('access-new');
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
300
413
|
describe('refreshPersistedSession — single-flight (no double-rotation)', () => {
|
|
301
414
|
it('coalesces two concurrent mints into ONE server rotation; the store holds the final current secret', async () => {
|
|
302
415
|
// The server rotates the secret on every mint. Two concurrent lanes must
|
package/src/session/refresh.ts
CHANGED
|
@@ -63,6 +63,21 @@ const MIN_SCHEDULE_DELAY_MS = 1_000;
|
|
|
63
63
|
const MIN_FAILURE_BACKOFF_MS = 5_000;
|
|
64
64
|
const MAX_FAILURE_BACKOFF_MS = 5 * 60_000;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* How long past `exp` to re-mint when a successful refresh handed back a token
|
|
68
|
+
* that is STILL inside the lead window.
|
|
69
|
+
*
|
|
70
|
+
* The device mint returns the session's stored access token until that token
|
|
71
|
+
* has actually expired, so asking again before `exp` only returns the same
|
|
72
|
+
* token. Re-arming from expiry in that state computed a negative delay, hit the
|
|
73
|
+
* {@link MIN_SCHEDULE_DELAY_MS} floor and re-minted once a second for the last
|
|
74
|
+
* minute of every token — 30+ mints, the server's whole per-minute budget, and
|
|
75
|
+
* the 429 that followed straddled the real expiry. Waiting until just past
|
|
76
|
+
* `exp` asks exactly once more, when the server will rotate. The margin covers
|
|
77
|
+
* the server's strict `exp < now` comparison and a little clock skew.
|
|
78
|
+
*/
|
|
79
|
+
export const REMINT_AFTER_EXPIRY_MS = 2_000;
|
|
80
|
+
|
|
66
81
|
export interface RefreshDeps {
|
|
67
82
|
oxy: OxyServices;
|
|
68
83
|
store: AuthStateStore;
|
|
@@ -99,6 +114,11 @@ export interface RefreshDeps {
|
|
|
99
114
|
* The device secret is FINE — it is the identity binding that went stale, so
|
|
100
115
|
* the caller must re-establish from the local key, never drop the credential.
|
|
101
116
|
* - `transient` — network / 5xx; keep the secret, a later attempt can succeed.
|
|
117
|
+
* - `session-ended` — the mint succeeded, but the local session was ended
|
|
118
|
+
* (`HttpService.endSession`, i.e. a sign-out) while it was in flight. Nothing
|
|
119
|
+
* is planted. The rotated secret is persisted only if the store still holds
|
|
120
|
+
* the credential that was presented — a store the sign-out cleared stays
|
|
121
|
+
* clear.
|
|
102
122
|
* - `persist-failed` — the mint succeeded but `nextDeviceSecret` could NOT be
|
|
103
123
|
* durably persisted. The token is deliberately NOT planted: advertising a
|
|
104
124
|
* healthy session on a secret that will not survive a reload is exactly the
|
|
@@ -111,6 +131,7 @@ export type DeviceSecretMintOutcome =
|
|
|
111
131
|
| { status: 'no-session' }
|
|
112
132
|
| { status: 'account-not-on-device' }
|
|
113
133
|
| { status: 'transient' }
|
|
134
|
+
| { status: 'session-ended' }
|
|
114
135
|
| { status: 'persist-failed' };
|
|
115
136
|
|
|
116
137
|
/**
|
|
@@ -144,6 +165,7 @@ export async function refreshDeviceSecretArm(deps: {
|
|
|
144
165
|
const { oxy, store } = deps;
|
|
145
166
|
const pin = deps.pin ?? null;
|
|
146
167
|
return oxy.httpService.runSingleFlightDeviceSecretMint(async () => {
|
|
168
|
+
const epoch = oxy.httpService.getSessionEpoch();
|
|
147
169
|
const persisted = await store.load();
|
|
148
170
|
if (!persisted?.deviceId || !persisted?.deviceSecret) {
|
|
149
171
|
return { status: 'no-secret' };
|
|
@@ -207,12 +229,26 @@ export async function refreshDeviceSecretArm(deps: {
|
|
|
207
229
|
expiresAt: mint.expiresAt,
|
|
208
230
|
...(bound ? { sessionId: bound.sessionId, userId: bound.accountId } : {}),
|
|
209
231
|
};
|
|
232
|
+
if (oxy.httpService.getSessionEpoch() !== epoch) {
|
|
233
|
+
// Signed out while minting. The server has rotated the secret, so a store
|
|
234
|
+
// that still holds the presented one must learn the rotation (the
|
|
235
|
+
// token-null lane keeps the store so a reload can restore); a store the
|
|
236
|
+
// sign-out cleared must NOT be refilled. Either way, plant nothing.
|
|
237
|
+
const current = await store.load();
|
|
238
|
+
if (current?.deviceId === persisted.deviceId && current.deviceSecret === persisted.deviceSecret) {
|
|
239
|
+
await store.save(next);
|
|
240
|
+
}
|
|
241
|
+
return { status: 'session-ended' };
|
|
242
|
+
}
|
|
210
243
|
// Persist nextDeviceSecret (read-back-verified) BEFORE planting the token.
|
|
211
244
|
// A failed durable persist must NOT plant.
|
|
212
245
|
const persistedOk = await store.save(next);
|
|
213
246
|
if (!persistedOk) {
|
|
214
247
|
return { status: 'persist-failed' };
|
|
215
248
|
}
|
|
249
|
+
if (oxy.httpService.getSessionEpoch() !== epoch) {
|
|
250
|
+
return { status: 'session-ended' };
|
|
251
|
+
}
|
|
216
252
|
oxy.setTokens(mint.accessToken);
|
|
217
253
|
return {
|
|
218
254
|
status: 'ok',
|
|
@@ -251,6 +287,7 @@ export async function refreshPersistedSession(deps: RefreshDeps): Promise<string
|
|
|
251
287
|
const identity = deps.identity ?? null;
|
|
252
288
|
// The shared keychain is never an identity-bound client's recovery path.
|
|
253
289
|
const allowSharedKeyFallback = identity ? false : (deps.allowSharedKeyFallback ?? isNative());
|
|
290
|
+
const epoch = oxy.httpService.getSessionEpoch();
|
|
254
291
|
// Resolved per call: a re-established identity session can move the pin, and a
|
|
255
292
|
// replaced/removed local key clears it (in which case arm 1 must NOT mint —
|
|
256
293
|
// an unpinned mint would adopt whatever account the device switched to).
|
|
@@ -263,6 +300,8 @@ export async function refreshPersistedSession(deps: RefreshDeps): Promise<string
|
|
|
263
300
|
switch (arm1.status) {
|
|
264
301
|
case 'ok':
|
|
265
302
|
return arm1.token;
|
|
303
|
+
case 'session-ended':
|
|
304
|
+
return null;
|
|
266
305
|
case 'transient':
|
|
267
306
|
logger.debug(
|
|
268
307
|
'Persisted deviceSecret mint failed (transient) — keeping store',
|
|
@@ -312,10 +351,18 @@ export async function refreshPersistedSession(deps: RefreshDeps): Promise<string
|
|
|
312
351
|
return recoverIdentitySession(oxy, store, identity);
|
|
313
352
|
}
|
|
314
353
|
|
|
315
|
-
|
|
354
|
+
// Never after a sign-out: the shared keychain holds an identity KEY, not a
|
|
355
|
+
// session, and using it here would sign the user straight back in.
|
|
356
|
+
if (allowSharedKeyFallback && !oxy.httpService.hasSessionEnded()) {
|
|
316
357
|
try {
|
|
317
|
-
|
|
358
|
+
// Planted here, not by the sign-in: a sign-out that lands while the
|
|
359
|
+
// challenge round-trips must win, or the shared keychain signs the user
|
|
360
|
+
// straight back in.
|
|
361
|
+
const session = await oxy.signInWithSharedIdentity({ plantTokens: false });
|
|
318
362
|
if (session?.accessToken) {
|
|
363
|
+
if (oxy.httpService.getSessionEpoch() !== epoch) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
319
366
|
// Repopulate the fast device-secret lane from the shared-key re-mint.
|
|
320
367
|
if (session.deviceId && session.deviceSecret) {
|
|
321
368
|
await store.save({
|
|
@@ -327,6 +374,10 @@ export async function refreshPersistedSession(deps: RefreshDeps): Promise<string
|
|
|
327
374
|
expiresAt: session.expiresAt,
|
|
328
375
|
});
|
|
329
376
|
}
|
|
377
|
+
if (oxy.httpService.getSessionEpoch() !== epoch) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
oxy.setTokens(session.accessToken);
|
|
330
381
|
return session.accessToken;
|
|
331
382
|
}
|
|
332
383
|
} catch (error) {
|
|
@@ -447,13 +498,27 @@ export function startTokenRefreshScheduler(oxy: OxyServices): TokenRefreshSchedu
|
|
|
447
498
|
if (expSeconds === null) {
|
|
448
499
|
return;
|
|
449
500
|
}
|
|
450
|
-
|
|
501
|
+
const untilExpiryMs = expSeconds * 1000 - Date.now();
|
|
502
|
+
// The server already answered this token's refresh with this very token:
|
|
503
|
+
// asking again before `exp` only gets it back again. Ask just after expiry,
|
|
504
|
+
// when the server rotates (see REMINT_AFTER_EXPIRY_MS).
|
|
505
|
+
if (untilExpiryMs > 0 && oxy.httpService.isAwaitingCurrentTokenExpiry?.()) {
|
|
506
|
+
armTimer(untilExpiryMs + REMINT_AFTER_EXPIRY_MS);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
armTimer(untilExpiryMs - TOKEN_REFRESH_LEAD_MS);
|
|
451
510
|
};
|
|
452
511
|
|
|
453
512
|
const runRefresh = (): void => {
|
|
454
513
|
// Clear any pending timer up front so an out-of-band trigger (focus) plus
|
|
455
514
|
// a fired timer can never double-run.
|
|
456
515
|
clearTimer();
|
|
516
|
+
// Another lane (a request-time preflight) already asked, and the server
|
|
517
|
+
// answered with this same token: re-arm for just past expiry instead.
|
|
518
|
+
if (oxy.httpService.isAwaitingCurrentTokenExpiry?.()) {
|
|
519
|
+
scheduleFromExpiry();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
457
522
|
void oxy.httpService.refreshAccessToken('preflight')
|
|
458
523
|
.then((token) => Boolean(token))
|
|
459
524
|
.catch(() => false)
|
|
@@ -498,10 +563,16 @@ export function startTokenRefreshScheduler(oxy: OxyServices): TokenRefreshSchedu
|
|
|
498
563
|
}
|
|
499
564
|
};
|
|
500
565
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
566
|
+
// Re-arm only on a token that actually CHANGED. The mint plants the token it
|
|
567
|
+
// returns even when that is the token already held, and treating the repeat
|
|
568
|
+
// as new re-armed from an in-lead-window expiry, i.e. at the 1s floor.
|
|
569
|
+
let lastSeenToken = oxy.getAccessToken();
|
|
570
|
+
const unsubscribeTokens = oxy.onTokensChanged((token) => {
|
|
571
|
+
if (disposed || token === lastSeenToken) {
|
|
572
|
+
return;
|
|
504
573
|
}
|
|
574
|
+
lastSeenToken = token;
|
|
575
|
+
schedule();
|
|
505
576
|
});
|
|
506
577
|
|
|
507
578
|
let removeFocusListener: (() => void) | null = null;
|
package/src/utils/errorUtils.ts
CHANGED
|
@@ -121,7 +121,7 @@ const nonEmptyString = (value: unknown): string | undefined =>
|
|
|
121
121
|
* - `{ error: '<CODE>', error_description }` — RFC 6749 §5.2 / RFC 6750 §3, the
|
|
122
122
|
* OAuth token and userinfo endpoints. `error_description` is the human text
|
|
123
123
|
* and `error` is the machine code, so both survive.
|
|
124
|
-
* - `{ message, code }` — e.g. the API's
|
|
124
|
+
* - `{ message, code }` — e.g. the API's middleware rejections.
|
|
125
125
|
* - `{ error: '<human message>' }` — legacy hand-rolled routes. With no sibling
|
|
126
126
|
* `message`/`error_description` the string is the message, not a code: a bare
|
|
127
127
|
* `error` string is not machine-readable enough to promote to `code`.
|