Skip to content

fix(apikey): a scope target is not always in universeIds - #38

Merged
dev-bap merged 1 commit into
mainfrom
fix/scope-targets-beyond-universeids
Aug 27, 2026
Merged

fix(apikey): a scope target is not always in universeIds#38
dev-bap merged 1 commit into
mainfrom
fix/scope-targets-beyond-universeids

Conversation

@dev-bap

@dev-bap dev-bap commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes #37.

The report is exact, including the diagnosis. IntrospectScope read universeIds and nothing else, and a scope whose target it could not find fell through to the * that target_parts uses for "no target at all". That is what produced the mirrored pair: missing on the universe, unasked on *.

The three shapes

Field Seen on Read as
universeIds: [id] universe, universe-places, universe-datastores.control the id
universeDatastores: [{universeId}] universe-datastores.objects, .versions the id
groupIds: [id] asset G<id>, the spelling it was sent with

userIds is handled the same way as groupIds. No capture shows it, but a creator target is sent as U<id>, and the guard below catches the spelling if it turns out to be another.

Ids are read whether they arrive as a string or a number. That is the same target written differently rather than a shape nobody understands, and refusing it would skip the verification over punctuation.

The part that is not about these three fields

The bug is not really "one field was missing". It is that an unread target became a confident wrong answer, which is the failure already written at the top of this module: filter_map(...ok()) turning "not understood" into "this key grants nothing".

So a non-empty array under a field name this build does not know is now an error, not a wildcard. A fourth shape surfaces as introspect answered in a shape this build cannot read and the verification is skipped, which is honest. Scalars and empty arrays are left alone: refusing to verify a key over an extra boolean would be the same overreach pointing the other way.

That error reaches nothing that can fail a command. create prints it and returns, and diagnostics::introspect_scopes already documents its Err as "unavailable", not "broken".

What this does not fix

A key scoped to a named data store is sent with two target parts, [universe_id, name]. Nothing shows how that comes back. The same guard reports it as universeDatastores[].datastoreName rather than dropping the name, because dropping it would verify a narrow key against a wide request and call it a match. So those keys move from a false green to no verification. If one can be created against live Open Cloud, that response is a few lines to add.

Worth flagging separately: permissions() treats every entry of target_parts as its own target, so a two-part datastore target is already counted as two. Left alone here, but it means a "target" is not quite the same thing on both sides of the comparison.

Tests

The fixture is the response from the issue, with the counter-example the same key carried.

  • a universeDatastores target is the universe it names, not *
  • the control and objects entries of one key agree on their universe
  • a groupIds target is restored to G<id>, and userIds to U<id>
  • an id may arrive as a number
  • an unknown field holding a non-empty array is reported, naming the field and the scope
  • a named data store inside the target is reported rather than dropped
  • an extra scalar is not a target

Plus one at the create level that runs the issue's request against the issue's response and asserts both warning lists are empty.

The post-create check announced that a scope had not been stored on its
universe, and that the same scope had appeared unasked on `*`, for a key
Roblox had stored exactly as requested. The two warnings mirrored each
other, which is the signature of a target that was not read rather than a
permission that was not stored.

Roblox returns targets under at least three field names, and which one
follows the scope: `universeIds` for `universe` and
`universe-datastores.control`, `universeDatastores: [{universeId}]` for
`universe-datastores.objects` and `.versions`, `groupIds` for a
creator-targeted scope such as `asset`. That is why one key carried both
the bug and its counter-example. The reader took `universeIds` alone, and
a scope whose target it could not find fell through to the `*` that means
"no target at all", which produced the second warning.

Cosmetic in effect, not in consequence: the check exists to catch a key
that cannot do its job, and one that cries wolf on a correct key is how a
real warning gets ignored.

A target under a field this build does not know is now reported rather
than read as a wildcard. That is the rule the module already applied to an
unreadable entry, and the reason is the same one written at the top of it:
what makes this class of bug survive is a reader that turns "not
understood" into a confident wrong answer. A fourth shape now surfaces as
"this build cannot read the answer".

The same guard covers a data store named inside the target. A key scoped
to one store is sent with two target parts, no capture shows how that
comes back, and dropping the name would verify a narrow key against a wide
request and call it a match.
@dev-bap
dev-bap merged commit 9970e64 into main Aug 27, 2026
10 checks passed
@dev-bap
dev-bap deleted the fix/scope-targets-beyond-universeids branch August 27, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

apikey create reports false scope drift for non-universeIds targets

1 participant