@pellux/goodvibes-tui 1.24.2 → 1.25.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/bin/goodvibes-daemon +24 -0
  4. package/docs/foundation-artifacts/operator-contract.json +3237 -1096
  5. package/package.json +7 -7
  6. package/src/config/goodvibes-home.ts +139 -0
  7. package/src/daemon/cli.ts +17 -6
  8. package/src/daemon/handlers/index.ts +7 -6
  9. package/src/input/commands/calendar-runtime.ts +245 -0
  10. package/src/input/commands/connection-status.ts +258 -0
  11. package/src/input/commands/mail-runtime.ts +267 -0
  12. package/src/input/commands/review-runtime.ts +13 -1
  13. package/src/input/commands.ts +4 -0
  14. package/src/input/settings-modal-connections.ts +102 -0
  15. package/src/input/settings-modal-types.ts +2 -1
  16. package/src/input/settings-modal.ts +27 -0
  17. package/src/main.ts +7 -2
  18. package/src/renderer/settings-modal-connections.ts +63 -0
  19. package/src/renderer/settings-modal-helpers.ts +2 -0
  20. package/src/renderer/settings-modal.ts +5 -0
  21. package/src/runtime/bootstrap-core.ts +1 -1
  22. package/src/runtime/context.ts +9 -0
  23. package/src/runtime/credential-composition.ts +54 -0
  24. package/src/runtime/daemon-handler-composition.ts +0 -4
  25. package/src/runtime/mail-composition.ts +65 -0
  26. package/src/runtime/services.ts +21 -24
  27. package/src/shell/ui-openers.ts +7 -0
  28. package/src/verification/verification-ledger.ts +58 -0
  29. package/src/version.ts +1 -1
  30. package/src/daemon/handlers/calendar/caldav-client.ts +0 -662
  31. package/src/daemon/handlers/calendar/ics.ts +0 -556
  32. package/src/daemon/handlers/calendar/index.ts +0 -316
  33. package/src/daemon/handlers/email/config.ts +0 -164
  34. package/src/daemon/handlers/email/imap-connector.ts +0 -441
  35. package/src/daemon/handlers/email/imap-parsing.ts +0 -499
  36. package/src/daemon/handlers/email/index.ts +0 -43
  37. package/src/daemon/handlers/email/read-handlers.ts +0 -80
  38. package/src/daemon/handlers/email/runtime.ts +0 -140
  39. package/src/daemon/handlers/email/smtp-connector.ts +0 -557
  40. package/src/daemon/handlers/email/validation.ts +0 -147
  41. package/src/daemon/handlers/email/write-handlers.ts +0 -133
