1+ import { serve } from '@hono/node-server' ;
12import {
23 ConfigExpr ,
34 InvocationExpr ,
@@ -13,30 +14,41 @@ import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
1314import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite' ;
1415import type { SchemaDef } from '@zenstackhq/orm/schema' ;
1516import { PolicyPlugin } from '@zenstackhq/plugin-policy' ;
16- import { RPCApiHandler } from '@zenstackhq/server/api' ;
17- import { createHonoHandler } from '@zenstackhq/server/hono' ;
18- import { serve } from '@hono/node-server' ;
19- import { Hono , type Context , type MiddlewareHandler } from 'hono' ;
20- import { cors } from 'hono/cors' ;
17+ import type { DataSourceProviderType } from '@zenstackhq/schema' ;
2118import type BetterSqlite3 from 'better-sqlite3' ;
2219import colors from 'colors' ;
2320import { createJiti } from 'jiti' ;
2421import type { createPool as MysqlCreatePool } from 'mysql2' ;
25- import { verify } from 'node:crypto' ;
2622import fs from 'node:fs' ;
2723import path from 'node:path' ;
2824import ora from 'ora' ;
2925import { detect , resolveCommand } from 'package-manager-detector' ;
3026import type { Pool as PgPoolType } from 'pg' ;
3127import { CliError } from '../cli-error' ;
28+ import {
29+ createProxyApp ,
30+ type CreateProxyAppOptions ,
31+ createSignatureMiddleware ,
32+ normalizePublicKey ,
33+ ProxyAuthError ,
34+ type ProxyAuthErrorCode ,
35+ resolveClient ,
36+ } from '../proxy' ;
3237import { execSync } from '../utils/exec-utils' ;
33- import { getVersion } from '../utils/version-utils' ;
3438import { getOutputPath , getSchemaFile , isPackageInstalled , loadPackage , loadSchemaDocument } from './action-utils' ;
35- import type { DataSourceProviderType } from '@zenstackhq/schema' ;
3639import { runPull } from './db' ;
37- import { z } from 'zod' ;
3840import { run as runGenerate } from './generate' ;
3941
42+ export {
43+ createProxyApp ,
44+ type CreateProxyAppOptions ,
45+ createSignatureMiddleware ,
46+ normalizePublicKey ,
47+ ProxyAuthError ,
48+ type ProxyAuthErrorCode ,
49+ resolveClient ,
50+ } ;
51+
4052type Options = {
4153 output ?: string ;
4254 schema ?: string ;
@@ -48,34 +60,6 @@ type Options = {
4860 introspect ?: boolean ;
4961} ;
5062
51- export const ProxyAuthError = {
52- MISSING_SIGNATURE_HEADER : 'Missing x-zenstack-signature header' ,
53- INVALID_TIMESTAMP : 'Request timestamp is expired or invalid' ,
54- INVALID_SIGNATURE_FORMAT : 'Invalid x-zenstack-signature format' ,
55- } as const ;
56-
57- export type ProxyAuthErrorCode = keyof typeof ProxyAuthError ;
58-
59- function rejectAuth ( c : Context , code : ProxyAuthErrorCode ) {
60- return c . json ( { code, message : ProxyAuthError [ code ] } , 401 ) ;
61- }
62-
63- const UserClaimSchema = z . discriminatedUnion ( 'type' , [
64- z . object ( { type : z . literal ( 'superUser' ) } ) ,
65- z . object ( { type : z . literal ( 'user' ) , data : z . record ( z . string ( ) , z . unknown ( ) ) } ) ,
66- ] ) ;
67-
68- type UserClaim = z . infer < typeof UserClaimSchema > ;
69-
70- function normalizePublicKey ( key : string ) : string {
71- key = key . trim ( ) ;
72- if ( key . startsWith ( '-----BEGIN PUBLIC KEY-----' ) ) {
73- return key ;
74- }
75- const b64 = key . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
76- return `-----BEGIN PUBLIC KEY-----\n${ b64 } \n-----END PUBLIC KEY-----` ;
77- }
78-
7963export async function run ( options : Options ) {
8064 // Resolve public key: CLI arg takes precedence, then ZENSTACK_STUDIO_AUTH_KEY env var.
8165 options = { ...options , studioAuthKey : options . studioAuthKey ?? process . env [ 'ZENSTACK_STUDIO_AUTH_KEY' ] } ;
@@ -258,176 +242,6 @@ export async function createDialect(provider: string, databaseUrl: string, schem
258242 throw new CliError ( `Unsupported database provider: ${ provider } ` ) ;
259243 }
260244}
261- export interface CreateProxyAppOptions {
262- client : ClientContract < SchemaDef > ;
263- schema : SchemaDef ;
264- authDb ?: ClientContract < SchemaDef > ;
265- auth ?: {
266- studioAuthKey : string ;
267- /** Seconds within which a signed request is considered valid. Defaults to 60. */
268- signatureToleranceSecs : number ;
269- } ;
270- cors ?: Parameters < typeof cors > [ 0 ] ;
271- }
272-
273- export function createProxyApp ( options : CreateProxyAppOptions ) : Hono ;
274- export function createProxyApp (
275- client : ClientContract < SchemaDef > ,
276- schema : SchemaDef ,
277- authDb ?: ClientContract < SchemaDef > ,
278- auth ?: {
279- studioAuthKey : string ;
280- signatureToleranceSecs : number ;
281- } ,
282- ) : Hono ;
283- export function createProxyApp (
284- optionsOrClient : CreateProxyAppOptions | ClientContract < SchemaDef > ,
285- schema ?: SchemaDef ,
286- authDb ?: ClientContract < SchemaDef > ,
287- auth ?: {
288- studioAuthKey : string ;
289- signatureToleranceSecs : number ;
290- } ,
291- ) : Hono {
292- let options : CreateProxyAppOptions ;
293- if ( 'client' in optionsOrClient && 'schema' in optionsOrClient ) {
294- options = optionsOrClient as CreateProxyAppOptions ;
295- } else {
296- options = {
297- client : optionsOrClient as ClientContract < SchemaDef > ,
298- schema : schema ! ,
299- authDb,
300- auth,
301- } ;
302- }
303-
304- const app = new Hono ( ) ;
305- app . use ( '*' , cors ( options . cors ) ) ;
306-
307- if ( options . auth ?. studioAuthKey ) {
308- const toleranceSecs = options . auth . signatureToleranceSecs ;
309- const normalizedKey = normalizePublicKey ( options . auth . studioAuthKey ) ;
310- const sigMiddleware = createSignatureMiddleware ( normalizedKey , toleranceSecs ) ;
311- app . use ( '/api/model/*' , sigMiddleware ) ;
312- app . use ( '/api/schema' , sigMiddleware ) ;
313- }
314-
315- app . use (
316- '/api/model/*' ,
317- createHonoHandler ( {
318- apiHandler : new RPCApiHandler ( { schema : options . schema } ) ,
319- getClient : ( c ) =>
320- resolveClient ( options . client , options . authDb ?? options . client , c , ! ! options . auth ?. studioAuthKey ) ,
321- } ) ,
322- ) ;
323-
324- app . get ( '/api/schema' , ( c ) => {
325- return c . json ( { ...options . schema , zenstackVersion : getVersion ( ) } ) ;
326- } ) ;
327-
328- return app ;
329- }
330-
331- function createSignatureMiddleware ( publicKey : string , toleranceSeconds : number ) : MiddlewareHandler {
332- let lastInvalidSigWarnAt = 0 ;
333- const WARN_THROTTLE_SECS = 60 ;
334-
335- function warnInvalidSignature ( ) {
336- const now = Math . floor ( Date . now ( ) / 1000 ) ;
337- if ( now - lastInvalidSigWarnAt >= WARN_THROTTLE_SECS ) {
338- lastInvalidSigWarnAt = now ;
339- console . warn (
340- colors . yellow (
341- 'Warning: Received a request with an invalid signature. ' +
342- 'Please double-check whether you have the correct public API key configured.' ,
343- ) ,
344- ) ;
345- }
346- }
347-
348- return async ( c , next ) => {
349- const signatureHeader = c . req . header ( 'x-zenstack-signature' ) ;
350- if ( ! signatureHeader ) {
351- return rejectAuth ( c , 'MISSING_SIGNATURE_HEADER' ) ;
352- }
353-
354- const parts = signatureHeader . split ( ',' ) ;
355- const timestampPart = parts . find ( ( p ) => p . startsWith ( 't=' ) ) ;
356- const sigPart = parts . find ( ( p ) => p . startsWith ( 'v1=' ) ) ;
357- if ( ! timestampPart || ! sigPart ) {
358- return rejectAuth ( c , 'INVALID_SIGNATURE_FORMAT' ) ;
359- }
360- const timestamp = timestampPart . substring ( 2 ) ;
361- const sig = sigPart . substring ( 3 ) ;
362-
363- const requestTime = parseInt ( timestamp , 10 ) ;
364- const now = Math . floor ( Date . now ( ) / 1000 ) ;
365- if ( isNaN ( requestTime ) || Math . abs ( now - requestTime ) > toleranceSeconds ) {
366- return rejectAuth ( c , 'INVALID_TIMESTAMP' ) ;
367- }
368-
369- let payload : string ;
370- if ( c . req . method === 'GET' || c . req . method === 'DELETE' ) {
371- const rawUrl = c . req . url ;
372- const qMark = rawUrl . indexOf ( '?' ) ;
373- payload = qMark >= 0 ? rawUrl . substring ( qMark + 1 ) : '' ;
374- } else {
375- payload = await c . req . text ( ) ;
376- }
377-
378- const authHeader = c . req . header ( 'authorization' ) ;
379- const authorizationToken = authHeader && authHeader . startsWith ( 'Bearer ' ) ? authHeader . substring ( 7 ) : undefined ;
380-
381- const message = authorizationToken ? `${ payload } ${ timestamp } ${ authorizationToken } ` : `${ payload } ${ timestamp } ` ;
382-
383- try {
384- const isValid = verify ( null , Buffer . from ( message , 'utf8' ) , publicKey , Buffer . from ( sig , 'base64url' ) ) ;
385- if ( ! isValid ) {
386- warnInvalidSignature ( ) ;
387- return rejectAuth ( c , 'INVALID_SIGNATURE_FORMAT' ) ;
388- }
389- } catch {
390- warnInvalidSignature ( ) ;
391- return rejectAuth ( c , 'INVALID_SIGNATURE_FORMAT' ) ;
392- }
393-
394- return next ( ) ;
395- } ;
396- }
397-
398- function resolveClient (
399- client : ClientContract < SchemaDef > ,
400- authDb : ClientContract < SchemaDef > ,
401- c : Context ,
402- isAuthKeyEnabled : boolean ,
403- ) : ClientContract < SchemaDef > {
404- const authHeader = c . req . header ( 'authorization' ) ;
405-
406- if ( ! isAuthKeyEnabled && ! authHeader ) {
407- return client ;
408- }
409-
410- if ( ! authHeader ?. startsWith ( 'Bearer ' ) ) {
411- return authDb ;
412- }
413-
414- const token = authHeader . substring ( 7 ) ;
415- let claim : UserClaim ;
416- try {
417- claim = UserClaimSchema . parse ( JSON . parse ( Buffer . from ( token , 'base64' ) . toString ( 'utf8' ) ) ) ;
418- } catch ( err ) {
419- console . error (
420- colors . red ( `Failed to parse user claim from token: ${ err instanceof Error ? err . message : String ( err ) } ` ) ,
421- ) ;
422- return authDb ;
423- }
424-
425- if ( claim . type === 'superUser' ) {
426- return client ;
427- } else {
428- return authDb . $setAuth ( claim . data as any ) as ClientContract < SchemaDef > ;
429- }
430- }
431245
432246function startServer (
433247 client : ClientContract < SchemaDef > ,
0 commit comments