axvault 1.0.0 → 1.2.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.
@@ -1,157 +1,9 @@
1
1
  /**
2
2
  * API key management command handlers.
3
+ *
4
+ * Re-exports handlers from separate modules for better maintainability.
3
5
  */
4
- import { getServerConfig } from "../config.js";
5
- import { closeDatabase, getDatabase } from "../db/client.js";
6
- import { runMigrations } from "../db/migrations.js";
7
- import { createApiKey, deleteApiKey, listApiKeys, } from "../db/repositories/api-keys.js";
8
- import { containsControlChars, formatAccessList, formatDateForJson, formatKeyRow, getAccessListErrorMessage, getErrorMessage, isValidKeyId, normalizeAccessList, parseAccessList, sanitizeForTsv, } from "../lib/format.js";
9
- export function handleKeyCreate(options) {
10
- // Validate key name (reject control characters)
11
- if (containsControlChars(options.name)) {
12
- console.error("Error: Key name contains control characters.");
13
- process.exitCode = 2;
14
- return;
15
- }
16
- // Parse and validate access lists
17
- const readResult = parseAccessList(options.read);
18
- const writeResult = parseAccessList(options.write);
19
- if (readResult.error) {
20
- console.error(`Error: ${getAccessListErrorMessage(readResult.error)}`);
21
- process.exitCode = 2;
22
- return;
23
- }
24
- if (writeResult.error) {
25
- console.error(`Error: ${getAccessListErrorMessage(writeResult.error)}`);
26
- process.exitCode = 2;
27
- return;
28
- }
29
- // Normalize wildcards (warn if mixed with specific entries)
30
- const readNorm = normalizeAccessList(readResult.entries, "read");
31
- const writeNorm = normalizeAccessList(writeResult.entries, "write");
32
- if (readNorm.warning)
33
- console.warn(`Warning: ${readNorm.warning}`);
34
- if (writeNorm.warning)
35
- console.warn(`Warning: ${writeNorm.warning}`);
36
- const readAccess = readNorm.normalized;
37
- const writeAccess = writeNorm.normalized;
38
- // Validate: at least one access must be specified
39
- if (readAccess.length === 0 && writeAccess.length === 0) {
40
- console.error("Error: At least one of --read or --write must be specified.");
41
- console.error("Try 'axvault key create --help' for more information.");
42
- process.exitCode = 2;
43
- return;
44
- }
45
- try {
46
- const config = getServerConfig(options);
47
- const database = getDatabase(config.databasePath);
48
- runMigrations(database);
49
- const apiKey = createApiKey(database, {
50
- name: options.name,
51
- readAccess,
52
- writeAccess,
53
- });
54
- if (options.json) {
55
- console.warn("Warning: JSON output contains the secret key. Avoid logging in CI.");
56
- console.log(JSON.stringify({
57
- id: apiKey.id,
58
- name: apiKey.name,
59
- key: apiKey.key,
60
- readAccess: apiKey.readAccess,
61
- writeAccess: apiKey.writeAccess,
62
- createdAt: apiKey.createdAt.toISOString(),
63
- }, undefined, 2));
64
- }
65
- else {
66
- console.error(`Created API key: ${sanitizeForTsv(apiKey.name)}`);
67
- console.error(`ID: ${sanitizeForTsv(apiKey.id)}`);
68
- console.error(`Read access: ${sanitizeForTsv(formatAccessList(apiKey.readAccess))}`);
69
- console.error(`Write access: ${sanitizeForTsv(formatAccessList(apiKey.writeAccess))}`);
70
- console.error("");
71
- // Output the secret key to stdout for piping
72
- console.log(apiKey.key);
73
- console.error("");
74
- console.error("Save this key securely - it cannot be retrieved later.");
75
- }
76
- }
77
- catch (error) {
78
- console.error(`Error: Failed to create API key: ${getErrorMessage(error)}`);
79
- process.exitCode = 1;
80
- }
81
- finally {
82
- closeDatabase();
83
- }
84
- }
85
- export function handleKeyList(options) {
86
- try {
87
- const config = getServerConfig(options);
88
- const database = getDatabase(config.databasePath);
89
- runMigrations(database);
90
- const keys = listApiKeys(database);
91
- if (options.json) {
92
- const output = keys.map((key) => ({
93
- id: key.id,
94
- name: key.name,
95
- readAccess: key.readAccess,
96
- writeAccess: key.writeAccess,
97
- createdAt: key.createdAt.toISOString(),
98
- lastUsedAt: formatDateForJson(key.lastUsedAt),
99
- }));
100
- console.log(JSON.stringify(output, undefined, 2));
101
- }
102
- else if (keys.length === 0) {
103
- console.error("No API keys found.\nCreate one with: axvault key create --name <name> [--read <access>] [--write <access>]");
104
- }
105
- else {
106
- console.log("ID\tNAME\tREAD ACCESS\tWRITE ACCESS\tLAST USED");
107
- for (const key of keys)
108
- console.log(formatKeyRow(key));
109
- }
110
- }
111
- catch (error) {
112
- console.error(`Error: Failed to list API keys: ${getErrorMessage(error)}`);
113
- process.exitCode = 1;
114
- }
115
- finally {
116
- closeDatabase();
117
- }
118
- }
119
- export function handleKeyRevoke(id, options) {
120
- // Reject inputs containing control characters BEFORE trimming
121
- // (prevents silent bypass where \n or \t would be removed by trim)
122
- if (containsControlChars(id)) {
123
- console.error("Error: Invalid API key ID: contains control characters.");
124
- console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
125
- process.exitCode = 2;
126
- return;
127
- }
128
- // Trim whitespace from ID (common copy-paste issue)
129
- const trimmedId = id.trim();
130
- // Validate ID format (k_ + 12 hex chars)
131
- if (!isValidKeyId(trimmedId)) {
132
- console.error(`Error: Invalid API key ID format: ${trimmedId}`);
133
- console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
134
- process.exitCode = 2;
135
- return;
136
- }
137
- try {
138
- const config = getServerConfig(options);
139
- const database = getDatabase(config.databasePath);
140
- runMigrations(database);
141
- const deleted = deleteApiKey(database, trimmedId);
142
- if (deleted) {
143
- console.error(`Revoked API key: ${sanitizeForTsv(trimmedId)}`);
144
- }
145
- else {
146
- console.error(`Error: API key not found: ${sanitizeForTsv(trimmedId)}`);
147
- process.exitCode = 1;
148
- }
149
- }
150
- catch (error) {
151
- console.error(`Error: Failed to revoke API key: ${getErrorMessage(error)}`);
152
- process.exitCode = 1;
153
- }
154
- finally {
155
- closeDatabase();
156
- }
157
- }
6
+ export { handleKeyCreate } from "./key-create.js";
7
+ export { handleKeyList } from "./key-list.js";
8
+ export { handleKeyRevoke } from "./key-revoke.js";
9
+ export { handleKeyUpdate } from "./key-update.js";
@@ -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 = 4;
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 = 4;
7
7
  /** Run all pending migrations */
