@gakr-gakr/nostr 0.1.0
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/README.md +142 -0
- package/api.ts +10 -0
- package/autobot.plugin.json +15 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +95 -0
- package/package.json +68 -0
- package/runtime-api.ts +6 -0
- package/setup-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/channel-api.ts +11 -0
- package/src/channel.setup.ts +234 -0
- package/src/channel.ts +215 -0
- package/src/config-schema.ts +98 -0
- package/src/default-relays.ts +1 -0
- package/src/gateway.ts +321 -0
- package/src/inbound-direct-dm-runtime.ts +1 -0
- package/src/metrics.ts +458 -0
- package/src/nostr-bus.ts +789 -0
- package/src/nostr-key-utils.ts +94 -0
- package/src/nostr-profile-core.ts +134 -0
- package/src/nostr-profile-http-runtime.ts +6 -0
- package/src/nostr-profile-http.ts +583 -0
- package/src/nostr-profile-import.ts +262 -0
- package/src/nostr-profile-url-safety.ts +21 -0
- package/src/nostr-profile.ts +144 -0
- package/src/nostr-state-store.ts +206 -0
- package/src/runtime.ts +9 -0
- package/src/seen-tracker.ts +289 -0
- package/src/session-route.ts +25 -0
- package/src/setup-surface.ts +267 -0
- package/src/types.ts +117 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @gakr-gakr/nostr
|
|
2
|
+
|
|
3
|
+
Nostr DM channel plugin for AutoBot using NIP-04 encrypted direct messages.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This extension adds Nostr as a messaging channel to AutoBot. It enables your bot to:
|
|
8
|
+
|
|
9
|
+
- Receive encrypted DMs from Nostr users
|
|
10
|
+
- Send encrypted responses back
|
|
11
|
+
- Work with any NIP-04 compatible Nostr client (Damus, Amethyst, etc.)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
autobot plugins install @gakr-gakr/nostr
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Setup
|
|
20
|
+
|
|
21
|
+
1. Generate a Nostr keypair (if you don't have one):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Using nak CLI
|
|
25
|
+
nak key generate
|
|
26
|
+
|
|
27
|
+
# Or use any Nostr key generator
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Add to your config:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"channels": {
|
|
35
|
+
"nostr": {
|
|
36
|
+
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
|
37
|
+
"relays": ["wss://relay.damus.io", "wss://nos.lol"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. Set the environment variable:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export NOSTR_PRIVATE_KEY="nsec1..." # or hex format
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
4. Restart the gateway
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
| Key | Type | Default | Description |
|
|
54
|
+
| ------------ | -------- | ------------------------------------------- | ---------------------------------------------------------- |
|
|
55
|
+
| `privateKey` | string | required | Bot's private key (nsec or hex format) |
|
|
56
|
+
| `relays` | string[] | `["wss://relay.damus.io", "wss://nos.lol"]` | WebSocket relay URLs |
|
|
57
|
+
| `dmPolicy` | string | `"pairing"` | Access control: `pairing`, `allowlist`, `open`, `disabled` |
|
|
58
|
+
| `allowFrom` | string[] | `[]` | Allowed sender pubkeys (npub or hex) |
|
|
59
|
+
| `enabled` | boolean | `true` | Enable/disable the channel |
|
|
60
|
+
| `name` | string | - | Display name for the account |
|
|
61
|
+
|
|
62
|
+
## Access Control
|
|
63
|
+
|
|
64
|
+
### DM Policies
|
|
65
|
+
|
|
66
|
+
- **pairing** (default): Unknown senders receive a pairing code to request access
|
|
67
|
+
- **allowlist**: Only pubkeys in `allowFrom` can message the bot
|
|
68
|
+
- **open**: Anyone can message the bot (use with caution)
|
|
69
|
+
- **disabled**: DMs are disabled
|
|
70
|
+
|
|
71
|
+
Inbound event signatures are verified before policy enforcement and NIP-04 decryption.
|
|
72
|
+
Unknown senders in `pairing` mode can receive a pairing reply, but their original DM body is not
|
|
73
|
+
processed unless approved.
|
|
74
|
+
|
|
75
|
+
### Example: Allowlist Mode
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"channels": {
|
|
80
|
+
"nostr": {
|
|
81
|
+
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
|
82
|
+
"dmPolicy": "allowlist",
|
|
83
|
+
"allowFrom": ["npub1abc...", "0123456789abcdef..."]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
### Local Relay (Recommended)
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Using strfry
|
|
95
|
+
docker run -p 7777:7777 ghcr.io/hoytech/strfry
|
|
96
|
+
|
|
97
|
+
# Configure autobot to use local relay
|
|
98
|
+
"relays": ["ws://localhost:7777"]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Manual Test
|
|
102
|
+
|
|
103
|
+
1. Start the gateway with Nostr configured
|
|
104
|
+
2. Open Damus, Amethyst, or another Nostr client
|
|
105
|
+
3. Send a DM to your bot's npub
|
|
106
|
+
4. Verify the bot responds
|
|
107
|
+
|
|
108
|
+
## Protocol Support
|
|
109
|
+
|
|
110
|
+
| NIP | Status | Notes |
|
|
111
|
+
| ------ | --------- | ---------------------- |
|
|
112
|
+
| NIP-01 | Supported | Basic event structure |
|
|
113
|
+
| NIP-04 | Supported | Encrypted DMs (kind:4) |
|
|
114
|
+
| NIP-17 | Planned | Gift-wrapped DMs (v2) |
|
|
115
|
+
|
|
116
|
+
## Security Notes
|
|
117
|
+
|
|
118
|
+
- Private keys are never logged
|
|
119
|
+
- Event signatures are verified before processing
|
|
120
|
+
- Sender policy is checked before expensive crypto work
|
|
121
|
+
- Inbound DMs are rate-limited and oversized payloads are dropped before decrypt
|
|
122
|
+
- Use environment variables for keys, never commit to config files
|
|
123
|
+
- Consider using `allowlist` mode in production
|
|
124
|
+
|
|
125
|
+
## Troubleshooting
|
|
126
|
+
|
|
127
|
+
### Bot not receiving messages
|
|
128
|
+
|
|
129
|
+
1. Verify private key is correctly configured
|
|
130
|
+
2. Check relay connectivity
|
|
131
|
+
3. Ensure `enabled` is not set to `false`
|
|
132
|
+
4. Check the bot's public key matches what you're sending to
|
|
133
|
+
|
|
134
|
+
### Messages not being delivered
|
|
135
|
+
|
|
136
|
+
1. Check relay URLs are correct (must use `wss://`)
|
|
137
|
+
2. Verify relays are online and accepting connections
|
|
138
|
+
3. Check for rate limiting (reduce message frequency)
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/api.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getPluginRuntimeGatewayRequestScope,
|
|
3
|
+
type AutoBotConfig,
|
|
4
|
+
type PluginRuntime,
|
|
5
|
+
} from "./runtime-api.js";
|
|
6
|
+
export { nostrPlugin } from "./src/channel.js";
|
|
7
|
+
export { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
|
|
8
|
+
export { getNostrRuntime, setNostrRuntime } from "./src/runtime.js";
|
|
9
|
+
export { resolveNostrAccount } from "./src/types.js";
|
|
10
|
+
export type { ResolvedNostrAccount } from "./src/types.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nostr",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": false
|
|
5
|
+
},
|
|
6
|
+
"channels": ["nostr"],
|
|
7
|
+
"channelEnvVars": {
|
|
8
|
+
"nostr": ["NOSTR_PRIVATE_KEY"]
|
|
9
|
+
},
|
|
10
|
+
"configSchema": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": false,
|
|
13
|
+
"properties": {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { nostrPlugin } from "./src/channel.js";
|
package/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineBundledChannelEntry,
|
|
3
|
+
loadBundledEntryExportSync,
|
|
4
|
+
} from "autobot/plugin-sdk/channel-entry-contract";
|
|
5
|
+
import type { AutoBotConfig, PluginRuntime, ResolvedNostrAccount } from "./api.js";
|
|
6
|
+
|
|
7
|
+
function createNostrProfileHttpHandler() {
|
|
8
|
+
return loadBundledEntryExportSync<
|
|
9
|
+
(params: Record<string, unknown>) => (ctx: unknown) => Promise<void> | void
|
|
10
|
+
>(import.meta.url, {
|
|
11
|
+
specifier: "./api.js",
|
|
12
|
+
exportName: "createNostrProfileHttpHandler",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getNostrRuntime() {
|
|
17
|
+
return loadBundledEntryExportSync<() => PluginRuntime>(import.meta.url, {
|
|
18
|
+
specifier: "./api.js",
|
|
19
|
+
exportName: "getNostrRuntime",
|
|
20
|
+
})();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveNostrAccount(params: { cfg: unknown; accountId: string }) {
|
|
24
|
+
return loadBundledEntryExportSync<
|
|
25
|
+
(params: { cfg: unknown; accountId: string }) => ResolvedNostrAccount
|
|
26
|
+
>(import.meta.url, {
|
|
27
|
+
specifier: "./api.js",
|
|
28
|
+
exportName: "resolveNostrAccount",
|
|
29
|
+
})(params);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default defineBundledChannelEntry({
|
|
33
|
+
id: "nostr",
|
|
34
|
+
name: "Nostr",
|
|
35
|
+
description: "Nostr DM channel plugin via NIP-04",
|
|
36
|
+
importMetaUrl: import.meta.url,
|
|
37
|
+
plugin: {
|
|
38
|
+
specifier: "./channel-plugin-api.js",
|
|
39
|
+
exportName: "nostrPlugin",
|
|
40
|
+
},
|
|
41
|
+
runtime: {
|
|
42
|
+
specifier: "./api.js",
|
|
43
|
+
exportName: "setNostrRuntime",
|
|
44
|
+
},
|
|
45
|
+
registerFull(api) {
|
|
46
|
+
const httpHandler = createNostrProfileHttpHandler()({
|
|
47
|
+
getConfigProfile: (accountId: string) => {
|
|
48
|
+
const runtime = getNostrRuntime();
|
|
49
|
+
const cfg = runtime.config.current() as AutoBotConfig;
|
|
50
|
+
const account = resolveNostrAccount({ cfg, accountId });
|
|
51
|
+
return account.profile;
|
|
52
|
+
},
|
|
53
|
+
updateConfigProfile: async (_accountId: string, profile: unknown) => {
|
|
54
|
+
const runtime = getNostrRuntime();
|
|
55
|
+
|
|
56
|
+
await runtime.config.mutateConfigFile({
|
|
57
|
+
afterWrite: { mode: "auto" },
|
|
58
|
+
mutate: (draft) => {
|
|
59
|
+
const channels = (draft.channels ?? {}) as Record<string, unknown>;
|
|
60
|
+
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
draft.channels = {
|
|
63
|
+
...channels,
|
|
64
|
+
nostr: {
|
|
65
|
+
...nostrConfig,
|
|
66
|
+
profile,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
getAccountInfo: (accountId: string) => {
|
|
73
|
+
const runtime = getNostrRuntime();
|
|
74
|
+
const cfg = runtime.config.current() as AutoBotConfig;
|
|
75
|
+
const account = resolveNostrAccount({ cfg, accountId });
|
|
76
|
+
if (!account.configured || !account.publicKey) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
pubkey: account.publicKey,
|
|
81
|
+
relays: account.relays,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
log: api.logger,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
api.registerHttpRoute({
|
|
88
|
+
path: "/api/channels/nostr",
|
|
89
|
+
auth: "gateway",
|
|
90
|
+
match: "prefix",
|
|
91
|
+
gatewayRuntimeScopeSurface: "trusted-operator",
|
|
92
|
+
handler: httpHandler,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gakr-gakr/nostr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AutoBot Nostr channel plugin for NIP-04 encrypted DMs",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/autobot/autobot"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"nostr-tools": "2.23.3",
|
|
12
|
+
"zod": "4.4.3"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@gakr-gakr/plugin-sdk": "workspace:*",
|
|
16
|
+
"@gakr-gakr/autobot": "workspace:*",
|
|
17
|
+
"autobot": "workspace:@gakr-gakr/autobot@*"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@gakr-gakr/autobot": ">=0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"@gakr-gakr/autobot": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"autobot": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"./index.ts"
|
|
30
|
+
],
|
|
31
|
+
"setupEntry": "./setup-entry.ts",
|
|
32
|
+
"channel": {
|
|
33
|
+
"id": "nostr",
|
|
34
|
+
"label": "Nostr",
|
|
35
|
+
"selectionLabel": "Nostr (NIP-04 DMs)",
|
|
36
|
+
"docsPath": "/channels/nostr",
|
|
37
|
+
"docsLabel": "nostr",
|
|
38
|
+
"blurb": "Decentralized protocol; encrypted DMs via NIP-04.",
|
|
39
|
+
"order": 55,
|
|
40
|
+
"quickstartAllowFrom": true,
|
|
41
|
+
"cliAddOptions": [
|
|
42
|
+
{
|
|
43
|
+
"flags": "--private-key <key>",
|
|
44
|
+
"description": "Nostr private key (nsec... or hex)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"flags": "--relay-urls <list>",
|
|
48
|
+
"description": "Nostr relay URLs (comma-separated)"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"install": {
|
|
53
|
+
"npmSpec": "@gakr-gakr/nostr",
|
|
54
|
+
"defaultChoice": "npm",
|
|
55
|
+
"minHostVersion": ">=2026.4.10"
|
|
56
|
+
},
|
|
57
|
+
"compat": {
|
|
58
|
+
"pluginApi": ">=2026.5.19"
|
|
59
|
+
},
|
|
60
|
+
"build": {
|
|
61
|
+
"autobotVersion": "2026.5.19"
|
|
62
|
+
},
|
|
63
|
+
"release": {
|
|
64
|
+
"publishToClawHub": true,
|
|
65
|
+
"publishToNpm": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/runtime-api.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Private runtime barrel for the bundled Nostr extension.
|
|
2
|
+
// Keep this barrel thin and aligned with the local extension surface.
|
|
3
|
+
|
|
4
|
+
export type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
5
|
+
export { getPluginRuntimeGatewayRequestScope } from "autobot/plugin-sdk/plugin-runtime";
|
|
6
|
+
export type { PluginRuntime } from "autobot/plugin-sdk/runtime-store";
|
package/setup-api.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { nostrSetupAdapter, nostrSetupWizard } from "./src/setup-surface.js";
|
package/setup-entry.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineBundledChannelSetupEntry } from "autobot/plugin-sdk/channel-entry-contract";
|
|
2
|
+
|
|
3
|
+
export default defineBundledChannelSetupEntry({
|
|
4
|
+
importMetaUrl: import.meta.url,
|
|
5
|
+
plugin: {
|
|
6
|
+
specifier: "./setup-plugin-api.js",
|
|
7
|
+
exportName: "nostrSetupPlugin",
|
|
8
|
+
},
|
|
9
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildChannelConfigSchema,
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
formatPairingApproveHint,
|
|
5
|
+
type ChannelPlugin,
|
|
6
|
+
} from "autobot/plugin-sdk/channel-plugin-common";
|
|
7
|
+
export type { ChannelOutboundAdapter } from "autobot/plugin-sdk/channel-contract";
|
|
8
|
+
export {
|
|
9
|
+
collectStatusIssuesFromLastError,
|
|
10
|
+
createDefaultChannelRuntimeState,
|
|
11
|
+
} from "autobot/plugin-sdk/status-helpers";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describeAccountSnapshot } from "autobot/plugin-sdk/account-helpers";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
3
|
+
import { patchTopLevelChannelConfigSection } from "autobot/plugin-sdk/setup";
|
|
4
|
+
import {
|
|
5
|
+
createDelegatedSetupWizardProxy,
|
|
6
|
+
createStandardChannelSetupStatus,
|
|
7
|
+
DEFAULT_ACCOUNT_ID,
|
|
8
|
+
createSetupTranslator,
|
|
9
|
+
type ChannelSetupAdapter,
|
|
10
|
+
} from "autobot/plugin-sdk/setup-runtime";
|
|
11
|
+
import { buildChannelConfigSchema, type ChannelPlugin } from "./channel-api.js";
|
|
12
|
+
import { NostrConfigSchema } from "./config-schema.js";
|
|
13
|
+
import { DEFAULT_RELAYS } from "./default-relays.js";
|
|
14
|
+
|
|
15
|
+
const t = createSetupTranslator();
|
|
16
|
+
|
|
17
|
+
const channel = "nostr" as const;
|
|
18
|
+
|
|
19
|
+
type NostrAccountConfig = {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
name?: string;
|
|
22
|
+
defaultAccount?: string;
|
|
23
|
+
privateKey?: unknown;
|
|
24
|
+
relays?: string[];
|
|
25
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
26
|
+
allowFrom?: Array<string | number>;
|
|
27
|
+
profile?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ResolvedNostrSetupAccount = {
|
|
31
|
+
accountId: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
configured: boolean;
|
|
35
|
+
privateKey: string;
|
|
36
|
+
publicKey: string;
|
|
37
|
+
relays: string[];
|
|
38
|
+
profile?: unknown;
|
|
39
|
+
config: NostrAccountConfig;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function getNostrConfig(cfg: AutoBotConfig): NostrAccountConfig | undefined {
|
|
43
|
+
return (cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
|
44
|
+
| NostrAccountConfig
|
|
45
|
+
| undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function listSetupNostrAccountIds(cfg: AutoBotConfig): string[] {
|
|
49
|
+
const nostrCfg = getNostrConfig(cfg);
|
|
50
|
+
const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
|
|
51
|
+
if (!privateKey) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return [resolveDefaultSetupNostrAccountId(cfg)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveDefaultSetupNostrAccountId(cfg: AutoBotConfig): string {
|
|
58
|
+
const configured = getNostrConfig(cfg)?.defaultAccount;
|
|
59
|
+
return typeof configured === "string" && configured.trim()
|
|
60
|
+
? configured.trim()
|
|
61
|
+
: DEFAULT_ACCOUNT_ID;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveSetupNostrAccount(params: {
|
|
65
|
+
cfg: AutoBotConfig;
|
|
66
|
+
accountId?: string | null;
|
|
67
|
+
}): ResolvedNostrSetupAccount {
|
|
68
|
+
const nostrCfg = getNostrConfig(params.cfg);
|
|
69
|
+
const accountId = params.accountId?.trim() || resolveDefaultSetupNostrAccountId(params.cfg);
|
|
70
|
+
const privateKey = typeof nostrCfg?.privateKey === "string" ? nostrCfg.privateKey.trim() : "";
|
|
71
|
+
const configured = Boolean(privateKey);
|
|
72
|
+
return {
|
|
73
|
+
accountId,
|
|
74
|
+
name: typeof nostrCfg?.name === "string" ? nostrCfg.name : undefined,
|
|
75
|
+
enabled: nostrCfg?.enabled !== false,
|
|
76
|
+
configured,
|
|
77
|
+
privateKey,
|
|
78
|
+
publicKey: "",
|
|
79
|
+
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
|
|
80
|
+
profile: nostrCfg?.profile,
|
|
81
|
+
config: {
|
|
82
|
+
enabled: nostrCfg?.enabled,
|
|
83
|
+
name: nostrCfg?.name,
|
|
84
|
+
privateKey: nostrCfg?.privateKey,
|
|
85
|
+
relays: nostrCfg?.relays,
|
|
86
|
+
dmPolicy: nostrCfg?.dmPolicy,
|
|
87
|
+
allowFrom: nostrCfg?.allowFrom,
|
|
88
|
+
profile: nostrCfg?.profile,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildNostrSetupPatch(accountId: string, patch: Record<string, unknown>) {
|
|
94
|
+
return {
|
|
95
|
+
...(accountId !== DEFAULT_ACCOUNT_ID ? { defaultAccount: accountId } : {}),
|
|
96
|
+
...patch,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseRelayUrls(raw: string): { relays: string[]; error?: string } {
|
|
101
|
+
const entries = raw
|
|
102
|
+
.split(/[,\n]/)
|
|
103
|
+
.map((entry) => entry.trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
const relays: string[] = [];
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = new URL(entry);
|
|
109
|
+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
110
|
+
return { relays: [], error: `Relay must use ws:// or wss:// (${entry})` };
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
return { relays: [], error: `Invalid relay URL: ${entry}` };
|
|
114
|
+
}
|
|
115
|
+
relays.push(entry);
|
|
116
|
+
}
|
|
117
|
+
return { relays: [...new Set(relays)] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function looksLikeNostrPrivateKey(privateKey: string): boolean {
|
|
121
|
+
return privateKey.startsWith("nsec1") || /^[0-9a-fA-F]{64}$/.test(privateKey);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nostrSetupAdapter: ChannelSetupAdapter = {
|
|
125
|
+
resolveAccountId: ({ cfg, accountId }) =>
|
|
126
|
+
accountId?.trim() || resolveDefaultSetupNostrAccountId(cfg),
|
|
127
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
128
|
+
patchTopLevelChannelConfigSection({
|
|
129
|
+
cfg,
|
|
130
|
+
channel,
|
|
131
|
+
patch: buildNostrSetupPatch(accountId, name?.trim() ? { name: name.trim() } : {}),
|
|
132
|
+
}),
|
|
133
|
+
validateInput: ({ input }) => {
|
|
134
|
+
const typedInput = input as {
|
|
135
|
+
useEnv?: boolean;
|
|
136
|
+
privateKey?: string;
|
|
137
|
+
relayUrls?: string;
|
|
138
|
+
};
|
|
139
|
+
if (!typedInput.useEnv) {
|
|
140
|
+
const privateKey = typedInput.privateKey?.trim();
|
|
141
|
+
if (!privateKey) {
|
|
142
|
+
return "Nostr requires --private-key or --use-env.";
|
|
143
|
+
}
|
|
144
|
+
if (!looksLikeNostrPrivateKey(privateKey)) {
|
|
145
|
+
return "Nostr private key must be valid nsec or 64-character hex.";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (typedInput.relayUrls?.trim()) {
|
|
149
|
+
return parseRelayUrls(typedInput.relayUrls).error ?? null;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
154
|
+
const typedInput = input as {
|
|
155
|
+
useEnv?: boolean;
|
|
156
|
+
privateKey?: string;
|
|
157
|
+
relayUrls?: string;
|
|
158
|
+
};
|
|
159
|
+
const relayResult = typedInput.relayUrls?.trim()
|
|
160
|
+
? parseRelayUrls(typedInput.relayUrls)
|
|
161
|
+
: { relays: [] };
|
|
162
|
+
return patchTopLevelChannelConfigSection({
|
|
163
|
+
cfg,
|
|
164
|
+
channel,
|
|
165
|
+
enabled: true,
|
|
166
|
+
clearFields: typedInput.useEnv ? ["privateKey"] : undefined,
|
|
167
|
+
patch: buildNostrSetupPatch(accountId, {
|
|
168
|
+
...(typedInput.useEnv ? {} : { privateKey: typedInput.privateKey?.trim() }),
|
|
169
|
+
...(relayResult.relays.length > 0 ? { relays: relayResult.relays } : {}),
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const nostrSetupWizard = createDelegatedSetupWizardProxy({
|
|
176
|
+
channel,
|
|
177
|
+
loadWizard: async () => (await import("./setup-surface.js")).nostrSetupWizard,
|
|
178
|
+
status: {
|
|
179
|
+
...createStandardChannelSetupStatus({
|
|
180
|
+
channelLabel: "Nostr",
|
|
181
|
+
configuredLabel: t("wizard.channels.statusConfigured"),
|
|
182
|
+
unconfiguredLabel: t("wizard.channels.statusNeedsPrivateKey"),
|
|
183
|
+
configuredHint: t("wizard.channels.statusConfigured"),
|
|
184
|
+
unconfiguredHint: t("wizard.channels.statusNeedsPrivateKey"),
|
|
185
|
+
configuredScore: 1,
|
|
186
|
+
unconfiguredScore: 0,
|
|
187
|
+
includeStatusLine: true,
|
|
188
|
+
resolveConfigured: ({ cfg, accountId }) =>
|
|
189
|
+
resolveSetupNostrAccount({ cfg, accountId }).configured,
|
|
190
|
+
resolveExtraStatusLines: ({ cfg }) => {
|
|
191
|
+
const account = resolveSetupNostrAccount({ cfg });
|
|
192
|
+
return [`Relays: ${account.relays.length || DEFAULT_RELAYS.length}`];
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
resolveShouldPromptAccountIds: () => false,
|
|
197
|
+
delegatePrepare: true,
|
|
198
|
+
delegateFinalize: true,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const nostrSetupPlugin: ChannelPlugin<ResolvedNostrSetupAccount> = {
|
|
202
|
+
id: channel,
|
|
203
|
+
meta: {
|
|
204
|
+
id: channel,
|
|
205
|
+
label: "Nostr",
|
|
206
|
+
selectionLabel: "Nostr",
|
|
207
|
+
docsPath: "/channels/nostr",
|
|
208
|
+
docsLabel: "nostr",
|
|
209
|
+
blurb: "Decentralized DMs via Nostr relays (NIP-04)",
|
|
210
|
+
order: 100,
|
|
211
|
+
},
|
|
212
|
+
capabilities: {
|
|
213
|
+
chatTypes: ["direct"],
|
|
214
|
+
media: false,
|
|
215
|
+
},
|
|
216
|
+
reload: { configPrefixes: ["channels.nostr"] },
|
|
217
|
+
configSchema: buildChannelConfigSchema(NostrConfigSchema),
|
|
218
|
+
setup: nostrSetupAdapter,
|
|
219
|
+
setupWizard: nostrSetupWizard,
|
|
220
|
+
config: {
|
|
221
|
+
listAccountIds: listSetupNostrAccountIds,
|
|
222
|
+
resolveAccount: (cfg, accountId) => resolveSetupNostrAccount({ cfg, accountId }),
|
|
223
|
+
defaultAccountId: resolveDefaultSetupNostrAccountId,
|
|
224
|
+
isConfigured: (account) => account.configured,
|
|
225
|
+
describeAccount: (account) =>
|
|
226
|
+
describeAccountSnapshot({
|
|
227
|
+
account,
|
|
228
|
+
configured: account.configured,
|
|
229
|
+
extra: {
|
|
230
|
+
publicKey: account.publicKey,
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
},
|
|
234
|
+
};
|