@descix/cli 1.0.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/README.md +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- package/vendor/mcp/transports/stdio-transport.js +118 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descix/service-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DeSciX Add-on Service Starter Template (MCP-Native)",
|
|
5
|
+
"main": "app.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node app.js",
|
|
9
|
+
"dev": "nodemon app.js",
|
|
10
|
+
"register": "node scripts/register.js",
|
|
11
|
+
"gen-jwt-secret": "node -e 'console.log(require(\"crypto\").randomBytes(32).toString(\"hex\"))'"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@descix/cloud-core": "^1.0.0",
|
|
15
|
+
"@google-cloud/firestore": "^7.1.0",
|
|
16
|
+
"@google-cloud/secret-manager": "^5.0.1",
|
|
17
|
+
"@google-cloud/storage": "^7.7.0",
|
|
18
|
+
"axios": "^1.6.2",
|
|
19
|
+
"cors": "^2.8.5",
|
|
20
|
+
"dotenv": "^16.3.1",
|
|
21
|
+
"express": "^4.18.2",
|
|
22
|
+
"google-auth-library": "^9.4.1",
|
|
23
|
+
"@descix/sdk": "^1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"nodemon": "^3.0.2"
|
|
27
|
+
},
|
|
28
|
+
"license": "@Daita_DeSci_Community_License_v1/Daita_DeSci_Community_License_v1.pdf"
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Registration Script
|
|
3
|
+
*
|
|
4
|
+
* Registers the service manifest with DeSciX Cloud.
|
|
5
|
+
* Usage: npm run register
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { Firestore } from '@google-cloud/firestore';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Load Manifest
|
|
17
|
+
const manifestPath = path.resolve(__dirname, '../manifest.json');
|
|
18
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
19
|
+
|
|
20
|
+
async function register() {
|
|
21
|
+
console.log(`[Register] Registering service: ${manifest.service.name}`);
|
|
22
|
+
|
|
23
|
+
// Connect to Firestore (uses ADC)
|
|
24
|
+
// Assumes we are running in a context with access to the Core project
|
|
25
|
+
// OR we might need to hit an admin API endpoint on Core if we don't have direct DB access.
|
|
26
|
+
// For this template, we'll assume direct DB access (common for internal services)
|
|
27
|
+
// or provide instructions to copy output to Core.
|
|
28
|
+
|
|
29
|
+
// OPTION: We could check if we have CREDENTIALS, if not, output the JSON
|
|
30
|
+
// for the user to paste into the Core admin console.
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const firestore = new Firestore();
|
|
34
|
+
const docRef = firestore.collection('ServiceManifests').doc(manifest.service.name);
|
|
35
|
+
|
|
36
|
+
await docRef.set({
|
|
37
|
+
...manifest,
|
|
38
|
+
registeredAt: new Date().toISOString(),
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
status: 'active'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(`[Register] SUCCESS: Registered ${manifest.service.name}`);
|
|
44
|
+
console.log(`[Register] Domain: ${manifest.service.domain}`);
|
|
45
|
+
console.log(`[Register] Commands: ${Object.keys(manifest.commands).join(', ')}`);
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('[Register] FAILED:', error.message);
|
|
49
|
+
console.log('\n[Tip] If you don\'t have direct Firestore access, create a document in the "ServiceManifests" collection with this ID and content:');
|
|
50
|
+
console.log(`ID: ${manifest.service.name}`);
|
|
51
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
register();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service API Router
|
|
3
|
+
*
|
|
4
|
+
* Routes incoming JSON commands to handlers.
|
|
5
|
+
* Expects `_descix` context injected by the Core Broker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
const apiRouter = express.Router();
|
|
13
|
+
|
|
14
|
+
// Command Registry
|
|
15
|
+
const commandRegistry = {
|
|
16
|
+
// Dynamic loading will happen in the handler
|
|
17
|
+
'sample_hello': 'sampleCommands.js',
|
|
18
|
+
'sample_analyze': 'sampleCommands.js'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class ServiceCommandHandler {
|
|
22
|
+
static async invoke(command, params) {
|
|
23
|
+
// 1. Check registry
|
|
24
|
+
const fileName = commandRegistry[command];
|
|
25
|
+
if (!fileName) {
|
|
26
|
+
throw new Error(`Unknown command: ${command}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Load module (simple dynamic import)
|
|
30
|
+
try {
|
|
31
|
+
const module = await import(`./commandHandlers/${fileName}`);
|
|
32
|
+
const handler = module.commands[command];
|
|
33
|
+
|
|
34
|
+
if (!handler) {
|
|
35
|
+
throw new Error(`Handler not found for ${command} in ${fileName}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. Execute
|
|
39
|
+
// We pass params directly. The handler is responsible for using _descix context.
|
|
40
|
+
return await handler(params);
|
|
41
|
+
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`[CommandHandler] Error invoking ${command}:`, error);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Main Route
|
|
50
|
+
apiRouter.post('/:command', async (req, res) => {
|
|
51
|
+
const { command } = req.params;
|
|
52
|
+
const params = req.body;
|
|
53
|
+
|
|
54
|
+
// The Core Broker injects this context
|
|
55
|
+
const context = params._descix;
|
|
56
|
+
|
|
57
|
+
if (!context) {
|
|
58
|
+
// In production, you might reject requests without context
|
|
59
|
+
// In dev, we might allow direct calls
|
|
60
|
+
console.warn(`[ApiFront] Warning: Request to ${command} missing _descix context`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
console.log(`[ApiFront] Received command: ${command}`);
|
|
65
|
+
const result = await ServiceCommandHandler.invoke(command, params);
|
|
66
|
+
res.json(result);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
res.status(500).json({
|
|
69
|
+
status: 'ERROR',
|
|
70
|
+
message: error.message
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default apiRouter;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mcpClient } from '../mcpClient.js';
|
|
2
|
+
|
|
3
|
+
export const commands = {
|
|
4
|
+
/**
|
|
5
|
+
* Simple echo command
|
|
6
|
+
*/
|
|
7
|
+
sample_hello: async (params) => {
|
|
8
|
+
const { message, _descix } = params;
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
status: 'OK',
|
|
12
|
+
message: `Echo: ${message}`,
|
|
13
|
+
user: _descix?.user?.email || 'Guest'
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Demonstrates Loopback:
|
|
19
|
+
* 1. Receives a query
|
|
20
|
+
* 2. Calls Core Tool (query_knowledge_base) via MCP Client
|
|
21
|
+
* 3. Returns combined result
|
|
22
|
+
*/
|
|
23
|
+
sample_analyze: async (params) => {
|
|
24
|
+
const { query, _descix } = params;
|
|
25
|
+
|
|
26
|
+
if (!_descix?.user) {
|
|
27
|
+
throw new Error('Authentication required for analysis');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`[SampleService] Analyzing '${query}' for user ${_descix.user.id}`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// CALL CORE TOOL: query_knowledge_base
|
|
34
|
+
// This goes Service -> Cloud -> Vector DB -> Cloud -> Service
|
|
35
|
+
const ragResult = await mcpClient.callTool('query_knowledge_base', {
|
|
36
|
+
community_id: 'descix',
|
|
37
|
+
app_id: 'docs',
|
|
38
|
+
kb_id: 'sdk',
|
|
39
|
+
query: query
|
|
40
|
+
}, _descix);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
status: 'OK',
|
|
44
|
+
analysis: `Analyzed query: ${query}`,
|
|
45
|
+
rag_findings: ragResult || 'No results found',
|
|
46
|
+
timestamp: new Date().toISOString()
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('[SampleService] RAG lookup failed:', error);
|
|
50
|
+
return {
|
|
51
|
+
status: 'ERROR',
|
|
52
|
+
message: 'Analysis failed due to upstream error',
|
|
53
|
+
details: error.message
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeSciX MCP Loopback Client
|
|
3
|
+
*
|
|
4
|
+
* Enables the service to call Core Tools (e.g. RAG, Purchase) or other Service Tools
|
|
5
|
+
* via the Federated MCP Broker (DeSciX_Cloud).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
import { utils } from './utils.js';
|
|
10
|
+
import { Signer } from '@descix/sdk'; // Assuming descix-sdk is available in the container
|
|
11
|
+
|
|
12
|
+
class McpClient {
|
|
13
|
+
constructor() {
|
|
14
|
+
// Initialize signer if key is available
|
|
15
|
+
if (utils.SERVICE_KEY) {
|
|
16
|
+
this.signer = new Signer(utils.SERVICE_KEY.privateKey, utils.SERVICE_KEY.slotId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Call a tool via the Core Broker
|
|
22
|
+
* @param {string} toolName - Name of the tool to call (e.g., 'query_knowledge_base')
|
|
23
|
+
* @param {Object} args - Tool arguments
|
|
24
|
+
* @param {Object} context - User context object (from _descix) to pass identity
|
|
25
|
+
* @returns {Promise<any>} - Tool result
|
|
26
|
+
*/
|
|
27
|
+
async callTool(toolName, args, context) {
|
|
28
|
+
if (!utils.CORE_API_URL) {
|
|
29
|
+
throw new Error('CORE_API_URL not configured');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Construct the payload simulating a PWA or internal request
|
|
33
|
+
const payload = {
|
|
34
|
+
command: toolName,
|
|
35
|
+
params: {
|
|
36
|
+
...args,
|
|
37
|
+
// Critical: Pass the original user context back to Core
|
|
38
|
+
// The Core trusts this service (authenticated via Service Secret or VPC)
|
|
39
|
+
// In this simplified template, we pass the user ID we received
|
|
40
|
+
user_id: context?.user?.id || utils.OWNER_USER_ID,
|
|
41
|
+
// If we have owner credentials, use them as fallback for auth
|
|
42
|
+
wallet_address: utils.OWNER_WALLET_ADDRESS,
|
|
43
|
+
signature: utils.OWNER_SIGNATURE
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
console.log(`[McpClient] Calling ${toolName} on ${utils.CORE_API_URL}`);
|
|
49
|
+
|
|
50
|
+
// Get headers (Signature or Legacy Secret)
|
|
51
|
+
const headers = {
|
|
52
|
+
'Content-Type': 'application/json'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (this.signer) {
|
|
56
|
+
// Sign the payload string
|
|
57
|
+
const signatureHeaders = this.signer.getHeaders(JSON.stringify(payload));
|
|
58
|
+
Object.assign(headers, signatureHeaders);
|
|
59
|
+
} else if (utils.SERVICE_SECRET) {
|
|
60
|
+
// Fallback to legacy secret
|
|
61
|
+
headers['Authorization'] = `Bearer ${utils.SERVICE_SECRET}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const response = await axios.post(utils.CORE_API_URL, payload, { headers });
|
|
65
|
+
|
|
66
|
+
if (response.data?.status === 'OK') {
|
|
67
|
+
return response.data.message;
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(response.data?.message || 'Unknown error from Core');
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`[McpClient] Error calling ${toolName}:`, error.message);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const mcpClient = new McpClient();
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Configuration Bootstrap
|
|
3
|
+
*
|
|
4
|
+
* STANDARD BOOTSTRAP PATTERN:
|
|
5
|
+
* - .env: Only bootstrap keys (CONFIG_SECRET_NAME, CONFIG_SECRET_VERSION, DEPLOY_ENV)
|
|
6
|
+
* - defaults-config.json: Instance defaults (checked in)
|
|
7
|
+
* - Secret Manager: Global config
|
|
8
|
+
* - dev-overrides.json: Local dev overrides (gitignored)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
13
|
+
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
|
14
|
+
import dotenv from 'dotenv';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
class ServiceConfig {
|
|
24
|
+
constructor() {
|
|
25
|
+
if (ServiceConfig.instance) return ServiceConfig.instance;
|
|
26
|
+
|
|
27
|
+
this.__appDir = path.resolve(__dirname, '../');
|
|
28
|
+
|
|
29
|
+
// 1. Load Bootstrap .env
|
|
30
|
+
if (process.env.DEPLOY_ENV !== 'production') {
|
|
31
|
+
dotenv.config({ path: path.resolve(this.__appDir, '.env') });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.CONFIG_SECRET_NAME = process.env.CONFIG_SECRET_NAME || null;
|
|
35
|
+
this.CONFIG_SECRET_VERSION = process.env.CONFIG_SECRET_VERSION || null;
|
|
36
|
+
this.DEPLOY_ENV = process.env.DEPLOY_ENV || null;
|
|
37
|
+
|
|
38
|
+
// 2. Initialize Config Keys (All NULL - no hardcoded defaults)
|
|
39
|
+
this.DEBUG_LOCAL = null;
|
|
40
|
+
this.LOCAL_PORT = null;
|
|
41
|
+
this.CORE_API_URL = null; // URL for DeSciX Core (for loopback)
|
|
42
|
+
this.SERVICE_SECRET = null; // Secret for authenticating loopback calls (Legacy)
|
|
43
|
+
this.SERVICE_KEY = null; // Service Delegate Key (New)
|
|
44
|
+
|
|
45
|
+
// Owner Credentials (for loopback API calls)
|
|
46
|
+
this.OWNER_WALLET_ADDRESS = null;
|
|
47
|
+
this.OWNER_SIGNATURE = null;
|
|
48
|
+
this.OWNER_USER_ID = null;
|
|
49
|
+
|
|
50
|
+
this.GOOGLE_PROJECT_ID = null;
|
|
51
|
+
this.secretClient = null;
|
|
52
|
+
this.PORT = process.env.PORT || null;
|
|
53
|
+
|
|
54
|
+
// Load instance defaults immediately
|
|
55
|
+
this._loadDefaults();
|
|
56
|
+
|
|
57
|
+
ServiceConfig.instance = this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_mergeConfig(config) {
|
|
61
|
+
for (const [key, value] of Object.entries(config)) {
|
|
62
|
+
if (key.startsWith('_')) continue;
|
|
63
|
+
// Allow dynamic expansion (undefined) or overwrite (null/value)
|
|
64
|
+
this[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_loadDefaults() {
|
|
69
|
+
const defaultsPath = path.resolve(this.__appDir, 'defaults-config.json');
|
|
70
|
+
if (fs.existsSync(defaultsPath)) {
|
|
71
|
+
try {
|
|
72
|
+
this._mergeConfig(require(defaultsPath));
|
|
73
|
+
console.log('[Config] Loaded defaults-config.json');
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('[Config] Error loading defaults-config.json:', e.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_loadDevOverrides() {
|
|
81
|
+
if (this.DEPLOY_ENV !== 'dev') return;
|
|
82
|
+
const overridesPath = path.resolve(this.__appDir, 'dev-overrides.json');
|
|
83
|
+
if (fs.existsSync(overridesPath)) {
|
|
84
|
+
try {
|
|
85
|
+
this._mergeConfig(require(overridesPath));
|
|
86
|
+
console.log('[Config] Loaded dev-overrides.json');
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error('[Config] Error loading dev-overrides.json:', e.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async accessSecretVersion(secretName, version) {
|
|
94
|
+
if (!version) throw new Error('[Config] CRITICAL: CONFIG_SECRET_VERSION not set');
|
|
95
|
+
if (!this.secretClient) this.secretClient = new SecretManagerServiceClient();
|
|
96
|
+
|
|
97
|
+
const name = `projects/${this.GOOGLE_PROJECT_ID}/secrets/${secretName}/versions/${version}`;
|
|
98
|
+
const [response] = await this.secretClient.accessSecretVersion({ name });
|
|
99
|
+
return response.payload.data.toString('utf8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _registerWithWorkspace() {
|
|
103
|
+
if (this.DEPLOY_ENV !== 'dev' && !this.DEBUG_LOCAL) return;
|
|
104
|
+
|
|
105
|
+
if (this.DEBUG_LOCAL) {
|
|
106
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let communityId = null;
|
|
110
|
+
let appId = null;
|
|
111
|
+
let workspacePath = null;
|
|
112
|
+
|
|
113
|
+
// Walk up from the service directory to find .descix/workspace.json
|
|
114
|
+
let searchDir = this.__appDir;
|
|
115
|
+
for (let i = 0; i < 10; i++) {
|
|
116
|
+
const candidate = path.resolve(searchDir, '.descix/workspace.json');
|
|
117
|
+
if (fs.existsSync(candidate)) {
|
|
118
|
+
workspacePath = candidate;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
const parent = path.dirname(searchDir);
|
|
122
|
+
if (parent === searchDir) break;
|
|
123
|
+
searchDir = parent;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Detect context from manifest.json
|
|
127
|
+
try {
|
|
128
|
+
const manifestPath = path.resolve(this.__appDir, 'manifest.json');
|
|
129
|
+
if (fs.existsSync(manifestPath)) {
|
|
130
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
131
|
+
communityId = manifest.service?.community_id || manifest.community_id;
|
|
132
|
+
appId = manifest.service?.app_id || manifest.app_id;
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.warn('[Config] Failed to detect app context from manifest:', e.message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!communityId || !appId) {
|
|
139
|
+
console.log('[Config] No communityId/appId — skipping workspace registration');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!workspacePath) {
|
|
144
|
+
console.log('[Config] No .descix/workspace.json found — skipping workspace registration');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Write service port into workspace.json for descix-serve to pick up
|
|
149
|
+
try {
|
|
150
|
+
const workspace = JSON.parse(fs.readFileSync(workspacePath, 'utf-8'));
|
|
151
|
+
if (!workspace.communities) workspace.communities = {};
|
|
152
|
+
if (!workspace.communities[communityId]) workspace.communities[communityId] = { apps: {} };
|
|
153
|
+
if (!workspace.communities[communityId].apps) workspace.communities[communityId].apps = {};
|
|
154
|
+
if (!workspace.communities[communityId].apps[appId]) workspace.communities[communityId].apps[appId] = {};
|
|
155
|
+
|
|
156
|
+
const appEntry = workspace.communities[communityId].apps[appId];
|
|
157
|
+
if (!appEntry.service) appEntry.service = {};
|
|
158
|
+
appEntry.service.port = parseInt(this.PORT, 10);
|
|
159
|
+
|
|
160
|
+
fs.writeFileSync(workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
|
|
161
|
+
console.log(`[Config] Registered ${communityId}/${appId} service port ${this.PORT} in workspace.json`);
|
|
162
|
+
|
|
163
|
+
this._workspacePath = workspacePath;
|
|
164
|
+
this._registeredCommunityId = communityId;
|
|
165
|
+
this._registeredAppId = appId;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.warn('[Config] Failed to write workspace.json:', e.message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
cleanupWorkspaceRegistration() {
|
|
172
|
+
if (!this._workspacePath || !this._registeredCommunityId || !this._registeredAppId) return;
|
|
173
|
+
try {
|
|
174
|
+
const workspace = JSON.parse(fs.readFileSync(this._workspacePath, 'utf-8'));
|
|
175
|
+
const appEntry = workspace.communities?.[this._registeredCommunityId]?.apps?.[this._registeredAppId];
|
|
176
|
+
if (appEntry?.service) {
|
|
177
|
+
delete appEntry.service.port;
|
|
178
|
+
}
|
|
179
|
+
fs.writeFileSync(this._workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
|
|
180
|
+
console.log(`[Config] Cleaned up workspace registration for ${this._registeredCommunityId}/${this._registeredAppId}`);
|
|
181
|
+
} catch {
|
|
182
|
+
// Best-effort cleanup on shutdown
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async initialize() {
|
|
187
|
+
// 1. Get Project ID
|
|
188
|
+
const auth = new GoogleAuth();
|
|
189
|
+
this.GOOGLE_PROJECT_ID = await auth.getProjectId();
|
|
190
|
+
|
|
191
|
+
// 2. Load from Secret Manager
|
|
192
|
+
if (this.CONFIG_SECRET_NAME) {
|
|
193
|
+
try {
|
|
194
|
+
const payload = await this.accessSecretVersion(
|
|
195
|
+
this.CONFIG_SECRET_NAME,
|
|
196
|
+
this.CONFIG_SECRET_VERSION
|
|
197
|
+
);
|
|
198
|
+
this._mergeConfig(JSON.parse(payload));
|
|
199
|
+
console.log(`[Config] Loaded from Secret Manager: ${this.CONFIG_SECRET_NAME}`);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn(`[Config] Secret Manager unavailable:`, error.message);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 3. Apply Dev Overrides
|
|
206
|
+
this._loadDevOverrides();
|
|
207
|
+
|
|
208
|
+
// 4. Validate Port (Allow 'auto' or 0)
|
|
209
|
+
if (!this.PORT && this.PORT !== 0) {
|
|
210
|
+
if (this.DEBUG_LOCAL && this.LOCAL_PORT) {
|
|
211
|
+
this.PORT = this.LOCAL_PORT;
|
|
212
|
+
} else {
|
|
213
|
+
// If dev, default to auto-assign
|
|
214
|
+
if (this.DEPLOY_ENV === 'dev' || this.DEBUG_LOCAL) {
|
|
215
|
+
this.PORT = 'auto';
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error('[Config] CRITICAL: PORT not configured');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 5. Default Core API URL if not set
|
|
223
|
+
if (!this.CORE_API_URL) {
|
|
224
|
+
this.CORE_API_URL = this.DEBUG_LOCAL
|
|
225
|
+
? 'https://localhost:4000/apifront'
|
|
226
|
+
: 'https://descix-cloud.descix.net/apifront';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 6. Register service port in workspace.json for descix-serve gateway
|
|
230
|
+
await this._registerWithWorkspace();
|
|
231
|
+
|
|
232
|
+
// Clean up registration on shutdown
|
|
233
|
+
process.on('SIGINT', () => this.cleanupWorkspaceRegistration());
|
|
234
|
+
process.on('SIGTERM', () => this.cleanupWorkspaceRegistration());
|
|
235
|
+
|
|
236
|
+
console.log(`[Config] Final: port=${this.PORT}, core=${this.CORE_API_URL}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const serviceConfig = new ServiceConfig();
|
|
241
|
+
|
|
242
|
+
async function initializeServiceConfig() {
|
|
243
|
+
await serviceConfig.initialize();
|
|
244
|
+
return serviceConfig;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { initializeServiceConfig, serviceConfig as utils };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"serviceName": "sample-service",
|
|
3
|
+
"description": "DeSciX Add-on Service",
|
|
4
|
+
"configChecks": [
|
|
5
|
+
{
|
|
6
|
+
"key": "CONFIG_SECRET_NAME",
|
|
7
|
+
"source": ".env",
|
|
8
|
+
"required": true,
|
|
9
|
+
"description": "Secret Manager secret name"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"key": "CONFIG_SECRET_VERSION",
|
|
13
|
+
"source": ".env",
|
|
14
|
+
"required": true,
|
|
15
|
+
"description": "Secret Manager version"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|