mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-22 18:55:22 +03:00
feat: Sync to OSS repo
This commit is contained in:
180
packages/core/src/actor.ts
Normal file
180
packages/core/src/actor.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Context } from './context.js';
|
||||
import { ErrorCodes, VisibleError } from './error.js';
|
||||
|
||||
const Public = z.object({
|
||||
type: z.literal('public'),
|
||||
properties: z.object({})
|
||||
});
|
||||
|
||||
const User = z.object({
|
||||
type: z.literal('user'),
|
||||
properties: z.object({
|
||||
userID: z.string(),
|
||||
linkedAccountID: z.string(),
|
||||
fingerprint: z.string().optional()
|
||||
})
|
||||
});
|
||||
|
||||
const Member = z.object({
|
||||
type: z.literal('member'),
|
||||
properties: z.object({
|
||||
userID: z.string(),
|
||||
teamID: z.string(),
|
||||
role: z.enum(['owner', 'admin', 'member'])
|
||||
})
|
||||
});
|
||||
|
||||
const System = z.object({
|
||||
type: z.literal('system'),
|
||||
properties: z.object({
|
||||
teamID: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
const Admin = z.object({
|
||||
type: z.literal('admin'),
|
||||
properties: z.object({})
|
||||
});
|
||||
|
||||
/**
|
||||
* A registered nessh host, authenticated by its own credentials.
|
||||
*
|
||||
* Deliberately not a `user`: the box acts on behalf of whoever is logged into
|
||||
* it, which is not the same authority as its owner. It carries `ownerUserID`
|
||||
* for attribution, but `Actor.userID` refuses it, so a route written for a
|
||||
* signed-in human cannot silently accept a box instead.
|
||||
*/
|
||||
const Machine = z.object({
|
||||
type: z.literal('machine'),
|
||||
properties: z.object({
|
||||
machineID: z.string(),
|
||||
ownerUserID: z.string(),
|
||||
teamID: z.string().optional()
|
||||
})
|
||||
});
|
||||
|
||||
const ActorInfo = z.discriminatedUnion('type', [Public, User, Member, System, Admin, Machine]);
|
||||
type ActorInfo = z.infer<typeof ActorInfo>;
|
||||
|
||||
const _context = Context.create<ActorInfo>();
|
||||
|
||||
function _use(): ActorInfo {
|
||||
return _context.use();
|
||||
}
|
||||
|
||||
function _with<T>(value: ActorInfo, fn: () => T): T {
|
||||
return _context.provide(value, fn);
|
||||
}
|
||||
|
||||
function _assert<T extends ActorInfo['type']>(type: T): Extract<ActorInfo, { type: T }> {
|
||||
const actor = _use();
|
||||
if (actor.type !== type) {
|
||||
throw new VisibleError(
|
||||
'internal',
|
||||
ErrorCodes.Server.INTERNAL_ERROR,
|
||||
`Expected actor type ${type}, got ${actor.type}`
|
||||
);
|
||||
}
|
||||
return actor as Extract<ActorInfo, { type: T }>;
|
||||
}
|
||||
|
||||
export const Actor = {
|
||||
Info: ActorInfo,
|
||||
|
||||
use: _use,
|
||||
with: _with,
|
||||
assert: _assert,
|
||||
|
||||
get type(): ActorInfo['type'] {
|
||||
return _use().type;
|
||||
},
|
||||
|
||||
get userID(): string {
|
||||
const actor = _use();
|
||||
if (actor.type === 'user' || actor.type === 'member') {
|
||||
return actor.properties.userID;
|
||||
}
|
||||
if (actor.type === 'machine') {
|
||||
// A box holds credentials but is not its owner. Refusing here is
|
||||
// what keeps a route written for a human from accepting a box —
|
||||
// and it is a caller error, not a server fault, so it must not
|
||||
// surface as a 500.
|
||||
throw new VisibleError(
|
||||
'forbidden',
|
||||
ErrorCodes.Permission.INSUFFICIENT_PERMISSIONS,
|
||||
'A machine cannot act as its owner; this route requires a user session'
|
||||
);
|
||||
}
|
||||
throw new VisibleError(
|
||||
'internal',
|
||||
ErrorCodes.Server.INTERNAL_ERROR,
|
||||
`Actor type ${actor.type} has no userID`
|
||||
);
|
||||
},
|
||||
|
||||
get machineID(): string {
|
||||
const actor = _use();
|
||||
if (actor.type === 'machine') {
|
||||
return actor.properties.machineID;
|
||||
}
|
||||
throw new VisibleError(
|
||||
'internal',
|
||||
ErrorCodes.Server.INTERNAL_ERROR,
|
||||
`Actor type ${actor.type} has no machineID`
|
||||
);
|
||||
},
|
||||
|
||||
get linkedAccountID(): string {
|
||||
const actor = _use();
|
||||
if (actor.type === 'user') {
|
||||
return actor.properties.linkedAccountID;
|
||||
}
|
||||
throw new VisibleError(
|
||||
'internal',
|
||||
ErrorCodes.Server.INTERNAL_ERROR,
|
||||
`Actor type ${actor.type} has no linkedAccountID`
|
||||
);
|
||||
},
|
||||
|
||||
get fingerprint(): string | undefined {
|
||||
const actor = _use();
|
||||
if (actor.type === 'user') {
|
||||
return actor.properties.fingerprint;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
get useTeam(): string {
|
||||
const actor = _use();
|
||||
if (actor.type === 'member' || actor.type === 'system') {
|
||||
return actor.properties.teamID;
|
||||
}
|
||||
throw new VisibleError(
|
||||
'internal',
|
||||
ErrorCodes.Server.INTERNAL_ERROR,
|
||||
`Actor type ${actor.type} has no team scope`
|
||||
);
|
||||
},
|
||||
|
||||
get role(): 'owner' | 'admin' | 'member' {
|
||||
const actor = _use();
|
||||
if (actor.type === 'member') {
|
||||
return actor.properties.role;
|
||||
}
|
||||
throw new VisibleError(
|
||||
'internal',
|
||||
ErrorCodes.Server.INTERNAL_ERROR,
|
||||
`Actor type ${actor.type} has no role`
|
||||
);
|
||||
},
|
||||
|
||||
get isSignedIn(): boolean {
|
||||
try {
|
||||
return _use().type !== 'public';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user