@lemoncloud/chatic-sockets-lib 0.3.4 → 0.4.1
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/client-socket-v2/auth-controller.d.ts +111 -0
- package/dist/client-socket-v2/auth-controller.js +344 -0
- package/dist/client-socket-v2/create-client-socket-v2.js +9 -2
- package/dist/client-socket-v2/gateways/auth-gateway.d.ts +4 -1
- package/dist/client-socket-v2/gateways/auth-gateway.js +3 -0
- package/dist/client-socket-v2/index.d.ts +1 -0
- package/dist/client-socket-v2/index.js +1 -0
- package/dist/client-socket-v2/plans/channel-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/plans/channel-sync-plan.js +1 -0
- package/dist/client-socket-v2/plans/chat-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/plans/chat-sync-plan.js +1 -0
- package/dist/client-socket-v2/plans/device-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/plans/device-sync-plan.js +1 -0
- package/dist/client-socket-v2/plans/join-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/plans/join-sync-plan.js +1 -0
- package/dist/client-socket-v2/plans/place-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/plans/place-sync-plan.js +1 -0
- package/dist/client-socket-v2/plans/profile-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/plans/profile-sync-plan.js +1 -0
- package/dist/client-socket-v2/sync-scheduler.d.ts +5 -0
- package/dist/client-socket-v2/sync-scheduler.js +51 -11
- package/dist/client-socket-v2/types.d.ts +4 -0
- package/dist/lib/auth/contracts.d.ts +65 -0
- package/dist/lib/auth/contracts.js +7 -0
- package/dist/lib/auth/types.d.ts +60 -7
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AuthUpdateState, AuthTokenView } from '../lib/auth/contracts';
|
|
2
|
+
import type { ClientSocketV2, SharedTimerScheduler } from './types';
|
|
3
|
+
/** Auth state surfaced to the app: server states plus the client-terminal 'expired'. */
|
|
4
|
+
export declare type AuthControllerState = AuthUpdateState | 'expired';
|
|
5
|
+
/** App-provided signing callback (stateless); the SDK injects the current token, the app returns the lemon hmac signature. */
|
|
6
|
+
export declare type AuthSignCallback = (token: string, ctx?: {
|
|
7
|
+
target?: string;
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
signature: string;
|
|
10
|
+
current: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** Registration input for `register()`. */
|
|
13
|
+
export interface AuthRegisterOptions {
|
|
14
|
+
token: string;
|
|
15
|
+
authId: string;
|
|
16
|
+
sign: AuthSignCallback;
|
|
17
|
+
}
|
|
18
|
+
/** Per-call switch hooks. */
|
|
19
|
+
export interface AuthSwitchHandlers {
|
|
20
|
+
onSuccess?(res: AuthTokenView): void;
|
|
21
|
+
onError?(err: AuthSwitchError): void;
|
|
22
|
+
}
|
|
23
|
+
/** Typed error capturing the switch failure phase. */
|
|
24
|
+
export declare class AuthSwitchError extends Error {
|
|
25
|
+
readonly phase: 'not-connected' | 'sign' | 'server';
|
|
26
|
+
readonly cause?: unknown;
|
|
27
|
+
readonly serverResponse?: AuthTokenView;
|
|
28
|
+
constructor(opts: {
|
|
29
|
+
phase: AuthSwitchError['phase'];
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
serverResponse?: AuthTokenView;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export interface AuthControllerOptionsPartial {
|
|
35
|
+
refreshIntervalMs?: number;
|
|
36
|
+
minBackoffMs?: number;
|
|
37
|
+
maxBackoffMs?: number;
|
|
38
|
+
backoffFactor?: number;
|
|
39
|
+
maxFailures?: number;
|
|
40
|
+
validatingTimeoutMs?: number;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
}
|
|
43
|
+
export interface AuthControllerOptions extends AuthControllerOptionsPartial {
|
|
44
|
+
client: ClientSocketV2;
|
|
45
|
+
timerScheduler?: SharedTimerScheduler;
|
|
46
|
+
}
|
|
47
|
+
export interface AuthController {
|
|
48
|
+
readonly state: AuthControllerState;
|
|
49
|
+
/** Current token, for the app's HTTP Authorization header. */
|
|
50
|
+
readonly token: string;
|
|
51
|
+
register(opts: AuthRegisterOptions): void;
|
|
52
|
+
/** Resolves once authenticated (immediately if already); rejects if auth expires. */
|
|
53
|
+
ready(): Promise<void>;
|
|
54
|
+
switch(target: string, handlers?: AuthSwitchHandlers): Promise<AuthTokenView>;
|
|
55
|
+
/** Clear local auth state and stop the scheduler. */
|
|
56
|
+
logout(): Promise<void>;
|
|
57
|
+
onAuthState(listener: (state: AuthControllerState) => void): () => void;
|
|
58
|
+
/** Fires on every refresh/switch success (including SDK periodic refresh) with the full payload. */
|
|
59
|
+
onTokenRefresh(listener: (view: AuthTokenView) => void): () => void;
|
|
60
|
+
}
|
|
61
|
+
export declare class AuthControllerImpl implements AuthController {
|
|
62
|
+
private readonly options;
|
|
63
|
+
private readonly refreshIntervalMs;
|
|
64
|
+
private readonly minBackoffMs;
|
|
65
|
+
private readonly maxBackoffMs;
|
|
66
|
+
private readonly backoffFactor;
|
|
67
|
+
private readonly maxFailures;
|
|
68
|
+
private readonly validatingTimeoutMs;
|
|
69
|
+
private readonly now;
|
|
70
|
+
private readonly timerScheduler?;
|
|
71
|
+
private readonly gateway;
|
|
72
|
+
private readonly unsubs;
|
|
73
|
+
private readonly stateListeners;
|
|
74
|
+
private readonly tokenListeners;
|
|
75
|
+
private _state;
|
|
76
|
+
private epoch;
|
|
77
|
+
private failures;
|
|
78
|
+
private _token;
|
|
79
|
+
private authId;
|
|
80
|
+
private signCallback?;
|
|
81
|
+
private active;
|
|
82
|
+
constructor(options: AuthControllerOptions);
|
|
83
|
+
get state(): AuthControllerState;
|
|
84
|
+
get token(): string;
|
|
85
|
+
/** Register token, authId, and sign callback. Idempotent; resumes auth when inactive (after logout or expiry). */
|
|
86
|
+
register: (opts: AuthRegisterOptions) => void;
|
|
87
|
+
/** Resolves once authenticated (immediately if already); rejects if auth expires. */
|
|
88
|
+
ready: () => Promise<void>;
|
|
89
|
+
/** Switch to another site. */
|
|
90
|
+
switch: (target: string, handlers?: AuthSwitchHandlers) => Promise<AuthTokenView>;
|
|
91
|
+
/** Clear local auth state and stop the scheduler. Best-effort server notify (revokes the backend session). */
|
|
92
|
+
logout: () => Promise<void>;
|
|
93
|
+
onAuthState: (listener: (state: AuthControllerState) => void) => (() => void);
|
|
94
|
+
onTokenRefresh: (listener: (view: AuthTokenView) => void) => (() => void);
|
|
95
|
+
private emitToken;
|
|
96
|
+
start: () => void;
|
|
97
|
+
stop: () => void;
|
|
98
|
+
destroy: () => void;
|
|
99
|
+
private sendUpdate;
|
|
100
|
+
private runRefresh;
|
|
101
|
+
private handleAuthResponse;
|
|
102
|
+
private handleFailed;
|
|
103
|
+
private runReauth;
|
|
104
|
+
private computeBackoffDelay;
|
|
105
|
+
private scheduleRefresh;
|
|
106
|
+
private rearmRefreshTimer;
|
|
107
|
+
private schedule;
|
|
108
|
+
private clearTimer;
|
|
109
|
+
private clearTimers;
|
|
110
|
+
private setState;
|
|
111
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AuthControllerImpl = exports.AuthSwitchError = void 0;
|
|
13
|
+
const auth_gateway_1 = require("./gateways/auth-gateway");
|
|
14
|
+
/** Typed error capturing the switch failure phase. */
|
|
15
|
+
class AuthSwitchError extends Error {
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
super(`auth.switch failed: ${opts.phase}`);
|
|
18
|
+
this.phase = opts.phase;
|
|
19
|
+
this.cause = opts.cause;
|
|
20
|
+
this.serverResponse = opts.serverResponse;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.AuthSwitchError = AuthSwitchError;
|
|
24
|
+
class AuthControllerImpl {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.unsubs = [];
|
|
29
|
+
this.stateListeners = new Set();
|
|
30
|
+
this.tokenListeners = new Set();
|
|
31
|
+
this._state = '';
|
|
32
|
+
this.epoch = 0;
|
|
33
|
+
this.failures = 0;
|
|
34
|
+
this._token = '';
|
|
35
|
+
this.authId = '';
|
|
36
|
+
this.active = false;
|
|
37
|
+
/** Register token, authId, and sign callback. Idempotent; resumes auth when inactive (after logout or expiry). */
|
|
38
|
+
this.register = (opts) => {
|
|
39
|
+
this._token = opts.token;
|
|
40
|
+
this.authId = opts.authId;
|
|
41
|
+
this.signCallback = opts.sign;
|
|
42
|
+
if (!this.active) {
|
|
43
|
+
this.failures = 0;
|
|
44
|
+
this.active = true;
|
|
45
|
+
this.setState('pending');
|
|
46
|
+
if (this.options.client.state === 'connected') {
|
|
47
|
+
this.epoch++;
|
|
48
|
+
void this.sendUpdate();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** Resolves once authenticated (immediately if already); rejects if auth expires. */
|
|
53
|
+
this.ready = () => {
|
|
54
|
+
if (this._state === 'authenticated')
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const off = this.onAuthState(s => {
|
|
58
|
+
if (s === 'authenticated') {
|
|
59
|
+
off();
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
else if (s === 'expired' || s === '') {
|
|
63
|
+
off();
|
|
64
|
+
reject(new Error(`401 UNAUTHORIZED - auth ${s === 'expired' ? 'expired' : 'logged out'}`));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
/** Switch to another site. */
|
|
70
|
+
this.switch = (target, handlers) => __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
var _h, _j, _k, _l, _m, _o, _p, _q;
|
|
72
|
+
if (this.options.client.state !== 'connected') {
|
|
73
|
+
const err = new AuthSwitchError({ phase: 'not-connected' });
|
|
74
|
+
(_h = handlers === null || handlers === void 0 ? void 0 : handlers.onError) === null || _h === void 0 ? void 0 : _h.call(handlers, err);
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
this.epoch++;
|
|
78
|
+
const myEpoch = this.epoch;
|
|
79
|
+
/** suspend periodic refresh to avoid an epoch race with switch */
|
|
80
|
+
this.clearTimer('auth:refresh');
|
|
81
|
+
let signResult;
|
|
82
|
+
try {
|
|
83
|
+
signResult = (_k = (yield ((_j = this.signCallback) === null || _j === void 0 ? void 0 : _j.call(this, this._token, { target })))) !== null && _k !== void 0 ? _k : { signature: '', current: '' };
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
const err = new AuthSwitchError({ phase: 'sign', cause });
|
|
87
|
+
(_l = handlers === null || handlers === void 0 ? void 0 : handlers.onError) === null || _l === void 0 ? void 0 : _l.call(handlers, err);
|
|
88
|
+
this.rearmRefreshTimer();
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const res = yield this.gateway.switch({
|
|
93
|
+
current: signResult.current,
|
|
94
|
+
signature: signResult.signature,
|
|
95
|
+
authId: this.authId,
|
|
96
|
+
target,
|
|
97
|
+
});
|
|
98
|
+
const view = res;
|
|
99
|
+
if (myEpoch !== this.epoch)
|
|
100
|
+
return view;
|
|
101
|
+
this._token = (_o = (_m = view === null || view === void 0 ? void 0 : view.Token) === null || _m === void 0 ? void 0 : _m.identityToken) !== null && _o !== void 0 ? _o : this._token;
|
|
102
|
+
this.setState('authenticated');
|
|
103
|
+
this.emitToken(view);
|
|
104
|
+
(_p = handlers === null || handlers === void 0 ? void 0 : handlers.onSuccess) === null || _p === void 0 ? void 0 : _p.call(handlers, view);
|
|
105
|
+
return view;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
if (e instanceof AuthSwitchError)
|
|
109
|
+
throw e;
|
|
110
|
+
const err = new AuthSwitchError({ phase: 'server', cause: e });
|
|
111
|
+
(_q = handlers === null || handlers === void 0 ? void 0 : handlers.onError) === null || _q === void 0 ? void 0 : _q.call(handlers, err);
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.rearmRefreshTimer();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
/** Clear local auth state and stop the scheduler. Best-effort server notify (revokes the backend session). */
|
|
119
|
+
this.logout = () => __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
/** bump epoch so a late in-flight refresh/switch/reauth response can't resurrect auth after logout */
|
|
121
|
+
this.epoch++;
|
|
122
|
+
this.stop();
|
|
123
|
+
this._token = '';
|
|
124
|
+
this.authId = '';
|
|
125
|
+
this.signCallback = undefined;
|
|
126
|
+
this.setState('');
|
|
127
|
+
if (this.options.client.state === 'connected') {
|
|
128
|
+
yield this.gateway.logout({}).catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
this.onAuthState = (listener) => {
|
|
132
|
+
this.stateListeners.add(listener);
|
|
133
|
+
return () => this.stateListeners.delete(listener);
|
|
134
|
+
};
|
|
135
|
+
this.onTokenRefresh = (listener) => {
|
|
136
|
+
this.tokenListeners.add(listener);
|
|
137
|
+
return () => this.tokenListeners.delete(listener);
|
|
138
|
+
};
|
|
139
|
+
this.emitToken = (view) => {
|
|
140
|
+
this.tokenListeners.forEach(l => l(view));
|
|
141
|
+
};
|
|
142
|
+
this.start = () => {
|
|
143
|
+
this.active = true;
|
|
144
|
+
if (this.options.client.state === 'connected' && this._token) {
|
|
145
|
+
this.epoch++;
|
|
146
|
+
void this.sendUpdate();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
this.stop = () => {
|
|
150
|
+
this.active = false;
|
|
151
|
+
this.clearTimers();
|
|
152
|
+
};
|
|
153
|
+
this.destroy = () => {
|
|
154
|
+
this.stop();
|
|
155
|
+
this.unsubs.splice(0).forEach(unsub => unsub());
|
|
156
|
+
this.stateListeners.clear();
|
|
157
|
+
this.tokenListeners.clear();
|
|
158
|
+
};
|
|
159
|
+
this.sendUpdate = () => __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
var _r;
|
|
161
|
+
if (!this._token)
|
|
162
|
+
return;
|
|
163
|
+
const myEpoch = this.epoch;
|
|
164
|
+
this.setState('pending');
|
|
165
|
+
try {
|
|
166
|
+
const res = yield this.gateway.update({ token: this._token });
|
|
167
|
+
if (myEpoch !== this.epoch)
|
|
168
|
+
return;
|
|
169
|
+
this.handleAuthResponse((_r = res === null || res === void 0 ? void 0 : res.state) !== null && _r !== void 0 ? _r : 'failed');
|
|
170
|
+
}
|
|
171
|
+
catch (_s) {
|
|
172
|
+
if (myEpoch !== this.epoch)
|
|
173
|
+
return;
|
|
174
|
+
this.handleFailed();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
this.runRefresh = () => __awaiter(this, void 0, void 0, function* () {
|
|
178
|
+
var _t, _u;
|
|
179
|
+
if (!this.signCallback)
|
|
180
|
+
return;
|
|
181
|
+
this.epoch++;
|
|
182
|
+
const myEpoch = this.epoch;
|
|
183
|
+
let signResult;
|
|
184
|
+
try {
|
|
185
|
+
signResult = yield this.signCallback(this._token);
|
|
186
|
+
}
|
|
187
|
+
catch (_v) {
|
|
188
|
+
if (myEpoch !== this.epoch)
|
|
189
|
+
return;
|
|
190
|
+
this.handleFailed();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const res = yield this.gateway.refresh({
|
|
195
|
+
current: signResult.current,
|
|
196
|
+
signature: signResult.signature,
|
|
197
|
+
authId: this.authId,
|
|
198
|
+
});
|
|
199
|
+
if (myEpoch !== this.epoch)
|
|
200
|
+
return;
|
|
201
|
+
const view = res;
|
|
202
|
+
this._token = (_u = (_t = view === null || view === void 0 ? void 0 : view.Token) === null || _t === void 0 ? void 0 : _t.identityToken) !== null && _u !== void 0 ? _u : this._token;
|
|
203
|
+
this.failures = 0;
|
|
204
|
+
this.setState('authenticated');
|
|
205
|
+
this.scheduleRefresh();
|
|
206
|
+
this.emitToken(view);
|
|
207
|
+
}
|
|
208
|
+
catch (_w) {
|
|
209
|
+
if (myEpoch !== this.epoch)
|
|
210
|
+
return;
|
|
211
|
+
this.handleFailed();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
this.handleAuthResponse = (state) => {
|
|
215
|
+
if (state === 'authenticated') {
|
|
216
|
+
this.failures = 0;
|
|
217
|
+
this.setState('authenticated');
|
|
218
|
+
this.scheduleRefresh();
|
|
219
|
+
}
|
|
220
|
+
else if (state === 'validating') {
|
|
221
|
+
this.setState('validating');
|
|
222
|
+
this.schedule('auth:validating', this.validatingTimeoutMs, () => {
|
|
223
|
+
if (this._state === 'validating')
|
|
224
|
+
this.handleFailed();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.handleFailed();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
this.handleFailed = () => {
|
|
232
|
+
this.failures++;
|
|
233
|
+
this.setState('failed');
|
|
234
|
+
this.clearTimer('auth:refresh');
|
|
235
|
+
this.clearTimer('auth:validating');
|
|
236
|
+
if (this.failures > this.maxFailures) {
|
|
237
|
+
this.setState('expired');
|
|
238
|
+
this.active = false;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const delay = this.computeBackoffDelay(this.failures - 1);
|
|
242
|
+
this.schedule('auth:reauth', delay, () => void this.runReauth());
|
|
243
|
+
};
|
|
244
|
+
this.runReauth = () => __awaiter(this, void 0, void 0, function* () {
|
|
245
|
+
var _x, _y;
|
|
246
|
+
if (!this.signCallback)
|
|
247
|
+
return;
|
|
248
|
+
this.epoch++;
|
|
249
|
+
const myEpoch = this.epoch;
|
|
250
|
+
let signResult;
|
|
251
|
+
try {
|
|
252
|
+
signResult = yield this.signCallback(this._token);
|
|
253
|
+
}
|
|
254
|
+
catch (_z) {
|
|
255
|
+
if (myEpoch !== this.epoch)
|
|
256
|
+
return;
|
|
257
|
+
this.handleFailed();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const res = yield this.gateway.refresh({
|
|
262
|
+
current: signResult.current,
|
|
263
|
+
signature: signResult.signature,
|
|
264
|
+
authId: this.authId,
|
|
265
|
+
});
|
|
266
|
+
if (myEpoch !== this.epoch)
|
|
267
|
+
return;
|
|
268
|
+
const view = res;
|
|
269
|
+
this._token = (_y = (_x = view === null || view === void 0 ? void 0 : view.Token) === null || _x === void 0 ? void 0 : _x.identityToken) !== null && _y !== void 0 ? _y : this._token;
|
|
270
|
+
this.failures = 0;
|
|
271
|
+
this.setState('authenticated');
|
|
272
|
+
this.scheduleRefresh();
|
|
273
|
+
this.emitToken(view);
|
|
274
|
+
}
|
|
275
|
+
catch (_0) {
|
|
276
|
+
if (myEpoch !== this.epoch)
|
|
277
|
+
return;
|
|
278
|
+
this.handleFailed();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
this.computeBackoffDelay = (attempt) => {
|
|
282
|
+
const expDelay = Math.min(this.minBackoffMs * Math.pow(this.backoffFactor, attempt), this.maxBackoffMs);
|
|
283
|
+
const jitter = expDelay * (Math.random() * 2 - 1) * 0.3;
|
|
284
|
+
return Math.max(this.minBackoffMs, Math.min(this.maxBackoffMs, expDelay + jitter));
|
|
285
|
+
};
|
|
286
|
+
this.scheduleRefresh = () => {
|
|
287
|
+
this.schedule('auth:refresh', this.refreshIntervalMs, () => void this.runRefresh());
|
|
288
|
+
};
|
|
289
|
+
this.rearmRefreshTimer = () => {
|
|
290
|
+
if (this._state === 'authenticated')
|
|
291
|
+
this.scheduleRefresh();
|
|
292
|
+
};
|
|
293
|
+
this.schedule = (key, delayMs, task) => {
|
|
294
|
+
if (this.timerScheduler) {
|
|
295
|
+
this.timerScheduler.schedule(key, delayMs, task);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
setTimeout(task, delayMs);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
this.clearTimer = (key) => {
|
|
302
|
+
var _a;
|
|
303
|
+
(_a = this.timerScheduler) === null || _a === void 0 ? void 0 : _a.cancel(key);
|
|
304
|
+
};
|
|
305
|
+
this.clearTimers = () => {
|
|
306
|
+
this.clearTimer('auth:refresh');
|
|
307
|
+
this.clearTimer('auth:reauth');
|
|
308
|
+
this.clearTimer('auth:validating');
|
|
309
|
+
};
|
|
310
|
+
this.setState = (next) => {
|
|
311
|
+
if (this._state === next)
|
|
312
|
+
return;
|
|
313
|
+
this._state = next;
|
|
314
|
+
this.stateListeners.forEach(l => l(next));
|
|
315
|
+
};
|
|
316
|
+
this.refreshIntervalMs = (_a = options.refreshIntervalMs) !== null && _a !== void 0 ? _a : 1800000;
|
|
317
|
+
this.minBackoffMs = (_b = options.minBackoffMs) !== null && _b !== void 0 ? _b : 1000;
|
|
318
|
+
this.maxBackoffMs = (_c = options.maxBackoffMs) !== null && _c !== void 0 ? _c : 30000;
|
|
319
|
+
this.backoffFactor = (_d = options.backoffFactor) !== null && _d !== void 0 ? _d : 2;
|
|
320
|
+
this.maxFailures = (_e = options.maxFailures) !== null && _e !== void 0 ? _e : 5;
|
|
321
|
+
this.validatingTimeoutMs = (_f = options.validatingTimeoutMs) !== null && _f !== void 0 ? _f : 15000;
|
|
322
|
+
this.now = (_g = options.now) !== null && _g !== void 0 ? _g : (() => Date.now());
|
|
323
|
+
this.timerScheduler = options.timerScheduler;
|
|
324
|
+
this.gateway = (0, auth_gateway_1.createAuthGateway)(options.client);
|
|
325
|
+
this.unsubs.push(options.client.onState(event => {
|
|
326
|
+
if (!this.active)
|
|
327
|
+
return;
|
|
328
|
+
if (event.next === 'connected') {
|
|
329
|
+
this.epoch++;
|
|
330
|
+
void this.sendUpdate();
|
|
331
|
+
}
|
|
332
|
+
if (event.next === 'closed' || event.next === 'closing') {
|
|
333
|
+
this.clearTimers();
|
|
334
|
+
}
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
get state() {
|
|
338
|
+
return this._state;
|
|
339
|
+
}
|
|
340
|
+
get token() {
|
|
341
|
+
return this._token;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
exports.AuthControllerImpl = AuthControllerImpl;
|
|
@@ -17,6 +17,7 @@ const pending_request_store_1 = require("./pending-request-store");
|
|
|
17
17
|
const reconnect_controller_1 = require("./reconnect-controller");
|
|
18
18
|
const shared_timer_scheduler_1 = require("./shared-timer-scheduler");
|
|
19
19
|
const socket_transport_1 = require("./socket-transport");
|
|
20
|
+
const auth_controller_1 = require("./auth-controller");
|
|
20
21
|
const buildMid = () => `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
21
22
|
const parseMessage = (raw) => {
|
|
22
23
|
const text = `${raw !== null && raw !== void 0 ? raw : ''}`.trim();
|
|
@@ -136,10 +137,11 @@ class ClientSocketV2Impl {
|
|
|
136
137
|
};
|
|
137
138
|
this.onType = (type, listener) => this.router.onType(type, listener);
|
|
138
139
|
this.destroy = () => {
|
|
139
|
-
var _a, _b;
|
|
140
|
-
/** 순서 invariant: reconnect.destroy → keepAlive.destroy → transport.disconnect (그 close 이벤트를 reconnect가 못 듣게 하기 위함) → 나머지 정리 */
|
|
140
|
+
var _a, _b, _c;
|
|
141
|
+
/** 순서 invariant: reconnect.destroy → keepAlive.destroy → auth.destroy → transport.disconnect (그 close 이벤트를 reconnect가 못 듣게 하기 위함) → 나머지 정리 */
|
|
141
142
|
(_a = this.reconnect) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
142
143
|
(_b = this.keepAlive) === null || _b === void 0 ? void 0 : _b.destroy();
|
|
144
|
+
(_c = this.auth) === null || _c === void 0 ? void 0 : _c.destroy();
|
|
143
145
|
void this.transport.disconnect().catch(() => undefined);
|
|
144
146
|
this.transportUnsubs.splice(0).forEach(unsub => unsub());
|
|
145
147
|
this.rejectQueuedRequests(new Error(`499 CLIENT CLOSED REQUEST - client destroyed`));
|
|
@@ -283,6 +285,11 @@ class ClientSocketV2Impl {
|
|
|
283
285
|
if (options.reconnect !== false) {
|
|
284
286
|
this.reconnect = new reconnect_controller_1.AutoReconnectController(Object.assign({ client: this, timerScheduler: this.timerScheduler }, (options.reconnect || {})));
|
|
285
287
|
}
|
|
288
|
+
if (options.auth !== false) {
|
|
289
|
+
const auth = new auth_controller_1.AuthControllerImpl(Object.assign({ client: this, timerScheduler: this.timerScheduler }, (options.auth || {})));
|
|
290
|
+
this.auth = auth;
|
|
291
|
+
auth.start();
|
|
292
|
+
}
|
|
286
293
|
}
|
|
287
294
|
get state() {
|
|
288
295
|
return this._state;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { AuthUpdateInput, AuthUpdateResponse } from '../../lib/auth/types';
|
|
1
|
+
import type { AuthRefreshInput, AuthRefreshResponse, AuthSwitchInput, AuthSwitchResponse, AuthLogoutInput, AuthLogoutResponse, AuthUpdateInput, AuthUpdateResponse } from '../../lib/auth/types';
|
|
2
2
|
import type { ClientSocketV2 } from '../types';
|
|
3
3
|
export interface AuthGateway {
|
|
4
4
|
update(data: AuthUpdateInput): Promise<AuthUpdateResponse>;
|
|
5
|
+
refresh(data: AuthRefreshInput): Promise<AuthRefreshResponse>;
|
|
6
|
+
switch(data: AuthSwitchInput): Promise<AuthSwitchResponse>;
|
|
7
|
+
logout(data?: AuthLogoutInput): Promise<AuthLogoutResponse>;
|
|
5
8
|
}
|
|
6
9
|
export declare const createAuthGateway: (client: ClientSocketV2) => AuthGateway;
|
|
@@ -6,6 +6,9 @@ const createAuthGateway = (client) => {
|
|
|
6
6
|
const gateway = (0, create_domain_gateway_1.createDomainGateway)('auth', client);
|
|
7
7
|
return {
|
|
8
8
|
update: data => gateway.request('update', data),
|
|
9
|
+
refresh: data => gateway.request('refresh', data),
|
|
10
|
+
switch: data => gateway.request('switch', data),
|
|
11
|
+
logout: data => gateway.request('logout', data !== null && data !== void 0 ? data : {}),
|
|
9
12
|
};
|
|
10
13
|
};
|
|
11
14
|
exports.createAuthGateway = createAuthGateway;
|
|
@@ -27,6 +27,7 @@ export * from './gateways/create-domain-gateway';
|
|
|
27
27
|
export * from './gateways/device-gateway';
|
|
28
28
|
export * from './gateways/channel-gateway';
|
|
29
29
|
export * from './gateways/auth-gateway';
|
|
30
|
+
export * from './auth-controller';
|
|
30
31
|
export * from './gateways/chat-gateway';
|
|
31
32
|
export * from './gateways/join-gateway';
|
|
32
33
|
export * from './gateways/cloud-gateway';
|
|
@@ -44,6 +44,7 @@ __exportStar(require("./gateways/create-domain-gateway"), exports);
|
|
|
44
44
|
__exportStar(require("./gateways/device-gateway"), exports);
|
|
45
45
|
__exportStar(require("./gateways/channel-gateway"), exports);
|
|
46
46
|
__exportStar(require("./gateways/auth-gateway"), exports);
|
|
47
|
+
__exportStar(require("./auth-controller"), exports);
|
|
47
48
|
__exportStar(require("./gateways/chat-gateway"), exports);
|
|
48
49
|
__exportStar(require("./gateways/join-gateway"), exports);
|
|
49
50
|
__exportStar(require("./gateways/cloud-gateway"), exports);
|
|
@@ -24,6 +24,7 @@ export interface ChannelSyncPlanOptions<TView extends SyncableView = SyncableVie
|
|
|
24
24
|
export declare class ChannelSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<ChannelSyncTarget> {
|
|
25
25
|
private readonly options;
|
|
26
26
|
readonly domain = "channel";
|
|
27
|
+
readonly requiresAuth = true;
|
|
27
28
|
readonly idleBackoff: SyncBackoffOptions;
|
|
28
29
|
constructor(options?: ChannelSyncPlanOptions<TView>);
|
|
29
30
|
supports: (target: SyncTargetDescriptor) => target is ChannelSyncTarget;
|
|
@@ -20,6 +20,7 @@ class ChannelSyncPlan {
|
|
|
20
20
|
var _a;
|
|
21
21
|
this.options = options;
|
|
22
22
|
this.domain = 'channel';
|
|
23
|
+
this.requiresAuth = true;
|
|
23
24
|
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'channel';
|
|
24
25
|
this.getKey = (target) => { var _a; return `channel:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
25
26
|
this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
|
|
@@ -41,6 +41,7 @@ export interface ChatSyncPlanOptions<TMessage extends ChatMessageLike = ChatMess
|
|
|
41
41
|
export declare class ChatSyncPlan<TMessage extends ChatMessageLike = ChatMessageLike> implements DomainSyncPlan<ChatSyncTarget> {
|
|
42
42
|
private readonly options;
|
|
43
43
|
readonly domain = "chat";
|
|
44
|
+
readonly requiresAuth = true;
|
|
44
45
|
readonly idleBackoff: SyncBackoffOptions;
|
|
45
46
|
private readonly chains;
|
|
46
47
|
constructor(options?: ChatSyncPlanOptions<TMessage>);
|
|
@@ -22,6 +22,7 @@ class ChatSyncPlan {
|
|
|
22
22
|
var _a;
|
|
23
23
|
this.options = options;
|
|
24
24
|
this.domain = 'chat';
|
|
25
|
+
this.requiresAuth = true;
|
|
25
26
|
this.chains = new Map();
|
|
26
27
|
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'chat';
|
|
27
28
|
this.getKey = (target) => { var _a; return `chat:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
@@ -21,6 +21,7 @@ export interface DeviceSyncPlanOptions {
|
|
|
21
21
|
export declare class DeviceSyncPlan implements DomainSyncPlan<DeviceSyncTarget> {
|
|
22
22
|
private readonly options;
|
|
23
23
|
readonly domain = "device";
|
|
24
|
+
readonly requiresAuth = false;
|
|
24
25
|
readonly failurePolicy: SyncFailurePolicy;
|
|
25
26
|
readonly idleBackoff: SyncBackoffOptions;
|
|
26
27
|
constructor(options?: DeviceSyncPlanOptions);
|
|
@@ -15,6 +15,7 @@ class DeviceSyncPlan {
|
|
|
15
15
|
var _a, _b;
|
|
16
16
|
this.options = options;
|
|
17
17
|
this.domain = 'device';
|
|
18
|
+
this.requiresAuth = false;
|
|
18
19
|
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'device';
|
|
19
20
|
this.getKey = (target) => { var _a; return `device:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : 'current'}`; };
|
|
20
21
|
this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
|
|
@@ -24,6 +24,7 @@ export interface JoinSyncPlanOptions<TView extends SyncableView = SyncableView>
|
|
|
24
24
|
export declare class JoinSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<JoinSyncTarget> {
|
|
25
25
|
private readonly options;
|
|
26
26
|
readonly domain = "join";
|
|
27
|
+
readonly requiresAuth = true;
|
|
27
28
|
readonly idleBackoff: SyncBackoffOptions;
|
|
28
29
|
constructor(options?: JoinSyncPlanOptions<TView>);
|
|
29
30
|
supports: (target: SyncTargetDescriptor) => target is JoinSyncTarget;
|
|
@@ -20,6 +20,7 @@ class JoinSyncPlan {
|
|
|
20
20
|
var _a;
|
|
21
21
|
this.options = options;
|
|
22
22
|
this.domain = 'join';
|
|
23
|
+
this.requiresAuth = true;
|
|
23
24
|
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'join';
|
|
24
25
|
this.getKey = (target) => { var _a; return `join:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
25
26
|
// Default poll interval (10s); the scheduler backs this off x2 up to 60s while idle.
|
|
@@ -24,6 +24,7 @@ export interface PlaceSyncPlanOptions<TView extends SyncableView = SyncableView>
|
|
|
24
24
|
export declare class PlaceSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<PlaceSyncTarget> {
|
|
25
25
|
private readonly options;
|
|
26
26
|
readonly domain = "place";
|
|
27
|
+
readonly requiresAuth = true;
|
|
27
28
|
readonly idleBackoff: SyncBackoffOptions;
|
|
28
29
|
constructor(options?: PlaceSyncPlanOptions<TView>);
|
|
29
30
|
supports: (target: SyncTargetDescriptor) => target is PlaceSyncTarget;
|
|
@@ -20,6 +20,7 @@ class PlaceSyncPlan {
|
|
|
20
20
|
var _a;
|
|
21
21
|
this.options = options;
|
|
22
22
|
this.domain = 'place';
|
|
23
|
+
this.requiresAuth = true;
|
|
23
24
|
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'place';
|
|
24
25
|
this.getKey = (target) => { var _a; return `place:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
25
26
|
this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
|
|
@@ -24,6 +24,7 @@ export interface ProfileSyncPlanOptions<TView extends SyncableView = SyncableVie
|
|
|
24
24
|
export declare class ProfileSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<ProfileSyncTarget> {
|
|
25
25
|
private readonly options;
|
|
26
26
|
readonly domain = "profile";
|
|
27
|
+
readonly requiresAuth = true;
|
|
27
28
|
readonly idleBackoff: SyncBackoffOptions;
|
|
28
29
|
constructor(options?: ProfileSyncPlanOptions<TView>);
|
|
29
30
|
supports: (target: SyncTargetDescriptor) => target is ProfileSyncTarget;
|
|
@@ -20,6 +20,7 @@ class ProfileSyncPlan {
|
|
|
20
20
|
var _a;
|
|
21
21
|
this.options = options;
|
|
22
22
|
this.domain = 'profile';
|
|
23
|
+
this.requiresAuth = true;
|
|
23
24
|
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'profile';
|
|
24
25
|
this.getKey = (target) => { var _a; return `profile:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
25
26
|
this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
|
|
@@ -27,6 +27,7 @@ export declare class DomainSyncScheduler implements SyncScheduler {
|
|
|
27
27
|
private readonly random;
|
|
28
28
|
private readonly unsubs;
|
|
29
29
|
constructor(options: SyncSchedulerOptions);
|
|
30
|
+
private isEntryAuthReady;
|
|
30
31
|
start: (target: SyncTargetDescriptor) => void;
|
|
31
32
|
stop: (target: SyncTargetDescriptor) => void;
|
|
32
33
|
stopAll: () => void;
|
|
@@ -36,7 +37,11 @@ export declare class DomainSyncScheduler implements SyncScheduler {
|
|
|
36
37
|
private resolvePlan;
|
|
37
38
|
private findEntry;
|
|
38
39
|
private buildContext;
|
|
40
|
+
/** run the connect-time hook (may poll the server, e.g. chat catch-up) then fresh-start the poll loop. */
|
|
41
|
+
private activateEntry;
|
|
39
42
|
private handleConnected;
|
|
43
|
+
private resumeAuthGated;
|
|
44
|
+
private pauseAuthGated;
|
|
40
45
|
private stopAllTimers;
|
|
41
46
|
private clearTimer;
|
|
42
47
|
private scheduleNow;
|
|
@@ -24,6 +24,12 @@ class DomainSyncScheduler {
|
|
|
24
24
|
this.targets = new Map();
|
|
25
25
|
this.snapshots = new Map();
|
|
26
26
|
this.unsubs = [];
|
|
27
|
+
this.isEntryAuthReady = (entry) => {
|
|
28
|
+
if (!entry.plan.requiresAuth)
|
|
29
|
+
return true;
|
|
30
|
+
const auth = this.options.client.auth;
|
|
31
|
+
return !!auth && auth.state === 'authenticated';
|
|
32
|
+
};
|
|
27
33
|
this.start = (target) => {
|
|
28
34
|
const plan = this.resolvePlan(target);
|
|
29
35
|
const key = plan.getKey(target);
|
|
@@ -32,8 +38,9 @@ class DomainSyncScheduler {
|
|
|
32
38
|
entry.target = Object.assign(Object.assign({}, entry.target), target);
|
|
33
39
|
return;
|
|
34
40
|
}
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
const created = { target, plan };
|
|
42
|
+
this.targets.set(key, created);
|
|
43
|
+
if (this.options.client.state === 'connected' && this.isEntryAuthReady(created))
|
|
37
44
|
this.scheduleNow(key);
|
|
38
45
|
};
|
|
39
46
|
this.stop = (target) => {
|
|
@@ -104,18 +111,40 @@ class DomainSyncScheduler {
|
|
|
104
111
|
}),
|
|
105
112
|
stop: target => this.stop(target),
|
|
106
113
|
});
|
|
107
|
-
|
|
114
|
+
/** run the connect-time hook (may poll the server, e.g. chat catch-up) then fresh-start the poll loop. */
|
|
115
|
+
this.activateEntry = (key, entry, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
108
116
|
var _k, _l;
|
|
117
|
+
yield ((_l = (_k = entry.plan).onConnected) === null || _l === void 0 ? void 0 : _l.call(_k, entry.target, ctx));
|
|
118
|
+
entry.failures = 0;
|
|
119
|
+
entry.goneStreak = 0;
|
|
120
|
+
entry.idleStreak = 0;
|
|
121
|
+
this.scheduleNow(key);
|
|
122
|
+
});
|
|
123
|
+
this.handleConnected = () => __awaiter(this, void 0, void 0, function* () {
|
|
109
124
|
const ctx = this.buildContext();
|
|
110
125
|
for (const [key, entry] of this.targets.entries()) {
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
entry
|
|
114
|
-
|
|
115
|
-
yield
|
|
116
|
-
this.scheduleNow(key);
|
|
126
|
+
// gated (requiresAuth) entries activate on `authenticated` instead — see resumeAuthGated.
|
|
127
|
+
// onConnected itself may poll the server (chat catch-up), so it must be gated too, not just run().
|
|
128
|
+
if (!this.isEntryAuthReady(entry))
|
|
129
|
+
continue;
|
|
130
|
+
yield this.activateEntry(key, entry, ctx);
|
|
117
131
|
}
|
|
118
132
|
});
|
|
133
|
+
this.resumeAuthGated = () => __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
if (this.options.client.state !== 'connected')
|
|
135
|
+
return;
|
|
136
|
+
const ctx = this.buildContext();
|
|
137
|
+
for (const [key, entry] of this.targets.entries()) {
|
|
138
|
+
if (entry.plan.requiresAuth)
|
|
139
|
+
yield this.activateEntry(key, entry, ctx);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this.pauseAuthGated = () => {
|
|
143
|
+
this.targets.forEach(entry => {
|
|
144
|
+
if (entry.plan.requiresAuth)
|
|
145
|
+
this.clearTimer(entry);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
119
148
|
this.stopAllTimers = () => {
|
|
120
149
|
this.targets.forEach(entry => this.clearTimer(entry));
|
|
121
150
|
};
|
|
@@ -143,7 +172,7 @@ class DomainSyncScheduler {
|
|
|
143
172
|
if (!entry)
|
|
144
173
|
return;
|
|
145
174
|
this.clearTimer(entry);
|
|
146
|
-
if (this.options.client.state !== 'connected')
|
|
175
|
+
if (this.options.client.state !== 'connected' || !this.isEntryAuthReady(entry))
|
|
147
176
|
return;
|
|
148
177
|
const baseMs = (_d = (_a = entry.target.intervalMs) !== null && _a !== void 0 ? _a : (_c = (_b = entry.plan).getIntervalMs) === null || _c === void 0 ? void 0 : _c.call(_b, entry.target)) !== null && _d !== void 0 ? _d : 5000;
|
|
149
178
|
const intervalMs = this.computeInterval(entry, baseMs);
|
|
@@ -170,7 +199,7 @@ class DomainSyncScheduler {
|
|
|
170
199
|
this.toTimerKey = (key) => `${this.timerPrefix}${key}`;
|
|
171
200
|
this.runEntry = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
172
201
|
const entry = this.targets.get(key);
|
|
173
|
-
if (!entry || this.options.client.state !== 'connected')
|
|
202
|
+
if (!entry || this.options.client.state !== 'connected' || !this.isEntryAuthReady(entry))
|
|
174
203
|
return;
|
|
175
204
|
if (entry.inFlight)
|
|
176
205
|
return;
|
|
@@ -236,6 +265,8 @@ class DomainSyncScheduler {
|
|
|
236
265
|
return;
|
|
237
266
|
if (!type.startsWith(`${entry.target.type}.`))
|
|
238
267
|
return;
|
|
268
|
+
if (!this.isEntryAuthReady(entry))
|
|
269
|
+
return;
|
|
239
270
|
// nudge resets idle backoff (best-effort)
|
|
240
271
|
entry.idleStreak = 0;
|
|
241
272
|
yield entry.plan.onTrigger(entry.target, message, ctx).catch(() => undefined);
|
|
@@ -261,6 +292,15 @@ class DomainSyncScheduler {
|
|
|
261
292
|
this.unsubs.push(options.client.onMessage(({ message }) => {
|
|
262
293
|
void this.handleTrigger(message);
|
|
263
294
|
}));
|
|
295
|
+
const auth = options.client.auth;
|
|
296
|
+
if (auth) {
|
|
297
|
+
this.unsubs.push(auth.onAuthState(state => {
|
|
298
|
+
if (state === 'authenticated')
|
|
299
|
+
void this.resumeAuthGated();
|
|
300
|
+
else
|
|
301
|
+
this.pauseAuthGated();
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
264
304
|
}
|
|
265
305
|
}
|
|
266
306
|
exports.DomainSyncScheduler = DomainSyncScheduler;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolveSocketPacketType, SocketMessage, SocketPacketInputType, SocketPacketRequestData, SocketPacketResponseData, SocketPacketType } from '../lib/types';
|
|
2
2
|
import type { DeviceBootstrapInput } from '../lib/device/types';
|
|
3
|
+
import type { AuthController, AuthControllerOptionsPartial } from './auth-controller';
|
|
3
4
|
export declare type ClientSocketState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
4
5
|
export interface SocketLike {
|
|
5
6
|
readonly readyState: number;
|
|
@@ -81,6 +82,7 @@ export interface ClientSocketOptions {
|
|
|
81
82
|
maxPendingRequests?: number;
|
|
82
83
|
keepAlive?: false | KeepAliveLoopOptionsPartial;
|
|
83
84
|
reconnect?: false | AutoReconnectOptionsPartial;
|
|
85
|
+
auth?: false | AuthControllerOptionsPartial;
|
|
84
86
|
createMid?: () => string;
|
|
85
87
|
now?: () => number;
|
|
86
88
|
}
|
|
@@ -103,6 +105,7 @@ export interface ClientSocketV2 {
|
|
|
103
105
|
readonly timerScheduler?: SharedTimerScheduler;
|
|
104
106
|
readonly keepAlive?: KeepAliveLoopControl;
|
|
105
107
|
readonly reconnect?: ReconnectController;
|
|
108
|
+
readonly auth?: AuthController;
|
|
106
109
|
connect(): Promise<void>;
|
|
107
110
|
disconnect(code?: number, reason?: string): Promise<void>;
|
|
108
111
|
send<TType extends SocketPacketInputType>(type: TType, data?: SocketPacketRequestData<ResolveSocketPacketType<TType>>): void;
|
|
@@ -163,6 +166,7 @@ export interface DomainSyncContext {
|
|
|
163
166
|
}
|
|
164
167
|
export interface DomainSyncPlan<TTarget extends SyncTargetDescriptor = SyncTargetDescriptor> {
|
|
165
168
|
readonly domain: string;
|
|
169
|
+
readonly requiresAuth?: boolean;
|
|
166
170
|
readonly failurePolicy?: SyncFailurePolicy;
|
|
167
171
|
readonly idleBackoff?: SyncBackoffOptions;
|
|
168
172
|
supports(target: SyncTargetDescriptor): target is TTarget;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lib/auth/contracts.ts`
|
|
3
|
+
* - client-safe shared auth contracts.
|
|
4
|
+
* - keep this file free from server-only runtime dependencies.
|
|
5
|
+
*/
|
|
6
|
+
export declare type AuthUpdateState = '' | 'pending' | 'validating' | 'authenticated' | 'failed' | 'disconnected';
|
|
7
|
+
export interface AuthUserHead {
|
|
8
|
+
/** id of user */
|
|
9
|
+
id?: string;
|
|
10
|
+
/** name of user */
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
/** AWS Cognito temporary credential. */
|
|
14
|
+
export interface AuthCredential {
|
|
15
|
+
AccessKeyId?: string;
|
|
16
|
+
SecretKey?: string;
|
|
17
|
+
SessionToken?: string;
|
|
18
|
+
/** ISO expiration timestamp */
|
|
19
|
+
Expiration?: string;
|
|
20
|
+
}
|
|
21
|
+
/** issued identity token bundle. */
|
|
22
|
+
export interface AuthToken {
|
|
23
|
+
authId?: string;
|
|
24
|
+
accountId?: string;
|
|
25
|
+
identityId?: string;
|
|
26
|
+
identityPoolId?: string;
|
|
27
|
+
/** JWT identity token — SSoT used for HTTP `Authorization` headers */
|
|
28
|
+
identityToken?: string;
|
|
29
|
+
credential?: AuthCredential;
|
|
30
|
+
}
|
|
31
|
+
/** auth identity record (user/site binding) embedded in the token view. */
|
|
32
|
+
export interface AuthRecord {
|
|
33
|
+
id: string;
|
|
34
|
+
accountId?: string;
|
|
35
|
+
userId?: string;
|
|
36
|
+
siteId?: string;
|
|
37
|
+
refreshedAt?: number;
|
|
38
|
+
}
|
|
39
|
+
/** client-safe view of the refresh/switch token payload (mirrors backend `UserTokenView`). */
|
|
40
|
+
export interface AuthTokenView {
|
|
41
|
+
/** user id */
|
|
42
|
+
id: string;
|
|
43
|
+
accountId?: string;
|
|
44
|
+
/** role of the user, e.g. `guest` */
|
|
45
|
+
userRole?: string;
|
|
46
|
+
/** status of the user, e.g. `active` */
|
|
47
|
+
userStatus?: string;
|
|
48
|
+
/** issued token bundle; `Token.identityToken` is the SSoT */
|
|
49
|
+
Token?: AuthToken;
|
|
50
|
+
/** auth identity record */
|
|
51
|
+
$auth?: AuthRecord;
|
|
52
|
+
/** cloud id */
|
|
53
|
+
cloudId?: string;
|
|
54
|
+
}
|
|
55
|
+
/** client-safe view of the logout result (curated from backend `UserLogoutResult`). */
|
|
56
|
+
export interface AuthLogoutView {
|
|
57
|
+
/** logged-out user id */
|
|
58
|
+
id?: string;
|
|
59
|
+
/** revoked auth-id */
|
|
60
|
+
authId?: string;
|
|
61
|
+
/** revoke flag (1 when revoked) */
|
|
62
|
+
revoked?: number;
|
|
63
|
+
/** revoke completion time (epoch ms) */
|
|
64
|
+
revokedAt?: number;
|
|
65
|
+
}
|
package/dist/lib/auth/types.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import type { InferSocketError, InferSocketRequest, InferSocketResponse
|
|
2
|
-
|
|
1
|
+
import type { InferSocketError, InferSocketRequest, InferSocketResponse } from '../types';
|
|
2
|
+
import type { SocketErrorMessage, SocketRequestMessage, SocketResponseMessage } from '../types';
|
|
3
|
+
import { AuthUpdateState, AuthUserHead } from './contracts';
|
|
3
4
|
export interface AuthUpdateRequestData {
|
|
4
5
|
token?: string;
|
|
5
6
|
dryRun?: boolean;
|
|
6
7
|
}
|
|
7
|
-
export interface AuthUpdateMemberHead {
|
|
8
|
-
id?: string;
|
|
9
|
-
name?: string;
|
|
10
|
-
}
|
|
11
8
|
export interface AuthUpdateResponseData {
|
|
12
9
|
connId?: string;
|
|
13
10
|
deviceId?: string;
|
|
@@ -16,7 +13,27 @@ export interface AuthUpdateResponseData {
|
|
|
16
13
|
state?: AuthUpdateState;
|
|
17
14
|
stateAt?: number;
|
|
18
15
|
error?: string;
|
|
19
|
-
member$?:
|
|
16
|
+
member$?: AuthUserHead;
|
|
17
|
+
}
|
|
18
|
+
export interface AuthRefreshRequestData {
|
|
19
|
+
/** ISO timestamp used to compute the signature */
|
|
20
|
+
current: string;
|
|
21
|
+
/** app-computed proof signature (lemon hmac) */
|
|
22
|
+
signature: string;
|
|
23
|
+
/** backend auth-id; used as `/oauth/{authId}/refresh` path */
|
|
24
|
+
authId: string;
|
|
25
|
+
/** (internal) userAgent the signature */
|
|
26
|
+
userAgent?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface AuthSwitchRequestData extends AuthRefreshRequestData {
|
|
29
|
+
/** 전환할 사이트 ID */
|
|
30
|
+
target: string;
|
|
31
|
+
}
|
|
32
|
+
export interface AuthLogoutRequestData {
|
|
33
|
+
/** (internal) auth-id */
|
|
34
|
+
authId?: string;
|
|
35
|
+
/** (internal) userAgent the signature */
|
|
36
|
+
userAgent?: string;
|
|
20
37
|
}
|
|
21
38
|
declare module '../types' {
|
|
22
39
|
interface SocketPacketRegistry {
|
|
@@ -25,6 +42,21 @@ declare module '../types' {
|
|
|
25
42
|
response: AuthUpdateResponseData;
|
|
26
43
|
error: null;
|
|
27
44
|
};
|
|
45
|
+
'auth.refresh': {
|
|
46
|
+
request: AuthRefreshRequestData;
|
|
47
|
+
response: unknown;
|
|
48
|
+
error: null;
|
|
49
|
+
};
|
|
50
|
+
'auth.switch': {
|
|
51
|
+
request: AuthSwitchRequestData;
|
|
52
|
+
response: unknown;
|
|
53
|
+
error: null;
|
|
54
|
+
};
|
|
55
|
+
'auth.logout': {
|
|
56
|
+
request: AuthLogoutRequestData;
|
|
57
|
+
response: unknown;
|
|
58
|
+
error: null;
|
|
59
|
+
};
|
|
28
60
|
}
|
|
29
61
|
}
|
|
30
62
|
export declare type AuthUpdateType = 'auth.update';
|
|
@@ -34,3 +66,24 @@ export declare type AuthUpdateErrorData = InferSocketError<AuthUpdateType>;
|
|
|
34
66
|
export declare type AuthUpdateRequestMessage = SocketRequestMessage<AuthUpdateType>;
|
|
35
67
|
export declare type AuthUpdateResponseMessage = SocketResponseMessage<AuthUpdateType>;
|
|
36
68
|
export declare type AuthUpdateErrorMessage = SocketErrorMessage<AuthUpdateType>;
|
|
69
|
+
export declare type AuthRefreshType = 'auth.refresh';
|
|
70
|
+
export declare type AuthRefreshInput = InferSocketRequest<AuthRefreshType>;
|
|
71
|
+
export declare type AuthRefreshResponse = InferSocketResponse<AuthRefreshType>;
|
|
72
|
+
export declare type AuthRefreshErrorData = InferSocketError<AuthRefreshType>;
|
|
73
|
+
export declare type AuthRefreshRequestMessage = SocketRequestMessage<AuthRefreshType>;
|
|
74
|
+
export declare type AuthRefreshResponseMessage = SocketResponseMessage<AuthRefreshType>;
|
|
75
|
+
export declare type AuthRefreshErrorMessage = SocketErrorMessage<AuthRefreshType>;
|
|
76
|
+
export declare type AuthSwitchType = 'auth.switch';
|
|
77
|
+
export declare type AuthSwitchInput = InferSocketRequest<AuthSwitchType>;
|
|
78
|
+
export declare type AuthSwitchResponse = InferSocketResponse<AuthSwitchType>;
|
|
79
|
+
export declare type AuthSwitchErrorData = InferSocketError<AuthSwitchType>;
|
|
80
|
+
export declare type AuthSwitchRequestMessage = SocketRequestMessage<AuthSwitchType>;
|
|
81
|
+
export declare type AuthSwitchResponseMessage = SocketResponseMessage<AuthSwitchType>;
|
|
82
|
+
export declare type AuthSwitchErrorMessage = SocketErrorMessage<AuthSwitchType>;
|
|
83
|
+
export declare type AuthLogoutType = 'auth.logout';
|
|
84
|
+
export declare type AuthLogoutInput = InferSocketRequest<AuthLogoutType>;
|
|
85
|
+
export declare type AuthLogoutResponse = InferSocketResponse<AuthLogoutType>;
|
|
86
|
+
export declare type AuthLogoutErrorData = InferSocketError<AuthLogoutType>;
|
|
87
|
+
export declare type AuthLogoutRequestMessage = SocketRequestMessage<AuthLogoutType>;
|
|
88
|
+
export declare type AuthLogoutResponseMessage = SocketResponseMessage<AuthLogoutType>;
|
|
89
|
+
export declare type AuthLogoutErrorMessage = SocketErrorMessage<AuthLogoutType>;
|
package/package.json
CHANGED