@openclaw/nextcloud-talk 2026.5.2-beta.2 → 2026.5.3-beta.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.
Files changed (61) hide show
  1. package/dist/api.js +2 -0
  2. package/dist/channel-BVVRsVr5.js +1788 -0
  3. package/dist/channel-plugin-api.js +2 -0
  4. package/dist/contract-api.js +2 -0
  5. package/dist/doctor-contract-CYlB-4Bf.js +7 -0
  6. package/dist/doctor-contract-api.js +2 -0
  7. package/dist/index.js +22 -0
  8. package/dist/runtime-api-BcCzeRN9.js +15 -0
  9. package/dist/runtime-api.js +2 -0
  10. package/dist/secret-contract-api.js +2 -0
  11. package/dist/secret-contract-bczDw2-2.js +86 -0
  12. package/dist/setup-entry.js +15 -0
  13. package/package.json +14 -6
  14. package/api.ts +0 -1
  15. package/channel-plugin-api.ts +0 -1
  16. package/contract-api.ts +0 -4
  17. package/doctor-contract-api.ts +0 -1
  18. package/index.ts +0 -20
  19. package/runtime-api.ts +0 -33
  20. package/secret-contract-api.ts +0 -5
  21. package/setup-entry.ts +0 -13
  22. package/src/accounts.ts +0 -139
  23. package/src/approval-auth.test.ts +0 -17
  24. package/src/approval-auth.ts +0 -27
  25. package/src/channel-api.ts +0 -5
  26. package/src/channel.adapters.ts +0 -52
  27. package/src/channel.core.test.ts +0 -75
  28. package/src/channel.lifecycle.test.ts +0 -81
  29. package/src/channel.ts +0 -195
  30. package/src/config-schema.ts +0 -79
  31. package/src/core.test.ts +0 -397
  32. package/src/doctor-contract.ts +0 -9
  33. package/src/doctor.test.ts +0 -40
  34. package/src/doctor.ts +0 -10
  35. package/src/gateway.ts +0 -109
  36. package/src/inbound.authz.test.ts +0 -149
  37. package/src/inbound.behavior.test.ts +0 -202
  38. package/src/inbound.ts +0 -320
  39. package/src/monitor-runtime.ts +0 -138
  40. package/src/monitor.replay.test.ts +0 -279
  41. package/src/monitor.test-fixtures.ts +0 -30
  42. package/src/monitor.test-harness.ts +0 -59
  43. package/src/monitor.ts +0 -385
  44. package/src/normalize.ts +0 -44
  45. package/src/policy.ts +0 -180
  46. package/src/replay-guard.ts +0 -128
  47. package/src/room-info.test.ts +0 -116
  48. package/src/room-info.ts +0 -148
  49. package/src/runtime.ts +0 -9
  50. package/src/secret-contract.ts +0 -103
  51. package/src/secret-input.ts +0 -4
  52. package/src/send.cfg-threading.test.ts +0 -153
  53. package/src/send.runtime.ts +0 -8
  54. package/src/send.ts +0 -236
  55. package/src/session-route.ts +0 -40
  56. package/src/setup-core.ts +0 -248
  57. package/src/setup-surface.ts +0 -190
  58. package/src/setup.test.ts +0 -422
  59. package/src/signature.ts +0 -82
  60. package/src/types.ts +0 -193
  61. package/tsconfig.json +0 -16
