viasocket-apps 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Walkover Web Solution
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # viasocket-apps
2
+
3
+ Server-side client for the viaSocket Apps API. Let your users connect Slack, GitHub, Google
4
+ Sheets or any of 2,300+ apps from your own product, then run actions and receive events through
5
+ one API — with the OAuth, token refresh, per-app request shapes, polling and retries handled for
6
+ you.
7
+
8
+ Zero dependencies. Node 20+, Bun, Deno and edge runtimes.
9
+
10
+ ```sh
11
+ npm install viasocket-apps
12
+ ```
13
+
14
+ The same calls are documented as plain HTTP in your viaSocket dashboard under
15
+ **Integrations → your Apps API integration → API reference**. Use whichever suits your stack;
16
+ this package is a thin, typed layer over those endpoints and adds nothing they don't have.
17
+
18
+ ## Server
19
+
20
+ ```js
21
+ import { ViaSocket } from 'viasocket-apps'
22
+
23
+ const viasocket = new ViaSocket({
24
+ orgId: process.env.VIASOCKET_ORG_ID,
25
+ projectId: process.env.VIASOCKET_PROJECT_ID,
26
+ secret: process.env.VIASOCKET_EMBED_SECRET // server-side only
27
+ })
28
+
29
+ // Everything is done for one of your end users, identified by an id you choose.
30
+ const user = viasocket.user('user_123')
31
+
32
+ // 1. Your frontend needs a token to open the connect popup.
33
+ const embedToken = await user.token()
34
+
35
+ // 2. After the popup posts back an auth_id: enable the app, once, to run actions.
36
+ const scriptId = (await user.findEnabled(serviceId)) ?? (await user.enable(serviceId, authId)).scriptId
37
+
38
+ // 3. Fill a dropdown with the user's real data.
39
+ const { options } = await user.listOptions(actionVersionId, {
40
+ fieldKey: 'spreadsheet_Id',
41
+ authId,
42
+ existingFields: {}
43
+ })
44
+
45
+ // 4. Run an action. No token — the script_id is the credential.
46
+ const result = await viasocket.runAction(scriptId, actionVersionId, {
47
+ spreadsheet_Id: options[0].value,
48
+ grid_Id: '0',
49
+ column_key: true
50
+ })
51
+
52
+ // 5. Or subscribe to an event. Needs only the connection, not enable().
53
+ const subscription = await user.subscribe(triggerVersionId, {
54
+ authId,
55
+ inputData: { channel_id: ['C08SXCV3J85'] },
56
+ webhook: 'https://your-app.com/webhooks/viasocket',
57
+ meta: { userId: 'user_123' }
58
+ })
59
+ ```
60
+
61
+ ## Browser
62
+
63
+ ```js
64
+ import { connect } from 'viasocket-apps/browser'
65
+
66
+ // embedToken comes from your backend (user.token()), signed for the signed-in user.
67
+ const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc' })
68
+ // Send authId to your backend, which enables the app and stores the script_id.
69
+ ```
70
+
71
+ `connect()` rejects with a `ViaSocketConnectError` whose `code` is `'closed'` (the user gave up),
72
+ `'rejected'` (the app said no) or `'script'` (the connect script did not load).
73
+
74
+ ## Methods
75
+
76
+ | Call | What it does |
77
+ | --- | --- |
78
+ | `viasocket.user(id)` | Scope every call below to one end user. |
79
+ | `user.token()` | Signed embed token for that user; hand it to the frontend. |
80
+ | `user.enable(serviceId, authId)` | Turn a connection into a `scriptId` for running actions. Once per connection. |
81
+ | `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. |
83
+ | `viasocket.runAction(scriptId, actionVersionId, inputData)` | Run one action. Returns the app's own response. |
84
+ | `user.subscribe(triggerVersionId, { authId, inputData, webhook \| code, meta })` | Subscribe to an app event. Returns `{ scriptId, hookUrl, … }`. |
85
+ | `user.updateSubscription(scriptId, { code, meta })` | Change a live subscription in place. |
86
+ | `user.listFlows()` | Every enabled app and subscription this user has. |
87
+ | `user.disableFlow(scriptId)` / `user.enableFlow(scriptId)` | Turn a flow off or back on. Disabling a subscription ends it. |
88
+ | `user.listConnections()` | Every app this user has connected. |
89
+ | `user.revokeConnection(authId)` | Disconnect an app. Disable its flows first. |
90
+ | `signEmbedToken({ orgId, projectId, uniqueIdentifier, secret })` | The signing step on its own, if you want it without a client. |
91
+
92
+ Every failure — a non-2xx status or a body with `success: false` — throws a `ViaSocketError`
93
+ carrying the API's own `message`, the HTTP `status`, and the parsed `body`.
94
+
95
+ ## Rules the API holds you to
96
+
97
+ 1. **The secret and the token stay on the server.** The frontend gets a token per popup, nothing more.
98
+ 2. **A `scriptId` is a credential.** Anyone holding it can run that app as that user.
99
+ 3. **Ids are fetched, never guessed.** Spreadsheet ids, channel ids, repository ids all come from `listOptions`.
100
+ 4. **`existingFields` is not optional when a field depends on another.** Options are scoped by it.
101
+ 5. **One `uniqueIdentifier` per end user, forever.** Connections and subscriptions are isolated by it.
102
+ 6. **Enable only for actions, and check `findEnabled` first.** Subscriptions need just the connection.
103
+
104
+ ## Environments
105
+
106
+ Defaults target production: `https://flow-api.viasocket.com` for the API and
107
+ `https://flow.sokt.io` for running actions. Pass `apiBaseUrl` / `runBaseUrl` to point elsewhere,
108
+ and `scriptUrl` to `connect()` for a non-production connect script.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * The browser half: opening the app's own consent screen and getting the connection back.
4
+ *
5
+ * import { connect } from 'viasocket-apps/browser'
6
+ * const { authId } = await connect({ embedToken, serviceId })
7
+ *
8
+ * It wraps viaSocket's standalone connect script — the popup, the postMessage handshake, the
9
+ * three message types — behind one promise. Nothing here touches the signing secret: the
10
+ * embedToken comes from your backend, already signed for this end user.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ViaSocketConnectError = void 0;
14
+ exports.loadConnectScript = loadConnectScript;
15
+ exports.connect = connect;
16
+ const DEFAULT_SCRIPT_URL = 'https://embed.viasocket.com/prod-connectcomponent.js';
17
+ const SCRIPT_ID = 'viasocket-connect-script';
18
+ class ViaSocketConnectError extends Error {
19
+ code;
20
+ serviceId;
21
+ constructor(code, serviceId, message) {
22
+ super(message);
23
+ this.name = 'ViaSocketConnectError';
24
+ this.code = code;
25
+ this.serviceId = serviceId;
26
+ }
27
+ }
28
+ exports.ViaSocketConnectError = ViaSocketConnectError;
29
+ /** Loaded once per page, however many apps get connected. */
30
+ let scriptPromise = null;
31
+ /**
32
+ * Loads the connect script ahead of the click, so the popup opens without a delay. Optional —
33
+ * `connect()` loads it on demand.
34
+ */
35
+ function loadConnectScript(scriptUrl = DEFAULT_SCRIPT_URL) {
36
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
37
+ return Promise.reject(new ViaSocketConnectError('unavailable', '', 'connect() only runs in a browser'));
38
+ }
39
+ if (typeof window.openViasocketConnection === 'function')
40
+ return Promise.resolve();
41
+ if (scriptPromise)
42
+ return scriptPromise;
43
+ scriptPromise = new Promise((resolve, reject) => {
44
+ const fail = () => {
45
+ scriptPromise = null;
46
+ document.getElementById(SCRIPT_ID)?.remove();
47
+ reject(new ViaSocketConnectError('script', '', `Could not load the viaSocket connect script from ${scriptUrl}`));
48
+ };
49
+ const existing = document.getElementById(SCRIPT_ID);
50
+ const script = existing ?? document.createElement('script');
51
+ script.addEventListener('load', () => resolve(), { once: true });
52
+ script.addEventListener('error', fail, { once: true });
53
+ if (!existing) {
54
+ script.id = SCRIPT_ID;
55
+ script.src = scriptUrl;
56
+ document.body.appendChild(script);
57
+ }
58
+ });
59
+ return scriptPromise;
60
+ }
61
+ /**
62
+ * Opens the consent popup for one app and resolves with the connection it creates.
63
+ *
64
+ * Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user gave up,
65
+ * 'rejected' if the app said no, 'script' if the connect script could not load.
66
+ */
67
+ async function connect(options) {
68
+ const serviceId = options?.serviceId;
69
+ if (!options?.embedToken)
70
+ throw new ViaSocketConnectError('unavailable', serviceId ?? '', 'connect: embedToken is required');
71
+ if (!serviceId)
72
+ throw new ViaSocketConnectError('unavailable', '', 'connect: serviceId is required');
73
+ await loadConnectScript(options.scriptUrl);
74
+ const open = window.openViasocketConnection;
75
+ if (typeof open !== 'function') {
76
+ throw new ViaSocketConnectError('script', serviceId, 'The connect script loaded but did not expose openViasocketConnection()');
77
+ }
78
+ return new Promise((resolve, reject) => {
79
+ const onMessage = (event) => {
80
+ const payload = event?.data;
81
+ if (!payload || typeof payload.type !== 'string' || !payload.type.startsWith('viasocket_connection'))
82
+ return;
83
+ // Two apps can be connecting in one page; only this app's messages are ours.
84
+ if (payload.serviceId && payload.serviceId !== serviceId)
85
+ return;
86
+ window.removeEventListener('message', onMessage);
87
+ if (payload.type === 'viasocket_connection_success') {
88
+ const connection = (payload.data ?? {});
89
+ const authId = typeof connection.id === 'string' ? connection.id : '';
90
+ if (!authId) {
91
+ reject(new ViaSocketConnectError('rejected', serviceId, 'The connection succeeded but carried no id'));
92
+ return;
93
+ }
94
+ resolve({ authId, serviceId, connection });
95
+ }
96
+ else if (payload.type === 'viasocket_connection_error') {
97
+ reject(new ViaSocketConnectError('rejected', serviceId, payload.error?.message || 'The app rejected the connection'));
98
+ }
99
+ else if (payload.type === 'viasocket_connection_closed') {
100
+ reject(new ViaSocketConnectError('closed', serviceId, 'The popup was closed before the connection finished'));
101
+ }
102
+ };
103
+ window.addEventListener('message', onMessage);
104
+ open(options.embedToken, serviceId);
105
+ });
106
+ }
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * The client. One `ViaSocket` per deployment, one `UserScope` per end user.
4
+ *
5
+ * The split mirrors the API's own model: every call except running an action is made *for* a
6
+ * specific end user (the `unique_identifier` in the token), while running an action needs no
7
+ * token at all — the `script_id` is the credential. Putting `runAction` on the root client and
8
+ * everything else behind `user(id)` makes that distinction structural rather than something a
9
+ * reader has to remember.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.UserScope = exports.ViaSocket = void 0;
13
+ const errors_js_1 = require("./errors.js");
14
+ const http_js_1 = require("./http.js");
15
+ const token_js_1 = require("./token.js");
16
+ const DEFAULT_API_BASE_URL = 'https://flow-api.viasocket.com';
17
+ const DEFAULT_RUN_BASE_URL = 'https://flow.sokt.io';
18
+ function required(name, value) {
19
+ if (typeof value !== 'string' || !value.trim())
20
+ throw new errors_js_1.ViaSocketError(`${name} is required`, { status: null });
21
+ return value;
22
+ }
23
+ const trimSlash = (url) => url.replace(/\/+$/, '');
24
+ class ViaSocket {
25
+ orgId;
26
+ projectId;
27
+ apiBaseUrl;
28
+ runBaseUrl;
29
+ secret;
30
+ /** @internal */
31
+ transport;
32
+ constructor(options) {
33
+ this.orgId = required('orgId', options?.orgId);
34
+ this.projectId = required('projectId', options?.projectId);
35
+ this.secret = required('secret', options?.secret);
36
+ this.apiBaseUrl = trimSlash(options.apiBaseUrl || DEFAULT_API_BASE_URL);
37
+ this.runBaseUrl = trimSlash(options.runBaseUrl || DEFAULT_RUN_BASE_URL);
38
+ const transport = options.fetch ?? globalThis.fetch;
39
+ if (typeof transport !== 'function') {
40
+ throw new errors_js_1.ViaSocketError('No fetch available. Use Node 20+, or pass `fetch` in the options.', { status: null });
41
+ }
42
+ // Bound so a bare `fetch` reference still has the right `this` on runtimes that care.
43
+ this.transport = options.fetch ? transport : transport.bind(globalThis);
44
+ }
45
+ /** Everything that is done for one of your end users. */
46
+ user(uniqueIdentifier) {
47
+ return new UserScope(this, required('uniqueIdentifier', uniqueIdentifier));
48
+ }
49
+ /**
50
+ * A signed embed token for one end user — what the connect popup on your frontend needs.
51
+ * Sign on demand per request; there is nothing to gain from caching it in the browser.
52
+ */
53
+ tokenFor(uniqueIdentifier) {
54
+ return (0, token_js_1.signEmbedToken)({
55
+ orgId: this.orgId,
56
+ projectId: this.projectId,
57
+ uniqueIdentifier: required('uniqueIdentifier', uniqueIdentifier),
58
+ secret: this.secret
59
+ });
60
+ }
61
+ /**
62
+ * Runs one action. No token: the script_id from `user.enable()` is the credential, which is
63
+ * exactly why this call belongs on your server.
64
+ */
65
+ async runAction(scriptId, actionVersionId, inputData) {
66
+ const payload = await (0, http_js_1.request)({
67
+ method: 'POST',
68
+ url: `${this.runBaseUrl}/func/${required('scriptId', scriptId)}`,
69
+ body: JSON.stringify({ action_version_id: required('actionVersionId', actionVersionId), inputData: inputData ?? {} }),
70
+ fetchImpl: this.transport
71
+ });
72
+ return payload.data;
73
+ }
74
+ }
75
+ exports.ViaSocket = ViaSocket;
76
+ /**
77
+ * Calls made on behalf of one end user. Obtained from `viasocket.user(id)`.
78
+ *
79
+ * The token is signed once per scope and reused: it carries no expiry, so there is nothing to
80
+ * refresh, and re-signing per call would only add work.
81
+ */
82
+ class UserScope {
83
+ client;
84
+ uniqueIdentifier;
85
+ tokenPromise;
86
+ constructor(client, uniqueIdentifier) {
87
+ this.client = client;
88
+ this.uniqueIdentifier = uniqueIdentifier;
89
+ }
90
+ /** The signed embed token for this user. */
91
+ token() {
92
+ this.tokenPromise ??= this.client.tokenFor(this.uniqueIdentifier);
93
+ return this.tokenPromise;
94
+ }
95
+ async call(method, path, body) {
96
+ return (0, http_js_1.request)({
97
+ method,
98
+ url: `${this.client.apiBaseUrl}${path}`,
99
+ token: await this.token(),
100
+ body: body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body),
101
+ fetchImpl: this.client.transport
102
+ });
103
+ }
104
+ /**
105
+ * Turns a connection into a script_id you can run actions with. Needed only for actions —
106
+ * subscribing to a trigger takes the auth_id alone. Do it once per connection; see
107
+ * `findEnabled()` before calling it again.
108
+ */
109
+ async enable(serviceId, authId) {
110
+ const { data } = await this.call('POST', `/embed/enable/${required('serviceId', serviceId)}/${required('authId', authId)}`, '');
111
+ return { scriptId: data.script_id, webhookUrl: data.webhook_url, serviceId: data.service_id, authId: data.auth_id };
112
+ }
113
+ /**
114
+ * The values one field accepts, scoped by what the user has already chosen.
115
+ *
116
+ * Handles both response shapes the endpoint uses — a plain array, or `{ data, offset }` on
117
+ * fields that paginate — so the caller always gets `{ options, offset }`.
118
+ */
119
+ async listOptions(actionVersionId, params) {
120
+ const { data } = await this.call('POST', `/embed/list-options/${required('actionVersionId', actionVersionId)}`, {
121
+ fieldKey: required('fieldKey', params?.fieldKey),
122
+ auth_id: required('authId', params?.authId),
123
+ existingFields: params?.existingFields ?? {}
124
+ });
125
+ if (Array.isArray(data))
126
+ return { options: data, offset: null };
127
+ return { options: Array.isArray(data?.data) ? data.data : [], offset: data?.offset ?? null };
128
+ }
129
+ /**
130
+ * Subscribes to an app event. Give `webhook` (a URL we POST each event to) or `code`
131
+ * (JavaScript we run per event) — one or the other.
132
+ */
133
+ async subscribe(triggerVersionId, params) {
134
+ if (!params?.webhook === !params?.code) {
135
+ throw new errors_js_1.ViaSocketError('subscribe: pass exactly one of `webhook` or `code`', { status: null });
136
+ }
137
+ const body = {
138
+ auth_id: required('authId', params.authId),
139
+ inputData: params.inputData ?? {},
140
+ meta: params.meta ?? {}
141
+ };
142
+ if (params.webhook)
143
+ body.webhook = params.webhook;
144
+ if (params.code)
145
+ body.code = params.code;
146
+ const { data } = await this.call('POST', `/embed/subscribe-event/${required('triggerVersionId', triggerVersionId)}`, body);
147
+ return {
148
+ scriptId: data.script_id,
149
+ hookUrl: data.inputData?.hookUrl ?? '',
150
+ title: data.title,
151
+ authId: data.auth_id,
152
+ inputData: data.inputData ?? {},
153
+ meta: data.meta ?? {}
154
+ };
155
+ }
156
+ /** Changes a live subscription in place. The script_id does not change. */
157
+ async updateSubscription(subscriptionScriptId, params) {
158
+ const { data } = await this.call('PUT', `/embed/update-subscribed-event/${required('subscriptionScriptId', subscriptionScriptId)}`, params ?? {});
159
+ return {
160
+ scriptId: data.script_id,
161
+ hookUrl: data.inputData?.hookUrl ?? '',
162
+ title: data.title,
163
+ authId: data.auth_id,
164
+ inputData: data.inputData ?? {},
165
+ meta: data.meta ?? {}
166
+ };
167
+ }
168
+ /** Every flow this user has: one per enabled app, one per trigger subscription. */
169
+ async listFlows() {
170
+ const { data } = await this.call('GET', `/projects/${this.client.projectId}/integrations`);
171
+ return (data?.flows ?? []).map((flow) => ({
172
+ id: flow.id,
173
+ title: flow.title,
174
+ status: flow.status,
175
+ webhook: flow.webhook,
176
+ description: flow.description ?? '',
177
+ authId: flow.auth_id,
178
+ serviceId: flow.service_id
179
+ }));
180
+ }
181
+ /**
182
+ * The script_id of an app this user already enabled, or null. Call it before `enable()` —
183
+ * enabling twice leaves you with two script_ids for one app.
184
+ */
185
+ async findEnabled(serviceId) {
186
+ const flows = await this.listFlows();
187
+ return flows.find((flow) => flow.serviceId === required('serviceId', serviceId) && flow.status === 'active')?.id ?? null;
188
+ }
189
+ /** Turns a flow off (0) or back on (1): an enabled app, or a trigger subscription. */
190
+ async setFlowStatus(scriptId, status) {
191
+ const { data } = await this.call('PUT', `/embed/updatestatus/${required('scriptId', scriptId)}?status=${status}`);
192
+ // The response is the whole flow record; nothing else in it is anything a caller acts on.
193
+ return { id: data.id, status: String(data.status) };
194
+ }
195
+ disableFlow(scriptId) {
196
+ return this.setFlowStatus(scriptId, 0);
197
+ }
198
+ enableFlow(scriptId) {
199
+ return this.setFlowStatus(scriptId, 1);
200
+ }
201
+ /**
202
+ * Every app this user has connected. Returned as the API sends it: the shape of each entry
203
+ * is not yet documented, so nothing is mapped or dropped here.
204
+ */
205
+ async listConnections() {
206
+ const { data } = await this.call('GET', '/embed/authentications');
207
+ return Array.isArray(data) ? data : [];
208
+ }
209
+ /** Disconnects an app. Disable the flows built on it first, or they stop working. */
210
+ async revokeConnection(authId) {
211
+ await this.call('DELETE', `/embed/authentications/revoke/${required('authId', authId)}`);
212
+ }
213
+ }
214
+ exports.UserScope = UserScope;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ViaSocketError = void 0;
4
+ /**
5
+ * Thrown for any call that did not succeed — a non-2xx status, or a 2xx whose body says
6
+ * `success: false`. Both carry the message the API gave, because that message is almost
7
+ * always the explanation ("existingFields missing spreadsheet_Id", "invalid token").
8
+ */
9
+ class ViaSocketError extends Error {
10
+ /** HTTP status, or null when the request never got a response. */
11
+ status;
12
+ /** The parsed response body, when there was one. */
13
+ body;
14
+ constructor(message, options) {
15
+ super(message);
16
+ this.name = 'ViaSocketError';
17
+ this.status = options.status;
18
+ this.body = options.body;
19
+ }
20
+ }
21
+ exports.ViaSocketError = ViaSocketError;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /**
3
+ * The one place a request is made and a response judged.
4
+ *
5
+ * viaSocket answers every call with the same envelope, and it can say "no" two ways: a non-2xx
6
+ * status, or a 2xx whose body carries `success: false`. Callers should never have to remember
7
+ * that, so both become a ViaSocketError here with the API's own message attached — that message
8
+ * is almost always the explanation.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.request = request;
12
+ const errors_js_1 = require("./errors.js");
13
+ async function request(options) {
14
+ const headers = { 'Content-Type': 'application/json' };
15
+ if (options.token)
16
+ headers.authorization = options.token;
17
+ let response;
18
+ try {
19
+ response = await options.fetchImpl(options.url, { method: options.method, headers, body: options.body });
20
+ }
21
+ catch (error) {
22
+ throw new errors_js_1.ViaSocketError(`Request to ${options.url} failed: ${error?.message || 'network error'}`, { status: null });
23
+ }
24
+ const text = await response.text();
25
+ let payload = null;
26
+ try {
27
+ payload = text ? JSON.parse(text) : null;
28
+ }
29
+ catch {
30
+ payload = text;
31
+ }
32
+ const envelope = payload && typeof payload === 'object' ? payload : null;
33
+ if (!response.ok || envelope?.success === false) {
34
+ const message = envelope?.message || envelope?.error?.message || `viaSocket responded ${response.status} for ${options.method} ${options.url}`;
35
+ throw new errors_js_1.ViaSocketError(message, { status: response.status, body: payload });
36
+ }
37
+ return (envelope ?? { success: true, data: payload });
38
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * viasocket-apps — server-side client for the viaSocket Apps API.
4
+ *
5
+ * import { ViaSocket } from 'viasocket-apps'
6
+ *
7
+ * const viasocket = new ViaSocket({ orgId, projectId, secret: process.env.VIASOCKET_EMBED_SECRET })
8
+ * const user = viasocket.user(endUserId)
9
+ *
10
+ * const token = await user.token() // for the connect popup
11
+ * const { scriptId } = await user.enable(serviceId, authId) // once, for actions
12
+ * const { options } = await user.listOptions(actionVersionId, {...}) // fill a dropdown
13
+ * const result = await viasocket.runAction(scriptId, actionVersionId, inputData)
14
+ * const sub = await user.subscribe(triggerVersionId, { authId, inputData, webhook })
15
+ *
16
+ * The browser half — opening the connect popup — lives at 'viasocket-apps/browser'.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.signEmbedToken = exports.ViaSocketError = exports.UserScope = exports.ViaSocket = void 0;
20
+ var client_js_1 = require("./client.js");
21
+ Object.defineProperty(exports, "ViaSocket", { enumerable: true, get: function () { return client_js_1.ViaSocket; } });
22
+ Object.defineProperty(exports, "UserScope", { enumerable: true, get: function () { return client_js_1.UserScope; } });
23
+ var errors_js_1 = require("./errors.js");
24
+ Object.defineProperty(exports, "ViaSocketError", { enumerable: true, get: function () { return errors_js_1.ViaSocketError; } });
25
+ var token_js_1 = require("./token.js");
26
+ Object.defineProperty(exports, "signEmbedToken", { enumerable: true, get: function () { return token_js_1.signEmbedToken; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * The embed token: an HS256 JWT of { org_id, project_id, unique_identifier }, signed with the
4
+ * org secret.
5
+ *
6
+ * Implemented on WebCrypto rather than a JWT library so the package has no dependencies and
7
+ * runs unchanged on Node 20+, Bun, Deno and edge runtimes. The payload is exactly the three
8
+ * claims the platform reads, in that order — nothing is added, because the token's only job is
9
+ * to say which end user a call is for.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.signEmbedToken = signEmbedToken;
13
+ const errors_js_1 = require("./errors.js");
14
+ const encoder = new TextEncoder();
15
+ /** Standard base64url: the URL-safe alphabet, no padding. */
16
+ function base64Url(bytes) {
17
+ let binary = '';
18
+ for (const byte of bytes)
19
+ binary += String.fromCharCode(byte);
20
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
21
+ }
22
+ function encodeSegment(value) {
23
+ return base64Url(encoder.encode(JSON.stringify(value)));
24
+ }
25
+ async function signEmbedToken(input) {
26
+ for (const [name, value] of Object.entries(input)) {
27
+ if (typeof value !== 'string' || !value) {
28
+ throw new errors_js_1.ViaSocketError(`signEmbedToken: ${name} is required`, { status: null });
29
+ }
30
+ }
31
+ const header = encodeSegment({ alg: 'HS256', typ: 'JWT' });
32
+ const payload = encodeSegment({
33
+ org_id: input.orgId,
34
+ project_id: input.projectId,
35
+ unique_identifier: input.uniqueIdentifier
36
+ });
37
+ const signingInput = `${header}.${payload}`;
38
+ const key = await crypto.subtle.importKey('raw', encoder.encode(input.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
39
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(signingInput));
40
+ return `${signingInput}.${base64Url(new Uint8Array(signature))}`;
41
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * Public types. Everything the API returns in snake_case is mapped to camelCase here, so the
4
+ * shape a caller sees is the shape a JavaScript codebase expects — and so that if a field is
5
+ * ever renamed upstream, it is renamed in one place.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The browser half: opening the app's own consent screen and getting the connection back.
3
+ *
4
+ * import { connect } from 'viasocket-apps/browser'
5
+ * const { authId } = await connect({ embedToken, serviceId })
6
+ *
7
+ * It wraps viaSocket's standalone connect script — the popup, the postMessage handshake, the
8
+ * three message types — behind one promise. Nothing here touches the signing secret: the
9
+ * embedToken comes from your backend, already signed for this end user.
10
+ */
11
+ export type ConnectErrorCode =
12
+ /** The user closed the popup before finishing. Nothing was created. */
13
+ 'closed'
14
+ /** The app refused the authorisation. */
15
+ | 'rejected'
16
+ /** The connect script could not be loaded. */
17
+ | 'script'
18
+ /** Not running in a browser, or the popup could not be opened. */
19
+ | 'unavailable';
20
+ export declare class ViaSocketConnectError extends Error {
21
+ readonly code: ConnectErrorCode;
22
+ readonly serviceId: string;
23
+ constructor(code: ConnectErrorCode, serviceId: string, message: string);
24
+ }
25
+ export interface ConnectOptions {
26
+ /** Signed on your backend for the current end user — see `viasocket.user(id).token()`. */
27
+ embedToken: string;
28
+ /** The app to connect. Its service_id is shown on every app in the Apps API reference. */
29
+ serviceId: string;
30
+ /** Where the connect script is served from. Only override for a non-production environment. */
31
+ scriptUrl?: string;
32
+ }
33
+ export interface ConnectResult {
34
+ /** The connection. Every later call for this app takes it as auth_id. */
35
+ authId: string;
36
+ serviceId: string;
37
+ /** The full connection record the popup posted back. */
38
+ connection: Record<string, unknown>;
39
+ }
40
+ declare global {
41
+ interface Window {
42
+ openViasocketConnection?: (embedToken: string, serviceId: string) => void;
43
+ }
44
+ }
45
+ /**
46
+ * Loads the connect script ahead of the click, so the popup opens without a delay. Optional —
47
+ * `connect()` loads it on demand.
48
+ */
49
+ export declare function loadConnectScript(scriptUrl?: string): Promise<void>;
50
+ /**
51
+ * Opens the consent popup for one app and resolves with the connection it creates.
52
+ *
53
+ * Rejects with a ViaSocketConnectError whose `code` says why: 'closed' if the user gave up,
54
+ * 'rejected' if the app said no, 'script' if the connect script could not load.
55
+ */
56
+ export declare function connect(options: ConnectOptions): Promise<ConnectResult>;