@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,1652 @@
|
|
|
1
|
+
import { decryptFromNpub, encryptForNpub } from './nostr.js';
|
|
2
|
+
import { recordFamilyHash as buildRecordFamilyHash } from '@nostr-superbased/core/records';
|
|
3
|
+
|
|
4
|
+
const BLOCK_DOCUMENT_FORMAT = 'block_document_v1';
|
|
5
|
+
const REACTION_COLLECTION_SPACE = 'reaction';
|
|
6
|
+
const REACTION_OPTIONS = Object.freeze({
|
|
7
|
+
thumbs_up: ':thumbs_up:',
|
|
8
|
+
smile: ':smile:',
|
|
9
|
+
heart: ':heart:',
|
|
10
|
+
eyes: ':eyes:',
|
|
11
|
+
party: ':party:',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function makeDocumentBlockId(index, startLine) {
|
|
15
|
+
return `block-${index}-${startLine}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function blockText(block) {
|
|
19
|
+
return String(block?.raw ?? block?.text ?? '').trimEnd();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseMarkdownBlocks(content = '') {
|
|
23
|
+
const source = String(content || '').replace(/\r\n?/g, '\n');
|
|
24
|
+
if (!source.trim()) return [];
|
|
25
|
+
const lines = source.split('\n');
|
|
26
|
+
const blocks = [];
|
|
27
|
+
let currentLines = [];
|
|
28
|
+
let startLine = 1;
|
|
29
|
+
|
|
30
|
+
const flush = () => {
|
|
31
|
+
if (currentLines.length === 0) return;
|
|
32
|
+
const raw = currentLines.join('\n').trimEnd();
|
|
33
|
+
if (raw) {
|
|
34
|
+
blocks.push({
|
|
35
|
+
id: makeDocumentBlockId(blocks.length, startLine),
|
|
36
|
+
type: 'markdown',
|
|
37
|
+
text: raw,
|
|
38
|
+
attrs: {},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
currentLines = [];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
45
|
+
const line = lines[index] ?? '';
|
|
46
|
+
if (!line.trim()) {
|
|
47
|
+
flush();
|
|
48
|
+
startLine = index + 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (currentLines.length === 0) startLine = index + 1;
|
|
52
|
+
currentLines.push(line);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
flush();
|
|
56
|
+
return blocks;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeDocumentBlocks(blocks = [], fallbackContent = '') {
|
|
60
|
+
const sourceBlocks = Array.isArray(blocks) ? blocks : [];
|
|
61
|
+
if (sourceBlocks.length === 0) return parseMarkdownBlocks(fallbackContent);
|
|
62
|
+
return sourceBlocks
|
|
63
|
+
.map((block, index) => {
|
|
64
|
+
const text = blockText(block);
|
|
65
|
+
if (!text) return null;
|
|
66
|
+
return {
|
|
67
|
+
id: String(block?.id || '').trim() || makeDocumentBlockId(index, index + 1),
|
|
68
|
+
type: String(block?.type || '').trim() || 'markdown',
|
|
69
|
+
text,
|
|
70
|
+
attrs: block?.attrs && typeof block.attrs === 'object' && !Array.isArray(block.attrs)
|
|
71
|
+
? { ...block.attrs }
|
|
72
|
+
: {},
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseCiphertextEnvelope(value) {
|
|
79
|
+
if (typeof value !== 'string') return null;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(value);
|
|
82
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.ciphertext === 'string') {
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parsePayloadJson(value) {
|
|
92
|
+
if (typeof value !== 'string') return value;
|
|
93
|
+
return JSON.parse(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeGroupRef(value) {
|
|
97
|
+
const ref = String(value || '').trim();
|
|
98
|
+
return ref || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeKeyVersion(value) {
|
|
102
|
+
return Number.isInteger(value) ? value : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveAccessibleGroupEntry(groupKeys, groupRef, options = {}) {
|
|
106
|
+
const ref = normalizeGroupRef(groupRef);
|
|
107
|
+
if (!ref) return null;
|
|
108
|
+
const keyVersion = normalizeKeyVersion(options.keyVersion);
|
|
109
|
+
const entry = groupKeys.get(ref, keyVersion != null ? { keyVersion } : {});
|
|
110
|
+
if (!entry?.groupNpub) return null;
|
|
111
|
+
return entry;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function latestKeyEntry(keyring) {
|
|
115
|
+
let latest = null;
|
|
116
|
+
for (const entry of keyring.values()) {
|
|
117
|
+
if (!latest || (entry.keyVersion ?? 0) > (latest.keyVersion ?? 0)) latest = entry;
|
|
118
|
+
}
|
|
119
|
+
return latest;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveGroupPayloadId(payload) {
|
|
123
|
+
return normalizeGroupRef(payload?.group_id) || normalizeGroupRef(payload?.group_npub);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectGroupIds(groupPayloads = []) {
|
|
127
|
+
return [...new Set(groupPayloads.map((payload) => resolveGroupPayloadId(payload)).filter(Boolean))];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectRecordDeliveryGroupRefs(record = {}) {
|
|
131
|
+
const refs = [];
|
|
132
|
+
for (const ref of (record.group_ids || [])) {
|
|
133
|
+
const normalized = normalizeGroupRef(ref);
|
|
134
|
+
if (normalized) refs.push(normalized);
|
|
135
|
+
}
|
|
136
|
+
for (const payload of (record.group_payloads || [])) {
|
|
137
|
+
const normalized = resolveGroupPayloadId(payload);
|
|
138
|
+
if (normalized) refs.push(normalized);
|
|
139
|
+
}
|
|
140
|
+
return [...new Set(refs)];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildGroupPayloadRefMap(groupPayloads = []) {
|
|
144
|
+
const map = new Map();
|
|
145
|
+
for (const payload of groupPayloads) {
|
|
146
|
+
const stableId = payload?.group_id || payload?.group_npub || null;
|
|
147
|
+
if (!stableId) continue;
|
|
148
|
+
if (payload?.group_npub) map.set(payload.group_npub, stableId);
|
|
149
|
+
if (payload?.group_id) map.set(payload.group_id, payload.group_id);
|
|
150
|
+
}
|
|
151
|
+
return map;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveGroupRefViaPayloads(ref, refMap) {
|
|
155
|
+
const value = normalizeGroupRef(ref);
|
|
156
|
+
if (!value) return null;
|
|
157
|
+
return refMap.get(value) || value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeShares(dataShares = [], groupPayloads = []) {
|
|
161
|
+
const refMap = buildGroupPayloadRefMap(groupPayloads);
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(dataShares) && dataShares.length > 0) {
|
|
164
|
+
return dataShares.map((share) => {
|
|
165
|
+
const type = share.type === 'person' ? 'person' : 'group';
|
|
166
|
+
const groupRef = resolveGroupRefViaPayloads(share.group_id || share.group_npub, refMap);
|
|
167
|
+
const viaGroupRef = resolveGroupRefViaPayloads(share.via_group_id || share.via_group_npub, refMap);
|
|
168
|
+
const key = type === 'person'
|
|
169
|
+
? (share.key ?? share.person_npub)
|
|
170
|
+
: groupRef;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type,
|
|
174
|
+
key,
|
|
175
|
+
access: share.access === 'write' ? 'write' : 'read',
|
|
176
|
+
label: share.label ?? '',
|
|
177
|
+
person_npub: share.person_npub ?? null,
|
|
178
|
+
group_id: groupRef,
|
|
179
|
+
group_npub: share.group_npub ?? null,
|
|
180
|
+
via_group_id: viaGroupRef,
|
|
181
|
+
via_group_npub: share.via_group_npub ?? null,
|
|
182
|
+
inherited: share.inherited === true,
|
|
183
|
+
inherited_from_directory_id: share.inherited_from_directory_id ?? null,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return groupPayloads.map((payload) => {
|
|
189
|
+
const groupRef = payload.group_id ?? payload.group_npub;
|
|
190
|
+
return {
|
|
191
|
+
type: 'group',
|
|
192
|
+
key: groupRef,
|
|
193
|
+
access: payload.write ? 'write' : 'read',
|
|
194
|
+
label: '',
|
|
195
|
+
person_npub: null,
|
|
196
|
+
group_id: groupRef,
|
|
197
|
+
group_epoch: payload.group_epoch ?? null,
|
|
198
|
+
group_npub: payload.group_npub,
|
|
199
|
+
via_group_id: null,
|
|
200
|
+
via_group_npub: null,
|
|
201
|
+
inherited: false,
|
|
202
|
+
inherited_from_directory_id: null,
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function recordFamilyHash(appNpub, collectionSpace) {
|
|
208
|
+
return buildRecordFamilyHash(appNpub, collectionSpace);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function unwrapGroupKey(session, keyRow) {
|
|
212
|
+
const nsec = decryptFromNpub(session.secret, keyRow.wrapped_by_npub, keyRow.wrapped_group_nsec);
|
|
213
|
+
return {
|
|
214
|
+
groupId: keyRow.group_id ?? keyRow.group_npub,
|
|
215
|
+
groupNpub: keyRow.group_npub,
|
|
216
|
+
keyVersion: keyRow.key_version ?? keyRow.epoch ?? 1,
|
|
217
|
+
nsec,
|
|
218
|
+
secret: session.constructor?.decodeNsec ? session.constructor.decodeNsec(nsec) : null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function loadGroupKeyMap(session, keyRows, decodeNsec) {
|
|
223
|
+
const byId = new Map();
|
|
224
|
+
const byNpub = new Map();
|
|
225
|
+
for (const row of keyRows) {
|
|
226
|
+
const nsec = decryptFromNpub(session.secret, row.wrapped_by_npub, row.wrapped_group_nsec);
|
|
227
|
+
const entry = {
|
|
228
|
+
groupNpub: row.group_npub,
|
|
229
|
+
groupId: row.group_id ?? row.group_npub,
|
|
230
|
+
keyVersion: row.key_version ?? row.epoch ?? 1,
|
|
231
|
+
nsec,
|
|
232
|
+
secret: decodeNsec(nsec),
|
|
233
|
+
};
|
|
234
|
+
byNpub.set(entry.groupNpub, entry);
|
|
235
|
+
const keyring = byId.get(entry.groupId) ?? new Map();
|
|
236
|
+
keyring.set(entry.keyVersion, entry);
|
|
237
|
+
byId.set(entry.groupId, keyring);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const get = (groupRef, options = {}) => {
|
|
241
|
+
const ref = normalizeGroupRef(groupRef);
|
|
242
|
+
if (!ref) return null;
|
|
243
|
+
const keyVersion = normalizeKeyVersion(options.keyVersion);
|
|
244
|
+
const keyring = byId.get(ref);
|
|
245
|
+
if (keyring?.size) {
|
|
246
|
+
if (keyVersion != null && keyring.has(keyVersion)) return keyring.get(keyVersion) ?? null;
|
|
247
|
+
return latestKeyEntry(keyring);
|
|
248
|
+
}
|
|
249
|
+
return byNpub.get(ref) ?? null;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
byId,
|
|
254
|
+
byNpub,
|
|
255
|
+
get,
|
|
256
|
+
getCurrent(groupRef) {
|
|
257
|
+
return get(groupRef);
|
|
258
|
+
},
|
|
259
|
+
has(groupRef, options = {}) {
|
|
260
|
+
return Boolean(get(groupRef, options));
|
|
261
|
+
},
|
|
262
|
+
resolveGroupId(groupRef) {
|
|
263
|
+
return get(groupRef)?.groupId ?? normalizeGroupRef(groupRef);
|
|
264
|
+
},
|
|
265
|
+
resolveGroupNpub(groupRef) {
|
|
266
|
+
const ref = normalizeGroupRef(groupRef);
|
|
267
|
+
if (!ref) return null;
|
|
268
|
+
return get(groupRef)?.groupNpub ?? (byNpub.has(ref) ? ref : null);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function decryptRecordPayload(record, session, groupKeys, wsSession = null) {
|
|
274
|
+
const ownerCiphertext = record.owner_payload?.ciphertext ?? record.owner_payload;
|
|
275
|
+
|
|
276
|
+
// Workspace session key path: check if owner_payload is wrapped in an envelope from our ws_key
|
|
277
|
+
if (ownerCiphertext && wsSession) {
|
|
278
|
+
const ownerEnvelope = parseCiphertextEnvelope(ownerCiphertext);
|
|
279
|
+
if (ownerEnvelope?.encrypted_by_npub === wsSession.npub) {
|
|
280
|
+
return parsePayloadJson(decryptFromNpub(wsSession.secret, ownerEnvelope.encrypted_by_npub, ownerEnvelope.ciphertext));
|
|
281
|
+
}
|
|
282
|
+
// Also handle bare ciphertext from our ws_key (signature_npub match without envelope)
|
|
283
|
+
if (!ownerEnvelope && record.signature_npub === wsSession.npub) {
|
|
284
|
+
return parsePayloadJson(decryptFromNpub(wsSession.secret, wsSession.npub, ownerCiphertext));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Real identity path (pre-migration records)
|
|
289
|
+
if (ownerCiphertext && record.owner_npub === session.npub) {
|
|
290
|
+
return parsePayloadJson(decryptFromNpub(session.secret, record.signature_npub || record.owner_npub, ownerCiphertext));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const payload of (record.group_payloads || [])) {
|
|
294
|
+
const keyVersion = normalizeKeyVersion(payload.group_epoch);
|
|
295
|
+
const keyEntry = groupKeys.get(payload.group_id, { keyVersion })
|
|
296
|
+
|| groupKeys.get(payload.group_npub);
|
|
297
|
+
if (!keyEntry?.secret) continue;
|
|
298
|
+
const envelope = parseCiphertextEnvelope(payload.ciphertext);
|
|
299
|
+
const senderNpub = envelope?.encrypted_by_npub || record.signature_npub || payload.group_npub;
|
|
300
|
+
const ciphertext = envelope?.ciphertext || payload.ciphertext;
|
|
301
|
+
return parsePayloadJson(decryptFromNpub(keyEntry.secret, senderNpub, ciphertext));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
throw new Error(`Unable to decrypt record ${record.record_id}: no matching group key`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function decryptAppSchemaManifest(manifest, session, groupKeys, wsSession = null) {
|
|
308
|
+
return decryptRecordPayload({
|
|
309
|
+
record_id: manifest.id || `${manifest.app_npub || 'app'}:schema:${manifest.schema_hash || 'unknown'}`,
|
|
310
|
+
owner_npub: manifest.workspace_owner_npub,
|
|
311
|
+
signature_npub: manifest.created_by_npub || manifest.workspace_owner_npub,
|
|
312
|
+
owner_payload: manifest.owner_payload,
|
|
313
|
+
group_payloads: manifest.group_payloads || [],
|
|
314
|
+
}, session, groupKeys, wsSession);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeRecordState(data) {
|
|
318
|
+
return data.record_state ?? 'active';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function assertReactionEmoji(emoji) {
|
|
322
|
+
const token = String(emoji || '').trim();
|
|
323
|
+
if (!Object.prototype.hasOwnProperty.call(REACTION_OPTIONS, token)) {
|
|
324
|
+
throw new Error(`Unsupported reaction emoji: ${token || '(missing)'}`);
|
|
325
|
+
}
|
|
326
|
+
return token;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function assertReactionTargetFamily(targetFamilyHash) {
|
|
330
|
+
const family = String(targetFamilyHash || '').trim();
|
|
331
|
+
if (!family.endsWith(':chat_message') && !family.endsWith(':comment')) {
|
|
332
|
+
throw new Error(`Unsupported reaction target family: ${family || '(missing)'}`);
|
|
333
|
+
}
|
|
334
|
+
return family;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function reactionShortcode(emoji) {
|
|
338
|
+
return REACTION_OPTIONS[assertReactionEmoji(emoji)];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeReportDeclarationType(value) {
|
|
342
|
+
const type = String(value || '').trim().toLowerCase();
|
|
343
|
+
if (['metric', 'timeseries', 'table', 'text'].includes(type)) return type;
|
|
344
|
+
return 'text';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function normalizeReportPayloadObject(payload) {
|
|
348
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return {};
|
|
349
|
+
return payload;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const LEGACY_LEVEL_MAP = { product: 'l1', project: 'l2', deliverable: 'l3' };
|
|
353
|
+
|
|
354
|
+
export function normalizeScopeLevel(level) {
|
|
355
|
+
const value = String(level || '').trim().toLowerCase();
|
|
356
|
+
if (!value) return null;
|
|
357
|
+
if (LEGACY_LEVEL_MAP[value]) return LEGACY_LEVEL_MAP[value];
|
|
358
|
+
if (/^l[1-5]$/.test(value)) return value;
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function scopeDepth(level) {
|
|
363
|
+
const normalized = normalizeScopeLevel(level);
|
|
364
|
+
if (!normalized) return null;
|
|
365
|
+
return Number(normalized.charAt(1));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function computeScopeLineage(selfId, level, parent) {
|
|
369
|
+
const result = {
|
|
370
|
+
level,
|
|
371
|
+
parent_id: parent?.record_id ?? null,
|
|
372
|
+
l1_id: null,
|
|
373
|
+
l2_id: null,
|
|
374
|
+
l3_id: null,
|
|
375
|
+
l4_id: null,
|
|
376
|
+
l5_id: null,
|
|
377
|
+
};
|
|
378
|
+
if (parent) {
|
|
379
|
+
for (let i = 1; i <= 5; i++) result[`l${i}_id`] = parent[`l${i}_id`] ?? null;
|
|
380
|
+
}
|
|
381
|
+
const depth = scopeDepth(level);
|
|
382
|
+
if (depth) result[`l${depth}_id`] = selfId;
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function buildScopeTags(scope) {
|
|
387
|
+
if (!scope) {
|
|
388
|
+
return { scope_id: null, scope_l1_id: null, scope_l2_id: null, scope_l3_id: null, scope_l4_id: null, scope_l5_id: null };
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
scope_id: scope.record_id ?? null,
|
|
392
|
+
scope_l1_id: scope.l1_id ?? null,
|
|
393
|
+
scope_l2_id: scope.l2_id ?? null,
|
|
394
|
+
scope_l3_id: scope.l3_id ?? null,
|
|
395
|
+
scope_l4_id: scope.l4_id ?? null,
|
|
396
|
+
scope_l5_id: scope.l5_id ?? null,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function normalizeScopedRecordLineage(data) {
|
|
401
|
+
if (data.scope_l1_id !== undefined || data.scope_l2_id !== undefined) {
|
|
402
|
+
return {
|
|
403
|
+
scope_l1_id: data.scope_l1_id ?? null,
|
|
404
|
+
scope_l2_id: data.scope_l2_id ?? null,
|
|
405
|
+
scope_l3_id: data.scope_l3_id ?? null,
|
|
406
|
+
scope_l4_id: data.scope_l4_id ?? null,
|
|
407
|
+
scope_l5_id: data.scope_l5_id ?? null,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
scope_l1_id: data.scope_product_id ?? null,
|
|
412
|
+
scope_l2_id: data.scope_project_id ?? null,
|
|
413
|
+
scope_l3_id: data.scope_deliverable_id ?? null,
|
|
414
|
+
scope_l4_id: null,
|
|
415
|
+
scope_l5_id: null,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeScopeRecordLineage(data, selfId) {
|
|
420
|
+
if (data.l1_id !== undefined || data.l2_id !== undefined) {
|
|
421
|
+
return {
|
|
422
|
+
l1_id: data.l1_id ?? null,
|
|
423
|
+
l2_id: data.l2_id ?? null,
|
|
424
|
+
l3_id: data.l3_id ?? null,
|
|
425
|
+
l4_id: data.l4_id ?? null,
|
|
426
|
+
l5_id: data.l5_id ?? null,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const normalized = normalizeScopeLevel(data.level);
|
|
430
|
+
if (normalized === 'l1') {
|
|
431
|
+
return { l1_id: selfId, l2_id: null, l3_id: null, l4_id: null, l5_id: null };
|
|
432
|
+
}
|
|
433
|
+
if (normalized === 'l2') {
|
|
434
|
+
return { l1_id: data.product_id ?? null, l2_id: selfId, l3_id: null, l4_id: null, l5_id: null };
|
|
435
|
+
}
|
|
436
|
+
if (normalized === 'l3') {
|
|
437
|
+
return { l1_id: data.product_id ?? null, l2_id: data.project_id ?? null, l3_id: selfId, l4_id: null, l5_id: null };
|
|
438
|
+
}
|
|
439
|
+
return { l1_id: null, l2_id: null, l3_id: null, l4_id: null, l5_id: null };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeReportScope(scope = {}) {
|
|
443
|
+
const nextScope = scope && typeof scope === 'object' && !Array.isArray(scope) ? scope : {};
|
|
444
|
+
const level = normalizeScopeLevel(nextScope.level ?? nextScope.scope_level) ?? null;
|
|
445
|
+
if (nextScope.l1_id !== undefined || nextScope.l2_id !== undefined) {
|
|
446
|
+
return {
|
|
447
|
+
id: nextScope.id ?? nextScope.scope_id ?? null,
|
|
448
|
+
level,
|
|
449
|
+
l1_id: nextScope.l1_id ?? null,
|
|
450
|
+
l2_id: nextScope.l2_id ?? null,
|
|
451
|
+
l3_id: nextScope.l3_id ?? null,
|
|
452
|
+
l4_id: nextScope.l4_id ?? null,
|
|
453
|
+
l5_id: nextScope.l5_id ?? null,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
id: nextScope.id ?? nextScope.scope_id ?? null,
|
|
458
|
+
level,
|
|
459
|
+
l1_id: nextScope.product_id ?? nextScope.scope_product_id ?? null,
|
|
460
|
+
l2_id: nextScope.project_id ?? nextScope.scope_project_id ?? null,
|
|
461
|
+
l3_id: nextScope.deliverable_id ?? nextScope.scope_deliverable_id ?? null,
|
|
462
|
+
l4_id: null,
|
|
463
|
+
l5_id: null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function normalizeReportMetadata(metadata = {}, recordState = 'active') {
|
|
468
|
+
const nextMetadata = metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? metadata : {};
|
|
469
|
+
return {
|
|
470
|
+
title: String(nextMetadata.title || '').trim(),
|
|
471
|
+
generated_at: nextMetadata.generated_at ?? null,
|
|
472
|
+
record_state: nextMetadata.record_state ?? recordState,
|
|
473
|
+
surface: nextMetadata.surface ?? null,
|
|
474
|
+
scope: normalizeReportScope(nextMetadata.scope),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function reportScopeFromRecord(record = {}) {
|
|
479
|
+
return normalizeReportScope({
|
|
480
|
+
id: record.scope_id ?? null,
|
|
481
|
+
level: record.scope_level ?? null,
|
|
482
|
+
l1_id: record.scope_l1_id ?? null,
|
|
483
|
+
l2_id: record.scope_l2_id ?? null,
|
|
484
|
+
l3_id: record.scope_l3_id ?? null,
|
|
485
|
+
l4_id: record.scope_l4_id ?? null,
|
|
486
|
+
l5_id: record.scope_l5_id ?? null,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function inboundGroup(group) {
|
|
491
|
+
const currentGroupNpub = group.current_group_npub ?? group.group_npub ?? group.id;
|
|
492
|
+
return {
|
|
493
|
+
group_npub: currentGroupNpub,
|
|
494
|
+
current_group_npub: currentGroupNpub,
|
|
495
|
+
group_id: group.id ?? group.group_id ?? group.group_npub,
|
|
496
|
+
current_epoch: Number(group.current_epoch || 1),
|
|
497
|
+
owner_npub: group.owner_npub,
|
|
498
|
+
name: group.name ?? '',
|
|
499
|
+
group_kind: group.group_kind || 'shared',
|
|
500
|
+
private_member_npub: group.private_member_npub ?? null,
|
|
501
|
+
member_npubs: [...(group.members ?? group.member_npubs ?? [])].map((member) => typeof member === 'string' ? member : member.member_npub).filter(Boolean),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function inboundChannel(record, payload) {
|
|
506
|
+
const data = payload.data ?? payload;
|
|
507
|
+
return {
|
|
508
|
+
record_id: record.record_id,
|
|
509
|
+
owner_npub: record.owner_npub,
|
|
510
|
+
title: data.title ?? '',
|
|
511
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
512
|
+
participant_npubs: Array.isArray(data.participant_npubs) ? data.participant_npubs : [record.owner_npub],
|
|
513
|
+
scope_id: data.scope_id ?? null,
|
|
514
|
+
...normalizeScopedRecordLineage(data),
|
|
515
|
+
record_state: normalizeRecordState(data),
|
|
516
|
+
version: record.version ?? 1,
|
|
517
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function inboundChatMessage(record, payload) {
|
|
522
|
+
const data = payload.data ?? payload;
|
|
523
|
+
return {
|
|
524
|
+
record_id: record.record_id,
|
|
525
|
+
owner_npub: record.owner_npub,
|
|
526
|
+
channel_id: data.channel_id,
|
|
527
|
+
parent_message_id: data.parent_message_id ?? null,
|
|
528
|
+
body: data.body ?? '',
|
|
529
|
+
attachments: Array.isArray(data.attachments) ? data.attachments : [],
|
|
530
|
+
sender_npub: data.sender_npub ?? record.signature_npub ?? record.owner_npub,
|
|
531
|
+
record_state: normalizeRecordState(data),
|
|
532
|
+
version: record.version ?? 1,
|
|
533
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function inboundTask(record, payload) {
|
|
538
|
+
const data = payload.data ?? payload;
|
|
539
|
+
const gp = record.group_payloads || [];
|
|
540
|
+
const refMap = buildGroupPayloadRefMap(gp);
|
|
541
|
+
return {
|
|
542
|
+
record_id: record.record_id,
|
|
543
|
+
owner_npub: record.owner_npub,
|
|
544
|
+
title: data.title ?? '',
|
|
545
|
+
description: data.description ?? '',
|
|
546
|
+
state: data.state ?? 'new',
|
|
547
|
+
priority: data.priority ?? 'sand',
|
|
548
|
+
assigned_to_npub: data.assigned_to_npub ?? null,
|
|
549
|
+
parent_task_id: data.parent_task_id ?? null,
|
|
550
|
+
board_group_id: resolveGroupRefViaPayloads(data.board_group_id, refMap),
|
|
551
|
+
scheduled_for: data.scheduled_for ?? null,
|
|
552
|
+
tags: data.tags ?? '',
|
|
553
|
+
scope_id: data.scope_id ?? null,
|
|
554
|
+
...normalizeScopedRecordLineage(data),
|
|
555
|
+
references: Array.isArray(data.references) ? data.references : [],
|
|
556
|
+
predecessor_task_ids: Array.isArray(data.predecessor_task_ids) ? data.predecessor_task_ids : [],
|
|
557
|
+
flow_id: data.flow_id ?? null,
|
|
558
|
+
flow_run_id: data.flow_run_id ?? null,
|
|
559
|
+
flow_step: Number.isFinite(Number(data.flow_step)) ? Number(data.flow_step) : null,
|
|
560
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
561
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
562
|
+
record_state: normalizeRecordState(data),
|
|
563
|
+
version: record.version ?? 1,
|
|
564
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function inboundComment(record, payload) {
|
|
569
|
+
const data = payload.data ?? payload;
|
|
570
|
+
return {
|
|
571
|
+
record_id: record.record_id,
|
|
572
|
+
owner_npub: record.owner_npub,
|
|
573
|
+
target_record_id: data.target_record_id ?? null,
|
|
574
|
+
target_record_family_hash: data.target_record_family_hash ?? null,
|
|
575
|
+
parent_comment_id: data.parent_comment_id ?? null,
|
|
576
|
+
anchor_line_number: Number.isFinite(Number(data.anchor_line_number)) ? Number(data.anchor_line_number) : null,
|
|
577
|
+
comment_status: data.comment_status === 'resolved' ? 'resolved' : 'open',
|
|
578
|
+
body: data.body ?? '',
|
|
579
|
+
attachments: Array.isArray(data.attachments) ? data.attachments : [],
|
|
580
|
+
sender_npub: record.signature_npub ?? record.owner_npub,
|
|
581
|
+
record_state: normalizeRecordState(data),
|
|
582
|
+
version: record.version ?? 1,
|
|
583
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function inboundReaction(record, payload) {
|
|
588
|
+
const data = payload.data ?? payload;
|
|
589
|
+
const targetRecordId = String(data.target_record_id || '').trim();
|
|
590
|
+
const emoji = assertReactionEmoji(data.emoji);
|
|
591
|
+
const reactorNpub = String(data.reactor_npub || record.signature_npub || record.owner_npub || '').trim();
|
|
592
|
+
if (!targetRecordId) throw new Error('reaction target_record_id is required');
|
|
593
|
+
if (!reactorNpub) throw new Error('reaction reactor_npub is required');
|
|
594
|
+
return {
|
|
595
|
+
record_id: record.record_id,
|
|
596
|
+
owner_npub: record.owner_npub,
|
|
597
|
+
target_record_id: targetRecordId,
|
|
598
|
+
target_record_family_hash: assertReactionTargetFamily(data.target_record_family_hash),
|
|
599
|
+
emoji,
|
|
600
|
+
emoji_shortcode: data.emoji_shortcode || reactionShortcode(emoji),
|
|
601
|
+
reactor_npub: reactorNpub,
|
|
602
|
+
sender_npub: record.signature_npub ?? record.owner_npub,
|
|
603
|
+
record_state: data.record_state === 'deleted' ? 'deleted' : 'active',
|
|
604
|
+
version: record.version ?? 1,
|
|
605
|
+
created_at: record.created_at ?? record.updated_at ?? new Date().toISOString(),
|
|
606
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function inboundDirectory(record, payload) {
|
|
611
|
+
const data = payload.data ?? payload;
|
|
612
|
+
return {
|
|
613
|
+
record_id: record.record_id,
|
|
614
|
+
owner_npub: record.owner_npub,
|
|
615
|
+
title: data.title ?? 'Untitled directory',
|
|
616
|
+
parent_directory_id: data.parent_directory_id ?? null,
|
|
617
|
+
scope_id: data.scope_id ?? null,
|
|
618
|
+
...normalizeScopedRecordLineage(data),
|
|
619
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
620
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
621
|
+
record_state: normalizeRecordState(data),
|
|
622
|
+
version: record.version ?? 1,
|
|
623
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function inboundDocument(record, payload) {
|
|
628
|
+
const data = payload.data ?? payload;
|
|
629
|
+
return {
|
|
630
|
+
record_id: record.record_id,
|
|
631
|
+
owner_npub: record.owner_npub,
|
|
632
|
+
title: data.title ?? 'Untitled document',
|
|
633
|
+
content: data.content ?? '',
|
|
634
|
+
parent_directory_id: data.parent_directory_id ?? null,
|
|
635
|
+
scope_id: data.scope_id ?? null,
|
|
636
|
+
...normalizeScopedRecordLineage(data),
|
|
637
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
638
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
639
|
+
record_state: normalizeRecordState(data),
|
|
640
|
+
version: record.version ?? 1,
|
|
641
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function inboundAudioNote(record, payload) {
|
|
646
|
+
const data = payload.data ?? payload;
|
|
647
|
+
return {
|
|
648
|
+
record_id: record.record_id,
|
|
649
|
+
owner_npub: record.owner_npub,
|
|
650
|
+
target_record_id: data.target_record_id ?? null,
|
|
651
|
+
target_record_family_hash: data.target_record_family_hash ?? null,
|
|
652
|
+
title: data.title ?? 'Voice note',
|
|
653
|
+
storage_object_id: data.storage_object_id ?? null,
|
|
654
|
+
mime_type: data.mime_type ?? 'audio/webm;codecs=opus',
|
|
655
|
+
duration_seconds: Number.isFinite(Number(data.duration_seconds)) ? Number(data.duration_seconds) : null,
|
|
656
|
+
size_bytes: Number.isFinite(Number(data.size_bytes)) ? Number(data.size_bytes) : 0,
|
|
657
|
+
media_encryption: data.media_encryption ?? null,
|
|
658
|
+
waveform_preview: Array.isArray(data.waveform_preview) ? data.waveform_preview : [],
|
|
659
|
+
transcript_status: data.transcript_status ?? 'pending',
|
|
660
|
+
transcript_preview: data.transcript_preview ?? null,
|
|
661
|
+
transcript: data.transcript ?? null,
|
|
662
|
+
summary: data.summary ?? null,
|
|
663
|
+
sender_npub: record.signature_npub ?? record.owner_npub,
|
|
664
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
665
|
+
record_state: normalizeRecordState(data),
|
|
666
|
+
version: record.version ?? 1,
|
|
667
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function inboundScope(record, payload) {
|
|
672
|
+
const data = payload.data ?? payload;
|
|
673
|
+
const level = normalizeScopeLevel(data.level) ?? data.level ?? null;
|
|
674
|
+
const lineage = normalizeScopeRecordLineage(data, record.record_id);
|
|
675
|
+
return {
|
|
676
|
+
record_id: record.record_id,
|
|
677
|
+
owner_npub: record.owner_npub,
|
|
678
|
+
level,
|
|
679
|
+
title: data.title ?? '',
|
|
680
|
+
description: data.description ?? '',
|
|
681
|
+
parent_id: data.parent_id ?? null,
|
|
682
|
+
...lineage,
|
|
683
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
684
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
685
|
+
record_state: normalizeRecordState(data),
|
|
686
|
+
version: record.version ?? 1,
|
|
687
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function inboundSchedule(record, payload) {
|
|
692
|
+
const data = payload.data ?? payload;
|
|
693
|
+
const gp = record.group_payloads || [];
|
|
694
|
+
const refMap = buildGroupPayloadRefMap(gp);
|
|
695
|
+
const rawDays = data.days ?? data.days_json ?? [];
|
|
696
|
+
const days = Array.isArray(rawDays) ? rawDays : (typeof rawDays === 'string' ? rawDays.split(',').map((d) => d.trim()).filter(Boolean) : []);
|
|
697
|
+
return {
|
|
698
|
+
record_id: record.record_id,
|
|
699
|
+
owner_npub: record.owner_npub,
|
|
700
|
+
title: data.title ?? '',
|
|
701
|
+
description: data.description ?? '',
|
|
702
|
+
time_start: data.time_start ?? null,
|
|
703
|
+
time_end: data.time_end ?? null,
|
|
704
|
+
days,
|
|
705
|
+
timezone: data.timezone ?? 'Australia/Perth',
|
|
706
|
+
assigned_group_id: resolveGroupRefViaPayloads(data.assigned_group_id ?? data.assigned_to_npub, refMap),
|
|
707
|
+
active: data.active === true || data.active === 1,
|
|
708
|
+
last_run: data.last_run ?? null,
|
|
709
|
+
repeat: data.repeat ?? 'daily',
|
|
710
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
711
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
712
|
+
record_state: normalizeRecordState(data),
|
|
713
|
+
version: record.version ?? 1,
|
|
714
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function inboundReport(record, payload) {
|
|
719
|
+
const metadata = normalizeReportMetadata(payload.metadata, 'active');
|
|
720
|
+
const declaration = payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
|
|
721
|
+
? payload.data
|
|
722
|
+
: {};
|
|
723
|
+
return {
|
|
724
|
+
record_id: record.record_id,
|
|
725
|
+
owner_npub: record.owner_npub,
|
|
726
|
+
title: metadata.title || '',
|
|
727
|
+
surface: metadata.surface ?? null,
|
|
728
|
+
generated_at: metadata.generated_at ?? record.updated_at ?? new Date().toISOString(),
|
|
729
|
+
metadata,
|
|
730
|
+
declaration_type: normalizeReportDeclarationType(declaration.declaration_type),
|
|
731
|
+
payload: normalizeReportPayloadObject(declaration.payload),
|
|
732
|
+
scope_id: metadata.scope.id ?? null,
|
|
733
|
+
scope_level: metadata.scope.level ?? null,
|
|
734
|
+
scope_l1_id: metadata.scope.l1_id ?? null,
|
|
735
|
+
scope_l2_id: metadata.scope.l2_id ?? null,
|
|
736
|
+
scope_l3_id: metadata.scope.l3_id ?? null,
|
|
737
|
+
scope_l4_id: metadata.scope.l4_id ?? null,
|
|
738
|
+
scope_l5_id: metadata.scope.l5_id ?? null,
|
|
739
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
740
|
+
record_state: metadata.record_state ?? 'active',
|
|
741
|
+
version: record.version ?? 1,
|
|
742
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function outboundSchedule(appNpub, session, groupKeys, schedule, patch = {}) {
|
|
747
|
+
const next = { ...schedule, ...patch };
|
|
748
|
+
const payload = {
|
|
749
|
+
app_namespace: appNpub,
|
|
750
|
+
collection_space: 'schedule',
|
|
751
|
+
schema_version: 1,
|
|
752
|
+
record_id: schedule.record_id,
|
|
753
|
+
data: {
|
|
754
|
+
title: next.title,
|
|
755
|
+
description: next.description ?? '',
|
|
756
|
+
time_start: next.time_start ?? null,
|
|
757
|
+
time_end: next.time_end ?? null,
|
|
758
|
+
days: Array.isArray(next.days) ? next.days : [],
|
|
759
|
+
timezone: next.timezone ?? 'Australia/Perth',
|
|
760
|
+
assigned_group_id: next.assigned_group_id ?? null,
|
|
761
|
+
active: next.active === true || next.active === 1,
|
|
762
|
+
last_run: next.last_run ?? null,
|
|
763
|
+
repeat: next.repeat ?? 'daily',
|
|
764
|
+
shares: next.shares ?? [],
|
|
765
|
+
record_state: next.record_state ?? 'active',
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
const plaintext = JSON.stringify(payload);
|
|
769
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
770
|
+
groupKeys,
|
|
771
|
+
resolveWriteGroup(session, groupKeys, next, next.assigned_group_id ?? next.board_group_id)
|
|
772
|
+
);
|
|
773
|
+
return {
|
|
774
|
+
record_id: schedule.record_id,
|
|
775
|
+
owner_npub: schedule.owner_npub,
|
|
776
|
+
record_family_hash: recordFamilyHash(appNpub, 'schedule'),
|
|
777
|
+
version: (schedule.version ?? 1) + 1,
|
|
778
|
+
previous_version: schedule.version ?? 1,
|
|
779
|
+
signature_npub: session.npub,
|
|
780
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
781
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
782
|
+
owner_payload: encryptOwnerPayload(schedule.owner_npub, plaintext, session),
|
|
783
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function outboundReport(appNpub, session, groupKeys, report, patch = {}) {
|
|
788
|
+
const next = { ...report, ...patch };
|
|
789
|
+
const metadata = normalizeReportMetadata({
|
|
790
|
+
title: next.title ?? report.title ?? '',
|
|
791
|
+
generated_at: next.generated_at ?? report.generated_at ?? null,
|
|
792
|
+
surface: next.surface ?? report.surface ?? null,
|
|
793
|
+
record_state: next.record_state ?? report.record_state ?? 'active',
|
|
794
|
+
scope: reportScopeFromRecord(next),
|
|
795
|
+
}, next.record_state ?? report.record_state ?? 'active');
|
|
796
|
+
const data = {
|
|
797
|
+
declaration_type: normalizeReportDeclarationType(next.declaration_type ?? report.declaration_type),
|
|
798
|
+
payload: normalizeReportPayloadObject(next.payload ?? report.payload),
|
|
799
|
+
};
|
|
800
|
+
const payload = {
|
|
801
|
+
app_namespace: appNpub,
|
|
802
|
+
collection_space: 'report',
|
|
803
|
+
schema_version: 1,
|
|
804
|
+
record_id: report.record_id,
|
|
805
|
+
metadata,
|
|
806
|
+
data,
|
|
807
|
+
};
|
|
808
|
+
const plaintext = JSON.stringify(payload);
|
|
809
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
810
|
+
groupKeys,
|
|
811
|
+
next.write_group_id
|
|
812
|
+
?? next.write_group_npub
|
|
813
|
+
?? resolveWriteGroup(session, groupKeys, { group_ids: next.group_ids || [] }, next.group_ids?.[0] ?? null)
|
|
814
|
+
);
|
|
815
|
+
return {
|
|
816
|
+
record_id: report.record_id,
|
|
817
|
+
owner_npub: report.owner_npub,
|
|
818
|
+
record_family_hash: recordFamilyHash(appNpub, 'report'),
|
|
819
|
+
version: (report.version ?? 0) + 1,
|
|
820
|
+
previous_version: report.version ?? 0,
|
|
821
|
+
signature_npub: session.npub,
|
|
822
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
823
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
824
|
+
owner_payload: encryptOwnerPayload(report.owner_npub, plaintext, session),
|
|
825
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export function makeGroupWriteShare(groupRef, label = '') {
|
|
830
|
+
const groupId = typeof groupRef === 'object'
|
|
831
|
+
? normalizeGroupRef(groupRef.group_id ?? groupRef.groupId ?? groupRef.id)
|
|
832
|
+
: normalizeGroupRef(groupRef);
|
|
833
|
+
const groupNpub = typeof groupRef === 'object'
|
|
834
|
+
? normalizeGroupRef(groupRef.current_group_npub ?? groupRef.group_npub ?? groupRef.groupNpub)
|
|
835
|
+
: null;
|
|
836
|
+
return {
|
|
837
|
+
type: 'group',
|
|
838
|
+
key: groupId || groupNpub,
|
|
839
|
+
access: 'write',
|
|
840
|
+
label,
|
|
841
|
+
person_npub: null,
|
|
842
|
+
group_id: groupId || groupNpub,
|
|
843
|
+
group_npub: groupNpub,
|
|
844
|
+
via_group_id: null,
|
|
845
|
+
via_group_npub: null,
|
|
846
|
+
inherited: false,
|
|
847
|
+
inherited_from_directory_id: null,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function encryptOwnerPayload(ownerNpub, plaintext, session) {
|
|
852
|
+
if (session.isWorkspaceKey) {
|
|
853
|
+
// Workspace key path: self-encrypt and wrap in envelope (matches FD format)
|
|
854
|
+
return {
|
|
855
|
+
ciphertext: JSON.stringify({
|
|
856
|
+
encrypted_by_npub: session.npub,
|
|
857
|
+
ciphertext: encryptForNpub(session.secret, session.npub, plaintext),
|
|
858
|
+
ws_key_epoch: session.wsKeyEpoch ?? 1,
|
|
859
|
+
}),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return { ciphertext: encryptForNpub(session.secret, ownerNpub, plaintext) };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function buildGroupPayloads(groupIds, plaintext, session, groupKeys, canWriteMap = null) {
|
|
866
|
+
const uniqueGroups = new Map();
|
|
867
|
+
const requestedRefs = [];
|
|
868
|
+
for (const value of (groupIds || [])) {
|
|
869
|
+
const ref = normalizeGroupRef(value);
|
|
870
|
+
if (!ref) continue;
|
|
871
|
+
requestedRefs.push(ref);
|
|
872
|
+
const keyEntry = resolveAccessibleGroupEntry(groupKeys, ref);
|
|
873
|
+
if (!keyEntry?.groupNpub) continue;
|
|
874
|
+
const stableGroupId = keyEntry.groupId || ref;
|
|
875
|
+
if (!uniqueGroups.has(stableGroupId)) uniqueGroups.set(stableGroupId, keyEntry);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (uniqueGroups.size === 0 && requestedRefs.length > 0) {
|
|
879
|
+
throw new Error(`No group key loaded for ${requestedRefs[0]}`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return [...uniqueGroups.entries()].map(([stableGroupId, keyEntry]) => ({
|
|
883
|
+
group_id: keyEntry.groupId || stableGroupId,
|
|
884
|
+
group_epoch: keyEntry.keyVersion || undefined,
|
|
885
|
+
group_npub: keyEntry.groupNpub,
|
|
886
|
+
ciphertext: JSON.stringify({
|
|
887
|
+
encrypted_by_npub: session.npub,
|
|
888
|
+
ciphertext: encryptForNpub(session.secret, keyEntry.groupNpub, plaintext),
|
|
889
|
+
}),
|
|
890
|
+
write: canWriteMap instanceof Map
|
|
891
|
+
? (canWriteMap.get(stableGroupId) === true || canWriteMap.get(keyEntry.groupNpub) === true)
|
|
892
|
+
: true,
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export function resolveCommentTargetGroupIds(targetRecord, groupKeyMap) {
|
|
897
|
+
const targetRefs = collectRecordDeliveryGroupRefs(targetRecord);
|
|
898
|
+
const targetGroupIds = [];
|
|
899
|
+
const missingGroupIds = [];
|
|
900
|
+
const seenTargets = new Set();
|
|
901
|
+
const seenMissing = new Set();
|
|
902
|
+
|
|
903
|
+
for (const ref of targetRefs) {
|
|
904
|
+
const keyEntry = resolveAccessibleGroupEntry(groupKeyMap, ref);
|
|
905
|
+
if (keyEntry?.groupId) {
|
|
906
|
+
if (!seenTargets.has(keyEntry.groupId)) {
|
|
907
|
+
seenTargets.add(keyEntry.groupId);
|
|
908
|
+
targetGroupIds.push(keyEntry.groupId);
|
|
909
|
+
}
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (!seenMissing.has(ref)) {
|
|
913
|
+
seenMissing.add(ref);
|
|
914
|
+
missingGroupIds.push(ref);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return { targetGroupIds, missingGroupIds };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function resolveWriteGroup(session, groupKeys, resource, fallbackGroupId = null) {
|
|
922
|
+
const resolveGroupId = (groupRef) => resolveAccessibleGroupEntry(groupKeys, groupRef)?.groupId ?? null;
|
|
923
|
+
const explicit = resolveGroupId(fallbackGroupId);
|
|
924
|
+
if (explicit) return explicit;
|
|
925
|
+
const shares = Array.isArray(resource?.shares) ? resource.shares : [];
|
|
926
|
+
const directPersonShare = shares.find((share) => (
|
|
927
|
+
share?.person_npub === session.npub
|
|
928
|
+
&& share?.access === 'write'
|
|
929
|
+
&& (
|
|
930
|
+
normalizeGroupRef(share?.via_group_id)
|
|
931
|
+
|| normalizeGroupRef(share?.via_group_npub)
|
|
932
|
+
)
|
|
933
|
+
&& resolveGroupId(share?.via_group_id ?? share?.via_group_npub)
|
|
934
|
+
));
|
|
935
|
+
if (directPersonShare?.via_group_id || directPersonShare?.via_group_npub) {
|
|
936
|
+
return resolveGroupId(directPersonShare.via_group_id ?? directPersonShare.via_group_npub);
|
|
937
|
+
}
|
|
938
|
+
const writableGroupShare = shares.find((share) => (
|
|
939
|
+
normalizeGroupRef(share?.group_id ?? share?.group_npub)
|
|
940
|
+
&& share?.access === 'write'
|
|
941
|
+
&& resolveGroupId(share?.group_id ?? share?.group_npub)
|
|
942
|
+
));
|
|
943
|
+
if (writableGroupShare?.group_id || writableGroupShare?.group_npub) {
|
|
944
|
+
return resolveGroupId(writableGroupShare.group_id ?? writableGroupShare.group_npub);
|
|
945
|
+
}
|
|
946
|
+
for (const groupId of (resource?.group_ids || [])) {
|
|
947
|
+
const resolved = resolveGroupId(groupId);
|
|
948
|
+
if (resolved) return resolved;
|
|
949
|
+
}
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function resolveWriteGroupMetadata(groupKeys, groupId) {
|
|
954
|
+
const resolvedGroupId = groupKeys.resolveGroupId(groupId);
|
|
955
|
+
if (!resolvedGroupId) return { groupId: null, groupNpub: null };
|
|
956
|
+
return {
|
|
957
|
+
groupId: resolvedGroupId,
|
|
958
|
+
groupNpub: groupKeys.resolveGroupNpub(resolvedGroupId),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function outboundChatMessage(appNpub, session, groupKeys, channel, {
|
|
963
|
+
recordId,
|
|
964
|
+
body,
|
|
965
|
+
parentMessageId = null,
|
|
966
|
+
attachments = [],
|
|
967
|
+
version = 1,
|
|
968
|
+
previousVersion = 0,
|
|
969
|
+
recordState = 'active',
|
|
970
|
+
}) {
|
|
971
|
+
const payload = {
|
|
972
|
+
app_namespace: appNpub,
|
|
973
|
+
collection_space: 'chat_message',
|
|
974
|
+
schema_version: 1,
|
|
975
|
+
record_id: recordId,
|
|
976
|
+
data: {
|
|
977
|
+
channel_id: channel.record_id,
|
|
978
|
+
parent_message_id: parentMessageId,
|
|
979
|
+
body,
|
|
980
|
+
attachments,
|
|
981
|
+
record_state: recordState,
|
|
982
|
+
},
|
|
983
|
+
};
|
|
984
|
+
const plaintext = JSON.stringify(payload);
|
|
985
|
+
const writeGroup = resolveWriteGroupMetadata(groupKeys, resolveWriteGroup(session, groupKeys, channel));
|
|
986
|
+
return {
|
|
987
|
+
record_id: recordId,
|
|
988
|
+
owner_npub: channel.owner_npub,
|
|
989
|
+
record_family_hash: recordFamilyHash(appNpub, 'chat_message'),
|
|
990
|
+
version,
|
|
991
|
+
previous_version: previousVersion,
|
|
992
|
+
signature_npub: session.npub,
|
|
993
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
994
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
995
|
+
owner_payload: encryptOwnerPayload(channel.owner_npub, plaintext, session),
|
|
996
|
+
group_payloads: buildGroupPayloads(channel.group_ids || [], plaintext, session, groupKeys),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export function outboundChannel(appNpub, session, groupKeys, {
|
|
1001
|
+
recordId,
|
|
1002
|
+
ownerNpub,
|
|
1003
|
+
title,
|
|
1004
|
+
groupIds = [],
|
|
1005
|
+
participantNpubs = [],
|
|
1006
|
+
scopeId = null,
|
|
1007
|
+
scopeL1Id = null,
|
|
1008
|
+
scopeL2Id = null,
|
|
1009
|
+
scopeL3Id = null,
|
|
1010
|
+
scopeL4Id = null,
|
|
1011
|
+
scopeL5Id = null,
|
|
1012
|
+
version = 1,
|
|
1013
|
+
previousVersion = 0,
|
|
1014
|
+
writeGroupNpub = null,
|
|
1015
|
+
recordState = 'active',
|
|
1016
|
+
}) {
|
|
1017
|
+
const payload = {
|
|
1018
|
+
app_namespace: appNpub,
|
|
1019
|
+
collection_space: 'channel',
|
|
1020
|
+
schema_version: 1,
|
|
1021
|
+
record_id: recordId,
|
|
1022
|
+
data: {
|
|
1023
|
+
title,
|
|
1024
|
+
participant_npubs: participantNpubs,
|
|
1025
|
+
scope_id: scopeId,
|
|
1026
|
+
scope_l1_id: scopeL1Id,
|
|
1027
|
+
scope_l2_id: scopeL2Id,
|
|
1028
|
+
scope_l3_id: scopeL3Id,
|
|
1029
|
+
scope_l4_id: scopeL4Id,
|
|
1030
|
+
scope_l5_id: scopeL5Id,
|
|
1031
|
+
record_state: recordState,
|
|
1032
|
+
},
|
|
1033
|
+
};
|
|
1034
|
+
const plaintext = JSON.stringify(payload);
|
|
1035
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1036
|
+
groupKeys,
|
|
1037
|
+
writeGroupNpub || resolveWriteGroup(session, groupKeys, { group_ids: groupIds })
|
|
1038
|
+
);
|
|
1039
|
+
return {
|
|
1040
|
+
record_id: recordId,
|
|
1041
|
+
owner_npub: ownerNpub,
|
|
1042
|
+
record_family_hash: recordFamilyHash(appNpub, 'channel'),
|
|
1043
|
+
version,
|
|
1044
|
+
previous_version: previousVersion,
|
|
1045
|
+
signature_npub: session.npub,
|
|
1046
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1047
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1048
|
+
owner_payload: encryptOwnerPayload(ownerNpub, plaintext, session),
|
|
1049
|
+
group_payloads: buildGroupPayloads(groupIds || [], plaintext, session, groupKeys),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export function outboundTask(appNpub, session, groupKeys, task, patch = {}) {
|
|
1054
|
+
const next = { ...task, ...patch };
|
|
1055
|
+
const payload = {
|
|
1056
|
+
app_namespace: appNpub,
|
|
1057
|
+
collection_space: 'task',
|
|
1058
|
+
schema_version: 1,
|
|
1059
|
+
record_id: task.record_id,
|
|
1060
|
+
data: {
|
|
1061
|
+
title: next.title,
|
|
1062
|
+
description: next.description ?? '',
|
|
1063
|
+
state: next.state ?? 'new',
|
|
1064
|
+
priority: next.priority ?? 'sand',
|
|
1065
|
+
assigned_to_npub: next.assigned_to_npub ?? null,
|
|
1066
|
+
parent_task_id: next.parent_task_id ?? null,
|
|
1067
|
+
board_group_id: next.board_group_id ?? null,
|
|
1068
|
+
scheduled_for: next.scheduled_for ?? null,
|
|
1069
|
+
tags: next.tags ?? '',
|
|
1070
|
+
scope_id: next.scope_id ?? null,
|
|
1071
|
+
scope_l1_id: next.scope_l1_id ?? null,
|
|
1072
|
+
scope_l2_id: next.scope_l2_id ?? null,
|
|
1073
|
+
scope_l3_id: next.scope_l3_id ?? null,
|
|
1074
|
+
scope_l4_id: next.scope_l4_id ?? null,
|
|
1075
|
+
scope_l5_id: next.scope_l5_id ?? null,
|
|
1076
|
+
references: next.references ?? [],
|
|
1077
|
+
predecessor_task_ids: Array.isArray(next.predecessor_task_ids) && next.predecessor_task_ids.length > 0 ? next.predecessor_task_ids : null,
|
|
1078
|
+
flow_id: next.flow_id ?? null,
|
|
1079
|
+
flow_run_id: next.flow_run_id ?? null,
|
|
1080
|
+
flow_step: next.flow_step != null && Number.isFinite(Number(next.flow_step)) ? Number(next.flow_step) : null,
|
|
1081
|
+
shares: next.shares ?? [],
|
|
1082
|
+
record_state: next.record_state ?? 'active',
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
const plaintext = JSON.stringify(payload);
|
|
1086
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1087
|
+
groupKeys,
|
|
1088
|
+
resolveWriteGroup(session, groupKeys, next, next.board_group_id)
|
|
1089
|
+
);
|
|
1090
|
+
return {
|
|
1091
|
+
record_id: task.record_id,
|
|
1092
|
+
owner_npub: task.owner_npub,
|
|
1093
|
+
record_family_hash: recordFamilyHash(appNpub, 'task'),
|
|
1094
|
+
version: (task.version ?? 1) + 1,
|
|
1095
|
+
previous_version: task.version ?? 1,
|
|
1096
|
+
signature_npub: session.npub,
|
|
1097
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1098
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1099
|
+
owner_payload: encryptOwnerPayload(task.owner_npub, plaintext, session),
|
|
1100
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export function outboundComment(appNpub, session, groupKeys, target, {
|
|
1105
|
+
recordId,
|
|
1106
|
+
body,
|
|
1107
|
+
parentCommentId = null,
|
|
1108
|
+
anchorBlockId = null,
|
|
1109
|
+
anchorLineNumber = null,
|
|
1110
|
+
attachments = [],
|
|
1111
|
+
commentStatus = 'open',
|
|
1112
|
+
recordState = 'active',
|
|
1113
|
+
version = 1,
|
|
1114
|
+
previousVersion = 0,
|
|
1115
|
+
}) {
|
|
1116
|
+
const isDocumentTarget = target.content != null || target.content_format != null || Array.isArray(target.content_blocks);
|
|
1117
|
+
const delivery = resolveCommentTargetGroupIds(target, groupKeys);
|
|
1118
|
+
if (delivery.targetGroupIds.length === 0) {
|
|
1119
|
+
const requested = collectRecordDeliveryGroupRefs(target);
|
|
1120
|
+
const detail = requested.length > 0 ? ` Requested groups: ${requested.join(', ')}` : '';
|
|
1121
|
+
throw new Error(`Cannot create comment for ${target.record_id}: no encryptable target group key is loaded.${detail}`);
|
|
1122
|
+
}
|
|
1123
|
+
if (isDocumentTarget && delivery.missingGroupIds.length > 0) {
|
|
1124
|
+
throw new Error(`Cannot create document comment for ${target.record_id}: missing group keys for ${delivery.missingGroupIds.join(', ')}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (delivery.missingGroupIds.length > 0) {
|
|
1127
|
+
console.warn(`Warning: comment delivery for ${target.record_id} skipped groups without local keys: ${delivery.missingGroupIds.join(', ')}`);
|
|
1128
|
+
}
|
|
1129
|
+
const targetDeliveryGroupCount = collectRecordDeliveryGroupRefs(target).length;
|
|
1130
|
+
if (targetDeliveryGroupCount > 1 && delivery.targetGroupIds.length === 1) {
|
|
1131
|
+
console.warn(`Warning: comment delivery for ${target.record_id} resolved to one group (${delivery.targetGroupIds[0]}) from ${targetDeliveryGroupCount} target groups.`);
|
|
1132
|
+
}
|
|
1133
|
+
const targetFamilyHash = target.content != null
|
|
1134
|
+
? recordFamilyHash(appNpub, 'document')
|
|
1135
|
+
: recordFamilyHash(appNpub, 'task');
|
|
1136
|
+
const payload = {
|
|
1137
|
+
app_namespace: appNpub,
|
|
1138
|
+
collection_space: 'comment',
|
|
1139
|
+
schema_version: 1,
|
|
1140
|
+
record_id: recordId,
|
|
1141
|
+
data: {
|
|
1142
|
+
target_record_id: target.record_id,
|
|
1143
|
+
target_record_family_hash: targetFamilyHash,
|
|
1144
|
+
parent_comment_id: parentCommentId,
|
|
1145
|
+
anchor_block_id: anchorBlockId,
|
|
1146
|
+
anchor_line_number: anchorLineNumber,
|
|
1147
|
+
comment_status: commentStatus,
|
|
1148
|
+
body,
|
|
1149
|
+
attachments,
|
|
1150
|
+
record_state: recordState,
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
const plaintext = JSON.stringify(payload);
|
|
1154
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1155
|
+
groupKeys,
|
|
1156
|
+
resolveWriteGroup(session, groupKeys, target, target.board_group_id)
|
|
1157
|
+
);
|
|
1158
|
+
return {
|
|
1159
|
+
record_id: recordId,
|
|
1160
|
+
owner_npub: target.owner_npub,
|
|
1161
|
+
record_family_hash: recordFamilyHash(appNpub, 'comment'),
|
|
1162
|
+
version,
|
|
1163
|
+
previous_version: previousVersion,
|
|
1164
|
+
signature_npub: session.npub,
|
|
1165
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1166
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1167
|
+
owner_payload: encryptOwnerPayload(target.owner_npub, plaintext, session),
|
|
1168
|
+
group_payloads: buildGroupPayloads(delivery.targetGroupIds, plaintext, session, groupKeys),
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
export function outboundReaction(appNpub, session, groupKeys, reaction, patch = {}) {
|
|
1173
|
+
const next = { ...reaction, ...patch };
|
|
1174
|
+
const targetRecordId = String(next.target_record_id || next.targetRecordId || '').trim();
|
|
1175
|
+
const targetFamilyHash = assertReactionTargetFamily(next.target_record_family_hash || next.targetRecordFamilyHash);
|
|
1176
|
+
const emoji = assertReactionEmoji(next.emoji);
|
|
1177
|
+
const reactorNpub = String(next.reactor_npub || next.reactorNpub || session.npub || '').trim();
|
|
1178
|
+
const ownerNpub = String(next.owner_npub || next.ownerNpub || '').trim();
|
|
1179
|
+
const recordId = next.record_id || next.recordId;
|
|
1180
|
+
if (!recordId) throw new Error('reaction record_id is required');
|
|
1181
|
+
if (!targetRecordId) throw new Error('reaction target_record_id is required');
|
|
1182
|
+
if (!reactorNpub) throw new Error('reaction reactor_npub is required');
|
|
1183
|
+
if (!ownerNpub) throw new Error('reaction owner_npub is required');
|
|
1184
|
+
|
|
1185
|
+
const groupIds = next.target_group_ids || next.targetGroupIds || next.group_ids || [];
|
|
1186
|
+
const payload = {
|
|
1187
|
+
app_namespace: appNpub,
|
|
1188
|
+
collection_space: REACTION_COLLECTION_SPACE,
|
|
1189
|
+
schema_version: 1,
|
|
1190
|
+
record_id: recordId,
|
|
1191
|
+
data: {
|
|
1192
|
+
target_record_id: targetRecordId,
|
|
1193
|
+
target_record_family_hash: targetFamilyHash,
|
|
1194
|
+
emoji,
|
|
1195
|
+
emoji_shortcode: reactionShortcode(emoji),
|
|
1196
|
+
reactor_npub: reactorNpub,
|
|
1197
|
+
record_state: next.record_state === 'deleted' ? 'deleted' : 'active',
|
|
1198
|
+
},
|
|
1199
|
+
};
|
|
1200
|
+
const plaintext = JSON.stringify(payload);
|
|
1201
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1202
|
+
groupKeys,
|
|
1203
|
+
next.write_group_id
|
|
1204
|
+
?? next.write_group_npub
|
|
1205
|
+
?? next.write_group_ref
|
|
1206
|
+
?? next.writeGroupRef
|
|
1207
|
+
?? resolveWriteGroup(session, groupKeys, { group_ids: groupIds }, groupIds[0] ?? null)
|
|
1208
|
+
);
|
|
1209
|
+
return {
|
|
1210
|
+
record_id: recordId,
|
|
1211
|
+
owner_npub: ownerNpub,
|
|
1212
|
+
record_family_hash: recordFamilyHash(appNpub, REACTION_COLLECTION_SPACE),
|
|
1213
|
+
version: next.version ?? ((reaction.version ?? 0) + 1),
|
|
1214
|
+
previous_version: next.previous_version ?? reaction.version ?? 0,
|
|
1215
|
+
signature_npub: next.signature_npub || session.npub,
|
|
1216
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1217
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1218
|
+
owner_payload: encryptOwnerPayload(ownerNpub, plaintext, session),
|
|
1219
|
+
group_payloads: buildGroupPayloads(groupIds, plaintext, session, groupKeys),
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
export function outboundDocument(appNpub, session, groupKeys, document, patch = {}) {
|
|
1224
|
+
const next = { ...document, ...patch };
|
|
1225
|
+
const content = next.content ?? document.content ?? '';
|
|
1226
|
+
const contentBlocks = normalizeDocumentBlocks(next.content_blocks ?? document.content_blocks, content);
|
|
1227
|
+
const payload = {
|
|
1228
|
+
app_namespace: appNpub,
|
|
1229
|
+
collection_space: 'document',
|
|
1230
|
+
schema_version: 1,
|
|
1231
|
+
record_id: document.record_id,
|
|
1232
|
+
data: {
|
|
1233
|
+
title: next.title ?? document.title ?? 'Untitled document',
|
|
1234
|
+
content,
|
|
1235
|
+
content_format: next.content_format ?? document.content_format ?? BLOCK_DOCUMENT_FORMAT,
|
|
1236
|
+
content_blocks: contentBlocks,
|
|
1237
|
+
parent_directory_id: next.parent_directory_id ?? document.parent_directory_id ?? null,
|
|
1238
|
+
scope_id: next.scope_id ?? document.scope_id ?? null,
|
|
1239
|
+
scope_l1_id: next.scope_l1_id ?? document.scope_l1_id ?? null,
|
|
1240
|
+
scope_l2_id: next.scope_l2_id ?? document.scope_l2_id ?? null,
|
|
1241
|
+
scope_l3_id: next.scope_l3_id ?? document.scope_l3_id ?? null,
|
|
1242
|
+
scope_l4_id: next.scope_l4_id ?? document.scope_l4_id ?? null,
|
|
1243
|
+
scope_l5_id: next.scope_l5_id ?? document.scope_l5_id ?? null,
|
|
1244
|
+
shares: next.shares ?? document.shares ?? [],
|
|
1245
|
+
record_state: next.record_state ?? document.record_state ?? 'active',
|
|
1246
|
+
},
|
|
1247
|
+
};
|
|
1248
|
+
const plaintext = JSON.stringify(payload);
|
|
1249
|
+
const writeGroup = resolveWriteGroupMetadata(groupKeys, resolveWriteGroup(session, groupKeys, next));
|
|
1250
|
+
return {
|
|
1251
|
+
record_id: document.record_id,
|
|
1252
|
+
owner_npub: document.owner_npub,
|
|
1253
|
+
record_family_hash: recordFamilyHash(appNpub, 'document'),
|
|
1254
|
+
version: (document.version ?? 1) + 1,
|
|
1255
|
+
previous_version: document.version ?? 1,
|
|
1256
|
+
signature_npub: session.npub,
|
|
1257
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1258
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1259
|
+
owner_payload: encryptOwnerPayload(document.owner_npub, plaintext, session),
|
|
1260
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export function outboundAudioNote(appNpub, session, groupKeys, {
|
|
1265
|
+
recordId,
|
|
1266
|
+
ownerNpub,
|
|
1267
|
+
targetRecordId = null,
|
|
1268
|
+
targetRecordFamilyHash = null,
|
|
1269
|
+
title = 'Voice note',
|
|
1270
|
+
storageObjectId,
|
|
1271
|
+
mimeType = 'audio/webm;codecs=opus',
|
|
1272
|
+
durationSeconds = null,
|
|
1273
|
+
sizeBytes = 0,
|
|
1274
|
+
mediaEncryption = null,
|
|
1275
|
+
waveformPreview = [],
|
|
1276
|
+
transcriptStatus = 'pending',
|
|
1277
|
+
transcriptPreview = null,
|
|
1278
|
+
transcript = null,
|
|
1279
|
+
summary = null,
|
|
1280
|
+
targetGroupIds = [],
|
|
1281
|
+
version = 1,
|
|
1282
|
+
previousVersion = 0,
|
|
1283
|
+
writeGroupNpub = null,
|
|
1284
|
+
recordState = 'active',
|
|
1285
|
+
}) {
|
|
1286
|
+
const payload = {
|
|
1287
|
+
app_namespace: appNpub,
|
|
1288
|
+
collection_space: 'audio_note',
|
|
1289
|
+
schema_version: 1,
|
|
1290
|
+
record_id: recordId,
|
|
1291
|
+
data: {
|
|
1292
|
+
target_record_id: targetRecordId,
|
|
1293
|
+
target_record_family_hash: targetRecordFamilyHash,
|
|
1294
|
+
title,
|
|
1295
|
+
storage_object_id: storageObjectId,
|
|
1296
|
+
mime_type: mimeType,
|
|
1297
|
+
duration_seconds: durationSeconds,
|
|
1298
|
+
size_bytes: sizeBytes,
|
|
1299
|
+
media_encryption: mediaEncryption,
|
|
1300
|
+
waveform_preview: waveformPreview,
|
|
1301
|
+
transcript_status: transcriptStatus,
|
|
1302
|
+
transcript_preview: transcriptPreview,
|
|
1303
|
+
transcript,
|
|
1304
|
+
summary,
|
|
1305
|
+
record_state: recordState,
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1308
|
+
const plaintext = JSON.stringify(payload);
|
|
1309
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1310
|
+
groupKeys,
|
|
1311
|
+
writeGroupNpub || resolveWriteGroup(session, groupKeys, { group_ids: targetGroupIds })
|
|
1312
|
+
);
|
|
1313
|
+
return {
|
|
1314
|
+
record_id: recordId,
|
|
1315
|
+
owner_npub: ownerNpub,
|
|
1316
|
+
record_family_hash: recordFamilyHash(appNpub, 'audio_note'),
|
|
1317
|
+
version,
|
|
1318
|
+
previous_version: previousVersion,
|
|
1319
|
+
signature_npub: session.npub,
|
|
1320
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1321
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1322
|
+
owner_payload: encryptOwnerPayload(ownerNpub, plaintext, session),
|
|
1323
|
+
group_payloads: buildGroupPayloads(targetGroupIds || [], plaintext, session, groupKeys),
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
export function outboundDirectory(appNpub, session, groupKeys, directory, patch = {}) {
|
|
1328
|
+
const next = { ...directory, ...patch };
|
|
1329
|
+
const payload = {
|
|
1330
|
+
app_namespace: appNpub,
|
|
1331
|
+
collection_space: 'directory',
|
|
1332
|
+
schema_version: 1,
|
|
1333
|
+
record_id: directory.record_id,
|
|
1334
|
+
data: {
|
|
1335
|
+
title: next.title ?? directory.title ?? 'Untitled directory',
|
|
1336
|
+
parent_directory_id: next.parent_directory_id ?? directory.parent_directory_id ?? null,
|
|
1337
|
+
scope_id: next.scope_id ?? directory.scope_id ?? null,
|
|
1338
|
+
scope_l1_id: next.scope_l1_id ?? directory.scope_l1_id ?? null,
|
|
1339
|
+
scope_l2_id: next.scope_l2_id ?? directory.scope_l2_id ?? null,
|
|
1340
|
+
scope_l3_id: next.scope_l3_id ?? directory.scope_l3_id ?? null,
|
|
1341
|
+
scope_l4_id: next.scope_l4_id ?? directory.scope_l4_id ?? null,
|
|
1342
|
+
scope_l5_id: next.scope_l5_id ?? directory.scope_l5_id ?? null,
|
|
1343
|
+
shares: next.shares ?? directory.shares ?? [],
|
|
1344
|
+
record_state: next.record_state ?? directory.record_state ?? 'active',
|
|
1345
|
+
},
|
|
1346
|
+
};
|
|
1347
|
+
const plaintext = JSON.stringify(payload);
|
|
1348
|
+
const writeGroup = resolveWriteGroupMetadata(groupKeys, resolveWriteGroup(session, groupKeys, next));
|
|
1349
|
+
return {
|
|
1350
|
+
record_id: directory.record_id,
|
|
1351
|
+
owner_npub: directory.owner_npub,
|
|
1352
|
+
record_family_hash: recordFamilyHash(appNpub, 'directory'),
|
|
1353
|
+
version: (directory.version ?? 1) + 1,
|
|
1354
|
+
previous_version: directory.version ?? 1,
|
|
1355
|
+
signature_npub: session.npub,
|
|
1356
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1357
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1358
|
+
owner_payload: encryptOwnerPayload(directory.owner_npub, plaintext, session),
|
|
1359
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export function outboundScope(appNpub, session, groupKeys, scope, patch = {}) {
|
|
1364
|
+
const next = { ...scope, ...patch };
|
|
1365
|
+
const payload = {
|
|
1366
|
+
app_namespace: appNpub,
|
|
1367
|
+
collection_space: 'scope',
|
|
1368
|
+
schema_version: 1,
|
|
1369
|
+
record_id: scope.record_id,
|
|
1370
|
+
data: {
|
|
1371
|
+
title: next.title ?? scope.title ?? '',
|
|
1372
|
+
description: next.description ?? scope.description ?? '',
|
|
1373
|
+
level: next.level ?? scope.level ?? 'l1',
|
|
1374
|
+
parent_id: next.parent_id ?? scope.parent_id ?? null,
|
|
1375
|
+
l1_id: next.l1_id ?? scope.l1_id ?? null,
|
|
1376
|
+
l2_id: next.l2_id ?? scope.l2_id ?? null,
|
|
1377
|
+
l3_id: next.l3_id ?? scope.l3_id ?? null,
|
|
1378
|
+
l4_id: next.l4_id ?? scope.l4_id ?? null,
|
|
1379
|
+
l5_id: next.l5_id ?? scope.l5_id ?? null,
|
|
1380
|
+
record_state: next.record_state ?? scope.record_state ?? 'active',
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
const plaintext = JSON.stringify(payload);
|
|
1384
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1385
|
+
groupKeys,
|
|
1386
|
+
resolveWriteGroup(session, groupKeys, next, next.group_ids?.[0] ?? null)
|
|
1387
|
+
);
|
|
1388
|
+
return {
|
|
1389
|
+
record_id: scope.record_id,
|
|
1390
|
+
owner_npub: scope.owner_npub,
|
|
1391
|
+
record_family_hash: recordFamilyHash(appNpub, 'scope'),
|
|
1392
|
+
version: (scope.version ?? 1) + 1,
|
|
1393
|
+
previous_version: scope.version ?? 1,
|
|
1394
|
+
signature_npub: session.npub,
|
|
1395
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1396
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1397
|
+
owner_payload: encryptOwnerPayload(scope.owner_npub, plaintext, session),
|
|
1398
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
export function inboundFlow(record, payload) {
|
|
1403
|
+
const data = payload.data ?? payload;
|
|
1404
|
+
return {
|
|
1405
|
+
record_id: record.record_id,
|
|
1406
|
+
owner_npub: record.owner_npub,
|
|
1407
|
+
title: data.title ?? '',
|
|
1408
|
+
description: data.description ?? '',
|
|
1409
|
+
steps: Array.isArray(data.steps) ? data.steps : [],
|
|
1410
|
+
next_flow_id: data.next_flow_id ?? null,
|
|
1411
|
+
scope_id: data.scope_id ?? null,
|
|
1412
|
+
...normalizeScopedRecordLineage(data),
|
|
1413
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
1414
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
1415
|
+
record_state: normalizeRecordState(data),
|
|
1416
|
+
version: record.version ?? 1,
|
|
1417
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
export function outboundFlow(appNpub, session, groupKeys, flow, patch = {}) {
|
|
1422
|
+
const next = { ...flow, ...patch };
|
|
1423
|
+
const payload = {
|
|
1424
|
+
app_namespace: appNpub,
|
|
1425
|
+
collection_space: 'flow',
|
|
1426
|
+
schema_version: 1,
|
|
1427
|
+
record_id: flow.record_id,
|
|
1428
|
+
data: {
|
|
1429
|
+
title: next.title,
|
|
1430
|
+
description: next.description ?? '',
|
|
1431
|
+
steps: Array.isArray(next.steps) ? next.steps : [],
|
|
1432
|
+
next_flow_id: next.next_flow_id ?? null,
|
|
1433
|
+
scope_id: next.scope_id ?? null,
|
|
1434
|
+
scope_l1_id: next.scope_l1_id ?? null,
|
|
1435
|
+
scope_l2_id: next.scope_l2_id ?? null,
|
|
1436
|
+
scope_l3_id: next.scope_l3_id ?? null,
|
|
1437
|
+
scope_l4_id: next.scope_l4_id ?? null,
|
|
1438
|
+
scope_l5_id: next.scope_l5_id ?? null,
|
|
1439
|
+
shares: next.shares ?? [],
|
|
1440
|
+
record_state: next.record_state ?? 'active',
|
|
1441
|
+
},
|
|
1442
|
+
};
|
|
1443
|
+
const plaintext = JSON.stringify(payload);
|
|
1444
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1445
|
+
groupKeys,
|
|
1446
|
+
resolveWriteGroup(session, groupKeys, next, next.group_ids?.[0] ?? null)
|
|
1447
|
+
);
|
|
1448
|
+
return {
|
|
1449
|
+
record_id: flow.record_id,
|
|
1450
|
+
owner_npub: flow.owner_npub,
|
|
1451
|
+
record_family_hash: recordFamilyHash(appNpub, 'flow'),
|
|
1452
|
+
version: (flow.version ?? 0) + 1,
|
|
1453
|
+
previous_version: flow.version ?? 0,
|
|
1454
|
+
signature_npub: session.npub,
|
|
1455
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1456
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1457
|
+
owner_payload: encryptOwnerPayload(flow.owner_npub, plaintext, session),
|
|
1458
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function normalizeApprovalArtifactRef(appNpub, ref, options = {}) {
|
|
1463
|
+
if (!ref || typeof ref !== 'object') return null;
|
|
1464
|
+
const canonicalOnly = options.canonicalOnly === true;
|
|
1465
|
+
const requireFamilyHash = options.requireFamilyHash === true;
|
|
1466
|
+
const recordId = ref.record_id ?? ref.id ?? null;
|
|
1467
|
+
if (!recordId) return null;
|
|
1468
|
+
const familyType = typeof ref.type === 'string' && ref.type ? ref.type : null;
|
|
1469
|
+
const familyHash = ref.record_family_hash || (familyType ? recordFamilyHash(appNpub, familyType) : null);
|
|
1470
|
+
if (requireFamilyHash && !familyHash) {
|
|
1471
|
+
throw new Error(`Approval artifact ref ${recordId} is missing record_family_hash`);
|
|
1472
|
+
}
|
|
1473
|
+
const normalized = {
|
|
1474
|
+
record_id: recordId,
|
|
1475
|
+
...(familyHash ? { record_family_hash: familyHash } : {}),
|
|
1476
|
+
};
|
|
1477
|
+
if (canonicalOnly) return normalized;
|
|
1478
|
+
return {
|
|
1479
|
+
...ref,
|
|
1480
|
+
...normalized,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function normalizeApprovalArtifactRefs(appNpub, refs, options = {}) {
|
|
1485
|
+
if (!Array.isArray(refs)) return [];
|
|
1486
|
+
return refs
|
|
1487
|
+
.map((ref) => normalizeApprovalArtifactRef(appNpub, ref, options))
|
|
1488
|
+
.filter(Boolean);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
export function inboundApproval(record, payload) {
|
|
1492
|
+
const data = payload.data ?? payload;
|
|
1493
|
+
const appNpub = payload.app_namespace ?? null;
|
|
1494
|
+
return {
|
|
1495
|
+
record_id: record.record_id,
|
|
1496
|
+
owner_npub: record.owner_npub,
|
|
1497
|
+
title: data.title ?? '',
|
|
1498
|
+
description: data.description ?? '',
|
|
1499
|
+
flow_id: data.flow_id ?? null,
|
|
1500
|
+
flow_run_id: data.flow_run_id ?? null,
|
|
1501
|
+
flow_step: Number.isFinite(Number(data.flow_step)) ? Number(data.flow_step) : null,
|
|
1502
|
+
task_ids: Array.isArray(data.task_ids) ? data.task_ids : [],
|
|
1503
|
+
status: data.status ?? 'pending',
|
|
1504
|
+
approval_mode: data.approval_mode ?? 'manual',
|
|
1505
|
+
brief: data.brief ?? '',
|
|
1506
|
+
approver_whitelist: Array.isArray(data.approver_whitelist) ? data.approver_whitelist : [],
|
|
1507
|
+
confidence_score: Number.isFinite(Number(data.confidence_score)) ? Number(data.confidence_score) : null,
|
|
1508
|
+
approved_by: data.approved_by ?? null,
|
|
1509
|
+
approved_at: data.approved_at ?? null,
|
|
1510
|
+
decision_note: data.decision_note ?? null,
|
|
1511
|
+
agent_review_by: data.agent_review_by ?? null,
|
|
1512
|
+
agent_review_note: data.agent_review_note ?? null,
|
|
1513
|
+
artifact_refs: normalizeApprovalArtifactRefs(appNpub, data.artifact_refs),
|
|
1514
|
+
revision_task_id: data.revision_task_id ?? null,
|
|
1515
|
+
scope_id: data.scope_id ?? null,
|
|
1516
|
+
...normalizeScopedRecordLineage(data),
|
|
1517
|
+
shares: normalizeShares(data.shares, record.group_payloads || []),
|
|
1518
|
+
group_ids: collectGroupIds(record.group_payloads || []),
|
|
1519
|
+
record_state: normalizeRecordState(data),
|
|
1520
|
+
version: record.version ?? 1,
|
|
1521
|
+
updated_at: record.updated_at ?? new Date().toISOString(),
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
export function outboundApproval(appNpub, session, groupKeys, approval, patch = {}) {
|
|
1526
|
+
const next = { ...approval, ...patch };
|
|
1527
|
+
const payload = {
|
|
1528
|
+
app_namespace: appNpub,
|
|
1529
|
+
collection_space: 'approval',
|
|
1530
|
+
schema_version: 1,
|
|
1531
|
+
record_id: approval.record_id,
|
|
1532
|
+
data: {
|
|
1533
|
+
title: next.title,
|
|
1534
|
+
description: next.description ?? '',
|
|
1535
|
+
flow_id: next.flow_id ?? null,
|
|
1536
|
+
flow_run_id: next.flow_run_id ?? null,
|
|
1537
|
+
flow_step: Number.isFinite(Number(next.flow_step)) ? Number(next.flow_step) : null,
|
|
1538
|
+
task_ids: Array.isArray(next.task_ids) ? next.task_ids : [],
|
|
1539
|
+
status: next.status ?? 'pending',
|
|
1540
|
+
approval_mode: next.approval_mode ?? 'manual',
|
|
1541
|
+
brief: next.brief ?? '',
|
|
1542
|
+
approver_whitelist: Array.isArray(next.approver_whitelist) ? next.approver_whitelist : [],
|
|
1543
|
+
confidence_score: Number.isFinite(Number(next.confidence_score)) ? Number(next.confidence_score) : null,
|
|
1544
|
+
approved_by: next.approved_by ?? null,
|
|
1545
|
+
approved_at: next.approved_at ?? null,
|
|
1546
|
+
decision_note: next.decision_note ?? null,
|
|
1547
|
+
agent_review_by: next.agent_review_by ?? null,
|
|
1548
|
+
agent_review_note: next.agent_review_note ?? null,
|
|
1549
|
+
artifact_refs: normalizeApprovalArtifactRefs(appNpub, next.artifact_refs, {
|
|
1550
|
+
canonicalOnly: true,
|
|
1551
|
+
requireFamilyHash: true,
|
|
1552
|
+
}),
|
|
1553
|
+
revision_task_id: next.revision_task_id ?? null,
|
|
1554
|
+
scope_id: next.scope_id ?? null,
|
|
1555
|
+
scope_l1_id: next.scope_l1_id ?? null,
|
|
1556
|
+
scope_l2_id: next.scope_l2_id ?? null,
|
|
1557
|
+
scope_l3_id: next.scope_l3_id ?? null,
|
|
1558
|
+
scope_l4_id: next.scope_l4_id ?? null,
|
|
1559
|
+
scope_l5_id: next.scope_l5_id ?? null,
|
|
1560
|
+
shares: next.shares ?? [],
|
|
1561
|
+
record_state: next.record_state ?? 'active',
|
|
1562
|
+
},
|
|
1563
|
+
};
|
|
1564
|
+
const plaintext = JSON.stringify(payload);
|
|
1565
|
+
const writeGroup = resolveWriteGroupMetadata(
|
|
1566
|
+
groupKeys,
|
|
1567
|
+
resolveWriteGroup(session, groupKeys, next, next.group_ids?.[0] ?? null)
|
|
1568
|
+
);
|
|
1569
|
+
return {
|
|
1570
|
+
record_id: approval.record_id,
|
|
1571
|
+
owner_npub: approval.owner_npub,
|
|
1572
|
+
record_family_hash: recordFamilyHash(appNpub, 'approval'),
|
|
1573
|
+
version: (approval.version ?? 0) + 1,
|
|
1574
|
+
previous_version: approval.version ?? 0,
|
|
1575
|
+
signature_npub: session.npub,
|
|
1576
|
+
write_group_id: writeGroup.groupId || undefined,
|
|
1577
|
+
write_group_npub: writeGroup.groupNpub || undefined,
|
|
1578
|
+
owner_payload: encryptOwnerPayload(approval.owner_npub, plaintext, session),
|
|
1579
|
+
group_payloads: buildGroupPayloads(next.group_ids || [], plaintext, session, groupKeys),
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// --- reference and flow linkage helpers ---
|
|
1584
|
+
|
|
1585
|
+
const MENTION_TOKEN_RE = /@\[.*?\]\(mention:(\w+):([^)]+)\)/g;
|
|
1586
|
+
|
|
1587
|
+
export function parseReferencesFromDescription(description) {
|
|
1588
|
+
if (!description) return [];
|
|
1589
|
+
const seen = new Set();
|
|
1590
|
+
const refs = [];
|
|
1591
|
+
let match;
|
|
1592
|
+
const re = new RegExp(MENTION_TOKEN_RE.source, 'g');
|
|
1593
|
+
while ((match = re.exec(description)) !== null) {
|
|
1594
|
+
const type = match[1];
|
|
1595
|
+
const id = match[2];
|
|
1596
|
+
if (type === 'person') continue;
|
|
1597
|
+
const key = `${type}:${id}`;
|
|
1598
|
+
if (seen.has(key)) continue;
|
|
1599
|
+
seen.add(key);
|
|
1600
|
+
refs.push({ type, id });
|
|
1601
|
+
}
|
|
1602
|
+
return refs;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const RUN_FLOW_RE = /^run\s+flow:\s*(.+)/i;
|
|
1606
|
+
|
|
1607
|
+
export function parseFlowReferenceFromText(text) {
|
|
1608
|
+
if (!text) return null;
|
|
1609
|
+
const firstLine = text.split('\n')[0];
|
|
1610
|
+
const match = RUN_FLOW_RE.exec(firstLine.trim());
|
|
1611
|
+
if (!match) return null;
|
|
1612
|
+
const flowTitle = match[1].trim();
|
|
1613
|
+
if (!flowTitle) return null;
|
|
1614
|
+
return { flowTitle };
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
export function resolveFlowLinkage({ title, description, references, flows }) {
|
|
1618
|
+
const refs = [...(references || [])];
|
|
1619
|
+
const titleParsed = parseFlowReferenceFromText(title);
|
|
1620
|
+
const isRunIntent = !!titleParsed;
|
|
1621
|
+
|
|
1622
|
+
let resolvedFlowId = null;
|
|
1623
|
+
if (titleParsed && Array.isArray(flows)) {
|
|
1624
|
+
const match = flows.find(
|
|
1625
|
+
(f) => f.title && f.title.toLowerCase() === titleParsed.flowTitle.toLowerCase()
|
|
1626
|
+
);
|
|
1627
|
+
if (match) {
|
|
1628
|
+
resolvedFlowId = match.record_id;
|
|
1629
|
+
const alreadyReferenced = refs.some(
|
|
1630
|
+
(r) => r.type === 'flow' && r.id === match.record_id
|
|
1631
|
+
);
|
|
1632
|
+
if (!alreadyReferenced) {
|
|
1633
|
+
refs.push({ type: 'flow', id: match.record_id });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (!resolvedFlowId) {
|
|
1639
|
+
const flowRef = refs.find((r) => r.type === 'flow');
|
|
1640
|
+
if (flowRef && Array.isArray(flows)) {
|
|
1641
|
+
const match = flows.find((f) => f.record_id === flowRef.id);
|
|
1642
|
+
if (match) resolvedFlowId = match.record_id;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return {
|
|
1647
|
+
flow_id: resolvedFlowId,
|
|
1648
|
+
flow_run_id: resolvedFlowId && isRunIntent ? crypto.randomUUID() : null,
|
|
1649
|
+
flow_step: resolvedFlowId && isRunIntent ? 1 : null,
|
|
1650
|
+
references: resolvedFlowId ? refs : references || [],
|
|
1651
|
+
};
|
|
1652
|
+
}
|