@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.
Files changed (58) hide show
  1. package/klaw.plugin.json +799 -2
  2. package/package.json +16 -4
  3. package/api.ts +0 -1
  4. package/channel-plugin-api.ts +0 -1
  5. package/contract-api.ts +0 -4
  6. package/doctor-contract-api.ts +0 -1
  7. package/index.ts +0 -20
  8. package/runtime-api.ts +0 -29
  9. package/secret-contract-api.ts +0 -5
  10. package/setup-entry.ts +0 -13
  11. package/src/accounts.test.ts +0 -31
  12. package/src/accounts.ts +0 -149
  13. package/src/api-credentials.ts +0 -31
  14. package/src/approval-auth.test.ts +0 -17
  15. package/src/approval-auth.ts +0 -27
  16. package/src/bot-preflight.test.ts +0 -135
  17. package/src/bot-preflight.ts +0 -183
  18. package/src/channel-api.ts +0 -5
  19. package/src/channel.adapters.ts +0 -52
  20. package/src/channel.core.test.ts +0 -75
  21. package/src/channel.lifecycle.test.ts +0 -91
  22. package/src/channel.status.test.ts +0 -28
  23. package/src/channel.ts +0 -225
  24. package/src/config-schema.ts +0 -79
  25. package/src/core.test.ts +0 -325
  26. package/src/doctor-contract.ts +0 -9
  27. package/src/doctor.test.ts +0 -87
  28. package/src/doctor.ts +0 -40
  29. package/src/gateway.ts +0 -109
  30. package/src/inbound.authz.test.ts +0 -146
  31. package/src/inbound.behavior.test.ts +0 -309
  32. package/src/inbound.ts +0 -392
  33. package/src/message-actions.test.ts +0 -270
  34. package/src/message-actions.ts +0 -82
  35. package/src/message-adapter.ts +0 -28
  36. package/src/monitor-runtime.ts +0 -138
  37. package/src/monitor.replay.test.ts +0 -276
  38. package/src/monitor.test-fixtures.ts +0 -30
  39. package/src/monitor.test-harness.ts +0 -59
  40. package/src/monitor.ts +0 -385
  41. package/src/normalize.ts +0 -44
  42. package/src/policy.ts +0 -111
  43. package/src/replay-guard.ts +0 -128
  44. package/src/room-info.test.ts +0 -160
  45. package/src/room-info.ts +0 -130
  46. package/src/runtime.ts +0 -9
  47. package/src/secret-contract.ts +0 -103
  48. package/src/secret-input.ts +0 -4
  49. package/src/send.cfg-threading.test.ts +0 -359
  50. package/src/send.runtime.ts +0 -8
  51. package/src/send.ts +0 -269
  52. package/src/session-route.ts +0 -40
  53. package/src/setup-core.ts +0 -250
  54. package/src/setup-surface.ts +0 -195
  55. package/src/setup.test.ts +0 -445
  56. package/src/signature.ts +0 -82
  57. package/src/types.ts +0 -195
  58. package/tsconfig.json +0 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodelyth/nextcloud-talk",
3
- "version": "2026.5.42",
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";
@@ -1 +0,0 @@
1
- export { nextcloudTalkPlugin } from "./src/channel.js";
package/contract-api.ts DELETED
@@ -1,4 +0,0 @@
1
- export {
2
- collectRuntimeConfigAssignments,
3
- secretTargetRegistryEntries,
4
- } from "./src/secret-contract.js";
@@ -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";
@@ -1,5 +0,0 @@
1
- export {
2
- channelSecrets,
3
- collectRuntimeConfigAssignments,
4
- secretTargetRegistryEntries,
5
- } from "./src/secret-contract.js";
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
- });
@@ -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
- }
@@ -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
- });
@@ -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
- });