axvault 1.0.0 → 1.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/README.md CHANGED
@@ -163,11 +163,14 @@ curl -X PUT https://vault.example.com/api/v1/credentials/claude/prod \
163
163
  -H "Authorization: Bearer <api_key>" \
164
164
  -H "Content-Type: application/json" \
165
165
  -d '{
166
- "data": {"accessToken": "...", "refreshToken": "..."},
166
+ "type": "oauth",
167
+ "data": {"access_token": "...", "refresh_token": "..."},
167
168
  "expiresAt": "2025-12-31T23:59:59Z"
168
169
  }'
169
170
  ```
170
171
 
172
+ The `type` field is required and must be either `"oauth"` (for refreshable tokens) or `"api-key"` (for static keys). Only `oauth` credentials are eligible for auto-refresh.
173
+
171
174
  ### Retrieve a Credential
172
175
 
173
176
  ```bash
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * axvault - Remote credential storage server for axpoint.
3
+ * axvault - Remote credential storage server for axkit.
4
4
  *
5
5
  * Stores agent credentials and serves them via API.
6
6
  */
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * axvault - Remote credential storage server for axpoint.
3
+ * axvault - Remote credential storage server for axkit.
4
4
  *
5
5
  * Stores agent credentials and serves them via API.
6
6
  */
@@ -8,13 +8,14 @@ import { deleteCredential, listCredentials, } from "../db/repositories/credentia
8
8
  import { containsControlChars, formatDateForJson, formatExpiresAt, formatRelativeTime, getErrorMessage, sanitizeForTsv, } from "../lib/format.js";
9
9
  /** Print credentials as TSV table */
10
10
  function printCredentialTable(credentials) {
11
- console.log("AGENT\tNAME\tEXPIRES\tUPDATED");
11
+ console.log("AGENT\tNAME\tTYPE\tEXPIRES\tUPDATED");
12
12
  for (const cred of credentials) {
13
13
  const agent = sanitizeForTsv(cred.agent);
14
14
  const name = sanitizeForTsv(cred.name);
15
+ const type = sanitizeForTsv(cred.type);
15
16
  const expires = formatExpiresAt(cred.expiresAt);
16
17
  const updated = formatRelativeTime(cred.updatedAt);
17
- console.log(`${agent}\t${name}\t${expires}\t${updated}`);
18
+ console.log(`${agent}\t${name}\t${type}\t${expires}\t${updated}`);
18
19
  }
19
20
  }
20
21
  /**
@@ -59,6 +60,7 @@ export function handleCredentialList(options) {
59
60
  const output = credentials.map((cred) => ({
60
61
  agent: cred.agent,
61
62
  name: cred.name,
63
+ type: cred.type,
62
64
  createdAt: cred.createdAt.toISOString(),
63
65
  updatedAt: cred.updatedAt.toISOString(),
64
66
  expiresAt: formatDateForJson(cred.expiresAt),
@@ -4,7 +4,7 @@
4
4
  * Uses a simple version-based migration system.
5
5
  */
6
6
  import type Database from "better-sqlite3";
7
- declare const CURRENT_VERSION = 2;
7
+ declare const CURRENT_VERSION = 3;
8
8
  /** Run all pending migrations */
9
9
  declare function runMigrations(database: Database.Database): void;
10
10
  /** Get current schema version */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Uses a simple version-based migration system.
5
5
  */
6
- const CURRENT_VERSION = 2;
6
+ const CURRENT_VERSION = 3;
7
7
  /** Run all pending migrations */
8
8
  function runMigrations(database) {
9
9
  const version = getSchemaVersion(database);
@@ -13,6 +13,9 @@ function runMigrations(database) {
13
13
  if (version < 2) {
14
14
  migrateToV2(database);
15
15
  }
16
+ if (version < 3) {
17
+ migrateToV3(database);
18
+ }
16
19
  }
17
20
  /** Get current schema version */
18
21
  function getSchemaVersion(database) {
@@ -93,4 +96,18 @@ function migrateToV2(database) {
93
96
  setSchemaVersion(database, 2);
94
97
  })();
95
98
  }
