Skip to content

Commit f317710

Browse files
committed
feat: improve typings with Constructor type
1 parent cfa163b commit f317710

3 files changed

Lines changed: 56 additions & 18 deletions

File tree

src/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { ApiClient as Auth0APIClient } from "@auth0/auth0-api-js";
3+
import type { AIChatAgent } from "agents/ai-chat-agent";
34
import { AsyncLocalStorage } from "node:async_hooks";
4-
import { Connection, ConnectionContext, WSMessage } from "partyserver";
5+
import { Connection, ConnectionContext, Server, WSMessage } from "partyserver";
56
import {
67
InsufficientScopeError,
78
InvalidTokenError,
89
UnauthorizedError,
910
} from "./bearer/errors.js";
1011
import getToken from "./bearer/index.js";
11-
import { AuthenticatedServer, TokenSet, WithAuthParams } from "./types.js";
12+
import {
13+
AuthenticatedServer,
14+
Constructor,
15+
TokenSet,
16+
WithAuthParams,
17+
} from "./types.js";
1218

1319
export interface Token {
1420
sub?: string;
@@ -56,11 +62,11 @@ function validateScopes(
5662
*/
5763
export const WithAuth = <
5864
Env extends { AUTH0_DOMAIN: string; AUTH0_AUDIENCE: string },
59-
TBase extends new (...args: any[]) => any,
65+
TBase extends Constructor<Server<Env>> | Constructor<AIChatAgent<Env>>,
6066
>(
6167
Base: TBase,
6268
options: WithAuthParams = { authRequired: true },
63-
): TBase & (new (...args: any[]) => AuthenticatedServer) => {
69+
) => {
6470
const authRequired = options.authRequired ?? true;
6571
const debug = options.debug ?? (() => {});
6672
// I had to do this because:
@@ -326,7 +332,7 @@ export const WithAuth = <
326332
tokenSetPerConnection.delete(connection.id);
327333
super.onClose(connection, code, reason, wasClean);
328334
}
329-
} as TBase & (new (...args: any[]) => AuthenticatedServer);
335+
};
330336
};
331337

332338
export { OwnedAgent, WithOwnership } from "./withOwnership.js";

src/withOwnership.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { Connection } from "partyserver";
3-
import { AuthorizedServer } from "./types.js";
2+
import type { AIChatAgent } from "agents/ai-chat-agent";
3+
import { Connection, Server } from "partyserver";
4+
import { AuthenticatedServer, AuthorizedServer, Constructor } from "./types.js";
45

