@openclaw/nextcloud-talk 2026.3.12 → 2026.5.1-beta.2

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 (60) hide show
  1. package/api.ts +1 -0
  2. package/channel-plugin-api.ts +1 -0
  3. package/contract-api.ts +4 -0
  4. package/doctor-contract-api.ts +1 -0
  5. package/index.ts +15 -12
  6. package/openclaw.plugin.json +804 -1
  7. package/package.json +31 -4
  8. package/runtime-api.ts +33 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/setup-entry.ts +13 -0
  11. package/src/accounts.ts +25 -42
  12. package/src/approval-auth.test.ts +17 -0
  13. package/src/approval-auth.ts +27 -0
  14. package/src/channel-api.ts +5 -0
  15. package/src/channel.adapters.ts +52 -0
  16. package/src/channel.core.test.ts +75 -0
  17. package/src/channel.lifecycle.test.ts +81 -0
  18. package/src/channel.ts +157 -388
  19. package/src/config-schema.ts +27 -22
  20. package/src/core.test.ts +397 -0
  21. package/src/doctor-contract.ts +9 -0
  22. package/src/doctor.test.ts +40 -0
  23. package/src/doctor.ts +10 -0
  24. package/src/gateway.ts +109 -0
  25. package/src/inbound.authz.test.ts +87 -22
  26. package/src/inbound.behavior.test.ts +202 -0
  27. package/src/inbound.ts +28 -26
  28. package/src/monitor-runtime.ts +138 -0
  29. package/src/monitor.replay.test.ts +238 -0
  30. package/src/monitor.test-harness.ts +2 -2
  31. package/src/monitor.ts +125 -155
  32. package/src/normalize.ts +7 -2
  33. package/src/policy.ts +23 -31
  34. package/src/replay-guard.ts +74 -11
  35. package/src/room-info.test.ts +116 -0
  36. package/src/room-info.ts +11 -3
  37. package/src/runtime.ts +6 -3
  38. package/src/secret-contract.ts +103 -0
  39. package/src/secret-input.ts +1 -10
  40. package/src/send.cfg-threading.test.ts +153 -0
  41. package/src/send.runtime.ts +8 -0
  42. package/src/send.ts +126 -106
  43. package/src/session-route.ts +40 -0
  44. package/src/setup-core.ts +248 -0
  45. package/src/setup-surface.ts +190 -0
  46. package/src/setup.test.ts +422 -0
  47. package/src/signature.ts +20 -10
  48. package/src/types.ts +19 -16
  49. package/tsconfig.json +16 -0
  50. package/src/accounts.test.ts +0 -30
  51. package/src/channel.startup.test.ts +0 -83
  52. package/src/config-schema.test.ts +0 -36
  53. package/src/format.ts +0 -79
  54. package/src/monitor.auth-order.test.ts +0 -28
  55. package/src/monitor.backend.test.ts +0 -27
  56. package/src/monitor.read-body.test.ts +0 -16
  57. package/src/onboarding.ts +0 -302
  58. package/src/policy.test.ts +0 -138
  59. package/src/replay-guard.test.ts +0 -70
  60. package/src/send.test.ts +0 -104
package/package.json CHANGED
@@ -1,15 +1,32 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.3.12",
3
+ "version": "2026.5.1-beta.2",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/openclaw/openclaw"
8
+ },
5
9
  "type": "module",
6
10
  "dependencies": {
7
- "zod": "^4.3.6"
11
+ "zod": "^4.4.1"
12
+ },
13
+ "devDependencies": {
14
+ "@openclaw/plugin-sdk": "workspace:*",
15
+ "openclaw": "workspace:*"
16
+ },
17
+ "peerDependencies": {
18
+ "openclaw": ">=2026.4.25"
19
+ },
20
+ "peerDependenciesMeta": {
21
+ "openclaw": {
22
+ "optional": true
23
+ }
8
24
  },
