viasocket-apps 0.1.2 → 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 CHANGED
@@ -88,7 +88,9 @@ const subscription = await user.subscribe(triggerVersionId, {
88
88
  import { connect } from 'viasocket-apps/browser'
89
89
 
90
90
  // embedToken comes from your backend (user.token()), signed for the signed-in user.
91
- const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc' })
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 })
92
94
  // Send authId to your backend, which enables the app and stores the script_id.
93
95
  ```
94
96
 
@@ -105,6 +107,9 @@ const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc' })
105
107
  | `user.findEnabled(serviceId)` | The `scriptId` if this app is already enabled, else `null`. Call before `enable`. |
106
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. |
107
109
  | `viasocket.runAction(scriptId, actionVersionId, inputData)` | Run one action. Returns the app's own response. |
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. |
108
113
  | `user.subscribe(triggerVersionId, { authId, inputData, code \| webhook, meta })` | Subscribe to an app event with a handler we run per event. Returns `{ scriptId, hookUrl, … }`. |
109
114
  | `user.updateSubscription(scriptId, { code, meta })` | Change a live subscription in place. |
110
115
 
@@ -101,6 +101,14 @@ async function connect(options) {
101
101
  }
102
102
  };
103
103
  window.addEventListener('message', onMessage);
104
- open(options.embedToken, serviceId);
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
  }
@@ -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));
@@ -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 {};
@@ -95,6 +95,14 @@ export async function connect(options) {
95
95
  }
96
96
  };
97
97
  window.addEventListener('message', onMessage);
98
- open(options.embedToken, serviceId);
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
  }
@@ -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
  /**
@@ -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));
@@ -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';
@@ -98,6 +98,43 @@ export interface Flow {
98
98
  }
99
99
  export type FlowStatus = 0 | 1;
100
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
+ }
101
138
  export interface Envelope<T = unknown> {
102
139
  success: boolean;
103
140
  message?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viasocket-apps",
3
- "version": "0.1.2",
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",