@happyvertical/smrt-messages 0.60.8 → 0.60.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -0
- package/README.md +40 -0
- package/agents/notification-outbox.md +116 -0
- package/dist/chunks/{AccountCollection-O-zbituV.js → AccountCollection-By48bdwt.js} +2 -2
- package/dist/chunks/{AccountCollection-O-zbituV.js.map → AccountCollection-By48bdwt.js.map} +1 -1
- package/dist/chunks/{Email-MRtj6D2s.js → Email-CMUK3aWU.js} +3 -3
- package/dist/chunks/{Email-MRtj6D2s.js.map → Email-CMUK3aWU.js.map} +1 -1
- package/dist/chunks/{EmailFolder-qpOwfo_O.js → EmailFolder-DLJvqNqI.js} +2 -2
- package/dist/chunks/{EmailFolder-qpOwfo_O.js.map → EmailFolder-DLJvqNqI.js.map} +1 -1
- package/dist/chunks/{Message-5Xz9tauF.js → Message-DXpqot5f.js} +52 -5
- package/dist/chunks/Message-DXpqot5f.js.map +1 -0
- package/dist/chunks/{MessageCollection-m1C6fDwS.js → MessageCollection-CMGaSGBc.js} +2 -2
- package/dist/chunks/{MessageCollection-m1C6fDwS.js.map → MessageCollection-CMGaSGBc.js.map} +1 -1
- package/dist/chunks/__smrt-register__-CaGLtiEU.js +6 -0
- package/dist/chunks/__smrt-register__-CaGLtiEU.js.map +1 -0
- package/dist/collections/NotificationDeliveryCollection.d.ts +6 -0
- package/dist/collections/NotificationDeliveryCollection.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +780 -38
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1135 -89
- package/dist/models/Message.d.ts +15 -0
- package/dist/models/Message.d.ts.map +1 -1
- package/dist/models/NotificationDelivery.d.ts +128 -0
- package/dist/models/NotificationDelivery.d.ts.map +1 -0
- package/dist/senders/EmailSender.d.ts +9 -0
- package/dist/senders/EmailSender.d.ts.map +1 -1
- package/dist/services/NotificationOutboxService.d.ts +226 -0
- package/dist/services/NotificationOutboxService.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +470 -9
- package/dist/smrt-knowledge.okf.md +155 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -16
- package/dist/chunks/Message-5Xz9tauF.js.map +0 -1
- package/dist/chunks/__smrt-register__-DwNoP4GB.js +0 -6
- package/dist/chunks/__smrt-register__-DwNoP4GB.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.10",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "a997261a79d14f9861d9e7960f327718b4f53fe5d605bc79ec9813b59191c686",
|
|
11
|
+
"packageJson": "5ea3a0c6c3f70caa37499d8ce65a3dd662dbff84c22e012042311d2443dd08b6",
|
|
12
|
+
"agents": "8a288e588d8fe22e11912ff3e786e65c92bde9f69df6476f2be3b8eb7058528f",
|
|
13
|
+
"moduleDoc:agents/notification-outbox.md": "c52c52bc6e86da5520982f79ec46f47a926fa8dc1a12e44f4e6bbb5b1fdbd4e3"
|
|
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:8",
|
|
83
84
|
"sensitive-fields-excluded",
|
|
84
85
|
"sti-inheritance"
|
|
85
86
|
],
|
|
@@ -2805,6 +2806,23 @@
|
|
|
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
|
+
},
|
|
2808
2826
|
{
|
|
2809
2827
|
"name": "PersonaMessageRouteCollection",
|
|
2810
2828
|
"qualifiedName": "@happyvertical/smrt-messages:PersonaMessageRouteCollection",
|
|
@@ -4123,6 +4141,7 @@
|
|
|
4123
4141
|
}
|
|
4124
4142
|
],
|
|
4125
4143
|
"methods": [
|
|
4144
|
+
"abandonSendAttempt",
|
|
4126
4145
|
"createForward",
|
|
4127
4146
|
"createReply",
|
|
4128
4147
|
"getAccount",
|
|
@@ -4140,6 +4159,7 @@
|
|
|
4140
4159
|
"getThreadEmails",
|
|
4141
4160
|
"getThreadMessages",
|
|
4142
4161
|
"getToAddresses",
|
|
4162
|
+
"isSendOutcomeUnknown",
|
|
4143
4163
|
"isUnread",
|
|
4144
4164
|
"markRead",
|
|
4145
4165
|
"markUnread",
|
|
@@ -4153,6 +4173,14 @@
|
|
|
4153
4173
|
"toggleFlagged"
|
|
4154
4174
|
],
|
|
4155
4175
|
"methodSignatures": [
|
|
4176
|
+
{
|
|
4177
|
+
"name": "abandonSendAttempt",
|
|
4178
|
+
"async": true,
|
|
4179
|
+
"params": [
|
|
4180
|
+
"reason: string"
|
|
4181
|
+
],
|
|
4182
|
+
"returns": "Promise<boolean>"
|
|
4183
|
+
},
|
|
4156
4184
|
{
|
|
4157
4185
|
"name": "createForward",
|
|
4158
4186
|
"returns": "Email"
|
|
@@ -4233,6 +4261,10 @@
|
|
|
4233
4261
|
"name": "getToAddresses",
|
|
4234
4262
|
"returns": "Array<object>"
|
|
4235
4263
|
},
|
|
4264
|
+
{
|
|
4265
|
+
"name": "isSendOutcomeUnknown",
|
|
4266
|
+
"returns": "boolean"
|
|
4267
|
+
},
|
|
4236
4268
|
{
|
|
4237
4269
|
"name": "isUnread",
|
|
4238
4270
|
"returns": "boolean"
|
|
@@ -4460,6 +4492,18 @@
|
|
|
4460
4492
|
"operation": "send",
|
|
4461
4493
|
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4462
4494
|
},
|
|
4495
|
+
{
|
|
4496
|
+
"kind": "cli",
|
|
4497
|
+
"name": "email_isSendOutcomeUnknown",
|
|
4498
|
+
"operation": "isSendOutcomeUnknown",
|
|
4499
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4500
|
+
},
|
|
4501
|
+
{
|
|
4502
|
+
"kind": "cli",
|
|
4503
|
+
"name": "email_abandonSendAttempt",
|
|
4504
|
+
"operation": "abandonSendAttempt",
|
|
4505
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4506
|
+
},
|
|
4463
4507
|
{
|
|
4464
4508
|
"kind": "cli",
|
|
4465
4509
|
"name": "email_retrySend",
|
|
@@ -4558,6 +4602,13 @@
|
|
|
4558
4602
|
}
|
|
4559
4603
|
],
|
|
4560
4604
|
"withheldSurfaces": [
|
|
4605
|
+
{
|
|
4606
|
+
"kind": "api",
|
|
4607
|
+
"operation": "abandonSendAttempt",
|
|
4608
|
+
"code": "not-included",
|
|
4609
|
+
"reason": "not listed in api.include",
|
|
4610
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4611
|
+
},
|
|
4561
4612
|
{
|
|
4562
4613
|
"kind": "api",
|
|
4563
4614
|
"operation": "createForward",
|
|
@@ -4677,6 +4728,13 @@
|
|
|
4677
4728
|
"reason": "not listed in api.include",
|
|
4678
4729
|
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4679
4730
|
},
|
|
4731
|
+
{
|
|
4732
|
+
"kind": "api",
|
|
4733
|
+
"operation": "isSendOutcomeUnknown",
|
|
4734
|
+
"code": "not-included",
|
|
4735
|
+
"reason": "not listed in api.include",
|
|
4736
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
4737
|
+
},
|
|
4680
4738
|
{
|
|
4681
4739
|
"kind": "api",
|
|
4682
4740
|
"operation": "isUnread",
|
|
@@ -6090,6 +6148,7 @@
|
|
|
6090
6148
|
}
|
|
6091
6149
|
],
|
|
6092
6150
|
"methods": [
|
|
6151
|
+
"abandonSendAttempt",
|
|
6093
6152
|
"createForward",
|
|
6094
6153
|
"createReply",
|
|
6095
6154
|
"getAccount",
|
|
@@ -6099,6 +6158,7 @@
|
|
|
6099
6158
|
"getPreview",
|
|
6100
6159
|
"getThreadMessages",
|
|
6101
6160
|
"getToAddresses",
|
|
6161
|
+
"isSendOutcomeUnknown",
|
|
6102
6162
|
"isUnread",
|
|
6103
6163
|
"markRead",
|
|
6104
6164
|
"markUnread",
|
|
@@ -6109,6 +6169,14 @@
|
|
|
6109
6169
|
"toggleFlagged"
|
|
6110
6170
|
],
|
|
6111
6171
|
"methodSignatures": [
|
|
6172
|
+
{
|
|
6173
|
+
"name": "abandonSendAttempt",
|
|
6174
|
+
"async": true,
|
|
6175
|
+
"params": [
|
|
6176
|
+
"reason: string"
|
|
6177
|
+
],
|
|
6178
|
+
"returns": "Promise<boolean>"
|
|
6179
|
+
},
|
|
6112
6180
|
{
|
|
6113
6181
|
"name": "createForward",
|
|
6114
6182
|
"returns": "Message"
|
|
@@ -6155,6 +6223,10 @@
|
|
|
6155
6223
|
"name": "getToAddresses",
|
|
6156
6224
|
"returns": "Array<object>"
|
|
6157
6225
|
},
|
|
6226
|
+
{
|
|
6227
|
+
"name": "isSendOutcomeUnknown",
|
|
6228
|
+
"returns": "boolean"
|
|
6229
|
+
},
|
|
6158
6230
|
{
|
|
6159
6231
|
"name": "isUnread",
|
|
6160
6232
|
"returns": "boolean"
|
|
@@ -6348,6 +6420,18 @@
|
|
|
6348
6420
|
"operation": "send",
|
|
6349
6421
|
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6350
6422
|
},
|
|
6423
|
+
{
|
|
6424
|
+
"kind": "cli",
|
|
6425
|
+
"name": "message_isSendOutcomeUnknown",
|
|
6426
|
+
"operation": "isSendOutcomeUnknown",
|
|
6427
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6428
|
+
},
|
|
6429
|
+
{
|
|
6430
|
+
"kind": "cli",
|
|
6431
|
+
"name": "message_abandonSendAttempt",
|
|
6432
|
+
"operation": "abandonSendAttempt",
|
|
6433
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6434
|
+
},
|
|
6351
6435
|
{
|
|
6352
6436
|
"kind": "cli",
|
|
6353
6437
|
"name": "message_retrySend",
|
|
@@ -6380,6 +6464,13 @@
|
|
|
6380
6464
|
}
|
|
6381
6465
|
],
|
|
6382
6466
|
"withheldSurfaces": [
|
|
6467
|
+
{
|
|
6468
|
+
"kind": "api",
|
|
6469
|
+
"operation": "abandonSendAttempt",
|
|
6470
|
+
"code": "not-included",
|
|
6471
|
+
"reason": "not listed in api.include",
|
|
6472
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6473
|
+
},
|
|
6383
6474
|
{
|
|
6384
6475
|
"kind": "api",
|
|
6385
6476
|
"operation": "createForward",
|
|
@@ -6443,6 +6534,13 @@
|
|
|
6443
6534
|
"reason": "not listed in api.include",
|
|
6444
6535
|
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6445
6536
|
},
|
|
6537
|
+
{
|
|
6538
|
+
"kind": "api",
|
|
6539
|
+
"operation": "isSendOutcomeUnknown",
|
|
6540
|
+
"code": "not-included",
|
|
6541
|
+
"reason": "not listed in api.include",
|
|
6542
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
6543
|
+
},
|
|
6446
6544
|
{
|
|
6447
6545
|
"kind": "api",
|
|
6448
6546
|
"operation": "isUnread",
|
|
@@ -6682,6 +6780,234 @@
|
|
|
6682
6780
|
"tags": [],
|
|
6683
6781
|
"risks": []
|
|
6684
6782
|
},
|
|
6783
|
+
{
|
|
6784
|
+
"name": "NotificationDelivery",
|
|
6785
|
+
"qualifiedName": "@happyvertical/smrt-messages:NotificationDelivery",
|
|
6786
|
+
"collection": "notificationdeliveries",
|
|
6787
|
+
"description": "One durable, source-keyed notification delivery to one person over one external channel (the outbox row behind NotificationOutboxService).",
|
|
6788
|
+
"tableName": "notification_deliveries",
|
|
6789
|
+
"packageName": "@happyvertical/smrt-messages",
|
|
6790
|
+
"extends": "SmrtObject",
|
|
6791
|
+
"fields": [
|
|
6792
|
+
{
|
|
6793
|
+
"name": "tenantId",
|
|
6794
|
+
"type": "text",
|
|
6795
|
+
"required": true,
|
|
6796
|
+
"columnType": "UUID"
|
|
6797
|
+
},
|
|
6798
|
+
{
|
|
6799
|
+
"name": "recipientUserId",
|
|
6800
|
+
"type": "crossPackageRef",
|
|
6801
|
+
"required": true,
|
|
6802
|
+
"related": "@happyvertical/smrt-users:User",
|
|
6803
|
+
"columnType": "UUID"
|
|
6804
|
+
},
|
|
6805
|
+
{
|
|
6806
|
+
"name": "channel",
|
|
6807
|
+
"type": "text",
|
|
6808
|
+
"required": true,
|
|
6809
|
+
"columnType": "TEXT",
|
|
6810
|
+
"default": "email"
|
|
6811
|
+
},
|
|
6812
|
+
{
|
|
6813
|
+
"name": "sourceRef",
|
|
6814
|
+
"type": "text",
|
|
6815
|
+
"required": true,
|
|
6816
|
+
"columnType": "TEXT"
|
|
6817
|
+
},
|
|
6818
|
+
{
|
|
6819
|
+
"name": "kind",
|
|
6820
|
+
"type": "text",
|
|
6821
|
+
"required": true,
|
|
6822
|
+
"columnType": "TEXT"
|
|
6823
|
+
},
|
|
6824
|
+
{
|
|
6825
|
+
"name": "subject",
|
|
6826
|
+
"type": "text",
|
|
6827
|
+
"required": true,
|
|
6828
|
+
"columnType": "TEXT"
|
|
6829
|
+
},
|
|
6830
|
+
{
|
|
6831
|
+
"name": "textBody",
|
|
6832
|
+
"type": "text",
|
|
6833
|
+
"required": true,
|
|
6834
|
+
"columnType": "TEXT"
|
|
6835
|
+
},
|
|
6836
|
+
{
|
|
6837
|
+
"name": "href",
|
|
6838
|
+
"type": "text",
|
|
6839
|
+
"required": true,
|
|
6840
|
+
"columnType": "TEXT"
|
|
6841
|
+
},
|
|
6842
|
+
{
|
|
6843
|
+
"name": "recipientAddress",
|
|
6844
|
+
"type": "text",
|
|
6845
|
+
"required": true,
|
|
6846
|
+
"columnType": "TEXT"
|
|
6847
|
+
},
|
|
6848
|
+
{
|
|
6849
|
+
"name": "recipientName",
|
|
6850
|
+
"type": "text",
|
|
6851
|
+
"required": false,
|
|
6852
|
+
"columnType": "TEXT"
|
|
6853
|
+
},
|
|
6854
|
+
{
|
|
6855
|
+
"name": "accountId",
|
|
6856
|
+
"type": "foreignKey",
|
|
6857
|
+
"required": false,
|
|
6858
|
+
"related": "Account",
|
|
6859
|
+
"columnType": "UUID"
|
|
6860
|
+
},
|
|
6861
|
+
{
|
|
6862
|
+
"name": "messageId",
|
|
6863
|
+
"type": "foreignKey",
|
|
6864
|
+
"required": false,
|
|
6865
|
+
"related": "Message",
|
|
6866
|
+
"columnType": "UUID"
|
|
6867
|
+
},
|
|
6868
|
+
{
|
|
6869
|
+
"name": "status",
|
|
6870
|
+
"type": "text",
|
|
6871
|
+
"required": false,
|
|
6872
|
+
"columnType": "TEXT",
|
|
6873
|
+
"default": "pending"
|
|
6874
|
+
},
|
|
6875
|
+
{
|
|
6876
|
+
"name": "stateVersion",
|
|
6877
|
+
"type": "integer",
|
|
6878
|
+
"required": true,
|
|
6879
|
+
"columnType": "INTEGER",
|
|
6880
|
+
"default": 0
|
|
6881
|
+
},
|
|
6882
|
+
{
|
|
6883
|
+
"name": "transitionId",
|
|
6884
|
+
"type": "text",
|
|
6885
|
+
"required": false,
|
|
6886
|
+
"columnType": "TEXT"
|
|
6887
|
+
},
|
|
6888
|
+
{
|
|
6889
|
+
"name": "attempts",
|
|
6890
|
+
"type": "integer",
|
|
6891
|
+
"required": true,
|
|
6892
|
+
"columnType": "INTEGER",
|
|
6893
|
+
"default": 0
|
|
6894
|
+
},
|
|
6895
|
+
{
|
|
6896
|
+
"name": "maxAttempts",
|
|
6897
|
+
"type": "integer",
|
|
6898
|
+
"required": true,
|
|
6899
|
+
"columnType": "INTEGER",
|
|
6900
|
+
"default": 5
|
|
6901
|
+
},
|
|
6902
|
+
{
|
|
6903
|
+
"name": "nextAttemptAt",
|
|
6904
|
+
"type": "text",
|
|
6905
|
+
"required": false,
|
|
6906
|
+
"columnType": "TEXT"
|
|
6907
|
+
},
|
|
6908
|
+
{
|
|
6909
|
+
"name": "leaseToken",
|
|
6910
|
+
"type": "text",
|
|
6911
|
+
"required": false,
|
|
6912
|
+
"columnType": "TEXT"
|
|
6913
|
+
},
|
|
6914
|
+
{
|
|
6915
|
+
"name": "leaseExpiresAt",
|
|
6916
|
+
"type": "datetime",
|
|
6917
|
+
"required": false,
|
|
6918
|
+
"columnType": "TIMESTAMP"
|
|
6919
|
+
},
|
|
6920
|
+
{
|
|
6921
|
+
"name": "handedOffAt",
|
|
6922
|
+
"type": "datetime",
|
|
6923
|
+
"required": false,
|
|
6924
|
+
"columnType": "TIMESTAMP"
|
|
6925
|
+
},
|
|
6926
|
+
{
|
|
6927
|
+
"name": "deliveredAt",
|
|
6928
|
+
"type": "datetime",
|
|
6929
|
+
"required": false,
|
|
6930
|
+
"columnType": "TIMESTAMP"
|
|
6931
|
+
},
|
|
6932
|
+
{
|
|
6933
|
+
"name": "providerMessageId",
|
|
6934
|
+
"type": "text",
|
|
6935
|
+
"required": false,
|
|
6936
|
+
"columnType": "TEXT"
|
|
6937
|
+
},
|
|
6938
|
+
{
|
|
6939
|
+
"name": "lastError",
|
|
6940
|
+
"type": "text",
|
|
6941
|
+
"required": false,
|
|
6942
|
+
"columnType": "TEXT"
|
|
6943
|
+
}
|
|
6944
|
+
],
|
|
6945
|
+
"relationships": [
|
|
6946
|
+
{
|
|
6947
|
+
"name": "recipientUserId",
|
|
6948
|
+
"type": "crossPackageRef",
|
|
6949
|
+
"required": true,
|
|
6950
|
+
"related": "@happyvertical/smrt-users:User",
|
|
6951
|
+
"columnType": "UUID"
|
|
6952
|
+
},
|
|
6953
|
+
{
|
|
6954
|
+
"name": "accountId",
|
|
6955
|
+
"type": "foreignKey",
|
|
6956
|
+
"required": false,
|
|
6957
|
+
"related": "Account",
|
|
6958
|
+
"columnType": "UUID"
|
|
6959
|
+
},
|
|
6960
|
+
{
|
|
6961
|
+
"name": "messageId",
|
|
6962
|
+
"type": "foreignKey",
|
|
6963
|
+
"required": false,
|
|
6964
|
+
"related": "Message",
|
|
6965
|
+
"columnType": "UUID"
|
|
6966
|
+
}
|
|
6967
|
+
],
|
|
6968
|
+
"methods": [
|
|
6969
|
+
"transition"
|
|
6970
|
+
],
|
|
6971
|
+
"methodSignatures": [
|
|
6972
|
+
{
|
|
6973
|
+
"name": "transition",
|
|
6974
|
+
"async": true,
|
|
6975
|
+
"params": [
|
|
6976
|
+
"changes: NotificationDeliveryTransition",
|
|
6977
|
+
"expected?: object"
|
|
6978
|
+
],
|
|
6979
|
+
"returns": "Promise<boolean>"
|
|
6980
|
+
}
|
|
6981
|
+
],
|
|
6982
|
+
"tenant": {
|
|
6983
|
+
"scoped": true,
|
|
6984
|
+
"mode": "required",
|
|
6985
|
+
"field": "tenantId"
|
|
6986
|
+
},
|
|
6987
|
+
"conflictColumns": [
|
|
6988
|
+
"tenant_id",
|
|
6989
|
+
"recipient_user_id",
|
|
6990
|
+
"channel",
|
|
6991
|
+
"source_ref"
|
|
6992
|
+
],
|
|
6993
|
+
"surfaces": [],
|
|
6994
|
+
"withheldSurfaces": [
|
|
6995
|
+
{
|
|
6996
|
+
"kind": "api",
|
|
6997
|
+
"operation": "transition",
|
|
6998
|
+
"code": "api-disabled",
|
|
6999
|
+
"reason": "api is disabled",
|
|
7000
|
+
"objectName": "@happyvertical/smrt-messages:NotificationDelivery"
|
|
7001
|
+
}
|
|
7002
|
+
],
|
|
7003
|
+
"relationshipFeatures": [
|
|
7004
|
+
"crossPackageRef",
|
|
7005
|
+
"foreignKey",
|
|
7006
|
+
"uuidColumns"
|
|
7007
|
+
],
|
|
7008
|
+
"tags": [],
|
|
7009
|
+
"risks": []
|
|
7010
|
+
},
|
|
6685
7011
|
{
|
|
6686
7012
|
"name": "PersonaMessageRoute",
|
|
6687
7013
|
"qualifiedName": "@happyvertical/smrt-messages:PersonaMessageRoute",
|
|
@@ -7422,6 +7748,7 @@
|
|
|
7422
7748
|
}
|
|
7423
7749
|
],
|
|
7424
7750
|
"methods": [
|
|
7751
|
+
"abandonSendAttempt",
|
|
7425
7752
|
"createForward",
|
|
7426
7753
|
"createReply",
|
|
7427
7754
|
"getAccount",
|
|
@@ -7435,6 +7762,7 @@
|
|
|
7435
7762
|
"getToAddresses",
|
|
7436
7763
|
"getTotalReactions",
|
|
7437
7764
|
"isInThread",
|
|
7765
|
+
"isSendOutcomeUnknown",
|
|
7438
7766
|
"isUnread",
|
|
7439
7767
|
"markRead",
|
|
7440
7768
|
"markUnread",
|
|
@@ -7445,6 +7773,14 @@
|
|
|
7445
7773
|
"toggleFlagged"
|
|
7446
7774
|
],
|
|
7447
7775
|
"methodSignatures": [
|
|
7776
|
+
{
|
|
7777
|
+
"name": "abandonSendAttempt",
|
|
7778
|
+
"async": true,
|
|
7779
|
+
"params": [
|
|
7780
|
+
"reason: string"
|
|
7781
|
+
],
|
|
7782
|
+
"returns": "Promise<boolean>"
|
|
7783
|
+
},
|
|
7448
7784
|
{
|
|
7449
7785
|
"name": "createForward",
|
|
7450
7786
|
"returns": "Message"
|
|
@@ -7507,6 +7843,10 @@
|
|
|
7507
7843
|
"name": "isInThread",
|
|
7508
7844
|
"returns": "boolean"
|
|
7509
7845
|
},
|
|
7846
|
+
{
|
|
7847
|
+
"name": "isSendOutcomeUnknown",
|
|
7848
|
+
"returns": "boolean"
|
|
7849
|
+
},
|
|
7510
7850
|
{
|
|
7511
7851
|
"name": "isUnread",
|
|
7512
7852
|
"returns": "boolean"
|
|
@@ -7697,6 +8037,18 @@
|
|
|
7697
8037
|
"operation": "send",
|
|
7698
8038
|
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
7699
8039
|
},
|
|
8040
|
+
{
|
|
8041
|
+
"kind": "cli",
|
|
8042
|
+
"name": "slackmessage_isSendOutcomeUnknown",
|
|
8043
|
+
"operation": "isSendOutcomeUnknown",
|
|
8044
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8045
|
+
},
|
|
8046
|
+
{
|
|
8047
|
+
"kind": "cli",
|
|
8048
|
+
"name": "slackmessage_abandonSendAttempt",
|
|
8049
|
+
"operation": "abandonSendAttempt",
|
|
8050
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8051
|
+
},
|
|
7700
8052
|
{
|
|
7701
8053
|
"kind": "cli",
|
|
7702
8054
|
"name": "slackmessage_retrySend",
|
|
@@ -7753,6 +8105,13 @@
|
|
|
7753
8105
|
}
|
|
7754
8106
|
],
|
|
7755
8107
|
"withheldSurfaces": [
|
|
8108
|
+
{
|
|
8109
|
+
"kind": "api",
|
|
8110
|
+
"operation": "abandonSendAttempt",
|
|
8111
|
+
"code": "not-included",
|
|
8112
|
+
"reason": "not listed in api.include",
|
|
8113
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8114
|
+
},
|
|
7756
8115
|
{
|
|
7757
8116
|
"kind": "api",
|
|
7758
8117
|
"operation": "createForward",
|
|
@@ -7844,6 +8203,13 @@
|
|
|
7844
8203
|
"reason": "not listed in api.include",
|
|
7845
8204
|
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
7846
8205
|
},
|
|
8206
|
+
{
|
|
8207
|
+
"kind": "api",
|
|
8208
|
+
"operation": "isSendOutcomeUnknown",
|
|
8209
|
+
"code": "not-included",
|
|
8210
|
+
"reason": "not listed in api.include",
|
|
8211
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
8212
|
+
},
|
|
7847
8213
|
{
|
|
7848
8214
|
"kind": "api",
|
|
7849
8215
|
"operation": "isUnread",
|
|
@@ -8476,6 +8842,7 @@
|
|
|
8476
8842
|
}
|
|
8477
8843
|
],
|
|
8478
8844
|
"methods": [
|
|
8845
|
+
"abandonSendAttempt",
|
|
8479
8846
|
"createForward",
|
|
8480
8847
|
"createReply",
|
|
8481
8848
|
"getAccount",
|
|
@@ -8489,6 +8856,7 @@
|
|
|
8489
8856
|
"getPreview",
|
|
8490
8857
|
"getThreadMessages",
|
|
8491
8858
|
"getToAddresses",
|
|
8859
|
+
"isSendOutcomeUnknown",
|
|
8492
8860
|
"isUnread",
|
|
8493
8861
|
"markRead",
|
|
8494
8862
|
"markUnread",
|
|
@@ -8499,6 +8867,14 @@
|
|
|
8499
8867
|
"toggleFlagged"
|
|
8500
8868
|
],
|
|
8501
8869
|
"methodSignatures": [
|
|
8870
|
+
{
|
|
8871
|
+
"name": "abandonSendAttempt",
|
|
8872
|
+
"async": true,
|
|
8873
|
+
"params": [
|
|
8874
|
+
"reason: string"
|
|
8875
|
+
],
|
|
8876
|
+
"returns": "Promise<boolean>"
|
|
8877
|
+
},
|
|
8502
8878
|
{
|
|
8503
8879
|
"name": "createForward",
|
|
8504
8880
|
"returns": "Message"
|
|
@@ -8561,6 +8937,10 @@
|
|
|
8561
8937
|
"name": "getToAddresses",
|
|
8562
8938
|
"returns": "Array<object>"
|
|
8563
8939
|
},
|
|
8940
|
+
{
|
|
8941
|
+
"name": "isSendOutcomeUnknown",
|
|
8942
|
+
"returns": "boolean"
|
|
8943
|
+
},
|
|
8564
8944
|
{
|
|
8565
8945
|
"name": "isUnread",
|
|
8566
8946
|
"returns": "boolean"
|
|
@@ -8751,6 +9131,18 @@
|
|
|
8751
9131
|
"operation": "send",
|
|
8752
9132
|
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
8753
9133
|
},
|
|
9134
|
+
{
|
|
9135
|
+
"kind": "cli",
|
|
9136
|
+
"name": "tweet_isSendOutcomeUnknown",
|
|
9137
|
+
"operation": "isSendOutcomeUnknown",
|
|
9138
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9139
|
+
},
|
|
9140
|
+
{
|
|
9141
|
+
"kind": "cli",
|
|
9142
|
+
"name": "tweet_abandonSendAttempt",
|
|
9143
|
+
"operation": "abandonSendAttempt",
|
|
9144
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9145
|
+
},
|
|
8754
9146
|
{
|
|
8755
9147
|
"kind": "cli",
|
|
8756
9148
|
"name": "tweet_retrySend",
|
|
@@ -8807,6 +9199,13 @@
|
|
|
8807
9199
|
}
|
|
8808
9200
|
],
|
|
8809
9201
|
"withheldSurfaces": [
|
|
9202
|
+
{
|
|
9203
|
+
"kind": "api",
|
|
9204
|
+
"operation": "abandonSendAttempt",
|
|
9205
|
+
"code": "not-included",
|
|
9206
|
+
"reason": "not listed in api.include",
|
|
9207
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9208
|
+
},
|
|
8810
9209
|
{
|
|
8811
9210
|
"kind": "api",
|
|
8812
9211
|
"operation": "createForward",
|
|
@@ -8898,6 +9297,13 @@
|
|
|
8898
9297
|
"reason": "not listed in api.include",
|
|
8899
9298
|
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
8900
9299
|
},
|
|
9300
|
+
{
|
|
9301
|
+
"kind": "api",
|
|
9302
|
+
"operation": "isSendOutcomeUnknown",
|
|
9303
|
+
"code": "not-included",
|
|
9304
|
+
"reason": "not listed in api.include",
|
|
9305
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
9306
|
+
},
|
|
8901
9307
|
{
|
|
8902
9308
|
"kind": "api",
|
|
8903
9309
|
"operation": "isUnread",
|
|
@@ -10871,6 +11277,18 @@
|
|
|
10871
11277
|
"operation": "send",
|
|
10872
11278
|
"objectName": "@happyvertical/smrt-messages:Email"
|
|
10873
11279
|
},
|
|
11280
|
+
{
|
|
11281
|
+
"kind": "cli",
|
|
11282
|
+
"name": "email_isSendOutcomeUnknown",
|
|
11283
|
+
"operation": "isSendOutcomeUnknown",
|
|
11284
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
11285
|
+
},
|
|
11286
|
+
{
|
|
11287
|
+
"kind": "cli",
|
|
11288
|
+
"name": "email_abandonSendAttempt",
|
|
11289
|
+
"operation": "abandonSendAttempt",
|
|
11290
|
+
"objectName": "@happyvertical/smrt-messages:Email"
|
|
11291
|
+
},
|
|
10874
11292
|
{
|
|
10875
11293
|
"kind": "cli",
|
|
10876
11294
|
"name": "email_retrySend",
|
|
@@ -11419,6 +11837,18 @@
|
|
|
11419
11837
|
"operation": "send",
|
|
11420
11838
|
"objectName": "@happyvertical/smrt-messages:Message"
|
|
11421
11839
|
},
|
|
11840
|
+
{
|
|
11841
|
+
"kind": "cli",
|
|
11842
|
+
"name": "message_isSendOutcomeUnknown",
|
|
11843
|
+
"operation": "isSendOutcomeUnknown",
|
|
11844
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
11845
|
+
},
|
|
11846
|
+
{
|
|
11847
|
+
"kind": "cli",
|
|
11848
|
+
"name": "message_abandonSendAttempt",
|
|
11849
|
+
"operation": "abandonSendAttempt",
|
|
11850
|
+
"objectName": "@happyvertical/smrt-messages:Message"
|
|
11851
|
+
},
|
|
11422
11852
|
{
|
|
11423
11853
|
"kind": "cli",
|
|
11424
11854
|
"name": "message_retrySend",
|
|
@@ -11707,6 +12137,18 @@
|
|
|
11707
12137
|
"operation": "send",
|
|
11708
12138
|
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
11709
12139
|
},
|
|
12140
|
+
{
|
|
12141
|
+
"kind": "cli",
|
|
12142
|
+
"name": "slackmessage_isSendOutcomeUnknown",
|
|
12143
|
+
"operation": "isSendOutcomeUnknown",
|
|
12144
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
12145
|
+
},
|
|
12146
|
+
{
|
|
12147
|
+
"kind": "cli",
|
|
12148
|
+
"name": "slackmessage_abandonSendAttempt",
|
|
12149
|
+
"operation": "abandonSendAttempt",
|
|
12150
|
+
"objectName": "@happyvertical/smrt-messages:SlackMessage"
|
|
12151
|
+
},
|
|
11710
12152
|
{
|
|
11711
12153
|
"kind": "cli",
|
|
11712
12154
|
"name": "slackmessage_retrySend",
|
|
@@ -11939,6 +12381,18 @@
|
|
|
11939
12381
|
"operation": "send",
|
|
11940
12382
|
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
11941
12383
|
},
|
|
12384
|
+
{
|
|
12385
|
+
"kind": "cli",
|
|
12386
|
+
"name": "tweet_isSendOutcomeUnknown",
|
|
12387
|
+
"operation": "isSendOutcomeUnknown",
|
|
12388
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
12389
|
+
},
|
|
12390
|
+
{
|
|
12391
|
+
"kind": "cli",
|
|
12392
|
+
"name": "tweet_abandonSendAttempt",
|
|
12393
|
+
"operation": "abandonSendAttempt",
|
|
12394
|
+
"objectName": "@happyvertical/smrt-messages:Tweet"
|
|
12395
|
+
},
|
|
11942
12396
|
{
|
|
11943
12397
|
"kind": "cli",
|
|
11944
12398
|
"name": "tweet_retrySend",
|
|
@@ -12188,14 +12642,21 @@
|
|
|
12188
12642
|
],
|
|
12189
12643
|
"prompts": [],
|
|
12190
12644
|
"relationshipsV2": {
|
|
12191
|
-
"foreignKeyFields":
|
|
12192
|
-
"crossPackageRefFields":
|
|
12645
|
+
"foreignKeyFields": 18,
|
|
12646
|
+
"crossPackageRefFields": 8,
|
|
12193
12647
|
"junctionCollections": 0,
|
|
12194
12648
|
"hierarchicalObjects": 0,
|
|
12195
12649
|
"polymorphicAssociations": 0,
|
|
12196
|
-
"uuidColumns":
|
|
12650
|
+
"uuidColumns": 106
|
|
12197
12651
|
},
|
|
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",
|
|
12652
|
+
"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## 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",
|
|
12653
|
+
"moduleDocs": [
|
|
12654
|
+
{
|
|
12655
|
+
"path": "agents/notification-outbox.md",
|
|
12656
|
+
"module": "notification-outbox",
|
|
12657
|
+
"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 has no preference store of its own.\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## Schema\n\nNew table `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"
|
|
12658
|
+
}
|
|
12659
|
+
],
|
|
12199
12660
|
"recipes": [
|
|
12200
12661
|
{
|
|
12201
12662
|
"id": "messages.mailbox",
|