@voltro/plugin-matchmaking 0.0.0-stage → 0.88.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +24492 -0
  2. package/LICENSE +61 -0
  3. package/README.md +272 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +8333 -0
  6. package/dist/admission.entity.js +17 -0
  7. package/dist/assignment.js +125 -0
  8. package/dist/avoidPairs.js +4 -0
  9. package/dist/contract.js +105 -0
  10. package/dist/director-app/matchmaking.director.workflow.server.tsx +5 -0
  11. package/dist/director-app/matchmaking.director.workflow.tsx +13 -0
  12. package/dist/director.js +72 -0
  13. package/dist/directorContract.js +15 -0
  14. package/dist/directorWorkflow.js +5 -0
  15. package/dist/errors.d.ts +13 -0
  16. package/dist/errors.js +20 -0
  17. package/dist/index.d.ts +523 -0
  18. package/dist/index.js +12 -0
  19. package/dist/inspect.js +40 -0
  20. package/dist/packing.js +71 -0
  21. package/dist/parties.js +316 -0
  22. package/dist/party.entity.js +33 -0
  23. package/dist/partyComponents.js +176 -0
  24. package/dist/partyContract.js +24 -0
  25. package/dist/plugin.js +257 -0
  26. package/dist/port.d.ts +116 -0
  27. package/dist/port.js +4 -0
  28. package/dist/queue.d.ts +91 -0
  29. package/dist/queue.js +79 -0
  30. package/dist/queueDeclarations.js +27 -0
  31. package/dist/roomsAllocator.d.ts +67 -0
  32. package/dist/roomsAllocator.js +75 -0
  33. package/dist/rpc.d.ts +247 -0
  34. package/dist/rpc.js +201 -0
  35. package/dist/server.d.ts +337 -0
  36. package/dist/server.js +8 -0
  37. package/dist/service.js +369 -0
  38. package/dist/strategies.d.ts +97 -0
  39. package/dist/strategies.js +140 -0
  40. package/dist/testing.d.ts +296 -0
  41. package/dist/testing.js +151 -0
  42. package/dist/ticketContract.d.ts +52 -0
  43. package/dist/ticketContract.js +30 -0
  44. package/dist/tickets.entity.js +49 -0
  45. package/dist/web.d.ts +279 -0
  46. package/dist/web.js +7 -0
  47. package/package.json +104 -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,273 @@
