feat: Sync to OSS repo

This commit is contained in:
Wanjohi
2026-08-06 22:13:51 +03:00
parent 46d2a56180
commit 3faac3008f
144 changed files with 27561 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
import { randomBytes } from 'node:crypto';
import { and, eq, isNull, sql } from 'drizzle-orm';
import z from 'zod';
import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { Member } from '../team/member.js';
import { MachineTable } from './machine.sql.js';
/**
* Registered host identity.
*
* A box trades an owner-supplied token for an assigned id and a secret, then
* authenticates as itself. The alternative — deriving an id from
* `/etc/machine-id` or a hardware fingerprint — was rejected: self-hosted
* boxes mean the operator is not automatically trusted, and every such input
* is operator-editable, so uniqueness would rest on nobody choosing to lie.
*/
export namespace Machine {
/** Length in bytes before base64url encoding. */
const SECRET_BYTES = 32;
export const Info = z
.object({
id: z.string().meta({
description: 'Unique identifier for the machine',
example: Examples.Machine.id
}),
ownerUserId: z.string().meta({
description: 'The user who registered this machine',
example: Examples.Machine.ownerUserId
}),
teamId: z.string().optional().nullable().meta({
description: 'The team this machine belongs to, when registered inside one',
example: Examples.Machine.teamId
}),
label: z.string().meta({
description: 'Human-readable name for the box',
example: Examples.Machine.label
}),
lastSeen: z.iso.datetime().optional().nullable().meta({
description: 'When this machine last authenticated',
example: Examples.Machine.lastSeen
})
})
.meta({
ref: 'Machine',
description: 'A registered nessh host',
example: Examples.Machine
});
export type Info = z.infer<typeof Info>;
function generateSecret(): string {
return `msk_${randomBytes(SECRET_BYTES).toString('base64url')}`;
}
async function hashSecret(secret: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
/** Length-independent, content-constant comparison of two hex digests. */
function secureEquals(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}
/**
* Register a box. The secret is returned here and nowhere else — it is
* stored only as a digest, so a lost secret means re-registering rather
* than looking it up.
*/
export const register = fn(
Info.pick({ id: true, ownerUserId: true, teamId: true, label: true }),
async (input) => {
const secret = generateSecret();
await Database.use(async (tx) => {
await tx.insert(MachineTable).values({
id: input.id,
ownerUserId: input.ownerUserId,
teamId: input.teamId ?? null,
label: input.label,
secretHash: await hashSecret(secret),
lastSeen: null
});
});
return { id: input.id, secret };
}
);
/**
* Resolve credentials to a machine, or `null`. Looks the row up by id and
* then compares digests, so a wrong id and a wrong secret are refused the
* same way and neither reveals which half was wrong.
*/
export const authenticate = fn(
z.object({ id: z.string(), secret: z.string() }),
async (input) => {
return Database.use(async (tx) => {
return tx
.select()
.from(MachineTable)
.where(and(eq(MachineTable.id, input.id), isNull(MachineTable.timeDeleted)))
.then(async (rows) => {
const row = rows.at(0);
if (!row) {
return null;
}
if (!secureEquals(row.secretHash, await hashSecret(input.secret))) {
return null;
}
// Serialized here, so `secretHash` never leaves this
// function even in memory — the caller cannot leak what
// it was never handed.
return serialize(row);
});
});
}
);
/**
* Move a box into a team, or back out of one with `teamId: null`.
*
* Scoped to the owner in the query itself, so a machine belonging to
* someone else is a miss rather than a permission check that could be
* forgotten. Membership of the *target* team is the caller's to verify —
* this function knows about machines, not about who belongs where.
*
* Deliberately not ownership transfer. Scoping keeps the same owner and
* should be easy; handing a box to a different person should not be, and
* is left to re-registration until renting makes it worth building.
*/
export const setTeam = fn(
Info.pick({ id: true, ownerUserId: true, teamId: true }),
async (input) => {
return Database.use(async (tx) => {
return tx
.update(MachineTable)
.set({ teamId: input.teamId ?? null })
.where(
and(
eq(MachineTable.id, input.id),
eq(MachineTable.ownerUserId, input.ownerUserId),
isNull(MachineTable.timeDeleted)
)
)
.returning()
.then((rows) => {
const row = rows.at(0);
return row ? serialize(row) : null;
});
});
}
);
export const touchLastSeen = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(MachineTable)
.set({ lastSeen: sql`now()` })
.where(eq(MachineTable.id, id));
});
});
export const fromID = fn(Info.shape.id, async (id) => {
return Database.use(async (tx) => {
return tx
.select()
.from(MachineTable)
.where(and(eq(MachineTable.id, id), isNull(MachineTable.timeDeleted)))
.then((rows) => {
const row = rows.at(0);
return row ? serialize(row) : null;
});
});
});
/** Why a user may — or may not — use a box. */
export const Entitlement = z.object({
entitled: z.boolean(),
/** `owner`, `team`, or `none`. Present so a refusal can explain itself. */
reason: z.enum(['owner', 'team', 'none'])
});
export type Entitlement = z.infer<typeof Entitlement>;
/**
* Whether a user may use a box.
*
* The whole access model in one function: a solo box (`teamId` null) is the
* owner's alone, and a team-scoped box is open to that team. Multi-user
* access is the paid tier, so this is the line the paywall sits on — worth
* having exactly one implementation of.
*
* Membership is read live rather than cached in the machine row, so
* removing someone from a team takes their box access with it and nobody
* has to remember to revoke anything.
*/
export const entitlement = fn(
z.object({ machineId: z.string(), userId: z.string() }),
async (input): Promise<Entitlement> => {
const machine = await fromID(input.machineId);
if (!machine) {
return { entitled: false, reason: 'none' };
}
if (machine.ownerUserId === input.userId) {
return { entitled: true, reason: 'owner' };
}
if (!machine.teamId) {
// A solo box. Nobody but the owner, whatever else is true.
return { entitled: false, reason: 'none' };
}
const membership = await Member.findByTeamAndUser({
teamId: machine.teamId,
userId: input.userId
});
return membership ? { entitled: true, reason: 'team' } : { entitled: false, reason: 'none' };
}
);
export const listByOwner = fn(Info.shape.ownerUserId, async (ownerUserId) => {
return Database.use(async (tx) => {
return tx
.select()
.from(MachineTable)
.where(and(eq(MachineTable.ownerUserId, ownerUserId), isNull(MachineTable.timeDeleted)))
.orderBy(MachineTable.timeCreated)
.then((rows) => rows.map(serialize));
});
});
export const remove = fn(Info.shape.id, async (id) => {
await Database.use(async (tx) => {
await tx
.update(MachineTable)
.set({ timeDeleted: sql`now()` })
.where(eq(MachineTable.id, id));
});
});
export function serialize(input: typeof MachineTable.$inferSelect): z.infer<typeof Info> {
return {
id: input.id,
ownerUserId: input.ownerUserId,
teamId: input.teamId,
label: input.label,
lastSeen: input.lastSeen?.toISOString() ?? null
};
}
}

