@joeybuilt/plexo-sdk 0.1.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/dist/connect/index.d.ts +246 -0
- package/dist/connect/index.js +392 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/index.d.ts +1925 -0
- package/dist/index.js +599 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
interface AppToolConfig {
|
|
2
|
+
description: string;
|
|
3
|
+
inputSchema?: Record<string, unknown>;
|
|
4
|
+
outputSchema?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface AppChannelConfig {
|
|
7
|
+
description: string;
|
|
8
|
+
transport?: 'api' | 'webhook' | 'poll';
|
|
9
|
+
direction?: 'inbound' | 'outbound' | 'bidirectional';
|
|
10
|
+
}
|
|
11
|
+
interface AppConnectorConfig {
|
|
12
|
+
description: string;
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Lightweight extension declaration an app registers with Plexo Core. */
|
|
16
|
+
type AppExtension = {
|
|
17
|
+
id: string;
|
|
18
|
+
type: 'tool';
|
|
19
|
+
name: string;
|
|
20
|
+
config: AppToolConfig;
|
|
21
|
+
} | {
|
|
22
|
+
id: string;
|
|
23
|
+
type: 'channel';
|
|
24
|
+
name: string;
|
|
25
|
+
config: AppChannelConfig;
|
|
26
|
+
} | {
|
|
27
|
+
id: string;
|
|
28
|
+
type: 'connector';
|
|
29
|
+
name: string;
|
|
30
|
+
config: AppConnectorConfig;
|
|
31
|
+
};
|
|
32
|
+
interface ResilienceOptions {
|
|
33
|
+
/** Per-call timeout in ms. Default: 15_000. */
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
/** Retry backoff schedule for registration. Default: [5_000, 15_000, 45_000]. */
|
|
36
|
+
registrationBackoffMs?: number[];
|
|
37
|
+
}
|
|
38
|
+
interface PlexoClientOptions {
|
|
39
|
+
/** Unique app identifier. Must match PLEXO_APP_ID (or APP_ID) in env. */
|
|
40
|
+
appId: string;
|
|
41
|
+
/** Plexo Core base URL. e.g. https://plexo.example.com */
|
|
42
|
+
plexoUrl: string;
|
|
43
|
+
/** Shared service key — must match PLEXO_SERVICE_KEY on Plexo Core. */
|
|
44
|
+
serviceKey: string;
|
|
45
|
+
/** Human-readable app name shown in Plexo UI. Defaults to appId. */
|
|
46
|
+
displayName?: string;
|
|
47
|
+
/** Tools, channels, and connectors this app exposes to Plexo agents. */
|
|
48
|
+
extensions?: AppExtension[];
|
|
49
|
+
/** Event topics this app may emit. e.g. ['my-app.order.created'] */
|
|
50
|
+
eventContracts?: string[];
|
|
51
|
+
resilience?: ResilienceOptions;
|
|
52
|
+
/** Override fetch for testing or custom retry logic. */
|
|
53
|
+
fetchImpl?: typeof fetch;
|
|
54
|
+
}
|
|
55
|
+
interface PlexoConnection {
|
|
56
|
+
id: string;
|
|
57
|
+
registryId: string;
|
|
58
|
+
name: string;
|
|
59
|
+
status: 'active' | 'disconnected' | 'error' | 'expired';
|
|
60
|
+
scopesGranted: string[] | null;
|
|
61
|
+
lastVerifiedAt: string | null;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
}
|
|
64
|
+
interface PlexoToken {
|
|
65
|
+
access_token: string | null;
|
|
66
|
+
refresh_token: string | null;
|
|
67
|
+
expires_at: string | null;
|
|
68
|
+
email: string | null;
|
|
69
|
+
scope: string | null;
|
|
70
|
+
}
|
|
71
|
+
interface AiMessage {
|
|
72
|
+
role: 'system' | 'user' | 'assistant';
|
|
73
|
+
content: string;
|
|
74
|
+
}
|
|
75
|
+
interface AiCompleteOptions {
|
|
76
|
+
messages: AiMessage[];
|
|
77
|
+
systemPrompt?: string;
|
|
78
|
+
maxTokens?: number;
|
|
79
|
+
taskType?: string;
|
|
80
|
+
}
|
|
81
|
+
interface ChatOptions {
|
|
82
|
+
message: string;
|
|
83
|
+
sessionId?: string;
|
|
84
|
+
sessionContext?: {
|
|
85
|
+
activeView?: {
|
|
86
|
+
type: string;
|
|
87
|
+
id: string;
|
|
88
|
+
summary: string;
|
|
89
|
+
};
|
|
90
|
+
appState?: Record<string, unknown>;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
interface ChatReply {
|
|
94
|
+
reply: string;
|
|
95
|
+
conversationId?: string;
|
|
96
|
+
sessionId?: string;
|
|
97
|
+
taskId?: string;
|
|
98
|
+
}
|
|
99
|
+
interface DispatchOptions {
|
|
100
|
+
channel: 'email' | 'push' | 'sms' | 'telegram' | string;
|
|
101
|
+
recipientUserId: string;
|
|
102
|
+
message: {
|
|
103
|
+
subject?: string;
|
|
104
|
+
text: string;
|
|
105
|
+
attachments?: unknown[];
|
|
106
|
+
metadata?: Record<string, unknown>;
|
|
107
|
+
};
|
|
108
|
+
idempotencyKey: string;
|
|
109
|
+
}
|
|
110
|
+
interface DispatchResult {
|
|
111
|
+
messageId?: string;
|
|
112
|
+
deliveryStatus?: string;
|
|
113
|
+
}
|
|
114
|
+
interface AppProfile {
|
|
115
|
+
appId: string;
|
|
116
|
+
displayName: string;
|
|
117
|
+
domain: string;
|
|
118
|
+
extensions: AppExtension[];
|
|
119
|
+
eventContracts: string[];
|
|
120
|
+
}
|
|
121
|
+
type InboundEventType = 'connection.connected' | 'connection.disconnected' | 'connection.error' | 'task.created' | 'task.updated' | 'task.completed' | 'workspace.updated' | string;
|
|
122
|
+
interface InboundEvent {
|
|
123
|
+
type: InboundEventType;
|
|
124
|
+
workspaceId: string;
|
|
125
|
+
userId?: string;
|
|
126
|
+
payload: Record<string, unknown>;
|
|
127
|
+
timestamp: string;
|
|
128
|
+
}
|
|
129
|
+
interface DataQuery {
|
|
130
|
+
tool: string;
|
|
131
|
+
workspaceId: string;
|
|
132
|
+
userId: string;
|
|
133
|
+
params: Record<string, unknown>;
|
|
134
|
+
requestId: string;
|
|
135
|
+
}
|
|
136
|
+
interface DataResponse {
|
|
137
|
+
requestId: string;
|
|
138
|
+
result?: unknown;
|
|
139
|
+
error?: string;
|
|
140
|
+
}
|
|
141
|
+
interface InboundHandlers {
|
|
142
|
+
onEvent?: (event: InboundEvent) => void | Promise<void>;
|
|
143
|
+
onDataQuery?: (query: DataQuery) => Promise<unknown>;
|
|
144
|
+
}
|
|
145
|
+
interface InboundVerifyResult {
|
|
146
|
+
ok: boolean;
|
|
147
|
+
error?: string;
|
|
148
|
+
}
|
|
149
|
+
interface TestConnectionResult {
|
|
150
|
+
ok: boolean;
|
|
151
|
+
latencyMs: number;
|
|
152
|
+
plexoVersion?: string;
|
|
153
|
+
error?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
declare class PlexoClient {
|
|
157
|
+
#private;
|
|
158
|
+
constructor(opts: PlexoClientOptions);
|
|
159
|
+
get appId(): string;
|
|
160
|
+
get isConfigured(): boolean;
|
|
161
|
+
register(): Promise<void>;
|
|
162
|
+
ensureWorkspace(userId: string, email?: string): Promise<string>;
|
|
163
|
+
getInstalledConnections(workspaceId: string): Promise<PlexoConnection[]>;
|
|
164
|
+
getToken(workspaceId: string, registryId: string): Promise<PlexoToken | null>;
|
|
165
|
+
disconnect(workspaceId: string, connectionId: string): Promise<void>;
|
|
166
|
+
oauthPopupUrl(provider: string, workspaceId: string): string;
|
|
167
|
+
aiComplete(workspaceId: string, opts: AiCompleteOptions): Promise<string>;
|
|
168
|
+
chatMessage(workspaceId: string, userId: string, opts: ChatOptions): Promise<ChatReply>;
|
|
169
|
+
dispatch(opts: DispatchOptions): Promise<DispatchResult>;
|
|
170
|
+
/** Returns a framework-agnostic handler for Plexo → app push requests. */
|
|
171
|
+
inbound(handlers: InboundHandlers): {
|
|
172
|
+
handle(req: Request): Promise<Response>;
|
|
173
|
+
};
|
|
174
|
+
testConnection(): Promise<TestConnectionResult>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare class PlexoNotConfiguredError extends Error {
|
|
178
|
+
readonly name = "PlexoNotConfiguredError";
|
|
179
|
+
constructor(missing: string);
|
|
180
|
+
}
|
|
181
|
+
declare class PlexoUnreachableError extends Error {
|
|
182
|
+
readonly name = "PlexoUnreachableError";
|
|
183
|
+
readonly url: string;
|
|
184
|
+
constructor(url: string, cause?: unknown);
|
|
185
|
+
}
|
|
186
|
+
declare class PlexoApiError extends Error {
|
|
187
|
+
readonly name: string;
|
|
188
|
+
readonly status: number;
|
|
189
|
+
readonly path: string;
|
|
190
|
+
constructor(status: number, path: string, detail?: string);
|
|
191
|
+
}
|
|
192
|
+
declare class PlexoAuthError extends PlexoApiError {
|
|
193
|
+
readonly name: "PlexoAuthError";
|
|
194
|
+
constructor(path: string);
|
|
195
|
+
}
|
|
196
|
+
declare class PlexoRateLimitedError extends PlexoApiError {
|
|
197
|
+
readonly name: "PlexoRateLimitedError";
|
|
198
|
+
readonly retryAfterMs: number;
|
|
199
|
+
constructor(path: string, retryAfterSec?: number);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
declare function verifyInboundSignature(body: string, signature: string | null, timestamp: string | null, serviceKey: string): InboundVerifyResult;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @plexo/sdk — connect module
|
|
206
|
+
*
|
|
207
|
+
* Universal client for connecting any app to Plexo Core.
|
|
208
|
+
*
|
|
209
|
+
* Quick start:
|
|
210
|
+
*
|
|
211
|
+
* import { createPlexoClient } from '@joeybuilt/plexo-sdk/connect'
|
|
212
|
+
*
|
|
213
|
+
* export const plexo = createPlexoClient({
|
|
214
|
+
* appId: process.env.APP_ID ?? 'my-app',
|
|
215
|
+
* plexoUrl: process.env.PLEXO_URL ?? '',
|
|
216
|
+
* serviceKey: process.env.SERVICE_KEY ?? '',
|
|
217
|
+
* extensions: [
|
|
218
|
+
* { id: 'my-app.todos.list', type: 'tool', name: 'List Todos',
|
|
219
|
+
* config: { description: 'List todos for the current user' } },
|
|
220
|
+
* ],
|
|
221
|
+
* eventContracts: ['my-app.todo.created'],
|
|
222
|
+
* })
|
|
223
|
+
*
|
|
224
|
+
* Register at boot (Next.js instrumentation.ts, Express server start, etc.):
|
|
225
|
+
* await plexo.register()
|
|
226
|
+
*
|
|
227
|
+
* Per-user workspace (call once on first login):
|
|
228
|
+
* const workspaceId = await plexo.ensureWorkspace(userId, email)
|
|
229
|
+
*
|
|
230
|
+
* AI / chat:
|
|
231
|
+
* const text = await plexo.aiComplete(workspaceId, { messages: [...] })
|
|
232
|
+
* const reply = await plexo.chatMessage(workspaceId, userId, { message })
|
|
233
|
+
*
|
|
234
|
+
* Inbound events + data queries (mount at POST /api/plexo/events):
|
|
235
|
+
* export const POST = (req: Request) =>
|
|
236
|
+
* plexo.inbound({ onEvent, onDataQuery }).handle(req)
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a configured Plexo client.
|
|
241
|
+
* Call `await client.register()` once at app boot to announce this app to
|
|
242
|
+
* Plexo Core and make its extensions discoverable.
|
|
243
|
+
*/
|
|
244
|
+
declare function createPlexoClient(opts: PlexoClientOptions): PlexoClient;
|
|
245
|
+
|
|
246
|
+
export { type AiCompleteOptions, type AiMessage, type AppChannelConfig, type AppConnectorConfig, type AppExtension, type AppProfile, type AppToolConfig, type ChatOptions, type ChatReply, type DataQuery, type DataResponse, type DispatchOptions, type DispatchResult, type InboundEvent, type InboundEventType, type InboundHandlers, type InboundVerifyResult, PlexoApiError, PlexoAuthError, PlexoClient, type PlexoClientOptions, type PlexoConnection, PlexoNotConfiguredError, PlexoRateLimitedError, type PlexoToken, PlexoUnreachableError, type ResilienceOptions, type TestConnectionResult, createPlexoClient, verifyInboundSignature };
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// src/connect/errors.ts
|
|
2
|
+
var PlexoNotConfiguredError = class extends Error {
|
|
3
|
+
name = "PlexoNotConfiguredError";
|
|
4
|
+
constructor(missing) {
|
|
5
|
+
super(`PlexoClient not configured: ${missing} is required`);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var PlexoUnreachableError = class extends Error {
|
|
9
|
+
name = "PlexoUnreachableError";
|
|
10
|
+
url;
|
|
11
|
+
constructor(url, cause) {
|
|
12
|
+
super(`Plexo Core unreachable at ${url}`);
|
|
13
|
+
this.url = url;
|
|
14
|
+
if (cause) this.cause = cause;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var PlexoApiError = class extends Error {
|
|
18
|
+
name = "PlexoApiError";
|
|
19
|
+
status;
|
|
20
|
+
path;
|
|
21
|
+
constructor(status, path, detail) {
|
|
22
|
+
super(`Plexo API error ${status} on ${path}${detail ? `: ${detail}` : ""}`);
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.path = path;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var PlexoAuthError = class extends PlexoApiError {
|
|
28
|
+
name = "PlexoAuthError";
|
|
29
|
+
constructor(path) {
|
|
30
|
+
super(401, path, "invalid or missing service key");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var PlexoRateLimitedError = class extends PlexoApiError {
|
|
34
|
+
name = "PlexoRateLimitedError";
|
|
35
|
+
retryAfterMs;
|
|
36
|
+
constructor(path, retryAfterSec) {
|
|
37
|
+
super(429, path, "rate limited");
|
|
38
|
+
this.retryAfterMs = (retryAfterSec ?? 60) * 1e3;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/connect/inbound.ts
|
|
43
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
44
|
+
var SKEW_MS = 5 * 60 * 1e3;
|
|
45
|
+
function verifyInboundSignature(body, signature, timestamp, serviceKey) {
|
|
46
|
+
if (!signature || !timestamp) {
|
|
47
|
+
return { ok: false, error: "missing X-Plexo-Signature or X-Plexo-Timestamp" };
|
|
48
|
+
}
|
|
49
|
+
const ts = Date.parse(timestamp);
|
|
50
|
+
if (Number.isNaN(ts) || Math.abs(Date.now() - ts) > SKEW_MS) {
|
|
51
|
+
return { ok: false, error: "timestamp skew exceeds 5 minutes" };
|
|
52
|
+
}
|
|
53
|
+
const expected = "sha256=" + createHmac("sha256", serviceKey).update(body).digest("hex");
|
|
54
|
+
const sigBuf = Buffer.from(signature);
|
|
55
|
+
const expBuf = Buffer.from(expected);
|
|
56
|
+
if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
|
|
57
|
+
return { ok: false, error: "signature mismatch" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
function createInboundRouter(serviceKey, handlers) {
|
|
62
|
+
return {
|
|
63
|
+
async handle(req) {
|
|
64
|
+
const body = await req.text();
|
|
65
|
+
const sig = req.headers.get("x-plexo-signature");
|
|
66
|
+
const ts = req.headers.get("x-plexo-timestamp");
|
|
67
|
+
const check = verifyInboundSignature(body, sig, ts, serviceKey);
|
|
68
|
+
if (!check.ok) {
|
|
69
|
+
return new Response(
|
|
70
|
+
JSON.stringify({ error: check.error }),
|
|
71
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(body);
|
|
77
|
+
} catch {
|
|
78
|
+
return new Response(
|
|
79
|
+
JSON.stringify({ error: "invalid JSON" }),
|
|
80
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const msg = parsed;
|
|
84
|
+
const kind = msg["kind"];
|
|
85
|
+
try {
|
|
86
|
+
if (kind === "event" && handlers.onEvent) {
|
|
87
|
+
await handlers.onEvent(msg["event"]);
|
|
88
|
+
return new Response(null, { status: 204 });
|
|
89
|
+
}
|
|
90
|
+
if (kind === "data_query" && handlers.onDataQuery) {
|
|
91
|
+
const query = msg["query"];
|
|
92
|
+
const result = await handlers.onDataQuery(query);
|
|
93
|
+
const response = { requestId: query.requestId, result };
|
|
94
|
+
return new Response(
|
|
95
|
+
JSON.stringify(response),
|
|
96
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return new Response(null, { status: 204 });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const response = {
|
|
102
|
+
requestId: msg["query"]?.requestId ?? "",
|
|
103
|
+
error: err instanceof Error ? err.message : String(err)
|
|
104
|
+
};
|
|
105
|
+
return new Response(
|
|
106
|
+
JSON.stringify(response),
|
|
107
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/connect/registration.ts
|
|
115
|
+
function buildHeaders(opts) {
|
|
116
|
+
return {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
Authorization: `Bearer ${opts.serviceKey}`,
|
|
119
|
+
"X-App-Id": opts.appId
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function post(opts, path, body) {
|
|
123
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetchImpl(
|
|
126
|
+
`${opts.plexoUrl.replace(/\/$/, "")}${path}`,
|
|
127
|
+
{
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: buildHeaders(opts),
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
signal: AbortSignal.timeout(8e3)
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
const detail = await res.text().catch(() => `HTTP ${res.status}`);
|
|
136
|
+
console.error(`[plexo/connect] registration rejected (${res.status}): ${detail}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.warn(`[plexo/connect] registration attempt failed: ${err.message}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function register(opts, profile) {
|
|
146
|
+
const backoff = opts.resilience?.registrationBackoffMs ?? [5e3, 15e3, 45e3];
|
|
147
|
+
if (await post(opts, "/api/v1/profiles/register", profile)) {
|
|
148
|
+
console.info(`[plexo/connect] registered appId=${opts.appId}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const delay of backoff) {
|
|
152
|
+
console.info(`[plexo/connect] retrying registration in ${delay / 1e3}s`);
|
|
153
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
154
|
+
if (await post(opts, "/api/v1/profiles/register", profile)) {
|
|
155
|
+
console.info(`[plexo/connect] registered appId=${opts.appId}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.warn("[plexo/connect] registration retries exhausted \u2014 running in standalone mode");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/connect/client.ts
|
|
163
|
+
var PlexoClient = class {
|
|
164
|
+
#opts;
|
|
165
|
+
#base;
|
|
166
|
+
constructor(opts) {
|
|
167
|
+
this.#opts = opts;
|
|
168
|
+
this.#base = opts.plexoUrl.replace(/\/$/, "");
|
|
169
|
+
}
|
|
170
|
+
get appId() {
|
|
171
|
+
return this.#opts.appId;
|
|
172
|
+
}
|
|
173
|
+
get isConfigured() {
|
|
174
|
+
return !!(this.#opts.plexoUrl && this.#opts.serviceKey);
|
|
175
|
+
}
|
|
176
|
+
// -----------------------------------------------------------------------
|
|
177
|
+
// Registration
|
|
178
|
+
// -----------------------------------------------------------------------
|
|
179
|
+
async register() {
|
|
180
|
+
if (!this.isConfigured) return;
|
|
181
|
+
let domain = "";
|
|
182
|
+
try {
|
|
183
|
+
domain = new URL(this.#opts.plexoUrl).host;
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
const profile = {
|
|
187
|
+
appId: this.#opts.appId,
|
|
188
|
+
displayName: this.#opts.displayName ?? this.#opts.appId,
|
|
189
|
+
domain,
|
|
190
|
+
extensions: this.#opts.extensions ?? [],
|
|
191
|
+
eventContracts: this.#opts.eventContracts ?? []
|
|
192
|
+
};
|
|
193
|
+
await register(this.#opts, profile);
|
|
194
|
+
}
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// Workspace
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
async ensureWorkspace(userId, email) {
|
|
199
|
+
const data = await this.#post(
|
|
200
|
+
"/api/v1/auth/workspace/ensure",
|
|
201
|
+
{ userId, email, name: this.#opts.displayName ?? this.#opts.appId },
|
|
202
|
+
{ userId }
|
|
203
|
+
);
|
|
204
|
+
return data.workspaceId;
|
|
205
|
+
}
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
// Connections
|
|
208
|
+
// -----------------------------------------------------------------------
|
|
209
|
+
async getInstalledConnections(workspaceId) {
|
|
210
|
+
try {
|
|
211
|
+
const data = await this.#get(
|
|
212
|
+
`/api/v1/connections/installed?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
213
|
+
);
|
|
214
|
+
return data.items ?? [];
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async getToken(workspaceId, registryId) {
|
|
220
|
+
try {
|
|
221
|
+
return await this.#get(
|
|
222
|
+
`/api/v1/connections/token?workspaceId=${encodeURIComponent(workspaceId)}®istryId=${encodeURIComponent(registryId)}`
|
|
223
|
+
);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (err instanceof PlexoApiError && err.status === 404) return null;
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async disconnect(workspaceId, connectionId) {
|
|
230
|
+
await this.#delete(
|
|
231
|
+
`/api/v1/connections/installed/${encodeURIComponent(connectionId)}?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
oauthPopupUrl(provider, workspaceId) {
|
|
235
|
+
return `${this.#base}/api/oauth/${encodeURIComponent(provider)}/start?workspaceId=${encodeURIComponent(workspaceId)}`;
|
|
236
|
+
}
|
|
237
|
+
// -----------------------------------------------------------------------
|
|
238
|
+
// AI
|
|
239
|
+
// -----------------------------------------------------------------------
|
|
240
|
+
async aiComplete(workspaceId, opts) {
|
|
241
|
+
const data = await this.#post(
|
|
242
|
+
"/api/v1/ai/complete",
|
|
243
|
+
{ workspaceId, ...opts },
|
|
244
|
+
{},
|
|
245
|
+
3e4
|
|
246
|
+
);
|
|
247
|
+
return data.text ?? "";
|
|
248
|
+
}
|
|
249
|
+
async chatMessage(workspaceId, userId, opts) {
|
|
250
|
+
const data = await this.#post(
|
|
251
|
+
"/api/v1/chat/message",
|
|
252
|
+
{ workspaceId, forceConversation: true, ...opts },
|
|
253
|
+
{ userId },
|
|
254
|
+
3e4
|
|
255
|
+
);
|
|
256
|
+
return {
|
|
257
|
+
reply: data.reply ?? "",
|
|
258
|
+
conversationId: data.conversationId,
|
|
259
|
+
sessionId: data.sessionId,
|
|
260
|
+
taskId: data.taskId
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// -----------------------------------------------------------------------
|
|
264
|
+
// Channel dispatch
|
|
265
|
+
// -----------------------------------------------------------------------
|
|
266
|
+
async dispatch(opts) {
|
|
267
|
+
const data = await this.#post(
|
|
268
|
+
"/api/v1/channel/dispatch",
|
|
269
|
+
opts
|
|
270
|
+
);
|
|
271
|
+
return { messageId: data.messageId, deliveryStatus: data.deliveryStatus };
|
|
272
|
+
}
|
|
273
|
+
// -----------------------------------------------------------------------
|
|
274
|
+
// Inbound
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
/** Returns a framework-agnostic handler for Plexo → app push requests. */
|
|
277
|
+
inbound(handlers) {
|
|
278
|
+
return createInboundRouter(this.#opts.serviceKey, handlers);
|
|
279
|
+
}
|
|
280
|
+
// -----------------------------------------------------------------------
|
|
281
|
+
// Utilities
|
|
282
|
+
// -----------------------------------------------------------------------
|
|
283
|
+
async testConnection() {
|
|
284
|
+
const start = Date.now();
|
|
285
|
+
try {
|
|
286
|
+
const fetchImpl = this.#opts.fetchImpl ?? fetch;
|
|
287
|
+
const res = await fetchImpl(`${this.#base}/api/health`, {
|
|
288
|
+
signal: AbortSignal.timeout(5e3)
|
|
289
|
+
});
|
|
290
|
+
const latencyMs = Date.now() - start;
|
|
291
|
+
if (!res.ok) {
|
|
292
|
+
return { ok: false, latencyMs, error: `HTTP ${res.status}` };
|
|
293
|
+
}
|
|
294
|
+
const body = await res.json().catch(() => ({}));
|
|
295
|
+
return { ok: true, latencyMs, plexoVersion: body["version"] };
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
latencyMs: Date.now() - start,
|
|
300
|
+
error: err instanceof Error ? err.message : String(err)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// -----------------------------------------------------------------------
|
|
305
|
+
// HTTP helpers
|
|
306
|
+
// -----------------------------------------------------------------------
|
|
307
|
+
#headers(extra = {}) {
|
|
308
|
+
const h = {
|
|
309
|
+
"Content-Type": "application/json",
|
|
310
|
+
Authorization: `Bearer ${this.#opts.serviceKey}`,
|
|
311
|
+
"X-App-Id": this.#opts.appId
|
|
312
|
+
};
|
|
313
|
+
if (extra.userId) h["X-User-Id"] = extra.userId;
|
|
314
|
+
return h;
|
|
315
|
+
}
|
|
316
|
+
async #get(path) {
|
|
317
|
+
const fetchImpl = this.#opts.fetchImpl ?? fetch;
|
|
318
|
+
const timeout = this.#opts.resilience?.timeoutMs ?? 15e3;
|
|
319
|
+
let res;
|
|
320
|
+
try {
|
|
321
|
+
res = await fetchImpl(`${this.#base}${path}`, {
|
|
322
|
+
headers: this.#headers(),
|
|
323
|
+
signal: AbortSignal.timeout(timeout)
|
|
324
|
+
});
|
|
325
|
+
} catch (err) {
|
|
326
|
+
throw new PlexoUnreachableError(this.#base, err);
|
|
327
|
+
}
|
|
328
|
+
return this.#parse(res, path);
|
|
329
|
+
}
|
|
330
|
+
async #post(path, body, extra = {}, timeoutMs) {
|
|
331
|
+
const fetchImpl = this.#opts.fetchImpl ?? fetch;
|
|
332
|
+
const timeout = timeoutMs ?? this.#opts.resilience?.timeoutMs ?? 15e3;
|
|
333
|
+
let res;
|
|
334
|
+
try {
|
|
335
|
+
res = await fetchImpl(`${this.#base}${path}`, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: this.#headers(extra),
|
|
338
|
+
body: JSON.stringify(body),
|
|
339
|
+
signal: AbortSignal.timeout(timeout)
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
throw new PlexoUnreachableError(this.#base, err);
|
|
343
|
+
}
|
|
344
|
+
return this.#parse(res, path);
|
|
345
|
+
}
|
|
346
|
+
async #delete(path) {
|
|
347
|
+
const fetchImpl = this.#opts.fetchImpl ?? fetch;
|
|
348
|
+
const timeout = this.#opts.resilience?.timeoutMs ?? 15e3;
|
|
349
|
+
let res;
|
|
350
|
+
try {
|
|
351
|
+
res = await fetchImpl(`${this.#base}${path}`, {
|
|
352
|
+
method: "DELETE",
|
|
353
|
+
headers: this.#headers(),
|
|
354
|
+
signal: AbortSignal.timeout(timeout)
|
|
355
|
+
});
|
|
356
|
+
} catch (err) {
|
|
357
|
+
throw new PlexoUnreachableError(this.#base, err);
|
|
358
|
+
}
|
|
359
|
+
if (!res.ok && res.status !== 404) {
|
|
360
|
+
await this.#parse(res, path);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async #parse(res, path) {
|
|
364
|
+
if (res.status === 401) throw new PlexoAuthError(path);
|
|
365
|
+
if (res.status === 429) {
|
|
366
|
+
const retry = Number(res.headers.get("retry-after"));
|
|
367
|
+
throw new PlexoRateLimitedError(path, Number.isNaN(retry) ? void 0 : retry);
|
|
368
|
+
}
|
|
369
|
+
if (!res.ok) {
|
|
370
|
+
const detail = await res.text().catch(() => "");
|
|
371
|
+
throw new PlexoApiError(res.status, path, detail.slice(0, 300));
|
|
372
|
+
}
|
|
373
|
+
if (res.status === 204) return void 0;
|
|
374
|
+
return res.json();
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/connect/index.ts
|
|
379
|
+
function createPlexoClient(opts) {
|
|
380
|
+
return new PlexoClient(opts);
|
|
381
|
+
}
|
|
382
|
+
export {
|
|
383
|
+
PlexoApiError,
|
|
384
|
+
PlexoAuthError,
|
|
385
|
+
PlexoClient,
|
|
386
|
+
PlexoNotConfiguredError,
|
|
387
|
+
PlexoRateLimitedError,
|
|
388
|
+
PlexoUnreachableError,
|
|
389
|
+
createPlexoClient,
|
|
390
|
+
verifyInboundSignature
|
|
391
|
+
};
|
|
392
|
+
//# sourceMappingURL=index.js.map
|