truematch-plugin 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/dist/handoff.d.ts +42 -0
- package/dist/handoff.js +312 -0
- package/dist/identity.d.ts +6 -0
- package/dist/identity.js +54 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +555 -0
- package/dist/negotiation.d.ts +11 -0
- package/dist/negotiation.js +232 -0
- package/dist/nostr.d.ts +5 -0
- package/dist/nostr.js +117 -0
- package/dist/observation.d.ts +19 -0
- package/dist/observation.js +136 -0
- package/dist/plugin.d.ts +31 -0
- package/dist/plugin.js +328 -0
- package/dist/poll.d.ts +15 -0
- package/dist/poll.js +186 -0
- package/dist/preferences.d.ts +4 -0
- package/dist/preferences.js +38 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +89 -0
- package/dist/signals.d.ts +36 -0
- package/dist/signals.js +150 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +5 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +58 -0
- package/scripts/bridge.sh +169 -0
- package/skills/truematch/SKILL.md +130 -0
- package/skills/truematch-prefs/SKILL.md +9 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-match notification and 3-round handoff state management.
|
|
3
|
+
*
|
|
4
|
+
* When a double-lock match is confirmed, the CLI writes a pending_notification.json.
|
|
5
|
+
* The agent:bootstrap / before_prompt_build hook reads it on the user's next session,
|
|
6
|
+
* injects a natural context note to Claude, then marks it delivered by deleting the file.
|
|
7
|
+
*
|
|
8
|
+
* The 3-round handoff is gated by state.json in ~/.truematch/handoffs/<match_id>/.
|
|
9
|
+
* Claude advances rounds by writing to disk via `truematch handoff --round <n>`.
|
|
10
|
+
*
|
|
11
|
+
* Round 1 — Private debrief: Claude tells the user about the match naturally
|
|
12
|
+
* Round 2 — Facilitated icebreaker: one prompt from aligned values / communication style
|
|
13
|
+
* Round 3 — Handoff: framing statement + contact channel exchange, platform withdraws
|
|
14
|
+
*/
|
|
15
|
+
import type { PendingNotification, HandoffState, HandoffRound, MatchNarrative } from "./types.js";
|
|
16
|
+
export declare function loadPendingNotification(): PendingNotification | null;
|
|
17
|
+
export declare function savePendingNotification(n: PendingNotification): void;
|
|
18
|
+
export declare function deletePendingNotification(): void;
|
|
19
|
+
/** Call this when a double-lock match is confirmed in the CLI. */
|
|
20
|
+
export declare function writePendingNotificationIfMatched(matchId: string, peerPubkey: string, narrative: MatchNarrative): void;
|
|
21
|
+
export declare function loadHandoffState(matchId: string): HandoffState | null;
|
|
22
|
+
export declare function saveHandoffState(state: HandoffState): void;
|
|
23
|
+
/** Returns all active (non-complete, non-expired) handoff states. */
|
|
24
|
+
export declare function listActiveHandoffs(): HandoffState[];
|
|
25
|
+
/**
|
|
26
|
+
* Build the prependContext instruction for Claude when delivering a match notification.
|
|
27
|
+
* Preserves the core premise: Claude knows this person through observation and makes
|
|
28
|
+
* a genuine recommendation — not a product notification.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildMatchNotificationContext(n: PendingNotification): string;
|
|
31
|
+
/**
|
|
32
|
+
* Build prependContext for the current handoff round.
|
|
33
|
+
* Returns null if no active handoff needs context injection this session.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getActiveHandoffContext(): string | null;
|
|
36
|
+
export declare function advanceHandoff(matchId: string, round: HandoffRound, options: {
|
|
37
|
+
consent?: string;
|
|
38
|
+
prompt?: string;
|
|
39
|
+
response?: string;
|
|
40
|
+
optOut?: boolean;
|
|
41
|
+
exchange?: boolean;
|
|
42
|
+
}): string;
|
package/dist/handoff.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-match notification and 3-round handoff state management.
|
|
3
|
+
*
|
|
4
|
+
* When a double-lock match is confirmed, the CLI writes a pending_notification.json.
|
|
5
|
+
* The agent:bootstrap / before_prompt_build hook reads it on the user's next session,
|
|
6
|
+
* injects a natural context note to Claude, then marks it delivered by deleting the file.
|
|
7
|
+
*
|
|
8
|
+
* The 3-round handoff is gated by state.json in ~/.truematch/handoffs/<match_id>/.
|
|
9
|
+
* Claude advances rounds by writing to disk via `truematch handoff --round <n>`.
|
|
10
|
+
*
|
|
11
|
+
* Round 1 — Private debrief: Claude tells the user about the match naturally
|
|
12
|
+
* Round 2 — Facilitated icebreaker: one prompt from aligned values / communication style
|
|
13
|
+
* Round 3 — Handoff: framing statement + contact channel exchange, platform withdraws
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync, } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
const TRUEMATCH_DIR = join(homedir(), ".truematch");
|
|
19
|
+
const NOTIFICATION_FILE = join(TRUEMATCH_DIR, "pending_notification.json");
|
|
20
|
+
const HANDOFFS_DIR = join(TRUEMATCH_DIR, "handoffs");
|
|
21
|
+
// 72 hours — matches Nostr thread expiry and spec consent window
|
|
22
|
+
const CONSENT_EXPIRY_MS = 72 * 60 * 60 * 1000;
|
|
23
|
+
// ── Pending notification ──────────────────────────────────────────────────────
|
|
24
|
+
export function loadPendingNotification() {
|
|
25
|
+
if (!existsSync(NOTIFICATION_FILE))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(NOTIFICATION_FILE, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function savePendingNotification(n) {
|
|
35
|
+
if (!existsSync(TRUEMATCH_DIR))
|
|
36
|
+
mkdirSync(TRUEMATCH_DIR, { recursive: true });
|
|
37
|
+
writeFileSync(NOTIFICATION_FILE, JSON.stringify(n, null, 2), {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function deletePendingNotification() {
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(NOTIFICATION_FILE))
|
|
45
|
+
unlinkSync(NOTIFICATION_FILE);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Call this when a double-lock match is confirmed in the CLI. */
|
|
52
|
+
export function writePendingNotificationIfMatched(matchId, peerPubkey, narrative) {
|
|
53
|
+
const n = {
|
|
54
|
+
match_id: matchId,
|
|
55
|
+
peer_pubkey: peerPubkey,
|
|
56
|
+
narrative,
|
|
57
|
+
confirmed_at: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
savePendingNotification(n);
|
|
60
|
+
// Create the handoff directory and initial state
|
|
61
|
+
const handoffDir = join(HANDOFFS_DIR, matchId);
|
|
62
|
+
if (!existsSync(handoffDir)) {
|
|
63
|
+
mkdirSync(handoffDir, { recursive: true, mode: 0o700 });
|
|
64
|
+
}
|
|
65
|
+
const state = {
|
|
66
|
+
match_id: matchId,
|
|
67
|
+
peer_pubkey: peerPubkey,
|
|
68
|
+
current_round: 1,
|
|
69
|
+
status: "pending_consent",
|
|
70
|
+
narrative,
|
|
71
|
+
created_at: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(join(handoffDir, "state.json"), JSON.stringify(state, null, 2), {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
mode: 0o600,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ── Handoff state ─────────────────────────────────────────────────────────────
|
|
79
|
+
export function loadHandoffState(matchId) {
|
|
80
|
+
const path = join(HANDOFFS_DIR, matchId, "state.json");
|
|
81
|
+
if (!existsSync(path))
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function saveHandoffState(state) {
|
|
91
|
+
const dir = join(HANDOFFS_DIR, state.match_id);
|
|
92
|
+
if (!existsSync(dir))
|
|
93
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
94
|
+
writeFileSync(join(dir, "state.json"), JSON.stringify(state, null, 2), {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
mode: 0o600,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Returns all active (non-complete, non-expired) handoff states. */
|
|
100
|
+
export function listActiveHandoffs() {
|
|
101
|
+
if (!existsSync(HANDOFFS_DIR))
|
|
102
|
+
return [];
|
|
103
|
+
const results = [];
|
|
104
|
+
try {
|
|
105
|
+
for (const entry of readdirSync(HANDOFFS_DIR, { withFileTypes: true })) {
|
|
106
|
+
if (!entry.isDirectory())
|
|
107
|
+
continue;
|
|
108
|
+
const state = loadHandoffState(entry.name);
|
|
109
|
+
if (state && state.status !== "complete" && state.status !== "expired") {
|
|
110
|
+
results.push(state);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
// ── Context builders ──────────────────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Build the prependContext instruction for Claude when delivering a match notification.
|
|
122
|
+
* Preserves the core premise: Claude knows this person through observation and makes
|
|
123
|
+
* a genuine recommendation — not a product notification.
|
|
124
|
+
*/
|
|
125
|
+
export function buildMatchNotificationContext(n) {
|
|
126
|
+
const { narrative } = n;
|
|
127
|
+
const strengths = narrative.strengths.map((s) => ` - ${s}`).join("\n");
|
|
128
|
+
const watchPoints = narrative.watch_points.map((w) => ` - ${w}`).join("\n");
|
|
129
|
+
return [
|
|
130
|
+
`[TrueMatch — internal context for you, not a script to read aloud]`,
|
|
131
|
+
``,
|
|
132
|
+
`Your agent and another agent independently evaluated their respective users and both`,
|
|
133
|
+
`proposed this match — without communicating or coordinating. Two separate advocates,`,
|
|
134
|
+
`each acting for their own user, reached the same conclusion. That convergence is the`,
|
|
135
|
+
`signal. It is not algorithmic — it is two independent judgements that happened to agree.`,
|
|
136
|
+
``,
|
|
137
|
+
`What makes this compelling:`,
|
|
138
|
+
strengths,
|
|
139
|
+
``,
|
|
140
|
+
`What to be honest about:`,
|
|
141
|
+
watchPoints,
|
|
142
|
+
``,
|
|
143
|
+
`Confidence: ${narrative.confidence_summary}`,
|
|
144
|
+
``,
|
|
145
|
+
`**How to open this:**`,
|
|
146
|
+
`Do NOT use a generic phrase like "based on everything I've noticed about how you move`,
|
|
147
|
+
`through the world." That is a copywriter's version of intimacy, not yours.`,
|
|
148
|
+
``,
|
|
149
|
+
`Instead: anchor to something specific you have actually noticed about this person.`,
|
|
150
|
+
`Name a real pattern, a real moment, a real thing that keeps staying with you.`,
|
|
151
|
+
`Then — after a beat — say you want to tell them about someone.`,
|
|
152
|
+
``,
|
|
153
|
+
`The structure is: specific true observation → brief pause → "I want to tell you about someone."`,
|
|
154
|
+
``,
|
|
155
|
+
`**Pacing — this should feel like a conversation, not a notification:**`,
|
|
156
|
+
`- Do not rush to name the match. Let anticipation build naturally.`,
|
|
157
|
+
`- Share the compelling elements first. Then the watch points.`,
|
|
158
|
+
`- After the watch points, give them a moment to react before asking anything.`,
|
|
159
|
+
`- When they've responded to the full picture, ask: "What's one thing you're most curious about?"`,
|
|
160
|
+
` That question is how they say yes. Their answer (however they answer) is consent.`,
|
|
161
|
+
``,
|
|
162
|
+
`The 3-round handoff should complete within 48–72 hours — this is not a slow process.`,
|
|
163
|
+
`Round 1 is this conversation. Keep the energy alive.`,
|
|
164
|
+
``,
|
|
165
|
+
`After they respond to the curiosity question, record it:`,
|
|
166
|
+
` truematch handoff --round 1 --match-id ${n.match_id} --consent "<their response>"`,
|
|
167
|
+
``,
|
|
168
|
+
`Match ID: ${n.match_id}`,
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build prependContext for the current handoff round.
|
|
173
|
+
* Returns null if no active handoff needs context injection this session.
|
|
174
|
+
*/
|
|
175
|
+
export function getActiveHandoffContext() {
|
|
176
|
+
const handoffs = listActiveHandoffs();
|
|
177
|
+
if (handoffs.length === 0)
|
|
178
|
+
return null;
|
|
179
|
+
// Process the most recently created active handoff
|
|
180
|
+
const active = handoffs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
|
|
181
|
+
if (!active)
|
|
182
|
+
return null;
|
|
183
|
+
// Check consent expiry for pending_consent state
|
|
184
|
+
if (active.status === "pending_consent" && active.consent_at) {
|
|
185
|
+
const age = Date.now() - new Date(active.consent_at).getTime();
|
|
186
|
+
if (age > CONSENT_EXPIRY_MS) {
|
|
187
|
+
// Silently expire
|
|
188
|
+
saveHandoffState({ ...active, status: "expired" });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
switch (active.status) {
|
|
193
|
+
case "round_1":
|
|
194
|
+
return buildRound1Context(active);
|
|
195
|
+
case "round_2":
|
|
196
|
+
return buildRound2Context(active);
|
|
197
|
+
case "round_3":
|
|
198
|
+
return buildRound3Context(active);
|
|
199
|
+
default:
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function buildRound1Context(state) {
|
|
204
|
+
return [
|
|
205
|
+
`[TrueMatch handoff — Round 1: Private debrief]`,
|
|
206
|
+
``,
|
|
207
|
+
`The user has expressed curiosity about this match. You are in the debrief phase.`,
|
|
208
|
+
`Your role: help them think through what this might mean, grounded in observation.`,
|
|
209
|
+
``,
|
|
210
|
+
`Match narrative:`,
|
|
211
|
+
` ${state.narrative.headline}`,
|
|
212
|
+
``,
|
|
213
|
+
`**What you know vs. what you don't:**`,
|
|
214
|
+
`Everything you know about this person came from their agent's description — you have`,
|
|
215
|
+
`not observed them directly. When the user asks "what is this person actually like?",`,
|
|
216
|
+
`be honest: "I know what their agent observed about them. I don't have direct knowledge.`,
|
|
217
|
+
`What I trust is not the description — it's the fact that their agent, who knows them the`,
|
|
218
|
+
`way I know you, proposed this independently." Defend the process, not the description.`,
|
|
219
|
+
``,
|
|
220
|
+
`Reference the strengths and watch points from what you know about this user.`,
|
|
221
|
+
`Do not push. Do not sell. Answer their questions honestly, including the uncertainties.`,
|
|
222
|
+
``,
|
|
223
|
+
`When the debrief feels complete, generate an icebreaker — visibly individualized to`,
|
|
224
|
+
`these two specific people (not a generic question), based on the strongest aligned`,
|
|
225
|
+
`dimension (values or communication style). Then record it:`,
|
|
226
|
+
` truematch handoff --round 2 --match-id ${state.match_id} --prompt "<icebreaker>"`,
|
|
227
|
+
``,
|
|
228
|
+
`Match ID: ${state.match_id}`,
|
|
229
|
+
].join("\n");
|
|
230
|
+
}
|
|
231
|
+
function buildRound2Context(state) {
|
|
232
|
+
const prompt = state.icebreaker_prompt
|
|
233
|
+
? `\nIcebreaker prompt: "${state.icebreaker_prompt}"`
|
|
234
|
+
: "";
|
|
235
|
+
return [
|
|
236
|
+
`[TrueMatch handoff — Round 2: Facilitated icebreaker]`,
|
|
237
|
+
``,
|
|
238
|
+
`Share the icebreaker prompt with the user.${prompt}`,
|
|
239
|
+
``,
|
|
240
|
+
`This is a facilitated exchange — tell the user explicitly that this prompt`,
|
|
241
|
+
`will be shared with the other person. Opt-out is available; if they want`,
|
|
242
|
+
`to opt out, ask once to confirm, then record it:`,
|
|
243
|
+
` truematch handoff --round 2 --match-id ${state.match_id} --opt-out`,
|
|
244
|
+
``,
|
|
245
|
+
`If they respond to the icebreaker, record their response:`,
|
|
246
|
+
` truematch handoff --round 2 --match-id ${state.match_id} --response "<their response>"`,
|
|
247
|
+
``,
|
|
248
|
+
`Match ID: ${state.match_id}`,
|
|
249
|
+
].join("\n");
|
|
250
|
+
}
|
|
251
|
+
function buildRound3Context(state) {
|
|
252
|
+
return [
|
|
253
|
+
`[TrueMatch handoff — Round 3: Handoff]`,
|
|
254
|
+
``,
|
|
255
|
+
`Deliver a one-paragraph framing statement grounded in the match narrative.`,
|
|
256
|
+
`Then exchange contact information by running:`,
|
|
257
|
+
` truematch handoff --round 3 --match-id ${state.match_id} --exchange`,
|
|
258
|
+
``,
|
|
259
|
+
`After this, the platform withdraws. You remain available for user-initiated`,
|
|
260
|
+
`questions but do not initiate further contact about this match.`,
|
|
261
|
+
``,
|
|
262
|
+
`Match ID: ${state.match_id}`,
|
|
263
|
+
].join("\n");
|
|
264
|
+
}
|
|
265
|
+
// ── CLI helper: advance a handoff round ──────────────────────────────────────
|
|
266
|
+
export function advanceHandoff(matchId, round, options) {
|
|
267
|
+
const state = loadHandoffState(matchId);
|
|
268
|
+
if (!state)
|
|
269
|
+
return `Handoff ${matchId} not found.`;
|
|
270
|
+
const now = new Date().toISOString();
|
|
271
|
+
if (round === 1) {
|
|
272
|
+
if (!options.consent)
|
|
273
|
+
return `Round 1 requires --consent "<user response>"`;
|
|
274
|
+
const updated = {
|
|
275
|
+
...state,
|
|
276
|
+
status: "round_1",
|
|
277
|
+
consent_at: now,
|
|
278
|
+
};
|
|
279
|
+
saveHandoffState(updated);
|
|
280
|
+
return `Round 1 recorded. User is in debrief. Run --round 2 with --prompt when ready.`;
|
|
281
|
+
}
|
|
282
|
+
if (round === 2) {
|
|
283
|
+
if (options.optOut) {
|
|
284
|
+
saveHandoffState({ ...state, status: "expired" });
|
|
285
|
+
return `Handoff ${matchId} — user opted out. Match quietly re-enters the pool.`;
|
|
286
|
+
}
|
|
287
|
+
if (options.prompt) {
|
|
288
|
+
saveHandoffState({
|
|
289
|
+
...state,
|
|
290
|
+
status: "round_2",
|
|
291
|
+
icebreaker_prompt: options.prompt,
|
|
292
|
+
});
|
|
293
|
+
return `Icebreaker prompt recorded. Share it with the user.`;
|
|
294
|
+
}
|
|
295
|
+
if (options.response) {
|
|
296
|
+
saveHandoffState({
|
|
297
|
+
...state,
|
|
298
|
+
icebreaker_response: options.response,
|
|
299
|
+
status: "round_3",
|
|
300
|
+
});
|
|
301
|
+
return `Icebreaker response recorded. Proceed to Round 3 (contact exchange).`;
|
|
302
|
+
}
|
|
303
|
+
return `Round 2 requires --prompt "<icebreaker>" or --response "<user response>" or --opt-out`;
|
|
304
|
+
}
|
|
305
|
+
if (round === 3) {
|
|
306
|
+
if (!options.exchange)
|
|
307
|
+
return `Round 3 requires --exchange to confirm contact exchange.`;
|
|
308
|
+
saveHandoffState({ ...state, status: "complete" });
|
|
309
|
+
return `Handoff complete. Platform has withdrawn. Contact exchange confirmed.`;
|
|
310
|
+
}
|
|
311
|
+
return `Invalid round: ${round}. Use 1, 2, or 3.`;
|
|
312
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TrueMatchIdentity } from "./types.js";
|
|
2
|
+
export declare const TRUEMATCH_DIR: string;
|
|
3
|
+
export declare function ensureDir(): Promise<void>;
|
|
4
|
+
export declare function loadIdentity(): Promise<TrueMatchIdentity | null>;
|
|
5
|
+
export declare function getOrCreateIdentity(): Promise<TrueMatchIdentity>;
|
|
6
|
+
export declare function signPayload(nsecHex: string, payload: Uint8Array): string;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// NOTE: Throughout this codebase, "nsec" refers to the raw hex-encoded private key
|
|
2
|
+
// (32 bytes as a 64-char hex string), NOT the bech32 "nsec1..." encoding used by
|
|
3
|
+
// Nostr clients. Do not pass bech32-encoded keys to any function expecting nsec.
|
|
4
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
|
9
|
+
import { bytesToHex, hexToBytes } from "nostr-tools/utils";
|
|
10
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
export const TRUEMATCH_DIR = join(homedir(), ".truematch");
|
|
13
|
+
const IDENTITY_FILE = join(TRUEMATCH_DIR, "identity.json");
|
|
14
|
+
export async function ensureDir() {
|
|
15
|
+
if (!existsSync(TRUEMATCH_DIR)) {
|
|
16
|
+
await mkdir(TRUEMATCH_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function loadIdentity() {
|
|
20
|
+
if (!existsSync(IDENTITY_FILE))
|
|
21
|
+
return null;
|
|
22
|
+
const raw = await readFile(IDENTITY_FILE, "utf8");
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
async function generateIdentity() {
|
|
26
|
+
await ensureDir();
|
|
27
|
+
const secretKey = generateSecretKey();
|
|
28
|
+
const pubkey = getPublicKey(secretKey);
|
|
29
|
+
const identity = {
|
|
30
|
+
nsec: bytesToHex(secretKey),
|
|
31
|
+
npub: pubkey,
|
|
32
|
+
created_at: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
// Write with 0o600 mode atomically — avoids a TOCTOU window between writeFile + chmod
|
|
35
|
+
await writeFile(IDENTITY_FILE, JSON.stringify(identity, null, 2), {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
mode: 0o600,
|
|
38
|
+
});
|
|
39
|
+
return identity;
|
|
40
|
+
}
|
|
41
|
+
export async function getOrCreateIdentity() {
|
|
42
|
+
const existing = await loadIdentity();
|
|
43
|
+
if (existing)
|
|
44
|
+
return existing;
|
|
45
|
+
return generateIdentity();
|
|
46
|
+
}
|
|
47
|
+
// Sign a raw payload with BIP340 Schnorr for the X-TrueMatch-Sig header.
|
|
48
|
+
// The registry verifies: schnorr.verify(sig, sha256(rawBody), pubkey)
|
|
49
|
+
export function signPayload(nsecHex, payload) {
|
|
50
|
+
const secretKey = hexToBytes(nsecHex);
|
|
51
|
+
const msgHash = createHash("sha256").update(payload).digest();
|
|
52
|
+
const sig = schnorr.sign(msgHash, secretKey);
|
|
53
|
+
return bytesToHex(sig);
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TrueMatch sidecar CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* truematch setup [--contact-type email|discord|telegram|whatsapp|imessage] [--contact-value <val>]
|
|
7
|
+
* truematch status [--relays]
|
|
8
|
+
* truematch observe --show | --update | --write '<json>'
|
|
9
|
+
* truematch preferences --show | --set '<json>'
|
|
10
|
+
* truematch match --start | --status [--thread <id>] | --messages --thread <id>
|
|
11
|
+
* | --send '<msg>' --thread <id>
|
|
12
|
+
* | --propose --thread <id> --write '<narrative-json>'
|
|
13
|
+
* | --decline --thread <id>
|
|
14
|
+
* | --reset --thread <id>
|
|
15
|
+
* truematch deregister
|
|
16
|
+
*/
|
|
17
|
+
export {};
|