@fragno-dev/project-connector-fragment 0.0.0-stage → 0.0.1
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/LICENSE.md +16 -0
- package/README.md +285 -2
- package/bin/run.js +4 -0
- package/dist/browser/client/react.d.ts +174 -0
- package/dist/browser/client/react.d.ts.map +1 -0
- package/dist/browser/client/react.js +195 -0
- package/dist/browser/client/react.js.map +1 -0
- package/dist/browser/client/solid.d.ts +177 -0
- package/dist/browser/client/solid.d.ts.map +1 -0
- package/dist/browser/client/solid.js +130 -0
- package/dist/browser/client/solid.js.map +1 -0
- package/dist/browser/client/svelte.d.ts +177 -0
- package/dist/browser/client/svelte.d.ts.map +1 -0
- package/dist/browser/client/svelte.js +132 -0
- package/dist/browser/client/svelte.js.map +1 -0
- package/dist/browser/client/vanilla.d.ts +204 -0
- package/dist/browser/client/vanilla.d.ts.map +1 -0
- package/dist/browser/client/vanilla.js +160 -0
- package/dist/browser/client/vanilla.js.map +1 -0
- package/dist/browser/client/vue.d.ts +177 -0
- package/dist/browser/client/vue.d.ts.map +1 -0
- package/dist/browser/client/vue.js +128 -0
- package/dist/browser/client/vue.js.map +1 -0
- package/dist/browser/client-BCzNDSlW.d.ts +711 -0
- package/dist/browser/client-BCzNDSlW.d.ts.map +1 -0
- package/dist/browser/index.d.ts +504 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/src-CSPQuoKz.js +2286 -0
- package/dist/browser/src-CSPQuoKz.js.map +1 -0
- package/dist/cli/authorization-browser.js +20 -0
- package/dist/cli/authorization-browser.js.map +1 -0
- package/dist/cli/cli.d.ts +6 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +258 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/definition.js +13 -0
- package/dist/cli/definition.js.map +1 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/project-connector-client.js +235 -0
- package/dist/cli/project-connector-client.js.map +1 -0
- package/dist/cli/project-connector-contracts.js +125 -0
- package/dist/cli/project-connector-contracts.js.map +1 -0
- package/dist/cli/routes.js +449 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/schema.js +18 -0
- package/dist/cli/schema.js.map +1 -0
- package/dist/node/definition.d.ts +170 -0
- package/dist/node/definition.d.ts.map +1 -0
- package/dist/node/definition.js +13 -0
- package/dist/node/definition.js.map +1 -0
- package/dist/node/index.d.ts +456 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +36 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/project-connector-client.d.ts +13 -0
- package/dist/node/project-connector-client.d.ts.map +1 -0
- package/dist/node/project-connector-client.js +235 -0
- package/dist/node/project-connector-client.js.map +1 -0
- package/dist/node/project-connector-contracts.d.ts +179 -0
- package/dist/node/project-connector-contracts.d.ts.map +1 -0
- package/dist/node/project-connector-contracts.js +125 -0
- package/dist/node/project-connector-contracts.js.map +1 -0
- package/dist/node/routes.d.ts +307 -0
- package/dist/node/routes.d.ts.map +1 -0
- package/dist/node/routes.js +449 -0
- package/dist/node/routes.js.map +1 -0
- package/dist/node/schema.d.ts +49 -0
- package/dist/node/schema.d.ts.map +1 -0
- package/dist/node/schema.js +18 -0
- package/dist/node/schema.js.map +1 -0
- package/dist/node/testing/project-connector-test-gateway.d.ts +63 -0
- package/dist/node/testing/project-connector-test-gateway.d.ts.map +1 -0
- package/dist/node/testing/project-connector-test-gateway.js +370 -0
- package/dist/node/testing/project-connector-test-gateway.js.map +1 -0
- package/package.json +111 -3
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { projectConnectorConnectionSchema, projectConnectorHttpUrlSchema } from "./project-connector-contracts.js";
|
|
3
|
+
import { createProjectConnectorFragment } from "./index.js";
|
|
4
|
+
import { resolveAuthorizationBrowserLaunch } from "./authorization-browser.js";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { setTimeout } from "node:timers/promises";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { parseArgs, parseEnv } from "node:util";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { migrate } from "@fragno-dev/db";
|
|
15
|
+
//#region src/cli/cli.ts
|
|
16
|
+
const USAGE = `fragno-project-connector <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
check Verify gateway reachability and project-key authentication
|
|
20
|
+
connect <service> Authorize an account, wait, and verify its read-only profile
|
|
21
|
+
status <request-id> Refresh a saved authorization request
|
|
22
|
+
accounts List this user's locally verified account bindings
|
|
23
|
+
profile <account-id> Read a connected account's provider profile
|
|
24
|
+
execute <account-id> <action> Execute an action (may have side effects)
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--env-file <path> Load OOMOL_CONNECTOR_BASE_URL / OOMOL_PROJECT_API_KEY
|
|
28
|
+
--user-id <id> Product user for the local CLI (default: cli-user)
|
|
29
|
+
--connection-name <name> Explicit connection name for connect (default: work)
|
|
30
|
+
--provider-config-id <id> Select a provider config instead of resolving the service
|
|
31
|
+
--data-dir <path> SQLite storage (default: ~/.fragno/project-connector-fragment)
|
|
32
|
+
--port <port> Loopback OAuth return port (default: 3930)
|
|
33
|
+
--timeout-ms <ms> Maximum OAuth wait (default: 600000)
|
|
34
|
+
--input <json> Action input object (default: {})
|
|
35
|
+
--no-open Print the authorization link without opening it
|
|
36
|
+
--beep Play a sound when browser authorization is needed
|
|
37
|
+
-h, --help Show help
|
|
38
|
+
|
|
39
|
+
The key remains in this process. Commands exercise the fragment's real HTTP handler
|
|
40
|
+
and persist verified bindings in SQLite. Do not expose this local CLI as an app server.`;
|
|
41
|
+
function openAuthorizationBrowser(url) {
|
|
42
|
+
const { command, args } = resolveAuthorizationBrowserLaunch(process.platform, url);
|
|
43
|
+
const child = spawn(command, args, {
|
|
44
|
+
stdio: "ignore",
|
|
45
|
+
detached: true,
|
|
46
|
+
shell: false,
|
|
47
|
+
windowsHide: true
|
|
48
|
+
});
|
|
49
|
+
child.on("error", () => {
|
|
50
|
+
console.error("Project Connector CLI: could not open browser; open the printed URL manually.");
|
|
51
|
+
});
|
|
52
|
+
child.unref();
|
|
53
|
+
}
|
|
54
|
+
function playAuthorizationBeep() {
|
|
55
|
+
process.stdout.write("\x07");
|
|
56
|
+
if (process.platform === "darwin") spawn("afplay", ["/System/Library/Sounds/Glass.aiff"], { stdio: "ignore" }).on("error", () => void 0);
|
|
57
|
+
}
|
|
58
|
+
/** Runs local connector commands through the same fragment HTTP boundary as an application. */
|
|
59
|
+
async function runProjectConnectorCli(argv, logger = console) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = parseArgs({
|
|
62
|
+
args: argv[0] === "--" ? argv.slice(1) : argv,
|
|
63
|
+
allowPositionals: true,
|
|
64
|
+
options: {
|
|
65
|
+
"env-file": {
|
|
66
|
+
type: "string",
|
|
67
|
+
default: ""
|
|
68
|
+
},
|
|
69
|
+
"user-id": {
|
|
70
|
+
type: "string",
|
|
71
|
+
default: "cli-user"
|
|
72
|
+
},
|
|
73
|
+
"connection-name": {
|
|
74
|
+
type: "string",
|
|
75
|
+
default: "work"
|
|
76
|
+
},
|
|
77
|
+
"provider-config-id": {
|
|
78
|
+
type: "string",
|
|
79
|
+
default: ""
|
|
80
|
+
},
|
|
81
|
+
"data-dir": {
|
|
82
|
+
type: "string",
|
|
83
|
+
default: path.join(homedir(), ".fragno", "project-connector-fragment")
|
|
84
|
+
},
|
|
85
|
+
port: {
|
|
86
|
+
type: "string",
|
|
87
|
+
default: "3930"
|
|
88
|
+
},
|
|
89
|
+
"timeout-ms": {
|
|
90
|
+
type: "string",
|
|
91
|
+
default: "600000"
|
|
92
|
+
},
|
|
93
|
+
input: {
|
|
94
|
+
type: "string",
|
|
95
|
+
default: "{}"
|
|
96
|
+
},
|
|
97
|
+
"no-open": {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
default: false
|
|
100
|
+
},
|
|
101
|
+
beep: {
|
|
102
|
+
type: "boolean",
|
|
103
|
+
default: false
|
|
104
|
+
},
|
|
105
|
+
help: {
|
|
106
|
+
type: "boolean",
|
|
107
|
+
short: "h",
|
|
108
|
+
default: false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const [command, ...args] = parsed.positionals;
|
|
113
|
+
if (parsed.values.help || !command) {
|
|
114
|
+
logger.log(USAGE);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
if (![
|
|
118
|
+
"check",
|
|
119
|
+
"connect",
|
|
120
|
+
"status",
|
|
121
|
+
"accounts",
|
|
122
|
+
"profile",
|
|
123
|
+
"execute"
|
|
124
|
+
].includes(command)) throw new Error("Project Connector CLI: unknown command");
|
|
125
|
+
if ([
|
|
126
|
+
"connect",
|
|
127
|
+
"status",
|
|
128
|
+
"profile"
|
|
129
|
+
].includes(command) && args.length !== 1 || command === "execute" && args.length !== 2 || ["check", "accounts"].includes(command) && args.length !== 0) throw new Error("Project Connector CLI: invalid command arguments; use --help");
|
|
130
|
+
const envFile = parsed.values["env-file"];
|
|
131
|
+
const env = {
|
|
132
|
+
...envFile ? parseEnv(await readFile(envFile, "utf8")) : {},
|
|
133
|
+
...process.env
|
|
134
|
+
};
|
|
135
|
+
const baseUrl = projectConnectorHttpUrlSchema.parse(env["OOMOL_CONNECTOR_BASE_URL"]);
|
|
136
|
+
const apiKey = z.string().min(1).parse(env["OOMOL_PROJECT_API_KEY"]);
|
|
137
|
+
const externalUserId = z.string().min(1).parse(parsed.values["user-id"]);
|
|
138
|
+
const port = z.coerce.number().int().min(1).max(65535).parse(parsed.values.port);
|
|
139
|
+
const timeoutMs = z.coerce.number().int().positive().max(6e5).parse(parsed.values["timeout-ms"]);
|
|
140
|
+
const returnUri = `http://127.0.0.1:${port}/connected`;
|
|
141
|
+
const dataDir = parsed.values["data-dir"];
|
|
142
|
+
await mkdir(dataDir, {
|
|
143
|
+
recursive: true,
|
|
144
|
+
mode: 448
|
|
145
|
+
});
|
|
146
|
+
process.env["FRAGNO_DATA_DIR"] = dataDir;
|
|
147
|
+
const fragment = createProjectConnectorFragment({
|
|
148
|
+
baseUrl,
|
|
149
|
+
apiKey,
|
|
150
|
+
catalogApiKey: env["OOMOL_CONNECTOR_CATALOG_API_KEY"]?.trim() || null,
|
|
151
|
+
getExternalUserId: () => externalUserId,
|
|
152
|
+
allowedReturnUrls: (url) => url.toString() === returnUri
|
|
153
|
+
}, { databaseNamespace: "project-connector-fragment" });
|
|
154
|
+
try {
|
|
155
|
+
await migrate(fragment);
|
|
156
|
+
async function call(method, route, body = null) {
|
|
157
|
+
const response = await fragment.handler(new Request(`http://localhost${fragment.mountRoute}${route}`, {
|
|
158
|
+
method,
|
|
159
|
+
headers: { "content-type": "application/json" },
|
|
160
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {}
|
|
161
|
+
}));
|
|
162
|
+
if (!response) throw new Error("Project Connector CLI: fragment did not handle the request");
|
|
163
|
+
const result = await response.json();
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
const failure = z.object({
|
|
166
|
+
code: z.string(),
|
|
167
|
+
message: z.string()
|
|
168
|
+
}).parse(result);
|
|
169
|
+
throw new Error(`Project Connector CLI: ${failure.code}: ${failure.message}`);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
switch (command) {
|
|
174
|
+
case "check":
|
|
175
|
+
logger.log(JSON.stringify(await call("GET", "/status"), null, 2));
|
|
176
|
+
break;
|
|
177
|
+
case "status":
|
|
178
|
+
logger.log(JSON.stringify(await call("POST", `/connection-requests/${encodeURIComponent(args[0])}/refresh`), null, 2));
|
|
179
|
+
break;
|
|
180
|
+
case "accounts": {
|
|
181
|
+
let cursor = null;
|
|
182
|
+
do {
|
|
183
|
+
const page = z.object({
|
|
184
|
+
accounts: z.array(z.unknown()),
|
|
185
|
+
cursor: z.string().nullable()
|
|
186
|
+
}).parse(await call("GET", `/accounts${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
|
|
187
|
+
for (const account of page.accounts) logger.log(JSON.stringify(account, null, 2));
|
|
188
|
+
cursor = page.cursor;
|
|
189
|
+
} while (cursor);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "profile":
|
|
193
|
+
logger.log(JSON.stringify(await call("GET", `/accounts/${encodeURIComponent(args[0])}/profile`), null, 2));
|
|
194
|
+
break;
|
|
195
|
+
case "execute": {
|
|
196
|
+
const input = z.record(z.string(), z.unknown()).parse(JSON.parse(parsed.values.input));
|
|
197
|
+
logger.log(JSON.stringify(await call("POST", `/accounts/${encodeURIComponent(args[0])}/actions/${encodeURIComponent(args[1])}`, { input }), null, 2));
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "connect": {
|
|
201
|
+
const callback = createServer((req, res) => {
|
|
202
|
+
if (req.url?.split("?")[0] !== "/connected") {
|
|
203
|
+
res.writeHead(404).end("Not found");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end("Return to the terminal. It will verify your connection securely.");
|
|
207
|
+
});
|
|
208
|
+
await new Promise((resolve, reject) => {
|
|
209
|
+
callback.once("error", reject);
|
|
210
|
+
callback.listen(port, "127.0.0.1", resolve);
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
const providerConfigId = parsed.values["provider-config-id"];
|
|
214
|
+
const connection = projectConnectorConnectionSchema.parse(await call("POST", "/connection-requests", {
|
|
215
|
+
...providerConfigId ? { providerConfigId } : { service: args[0] },
|
|
216
|
+
connectionName: parsed.values["connection-name"],
|
|
217
|
+
returnUri
|
|
218
|
+
}));
|
|
219
|
+
logger.log(`Connection request: ${connection.id}\nAuthorize in your browser:\n${connection.authorizationUrl}`);
|
|
220
|
+
if (parsed.values.beep) playAuthorizationBeep();
|
|
221
|
+
if (!parsed.values["no-open"]) openAuthorizationBrowser(connection.authorizationUrl);
|
|
222
|
+
const deadline = Date.now() + timeoutMs;
|
|
223
|
+
while (Date.now() < deadline) {
|
|
224
|
+
const refreshed = projectConnectorConnectionSchema.parse(await call("POST", `/connection-requests/${encodeURIComponent(connection.id)}/refresh`));
|
|
225
|
+
if (refreshed.state.status === "connected") {
|
|
226
|
+
logger.log(`Connected account: ${refreshed.state.connectedAccountId}`);
|
|
227
|
+
logger.log(JSON.stringify(await call("GET", `/accounts/${encodeURIComponent(refreshed.state.connectedAccountId)}/profile`), null, 2));
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
if (refreshed.state.status !== "initiated") throw new Error(`Project Connector CLI: authorization ${refreshed.state.status}`);
|
|
231
|
+
await setTimeout(Math.min(2e3, Math.max(0, deadline - Date.now())));
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`Project Connector CLI: authorization timed out; resume with status ${connection.id}`);
|
|
234
|
+
} finally {
|
|
235
|
+
callback.closeAllConnections();
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
callback.close((error) => {
|
|
238
|
+
if (error) reject(error);
|
|
239
|
+
else resolve();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
} finally {
|
|
247
|
+
await fragment.$internal.deps.databaseAdapter.close();
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error(error instanceof z.ZodError ? "Project Connector CLI: invalid configuration or input; check environment variables and --help" : error instanceof Error ? error.message : "Project Connector CLI: command failed");
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = await runProjectConnectorCli(process.argv.slice(2));
|
|
255
|
+
//#endregion
|
|
256
|
+
export { runProjectConnectorCli };
|
|
257
|
+
|
|
258
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":["sleep"],"sources":["../../src/cli/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn } from \"node:child_process\";\nimport { mkdir, readFile } from \"node:fs/promises\";\nimport { createServer } from \"node:http\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { pathToFileURL } from \"node:url\";\nimport { parseArgs, parseEnv } from \"node:util\";\n\nimport { z } from \"zod\";\n\nimport { migrate } from \"@fragno-dev/db\";\n\nimport { createProjectConnectorFragment } from \"../index\";\nimport {\n projectConnectorConnectionSchema,\n projectConnectorHttpUrlSchema,\n} from \"../project-connector-contracts\";\nimport { resolveAuthorizationBrowserLaunch } from \"./authorization-browser\";\n\nconst USAGE = `fragno-project-connector <command> [options]\n\nCommands:\n check Verify gateway reachability and project-key authentication\n connect <service> Authorize an account, wait, and verify its read-only profile\n status <request-id> Refresh a saved authorization request\n accounts List this user's locally verified account bindings\n profile <account-id> Read a connected account's provider profile\n execute <account-id> <action> Execute an action (may have side effects)\n\nOptions:\n --env-file <path> Load OOMOL_CONNECTOR_BASE_URL / OOMOL_PROJECT_API_KEY\n --user-id <id> Product user for the local CLI (default: cli-user)\n --connection-name <name> Explicit connection name for connect (default: work)\n --provider-config-id <id> Select a provider config instead of resolving the service\n --data-dir <path> SQLite storage (default: ~/.fragno/project-connector-fragment)\n --port <port> Loopback OAuth return port (default: 3930)\n --timeout-ms <ms> Maximum OAuth wait (default: 600000)\n --input <json> Action input object (default: {})\n --no-open Print the authorization link without opening it\n --beep Play a sound when browser authorization is needed\n -h, --help Show help\n\nThe key remains in this process. Commands exercise the fragment's real HTTP handler\nand persist verified bindings in SQLite. Do not expose this local CLI as an app server.`;\n\nfunction openAuthorizationBrowser(url: string) {\n const { command, args } = resolveAuthorizationBrowserLaunch(process.platform, url);\n const child = spawn(command, args, {\n stdio: \"ignore\",\n detached: true,\n shell: false,\n windowsHide: true,\n });\n child.on(\"error\", () => {\n console.error(\"Project Connector CLI: could not open browser; open the printed URL manually.\");\n });\n child.unref();\n}\n\nfunction playAuthorizationBeep() {\n process.stdout.write(\"\\u0007\");\n if (process.platform === \"darwin\") {\n const child = spawn(\"afplay\", [\"/System/Library/Sounds/Glass.aiff\"], { stdio: \"ignore\" });\n child.on(\"error\", () => undefined);\n }\n}\n\n/** Runs local connector commands through the same fragment HTTP boundary as an application. */\nexport async function runProjectConnectorCli(\n argv: string[],\n logger: Pick<Console, \"log\" | \"error\"> = console,\n): Promise<number> {\n try {\n const parsed = parseArgs({\n args: argv[0] === \"--\" ? argv.slice(1) : argv,\n allowPositionals: true,\n options: {\n \"env-file\": { type: \"string\", default: \"\" },\n \"user-id\": { type: \"string\", default: \"cli-user\" },\n \"connection-name\": { type: \"string\", default: \"work\" },\n \"provider-config-id\": { type: \"string\", default: \"\" },\n \"data-dir\": {\n type: \"string\",\n default: path.join(homedir(), \".fragno\", \"project-connector-fragment\"),\n },\n port: { type: \"string\", default: \"3930\" },\n \"timeout-ms\": { type: \"string\", default: \"600000\" },\n input: { type: \"string\", default: \"{}\" },\n \"no-open\": { type: \"boolean\", default: false },\n beep: { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n const [command, ...args] = parsed.positionals;\n if (parsed.values.help || !command) {\n logger.log(USAGE);\n return 0;\n }\n if (![\"check\", \"connect\", \"status\", \"accounts\", \"profile\", \"execute\"].includes(command)) {\n throw new Error(\"Project Connector CLI: unknown command\");\n }\n if (\n ([\"connect\", \"status\", \"profile\"].includes(command) && args.length !== 1) ||\n (command === \"execute\" && args.length !== 2) ||\n ([\"check\", \"accounts\"].includes(command) && args.length !== 0)\n ) {\n throw new Error(\"Project Connector CLI: invalid command arguments; use --help\");\n }\n const envFile = parsed.values[\"env-file\"];\n const env = { ...(envFile ? parseEnv(await readFile(envFile, \"utf8\")) : {}), ...process.env };\n const baseUrl = projectConnectorHttpUrlSchema.parse(env[\"OOMOL_CONNECTOR_BASE_URL\"]);\n const apiKey = z.string().min(1).parse(env[\"OOMOL_PROJECT_API_KEY\"]);\n const externalUserId = z.string().min(1).parse(parsed.values[\"user-id\"]);\n const port = z.coerce.number().int().min(1).max(65535).parse(parsed.values.port);\n const timeoutMs = z.coerce\n .number()\n .int()\n .positive()\n .max(600_000)\n .parse(parsed.values[\"timeout-ms\"]);\n const returnUri = `http://127.0.0.1:${port}/connected`;\n const dataDir = parsed.values[\"data-dir\"];\n await mkdir(dataDir, { recursive: true, mode: 0o700 });\n process.env[\"FRAGNO_DATA_DIR\"] = dataDir;\n const fragment = createProjectConnectorFragment(\n {\n baseUrl,\n apiKey,\n catalogApiKey: env[\"OOMOL_CONNECTOR_CATALOG_API_KEY\"]?.trim() || null,\n getExternalUserId: () => externalUserId,\n allowedReturnUrls: (url) => url.toString() === returnUri,\n },\n { databaseNamespace: \"project-connector-fragment\" },\n );\n try {\n await migrate(fragment);\n async function call(\n method: \"GET\" | \"POST\",\n route: string,\n body: unknown = null,\n ): Promise<unknown> {\n const response = await fragment.handler(\n new Request(`http://localhost${fragment.mountRoute}${route}`, {\n method,\n headers: { \"content-type\": \"application/json\" },\n ...(method === \"POST\" ? { body: JSON.stringify(body) } : {}),\n }),\n );\n if (!response) {\n throw new Error(\"Project Connector CLI: fragment did not handle the request\");\n }\n const result: unknown = await response.json();\n if (!response.ok) {\n const failure = z.object({ code: z.string(), message: z.string() }).parse(result);\n throw new Error(`Project Connector CLI: ${failure.code}: ${failure.message}`);\n }\n return result;\n }\n switch (command) {\n case \"check\":\n logger.log(JSON.stringify(await call(\"GET\", \"/status\"), null, 2));\n break;\n case \"status\":\n logger.log(\n JSON.stringify(\n await call(\"POST\", `/connection-requests/${encodeURIComponent(args[0])}/refresh`),\n null,\n 2,\n ),\n );\n break;\n case \"accounts\": {\n let cursor: string | null = null;\n do {\n const page = z\n .object({ accounts: z.array(z.unknown()), cursor: z.string().nullable() })\n .parse(\n await call(\n \"GET\",\n `/accounts${cursor ? `?cursor=${encodeURIComponent(cursor)}` : \"\"}`,\n ),\n );\n for (const account of page.accounts) {\n logger.log(JSON.stringify(account, null, 2));\n }\n cursor = page.cursor;\n } while (cursor);\n break;\n }\n case \"profile\":\n logger.log(\n JSON.stringify(\n await call(\"GET\", `/accounts/${encodeURIComponent(args[0])}/profile`),\n null,\n 2,\n ),\n );\n break;\n case \"execute\": {\n const input = z.record(z.string(), z.unknown()).parse(JSON.parse(parsed.values.input));\n logger.log(\n JSON.stringify(\n await call(\n \"POST\",\n `/accounts/${encodeURIComponent(args[0])}/actions/${encodeURIComponent(args[1])}`,\n { input },\n ),\n null,\n 2,\n ),\n );\n break;\n }\n case \"connect\": {\n // The return page is navigation only. Account binding happens exclusively\n // through polling the saved request with the backend project key.\n const callback = createServer((req, res) => {\n if (req.url?.split(\"?\")[0] !== \"/connected\") {\n res.writeHead(404).end(\"Not found\");\n return;\n }\n res\n .writeHead(200, { \"content-type\": \"text/plain; charset=utf-8\" })\n .end(\"Return to the terminal. It will verify your connection securely.\");\n });\n await new Promise<void>((resolve, reject) => {\n callback.once(\"error\", reject);\n callback.listen(port, \"127.0.0.1\", resolve);\n });\n try {\n const providerConfigId = parsed.values[\"provider-config-id\"];\n const connection = projectConnectorConnectionSchema.parse(\n await call(\"POST\", \"/connection-requests\", {\n ...(providerConfigId ? { providerConfigId } : { service: args[0] }),\n connectionName: parsed.values[\"connection-name\"],\n returnUri,\n }),\n );\n logger.log(\n `Connection request: ${connection.id}\\nAuthorize in your browser:\\n${connection.authorizationUrl}`,\n );\n if (parsed.values.beep) {\n playAuthorizationBeep();\n }\n if (!parsed.values[\"no-open\"]) {\n openAuthorizationBrowser(connection.authorizationUrl);\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const refreshed = projectConnectorConnectionSchema.parse(\n await call(\n \"POST\",\n `/connection-requests/${encodeURIComponent(connection.id)}/refresh`,\n ),\n );\n if (refreshed.state.status === \"connected\") {\n logger.log(`Connected account: ${refreshed.state.connectedAccountId}`);\n logger.log(\n JSON.stringify(\n await call(\n \"GET\",\n `/accounts/${encodeURIComponent(refreshed.state.connectedAccountId)}/profile`,\n ),\n null,\n 2,\n ),\n );\n return 0;\n }\n if (refreshed.state.status !== \"initiated\") {\n throw new Error(`Project Connector CLI: authorization ${refreshed.state.status}`);\n }\n await sleep(Math.min(2000, Math.max(0, deadline - Date.now())));\n }\n throw new Error(\n `Project Connector CLI: authorization timed out; resume with status ${connection.id}`,\n );\n } finally {\n callback.closeAllConnections();\n await new Promise<void>((resolve, reject) => {\n callback.close((error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n }\n }\n return 0;\n } finally {\n await fragment.$internal.deps.databaseAdapter.close();\n }\n } catch (error) {\n logger.error(\n error instanceof z.ZodError\n ? \"Project Connector CLI: invalid configuration or input; check environment variables and --help\"\n : error instanceof Error\n ? error.message\n : \"Project Connector CLI: command failed\",\n );\n return 1;\n }\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n process.exitCode = await runProjectConnectorCli(process.argv.slice(2));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createProjectConnectorClient } from "./project-connector-client.js";
|
|
2
|
+
import { projectConnectorSchema } from "./schema.js";
|
|
3
|
+
import { withDatabase } from "@fragno-dev/db";
|
|
4
|
+
import { defineFragment } from "@fragno-dev/core";
|
|
5
|
+
//#region src/definition.ts
|
|
6
|
+
/** OOMOL Project Connector dependencies and scoped account persistence. */
|
|
7
|
+
const projectConnectorFragmentDefinition = defineFragment("project-connector-fragment").extend(withDatabase(projectConnectorSchema)).withDependencies(({ config }) => ({ projectConnector: createProjectConnectorClient(config) })).provideHooks(({ defineHook, config }) => ({ onConnectionReadinessChanged: defineHook(async function(payload) {
|
|
8
|
+
await config.onConnectionReadinessChanged?.(payload, this);
|
|
9
|
+
}) })).build();
|
|
10
|
+
//#endregion
|
|
11
|
+
export { projectConnectorFragmentDefinition };
|
|
12
|
+
|
|
13
|
+
//# sourceMappingURL=definition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definition.js","names":[],"sources":["../../src/definition.ts"],"sourcesContent":["import type { z } from \"zod\";\n\nimport { defineFragment } from \"@fragno-dev/core\";\nimport { withDatabase, type HookContext, type HookFn } from \"@fragno-dev/db\";\n\nimport {\n createProjectConnectorClient,\n type ProjectConnectorClientConfig,\n} from \"./project-connector-client\";\nimport type { projectConnectorNamedConnectionSchema } from \"./project-connector-contracts\";\nimport { projectConnectorSchema } from \"./schema\";\n\nexport interface ProjectConnectorConnectionReadinessChangedPayload {\n externalUserId: string;\n /** Provider service of the named connection's configuration, e.g. gmail. */\n service: string;\n connection: z.output<typeof projectConnectorNamedConnectionSchema>;\n /** Whether a confirmed account exists under this name. */\n ready: boolean;\n}\n\n/** Integrators own authentication and restrict OAuth return URLs; project keys remain server-only. */\nexport type ProjectConnectorFragmentConfig = ProjectConnectorClientConfig & {\n getExternalUserId: (headers: Headers) => string | null | Promise<string | null>;\n allowedReturnUrls: (url: URL) => boolean;\n /**\n * Fires when a write changes whether a named connection has a confirmed account: not ready when\n * the first request for the name starts, ready when a refresh confirms its first account, and not\n * ready for a name left without an account when a refresh moves its account ID to another name.\n * The gateway does not report consent or revocation, so both are noticed only through refresh.\n */\n onConnectionReadinessChanged?: (\n payload: ProjectConnectorConnectionReadinessChangedPayload,\n context: HookContext,\n ) => Promise<void> | void;\n};\n\ntype ProjectConnectorHooksMap = {\n onConnectionReadinessChanged: HookFn<ProjectConnectorConnectionReadinessChangedPayload>;\n};\n\n/** OOMOL Project Connector dependencies and scoped account persistence. */\nexport const projectConnectorFragmentDefinition = defineFragment<ProjectConnectorFragmentConfig>(\n \"project-connector-fragment\",\n)\n .extend(withDatabase(projectConnectorSchema))\n .withDependencies(({ config }) => ({\n projectConnector: createProjectConnectorClient(config),\n }))\n .provideHooks<ProjectConnectorHooksMap>(({ defineHook, config }) => ({\n onConnectionReadinessChanged: defineHook(async function (payload) {\n await config.onConnectionReadinessChanged?.(payload, this);\n }),\n }))\n .build();\n"],"mappings":";;;;;;AA0CA,MAAa,qCAAqC,eAChD,4BACF,CAAC,CACE,OAAO,aAAa,sBAAsB,CAAC,CAAC,CAC5C,kBAAkB,EAAE,cAAc,EACjC,kBAAkB,6BAA6B,MAAM,EACvD,EAAE,CAAC,CACF,cAAwC,EAAE,YAAY,cAAc,EACnE,8BAA8B,WAAW,eAAgB,SAAS;CAChE,MAAM,OAAO,+BAA+B,SAAS,IAAI;AAC3D,CAAC,EACH,EAAE,CAAC,CACF,MAAM"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { projectConnectorFragmentDefinition } from "./definition.js";
|
|
2
|
+
import { projectConnectorRoutes } from "./routes.js";
|
|
3
|
+
import "@fragno-dev/core/client";
|
|
4
|
+
import { instantiate } from "@fragno-dev/core";
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const routes = [projectConnectorRoutes];
|
|
7
|
+
/** Creates a project-scoped connector with integrator-owned user authentication. */
|
|
8
|
+
function createProjectConnectorFragment(config, fragnoConfig) {
|
|
9
|
+
return instantiate(projectConnectorFragmentDefinition).withConfig(config).withRoutes(routes).withOptions(fragnoConfig).build();
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { createProjectConnectorFragment };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["import { createClientBuilder, type FragnoPublicClientConfig } from \"@fragno-dev/core/client\";\n\nimport { instantiate } from \"@fragno-dev/core\";\nimport type { FragnoPublicConfigWithDatabase } from \"@fragno-dev/db\";\n\nimport {\n projectConnectorFragmentDefinition,\n type ProjectConnectorFragmentConfig,\n} from \"./definition\";\nimport { projectConnectorRoutes } from \"./routes\";\n\nconst routes = [projectConnectorRoutes] as const;\n\n/** Creates a project-scoped connector with integrator-owned user authentication. */\nexport function createProjectConnectorFragment(\n config: ProjectConnectorFragmentConfig,\n fragnoConfig: FragnoPublicConfigWithDatabase,\n) {\n return instantiate(projectConnectorFragmentDefinition)\n .withConfig(config)\n .withRoutes(routes)\n .withOptions(fragnoConfig)\n .build();\n}\n\n/** Discover project OAuth configurations and select only server-verified accounts for actions. */\nexport function createProjectConnectorFragmentClients(fragnoConfig: FragnoPublicClientConfig = {}) {\n const builder = createClientBuilder(projectConnectorFragmentDefinition, fragnoConfig, routes);\n return {\n useProviderConfigs: builder.createHook(\"/provider-configs\"),\n useProviderActions: builder.createHook(\"/provider-configs/:providerConfigId/actions\"),\n useStatus: builder.createHook(\"/status\"),\n useAccounts: builder.createHook(\"/accounts\"),\n useNamedAccount: builder.createHook(\"/accounts/by-name\"),\n useNamedConnectionRequest: builder.createHook(\"/connection-requests/by-name\"),\n useProfile: builder.createHook(\"/accounts/:accountId/profile\"),\n connect: builder.createMutator(\"POST\", \"/connection-requests\", (invalidate) => {\n invalidate(\"GET\", \"/connection-requests/by-name\", {});\n }),\n refreshConnection: builder.createMutator(\n \"POST\",\n \"/connection-requests/:requestId/refresh\",\n (invalidate) => {\n invalidate(\"GET\", \"/accounts\", {});\n invalidate(\"GET\", \"/accounts/by-name\", {});\n invalidate(\"GET\", \"/connection-requests/by-name\", {});\n },\n ),\n executeAction: builder.createMutator(\"POST\", \"/accounts/:accountId/actions/:actionId\"),\n };\n}\n"],"mappings":";;;;;AAWA,MAAM,SAAS,CAAC,sBAAsB;;AAGtC,SAAgB,+BACd,QACA,cACA;CACA,OAAO,YAAY,kCAAkC,CAAC,CACnD,WAAW,MAAM,CAAC,CAClB,WAAW,MAAM,CAAC,CAClB,YAAY,YAAY,CAAC,CACzB,MAAM;AACX"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { projectConnectorActionSchema, projectConnectorConnectionStateSchema, projectConnectorExecutionSchema, projectConnectorHttpUrlSchema, projectConnectorProfileSchema, projectConnectorProviderConfigsSchema } from "./project-connector-contracts.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Connector, ConnectorError, ProjectConnector } from "@oomol-lab/connector";
|
|
4
|
+
//#region src/project-connector-client.ts
|
|
5
|
+
const projectConnectorConnectionRequestSchema = z.object({
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
projectId: z.string().min(1),
|
|
8
|
+
providerConfigId: z.string().min(1),
|
|
9
|
+
externalUserId: z.string().min(1),
|
|
10
|
+
service: z.string().min(1),
|
|
11
|
+
connectionName: z.string().nullable(),
|
|
12
|
+
authorizationUrl: projectConnectorHttpUrlSchema,
|
|
13
|
+
expiresAt: z.string()
|
|
14
|
+
}).and(projectConnectorConnectionStateSchema);
|
|
15
|
+
const projectConnectorCallOptions = { retries: 0 };
|
|
16
|
+
const projectConnectorDiscoveryEnvelopeSchema = z.object({
|
|
17
|
+
success: z.literal(true),
|
|
18
|
+
data: projectConnectorProviderConfigsSchema.safeExtend({ providerConfigs: z.array(projectConnectorProviderConfigsSchema.shape.providerConfigs.element.extend({ actionIds: z.array(z.string().min(1)) })) })
|
|
19
|
+
});
|
|
20
|
+
const projectConnectorCatalogActionSchema = projectConnectorActionSchema.extend({ description: z.string().nullish().transform((description) => description ?? null) });
|
|
21
|
+
function createOomolProjectConnectorSdk(config) {
|
|
22
|
+
return new ProjectConnector({
|
|
23
|
+
...config,
|
|
24
|
+
fetch: fetchProjectConnectorWithoutRedirects
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** A safe SDK failure exposes its code, never its raw response data or project API key. */
|
|
28
|
+
var ProjectConnectorClientError = class extends Error {
|
|
29
|
+
code;
|
|
30
|
+
status;
|
|
31
|
+
constructor(code, status) {
|
|
32
|
+
super(`Project Connector request failed: ${code} (HTTP ${status})`);
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.name = "ProjectConnectorClientError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function parseProjectConnectorApiBaseUrl(value) {
|
|
39
|
+
const baseUrl = new URL(projectConnectorHttpUrlSchema.parse(value));
|
|
40
|
+
const pathname = baseUrl.pathname.replace(/\/+$/, "");
|
|
41
|
+
if (baseUrl.search || baseUrl.hash || pathname !== "/v1") throw new Error("Project Connector baseUrl must be an origin followed by /v1");
|
|
42
|
+
baseUrl.pathname = pathname;
|
|
43
|
+
return baseUrl.toString().replace(/\/$/, "");
|
|
44
|
+
}
|
|
45
|
+
function parseProjectConnectorApiKey(value) {
|
|
46
|
+
return z.string().min(1).regex(/^[^\s]+$/).parse(value);
|
|
47
|
+
}
|
|
48
|
+
function projectConnectorClientErrorFromKnownFailure(error) {
|
|
49
|
+
if (error instanceof ProjectConnectorClientError) return error;
|
|
50
|
+
if (error instanceof ConnectorError) {
|
|
51
|
+
if (error.cause instanceof ProjectConnectorClientError) return error.cause;
|
|
52
|
+
console.warn("Project Connector gateway request failed", {
|
|
53
|
+
code: error.code,
|
|
54
|
+
status: error.status,
|
|
55
|
+
message: error.message,
|
|
56
|
+
requestId: error.requestId
|
|
57
|
+
});
|
|
58
|
+
return new ProjectConnectorClientError(error.code, error.status);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function callProjectConnector(operation) {
|
|
63
|
+
try {
|
|
64
|
+
return await operation();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const knownFailure = projectConnectorClientErrorFromKnownFailure(error);
|
|
67
|
+
if (knownFailure) throw knownFailure;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseProjectConnectorResponse(schema, value) {
|
|
72
|
+
const parsed = schema.safeParse(value);
|
|
73
|
+
if (!parsed.success) throw new ProjectConnectorClientError("invalid_response", 502);
|
|
74
|
+
return parsed.data;
|
|
75
|
+
}
|
|
76
|
+
async function fetchProjectConnectorWithoutRedirects(input, init) {
|
|
77
|
+
const response = await globalThis.fetch(input, {
|
|
78
|
+
...init,
|
|
79
|
+
redirect: "manual"
|
|
80
|
+
});
|
|
81
|
+
if ([
|
|
82
|
+
301,
|
|
83
|
+
302,
|
|
84
|
+
303,
|
|
85
|
+
307,
|
|
86
|
+
308
|
|
87
|
+
].includes(response.status)) {
|
|
88
|
+
await response.body?.cancel();
|
|
89
|
+
throw new ProjectConnectorClientError("unexpected_redirect", response.status);
|
|
90
|
+
}
|
|
91
|
+
return response;
|
|
92
|
+
}
|
|
93
|
+
async function fetchProjectConnectorProviderConfigs(config) {
|
|
94
|
+
const signal = AbortSignal.timeout(3e4);
|
|
95
|
+
let response;
|
|
96
|
+
try {
|
|
97
|
+
response = await fetchProjectConnectorWithoutRedirects(`${config.baseUrl}/saas/oauth/provider-configs`, {
|
|
98
|
+
method: "GET",
|
|
99
|
+
headers: { authorization: `Bearer ${config.apiKey}` },
|
|
100
|
+
signal
|
|
101
|
+
});
|
|
102
|
+
} catch (cause) {
|
|
103
|
+
if (signal.aborted) throw new ProjectConnectorClientError("client_timeout", 0);
|
|
104
|
+
if (cause instanceof TypeError) throw new ProjectConnectorClientError("client_network_error", 0);
|
|
105
|
+
throw cause;
|
|
106
|
+
}
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
payload = await response.json();
|
|
110
|
+
} catch (cause) {
|
|
111
|
+
if (signal.aborted) throw new ProjectConnectorClientError("client_timeout", 0);
|
|
112
|
+
if (cause instanceof TypeError) throw new ProjectConnectorClientError("client_network_error", 0);
|
|
113
|
+
if (!(cause instanceof SyntaxError)) throw cause;
|
|
114
|
+
payload = null;
|
|
115
|
+
}
|
|
116
|
+
const failure = z.object({ errorCode: z.string().min(1) }).safeParse(payload);
|
|
117
|
+
if (!response.ok || failure.success) throw new ProjectConnectorClientError(failure.success ? failure.data.errorCode : response.status === 429 ? "rate_limited" : "provider_error", response.status);
|
|
118
|
+
return parseProjectConnectorResponse(projectConnectorDiscoveryEnvelopeSchema, payload).data;
|
|
119
|
+
}
|
|
120
|
+
/** Project Connector adapter with retries disabled and project keys confined to server requests. */
|
|
121
|
+
function createProjectConnectorClient(config, createSdk = createOomolProjectConnectorSdk) {
|
|
122
|
+
const apiKey = parseProjectConnectorApiKey(config.apiKey);
|
|
123
|
+
const baseUrl = parseProjectConnectorApiBaseUrl(config.baseUrl);
|
|
124
|
+
const client = createSdk({
|
|
125
|
+
apiKey,
|
|
126
|
+
baseUrl,
|
|
127
|
+
maxRetries: 0
|
|
128
|
+
});
|
|
129
|
+
const catalog = config.catalogApiKey === null ? null : new Connector({
|
|
130
|
+
apiKey: parseProjectConnectorApiKey(config.catalogApiKey),
|
|
131
|
+
baseUrl,
|
|
132
|
+
maxRetries: 0,
|
|
133
|
+
fetch: fetchProjectConnectorWithoutRedirects
|
|
134
|
+
}).catalog;
|
|
135
|
+
return {
|
|
136
|
+
async listProviderConfigs() {
|
|
137
|
+
const discovery = await fetchProjectConnectorProviderConfigs({
|
|
138
|
+
apiKey,
|
|
139
|
+
baseUrl
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
projectId: discovery.projectId,
|
|
143
|
+
providerConfigs: discovery.providerConfigs.map((config) => ({
|
|
144
|
+
id: config.id,
|
|
145
|
+
service: config.service,
|
|
146
|
+
displayName: config.displayName,
|
|
147
|
+
callbackUrl: config.callbackUrl,
|
|
148
|
+
effectiveScopes: config.effectiveScopes,
|
|
149
|
+
proxyAvailable: config.proxyAvailable
|
|
150
|
+
}))
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
async listProviderActions(providerConfigId) {
|
|
154
|
+
const discovery = await fetchProjectConnectorProviderConfigs({
|
|
155
|
+
apiKey,
|
|
156
|
+
baseUrl
|
|
157
|
+
});
|
|
158
|
+
const config = discovery.providerConfigs.find((config) => config.id === providerConfigId);
|
|
159
|
+
if (!config) return null;
|
|
160
|
+
const actions = [];
|
|
161
|
+
if (config.actionIds.length > 0) {
|
|
162
|
+
if (!catalog) throw new ProjectConnectorClientError("catalog_not_configured", 503);
|
|
163
|
+
const serviceActions = parseProjectConnectorResponse(z.array(projectConnectorCatalogActionSchema).refine((actions) => new Set(actions.map((action) => action.id)).size === actions.length, "Catalog action IDs must be unique"), await callProjectConnector(() => catalog.actions(config.service, projectConnectorCallOptions)));
|
|
164
|
+
const actionsById = new Map(serviceActions.map((action) => [action.id, action]));
|
|
165
|
+
for (const actionId of new Set(config.actionIds)) {
|
|
166
|
+
const action = actionsById.get(actionId);
|
|
167
|
+
if (!action) throw new ProjectConnectorClientError("action_not_found", 502);
|
|
168
|
+
if (action.service !== config.service) throw new ProjectConnectorClientError("action_identity_mismatch", 502);
|
|
169
|
+
actions.push(action);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
projectId: discovery.projectId,
|
|
174
|
+
providerConfigId: config.id,
|
|
175
|
+
actions
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
async check() {
|
|
179
|
+
try {
|
|
180
|
+
await client.getConnectionRequest(`fragno-check-${crypto.randomUUID()}`, projectConnectorCallOptions);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const knownFailure = projectConnectorClientErrorFromKnownFailure(error);
|
|
183
|
+
if (knownFailure?.status === 404 && knownFailure.code === "connection_request_not_found") return { authenticated: true };
|
|
184
|
+
if (knownFailure) throw knownFailure;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
throw new ProjectConnectorClientError("unexpected_health_response", 502);
|
|
188
|
+
},
|
|
189
|
+
async connect(externalUserId, input) {
|
|
190
|
+
const request = await callProjectConnector(() => client.connect.oauth(externalUserId, {
|
|
191
|
+
..."providerConfigId" in input ? { providerConfigId: input.providerConfigId } : { service: input.service },
|
|
192
|
+
connectionName: input.connectionName,
|
|
193
|
+
returnUri: input.returnUri
|
|
194
|
+
}, projectConnectorCallOptions));
|
|
195
|
+
return parseProjectConnectorResponse(projectConnectorConnectionRequestSchema, request);
|
|
196
|
+
},
|
|
197
|
+
async getConnectionRequest(requestId) {
|
|
198
|
+
const request = await callProjectConnector(() => client.getConnectionRequest(requestId, projectConnectorCallOptions));
|
|
199
|
+
return parseProjectConnectorResponse(projectConnectorConnectionRequestSchema, request);
|
|
200
|
+
},
|
|
201
|
+
async getProfile(accountId) {
|
|
202
|
+
return parseProjectConnectorResponse(projectConnectorProfileSchema, await callProjectConnector(() => client.getUserProfile(accountId, projectConnectorCallOptions)));
|
|
203
|
+
},
|
|
204
|
+
async execute(account, actionId, input) {
|
|
205
|
+
const result = await callProjectConnector(() => client.executeRaw(account.externalUserId, actionId, input, {
|
|
206
|
+
providerConfigId: account.providerConfigId,
|
|
207
|
+
connectedAccountId: account.id,
|
|
208
|
+
retries: 0
|
|
209
|
+
}));
|
|
210
|
+
return parseProjectConnectorResponse(projectConnectorExecutionSchema, {
|
|
211
|
+
executionId: result.executionId,
|
|
212
|
+
actionId: result.actionId,
|
|
213
|
+
output: result.data
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** Pins the SDK response to the saved project, provider, user, and request identity. */
|
|
219
|
+
function confirmProjectConnectorRequest(expected, actual) {
|
|
220
|
+
if (actual.id !== expected.id || actual.projectId !== expected.projectId || actual.providerConfigId !== expected.providerConfigId || actual.externalUserId !== expected.externalUserId || actual.service !== expected.service || actual.connectionName !== expected.connectionName) throw new ProjectConnectorClientError("connection_identity_mismatch", 502);
|
|
221
|
+
if (actual.status === "initiated" || actual.status === "expired") return { status: actual.status };
|
|
222
|
+
if (actual.status === "connected") return {
|
|
223
|
+
status: "connected",
|
|
224
|
+
connectedAccountId: actual.connectedAccountId
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
status: "failed",
|
|
228
|
+
errorCode: actual.errorCode,
|
|
229
|
+
errorMessage: actual.errorMessage
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
export { ProjectConnectorClientError, confirmProjectConnectorRequest, createProjectConnectorClient };
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=project-connector-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-connector-client.js","names":[],"sources":["../../src/project-connector-client.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport {\n Connector,\n ConnectorError,\n ProjectConnector,\n type ProjectApi,\n type ProjectCallOptions,\n type ProjectConnectorConfig,\n} from \"@oomol-lab/connector\";\n\nimport {\n projectConnectorActionSchema,\n projectConnectorConnectionStateSchema,\n type projectConnectorConnectInputSchema,\n projectConnectorExecutionSchema,\n projectConnectorHttpUrlSchema,\n projectConnectorProfileSchema,\n projectConnectorProviderConfigsSchema,\n type ProjectConnectorConnectionState,\n} from \"./project-connector-contracts\";\n\nconst projectConnectorConnectionRequestSchema = z\n .object({\n id: z.string().min(1),\n projectId: z.string().min(1),\n providerConfigId: z.string().min(1),\n externalUserId: z.string().min(1),\n service: z.string().min(1),\n connectionName: z.string().nullable(),\n authorizationUrl: projectConnectorHttpUrlSchema,\n expiresAt: z.string(),\n })\n .and(projectConnectorConnectionStateSchema);\n\nconst projectConnectorCallOptions = { retries: 0 } satisfies ProjectCallOptions;\nconst projectConnectorDiscoveryEnvelopeSchema = z.object({\n success: z.literal(true),\n data: projectConnectorProviderConfigsSchema.safeExtend({\n providerConfigs: z.array(\n projectConnectorProviderConfigsSchema.shape.providerConfigs.element.extend({\n actionIds: z.array(z.string().min(1)),\n }),\n ),\n }),\n});\n\nconst projectConnectorCatalogActionSchema = projectConnectorActionSchema.extend({\n description: z\n .string()\n .nullish()\n .transform((description) => description ?? null),\n});\n\ntype ProjectConnectorSdk = Pick<\n ProjectApi,\n \"getConnectionRequest\" | \"getUserProfile\" | \"executeRaw\"\n> & {\n readonly connect: Pick<ProjectApi[\"connect\"], \"oauth\">;\n};\n\ntype CreateProjectConnectorSdk = (config: ProjectConnectorConfig) => ProjectConnectorSdk;\n\nfunction createOomolProjectConnectorSdk(config: ProjectConnectorConfig): ProjectConnectorSdk {\n return new ProjectConnector({\n ...config,\n // The wrapper also avoids the SDK calling native Workers fetch with a transport receiver.\n fetch: fetchProjectConnectorWithoutRedirects,\n });\n}\n\n/** A safe SDK failure exposes its code, never its raw response data or project API key. */\nexport class ProjectConnectorClientError extends Error {\n constructor(\n readonly code: string,\n readonly status: number,\n ) {\n super(`Project Connector request failed: ${code} (HTTP ${status})`);\n this.name = \"ProjectConnectorClientError\";\n }\n}\n\n/** Server-only gateway credentials; null catalogApiKey disables action discovery, not account operations. */\nexport type ProjectConnectorClientConfig = {\n baseUrl: string;\n apiKey: string;\n catalogApiKey: string | null;\n};\n\nfunction parseProjectConnectorApiBaseUrl(value: string): string {\n const baseUrl = new URL(projectConnectorHttpUrlSchema.parse(value));\n const pathname = baseUrl.pathname.replace(/\\/+$/, \"\");\n if (baseUrl.search || baseUrl.hash || pathname !== \"/v1\") {\n throw new Error(\"Project Connector baseUrl must be an origin followed by /v1\");\n }\n baseUrl.pathname = pathname;\n return baseUrl.toString().replace(/\\/$/, \"\");\n}\n\nfunction parseProjectConnectorApiKey(value: string): string {\n return z\n .string()\n .min(1)\n .regex(/^[^\\s]+$/)\n .parse(value);\n}\n\nfunction projectConnectorClientErrorFromKnownFailure(\n error: unknown,\n): ProjectConnectorClientError | null {\n if (error instanceof ProjectConnectorClientError) {\n return error;\n }\n if (error instanceof ConnectorError) {\n // The SDK wraps transport rejections as network errors; retain our safe redirect diagnosis.\n if (error.cause instanceof ProjectConnectorClientError) {\n return error.cause;\n }\n // Callers only see the code; the gateway's message (e.g. which input field it rejected) stays in\n // server logs. Gateway messages never contain the project API key.\n console.warn(\"Project Connector gateway request failed\", {\n code: error.code,\n status: error.status,\n message: error.message,\n requestId: error.requestId,\n });\n return new ProjectConnectorClientError(error.code, error.status);\n }\n return null;\n}\n\nasync function callProjectConnector<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n const knownFailure = projectConnectorClientErrorFromKnownFailure(error);\n if (knownFailure) {\n throw knownFailure;\n }\n throw error;\n }\n}\n\nfunction parseProjectConnectorResponse<T>(schema: z.ZodType<T>, value: unknown): T {\n const parsed = schema.safeParse(value);\n if (!parsed.success) {\n throw new ProjectConnectorClientError(\"invalid_response\", 502);\n }\n return parsed.data;\n}\n\nasync function fetchProjectConnectorWithoutRedirects(\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n): Promise<Response> {\n // Workers rejects redirect: \"error\" before sending the request. Manual mode keeps credentials\n // on the original host; reject redirects before interpreting the response body.\n const response = await globalThis.fetch(input, { ...init, redirect: \"manual\" });\n if ([301, 302, 303, 307, 308].includes(response.status)) {\n await response.body?.cancel();\n throw new ProjectConnectorClientError(\"unexpected_redirect\", response.status);\n }\n return response;\n}\n\nasync function fetchProjectConnectorProviderConfigs(\n config: Pick<ProjectConnectorClientConfig, \"baseUrl\" | \"apiKey\">,\n) {\n // The SDK has no discovery method; redirects must not forward the project key elsewhere.\n const signal = AbortSignal.timeout(30_000);\n let response: Response;\n try {\n response = await fetchProjectConnectorWithoutRedirects(\n `${config.baseUrl}/saas/oauth/provider-configs`,\n {\n method: \"GET\",\n headers: { authorization: `Bearer ${config.apiKey}` },\n signal,\n },\n );\n } catch (cause) {\n if (signal.aborted) {\n throw new ProjectConnectorClientError(\"client_timeout\", 0);\n }\n if (cause instanceof TypeError) {\n throw new ProjectConnectorClientError(\"client_network_error\", 0);\n }\n throw cause;\n }\n let payload: unknown;\n try {\n payload = await response.json();\n } catch (cause) {\n if (signal.aborted) {\n throw new ProjectConnectorClientError(\"client_timeout\", 0);\n }\n if (cause instanceof TypeError) {\n throw new ProjectConnectorClientError(\"client_network_error\", 0);\n }\n if (!(cause instanceof SyntaxError)) {\n throw cause;\n }\n payload = null;\n }\n const failure = z.object({ errorCode: z.string().min(1) }).safeParse(payload);\n if (!response.ok || failure.success) {\n throw new ProjectConnectorClientError(\n failure.success\n ? failure.data.errorCode\n : response.status === 429\n ? \"rate_limited\"\n : \"provider_error\",\n response.status,\n );\n }\n return parseProjectConnectorResponse(projectConnectorDiscoveryEnvelopeSchema, payload).data;\n}\n\n/** Project Connector adapter with retries disabled and project keys confined to server requests. */\nexport function createProjectConnectorClient(\n config: ProjectConnectorClientConfig,\n createSdk: CreateProjectConnectorSdk = createOomolProjectConnectorSdk,\n) {\n const apiKey = parseProjectConnectorApiKey(config.apiKey);\n const baseUrl = parseProjectConnectorApiBaseUrl(config.baseUrl);\n const client = createSdk({ apiKey, baseUrl, maxRetries: 0 });\n // Project credentials and catalog credentials have different authority; never substitute one for the other.\n const catalog =\n config.catalogApiKey === null\n ? null\n : new Connector({\n apiKey: parseProjectConnectorApiKey(config.catalogApiKey),\n baseUrl,\n maxRetries: 0,\n fetch: fetchProjectConnectorWithoutRedirects,\n }).catalog;\n\n return {\n async listProviderConfigs() {\n const discovery = await fetchProjectConnectorProviderConfigs({ apiKey, baseUrl });\n return {\n projectId: discovery.projectId,\n providerConfigs: discovery.providerConfigs.map((config) => ({\n id: config.id,\n service: config.service,\n displayName: config.displayName,\n callbackUrl: config.callbackUrl,\n effectiveScopes: config.effectiveScopes,\n proxyAvailable: config.proxyAvailable,\n })),\n };\n },\n async listProviderActions(providerConfigId: string) {\n const discovery = await fetchProjectConnectorProviderConfigs({ apiKey, baseUrl });\n const config = discovery.providerConfigs.find((config) => config.id === providerConfigId);\n if (!config) {\n return null;\n }\n const actions: z.output<typeof projectConnectorActionSchema>[] = [];\n if (config.actionIds.length > 0) {\n if (!catalog) {\n throw new ProjectConnectorClientError(\"catalog_not_configured\", 503);\n }\n const serviceActions = parseProjectConnectorResponse(\n z\n .array(projectConnectorCatalogActionSchema)\n .refine(\n (actions) => new Set(actions.map((action) => action.id)).size === actions.length,\n \"Catalog action IDs must be unique\",\n ),\n await callProjectConnector(() =>\n catalog.actions(config.service, projectConnectorCallOptions),\n ),\n );\n const actionsById = new Map(serviceActions.map((action) => [action.id, action]));\n // Fetching the service catalog never expands this configuration's allowlist.\n for (const actionId of new Set(config.actionIds)) {\n const action = actionsById.get(actionId);\n if (!action) {\n throw new ProjectConnectorClientError(\"action_not_found\", 502);\n }\n if (action.service !== config.service) {\n throw new ProjectConnectorClientError(\"action_identity_mismatch\", 502);\n }\n actions.push(action);\n }\n }\n return {\n projectId: discovery.projectId,\n providerConfigId: config.id,\n actions,\n };\n },\n async check() {\n // The project API has no health endpoint. An authenticated lookup of a fresh,\n // nonexistent request must return connection_request_not_found, not Unauthorized.\n try {\n await client.getConnectionRequest(\n `fragno-check-${crypto.randomUUID()}`,\n projectConnectorCallOptions,\n );\n } catch (error) {\n const knownFailure = projectConnectorClientErrorFromKnownFailure(error);\n if (knownFailure?.status === 404 && knownFailure.code === \"connection_request_not_found\") {\n return { authenticated: true as const };\n }\n if (knownFailure) {\n throw knownFailure;\n }\n throw error;\n }\n throw new ProjectConnectorClientError(\"unexpected_health_response\", 502);\n },\n async connect(\n externalUserId: string,\n input: z.infer<typeof projectConnectorConnectInputSchema>,\n ) {\n const request = await callProjectConnector(() =>\n client.connect.oauth(\n externalUserId,\n {\n ...(\"providerConfigId\" in input\n ? { providerConfigId: input.providerConfigId }\n : { service: input.service }),\n connectionName: input.connectionName,\n returnUri: input.returnUri,\n },\n projectConnectorCallOptions,\n ),\n );\n return parseProjectConnectorResponse(projectConnectorConnectionRequestSchema, request);\n },\n async getConnectionRequest(requestId: string) {\n const request = await callProjectConnector(() =>\n client.getConnectionRequest(requestId, projectConnectorCallOptions),\n );\n return parseProjectConnectorResponse(projectConnectorConnectionRequestSchema, request);\n },\n async getProfile(accountId: string) {\n const profile = await callProjectConnector(() =>\n client.getUserProfile(accountId, projectConnectorCallOptions),\n );\n return parseProjectConnectorResponse(projectConnectorProfileSchema, profile);\n },\n async execute(\n account: { externalUserId: string; providerConfigId: string; id: string },\n actionId: string,\n input: Record<string, unknown>,\n ) {\n const result = await callProjectConnector(() =>\n client.executeRaw(account.externalUserId, actionId, input, {\n providerConfigId: account.providerConfigId,\n connectedAccountId: account.id,\n retries: 0,\n }),\n );\n return parseProjectConnectorResponse(projectConnectorExecutionSchema, {\n executionId: result.executionId,\n actionId: result.actionId,\n output: result.data,\n });\n },\n };\n}\n\n/** Pins the SDK response to the saved project, provider, user, and request identity. */\nexport function confirmProjectConnectorRequest(\n expected: {\n id: string;\n projectId: string;\n providerConfigId: string;\n externalUserId: string;\n service: string;\n connectionName: string;\n },\n actual: Awaited<\n ReturnType<ReturnType<typeof createProjectConnectorClient>[\"getConnectionRequest\"]>\n >,\n): ProjectConnectorConnectionState {\n if (\n actual.id !== expected.id ||\n actual.projectId !== expected.projectId ||\n actual.providerConfigId !== expected.providerConfigId ||\n actual.externalUserId !== expected.externalUserId ||\n actual.service !== expected.service ||\n actual.connectionName !== expected.connectionName\n ) {\n throw new ProjectConnectorClientError(\"connection_identity_mismatch\", 502);\n }\n if (actual.status === \"initiated\" || actual.status === \"expired\") {\n return { status: actual.status };\n }\n if (actual.status === \"connected\") {\n return { status: \"connected\", connectedAccountId: actual.connectedAccountId };\n }\n return { status: \"failed\", errorCode: actual.errorCode, errorMessage: actual.errorMessage };\n}\n"],"mappings":"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"}
|