Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/devices/relationship-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const command = z.discriminatedUnion('action', [
z.object({ action: z.literal('cancel'), id, confirm: z.literal(true) }),
]);

export const RELATIONSHIP_ACTIONS: readonly string[] = command.options.map(
(option) => option.shape.action.value,
);

export function createRelationshipHttp(control: RelationshipControl) {
return async (request: Request): Promise<Response> => {
const reply = (body: unknown, status = 200) =>
Expand Down
50 changes: 50 additions & 0 deletions packages/core/test/device-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ async function fakeDsh(
token: 'launch-token',
seen: [] as Record<string, unknown>[],
upgrades: [] as string[],
mints: 0,
};
const cookie = () => `dsh=${String(state.secret)}`;
let gateway: ReturnType<typeof createDeviceGateway> | undefined;
const server = createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://x');
if (url.pathname.startsWith(DEVICE_GATEWAY_PREFIX)) return void gateway!.handle(req, res);
if (url.pathname === '/' && url.searchParams.get('token') === state.token) {
state.mints++;
res.writeHead(303, { 'set-cookie': `${cookie()}; HttpOnly`, location: '/' });
return res.end();
}
Expand Down Expand Up @@ -317,6 +319,54 @@ it('does not let a paired device manage or inherit Host relationships', async ()
expect(host.pairing.admit(`Device ${paired.credential}`).ok).toBe(true);
});

it('never uses the Owner session for a grant that is not bound to the current Owner', async () => {
let ownerHumanId = OWNER_HUMAN_CONTEXT.humanId;
const host = await fakeDsh(
undefined,
undefined,
undefined,
undefined,
undefined,
() => ownerHumanId,
);
const paired = await pair(host.base, host.pairing.createOffer().offer);
const authorization = `Device ${paired.body.credential}`;
const ticket = (await (
await fetch(`${host.base}/botharness-device/stream-ticket`, {
method: 'POST',
headers: { authorization },
})
).json()) as { ticket: string };
ownerHumanId = 'not-the-owner';
const refused = { code: 'device-revoked' };
const unary = await fetch(`${host.base}/botharness-device/api/botharness/list`, {
method: 'POST',
headers: { authorization, 'content-type': 'application/json' },
body: '{"type":"client-request"}',
});
expect([unary.status, await unary.json()]).toEqual([401, refused]);
const avatar = await fetch(
`${host.base}/botharness-device/api/botharness/bot-avatar?slug=ada&v=portrait-1`,
{ headers: { authorization } },
);
expect([avatar.status, await avatar.json()]).toEqual([401, refused]);
const issued = await fetch(`${host.base}/botharness-device/stream-ticket`, {
method: 'POST',
headers: { authorization },
});
expect([issued.status, await issued.json()]).toEqual([401, refused]);
expect(await stream(host.port, { authorization })).toEqual({
status: 401,
body: JSON.stringify(refused),
});
expect(
await stream(host.port, {}, `/botharness-device/api/remote.mux?ticket=${ticket.ticket}`),
).toEqual({ status: 401, body: JSON.stringify(refused) });
expect(host.state.mints).toBe(0);
expect(host.state.seen).toEqual([]);
expect(host.state.upgrades).toEqual([]);
});

it('attributes Feed acknowledgement to the authenticated device and refuses revoked or missing credentials', async () => {
const host = await fakeDsh();
const paired = host.pairing.redeem({ offer: host.pairing.createOffer().offer });
Expand Down
6 changes: 5 additions & 1 deletion packages/core/test/team-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createAttachmentStore } from '../src/attachments/store.js';
import { DatabaseSync } from 'node:sqlite';
import { BOT_HARNESS_SCHEMA_PLAN } from '../src/database/schema-plan.js';
import { createDevicePairing } from '../src/devices/pairing.js';
import { RELATIONSHIP_ACTIONS } from '../src/devices/relationship-http.js';
import { createServer, IncomingMessage, request as httpRequest } from 'node:http';
import { Socket } from 'node:net';
import { readFileSync } from 'node:fs';
Expand Down Expand Up @@ -1185,7 +1186,10 @@ describe('member HTTP carrier', { timeout: 60_000 }, () => {
const http = await memberHttp(f);
const cookie =
(await http.signIn('bob', 'member-password-1')).headers.get('set-cookie') ?? '';
for (const action of ['list', 'initiate', 'approve', 'sync', 'enroll', 'disconnect']) {
expect(RELATIONSHIP_ACTIONS).toEqual(
expect.arrayContaining(['initiate', 'approve', 'enroll', 'disconnect']),
);
for (const action of ['list', ...RELATIONSHIP_ACTIONS]) {
const response = await fetch(`${http.origin}${TEAM_PATH}/api/botharness/relationships`, {
method: action === 'list' ? 'GET' : 'POST',
headers: { cookie, 'content-type': 'application/json' },
Expand Down
Loading