@oxyhq/services 18.1.0 → 19.0.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/lib/commonjs/ui/context/OxyContext.js +49 -93
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +15 -29
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/session/authStore.js +12 -50
- package/lib/commonjs/ui/session/authStore.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +6 -11
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
- package/lib/commonjs/ui/session/tokenTransport.js +3 -4
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +49 -93
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +15 -29
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/session/authStore.js +13 -51
- package/lib/module/ui/session/authStore.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +6 -11
- package/lib/module/ui/session/createSessionClient.js.map +1 -1
- package/lib/module/ui/session/tokenTransport.js +3 -4
- package/lib/module/ui/session/tokenTransport.js.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +15 -6
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +4 -5
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/authStore.d.ts +11 -15
- package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +6 -10
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +3 -4
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +15 -6
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +4 -5
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/authStore.d.ts +11 -15
- package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +6 -10
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +3 -4
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ui/context/OxyContext.tsx +57 -100
- package/src/ui/context/__tests__/OxyContext.coldBoot.test.tsx +13 -12
- package/src/ui/context/hooks/useAuthOperations.ts +18 -27
- package/src/ui/session/__tests__/createSessionClient.test.ts +3 -1
- package/src/ui/session/__tests__/tokenTransport.test.ts +6 -5
- package/src/ui/session/authStore.ts +12 -55
- package/src/ui/session/createSessionClient.ts +5 -10
- package/src/ui/session/tokenTransport.ts +3 -4
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* `OxyContextProvider` cold-boot opt-out (IdP mode).
|
|
3
3
|
*
|
|
4
4
|
* The provider is the ecosystem's device-first session authority by default:
|
|
5
|
-
* it runs `runSessionColdBoot` on mount
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* it runs `runSessionColdBoot` on mount. The IdP host (`auth.oxy.so`) is NOT a
|
|
6
|
+
* session authority, so it mounts the provider with `coldBoot={false}` — the
|
|
7
|
+
* cold boot must never run (so the bearer-authenticated device socket never
|
|
8
|
+
* starts), yet interactive sign-in must still commit a normal session on that
|
|
9
|
+
* origin.
|
|
10
10
|
*/
|
|
11
11
|
import { render, act, waitFor } from '@testing-library/react';
|
|
12
12
|
import type { ReactNode } from 'react';
|
|
@@ -105,7 +105,8 @@ describe('OxyContextProvider coldBoot opt-out (IdP mode)', () => {
|
|
|
105
105
|
// No boot spinner: private-API readiness settles to a definitive "no".
|
|
106
106
|
expect(getState()?.isPrivateApiPending).toBe(false);
|
|
107
107
|
expect(getState()?.canUsePrivateApi).toBe(false);
|
|
108
|
-
//
|
|
108
|
+
// IdP mode never runs the cold boot and never signs in, so the device
|
|
109
|
+
// socket is never started.
|
|
109
110
|
expect(lastSessionClient().start).not.toHaveBeenCalled();
|
|
110
111
|
});
|
|
111
112
|
|
|
@@ -116,7 +117,7 @@ describe('OxyContextProvider coldBoot opt-out (IdP mode)', () => {
|
|
|
116
117
|
deviceId: 'd-1',
|
|
117
118
|
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
|
|
118
119
|
accessToken: 'access-1',
|
|
119
|
-
|
|
120
|
+
deviceSecret: 'ds-1',
|
|
120
121
|
user: { id: 'u-1', username: 'alice' },
|
|
121
122
|
};
|
|
122
123
|
jest.spyOn(oxy, 'passwordSignIn').mockResolvedValue(loginResult);
|
|
@@ -144,11 +145,11 @@ describe('OxyContextProvider coldBoot opt-out (IdP mode)', () => {
|
|
|
144
145
|
expect(getState()?.isAuthenticated).toBe(true);
|
|
145
146
|
});
|
|
146
147
|
expect(getState()?.user?.id).toBe('u-1');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
148
|
+
// Zero-cookie cutover: no `deviceToken` is threaded into `passwordSignIn`.
|
|
149
|
+
expect(oxy.passwordSignIn).toHaveBeenCalledWith('alice', 'secret', {
|
|
150
|
+
deviceName: undefined,
|
|
151
|
+
deviceFingerprint: undefined,
|
|
152
|
+
});
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
it('default (coldBoot omitted): runs the device-first cold boot', async () => {
|
|
@@ -15,10 +15,9 @@ export interface UseAuthOperationsOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* The device-first persisted auth-state store. On EXPLICIT full sign-out the
|
|
17
17
|
* session blob is cleared (`store.clear()`) so a reload's cold boot finds no
|
|
18
|
-
*
|
|
19
|
-
* persisted so the next boot
|
|
20
|
-
*
|
|
21
|
-
* cleared here.
|
|
18
|
+
* device credential to restore; on sign-in the returned zero-cookie device
|
|
19
|
+
* credential (`deviceId` + `deviceSecret`) is persisted so the next boot
|
|
20
|
+
* warm-restores without a redirect.
|
|
22
21
|
*/
|
|
23
22
|
store: AuthStateStore;
|
|
24
23
|
activeSessionId: string | null;
|
|
@@ -61,7 +60,7 @@ const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR';
|
|
|
61
60
|
const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
|
|
62
61
|
|
|
63
62
|
/**
|
|
64
|
-
* Clear the persisted
|
|
63
|
+
* Clear the persisted device credential on an explicit full sign-out.
|
|
65
64
|
* Best-effort and non-blocking: sign-out must never fail because a storage
|
|
66
65
|
* write threw. Exported so `OxyContext`'s zero-account branch (a REMOTE full
|
|
67
66
|
* sign-out pushed over the socket) runs the EXACT same cleanup as the local
|
|
@@ -136,29 +135,21 @@ export const useAuthOperations = ({
|
|
|
136
135
|
deviceFingerprint,
|
|
137
136
|
);
|
|
138
137
|
|
|
139
|
-
// Persist the
|
|
138
|
+
// Persist the durable device credential so a reload warm-restores this
|
|
140
139
|
// session without a redirect. `verifyChallenge` plants the access token
|
|
141
|
-
// internally; when the
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// `
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
140
|
+
// internally; when the response also carries the zero-cookie device
|
|
141
|
+
// credential (`deviceId` + `deviceSecret`), persist the durable blob so the
|
|
142
|
+
// next cold boot re-mints an access token from it. Best-effort — a failed
|
|
143
|
+
// persist never fails the sign-in. `SessionLoginResponse` does not type
|
|
144
|
+
// `deviceSecret`; read it defensively from the runtime payload.
|
|
145
|
+
const deviceSecret = (sessionResponse as { deviceSecret?: string }).deviceSecret;
|
|
146
|
+
if (sessionResponse.deviceId && deviceSecret) {
|
|
148
147
|
try {
|
|
149
|
-
const deviceToken = (await store.loadDeviceToken()) ?? undefined;
|
|
150
|
-
// Phase 2c: mirror the refreshToken handling for the rotating
|
|
151
|
-
// deviceSecret — persist it with the response's deviceId so the next
|
|
152
|
-
// cold boot restores via the zero-cookie mint. `SessionLoginResponse`
|
|
153
|
-
// types neither; read the secret defensively from the runtime payload.
|
|
154
|
-
const deviceSecret = (sessionResponse as { deviceSecret?: string }).deviceSecret;
|
|
155
148
|
await store.save({
|
|
156
149
|
sessionId: sessionResponse.sessionId,
|
|
157
|
-
refreshToken,
|
|
158
150
|
userId: sessionResponse.user.id,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
...(deviceSecret ? { deviceSecret } : {}),
|
|
151
|
+
deviceId: sessionResponse.deviceId,
|
|
152
|
+
deviceSecret,
|
|
162
153
|
...(sessionResponse.accessToken ? { accessToken: sessionResponse.accessToken } : {}),
|
|
163
154
|
...(sessionResponse.expiresAt ? { expiresAt: sessionResponse.expiresAt } : {}),
|
|
164
155
|
});
|
|
@@ -309,7 +300,7 @@ export const useAuthOperations = ({
|
|
|
309
300
|
|
|
310
301
|
if (sessionToLogout === activeSessionId && remainingAccounts.length === 0) {
|
|
311
302
|
// Genuine FULL sign-out (no sessions remain): clear the persisted
|
|
312
|
-
//
|
|
303
|
+
// device credential so a reload's cold boot finds nothing to restore,
|
|
313
304
|
// then tear down local state.
|
|
314
305
|
clearPersistedAuthSafe(store, logger);
|
|
315
306
|
await clearSessionState();
|
|
@@ -363,9 +354,9 @@ export const useAuthOperations = ({
|
|
|
363
354
|
// `{ all: true }`) — replaces the bearer `logoutAllSessions` +
|
|
364
355
|
// web-cookie `logoutAllSessionsViaCookie` pair.
|
|
365
356
|
await sessionClient.signOut({ all: true });
|
|
366
|
-
// logoutAll is ALWAYS a full sign-out: clear the persisted
|
|
367
|
-
// so the next cold boot finds no session to restore, then tear
|
|
368
|
-
//
|
|
357
|
+
// logoutAll is ALWAYS a full sign-out: clear the persisted device
|
|
358
|
+
// credential so the next cold boot finds no session to restore, then tear
|
|
359
|
+
// down local state.
|
|
369
360
|
clearPersistedAuthSafe(store, logger);
|
|
370
361
|
await clearSessionState();
|
|
371
362
|
} catch (error) {
|
|
@@ -16,12 +16,14 @@ jest.mock('socket.io-client', () => ({ __esModule: true, io: (...args: unknown[]
|
|
|
16
16
|
|
|
17
17
|
import { createSessionClient } from '../createSessionClient';
|
|
18
18
|
|
|
19
|
+
// Sockets are bearer-only: `start()` opens a socket only when a bearer token is
|
|
20
|
+
// held. This mock reports one so the wiring test exercises the socket factory.
|
|
19
21
|
function fakeOxy() {
|
|
20
22
|
const listeners = new Set<(t: string | null) => void>();
|
|
21
23
|
return {
|
|
22
24
|
makeRequest: jest.fn().mockResolvedValue(undefined),
|
|
23
25
|
getBaseURL: jest.fn().mockReturnValue('https://api.oxy.so'),
|
|
24
|
-
getAccessToken: jest.fn().mockReturnValue(
|
|
26
|
+
getAccessToken: jest.fn().mockReturnValue('bearer.jwt.token'),
|
|
25
27
|
setTokens: jest.fn(),
|
|
26
28
|
onTokensChanged: jest.fn((l: (t: string | null) => void) => {
|
|
27
29
|
listeners.add(l);
|
|
@@ -2,11 +2,12 @@ import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
|
2
2
|
import { logger, createMemoryAuthStateStore, refreshPersistedSession } from '@oxyhq/core';
|
|
3
3
|
import { createTokenTransport } from '../tokenTransport';
|
|
4
4
|
|
|
5
|
-
// The device-first transport mints a fallback token
|
|
6
|
-
//
|
|
7
|
-
// there is no `silentSignIn`/`signInWithSharedIdentity` arm
|
|
8
|
-
// one core function so the transport's own coalescing /
|
|
9
|
-
// exercised in isolation; keep the real `logger` +
|
|
5
|
+
// The device-first transport mints a fallback token from the persisted
|
|
6
|
+
// zero-cookie device credential via `refreshPersistedSession` (the ONE unified
|
|
7
|
+
// refresh path) — there is no `silentSignIn`/`signInWithSharedIdentity` arm
|
|
8
|
+
// anymore. Mock that one core function so the transport's own coalescing /
|
|
9
|
+
// logging contract can be exercised in isolation; keep the real `logger` +
|
|
10
|
+
// memory store.
|
|
10
11
|
jest.mock('@oxyhq/core', () => {
|
|
11
12
|
const actual = jest.requireActual('@oxyhq/core');
|
|
12
13
|
return { __esModule: true, ...actual, refreshPersistedSession: jest.fn() };
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Platform `AuthStateStore` for `@oxyhq/services`.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The zero-cookie device model persists the durable device credential
|
|
5
|
+
* (`deviceId` + `deviceSecret`) per origin (web) / per device (native), and the
|
|
6
|
+
* SDK re-mints the access token from that credential on cold boot. `@oxyhq/core`
|
|
7
|
+
* owns the store shape + logic (`createWebAuthStateStore` /
|
|
8
|
+
* `createNativeAuthStateStore`); this module only supplies the platform storage
|
|
9
|
+
* seam:
|
|
9
10
|
*
|
|
10
11
|
* - web → `createWebAuthStateStore()` (localStorage, in-memory fallback).
|
|
11
|
-
* - native → `createNativeAuthStateStore(secureKV)` for the SESSION blob
|
|
12
|
-
*
|
|
13
|
-
* long-lived DEVICE token delegated to `KeyManager`'s shared keychain
|
|
14
|
-
* (`group.so.oxy.shared`) instead of per-app storage — so an in-app login on
|
|
15
|
-
* any native Oxy app attributes to the SAME server-side DeviceSession.
|
|
12
|
+
* - native → `createNativeAuthStateStore(secureKV)` for the SESSION blob over
|
|
13
|
+
* `expo-secure-store` (AsyncStorage fallback).
|
|
16
14
|
*
|
|
17
15
|
* `expo-secure-store` is loaded via a runtime-computed dynamic import (the same
|
|
18
16
|
* optional-native-module pattern `OxyProvider` uses for netinfo / keyboard
|
|
@@ -20,12 +18,10 @@
|
|
|
20
18
|
* back to AsyncStorage rather than crashing.
|
|
21
19
|
*/
|
|
22
20
|
import {
|
|
23
|
-
KeyManager,
|
|
24
21
|
createWebAuthStateStore,
|
|
25
22
|
createNativeAuthStateStore,
|
|
26
23
|
type AuthStateStore,
|
|
27
24
|
type NativeKeyValueStorage,
|
|
28
|
-
logger,
|
|
29
25
|
} from '@oxyhq/core';
|
|
30
26
|
import { createPlatformStorage, isReactNative } from '../utils/storageHelpers';
|
|
31
27
|
|
|
@@ -112,53 +108,14 @@ function createNativeSecureKeyValueStorage(): NativeKeyValueStorage {
|
|
|
112
108
|
/**
|
|
113
109
|
* Build the platform {@link AuthStateStore} for this runtime.
|
|
114
110
|
*
|
|
115
|
-
* Native
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* a locked/failed shared-keychain read degrades to "no device token" rather
|
|
119
|
-
* than throwing out of cold boot / refresh.
|
|
111
|
+
* Native persists the SESSION blob (`deviceId` + `deviceSecret` + cached access
|
|
112
|
+
* token) per-app in SecureStore; the SDK re-mints the access token from the
|
|
113
|
+
* device credential on the next cold boot.
|
|
120
114
|
*/
|
|
121
115
|
export function createPlatformAuthStateStore(): AuthStateStore {
|
|
122
116
|
if (!isReactNative()) {
|
|
123
117
|
return createWebAuthStateStore();
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
...base,
|
|
129
|
-
loadDeviceToken: async () => {
|
|
130
|
-
try {
|
|
131
|
-
return await KeyManager.getSharedDeviceToken();
|
|
132
|
-
} catch (error) {
|
|
133
|
-
logger.debug(
|
|
134
|
-
'Shared device-token read failed (treating as none)',
|
|
135
|
-
{ component: 'authStore', method: 'loadDeviceToken' },
|
|
136
|
-
error,
|
|
137
|
-
);
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
saveDeviceToken: async (token) => {
|
|
142
|
-
try {
|
|
143
|
-
await KeyManager.setSharedDeviceToken(token);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
logger.debug(
|
|
146
|
-
'Shared device-token write failed (non-fatal)',
|
|
147
|
-
{ component: 'authStore', method: 'saveDeviceToken' },
|
|
148
|
-
error,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
clearDeviceToken: async () => {
|
|
153
|
-
try {
|
|
154
|
-
await KeyManager.clearSharedDeviceToken();
|
|
155
|
-
} catch (error) {
|
|
156
|
-
logger.debug(
|
|
157
|
-
'Shared device-token clear failed (non-fatal)',
|
|
158
|
-
{ component: 'authStore', method: 'clearDeviceToken' },
|
|
159
|
-
error,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
};
|
|
120
|
+
return createNativeAuthStateStore(createNativeSecureKeyValueStorage());
|
|
164
121
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
createSessionClientHost,
|
|
5
5
|
type AuthStateStore,
|
|
6
6
|
type OxyServices,
|
|
7
|
-
type SessionClientOptions,
|
|
8
7
|
} from '@oxyhq/core';
|
|
9
8
|
import { createTokenTransport } from './tokenTransport';
|
|
10
9
|
|
|
@@ -20,31 +19,27 @@ import { createTokenTransport } from './tokenTransport';
|
|
|
20
19
|
* core's lazy dynamic import of a bare specifier — bundler-fragile in
|
|
21
20
|
* Metro/Expo-web against the published core dist.
|
|
22
21
|
* - the device-first {@link createTokenTransport}, which mints a fallback token
|
|
23
|
-
*
|
|
22
|
+
* from the persisted device credential (`store` → `deviceId` + `deviceSecret`).
|
|
24
23
|
*
|
|
25
24
|
* `onUnauthenticated` fires when an applied device state has zero accounts (a
|
|
26
25
|
* device signout-all): the provider clears the persisted store + local state so
|
|
27
26
|
* a reload does not try to restore a dead session. The host is returned
|
|
28
27
|
* alongside the client so the caller can call `host.setCurrentAccountId(...)`
|
|
29
28
|
* as the active account changes.
|
|
29
|
+
*
|
|
30
|
+
* The realtime socket is bearer-only: a signed-out tab cannot join any device
|
|
31
|
+
* room, so there is no signed-out socket wiring.
|
|
30
32
|
*/
|
|
31
33
|
export function createSessionClient(
|
|
32
34
|
oxyServices: OxyServices,
|
|
33
35
|
store: AuthStateStore,
|
|
34
36
|
onUnauthenticated?: () => void,
|
|
35
|
-
/**
|
|
36
|
-
* Optional signed-out realtime wiring: `signedOutSocketAuth` (open the socket
|
|
37
|
-
* while signed out — web returns `true` to ride the `oxy_device` cookie,
|
|
38
|
-
* native returns the shared device token) and `onSessionAppeared` (self-acquire
|
|
39
|
-
* when a sibling signs in on this device).
|
|
40
|
-
*/
|
|
41
|
-
extra?: Pick<SessionClientOptions, 'signedOutSocketAuth' | 'onSessionAppeared'>,
|
|
42
37
|
): {
|
|
43
38
|
client: SessionClient;
|
|
44
39
|
host: ReturnType<typeof createSessionClientHost>;
|
|
45
40
|
} {
|
|
46
41
|
const host = createSessionClientHost(oxyServices);
|
|
47
42
|
const transport = createTokenTransport(oxyServices, store);
|
|
48
|
-
const client = new SessionClient(host, { transport, socketFactory: io, onUnauthenticated
|
|
43
|
+
const client = new SessionClient(host, { transport, socketFactory: io, onUnauthenticated });
|
|
49
44
|
return { client, host };
|
|
50
45
|
}
|
|
@@ -13,10 +13,9 @@ import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
|
13
13
|
* `ensureActiveToken` is the fallback the client uses when a `session_state`
|
|
14
14
|
* push arrived WITHOUT an embedded `activeToken` and the app currently holds no
|
|
15
15
|
* bearer. It mints one through the ONE unified refresh path
|
|
16
|
-
* (`refreshPersistedSession`)
|
|
17
|
-
* (`
|
|
18
|
-
*
|
|
19
|
-
* persisted refresh token is the durable web credential.
|
|
16
|
+
* (`refreshPersistedSession`), which presents the persisted zero-cookie device
|
|
17
|
+
* credential (`deviceId` + `deviceSecret`) at `POST /session/device/token`. The
|
|
18
|
+
* persisted device credential is the sole durable restore credential.
|
|
20
19
|
*
|
|
21
20
|
* Concurrent pushes coalesce onto one in-flight mint. A failure is logged and
|
|
22
21
|
* swallowed: this method must never throw out (it runs inside the socket state
|