viasocket-apps 0.1.1 → 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 +66 -19
- package/dist/cjs/browser.js +9 -1
- package/dist/cjs/client.js +78 -4
- package/dist/cjs/http.js +7 -5
- package/dist/cjs/index.js +2 -2
- package/dist/esm/browser.d.ts +14 -1
- package/dist/esm/browser.js +9 -1
- package/dist/esm/client.d.ts +25 -4
- package/dist/esm/client.js +78 -4
- package/dist/esm/http.d.ts +5 -4
- package/dist/esm/http.js +7 -5
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/types.d.ts +47 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,28 @@ The same calls are documented as plain HTTP in your viaSocket dashboard under
|
|
|
15
15
|
**Integrations → your Apps API integration → API reference**. Use whichever suits your stack;
|
|
16
16
|
this package is a thin, typed layer over those endpoints and adds nothing they don't have.
|
|
17
17
|
|
|
18
|
+
## For coding agents
|
|
19
|
+
|
|
20
|
+
[](https://skills.sh/Walkover-Web-Solution/viasocket-apps)
|
|
21
|
+
|
|
22
|
+
This repo also carries the **viaSocket integrations skill** — what a coding agent reads to
|
|
23
|
+
integrate any app into your product, with or without this package. Install it into Claude Code,
|
|
24
|
+
Cursor, Codex, Copilot, Gemini CLI, Windsurf, Cline and 20+ other agents:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx skills add Walkover-Web-Solution/viasocket-apps
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add `-g` to install it for every project. No install? Paste this into your agent:
|
|
31
|
+
`Read https://flow.viasocket.com/documentation/skill.md and follow it.`
|
|
32
|
+
|
|
33
|
+
The agent needs three values from the viaSocket dashboard (**Integrations → Create embed →
|
|
34
|
+
Install Code**): `org_id` and `project_id`, which it asks for, and `VIASOCKET_EMBED_SECRET`, which
|
|
35
|
+
goes in your server's `.env`. The skill is
|
|
36
|
+
[skills/viasocket-integrations/SKILL.md](skills/viasocket-integrations/SKILL.md), generated by
|
|
37
|
+
viaSocket and mirrored here; the same text filled with your ids is served at
|
|
38
|
+
`https://flow.viasocket.com/documentation/skill.md?org=<org_id>&project=<project_id>`.
|
|
39
|
+
|
|
18
40
|
## Server
|
|
19
41
|
|
|
20
42
|
```js
|
|
@@ -49,12 +71,14 @@ const result = await viasocket.runAction(scriptId, actionVersionId, {
|
|
|
49
71
|
column_key: true
|
|
50
72
|
})
|
|
51
73
|
|
|
52
|
-
// 5. Or subscribe to an event. Needs only the connection, not enable().
|
|
74
|
+
// 5. Or subscribe to an event. Needs only the connection, not enable(). `code` is a handler we
|
|
75
|
+
// run on our servers when the event fires — see "Events" below. Save the scriptId it returns
|
|
76
|
+
// together with the inputData and handler you sent: the flows list never returns those.
|
|
53
77
|
const subscription = await user.subscribe(triggerVersionId, {
|
|
54
78
|
authId,
|
|
55
79
|
inputData: { channel_id: ['C08SXCV3J85'] },
|
|
56
|
-
|
|
57
|
-
meta: {
|
|
80
|
+
code: handler,
|
|
81
|
+
meta: { user_id: 'user_123' }
|
|
58
82
|
})
|
|
59
83
|
```
|
|
60
84
|
|
|
@@ -64,7 +88,9 @@ const subscription = await user.subscribe(triggerVersionId, {
|
|
|
64
88
|
import { connect } from 'viasocket-apps/browser'
|
|
65
89
|
|
|
66
90
|
// embedToken comes from your backend (user.token()), signed for the signed-in user.
|
|
67
|
-
|
|
91
|
+
// actions (optional): the rowids of the actions and triggers you use — the popup then asks only
|
|
92
|
+
// for their scopes; skipActionSelection goes straight to the app's consent screen.
|
|
93
|
+
const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc', actions: ['<action rowid>'], skipActionSelection: true })
|
|
68
94
|
// Send authId to your backend, which enables the app and stores the script_id.
|
|
69
95
|
```
|
|
70
96
|
|
|
@@ -79,27 +105,48 @@ const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc' })
|
|
|
79
105
|
| `user.token()` | Signed embed token for that user; hand it to the frontend. |
|
|
80
106
|
| `user.enable(serviceId, authId)` | Turn a connection into a `scriptId` for running actions. Once per connection. |
|
|
81
107
|
| `user.findEnabled(serviceId)` | The `scriptId` if this app is already enabled, else `null`. Call before `enable`. |
|
|
82
|
-
| `user.listOptions(actionVersionId, { fieldKey, authId, existingFields })` | The values a field accepts, as `{ options, offset }`. Both response shapes normalised. |
|
|
108
|
+
| `user.listOptions(actionVersionId, { fieldKey, authId, existingFields })` | The values a field accepts, as `{ options, offset }`. Both response shapes normalised; an unreadable connection (revoked, another user's `auth_id`) throws a `ViaSocketError` with status 400 even though the API answers 200. |
|
|
83
109
|
| `viasocket.runAction(scriptId, actionVersionId, inputData)` | Run one action. Returns the app's own response. |
|
|
84
|
-
| `
|
|
110
|
+
| `viasocket.catalog.search(key)` | The best 30 apps for what the user typed, as `{ serviceId, name, iconUrl, description }`. No token. |
|
|
111
|
+
| `viasocket.catalog.list({ limit, offset, category })` | Every app, most used first, 200 a page; an empty page ends it. Drop apps whose `versions()` is empty. No token. |
|
|
112
|
+
| `viasocket.catalog.versions(serviceId)` | Every published action and trigger of one app, as `{ actions, triggers }`, each with its `inputjson` schema and version id. No token. |
|
|
113
|
+
| `user.subscribe(triggerVersionId, { authId, inputData, code \| webhook, meta })` | Subscribe to an app event with a handler we run per event. Returns `{ scriptId, hookUrl, … }`. |
|
|
85
114
|
| `user.updateSubscription(scriptId, { code, meta })` | Change a live subscription in place. |
|
|
86
115
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
## Events
|
|
117
|
+
|
|
118
|
+
Subscribing to an event means telling us **what to do when it fires**. That is `code`: a handler we
|
|
119
|
+
run on viaSocket's servers per event, with the event in `context.req.body`. Your server is not in
|
|
120
|
+
the path. Most often the handler runs an action in another app the user connected — the run URL
|
|
121
|
+
takes no token, the `script_id` is the credential — or calls your own API with your own auth header
|
|
122
|
+
baked in. It is a string executed away from your process, so it must stand alone: no imports,
|
|
123
|
+
nothing from your codebase; `axios`, `fetch` and `context` are in scope.
|
|
91
124
|
|
|
92
125
|
```js
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
// A string, not a function: it runs on viaSocket's servers each time the event fires.
|
|
127
|
+
// In scope there: axios, fetch, context. The event the app sent is context.req.body.
|
|
128
|
+
// ${…} are YOUR variables, baked in per user when you subscribe.
|
|
129
|
+
const handler = `
|
|
130
|
+
const event = context.req.body
|
|
131
|
+
const response = await fetch("https://flow.sokt.io/func/${slackScriptId}", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
action_version_id: "${slackSendMessageVersionId}",
|
|
136
|
+
inputData: { channel_id: "${channelPickedAtSetup}", text: "New mail from " + event.from + ": " + event.subject }
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
return await response.json()
|
|
140
|
+
`
|
|
141
|
+
|
|
142
|
+
await user.subscribe(gmailNewMailVersionId, { authId, inputData: {}, code: handler, meta: { user_id: uniqueIdentifier } })
|
|
102
143
|
```
|
|
144
|
+
|
|
145
|
+
`webhook` is the edge case: a public, unauthenticated URL of yours that we POST every raw event to.
|
|
146
|
+
Use it only when that is what you want, and never both.
|
|
147
|
+
|
|
148
|
+
Some triggers are polled rather than pushed. For those, `inputData` may carry `scheduledTime`, the
|
|
149
|
+
minutes between checks, as a string: `"5"` or `"15"`.
|
|
103
150
|
| `user.listFlows()` | Every enabled app and subscription this user has. |
|
|
104
151
|
| `user.disableFlow(scriptId)` / `user.enableFlow(scriptId)` | Turn a flow off or back on. Disabling a subscription ends it. |
|
|
105
152
|
| `user.listConnections()` | Every app this user has connected. |
|
package/dist/cjs/browser.js
CHANGED
|
@@ -101,6 +101,14 @@ async function connect(options) {
|
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
103
|
window.addEventListener('message', onMessage);
|
|
104
|
-
|
|
104
|
+
const scriptOptions = {};
|
|
105
|
+
if (options.actions?.length)
|
|
106
|
+
scriptOptions.filteredActions = options.actions;
|
|
107
|
+
if (options.skipActionSelection)
|
|
108
|
+
scriptOptions.skipActionSelection = true;
|
|
109
|
+
if (Object.keys(scriptOptions).length)
|
|
110
|
+
open(options.embedToken, serviceId, scriptOptions);
|
|
111
|
+
else
|
|
112
|
+
open(options.embedToken, serviceId);
|
|
105
113
|
});
|
|
106
114
|
}
|
package/dist/cjs/client.js
CHANGED
|
@@ -15,6 +15,13 @@ const http_js_1 = require("./http.js");
|
|
|
15
15
|
const token_js_1 = require("./token.js");
|
|
16
16
|
const DEFAULT_API_BASE_URL = 'https://flow-api.viasocket.com';
|
|
17
17
|
const DEFAULT_RUN_BASE_URL = 'https://flow.sokt.io';
|
|
18
|
+
/** The public catalog: no token, the same data in every environment. */
|
|
19
|
+
const CATALOG = {
|
|
20
|
+
search: 'https://flow.sokt.io/func/scri12BSufQM',
|
|
21
|
+
list: 'https://plug-service.viasocket.com/api/v1/plugins/all',
|
|
22
|
+
versions: 'https://flow.sokt.io/func/scriolZue69X'
|
|
23
|
+
};
|
|
24
|
+
const LIST_LIMIT_MAX = 200;
|
|
18
25
|
function required(name, value) {
|
|
19
26
|
if (typeof value !== 'string' || !value.trim())
|
|
20
27
|
throw new errors_js_1.ViaSocketError(`${name} is required`, { status: null });
|
|
@@ -42,6 +49,67 @@ class ViaSocket {
|
|
|
42
49
|
// Bound so a bare `fetch` reference still has the right `this` on runtimes that care.
|
|
43
50
|
this.transport = options.fetch ? transport : transport.bind(globalThis);
|
|
44
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The catalog: which apps exist and what each can do. Public, no token, nothing per user — so
|
|
54
|
+
* it lives on the root client and is safe to call from a browser too.
|
|
55
|
+
*/
|
|
56
|
+
catalog = {
|
|
57
|
+
/** The best 30 apps for what the user typed. A type-ahead: an empty key returns nothing. */
|
|
58
|
+
search: async (key) => {
|
|
59
|
+
const url = `${CATALOG.search}?key=${encodeURIComponent(required('key', key))}`;
|
|
60
|
+
const { data } = await (0, http_js_1.request)({ method: 'GET', url, fetchImpl: this.transport });
|
|
61
|
+
return (Array.isArray(data) ? data : []).map((row) => ({
|
|
62
|
+
serviceId: row.service_id,
|
|
63
|
+
name: row.name,
|
|
64
|
+
description: row.description ?? '',
|
|
65
|
+
iconUrl: row.iconurl ?? ''
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Every app, most used first, a page at a time (200 at most). An empty page ends it. The list
|
|
70
|
+
* is the whole table — far down it holds apps with nothing published — so a grid drops every
|
|
71
|
+
* app whose `versions()` comes back empty.
|
|
72
|
+
*/
|
|
73
|
+
list: async (params = {}) => {
|
|
74
|
+
const query = new URLSearchParams({
|
|
75
|
+
limit: String(Math.min(Math.max(1, params.limit ?? LIST_LIMIT_MAX), LIST_LIMIT_MAX)),
|
|
76
|
+
offset: String(Math.max(0, params.offset ?? 0))
|
|
77
|
+
});
|
|
78
|
+
if (params.category)
|
|
79
|
+
query.set('category', params.category);
|
|
80
|
+
// The body is `{ message, data: [...] }` with no `success` key, so it is not the envelope and
|
|
81
|
+
// arrives whole as `data`.
|
|
82
|
+
const { data } = await (0, http_js_1.request)({
|
|
83
|
+
method: 'GET',
|
|
84
|
+
url: `${CATALOG.list}?${query}`,
|
|
85
|
+
fetchImpl: this.transport
|
|
86
|
+
});
|
|
87
|
+
const rows = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : [];
|
|
88
|
+
return rows.map((row) => ({
|
|
89
|
+
serviceId: String(row.rowid ?? ''),
|
|
90
|
+
name: String(row.name ?? ''),
|
|
91
|
+
description: String(row.description ?? ''),
|
|
92
|
+
iconUrl: String(row.iconurl ?? ''),
|
|
93
|
+
category: Array.isArray(row.category) ? row.category : [],
|
|
94
|
+
domain: typeof row.domain === 'string' ? row.domain : undefined
|
|
95
|
+
}));
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Every published action and trigger of one app, with the schema (`inputjson`) a form is
|
|
99
|
+
* rendered from and the version id the run and subscribe calls take. Empty for an app with
|
|
100
|
+
* nothing published. Cache it per app; ids are stable and schemas change rarely.
|
|
101
|
+
*/
|
|
102
|
+
versions: async (serviceId) => {
|
|
103
|
+
const { data } = await (0, http_js_1.request)({
|
|
104
|
+
method: 'POST',
|
|
105
|
+
url: CATALOG.versions,
|
|
106
|
+
body: JSON.stringify({ service_id: required('serviceId', serviceId) }),
|
|
107
|
+
fetchImpl: this.transport
|
|
108
|
+
});
|
|
109
|
+
const rows = Array.isArray(data) ? data : [];
|
|
110
|
+
return { actions: rows.filter((row) => row.type === 'action'), triggers: rows.filter((row) => row.type === 'trigger') };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
45
113
|
/** Everything that is done for one of your end users. */
|
|
46
114
|
user(uniqueIdentifier) {
|
|
47
115
|
return new UserScope(this, required('uniqueIdentifier', uniqueIdentifier));
|
|
@@ -124,16 +192,22 @@ class UserScope {
|
|
|
124
192
|
});
|
|
125
193
|
if (Array.isArray(data))
|
|
126
194
|
return { options: data, offset: null };
|
|
195
|
+
// A connection that cannot be read (revoked, or another user's auth_id) is answered with 200 and
|
|
196
|
+
// success: true, the failure nested in data. It is an error, and the message is the explanation.
|
|
197
|
+
if (data?.response && Number(data.response.status) >= 400) {
|
|
198
|
+
throw new errors_js_1.ViaSocketError(data.response.data?.message || 'list-options failed', { status: Number(data.response.status), body: data });
|
|
199
|
+
}
|
|
127
200
|
return { options: Array.isArray(data?.data) ? data.data : [], offset: data?.offset ?? null };
|
|
128
201
|
}
|
|
129
202
|
/**
|
|
130
|
-
* Subscribes to an app event
|
|
131
|
-
*
|
|
132
|
-
*
|
|
203
|
+
* Subscribes to an app event with a handler: `code`, a self-contained script we run on our
|
|
204
|
+
* servers each time the event fires, with the event at `context.req.body`. It runs an action in
|
|
205
|
+
* another app the user connected, or calls your own API. `webhook` (a public URL we POST raw
|
|
206
|
+
* events to) is the edge case. Exactly one of the two.
|
|
133
207
|
*/
|
|
134
208
|
async subscribe(triggerVersionId, params) {
|
|
135
209
|
if (!params?.webhook === !params?.code) {
|
|
136
|
-
throw new errors_js_1.ViaSocketError('subscribe: pass exactly one of `
|
|
210
|
+
throw new errors_js_1.ViaSocketError('subscribe: pass exactly one of `code` (a handler we run per event) or `webhook`', { status: null });
|
|
137
211
|
}
|
|
138
212
|
const body = {
|
|
139
213
|
auth_id: required('authId', params.authId),
|
package/dist/cjs/http.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* that message is almost always the explanation.
|
|
9
9
|
*
|
|
10
10
|
* The action runner is the exception: some actions answer in that envelope, many hand back their
|
|
11
|
-
* own result at the top level with no `success` or `data` key at all (Gmail's list-mails returns
|
|
12
|
-
* `{ emails, nextPageToken }`)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* own result at the top level — with no `success` or `data` key at all (Gmail's list-mails returns
|
|
12
|
+
* `{ emails, nextPageToken }`), or with a `success` key of their own and no `data` (Slack's "Find
|
|
13
|
+
* Public channel" returns `{ success, channels }`). So a body is the envelope only when it carries a
|
|
14
|
+
* boolean `success` AND a `data` key, or is a refusal (`success: false`); anything else is the
|
|
15
|
+
* result itself. Getting this wrong returned `undefined` for every such action.
|
|
15
16
|
*/
|
|
16
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
18
|
exports.request = request;
|
|
@@ -35,7 +36,8 @@ async function request(options) {
|
|
|
35
36
|
catch {
|
|
36
37
|
payload = text;
|
|
37
38
|
}
|
|
38
|
-
const
|
|
39
|
+
const shaped = payload !== null && typeof payload === 'object' ? payload : null;
|
|
40
|
+
const isEnvelope = shaped !== null && typeof shaped.success === 'boolean' && ('data' in shaped || shaped.success === false);
|
|
39
41
|
const envelope = isEnvelope ? payload : null;
|
|
40
42
|
if (!response.ok || envelope?.success === false) {
|
|
41
43
|
const raw = payload && typeof payload === 'object' ? payload : null;
|
package/dist/cjs/index.js
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* import { ViaSocket } from 'viasocket-apps'
|
|
6
6
|
*
|
|
7
7
|
* const viasocket = new ViaSocket({ orgId, projectId, secret: process.env.VIASOCKET_EMBED_SECRET })
|
|
8
|
-
* const user = viasocket.user(
|
|
8
|
+
* const user = viasocket.user(uniqueIdentifier)
|
|
9
9
|
*
|
|
10
10
|
* const token = await user.token() // for the connect popup
|
|
11
11
|
* const { scriptId } = await user.enable(serviceId, authId) // once, for actions
|
|
12
12
|
* const { options } = await user.listOptions(actionVersionId, {...}) // fill a dropdown
|
|
13
13
|
* const result = await viasocket.runAction(scriptId, actionVersionId, inputData)
|
|
14
|
-
* const sub = await user.subscribe(triggerVersionId, { authId, inputData,
|
|
14
|
+
* const sub = await user.subscribe(triggerVersionId, { authId, inputData, code: handler })
|
|
15
15
|
*
|
|
16
16
|
* The browser half — opening the connect popup — lives at 'viasocket-apps/browser'.
|
|
17
17
|
*/
|
package/dist/esm/browser.d.ts
CHANGED
|
@@ -27,9 +27,21 @@ export interface ConnectOptions {
|
|
|
27
27
|
embedToken: string;
|
|
28
28
|
/** The app to connect. Its service_id is shown on every app in the Apps API reference. */
|
|
29
29
|
serviceId: string;
|
|
30
|
+
/**
|
|
31
|
+
* The rowids of the actions and triggers this product uses (each is listed with its action in
|
|
32
|
+
* the app's document). The popup then asks only for their scopes. Omit to offer every action.
|
|
33
|
+
*/
|
|
34
|
+
actions?: string[];
|
|
35
|
+
/** With `actions`: skip the action list and open the app's consent screen directly. */
|
|
36
|
+
skipActionSelection?: boolean;
|
|
30
37
|
/** Where the connect script is served from. Only override for a non-production environment. */
|
|
31
38
|
scriptUrl?: string;
|
|
32
39
|
}
|
|
40
|
+
/** The third argument of the connect script's `openViasocketConnection`. */
|
|
41
|
+
interface ConnectScriptOptions {
|
|
42
|
+
filteredActions?: string[];
|
|
43
|
+
skipActionSelection?: boolean;
|
|
44
|
+
}
|
|
33
45
|
export interface ConnectResult {
|
|
34
46
|
/** The connection. Every later call for this app takes it as auth_id. */
|
|
35
47
|
authId: string;
|
|
@@ -39,7 +51,7 @@ export interface ConnectResult {
|
|
|
39
51
|
}
|
|
40
52
|
declare global {
|
|
41
53
|
interface Window {
|
|
42
|
-
openViasocketConnection?: (embedToken: string, serviceId: string) => void;
|
|
54
|
+
openViasocketConnection?: (embedToken: string, serviceId: string, options?: ConnectScriptOptions) => void;
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
/**
|
|
@@ -54,3 +66,4 @@ export declare function loadConnectScript(scriptUrl?: string): Promise<void>;
|
|
|
54
66
|
* 'rejected' if the app said no, 'script' if the connect script could not load.
|
|
55
67
|
*/
|
|
56
68
|
export declare function connect(options: ConnectOptions): Promise<ConnectResult>;
|
|
69
|
+
export {};
|
package/dist/esm/browser.js
CHANGED
|
@@ -95,6 +95,14 @@ export async function connect(options) {
|
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
97
|
window.addEventListener('message', onMessage);
|
|
98
|
-
|
|
98
|
+
const scriptOptions = {};
|
|
99
|
+
if (options.actions?.length)
|
|
100
|
+
scriptOptions.filteredActions = options.actions;
|
|
101
|
+
if (options.skipActionSelection)
|
|
102
|
+
scriptOptions.skipActionSelection = true;
|
|
103
|
+
if (Object.keys(scriptOptions).length)
|
|
104
|
+
open(options.embedToken, serviceId, scriptOptions);
|
|
105
|
+
else
|
|
106
|
+
open(options.embedToken, serviceId);
|
|
99
107
|
});
|
|
100
108
|
}
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* everything else behind `user(id)` makes that distinction structural rather than something a
|
|
8
8
|
* reader has to remember.
|
|
9
9
|
*/
|
|
10
|
-
import type { EnableResult, Flow, FlowStatus, ListOptionsParams, ListOptionsResult, SubscribeParams, SubscribeResult, UpdateSubscriptionParams, ViaSocketOptions } from './types.js';
|
|
10
|
+
import type { CatalogApp, CatalogVersions, EnableResult, Flow, FlowStatus, ListOptionsParams, ListAppsParams, ListOptionsResult, SubscribeParams, SubscribeResult, UpdateSubscriptionParams, ViaSocketOptions } from './types.js';
|
|
11
11
|
export declare class ViaSocket {
|
|
12
12
|
readonly orgId: string;
|
|
13
13
|
readonly projectId: string;
|
|
@@ -15,6 +15,26 @@ export declare class ViaSocket {
|
|
|
15
15
|
readonly runBaseUrl: string;
|
|
16
16
|
private readonly secret;
|
|
17
17
|
constructor(options: ViaSocketOptions);
|
|
18
|
+
/**
|
|
19
|
+
* The catalog: which apps exist and what each can do. Public, no token, nothing per user — so
|
|
20
|
+
* it lives on the root client and is safe to call from a browser too.
|
|
21
|
+
*/
|
|
22
|
+
readonly catalog: {
|
|
23
|
+
/** The best 30 apps for what the user typed. A type-ahead: an empty key returns nothing. */
|
|
24
|
+
search: (key: string) => Promise<CatalogApp[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Every app, most used first, a page at a time (200 at most). An empty page ends it. The list
|
|
27
|
+
* is the whole table — far down it holds apps with nothing published — so a grid drops every
|
|
28
|
+
* app whose `versions()` comes back empty.
|
|
29
|
+
*/
|
|
30
|
+
list: (params?: ListAppsParams) => Promise<CatalogApp[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Every published action and trigger of one app, with the schema (`inputjson`) a form is
|
|
33
|
+
* rendered from and the version id the run and subscribe calls take. Empty for an app with
|
|
34
|
+
* nothing published. Cache it per app; ids are stable and schemas change rarely.
|
|
35
|
+
*/
|
|
36
|
+
versions: (serviceId: string) => Promise<CatalogVersions>;
|
|
37
|
+
};
|
|
18
38
|
/** Everything that is done for one of your end users. */
|
|
19
39
|
user(uniqueIdentifier: string): UserScope;
|
|
20
40
|
/**
|
|
@@ -56,9 +76,10 @@ export declare class UserScope {
|
|
|
56
76
|
*/
|
|
57
77
|
listOptions(actionVersionId: string, params: ListOptionsParams): Promise<ListOptionsResult>;
|
|
58
78
|
/**
|
|
59
|
-
* Subscribes to an app event
|
|
60
|
-
*
|
|
61
|
-
*
|
|
79
|
+
* Subscribes to an app event with a handler: `code`, a self-contained script we run on our
|
|
80
|
+
* servers each time the event fires, with the event at `context.req.body`. It runs an action in
|
|
81
|
+
* another app the user connected, or calls your own API. `webhook` (a public URL we POST raw
|
|
82
|
+
* events to) is the edge case. Exactly one of the two.
|
|
62
83
|
*/
|
|
63
84
|
subscribe(triggerVersionId: string, params: SubscribeParams): Promise<SubscribeResult>;
|
|
64
85
|
/** Changes a live subscription in place. The script_id does not change. */
|
package/dist/esm/client.js
CHANGED
|
@@ -12,6 +12,13 @@ import { request } from './http.js';
|
|
|
12
12
|
import { signEmbedToken } from './token.js';
|
|
13
13
|
const DEFAULT_API_BASE_URL = 'https://flow-api.viasocket.com';
|
|
14
14
|
const DEFAULT_RUN_BASE_URL = 'https://flow.sokt.io';
|
|
15
|
+
/** The public catalog: no token, the same data in every environment. */
|
|
16
|
+
const CATALOG = {
|
|
17
|
+
search: 'https://flow.sokt.io/func/scri12BSufQM',
|
|
18
|
+
list: 'https://plug-service.viasocket.com/api/v1/plugins/all',
|
|
19
|
+
versions: 'https://flow.sokt.io/func/scriolZue69X'
|
|
20
|
+
};
|
|
21
|
+
const LIST_LIMIT_MAX = 200;
|
|
15
22
|
function required(name, value) {
|
|
16
23
|
if (typeof value !== 'string' || !value.trim())
|
|
17
24
|
throw new ViaSocketError(`${name} is required`, { status: null });
|
|
@@ -39,6 +46,67 @@ export class ViaSocket {
|
|
|
39
46
|
// Bound so a bare `fetch` reference still has the right `this` on runtimes that care.
|
|
40
47
|
this.transport = options.fetch ? transport : transport.bind(globalThis);
|
|
41
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The catalog: which apps exist and what each can do. Public, no token, nothing per user — so
|
|
51
|
+
* it lives on the root client and is safe to call from a browser too.
|
|
52
|
+
*/
|
|
53
|
+
catalog = {
|
|
54
|
+
/** The best 30 apps for what the user typed. A type-ahead: an empty key returns nothing. */
|
|
55
|
+
search: async (key) => {
|
|
56
|
+
const url = `${CATALOG.search}?key=${encodeURIComponent(required('key', key))}`;
|
|
57
|
+
const { data } = await request({ method: 'GET', url, fetchImpl: this.transport });
|
|
58
|
+
return (Array.isArray(data) ? data : []).map((row) => ({
|
|
59
|
+
serviceId: row.service_id,
|
|
60
|
+
name: row.name,
|
|
61
|
+
description: row.description ?? '',
|
|
62
|
+
iconUrl: row.iconurl ?? ''
|
|
63
|
+
}));
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Every app, most used first, a page at a time (200 at most). An empty page ends it. The list
|
|
67
|
+
* is the whole table — far down it holds apps with nothing published — so a grid drops every
|
|
68
|
+
* app whose `versions()` comes back empty.
|
|
69
|
+
*/
|
|
70
|
+
list: async (params = {}) => {
|
|
71
|
+
const query = new URLSearchParams({
|
|
72
|
+
limit: String(Math.min(Math.max(1, params.limit ?? LIST_LIMIT_MAX), LIST_LIMIT_MAX)),
|
|
73
|
+
offset: String(Math.max(0, params.offset ?? 0))
|
|
74
|
+
});
|
|
75
|
+
if (params.category)
|
|
76
|
+
query.set('category', params.category);
|
|
77
|
+
// The body is `{ message, data: [...] }` with no `success` key, so it is not the envelope and
|
|
78
|
+
// arrives whole as `data`.
|
|
79
|
+
const { data } = await request({
|
|
80
|
+
method: 'GET',
|
|
81
|
+
url: `${CATALOG.list}?${query}`,
|
|
82
|
+
fetchImpl: this.transport
|
|
83
|
+
});
|
|
84
|
+
const rows = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : [];
|
|
85
|
+
return rows.map((row) => ({
|
|
86
|
+
serviceId: String(row.rowid ?? ''),
|
|
87
|
+
name: String(row.name ?? ''),
|
|
88
|
+
description: String(row.description ?? ''),
|
|
89
|
+
iconUrl: String(row.iconurl ?? ''),
|
|
90
|
+
category: Array.isArray(row.category) ? row.category : [],
|
|
91
|
+
domain: typeof row.domain === 'string' ? row.domain : undefined
|
|
92
|
+
}));
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Every published action and trigger of one app, with the schema (`inputjson`) a form is
|
|
96
|
+
* rendered from and the version id the run and subscribe calls take. Empty for an app with
|
|
97
|
+
* nothing published. Cache it per app; ids are stable and schemas change rarely.
|
|
98
|
+
*/
|
|
99
|
+
versions: async (serviceId) => {
|
|
100
|
+
const { data } = await request({
|
|
101
|
+
method: 'POST',
|
|
102
|
+
url: CATALOG.versions,
|
|
103
|
+
body: JSON.stringify({ service_id: required('serviceId', serviceId) }),
|
|
104
|
+
fetchImpl: this.transport
|
|
105
|
+
});
|
|
106
|
+
const rows = Array.isArray(data) ? data : [];
|
|
107
|
+
return { actions: rows.filter((row) => row.type === 'action'), triggers: rows.filter((row) => row.type === 'trigger') };
|
|
108
|
+
}
|
|
109
|
+
};
|
|
42
110
|
/** Everything that is done for one of your end users. */
|
|
43
111
|
user(uniqueIdentifier) {
|
|
44
112
|
return new UserScope(this, required('uniqueIdentifier', uniqueIdentifier));
|
|
@@ -120,16 +188,22 @@ export class UserScope {
|
|
|
120
188
|
});
|
|
121
189
|
if (Array.isArray(data))
|
|
122
190
|
return { options: data, offset: null };
|
|
191
|
+
// A connection that cannot be read (revoked, or another user's auth_id) is answered with 200 and
|
|
192
|
+
// success: true, the failure nested in data. It is an error, and the message is the explanation.
|
|
193
|
+
if (data?.response && Number(data.response.status) >= 400) {
|
|
194
|
+
throw new ViaSocketError(data.response.data?.message || 'list-options failed', { status: Number(data.response.status), body: data });
|
|
195
|
+
}
|
|
123
196
|
return { options: Array.isArray(data?.data) ? data.data : [], offset: data?.offset ?? null };
|
|
124
197
|
}
|
|
125
198
|
/**
|
|
126
|
-
* Subscribes to an app event
|
|
127
|
-
*
|
|
128
|
-
*
|
|
199
|
+
* Subscribes to an app event with a handler: `code`, a self-contained script we run on our
|
|
200
|
+
* servers each time the event fires, with the event at `context.req.body`. It runs an action in
|
|
201
|
+
* another app the user connected, or calls your own API. `webhook` (a public URL we POST raw
|
|
202
|
+
* events to) is the edge case. Exactly one of the two.
|
|
129
203
|
*/
|
|
130
204
|
async subscribe(triggerVersionId, params) {
|
|
131
205
|
if (!params?.webhook === !params?.code) {
|
|
132
|
-
throw new ViaSocketError('subscribe: pass exactly one of `
|
|
206
|
+
throw new ViaSocketError('subscribe: pass exactly one of `code` (a handler we run per event) or `webhook`', { status: null });
|
|
133
207
|
}
|
|
134
208
|
const body = {
|
|
135
209
|
auth_id: required('authId', params.authId),
|
package/dist/esm/http.d.ts
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* that message is almost always the explanation.
|
|
8
8
|
*
|
|
9
9
|
* The action runner is the exception: some actions answer in that envelope, many hand back their
|
|
10
|
-
* own result at the top level with no `success` or `data` key at all (Gmail's list-mails returns
|
|
11
|
-
* `{ emails, nextPageToken }`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* own result at the top level — with no `success` or `data` key at all (Gmail's list-mails returns
|
|
11
|
+
* `{ emails, nextPageToken }`), or with a `success` key of their own and no `data` (Slack's "Find
|
|
12
|
+
* Public channel" returns `{ success, channels }`). So a body is the envelope only when it carries a
|
|
13
|
+
* boolean `success` AND a `data` key, or is a refusal (`success: false`); anything else is the
|
|
14
|
+
* result itself. Getting this wrong returned `undefined` for every such action.
|
|
14
15
|
*/
|
|
15
16
|
import type { Envelope } from './types.js';
|
|
16
17
|
export interface RequestOptions {
|
package/dist/esm/http.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* that message is almost always the explanation.
|
|
8
8
|
*
|
|
9
9
|
* The action runner is the exception: some actions answer in that envelope, many hand back their
|
|
10
|
-
* own result at the top level with no `success` or `data` key at all (Gmail's list-mails returns
|
|
11
|
-
* `{ emails, nextPageToken }`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* own result at the top level — with no `success` or `data` key at all (Gmail's list-mails returns
|
|
11
|
+
* `{ emails, nextPageToken }`), or with a `success` key of their own and no `data` (Slack's "Find
|
|
12
|
+
* Public channel" returns `{ success, channels }`). So a body is the envelope only when it carries a
|
|
13
|
+
* boolean `success` AND a `data` key, or is a refusal (`success: false`); anything else is the
|
|
14
|
+
* result itself. Getting this wrong returned `undefined` for every such action.
|
|
14
15
|
*/
|
|
15
16
|
import { ViaSocketError } from './errors.js';
|
|
16
17
|
export async function request(options) {
|
|
@@ -32,7 +33,8 @@ export async function request(options) {
|
|
|
32
33
|
catch {
|
|
33
34
|
payload = text;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
+
const shaped = payload !== null && typeof payload === 'object' ? payload : null;
|
|
37
|
+
const isEnvelope = shaped !== null && typeof shaped.success === 'boolean' && ('data' in shaped || shaped.success === false);
|
|
36
38
|
const envelope = isEnvelope ? payload : null;
|
|
37
39
|
if (!response.ok || envelope?.success === false) {
|
|
38
40
|
const raw = payload && typeof payload === 'object' ? payload : null;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* import { ViaSocket } from 'viasocket-apps'
|
|
5
5
|
*
|
|
6
6
|
* const viasocket = new ViaSocket({ orgId, projectId, secret: process.env.VIASOCKET_EMBED_SECRET })
|
|
7
|
-
* const user = viasocket.user(
|
|
7
|
+
* const user = viasocket.user(uniqueIdentifier)
|
|
8
8
|
*
|
|
9
9
|
* const token = await user.token() // for the connect popup
|
|
10
10
|
* const { scriptId } = await user.enable(serviceId, authId) // once, for actions
|
|
11
11
|
* const { options } = await user.listOptions(actionVersionId, {...}) // fill a dropdown
|
|
12
12
|
* const result = await viasocket.runAction(scriptId, actionVersionId, inputData)
|
|
13
|
-
* const sub = await user.subscribe(triggerVersionId, { authId, inputData,
|
|
13
|
+
* const sub = await user.subscribe(triggerVersionId, { authId, inputData, code: handler })
|
|
14
14
|
*
|
|
15
15
|
* The browser half — opening the connect popup — lives at 'viasocket-apps/browser'.
|
|
16
16
|
*/
|
|
@@ -18,4 +18,4 @@ export { ViaSocket, UserScope } from './client.js';
|
|
|
18
18
|
export { ViaSocketError } from './errors.js';
|
|
19
19
|
export { signEmbedToken } from './token.js';
|
|
20
20
|
export type { SignEmbedTokenInput } from './token.js';
|
|
21
|
-
export type { EnableResult, Envelope, Flow, FlowStatus, ListOptionsParams, ListOptionsResult, Option, SubscribeParams, SubscribeResult, UpdateSubscriptionParams, ViaSocketOptions } from './types.js';
|
|
21
|
+
export type { CatalogApp, CatalogVersion, CatalogVersions, EnableResult, Envelope, Flow, FlowStatus, ListAppsParams, ListOptionsParams, ListOptionsResult, Option, SubscribeParams, SubscribeResult, UpdateSubscriptionParams, ViaSocketOptions } from './types.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* import { ViaSocket } from 'viasocket-apps'
|
|
5
5
|
*
|
|
6
6
|
* const viasocket = new ViaSocket({ orgId, projectId, secret: process.env.VIASOCKET_EMBED_SECRET })
|
|
7
|
-
* const user = viasocket.user(
|
|
7
|
+
* const user = viasocket.user(uniqueIdentifier)
|
|
8
8
|
*
|
|
9
9
|
* const token = await user.token() // for the connect popup
|
|
10
10
|
* const { scriptId } = await user.enable(serviceId, authId) // once, for actions
|
|
11
11
|
* const { options } = await user.listOptions(actionVersionId, {...}) // fill a dropdown
|
|
12
12
|
* const result = await viasocket.runAction(scriptId, actionVersionId, inputData)
|
|
13
|
-
* const sub = await user.subscribe(triggerVersionId, { authId, inputData,
|
|
13
|
+
* const sub = await user.subscribe(triggerVersionId, { authId, inputData, code: handler })
|
|
14
14
|
*
|
|
15
15
|
* The browser half — opening the connect popup — lives at 'viasocket-apps/browser'.
|
|
16
16
|
*/
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -50,19 +50,23 @@ export interface EnableResult {
|
|
|
50
50
|
}
|
|
51
51
|
export interface SubscribeParams {
|
|
52
52
|
authId: string;
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The event's own configuration — which channel, which sheet. For polled triggers it may also
|
|
55
|
+
* carry `scheduledTime`, the minutes between checks as a string: "5" or "15".
|
|
56
|
+
*/
|
|
54
57
|
inputData: Record<string, unknown>;
|
|
55
|
-
/** Your endpoint. We POST every event to it. Use this or `code`, not both. */
|
|
56
|
-
webhook?: string;
|
|
57
58
|
/**
|
|
58
|
-
*
|
|
59
|
+
* What to do when the event fires: a script we run on viaSocket's servers per event. The
|
|
60
|
+
* default. It runs an action in another app the user connected, or calls your own API.
|
|
59
61
|
*
|
|
60
62
|
* It is a string of JavaScript executed away from your process, so it must stand alone — no
|
|
61
63
|
* imports and no reference to anything in your codebase. In scope there: `axios`, `fetch` and
|
|
62
|
-
* `context`; the event the app sent is `context.req.body`. Bake in any value of yours (a
|
|
63
|
-
*
|
|
64
|
+
* `context`; the event the app sent is `context.req.body`. Bake in any value of yours (a
|
|
65
|
+
* script_id, a picked id, an API key) when you build the string. Exactly one of `code` or `webhook`.
|
|
64
66
|
*/
|
|
65
67
|
code?: string;
|
|
68
|
+
/** Edge case: a public, unauthenticated URL of yours. We POST every raw event to it. */
|
|
69
|
+
webhook?: string;
|
|
66
70
|
/** Anything of yours; handed back with every delivery. */
|
|
67
71
|
meta?: Record<string, unknown>;
|
|
68
72
|
}
|
|
@@ -94,6 +98,43 @@ export interface Flow {
|
|
|
94
98
|
}
|
|
95
99
|
export type FlowStatus = 0 | 1;
|
|
96
100
|
/** The envelope every viaSocket endpoint answers with. */
|
|
101
|
+
/** One app of the catalog, from a search or the full list. */
|
|
102
|
+
export interface CatalogApp {
|
|
103
|
+
/** The `service_id` every other call takes. */
|
|
104
|
+
serviceId: string;
|
|
105
|
+
name: string;
|
|
106
|
+
description: string;
|
|
107
|
+
iconUrl: string;
|
|
108
|
+
/** Only the full list carries these. */
|
|
109
|
+
category?: string[];
|
|
110
|
+
domain?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface ListAppsParams {
|
|
113
|
+
/** Up to 200, the API's cap. */
|
|
114
|
+
limit?: number;
|
|
115
|
+
offset?: number;
|
|
116
|
+
/** One of the catalog's category names, e.g. "CRM". */
|
|
117
|
+
category?: string;
|
|
118
|
+
}
|
|
119
|
+
/** One published action or trigger version of an app, as the catalog returns it. */
|
|
120
|
+
export interface CatalogVersion {
|
|
121
|
+
/** The `action_version_id` (or trigger version id) the run and subscribe calls take. */
|
|
122
|
+
actionversionrecordid: string;
|
|
123
|
+
type: 'action' | 'trigger' | string;
|
|
124
|
+
name: string;
|
|
125
|
+
description: string;
|
|
126
|
+
/** The field schema a form is rendered from; `inputData` follows it. */
|
|
127
|
+
inputjson: Record<string, unknown>;
|
|
128
|
+
sampledata?: unknown;
|
|
129
|
+
pluginrecordid?: string;
|
|
130
|
+
iconurl?: string;
|
|
131
|
+
authtype?: string;
|
|
132
|
+
[key: string]: unknown;
|
|
133
|
+
}
|
|
134
|
+
export interface CatalogVersions {
|
|
135
|
+
actions: CatalogVersion[];
|
|
136
|
+
triggers: CatalogVersion[];
|
|
137
|
+
}
|
|
97
138
|
export interface Envelope<T = unknown> {
|
|
98
139
|
success: boolean;
|
|
99
140
|
message?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viasocket-apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Server-side client for the viaSocket Apps API: let your users connect Slack, GitHub, Google Sheets or any of 2,300+ apps from your own product, then run actions and receive events through one API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"viasocket",
|