56
interface DurableObjectState {
67
readonly storage: {
@@ -25,17 +26,21 @@ const isDurableObjectState = (state: any): state is DurableObjectState => {
2526
*
2627
* Every time a connection or request is made, the ownership is checked.
2728
*
28-
* @param Base - The base class to extend from.
29+
* @param Base - The base class to extend from. This should be a class that extends `Server` or `AIChatAgent` and implements `AuthenticatedServer`.
2930
*
3031
* @returns - A new class that extends the base class with ownership functionality.
3132
*/
32-
export const WithOwnership = <TBase extends new (...args: any[]) => any>(
33+
export const WithOwnership = <
34+
TBase extends
35+
| Constructor<Server<any> & AuthenticatedServer>
36+
| Constructor<AIChatAgent<any> & AuthenticatedServer>,
37+
>(
3338
Base: TBase,
3439
options: { debug?: (message: string, ctx: any) => void } = {},
35-
): TBase & (new (...args: any[]) => AuthorizedServer) => {
40+
): TBase & Constructor<AuthorizedServer> => {
3641
const debug = options.debug ?? (() => {});
3742

38-
return class extends Base {
43+
return class WithOwnership extends Base {
3944
async onAuthorizedConnect(connection: any, ctx: any): Promise<void> {
4045
debug("Authenticated connection", {
4146
connection,
@@ -58,7 +63,8 @@ export const WithOwnership = <TBase extends new (...args: any[]) => any>(
5863
* @returns - A boolean indicating if the current user is the owner.
5964
*/
6065
async #isCurrentUserOwner(): Promise<boolean> {
61-
const userInfo = this.getClaims();
66+
// Base class implements AuthenticatedServer, so getClaims() is available
67+
const userInfo = (this as any as AuthenticatedServer).getClaims();
6268
const objectOwner = await this.getOwner();
6369
if (objectOwner !== userInfo?.sub) {
6470
return false;
@@ -87,6 +93,8 @@ export const WithOwnership = <TBase extends new (...args: any[]) => any>(
8793
}
8894

8995
#getDurableStorage() {
96+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
97+
// @ts-expect-error
9098
const ctx = this.ctx;
9199
if (!isDurableObjectState(ctx)) {
92100
throw new Error(
@@ -110,7 +118,7 @@ export const WithOwnership = <TBase extends new (...args: any[]) => any>(
110118
async getOwner(): Promise<string | undefined> {
111119
return this.#getDurableStorage().get("owner");
112120
}
113-
} as TBase & (new (...args: any[]) => AuthorizedServer);
121+
} as unknown as TBase & Constructor<AuthorizedServer>;
114122
};
115123

116124
/**

test/flumix-compatibility.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("Flumix Compatibility", () => {
4545
// Test composing multiple mixins
4646
type BaseClass = typeof AIChatAgent<TestEnv>;
4747
type WithAuthClass = ReturnType<typeof AuthAgent<TestEnv, BaseClass>>;
48-
type ComposedClass = ReturnType<typeof OwnedAgent<WithAuthClass>>;
48+
type ComposedClass = ReturnType<typeof OwnedAgent<TestEnv, WithAuthClass>>;
4949
type ComposedInstance = InstanceType<ComposedClass>;
5050

5151
// Verify it has methods from both mixins
@@ -80,15 +80,39 @@ describe("Flumix Compatibility", () => {
8080
// import it, triggering the cloudflare: protocol error. Instead, we verify
8181
// that our mixins work with flumix using a mock base class.
8282

83+
// Mock the AIChatAgent class structure for type compatibility
8384
class MockServer<Env> {
8485
constructor(
8586
// eslint-disable-next-line @typescript-eslint/no-explicit-any
86-
public party: any,
87+
public ctx: any,
8788
public env?: Env,
8889
) {}
90+
91+
// Add stub methods to satisfy AIChatAgent interface
92+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
93+
async fetch(_request: Request): Promise<Response> {
94+
return new Response();
95+
}
96+
97+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
98+
async onConnect(_connection: any, _ctx: any): Promise<void> {}
99+
100+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
101+
async onMessage(_connection: any, _message: any): Promise<void> {}
102+
103+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
104+
async onRequest(_request: Request): Promise<Response> {
105+
return new Response();
106+
}
107+
108+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
109+
async onClose(_connection: any, _code: number, _reason: string, _wasClean: boolean): Promise<void> {}
110+
111+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
112+
onError(_connection: any, _error: unknown): void {}
89113
}
90114

91-
const SuperAgent = extend(MockServer<TestEnv>)
115+
const SuperAgent = extend(MockServer<TestEnv> as unknown as typeof AIChatAgent<TestEnv>)
92116
.with(AuthAgent)
93117
.with(OwnedAgent)
94118
.build();
@@ -123,8 +147,8 @@ describe("Flumix Compatibility", () => {
123147
AUTH0_DOMAIN: "test.auth0.com",
124148
AUTH0_AUDIENCE: "https://api.test.com",
125149
};
126-
const mockParty = {};
127-
const instance = new SuperAgent(mockParty, mockEnv);
150+
const mockCtx = {};
151+
const instance = new SuperAgent(mockCtx, mockEnv);
128152
expect(instance).toBeDefined();
129153
expect(typeof instance.getClaims).toBe("function");
130154
// eslint-disable-next-line @typescript-eslint/no-explicit-any

0 commit comments

Comments
 (0)