View File

@@ -0,0 +1,38 @@
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core';
import { id, timestamps, ulid, utc } from '../db/types.js';
import { UserTable } from '../user/user.sql.js';
/**
* A registered nessh host — the *box* that runs downloads and serves SSH, not
* the laptop someone connects from. (`nessh-tui-redesign-guide.md` §7.2 uses
* "machine" for the other end of that connection; this table is the host end.)
*
* A box does not assert who it is. It registers once against an owner's token
* and is handed an id and a secret, so ids are unique because the API assigns
* them rather than because a self-reported string happened not to collide.
*/
export const MachineTable = pgTable(
'machine',
{
...id,
...timestamps,
ownerUserId: ulid('owner_user_id')
.notNull()
.references(() => UserTable.id, { onDelete: 'cascade' }),
// Set only when the box was registered by someone acting inside a team.
// A personal box has no team, and requiring one would make registering
// impossible for the single-operator case that self-hosting is.
teamId: ulid('team_id'),
label: text('label').notNull(),
// The secret itself is returned exactly once, at registration, and never
// stored: a leaked database must not yield working box credentials.
secretHash: text('secret_hash').notNull(),
lastSeen: utc('last_seen')
},
(t) => [
uniqueIndex('machine_secret_hash_unique').on(t.secretHash),
index('machine_owner_idx').on(t.ownerUserId),
index('machine_team_idx').on(t.teamId)
]
);