package/src/core.test.ts DELETED
@@ -1,397 +0,0 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import {
6
- looksLikeNextcloudTalkTargetId,
7
- normalizeNextcloudTalkMessagingTarget,
8
- stripNextcloudTalkTargetPrefix,
9
- } from "./normalize.js";
10
- import { resolveNextcloudTalkAllowlistMatch, resolveNextcloudTalkGroupAllow } from "./policy.js";
11
- import { createNextcloudTalkReplayGuard } from "./replay-guard.js";
12
- import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
13
- import {
14
- extractNextcloudTalkHeaders,
15
- generateNextcloudTalkSignature,
16
- verifyNextcloudTalkSignature,
17
- } from "./signature.js";
18
-
19
- const tempDirs: string[] = [];
20
-
21
- afterEach(async () => {
22
- while (tempDirs.length > 0) {
23
- const dir = tempDirs.pop();
24
- if (dir) {
25
- await rm(dir, { recursive: true, force: true });
26
- }
27
- }
28
- });
29
-
30
- async function makeTempDir(): Promise<string> {
31
- const dir = await mkdtemp(path.join(os.tmpdir(), "nextcloud-talk-replay-"));
32
- tempDirs.push(dir);
33
- return dir;
34
- }
35
-
36
- describe("nextcloud talk core", () => {
37
- it("builds an outbound session route for normalized room targets", () => {
38
- const route = resolveNextcloudTalkOutboundSessionRoute({
39
- cfg: {},
40
- agentId: "main",
41
- accountId: "acct-1",
42
- target: "nextcloud-talk:room-123",
43
- });
44
-
45
- expect(route).toMatchObject({
46
- peer: {
47
- kind: "group",
48
- id: "room-123",
49
- },
50
- from: "nextcloud-talk:room:room-123",
51
- to: "nextcloud-talk:room-123",
52
- });
53
- });
54
-
55
- it("returns null when the target cannot be normalized to a room id", () => {
56
- expect(
57
- resolveNextcloudTalkOutboundSessionRoute({
58
- cfg: {},
59
- agentId: "main",
60
- accountId: "acct-1",
61
- target: "",
62
- }),
63
- ).toBeNull();
64
- });
65
-
66
- it("normalizes and recognizes supported room target formats", () => {
67
- expect(stripNextcloudTalkTargetPrefix(" room:abc123 ")).toBe("abc123");
68
- expect(stripNextcloudTalkTargetPrefix("nextcloud-talk:room:AbC123")).toBe("AbC123");
69
- expect(stripNextcloudTalkTargetPrefix("nc-talk:room:ops")).toBe("ops");
70
- expect(stripNextcloudTalkTargetPrefix("nc:room:ops")).toBe("ops");
71
- expect(stripNextcloudTalkTargetPrefix("room: ")).toBeUndefined();
72
-
73
- expect(normalizeNextcloudTalkMessagingTarget("room:AbC123")).toBe("nextcloud-talk:abc123");
74
- expect(normalizeNextcloudTalkMessagingTarget("nc-talk:room:Ops")).toBe("nextcloud-talk:ops");
75
-
76
- expect(looksLikeNextcloudTalkTargetId("nextcloud-talk:room:abc12345")).toBe(true);
77
- expect(looksLikeNextcloudTalkTargetId("nc:opsroom1")).toBe(true);
78
- expect(looksLikeNextcloudTalkTargetId("abc12345")).toBe(true);
79
- expect(looksLikeNextcloudTalkTargetId("")).toBe(false);
80
- });
81
-
82
- it("verifies generated signatures and extracts normalized headers", () => {
83
- const body = JSON.stringify({ hello: "world" });
84
- const generated = generateNextcloudTalkSignature({
85
- body,
86
- secret: "secret-123",
87
- });
88
-
89
- expect(generated.random).toMatch(/^[0-9a-f]{64}$/);
90
- expect(generated.signature).toMatch(/^[0-9a-f]{64}$/);
91
- expect(
92
- verifyNextcloudTalkSignature({
93
- signature: generated.signature,
94
- random: generated.random,
95
- body,
96
- secret: "secret-123",
97
- }),
98
- ).toBe(true);
99
- expect(
100
- verifyNextcloudTalkSignature({
101
- signature: "",
102
- random: "abc",
103
- body: "body",
104
- secret: "secret",
105
- }),
106
- ).toBe(false);
107
- expect(
108
- verifyNextcloudTalkSignature({
109
- signature: "deadbeef",
110
- random: "abc",
111
- body: "body",
112
- secret: "secret",
113
- }),
114
- ).toBe(false);
115
-
116
- expect(
117
- extractNextcloudTalkHeaders({
118
- "x-nextcloud-talk-signature": "sig",
119
- "x-nextcloud-talk-random": "rand",
120
- "x-nextcloud-talk-backend": "backend",
121
- }),
122
- ).toEqual({
123
- signature: "sig",
124
- random: "rand",
125
- backend: "backend",
126
- });
127
- expect(
128
- extractNextcloudTalkHeaders({
129
- "X-Nextcloud-Talk-Signature": "sig",
130
- }),
131
- ).toBeNull();
132
- });
133
-
134
- it("rejects tampered bodies, wrong secrets, and tampered signatures", () => {
135
- const body = JSON.stringify({ hello: "world" });
136
- const generated = generateNextcloudTalkSignature({
137
- body,
138
- secret: "secret-123",
139
- });
140
-
141
- expect(
142
- verifyNextcloudTalkSignature({
143
- signature: generated.signature,
144
- random: generated.random,
145
- body: JSON.stringify({ hello: "tampered" }),
146
- secret: "secret-123",
147
- }),
148
- ).toBe(false);
149
- expect(
150
- verifyNextcloudTalkSignature({
151
- signature: generated.signature,
152
- random: generated.random,
153
- body,
154
- secret: "wrong-secret",
155
- }),
156
- ).toBe(false);
157
- expect(
158
- verifyNextcloudTalkSignature({
159
- signature: "a".repeat(generated.signature.length),
160
- random: generated.random,
161
- body,
162
- secret: "secret-123",
163
- }),
164
- ).toBe(false);
165
- });
166
-
167
- it("takes the first value from array-backed headers", () => {
168
- expect(
169
- extractNextcloudTalkHeaders({
170
- "x-nextcloud-talk-signature": ["sig1", "sig2"],
171
- "x-nextcloud-talk-random": ["rand1", "rand2"],
172
- "x-nextcloud-talk-backend": ["backend1", "backend2"],
173
- }),
174
- ).toEqual({
175
- signature: "sig1",
176
- random: "rand1",
177
- backend: "backend1",
178
- });
179
- });
180
-
181
- it("still runs timingSafeEqual when the supplied signature length mismatches", async () => {
182
- const timingSafeEqualMock = vi.fn();
183
-
184
- vi.resetModules();
185
- vi.doMock("node:crypto", async (importOriginal) => {
186
- const actual = await importOriginal<typeof import("node:crypto")>();
187
- return {
188
- ...actual,
189
- timingSafeEqual: vi.fn((left: NodeJS.ArrayBufferView, right: NodeJS.ArrayBufferView) => {
190
- timingSafeEqualMock(left, right);
191
- return actual.timingSafeEqual(left, right);
192
- }),
193
- };
194
- });
195
-
196
- const { generateNextcloudTalkSignature, verifyNextcloudTalkSignature } =
197
- await import("./signature.js");
198
- const body = JSON.stringify({ hello: "world" });
199
- const generated = generateNextcloudTalkSignature({
200
- body,
201
- secret: "secret-123",
202
- });
203
- const shortSignature = generated.signature.slice(0, 12);
204
-
205
- expect(
206
- verifyNextcloudTalkSignature({
207
- signature: shortSignature,
208
- random: generated.random,
209
- body,
210
- secret: "secret-123",
211
- }),
212
- ).toBe(false);
213
-
214
- expect(timingSafeEqualMock).toHaveBeenCalledOnce();
215
- const [leftBuffer, rightBuffer] = timingSafeEqualMock.mock.calls[0] ?? [];
216
- expect(Buffer.isBuffer(leftBuffer)).toBe(true);
217
- expect(Buffer.isBuffer(rightBuffer)).toBe(true);
218
- if (!Buffer.isBuffer(leftBuffer) || !Buffer.isBuffer(rightBuffer)) {
219
- throw new TypeError("Expected timingSafeEqual to receive Buffer arguments");
220
- }
221
- expect(leftBuffer).toHaveLength(rightBuffer.length);
222
-
223
- vi.doUnmock("node:crypto");
224
- vi.resetModules();
225
- });
226
-
227
- it("persists replay decisions across guard instances and scopes account namespaces", async () => {
228
- const stateDir = await makeTempDir();
229
-
230
- const firstGuard = createNextcloudTalkReplayGuard({ stateDir });
231
- const firstAttempt = await firstGuard.shouldProcessMessage({
232
- accountId: "account-a",
233
- roomToken: "room-1",
234
- messageId: "msg-1",
235
- });
236
- const replayAttempt = await firstGuard.shouldProcessMessage({
237
- accountId: "account-a",
238
- roomToken: "room-1",
239
- messageId: "msg-1",
240
- });
241
-
242
- const secondGuard = createNextcloudTalkReplayGuard({ stateDir });
243
- const restartReplayAttempt = await secondGuard.shouldProcessMessage({
244
- accountId: "account-a",
245
- roomToken: "room-1",
246
- messageId: "msg-1",
247
- });
248
- const otherAccountFirstAttempt = await secondGuard.shouldProcessMessage({
249
- accountId: "account-b",
250
- roomToken: "room-1",
251
- messageId: "msg-1",
252
- });
253
-
254
- expect(firstAttempt).toBe(true);
255
- expect(replayAttempt).toBe(false);
256
- expect(restartReplayAttempt).toBe(false);
257
- expect(otherAccountFirstAttempt).toBe(true);
258
- });
259
-
260
- it("releases in-flight replay claims when processing fails", async () => {
261
- const guard = createNextcloudTalkReplayGuard({});
262
-
263
- const firstClaim = await guard.claimMessage({
264
- accountId: "account-a",
265
- roomToken: "room-1",
266
- messageId: "msg-claim",
267
- });
268
- const secondClaim = await guard.claimMessage({
269
- accountId: "account-a",
270
- roomToken: "room-1",
271
- messageId: "msg-claim",
272
- });
273
-
274
- expect(firstClaim).toBe("claimed");
275
- expect(secondClaim).toBe("inflight");
276
-
277
- guard.releaseMessage({
278
- accountId: "account-a",
279
- roomToken: "room-1",
280
- messageId: "msg-claim",
281
- error: new Error("transient"),
282
- });
283
-
284
- const retryClaim = await guard.claimMessage({
285
- accountId: "account-a",
286
- roomToken: "room-1",
287
- messageId: "msg-claim",
288
- });
289
- expect(retryClaim).toBe("claimed");
290
- });
291
-
292
- it("resolves allowlist matches and group policy decisions", () => {
293
- expect(
294
- resolveNextcloudTalkAllowlistMatch({
295
- allowFrom: ["*"],
296
- senderId: "user-id",
297
- }).allowed,
298
- ).toBe(true);
299
- expect(
300
- resolveNextcloudTalkAllowlistMatch({
301
- allowFrom: ["nc:User-Id"],
302
- senderId: "user-id",
303
- }),
304
- ).toEqual({ allowed: true, matchKey: "user-id", matchSource: "id" });
305
- expect(
306
- resolveNextcloudTalkAllowlistMatch({
307
- allowFrom: ["allowed"],
308
- senderId: "other",
309
- }).allowed,
310
- ).toBe(false);
311
-
312
- expect(
313
- resolveNextcloudTalkGroupAllow({
314
- groupPolicy: "disabled",
315
- outerAllowFrom: ["owner"],
316
- innerAllowFrom: ["room-user"],
317
- senderId: "owner",
318
- }),
319
- ).toEqual({
320
- allowed: false,
321
- outerMatch: { allowed: false },
322
- innerMatch: { allowed: false },
323
- });
324
- expect(
325
- resolveNextcloudTalkGroupAllow({
326
- groupPolicy: "open",
327
- outerAllowFrom: [],
328
- innerAllowFrom: [],
329
- senderId: "owner",
330
- }),
331
- ).toEqual({
332
- allowed: true,
333
- outerMatch: { allowed: true },
334
- innerMatch: { allowed: true },
335
- });
336
- expect(
337
- resolveNextcloudTalkGroupAllow({
338
- groupPolicy: "allowlist",
339
- outerAllowFrom: [],
340
- innerAllowFrom: [],
341
- senderId: "owner",
342
- }),
343
- ).toEqual({
344
- allowed: false,
345
- outerMatch: { allowed: false },
346
- innerMatch: { allowed: false },
347
- });
348
- expect(
349
- resolveNextcloudTalkGroupAllow({
350
- groupPolicy: "allowlist",
351
- outerAllowFrom: [],
352
- innerAllowFrom: ["room-user"],
353
- senderId: "room-user",
354
- }),
355
- ).toEqual({
356
- allowed: true,
357
- outerMatch: { allowed: false },
358
- innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
359
- });
360
- expect(
361
- resolveNextcloudTalkGroupAllow({
362
- groupPolicy: "allowlist",
363
- outerAllowFrom: ["team-owner"],
364
- innerAllowFrom: ["room-user"],
365
- senderId: "room-user",
366
- }),
367
- ).toEqual({
368
- allowed: false,
369
- outerMatch: { allowed: false },
370
- innerMatch: { allowed: true, matchKey: "room-user", matchSource: "id" },
371
- });
372
- expect(
373
- resolveNextcloudTalkGroupAllow({
374
- groupPolicy: "allowlist",
375
- outerAllowFrom: ["team-owner"],
376
- innerAllowFrom: ["room-user"],
377
- senderId: "team-owner",
378
- }),
379
- ).toEqual({
380
- allowed: false,
381
- outerMatch: { allowed: true, matchKey: "team-owner", matchSource: "id" },
382
- innerMatch: { allowed: false },
383
- });
384
- expect(
385
- resolveNextcloudTalkGroupAllow({
386
- groupPolicy: "allowlist",
387
- outerAllowFrom: ["shared-user"],
388
- innerAllowFrom: ["shared-user"],
389
- senderId: "shared-user",
390
- }),
391
- ).toEqual({
392
- allowed: true,
393
- outerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
394
- innerMatch: { allowed: true, matchKey: "shared-user", matchSource: "id" },
395
- });
396
- });
397
- });
@@ -1,9 +0,0 @@
1
- import { createLegacyPrivateNetworkDoctorContract } from "openclaw/plugin-sdk/ssrf-runtime";
2
-
3
- const contract = createLegacyPrivateNetworkDoctorContract({
4
- channelKey: "nextcloud-talk",
5
- });
6
-
7
- export const legacyConfigRules = contract.legacyConfigRules;
8
-
9
- export const normalizeCompatibilityConfig = contract.normalizeCompatibilityConfig;
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { nextcloudTalkDoctor } from "./doctor.js";
3
-
4
- describe("nextcloud-talk doctor", () => {
5
- it("normalizes legacy private-network aliases", () => {
6
- const normalize = nextcloudTalkDoctor.normalizeCompatibilityConfig;
7
- expect(normalize).toBeDefined();
8
- if (!normalize) {
9
- return;
10
- }
11
-
12
- const result = normalize({
13
- cfg: {
14
- channels: {
15
- "nextcloud-talk": {
16
- allowPrivateNetwork: true,
17
- accounts: {
18
- work: {
19
- allowPrivateNetwork: false,
20
- },
21
- },
22
- },
23
- },
24
- } as never,
25
- });
26
-
27
- expect(result.config.channels?.["nextcloud-talk"]?.network).toEqual({
28
- dangerouslyAllowPrivateNetwork: true,
29
- });
30
- expect(
31
- (
32
- result.config.channels?.["nextcloud-talk"]?.accounts?.work as
33
- | { network?: Record<string, unknown> }
34
- | undefined
35
- )?.network,
36
- ).toEqual({
37
- dangerouslyAllowPrivateNetwork: false,
38
- });
39
- });
40
- });
package/src/doctor.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
2
- import {
3
- legacyConfigRules as NEXTCLOUD_TALK_LEGACY_CONFIG_RULES,
4
- normalizeCompatibilityConfig as normalizeNextcloudTalkCompatibilityConfig,
5
- } from "./doctor-contract.js";
6
-
7
- export const nextcloudTalkDoctor: ChannelDoctorAdapter = {
8
- legacyConfigRules: NEXTCLOUD_TALK_LEGACY_CONFIG_RULES,
9
- normalizeCompatibilityConfig: normalizeNextcloudTalkCompatibilityConfig,
10
- };
package/src/gateway.ts DELETED
@@ -1,109 +0,0 @@
1
- import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle";
2
- import { runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared";
3
- import { resolveNextcloudTalkAccount, type ResolvedNextcloudTalkAccount } from "./accounts.js";
4
- import {
5
- clearAccountEntryFields,
6
- DEFAULT_ACCOUNT_ID,
7
- type ChannelPlugin,
8
- type OpenClawConfig,
9
- } from "./channel-api.js";
10
- import { monitorNextcloudTalkProvider } from "./monitor-runtime.js";
11
- import { getNextcloudTalkRuntime } from "./runtime.js";
12
- import type { CoreConfig } from "./types.js";
13
-
14
- export const nextcloudTalkGatewayAdapter: NonNullable<
15
- ChannelPlugin<ResolvedNextcloudTalkAccount>["gateway"]
16
- > = {
17
- startAccount: async (ctx) => {
18
- const account = ctx.account;
19
- if (!account.secret || !account.baseUrl) {
20
- throw new Error(
21
- `Nextcloud Talk not configured for account "${account.accountId}" (missing secret or baseUrl)`,
22
- );
23
- }
24
-
25
- ctx.log?.info(`[${account.accountId}] starting Nextcloud Talk webhook server`);
26
-
27
- const statusSink = createAccountStatusSink({
28
- accountId: ctx.accountId,
29
- setStatus: ctx.setStatus,
30
- });
31
-
32
- await runStoppablePassiveMonitor({
33
- abortSignal: ctx.abortSignal,
34
- start: async () =>
35
- await monitorNextcloudTalkProvider({
36
- accountId: account.accountId,
37
- config: ctx.cfg as CoreConfig,
38
- runtime: ctx.runtime,
39
- abortSignal: ctx.abortSignal,
40
- statusSink,
41
- }),
42
- });
43
- },
44
- logoutAccount: async ({ accountId, cfg }) => {
45
- const nextCfg = { ...cfg } as OpenClawConfig;
46
- const nextSection = cfg.channels?.["nextcloud-talk"]
47
- ? { ...cfg.channels["nextcloud-talk"] }
48
- : undefined;
49
- let cleared = false;
50
- let changed = false;
51
-
52
- if (nextSection) {
53
- if (accountId === DEFAULT_ACCOUNT_ID && nextSection.botSecret) {
54
- delete nextSection.botSecret;
55
- cleared = true;
56
- changed = true;
57
- }
58
- const accountCleanup = clearAccountEntryFields({
59
- accounts: nextSection.accounts as Record<string, object> | undefined,
60
- accountId,
61
- fields: ["botSecret"],
62
- });
63
- if (accountCleanup.changed) {
64
- changed = true;
65
- if (accountCleanup.cleared) {
66
- cleared = true;
67
- }
68
- if (accountCleanup.nextAccounts) {
69
- nextSection.accounts = accountCleanup.nextAccounts as Record<string, unknown>;
70
- } else {
71
- delete nextSection.accounts;
72
- }
73
- }
74
- }
75
-
76
- if (changed) {
77
- if (nextSection && Object.keys(nextSection).length > 0) {
78
- nextCfg.channels = { ...nextCfg.channels, "nextcloud-talk": nextSection };
79
- } else {
80
- const nextChannels = { ...nextCfg.channels } as Record<string, unknown>;
81
- delete nextChannels["nextcloud-talk"];
82
- if (Object.keys(nextChannels).length > 0) {
83
- nextCfg.channels = nextChannels as OpenClawConfig["channels"];
84
- } else {
85
- delete nextCfg.channels;
86
- }
87
- }
88
- }
89
-
90
- const resolved = resolveNextcloudTalkAccount({
91
- cfg: changed ? (nextCfg as CoreConfig) : (cfg as CoreConfig),
92
- accountId,
93
- });
94
- const loggedOut = resolved.secretSource === "none";
95
-
96
- if (changed) {
97
- await getNextcloudTalkRuntime().config.replaceConfigFile({
98
- nextConfig: nextCfg,
99
- afterWrite: { mode: "auto" },
100
- });
101
- }
102
-
103
- return {
104
- cleared,
105
- envSecret: Boolean(process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()),
106
- loggedOut,
107
- };
108
- },
109
- };