@inixiative/signet 0.2.2 → 0.3.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 +15 -21
- package/package.json +2 -2
- package/src/client.ts +32 -1
- package/src/contracts.ts +8 -1
- package/src/index.ts +1 -0
- package/src/installationPairing.ts +201 -0
- package/src/installationSocket.ts +11 -1
package/README.md
CHANGED
|
@@ -11,33 +11,27 @@ This package is the one implementation of that protocol, shared by Kingdom (whic
|
|
|
11
11
|
- `files`: private 0600 JSON files for keys and credentials.
|
|
12
12
|
- `client`:
|
|
13
13
|
- an **Installation** (a local Foundry, Archive or Oracle) is an actor known to Kingdom by its key: `registerInstallation`, then `requestRegistration` returns a review code a person claims in Kingdom (a `registerIntegration` Inquiry), then `collectSignet` and `saveCollectedSignet`. `installationInquiries`, `installationSignets`, `cancelInstallationInquiry`, `enrollInstallationSignet` and `rotateInstallationKey` act as the installation;
|
|
14
|
+
- `pairInstallation` does the whole pairing (register, request, wait on the socket, confirm the owner, collect) and `holdInstallationSignets` keeps one credential file per Signet the installation's integrations hold, enrolling later grants and dropping revoked ones;
|
|
14
15
|
- `InstallationSocket`, Kingdom's live line to an installation: it proves the key, receives a snapshot of the installation's inquiries and Signets on connect and on every change, keeps the installation's integrations marked online, advertises its viewer URL, and polls the same reads while the socket is down;
|
|
15
|
-
- `SignetClient`, which renews its access token and signs each `describe`
|
|
16
|
+
- `SignetClient`, which renews its access token and signs each call: `describe`, `execute`, task settlement, and the run actions a Foundry uses for inference (`resolveRun`, `delegateRun`, `renewRun`, `revokeRun`).
|
|
16
17
|
|
|
17
18
|
```ts
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const { reviewCode } = await requestRegistration(kingdom, keyFile, {
|
|
26
|
-
name: 'Laptop Archive', lifecycle: 'ongoing', resources: [], expiresAt: null, maxRequests: null, maxConcurrent: 4,
|
|
19
|
+
import { holdInstallationSignets, InstallationSocket, pairInstallation, SignetClient } from '@inixiative/signet';
|
|
20
|
+
|
|
21
|
+
const paired = await pairInstallation({
|
|
22
|
+
url: kingdom, root: dataDir, kind: 'archive', name: 'Laptop Archive', sourceId,
|
|
23
|
+
terms: { name: 'Laptop Archive', lifecycle: 'ongoing', resources: [], expiresAt: null, maxRequests: null, maxConcurrent: 4 },
|
|
24
|
+
onReview: ({ review }) => console.log(`Approve in Kingdom: ${review}`),
|
|
25
|
+
confirmOwner: async ({ ownerName }) => confirm(`Pair with ${ownerName}?`),
|
|
27
26
|
});
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
|
|
28
|
+
new InstallationSocket({
|
|
30
29
|
url: kingdom,
|
|
31
|
-
keyFile,
|
|
32
|
-
onSnapshot:
|
|
33
|
-
|
|
34
|
-
const collected = approved && (await collectSignet(kingdom, keyFile, approved.id));
|
|
35
|
-
if (collected) await saveCollectedSignet(credentialFile, kingdom, keyFile, collected);
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
socket.start();
|
|
30
|
+
keyFile: paired.keyFile,
|
|
31
|
+
onSnapshot: ({ signets }) => holdInstallationSignets(kingdom, paired.keyFile, paired.directory, signets),
|
|
32
|
+
}).start();
|
|
39
33
|
|
|
40
|
-
const signet =
|
|
34
|
+
const signet = await SignetClient.fromFile(paired.credentialFile);
|
|
41
35
|
await signet.execute({ integrationId, operation: 'sessions.write', input: { resourceId, snapshot, previousDigest: null } });
|
|
42
36
|
```
|
|
43
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/signet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Present Kingdom Signets: the wire contracts, DPoP proofs, device pairing and a renewing client shared by Kingdom, Archive, Foundry and Oracle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@biomejs/biome": "2.5.2",
|
|
37
|
-
"@inixiative/config": "^0.5.
|
|
37
|
+
"@inixiative/config": "^0.5.59",
|
|
38
38
|
"@types/bun": "1.4.2",
|
|
39
39
|
"lefthook": "2.1.9",
|
|
40
40
|
"typescript": "6.0.3",
|
package/src/client.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
registerInstallationResponseSchema,
|
|
17
17
|
renewalCredentialPattern,
|
|
18
18
|
requestRegistrationResponseSchema,
|
|
19
|
+
setInstallationCredentialResponseSchema,
|
|
19
20
|
signetDescriptionSchema,
|
|
20
21
|
signetKeySchema,
|
|
21
22
|
signetLifecycleSchema,
|
|
@@ -346,6 +347,23 @@ export const enrollInstallationSignet = (
|
|
|
346
347
|
options,
|
|
347
348
|
);
|
|
348
349
|
|
|
350
|
+
/** Give Kingdom the credential it uses to reach this installation's integration (Foundry's tunnel token). */
|
|
351
|
+
export const setInstallationCredential = (
|
|
352
|
+
url: string,
|
|
353
|
+
keyFile: string,
|
|
354
|
+
integrationId: string,
|
|
355
|
+
token: string,
|
|
356
|
+
options: SignetRequestOptions = {},
|
|
357
|
+
) =>
|
|
358
|
+
installationPost(
|
|
359
|
+
url,
|
|
360
|
+
keyFile,
|
|
361
|
+
'setInstallationCredential',
|
|
362
|
+
{ integrationId, token },
|
|
363
|
+
setInstallationCredentialResponseSchema,
|
|
364
|
+
options,
|
|
365
|
+
);
|
|
366
|
+
|
|
349
367
|
/** Move this installation to a new key; both keys sign, the new proof bound to the current key. */
|
|
350
368
|
export async function rotateInstallationKey(
|
|
351
369
|
url: string,
|
|
@@ -384,13 +402,26 @@ export async function saveCollectedSignet(
|
|
|
384
402
|
return credential;
|
|
385
403
|
}
|
|
386
404
|
|
|
387
|
-
export type SignetAction =
|
|
405
|
+
export type SignetAction =
|
|
406
|
+
| 'describe'
|
|
407
|
+
| 'execute'
|
|
408
|
+
| 'closeTask'
|
|
409
|
+
| 'verifyAuthority'
|
|
410
|
+
| 'settleTask'
|
|
411
|
+
| 'resolveRun'
|
|
412
|
+
| 'delegateRun'
|
|
413
|
+
| 'renewRun'
|
|
414
|
+
| 'revokeRun';
|
|
388
415
|
const signetActions: SignetAction[] = [
|
|
389
416
|
'describe',
|
|
390
417
|
'execute',
|
|
391
418
|
'closeTask',
|
|
392
419
|
'verifyAuthority',
|
|
393
420
|
'settleTask',
|
|
421
|
+
'resolveRun',
|
|
422
|
+
'delegateRun',
|
|
423
|
+
'renewRun',
|
|
424
|
+
'revokeRun',
|
|
394
425
|
];
|
|
395
426
|
|
|
396
427
|
/** Presents one enrolled Signet: renews its access token and signs each call with the device key. */
|
package/src/contracts.ts
CHANGED
|
@@ -57,7 +57,6 @@ export type SignetResource = z.infer<typeof signetResourceSchema>;
|
|
|
57
57
|
|
|
58
58
|
export const signetEnrollmentSchema = z.strictObject({
|
|
59
59
|
signetId: z.uuid(),
|
|
60
|
-
runtimeInstallationId: z.uuid().optional(),
|
|
61
60
|
expectedRevision: z.number().int().min(1),
|
|
62
61
|
name: z.string().trim().min(1).max(120),
|
|
63
62
|
publicKey: publicClientKeySchema,
|
|
@@ -169,6 +168,8 @@ export const installationSignetsResponseSchema = z.object({
|
|
|
169
168
|
signetId: z.uuid(),
|
|
170
169
|
integrationId: z.uuid(),
|
|
171
170
|
name: z.string(),
|
|
171
|
+
/** The enrollment that is current for this installation's key, if any. */
|
|
172
|
+
enrollmentId: z.uuid().nullable(),
|
|
172
173
|
owner: ownerRefSchema,
|
|
173
174
|
}),
|
|
174
175
|
),
|
|
@@ -236,3 +237,9 @@ export const installationSnapshotSchema = installationInquiriesResponseSchema.ex
|
|
|
236
237
|
);
|
|
237
238
|
/** The path a socket proof is bound to (POST), though the socket itself opens at the API origin. */
|
|
238
239
|
export const installationSocketAction = 'installationSocket';
|
|
240
|
+
/** An installation hands Kingdom the credential Kingdom uses to reach it (a Foundry's tunnel token). */
|
|
241
|
+
export const setInstallationCredentialSchema = z.strictObject({
|
|
242
|
+
integrationId: z.uuid(),
|
|
243
|
+
token: z.string().min(16).max(4096),
|
|
244
|
+
});
|
|
245
|
+
export const setInstallationCredentialResponseSchema = z.object({ credentialId: z.uuid() });
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
collectSignet,
|
|
6
|
+
enrollInstallationSignet,
|
|
7
|
+
generateSignetKey,
|
|
8
|
+
installationInquiries,
|
|
9
|
+
kingdomUrl,
|
|
10
|
+
registerInstallation,
|
|
11
|
+
requestRegistration,
|
|
12
|
+
saveCollectedSignet,
|
|
13
|
+
signetCredentialSchema,
|
|
14
|
+
} from './client';
|
|
15
|
+
import type { InstallationKind, OwnerRef, RegistrationTerms } from './contracts';
|
|
16
|
+
import { readPrivateJson, writePrivateJson } from './files';
|
|
17
|
+
import { type InstallationSnapshot, InstallationSocket } from './installationSocket';
|
|
18
|
+
|
|
19
|
+
/** Where an installation keeps what it holds for one Kingdom: its key and one file per Signet. */
|
|
20
|
+
export async function installationDirectory(root: string, url: string) {
|
|
21
|
+
const directory = join(root, new URL(kingdomUrl(url)).host);
|
|
22
|
+
const keyFile = join(directory, 'installation-key.json');
|
|
23
|
+
if (!existsSync(keyFile)) await writePrivateJson(keyFile, generateSignetKey());
|
|
24
|
+
return { directory, keyFile };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const signetCredentialFile = (directory: string, signetId: string) =>
|
|
28
|
+
join(directory, `signet-${signetId}.json`);
|
|
29
|
+
|
|
30
|
+
export type HeldSignet = {
|
|
31
|
+
signetId: string;
|
|
32
|
+
integrationId: string;
|
|
33
|
+
owner: OwnerRef;
|
|
34
|
+
credentialFile: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const heldEnrollment = async (credentialFile: string) => {
|
|
38
|
+
if (!existsSync(credentialFile)) return null;
|
|
39
|
+
const parsed = signetCredentialSchema.safeParse(
|
|
40
|
+
await readPrivateJson(credentialFile).catch(() => null),
|
|
41
|
+
);
|
|
42
|
+
return parsed.success ? parsed.data.enrollmentId : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const lockStaleMs = 60_000;
|
|
46
|
+
|
|
47
|
+
/** One holder at a time per directory, so two processes never enroll over each other. */
|
|
48
|
+
async function withHoldLock<T>(directory: string, fn: () => Promise<T>): Promise<T> {
|
|
49
|
+
const lock = join(directory, '.hold-lock');
|
|
50
|
+
for (const started = Date.now(); ; ) {
|
|
51
|
+
try {
|
|
52
|
+
await mkdir(lock);
|
|
53
|
+
break;
|
|
54
|
+
} catch {
|
|
55
|
+
const age = await stat(lock).then(
|
|
56
|
+
(info) => Date.now() - info.mtimeMs,
|
|
57
|
+
() => 0,
|
|
58
|
+
);
|
|
59
|
+
if (age > lockStaleMs) await rm(lock, { recursive: true, force: true });
|
|
60
|
+
else if (Date.now() - started > lockStaleMs) throw Error('Signet holder lock busy');
|
|
61
|
+
else await new Promise((resolve) => setTimeout(resolve, 100));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return await fn();
|
|
66
|
+
} finally {
|
|
67
|
+
await rm(lock, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Keeps a credential file for every Signet a snapshot lists: enrolls this installation's key when
|
|
73
|
+
* Kingdom reports no current enrollment for it, or when the file holds an enrollment Kingdom no
|
|
74
|
+
* longer reports as current (a key rotation, a revoked or expired enrollment). Files are never
|
|
75
|
+
* deleted here: a Signet missing from a snapshot (a paused integration) keeps its file.
|
|
76
|
+
*/
|
|
77
|
+
export async function holdInstallationSignets(
|
|
78
|
+
url: string,
|
|
79
|
+
keyFile: string,
|
|
80
|
+
directory: string,
|
|
81
|
+
signets: InstallationSnapshot['signets'],
|
|
82
|
+
): Promise<HeldSignet[]> {
|
|
83
|
+
return withHoldLock(directory, async () => {
|
|
84
|
+
const held: HeldSignet[] = [];
|
|
85
|
+
for (const signet of signets) {
|
|
86
|
+
const credentialFile = signetCredentialFile(directory, signet.signetId);
|
|
87
|
+
const enrollmentId = await heldEnrollment(credentialFile);
|
|
88
|
+
if (!enrollmentId || !signet.enrollmentId || enrollmentId !== signet.enrollmentId)
|
|
89
|
+
await saveCollectedSignet(
|
|
90
|
+
credentialFile,
|
|
91
|
+
url,
|
|
92
|
+
keyFile,
|
|
93
|
+
await enrollInstallationSignet(url, keyFile, signet.signetId),
|
|
94
|
+
);
|
|
95
|
+
held.push({
|
|
96
|
+
signetId: signet.signetId,
|
|
97
|
+
integrationId: signet.integrationId,
|
|
98
|
+
owner: signet.owner,
|
|
99
|
+
credentialFile,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return held;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type InstallationPairing = {
|
|
107
|
+
url: string;
|
|
108
|
+
root: string;
|
|
109
|
+
kind: InstallationKind;
|
|
110
|
+
name: string;
|
|
111
|
+
sourceId?: string;
|
|
112
|
+
terms: RegistrationTerms;
|
|
113
|
+
onReview: (review: { reviewCode: string; review: string; expiresAt: string }) => void;
|
|
114
|
+
confirmOwner?: (owner: { ownerName: string | null; owner: OwnerRef | null }) => Promise<boolean>;
|
|
115
|
+
/** Failures while waiting (a poll Kingdom refused, a dropped socket); waiting continues. */
|
|
116
|
+
onError?: (error: unknown) => void;
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
socketOptions?: { pollMs?: number; retryBaseMs?: number; authTimeoutMs?: number };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Registers this installation with Kingdom and asks to become an owner's integration. A person
|
|
123
|
+
* claims the review code in Kingdom; the installation hears the outcome over its socket (polling
|
|
124
|
+
* while the socket is down), confirms the owner, and collects its Signet.
|
|
125
|
+
*/
|
|
126
|
+
export async function pairInstallation(input: InstallationPairing) {
|
|
127
|
+
const url = kingdomUrl(input.url);
|
|
128
|
+
const { directory, keyFile } = await installationDirectory(input.root, url);
|
|
129
|
+
await registerInstallation(url, keyFile, {
|
|
130
|
+
kind: input.kind,
|
|
131
|
+
name: input.name,
|
|
132
|
+
...(input.sourceId ? { sourceId: input.sourceId } : {}),
|
|
133
|
+
});
|
|
134
|
+
input.signal?.throwIfAborted();
|
|
135
|
+
const baseline = await installationInquiries(url, keyFile);
|
|
136
|
+
const earlier = new Set(baseline.inquiries.map((inquiry) => inquiry.id));
|
|
137
|
+
const pending = await requestRegistration(url, keyFile, input.terms);
|
|
138
|
+
input.onReview({
|
|
139
|
+
reviewCode: pending.reviewCode,
|
|
140
|
+
review: `${url}/dashboard?reviewSignet=${pending.reviewCode}`,
|
|
141
|
+
expiresAt: pending.expiresAt,
|
|
142
|
+
});
|
|
143
|
+
const inquiry = await new Promise<InstallationSnapshot['inquiries'][number]>(
|
|
144
|
+
(resolve, reject) => {
|
|
145
|
+
const socket = new InstallationSocket({
|
|
146
|
+
url,
|
|
147
|
+
keyFile,
|
|
148
|
+
...input.socketOptions,
|
|
149
|
+
onError: input.onError,
|
|
150
|
+
onSnapshot: (snapshot) => {
|
|
151
|
+
const settled = settledInquiry(snapshot, earlier, baseline.declinedAt);
|
|
152
|
+
if (!settled) return;
|
|
153
|
+
socket.close();
|
|
154
|
+
if (settled instanceof Error) reject(settled);
|
|
155
|
+
else resolve(settled);
|
|
156
|
+
},
|
|
157
|
+
onRevoked: () => reject(Error('Kingdom revoked this installation')),
|
|
158
|
+
});
|
|
159
|
+
if (input.signal?.aborted) return reject(input.signal.reason);
|
|
160
|
+
input.signal?.addEventListener('abort', () => {
|
|
161
|
+
socket.close();
|
|
162
|
+
reject(input.signal!.reason);
|
|
163
|
+
});
|
|
164
|
+
socket.start();
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
if (input.confirmOwner && !(await input.confirmOwner(inquiry)))
|
|
168
|
+
throw Error('Not collected: the owner was not confirmed');
|
|
169
|
+
const collected = await collectSignet(url, keyFile, inquiry.id);
|
|
170
|
+
if (!collected) throw Error('Kingdom has not released the Signet yet; try again');
|
|
171
|
+
const credentialFile = signetCredentialFile(directory, collected.signetId);
|
|
172
|
+
await saveCollectedSignet(credentialFile, url, keyFile, collected);
|
|
173
|
+
return {
|
|
174
|
+
url,
|
|
175
|
+
keyFile,
|
|
176
|
+
directory,
|
|
177
|
+
credentialFile,
|
|
178
|
+
signetId: collected.signetId,
|
|
179
|
+
integrationId: collected.integrationId,
|
|
180
|
+
owner: collected.owner,
|
|
181
|
+
ownerName: inquiry.ownerName,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const settledInquiry = (
|
|
186
|
+
snapshot: InstallationSnapshot,
|
|
187
|
+
earlier: Set<string>,
|
|
188
|
+
earlierDeclinedAt: string | null,
|
|
189
|
+
) => {
|
|
190
|
+
if (snapshot.declinedAt && snapshot.declinedAt !== earlierDeclinedAt)
|
|
191
|
+
return Error('The registration was declined in Kingdom');
|
|
192
|
+
const inquiry = snapshot.inquiries.find(
|
|
193
|
+
(item) => item.type === 'registerIntegration' && !earlier.has(item.id),
|
|
194
|
+
);
|
|
195
|
+
if (inquiry?.status === 'approved') return inquiry;
|
|
196
|
+
if (inquiry && inquiry.status !== 'sent')
|
|
197
|
+
return Error(`The registration was ${inquiry.status} in Kingdom`);
|
|
198
|
+
if (!inquiry && !snapshot.pending)
|
|
199
|
+
return Error('The review code expired before it was claimed; pair again');
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
@@ -19,6 +19,8 @@ export type InstallationSocketOptions = {
|
|
|
19
19
|
pollMs?: number;
|
|
20
20
|
retryBaseMs?: number;
|
|
21
21
|
retryMaxMs?: number;
|
|
22
|
+
/** A Kingdom that never answers the proof (one without Installation sockets) is treated as down. */
|
|
23
|
+
authTimeoutMs?: number;
|
|
22
24
|
WebSocket?: typeof WebSocket;
|
|
23
25
|
};
|
|
24
26
|
|
|
@@ -36,6 +38,7 @@ export class InstallationSocket {
|
|
|
36
38
|
private pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
37
39
|
private pollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
40
|
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
private authTimer: ReturnType<typeof setTimeout> | null = null;
|
|
39
42
|
private authenticated = false;
|
|
40
43
|
private polling = false;
|
|
41
44
|
private pollGeneration = 0;
|
|
@@ -97,7 +100,8 @@ export class InstallationSocket {
|
|
|
97
100
|
try {
|
|
98
101
|
switch (frame.type) {
|
|
99
102
|
case 'connected':
|
|
100
|
-
this.
|
|
103
|
+
this.authTimer = setTimeout(() => socket.close(), this.options.authTimeoutMs ?? 10_000);
|
|
104
|
+
this.sendOn(socket, {
|
|
101
105
|
action: 'authenticateInstallation',
|
|
102
106
|
proof: await signetProof(
|
|
103
107
|
this.options.url,
|
|
@@ -107,6 +111,8 @@ export class InstallationSocket {
|
|
|
107
111
|
});
|
|
108
112
|
return;
|
|
109
113
|
case 'installation':
|
|
114
|
+
if (this.authTimer) clearTimeout(this.authTimer);
|
|
115
|
+
this.authTimer = null;
|
|
110
116
|
this.authenticated = true;
|
|
111
117
|
this.attempts = 0;
|
|
112
118
|
this.stopPolling();
|
|
@@ -152,6 +158,8 @@ export class InstallationSocket {
|
|
|
152
158
|
if (socket !== this.socket) return;
|
|
153
159
|
this.socket = null;
|
|
154
160
|
this.authenticated = false;
|
|
161
|
+
if (this.authTimer) clearTimeout(this.authTimer);
|
|
162
|
+
this.authTimer = null;
|
|
155
163
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
156
164
|
this.pingTimer = null;
|
|
157
165
|
if (this.stopped) return;
|
|
@@ -186,6 +194,8 @@ export class InstallationSocket {
|
|
|
186
194
|
|
|
187
195
|
private clearTimers() {
|
|
188
196
|
this.stopPolling();
|
|
197
|
+
if (this.authTimer) clearTimeout(this.authTimer);
|
|
198
|
+
this.authTimer = null;
|
|
189
199
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
190
200
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
191
201
|
this.pingTimer = null;
|