@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/models/Message.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import '../__smrt-register__.js';
|
|
7
7
|
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
8
8
|
import type { MessageOptions, MessageSendResult, SendMessageOptions, SendStatus } from "../types.js";
|
|
9
|
+
/** `metadata` key recording that the last failed send may have been accepted. */
|
|
10
|
+
export declare const SEND_OUTCOME_UNKNOWN_METADATA_KEY = "sendOutcomeUnknown";
|
|
9
11
|
export declare class Message extends SmrtObject {
|
|
10
12
|
tenantId: string | null;
|
|
11
13
|
accountId: string;
|
|
@@ -94,6 +96,19 @@ export declare class Message extends SmrtObject {
|
|
|
94
96
|
* Send this message via its account's sender
|
|
95
97
|
*/
|
|
96
98
|
send(options?: SendMessageOptions): Promise<MessageSendResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Whether the latest failed send may still have been accepted by the
|
|
101
|
+
* provider (see `MessageSendResult.outcomeUnknown`).
|
|
102
|
+
*/
|
|
103
|
+
isSendOutcomeUnknown(): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Release a send claim whose sender is gone (a crash or lost lease left the
|
|
106
|
+
* row in `sending`): move it to `failed` with an unknown outcome so the
|
|
107
|
+
* owner of the delivery decision can send it again. Returns false, changing
|
|
108
|
+
* nothing, unless the stored row is still `sending`. Only for callers that
|
|
109
|
+
* have established that no live sender holds the claim.
|
|
110
|
+
*/
|
|
111
|
+
abandonSendAttempt(reason: string): Promise<boolean>;
|
|
97
112
|
/**
|
|
98
113
|
* Retry sending a failed message
|
|
99
114
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Message.d.ts","sourceRoot":"","sources":["../../src/models/Message.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAIL,UAAU,EAIX,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACX,MAAM,aAAU,CAAC;AAElB,qBAOa,OAAQ,SAAQ,UAAU;IAErC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAM/B,SAAS,SAAM;IAQf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IAGhC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAKjC,aAAa,SAAM;IAEnB,QAAQ,SAAM;IAEd,OAAO,SAAM;IAEb,IAAI,SAAM;IAEV,WAAW,SAAM;IAEjB,QAAQ,SAAM;IAEd,WAAW,SAAM;IAEjB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEzB,MAAM,UAAS;IAEf,SAAS,UAAS;IAElB,cAAc,UAAS;IAEvB,IAAI,SAAK;IAIT,QAAQ,SAAM;IAOd,UAAU,EAAE,UAAU,CAAW;IAEjC,MAAM,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE3B,SAAS,SAAM;IAEf,UAAU,SAAK;IAEf,UAAU,SAAK;IAEf,eAAe,EAAE,IAAI,GAAG,IAAI,CAAQ;IAGpC,kBAAkB,SAAM;IAIxB,SAAS,OAAc;IAEvB,SAAS,OAAc;gBAEX,OAAO,GAAE,cAAmB;YAqC1B,qBAAqB;
|
|
1
|
+
{"version":3,"file":"Message.d.ts","sourceRoot":"","sources":["../../src/models/Message.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAIL,UAAU,EAIX,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACX,MAAM,aAAU,CAAC;AAElB,iFAAiF;AACjF,eAAO,MAAM,iCAAiC,uBAAuB,CAAC;AA8BtE,qBAOa,OAAQ,SAAQ,UAAU;IAErC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAM/B,SAAS,SAAM;IAQf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IAGhC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IAKjC,aAAa,SAAM;IAEnB,QAAQ,SAAM;IAEd,OAAO,SAAM;IAEb,IAAI,SAAM;IAEV,WAAW,SAAM;IAEjB,QAAQ,SAAM;IAEd,WAAW,SAAM;IAEjB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAQ;IAEzB,MAAM,UAAS;IAEf,SAAS,UAAS;IAElB,cAAc,UAAS;IAEvB,IAAI,SAAK;IAIT,QAAQ,SAAM;IAOd,UAAU,EAAE,UAAU,CAAW;IAEjC,MAAM,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE3B,SAAS,SAAM;IAEf,UAAU,SAAK;IAEf,UAAU,SAAK;IAEf,eAAe,EAAE,IAAI,GAAG,IAAI,CAAQ;IAGpC,kBAAkB,SAAM;IAIxB,SAAS,OAAc;IAEvB,SAAS,OAAc;gBAEX,OAAO,GAAE,cAAmB;YAqC1B,qBAAqB;IA8DnC;;OAEG;IACH,cAAc,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAS3D;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI;IAI1E;;OAEG;IACH,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAStC;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIhD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAMjC;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAMpC;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,UAAU,CAAC,SAAS,SAAM,GAAG,MAAM;IAMnC;;OAEG;IACG,UAAU;IAWV,WAAW;IASjB;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAW7C;;OAEG;IACG,cAAc;IAapB;;OAEG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoJpE;;;OAGG;IACH,oBAAoB,IAAI,OAAO;IAO/B;;;;;;OAMG;IACG,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB1D;;OAEG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAwBzE;;;;;;;;OAQG;IACH,SAAS,CAAC,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IASjD;;OAEG;IACH,WAAW,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO;IA0BvD;;OAEG;IACH,aAAa,IAAI,OAAO;IAyBxB;;OAEG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;CAapC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import '../__smrt-register__.js';
|
|
3
|
+
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
4
|
+
/** Channels the notification outbox can deliver through. */
|
|
5
|
+
export type NotificationDeliveryChannel = 'email';
|
|
6
|
+
export declare const NOTIFICATION_DELIVERY_CHANNELS: readonly NotificationDeliveryChannel[];
|
|
7
|
+
/**
|
|
8
|
+
* Where a delivery is in its lifecycle.
|
|
9
|
+
*
|
|
10
|
+
* - `pending`: waiting for `nextAttemptAt`.
|
|
11
|
+
* - `sending`: leased by one dispatcher until `leaseExpiresAt`.
|
|
12
|
+
* - `sent`: the provider accepted it (terminal).
|
|
13
|
+
* - `failed`: rejected for good or out of attempts (terminal).
|
|
14
|
+
* - `uncertain`: handed to a provider whose outcome is unknown; waits for
|
|
15
|
+
* reconciliation and is never resent automatically.
|
|
16
|
+
* - `cancelled`: suppressed by the host before handoff (terminal).
|
|
17
|
+
*/
|
|
18
|
+
export type NotificationDeliveryStatus = 'pending' | 'sending' | 'sent' | 'failed' | 'uncertain' | 'cancelled';
|
|
19
|
+
export interface NotificationDeliveryOptions extends SmrtObjectOptions {
|
|
20
|
+
tenantId?: string;
|
|
21
|
+
recipientUserId?: string;
|
|
22
|
+
channel?: NotificationDeliveryChannel;
|
|
23
|
+
sourceRef?: string;
|
|
24
|
+
kind?: string;
|
|
25
|
+
subject?: string;
|
|
26
|
+
textBody?: string;
|
|
27
|
+
href?: string;
|
|
28
|
+
recipientAddress?: string;
|
|
29
|
+
recipientName?: string;
|
|
30
|
+
accountId?: string;
|
|
31
|
+
messageId?: string | null;
|
|
32
|
+
status?: NotificationDeliveryStatus;
|
|
33
|
+
stateVersion?: number;
|
|
34
|
+
transitionId?: string;
|
|
35
|
+
attempts?: number;
|
|
36
|
+
maxAttempts?: number;
|
|
37
|
+
nextAttemptAt?: Date;
|
|
38
|
+
leaseToken?: string | null;
|
|
39
|
+
leaseExpiresAt?: Date | null;
|
|
40
|
+
handedOffAt?: Date | null;
|
|
41
|
+
deliveredAt?: Date | null;
|
|
42
|
+
providerMessageId?: string;
|
|
43
|
+
lastError?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Column values a lease transition may change (camelCase field names). */
|
|
46
|
+
export type NotificationDeliveryTransition = Partial<Pick<NotificationDelivery, 'messageId' | 'status' | 'attempts' | 'nextAttemptAt' | 'leaseToken' | 'leaseExpiresAt' | 'handedOffAt' | 'deliveredAt' | 'providerMessageId' | 'lastError'>>;
|
|
47
|
+
/**
|
|
48
|
+
* One durable, source-keyed notification delivery to one person over one
|
|
49
|
+
* external channel (the outbox row behind `NotificationOutboxService`).
|
|
50
|
+
*
|
|
51
|
+
* `sourceRef` is the idempotency key: one row per
|
|
52
|
+
* `(tenant, recipient, channel, sourceRef)` (the conflict identity), and that
|
|
53
|
+
* row produces at most one outbound `Message`. The recipient address is a
|
|
54
|
+
* snapshot taken from the host's recipient resolver at enqueue time, never a
|
|
55
|
+
* caller-chosen address.
|
|
56
|
+
*
|
|
57
|
+
* Every state change after creation goes through {@link transition}, a
|
|
58
|
+
* compare-and-set on `stateVersion`, so concurrent dispatchers cannot both
|
|
59
|
+
* hold the lease or overwrite each other's outcome. Generated REST/MCP/CLI
|
|
60
|
+
* are off: rows carry recipient addresses and are reached only through the
|
|
61
|
+
* service.
|
|
62
|
+
*/
|
|
63
|
+
export declare class NotificationDelivery extends SmrtObject {
|
|
64
|
+
tenantId: string;
|
|
65
|
+
/** The person being told (`smrt-users:User`, uuid). */
|
|
66
|
+
recipientUserId: string;
|
|
67
|
+
/** Delivery channel; only `email` today. */
|
|
68
|
+
channel: NotificationDeliveryChannel;
|
|
69
|
+
/** Idempotency key for the underlying event (see class docs). */
|
|
70
|
+
sourceRef: string;
|
|
71
|
+
/** Host-defined type, e.g. `project-question.asked`. */
|
|
72
|
+
kind: string;
|
|
73
|
+
/** Email subject line. */
|
|
74
|
+
subject: string;
|
|
75
|
+
/** Plain-text body; the deep link is appended on its own line. */
|
|
76
|
+
textBody: string;
|
|
77
|
+
/** Absolute deep link on the host's configured origin (no credentials). */
|
|
78
|
+
href: string;
|
|
79
|
+
/** Address snapshot from the host recipient resolver. */
|
|
80
|
+
recipientAddress: string;
|
|
81
|
+
/** Display name snapshot from the host recipient resolver. */
|
|
82
|
+
recipientName: string;
|
|
83
|
+
/** Sender `EmailAccount` in the same tenant. */
|
|
84
|
+
accountId: string;
|
|
85
|
+
/** The single outbound `Message` (created under the first lease). */
|
|
86
|
+
messageId: string | null;
|
|
87
|
+
status: NotificationDeliveryStatus;
|
|
88
|
+
/** Compare-and-set counter; bumped by every transition. */
|
|
89
|
+
stateVersion: number;
|
|
90
|
+
/** Unique id of the transition that wrote the current state. */
|
|
91
|
+
transitionId: string;
|
|
92
|
+
/** Leases acquired so far (provider handoffs are a subset). */
|
|
93
|
+
attempts: number;
|
|
94
|
+
/** Lease budget before the delivery fails for good. */
|
|
95
|
+
maxAttempts: number;
|
|
96
|
+
/** Earliest time a dispatcher may lease a `pending` row. */
|
|
97
|
+
nextAttemptAt: Date;
|
|
98
|
+
/** Proof of the current lease holder; cleared when the lease ends. */
|
|
99
|
+
leaseToken: string | null;
|
|
100
|
+
/** When the current lease lapses and the row becomes recoverable. */
|
|
101
|
+
leaseExpiresAt: Date | null;
|
|
102
|
+
/** Set just before the current attempt reaches the provider. */
|
|
103
|
+
handedOffAt: Date | null;
|
|
104
|
+
/** When the provider accepted the message. */
|
|
105
|
+
deliveredAt: Date | null;
|
|
106
|
+
/** Provider message id, when the provider returns one. */
|
|
107
|
+
providerMessageId: string;
|
|
108
|
+
/** The latest failure or uncertainty reason. */
|
|
109
|
+
lastError: string;
|
|
110
|
+
constructor(options?: NotificationDeliveryOptions);
|
|
111
|
+
/** Whether no further automatic work will happen on this row. */
|
|
112
|
+
get isSettled(): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Apply `changes` only if the stored row is still at `stateVersion` (and,
|
|
115
|
+
* when `leaseToken` is given, still held by that lease). Returns whether
|
|
116
|
+
* this caller won; on success the instance reflects the stored row.
|
|
117
|
+
*
|
|
118
|
+
* PostgreSQL and remote LibSQL evaluate the guard in one conditional
|
|
119
|
+
* UPDATE; local embedded engines use the process write queue, matching
|
|
120
|
+
* `Message.send()`'s claim. The durable row is re-read either way, and the
|
|
121
|
+
* write is recognised by its unique `transitionId`, because DuckDB's update
|
|
122
|
+
* result over-reports affected rows.
|
|
123
|
+
*/
|
|
124
|
+
transition(changes: NotificationDeliveryTransition, expected?: {
|
|
125
|
+
leaseToken?: string;
|
|
126
|
+
}): Promise<boolean>;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=NotificationDelivery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationDelivery.d.ts","sourceRoot":"","sources":["../../src/models/NotificationDelivery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import '../__smrt-register__.js';
|
|
3
|
+
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
4
|
+
import type { NotificationDeliveryChannel } from './NotificationDelivery.js';
|
|
5
|
+
/** `*` (everything in the tenant) or a host key such as `project:<uuid>`. */
|
|
6
|
+
export declare const NOTIFICATION_OPT_OUT_SCOPE_PATTERN: RegExp;
|
|
7
|
+
/** The scope that matches every notification of the channel in the tenant. */
|
|
8
|
+
export declare const NOTIFICATION_OPT_OUT_ALL_SCOPE = "*";
|
|
9
|
+
export declare function isValidNotificationOptOutScope(scope: unknown): scope is string;
|
|
10
|
+
export interface NotificationOptOutOptions extends SmrtObjectOptions {
|
|
11
|
+
tenantId?: string;
|
|
12
|
+
recipientUserId?: string;
|
|
13
|
+
channel?: NotificationDeliveryChannel;
|
|
14
|
+
scope?: string;
|
|
15
|
+
optedOutAt?: Date | null;
|
|
16
|
+
updatedBy?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One person's opt-out of one notification channel for one scope, in one
|
|
20
|
+
* tenant: the preference store behind the outbox's `isSuppressed` hook.
|
|
21
|
+
*
|
|
22
|
+
* Identity is `(tenant, recipient, channel, scope)` (the conflict identity).
|
|
23
|
+
* `optedOutAt` is set while the person is opted out and cleared by an opt-in,
|
|
24
|
+
* so the row keeps its history. `scope` is a host-defined key validated by
|
|
25
|
+
* {@link NOTIFICATION_OPT_OUT_SCOPE_PATTERN}. Read and change rows through
|
|
26
|
+
* `NotificationOptOutService`; the generated REST/MCP/CLI are off because the
|
|
27
|
+
* service is the authorization boundary.
|
|
28
|
+
*/
|
|
29
|
+
export declare class NotificationOptOut extends SmrtObject {
|
|
30
|
+
tenantId: string;
|
|
31
|
+
/** The person who opted out (`smrt-users:User`, uuid). */
|
|
32
|
+
recipientUserId: string;
|
|
33
|
+
/** Channel opted out of; only `email` today. */
|
|
34
|
+
channel: NotificationDeliveryChannel;
|
|
35
|
+
/** `*` or a host key such as `project:<uuid>`. */
|
|
36
|
+
scope: string;
|
|
37
|
+
/** When the opt-out took effect; null once the person opted back in. */
|
|
38
|
+
optedOutAt: Date | null;
|
|
39
|
+
/** Who last changed the row: the person's user id, or `system` for host code. */
|
|
40
|
+
updatedBy: string;
|
|
41
|
+
constructor(options?: NotificationOptOutOptions);
|
|
42
|
+
get isOptedOut(): boolean;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=NotificationOptOut.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationOptOut.d.ts","sourceRoot":"","sources":["../../src/models/NotificationOptOut.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAGL,UAAU,EAEX,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAE7E,6EAA6E;AAC7E,eAAO,MAAM,kCAAkC,QACI,CAAC;AAEpD,8EAA8E;AAC9E,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,MAAM,CAIjB;AAED,MAAM,WAAW,yBAA0B,SAAQ,iBAAiB;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,2BAA2B,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,qBAQa,kBAAmB,SAAQ,UAAU;IAEhD,QAAQ,SAAM;IAEd,0DAA0D;IAE1D,eAAe,SAAM;IAErB,gDAAgD;IAEhD,OAAO,EAAE,2BAA2B,CAAW;IAE/C,kDAAkD;IAElD,KAAK,SAAM;IAEX,wEAAwE;IACxE,UAAU,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE/B,iFAAiF;IACjF,SAAS,SAAM;gBAEH,OAAO,GAAE,yBAA8B;IAWnD,IAAI,UAAU,IAAI,OAAO,CAExB;CACF"}
|
|
@@ -23,4 +23,13 @@ export declare class EmailSender implements MessageSenderInterface {
|
|
|
23
23
|
*/
|
|
24
24
|
private collectAttachments;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether a provider send error proves the message was not accepted: the
|
|
28
|
+
* server answered with an SMTP reply code, the SDK classified it as an
|
|
29
|
+
* authentication/validation failure, a send error accepted no recipient, or
|
|
30
|
+
* the connection was never made. Anything else (a timeout, a reset
|
|
31
|
+
* connection, an unclassified error) may have happened after the server took
|
|
32
|
+
* the message, so its outcome is unknown.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isDefiniteSendRejection(error: unknown): boolean;
|
|
26
35
|
//# sourceMappingURL=EmailSender.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailSender.d.ts","sourceRoot":"","sources":["../../src/senders/EmailSender.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAAwB,CAAC;AAC3D,OAAO,KAAK,EACV,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,aAAU,CAAC;AAElB,qBAAa,WAAY,YAAW,sBAAsB;IACxD,QAAQ,CAAC,YAAY,WAAW;IAChC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAe;gBAElB,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY;IAKtD,OAAO,IAAI,OAAO;IAIZ,IAAI,CACR,OAAO,EAAE,KAAK,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"EmailSender.d.ts","sourceRoot":"","sources":["../../src/senders/EmailSender.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAAwB,CAAC;AAC3D,OAAO,KAAK,EACV,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,aAAU,CAAC;AAElB,qBAAa,WAAY,YAAW,sBAAsB;IACxD,QAAQ,CAAC,YAAY,WAAW;IAChC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAe;gBAElB,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY;IAKtD,OAAO,IAAI,OAAO;IAIZ,IAAI,CACR,OAAO,EAAE,KAAK,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,iBAAiB,CAAC;IA+D7B;;;;;;;;OAQG;YACW,kBAAkB;CAyEjC;AAkBD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmB/D"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import { type NotificationDeliveryChannel } from '../models/NotificationDelivery.js';
|
|
3
|
+
import { NotificationOptOut } from '../models/NotificationOptOut.js';
|
|
4
|
+
import type { NotificationSuppressionRequest } from './NotificationOutboxService.js';
|
|
5
|
+
/** The signed-in person making a request, as the host authenticated them. */
|
|
6
|
+
export interface NotificationOptOutPrincipal {
|
|
7
|
+
userId: string;
|
|
8
|
+
/**
|
|
9
|
+
* The tenant the principal is authenticated in. Required and must equal the
|
|
10
|
+
* request's tenant: a principal never acts in another tenant. A host whose
|
|
11
|
+
* recipient is authenticated elsewhere (e.g. a member of a child tenant)
|
|
12
|
+
* authorizes the recipient itself and makes a trusted call without a
|
|
13
|
+
* principal instead.
|
|
14
|
+
*/
|
|
15
|
+
tenantId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface NotificationOptOutKey {
|
|
18
|
+
tenantId: string;
|
|
19
|
+
recipientUserId: string;
|
|
20
|
+
channel: NotificationDeliveryChannel;
|
|
21
|
+
/** `*` or a host key such as `project:<uuid>`. */
|
|
22
|
+
scope: string;
|
|
23
|
+
}
|
|
24
|
+
export interface NotificationOptOutChange extends NotificationOptOutKey {
|
|
25
|
+
/**
|
|
26
|
+
* The signed-in person, authenticated in this tenant. When present only the
|
|
27
|
+
* recipient themself may change the row, and only in the principal's own
|
|
28
|
+
* tenant; when omitted the call is trusted host code that has already
|
|
29
|
+
* authenticated the recipient (e.g. a signed unsubscribe link, or a host
|
|
30
|
+
* route whose member belongs to a child tenant of the delivery tenant).
|
|
31
|
+
*/
|
|
32
|
+
principal?: NotificationOptOutPrincipal;
|
|
33
|
+
}
|
|
34
|
+
export interface NotificationOptOutListRequest {
|
|
35
|
+
tenantId: string;
|
|
36
|
+
recipientUserId: string;
|
|
37
|
+
principal: NotificationOptOutPrincipal;
|
|
38
|
+
}
|
|
39
|
+
export type NotificationOptOutDenialReason = 'principal' | 'tenant';
|
|
40
|
+
/** Thrown when a principal tries to read or change another person's opt-out. */
|
|
41
|
+
export declare class NotificationOptOutDeniedError extends Error {
|
|
42
|
+
readonly reason: NotificationOptOutDenialReason;
|
|
43
|
+
constructor(reason: NotificationOptOutDenialReason, message: string);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The per-recipient opt-out ledger behind the outbox's `isSuppressed` hook.
|
|
47
|
+
*
|
|
48
|
+
* Writes are idempotent and, with a `principal`, limited to the recipient's
|
|
49
|
+
* own rows. {@link isOptedOut} is a trusted read for suppression: it runs
|
|
50
|
+
* where the recipient is not the caller (a manager enqueues, a worker
|
|
51
|
+
* dispatches) and is always scoped to the given tenant.
|
|
52
|
+
*/
|
|
53
|
+
export declare class NotificationOptOutService {
|
|
54
|
+
private readonly options;
|
|
55
|
+
constructor(options: {
|
|
56
|
+
db: SmrtClassOptions['db'];
|
|
57
|
+
});
|
|
58
|
+
private collection;
|
|
59
|
+
/** Opt the recipient out of `scope`; a repeat leaves the row untouched. */
|
|
60
|
+
optOut(change: NotificationOptOutChange): Promise<NotificationOptOut>;
|
|
61
|
+
/** Clear an opt-out; a repeat, or no row at all, is a no-op. */
|
|
62
|
+
optIn(change: NotificationOptOutChange): Promise<NotificationOptOut>;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the recipient has opted out of `scope` (a `*` row matches every
|
|
65
|
+
* scope). Trusted read: no principal, always under the tenant's scope.
|
|
66
|
+
*/
|
|
67
|
+
isOptedOut(key: NotificationOptOutKey): Promise<boolean>;
|
|
68
|
+
/** The recipient's own active opt-outs, in one tenant. */
|
|
69
|
+
listForRecipient(request: NotificationOptOutListRequest): Promise<NotificationOptOut[]>;
|
|
70
|
+
private change;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build an `isSuppressed` function for `NotificationOutboxOptions` from the
|
|
74
|
+
* ledger. `scopeOf` maps a notification to the scope a recipient can opt out
|
|
75
|
+
* of (e.g. `project:<uuid>` from its `sourceRef`), or null when the
|
|
76
|
+
* notification is not opt-out-able. A `*` opt-out suppresses any notification
|
|
77
|
+
* `scopeOf` maps to a scope; returning `'*'` checks only the tenant-wide row.
|
|
78
|
+
*/
|
|
79
|
+
export declare function optOutSuppression(service: Pick<NotificationOptOutService, 'isOptedOut'>, scopeOf: (request: NotificationSuppressionRequest) => string | null | Promise<string | null>): (request: NotificationSuppressionRequest) => Promise<boolean>;
|
|
80
|
+
//# sourceMappingURL=NotificationOptOutService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationOptOutService.d.ts","sourceRoot":"","sources":["../../src/services/NotificationOptOutService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAGL,kBAAkB,EACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAErF,6EAA6E;AAC7E,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,2BAA2B,CAAC;IACrC,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;IACrE;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,2BAA2B,CAAC;CACzC;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,2BAA2B,CAAC;CACxC;AAED,MAAM,MAAM,8BAA8B,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEpE,gFAAgF;AAChF,qBAAa,6BAA8B,SAAQ,KAAK;IAEpD,QAAQ,CAAC,MAAM,EAAE,8BAA8B;gBAAtC,MAAM,EAAE,8BAA8B,EAC/C,OAAO,EAAE,MAAM;CAKlB;AA+CD;;;;;;;GAOG;AACH,qBAAa,yBAAyB;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE;QAAE,EAAE,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAA;KAAE;IAEpE,OAAO,CAAC,UAAU;IAIlB,2EAA2E;IACrE,MAAM,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAI3E,gEAAgE;IAC1D,KAAK,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAI1E;;;OAGG;IACG,UAAU,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC;IAc9D,0DAA0D;IACpD,gBAAgB,CACpB,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,kBAAkB,EAAE,CAAC;YAmBlB,MAAM;CA6DrB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,IAAI,CAAC,yBAAyB,EAAE,YAAY,CAAC,EACtD,OAAO,EAAE,CACP,OAAO,EAAE,8BAA8B,KACpC,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAC1C,CAAC,OAAO,EAAE,8BAA8B,KAAK,OAAO,CAAC,OAAO,CAAC,CAW/D"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import type { Message } from '../models/Message.js';
|
|
3
|
+
import type { NotificationDelivery, NotificationDeliveryChannel } from '../models/NotificationDelivery.js';
|
|
4
|
+
import { type MessagingPrincipal } from '../permissions.js';
|
|
5
|
+
/** What a transport knows about one provider handoff. */
|
|
6
|
+
export type NotificationTransportOutcome = {
|
|
7
|
+
status: 'accepted';
|
|
8
|
+
providerMessageId?: string;
|
|
9
|
+
} | {
|
|
10
|
+
status: 'rejected';
|
|
11
|
+
error: string;
|
|
12
|
+
retryable: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
status: 'unknown';
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
/** What a transport can say about an earlier handoff during recovery. */
|
|
18
|
+
export type NotificationTransportReconciliation = NotificationTransportOutcome | {
|
|
19
|
+
status: 'not-sent';
|
|
20
|
+
};
|
|
21
|
+
export interface NotificationTransportRequest {
|
|
22
|
+
delivery: NotificationDelivery;
|
|
23
|
+
/** The delivery's single outbound message (an `Email` for `email`). */
|
|
24
|
+
message: Message;
|
|
25
|
+
/** Stable per delivery; pass it to providers that dedupe on a key. */
|
|
26
|
+
idempotencyKey: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Hands one outbound message to a provider. The default
|
|
30
|
+
* ({@link messageSendTransport}) delivers through `Message.send()` and the
|
|
31
|
+
* message's account sender; tests and hosts with their own provider inject
|
|
32
|
+
* another.
|
|
33
|
+
*/
|
|
34
|
+
export interface NotificationTransport {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/**
|
|
37
|
+
* True only when the provider deduplicates on `idempotencyKey`, so sending
|
|
38
|
+
* again after an unknown outcome cannot deliver twice. Then an unknown
|
|
39
|
+
* outcome is retried; otherwise it becomes `uncertain`.
|
|
40
|
+
*/
|
|
41
|
+
readonly idempotent?: boolean;
|
|
42
|
+
send(request: NotificationTransportRequest): Promise<NotificationTransportOutcome>;
|
|
43
|
+
/**
|
|
44
|
+
* After a crash or lease loss following a handoff, report what is known
|
|
45
|
+
* from durable state. Omit when nothing can be known (outcome: uncertain).
|
|
46
|
+
*/
|
|
47
|
+
reconcile?(request: NotificationTransportRequest): Promise<NotificationTransportReconciliation>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Delivers through the canonical `Message.send()` (account → provider sender)
|
|
51
|
+
* and reconciles from the message's own `sendStatus`.
|
|
52
|
+
*
|
|
53
|
+
* SMTP and the SDK email providers take no idempotency key, so this
|
|
54
|
+
* transport is not idempotent: a handoff whose outcome is lost stays
|
|
55
|
+
* `uncertain` instead of risking a second email.
|
|
56
|
+
*/
|
|
57
|
+
export declare const messageSendTransport: NotificationTransport;
|
|
58
|
+
export type NotificationOutboxDenialReason = 'permission' | 'recipient' | 'sender' | 'link';
|
|
59
|
+
/** Thrown when the host's policy refuses an enqueue; nothing is stored. */
|
|
60
|
+
export declare class NotificationOutboxDeniedError extends Error {
|
|
61
|
+
readonly reason: NotificationOutboxDenialReason;
|
|
62
|
+
constructor(reason: NotificationOutboxDenialReason, message: string);
|
|
63
|
+
}
|
|
64
|
+
export interface NotificationRecipientRequest {
|
|
65
|
+
tenantId: string;
|
|
66
|
+
recipientUserId: string;
|
|
67
|
+
channel: NotificationDeliveryChannel;
|
|
68
|
+
}
|
|
69
|
+
export interface NotificationRecipient {
|
|
70
|
+
address: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface NotificationSuppressionRequest extends NotificationRecipientRequest {
|
|
74
|
+
kind: string;
|
|
75
|
+
sourceRef: string;
|
|
76
|
+
}
|
|
77
|
+
export interface NotificationOutboxOptions {
|
|
78
|
+
db: SmrtClassOptions['db'];
|
|
79
|
+
/**
|
|
80
|
+
* Host authorization for recipients: the address of `recipientUserId` when
|
|
81
|
+
* the host allows notifying them in `tenantId` (e.g. a member of that
|
|
82
|
+
* tenant), otherwise null, which denies the enqueue. Callers never supply
|
|
83
|
+
* an address.
|
|
84
|
+
*/
|
|
85
|
+
resolveRecipient(request: NotificationRecipientRequest): Promise<NotificationRecipient | null> | NotificationRecipient | null;
|
|
86
|
+
/** The sending `EmailAccount` id for a tenant (deployment setup). */
|
|
87
|
+
resolveSenderAccountId(request: {
|
|
88
|
+
tenantId: string;
|
|
89
|
+
}): Promise<string | null> | string | null;
|
|
90
|
+
/** Absolute http(s) URL deep links are built on, e.g. `https://app.example.com`. */
|
|
91
|
+
linkBaseUrl: string;
|
|
92
|
+
/** Optional opt-out hook, checked at enqueue and again before handoff. */
|
|
93
|
+
isSuppressed?(request: NotificationSuppressionRequest): Promise<boolean> | boolean;
|
|
94
|
+
/** Provider seam; defaults to {@link messageSendTransport}. */
|
|
95
|
+
transport?: NotificationTransport;
|
|
96
|
+
/**
|
|
97
|
+
* When set, enqueue and reconcile require `messages.send` in the tenant.
|
|
98
|
+
* Omit only for trusted system code that performs its own authorization.
|
|
99
|
+
*/
|
|
100
|
+
principal?: MessagingPrincipal;
|
|
101
|
+
/** Lease length; must exceed the slowest provider handoff (default 5 min). */
|
|
102
|
+
leaseMs?: number;
|
|
103
|
+
/** Lease budget per delivery (default 5). */
|
|
104
|
+
maxAttempts?: number;
|
|
105
|
+
/** Delay before attempt `n + 1` after `n` failed (default 1 min · 2ⁿ⁻¹, ≤ 1 h). */
|
|
106
|
+
retryDelayMs?(attempts: number): number;
|
|
107
|
+
/** Clock (tests). */
|
|
108
|
+
now?(): Date;
|
|
109
|
+
}
|
|
110
|
+
export interface NotificationEmailInput {
|
|
111
|
+
tenantId: string;
|
|
112
|
+
recipientUserId: string;
|
|
113
|
+
/** Idempotency key for the event, e.g. `project-question:<id>:asked`. */
|
|
114
|
+
sourceRef: string;
|
|
115
|
+
/** Host-defined type, e.g. `project-question.asked`. */
|
|
116
|
+
kind: string;
|
|
117
|
+
/** One line; CR/LF are rejected. */
|
|
118
|
+
subject: string;
|
|
119
|
+
/** Plain-text body; the deep link is appended on its own line. */
|
|
120
|
+
text: string;
|
|
121
|
+
/** Host-relative path of the deep link (`/portal/...`). */
|
|
122
|
+
path: string;
|
|
123
|
+
/** Do not send before this time (default: now). */
|
|
124
|
+
notBefore?: Date;
|
|
125
|
+
}
|
|
126
|
+
export interface NotificationEnqueueResult {
|
|
127
|
+
delivery: NotificationDelivery;
|
|
128
|
+
/** False when `sourceRef` matched an existing delivery (returned untouched). */
|
|
129
|
+
created: boolean;
|
|
130
|
+
}
|
|
131
|
+
export type NotificationDispatchOutcome = 'sent' | 'retry-scheduled' | 'failed' | 'uncertain' | 'cancelled' | 'lease-lost';
|
|
132
|
+
export interface NotificationDispatchReport {
|
|
133
|
+
/** Rows this call won a lease on. */
|
|
134
|
+
leased: number;
|
|
135
|
+
outcomes: Array<{
|
|
136
|
+
deliveryId: string;
|
|
137
|
+
tenantId: string;
|
|
138
|
+
outcome: NotificationDispatchOutcome;
|
|
139
|
+
}>;
|
|
140
|
+
}
|
|
141
|
+
export interface NotificationDispatchOptions {
|
|
142
|
+
/** Tenants to dispatch; omit to discover due rows across all tenants. */
|
|
143
|
+
tenantIds?: readonly string[];
|
|
144
|
+
/** Maximum rows to lease in this call (default 25, max 200). */
|
|
145
|
+
limit?: number;
|
|
146
|
+
}
|
|
147
|
+
export type NotificationReconcileOutcome = 'sent' | 'retry' | 'failed';
|
|
148
|
+
/**
|
|
149
|
+
* The id of a delivery's one outbound message: a name-based uuid derived from
|
|
150
|
+
* the delivery id, so every dispatcher computes the same identity.
|
|
151
|
+
*/
|
|
152
|
+
export declare function notificationDeliveryMessageId(deliveryId: string): Promise<string>;
|
|
153
|
+
/** The correlation id that ties a delivery to its one outbound message. */
|
|
154
|
+
export declare function notificationDeliveryCorrelationId(deliveryId: string): string;
|
|
155
|
+
/**
|
|
156
|
+
* Build an absolute deep link from a host-relative `path` on `baseUrl`.
|
|
157
|
+
*
|
|
158
|
+
* The link always stays on the configured origin (no `//host`, backslash, or
|
|
159
|
+
* scheme tricks) and never carries credentials: the destination is an
|
|
160
|
+
* ordinary authenticated route, so the recipient signs in as themselves.
|
|
161
|
+
*/
|
|
162
|
+
export declare function buildNotificationLink(baseUrl: string, path: string): string;
|
|
163
|
+
/**
|
|
164
|
+
* Source-keyed, durable notification email outbox.
|
|
165
|
+
*
|
|
166
|
+
* `enqueueEmail()` stores one `NotificationDelivery` per
|
|
167
|
+
* `(tenant, recipient, email, sourceRef)` — a repeated event returns the
|
|
168
|
+
* existing row — after the host has authorized the recipient and sender.
|
|
169
|
+
* `dispatchDue()` leases due rows with a compare-and-set, creates the single
|
|
170
|
+
* outbound `Email` under the lease, hands it to the transport, and records
|
|
171
|
+
* the outcome; it is safe to run from several workers at once and to crash
|
|
172
|
+
* at any point. See `packages/messages/agents/notification-outbox.md`.
|
|
173
|
+
*/
|
|
174
|
+
export declare class NotificationOutboxService {
|
|
175
|
+
private readonly options;
|
|
176
|
+
private readonly transport;
|
|
177
|
+
private readonly leaseMs;
|
|
178
|
+
private readonly maxAttempts;
|
|
179
|
+
private readonly retryDelayMs;
|
|
180
|
+
private readonly now;
|
|
181
|
+
constructor(options: NotificationOutboxOptions);
|
|
182
|
+
private collection;
|
|
183
|
+
private authorize;
|
|
184
|
+
/** Store one email delivery for the event unless `sourceRef` already has one. */
|
|
185
|
+
enqueueEmail(input: NotificationEmailInput): Promise<NotificationEnqueueResult>;
|
|
186
|
+
/** Lease and deliver due rows; returns what happened to each leased row. */
|
|
187
|
+
dispatchDue(options?: NotificationDispatchOptions): Promise<NotificationDispatchReport>;
|
|
188
|
+
/** Deliveries waiting for a reconciliation decision, oldest first. */
|
|
189
|
+
listUncertain(options: {
|
|
190
|
+
tenantId: string;
|
|
191
|
+
limit?: number;
|
|
192
|
+
}): Promise<NotificationDelivery[]>;
|
|
193
|
+
/**
|
|
194
|
+
* Settle an `uncertain` delivery after checking the provider out of band:
|
|
195
|
+
* `sent` (it arrived), `failed` (give up), or `retry` (send again,
|
|
196
|
+
* accepting the risk of a duplicate; a message left claimed by a dead
|
|
197
|
+
* sender is released first). Returns false when the row is not uncertain
|
|
198
|
+
* any more, or for `retry` when the message is recorded as sent.
|
|
199
|
+
*/
|
|
200
|
+
reconcile(input: {
|
|
201
|
+
tenantId: string;
|
|
202
|
+
deliveryId: string;
|
|
203
|
+
outcome: NotificationReconcileOutcome;
|
|
204
|
+
note?: string;
|
|
205
|
+
}): Promise<boolean>;
|
|
206
|
+
private requireSenderAccount;
|
|
207
|
+
private isSuppressed;
|
|
208
|
+
private findDue;
|
|
209
|
+
/** Lease one candidate and carry it to an outcome; null when not leased. */
|
|
210
|
+
private dispatchOne;
|
|
211
|
+
private recoverHandoff;
|
|
212
|
+
private applyOutcome;
|
|
213
|
+
private scheduleRetry;
|
|
214
|
+
/** End the lease in a terminal status; reports lease loss instead of overwriting. */
|
|
215
|
+
private settle;
|
|
216
|
+
private endLease;
|
|
217
|
+
private loadMessage;
|
|
218
|
+
/**
|
|
219
|
+
* The delivery's single outbound `Email`, created under the lease. Its id
|
|
220
|
+
* is derived from the delivery id and inserted insert-only, so a stale
|
|
221
|
+
* dispatcher that resumes after its lease was taken over hits the stored
|
|
222
|
+
* row (a unique violation) instead of creating a second email.
|
|
223
|
+
*/
|
|
224
|
+
private ensureMessage;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=NotificationOutboxService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationOutboxService.d.ts","sourceRoot":"","sources":["../../src/services/NotificationOutboxService.ts"],"names":[],"mappings":"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"}
|