@kodelyth/nextcloud-talk 2026.5.42 → 2026.6.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/klaw.plugin.json +799 -2
- package/package.json +16 -4
- package/api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -4
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -29
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -31
- package/src/accounts.ts +0 -149
- package/src/api-credentials.ts +0 -31
- package/src/approval-auth.test.ts +0 -17
- package/src/approval-auth.ts +0 -27
- package/src/bot-preflight.test.ts +0 -135
- package/src/bot-preflight.ts +0 -183
- package/src/channel-api.ts +0 -5
- package/src/channel.adapters.ts +0 -52
- package/src/channel.core.test.ts +0 -75
- package/src/channel.lifecycle.test.ts +0 -91
- package/src/channel.status.test.ts +0 -28
- package/src/channel.ts +0 -225
- package/src/config-schema.ts +0 -79
- package/src/core.test.ts +0 -325
- package/src/doctor-contract.ts +0 -9
- package/src/doctor.test.ts +0 -87
- package/src/doctor.ts +0 -40
- package/src/gateway.ts +0 -109
- package/src/inbound.authz.test.ts +0 -146
- package/src/inbound.behavior.test.ts +0 -309
- package/src/inbound.ts +0 -392
- package/src/message-actions.test.ts +0 -270
- package/src/message-actions.ts +0 -82
- package/src/message-adapter.ts +0 -28
- package/src/monitor-runtime.ts +0 -138
- package/src/monitor.replay.test.ts +0 -276
- package/src/monitor.test-fixtures.ts +0 -30
- package/src/monitor.test-harness.ts +0 -59
- package/src/monitor.ts +0 -385
- package/src/normalize.ts +0 -44
- package/src/policy.ts +0 -111
- package/src/replay-guard.ts +0 -128
- package/src/room-info.test.ts +0 -160
- package/src/room-info.ts +0 -130
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -103
- package/src/secret-input.ts +0 -4
- package/src/send.cfg-threading.test.ts +0 -359
- package/src/send.runtime.ts +0 -8
- package/src/send.ts +0 -269
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -250
- package/src/setup-surface.ts +0 -195
- package/src/setup.test.ts +0 -445
- package/src/signature.ts +0 -82
- package/src/types.ts +0 -195
- package/tsconfig.json +0 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kodelyth/nextcloud-talk",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.6.1",
|
|
4
4
|
"description": "Klaw Nextcloud Talk channel plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"@kodelyth/klaw": "2026.5.42"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@kodelyth/klaw": ">=2026.5.19"
|
|
15
|
+
"@kodelyth/klaw": ">=2026.5.19",
|
|
16
|
+
"klaw": ">=2026.5.39"
|
|
16
17
|
},
|
|
17
18
|
"peerDependenciesMeta": {
|
|
18
19
|
"@kodelyth/klaw": {
|
|
19
20
|
"optional": true
|
|
21
|
+
},
|
|
22
|
+
"klaw": {
|
|
23
|
+
"optional": true
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"klaw": {
|
|
@@ -52,9 +56,17 @@
|
|
|
52
56
|
"release": {
|
|
53
57
|
"publishToClawHub": true,
|
|
54
58
|
"publishToNpm": true
|
|
55
|
-
}
|
|
59
|
+
},
|
|
60
|
+
"runtimeExtensions": [
|
|
61
|
+
"./dist/index.js"
|
|
62
|
+
],
|
|
63
|
+
"runtimeSetupEntry": "./dist/setup-entry.js"
|
|
56
64
|
},
|
|
57
65
|
"dependencies": {
|
|
58
66
|
"zod": "4.4.3"
|
|
59
|
-
}
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist/**",
|
|
70
|
+
"klaw.plugin.json"
|
|
71
|
+
]
|
|
60
72
|
}
|
package/api.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { nextcloudTalkPlugin } from "./src/channel.js";
|
package/channel-plugin-api.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { nextcloudTalkPlugin } from "./src/channel.js";
|
package/contract-api.ts
DELETED
package/doctor-contract-api.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
package/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { defineBundledChannelEntry } from "klaw/plugin-sdk/channel-entry-contract";
|
|
2
|
-
|
|
3
|
-
export default defineBundledChannelEntry({
|
|
4
|
-
id: "nextcloud-talk",
|
|
5
|
-
name: "Nextcloud Talk",
|
|
6
|
-
description: "Nextcloud Talk channel plugin",
|
|
7
|
-
importMetaUrl: import.meta.url,
|
|
8
|
-
plugin: {
|
|
9
|
-
specifier: "./channel-plugin-api.js",
|
|
10
|
-
exportName: "nextcloudTalkPlugin",
|
|
11
|
-
},
|
|
12
|
-
secrets: {
|
|
13
|
-
specifier: "./secret-contract-api.js",
|
|
14
|
-
exportName: "channelSecrets",
|
|
15
|
-
},
|
|
16
|
-
runtime: {
|
|
17
|
-
specifier: "./runtime-api.js",
|
|
18
|
-
exportName: "setNextcloudTalkRuntime",
|
|
19
|
-
},
|
|
20
|
-
});
|
package/runtime-api.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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 "klaw/plugin-sdk/allow-from";
|
|
5
|
-
export type { ChannelGroupContext } from "klaw/plugin-sdk/channel-contract";
|
|
6
|
-
export { logInboundDrop } from "klaw/plugin-sdk/channel-logging";
|
|
7
|
-
export { createChannelPairingController } from "klaw/plugin-sdk/channel-pairing";
|
|
8
|
-
export type {
|
|
9
|
-
BlockStreamingCoalesceConfig,
|
|
10
|
-
DmConfig,
|
|
11
|
-
DmPolicy,
|
|
12
|
-
GroupPolicy,
|
|
13
|
-
GroupToolPolicyConfig,
|
|
14
|
-
KlawConfig,
|
|
15
|
-
} from "klaw/plugin-sdk/config-contracts";
|
|
16
|
-
export {
|
|
17
|
-
GROUP_POLICY_BLOCKED_LABEL,
|
|
18
|
-
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
19
|
-
resolveDefaultGroupPolicy,
|
|
20
|
-
warnMissingProviderGroupPolicyFallbackOnce,
|
|
21
|
-
} from "klaw/plugin-sdk/runtime-group-policy";
|
|
22
|
-
export { createChannelMessageReplyPipeline } from "klaw/plugin-sdk/channel-message";
|
|
23
|
-
export type { OutboundReplyPayload } from "klaw/plugin-sdk/reply-payload";
|
|
24
|
-
export { deliverFormattedTextWithAttachments } from "klaw/plugin-sdk/reply-payload";
|
|
25
|
-
export type { PluginRuntime } from "klaw/plugin-sdk/runtime-store";
|
|
26
|
-
export type { RuntimeEnv } from "klaw/plugin-sdk/runtime";
|
|
27
|
-
export type { SecretInput } from "klaw/plugin-sdk/secret-input";
|
|
28
|
-
export { fetchWithSsrFGuard } from "klaw/plugin-sdk/ssrf-runtime";
|
|
29
|
-
export { setNextcloudTalkRuntime } from "./src/runtime.js";
|
package/secret-contract-api.ts
DELETED
package/setup-entry.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineBundledChannelSetupEntry } from "klaw/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.test.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
listNextcloudTalkAccountIds,
|
|
4
|
-
resolveDefaultNextcloudTalkAccountId,
|
|
5
|
-
resolveNextcloudTalkAccount,
|
|
6
|
-
} from "./accounts.js";
|
|
7
|
-
import type { CoreConfig } from "./types.js";
|
|
8
|
-
|
|
9
|
-
describe("Nextcloud Talk account resolution", () => {
|
|
10
|
-
it("preserves top-level default account when named accounts are configured", () => {
|
|
11
|
-
const cfg = {
|
|
12
|
-
channels: {
|
|
13
|
-
"nextcloud-talk": {
|
|
14
|
-
baseUrl: "https://cloud.example.com",
|
|
15
|
-
botSecret: "shared-secret",
|
|
16
|
-
accounts: {
|
|
17
|
-
work: { enabled: false },
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
} satisfies CoreConfig;
|
|
22
|
-
|
|
23
|
-
expect(listNextcloudTalkAccountIds(cfg)).toEqual(["default", "work"]);
|
|
24
|
-
expect(resolveDefaultNextcloudTalkAccountId(cfg)).toBe("default");
|
|
25
|
-
expect(resolveNextcloudTalkAccount({ cfg })).toMatchObject({
|
|
26
|
-
accountId: "default",
|
|
27
|
-
baseUrl: "https://cloud.example.com",
|
|
28
|
-
secret: "shared-secret",
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
});
|
package/src/accounts.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createAccountListHelpers,
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
hasConfiguredAccountValue,
|
|
5
|
-
normalizeAccountId,
|
|
6
|
-
resolveAccountWithDefaultFallback,
|
|
7
|
-
resolveMergedAccountConfig,
|
|
8
|
-
} from "klaw/plugin-sdk/account-core";
|
|
9
|
-
import { tryReadSecretFileSync } from "klaw/plugin-sdk/secret-file-runtime";
|
|
10
|
-
import {
|
|
11
|
-
normalizeLowercaseStringOrEmpty,
|
|
12
|
-
normalizeOptionalString,
|
|
13
|
-
} from "klaw/plugin-sdk/string-coerce-runtime";
|
|
14
|
-
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
15
|
-
import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
|
|
16
|
-
|
|
17
|
-
function isTruthyEnvValue(value?: string): boolean {
|
|
18
|
-
const normalized = normalizeLowercaseStringOrEmpty(value);
|
|
19
|
-
return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const debugAccounts = (...args: unknown[]) => {
|
|
23
|
-
if (isTruthyEnvValue(process.env.KLAW_DEBUG_NEXTCLOUD_TALK_ACCOUNTS)) {
|
|
24
|
-
console.warn("[nextcloud-talk:accounts]", ...args);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type ResolvedNextcloudTalkAccount = {
|
|
29
|
-
accountId: string;
|
|
30
|
-
enabled: boolean;
|
|
31
|
-
name?: string;
|
|
32
|
-
baseUrl: string;
|
|
33
|
-
secret: string;
|
|
34
|
-
secretSource: "env" | "secretFile" | "config" | "none";
|
|
35
|
-
config: NextcloudTalkAccountConfig;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const {
|
|
39
|
-
listAccountIds: listNextcloudTalkAccountIdsInternal,
|
|
40
|
-
resolveDefaultAccountId: resolveDefaultNextcloudTalkAccountId,
|
|
41
|
-
} = createAccountListHelpers("nextcloud-talk", {
|
|
42
|
-
normalizeAccountId,
|
|
43
|
-
hasImplicitDefaultAccount: (cfg) => {
|
|
44
|
-
const channel = cfg.channels?.["nextcloud-talk"];
|
|
45
|
-
return Boolean(
|
|
46
|
-
channel?.baseUrl?.trim() &&
|
|
47
|
-
(hasConfiguredAccountValue(channel.botSecret) ||
|
|
48
|
-
channel.botSecretFile?.trim() ||
|
|
49
|
-
process.env.NEXTCLOUD_TALK_BOT_SECRET?.trim()),
|
|
50
|
-
);
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
export { resolveDefaultNextcloudTalkAccountId };
|
|
54
|
-
|
|
55
|
-
export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
|
|
56
|
-
const ids = listNextcloudTalkAccountIdsInternal(cfg);
|
|
57
|
-
debugAccounts("listNextcloudTalkAccountIds", ids);
|
|
58
|
-
return ids;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function mergeNextcloudTalkAccountConfig(
|
|
62
|
-
cfg: CoreConfig,
|
|
63
|
-
accountId: string,
|
|
64
|
-
): NextcloudTalkAccountConfig {
|
|
65
|
-
return resolveMergedAccountConfig<NextcloudTalkAccountConfig>({
|
|
66
|
-
channelConfig: cfg.channels?.["nextcloud-talk"] as NextcloudTalkAccountConfig | undefined,
|
|
67
|
-
accounts: cfg.channels?.["nextcloud-talk"]?.accounts as
|
|
68
|
-
| Record<string, Partial<NextcloudTalkAccountConfig>>
|
|
69
|
-
| undefined,
|
|
70
|
-
accountId,
|
|
71
|
-
omitKeys: ["defaultAccount"],
|
|
72
|
-
normalizeAccountId,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function resolveNextcloudTalkSecret(
|
|
77
|
-
cfg: CoreConfig,
|
|
78
|
-
opts: { accountId?: string },
|
|
79
|
-
): { secret: string; source: ResolvedNextcloudTalkAccount["secretSource"] } {
|
|
80
|
-
const resolvedAccountId = opts.accountId ?? resolveDefaultNextcloudTalkAccountId(cfg);
|
|
81
|
-
const merged = mergeNextcloudTalkAccountConfig(cfg, resolvedAccountId);
|
|
82
|
-
|
|
83
|
-
const envSecret = normalizeOptionalString(process.env.NEXTCLOUD_TALK_BOT_SECRET);
|
|
84
|
-
if (envSecret && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
85
|
-
return { secret: envSecret, source: "env" };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (merged.botSecretFile) {
|
|
89
|
-
const fileSecret = tryReadSecretFileSync(
|
|
90
|
-
merged.botSecretFile,
|
|
91
|
-
"Nextcloud Talk bot secret file",
|
|
92
|
-
{ rejectSymlink: true },
|
|
93
|
-
);
|
|
94
|
-
if (fileSecret) {
|
|
95
|
-
return { secret: fileSecret, source: "secretFile" };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const inlineSecret = normalizeResolvedSecretInputString({
|
|
100
|
-
value: merged.botSecret,
|
|
101
|
-
path: `channels.nextcloud-talk.accounts.${resolvedAccountId}.botSecret`,
|
|
102
|
-
});
|
|
103
|
-
if (inlineSecret) {
|
|
104
|
-
return { secret: inlineSecret, source: "config" };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return { secret: "", source: "none" };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function resolveNextcloudTalkAccount(params: {
|
|
111
|
-
cfg: CoreConfig;
|
|
112
|
-
accountId?: string | null;
|
|
113
|
-
}): ResolvedNextcloudTalkAccount {
|
|
114
|
-
const baseEnabled = params.cfg.channels?.["nextcloud-talk"]?.enabled !== false;
|
|
115
|
-
const resolvedAccountId = params.accountId ?? resolveDefaultNextcloudTalkAccountId(params.cfg);
|
|
116
|
-
|
|
117
|
-
const resolve = (accountId: string) => {
|
|
118
|
-
const merged = mergeNextcloudTalkAccountConfig(params.cfg, accountId);
|
|
119
|
-
const accountEnabled = merged.enabled !== false;
|
|
120
|
-
const enabled = baseEnabled && accountEnabled;
|
|
121
|
-
const secretResolution = resolveNextcloudTalkSecret(params.cfg, { accountId });
|
|
122
|
-
const baseUrl = merged.baseUrl?.trim()?.replace(/\/$/, "") ?? "";
|
|
123
|
-
|
|
124
|
-
debugAccounts("resolve", {
|
|
125
|
-
accountId,
|
|
126
|
-
enabled,
|
|
127
|
-
secretSource: secretResolution.source,
|
|
128
|
-
baseUrl: baseUrl ? "[set]" : "[missing]",
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
accountId,
|
|
133
|
-
enabled,
|
|
134
|
-
name: normalizeOptionalString(merged.name),
|
|
135
|
-
baseUrl,
|
|
136
|
-
secret: secretResolution.secret,
|
|
137
|
-
secretSource: secretResolution.source,
|
|
138
|
-
config: merged,
|
|
139
|
-
} satisfies ResolvedNextcloudTalkAccount;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return resolveAccountWithDefaultFallback({
|
|
143
|
-
accountId: resolvedAccountId,
|
|
144
|
-
normalizeAccountId,
|
|
145
|
-
resolvePrimary: resolve,
|
|
146
|
-
hasCredential: (account) => account.secretSource !== "none",
|
|
147
|
-
resolveDefaultAccountId: () => resolveDefaultNextcloudTalkAccountId(params.cfg),
|
|
148
|
-
});
|
|
149
|
-
}
|
package/src/api-credentials.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { normalizeResolvedSecretInputString } from "./secret-input.js";
|
|
3
|
-
|
|
4
|
-
export function resolveNextcloudTalkApiCredentials(params: {
|
|
5
|
-
apiUser?: string;
|
|
6
|
-
apiPassword?: unknown;
|
|
7
|
-
apiPasswordFile?: string;
|
|
8
|
-
}): { apiUser: string; apiPassword: string } | undefined {
|
|
9
|
-
const apiUser = params.apiUser?.trim();
|
|
10
|
-
if (!apiUser) {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const inlinePassword = normalizeResolvedSecretInputString({
|
|
15
|
-
value: params.apiPassword,
|
|
16
|
-
path: "channels.nextcloud-talk.apiPassword",
|
|
17
|
-
});
|
|
18
|
-
if (inlinePassword) {
|
|
19
|
-
return { apiUser, apiPassword: inlinePassword };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!params.apiPasswordFile) {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const filePassword = readFileSync(params.apiPasswordFile, "utf-8").trim();
|
|
27
|
-
return filePassword ? { apiUser, apiPassword: filePassword } : undefined;
|
|
28
|
-
} catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
});
|
package/src/approval-auth.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createResolvedApproverActionAuthAdapter,
|
|
3
|
-
resolveApprovalApprovers,
|
|
4
|
-
} from "klaw/plugin-sdk/approval-auth-runtime";
|
|
5
|
-
import { normalizeOptionalLowercaseString } from "klaw/plugin-sdk/string-coerce-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
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
|
|
3
|
-
|
|
4
|
-
const hoisted = vi.hoisted(() => ({
|
|
5
|
-
fetchWithSsrFGuard: vi.fn(),
|
|
6
|
-
ssrfPolicyFromPrivateNetworkOptIn: vi.fn(() => undefined),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
vi.mock("../runtime-api.js", () => ({
|
|
10
|
-
fetchWithSsrFGuard: hoisted.fetchWithSsrFGuard,
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock("./send.runtime.js", () => ({
|
|
14
|
-
ssrfPolicyFromPrivateNetworkOptIn: hoisted.ssrfPolicyFromPrivateNetworkOptIn,
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
const { probeNextcloudTalkBotResponseFeature } = await import("./bot-preflight.js");
|
|
18
|
-
|
|
19
|
-
function account(
|
|
20
|
-
overrides: Partial<ResolvedNextcloudTalkAccount> = {},
|
|
21
|
-
): ResolvedNextcloudTalkAccount {
|
|
22
|
-
return {
|
|
23
|
-
accountId: "default",
|
|
24
|
-
enabled: true,
|
|
25
|
-
baseUrl: "https://cloud.example.com",
|
|
26
|
-
secret: "secret",
|
|
27
|
-
secretSource: "config",
|
|
28
|
-
config: {
|
|
29
|
-
baseUrl: "https://cloud.example.com",
|
|
30
|
-
botSecret: "secret",
|
|
31
|
-
apiUser: "admin",
|
|
32
|
-
apiPassword: "app-password",
|
|
33
|
-
webhookPublicUrl: "https://bot.example.com/nextcloud-talk-webhook",
|
|
34
|
-
},
|
|
35
|
-
...overrides,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function mockBotAdmin(features: number): void {
|
|
40
|
-
hoisted.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
41
|
-
response: new Response(
|
|
42
|
-
JSON.stringify({
|
|
43
|
-
ocs: {
|
|
44
|
-
data: [
|
|
45
|
-
{
|
|
46
|
-
id: 7,
|
|
47
|
-
name: "Klaw",
|
|
48
|
-
url: "https://bot.example.com/nextcloud-talk-webhook",
|
|
49
|
-
features,
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
}),
|
|
54
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
55
|
-
),
|
|
56
|
-
release: async () => {},
|
|
57
|
-
finalUrl: "https://cloud.example.com/ocs/v2.php/apps/spreed/api/v1/bot/admin",
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe("probeNextcloudTalkBotResponseFeature", () => {
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
hoisted.fetchWithSsrFGuard.mockClear();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
afterEach(() => {
|
|
67
|
-
hoisted.fetchWithSsrFGuard.mockReset();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("passes when the matching bot has the response feature bit", async () => {
|
|
71
|
-
mockBotAdmin(1 | 2 | 8);
|
|
72
|
-
|
|
73
|
-
await expect(probeNextcloudTalkBotResponseFeature({ account: account() })).resolves.toEqual({
|
|
74
|
-
ok: true,
|
|
75
|
-
code: "ok",
|
|
76
|
-
botId: "7",
|
|
77
|
-
botName: "Klaw",
|
|
78
|
-
features: 11,
|
|
79
|
-
message: 'Nextcloud Talk bot "Klaw" has the response feature.',
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("reports missing response feature for the matching webhook bot", async () => {
|
|
84
|
-
mockBotAdmin(1 | 8);
|
|
85
|
-
|
|
86
|
-
await expect(probeNextcloudTalkBotResponseFeature({ account: account() })).resolves.toEqual({
|
|
87
|
-
ok: false,
|
|
88
|
-
code: "missing_response_feature",
|
|
89
|
-
botId: "7",
|
|
90
|
-
botName: "Klaw",
|
|
91
|
-
features: 9,
|
|
92
|
-
message:
|
|
93
|
-
'Nextcloud Talk bot "Klaw" (7) is missing the response feature (features=9); outbound replies will fail. Run ./occ talk:bot:state --feature webhook --feature response --feature reaction 7 1 or reinstall the bot with --feature response.',
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("reports malformed bot admin JSON with a stable channel error", async () => {
|
|
98
|
-
hoisted.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
99
|
-
response: new Response("{ nope", {
|
|
100
|
-
status: 200,
|
|
101
|
-
headers: { "content-type": "application/json" },
|
|
102
|
-
}),
|
|
103
|
-
release: async () => {},
|
|
104
|
-
finalUrl: "https://cloud.example.com/ocs/v2.php/apps/spreed/api/v1/bot/admin",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await expect(probeNextcloudTalkBotResponseFeature({ account: account() })).resolves.toEqual({
|
|
108
|
-
ok: false,
|
|
109
|
-
code: "request_failed",
|
|
110
|
-
message:
|
|
111
|
-
"Nextcloud Talk bot response feature probe failed: Nextcloud Talk bot response feature probe failed: malformed JSON response",
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("skips when API credentials are absent", async () => {
|
|
116
|
-
await expect(
|
|
117
|
-
probeNextcloudTalkBotResponseFeature({
|
|
118
|
-
account: account({
|
|
119
|
-
config: {
|
|
120
|
-
baseUrl: "https://cloud.example.com",
|
|
121
|
-
botSecret: "secret",
|
|
122
|
-
webhookPublicUrl: "https://bot.example.com/nextcloud-talk-webhook",
|
|
123
|
-
},
|
|
124
|
-
}),
|
|
125
|
-
}),
|
|
126
|
-
).resolves.toEqual({
|
|
127
|
-
ok: true,
|
|
128
|
-
skipped: true,
|
|
129
|
-
code: "missing_api_credentials",
|
|
130
|
-
message:
|
|
131
|
-
"Nextcloud Talk bot response feature probe skipped: apiUser/apiPassword are not configured.",
|
|
132
|
-
});
|
|
133
|
-
expect(hoisted.fetchWithSsrFGuard).not.toHaveBeenCalled();
|
|
134
|
-
});
|
|
135
|
-
});
|