@happyvertical/smrt-comments 0.57.1 → 0.57.3

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.
Files changed (50) hide show
  1. package/README.md +72 -0
  2. package/dist/chunks/participation-LA_cfrM2.js +276 -0
  3. package/dist/chunks/participation-LA_cfrM2.js.map +1 -0
  4. package/dist/collections/CommentReportCollection.d.ts +6 -0
  5. package/dist/collections/CommentReportCollection.d.ts.map +1 -0
  6. package/dist/collections/CommentRevisionCollection.d.ts +6 -0
  7. package/dist/collections/CommentRevisionCollection.d.ts.map +1 -0
  8. package/dist/collections/ParticipationCollection.d.ts +14 -0
  9. package/dist/collections/ParticipationCollection.d.ts.map +1 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +364 -59
  13. package/dist/index.js.map +1 -1
  14. package/dist/manifest.json +1853 -224
  15. package/dist/models/Comment.d.ts +8 -1
  16. package/dist/models/Comment.d.ts.map +1 -1
  17. package/dist/models/CommentReport.d.ts +10 -0
  18. package/dist/models/CommentReport.d.ts.map +1 -0
  19. package/dist/models/CommentRevision.d.ts +9 -0
  20. package/dist/models/CommentRevision.d.ts.map +1 -0
  21. package/dist/models/ParticipationBan.d.ts +12 -0
  22. package/dist/models/ParticipationBan.d.ts.map +1 -0
  23. package/dist/models/ParticipationPolicy.d.ts +13 -0
  24. package/dist/models/ParticipationPolicy.d.ts.map +1 -0
  25. package/dist/models/ParticipationRateBucket.d.ts +8 -0
  26. package/dist/models/ParticipationRateBucket.d.ts.map +1 -0
  27. package/dist/participation-contract.d.ts +37 -0
  28. package/dist/participation-contract.d.ts.map +1 -0
  29. package/dist/participation.d.ts +4 -0
  30. package/dist/participation.d.ts.map +1 -0
  31. package/dist/participation.js +2 -0
  32. package/dist/recipes.d.ts +6 -1
  33. package/dist/recipes.d.ts.map +1 -1
  34. package/dist/services/CommentService.d.ts +10 -1
  35. package/dist/services/CommentService.d.ts.map +1 -1
  36. package/dist/services/ParticipationService.d.ts +25 -0
  37. package/dist/services/ParticipationService.d.ts.map +1 -0
  38. package/dist/smrt-knowledge.json +477 -12
  39. package/dist/svelte/ModerationQueue.svelte +60 -0
  40. package/dist/svelte/ModerationQueue.svelte.d.ts +17 -0
  41. package/dist/svelte/ModerationQueue.svelte.d.ts.map +1 -0
  42. package/dist/svelte/RecordComments.svelte +21 -6
  43. package/dist/svelte/RecordComments.svelte.d.ts +2 -0
  44. package/dist/svelte/RecordComments.svelte.d.ts.map +1 -1
  45. package/dist/svelte/index.d.ts +2 -0
  46. package/dist/svelte/index.d.ts.map +1 -1
  47. package/dist/svelte/index.js +1 -0
  48. package/dist/types.d.ts +7 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +9 -5
