fvtt-world-cli 1.1.0 → 1.1.2

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/docs/security.md CHANGED
@@ -1,366 +1,193 @@
1
1
  # Security
2
2
 
3
- fvtt-world-cli is a local administration tool. It gives an authenticated caller access to the
4
- authority of the connected Foundry GM client, within the typed command surface and explicit file
5
- boundary described here.
3
+ `fvtt-world-cli` is a local administration tool. An authenticated caller acts with the connected
4
+ GM client's authority, limited by the commands, permissions, and file rules described here.
6
5
 
7
6
  ## Trust boundary
8
7
 
9
- A request travels from the local CLI through the authenticated loopback daemon to the authenticated
10
- Foundry GM bridge, which executes it through Foundry's APIs. The CLI input is untrusted. The daemon
11
- authenticates and routes requests but does not grant Foundry permissions. The bridge validates
12
- requests again and executes them as the connected Foundry user.
8
+ Requests travel through the authenticated loopback daemon to a Foundry GM browser. The daemon
9
+ routes requests; the bridge validates them and executes through Foundry's APIs as the connected GM.
13
10
 
14
- The design assumes a trusted local machine and a trusted GM-controlled Foundry session. It is not a
15
- public multi-tenant API.
11
+ The design assumes a trusted local machine and GM session. Processes running as the same OS user
12
+ may be able to read the CLI credential. A compromised GM browser or installed module is already
13
+ inside the trusted runtime.
14
+
15
+ Reads and search can expose any content the connected GM can see, including material hidden from
16
+ players.
16
17
 
17
18
  ## Authentication
18
19
 
