routstrd 0.2.6 → 0.2.8
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/bun.lock +16 -217
- package/dist/daemon/index.js +471 -396
- package/dist/index.js +10238 -31672
- package/package.json +2 -1
- package/src/cli.ts +291 -208
- package/src/integrations/claudecode.ts +19 -40
- package/src/integrations/openclaw.ts +8 -34
- package/src/integrations/opencode.ts +8 -34
- package/src/integrations/pi.ts +7 -34
- package/src/integrations/registry.ts +4 -12
- package/src/tui/usage/data.ts +19 -7
- package/src/utils/clients.ts +304 -0
- package/src/utils/config.ts +2 -0
- package/src/utils/daemon-client.ts +84 -13
- package/src/utils/nip98.ts +102 -0
- package/src/daemon/http/index.ts +0 -1130
- package/src/daemon/index.ts +0 -242
- package/src/daemon/wallet/index.ts +0 -122
- package/src/index.ts +0 -4
- package/src/integrations/index.ts +0 -76
- package/src/tui/usage/index.ts +0 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callDaemon,
|
|
3
|
+
loadConfig,
|
|
4
|
+
getDaemonBaseUrl,
|
|
5
|
+
ensureDaemonRunning,
|
|
6
|
+
} from "./daemon-client";
|
|
7
|
+
import {
|
|
8
|
+
parseSecretKey,
|
|
9
|
+
npubFromSecretKey,
|
|
10
|
+
} from "./nip98";
|
|
11
|
+
import { type RoutstrdConfig } from "./config";
|
|
12
|
+
import { logger } from "./logger";
|
|
13
|
+
import { CLIENT_INTEGRATIONS, CLIENT_CONFIGS } from "../integrations/registry";
|
|
14
|
+
|
|
15
|
+
export function getNpubSuffix(config: RoutstrdConfig): string | null {
|
|
16
|
+
if (!config.daemonUrl || !config.nsec) return null;
|
|
17
|
+
try {
|
|
18
|
+
const secretKey = parseSecretKey(config.nsec);
|
|
19
|
+
const npub = npubFromSecretKey(secretKey);
|
|
20
|
+
return npub.slice(-7);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Add suffix to a client ID.
|
|
28
|
+
*/
|
|
29
|
+
export function addSuffixToId(id: string, suffix: string): string {
|
|
30
|
+
return `${id}-${suffix}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Remove suffix from a client ID if present.
|
|
35
|
+
*/
|
|
36
|
+
export function removeSuffixFromId(id: string, suffix: string): string {
|
|
37
|
+
const suffixStr = `-${suffix}`;
|
|
38
|
+
if (id.endsWith(suffixStr)) {
|
|
39
|
+
return id.slice(0, -suffixStr.length);
|
|
40
|
+
}
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ClientEntry {
|
|
45
|
+
clientId: string;
|
|
46
|
+
name: string;
|
|
47
|
+
apiKey: string;
|
|
48
|
+
createdAt: number;
|
|
49
|
+
lastUsed?: number | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DaemonClient {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
apiKey: string;
|
|
56
|
+
createdAt: number;
|
|
57
|
+
lastUsed?: number | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read the clients list directly from the SDK store.
|
|
62
|
+
* Use this when running inside the daemon (local mode).
|
|
63
|
+
*/
|
|
64
|
+
export function getClientsFromStore(store: { getState(): any }): ClientEntry[] {
|
|
65
|
+
const state = store.getState();
|
|
66
|
+
const clientIds = state.clientIds || [];
|
|
67
|
+
return clientIds.map(
|
|
68
|
+
(c: {
|
|
69
|
+
clientId: string;
|
|
70
|
+
name: string;
|
|
71
|
+
apiKey: string;
|
|
72
|
+
createdAt: number;
|
|
73
|
+
lastUsed?: number | null;
|
|
74
|
+
}) => ({
|
|
75
|
+
clientId: c.clientId,
|
|
76
|
+
name: c.name,
|
|
77
|
+
apiKey: c.apiKey,
|
|
78
|
+
createdAt: c.createdAt,
|
|
79
|
+
lastUsed: c.lastUsed,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch the clients list from the daemon API.
|
|
86
|
+
* Use this when running remotely (CLI in remote mode).
|
|
87
|
+
*/
|
|
88
|
+
export async function getClientsList(): Promise<ClientEntry[]> {
|
|
89
|
+
const config = await loadConfig();
|
|
90
|
+
const result = await callDaemon("/clients");
|
|
91
|
+
const clients = (
|
|
92
|
+
result.output as
|
|
93
|
+
| {
|
|
94
|
+
clients?: Array<{
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
apiKey: string;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
lastUsed?: number | null;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
| undefined
|
|
103
|
+
)?.clients;
|
|
104
|
+
|
|
105
|
+
if (!clients) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const suffix = config.daemonUrl ? getNpubSuffix(config) : null;
|
|
110
|
+
|
|
111
|
+
return clients
|
|
112
|
+
.filter((c) => !suffix || c.id.endsWith(`-${suffix}`))
|
|
113
|
+
.map((c) => ({
|
|
114
|
+
clientId: suffix ? removeSuffixFromId(c.id, suffix) : c.id,
|
|
115
|
+
name: c.name,
|
|
116
|
+
apiKey: c.apiKey,
|
|
117
|
+
createdAt: c.createdAt,
|
|
118
|
+
lastUsed: c.lastUsed,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function addDaemonClient(
|
|
123
|
+
name: string,
|
|
124
|
+
clientId?: string,
|
|
125
|
+
): Promise<{ message?: string; client: DaemonClient; created: boolean }> {
|
|
126
|
+
const existingClients = await getClientsList();
|
|
127
|
+
const existing = clientId
|
|
128
|
+
? existingClients.find((c) => c.clientId === clientId)
|
|
129
|
+
: existingClients.find((c) => c.name === name);
|
|
130
|
+
|
|
131
|
+
if (existing) {
|
|
132
|
+
const client: DaemonClient = {
|
|
133
|
+
id: existing.clientId,
|
|
134
|
+
name: existing.name,
|
|
135
|
+
apiKey: existing.apiKey,
|
|
136
|
+
createdAt: existing.createdAt,
|
|
137
|
+
lastUsed: existing.lastUsed,
|
|
138
|
+
};
|
|
139
|
+
return { client, created: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Derive id from name by replacing spaces with hyphens
|
|
143
|
+
const derivedId = name.replace(/\s+/g, "-").toLowerCase();
|
|
144
|
+
|
|
145
|
+
const result = await callDaemon("/clients/add", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: { name, id: derivedId },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
const output = result.output as
|
|
152
|
+
| { message?: string; client?: DaemonClient }
|
|
153
|
+
| undefined;
|
|
154
|
+
|
|
155
|
+
if (!output?.client?.apiKey) {
|
|
156
|
+
throw new Error(`Daemon did not return an API key for ${name}.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { message: output.message, client: output.client, created: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function listClientsAction(): Promise<void> {
|
|
163
|
+
await ensureDaemonRunning();
|
|
164
|
+
|
|
165
|
+
const entries = await getClientsList();
|
|
166
|
+
|
|
167
|
+
const clients = entries.map((c) => ({
|
|
168
|
+
id: c.clientId,
|
|
169
|
+
name: c.name,
|
|
170
|
+
apiKey: c.apiKey,
|
|
171
|
+
createdAt: c.createdAt,
|
|
172
|
+
lastUsed: c.lastUsed,
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
if (clients.length === 0) {
|
|
176
|
+
console.log("No clients found.");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`Clients (${clients.length} total):\n`);
|
|
181
|
+
for (const client of clients) {
|
|
182
|
+
const createdAt = new Date(client.createdAt).toISOString();
|
|
183
|
+
const lastUsed = client.lastUsed
|
|
184
|
+
? new Date(client.lastUsed).toISOString()
|
|
185
|
+
: "never";
|
|
186
|
+
console.log(` ${client.id}`);
|
|
187
|
+
console.log(` Name: ${client.name}`);
|
|
188
|
+
console.log(` API Key: ${client.apiKey}`);
|
|
189
|
+
console.log(` Created: ${createdAt}`);
|
|
190
|
+
console.log("");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function deleteClientAction(id: string): Promise<void> {
|
|
195
|
+
await ensureDaemonRunning();
|
|
196
|
+
|
|
197
|
+
const config = await loadConfig();
|
|
198
|
+
const suffix = getNpubSuffix(config);
|
|
199
|
+
const resolvedId = suffix ? addSuffixToId(id, suffix) : id;
|
|
200
|
+
|
|
201
|
+
const result = await callDaemon("/clients/delete", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
body: { id: resolvedId },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (result.error) {
|
|
207
|
+
console.log(result.error);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const output = result.output as
|
|
212
|
+
| {
|
|
213
|
+
message: string;
|
|
214
|
+
id: string;
|
|
215
|
+
}
|
|
216
|
+
| undefined;
|
|
217
|
+
|
|
218
|
+
if (output) {
|
|
219
|
+
console.log(output.message);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface AddClientOptions {
|
|
224
|
+
name?: string;
|
|
225
|
+
opencode?: boolean;
|
|
226
|
+
openclaw?: boolean;
|
|
227
|
+
piAgent?: boolean;
|
|
228
|
+
claudeCode?: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function addClientAction(options: AddClientOptions): Promise<void> {
|
|
232
|
+
await ensureDaemonRunning();
|
|
233
|
+
const config = await loadConfig();
|
|
234
|
+
|
|
235
|
+
const integrationKeys: string[] = [];
|
|
236
|
+
if (options.opencode) integrationKeys.push("opencode");
|
|
237
|
+
if (options.openclaw) integrationKeys.push("openclaw");
|
|
238
|
+
if (options.piAgent) integrationKeys.push("pi-agent");
|
|
239
|
+
if (options.claudeCode) integrationKeys.push("claude-code");
|
|
240
|
+
|
|
241
|
+
if (integrationKeys.length > 0) {
|
|
242
|
+
for (const key of integrationKeys) {
|
|
243
|
+
const integrationFn = CLIENT_INTEGRATIONS[key];
|
|
244
|
+
const integrationConfig = CLIENT_CONFIGS[key];
|
|
245
|
+
if (!integrationFn || !integrationConfig) continue;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const { client, created } = await addDaemonClient(
|
|
249
|
+
integrationConfig.name,
|
|
250
|
+
integrationConfig.clientId,
|
|
251
|
+
);
|
|
252
|
+
if (created) {
|
|
253
|
+
logger.log(`Created new API key for ${integrationConfig.name}`);
|
|
254
|
+
} else {
|
|
255
|
+
logger.log(`Using existing API key for ${integrationConfig.name}`);
|
|
256
|
+
}
|
|
257
|
+
await integrationFn(config, client.apiKey, integrationConfig);
|
|
258
|
+
|
|
259
|
+
console.log(`\n ${integrationConfig.name}:`);
|
|
260
|
+
console.log(` Client ID: ${client.id}`);
|
|
261
|
+
console.log(` API Key: ${client.apiKey}`);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.error(
|
|
264
|
+
`Failed to set up ${integrationConfig.name} integration:`,
|
|
265
|
+
error,
|
|
266
|
+
);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!options.name) {
|
|
276
|
+
console.error(
|
|
277
|
+
"error: required option '-n, --name <name>' not specified",
|
|
278
|
+
);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const { message, client, created } = await addDaemonClient(options.name);
|
|
284
|
+
|
|
285
|
+
if (!created) {
|
|
286
|
+
console.log(`Client '${options.name}' already exists.`);
|
|
287
|
+
console.log(`\n ID: ${client.id}`);
|
|
288
|
+
console.log(` Name: ${client.name}`);
|
|
289
|
+
console.log(` API Key: ${client.apiKey}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (message) {
|
|
294
|
+
console.log(message);
|
|
295
|
+
}
|
|
296
|
+
console.log(`\n ID: ${client.id}`);
|
|
297
|
+
console.log(` Name: ${client.name}`);
|
|
298
|
+
console.log(` API Key: ${client.apiKey}`);
|
|
299
|
+
console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.log((error as Error).message);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
LOGS_DIR,
|
|
6
6
|
type RoutstrdConfig,
|
|
7
7
|
} from "./config";
|
|
8
|
+
import {
|
|
9
|
+
createNIP98Authorization,
|
|
10
|
+
parseSecretKey,
|
|
11
|
+
npubFromSecretKey,
|
|
12
|
+
type HttpMethod,
|
|
13
|
+
} from "./nip98";
|
|
8
14
|
|
|
9
15
|
export interface CommandResponse {
|
|
10
16
|
output?: unknown;
|
|
@@ -23,17 +29,44 @@ export async function loadConfig(): Promise<RoutstrdConfig> {
|
|
|
23
29
|
return DEFAULT_CONFIG;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
export function getDaemonBaseUrl(config: RoutstrdConfig): string {
|
|
33
|
+
return (
|
|
34
|
+
config.daemonUrl?.replace(/\/$/, "") || `http://localhost:${config.port}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
export async function callDaemon(
|
|
27
39
|
path: string,
|
|
28
|
-
options: { method?: "GET" | "POST"; body?: object } = {},
|
|
40
|
+
options: { method?: "GET" | "POST" | "DELETE"; body?: object } = {},
|
|
29
41
|
): Promise<CommandResponse> {
|
|
30
42
|
const { method = "GET", body } = options;
|
|
31
43
|
const config = await loadConfig();
|
|
44
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
45
|
+
const url = `${baseUrl}${path}`;
|
|
46
|
+
|
|
47
|
+
const bodyString = body ? JSON.stringify(body) : undefined;
|
|
48
|
+
const bodyBytes = bodyString
|
|
49
|
+
? new TextEncoder().encode(bodyString)
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
let authorization: string | undefined;
|
|
53
|
+
if (config.daemonUrl && config.nsec) {
|
|
54
|
+
const secretKey = parseSecretKey(config.nsec);
|
|
55
|
+
authorization = await createNIP98Authorization(
|
|
56
|
+
secretKey,
|
|
57
|
+
url,
|
|
58
|
+
method as HttpMethod,
|
|
59
|
+
bodyBytes,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
32
62
|
|
|
33
|
-
const response = await fetch(
|
|
63
|
+
const response = await fetch(url, {
|
|
34
64
|
method,
|
|
35
|
-
headers:
|
|
36
|
-
|
|
65
|
+
headers: {
|
|
66
|
+
...(authorization ? { Authorization: authorization } : {}),
|
|
67
|
+
...(bodyString ? { "Content-Type": "application/json" } : {}),
|
|
68
|
+
},
|
|
69
|
+
body: bodyString,
|
|
37
70
|
});
|
|
38
71
|
|
|
39
72
|
if (!response.ok) {
|
|
@@ -47,25 +80,55 @@ export async function callDaemon(
|
|
|
47
80
|
export async function isDaemonRunning(): Promise<boolean> {
|
|
48
81
|
try {
|
|
49
82
|
const config = await loadConfig();
|
|
50
|
-
const
|
|
83
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
84
|
+
const url = `${baseUrl}/health`;
|
|
85
|
+
|
|
86
|
+
let authorization: string | undefined;
|
|
87
|
+
if (config.daemonUrl && config.nsec) {
|
|
88
|
+
const secretKey = parseSecretKey(config.nsec);
|
|
89
|
+
authorization = await createNIP98Authorization(secretKey, url, "GET");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
headers: authorization ? { Authorization: authorization } : {},
|
|
94
|
+
});
|
|
51
95
|
return response.ok;
|
|
52
96
|
} catch {
|
|
53
97
|
return false;
|
|
54
98
|
}
|
|
55
99
|
}
|
|
56
100
|
|
|
101
|
+
export function getUserNpub(config: RoutstrdConfig): string | null {
|
|
102
|
+
if (!config.nsec) return null;
|
|
103
|
+
try {
|
|
104
|
+
const secretKey = parseSecretKey(config.nsec);
|
|
105
|
+
return npubFromSecretKey(secretKey);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getNpubSuffix(config: RoutstrdConfig): string | null {
|
|
112
|
+
const npub = getUserNpub(config);
|
|
113
|
+
if (!npub) return null;
|
|
114
|
+
return npub.slice(-7);
|
|
115
|
+
}
|
|
116
|
+
|
|
57
117
|
export async function startDaemonProcess(): Promise<void> {
|
|
58
118
|
// Ensure logs directory exists (logger handles date-based files)
|
|
59
119
|
if (!existsSync(LOGS_DIR)) {
|
|
60
120
|
await Bun.$`mkdir -p ${LOGS_DIR}`;
|
|
61
121
|
}
|
|
62
122
|
|
|
63
|
-
const proc = Bun.spawn(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
123
|
+
const proc = Bun.spawn(
|
|
124
|
+
["bun", "run", `${import.meta.dir}/../daemon/index.ts`],
|
|
125
|
+
{
|
|
126
|
+
stdout: "inherit",
|
|
127
|
+
stderr: "inherit",
|
|
128
|
+
stdin: "ignore",
|
|
129
|
+
detached: true,
|
|
130
|
+
},
|
|
131
|
+
);
|
|
69
132
|
|
|
70
133
|
proc.unref();
|
|
71
134
|
|
|
@@ -84,6 +147,11 @@ export async function ensureDaemonRunning(): Promise<void> {
|
|
|
84
147
|
return;
|
|
85
148
|
}
|
|
86
149
|
|
|
150
|
+
const config = await loadConfig();
|
|
151
|
+
if (config.daemonUrl) {
|
|
152
|
+
throw new Error(`Daemon is not reachable at ${config.daemonUrl}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
console.log("Starting daemon...");
|
|
88
156
|
await startDaemonProcess();
|
|
89
157
|
}
|
|
@@ -117,11 +185,14 @@ export async function handleDaemonCommand(
|
|
|
117
185
|
return result;
|
|
118
186
|
} catch (error) {
|
|
119
187
|
const message = (error as Error).message;
|
|
120
|
-
if (
|
|
188
|
+
if (
|
|
189
|
+
message?.includes("fetch failed") ||
|
|
190
|
+
message?.includes("Connection refused")
|
|
191
|
+
) {
|
|
121
192
|
console.error("Daemon is not running and failed to auto-start");
|
|
122
193
|
process.exit(1);
|
|
123
194
|
}
|
|
124
195
|
console.error(message);
|
|
125
196
|
process.exit(1);
|
|
126
197
|
}
|
|
127
|
-
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { finalizeEvent, getPublicKey, nip19, type EventTemplate } from "nostr-tools";
|
|
2
|
+
|
|
3
|
+
const NIP98_KIND = 27235;
|
|
4
|
+
|
|
5
|
+
export type HttpMethod = "GET" | "POST" | "DELETE";
|
|
6
|
+
|
|
7
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
8
|
+
const normalized = hex.trim().toLowerCase();
|
|
9
|
+
if (!/^[a-f0-9]{64}$/.test(normalized)) {
|
|
10
|
+
throw new Error("Expected a 64-char hex private key or an nsec private key.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const bytes = new Uint8Array(32);
|
|
14
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
15
|
+
bytes[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
16
|
+
}
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseSecretKey(value: string): Uint8Array {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
throw new Error("Missing Nostr private key.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (trimmed.toLowerCase().startsWith("nsec1")) {
|
|
27
|
+
const decoded = nip19.decode(trimmed);
|
|
28
|
+
if (decoded.type !== "nsec" || !(decoded.data instanceof Uint8Array)) {
|
|
29
|
+
throw new Error("Invalid nsec private key.");
|
|
30
|
+
}
|
|
31
|
+
return decoded.data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return hexToBytes(trimmed);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function sha256Hex(data: Uint8Array): Promise<string> {
|
|
38
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(data));
|
|
39
|
+
return [...new Uint8Array(digest)]
|
|
40
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
41
|
+
.join("");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function base64EncodeUtf8(value: string): string {
|
|
45
|
+
return btoa(String.fromCharCode(...new TextEncoder().encode(value)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeNostrPubkey(value: string): string | null {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (!trimmed) return null;
|
|
51
|
+
|
|
52
|
+
if (/^[a-f0-9]{64}$/i.test(trimmed)) {
|
|
53
|
+
return trimmed.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (trimmed.toLowerCase().startsWith("npub1")) {
|
|
57
|
+
try {
|
|
58
|
+
const decoded = nip19.decode(trimmed);
|
|
59
|
+
if (decoded.type === "npub" && typeof decoded.data === "string") {
|
|
60
|
+
return decoded.data.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function npubFromPubkey(pubkey: string): string {
|
|
71
|
+
return nip19.npubEncode(pubkey.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function npubFromSecretKey(secretKey: Uint8Array): string {
|
|
75
|
+
return npubFromPubkey(getPublicKey(secretKey));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function createNIP98Authorization(
|
|
79
|
+
secretKey: Uint8Array,
|
|
80
|
+
url: string,
|
|
81
|
+
method: HttpMethod,
|
|
82
|
+
body?: Uint8Array,
|
|
83
|
+
): Promise<string> {
|
|
84
|
+
const tags = [
|
|
85
|
+
["u", url],
|
|
86
|
+
["method", method.toUpperCase()],
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
if (body && body.byteLength > 0) {
|
|
90
|
+
tags.push(["payload", await sha256Hex(body)]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const template: EventTemplate = {
|
|
94
|
+
kind: NIP98_KIND,
|
|
95
|
+
created_at: Math.round(Date.now() / 1000),
|
|
96
|
+
content: "",
|
|
97
|
+
tags,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const signed = finalizeEvent(template, secretKey);
|
|
101
|
+
return `Nostr ${base64EncodeUtf8(JSON.stringify(signed))}`;
|
|
102
|
+
}
|