99
+ /**
100
+ * Migration to version 3: Add type column to credentials
101
+ *
102
+ * Explicit credential type ("oauth" or "api-key") instead of inferring from data.
103
+ * Existing credentials without type must be re-uploaded.
104
+ */
105
+ function migrateToV3(database) {
106
+ database.transaction(() => {
107
+ database.exec(`
108
+ ALTER TABLE credentials ADD COLUMN type TEXT
109
+ `);
110
+ setSchemaVersion(database, 3);
111
+ })();
112
+ }
96
113
  export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -4,10 +4,13 @@
4
4
  * Manages encrypted credential storage.
5
5
  */
6
6
  import type Database from "better-sqlite3";
7
+ /** Valid credential types */
8
+ type CredentialType = "oauth" | "api-key";
7
9
  /** Credential record stored in database */
8
10
  interface CredentialRecord {
9
11
  agent: string;
10
12
  name: string;
13
+ type: CredentialType;
11
14
  encryptedData: Buffer;
12
15
  salt: Buffer;
13
16
  iv: Buffer;
@@ -20,6 +23,7 @@ interface CredentialRecord {
20
23
  interface CredentialMetadata {
21
24
  agent: string;
22
25
  name: string;
26
+ type: CredentialType;
23
27
  createdAt: Date;
24
28
  updatedAt: Date;
25
29
  expiresAt: Date | undefined;
@@ -28,6 +32,7 @@ interface CredentialMetadata {
28
32
  declare function upsertCredential(database: Database.Database, credential: {
29
33
  agent: string;
30
34
  name: string;
35
+ type: CredentialType;
31
36
  encryptedData: Buffer;
32
37
  salt: Buffer;
33
38
  iv: Buffer;
@@ -45,4 +50,4 @@ declare function deleteCredential(database: Database.Database, agent: string, na
45
50
  /** Update expiration time after refresh */
46
51
  declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
47
52
  export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
48
- export type { CredentialMetadata, CredentialRecord };
53
+ export type { CredentialMetadata, CredentialRecord, CredentialType };
@@ -3,14 +3,19 @@
3
3
  *
4
4
  * Manages encrypted credential storage.
5
5
  */
6
+ /** Validate credential type from database */
7
+ function validateType(type) {
8
+ if (type !== "oauth" && type !== "api-key") {
9
+ throw new Error(`Invalid credential type: ${type}`);
10
+ }
11
+ return type;
12
+ }
6
13
  /** Convert database row to record */
7
14
  function rowToRecord(row) {
8
- if (!row.salt) {
9
- throw new Error("Credential missing salt - database may need migration");
10
- }
11
15
  return {
12
16
  agent: row.agent,
13
17
  name: row.name,
18
+ type: validateType(row.type),
14
19
  encryptedData: row.encrypted_data,
15
20
  salt: row.salt,
16
21
  iv: row.iv,
@@ -25,6 +30,7 @@ function rowToMetadata(row) {
25
30
  return {
26
31
  agent: row.agent,
27
32
  name: row.name,
33
+ type: validateType(row.type),
28
34
  createdAt: new Date(row.created_at),
29
35
  updatedAt: new Date(row.updated_at),
30
36
  expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
@@ -35,25 +41,25 @@ function upsertCredential(database, credential) {
35
41
  const now = Date.now();
36
42
  /* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
37
43
  database
38
- .prepare(`INSERT INTO credentials (agent, name, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
39
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
44
+ .prepare(`INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
45
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
40
46
  ON CONFLICT(agent, name) DO UPDATE SET
41
- encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
47
+ type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
42
48
  updated_at = excluded.updated_at, expires_at = excluded.expires_at`)
43
- .run(credential.agent, credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
49
+ .run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
44
50
  /* eslint-enable unicorn/no-null */
45
51
  }
46
52
  /** Get a credential by agent and name */
47
53
  function getCredential(database, agent, name) {
48
54
  const row = database
49
- .prepare(`SELECT agent, name, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
55
+ .prepare(`SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
50
56
  .get(agent, name);
51
57
  return row ? rowToRecord(row) : undefined;
52
58
  }
53
59
  /** List all credentials (metadata only) */
54
60
  function listCredentials(database) {
55
61
  const rows = database
56
- .prepare(`SELECT agent, name, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
62
+ .prepare(`SELECT agent, name, type, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
57
63
  .all();
58
64
  return rows.map((row) => rowToMetadata(row));
59
65
  }
@@ -26,8 +26,9 @@ export interface AuditLogRow {
26
26
  export interface CredentialRow {
27
27
  agent: string;
28
28
  name: string;
29
+ type: string;
29
30
  encrypted_data: Buffer;
30
- salt: Buffer | null;
31
+ salt: Buffer;
31
32
  iv: Buffer;
32
33
  auth_tag: Buffer;
33
34
  created_at: number;
@@ -38,6 +39,7 @@ export interface CredentialRow {
38
39
  export interface MetadataRow {
39
40
  agent: string;
40
41
  name: string;
42
+ type: string;
41
43
  created_at: number;
42
44
  updated_at: number;
43
45
  expires_at: number | null;
@@ -30,28 +30,7 @@ function createGetCredentialHandler(database, config) {
30
30
  return;
31
31
  }
32
32
  // Fetch credential from database
33
- let credential;
34
- try {
35
- credential = getCredential(database, agent, name);
36
- }
37
- catch (error) {
38
- // Handle legacy credentials stored without salt (pre-v2 migration)
39
- if (error instanceof Error && error.message.includes("missing salt")) {
40
- logAccess(database, {
41
- apiKeyId: apiKey.id,
42
- action: "read",
43
- agent,
44
- name,
45
- success: false,
46
- errorMessage: "Legacy credential requires re-upload",
47
- });
48
- response.status(410).json({
49
- error: "This credential was stored with legacy encryption and must be deleted and re-uploaded",
50
- });
51
- return;
52
- }
53
- throw error;
54
- }
33
+ const credential = getCredential(database, agent, name);
55
34
  if (!credential) {
56
35
  logAccess(database, {
57
36
  apiKeyId: apiKey.id,
@@ -90,10 +69,11 @@ function createGetCredentialHandler(database, config) {
90
69
  let wasRefreshed = false;
91
70
  let refreshFailed = false;
92
71
  if (config.refreshThresholdSeconds > 0 &&
72
+ credential.type === "oauth" &&
93
73
  isRefreshable(data) &&
94
74
  needsRefresh(data, config.refreshThresholdSeconds)) {
95
75
  try {
96
- const refreshResult = await refreshWithMutex(database, agent, name, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
76
+ const refreshResult = await refreshWithMutex(database, agent, name, credential.type, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
97
77
  if (refreshResult.ok) {
98
78
  finalData = refreshResult.data;
99
79
  finalExpiresAt = refreshResult.expiresAt;
@@ -101,8 +81,30 @@ function createGetCredentialHandler(database, config) {
101
81
  wasRefreshed = true;
102
82
  }
103
83
  else {
104
- // Refresh failed - will return stale credentials with warning header
105
- // Error details logged by refreshWithMutex to audit log
84
+ // Refresh failed - re-fetch to check if credential was deleted/modified
85
+ const currentCredential = getCredential(database, agent, name);
86
+ if (!currentCredential) {
87
+ // Credential was deleted during refresh - don't leak old data
88
+ logAccess(database, {
89
+ apiKeyId: apiKey.id,
90
+ action: "read",
91
+ agent,
92
+ name,
93
+ success: false,
94
+ errorMessage: "Credential deleted during refresh",
95
+ });
96
+ response.status(404).json({ error: "Credential not found" });
97
+ return;
98
+ }
99
+ if (currentCredential.updatedAt.getTime() !==
100
+ credential.updatedAt.getTime()) {
101
+ // Credential was modified during refresh - return fresh data
102
+ const freshData = decryptCredential(currentCredential);
103
+ finalData = freshData;
104
+ finalExpiresAt = currentCredential.expiresAt;
105
+ finalUpdatedAt = currentCredential.updatedAt;
106
+ }
107
+ // Otherwise credential unchanged, return original with warning
106
108
  refreshFailed = true;
107
109
  }
108
110
  }
@@ -134,6 +136,7 @@ function createGetCredentialHandler(database, config) {
134
136
  response.json({
135
137
  agent,
136
138
  name,
139
+ type: credential.type,
137
140
  data: finalData,
138
141
  expiresAt: finalExpiresAt?.toISOString(),
139
142
  updatedAt: finalUpdatedAt.toISOString(),
@@ -20,6 +20,7 @@ function createListCredentialsHandler(database) {
20
20
  credentials: credentials.map((cred) => ({
21
21
  agent: cred.agent,
22
22
  name: cred.name,
23
+ type: cred.type,
23
24
  expiresAt: cred.expiresAt?.toISOString(),
24
25
  updatedAt: cred.updatedAt.toISOString(),
25
26
  })),
@@ -26,7 +26,10 @@ function createPutCredentialHandler(database) {
26
26
  return;
27
27
  }
28
28
  const body = request.body;
29
- if (!body || typeof body.data !== "object" || body.data === null) {
29
+ if (!body ||
30
+ typeof body.data !== "object" ||
31
+ body.data === null ||
32
+ Array.isArray(body.data)) {
30
33
  logAccess(database, {
31
34
  apiKeyId: apiKey.id,
32
35
  action: "write",
@@ -40,6 +43,21 @@ function createPutCredentialHandler(database) {
40
43
  .json({ error: "Request body must include 'data' object" });
41
44
  return;
42
45
  }
46
+ if (body.type !== "oauth" && body.type !== "api-key") {
47
+ logAccess(database, {
48
+ apiKeyId: apiKey.id,
49
+ action: "write",
50
+ agent,
51
+ name,
52
+ success: false,
53
+ errorMessage: "Invalid type",
54
+ });
55
+ response.status(400).json({
56
+ error: 'Request body must include \'type\' ("oauth" or "api-key")',
57
+ });
58
+ return;
59
+ }
60
+ const credentialType = body.type;
43
61
  try {
44
62
  const encrypted = encryptCredential(body.data);
45
63
  let expiresAt;
@@ -61,6 +79,7 @@ function createPutCredentialHandler(database) {
61
79
  upsertCredential(database, {
62
80
  agent,
63
81
  name,
82
+ type: credentialType,
64
83
  ...encrypted,
65
84
  expiresAt,
66
85
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * axvault - Remote credential storage server for axpoint.
2
+ * axvault - Remote credential storage server for axkit.
3
3
  *
4
4
  * This module exports types and functions for programmatic use.
5
5
  */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * axvault - Remote credential storage server for axpoint.
2
+ * axvault - Remote credential storage server for axkit.
3
3
  *
4
4
  * This module exports types and functions for programmatic use.
5
5
  */
@@ -5,6 +5,7 @@
5
5
  * with axauth's refresh functionality.
6
6
  */
7
7
  import type Database from "better-sqlite3";
8
+ import type { CredentialType } from "../db/repositories/credentials.js";
8
9
  /** Key for credential-specific mutex */
9
10
  type CredentialKey = `${string}/${string}`;
10
11
  /** Result type for the full refresh operation */
@@ -40,12 +41,13 @@ declare function buildKey(agent: string, name: string): CredentialKey;
40
41
  * @param database - Database connection
41
42
  * @param agent - Agent name
42
43
  * @param name - Credential name
44
+ * @param type - Credential type (must be "oauth"; caller must gate on type)
43
45
  * @param data - Current decrypted credential data
44
46
  * @param apiKeyId - API key ID for audit logging
45
47
  * @param originalUpdatedAt - Original updatedAt for optimistic locking
46
48
  * @param options - Refresh options
47
49
  * @returns Refresh result with new data, expiresAt, updatedAt or error
48
50
  */
49
- declare function refreshWithMutex(database: Database.Database, agent: string, name: string, data: Record<string, unknown>, apiKeyId: string, originalUpdatedAt: Date, options: RefreshManagerOptions): Promise<FullRefreshResult>;
51
+ declare function refreshWithMutex(database: Database.Database, agent: string, name: string, type: CredentialType, data: Record<string, unknown>, apiKeyId: string, originalUpdatedAt: Date, options: RefreshManagerOptions): Promise<FullRefreshResult>;
50
52
  export { extractExpiryDate, isRefreshable, needsRefresh, toAxauthCredentials, } from "./check-refresh.js";
51
53
  export { buildKey, refreshWithMutex };
@@ -19,7 +19,7 @@ function buildKey(agent, name) {
19
19
  * Execute the full refresh operation: call axauth, validate, persist.
20
20
  * This is the inner operation that gets stored in the mutex.
21
21
  */
22
- async function executeRefresh(database, agent, name, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options) {
22
+ async function executeRefresh(database, agent, name, type, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options) {
23
23
  const logContext = { database, apiKeyId, agent, name };
24
24
  // Map to axauth format
25
25
  const creds = toAxauthCredentials(agent, data);
@@ -54,6 +54,7 @@ async function executeRefresh(database, agent, name, data, apiKeyId, originalUpd
54
54
  upsertCredential(database, {
55
55
  agent,
56
56
  name,
57
+ type,
57
58
  ...encrypted,
58
59
  expiresAt,
59
60
  });
@@ -98,13 +99,14 @@ async function executeRefresh(database, agent, name, data, apiKeyId, originalUpd
98
99
  * @param database - Database connection
99
100
  * @param agent - Agent name
100
101
  * @param name - Credential name
102
+ * @param type - Credential type (must be "oauth"; caller must gate on type)
101
103
  * @param data - Current decrypted credential data
102
104
  * @param apiKeyId - API key ID for audit logging
103
105
  * @param originalUpdatedAt - Original updatedAt for optimistic locking
104
106
  * @param options - Refresh options
105
107
  * @returns Refresh result with new data, expiresAt, updatedAt or error
106
108
  */
107
- async function refreshWithMutex(database, agent, name, data, apiKeyId, originalUpdatedAt, options) {
109
+ async function refreshWithMutex(database, agent, name, type, data, apiKeyId, originalUpdatedAt, options) {
108
110
  const key = buildKey(agent, name);
109
111
  // Check if refresh already in progress
110
112
  const existing = pendingRefreshes.get(key);
@@ -115,7 +117,7 @@ async function refreshWithMutex(database, agent, name, data, apiKeyId, originalU
115
117
  }
116
118
  // Start new refresh - store promise for the FULL operation
117
119
  const refreshStartedAt = new Date();
118
- const fullOperationPromise = executeRefresh(database, agent, name, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options);
120
+ const fullOperationPromise = executeRefresh(database, agent, name, type, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options);
119
121
  pendingRefreshes.set(key, {
120
122
  promise: fullOperationPromise,
121
123
  startedAt: refreshStartedAt,
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "name": "axvault",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.0.0",
6
- "description": "Remote credential storage server for axpoint",
5
+ "version": "1.1.0",
6
+ "description": "Remote credential storage server for axkit",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/Jercik/axvault.git"
@@ -81,7 +81,7 @@
81
81
  "@types/node": "^25.0.3",
82
82
  "@vitest/coverage-v8": "^4.0.16",
83
83
  "eslint": "^9.39.2",
84
- "eslint-config-axpoint": "^1.0.0",
84
+ "eslint-config-axkit": "^1.0.0",
85
85
  "fta-check": "^1.5.1",
86
86
  "fta-cli": "^3.0.0",
87
87
  "knip": "^5.80.0",