@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/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-messages",
|
|
6
|
-
"packageVersion": "0.60.
|
|
6
|
+
"packageVersion": "0.60.11",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "fa496fbecf14dd88cff8c80445d4e21125abf2242b4bae0d1ee093bf2875216b",
|
|
11
|
+
"packageJson": "47ae1990a3090feb3d7373881e730c091a23273015fde2bea5b12f9c3e53cf4d",
|
|
12
|
+
"agents": "25944fbc57127569409696f81aa8bac48529378364e4a59f57586b16780281ae",
|
|
13
|
+
"moduleDoc:agents/notification-outbox.md": "ba0479c05ffafd22be62e20c0fb8c6eb6db9294e95fd6d988ac1e2f47510e98c"
|
|
13
14
|
},
|
|
14
15
|
"exports": [
|
|
15
16
|
".",
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
"typescript"
|
|
80
81
|
],
|
|
81
82
|
"risks": [
|
|
82
|
-
"cross-package-refs:
|
|
83
|
+
"cross-package-refs:9",
|
|
83
84
|
"sensitive-fields-excluded",
|
|
84
85
|
"sti-inheritance"
|
|
85
86
|
],
|
|
@@ -2805,6 +2806,40 @@
|
|
|
2805
2806
|
"tags": [],
|
|
2806
2807
|
"risks": []
|
|
2807
2808
|
},
|
|
2809
|
+
{
|
|
2810
|
+
"name": "NotificationDeliveryCollection",
|
|
2811
|
+
"qualifiedName": "@happyvertical/smrt-messages:NotificationDeliveryCollection",
|
|
2812
|
+
"collection": "notificationdeliveries",
|
|
2813
|
+
"tableName": "notification_deliveries",
|
|
2814
|
+
"packageName": "@happyvertical/smrt-messages",
|
|
2815
|
+
"extends": "SmrtCollection",
|
|
2816
|
+
"fields": [],
|
|
2817
|
+
"relationships": [],
|
|
2818
|
+
"methods": [],
|
|
2819
|
+
"surfaces": [],
|
|
2820
|
+
"relationshipFeatures": [
|
|
2821
|
+
"uuidColumns"
|
|
2822
|
+
],
|
|
2823
|
+
"tags": [],
|
|
2824
|
+
"risks": []
|
|
2825
|
+
},
|
|
2826
|
+
{
|
|
2827
|
+
"name": "NotificationOptOutCollection",
|
|
2828
|
+
"qualifiedName": "@happyvertical/smrt-messages:NotificationOptOutCollection",
|
|
2829
|
+
"collection": "notificationoptouts",
|
|
2830
|
+
"tableName": "notification_opt_outs",
|
|
2831
|
+
"packageName": "@happyvertical/smrt-messages",
|
|
2832
|
+
"extends": "SmrtCollection",
|
|
2833
|
+
"fields": [],
|
|
2834
|
+
"relationships": [],
|
|
2835
|
+
"methods": [],
|
|
2836
|
+
"surfaces": [],
|
|
2837
|
+
"relationshipFeatures": [
|
|
2838
|
+
"uuidColumns"
|
|
2839
|
+
],
|
|
2840
|
+
"tags": [],
|
|
2841
|
+
"risks": []
|
|
2842
|
+
},
|
|
2808
2843
|
{
|
|
2809
2844
|
"name": "PersonaMessageRouteCollection",
|
|
2810
2845
|
"qualifiedName": "@happyvertical/smrt-messages:PersonaMessageRouteCollection",
|
|
@@ -4123,6 +4158,7 @@
|
|
|
4123
4158
|
}
|
|
4124
4159
|
],
|
|
4125
4160
|
"methods": [
|
|
4161
|
+
"abandonSendAttempt",
|
|
4126
4162
|
"createForward",
|
|
4127
4163
|
"createReply",
|
|
4128
4164
|
"getAccount",
|
|
@@ -4140,6 +4176,7 @@
|
|
|
4140
4176
|
"getThreadEmails",
|
|
4141
4177
|
"getThreadMessages",
|
|
4142
4178
|
"getToAddresses",
|
|
4179
|
+
"isSendOutcomeUnknown",
|
|
4143
4180
|
"isUnread",
|
|
4144
4181
|
"markRead",
|
|
4145
4182
|
"markUnread",
|
|
@@ -4153,6 +4190,14 @@
|
|
|
4153
4190
|
"toggleFlagged"
|
|
4154
4191
|
],
|
|
4155
4192
|
"methodSignatures": [
|
|
4193
|
+
{
|
|
4194
|
+
"name": "abandonSendAttempt",
|
|
4195
|
+
"async": true,
|
|
4196
|
+
"params": [
|
|
4197
|
+
"reason: string"
|
|
4198
|
+
],
|
|
4199
|
+
"returns": "Promise<boolean>"
|
|
4200
|
+
},
|
|
4156
4201
|
{
|
|
4157
4202
|
"name": "createForward",
|
|
4158
4203
|
"returns": "Email"
|
|
@@ -4233,6 +4278,10 @@
|
|
|
4233
4278
|
"name": "getToAddresses",
|
|
4234
4279
|
"returns": "Array<object>"
|
|
4235
4280
|
},
|
|
4281
|
+
{
|
|
4282
|
+
"name": "isSendOutcomeUnknown",
|
|
4283
|
+
"returns": "boolean"
|
|
4284
|
+
},
|
|
4236
4285
|
{
|
|
4237
4286
|
"name": "isUnread",
|
|
4238
4287
|
"returns": "boolean"
|
|
@@ -4460,6 +4509,18 @@
|
|
|
4460
4509
|
"operation": "send",
|
|
4461
4510
|
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4462
4511
|
},
|
|
4512
|
+
{
|
|
4513
|
+
"kind": "cli",
|
|
4514
|
+
"name": "email_isSendOutcomeUnknown",
|
|
4515
|
+
"operation": "isSendOutcomeUnknown",
|
|
4516
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4517
|
+
},
|
|
4518
|
+
{
|
|
4519
|
+
"kind": "cli",
|
|
4520
|
+
"name": "email_abandonSendAttempt",
|
|
4521
|
+
"operation": "abandonSendAttempt",
|
|
4522
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4523
|
+
},
|
|
4463
4524
|
{
|
|
4464
4525
|
"kind": "cli",
|
|
4465
4526
|
"name": "email_retrySend",
|
|
@@ -4558,6 +4619,13 @@
|
|
|
4558
4619
|
}
|
|
4559
4620
|
],
|
|
4560
4621
|
"withheldSurfaces": [
|
|
4622
|
+
{
|
|
4623
|
+
"kind": "api",
|
|
4624
|
+
"operation": "abandonSendAttempt",
|
|
4625
|
+
"code": "not-included",
|
|
4626
|
+
"reason": "not listed in api.include",
|
|
4627
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4628
|
+
},
|
|
4561
4629
|
{
|
|
4562
4630
|
"kind": "api",
|
|
4563
4631
|
"operation": "createForward",
|
|
@@ -4677,6 +4745,13 @@
|
|
|
4677
4745
|
"reason": "not listed in api.include",
|
|
4678
4746
|
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4679
4747
|
},
|
|
4748
|
+
{
|
|
4749
|
+
"kind": "api",
|
|
4750
|
+
"operation": "isSendOutcomeUnknown",
|
|
4751
|
+
"code": "not-included",
|
|
4752
|
+
"reason": "not listed in api.include",
|
|
4753
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4754
|
+
},
|
|
4680
4755
|
{
|
|
4681
4756
|
"kind": "api",
|
|
4682
4757
|
"operation": "isUnread",
|
|
@@ -6090,6 +6165,7 @@
|
|
|
6090
6165
|
}
|
|
6091
6166
|
],
|
|
6092
6167
|
"methods": [
|
|
6168
|
+
"abandonSendAttempt",
|
|
6093
6169
|
"createForward",
|
|
6094
6170
|
"createReply",
|
|
6095
6171
|
"getAccount",
|
|
@@ -6099,6 +6175,7 @@
|
|
|
6099
6175
|
"getPreview",
|
|
6100
6176
|
"getThreadMessages",
|
|
6101
6177
|
"getToAddresses",
|
|
6178
|
+
"isSendOutcomeUnknown",
|
|
6102
6179
|
"isUnread",
|
|
6103
6180
|
"markRead",
|
|
6104
6181
|
"markUnread",
|
|
@@ -6109,6 +6186,14 @@
|
|
|
6109
6186
|
"toggleFlagged"
|
|
6110
6187
|
],
|
|
6111
6188
|
"methodSignatures": [
|
|
6189
|
+
{
|
|
6190
|
+
"name": "abandonSendAttempt",
|
|
6191
|
+
"async": true,
|
|
6192
|
+
"params": [
|
|
6193
|
+
"reason: string"
|
|
6194
|
+
],
|
|
6195
|
+
"returns": "Promise<boolean>"
|
|
6196
|
+
},
|
|
6112
6197
|
{
|
|
6113
6198
|
"name": "createForward",
|
|
6114
6199
|
"returns": "Message"
|
|
@@ -6155,6 +6240,10 @@
|
|
|
6155
6240
|
"name": "getToAddresses",
|
|
6156
6241
|
"returns": "Array<object>"
|
|
6157
6242
|
},
|
|
6243
|
+
{
|
|
6244
|
+
"name": "isSendOutcomeUnknown",
|
|
6245
|
+
"returns": "boolean"
|
|
6246
|
+
},
|
|
6158
6247
|
{
|
|
6159
6248
|
"name": "isUnread",
|
|
6160
6249
|
"returns": "boolean"
|
|
@@ -6348,6 +6437,18 @@
|
|
|
6348
6437
|
"operation": "send",
|
|
6349
6438
|
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6350
6439
|
},
|
|
6440
|
+
{
|
|
6441
|
+
"kind": "cli",
|
|
6442
|
+
"name": "message_isSendOutcomeUnknown",
|
|
6443
|
+
"operation": "isSendOutcomeUnknown",
|
|
6444
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6445
|
+
},
|
|
6446
|
+
{
|
|
6447
|
+
"kind": "cli",
|
|
6448
|
+
"name": "message_abandonSendAttempt",
|
|
6449
|
+
"operation": "abandonSendAttempt",
|
|
6450
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6451
|
+
},
|
|
6351
6452
|
{
|
|
6352
6453
|
"kind": "cli",
|
|
6353
6454
|
"name": "message_retrySend",
|
|
@@ -6380,6 +6481,13 @@
|
|
|
6380
6481
|
}
|
|
6381
6482
|
],
|
|
6382
6483
|
"withheldSurfaces": [
|
|
6484
|
+
{
|
|
6485
|
+
"kind": "api",
|
|
6486
|
+
"operation": "abandonSendAttempt",
|
|
6487
|
+
"code": "not-included",
|
|
6488
|
+
"reason": "not listed in api.include",
|
|
6489
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6490
|
+
},
|
|
6383
6491
|
{
|
|
6384
6492
|
"kind": "api",
|
|
6385
6493
|
"operation": "createForward",
|
|
@@ -6443,6 +6551,13 @@
|
|
|
6443
6551
|
"reason": "not listed in api.include",
|
|
6444
6552
|
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6445
6553
|
},
|
|
6554
|
+
{
|
|
6555
|
+
"kind": "api",
|
|
6556
|
+
"operation": "isSendOutcomeUnknown",
|
|
6557
|
+
"code": "not-included",
|
|
6558
|
+
"reason": "not listed in api.include",
|
|
6559
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6560
|
+
},
|
|
6446
6561
|
{
|
|
6447
6562
|
"kind": "api",
|
|
6448
6563
|
"operation": "isUnread",
|
|
@@ -6682,6 +6797,311 @@
|
|
|
6682
6797
|
"tags": [],
|
|
6683
6798
|
"risks": []
|
|
6684
6799
|
},
|
|
6800
|
+
{
|
|
6801
|
+
"name": "NotificationDelivery",
|
|
6802
|
+
"qualifiedName": "@happyvertical/smrt-messages:NotificationDelivery",
|
|
6803
|
+
"collection": "notificationdeliveries",
|
|
6804
|
+
"description": "One durable, source-keyed notification delivery to one person over one external channel (the outbox row behind NotificationOutboxService).",
|
|
6805
|
+
"tableName": "notification_deliveries",
|
|
6806
|
+
"packageName": "@happyvertical/smrt-messages",
|
|
6807
|
+
"extends": "SmrtObject",
|
|
6808
|
+
"fields": [
|
|
6809
|
+
{
|
|
6810
|
+
"name": "tenantId",
|
|
6811
|
+
"type": "text",
|
|
6812
|
+
"required": true,
|
|
6813
|
+
"columnType": "UUID"
|
|
6814
|
+
},
|
|
6815
|
+
{
|
|
6816
|
+
"name": "recipientUserId",
|
|
6817
|
+
"type": "crossPackageRef",
|
|
6818
|
+
"required": true,
|
|
6819
|
+
"related": "@happyvertical/smrt-users:User",
|
|
6820
|
+
"columnType": "UUID"
|
|
6821
|
+
},
|
|
6822
|
+
{
|
|
6823
|
+
"name": "channel",
|
|
6824
|
+
"type": "text",
|
|
6825
|
+
"required": true,
|
|
6826
|
+
"columnType": "TEXT",
|
|
6827
|
+
"default": "email"
|
|
6828
|
+
},
|
|
6829
|
+
{
|
|
6830
|
+
"name": "sourceRef",
|
|
6831
|
+
"type": "text",
|
|
6832
|
+
"required": true,
|
|
6833
|
+
"columnType": "TEXT"
|
|
6834
|
+
},
|
|
6835
|
+
{
|
|
6836
|
+
"name": "kind",
|
|
6837
|
+
"type": "text",
|
|
6838
|
+
"required": true,
|
|
6839
|
+
"columnType": "TEXT"
|
|
6840
|
+
},
|
|
6841
|
+
{
|
|
6842
|
+
"name": "subject",
|
|
6843
|
+
"type": "text",
|
|
6844
|
+
"required": true,
|
|
6845
|
+
"columnType": "TEXT"
|
|
6846
|
+
},
|
|
6847
|
+
{
|
|
6848
|
+
"name": "textBody",
|
|
6849
|
+
"type": "text",
|
|
6850
|
+
"required": true,
|
|
6851
|
+
"columnType": "TEXT"
|
|
6852
|
+
},
|
|
6853
|
+
{
|
|
6854
|
+
"name": "href",
|
|
6855
|
+
"type": "text",
|
|
6856
|
+
"required": true,
|
|
6857
|
+
"columnType": "TEXT"
|
|
6858
|
+
},
|
|
6859
|
+
{
|
|
6860
|
+
"name": "recipientAddress",
|
|
6861
|
+
"type": "text",
|
|
6862
|
+
"required": true,
|
|
6863
|
+
"columnType": "TEXT"
|
|
6864
|
+
},
|
|
6865
|
+
{
|
|
6866
|
+
"name": "recipientName",
|
|
6867
|
+
"type": "text",
|
|
6868
|
+
"required": false,
|
|
6869
|
+
"columnType": "TEXT"
|
|
6870
|
+
},
|
|
6871
|
+
{
|
|
6872
|
+
"name": "accountId",
|
|
6873
|
+
"type": "foreignKey",
|
|
6874
|
+
"required": false,
|
|
6875
|
+
"related": "Account",
|
|
6876
|
+
"columnType": "UUID"
|
|
6877
|
+
},
|
|
6878
|
+
{
|
|
6879
|
+
"name": "messageId",
|
|
6880
|
+
"type": "foreignKey",
|
|
6881
|
+
"required": false,
|
|
6882
|
+
"related": "Message",
|
|
6883
|
+
"columnType": "UUID"
|
|
6884
|
+
},
|
|
6885
|
+
{
|
|
6886
|
+
"name": "status",
|
|
6887
|
+
"type": "text",
|
|
6888
|
+
"required": false,
|
|
6889
|
+
"columnType": "TEXT",
|
|
6890
|
+
"default": "pending"
|
|
6891
|
+
},
|
|
6892
|
+
{
|
|
6893
|
+
"name": "stateVersion",
|
|
6894
|
+
"type": "integer",
|
|
6895
|
+
"required": true,
|
|
6896
|
+
"columnType": "INTEGER",
|
|
6897
|
+
"default": 0
|
|
6898
|
+
},
|
|
6899
|
+
{
|
|
6900
|
+
"name": "transitionId",
|
|
6901
|
+
"type": "text",
|
|
6902
|
+
"required": false,
|
|
6903
|
+
"columnType": "TEXT"
|
|
6904
|
+
},
|
|
6905
|
+
{
|
|
6906
|
+
"name": "attempts",
|
|
6907
|
+
"type": "integer",
|
|
6908
|
+
"required": true,
|
|
6909
|
+
"columnType": "INTEGER",
|
|
6910
|
+
"default": 0
|
|
6911
|
+
},
|
|
6912
|
+
{
|
|
6913
|
+
"name": "maxAttempts",
|
|
6914
|
+
"type": "integer",
|
|
6915
|
+
"required": true,
|
|
6916
|
+
"columnType": "INTEGER",
|
|
6917
|
+
"default": 5
|
|
6918
|
+
},
|
|
6919
|
+
{
|
|
6920
|
+
"name": "nextAttemptAt",
|
|
6921
|
+
"type": "text",
|
|
6922
|
+
"required": false,
|
|
6923
|
+
"columnType": "TEXT"
|
|
6924
|
+
},
|
|
6925
|
+
{
|
|
6926
|
+
"name": "leaseToken",
|
|
6927
|
+
"type": "text",
|
|
6928
|
+
"required": false,
|
|
6929
|
+
"columnType": "TEXT"
|
|
6930
|
+
},
|
|
6931
|
+
{
|
|
6932
|
+
"name": "leaseExpiresAt",
|
|
6933
|
+
"type": "datetime",
|
|
6934
|
+
"required": false,
|
|
6935
|
+
"columnType": "TIMESTAMP"
|
|
6936
|
+
},
|
|
6937
|
+
{
|
|
6938
|
+
"name": "handedOffAt",
|
|
6939
|
+
"type": "datetime",
|
|
6940
|
+
"required": false,
|
|
6941
|
+
"columnType": "TIMESTAMP"
|
|
6942
|
+
},
|
|
6943
|
+
{
|
|
6944
|
+
"name": "deliveredAt",
|
|
6945
|
+
"type": "datetime",
|
|
6946
|
+
"required": false,
|
|
6947
|
+
"columnType": "TIMESTAMP"
|
|
6948
|
+
},
|
|
6949
|
+
{
|
|
6950
|
+
"name": "providerMessageId",
|
|
6951
|
+
"type": "text",
|
|
6952
|
+
"required": false,
|
|
6953
|
+
"columnType": "TEXT"
|
|
6954
|
+
},
|
|
6955
|
+
{
|
|
6956
|
+
"name": "lastError",
|
|
6957
|
+
"type": "text",
|
|
6958
|
+
"required": false,
|
|
6959
|
+
"columnType": "TEXT"
|
|
6960
|
+
}
|
|
6961
|
+
],
|
|
6962
|
+
"relationships": [
|
|
6963
|
+
{
|
|
6964
|
+
"name": "recipientUserId",
|
|
6965
|
+
"type": "crossPackageRef",
|
|
6966
|
+
"required": true,
|
|
6967
|
+
"related": "@happyvertical/smrt-users:User",
|
|
6968
|
+
"columnType": "UUID"
|
|
6969
|
+
},
|
|
6970
|
+
{
|
|
6971
|
+
"name": "accountId",
|
|
6972
|
+
"type": "foreignKey",
|
|
6973
|
+
"required": false,
|
|
6974
|
+
"related": "Account",
|
|
6975
|
+
"columnType": "UUID"
|
|
6976
|
+
},
|
|
6977
|
+
{
|
|
6978
|
+
"name": "messageId",
|
|
6979
|
+
"type": "foreignKey",
|
|
6980
|
+
"required": false,
|
|
6981
|
+
"related": "Message",
|
|
6982
|
+
"columnType": "UUID"
|
|
6983
|
+
}
|
|
6984
|
+
],
|
|
6985
|
+
"methods": [
|
|
6986
|
+
"transition"
|
|
6987
|
+
],
|
|
6988
|
+
"methodSignatures": [
|
|
6989
|
+
{
|
|
6990
|
+
"name": "transition",
|
|
6991
|
+
"async": true,
|
|
6992
|
+
"params": [
|
|
6993
|
+
"changes: NotificationDeliveryTransition",
|
|
6994
|
+
"expected?: object"
|
|
6995
|
+
],
|
|
6996
|
+
"returns": "Promise<boolean>"
|
|
6997
|
+
}
|
|
6998
|
+
],
|
|
6999
|
+
"tenant": {
|
|
7000
|
+
"scoped": true,
|
|
7001
|
+
"mode": "required",
|
|
7002
|
+
"field": "tenantId"
|
|
7003
|
+
},
|
|
7004
|
+
"conflictColumns": [
|
|
7005
|
+
"tenant_id",
|
|
7006
|
+
"recipient_user_id",
|
|
7007
|
+
"channel",
|
|
7008
|
+
"source_ref"
|
|
7009
|
+
],
|
|
7010
|
+
"surfaces": [],
|
|
7011
|
+
"withheldSurfaces": [
|
|
7012
|
+
{
|
|
7013
|
+
"kind": "api",
|
|
7014
|
+
"operation": "transition",
|
|
7015
|
+
"code": "api-disabled",
|
|
7016
|
+
"reason": "api is disabled",
|
|
7017
|
+
"objectName": "@happyvertical/smrt-messages:NotificationDelivery"
|
|
7018
|
+
}
|
|
7019
|
+
],
|
|
7020
|
+
"relationshipFeatures": [
|
|
7021
|
+
"crossPackageRef",
|
|
7022
|
+
"foreignKey",
|
|
7023
|
+
"uuidColumns"
|
|
7024
|
+
],
|
|
7025
|
+
"tags": [],
|
|
7026
|
+
"risks": []
|
|
7027
|
+
},
|
|
7028
|
+
{
|
|
7029
|
+
"name": "NotificationOptOut",
|
|
7030
|
+
"qualifiedName": "@happyvertical/smrt-messages:NotificationOptOut",
|
|
7031
|
+
"collection": "notificationoptouts",
|
|
7032
|
+
"description": "One person's opt-out of one notification channel for one scope, in one tenant: the preference store behind the outbox's isSuppressed hook.",
|
|
7033
|
+
"tableName": "notification_opt_outs",
|
|
7034
|
+
"packageName": "@happyvertical/smrt-messages",
|
|
7035
|
+
"extends": "SmrtObject",
|
|
7036
|
+
"fields": [
|
|
7037
|
+
{
|
|
7038
|
+
"name": "tenantId",
|
|
7039
|
+
"type": "text",
|
|
7040
|
+
"required": true,
|
|
7041
|
+
"columnType": "UUID"
|
|
7042
|
+
},
|
|
7043
|
+
{
|
|
7044
|
+
"name": "recipientUserId",
|
|
7045
|
+
"type": "crossPackageRef",
|
|
7046
|
+
"required": true,
|
|
7047
|
+
"related": "@happyvertical/smrt-users:User",
|
|
7048
|
+
"columnType": "UUID"
|
|
7049
|
+
},
|
|
7050
|
+
{
|
|
7051
|
+
"name": "channel",
|
|
7052
|
+
"type": "text",
|
|
7053
|
+
"required": true,
|
|
7054
|
+
"columnType": "TEXT",
|
|
7055
|
+
"default": "email"
|
|
7056
|
+
},
|
|
7057
|
+
{
|
|
7058
|
+
"name": "scope",
|
|
7059
|
+
"type": "text",
|
|
7060
|
+
"required": true,
|
|
7061
|
+
"columnType": "TEXT"
|
|
7062
|
+
},
|
|
7063
|
+
{
|
|
7064
|
+
"name": "optedOutAt",
|
|
7065
|
+
"type": "datetime",
|
|
7066
|
+
"required": false,
|
|
7067
|
+
"columnType": "TIMESTAMP"
|
|
7068
|
+
},
|
|
7069
|
+
{
|
|
7070
|
+
"name": "updatedBy",
|
|
7071
|
+
"type": "text",
|
|
7072
|
+
"required": false,
|
|
7073
|
+
"columnType": "TEXT"
|
|
7074
|
+
}
|
|
7075
|
+
],
|
|
7076
|
+
"relationships": [
|
|
7077
|
+
{
|
|
7078
|
+
"name": "recipientUserId",
|
|
7079
|
+
"type": "crossPackageRef",
|
|
7080
|
+
"required": true,
|
|
7081
|
+
"related": "@happyvertical/smrt-users:User",
|
|
7082
|
+
"columnType": "UUID"
|
|
7083
|
+
}
|
|
7084
|
+
],
|
|
7085
|
+
"methods": [],
|
|
7086
|
+
"tenant": {
|
|
7087
|
+
"scoped": true,
|
|
7088
|
+
"mode": "required",
|
|
7089
|
+
"field": "tenantId"
|
|
7090
|
+
},
|
|
7091
|
+
"conflictColumns": [
|
|
7092
|
+
"tenant_id",
|
|
7093
|
+
"recipient_user_id",
|
|
7094
|
+
"channel",
|
|
7095
|
+
"scope"
|
|
7096
|
+
],
|
|
7097
|
+
"surfaces": [],
|
|
7098
|
+
"relationshipFeatures": [
|
|
7099
|
+
"crossPackageRef",
|
|
7100
|
+
"uuidColumns"
|
|
7101
|
+
],
|
|
7102
|
+
"tags": [],
|
|
7103
|
+
"risks": []
|
|
7104
|
+
},
|
|
6685
7105
|
{
|
|
6686
7106
|
"name": "PersonaMessageRoute",
|
|
6687
7107
|
"qualifiedName": "@happyvertical/smrt-messages:PersonaMessageRoute",
|
|
@@ -7422,6 +7842,7 @@
|
|
|
7422
7842
|
}
|
|
7423
7843
|
],
|
|
7424
7844
|
"methods": [
|
|
7845
|
+
"abandonSendAttempt",
|
|
7425
7846
|
"createForward",
|
|
7426
7847
|
"createReply",
|
|
7427
7848
|
"getAccount",
|
|
@@ -7435,6 +7856,7 @@
|
|
|
7435
7856
|
"getToAddresses",
|
|
7436
7857
|
"getTotalReactions",
|
|
7437
7858
|
"isInThread",
|
|
7859
|
+
"isSendOutcomeUnknown",
|
|
7438
7860
|
"isUnread",
|
|
7439
7861
|
"markRead",
|
|
7440
7862
|
"markUnread",
|
|
@@ -7445,6 +7867,14 @@
|
|
|
7445
7867
|
"toggleFlagged"
|
|
7446
7868
|
],
|
|
7447
7869
|
"methodSignatures": [
|
|
7870
|
+
{
|
|
7871
|
+
"name": "abandonSendAttempt",
|
|
7872
|
+
"async": true,
|
|
7873
|
+
"params": [
|
|
7874
|
+
"reason: string"
|
|
7875
|
+
],
|
|
7876
|
+
"returns": "Promise<boolean>"
|
|
7877
|
+
},
|
|
7448
7878
|
{
|
|
7449
7879
|
"name": "createForward",
|
|
7450
7880
|
"returns": "Message"
|
|
@@ -7507,6 +7937,10 @@
|
|
|
7507
7937
|
"name": "isInThread",
|
|
7508
7938
|
"returns": "boolean"
|
|
7509
7939
|
},
|
|
7940
|
+
{
|
|
7941
|
+
"name": "isSendOutcomeUnknown",
|
|
7942
|
+
"returns": "boolean"
|
|
7943
|
+
},
|
|
7510
7944
|
{
|
|
7511
7945
|
"name": "isUnread",
|
|
7512
7946
|
"returns": "boolean"
|
|
@@ -7697,6 +8131,18 @@
|
|
|
7697
8131
|
"operation": "send",
|
|
7698
8132
|
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
7699
8133
|
},
|
|
8134
|
+
{
|
|
8135
|
+
"kind": "cli",
|
|
8136
|
+
"name": "slackmessage_isSendOutcomeUnknown",
|
|
8137
|
+
"operation": "isSendOutcomeUnknown",
|
|
8138
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8139
|
+
},
|
|
8140
|
+
{
|
|
8141
|
+
"kind": "cli",
|
|
8142
|
+
"name": "slackmessage_abandonSendAttempt",
|
|
8143
|
+
"operation": "abandonSendAttempt",
|
|
8144
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8145
|
+
},
|
|
7700
8146
|
{
|
|
7701
8147
|
"kind": "cli",
|
|
7702
8148
|
"name": "slackmessage_retrySend",
|
|
@@ -7753,6 +8199,13 @@
|
|
|
7753
8199
|
}
|
|
7754
8200
|
],
|
|
7755
8201
|
"withheldSurfaces": [
|
|
8202
|
+
{
|
|
8203
|
+
"kind": "api",
|
|
8204
|
+
"operation": "abandonSendAttempt",
|
|
8205
|
+
"code": "not-included",
|
|
8206
|
+
"reason": "not listed in api.include",
|
|
8207
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8208
|
+
},
|
|
7756
8209
|
{
|
|
7757
8210
|
"kind": "api",
|
|
7758
8211
|
"operation": "createForward",
|
|
@@ -7844,6 +8297,13 @@
|
|
|
7844
8297
|
"reason": "not listed in api.include",
|
|
7845
8298
|
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
7846
8299
|
},
|
|
8300
|
+
{
|
|
8301
|
+
"kind": "api",
|
|
8302
|
+
"operation": "isSendOutcomeUnknown",
|
|
8303
|
+
"code": "not-included",
|
|
8304
|
+
"reason": "not listed in api.include",
|
|
8305
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8306
|
+
},
|
|
7847
8307
|
{
|
|
7848
8308
|
"kind": "api",
|
|
7849
8309
|
"operation": "isUnread",
|
|
@@ -8476,6 +8936,7 @@
|
|
|
8476
8936
|
}
|
|
8477
8937
|
],
|
|
8478
8938
|
"methods": [
|
|
8939
|
+
"abandonSendAttempt",
|
|
8479
8940
|
"createForward",
|
|
8480
8941
|
"createReply",
|
|
8481
8942
|
"getAccount",
|
|
@@ -8489,6 +8950,7 @@
|
|
|
8489
8950
|
"getPreview",
|
|
8490
8951
|
"getThreadMessages",
|
|
8491
8952
|
"getToAddresses",
|
|
8953
|
+
"isSendOutcomeUnknown",
|
|
8492
8954
|
"isUnread",
|
|
8493
8955
|
"markRead",
|
|
8494
8956
|
"markUnread",
|
|
@@ -8499,6 +8961,14 @@
|
|
|
8499
8961
|
"toggleFlagged"
|
|
8500
8962
|
],
|
|
8501
8963
|
"methodSignatures": [
|
|
8964
|
+
{
|
|
8965
|
+
"name": "abandonSendAttempt",
|
|
8966
|
+
"async": true,
|
|
8967
|
+
"params": [
|
|
8968
|
+
"reason: string"
|
|
8969
|
+
],
|
|
8970
|
+
"returns": "Promise<boolean>"
|
|
8971
|
+
},
|
|
8502
8972
|
{
|
|
8503
8973
|
"name": "createForward",
|
|
8504
8974
|
"returns": "Message"
|
|
@@ -8561,6 +9031,10 @@
|
|
|
8561
9031
|
"name": "getToAddresses",
|
|
8562
9032
|
"returns": "Array<object>"
|
|
8563
9033
|
},
|
|
9034
|
+
{
|
|
9035
|
+
"name": "isSendOutcomeUnknown",
|
|
9036
|
+
"returns": "boolean"
|
|
9037
|
+
},
|
|
8564
9038
|
{
|
|
8565
9039
|
"name": "isUnread",
|
|
8566
9040
|
"returns": "boolean"
|
|
@@ -8751,6 +9225,18 @@
|
|
|
8751
9225
|
"operation": "send",
|
|
8752
9226
|
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
8753
9227
|
},
|
|
9228
|
+
{
|
|
9229
|
+
"kind": "cli",
|
|
9230
|
+
"name": "tweet_isSendOutcomeUnknown",
|
|
9231
|
+
"operation": "isSendOutcomeUnknown",
|
|
9232
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9233
|
+
},
|
|
9234
|
+
{
|
|
9235
|
+
"kind": "cli",
|
|
9236
|
+
"name": "tweet_abandonSendAttempt",
|
|
9237
|
+
"operation": "abandonSendAttempt",
|
|
9238
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9239
|
+
},
|
|
8754
9240
|
{
|
|
8755
9241
|
"kind": "cli",
|
|
8756
9242
|
"name": "tweet_retrySend",
|
|
@@ -8807,6 +9293,13 @@
|
|
|
8807
9293
|
}
|
|
8808
9294
|
],
|
|
8809
9295
|
"withheldSurfaces": [
|
|
9296
|
+
{
|
|
9297
|
+
"kind": "api",
|
|
9298
|
+
"operation": "abandonSendAttempt",
|
|
9299
|
+
"code": "not-included",
|
|
9300
|
+
"reason": "not listed in api.include",
|
|
9301
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9302
|
+
},
|
|
8810
9303
|
{
|
|
8811
9304
|
"kind": "api",
|
|
8812
9305
|
"operation": "createForward",
|
|
@@ -8898,6 +9391,13 @@
|
|
|
8898
9391
|
"reason": "not listed in api.include",
|
|
8899
9392
|
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
8900
9393
|
},
|
|
9394
|
+
{
|
|
9395
|
+
"kind": "api",
|
|
9396
|
+
"operation": "isSendOutcomeUnknown",
|
|
9397
|
+
"code": "not-included",
|
|
9398
|
+
"reason": "not listed in api.include",
|
|
9399
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9400
|
+
},
|
|
8901
9401
|
{
|
|
8902
9402
|
"kind": "api",
|
|
8903
9403
|
"operation": "isUnread",
|
|
@@ -10871,6 +11371,18 @@
|
|
|
10871
11371
|
"operation": "send",
|
|
10872
11372
|
"objectName": "@happyvertical/smrt-messages:Email"
|
|
10873
11373
|
},
|
|
11374
|
+
{
|
|
11375
|
+
"kind": "cli",
|
|
11376
|
+
"name": "email_isSendOutcomeUnknown",
|
|
11377
|
+
"operation": "isSendOutcomeUnknown",
|
|
11378
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
11379
|
+
},
|
|
11380
|
+
{
|
|
11381
|
+
"kind": "cli",
|
|
11382
|
+
"name": "email_abandonSendAttempt",
|
|
11383
|
+
"operation": "abandonSendAttempt",
|
|
11384
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
11385
|
+
},
|
|
10874
11386
|
{
|
|
10875
11387
|
"kind": "cli",
|
|
10876
11388
|
"name": "email_retrySend",
|
|
@@ -11419,6 +11931,18 @@
|
|
|
11419
11931
|
"operation": "send",
|
|
11420
11932
|
"objectName": "@happyvertical/smrt-messages:Message"
|
|
11421
11933
|
},
|
|
11934
|
+
{
|
|
11935
|
+
"kind": "cli",
|
|
11936
|
+
"name": "message_isSendOutcomeUnknown",
|
|
11937
|
+
"operation": "isSendOutcomeUnknown",
|
|
11938
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
11939
|
+
},
|
|
11940
|
+
{
|
|
11941
|
+
"kind": "cli",
|
|
11942
|
+
"name": "message_abandonSendAttempt",
|
|
11943
|
+
"operation": "abandonSendAttempt",
|
|
11944
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
11945
|
+
},
|
|
11422
11946
|
{
|
|
11423
11947
|
"kind": "cli",
|
|
11424
11948
|
"name": "message_retrySend",
|
|
@@ -11707,6 +12231,18 @@
|
|
|
11707
12231
|
"operation": "send",
|
|
11708
12232
|
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
11709
12233
|
},
|
|
12234
|
+
{
|
|
12235
|
+
"kind": "cli",
|
|
12236
|
+
"name": "slackmessage_isSendOutcomeUnknown",
|
|
12237
|
+
"operation": "isSendOutcomeUnknown",
|
|
12238
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
12239
|
+
},
|
|
12240
|
+
{
|
|
12241
|
+
"kind": "cli",
|
|
12242
|
+
"name": "slackmessage_abandonSendAttempt",
|
|
12243
|
+
"operation": "abandonSendAttempt",
|
|
12244
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
12245
|
+
},
|
|
11710
12246
|
{
|
|
11711
12247
|
"kind": "cli",
|
|
11712
12248
|
"name": "slackmessage_retrySend",
|
|
@@ -11939,6 +12475,18 @@
|
|
|
11939
12475
|
"operation": "send",
|
|
11940
12476
|
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
11941
12477
|
},
|
|
12478
|
+
{
|
|
12479
|
+
"kind": "cli",
|
|
12480
|
+
"name": "tweet_isSendOutcomeUnknown",
|
|
12481
|
+
"operation": "isSendOutcomeUnknown",
|
|
12482
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
12483
|
+
},
|
|
12484
|
+
{
|
|
12485
|
+
"kind": "cli",
|
|
12486
|
+
"name": "tweet_abandonSendAttempt",
|
|
12487
|
+
"operation": "abandonSendAttempt",
|
|
12488
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
12489
|
+
},
|
|
11942
12490
|
{
|
|
11943
12491
|
"kind": "cli",
|
|
11944
12492
|
"name": "tweet_retrySend",
|
|
@@ -12188,14 +12736,21 @@
|
|
|
12188
12736
|
],
|
|
12189
12737
|
"prompts": [],
|
|
12190
12738
|
"relationshipsV2": {
|
|
12191
|
-
"foreignKeyFields":
|
|
12192
|
-
"crossPackageRefFields":
|
|
12739
|
+
"foreignKeyFields": 18,
|
|
12740
|
+
"crossPackageRefFields": 9,
|
|
12193
12741
|
"junctionCollections": 0,
|
|
12194
12742
|
"hierarchicalObjects": 0,
|
|
12195
12743
|
"polymorphicAssociations": 0,
|
|
12196
|
-
"uuidColumns":
|
|
12744
|
+
"uuidColumns": 112
|
|
12197
12745
|
},
|
|
12198
|
-
"agentDoc": "# @happyvertical/smrt-messages\n\nMulti-channel messaging with STI hierarchies for both messages and accounts. Credential encryption via smrt-secrets.\n\n## STI Hierarchies\n\n**Messages** (share `messages` table):\n- `Message` (base): accountId, personaId, endpointId, correlationId, threadId, subject, body, fromAddress, toAddresses (JSON), sendStatus (draft/sending/sent/failed), retryCount\n- `Email`: messageId (RFC 822), inReplyTo, ccAddresses, bccAddresses, htmlBody, textBody, folderId, labels, headers\n- `Tweet`: tweetId, retweetCount, likeCount, mediaUrls, hashtags, mentions\n- `SlackMessage`: channelId, slackTs, slackThreadTs, reactions, blocks\n\n**Accounts** (share `accounts` table):\n- `Account` (base): channelType, providerType, public configuration,\n credentialSecretId, isActive, lastSyncAt, deprecated sensitive settings\n- `EmailAccount`, `TwitterAccount`, `SlackAccount`, `ZulipAccount`,\n `TelegramAccount`: provider-specific identity/sender behavior\n\n## Persona messaging\n\n- `MessagingEndpoint`: tenant-owned, write-only destination JSON with masked\n display helper.\n- `PersonaMessageRoute`: persona → account + endpoint, selected by purpose and\n descending priority.\n- `PersonaMessagingService`: persists and sends through an enabled route.\n- Provider registry: public setup descriptors + optional sender factory. Keep\n channel (`sms`) separate from provider (`twilio`).\n- Permissions: `messages.send`, `messages.manage-routes`,\n `messages.manage-credentials`.\n- `MessagingSettingsService` is the permissioned server boundary;\n its host-supplied `resolvePersonaTenantId` proves cross-package ownership.\n `MessagingSettingsPanel` is the callback-driven Svelte surface.\n- Account, endpoint, and route generated surfaces are read-only. Do not add\n generated mutations around the settings service or make secret fields writable.\n- **Recipe `messages.slack`** (`src/recipes.ts`, help in `src/help/slack.md`): lists Slack messages and workspaces beside the Mailbox and Notifications recipes; workspaces stay read-only and sending goes through the senders, not generated routes. Fields it shows carry `@field({ description })` in plain words; keep them when editing the models.\n\n## In-app notifications (`UserNotification`)\n\nThe in-app channel: one stored row per person, with read state.\n`user_notifications`: `tenantId` (required), `recipientUserId`\n(`@crossPackageRef` smrt-users:User, uuid), `kind`, `title`, `body`, `href`,\n`severity` (info/success/warning/error), `sourceRef`, `occurredAt`, `readAt`,\n`dismissedAt`. Conflict identity `(tenant_id, recipient_user_id, source_ref)`.\n\n- `UserNotificationService` is the only access path: `notify()` /\n `notifyMany()` (dedupe on `sourceRef`, existing row returned untouched;\n a unique ref is generated when omitted), `listForUser()` / `countUnread()`\n across explicit `tenantIds` (each read under `withTenant`), `markRead()`,\n `dismiss()` (also marks read), `markAllRead()`. Every write filters on the\n recipient, so another user's id matches nothing.\n- Generated REST/MCP/CLI are off (`api`/`mcp`/`cli: false`): a tenant-scoped\n list would expose every member's rows. Hosts expose their own routes that\n pass the signed-in user's id, never a request-chosen one.\n- Hosts decide who is told (e.g. `SocialPost.createdByUserId` for a failed\n post). New table: consumers run `db:migrate`.\n\n## Credential Security\n\nAccount credentials stored via tenant-bound `credentialSecretId` →\nsmrt-secrets. Use `setCredentials()`/`getCredentials()` — never store passwords\ndirectly. Credential descriptor schemas contain field definitions, never values.\n\n## Send Lifecycle\n\n`message.send()`: resolves account → creates provider sender → updates sendStatus. Retry support with `maxRetries` budget.\n`EmailSender` attaches the message's stored `Attachment` rows (read through\n`Attachment.readContent()`, under the message's tenant context) plus\n`SendEmailOptions.attachments` bytes; an unreadable or file-less row fails the\nsend with the error on `sendError`, never a silent drop (#3808).\n`PersonaMessagingService.send({ attachments })` accepts them on email routes only.\n\n## Provider entry points (bundle boundary, #1979)\n\nThe package root is provider-neutral: models, collections, services,\npermissions, and the provider registry carry **zero provider-SDK weight**.\nProvider SDK wrappers (`@happyvertical/email`, `@happyvertical/messages`) are\nreachable only through explicit entry points, imported once at server startup:\n\n```ts\nimport '@happyvertical/smrt-messages/providers/email'; // smtp/imap/pop3/gmail\nimport '@happyvertical/smrt-messages/providers/slack';\nimport '@happyvertical/smrt-messages/providers/twitter';\nimport '@happyvertical/smrt-messages/providers/all'; // all of the above\n```\n\n- Without the matching entry, `EmailAccount.createClient()` (send + mailbox\n sync) throws an actionable error and `SlackSender`/`TweetSender` return a\n failure result naming the entry to import. Telegram and Zulip are\n fetch-based and always work from the root.\n- Builtin registry definitions stay metadata-only (setup fields for UIs); the\n entries upgrade them with `createEmailClient`/`createMessageClient`/\n `createSender` hooks. Never import a provider SDK from any module reachable\n from `src/index.ts` — the consumer bundle gate (`packages/bundle-gate`)\n fails CI when one becomes reachable.\n- `Attachment.readContent()` loads `@happyvertical/files` through core's\n `importOptionalDependency` boundary and keeps its null-on-failure contract\n in builds where the SDK is absent.\n\n## Gotchas\n\n- **Manifest before decorators**: every decorated model imports\n `../__smrt-register__.js`, and `package.json#sideEffects` lists\n `./src/__smrt-register__.ts`. Without both, the library build dropped or\n reordered the self-registration, so dist classes registered with only their\n decorated fields and could adopt smrt-ledgers' same-named `Account` (#3098).\n- **`Account` is not unique**: smrt-ledgers also declares `Account` (on\n `ledger_accounts`). Resolve by constructor or qualified name; the\n coexistence tests (`ledger-account-identity-*`, and the PostgreSQL lane\n `pnpm test:postgres`) load both packages in both orders.\n- **STI `_meta_type`**: qualified format `@happyvertical/smrt-messages:Email`\n- **JSON fields**: all address/metadata fields are JSON strings with `getX()`/`setX()` helpers\n- **RFC 822 threading**: Email uses `inReplyTo`/`messageId`/`references` — manual management required\n- **Attachment.messageId**: use this field (not `emailId`); old getter/setter mapped via deprecation wrapper\n- **Optional tenancy**: `@TenantScoped({ mode: 'optional' })` on Message, Account, Attachment\n- **Required tenancy**: endpoints and persona routes are always tenant scoped\n- **Secrets are write-only**: never serialize credentials or endpoint.address to an app\n\n## Intake byte snapshots\n\n`EmailAccount.readIntakeMessage(providerLookup, maxBytes, imapIdentity?)` is the authorized host seam for\nopt-in ingestion. It copies full-message attachment bytes from the registered\nemail provider and fails when only attachment metadata is available; it never\nreads provider-supplied paths or advances sync checkpoints. Keep this path\nprovider-neutral and exclude arbitrary headers/credentials from its snapshot.\nIMAP lookup must include folder/UIDVALIDITY/UID verification; sender Message-ID\nis a lookup hint, never the durable delivery identity.\n",
|
|
12746
|
+
"agentDoc": "# @happyvertical/smrt-messages\n\nMulti-channel messaging with STI hierarchies for both messages and accounts. Credential encryption via smrt-secrets.\n\n## STI Hierarchies\n\n**Messages** (share `messages` table):\n- `Message` (base): accountId, personaId, endpointId, correlationId, threadId, subject, body, fromAddress, toAddresses (JSON), sendStatus (draft/sending/sent/failed), retryCount\n- `Email`: messageId (RFC 822), inReplyTo, ccAddresses, bccAddresses, htmlBody, textBody, folderId, labels, headers\n- `Tweet`: tweetId, retweetCount, likeCount, mediaUrls, hashtags, mentions\n- `SlackMessage`: channelId, slackTs, slackThreadTs, reactions, blocks\n\n**Accounts** (share `accounts` table):\n- `Account` (base): channelType, providerType, public configuration,\n credentialSecretId, isActive, lastSyncAt, deprecated sensitive settings\n- `EmailAccount`, `TwitterAccount`, `SlackAccount`, `ZulipAccount`,\n `TelegramAccount`: provider-specific identity/sender behavior\n\n## Persona messaging\n\n- `MessagingEndpoint`: tenant-owned, write-only destination JSON with masked\n display helper.\n- `PersonaMessageRoute`: persona → account + endpoint, selected by purpose and\n descending priority.\n- `PersonaMessagingService`: persists and sends through an enabled route.\n- Provider registry: public setup descriptors + optional sender factory. Keep\n channel (`sms`) separate from provider (`twilio`).\n- Permissions: `messages.send`, `messages.manage-routes`,\n `messages.manage-credentials`.\n- `MessagingSettingsService` is the permissioned server boundary;\n its host-supplied `resolvePersonaTenantId` proves cross-package ownership.\n `MessagingSettingsPanel` is the callback-driven Svelte surface.\n- Account, endpoint, and route generated surfaces are read-only. Do not add\n generated mutations around the settings service or make secret fields writable.\n- **Recipe `messages.slack`** (`src/recipes.ts`, help in `src/help/slack.md`): lists Slack messages and workspaces beside the Mailbox and Notifications recipes; workspaces stay read-only and sending goes through the senders, not generated routes. Fields it shows carry `@field({ description })` in plain words; keep them when editing the models.\n\n## In-app notifications (`UserNotification`)\n\nThe in-app channel: one stored row per person, with read state.\n`user_notifications`: `tenantId` (required), `recipientUserId`\n(`@crossPackageRef` smrt-users:User, uuid), `kind`, `title`, `body`, `href`,\n`severity` (info/success/warning/error), `sourceRef`, `occurredAt`, `readAt`,\n`dismissedAt`. Conflict identity `(tenant_id, recipient_user_id, source_ref)`.\n\n- `UserNotificationService` is the only access path: `notify()` /\n `notifyMany()` (dedupe on `sourceRef`, existing row returned untouched;\n a unique ref is generated when omitted), `listForUser()` / `countUnread()`\n across explicit `tenantIds` (each read under `withTenant`), `markRead()`,\n `dismiss()` (also marks read), `markAllRead()`. Every write filters on the\n recipient, so another user's id matches nothing.\n- Generated REST/MCP/CLI are off (`api`/`mcp`/`cli: false`): a tenant-scoped\n list would expose every member's rows. Hosts expose their own routes that\n pass the signed-in user's id, never a request-chosen one.\n- Hosts decide who is told (e.g. `SocialPost.createdByUserId` for a failed\n post). New table: consumers run `db:migrate`.\n\n## Notification email outbox (`NotificationDelivery`)\n\n`NotificationOutboxService` is the external-channel counterpart of\n`UserNotificationService`: `enqueueEmail()` stores one delivery per\n`(tenant, recipient, channel, sourceRef)` after the host resolves the\nrecipient address and same-tenant sender account (callers never choose an\naddress); `dispatchDue()` leases due rows by compare-and-set\n(`NotificationDelivery.transition()`), creates the single outbound `Email`\nunder the lease, and settles sent/retry/failed/uncertain. Never resend an\n`uncertain` row automatically. Contract, retry, and reconciliation policy:\n[agents/notification-outbox.md](agents/notification-outbox.md). New table:\nconsumers run `db:migrate`.\n\n`NotificationOptOut` (`notification_opt_outs`, tenant-required, generated\nsurfaces off) is the per-recipient opt-out ledger for the outbox's\n`isSuppressed` hook, keyed `(tenant, recipient, channel, scope)` with scope `*`\nor `project:<uuid>`-style keys. `NotificationOptOutService`: idempotent\n`optOut`/`optIn` (with a `principal` only `userId === recipientUserId` may\nchange a row, and only in the principal's own `tenantId`, else\n`NotificationOptOutDeniedError`; no principal = trusted host code), trusted `isOptedOut` (a `*` row matches every scope), `listForRecipient`\n(principal required). `optOutSuppression(service, scopeOf)` builds the\n`isSuppressed` function. Details in the outbox doc; consumers run `db:migrate`.\n\n## Credential Security\n\nAccount credentials stored via tenant-bound `credentialSecretId` →\nsmrt-secrets. Use `setCredentials()`/`getCredentials()` — never store passwords\ndirectly. Credential descriptor schemas contain field definitions, never values.\n\n## Send Lifecycle\n\n`message.send()`: resolves account → creates provider sender → updates sendStatus. Retry support with `maxRetries` budget.\nA failure the provider may still have accepted carries\n`MessageSendResult.outcomeUnknown` (set by `EmailSender` unless\n`isDefiniteSendRejection()` proves a refusal, and when a sender throws) and is\npersisted as `metadata.sendOutcomeUnknown`; never auto-resend it.\n`EmailSender` attaches the message's stored `Attachment` rows (read through\n`Attachment.readContent()`, under the message's tenant context) plus\n`SendEmailOptions.attachments` bytes; an unreadable or file-less row fails the\nsend with the error on `sendError`, never a silent drop (#3808).\n`PersonaMessagingService.send({ attachments })` accepts them on email routes only.\n\n## Provider entry points (bundle boundary, #1979)\n\nThe package root is provider-neutral: models, collections, services,\npermissions, and the provider registry carry **zero provider-SDK weight**.\nProvider SDK wrappers (`@happyvertical/email`, `@happyvertical/messages`) are\nreachable only through explicit entry points, imported once at server startup:\n\n```ts\nimport '@happyvertical/smrt-messages/providers/email'; // smtp/imap/pop3/gmail\nimport '@happyvertical/smrt-messages/providers/slack';\nimport '@happyvertical/smrt-messages/providers/twitter';\nimport '@happyvertical/smrt-messages/providers/all'; // all of the above\n```\n\n- Without the matching entry, `EmailAccount.createClient()` (send + mailbox\n sync) throws an actionable error and `SlackSender`/`TweetSender` return a\n failure result naming the entry to import. Telegram and Zulip are\n fetch-based and always work from the root.\n- Builtin registry definitions stay metadata-only (setup fields for UIs); the\n entries upgrade them with `createEmailClient`/`createMessageClient`/\n `createSender` hooks. Never import a provider SDK from any module reachable\n from `src/index.ts` — the consumer bundle gate (`packages/bundle-gate`)\n fails CI when one becomes reachable.\n- `Attachment.readContent()` loads `@happyvertical/files` through core's\n `importOptionalDependency` boundary and keeps its null-on-failure contract\n in builds where the SDK is absent.\n\n## Gotchas\n\n- **Manifest before decorators**: every decorated model imports\n `../__smrt-register__.js`, and `package.json#sideEffects` lists\n `./src/__smrt-register__.ts`. Without both, the library build dropped or\n reordered the self-registration, so dist classes registered with only their\n decorated fields and could adopt smrt-ledgers' same-named `Account` (#3098).\n- **`Account` is not unique**: smrt-ledgers also declares `Account` (on\n `ledger_accounts`). Resolve by constructor or qualified name; the\n coexistence tests (`ledger-account-identity-*`, and the PostgreSQL lane\n `pnpm test:postgres`) load both packages in both orders.\n- **STI `_meta_type`**: qualified format `@happyvertical/smrt-messages:Email`\n- **JSON fields**: all address/metadata fields are JSON strings with `getX()`/`setX()` helpers\n- **RFC 822 threading**: Email uses `inReplyTo`/`messageId`/`references` — manual management required\n- **Attachment.messageId**: use this field (not `emailId`); old getter/setter mapped via deprecation wrapper\n- **Optional tenancy**: `@TenantScoped({ mode: 'optional' })` on Message, Account, Attachment\n- **Required tenancy**: endpoints and persona routes are always tenant scoped\n- **Secrets are write-only**: never serialize credentials or endpoint.address to an app\n\n## Intake byte snapshots\n\n`EmailAccount.readIntakeMessage(providerLookup, maxBytes, imapIdentity?)` is the authorized host seam for\nopt-in ingestion. It copies full-message attachment bytes from the registered\nemail provider and fails when only attachment metadata is available; it never\nreads provider-supplied paths or advances sync checkpoints. Keep this path\nprovider-neutral and exclude arbitrary headers/credentials from its snapshot.\nIMAP lookup must include folder/UIDVALIDITY/UID verification; sender Message-ID\nis a lookup hint, never the durable delivery identity.\n",
|
|
12747
|
+
"moduleDocs": [
|
|
12748
|
+
{
|
|
12749
|
+
"path": "agents/notification-outbox.md",
|
|
12750
|
+
"module": "notification-outbox",
|
|
12751
|
+
"content": "# Notification email outbox (#4237)\n\n`NotificationOutboxService` turns a host event into at most one outbound email\nper recipient and delivers it from a crash-safe, leased outbox. It pairs with\n`UserNotificationService`: hosts that tell someone in-app and by email pass the\nsame `sourceRef` to both.\n\nSource: `src/models/NotificationDelivery.ts`,\n`src/services/NotificationOutboxService.ts`,\ntests `src/__tests__/notification-outbox*.test.ts`.\n\n## Enqueue\n\n`enqueueEmail({ tenantId, recipientUserId, sourceRef, kind, subject, text, path, notBefore? })`\nstores one `NotificationDelivery` (`notification_deliveries`) per\n`(tenant_id, recipient_user_id, channel, source_ref)`, the conflict identity. A\nrepeat (or a concurrent duplicate that loses the insert) returns the stored row\nuntouched with `created: false`. Encode each distinct event in `sourceRef`, e.g.\n`project-question:<id>:asked` and `project-question:<id>:reminder:<n>`.\n\nHost authorization, all checked before anything is stored:\n\n- `resolveRecipient({ tenantId, recipientUserId, channel })` returns the address\n the host allows for that user in that tenant, or `null` (denied with\n `NotificationOutboxDeniedError`, reason `recipient`). Callers never pass an\n address; the resolved one is snapshotted on the row.\n- `resolveSenderAccountId({ tenantId })` names an active `EmailAccount` owned by\n the same tenant (reason `sender` otherwise). Configuring that account is\n deployment setup; the outbox has no mail transport of its own.\n- With `principal`, enqueue, `listUncertain`, and `reconcile` require\n `messages.send` in the tenant (reason `permission`). Omit it only for trusted\n system code that authorizes itself.\n- `isSuppressed(...)` (optional) is the opt-out hook. A suppressed enqueue stores\n a `cancelled` row (so the event stays deduplicated); suppression at dispatch\n cancels before handoff. The package's own preference store is the\n [opt-out ledger](#opt-out-ledger); wire it with `optOutSuppression`.\n\nDeep links: `path` must be host-relative (`/portal/...`); it is joined to\n`linkBaseUrl` (absolute http(s), no credentials/query/fragment, validated at\nconstruction) and must stay on that origin — `//host`, backslashes, whitespace,\nand control characters are refused (reason `link`). The link is stored on the\nrow, so a duplicate enqueue keeps the first link. Links carry no tokens: they\nlead to the host's normal authenticated route. Subjects must be one line.\n\n## Dispatch\n\n`dispatchDue({ tenantIds?, limit? })` (default 25, max 200) leases due rows —\n`pending` with `nextAttemptAt <= now`, and `sending` rows whose lease lapsed —\nfor the given tenants, or every tenant (discovered under `withSystemContext`)\nwhen `tenantIds` is omitted. Each row is processed under `withTenant(row.tenantId)`.\nRun it from a scheduled job; several workers may run it concurrently.\n\nEvery state change is `NotificationDelivery.transition()`: a compare-and-set on\n`stateVersion` (and the lease token for a lease holder) that writes a unique\n`transitionId` and re-reads the row to prove the write won. PostgreSQL and\nremote LibSQL use one conditional `UPDATE`; local embedded engines use the\nprocess write queue, like `Message.send()`'s claim.\n\nPer row:\n\n1. Acquire: `status = sending`, new `leaseToken`, `leaseExpiresAt = now + leaseMs`\n (default 5 min; must exceed the slowest provider call), `attempts + 1`.\n2. Create the single outbound `Email` and record `messageId`. Its id is\n `notificationDeliveryMessageId(deliveryId)` (a name-based uuid) and it is\n inserted insert-only, so a crashed or stale dispatcher finds or collides\n with the stored row instead of creating a second email; its correlation id\n is `notification-delivery:<deliveryId>`.\n3. Record `handedOffAt`, then call `transport.send({ delivery, message, idempotencyKey })`.\n4. Settle with the lease token: `accepted` → `sent`; `rejected` + `retryable`\n → `pending` after `retryDelayMs(attempts)` (default 1 min · 2ⁿ⁻¹, capped at\n 1 h) until `maxAttempts` (default 5), then `failed`; non-retryable → `failed`;\n `unknown` (including a thrown send) → `uncertain`. A holder whose lease was\n taken over gets `lease-lost` and writes nothing.\n\n## Provider idempotency and reconciliation policy\n\nDelivery is at-most-once once a provider may have seen the message.\n\n- A lapsed lease with no `handedOffAt` never reached the provider: it is resent.\n- Ambiguous provider failures are not rejections. `EmailSender` sets\n `MessageSendResult.outcomeUnknown` unless `isDefiniteSendRejection()` proves\n the send was refused (an SMTP reply code, an SDK auth/validation error, a\n send error that accepted no recipient, or a connection that was never made);\n `Message.send()` sets it when a sender throws. `Message` persists the marker\n in `metadata.sendOutcomeUnknown` with the `failed` status\n (`Message.isSendOutcomeUnknown()`), and `messageSendTransport` maps it to\n `unknown`, so the delivery becomes `uncertain` instead of being retried.\n- A lapsed lease after `handedOffAt` asks `transport.reconcile()`. The default\n `messageSendTransport` reads the canonical `Message.sendStatus`: `sent` →\n delivery `sent` (no resend), `failed` → normal retry (or `uncertain` when the\n failure carries the outcome-unknown marker), `draft`/`pending` → resend,\n anything else (e.g. still `sending`) → `uncertain`.\n- `uncertain` rows are never resent automatically. Operators list them with\n `listUncertain({ tenantId })` and settle them with\n `reconcile({ tenantId, deliveryId, outcome: 'sent' | 'failed' | 'retry', note? })`\n after checking the provider; `retry` accepts the risk of a duplicate. It\n first releases a message a dead sender left `sending`\n (`Message.abandonSendAttempt()`, a compare-and-set to `failed`) so the next\n lease can send it, and refuses when the message is recorded as `sent`.\n- A transport with `idempotent: true` declares that its provider deduplicates on\n `idempotencyKey` (stable per delivery); unknown outcomes are then retried\n instead of parked. SMTP and the current SDK email providers take no key, so\n `messageSendTransport` is not idempotent.\n\n## Transport seam\n\n`NotificationTransport { name; idempotent?; send(request); reconcile?(request) }`.\nThe default delivers through `Message.send()` → account → registered provider\nsender (import `@happyvertical/smrt-messages/providers/email` at server\nstartup). Tests inject a fake; nothing in the outbox reaches a provider SDK.\n\n## Opt-out ledger\n\n`NotificationOptOut` (`notification_opt_outs`, tenant-required, generated\nREST/MCP/CLI off) records one row per `(tenant_id, recipient_user_id, channel,\nscope)`: `channel` is `email`; `scope` is `*` (all notification email in the\ntenant) or a host key such as `project:<uuid>` (lowercase prefix, `:`, then\n`[A-Za-z0-9._:-]{1,200}`; malformed scopes throw). `optedOutAt` is set while\nopted out and cleared by an opt-in (the row stays as history); `updatedBy` is\nthe person's user id, or `system` for host code. It exists because\n`smrt-preferences` is owner-read only, while suppression runs where the recipient\nis not the caller (a manager enqueues, a worker dispatches).\n\n`NotificationOptOutService({ db })`:\n\n- `optOut({ tenantId, recipientUserId, channel, scope, principal? })` and\n `optIn(...)` are idempotent. With `principal: { userId, tenantId }` only\n `principal.userId === recipientUserId` may change the row, and only when\n `principal.tenantId` equals the request's tenant: a principal never acts in\n another tenant (`NotificationOptOutDeniedError`, reason `principal` or\n `tenant`). Without a principal the call is trusted host code that\n authenticated the recipient itself (e.g. a signed unsubscribe link, or a host\n route whose member belongs to a child tenant of the delivery tenant).\n- `isOptedOut({ tenantId, recipientUserId, channel, scope })` is the trusted\n suppression read, under `withTenant`; a `*` row matches every scope.\n- `listForRecipient({ tenantId, recipientUserId, principal })` returns only the\n recipient's own active opt-outs; any other principal is denied.\n\n```ts\nconst optOuts = new NotificationOptOutService({ db });\nnew NotificationOutboxService({\n // ...\n // `scopeOf` maps the host's own event to a scope; null = not opt-out-able.\n // A question's sourceRef names the question, not its project, so the host\n // resolves the project itself (scopeOf may be async).\n isSuppressed: optOutSuppression(optOuts, async (request) => {\n const projectId = await projectIdForSourceRef(request.tenantId, request.sourceRef);\n return projectId ? `project:${projectId}` : null;\n }),\n});\n```\n\nBecause the outbox re-checks at dispatch, an opt-out recorded after enqueue\ncancels the pending delivery before handoff. Tests:\n`src/__tests__/notification-opt-out*.test.ts`.\n\n## Schema\n\nNew tables `notification_opt_outs` (same rules) and `notification_deliveries` (tenant-required, generated REST/MCP/CLI\noff): consumers run `db:migrate`. `recipient_user_id` is a cross-package uuid\nref to `smrt-users:User`; `account_id`/`message_id` are FKs to `accounts` and\n`messages`.\n"
|
|
12752
|
+
}
|
|
12753
|
+
],
|
|
12199
12754
|
"recipes": [
|
|
12200
12755
|
{
|
|
12201
12756
|
"id": "messages.mailbox",
|