8
8
  function runMigrations(database) {
9
9
  const version = getSchemaVersion(database);
@@ -13,6 +13,12 @@ function runMigrations(database) {
13
13
  if (version < 2) {
14
14
  migrateToV2(database);
15
15
  }
16
+ if (version < 3) {
17
+ migrateToV3(database);
18
+ }
19
+ if (version < 4) {
20
+ migrateToV4(database);
21
+ }
16
22
  }
17
23
  /** Get current schema version */
18
24
  function getSchemaVersion(database) {
@@ -93,4 +99,32 @@ function migrateToV2(database) {
93
99
  setSchemaVersion(database, 2);
94
100
  })();
95
101
  }
102
+ /**
103
+ * Migration to version 3: Add type column to credentials
104
+ *
105
+ * Explicit credential type ("oauth" or "api-key") instead of inferring from data.
106
+ * Existing credentials without type must be re-uploaded.
107
+ */
108
+ function migrateToV3(database) {
109
+ database.transaction(() => {
110
+ database.exec(`
111
+ ALTER TABLE credentials ADD COLUMN type TEXT
112
+ `);
113
+ setSchemaVersion(database, 3);
114
+ })();
115
+ }
116
+ /**
117
+ * Migration to version 4: Add grant_access column to api_keys
118
+ *
119
+ * The grant permission controls which credentials a key can delegate access to.
120
+ * Existing keys get an empty grant_access array (no delegation rights).
121
+ */
122
+ function migrateToV4(database) {
123
+ database.transaction(() => {
124
+ database.exec(`
125
+ ALTER TABLE api_keys ADD COLUMN grant_access TEXT NOT NULL DEFAULT '[]'
126
+ `);
127
+ setSchemaVersion(database, 4);
128
+ })();
129
+ }
96
130
  export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -11,6 +11,7 @@ interface ApiKeyRecord {
11
11
  keyHash: string;
12
12
  readAccess: string[];
13
13
  writeAccess: string[];
14
+ grantAccess: string[];
14
15
  createdAt: Date;
15
16
  lastUsedAt: Date | undefined;
16
17
  }
