@rebasepro/server-core 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/graphql/graphql-schema-generator.d.ts +9 -0
- package/dist/api/rest/api-generator.d.ts +2 -14
- package/dist/api/rest/query-parser.d.ts +2 -5
- package/dist/api/types.d.ts +8 -2
- package/dist/auth/adapter-middleware.d.ts +7 -1
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-types.d.ts +8 -0
- package/dist/auth/auth-hooks.d.ts +17 -0
- package/dist/auth/builtin-auth-adapter.d.ts +3 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/interfaces.d.ts +32 -2
- package/dist/auth/magic-link-routes.d.ts +30 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +2 -1
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/routes.d.ts +3 -1
- package/dist/auth/session-routes.d.ts +2 -0
- package/dist/backend-CIxN4FVm.js.map +1 -1
- package/dist/base64-js-C_frYBkI.js +1687 -0
- package/dist/base64-js-C_frYBkI.js.map +1 -0
- package/dist/{dist-CZKP-Xz4.js → dist-DMO-zF6D.js} +2 -2
- package/dist/{dist-CZKP-Xz4.js.map → dist-DMO-zF6D.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/templates.d.ts +8 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/functions/define-function.d.ts +50 -0
- package/dist/functions/index.d.ts +2 -0
- package/dist/index.d.ts +16 -18
- package/dist/index.es.js +1947 -2356
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +40615 -10203
- package/dist/index.umd.js.map +1 -1
- package/dist/init.d.ts +68 -12
- package/dist/jws-BqRRaK11.js +616 -0
- package/dist/jws-BqRRaK11.js.map +1 -0
- package/dist/{jwt-DHcQRGC3.js → jwt-BETC8a1J.js} +3 -611
- package/dist/jwt-BETC8a1J.js.map +1 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/{src-COaj0G3P.js → src-CB2PIpBe.js} +2 -2
- package/dist/{src-COaj0G3P.js.map → src-CB2PIpBe.js.map} +1 -1
- package/dist/src-DjzOT1kG.js +29746 -0
- package/dist/src-DjzOT1kG.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/index.d.ts +5 -2
- package/dist/storage/routes.d.ts +33 -1
- package/dist/storage/tus-handler.d.ts +3 -1
- package/dist/storage/types.d.ts +28 -3
- package/dist/utils/dev-port.d.ts +2 -0
- package/package.json +13 -5
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +50 -15
- package/src/api/openapi-generator.ts +2 -2
- package/src/api/rest/api-generator.ts +44 -123
- package/src/api/rest/query-parser.ts +6 -23
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +8 -2
- package/src/auth/adapter-middleware.ts +10 -2
- package/src/auth/admin-roles-route.ts +36 -0
- package/src/auth/admin-users-route.ts +302 -0
- package/src/auth/api-keys/api-key-middleware.ts +4 -3
- package/src/auth/api-keys/api-key-routes.ts +12 -2
- package/src/auth/api-keys/api-key-store.ts +83 -66
- package/src/auth/api-keys/api-key-types.ts +8 -0
- package/src/auth/apple-oauth.ts +2 -1
- package/src/auth/auth-hooks.ts +21 -0
- package/src/auth/bitbucket-oauth.ts +2 -1
- package/src/auth/builtin-auth-adapter.ts +29 -6
- package/src/auth/custom-auth-adapter.ts +2 -0
- package/src/auth/discord-oauth.ts +2 -1
- package/src/auth/facebook-oauth.ts +2 -1
- package/src/auth/github-oauth.ts +2 -1
- package/src/auth/gitlab-oauth.ts +2 -1
- package/src/auth/google-oauth.ts +8 -4
- package/src/auth/index.ts +2 -0
- package/src/auth/interfaces.ts +38 -2
- package/src/auth/linkedin-oauth.ts +2 -1
- package/src/auth/magic-link-routes.ts +167 -0
- package/src/auth/mfa-crypto.ts +91 -0
- package/src/auth/mfa-routes.ts +34 -10
- package/src/auth/microsoft-oauth.ts +2 -1
- package/src/auth/middleware.ts +14 -5
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +78 -10
- package/src/auth/session-routes.ts +15 -3
- package/src/auth/slack-oauth.ts +2 -1
- package/src/auth/spotify-oauth.ts +2 -1
- package/src/auth/twitter-oauth.ts +8 -1
- package/src/cron/cron-store.ts +25 -23
- package/src/email/index.ts +3 -2
- package/src/email/templates.ts +82 -0
- package/src/email/types.ts +16 -0
- package/src/functions/define-function.ts +59 -0
- package/src/functions/function-loader.ts +16 -0
- package/src/functions/index.ts +2 -0
- package/src/index.ts +70 -37
- package/src/init.ts +214 -25
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/storage/GCSStorageController.ts +334 -0
- package/src/storage/index.ts +9 -3
- package/src/storage/routes.ts +191 -23
- package/src/storage/tus-handler.ts +16 -4
- package/src/storage/types.ts +25 -3
- package/src/utils/dev-port.ts +13 -7
- package/test/api-generator.test.ts +1 -1
- package/test/auth-config-types.test.ts +40 -0
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- package/test/define-function.test.ts +45 -0
- package/test/env.test.ts +9 -19
- package/test/multi-datasource-routing.test.ts +113 -0
- package/test/routed-realtime-service.test.ts +86 -0
- package/test/storage-routes.test.ts +160 -0
- package/test/transform-auth-response.test.ts +305 -0
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/test/backend-hooks-data.test.ts +0 -477
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Cloud Storage controller (works with GCS and Firebase Storage)
|
|
3
|
+
*
|
|
4
|
+
* The `@google-cloud/storage` package is loaded lazily so it remains an
|
|
5
|
+
* optional peer dependency — users who don't need GCS never pay for the
|
|
6
|
+
* import cost or need it installed.
|
|
7
|
+
*/
|
|
8
|
+
import { StorageController, GCSStorageConfig } from "./types";
|
|
9
|
+
import { UploadFileProps, UploadFileResult, DownloadConfig, StorageListResult } from "@rebasepro/types";
|
|
10
|
+
/**
|
|
11
|
+
* Google Cloud Storage implementation of `StorageController`.
|
|
12
|
+
*
|
|
13
|
+
* Works with standard GCS buckets **and** Firebase Storage buckets
|
|
14
|
+
* (e.g. `"my-project.appspot.com"`).
|
|
15
|
+
*/
|
|
16
|
+
export declare class GCSStorageController implements StorageController {
|
|
17
|
+
private config;
|
|
18
|
+
private client;
|
|
19
|
+
constructor(config: GCSStorageConfig);
|
|
20
|
+
private getClient;
|
|
21
|
+
getType(): "gcs";
|
|
22
|
+
putObject({ file, key, metadata, bucket }: UploadFileProps): Promise<UploadFileResult>;
|
|
23
|
+
getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;
|
|
24
|
+
getObject(key: string, bucket?: string): Promise<File | null>;
|
|
25
|
+
deleteObject(key: string, bucket?: string): Promise<void>;
|
|
26
|
+
listObjects(prefix: string, options?: {
|
|
27
|
+
bucket?: string;
|
|
28
|
+
maxResults?: number;
|
|
29
|
+
pageToken?: string;
|
|
30
|
+
}): Promise<StorageListResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Validate file before upload
|
|
33
|
+
*/
|
|
34
|
+
private validateFile;
|
|
35
|
+
/**
|
|
36
|
+
* Get the bucket name — either from parameter or config.
|
|
37
|
+
*/
|
|
38
|
+
private getBucket;
|
|
39
|
+
/**
|
|
40
|
+
* Flatten nested metadata to string values (GCS custom metadata requirement)
|
|
41
|
+
*/
|
|
42
|
+
private flattenMetadata;
|
|
43
|
+
}
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Storage module for Rebase backend
|
|
3
3
|
*
|
|
4
|
-
* Provides pluggable file storage with
|
|
4
|
+
* Provides pluggable file storage with three built-in providers:
|
|
5
5
|
* - **Local filesystem** — zero config, great for dev and single-server deployments.
|
|
6
6
|
* - **S3-compatible** — works with AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
|
|
7
7
|
* Backblaze B2, DigitalOcean Spaces, and GCS (via S3 interop).
|
|
8
|
+
* - **Google Cloud Storage / Firebase Storage** — native GCS support via `@google-cloud/storage`
|
|
9
|
+
* (optional peer dependency, lazily loaded).
|
|
8
10
|
*
|
|
9
|
-
* For other providers (
|
|
11
|
+
* For other providers (Azure Blob, etc.), implement the
|
|
10
12
|
* `StorageController` interface and pass the instance directly to the `storage` config.
|
|
11
13
|
*/
|
|
12
14
|
export * from "./types";
|
|
13
15
|
export { LocalStorageController } from "./LocalStorageController";
|
|
14
16
|
export { S3StorageController } from "./S3StorageController";
|
|
17
|
+
export { GCSStorageController } from "./GCSStorageController";
|
|
15
18
|
export { createStorageRoutes } from "./routes";
|
|
16
19
|
export type { StorageRoutesConfig } from "./routes";
|
|
17
20
|
export * from "./storage-registry";
|
package/dist/storage/routes.d.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Storage REST API routes using Hono
|
|
3
|
+
*
|
|
4
|
+
* Supports multi-backend routing via `StorageRegistry`. Each endpoint
|
|
5
|
+
* accepts an optional `storageId` parameter (query string or form field)
|
|
6
|
+
* to target a named storage backend. When omitted, the default backend
|
|
7
|
+
* is used.
|
|
3
8
|
*/
|
|
4
9
|
import { Hono } from "hono";
|
|
5
10
|
import { StorageController } from "./types";
|
|
11
|
+
import type { StorageRegistry } from "./storage-registry";
|
|
12
|
+
import { type StorageSourceDefinition, type AuthAdapter } from "@rebasepro/types";
|
|
6
13
|
import { HonoEnv } from "../api/types";
|
|
7
14
|
export interface StorageRoutesConfig {
|
|
8
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Single storage controller (backward-compatible).
|
|
17
|
+
* Used as fallback when no `registry` is provided.
|
|
18
|
+
*/
|
|
19
|
+
controller?: StorageController;
|
|
20
|
+
/**
|
|
21
|
+
* Full storage registry for multi-backend routing.
|
|
22
|
+
* When provided, endpoints resolve the controller from `storageId`
|
|
23
|
+
* parameter. Takes precedence over `controller`.
|
|
24
|
+
*/
|
|
25
|
+
registry?: StorageRegistry;
|
|
26
|
+
/**
|
|
27
|
+
* Declared storage sources, surfaced by `GET /sources` so the client can
|
|
28
|
+
* bootstrap its registry. Carries the frontend `transport` (server vs
|
|
29
|
+
* direct) and human-readable labels. Server-transport sources are also
|
|
30
|
+
* derived from the registry; `direct` sources (e.g. Firebase Storage) only
|
|
31
|
+
* exist here since the backend does not proxy them.
|
|
32
|
+
*/
|
|
33
|
+
sources?: StorageSourceDefinition[];
|
|
9
34
|
/** Base path for storage routes (default: '/api/storage') */
|
|
10
35
|
basePath?: string;
|
|
11
36
|
/** Require authentication for write operations (default: true) */
|
|
@@ -13,6 +38,13 @@ export interface StorageRoutesConfig {
|
|
|
13
38
|
/** Allow unauthenticated read access to stored files (default: false).
|
|
14
39
|
* When false and requireAuth is true, reads also require authentication. */
|
|
15
40
|
publicRead?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When provided, storage routes delegate auth to this adapter instead
|
|
43
|
+
* of the built-in JWT module. This mirrors how data routes use
|
|
44
|
+
* `createAdapterAuthMiddleware()` and avoids the "JWT secret not
|
|
45
|
+
* configured" crash when `configureJwt()` was never called.
|
|
46
|
+
*/
|
|
47
|
+
authAdapter?: AuthAdapter;
|
|
16
48
|
}
|
|
17
49
|
/**
|
|
18
50
|
* Extract the wildcard portion of a route path from the full request path.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Context } from "hono";
|
|
11
11
|
import type { StorageController } from "./types";
|
|
12
|
+
import type { StorageRegistry } from "./storage-registry";
|
|
12
13
|
/**
|
|
13
14
|
* TUS resumable upload handler.
|
|
14
15
|
*
|
|
@@ -18,10 +19,11 @@ import type { StorageController } from "./types";
|
|
|
18
19
|
*/
|
|
19
20
|
export declare class TusHandler {
|
|
20
21
|
private storageController?;
|
|
22
|
+
private storageRegistry?;
|
|
21
23
|
private uploads;
|
|
22
24
|
private tusDir;
|
|
23
25
|
private cleanupTimer?;
|
|
24
|
-
constructor(storageBaseDir: string, storageController?: StorageController | undefined);
|
|
26
|
+
constructor(storageBaseDir: string, storageController?: StorageController | undefined, storageRegistry?: StorageRegistry | undefined);
|
|
25
27
|
/** Ensure the temp directory exists. */
|
|
26
28
|
private ensureDir;
|
|
27
29
|
/** Start periodic cleanup of stale uploads. */
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -41,18 +41,43 @@ export interface S3StorageConfig {
|
|
|
41
41
|
signedUrlExpiration?: number;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* Storage configuration
|
|
44
|
+
* Google Cloud Storage configuration (works with GCS and Firebase Storage)
|
|
45
|
+
*/
|
|
46
|
+
export interface GCSStorageConfig {
|
|
47
|
+
type: "gcs";
|
|
48
|
+
/** GCS bucket name (e.g. "my-project.appspot.com" for Firebase Storage) */
|
|
49
|
+
bucket: string;
|
|
50
|
+
/** GCP project ID (optional, auto-detected from credentials) */
|
|
51
|
+
projectId?: string;
|
|
52
|
+
/** Path to service account JSON key file */
|
|
53
|
+
keyFilename?: string;
|
|
54
|
+
/** Service account credentials object (alternative to keyFilename) */
|
|
55
|
+
credentials?: {
|
|
56
|
+
client_email: string;
|
|
57
|
+
private_key: string;
|
|
58
|
+
project_id?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Maximum file size in bytes (default: 50MB) */
|
|
61
|
+
maxFileSize?: number;
|
|
62
|
+
/** Allowed MIME types */
|
|
63
|
+
allowedMimeTypes?: string[];
|
|
64
|
+
/** Signed URL expiration in seconds (default: 3600) */
|
|
65
|
+
signedUrlExpiration?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.
|
|
45
69
|
*
|
|
46
70
|
* **Built-in providers:**
|
|
47
71
|
* - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).
|
|
48
72
|
* - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,
|
|
49
73
|
* Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).
|
|
74
|
+
* - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.
|
|
50
75
|
*
|
|
51
76
|
* **Custom providers:**
|
|
52
|
-
* For cloud
|
|
77
|
+
* For other cloud storage (Azure Blob, etc.), implement the `StorageController`
|
|
53
78
|
* interface and pass the instance directly to the `storage` config.
|
|
54
79
|
*/
|
|
55
|
-
export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig;
|
|
80
|
+
export type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;
|
|
56
81
|
/**
|
|
57
82
|
* Storage controller interface for backend implementations
|
|
58
83
|
*/
|
package/dist/utils/dev-port.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export declare function listenWithPortRetry(server: Server, startPort: number, o
|
|
|
28
28
|
maxAttempts?: number;
|
|
29
29
|
/** Absolute path to write the resolved port file into. Defaults to `process.cwd()`. */
|
|
30
30
|
portFileDir?: string;
|
|
31
|
+
/** Service key to include in the state file for MCP server auto-discovery. */
|
|
32
|
+
serviceKey?: string;
|
|
31
33
|
}): Promise<number>;
|
|
32
34
|
/**
|
|
33
35
|
* Clean up the dev port file and state file (call on graceful shutdown).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Database-Agnostic Backend Core for Rebase",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"ts-morph": "28.0.0",
|
|
55
55
|
"ws": "^8.21.0",
|
|
56
56
|
"zod": "^4.4.3",
|
|
57
|
-
"@rebasepro/client": "0.
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
57
|
+
"@rebasepro/client": "0.8.0",
|
|
58
|
+
"@rebasepro/common": "0.8.0",
|
|
59
|
+
"@rebasepro/types": "0.8.0",
|
|
60
|
+
"@rebasepro/utils": "0.8.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/jest": "^30.0.0",
|
|
@@ -74,6 +74,14 @@
|
|
|
74
74
|
"typescript": "^6.0.3",
|
|
75
75
|
"vite": "^8.0.16"
|
|
76
76
|
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@google-cloud/storage": "^7.0.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"@google-cloud/storage": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
84
|
+
},
|
|
77
85
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
78
86
|
"publishConfig": {
|
|
79
87
|
"access": "public"
|
package/src/api/errors.ts
CHANGED
|
@@ -145,12 +145,26 @@ export const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {
|
|
|
145
145
|
|
|
146
146
|
// Handle DB connection and specific system errors for better logging
|
|
147
147
|
let logMessage = error.message;
|
|
148
|
+
|
|
149
|
+
// Resolve the actual cause — Node's net module wraps dual-stack failures
|
|
150
|
+
// in an AggregateError whose inner errors carry the real address/port.
|
|
151
|
+
let resolvedCause: PgLikeError | undefined;
|
|
148
152
|
if (error.cause && typeof error.cause === "object" && error.cause !== null && "code" in error.cause) {
|
|
149
|
-
const cause = error.cause as PgLikeError;
|
|
153
|
+
const cause = error.cause as PgLikeError & { errors?: PgLikeError[] };
|
|
154
|
+
if (cause.code === "ECONNREFUSED" && !cause.address && Array.isArray(cause.errors)) {
|
|
155
|
+
// AggregateError — pick the first inner error that has address info
|
|
156
|
+
resolvedCause = cause.errors.find(e => e.address) || cause;
|
|
157
|
+
} else {
|
|
158
|
+
resolvedCause = cause;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (resolvedCause) {
|
|
163
|
+
const cause = resolvedCause;
|
|
150
164
|
if (cause.code === "ENETUNREACH") {
|
|
151
165
|
logMessage = `Network unreachable. Cannot connect to database at ${cause.address}:${cause.port}.`;
|
|
152
166
|
} else if (cause.code === "ECONNREFUSED") {
|
|
153
|
-
logMessage = `Connection refused to database at ${cause.address}:${cause.port}
|
|
167
|
+
logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;
|
|
154
168
|
} else if (cause.code === "42703" || cause.code === "42P01") {
|
|
155
169
|
code = "SCHEMA_DRIFT";
|
|
156
170
|
const issue = cause.code === "42703" ? "column" : "table";
|
|
@@ -11,7 +11,15 @@ import {
|
|
|
11
11
|
GraphQLInputObjectType,
|
|
12
12
|
GraphQLInputFieldConfig
|
|
13
13
|
} from "graphql";
|
|
14
|
-
import { DataDriver, EntityCollection, FetchCollectionProps, Property, Entity } from "@rebasepro/types";
|
|
14
|
+
import { DataDriver, EntityCollection, FetchCollectionProps, Property, Entity, getCollectionDataPath } from "@rebasepro/types";
|
|
15
|
+
import { isOperationAllowed, type ApiKeyOperation } from "../../auth/api-keys/api-key-permission-guard";
|
|
16
|
+
import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
|
|
17
|
+
|
|
18
|
+
/** Context shape provided by @hono/graphql-server (Hono's Context object). */
|
|
19
|
+
type GraphQLResolverContext = {
|
|
20
|
+
get?: (key: string) => unknown;
|
|
21
|
+
driver?: DataDriver;
|
|
22
|
+
};
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Lightweight GraphQL schema generator that leverages existing DataDriver
|
|
@@ -28,6 +36,28 @@ export class GraphQLSchemaGenerator {
|
|
|
28
36
|
this.driver = driver;
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Enforce API key permission scoping for a GraphQL resolver.
|
|
41
|
+
*
|
|
42
|
+
* Extracts the `apiKey` from the Hono context passed by `@hono/graphql-server`.
|
|
43
|
+
* If the request was made with an API key and the key does not have the
|
|
44
|
+
* required permission for the target collection/operation, an error is thrown.
|
|
45
|
+
* Non-API-key requests (e.g. session auth) are allowed through.
|
|
46
|
+
*/
|
|
47
|
+
private enforceApiKeyPermission(
|
|
48
|
+
context: GraphQLResolverContext | undefined,
|
|
49
|
+
collectionSlug: string,
|
|
50
|
+
operation: ApiKeyOperation
|
|
51
|
+
): void {
|
|
52
|
+
const apiKey = context?.get?.("apiKey") as ApiKeyMasked | undefined;
|
|
53
|
+
if (!apiKey) return;
|
|
54
|
+
if (!isOperationAllowed(apiKey.permissions, collectionSlug, operation)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`API key does not have "${operation}" permission for collection "${collectionSlug}"`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
/**
|
|
32
62
|
* Generate complete GraphQL schema using existing DataDriver
|
|
33
63
|
*/
|
|
@@ -184,11 +214,12 @@ export class GraphQLSchemaGenerator {
|
|
|
184
214
|
id: { type: new GraphQLNonNull(GraphQLString) }
|
|
185
215
|
},
|
|
186
216
|
resolve: async (_, args, context: unknown) => {
|
|
187
|
-
const ctx = context as
|
|
188
|
-
|
|
217
|
+
const ctx = context as GraphQLResolverContext | undefined;
|
|
218
|
+
this.enforceApiKeyPermission(ctx, collection.slug, "read");
|
|
219
|
+
const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
|
|
189
220
|
if (!ds) throw new Error("Scoped driver not available");
|
|
190
221
|
const entity = await ds.fetchEntity({
|
|
191
|
-
path: collection
|
|
222
|
+
path: getCollectionDataPath(collection),
|
|
192
223
|
entityId: args.id,
|
|
193
224
|
collection
|
|
194
225
|
});
|
|
@@ -208,8 +239,9 @@ defaultValue: 0 },
|
|
|
208
239
|
orderBy: { type: GraphQLString }
|
|
209
240
|
},
|
|
210
241
|
resolve: async (_, args, context: unknown) => {
|
|
211
|
-
const ctx = context as
|
|
212
|
-
|
|
242
|
+
const ctx = context as GraphQLResolverContext | undefined;
|
|
243
|
+
this.enforceApiKeyPermission(ctx, collection.slug, "read");
|
|
244
|
+
const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
|
|
213
245
|
if (!ds) throw new Error("Scoped driver not available");
|
|
214
246
|
let filter: FetchCollectionProps["filter"] | undefined;
|
|
215
247
|
if (args.where) {
|
|
@@ -224,7 +256,7 @@ defaultValue: 0 },
|
|
|
224
256
|
}
|
|
225
257
|
}
|
|
226
258
|
const entities = await ds.fetchCollection({
|
|
227
|
-
path: collection
|
|
259
|
+
path: getCollectionDataPath(collection),
|
|
228
260
|
collection,
|
|
229
261
|
filter,
|
|
230
262
|
limit: args.limit,
|
|
@@ -261,8 +293,9 @@ defaultValue: 0 },
|
|
|
261
293
|
input: { type: new GraphQLNonNull(inputType) }
|
|
262
294
|
},
|
|
263
295
|
resolve: async (_, args, context: unknown) => {
|
|
264
|
-
const ctx = context as
|
|
265
|
-
|
|
296
|
+
const ctx = context as GraphQLResolverContext | undefined;
|
|
297
|
+
this.enforceApiKeyPermission(ctx, collection.slug, "write");
|
|
298
|
+
const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
|
|
266
299
|
if (!ds) throw new Error("Scoped driver not available");
|
|
267
300
|
const path = collection.slug;
|
|
268
301
|
const entity = await ds.saveEntity({
|
|
@@ -283,11 +316,12 @@ defaultValue: 0 },
|
|
|
283
316
|
input: { type: new GraphQLNonNull(inputType) }
|
|
284
317
|
},
|
|
285
318
|
resolve: async (_, args, context: unknown) => {
|
|
286
|
-
const ctx = context as
|
|
287
|
-
|
|
319
|
+
const ctx = context as GraphQLResolverContext | undefined;
|
|
320
|
+
this.enforceApiKeyPermission(ctx, collection.slug, "write");
|
|
321
|
+
const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
|
|
288
322
|
if (!ds) throw new Error("Scoped driver not available");
|
|
289
323
|
const entity = await ds.saveEntity({
|
|
290
|
-
path: collection
|
|
324
|
+
path: getCollectionDataPath(collection),
|
|
291
325
|
entityId: args.id,
|
|
292
326
|
values: args.input,
|
|
293
327
|
collection,
|
|
@@ -304,12 +338,13 @@ defaultValue: 0 },
|
|
|
304
338
|
id: { type: new GraphQLNonNull(GraphQLString) }
|
|
305
339
|
},
|
|
306
340
|
resolve: async (_, args, context: unknown) => {
|
|
341
|
+
const ctx = context as GraphQLResolverContext | undefined;
|
|
342
|
+
this.enforceApiKeyPermission(ctx, collection.slug, "delete");
|
|
307
343
|
try {
|
|
308
|
-
const
|
|
309
|
-
const ds = ctx?.driver;
|
|
344
|
+
const ds = ctx?.get?.("driver") as DataDriver | undefined ?? (ctx as Record<string, unknown> | undefined)?.driver as DataDriver | undefined;
|
|
310
345
|
if (!ds) throw new Error("Scoped driver not available");
|
|
311
346
|
const existingEntity = await ds.fetchEntity({
|
|
312
|
-
path: collection
|
|
347
|
+
path: getCollectionDataPath(collection),
|
|
313
348
|
entityId: args.id,
|
|
314
349
|
collection
|
|
315
350
|
});
|
|
@@ -491,9 +491,9 @@ function convertPropertyToSchema(property: Property): Record<string, unknown> {
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
if (sp.email) base.format = "email";
|
|
494
|
-
if (sp.url) base.format = "uri";
|
|
494
|
+
if (sp.ui?.url) base.format = "uri";
|
|
495
495
|
if (sp.storage) base.format = "uri";
|
|
496
|
-
if (sp.markdown) base.description = (base.description || "") + " (Markdown)";
|
|
496
|
+
if (sp.ui?.markdown) base.description = (base.description || "") + " (Markdown)";
|
|
497
497
|
|
|
498
498
|
return base;
|
|
499
499
|
}
|