feat(cloud): workspace numbers (W-123), org slug links and synced sidebar prefs on the control plane (G13, G14)

This commit is contained in:
Neil
2026-11-10 19:22:39 -08:00
parent ff845fe03c
commit 46b467d847
7 changed files with 529 additions and 27 deletions
@@ -0,0 +1,40 @@
-- G14: every cloud workspace gets a short number in its account (W-123), allocated from a per-account
-- counter so a number is never reused, even after the row is destroyed. Orgs get a URL slug for links
-- (orca://cloud/<slug>/W-123). G13: each member's sidebar prefs for cloud workspaces (pins, their
-- order, and the order of sections) live on the account so every device shows the same sidebar.
ALTER TABLE sandboxes ADD COLUMN number INTEGER;
-- Backfill: existing rows numbered by creation order within their account.
UPDATE sandboxes SET number = (
SELECT COUNT(*) FROM sandboxes AS earlier
WHERE earlier.account_id = sandboxes.account_id
AND (earlier.created_at < sandboxes.created_at OR (earlier.created_at = sandboxes.created_at AND earlier.id <= sandboxes.id))
);
CREATE UNIQUE INDEX sandboxes_account_number ON sandboxes (account_id, number) WHERE number IS NOT NULL;
CREATE TABLE org_workspace_counters (
account_id TEXT PRIMARY KEY,
last_number INTEGER NOT NULL
);
INSERT INTO org_workspace_counters (account_id, last_number)
SELECT account_id, MAX(number) FROM sandboxes GROUP BY account_id;
CREATE TABLE org_slugs (
account_id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
created_by TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE workspace_prefs (
account_id TEXT NOT NULL,
user_id TEXT NOT NULL,
document TEXT NOT NULL,
revision INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
updated_by_device TEXT,
PRIMARY KEY (account_id, user_id)
);
+34 -3
View File
@@ -24,7 +24,8 @@
// POST /v1/device-sessions | GET | DELETE /:id per-phone access (see device-sessions.ts)
// Phones sign their own EdDSA tokens (device-sessions.ts) and act as their user, except that they
// cannot enroll devices, manage secrets or connect GitHub.
// /v1/team/workspaces[/:id[/presence|wake|lease|grants[/:grantId]]], /v1/team/links/resolve, /v1/team/prompts
// /v1/team/workspaces[/:id[/presence|wake|lease|grants[/:grantId]]], /v1/team/links/resolve, /v1/team/prompts,
// /v1/team/prefs (src/workspace-prefs.ts), /v1/team/org (src/org-slugs.ts)
// team workspace registry, presence, per-device grants (src/workspaces.ts, src/grants.ts)
// POST /v1/team/workspaces/:id/wake?wait=30s: any member who can open it wakes it (single-flight,
// billed to the account, counted on the owner's seat like every wake) -> {state, endpoint: null, …}
@@ -186,7 +187,10 @@ import {
userAuthorizeUrl
} from './github-user-tokens'
import { MEMBER_GIT_WINDOW_MS, memberGitRoutes, memberSentWithin, recordMemberSend } from './member-git'
import { getWorkspacePrefs, putWorkspacePrefs } from './workspace-prefs'
import { getOrgSlug, putOrgSlug } from './org-slugs'
import {
LinkOtherOrgError,
PatchSchema,
PresenceSchema,
WORKSPACE_ERROR_STATUS,
@@ -1480,7 +1484,9 @@ export async function handleRequest(request: Request, env: Env, deps: Deps = {})
url.pathname === '/v1/team/workspaces' ||
url.pathname.startsWith('/v1/team/workspaces/') ||
url.pathname === '/v1/team/links/resolve' ||
url.pathname === '/v1/team/prompts'
url.pathname === '/v1/team/prompts' ||
url.pathname === '/v1/team/prefs' ||
url.pathname === '/v1/team/org'
) {
const caller = await authenticateDesktop(env, request, (deps.now ?? Date.now)())
if (!caller) {
@@ -1818,7 +1824,32 @@ async function workspaceRoute(request: Request, url: URL, env: Env, deps: Deps,
// Labels for teammates' lists; a failure here must never fail the request itself.
await rememberCallerName(db, caller, now).catch((error: unknown) => console.warn('member_name_write_failed', String(error)))
if (url.pathname === '/v1/team/links/resolve' && request.method === 'GET') {
return json(await resolveLink(db, caller, url, now))
try {
return json(await resolveLink(db, caller, url, now))
} catch (error) {
if (error instanceof LinkOtherOrgError) {
return json({ error: 'not_found', reason: 'other_org', orgId: error.orgId }, 404)
}
throw error
}
}
if (url.pathname === '/v1/team/prefs') {
if (request.method === 'GET') {
return json(await getWorkspacePrefs(db, caller))
}
if (request.method === 'PUT') {
return json(await putWorkspacePrefs(db, caller, await readJson(request), now))
}
return fail('not_found', 404)
}
if (url.pathname === '/v1/team/org') {
if (request.method === 'GET') {
return json(await getOrgSlug(db, caller, url, now))
}
if (request.method === 'PUT') {
return json(await putOrgSlug(db, caller, await readJson(request), now))
}
return fail('not_found', 404)
}
if (url.pathname === '/v1/team/prompts') {
if (request.method === 'GET') {
+123
View File
@@ -0,0 +1,123 @@
// Org URL slugs for workspace links (orca://cloud/<slug>/W-123).
// GET /v1/team/org?name=<org display name> -> {slug, orgId}; the first call names it from `name`
// PUT /v1/team/org {slug} org owner/admin renames it (409 when another org has it)
// Auth owns orgs and knows their names; this Worker only sees ids. So the first member to ask
// proposes a slug from the name their client shows, and the Worker makes it unique. Personal
// accounts have no slug: their links keep the sandbox id form (orca://cloud/w/<sbx>).
import { z } from 'zod'
import type { SandboxCaller } from './auth'
import { WorkspaceError, isOrgAdmin } from './workspaces'
/** Path words a slug can't take: they'd read as a different link form. */
const RESERVED = new Set(['w', 'c', 'me', 'api', 'app', 'new', 'org', 'team', 'settings', 'cloud', 'orca', 'admin', 'help'])
export const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/
export function isValidSlug(value: string): boolean {
return SLUG.test(value) && !value.includes('--') && !RESERVED.has(value)
}
/** "Acme Labs, Inc." → "acme-labs-inc"; falls back to "team" when nothing usable is left. */
export function slugify(name: string): string {
const base = name
.normalize('NFKD')
.replace(/[̀-ͯ]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 32)
.replace(/-+$/g, '')
return base.length >= 2 && !RESERVED.has(base) ? base : base ? `${base}-team` : 'team'
}
type Row = { account_id: string; slug: string }
/** `org:abc` / `dev:org:abc` → `abc` (the id the desktop's org list uses). */
export function orgIdOf(accountId: string): string | null {
const match = /^(?:dev:)?org:(.+)$/.exec(accountId)
return match ? match[1]! : null
}
export async function slugForAccount(db: D1Database, accountId: string): Promise<string | null> {
const row = await db.prepare('SELECT slug FROM org_slugs WHERE account_id = ?').bind(accountId).first<{ slug: string }>()
return row?.slug ?? null
}
export async function accountForSlug(db: D1Database, slug: string): Promise<string | null> {
if (!isValidSlug(slug)) {
return null
}
const row = await db.prepare('SELECT account_id FROM org_slugs WHERE slug = ?').bind(slug).first<Row>()
return row?.account_id ?? null
}
const NameQuery = z.string().trim().min(1).max(120)
export async function getOrgSlug(db: D1Database, caller: SandboxCaller, url: URL, now: number) {
const orgId = caller.orgId ? orgIdOf(caller.accountId) : null
if (!orgId) {
return { slug: null, orgId: null }
}
const existing = await slugForAccount(db, caller.accountId)
if (existing) {
return { slug: existing, orgId }
}
const name = NameQuery.safeParse(url.searchParams.get('name') ?? '')
if (!name.success) {
// Nothing to name it from yet; the next client that knows the org's name will.
return { slug: null, orgId }
}
const base = slugify(name.data)
for (let attempt = 0; attempt < 20; attempt += 1) {
const candidate = attempt === 0 ? base : `${base.slice(0, 32)}-${attempt + 1}`
const result = await db
.prepare('INSERT OR IGNORE INTO org_slugs (account_id, slug, created_at, created_by, updated_at) VALUES (?, ?, ?, ?, ?)')
.bind(caller.accountId, candidate, now, caller.userId, now)
.run()
if (result.meta.changes === 1) {
return { slug: candidate, orgId }
}
// Either another org holds this slug, or a teammate just named ours (then use theirs).
const raced = await slugForAccount(db, caller.accountId)
if (raced) {
return { slug: raced, orgId }
}
}
throw new WorkspaceError('conflict', 'slug_exhausted')
}
const PutSchema = z.object({ slug: z.string().min(1).max(40) }).strict()
export async function putOrgSlug(db: D1Database, caller: SandboxCaller, body: unknown, now: number) {
const orgId = caller.orgId ? orgIdOf(caller.accountId) : null
if (!orgId) {
throw new WorkspaceError('invalid_request', 'personal_account')
}
if (!isOrgAdmin(caller)) {
throw new WorkspaceError('forbidden', 'admin_only')
}
const parsed = PutSchema.safeParse(body)
if (!parsed.success || !isValidSlug(parsed.data.slug)) {
throw new WorkspaceError('invalid_request', 'invalid_slug')
}
const slug = parsed.data.slug
const holder = await accountForSlug(db, slug)
if (holder && holder !== caller.accountId) {
throw new WorkspaceError('conflict', 'slug_taken')
}
await db
.prepare(
`INSERT INTO org_slugs (account_id, slug, created_at, created_by, updated_at) VALUES (?, ?, ?, ?, ?)
ON CONFLICT (account_id) DO UPDATE SET slug = excluded.slug, updated_at = excluded.updated_at`
)
.bind(caller.accountId, slug, now, caller.userId, now)
.run()
.catch((error: unknown) => {
// The UNIQUE index lost a race with another org claiming the same slug.
if (String(error).includes('UNIQUE')) {
throw new WorkspaceError('conflict', 'slug_taken')
}
throw error
})
return { slug, orgId }
}
+24 -12
View File
@@ -71,6 +71,8 @@ export type SandboxRow = {
last_activity_at: number | null
last_activity_by: string | null
token_usd_micros: number | null
/** 0051: this workspace's number in its account (shown as W-123); null only on rows from before it. */
number?: number | null
}
export type WorkspaceVisibility = 'private' | 'team'
@@ -118,18 +120,28 @@ export async function insertSandbox(
Partial<Pick<SandboxRow, 'environment_id' | 'environment_build_id' | 'source_snapshot_id' | 'name' | 'repo_branch' | 'visibility' | 'section'>>,
now: number
): Promise<SandboxRow> {
await db
.prepare(
`INSERT INTO sandboxes (id, account_id, user_id, workspace_id, provider, provider_name, status,
machine_size, region, generation, created_at, updated_at, repo_url, environment_id, environment_build_id,
source_snapshot_id, created_by, name, repo_branch, visibility, section, last_activity_at, last_activity_by)
VALUES (?, ?, ?, ?, ?, ?, 'creating', ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(row.id, row.account_id, row.user_id, row.workspace_id, row.provider, row.provider_name,
row.machine_size, row.region, now, now, row.repo_url, row.environment_id ?? null,
row.environment_build_id ?? null, row.source_snapshot_id ?? null, row.user_id, row.name ?? null,
row.repo_branch ?? null, row.visibility ?? 'private', row.section ?? null, now, row.user_id)
.run()
// One transaction: bump the account's counter, then take its value as this row's number (W-123).
// A failed insert rolls the bump back; a destroyed row keeps its number, so none is ever reused.
await db.batch([
db
.prepare(
`INSERT INTO org_workspace_counters (account_id, last_number) VALUES (?, 1)
ON CONFLICT (account_id) DO UPDATE SET last_number = last_number + 1`
)
.bind(row.account_id),
db
.prepare(
`INSERT INTO sandboxes (id, account_id, user_id, workspace_id, provider, provider_name, status,
machine_size, region, generation, created_at, updated_at, repo_url, environment_id, environment_build_id,
source_snapshot_id, created_by, name, repo_branch, visibility, section, last_activity_at, last_activity_by, number)
VALUES (?, ?, ?, ?, ?, ?, 'creating', ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
(SELECT last_number FROM org_workspace_counters WHERE account_id = ?))`
)
.bind(row.id, row.account_id, row.user_id, row.workspace_id, row.provider, row.provider_name,
row.machine_size, row.region, now, now, row.repo_url, row.environment_id ?? null,
row.environment_build_id ?? null, row.source_snapshot_id ?? null, row.user_id, row.name ?? null,
row.repo_branch ?? null, row.visibility ?? 'private', row.section ?? null, now, row.user_id, row.account_id)
])
const inserted = await getSandbox(db, row.id)
if (!inserted) {
throw new Error('sandbox insert lost')
@@ -0,0 +1,86 @@
// A member's own sidebar arrangement of cloud workspaces, stored on the account so every device
// (desktops and the phone) shows the same pins, order and sections.
// GET /v1/team/prefs -> {document, revision, updatedAt}
// PUT /v1/team/prefs {document, revision} `revision` is the one the client edited (compare-and-set:
// a stale write gets 409 and the client re-applies its edit)
// Per member, per account: switching teams switches arrangements. Ids that no longer exist are
// harmless (clients skip them) and are dropped the next time that client writes.
import { z } from 'zod'
import type { SandboxCaller } from './auth'
import { WorkspaceError } from './workspaces'
const SandboxId = z.string().regex(/^sbx_[0-9a-f]{24}$/)
const SectionName = z.string().trim().min(1).max(64).regex(/^[^\u0000-\u001f]+$/)
const unique = (list: readonly string[]) => new Set(list).size === list.length
export const WorkspacePrefsDocumentSchema = z
.object({
/** Pinned workspaces, top to bottom. */
pinned: z.array(SandboxId).max(200).refine(unique, 'duplicate id'),
/** Manual order of the rest (drag to reorder); workspaces not listed follow, newest first. */
order: z.array(SandboxId).max(500).refine(unique, 'duplicate id'),
/** Section order, including sections that are empty right now. */
sections: z.array(SectionName).max(50).refine(unique, 'duplicate section'),
/** Sections this member keeps collapsed. */
collapsed: z.array(SectionName).max(50).refine(unique, 'duplicate section')
})
.strict()
.refine((doc) => !doc.pinned.some((id) => doc.order.includes(id)), 'pinned ids stay out of order')
export type WorkspacePrefsDocument = z.infer<typeof WorkspacePrefsDocumentSchema>
const PutSchema = z
.object({
document: WorkspacePrefsDocumentSchema,
revision: z.number().int().min(0),
/** Which device wrote it (a label for debugging sync, never trusted for access). */
deviceId: z.string().min(1).max(128).regex(/^[A-Za-z0-9_.:-]+$/).optional()
})
.strict()
export const EMPTY_PREFS: WorkspacePrefsDocument = { pinned: [], order: [], sections: [], collapsed: [] }
type Row = { document: string; revision: number; updated_at: number }
export async function getWorkspacePrefs(db: D1Database, caller: SandboxCaller) {
const row = await db
.prepare('SELECT document, revision, updated_at FROM workspace_prefs WHERE account_id = ? AND user_id = ?')
.bind(caller.accountId, caller.userId)
.first<Row>()
if (!row) {
return { document: EMPTY_PREFS, revision: 0, updatedAt: null }
}
// A document from a newer schema parses leniently: unknown keys are ignored, bad ones reset.
const parsed = WorkspacePrefsDocumentSchema.safeParse(JSON.parse(row.document))
return { document: parsed.success ? parsed.data : EMPTY_PREFS, revision: row.revision, updatedAt: row.updated_at }
}
export async function putWorkspacePrefs(db: D1Database, caller: SandboxCaller, body: unknown, now: number) {
const parsed = PutSchema.safeParse(body)
if (!parsed.success) {
throw new WorkspaceError('invalid_request')
}
const { document, revision, deviceId } = parsed.data
const text = JSON.stringify(document)
const result =
revision === 0
? await db
.prepare(
`INSERT OR IGNORE INTO workspace_prefs (account_id, user_id, document, revision, updated_at, updated_by_device)
VALUES (?, ?, ?, 1, ?, ?)`
)
.bind(caller.accountId, caller.userId, text, now, deviceId ?? null)
.run()
: await db
.prepare(
`UPDATE workspace_prefs SET document = ?, revision = revision + 1, updated_at = ?, updated_by_device = ?
WHERE account_id = ? AND user_id = ? AND revision = ?`
)
.bind(text, now, deviceId ?? null, caller.accountId, caller.userId, revision)
.run()
if (result.meta.changes !== 1) {
throw new WorkspaceError('conflict', 'stale_revision')
}
return getWorkspacePrefs(db, caller)
}
+47 -12
View File
@@ -4,7 +4,7 @@
// GET /v1/team/workspaces/:id
// PATCH /v1/team/workspaces/:id {title?, branch?, visibility?, section?, archived?, ownerUserId?, tokenUsdMicros?}
// POST /v1/team/workspaces/:id/presence {clientId, state: viewing|typing|left, chatId?}
// GET /v1/team/links/resolve?w=<sandboxId>&c=<chatId>
// GET /v1/team/links/resolve?w=<sandboxId>&c=<chatId> or ?org=<slug>&n=<number>&c=<chatId>
// Access rule: a workspace is visible to its owner, and to every member of the owning org when
// visibility is `team`. Org owners/admins additionally see private rows' metadata (scope=all) so a
// departed member's workspaces can be reassigned or archived — metadata only, never access.
@@ -15,6 +15,7 @@ import { getSandbox, touchActivity, updateRegistry, type SandboxRow } from './st
import { memberNamesFor } from './member-names'
import { readSnapshotTimes } from './read-snapshots'
import { sandboxView } from './wire'
import { accountForSlug, orgIdOf, slugForAccount } from './org-slugs'
import { ORCA_CLOUD_OVERAGE_CENTS_PER_HOUR } from '../../../../src/shared/hosted-sandbox/hosted-sandbox-wire'
export const PRESENCE_TTL_MS = 45_000
@@ -170,11 +171,12 @@ function repoView(row: SandboxRow) {
export async function workspaceViews(db: D1Database, caller: SandboxCaller, rows: readonly SandboxRow[], now: number) {
const ids = rows.map((r) => r.id)
const [presence, compute, granted, readAt] = await Promise.all([
const [presence, compute, granted, readAt, slug] = await Promise.all([
presenceFor(db, ids, now),
computeMsFor(db, ids),
activeGrantUsers(db, ids, caller.userId),
readSnapshotTimes(db, ids)
readSnapshotTimes(db, ids),
rows.length > 0 ? slugForAccount(db, caller.accountId) : Promise.resolve(null)
])
const referenced = (row: SandboxRow, live: readonly PresenceRow[]) =>
[row.user_id, row.created_by, row.last_activity_by, ...live.map((p) => p.user_id)].filter(
@@ -196,6 +198,10 @@ export async function workspaceViews(db: D1Database, caller: SandboxCaller, rows
return {
...sandboxView(row),
title: row.name,
// W-123 in its account; null only for a row that predates numbering and was never backfilled.
number: row.number ?? null,
// The link to copy: org slug + number when both exist, else the sandbox id form.
link: workspaceLink(row, slug),
repo: repoView(row),
visibility: row.visibility,
section: row.section,
@@ -368,27 +374,56 @@ export async function reportPresence(
return { presence: view!.presence }
}
const LinkQuery = z.object({
w: z.string().regex(/^sbx_[0-9a-f]{24}$/),
c: z.string().min(1).max(128).regex(/^[A-Za-z0-9_.:-]+$/).optional()
})
const ChatParam = z.string().min(1).max(128).regex(/^[A-Za-z0-9_.:-]+$/).optional()
const LinkQuery = z.union([
z.object({ w: z.string().regex(/^sbx_[0-9a-f]{24}$/), c: ChatParam }),
z.object({ org: z.string().min(1).max(40), n: z.coerce.number().int().min(1).max(1e9), c: ChatParam })
])
/** orca://cloud/w/<id>[/c/<chat>] → the workspace if the caller can see it; otherwise not_found. */
/** A slug link that names a different org than the caller's current one. */
export class LinkOtherOrgError extends WorkspaceError {
constructor(readonly orgId: string | null) {
super('not_found', 'other_org')
}
}
/**
* orca://cloud/w/<id>[/c/<chat>] or orca://cloud/<slug>/W-<n>[/c/<chat>] → the workspace if the caller
* can see it; otherwise not_found. A slug link for another org answers not_found with reason
* `other_org` and that org's id, so a client that belongs to it can offer to switch teams.
*/
export async function resolveLink(db: D1Database, caller: SandboxCaller, url: URL, now: number) {
const query = LinkQuery.safeParse(Object.fromEntries(url.searchParams))
if (!query.success) {
throw new WorkspaceError('invalid_request')
}
const row = await getSandbox(db, query.data.w)
const q = query.data
let row: SandboxRow | null
if ('w' in q) {
row = await getSandbox(db, q.w)
} else {
const accountId = await accountForSlug(db, q.org)
if (!accountId) {
throw new WorkspaceError('not_found')
}
if (accountId !== caller.accountId) {
throw new LinkOtherOrgError(orgIdOf(accountId))
}
row = await db
.prepare('SELECT * FROM sandboxes WHERE account_id = ? AND number = ?')
.bind(accountId, q.n)
.first<SandboxRow>()
}
if (!row || !canSee(caller, row)) {
// Private, other org, or gone: the link reveals nothing about which.
throw new WorkspaceError('not_found')
}
const [view] = await workspaceViews(db, caller, [row], now)
return { workspace: view, chatId: query.data.c ?? null }
return { workspace: view, chatId: q.c ?? null }
}
/** Canonical deep link for a workspace (and optionally a chat in it). */
export function workspaceLink(id: string, chatId?: string): string {
return `orca://cloud/w/${id}${chatId ? `/c/${encodeURIComponent(chatId)}` : ''}`
export function workspaceLink(row: Pick<SandboxRow, 'id' | 'number'>, slug: string | null, chatId?: string): string {
const chat = chatId ? `/c/${encodeURIComponent(chatId)}` : ''
return slug && row.number ? `orca://cloud/${slug}/W-${row.number}${chat}` : `orca://cloud/w/${row.id}${chat}`
}
@@ -0,0 +1,175 @@
import { readFileSync, readdirSync } from 'node:fs'
import { DatabaseSync } from 'node:sqlite'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { isValidSlug, slugify } from '../src/org-slugs'
import { createHarness } from './harness'
type Role = 'owner' | 'admin' | 'member'
type Who = { sub: string; org?: string; role: Role }
async function setup() {
const h = await createHarness()
const req = async (who: Who, method: string, path: string, body?: unknown) =>
h.call(method, path, { bearer: await h.token(who.sub, who.org, 'sandbox', who.org ? { org_role: who.role } : {}), body })
let n = 0
const create = async (who: Who, extra: Record<string, unknown> = {}) => {
n += 1
const res = await req(who, 'POST', '/v1/sandboxes', { workspaceId: `ws-${n}`, machineSize: 'standard', ...extra })
expect(res.status).toBe(200)
return res.body.sandbox.id as string
}
const alice: Who = { sub: 'alice', org: 'acme', role: 'member' }
const bob: Who = { sub: 'bob', org: 'acme', role: 'member' }
const admin: Who = { sub: 'carol', org: 'acme', role: 'admin' }
const eve: Who = { sub: 'eve', org: 'other', role: 'owner' }
const solo: Who = { sub: 'sam', role: 'member' }
return { h, req, create, alice, bob, admin, eve, solo }
}
describe('workspace numbers (W-123)', () => {
it('numbers workspaces per account and never reuses a number', async () => {
const t = await setup()
const a1 = await t.create(t.alice)
const a2 = await t.create(t.bob, { visibility: 'team' })
const e1 = await t.create(t.eve)
const view = async (who: Who, id: string) => (await t.req(who, 'GET', `/v1/team/workspaces/${id}`)).body.workspace
expect((await view(t.alice, a1)).number).toBe(1)
expect((await view(t.bob, a2)).number).toBe(2)
expect((await view(t.eve, e1)).number).toBe(1)
expect((await t.req(t.alice, 'DELETE', `/v1/sandboxes/${a1}`)).status).toBe(200)
const a3 = await t.create(t.alice)
expect((await view(t.alice, a3)).number).toBe(3)
})
it('backfills existing rows by creation order and seeds the counter', () => {
const raw = new DatabaseSync(':memory:')
const dir = fileURLToPath(new URL('../migrations/', import.meta.url).href)
const files = readdirSync(dir).filter((f) => f.endsWith('.sql')).sort()
const at = files.indexOf('0051_workspace_numbers_prefs.sql')
for (const file of files.slice(0, at)) {
raw.exec(readFileSync(`${dir}${file}`, 'utf8'))
}
const insert = raw.prepare(
`INSERT INTO sandboxes (id, account_id, user_id, workspace_id, provider, provider_name, status, machine_size, region,
generation, created_at, updated_at) VALUES (?, ?, 'u', ?, 'vercel', ?, 'destroyed', 'standard', 'iad1', 0, ?, ?)`
)
insert.run('sbx_b', 'org:a', 'w2', 'p2', 20, 20)
insert.run('sbx_a', 'org:a', 'w1', 'p1', 10, 10)
insert.run('sbx_c', 'org:a', 'w3', 'p3', 20, 20)
insert.run('sbx_z', 'org:b', 'w1', 'pz', 5, 5)
for (const file of files.slice(at)) {
raw.exec(readFileSync(`${dir}${file}`, 'utf8'))
}
expect(raw.prepare('SELECT id, number FROM sandboxes ORDER BY id').all()).toEqual([
{ id: 'sbx_a', number: 1 },
{ id: 'sbx_b', number: 2 },
{ id: 'sbx_c', number: 3 },
{ id: 'sbx_z', number: 1 }
])
expect(raw.prepare('SELECT * FROM org_workspace_counters ORDER BY account_id').all()).toEqual([
{ account_id: 'org:a', last_number: 3 },
{ account_id: 'org:b', last_number: 1 }
])
})
})
describe('org slugs', () => {
it('slugifies names and refuses reserved or malformed slugs', () => {
expect(slugify('Acme Labs, Inc.')).toBe('acme-labs-inc')
expect(slugify('Ünïcode Crew')).toBe('unicode-crew')
expect(slugify('!!!')).toBe('team')
expect(slugify('w')).toBe('w-team')
expect(isValidSlug('acme')).toBe(true)
expect(isValidSlug('w')).toBe(false)
expect(isValidSlug('-acme')).toBe(false)
expect(isValidSlug('a--b')).toBe(false)
expect(isValidSlug('Acme')).toBe(false)
})
it('names an org once from the first name it hears, uniquely across orgs', async () => {
const t = await setup()
expect((await t.req(t.alice, 'GET', '/v1/team/org')).body).toEqual({ slug: null, orgId: 'acme' })
expect((await t.req(t.alice, 'GET', '/v1/team/org?name=Acme%20Labs')).body).toEqual({ slug: 'acme-labs', orgId: 'acme' })
// A teammate whose client shows another name still gets the org's slug.
expect((await t.req(t.bob, 'GET', '/v1/team/org?name=Whatever')).body.slug).toBe('acme-labs')
expect((await t.req(t.eve, 'GET', '/v1/team/org?name=Acme%20Labs')).body.slug).toBe('acme-labs-2')
expect((await t.req(t.solo, 'GET', '/v1/team/org?name=Sam')).body).toEqual({ slug: null, orgId: null })
})
it('lets only admins rename, and not onto another org', async () => {
const t = await setup()
await t.req(t.alice, 'GET', '/v1/team/org?name=Acme')
await t.req(t.eve, 'GET', '/v1/team/org?name=Other')
expect((await t.req(t.alice, 'PUT', '/v1/team/org', { slug: 'acme-co' })).status).toBe(403)
expect((await t.req(t.admin, 'PUT', '/v1/team/org', { slug: 'other' })).status).toBe(409)
expect((await t.req(t.admin, 'PUT', '/v1/team/org', { slug: 'w' })).status).toBe(400)
expect((await t.req(t.admin, 'PUT', '/v1/team/org', { slug: 'acme-co' })).body).toEqual({ slug: 'acme-co', orgId: 'acme' })
expect((await t.req(t.bob, 'GET', '/v1/team/org')).body.slug).toBe('acme-co')
})
})
describe('slug links', () => {
it('copies org slug links and resolves them only for those who can see the workspace', async () => {
const t = await setup()
const id = await t.create(t.alice)
const before = (await t.req(t.alice, 'GET', `/v1/team/workspaces/${id}`)).body.workspace
// No slug yet: the id form still works.
expect(before.link).toBe(`orca://cloud/w/${id}`)
await t.req(t.alice, 'GET', '/v1/team/org?name=Acme')
const after = (await t.req(t.alice, 'GET', '/v1/team/workspaces?scope=mine')).body.workspaces[0]
expect(after.link).toBe('orca://cloud/acme/W-1')
const resolved = await t.req(t.alice, 'GET', '/v1/team/links/resolve?org=acme&n=1&c=s:abc')
expect(resolved.status).toBe(200)
expect(resolved.body).toMatchObject({ workspace: { id, number: 1 }, chatId: 's:abc' })
// Private to Alice: Bob sees nothing until she shares it.
expect((await t.req(t.bob, 'GET', '/v1/team/links/resolve?org=acme&n=1')).status).toBe(404)
await t.req(t.alice, 'PATCH', `/v1/team/workspaces/${id}`, { visibility: 'team' })
expect((await t.req(t.bob, 'GET', '/v1/team/links/resolve?org=acme&n=1')).body.workspace.id).toBe(id)
expect((await t.req(t.bob, 'GET', '/v1/team/links/resolve?org=acme&n=2')).status).toBe(404)
// Another org: not found, but says which org so a member of both can switch.
const other = await t.req(t.eve, 'GET', '/v1/team/links/resolve?org=acme&n=1')
expect(other.status).toBe(404)
expect(other.body).toEqual({ error: 'not_found', reason: 'other_org', orgId: 'acme' })
expect((await t.req(t.eve, 'GET', '/v1/team/links/resolve?org=nope&n=1')).body).toEqual({ error: 'not_found' })
expect((await t.req(t.alice, 'GET', '/v1/team/links/resolve?org=acme&n=0')).status).toBe(400)
})
})
describe('workspace prefs (synced pins and order)', () => {
const A = `sbx_${'a'.repeat(24)}`
const B = `sbx_${'b'.repeat(24)}`
it('stores one document per member and account with compare-and-set', async () => {
const t = await setup()
expect((await t.req(t.alice, 'GET', '/v1/team/prefs')).body).toEqual({
document: { pinned: [], order: [], sections: [], collapsed: [] },
revision: 0,
updatedAt: null
})
const doc = { pinned: [A], order: [B], sections: ['Auth', 'Billing'], collapsed: ['Billing'] }
const first = await t.req(t.alice, 'PUT', '/v1/team/prefs', { document: doc, revision: 0, deviceId: 'desk-1' })
expect(first.status).toBe(200)
expect(first.body).toMatchObject({ document: doc, revision: 1 })
// A second device that still holds revision 0 must re-read before writing.
const stale = await t.req(t.alice, 'PUT', '/v1/team/prefs', { document: { ...doc, pinned: [] }, revision: 0 })
expect(stale.status).toBe(409)
const second = await t.req(t.alice, 'PUT', '/v1/team/prefs', { document: { ...doc, pinned: [B, A], order: [] }, revision: 1 })
expect(second.body.revision).toBe(2)
expect((await t.req(t.alice, 'GET', '/v1/team/prefs')).body.document.pinned).toEqual([B, A])
// Teammates and other accounts have their own.
expect((await t.req(t.bob, 'GET', '/v1/team/prefs')).body.revision).toBe(0)
expect((await t.req({ ...t.alice, org: 'other' }, 'GET', '/v1/team/prefs')).body.revision).toBe(0)
expect((await t.req(t.solo, 'PUT', '/v1/team/prefs', { document: doc, revision: 0 })).status).toBe(200)
})
it('rejects malformed documents', async () => {
const t = await setup()
const put = (document: unknown) => t.req(t.alice, 'PUT', '/v1/team/prefs', { document, revision: 0 })
expect((await put({ pinned: [A], order: [A], sections: [], collapsed: [] })).status).toBe(400)
expect((await put({ pinned: [A, A], order: [], sections: [], collapsed: [] })).status).toBe(400)
expect((await put({ pinned: ['nope'], order: [], sections: [], collapsed: [] })).status).toBe(400)
expect((await put({ pinned: [], order: [], sections: ['x'], collapsed: [], extra: 1 })).status).toBe(400)
})
})