feat(threads): channel backend — discover (browse), public self-join, leave
The generic primitives channels need beyond dm/group, kept policy-governed and scope-fenced (kernel never interprets 'channel'/'public'): - discoverThreads(principal, filter): scope-wide browse (NOT membership-scoped) matching an opaque metadata filter, each result flagged joined; governed by iios.thread.discover (scope fence in the query). REST: GET /v1/threads/discover - open self-join for PUBLIC channels: openThread now passes visibility into the join decision; dev-OPA iios.thread.join allows membership=channel+visibility= public (dm/group/private-channel stay invite-only) - leaveThread + iios.thread.leave + REST DELETE /v1/threads/:id/me - tests: public self-join vs private denied, discover joined-flags + group excluded + leave; dev-opa join-public/discover/leave rules (29 green) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,20 @@ describe('DevOpaPort (dev policy plane — membership rules)', () => {
|
||||
expect(big.obligations[0]?.reason).toMatch(/too large/);
|
||||
});
|
||||
|
||||
it('a PUBLIC channel allows open self-join; private channel and group do not', async () => {
|
||||
expect((await opa.decide({ action: 'iios.thread.join', membership: 'channel', visibility: 'public', alreadyMember: false })).allow).toBe(true);
|
||||
const priv = await opa.decide({ action: 'iios.thread.join', membership: 'channel', visibility: 'private', alreadyMember: false });
|
||||
expect(priv.allow).toBe(false);
|
||||
expect((await opa.decide({ action: 'iios.thread.join', membership: 'group', alreadyMember: false })).allow).toBe(false);
|
||||
// an existing member of a private channel can still (re)open it
|
||||
expect((await opa.decide({ action: 'iios.thread.join', membership: 'channel', visibility: 'private', alreadyMember: true })).allow).toBe(true);
|
||||
});
|
||||
|
||||
it('discover and leave are allowed (scope fence / member-implied)', async () => {
|
||||
expect((await opa.decide({ action: 'iios.thread.discover' })).allow).toBe(true);
|
||||
expect((await opa.decide({ action: 'iios.thread.leave', membership: 'channel' })).allow).toBe(true);
|
||||
});
|
||||
|
||||
it('self-join is governed only on membership threads', async () => {
|
||||
// generic / support thread (no membership attr) → open join, unchanged
|
||||
expect((await opa.decide({ action: 'iios.thread.join', alreadyMember: false })).allow).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user