@stacksolo/plugin-gcp-kernel 0.1.1 → 0.1.2
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/index.js +30 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/service/Dockerfile +1 -1
- package/service/src/index.ts +9 -0
- package/service/src/routes/access.ts +143 -0
- package/service/src/routes/auth.ts +39 -0
- package/service/src/services/access.ts +235 -0
package/dist/index.js
CHANGED
|
@@ -93,6 +93,23 @@ var gcpKernelResource = defineResource({
|
|
|
93
93
|
// GCP Kernel - Serverless kernel using Cloud Run + Pub/Sub
|
|
94
94
|
// =============================================================================
|
|
95
95
|
|
|
96
|
+
// Enable required APIs
|
|
97
|
+
const ${varName}FirestoreApi = new ProjectService(this, '${name}-firestore-api', {
|
|
98
|
+
service: 'firestore.googleapis.com',
|
|
99
|
+
disableOnDestroy: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Create Firestore database (required for access control)
|
|
103
|
+
// Using FIRESTORE_NATIVE mode for document-based access control
|
|
104
|
+
const ${varName}FirestoreDb = new FirestoreDatabase(this, '${name}-firestore-db', {
|
|
105
|
+
project: '${projectId}',
|
|
106
|
+
name: '(default)',
|
|
107
|
+
locationId: '${location}',
|
|
108
|
+
type: 'FIRESTORE_NATIVE',
|
|
109
|
+
deleteProtectionState: 'DELETE_PROTECTION_DISABLED',
|
|
110
|
+
dependsOn: [${varName}FirestoreApi],
|
|
111
|
+
});
|
|
112
|
+
|
|
96
113
|
// Service account for the GCP kernel
|
|
97
114
|
const ${varName}Sa = new ServiceAccount(this, '${name}-sa', {
|
|
98
115
|
accountId: '${name}-gcp-kernel',
|
|
@@ -113,6 +130,13 @@ new ProjectIamMember(this, '${name}-pubsub-iam', {
|
|
|
113
130
|
member: \`serviceAccount:\${${varName}Sa.email}\`,
|
|
114
131
|
});
|
|
115
132
|
|
|
133
|
+
// Grant Firestore/Datastore access for access control
|
|
134
|
+
new ProjectIamMember(this, '${name}-firestore-iam', {
|
|
135
|
+
project: '${projectId}',
|
|
136
|
+
role: 'roles/datastore.user',
|
|
137
|
+
member: \`serviceAccount:\${${varName}Sa.email}\`,
|
|
138
|
+
});
|
|
139
|
+
|
|
116
140
|
// =============================================================================
|
|
117
141
|
// Pub/Sub Topics for Events
|
|
118
142
|
// =============================================================================
|
|
@@ -137,6 +161,7 @@ const ${varName}Service = new CloudRunV2Service(this, '${name}', {
|
|
|
137
161
|
name: '${name}-gcp-kernel',
|
|
138
162
|
location: '${location}',
|
|
139
163
|
ingress: 'INGRESS_TRAFFIC_ALL',
|
|
164
|
+
deletionProtection: false,
|
|
140
165
|
|
|
141
166
|
template: {
|
|
142
167
|
serviceAccount: ${varName}Sa.email,
|
|
@@ -150,7 +175,7 @@ const ${varName}Service = new CloudRunV2Service(this, '${name}', {
|
|
|
150
175
|
},
|
|
151
176
|
},
|
|
152
177
|
env: [
|
|
153
|
-
|
|
178
|
+
// Note: PORT is automatically set by Cloud Run, don't specify it
|
|
154
179
|
{ name: 'GCP_PROJECT_ID', value: '${projectId}' },
|
|
155
180
|
{ name: 'FIREBASE_PROJECT_ID', value: '${firebaseProjectId}' },
|
|
156
181
|
{ name: 'GCS_BUCKET', value: '${storageBucket}' },
|
|
@@ -175,6 +200,8 @@ new CloudRunV2ServiceIamMember(this, '${name}-public', {
|
|
|
175
200
|
});`;
|
|
176
201
|
return {
|
|
177
202
|
imports: [
|
|
203
|
+
"import { ProjectService } from '@cdktf/provider-google/lib/project-service';",
|
|
204
|
+
"import { FirestoreDatabase } from '@cdktf/provider-google/lib/firestore-database';",
|
|
178
205
|
"import { ServiceAccount } from '@cdktf/provider-google/lib/service-account';",
|
|
179
206
|
"import { ProjectIamMember } from '@cdktf/provider-google/lib/project-iam-member';",
|
|
180
207
|
"import { CloudRunV2Service } from '@cdktf/provider-google/lib/cloud-run-v2-service';",
|
|
@@ -218,7 +245,7 @@ new CloudRunV2ServiceIamMember(this, '${name}-public', {
|
|
|
218
245
|
});
|
|
219
246
|
|
|
220
247
|
// src/index.ts
|
|
221
|
-
var VERSION = "0.1.
|
|
248
|
+
var VERSION = "0.1.2";
|
|
222
249
|
var plugin = {
|
|
223
250
|
name: "@stacksolo/plugin-gcp-kernel",
|
|
224
251
|
version: VERSION,
|
|
@@ -232,7 +259,7 @@ var plugin = {
|
|
|
232
259
|
http: 8080
|
|
233
260
|
},
|
|
234
261
|
env: {
|
|
235
|
-
PORT
|
|
262
|
+
// PORT is automatically set by Cloud Run - don't specify it
|
|
236
263
|
GCP_PROJECT_ID: "",
|
|
237
264
|
FIREBASE_PROJECT_ID: "",
|
|
238
265
|
GCS_BUCKET: "",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resources/gcp-kernel.ts","../src/index.ts"],"sourcesContent":["import { defineResource, type ResourceConfig } from '@stacksolo/core';\n\nfunction toVariableName(name: string): string {\n return name.replace(/[^a-zA-Z0-9]/g, '_').replace(/^(\\d)/, '_$1');\n}\n\n/**\n * GCP Kernel Resource\n *\n * A fully GCP-native kernel implementation using:\n * - Cloud Run for HTTP endpoints (auth, files, events)\n * - Cloud Pub/Sub for event messaging (replaces NATS/JetStream)\n * - Cloud Storage for file operations\n * - Firebase Admin SDK for token validation\n *\n * This is the serverless alternative to the K8s kernel which uses embedded NATS.\n */\nexport const gcpKernelResource = defineResource({\n id: 'gcp-kernel:gcp_kernel',\n provider: 'gcp-kernel',\n name: 'GCP Kernel',\n description: 'Serverless kernel using Cloud Run + Pub/Sub (alternative to NATS-based K8s kernel)',\n icon: 'cpu',\n\n configSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n title: 'Name',\n description: 'Resource name for references (@gcp-kernel/<name>)',\n default: 'kernel',\n },\n location: {\n type: 'string',\n title: 'Region',\n description: 'GCP region (defaults to project region)',\n },\n cpu: {\n type: 'string',\n title: 'CPU',\n description: 'CPU allocation',\n default: '1',\n enum: ['1', '2', '4'],\n },\n memory: {\n type: 'string',\n title: 'Memory',\n description: 'Memory allocation',\n default: '512Mi',\n enum: ['256Mi', '512Mi', '1Gi', '2Gi'],\n },\n minInstances: {\n type: 'number',\n title: 'Min Instances',\n description: 'Minimum instances (0 for scale-to-zero)',\n default: 0,\n },\n maxInstances: {\n type: 'number',\n title: 'Max Instances',\n description: 'Maximum instances',\n default: 10,\n },\n firebaseProjectId: {\n type: 'string',\n title: 'Firebase Project ID',\n description: 'Firebase project for auth token validation',\n },\n storageBucket: {\n type: 'string',\n title: 'Storage Bucket',\n description: 'GCS bucket for file uploads',\n },\n eventRetentionDays: {\n type: 'number',\n title: 'Event Retention Days',\n description: 'How long to retain events in Pub/Sub',\n default: 7,\n },\n },\n required: ['firebaseProjectId', 'storageBucket'],\n },\n\n defaultConfig: {\n name: 'kernel',\n cpu: '1',\n memory: '512Mi',\n minInstances: 0,\n maxInstances: 10,\n eventRetentionDays: 7,\n },\n\n generate: (config: ResourceConfig) => {\n const varName = toVariableName(config.name as string);\n const name = (config.name as string) || 'kernel';\n const location = (config.location as string) || '${var.region}';\n const cpu = (config.cpu as string) || '1';\n const memory = (config.memory as string) || '512Mi';\n const minInstances = (config.minInstances as number) ?? 0;\n const maxInstances = (config.maxInstances as number) ?? 10;\n const firebaseProjectId = config.firebaseProjectId as string;\n const storageBucket = config.storageBucket as string;\n const eventRetentionDays = (config.eventRetentionDays as number) ?? 7;\n const projectId = (config.projectId as string) || '${var.project_id}';\n\n // Convert retention days to seconds\n const messageRetentionSeconds = eventRetentionDays * 24 * 60 * 60;\n\n const code = `// =============================================================================\n// GCP Kernel - Serverless kernel using Cloud Run + Pub/Sub\n// =============================================================================\n\n// Service account for the GCP kernel\nconst ${varName}Sa = new ServiceAccount(this, '${name}-sa', {\n accountId: '${name}-gcp-kernel',\n displayName: 'GCP Kernel Service Account',\n});\n\n// Grant storage access for files service\nnew ProjectIamMember(this, '${name}-storage-iam', {\n project: '${projectId}',\n role: 'roles/storage.objectAdmin',\n member: \\`serviceAccount:\\${${varName}Sa.email}\\`,\n});\n\n// Grant Pub/Sub access for events service\nnew ProjectIamMember(this, '${name}-pubsub-iam', {\n project: '${projectId}',\n role: 'roles/pubsub.editor',\n member: \\`serviceAccount:\\${${varName}Sa.email}\\`,\n});\n\n// =============================================================================\n// Pub/Sub Topics for Events\n// =============================================================================\n\n// Main events topic\nconst ${varName}EventsTopic = new PubsubTopic(this, '${name}-events-topic', {\n name: 'stacksolo-${name}-events',\n messageRetentionDuration: '${messageRetentionSeconds}s',\n});\n\n// Dead letter topic for failed message delivery\nconst ${varName}DlqTopic = new PubsubTopic(this, '${name}-dlq-topic', {\n name: 'stacksolo-${name}-events-dlq',\n messageRetentionDuration: '${messageRetentionSeconds * 2}s',\n});\n\n// =============================================================================\n// Cloud Run Service\n// =============================================================================\n\nconst ${varName}Service = new CloudRunV2Service(this, '${name}', {\n name: '${name}-gcp-kernel',\n location: '${location}',\n ingress: 'INGRESS_TRAFFIC_ALL',\n\n template: {\n serviceAccount: ${varName}Sa.email,\n containers: [{\n image: 'gcr.io/${projectId}/stacksolo-gcp-kernel:latest',\n ports: { containerPort: 8080 },\n resources: {\n limits: {\n cpu: '${cpu}',\n memory: '${memory}',\n },\n },\n env: [\n { name: 'PORT', value: '8080' },\n { name: 'GCP_PROJECT_ID', value: '${projectId}' },\n { name: 'FIREBASE_PROJECT_ID', value: '${firebaseProjectId}' },\n { name: 'GCS_BUCKET', value: '${storageBucket}' },\n { name: 'PUBSUB_EVENTS_TOPIC', value: \\`\\${${varName}EventsTopic.name}\\` },\n { name: 'PUBSUB_DLQ_TOPIC', value: \\`\\${${varName}DlqTopic.name}\\` },\n { name: 'KERNEL_TYPE', value: 'gcp' },\n ],\n }],\n scaling: {\n minInstanceCount: ${minInstances},\n maxInstanceCount: ${maxInstances},\n },\n },\n});\n\n// Allow unauthenticated access (kernel validates tokens internally)\nnew CloudRunV2ServiceIamMember(this, '${name}-public', {\n name: ${varName}Service.name,\n location: ${varName}Service.location,\n role: 'roles/run.invoker',\n member: 'allUsers',\n});`;\n\n return {\n imports: [\n \"import { ServiceAccount } from '@cdktf/provider-google/lib/service-account';\",\n \"import { ProjectIamMember } from '@cdktf/provider-google/lib/project-iam-member';\",\n \"import { CloudRunV2Service } from '@cdktf/provider-google/lib/cloud-run-v2-service';\",\n \"import { CloudRunV2ServiceIamMember } from '@cdktf/provider-google/lib/cloud-run-v2-service-iam-member';\",\n \"import { PubsubTopic } from '@cdktf/provider-google/lib/pubsub-topic';\",\n ],\n code,\n outputs: [\n `export const ${varName}Url = ${varName}Service.uri;`,\n `export const ${varName}AuthUrl = \\`\\${${varName}Service.uri}/auth/validate\\`;`,\n `export const ${varName}FilesUrl = \\`\\${${varName}Service.uri}/files\\`;`,\n `export const ${varName}EventsUrl = \\`\\${${varName}Service.uri}/events\\`;`,\n `export const ${varName}EventsTopic = ${varName}EventsTopic.name;`,\n ],\n };\n },\n\n estimateCost: (config: ResourceConfig) => {\n const minInstances = (config.minInstances as number) ?? 0;\n const cpu = parseFloat((config.cpu as string) || '1');\n const memory = parseFloat(((config.memory as string) || '512Mi').replace('Mi', '')) / 1024 || 0.5;\n\n // Cloud Run pricing\n let cloudRunCost = 0;\n if (minInstances > 0) {\n // Always-on instances\n const hoursPerMonth = 730;\n const cpuCost = minInstances * cpu * hoursPerMonth * 0.00002400 * 3600;\n const memoryCost = minInstances * memory * hoursPerMonth * 0.00000250 * 3600;\n cloudRunCost = cpuCost + memoryCost;\n } else {\n // Pay-per-use estimate (assuming 100k requests/month @ 200ms avg)\n const estimatedSeconds = 100000 * 0.2;\n cloudRunCost = estimatedSeconds * cpu * 0.00002400 + estimatedSeconds * memory * 0.00000250;\n }\n\n // Pub/Sub pricing (~$0.40/million messages, assume 100k messages/month)\n const pubsubCost = 0.04;\n\n return {\n monthly: Math.round(cloudRunCost + pubsubCost),\n currency: 'USD',\n breakdown: [\n { item: `Cloud Run (${minInstances > 0 ? 'always-on' : 'scale-to-zero'})`, amount: Math.round(cloudRunCost) },\n { item: 'Pub/Sub (~100k messages)', amount: Math.round(pubsubCost * 100) / 100 },\n ],\n };\n },\n});\n","/**\n * StackSolo GCP Kernel Plugin\n *\n * GCP-native kernel implementation using Cloud Run + Pub/Sub.\n * This is the serverless alternative to the NATS-based kernel plugin.\n *\n * Endpoints:\n * - GET /health - Health check\n * - POST /auth/validate - Validate Firebase token\n * - POST /files/* - File operations (upload-url, download-url, list, delete, move, metadata)\n * - POST /events/publish - Publish event to Pub/Sub\n * - POST /events/subscribe - Register HTTP push subscription\n */\n\nimport type { Plugin } from '@stacksolo/core';\nimport { gcpKernelResource } from './resources/index';\n\n/** Plugin version - must match package.json */\nconst VERSION = '0.1.1';\n\nexport const plugin: Plugin = {\n name: '@stacksolo/plugin-gcp-kernel',\n version: VERSION,\n resources: [gcpKernelResource],\n services: [\n {\n name: 'gcp-kernel',\n image: `ghcr.io/monkeybarrels/stacksolo-gcp-kernel:${VERSION}`,\n sourcePath: './service',\n ports: {\n http: 8080,\n },\n env: {\n PORT: '8080',\n GCP_PROJECT_ID: '',\n FIREBASE_PROJECT_ID: '',\n GCS_BUCKET: '',\n PUBSUB_EVENTS_TOPIC: '',\n },\n resources: {\n cpu: '1',\n memory: '512Mi',\n },\n },\n ],\n};\n\nexport default plugin;\n\n// Re-export types\nexport type { GcpKernelConfig, GcpKernelOutputs } from './types';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/resources/gcp-kernel.ts","../src/index.ts"],"sourcesContent":["import { defineResource, type ResourceConfig } from '@stacksolo/core';\n\nfunction toVariableName(name: string): string {\n return name.replace(/[^a-zA-Z0-9]/g, '_').replace(/^(\\d)/, '_$1');\n}\n\n/**\n * GCP Kernel Resource\n *\n * A fully GCP-native kernel implementation using:\n * - Cloud Run for HTTP endpoints (auth, files, events)\n * - Cloud Pub/Sub for event messaging (replaces NATS/JetStream)\n * - Cloud Storage for file operations\n * - Firebase Admin SDK for token validation\n *\n * This is the serverless alternative to the K8s kernel which uses embedded NATS.\n */\nexport const gcpKernelResource = defineResource({\n id: 'gcp-kernel:gcp_kernel',\n provider: 'gcp-kernel',\n name: 'GCP Kernel',\n description: 'Serverless kernel using Cloud Run + Pub/Sub (alternative to NATS-based K8s kernel)',\n icon: 'cpu',\n\n configSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n title: 'Name',\n description: 'Resource name for references (@gcp-kernel/<name>)',\n default: 'kernel',\n },\n location: {\n type: 'string',\n title: 'Region',\n description: 'GCP region (defaults to project region)',\n },\n cpu: {\n type: 'string',\n title: 'CPU',\n description: 'CPU allocation',\n default: '1',\n enum: ['1', '2', '4'],\n },\n memory: {\n type: 'string',\n title: 'Memory',\n description: 'Memory allocation',\n default: '512Mi',\n enum: ['256Mi', '512Mi', '1Gi', '2Gi'],\n },\n minInstances: {\n type: 'number',\n title: 'Min Instances',\n description: 'Minimum instances (0 for scale-to-zero)',\n default: 0,\n },\n maxInstances: {\n type: 'number',\n title: 'Max Instances',\n description: 'Maximum instances',\n default: 10,\n },\n firebaseProjectId: {\n type: 'string',\n title: 'Firebase Project ID',\n description: 'Firebase project for auth token validation',\n },\n storageBucket: {\n type: 'string',\n title: 'Storage Bucket',\n description: 'GCS bucket for file uploads',\n },\n eventRetentionDays: {\n type: 'number',\n title: 'Event Retention Days',\n description: 'How long to retain events in Pub/Sub',\n default: 7,\n },\n },\n required: ['firebaseProjectId', 'storageBucket'],\n },\n\n defaultConfig: {\n name: 'kernel',\n cpu: '1',\n memory: '512Mi',\n minInstances: 0,\n maxInstances: 10,\n eventRetentionDays: 7,\n },\n\n generate: (config: ResourceConfig) => {\n const varName = toVariableName(config.name as string);\n const name = (config.name as string) || 'kernel';\n const location = (config.location as string) || '${var.region}';\n const cpu = (config.cpu as string) || '1';\n const memory = (config.memory as string) || '512Mi';\n const minInstances = (config.minInstances as number) ?? 0;\n const maxInstances = (config.maxInstances as number) ?? 10;\n const firebaseProjectId = config.firebaseProjectId as string;\n const storageBucket = config.storageBucket as string;\n const eventRetentionDays = (config.eventRetentionDays as number) ?? 7;\n const projectId = (config.projectId as string) || '${var.project_id}';\n\n // Convert retention days to seconds\n const messageRetentionSeconds = eventRetentionDays * 24 * 60 * 60;\n\n const code = `// =============================================================================\n// GCP Kernel - Serverless kernel using Cloud Run + Pub/Sub\n// =============================================================================\n\n// Enable required APIs\nconst ${varName}FirestoreApi = new ProjectService(this, '${name}-firestore-api', {\n service: 'firestore.googleapis.com',\n disableOnDestroy: false,\n});\n\n// Create Firestore database (required for access control)\n// Using FIRESTORE_NATIVE mode for document-based access control\nconst ${varName}FirestoreDb = new FirestoreDatabase(this, '${name}-firestore-db', {\n project: '${projectId}',\n name: '(default)',\n locationId: '${location}',\n type: 'FIRESTORE_NATIVE',\n deleteProtectionState: 'DELETE_PROTECTION_DISABLED',\n dependsOn: [${varName}FirestoreApi],\n});\n\n// Service account for the GCP kernel\nconst ${varName}Sa = new ServiceAccount(this, '${name}-sa', {\n accountId: '${name}-gcp-kernel',\n displayName: 'GCP Kernel Service Account',\n});\n\n// Grant storage access for files service\nnew ProjectIamMember(this, '${name}-storage-iam', {\n project: '${projectId}',\n role: 'roles/storage.objectAdmin',\n member: \\`serviceAccount:\\${${varName}Sa.email}\\`,\n});\n\n// Grant Pub/Sub access for events service\nnew ProjectIamMember(this, '${name}-pubsub-iam', {\n project: '${projectId}',\n role: 'roles/pubsub.editor',\n member: \\`serviceAccount:\\${${varName}Sa.email}\\`,\n});\n\n// Grant Firestore/Datastore access for access control\nnew ProjectIamMember(this, '${name}-firestore-iam', {\n project: '${projectId}',\n role: 'roles/datastore.user',\n member: \\`serviceAccount:\\${${varName}Sa.email}\\`,\n});\n\n// =============================================================================\n// Pub/Sub Topics for Events\n// =============================================================================\n\n// Main events topic\nconst ${varName}EventsTopic = new PubsubTopic(this, '${name}-events-topic', {\n name: 'stacksolo-${name}-events',\n messageRetentionDuration: '${messageRetentionSeconds}s',\n});\n\n// Dead letter topic for failed message delivery\nconst ${varName}DlqTopic = new PubsubTopic(this, '${name}-dlq-topic', {\n name: 'stacksolo-${name}-events-dlq',\n messageRetentionDuration: '${messageRetentionSeconds * 2}s',\n});\n\n// =============================================================================\n// Cloud Run Service\n// =============================================================================\n\nconst ${varName}Service = new CloudRunV2Service(this, '${name}', {\n name: '${name}-gcp-kernel',\n location: '${location}',\n ingress: 'INGRESS_TRAFFIC_ALL',\n deletionProtection: false,\n\n template: {\n serviceAccount: ${varName}Sa.email,\n containers: [{\n image: 'gcr.io/${projectId}/stacksolo-gcp-kernel:latest',\n ports: { containerPort: 8080 },\n resources: {\n limits: {\n cpu: '${cpu}',\n memory: '${memory}',\n },\n },\n env: [\n // Note: PORT is automatically set by Cloud Run, don't specify it\n { name: 'GCP_PROJECT_ID', value: '${projectId}' },\n { name: 'FIREBASE_PROJECT_ID', value: '${firebaseProjectId}' },\n { name: 'GCS_BUCKET', value: '${storageBucket}' },\n { name: 'PUBSUB_EVENTS_TOPIC', value: \\`\\${${varName}EventsTopic.name}\\` },\n { name: 'PUBSUB_DLQ_TOPIC', value: \\`\\${${varName}DlqTopic.name}\\` },\n { name: 'KERNEL_TYPE', value: 'gcp' },\n ],\n }],\n scaling: {\n minInstanceCount: ${minInstances},\n maxInstanceCount: ${maxInstances},\n },\n },\n});\n\n// Allow unauthenticated access (kernel validates tokens internally)\nnew CloudRunV2ServiceIamMember(this, '${name}-public', {\n name: ${varName}Service.name,\n location: ${varName}Service.location,\n role: 'roles/run.invoker',\n member: 'allUsers',\n});`;\n\n return {\n imports: [\n \"import { ProjectService } from '@cdktf/provider-google/lib/project-service';\",\n \"import { FirestoreDatabase } from '@cdktf/provider-google/lib/firestore-database';\",\n \"import { ServiceAccount } from '@cdktf/provider-google/lib/service-account';\",\n \"import { ProjectIamMember } from '@cdktf/provider-google/lib/project-iam-member';\",\n \"import { CloudRunV2Service } from '@cdktf/provider-google/lib/cloud-run-v2-service';\",\n \"import { CloudRunV2ServiceIamMember } from '@cdktf/provider-google/lib/cloud-run-v2-service-iam-member';\",\n \"import { PubsubTopic } from '@cdktf/provider-google/lib/pubsub-topic';\",\n ],\n code,\n outputs: [\n `export const ${varName}Url = ${varName}Service.uri;`,\n `export const ${varName}AuthUrl = \\`\\${${varName}Service.uri}/auth/validate\\`;`,\n `export const ${varName}FilesUrl = \\`\\${${varName}Service.uri}/files\\`;`,\n `export const ${varName}EventsUrl = \\`\\${${varName}Service.uri}/events\\`;`,\n `export const ${varName}EventsTopic = ${varName}EventsTopic.name;`,\n ],\n };\n },\n\n estimateCost: (config: ResourceConfig) => {\n const minInstances = (config.minInstances as number) ?? 0;\n const cpu = parseFloat((config.cpu as string) || '1');\n const memory = parseFloat(((config.memory as string) || '512Mi').replace('Mi', '')) / 1024 || 0.5;\n\n // Cloud Run pricing\n let cloudRunCost = 0;\n if (minInstances > 0) {\n // Always-on instances\n const hoursPerMonth = 730;\n const cpuCost = minInstances * cpu * hoursPerMonth * 0.00002400 * 3600;\n const memoryCost = minInstances * memory * hoursPerMonth * 0.00000250 * 3600;\n cloudRunCost = cpuCost + memoryCost;\n } else {\n // Pay-per-use estimate (assuming 100k requests/month @ 200ms avg)\n const estimatedSeconds = 100000 * 0.2;\n cloudRunCost = estimatedSeconds * cpu * 0.00002400 + estimatedSeconds * memory * 0.00000250;\n }\n\n // Pub/Sub pricing (~$0.40/million messages, assume 100k messages/month)\n const pubsubCost = 0.04;\n\n return {\n monthly: Math.round(cloudRunCost + pubsubCost),\n currency: 'USD',\n breakdown: [\n { item: `Cloud Run (${minInstances > 0 ? 'always-on' : 'scale-to-zero'})`, amount: Math.round(cloudRunCost) },\n { item: 'Pub/Sub (~100k messages)', amount: Math.round(pubsubCost * 100) / 100 },\n ],\n };\n },\n});\n","/**\n * StackSolo GCP Kernel Plugin\n *\n * GCP-native kernel implementation using Cloud Run + Pub/Sub.\n * This is the serverless alternative to the NATS-based kernel plugin.\n *\n * Endpoints:\n * - GET /health - Health check\n * - POST /auth/validate - Validate Firebase token\n * - POST /files/* - File operations (upload-url, download-url, list, delete, move, metadata)\n * - POST /events/publish - Publish event to Pub/Sub\n * - POST /events/subscribe - Register HTTP push subscription\n */\n\nimport type { Plugin } from '@stacksolo/core';\nimport { gcpKernelResource } from './resources/index';\n\n/** Plugin version - must match package.json */\nconst VERSION = '0.1.2';\n\nexport const plugin: Plugin = {\n name: '@stacksolo/plugin-gcp-kernel',\n version: VERSION,\n resources: [gcpKernelResource],\n services: [\n {\n name: 'gcp-kernel',\n image: `ghcr.io/monkeybarrels/stacksolo-gcp-kernel:${VERSION}`,\n sourcePath: './service',\n ports: {\n http: 8080,\n },\n env: {\n // PORT is automatically set by Cloud Run - don't specify it\n GCP_PROJECT_ID: '',\n FIREBASE_PROJECT_ID: '',\n GCS_BUCKET: '',\n PUBSUB_EVENTS_TOPIC: '',\n },\n resources: {\n cpu: '1',\n memory: '512Mi',\n },\n },\n ],\n};\n\nexport default plugin;\n\n// Re-export types\nexport type { GcpKernelConfig, GcpKernelOutputs } from './types';\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/service/Dockerfile
CHANGED
package/service/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Cloud Pub/Sub for event messaging (replaces NATS/JetStream)
|
|
7
7
|
* - Cloud Storage for file operations
|
|
8
8
|
* - Firebase Admin SDK for token validation
|
|
9
|
+
* - Firestore for access control
|
|
9
10
|
*
|
|
10
11
|
* Endpoints:
|
|
11
12
|
* - GET /health - Health check
|
|
@@ -20,6 +21,11 @@
|
|
|
20
21
|
* - POST /events/subscribe - Register HTTP push subscription
|
|
21
22
|
* - POST /events/unsubscribe - Remove subscription
|
|
22
23
|
* - GET /events/subscriptions - List subscriptions
|
|
24
|
+
* - POST /access/grant - Grant access to a resource
|
|
25
|
+
* - POST /access/revoke - Revoke access from a resource
|
|
26
|
+
* - POST /access/check - Check if member has access
|
|
27
|
+
* - GET /access/list - List members with access
|
|
28
|
+
* - GET /access/resources - List all protected resources
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
31
|
import express from 'express';
|
|
@@ -30,6 +36,7 @@ import { healthRouter } from './routes/health.js';
|
|
|
30
36
|
import { authRouter } from './routes/auth.js';
|
|
31
37
|
import { filesRouter } from './routes/files.js';
|
|
32
38
|
import { eventsRouter } from './routes/events.js';
|
|
39
|
+
import { accessRouter } from './routes/access.js';
|
|
33
40
|
|
|
34
41
|
const app = express();
|
|
35
42
|
const PORT = parseInt(process.env.PORT || '8080', 10);
|
|
@@ -51,6 +58,7 @@ app.use('/health', healthRouter);
|
|
|
51
58
|
app.use('/auth', authRouter);
|
|
52
59
|
app.use('/files', filesRouter);
|
|
53
60
|
app.use('/events', eventsRouter);
|
|
61
|
+
app.use('/access', accessRouter);
|
|
54
62
|
|
|
55
63
|
// Root endpoint
|
|
56
64
|
app.get('/', (_req, res) => {
|
|
@@ -63,6 +71,7 @@ app.get('/', (_req, res) => {
|
|
|
63
71
|
auth: '/auth/validate',
|
|
64
72
|
files: '/files/*',
|
|
65
73
|
events: '/events/*',
|
|
74
|
+
access: '/access/*',
|
|
66
75
|
},
|
|
67
76
|
});
|
|
68
77
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control Routes
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for managing dynamic access control via Firestore.
|
|
5
|
+
*
|
|
6
|
+
* POST /access/grant - Grant access to a resource
|
|
7
|
+
* POST /access/revoke - Revoke access from a resource
|
|
8
|
+
* POST /access/check - Check if member has access
|
|
9
|
+
* GET /access/list - List members with access to a resource
|
|
10
|
+
* GET /access/resources - List all protected resources
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Router } from 'express';
|
|
14
|
+
import {
|
|
15
|
+
grantAccess,
|
|
16
|
+
revokeAccess,
|
|
17
|
+
checkAccess,
|
|
18
|
+
listAccess,
|
|
19
|
+
listResources,
|
|
20
|
+
} from '../services/access.js';
|
|
21
|
+
|
|
22
|
+
export const accessRouter = Router();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Grant access to a resource
|
|
26
|
+
*
|
|
27
|
+
* POST /access/grant
|
|
28
|
+
* Body: { resource, member, permissions, grantedBy }
|
|
29
|
+
*/
|
|
30
|
+
accessRouter.post('/grant', async (req, res) => {
|
|
31
|
+
const { resource, member, permissions, grantedBy } = req.body;
|
|
32
|
+
|
|
33
|
+
if (!resource || !member || !permissions || !grantedBy) {
|
|
34
|
+
return res.status(400).json({
|
|
35
|
+
error: 'Missing required fields: resource, member, permissions, grantedBy',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!Array.isArray(permissions)) {
|
|
40
|
+
return res.status(400).json({
|
|
41
|
+
error: 'permissions must be an array',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await grantAccess({ resource, member, permissions, grantedBy });
|
|
46
|
+
|
|
47
|
+
if (result.success) {
|
|
48
|
+
return res.json({ granted: true, resource, member, permissions });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return res.status(500).json({ error: result.error });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Revoke access from a resource
|
|
56
|
+
*
|
|
57
|
+
* POST /access/revoke
|
|
58
|
+
* Body: { resource, member, revokedBy }
|
|
59
|
+
*/
|
|
60
|
+
accessRouter.post('/revoke', async (req, res) => {
|
|
61
|
+
const { resource, member, revokedBy } = req.body;
|
|
62
|
+
|
|
63
|
+
if (!resource || !member || !revokedBy) {
|
|
64
|
+
return res.status(400).json({
|
|
65
|
+
error: 'Missing required fields: resource, member, revokedBy',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await revokeAccess({ resource, member, revokedBy });
|
|
70
|
+
|
|
71
|
+
if (result.success) {
|
|
72
|
+
return res.json({ revoked: true, resource, member });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (result.error === 'Access grant not found') {
|
|
76
|
+
return res.status(404).json({ error: result.error });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return res.status(500).json({ error: result.error });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if member has access to a resource
|
|
84
|
+
*
|
|
85
|
+
* POST /access/check
|
|
86
|
+
* Body: { resource, member, permission? }
|
|
87
|
+
*/
|
|
88
|
+
accessRouter.post('/check', async (req, res) => {
|
|
89
|
+
const { resource, member, permission } = req.body;
|
|
90
|
+
|
|
91
|
+
if (!resource || !member) {
|
|
92
|
+
return res.status(400).json({
|
|
93
|
+
error: 'Missing required fields: resource, member',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = await checkAccess({ resource, member, permission });
|
|
98
|
+
|
|
99
|
+
// Transform response to match runtime expectation (hasAccess instead of allowed)
|
|
100
|
+
return res.json({
|
|
101
|
+
hasAccess: result.allowed,
|
|
102
|
+
permissions: result.permissions || [],
|
|
103
|
+
reason: result.reason,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List members with access to a resource
|
|
109
|
+
*
|
|
110
|
+
* GET /access/list?resource=xxx
|
|
111
|
+
*/
|
|
112
|
+
accessRouter.get('/list', async (req, res) => {
|
|
113
|
+
const { resource } = req.query;
|
|
114
|
+
|
|
115
|
+
if (!resource || typeof resource !== 'string') {
|
|
116
|
+
return res.status(400).json({
|
|
117
|
+
error: 'Missing required query param: resource',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = await listAccess({ resource });
|
|
122
|
+
|
|
123
|
+
if (result.error) {
|
|
124
|
+
return res.status(500).json({ error: result.error });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return res.json({ resource, members: result.members });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* List all protected resources
|
|
132
|
+
*
|
|
133
|
+
* GET /access/resources
|
|
134
|
+
*/
|
|
135
|
+
accessRouter.get('/resources', async (_req, res) => {
|
|
136
|
+
const result = await listResources();
|
|
137
|
+
|
|
138
|
+
if (result.error) {
|
|
139
|
+
return res.status(500).json({ error: result.error });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return res.json({ resources: result.resources });
|
|
143
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Routes
|
|
3
3
|
*
|
|
4
|
+
* GET /auth/whoami - Get current user from IAP headers
|
|
4
5
|
* POST /auth/validate - Validate Firebase ID token
|
|
5
6
|
*/
|
|
6
7
|
|
|
@@ -9,6 +10,44 @@ import { validateToken } from '../services/firebase.js';
|
|
|
9
10
|
|
|
10
11
|
export const authRouter = Router();
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Get current user identity from IAP headers
|
|
15
|
+
*
|
|
16
|
+
* GET /auth/whoami
|
|
17
|
+
*
|
|
18
|
+
* Returns the authenticated user's email and ID from IAP headers.
|
|
19
|
+
* This endpoint is useful for SPAs that need to know who is logged in.
|
|
20
|
+
*/
|
|
21
|
+
authRouter.get('/whoami', (req: Request, res: Response) => {
|
|
22
|
+
// IAP sets these headers after authentication
|
|
23
|
+
const iapEmail = req.headers['x-goog-authenticated-user-email'] as string | undefined;
|
|
24
|
+
const iapId = req.headers['x-goog-authenticated-user-id'] as string | undefined;
|
|
25
|
+
|
|
26
|
+
if (!iapEmail) {
|
|
27
|
+
res.status(401).json({
|
|
28
|
+
authenticated: false,
|
|
29
|
+
error: 'No IAP authentication detected',
|
|
30
|
+
hint: 'This endpoint requires requests to pass through Identity-Aware Proxy',
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// IAP email format: "accounts.google.com:user@example.com"
|
|
36
|
+
const email = iapEmail.replace('accounts.google.com:', '');
|
|
37
|
+
const id = iapId?.replace('accounts.google.com:', '');
|
|
38
|
+
|
|
39
|
+
res.json({
|
|
40
|
+
authenticated: true,
|
|
41
|
+
email,
|
|
42
|
+
id,
|
|
43
|
+
// Include raw headers for debugging
|
|
44
|
+
raw: {
|
|
45
|
+
email: iapEmail,
|
|
46
|
+
id: iapId,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
12
51
|
interface ValidateRequest {
|
|
13
52
|
token: string;
|
|
14
53
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control Service
|
|
3
|
+
*
|
|
4
|
+
* Manages dynamic access control via Firestore.
|
|
5
|
+
* Uses the kernel_access collection to store access rules.
|
|
6
|
+
*
|
|
7
|
+
* Collection structure:
|
|
8
|
+
* kernel_access/{resource}/members/{email} => { granted, grantedBy, permissions }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import admin from 'firebase-admin';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_COLLECTION = 'kernel_access';
|
|
14
|
+
|
|
15
|
+
interface AccessGrant {
|
|
16
|
+
granted: admin.firestore.Timestamp;
|
|
17
|
+
grantedBy: string;
|
|
18
|
+
permissions: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AccessEntry {
|
|
22
|
+
member: string;
|
|
23
|
+
granted: Date;
|
|
24
|
+
grantedBy: string;
|
|
25
|
+
permissions: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AuditEntry {
|
|
29
|
+
action: 'grant' | 'revoke';
|
|
30
|
+
resource: string;
|
|
31
|
+
member: string;
|
|
32
|
+
permissions?: string[];
|
|
33
|
+
performedBy: string;
|
|
34
|
+
timestamp: admin.firestore.Timestamp;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getFirestore(): admin.firestore.Firestore {
|
|
38
|
+
return admin.firestore();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getCollection(): string {
|
|
42
|
+
return process.env.ACCESS_COLLECTION || DEFAULT_COLLECTION;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Grant access to a resource for a member
|
|
47
|
+
*/
|
|
48
|
+
export async function grantAccess(params: {
|
|
49
|
+
resource: string;
|
|
50
|
+
member: string;
|
|
51
|
+
permissions: string[];
|
|
52
|
+
grantedBy: string;
|
|
53
|
+
}): Promise<{ success: boolean; error?: string }> {
|
|
54
|
+
const { resource, member, permissions, grantedBy } = params;
|
|
55
|
+
const db = getFirestore();
|
|
56
|
+
const collection = getCollection();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const docRef = db
|
|
60
|
+
.collection(collection)
|
|
61
|
+
.doc(resource)
|
|
62
|
+
.collection('members')
|
|
63
|
+
.doc(member);
|
|
64
|
+
|
|
65
|
+
const grant: AccessGrant = {
|
|
66
|
+
granted: admin.firestore.Timestamp.now(),
|
|
67
|
+
grantedBy,
|
|
68
|
+
permissions,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await docRef.set(grant);
|
|
72
|
+
|
|
73
|
+
// Audit log
|
|
74
|
+
await db.collection(`${collection}_audit`).add({
|
|
75
|
+
action: 'grant',
|
|
76
|
+
resource,
|
|
77
|
+
member,
|
|
78
|
+
permissions,
|
|
79
|
+
performedBy: grantedBy,
|
|
80
|
+
timestamp: admin.firestore.Timestamp.now(),
|
|
81
|
+
} as AuditEntry);
|
|
82
|
+
|
|
83
|
+
return { success: true };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : 'Failed to grant access';
|
|
86
|
+
return { success: false, error: message };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Revoke access from a member for a resource
|
|
92
|
+
*/
|
|
93
|
+
export async function revokeAccess(params: {
|
|
94
|
+
resource: string;
|
|
95
|
+
member: string;
|
|
96
|
+
revokedBy: string;
|
|
97
|
+
}): Promise<{ success: boolean; error?: string }> {
|
|
98
|
+
const { resource, member, revokedBy } = params;
|
|
99
|
+
const db = getFirestore();
|
|
100
|
+
const collection = getCollection();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const docRef = db
|
|
104
|
+
.collection(collection)
|
|
105
|
+
.doc(resource)
|
|
106
|
+
.collection('members')
|
|
107
|
+
.doc(member);
|
|
108
|
+
|
|
109
|
+
const doc = await docRef.get();
|
|
110
|
+
if (!doc.exists) {
|
|
111
|
+
return { success: false, error: 'Access grant not found' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await docRef.delete();
|
|
115
|
+
|
|
116
|
+
// Audit log
|
|
117
|
+
await db.collection(`${collection}_audit`).add({
|
|
118
|
+
action: 'revoke',
|
|
119
|
+
resource,
|
|
120
|
+
member,
|
|
121
|
+
performedBy: revokedBy,
|
|
122
|
+
timestamp: admin.firestore.Timestamp.now(),
|
|
123
|
+
} as AuditEntry);
|
|
124
|
+
|
|
125
|
+
return { success: true };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : 'Failed to revoke access';
|
|
128
|
+
return { success: false, error: message };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if a member has access to a resource
|
|
134
|
+
*/
|
|
135
|
+
export async function checkAccess(params: {
|
|
136
|
+
resource: string;
|
|
137
|
+
member: string;
|
|
138
|
+
permission?: string;
|
|
139
|
+
}): Promise<{ allowed: boolean; reason?: string; permissions?: string[] }> {
|
|
140
|
+
const { resource, member, permission } = params;
|
|
141
|
+
const db = getFirestore();
|
|
142
|
+
const collection = getCollection();
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const docRef = db
|
|
146
|
+
.collection(collection)
|
|
147
|
+
.doc(resource)
|
|
148
|
+
.collection('members')
|
|
149
|
+
.doc(member);
|
|
150
|
+
|
|
151
|
+
const doc = await docRef.get();
|
|
152
|
+
|
|
153
|
+
if (!doc.exists) {
|
|
154
|
+
return { allowed: false, reason: 'no_grant' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = doc.data() as AccessGrant;
|
|
158
|
+
|
|
159
|
+
// If specific permission requested, check it
|
|
160
|
+
if (permission && !data.permissions.includes(permission)) {
|
|
161
|
+
return {
|
|
162
|
+
allowed: false,
|
|
163
|
+
reason: 'permission_denied',
|
|
164
|
+
permissions: data.permissions,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
allowed: true,
|
|
170
|
+
reason: 'explicit_grant',
|
|
171
|
+
permissions: data.permissions,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : 'Failed to check access';
|
|
175
|
+
return { allowed: false, reason: message };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* List all members with access to a resource
|
|
181
|
+
*/
|
|
182
|
+
export async function listAccess(params: {
|
|
183
|
+
resource: string;
|
|
184
|
+
}): Promise<{ members: AccessEntry[]; error?: string }> {
|
|
185
|
+
const { resource } = params;
|
|
186
|
+
const db = getFirestore();
|
|
187
|
+
const collection = getCollection();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const snapshot = await db
|
|
191
|
+
.collection(collection)
|
|
192
|
+
.doc(resource)
|
|
193
|
+
.collection('members')
|
|
194
|
+
.get();
|
|
195
|
+
|
|
196
|
+
const members: AccessEntry[] = [];
|
|
197
|
+
|
|
198
|
+
snapshot.forEach((doc) => {
|
|
199
|
+
const data = doc.data() as AccessGrant;
|
|
200
|
+
members.push({
|
|
201
|
+
member: doc.id,
|
|
202
|
+
granted: data.granted.toDate(),
|
|
203
|
+
grantedBy: data.grantedBy,
|
|
204
|
+
permissions: data.permissions,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { members };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : 'Failed to list access';
|
|
211
|
+
return { members: [], error: message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* List all protected resources
|
|
217
|
+
*/
|
|
218
|
+
export async function listResources(): Promise<{ resources: string[]; error?: string }> {
|
|
219
|
+
const db = getFirestore();
|
|
220
|
+
const collection = getCollection();
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const snapshot = await db.collection(collection).get();
|
|
224
|
+
|
|
225
|
+
const resources: string[] = [];
|
|
226
|
+
snapshot.forEach((doc) => {
|
|
227
|
+
resources.push(doc.id);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return { resources };
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const message = error instanceof Error ? error.message : 'Failed to list resources';
|
|
233
|
+
return { resources: [], error: message };
|
|
234
|
+
}
|
|
235
|
+
}
|