@framers/agentos-ext-channel-email 0.3.0 → 0.4.2

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 (48) hide show
  1. package/README.md +70 -5
  2. package/dist/EmailChannelAdapter.d.ts +3 -1
  3. package/dist/EmailChannelAdapter.d.ts.map +1 -1
  4. package/dist/EmailChannelAdapter.js +3 -1
  5. package/dist/EmailChannelAdapter.js.map +1 -1
  6. package/dist/EmailService.d.ts +48 -16
  7. package/dist/EmailService.d.ts.map +1 -1
  8. package/dist/EmailService.js +130 -67
  9. package/dist/EmailService.js.map +1 -1
  10. package/dist/ResendTransport.d.ts +5 -2
  11. package/dist/ResendTransport.d.ts.map +1 -1
  12. package/dist/ResendTransport.js.map +1 -1
  13. package/dist/index.d.ts +5 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +12 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/messageHeaders.d.ts +50 -10
  18. package/dist/messageHeaders.d.ts.map +1 -1
  19. package/dist/messageHeaders.js +175 -15
  20. package/dist/messageHeaders.js.map +1 -1
  21. package/dist/messageText.d.ts +9 -1
  22. package/dist/messageText.d.ts.map +1 -1
  23. package/dist/messageText.js +17 -1
  24. package/dist/messageText.js.map +1 -1
  25. package/dist/preferences/RecipientCapability.d.ts +100 -0
  26. package/dist/preferences/RecipientCapability.d.ts.map +1 -0
  27. package/dist/preferences/RecipientCapability.js +168 -0
  28. package/dist/preferences/RecipientCapability.js.map +1 -0
  29. package/dist/preferences/SuppressionStore.d.ts +72 -0
  30. package/dist/preferences/SuppressionStore.d.ts.map +1 -0
  31. package/dist/preferences/SuppressionStore.js +91 -0
  32. package/dist/preferences/SuppressionStore.js.map +1 -0
  33. package/dist/preferences/database.d.ts +34 -0
  34. package/dist/preferences/database.d.ts.map +1 -0
  35. package/dist/preferences/database.js +41 -0
  36. package/dist/preferences/database.js.map +1 -0
  37. package/dist/preferences/index.d.ts +15 -0
  38. package/dist/preferences/index.d.ts.map +1 -0
  39. package/dist/preferences/index.js +30 -0
  40. package/dist/preferences/index.js.map +1 -0
  41. package/dist/tools/extractCodes.js +1 -1
  42. package/dist/tools/extractCodes.js.map +1 -1
  43. package/dist/tools/reply.d.ts +2 -1
  44. package/dist/tools/reply.d.ts.map +1 -1
  45. package/dist/tools/reply.js +3 -2
  46. package/dist/tools/reply.js.map +1 -1
  47. package/manifest.json +4 -3
  48. package/package.json +3 -2
@@ -5,11 +5,12 @@
5
5
  * object as a body and reads the file its `path` names, or fetches the address its `href` names, into the message,
6
6
  * and it writes an object header value marked `prepared` into the message as it is, line breaks included. A model
7
7
  * writes the tools' arguments and the channel's blocks as JSON, so such an object reaches the service whatever the
8
- * TypeScript types say.
8
+ * TypeScript types say. A subject is then put on one line, as nodemailer writes one, so Resend is given the same.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.requireText = requireText;
12
12
  exports.optionalText = optionalText;
13
+ exports.oneLine = oneLine;
13
14
  /**
14
15
  * Returns `value` when it is a string, and otherwise throws a `TypeError` that names `field` and never the value.
15
16
  *
@@ -35,4 +36,19 @@ function optionalText(field, value) {
35
36
  return value;
36
37
  throw new TypeError(`The ${field} was refused: give it as a string or leave it out`);
37
38
  }
39
+ /**
40
+ * A line break, CR LF counted as one, or another character a subject cannot hold on one line: a control character other
41
+ * than the tab, or a Unicode line or paragraph separator.
42
+ */
43
+ const BREAKS_A_LINE = /\r\n|[\x00-\x08\x0a-\x1f\x7f-\x9f\u2028\u2029]/g;
44
+ /**
45
+ * `text` on one line, each line break or other control character but the tab written as a space. nodemailer writes a
46
+ * line break in a subject as a space, and `EmailService` gives Resend the subject it gives nodemailer, so a subject is
47
+ * sent alike through both transports, whoever wrote it: a caller, or the original a reply answers.
48
+ *
49
+ * @param text The subject as given.
50
+ */
51
+ function oneLine(text) {
52
+ return text.replace(BREAKS_A_LINE, ' ');
53
+ }
38
54
  //# sourceMappingURL=messageText.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messageText.js","sourceRoot":"","sources":["../src/messageText.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAQH,kCAGC;AASD,oCAIC;AAtBD;;;;;GAKG;AACH,SAAgB,WAAW,CAAC,KAAa,EAAE,KAAc;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,mCAAmC,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,KAAa,EAAE,KAAc;IACxD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,mDAAmD,CAAC,CAAC;AACvF,CAAC"}
