@xpufx/paseo-twofado 0.1.0 → 0.1.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/client/approvals.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
copyToClipboard,
|
|
25
25
|
usePluginSettings,
|
|
26
26
|
usePluginTheme,
|
|
27
|
-
} from "paseo-plugin-helper/
|
|
27
|
+
} from "./vendor/paseo-plugin-helper/index";
|
|
28
28
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
29
29
|
import { useEffect, useRef, useState } from "react";
|
|
30
30
|
import { Easing, Text, View } from "react-native";
|
|
@@ -24,16 +24,23 @@ export interface ClipboardEnvironment {
|
|
|
24
24
|
* `react-native-web` `Clipboard.setString` reports success even when its
|
|
25
25
|
* `document.execCommand("copy")` fails, which leaves the previous clipboard
|
|
26
26
|
* item in place while the UI claims success (xpufx-org/paseo#278); it is only
|
|
27
|
-
* usable off-DOM (native), where it is the real platform clipboard.
|
|
28
|
-
*
|
|
27
|
+
* usable off-DOM (native), where it is the real platform clipboard. The same
|
|
28
|
+
* applies to any `setStringAsync` whose DOM fallback is that unverified
|
|
29
|
+
* `execCommand`. In a DOM the checked `execCommand` fallback is preferred to
|
|
30
|
+
* those unverifiable paths.
|
|
29
31
|
*/
|
|
30
32
|
export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
|
|
31
33
|
const tiers: ClipboardTier[] = [];
|
|
32
34
|
if (env.hasNavigatorClipboard) tiers.push("navigator");
|
|
33
35
|
if (env.hasHostCopyText) tiers.push("host");
|
|
34
|
-
|
|
36
|
+
// On a DOM both RN-web paths are unverifiable: `setString` reports success
|
|
37
|
+
// even when its `execCommand` no-ops, and a `setStringAsync` that falls back
|
|
38
|
+
// to it does the same. Off-DOM they are the real native clipboard, so they
|
|
39
|
+
// lead there; on a DOM only the checked `execCommand` fallback is honest.
|
|
40
|
+
const rnDomOk = !env.isDom;
|
|
41
|
+
if (env.hasRnSetStringAsync && rnDomOk) {
|
|
35
42
|
tiers.push("rnAsync");
|
|
36
|
-
} else if (env.hasRnClipboard &&
|
|
43
|
+
} else if (env.hasRnClipboard && rnDomOk) {
|
|
37
44
|
tiers.push("rnSync");
|
|
38
45
|
}
|
|
39
46
|
tiers.push("execCommand");
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
3
|
+
import { Icon, Modal, ScrollView, useToast } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import {
|
|
5
|
+
initClientHelpers,
|
|
6
|
+
registerSidebarSurface,
|
|
7
|
+
} from "./client/vendor/paseo-plugin-helper/index";
|
|
8
|
+
import { ErrorBoundary } from "./client/error-boundary";
|
|
9
|
+
import {
|
|
10
|
+
ApprovalHeaderIcon,
|
|
11
|
+
ApprovalSurface,
|
|
12
|
+
trackHeaderButton,
|
|
13
|
+
untrackHeaderButton,
|
|
14
|
+
} from "./client/approvals";
|
|
15
|
+
|
|
16
|
+
export default function contribute(client: PluginClientContext) {
|
|
17
|
+
initClientHelpers({ Icon, Modal, useRpc, useToast, ScrollView });
|
|
18
|
+
|
|
19
|
+
const Surface = (props: PluginSurfaceProps) => (
|
|
20
|
+
<ErrorBoundary label="approvals surface" fallback={null}>
|
|
21
|
+
<ApprovalSurface {...props} />
|
|
22
|
+
</ErrorBoundary>
|
|
23
|
+
);
|
|
24
|
+
// Helper primitive instead of a handrolled surface registration: besides
|
|
25
|
+
// injecting the theme provider it marks the subtree as helper-scroll-owned,
|
|
26
|
+
// so the page owns a working scroll region on a wide desktop window
|
|
27
|
+
// (xpufx-org/paseo#213) rather than clipping.
|
|
28
|
+
registerSidebarSurface(client, {
|
|
29
|
+
id: "approvals",
|
|
30
|
+
title: "2fado",
|
|
31
|
+
icon: "ShieldCheck",
|
|
32
|
+
Component: Surface,
|
|
33
|
+
});
|
|
34
|
+
client.addCommandCenterItem({
|
|
35
|
+
id: "open-approvals",
|
|
36
|
+
title: "Open 2fado",
|
|
37
|
+
icon: "ShieldCheck",
|
|
38
|
+
context: "global",
|
|
39
|
+
onSelect({ openSurface }) {
|
|
40
|
+
openSurface("approvals");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const buttons = new Map<string, () => void>();
|
|
45
|
+
const addButton = (workspaceId: string) => {
|
|
46
|
+
if (!workspaceId || buttons.has(workspaceId)) return;
|
|
47
|
+
const registration = client.addHeaderButton({
|
|
48
|
+
id: "twofado",
|
|
49
|
+
workspaceId,
|
|
50
|
+
button: {
|
|
51
|
+
title: "2fado",
|
|
52
|
+
icon: ApprovalHeaderIcon,
|
|
53
|
+
behavior: { kind: "action", onPress: () => client.openSurface("approvals") },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
buttons.set(workspaceId, () => {
|
|
57
|
+
untrackHeaderButton(workspaceId);
|
|
58
|
+
registration.remove();
|
|
59
|
+
});
|
|
60
|
+
trackHeaderButton(workspaceId, registration);
|
|
61
|
+
};
|
|
62
|
+
const unsubscribe = client.paseo.agents.subscribe((update) => {
|
|
63
|
+
if (update.kind !== "upsert" || !update.agent.workspaceId) return;
|
|
64
|
+
addButton(update.agent.workspaceId);
|
|
65
|
+
});
|
|
66
|
+
// Register buttons for agents already present: subscribe only fires on
|
|
67
|
+
// future upserts, so without this the icon is missing after a Paseo
|
|
68
|
+
// restart until the next agent event — including when the daemon is down
|
|
69
|
+
// and the red down-state should be showing.
|
|
70
|
+
void Promise.resolve()
|
|
71
|
+
.then(() => client.paseo.agents.list())
|
|
72
|
+
.then(
|
|
73
|
+
(res) => {
|
|
74
|
+
const entries = Array.isArray(
|
|
75
|
+
(res as unknown as { entries?: unknown }).entries,
|
|
76
|
+
)
|
|
77
|
+
? (res as unknown as { entries: Array<{ agent?: { workspaceId?: unknown } }> }).entries
|
|
78
|
+
: [];
|
|
79
|
+
for (const { agent } of entries) {
|
|
80
|
+
if (typeof agent?.workspaceId === "string") addButton(agent.workspaceId);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
(err) => console.warn("[2fado] Failed to list agents for header buttons:", err),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
unsubscribe();
|
|
88
|
+
for (const remove of buttons.values()) remove();
|
|
89
|
+
buttons.clear();
|
|
90
|
+
};
|
|
91
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
3
|
+
import { PluginStorage, createPluginLogger, registerSettingsRpc } from "./server/vendor/paseo-plugin-helper/index";
|
|
4
|
+
import {
|
|
5
|
+
approvalAck,
|
|
6
|
+
approvalSettings,
|
|
7
|
+
approvalStatus,
|
|
8
|
+
approvalTelegramInfo,
|
|
9
|
+
approvalTelegramSetConfig,
|
|
10
|
+
daemonHealth,
|
|
11
|
+
migrateLegacyNotificationTarget,
|
|
12
|
+
pendingList,
|
|
13
|
+
policyAddRule,
|
|
14
|
+
recentList,
|
|
15
|
+
verdict,
|
|
16
|
+
} from "./shared/approval";
|
|
17
|
+
import type { ApprovalSettingsValues } from "./shared/approval";
|
|
18
|
+
import {
|
|
19
|
+
addPolicyRule,
|
|
20
|
+
getHealth,
|
|
21
|
+
getStatus,
|
|
22
|
+
getTelegramInfo,
|
|
23
|
+
listPending,
|
|
24
|
+
listRecent,
|
|
25
|
+
setTelegramConfig,
|
|
26
|
+
submitAck,
|
|
27
|
+
submitVerdict,
|
|
28
|
+
} from "./server/twofado";
|
|
29
|
+
|
|
30
|
+
const log = createPluginLogger("twofado");
|
|
31
|
+
|
|
32
|
+
function migrateLegacySettingsFile(storage: PluginStorage<ApprovalSettingsValues>): void {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(storage.filePath, "utf8");
|
|
35
|
+
const { data, changed } = migrateLegacyNotificationTarget(JSON.parse(raw));
|
|
36
|
+
if (!changed) return;
|
|
37
|
+
fs.writeFileSync(storage.filePath, JSON.stringify(data, null, 2));
|
|
38
|
+
log.info("migrated legacy telegramFallback to notificationTarget");
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Missing or unreadable file: schema defaults apply.
|
|
41
|
+
log.warn("legacy settings migration skipped", { error: err });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function contribute(server: PluginServerContext) {
|
|
46
|
+
const storage = new PluginStorage("twofado", "settings.json", {
|
|
47
|
+
defaultData: approvalSettings.defaultSettings,
|
|
48
|
+
schema: approvalSettings.schema,
|
|
49
|
+
});
|
|
50
|
+
migrateLegacySettingsFile(storage);
|
|
51
|
+
registerSettingsRpc(server, approvalSettings, storage, {
|
|
52
|
+
onUpdate: (next, prev) => {
|
|
53
|
+
log.info("settings updated", {
|
|
54
|
+
socketPath: next.socketPath,
|
|
55
|
+
notificationTarget: next.notificationTarget,
|
|
56
|
+
});
|
|
57
|
+
if (next.telegramBotToken || next.telegramChatId || next.telegramApprovers) {
|
|
58
|
+
const approvers = next.telegramApprovers
|
|
59
|
+
? next.telegramApprovers
|
|
60
|
+
.split(/[\s,]+/)
|
|
61
|
+
.map((s) => s.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
: [];
|
|
64
|
+
void setTelegramConfig({
|
|
65
|
+
botToken: next.telegramBotToken || undefined,
|
|
66
|
+
chatId: next.telegramChatId || undefined,
|
|
67
|
+
approvers,
|
|
68
|
+
notificationTarget: next.notificationTarget,
|
|
69
|
+
socketPath: next.socketPath,
|
|
70
|
+
}).catch((err) => log.warn("failed to sync telegram config to daemon", { error: err }));
|
|
71
|
+
} else if (prev && next.notificationTarget !== prev.notificationTarget) {
|
|
72
|
+
void (async () => {
|
|
73
|
+
const info = await getTelegramInfo({ socketPath: next.socketPath }).catch((err) => {
|
|
74
|
+
log.warn("telegram info fetch failed during target sync", { error: err });
|
|
75
|
+
return undefined;
|
|
76
|
+
});
|
|
77
|
+
await setTelegramConfig({
|
|
78
|
+
chatId: info?.chatId,
|
|
79
|
+
approvers: info?.approvers,
|
|
80
|
+
notificationTarget: next.notificationTarget,
|
|
81
|
+
socketPath: next.socketPath,
|
|
82
|
+
});
|
|
83
|
+
})().catch((err) => log.warn("failed to sync notification target to daemon", { error: err }));
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
server.handle(pendingList, (input) => listPending(input));
|
|
88
|
+
server.handle(verdict, (input) => submitVerdict(input));
|
|
89
|
+
server.handle(approvalAck, (input) => submitAck(input));
|
|
90
|
+
server.handle(recentList, (input) => listRecent(input));
|
|
91
|
+
server.handle(approvalStatus, (input) => getStatus(input));
|
|
92
|
+
server.handle(daemonHealth, (input) => getHealth(input));
|
|
93
|
+
server.handle(approvalTelegramInfo, (input) => getTelegramInfo(input));
|
|
94
|
+
server.handle(approvalTelegramSetConfig, (input) => setTelegramConfig(input));
|
|
95
|
+
server.handle(policyAddRule, (input) => addPolicyRule(input));
|
|
96
|
+
return () => {};
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@xpufx/paseo-twofado",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"typecheck": "tsc --noEmit"
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"paseo-plugin.json",
|
|
22
22
|
"README.md",
|
|
23
23
|
"LICENSE",
|
|
24
|
+
"index.client.tsx",
|
|
25
|
+
"index.server.ts",
|
|
24
26
|
"client",
|
|
25
27
|
"server",
|
|
26
28
|
"shared",
|
package/server/twofado.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import os from "node:os";
|
|
3
|
-
import type { RpcInput, RpcOutput } from "paseo-plugin-helper/
|
|
4
|
-
import { createPluginLogger, guardRpcHandler } from "paseo-plugin-helper/
|
|
3
|
+
import type { RpcInput, RpcOutput } from "../shared/vendor/paseo-plugin-helper/index";
|
|
4
|
+
import { createPluginLogger, guardRpcHandler } from "./vendor/paseo-plugin-helper/index";
|
|
5
5
|
import {
|
|
6
6
|
approvalAck,
|
|
7
7
|
approvalSettings,
|
package/shared/approval.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { defineContract, defineSettingsContract } from "paseo-plugin-helper/
|
|
2
|
+
import { defineContract, defineSettingsContract } from "./vendor/paseo-plugin-helper/index";
|
|
3
3
|
|
|
4
4
|
export const notificationTargets = ["telegram", "paseo", "both"] as const;
|
|
5
5
|
export type NotificationTarget = (typeof notificationTargets)[number];
|