@happyvertical/smrt-messages 0.60.9 → 0.60.11
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/AGENTS.md +27 -0
- package/README.md +74 -0
- package/agents/notification-outbox.md +162 -0
- package/dist/chunks/{AccountCollection-CPsjabD-.js → AccountCollection-BGzfQG2I.js} +2 -2
- package/dist/chunks/{AccountCollection-CPsjabD-.js.map → AccountCollection-BGzfQG2I.js.map} +1 -1
- package/dist/chunks/{Email-C85SNWH0.js → Email-CBIrfFLm.js} +3 -3
- package/dist/chunks/{Email-C85SNWH0.js.map → Email-CBIrfFLm.js.map} +1 -1
- package/dist/chunks/{EmailFolder-Ia8OFxRn.js → EmailFolder-Cz2JU5Z1.js} +2 -2
- package/dist/chunks/{EmailFolder-Ia8OFxRn.js.map → EmailFolder-Cz2JU5Z1.js.map} +1 -1
- package/dist/chunks/{Message-x-bj-WcS.js → Message-DiO_FvAR.js} +52 -5
- package/dist/chunks/Message-DiO_FvAR.js.map +1 -0
- package/dist/chunks/{MessageCollection-BM4y2_mB.js → MessageCollection-uui10uhg.js} +2 -2
- package/dist/chunks/{MessageCollection-BM4y2_mB.js.map → MessageCollection-uui10uhg.js.map} +1 -1
- package/dist/chunks/__smrt-register__-DyTpUDMS.js +6 -0
- package/dist/chunks/__smrt-register__-DyTpUDMS.js.map +1 -0
- package/dist/collections/NotificationDeliveryCollection.d.ts +6 -0
- package/dist/collections/NotificationDeliveryCollection.d.ts.map +1 -0
- package/dist/collections/NotificationOptOutCollection.d.ts +6 -0
- package/dist/collections/NotificationOptOutCollection.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +989 -38
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +3870 -2495
- package/dist/models/Message.d.ts +15 -0
- package/dist/models/Message.d.ts.map +1 -1
- package/dist/models/NotificationDelivery.d.ts +128 -0
- package/dist/models/NotificationDelivery.d.ts.map +1 -0
- package/dist/models/NotificationOptOut.d.ts +44 -0
- package/dist/models/NotificationOptOut.d.ts.map +1 -0
- package/dist/senders/EmailSender.d.ts +9 -0
- package/dist/senders/EmailSender.d.ts.map +1 -1
- package/dist/services/NotificationOptOutService.d.ts +80 -0
- package/dist/services/NotificationOptOutService.d.ts.map +1 -0
- package/dist/services/NotificationOutboxService.d.ts +226 -0
- package/dist/services/NotificationOutboxService.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +564 -9
- package/dist/smrt-knowledge.okf.md +157 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -16
- package/dist/chunks/Message-x-bj-WcS.js.map +0 -1
- package/dist/chunks/__smrt-register__-B7iWfX8p.js +0 -6
- package/dist/chunks/__smrt-register__-B7iWfX8p.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunks/rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import "./chunks/__smrt-register__-
|
|
2
|
+
import "./chunks/__smrt-register__-DyTpUDMS.js";
|
|
3
3
|
import { a as registerMessagingProvider, n as getMessagingProvider, o as ZulipSender, r as listMessagingProviders, s as TelegramSender, t as ensureBuiltinMessagingProvidersRegistered } from "./chunks/providers-4iLula-V.js";
|
|
4
|
-
import { r as Account, t as AccountCollection } from "./chunks/AccountCollection-
|
|
5
|
-
import { t as Message } from "./chunks/Message-
|
|
6
|
-
import { t as MessageCollection } from "./chunks/MessageCollection-
|
|
7
|
-
import { t as Email } from "./chunks/Email-
|
|
8
|
-
import { t as EmailFolder } from "./chunks/EmailFolder-
|
|
4
|
+
import { r as Account, t as AccountCollection } from "./chunks/AccountCollection-BGzfQG2I.js";
|
|
5
|
+
import { t as Message } from "./chunks/Message-DiO_FvAR.js";
|
|
6
|
+
import { t as MessageCollection } from "./chunks/MessageCollection-uui10uhg.js";
|
|
7
|
+
import { t as Email } from "./chunks/Email-CBIrfFLm.js";
|
|
8
|
+
import { t as EmailFolder } from "./chunks/EmailFolder-Cz2JU5Z1.js";
|
|
9
9
|
import { t as SlackSender } from "./chunks/SlackSender-CjSvwY1K.js";
|
|
10
10
|
import { t as TweetSender } from "./chunks/TweetSender-CcLPNNi1.js";
|
|
11
|
-
import { SmrtCollection, SmrtObject, SmrtRecipe, crossPackageRef, field, foreignKey, importOptionalDependency, isUniqueViolationError, smrt } from "@happyvertical/smrt-core";
|
|
11
|
+
import { SmrtCollection, SmrtObject, SmrtRecipe, crossPackageRef, field, foreignKey, importOptionalDependency, isUniqueViolationError, smrt, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue } from "@happyvertical/smrt-core";
|
|
12
12
|
import { registerPermissionDefinitions } from "@happyvertical/smrt-users";
|
|
13
|
-
import { TenantScoped, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
13
|
+
import { TenantScoped, queryGlobal, queryWithGlobals, tenantId, withSystemContext, withTenant } from "@happyvertical/smrt-tenancy";
|
|
14
14
|
//#region src/permissions.ts
|
|
15
15
|
var SEND_MESSAGES_PERMISSION = "messages.send";
|
|
16
16
|
var MANAGE_MESSAGE_ROUTES_PERMISSION = "messages.manage-routes";
|
|
@@ -54,21 +54,21 @@ function ensureMessagingPermissionsRegistered() {
|
|
|
54
54
|
}
|
|
55
55
|
//#endregion
|
|
56
56
|
//#region src/models/Attachment.ts
|
|
57
|
-
var __defProp$
|
|
58
|
-
var __getOwnPropDesc$
|
|
59
|
-
var __defNormalProp$
|
|
57
|
+
var __defProp$14 = Object.defineProperty;
|
|
58
|
+
var __getOwnPropDesc$14 = Object.getOwnPropertyDescriptor;
|
|
59
|
+
var __defNormalProp$14 = (obj, key, value) => key in obj ? __defProp$14(obj, key, {
|
|
60
60
|
enumerable: true,
|
|
61
61
|
configurable: true,
|
|
62
62
|
writable: true,
|
|
63
63
|
value
|
|
64
64
|
}) : obj[key] = value;
|
|
65
|
-
var __decorateClass$
|
|
66
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$
|
|
65
|
+
var __decorateClass$14 = (decorators, target, key, kind) => {
|
|
66
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$14(target, key) : target;
|
|
67
67
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
68
|
-
if (kind && result) __defProp$
|
|
68
|
+
if (kind && result) __defProp$14(target, key, result);
|
|
69
69
|
return result;
|
|
70
70
|
};
|
|
71
|
-
var __publicField$
|
|
71
|
+
var __publicField$14 = (obj, key, value) => __defNormalProp$14(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
72
72
|
function splitAbsolutePath(filePath) {
|
|
73
73
|
const windows = /^[A-Za-z]:[\\/]/.test(filePath);
|
|
74
74
|
if (!windows && !filePath.startsWith("/")) return null;
|
|
@@ -107,7 +107,7 @@ var Attachment = class extends SmrtObject {
|
|
|
107
107
|
*/
|
|
108
108
|
async getMessage() {
|
|
109
109
|
if (!this.messageId) return null;
|
|
110
|
-
const { MessageCollection } = await import("./chunks/MessageCollection-
|
|
110
|
+
const { MessageCollection } = await import("./chunks/MessageCollection-uui10uhg.js").then((n) => n.n);
|
|
111
111
|
return await (await MessageCollection.create(this.options)).get({ id: this.messageId });
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
@@ -187,10 +187,10 @@ var Attachment = class extends SmrtObject {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
};
|
|
190
|
-
__publicField$
|
|
191
|
-
__decorateClass$
|
|
192
|
-
__decorateClass$
|
|
193
|
-
Attachment = __decorateClass$
|
|
190
|
+
__publicField$14(Attachment, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
191
|
+
__decorateClass$14([tenantId({ nullable: true })], Attachment.prototype, "tenantId", 2);
|
|
192
|
+
__decorateClass$14([foreignKey("Message")], Attachment.prototype, "messageId", 2);
|
|
193
|
+
Attachment = __decorateClass$14([TenantScoped({ mode: "optional" }), smrt({
|
|
194
194
|
api: { include: [
|
|
195
195
|
"list",
|
|
196
196
|
"get",
|
|
@@ -317,21 +317,21 @@ var AttachmentCollection = class extends SmrtCollection {
|
|
|
317
317
|
};
|
|
318
318
|
//#endregion
|
|
319
319
|
//#region src/models/MessagingEndpoint.ts
|
|
320
|
-
var __defProp$
|
|
321
|
-
var __getOwnPropDesc$
|
|
322
|
-
var __defNormalProp$
|
|
320
|
+
var __defProp$13 = Object.defineProperty;
|
|
321
|
+
var __getOwnPropDesc$13 = Object.getOwnPropertyDescriptor;
|
|
322
|
+
var __defNormalProp$13 = (obj, key, value) => key in obj ? __defProp$13(obj, key, {
|
|
323
323
|
enumerable: true,
|
|
324
324
|
configurable: true,
|
|
325
325
|
writable: true,
|
|
326
326
|
value
|
|
327
327
|
}) : obj[key] = value;
|
|
328
|
-
var __decorateClass$
|
|
329
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$
|
|
328
|
+
var __decorateClass$13 = (decorators, target, key, kind) => {
|
|
329
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$13(target, key) : target;
|
|
330
330
|
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
331
|
-
if (kind && result) __defProp$
|
|
331
|
+
if (kind && result) __defProp$13(target, key, result);
|
|
332
332
|
return result;
|
|
333
333
|
};
|
|
334
|
-
var __publicField$
|
|
334
|
+
var __publicField$13 = (obj, key, value) => __defNormalProp$13(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
335
335
|
var MessagingEndpoint = class extends SmrtObject {
|
|
336
336
|
tenantId = "";
|
|
337
337
|
profileId = null;
|
|
@@ -376,16 +376,16 @@ var MessagingEndpoint = class extends SmrtObject {
|
|
|
376
376
|
return value.length <= 4 ? "••••" : `\u2022\u2022\u2022\u2022${value.slice(-4)}`;
|
|
377
377
|
}
|
|
378
378
|
};
|
|
379
|
-
__publicField$
|
|
380
|
-
__decorateClass$
|
|
381
|
-
__decorateClass$
|
|
382
|
-
__decorateClass$
|
|
383
|
-
__decorateClass$
|
|
384
|
-
__decorateClass$
|
|
379
|
+
__publicField$13(MessagingEndpoint, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
380
|
+
__decorateClass$13([tenantId()], MessagingEndpoint.prototype, "tenantId", 2);
|
|
381
|
+
__decorateClass$13([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], MessagingEndpoint.prototype, "profileId", 2);
|
|
382
|
+
__decorateClass$13([field({ required: true })], MessagingEndpoint.prototype, "label", 2);
|
|
383
|
+
__decorateClass$13([field({ required: true })], MessagingEndpoint.prototype, "channel", 2);
|
|
384
|
+
__decorateClass$13([field({
|
|
385
385
|
required: true,
|
|
386
386
|
sensitive: true
|
|
387
387
|
})], MessagingEndpoint.prototype, "address", 2);
|
|
388
|
-
MessagingEndpoint = __decorateClass$
|
|
388
|
+
MessagingEndpoint = __decorateClass$13([TenantScoped({ mode: "required" }), smrt({
|
|
389
389
|
tableName: "messaging_endpoints",
|
|
390
390
|
api: { include: ["list", "get"] },
|
|
391
391
|
mcp: { include: ["list", "get"] },
|
|
@@ -403,6 +403,262 @@ var MessagingEndpointCollection = class extends SmrtCollection {
|
|
|
403
403
|
static _itemClass = MessagingEndpoint;
|
|
404
404
|
};
|
|
405
405
|
//#endregion
|
|
406
|
+
//#region src/models/NotificationDelivery.ts
|
|
407
|
+
var __defProp$12 = Object.defineProperty;
|
|
408
|
+
var __getOwnPropDesc$12 = Object.getOwnPropertyDescriptor;
|
|
409
|
+
var __defNormalProp$12 = (obj, key, value) => key in obj ? __defProp$12(obj, key, {
|
|
410
|
+
enumerable: true,
|
|
411
|
+
configurable: true,
|
|
412
|
+
writable: true,
|
|
413
|
+
value
|
|
414
|
+
}) : obj[key] = value;
|
|
415
|
+
var __decorateClass$12 = (decorators, target, key, kind) => {
|
|
416
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$12(target, key) : target;
|
|
417
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
418
|
+
if (kind && result) __defProp$12(target, key, result);
|
|
419
|
+
return result;
|
|
420
|
+
};
|
|
421
|
+
var __publicField$12 = (obj, key, value) => __defNormalProp$12(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
422
|
+
var NOTIFICATION_DELIVERY_CHANNELS = ["email"];
|
|
423
|
+
var TRANSITION_COLUMNS = {
|
|
424
|
+
messageId: "message_id",
|
|
425
|
+
status: "status",
|
|
426
|
+
attempts: "attempts",
|
|
427
|
+
nextAttemptAt: "next_attempt_at",
|
|
428
|
+
leaseToken: "lease_token",
|
|
429
|
+
leaseExpiresAt: "lease_expires_at",
|
|
430
|
+
handedOffAt: "handed_off_at",
|
|
431
|
+
deliveredAt: "delivered_at",
|
|
432
|
+
providerMessageId: "provider_message_id",
|
|
433
|
+
lastError: "last_error"
|
|
434
|
+
};
|
|
435
|
+
var NotificationDelivery = class extends SmrtObject {
|
|
436
|
+
tenantId = "";
|
|
437
|
+
recipientUserId = "";
|
|
438
|
+
channel = "email";
|
|
439
|
+
sourceRef = "";
|
|
440
|
+
kind = "";
|
|
441
|
+
subject = "";
|
|
442
|
+
textBody = "";
|
|
443
|
+
href = "";
|
|
444
|
+
recipientAddress = "";
|
|
445
|
+
/** Display name snapshot from the host recipient resolver. */
|
|
446
|
+
recipientName = "";
|
|
447
|
+
accountId = "";
|
|
448
|
+
messageId = null;
|
|
449
|
+
status = "pending";
|
|
450
|
+
/** Compare-and-set counter; bumped by every transition. */
|
|
451
|
+
stateVersion = 0;
|
|
452
|
+
/** Unique id of the transition that wrote the current state. */
|
|
453
|
+
transitionId = "";
|
|
454
|
+
/** Leases acquired so far (provider handoffs are a subset). */
|
|
455
|
+
attempts = 0;
|
|
456
|
+
/** Lease budget before the delivery fails for good. */
|
|
457
|
+
maxAttempts = 5;
|
|
458
|
+
/** Earliest time a dispatcher may lease a `pending` row. */
|
|
459
|
+
nextAttemptAt = /* @__PURE__ */ new Date();
|
|
460
|
+
/** Proof of the current lease holder; cleared when the lease ends. */
|
|
461
|
+
leaseToken = null;
|
|
462
|
+
/** When the current lease lapses and the row becomes recoverable. */
|
|
463
|
+
leaseExpiresAt = null;
|
|
464
|
+
/** Set just before the current attempt reaches the provider. */
|
|
465
|
+
handedOffAt = null;
|
|
466
|
+
/** When the provider accepted the message. */
|
|
467
|
+
deliveredAt = null;
|
|
468
|
+
/** Provider message id, when the provider returns one. */
|
|
469
|
+
providerMessageId = "";
|
|
470
|
+
/** The latest failure or uncertainty reason. */
|
|
471
|
+
lastError = "";
|
|
472
|
+
constructor(options = {}) {
|
|
473
|
+
super(options);
|
|
474
|
+
const assign = (key) => {
|
|
475
|
+
if (options[key] !== void 0) this[key] = options[key];
|
|
476
|
+
};
|
|
477
|
+
for (const key of [
|
|
478
|
+
"tenantId",
|
|
479
|
+
"recipientUserId",
|
|
480
|
+
"channel",
|
|
481
|
+
"sourceRef",
|
|
482
|
+
"kind",
|
|
483
|
+
"subject",
|
|
484
|
+
"textBody",
|
|
485
|
+
"href",
|
|
486
|
+
"recipientAddress",
|
|
487
|
+
"recipientName",
|
|
488
|
+
"accountId",
|
|
489
|
+
"messageId",
|
|
490
|
+
"status",
|
|
491
|
+
"stateVersion",
|
|
492
|
+
"transitionId",
|
|
493
|
+
"attempts",
|
|
494
|
+
"maxAttempts",
|
|
495
|
+
"nextAttemptAt",
|
|
496
|
+
"leaseToken",
|
|
497
|
+
"leaseExpiresAt",
|
|
498
|
+
"handedOffAt",
|
|
499
|
+
"deliveredAt",
|
|
500
|
+
"providerMessageId",
|
|
501
|
+
"lastError"
|
|
502
|
+
]) assign(key);
|
|
503
|
+
}
|
|
504
|
+
/** Whether no further automatic work will happen on this row. */
|
|
505
|
+
get isSettled() {
|
|
506
|
+
return this.status === "sent" || this.status === "failed" || this.status === "cancelled" || this.status === "uncertain";
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Apply `changes` only if the stored row is still at `stateVersion` (and,
|
|
510
|
+
* when `leaseToken` is given, still held by that lease). Returns whether
|
|
511
|
+
* this caller won; on success the instance reflects the stored row.
|
|
512
|
+
*
|
|
513
|
+
* PostgreSQL and remote LibSQL evaluate the guard in one conditional
|
|
514
|
+
* UPDATE; local embedded engines use the process write queue, matching
|
|
515
|
+
* `Message.send()`'s claim. The durable row is re-read either way, and the
|
|
516
|
+
* write is recognised by its unique `transitionId`, because DuckDB's update
|
|
517
|
+
* result over-reports affected rows.
|
|
518
|
+
*/
|
|
519
|
+
async transition(changes, expected = {}) {
|
|
520
|
+
if (!this.id) throw new Error("Cannot transition an unsaved delivery.");
|
|
521
|
+
const fromVersion = this.stateVersion;
|
|
522
|
+
const nextVersion = fromVersion + 1;
|
|
523
|
+
const transitionId = crypto.randomUUID();
|
|
524
|
+
const values = {
|
|
525
|
+
state_version: nextVersion,
|
|
526
|
+
transition_id: transitionId,
|
|
527
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
528
|
+
};
|
|
529
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
530
|
+
const column = TRANSITION_COLUMNS[key];
|
|
531
|
+
if (!column) throw new Error(`'${key}' is not a transition field.`);
|
|
532
|
+
values[column] = value;
|
|
533
|
+
}
|
|
534
|
+
const matches = (row) => row !== null && Number(row.state_version) === fromVersion && (expected.leaseToken === void 0 || row.lease_token === expected.leaseToken);
|
|
535
|
+
const useEmbeddedFallback = usesEmbeddedRevisionFallback(this.db);
|
|
536
|
+
await withEmbeddedWriteQueue(this.db, useEmbeddedFallback, async () => {
|
|
537
|
+
if (useEmbeddedFallback) {
|
|
538
|
+
const current = await this.getCanonicalPersistedRow({ id: this.id });
|
|
539
|
+
if (!matches(current)) return;
|
|
540
|
+
await this.db.upsert(this.tableName, ["id"], {
|
|
541
|
+
...current,
|
|
542
|
+
...values
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
await this.db.update(this.tableName, {
|
|
547
|
+
id: this.id,
|
|
548
|
+
tenant_id: this.tenantId,
|
|
549
|
+
state_version: fromVersion,
|
|
550
|
+
...expected.leaseToken === void 0 ? {} : { lease_token: expected.leaseToken }
|
|
551
|
+
}, values);
|
|
552
|
+
});
|
|
553
|
+
const verified = await this.getCanonicalPersistedRow({ id: this.id });
|
|
554
|
+
if (!verified || verified.transition_id !== transitionId) return false;
|
|
555
|
+
await this.completePersistedUpdate(verified);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
__publicField$12(NotificationDelivery, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
560
|
+
__decorateClass$12([tenantId()], NotificationDelivery.prototype, "tenantId", 2);
|
|
561
|
+
__decorateClass$12([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], NotificationDelivery.prototype, "recipientUserId", 2);
|
|
562
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "channel", 2);
|
|
563
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "sourceRef", 2);
|
|
564
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "kind", 2);
|
|
565
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "subject", 2);
|
|
566
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "textBody", 2);
|
|
567
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "href", 2);
|
|
568
|
+
__decorateClass$12([field({ required: true })], NotificationDelivery.prototype, "recipientAddress", 2);
|
|
569
|
+
__decorateClass$12([foreignKey("Account")], NotificationDelivery.prototype, "accountId", 2);
|
|
570
|
+
__decorateClass$12([foreignKey("Message")], NotificationDelivery.prototype, "messageId", 2);
|
|
571
|
+
NotificationDelivery = __decorateClass$12([TenantScoped({ mode: "required" }), smrt({
|
|
572
|
+
tableName: "notification_deliveries",
|
|
573
|
+
api: false,
|
|
574
|
+
mcp: false,
|
|
575
|
+
cli: false,
|
|
576
|
+
conflictColumns: [
|
|
577
|
+
"tenant_id",
|
|
578
|
+
"recipient_user_id",
|
|
579
|
+
"channel",
|
|
580
|
+
"source_ref"
|
|
581
|
+
],
|
|
582
|
+
indexes: [{
|
|
583
|
+
name: "notification_deliveries_due_idx",
|
|
584
|
+
columns: ["status", "nextAttemptAt"]
|
|
585
|
+
}, {
|
|
586
|
+
name: "notification_deliveries_lease_idx",
|
|
587
|
+
columns: ["status", "leaseExpiresAt"]
|
|
588
|
+
}]
|
|
589
|
+
})], NotificationDelivery);
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/collections/NotificationDeliveryCollection.ts
|
|
592
|
+
var NotificationDeliveryCollection = class extends SmrtCollection {
|
|
593
|
+
static _itemClass = NotificationDelivery;
|
|
594
|
+
};
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/models/NotificationOptOut.ts
|
|
597
|
+
var __defProp$11 = Object.defineProperty;
|
|
598
|
+
var __getOwnPropDesc$11 = Object.getOwnPropertyDescriptor;
|
|
599
|
+
var __defNormalProp$11 = (obj, key, value) => key in obj ? __defProp$11(obj, key, {
|
|
600
|
+
enumerable: true,
|
|
601
|
+
configurable: true,
|
|
602
|
+
writable: true,
|
|
603
|
+
value
|
|
604
|
+
}) : obj[key] = value;
|
|
605
|
+
var __decorateClass$11 = (decorators, target, key, kind) => {
|
|
606
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$11(target, key) : target;
|
|
607
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
608
|
+
if (kind && result) __defProp$11(target, key, result);
|
|
609
|
+
return result;
|
|
610
|
+
};
|
|
611
|
+
var __publicField$11 = (obj, key, value) => __defNormalProp$11(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
612
|
+
var NOTIFICATION_OPT_OUT_SCOPE_PATTERN = /^(?:\*|[a-z][a-z0-9-]*:[A-Za-z0-9._:-]{1,200})$/;
|
|
613
|
+
var NOTIFICATION_OPT_OUT_ALL_SCOPE = "*";
|
|
614
|
+
function isValidNotificationOptOutScope(scope) {
|
|
615
|
+
return typeof scope === "string" && NOTIFICATION_OPT_OUT_SCOPE_PATTERN.test(scope);
|
|
616
|
+
}
|
|
617
|
+
var NotificationOptOut = class extends SmrtObject {
|
|
618
|
+
tenantId = "";
|
|
619
|
+
recipientUserId = "";
|
|
620
|
+
channel = "email";
|
|
621
|
+
scope = "";
|
|
622
|
+
/** When the opt-out took effect; null once the person opted back in. */
|
|
623
|
+
optedOutAt = null;
|
|
624
|
+
/** Who last changed the row: the person's user id, or `system` for host code. */
|
|
625
|
+
updatedBy = "";
|
|
626
|
+
constructor(options = {}) {
|
|
627
|
+
super(options);
|
|
628
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
629
|
+
if (options.recipientUserId !== void 0) this.recipientUserId = options.recipientUserId;
|
|
630
|
+
if (options.channel !== void 0) this.channel = options.channel;
|
|
631
|
+
if (options.scope !== void 0) this.scope = options.scope;
|
|
632
|
+
if (options.optedOutAt !== void 0) this.optedOutAt = options.optedOutAt;
|
|
633
|
+
if (options.updatedBy !== void 0) this.updatedBy = options.updatedBy;
|
|
634
|
+
}
|
|
635
|
+
get isOptedOut() {
|
|
636
|
+
return this.optedOutAt !== null;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
__publicField$11(NotificationOptOut, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
640
|
+
__decorateClass$11([tenantId()], NotificationOptOut.prototype, "tenantId", 2);
|
|
641
|
+
__decorateClass$11([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], NotificationOptOut.prototype, "recipientUserId", 2);
|
|
642
|
+
__decorateClass$11([field({ required: true })], NotificationOptOut.prototype, "channel", 2);
|
|
643
|
+
__decorateClass$11([field({ required: true })], NotificationOptOut.prototype, "scope", 2);
|
|
644
|
+
NotificationOptOut = __decorateClass$11([TenantScoped({ mode: "required" }), smrt({
|
|
645
|
+
tableName: "notification_opt_outs",
|
|
646
|
+
api: false,
|
|
647
|
+
mcp: false,
|
|
648
|
+
cli: false,
|
|
649
|
+
conflictColumns: [
|
|
650
|
+
"tenant_id",
|
|
651
|
+
"recipient_user_id",
|
|
652
|
+
"channel",
|
|
653
|
+
"scope"
|
|
654
|
+
]
|
|
655
|
+
})], NotificationOptOut);
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/collections/NotificationOptOutCollection.ts
|
|
658
|
+
var NotificationOptOutCollection = class extends SmrtCollection {
|
|
659
|
+
static _itemClass = NotificationOptOut;
|
|
660
|
+
};
|
|
661
|
+
//#endregion
|
|
406
662
|
//#region src/models/PersonaMessageRoute.ts
|
|
407
663
|
var __defProp$10 = Object.defineProperty;
|
|
408
664
|
var __getOwnPropDesc$10 = Object.getOwnPropertyDescriptor;
|
|
@@ -688,7 +944,7 @@ var EmailAccount = class extends Account {
|
|
|
688
944
|
for (const folderName of foldersToSync) try {
|
|
689
945
|
let folder = await folderCollection.getByPath(accountId, folderName);
|
|
690
946
|
if (!folder) {
|
|
691
|
-
const { EmailFolder } = await import("./chunks/EmailFolder-
|
|
947
|
+
const { EmailFolder } = await import("./chunks/EmailFolder-Cz2JU5Z1.js").then((n) => n.n);
|
|
692
948
|
folder = new EmailFolder({
|
|
693
949
|
...this.childOptions(),
|
|
694
950
|
accountId,
|
|
@@ -713,7 +969,7 @@ var EmailAccount = class extends Account {
|
|
|
713
969
|
result.messagesSkipped++;
|
|
714
970
|
continue;
|
|
715
971
|
}
|
|
716
|
-
const { Email } = await import("./chunks/Email-
|
|
972
|
+
const { Email } = await import("./chunks/Email-CBIrfFLm.js").then((n) => n.n);
|
|
717
973
|
let email = existingEmail;
|
|
718
974
|
if (!email) {
|
|
719
975
|
email = new Email({
|
|
@@ -1924,7 +2180,10 @@ ZulipAccount = __decorateClass([smrt({
|
|
|
1924
2180
|
})], ZulipAccount);
|
|
1925
2181
|
//#endregion
|
|
1926
2182
|
//#region src/senders/EmailSender.ts
|
|
1927
|
-
var EmailSender_exports = /* @__PURE__ */ __exportAll({
|
|
2183
|
+
var EmailSender_exports = /* @__PURE__ */ __exportAll({
|
|
2184
|
+
EmailSender: () => EmailSender,
|
|
2185
|
+
isDefiniteSendRejection: () => isDefiniteSendRejection
|
|
2186
|
+
});
|
|
1928
2187
|
var EmailSender = class {
|
|
1929
2188
|
providerType = "email";
|
|
1930
2189
|
client;
|
|
@@ -1987,6 +2246,7 @@ var EmailSender = class {
|
|
|
1987
2246
|
return {
|
|
1988
2247
|
success: false,
|
|
1989
2248
|
error: error instanceof Error ? error.message : String(error),
|
|
2249
|
+
...isDefiniteSendRejection(error) ? {} : { outcomeUnknown: true },
|
|
1990
2250
|
sentAt: /* @__PURE__ */ new Date()
|
|
1991
2251
|
};
|
|
1992
2252
|
}
|
|
@@ -2033,6 +2293,31 @@ var EmailSender = class {
|
|
|
2033
2293
|
return resolved;
|
|
2034
2294
|
}
|
|
2035
2295
|
};
|
|
2296
|
+
var DEFINITE_EMAIL_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2297
|
+
"AUTHENTICATION_ERROR",
|
|
2298
|
+
"AUTHORIZATION_ERROR",
|
|
2299
|
+
"INVALID_MESSAGE",
|
|
2300
|
+
"ATTACHMENT_ERROR"
|
|
2301
|
+
]);
|
|
2302
|
+
var NEVER_CONNECTED_CODES = /* @__PURE__ */ new Set([
|
|
2303
|
+
"ECONNREFUSED",
|
|
2304
|
+
"ENOTFOUND",
|
|
2305
|
+
"EHOSTUNREACH",
|
|
2306
|
+
"EAI_AGAIN"
|
|
2307
|
+
]);
|
|
2308
|
+
function isDefiniteSendRejection(error) {
|
|
2309
|
+
if (!error || typeof error !== "object") return false;
|
|
2310
|
+
const candidates = [error, error.cause].filter((value) => !!value && typeof value === "object");
|
|
2311
|
+
for (const candidate of candidates) {
|
|
2312
|
+
if (typeof candidate.responseCode === "number") return true;
|
|
2313
|
+
if (typeof candidate.code === "string") {
|
|
2314
|
+
if (DEFINITE_EMAIL_ERROR_CODES.has(candidate.code)) return true;
|
|
2315
|
+
if (NEVER_CONNECTED_CODES.has(candidate.code)) return true;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
const sendError = error;
|
|
2319
|
+
return sendError.code === "SEND_ERROR" && Array.isArray(sendError.accepted) && sendError.accepted.length === 0;
|
|
2320
|
+
}
|
|
2036
2321
|
//#endregion
|
|
2037
2322
|
//#region src/services/MessagingSettingsService.ts
|
|
2038
2323
|
var MessagingSettingsService = class {
|
|
@@ -2231,6 +2516,672 @@ var MessagingSettingsService = class {
|
|
|
2231
2516
|
}
|
|
2232
2517
|
};
|
|
2233
2518
|
//#endregion
|
|
2519
|
+
//#region src/services/NotificationOptOutService.ts
|
|
2520
|
+
var NotificationOptOutDeniedError = class extends Error {
|
|
2521
|
+
constructor(reason, message) {
|
|
2522
|
+
super(message);
|
|
2523
|
+
this.reason = reason;
|
|
2524
|
+
this.name = "NotificationOptOutDeniedError";
|
|
2525
|
+
}
|
|
2526
|
+
reason;
|
|
2527
|
+
};
|
|
2528
|
+
function requireText$2(value, name) {
|
|
2529
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
2530
|
+
if (!text) throw new Error(`A notification opt-out ${name} is required.`);
|
|
2531
|
+
return text;
|
|
2532
|
+
}
|
|
2533
|
+
function validate(key) {
|
|
2534
|
+
const channel = key.channel;
|
|
2535
|
+
if (!NOTIFICATION_DELIVERY_CHANNELS.includes(channel)) throw new Error(`Unknown notification channel '${String(channel)}'.`);
|
|
2536
|
+
if (!isValidNotificationOptOutScope(key.scope)) throw new Error(`Invalid notification opt-out scope '${String(key.scope)}': use '*' or a key like 'project:<id>'.`);
|
|
2537
|
+
return {
|
|
2538
|
+
tenantId: requireText$2(key.tenantId, "tenant"),
|
|
2539
|
+
recipientUserId: requireText$2(key.recipientUserId, "recipient"),
|
|
2540
|
+
channel,
|
|
2541
|
+
scope: key.scope
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
function authorize(principal, tenantId, recipientUserId) {
|
|
2545
|
+
if (!principal) return "system";
|
|
2546
|
+
if (!principal.tenantId || principal.tenantId !== tenantId) throw new NotificationOptOutDeniedError("tenant", "Principal does not belong to this tenant.");
|
|
2547
|
+
if (!principal.userId || principal.userId !== recipientUserId) throw new NotificationOptOutDeniedError("principal", "Only the recipient may change or list their notification opt-outs.");
|
|
2548
|
+
return principal.userId;
|
|
2549
|
+
}
|
|
2550
|
+
var NotificationOptOutService = class {
|
|
2551
|
+
constructor(options) {
|
|
2552
|
+
this.options = options;
|
|
2553
|
+
}
|
|
2554
|
+
options;
|
|
2555
|
+
collection() {
|
|
2556
|
+
return NotificationOptOutCollection.create({ db: this.options.db });
|
|
2557
|
+
}
|
|
2558
|
+
/** Opt the recipient out of `scope`; a repeat leaves the row untouched. */
|
|
2559
|
+
async optOut(change) {
|
|
2560
|
+
return this.change(change, true);
|
|
2561
|
+
}
|
|
2562
|
+
/** Clear an opt-out; a repeat, or no row at all, is a no-op. */
|
|
2563
|
+
async optIn(change) {
|
|
2564
|
+
return this.change(change, false);
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Whether the recipient has opted out of `scope` (a `*` row matches every
|
|
2568
|
+
* scope). Trusted read: no principal, always under the tenant's scope.
|
|
2569
|
+
*/
|
|
2570
|
+
async isOptedOut(key) {
|
|
2571
|
+
const { tenantId, recipientUserId, channel, scope } = validate(key);
|
|
2572
|
+
const scopes = scope === "*" ? [scope] : [scope, "*"];
|
|
2573
|
+
return withTenant({ tenantId }, async () => {
|
|
2574
|
+
return (await (await this.collection()).list({ where: {
|
|
2575
|
+
tenantId,
|
|
2576
|
+
recipientUserId,
|
|
2577
|
+
channel,
|
|
2578
|
+
"scope in": scopes
|
|
2579
|
+
} })).some((row) => row.optedOutAt !== null);
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
/** The recipient's own active opt-outs, in one tenant. */
|
|
2583
|
+
async listForRecipient(request) {
|
|
2584
|
+
const tenantId = requireText$2(request.tenantId, "tenant");
|
|
2585
|
+
const recipientUserId = requireText$2(request.recipientUserId, "recipient");
|
|
2586
|
+
if (!request.principal) throw new NotificationOptOutDeniedError("principal", "A principal is required to list opt-outs.");
|
|
2587
|
+
authorize(request.principal, tenantId, recipientUserId);
|
|
2588
|
+
return withTenant({ tenantId }, async () => {
|
|
2589
|
+
return (await (await this.collection()).list({
|
|
2590
|
+
where: {
|
|
2591
|
+
tenantId,
|
|
2592
|
+
recipientUserId
|
|
2593
|
+
},
|
|
2594
|
+
orderBy: "scope ASC"
|
|
2595
|
+
})).filter((row) => row.optedOutAt !== null);
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
async change(change, optedOut) {
|
|
2599
|
+
const key = validate(change);
|
|
2600
|
+
const updatedBy = authorize(change.principal, key.tenantId, key.recipientUserId);
|
|
2601
|
+
const { tenantId, recipientUserId, channel, scope } = key;
|
|
2602
|
+
return withTenant({ tenantId }, async () => {
|
|
2603
|
+
const rows = await this.collection();
|
|
2604
|
+
const find = async () => (await rows.list({
|
|
2605
|
+
where: {
|
|
2606
|
+
tenantId,
|
|
2607
|
+
recipientUserId,
|
|
2608
|
+
channel,
|
|
2609
|
+
scope
|
|
2610
|
+
},
|
|
2611
|
+
limit: 1
|
|
2612
|
+
}))[0];
|
|
2613
|
+
const apply = async (row) => {
|
|
2614
|
+
if (row.isOptedOut === optedOut) return row;
|
|
2615
|
+
row.optedOutAt = optedOut ? /* @__PURE__ */ new Date() : null;
|
|
2616
|
+
row.updatedBy = updatedBy;
|
|
2617
|
+
await row.save();
|
|
2618
|
+
return row;
|
|
2619
|
+
};
|
|
2620
|
+
const existing = await find();
|
|
2621
|
+
if (existing) return apply(existing);
|
|
2622
|
+
if (!optedOut) return new NotificationOptOut({
|
|
2623
|
+
tenantId,
|
|
2624
|
+
recipientUserId,
|
|
2625
|
+
channel,
|
|
2626
|
+
scope,
|
|
2627
|
+
optedOutAt: null,
|
|
2628
|
+
updatedBy
|
|
2629
|
+
});
|
|
2630
|
+
try {
|
|
2631
|
+
return await rows.create({
|
|
2632
|
+
_insertOnly: true,
|
|
2633
|
+
tenantId,
|
|
2634
|
+
recipientUserId,
|
|
2635
|
+
channel,
|
|
2636
|
+
scope,
|
|
2637
|
+
optedOutAt: /* @__PURE__ */ new Date(),
|
|
2638
|
+
updatedBy
|
|
2639
|
+
});
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
if (!isUniqueViolationError(error)) throw error;
|
|
2642
|
+
const winner = await find();
|
|
2643
|
+
if (!winner) throw error;
|
|
2644
|
+
return apply(winner);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
function optOutSuppression(service, scopeOf) {
|
|
2650
|
+
return async (request) => {
|
|
2651
|
+
const scope = await scopeOf(request);
|
|
2652
|
+
if (scope === null || scope === void 0) return false;
|
|
2653
|
+
return service.isOptedOut({
|
|
2654
|
+
tenantId: request.tenantId,
|
|
2655
|
+
recipientUserId: request.recipientUserId,
|
|
2656
|
+
channel: request.channel,
|
|
2657
|
+
scope
|
|
2658
|
+
});
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
//#endregion
|
|
2662
|
+
//#region src/services/NotificationOutboxService.ts
|
|
2663
|
+
var messageSendTransport = {
|
|
2664
|
+
name: "message-send",
|
|
2665
|
+
idempotent: false,
|
|
2666
|
+
async send({ message }) {
|
|
2667
|
+
const result = await message.send();
|
|
2668
|
+
if (result.success) return {
|
|
2669
|
+
status: "accepted",
|
|
2670
|
+
...result.providerMessageId ? { providerMessageId: result.providerMessageId } : {}
|
|
2671
|
+
};
|
|
2672
|
+
if (result.outcomeUnknown || message.sendStatus === "sending" || message.sendStatus === "sent") return {
|
|
2673
|
+
status: "unknown",
|
|
2674
|
+
error: result.error ?? "The provider did not confirm the outcome."
|
|
2675
|
+
};
|
|
2676
|
+
return {
|
|
2677
|
+
status: "rejected",
|
|
2678
|
+
error: result.error ?? "Send failed",
|
|
2679
|
+
retryable: true
|
|
2680
|
+
};
|
|
2681
|
+
},
|
|
2682
|
+
async reconcile({ message }) {
|
|
2683
|
+
const messages = await MessageCollection.create({ db: message.db });
|
|
2684
|
+
const stored = message.id ? await messages.get({ id: message.id }) : null;
|
|
2685
|
+
switch (stored?.sendStatus) {
|
|
2686
|
+
case "sent": return { status: "accepted" };
|
|
2687
|
+
case "failed":
|
|
2688
|
+
if (stored.isSendOutcomeUnknown()) return {
|
|
2689
|
+
status: "unknown",
|
|
2690
|
+
error: stored.sendError || "The provider did not confirm the outcome."
|
|
2691
|
+
};
|
|
2692
|
+
return {
|
|
2693
|
+
status: "rejected",
|
|
2694
|
+
error: stored.sendError || "Send failed",
|
|
2695
|
+
retryable: true
|
|
2696
|
+
};
|
|
2697
|
+
case "draft":
|
|
2698
|
+
case "pending":
|
|
2699
|
+
case "scheduled": return { status: "not-sent" };
|
|
2700
|
+
default: return {
|
|
2701
|
+
status: "unknown",
|
|
2702
|
+
error: "The provider handoff did not record an outcome."
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
var NotificationOutboxDeniedError = class extends Error {
|
|
2708
|
+
constructor(reason, message) {
|
|
2709
|
+
super(message);
|
|
2710
|
+
this.reason = reason;
|
|
2711
|
+
this.name = "NotificationOutboxDeniedError";
|
|
2712
|
+
}
|
|
2713
|
+
reason;
|
|
2714
|
+
};
|
|
2715
|
+
var UUID_PATTERN$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2716
|
+
var DEFAULT_LEASE_MS = 5 * 6e4;
|
|
2717
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
2718
|
+
var DEFAULT_LIMIT$1 = 25;
|
|
2719
|
+
var MAX_LIMIT$1 = 200;
|
|
2720
|
+
function defaultRetryDelayMs(attempts) {
|
|
2721
|
+
return Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 60 * 6e4);
|
|
2722
|
+
}
|
|
2723
|
+
function requireText$1(value, name) {
|
|
2724
|
+
const text = value?.trim();
|
|
2725
|
+
if (!text) throw new Error(`A notification ${name} is required.`);
|
|
2726
|
+
return text;
|
|
2727
|
+
}
|
|
2728
|
+
function errorText(error) {
|
|
2729
|
+
return error instanceof Error ? error.message : String(error);
|
|
2730
|
+
}
|
|
2731
|
+
async function notificationDeliveryMessageId(deliveryId) {
|
|
2732
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`smrt-messages:notification-delivery:${deliveryId}`));
|
|
2733
|
+
const bytes = new Uint8Array(digest).slice(0, 16);
|
|
2734
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
2735
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2736
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
|
|
2737
|
+
return [
|
|
2738
|
+
hex.slice(0, 4),
|
|
2739
|
+
hex.slice(4, 6),
|
|
2740
|
+
hex.slice(6, 8),
|
|
2741
|
+
hex.slice(8, 10),
|
|
2742
|
+
hex.slice(10, 16)
|
|
2743
|
+
].map((group) => group.join("")).join("-");
|
|
2744
|
+
}
|
|
2745
|
+
function notificationDeliveryCorrelationId(deliveryId) {
|
|
2746
|
+
return `notification-delivery:${deliveryId}`;
|
|
2747
|
+
}
|
|
2748
|
+
function buildNotificationLink(baseUrl, path) {
|
|
2749
|
+
let base;
|
|
2750
|
+
try {
|
|
2751
|
+
base = new URL(baseUrl);
|
|
2752
|
+
} catch {
|
|
2753
|
+
throw new Error("Notification link base URL must be an absolute URL.");
|
|
2754
|
+
}
|
|
2755
|
+
if (base.protocol !== "https:" && base.protocol !== "http:") throw new Error("Notification link base URL must use http or https.");
|
|
2756
|
+
if (base.username || base.password || base.search || base.hash) throw new Error("Notification link base URL must not carry credentials, a query, or a fragment.");
|
|
2757
|
+
if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//") || /[\\\s]/u.test(path) || /[\u0000-\u001f\u007f]/u.test(path)) throw new NotificationOutboxDeniedError("link", "Notification links must be a host-relative path such as /inbox/123.");
|
|
2758
|
+
const prefix = base.pathname.replace(/\/+$/u, "");
|
|
2759
|
+
const link = new URL(`${prefix}${path}`, base.origin);
|
|
2760
|
+
if (link.origin !== base.origin) throw new NotificationOutboxDeniedError("link", "Notification links must stay on the configured origin.");
|
|
2761
|
+
return link.toString();
|
|
2762
|
+
}
|
|
2763
|
+
var NotificationOutboxService = class {
|
|
2764
|
+
constructor(options) {
|
|
2765
|
+
this.options = options;
|
|
2766
|
+
buildNotificationLink(options.linkBaseUrl, "/");
|
|
2767
|
+
this.transport = options.transport ?? messageSendTransport;
|
|
2768
|
+
this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
2769
|
+
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2770
|
+
this.retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
|
|
2771
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2772
|
+
if (!Number.isInteger(this.leaseMs) || this.leaseMs < 1e3) throw new Error("Notification lease must be at least 1000 ms.");
|
|
2773
|
+
if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) throw new Error("Notification maxAttempts must be a positive integer.");
|
|
2774
|
+
}
|
|
2775
|
+
options;
|
|
2776
|
+
transport;
|
|
2777
|
+
leaseMs;
|
|
2778
|
+
maxAttempts;
|
|
2779
|
+
retryDelayMs;
|
|
2780
|
+
now;
|
|
2781
|
+
collection() {
|
|
2782
|
+
return NotificationDeliveryCollection.create({ db: this.options.db });
|
|
2783
|
+
}
|
|
2784
|
+
authorize(tenantId) {
|
|
2785
|
+
if (!this.options.principal) return;
|
|
2786
|
+
try {
|
|
2787
|
+
assertMessagingPermission(this.options.principal, tenantId, SEND_MESSAGES_PERMISSION);
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
throw new NotificationOutboxDeniedError("permission", errorText(error));
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
/** Store one email delivery for the event unless `sourceRef` already has one. */
|
|
2793
|
+
async enqueueEmail(input) {
|
|
2794
|
+
const tenantId = requireText$1(input.tenantId, "tenant");
|
|
2795
|
+
const recipientUserId = requireText$1(input.recipientUserId, "recipient");
|
|
2796
|
+
const sourceRef = requireText$1(input.sourceRef, "sourceRef");
|
|
2797
|
+
const kind = requireText$1(input.kind, "kind");
|
|
2798
|
+
const subject = requireText$1(input.subject, "subject");
|
|
2799
|
+
const text = requireText$1(input.text, "text");
|
|
2800
|
+
if (/[\r\n]/u.test(subject)) throw new Error("A notification subject must be a single line.");
|
|
2801
|
+
const channel = "email";
|
|
2802
|
+
this.authorize(tenantId);
|
|
2803
|
+
const href = buildNotificationLink(this.options.linkBaseUrl, input.path);
|
|
2804
|
+
return withTenant({ tenantId }, async () => {
|
|
2805
|
+
const deliveries = await this.collection();
|
|
2806
|
+
const findExisting = async () => (await deliveries.list({
|
|
2807
|
+
where: {
|
|
2808
|
+
tenantId,
|
|
2809
|
+
recipientUserId,
|
|
2810
|
+
channel,
|
|
2811
|
+
sourceRef
|
|
2812
|
+
},
|
|
2813
|
+
limit: 1
|
|
2814
|
+
}))[0];
|
|
2815
|
+
const existing = await findExisting();
|
|
2816
|
+
if (existing) return {
|
|
2817
|
+
delivery: existing,
|
|
2818
|
+
created: false
|
|
2819
|
+
};
|
|
2820
|
+
const recipient = await this.options.resolveRecipient({
|
|
2821
|
+
tenantId,
|
|
2822
|
+
recipientUserId,
|
|
2823
|
+
channel
|
|
2824
|
+
});
|
|
2825
|
+
const address = recipient?.address?.trim();
|
|
2826
|
+
if (!address || /[\s,;<>]/u.test(address) || !address.includes("@")) throw new NotificationOutboxDeniedError("recipient", "The host did not authorize an email recipient for this user.");
|
|
2827
|
+
const accountId = await this.requireSenderAccount(tenantId);
|
|
2828
|
+
const suppressed = await this.isSuppressed({
|
|
2829
|
+
tenantId,
|
|
2830
|
+
recipientUserId,
|
|
2831
|
+
channel,
|
|
2832
|
+
kind,
|
|
2833
|
+
sourceRef
|
|
2834
|
+
});
|
|
2835
|
+
try {
|
|
2836
|
+
return {
|
|
2837
|
+
delivery: await deliveries.create({
|
|
2838
|
+
_insertOnly: true,
|
|
2839
|
+
tenantId,
|
|
2840
|
+
recipientUserId,
|
|
2841
|
+
channel,
|
|
2842
|
+
sourceRef,
|
|
2843
|
+
kind,
|
|
2844
|
+
subject,
|
|
2845
|
+
textBody: text,
|
|
2846
|
+
href,
|
|
2847
|
+
recipientAddress: address,
|
|
2848
|
+
recipientName: recipient?.name?.trim() ?? "",
|
|
2849
|
+
accountId,
|
|
2850
|
+
messageId: null,
|
|
2851
|
+
status: suppressed ? "cancelled" : "pending",
|
|
2852
|
+
stateVersion: 0,
|
|
2853
|
+
transitionId: "",
|
|
2854
|
+
attempts: 0,
|
|
2855
|
+
maxAttempts: this.maxAttempts,
|
|
2856
|
+
nextAttemptAt: input.notBefore ?? this.now(),
|
|
2857
|
+
leaseToken: null,
|
|
2858
|
+
leaseExpiresAt: null,
|
|
2859
|
+
handedOffAt: null,
|
|
2860
|
+
deliveredAt: null,
|
|
2861
|
+
providerMessageId: "",
|
|
2862
|
+
lastError: suppressed ? "Suppressed by recipient preference." : ""
|
|
2863
|
+
}),
|
|
2864
|
+
created: true
|
|
2865
|
+
};
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
if (!isUniqueViolationError(error)) throw error;
|
|
2868
|
+
const winner = await findExisting();
|
|
2869
|
+
if (!winner) throw error;
|
|
2870
|
+
return {
|
|
2871
|
+
delivery: winner,
|
|
2872
|
+
created: false
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
/** Lease and deliver due rows; returns what happened to each leased row. */
|
|
2878
|
+
async dispatchDue(options = {}) {
|
|
2879
|
+
const limit = Math.min(options.limit ?? DEFAULT_LIMIT$1, MAX_LIMIT$1);
|
|
2880
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("Notification dispatch limit must be a positive integer.");
|
|
2881
|
+
const candidates = await this.findDue(options.tenantIds, limit);
|
|
2882
|
+
const report = {
|
|
2883
|
+
leased: 0,
|
|
2884
|
+
outcomes: []
|
|
2885
|
+
};
|
|
2886
|
+
for (const candidate of candidates) {
|
|
2887
|
+
if (report.leased >= limit) break;
|
|
2888
|
+
const outcome = await withTenant({ tenantId: candidate.tenantId }, () => this.dispatchOne(candidate));
|
|
2889
|
+
if (!outcome) continue;
|
|
2890
|
+
report.leased += 1;
|
|
2891
|
+
report.outcomes.push({
|
|
2892
|
+
deliveryId: candidate.id,
|
|
2893
|
+
tenantId: candidate.tenantId,
|
|
2894
|
+
outcome
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
return report;
|
|
2898
|
+
}
|
|
2899
|
+
/** Deliveries waiting for a reconciliation decision, oldest first. */
|
|
2900
|
+
async listUncertain(options) {
|
|
2901
|
+
const tenantId = requireText$1(options.tenantId, "tenant");
|
|
2902
|
+
this.authorize(tenantId);
|
|
2903
|
+
return withTenant({ tenantId }, async () => (await this.collection()).list({
|
|
2904
|
+
where: {
|
|
2905
|
+
tenantId,
|
|
2906
|
+
status: "uncertain"
|
|
2907
|
+
},
|
|
2908
|
+
orderBy: "handedOffAt ASC",
|
|
2909
|
+
limit: Math.min(options.limit ?? DEFAULT_LIMIT$1, MAX_LIMIT$1)
|
|
2910
|
+
}));
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Settle an `uncertain` delivery after checking the provider out of band:
|
|
2914
|
+
* `sent` (it arrived), `failed` (give up), or `retry` (send again,
|
|
2915
|
+
* accepting the risk of a duplicate; a message left claimed by a dead
|
|
2916
|
+
* sender is released first). Returns false when the row is not uncertain
|
|
2917
|
+
* any more, or for `retry` when the message is recorded as sent.
|
|
2918
|
+
*/
|
|
2919
|
+
async reconcile(input) {
|
|
2920
|
+
const tenantId = requireText$1(input.tenantId, "tenant");
|
|
2921
|
+
this.authorize(tenantId);
|
|
2922
|
+
if (!UUID_PATTERN$1.test(input.deliveryId ?? "")) return false;
|
|
2923
|
+
return withTenant({ tenantId }, async () => {
|
|
2924
|
+
const delivery = (await (await this.collection()).list({
|
|
2925
|
+
where: {
|
|
2926
|
+
tenantId,
|
|
2927
|
+
id: input.deliveryId
|
|
2928
|
+
},
|
|
2929
|
+
limit: 1
|
|
2930
|
+
}))[0];
|
|
2931
|
+
if (delivery?.status !== "uncertain") return false;
|
|
2932
|
+
const note = input.note?.trim();
|
|
2933
|
+
const now = this.now();
|
|
2934
|
+
switch (input.outcome) {
|
|
2935
|
+
case "sent": return delivery.transition({
|
|
2936
|
+
status: "sent",
|
|
2937
|
+
deliveredAt: now,
|
|
2938
|
+
...note ? { lastError: note } : {}
|
|
2939
|
+
});
|
|
2940
|
+
case "failed": return delivery.transition({
|
|
2941
|
+
status: "failed",
|
|
2942
|
+
lastError: note || delivery.lastError
|
|
2943
|
+
});
|
|
2944
|
+
case "retry": {
|
|
2945
|
+
const message = delivery.messageId ? await this.loadMessage(delivery.messageId) : null;
|
|
2946
|
+
if (message?.sendStatus === "sent") return false;
|
|
2947
|
+
if (message?.sendStatus === "sending" && !await message.abandonSendAttempt(note || "Released for an operator-approved retry.")) return false;
|
|
2948
|
+
return delivery.transition({
|
|
2949
|
+
status: "pending",
|
|
2950
|
+
nextAttemptAt: now,
|
|
2951
|
+
handedOffAt: null,
|
|
2952
|
+
attempts: Math.min(delivery.attempts, delivery.maxAttempts - 1),
|
|
2953
|
+
...note ? { lastError: note } : {}
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
default: throw new Error(`Unknown reconcile outcome '${input.outcome}'.`);
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
async requireSenderAccount(tenantId) {
|
|
2961
|
+
const accountId = (await this.options.resolveSenderAccountId({ tenantId }))?.trim();
|
|
2962
|
+
const account = accountId ? await (await AccountCollection.create({ db: this.options.db })).get({ id: accountId }) : null;
|
|
2963
|
+
if (!account || !(account instanceof EmailAccount) || account.tenantId !== tenantId || !account.isActive) throw new NotificationOutboxDeniedError("sender", "No active email sender account is configured for this tenant.");
|
|
2964
|
+
return account.id;
|
|
2965
|
+
}
|
|
2966
|
+
async isSuppressed(request) {
|
|
2967
|
+
return Boolean(await this.options.isSuppressed?.(request));
|
|
2968
|
+
}
|
|
2969
|
+
async findDue(tenantIds, limit) {
|
|
2970
|
+
const now = this.now();
|
|
2971
|
+
const read = async (tenantFilter) => {
|
|
2972
|
+
const deliveries = await this.collection();
|
|
2973
|
+
const [due, expired] = await Promise.all([deliveries.list({
|
|
2974
|
+
where: {
|
|
2975
|
+
...tenantFilter,
|
|
2976
|
+
status: "pending",
|
|
2977
|
+
"nextAttemptAt <=": now
|
|
2978
|
+
},
|
|
2979
|
+
orderBy: "nextAttemptAt ASC",
|
|
2980
|
+
limit
|
|
2981
|
+
}), deliveries.list({
|
|
2982
|
+
where: {
|
|
2983
|
+
...tenantFilter,
|
|
2984
|
+
status: "sending",
|
|
2985
|
+
"leaseExpiresAt <=": now
|
|
2986
|
+
},
|
|
2987
|
+
orderBy: "leaseExpiresAt ASC",
|
|
2988
|
+
limit
|
|
2989
|
+
})]);
|
|
2990
|
+
return [...expired, ...due];
|
|
2991
|
+
};
|
|
2992
|
+
return (tenantIds === void 0 ? await withSystemContext(() => read({})) : (await Promise.all([...new Set(tenantIds.filter(Boolean))].map((tenantId) => withTenant({ tenantId }, () => read({ tenantId }))))).flat()).slice(0, limit * 2);
|
|
2993
|
+
}
|
|
2994
|
+
/** Lease one candidate and carry it to an outcome; null when not leased. */
|
|
2995
|
+
async dispatchOne(candidate) {
|
|
2996
|
+
const delivery = (await (await this.collection()).list({
|
|
2997
|
+
where: {
|
|
2998
|
+
tenantId: candidate.tenantId,
|
|
2999
|
+
id: candidate.id
|
|
3000
|
+
},
|
|
3001
|
+
limit: 1
|
|
3002
|
+
}))[0];
|
|
3003
|
+
if (!delivery) return null;
|
|
3004
|
+
const now = this.now();
|
|
3005
|
+
const recovering = delivery.status === "sending" && delivery.leaseExpiresAt !== null && delivery.leaseExpiresAt.getTime() <= now.getTime();
|
|
3006
|
+
const due = delivery.status === "pending" && delivery.nextAttemptAt.getTime() <= now.getTime();
|
|
3007
|
+
if (!recovering && !due) return null;
|
|
3008
|
+
const leaseToken = crypto.randomUUID();
|
|
3009
|
+
const leaseExpiresAt = new Date(now.getTime() + this.leaseMs);
|
|
3010
|
+
const priorHandoff = recovering && delivery.handedOffAt !== null;
|
|
3011
|
+
if (!await delivery.transition({
|
|
3012
|
+
status: "sending",
|
|
3013
|
+
leaseToken,
|
|
3014
|
+
leaseExpiresAt,
|
|
3015
|
+
attempts: priorHandoff ? delivery.attempts : delivery.attempts + 1,
|
|
3016
|
+
...priorHandoff ? {} : { handedOffAt: null }
|
|
3017
|
+
})) return null;
|
|
3018
|
+
try {
|
|
3019
|
+
if (priorHandoff) {
|
|
3020
|
+
const resolved = await this.recoverHandoff(delivery, leaseToken);
|
|
3021
|
+
if (resolved) return resolved;
|
|
3022
|
+
}
|
|
3023
|
+
if (await this.isSuppressed({
|
|
3024
|
+
tenantId: delivery.tenantId,
|
|
3025
|
+
recipientUserId: delivery.recipientUserId,
|
|
3026
|
+
channel: delivery.channel,
|
|
3027
|
+
kind: delivery.kind,
|
|
3028
|
+
sourceRef: delivery.sourceRef
|
|
3029
|
+
})) return this.settle(delivery, leaseToken, {
|
|
3030
|
+
status: "cancelled",
|
|
3031
|
+
lastError: "Suppressed by recipient preference."
|
|
3032
|
+
});
|
|
3033
|
+
const message = await this.ensureMessage(delivery, leaseToken);
|
|
3034
|
+
if (!message) return "lease-lost";
|
|
3035
|
+
if (!await delivery.transition({ handedOffAt: this.now() }, { leaseToken })) return "lease-lost";
|
|
3036
|
+
let outcome;
|
|
3037
|
+
try {
|
|
3038
|
+
outcome = await this.transport.send({
|
|
3039
|
+
delivery,
|
|
3040
|
+
message,
|
|
3041
|
+
idempotencyKey: notificationDeliveryCorrelationId(delivery.id)
|
|
3042
|
+
});
|
|
3043
|
+
} catch (error) {
|
|
3044
|
+
outcome = {
|
|
3045
|
+
status: "unknown",
|
|
3046
|
+
error: errorText(error)
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
return this.applyOutcome(delivery, leaseToken, outcome);
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
if (delivery.handedOffAt !== null) throw error;
|
|
3052
|
+
return this.applyOutcome(delivery, leaseToken, {
|
|
3053
|
+
status: "rejected",
|
|
3054
|
+
error: errorText(error),
|
|
3055
|
+
retryable: true
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
async recoverHandoff(delivery, leaseToken) {
|
|
3060
|
+
const message = delivery.messageId ? await this.loadMessage(delivery.messageId) : null;
|
|
3061
|
+
let known = {
|
|
3062
|
+
status: "unknown",
|
|
3063
|
+
error: "Lease lapsed after the provider handoff; outcome unknown."
|
|
3064
|
+
};
|
|
3065
|
+
if (message && this.transport.reconcile) try {
|
|
3066
|
+
known = await this.transport.reconcile({
|
|
3067
|
+
delivery,
|
|
3068
|
+
message,
|
|
3069
|
+
idempotencyKey: notificationDeliveryCorrelationId(delivery.id)
|
|
3070
|
+
});
|
|
3071
|
+
} catch (error) {
|
|
3072
|
+
known = {
|
|
3073
|
+
status: "unknown",
|
|
3074
|
+
error: errorText(error)
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
if (known.status === "not-sent") {
|
|
3078
|
+
if (!await delivery.transition({
|
|
3079
|
+
handedOffAt: null,
|
|
3080
|
+
attempts: delivery.attempts + 1
|
|
3081
|
+
}, { leaseToken })) return "lease-lost";
|
|
3082
|
+
return null;
|
|
3083
|
+
}
|
|
3084
|
+
if (known.status === "unknown" && this.transport.idempotent) {
|
|
3085
|
+
if (!await delivery.transition({
|
|
3086
|
+
handedOffAt: null,
|
|
3087
|
+
attempts: delivery.attempts + 1
|
|
3088
|
+
}, { leaseToken })) return "lease-lost";
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
return this.applyOutcome(delivery, leaseToken, known);
|
|
3092
|
+
}
|
|
3093
|
+
async applyOutcome(delivery, leaseToken, outcome) {
|
|
3094
|
+
if (outcome.status === "accepted") return this.settle(delivery, leaseToken, {
|
|
3095
|
+
status: "sent",
|
|
3096
|
+
deliveredAt: this.now(),
|
|
3097
|
+
providerMessageId: outcome.providerMessageId ?? "",
|
|
3098
|
+
lastError: ""
|
|
3099
|
+
});
|
|
3100
|
+
if (outcome.status === "unknown") {
|
|
3101
|
+
if (this.transport.idempotent && delivery.attempts < delivery.maxAttempts) return this.scheduleRetry(delivery, leaseToken, outcome.error);
|
|
3102
|
+
return this.settle(delivery, leaseToken, {
|
|
3103
|
+
status: "uncertain",
|
|
3104
|
+
lastError: outcome.error
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
if (outcome.retryable && delivery.attempts < delivery.maxAttempts) return this.scheduleRetry(delivery, leaseToken, outcome.error);
|
|
3108
|
+
return this.settle(delivery, leaseToken, {
|
|
3109
|
+
status: "failed",
|
|
3110
|
+
lastError: outcome.error
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
async scheduleRetry(delivery, leaseToken, error) {
|
|
3114
|
+
const delay = this.retryDelayMs(delivery.attempts);
|
|
3115
|
+
return await this.endLease(delivery, leaseToken, {
|
|
3116
|
+
status: "pending",
|
|
3117
|
+
nextAttemptAt: new Date(this.now().getTime() + Math.max(0, delay)),
|
|
3118
|
+
handedOffAt: null,
|
|
3119
|
+
lastError: error
|
|
3120
|
+
}) ? "retry-scheduled" : "lease-lost";
|
|
3121
|
+
}
|
|
3122
|
+
/** End the lease in a terminal status; reports lease loss instead of overwriting. */
|
|
3123
|
+
async settle(delivery, leaseToken, changes) {
|
|
3124
|
+
return await this.endLease(delivery, leaseToken, changes) ? changes.status : "lease-lost";
|
|
3125
|
+
}
|
|
3126
|
+
endLease(delivery, leaseToken, changes) {
|
|
3127
|
+
return delivery.transition({
|
|
3128
|
+
...changes,
|
|
3129
|
+
leaseToken: null,
|
|
3130
|
+
leaseExpiresAt: null
|
|
3131
|
+
}, { leaseToken });
|
|
3132
|
+
}
|
|
3133
|
+
async loadMessage(messageId) {
|
|
3134
|
+
return await (await MessageCollection.create({ db: this.options.db })).get({ id: messageId }) ?? null;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* The delivery's single outbound `Email`, created under the lease. Its id
|
|
3138
|
+
* is derived from the delivery id and inserted insert-only, so a stale
|
|
3139
|
+
* dispatcher that resumes after its lease was taken over hits the stored
|
|
3140
|
+
* row (a unique violation) instead of creating a second email.
|
|
3141
|
+
*/
|
|
3142
|
+
async ensureMessage(delivery, leaseToken) {
|
|
3143
|
+
if (delivery.messageId) {
|
|
3144
|
+
const message2 = await this.loadMessage(delivery.messageId);
|
|
3145
|
+
if (message2) return message2;
|
|
3146
|
+
}
|
|
3147
|
+
const messageId = await notificationDeliveryMessageId(delivery.id);
|
|
3148
|
+
let message = await this.loadMessage(messageId);
|
|
3149
|
+
if (!message) {
|
|
3150
|
+
const body = `${delivery.textBody}
|
|
3151
|
+
|
|
3152
|
+
${delivery.href}
|
|
3153
|
+
`;
|
|
3154
|
+
const emails = await EmailCollection.create({ db: this.options.db });
|
|
3155
|
+
try {
|
|
3156
|
+
await emails.create({
|
|
3157
|
+
_insertOnly: true,
|
|
3158
|
+
id: messageId,
|
|
3159
|
+
tenantId: delivery.tenantId,
|
|
3160
|
+
accountId: delivery.accountId,
|
|
3161
|
+
correlationId: notificationDeliveryCorrelationId(delivery.id),
|
|
3162
|
+
subject: delivery.subject,
|
|
3163
|
+
body,
|
|
3164
|
+
textBody: body,
|
|
3165
|
+
toAddresses: JSON.stringify([{
|
|
3166
|
+
address: delivery.recipientAddress,
|
|
3167
|
+
...delivery.recipientName ? { name: delivery.recipientName } : {}
|
|
3168
|
+
}]),
|
|
3169
|
+
sendStatus: "pending",
|
|
3170
|
+
maxRetries: 0
|
|
3171
|
+
});
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
if (!isUniqueViolationError(error)) throw error;
|
|
3174
|
+
}
|
|
3175
|
+
message = await this.loadMessage(messageId);
|
|
3176
|
+
if (!message) throw new Error("The outbound email could not be stored.");
|
|
3177
|
+
}
|
|
3178
|
+
if (delivery.messageId !== message.id) {
|
|
3179
|
+
if (!await delivery.transition({ messageId: message.id }, { leaseToken })) return null;
|
|
3180
|
+
}
|
|
3181
|
+
return message;
|
|
3182
|
+
}
|
|
3183
|
+
};
|
|
3184
|
+
//#endregion
|
|
2234
3185
|
//#region src/services/PersonaMessagingService.ts
|
|
2235
3186
|
var PersonaMessagingService = class {
|
|
2236
3187
|
constructor(options) {
|
|
@@ -2759,6 +3710,6 @@ var SlackRecipe = class extends SmrtRecipe {
|
|
|
2759
3710
|
ensureBuiltinMessagingProvidersRegistered();
|
|
2760
3711
|
ensureMessagingPermissionsRegistered();
|
|
2761
3712
|
//#endregion
|
|
2762
|
-
export { Account, AccountCollection, Attachment, AttachmentCollection, Blacklist, BlacklistCollection, Email, EmailAccount, EmailAccountCollection, EmailAttachment, EmailAttachmentCollection, EmailCollection, EmailFolder, EmailFolderCollection, EmailSender, MANAGE_MESSAGE_CREDENTIALS_PERMISSION, MANAGE_MESSAGE_ROUTES_PERMISSION, MESSAGING_PERMISSION_DEFINITIONS, MailboxRecipe, Message, MessageCollection, MessagingEndpoint, MessagingEndpointCollection, MessagingSettingsService, NotificationsRecipe, PersonaMessageRoute, PersonaMessageRouteCollection, PersonaMessagingService, SEND_MESSAGES_PERMISSION, SlackAccount, SlackMessage, SlackRecipe, SlackSender, TelegramAccount, TelegramSender, Tweet, TweetSender, TwitterAccount, USER_NOTIFICATION_SEVERITIES, UserNotification, UserNotificationCollection, UserNotificationService, Whitelist, WhitelistCollection, ZulipAccount, ZulipSender, AttachmentCollection_exports as a, assertMessagingPermission, ensureBuiltinMessagingProvidersRegistered, ensureMessagingPermissionsRegistered, getMessagingProvider, MessagingEndpointCollection_exports as i, listMessagingProviders, messagingPrincipalFromPermissions, EmailCollection_exports as n, EmailAccountCollection_exports as r, registerMessagingProvider, EmailFolderCollection_exports as t };
|
|
3713
|
+
export { Account, AccountCollection, Attachment, AttachmentCollection, Blacklist, BlacklistCollection, Email, EmailAccount, EmailAccountCollection, EmailAttachment, EmailAttachmentCollection, EmailCollection, EmailFolder, EmailFolderCollection, EmailSender, MANAGE_MESSAGE_CREDENTIALS_PERMISSION, MANAGE_MESSAGE_ROUTES_PERMISSION, MESSAGING_PERMISSION_DEFINITIONS, MailboxRecipe, Message, MessageCollection, MessagingEndpoint, MessagingEndpointCollection, MessagingSettingsService, NOTIFICATION_DELIVERY_CHANNELS, NOTIFICATION_OPT_OUT_ALL_SCOPE, NOTIFICATION_OPT_OUT_SCOPE_PATTERN, NotificationDelivery, NotificationDeliveryCollection, NotificationOptOut, NotificationOptOutCollection, NotificationOptOutDeniedError, NotificationOptOutService, NotificationOutboxDeniedError, NotificationOutboxService, NotificationsRecipe, PersonaMessageRoute, PersonaMessageRouteCollection, PersonaMessagingService, SEND_MESSAGES_PERMISSION, SlackAccount, SlackMessage, SlackRecipe, SlackSender, TelegramAccount, TelegramSender, Tweet, TweetSender, TwitterAccount, USER_NOTIFICATION_SEVERITIES, UserNotification, UserNotificationCollection, UserNotificationService, Whitelist, WhitelistCollection, ZulipAccount, ZulipSender, AttachmentCollection_exports as a, assertMessagingPermission, buildNotificationLink, ensureBuiltinMessagingProvidersRegistered, ensureMessagingPermissionsRegistered, getMessagingProvider, MessagingEndpointCollection_exports as i, isValidNotificationOptOutScope, listMessagingProviders, messageSendTransport, messagingPrincipalFromPermissions, EmailCollection_exports as n, notificationDeliveryCorrelationId, notificationDeliveryMessageId, optOutSuppression, EmailAccountCollection_exports as r, registerMessagingProvider, EmailFolderCollection_exports as t };
|
|
2763
3714
|
|
|
2764
3715
|
//# sourceMappingURL=index.js.map
|