axvault 1.8.2 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commands/credential.js +7 -4
- package/dist/commands/serve.js +2 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.js +26 -1
- package/dist/db/repositories/credentials-queries.d.ts +5 -7
- package/dist/db/repositories/credentials-queries.js +7 -8
- package/dist/db/repositories/credentials.d.ts +2 -0
- package/dist/db/repositories/credentials.js +1 -1
- package/dist/db/repositories/list-credentials-paginated.js +1 -1
- package/dist/db/repositories/parse-credential-row.d.ts +4 -0
- package/dist/db/repositories/parse-credential-row.js +4 -0
- package/dist/db/types.d.ts +5 -1
- package/dist/handlers/create-key.d.ts +10 -0
- package/dist/handlers/create-key.js +63 -0
- package/dist/handlers/delete-key.d.ts +14 -0
- package/dist/handlers/delete-key.js +25 -0
- package/dist/handlers/get-credential.js +7 -1
- package/dist/handlers/get-key.d.ts +14 -0
- package/dist/handlers/get-key.js +20 -0
- package/dist/handlers/list-credentials.js +12 -10
- package/dist/handlers/list-keys.d.ts +10 -0
- package/dist/handlers/list-keys.js +15 -0
- package/dist/handlers/put-credential.js +11 -2
- package/dist/handlers/refresh-credential-on-read.d.ts +2 -0
- package/dist/handlers/refresh-credential-on-read.js +15 -4
- package/dist/handlers/serialize-key.d.ts +20 -0
- package/dist/handlers/serialize-key.js +21 -0
- package/dist/handlers/update-key.d.ts +14 -0
- package/dist/handlers/update-key.js +90 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/require-grant-access.d.ts +9 -0
- package/dist/middleware/require-grant-access.js +16 -0
- package/dist/middleware/validate-key-id.d.ts +9 -0
- package/dist/middleware/validate-key-id.js +19 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +25 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -192,6 +192,8 @@ This command requires `--force` or `--yes` to confirm.
|
|
|
192
192
|
|
|
193
193
|
### Container Deployments
|
|
194
194
|
|
|
195
|
+
Container images are published automatically to `registry.j4k.dev/axvault` on every release (multi-arch: amd64 + arm64). Use `workflow_dispatch` on the `publish-image` workflow to rebuild manually.
|
|
196
|
+
|
|
195
197
|
#### Running the Container
|
|
196
198
|
|
|
197
199
|
The image uses an external UID pattern—no user is baked into the image. **Always specify a non-root user** with `-u`/`--user` to limit container privileges:
|
|
@@ -7,14 +7,16 @@ import { runMigrations } from "../db/migrations.js";
|
|
|
7
7
|
import { deleteCredential, listCredentials, } from "../db/repositories/credentials.js";
|
|
8
8
|
import { containsControlChars, formatRelativeTime, getErrorMessage, sanitizeForTsv, } from "../lib/format.js";
|
|
9
9
|
import { CREDENTIAL_NAME_FORMAT_DESCRIPTION, isValidCredentialName, } from "../lib/credential-name.js";
|
|
10
|
-
/** Print credentials as TSV table
|
|
10
|
+
/** Print credentials as TSV table */
|
|
11
11
|
function printCredentialTable(credentials) {
|
|
12
|
-
console.log("NAME\tCREATED\tUPDATED");
|
|
12
|
+
console.log("NAME\tAGENT\tPROVIDER\tCREATED\tUPDATED");
|
|
13
13
|
for (const cred of credentials) {
|
|
14
14
|
const name = sanitizeForTsv(cred.name);
|
|
15
|
+
const agent = cred.agent || "";
|
|
16
|
+
const provider = cred.provider ?? "";
|
|
15
17
|
const created = formatRelativeTime(cred.createdAt);
|
|
16
18
|
const updated = formatRelativeTime(cred.updatedAt);
|
|
17
|
-
console.log(`${name}\t${created}\t${updated}`);
|
|
19
|
+
console.log(`${name}\t${agent}\t${provider}\t${created}\t${updated}`);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
@@ -53,9 +55,10 @@ export function handleCredentialList(options) {
|
|
|
53
55
|
runMigrations(database);
|
|
54
56
|
const credentials = listCredentials(database);
|
|
55
57
|
if (options.json) {
|
|
56
|
-
// Opaque schema: type/provider/expiresAt not available without decrypting
|
|
57
58
|
const output = credentials.map((cred) => ({
|
|
58
59
|
name: cred.name,
|
|
60
|
+
...(cred.agent !== "" && { agent: cred.agent }),
|
|
61
|
+
...(cred.provider !== undefined && { provider: cred.provider }),
|
|
59
62
|
createdAt: cred.createdAt.toISOString(),
|
|
60
63
|
updatedAt: cred.updatedAt.toISOString(),
|
|
61
64
|
}));
|
package/dist/commands/serve.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { getServerConfig } from "../config.js";
|
|
7
7
|
import { getDatabase, closeDatabase } from "../db/client.js";
|
|
8
8
|
import { runMigrations } from "../db/migrations.js";
|
|
9
|
-
import { createHealthRouter, createCredentialRouter, } from "../server/routes.js";
|
|
9
|
+
import { createHealthRouter, createCredentialRouter, createKeyRouter, } from "../server/routes.js";
|
|
10
10
|
import { createServer } from "../server/server.js";
|
|
11
11
|
export async function handleServe(options) {
|
|
12
12
|
let config;
|
|
@@ -68,6 +68,7 @@ export async function handleServe(options) {
|
|
|
68
68
|
refreshThresholdSeconds: config.refreshThresholdSeconds,
|
|
69
69
|
refreshTimeoutMs: config.refreshTimeoutMs,
|
|
70
70
|
}),
|
|
71
|
+
createKeyRouter(database),
|
|
71
72
|
]);
|
|
72
73
|
// Graceful shutdown handler
|
|
73
74
|
const shutdown = () => {
|
package/dist/db/migrations.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Credentials are identified by name only - the blob contains all metadata.
|
|
6
6
|
*/
|
|
7
7
|
import type Database from "better-sqlite3";
|
|
8
|
-
declare const CURRENT_VERSION =
|
|
8
|
+
declare const CURRENT_VERSION = 2;
|
|
9
9
|
/** Run all pending migrations */
|
|
10
10
|
declare function runMigrations(database: Database.Database): void;
|
|
11
11
|
/** Get current schema version */
|
package/dist/db/migrations.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Simple schema with opaque credential storage.
|
|
5
5
|
* Credentials are identified by name only - the blob contains all metadata.
|
|
6
6
|
*/
|
|
7
|
-
const CURRENT_VERSION =
|
|
7
|
+
const CURRENT_VERSION = 2;
|
|
8
8
|
/** Run all pending migrations */
|
|
9
9
|
function runMigrations(database) {
|
|
10
10
|
let version = getSchemaVersion(database);
|
|
@@ -17,6 +17,11 @@ function runMigrations(database) {
|
|
|
17
17
|
version = 1;
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
|
+
if (version === 1) {
|
|
21
|
+
migrateToV2(database);
|
|
22
|
+
version = 2;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
20
25
|
throw new Error(`Unsupported database schema version v${version} (expected v${CURRENT_VERSION}). Delete the database file to reinitialize.`);
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -88,4 +93,24 @@ function migrateToV1(database) {
|
|
|
88
93
|
setSchemaVersion(database, 1);
|
|
89
94
|
})();
|
|
90
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Migration to version 2: Add agent and provider columns
|
|
98
|
+
*
|
|
99
|
+
* Moves routing metadata (agent, provider) from the encrypted blob to
|
|
100
|
+
* unencrypted columns. This enables refresh-on-read to pass agent/provider
|
|
101
|
+
* to axauth without parsing the blob, and allows future filtering by agent.
|
|
102
|
+
*
|
|
103
|
+
* Existing rows get empty agent (new PUTs populate the column).
|
|
104
|
+
*/
|
|
105
|
+
function migrateToV2(database) {
|
|
106
|
+
database.transaction(() => {
|
|
107
|
+
database.exec(`
|
|
108
|
+
ALTER TABLE credentials ADD COLUMN agent TEXT NOT NULL DEFAULT ''
|
|
109
|
+
`);
|
|
110
|
+
database.exec(`
|
|
111
|
+
ALTER TABLE credentials ADD COLUMN provider TEXT DEFAULT NULL
|
|
112
|
+
`);
|
|
113
|
+
setSchemaVersion(database, 2);
|
|
114
|
+
})();
|
|
115
|
+
}
|
|
91
116
|
export { CURRENT_VERSION, getSchemaVersion, runMigrations };
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQL queries for credentials repository.
|
|
3
|
-
*
|
|
4
|
-
* Name-only primary key, opaque blob storage.
|
|
5
3
|
*/
|
|
6
|
-
export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at\n RETURNING created_at, updated_at";
|
|
4
|
+
export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n agent = excluded.agent, provider = excluded.provider,\n encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at\n RETURNING created_at, updated_at";
|
|
7
5
|
export declare const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = "\n UPDATE credentials\n SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?\n WHERE name = ? AND updated_at = ?";
|
|
8
|
-
export declare const SELECT_CREDENTIAL = "\n SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at\n FROM credentials WHERE name = ?";
|
|
9
|
-
export declare const SELECT_ALL_METADATA = "\n SELECT name, created_at, updated_at\n FROM credentials ORDER BY name";
|
|
10
|
-
export declare const SELECT_METADATA_PAGINATED = "\n SELECT name, created_at, updated_at\n FROM credentials\n WHERE name > ?\n ORDER BY name\n LIMIT ?";
|
|
11
|
-
export declare const SELECT_METADATA_FIRST_PAGE = "\n SELECT name, created_at, updated_at\n FROM credentials\n ORDER BY name\n LIMIT ?";
|
|
6
|
+
export declare const SELECT_CREDENTIAL = "\n SELECT name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at\n FROM credentials WHERE name = ?";
|
|
7
|
+
export declare const SELECT_ALL_METADATA = "\n SELECT name, agent, provider, created_at, updated_at\n FROM credentials ORDER BY name";
|
|
8
|
+
export declare const SELECT_METADATA_PAGINATED = "\n SELECT name, agent, provider, created_at, updated_at\n FROM credentials\n WHERE name > ?\n ORDER BY name\n LIMIT ?";
|
|
9
|
+
export declare const SELECT_METADATA_FIRST_PAGE = "\n SELECT name, agent, provider, created_at, updated_at\n FROM credentials\n ORDER BY name\n LIMIT ?";
|
|
12
10
|
export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE name = ?";
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQL queries for credentials repository.
|
|
3
|
-
*
|
|
4
|
-
* Name-only primary key, opaque blob storage.
|
|
5
3
|
*/
|
|
6
4
|
export const UPSERT_CREDENTIAL = `
|
|
7
|
-
INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)
|
|
8
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
5
|
+
INSERT INTO credentials (name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at)
|
|
6
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9
7
|
ON CONFLICT(name) DO UPDATE SET
|
|
8
|
+
agent = excluded.agent, provider = excluded.provider,
|
|
10
9
|
encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
|
|
11
10
|
updated_at = excluded.updated_at
|
|
12
11
|
RETURNING created_at, updated_at`;
|
|
@@ -15,19 +14,19 @@ export const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = `
|
|
|
15
14
|
SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?
|
|
16
15
|
WHERE name = ? AND updated_at = ?`;
|
|
17
16
|
export const SELECT_CREDENTIAL = `
|
|
18
|
-
SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at
|
|
17
|
+
SELECT name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at
|
|
19
18
|
FROM credentials WHERE name = ?`;
|
|
20
19
|
export const SELECT_ALL_METADATA = `
|
|
21
|
-
SELECT name, created_at, updated_at
|
|
20
|
+
SELECT name, agent, provider, created_at, updated_at
|
|
22
21
|
FROM credentials ORDER BY name`;
|
|
23
22
|
export const SELECT_METADATA_PAGINATED = `
|
|
24
|
-
SELECT name, created_at, updated_at
|
|
23
|
+
SELECT name, agent, provider, created_at, updated_at
|
|
25
24
|
FROM credentials
|
|
26
25
|
WHERE name > ?
|
|
27
26
|
ORDER BY name
|
|
28
27
|
LIMIT ?`;
|
|
29
28
|
export const SELECT_METADATA_FIRST_PAGE = `
|
|
30
|
-
SELECT name, created_at, updated_at
|
|
29
|
+
SELECT name, agent, provider, created_at, updated_at
|
|
31
30
|
FROM credentials
|
|
32
31
|
ORDER BY name
|
|
33
32
|
LIMIT ?`;
|
|
@@ -14,6 +14,8 @@ interface UpsertTimestamps {
|
|
|
14
14
|
/** Store or update a credential, returning the resulting timestamps */
|
|
15
15
|
declare function upsertCredential(database: Database.Database, credential: {
|
|
16
16
|
name: string;
|
|
17
|
+
agent: string;
|
|
18
|
+
provider: string | undefined;
|
|
17
19
|
encryptedData: Buffer;
|
|
18
20
|
salt: Buffer;
|
|
19
21
|
iv: Buffer;
|
|
@@ -11,7 +11,7 @@ function upsertCredential(database, credential) {
|
|
|
11
11
|
const now = Date.now();
|
|
12
12
|
const row = database
|
|
13
13
|
.prepare(SQL.UPSERT_CREDENTIAL)
|
|
14
|
-
.get(credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now);
|
|
14
|
+
.get(credential.name, credential.agent, credential.provider ?? undefined, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now);
|
|
15
15
|
return {
|
|
16
16
|
createdAt: new Date(row.created_at),
|
|
17
17
|
updatedAt: new Date(row.updated_at),
|
|
@@ -16,7 +16,7 @@ function buildFilteredQuery(names, cursor, limit) {
|
|
|
16
16
|
const placeholders = names.map(() => "?").join(", ");
|
|
17
17
|
const cursorClause = cursor ? " AND name > ?" : "";
|
|
18
18
|
const sql = `
|
|
19
|
-
SELECT name, created_at, updated_at
|
|
19
|
+
SELECT name, agent, provider, created_at, updated_at
|
|
20
20
|
FROM credentials
|
|
21
21
|
WHERE name IN (${placeholders})${cursorClause}
|
|
22
22
|
ORDER BY name
|
|
@@ -7,6 +7,8 @@ import type { CredentialRow, MetadataRow } from "../types.js";
|
|
|
7
7
|
/** Credential record stored in database */
|
|
8
8
|
interface CredentialRecord {
|
|
9
9
|
name: string;
|
|
10
|
+
agent: string;
|
|
11
|
+
provider: string | undefined;
|
|
10
12
|
encryptedData: Buffer;
|
|
11
13
|
salt: Buffer;
|
|
12
14
|
iv: Buffer;
|
|
@@ -17,6 +19,8 @@ interface CredentialRecord {
|
|
|
17
19
|
/** Credential metadata (without encrypted data) */
|
|
18
20
|
interface CredentialMetadata {
|
|
19
21
|
name: string;
|
|
22
|
+
agent: string;
|
|
23
|
+
provider: string | undefined;
|
|
20
24
|
createdAt: Date;
|
|
21
25
|
updatedAt: Date;
|
|
22
26
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
function rowToRecord(row) {
|
|
8
8
|
return {
|
|
9
9
|
name: row.name,
|
|
10
|
+
agent: row.agent,
|
|
11
|
+
provider: row.provider ?? undefined,
|
|
10
12
|
encryptedData: row.encrypted_data,
|
|
11
13
|
salt: row.salt,
|
|
12
14
|
iv: row.iv,
|
|
@@ -19,6 +21,8 @@ function rowToRecord(row) {
|
|
|
19
21
|
function rowToMetadata(row) {
|
|
20
22
|
return {
|
|
21
23
|
name: row.name,
|
|
24
|
+
agent: row.agent,
|
|
25
|
+
provider: row.provider ?? undefined,
|
|
22
26
|
createdAt: new Date(row.created_at),
|
|
23
27
|
updatedAt: new Date(row.updated_at),
|
|
24
28
|
};
|
package/dist/db/types.d.ts
CHANGED
|
@@ -23,9 +23,11 @@ export interface AuditLogRow {
|
|
|
23
23
|
success: number;
|
|
24
24
|
error_message: string | null;
|
|
25
25
|
}
|
|
26
|
-
/** Raw credential row from database
|
|
26
|
+
/** Raw credential row from database */
|
|
27
27
|
export interface CredentialRow {
|
|
28
28
|
name: string;
|
|
29
|
+
agent: string;
|
|
30
|
+
provider: string | null;
|
|
29
31
|
encrypted_data: Buffer;
|
|
30
32
|
salt: Buffer;
|
|
31
33
|
iv: Buffer;
|
|
@@ -36,6 +38,8 @@ export interface CredentialRow {
|
|
|
36
38
|
/** Raw credential metadata row from database */
|
|
37
39
|
export interface MetadataRow {
|
|
38
40
|
name: string;
|
|
41
|
+
agent: string;
|
|
42
|
+
provider: string | null;
|
|
39
43
|
created_at: number;
|
|
40
44
|
updated_at: number;
|
|
41
45
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/v1/keys handler.
|
|
3
|
+
*
|
|
4
|
+
* Creates a new API key and returns the one-time secret.
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestHandler } from "express";
|
|
7
|
+
import type Database from "better-sqlite3";
|
|
8
|
+
/** Create a new API key */
|
|
9
|
+
declare function createCreateKeyHandler(database: Database.Database): RequestHandler;
|
|
10
|
+
export { createCreateKeyHandler };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/v1/keys handler.
|
|
3
|
+
*
|
|
4
|
+
* Creates a new API key and returns the one-time secret.
|
|
5
|
+
*/
|
|
6
|
+
import { createApiKey } from "../db/repositories/api-keys.js";
|
|
7
|
+
import { serializeKeyForResponse } from "./serialize-key.js";
|
|
8
|
+
/** Validate that the request body has valid access lists */
|
|
9
|
+
function isValidAccessLists(body) {
|
|
10
|
+
const { readAccess, writeAccess, grantAccess } = body;
|
|
11
|
+
if (!Array.isArray(readAccess) ||
|
|
12
|
+
!readAccess.every((v) => typeof v === "string"))
|
|
13
|
+
return false;
|
|
14
|
+
if (!Array.isArray(writeAccess) ||
|
|
15
|
+
!writeAccess.every((v) => typeof v === "string"))
|
|
16
|
+
return false;
|
|
17
|
+
if (!Array.isArray(grantAccess) ||
|
|
18
|
+
!grantAccess.every((v) => typeof v === "string"))
|
|
19
|
+
return false;
|
|
20
|
+
// At least one access list must be non-empty
|
|
21
|
+
if (readAccess.length === 0 &&
|
|
22
|
+
writeAccess.length === 0 &&
|
|
23
|
+
grantAccess.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
/** Create a new API key */
|
|
28
|
+
function createCreateKeyHandler(database) {
|
|
29
|
+
return (request, response) => {
|
|
30
|
+
const body = request.body;
|
|
31
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
32
|
+
response
|
|
33
|
+
.status(400)
|
|
34
|
+
.json({ error: "Request body must be a JSON object" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const record = body;
|
|
38
|
+
const { name } = record;
|
|
39
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
40
|
+
response
|
|
41
|
+
.status(400)
|
|
42
|
+
.json({ error: "name is required and must be a non-empty string" });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!isValidAccessLists(record)) {
|
|
46
|
+
response.status(400).json({
|
|
47
|
+
error: "readAccess, writeAccess, and grantAccess must be string arrays; at least one must be non-empty",
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const keyWithSecret = createApiKey(database, {
|
|
52
|
+
name: name.trim(),
|
|
53
|
+
readAccess: record.readAccess,
|
|
54
|
+
writeAccess: record.writeAccess,
|
|
55
|
+
grantAccess: record.grantAccess,
|
|
56
|
+
});
|
|
57
|
+
response.status(201).json({
|
|
58
|
+
...serializeKeyForResponse(keyWithSecret),
|
|
59
|
+
key: keyWithSecret.key,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export { createCreateKeyHandler };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DELETE /api/v1/keys/:id handler.
|
|
3
|
+
*
|
|
4
|
+
* Revokes an API key.
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestHandler } from "express";
|
|
7
|
+
import type Database from "better-sqlite3";
|
|
8
|
+
/** Handler type for key routes with id param */
|
|
9
|
+
type KeyHandler = RequestHandler<{
|
|
10
|
+
id: string;
|
|
11
|
+
}, unknown, unknown, unknown>;
|
|
12
|
+
/** Revoke an API key */
|
|
13
|
+
declare function createDeleteKeyHandler(database: Database.Database): KeyHandler;
|
|
14
|
+
export { createDeleteKeyHandler };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DELETE /api/v1/keys/:id handler.
|
|
3
|
+
*
|
|
4
|
+
* Revokes an API key.
|
|
5
|
+
*/
|
|
6
|
+
import { deleteApiKey } from "../db/repositories/api-keys.js";
|
|
7
|
+
/** Revoke an API key */
|
|
8
|
+
function createDeleteKeyHandler(database) {
|
|
9
|
+
return (request, response) => {
|
|
10
|
+
const { apiKey } = request;
|
|
11
|
+
const { id } = request.params;
|
|
12
|
+
// Prevent self-deletion
|
|
13
|
+
if (apiKey.id === id) {
|
|
14
|
+
response.status(403).json({ error: "Cannot revoke your own API key" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const deleted = deleteApiKey(database, id);
|
|
18
|
+
if (!deleted) {
|
|
19
|
+
response.status(404).json({ error: "API key not found" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
response.status(204).end();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export { createDeleteKeyHandler };
|
|
@@ -67,6 +67,8 @@ function createGetCredentialHandler(database, config) {
|
|
|
67
67
|
apiKeyId: apiKey.id,
|
|
68
68
|
name,
|
|
69
69
|
blob,
|
|
70
|
+
agent: credential.agent,
|
|
71
|
+
provider: credential.provider,
|
|
70
72
|
expectedUpdatedAt: credential.updatedAt,
|
|
71
73
|
refreshThresholdSeconds: config.refreshThresholdSeconds,
|
|
72
74
|
refreshTimeoutMs: config.refreshTimeoutMs,
|
|
@@ -94,9 +96,13 @@ function createGetCredentialHandler(database, config) {
|
|
|
94
96
|
if (refreshFailed) {
|
|
95
97
|
response.setHeader("X-Axvault-Refresh-Failed", "true");
|
|
96
98
|
}
|
|
97
|
-
// Return the blob
|
|
99
|
+
// Return the blob along with routing metadata from columns
|
|
98
100
|
response.json({
|
|
99
101
|
name,
|
|
102
|
+
...(credential.agent !== "" && { agent: credential.agent }),
|
|
103
|
+
...(credential.provider !== undefined && {
|
|
104
|
+
provider: credential.provider,
|
|
105
|
+
}),
|
|
100
106
|
credential: finalBlob,
|
|
101
107
|
updatedAt: finalUpdatedAt.toISOString(),
|
|
102
108
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/v1/keys/:id handler.
|
|
3
|
+
*
|
|
4
|
+
* Returns a single API key by ID.
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestHandler } from "express";
|
|
7
|
+
import type Database from "better-sqlite3";
|
|
8
|
+
/** Handler type for key routes with id param */
|
|
9
|
+
type KeyHandler = RequestHandler<{
|
|
10
|
+
id: string;
|
|
11
|
+
}, unknown, unknown, unknown>;
|
|
12
|
+
/** Get a single API key by ID */
|
|
13
|
+
declare function createGetKeyHandler(database: Database.Database): KeyHandler;
|
|
14
|
+
export { createGetKeyHandler };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/v1/keys/:id handler.
|
|
3
|
+
*
|
|
4
|
+
* Returns a single API key by ID.
|
|
5
|
+
*/
|
|
6
|
+
import { findApiKeyById } from "../db/repositories/api-keys.js";
|
|
7
|
+
import { serializeKeyForResponse } from "./serialize-key.js";
|
|
8
|
+
/** Get a single API key by ID */
|
|
9
|
+
function createGetKeyHandler(database) {
|
|
10
|
+
return (request, response) => {
|
|
11
|
+
const { id } = request.params;
|
|
12
|
+
const key = findApiKeyById(database, id);
|
|
13
|
+
if (!key) {
|
|
14
|
+
response.status(404).json({ error: "API key not found" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
response.json(serializeKeyForResponse(key));
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export { createGetKeyHandler };
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { listCredentialsForApiKey, listCredentialsPaginated, } from "../db/repositories/credentials.js";
|
|
12
12
|
import { logAccess } from "../db/repositories/audit-log.js";
|
|
13
|
+
/** Format credential metadata for API response */
|
|
14
|
+
function formatCredentialMetadata(cred) {
|
|
15
|
+
return {
|
|
16
|
+
name: cred.name,
|
|
17
|
+
...(cred.agent !== "" && { agent: cred.agent }),
|
|
18
|
+
...(cred.provider !== undefined && { provider: cred.provider }),
|
|
19
|
+
createdAt: cred.createdAt.toISOString(),
|
|
20
|
+
updatedAt: cred.updatedAt.toISOString(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
13
23
|
/** Maximum page size when limit is specified */
|
|
14
24
|
const MAX_LIMIT = 1000;
|
|
15
25
|
/**
|
|
@@ -61,11 +71,7 @@ function createListCredentialsHandler(database) {
|
|
|
61
71
|
success: true,
|
|
62
72
|
});
|
|
63
73
|
response.json({
|
|
64
|
-
credentials: credentials.map((cred) => (
|
|
65
|
-
name: cred.name,
|
|
66
|
-
createdAt: cred.createdAt.toISOString(),
|
|
67
|
-
updatedAt: cred.updatedAt.toISOString(),
|
|
68
|
-
})),
|
|
74
|
+
credentials: credentials.map((cred) => formatCredentialMetadata(cred)),
|
|
69
75
|
});
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
@@ -92,11 +98,7 @@ function createListCredentialsHandler(database) {
|
|
|
92
98
|
success: true,
|
|
93
99
|
});
|
|
94
100
|
response.json({
|
|
95
|
-
credentials: result.credentials.map((cred) => (
|
|
96
|
-
name: cred.name,
|
|
97
|
-
createdAt: cred.createdAt.toISOString(),
|
|
98
|
-
updatedAt: cred.updatedAt.toISOString(),
|
|
99
|
-
})),
|
|
101
|
+
credentials: result.credentials.map((cred) => formatCredentialMetadata(cred)),
|
|
100
102
|
...(result.nextCursor !== undefined && {
|
|
101
103
|
nextCursor: result.nextCursor,
|
|
102
104
|
}),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/v1/keys handler.
|
|
3
|
+
*
|
|
4
|
+
* Lists all API keys (metadata only, no secrets).
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestHandler } from "express";
|
|
7
|
+
import type Database from "better-sqlite3";
|
|
8
|
+
/** List all API keys */
|
|
9
|
+
declare function createListKeysHandler(database: Database.Database): RequestHandler;
|
|
10
|
+
export { createListKeysHandler };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/v1/keys handler.
|
|
3
|
+
*
|
|
4
|
+
* Lists all API keys (metadata only, no secrets).
|
|
5
|
+
*/
|
|
6
|
+
import { listApiKeys } from "../db/repositories/api-keys.js";
|
|
7
|
+
import { serializeKeyForResponse } from "./serialize-key.js";
|
|
8
|
+
/** List all API keys */
|
|
9
|
+
function createListKeysHandler(database) {
|
|
10
|
+
return (_request, response) => {
|
|
11
|
+
const keys = listApiKeys(database);
|
|
12
|
+
response.json({ keys: keys.map((key) => serializeKeyForResponse(key)) });
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export { createListKeysHandler };
|
|
@@ -48,10 +48,19 @@ function createPutCredentialHandler(database) {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
try {
|
|
51
|
-
//
|
|
52
|
-
const
|
|
51
|
+
// Extract routing metadata from body before encrypting
|
|
52
|
+
const bodyRecord = body;
|
|
53
|
+
const agent = typeof bodyRecord.agent === "string" ? bodyRecord.agent : "";
|
|
54
|
+
const provider = typeof bodyRecord.provider === "string"
|
|
55
|
+
? bodyRecord.provider
|
|
56
|
+
: undefined;
|
|
57
|
+
// Encrypt the credential blob (agent/provider stored as columns, not in blob)
|
|
58
|
+
const credentialBlob = Object.fromEntries(Object.entries(bodyRecord).filter(([key]) => key !== "agent" && key !== "provider"));
|
|
59
|
+
const encrypted = encryptCredential(credentialBlob);
|
|
53
60
|
const timestamps = upsertCredential(database, {
|
|
54
61
|
name,
|
|
62
|
+
agent,
|
|
63
|
+
provider,
|
|
55
64
|
...encrypted,
|
|
56
65
|
});
|
|
57
66
|
logAccess(database, {
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* stays small enough for static complexity checks (FTA).
|
|
6
6
|
*/
|
|
7
7
|
import { isCredentialExpired, isRefreshable, refreshBlob, } from "axauth";
|
|
8
|
-
import { isRefreshableCredentialType, parseCredentials } from "axshared";
|
|
8
|
+
import { isValidAgentCli, isRefreshableCredentialType, parseCredentials } from "axshared";
|
|
9
9
|
import { getCredential, updateCredentialIfUnchanged, } from "../db/repositories/credentials.js";
|
|
10
10
|
import { logAccess } from "../db/repositories/audit-log.js";
|
|
11
11
|
import { decryptCredential, encryptCredential } from "../lib/encryption.js";
|
|
12
12
|
/** Per-credential mutex to prevent concurrent refreshes */
|
|
13
13
|
const pendingRefreshes = new Map();
|
|
14
|
-
function getRefreshPromise(name, blob, refreshTimeoutMs) {
|
|
14
|
+
function getRefreshPromise(name, blob, agent, provider, refreshTimeoutMs) {
|
|
15
15
|
const existing = pendingRefreshes.get(name);
|
|
16
16
|
if (existing)
|
|
17
17
|
return existing;
|
|
18
|
-
const promise = refreshBlob(blob, { timeout: refreshTimeoutMs });
|
|
18
|
+
const promise = refreshBlob(blob, { agent, provider, timeout: refreshTimeoutMs });
|
|
19
19
|
pendingRefreshes.set(name, promise);
|
|
20
20
|
void promise.finally(() => {
|
|
21
21
|
if (pendingRefreshes.get(name) === promise) {
|
|
@@ -34,6 +34,17 @@ async function refreshCredentialOnRead(options) {
|
|
|
34
34
|
refreshFailed: false,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
// Refresh requires a valid agent ID (empty means pre-v2 credential)
|
|
38
|
+
if (!isValidAgentCli(options.agent)) {
|
|
39
|
+
return {
|
|
40
|
+
status: "ok",
|
|
41
|
+
blob: options.blob,
|
|
42
|
+
updatedAt: options.expectedUpdatedAt,
|
|
43
|
+
wasRefreshed: false,
|
|
44
|
+
refreshFailed: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const agent = options.agent;
|
|
37
48
|
const parsedCredentials = parseCredentials(options.blob);
|
|
38
49
|
if (parsedCredentials === undefined ||
|
|
39
50
|
!isRefreshableCredentialType(parsedCredentials.type) ||
|
|
@@ -48,7 +59,7 @@ async function refreshCredentialOnRead(options) {
|
|
|
48
59
|
};
|
|
49
60
|
}
|
|
50
61
|
try {
|
|
51
|
-
const refreshResult = await getRefreshPromise(options.name, options.blob, options.refreshTimeoutMs);
|
|
62
|
+
const refreshResult = await getRefreshPromise(options.name, options.blob, agent, options.provider, options.refreshTimeoutMs);
|
|
52
63
|
if (refreshResult.ok) {
|
|
53
64
|
const encrypted = encryptCredential(refreshResult.blob);
|
|
54
65
|
const updateResult = updateCredentialIfUnchanged(options.database, {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key serialization for HTTP responses.
|
|
3
|
+
*
|
|
4
|
+
* Converts ApiKeyRecord to a safe JSON shape (omits keyHash, converts dates).
|
|
5
|
+
*/
|
|
6
|
+
import type { ApiKeyRecord } from "../db/repositories/api-keys.js";
|
|
7
|
+
/** JSON-safe API key response shape */
|
|
8
|
+
interface SerializedKey {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
keyPrefix: string | null;
|
|
12
|
+
readAccess: string[];
|
|
13
|
+
writeAccess: string[];
|
|
14
|
+
grantAccess: string[];
|
|
15
|
+
createdAt: string;
|
|
16
|
+
lastUsedAt: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Convert an ApiKeyRecord to the JSON response shape */
|
|
19
|
+
declare function serializeKeyForResponse(key: ApiKeyRecord): SerializedKey;
|
|
20
|
+
export { serializeKeyForResponse };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key serialization for HTTP responses.
|
|
3
|
+
*
|
|
4
|
+
* Converts ApiKeyRecord to a safe JSON shape (omits keyHash, converts dates).
|
|
5
|
+
*/
|
|
6
|
+
/** Convert an ApiKeyRecord to the JSON response shape */
|
|
7
|
+
function serializeKeyForResponse(key) {
|
|
8
|
+
return {
|
|
9
|
+
id: key.id,
|
|
10
|
+
name: key.name,
|
|
11
|
+
// eslint-disable-next-line unicorn/no-null -- JSON API requires null for missing values
|
|
12
|
+
keyPrefix: key.keyPrefix ?? null,
|
|
13
|
+
readAccess: key.readAccess,
|
|
14
|
+
writeAccess: key.writeAccess,
|
|
15
|
+
grantAccess: key.grantAccess,
|
|
16
|
+
createdAt: key.createdAt.toISOString(),
|
|
17
|
+
// eslint-disable-next-line unicorn/no-null -- JSON API requires null for missing values
|
|
18
|
+
lastUsedAt: key.lastUsedAt?.toISOString() ?? null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export { serializeKeyForResponse };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PATCH /api/v1/keys/:id handler.
|
|
3
|
+
*
|
|
4
|
+
* Updates an API key's access permissions.
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestHandler } from "express";
|
|
7
|
+
import type Database from "better-sqlite3";
|
|
8
|
+
/** Handler type for key routes with id param */
|
|
9
|
+
type KeyHandler = RequestHandler<{
|
|
10
|
+
id: string;
|
|
11
|
+
}, unknown, unknown, unknown>;
|
|
12
|
+
/** Update an API key's permissions */
|
|
13
|
+
declare function createUpdateKeyHandler(database: Database.Database): KeyHandler;
|
|
14
|
+
export { createUpdateKeyHandler };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PATCH /api/v1/keys/:id handler.
|
|
3
|
+
*
|
|
4
|
+
* Updates an API key's access permissions.
|
|
5
|
+
*/
|
|
6
|
+
import { findApiKeyById, updateApiKeyAccess, } from "../db/repositories/api-keys.js";
|
|
7
|
+
import { serializeKeyForResponse } from "./serialize-key.js";
|
|
8
|
+
/** Check if value is a string array */
|
|
9
|
+
function isStringArray(value) {
|
|
10
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
11
|
+
}
|
|
12
|
+
/** Update an API key's permissions */
|
|
13
|
+
function createUpdateKeyHandler(database) {
|
|
14
|
+
return (request, response) => {
|
|
15
|
+
const { apiKey } = request;
|
|
16
|
+
const { id } = request.params;
|
|
17
|
+
// Prevent self-modification
|
|
18
|
+
if (apiKey.id === id) {
|
|
19
|
+
response.status(403).json({ error: "Cannot modify your own API key" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const body = request.body;
|
|
23
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
24
|
+
response
|
|
25
|
+
.status(400)
|
|
26
|
+
.json({ error: "Request body must be a JSON object" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const record = body;
|
|
30
|
+
const { readAccess, writeAccess, grantAccess } = record;
|
|
31
|
+
// At least one field must be provided
|
|
32
|
+
if (readAccess === undefined &&
|
|
33
|
+
writeAccess === undefined &&
|
|
34
|
+
grantAccess === undefined) {
|
|
35
|
+
response.status(400).json({
|
|
36
|
+
error: "At least one of readAccess, writeAccess, or grantAccess is required",
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Validate types of provided fields
|
|
41
|
+
if (readAccess !== undefined && !isStringArray(readAccess)) {
|
|
42
|
+
response
|
|
43
|
+
.status(400)
|
|
44
|
+
.json({ error: "readAccess must be an array of strings" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (writeAccess !== undefined && !isStringArray(writeAccess)) {
|
|
48
|
+
response
|
|
49
|
+
.status(400)
|
|
50
|
+
.json({ error: "writeAccess must be an array of strings" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (grantAccess !== undefined && !isStringArray(grantAccess)) {
|
|
54
|
+
response
|
|
55
|
+
.status(400)
|
|
56
|
+
.json({ error: "grantAccess must be an array of strings" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Check target key exists before update
|
|
60
|
+
const existingKey = findApiKeyById(database, id);
|
|
61
|
+
if (!existingKey) {
|
|
62
|
+
response.status(404).json({ error: "API key not found" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Compute resulting access lists to validate at least one is non-empty
|
|
66
|
+
const resultReadAccess = readAccess ?? existingKey.readAccess;
|
|
67
|
+
const resultWriteAccess = writeAccess ?? existingKey.writeAccess;
|
|
68
|
+
const resultGrantAccess = grantAccess ?? existingKey.grantAccess;
|
|
69
|
+
if (resultReadAccess.length === 0 &&
|
|
70
|
+
resultWriteAccess.length === 0 &&
|
|
71
|
+
resultGrantAccess.length === 0) {
|
|
72
|
+
response.status(400).json({
|
|
73
|
+
error: "Key must retain at least one non-empty access list",
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
updateApiKeyAccess(database, id, {
|
|
78
|
+
readAccess,
|
|
79
|
+
writeAccess,
|
|
80
|
+
grantAccess,
|
|
81
|
+
});
|
|
82
|
+
const updatedKey = findApiKeyById(database, id);
|
|
83
|
+
if (!updatedKey) {
|
|
84
|
+
response.status(404).json({ error: "API key not found" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
response.json(serializeKeyForResponse(updatedKey));
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export { createUpdateKeyHandler };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,11 +7,11 @@ export type { ServerConfig } from "./config.js";
|
|
|
7
7
|
export { getServerConfig } from "./config.js";
|
|
8
8
|
export type { AxvaultServer } from "./server/server.js";
|
|
9
9
|
export { createServer } from "./server/server.js";
|
|
10
|
-
export { createApiRouter } from "./server/routes.js";
|
|
10
|
+
export { createApiRouter, createKeyRouter } from "./server/routes.js";
|
|
11
11
|
export { closeDatabase, getDatabase, isDatabaseConnected, } from "./db/client.js";
|
|
12
12
|
export { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "./db/migrations.js";
|
|
13
13
|
export type { ApiKeyRecord, ApiKeyWithSecret, } from "./db/repositories/api-keys.js";
|
|
14
|
-
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, } from "./db/repositories/api-keys.js";
|
|
14
|
+
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, } from "./db/repositories/api-keys.js";
|
|
15
15
|
export type { AuditLogEntry } from "./db/repositories/audit-log.js";
|
|
16
16
|
export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs, } from "./db/repositories/audit-log.js";
|
|
17
17
|
export type { CredentialMetadata, CredentialRecord, } from "./db/repositories/credentials.js";
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { getServerConfig } from "./config.js";
|
|
7
7
|
export { createServer } from "./server/server.js";
|
|
8
|
-
export { createApiRouter } from "./server/routes.js";
|
|
8
|
+
export { createApiRouter, createKeyRouter } from "./server/routes.js";
|
|
9
9
|
// Database client
|
|
10
10
|
export { closeDatabase, getDatabase, isDatabaseConnected, } from "./db/client.js";
|
|
11
11
|
export { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "./db/migrations.js";
|
|
12
|
-
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, } from "./db/repositories/api-keys.js";
|
|
12
|
+
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, } from "./db/repositories/api-keys.js";
|
|
13
13
|
export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs, } from "./db/repositories/audit-log.js";
|
|
14
14
|
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, listCredentialsPaginated, upsertCredential, } from "./db/repositories/credentials.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grant access authorization middleware.
|
|
3
|
+
*
|
|
4
|
+
* Ensures the authenticated API key has grant access to all resources ("*").
|
|
5
|
+
*/
|
|
6
|
+
import type { NextFunction, Request, Response } from "express";
|
|
7
|
+
/** Require the caller's API key to have full grant access ("*") */
|
|
8
|
+
declare function requireGrantAccess(request: Request, response: Response, next: NextFunction): void;
|
|
9
|
+
export { requireGrantAccess };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grant access authorization middleware.
|
|
3
|
+
*
|
|
4
|
+
* Ensures the authenticated API key has grant access to all resources ("*").
|
|
5
|
+
*/
|
|
6
|
+
import { hasGrantAccess } from "../db/repositories/api-keys.js";
|
|
7
|
+
/** Require the caller's API key to have full grant access ("*") */
|
|
8
|
+
function requireGrantAccess(request, response, next) {
|
|
9
|
+
const { apiKey } = request;
|
|
10
|
+
if (!hasGrantAccess(apiKey, "*")) {
|
|
11
|
+
response.status(403).json({ error: "Grant access required" });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
}
|
|
16
|
+
export { requireGrantAccess };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key ID parameter validation middleware.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the :id path parameter matches the expected key ID format.
|
|
5
|
+
*/
|
|
6
|
+
import type { NextFunction, Request, Response } from "express";
|
|
7
|
+
/** Validate API key ID path parameter */
|
|
8
|
+
declare function validateKeyId(request: Request, response: Response, next: NextFunction): void;
|
|
9
|
+
export { validateKeyId };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key ID parameter validation middleware.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the :id path parameter matches the expected key ID format.
|
|
5
|
+
*/
|
|
6
|
+
/** Key ID format: "k_" followed by exactly 12 hex characters */
|
|
7
|
+
const KEY_ID_PATTERN = /^k_[\da-f]{12}$/u;
|
|
8
|
+
/** Validate API key ID path parameter */
|
|
9
|
+
function validateKeyId(request, response, next) {
|
|
10
|
+
const { id } = request.params;
|
|
11
|
+
if (id !== undefined && !KEY_ID_PATTERN.test(id)) {
|
|
12
|
+
response.status(400).json({
|
|
13
|
+
error: "Invalid key ID format. Must be 'k_' followed by 12 hex characters.",
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
next();
|
|
18
|
+
}
|
|
19
|
+
export { validateKeyId };
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ import { type GetCredentialConfig } from "../handlers/get-credential.js";
|
|
|
8
8
|
export declare function createHealthRouter(): Router;
|
|
9
9
|
/** Create credential API router (auth required) */
|
|
10
10
|
export declare function createCredentialRouter(database: Database.Database, config: GetCredentialConfig): Router;
|
|
11
|
+
/** Create API key management router (auth + grant access required) */
|
|
12
|
+
export declare function createKeyRouter(database: Database.Database): Router;
|
|
11
13
|
/** Create all API routers (legacy compatibility) */
|
|
12
14
|
export declare function createApiRouter(): Router;
|
package/dist/server/routes.js
CHANGED
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from "express";
|
|
5
5
|
import packageJson from "../../package.json" with { type: "json" };
|
|
6
|
+
import { createCreateKeyHandler } from "../handlers/create-key.js";
|
|
6
7
|
import { createDeleteCredentialHandler } from "../handlers/delete-credential.js";
|
|
8
|
+
import { createDeleteKeyHandler } from "../handlers/delete-key.js";
|
|
7
9
|
import { createGetCredentialHandler, } from "../handlers/get-credential.js";
|
|
10
|
+
import { createGetKeyHandler } from "../handlers/get-key.js";
|
|
8
11
|
import { createListCredentialsHandler } from "../handlers/list-credentials.js";
|
|
12
|
+
import { createListKeysHandler } from "../handlers/list-keys.js";
|
|
9
13
|
import { createPutCredentialHandler } from "../handlers/put-credential.js";
|
|
14
|
+
import { createUpdateKeyHandler } from "../handlers/update-key.js";
|
|
10
15
|
import { createAuthMiddleware } from "../middleware/auth.js";
|
|
16
|
+
import { requireGrantAccess } from "../middleware/require-grant-access.js";
|
|
17
|
+
import { validateKeyId } from "../middleware/validate-key-id.js";
|
|
11
18
|
import { validateParameters } from "../middleware/validate-parameters.js";
|
|
12
19
|
/** Create health check router (no auth required) */
|
|
13
20
|
export function createHealthRouter() {
|
|
@@ -36,6 +43,24 @@ export function createCredentialRouter(database, config) {
|
|
|
36
43
|
router.delete("/api/v1/credentials/:name", validateParameters, createDeleteCredentialHandler(database));
|
|
37
44
|
return router;
|
|
38
45
|
}
|
|
46
|
+
/** Create API key management router (auth + grant access required) */
|
|
47
|
+
export function createKeyRouter(database) {
|
|
48
|
+
const router = Router();
|
|
49
|
+
const authMiddleware = createAuthMiddleware(database);
|
|
50
|
+
// All key routes require authentication and grant access
|
|
51
|
+
router.use("/api/v1/keys", authMiddleware, requireGrantAccess);
|
|
52
|
+
// List all keys
|
|
53
|
+
router.get("/api/v1/keys", createListKeysHandler(database));
|
|
54
|
+
// Create a new key
|
|
55
|
+
router.post("/api/v1/keys", createCreateKeyHandler(database));
|
|
56
|
+
// Get a single key
|
|
57
|
+
router.get("/api/v1/keys/:id", validateKeyId, createGetKeyHandler(database));
|
|
58
|
+
// Update key permissions
|
|
59
|
+
router.patch("/api/v1/keys/:id", validateKeyId, createUpdateKeyHandler(database));
|
|
60
|
+
// Revoke a key
|
|
61
|
+
router.delete("/api/v1/keys/:id", validateKeyId, createDeleteKeyHandler(database));
|
|
62
|
+
return router;
|
|
63
|
+
}
|
|
39
64
|
/** Create all API routers (legacy compatibility) */
|
|
40
65
|
export function createApiRouter() {
|
|
41
66
|
// For backward compatibility, returns just health router
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "axvault",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.9.1",
|
|
6
6
|
"description": "Remote credential storage server for axkit",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@commander-js/extra-typings": "^14.0.0",
|
|
52
|
-
"axauth": "^3.1.
|
|
53
|
-
"axshared": "
|
|
52
|
+
"axauth": "^3.1.4",
|
|
53
|
+
"axshared": "5.0.0",
|
|
54
54
|
"better-sqlite3": "^12.6.2",
|
|
55
55
|
"commander": "^14.0.2",
|
|
56
56
|
"express": "^5.2.1"
|