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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ data/*.tmp
data/workspaces/
data/.pid

# Goal sessions database and its generated auth secret (ALIGN_GOAL_SESSIONS=1)
data/v2/

# Sparta local runtime (worlds, dashboard state)
.worlds/
.worlds-dashboard/
Expand Down
15 changes: 15 additions & 0 deletions app/api/v2/auth/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { connection } from "next/server";
import { getAuth } from "@/lib/v2/auth";
import { ready } from "@/lib/v2/init";

// Better Auth's endpoints (sign-in, OAuth callbacks, session, sign-out) under /api/v2/auth.
// A device token is never a browser session: any Authorization header is refused here too.
async function handle(req: Request): Promise<Response> {
await connection();
if (req.headers.get("authorization") !== null) return Response.json({ error: "bearer_not_allowed", message: "Web routes do not accept device tokens" }, { status: 403 });
await ready();
return getAuth().handler(req);
}

export const GET = handle;
export const POST = handle;
10 changes: 10 additions & 0 deletions app/api/v2/device/approve/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { decideRequest } from "@/lib/v2/devices";
import { readJson, route, webWrite } from "@/lib/v2/http";
import { str, strList } from "@/lib/v2/validate";

// POST /api/v2/device/approve (cookie) { user_code, workspace_ids }: grant this device the chosen workspaces.
export const POST = route(async (req) => {
const p = await webWrite(req);
const b = await readJson(req, 8192);
return decideRequest(p.user.id, str(b, "user_code", 20), "approve", strList(b, "workspace_ids", { max: 50, itemMax: 64 }));
});
10 changes: 10 additions & 0 deletions app/api/v2/device/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { authorize } from "@/lib/v2/devices";
import { clientKey, originOf, readJson, rejectBrowser, route } from "@/lib/v2/http";
import { b64url, str } from "@/lib/v2/validate";

// POST /api/v2/device/authorize (no auth; 10 per hour per client): start pairing a device key.
export const POST = route(async (req) => {
rejectBrowser(req);
const b = await readJson(req, 4096);
return authorize({ device_name: str(b, "device_name", 100), public_key: b64url(b, "public_key", 43, 43) }, clientKey(req), originOf(req));
});
10 changes: 10 additions & 0 deletions app/api/v2/device/deny/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { decideRequest } from "@/lib/v2/devices";
import { readJson, route, webWrite } from "@/lib/v2/http";
import { str } from "@/lib/v2/validate";

// POST /api/v2/device/deny (cookie) { user_code }
export const POST = route(async (req) => {
const p = await webWrite(req);
const b = await readJson(req, 4096);
return decideRequest(p.user.id, str(b, "user_code", 20), "deny", []);
});
10 changes: 10 additions & 0 deletions app/api/v2/device/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { refresh } from "@/lib/v2/devices";
import { readJson, rejectBrowser, route } from "@/lib/v2/http";
import { b64url, str } from "@/lib/v2/validate";

// POST /api/v2/device/refresh: proof of possession (Ed25519 over the exact message), rotating refresh token.
export const POST = route(async (req) => {
rejectBrowser(req);
const b = await readJson(req, 4096);
return refresh({ device_id: str(b, "device_id", 64), refresh_token: str(b, "refresh_token", 200), ts: str(b, "ts", 40), sig: b64url(b, "sig", 86, 88) });
});
8 changes: 8 additions & 0 deletions app/api/v2/device/requests/[code]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { lookupRequest } from "@/lib/v2/devices";
import { route, webAuth } from "@/lib/v2/http";

// GET /api/v2/device/requests/:user_code (cookie): the pending device behind a code, for /app/device.
export const GET = route<{ code: string }>(async (req, { code }) => {
const p = await webAuth(req);
return lookupRequest(p.user.id, code);
});
12 changes: 12 additions & 0 deletions app/api/v2/device/token/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { token } from "@/lib/v2/devices";
import { badRequest } from "@/lib/v2/errors";
import { readJson, rejectBrowser, route } from "@/lib/v2/http";
import { b64url } from "@/lib/v2/validate";

// POST /api/v2/device/token: the agent polls until the user approves; 200 exactly once.
export const POST = route(async (req) => {
rejectBrowser(req);
const b = await readJson(req, 4096);
if (b.grant_type !== "urn:ietf:params:oauth:grant-type:device_code") throw badRequest("unsupported grant_type", "unsupported_grant_type");
return token(b64url(b, "device_code", 43, 128));
});
13 changes: 13 additions & 0 deletions app/api/v2/devices/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { deviceOwnedBy, revokeDevice } from "@/lib/v2/devices";
import { notFound } from "@/lib/v2/errors";
import { idParam, noContent, route, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";

// DELETE /api/v2/devices/:id (cookie): revoke one of my devices.
export const DELETE = route<{ id: string }>(async (req, params) => {
const p = await webWrite(req);
const id = idParam(params.id, ID_RE);
if (!deviceOwnedBy(id, p.user.id)) throw notFound();
await revokeDevice(id, "revoked_on_web");
return noContent();
});
8 changes: 8 additions & 0 deletions app/api/v2/devices/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { listDevices } from "@/lib/v2/devices";
import { route, webAuth } from "@/lib/v2/http";

// GET /api/v2/devices (cookie): my paired devices.
export const GET = route(async (req) => {
const p = await webAuth(req);
return { devices: listDevices(p.user.id) };
});
9 changes: 9 additions & 0 deletions app/api/v2/devices/self/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { revokeDevice } from "@/lib/v2/devices";
import { deviceAuth, noContent, route } from "@/lib/v2/http";

// DELETE /api/v2/devices/self (Bearer): log this device out (revokes all its tokens).
export const DELETE = route(async (req) => {
const d = deviceAuth(req);
await revokeDevice(d.deviceId, "logout");
return noContent();
});
10 changes: 10 additions & 0 deletions app/api/v2/directives/[id]/receipts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RECEIPT_STATES, recordReceipt } from "@/lib/v2/directives";
import { deviceAuth, idParam, readJson, route } from "@/lib/v2/http";
import { ID_RE, iso, oneOf, str } from "@/lib/v2/validate";

// POST /api/v2/directives/:id/receipts (Bearer, at most 2 KB) { session_key, state, at }: idempotent.
export const POST = route<{ id: string }>(async (req, params) => {
const dev = deviceAuth(req);
const b = await readJson(req, 2048);
return recordReceipt(dev, idParam(params.id, ID_RE), str(b, "session_key", 43), oneOf(b, "state", RECEIPT_STATES), iso(b, "at"));
});
10 changes: 10 additions & 0 deletions app/api/v2/directives/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { directivesFor, parseCursor } from "@/lib/v2/directives";
import { deviceAuth, route } from "@/lib/v2/http";

// GET /api/v2/directives?cursor=<seq> (Bearer): up to 100 directives for this device's sessions.
export const GET = route(async (req) => {
const dev = deviceAuth(req);
const cursor = parseCursor(new URL(req.url).searchParams.get("cursor"));
const directives = directivesFor(dev, cursor, 100);
return { directives, next_cursor: String(directives.length ? directives[directives.length - 1].seq : cursor) };
});
8 changes: 8 additions & 0 deletions app/api/v2/invites/[token]/accept/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { route, webWrite } from "@/lib/v2/http";
import { acceptInvite } from "@/lib/v2/workspaces";

// POST /api/v2/invites/:token/accept (cookie): join, if the signed-in email is the invited one.
export const POST = route<{ token: string }>(async (req, { token }) => {
const p = await webWrite(req);
return acceptInvite(token, p.user, p.emailVerified);
});
5 changes: 5 additions & 0 deletions app/api/v2/invites/[token]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { route, webAuth } from "@/lib/v2/http";
import { previewInvite } from "@/lib/v2/workspaces";

// GET /api/v2/invites/:token (cookie): which workspace and role, and whether my email matches.
export const GET = route<{ token: string }>(async (req, { token }) => previewInvite(token, (await webAuth(req)).user));
12 changes: 12 additions & 0 deletions app/api/v2/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { eitherAuth, route } from "@/lib/v2/http";
import { workspacesFor } from "@/lib/v2/workspaces";

// GET /api/v2/me (Bearer or cookie): who this is, the device (Bearer only), and the workspaces in reach.
export const GET = route(async (req) => {
const p = await eitherAuth(req);
return {
user: p.user,
device: p.kind === "device" ? { id: p.deviceId, name: p.deviceName } : null,
workspaces: workspacesFor(p),
};
});
19 changes: 19 additions & 0 deletions app/api/v2/workspaces/[ws]/activity/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { activityFor, ingestActivity } from "@/lib/v2/activity";
import { deviceAuth, idParam, readJson, route } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST (Bearer) { items: [...] }: this device's in-flight activity, at most 200 items and 128 KB.
export const POST = route<{ ws: string }>(async (req, params) => {
const dev = deviceAuth(req);
const pp = enter(dev, idParam(params.ws, ID_RE));
return ingestActivity(pp, dev, await readJson(req, 128 * 1024));
});

// GET (Bearer) ?after=<seq>: unexpired activity on goals this user may read, oldest first, at most 500.
export const GET = route<{ ws: string }>(async (req, params) => {
const dev = deviceAuth(req);
const pp = enter(dev, idParam(params.ws, ID_RE));
const after = Number(new URL(req.url).searchParams.get("after") || 0);
return activityFor(pp, Number.isSafeInteger(after) && after >= 0 ? after : 0);
});
10 changes: 10 additions & 0 deletions app/api/v2/workspaces/[ws]/alignments/[id]/[action]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { actOnAlignment, assertAction } from "@/lib/v2/alignments";
import { idParam, readJson, route, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST .../alignments/:id/{approve|counter|accept_counter|reject|withdraw|revoke} (cookie). approve carries revision.
export const POST = route<{ ws: string; id: string; action: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
return actOnAlignment(pp, idParam(params.id, ID_RE), assertAction(params.action), await readJson(req, 16 * 1024));
});
7 changes: 7 additions & 0 deletions app/api/v2/workspaces/[ws]/alignments/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getAlignment } from "@/lib/v2/alignments";
import { idParam, route, webAuth } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// GET (cookie): one request I am part of.
export const GET = route<{ ws: string; id: string }>(async (req, params) => getAlignment(enter(await webAuth(req), idParam(params.ws, ID_RE)), idParam(params.id, ID_RE)));
12 changes: 12 additions & 0 deletions app/api/v2/workspaces/[ws]/alignments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createAlignment, listAlignments } from "@/lib/v2/alignments";
import { idParam, readJson, route, webAuth, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// GET (cookie): the requests I am part of. POST { target_goal_id, source_goal_id, choice, session_keys, note }.
export const GET = route<{ ws: string }>(async (req, params) => listAlignments(enter(await webAuth(req), idParam(params.ws, ID_RE))));

export const POST = route<{ ws: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
return Response.json(await createAlignment(pp, await readJson(req, 16 * 1024)), { status: 201 });
});
11 changes: 11 additions & 0 deletions app/api/v2/workspaces/[ws]/goals/[goal]/review/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { confirmParent } from "@/lib/v2/goals";
import { idParam, readJson, route, webWrite } from "@/lib/v2/http";
import { ID_RE, str } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST (cookie, the goal's owner) { parent_id }: this goal still fits the parent's current version.
export const POST = route<{ ws: string; goal: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
const b = await readJson(req, 4096);
return confirmParent(pp, idParam(params.goal, ID_RE), idParam(str(b, "parent_id", 80), ID_RE));
});
7 changes: 7 additions & 0 deletions app/api/v2/workspaces/[ws]/goals/[goal]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { goalDetail } from "@/lib/v2/goals";
import { idParam, route, webAuth } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// GET (cookie): one goal, its versions and what contributes to it.
export const GET = route<{ ws: string; goal: string }>(async (req, params) => goalDetail(enter(await webAuth(req), idParam(params.ws, ID_RE)), idParam(params.goal, ID_RE)));
10 changes: 10 additions & 0 deletions app/api/v2/workspaces/[ws]/goals/[goal]/versions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { newGoalVersion } from "@/lib/v2/goals";
import { idParam, readJson, route, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST (cookie, the goal's owner) { version, title?, measures?, anchors?, status? }: a new immutable version.
export const POST = route<{ ws: string; goal: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
return Response.json(await newGoalVersion(pp, idParam(params.goal, ID_RE), await readJson(req, 32 * 1024)), { status: 201 });
});
21 changes: 21 additions & 0 deletions app/api/v2/workspaces/[ws]/goals/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createGoal, goalsFeed } from "@/lib/v2/goals";
import { eitherAuth, idParam, readJson, route, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// GET (Bearer or cookie): the goals feed, with ETag / If-None-Match -> 304.
export const GET = route<{ ws: string }>(async (req, params) => {
const pp = enter(await eitherAuth(req), idParam(params.ws, ID_RE));
const feed = goalsFeed(pp);
const etag = `"${feed.version}"`;
const headers = { etag, "cache-control": "private, no-cache" };
const inm = (req.headers.get("if-none-match") || "").split(",").map((t) => t.trim().replace(/^W\//, ""));
if (inm.includes(etag) || inm.includes("*")) return new Response(null, { status: 304, headers });
return Response.json(feed, { headers });
});

// POST (cookie) { title, measures, visibility, pod_id, anchors }: a new goal owned by me.
export const POST = route<{ ws: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
return Response.json(await createGoal(pp, await readJson(req, 32 * 1024)), { status: 201 });
});
9 changes: 9 additions & 0 deletions app/api/v2/workspaces/[ws]/invites/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { idParam, noContent, route, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { deleteInvite, enter } from "@/lib/v2/workspaces";

// DELETE (owners): withdraw an invite that hasn't been accepted.
export const DELETE = route<{ ws: string; id: string }>(async (req, params) => {
await deleteInvite(enter(await webWrite(req), idParam(params.ws, ID_RE)), idParam(params.id, ID_RE));
return noContent();
});
13 changes: 13 additions & 0 deletions app/api/v2/workspaces/[ws]/invites/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { idParam, readJson, route, webAuth, webWrite } from "@/lib/v2/http";
import { WS_ROLES } from "@/lib/v2/policy";
import { ID_RE, oneOf, str } from "@/lib/v2/validate";
import { createInvite, enter, listInvites } from "@/lib/v2/workspaces";

// GET (owners): open and accepted invites. POST { email, role } (owners): an invite link to send yourself.
export const GET = route<{ ws: string }>(async (req, params) => ({ invites: listInvites(enter(await webAuth(req), idParam(params.ws, ID_RE))) }));

export const POST = route<{ ws: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
const b = await readJson(req, 4096);
return Response.json(await createInvite(pp, str(b, "email", 254), oneOf(b, "role", WS_ROLES)), { status: 201 });
});
9 changes: 9 additions & 0 deletions app/api/v2/workspaces/[ws]/pods/[pod]/members/[user]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { idParam, noContent, route, webWrite } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter, removePodMember } from "@/lib/v2/workspaces";

// DELETE (workspace owners or the pod's lead): remove a pod member.
export const DELETE = route<{ ws: string; pod: string; user: string }>(async (req, params) => {
await removePodMember(enter(await webWrite(req), idParam(params.ws, ID_RE)), idParam(params.pod, ID_RE), idParam(params.user, ID_RE));
return noContent();
});
11 changes: 11 additions & 0 deletions app/api/v2/workspaces/[ws]/pods/[pod]/members/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { idParam, readJson, route, webWrite } from "@/lib/v2/http";
import { POD_ROLES } from "@/lib/v2/policy";
import { ID_RE, oneOf, str } from "@/lib/v2/validate";
import { enter, setPodMember } from "@/lib/v2/workspaces";

// POST (workspace owners or the pod's lead) { user_id, role: lead|member }: add or change a pod member.
export const POST = route<{ ws: string; pod: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
const b = await readJson(req, 4096);
return setPodMember(pp, idParam(params.pod, ID_RE), str(b, "user_id", 64), oneOf(b, "role", POD_ROLES));
});
10 changes: 10 additions & 0 deletions app/api/v2/workspaces/[ws]/pods/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { idParam, readJson, route, webWrite } from "@/lib/v2/http";
import { ID_RE, str } from "@/lib/v2/validate";
import { createPod, enter } from "@/lib/v2/workspaces";

// POST /api/v2/workspaces/:ws/pods (owners) { name }
export const POST = route<{ ws: string }>(async (req, params) => {
const pp = enter(await webWrite(req), idParam(params.ws, ID_RE));
const b = await readJson(req, 4096);
return Response.json(await createPod(pp, str(b, "name", 100)), { status: 201 });
});
6 changes: 6 additions & 0 deletions app/api/v2/workspaces/[ws]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { idParam, route, webAuth } from "@/lib/v2/http";
import { ID_RE } from "@/lib/v2/validate";
import { enter, workspaceDetail } from "@/lib/v2/workspaces";

// GET /api/v2/workspaces/:ws (cookie): name, my role, members, pods.
export const GET = route<{ ws: string }>(async (req, params) => workspaceDetail(enter(await webAuth(req), idParam(params.ws, ID_RE))));
11 changes: 11 additions & 0 deletions app/api/v2/workspaces/[ws]/sessions/[key]/epochs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { deviceAuth, idParam, readJson, route } from "@/lib/v2/http";
import { openBindingEpoch } from "@/lib/v2/sessions";
import { ID_RE, SESSION_KEY_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST (Bearer): open a binding epoch (closes the previous one at starts_at).
export const POST = route<{ ws: string; key: string }>(async (req, params) => {
const dev = deviceAuth(req);
const pp = enter(dev, idParam(params.ws, ID_RE));
return openBindingEpoch(pp, dev, idParam(params.key, SESSION_KEY_RE), await readJson(req, 4096));
});
9 changes: 9 additions & 0 deletions app/api/v2/workspaces/[ws]/sessions/[key]/resume/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { idParam, route, webWrite } from "@/lib/v2/http";
import { setAgentStop } from "@/lib/v2/sessions";
import { ID_RE, SESSION_KEY_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST (cookie; the session's owner or a workspace owner): clear the agent's local stop latch (resume directive).
export const POST = route<{ ws: string; key: string }>(async (req, params) =>
setAgentStop(enter(await webWrite(req), idParam(params.ws, ID_RE)), idParam(params.key, SESSION_KEY_RE), false),
);
17 changes: 17 additions & 0 deletions app/api/v2/workspaces/[ws]/sessions/[key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { deviceAuth, eitherWrite, idParam, noContent, readJson, route } from "@/lib/v2/http";
import { deleteSession, putSession } from "@/lib/v2/sessions";
import { ID_RE, SESSION_KEY_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// PUT (Bearer): share or update a session. DELETE (Bearer or cookie, owner): hard-delete its shared history.
export const PUT = route<{ ws: string; key: string }>(async (req, params) => {
const dev = deviceAuth(req);
const pp = enter(dev, idParam(params.ws, ID_RE));
return putSession(pp, dev, idParam(params.key, SESSION_KEY_RE), await readJson(req, 8192));
});

export const DELETE = route<{ ws: string; key: string }>(async (req, params) => {
const p = await eitherWrite(req);
await deleteSession(enter(p, idParam(params.ws, ID_RE)), p, idParam(params.key, SESSION_KEY_RE));
return noContent();
});
10 changes: 10 additions & 0 deletions app/api/v2/workspaces/[ws]/sessions/[key]/stop-sharing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { eitherWrite, idParam, route } from "@/lib/v2/http";
import { stopSharing } from "@/lib/v2/sessions";
import { ID_RE, SESSION_KEY_RE } from "@/lib/v2/validate";
import { enter } from "@/lib/v2/workspaces";

// POST (Bearer or cookie, owner): stop sharing from now on; no more uploads are accepted.
export const POST = route<{ ws: string; key: string }>(async (req, params) => {
const p = await eitherWrite(req);
return stopSharing(enter(p, idParam(params.ws, ID_RE)), p, idParam(params.key, SESSION_KEY_RE));
});
Loading
Loading