1
- # Temporary Holding Version
1
+ # Matchmaking foundations
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This implementation is **built**. It contains queue descriptions, pure matching
4
+ policies and an installable SQL solo-ticket admission adapter.
5
+
6
+ ## Take
7
+
8
+ `@voltro/plugin-matchmaking/queue` exports `defineQueue` and `runQueueMatch`;
9
+ `./strategies` exports `fifo`, `skillWindow`, `regionLatency`, `composite` and
10
+ `validateMatchOutput`. The root also exports these functions and `packTeams`.
11
+ The server root also exports `matchmakingPlugin(options)` and `MatchmakingPort`.
12
+ The factory installs three admission tables and the enqueue/cancel/status RPCs;
13
+ it requires explicit `queues`, trusted `prepareTicket` and transactional `admit`
14
+ callbacks. No default skill/policy provider, Director or Room allocation exists
15
+ yet. A queue descriptor alone does not activate matching. Browser descriptors
16
+ import `./rpc`, errors from `./errors`; the root now includes server-only code.
17
+
18
+ ```ts
19
+ import { Schema } from 'effect'
20
+ import { defineQueue } from '@voltro/plugin-matchmaking/queue'
21
+ import { skillWindow } from '@voltro/plugin-matchmaking/strategies'
22
+
23
+ const ticket = Schema.Struct({
24
+ skill: Schema.Number,
25
+ region: Schema.String,
26
+ latencies: Schema.Record(Schema.String, Schema.Number),
27
+ })
28
+
29
+ const queue = defineQueue({
30
+ name: 'ranked-2v2', room: 'arena', ticket,
31
+ teams: { count: 2, size: 2 },
32
+ pools: { byRegion: attributes => attributes.region },
33
+ matchFunction: skillWindow({ initialWindow: 100, growPerSecond: 25,
34
+ maxWindow: 600, latencyMaxMs: 120 }),
35
+ minQuality: { initial: 0.8, after: [[30000, 0.5], [90000, 0]] },
36
+ })
37
+ ```
38
+
39
+ `runQueueMatch(queue, { tickets, now, avoid })` takes server-owned ticket evidence
40
+ and an explicit clock. Tickets contain `id`, `tenantId`, `queue`, `members`,
41
+ `createdAt`, `attributes`; this is a pure Director input, not a player RPC state
42
+ payload. The function schema-checks attributes without silently coercing data,
43
+ partitions through the captured pool callback, executes the actual matching slot
44
+ and validates its output. Ticket tenant mixing, duplicate ticket/subject IDs,
45
+ future creation times, sparse arrays, non-JSON attributes and budgets refuse.
46
+ Object key order does not change JSON identity.
47
+
48
+ ## Parties
49
+
50
+ `matchmakingPlugin({ …, parties: { maxPartySize?, partyInviteTtlMs? } })` installs
51
+ ephemeral SQL parties (default size 8, max 256; invites live 10 minutes by default).
52
+ Routes under `matchmaking.parties.*`: `create` (idempotent per `requestId`, the caller
53
+ becomes leader; `approval: 'leader' | 'open'`), `invite` (leader only), `join`
54
+ (needs a live invite unless the party is open; `party_full` at capacity), `leave`,
55
+ `kick` and `transferLeader` (leader only), `setReady`, plus the live queries
56
+ `mine` (the caller's party or `null`) and `invites`. Hooks in `./web`: `useParty`,
57
+ `usePartyInvites`, `useCreateParty`, `useInviteToParty`, `useJoinParty`,
58
+ `useLeaveParty`, `useKickFromParty`, `useSetPartyReady`, `useTransferPartyLeader`, and the
59
+ presentational `PartyPanel` (en/de `partyLabels`).
60
+
61
+ A subject belongs to at most one party per tenant (`already_in_party`). When the
62
+ leader leaves, the longest-standing member takes over; the last member leaving
63
+ dissolves the party and its invites. `PartyMembershipPort` answers current
64
+ membership for other plugins. Export adds `parties` (own membership, sent and
65
+ received invites); erase leaves the party first, then deletes invites, then runs
66
+ the ticket hook.
67
+
68
+ Party search: the leader calls `matchmaking.parties.search({ partyId, queue,
69
+ requestId, preferences })` (`useSearchAsParty`) once every member is ready
70
+ (`party_not_ready` otherwise). One ticket lists all members and every member's claim
71
+ points at it, so nobody can search alone meanwhile (`already_searching`). Members read
72
+ it with `matchmaking.status`; `mine` reports it as `searchTicketId`. Any member stops
73
+ it with `matchmaking.parties.cancelSearch` (`useCancelPartySearch`); the solo
74
+ `matchmaking.cancel` refuses party tickets. Join, leave, kick, leader transfer and
75
+ withdrawn readiness cancel an open search in the same transaction. The leader's
76
+ `prepareTicket` result stands for the party. Locks go party → member claims (sorted)
77
+ → queue. The `party.invited` event and presence remain open.
78
+
79
+ ### Server roster composition
80
+
81
+ Resolve `PartyRosterPort` from `./port` through the scoped Effect context for
82
+ trusted server composition. `read(tenantId, partyId)` returns `null` for a missing
83
+ party in that tenant, otherwise a detached, frozen
84
+ `{ tenantId, partyId, leaderId, memberIds }` snapshot (leader first, remaining IDs
85
+ in stable order). The default reader locks the party before its members, matching
86
+ the writers' order without writing admission counters, tickets or timestamps.
87
+ Current leader transfer, privacy erasure and final-member dissolution are observed.
88
+ Malformed/corrupt membership refuses instead of returning a partial roster.
89
+
90
+ This adds no player RPC. The caller must authorize its operation and resolve
91
+ actual member Subjects and current World locations. IDs do not imply Subject
92
+ kinds or movement authority; a snapshot is not a lease. A durable World command
93
+ must retain its original captured members on replay. Reads created before binding,
94
+ queued across rebinding/deactivation, or completed after a binding change refuse
95
+ with `unavailable`. Full replacements supply this separate port in their scoped
96
+ Layer; `fakePartyRosterLayer(read)` in `./testing` accepts the complete reader and
97
+ fails closed when omitted. The existing membership port is unchanged.
98
+
99
+ ## Configure
100
+
101
+ | Option | Default | Authoring budget |
102
+ |---|---|---|
103
+ | `teams` | required | At most 64 teams, 256 players/team, 4,096 total slots |
104
+ | `maxTickets` | 10,000 | Maximum supplied tickets; not accepted Director capacity |
105
+ | `maxSearchNodes` | 100,000 | Branch inspections shared across policies and partitions |
106
+ | `maxAvoidPairs` | 10,000 | Maximum supplied symmetric exclusion pairs |
107
+ | `maxAttributeBytes` | 16,384 | UTF-8 JSON bytes per ticket; JSON depth at most 32 |
108
+ | `minQuality` | initial 0, no steps | Non-increasing quality floor with strictly increasing wait times |
109
+ | `cycleMs` | 1,000 | Descriptor only; no scheduler is installed |
110
+ | `ticketTtlMs` | 300,000 | SQL ticket lifetime; owner-triggered expiry, no sweep job yet |
111
+ | `maxPreferencesBytes` | 4,096 | Bounded JSON preference intent; at most 65,536 bytes |
112
+ | `maxExportRows` | 10,000 | Refuses an incomplete privacy export/erase; at most 100,000 rows |
113
+
114
+ Rules, schedules and callback selection are captured at description time.
115
+ A matching call copies/fixes its ticket data; the solver owns no clock, timer,
116
+ connection, store or per-process authoritative ticket registry. Parties are
117
+ indivisible. Parties larger than a team stay in leftovers. The iterative solver
118
+ backtracks when first-fit packing fails and stops at the configured branch budget,
119
+ returning `exhausted`, `searchNodes` and every unassigned ticket. These counters
120
+ are algorithm observations, not measured CPU duration or throughput.
121
+
122
+ ## Hook
123
+
124
+ `matchmaking.enqueue` and `matchmaking.cancel` are Actions: the adapter owns
125
+ their SQL transactions. `matchmaking.status` is a reactive Query on the ticket
126
+ table; the executor checks the verified caller and exact tenant/owner. The wire
127
+ accepts only `{ queue, requestId, preferences }`, never authoritative attributes,
128
+ party members, state or assignments. Required `prepareTicket` resolves trusted
129
+ attributes before SQL; required `admit` rechecks durable eligibility inside the
130
+ borrowed transaction. Actual default Social/Sanctions integration is still open.
131
+
132
+ Subject and queue admission rows serialize one live solo search across queues
133
+ and the live queue cap. Exact request receipts return the committed ticket before
134
+ calling either provider again, through the subject erase fence; changed intent
135
+ refuses. Cancel is repeatable. Starting a new intention expires the owner’s old
136
+ expired search atomically. Status is read-only and may show `open` after its
137
+ `expiresAt` until a new intention or future sweep persists expiry.
138
+
139
+ `./web` provides `useEnqueueMatchmaking`, `useCancelMatchmaking` and
140
+ `useMatchmakingStatus(ticketId, apiName)`. Actions are explicit; absent ticket
141
+ selection keeps the subscription idle. No hidden polling/retry/auto-enqueue is
142
+ installed. Assignment events, listener revocation, workflow triggers, ordinary
143
+ queue discovery and the combined `useMatchmaking` lobby remain unfinished.
144
+ A pure match result grants no seat/session/token. SQL is ticket truth; no hot
145
+ index or additional authoritative per-process ticket registry is introduced.
146
+
147
+ ## Override
148
+
149
+ `fifo()` prefers canonical oldest-ticket/ID order and returns quality 1.
150
+ `skillWindow()` grows the permitted skill spread using the oldest candidate's
151
+ waiting time, up to `maxWindow`; quality is `1 - spread / maxWindow` (or 1 for
152
+ zero spread/zero maximum). With `latencyMaxMs`, all candidates need the same
153
+ region and bounded latency evidence for that region. `regionLatency()` applies
154
+ that region rule and uses `1 - worstLatency / latencyMaxMs`, with quality 1 at
155
+ zero latency/zero maximum. These caller-supplied values are not verified network
156
+ measurements. Skill and latency providers remain author/server authority.
157
+
158
+ `composite([first, second])` applies policies to leftovers in order, sharing the
159
+ original search budget. Every custom result is checked for full team size,
160
+ whole original tickets, disjoint assignment, explicit complete leftovers,
161
+ configured minimum quality and exclusion pairs. Missing/forged/foreign tickets
162
+ refuse; no automatic fallback overwrites a failed slot. An `avoid` pair excludes
163
+ the two subjects from the entire match, including opposite teams and the same
164
+ party. Actual `SocialBlockPort` acquisition remains future work; this pure input
165
+ alone does not establish integration with Social or Sanctions.
166
+
167
+ ## Extend or replace
168
+
169
+ `./port` exports the three-method `MatchmakingPort`; routes resolve that Tag
170
+ from their Effect context, so a replacement Layer uses the same RPC contract.
171
+ `./testing` exports `fakeMatchmakingService`/`fakeMatchmakingLayer`, with missing
172
+ methods failing closed. The default service clears on plugin deactivation and
173
+ one plugin instance refuses binding another app store.
174
+
175
+ `./server` exports `dataStoreMatchmakingTickets`, `TicketAdmissionOptions` and
176
+ the ticket/subject-admission/queue-admission table handles. This standalone
177
+ adapter borrows the supplied DataStore and exposes the same service plus
178
+ export/erase. Tables are `_voltro_mm_tickets`, `_voltro_mm_subject_admission`
179
+ and `_voltro_mm_queue_admission`. Export projects only the target’s ticket DTOs;
180
+ erase deletes those tickets and retains a redacted hashed subject fence with no
181
+ plain subject ID or active ticket pointer. Peers and queue budgets remain. The
182
+ fence prevents recreation after adapter reopening; this is not a compliance
183
+ claim. All privacy hooks refuse bounded incomplete work rather than silently
184
+ truncate.
185
+
186
+ ## Panel and GM
187
+
188
+ The read-only process `/status` inspect endpoint reports captured
189
+ queue/admission budgets, required author slots and the default adapter binding.
190
+ It does not read SQL or enumerate tickets/players/attributes.
191
+ `directorInstalled:false` expressly means matching/allocation is unfinished. The
192
+ shared bilingual Matchmaking page is wired into both Local and Cloud dashboards,
193
+ their current instance/replica selection and the shared AppInspector feature
194
+ catalog. Cloud uses authorized app-scoped plugin-inventory validation; Local
195
+ uses the existing read credential transport. Unknown/malformed snapshots fail
196
+ instead of becoming empty data. Reload remains focusable while duplicate refresh
197
+ admission is guarded. No live queue-depth/match counters, GM writes or capacity
198
+ claim exists yet; these remain open alongside templates/manifests and
199
+ allocation-aware panel additions.
200
+
201
+ ## Tests
202
+
203
+ `./testing` exports `matchmakingInputFixture` and `matchFunctionFixture`, using
204
+ actual pure policies. Adapter/plugin tests additionally author actual
205
+ transactional rollback, intent/identity capture, port replacement, caps and
206
+ erase. Six native fixtures (PostgreSQL, MySQL, MariaDB, MSSQL, SQLite, Turso)
207
+ each author SQL roundtrip, two-pool owner/queue races, rollback, successor
208
+ expiry and privacy; missing fixture credentials fail rather than become
209
+ acceptance skips. No native fixture has run.
210
+
211
+ ## Trusted SQL Director planning
212
+
213
+ The installed `plugin.matchmaking.plan(subject, { tenantId, queue })` facade and
214
+ `dataStoreMatchmakingTickets(...).director.plan(...)` now implement the read/
215
+ planning stage over the same borrowed SQL ticket store. `./server` additionally
216
+ exports `dataStoreMatchmakingPlanner(store, { queues, avoidPairs, now })` for an
217
+ explicit server-owned composition. Queue/callback selection is captured with the
218
+ same single declaration-capture implementation used by admission. The stage
219
+ requires a trusted system Subject; human, anonymous and external service callers
220
+ refuse. No player planning route or new port authority is added.
221
+
222
+ ```ts
223
+ import { Effect, Schema } from 'effect'
224
+ import { systemSubject } from '@voltro/protocol'
225
+ import { matchmakingPlugin } from '@voltro/plugin-matchmaking'
226
+ import { defineQueue } from '@voltro/plugin-matchmaking/queue'
227
+ import { fifo } from '@voltro/plugin-matchmaking/strategies'
228
+
229
+ export const matchmaking = matchmakingPlugin({
230
+ queues: [defineQueue({ name: 'duels', room: 'arena', ticket: Schema.Struct({ skill: Schema.Number }),
231
+ teams: { count: 2, size: 1 }, matchFunction: fifo<{ readonly skill: number }>() })],
232
+ prepareTicket: () => Effect.succeed({ skill: 100 }),
233
+ admit: () => Effect.void,
234
+ })
235
+ // In trusted server code, after this same plugin has bound its SQL adapter:
236
+ const proposal = matchmaking.matchmaking.plan(systemSubject('queue-planning'), { tenantId: 'game', queue: 'duels' })
237
+ ```
238
+
239
+ This thin example's skill/admission rules are intentionally author supplied;
240
+ production default Players/Sanctions bindings remain unfinished. Planning reads
241
+ only current tenant/queue open tickets with expiry strictly after the captured
242
+ clock. A complete scan over `maxTickets` refuses instead of returning a truncated
243
+ queue. It validates actual SQL roster/scope/time and detached attribute budgets,
244
+ then runs the real queue partition/match/output admission. It returns proposals,
245
+ complete leftovers, search-node count/exhaustion and observation scope/time.
246
+ These are read observations, not ticket claims, assignments or measured capacity.
247
+ Repeated reads can propose the same tickets. A later writer must atomically
248
+ recheck/claim all original tickets and admit an idempotent Room allocation.
249
+ No singleton workflow, debounce/timer, SQL claim/assignment, allocation/retry,
250
+ backfill or event settlement is activated by this facade.
251
+
252
+ `avoidPairs(context)` is a captured Effect slot with immutable scoped candidate
253
+ records, explicit `maxPairs` and the actual DataStore read query capability; it
254
+ exposes no mutators. Root, `./server` and browser-safe `./strategies` export the
255
+ single `defaultMatchmakingAvoidPairs`, whose empty policy is for an app without a
256
+ block provider. `./testing` supplies `matchmakingAvoidPairsFixture(query)` using
257
+ the actual supplied read capability. A custom provider's pairs pass the same
258
+ bounded pure matcher validation; its typed refusal retains the error code.
259
+ Automatic optional Social block binding/shadow-pool enforcement remains open;
260
+ empty defaults are not evidence that an installed Social plugin is integrated.
261
+ Both dashboards report actual default/custom provider selection while keeping
262
+ `directorInstalled: false`. No queue depth/player data is in inspection.
263
+
264
+ The plan contains owner/attribute data for trusted processing only; do not put
265
+ it in a workflow payload/result or expose it to clients. A durable Director
266
+ needs opaque persisted identities and privacy-aware settlement. Plugin
267
+ deactivation clears the owned facade; ordinary client routing still resolves
268
+ MatchmakingPort and keeps its existing replacement semantics.
269
+
270
+
271
+ ## Production license admission
272
+
273
+ Declare the actual `licensingPlugin()` before this plugin in the API app. Production and staging activation require its verified active or grace deployment-wide license to grant `matchmaking`. Explicit development/test mode stays free without recurring per-instance notices; CLI owns the actual template first-boot receipt. Tenant-scoped licenses refuse until actual tenant-bound authority exists. This server-only gate preserves normal schema, store binding and cleanup; it runs before the original activation hook, not before schema/store binding.
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.