Further login process

* APIUtils addition: query validation
* Coach and Server accounts are now properly created if they do not exist
* Profiles now cannot be IDs 1 or 2 (reservedIds)
* Fixed profile username exists bug
* Added relationship manager
* Started relationship management
* DeviceClass and VRMovementMode enum defaults for reserved profiles
* Presence update simplification
* Progression fixes
* Relationship query and object fixes
* Base configuration is now rate limited
* Progression route no longer requires authentication, instead is rate limited
* Base relationships with reserved profiles (Coach and Server)
* DeviceClass required for login
* Get presence route
* Socket route no longer logs
* Socket target base finished
This commit is contained in:
2025-03-30 19:29:57 -04:00
parent 026f9c8bd8
commit 639e809a20
19 changed files with 270 additions and 81 deletions

View File

@@ -74,6 +74,15 @@ export const validateRequestBody = <T>(schema: z.ZodSchema<T>) => (rq: express.R
rs.status(400).json(genericResponseFormat(true, "Bad request", undefined, error.errors));
}
};
export const validateQuery = <T>(schema: z.ZodSchema<T>) => (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
try {
schema.parse(rq.query);
nxt();
} catch (error) {
if (error instanceof z.ZodError)
rs.status(400).json(genericResponseFormat(true, "Bad request", undefined, error.errors));
}
};
type genericResponse = {
failure: boolean,

View File

@@ -6,7 +6,7 @@ import Logging from "@proxnet/undead-logging";
const log = new Logging("Presence");
interface PresenceExport {
export interface PresenceExport {
roomInstance: RoomInstance | null;
playerId: number;
statusVisibility: PlayerStatusVisibility;
@@ -14,7 +14,6 @@ interface PresenceExport {
vrMovementMode?: VRMovementMode;
}
// Hot mess
class PlayerPresence {
intervalId: number;
@@ -59,27 +58,21 @@ class PlayerPresence {
if (visibilityResult.success) this.statusVisibility = visibilityResult.data;
}
async updateEnums() {
async update() {
this.updateOffline();
if (!this.offline) await this.updateStatusVisibility();
else this.statusVisibility = PlayerStatusVisibility.Offline;
// deviceClass
const DeviceClassEnum = z.nativeEnum(DeviceClass);
type DeviceClassEnum = z.infer<typeof DeviceClassEnum>;
this.deviceClass = await this.#profile.getKnownDeviceClass();
const deviceClassResult = DeviceClassEnum.safeParse(await this.#profile.getKnownDeviceClass());
if (deviceClassResult.success) this.deviceClass = deviceClassResult.data;
// vrMovementMode
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>;
const vrMovementMoveResult = VRMovementModeEnum.safeParse(await this.#profile.getVRMovementMode());
if (vrMovementMoveResult.success) this.vrMovementMode = vrMovementMoveResult.data;
this.vrMovementMode = await this.#profile.getVRMovementMode();
}
/**
* Export presence object. Please make sure to update values with `Presence.update()` (async) before calling this.
*/
async export() {
await this.updateEnums();
await this.update();
const exp: PresenceExport = {
playerId: this.playerId,
roomInstance: this.roomInstance,
@@ -108,7 +101,10 @@ class PresenceBase {
*/
async getAllPresences() {
const presSet: Set<PresenceExport> = new Set();
for (const pres of presence.values()) presSet.add(await pres.export());
for (const pres of presence.values()) {
await pres.update();
presSet.add(await pres.export());
}
return presSet;
}
@@ -119,7 +115,7 @@ class PresenceBase {
async create(player: Profile) {
if (!presence.values().find(pres => pres.playerId == player.getId())) {
const pres = new PlayerPresence(player);
await pres.updateEnums();
await pres.update();
presence.add(pres);
}
log.d(`Presences: ${JSON.stringify(Array.from(await Presence.getAllPresences()))}`);
@@ -130,7 +126,7 @@ class PresenceBase {
if (pres) return pres;
else {
const pres = new PlayerPresence(player, true);
await pres.updateEnums();
await pres.update();
presence.add(pres);
return pres;
}

View File

@@ -57,7 +57,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
if (xp >= item.RequiredXp) {
const current = config?.LevelProgressionMaps[config?.LevelProgressionMaps.indexOf(item)];
if (typeof current == 'undefined') return null;
if (typeof current == 'undefined') return 1;
else return current.Level;
}
@@ -72,7 +72,9 @@ export class ProfileProgressionManager extends ProfileContentManager {
const parsedData = parseInt(data);
if (isNaN(parsedData)) {
log.w(`Parsed xp data for ${this.profileId} is NaN!`);
return 0; // fallback since progression data is required
const one = config?.LevelProgressionMaps[1];
if (typeof one == 'undefined' && !one) return 0; // fallback since progression data is required
else return one.RequiredXp;
} else return parsedData;
}

View File

@@ -0,0 +1,64 @@
import { Profile } from "../profiles.ts";
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
enum RelationshipType {
None,
FriendRequestSent,
FriendRequestReceived,
Friend
}
enum ReciprocalStatus {
None,
Local,
Remote,
Mutual
}
interface Relationship {
PlayerID: number, // target player, not this player
RelationshipType: RelationshipType,
Muted: ReciprocalStatus,
Ignored: ReciprocalStatus
}
export class ProfileRelationshipManager extends ProfileContentManager {
#baseRelationships: Relationship[] = [
{
PlayerID: 1,
RelationshipType: RelationshipType.Friend,
Muted: ReciprocalStatus.None,
Ignored: ReciprocalStatus.None
},
{
PlayerID: 2,
RelationshipType: RelationshipType.Friend,
Muted: ReciprocalStatus.None,
Ignored: ReciprocalStatus.None
}
]
getRelationships() {
return this.#baseRelationships; // temporary
}
setPlayerIgnored(player: Profile) {
}
setPlayerMuted(player: Profile) {
}
getPlayerRelationship(player: Profile) {
}
sendPlayerFriendRequest(player: Profile) {
}
}

View File

@@ -5,9 +5,10 @@ export class ProfileReputationManager extends ProfileContentManager {
async getReputation() { // async temporary
return {
AccountId: this.profileId,
Noterity: 0.0,
Noteriety: 0.0,
CheerGeneral: 0,
CheerHelpful: 0,
CheerGreatHost: 0,
CheerSportsman: 0,
CheerCreative: 0,
CheerCredit: 0,

View File

@@ -12,6 +12,7 @@ import { ProfileSettingsManager } from "./profile/settings.ts";
import { ProfileProgressionManager } from "./profile/progression.ts";
import { ProfileReputationManager } from "./profile/reputation.ts";
import Logging from "@proxnet/undead-logging";
import { ProfileRelationshipManager } from "./profile/relationships.ts";
const config = Config.getConfig();
@@ -19,11 +20,17 @@ const log = new Logging("Profiles");
interface ProfileInitOptions {
username: string;
/**
* Ignore the random generation of profile IDs; use this ID.
*
* Make sure the target account does not exist before using this option.
*/
id?: number;
}
interface AccountExport {
accountId: number;
profileImage: string;
isJunior: boolean;
isJunior?: boolean;
platforms: number;
username: string;
displayName: string;
@@ -34,6 +41,8 @@ export interface ProfileTokenFormat extends TokenBaseFormat {
typ: AuthType.Game;
}
const reservedIds = [1, 2];
class Profile {
static async exists(id: number) {
return (await Redis.Database.exists(
@@ -44,6 +53,11 @@ class Profile {
),
)) >= 1;
}
static async existsByName(name: string) {
return (await Redis.Database.exists(
Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name),
)) >= 1;
}
static async getUniqueId() {
let id = Math.round(Math.random() * Math.pow(2, 31));
@@ -55,18 +69,15 @@ class Profile {
Redis.KeyGroups.Profiles.Username,
),
)) >= 1
) {
id = await this.getUniqueId();
}
|| reservedIds.includes(id)
) id = await this.getUniqueId();
return id;
}
static async byName(name: string) {
const id = await Redis.Database.get(
Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name),
);
const id = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name));
if (id == null) return null;
else return new Profile(parseInt(id, 10));
else return new Profile(parseInt(id));
}
static async getUniqueUsername() {
@@ -83,12 +94,11 @@ class Profile {
static async init(options?: ProfileInitOptions) {
const optionsSpecified = typeof options !== "undefined";
if (options?.username) {
const existingUser = await Profile.byName(options.username);
if (existingUser == null) return null;
}
if (options?.username && await Profile.existsByName(options.username)) return null;
const newId = await this.getUniqueId();
let newId: number;
if (options?.id) newId = options.id;
else newId = await this.getUniqueId();
const newUsername = optionsSpecified
? options.username
: await this.getUniqueUsername();
@@ -153,6 +163,7 @@ class Profile {
Settings = new ProfileSettingsManager();
Progression = new ProfileProgressionManager();
Reputation = new ProfileReputationManager();
Relationships = new ProfileRelationshipManager();
constructor(id: number) {
this.#id = id;
@@ -161,6 +172,7 @@ class Profile {
this.Settings.setProfile(this.#id);
this.Progression.setProfile(this.#id);
this.Reputation.setProfile(this.#id);
this.Relationships.setProfile(this.#id);
}
setInstance(instance: RoomInstance | null) {
@@ -183,11 +195,13 @@ class Profile {
return await Profile.getExportAccount(this.#id);
}
async setKnownDeviceClass(deviceClass: string | number) {
async setKnownDeviceClass(deviceClass: DeviceClass) {
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass), deviceClass);
}
async getKnownDeviceClass() {
if (reservedIds.includes(this.#id)) return DeviceClass.Unknown;
const data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass));
if (data == null) {
log.w(`No known device class for ${this.#id}`);
@@ -212,14 +226,16 @@ class Profile {
}
async getVRMovementMode() {
if (reservedIds.includes(this.#id)) return VRMovementMode.Teleport;
const data = await this.Settings.getSetting(SettingKey.VRMovementMode);
if (data == null) {
log.w(`No known device class for ${this.#id}`);
log.w(`No known VR movement mode for ${this.#id} (harmless if OOBE not ran)`);
return VRMovementMode.Teleport;
}
const parsedData = parseInt(data);
if (isNaN(parsedData)) {
log.w(`Malformed device class for ${this.#id}`);
log.w(`Malformed VR movement mode for ${this.#id}`);
return VRMovementMode.Teleport;
}
@@ -258,6 +274,7 @@ class Profile {
const profiles: Map<number, Profile> = new Map()
// Control what is available to references
class UnifiedProfileBase {
get(id: number) {
@@ -278,6 +295,10 @@ class UnifiedProfileBase {
return await Profile.exists(id);
}
async existsByName(name: string) {
return await Profile.existsByName(name);
}
}
const UnifiedProfile = new UnifiedProfileBase();

View File

@@ -188,8 +188,8 @@ try {
PLACE TEST HERE
*/
if (!(await UnifiedProfile.exists(1))) UnifiedProfile.create({ username: "Coach" }); // create Coach if they do not exist
if (!(await UnifiedProfile.exists(2))) UnifiedProfile.create({ username: "Server" }); // create Server if they do not exist
if (!(await UnifiedProfile.existsByName("Coach"))) UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
if (!(await UnifiedProfile.existsByName("Server"))) UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
});

View File

@@ -28,6 +28,12 @@ route.router.post("/create",
async (_rq, rs) => {
const newAcc = await Profile.init();
if (newAcc == null) {
rs.json({
success: false
});
return;
}
rs.locals.user.addAssociatedProfile(newAcc.getId());

View File

@@ -3,7 +3,9 @@ import { GameConfigs } from "../../data/config.ts";
export const route = APIUtils.createRouter("/config");
route.router.get("/v2", (_rq, rs) => {
const rateLimit = new APIUtils.RateLimiter(60, 2);
route.router.get("/v2", rateLimit.middle(), (_rq, rs) => {
const config = GameConfigs.getConfig();
if (config == null) rs.sendStatus(500);
else rs.json(config);
@@ -11,6 +13,7 @@ route.router.get("/v2", (_rq, rs) => {
route.router.get('/v1/amplitude',
APIUtils.setCacheAllowed,
rateLimit.middle(),
(_rq, rs) => {
rs.json({AmplitudeKey: ""});
}

View File

@@ -10,7 +10,7 @@ route.router.get('/v1/:id',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
(rq: express.Request<{ id: string }>, rs) => {
async (rq: express.Request<{ id: string }>, rs) => {
const unparsedPlayerId = rq.params.id;
const parsedPlayerId = parseInt(unparsedPlayerId);
if (isNaN(parsedPlayerId)) {
@@ -18,7 +18,7 @@ route.router.get('/v1/:id',
return;
}
rs.json(UnifiedProfile.get(parsedPlayerId).Reputation.getReputation());
rs.json(await UnifiedProfile.get(parsedPlayerId).Reputation.getReputation());
}
);

View File

@@ -5,11 +5,13 @@ import UnifiedProfile from "../../data/profiles.ts";
const log = new Logging("ProgressionRoute");
const rateLimit = new APIUtils.RateLimiter(60, 2);
export const route = APIUtils.createRouter("/players");
route.router.get('/v1/progression/:id',
APIUtils.Authentication,
rateLimit.middle(),
async (rq: express.Request<{ id: string }>, rs) => {
const unparsedPlayerId = rq.params.id;

View File

@@ -9,7 +9,7 @@ route.router.get('/v2/get',
APIUtils.AuthenticationType(AuthType.Game),
(_rq, rs) => {
rs.json([]); // temporary
rs.json(rs.locals.profile.Relationships.getRelationships());
}
);

View File

@@ -98,6 +98,7 @@ route.router.post("/token",
rq.body.platform === "0",
rq.body.ver === '20191120',
rq.body.device_class.length === 1,
!isNaN(Number(rq.body.device_class)),
!(rq.body.device_id.length > 96),
!(rq.body.client_secret.length > 96),
!(rq.body.platform_id.length > 32),
@@ -110,7 +111,6 @@ route.router.post("/token",
return;
}
const accounts = await rs.locals.user.getAssociatedProfiles();
let targetAccount: number;
if (rq.body.grant_type == 'cached_login') targetAccount = parseInt(rq.body.account_id);
@@ -137,6 +137,8 @@ route.router.post("/token",
requestFailed();
return;
}
const accounts = await rs.locals.user.getAssociatedProfiles();
if (!accounts.has(targetAccount)) {
requestFailed("access_denied");
return;
@@ -160,6 +162,6 @@ route.router.post("/token",
refresh_token: token,
});
await profile.setKnownDeviceClass(rq.body.device_class);
await profile.setKnownDeviceClass(Number(rq.body.device_class));
},
);

View File

@@ -2,9 +2,10 @@ import { z } from "zod";
import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express";
import Matchmaking from "../../data/live/base.ts";
import Presence from "../../data/live/presence.ts";
import Presence, { PresenceExport } from "../../data/live/presence.ts";
import { AuthType } from "../../data/users.ts";
import Logging from "@proxnet/undead-logging";
import UnifiedProfile from "../../data/profiles.ts";
const log = new Logging("MatchPlayerRoute");
@@ -18,6 +19,31 @@ const LoginSchema = z.object({
LoginLock: z.string().uuid("LoginLock must be a UUIDv4")
});
route.router.get('/',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
APIUtils.validateQuery(z.object({ id: z.union([z.string(), z.array(z.string())]) })),
async (rq: express.Request<NoBody, PresenceExport[], NoBody, { id: string[] | string }>, rs) => {
let ids: number[] = [];
if (typeof rq.query.id == 'object') ids = rq.query.id.map(val => parseInt(val));
else ids.push(parseInt(rq.query.id));
ids = ids.filter(val => !isNaN(val));
const presExport: PresenceExport[] = [];
for (const id of ids) {
const pres = await Presence.get(UnifiedProfile.get(id));
await pres.update();
presExport.push(await pres.export());
}
rs.json(presExport);
log.d(JSON.stringify(presExport));
}
)
route.router.post('/login',
APIUtils.Authentication,
@@ -40,8 +66,9 @@ route.router.post('/logout',
express.urlencoded({extended: true}),
APIUtils.validateRequestBody(LoginSchema),
(rq, rs) => {
(_rq, rs) => {
Matchmaking.deleteLoginLock(rs.locals.profile);
rs.sendStatus(200);
}
)
@@ -56,6 +83,7 @@ route.router.post('/heartbeat',
async (_rq, rs) => {
const pres = await Presence.get(rs.locals.profile);
await pres.update();
log.d(`pres heartbeat for ${rs.locals.profile.getId()}: ${JSON.stringify(await pres.export())}`);
rs.json(await pres.export());
}

View File

@@ -2,7 +2,6 @@ import { APIUtils } from "../apiutils.ts";
import { Config } from "../config.ts";
import { AuthType } from "../data/users.ts";
import { SocketHandoff } from "./handoff.ts";
import express from "express";
const config = Config.getConfig();
@@ -12,8 +11,6 @@ route.router.post('/hub/v1/negotiate',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }),
APIUtils.logBody,
(_rq, rs) => {
const handoff = new SocketHandoff();

View File

@@ -1,6 +1,18 @@
import { Profile } from "../data/profiles.ts";
import Logging from "@proxnet/undead-logging";
import { Message, MessageKind, SignalMessageType, SignalRMessage, SignalRMessageSchema, TargetResult, TargetResultFailure, TargetResultSuccess, TargetResultType } from "./types.ts";
import {
CompletionMessage,
Message,
MessageKind,
SignalMessageType,
SignalRMessage,
SignalRMessageSchema,
TargetResult,
TargetResultFailure,
TargetResultNotATarget,
TargetResultSuccess,
TargetResultType
} from "./types.ts";
import { SocketTarget } from "./targets/targetbase.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
@@ -26,22 +38,55 @@ export class SignalRSocketHandler {
}
async #dispatchTarget<T = unknown>(target: string, args: object[]): Promise<TargetResult> {
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
const targetExec = this.#Targets.get(target);
if (!targetExec) return { type: TargetResultType.Failure } as TargetResultFailure;
else return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
if (!targetExec) return { type: TargetResultType.NotATarget } as TargetResultNotATarget;
else {
try {
return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
} catch (err) {
this.#log.w(`Target '${target}' function error: ${err}`);
if (err instanceof Error) return { type: TargetResultType.Failure, err: err } as TargetResultFailure;
else return { type: TargetResultType.Failure, err: `${err}` } as TargetResultFailure;
}
}
}
#onMessage(message: Message) {
async #onMessage(message: Message) {
if (message.kind == MessageKind.Protocol) {
this.sendRaw({});
return;
} else {
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
if (res.type == TargetResultType.Success) {
const signalRes: CompletionMessage = {
type: SignalMessageType.Completion,
invocationId: message.data.invocationId,
result: JSON.stringify(res.data)
}
this.sendRaw(signalRes);
} else if (res.type == TargetResultType.Failure) {
const signalRes: CompletionMessage = {
type: SignalMessageType.Completion,
invocationId: message.data.invocationId,
error: res.err instanceof Error ? res.err.message : res.err
}
this.sendRaw(signalRes);
} else {
const signalRes: CompletionMessage = {
type: SignalMessageType.Completion,
invocationId: message.data.invocationId,
error: "Target not found"
}
this.sendRaw(signalRes);
}
}
}
}
async #init() {
#init() {
this.#log.source += this.#profile.getId().toString();
this.#log.i(`Created hub socket`);
@@ -82,6 +127,9 @@ export class SignalRSocketHandler {
sendRaw(data: object) {
this.#socket.send(`${JSON.stringify(data)}\u001e`);
// todo sometime: make this less confusing
const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
}
}

View File

@@ -1,5 +1,10 @@
import { z } from "zod";
import { SocketTarget } from "./targetbase.ts";
const ArgumentSchema = z.object({
PlayerIds: z.array(z.number())
});
export class PlayerSocketSubscriptionTarget extends SocketTarget {
subscriptions: number[] = [];
@@ -9,8 +14,12 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
}
// deno-lint-ignore require-await
override async exec(_args: (object | string | number | boolean)[]) {
return;
override async exec(args: unknown) {
const parsed = ArgumentSchema.safeParse(args);
if (parsed.success) {
this.setSubscriptions(parsed.data.PlayerIds);
return;
} else throw new Error("Invalid arguments");
}
}

View File

@@ -13,7 +13,7 @@ export class SocketTarget {
}
// deno-lint-ignore require-await
async exec(_args: (object | string | number | boolean)[]) {
async exec(_args: unknown) {
throw new Error("Execution for this target is not set.");
}

View File

@@ -4,14 +4,14 @@ export enum MessageKind {
Protocol,
Data
}
interface MessageBase {
export interface MessageBase {
kind: MessageKind
}
interface DataMessage extends MessageBase {
export interface DataMessage extends MessageBase {
kind: MessageKind.Data,
data: SignalRMessage
}
interface ProtocolMessage extends MessageBase {
export interface ProtocolMessage extends MessageBase {
kind: MessageKind.Protocol
}
export type Message = ProtocolMessage | DataMessage;
@@ -34,68 +34,68 @@ export enum SignalMessageType {
Close
}
interface BaseMessage {
export interface BaseMessage {
type: SignalMessageType;
}
interface InvocationMessage extends BaseMessage {
export interface InvocationMessage extends BaseMessage {
type: SignalMessageType.Invocation;
target: string;
arguments: unknown[];
invocationId?: string;
}
interface StreamItemMessage extends BaseMessage {
export interface StreamItemMessage extends BaseMessage {
type: SignalMessageType.StreamItem;
invocationId: string;
item: unknown;
}
interface CompletionMessage extends BaseMessage {
export interface CompletionMessage extends BaseMessage {
type: SignalMessageType.Completion;
invocationId: string;
result?: unknown;
error?: string;
}
interface PingMessage extends BaseMessage {
export interface PingMessage extends BaseMessage {
type: SignalMessageType.Ping;
}
interface CloseMessage extends BaseMessage {
export interface CloseMessage extends BaseMessage {
type: SignalMessageType.Close;
error?: string;
}
const BaseMessageSchema = z.object({
export const BaseMessageSchema = z.object({
type: z.nativeEnum(SignalMessageType),
});
const InvocationMessageSchema = BaseMessageSchema.extend({
export const InvocationMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Invocation),
target: z.string(),
arguments: z.array(z.unknown()),
invocationId: z.string().optional(),
});
const StreamItemMessageSchema = BaseMessageSchema.extend({
export const StreamItemMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.StreamItem),
invocationId: z.string(),
item: z.unknown(),
});
const CompletionMessageSchema = BaseMessageSchema.extend({
export const CompletionMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Completion),
invocationId: z.string(),
result: z.unknown().optional(),
error: z.string().optional(),
});
const PingMessageSchema = BaseMessageSchema.extend({
export const PingMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Ping),
});
const CloseMessageSchema = BaseMessageSchema.extend({
export const CloseMessageSchema = BaseMessageSchema.extend({
type: z.literal(SignalMessageType.Close),
error: z.string().optional(),
});
@@ -113,7 +113,7 @@ export enum TargetResultType {
Failure,
NotATarget
}
interface TargetResultBase {
export interface TargetResultBase {
type: TargetResultType
}
export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
@@ -122,6 +122,7 @@ export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
}
export interface TargetResultFailure extends TargetResultBase {
type: TargetResultType.Failure
err: string | Error
}
export interface TargetResultNotATarget extends TargetResultBase {
type: TargetResultType.NotATarget