@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.
@@ -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
+ }