1
+ {"version":3,"file":"messageText.js","sourceRoot":"","sources":["../src/messageText.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAQH,kCAGC;AASD,oCAIC;AAeD,0BAEC;AAvCD;;;;;GAKG;AACH,SAAgB,WAAW,CAAC,KAAa,EAAE,KAAc;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,mCAAmC,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,KAAa,EAAE,KAAc;IACxD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,mDAAmD,CAAC,CAAC;AACvF,CAAC;AAED;;;GAGG;AACH,MAAM,aAAa,GAAG,iDAAiD,CAAC;AAExE;;;;;;GAMG;AACH,SAAgB,OAAO,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,100 @@
1
+ import type { EmailPreferencesDb } from './database.js';
2
+ /** What a token names: the recipient it was issued for and the scope it acts on. */
3
+ export interface RecipientGrant {
4
+ /** The recipient, such as an account id. */
5
+ readonly recipient: string;
6
+ /** The scope, such as one kind of mail. */
7
+ readonly scope: string;
8
+ }
9
+ /** The options of one `issue`. */
10
+ export interface RecipientCapabilityIssueOptions {
11
+ /** When the token stops working, in epoch milliseconds. A token issued without it works until it is revoked. */
12
+ readonly expiresAt?: number;
13
+ }
14
+ /** The options of a `SqlRecipientCapability`. */
15
+ export interface SqlRecipientCapabilityOptions {
16
+ /** The clock `find` reads an expiry against and `revoke` records, in epoch milliseconds: `Date.now` unless given. */
17
+ readonly now?: () => number;
18
+ }
19
+ /**
20
+ * Opaque tokens that each name a recipient and a scope, kept as their SHA-256 alone. A token carries neither: only
21
+ * `find` maps it to them, and the store holds nothing a token can be recovered from.
22
+ */
23
+ export interface RecipientCapability {
24
+ /**
25
+ * Issues the token for a nonce, such as a message's id: base64url of HMAC-SHA256 over the nonce under the key, 43
26
+ * characters. Issuing again with the same nonce and key, for the same recipient, scope and expiry, answers the same
27
+ * token and writes nothing; for another recipient, scope or expiry it throws, and the first token keeps its recipient
28
+ * and scope. A nonce whose token was revoked throws for every recipient, scope and expiry, so a message that needs a
29
+ * working token after `revoke` takes a new nonce.
30
+ *
31
+ * @param recipient The recipient: 1 to 512 characters, with no U+0000 and no lone surrogate.
32
+ * @param scope The scope, under the same rule.
33
+ * @param nonce The nonce, under the same rule: one message, one nonce, never another message's.
34
+ * @param key The key, 32 bytes or more, kept secret by the caller.
35
+ * @param options The token's expiry, if it has one.
36
+ * @returns The token.
37
+ */
38
+ issue(recipient: string, scope: string, nonce: string, key: Uint8Array, options?: RecipientCapabilityIssueOptions): Promise<string>;
39
+ /**
40
+ * The recipient and the scope the token was issued for, or `null` for a token that is malformed, unknown, revoked or
41
+ * past its expiry. The token is found by its SHA-256 and compared in constant time; a malformed token reaches no
42
+ * database.
43
+ *
44
+ * @param token The token as the link carried it.
45
+ */
46
+ find(token: string): Promise<RecipientGrant | null>;
47
+ /**
48
+ * Ends every token issued for the recipient, in every scope. Each token's row stays, marked revoked, so its nonce is
49
+ * never issued again; a token issued afterwards from a new nonce works.
50
+ *
51
+ * @param recipient The recipient: 1 to 512 characters, with no U+0000 and no lone surrogate.
52
+ * @returns How many tokens it ended: a token ended by an earlier revoke is not counted again.
53
+ */
54
+ revoke(recipient: string): Promise<number>;
55
+ }
56
+ /**
57
+ * The token for a nonce under a key: base64url of HMAC-SHA256, 43 characters.
58
+ *
59
+ * @param nonce The nonce, as UTF-8.
60
+ * @param key The key's bytes.
61
+ */
62
+ export declare function capabilityToken(nonce: string, key: Uint8Array): string;
63
+ /**
64
+ * What the store keeps of a token: its SHA-256 in lower-case hexadecimal.
65
+ *
66
+ * @param token The token, as UTF-8.
67
+ */
68
+ export declare function capabilityTokenHash(token: string): string;
69
+ /**
70
+ * The recipient capabilities' table, `<prefix>_capabilities`, and the index `revoke` reads, for a host's migration: one
71
+ * row a token, its SHA-256 the key, with the recipient, the scope, an optional expiry and the time the token was
72
+ * revoked, if it was. Both statements are `IF NOT EXISTS`, so they can run at each start, and they run on SQLite and on
73
+ * PostgreSQL. A row refuses its nonce for as long as it stays: a host that deletes rows, by its own foreign key with
74
+ * `ON DELETE CASCADE` or by hand, never issues their nonces again, and a host that cannot promise that calls `revoke`
75
+ * with each deletion and keeps the rows.
76
+ *
77
+ * @param prefix A lower-case letter followed by at most 39 lower-case letters, digits or underscores.
78
+ * @throws RangeError when the prefix does not fit.
79
+ */
80
+ export declare function recipientCapabilityDdl(prefix: string): string;
81
+ /**
82
+ * Recipient capabilities on a host's SQL database, in the table `recipientCapabilityDdl` makes under the same prefix.
83
+ * Each argument is checked before any statement, and an error from the database reaches the caller as it was thrown.
84
+ */
85
+ export declare class SqlRecipientCapability implements RecipientCapability {
86
+ private readonly db;
87
+ private readonly table;
88
+ private readonly now;
89
+ /**
90
+ * @param db The database: `@framers/sql-storage-adapter`'s adapter, or any handle of `EmailPreferencesDb`'s shape.
91
+ * @param prefix The prefix the table was made under.
92
+ * @param options The clock that expiries are read against and revokes record.
93
+ * @throws RangeError when the prefix does not fit.
94
+ */
95
+ constructor(db: EmailPreferencesDb, prefix: string, options?: SqlRecipientCapabilityOptions);
96
+ issue(recipient: string, scope: string, nonce: string, key: Uint8Array, options?: RecipientCapabilityIssueOptions): Promise<string>;
97
+ find(token: string): Promise<RecipientGrant | null>;
98
+ revoke(recipient: string): Promise<number>;
99
+ }
100
+ //# sourceMappingURL=RecipientCapability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecipientCapability.d.ts","sourceRoot":"","sources":["../../src/preferences/RecipientCapability.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kCAAkC;AAClC,MAAM,WAAW,+BAA+B;IAC9C,gHAAgH;IAChH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,iDAAiD;AACjD,MAAM,WAAW,6BAA6B;IAC5C,qHAAqH;IACrH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;;;;;OAaG;IACH,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,+BAA+B,GACxC,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;;;;OAMG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAEpD;;;;;;OAMG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5C;AAQD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAuDD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY7D;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAW9D,OAAO,CAAC,QAAQ,CAAC,EAAE;IAVrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC;;;;;OAKG;gBAEgB,EAAE,EAAE,kBAAkB,EACvC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,6BAAkC;IAMvC,KAAK,CACT,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,+BAA+B,GACxC,OAAO,CAAC,MAAM,CAAC;IA4BZ,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAgBnD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CASjD"}
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SqlRecipientCapability = void 0;
4
+ exports.capabilityToken = capabilityToken;
5
+ exports.capabilityTokenHash = capabilityTokenHash;
6
+ exports.recipientCapabilityDdl = recipientCapabilityDdl;
7
+ /**
8
+ * @fileoverview Recipient capabilities: opaque tokens that each name one recipient and one scope, such as the link in a
9
+ * message that lets its reader stop one kind of mail without signing in. A token is base64url of HMAC-SHA256 over the
10
+ * caller's nonce under the caller's key, so a message retried with the same nonce carries the same token, and only the
11
+ * token's SHA-256 is stored, beside the recipient and the scope. A token works until its recipient's tokens are
12
+ * revoked, or until the expiry its issuer set. A revoked token's row stays, marked revoked, so its nonce is never issued
13
+ * again: a revoked link never works again, and never comes to name another recipient or scope.
14
+ */
15
+ const node_crypto_1 = require("node:crypto");
16
+ const database_js_1 = require("./database.js");
17
+ /** A token: 43 base64url characters, 32 bytes without padding. */
18
+ const TOKEN = /^[A-Za-z0-9_-]{43}$/;
19
+ /** The shortest key `issue` takes, HMAC-SHA256's output length; RFC 2104 section 3 discourages shorter keys. */
20
+ const KEY_BYTES = 32;
21
+ /**
22
+ * The token for a nonce under a key: base64url of HMAC-SHA256, 43 characters.
23
+ *
24
+ * @param nonce The nonce, as UTF-8.
25
+ * @param key The key's bytes.
26
+ */
27
+ function capabilityToken(nonce, key) {
28
+ return (0, node_crypto_1.createHmac)('sha256', key).update(nonce, 'utf8').digest('base64url');
29
+ }
30
+ /**
31
+ * What the store keeps of a token: its SHA-256 in lower-case hexadecimal.
32
+ *
33
+ * @param token The token, as UTF-8.
34
+ */
35
+ function capabilityTokenHash(token) {
36
+ return (0, node_crypto_1.createHash)('sha256').update(token, 'utf8').digest('hex');
37
+ }
38
+ /** Returns the key when it holds 32 bytes or more, and otherwise throws a `TypeError` that never holds the key. */
39
+ function requireKey(key) {
40
+ if (key instanceof Uint8Array && key.byteLength >= KEY_BYTES)
41
+ return key;
42
+ throw new TypeError(`The key was refused: give it as ${KEY_BYTES} bytes or more, such as a Buffer`);
43
+ }
44
+ /** The expiry `issue`'s options set, or `null` for none; anything but a whole number of milliseconds is refused. */
45
+ function requireExpiry(options) {
46
+ if (options === undefined)
47
+ return null;
48
+ if (typeof options !== 'object' || options === null) {
49
+ throw new TypeError('The options were refused: give them as an object or leave them out');
50
+ }
51
+ const expiresAt = options.expiresAt;
52
+ if (expiresAt === undefined)
53
+ return null;
54
+ if (typeof expiresAt === 'number' && Number.isSafeInteger(expiresAt))
55
+ return expiresAt;
56
+ throw new TypeError('The expiry was refused: give it as a whole number of epoch milliseconds or leave it out');
57
+ }
58
+ /** A stored expiry as a number, or `null` for none: PostgreSQL answers a BIGINT as text. */
59
+ function expiryOf(value) {
60
+ return value === null || value === undefined ? null : Number(value);
61
+ }
62
+ /**
63
+ * The time a revoke records: the clock's reading in whole epoch milliseconds, or 0 when it answers no such number. A
64
+ * revoke must mark its rows whatever the clock says: SQLite stores a NaN as NULL, which would leave the tokens working.
65
+ */
66
+ function revokedAtOf(reading) {
67
+ const at = typeof reading === 'number' ? Math.trunc(reading) : Number.NaN;
68
+ return Number.isSafeInteger(at) ? at : 0;
69
+ }
70
+ /** Whether two strings are equal, compared in time that does not depend on where they differ. */
71
+ function equalInConstantTime(left, right) {
72
+ const a = Buffer.from(left, 'utf8');
73
+ const b = Buffer.from(right, 'utf8');
74
+ return a.length === b.length && (0, node_crypto_1.timingSafeEqual)(a, b);
75
+ }
76
+ /** Whether a stored row was revoked: any value in `revoked_at`, which PostgreSQL answers as text. */
77
+ function isRevoked(row) {
78
+ return row.revoked_at !== null && row.revoked_at !== undefined;
79
+ }
80
+ /**
81
+ * The recipient capabilities' table, `<prefix>_capabilities`, and the index `revoke` reads, for a host's migration: one
82
+ * row a token, its SHA-256 the key, with the recipient, the scope, an optional expiry and the time the token was
83
+ * revoked, if it was. Both statements are `IF NOT EXISTS`, so they can run at each start, and they run on SQLite and on
84
+ * PostgreSQL. A row refuses its nonce for as long as it stays: a host that deletes rows, by its own foreign key with
85
+ * `ON DELETE CASCADE` or by hand, never issues their nonces again, and a host that cannot promise that calls `revoke`
86
+ * with each deletion and keeps the rows.
87
+ *
88
+ * @param prefix A lower-case letter followed by at most 39 lower-case letters, digits or underscores.
89
+ * @throws RangeError when the prefix does not fit.
90
+ */
91
+ function recipientCapabilityDdl(prefix) {
92
+ const table = `${(0, database_js_1.requirePrefix)(prefix)}_capabilities`;
93
+ return [
94
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
95
+ ' token_hash TEXT PRIMARY KEY,',
96
+ ' recipient TEXT NOT NULL,',
97
+ ' scope TEXT NOT NULL,',
98
+ ' expires_at BIGINT,',
99
+ ' revoked_at BIGINT',
100
+ ');',
101
+ `CREATE INDEX IF NOT EXISTS ${table}_recipient ON ${table} (recipient);`,
102
+ ].join('\n');
103
+ }
104
+ /**
105
+ * Recipient capabilities on a host's SQL database, in the table `recipientCapabilityDdl` makes under the same prefix.
106
+ * Each argument is checked before any statement, and an error from the database reaches the caller as it was thrown.
107
+ */
108
+ class SqlRecipientCapability {
109
+ /**
110
+ * @param db The database: `@framers/sql-storage-adapter`'s adapter, or any handle of `EmailPreferencesDb`'s shape.
111
+ * @param prefix The prefix the table was made under.
112
+ * @param options The clock that expiries are read against and revokes record.
113
+ * @throws RangeError when the prefix does not fit.
114
+ */
115
+ constructor(db, prefix, options = {}) {
116
+ this.db = db;
117
+ this.table = `${(0, database_js_1.requirePrefix)(prefix)}_capabilities`;
118
+ this.now = options.now ?? Date.now;
119
+ }
120
+ async issue(recipient, scope, nonce, key, options) {
121
+ (0, database_js_1.requireName)('recipient', recipient);
122
+ (0, database_js_1.requireName)('scope', scope);
123
+ (0, database_js_1.requireName)('nonce', nonce);
124
+ const expiresAt = requireExpiry(options);
125
+ const token = capabilityToken(nonce, requireKey(key));
126
+ const tokenHash = capabilityTokenHash(token);
127
+ const inserted = await this.db.run(`INSERT INTO ${this.table} (token_hash, recipient, scope, expires_at) VALUES (?, ?, ?, ?) ` +
128
+ 'ON CONFLICT (token_hash) DO NOTHING', [tokenHash, recipient, scope, expiresAt]);
129
+ if (inserted.changes === 1)
130
+ return token;
131
+ // The nonce was issued before, or a handle that reports no `changes` wrote it: the stored row decides.
132
+ const row = await this.db.get(`SELECT recipient, scope, expires_at, revoked_at FROM ${this.table} WHERE token_hash = ?`, [tokenHash]);
133
+ if (row === null || row === undefined) {
134
+ throw new Error('The token was not stored: its row was deleted while it was issued');
135
+ }
136
+ if (isRevoked(row)) {
137
+ throw new Error('The token was refused: its nonce was revoked under this key; give the message a new nonce');
138
+ }
139
+ if (row.recipient === recipient && row.scope === scope && expiryOf(row.expires_at) === expiresAt)
140
+ return token;
141
+ throw new Error('The token was refused: its nonce was issued under this key for another recipient, scope or expiry');
142
+ }
143
+ async find(token) {
144
+ if (typeof token !== 'string' || !TOKEN.test(token))
145
+ return null;
146
+ const tokenHash = capabilityTokenHash(token);
147
+ const row = await this.db.get(`SELECT token_hash, recipient, scope, expires_at, revoked_at FROM ${this.table} WHERE token_hash = ?`, [tokenHash]);
148
+ if (row === null || row === undefined || typeof row.token_hash !== 'string')
149
+ return null;
150
+ if (!equalInConstantTime(row.token_hash, tokenHash))
151
+ return null;
152
+ if (isRevoked(row))
153
+ return null;
154
+ const expiresAt = expiryOf(row.expires_at);
155
+ // Written so that a clock that answers NaN ends the token rather than keeping it.
156
+ if (expiresAt !== null && !(this.now() < expiresAt))
157
+ return null;
158
+ return { recipient: row.recipient, scope: row.scope };
159
+ }
160
+ async revoke(recipient) {
161
+ const name = (0, database_js_1.requireName)('recipient', recipient);
162
+ // The rows stay, so `issue` can refuse their nonces; only the live ones are marked and counted.
163
+ const result = await this.db.run(`UPDATE ${this.table} SET revoked_at = ? WHERE recipient = ? AND revoked_at IS NULL`, [revokedAtOf(this.now()), name]);
164
+ return result.changes ?? 0;
165
+ }
166
+ }
167
+ exports.SqlRecipientCapability = SqlRecipientCapability;
168
+ //# sourceMappingURL=RecipientCapability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecipientCapability.js","sourceRoot":"","sources":["../../src/preferences/RecipientCapability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @fileoverview The suppression record: the addresses a host must not mail, by scope, each named by the SHA-256 of the
3
+ * address that the host computes, so the record never holds an address. A host's senders read it before a send, and
4
+ * the handler of its provider's events writes it when a message bounces, draws a complaint or meets the provider's own
5
+ * suppression list. A second record of an address writes nothing, so a provider's repeated event is harmless.
6
+ */
7
+ import type { EmailPreferencesDb } from './database.js';
8
+ /** The reasons an address is suppressed, in the order the README lists them. */
9
+ export declare const SUPPRESSION_REASONS: readonly ["bounce", "complaint", "suppressed", "manual"];
10
+ /**
11
+ * Why an address is suppressed: `bounce` (a message to it bounced), `complaint` (its reader reported a message as
12
+ * spam), `suppressed` (the provider refused a message because the address was already on its own list) or `manual`
13
+ * (an operator, or the person, asked).
14
+ */
15
+ export type SuppressionReason = (typeof SUPPRESSION_REASONS)[number];
16
+ /** The scope of a host that keeps one list: every reader and writer of the record passes it. */
17
+ export declare const DEFAULT_SUPPRESSION_SCOPE = "mail";
18
+ /**
19
+ * The addresses a host must not mail, by scope. A scope is one list, such as all of a host's mail or one sending
20
+ * domain's, and most hosts keep one, `DEFAULT_SUPPRESSION_SCOPE`. An address is named by its hash, the SHA-256 of the
21
+ * address as 64 lower-case hexadecimal characters, which the host computes the same way wherever it reads or writes the
22
+ * record, such as over the address lower-cased. Anything else is refused with a `TypeError`, so an address passed by
23
+ * mistake is never stored and never answered as not suppressed.
24
+ */
25
+ export interface SuppressionStore {
26
+ /**
27
+ * Whether the address is suppressed in the scope.
28
+ *
29
+ * @param scope The list: 1 to 512 characters, with no U+0000 and no lone surrogate.
30
+ * @param addressHash The SHA-256 of the address as 64 lower-case hexadecimal characters.
31
+ */
32
+ isSuppressed(scope: string, addressHash: string): Promise<boolean>;
33
+ /**
34
+ * Records the address as suppressed in the scope, with the reason and the day, and no end. A second record of the
35
+ * address in the scope writes nothing and leaves the first as it was, whatever its reason or day.
36
+ *
37
+ * @param scope The list: 1 to 512 characters, with no U+0000 and no lone surrogate.
38
+ * @param addressHash The SHA-256 of the address as 64 lower-case hexadecimal characters.
39
+ * @param reason Why the address is suppressed.
40
+ * @param day The calendar day of the event, `YYYY-MM-DD`.
41
+ * @returns `true` when this call recorded the address, `false` when the scope held it already.
42
+ */
43
+ suppress(scope: string, addressHash: string, reason: SuppressionReason, day: string): Promise<boolean>;
44
+ }
45
+ /**
46
+ * The suppression record's table, `<prefix>_suppressions`, for a host's migration: one row an address and scope, with
47
+ * the reason and the day, `CREATE TABLE IF NOT EXISTS` so it can run at each start. It runs on SQLite and on
48
+ * PostgreSQL.
49
+ *
50
+ * @param prefix A lower-case letter followed by at most 39 lower-case letters, digits or underscores.
51
+ * @throws RangeError when the prefix does not fit.
52
+ */
53
+ export declare function suppressionStoreDdl(prefix: string): string;
54
+ /**
55
+ * The suppression record on a host's SQL database, in the table `suppressionStoreDdl` makes under the same prefix. A
56
+ * record is one `INSERT ... ON CONFLICT DO NOTHING`, so two hosts recording one address at once leave one row. Each
57
+ * argument is checked before any statement, and an error from the database reaches the caller as it was thrown: no
58
+ * failure is answered as `false`.
59
+ */
60
+ export declare class SqlSuppressionStore implements SuppressionStore {
61
+ private readonly db;
62
+ private readonly table;
63
+ /**
64
+ * @param db The database: `@framers/sql-storage-adapter`'s adapter, or any handle of `EmailPreferencesDb`'s shape.
65
+ * @param prefix The prefix the table was made under.
66
+ * @throws RangeError when the prefix does not fit.
67
+ */
68
+ constructor(db: EmailPreferencesDb, prefix: string);
69
+ isSuppressed(scope: string, addressHash: string): Promise<boolean>;
70
+ suppress(scope: string, addressHash: string, reason: SuppressionReason, day: string): Promise<boolean>;
71
+ }
72
+ //# sourceMappingURL=SuppressionStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SuppressionStore.d.ts","sourceRoot":"","sources":["../../src/preferences/SuppressionStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,0DAA2D,CAAC;AAE5F;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAErE,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,SAAS,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEnE;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxG;AAwCD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAW1D;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,gBAAgB;IASxD,OAAO,CAAC,QAAQ,CAAC,EAAE;IARrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B;;;;OAIG;gBAEgB,EAAE,EAAE,kBAAkB,EACvC,MAAM,EAAE,MAAM;IAKV,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAS7G"}
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SqlSuppressionStore = exports.DEFAULT_SUPPRESSION_SCOPE = exports.SUPPRESSION_REASONS = void 0;
4
+ exports.suppressionStoreDdl = suppressionStoreDdl;
5
+ const database_js_1 = require("./database.js");
6
+ /** The reasons an address is suppressed, in the order the README lists them. */
7
+ exports.SUPPRESSION_REASONS = ['bounce', 'complaint', 'suppressed', 'manual'];
8
+ /** The scope of a host that keeps one list: every reader and writer of the record passes it. */
9
+ exports.DEFAULT_SUPPRESSION_SCOPE = 'mail';
10
+ /** The SHA-256 of an address in lower-case hexadecimal. */
11
+ const ADDRESS_HASH = /^[0-9a-f]{64}$/;
12
+ /** A calendar day, `YYYY-MM-DD`. */
13
+ const DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
14
+ /**
15
+ * Returns the hash when it is 64 lower-case hexadecimal characters, and otherwise throws a `TypeError` that never holds
16
+ * the value.
17
+ */
18
+ function requireAddressHash(value) {
19
+ if (typeof value === 'string' && ADDRESS_HASH.test(value))
20
+ return value;
21
+ throw new TypeError('The address hash was refused: give the SHA-256 of the address as 64 lower-case hexadecimal characters');
22
+ }
23
+ /** Returns the reason when it is one of `SUPPRESSION_REASONS`, and otherwise throws a `TypeError`. */
24
+ function requireReason(value) {
25
+ if (typeof value === 'string' && exports.SUPPRESSION_REASONS.includes(value)) {
26
+ return value;
27
+ }
28
+ throw new TypeError('The reason was refused: give bounce, complaint, suppressed or manual');
29
+ }
30
+ /** Returns the day when it is a calendar date written `YYYY-MM-DD`, and otherwise throws a `TypeError`. */
31
+ function requireDay(value) {
32
+ if (typeof value === 'string') {
33
+ const parts = DAY.exec(value);
34
+ // A date that does not exist, such as the 30th of February, comes back from Date.UTC as another day.
35
+ if (parts !== null &&
36
+ new Date(Date.UTC(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))).toISOString().slice(0, 10) === value) {
37
+ return value;
38
+ }
39
+ }
40
+ throw new TypeError('The day was refused: give a calendar date as YYYY-MM-DD');
41
+ }
42
+ /**
43
+ * The suppression record's table, `<prefix>_suppressions`, for a host's migration: one row an address and scope, with
44
+ * the reason and the day, `CREATE TABLE IF NOT EXISTS` so it can run at each start. It runs on SQLite and on
45
+ * PostgreSQL.
46
+ *
47
+ * @param prefix A lower-case letter followed by at most 39 lower-case letters, digits or underscores.
48
+ * @throws RangeError when the prefix does not fit.
49
+ */
50
+ function suppressionStoreDdl(prefix) {
51
+ const table = `${(0, database_js_1.requirePrefix)(prefix)}_suppressions`;
52
+ return [
53
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
54
+ ' scope TEXT NOT NULL,',
55
+ ' address_hash TEXT NOT NULL,',
56
+ ' reason TEXT NOT NULL,',
57
+ ' suppressed_on TEXT NOT NULL,',
58
+ ' PRIMARY KEY (scope, address_hash)',
59
+ ');',
60
+ ].join('\n');
61
+ }
62
+ /**
63
+ * The suppression record on a host's SQL database, in the table `suppressionStoreDdl` makes under the same prefix. A
64
+ * record is one `INSERT ... ON CONFLICT DO NOTHING`, so two hosts recording one address at once leave one row. Each
65
+ * argument is checked before any statement, and an error from the database reaches the caller as it was thrown: no
66
+ * failure is answered as `false`.
67
+ */
68
+ class SqlSuppressionStore {
69
+ /**
70
+ * @param db The database: `@framers/sql-storage-adapter`'s adapter, or any handle of `EmailPreferencesDb`'s shape.
71
+ * @param prefix The prefix the table was made under.
72
+ * @throws RangeError when the prefix does not fit.
73
+ */
74
+ constructor(db, prefix) {
75
+ this.db = db;
76
+ this.table = `${(0, database_js_1.requirePrefix)(prefix)}_suppressions`;
77
+ }
78
+ async isSuppressed(scope, addressHash) {
79
+ const params = [(0, database_js_1.requireName)('scope', scope), requireAddressHash(addressHash)];
80
+ const row = await this.db.get(`SELECT 1 AS hit FROM ${this.table} WHERE scope = ? AND address_hash = ?`, params);
81
+ return row !== null && row !== undefined;
82
+ }
83
+ async suppress(scope, addressHash, reason, day) {
84
+ const params = [(0, database_js_1.requireName)('scope', scope), requireAddressHash(addressHash), requireReason(reason), requireDay(day)];
85
+ const result = await this.db.run(`INSERT INTO ${this.table} (scope, address_hash, reason, suppressed_on) VALUES (?, ?, ?, ?) ` +
86
+ 'ON CONFLICT (scope, address_hash) DO NOTHING', params);
87
+ return result.changes === 1;
88
+ }
89
+ }
90
+ exports.SqlSuppressionStore = SqlSuppressionStore;
91
+ //# sourceMappingURL=SuppressionStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SuppressionStore.js","sourceRoot":"","sources":["../../src/preferences/SuppressionStore.ts"],"names":[],"mappings":";;;AAiGA,kDAWC;AArGD,+CAA2D;AAE3D,gFAAgF;AACnE,QAAA,mBAAmB,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,CAAU,CAAC;AAS5F,gGAAgG;AACnF,QAAA,yBAAyB,GAAG,MAAM,CAAC;AA+BhD,2DAA2D;AAC3D,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAEtC,oCAAoC;AACpC,MAAM,GAAG,GAAG,2BAA2B,CAAC;AAExC;;;GAGG;AACH,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxE,MAAM,IAAI,SAAS,CAAC,uGAAuG,CAAC,CAAC;AAC/H,CAAC;AAED,sGAAsG;AACtG,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAK,2BAAyC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5F,OAAO,KAA0B,CAAC;IACpC,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,sEAAsE,CAAC,CAAC;AAC9F,CAAC;AAED,2GAA2G;AAC3G,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,qGAAqG;QACrG,IACE,KAAK,KAAK,IAAI;YACd,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,EACjH,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,yDAAyD,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,mBAAmB,CAAC,MAAc;IAChD,MAAM,KAAK,GAAG,GAAG,IAAA,2BAAa,EAAC,MAAM,CAAC,eAAe,CAAC;IACtD,OAAO;QACL,8BAA8B,KAAK,IAAI;QACvC,wBAAwB;QACxB,+BAA+B;QAC/B,yBAAyB;QACzB,gCAAgC;QAChC,qCAAqC;QACrC,IAAI;KACL,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAa,mBAAmB;IAG9B;;;;OAIG;IACH,YACmB,EAAsB,EACvC,MAAc;QADG,OAAE,GAAF,EAAE,CAAoB;QAGvC,IAAI,CAAC,KAAK,GAAG,GAAG,IAAA,2BAAa,EAAC,MAAM,CAAC,eAAe,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAa,EAAE,WAAmB;QACnD,MAAM,MAAM,GAAG,CAAC,IAAA,yBAAW,EAAC,OAAO,EAAE,KAAK,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAC;QAC9E,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAC3B,wBAAwB,IAAI,CAAC,KAAK,uCAAuC,EACzE,MAAM,CACP,CAAC;QACF,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,KAAa,EAAE,WAAmB,EAAE,MAAyB,EAAE,GAAW;QACvF,MAAM,MAAM,GAAG,CAAC,IAAA,yBAAW,EAAC,OAAO,EAAE,KAAK,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QACtH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,GAAG,CAC9B,eAAe,IAAI,CAAC,KAAK,oEAAoE;YAC3F,8CAA8C,EAChD,MAAM,CACP,CAAC;QACF,OAAO,MAAM,CAAC,OAAO,KAAK,CAAC,CAAC;IAC9B,CAAC;CACF;AAjCD,kDAiCC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @fileoverview What the preference stores need of a database, and the checks of the table prefix and the names they
3
+ * take. Nothing here imports a database driver: the host passes its own handle.
4
+ */
5
+ /**
6
+ * What `SqlSuppressionStore` and `SqlRecipientCapability` need of a database: a statement that answers the number of
7
+ * rows it changed, and a query that answers its first row, both with `?` placeholders and positional parameters.
8
+ * `@framers/sql-storage-adapter`'s `StorageAdapter` has this shape on SQLite and on PostgreSQL, and so does the handle
9
+ * its `transaction` gives a callback, so a store made on that handle writes inside the transaction.
10
+ */
11
+ export interface EmailPreferencesDb {
12
+ /** Runs one statement; `changes` is the number of rows it inserted, updated or deleted. */
13
+ run(sql: string, params?: unknown[]): Promise<{
14
+ changes?: number;
15
+ }>;
16
+ /** Runs one query and answers its first row, or `null` (or `undefined`) when it has none. */
17
+ get<T>(sql: string, params?: unknown[]): Promise<T | null | undefined>;
18
+ }
19
+ /**
20
+ * Returns the prefix when it is a lower-case letter followed by at most 39 lower-case letters, digits or underscores,
21
+ * and otherwise throws a `RangeError`. The prefix is written into the SQL as it is, so nothing else passes.
22
+ *
23
+ * @param prefix The table prefix the caller gave.
24
+ */
25
+ export declare function requirePrefix(prefix: unknown): string;
26
+ /**
27
+ * Returns `value` when it is a string of 1 to 512 characters that a text column holds as it is, with no U+0000 and no
28
+ * lone surrogate, and otherwise throws a `TypeError` that names `field` and never the value.
29
+ *
30
+ * @param field The argument's name, such as `scope`.
31
+ * @param value The value the caller gave.
32
+ */
33
+ export declare function requireName(field: string, value: unknown): string;
34
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/preferences/database.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,6FAA6F;IAC7F,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACxE;AAKD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAKrD;AAUD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAKjE"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview What the preference stores need of a database, and the checks of the table prefix and the names they
4
+ * take. Nothing here imports a database driver: the host passes its own handle.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.requirePrefix = requirePrefix;
8
+ exports.requireName = requireName;
9
+ /** A table prefix: a lower-case letter, then at most 39 lower-case letters, digits or underscores. */
10
+ const PREFIX = /^[a-z][a-z0-9_]{0,39}$/;
11
+ /**
12
+ * Returns the prefix when it is a lower-case letter followed by at most 39 lower-case letters, digits or underscores,
13
+ * and otherwise throws a `RangeError`. The prefix is written into the SQL as it is, so nothing else passes.
14
+ *
15
+ * @param prefix The table prefix the caller gave.
16
+ */
17
+ function requirePrefix(prefix) {
18
+ if (typeof prefix === 'string' && PREFIX.test(prefix))
19
+ return prefix;
20
+ throw new RangeError('The table prefix was refused: give a lower-case letter followed by at most 39 lower-case letters, digits or underscores');
21
+ }
22
+ /**
23
+ * What a text column cannot hold as given: U+0000, which PostgreSQL's text refuses, and a lone surrogate, which UTF-8
24
+ * cannot encode, so a driver writes something else in its place (node-postgres writes U+FFFD): the row would name
25
+ * another value, and two values would meet in one. With the `u` flag a surrogate pair is one code point and does not
26
+ * match.
27
+ */
28
+ const UNSTORABLE = /\u0000|[\uD800-\uDFFF]/u;
29
+ /**
30
+ * Returns `value` when it is a string of 1 to 512 characters that a text column holds as it is, with no U+0000 and no
31
+ * lone surrogate, and otherwise throws a `TypeError` that names `field` and never the value.
32
+ *
33
+ * @param field The argument's name, such as `scope`.
34
+ * @param value The value the caller gave.
35
+ */
36
+ function requireName(field, value) {
37
+ if (typeof value === 'string' && value.length > 0 && value.length <= 512 && !UNSTORABLE.test(value))
38
+ return value;
39
+ throw new TypeError(`The ${field} was refused: give it as a string of 1 to 512 characters with no U+0000 and no lone surrogate`);
40
+ }
41
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/preferences/database.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAwBH,sCAKC;AAiBD,kCAKC;AApCD,sGAAsG;AACtG,MAAM,MAAM,GAAG,wBAAwB,CAAC;AAExC;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACrE,MAAM,IAAI,UAAU,CAClB,yHAAyH,CAC1H,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,GAAG,yBAAyB,CAAC;AAE7C;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,KAAa,EAAE,KAAc;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAClH,MAAM,IAAI,SAAS,CACjB,OAAO,KAAK,+FAA+F,CAC5G,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ export type { EmailPreferencesDb } from './database.js';
2
+ export { DEFAULT_SUPPRESSION_SCOPE, SUPPRESSION_REASONS, SqlSuppressionStore, suppressionStoreDdl } from './SuppressionStore.js';
3
+ export type { SuppressionReason, SuppressionStore } from './SuppressionStore.js';
4
+ export { SqlRecipientCapability, recipientCapabilityDdl } from './RecipientCapability.js';
5
+ export type { RecipientCapability, RecipientCapabilityIssueOptions, RecipientGrant, SqlRecipientCapabilityOptions, } from './RecipientCapability.js';
6
+ /**
7
+ * Both tables' DDL under one prefix, the suppression record's then the recipient capabilities', for a host whose one
8
+ * migration makes both. A host whose tables come from different migrations runs `suppressionStoreDdl` and
9
+ * `recipientCapabilityDdl` where each belongs.
10
+ *
11
+ * @param prefix A lower-case letter followed by at most 39 lower-case letters, digits or underscores.
12
+ * @throws RangeError when the prefix does not fit.
13
+ */
14
+ export declare function emailPreferencesDdl(prefix: string): string;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/preferences/index.ts"],"names":[],"mappings":"AAOA,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjI,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC1F,YAAY,EACV,mBAAmB,EACnB,+BAA+B,EAC/B,cAAc,EACd,6BAA6B,GAC9B,MAAM,0BAA0B,CAAC;AAElC;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D"}