@happyvertical/smrt-messages 0.60.9 → 0.60.10
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 +17 -0
- package/README.md +40 -0
- package/agents/notification-outbox.md +116 -0
- package/dist/chunks/{AccountCollection-CPsjabD-.js → AccountCollection-By48bdwt.js} +2 -2
- package/dist/chunks/{AccountCollection-CPsjabD-.js.map → AccountCollection-By48bdwt.js.map} +1 -1
- package/dist/chunks/{Email-C85SNWH0.js → Email-CMUK3aWU.js} +3 -3
- package/dist/chunks/{Email-C85SNWH0.js.map → Email-CMUK3aWU.js.map} +1 -1
- package/dist/chunks/{EmailFolder-Ia8OFxRn.js → EmailFolder-DLJvqNqI.js} +2 -2
- package/dist/chunks/{EmailFolder-Ia8OFxRn.js.map → EmailFolder-DLJvqNqI.js.map} +1 -1
- package/dist/chunks/{Message-x-bj-WcS.js → Message-DXpqot5f.js} +52 -5
- package/dist/chunks/Message-DXpqot5f.js.map +1 -0
- package/dist/chunks/{MessageCollection-BM4y2_mB.js → MessageCollection-CMGaSGBc.js} +2 -2
- package/dist/chunks/{MessageCollection-BM4y2_mB.js.map → MessageCollection-CMGaSGBc.js.map} +1 -1
- package/dist/chunks/__smrt-register__-CaGLtiEU.js +6 -0
- package/dist/chunks/__smrt-register__-CaGLtiEU.js.map +1 -0
- package/dist/collections/NotificationDeliveryCollection.d.ts +6 -0
- package/dist/collections/NotificationDeliveryCollection.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +780 -38
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1135 -89
- 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/senders/EmailSender.d.ts +9 -0
- package/dist/senders/EmailSender.d.ts.map +1 -1
- package/dist/services/NotificationOutboxService.d.ts +226 -0
- package/dist/services/NotificationOutboxService.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +470 -9
- package/dist/smrt-knowledge.okf.md +155 -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__-CaGLtiEU.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-By48bdwt.js";
|
|
5
|
+
import { t as Message } from "./chunks/Message-DXpqot5f.js";
|
|
6
|
+
import { t as MessageCollection } from "./chunks/MessageCollection-CMGaSGBc.js";
|
|
7
|
+
import { t as Email } from "./chunks/Email-CMUK3aWU.js";
|
|
8
|
+
import { t as EmailFolder } from "./chunks/EmailFolder-DLJvqNqI.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$13 = Object.defineProperty;
|
|
58
|
+
var __getOwnPropDesc$13 = Object.getOwnPropertyDescriptor;
|
|
59
|
+
var __defNormalProp$13 = (obj, key, value) => key in obj ? __defProp$13(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$13 = (decorators, target, key, kind) => {
|
|
66
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$13(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$13(target, key, result);
|
|
69
69
|
return result;
|
|
70
70
|
};
|
|
71
|
-
var __publicField$
|
|
71
|
+
var __publicField$13 = (obj, key, value) => __defNormalProp$13(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-CMGaSGBc.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$13(Attachment, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
191
|
+
__decorateClass$13([tenantId({ nullable: true })], Attachment.prototype, "tenantId", 2);
|
|
192
|
+
__decorateClass$13([foreignKey("Message")], Attachment.prototype, "messageId", 2);
|
|
193
|
+
Attachment = __decorateClass$13([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$12 = Object.defineProperty;
|
|
321
|
+
var __getOwnPropDesc$12 = Object.getOwnPropertyDescriptor;
|
|
322
|
+
var __defNormalProp$12 = (obj, key, value) => key in obj ? __defProp$12(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$12 = (decorators, target, key, kind) => {
|
|
329
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$12(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$12(target, key, result);
|
|
332
332
|
return result;
|
|
333
333
|
};
|
|
334
|
-
var __publicField$
|
|
334
|
+
var __publicField$12 = (obj, key, value) => __defNormalProp$12(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$12(MessagingEndpoint, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
380
|
+
__decorateClass$12([tenantId()], MessagingEndpoint.prototype, "tenantId", 2);
|
|
381
|
+
__decorateClass$12([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], MessagingEndpoint.prototype, "profileId", 2);
|
|
382
|
+
__decorateClass$12([field({ required: true })], MessagingEndpoint.prototype, "label", 2);
|
|
383
|
+
__decorateClass$12([field({ required: true })], MessagingEndpoint.prototype, "channel", 2);
|
|
384
|
+
__decorateClass$12([field({
|
|
385
385
|
required: true,
|
|
386
386
|
sensitive: true
|
|
387
387
|
})], MessagingEndpoint.prototype, "address", 2);
|
|
388
|
-
MessagingEndpoint = __decorateClass$
|
|
388
|
+
MessagingEndpoint = __decorateClass$12([TenantScoped({ mode: "required" }), smrt({
|
|
389
389
|
tableName: "messaging_endpoints",
|
|
390
390
|
api: { include: ["list", "get"] },
|
|
391
391
|
mcp: { include: ["list", "get"] },
|
|
@@ -403,6 +403,196 @@ var MessagingEndpointCollection = class extends SmrtCollection {
|
|
|
403
403
|
static _itemClass = MessagingEndpoint;
|
|
404
404
|
};
|
|
405
405
|
//#endregion
|
|
406
|
+
//#region src/models/NotificationDelivery.ts
|
|
407
|
+
var __defProp$11 = Object.defineProperty;
|
|
408
|
+
var __getOwnPropDesc$11 = Object.getOwnPropertyDescriptor;
|
|
409
|
+
var __defNormalProp$11 = (obj, key, value) => key in obj ? __defProp$11(obj, key, {
|
|
410
|
+
enumerable: true,
|
|
411
|
+
configurable: true,
|
|
412
|
+
writable: true,
|
|
413
|
+
value
|
|
414
|
+
}) : obj[key] = value;
|
|
415
|
+
var __decorateClass$11 = (decorators, target, key, kind) => {
|
|
416
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$11(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$11(target, key, result);
|
|
419
|
+
return result;
|
|
420
|
+
};
|
|
421
|
+
var __publicField$11 = (obj, key, value) => __defNormalProp$11(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$11(NotificationDelivery, "__smrtPackage__", "@happyvertical/smrt-messages");
|
|
560
|
+
__decorateClass$11([tenantId()], NotificationDelivery.prototype, "tenantId", 2);
|
|
561
|
+
__decorateClass$11([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], NotificationDelivery.prototype, "recipientUserId", 2);
|
|
562
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "channel", 2);
|
|
563
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "sourceRef", 2);
|
|
564
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "kind", 2);
|
|
565
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "subject", 2);
|
|
566
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "textBody", 2);
|
|
567
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "href", 2);
|
|
568
|
+
__decorateClass$11([field({ required: true })], NotificationDelivery.prototype, "recipientAddress", 2);
|
|
569
|
+
__decorateClass$11([foreignKey("Account")], NotificationDelivery.prototype, "accountId", 2);
|
|
570
|
+
__decorateClass$11([foreignKey("Message")], NotificationDelivery.prototype, "messageId", 2);
|
|
571
|
+
NotificationDelivery = __decorateClass$11([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
|
|
406
596
|
//#region src/models/PersonaMessageRoute.ts
|
|
407
597
|
var __defProp$10 = Object.defineProperty;
|
|
408
598
|
var __getOwnPropDesc$10 = Object.getOwnPropertyDescriptor;
|
|
@@ -688,7 +878,7 @@ var EmailAccount = class extends Account {
|
|
|
688
878
|
for (const folderName of foldersToSync) try {
|
|
689
879
|
let folder = await folderCollection.getByPath(accountId, folderName);
|
|
690
880
|
if (!folder) {
|
|
691
|
-
const { EmailFolder } = await import("./chunks/EmailFolder-
|
|
881
|
+
const { EmailFolder } = await import("./chunks/EmailFolder-DLJvqNqI.js").then((n) => n.n);
|
|
692
882
|
folder = new EmailFolder({
|
|
693
883
|
...this.childOptions(),
|
|
694
884
|
accountId,
|
|
@@ -713,7 +903,7 @@ var EmailAccount = class extends Account {
|
|
|
713
903
|
result.messagesSkipped++;
|
|
714
904
|
continue;
|
|
715
905
|
}
|
|
716
|
-
const { Email } = await import("./chunks/Email-
|
|
906
|
+
const { Email } = await import("./chunks/Email-CMUK3aWU.js").then((n) => n.n);
|
|
717
907
|
let email = existingEmail;
|
|
718
908
|
if (!email) {
|
|
719
909
|
email = new Email({
|
|
@@ -1924,7 +2114,10 @@ ZulipAccount = __decorateClass([smrt({
|
|
|
1924
2114
|
})], ZulipAccount);
|
|
1925
2115
|
//#endregion
|
|
1926
2116
|
//#region src/senders/EmailSender.ts
|
|
1927
|
-
var EmailSender_exports = /* @__PURE__ */ __exportAll({
|
|
2117
|
+
var EmailSender_exports = /* @__PURE__ */ __exportAll({
|
|
2118
|
+
EmailSender: () => EmailSender,
|
|
2119
|
+
isDefiniteSendRejection: () => isDefiniteSendRejection
|
|
2120
|
+
});
|
|
1928
2121
|
var EmailSender = class {
|
|
1929
2122
|
providerType = "email";
|
|
1930
2123
|
client;
|
|
@@ -1987,6 +2180,7 @@ var EmailSender = class {
|
|
|
1987
2180
|
return {
|
|
1988
2181
|
success: false,
|
|
1989
2182
|
error: error instanceof Error ? error.message : String(error),
|
|
2183
|
+
...isDefiniteSendRejection(error) ? {} : { outcomeUnknown: true },
|
|
1990
2184
|
sentAt: /* @__PURE__ */ new Date()
|
|
1991
2185
|
};
|
|
1992
2186
|
}
|
|
@@ -2033,6 +2227,31 @@ var EmailSender = class {
|
|
|
2033
2227
|
return resolved;
|
|
2034
2228
|
}
|
|
2035
2229
|
};
|
|
2230
|
+
var DEFINITE_EMAIL_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2231
|
+
"AUTHENTICATION_ERROR",
|
|
2232
|
+
"AUTHORIZATION_ERROR",
|
|
2233
|
+
"INVALID_MESSAGE",
|
|
2234
|
+
"ATTACHMENT_ERROR"
|
|
2235
|
+
]);
|
|
2236
|
+
var NEVER_CONNECTED_CODES = /* @__PURE__ */ new Set([
|
|
2237
|
+
"ECONNREFUSED",
|
|
2238
|
+
"ENOTFOUND",
|
|
2239
|
+
"EHOSTUNREACH",
|
|
2240
|
+
"EAI_AGAIN"
|
|
2241
|
+
]);
|
|
2242
|
+
function isDefiniteSendRejection(error) {
|
|
2243
|
+
if (!error || typeof error !== "object") return false;
|
|
2244
|
+
const candidates = [error, error.cause].filter((value) => !!value && typeof value === "object");
|
|
2245
|
+
for (const candidate of candidates) {
|
|
2246
|
+
if (typeof candidate.responseCode === "number") return true;
|
|
2247
|
+
if (typeof candidate.code === "string") {
|
|
2248
|
+
if (DEFINITE_EMAIL_ERROR_CODES.has(candidate.code)) return true;
|
|
2249
|
+
if (NEVER_CONNECTED_CODES.has(candidate.code)) return true;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const sendError = error;
|
|
2253
|
+
return sendError.code === "SEND_ERROR" && Array.isArray(sendError.accepted) && sendError.accepted.length === 0;
|
|
2254
|
+
}
|
|
2036
2255
|
//#endregion
|
|
2037
2256
|
//#region src/services/MessagingSettingsService.ts
|
|
2038
2257
|
var MessagingSettingsService = class {
|
|
@@ -2231,6 +2450,529 @@ var MessagingSettingsService = class {
|
|
|
2231
2450
|
}
|
|
2232
2451
|
};
|
|
2233
2452
|
//#endregion
|
|
2453
|
+
//#region src/services/NotificationOutboxService.ts
|
|
2454
|
+
var messageSendTransport = {
|
|
2455
|
+
name: "message-send",
|
|
2456
|
+
idempotent: false,
|
|
2457
|
+
async send({ message }) {
|
|
2458
|
+
const result = await message.send();
|
|
2459
|
+
if (result.success) return {
|
|
2460
|
+
status: "accepted",
|
|
2461
|
+
...result.providerMessageId ? { providerMessageId: result.providerMessageId } : {}
|
|
2462
|
+
};
|
|
2463
|
+
if (result.outcomeUnknown || message.sendStatus === "sending" || message.sendStatus === "sent") return {
|
|
2464
|
+
status: "unknown",
|
|
2465
|
+
error: result.error ?? "The provider did not confirm the outcome."
|
|
2466
|
+
};
|
|
2467
|
+
return {
|
|
2468
|
+
status: "rejected",
|
|
2469
|
+
error: result.error ?? "Send failed",
|
|
2470
|
+
retryable: true
|
|
2471
|
+
};
|
|
2472
|
+
},
|
|
2473
|
+
async reconcile({ message }) {
|
|
2474
|
+
const messages = await MessageCollection.create({ db: message.db });
|
|
2475
|
+
const stored = message.id ? await messages.get({ id: message.id }) : null;
|
|
2476
|
+
switch (stored?.sendStatus) {
|
|
2477
|
+
case "sent": return { status: "accepted" };
|
|
2478
|
+
case "failed":
|
|
2479
|
+
if (stored.isSendOutcomeUnknown()) return {
|
|
2480
|
+
status: "unknown",
|
|
2481
|
+
error: stored.sendError || "The provider did not confirm the outcome."
|
|
2482
|
+
};
|
|
2483
|
+
return {
|
|
2484
|
+
status: "rejected",
|
|
2485
|
+
error: stored.sendError || "Send failed",
|
|
2486
|
+
retryable: true
|
|
2487
|
+
};
|
|
2488
|
+
case "draft":
|
|
2489
|
+
case "pending":
|
|
2490
|
+
case "scheduled": return { status: "not-sent" };
|
|
2491
|
+
default: return {
|
|
2492
|
+
status: "unknown",
|
|
2493
|
+
error: "The provider handoff did not record an outcome."
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
var NotificationOutboxDeniedError = class extends Error {
|
|
2499
|
+
constructor(reason, message) {
|
|
2500
|
+
super(message);
|
|
2501
|
+
this.reason = reason;
|
|
2502
|
+
this.name = "NotificationOutboxDeniedError";
|
|
2503
|
+
}
|
|
2504
|
+
reason;
|
|
2505
|
+
};
|
|
2506
|
+
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;
|
|
2507
|
+
var DEFAULT_LEASE_MS = 5 * 6e4;
|
|
2508
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
2509
|
+
var DEFAULT_LIMIT$1 = 25;
|
|
2510
|
+
var MAX_LIMIT$1 = 200;
|
|
2511
|
+
function defaultRetryDelayMs(attempts) {
|
|
2512
|
+
return Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 60 * 6e4);
|
|
2513
|
+
}
|
|
2514
|
+
function requireText$1(value, name) {
|
|
2515
|
+
const text = value?.trim();
|
|
2516
|
+
if (!text) throw new Error(`A notification ${name} is required.`);
|
|
2517
|
+
return text;
|
|
2518
|
+
}
|
|
2519
|
+
function errorText(error) {
|
|
2520
|
+
return error instanceof Error ? error.message : String(error);
|
|
2521
|
+
}
|
|
2522
|
+
async function notificationDeliveryMessageId(deliveryId) {
|
|
2523
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`smrt-messages:notification-delivery:${deliveryId}`));
|
|
2524
|
+
const bytes = new Uint8Array(digest).slice(0, 16);
|
|
2525
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
2526
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2527
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
|
|
2528
|
+
return [
|
|
2529
|
+
hex.slice(0, 4),
|
|
2530
|
+
hex.slice(4, 6),
|
|
2531
|
+
hex.slice(6, 8),
|
|
2532
|
+
hex.slice(8, 10),
|
|
2533
|
+
hex.slice(10, 16)
|
|
2534
|
+
].map((group) => group.join("")).join("-");
|
|
2535
|
+
}
|
|
2536
|
+
function notificationDeliveryCorrelationId(deliveryId) {
|
|
2537
|
+
return `notification-delivery:${deliveryId}`;
|
|
2538
|
+
}
|
|
2539
|
+
function buildNotificationLink(baseUrl, path) {
|
|
2540
|
+
let base;
|
|
2541
|
+
try {
|
|
2542
|
+
base = new URL(baseUrl);
|
|
2543
|
+
} catch {
|
|
2544
|
+
throw new Error("Notification link base URL must be an absolute URL.");
|
|
2545
|
+
}
|
|
2546
|
+
if (base.protocol !== "https:" && base.protocol !== "http:") throw new Error("Notification link base URL must use http or https.");
|
|
2547
|
+
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.");
|
|
2548
|
+
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.");
|
|
2549
|
+
const prefix = base.pathname.replace(/\/+$/u, "");
|
|
2550
|
+
const link = new URL(`${prefix}${path}`, base.origin);
|
|
2551
|
+
if (link.origin !== base.origin) throw new NotificationOutboxDeniedError("link", "Notification links must stay on the configured origin.");
|
|
2552
|
+
return link.toString();
|
|
2553
|
+
}
|
|
2554
|
+
var NotificationOutboxService = class {
|
|
2555
|
+
constructor(options) {
|
|
2556
|
+
this.options = options;
|
|
2557
|
+
buildNotificationLink(options.linkBaseUrl, "/");
|
|
2558
|
+
this.transport = options.transport ?? messageSendTransport;
|
|
2559
|
+
this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
2560
|
+
this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
2561
|
+
this.retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
|
|
2562
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2563
|
+
if (!Number.isInteger(this.leaseMs) || this.leaseMs < 1e3) throw new Error("Notification lease must be at least 1000 ms.");
|
|
2564
|
+
if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) throw new Error("Notification maxAttempts must be a positive integer.");
|
|
2565
|
+
}
|
|
2566
|
+
options;
|
|
2567
|
+
transport;
|
|
2568
|
+
leaseMs;
|
|
2569
|
+
maxAttempts;
|
|
2570
|
+
retryDelayMs;
|
|
2571
|
+
now;
|
|
2572
|
+
collection() {
|
|
2573
|
+
return NotificationDeliveryCollection.create({ db: this.options.db });
|
|
2574
|
+
}
|
|
2575
|
+
authorize(tenantId) {
|
|
2576
|
+
if (!this.options.principal) return;
|
|
2577
|
+
try {
|
|
2578
|
+
assertMessagingPermission(this.options.principal, tenantId, SEND_MESSAGES_PERMISSION);
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
throw new NotificationOutboxDeniedError("permission", errorText(error));
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
/** Store one email delivery for the event unless `sourceRef` already has one. */
|
|
2584
|
+
async enqueueEmail(input) {
|
|
2585
|
+
const tenantId = requireText$1(input.tenantId, "tenant");
|
|
2586
|
+
const recipientUserId = requireText$1(input.recipientUserId, "recipient");
|
|
2587
|
+
const sourceRef = requireText$1(input.sourceRef, "sourceRef");
|
|
2588
|
+
const kind = requireText$1(input.kind, "kind");
|
|
2589
|
+
const subject = requireText$1(input.subject, "subject");
|
|
2590
|
+
const text = requireText$1(input.text, "text");
|
|
2591
|
+
if (/[\r\n]/u.test(subject)) throw new Error("A notification subject must be a single line.");
|
|
2592
|
+
const channel = "email";
|
|
2593
|
+
this.authorize(tenantId);
|
|
2594
|
+
const href = buildNotificationLink(this.options.linkBaseUrl, input.path);
|
|
2595
|
+
return withTenant({ tenantId }, async () => {
|
|
2596
|
+
const deliveries = await this.collection();
|
|
2597
|
+
const findExisting = async () => (await deliveries.list({
|
|
2598
|
+
where: {
|
|
2599
|
+
tenantId,
|
|
2600
|
+
recipientUserId,
|
|
2601
|
+
channel,
|
|
2602
|
+
sourceRef
|
|
2603
|
+
},
|
|
2604
|
+
limit: 1
|
|
2605
|
+
}))[0];
|
|
2606
|
+
const existing = await findExisting();
|
|
2607
|
+
if (existing) return {
|
|
2608
|
+
delivery: existing,
|
|
2609
|
+
created: false
|
|
2610
|
+
};
|
|
2611
|
+
const recipient = await this.options.resolveRecipient({
|
|
2612
|
+
tenantId,
|
|
2613
|
+
recipientUserId,
|
|
2614
|
+
channel
|
|
2615
|
+
});
|
|
2616
|
+
const address = recipient?.address?.trim();
|
|
2617
|
+
if (!address || /[\s,;<>]/u.test(address) || !address.includes("@")) throw new NotificationOutboxDeniedError("recipient", "The host did not authorize an email recipient for this user.");
|
|
2618
|
+
const accountId = await this.requireSenderAccount(tenantId);
|
|
2619
|
+
const suppressed = await this.isSuppressed({
|
|
2620
|
+
tenantId,
|
|
2621
|
+
recipientUserId,
|
|
2622
|
+
channel,
|
|
2623
|
+
kind,
|
|
2624
|
+
sourceRef
|
|
2625
|
+
});
|
|
2626
|
+
try {
|
|
2627
|
+
return {
|
|
2628
|
+
delivery: await deliveries.create({
|
|
2629
|
+
_insertOnly: true,
|
|
2630
|
+
tenantId,
|
|
2631
|
+
recipientUserId,
|
|
2632
|
+
channel,
|
|
2633
|
+
sourceRef,
|
|
2634
|
+
kind,
|
|
2635
|
+
subject,
|
|
2636
|
+
textBody: text,
|
|
2637
|
+
href,
|
|
2638
|
+
recipientAddress: address,
|
|
2639
|
+
recipientName: recipient?.name?.trim() ?? "",
|
|
2640
|
+
accountId,
|
|
2641
|
+
messageId: null,
|
|
2642
|
+
status: suppressed ? "cancelled" : "pending",
|
|
2643
|
+
stateVersion: 0,
|
|
2644
|
+
transitionId: "",
|
|
2645
|
+
attempts: 0,
|
|
2646
|
+
maxAttempts: this.maxAttempts,
|
|
2647
|
+
nextAttemptAt: input.notBefore ?? this.now(),
|
|
2648
|
+
leaseToken: null,
|
|
2649
|
+
leaseExpiresAt: null,
|
|
2650
|
+
handedOffAt: null,
|
|
2651
|
+
deliveredAt: null,
|
|
2652
|
+
providerMessageId: "",
|
|
2653
|
+
lastError: suppressed ? "Suppressed by recipient preference." : ""
|
|
2654
|
+
}),
|
|
2655
|
+
created: true
|
|
2656
|
+
};
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
if (!isUniqueViolationError(error)) throw error;
|
|
2659
|
+
const winner = await findExisting();
|
|
2660
|
+
if (!winner) throw error;
|
|
2661
|
+
return {
|
|
2662
|
+
delivery: winner,
|
|
2663
|
+
created: false
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
/** Lease and deliver due rows; returns what happened to each leased row. */
|
|
2669
|
+
async dispatchDue(options = {}) {
|
|
2670
|
+
const limit = Math.min(options.limit ?? DEFAULT_LIMIT$1, MAX_LIMIT$1);
|
|
2671
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("Notification dispatch limit must be a positive integer.");
|
|
2672
|
+
const candidates = await this.findDue(options.tenantIds, limit);
|
|
2673
|
+
const report = {
|
|
2674
|
+
leased: 0,
|
|
2675
|
+
outcomes: []
|
|
2676
|
+
};
|
|
2677
|
+
for (const candidate of candidates) {
|
|
2678
|
+
if (report.leased >= limit) break;
|
|
2679
|
+
const outcome = await withTenant({ tenantId: candidate.tenantId }, () => this.dispatchOne(candidate));
|
|
2680
|
+
if (!outcome) continue;
|
|
2681
|
+
report.leased += 1;
|
|
2682
|
+
report.outcomes.push({
|
|
2683
|
+
deliveryId: candidate.id,
|
|
2684
|
+
tenantId: candidate.tenantId,
|
|
2685
|
+
outcome
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
return report;
|
|
2689
|
+
}
|
|
2690
|
+
/** Deliveries waiting for a reconciliation decision, oldest first. */
|
|
2691
|
+
async listUncertain(options) {
|
|
2692
|
+
const tenantId = requireText$1(options.tenantId, "tenant");
|
|
2693
|
+
this.authorize(tenantId);
|
|
2694
|
+
return withTenant({ tenantId }, async () => (await this.collection()).list({
|
|
2695
|
+
where: {
|
|
2696
|
+
tenantId,
|
|
2697
|
+
status: "uncertain"
|
|
2698
|
+
},
|
|
2699
|
+
orderBy: "handedOffAt ASC",
|
|
2700
|
+
limit: Math.min(options.limit ?? DEFAULT_LIMIT$1, MAX_LIMIT$1)
|
|
2701
|
+
}));
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Settle an `uncertain` delivery after checking the provider out of band:
|
|
2705
|
+
* `sent` (it arrived), `failed` (give up), or `retry` (send again,
|
|
2706
|
+
* accepting the risk of a duplicate; a message left claimed by a dead
|
|
2707
|
+
* sender is released first). Returns false when the row is not uncertain
|
|
2708
|
+
* any more, or for `retry` when the message is recorded as sent.
|
|
2709
|
+
*/
|
|
2710
|
+
async reconcile(input) {
|
|
2711
|
+
const tenantId = requireText$1(input.tenantId, "tenant");
|
|
2712
|
+
this.authorize(tenantId);
|
|
2713
|
+
if (!UUID_PATTERN$1.test(input.deliveryId ?? "")) return false;
|
|
2714
|
+
return withTenant({ tenantId }, async () => {
|
|
2715
|
+
const delivery = (await (await this.collection()).list({
|
|
2716
|
+
where: {
|
|
2717
|
+
tenantId,
|
|
2718
|
+
id: input.deliveryId
|
|
2719
|
+
},
|
|
2720
|
+
limit: 1
|
|
2721
|
+
}))[0];
|
|
2722
|
+
if (delivery?.status !== "uncertain") return false;
|
|
2723
|
+
const note = input.note?.trim();
|
|
2724
|
+
const now = this.now();
|
|
2725
|
+
switch (input.outcome) {
|
|
2726
|
+
case "sent": return delivery.transition({
|
|
2727
|
+
status: "sent",
|
|
2728
|
+
deliveredAt: now,
|
|
2729
|
+
...note ? { lastError: note } : {}
|
|
2730
|
+
});
|
|
2731
|
+
case "failed": return delivery.transition({
|
|
2732
|
+
status: "failed",
|
|
2733
|
+
lastError: note || delivery.lastError
|
|
2734
|
+
});
|
|
2735
|
+
case "retry": {
|
|
2736
|
+
const message = delivery.messageId ? await this.loadMessage(delivery.messageId) : null;
|
|
2737
|
+
if (message?.sendStatus === "sent") return false;
|
|
2738
|
+
if (message?.sendStatus === "sending" && !await message.abandonSendAttempt(note || "Released for an operator-approved retry.")) return false;
|
|
2739
|
+
return delivery.transition({
|
|
2740
|
+
status: "pending",
|
|
2741
|
+
nextAttemptAt: now,
|
|
2742
|
+
handedOffAt: null,
|
|
2743
|
+
attempts: Math.min(delivery.attempts, delivery.maxAttempts - 1),
|
|
2744
|
+
...note ? { lastError: note } : {}
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
default: throw new Error(`Unknown reconcile outcome '${input.outcome}'.`);
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
async requireSenderAccount(tenantId) {
|
|
2752
|
+
const accountId = (await this.options.resolveSenderAccountId({ tenantId }))?.trim();
|
|
2753
|
+
const account = accountId ? await (await AccountCollection.create({ db: this.options.db })).get({ id: accountId }) : null;
|
|
2754
|
+
if (!account || !(account instanceof EmailAccount) || account.tenantId !== tenantId || !account.isActive) throw new NotificationOutboxDeniedError("sender", "No active email sender account is configured for this tenant.");
|
|
2755
|
+
return account.id;
|
|
2756
|
+
}
|
|
2757
|
+
async isSuppressed(request) {
|
|
2758
|
+
return Boolean(await this.options.isSuppressed?.(request));
|
|
2759
|
+
}
|
|
2760
|
+
async findDue(tenantIds, limit) {
|
|
2761
|
+
const now = this.now();
|
|
2762
|
+
const read = async (tenantFilter) => {
|
|
2763
|
+
const deliveries = await this.collection();
|
|
2764
|
+
const [due, expired] = await Promise.all([deliveries.list({
|
|
2765
|
+
where: {
|
|
2766
|
+
...tenantFilter,
|
|
2767
|
+
status: "pending",
|
|
2768
|
+
"nextAttemptAt <=": now
|
|
2769
|
+
},
|
|
2770
|
+
orderBy: "nextAttemptAt ASC",
|
|
2771
|
+
limit
|
|
2772
|
+
}), deliveries.list({
|
|
2773
|
+
where: {
|
|
2774
|
+
...tenantFilter,
|
|
2775
|
+
status: "sending",
|
|
2776
|
+
"leaseExpiresAt <=": now
|
|
2777
|
+
},
|
|
2778
|
+
orderBy: "leaseExpiresAt ASC",
|
|
2779
|
+
limit
|
|
2780
|
+
})]);
|
|
2781
|
+
return [...expired, ...due];
|
|
2782
|
+
};
|
|
2783
|
+
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);
|
|
2784
|
+
}
|
|
2785
|
+
/** Lease one candidate and carry it to an outcome; null when not leased. */
|
|
2786
|
+
async dispatchOne(candidate) {
|
|
2787
|
+
const delivery = (await (await this.collection()).list({
|
|
2788
|
+
where: {
|
|
2789
|
+
tenantId: candidate.tenantId,
|
|
2790
|
+
id: candidate.id
|
|
2791
|
+
},
|
|
2792
|
+
limit: 1
|
|
2793
|
+
}))[0];
|
|
2794
|
+
if (!delivery) return null;
|
|
2795
|
+
const now = this.now();
|
|
2796
|
+
const recovering = delivery.status === "sending" && delivery.leaseExpiresAt !== null && delivery.leaseExpiresAt.getTime() <= now.getTime();
|
|
2797
|
+
const due = delivery.status === "pending" && delivery.nextAttemptAt.getTime() <= now.getTime();
|
|
2798
|
+
if (!recovering && !due) return null;
|
|
2799
|
+
const leaseToken = crypto.randomUUID();
|
|
2800
|
+
const leaseExpiresAt = new Date(now.getTime() + this.leaseMs);
|
|
2801
|
+
const priorHandoff = recovering && delivery.handedOffAt !== null;
|
|
2802
|
+
if (!await delivery.transition({
|
|
2803
|
+
status: "sending",
|
|
2804
|
+
leaseToken,
|
|
2805
|
+
leaseExpiresAt,
|
|
2806
|
+
attempts: priorHandoff ? delivery.attempts : delivery.attempts + 1,
|
|
2807
|
+
...priorHandoff ? {} : { handedOffAt: null }
|
|
2808
|
+
})) return null;
|
|
2809
|
+
try {
|
|
2810
|
+
if (priorHandoff) {
|
|
2811
|
+
const resolved = await this.recoverHandoff(delivery, leaseToken);
|
|
2812
|
+
if (resolved) return resolved;
|
|
2813
|
+
}
|
|
2814
|
+
if (await this.isSuppressed({
|
|
2815
|
+
tenantId: delivery.tenantId,
|
|
2816
|
+
recipientUserId: delivery.recipientUserId,
|
|
2817
|
+
channel: delivery.channel,
|
|
2818
|
+
kind: delivery.kind,
|
|
2819
|
+
sourceRef: delivery.sourceRef
|
|
2820
|
+
})) return this.settle(delivery, leaseToken, {
|
|
2821
|
+
status: "cancelled",
|
|
2822
|
+
lastError: "Suppressed by recipient preference."
|
|
2823
|
+
});
|
|
2824
|
+
const message = await this.ensureMessage(delivery, leaseToken);
|
|
2825
|
+
if (!message) return "lease-lost";
|
|
2826
|
+
if (!await delivery.transition({ handedOffAt: this.now() }, { leaseToken })) return "lease-lost";
|
|
2827
|
+
let outcome;
|
|
2828
|
+
try {
|
|
2829
|
+
outcome = await this.transport.send({
|
|
2830
|
+
delivery,
|
|
2831
|
+
message,
|
|
2832
|
+
idempotencyKey: notificationDeliveryCorrelationId(delivery.id)
|
|
2833
|
+
});
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
outcome = {
|
|
2836
|
+
status: "unknown",
|
|
2837
|
+
error: errorText(error)
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
return this.applyOutcome(delivery, leaseToken, outcome);
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
if (delivery.handedOffAt !== null) throw error;
|
|
2843
|
+
return this.applyOutcome(delivery, leaseToken, {
|
|
2844
|
+
status: "rejected",
|
|
2845
|
+
error: errorText(error),
|
|
2846
|
+
retryable: true
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
async recoverHandoff(delivery, leaseToken) {
|
|
2851
|
+
const message = delivery.messageId ? await this.loadMessage(delivery.messageId) : null;
|
|
2852
|
+
let known = {
|
|
2853
|
+
status: "unknown",
|
|
2854
|
+
error: "Lease lapsed after the provider handoff; outcome unknown."
|
|
2855
|
+
};
|
|
2856
|
+
if (message && this.transport.reconcile) try {
|
|
2857
|
+
known = await this.transport.reconcile({
|
|
2858
|
+
delivery,
|
|
2859
|
+
message,
|
|
2860
|
+
idempotencyKey: notificationDeliveryCorrelationId(delivery.id)
|
|
2861
|
+
});
|
|
2862
|
+
} catch (error) {
|
|
2863
|
+
known = {
|
|
2864
|
+
status: "unknown",
|
|
2865
|
+
error: errorText(error)
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
if (known.status === "not-sent") {
|
|
2869
|
+
if (!await delivery.transition({
|
|
2870
|
+
handedOffAt: null,
|
|
2871
|
+
attempts: delivery.attempts + 1
|
|
2872
|
+
}, { leaseToken })) return "lease-lost";
|
|
2873
|
+
return null;
|
|
2874
|
+
}
|
|
2875
|
+
if (known.status === "unknown" && this.transport.idempotent) {
|
|
2876
|
+
if (!await delivery.transition({
|
|
2877
|
+
handedOffAt: null,
|
|
2878
|
+
attempts: delivery.attempts + 1
|
|
2879
|
+
}, { leaseToken })) return "lease-lost";
|
|
2880
|
+
return null;
|
|
2881
|
+
}
|
|
2882
|
+
return this.applyOutcome(delivery, leaseToken, known);
|
|
2883
|
+
}
|
|
2884
|
+
async applyOutcome(delivery, leaseToken, outcome) {
|
|
2885
|
+
if (outcome.status === "accepted") return this.settle(delivery, leaseToken, {
|
|
2886
|
+
status: "sent",
|
|
2887
|
+
deliveredAt: this.now(),
|
|
2888
|
+
providerMessageId: outcome.providerMessageId ?? "",
|
|
2889
|
+
lastError: ""
|
|
2890
|
+
});
|
|
2891
|
+
if (outcome.status === "unknown") {
|
|
2892
|
+
if (this.transport.idempotent && delivery.attempts < delivery.maxAttempts) return this.scheduleRetry(delivery, leaseToken, outcome.error);
|
|
2893
|
+
return this.settle(delivery, leaseToken, {
|
|
2894
|
+
status: "uncertain",
|
|
2895
|
+
lastError: outcome.error
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
if (outcome.retryable && delivery.attempts < delivery.maxAttempts) return this.scheduleRetry(delivery, leaseToken, outcome.error);
|
|
2899
|
+
return this.settle(delivery, leaseToken, {
|
|
2900
|
+
status: "failed",
|
|
2901
|
+
lastError: outcome.error
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
async scheduleRetry(delivery, leaseToken, error) {
|
|
2905
|
+
const delay = this.retryDelayMs(delivery.attempts);
|
|
2906
|
+
return await this.endLease(delivery, leaseToken, {
|
|
2907
|
+
status: "pending",
|
|
2908
|
+
nextAttemptAt: new Date(this.now().getTime() + Math.max(0, delay)),
|
|
2909
|
+
handedOffAt: null,
|
|
2910
|
+
lastError: error
|
|
2911
|
+
}) ? "retry-scheduled" : "lease-lost";
|
|
2912
|
+
}
|
|
2913
|
+
/** End the lease in a terminal status; reports lease loss instead of overwriting. */
|
|
2914
|
+
async settle(delivery, leaseToken, changes) {
|
|
2915
|
+
return await this.endLease(delivery, leaseToken, changes) ? changes.status : "lease-lost";
|
|
2916
|
+
}
|
|
2917
|
+
endLease(delivery, leaseToken, changes) {
|
|
2918
|
+
return delivery.transition({
|
|
2919
|
+
...changes,
|
|
2920
|
+
leaseToken: null,
|
|
2921
|
+
leaseExpiresAt: null
|
|
2922
|
+
}, { leaseToken });
|
|
2923
|
+
}
|
|
2924
|
+
async loadMessage(messageId) {
|
|
2925
|
+
return await (await MessageCollection.create({ db: this.options.db })).get({ id: messageId }) ?? null;
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* The delivery's single outbound `Email`, created under the lease. Its id
|
|
2929
|
+
* is derived from the delivery id and inserted insert-only, so a stale
|
|
2930
|
+
* dispatcher that resumes after its lease was taken over hits the stored
|
|
2931
|
+
* row (a unique violation) instead of creating a second email.
|
|
2932
|
+
*/
|
|
2933
|
+
async ensureMessage(delivery, leaseToken) {
|
|
2934
|
+
if (delivery.messageId) {
|
|
2935
|
+
const message2 = await this.loadMessage(delivery.messageId);
|
|
2936
|
+
if (message2) return message2;
|
|
2937
|
+
}
|
|
2938
|
+
const messageId = await notificationDeliveryMessageId(delivery.id);
|
|
2939
|
+
let message = await this.loadMessage(messageId);
|
|
2940
|
+
if (!message) {
|
|
2941
|
+
const body = `${delivery.textBody}
|
|
2942
|
+
|
|
2943
|
+
${delivery.href}
|
|
2944
|
+
`;
|
|
2945
|
+
const emails = await EmailCollection.create({ db: this.options.db });
|
|
2946
|
+
try {
|
|
2947
|
+
await emails.create({
|
|
2948
|
+
_insertOnly: true,
|
|
2949
|
+
id: messageId,
|
|
2950
|
+
tenantId: delivery.tenantId,
|
|
2951
|
+
accountId: delivery.accountId,
|
|
2952
|
+
correlationId: notificationDeliveryCorrelationId(delivery.id),
|
|
2953
|
+
subject: delivery.subject,
|
|
2954
|
+
body,
|
|
2955
|
+
textBody: body,
|
|
2956
|
+
toAddresses: JSON.stringify([{
|
|
2957
|
+
address: delivery.recipientAddress,
|
|
2958
|
+
...delivery.recipientName ? { name: delivery.recipientName } : {}
|
|
2959
|
+
}]),
|
|
2960
|
+
sendStatus: "pending",
|
|
2961
|
+
maxRetries: 0
|
|
2962
|
+
});
|
|
2963
|
+
} catch (error) {
|
|
2964
|
+
if (!isUniqueViolationError(error)) throw error;
|
|
2965
|
+
}
|
|
2966
|
+
message = await this.loadMessage(messageId);
|
|
2967
|
+
if (!message) throw new Error("The outbound email could not be stored.");
|
|
2968
|
+
}
|
|
2969
|
+
if (delivery.messageId !== message.id) {
|
|
2970
|
+
if (!await delivery.transition({ messageId: message.id }, { leaseToken })) return null;
|
|
2971
|
+
}
|
|
2972
|
+
return message;
|
|
2973
|
+
}
|
|
2974
|
+
};
|
|
2975
|
+
//#endregion
|
|
2234
2976
|
//#region src/services/PersonaMessagingService.ts
|
|
2235
2977
|
var PersonaMessagingService = class {
|
|
2236
2978
|
constructor(options) {
|
|
@@ -2759,6 +3501,6 @@ var SlackRecipe = class extends SmrtRecipe {
|
|
|
2759
3501
|
ensureBuiltinMessagingProvidersRegistered();
|
|
2760
3502
|
ensureMessagingPermissionsRegistered();
|
|
2761
3503
|
//#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 };
|
|
3504
|
+
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, NotificationDelivery, NotificationDeliveryCollection, 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, listMessagingProviders, messageSendTransport, messagingPrincipalFromPermissions, EmailCollection_exports as n, notificationDeliveryCorrelationId, notificationDeliveryMessageId, EmailAccountCollection_exports as r, registerMessagingProvider, EmailFolderCollection_exports as t };
|
|
2763
3505
|
|
|
2764
3506
|
//# sourceMappingURL=index.js.map
|