@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.
- package/api.ts +1 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +4 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +804 -1
- package/package.json +31 -4
- package/runtime-api.ts +33 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.ts +25 -42
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +27 -0
- package/src/channel-api.ts +5 -0
- package/src/channel.adapters.ts +52 -0
- package/src/channel.core.test.ts +75 -0
- package/src/channel.lifecycle.test.ts +81 -0
- package/src/channel.ts +157 -388
- package/src/config-schema.ts +27 -22
- package/src/core.test.ts +397 -0
- package/src/doctor-contract.ts +9 -0
- package/src/doctor.test.ts +40 -0
- package/src/doctor.ts +10 -0
- package/src/gateway.ts +109 -0
- package/src/inbound.authz.test.ts +87 -22
- package/src/inbound.behavior.test.ts +202 -0
- package/src/inbound.ts +28 -26
- package/src/monitor-runtime.ts +138 -0
- package/src/monitor.replay.test.ts +238 -0
- package/src/monitor.test-harness.ts +2 -2
- package/src/monitor.ts +125 -155
- package/src/normalize.ts +7 -2
- package/src/policy.ts +23 -31
- package/src/replay-guard.ts +74 -11
- package/src/room-info.test.ts +116 -0
- package/src/room-info.ts +11 -3
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +103 -0
- package/src/secret-input.ts +1 -10
- package/src/send.cfg-threading.test.ts +153 -0
- package/src/send.runtime.ts +8 -0
- package/src/send.ts +126 -106
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +248 -0
- package/src/setup-surface.ts +190 -0
- package/src/setup.test.ts +422 -0
- package/src/signature.ts +20 -10
- package/src/types.ts +19 -16
- package/tsconfig.json +16 -0
- package/src/accounts.test.ts +0 -30
- package/src/channel.startup.test.ts +0 -83
- package/src/config-schema.test.ts +0 -36
- package/src/format.ts +0 -79
- package/src/monitor.auth-order.test.ts +0 -28
- package/src/monitor.backend.test.ts +0 -27
- package/src/monitor.read-body.test.ts +0 -16
- package/src/onboarding.ts +0 -302
- package/src/policy.test.ts +0 -138
- package/src/replay-guard.test.ts +0 -70
- 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
|
+
"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.
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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";
|
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
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
defaultAccount
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
70
|
+
const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
|
|
71
|
+
const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
|
|
84
72
|
|
|
85
|
-
const envSecret = process.env.NEXTCLOUD_TALK_BOT_SECRET
|
|
86
|
-
if (envSecret &&
|
|
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.${
|
|
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
|
|
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:
|
|
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
|
+
});
|