@embrasure/ember 0.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.
- package/README.md +58 -0
- package/dist/auth.d.ts +24 -0
- package/dist/auth.js +305 -0
- package/dist/catalog.d.ts +180 -0
- package/dist/catalog.js +115 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +167 -0
- package/dist/operator.d.ts +36 -0
- package/dist/operator.js +493 -0
- package/dist/plugin-deletion.d.ts +29 -0
- package/dist/plugin-deletion.js +30 -0
- package/dist/plugin-edits.d.ts +125 -0
- package/dist/plugin-edits.js +73 -0
- package/dist/plugin-policy.d.ts +71 -0
- package/dist/plugin-policy.js +47 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +274 -0
- package/dist/query-response.d.ts +31 -0
- package/dist/query-response.js +15 -0
- package/dist/types.d.ts +192 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Ember CLI and MCP
|
|
2
|
+
|
|
3
|
+
The v0.2.0 tool contract replaces the original setup/status/audit tool names. Refresh your MCP client's tool list after upgrading.
|
|
4
|
+
|
|
5
|
+
`@embrasure/ember` gives engineers and agents one safe interface for operating an Ember managed data warehouse.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @embrasure/ember auth login
|
|
9
|
+
npx @embrasure/ember setup
|
|
10
|
+
npx @embrasure/ember status
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
OAuth-capable MCP clients can connect directly to:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
https://embrasure.ai/api/mcp/ember
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Clients without remote OAuth can run `ember mcp serve`. Both transports expose exactly five tools: `warehouse`, `sources`, `ingestion`, `catalog`, and `query`.
|
|
20
|
+
|
|
21
|
+
Source secrets are never MCP arguments. OAuth and browser-configured sources return a secure handoff URL. The CLI can read database credentials from an environment variable or stdin:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
ember source connect postgres --credential-env DATABASE_URL
|
|
25
|
+
printf '%s' "$DATABASE_URL" | ember source connect postgres --credential-stdin
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Ingestion planning selects every eligible discovered table automatically and reports blocked tables. Batch ingestion also adds newly discovered eligible tables. Use `--tables` only to explicitly restrict tables, columns, keys, or cursors; an explicit table list disables automatic additions. CDC engines require another plan for new tables. A successful start means ingestion was scheduled; inspect status, then verify an answer with a read-only query.
|
|
29
|
+
|
|
30
|
+
All warehouse SQL is server-enforced as read-only. Query and ingestion operations return handles for polling, pagination, and cancellation.
|
|
31
|
+
|
|
32
|
+
Start business questions with `catalog` so the agent can find the agreed metric definition before writing SQL:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
ember catalog search "latest onboarding rates"
|
|
36
|
+
ember catalog describe <object-id>
|
|
37
|
+
ember catalog tables ember
|
|
38
|
+
ember catalog table ember <table>
|
|
39
|
+
ember warehouse usage
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Search returns up to 10 matches by default, with semantic meaning, verification, conflicts, and freshness.
|
|
43
|
+
Use `--refs` to anchor the search to known source or relation references. Search is bounded; refine the
|
|
44
|
+
question if results are incomplete. Table listings support `--next-token`, and table descriptions support
|
|
45
|
+
`--column-offset` for wide schemas. Internal audit logging continues without an exposed `audit` tool.
|
|
46
|
+
|
|
47
|
+
Remote OAuth starts with read access and requests additional scopes for changes. MCP calls validate the
|
|
48
|
+
session on every request. Oversized pages return an error; retry the same cursor with a smaller limit
|
|
49
|
+
(up to 100 rows for query results). After a timeout or cancellation, check operation status before
|
|
50
|
+
retrying a change. Cancelling the client request does not undo accepted warehouse work.
|
|
51
|
+
|
|
52
|
+
## Embrasure plugin
|
|
53
|
+
|
|
54
|
+
The public Embrasure plugin at `https://embrasure.ai/api/mcp` reuses this operator with
|
|
55
|
+
`createEmberMcpHttpHandler(resolveOperator, { surface: "embrasure" })`. It retains the Embrasure
|
|
56
|
+
name and enables additional catalog save/correct and ingestion inspect_table/edit_table actions.
|
|
57
|
+
These edits preview by default and use the existing workspace-authorized APIs. The standalone
|
|
58
|
+
Ember CLI and MCP schemas remain unchanged. Both surfaces keep SQL read-only.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const DEFAULT_API_URL = "https://api.embrasure.ai";
|
|
2
|
+
export declare const DEFAULT_WEB_URL = "https://app.embrasure.ai";
|
|
3
|
+
export type EmberConfig = {
|
|
4
|
+
apiBaseUrl?: string;
|
|
5
|
+
webBaseUrl?: string;
|
|
6
|
+
workspaceId?: string;
|
|
7
|
+
expiresAt?: string;
|
|
8
|
+
refreshExpiresAt?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
deviceId?: string;
|
|
11
|
+
};
|
|
12
|
+
export type Runtime = {
|
|
13
|
+
apiBaseUrl: string;
|
|
14
|
+
webBaseUrl: string;
|
|
15
|
+
workspaceId: string | null;
|
|
16
|
+
accessToken: string | null;
|
|
17
|
+
};
|
|
18
|
+
export declare function emberConfigPath(): string;
|
|
19
|
+
export declare function readEmberConfig(): Promise<EmberConfig>;
|
|
20
|
+
export declare function resolveRuntime(overrides?: Partial<Runtime>, env?: NodeJS.ProcessEnv): Promise<Runtime>;
|
|
21
|
+
export declare function browserLogin(runtime: Runtime, open?: boolean): Promise<Runtime>;
|
|
22
|
+
export declare function logout(): Promise<{
|
|
23
|
+
sessionRevoked: boolean;
|
|
24
|
+
}>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { EMBER_VERSION } from "./version.js";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { mkdir, open as openFile, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { homedir, hostname, platform } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
const SERVICE = "ai.embrasure.ember";
|
|
9
|
+
const ACCOUNT = "refresh_token";
|
|
10
|
+
const AUTH_REQUEST_TIMEOUT_MS = 20_000;
|
|
11
|
+
let memorySession = null;
|
|
12
|
+
let refreshInFlight = null;
|
|
13
|
+
export const DEFAULT_API_URL = "https://api.embrasure.ai";
|
|
14
|
+
export const DEFAULT_WEB_URL = "https://app.embrasure.ai";
|
|
15
|
+
export function emberConfigPath() { return join(homedir(), ".ember", "config.json"); }
|
|
16
|
+
export async function readEmberConfig() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await readFile(emberConfigPath(), "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
22
|
+
return {};
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function writeEmberConfig(config) {
|
|
27
|
+
const path = emberConfigPath();
|
|
28
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
29
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
export async function resolveRuntime(overrides = {}, env = process.env) {
|
|
32
|
+
const config = await readEmberConfig();
|
|
33
|
+
const apiBaseUrl = trim(overrides.apiBaseUrl ?? env.EMBER_API_BASE_URL ?? config.apiBaseUrl ?? DEFAULT_API_URL);
|
|
34
|
+
const webBaseUrl = trim(overrides.webBaseUrl ?? env.EMBER_WEB_BASE_URL ?? config.webBaseUrl ?? DEFAULT_WEB_URL);
|
|
35
|
+
const workspaceId = overrides.workspaceId ?? env.EMBER_WORKSPACE_ID ?? config.workspaceId ?? null;
|
|
36
|
+
let accessToken = overrides.accessToken ?? env.EMBER_API_TOKEN ?? null;
|
|
37
|
+
if (!overrides.accessToken && !env.EMBER_API_TOKEN) {
|
|
38
|
+
if (memorySession?.accessToken && memorySession.sessionId === config.sessionId && !expiresSoon(memorySession.expiresAt)) {
|
|
39
|
+
return { apiBaseUrl, webBaseUrl, workspaceId, accessToken: memorySession.accessToken };
|
|
40
|
+
}
|
|
41
|
+
const payload = await withRefreshLock(async () => {
|
|
42
|
+
const refreshToken = await readKeychain();
|
|
43
|
+
if (!refreshToken)
|
|
44
|
+
return null;
|
|
45
|
+
const refreshed = await refreshSession(apiBaseUrl, refreshToken);
|
|
46
|
+
if (typeof refreshed.access_token !== "string")
|
|
47
|
+
throw new Error("Ember login expired. Run `ember auth login`.");
|
|
48
|
+
await storeSession(await readEmberConfig(), refreshed, apiBaseUrl, webBaseUrl, workspaceId);
|
|
49
|
+
return refreshed;
|
|
50
|
+
});
|
|
51
|
+
if (payload) {
|
|
52
|
+
accessToken = typeof payload.access_token === "string" ? payload.access_token : null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { apiBaseUrl, webBaseUrl, workspaceId, accessToken };
|
|
56
|
+
}
|
|
57
|
+
async function withRefreshLock(action) {
|
|
58
|
+
const directory = dirname(emberConfigPath());
|
|
59
|
+
const lockPath = join(directory, "refresh.lock");
|
|
60
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
61
|
+
const deadline = Date.now() + 75_000;
|
|
62
|
+
while (true) {
|
|
63
|
+
try {
|
|
64
|
+
const handle = await openFile(lockPath, "wx", 0o600);
|
|
65
|
+
const owner = `${process.pid}:${randomUUID()}\n`;
|
|
66
|
+
try {
|
|
67
|
+
await handle.writeFile(owner);
|
|
68
|
+
return await action();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await handle.close();
|
|
72
|
+
const currentOwner = await readFile(lockPath, "utf8").catch(() => null);
|
|
73
|
+
if (currentOwner === owner)
|
|
74
|
+
await rm(lockPath, { force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST")
|
|
79
|
+
throw error;
|
|
80
|
+
try {
|
|
81
|
+
const lock = await stat(lockPath);
|
|
82
|
+
if (Date.now() - lock.mtimeMs > 30_000) {
|
|
83
|
+
await rm(lockPath, { force: true });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (lockError) {
|
|
88
|
+
if (!lockError || typeof lockError !== "object" || !("code" in lockError) || lockError.code !== "ENOENT")
|
|
89
|
+
throw lockError;
|
|
90
|
+
}
|
|
91
|
+
if (Date.now() >= deadline)
|
|
92
|
+
throw new Error("Timed out waiting for another Ember process to refresh authentication.");
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 75 + Math.floor(Math.random() * 75)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function browserLogin(runtime, open = true) {
|
|
98
|
+
const state = randomBytes(24).toString("base64url");
|
|
99
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
100
|
+
const challenge = createHash("sha256").update(verifier, "ascii").digest("base64url");
|
|
101
|
+
const config = await readEmberConfig();
|
|
102
|
+
const deviceId = config.deviceId ?? randomUUID();
|
|
103
|
+
const callback = await callbackServer(state);
|
|
104
|
+
const url = new URL("/cli-auth/complete", runtime.webBaseUrl);
|
|
105
|
+
for (const [key, value] of Object.entries({
|
|
106
|
+
state, api_base_url: runtime.apiBaseUrl, return_url: callback.returnUrl, code_challenge: challenge,
|
|
107
|
+
code_challenge_method: "S256", device_id: deviceId, device_name: hostname().slice(0, 200) || "This computer",
|
|
108
|
+
platform: platform(), client: "ember", client_version: EMBER_VERSION, workspace_id: runtime.workspaceId ?? undefined,
|
|
109
|
+
scope: "read write admin",
|
|
110
|
+
}))
|
|
111
|
+
if (value)
|
|
112
|
+
url.searchParams.set(key, value);
|
|
113
|
+
process.stderr.write(`Open to sign in:\n${url.toString()}\n`);
|
|
114
|
+
if (open)
|
|
115
|
+
await openBrowser(url.toString());
|
|
116
|
+
try {
|
|
117
|
+
const code = await callback.wait();
|
|
118
|
+
const response = await fetch(`${runtime.apiBaseUrl}/v1/auth/session/token`, {
|
|
119
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
120
|
+
body: JSON.stringify({ grant_type: "authorization_code", code, code_verifier: verifier, redirect_uri: callback.returnUrl }),
|
|
121
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
|
|
122
|
+
});
|
|
123
|
+
const payload = await response.json().catch(() => ({}));
|
|
124
|
+
if (!response.ok || typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string" || typeof payload.workspace_id !== "string") {
|
|
125
|
+
throw new Error(typeof payload.detail === "string" ? payload.detail : "Browser sign-in failed.");
|
|
126
|
+
}
|
|
127
|
+
await withRefreshLock(() => storeSession({ ...config, deviceId }, payload, runtime.apiBaseUrl, runtime.webBaseUrl, payload.workspace_id));
|
|
128
|
+
return { ...runtime, accessToken: payload.access_token, workspaceId: payload.workspace_id };
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await callback.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function logout() {
|
|
135
|
+
return withRefreshLock(async () => {
|
|
136
|
+
const config = await readEmberConfig();
|
|
137
|
+
const refreshToken = await readKeychain();
|
|
138
|
+
let sessionRevoked = false;
|
|
139
|
+
if (refreshToken) {
|
|
140
|
+
const response = await fetch(`${trim(config.apiBaseUrl ?? DEFAULT_API_URL)}/v1/auth/session/revoke`, {
|
|
141
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }),
|
|
142
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
|
|
143
|
+
}).catch(() => null);
|
|
144
|
+
sessionRevoked = response?.ok ?? false;
|
|
145
|
+
}
|
|
146
|
+
await deleteKeychain();
|
|
147
|
+
await rm(emberConfigPath(), { force: true });
|
|
148
|
+
return { sessionRevoked };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function storeSession(config, payload, apiBaseUrl, webBaseUrl, workspaceId) {
|
|
152
|
+
if (typeof payload.refresh_token === "string")
|
|
153
|
+
await writeKeychain(payload.refresh_token);
|
|
154
|
+
const sessionId = stringValue(payload.session_id) ?? config.sessionId;
|
|
155
|
+
const expiresAt = stringValue(payload.expires_at);
|
|
156
|
+
if (typeof payload.access_token === "string")
|
|
157
|
+
memorySession = { sessionId, accessToken: payload.access_token, expiresAt };
|
|
158
|
+
await writeEmberConfig({ ...config, apiBaseUrl, webBaseUrl, workspaceId: typeof payload.workspace_id === "string" ? payload.workspace_id : workspaceId ?? undefined, expiresAt, refreshExpiresAt: stringValue(payload.refresh_expires_at), sessionId });
|
|
159
|
+
}
|
|
160
|
+
async function refreshSession(apiBaseUrl, refreshToken) {
|
|
161
|
+
if (refreshInFlight)
|
|
162
|
+
return refreshInFlight;
|
|
163
|
+
refreshInFlight = (async () => {
|
|
164
|
+
let token = refreshToken;
|
|
165
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
166
|
+
const response = await fetch(`${apiBaseUrl}/v1/auth/session/refresh`, {
|
|
167
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: token }),
|
|
168
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
|
|
169
|
+
});
|
|
170
|
+
const payload = await response.json().catch(() => ({}));
|
|
171
|
+
if (response.ok && typeof payload.access_token === "string")
|
|
172
|
+
return payload;
|
|
173
|
+
if (attempt === 0) {
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
175
|
+
const rotated = await readKeychain();
|
|
176
|
+
if (rotated && rotated !== token) {
|
|
177
|
+
token = rotated;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw new Error("Ember login expired. Run `ember auth login`.");
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Ember login expired. Run `ember auth login`.");
|
|
184
|
+
})();
|
|
185
|
+
try {
|
|
186
|
+
return await refreshInFlight;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
refreshInFlight = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function expiresSoon(value) {
|
|
193
|
+
if (!value)
|
|
194
|
+
return true;
|
|
195
|
+
const expiresAt = Date.parse(value);
|
|
196
|
+
return !Number.isFinite(expiresAt) || expiresAt <= Date.now() + 60_000;
|
|
197
|
+
}
|
|
198
|
+
function stringValue(value) { return typeof value === "string" ? value : undefined; }
|
|
199
|
+
function trim(value) { return value.replace(/\/+$/, ""); }
|
|
200
|
+
async function callbackServer(state) {
|
|
201
|
+
let resolveCode;
|
|
202
|
+
let rejectCode;
|
|
203
|
+
const promise = new Promise((resolve, reject) => { resolveCode = resolve; rejectCode = reject; });
|
|
204
|
+
const server = createServer((request, response) => {
|
|
205
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
206
|
+
const code = url.searchParams.get("code");
|
|
207
|
+
if (url.pathname !== "/callback") {
|
|
208
|
+
response.writeHead(404).end("Not found.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (request.method !== "GET" || url.searchParams.get("state") !== state || !code) {
|
|
212
|
+
response.writeHead(400).end("Ember sign-in failed. Return to your terminal.");
|
|
213
|
+
rejectCode(new Error("Browser sign-in returned an invalid callback."));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
response.writeHead(200, { "Content-Type": "text/plain" }).end("Ember is signed in. You can close this tab.");
|
|
217
|
+
resolveCode(code);
|
|
218
|
+
});
|
|
219
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
|
|
220
|
+
const address = server.address();
|
|
221
|
+
if (!address || typeof address === "string")
|
|
222
|
+
throw new Error("Could not start login callback.");
|
|
223
|
+
const timeout = setTimeout(() => rejectCode(new Error("Timed out waiting for browser sign-in.")), 180_000);
|
|
224
|
+
timeout.unref();
|
|
225
|
+
return { returnUrl: `http://127.0.0.1:${address.port}/callback`, wait: () => promise, close: async () => { clearTimeout(timeout); await new Promise((resolve) => server.close(() => resolve())); } };
|
|
226
|
+
}
|
|
227
|
+
function openBrowser(url) {
|
|
228
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
229
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
230
|
+
return run(command, args).catch(() => undefined);
|
|
231
|
+
}
|
|
232
|
+
async function readKeychain() {
|
|
233
|
+
if (process.platform === "darwin")
|
|
234
|
+
return run("ruby", ["-e", MAC_KEYCHAIN_SCRIPT, "read", SERVICE, ACCOUNT]).then((v) => v.trim() || null).catch(() => null);
|
|
235
|
+
if (process.platform === "linux")
|
|
236
|
+
return run("secret-tool", ["lookup", "service", SERVICE, "account", ACCOUNT]).then((v) => v.trim() || null).catch(() => null);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
async function writeKeychain(value) {
|
|
240
|
+
memorySession = null;
|
|
241
|
+
if (process.platform === "darwin") {
|
|
242
|
+
await run("ruby", ["-e", MAC_KEYCHAIN_SCRIPT, "write", SERVICE, ACCOUNT], value);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (process.platform === "linux") {
|
|
246
|
+
await run("secret-tool", ["clear", "service", SERVICE, "account", ACCOUNT]).catch(() => undefined);
|
|
247
|
+
await run("secret-tool", ["store", "--label=Ember CLI auth", "service", SERVICE, "account", ACCOUNT], value);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
throw new Error("Ember browser login requires macOS Keychain or Linux Secret Service. Use EMBER_API_TOKEN on this platform.");
|
|
251
|
+
}
|
|
252
|
+
async function deleteKeychain() {
|
|
253
|
+
memorySession = null;
|
|
254
|
+
if (process.platform === "darwin")
|
|
255
|
+
await run("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT]).catch(() => undefined);
|
|
256
|
+
if (process.platform === "linux")
|
|
257
|
+
await run("secret-tool", ["clear", "service", SERVICE, "account", ACCOUNT]).catch(() => undefined);
|
|
258
|
+
}
|
|
259
|
+
function run(command, args, input) {
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
262
|
+
let stdout = "";
|
|
263
|
+
let stderr = "";
|
|
264
|
+
const timeout = setTimeout(() => { child.kill(); reject(new Error(`${command} timed out.`)); }, 5_000);
|
|
265
|
+
timeout.unref();
|
|
266
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
267
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
268
|
+
child.once("error", (error) => { clearTimeout(timeout); reject(error); });
|
|
269
|
+
child.once("close", (code) => { clearTimeout(timeout); code === 0 ? resolve(stdout) : reject(new Error(stderr.trim() || `${command} failed.`)); });
|
|
270
|
+
child.stdin.end(input);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const MAC_KEYCHAIN_SCRIPT = String.raw `
|
|
274
|
+
require 'fiddle'
|
|
275
|
+
require 'fiddle/import'
|
|
276
|
+
module Security
|
|
277
|
+
extend Fiddle::Importer
|
|
278
|
+
dlload '/System/Library/Frameworks/Security.framework/Security'
|
|
279
|
+
extern 'int SecKeychainAddGenericPassword(void*, unsigned int, void*, unsigned int, void*, unsigned int, void*, void*)'
|
|
280
|
+
extern 'int SecKeychainFindGenericPassword(void*, unsigned int, void*, unsigned int, void*, void*, void*, void*)'
|
|
281
|
+
extern 'int SecKeychainItemModifyAttributesAndData(void*, void*, unsigned int, void*)'
|
|
282
|
+
extern 'int SecKeychainItemFreeContent(void*, void*)'
|
|
283
|
+
end
|
|
284
|
+
op, service, account = ARGV
|
|
285
|
+
if op == 'write'
|
|
286
|
+
secret = STDIN.read
|
|
287
|
+
item_ref = [0].pack('J')
|
|
288
|
+
found = Security.SecKeychainFindGenericPassword(0, service.bytesize, service, account.bytesize, account, 0, 0, item_ref)
|
|
289
|
+
status = if found == 0
|
|
290
|
+
Security.SecKeychainItemModifyAttributesAndData(item_ref.unpack1('J'), 0, secret.bytesize, secret)
|
|
291
|
+
else
|
|
292
|
+
Security.SecKeychainAddGenericPassword(0, service.bytesize, service, account.bytesize, account, secret.bytesize, secret, 0)
|
|
293
|
+
end
|
|
294
|
+
abort("Keychain write failed: #{status}") unless status == 0
|
|
295
|
+
elsif op == 'read'
|
|
296
|
+
length = [0].pack('L!')
|
|
297
|
+
data = [0].pack('J')
|
|
298
|
+
status = Security.SecKeychainFindGenericPassword(0, service.bytesize, service, account.bytesize, account, length, data, 0)
|
|
299
|
+
exit 0 unless status == 0
|
|
300
|
+
byte_count = length.unpack1('L!')
|
|
301
|
+
pointer = data.unpack1('J')
|
|
302
|
+
print Fiddle::Pointer.new(pointer)[0, byte_count]
|
|
303
|
+
Security.SecKeychainItemFreeContent(0, pointer)
|
|
304
|
+
end
|
|
305
|
+
`;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { EmbrasureApiClient, UnifiedContextObject } from "@embrasure/api-client";
|
|
2
|
+
import type { CatalogInput } from "./types.js";
|
|
3
|
+
export declare function readCatalog(client: EmbrasureApiClient, workspaceId: string, input: CatalogInput): Promise<{
|
|
4
|
+
ok: boolean;
|
|
5
|
+
action: string;
|
|
6
|
+
data: {
|
|
7
|
+
database: string;
|
|
8
|
+
next_token: string | null;
|
|
9
|
+
tables: {
|
|
10
|
+
column_count: number;
|
|
11
|
+
database: string;
|
|
12
|
+
name: string;
|
|
13
|
+
catalog_name: string;
|
|
14
|
+
schema_name: string;
|
|
15
|
+
table_name: string;
|
|
16
|
+
logical_schema?: string | null;
|
|
17
|
+
logical_table?: string | null;
|
|
18
|
+
source_schema?: string | null;
|
|
19
|
+
source_table?: string | null;
|
|
20
|
+
query_name: string;
|
|
21
|
+
relation_type?: "table" | "view";
|
|
22
|
+
created_at?: string | null;
|
|
23
|
+
updated_at?: string | null;
|
|
24
|
+
}[];
|
|
25
|
+
object_id?: undefined;
|
|
26
|
+
object?: undefined;
|
|
27
|
+
related?: undefined;
|
|
28
|
+
edges?: undefined;
|
|
29
|
+
join_plans?: undefined;
|
|
30
|
+
evidence?: undefined;
|
|
31
|
+
evidence_links?: undefined;
|
|
32
|
+
observations?: undefined;
|
|
33
|
+
conflicts?: undefined;
|
|
34
|
+
freshness?: undefined;
|
|
35
|
+
coverage?: undefined;
|
|
36
|
+
warnings?: undefined;
|
|
37
|
+
query_id?: undefined;
|
|
38
|
+
matches?: undefined;
|
|
39
|
+
meaning_candidates?: undefined;
|
|
40
|
+
memories?: undefined;
|
|
41
|
+
truncated?: undefined;
|
|
42
|
+
exhaustive?: undefined;
|
|
43
|
+
};
|
|
44
|
+
next_action: string;
|
|
45
|
+
} | {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
action: string;
|
|
48
|
+
data: {
|
|
49
|
+
columns: import("@embrasure/api-client").WarehouseTableColumnResponse[];
|
|
50
|
+
column_count: number;
|
|
51
|
+
column_offset: number;
|
|
52
|
+
next_column_offset: number | null;
|
|
53
|
+
database: string;
|
|
54
|
+
name: string;
|
|
55
|
+
catalog_name: string;
|
|
56
|
+
schema_name: string;
|
|
57
|
+
table_name: string;
|
|
58
|
+
logical_schema?: string | null;
|
|
59
|
+
logical_table?: string | null;
|
|
60
|
+
source_schema?: string | null;
|
|
61
|
+
source_table?: string | null;
|
|
62
|
+
query_name: string;
|
|
63
|
+
relation_type?: "table" | "view";
|
|
64
|
+
created_at?: string | null;
|
|
65
|
+
updated_at?: string | null;
|
|
66
|
+
next_token?: undefined;
|
|
67
|
+
tables?: undefined;
|
|
68
|
+
object_id?: undefined;
|
|
69
|
+
object?: undefined;
|
|
70
|
+
related?: undefined;
|
|
71
|
+
edges?: undefined;
|
|
72
|
+
join_plans?: undefined;
|
|
73
|
+
evidence?: undefined;
|
|
74
|
+
evidence_links?: undefined;
|
|
75
|
+
observations?: undefined;
|
|
76
|
+
conflicts?: undefined;
|
|
77
|
+
freshness?: undefined;
|
|
78
|
+
coverage?: undefined;
|
|
79
|
+
warnings?: undefined;
|
|
80
|
+
query_id?: undefined;
|
|
81
|
+
matches?: undefined;
|
|
82
|
+
meaning_candidates?: undefined;
|
|
83
|
+
memories?: undefined;
|
|
84
|
+
truncated?: undefined;
|
|
85
|
+
exhaustive?: undefined;
|
|
86
|
+
};
|
|
87
|
+
next_action: string;
|
|
88
|
+
} | {
|
|
89
|
+
ok: boolean;
|
|
90
|
+
action: string;
|
|
91
|
+
data: {
|
|
92
|
+
object_id: string;
|
|
93
|
+
object: UnifiedContextObject;
|
|
94
|
+
related: {
|
|
95
|
+
object_id: string;
|
|
96
|
+
kind: string;
|
|
97
|
+
stable_key: string;
|
|
98
|
+
title: string;
|
|
99
|
+
summary: string | undefined;
|
|
100
|
+
summary_truncated: boolean;
|
|
101
|
+
connector_id: string | null | undefined;
|
|
102
|
+
source_id: string | null | undefined;
|
|
103
|
+
verification_status: string;
|
|
104
|
+
truth_kind: import("@embrasure/api-client").ContextTruthKind | undefined;
|
|
105
|
+
authority: Record<string, unknown> | undefined;
|
|
106
|
+
authority_score: number | null | undefined;
|
|
107
|
+
confidence: number;
|
|
108
|
+
updated_at: string | null | undefined;
|
|
109
|
+
valid_until: string | null | undefined;
|
|
110
|
+
match: {
|
|
111
|
+
streams?: string[];
|
|
112
|
+
} | null | undefined;
|
|
113
|
+
}[];
|
|
114
|
+
edges: Record<string, import("@embrasure/api-client").UnifiedContextEdge>;
|
|
115
|
+
join_plans: import("@embrasure/api-client").ContextJoinPlan[] | undefined;
|
|
116
|
+
evidence: Record<string, import("@embrasure/api-client").ContextEvidence>;
|
|
117
|
+
evidence_links: import("@embrasure/api-client").ContextEvidenceLink[];
|
|
118
|
+
observations: import("@embrasure/api-client").ContextObservation[];
|
|
119
|
+
conflicts: Record<string, unknown>[] | undefined;
|
|
120
|
+
freshness: Record<string, unknown> | undefined;
|
|
121
|
+
coverage: import("@embrasure/api-client").ContextCoverageSummary;
|
|
122
|
+
warnings: string[] | undefined;
|
|
123
|
+
database?: undefined;
|
|
124
|
+
next_token?: undefined;
|
|
125
|
+
tables?: undefined;
|
|
126
|
+
query_id?: undefined;
|
|
127
|
+
matches?: undefined;
|
|
128
|
+
meaning_candidates?: undefined;
|
|
129
|
+
memories?: undefined;
|
|
130
|
+
truncated?: undefined;
|
|
131
|
+
exhaustive?: undefined;
|
|
132
|
+
};
|
|
133
|
+
next_action: string;
|
|
134
|
+
} | {
|
|
135
|
+
ok: boolean;
|
|
136
|
+
action: string;
|
|
137
|
+
data: {
|
|
138
|
+
query_id: string;
|
|
139
|
+
matches: {
|
|
140
|
+
object_id: string;
|
|
141
|
+
kind: string;
|
|
142
|
+
stable_key: string;
|
|
143
|
+
title: string;
|
|
144
|
+
summary: string | undefined;
|
|
145
|
+
summary_truncated: boolean;
|
|
146
|
+
connector_id: string | null | undefined;
|
|
147
|
+
source_id: string | null | undefined;
|
|
148
|
+
verification_status: string;
|
|
149
|
+
truth_kind: import("@embrasure/api-client").ContextTruthKind | undefined;
|
|
150
|
+
authority: Record<string, unknown> | undefined;
|
|
151
|
+
authority_score: number | null | undefined;
|
|
152
|
+
confidence: number;
|
|
153
|
+
updated_at: string | null | undefined;
|
|
154
|
+
valid_until: string | null | undefined;
|
|
155
|
+
match: {
|
|
156
|
+
streams?: string[];
|
|
157
|
+
} | null | undefined;
|
|
158
|
+
}[];
|
|
159
|
+
meaning_candidates: Record<string, unknown>[];
|
|
160
|
+
memories: Record<string, unknown>[];
|
|
161
|
+
conflicts: Record<string, unknown>[] | undefined;
|
|
162
|
+
freshness: Record<string, unknown> | undefined;
|
|
163
|
+
coverage: import("@embrasure/api-client").ContextCoverageSummary;
|
|
164
|
+
warnings: string[] | undefined;
|
|
165
|
+
truncated: boolean;
|
|
166
|
+
exhaustive: boolean;
|
|
167
|
+
database?: undefined;
|
|
168
|
+
next_token?: undefined;
|
|
169
|
+
tables?: undefined;
|
|
170
|
+
object_id?: undefined;
|
|
171
|
+
object?: undefined;
|
|
172
|
+
related?: undefined;
|
|
173
|
+
edges?: undefined;
|
|
174
|
+
join_plans?: undefined;
|
|
175
|
+
evidence?: undefined;
|
|
176
|
+
evidence_links?: undefined;
|
|
177
|
+
observations?: undefined;
|
|
178
|
+
};
|
|
179
|
+
next_action: string;
|
|
180
|
+
}>;
|