19
- - The daemon hard-rejects non-loopback listen hosts and validates the exact HTTP Host.
20
- - A random device-local credential authenticates CLI and future Companion clients with a first-message
21
- `client.hello`; it is stored only in the per-user config file.
22
- - Every first frame and subsequent transport message is checked against a closed protocol schema
23
- before role assignment or dispatch. Malformed first frames are closed immediately and cannot keep
24
- an unauthenticated heartbeat connection alive.
25
- - Each browser/world/GM profile has an independent bridge credential. Only its SHA-256 digest is kept
26
- by the daemon; the clear credential stays in client-scoped Foundry storage.
27
- - A pairing profile is identified by Origin, world, GM, and a per-browser client identifier that the
28
- browser generates once and keeps in the same client-scoped storage as its credential. The identifier
29
- is self-asserted and grants nothing: it only scopes which stored record a re-pair replaces, so one
30
- browser re-pairing cannot rotate another browser's credential out from under it. Access remains
31
- gated by human approval of the pairing code and by the credential-digest check on every hello, and a
32
- hello whose client identifier does not match its pairing record is rejected as `UNAUTHORIZED`. That
33
- storage is shared by every tab of one browser, so a first-use identifier can be generated twice when
34
- two tabs start pairing at once; the browser discards an approved credential whose identifier no longer
35
- matches the stored one instead of keeping a pairing the hello gate would refuse.
36
- - The browser label attached to a pairing is constrained by the protocol schema to 1 to 64
37
- characters, with whitespace-only values rejected along with control, zero-width,
38
- bidirectional-override, and Unicode tag characters; a violating request is refused rather than
39
- repaired. Rejecting a whitespace-only label removes the simplest way to send a label that reads
40
- as nothing; it is not a guarantee that a label renders visibly, because blank-rendering filler
41
- characters pass the pattern.
42
- The client identifier printed beside the label is what reliably distinguishes two browsers, including
43
- two that share one label. The pairing socket is unauthenticated, so any local process can send a
44
- pairing request, and an unfiltered label would reach a terminal as an escape-sequence injection or as
45
- hidden text in agent output. Both approval prompts, the one `auth approve` shows and the one the
46
- `auth` pairing wait shows, print it as raw text, and JSON output escapes C0 control characters but
47
- not C1, zero-width, bidirectional-override, or tag ones. The constraint covers the label field only;
48
- the identity fields printed beside it are described under Known risks.
49
- - The module removes those same characters from the label it collects in the Authorization window, and
50
- also normalizes it to NFC, trims it, and caps its length, before the pairing request goes out. That
51
- sanitizer is a usability layer, not the boundary: it keeps an honest browser from being refused over
52
- a stray paste, while the schema check in the daemon remains the enforcement point, so a request from
53
- any other local process is still refused rather than repaired.
54
- - Browser pairing and bridge sockets require a syntactically valid HTTP(S) Origin, matched exactly
55
- after pairing. Credentials never appear in URLs, command lines, environment variables, or output.
56
- - The bridge starts only for a configured GM client. The module hides its settings and category from
57
- users below the Assistant GM role, and only gamemasters can open its windows. The settings remain
58
- registered in that browser profile for a later GM session.
59
- - Another pairing cannot displace the active bridge. A same-pairing reconnect may take over after a
60
- reload; requests already forwarded to the displaced socket fail immediately as indeterminate.
61
- Abnormal close retains a 30-second lease against other pairings, while intentional goodbye,
62
- release, and revocation do not create a lease. Release uses a dedicated terminal close code so the
63
- released browser cannot automatically reclaim the slot.
64
- - Only the exact active authenticated bridge socket can send `bridge.goodbye` or revoke its own
65
- pairing. A socket is not assigned the bridge role or pairing identity until the complete handshake
66
- has authenticated.
67
- - Browser Unpair deletes its clear credential only after a correlated successful revocation response.
68
- On failure it retains the credential for retry; Forget local is a separate action that deliberately
69
- leaves the daemon-side profile active.
70
- - Protocol-version or authentication rejection stops that bridge load until the Foundry client is
71
- reloaded. `BRIDGE_BUSY` is reported separately, preserves the credential, and permits manual Retry
72
- after the current owner is released.
73
- - A bridge that loses live GM authority returns correlated `PERMISSION_DENIED` before it closes and
74
- does not route the denied command, preserving a determinate no-mutation outcome.
75
-
76
- Loopback reduces exposure but does not replace authentication. Other processes running as the same
77
- local user may be able to read local configuration or connect to local ports. The daemon accepts only
78
- the exact normalized Host authority configured for its listener; post-bind publication of a numeric
79
- runtime address does not broaden that authority or invalidate an explicitly configured `localhost`.
20
+ - The daemon refuses non-loopback listen hosts and validates the exact configured HTTP Host.
21
+ Loopback does not replace authentication.
22
+ - Local clients authenticate with a random credential stored in the per-user configuration.
23
+ The daemon validates the first frame before assigning a socket's role.
24
+ - Each Origin/world/GM/browser pairing has its own bridge credential. The daemon stores only its
25
+ SHA-256 digest; the clear credential stays in client-scoped Foundry storage. Credentials do not
26
+ appear in URLs, command lines, or output.
27
+ - Browser sockets require a valid HTTP(S) Origin that matches the paired Origin exactly. The
28
+ hello must also match the stored browser client id.
29
+ - The bridge requires GM authority. Losing that authority causes command rejection before
30
+ dispatch and ends the connection.
31
+ - Another pairing cannot displace the active bridge. Only the authenticated active socket may
32
+ release ownership or revoke its own pairing.
33
+
34
+ Unpair deletes the browser's credential only after the daemon confirms revocation. *Forget local*
35
+ removes that local credential without revoking the daemon record. Local commands can revoke a
36
+ pairing or rotate the CLI credential; see [Authorization commands](commands.md#authorization-commands).
80
37
 
81
38
  ## Command boundary
82
39
 
83
- The bridge accepts only explicit commands registered in the protocol and advertised during the
84
- handshake. Each command has a schema and a known handler. There is no generic method invocation or
85
- universal "edit anything" endpoint.
86
-
87
- Mutations use Foundry Document APIs or a reviewed typed Foundry action. Raw live world databases are
88
- outside the command surface.
40
+ The bridge accepts explicit, schema-validated commands advertised during the handshake. Document
41
+ mutations use Foundry Document APIs; action commands call fixed, reviewed methods. There is no
42
+ generic RPC, arbitrary JavaScript evaluation, or direct access to live world databases.
43
+ Compendium commands cannot edit packs; imports create world documents.
89
44
 
90
- ## Validation and protected metadata
45
+ ### Validation and protected metadata
91
46
 
92
- Closed document families enumerate writable fields. Open families allow Foundry-, system-, or
93
- module-defined data but pass every write and preview through shared protected-metadata sanitization.
47
+ Closed document schemas enumerate writable fields. Open schemas allow system and module data but
48
+ sanitize protected metadata on every write and preview. Identity, statistics, authorship, and raw
49
+ ownership cannot be set through ordinary content payloads. Foundry also applies its own DataModel
50
+ and permission checks.
94
51
 
95
- Document identity, statistics, authorship, and raw ownership are server-controlled. Ordinary content
96
- payloads cannot set them. Supported ownership changes use dedicated commands with GM and schema
97
- checks.
52
+ ### Document ownership
98
53
 
99
- Validation occurs in the CLI and again in the bridge. Foundry performs its own DataModel and
100
- permission validation before persistence.
54
+ Dedicated `<family>.ownership.set` commands change supported default or per-user access levels.
55
+ Embedded documents often inherit access from their parent.
101
56
 
102
57
  ## Permissions and destructive actions
103
58
 
104
- The bridge requires a GM session for mutations. Delete commands can require explicit force where the
105
- document has high-risk references or consequences. A force flag acknowledges the command's defined
106
- guard; it does not bypass Foundry permissions, module hooks, validation, or filesystem containment.
107
-
108
- Foundry hooks can veto or partially apply writes. Mutation handlers confirm observable stored state
109
- where the command contract requires it and return structured partial or failed outcomes instead of
110
- claiming success.
111
-
112
- Every command also carries an allow, approve, or deny permission in the GM client holding the
113
- bridge. The Foundry module enforces it after authentication, GM authority, and write-permission
114
- checks but before dispatch. It covers reads, writes, bulk envelopes, and previews. The default
115
- policy denies commands that can execute code, change who can do what, or persist outside the
116
- world's own data; requires approval for destructive commands plus `system.reload` and
117
- `user.create`, where the destructive set is the `delete` and `delete-many` verbs, `file.move`,
118
- `scene.fog.reset`, and `chat.flush`; and allows the other policy-controlled commands. A
119
- denied-by-default command is a deliberate opt-in: it exists so a GM can enable it knowingly, not
120
- so it can run out of the box.
121
-
122
- `system.ping`, `system.info`, and the internal approval-wait commands always run. They let the bridge
123
- report its state and finish decisions already taken without changing world content. Stored policy
124
- overrides cannot change them, and the permissions window omits them. Pairing and other `auth`
125
- operations run in the daemon, so the command policy does not apply.
126
-
127
- A denied command is absent from the CLI's `commands` listing while the bridge is reachable. This
128
- reduces irrelevant choices for automated callers but does not enforce the policy. The Foundry module
129
- enforces it at dispatch and returns `COMMAND_DENIED` if a caller sends the command anyway.
130
-
131
- The original request does not execute a command that requires approval. The module holds it in
132
- memory until the GM decides. After Allow, the module repeats readiness, GM authority, parameter,
133
- write-permission, policy, and family checks. A command changed to deny while waiting returns
134
- `COMMAND_DENIED`. Changing it between allow and approve does not create a second approval. A denied,
135
- expired, or confirmed-cancelled request never runs.
136
-
137
- The module returns a random 128-bit `approvalId` only to the original caller. Reading or cancelling
138
- the decision requires that identifier.
139
-
140
- The GM-only Command Approval window shows the command, remaining time, target documents or managed
141
- paths, and parameters. It reports binary upload content by size instead of rendering the payload,
142
- and any text longer than 16,384 characters, including a macro body, by its character count for the
143
- same reason. A macro long enough to cross that line is therefore approved on its name and origin
144
- rather than on its code, and `macro.create` runs by itself under the shipped defaults, so a GM who
145
- wants every body reviewable moves `macro.create` and `macro.update` to approve as well. The command
146
- envelope has no caller identity, so the window cannot name the requester. The GM approves the
147
- displayed invocation, not a person or process.
148
-
149
- ## Document ownership
150
-
151
- Ownership is access policy rather than ordinary document content. Raw `ownership` is excluded from
152
- normal create and update payloads. Dedicated `<family>.ownership.set` commands change the default or
153
- per-user level on supported families.
154
-
155
- Read projections expose ownership only where the public command contract intentionally includes it.
156
- Embedded documents often derive access from a parent and do not provide an independent ownership
157
- surface. What each family exposes is described by its own schema and result, not by another
158
- family's behavior.
59
+ The GM browser assigns commands one of three modes: allow, approve, or deny. Its policy covers
60
+ reads, writes, bulk commands, and dry runs. Permissions belong to that browser profile; a second
61
+ paired browser can have a different policy.
62
+
63
+ By default, code execution, setting writes, and user role or permission changes are denied.
64
+ Destructive operations, GM-client reload, and user creation require approval. The Command permissions
65
+ window shows the full current policy. State checks and internal approval controls remain available;
66
+ daemon-side `auth` commands are outside this policy.
67
+
68
+ The module enforces command permissions at dispatch. An approved request passes the normal guards
69
+ again and cannot run if its command was changed to deny while waiting. A force flag acknowledges
70
+ a command's specific guard but does not bypass permissions, hooks, validation, or file containment.
71
+
72
+ Denial, expiry, or confirmed cancellation prevents execution. Approval does not lock world state,
73
+ and the browser stores decisions only in memory. A disconnected caller's request can remain
74
+ actionable until a decision or expiry. Choose an approval timeout that gives the GM enough time
75
+ without leaving forgotten requests open. See [Approval flow](protocol.md#approval-flow) for
76
+ cancellation and lost-state handling.
77
+
78
+ ### What the approval window shows
79
+
80
+ The GM-only window shows the command, targets, parameters, and remaining time. Binary uploads
81
+ appear by size. Text over 16,384 characters, including macro bodies, is replaced by its character
82
+ count. For `macro.execute`, inspect longer bodies in Foundry's macro editor before approving.
83
+ Proposed `macro.create` or `macro.update` content may not exist there yet; inspect it separately
84
+ or decline if the window does not show enough to review.
85
+
86
+ The request carries no caller identity. The GM approves the displayed invocation, but the window
87
+ cannot identify the local person or process that requested it.
88
+ Reading or cancelling a decision requires its opaque approval identifier.
159
89
 
160
90
  ## Executable content
161
91
 
162
- The bridge executes no JavaScript that the GM has not explicitly enabled and cannot see before it
163
- runs. There is no `eval`-style command, and CLI-supplied data cannot create a hidden execution path
164
- through ordinary writes.
165
-
166
- - `macro.execute` is the one way to run code, and it runs only stored world macros. It is denied by
167
- default; a GM who enables it can keep it on approve, where the Command Approval window shows the
168
- macro's type and its command body, up to the length cap described above, before anything runs.
169
- The `macro.create → macro.execute → macro.delete` chain is the sanctioned path for ad-hoc code,
170
- so a GM who wants only vetted macros to run sets `macro.create` and `macro.update` to approve or
171
- deny while `macro.execute` stays enabled. A script macro that throws fails the command with a
172
- structured error naming what the macro raised, and the failure is partial by nature: whatever
173
- the macro changed before it threw stays changed. A macro that catches its own errors still
174
- reports a `null` return, so results do not prove success and effects deserve a read-back.
175
- - Macro bodies and chat content written by ordinary document commands are stored, not executed.
176
- - Action commands invoke only their fixed typed Foundry methods.
177
- - Ordinary region-behavior writes reject core script- or macro-executing types through a shared
178
- guard, including on nested behaviors supplied with a region write. The dedicated
179
- `scene.region.behavior.executable` commands, denied by default, accept exactly the `executeMacro`
180
- type and require the referenced world macro to exist; the approval window names the macro, the
181
- trigger events, and whether the behavior fires for everyone. Those commands accept a behavior's
182
- `system` only in a spelling the guard and that window can both read: a create takes only the plain
183
- `system` object, an update or clone also accepts single-field `system.<field>` paths, and no route
184
- accepts both spellings at once, a path deeper than one field (`system.events.<n>`), a
185
- forced-replacement or forced-deletion operator key, or a read field carrying the wrong type (a
186
- non-array `events`, a non-boolean `everyone`). Each of those could store a macro reference or a
187
- trigger set that differs from the one the macro check validated and the approval window showed. An
188
- `executeMacro` behavior triggers later on player-driven region events, with `everyone: true`
189
- running the macro on every connected client.
190
- - `executeScript` behaviors are not supported on any route: Foundry runs their source in every
191
- connected player's browser with no per-user execution check, which no popup can make reviewable.
192
-
193
- Installed systems and modules remain trusted Foundry code. They can register behavior types, hooks,
194
- ActiveEffect interpretations, or other data-driven features whose effects the bridge cannot classify
195
- universally. Operators must review system/module semantics when authoring content those extensions
196
- interpret.
197
-
198
- A typed Foundry action can also trigger existing GM-authored automation. Combat changes, table draws,
199
- card actions, document hooks, region triggers, and game-system workflows can cause secondary writes
200
- or chat output after the direct command.
92
+ Code execution requires the GM to enable dedicated commands. Ordinary document writes store macro
93
+ bodies without executing them and reject core executable region behavior types.
201
94
 
202
- ## Settings
95
+ ### World macros
203
96
 
204
- Listing returns registration metadata, while reading a value requires an explicit namespace and key,
205
- singly or in a batch. Values are serialized with bounded depth, node count, and byte size, and this
206
- module's own secret-bearing settings are redacted from every read.
207
-
208
- `setting.set` and `setting.set-many` exist because module configuration is a legitimate
209
- administration task, and they are denied by default because settings can alter global security and
210
- runtime behavior and frequently invoke module callbacks: a write to `core.permissions` or
211
- `core.moduleConfiguration` changes what the GM client itself can do or load. Writes go through
212
- Foundry's own registration and DataField validation, and only registered settings are writable. The
213
- approval behavior shows the stored value next to the proposed one before a write runs.
214
-
215
- The command policy stays beyond the CLI's reach by construction: the write commands refuse this
216
- module's namespace with a structured error in every mode, real, dry-run, and per bulk element
217
- alike, so no policy setting, credential, or approval timeout can be changed from the surface the
218
- policy governs. The client-scoped `commandPolicy` and `approvalTimeoutMinutes` settings remain readable
219
- because they contain no secrets, and only a GM using Foundry changes them: the Command permissions
220
- window writes the policy, and the main Module Settings form writes the timeout and enforces its
221
- bounds. Another browser profile or machine has its own values.
97
+ `macro.execute` runs a stored world macro and is denied by default. Approve mode lets the GM review
98
+ each invocation within the window's [text display limit](#what-the-approval-window-shows).
222
99
 
223
- ## Users
100
+ The module captures the macro body and type when approval is requested. The window shows that
101
+ capture, and execution checks it against the stored macro immediately before dispatch. An edit,
102
+ deletion, or missing capture returns `APPROVAL_STALE` without running the macro. Read the current
103
+ macro before requesting fresh approval; follow the [retry rules](protocol.md#delivery-states-and-retries).
224
104
 
225
- User accounts are managed through explicit per-purpose commands rather than one open patch surface.
226
-
227
- - No command reads or writes `password` or `passwordSalt`. Foundry transmits a set password in
228
- clear text and hashes it server-side, so a password path through the bridge would expose secrets
229
- in transcripts and logs; password changes stay in Foundry's own UI.
230
- - `user.update` edits profile fields and is allowed by default because none of them grant
231
- authority; the fields are name, color, pronouns, avatar, assigned character, and flags.
232
- - `user.create` and `user.delete` ask for approval by default. `user.role.set` and
233
- `user.permissions.set` are denied by default because they change who can do what.
234
- - A `user.create` carries whatever role it asks for, up to the caller's own, so an approved one can
235
- mint a gamemaster. That is why it asks for approval rather than running by itself: the approval
236
- window names the account and the role, including the player role Foundry gives when the command
237
- asks for none, and the GM reading it is the review point. A GM who does not want that decision
238
- in the loop at all sets `user.create` to deny.
239
- - The bridge GM's own account is self-protected: `user.role.set`, `user.permissions.set`, and
240
- `user.delete` aimed at the user holding the bridge are refused with a structured error, so
241
- automation cannot demote, remove, or strip the restricted menus from the account it runs through.
242
- A second GM account carries no such guard; deciding about it is exactly what enabling the command
243
- means.
244
- - Foundry's server-side limits stay in force underneath: a role cannot be raised above the caller's
245
- own, and the last gamemaster account can be neither demoted nor deleted. Those refusals surface
246
- as permission errors with Foundry's own message.
247
-
248
- ## Search
249
-
250
- `world.search` indexes selected authored world and optional compendium content in the connected GM
251
- client. Search results can reveal content visible to that GM, including text not visible to ordinary
252
- players.
105
+ Enabling execution also permits a caller to create a macro, execute it, then delete it. To restrict
106
+ execution to vetted macros, set `macro.create` and `macro.update` to approve or deny as well.
253
107
 
254
- Queries and responses are bounded. Search indexes are runtime caches and can be stale until their
255
- invalidation or rebuild completes. Search is a discovery surface, not an authorization boundary.
108
+ A macro timeout does not stop the macro. A thrown error does not undo earlier changes, and a
109
+ macro that catches its errors may return normally. Verify effects before retrying.
256
110
 
257
- ## File write boundary
111
+ ### Executable region behaviors
258
112
 
259
- File commands operate only through Foundry's managed `data` source.
113
+ Ordinary behavior writes, including behaviors nested inside a region, reject core executable
114
+ types. The separate `scene.region.behavior.executable` commands are denied by default and accept
115
+ only `executeMacro` with an existing world macro. The approval window shows the macro reference,
116
+ events, and whether it runs for everyone. Payload guards reject ambiguous representations of
117
+ those fields so validation and review refer to the same values.
260
118
 
261
- Reads address normalized managed-data paths. Writes are restricted to the active world's
262
- `worlds/<worldId>/` tree and exclude:
119
+ These behaviors run on future region events. With `everyone: true`, the macro runs on every
120
+ connected client. Approval authorizes that reference and its future triggers, with no new decision
121
+ for each event. It does not freeze the macro body.
263
122
 
264
- - `world.json`;
265
- - `data/` and all descendants;
266
- - `packs/` and all descendants.
123
+ `executeScript` is unsupported on every route because Foundry executes it in player browsers
124
+ without a per-user execution check.
267
125
 
268
- Containment and exclusions are segment-aware and are checked before payload decoding or capability
269
- dispatch. Path normalization rejects a segment whose literal or percent-decoded form is `.` or `..`,
270
- and a segment whose decoded form contains a path separator (so a percent-encoded `/` or `\` cannot
271
- smuggle traversal inside one segment). Absolute host paths, traversal, sibling-prefix tricks, and
272
- percent-encoded traversal (`%2e%2e`, `..%2f..`) therefore cannot cross the boundary; this applies to
273
- every managed-path caller, including `image show`.
126
+ ### Existing Foundry automation
274
127
 
275
- The module accepts upload content over the authenticated local transport and a managed data-relative
276
- destination. It does not read arbitrary files from the operator machine. The CLI may read an explicit
277
- local source file supplied by the operator, then sends its bytes as data.
128
+ Systems and modules can interpret ActiveEffects, register behavior types, and attach hooks to
129
+ ordinary document changes. Typed actions such as combat transitions, table draws, and card deals
130
+ can trigger existing automation and secondary writes. The bridge cannot classify or undo every
131
+ side effect of trusted Foundry code. This also applies to content imported from compendiums.
278
132
 
279
- File mutation does not rewrite document references. Reference changes require an explicit document
280
- command so their intent and permissions remain visible.
133
+ ## Settings
281
134
 
282
- ## Compendium imports
135
+ Setting writes can change security, loaded modules, or runtime behavior and invoke callbacks.
136
+ Approval shows the stored and proposed values.
283
137
 
284
- Compendium commands are read-only with respect to packs. Supported import commands create a new world
285
- document from a pack entry using a closed, family-compatible override schema.
138
+ Writes to this module's own namespace are forbidden, including in dry runs and bulk operations.
139
+ A command therefore cannot enable itself, change its approval timeout, or overwrite credentials.
140
+ A GM changes these settings in Foundry, separately for each browser profile.
286
141
 
287
- Imports preserve legitimate authored source data after normalization. Installed system or module
288
- content can still carry data-driven behavior; importing is not a security audit of the source pack.
289
- The bridge does not expose arbitrary compendium writes.
142
+ Reads redact this module's secret-bearing settings. Non-secret permission settings remain readable.
290
143
 
291
- ## Availability and resource limits
144
+ ## Users
145
+
146
+ - Passwords and password salts cannot be read or written. Change passwords in Foundry's UI.
147
+ - Creating a user can grant any role up to the caller's own, including GM. Review the role shown
148
+ in the approval window; set `user.create` to deny if automation should not create accounts.
149
+ - The account holding the bridge cannot delete itself or change its own role or permissions.
150
+ Other GM accounts do not have this bridge-specific protection.
151
+ - Foundry also prevents raising a role above the caller's and demoting or deleting the last GM.
152
+
153
+ ## File write boundary
154
+
155
+ File commands use Foundry's managed `data` source. Reads may address managed assets; writes stay
156
+ under the active world's `worlds/<worldId>/` tree and exclude `world.json`, `data/**`, and `packs/**`.
157
+
158
+ Containment checks run before payload decoding and capability dispatch. They examine path segments
159
+ and their percent-decoded forms, rejecting traversal, encoded separators, absolute host paths, and
160
+ sibling-prefix tricks. Managed local paths used by `image show` follow the same normalization rules.
292
161
 
293
- The daemon and module bound uploads, WebSocket frames, search work, batch sizes, and selected result
294
- shapes. Oversized operations return structured errors where possible without dropping the shared
295
- bridge session.
162
+ The CLI may read an explicitly supplied local upload source. It sends bytes and a managed-data
163
+ relative destination; the module cannot read arbitrary operator-machine files. File mutations do
164
+ not rewrite document references. Reference changes require a separate document command.
165
+
166
+ `image show` also accepts off-host HTTP(S) URLs. These make the targeted players' browsers fetch
167
+ and display the remote image; they are outside the managed-file boundary.
168
+
169
+ ## Availability and resource limits
296
170
 
297
- The module limits the number and combined size of approval requests. It refuses an excess request
298
- before display or execution. It does not discard an unread outcome to admit a new request.
171
+ The daemon and module bound uploads, transport frames, search work, batches, and approval queues.
172
+ Oversized requests return structured errors where possible.
299
173
 
300
- The system remains susceptible to ordinary local denial of service by an authorized caller issuing
301
- many expensive Foundry operations. It is designed for cooperative local automation, not hostile
302
- multi-user scheduling.
174
+ These limits do not prevent an authorized caller from exhausting browser resources through many
175
+ expensive operations. The tool assumes cooperative local automation.
303
176
 
304
177
  ## Known risks
305
178
 
306
- - A stolen device credential lets a local process use the active bridge; a stolen bridge credential
307
- lets a matching Origin/world/user runtime authenticate that pairing.
308
- - Another OS user able to reach loopback can submit a pairing request with a forged Origin, client
309
- identifier, and label. Approval therefore trusts that the displayed pending request came from the
310
- operator's own browser.
311
- - The label and the client identifier are the only pairing identity fields constrained against terminal
312
- escape sequences. World title and GM name arrive on the same unauthenticated pairing request without
313
- that constraint, and the CLI prints them as raw text wherever it renders a pending request or a
314
- stored profile: both approval prompts and the `auth prune` candidate listing, which precedes an
315
- irreversible deletion. A local process can therefore redraw the identity lines an operator reads
316
- before answering, and a title that survived one approval is re-rendered from the stored record every
317
- time a later command lists it.
318
- - `auth approve` with no code refuses while several requests are pending, and the `auth` pairing wait
319
- instead renders the earliest one and asks about it. A request a local process forged before the
320
- operator clicked Pair in the browser is therefore the one the wait offers, which is a reason to read
321
- the rendered identity rather than answer the prompt by reflex. Both commands approve only the request
322
- whose identity they displayed.
323
- - A compromised GM browser session or installed Foundry module is already inside the trusted runtime.
324
- - Foundry, systems, and modules can attach hooks and side effects to otherwise ordinary writes.
325
- - Timeouts and disconnects can leave delivery indeterminate; a mutation may have committed.
326
- - Native Foundry batch operations are not transactional and can partially apply.
327
- - Search and read commands can expose GM-visible world content to the local caller.
328
- - Large but permitted content can consume browser memory and processing time.
329
- - An approved command executes when the GM decides, so the world may have changed while it waited.
330
- Approval does not lock world state.
331
- - Decisions waiting for a GM live only in that browser session. Reloading the GM client or ending
332
- its bridge session discards them. Waiting callers receive an indeterminate result.
333
- - A caller that disappears without a confirmed cancellation leaves its request actionable on the
334
- GM's screen until the GM decides or the timeout expires. The command envelope carries no client
335
- identity, so the module cannot tell that the caller is gone.
336
- - The daemon keeps reservations, approval links, and lost-in-flight idempotency keys in one bounded
337
- store shared by its clients. It returns `IDEMPOTENCY_STORE_FULL` instead of evicting an
338
- indeterminate key. Capacity returns when earlier keys settle or expire, or when a daemon restart,
339
- world switch, or pairing switch clears the store.
340
- - Command permissions belong to a browser profile. A second paired browser, or the same browser with
341
- a fresh profile, holds its own permissions, and whichever client holds the bridge is the one whose
342
- permissions apply.
343
- - Declarative content can reference existing executable or module-interpreted content.
344
- - `image show` accepts an arbitrary off-host `http(s)` URL as its source and pushes it to the
345
- targeted players' clients unchanged, so an enabled caller can make those browsers fetch and display
346
- a remote image the operator did not host. Only local sources are confined to the managed data
347
- boundary; a URL source is a display of external content, not a file read.
348
- - An enabled `macro.execute` or executable-behavior command makes the GM's enablement and approval
349
- discipline the effective code-review boundary; an enabled `setting.set` can change core settings
350
- that alter the GM client's own capabilities or take it down until a reload.
351
-
352
- ## Operator guidance
353
-
354
- - Keep the daemon on loopback, protect the per-user config, and review Origin/world/GM before approval.
355
- - Confirm the connected world and GM identity before mutation.
356
- - Use JSON output, dry runs, stable idempotency keys, and post-write reads for automation.
357
- - Review every outcome of a bulk operation.
358
- - Treat forwarded timeouts and disconnects as potentially committed.
359
- - Set the approval timeout to the time a GM realistically needs to answer. A long timeout keeps a
360
- request actionable long after its caller gave up; a short one refuses work the GM would have
361
- approved. Either way the expiry never executes the command.
362
- - Configure command permissions in every browser profile that holds the bridge, and review them
363
- after an update that adds commands.
364
- - Review installed systems and modules before authoring automation-sensitive data.
365
- - Back up important worlds before large migrations.
366
- - Run the live smoke workflow only in a designated test world.
179
+ - A stolen CLI credential permits use of the active bridge. A stolen bridge credential permits
180
+ authentication with the matching pairing identity and Origin.
181
+ - A local process can forge a pairing request's Origin, browser id, and label. Bare `auth` offers
182
+ the earliest pending request, which may precede the operator's own request. Check the displayed
183
+ identity before answering; approval applies only to the request shown.
184
+ - World titles and GM names arrive in unauthenticated pairing requests without terminal-escape
185
+ restrictions. The CLI prints them as raw text in prompts and profile listings, including
186
+ `auth prune`. A forged value can redraw identity lines, and a stored value can do so again later.
187
+
188
+ ## Reporting a vulnerability
189
+
190
+ Suspected vulnerabilities are best reported privately through GitHub security advisories:
191
+ <https://github.com/DroiD16/fvtt-world-cli/security/advisories/new>. A private report leaves time
192
+ to publish a fix before the details are public, so the issue tracker is the wrong place for one.
193
+ Fixes land in the latest release; earlier releases are not patched separately.