9
25
  "openclaw": {
10
26
  "extensions": [
11
27
  "./index.ts"
12
28
  ],
29
+ "setupEntry": "./setup-entry.ts",
13
30
  "channel": {
14
31
  "id": "nextcloud-talk",
15
32
  "label": "Nextcloud Talk",
@@ -26,8 +43,18 @@
26
43
  },
27
44
  "install": {
28
45
  "npmSpec": "@openclaw/nextcloud-talk",
29
- "localPath": "extensions/nextcloud-talk",
30
- "defaultChoice": "npm"
46
+ "defaultChoice": "npm",
47
+ "minHostVersion": ">=2026.4.10"
48
+ },
49
+ "compat": {
50
+ "pluginApi": ">=2026.4.25"
51
+ },
52
+ "build": {
53
+ "openclawVersion": "2026.5.1-beta.2"
54
+ },
55
+ "release": {
56
+ "publishToClawHub": true,
57
+ "publishToNpm": true
31
58
  }
32
59
  }
33
60
  }
package/runtime-api.ts ADDED
@@ -0,0 +1,33 @@
1
+ // Private runtime barrel for the bundled Nextcloud Talk extension.
2
+ // Keep this barrel thin and aligned with the local extension surface.
3
+
4
+ export type { AllowlistMatch } from "openclaw/plugin-sdk/allow-from";
5
+ export type { ChannelGroupContext } from "openclaw/plugin-sdk/channel-contract";
6
+ export { logInboundDrop } from "openclaw/plugin-sdk/channel-logging";
7
+ export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
8
+ export {
9
+ readStoreAllowFromForDmPolicy,
10
+ resolveDmGroupAccessWithCommandGate,
11
+ } from "openclaw/plugin-sdk/channel-policy";
12
+ export type {
13
+ BlockStreamingCoalesceConfig,
14
+ DmConfig,
15
+ DmPolicy,
16
+ GroupPolicy,
17
+ GroupToolPolicyConfig,
18
+ OpenClawConfig,
19
+ } from "openclaw/plugin-sdk/config-types";
20
+ export {
21
+ GROUP_POLICY_BLOCKED_LABEL,
22
+ resolveAllowlistProviderRuntimeGroupPolicy,
23
+ resolveDefaultGroupPolicy,
24
+ warnMissingProviderGroupPolicyFallbackOnce,
25
+ } from "openclaw/plugin-sdk/runtime-group-policy";
26
+ export { dispatchInboundReplyWithBase } from "openclaw/plugin-sdk/inbound-reply-dispatch";
27
+ export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";
28
+ export { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
29
+ export type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
30
+ export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
31
+ export type { SecretInput } from "openclaw/plugin-sdk/secret-input";
32
+ export { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
33
+ export { setNextcloudTalkRuntime } from "./src/runtime.js";
@@ -0,0 +1,5 @@
1
+ export {
2
+ channelSecrets,
3
+ collectRuntimeConfigAssignments,
4
+ secretTargetRegistryEntries,
5
+ } from "./src/secret-contract.js";
package/setup-entry.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./api.js",
7
+ exportName: "nextcloudTalkPlugin",
8
+ },
9
+ secrets: {
10
+ specifier: "./secret-contract-api.js",
11
+ exportName: "channelSecrets",
12
+ },
13
+ });
package/src/accounts.ts CHANGED
@@ -1,15 +1,20 @@
1
- import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
2
1
  import {
3
2
  createAccountListHelpers,
4
3
  DEFAULT_ACCOUNT_ID,
5
4
  normalizeAccountId,
6
5
  resolveAccountWithDefaultFallback,
7
- } from "openclaw/plugin-sdk/nextcloud-talk";
6
+ resolveMergedAccountConfig,
7
+ } from "openclaw/plugin-sdk/account-core";
8
+ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
9
+ import {
10
+ normalizeLowercaseStringOrEmpty,
11
+ normalizeOptionalString,
12
+ } from "openclaw/plugin-sdk/text-runtime";
8
13
  import { normalizeResolvedSecretInputString } from "./secret-input.js";
9
14
  import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
10
15
 
