@pellux/goodvibes-tui 0.25.0 → 0.26.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/CHANGELOG.md +5 -3
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +2419 -1040
- package/package.json +2 -2
- package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +28 -24
- package/src/daemon/handlers/calendar/index.ts +316 -0
- package/src/daemon/handlers/context.ts +29 -0
- package/src/daemon/handlers/contracts.ts +77 -0
- package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
- package/src/daemon/handlers/drafts/index.ts +17 -0
- package/src/daemon/handlers/drafts/register.ts +331 -0
- package/src/daemon/handlers/email/config.ts +164 -0
- package/src/daemon/handlers/email/index.ts +43 -0
- package/src/daemon/handlers/email/read-handlers.ts +80 -0
- package/src/daemon/handlers/email/runtime.ts +140 -0
- package/src/daemon/handlers/email/validation.ts +147 -0
- package/src/daemon/handlers/email/write-handlers.ts +133 -0
- package/src/daemon/handlers/errors.ts +18 -0
- package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
- package/src/daemon/handlers/inbox/index.ts +210 -0
- package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
- package/src/daemon/{channels → handlers}/inbox/poller.ts +4 -4
- package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
- package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +8 -10
- package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +2 -2
- package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +9 -11
- package/src/daemon/handlers/index.ts +107 -0
- package/src/daemon/handlers/register.ts +161 -0
- package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
- package/src/daemon/handlers/remote/backends/index.ts +40 -0
- package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
- package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
- package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
- package/src/daemon/handlers/remote/index.ts +119 -0
- package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
- package/src/daemon/handlers/remote/service.ts +191 -0
- package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
- package/src/daemon/handlers/routing/index.ts +261 -0
- package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
- package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
- package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
- package/src/daemon/handlers/triage/index.ts +57 -0
- package/src/daemon/handlers/triage/integration.ts +212 -0
- package/src/daemon/{triage → handlers/triage}/pipeline.ts +58 -70
- package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
- package/src/daemon/handlers/triage/tagger/discord.ts +186 -0
- package/src/daemon/handlers/triage/tagger/imap.ts +383 -0
- package/src/daemon/handlers/triage/tagger/index.ts +184 -0
- package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
- package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
- package/src/daemon/handlers/triage/types.ts +50 -0
- package/src/runtime/services.ts +48 -35
- package/src/version.ts +1 -1
- package/src/daemon/calendar/index.ts +0 -52
- package/src/daemon/calendar/register.ts +0 -527
- package/src/daemon/channels/drafts/index.ts +0 -22
- package/src/daemon/channels/drafts/register.ts +0 -449
- package/src/daemon/channels/inbox/index.ts +0 -58
- package/src/daemon/channels/inbox/register.ts +0 -247
- package/src/daemon/channels/routing/index.ts +0 -39
- package/src/daemon/channels/routing/register.ts +0 -296
- package/src/daemon/email/index.ts +0 -68
- package/src/daemon/email/register.ts +0 -715
- package/src/daemon/operator/index.ts +0 -43
- package/src/daemon/operator/register-helper.ts +0 -150
- package/src/daemon/operator/surfaces.ts +0 -137
- package/src/daemon/operator/types.ts +0 -207
- package/src/daemon/remote/backends/index.ts +0 -34
- package/src/daemon/remote/index.ts +0 -74
- package/src/daemon/remote/register.ts +0 -411
- package/src/daemon/triage/index.ts +0 -59
- package/src/daemon/triage/integration.ts +0 -179
- package/src/daemon/triage/register.ts +0 -231
- package/src/daemon/triage/tagger.ts +0 -777
- /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
- /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
- /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
- /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
- /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
- /package/src/daemon/{channels → handlers}/inbox/providers/email.ts +0 -0
- /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AtRestCipher } from '
|
|
3
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { AtRestCipher } from '../credentials.ts';
|
|
3
|
+
import { HandlerSqliteStore } from '../sqlite-store.ts';
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
|
-
// Draft Sync Backend store.
|
|
6
|
+
// Draft Sync Backend store (rebased onto the daemon handler foundation).
|
|
7
7
|
//
|
|
8
8
|
// Mirrors the agent's local draft store (channels/drafts.json) server-side so
|
|
9
9
|
// drafts are visible across surfaces. The agent's local file remains the source
|
|
@@ -13,15 +13,27 @@ import { redactWebhook, sha256First } from '../../operator/index.ts';
|
|
|
13
13
|
//
|
|
14
14
|
// SECURITY POSTURE:
|
|
15
15
|
// - The plaintext message body is NEVER persisted. It is encrypted at rest
|
|
16
|
-
// via the daemon at-rest cipher (AES-256-GCM)
|
|
16
|
+
// via the daemon at-rest cipher (AES-256-GCM) as `bodyEnc`.
|
|
17
17
|
// - The webhook URL is encrypted at rest (`webhookEnc`) and is ALWAYS
|
|
18
18
|
// redacted ('[redacted]') in every list/get response — the raw URL never
|
|
19
19
|
// leaves the store.
|
|
20
20
|
// - `messageDigest` (sha256First(body, 12)) is computed at save time and
|
|
21
|
-
// stored alongside, so reads never need to decrypt the body. The
|
|
22
|
-
//
|
|
21
|
+
// stored alongside, so reads never need to decrypt the body. The required
|
|
22
|
+
// `message` field of every emitted record carries the DIGEST, never the
|
|
23
|
+
// plaintext body.
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
|
|
26
|
+
/** SHA-256 of `input`, truncated to the first `hexChars` hex characters. */
|
|
27
|
+
export function sha256First(input: string, hexChars: number): string {
|
|
28
|
+
const digest = createHash('sha256').update(input, 'utf-8').digest('hex');
|
|
29
|
+
return digest.slice(0, Math.max(0, hexChars));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Redact a stored webhook for responses: present → '[redacted]', else undefined. */
|
|
33
|
+
export function redactWebhook(value?: string | null): string | undefined {
|
|
34
|
+
return value === undefined || value === null || value === '' ? undefined : '[redacted]';
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export type DraftStatus = 'draft' | 'queued' | 'sent' | 'failed';
|
|
26
38
|
|
|
27
39
|
/** Statuses callers are permitted to write via save(). */
|
|
@@ -36,21 +48,25 @@ export const ALL_DRAFT_STATUSES: readonly DraftStatus[] = [
|
|
|
36
48
|
];
|
|
37
49
|
|
|
38
50
|
export const DRAFT_MESSAGE_DIGEST_HEX = 12;
|
|
39
|
-
|
|
51
|
+
export const DEFAULT_DRAFT_VERSION = 1;
|
|
40
52
|
export const DEFAULT_DRAFT_LIST_LIMIT = 50;
|
|
41
53
|
export const MAX_DRAFT_LIST_LIMIT = 200;
|
|
42
54
|
|
|
43
55
|
/**
|
|
44
|
-
* Public draft record returned by list/get.
|
|
45
|
-
*
|
|
56
|
+
* Public draft record returned by list/get/save. Matches the SDK
|
|
57
|
+
* CHANNEL_DRAFT_SCHEMA: the required `message` field carries the message
|
|
58
|
+
* DIGEST (never the plaintext body). `webhook` is always '[redacted]' when set.
|
|
59
|
+
* `messageDigest` is emitted ONLY by `get` (whose output schema permits
|
|
60
|
+
* additional properties); strict list/save records omit it.
|
|
46
61
|
*/
|
|
47
62
|
export interface DraftRecord {
|
|
63
|
+
version: number;
|
|
48
64
|
id: string;
|
|
49
65
|
createdAt: string; // ISO-8601
|
|
50
66
|
updatedAt: string; // ISO-8601
|
|
51
67
|
status: DraftStatus;
|
|
68
|
+
message: string; // sha256First(body, 12) digest — plaintext body is NEVER transmitted
|
|
52
69
|
title?: string;
|
|
53
|
-
messageDigest: string; // sha256First(body, 12) — body is NOT transmitted
|
|
54
70
|
channel?: string;
|
|
55
71
|
route?: string;
|
|
56
72
|
webhook?: string; // '[redacted]' when a webhook is stored, else omitted
|
|
@@ -60,9 +76,10 @@ export interface DraftRecord {
|
|
|
60
76
|
sendError?: string;
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
/** Normalized input accepted by upsert(). */
|
|
79
|
+
/** Normalized input accepted by upsert(). `message` is the FULL plaintext body. */
|
|
64
80
|
export interface DraftSaveInput {
|
|
65
81
|
id?: string;
|
|
82
|
+
version?: number;
|
|
66
83
|
title?: string;
|
|
67
84
|
message: string;
|
|
68
85
|
channel?: string;
|
|
@@ -71,13 +88,11 @@ export interface DraftSaveInput {
|
|
|
71
88
|
link?: string;
|
|
72
89
|
tags?: string[];
|
|
73
90
|
status?: DraftStatus;
|
|
91
|
+
createdAt?: string;
|
|
74
92
|
/**
|
|
75
93
|
* Caller-supplied last-modified timestamp (ISO-8601). When provided it is
|
|
76
|
-
* persisted verbatim, enabling the sync contract's
|
|
77
|
-
* conflict
|
|
78
|
-
* an integrator can push the agent's authoritative updatedAt rather than
|
|
79
|
-
* having it overwritten by daemon-local time. When omitted, the store stamps
|
|
80
|
-
* updatedAt = now().
|
|
94
|
+
* persisted verbatim, enabling the sync contract's 'most recent updatedAt
|
|
95
|
+
* wins' conflict model; when omitted the store stamps updatedAt = now().
|
|
81
96
|
*/
|
|
82
97
|
updatedAt?: string;
|
|
83
98
|
}
|
|
@@ -88,7 +103,7 @@ export interface DraftListQuery {
|
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
export interface DraftSaveResult {
|
|
91
|
-
|
|
106
|
+
draft: DraftRecord;
|
|
92
107
|
created: boolean;
|
|
93
108
|
}
|
|
94
109
|
|
|
@@ -106,6 +121,7 @@ export interface DraftSyncStoreOptions {
|
|
|
106
121
|
const SCHEMA: string[] = [
|
|
107
122
|
`CREATE TABLE IF NOT EXISTS drafts (
|
|
108
123
|
id TEXT PRIMARY KEY,
|
|
124
|
+
version INTEGER NOT NULL,
|
|
109
125
|
createdAt TEXT NOT NULL,
|
|
110
126
|
updatedAt TEXT NOT NULL,
|
|
111
127
|
status TEXT NOT NULL,
|
|
@@ -126,6 +142,7 @@ const SCHEMA: string[] = [
|
|
|
126
142
|
|
|
127
143
|
interface DraftRow {
|
|
128
144
|
id: string;
|
|
145
|
+
version: number;
|
|
129
146
|
createdAt: string;
|
|
130
147
|
updatedAt: string;
|
|
131
148
|
status: string;
|
|
@@ -162,19 +179,25 @@ function normalizeStatus(status: DraftStatus | undefined): DraftStatus {
|
|
|
162
179
|
return status ?? 'draft';
|
|
163
180
|
}
|
|
164
181
|
|
|
182
|
+
function normalizeVersion(version: number | undefined): number {
|
|
183
|
+
if (version === undefined || !Number.isFinite(version)) return DEFAULT_DRAFT_VERSION;
|
|
184
|
+
const floored = Math.floor(version);
|
|
185
|
+
return floored < 1 ? DEFAULT_DRAFT_VERSION : floored;
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
/**
|
|
166
|
-
* Server-side mirror of agent drafts. Wraps
|
|
189
|
+
* Server-side mirror of agent drafts. Wraps HandlerSqliteStore; all body and
|
|
167
190
|
* webhook material is encrypted at rest via the injected AtRestCipher.
|
|
168
191
|
*/
|
|
169
192
|
export class DraftSyncStore {
|
|
170
|
-
private readonly store:
|
|
193
|
+
private readonly store: HandlerSqliteStore;
|
|
171
194
|
private readonly cipher: AtRestCipher;
|
|
172
195
|
private readonly generateId: () => string;
|
|
173
196
|
private readonly now: () => string;
|
|
174
197
|
private initialized = false;
|
|
175
198
|
|
|
176
199
|
constructor(options: DraftSyncStoreOptions) {
|
|
177
|
-
this.store = new
|
|
200
|
+
this.store = new HandlerSqliteStore({
|
|
178
201
|
workingDirectory: options.workingDirectory,
|
|
179
202
|
fileName: options.fileName ?? 'drafts.sqlite',
|
|
180
203
|
schema: SCHEMA,
|
|
@@ -206,34 +229,26 @@ export class DraftSyncStore {
|
|
|
206
229
|
|
|
207
230
|
/**
|
|
208
231
|
* Create (no id) or update (id present) a draft. Encrypts the body and
|
|
209
|
-
* webhook at rest, computes messageDigest,
|
|
210
|
-
*
|
|
211
|
-
* updatedAt wins'
|
|
212
|
-
* preserved from the existing row.
|
|
232
|
+
* webhook at rest, computes messageDigest, preserves createdAt on update, and
|
|
233
|
+
* sets updatedAt to input.updatedAt when present (enabling 'most recent
|
|
234
|
+
* updatedAt wins') or now() otherwise.
|
|
213
235
|
*
|
|
214
236
|
* FULL-REPLACE SEMANTICS (intentional, per the full-snapshot sync contract):
|
|
215
|
-
* every caller-supplied field is
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* the same applies to title/channel/route/link. Callers performing a partial
|
|
221
|
-
* edit MUST re-supply every field they want to keep. The ONLY exceptions are
|
|
222
|
-
* sentResponseId/sendError, which are owned by the send pipeline and are
|
|
223
|
-
* deliberately omitted from the UPDATE SET clause so they survive a save().
|
|
224
|
-
*
|
|
225
|
-
* NOTE: caller is responsible for store.save() after a batch of mutations,
|
|
226
|
-
* or this can be invoked through DraftSyncStore.save(). The register layer
|
|
227
|
-
* saves after every mutation.
|
|
237
|
+
* every caller-supplied field is a complete snapshot, NOT a partial patch.
|
|
238
|
+
* The ON CONFLICT UPDATE SET clause assigns each column from the incoming row,
|
|
239
|
+
* so an OMITTED optional field is CLEARED on update (webhookEnc/tags/title/
|
|
240
|
+
* channel/route/link → NULL). The ONLY exceptions are sentResponseId/
|
|
241
|
+
* sendError, which are owned by the send pipeline and survive a save().
|
|
228
242
|
*/
|
|
229
243
|
async upsert(input: DraftSaveInput): Promise<DraftSaveResult> {
|
|
230
244
|
const existing = input.id !== undefined ? this.getRow(input.id) : null;
|
|
231
245
|
const created = existing === null;
|
|
232
246
|
const id = existing?.id ?? input.id ?? this.generateId();
|
|
233
247
|
const timestamp = this.now();
|
|
234
|
-
const createdAt = existing?.createdAt ?? timestamp;
|
|
248
|
+
const createdAt = existing?.createdAt ?? input.createdAt ?? timestamp;
|
|
235
249
|
const updatedAt = input.updatedAt ?? timestamp;
|
|
236
250
|
const status = normalizeStatus(input.status);
|
|
251
|
+
const version = normalizeVersion(input.version);
|
|
237
252
|
|
|
238
253
|
const bodyEnc = await this.cipher.encrypt(input.message);
|
|
239
254
|
const messageDigest = sha256First(input.message, DRAFT_MESSAGE_DIGEST_HEX);
|
|
@@ -248,10 +263,11 @@ export class DraftSyncStore {
|
|
|
248
263
|
|
|
249
264
|
this.store.run(
|
|
250
265
|
`INSERT INTO drafts (
|
|
251
|
-
id, createdAt, updatedAt, status, title, bodyEnc, messageDigest,
|
|
266
|
+
id, version, createdAt, updatedAt, status, title, bodyEnc, messageDigest,
|
|
252
267
|
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
253
|
-
) VALUES (
|
|
268
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
254
269
|
ON CONFLICT(id) DO UPDATE SET
|
|
270
|
+
version = excluded.version,
|
|
255
271
|
updatedAt = excluded.updatedAt,
|
|
256
272
|
status = excluded.status,
|
|
257
273
|
title = excluded.title,
|
|
@@ -264,6 +280,7 @@ export class DraftSyncStore {
|
|
|
264
280
|
tags = excluded.tags`,
|
|
265
281
|
[
|
|
266
282
|
id,
|
|
283
|
+
version,
|
|
267
284
|
createdAt,
|
|
268
285
|
updatedAt,
|
|
269
286
|
status,
|
|
@@ -276,14 +293,14 @@ export class DraftSyncStore {
|
|
|
276
293
|
nullable(input.link),
|
|
277
294
|
tagsJson,
|
|
278
295
|
// sentResponseId / sendError are owned by the send pipeline, not save().
|
|
279
|
-
// On update they are preserved (omitted from
|
|
280
|
-
// insert they default to NULL.
|
|
296
|
+
// On update they are preserved (omitted from UPDATE SET); on insert NULL.
|
|
281
297
|
null,
|
|
282
298
|
null,
|
|
283
299
|
],
|
|
284
300
|
);
|
|
285
301
|
|
|
286
|
-
|
|
302
|
+
const row = this.getRow(id);
|
|
303
|
+
return { draft: toRecord(row ?? this.fallbackRow(id, version, createdAt, updatedAt, status, messageDigest, webhookEnc, input)), created };
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
/** List drafts, newest-updated first, optionally filtered by status. */
|
|
@@ -292,13 +309,13 @@ export class DraftSyncStore {
|
|
|
292
309
|
const rows =
|
|
293
310
|
query.status !== undefined
|
|
294
311
|
? this.store.all<DraftRow>(
|
|
295
|
-
`SELECT id, createdAt, updatedAt, status, title, messageDigest,
|
|
312
|
+
`SELECT id, version, createdAt, updatedAt, status, title, messageDigest,
|
|
296
313
|
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
297
314
|
FROM drafts WHERE status = ? ORDER BY updatedAt DESC, id ASC LIMIT ?`,
|
|
298
315
|
[query.status, limit],
|
|
299
316
|
)
|
|
300
317
|
: this.store.all<DraftRow>(
|
|
301
|
-
`SELECT id, createdAt, updatedAt, status, title, messageDigest,
|
|
318
|
+
`SELECT id, version, createdAt, updatedAt, status, title, messageDigest,
|
|
302
319
|
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
303
320
|
FROM drafts ORDER BY updatedAt DESC, id ASC LIMIT ?`,
|
|
304
321
|
[limit],
|
|
@@ -306,6 +323,15 @@ export class DraftSyncStore {
|
|
|
306
323
|
return rows.map((row) => toRecord(row));
|
|
307
324
|
}
|
|
308
325
|
|
|
326
|
+
/** Total number of stored drafts (optionally status-filtered). */
|
|
327
|
+
count(status?: DraftStatus): number {
|
|
328
|
+
const row =
|
|
329
|
+
status !== undefined
|
|
330
|
+
? this.store.get<{ n: number }>('SELECT COUNT(*) AS n FROM drafts WHERE status = ?', [status])
|
|
331
|
+
: this.store.get<{ n: number }>('SELECT COUNT(*) AS n FROM drafts');
|
|
332
|
+
return row?.n ?? 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
309
335
|
/** Fetch a single draft as a redacted record, or null when absent. */
|
|
310
336
|
get(id: string): DraftRecord | null {
|
|
311
337
|
const row = this.getRow(id);
|
|
@@ -323,12 +349,45 @@ export class DraftSyncStore {
|
|
|
323
349
|
|
|
324
350
|
private getRow(id: string): DraftRow | null {
|
|
325
351
|
return this.store.get<DraftRow>(
|
|
326
|
-
`SELECT id, createdAt, updatedAt, status, title, messageDigest,
|
|
352
|
+
`SELECT id, version, createdAt, updatedAt, status, title, messageDigest,
|
|
327
353
|
channel, route, webhookEnc, link, tags, sentResponseId, sendError
|
|
328
354
|
FROM drafts WHERE id = ?`,
|
|
329
355
|
[id],
|
|
330
356
|
);
|
|
331
357
|
}
|
|
358
|
+
|
|
359
|
+
// Defensive synthesis if a read-back ever returns null (should not happen for
|
|
360
|
+
// the row we just wrote); keeps upsert total even under a degraded backend.
|
|
361
|
+
private fallbackRow(
|
|
362
|
+
id: string,
|
|
363
|
+
version: number,
|
|
364
|
+
createdAt: string,
|
|
365
|
+
updatedAt: string,
|
|
366
|
+
status: DraftStatus,
|
|
367
|
+
messageDigest: string,
|
|
368
|
+
webhookEnc: string | null,
|
|
369
|
+
input: DraftSaveInput,
|
|
370
|
+
): DraftRow {
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
version,
|
|
374
|
+
createdAt,
|
|
375
|
+
updatedAt,
|
|
376
|
+
status,
|
|
377
|
+
title: nullable(input.title),
|
|
378
|
+
messageDigest,
|
|
379
|
+
channel: nullable(input.channel),
|
|
380
|
+
route: nullable(input.route),
|
|
381
|
+
webhookEnc,
|
|
382
|
+
link: nullable(input.link),
|
|
383
|
+
tags:
|
|
384
|
+
input.tags !== undefined && input.tags.length > 0
|
|
385
|
+
? JSON.stringify(input.tags)
|
|
386
|
+
: null,
|
|
387
|
+
sentResponseId: null,
|
|
388
|
+
sendError: null,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
332
391
|
}
|
|
333
392
|
|
|
334
393
|
function clampLimit(limit: number | undefined): number {
|
|
@@ -339,20 +398,25 @@ function clampLimit(limit: number | undefined): number {
|
|
|
339
398
|
return floored;
|
|
340
399
|
}
|
|
341
400
|
|
|
342
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Map a stored row to a wire record. The required `message` field carries the
|
|
403
|
+
* DIGEST (never plaintext); the webhook is redacted; tags are parsed.
|
|
404
|
+
*/
|
|
405
|
+
export function toRecord(row: DraftRow): DraftRecord {
|
|
343
406
|
const record: DraftRecord = {
|
|
407
|
+
version: row.version,
|
|
344
408
|
id: row.id,
|
|
345
409
|
createdAt: row.createdAt,
|
|
346
410
|
updatedAt: row.updatedAt,
|
|
347
411
|
status: row.status as DraftStatus,
|
|
348
|
-
|
|
412
|
+
message: row.messageDigest,
|
|
349
413
|
};
|
|
350
414
|
if (row.title !== null) record.title = row.title;
|
|
351
415
|
if (row.channel !== null) record.channel = row.channel;
|
|
352
416
|
if (row.route !== null) record.route = row.route;
|
|
353
417
|
// Webhook is encrypted at rest; reads ALWAYS redact. Presence of webhookEnc
|
|
354
418
|
// means a webhook exists → emit '[redacted]'; the raw URL never leaves here.
|
|
355
|
-
const redacted = redactWebhook(row.webhookEnc
|
|
419
|
+
const redacted = redactWebhook(row.webhookEnc);
|
|
356
420
|
if (redacted !== undefined) record.webhook = redacted;
|
|
357
421
|
if (row.link !== null) record.link = row.link;
|
|
358
422
|
const tags = parseTags(row.tags);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channels.drafts.* handler surface barrel.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports only the concrete register entrypoint and the store/types other
|
|
5
|
+
* code may need. It imports concrete submodules (no project index barrels), so
|
|
6
|
+
* it introduces no import cycle.
|
|
7
|
+
*/
|
|
8
|
+
export { registerDraftMethods } from './register.ts';
|
|
9
|
+
export type { RegisterDraftsOptions } from './register.ts';
|
|
10
|
+
export { DraftSyncStore, sha256First, redactWebhook } from './draft-store.ts';
|
|
11
|
+
export type {
|
|
12
|
+
DraftListQuery,
|
|
13
|
+
DraftRecord,
|
|
14
|
+
DraftSaveInput,
|
|
15
|
+
DraftSaveResult,
|
|
16
|
+
DraftStatus,
|
|
17
|
+
} from './draft-store.ts';
|