axvault 1.2.0 → 1.3.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
@@ -192,13 +192,17 @@ curl -X PUT https://vault.example.com/api/v1/credentials/claude/prod \
192
192
  -H "Authorization: Bearer <api_key>" \
193
193
  -H "Content-Type: application/json" \
194
194
  -d '{
195
- "type": "oauth",
195
+ "type": "oauth-credentials",
196
196
  "data": {"access_token": "...", "refresh_token": "..."},
197
197
  "expiresAt": "2025-12-31T23:59:59Z"
198
198
  }'
199
199
  ```
200
200
 
201
- 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.
201
+ The `type` field is required and must be one of:
202
+
203
+ - `"oauth-credentials"` — Full OAuth with refresh_token (eligible for auto-refresh)
204
+ - `"oauth-token"` — Long-lived OAuth token like `CLAUDE_CODE_OAUTH_TOKEN` (static)
205
+ - `"api-key"` — API key (static)
202
206
 
203
207
  ### Retrieve a Credential
204
208
 
@@ -216,7 +220,7 @@ curl -X DELETE https://vault.example.com/api/v1/credentials/claude/prod \
216
220
 
217
221
  ## Auto-Refresh
218
222
 
219
- axvault automatically refreshes OAuth credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting.
223
+ axvault automatically refreshes `oauth-credentials` type credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting. Only credentials with a `refresh_token` in their data are eligible for auto-refresh.
220
224
 
221
225
  ### Access Control Note
222
226
 
@@ -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 = 4;
7
+ declare const CURRENT_VERSION = 5;
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 = 4;
6
+ const CURRENT_VERSION = 5;
7
7
  /** Run all pending migrations */
8
8
  function runMigrations(database) {
9
9
  const version = getSchemaVersion(database);
@@ -19,6 +19,9 @@ function runMigrations(database) {
19
19
  if (version < 4) {
20
20
  migrateToV4(database);
21
21
  }
22
+ if (version < 5) {
23
+ migrateToV5(database);
24
+ }
22
25
  }
23
26
  /** Get current schema version */
24
27
  function getSchemaVersion(database) {
@@ -127,4 +130,22 @@ function migrateToV4(database) {
127
130
  setSchemaVersion(database, 4);
128
131
  })();
129
132
  }
133
+ /**
134
+ * Migration to version 5: Rename credential type "oauth" to "oauth-credentials"
135
+ *
136
+ * Distinguishes refreshable OAuth credentials (oauth-credentials) from
137
+ * long-lived OAuth tokens like CLAUDE_CODE_OAUTH_TOKEN (oauth-token).
138
+ *
139
+ * - `oauth-credentials`: Full OAuth flow with accessToken, refreshToken, expiresAt (refreshable)
140
+ * - `oauth-token`: Long-lived OAuth token for CI/CD (static, no refresh)
141
+ * - `api-key`: API key (static)
142
+ */
143
+ function migrateToV5(database) {
144
+ database.transaction(() => {
145
+ database.exec(`
146
+ UPDATE credentials SET type = 'oauth-credentials' WHERE type = 'oauth'
147
+ `);
148
+ setSchemaVersion(database, 5);
149
+ })();
150
+ }
130
151
  export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SQL queries for credentials repository.
3
+ */
4
+ export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(agent, name) DO UPDATE SET\n type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at, expires_at = excluded.expires_at";
5
+ export declare const SELECT_CREDENTIAL = "\n SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at\n FROM credentials WHERE agent = ? AND name = ?";
6
+ export declare const SELECT_ALL_METADATA = "\n SELECT agent, name, type, created_at, updated_at, expires_at\n FROM credentials ORDER BY agent, name";
7
+ export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE agent = ? AND name = ?";
8
+ export declare const UPDATE_EXPIRES_AT = "\n UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SQL queries for credentials repository.
3
+ */
4
+ export const UPSERT_CREDENTIAL = `
5
+ INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
6
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7
+ ON CONFLICT(agent, name) DO UPDATE SET
8
+ type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
9
+ updated_at = excluded.updated_at, expires_at = excluded.expires_at`;
10
+ export const SELECT_CREDENTIAL = `
11
+ SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at
12
+ FROM credentials WHERE agent = ? AND name = ?`;
13
+ export const SELECT_ALL_METADATA = `
14
+ SELECT agent, name, type, created_at, updated_at, expires_at
15
+ FROM credentials ORDER BY agent, name`;
16
+ export const DELETE_CREDENTIAL = `
17
+ DELETE FROM credentials WHERE agent = ? AND name = ?`;
18
+ export const UPDATE_EXPIRES_AT = `
19
+ UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`;
@@ -4,30 +4,8 @@
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";
9
- /** Credential record stored in database */
10
- interface CredentialRecord {
11
- agent: string;
12
- name: string;
13
- type: CredentialType;
14
- encryptedData: Buffer;
15
- salt: Buffer;
16
- iv: Buffer;
17
- authTag: Buffer;
18
- createdAt: Date;
19
- updatedAt: Date;
20
- expiresAt: Date | undefined;
21
- }
22
- /** Credential metadata (without encrypted data) */
23
- interface CredentialMetadata {
24
- agent: string;
25
- name: string;
26
- type: CredentialType;
27
- createdAt: Date;
28
- updatedAt: Date;
29
- expiresAt: Date | undefined;
30
- }
7
+ import type { CredentialType } from "axshared";
8
+ import { type CredentialMetadata, type CredentialRecord } from "./parse-credential-row.js";
31
9
  /** Store or update a credential */
32
10
  declare function upsertCredential(database: Database.Database, credential: {
33
11
  agent: string;
@@ -50,4 +28,4 @@ declare function deleteCredential(database: Database.Database, agent: string, na
50
28
  /** Update expiration time after refresh */
51
29
  declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
52
30
  export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
53
- export type { CredentialMetadata, CredentialRecord, CredentialType };
31
+ export type { CredentialMetadata, CredentialRecord, } from "./parse-credential-row.js";
@@ -3,64 +3,25 @@
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
- }
13
- /** Convert database row to record */
14
- function rowToRecord(row) {
15
- return {
16
- agent: row.agent,
17
- name: row.name,
18
- type: validateType(row.type),
19
- encryptedData: row.encrypted_data,
20
- salt: row.salt,
21
- iv: row.iv,
22
- authTag: row.auth_tag,
23
- createdAt: new Date(row.created_at),
24
- updatedAt: new Date(row.updated_at),
25
- expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
26
- };
27
- }
28
- /** Convert metadata row to metadata */
29
- function rowToMetadata(row) {
30
- return {
31
- agent: row.agent,
32
- name: row.name,
33
- type: validateType(row.type),
34
- createdAt: new Date(row.created_at),
35
- updatedAt: new Date(row.updated_at),
36
- expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
37
- };
38
- }
6
+ import * as SQL from "./credentials-queries.js";
7
+ import { rowToMetadata, rowToRecord, } from "./parse-credential-row.js";
39
8
  /** Store or update a credential */
40
9
  function upsertCredential(database, credential) {
41
10
  const now = Date.now();
42
- /* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
11
+ // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
12
+ const expiresAt = credential.expiresAt?.getTime() ?? null;
43
13
  database
44
- .prepare(`INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
45
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
46
- ON CONFLICT(agent, name) DO UPDATE SET
47
- type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
48
- updated_at = excluded.updated_at, expires_at = excluded.expires_at`)
49
- .run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
50
- /* eslint-enable unicorn/no-null */
14
+ .prepare(SQL.UPSERT_CREDENTIAL)
15
+ .run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, expiresAt);
51
16
  }
52
17
  /** Get a credential by agent and name */
53
18
  function getCredential(database, agent, name) {
54
- const row = database
55
- .prepare(`SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
56
- .get(agent, name);
19
+ const row = database.prepare(SQL.SELECT_CREDENTIAL).get(agent, name);
57
20
  return row ? rowToRecord(row) : undefined;