11
16
  function isTruthyEnvValue(value?: string): boolean {
12
- const normalized = (value ?? "").trim().toLowerCase();
17
+ const normalized = normalizeLowercaseStringOrEmpty(value);
13
18
  return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
14
19
  }
15
20
 
@@ -43,47 +48,30 @@ export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
43
48
  return ids;
44
49
  }
45
50
 
46
- function resolveAccountConfig(
47
- cfg: CoreConfig,
48
- accountId: string,
49
- ): NextcloudTalkAccountConfig | undefined {
50
- const accounts = cfg.channels?.["nextcloud-talk"]?.accounts;
51
- if (!accounts || typeof accounts !== "object") {
52
- return undefined;
53
- }
54
- const direct = accounts[accountId] as NextcloudTalkAccountConfig | undefined;
55
- if (direct) {
56
- return direct;
57
- }
58
- const normalized = normalizeAccountId(accountId);
59
- const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
60
- return matchKey ? (accounts[matchKey] as NextcloudTalkAccountConfig | undefined) : undefined;
61
- }
62
-
63
51
  function mergeNextcloudTalkAccountConfig(
64
52
  cfg: CoreConfig,
65
53
  accountId: string,
66
54
  ): NextcloudTalkAccountConfig {
67
- const {
68
- accounts: _ignored,
69
- defaultAccount: _ignoredDefaultAccount,
70
- ...base
71
- } = (cfg.channels?.["nextcloud-talk"] ?? {}) as NextcloudTalkAccountConfig & {
72
- accounts?: unknown;
73
- defaultAccount?: unknown;
74
- };
75
- const account = resolveAccountConfig(cfg, accountId) ?? {};
76
- return { ...base, ...account };
55
+ return resolveMergedAccountConfig<NextcloudTalkAccountConfig>({
56
+ channelConfig: cfg.channels?.["nextcloud-talk"] as NextcloudTalkAccountConfig | undefined,
57
+ accounts: cfg.channels?.["nextcloud-talk"]?.accounts as
58
+ | Record<string, Partial<NextcloudTalkAccountConfig>>
59
+ | undefined,
60
+ accountId,
61
+ omitKeys: ["defaultAccount"],
62
+ normalizeAccountId,
63
+ });
77
64
  }
78
65
 
