asobi_player_export (asobi v0.84.0)

View Source

Everything 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

run(PlayerId)

-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.