c8y-nitro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli/commands/bootstrap.mjs +64 -0
- package/dist/cli/commands/roles.mjs +41 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +18 -0
- package/dist/cli/utils/c8y-api.mjs +172 -0
- package/dist/cli/utils/config.mjs +57 -0
- package/dist/cli/utils/env-file.mjs +61 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +51 -0
- package/dist/module/apiClient.mjs +207 -0
- package/dist/module/autoBootstrap.mjs +54 -0
- package/dist/module/c8yzip.mjs +66 -0
- package/dist/module/constants.mjs +6 -0
- package/dist/module/docker.mjs +101 -0
- package/dist/module/manifest.mjs +72 -0
- package/dist/module/probeCheck.mjs +30 -0
- package/dist/module/register.mjs +58 -0
- package/dist/module/runtime/handlers/liveness-readiness.ts +7 -0
- package/dist/module/runtime/middlewares/dev-user.ts +25 -0
- package/dist/module/runtime/plugins/c8y-variables.ts +24 -0
- package/dist/module/runtime.mjs +31 -0
- package/dist/package.mjs +7 -0
- package/dist/types/apiClient.d.mts +16 -0
- package/dist/types/manifest.d.mts +323 -0
- package/dist/types/roles.d.mts +4 -0
- package/dist/types/zip.d.mts +22 -0
- package/dist/types.d.mts +13 -0
- package/dist/types.mjs +1 -0
- package/dist/utils/client.d.mts +50 -0
- package/dist/utils/client.mjs +91 -0
- package/dist/utils/credentials.d.mts +66 -0
- package/dist/utils/credentials.mjs +117 -0
- package/dist/utils/internal/common.mjs +26 -0
- package/dist/utils/middleware.d.mts +89 -0
- package/dist/utils/middleware.mjs +62 -0
- package/dist/utils/resources.d.mts +28 -0
- package/dist/utils/resources.mjs +50 -0
- package/dist/utils.d.mts +5 -0
- package/dist/utils.mjs +6 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 schplitt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# c8y-nitro
|
|
2
|
+
|
|
3
|
+
Lightning fast Cumulocity IoT microservice development powered by [Nitro](https://v3.nitro.build).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⚡️ **Lightning Fast** - Built on Nitro's high performance engine
|
|
8
|
+
- 🔧 **Fully Configurable** - Everything configured via module options
|
|
9
|
+
- 📁 **Auto Zip Creation** - Automatically generates the deployable microservice zip
|
|
10
|
+
- 🎯 **API Client Generation** - Creates Cumulocity-compatible Angular API clients
|
|
11
|
+
- 📦 **Built-in Probes** - Automatic setup for liveliness and readiness probes
|
|
12
|
+
- 🚀 **Hot Module Reload** - Instant feedback during development
|
|
13
|
+
- 🔥 **File-based Routing** - Auto-discovered routes from your file structure
|
|
14
|
+
- 🛠️ **TypeScript First** - Full type safety with excellent DX
|
|
15
|
+
- 🔄 **Auto-Bootstrap** - Automatically registers and configures your microservice in development
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
### Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add c8y-nitro nitro@latest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
Configure your Cumulocity microservice in `nitro.config.ts`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import c8y from 'c8y-nitro'
|
|
31
|
+
|
|
32
|
+
export default defineNitroConfig({
|
|
33
|
+
c8y: {
|
|
34
|
+
// c8y-nitro configuration options go here
|
|
35
|
+
},
|
|
36
|
+
modules: [c8y()],
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
Create a `.env` or `.env.local` file with your development tenant credentials:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
C8Y_BASEURL=https://your-tenant.cumulocity.com
|
|
46
|
+
C8Y_DEVELOPMENT_TENANT=t12345
|
|
47
|
+
C8Y_DEVELOPMENT_USER=your-username
|
|
48
|
+
C8Y_DEVELOPMENT_PASSWORD=your-password
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then simply run `pnpm dev` - that's it! The module will automatically:
|
|
52
|
+
|
|
53
|
+
1. Check if the microservice exists on the tenant
|
|
54
|
+
2. Create it if needed (or use existing one without overwriting)
|
|
55
|
+
3. Subscribe your tenant to the microservice
|
|
56
|
+
4. Retrieve and save bootstrap credentials to your env file
|
|
57
|
+
|
|
58
|
+
After auto-bootstrap, your env file will contain:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
C8Y_BOOTSTRAP_TENANT=<bootstrap-tenant-id>
|
|
62
|
+
C8Y_BOOTSTRAP_USER=<bootstrap-username>
|
|
63
|
+
C8Y_BOOTSTRAP_PASSWORD=<generated-password>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Manual Bootstrap**: For more control or troubleshooting, you can use the [CLI bootstrap command](#cli-commands) to manually register your microservice.
|
|
67
|
+
|
|
68
|
+
## Automatic Zip Creation
|
|
69
|
+
|
|
70
|
+
`c8y-nitro` automatically generates a ready-to-deploy microservice zip package after each build. The process includes:
|
|
71
|
+
|
|
72
|
+
1. **Dockerfile Generation** - Creates an optimized Dockerfile using Node.js 22-slim
|
|
73
|
+
2. **Docker Image Build** - Builds and saves the Docker image to `image.tar`
|
|
74
|
+
3. **Manifest Generation** - Creates `cumulocity.json` from your package.json and configuration
|
|
75
|
+
4. **Zip Package** - Combines `image.tar` and `cumulocity.json` into a deployable zip file
|
|
76
|
+
|
|
77
|
+
> **Note**: Docker must be installed and available in your PATH.
|
|
78
|
+
|
|
79
|
+
The generated zip file (default: `<package-name>-<version>.zip` in root directory) is ready to upload directly to Cumulocity.
|
|
80
|
+
|
|
81
|
+
### Configuration
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { defineNitroConfig } from 'nitro/config'
|
|
85
|
+
import c8y from 'c8y-nitro'
|
|
86
|
+
|
|
87
|
+
export default defineNitroConfig({
|
|
88
|
+
c8y: {
|
|
89
|
+
zip: {
|
|
90
|
+
name: 'my-microservice.zip', // Custom zip name
|
|
91
|
+
outputDir: './dist', // Output directory
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
modules: [c8y()],
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Manifest Configuration
|
|
99
|
+
|
|
100
|
+
The `cumulocity.json` manifest is automatically generated from your `package.json` and can be customized via the `manifest` option.
|
|
101
|
+
|
|
102
|
+
**Auto-generated from package.json:**
|
|
103
|
+
|
|
104
|
+
- `name` (scope stripped), `version` - from package fields
|
|
105
|
+
- `provider.name` - from `author` field
|
|
106
|
+
- `provider.domain` - from `author.url` or `homepage`
|
|
107
|
+
- `provider.support` - from `bugs` or `author.email`
|
|
108
|
+
- `contextPath` - defaults to package name
|
|
109
|
+
|
|
110
|
+
### Basic Configuration
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
export default defineNitroConfig({
|
|
114
|
+
c8y: {
|
|
115
|
+
manifest: {
|
|
116
|
+
// Required API roles for the microservice
|
|
117
|
+
requiredRoles: ['ROLE_INVENTORY_READ', 'ROLE_ALARM_ADMIN'],
|
|
118
|
+
|
|
119
|
+
// Custom roles this microservice provides
|
|
120
|
+
roles: ['CUSTOM_MICROSERVICE_ROLE'],
|
|
121
|
+
|
|
122
|
+
// Resource limits
|
|
123
|
+
resources: {
|
|
124
|
+
cpu: '1',
|
|
125
|
+
memory: '1G'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
modules: [c8y()],
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For all available manifest options, see the [Cumulocity Microservice Manifest documentation](https://cumulocity.com/docs/microservice-sdk/general-aspects/#microservice-manifest).
|
|
134
|
+
|
|
135
|
+
> **Note**: Custom roles defined in the manifest are automatically available as TypeScript types for use in middleware and runtime code during development.
|
|
136
|
+
|
|
137
|
+
> **Note**: Health probe endpoints (`/_c8y_nitro/liveness` and `/_c8y_nitro/readiness`) are automatically injected if not manually defined.
|
|
138
|
+
|
|
139
|
+
## Development User Injection
|
|
140
|
+
|
|
141
|
+
During development, `c8y-nitro` automatically injects your development user credentials into all requests. This allows you to test authentication and authorization middlewares locally.
|
|
142
|
+
|
|
143
|
+
The module uses the development credentials from your `.env` file:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
C8Y_DEVELOPMENT_TENANT=t12345
|
|
147
|
+
C8Y_DEVELOPMENT_USER=your-username
|
|
148
|
+
C8Y_DEVELOPMENT_PASSWORD=your-password
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This enables testing of access control middlewares like `hasUserRequiredRole()` and `isUserFromAllowedTenant()` without needing to manually set authorization headers.
|
|
152
|
+
|
|
153
|
+
### Managing Development User Roles
|
|
154
|
+
|
|
155
|
+
Use the [CLI roles command](#cli-commands) to assign or remove your microservice's custom roles to your development user:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
npx c8y-nitro roles
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This interactive command lets you select which roles from your manifest to assign to your development user for testing.
|
|
162
|
+
|
|
163
|
+
## API Client Generation
|
|
164
|
+
|
|
165
|
+
For monorepo architectures, `c8y-nitro` can generate TypeScript Angular services that provide fully typed access to your microservice routes.
|
|
166
|
+
|
|
167
|
+
### Configuration
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
export default defineNitroConfig({
|
|
171
|
+
c8y: {
|
|
172
|
+
apiClient: {
|
|
173
|
+
dir: '../ui/src/app/services', // Output directory for generated client
|
|
174
|
+
contextPath: 'my-service' // Optional: override context path
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
modules: [c8y()],
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Generated Client
|
|
182
|
+
|
|
183
|
+
The generated service creates one method per route with automatic type inference:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Generated: my-serviceAPIClient.ts
|
|
187
|
+
@Injectable({ providedIn: 'root' })
|
|
188
|
+
export class GeneratedMyServiceAPIClient {
|
|
189
|
+
async GETHealth(): Promise<{ status: string }> { }
|
|
190
|
+
async GETUsersById(params: { id: string | number }): Promise<User> { }
|
|
191
|
+
async POSTUsers(body: CreateUserDto): Promise<User> { }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Usage in Angular
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { GeneratedMyServiceAPIClient } from './services/my-serviceAPIClient'
|
|
199
|
+
|
|
200
|
+
@Component({
|
|
201
|
+
/**
|
|
202
|
+
* ...
|
|
203
|
+
*/
|
|
204
|
+
})
|
|
205
|
+
export class MyComponent {
|
|
206
|
+
private api = inject(GeneratedMyServiceAPIClient)
|
|
207
|
+
|
|
208
|
+
async ngOnInit() {
|
|
209
|
+
const health = await this.api.GETHealth()
|
|
210
|
+
const user = await this.api.GETUsersById({ id: 123 })
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> **Note**: The client regenerates automatically when routes change during development.
|
|
216
|
+
|
|
217
|
+
## Utilities
|
|
218
|
+
|
|
219
|
+
`c8y-nitro` provides several utility functions to simplify common tasks in Cumulocity microservices.
|
|
220
|
+
|
|
221
|
+
To use these utilities, simply import them from `c8y-nitro/utils`:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { useUser, useUserClient } from 'c8y-nitro/utils'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Credentials
|
|
228
|
+
|
|
229
|
+
| Function | Description | Request Context |
|
|
230
|
+
| ---------------------------------- | --------------------------------------------------------- | :-------------: |
|
|
231
|
+
| `useSubscribedTenantCredentials()` | Get credentials for all subscribed tenants (cached 10min) | ❌ |
|
|
232
|
+
| `useDeployedTenantCredentials()` | Get credentials for the deployed tenant (cached 10 min) | ❌ |
|
|
233
|
+
| `useUserTenantCredentials()` | Get credentials for the current user's tenant | ✅ |
|
|
234
|
+
|
|
235
|
+
> **Note**: `useDeployedTenantCredentials()` shares its cache with `useSubscribedTenantCredentials()`. Both functions support `.invalidate()` and `.refresh()` methods. Invalidating or refreshing one will affect the other.
|
|
236
|
+
|
|
237
|
+
### Resources
|
|
238
|
+
|
|
239
|
+
| Function | Description | Request Context |
|
|
240
|
+
| ---------------- | ---------------------------------- | :-------------: |
|
|
241
|
+
| `useUser()` | Fetch current user from Cumulocity | ✅ |
|
|
242
|
+
| `useUserRoles()` | Get roles of the current user | ✅ |
|
|
243
|
+
|
|
244
|
+
### Client
|
|
245
|
+
|
|
246
|
+
| Function | Description | Request Context |
|
|
247
|
+
| ------------------------------ | --------------------------------------------------- | :-------------: |
|
|
248
|
+
| `useUserClient()` | Create client authenticated with user's credentials | ✅ |
|
|
249
|
+
| `useUserTenantClient()` | Create client for user's tenant (microservice user) | ✅ |
|
|
250
|
+
| `useSubscribedTenantClients()` | Create clients for all subscribed tenants | ❌ |
|
|
251
|
+
| `useDeployedTenantClient()` | Create client for the deployed tenant | ❌ |
|
|
252
|
+
|
|
253
|
+
### Middleware
|
|
254
|
+
|
|
255
|
+
| Function | Description | Request Context |
|
|
256
|
+
| ------------------------------------------ | ----------------------------------------- | :-------------: |
|
|
257
|
+
| `hasUserRequiredRole(role\|roles)` | Check if user has required role(s) | ✅ |
|
|
258
|
+
| `isUserFromAllowedTenant(tenant\|tenants)` | Check if user is from allowed tenant(s) | ✅ |
|
|
259
|
+
| `isUserFromDeployedTenant()` | Check if user is from the deployed tenant | ✅ |
|
|
260
|
+
|
|
261
|
+
## CLI Commands
|
|
262
|
+
|
|
263
|
+
| Command | Description |
|
|
264
|
+
| ----------- | ------------------------------------------------------- |
|
|
265
|
+
| `bootstrap` | Manually register microservice and retrieve credentials |
|
|
266
|
+
| `roles` | Manage development user roles |
|
|
267
|
+
|
|
268
|
+
For more information, run:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
npx c8y-nitro -h
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Development
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
# Install dependencies
|
|
278
|
+
pnpm install
|
|
279
|
+
|
|
280
|
+
# Run dev watcher
|
|
281
|
+
pnpm dev
|
|
282
|
+
|
|
283
|
+
# Build for production
|
|
284
|
+
pnpm build
|
|
285
|
+
|
|
286
|
+
# Run tests
|
|
287
|
+
pnpm test
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createC8yManifest } from "../../module/manifest.mjs";
|
|
2
|
+
import { createBasicAuthHeader, createMicroservice, findMicroserviceByName, getBootstrapCredentials, subscribeToApplication, updateMicroservice } from "../utils/c8y-api.mjs";
|
|
3
|
+
import { writeBootstrapCredentials } from "../utils/env-file.mjs";
|
|
4
|
+
import { loadC8yConfig, validateBootstrapEnv } from "../utils/config.mjs";
|
|
5
|
+
import { defineCommand, runCommand } from "citty";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/commands/bootstrap.ts
|
|
9
|
+
var bootstrap_default = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "bootstrap",
|
|
12
|
+
description: "Bootstrap your microservice to the development tenant"
|
|
13
|
+
},
|
|
14
|
+
args: {},
|
|
15
|
+
async run() {
|
|
16
|
+
consola.info("Loading configuration...");
|
|
17
|
+
const { env, c8yOptions, configDir } = await loadC8yConfig();
|
|
18
|
+
consola.info("Validating environment variables...");
|
|
19
|
+
const envVars = validateBootstrapEnv(env);
|
|
20
|
+
consola.info("Building manifest...");
|
|
21
|
+
const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
|
|
22
|
+
consola.success(`Manifest created for: ${manifest.name} v${manifest.version}`);
|
|
23
|
+
const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
24
|
+
consola.info(`Checking if microservice "${manifest.name}" exists...`);
|
|
25
|
+
const existingApp = await findMicroserviceByName(envVars.C8Y_BASEURL, manifest.name, authHeader);
|
|
26
|
+
let appId;
|
|
27
|
+
if (existingApp) {
|
|
28
|
+
consola.warn(`Microservice "${manifest.name}" already exists on development tenant (ID: ${existingApp.id})`);
|
|
29
|
+
if (!await consola.prompt("Do you want to update the existing microservice?", {
|
|
30
|
+
type: "confirm",
|
|
31
|
+
cancel: "reject"
|
|
32
|
+
})) {
|
|
33
|
+
consola.info("Bootstrap cancelled.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
consola.info("Updating microservice...");
|
|
37
|
+
appId = (await updateMicroservice(envVars.C8Y_BASEURL, existingApp.id, manifest, authHeader)).id;
|
|
38
|
+
consola.success(`Microservice updated successfully (ID: ${appId})`);
|
|
39
|
+
} else {
|
|
40
|
+
consola.info("Creating microservice...");
|
|
41
|
+
appId = (await createMicroservice(envVars.C8Y_BASEURL, manifest, authHeader)).id;
|
|
42
|
+
consola.success(`Microservice created successfully (ID: ${appId})`);
|
|
43
|
+
}
|
|
44
|
+
consola.info("Subscribing tenant to application...");
|
|
45
|
+
await subscribeToApplication(envVars.C8Y_BASEURL, envVars.C8Y_DEVELOPMENT_TENANT, appId, authHeader);
|
|
46
|
+
consola.success("Tenant subscribed to application");
|
|
47
|
+
consola.info("Fetching bootstrap credentials...");
|
|
48
|
+
const credentials = await getBootstrapCredentials(envVars.C8Y_BASEURL, appId, authHeader);
|
|
49
|
+
consola.info("Writing bootstrap credentials...");
|
|
50
|
+
const envFileName = await writeBootstrapCredentials(configDir, {
|
|
51
|
+
C8Y_BOOTSTRAP_TENANT: credentials.tenant,
|
|
52
|
+
C8Y_BOOTSTRAP_USER: credentials.name,
|
|
53
|
+
C8Y_BOOTSTRAP_PASSWORD: credentials.password
|
|
54
|
+
});
|
|
55
|
+
consola.success(`Bootstrap credentials written to ${envFileName}`);
|
|
56
|
+
if (manifest.roles && manifest.roles.length > 0) {
|
|
57
|
+
if (await consola.prompt("Do you want to manage microservice roles for your development user?", { type: "confirm" })) await runCommand(await import("./roles.mjs").then((r) => r.default), { rawArgs: [] });
|
|
58
|
+
}
|
|
59
|
+
consola.success("Bootstrap complete!");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { bootstrap_default as default };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createC8yManifest } from "../../module/manifest.mjs";
|
|
2
|
+
import { assignUserRole, createBasicAuthHeader, unassignUserRole } from "../utils/c8y-api.mjs";
|
|
3
|
+
import { loadC8yConfig, validateBootstrapEnv } from "../utils/config.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/commands/roles.ts
|
|
8
|
+
var roles_default = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "roles",
|
|
11
|
+
description: "Manage microservice roles for the development user"
|
|
12
|
+
},
|
|
13
|
+
args: {},
|
|
14
|
+
async run() {
|
|
15
|
+
consola.info("Loading configuration...");
|
|
16
|
+
const { env, c8yOptions, configDir } = await loadC8yConfig();
|
|
17
|
+
consola.info("Validating environment variables...");
|
|
18
|
+
const envVars = validateBootstrapEnv(env);
|
|
19
|
+
consola.info("Building manifest...");
|
|
20
|
+
const manifest = await createC8yManifest(configDir, c8yOptions?.manifest);
|
|
21
|
+
const authHeader = createBasicAuthHeader(envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, envVars.C8Y_DEVELOPMENT_PASSWORD);
|
|
22
|
+
if (manifest.roles && manifest.roles.length > 0) {
|
|
23
|
+
const rolesToAssign = await consola.prompt("Select roles to assign to your user (unselected roles will be removed):", {
|
|
24
|
+
type: "multiselect",
|
|
25
|
+
options: manifest.roles,
|
|
26
|
+
cancel: "reject",
|
|
27
|
+
required: false
|
|
28
|
+
});
|
|
29
|
+
consola.info("Managing user roles...");
|
|
30
|
+
const rolePromises = manifest.roles.map(async (role) => {
|
|
31
|
+
if (rolesToAssign.includes(role)) return await assignUserRole(envVars.C8Y_BASEURL, envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, role, authHeader);
|
|
32
|
+
else return await unassignUserRole(envVars.C8Y_BASEURL, envVars.C8Y_DEVELOPMENT_TENANT, envVars.C8Y_DEVELOPMENT_USER, role, authHeader);
|
|
33
|
+
});
|
|
34
|
+
await Promise.all(rolePromises);
|
|
35
|
+
consola.success("Role management complete");
|
|
36
|
+
} else consola.warn("No roles defined in manifest. Nothing to manage.");
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { roles_default as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { description, name, version } from "../package.mjs";
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/index.ts
|
|
5
|
+
runMain(defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name,
|
|
8
|
+
version,
|
|
9
|
+
description
|
|
10
|
+
},
|
|
11
|
+
subCommands: {
|
|
12
|
+
bootstrap: () => import("./commands/bootstrap.mjs").then((r) => r.default),
|
|
13
|
+
roles: () => import("./commands/roles.mjs").then((r) => r.default)
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/utils/c8y-api.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Basic Auth header for Cumulocity API requests.
|
|
6
|
+
* Format: base64(tenant/user:password)
|
|
7
|
+
* @param tenant - The Cumulocity tenant ID
|
|
8
|
+
* @param user - The username
|
|
9
|
+
* @param password - The password
|
|
10
|
+
*/
|
|
11
|
+
function createBasicAuthHeader(tenant, user, password) {
|
|
12
|
+
const credentials = `${tenant}/${user}:${password}`;
|
|
13
|
+
return `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a microservice with the given name already exists.
|
|
17
|
+
* @param baseUrl - The Cumulocity base URL
|
|
18
|
+
* @param name - The microservice name to search for
|
|
19
|
+
* @param authHeader - The Basic Auth header
|
|
20
|
+
* @returns The application if found, undefined otherwise
|
|
21
|
+
*/
|
|
22
|
+
async function findMicroserviceByName(baseUrl, name, authHeader) {
|
|
23
|
+
const url = `${baseUrl}/application/applications?name=${encodeURIComponent(name)}`;
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: authHeader,
|
|
28
|
+
Accept: "application/json"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) throw new Error(`Failed to query microservices: ${response.status} ${response.statusText}`, { cause: response });
|
|
32
|
+
return (await response.json()).applications.find((app) => app.name === name);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new microservice in Cumulocity.
|
|
36
|
+
* @param baseUrl - The Cumulocity base URL
|
|
37
|
+
* @param manifest - The microservice manifest
|
|
38
|
+
* @param authHeader - The Basic Auth header
|
|
39
|
+
* @returns The created application
|
|
40
|
+
*/
|
|
41
|
+
async function createMicroservice(baseUrl, manifest, authHeader) {
|
|
42
|
+
const url = `${baseUrl}/application/applications`;
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"Authorization": authHeader,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"Accept": "application/json"
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(manifest)
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const errorText = await response.text();
|
|
54
|
+
throw new Error(`Failed to create microservice: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
55
|
+
}
|
|
56
|
+
return await response.json();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Updates an existing microservice in Cumulocity.
|
|
60
|
+
* @param baseUrl - The Cumulocity base URL
|
|
61
|
+
* @param appId - The application ID to update
|
|
62
|
+
* @param manifest - The microservice manifest
|
|
63
|
+
* @param authHeader - The Basic Auth header
|
|
64
|
+
* @returns The updated application
|
|
65
|
+
*/
|
|
66
|
+
async function updateMicroservice(baseUrl, appId, manifest, authHeader) {
|
|
67
|
+
const url = `${baseUrl}/application/applications/${appId}`;
|
|
68
|
+
const { type, ...manifestWithoutType } = manifest;
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
method: "PUT",
|
|
71
|
+
headers: {
|
|
72
|
+
"Authorization": authHeader,
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Accept": "application/json"
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(manifestWithoutType)
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
throw new Error(`Failed to update microservice: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
81
|
+
}
|
|
82
|
+
return await response.json();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Subscribes a tenant to an application.
|
|
86
|
+
* @param baseUrl - The Cumulocity base URL
|
|
87
|
+
* @param tenantId - The tenant ID to subscribe
|
|
88
|
+
* @param appId - The application ID
|
|
89
|
+
* @param authHeader - The Basic Auth header
|
|
90
|
+
*/
|
|
91
|
+
async function subscribeToApplication(baseUrl, tenantId, appId, authHeader) {
|
|
92
|
+
const url = `${baseUrl}/tenant/tenants/${tenantId}/applications`;
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"Authorization": authHeader,
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"Accept": "application/json"
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({ application: { self: `${baseUrl}/application/applications/${appId}` } })
|
|
101
|
+
});
|
|
102
|
+
if (response.status === 409) return;
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const errorText = await response.text();
|
|
105
|
+
throw new Error(`Failed to subscribe tenant to application: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Fetches the bootstrap credentials for a microservice.
|
|
110
|
+
* @param baseUrl - The Cumulocity base URL
|
|
111
|
+
* @param appId - The application ID
|
|
112
|
+
* @param authHeader - The Basic Auth header
|
|
113
|
+
*/
|
|
114
|
+
async function getBootstrapCredentials(baseUrl, appId, authHeader) {
|
|
115
|
+
const url = `${baseUrl}/application/applications/${appId}/bootstrapUser`;
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
method: "GET",
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: authHeader,
|
|
120
|
+
Accept: "application/json"
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) throw new Error(`Failed to get bootstrap credentials: ${response.status} ${response.statusText}`, { cause: response });
|
|
124
|
+
return await response.json();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Assigns a role to a user in a tenant.
|
|
128
|
+
* @param baseUrl - The Cumulocity base URL
|
|
129
|
+
* @param tenantId - The tenant ID
|
|
130
|
+
* @param userId - The user ID
|
|
131
|
+
* @param roleId - The role ID to assign
|
|
132
|
+
* @param authHeader - The Basic Auth header
|
|
133
|
+
*/
|
|
134
|
+
async function assignUserRole(baseUrl, tenantId, userId, roleId, authHeader) {
|
|
135
|
+
const url = `${baseUrl}/user/${tenantId}/users/${userId}/roles`;
|
|
136
|
+
const response = await fetch(url, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
"Authorization": authHeader,
|
|
140
|
+
"Content-Type": "application/vnd.com.nsn.cumulocity.rolereference+json",
|
|
141
|
+
"Accept": "application/json"
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({ role: { self: `${baseUrl}/user/roles/${roleId}` } })
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const errorText = await response.text();
|
|
147
|
+
throw new Error(`Failed to assign role ${roleId}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Unassigns a role from a user in a tenant.
|
|
152
|
+
* @param baseUrl - The Cumulocity base URL
|
|
153
|
+
* @param tenantId - The tenant ID
|
|
154
|
+
* @param userId - The user ID
|
|
155
|
+
* @param roleId - The role ID to unassign
|
|
156
|
+
* @param authHeader - The Basic Auth header
|
|
157
|
+
*/
|
|
158
|
+
async function unassignUserRole(baseUrl, tenantId, userId, roleId, authHeader) {
|
|
159
|
+
const url = `${baseUrl}/user/${tenantId}/users/${userId}/roles/${roleId}`;
|
|
160
|
+
const response = await fetch(url, {
|
|
161
|
+
method: "DELETE",
|
|
162
|
+
headers: { Authorization: authHeader }
|
|
163
|
+
});
|
|
164
|
+
if (response.status === 404) return;
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const errorText = await response.text();
|
|
167
|
+
throw new Error(`Failed to unassign role ${roleId}: ${response.status} ${response.statusText}\n${errorText}`, { cause: response });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { assignUserRole, createBasicAuthHeader, createMicroservice, findMicroserviceByName, getBootstrapCredentials, subscribeToApplication, unassignUserRole, updateMicroservice };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { dirname } from "pathe";
|
|
2
|
+
import { loadConfig, loadDotenv } from "c12";
|
|
3
|
+
import process from "process";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/utils/config.ts
|
|
6
|
+
/**
|
|
7
|
+
* Loads c8y configuration from nitro.config and .env files.
|
|
8
|
+
* Searches in cwd.
|
|
9
|
+
*/
|
|
10
|
+
async function loadC8yConfig() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const env = await loadDotenv({
|
|
13
|
+
cwd,
|
|
14
|
+
fileName: [".env", ".env.local"]
|
|
15
|
+
});
|
|
16
|
+
const { config, _configFile } = await loadConfig({
|
|
17
|
+
configFile: "nitro.config",
|
|
18
|
+
cwd
|
|
19
|
+
});
|
|
20
|
+
if (!_configFile) throw new Error("No nitro.config file found. Please ensure you have a valid nitro.config file.");
|
|
21
|
+
const configDir = dirname(_configFile);
|
|
22
|
+
return {
|
|
23
|
+
env,
|
|
24
|
+
nitroConfig: config,
|
|
25
|
+
c8yOptions: config.c8y,
|
|
26
|
+
configDir,
|
|
27
|
+
configFile: _configFile
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Required environment variables for bootstrap command
|
|
32
|
+
*/
|
|
33
|
+
const REQUIRED_BOOTSTRAP_ENV_VARS = [
|
|
34
|
+
"C8Y_BASEURL",
|
|
35
|
+
"C8Y_DEVELOPMENT_TENANT",
|
|
36
|
+
"C8Y_DEVELOPMENT_USER",
|
|
37
|
+
"C8Y_DEVELOPMENT_PASSWORD"
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Validates that all required environment variables are present.
|
|
41
|
+
* @param env - Environment variables object
|
|
42
|
+
* @returns The validated environment variables
|
|
43
|
+
* @throws Error if any required variable is missing
|
|
44
|
+
*/
|
|
45
|
+
function validateBootstrapEnv(env) {
|
|
46
|
+
const missing = REQUIRED_BOOTSTRAP_ENV_VARS.filter((key) => !env[key]);
|
|
47
|
+
if (missing.length > 0) throw new Error(`Missing required environment variables:\n${missing.map((k) => ` - ${k}`).join("\n")}\n\nPlease set these in your .env or .env.local file.`);
|
|
48
|
+
return {
|
|
49
|
+
C8Y_BASEURL: env.C8Y_BASEURL.endsWith("/") ? env.C8Y_BASEURL.slice(0, -1) : env.C8Y_BASEURL,
|
|
50
|
+
C8Y_DEVELOPMENT_TENANT: env.C8Y_DEVELOPMENT_TENANT,
|
|
51
|
+
C8Y_DEVELOPMENT_USER: env.C8Y_DEVELOPMENT_USER,
|
|
52
|
+
C8Y_DEVELOPMENT_PASSWORD: env.C8Y_DEVELOPMENT_PASSWORD
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { loadC8yConfig, validateBootstrapEnv };
|