@@ -23,6 +24,7 @@ declare function createApiKey(database: Database.Database, options: {
23
24
  name: string;
24
25
  readAccess: string[];
25
26
  writeAccess: string[];
27
+ grantAccess: string[];
26
28
  }): ApiKeyWithSecret;
27
29
  /** Find API key by raw key value */
28
30
  declare function findApiKeyByKey(database: Database.Database, key: string): ApiKeyRecord | undefined;
@@ -38,5 +40,13 @@ declare function deleteApiKey(database: Database.Database, id: string): boolean;
38
40
  declare function hasReadAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
39
41
  /** Check if API key has write access to a credential */
40
42
  declare function hasWriteAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
41
- export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, };
43
+ /** Check if API key has grant access to a credential */
44
+ declare function hasGrantAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
45
+ /** Update an API key's access permissions */
46
+ declare function updateApiKeyAccess(database: Database.Database, id: string, options: {
47
+ readAccess?: string[];
48
+ writeAccess?: string[];
49
+ grantAccess?: string[];
50
+ }): boolean;
51
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, };
42
52
  export type { ApiKeyRecord, ApiKeyWithSecret };
@@ -12,6 +12,7 @@ function rowToRecord(row) {
12
12
  keyHash: row.key_hash,
13
13
  readAccess: JSON.parse(row.read_access),
14
14
  writeAccess: JSON.parse(row.write_access),
15
+ grantAccess: JSON.parse(row.grant_access),
15
16
  createdAt: new Date(row.created_at),
16
17
  lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
17
18
  };
