@habitusnet/bc365 2.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/CHANGELOG.md +25 -0
- package/README.md +48 -0
- package/bin/bc365.js +3 -0
- package/lib/auth.js +52 -0
- package/lib/cli.js +77 -0
- package/lib/discovery.js +38 -0
- package/lib/index.js +2 -0
- package/lib/onboard.js +74 -0
- package/lib/profiles.js +46 -0
- package/lib/versions.js +27 -0
- package/package.json +40 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [2.0.0] — Unreleased
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `bc365 onboard` CLI: auto-discovers tenant, environments, companies via Entra ID device code flow, writes `.mcp.json`
|
|
9
|
+
- `bc365 switch <profile>` for multi-tenant profile management
|
|
10
|
+
- `bc365 profiles` command to list saved profiles
|
|
11
|
+
- `bc365 check` command to check latest npm versions of bc365 packages
|
|
12
|
+
- Entra ID multi-tenant app (`bc365 CLI`) with device code flow authentication
|
|
13
|
+
- Token caching via OS keychain (`keytar`)
|
|
14
|
+
- Vendored mirror repos with daily upstream-watch and security scanning:
|
|
15
|
+
- `habitusnet/d365bc-admin-mcp`
|
|
16
|
+
- `habitusnet/mcp-business-central`
|
|
17
|
+
- npm package published as `@habitusnet/bc365`
|
|
18
|
+
- CI: Jest tests, npm audit, CodeQL on every PR
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Package name changed to `@habitusnet/bc365` for scoped npm distribution
|
|
22
|
+
- Minimum Node.js version: 20
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# bc365 — MCP Config Manager for Business Central
|
|
2
|
+
|
|
3
|
+
`@habitusnet/bc365` is a CLI that auto-discovers your Microsoft Dynamics 365 Business Central environments and writes `.mcp.json` for use with Claude / MCP-compatible AI tools.
|
|
4
|
+
|
|
5
|
+
## v2 — Smart Onboarding
|
|
6
|
+
|
|
7
|
+
Instead of manually editing `.mcp.json`, use the CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @habitusnet/bc365 onboard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Sign in with your Microsoft account. The CLI discovers your tenant, environments, and companies automatically, checks your BC permissions, and writes `.mcp.json`.
|
|
14
|
+
|
|
15
|
+
**Prerequisites:**
|
|
16
|
+
- Microsoft 365 / Azure AD account with access to Business Central
|
|
17
|
+
- Business Central environment with `D365 BUS FULL ACCESS` permission set
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @habitusnet/bc365
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or use without installing:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @habitusnet/bc365 onboard
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | Description |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `bc365 onboard` | Auto-discover tenant, environments, companies; write `.mcp.json` |
|
|
38
|
+
| `bc365 profiles` | List saved profiles |
|
|
39
|
+
| `bc365 switch <profile>` | Switch to a saved profile |
|
|
40
|
+
| `bc365 check` | Check latest npm versions of bc365 packages |
|
|
41
|
+
|
|
42
|
+
## Multi-Tenant Usage
|
|
43
|
+
|
|
44
|
+
For agencies managing multiple clients, see [SETUP.md](SETUP.md#multi-tenant-usage-agencies).
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
package/bin/bc365.js
ADDED
package/lib/auth.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { PublicClientApplication } from '@azure/msal-node';
|
|
2
|
+
import keytar from 'keytar';
|
|
3
|
+
|
|
4
|
+
const SERVICE = 'bc365';
|
|
5
|
+
const ACCOUNT = 'token';
|
|
6
|
+
const CLIENT_ID = process.env.BC365_CLIENT_ID ?? 'e9dd228a-569c-4ab2-8e0a-cda6b01555d9';
|
|
7
|
+
const AUTHORITY = 'https://login.microsoftonline.com/organizations';
|
|
8
|
+
const SCOPES = [
|
|
9
|
+
'https://api.businesscentral.dynamics.com/user_impersonation',
|
|
10
|
+
'User.Read',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Lazy PCA creation so tests can control the mock per-call via
|
|
14
|
+
// PublicClientApplication.mockImplementation() before invoking getToken().
|
|
15
|
+
function createPca() {
|
|
16
|
+
return new PublicClientApplication({
|
|
17
|
+
auth: { clientId: CLIENT_ID, authority: AUTHORITY },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getToken() {
|
|
22
|
+
const cached = await keytar.getPassword(SERVICE, ACCOUNT);
|
|
23
|
+
if (cached) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(cached);
|
|
26
|
+
if (parsed.expiresOn > Date.now() + 60_000) return parsed;
|
|
27
|
+
} catch {
|
|
28
|
+
// Corrupt cache — fall through to re-authenticate
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pca = createPca();
|
|
33
|
+
const response = await pca.acquireTokenByDeviceCode({
|
|
34
|
+
scopes: SCOPES,
|
|
35
|
+
deviceCodeCallback: (info) => {
|
|
36
|
+
console.log(info.message);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const token = {
|
|
41
|
+
accessToken: response.accessToken,
|
|
42
|
+
expiresOn: response.expiresOn instanceof Date
|
|
43
|
+
? response.expiresOn.getTime()
|
|
44
|
+
: response.expiresOn,
|
|
45
|
+
};
|
|
46
|
+
await keytar.setPassword(SERVICE, ACCOUNT, JSON.stringify(token));
|
|
47
|
+
return token;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function clearToken() {
|
|
51
|
+
await keytar.deletePassword(SERVICE, ACCOUNT);
|
|
52
|
+
}
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { onboard } from './onboard.js';
|
|
5
|
+
import { listProfiles, loadProfile } from './profiles.js';
|
|
6
|
+
import { checkVersions } from './versions.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version } = require('../package.json');
|
|
10
|
+
|
|
11
|
+
export const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('bc365')
|
|
15
|
+
.description('Business Central MCP onboarding and management CLI')
|
|
16
|
+
.version(version);
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('onboard')
|
|
20
|
+
.description('Auto-discover tenant/environment/company and write .mcp.json')
|
|
21
|
+
.option('-t, --tenant-id <id>', 'Azure AD tenant ID (skip Graph lookup)')
|
|
22
|
+
.option('-o, --output <path>', 'Output path for .mcp.json', '.mcp.json')
|
|
23
|
+
.option('-p, --profile <name>', 'Profile name to save (default: tenantId/envName)')
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
try {
|
|
26
|
+
await onboard({ tenantId: opts.tenantId, output: opts.output, profileName: opts.profile });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('switch <profile>')
|
|
35
|
+
.description('Switch active profile (writes .mcp.json from saved profile)')
|
|
36
|
+
.option('-o, --output <path>', 'Output path', '.mcp.json')
|
|
37
|
+
.action(async (profileName, opts) => {
|
|
38
|
+
try {
|
|
39
|
+
const profile = await loadProfile(profileName);
|
|
40
|
+
if (!profile) {
|
|
41
|
+
console.error(chalk.red(`✗ Profile '${profileName}' not found. Run 'bc365 onboard' first.`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const { writeFile } = await import('node:fs/promises');
|
|
45
|
+
const { buildMcpConfig } = await import('./onboard.js');
|
|
46
|
+
const config = buildMcpConfig(profile);
|
|
47
|
+
await writeFile(opts.output, JSON.stringify(config, null, 2), 'utf8');
|
|
48
|
+
console.log(chalk.green(`✓ Switched to profile '${profileName}'`));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.command('profiles')
|
|
57
|
+
.description('List saved tenant profiles')
|
|
58
|
+
.action(async () => {
|
|
59
|
+
const profiles = await listProfiles();
|
|
60
|
+
if (profiles.length === 0) {
|
|
61
|
+
console.log('No profiles saved yet. Run bc365 onboard to create one.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
profiles.forEach((p) => console.log(` ${p.name} (${p.tenantId})`));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
program
|
|
68
|
+
.command('check')
|
|
69
|
+
.description('Check latest versions of bc365 packages from npm registry')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
console.log('Checking npm registry...');
|
|
72
|
+
const results = await checkVersions();
|
|
73
|
+
results.forEach((r) => {
|
|
74
|
+
const status = r.latest ? chalk.green(r.latest) : chalk.yellow('unknown');
|
|
75
|
+
console.log(` ${r.package} latest: ${status}`);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/lib/discovery.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const BC_ADMIN_BASE = 'https://api.businesscentral.dynamics.com/admin/v2.21';
|
|
2
|
+
const BC_API_BASE = 'https://api.businesscentral.dynamics.com/v2.0';
|
|
3
|
+
const REQUIRED_PERMISSIONS = ['D365 BUS FULL ACCESS'];
|
|
4
|
+
|
|
5
|
+
async function bcFetch(url, token) {
|
|
6
|
+
const res = await fetch(url, {
|
|
7
|
+
headers: { Authorization: `Bearer ${token.accessToken}` },
|
|
8
|
+
});
|
|
9
|
+
if (!res.ok) throw new Error(`BC API error ${res.status}: ${url}`);
|
|
10
|
+
return res.json();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getEnvironments(token, opts = {}) {
|
|
14
|
+
const { tenantId, type } = opts;
|
|
15
|
+
const url = tenantId
|
|
16
|
+
? `${BC_ADMIN_BASE}/applications/BusinessCentral/environments?aadTenantId=${tenantId}`
|
|
17
|
+
: `${BC_ADMIN_BASE}/applications/BusinessCentral/environments`;
|
|
18
|
+
const data = await bcFetch(url, token);
|
|
19
|
+
const envs = data.value ?? [];
|
|
20
|
+
return type ? envs.filter((e) => e.type === type) : envs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getCompanies(env, token) {
|
|
24
|
+
const url = `${BC_API_BASE}/${encodeURIComponent(env.aadTenantId)}/${encodeURIComponent(env.name)}/api/v2.0/companies`;
|
|
25
|
+
const data = await bcFetch(url, token);
|
|
26
|
+
return data.value ?? [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getPermissions(env, token, opts = {}) {
|
|
30
|
+
const { companyId } = opts;
|
|
31
|
+
const url = `${BC_API_BASE}/${encodeURIComponent(env.aadTenantId)}/${encodeURIComponent(env.name)}/api/v2.0/companies(${encodeURIComponent(companyId)})/userPermissions`;
|
|
32
|
+
const data = await bcFetch(url, token);
|
|
33
|
+
const grantedRoles = (data.value ?? []).map((p) => p.roleId);
|
|
34
|
+
return {
|
|
35
|
+
present: REQUIRED_PERMISSIONS.filter((r) => grantedRoles.includes(r)),
|
|
36
|
+
missing: REQUIRED_PERMISSIONS.filter((r) => !grantedRoles.includes(r)),
|
|
37
|
+
};
|
|
38
|
+
}
|
package/lib/index.js
ADDED
package/lib/onboard.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { getToken } from './auth.js';
|
|
4
|
+
import { getEnvironments, getCompanies, getPermissions } from './discovery.js';
|
|
5
|
+
import { saveProfile } from './profiles.js';
|
|
6
|
+
|
|
7
|
+
const BC_API_BASE = 'https://api.businesscentral.dynamics.com/v2.0';
|
|
8
|
+
|
|
9
|
+
export function buildMcpConfig(ctx) {
|
|
10
|
+
const { tenantId, envName, companyId } = ctx;
|
|
11
|
+
return {
|
|
12
|
+
mcpServers: {
|
|
13
|
+
'bc-admin': {
|
|
14
|
+
type: 'stdio',
|
|
15
|
+
command: 'd365bc-admin-mcp',
|
|
16
|
+
env: { BC_TENANT_ID: tenantId },
|
|
17
|
+
},
|
|
18
|
+
'bc-data': {
|
|
19
|
+
type: 'stdio',
|
|
20
|
+
command: 'npx',
|
|
21
|
+
args: ['-y', '@habitusnet/mcp-business-central'],
|
|
22
|
+
env: {
|
|
23
|
+
BC_URL_SERVER: `${BC_API_BASE}/${tenantId}/${envName}/api/v2.0`,
|
|
24
|
+
BC_COMPANY: companyId,
|
|
25
|
+
BC_AUTH_TYPE: 'azure_cli',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function onboard(options = {}) {
|
|
33
|
+
const { tenantId, output = '.mcp.json', profileName } = options;
|
|
34
|
+
const token = await getToken();
|
|
35
|
+
|
|
36
|
+
const environments = await getEnvironments(token, { tenantId, type: 'Production' });
|
|
37
|
+
if (environments.length === 0) throw new Error('No Production environments found.');
|
|
38
|
+
|
|
39
|
+
const { envName } = environments.length === 1
|
|
40
|
+
? { envName: environments[0].name }
|
|
41
|
+
: await inquirer.prompt([{
|
|
42
|
+
type: 'list',
|
|
43
|
+
name: 'envName',
|
|
44
|
+
message: 'Select environment:',
|
|
45
|
+
choices: environments.map((e) => e.name),
|
|
46
|
+
}]);
|
|
47
|
+
|
|
48
|
+
const selectedEnv = environments.find((e) => e.name === envName);
|
|
49
|
+
const companies = await getCompanies(selectedEnv, token);
|
|
50
|
+
if (companies.length === 0) throw new Error('No companies found in this environment.');
|
|
51
|
+
|
|
52
|
+
const { companyId } = companies.length === 1
|
|
53
|
+
? { companyId: companies[0].id }
|
|
54
|
+
: await inquirer.prompt([{
|
|
55
|
+
type: 'list',
|
|
56
|
+
name: 'companyId',
|
|
57
|
+
message: 'Select company:',
|
|
58
|
+
choices: companies.map((c) => ({ name: c.name, value: c.id })),
|
|
59
|
+
}]);
|
|
60
|
+
|
|
61
|
+
const perms = await getPermissions(selectedEnv, token, { companyId });
|
|
62
|
+
if (perms.missing.length > 0) {
|
|
63
|
+
console.warn(`⚠️ Missing permissions: ${perms.missing.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ctx = { tenantId: selectedEnv.aadTenantId, envName, companyId };
|
|
67
|
+
const config = buildMcpConfig(ctx);
|
|
68
|
+
await writeFile(output, JSON.stringify(config, null, 2), 'utf8');
|
|
69
|
+
console.log(`✓ Wrote ${output}`);
|
|
70
|
+
|
|
71
|
+
const name = profileName ?? `${selectedEnv.aadTenantId}/${envName}`;
|
|
72
|
+
await saveProfile(name, ctx);
|
|
73
|
+
console.log(`✓ Saved profile '${name}'`);
|
|
74
|
+
}
|
package/lib/profiles.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
// PROFILE_PATH exported for external reference (computed at import time using real homedir)
|
|
6
|
+
export const PROFILE_PATH = join(homedir(), '.bc365', 'profiles.json');
|
|
7
|
+
|
|
8
|
+
// Lazy path helpers — always call homedir() at runtime so mocks work in tests
|
|
9
|
+
function getProfileDir() {
|
|
10
|
+
return join(homedir(), '.bc365');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getProfilePath() {
|
|
14
|
+
return join(homedir(), '.bc365', 'profiles.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readProfiles() {
|
|
18
|
+
try {
|
|
19
|
+
const data = await readFile(getProfilePath(), 'utf8');
|
|
20
|
+
return JSON.parse(data);
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function writeProfiles(profiles) {
|
|
27
|
+
const dir = getProfileDir();
|
|
28
|
+
await mkdir(dir, { recursive: true });
|
|
29
|
+
await writeFile(getProfilePath(), JSON.stringify(profiles, null, 2), 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function saveProfile(name, config) {
|
|
33
|
+
const profiles = await readProfiles();
|
|
34
|
+
profiles[name] = { ...config, savedAt: new Date().toISOString() };
|
|
35
|
+
await writeProfiles(profiles);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function listProfiles() {
|
|
39
|
+
const profiles = await readProfiles();
|
|
40
|
+
return Object.entries(profiles).map(([name, cfg]) => ({ name, ...cfg }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadProfile(name) {
|
|
44
|
+
const profiles = await readProfiles();
|
|
45
|
+
return profiles[name] ?? null;
|
|
46
|
+
}
|
package/lib/versions.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
2
|
+
|
|
3
|
+
const PACKAGES = [
|
|
4
|
+
'@habitusnet/bc365',
|
|
5
|
+
'@habitusnet/d365bc-admin-mcp',
|
|
6
|
+
'@habitusnet/mcp-business-central',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export async function getLatestVersion(pkg) {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(`${NPM_REGISTRY}/${encodeURIComponent(pkg)}`);
|
|
12
|
+
if (!res.ok) return null;
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
return data['dist-tags']?.latest ?? null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function checkVersions() {
|
|
21
|
+
return Promise.all(
|
|
22
|
+
PACKAGES.map(async (pkg) => ({
|
|
23
|
+
package: pkg,
|
|
24
|
+
latest: await getLatestVersion(pkg),
|
|
25
|
+
}))
|
|
26
|
+
);
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@habitusnet/bc365",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Smart onboarding CLI and MCP config manager for Business Central",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bc365": "./bin/bc365.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/",
|
|
15
|
+
"skills/",
|
|
16
|
+
"commands/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/habitusnet/mcp-d365-BC"
|
|
23
|
+
},
|
|
24
|
+
"engines": { "node": ">=20" },
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
|
|
27
|
+
"lint": "eslint lib/ bin/"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@azure/msal-node": "^2.15.0",
|
|
31
|
+
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"inquirer": "^10.1.5",
|
|
35
|
+
"keytar": "^7.9.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"jest": "^29.7.0"
|
|
39
|
+
}
|
|
40
|
+
}
|