@stacksolo/plugin-gcp-kernel 0.1.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/LICENSE +21 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/service/Dockerfile +23 -0
- package/service/package-lock.json +3231 -0
- package/service/package.json +26 -0
- package/service/src/index.ts +77 -0
- package/service/src/routes/auth.ts +51 -0
- package/service/src/routes/events.ts +148 -0
- package/service/src/routes/files.ts +230 -0
- package/service/src/routes/health.ts +22 -0
- package/service/src/services/firebase.ts +67 -0
- package/service/src/services/pubsub.ts +373 -0
- package/service/src/services/storage.ts +204 -0
- package/service/tsconfig.json +16 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stacksolo-gcp-kernel-service",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "StackSolo GCP kernel service - HTTP + Pub/Sub (serverless alternative to NATS kernel)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsx watch src/index.ts",
|
|
10
|
+
"start": "node dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@google-cloud/pubsub": "^4.0.0",
|
|
14
|
+
"@google-cloud/storage": "^7.0.0",
|
|
15
|
+
"firebase-admin": "^12.0.0",
|
|
16
|
+
"express": "^4.18.0",
|
|
17
|
+
"cors": "^2.8.5"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/express": "^4.17.21",
|
|
21
|
+
"@types/cors": "^2.8.17",
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"tsx": "^4.0.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCP Kernel Service
|
|
3
|
+
*
|
|
4
|
+
* A fully GCP-native kernel implementation:
|
|
5
|
+
* - Express HTTP server for all endpoints
|
|
6
|
+
* - Cloud Pub/Sub for event messaging (replaces NATS/JetStream)
|
|
7
|
+
* - Cloud Storage for file operations
|
|
8
|
+
* - Firebase Admin SDK for token validation
|
|
9
|
+
*
|
|
10
|
+
* Endpoints:
|
|
11
|
+
* - GET /health - Health check
|
|
12
|
+
* - POST /auth/validate - Validate Firebase token
|
|
13
|
+
* - POST /files/upload-url - Get signed upload URL
|
|
14
|
+
* - POST /files/download-url - Get signed download URL
|
|
15
|
+
* - POST /files/list - List files with prefix
|
|
16
|
+
* - POST /files/delete - Delete file
|
|
17
|
+
* - POST /files/move - Move/rename file
|
|
18
|
+
* - POST /files/metadata - Get file metadata
|
|
19
|
+
* - POST /events/publish - Publish event to Pub/Sub
|
|
20
|
+
* - POST /events/subscribe - Register HTTP push subscription
|
|
21
|
+
* - POST /events/unsubscribe - Remove subscription
|
|
22
|
+
* - GET /events/subscriptions - List subscriptions
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import express from 'express';
|
|
26
|
+
import cors from 'cors';
|
|
27
|
+
import { initializeFirebase } from './services/firebase.js';
|
|
28
|
+
import { startEventConsumer } from './services/pubsub.js';
|
|
29
|
+
import { healthRouter } from './routes/health.js';
|
|
30
|
+
import { authRouter } from './routes/auth.js';
|
|
31
|
+
import { filesRouter } from './routes/files.js';
|
|
32
|
+
import { eventsRouter } from './routes/events.js';
|
|
33
|
+
|
|
34
|
+
const app = express();
|
|
35
|
+
const PORT = parseInt(process.env.PORT || '8080', 10);
|
|
36
|
+
|
|
37
|
+
// Middleware
|
|
38
|
+
app.use(cors());
|
|
39
|
+
app.use(express.json());
|
|
40
|
+
|
|
41
|
+
// Initialize Firebase
|
|
42
|
+
initializeFirebase();
|
|
43
|
+
|
|
44
|
+
// Start event consumer for HTTP push delivery
|
|
45
|
+
startEventConsumer().catch((error) => {
|
|
46
|
+
console.error('Failed to start event consumer:', error);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Routes
|
|
50
|
+
app.use('/health', healthRouter);
|
|
51
|
+
app.use('/auth', authRouter);
|
|
52
|
+
app.use('/files', filesRouter);
|
|
53
|
+
app.use('/events', eventsRouter);
|
|
54
|
+
|
|
55
|
+
// Root endpoint
|
|
56
|
+
app.get('/', (_req, res) => {
|
|
57
|
+
res.json({
|
|
58
|
+
service: 'stacksolo-gcp-kernel',
|
|
59
|
+
version: '0.1.0',
|
|
60
|
+
type: 'gcp',
|
|
61
|
+
endpoints: {
|
|
62
|
+
health: '/health',
|
|
63
|
+
auth: '/auth/validate',
|
|
64
|
+
files: '/files/*',
|
|
65
|
+
events: '/events/*',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Start server
|
|
71
|
+
app.listen(PORT, () => {
|
|
72
|
+
console.log(`GCP Kernel service running on port ${PORT}`);
|
|
73
|
+
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
74
|
+
console.log(`GCP Project: ${process.env.GCP_PROJECT_ID || 'not set'}`);
|
|
75
|
+
console.log(`Storage Bucket: ${process.env.GCS_BUCKET || 'not set'}`);
|
|
76
|
+
console.log(`Pub/Sub Topic: ${process.env.PUBSUB_EVENTS_TOPIC || 'not set'}`);
|
|
77
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Routes
|
|
3
|
+
*
|
|
4
|
+
* POST /auth/validate - Validate Firebase ID token
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Router, Request, Response } from 'express';
|
|
8
|
+
import { validateToken } from '../services/firebase.js';
|
|
9
|
+
|
|
10
|
+
export const authRouter = Router();
|
|
11
|
+
|
|
12
|
+
interface ValidateRequest {
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
authRouter.post('/validate', async (req: Request, res: Response) => {
|
|
17
|
+
try {
|
|
18
|
+
const { token } = req.body as ValidateRequest;
|
|
19
|
+
|
|
20
|
+
if (!token) {
|
|
21
|
+
res.status(400).json({
|
|
22
|
+
error: 'Token is required',
|
|
23
|
+
code: 'MISSING_TOKEN',
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await validateToken(token);
|
|
29
|
+
|
|
30
|
+
if (result.valid) {
|
|
31
|
+
res.json({
|
|
32
|
+
valid: true,
|
|
33
|
+
uid: result.uid,
|
|
34
|
+
email: result.email,
|
|
35
|
+
claims: result.claims,
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
res.status(401).json({
|
|
39
|
+
valid: false,
|
|
40
|
+
error: result.error,
|
|
41
|
+
code: 'INVALID_TOKEN',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Auth validation error:', error);
|
|
46
|
+
res.status(500).json({
|
|
47
|
+
error: 'Internal server error',
|
|
48
|
+
code: 'INTERNAL_ERROR',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events Routes
|
|
3
|
+
*
|
|
4
|
+
* POST /events/publish - Publish event to Pub/Sub
|
|
5
|
+
* POST /events/subscribe - Register HTTP push subscription
|
|
6
|
+
* POST /events/unsubscribe - Remove subscription
|
|
7
|
+
* GET /events/subscriptions - List subscriptions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Router, Request, Response } from 'express';
|
|
11
|
+
import {
|
|
12
|
+
publishEvent,
|
|
13
|
+
createSubscription,
|
|
14
|
+
deleteSubscription,
|
|
15
|
+
listSubscriptions,
|
|
16
|
+
} from '../services/pubsub.js';
|
|
17
|
+
|
|
18
|
+
export const eventsRouter = Router();
|
|
19
|
+
|
|
20
|
+
// POST /events/publish
|
|
21
|
+
eventsRouter.post('/publish', async (req: Request, res: Response) => {
|
|
22
|
+
try {
|
|
23
|
+
const { eventType, data, metadata } = req.body as {
|
|
24
|
+
eventType: string;
|
|
25
|
+
data: unknown;
|
|
26
|
+
metadata?: Record<string, string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!eventType) {
|
|
30
|
+
res.status(400).json({
|
|
31
|
+
error: 'eventType is required',
|
|
32
|
+
code: 'MISSING_EVENT_TYPE',
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await publishEvent(eventType, data, metadata);
|
|
38
|
+
res.json({
|
|
39
|
+
published: true,
|
|
40
|
+
...result,
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error publishing event:', error);
|
|
44
|
+
res.status(500).json({
|
|
45
|
+
error: 'Failed to publish event',
|
|
46
|
+
code: 'INTERNAL_ERROR',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// POST /events/subscribe
|
|
52
|
+
eventsRouter.post('/subscribe', async (req: Request, res: Response) => {
|
|
53
|
+
try {
|
|
54
|
+
const { pattern, endpoint, serviceName } = req.body as {
|
|
55
|
+
pattern: string;
|
|
56
|
+
endpoint: string;
|
|
57
|
+
serviceName?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!pattern) {
|
|
61
|
+
res.status(400).json({
|
|
62
|
+
error: 'pattern is required',
|
|
63
|
+
code: 'MISSING_PATTERN',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!endpoint) {
|
|
69
|
+
res.status(400).json({
|
|
70
|
+
error: 'endpoint is required',
|
|
71
|
+
code: 'MISSING_ENDPOINT',
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate endpoint is a valid URL
|
|
77
|
+
try {
|
|
78
|
+
new URL(endpoint);
|
|
79
|
+
} catch {
|
|
80
|
+
res.status(400).json({
|
|
81
|
+
error: 'endpoint must be a valid URL',
|
|
82
|
+
code: 'INVALID_ENDPOINT',
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await createSubscription(pattern, endpoint, serviceName);
|
|
88
|
+
res.json({
|
|
89
|
+
subscribed: true,
|
|
90
|
+
...result,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Error creating subscription:', error);
|
|
94
|
+
res.status(500).json({
|
|
95
|
+
error: 'Failed to create subscription',
|
|
96
|
+
code: 'INTERNAL_ERROR',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// POST /events/unsubscribe
|
|
102
|
+
eventsRouter.post('/unsubscribe', async (req: Request, res: Response) => {
|
|
103
|
+
try {
|
|
104
|
+
const { subscriptionId } = req.body as { subscriptionId: string };
|
|
105
|
+
|
|
106
|
+
if (!subscriptionId) {
|
|
107
|
+
res.status(400).json({
|
|
108
|
+
error: 'subscriptionId is required',
|
|
109
|
+
code: 'MISSING_SUBSCRIPTION_ID',
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await deleteSubscription(subscriptionId);
|
|
115
|
+
res.json({
|
|
116
|
+
unsubscribed: true,
|
|
117
|
+
subscriptionId,
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof Error && error.message === 'NOT_FOUND') {
|
|
121
|
+
res.status(404).json({
|
|
122
|
+
error: 'Subscription not found',
|
|
123
|
+
code: 'NOT_FOUND',
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
console.error('Error deleting subscription:', error);
|
|
128
|
+
res.status(500).json({
|
|
129
|
+
error: 'Failed to delete subscription',
|
|
130
|
+
code: 'INTERNAL_ERROR',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// GET /events/subscriptions
|
|
136
|
+
eventsRouter.get('/subscriptions', async (req: Request, res: Response) => {
|
|
137
|
+
try {
|
|
138
|
+
const pattern = req.query.pattern as string | undefined;
|
|
139
|
+
const subs = listSubscriptions(pattern);
|
|
140
|
+
res.json({ subscriptions: subs });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error listing subscriptions:', error);
|
|
143
|
+
res.status(500).json({
|
|
144
|
+
error: 'Failed to list subscriptions',
|
|
145
|
+
code: 'INTERNAL_ERROR',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files Routes
|
|
3
|
+
*
|
|
4
|
+
* POST /files/upload-url - Get signed upload URL
|
|
5
|
+
* POST /files/download-url - Get signed download URL
|
|
6
|
+
* POST /files/list - List files with prefix
|
|
7
|
+
* POST /files/delete - Delete file
|
|
8
|
+
* POST /files/move - Move/rename file
|
|
9
|
+
* POST /files/metadata - Get file metadata
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Router, Request, Response } from 'express';
|
|
13
|
+
import {
|
|
14
|
+
validatePath,
|
|
15
|
+
getUploadUrl,
|
|
16
|
+
getDownloadUrl,
|
|
17
|
+
listFiles,
|
|
18
|
+
deleteFile,
|
|
19
|
+
moveFile,
|
|
20
|
+
getFileMetadata,
|
|
21
|
+
} from '../services/storage.js';
|
|
22
|
+
|
|
23
|
+
export const filesRouter = Router();
|
|
24
|
+
|
|
25
|
+
// POST /files/upload-url
|
|
26
|
+
filesRouter.post('/upload-url', async (req: Request, res: Response) => {
|
|
27
|
+
try {
|
|
28
|
+
const { path, contentType } = req.body as { path: string; contentType: string };
|
|
29
|
+
|
|
30
|
+
// Validate path
|
|
31
|
+
const validation = validatePath(path);
|
|
32
|
+
if (!validation.valid) {
|
|
33
|
+
res.status(400).json({
|
|
34
|
+
error: validation.error,
|
|
35
|
+
code: 'INVALID_PATH',
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate content type
|
|
41
|
+
if (!contentType) {
|
|
42
|
+
res.status(400).json({
|
|
43
|
+
error: 'Content type is required',
|
|
44
|
+
code: 'MISSING_CONTENT_TYPE',
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await getUploadUrl(path, contentType);
|
|
50
|
+
res.json(result);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error generating upload URL:', error);
|
|
53
|
+
res.status(500).json({
|
|
54
|
+
error: 'Failed to generate upload URL',
|
|
55
|
+
code: 'INTERNAL_ERROR',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// POST /files/download-url
|
|
61
|
+
filesRouter.post('/download-url', async (req: Request, res: Response) => {
|
|
62
|
+
try {
|
|
63
|
+
const { path } = req.body as { path: string };
|
|
64
|
+
|
|
65
|
+
// Validate path
|
|
66
|
+
const validation = validatePath(path);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
res.status(400).json({
|
|
69
|
+
error: validation.error,
|
|
70
|
+
code: 'INVALID_PATH',
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await getDownloadUrl(path);
|
|
76
|
+
res.json(result);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof Error && error.message === 'NOT_FOUND') {
|
|
79
|
+
res.status(404).json({
|
|
80
|
+
error: 'File not found',
|
|
81
|
+
code: 'NOT_FOUND',
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.error('Error generating download URL:', error);
|
|
86
|
+
res.status(500).json({
|
|
87
|
+
error: 'Failed to generate download URL',
|
|
88
|
+
code: 'INTERNAL_ERROR',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// POST /files/list
|
|
94
|
+
filesRouter.post('/list', async (req: Request, res: Response) => {
|
|
95
|
+
try {
|
|
96
|
+
const { prefix, maxResults, pageToken } = req.body as {
|
|
97
|
+
prefix?: string;
|
|
98
|
+
maxResults?: number;
|
|
99
|
+
pageToken?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await listFiles(prefix, maxResults, pageToken);
|
|
103
|
+
res.json(result);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Error listing files:', error);
|
|
106
|
+
res.status(500).json({
|
|
107
|
+
error: 'Failed to list files',
|
|
108
|
+
code: 'INTERNAL_ERROR',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// POST /files/delete
|
|
114
|
+
filesRouter.post('/delete', async (req: Request, res: Response) => {
|
|
115
|
+
try {
|
|
116
|
+
const { path } = req.body as { path: string };
|
|
117
|
+
|
|
118
|
+
// Validate path
|
|
119
|
+
const validation = validatePath(path);
|
|
120
|
+
if (!validation.valid) {
|
|
121
|
+
res.status(400).json({
|
|
122
|
+
error: validation.error,
|
|
123
|
+
code: 'INVALID_PATH',
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await deleteFile(path);
|
|
129
|
+
res.json({
|
|
130
|
+
deleted: true,
|
|
131
|
+
path,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof Error && error.message === 'NOT_FOUND') {
|
|
135
|
+
res.status(404).json({
|
|
136
|
+
error: 'File not found',
|
|
137
|
+
code: 'NOT_FOUND',
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.error('Error deleting file:', error);
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
error: 'Failed to delete file',
|
|
144
|
+
code: 'INTERNAL_ERROR',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// POST /files/move
|
|
150
|
+
filesRouter.post('/move', async (req: Request, res: Response) => {
|
|
151
|
+
try {
|
|
152
|
+
const { sourcePath, destinationPath } = req.body as {
|
|
153
|
+
sourcePath: string;
|
|
154
|
+
destinationPath: string;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Validate source path
|
|
158
|
+
const sourceValidation = validatePath(sourcePath);
|
|
159
|
+
if (!sourceValidation.valid) {
|
|
160
|
+
res.status(400).json({
|
|
161
|
+
error: `Source: ${sourceValidation.error}`,
|
|
162
|
+
code: 'INVALID_PATH',
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate destination path
|
|
168
|
+
const destValidation = validatePath(destinationPath);
|
|
169
|
+
if (!destValidation.valid) {
|
|
170
|
+
res.status(400).json({
|
|
171
|
+
error: `Destination: ${destValidation.error}`,
|
|
172
|
+
code: 'INVALID_PATH',
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await moveFile(sourcePath, destinationPath);
|
|
178
|
+
res.json({
|
|
179
|
+
moved: true,
|
|
180
|
+
sourcePath,
|
|
181
|
+
destinationPath,
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof Error && error.message === 'NOT_FOUND') {
|
|
185
|
+
res.status(404).json({
|
|
186
|
+
error: 'Source file not found',
|
|
187
|
+
code: 'NOT_FOUND',
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.error('Error moving file:', error);
|
|
192
|
+
res.status(500).json({
|
|
193
|
+
error: 'Failed to move file',
|
|
194
|
+
code: 'INTERNAL_ERROR',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// POST /files/metadata
|
|
200
|
+
filesRouter.post('/metadata', async (req: Request, res: Response) => {
|
|
201
|
+
try {
|
|
202
|
+
const { path } = req.body as { path: string };
|
|
203
|
+
|
|
204
|
+
// Validate path
|
|
205
|
+
const validation = validatePath(path);
|
|
206
|
+
if (!validation.valid) {
|
|
207
|
+
res.status(400).json({
|
|
208
|
+
error: validation.error,
|
|
209
|
+
code: 'INVALID_PATH',
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = await getFileMetadata(path);
|
|
215
|
+
res.json(result);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof Error && error.message === 'NOT_FOUND') {
|
|
218
|
+
res.status(404).json({
|
|
219
|
+
error: 'File not found',
|
|
220
|
+
code: 'NOT_FOUND',
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.error('Error getting file metadata:', error);
|
|
225
|
+
res.status(500).json({
|
|
226
|
+
error: 'Failed to get file metadata',
|
|
227
|
+
code: 'INTERNAL_ERROR',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Route
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
|
|
7
|
+
export const healthRouter = Router();
|
|
8
|
+
|
|
9
|
+
healthRouter.get('/', (_req, res) => {
|
|
10
|
+
res.json({
|
|
11
|
+
status: 'healthy',
|
|
12
|
+
service: 'gcp-kernel',
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
uptime: process.uptime(),
|
|
15
|
+
environment: {
|
|
16
|
+
gcpProject: process.env.GCP_PROJECT_ID || 'not set',
|
|
17
|
+
firebaseProject: process.env.FIREBASE_PROJECT_ID || 'not set',
|
|
18
|
+
bucket: process.env.GCS_BUCKET || 'not set',
|
|
19
|
+
topic: process.env.PUBSUB_EVENTS_TOPIC || 'not set',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin SDK Service
|
|
3
|
+
*
|
|
4
|
+
* Handles Firebase initialization and token validation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import admin from 'firebase-admin';
|
|
8
|
+
|
|
9
|
+
let initialized = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize Firebase Admin SDK
|
|
13
|
+
*/
|
|
14
|
+
export function initializeFirebase(): void {
|
|
15
|
+
if (initialized) return;
|
|
16
|
+
|
|
17
|
+
const projectId = process.env.FIREBASE_PROJECT_ID || process.env.GCP_PROJECT_ID;
|
|
18
|
+
|
|
19
|
+
if (!projectId) {
|
|
20
|
+
console.warn('FIREBASE_PROJECT_ID not set, auth validation will fail');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
admin.initializeApp({
|
|
26
|
+
projectId,
|
|
27
|
+
});
|
|
28
|
+
initialized = true;
|
|
29
|
+
console.log(`Firebase Admin initialized for project: ${projectId}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Failed to initialize Firebase Admin:', error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate a Firebase ID token
|
|
37
|
+
*/
|
|
38
|
+
export async function validateToken(token: string): Promise<{
|
|
39
|
+
valid: boolean;
|
|
40
|
+
uid?: string;
|
|
41
|
+
email?: string;
|
|
42
|
+
claims?: Record<string, unknown>;
|
|
43
|
+
error?: string;
|
|
44
|
+
}> {
|
|
45
|
+
if (!initialized) {
|
|
46
|
+
return { valid: false, error: 'Firebase not initialized' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const decodedToken = await admin.auth().verifyIdToken(token);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
valid: true,
|
|
54
|
+
uid: decodedToken.uid,
|
|
55
|
+
email: decodedToken.email,
|
|
56
|
+
claims: {
|
|
57
|
+
email_verified: decodedToken.email_verified,
|
|
58
|
+
name: decodedToken.name,
|
|
59
|
+
picture: decodedToken.picture,
|
|
60
|
+
...decodedToken,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : 'Token validation failed';
|
|
65
|
+
return { valid: false, error: message };
|
|
66
|
+
}
|
|
67
|
+
}
|