58
21
  }
59
22
  /** List all credentials (metadata only) */
60
23
  function listCredentials(database) {
61
- const rows = database
62
- .prepare(`SELECT agent, name, type, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
63
- .all();
24
+ const rows = database.prepare(SQL.SELECT_ALL_METADATA).all();
64
25
  return rows.map((row) => rowToMetadata(row));
65
26
  }
66
27
  /** List credentials accessible by an API key's read access list */
@@ -71,15 +32,12 @@ function listCredentialsForApiKey(database, readAccess) {
71
32
  }
72
33
  /** Delete a credential */
73
34
  function deleteCredential(database, agent, name) {
74
- return (database
75
- .prepare(`DELETE FROM credentials WHERE agent = ? AND name = ?`)
76
- .run(agent, name).changes > 0);
35
+ return database.prepare(SQL.DELETE_CREDENTIAL).run(agent, name).changes > 0;
77
36
  }
78
37
  /** Update expiration time after refresh */
79
38
  function updateExpiresAt(database, agent, name, expiresAt) {
80
- database
81
- .prepare(`UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`)
82
- // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
83
- .run(expiresAt?.getTime() ?? null, Date.now(), agent, name);
39
+ // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
40
+ const expires = expiresAt?.getTime() ?? null;
41
+ database.prepare(SQL.UPDATE_EXPIRES_AT).run(expires, Date.now(), agent, name);
84
42
  }
85
43
  export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure functions for parsing credential database rows.
3
+ */
4
+ import { CredentialType } from "axshared";
5
+ import type { CredentialRow, MetadataRow } from "../types.js";
6
+ /** Credential record stored in database */
7
+ interface CredentialRecord {
8
+ agent: string;
9
+ name: string;
10
+ type: CredentialType;
11
+ encryptedData: Buffer;
12
+ salt: Buffer;
13
+ iv: Buffer;
14
+ authTag: Buffer;
15
+ createdAt: Date;
16
+ updatedAt: Date;
17
+ expiresAt: Date | undefined;
18
+ }
19
+ /** Credential metadata (without encrypted data) */
20
+ interface CredentialMetadata {
21
+ agent: string;
22
+ name: string;
23
+ type: CredentialType;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ expiresAt: Date | undefined;
27
+ }
28
+ /** Convert database row to record */
29
+ declare function rowToRecord(row: CredentialRow): CredentialRecord;
30
+ /** Convert metadata row to metadata */
31
+ declare function rowToMetadata(row: MetadataRow): CredentialMetadata;
32
+ export { rowToMetadata, rowToRecord };
33
+ export type { CredentialMetadata, CredentialRecord };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure functions for parsing credential database rows.
3
+ */
4
+ import { CredentialType } from "axshared";
5
+ /** Parse and validate credential type from database */
6
+ function parseCredentialType(type) {
7
+ const result = CredentialType.safeParse(type);
8
+ if (!result.success) {
9
+ throw new Error(`Invalid credential type: ${type}`);
10
+ }
11
+ return result.data;
12
+ }
13
+ /** Convert database row to record */
14
+ function rowToRecord(row) {
15
+ return {
16
+ agent: row.agent,
17
+ name: row.name,
18
+ type: parseCredentialType(row.type),
19
+ encryptedData: row.encrypted_data,
20
+ salt: row.salt,
21
+ iv: row.iv,
22
+ authTag: row.auth_tag,
23
+ createdAt: new Date(row.created_at),
24
+ updatedAt: new Date(row.updated_at),
25
+ expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
26
+ };
27
+ }
28
+ /** Convert metadata row to metadata */
29
+ function rowToMetadata(row) {
30
+ return {
31
+ agent: row.agent,
32
+ name: row.name,
33
+ type: parseCredentialType(row.type),
34
+ createdAt: new Date(row.created_at),
35
+ updatedAt: new Date(row.updated_at),
36
+ expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
37
+ };
38
+ }
39
+ export { rowToMetadata, rowToRecord };
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * GET /api/v1/credentials/:agent/:name handler.
3
3
  */
4
+ import { isRefreshableCredentialType } from "axshared";
4
5
  import { hasReadAccess } from "../db/repositories/api-keys.js";
5
6
  import { getCredential } from "../db/repositories/credentials.js";
6
7
  import { logAccess } from "../db/repositories/audit-log.js";
@@ -69,7 +70,7 @@ function createGetCredentialHandler(database, config) {
69
70
  let wasRefreshed = false;
70
71
  let refreshFailed = false;
71
72
  if (config.refreshThresholdSeconds > 0 &&
72
- credential.type === "oauth" &&
73
+ isRefreshableCredentialType(credential.type) &&
73
74
  isRefreshable(data) &&
74
75
  needsRefresh(data, config.refreshThresholdSeconds)) {
75
76
  try {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * PUT /api/v1/credentials/:agent/:name handler.
3
3
  */
4
+ import { CredentialType, } from "axshared";
4
5
  import { hasWriteAccess } from "../db/repositories/api-keys.js";
5
6
  import { upsertCredential } from "../db/repositories/credentials.js";
6
7
  import { logAccess } from "../db/repositories/audit-log.js";
@@ -43,7 +44,8 @@ function createPutCredentialHandler(database) {
43
44
  .json({ error: "Request body must include 'data' object" });
44
45
  return;
45
46
  }
46
- if (body.type !== "oauth" && body.type !== "api-key") {
47
+ const typeResult = CredentialType.safeParse(body.type);
48
+ if (!typeResult.success) {
47
49
  logAccess(database, {
48
50
  apiKeyId: apiKey.id,
49
51
  action: "write",
@@ -53,11 +55,11 @@ function createPutCredentialHandler(database) {
53
55
  errorMessage: "Invalid type",
54
56
  });
55
57
  response.status(400).json({
56
- error: 'Request body must include \'type\' ("oauth" or "api-key")',
58
+ error: `Request body must include 'type' (${CredentialType.options.map((t) => `"${t}"`).join(", ")})`,
57
59
  });
58
60
  return;
59
61
  }
60
- const credentialType = body.type;
62
+ const credentialType = typeResult.data;
61
63
  try {
62
64
  const encrypted = encryptCredential(body.data);
63
65
  let expiresAt;
@@ -50,10 +50,10 @@ function toAxauthCredentials(agent, data) {
50
50
  if (!VALID_AGENTS.has(agent)) {
51
51
  return undefined;
52
52
  }
53
- // Only OAuth credentials can be refreshed
53
+ // Only OAuth credentials with refresh tokens can be refreshed
54
54
  return {
55
55
  agent: agent,
56
- type: "oauth",
56
+ type: "oauth-credentials",
57
57
  data,
58
58
  };
59
59
  }
@@ -5,7 +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
+ import type { CredentialType } from "axshared";
9
9
  /** Key for credential-specific mutex */
10
10
  type CredentialKey = `${string}/${string}`;
11
11
  /** Result type for the full refresh operation */
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.2.0",
5
+ "version": "1.3.0",
6
6
  "description": "Remote credential storage server for axkit",
7
7
  "repository": {
8
8
  "type": "git",
@@ -49,9 +49,9 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@commander-js/extra-typings": "^14.0.0",
52
- "axauth": "^1.7.2",
53
- "axshared": "^1.8.0",
54
- "better-sqlite3": "^12.5.0",
52
+ "axauth": "^1.9.0",
53
+ "axshared": "1.9.0",
54
+ "better-sqlite3": "^12.6.0",
55
55
  "commander": "^14.0.2",
56
56
  "express": "^5.2.1"
57
57
  },
@@ -78,13 +78,13 @@
78
78
  "@total-typescript/ts-reset": "^0.6.1",
79
79
  "@types/better-sqlite3": "^7.6.13",
80
80
  "@types/express": "^5.0.6",
81
- "@types/node": "^25.0.3",
81
+ "@types/node": "^25.0.5",
82
82
  "@vitest/coverage-v8": "^4.0.16",
83
83
  "eslint": "^9.39.2",
84
84
  "eslint-config-axkit": "^1.0.0",
85
85
  "fta-check": "^1.5.1",
86
86
  "fta-cli": "^3.0.0",
87
- "knip": "^5.80.0",
87
+ "knip": "^5.80.2",
88
88
  "prettier": "3.7.4",
89
89
  "semantic-release": "^25.0.2",
90
90
  "typescript": "^5.9.3",