axvault 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/LICENSE +21 -0
- package/README.md +213 -0
- package/bin/axvault +2 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +108 -0
- package/dist/commands/credential.d.ts +13 -0
- package/dist/commands/credential.js +113 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/key.d.ts +21 -0
- package/dist/commands/key.js +157 -0
- package/dist/commands/serve.d.ts +12 -0
- package/dist/commands/serve.js +93 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +44 -0
- package/dist/db/client.d.ts +13 -0
- package/dist/db/client.js +38 -0
- package/dist/db/migrations.d.ts +12 -0
- package/dist/db/migrations.js +96 -0
- package/dist/db/repositories/api-keys.d.ts +42 -0
- package/dist/db/repositories/api-keys.js +86 -0
- package/dist/db/repositories/audit-log.d.ts +37 -0
- package/dist/db/repositories/audit-log.js +58 -0
- package/dist/db/repositories/credentials.d.ts +48 -0
- package/dist/db/repositories/credentials.js +79 -0
- package/dist/db/types.d.ts +44 -0
- package/dist/db/types.js +4 -0
- package/dist/handlers/delete-credential.d.ts +15 -0
- package/dist/handlers/delete-credential.js +50 -0
- package/dist/handlers/get-credential.d.ts +21 -0
- package/dist/handlers/get-credential.js +143 -0
- package/dist/handlers/list-credentials.d.ts +12 -0
- package/dist/handlers/list-credentials.js +29 -0
- package/dist/handlers/put-credential.d.ts +15 -0
- package/dist/handlers/put-credential.js +94 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +14 -0
- package/dist/lib/encryption.d.ts +17 -0
- package/dist/lib/encryption.js +38 -0
- package/dist/lib/format.d.ts +92 -0
- package/dist/lib/format.js +216 -0
- package/dist/middleware/auth.d.ts +21 -0
- package/dist/middleware/auth.js +50 -0
- package/dist/middleware/validate-parameters.d.ts +10 -0
- package/dist/middleware/validate-parameters.js +26 -0
- package/dist/refresh/check-refresh.d.ts +40 -0
- package/dist/refresh/check-refresh.js +83 -0
- package/dist/refresh/log-refresh.d.ts +17 -0
- package/dist/refresh/log-refresh.js +35 -0
- package/dist/refresh/refresh-manager.d.ts +51 -0
- package/dist/refresh/refresh-manager.js +132 -0
- package/dist/server/routes.d.ts +12 -0
- package/dist/server/routes.js +44 -0
- package/dist/server/server.d.ts +18 -0
- package/dist/server/server.js +106 -0
- package/package.json +93 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh manager with per-credential mutex locking.
|
|
3
|
+
*
|
|
4
|
+
* Prevents concurrent refreshes of the same credential and coordinates
|
|
5
|
+
* with axauth's refresh functionality.
|
|
6
|
+
*/
|
|
7
|
+
import { refreshCredentials } from "axauth";
|
|
8
|
+
import { getCredential, upsertCredential, } from "../db/repositories/credentials.js";
|
|
9
|
+
import { encryptCredential } from "../lib/encryption.js";
|
|
10
|
+
import { extractExpiryDate, toAxauthCredentials } from "./check-refresh.js";
|
|
11
|
+
import { getErrorMessage, logRefreshError, logRefreshSuccess, } from "./log-refresh.js";
|
|
12
|
+
/** Map of in-progress refreshes to prevent concurrent operations */
|
|
13
|
+
const pendingRefreshes = new Map();
|
|
14
|
+
/** Build credential key for mutex */
|
|
15
|
+
function buildKey(agent, name) {
|
|
16
|
+
return `${agent}/${name}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Execute the full refresh operation: call axauth, validate, persist.
|
|
20
|
+
* This is the inner operation that gets stored in the mutex.
|
|
21
|
+
*/
|
|
22
|
+
async function executeRefresh(database, agent, name, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options) {
|
|
23
|
+
const logContext = { database, apiKeyId, agent, name };
|
|
24
|
+
// Map to axauth format
|
|
25
|
+
const creds = toAxauthCredentials(agent, data);
|
|
26
|
+
if (!creds) {
|
|
27
|
+
return { ok: false, error: `Unknown agent: ${agent}` };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = await refreshCredentials(creds, {
|
|
31
|
+
timeout: options.timeoutMs,
|
|
32
|
+
});
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
logRefreshError(logContext, result.error);
|
|
35
|
+
return { ok: false, error: result.error };
|
|
36
|
+
}
|
|
37
|
+
// Check if credential still exists and wasn't modified during refresh
|
|
38
|
+
const existingCredential = getCredential(database, agent, name);
|
|
39
|
+
if (!existingCredential) {
|
|
40
|
+
logRefreshError(logContext, "Credential was deleted during refresh");
|
|
41
|
+
return { ok: false, error: "Credential was deleted during refresh" };
|
|
42
|
+
}
|
|
43
|
+
// Optimistic locking: abort if credential was modified during refresh
|
|
44
|
+
if (existingCredential.updatedAt.getTime() !== originalUpdatedAt.getTime()) {
|
|
45
|
+
logRefreshError(logContext, "Credential was modified during refresh");
|
|
46
|
+
return { ok: false, error: "Credential was modified during refresh" };
|
|
47
|
+
}
|
|
48
|
+
// Persist newly refreshed credentials to database
|
|
49
|
+
// Merge original data with refreshed data to preserve fields like refresh_token
|
|
50
|
+
const newData = { ...data, ...result.credentials.data };
|
|
51
|
+
const encrypted = encryptCredential(newData);
|
|
52
|
+
const expiresAt = extractExpiryDate(newData);
|
|
53
|
+
try {
|
|
54
|
+
upsertCredential(database, {
|
|
55
|
+
agent,
|
|
56
|
+
name,
|
|
57
|
+
...encrypted,
|
|
58
|
+
expiresAt,
|
|
59
|
+
});
|
|
60
|
+
logRefreshSuccess(logContext);
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
data: newData,
|
|
64
|
+
expiresAt,
|
|
65
|
+
updatedAt: refreshStartedAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = getErrorMessage(error);
|
|
70
|
+
logRefreshError(logContext, `Persistence failed: ${message}`);
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `Failed to persist refreshed credentials: ${message}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Handle unexpected errors (e.g., refreshCredentials promise rejection)
|
|
79
|
+
const message = getErrorMessage(error);
|
|
80
|
+
logRefreshError(logContext, `Refresh error: ${message}`);
|
|
81
|
+
return { ok: false, error: `Refresh failed: ${message}` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Perform refresh with mutex locking.
|
|
86
|
+
*
|
|
87
|
+
* If a refresh is already in progress for this credential, waits for it
|
|
88
|
+
* rather than starting a new one. The mutex covers the FULL operation
|
|
89
|
+
* (refresh + validate + persist), ensuring all callers see the same result.
|
|
90
|
+
*
|
|
91
|
+
* Note: There is a small race window where a request that loaded stale
|
|
92
|
+
* credential data could start a duplicate refresh if it checks the mutex
|
|
93
|
+
* right after the previous refresh completes. This is rare (requires precise
|
|
94
|
+
* timing), harmless (produces correct results), and self-limiting (OAuth
|
|
95
|
+
* providers handle duplicate refreshes). The complexity of cooldown timers
|
|
96
|
+
* or reference counting isn't justified for this edge case.
|
|
97
|
+
*
|
|
98
|
+
* @param database - Database connection
|
|
99
|
+
* @param agent - Agent name
|
|
100
|
+
* @param name - Credential name
|
|
101
|
+
* @param data - Current decrypted credential data
|
|
102
|
+
* @param apiKeyId - API key ID for audit logging
|
|
103
|
+
* @param originalUpdatedAt - Original updatedAt for optimistic locking
|
|
104
|
+
* @param options - Refresh options
|
|
105
|
+
* @returns Refresh result with new data, expiresAt, updatedAt or error
|
|
106
|
+
*/
|
|
107
|
+
async function refreshWithMutex(database, agent, name, data, apiKeyId, originalUpdatedAt, options) {
|
|
108
|
+
const key = buildKey(agent, name);
|
|
109
|
+
// Check if refresh already in progress
|
|
110
|
+
const existing = pendingRefreshes.get(key);
|
|
111
|
+
if (existing) {
|
|
112
|
+
// Wait for existing FULL operation to complete
|
|
113
|
+
// All callers observe the same final success/failure result
|
|
114
|
+
return existing.promise;
|
|
115
|
+
}
|
|
116
|
+
// Start new refresh - store promise for the FULL operation
|
|
117
|
+
const refreshStartedAt = new Date();
|
|
118
|
+
const fullOperationPromise = executeRefresh(database, agent, name, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options);
|
|
119
|
+
pendingRefreshes.set(key, {
|
|
120
|
+
promise: fullOperationPromise,
|
|
121
|
+
startedAt: refreshStartedAt,
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
return await fullOperationPromise;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
// Always clean up mutex
|
|
128
|
+
pendingRefreshes.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export { extractExpiryDate, isRefreshable, needsRefresh, toAxauthCredentials, } from "./check-refresh.js";
|
|
132
|
+
export { buildKey, refreshWithMutex };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API route definitions using Express Router.
|
|
3
|
+
*/
|
|
4
|
+
import type Database from "better-sqlite3";
|
|
5
|
+
import { Router } from "express";
|
|
6
|
+
import { type GetCredentialConfig } from "../handlers/get-credential.js";
|
|
7
|
+
/** Create health check router (no auth required) */
|
|
8
|
+
export declare function createHealthRouter(): Router;
|
|
9
|
+
/** Create credential API router (auth required) */
|
|
10
|
+
export declare function createCredentialRouter(database: Database.Database, config: GetCredentialConfig): Router;
|
|
11
|
+
/** Create all API routers (legacy compatibility) */
|
|
12
|
+
export declare function createApiRouter(): Router;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API route definitions using Express Router.
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from "express";
|
|
5
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
6
|
+
import { createDeleteCredentialHandler } from "../handlers/delete-credential.js";
|
|
7
|
+
import { createGetCredentialHandler, } from "../handlers/get-credential.js";
|
|
8
|
+
import { createListCredentialsHandler } from "../handlers/list-credentials.js";
|
|
9
|
+
import { createPutCredentialHandler } from "../handlers/put-credential.js";
|
|
10
|
+
import { createAuthMiddleware } from "../middleware/auth.js";
|
|
11
|
+
import { validateParameters } from "../middleware/validate-parameters.js";
|
|
12
|
+
/** Create health check router (no auth required) */
|
|
13
|
+
export function createHealthRouter() {
|
|
14
|
+
const router = Router();
|
|
15
|
+
router.get("/api/v1/health", (_request, response) => {
|
|
16
|
+
response.json({
|
|
17
|
+
status: "ok",
|
|
18
|
+
version: packageJson.version,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return router;
|
|
22
|
+
}
|
|
23
|
+
/** Create credential API router (auth required) */
|
|
24
|
+
export function createCredentialRouter(database, config) {
|
|
25
|
+
const router = Router();
|
|
26
|
+
const authMiddleware = createAuthMiddleware(database);
|
|
27
|
+
// All credential routes require authentication
|
|
28
|
+
router.use("/api/v1/credentials", authMiddleware);
|
|
29
|
+
// List all accessible credentials
|
|
30
|
+
router.get("/api/v1/credentials", createListCredentialsHandler(database));
|
|
31
|
+
// Get a specific credential (with automatic refresh)
|
|
32
|
+
router.get("/api/v1/credentials/:agent/:name", validateParameters, createGetCredentialHandler(database, config));
|
|
33
|
+
// Store/update a credential
|
|
34
|
+
router.put("/api/v1/credentials/:agent/:name", validateParameters, createPutCredentialHandler(database));
|
|
35
|
+
// Delete a credential
|
|
36
|
+
router.delete("/api/v1/credentials/:agent/:name", validateParameters, createDeleteCredentialHandler(database));
|
|
37
|
+
return router;
|
|
38
|
+
}
|
|
39
|
+
/** Create all API routers (legacy compatibility) */
|
|
40
|
+
export function createApiRouter() {
|
|
41
|
+
// For backward compatibility, returns just health router
|
|
42
|
+
// Use createHealthRouter() and createCredentialRouter(db, config) directly for full API
|
|
43
|
+
return createHealthRouter();
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express HTTP server for axvault API.
|
|
3
|
+
*
|
|
4
|
+
* Designed for both CLI and library usage via factory pattern.
|
|
5
|
+
*/
|
|
6
|
+
import { type Express, type Router } from "express";
|
|
7
|
+
import type { ServerConfig } from "../config.js";
|
|
8
|
+
/** Server instance with lifecycle methods */
|
|
9
|
+
export interface AxvaultServer {
|
|
10
|
+
/** The Express application instance */
|
|
11
|
+
readonly app: Express;
|
|
12
|
+
/** Start listening on the configured host:port */
|
|
13
|
+
start(): Promise<void>;
|
|
14
|
+
/** Stop the server gracefully */
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Create an axvault server instance */
|
|
18
|
+
export declare function createServer(config: ServerConfig, routers: Router[]): AxvaultServer;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express HTTP server for axvault API.
|
|
3
|
+
*
|
|
4
|
+
* Designed for both CLI and library usage via factory pattern.
|
|
5
|
+
*/
|
|
6
|
+
import express, {} from "express";
|
|
7
|
+
/** Create and configure Express application */
|
|
8
|
+
function createApp(routers) {
|
|
9
|
+
const app = express();
|
|
10
|
+
// Security: disable X-Powered-By header
|
|
11
|
+
app.disable("x-powered-by");
|
|
12
|
+
// JSON body parsing middleware with size limit
|
|
13
|
+
app.use(express.json({ limit: "1mb" }));
|
|
14
|
+
// Register all routers
|
|
15
|
+
for (const router of routers) {
|
|
16
|
+
app.use(router);
|
|
17
|
+
}
|
|
18
|
+
// 404 handler for unmatched routes
|
|
19
|
+
app.use((_request, response) => {
|
|
20
|
+
response.status(404).json({ error: "Not found" });
|
|
21
|
+
});
|
|
22
|
+
// Error handling middleware - must be last and have 4 parameters
|
|
23
|
+
app.use((error, _request, response,
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Express requires 4 params
|
|
25
|
+
_next) => {
|
|
26
|
+
// Don't double-respond if headers already sent
|
|
27
|
+
if (response.headersSent) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Handle JSON parse errors
|
|
31
|
+
if (error instanceof SyntaxError && "body" in error) {
|
|
32
|
+
response.status(400).json({ error: "Invalid JSON" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Extract status from middleware errors (e.g., body-parser 413)
|
|
36
|
+
// Guard against null/primitive errors
|
|
37
|
+
const isErrorObject = error !== null && typeof error === "object";
|
|
38
|
+
const errorWithStatus = error;
|
|
39
|
+
const status = isErrorObject
|
|
40
|
+
? (errorWithStatus.status ?? errorWithStatus.statusCode ?? 500)
|
|
41
|
+
: 500;
|
|
42
|
+
// Log server errors
|
|
43
|
+
if (status >= 500) {
|
|
44
|
+
console.error("Server error:", error);
|
|
45
|
+
}
|
|
46
|
+
// Use generic message for 5xx, actual message for 4xx
|
|
47
|
+
const message = status >= 500
|
|
48
|
+
? "Internal server error"
|
|
49
|
+
: ((isErrorObject ? errorWithStatus.message : undefined) ??
|
|
50
|
+
"Request error");
|
|
51
|
+
response.status(status).json({ error: message });
|
|
52
|
+
});
|
|
53
|
+
return app;
|
|
54
|
+
}
|
|
55
|
+
/** Create an axvault server instance */
|
|
56
|
+
export function createServer(config, routers) {
|
|
57
|
+
const app = createApp(routers);
|
|
58
|
+
let server;
|
|
59
|
+
let stopPromise;
|
|
60
|
+
return {
|
|
61
|
+
app,
|
|
62
|
+
start() {
|
|
63
|
+
// Reset state for restart support
|
|
64
|
+
stopPromise = undefined;
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
// Startup error handler - removed after successful listen
|
|
67
|
+
const onStartupError = (error) => {
|
|
68
|
+
reject(error);
|
|
69
|
+
};
|
|
70
|
+
server = app.listen(config.port, config.host, () => {
|
|
71
|
+
server?.removeListener("error", onStartupError);
|
|
72
|
+
// Add persistent error handler only after startup succeeds
|
|
73
|
+
server?.on("error", (error) => {
|
|
74
|
+
console.error("Server error:", error);
|
|
75
|
+
});
|
|
76
|
+
console.error(`axvault listening on http://${config.host}:${config.port}`);
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
server.once("error", onStartupError);
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
stop() {
|
|
83
|
+
// Return memoized promise for idempotency
|
|
84
|
+
if (stopPromise) {
|
|
85
|
+
return stopPromise;
|
|
86
|
+
}
|
|
87
|
+
if (!server) {
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
}
|
|
90
|
+
// Capture reference before async operation
|
|
91
|
+
const serverToClose = server;
|
|
92
|
+
server = undefined;
|
|
93
|
+
stopPromise = new Promise((resolve, reject) => {
|
|
94
|
+
serverToClose.close((error) => {
|
|
95
|
+
if (error) {
|
|
96
|
+
reject(error);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
resolve();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return stopPromise;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "axvault",
|
|
3
|
+
"author": "Łukasz Jerciński",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Remote credential storage server for axpoint",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Jercik/axvault.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Jercik/axvault#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Jercik/axvault/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"axvault": "bin/axvault"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin/",
|
|
29
|
+
"dist/",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"prepare": "git config core.hooksPath .githooks",
|
|
35
|
+
"prepublishOnly": "pnpm run rebuild",
|
|
36
|
+
"build": "tsc -p tsconfig.app.json",
|
|
37
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
38
|
+
"format": "prettier --write .",
|
|
39
|
+
"format:check": "prettier --check .",
|
|
40
|
+
"fta": "fta-check",
|
|
41
|
+
"knip": "knip",
|
|
42
|
+
"lint": "eslint",
|
|
43
|
+
"rebuild": "pnpm run clean && pnpm run build",
|
|
44
|
+
"start": "pnpm -s run rebuild && node bin/axvault",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:coverage": "vitest run --coverage",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"typecheck": "tsc -b --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
52
|
+
"axauth": "^1.7.2",
|
|
53
|
+
"axshared": "^1.8.0",
|
|
54
|
+
"better-sqlite3": "^12.5.0",
|
|
55
|
+
"commander": "^14.0.2",
|
|
56
|
+
"express": "^5.2.1"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"ai",
|
|
60
|
+
"agent",
|
|
61
|
+
"cli",
|
|
62
|
+
"auth",
|
|
63
|
+
"authentication",
|
|
64
|
+
"credentials",
|
|
65
|
+
"claude-code",
|
|
66
|
+
"codex",
|
|
67
|
+
"gemini",
|
|
68
|
+
"opencode",
|
|
69
|
+
"llm",
|
|
70
|
+
"automation",
|
|
71
|
+
"coding-assistant"
|
|
72
|
+
],
|
|
73
|
+
"packageManager": "pnpm@10.27.0",
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=22.14.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@total-typescript/ts-reset": "^0.6.1",
|
|
79
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
80
|
+
"@types/express": "^5.0.6",
|
|
81
|
+
"@types/node": "^25.0.3",
|
|
82
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
83
|
+
"eslint": "^9.39.2",
|
|
84
|
+
"eslint-config-axpoint": "^1.0.0",
|
|
85
|
+
"fta-check": "^1.5.1",
|
|
86
|
+
"fta-cli": "^3.0.0",
|
|
87
|
+
"knip": "^5.80.0",
|
|
88
|
+
"prettier": "3.7.4",
|
|
89
|
+
"semantic-release": "^25.0.2",
|
|
90
|
+
"typescript": "^5.9.3",
|
|
91
|
+
"vitest": "^4.0.16"
|
|
92
|
+
}
|
|
93
|
+
}
|