package/README.md CHANGED
@@ -21,6 +21,10 @@ const comments = new CommentService({
21
21
  db,
22
22
  actor: { tenantId: session.tenantId, userId: session.userId },
23
23
  authorizeRecord: access => records.authorize(access),
24
+ participation: {
25
+ // Supplies trusted membership and entitlement facts.
26
+ authorize: request => entitlements.authorize(request),
27
+ },
24
28
  mentionNotifications: createCommentMentionNotifications({
25
29
  notifications: new UserNotificationService({ db }), // smrt-messages
26
30
  canNotify: mention => records.recipientMayRead(mention),
@@ -28,6 +32,47 @@ const comments = new CommentService({
28
32
  });
29
33
  ```
30
34
 
35
+ `participation` is an optional server-side entitlement adapter. It receives trusted
36
+ identity and record/action inputs and returns `{ allowed, member?, snapshot? }`.
37
+ Only literal `allowed: true` permits participation; `members_only` also requires
38
+ `member: true`. The immutable JSON snapshot is retained on each new comment.
39
+
40
+ `ParticipationService` owns reusable tenant/target policy selection, bans and
41
+ atomic fixed-window rate reservations. All CommentService creates invoke it,
42
+ even without an entitlement adapter. Configure `ParticipationPolicyCollection`
43
+ and `ParticipationBanCollection` on trusted administrative server paths under
44
+ `withTenant`; these models expose no automatic API/CLI/MCP operations. Migrate
45
+ all comments package models before deploying. A missing policy defaults to open;
46
+ an exact `(recordType, recordId, action)` policy replaces the tenant default
47
+ (blank record pair). Modes are `open`, `pre_moderated`, `members_only`, `closed`.
48
+ A duplicate applicable policy fails closed. Ban targets may be blank (tenant-wide)
49
+ or exact; a null expiry means indefinite. Ban decisions do not expose reasons.
50
+
51
+ `maxActions: 0` disables quota; otherwise set a positive integer `windowSeconds`.
52
+ Quota is per tenant, actor, action and selected policy scope in fixed UTC epoch
53
+ windows. A tenant policy shares quota across records. The persisted counter uses
54
+ one bounded atomic upsert; reservation and protected mutation commit or roll back
55
+ together. Never delete active rate buckets. Policy edits affect subsequent
56
+ checks, not transactions already authorized; changing a window length starts a
57
+ new quota namespace. Trusted administrators own policy and bucket retention.
58
+
59
+ Reactions/polls import `ParticipationService` and neutral types from
60
+ `@happyvertical/smrt-comments/participation`. Use `execute(request, (tx, decision)
61
+ => mutationUsing(tx))` for a new mutation. Within an existing transaction use
62
+ `executeInTransaction(tx, request, callback)` and propagate errors so the owner
63
+ rolls back. `authorize` checks current permission without spending quota. The
64
+ consumer owns its durable idempotency binding: replay retained mutations after
65
+ reauthorization, and call `execute` only for a genuinely new mutation. Comments
66
+ already implement this composition. No subscription or journalism rules live here.
67
+
68
+ `CommentService.edit` retains prior bodies and rechecks record/participation permission,
69
+ bans and the comment-action quota in the same transaction. Edits spend quota and
70
+ retain the original post-time snapshot. `report` appends a moderation report,
71
+ and `moderate` checks the host's record-level moderation permission before changing
72
+ visibility. Readers receive authorized projections; hosts filter non-visible rows
73
+ for their audience. `ModerationQueue` accepts pending/reported projections and an
74
+ `onmoderate` callback; the browser cannot grant moderation authority.
75
+
31
76
  The messages service is composed through a structural interface; there is no
32
77
  runtime dependency on messages. `canNotify` must check recipient membership and
33
78
  record access before exposing the comment. Notifications use a stable per-comment
@@ -71,3 +116,30 @@ tenant and author references. UUID casing changes across trusted actor contexts
71
116
  reuse the same comment, including after generic receipt retention. Unkeyed legacy SQLite
72
117
  listing remains scoped to its original tenant spelling; keyed rows are merged
73
118
  by equivalent UUID tenant without folding arbitrary textual tenant IDs.
119
+
120
+ Use lowercase UUIDs for administrative policy/ban tenant, actor and target identities;
121
+ request UUIDs are canonicalized before enforcement. Text identifiers retain case.
122
+ Thread projections may include `parentCommentId`; supply `onreply(parentId, body)`
123
+ to enable the reply composer. Host callbacks persist replies with that parent ID.
124
+
125
+ Edits to visible comments return to pending when current policy or entitlement
126
+ requires pre-moderation; edits never restore hidden or deleted comments.
127
+
128
+ Reports use the separate `report` participation action, so hosts can configure
129
+ report-specific limits without coupling them to comment quotas. Bans and entitlement
130
+ checks apply to every report attempt. Only visible comments are reportable. Each
131
+ actor/comment pair retains one report, including after generic receipt cleanup;
132
+ retries return that report without consuming quota or replacing its original reason.
133
+
134
+ Parentless keyed-create digests preserve the original v1 shape across upgrades.
135
+ After an authorized edit, replay verifies the original creation body against retained
136
+ revision history and returns the current comment; edited content is not a new create
137
+ payload. Revision history must be retained while the keyed comment is retained.
138
+
139
+ Mention delivery is limited to visible comments, including keyed retries. Pending,
140
+ hidden and deleted bodies never reach the mention adapter. Approval does not
141
+ send notifications automatically; hosts may explicitly retry the original keyed
142
+ create after approval to recover delivery. Moderation uses the save lifecycle on
143
+ a locked and freshly reloaded row, preserving change-feed and cache invalidation.
144
+ Parent comment UUIDs are canonicalized before lookup, persistence and retry hashing.
145
+ Reply-only embeds show a composer only while a reply target is selected.
@@ -0,0 +1,276 @@
1
+ import { ObjectRegistry, SmrtCollection, SmrtObject, digestRunOnceContent, field, smrt } from "@happyvertical/smrt-core";
2
+ import { TenantScoped, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
3
+ //#region src/__smrt-register__.ts
4
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-comments","packageVersion":"0.57.3","objects":{"@happyvertical/smrt-comments:CommentCollection":{"name":"commentcollection","className":"CommentCollection","qualifiedName":"@happyvertical/smrt-comments:CommentCollection","collection":"comments","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/CommentCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{"listForRecord":{"name":"listForRecord","async":true,"parameters":[{"name":"recordType","type":"string","optional":false},{"name":"recordId","type":"string","optional":false}],"returnType":"Promise<Comment[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"record_comments","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"Comment","exportName":"CommentCollection","collectionExportName":"CommentCollectionCollection","schema":{"tableName":"record_comments","ddl":"CREATE TABLE IF NOT EXISTS \\"record_comments\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"meta_type\\" TEXT NOT NULL,\\n \\"meta_id\\" TEXT NOT NULL,\\n \\"role\\" TEXT NOT NULL,\\n \\"sort_order\\" INTEGER DEFAULT 0,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"author_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL,\\n \\"mentions\\" TEXT,\\n \\"idempotency_key\\" TEXT,\\n \\"retry_tenant_id\\" TEXT,\\n \\"request_digest\\" TEXT,\\n \\"parent_comment_id\\" UUID,\\n \\"status\\" TEXT NOT NULL DEFAULT 'visible',\\n \\"participation_snapshot\\" TEXT,\\n \\"edited_at\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"meta_type":{"type":"TEXT","notNull":true,"unique":false},"meta_id":{"type":"TEXT","notNull":true,"unique":false},"role":{"type":"TEXT","notNull":true,"unique":false},"sort_order":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"author_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false},"mentions":{"type":"TEXT","notNull":false,"unique":false},"idempotency_key":{"type":"TEXT","notNull":false,"unique":false},"retry_tenant_id":{"type":"TEXT","notNull":false,"unique":false},"request_digest":{"type":"TEXT","notNull":false,"unique":false},"parent_comment_id":{"type":"UUID","referenceKind":"foreignKey","notNull":false,"unique":false},"status":{"type":"TEXT","notNull":true,"unique":false,"default":"visible"},"participation_snapshot":{"type":"TEXT","notNull":false,"unique":false},"edited_at":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"record_comments_tenant_request_key","columns":["retry_tenant_id","idempotency_key"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenant_id","meta_type","meta_id","created_at"]},{"name":"record_comments_parent_created_idx","columns":["tenant_id","parent_comment_id","created_at"]},{"name":"record_comments_moderation_idx","columns":["tenant_id","status","created_at"]},{"name":"record_comments_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"record_comments_meta_type_meta_id_idx","columns":["meta_type","meta_id"]},{"name":"record_comments_author_user_id_idx","columns":["author_user_id"]},{"name":"record_comments_parent_comment_id_idx","columns":["parent_comment_id"]}],"version":"35c502ab"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:CommentReportCollection":{"name":"commentreportcollection","className":"CommentReportCollection","qualifiedName":"@happyvertical/smrt-comments:CommentReportCollection","collection":"commentreports","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/CommentReportCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{},"decoratorConfig":{"tableName":"comment_reports","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"CommentReport","exportName":"CommentReportCollection","collectionExportName":"CommentReportCollectionCollection","schema":{"tableName":"comment_reports","ddl":"CREATE TABLE IF NOT EXISTS \\"comment_reports\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"comment_id\\" UUID NOT NULL,\\n \\"reporter_user_id\\" UUID NOT NULL,\\n \\"reason\\" TEXT NOT NULL,\\n \\"status\\" TEXT NOT NULL DEFAULT 'open'\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"comment_id":{"type":"UUID","referenceKind":"foreignKey","notNull":true,"unique":false},"reporter_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"reason":{"type":"TEXT","notNull":true,"unique":false},"status":{"type":"TEXT","notNull":true,"unique":false,"default":"open"}},"indexes":[{"name":"comment_reports_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"comment_reports_actor_target_unique","columns":["tenant_id","reporter_user_id","comment_id"],"unique":true},{"name":"comment_reports_queue_idx","columns":["tenant_id","status","created_at"]},{"name":"comment_reports_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"comment_reports_comment_id_idx","columns":["comment_id"]},{"name":"comment_reports_reporter_user_id_idx","columns":["reporter_user_id"]}],"version":"001951c2"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:CommentRevisionCollection":{"name":"commentrevisioncollection","className":"CommentRevisionCollection","qualifiedName":"@happyvertical/smrt-comments:CommentRevisionCollection","collection":"commentrevisions","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/CommentRevisionCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{},"decoratorConfig":{"tableName":"comment_revisions","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"CommentRevision","exportName":"CommentRevisionCollection","collectionExportName":"CommentRevisionCollectionCollection","schema":{"tableName":"comment_revisions","ddl":"CREATE TABLE IF NOT EXISTS \\"comment_revisions\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"comment_id\\" UUID NOT NULL,\\n \\"editor_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"comment_id":{"type":"UUID","referenceKind":"foreignKey","notNull":true,"unique":false},"editor_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false}},"indexes":[{"name":"comment_revisions_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"comment_revisions_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"comment_revisions_comment_id_idx","columns":["comment_id"]},{"name":"comment_revisions_editor_user_id_idx","columns":["editor_user_id"]}],"version":"69ea301e"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:ParticipationPolicyCollection":{"name":"participationpolicycollection","className":"ParticipationPolicyCollection","qualifiedName":"@happyvertical/smrt-comments:ParticipationPolicyCollection","collection":"participationpolicies","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/ParticipationCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{},"decoratorConfig":{"tableName":"participation_policies","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"ParticipationPolicy","exportName":"ParticipationPolicyCollection","collectionExportName":"ParticipationPolicyCollectionCollection","schema":{"tableName":"participation_policies","ddl":"CREATE TABLE IF NOT EXISTS \\"participation_policies\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"record_type\\" TEXT,\\n \\"record_id\\" TEXT,\\n \\"action\\" TEXT NOT NULL,\\n \\"moderation_mode\\" TEXT NOT NULL DEFAULT 'open',\\n \\"max_actions\\" INTEGER NOT NULL DEFAULT 0,\\n \\"window_seconds\\" INTEGER NOT NULL DEFAULT 0\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"record_type":{"type":"TEXT","notNull":false,"unique":false},"record_id":{"type":"TEXT","notNull":false,"unique":false},"action":{"type":"TEXT","notNull":true,"unique":false},"moderation_mode":{"type":"TEXT","notNull":true,"unique":false,"default":"open"},"max_actions":{"type":"INTEGER","notNull":true,"unique":false,"default":0},"window_seconds":{"type":"INTEGER","notNull":true,"unique":false,"default":0}},"indexes":[{"name":"participation_policies_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"participation_policies_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"7f2fdd9d"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:ParticipationBanCollection":{"name":"participationbancollection","className":"ParticipationBanCollection","qualifiedName":"@happyvertical/smrt-comments:ParticipationBanCollection","collection":"participationbans","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/ParticipationCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{},"decoratorConfig":{"tableName":"participation_bans","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"ParticipationBan","exportName":"ParticipationBanCollection","collectionExportName":"ParticipationBanCollectionCollection","schema":{"tableName":"participation_bans","ddl":"CREATE TABLE IF NOT EXISTS \\"participation_bans\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"actor_user_id\\" TEXT NOT NULL,\\n \\"record_type\\" TEXT,\\n \\"record_id\\" TEXT,\\n \\"reason\\" TEXT NOT NULL,\\n \\"expires_at\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"actor_user_id":{"type":"TEXT","notNull":true,"unique":false},"record_type":{"type":"TEXT","notNull":false,"unique":false},"record_id":{"type":"TEXT","notNull":false,"unique":false},"reason":{"type":"TEXT","notNull":true,"unique":false},"expires_at":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"participation_bans_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"participation_bans_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"27bbfb94"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:ParticipationRateBucketCollection":{"name":"participationratebucketcollection","className":"ParticipationRateBucketCollection","qualifiedName":"@happyvertical/smrt-comments:ParticipationRateBucketCollection","collection":"participationratebuckets","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/ParticipationCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{},"decoratorConfig":{"tableName":"participation_rate_buckets","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"ParticipationRateBucket","exportName":"ParticipationRateBucketCollection","collectionExportName":"ParticipationRateBucketCollectionCollection","schema":{"tableName":"participation_rate_buckets","ddl":"CREATE TABLE IF NOT EXISTS \\"participation_rate_buckets\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"bucket_key\\" TEXT NOT NULL,\\n \\"action_count\\" INTEGER NOT NULL DEFAULT 0\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"bucket_key":{"type":"TEXT","notNull":true,"unique":false},"action_count":{"type":"INTEGER","notNull":true,"unique":false,"default":0}},"indexes":[{"name":"participation_rate_buckets_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"participation_rate_bucket_unique","columns":["tenant_id","bucket_key"],"unique":true},{"name":"participation_rate_buckets_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"9f3707e0"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:Comment":{"name":"comment","className":"Comment","qualifiedName":"@happyvertical/smrt-comments:Comment","collection":"comments","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/Comment.ts","packageName":"@happyvertical/smrt-comments","fields":{"metaType":{"type":"text","required":true,"_meta":{"required":true}},"metaId":{"type":"text","required":true,"_meta":{"required":true}},"role":{"type":"text","required":true,"_meta":{"required":true}},"sortOrder":{"type":"integer","required":false,"default":0},"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"authorUserId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:User"},"body":{"type":"text","required":true,"_meta":{"required":true}},"mentions":{"type":"text","required":false},"idempotencyKey":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"retryTenantId":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"requestDigest":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"parentCommentId":{"type":"foreignKey","required":false,"related":"Comment","_meta":{"nullable":true,"constraint":false}},"status":{"type":"text","required":true,"default":"visible","_meta":{"required":true},"enum":["visible","hidden","pending","deleted"]},"participationSnapshot":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"editedAt":{"type":"datetime","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true}},"methods":{"hydrate":{"name":"hydrate","async":true,"parameters":[],"returnType":"Promise<T | null>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"record_comments","api":false,"mcp":false,"cli":false,"indexes":[{"name":"record_comments_tenant_request_key","columns":["retryTenantId","idempotencyKey"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenantId","metaType","metaId","createdAt"]},{"name":"record_comments_parent_created_idx","columns":["tenantId","parentCommentId","createdAt"]},{"name":"record_comments_moderation_idx","columns":["tenantId","status","createdAt"]}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtPolymorphicAssociation","exportName":"Comment","collectionExportName":"CommentCollection","validationRules":[{"field":"metaType","rule":"required","fieldType":"text"},{"field":"metaId","rule":"required","fieldType":"text"},{"field":"role","rule":"required","fieldType":"text"},{"field":"authorUserId","rule":"required","fieldType":"crossPackageRef"},{"field":"body","rule":"required","fieldType":"text"},{"field":"status","rule":"required","fieldType":"text"}],"schema":{"tableName":"record_comments","ddl":"CREATE TABLE IF NOT EXISTS \\"record_comments\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"meta_type\\" TEXT NOT NULL,\\n \\"meta_id\\" TEXT NOT NULL,\\n \\"role\\" TEXT NOT NULL,\\n \\"sort_order\\" INTEGER DEFAULT 0,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"author_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL,\\n \\"mentions\\" TEXT,\\n \\"idempotency_key\\" TEXT,\\n \\"retry_tenant_id\\" TEXT,\\n \\"request_digest\\" TEXT,\\n \\"parent_comment_id\\" UUID,\\n \\"status\\" TEXT NOT NULL DEFAULT 'visible',\\n \\"participation_snapshot\\" TEXT,\\n \\"edited_at\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"meta_type":{"type":"TEXT","notNull":true,"unique":false},"meta_id":{"type":"TEXT","notNull":true,"unique":false},"role":{"type":"TEXT","notNull":true,"unique":false},"sort_order":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"author_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false},"mentions":{"type":"TEXT","notNull":false,"unique":false},"idempotency_key":{"type":"TEXT","notNull":false,"unique":false},"retry_tenant_id":{"type":"TEXT","notNull":false,"unique":false},"request_digest":{"type":"TEXT","notNull":false,"unique":false},"parent_comment_id":{"type":"UUID","referenceKind":"foreignKey","notNull":false,"unique":false},"status":{"type":"TEXT","notNull":true,"unique":false,"default":"visible"},"participation_snapshot":{"type":"TEXT","notNull":false,"unique":false},"edited_at":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"record_comments_tenant_request_key","columns":["retry_tenant_id","idempotency_key"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenant_id","meta_type","meta_id","created_at"]},{"name":"record_comments_parent_created_idx","columns":["tenant_id","parent_comment_id","created_at"]},{"name":"record_comments_moderation_idx","columns":["tenant_id","status","created_at"]},{"name":"record_comments_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"record_comments_meta_type_meta_id_idx","columns":["meta_type","meta_id"]},{"name":"record_comments_author_user_id_idx","columns":["author_user_id"]},{"name":"record_comments_parent_comment_id_idx","columns":["parent_comment_id"]}],"version":"35c502ab"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:CommentReport":{"name":"commentreport","className":"CommentReport","qualifiedName":"@happyvertical/smrt-comments:CommentReport","collection":"commentreports","description":"A tenant-local report; moderation resolution never deletes the report.","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/CommentReport.ts","packageName":"@happyvertical/smrt-comments","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"commentId":{"type":"foreignKey","required":true,"related":"Comment","_meta":{"required":true,"constraint":false}},"reporterUserId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:User"},"reason":{"type":"text","required":true,"_meta":{"required":true}},"status":{"type":"text","required":true,"default":"open","_meta":{"required":true},"enum":["open","resolved","dismissed"]}},"methods":{},"decoratorConfig":{"tableName":"comment_reports","api":false,"mcp":false,"cli":false,"indexes":[{"name":"comment_reports_actor_target_unique","columns":["tenantId","reporterUserId","commentId"],"unique":true},{"name":"comment_reports_queue_idx","columns":["tenantId","status","createdAt"]}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"CommentReport","collectionExportName":"CommentReportCollection","validationRules":[{"field":"commentId","rule":"required","fieldType":"foreignKey"},{"field":"reporterUserId","rule":"required","fieldType":"crossPackageRef"},{"field":"reason","rule":"required","fieldType":"text"},{"field":"status","rule":"required","fieldType":"text"}],"schema":{"tableName":"comment_reports","ddl":"CREATE TABLE IF NOT EXISTS \\"comment_reports\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"comment_id\\" UUID NOT NULL,\\n \\"reporter_user_id\\" UUID NOT NULL,\\n \\"reason\\" TEXT NOT NULL,\\n \\"status\\" TEXT NOT NULL DEFAULT 'open'\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"comment_id":{"type":"UUID","referenceKind":"foreignKey","notNull":true,"unique":false},"reporter_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"reason":{"type":"TEXT","notNull":true,"unique":false},"status":{"type":"TEXT","notNull":true,"unique":false,"default":"open"}},"indexes":[{"name":"comment_reports_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"comment_reports_actor_target_unique","columns":["tenant_id","reporter_user_id","comment_id"],"unique":true},{"name":"comment_reports_queue_idx","columns":["tenant_id","status","created_at"]},{"name":"comment_reports_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"comment_reports_comment_id_idx","columns":["comment_id"]},{"name":"comment_reports_reporter_user_id_idx","columns":["reporter_user_id"]}],"version":"001951c2"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:CommentRevision":{"name":"commentrevision","className":"CommentRevision","qualifiedName":"@happyvertical/smrt-comments:CommentRevision","collection":"commentrevisions","description":"Immutable prior body retained whenever an author edits a comment.","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/CommentRevision.ts","packageName":"@happyvertical/smrt-comments","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"commentId":{"type":"foreignKey","required":true,"related":"Comment","_meta":{"required":true,"constraint":false}},"editorUserId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:User"},"body":{"type":"text","required":true,"_meta":{"required":true}}},"methods":{},"decoratorConfig":{"tableName":"comment_revisions","api":false,"mcp":false,"cli":false,"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"CommentRevision","collectionExportName":"CommentRevisionCollection","validationRules":[{"field":"commentId","rule":"required","fieldType":"foreignKey"},{"field":"editorUserId","rule":"required","fieldType":"crossPackageRef"},{"field":"body","rule":"required","fieldType":"text"}],"schema":{"tableName":"comment_revisions","ddl":"CREATE TABLE IF NOT EXISTS \\"comment_revisions\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"comment_id\\" UUID NOT NULL,\\n \\"editor_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"comment_id":{"type":"UUID","referenceKind":"foreignKey","notNull":true,"unique":false},"editor_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false}},"indexes":[{"name":"comment_revisions_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"comment_revisions_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"comment_revisions_comment_id_idx","columns":["comment_id"]},{"name":"comment_revisions_editor_user_id_idx","columns":["editor_user_id"]}],"version":"69ea301e"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:ParticipationBan":{"name":"participationban","className":"ParticipationBan","qualifiedName":"@happyvertical/smrt-comments:ParticipationBan","collection":"participationbans","description":"Tenant-local, reusable participation ban; a blank target applies tenant-wide.","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/ParticipationBan.ts","packageName":"@happyvertical/smrt-comments","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"actorUserId":{"type":"text","required":true,"_meta":{"required":true}},"recordType":{"type":"text","required":false},"recordId":{"type":"text","required":false},"reason":{"type":"text","required":true,"_meta":{"required":true}},"expiresAt":{"type":"datetime","required":false,"_meta":{"nullable":true}}},"methods":{},"decoratorConfig":{"tableName":"participation_bans","api":false,"mcp":false,"cli":false,"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"ParticipationBan","collectionExportName":"ParticipationBanCollection","validationRules":[{"field":"actorUserId","rule":"required","fieldType":"text"},{"field":"reason","rule":"required","fieldType":"text"}],"schema":{"tableName":"participation_bans","ddl":"CREATE TABLE IF NOT EXISTS \\"participation_bans\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"actor_user_id\\" TEXT NOT NULL,\\n \\"record_type\\" TEXT,\\n \\"record_id\\" TEXT,\\n \\"reason\\" TEXT NOT NULL,\\n \\"expires_at\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"actor_user_id":{"type":"TEXT","notNull":true,"unique":false},"record_type":{"type":"TEXT","notNull":false,"unique":false},"record_id":{"type":"TEXT","notNull":false,"unique":false},"reason":{"type":"TEXT","notNull":true,"unique":false},"expires_at":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"participation_bans_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"participation_bans_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"27bbfb94"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:ParticipationPolicy":{"name":"participationpolicy","className":"ParticipationPolicy","qualifiedName":"@happyvertical/smrt-comments:ParticipationPolicy","collection":"participationpolicies","description":"A generic tenant default or exact-record override for participation actions.","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/ParticipationPolicy.ts","packageName":"@happyvertical/smrt-comments","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"recordType":{"type":"text","required":false},"recordId":{"type":"text","required":false},"action":{"type":"text","required":true,"_meta":{"required":true}},"moderationMode":{"type":"text","required":true,"default":"open","_meta":{"required":true},"enum":["open","pre_moderated","members_only","closed"]},"maxActions":{"type":"integer","required":true,"default":0,"_meta":{"required":true}},"windowSeconds":{"type":"integer","required":true,"default":0,"_meta":{"required":true}}},"methods":{},"decoratorConfig":{"tableName":"participation_policies","api":false,"mcp":false,"cli":false,"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"ParticipationPolicy","collectionExportName":"ParticipationPolicyCollection","validationRules":[{"field":"action","rule":"required","fieldType":"text"},{"field":"moderationMode","rule":"required","fieldType":"text"},{"field":"maxActions","rule":"required","fieldType":"integer"},{"field":"windowSeconds","rule":"required","fieldType":"integer"}],"schema":{"tableName":"participation_policies","ddl":"CREATE TABLE IF NOT EXISTS \\"participation_policies\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"record_type\\" TEXT,\\n \\"record_id\\" TEXT,\\n \\"action\\" TEXT NOT NULL,\\n \\"moderation_mode\\" TEXT NOT NULL DEFAULT 'open',\\n \\"max_actions\\" INTEGER NOT NULL DEFAULT 0,\\n \\"window_seconds\\" INTEGER NOT NULL DEFAULT 0\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"record_type":{"type":"TEXT","notNull":false,"unique":false},"record_id":{"type":"TEXT","notNull":false,"unique":false},"action":{"type":"TEXT","notNull":true,"unique":false},"moderation_mode":{"type":"TEXT","notNull":true,"unique":false,"default":"open"},"max_actions":{"type":"INTEGER","notNull":true,"unique":false,"default":0},"window_seconds":{"type":"INTEGER","notNull":true,"unique":false,"default":0}},"indexes":[{"name":"participation_policies_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"participation_policies_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"7f2fdd9d"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:ParticipationRateBucket":{"name":"participationratebucket","className":"ParticipationRateBucket","qualifiedName":"@happyvertical/smrt-comments:ParticipationRateBucket","collection":"participationratebuckets","description":"Service-owned fixed-window counter. Never delete a currently active bucket.","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/ParticipationRateBucket.ts","packageName":"@happyvertical/smrt-comments","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"bucketKey":{"type":"text","required":true,"_meta":{"required":true}},"actionCount":{"type":"integer","required":true,"default":0,"_meta":{"required":true}}},"methods":{},"decoratorConfig":{"tableName":"participation_rate_buckets","api":false,"mcp":false,"cli":false,"indexes":[{"name":"participation_rate_bucket_unique","columns":["tenantId","bucketKey"],"unique":true}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"ParticipationRateBucket","collectionExportName":"ParticipationRateBucketCollection","validationRules":[{"field":"bucketKey","rule":"required","fieldType":"text"},{"field":"actionCount","rule":"required","fieldType":"integer"}],"schema":{"tableName":"participation_rate_buckets","ddl":"CREATE TABLE IF NOT EXISTS \\"participation_rate_buckets\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"bucket_key\\" TEXT NOT NULL,\\n \\"action_count\\" INTEGER NOT NULL DEFAULT 0\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"bucket_key":{"type":"TEXT","notNull":true,"unique":false},"action_count":{"type":"INTEGER","notNull":true,"unique":false,"default":0}},"indexes":[{"name":"participation_rate_buckets_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"participation_rate_bucket_unique","columns":["tenant_id","bucket_key"],"unique":true},{"name":"participation_rate_buckets_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"9f3707e0"},"importPath":"@happyvertical/smrt-comments"}},"moduleType":"smrt","recipes":[{"id":"comments.records","className":"RecordCommentsRecipe","label":"Record comments","summary":"Discuss work directly on the records it concerns.","synonyms":["comments","mentions","discussion"],"models":["@happyvertical/smrt-comments:Comment","@happyvertical/smrt-comments:CommentRevision","@happyvertical/smrt-comments:CommentReport","@happyvertical/smrt-comments:ParticipationPolicy","@happyvertical/smrt-comments:ParticipationBan","@happyvertical/smrt-comments:ParticipationRateBucket"],"nav":[],"requires":[],"group":{"id":"comments","label":"Comments","summary":"Keep discussions alongside the records your team works on."},"help":{"markdown":"## Overview\\n\\nRecord comments keep a discussion next to the work it concerns. Open a record\\nyou can access to read its discussion or add a comment. Comments are part of\\nthe record page, rather than a separate list of every discussion.\\n\\n## Tasks\\n\\n### Add a comment\\n\\nOpen the record, find Comments, write your message and choose Post comment.\\nYour app controls who can read the record and who can contribute to its discussion.\\n\\n### Mention someone\\n\\nIf your app provides mention selection, choose a teammate who can access the\\nrecord. When mention notifications are connected, that person receives a\\nnotification. Typing a name alone does not select a recipient.\\n\\n### Check a failed post\\n\\nIf posting reports an error, refresh the discussion before trying again. A\\ncomment may have been saved even when its mention notification could not be\\ndelivered. If it appears in the discussion, do not post it a second time.\\n","fieldRefs":[]},"surfaces":[{"kind":"playground","export":"@happyvertical/smrt-comments/svelte#RecordCommentsPlayground","label":"Record discussion preview"}],"runtime":"both","demoSeed":{"export":"@happyvertical/smrt-comments/svelte#recordCommentsDemo"},"demo":{"mode":"server","reasons":["Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."]}}],"smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-messages","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3624"],"via":["@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
5
+ //#endregion
6
+ //#region src/models/ParticipationBan.ts
7
+ var __defProp$2 = Object.defineProperty;
8
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
9
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value
14
+ }) : obj[key] = value;
15
+ var __decorateClass$2 = (decorators, target, key, kind) => {
16
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
17
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
18
+ if (kind && result) __defProp$2(target, key, result);
19
+ return result;
20
+ };
21
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+ var ParticipationBan = class extends SmrtObject {
23
+ tenantId = "";
24
+ actorUserId = "";
25
+ recordType = "";
26
+ recordId = "";
27
+ reason = "";
28
+ expiresAt = null;
29
+ async validateBeforeSave() {
30
+ for (const identity of [
31
+ this.tenantId,
32
+ this.actorUserId,
33
+ this.recordId
34
+ ]) if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identity) && identity !== identity.toLowerCase()) throw new TypeError("Participation administrative UUIDs must be lowercase.");
35
+ if (Boolean(this.recordType) !== Boolean(this.recordId)) throw new TypeError("Participation target requires both recordType and recordId.");
36
+ await super.validateBeforeSave();
37
+ }
38
+ };
39
+ __publicField$2(ParticipationBan, "__smrtPackage__", "@happyvertical/smrt-comments");
40
+ __decorateClass$2([tenantId()], ParticipationBan.prototype, "tenantId", 2);
41
+ __decorateClass$2([field({ required: true })], ParticipationBan.prototype, "actorUserId", 2);
42
+ __decorateClass$2([field()], ParticipationBan.prototype, "recordType", 2);
43
+ __decorateClass$2([field()], ParticipationBan.prototype, "recordId", 2);
44
+ __decorateClass$2([field({ required: true })], ParticipationBan.prototype, "reason", 2);
45
+ __decorateClass$2([field({
46
+ type: "datetime",
47
+ nullable: true
48
+ })], ParticipationBan.prototype, "expiresAt", 2);
49
+ ParticipationBan = __decorateClass$2([TenantScoped({ mode: "required" }), smrt({
50
+ tableName: "participation_bans",
51
+ api: false,
52
+ mcp: false,
53
+ cli: false
54
+ })], ParticipationBan);
55
+ //#endregion
56
+ //#region src/models/ParticipationPolicy.ts
57
+ var __defProp$1 = Object.defineProperty;
58
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
59
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value
64
+ }) : obj[key] = value;
65
+ var __decorateClass$1 = (decorators, target, key, kind) => {
66
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
67
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
68
+ if (kind && result) __defProp$1(target, key, result);
69
+ return result;
70
+ };
71
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
72
+ var ParticipationPolicy = class extends SmrtObject {
73
+ tenantId = "";
74
+ recordType = "";
75
+ recordId = "";
76
+ action = "comment";
77
+ moderationMode = "open";
78
+ maxActions = 0;
79
+ windowSeconds = 0;
80
+ async validateBeforeSave() {
81
+ for (const identity of [this.tenantId, this.recordId]) if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identity) && identity !== identity.toLowerCase()) throw new TypeError("Participation administrative UUIDs must be lowercase.");
82
+ if (Boolean(this.recordType) !== Boolean(this.recordId)) throw new TypeError("Participation target requires both recordType and recordId.");
83
+ if (![
84
+ "comment",
85
+ "react",
86
+ "vote",
87
+ "report"
88
+ ].includes(this.action) || ![
89
+ "open",
90
+ "pre_moderated",
91
+ "members_only",
92
+ "closed"
93
+ ].includes(this.moderationMode) || !Number.isSafeInteger(this.maxActions) || this.maxActions < 0 || !Number.isSafeInteger(this.windowSeconds) || this.windowSeconds < 0 || this.maxActions > 0 && this.windowSeconds === 0) throw new TypeError("Invalid participation policy.");
94
+ await super.validateBeforeSave();
95
+ }
96
+ };
97
+ __publicField$1(ParticipationPolicy, "__smrtPackage__", "@happyvertical/smrt-comments");
98
+ __decorateClass$1([tenantId()], ParticipationPolicy.prototype, "tenantId", 2);
99
+ __decorateClass$1([field()], ParticipationPolicy.prototype, "recordType", 2);
100
+ __decorateClass$1([field()], ParticipationPolicy.prototype, "recordId", 2);
101
+ __decorateClass$1([field({ required: true })], ParticipationPolicy.prototype, "action", 2);
102
+ __decorateClass$1([field({ required: true })], ParticipationPolicy.prototype, "moderationMode", 2);
103
+ __decorateClass$1([field({ required: true })], ParticipationPolicy.prototype, "maxActions", 2);
104
+ __decorateClass$1([field({ required: true })], ParticipationPolicy.prototype, "windowSeconds", 2);
105
+ ParticipationPolicy = __decorateClass$1([TenantScoped({ mode: "required" }), smrt({
106
+ tableName: "participation_policies",
107
+ api: false,
108
+ mcp: false,
109
+ cli: false
110
+ })], ParticipationPolicy);
111
+ //#endregion
112
+ //#region src/models/ParticipationRateBucket.ts
113
+ var __defProp = Object.defineProperty;
114
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
115
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
116
+ enumerable: true,
117
+ configurable: true,
118
+ writable: true,
119
+ value
120
+ }) : obj[key] = value;
121
+ var __decorateClass = (decorators, target, key, kind) => {
122
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
123
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
124
+ if (kind && result) __defProp(target, key, result);
125
+ return result;
126
+ };
127
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
128
+ var ParticipationRateBucket = class extends SmrtObject {
129
+ tenantId = "";
130
+ bucketKey = "";
131
+ actionCount = 0;
132
+ };
133
+ __publicField(ParticipationRateBucket, "__smrtPackage__", "@happyvertical/smrt-comments");
134
+ __decorateClass([tenantId()], ParticipationRateBucket.prototype, "tenantId", 2);
135
+ __decorateClass([field({ required: true })], ParticipationRateBucket.prototype, "bucketKey", 2);
136
+ __decorateClass([field({ required: true })], ParticipationRateBucket.prototype, "actionCount", 2);
137
+ ParticipationRateBucket = __decorateClass([TenantScoped({ mode: "required" }), smrt({
138
+ tableName: "participation_rate_buckets",
139
+ api: false,
140
+ mcp: false,
141
+ cli: false,
142
+ indexes: [{
143
+ name: "participation_rate_bucket_unique",
144
+ columns: ["tenantId", "bucketKey"],
145
+ unique: true
146
+ }]
147
+ })], ParticipationRateBucket);
148
+ //#endregion
149
+ //#region src/collections/ParticipationCollection.ts
150
+ var ParticipationPolicyCollection = class extends SmrtCollection {
151
+ static _itemClass = ParticipationPolicy;
152
+ };
153
+ var ParticipationBanCollection = class extends SmrtCollection {
154
+ static _itemClass = ParticipationBan;
155
+ };
156
+ var ParticipationRateBucketCollection = class extends SmrtCollection {
157
+ static _itemClass = ParticipationRateBucket;
158
+ };
159
+ //#endregion
160
+ //#region src/participation-contract.ts
161
+ var ParticipationDeniedError = class extends Error {
162
+ constructor(reason) {
163
+ super(reason ? `Participation denied: ${reason}` : "Participation denied.");
164
+ this.reason = reason;
165
+ this.name = "ParticipationDeniedError";
166
+ }
167
+ reason;
168
+ };
169
+ //#endregion
170
+ //#region src/services/ParticipationService.ts
171
+ var canonical = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value.toLowerCase() : value;
172
+ var ParticipationService = class {
173
+ constructor(options) {
174
+ this.options = options;
175
+ }
176
+ options;
177
+ normalize(request) {
178
+ if (!request || ![
179
+ "comment",
180
+ "react",
181
+ "vote",
182
+ "report"
183
+ ].includes(request.action) || [
184
+ request.tenantId,
185
+ request.actorUserId,
186
+ request.recordType,
187
+ request.recordId
188
+ ].some((value) => typeof value !== "string" || !value.trim())) throw new TypeError("Invalid participation request.");
189
+ return {
190
+ ...request,
191
+ tenantId: canonical(request.tenantId),
192
+ actorUserId: canonical(request.actorUserId),
193
+ recordId: canonical(request.recordId)
194
+ };
195
+ }
196
+ async evaluate(request, db) {
197
+ const now = (this.options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
198
+ if (!Number.isFinite(now)) throw new TypeError("Invalid participation clock.");
199
+ const rows = await (await ParticipationPolicyCollection.create({ db })).list({ where: {
200
+ tenantId: request.tenantId,
201
+ action: request.action
202
+ } });
203
+ const exact = rows.filter((p) => p.recordType === request.recordType && p.recordId === request.recordId);
204
+ const defaults = rows.filter((p) => p.recordType === "" && p.recordId === "");
205
+ if (exact.length > 1 || defaults.length > 1) throw new ParticipationDeniedError("Ambiguous participation policy.");
206
+ const policy = exact[0] ?? defaults[0];
207
+ if (policy && (![
208
+ "open",
209
+ "pre_moderated",
210
+ "members_only",
211
+ "closed"
212
+ ].includes(policy.moderationMode) || !Number.isSafeInteger(policy.maxActions) || policy.maxActions < 0 || !Number.isSafeInteger(policy.windowSeconds) || policy.windowSeconds < 0 || policy.maxActions > 0 && policy.windowSeconds < 1)) throw new ParticipationDeniedError("Invalid participation policy.");
213
+ if (policy?.moderationMode === "closed") throw new ParticipationDeniedError("Participation is closed.");
214
+ const bans = await ParticipationBanCollection.create({ db });
215
+ for (const ban of await bans.list({ where: {
216
+ tenantId: request.tenantId,
217
+ actorUserId: request.actorUserId
218
+ } })) if (ban.recordType === "" && ban.recordId === "" || ban.recordType === request.recordType && ban.recordId === request.recordId) {
219
+ const expiry = ban.expiresAt === null ? null : new Date(ban.expiresAt).getTime();
220
+ if (expiry === null || !Number.isFinite(expiry) || expiry > now) throw new ParticipationDeniedError("Actor is banned.");
221
+ }
222
+ const decision = this.options.entitlement ? await this.options.entitlement.authorize(request) : { allowed: true };
223
+ if (decision?.allowed !== true || policy?.moderationMode === "members_only" && decision.member !== true) throw new ParticipationDeniedError(decision?.reason ?? "Membership required.");
224
+ if (decision.initialCommentStatus !== void 0 && !["visible", "pending"].includes(decision.initialCommentStatus)) throw new ParticipationDeniedError("Invalid moderation decision.");
225
+ return {
226
+ policy,
227
+ now,
228
+ decision: {
229
+ ...decision,
230
+ initialCommentStatus: policy?.moderationMode === "pre_moderated" ? "pending" : decision.initialCommentStatus
231
+ }
232
+ };
233
+ }
234
+ /** Checks current permission without consuming quota (including keyed replays). */
235
+ async authorize(input) {
236
+ const request = this.normalize(input);
237
+ return withTenant({ tenantId: request.tenantId }, async () => (await this.evaluate(request, this.options.db)).decision);
238
+ }
239
+ async reserve(db, request, policy, now) {
240
+ if (!policy?.maxActions) return;
241
+ const buckets = await ParticipationRateBucketCollection.create({ db });
242
+ const bucketKey = digestRunOnceContent({
243
+ actor: request.actorUserId,
244
+ action: request.action,
245
+ recordType: policy.recordType,
246
+ recordId: policy.recordId,
247
+ windowSeconds: policy.windowSeconds,
248
+ window: Math.floor(now / (policy.windowSeconds * 1e3))
249
+ });
250
+ const table = `"${buckets.tableName.replace(/"/g, "\"\"")}"`;
251
+ const id = crypto.randomUUID();
252
+ if ((await db.query(`INSERT INTO ${table} (id, slug, context, tenant_id, bucket_key, action_count) VALUES (?, ?, '', ?, ?, 1) ON CONFLICT (tenant_id, bucket_key) DO UPDATE SET action_count = ${table}.action_count + 1 WHERE ${table}.action_count < ? RETURNING action_count`, id, id, request.tenantId, bucketKey, policy.maxActions)).rows.length !== 1) throw new ParticipationDeniedError("Rate limit exceeded.");
253
+ }
254
+ /** Run one new mutation atomically with its reservation. Caller owns retry identity. */
255
+ async execute(input, work) {
256
+ const request = this.normalize(input);
257
+ return withTenant({ tenantId: request.tenantId }, async () => {
258
+ const collection = await ParticipationRateBucketCollection.create({ db: this.options.db });
259
+ if (!collection.db.transaction) throw new Error("Participation requires transactions.");
260
+ return collection.db.transaction((tx) => this.executeInTransaction(tx, request, work));
261
+ });
262
+ }
263
+ /** Compose with an existing caller-owned transaction; propagate failures to roll it back. */
264
+ async executeInTransaction(tx, input, work) {
265
+ const request = this.normalize(input);
266
+ return withTenant({ tenantId: request.tenantId }, async () => {
267
+ const { policy, now, decision } = await this.evaluate(request, tx);
268
+ await this.reserve(tx, request, policy, now);
269
+ return work(tx, decision);
270
+ });
271
+ }
272
+ };
273
+ //#endregion
274
+ export { ParticipationRateBucketCollection as a, ParticipationBan as c, ParticipationPolicyCollection as i, ParticipationDeniedError as n, ParticipationRateBucket as o, ParticipationBanCollection as r, ParticipationPolicy as s, ParticipationService as t };
275
+
276
+ //# sourceMappingURL=participation-LA_cfrM2.js.map