@voltro/plugin-chat 0.0.0-stage → 0.87.0
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/CHANGELOG.md +24329 -0
- package/LICENSE +61 -0
- package/README.md +516 -2
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +14931 -0
- package/dist/admissions.entity.js +11 -0
- package/dist/channels.entity.js +20 -0
- package/dist/chat-app/events/chat.mentioned.event.ts +1 -0
- package/dist/chat-app/events/chat.messageChanged.event.ts +1 -0
- package/dist/chat-app/events/chat.typing.event.ts +1 -0
- package/dist/chat-app/schedules/chat.retention.cron.tsx +16 -0
- package/dist/chat-app/subscribers/chat.messages.subscribe.ts +4 -0
- package/dist/components.js +432 -0
- package/dist/contract.js +92 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +21 -0
- package/dist/eventProducer.d.ts +18 -0
- package/dist/eventProducer.js +46 -0
- package/dist/events.d.ts +38 -0
- package/dist/events.js +36 -0
- package/dist/evidence.entity.js +25 -0
- package/dist/index.d.ts +623 -0
- package/dist/index.js +181 -0
- package/dist/inspect.js +38 -0
- package/dist/media.entity.js +15 -0
- package/dist/members.entity.js +22 -0
- package/dist/mentionDraft.js +21 -0
- package/dist/mentionNotifications.js +110 -0
- package/dist/messages.entity.js +28 -0
- package/dist/reportEvidence.js +16 -0
- package/dist/retention.entity.js +11 -0
- package/dist/rpc.d.ts +295 -0
- package/dist/rpc.js +267 -0
- package/dist/rpcRoutes.js +97 -0
- package/dist/server.d.ts +200 -0
- package/dist/server.js +7 -0
- package/dist/service.js +1412 -0
- package/dist/strategies.d.ts +148 -0
- package/dist/strategies.js +90 -0
- package/dist/testing.d.ts +139 -0
- package/dist/testing.js +111 -0
- package/dist/typing.js +88 -0
- package/dist/typingInvalidation.js +86 -0
- package/dist/typingLease.js +59 -0
- package/dist/typingPublisher.js +59 -0
- package/dist/useChatTypingPublisher.js +33 -0
- package/dist/useTyping.js +24 -0
- package/dist/web.d.ts +354 -0
- package/dist/web.js +19 -0
- package/package.json +98 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Voltro — Proprietary Software License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Voltro UG. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and all associated source code, documentation, and other
|
|
6
|
+
materials (the "Software") are the proprietary property of Voltro UG
|
|
7
|
+
("Voltro"). This is NOT open-source software.
|
|
8
|
+
|
|
9
|
+
Your access to and use of the Software is governed exclusively by the Voltro
|
|
10
|
+
Terms of Service at:
|
|
11
|
+
|
|
12
|
+
https://voltro.cloud/legal/terms
|
|
13
|
+
|
|
14
|
+
By downloading, installing, or using the Software you agree to those Terms.
|
|
15
|
+
Subject to and conditioned on your continued compliance with them, Voltro
|
|
16
|
+
grants you a limited, non-exclusive, non-transferable, non-sublicensable,
|
|
17
|
+
revocable license to use the Software. The license covers use by your
|
|
18
|
+
authorised named users only, up to the number of seats (or other billing metric)
|
|
19
|
+
your plan or order covers; each named user must be an identified individual
|
|
20
|
+
using their own personal account, and accounts, access credentials and seats
|
|
21
|
+
must not be shared. All rights not expressly granted in the
|
|
22
|
+
Terms are reserved by Voltro. If you do not agree to the Terms, you have no
|
|
23
|
+
license and may not use the Software.
|
|
24
|
+
|
|
25
|
+
The Terms of Service — including the scope of permitted use, plan
|
|
26
|
+
entitlements, fees, and pricing — may change from time to time. The version in
|
|
27
|
+
force is the one published at the URL above. Commercial terms and pricing are
|
|
28
|
+
NOT part of this notice and are not fixed by, or granted in perpetuity by, the
|
|
29
|
+
version of the Software in which this file appears.
|
|
30
|
+
|
|
31
|
+
Permitted use — your own software. Voltro is a development framework. You may
|
|
32
|
+
use the Software to build, run, and commercially distribute your own
|
|
33
|
+
applications and services, and to charge your own customers for them, without
|
|
34
|
+
owing Voltro any share of that revenue or any further permission beyond the
|
|
35
|
+
Terms. Applications you build with the Software are yours.
|
|
36
|
+
|
|
37
|
+
Prohibited use — reselling Voltro. You may NOT redistribute, resell,
|
|
38
|
+
sublicense, publish, or otherwise provide the Software itself — in whole or in
|
|
39
|
+
part, in source or compiled form — to third parties as a library, framework,
|
|
40
|
+
SDK, template, starter, or development tool for them to build upon, whether
|
|
41
|
+
standalone or repackaged. Shipping the Software as a non-separable, bundled
|
|
42
|
+
dependency inside an application you deliver (for example, compiled into your
|
|
43
|
+
deployed app) is permitted; extracting or re-exposing it as a reusable
|
|
44
|
+
component for others is not.
|
|
45
|
+
|
|
46
|
+
Except as expressly permitted above or by the Terms, you may not copy, modify,
|
|
47
|
+
merge, publish, sublicense, sell, distribute, or create derivative works of the
|
|
48
|
+
Software, in whole or in part.
|
|
49
|
+
|
|
50
|
+
Third-party open-source components distributed alongside the Software are the
|
|
51
|
+
property of their respective owners and are licensed under their own terms; see
|
|
52
|
+
THIRD-PARTY-NOTICES.md where provided. Nothing in this notice limits your
|
|
53
|
+
rights, or extends Voltro's, in those components.
|
|
54
|
+
|
|
55
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
56
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
57
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
58
|
+
VOLTRO BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM,
|
|
59
|
+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
|
60
|
+
|
|
61
|
+
For licensing inquiries, contact Voltro UG.
|
package/README.md
CHANGED
|
@@ -1,3 +1,517 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @voltro/plugin-chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Take
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { chatPlugin } from '@voltro/plugin-chat'
|
|
7
|
+
|
|
8
|
+
const plugin = chatPlugin()
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The factory contributes `_voltro_chat_channels`, `_voltro_chat_members` and
|
|
12
|
+
`_voltro_chat_messages` plus server-owned `_voltro_chat_retention` sweep progress, eighteen typed RPCs, seventeen client hooks, committed message
|
|
13
|
+
metadata events, configuration inspection and user/guest export/erase hooks.
|
|
14
|
+
SQL owns membership, history, slow-mode admission and read receipts. Verified
|
|
15
|
+
human/guest subjects use stable IDs; tenant and acting identity come from the
|
|
16
|
+
request. Anonymous and machine self calls are refused.
|
|
17
|
+
|
|
18
|
+
Public actions are `chat.setTyping`, `dm`, `send`, `edit`, `remove`, `markRead`, `mute` and
|
|
19
|
+
`configure`, `original`, `leave`, `transferOwnership`; queries are `chat.messages`, `channel`, `membership`, `channels`, `unread`. A message is an
|
|
20
|
+
intention with `channelId`, `requestId`, `body`, optional `replyToId`; it never
|
|
21
|
+
accepts an author, tenant or arbitrary stored row. The request ID deterministically
|
|
22
|
+
identifies the committed sender intention. An exact retry reads its SQL receipt
|
|
23
|
+
before consuming rates again; changed content with the same ID refuses `conflict`.
|
|
24
|
+
Edits/deletes require `baseVersion`; only the author edits and an author or
|
|
25
|
+
same-channel mod/owner deletes. Normal reads hide deleted bodies and server-only
|
|
26
|
+
original text. The role-gated original action is written; moderator history UI remains open.
|
|
27
|
+
|
|
28
|
+
## Configure
|
|
29
|
+
|
|
30
|
+
| Option | Default | Meaning |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `maxBodyLength` | 2,000 code points | Message input budget |
|
|
33
|
+
| `pageSize` | 50 | Maximum raw history rows per page |
|
|
34
|
+
| `maxMembers` | 500 | Server channel-creation member limit |
|
|
35
|
+
| `maxChannelsPerSubject` | 100 | Channel-list read budget, not global membership admission |
|
|
36
|
+
| `retentionMs` | 7 days | Default channel history window; null means no configured expiry |
|
|
37
|
+
| `perChannelRate` | 2/s | Sender/channel attempt budget |
|
|
38
|
+
| `globalRate` | 10/s | Sender attempt budget across channels |
|
|
39
|
+
| `maxMentions` | 20 | Resolved mention budget |
|
|
40
|
+
| `maxExportRows` | 10,000 | Complete bounded export; overflow refuses |
|
|
41
|
+
| `denyWords` | empty | Explicit keyword policy; empty is no denylist coverage |
|
|
42
|
+
| `moderationAction` | `block` | Refuse flagged content, or persist under `flag` |
|
|
43
|
+
| `strategies` | four defaults | Named replacement rule slots |
|
|
44
|
+
| `rateLimit` | Registry Redis | Existing limiter connection/HotStore options |
|
|
45
|
+
| `now` | `Date.now` | Clock for rules and observations |
|
|
46
|
+
|
|
47
|
+
These are configuration budgets, not measured throughput. The limiter reuses the
|
|
48
|
+
Registry-backed HotStore adapter from `plugin-ratelimit`; the app owns connections.
|
|
49
|
+
Missing activation, backend errors and fail-open evidence refuse sends rather
|
|
50
|
+
than silently allowing them. Explicit injected HotStore state refuses activation
|
|
51
|
+
when deployment evidence declares multiple nodes. Use the Registry Redis path
|
|
52
|
+
for such deployments. This plugin opens no independent Redis connection.
|
|
53
|
+
|
|
54
|
+
Channel `slowModeMs` and member `mutedUntil` are authoritative SQL values.
|
|
55
|
+
Send admission writes the channel fence before reading membership/time, serializing
|
|
56
|
+
same-channel writes across stores. Mod/owner roles control mute/configure; ordinary
|
|
57
|
+
members cannot freeze a channel or mute another person. This conservative SQL
|
|
58
|
+
serialization has not been measured under load.
|
|
59
|
+
|
|
60
|
+
## Hook
|
|
61
|
+
|
|
62
|
+
The shipped source directory discovers a guarded `chat.messageChanged` event and
|
|
63
|
+
post-commit producer. Its routing key is `channelId`; payload carries only
|
|
64
|
+
`messageId`, `version`, `change`. Text, author identity and media are absent.
|
|
65
|
+
The listener scope is `chat:events:read`, for operators, not an authorization
|
|
66
|
+
shortcut for players. The producer selects tenant from committed rows and
|
|
67
|
+
suppresses unchanged-version admission writes. CDC delivery is best-effort,
|
|
68
|
+
not a durable archive or business event.
|
|
69
|
+
|
|
70
|
+
Normal player messages use a live query over message, channel, membership and
|
|
71
|
+
Social-relation triggers. The handler verifies actual membership/tenant and
|
|
72
|
+
filters authors blocked in either direction through `SocialBlockPort` before
|
|
73
|
+
returning rows. Without a Social port there is no block policy to evaluate;
|
|
74
|
+
DM creation refuses without that port. `canMessage` admits an existing friend
|
|
75
|
+
or the recipient request policy, after block checks. Party/guild/lobby channels
|
|
76
|
+
must be provisioned by trusted server integrations.
|
|
77
|
+
|
|
78
|
+
`dataStoreChat(...).createChannel` permits only an internal system subject or a
|
|
79
|
+
same-tenant service account with `chat:channels:write`. Its input owns kind, ref,
|
|
80
|
+
name, members and roles. A cold create inserts transactionally; after a lost race
|
|
81
|
+
or reply, only the exact SQL creation claim matches. Direct DM intentions construct
|
|
82
|
+
one canonical pair channel after recipient admission. Retention uses bounded persisted sweep steps through `ChatAdministration.prune`
|
|
83
|
+
and the contributed `chat.pruneHistory` schedule; details below. Install `plugin-audit` with its durable sink to audit
|
|
84
|
+
RPC actions. Chat does not claim to have built an independent audit trail.
|
|
85
|
+
|
|
86
|
+
## Channel lifecycle and match archive
|
|
87
|
+
|
|
88
|
+
This additional source is built. `ChatAdministration` in
|
|
89
|
+
`@voltro/plugin-chat/server` is provided by the installing plugin's normal
|
|
90
|
+
service Layer. It borrows the actual bound SQL store; no independent connection
|
|
91
|
+
is opened. `dataStoreChat` exposes the same administration methods for explicit
|
|
92
|
+
server adapters.
|
|
93
|
+
|
|
94
|
+
`addMember` and `removeMember` accept only an internal system identity or a
|
|
95
|
+
same-tenant service account holding `chat:members:write`. Their inputs contain
|
|
96
|
+
`tenantId`, `channelId`, `subjectId`; adding also requires `role: 'member' | 'mod'`.
|
|
97
|
+
The transaction acquires the channel writer fence before reading the roster cap.
|
|
98
|
+
An exact existing role is a no-op; role replacement refuses `conflict`. Adding
|
|
99
|
+
an owner is not allowed. DM membership cannot change; archived match rosters do
|
|
100
|
+
not admit new members. Existing owners cannot be removed through this operation.
|
|
101
|
+
The global `maxChannelsPerSubject` option remains a read budget; it does not
|
|
102
|
+
claim to enforce concurrent admission across different channels.
|
|
103
|
+
|
|
104
|
+
`chat.leave({ channelId })` uses the authenticated human/guest identity; it is
|
|
105
|
+
repeatable after removal. Owners must first call
|
|
106
|
+
`chat.transferOwnership({ channelId, targetId })`, which requires an existing
|
|
107
|
+
unmuted member and exactly one current owner. Demotion and promotion share the
|
|
108
|
+
same SQL transaction. DM leave/transfer refuses. `useChatLeave` and
|
|
109
|
+
`useChatTransferOwnership` expose these intentions through the existing mutation
|
|
110
|
+
client. Subsequent channel/history/send calls re-check membership. This source
|
|
111
|
+
has not established revocation of already buffered queries or active event streams.
|
|
112
|
+
|
|
113
|
+
`archiveMatch(subject, input)` requires a system identity or a same-tenant service
|
|
114
|
+
account holding `chat:archive:write`. Only trusted Room disposal/server integration
|
|
115
|
+
may supply `tenantId`, `matchId`, `name`, `members`, and `messages` containing
|
|
116
|
+
`id`, `authorId`, `body`, `at`, plus optional `retentionMs`. A browser cannot call
|
|
117
|
+
this operation. The transcript is detached at invocation, validates every author
|
|
118
|
+
against the supplied server roster and refuses future timestamps, duplicate IDs
|
|
119
|
+
and configured budgets. It uses `maxExportRows` as the transcript entry budget;
|
|
120
|
+
that number is not measured archive capacity. Every cold entry passes the actual
|
|
121
|
+
moderation strategy before any SQL write. Archived text never enters a Room tick.
|
|
122
|
+
|
|
123
|
+
Channel, memberships and messages commit together; the resulting match channel
|
|
124
|
+
starts frozen. A deterministic complete-transcript claim and per-message SQL
|
|
125
|
+
receipts admit exact retries, including different roster/message input order.
|
|
126
|
+
An existing exact archive is read before calling moderation again. Changed input,
|
|
127
|
+
conflicting IDs or incomplete/erased/retained-out receipts refuse rather than
|
|
128
|
+
recreate history. An insert failure rolls back the whole new archive. These
|
|
129
|
+
transaction and successor expectations are authored for all six native dialects,
|
|
130
|
+
with memory and installed-Layer controls; none has run. This is not replay,
|
|
131
|
+
report-evidence ingestion, ordinary boot or a process-restart proof.
|
|
132
|
+
|
|
133
|
+
Private `chat.typing`, listener membership revocation, Party/Guild automatic
|
|
134
|
+
synchronization and Room-disposal wiring remain open. The existing operator
|
|
135
|
+
metadata event is not a substitute for private player delivery.
|
|
136
|
+
|
|
137
|
+
## Override
|
|
138
|
+
|
|
139
|
+
`./strategies` exports each single default:
|
|
140
|
+
`keywordChatModeration` (`moderate`), `fixedChatRateBudgets` (`rateLimit`),
|
|
141
|
+
`noResolvedChatMentions` (`mentionResolver`), `channelChatRetention` (`retention`).
|
|
142
|
+
Moderation composes the existing `plugin-moderation` keyword provider before SQL.
|
|
143
|
+
A custom Effect may reject, flag or replace text within the input budget; provider
|
|
144
|
+
failures become an unavailable refusal. The empty default denylist performs no
|
|
145
|
+
profanity classification. AI/image/provider coverage is not implied.
|
|
146
|
+
|
|
147
|
+
A rate slot changes budgets, not the HotStore counter authority. Mention slots
|
|
148
|
+
resolve trusted subject IDs outside SQL, then membership/block validation occurs
|
|
149
|
+
before storing refs. The default resolves nothing; text is not treated as an
|
|
150
|
+
identity proof. Edited text clears earlier mention refs. Retention slots return
|
|
151
|
+
a bounded duration or null. The configuration panel identifies custom/default slots.
|
|
152
|
+
|
|
153
|
+
## Extend or replace
|
|
154
|
+
|
|
155
|
+
Chat is a leaf under the system contract: it supplies events and strategies,
|
|
156
|
+
not its own replacement port. Dependencies on Social use `SocialBlockPort`,
|
|
157
|
+
including a studio's alternate Layer. The three server table handles are exported;
|
|
158
|
+
JSON `metadata` fields persist small extensions and exports include actual row
|
|
159
|
+
columns. Static wire schemas do not yet propagate arbitrary mixin columns.
|
|
160
|
+
Complete S4a propagation, custom-column UI and the system-removal command remain open.
|
|
161
|
+
|
|
162
|
+
In-match chat belongs to a Room channel and never this SQL history path.
|
|
163
|
+
The trusted archive adapter is written below. Storage/scan/media variants,
|
|
164
|
+
sanctions reports, search, automatic party/guild membership synchronization and
|
|
165
|
+
retention scheduling remain to integrate.
|
|
166
|
+
No store read/write belongs in a Room tick.
|
|
167
|
+
|
|
168
|
+
## Panel and GM
|
|
169
|
+
|
|
170
|
+
`GET /status`, shared `ChatPage`, Local HTTP provider and Cloud authorized-app/
|
|
171
|
+
replica proxy are the four authored layers. They report dated configured budgets,
|
|
172
|
+
backend activation and binding, with no message/author text and no health/capacity
|
|
173
|
+
claim. Channel views, rate/flag metrics, moderator history and inspect write controls
|
|
174
|
+
remain to build; mute and freeze RPC admission is already authored.
|
|
175
|
+
|
|
176
|
+
`./web` exports `useChannel`, `useChannelList`, `useChatMessages`, `useChatSend`,
|
|
177
|
+
`useChatEdit`, `useChatRemove`. Pages use stable `(createdAt,id)` cursors in reverse
|
|
178
|
+
order. A blocked raw page can return fewer visible rows while retaining its cursor.
|
|
179
|
+
Typing delivery, actual server author projections, emote/media rendering, mentions completion, reports integration and moderator history UI remain open. Written cursor/components are described below.
|
|
180
|
+
|
|
181
|
+
## Tests
|
|
182
|
+
|
|
183
|
+
They encode guest identity, exact retry after rate exhaustion, changed intent
|
|
184
|
+
refusal, ownership, mute/slow mode, moderation, block- filtered server delivery,
|
|
185
|
+
stable timestamp pagination and erase/retry. Native
|
|
186
|
+
PostgreSQL/MySQL/MariaDB/MSSQL/SQLite/Turso cases exercise all three tables,
|
|
187
|
+
Unicode/JSON/date fields, two independent pool slow-mode and version admission,
|
|
188
|
+
successor readback and privacy. Missing native connection settings fail; they do
|
|
189
|
+
not silently skip acceptance. Four strategy fixtures and real generic
|
|
190
|
+
`systemContractConformance` registration cover declared exports, metadata
|
|
191
|
+
extension, events, table privacy and the inspect handler. Panel/proxy fixtures
|
|
192
|
+
are also written.
|
|
193
|
+
|
|
194
|
+
None ran.
|
|
195
|
+
|
|
196
|
+
## Privacy and limits
|
|
197
|
+
|
|
198
|
+
Export includes own memberships, authored messages including original content,
|
|
199
|
+
own evidence captures, the identity admission row and channels where the target
|
|
200
|
+
has owner membership. Erase removes membership, anonymizes/redacts authored
|
|
201
|
+
message content/refs, removes the subject from structured `payload.mentions` of
|
|
202
|
+
peer messages (peer text stays), clears owned channel names/metadata and
|
|
203
|
+
preserves peer history. User/guest stable identity and retry are encoded by
|
|
204
|
+
fixtures. Owner succession, media removal, volatile counters/event erasure and
|
|
205
|
+
cross-system privacy remain to finish.
|
|
206
|
+
|
|
207
|
+
### Read cursor and components
|
|
208
|
+
|
|
209
|
+
SQL read receipts store (lastReadAt,lastReadId) derived from a real channel
|
|
210
|
+
message. Older acknowledgements never regress, including equal timestamps.
|
|
211
|
+
The chat.unread live query excludes self-authored, deleted, expired and blocked
|
|
212
|
+
messages. Each channel scans at most maxExportRows raw candidates;
|
|
213
|
+
truncated:true means an incomplete count. Membership lists retain the channel
|
|
214
|
+
read budget. chat.original is an action restricted to same-channel mod/owner
|
|
215
|
+
membership, returning retained original text. It is no general player history.
|
|
216
|
+
|
|
217
|
+
The web entry additionally exports useUnread, useChatMarkRead and useChatOriginal.
|
|
218
|
+
ChatPanel, variable-height windowed MessageList, controlled Composer and
|
|
219
|
+
ChannelTabs reuse ui-shadcn with en/de labels, escaped text, unknown/deleted
|
|
220
|
+
identity, report-ID callbacks, character budget and loading/error/empty states.
|
|
221
|
+
The parent owns trusted Players projections, draft, request ID and uncertain
|
|
222
|
+
pending state. Credential/tenant session scopeKey and matching dataScopeKey fence
|
|
223
|
+
old data and draft/actions. Nothing auto-replays or clears after submission.
|
|
224
|
+
Focused rows remain mounted; semantic row positions and a keyboard-scrollable
|
|
225
|
+
viewport are provided. History updates do not auto-scroll the reader. Callers
|
|
226
|
+
bound retained page caches. No visual/accessibility performance is measured.
|
|
227
|
+
|
|
228
|
+
Typing, actual server author joins, emote/media rendering, mentions completion,
|
|
229
|
+
reports integration and moderator history UI remain open. Native
|
|
230
|
+
cursor/successor cases cover all six dialects. None ran; no acceptance evidence
|
|
231
|
+
is created by this addition.
|
|
232
|
+
|
|
233
|
+
## Sanctions admission
|
|
234
|
+
|
|
235
|
+
When installed, Chat consults SanctionsPort for the exact channel before new
|
|
236
|
+
sends and edits. A denied decision returns sanctioned; unavailable enforcement
|
|
237
|
+
refuses the write. An already committed exact send retry returns its receipt;
|
|
238
|
+
deletes remain allowed by normal ownership/moderation. No sanctions table is
|
|
239
|
+
read directly by Chat.
|
|
240
|
+
|
|
241
|
+
Production activation now source-checks the actual deployment-wide plugin grant.
|
|
242
|
+
Declare actual Licensing first; development/test remains free. See the bilingual
|
|
243
|
+
Licensing contract and manual update migration.
|
|
244
|
+
|
|
245
|
+
`chat.membership({ channelId })` and `useChatMembership` read only the authenticated request subject’s own SQL membership. Channel and membership are checked in one transaction with exact tenant/identity predicates; role, mute and own read cursor are authoritative values. Foreign identity fields are not input. Membership and channel changes are query sources. A successful snapshot does not replace server authorization of the next action or prove immediate revocation of buffered data.
|
|
246
|
+
|
|
247
|
+
The actual membership supplies `subjectId`; active hints expire after five seconds. Typing uses separate HotStore rate keys with configured channel/global budgets, writes no SQL rows and does not belong in Room ticks. Missing publisher refuses. The installed private policy checks membership at attach, then current memberships, sender state, expiry and Social blocking before each payload. Blocked/expired hints are withheld server-side; revoked recipient membership ends the stream on its next delivery.
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
`useTyping(channelId | null, apiName?)` returns `{ channelId, subjects, status }`. It consumes the private event through the existing event hook, keeps at most 128 subject IDs and uses a single monotonic timer. A hint lives for at most five local seconds and never beyond its remaining server expiry at receipt. Stop hints remove a peer; gaps, non-live state, channel/API/transport changes and unmount clear the display. Future wall-clock skew cannot create an indefinite lease. The hook does not publish hints or trust display names from events; resolve names from authorized player projections.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
`ChatPanel` accepts the `useTyping` result as `typing` and checks its channel and live status before displaying it. `TypingIndicator` resolves a single name only from the supplied authorized `authors`; unknown identities use “Someone is typing”, multiple peers use a bounded count. `chatTypingLabels` supplies English/German copy. The status region stays present without moving keyboard focus. `Composer.onTyping` reports actual editable input and stops on blur, submit, empty draft, disable and unmount.
|
|
254
|
+
|
|
255
|
+
Connect that callback to `useChatTypingPublisher(channelId | null, apiName?).activity`. This separate ephemeral sender captures the actual transport, has one shared in-flight slot across channel changes, a two-second active-hint interval and a one-second interruptible request timeout. It has no active queue or automatic replay; one explicit stop may follow an in-flight active hint. Channel/subject/unmount cleanup aborts the request and keeps the slot occupied through its actual finalizer. It exposes `pending` and `error`; a typing failure does not resend a message.
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
Private event policies can provide a scoped `monitor`. Registration happens before admission; its completion or failure ends the stream with a redacted access error and releases the bus subscription. The callback owns no detached worker. Chat uses one shared DataStore change listener while any typing monitors exist, capped at 4096 monitors per adapter. Relevant channel/membership/block changes and reported change-stream gaps invalidate subscriptions even when no typing payload arrives. Proven unchanged read receipts and foreign-tenant changes are ignored; incomplete row images invalidate conservatively. This uses the existing external-change injection path and performs no polling. Payload visibility checks remain in place.
|
|
259
|
+
|
|
260
|
+
## Authorized author names
|
|
261
|
+
|
|
262
|
+
`chat.authors({ channelId, subjectIds })` and `useChatAuthors(channelId, subjectIds)`
|
|
263
|
+
resolve at most 128 distinct subject IDs through the installed `PlayersPort`.
|
|
264
|
+
The requester and each returned identity must be current members of this exact
|
|
265
|
+
channel/tenant; Social blocks are checked in both directions. Authorization is
|
|
266
|
+
rechecked after asynchronous identity resolution. This is a bounded roster
|
|
267
|
+
projection, not public profile access: it returns only subject ID, the Players
|
|
268
|
+
roster display name (already including `#Tag`) and an avatar storage reference.
|
|
269
|
+
Missing Players/profile, former members and blocked peers produce no identity.
|
|
270
|
+
The UI keeps its unknown-author fallback for retained messages from former members.
|
|
271
|
+
|
|
272
|
+
Pass the result as `authors` to ChatPanel/MessageList/TypingIndicator only with
|
|
273
|
+
matching channel and current subject scope. `displayName` is the complete roster
|
|
274
|
+
name; do not append another tag. Names render as text; avatar references are not
|
|
275
|
+
minted URLs. Profile, member, channel and Social changes are live-query sources.
|
|
276
|
+
No names travel in typing payloads.
|
|
277
|
+
|
|
278
|
+
## Mentions from Players
|
|
279
|
+
|
|
280
|
+
The default `playerChatMentions` strategy resolves bounded `@Name#1234` tokens
|
|
281
|
+
through `PlayersPort.resolveName`. Names may contain spaces; tokens start at the
|
|
282
|
+
beginning of text or after whitespace/opening punctuation. Repeated resolved
|
|
283
|
+
subjects collapse to one ID. `maxMentions` bounds distinct lookup intentions before
|
|
284
|
+
any Players call. Missing Players/unknown names produce no mention; unavailable
|
|
285
|
+
providers refuse the operation. Chat verifies sender membership before resolving
|
|
286
|
+
names, and every target's actual channel membership and Social blocks inside send
|
|
287
|
+
admission. Client text cannot supply an authoritative subject ID. A rejected
|
|
288
|
+
nonmember/blocked mention refuses the send; it is not silently delivered.
|
|
289
|
+
|
|
290
|
+
`chat.mentionCandidates({ channelId, after? })` and
|
|
291
|
+
`useChatMentionCandidates(channelId, after?)` return `{ authors, after }`. Each
|
|
292
|
+
page scans at most 129 roster rows and resolves at most 128 identities through
|
|
293
|
+
the same authorized author projection. Use `after` even if filtering leaves
|
|
294
|
+
`authors` empty. There is no whole-roster preload or unbounded prefix scan.
|
|
295
|
+
Live-query sources include profiles, members, channel and Social relations. This
|
|
296
|
+
is implemented; committed mention notifications remain unfinished integration
|
|
297
|
+
work.
|
|
298
|
+
|
|
299
|
+
## Composer mention suggestions
|
|
300
|
+
|
|
301
|
+
Pass a current authorized candidate page as `mentionCandidates` to Composer or
|
|
302
|
+
ChatPanel. After an `@` token, up to eight matching names from at most 128 input
|
|
303
|
+
candidates appear as ordinary focusable buttons. Arrow Down moves from the draft
|
|
304
|
+
to the first choice, Tab traverses controls, and Escape closes suggestions.
|
|
305
|
+
Selection inserts the full roster label as text, retains the suffix and restores
|
|
306
|
+
the textarea cursor. It never sends or clears the draft. Selection ranges, email
|
|
307
|
+
substrings, mid-token cursors and edits exceeding the message budget are refused.
|
|
308
|
+
|
|
309
|
+
`mentionsLoading` hides stale candidates. `onLoadMoreMentions` exposes an
|
|
310
|
+
explicit next-page button, including empty filtered pages; the parent owns
|
|
311
|
+
paging and must replace candidates on channel/identity changes. ChatPanel's
|
|
312
|
+
existing scope gate hides suggestions and callbacks while data belongs to
|
|
313
|
+
another subject. Disabled or pending input exposes no suggestions.
|
|
314
|
+
`chatMentionLabels` provides en/de copy.
|
|
315
|
+
|
|
316
|
+
## Committed mention metadata and delivery admission
|
|
317
|
+
|
|
318
|
+
`chat.mentioned` is an operator-scoped event (`chat:events:read`), keyed by channel
|
|
319
|
+
and recipient subject, carrying only the message ID. The existing committed SQL
|
|
320
|
+
subscriber publishes it for inserted mention recipients (max100), deduplicates
|
|
321
|
+
recipients and skips the sender. Edits/deletes do not resend mentions. This is
|
|
322
|
+
best-effort CDC, not a durable notification or an exactly-once delivery promise.
|
|
323
|
+
Consumers must deduplicate by tenant/message/recipient if necessary.
|
|
324
|
+
|
|
325
|
+
`ChatAdministration.canNotifyMention(subject, { tenantId, channelId, messageId,
|
|
326
|
+
recipientId })` checks the actual retained message/mention, current channel
|
|
327
|
+
members, recipient notify flag and Social blocks. Only system or same-tenant
|
|
328
|
+
service account with `chat:notifications:send` may call it.
|
|
329
|
+
Missing/deleted/erased/expired state is ineligible; provider failures refuse.
|
|
330
|
+
Recheck at actual delivery, including after quiet-hours/digest deferral. No
|
|
331
|
+
transcript/name is needed in the queued payload.
|
|
332
|
+
|
|
333
|
+
## Optional mention notifications
|
|
334
|
+
|
|
335
|
+
`createChatMentionNotifications` from `@voltro/plugin-chat/server` composes the
|
|
336
|
+
existing ChatAdministration, Notifications dispatcher and Outbox. Supply a trusted
|
|
337
|
+
server `subject`, the owning app runtime's `run` executor, a getter for its actual
|
|
338
|
+
NotificationService, and `outboxForTenant` returning that app's existing facade
|
|
339
|
+
bound to the requested tenant. A missing/foreign durable tenant refuses delivery.
|
|
340
|
+
An optional `localeFor(recipientId, tenantId)` selects generic en/de copy; neither
|
|
341
|
+
message text nor author names enter the queued notification.
|
|
342
|
+
|
|
343
|
+
Register the returned `deliveryGuard` under `CHAT_MENTION_NOTIFICATION_GUARD` in
|
|
344
|
+
Notifications `deliveryGuards`. Export `subscriber` from an app `*.subscribe.ts`
|
|
345
|
+
and `outboxHandler` from an app `*.outbox.ts`. The subscriber interprets the same
|
|
346
|
+
committed recipients as `chat.mentioned`, checks current SQL admission and enqueues
|
|
347
|
+
through the existing facade. Explicit trusted consumers may call `enqueue(intent)`.
|
|
348
|
+
There is no automatic installation, independent connection, or fallback that skips
|
|
349
|
+
missing services. The bound app runtime must include ChatAdministration and the
|
|
350
|
+
same Social policy used by Chat.
|
|
351
|
+
|
|
352
|
+
The existing outbox key deduplicates pending sequential repeats; retries and
|
|
353
|
+
post-delivery repeats can still duplicate external effects. Subscriber delivery
|
|
354
|
+
is best-effort until enqueue, not atomic with the originating message commit.
|
|
355
|
+
The outbox handler rechecks admission, then uses the normal Notifications
|
|
356
|
+
renderer, preferences and delivery path. The persisted delivery guard checks
|
|
357
|
+
again before channels and after quiet/digest delays. Missing guards fail closed.
|
|
358
|
+
Failed channels fail the outbox attempt; already delivered channels can repeat
|
|
359
|
+
on retry. No exactly- once claim.
|
|
360
|
+
|
|
361
|
+
## Bounded retention schedule
|
|
362
|
+
|
|
363
|
+
The contributed `chat.pruneHistory` schedule runs every minute in UTC through the
|
|
364
|
+
normal scheduler (`onOverlap: skip`, `backfill: latest`). Each firing performs at
|
|
365
|
+
most ten `ChatAdministration.prune` steps. A step selects at most two channel IDs,
|
|
366
|
+
reads at most 201 expired message identities and deletes at most 200 messages from
|
|
367
|
+
one channel. It does not load message bodies. SQL owns `_voltro_chat_retention`
|
|
368
|
+
progress; the cursor and deletions commit together after writer/channel admission.
|
|
369
|
+
A busy channel advances to let other channels progress, then returns on a later
|
|
370
|
+
sweep. `cycleComplete` means the cursor wrapped, not that every expired row is gone.
|
|
371
|
+
|
|
372
|
+
System principals sweep app channels; service accounts require
|
|
373
|
+
`chat:retention:write` and are limited to their exact tenant with a separate
|
|
374
|
+
cursor. A null retention policy preserves history. Successor adapters resume
|
|
375
|
+
persisted progress. The result is `{ deleted, channelId, remainingInChannel,
|
|
376
|
+
cycleComplete }`. These are work budgets, not measured capacity or a
|
|
377
|
+
deletion-latency SLA. Retention removes history/receipts; replay of a
|
|
378
|
+
retained-out match archive must still refuse.
|
|
379
|
+
|
|
380
|
+
## Verified report context
|
|
381
|
+
|
|
382
|
+
Configure Sanctions' existing `strategies.evidenceCollector` with
|
|
383
|
+
`chatSanctionEvidenceCollector` from `@voltro/plugin-chat/server`. The ordinary
|
|
384
|
+
`sanctions.report` / `useReport` input supplies target, category, request ID, note
|
|
385
|
+
and `{ channelId, messageIds: [anchorId] }`; no new reporting transport is needed.
|
|
386
|
+
The collector borrows actual ChatAdministration from the app runtime, checks
|
|
387
|
+
reporter membership, tenant and the anchor's actual author/retention, then derives
|
|
388
|
+
at most 20 earlier + anchor + 20 later SQL references. Extra client context IDs,
|
|
389
|
+
wrong targets and unrelated anchors refuse. Human reports remain review-only.
|
|
390
|
+
|
|
391
|
+
The current `chat.context` evidence records server-observed message IDs,
|
|
392
|
+
versions, timestamps and deletion markers. It contains no transcript or author
|
|
393
|
+
identity. Each reference now carries a private Chat-owned evidence ID when
|
|
394
|
+
author content is available. The snapshot lifecycle is described below.
|
|
395
|
+
|
|
396
|
+
## Private evidence snapshots
|
|
397
|
+
|
|
398
|
+
Report collection captures up to 41 actual message versions in `_voltro_chat_evidence`
|
|
399
|
+
under the channel fence. Cases retain only references; author IDs, current text and
|
|
400
|
+
original text stay in this server-only table. An unexpired capture of the same
|
|
401
|
+
message/version is reused without extending its expiry. After expiry, a new opaque
|
|
402
|
+
ID prevents old case references from pointing to a later capture. `evidenceRetentionMs`
|
|
403
|
+
defaults to 30 days (maximum 365 days), independently of ordinary history retention.
|
|
404
|
+
|
|
405
|
+
`ChatAdministration.readReportEvidence(subject, { tenantId, evidenceIds })` reads at
|
|
406
|
+
most 41 references for system or exact-tenant `sanctions:manage`/`admin:full` operators.
|
|
407
|
+
Ordinary channel membership grants no access. Expired IDs return no record; erased
|
|
408
|
+
captures return redacted author/body/original fields. Captured content survives
|
|
409
|
+
ordinary message edit/removal/history pruning until evidence expiry or author erasure.
|
|
410
|
+
It does not copy arbitrary message payloads or media attachments.
|
|
411
|
+
|
|
412
|
+
Author export includes owned captures. Erasure locks relevant channels in stable
|
|
413
|
+
order, re-reads bounded ownership after admission and retries changed channel
|
|
414
|
+
sets (max4), then redacts source text and snapshots together. The retention step
|
|
415
|
+
also reads at most201 evidence identities/deletes at most200 expired captures;
|
|
416
|
+
its result adds `evidenceDeleted` and `evidenceRemainingInChannel`. No bodies
|
|
417
|
+
are loaded by the sweep.
|
|
418
|
+
|
|
419
|
+
## Joining open global channels
|
|
420
|
+
|
|
421
|
+
`kind: 'global'` channels may be created with an empty `members` list. Signed-in
|
|
422
|
+
users and guests then join them themselves with `chat.join({ channelId })`
|
|
423
|
+
(`useChatJoin` in `./web`). Join is idempotent, passes the Sanctions checkpoint,
|
|
424
|
+
respects `maxMembers` (`member_limit`) and `frozen`, refuses an erased identity
|
|
425
|
+
(`identity_required`) and every other channel kind (`forbidden`). Leave with
|
|
426
|
+
`chat.leave`. All other channel kinds stay server-administered through
|
|
427
|
+
`ChatAdministration`.
|
|
428
|
+
|
|
429
|
+
## Guild and party channels
|
|
430
|
+
|
|
431
|
+
Chat keeps Players, Social, Guild and Party observers in its complete query
|
|
432
|
+
`source` lists and marks only these optional integration observations with
|
|
433
|
+
`optionalSource`. Offline checks and both boot paths report absent providers
|
|
434
|
+
visibly without treating them as stale required tables. Installing a provider
|
|
435
|
+
retains all original invalidation triggers. Membership, block and roster
|
|
436
|
+
authorization continue through the existing ports on every applicable read or
|
|
437
|
+
write; the annotation grants no access and installs no provider.
|
|
438
|
+
|
|
439
|
+
`kind: 'guild'` channels (with `refId` = guild ID) may also start without
|
|
440
|
+
members. When `@voltro/plugin-guilds` is installed, its `GuildMembershipPort`
|
|
441
|
+
decides access (for `kind: 'party'` with `refId` = party ID, Matchmaking's
|
|
442
|
+
`PartyMembershipPort`): current guild members may `chat.join` the guild channel,
|
|
443
|
+
and sending (including every mention target), reading history and media URLs
|
|
444
|
+
require current guild membership, so a kicked or departed member loses access at
|
|
445
|
+
once even though the Chat membership row remains. Without Guilds, guild channels
|
|
446
|
+
stay server-administered through `ChatAdministration`. `chat.channel` and
|
|
447
|
+
`chat.membership` refuse a departed member; `chat.channels` and `chat.unread`
|
|
448
|
+
leave such guild channels out; author and mention-candidate projections skip
|
|
449
|
+
departed members; typing is neither accepted from nor delivered to them.
|
|
450
|
+
|
|
451
|
+
## Media messages
|
|
452
|
+
|
|
453
|
+
`chat.send` accepts an optional `mediaId`: the ID of a `_voltro_storage_refs` row the
|
|
454
|
+
sender uploaded through `@voltro/plugin-storage` in the same tenant. The body is then
|
|
455
|
+
an optional caption. Chat refuses a ref that belongs to someone else, another tenant,
|
|
456
|
+
a derivative or a password-protected file (`forbidden`/`not_found`). Without the
|
|
457
|
+
storage plugin, media sends are `forbidden`.
|
|
458
|
+
|
|
459
|
+
The `moderateMedia` slot decides admission. Its default, `typedChatMedia`, checks
|
|
460
|
+
`mediaContentTypes` (default PNG, JPEG, WebP, GIF) and `maxMediaBytes` (default
|
|
461
|
+
10 MiB, max 100 MiB). Image classification is an integration: replace the slot to
|
|
462
|
+
call a provider; `{ allowed: false }` is `content_rejected`, `flagged: true` marks
|
|
463
|
+
the message. Virus scanning stays in plugin-storage's upload scanner. Chat cannot
|
|
464
|
+
tell whether a ref skipped that scanner (for example a resumable upload); if your
|
|
465
|
+
app allows such paths, check them in `moderateMedia`.
|
|
466
|
+
|
|
467
|
+
The message stores `kind: 'media'` and `payload.media` (`id`, `contentType`, `size`,
|
|
468
|
+
`width`, `height`); bytes stay in storage. Readers call `chat.mediaUrl({ messageId })`.
|
|
469
|
+
It checks membership, Social blocks and retention, refuses removed media except for
|
|
470
|
+
channel mods/owners, and returns a storage URL valid for `mediaUrlTtlSec` (default
|
|
471
|
+
300 s, max 3600 s) with `expiresAt`. Chat does this authorization itself, so keep
|
|
472
|
+
chat uploads private; a public ref yields a URL that never expires. `MessageList`
|
|
473
|
+
and `ChatPanel` label media rows and call `onOpenMedia(messageId)`; they never
|
|
474
|
+
render a URL themselves. Editing a caption keeps the attachment.
|
|
475
|
+
|
|
476
|
+
`_voltro_chat_media` records one row per media message. Erasure and history
|
|
477
|
+
retention mark rows `deleting` (owner ID cleared) in the same transaction as the
|
|
478
|
+
message change. `ChatAdministration.sweepMedia(subject)` then deletes at most 50
|
|
479
|
+
queued blobs per step through plugin-storage, outside SQL, and keeps a blob
|
|
480
|
+
while another active row references it. A storage 404 counts as done; other
|
|
481
|
+
failures stay queued and are reported as `failed`. The contributed retention
|
|
482
|
+
schedule runs one sweep step per minute. Known limits: if the owner attaches the
|
|
483
|
+
same upload again while the sweep is deleting it, the new message can lose its
|
|
484
|
+
file; failing rows are retried first, so persistent storage errors can hold back
|
|
485
|
+
later rows. Export includes owned media rows within `maxExportRows`.
|
|
486
|
+
|
|
487
|
+
## Erasure admission tombstones
|
|
488
|
+
|
|
489
|
+
`_voltro_chat_admissions` holds one server-only row per tenant and Chat identity. Its
|
|
490
|
+
ID is derived from a SHA-256 hash of tenant and subject. Every path that adds a
|
|
491
|
+
membership — `createChannel` (including DMs), `addMember` and `archiveMatch` — seeds
|
|
492
|
+
that row, locks it before the channel and refuses an erased identity with
|
|
493
|
+
`identity_required`. Erasure locks the same row first, then the affected channels in
|
|
494
|
+
stable order, and in the same transaction sets `subjectId` to null and `erased` to
|
|
495
|
+
true. History retention never deletes these rows.
|
|
496
|
+
|
|
497
|
+
Consequences:
|
|
498
|
+
|
|
499
|
+
- An erased identity cannot rejoin Chat in that tenant. Use a new subject ID for a
|
|
500
|
+
new account. Other tenants are independent.
|
|
501
|
+
- The tombstone ID is hash-derived from tenant and subject. Anyone who already
|
|
502
|
+
knows both can recompute it; treat it as pseudonymous data in your records of
|
|
503
|
+
processing.
|
|
504
|
+
- Work prepared before erasure (a queued `archiveMatch` Effect, archive moderation
|
|
505
|
+
still running, a create whose reply was lost) re-checks the tombstone before it
|
|
506
|
+
writes or recovers, so it cannot recreate memberships or content.
|
|
507
|
+
- Ordinary send/edit/capture take only the channel fence; erasure shares those
|
|
508
|
+
fences, so it never takes locks in the reverse order.
|
|
509
|
+
- Export counts the admission row in `maxExportRows` and omits its internal fence
|
|
510
|
+
counter.
|
|
511
|
+
- Message versions are SQL `INT` columns. Edit and remove refuse
|
|
512
|
+
`baseVersion >= CHAT_MAX_MESSAGE_VERSION` (2147483647); erasure redaction
|
|
513
|
+
saturates at that ceiling instead of failing.
|
|
514
|
+
|
|
515
|
+
Mention redaction scans up to 100 `payload.mentions` array slots (the
|
|
516
|
+
`maxMentions` ceiling) per message with `jsonField` predicates. Its query cost
|
|
517
|
+
per dialect is not measured.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
We take the security of Voltro and the applications built on it seriously. This
|
|
4
|
+
document explains how to report a vulnerability and what to expect in return.
|
|
5
|
+
|
|
6
|
+
## Reporting a vulnerability
|
|
7
|
+
|
|
8
|
+
**Please do not open public issues or discuss suspected vulnerabilities in
|
|
9
|
+
public channels.** Report them privately by email:
|
|
10
|
+
|
|
11
|
+
**security contact: [support@voltro.dev](mailto:support@voltro.dev)**
|
|
12
|
+
(subject line: `SECURITY`).
|
|
13
|
+
|
|
14
|
+
Include, where you can:
|
|
15
|
+
|
|
16
|
+
- the affected package(s) and version(s) (e.g. `@voltro/runtime@x.y.z`);
|
|
17
|
+
- a description of the issue and its impact;
|
|
18
|
+
- a minimal reproduction or proof of concept;
|
|
19
|
+
- any suggested remediation.
|
|
20
|
+
|
|
21
|
+
If you'd like to encrypt your report, ask in a first plain email and we'll
|
|
22
|
+
arrange a key.
|
|
23
|
+
|
|
24
|
+
## What to expect
|
|
25
|
+
|
|
26
|
+
- **Acknowledgement** of your report within **3 business days**.
|
|
27
|
+
- An initial assessment (severity, affected surface) and a remediation plan as
|
|
28
|
+
soon as we've reproduced the issue.
|
|
29
|
+
- **Coordinated disclosure:** we'll agree a disclosure timeline with you and
|
|
30
|
+
credit you in the release notes if you wish. Please give us reasonable time to
|
|
31
|
+
ship a fix before any public disclosure.
|
|
32
|
+
|
|
33
|
+
We do not currently run a paid bug-bounty program, but we genuinely appreciate
|
|
34
|
+
responsible disclosure and will credit reporters.
|
|
35
|
+
|
|
36
|
+
## Software Bill of Materials (SBOM)
|
|
37
|
+
|
|
38
|
+
A CycloneDX SBOM of the third-party runtime dependency graph is generated for the
|
|
39
|
+
published packages (`pnpm gen:sbom` → `sbom.cdx.json`). If your procurement or
|
|
40
|
+
security team needs it, request it at
|
|
41
|
+
[support@voltro.dev](mailto:support@voltro.dev).
|
|
42
|
+
|
|
43
|
+
## Supported versions
|
|
44
|
+
|
|
45
|
+
Voltro is pre-1.0 and ships in lockstep: **only the latest published minor of
|
|
46
|
+
each `@voltro/*` package receives security fixes.** Please upgrade to the latest
|
|
47
|
+
release before reporting, and pin exact versions in production.
|
|
48
|
+
|
|
49
|
+
## Scope
|
|
50
|
+
|
|
51
|
+
In scope: the published `@voltro/*` packages and the framework's own code.
|
|
52
|
+
|
|
53
|
+
Out of scope: vulnerabilities in third-party dependencies that are already
|
|
54
|
+
tracked upstream (we monitor these via a production-dependency audit gate in
|
|
55
|
+
CI), and issues that require a non-default, explicitly-unsafe configuration. If
|
|
56
|
+
you're unsure, report it anyway — we'd rather hear about it.
|