@runwingman/flightdeck-cli 0.2.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/Dockerfile +21 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/docs/to_fdcli.md +78 -0
- package/install.sh +86 -0
- package/package.json +53 -0
- package/src/bot-helpers.js +774 -0
- package/src/chat-runtime.js +782 -0
- package/src/cli.js +2767 -0
- package/src/client.js +285 -0
- package/src/config.js +117 -0
- package/src/db.js +653 -0
- package/src/flow-steps.js +215 -0
- package/src/nostr.js +160 -0
- package/src/render.js +34 -0
- package/src/sqlite-runtime.js +17 -0
- package/src/storage.js +191 -0
- package/src/sync.js +900 -0
- package/src/token.js +72 -0
- package/src/translators.js +1652 -0
- package/src/workspace-keys.js +214 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
2
|
+
import { decryptFromNpub, encryptForNpub, encodeNsec, decodeNsec, createNip98AuthHeader } from './nostr.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a fresh workspace session keypair and produce the encrypted blob envelope.
|
|
6
|
+
*
|
|
7
|
+
* @param {Uint8Array} userSecret - The user's real Nostr private key
|
|
8
|
+
* @param {string} userNpub - The user's real npub
|
|
9
|
+
* @param {string} workspaceOwnerNpub - The workspace identity
|
|
10
|
+
* @returns {{ blob: object, wsKeySecret: Uint8Array, wsKeyNpub: string }}
|
|
11
|
+
*/
|
|
12
|
+
export function generateWorkspaceKey(userSecret, userNpub, workspaceOwnerNpub) {
|
|
13
|
+
const wsKeySecret = generateSecretKey();
|
|
14
|
+
const wsKeyPubkey = getPublicKey(wsKeySecret);
|
|
15
|
+
const wsKeyNpub = nip19.npubEncode(wsKeyPubkey);
|
|
16
|
+
const wsKeyNsec = encodeNsec(wsKeySecret);
|
|
17
|
+
|
|
18
|
+
const encryptedNsec = encryptForNpub(userSecret, userNpub, wsKeyNsec);
|
|
19
|
+
|
|
20
|
+
const blob = {
|
|
21
|
+
version: 1,
|
|
22
|
+
workspace_owner_npub: workspaceOwnerNpub,
|
|
23
|
+
ws_key_npub: wsKeyNpub,
|
|
24
|
+
ws_key_epoch: 1,
|
|
25
|
+
encrypted_nsec: encryptedNsec,
|
|
26
|
+
encrypted_by_npub: userNpub,
|
|
27
|
+
created_at: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return { blob, wsKeySecret, wsKeyNpub };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decrypt a workspace key blob using the user's real key.
|
|
35
|
+
* Validates that the blob was encrypted by the expected npub.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} blob - The encrypted workspace key blob envelope
|
|
38
|
+
* @param {Uint8Array} userSecret - The user's real Nostr private key
|
|
39
|
+
* @param {string} userNpub - The user's real npub (for validation)
|
|
40
|
+
* @returns {{ wsKeySecret: Uint8Array, wsKeyNpub: string, wsKeyEpoch: number }}
|
|
41
|
+
*/
|
|
42
|
+
export function decryptWorkspaceKey(blob, userSecret, userNpub) {
|
|
43
|
+
if (blob.encrypted_by_npub !== userNpub) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Workspace key blob was encrypted by ${blob.encrypted_by_npub}, not ${userNpub}. Possible key substitution.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const wsKeyNsec = decryptFromNpub(userSecret, blob.encrypted_by_npub, blob.encrypted_nsec);
|
|
50
|
+
const wsKeySecret = decodeNsec(wsKeyNsec);
|
|
51
|
+
|
|
52
|
+
// Validate the decrypted key matches the blob's stated ws_key_npub
|
|
53
|
+
const derivedPubkey = getPublicKey(wsKeySecret);
|
|
54
|
+
const derivedNpub = nip19.npubEncode(derivedPubkey);
|
|
55
|
+
if (derivedNpub !== blob.ws_key_npub) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Decrypted workspace key npub ${derivedNpub} does not match blob ws_key_npub ${blob.ws_key_npub}. Blob may be corrupt.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
wsKeySecret,
|
|
63
|
+
wsKeyNpub: blob.ws_key_npub,
|
|
64
|
+
wsKeyEpoch: blob.ws_key_epoch ?? 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a workspace session object suitable for use in translators and client auth.
|
|
70
|
+
* This replaces the real session for runtime operations.
|
|
71
|
+
*
|
|
72
|
+
* @param {Uint8Array} wsKeySecret - The decrypted workspace session private key
|
|
73
|
+
* @param {string} wsKeyNpub - The workspace session public key (npub)
|
|
74
|
+
* @param {number} wsKeyEpoch - The key epoch
|
|
75
|
+
* @param {string} userNpub - The user's real npub (for identity resolution)
|
|
76
|
+
* @returns {object} A workspace session object
|
|
77
|
+
*/
|
|
78
|
+
export function buildWorkspaceSession(wsKeySecret, wsKeyNpub, wsKeyEpoch, userNpub) {
|
|
79
|
+
const pubkey = getPublicKey(wsKeySecret);
|
|
80
|
+
return {
|
|
81
|
+
secret: wsKeySecret,
|
|
82
|
+
pubkey,
|
|
83
|
+
npub: wsKeyNpub,
|
|
84
|
+
userNpub,
|
|
85
|
+
wsKeyEpoch,
|
|
86
|
+
isWorkspaceKey: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Store a workspace key blob in the local SQLite database.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} db - SQLite database handle
|
|
94
|
+
* @param {object} blob - The encrypted workspace key blob envelope
|
|
95
|
+
* @param {string} userNpub - The user's real npub
|
|
96
|
+
*/
|
|
97
|
+
export function cacheWorkspaceKeyBlob(db, blob, userNpub) {
|
|
98
|
+
db.prepare(`
|
|
99
|
+
INSERT INTO workspace_keys (workspace_owner_npub, user_npub, ws_key_npub, ws_key_epoch, encrypted_blob, cached_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT(workspace_owner_npub) DO UPDATE SET
|
|
102
|
+
user_npub = excluded.user_npub,
|
|
103
|
+
ws_key_npub = excluded.ws_key_npub,
|
|
104
|
+
ws_key_epoch = excluded.ws_key_epoch,
|
|
105
|
+
encrypted_blob = excluded.encrypted_blob,
|
|
106
|
+
cached_at = excluded.cached_at
|
|
107
|
+
`).run(
|
|
108
|
+
blob.workspace_owner_npub,
|
|
109
|
+
userNpub,
|
|
110
|
+
blob.ws_key_npub,
|
|
111
|
+
blob.ws_key_epoch ?? 1,
|
|
112
|
+
JSON.stringify(blob),
|
|
113
|
+
new Date().toISOString(),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Retrieve a cached workspace key blob from the local SQLite database.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} db - SQLite database handle
|
|
121
|
+
* @param {string} workspaceOwnerNpub - The workspace identity
|
|
122
|
+
* @returns {object|null} The parsed blob envelope, or null if not cached
|
|
123
|
+
*/
|
|
124
|
+
export function getCachedWorkspaceKeyBlob(db, workspaceOwnerNpub) {
|
|
125
|
+
const row = db.prepare(
|
|
126
|
+
`SELECT encrypted_blob FROM workspace_keys WHERE workspace_owner_npub = ?`
|
|
127
|
+
).get(workspaceOwnerNpub);
|
|
128
|
+
if (!row?.encrypted_blob) return null;
|
|
129
|
+
return JSON.parse(row.encrypted_blob);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove a cached workspace key blob, optionally only when it matches the
|
|
134
|
+
* expected workspace key npub.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} db - SQLite database handle
|
|
137
|
+
* @param {string} workspaceOwnerNpub - The workspace identity
|
|
138
|
+
* @param {string|null} wsKeyNpub - Optional workspace key npub guard
|
|
139
|
+
*/
|
|
140
|
+
export function deleteCachedWorkspaceKeyBlob(db, workspaceOwnerNpub, wsKeyNpub = null) {
|
|
141
|
+
if (wsKeyNpub) {
|
|
142
|
+
db.prepare(
|
|
143
|
+
`DELETE FROM workspace_keys WHERE workspace_owner_npub = ? AND ws_key_npub = ?`
|
|
144
|
+
).run(workspaceOwnerNpub, wsKeyNpub);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
db.prepare(
|
|
148
|
+
`DELETE FROM workspace_keys WHERE workspace_owner_npub = ?`
|
|
149
|
+
).run(workspaceOwnerNpub);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Confirm Tower can resolve a workspace key back to the expected real user npub.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} options
|
|
156
|
+
* @param {object} options.client - SuperbasedClient instance using real-user auth
|
|
157
|
+
* @param {object} options.config - Workspace config
|
|
158
|
+
* @param {string} options.wsKeyNpub - Workspace key npub
|
|
159
|
+
* @param {string} options.userNpub - Real user npub
|
|
160
|
+
* @returns {Promise<boolean>}
|
|
161
|
+
*/
|
|
162
|
+
export async function isWorkspaceKeyRegistered({ client, config, wsKeyNpub, userNpub }) {
|
|
163
|
+
const result = await client.fetchWorkspaceKeyMappings(config.workspaceOwnerNpub);
|
|
164
|
+
return (result.mappings ?? []).some((entry) => (
|
|
165
|
+
entry?.ws_key_npub === wsKeyNpub && entry?.user_npub === userNpub
|
|
166
|
+
));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Bootstrap the workspace session key for a given workspace.
|
|
171
|
+
* - If a cached blob exists locally, decrypt it.
|
|
172
|
+
* - Otherwise, generate a new keypair, register with Tower, then cache it.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @param {object} options.db - SQLite database handle
|
|
176
|
+
* @param {object} options.realSession - The user's real Nostr session { secret, npub }
|
|
177
|
+
* @param {object} options.config - Workspace config { workspaceOwnerNpub, directHttpsUrl }
|
|
178
|
+
* @param {object} options.client - SuperbasedClient instance (uses real session for registration)
|
|
179
|
+
* @returns {Promise<{ wsSession: object, blob: object }>}
|
|
180
|
+
*/
|
|
181
|
+
export async function bootstrapWorkspaceKey({ db, realSession, config, client }) {
|
|
182
|
+
const cached = getCachedWorkspaceKeyBlob(db, config.workspaceOwnerNpub);
|
|
183
|
+
|
|
184
|
+
if (cached) {
|
|
185
|
+
const { wsKeySecret, wsKeyNpub, wsKeyEpoch } = decryptWorkspaceKey(
|
|
186
|
+
cached, realSession.secret, realSession.npub
|
|
187
|
+
);
|
|
188
|
+
const wsSession = buildWorkspaceSession(wsKeySecret, wsKeyNpub, wsKeyEpoch, realSession.npub);
|
|
189
|
+
return { wsSession, blob: cached };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// First time: generate and register before caching locally.
|
|
193
|
+
const { blob, wsKeySecret, wsKeyNpub } = generateWorkspaceKey(
|
|
194
|
+
realSession.secret, realSession.npub, config.workspaceOwnerNpub
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Register with Tower (uses real session NIP-98 auth)
|
|
198
|
+
await client.registerWorkspaceKey({
|
|
199
|
+
workspace_owner_npub: config.workspaceOwnerNpub,
|
|
200
|
+
ws_key_npub: wsKeyNpub,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
cacheWorkspaceKeyBlob(db, blob, realSession.npub);
|
|
204
|
+
|
|
205
|
+
const wsSession = buildWorkspaceSession(wsKeySecret, wsKeyNpub, 1, realSession.npub);
|
|
206
|
+
return { wsSession, blob };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create a NIP-98 auth header using the workspace session key.
|
|
211
|
+
*/
|
|
212
|
+
export function createWorkspaceNip98Auth(wsSession, url, method, body) {
|
|
213
|
+
return createNip98AuthHeader(url, method, body, wsSession.secret);
|
|
214
|
+
}
|