mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-30 22:52:25 +03:00
feat(core): one-time install tokens that register a host to a team
Installing on a host should be a command a person pastes, not a user session copied onto a machine. A token is issued for a team, spent by its first use, expires after an hour and is stored only as a digest, because it travels in that command and so lands in shell history. Redeeming is one conditional update inside the registration transaction: concurrent redemptions of one token register exactly one machine, and a registration that fails leaves the token unspent.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
import { id, timestamps, ulid, utc } from '../db/types.js';
|
||||
import { TeamTable } from '../team/team.sql.js';
|
||||
import { UserTable } from '../user/user.sql.js';
|
||||
import { MachineTable } from './machine.sql.js';
|
||||
|
||||
/**
|
||||
* A one-time credential that registers exactly one machine to one team.
|
||||
*
|
||||
* It exists so that installing on a host is a command a person pastes, rather
|
||||
* than a user session copied onto a machine. It travels in that command, so it
|
||||
* lands in shell history: that is why it is single-use, expires in minutes,
|
||||
* and is stored only as a digest.
|
||||
*/
|
||||
export const InstallTokenTable = pgTable(
|
||||
'install_token',
|
||||
{
|
||||
...id,
|
||||
...timestamps,
|
||||
teamId: ulid('team_id')
|
||||
.notNull()
|
||||
.references(() => TeamTable.id, { onDelete: 'cascade' }),
|
||||
// Who asked for it. They become the machine's owner, as they would have
|
||||
// by registering it with their own session.
|
||||
createdByUserId: ulid('created_by_user_id')
|
||||
.notNull()
|
||||
.references(() => UserTable.id, { onDelete: 'cascade' }),
|
||||
tokenHash: text('token_hash').notNull(),
|
||||
expiresAt: utc('expires_at').notNull(),
|
||||
redeemedAt: utc('redeemed_at'),
|
||||
// The machine it made. Null until redeemed; kept afterwards so a support
|
||||
// conversation can say which command produced which host.
|
||||
machineId: ulid('machine_id').references(() => MachineTable.id, { onDelete: 'set null' })
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('install_token_hash_unique').on(t.tokenHash),
|
||||
index('install_token_team_idx').on(t.teamId)
|
||||
]
|
||||
);
|
||||
Reference in New Issue
Block a user