asobi_player_export (asobi v0.84.0)
View SourceEverything core holds about one player, as one map.
The read half of asobi_player_erase, and it covers the same tables in the
same order, so "what would erasure delete" and "what would export return" stay
one list rather than two that drift.
Positive allowlists, never maps:without/2
Every row goes through a maps:with/2 projection, modelled on
asobi_ops_players:project/1. players carries hashed_password, so a
subtractive filter is one schema field away from exporting a credential;
player_tokens carries live bearer tokens, so the export reports that a
session existed and when, never the token itself. A guest identity's
provider_metadata is the device-secret verifier, so only its revoked flag
is exported.
The two tables with no foreign key
match_records.players is a jsonb list of ids and votes.votes_cast is a
jsonb object keyed by voter id. Neither has a foreign key, and both are records
about a player whatever the schema says about referential integrity, so both
are included - by jsonb containment and jsonb key existence respectively.
votes is projected differently from every other table here: the row holds
everybody's votes, so exporting votes_cast whole would hand one player the
private choices of everyone else in the match. Only this player's own choice
is lifted out. A privacy feature that leaks is worse than one that is missing.
zone_snapshots.entities is deliberately absent. It is opaque game-defined
state whose shape core does not know - a game may key entities by player id or
bury one inside entity state, and there is no projection core could apply to it
that would not either miss the data or export another player's. A game that
puts personal data there owns exporting it, the same way it owns erasing it.
Extensions
The payload's extensions key names every installed extension: the ones
whose asobi_extension:export_player/1 contributed data, and the ones
without the callback under a skipped marker. The callback could only ship
under that condition. asobi_extension:erase_player/1 earns its keep
because the foreign key physically forces an author to answer it; an export
callback has no such forcing function, so the artefact itself is the forcing
function - a skipped extension is a visible marker in the export a data
subject or auditor reads, not an absence nobody can detect.
A failing export_player/1 fails the whole export -
{error, {extension_export, Name, Reason}}, no artefact - because an
extension that promised data and could not deliver it is exactly the silently
incomplete export the marker exists to prevent. asobi_extension_export is
the walker, and its reads run in the same untransacted pass as core's own
sections: core's export takes no transaction, so wrapping only the extension
walk in one would buy consistency with nothing.
Summary
Functions
Every core row that names PlayerId, projected, plus every installed
extension's own sections.
Functions
-spec run(binary()) -> {ok, map()} | {error, not_found | {extension_export, asobi_extension:name(), term()}}.
Every core row that names PlayerId, projected, plus every installed
extension's own sections.
{error, not_found} when no such player exists - an empty export and a
missing account are different answers to a data-subject request.
{error, {extension_export, Name, Reason}} when an installed extension
failed to export: no partial artefact is produced.