feat(teams): the roster's audience projection, and optionalAuth to resolve it
TEAMS.md §3.3, as the eighth member of MODULE_API 1.6.0 — amended in place per the org lead, on the rule Protocol 4 was given in phase 2: a contract owes a bump only once it has landed on `main`. Two questions meet on the roster and they belong to different owners. WHICH ROWS a viewer may see is the module's, because the audience rungs and their configuration live there and core does not know what a rung is. WHAT A ROW LOOKS LIKE stays core's. So `projectRoster` answers with member KEYS, not rows. §3.3 said rows, and rows would let a module widen what is published — handing back a `userId` core had withheld — leaving core's field guarantee resting on every module's good behaviour. Core asks which rows and re-normalises the answer through its own public shape, so a module can narrow and cannot widen. "The module declines" needed splitting before it could be implemented. No module at all and a module whose rungs could not be consulted are opposite situations: the first withholds nothing and must serve the roster whole, the second must serve none of it. The refusal carries `projects`, and only `projects: true` fails closed. Without the split, bare core serves an empty roster on every Team page. This is also the first public route whose CONTENT depends on identity, which needed a middleware core did not have. `attachSession` only decodes a token, so a banned account, a password change or a logout would have kept working against the private half of a feed until the JWT expired. `optionalAuth` runs requireAuth's full database re-validation and, on any failure, continues ANONYMOUSLY rather than rejecting — a caller whose session is no longer good sees the public view, which is what they are entitled to. `GET /public/teams/:slug/activity` lands here for the same reason: §2.11's route table had no activity endpoint though §4.3 describes a filtered feed. Paged, with the visibility resolved from the session and never from a parameter. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -979,6 +979,12 @@ const doc = {
|
||||
properties: {
|
||||
teams: { type: 'array', items: { $ref: '#/components/schemas/PublicTeam' } },
|
||||
total: { type: 'integer', example: 12 },
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether this deployment has Teams at all — a provider is registered, or Teams exist from one that since went away. The `teams` nav feature flag resolves from this; false means bare core, where a Teams link would lead to a permanently empty page.',
|
||||
example: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
PublicTeamMember: {
|
||||
@@ -999,6 +1005,41 @@ const doc = {
|
||||
properties: {
|
||||
members: { type: 'array', items: { $ref: '#/components/schemas/PublicTeamMember' } },
|
||||
rosterSyncedAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
projected: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether the module applied its own audience projection to this roster. False means the module declined or does not project, and the roster was served at core’s public shape — never the full one.',
|
||||
example: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
PublicTeamActivityItem: {
|
||||
type: 'object',
|
||||
description:
|
||||
'`summary` is already-rendered text supplied by whoever pushed the item; core never composes one. `kind` and `payload` are opaque to core — only the module’s `team.overview` slot renders anything richer than the text.',
|
||||
properties: {
|
||||
id: { type: 'integer', example: 4821 },
|
||||
source: { type: 'string', description: '`core` or a module id.', example: 'uo' },
|
||||
kind: { type: 'string', example: 'uo.champion.completed' },
|
||||
summary: { type: 'string', example: 'Completed Champion Neira' },
|
||||
visibility: { type: 'string', enum: ['public', 'members'] },
|
||||
occurredAt: { type: 'string', format: 'date-time' },
|
||||
payload: { type: 'object', nullable: true, additionalProperties: true },
|
||||
},
|
||||
},
|
||||
PublicTeamActivity: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
items: { type: 'array', items: { $ref: '#/components/schemas/PublicTeamActivityItem' } },
|
||||
total: { type: 'integer', description: 'Matching rows for THIS caller’s visibility, so paging is honest.', example: 137 },
|
||||
limit: { type: 'integer', example: 50 },
|
||||
offset: { type: 'integer', example: 0 },
|
||||
scope: {
|
||||
type: 'string',
|
||||
enum: ['public', 'members'],
|
||||
description:
|
||||
'Which visibilities this caller received. `public` means members-only items were withheld — render that fact rather than presenting a filtered feed as the whole one.',
|
||||
},
|
||||
},
|
||||
},
|
||||
PlayerTeamList: {
|
||||
|
||||
Reference in New Issue
Block a user