feat(core-api): wire real gcs upload and vertex llm in dev

This commit is contained in:
zouantchaw
2026-02-24 09:58:22 -05:00
parent d3aec0da0b
commit e733f36d28
7 changed files with 223 additions and 59 deletions

View File

@@ -11,3 +11,4 @@
| 2026-02-24 | 0.1.6 | Added Cloud SQL-backed idempotency storage, migration script, and command API test coverage. | | 2026-02-24 | 0.1.6 | Added Cloud SQL-backed idempotency storage, migration script, and command API test coverage. |
| 2026-02-24 | 0.1.7 | Added `/health` endpoints and switched smoke checks to `/health` for Cloud Run compatibility. | | 2026-02-24 | 0.1.7 | Added `/health` endpoints and switched smoke checks to `/health` for Cloud Run compatibility. |
| 2026-02-24 | 0.1.8 | Enabled dev frontend reachability and made deploy auth mode environment-aware (`dev` public, `staging` private). | | 2026-02-24 | 0.1.8 | Enabled dev frontend reachability and made deploy auth mode environment-aware (`dev` public, `staging` private). |
| 2026-02-24 | 0.1.9 | Switched core API from mock behavior to real GCS upload/signed URLs and real Vertex model calls in dev deployment. |

View File

@@ -8,8 +8,10 @@
"name": "@krow/core-api", "name": "@krow/core-api",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@google-cloud/storage": "^7.16.0",
"express": "^4.21.2", "express": "^4.21.2",
"firebase-admin": "^13.0.2", "firebase-admin": "^13.0.2",
"google-auth-library": "^9.15.1",
"multer": "^2.0.2", "multer": "^2.0.2",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-http": "^10.3.0", "pino-http": "^10.3.0",
@@ -151,7 +153,6 @@
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
"integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"arrify": "^2.0.0", "arrify": "^2.0.0",
"extend": "^3.0.2" "extend": "^3.0.2"
@@ -165,7 +166,6 @@
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
"integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@@ -175,7 +175,6 @@
"resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
"integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"engines": { "engines": {
"node": ">=14" "node": ">=14"
} }
@@ -185,7 +184,6 @@
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz",
"integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"@google-cloud/paginator": "^5.0.0", "@google-cloud/paginator": "^5.0.0",
"@google-cloud/projectify": "^4.0.0", "@google-cloud/projectify": "^4.0.0",
@@ -212,7 +210,6 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
@@ -398,7 +395,6 @@
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
@@ -407,8 +403,7 @@
"version": "0.12.5", "version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/@types/jsonwebtoken": { "node_modules/@types/jsonwebtoken": {
"version": "9.0.10", "version": "9.0.10",
@@ -447,7 +442,6 @@
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz",
"integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"@types/caseless": "*", "@types/caseless": "*",
"@types/node": "*", "@types/node": "*",
@@ -459,15 +453,13 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/abort-controller": { "node_modules/abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"event-target-shim": "^5.0.0" "event-target-shim": "^5.0.0"
}, },
@@ -540,7 +532,6 @@
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -557,7 +548,6 @@
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
"integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"retry": "0.13.1" "retry": "0.13.1"
} }
@@ -566,7 +556,6 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/atomic-sleep": { "node_modules/atomic-sleep": {
@@ -731,7 +720,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
@@ -821,7 +809,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -876,7 +863,6 @@
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"end-of-stream": "^1.4.1", "end-of-stream": "^1.4.1",
"inherits": "^2.0.3", "inherits": "^2.0.3",
@@ -920,7 +906,6 @@
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"once": "^1.4.0" "once": "^1.4.0"
} }
@@ -959,7 +944,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -1001,7 +985,6 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@@ -1091,7 +1074,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"strnum": "^2.1.2" "strnum": "^2.1.2"
}, },
@@ -1160,7 +1142,6 @@
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@@ -1419,7 +1400,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -1457,8 +1437,7 @@
"url": "https://patreon.com/mdevils" "url": "https://patreon.com/mdevils"
} }
], ],
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
@@ -1491,7 +1470,6 @@
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"@tootallnate/once": "2", "@tootallnate/once": "2",
"agent-base": "6", "agent-base": "6",
@@ -1506,7 +1484,6 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"debug": "4" "debug": "4"
}, },
@@ -1519,7 +1496,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@@ -1536,8 +1512,7 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "7.0.6", "version": "7.0.6",
@@ -1860,7 +1835,6 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"mime": "cli.js" "mime": "cli.js"
}, },
@@ -2028,7 +2002,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"devOptional": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
@@ -2039,7 +2012,6 @@
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"yocto-queue": "^0.1.0" "yocto-queue": "^0.1.0"
}, },
@@ -2264,7 +2236,6 @@
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">= 4" "node": ">= 4"
} }
@@ -2274,7 +2245,6 @@
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
"integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"@types/request": "^2.48.8", "@types/request": "^2.48.8",
"extend": "^3.0.2", "extend": "^3.0.2",
@@ -2498,7 +2468,6 @@
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"stubs": "^3.0.0" "stubs": "^3.0.0"
} }
@@ -2507,8 +2476,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/streamsearch": { "node_modules/streamsearch": {
"version": "1.1.0", "version": "1.1.0",
@@ -2565,15 +2533,13 @@
"url": "https://github.com/sponsors/NaturalIntelligence" "url": "https://github.com/sponsors/NaturalIntelligence"
} }
], ],
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/stubs": { "node_modules/stubs": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/superagent": { "node_modules/superagent": {
"version": "10.3.0", "version": "10.3.0",
@@ -2681,7 +2647,6 @@
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
"integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"http-proxy-agent": "^5.0.0", "http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0",
@@ -2698,7 +2663,6 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"debug": "4" "debug": "4"
}, },
@@ -2711,7 +2675,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@@ -2729,7 +2692,6 @@
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"agent-base": "6", "agent-base": "6",
"debug": "4" "debug": "4"
@@ -2742,8 +2704,7 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/teeny-request/node_modules/uuid": { "node_modules/teeny-request/node_modules/uuid": {
"version": "9.0.1", "version": "9.0.1",
@@ -2754,7 +2715,6 @@
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
@@ -2921,7 +2881,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/xtend": { "node_modules/xtend": {
@@ -2983,7 +2942,6 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },

