Skip to content

Commit 5bf8b66

Browse files
authored
merge dev to main (v3.9.2) (#2811)
2 parents a658e39 + ca354fe commit 5bf8b66

58 files changed

Lines changed: 2453 additions & 1561 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "zenstack-v3",
33
"displayName": "ZenStack",
44
"description": "ZenStack",
5-
"version": "3.9.1",
5+
"version": "3.9.2",
66
"type": "module",
77
"author": {
88
"name": "ZenStack Team",

packages/auth-adapters/better-auth/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@zenstackhq/better-auth",
33
"displayName": "ZenStack Better Auth Adapter",
44
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
5-
"version": "3.9.1",
5+
"version": "3.9.2",
66
"type": "module",
77
"author": {
88
"name": "ZenStack Team",

packages/cli/package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@zenstackhq/cli",
33
"displayName": "ZenStack CLI",
44
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
5-
"version": "3.9.1",
5+
"version": "3.9.2",
66
"type": "module",
77
"author": {
88
"name": "ZenStack Team",
@@ -37,7 +37,20 @@
3737
"pack": "pnpm pack"
3838
},
3939
"exports": {
40-
"./package.json": "./package.json"
40+
"./package.json": {
41+
"import": "./package.json",
42+
"require": "./package.json"
43+
},
44+
"./proxy": {
45+
"import": {
46+
"types": "./dist/proxy.d.mts",
47+
"default": "./dist/proxy.mjs"
48+
},
49+
"require": {
50+
"types": "./dist/proxy.d.cts",
51+
"default": "./dist/proxy.cjs"
52+
}
53+
}
4154
},
4255
"dependencies": {
4356
"@zenstackhq/common-helpers": "workspace:*",

packages/cli/src/actions/proxy.ts

Lines changed: 21 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { serve } from '@hono/node-server';
12
import {
23
ConfigExpr,
34
InvocationExpr,
@@ -13,30 +14,41 @@ import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
1314
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
1415
import type { SchemaDef } from '@zenstackhq/orm/schema';
1516
import { 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';
2118
import type BetterSqlite3 from 'better-sqlite3';
2219
import colors from 'colors';
2320
import { createJiti } from 'jiti';
2421
import type { createPool as MysqlCreatePool } from 'mysql2';
25-
import { verify } from 'node:crypto';
2622
import fs from 'node:fs';
2723
import path from 'node:path';
2824
import ora from 'ora';
2925
import { detect, resolveCommand } from 'package-manager-detector';
3026
import type { Pool as PgPoolType } from 'pg';
3127
import { 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';
3237
import { execSync } from '../utils/exec-utils';
33-
import { getVersion } from '../utils/version-utils';
3438
import { getOutputPath, getSchemaFile, isPackageInstalled, loadPackage, loadSchemaDocument } from './action-utils';
35-
import type { DataSourceProviderType } from '@zenstackhq/schema';
3639
import { runPull } from './db';
37-
import { z } from 'zod';
3840
import { 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+
4052
type 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-
7963
export 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

432246
function startServer(
433247
client: ClientContract<SchemaDef>,

0 commit comments

Comments
 (0)