79
66
  function resolveNextcloudTalkSecret(
80
67
  cfg: CoreConfig,
81
68
  opts: { accountId?: string },
82
69
  ): { secret: string; source: ResolvedNextcloudTalkAccount["secretSource"] } {
83
- const merged = mergeNextcloudTalkAccountConfig(cfg, opts.accountId ?? DEFAULT_ACCOUNT_ID);
70
+ const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
71
+ const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
84
72
 
85
- const envSecret = process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim();
86
- if (envSecret && (!opts.accountId || opts.accountId === DEFAULT_ACCOUNT_ID)) {
73
+ const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
74
+ if (envSecret && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
87
75
  return { secret: envSecret, source: "env" };
88
76
  }
89
77
 
@@ -100,7 +88,7 @@ function resolveNextcloudTalkSecret(
100
88
 
101
89
  const inlineSecret = normalizeResolvedSecretInputString({
102
90
  value: merged.botSecret,
103
- path: `channels.nextcloud-talk.accounts.${opts.accountId ?? DEFAULT_ACCOUNT_ID}.botSecret`,
91
+ path: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecret`,
104
92
  });
105
93
  if (inlineSecret) {
106
94
  return { secret: inlineSecret, source: "config" };
@@ -114,6 +102,7 @@ export function resolveNextcloudTalkAccount(params: {
114
102
  accountId?: string | null;
115
103
  }): ResolvedNextcloudTalkAccount {
116
104
  const baseEnabled = params.cfg.channels?.["nextcloud-talk"]?.enabled !== false;
105
+ const resolvedAccountId = params.accountId ?? resolveDefaultNextcloudTalkAccountId(params.cfg);
117
106
 
118
107
  const resolve = (accountId: string) => {
119
108
  const merged = mergeNextcloudTalkAccountConfig(params.cfg, accountId);
@@ -132,7 +121,7 @@ export function resolveNextcloudTalkAccount(params: {
132
121
  return {
133
122
  accountId,
134
123
  enabled,
135
- name: merged.name?.trim() || undefined,
124
+ name: normalizeOptionalString(merged.name),
136
125
  baseUrl,
137
126
  secret: secretResolution.secret,
138
127
  secretSource: secretResolution.source,
@@ -141,16 +130,10 @@ export function resolveNextcloudTalkAccount(params: {
141
130
  };
142
131
 
143
132
  return resolveAccountWithDefaultFallback({
144
- accountId: params.accountId,
133
+ accountId: resolvedAccountId,
145
134
  normalizeAccountId,
146
135
  resolvePrimary: resolve,
147
136
  hasCredential: (account) => account.secretSource !== "none",
148
137
  resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg),
149
138
  });
150
139
  }
151
-
152
- export function listEnabledNextcloudTalkAccounts(cfg: CoreConfig): ResolvedNextcloudTalkAccount[] {
153
- return listNextcloudTalkAccountIds(cfg)
154
- .map((accountId) => resolveNextcloudTalkAccount({ cfg, accountId }))
155
- .filter((account) => account.enabled);
156
- }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nextcloudTalkApprovalAuth } from "./approval-auth.js";
3
+
4
+ describe("nextcloudTalkApprovalAuth", () => {
5
+ it("matches Nextcloud Talk actor ids case-insensitively", () => {
6
+ const cfg = { channels: { "nextcloud-talk": { allowFrom: ["Owner"] } } };
7
+
8
+ expect(
9
+ nextcloudTalkApprovalAuth.authorizeActorAction({
10
+ cfg,
11
+ senderId: "owner",
12
+ action: "approve",
13
+ approvalKind: "exec",
14
+ }),
15
+ ).toEqual({ authorized: true });
16
+ });
17
+ });
@@ -0,0 +1,27 @@
1
+ import {
2
+ createResolvedApproverActionAuthAdapter,
3
+ resolveApprovalApprovers,
4
+ } from "openclaw/plugin-sdk/approval-auth-runtime";
5
+ import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
6
+ import { resolveNextcloudTalkAccount } from "./accounts.js";
7
+ import type { CoreConfig } from "./types.js";
8
+
9
+ function normalizeNextcloudTalkApproverId(value: string | number): string | undefined {
10
+ return normalizeOptionalLowercaseString(
11
+ String(value)
12
+ .trim()
13
+ .replace(/^(nextcloud-talk|nc-talk|nc):/i, ""),
14
+ );
15
+ }
16
+
17
+ export const nextcloudTalkApprovalAuth = createResolvedApproverActionAuthAdapter({
18
+ channelLabel: "Nextcloud Talk",
19
+ resolveApprovers: ({ cfg, accountId }) => {
20
+ const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId });
21
+ return resolveApprovalApprovers({
22
+ allowFrom: account.config.allowFrom,
23
+ normalizeApprover: normalizeNextcloudTalkApproverId,
24
+ });
25
+ },
26
+ normalizeSenderId: (value) => normalizeNextcloudTalkApproverId(value),
27
+ });
@@ -0,0 +1,5 @@
1
+ export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-plugin-common";
2
+ export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
3
+ export { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
4
+ export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
5
+ export { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
@@ -0,0 +1,52 @@
1
+ import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
2
+ import {
3
+ adaptScopedAccountAccessor,
4
+ createScopedChannelConfigAdapter,
5
+ createScopedDmSecurityResolver,
6
+ } from "openclaw/plugin-sdk/channel-config-helpers";
7
+ import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
8
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
9
+ import {
10
+ listNextcloudTalkAccountIds,
11
+ resolveDefaultNextcloudTalkAccountId,
12
+ resolveNextcloudTalkAccount,
13
+ type ResolvedNextcloudTalkAccount,
14
+ } from "./accounts.js";
15
+ import type { CoreConfig } from "./types.js";
16
+
17
+ export const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter<
18
+ ResolvedNextcloudTalkAccount,
19
+ ResolvedNextcloudTalkAccount,
20
+ CoreConfig
21
+ >({
22
+ sectionKey: "nextcloud-talk",
23
+ listAccountIds: listNextcloudTalkAccountIds,
24
+ resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
25
+ defaultAccountId: resolveDefaultNextcloudTalkAccountId,
26
+ clearBaseFields: ["botSecret", "botSecretFile", "baseUrl", "name"],
27
+ resolveAllowFrom: (account) => account.config.allowFrom,
28
+ formatAllowFrom: (allowFrom) =>
29
+ formatAllowFromLowercase({
30
+ allowFrom,
31
+ stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i,
32
+ }),
33
+ });
34
+
35
+ export const nextcloudTalkSecurityAdapter = {
36
+ resolveDmPolicy: createScopedDmSecurityResolver<ResolvedNextcloudTalkAccount>({
37
+ channelKey: "nextcloud-talk",
38
+ resolvePolicy: (account) => account.config.dmPolicy,
39
+ resolveAllowFrom: (account) => account.config.allowFrom,
40
+ policyPathSuffix: "dmPolicy",
41
+ normalizeEntry: (raw) =>
42
+ normalizeLowercaseStringOrEmpty(raw.trim().replace(/^(nextcloud-talk|nc-talk|nc):/i, "")),
43
+ }),
44
+ };
45
+
46
+ export const nextcloudTalkPairingTextAdapter = {
47
+ idLabel: "nextcloudUserId",
48
+ message: "OpenClaw: your access has been approved.",
49
+ normalizeAllowEntry: createPairingPrefixStripper(/^(nextcloud-talk|nc-talk|nc):/i, (entry) =>
50
+ normalizeLowercaseStringOrEmpty(entry),
51
+ ),
52
+ };
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ nextcloudTalkConfigAdapter,
4
+ nextcloudTalkPairingTextAdapter,
5
+ nextcloudTalkSecurityAdapter,
6
+ } from "./channel.adapters.js";
7
+ import { NextcloudTalkConfigSchema } from "./config-schema.js";
8
+ import type { CoreConfig } from "./types.js";
9
+
10
+ describe("nextcloud talk channel core", () => {
11
+ it("accepts SecretRef botSecret and apiPassword at top-level", () => {
12
+ const result = NextcloudTalkConfigSchema.safeParse({
13
+ baseUrl: "https://cloud.example.com",
14
+ botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
15
+ apiUser: "bot",
16
+ apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
17
+ });
18
+ expect(result.success).toBe(true);
19
+ });
20
+
21
+ it("accepts SecretRef botSecret and apiPassword on account", () => {
22
+ const result = NextcloudTalkConfigSchema.safeParse({
23
+ accounts: {
24
+ main: {
25
+ baseUrl: "https://cloud.example.com",
26
+ botSecret: {
27
+ source: "env",
28
+ provider: "default",
29
+ id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
30
+ },
31
+ apiUser: "bot",
32
+ apiPassword: {
33
+ source: "env",
34
+ provider: "default",
35
+ id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
36
+ },
37
+ },
38
+ },
39
+ });
40
+ expect(result.success).toBe(true);
41
+ });
42
+
43
+ it("normalizes trimmed DM allowlist prefixes to lowercase ids", () => {
44
+ const resolveDmPolicy = nextcloudTalkSecurityAdapter.resolveDmPolicy;
45
+ if (!resolveDmPolicy) {
46
+ throw new Error("resolveDmPolicy unavailable");
47
+ }
48
+
49
+ const cfg = {
50
+ channels: {
51
+ "nextcloud-talk": {
52
+ baseUrl: "https://cloud.example.com",
53
+ botSecret: "secret",
54
+ dmPolicy: "allowlist",
55
+ allowFrom: [" nc:User-Id "],
56
+ },
57
+ },
58
+ } as CoreConfig;
59
+
60
+ const result = resolveDmPolicy({
61
+ cfg,
62
+ account: nextcloudTalkConfigAdapter.resolveAccount(cfg, "default"),
63
+ });
64
+ if (!result) {
65
+ throw new Error("nextcloud-talk resolveDmPolicy returned null");
66
+ }
67
+
68
+ expect(result.policy).toBe("allowlist");
69
+ expect(result.allowFrom).toEqual([" nc:User-Id "]);
70
+ expect(result.normalizeEntry?.(" nc:User-Id ")).toBe("user-id");
71
+ expect(nextcloudTalkPairingTextAdapter.normalizeAllowEntry(" nextcloud-talk:User-Id ")).toBe(
72
+ "user-id",
73
+ );
74
+ });
75
+ });
@@ -0,0 +1,81 @@
1
+ import { createStartAccountContext } from "openclaw/plugin-sdk/channel-test-helpers";
2
+ import {
3
+ expectStopPendingUntilAbort,
4
+ startAccountAndTrackLifecycle,
5
+ waitForStartedMocks,
6
+ } from "openclaw/plugin-sdk/channel-test-helpers";
7
+ import { afterEach, describe, expect, it, vi } from "vitest";
8
+ import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
9
+
10
+ const hoisted = vi.hoisted(() => ({
11
+ monitorNextcloudTalkProvider: vi.fn(),
12
+ }));
13
+
14
+ vi.mock("./monitor-runtime.js", () => ({
15
+ monitorNextcloudTalkProvider: hoisted.monitorNextcloudTalkProvider,
16
+ }));
17
+
18
+ const { nextcloudTalkGatewayAdapter } = await import("./gateway.js");
19
+
20
+ function buildAccount(): ResolvedNextcloudTalkAccount {
21
+ return {
22
+ accountId: "default",
23
+ enabled: true,
24
+ baseUrl: "https://nextcloud.example.com",
25
+ secret: "secret", // pragma: allowlist secret
26
+ secretSource: "config", // pragma: allowlist secret
27
+ config: {
28
+ baseUrl: "https://nextcloud.example.com",
29
+ botSecret: "secret", // pragma: allowlist secret
30
+ webhookPath: "/nextcloud-talk-webhook",
31
+ webhookPort: 8788,
32
+ },
33
+ };
34
+ }
35
+
36
+ function mockStartedMonitor() {
37
+ const stop = vi.fn();
38
+ hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
39
+ return stop;
40
+ }
41
+
42
+ function startNextcloudAccount(abortSignal?: AbortSignal) {
43
+ return nextcloudTalkGatewayAdapter.startAccount!(
44
+ createStartAccountContext({
45
+ account: buildAccount(),
46
+ abortSignal,
47
+ }),
48
+ );
49
+ }
50
+
51
+ describe("nextcloud-talk startAccount lifecycle", () => {
52
+ afterEach(() => {
53
+ vi.clearAllMocks();
54
+ });
55
+
56
+ it("keeps startAccount pending until abort, then stops the monitor", async () => {
57
+ const stop = mockStartedMonitor();
58
+ const { abort, task, isSettled } = startAccountAndTrackLifecycle({
59
+ startAccount: nextcloudTalkGatewayAdapter.startAccount!,
60
+ account: buildAccount(),
61
+ });
62
+ await expectStopPendingUntilAbort({
63
+ waitForStarted: waitForStartedMocks(hoisted.monitorNextcloudTalkProvider),
64
+ isSettled,
65
+ abort,
66
+ task,
67
+ stop,
68
+ });
69
+ });
70
+
71
+ it("stops immediately when startAccount receives an already-aborted signal", async () => {
72
+ const stop = mockStartedMonitor();
73
+ const abort = new AbortController();
74
+ abort.abort();
75
+
76
+ await startNextcloudAccount(abort.signal);
77
+
78
+ expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
79
+ expect(stop).toHaveBeenCalledOnce();
80
+ });
81
+ });