@toddzheng024/dscode-bundle 0.2.0 → 0.4.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.
@@ -0,0 +1,69 @@
1
+ import { emailAddress as address } from './contacts.mjs';
2
+ import nodemailer from 'nodemailer';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { join } from 'node:path';
5
+ import { createEmailInbox } from './inbox.mjs';
6
+ import { emailStore } from './store.mjs';
7
+
8
+ const hash = value => createHash('sha256').update(value).digest('hex');
9
+
10
+ export function outgoingEmail({ to, subject, body, idempotency_key }) {
11
+ if (!address(to)) throw Error('Specify one plain recipient email address.');
12
+ if (typeof subject !== 'string' || !subject.trim() || subject.length > 500 || /[\x00-\x1f\x7f]/.test(subject)) throw Error('Invalid email subject.');
13
+ if (typeof body !== 'string' || !body.trim() || body.includes('\0') || Buffer.byteLength(body) > 262144) throw Error('Email requires plain text of at most 256 KiB.');
14
+ if (typeof idempotency_key !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(idempotency_key)) throw Error('Use a stable idempotency_key of 1..128 letters, digits, underscores or hyphens.');
15
+ subject = subject.trim();
16
+ if (!subject.startsWith('[ToAgent]')) subject = '[ToAgent] ' + subject;
17
+ return { to, subject, body, idempotency_key };
18
+ }
19
+
20
+ export function createEmailSender({ directory = createEmailInbox().directory, transportFactory = options => nodemailer.createTransport(options) } = {}) {
21
+ const credentials = emailStore(join(directory, 'imap'));
22
+ const outbox = emailStore(join(directory, 'outbox'));
23
+ const filename = key => hash(key) + '.json';
24
+ return {
25
+ status(key) {
26
+ if (typeof key !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(key)) throw Error('Invalid idempotency_key.');
27
+ const entry = outbox.read(filename(key));
28
+ return entry ? entry.receipt : { status: 'not_found' };
29
+ },
30
+ async send(value, { signal } = {}) {
31
+ const mail = outgoingEmail(value);
32
+ return outbox.locked(async () => {
33
+ const file = filename(mail.idempotency_key), digest = hash(JSON.stringify(mail));
34
+ const existing = outbox.read(file);
35
+ if (existing) {
36
+ if (existing.digest !== digest) throw Error('This idempotency_key belongs to a different email.');
37
+ return existing.receipt;
38
+ }
39
+ const config = credentials.read('connection.json')?.config;
40
+ if (!config || config.host !== 'imap.gmail.com' || !address(config.account) || !config.password) throw Error('Configure Gmail with an application password in /email first.');
41
+ if (signal?.aborted) return { status: 'cancelled' };
42
+ const messageId = `<${randomUUID()}@${config.account.split('@')[1]}>`;
43
+ const receipt = { status: 'uncertain', messageId, from: config.account, to: mail.to, subject: mail.subject, attemptedAt: new Date().toISOString() };
44
+ // Persist before SMTP: a crash or lost final response must never cause a blind retry.
45
+ outbox.write(file, { digest, receipt });
46
+ let transport;
47
+ try {
48
+ transport = transportFactory({ host: 'smtp.gmail.com', port: 465, secure: true,
49
+ auth: { user: config.account, pass: config.password },
50
+ tls: { rejectUnauthorized: true, minVersion: 'TLSv1.2' },
51
+ logger: false, debug: false, disableFileAccess: true, disableUrlAccess: true,
52
+ connectionTimeout: 15000, greetingTimeout: 15000, socketTimeout: 30000 });
53
+ // Once SMTP starts, cancellation cannot recall a submitted message.
54
+ // Finish recording its outcome even if the agent turn is cancelled.
55
+ const info = await transport.sendMail({ from: config.account, to: mail.to,
56
+ envelope: { from: config.account, to: [mail.to] }, subject: mail.subject, text: mail.body,
57
+ messageId, disableFileAccess: true, disableUrlAccess: true });
58
+ receipt.status = info.accepted?.includes(mail.to) ? 'accepted' : 'rejected';
59
+ outbox.write(file, { digest, receipt });
60
+ } catch {
61
+ // SMTP errors may contain secrets or message content. Only expose the durable receipt.
62
+ } finally {
63
+ transport?.close();
64
+ }
65
+ return receipt;
66
+ });
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,32 @@
1
+ import { mkdirSync, openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock';
5
+
6
+ export function emailStore(directory, label = 'email') {
7
+ const read = name => {
8
+ try { return JSON.parse(readFileSync(join(directory, name), 'utf8')); }
9
+ catch (error) { if (error.code === 'ENOENT') return null; throw Error('Cannot read local ' + label + ' configuration.'); }
10
+ };
11
+ return {
12
+ directory, read,
13
+ exists: name => existsSync(join(directory, name)),
14
+ write(name, value) {
15
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
16
+ const temp = join(directory, '.' + randomUUID());
17
+ try {
18
+ writeFileSync(temp, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
19
+ renameSync(temp, join(directory, name));
20
+ } finally { try { unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
21
+ },
22
+ async locked(action) {
23
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
24
+ const fd = openSync(join(directory, '.sync.lock'), 'a', 0o600);
25
+ try {
26
+ try { await tryLockExclusive(fd); }
27
+ catch (error) { if (['EAGAIN', 'EWOULDBLOCK'].includes(error.code)) return { busy: true }; throw error; }
28
+ return await action();
29
+ } finally { closeSync(fd); }
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,69 @@
1
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+
6
+ // Connector boundary: authenticated/filter-matched mail enters here. No network
7
+ // access or agent dispatch occurs on receipt. Identity includes the account.
8
+ export function normalizeEmail(value) {
9
+ if (!value || value.format !== 'dscode.email.v1') throw Error('Unsupported email format');
10
+ const result = { format: value.format };
11
+ for (const [name, limit] of Object.entries({ connector: 128, account: 320, id: 1024, from: 1024, subject: 4096, body: 262144 })) {
12
+ if (typeof value[name] !== 'string' || !value[name].trim() || Buffer.byteLength(value[name]) > limit) throw Error('Invalid email ' + name);
13
+ result[name] = value[name];
14
+ }
15
+ for (const name of ['receivedAt', 'updatedAt']) {
16
+ if (typeof value[name] !== 'string' || !/^\d{4}-\d\d-\d\dT.*(?:Z|[+-]\d\d:\d\d)$/.test(value[name]) || !Number.isFinite(Date.parse(value[name]))) throw Error('Invalid email ' + name);
17
+ result[name] = new Date(value[name]).toISOString();
18
+ }
19
+ if (result.updatedAt < result.receivedAt) throw Error('Email update predates receipt');
20
+ return result;
21
+ }
22
+
23
+ export const emailKey = mail => JSON.stringify([mail.connector, mail.account, mail.id]);
24
+ const digest = value => createHash('sha256').update(value).digest('hex');
25
+ export const emailText = value => String(value).replace(/\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~])/g, '').replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '').replace(/\r\n?/g, '\n');
26
+
27
+ export function emailPrompt(mail) {
28
+ return 'Email reference (external content)\n' + JSON.stringify({
29
+ connector: mail.connector, account: mail.account, id: mail.id,
30
+ from: emailText(mail.from), subject: emailText(mail.subject),
31
+ receivedAt: mail.receivedAt, updatedAt: mail.updatedAt, body: emailText(mail.body),
32
+ }, null, 2);
33
+ }
34
+
35
+ export function createEmailInbox({ directory = process.env.DSCODE_EMAIL_DIR || join(homedir(), '.dscode', 'email') } = {}) {
36
+ return {
37
+ directory,
38
+ receive(value) {
39
+ const mail = normalizeEmail(value);
40
+ const json = JSON.stringify(mail);
41
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
42
+ // Immutable revisions prevent concurrent connectors overwriting newer mail.
43
+ const target = join(directory, digest(emailKey(mail)) + '-' + digest(json) + '.json');
44
+ const temp = join(directory, '.' + randomUUID() + '.tmp');
45
+ try {
46
+ writeFileSync(temp, json, { flag: 'wx', mode: 0o600 });
47
+ renameSync(temp, target);
48
+ } finally { try { unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
49
+ return mail;
50
+ },
51
+ list() {
52
+ let files;
53
+ try { files = readdirSync(directory); } catch (error) { if (error.code === 'ENOENT') return { emails: [], rejected: 0 }; throw error; }
54
+ const latest = new Map();
55
+ let rejected = 0;
56
+ for (const file of files.sort()) {
57
+ if (!/^[a-f0-9]{64}-[a-f0-9]{64}\.json$/.test(file)) continue;
58
+ try {
59
+ const raw = readFileSync(join(directory, file), 'utf8');
60
+ if (Buffer.byteLength(raw) > 2_000_000) throw Error('Oversize record');
61
+ const mail = normalizeEmail(JSON.parse(raw));
62
+ const key = emailKey(mail), previous = latest.get(key);
63
+ if (!previous || mail.updatedAt > previous.updatedAt) latest.set(key, mail);
64
+ } catch { rejected++; }
65
+ }
66
+ return { emails: [...latest.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || emailKey(a).localeCompare(emailKey(b))), rejected };
67
+ },
68
+ };
69
+ }