@vellumai/credential-executor 0.7.0 → 0.7.2
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/node_modules/@vellumai/service-contracts/package.json +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
- package/package.json +3 -2
- package/src/__tests__/ces-migrations-002-api-keys.test.ts +185 -0
- package/src/__tests__/ces-migrations-runner.test.ts +227 -0
- package/src/__tests__/cli.test.ts +139 -0
- package/src/__tests__/command-executor.test.ts +70 -41
- package/src/__tests__/local-token-refresh.test.ts +65 -38
- package/src/__tests__/toolstore.test.ts +65 -20
- package/src/__tests__/transport.test.ts +12 -3
- package/src/cli.ts +158 -0
- package/src/http/__tests__/credential-routes-normalization.test.ts +202 -0
- package/src/http/credential-routes.ts +53 -7
- package/src/main.ts +120 -50
- package/src/managed-main.ts +6 -0
- package/src/materializers/local-oauth-lookup.ts +7 -6
- package/src/materializers/local-token-refresh.ts +25 -15
- package/src/migrations/001-no-op.ts +19 -0
- package/src/migrations/002-api-keys-to-credentials.ts +60 -0
- package/src/migrations/registry.ts +15 -0
- package/src/migrations/runner.ts +146 -0
- package/src/migrations/types.ts +54 -0
- package/src/paths.ts +15 -11
|
@@ -393,16 +393,25 @@ describe("CES data paths", () => {
|
|
|
393
393
|
});
|
|
394
394
|
|
|
395
395
|
test("getBootstrapSocketPath respects CES_BOOTSTRAP_SOCKET env var", () => {
|
|
396
|
-
const
|
|
396
|
+
const savedSocket = process.env["CES_BOOTSTRAP_SOCKET"];
|
|
397
|
+
const savedDir = process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
398
|
+
// CES_BOOTSTRAP_SOCKET_DIR takes precedence; clear it so the
|
|
399
|
+
// CES_BOOTSTRAP_SOCKET fallback is actually exercised.
|
|
400
|
+
delete process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
397
401
|
process.env["CES_BOOTSTRAP_SOCKET"] = "/tmp/test-ces.sock";
|
|
398
402
|
try {
|
|
399
403
|
expect(getBootstrapSocketPath()).toBe("/tmp/test-ces.sock");
|
|
400
404
|
} finally {
|
|
401
|
-
if (
|
|
402
|
-
process.env["CES_BOOTSTRAP_SOCKET"] =
|
|
405
|
+
if (savedSocket !== undefined) {
|
|
406
|
+
process.env["CES_BOOTSTRAP_SOCKET"] = savedSocket;
|
|
403
407
|
} else {
|
|
404
408
|
delete process.env["CES_BOOTSTRAP_SOCKET"];
|
|
405
409
|
}
|
|
410
|
+
if (savedDir !== undefined) {
|
|
411
|
+
process.env["CES_BOOTSTRAP_SOCKET_DIR"] = savedDir;
|
|
412
|
+
} else {
|
|
413
|
+
delete process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
414
|
+
}
|
|
406
415
|
}
|
|
407
416
|
});
|
|
408
417
|
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CES CLI — lightweight credential CRUD for the CES container.
|
|
4
|
+
*
|
|
5
|
+
* Operates directly on the encrypted key store (`keys.enc` + `store.key`)
|
|
6
|
+
* without requiring the RPC server, HTTP routes, or a running assistant.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ces list
|
|
10
|
+
* ces get <account>
|
|
11
|
+
* ces set <account> <value>
|
|
12
|
+
* ces delete <account>
|
|
13
|
+
*
|
|
14
|
+
* Account format: `credential/<service>/<field>` (e.g. `credential/vellum/platform_organization_id`)
|
|
15
|
+
*
|
|
16
|
+
* Environment variables:
|
|
17
|
+
* CREDENTIAL_SECURITY_DIR — directory containing `keys.enc` + `store.key`
|
|
18
|
+
* CES_ASSISTANT_DATA_MOUNT — fallback root for `<mount>/.vellum/protected/`
|
|
19
|
+
*
|
|
20
|
+
* When neither is set, defaults to `~/.vellum/protected/` (local mode).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { createLocalSecureKeyBackend } from "./materializers/local-secure-key-backend.js";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Path resolution (mirrors managed-main.ts)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function resolveVellumRoot(): string {
|
|
32
|
+
const secDir = process.env["CREDENTIAL_SECURITY_DIR"]?.trim();
|
|
33
|
+
if (secDir) {
|
|
34
|
+
// CREDENTIAL_SECURITY_DIR points directly at the dir containing
|
|
35
|
+
// keys.enc, but createLocalSecureKeyBackend wants the parent
|
|
36
|
+
// (.vellum root) and appends /protected/ itself — unless
|
|
37
|
+
// CREDENTIAL_SECURITY_DIR is set, in which case the backend reads
|
|
38
|
+
// from that dir directly. So we pass dirname(secDir) as vellumRoot.
|
|
39
|
+
// Actually, looking at resolveSecurityDir(): if CREDENTIAL_SECURITY_DIR
|
|
40
|
+
// is set it uses that directly, ignoring vellumRoot. So vellumRoot
|
|
41
|
+
// can be anything — the env var takes precedence.
|
|
42
|
+
return join(secDir, "..");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mount = process.env["CES_ASSISTANT_DATA_MOUNT"]?.trim();
|
|
46
|
+
if (mount) {
|
|
47
|
+
return join(mount, ".vellum");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return join(homedir(), ".vellum");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// CLI
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
async function main(): Promise<void> {
|
|
58
|
+
const [command, ...args] = process.argv.slice(2);
|
|
59
|
+
|
|
60
|
+
if (!command || command === "--help" || command === "-h") {
|
|
61
|
+
printUsage();
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const vellumRoot = resolveVellumRoot();
|
|
66
|
+
const backend = createLocalSecureKeyBackend(vellumRoot);
|
|
67
|
+
|
|
68
|
+
switch (command) {
|
|
69
|
+
case "list": {
|
|
70
|
+
const accounts = await backend.list();
|
|
71
|
+
if (accounts.length === 0) {
|
|
72
|
+
console.log("(no credentials stored)");
|
|
73
|
+
} else {
|
|
74
|
+
for (const account of accounts.sort()) {
|
|
75
|
+
console.log(account);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "get": {
|
|
82
|
+
const account = args[0];
|
|
83
|
+
if (!account) {
|
|
84
|
+
console.error("Usage: ces get <account>");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const value = await backend.get(account);
|
|
88
|
+
if (value === undefined) {
|
|
89
|
+
console.error(`Not found: ${account}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// Write raw value to stdout (no trailing newline for piping)
|
|
93
|
+
process.stdout.write(value);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case "set": {
|
|
98
|
+
const account = args[0];
|
|
99
|
+
const value = args[1];
|
|
100
|
+
if (!account || value === undefined) {
|
|
101
|
+
console.error("Usage: ces set <account> <value>");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const ok = await backend.set(account, value);
|
|
105
|
+
if (ok) {
|
|
106
|
+
console.log(`Set: ${account}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.error(`Failed to set: ${account}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "delete": {
|
|
115
|
+
const account = args[0];
|
|
116
|
+
if (!account) {
|
|
117
|
+
console.error("Usage: ces delete <account>");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const result = await backend.delete(account);
|
|
121
|
+
if (result === "deleted") {
|
|
122
|
+
console.log(`Deleted: ${account}`);
|
|
123
|
+
} else {
|
|
124
|
+
console.error(`Not found: ${account}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
console.error(`Unknown command: ${command}`);
|
|
132
|
+
printUsage();
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function printUsage(): void {
|
|
138
|
+
console.log(`CES CLI — credential CRUD for the encrypted key store
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
ces list List all credential accounts
|
|
142
|
+
ces get <account> Get a credential value
|
|
143
|
+
ces set <account> <value> Set a credential value
|
|
144
|
+
ces delete <account> Delete a credential
|
|
145
|
+
|
|
146
|
+
Account format:
|
|
147
|
+
credential/<service>/<field>
|
|
148
|
+
Example: credential/vellum/platform_organization_id
|
|
149
|
+
|
|
150
|
+
Environment:
|
|
151
|
+
CREDENTIAL_SECURITY_DIR Directory containing keys.enc + store.key
|
|
152
|
+
CES_ASSISTANT_DATA_MOUNT Fallback: <mount>/.vellum/protected/`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
main().catch((err) => {
|
|
156
|
+
console.error(`Fatal: ${err instanceof Error ? err.message : err}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for credential account key normalization in the HTTP routes.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that colon-separated account names (e.g. `vellum:platform_organization_id`)
|
|
5
|
+
* are transparently normalized to the internal slash-separated format
|
|
6
|
+
* (e.g. `credential/vellum/platform_organization_id`) so that credentials
|
|
7
|
+
* stored via direct HTTP are findable by the gateway and assistant.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { handleCredentialRoute } from "../credential-routes.js";
|
|
13
|
+
import type { CredentialRouteDeps } from "../credential-routes.js";
|
|
14
|
+
import type { SecureKeyBackend } from "@vellumai/credential-storage";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const SERVICE_TOKEN = "test-token-normalization";
|
|
21
|
+
|
|
22
|
+
function makeDeps(): { deps: CredentialRouteDeps; store: Map<string, string> } {
|
|
23
|
+
const store = new Map<string, string>();
|
|
24
|
+
const backend: SecureKeyBackend = {
|
|
25
|
+
get: async (account: string) => store.get(account),
|
|
26
|
+
set: async (account: string, value: string) => {
|
|
27
|
+
store.set(account, value);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
delete: async (account: string) => {
|
|
31
|
+
if (!store.has(account)) return "not-found";
|
|
32
|
+
store.delete(account);
|
|
33
|
+
return "deleted";
|
|
34
|
+
},
|
|
35
|
+
list: async () => [...store.keys()],
|
|
36
|
+
};
|
|
37
|
+
return { deps: { backend, serviceToken: SERVICE_TOKEN }, store };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeRequest(
|
|
41
|
+
method: string,
|
|
42
|
+
path: string,
|
|
43
|
+
body?: unknown,
|
|
44
|
+
): Request {
|
|
45
|
+
const url = `http://localhost:8090${path}`;
|
|
46
|
+
const headers: Record<string, string> = {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Authorization: `Bearer ${SERVICE_TOKEN}`,
|
|
49
|
+
};
|
|
50
|
+
const init: RequestInit = { method, headers };
|
|
51
|
+
if (body !== undefined) {
|
|
52
|
+
init.body = JSON.stringify(body);
|
|
53
|
+
}
|
|
54
|
+
return new Request(url, init);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Tests
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
describe("credential route key normalization", () => {
|
|
62
|
+
it("normalizes colon-separated key on POST (set)", async () => {
|
|
63
|
+
const { deps, store } = makeDeps();
|
|
64
|
+
|
|
65
|
+
const req = makeRequest(
|
|
66
|
+
"POST",
|
|
67
|
+
"/v1/credentials/vellum%3Aplatform_organization_id",
|
|
68
|
+
{ value: "org-uuid-123" },
|
|
69
|
+
);
|
|
70
|
+
const res = await handleCredentialRoute(req, deps);
|
|
71
|
+
|
|
72
|
+
expect(res).not.toBeNull();
|
|
73
|
+
expect(res!.status).toBe(200);
|
|
74
|
+
const body = await res!.json();
|
|
75
|
+
expect(body.ok).toBe(true);
|
|
76
|
+
expect(body.account).toBe("credential/vellum/platform_organization_id");
|
|
77
|
+
|
|
78
|
+
// Verify it was stored under the normalized key
|
|
79
|
+
expect(store.get("credential/vellum/platform_organization_id")).toBe("org-uuid-123");
|
|
80
|
+
expect(store.has("vellum:platform_organization_id")).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("normalizes colon-separated key on GET", async () => {
|
|
84
|
+
const { deps, store } = makeDeps();
|
|
85
|
+
store.set("credential/vellum/platform_user_id", "user-uuid-456");
|
|
86
|
+
|
|
87
|
+
const req = makeRequest(
|
|
88
|
+
"GET",
|
|
89
|
+
"/v1/credentials/vellum%3Aplatform_user_id",
|
|
90
|
+
);
|
|
91
|
+
const res = await handleCredentialRoute(req, deps);
|
|
92
|
+
|
|
93
|
+
expect(res).not.toBeNull();
|
|
94
|
+
expect(res!.status).toBe(200);
|
|
95
|
+
const body = await res!.json();
|
|
96
|
+
expect(body.value).toBe("user-uuid-456");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("normalizes colon-separated key on DELETE", async () => {
|
|
100
|
+
const { deps, store } = makeDeps();
|
|
101
|
+
store.set("credential/vellum/temp_cred", "temp-value");
|
|
102
|
+
|
|
103
|
+
const req = makeRequest(
|
|
104
|
+
"DELETE",
|
|
105
|
+
"/v1/credentials/vellum%3Atemp_cred",
|
|
106
|
+
);
|
|
107
|
+
const res = await handleCredentialRoute(req, deps);
|
|
108
|
+
|
|
109
|
+
expect(res).not.toBeNull();
|
|
110
|
+
expect(res!.status).toBe(200);
|
|
111
|
+
expect(store.has("credential/vellum/temp_cred")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("passes through keys already in credential/ format", async () => {
|
|
115
|
+
const { deps, store } = makeDeps();
|
|
116
|
+
|
|
117
|
+
const req = makeRequest(
|
|
118
|
+
"POST",
|
|
119
|
+
"/v1/credentials/credential%2Fvellum%2Fassistant_api_key",
|
|
120
|
+
{ value: "api-key-789" },
|
|
121
|
+
);
|
|
122
|
+
const res = await handleCredentialRoute(req, deps);
|
|
123
|
+
|
|
124
|
+
expect(res).not.toBeNull();
|
|
125
|
+
expect(res!.status).toBe(200);
|
|
126
|
+
expect(store.get("credential/vellum/assistant_api_key")).toBe("api-key-789");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("passes through oauth/ prefixed keys", async () => {
|
|
130
|
+
const { deps, store } = makeDeps();
|
|
131
|
+
|
|
132
|
+
const req = makeRequest(
|
|
133
|
+
"POST",
|
|
134
|
+
"/v1/credentials/oauth%2Fconnection%2Faccess_token",
|
|
135
|
+
{ value: "token-abc" },
|
|
136
|
+
);
|
|
137
|
+
const res = await handleCredentialRoute(req, deps);
|
|
138
|
+
|
|
139
|
+
expect(res).not.toBeNull();
|
|
140
|
+
expect(res!.status).toBe(200);
|
|
141
|
+
expect(store.get("oauth/connection/access_token")).toBe("token-abc");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("normalizes colon-separated keys in bulk set", async () => {
|
|
145
|
+
const { deps, store } = makeDeps();
|
|
146
|
+
|
|
147
|
+
const req = makeRequest("POST", "/v1/credentials/bulk", {
|
|
148
|
+
credentials: [
|
|
149
|
+
{ account: "vellum:platform_organization_id", value: "org-1" },
|
|
150
|
+
{ account: "vellum:platform_user_id", value: "user-1" },
|
|
151
|
+
{ account: "credential/vellum/assistant_api_key", value: "key-1" },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
const res = await handleCredentialRoute(req, deps);
|
|
155
|
+
|
|
156
|
+
expect(res).not.toBeNull();
|
|
157
|
+
expect(res!.status).toBe(200);
|
|
158
|
+
const body = await res!.json();
|
|
159
|
+
|
|
160
|
+
expect(body.results).toHaveLength(3);
|
|
161
|
+
expect(body.results[0].account).toBe("credential/vellum/platform_organization_id");
|
|
162
|
+
expect(body.results[1].account).toBe("credential/vellum/platform_user_id");
|
|
163
|
+
expect(body.results[2].account).toBe("credential/vellum/assistant_api_key");
|
|
164
|
+
|
|
165
|
+
expect(store.get("credential/vellum/platform_organization_id")).toBe("org-1");
|
|
166
|
+
expect(store.get("credential/vellum/platform_user_id")).toBe("user-1");
|
|
167
|
+
expect(store.get("credential/vellum/assistant_api_key")).toBe("key-1");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("splits multi-colon keys at the last colon", async () => {
|
|
171
|
+
const { deps, store } = makeDeps();
|
|
172
|
+
|
|
173
|
+
const req = makeRequest(
|
|
174
|
+
"POST",
|
|
175
|
+
"/v1/credentials/integration%3Agoogle%3Aaccess_token",
|
|
176
|
+
{ value: "google-token" },
|
|
177
|
+
);
|
|
178
|
+
const res = await handleCredentialRoute(req, deps);
|
|
179
|
+
|
|
180
|
+
expect(res).not.toBeNull();
|
|
181
|
+
expect(res!.status).toBe(200);
|
|
182
|
+
const body = await res!.json();
|
|
183
|
+
// "integration:google:access_token" splits at last colon →
|
|
184
|
+
// service="integration:google", field="access_token"
|
|
185
|
+
expect(body.account).toBe("credential/integration:google/access_token");
|
|
186
|
+
expect(store.get("credential/integration:google/access_token")).toBe("google-token");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("returns normalized key in response body", async () => {
|
|
190
|
+
const { deps } = makeDeps();
|
|
191
|
+
|
|
192
|
+
const req = makeRequest(
|
|
193
|
+
"POST",
|
|
194
|
+
"/v1/credentials/slack_channel%3Abot_token",
|
|
195
|
+
{ value: "xoxb-test" },
|
|
196
|
+
);
|
|
197
|
+
const res = await handleCredentialRoute(req, deps);
|
|
198
|
+
|
|
199
|
+
const body = await res!.json();
|
|
200
|
+
expect(body.account).toBe("credential/slack_channel/bot_token");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -21,6 +21,49 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
21
21
|
|
|
22
22
|
import type { SecureKeyBackend } from "@vellumai/credential-storage";
|
|
23
23
|
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Account key normalization
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Known internal key prefixes. Keys in the encrypted store use slash-separated
|
|
30
|
+
* paths (e.g. `credential/vellum/platform_organization_id`), but callers
|
|
31
|
+
* (especially manual `curl` invocations) often use the colon-separated format
|
|
32
|
+
* visible in the CLI (e.g. `vellum:platform_organization_id`).
|
|
33
|
+
*
|
|
34
|
+
* This normalizer transparently converts colon-separated credential names
|
|
35
|
+
* to the internal format so writes land under the correct key. Without this,
|
|
36
|
+
* a credential stored as `vellum:platform_organization_id` would silently
|
|
37
|
+
* succeed but be invisible to the gateway and assistant, which look up
|
|
38
|
+
* `credential/vellum/platform_organization_id`.
|
|
39
|
+
*/
|
|
40
|
+
const CREDENTIAL_PREFIX = "credential/";
|
|
41
|
+
|
|
42
|
+
function normalizeAccountKey(account: string): string {
|
|
43
|
+
// Already in internal format — pass through
|
|
44
|
+
if (account.startsWith(CREDENTIAL_PREFIX)) {
|
|
45
|
+
return account;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Other known internal prefixes — pass through as-is
|
|
49
|
+
if (account.startsWith("oauth/")) {
|
|
50
|
+
return account;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Convert "service:field" → "credential/service/field"
|
|
54
|
+
// Use lastIndexOf to match the canonical split in secret-routes.ts
|
|
55
|
+
// (e.g. "integration:google:access_token" → service="integration:google", field="access_token")
|
|
56
|
+
const colonIdx = account.lastIndexOf(":");
|
|
57
|
+
if (colonIdx > 0 && colonIdx < account.length - 1) {
|
|
58
|
+
const service = account.slice(0, colonIdx);
|
|
59
|
+
const field = account.slice(colonIdx + 1);
|
|
60
|
+
return `${CREDENTIAL_PREFIX}${service}/${field}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Unrecognized format — return as-is (will likely fail lookup, which is fine)
|
|
64
|
+
return account;
|
|
65
|
+
}
|
|
66
|
+
|
|
24
67
|
// ---------------------------------------------------------------------------
|
|
25
68
|
// Auth
|
|
26
69
|
// ---------------------------------------------------------------------------
|
|
@@ -136,8 +179,9 @@ export async function handleCredentialRoute(
|
|
|
136
179
|
|
|
137
180
|
const results: Array<{ account: string; ok: boolean }> = [];
|
|
138
181
|
for (const entry of body.credentials as Array<{ account: string; value: string }>) {
|
|
139
|
-
const
|
|
140
|
-
|
|
182
|
+
const normalized = normalizeAccountKey(entry.account);
|
|
183
|
+
const ok = await backend.set(normalized, entry.value);
|
|
184
|
+
results.push({ account: normalized, ok: !!ok });
|
|
141
185
|
}
|
|
142
186
|
|
|
143
187
|
return new Response(
|
|
@@ -167,15 +211,17 @@ export async function handleCredentialRoute(
|
|
|
167
211
|
return null; // Not a credential route
|
|
168
212
|
}
|
|
169
213
|
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
214
|
+
const rawAccount = decodeURIComponent(accountSegment.slice(1));
|
|
215
|
+
if (!rawAccount) {
|
|
172
216
|
return new Response(
|
|
173
217
|
JSON.stringify({ error: "Account name is required" }),
|
|
174
218
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
175
|
-
|
|
176
|
-
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const account = normalizeAccountKey(rawAccount);
|
|
177
223
|
|
|
178
|
-
|
|
224
|
+
switch (req.method) {
|
|
179
225
|
// GET /v1/credentials/:account — get credential value
|
|
180
226
|
case "GET": {
|
|
181
227
|
const value = await backend.get(account);
|