viasocket-apps 0.2.0 → 0.4.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 +30 -7
- package/dist/cjs/browser.js +53 -15
- package/dist/cjs/client.js +18 -6
- package/dist/esm/browser.d.ts +36 -9
- package/dist/esm/browser.js +53 -15
- package/dist/esm/client.d.ts +8 -4
- package/dist/esm/client.js +18 -6
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/types.d.ts +50 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,18 @@ goes in your server's `.env`. The skill is
|
|
|
37
37
|
viaSocket and mirrored here; the same text filled with your ids is served at
|
|
38
38
|
`https://flow.viasocket.com/documentation/skill.md?org=<org_id>&project=<project_id>`.
|
|
39
39
|
|
|
40
|
+
Before you ship — with or without an agent — check the code against the live catalog:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
curl -fsSL https://flow.viasocket.com/viasocket-check.mjs -o viasocket-check.mjs
|
|
44
|
+
node viasocket-check.mjs src
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It finds every viaSocket id and URL in your code and says whether each names a real app, action or
|
|
48
|
+
trigger, is used where its call expects that kind of id (the two ids of an action look alike), and
|
|
49
|
+
whether any viasocket URL was built by hand. No dependencies; exits 1 on a failure, so it can be a
|
|
50
|
+
test. It reads no `.env` file and prints no line of code.
|
|
51
|
+
|
|
40
52
|
## Server
|
|
41
53
|
|
|
42
54
|
```js
|
|
@@ -88,14 +100,23 @@ const subscription = await user.subscribe(triggerVersionId, {
|
|
|
88
100
|
import { connect } from 'viasocket-apps/browser'
|
|
89
101
|
|
|
90
102
|
// embedToken comes from your backend (user.token()), signed for the signed-in user.
|
|
91
|
-
// actions (optional): the
|
|
103
|
+
// actions (optional): the action_ids and trigger_ids you use — the popup then asks only
|
|
92
104
|
// for their scopes; skipActionSelection goes straight to the app's consent screen.
|
|
93
|
-
const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc', actions: ['<
|
|
105
|
+
const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc', actions: ['<action_id>'], skipActionSelection: true })
|
|
94
106
|
// Send authId to your backend, which enables the app and stores the script_id.
|
|
95
107
|
```
|
|
96
108
|
|
|
97
|
-
`
|
|
98
|
-
`
|
|
109
|
+
`serviceId` is the app's `service_id` exactly as the catalog returns it — a bare id such as
|
|
110
|
+
`rowbu58rc`, never the app's name or slug, never the id with a name joined on. `connect()` loads
|
|
111
|
+
viaSocket's connect script from `https://flow.viasocket.com/connect.js` and lets it open the popup;
|
|
112
|
+
that script is the only supported way to open it, so never build a viasocket URL for it by hand.
|
|
113
|
+
|
|
114
|
+
`connect()` rejects with a `ViaSocketConnectError` whose `code` is `'closed'` (the user seems to
|
|
115
|
+
have given up), `'rejected'` (the app or viaSocket said no), `'invalid'` (`serviceId` is not a
|
|
116
|
+
service id) or `'script'` (the connect script did not load). `'closed'` is best-effort: once the
|
|
117
|
+
popup is on the app's own sign-in page a close can only be inferred, and arrives about a minute
|
|
118
|
+
later; if the user then finishes anyway, `onLateSuccess` is called with the same result a resolved
|
|
119
|
+
`connect()` gives. Treat `'closed'` as "nothing yet", not as a verdict.
|
|
99
120
|
|
|
100
121
|
## Methods
|
|
101
122
|
|
|
@@ -108,8 +129,8 @@ const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc', actions:
|
|
|
108
129
|
| `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. |
|
|
109
130
|
| `viasocket.runAction(scriptId, actionVersionId, inputData)` | Run one action. Returns the app's own response. |
|
|
110
131
|
| `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)` |
|
|
132
|
+
| `viasocket.catalog.list({ limit, offset, category })` | Every app, most used first, 200 a page; an empty page ends it. Drop apps whose `versions()` is `null` or empty. No token. |
|
|
133
|
+
| `viasocket.catalog.versions(serviceId)` | One app and every published action and trigger of it, as `{ service, actions, triggers }`. Each action has `action_id` and `action_version_id`, each trigger `trigger_id` and `trigger_version_id`, named as the calls take them: the version id for `runAction`, `listOptions` and `subscribe`; the other for `connect`, automations and the prebuilt UI. `input_schema` is the form schema. `null` for an unknown id. No token. |
|
|
113
134
|
| `user.subscribe(triggerVersionId, { authId, inputData, code \| webhook, meta })` | Subscribe to an app event with a handler we run per event. Returns `{ scriptId, hookUrl, … }`. |
|
|
114
135
|
| `user.updateSubscription(scriptId, { code, meta })` | Change a live subscription in place. |
|
|
115
136
|
|
|
@@ -169,7 +190,9 @@ carrying the API's own `message`, the HTTP `status`, and the parsed `body`.
|
|
|
169
190
|
|
|
170
191
|
Defaults target production: `https://flow-api.viasocket.com` for the API and
|
|
171
192
|
`https://flow.sokt.io` for running actions. Pass `apiBaseUrl` / `runBaseUrl` to point elsewhere,
|
|
172
|
-
and `scriptUrl` to `connect()` for a non-production connect script
|
|
193
|
+
and `scriptUrl` to `connect()` for a non-production connect script — the script picks its own API
|
|
194
|
+
and auth hosts from the origin it is served from, so that is that stack's copy
|
|
195
|
+
(`https://dev-flow.viasocket.com/connect.js`).
|
|
173
196
|
|
|
174
197
|
## License
|
|
175
198
|
|
package/dist/cjs/browser.js
CHANGED
|
@@ -8,13 +8,23 @@
|
|
|
8
8
|
* It wraps viaSocket's standalone connect script — the popup, the postMessage handshake, the
|
|
9
9
|
* three message types — behind one promise. Nothing here touches the signing secret: the
|
|
10
10
|
* embedToken comes from your backend, already signed for this end user.
|
|
11
|
+
*
|
|
12
|
+
* The script is served by viaSocket at a fixed URL and is the only supported way to open the
|
|
13
|
+
* popup: it registers the token, hands the session over, opens the right page for its
|
|
14
|
+
* environment, and listens for the result on both paths it can arrive by (the popup's own
|
|
15
|
+
* postMessage, and a relay for sign-in pages that sever the popup's link to this page). A
|
|
16
|
+
* viasocket URL built by hand opens a page that does not exist.
|
|
11
17
|
*/
|
|
12
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
19
|
exports.ViaSocketConnectError = void 0;
|
|
14
20
|
exports.loadConnectScript = loadConnectScript;
|
|
15
21
|
exports.connect = connect;
|
|
16
|
-
const DEFAULT_SCRIPT_URL = 'https://
|
|
22
|
+
const DEFAULT_SCRIPT_URL = 'https://flow.viasocket.com/connect.js';
|
|
17
23
|
const SCRIPT_ID = 'viasocket-connect-script';
|
|
24
|
+
/** A service id is one bare token of letters and digits: `rowo0bqrhj5g`, never `rowo0bqrhj5g_gmail`. */
|
|
25
|
+
const SERVICE_ID = /^[A-Za-z0-9]+$/;
|
|
26
|
+
/** How long a late success is still delivered after `closed` — the script's own bound. */
|
|
27
|
+
const LATE_SUCCESS_WINDOW_MS = 15 * 60 * 1000;
|
|
18
28
|
class ViaSocketConnectError extends Error {
|
|
19
29
|
code;
|
|
20
30
|
serviceId;
|
|
@@ -58,11 +68,13 @@ function loadConnectScript(scriptUrl = DEFAULT_SCRIPT_URL) {
|
|
|
58
68
|
});
|
|
59
69
|
return scriptPromise;
|
|
60
70
|
}
|
|
71
|
+
const RESULT_TYPES = ['viasocket_connection_success', 'viasocket_connection_error', 'viasocket_connection_closed'];
|
|
61
72
|
/**
|
|
62
73
|
* Opens the consent popup for one app and resolves with the connection it creates.
|
|
63
74
|
*
|
|
64
|
-
* Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user
|
|
65
|
-
*
|
|
75
|
+
* Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user seems to have
|
|
76
|
+
* given up (best-effort — see `onLateSuccess`), 'rejected' if the app or viaSocket said no,
|
|
77
|
+
* 'invalid' if the serviceId is not a service id, 'script' if the connect script could not load.
|
|
66
78
|
*/
|
|
67
79
|
async function connect(options) {
|
|
68
80
|
const serviceId = options?.serviceId;
|
|
@@ -70,36 +82,62 @@ async function connect(options) {
|
|
|
70
82
|
throw new ViaSocketConnectError('unavailable', serviceId ?? '', 'connect: embedToken is required');
|
|
71
83
|
if (!serviceId)
|
|
72
84
|
throw new ViaSocketConnectError('unavailable', '', 'connect: serviceId is required');
|
|
85
|
+
if (typeof serviceId !== 'string' || !SERVICE_ID.test(serviceId)) {
|
|
86
|
+
throw new ViaSocketConnectError('invalid', String(serviceId), `connect: "${serviceId}" is not a service id. Pass the service_id exactly as the catalog returns it (a bare id such as rowo0bqrhj5g), not the app's name or slug, and not the id with a name joined on`);
|
|
87
|
+
}
|
|
73
88
|
await loadConnectScript(options.scriptUrl);
|
|
74
89
|
const open = window.openViasocketConnection;
|
|
75
90
|
if (typeof open !== 'function') {
|
|
76
91
|
throw new ViaSocketConnectError('script', serviceId, 'The connect script loaded but did not expose openViasocketConnection()');
|
|
77
92
|
}
|
|
78
93
|
return new Promise((resolve, reject) => {
|
|
79
|
-
|
|
94
|
+
let closedReported = false;
|
|
95
|
+
let lateTimer = null;
|
|
96
|
+
const cleanup = () => {
|
|
97
|
+
window.removeEventListener('message', onMessage);
|
|
98
|
+
if (lateTimer)
|
|
99
|
+
clearTimeout(lateTimer);
|
|
100
|
+
};
|
|
101
|
+
const toResult = (payload) => {
|
|
102
|
+
const connection = (payload.data ?? {});
|
|
103
|
+
const authId = typeof connection.id === 'string' ? connection.id : '';
|
|
104
|
+
return authId ? { authId, serviceId, connection } : null;
|
|
105
|
+
};
|
|
106
|
+
function onMessage(event) {
|
|
80
107
|
const payload = event?.data;
|
|
81
|
-
if (!payload || typeof payload.type !== 'string' || !payload.type
|
|
108
|
+
if (!payload || typeof payload.type !== 'string' || !RESULT_TYPES.includes(payload.type))
|
|
82
109
|
return;
|
|
83
110
|
// Two apps can be connecting in one page; only this app's messages are ours.
|
|
84
111
|
if (payload.serviceId && payload.serviceId !== serviceId)
|
|
85
112
|
return;
|
|
86
|
-
|
|
113
|
+
if (payload.type === 'viasocket_connection_closed') {
|
|
114
|
+
if (closedReported)
|
|
115
|
+
return;
|
|
116
|
+
closedReported = true;
|
|
117
|
+
// Keep listening: a close on the app's sign-in page is inferred, and the script still
|
|
118
|
+
// delivers a success that arrives after it.
|
|
119
|
+
lateTimer = setTimeout(cleanup, LATE_SUCCESS_WINDOW_MS);
|
|
120
|
+
reject(new ViaSocketConnectError('closed', serviceId, 'The popup seems to have been closed before the connection finished'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
cleanup();
|
|
87
124
|
if (payload.type === 'viasocket_connection_success') {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
125
|
+
const result = toResult(payload);
|
|
126
|
+
if (closedReported) {
|
|
127
|
+
if (result)
|
|
128
|
+
options.onLateSuccess?.(result);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!result) {
|
|
91
132
|
reject(new ViaSocketConnectError('rejected', serviceId, 'The connection succeeded but carried no id'));
|
|
92
133
|
return;
|
|
93
134
|
}
|
|
94
|
-
resolve(
|
|
135
|
+
resolve(result);
|
|
95
136
|
}
|
|
96
|
-
else if (
|
|
137
|
+
else if (!closedReported) {
|
|
97
138
|
reject(new ViaSocketConnectError('rejected', serviceId, payload.error?.message || 'The app rejected the connection'));
|
|
98
139
|
}
|
|
99
|
-
|
|
100
|
-
reject(new ViaSocketConnectError('closed', serviceId, 'The popup was closed before the connection finished'));
|
|
101
|
-
}
|
|
102
|
-
};
|
|
140
|
+
}
|
|
103
141
|
window.addEventListener('message', onMessage);
|
|
104
142
|
const scriptOptions = {};
|
|
105
143
|
if (options.actions?.length)
|
package/dist/cjs/client.js
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_RUN_BASE_URL = 'https://flow.sokt.io';
|
|
|
19
19
|
const CATALOG = {
|
|
20
20
|
search: 'https://flow.sokt.io/func/scri12BSufQM',
|
|
21
21
|
list: 'https://plug-service.viasocket.com/api/v1/plugins/all',
|
|
22
|
-
versions: 'https://flow.sokt.io/func/
|
|
22
|
+
versions: 'https://flow.sokt.io/func/scriK4LFg2kc'
|
|
23
23
|
};
|
|
24
24
|
const LIST_LIMIT_MAX = 200;
|
|
25
25
|
function required(name, value) {
|
|
@@ -95,9 +95,13 @@ class ViaSocket {
|
|
|
95
95
|
}));
|
|
96
96
|
},
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* One app and every published action and trigger of it, as `{ service, actions, triggers }`.
|
|
99
|
+
* Each action carries `action_id` and `action_version_id`, each trigger `trigger_id` and
|
|
100
|
+
* `trigger_version_id` — named as the calls take them: the version id for `runAction`,
|
|
101
|
+
* `listOptions` and `subscribe`; the other for `connect`, automations and the prebuilt UI.
|
|
102
|
+
* `input_schema` is the field schema a form is rendered from. `null` for an unknown service id
|
|
103
|
+
* (the API answers 200 with an empty service); empty arrays for an app with nothing published.
|
|
104
|
+
* Cache it per app; ids are stable and schemas change rarely.
|
|
101
105
|
*/
|
|
102
106
|
versions: async (serviceId) => {
|
|
103
107
|
const { data } = await (0, http_js_1.request)({
|
|
@@ -106,8 +110,16 @@ class ViaSocket {
|
|
|
106
110
|
body: JSON.stringify({ service_id: required('serviceId', serviceId) }),
|
|
107
111
|
fetchImpl: this.transport
|
|
108
112
|
});
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
// No `success` key, so the body arrives whole as `data`; tolerate an envelope anyway.
|
|
114
|
+
const body = (data && 'service' in data ? data : data?.data) ?? {};
|
|
115
|
+
const service = body.service;
|
|
116
|
+
if (!service?.name)
|
|
117
|
+
return null;
|
|
118
|
+
return {
|
|
119
|
+
service,
|
|
120
|
+
actions: Array.isArray(body.actions) ? body.actions : [],
|
|
121
|
+
triggers: Array.isArray(body.triggers) ? body.triggers : []
|
|
122
|
+
};
|
|
111
123
|
}
|
|
112
124
|
};
|
|
113
125
|
/** Everything that is done for one of your end users. */
|
package/dist/esm/browser.d.ts
CHANGED
|
@@ -7,14 +7,26 @@
|
|
|
7
7
|
* It wraps viaSocket's standalone connect script — the popup, the postMessage handshake, the
|
|
8
8
|
* three message types — behind one promise. Nothing here touches the signing secret: the
|
|
9
9
|
* embedToken comes from your backend, already signed for this end user.
|
|
10
|
+
*
|
|
11
|
+
* The script is served by viaSocket at a fixed URL and is the only supported way to open the
|
|
12
|
+
* popup: it registers the token, hands the session over, opens the right page for its
|
|
13
|
+
* environment, and listens for the result on both paths it can arrive by (the popup's own
|
|
14
|
+
* postMessage, and a relay for sign-in pages that sever the popup's link to this page). A
|
|
15
|
+
* viasocket URL built by hand opens a page that does not exist.
|
|
10
16
|
*/
|
|
11
17
|
export type ConnectErrorCode =
|
|
12
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The popup seems to have been closed before finishing. Best-effort: once the popup is on the
|
|
20
|
+
* app's own sign-in page a close can only be inferred, and arrives about a minute later. If the
|
|
21
|
+
* user in fact finishes after that, `onLateSuccess` is called with the connection.
|
|
22
|
+
*/
|
|
13
23
|
'closed'
|
|
14
|
-
/** The app refused the authorisation. */
|
|
24
|
+
/** The app refused the authorisation, or viaSocket refused the request (an id that names no app). */
|
|
15
25
|
| 'rejected'
|
|
16
26
|
/** The connect script could not be loaded. */
|
|
17
27
|
| 'script'
|
|
28
|
+
/** The `serviceId` is not a bare service id — the app's name, its slug, or an id with a name joined on. */
|
|
29
|
+
| 'invalid'
|
|
18
30
|
/** Not running in a browser, or the popup could not be opened. */
|
|
19
31
|
| 'unavailable';
|
|
20
32
|
export declare class ViaSocketConnectError extends Error {
|
|
@@ -25,16 +37,30 @@ export declare class ViaSocketConnectError extends Error {
|
|
|
25
37
|
export interface ConnectOptions {
|
|
26
38
|
/** Signed on your backend for the current end user — see `viasocket.user(id).token()`. */
|
|
27
39
|
embedToken: string;
|
|
28
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The app to connect: its `service_id` exactly as the catalog returns it (`rowo0bqrhj5g`).
|
|
42
|
+
* Never the app's name or slug, never the id with a name joined on.
|
|
43
|
+
*/
|
|
29
44
|
serviceId: string;
|
|
30
45
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
46
|
+
* The `action_id`s and `trigger_id`s this product uses (listed under each one in the app's
|
|
47
|
+
* document and in `catalog.versions()`). The popup then asks only for their scopes. Omit to
|
|
48
|
+
* offer every action. Never a version id.
|
|
33
49
|
*/
|
|
34
50
|
actions?: string[];
|
|
35
51
|
/** With `actions`: skip the action list and open the app's consent screen directly. */
|
|
36
52
|
skipActionSelection?: boolean;
|
|
37
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Called if the connection completes after `connect()` already rejected with `'closed'` — the
|
|
55
|
+
* close was inferred while the user was still on the app's sign-in page. Treat it exactly like
|
|
56
|
+
* a resolved `connect()`.
|
|
57
|
+
*/
|
|
58
|
+
onLateSuccess?: (result: ConnectResult) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Where the connect script is served from. The script picks its API and auth hosts from its
|
|
61
|
+
* own origin, so for a non-production stack point this at that stack's copy
|
|
62
|
+
* (`https://dev-flow.viasocket.com/connect.js`).
|
|
63
|
+
*/
|
|
38
64
|
scriptUrl?: string;
|
|
39
65
|
}
|
|
40
66
|
/** The third argument of the connect script's `openViasocketConnection`. */
|
|
@@ -51,7 +77,7 @@ export interface ConnectResult {
|
|
|
51
77
|
}
|
|
52
78
|
declare global {
|
|
53
79
|
interface Window {
|
|
54
|
-
openViasocketConnection?: (embedToken: string, serviceId: string, options?: ConnectScriptOptions) => void;
|
|
80
|
+
openViasocketConnection?: (embedToken: string, serviceId: string, options?: ConnectScriptOptions) => Promise<unknown> | void;
|
|
55
81
|
}
|
|
56
82
|
}
|
|
57
83
|
/**
|
|
@@ -62,8 +88,9 @@ export declare function loadConnectScript(scriptUrl?: string): Promise<void>;
|
|
|
62
88
|
/**
|
|
63
89
|
* Opens the consent popup for one app and resolves with the connection it creates.
|
|
64
90
|
*
|
|
65
|
-
* Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user
|
|
66
|
-
*
|
|
91
|
+
* Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user seems to have
|
|
92
|
+
* given up (best-effort — see `onLateSuccess`), 'rejected' if the app or viaSocket said no,
|
|
93
|
+
* 'invalid' if the serviceId is not a service id, 'script' if the connect script could not load.
|
|
67
94
|
*/
|
|
68
95
|
export declare function connect(options: ConnectOptions): Promise<ConnectResult>;
|
|
69
96
|
export {};
|
package/dist/esm/browser.js
CHANGED
|
@@ -7,9 +7,19 @@
|
|
|
7
7
|
* It wraps viaSocket's standalone connect script — the popup, the postMessage handshake, the
|
|
8
8
|
* three message types — behind one promise. Nothing here touches the signing secret: the
|
|
9
9
|
* embedToken comes from your backend, already signed for this end user.
|
|
10
|
+
*
|
|
11
|
+
* The script is served by viaSocket at a fixed URL and is the only supported way to open the
|
|
12
|
+
* popup: it registers the token, hands the session over, opens the right page for its
|
|
13
|
+
* environment, and listens for the result on both paths it can arrive by (the popup's own
|
|
14
|
+
* postMessage, and a relay for sign-in pages that sever the popup's link to this page). A
|
|
15
|
+
* viasocket URL built by hand opens a page that does not exist.
|
|
10
16
|
*/
|
|
11
|
-
const DEFAULT_SCRIPT_URL = 'https://
|
|
17
|
+
const DEFAULT_SCRIPT_URL = 'https://flow.viasocket.com/connect.js';
|
|
12
18
|
const SCRIPT_ID = 'viasocket-connect-script';
|
|
19
|
+
/** A service id is one bare token of letters and digits: `rowo0bqrhj5g`, never `rowo0bqrhj5g_gmail`. */
|
|
20
|
+
const SERVICE_ID = /^[A-Za-z0-9]+$/;
|
|
21
|
+
/** How long a late success is still delivered after `closed` — the script's own bound. */
|
|
22
|
+
const LATE_SUCCESS_WINDOW_MS = 15 * 60 * 1000;
|
|
13
23
|
export class ViaSocketConnectError extends Error {
|
|
14
24
|
code;
|
|
15
25
|
serviceId;
|
|
@@ -52,11 +62,13 @@ export function loadConnectScript(scriptUrl = DEFAULT_SCRIPT_URL) {
|
|
|
52
62
|
});
|
|
53
63
|
return scriptPromise;
|
|
54
64
|
}
|
|
65
|
+
const RESULT_TYPES = ['viasocket_connection_success', 'viasocket_connection_error', 'viasocket_connection_closed'];
|
|
55
66
|
/**
|
|
56
67
|
* Opens the consent popup for one app and resolves with the connection it creates.
|
|
57
68
|
*
|
|
58
|
-
* Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user
|
|
59
|
-
*
|
|
69
|
+
* Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user seems to have
|
|
70
|
+
* given up (best-effort — see `onLateSuccess`), 'rejected' if the app or viaSocket said no,
|
|
71
|
+
* 'invalid' if the serviceId is not a service id, 'script' if the connect script could not load.
|
|
60
72
|
*/
|
|
61
73
|
export async function connect(options) {
|
|
62
74
|
const serviceId = options?.serviceId;
|
|
@@ -64,36 +76,62 @@ export async function connect(options) {
|
|
|
64
76
|
throw new ViaSocketConnectError('unavailable', serviceId ?? '', 'connect: embedToken is required');
|
|
65
77
|
if (!serviceId)
|
|
66
78
|
throw new ViaSocketConnectError('unavailable', '', 'connect: serviceId is required');
|
|
79
|
+
if (typeof serviceId !== 'string' || !SERVICE_ID.test(serviceId)) {
|
|
80
|
+
throw new ViaSocketConnectError('invalid', String(serviceId), `connect: "${serviceId}" is not a service id. Pass the service_id exactly as the catalog returns it (a bare id such as rowo0bqrhj5g), not the app's name or slug, and not the id with a name joined on`);
|
|
81
|
+
}
|
|
67
82
|
await loadConnectScript(options.scriptUrl);
|
|
68
83
|
const open = window.openViasocketConnection;
|
|
69
84
|
if (typeof open !== 'function') {
|
|
70
85
|
throw new ViaSocketConnectError('script', serviceId, 'The connect script loaded but did not expose openViasocketConnection()');
|
|
71
86
|
}
|
|
72
87
|
return new Promise((resolve, reject) => {
|
|
73
|
-
|
|
88
|
+
let closedReported = false;
|
|
89
|
+
let lateTimer = null;
|
|
90
|
+
const cleanup = () => {
|
|
91
|
+
window.removeEventListener('message', onMessage);
|
|
92
|
+
if (lateTimer)
|
|
93
|
+
clearTimeout(lateTimer);
|
|
94
|
+
};
|
|
95
|
+
const toResult = (payload) => {
|
|
96
|
+
const connection = (payload.data ?? {});
|
|
97
|
+
const authId = typeof connection.id === 'string' ? connection.id : '';
|
|
98
|
+
return authId ? { authId, serviceId, connection } : null;
|
|
99
|
+
};
|
|
100
|
+
function onMessage(event) {
|
|
74
101
|
const payload = event?.data;
|
|
75
|
-
if (!payload || typeof payload.type !== 'string' || !payload.type
|
|
102
|
+
if (!payload || typeof payload.type !== 'string' || !RESULT_TYPES.includes(payload.type))
|
|
76
103
|
return;
|
|
77
104
|
// Two apps can be connecting in one page; only this app's messages are ours.
|
|
78
105
|
if (payload.serviceId && payload.serviceId !== serviceId)
|
|
79
106
|
return;
|
|
80
|
-
|
|
107
|
+
if (payload.type === 'viasocket_connection_closed') {
|
|
108
|
+
if (closedReported)
|
|
109
|
+
return;
|
|
110
|
+
closedReported = true;
|
|
111
|
+
// Keep listening: a close on the app's sign-in page is inferred, and the script still
|
|
112
|
+
// delivers a success that arrives after it.
|
|
113
|
+
lateTimer = setTimeout(cleanup, LATE_SUCCESS_WINDOW_MS);
|
|
114
|
+
reject(new ViaSocketConnectError('closed', serviceId, 'The popup seems to have been closed before the connection finished'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
cleanup();
|
|
81
118
|
if (payload.type === 'viasocket_connection_success') {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
119
|
+
const result = toResult(payload);
|
|
120
|
+
if (closedReported) {
|
|
121
|
+
if (result)
|
|
122
|
+
options.onLateSuccess?.(result);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!result) {
|
|
85
126
|
reject(new ViaSocketConnectError('rejected', serviceId, 'The connection succeeded but carried no id'));
|
|
86
127
|
return;
|
|
87
128
|
}
|
|
88
|
-
resolve(
|
|
129
|
+
resolve(result);
|
|
89
130
|
}
|
|
90
|
-
else if (
|
|
131
|
+
else if (!closedReported) {
|
|
91
132
|
reject(new ViaSocketConnectError('rejected', serviceId, payload.error?.message || 'The app rejected the connection'));
|
|
92
133
|
}
|
|
93
|
-
|
|
94
|
-
reject(new ViaSocketConnectError('closed', serviceId, 'The popup was closed before the connection finished'));
|
|
95
|
-
}
|
|
96
|
-
};
|
|
134
|
+
}
|
|
97
135
|
window.addEventListener('message', onMessage);
|
|
98
136
|
const scriptOptions = {};
|
|
99
137
|
if (options.actions?.length)
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -29,11 +29,15 @@ export declare class ViaSocket {
|
|
|
29
29
|
*/
|
|
30
30
|
list: (params?: ListAppsParams) => Promise<CatalogApp[]>;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* One app and every published action and trigger of it, as `{ service, actions, triggers }`.
|
|
33
|
+
* Each action carries `action_id` and `action_version_id`, each trigger `trigger_id` and
|
|
34
|
+
* `trigger_version_id` — named as the calls take them: the version id for `runAction`,
|
|
35
|
+
* `listOptions` and `subscribe`; the other for `connect`, automations and the prebuilt UI.
|
|
36
|
+
* `input_schema` is the field schema a form is rendered from. `null` for an unknown service id
|
|
37
|
+
* (the API answers 200 with an empty service); empty arrays for an app with nothing published.
|
|
38
|
+
* Cache it per app; ids are stable and schemas change rarely.
|
|
35
39
|
*/
|
|
36
|
-
versions: (serviceId: string) => Promise<CatalogVersions>;
|
|
40
|
+
versions: (serviceId: string) => Promise<CatalogVersions | null>;
|
|
37
41
|
};
|
|
38
42
|
/** Everything that is done for one of your end users. */
|
|
39
43
|
user(uniqueIdentifier: string): UserScope;
|
package/dist/esm/client.js
CHANGED
|
@@ -16,7 +16,7 @@ const DEFAULT_RUN_BASE_URL = 'https://flow.sokt.io';
|
|
|
16
16
|
const CATALOG = {
|
|
17
17
|
search: 'https://flow.sokt.io/func/scri12BSufQM',
|
|
18
18
|
list: 'https://plug-service.viasocket.com/api/v1/plugins/all',
|
|
19
|
-
versions: 'https://flow.sokt.io/func/
|
|
19
|
+
versions: 'https://flow.sokt.io/func/scriK4LFg2kc'
|
|
20
20
|
};
|
|
21
21
|
const LIST_LIMIT_MAX = 200;
|
|
22
22
|
function required(name, value) {
|
|
@@ -92,9 +92,13 @@ export class ViaSocket {
|
|
|
92
92
|
}));
|
|
93
93
|
},
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* One app and every published action and trigger of it, as `{ service, actions, triggers }`.
|
|
96
|
+
* Each action carries `action_id` and `action_version_id`, each trigger `trigger_id` and
|
|
97
|
+
* `trigger_version_id` — named as the calls take them: the version id for `runAction`,
|
|
98
|
+
* `listOptions` and `subscribe`; the other for `connect`, automations and the prebuilt UI.
|
|
99
|
+
* `input_schema` is the field schema a form is rendered from. `null` for an unknown service id
|
|
100
|
+
* (the API answers 200 with an empty service); empty arrays for an app with nothing published.
|
|
101
|
+
* Cache it per app; ids are stable and schemas change rarely.
|
|
98
102
|
*/
|
|
99
103
|
versions: async (serviceId) => {
|
|
100
104
|
const { data } = await request({
|
|
@@ -103,8 +107,16 @@ export class ViaSocket {
|
|
|
103
107
|
body: JSON.stringify({ service_id: required('serviceId', serviceId) }),
|
|
104
108
|
fetchImpl: this.transport
|
|
105
109
|
});
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
// No `success` key, so the body arrives whole as `data`; tolerate an envelope anyway.
|
|
111
|
+
const body = (data && 'service' in data ? data : data?.data) ?? {};
|
|
112
|
+
const service = body.service;
|
|
113
|
+
if (!service?.name)
|
|
114
|
+
return null;
|
|
115
|
+
return {
|
|
116
|
+
service,
|
|
117
|
+
actions: Array.isArray(body.actions) ? body.actions : [],
|
|
118
|
+
triggers: Array.isArray(body.triggers) ? body.triggers : []
|
|
119
|
+
};
|
|
108
120
|
}
|
|
109
121
|
};
|
|
110
122
|
/** Everything that is done for one of your end users. */
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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 { CatalogApp, CatalogVersion, CatalogVersions, EnableResult, Envelope, Flow, FlowStatus, ListAppsParams, ListOptionsParams, ListOptionsResult, Option, SubscribeParams, SubscribeResult, UpdateSubscriptionParams, ViaSocketOptions } from './types.js';
|
|
21
|
+
export type { CatalogApp, CatalogAction, CatalogService, CatalogTrigger, CatalogVersion, CatalogVersions, EnableResult, Envelope, Flow, FlowStatus, ListAppsParams, ListOptionsParams, ListOptionsResult, Option, SubscribeParams, SubscribeResult, UpdateSubscriptionParams, ViaSocketOptions } from './types.js';
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -116,24 +116,60 @@ export interface ListAppsParams {
|
|
|
116
116
|
/** One of the catalog's category names, e.g. "CRM". */
|
|
117
117
|
category?: string;
|
|
118
118
|
}
|
|
119
|
-
/**
|
|
120
|
-
export interface
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
/** The app, once, at the top of `catalog.versions()`. */
|
|
120
|
+
export interface CatalogService {
|
|
121
|
+
service_id: string;
|
|
122
|
+
/** The display name. The API sends `null` for an unknown id; `versions()` then returns `null` instead of a catalog. */
|
|
123
|
+
name: string;
|
|
124
|
+
description: string | null;
|
|
125
|
+
icon_url: string | null;
|
|
126
|
+
/** `Auth2.0`, `apikey`, `NoAuth`, … Informational: connect handles it. */
|
|
127
|
+
auth_type: string | null;
|
|
128
|
+
/** `false` for an app without sign-in: no connect button, and `'NoAuth'` wherever a call takes an `authId`. */
|
|
129
|
+
requires_auth: boolean;
|
|
130
|
+
}
|
|
131
|
+
interface CatalogEventBase {
|
|
124
132
|
name: string;
|
|
125
133
|
description: string;
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The field schema a form is rendered from; `inputData` follows it. A block with a `source` or
|
|
136
|
+
* `optionsGenerator` key is a picker: fill it with `listOptions`, never by running what the key holds.
|
|
137
|
+
*/
|
|
138
|
+
input_schema: Record<string, unknown>;
|
|
139
|
+
/** A real response of the action, or a real event payload of the trigger. */
|
|
140
|
+
sample_output?: unknown;
|
|
141
|
+
updated_at?: string;
|
|
132
142
|
[key: string]: unknown;
|
|
133
143
|
}
|
|
144
|
+
/** One published action, its two ids named as the calls take them. */
|
|
145
|
+
export interface CatalogAction extends CatalogEventBase {
|
|
146
|
+
/**
|
|
147
|
+
* The action’s own id: what `connect`’s `actions`, an automation step’s `action_id` and the
|
|
148
|
+
* prebuilt UI’s `open: { actionId }` take. Looks like the version id and is not interchangeable with it.
|
|
149
|
+
*/
|
|
150
|
+
action_id: string;
|
|
151
|
+
/** What `runAction` and `listOptions` take — and only those. */
|
|
152
|
+
action_version_id: string;
|
|
153
|
+
}
|
|
154
|
+
/** One published trigger, its two ids named as the calls take them. */
|
|
155
|
+
export interface CatalogTrigger extends CatalogEventBase {
|
|
156
|
+
/**
|
|
157
|
+
* The trigger’s own id: what `connect`’s `actions`, an automation’s `trigger_id` and the prebuilt
|
|
158
|
+
* UI’s `open: { triggerId }` take. Looks like the version id and is not interchangeable with it.
|
|
159
|
+
*/
|
|
160
|
+
trigger_id: string;
|
|
161
|
+
/** What `subscribe` and `listOptions` take — and only those. */
|
|
162
|
+
trigger_version_id: string;
|
|
163
|
+
/** Not reliable yet: the API answers `true` for every trigger. */
|
|
164
|
+
polled?: boolean | null;
|
|
165
|
+
}
|
|
166
|
+
/** @deprecated The catalog no longer returns rows of one shape; use `CatalogAction` and `CatalogTrigger`. */
|
|
167
|
+
export type CatalogVersion = CatalogAction | CatalogTrigger;
|
|
168
|
+
/** What `catalog.versions()` returns for a known app. */
|
|
134
169
|
export interface CatalogVersions {
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
service: CatalogService;
|
|
171
|
+
actions: CatalogAction[];
|
|
172
|
+
triggers: CatalogTrigger[];
|
|
137
173
|
}
|
|
138
174
|
export interface Envelope<T = unknown> {
|
|
139
175
|
success: boolean;
|
|
@@ -141,3 +177,4 @@ export interface Envelope<T = unknown> {
|
|
|
141
177
|
data: T;
|
|
142
178
|
isCached?: boolean;
|
|
143
179
|
}
|
|
180
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viasocket-apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|