View File

@@ -11,8 +11,10 @@
"test": "node --test" "test": "node --test"
}, },
"dependencies": { "dependencies": {
"@google-cloud/storage": "^7.16.0",
"express": "^4.21.2", "express": "^4.21.2",
"firebase-admin": "^13.0.2", "firebase-admin": "^13.0.2",
"google-auth-library": "^9.15.1",
"multer": "^2.0.2", "multer": "^2.0.2",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-http": "^10.3.0", "pino-http": "^10.3.0",

View File

@@ -5,6 +5,8 @@ import { AppError } from '../lib/errors.js';
import { requireAuth, requirePolicy } from '../middleware/auth.js'; import { requireAuth, requirePolicy } from '../middleware/auth.js';
import { createSignedUrlSchema } from '../contracts/core/create-signed-url.js'; import { createSignedUrlSchema } from '../contracts/core/create-signed-url.js';
import { invokeLlmSchema } from '../contracts/core/invoke-llm.js'; import { invokeLlmSchema } from '../contracts/core/invoke-llm.js';
import { invokeVertexModel } from '../services/llm.js';
import { generateReadSignedUrl, uploadToGcs } from '../services/storage.js';
const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024; const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024;
const ALLOWED_FILE_TYPES = new Set(['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']); const ALLOWED_FILE_TYPES = new Set(['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']);
@@ -30,6 +32,14 @@ function mockSignedUrl(fileUri, expiresInSeconds) {
}; };
} }
function useMockSignedUrl() {
return process.env.SIGNED_URL_MOCK !== 'false';
}
function useMockUpload() {
return process.env.UPLOAD_MOCK !== 'false';
}
function parseBody(schema, body) { function parseBody(schema, body) {
const parsed = schema.safeParse(body); const parsed = schema.safeParse(body);
if (!parsed.success) { if (!parsed.success) {
@@ -64,9 +74,19 @@ async function handleUploadFile(req, res, next) {
const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_'); const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
const objectPath = `uploads/${req.actor.uid}/${Date.now()}_${safeName}`; const objectPath = `uploads/${req.actor.uid}/${Date.now()}_${safeName}`;
const fileUri = `gs://${bucket}/${objectPath}`;
if (!useMockUpload()) {
await uploadToGcs({
bucket,
objectPath,
contentType: file.mimetype,
buffer: file.buffer,
});
}
res.status(200).json({ res.status(200).json({
fileUri: `gs://${bucket}/${objectPath}`, fileUri,
contentType: file.mimetype, contentType: file.mimetype,
size: file.size, size: file.size,
bucket, bucket,
@@ -85,8 +105,12 @@ async function handleCreateSignedUrl(req, res, next) {
try { try {
const payload = parseBody(createSignedUrlSchema, req.body || {}); const payload = parseBody(createSignedUrlSchema, req.body || {});
const expiresInSeconds = payload.expiresInSeconds || 300; const expiresInSeconds = payload.expiresInSeconds || 300;
const signed = useMockSignedUrl()
const signed = mockSignedUrl(payload.fileUri, expiresInSeconds); ? mockSignedUrl(payload.fileUri, expiresInSeconds)
: await generateReadSignedUrl({
fileUri: payload.fileUri,
expiresInSeconds,
});
res.status(200).json({ res.status(200).json({
...signed, ...signed,
@@ -101,12 +125,22 @@ async function handleInvokeLlm(req, res, next) {
try { try {
const payload = parseBody(invokeLlmSchema, req.body || {}); const payload = parseBody(invokeLlmSchema, req.body || {});
const startedAt = Date.now();
if (process.env.LLM_MOCK === 'false') { if (process.env.LLM_MOCK === 'false') {
throw new AppError('MODEL_FAILED', 'Real model integration not wired yet', 501); const llmResult = await invokeVertexModel({
prompt: payload.prompt,
responseJsonSchema: payload.responseJsonSchema,
fileUrls: payload.fileUrls,
});
return res.status(200).json({
result: llmResult.result,
model: llmResult.model,
latencyMs: Date.now() - startedAt,
requestId: req.requestId,
});
} }
const startedAt = Date.now(); return res.status(200).json({
res.status(200).json({
result: { result: {
summary: 'Mock model response. Replace with Vertex AI integration.', summary: 'Mock model response. Replace with Vertex AI integration.',
inputPromptSize: payload.prompt.length, inputPromptSize: payload.prompt.length,

View File

@@ -0,0 +1,93 @@
import { GoogleAuth } from 'google-auth-library';
import { AppError } from '../lib/errors.js';
function buildVertexConfig() {
const project = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT;
const location = process.env.LLM_LOCATION || process.env.BACKEND_REGION || 'us-central1';
if (!project) {
throw new AppError('MODEL_FAILED', 'GCP project is required for model invocation', 500);
}
return {
project,
location,
};
}
function withTimeout(promise, timeoutMs) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => {
reject(new AppError('MODEL_TIMEOUT', `Model request exceeded ${timeoutMs}ms`, 504));
}, timeoutMs);
}),
]);
}
function toTextFromCandidate(candidate) {
if (!candidate?.content?.parts) {
return '';
}
return candidate.content.parts
.map((part) => part?.text || '')
.join('')
.trim();
}
export async function invokeVertexModel({ prompt, responseJsonSchema, fileUrls = [] }) {
const { project, location } = buildVertexConfig();
const model = process.env.LLM_MODEL || 'gemini-2.0-flash-001';
const timeoutMs = Number.parseInt(process.env.LLM_TIMEOUT_MS || '20000', 10);
const schemaText = JSON.stringify(responseJsonSchema);
const fileContext = fileUrls.length > 0 ? `\nFiles:\n${fileUrls.join('\n')}` : '';
const instruction = `Respond with strict JSON only. Follow this schema exactly:\n${schemaText}`;
const textPrompt = `${prompt}\n\n${instruction}${fileContext}`;
const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:generateContent`;
const auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
let response;
try {
const client = await auth.getClient();
response = await withTimeout(
client.request({
url,
method: 'POST',
data: {
contents: [{ role: 'user', parts: [{ text: textPrompt }] }],
generationConfig: {
temperature: 0.2,
responseMimeType: 'application/json',
},
},
}),
timeoutMs
);
} catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError('MODEL_FAILED', 'Model invocation failed', 502);
}
const text = toTextFromCandidate(response?.data?.candidates?.[0]);
if (!text) {
throw new AppError('MODEL_FAILED', 'Model returned empty response', 502);
}
try {
return {
model,
result: JSON.parse(text),
};
} catch {
return {
model,
result: {
raw: text,
},
};
}
}

View File

@@ -0,0 +1,59 @@
import { Storage } from '@google-cloud/storage';
import { AppError } from '../lib/errors.js';
const storage = new Storage();
function parseGsUri(fileUri) {
if (!fileUri.startsWith('gs://')) {
throw new AppError('VALIDATION_ERROR', 'fileUri must start with gs://', 400);
}
const raw = fileUri.replace('gs://', '');
const slashIndex = raw.indexOf('/');
if (slashIndex <= 0 || slashIndex >= raw.length - 1) {
throw new AppError('VALIDATION_ERROR', 'fileUri must include bucket and object path', 400);
}
return {
bucket: raw.slice(0, slashIndex),
path: raw.slice(slashIndex + 1),
};
}
function allowedBuckets() {
return new Set([
process.env.PUBLIC_BUCKET || 'krow-workforce-dev-public',
process.env.PRIVATE_BUCKET || 'krow-workforce-dev-private',
]);
}
export async function uploadToGcs({ bucket, objectPath, contentType, buffer }) {
const file = storage.bucket(bucket).file(objectPath);
await file.save(buffer, {
resumable: false,
contentType,
metadata: {
cacheControl: 'private, max-age=0',
},
});
}
export async function generateReadSignedUrl({ fileUri, expiresInSeconds }) {
const { bucket, path } = parseGsUri(fileUri);
if (!allowedBuckets().has(bucket)) {
throw new AppError('FORBIDDEN', `Bucket not allowed for signing: ${bucket}`, 403);
}
const file = storage.bucket(bucket).file(path);
const expiresAtMs = Date.now() + expiresInSeconds * 1000;
const [signedUrl] = await file.getSignedUrl({
version: 'v4',
action: 'read',
expires: expiresAtMs,
});
return {
signedUrl,
expiresAt: new Date(expiresAtMs).toISOString(),
};
}

View File

@@ -30,6 +30,7 @@ endif
BACKEND_CORE_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_ARTIFACT_REPO)/core-api:latest BACKEND_CORE_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_ARTIFACT_REPO)/core-api:latest
BACKEND_COMMAND_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_ARTIFACT_REPO)/command-api:latest BACKEND_COMMAND_IMAGE ?= $(BACKEND_REGION)-docker.pkg.dev/$(GCP_PROJECT_ID)/$(BACKEND_ARTIFACT_REPO)/command-api:latest
BACKEND_LOG_LIMIT ?= 100 BACKEND_LOG_LIMIT ?= 100
BACKEND_LLM_MODEL ?= gemini-2.0-flash-001
.PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core .PHONY: backend-help backend-enable-apis backend-bootstrap-dev backend-migrate-idempotency backend-deploy-core backend-deploy-commands backend-deploy-workers backend-smoke-core backend-smoke-commands backend-logs-core
@@ -54,8 +55,10 @@ backend-enable-apis:
secretmanager.googleapis.com \ secretmanager.googleapis.com \
cloudfunctions.googleapis.com \ cloudfunctions.googleapis.com \
eventarc.googleapis.com \ eventarc.googleapis.com \
aiplatform.googleapis.com \
storage.googleapis.com \ storage.googleapis.com \
iam.googleapis.com \ iam.googleapis.com \
iamcredentials.googleapis.com \
serviceusage.googleapis.com \ serviceusage.googleapis.com \
firebase.googleapis.com; do \ firebase.googleapis.com; do \
echo " - $$api"; \ echo " - $$api"; \
@@ -83,6 +86,20 @@ backend-bootstrap-dev: backend-enable-apis
else \ else \
echo " - Runtime service account already exists."; \ echo " - Runtime service account already exists."; \
fi fi
@echo "--> Ensuring runtime service account IAM roles..."
@gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \
--member="serviceAccount:$(BACKEND_RUNTIME_SA_EMAIL)" \
--role="roles/storage.objectAdmin" \
--quiet >/dev/null
@gcloud projects add-iam-policy-binding $(GCP_PROJECT_ID) \
--member="serviceAccount:$(BACKEND_RUNTIME_SA_EMAIL)" \
--role="roles/aiplatform.user" \
--quiet >/dev/null
@gcloud iam service-accounts add-iam-policy-binding $(BACKEND_RUNTIME_SA_EMAIL) \
--member="serviceAccount:$(BACKEND_RUNTIME_SA_EMAIL)" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=$(GCP_PROJECT_ID) \
--quiet >/dev/null
@echo "--> Ensuring storage buckets exist..." @echo "--> Ensuring storage buckets exist..."
@if ! gcloud storage buckets describe gs://$(BACKEND_PUBLIC_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \ @if ! gcloud storage buckets describe gs://$(BACKEND_PUBLIC_BUCKET) --project=$(GCP_PROJECT_ID) >/dev/null 2>&1; then \
gcloud storage buckets create gs://$(BACKEND_PUBLIC_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \ gcloud storage buckets create gs://$(BACKEND_PUBLIC_BUCKET) --location=$(BACKEND_REGION) --project=$(GCP_PROJECT_ID); \
@@ -112,7 +129,7 @@ backend-deploy-core:
--region=$(BACKEND_REGION) \ --region=$(BACKEND_REGION) \
--project=$(GCP_PROJECT_ID) \ --project=$(GCP_PROJECT_ID) \
--service-account=$(BACKEND_RUNTIME_SA_EMAIL) \ --service-account=$(BACKEND_RUNTIME_SA_EMAIL) \
--set-env-vars=APP_ENV=$(ENV),GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_PRIVATE_BUCKET) \ --set-env-vars=APP_ENV=$(ENV),GCP_PROJECT_ID=$(GCP_PROJECT_ID),PUBLIC_BUCKET=$(BACKEND_PUBLIC_BUCKET),PRIVATE_BUCKET=$(BACKEND_PRIVATE_BUCKET),UPLOAD_MOCK=false,SIGNED_URL_MOCK=false,LLM_MOCK=false,LLM_LOCATION=$(BACKEND_REGION),LLM_MODEL=$(BACKEND_LLM_MODEL),LLM_TIMEOUT_MS=20000 \
$(BACKEND_RUN_AUTH_FLAG) $(BACKEND_RUN_AUTH_FLAG)
@echo "✅ Core backend service deployed." @echo "✅ Core backend service deployed."