@@ -1,147 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Input validation, PII-safe digesting, and response shaping for the email
3
- // handler surface. Pure functions only (no I/O, no secrets).
4
- // ---------------------------------------------------------------------------
5
-
6
- import { createHash } from 'node:crypto';
7
- import { HandlerError } from '../errors.ts';
8
- import type { ImapEnvelopeSummary, ImapFullMessage } from './imap-connector.ts';
9
-
10
- export function asRecord(body: unknown): Record<string, unknown> {
11
- if (typeof body !== 'object' || body === null) {
12
- throw new HandlerError('Request body must be an object', 'EMAIL_BAD_INPUT', 400);
13
- }
14
- return body as Record<string, unknown>;
15
- }
16
-
17
- export function requireString(value: unknown, field: string): string {
18
- if (typeof value !== 'string' || value.trim().length === 0) {
19
- throw new HandlerError(`Field '${field}' is required`, 'EMAIL_BAD_INPUT', 400);
20
- }
21
- return value;
22
- }
23
-
24
- export function optionalString(value: unknown, field: string): string | undefined {
25
- if (value === undefined || value === null) return undefined;
26
- if (typeof value !== 'string') {
27
- throw new HandlerError(`Field '${field}' must be a string`, 'EMAIL_BAD_INPUT', 400);
28
- }
29
- return value;
30
- }
31
-
32
- /**
33
- * Extract the addr-spec from an RFC 5322 name-addr ("Display <a@b>"). Reads the
34
- * contents of the LAST <...> pair so a stray '<' inside a quoted display name
35
- * does not corrupt the result.
36
- */
37
- export function extractAddrSpec(entry: string): string {
38
- const open = entry.lastIndexOf('<');
39
- if (open !== -1) {
40
- const close = entry.indexOf('>', open + 1);
41
- if (close !== -1) return entry.slice(open + 1, close).trim();
42
- }
43
- return entry.trim();
44
- }
45
-
46
- export function validateEmailAddress(value: string, field: string): string {
47
- const entries = value.split(',').map((s) => s.trim()).filter(Boolean);
48
- if (entries.length === 0 || entries.some((e) => !/.+@.+\..+/.test(extractAddrSpec(e)))) {
49
- throw new HandlerError(`Field '${field}' must be a valid email address`, 'EMAIL_BAD_INPUT', 400);
50
- }
51
- return value;
52
- }
53
-
54
- export function clampLimit(value: unknown): number {
55
- if (value === undefined || value === null) return 10;
56
- const n = typeof value === 'number' ? value : Number(value);
57
- if (!Number.isFinite(n)) {
58
- throw new HandlerError("Field 'limit' must be a number", 'EMAIL_BAD_INPUT', 400);
59
- }
60
- return Math.min(100, Math.max(1, Math.floor(n)));
61
- }
62
-
63
- export function validateIsoDate(value: unknown): string | undefined {
64
- const str = optionalString(value, 'since');
65
- if (str === undefined) return undefined;
66
- if (Number.isNaN(new Date(str).getTime())) {
67
- throw new HandlerError("Field 'since' must be an ISO-8601 date", 'EMAIL_BAD_INPUT', 400);
68
- }
69
- return str;
70
- }
71
-
72
- export function requireUid(value: unknown): number {
73
- const n = typeof value === 'number' ? value : Number(value);
74
- if (!Number.isInteger(n) || n <= 0) {
75
- throw new HandlerError("Field 'uid' must be a positive integer", 'EMAIL_BAD_INPUT', 400);
76
- }
77
- return n;
78
- }
79
-
80
- /** Reduce an address to a stable, non-reversible digest for PII-safe logging. */
81
- export function addressDigest(address: string): string {
82
- return createHash('sha256').update(address.toLowerCase().trim(), 'utf-8').digest('hex').slice(0, 16);
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // Response contracts (must match EMAIL_INBOX_MESSAGE_SCHEMA /
87
- // EMAIL_MESSAGE_DETAIL_SCHEMA / {uid,draftId} / {messageId,sentAt}).
88
- // ---------------------------------------------------------------------------
89
-
90
- export interface InboxListResponse {
91
- messages: Array<{
92
- uid: number;
93
- from: string;
94
- subject: string;
95
- date: string;
96
- unread: boolean;
97
- bodyPreview: string;
98
- messageId: string;
99
- }>;
100
- total: number;
101
- }
102
-
103
- export interface InboxReadResponse {
104
- uid: number;
105
- from: string;
106
- subject: string;
107
- date: string;
108
- messageId: string;
109
- bodyText: string;
110
- bodyHtml?: string;
111
- attachments?: Array<{ filename: string; contentType: string; sizeBytes: number }>;
112
- }
113
-
114
- export interface DraftCreateResponse {
115
- uid: number;
116
- draftId: string;
117
- }
118
-
119
- export interface SendResponse {
120
- messageId: string;
121
- sentAt: string;
122
- }
123
-
124
- export function toListMessage(m: ImapEnvelopeSummary): InboxListResponse['messages'][number] {
125
- return {
126
- uid: m.uid,
127
- from: m.from,
128
- subject: m.subject,
129
- date: m.date,
130
- unread: m.unread,
131
- bodyPreview: m.bodyPreview,
132
- messageId: m.messageId,
133
- };
134
- }
135
-
136
- export function toReadMessage(m: ImapFullMessage): InboxReadResponse {
137
- return {
138
- uid: m.uid,
139
- from: m.from,
140
- subject: m.subject,
141
- date: m.date,
142
- messageId: m.messageId,
143
- bodyText: m.bodyText,
144
- ...(m.bodyHtml ? { bodyHtml: m.bodyHtml } : {}),
145
- ...(m.attachments && m.attachments.length > 0 ? { attachments: m.attachments } : {}),
146
- };
147
- }
@@ -1,133 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Write handlers: email.draft.create + email.send.
3
- //
4
- // email.draft.create is access:admin (no body.confirm in the SDK contract);
5
- // email.send is dangerous:true and confirmation-gated — index.ts registers it
6
- // with { confirm: true }, so the register wrapper enforces body.confirm===true
7
- // AND explicitUserRequest before this handler runs.
8
- //
9
- // Recipients are validated, sender/recipient addresses are reduced to a digest
10
- // before logging (PII strip), and draft bodies are encrypted at rest by the
11
- // runtime before persistence. No secret or raw address is echoed into a
12
- // response. No descriptor or schema is authored here.
13
- // ---------------------------------------------------------------------------
14
-
15
- import type { CatalogHandlerEntry, TypedHandler } from '../register.ts';
16
- import type { EmailRuntime } from './runtime.ts';
17
- import { buildRfc5322Message, generateMessageId } from './smtp-connector.ts';
18
- import {
19
- asRecord,
20
- requireString,
21
- optionalString,
22
- validateEmailAddress,
23
- addressDigest,
24
- type DraftCreateResponse,
25
- type SendResponse,
26
- } from './validation.ts';
27
-
28
- interface DraftCreateBody {
29
- to?: unknown;
30
- subject?: unknown;
31
- body?: unknown;
32
- inReplyTo?: unknown;
33
- references?: unknown;
34
- }
35
-
36
- interface SendBody {
37
- to?: unknown;
38
- subject?: unknown;
39
- body?: unknown;
40
- inReplyTo?: unknown;
41
- confirm?: unknown;
42
- }
43
-
44
- function draftCreateHandler(runtime: EmailRuntime): TypedHandler<DraftCreateBody, DraftCreateResponse> {
45
- return async ({ body }) => {
46
- const input = asRecord(body);
47
- const to = validateEmailAddress(requireString(input.to, 'to'), 'to');
48
- const subject = requireString(input.subject, 'subject');
49
- const draftBody = requireString(input.body, 'body');
50
- const inReplyTo = optionalString(input.inReplyTo, 'inReplyTo');
51
- const references = optionalString(input.references, 'references');
52
-
53
- const from = await runtime.smtpFrom();
54
- const messageId = generateMessageId(from);
55
- const raw = buildRfc5322Message({
56
- from,
57
- to,
58
- subject,
59
- body: draftBody,
60
- messageId,
61
- date: new Date(),
62
- ...(inReplyTo ? { inReplyTo } : {}),
63
- ...(references ? { references } : {}),
64
- });
65
- const appended = await runtime.withImap((imap) => imap.appendDraft(raw));
66
-
67
- const now = new Date().toISOString();
68
- const draftId = messageId.replace(/[<>]/g, '');
69
- await runtime.persistDraft({
70
- id: draftId,
71
- to,
72
- subject,
73
- plaintextBody: draftBody,
74
- status: 'draft',
75
- createdAt: now,
76
- updatedAt: now,
77
- metadata: { uid: appended.uid, mailbox: appended.mailbox, messageId },
78
- });
79
-
80
- runtime.logger.info('email.draft.create', {
81
- uid: appended.uid,
82
- draftId,
83
- recipient: addressDigest(to),
84
- });
85
- return { uid: appended.uid, draftId };
86
- };
87
- }
88
-
89
- function sendHandler(runtime: EmailRuntime): TypedHandler<SendBody, SendResponse> {
90
- return async ({ body }) => {
91
- const input = asRecord(body);
92
- const to = validateEmailAddress(requireString(input.to, 'to'), 'to');
93
- const subject = requireString(input.subject, 'subject');
94
- const sendBody = requireString(input.body, 'body');
95
- const inReplyTo = optionalString(input.inReplyTo, 'inReplyTo');
96
-
97
- const smtp = await runtime.openSmtp();
98
- let result: SendResponse;
99
- try {
100
- result = await smtp.send({
101
- to,
102
- subject,
103
- body: sendBody,
104
- ...(inReplyTo ? { inReplyTo } : {}),
105
- });
106
- } finally {
107
- await smtp.close();
108
- }
109
- runtime.logger.info('email.send', {
110
- messageId: result.messageId,
111
- sentAt: result.sentAt,
112
- recipient: addressDigest(to),
113
- });
114
- return result;
115
- };
116
- }
117
-
118
- /** Build the write-handler catalog entries bound to the shared runtime. */
119
- export function buildWriteHandlerEntries(runtime: EmailRuntime): CatalogHandlerEntry[] {
120
- return [
121
- {
122
- id: 'email.draft.create',
123
- handler: draftCreateHandler(runtime) as TypedHandler<unknown, unknown>,
124
- },
125
- {
126
- id: 'email.send',
127
- handler: sendHandler(runtime) as TypedHandler<unknown, unknown>,
128
- // dangerous:true SDK method — require explicit confirmation (body.confirm
129
- // === true AND explicitUserRequest) via the register wrapper.
130
- options: { confirm: true },
131
- },
132
- ];
133
- }