@wtfalch/email 0.6.1 → 0.7.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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PostmasterAudit } from './audit.js';
|
|
1
2
|
import type { Instance } from './instance.js';
|
|
2
3
|
export interface Applied {
|
|
3
4
|
/** For the audit row's `targetType`. */
|
|
@@ -21,19 +22,19 @@ export declare function createMailbox(instance: Instance, input: {
|
|
|
21
22
|
apex: string;
|
|
22
23
|
description?: string | null;
|
|
23
24
|
external: boolean;
|
|
24
|
-
}): Promise<Applied>;
|
|
25
|
+
}, audit?: PostmasterAudit): Promise<Applied>;
|
|
25
26
|
export declare function addAlias(instance: Instance, input: {
|
|
26
27
|
accountId: string;
|
|
27
28
|
name: string;
|
|
28
29
|
domainId: string;
|
|
29
30
|
apex: string;
|
|
30
|
-
}): Promise<Applied>;
|
|
31
|
+
}, audit?: PostmasterAudit): Promise<Applied>;
|
|
31
32
|
export declare function removeAlias(instance: Instance, input: {
|
|
32
33
|
accountId: string;
|
|
33
34
|
name: string;
|
|
34
35
|
domainId: string;
|
|
35
36
|
apex: string;
|
|
36
|
-
}): Promise<Applied>;
|
|
37
|
+
}, audit?: PostmasterAudit): Promise<Applied>;
|
|
37
38
|
/**
|
|
38
39
|
* Stop one app password working. The secret is never read, here or anywhere
|
|
39
40
|
* else: the entry is identified by its id and the surviving entries go back
|
|
@@ -58,4 +59,4 @@ export declare function revokeAppPassword(instance: Instance, input: {
|
|
|
58
59
|
credentialId: string;
|
|
59
60
|
domainId: string;
|
|
60
61
|
apex: string;
|
|
61
|
-
}): Promise<Applied>;
|
|
62
|
+
}, audit?: PostmasterAudit): Promise<Applied>;
|
package/dist/postmaster/apply.js
CHANGED
|
@@ -68,6 +68,21 @@ async function credentialIds(instance, accountId) {
|
|
|
68
68
|
.map((c) => c.credentialId)
|
|
69
69
|
.filter((id) => typeof id === 'string');
|
|
70
70
|
}
|
|
71
|
+
/** Records an applied write on the caller's audit, when it brought one. */
|
|
72
|
+
async function recorded(audit, action, applied) {
|
|
73
|
+
if (audit) {
|
|
74
|
+
await audit.write({
|
|
75
|
+
action,
|
|
76
|
+
actor: audit.actor,
|
|
77
|
+
target: { type: applied.targetType, id: applied.targetId },
|
|
78
|
+
outcome: 'success',
|
|
79
|
+
before: applied.before,
|
|
80
|
+
after: applied.after,
|
|
81
|
+
request: audit.requestId ? { id: audit.requestId } : undefined,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return applied;
|
|
85
|
+
}
|
|
71
86
|
/**
|
|
72
87
|
* The account as the server has it right now, confined to the domain the
|
|
73
88
|
* caller resolved. A mismatched domain gets the same answer as a missing
|
|
@@ -104,7 +119,7 @@ function isOurOwnAccount(instance, account, apex) {
|
|
|
104
119
|
* once would otherwise make two accounts, and the second would be the one
|
|
105
120
|
* mail stopped arriving at.
|
|
106
121
|
*/
|
|
107
|
-
export async function createMailbox(instance, input) {
|
|
122
|
+
export async function createMailbox(instance, input, audit) {
|
|
108
123
|
// Throws before any read when the request is one this app does not make
|
|
109
124
|
// (a whole address, an empty name, a domain that keeps its own passwords).
|
|
110
125
|
const value = newAccount(input);
|
|
@@ -115,40 +130,40 @@ export async function createMailbox(instance, input) {
|
|
|
115
130
|
throw new MailWriteRefused(`${address} already has a mailbox`);
|
|
116
131
|
}
|
|
117
132
|
const id = await instance.client.create('Account', value);
|
|
118
|
-
return {
|
|
133
|
+
return recorded(audit, 'mail.mailbox_created', {
|
|
119
134
|
targetType: 'mailbox',
|
|
120
135
|
targetId: address,
|
|
121
136
|
before: null,
|
|
122
137
|
after: { id, name: local, domainId: input.domainId, description: input.description ?? null },
|
|
123
138
|
message: `${address} now has a mailbox. They sign in through the issuer and make an app password for their mail client.`,
|
|
124
|
-
};
|
|
139
|
+
});
|
|
125
140
|
}
|
|
126
|
-
export async function addAlias(instance, input) {
|
|
141
|
+
export async function addAlias(instance, input, audit) {
|
|
127
142
|
const account = await readAccount(instance, input.accountId, input.domainId);
|
|
128
143
|
const aliases = withAlias(account, { name: input.name, domainId: input.domainId });
|
|
129
144
|
await instance.client.update('Account', account.id, { aliases });
|
|
130
145
|
const added = Object.values(aliases).at(-1)?.name;
|
|
131
146
|
const lost = missingSince(Object.values(aliases).map((a) => a.name), await aliasNames(instance, account.id));
|
|
132
|
-
return {
|
|
147
|
+
return recorded(audit, 'mail.alias_added', {
|
|
133
148
|
targetType: 'mail_alias',
|
|
134
149
|
targetId: `${added}@${input.apex}`,
|
|
135
150
|
before: null,
|
|
136
151
|
after: { alias: added, mailbox: `${account.name}@${input.apex}` },
|
|
137
152
|
message: `${added}@${input.apex} is now delivered to ${account.name}@${input.apex}.${lost}`,
|
|
138
|
-
};
|
|
153
|
+
});
|
|
139
154
|
}
|
|
140
|
-
export async function removeAlias(instance, input) {
|
|
155
|
+
export async function removeAlias(instance, input, audit) {
|
|
141
156
|
const account = await readAccount(instance, input.accountId, input.domainId);
|
|
142
157
|
const aliases = withoutAlias(account, input.name);
|
|
143
158
|
await instance.client.update('Account', account.id, { aliases });
|
|
144
159
|
const lost = missingSince(Object.values(aliases).map((a) => a.name), await aliasNames(instance, account.id));
|
|
145
|
-
return {
|
|
160
|
+
return recorded(audit, 'mail.alias_removed', {
|
|
146
161
|
targetType: 'mail_alias',
|
|
147
162
|
targetId: `${input.name}@${input.apex}`,
|
|
148
163
|
before: { alias: input.name, mailbox: `${account.name}@${input.apex}` },
|
|
149
164
|
after: null,
|
|
150
165
|
message: `${input.name}@${input.apex} is no longer delivered anywhere. Mail sent to it will be rejected.${lost}`,
|
|
151
|
-
};
|
|
166
|
+
});
|
|
152
167
|
}
|
|
153
168
|
/**
|
|
154
169
|
* Stop one app password working. The secret is never read, here or anywhere
|
|
@@ -169,7 +184,7 @@ export async function removeAlias(instance, input) {
|
|
|
169
184
|
* limits here; its credentials belong to the bootstrap and to this app,
|
|
170
185
|
* never to a mail client, and the web admin is where they are managed.
|
|
171
186
|
*/
|
|
172
|
-
export async function revokeAppPassword(instance, input) {
|
|
187
|
+
export async function revokeAppPassword(instance, input, audit) {
|
|
173
188
|
const account = await readAccount(instance, input.accountId, input.domainId);
|
|
174
189
|
if (isOurOwnAccount(instance, account, input.apex)) {
|
|
175
190
|
throw new MailWriteRefused(`${instance.user} is the account this management app signs in as, and one of its app passwords is serving this request. Manage its credentials in the web admin.`);
|
|
@@ -181,11 +196,11 @@ export async function revokeAppPassword(instance, input) {
|
|
|
181
196
|
const lost = missingSince(Object.values(credentials)
|
|
182
197
|
.map((c) => c.credentialId)
|
|
183
198
|
.filter((id) => typeof id === 'string'), await credentialIds(instance, account.id));
|
|
184
|
-
return {
|
|
199
|
+
return recorded(audit, 'mail.app_password_revoked', {
|
|
185
200
|
targetType: 'mail_app_password',
|
|
186
201
|
targetId: input.credentialId,
|
|
187
202
|
before: { mailbox: `${account.name}@${input.apex}`, description },
|
|
188
203
|
after: null,
|
|
189
204
|
message: `That app password no longer works${description ? ` (${description})` : ''}. Any mail client using it will be asked to sign in again.${lost}`,
|
|
190
|
-
};
|
|
205
|
+
});
|
|
191
206
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The events the postmaster's four writes record, when a caller hands them
|
|
3
|
+
* an audit. Declared here so a host can merge them into its ledger's
|
|
4
|
+
* vocabulary before it binds a writer to the `mail` namespace; the names sit
|
|
5
|
+
* in that namespace and nowhere else. `tenantVisible` is true throughout:
|
|
6
|
+
* these are an organisation's own facts about its own mail.
|
|
7
|
+
*/
|
|
8
|
+
export declare const POSTMASTER_AUDIT_EVENTS: {
|
|
9
|
+
readonly 'mail.mailbox_created': {
|
|
10
|
+
readonly tenantVisible: true;
|
|
11
|
+
};
|
|
12
|
+
readonly 'mail.alias_added': {
|
|
13
|
+
readonly tenantVisible: true;
|
|
14
|
+
};
|
|
15
|
+
readonly 'mail.alias_removed': {
|
|
16
|
+
readonly tenantVisible: true;
|
|
17
|
+
};
|
|
18
|
+
readonly 'mail.app_password_revoked': {
|
|
19
|
+
readonly tenantVisible: true;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type PostmasterAuditAction = keyof typeof POSTMASTER_AUDIT_EVENTS;
|
|
23
|
+
/** One write, as the host's writer receives it. `targetId` is an address or a credential id, never a secret. */
|
|
24
|
+
export interface PostmasterAuditEvent {
|
|
25
|
+
readonly action: PostmasterAuditAction;
|
|
26
|
+
readonly actor: {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly display: string;
|
|
29
|
+
};
|
|
30
|
+
readonly target: {
|
|
31
|
+
readonly type: 'mailbox' | 'mail_alias' | 'mail_app_password';
|
|
32
|
+
readonly id: string;
|
|
33
|
+
};
|
|
34
|
+
readonly outcome: 'success';
|
|
35
|
+
readonly before: unknown;
|
|
36
|
+
readonly after: unknown;
|
|
37
|
+
readonly request?: {
|
|
38
|
+
readonly id?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What a caller passes to a write: a writer the host bound to its ledger and
|
|
43
|
+
* to the `mail` namespace, the principal the host resolved for this request,
|
|
44
|
+
* and the request id that joins the row to the host's event log. The write
|
|
45
|
+
* records itself after the mail server has answered, so the row carries what
|
|
46
|
+
* actually happened; a write the server refused throws before this is
|
|
47
|
+
* called, and the caller records that refusal as it sees fit.
|
|
48
|
+
*/
|
|
49
|
+
export interface PostmasterAudit {
|
|
50
|
+
readonly write: (event: PostmasterAuditEvent) => Promise<void>;
|
|
51
|
+
readonly actor: {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly display: string;
|
|
54
|
+
};
|
|
55
|
+
readonly requestId?: string;
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The events the postmaster's four writes record, when a caller hands them
|
|
3
|
+
* an audit. Declared here so a host can merge them into its ledger's
|
|
4
|
+
* vocabulary before it binds a writer to the `mail` namespace; the names sit
|
|
5
|
+
* in that namespace and nowhere else. `tenantVisible` is true throughout:
|
|
6
|
+
* these are an organisation's own facts about its own mail.
|
|
7
|
+
*/
|
|
8
|
+
export const POSTMASTER_AUDIT_EVENTS = {
|
|
9
|
+
'mail.mailbox_created': { tenantVisible: true },
|
|
10
|
+
'mail.alias_added': { tenantVisible: true },
|
|
11
|
+
'mail.alias_removed': { tenantVisible: true },
|
|
12
|
+
'mail.app_password_revoked': { tenantVisible: true },
|
|
13
|
+
};
|
|
@@ -22,3 +22,4 @@ export { planMailboxes, withPassword } from './mailboxes.js';
|
|
|
22
22
|
export { overview, pickDomain, type AccountView, type CredentialSummary, type DomainView, type Managed, type Overview, type OverviewInput, type PeopleView, type Person, type WireAccount, type WireAlias, type WireDomain, type WireSystemSettings, } from './overview.js';
|
|
23
23
|
export { MailWriteRefused, newAccount, withAlias, withoutAlias, withoutAppPassword, } from './writes.js';
|
|
24
24
|
export { addAlias, createMailbox, removeAlias, revokeAppPassword, type Applied, } from './apply.js';
|
|
25
|
+
export { POSTMASTER_AUDIT_EVENTS, type PostmasterAudit, type PostmasterAuditAction, type PostmasterAuditEvent, } from './audit.js';
|
package/dist/postmaster/index.js
CHANGED
|
@@ -22,3 +22,4 @@ export { planMailboxes, withPassword } from './mailboxes.js';
|
|
|
22
22
|
export { overview, pickDomain, } from './overview.js';
|
|
23
23
|
export { MailWriteRefused, newAccount, withAlias, withoutAlias, withoutAppPassword, } from './writes.js';
|
|
24
24
|
export { addAlias, createMailbox, removeAlias, revokeAppPassword, } from './apply.js';
|
|
25
|
+
export { POSTMASTER_AUDIT_EVENTS, } from './audit.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The wtfalch estate: reading a mailbox over JMAP, and administering the Stalwart server it lives on. Two entries with no code in common.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|