@@ -20,7 +21,7 @@ function rowToRecord(row) {
20
21
  function hashApiKey(key) {
21
22
  return createHash("sha256").update(key).digest("hex");
22
23
  }
23
- const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, created_at, last_used_at`;
24
+ const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, grant_access, created_at, last_used_at`;
24
25
  /** Create a new API key */
25
26
  function createApiKey(database, options) {
26
27
  const id = `k_${randomBytes(6).toString("hex")}`;
@@ -28,9 +29,9 @@ function createApiKey(database, options) {
28
29
  const keyHash = hashApiKey(key);
29
30
  const now = Date.now();
30
31
  database
31
- .prepare(`INSERT INTO api_keys (id, name, key_hash, read_access, write_access, created_at)
32
- VALUES (?, ?, ?, ?, ?, ?)`)
33
- .run(id, options.name, keyHash, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), now);
32
+ .prepare(`INSERT INTO api_keys (id, name, key_hash, read_access, write_access, grant_access, created_at)
33
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
34
+ .run(id, options.name, keyHash, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), JSON.stringify(options.grantAccess), now);
34
35
  return {
35
36
  id,
36
37
  name: options.name,
@@ -38,6 +39,7 @@ function createApiKey(database, options) {
38
39
  keyHash,
39
40
  readAccess: options.readAccess,
40
41
  writeAccess: options.writeAccess,
42
+ grantAccess: options.grantAccess,
41
43
  createdAt: new Date(now),
42
44
  lastUsedAt: undefined,
43
45
  };
@@ -83,4 +85,31 @@ function hasWriteAccess(apiKey, agent, name) {
83
85
  const path = `${agent}/${name}`;
84
86
  return apiKey.writeAccess.includes("*") || apiKey.writeAccess.includes(path);
85
87
  }
86
- export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, };
88
+ /** Check if API key has grant access to a credential */
89
+ function hasGrantAccess(apiKey, agent, name) {
90
+ const path = `${agent}/${name}`;
91
+ return apiKey.grantAccess.includes("*") || apiKey.grantAccess.includes(path);
92
+ }
93
+ /** Update an API key's access permissions */
94
+ function updateApiKeyAccess(database, id, options) {
95
+ const updates = [];
96
+ const values = [];
97
+ if (options.readAccess !== undefined) {
98
+ updates.push("read_access = ?");
99
+ values.push(JSON.stringify(options.readAccess));
100
+ }
101
+ if (options.writeAccess !== undefined) {
102
+ updates.push("write_access = ?");
103
+ values.push(JSON.stringify(options.writeAccess));
104
+ }
105
+ if (options.grantAccess !== undefined) {
106
+ updates.push("grant_access = ?");
107
+ values.push(JSON.stringify(options.grantAccess));
108
+ }
109
+ if (updates.length === 0)
110
+ return false;
111
+ values.push(id);
112
+ const sql = `UPDATE api_keys SET ${updates.join(", ")} WHERE id = ?`;
113
+ return database.prepare(sql).run(...values).changes > 0;
114
+ }
115
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, };
@@ -9,7 +9,7 @@ interface AuditLogEntry {
9
9
  id: number;
10
10
  timestamp: Date;
11
11
  apiKeyId: string | undefined;
12
- action: "auth" | "read" | "write" | "delete" | "refresh" | "list";
12
+ action: "auth" | "read" | "write" | "delete" | "refresh" | "list" | "grant";
13
13
  agent: string | undefined;
14
14
  name: string | undefined;
15
15
  success: boolean;
@@ -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
  }
@@ -8,6 +8,7 @@ export interface ApiKeyRow {
8
8
  key_hash: string;
9
9
  read_access: string;
10
10
  write_access: string;
11
+ grant_access: string;
11
12
  created_at: number;
12
13
  last_used_at: number | null;
13
14
  }
@@ -26,8 +27,9 @@ export interface AuditLogRow {
26
27
  export interface CredentialRow {
27
28
  agent: string;
28
29
  name: string;
30
+ type: string;
29
31
  encrypted_data: Buffer;
30
- salt: Buffer | null;
32
+ salt: Buffer;
31
33
  iv: Buffer;
32
34
  auth_tag: Buffer;
33
35
  created_at: number;
@@ -38,6 +40,7 @@ export interface CredentialRow {
38
40
  export interface MetadataRow {
39
41
  agent: string;
40
42
  name: string;
43
+ type: string;
41
44
  created_at: number;
42
45
  updated_at: number;
43
46
  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
  */
@@ -39,6 +39,7 @@ export declare function formatKeyRow(key: {
39
39
  name: string;
40
40
  readAccess: string[];
41
41
  writeAccess: string[];
42
+ grantAccess: string[];
42
43
  lastUsedAt?: Date;
43
44
  }): string;
44
45
  /**
@@ -74,8 +74,9 @@ export function formatKeyRow(key) {
74
74
  const name = sanitizeForTsv(key.name);
75
75
  const readAccess = sanitizeForTsv(formatAccessList(key.readAccess));
76
76
  const writeAccess = sanitizeForTsv(formatAccessList(key.writeAccess));
77
+ const grantAccess = sanitizeForTsv(formatAccessList(key.grantAccess));
77
78
  const lastUsed = formatRelativeTime(key.lastUsedAt);
78
- return `${id}\t${name}\t${readAccess}\t${writeAccess}\t${lastUsed}`;
79
+ return `${id}\t${name}\t${readAccess}\t${writeAccess}\t${grantAccess}\t${lastUsed}`;
79
80
  }
80
81
  /**
81
82
  * Validate access list entry format.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Parse and validate access list options from CLI flags.
3
+ */
4
+ interface ParsedAccessOptions {
5
+ addRead: string[];
6
+ addWrite: string[];
7
+ addGrant: string[];
8
+ removeRead: string[];
9
+ removeWrite: string[];
10
+ removeGrant: string[];
11
+ }
12
+ interface AccessOptions {
13
+ addRead?: string;
14
+ addWrite?: string;
15
+ addGrant?: string;
16
+ removeRead?: string;
17
+ removeWrite?: string;
18
+ removeGrant?: string;
19
+ }
20
+ /**
21
+ * Parse all access list options and validate them.
22
+ * Returns parsed entries or logs error and sets exit code.
23
+ */
24
+ export declare function parseAccessOptions(options: AccessOptions): ParsedAccessOptions | undefined;
25
+ /**
26
+ * Compute updated access list by adding and removing entries.
27
+ */
28
+ export declare function computeUpdatedAccess(current: string[], toAdd: string[], toRemove: string[]): string[];
29
+ /**
30
+ * Normalize all access lists and print warnings.
31
+ */
32
+ export declare function normalizeAllAccess(readAccess: string[], writeAccess: string[], grantAccess: string[]): {
33
+ read: string[];
34
+ write: string[];
35
+ grant: string[];
36
+ };
37
+ export {};