Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 11 additions & 41 deletions packages/cli/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export function normalizePublicKey(key: string): string {
return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----`;
}

export interface CreateProxyAppOptions {
client: ClientContract<SchemaDef>;
schema: SchemaDef;
authDb?: ClientContract<SchemaDef>;
export interface CreateProxyAppOptions<Schema extends SchemaDef = SchemaDef> {
client: ClientContract<Schema>;
schema: Schema;
authDb?: ClientContract<Schema>;
auth?: {
studioAuthKey?: string;
/** Seconds within which a signed request is considered valid. Defaults to 60. */
Expand All @@ -49,37 +49,7 @@ export interface CreateProxyAppOptions {
cors?: Parameters<typeof cors>[0];
}

export function createProxyApp(options: CreateProxyAppOptions): Hono;
export function createProxyApp(
client: ClientContract<SchemaDef>,
schema: SchemaDef,
authDb?: ClientContract<SchemaDef>,
auth?: {
studioAuthKey?: string;
signatureToleranceSecs?: number;
},
): Hono;
export function createProxyApp(
optionsOrClient: CreateProxyAppOptions | ClientContract<SchemaDef>,
schema?: SchemaDef,
authDb?: ClientContract<SchemaDef>,
auth?: {
studioAuthKey?: string;
signatureToleranceSecs?: number;
},
): Hono {
let options: CreateProxyAppOptions;
if ('client' in optionsOrClient && 'schema' in optionsOrClient) {
options = optionsOrClient as CreateProxyAppOptions;
} else {
options = {
client: optionsOrClient as ClientContract<SchemaDef>,
schema: schema!,
authDb,
auth,
};
}

export function createProxyApp<Schema extends SchemaDef = SchemaDef>(options: CreateProxyAppOptions<Schema>): Hono {
const app = new Hono();
app.use('*', cors(options.cors));

Expand All @@ -93,7 +63,7 @@ export function createProxyApp(

app.use(
'/api/model/*',
createHonoHandler({
createHonoHandler<Schema>({
apiHandler: new RPCApiHandler({ schema: options.schema }),
getClient: (c) =>
resolveClient(options.client, options.authDb ?? options.client, c, !!options.auth?.studioAuthKey),
Expand Down Expand Up @@ -174,12 +144,12 @@ export function createSignatureMiddleware(publicKey: string, toleranceSeconds: n
};
}

export function resolveClient(
client: ClientContract<SchemaDef>,
authDb: ClientContract<SchemaDef>,
export function resolveClient<Schema extends SchemaDef = SchemaDef>(
client: ClientContract<Schema>,
authDb: ClientContract<Schema>,
c: Context,
isAuthKeyEnabled: boolean,
): ClientContract<SchemaDef> {
): ClientContract<Schema> {
const authHeader = c.req.header('authorization');

if (!isAuthKeyEnabled && !authHeader) {
Expand All @@ -204,6 +174,6 @@ export function resolveClient(
if (claim.type === 'superUser') {
return client;
} else {
return authDb.$setAuth(claim.data as any) as ClientContract<SchemaDef>;
return authDb.$setAuth(claim.data as any) as ClientContract<Schema>;
}
}
123 changes: 92 additions & 31 deletions packages/cli/test/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,14 @@ async function createPolicyApp(zmodel: string) {
const authDb = client.$use(new PolicyPlugin());
return {
client,
app: createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
app: createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
},
}),
};
}
Expand Down Expand Up @@ -106,7 +111,11 @@ describe('CLI proxy tests', () => {

const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb);
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
});
const baseUrl = await startAt(app);

const r = await fetch(`${baseUrl}/api/schema`);
Expand Down Expand Up @@ -169,7 +178,11 @@ describe('CLI proxy tests', () => {
});

const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb);
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
});
const baseUrl = await startAt(app);

// Create a user via the proxy API.
Expand Down Expand Up @@ -217,7 +230,11 @@ describe('CLI proxy tests', () => {

const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb);
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
});
const baseUrl = await startAt(app);

const txRes = await fetch(`${baseUrl}/api/model/$transaction/sequential`, {
Expand Down Expand Up @@ -397,7 +414,11 @@ describe('CLI proxy tests', () => {
// No studioAuthKey — backwards-compatible mode
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb);
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
});
const baseUrl = await startAt(app);

// No signature header — should still work
Expand Down Expand Up @@ -426,9 +447,14 @@ describe('CLI proxy tests', () => {
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
// Pass the key as raw base64 DER — no PEM markers
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY_DER,
signatureToleranceSecs: 60,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY_DER,
signatureToleranceSecs: 60,
},
});
const baseUrl = await startAt(app);

Expand All @@ -449,9 +475,14 @@ describe('CLI proxy tests', () => {
// No studioAuthKey option — would normally fall back to env var via run();
// here we verify the middleware still works when the resolved key is provided.
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: process.env['ZENSTACK_STUDIO_AUTH_KEY'],
signatureToleranceSecs: 60,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: process.env['ZENSTACK_STUDIO_AUTH_KEY'],
signatureToleranceSecs: 60,
},
});
const baseUrl = await startAt(app);

Expand Down Expand Up @@ -480,9 +511,14 @@ describe('CLI proxy tests', () => {
it('should reject a request whose timestamp is older than the tolerance window', async () => {
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
},
});
const baseUrl = await startAt(app);

Expand All @@ -507,9 +543,14 @@ describe('CLI proxy tests', () => {
it('should reject a request whose timestamp is too far in the future', async () => {
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
},
});
const baseUrl = await startAt(app);

Expand All @@ -535,9 +576,14 @@ describe('CLI proxy tests', () => {
const client = await createTestClient(zmodel);
// Custom tolerance of 300 seconds
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 300,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 300,
},
});
const baseUrl = await startAt(app);

Expand All @@ -561,9 +607,14 @@ describe('CLI proxy tests', () => {
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
// Very tight tolerance of 5 seconds
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 5,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 5,
},
});
const baseUrl = await startAt(app);

Expand Down Expand Up @@ -597,9 +648,14 @@ describe('CLI proxy tests', () => {
it('should reject a valid signature if it was produced without the Authorization token', async () => {
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
},
});
const baseUrl = await startAt(app);

Expand Down Expand Up @@ -627,9 +683,14 @@ describe('CLI proxy tests', () => {
it('should accept a request where the signature covers the Authorization token', async () => {
const client = await createTestClient(zmodel);
const authDb = client.$use(new PolicyPlugin());
const app = createProxyApp(client, client.$schema, authDb, {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
const app = createProxyApp({
client,
schema: client.$schema,
authDb,
auth: {
studioAuthKey: TEST_PUBLIC_KEY,
signatureToleranceSecs: 60,
},
});
const baseUrl = await startAt(app);

Expand Down
Loading