@withone/connect 0.13.2 → 0.15.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.
@@ -153,6 +153,12 @@ function tokenScopes(accessToken) {
153
153
 
154
154
  /** One catalog action for a platform. */
155
155
 
156
+ /**
157
+ * Everything One knows about one action: the guide a caller reads before
158
+ * running it (`knowledge`, Markdown), the shape of its input, and the
159
+ * method and path the SDK runs it with.
160
+ */
161
+
156
162
  /**
157
163
  * - `not_connected`: nothing is stored for this user.
158
164
  * - `reconnect_required` (key mode): One will not act for this user.
@@ -468,15 +474,53 @@ function createTokenCredential(tokenStore, postToken) {
468
474
  * const { redirectUrl, cookie } = oneConnect.startAuthorization({ loginHint });
469
475
  * // in the callback route
470
476
  * const result = await oneConnect.completeAuthorization({ userId, url, getCookie });
471
- * // afterwards
477
+ * // afterwards, the same way the One CLI works: find the action, read
478
+ * // its knowledge, run it
472
479
  * const rows = await oneConnect.listConnections(userId);
473
- * const reply = await oneConnect.runAction(userId, { connectionKey, actionId, method, path });
480
+ * const [action] = await oneConnect.searchActions(userId, "stripe", "create an invoice");
481
+ * const guide = await oneConnect.getActionKnowledge(userId, action._id);
482
+ * const reply = await oneConnect.runAction(userId, { connectionKey, actionId: action._id, body });
474
483
  *
475
484
  * The Next.js and Node adapters turn the first two into route handlers.
476
485
  * See `./key` and `./token` for what each mode stores and sends.
477
486
  */
478
487
  const CATALOG_PAGE_SIZE = 100;
479
488
  const CATALOG_MAX_PAGES = 20;
489
+ const SEARCH_DEFAULT_LIMIT = 5;
490
+ const isPlainObject = value => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Blob) && !(value instanceof FormData);
491
+
492
+ /** Fills a path's `{{placeholders}}`. A placeholder with no value is an
493
+ * error the caller can read, rather than a request One cannot route. */
494
+ function fillPath(path, params = {}) {
495
+ return path.replace(/\{\{([^}]+)\}\}/g, (_match, name) => {
496
+ const key = name.trim();
497
+ const value = params[key];
498
+ if (value === undefined || value === null || value === "") throw new OneConnectError("request_failed", `The action's path needs a value for {{${key}}}; pass it in pathParams.`);
499
+ return encodeURIComponent(String(value));
500
+ });
501
+ }
502
+
503
+ /** `application/x-www-form-urlencoded` with nested objects and arrays in
504
+ * bracket notation, as Stripe and other form providers read it. */
505
+ function formEncode(value) {
506
+ const out = new URLSearchParams();
507
+ const walk = (prefix, v) => {
508
+ if (v === undefined || v === null) return;
509
+ if (Array.isArray(v)) v.forEach((item, i) => walk(`${prefix}[${typeof item === "object" ? i : ""}]`, item));else if (typeof v === "object") for (const [k, inner] of Object.entries(v)) walk(prefix ? `${prefix}[${k}]` : k, inner);else out.append(prefix, String(v));
510
+ };
511
+ walk("", value);
512
+ return out.toString();
513
+ }
514
+ const toKnowledge = row => ({
515
+ _id: String(row._id ?? ""),
516
+ title: String(row.title ?? ""),
517
+ method: String(row.method ?? "").toUpperCase(),
518
+ path: String(row.path ?? ""),
519
+ tags: Array.isArray(row.tags) ? row.tags.map(String) : [],
520
+ knowledge: String(row.knowledge ?? ""),
521
+ ioSchema: row.ioSchema,
522
+ platform: row.connectionPlatform ? String(row.connectionPlatform) : undefined
523
+ });
480
524
 
481
525
  /** What an app does with One in either mode. */
482
526
 
@@ -651,29 +695,34 @@ function createOneConnect(config) {
651
695
  const body = await response.json();
652
696
  return body.rows ?? [];
653
697
  };
698
+
699
+ /**
700
+ * Turns a catalog refusal into the right error. A One API that does not
701
+ * take the credential on its catalog answers the same bare 403 as a
702
+ * consent that is gone; asking what the grant reaches tells them apart.
703
+ */
704
+ const catalogRefusal = async (userId, what, response) => {
705
+ const refused = credential.refusal(response.status, await response.text());
706
+ if (refused?.code === "reconnect_required") {
707
+ if (await consentStands(userId, refused)) return new OneConnectError("request_failed", `One's ${what} refused the ${keyCredential ? "connect key" : "access token"} (HTTP 403) although the user's consent stands: this One API does not accept it on the ${what}.`, response.status);
708
+ return refused;
709
+ }
710
+ if (refused) return refused;
711
+ return new OneConnectError("request_failed", `One refused the ${what} request (HTTP ${response.status}).`, response.status);
712
+ };
713
+ const slim = rows => rows.map(row => ({
714
+ _id: String(row._id ?? row.systemId ?? ""),
715
+ title: String(row.title ?? ""),
716
+ method: String(row.method ?? "").toUpperCase(),
717
+ path: String(row.path ?? ""),
718
+ ...(Array.isArray(row.tags) ? {
719
+ tags: row.tags.map(String)
720
+ } : {})
721
+ }));
654
722
  const listActions = async (userId, platform) => {
655
723
  const pageUrl = page => `/knowledge?connectionPlatform=${encodeURIComponent(platform)}&limit=${CATALOG_PAGE_SIZE}&page=${page}`;
656
- const slim = rows => rows.map(row => ({
657
- _id: String(row._id ?? ""),
658
- title: String(row.title ?? ""),
659
- method: String(row.method ?? ""),
660
- path: String(row.path ?? "")
661
- }));
662
724
  const first = await oneFetch(userId, pageUrl(1));
663
- if (!first.ok) {
664
- const refused = credential.refusal(first.status, await first.text());
665
- if (refused?.code === "reconnect_required") {
666
- // The same bare 403 is what a One API answers when its catalog
667
- // does not take the connect key. If the consent stands, that is
668
- // what happened, and asking the user to reconnect would not help.
669
- if (await consentStands(userId, refused)) {
670
- throw new OneConnectError("request_failed", "One's action catalog refused the connect key (HTTP 403) although the user's consent stands: this One API does not accept the connect key on the catalog.", first.status);
671
- }
672
- throw refused;
673
- }
674
- if (refused) throw refused;
675
- throw new OneConnectError("request_failed", `One refused the catalog request (HTTP ${first.status}).`, first.status);
676
- }
725
+ if (!first.ok) throw await catalogRefusal(userId, "action catalog", first);
677
726
  const page1 = await first.json();
678
727
  const pages = Math.min(Math.max(page1.pages ?? 1, 1), CATALOG_MAX_PAGES);
679
728
  const rest = await Promise.all(Array.from({
@@ -681,19 +730,90 @@ function createOneConnect(config) {
681
730
  }, (_, index) => oneFetch(userId, pageUrl(index + 2)).then(response => response.ok ? response.json() : null).catch(() => null)));
682
731
  return slim([page1, ...rest].flatMap(page => page?.rows ?? []));
683
732
  };
733
+
734
+ /** The actions that fit a request in words, best first: One's search,
735
+ * the one the One CLI's `actions search` uses. */
736
+ const searchActions = async (userId, platform, query, options = {}) => {
737
+ const params = new URLSearchParams({
738
+ query,
739
+ limit: String(options.limit ?? SEARCH_DEFAULT_LIMIT),
740
+ [options.mode === "knowledge" ? "knowledgeAgent" : "executeAgent"]: "true"
741
+ });
742
+ const response = await oneFetch(userId, `/available-actions/search/${encodeURIComponent(platform)}?${params}`);
743
+ if (!response.ok) throw await catalogRefusal(userId, "action search", response);
744
+ const body = await response.json();
745
+ return slim(Array.isArray(body) ? body : body.rows ?? []);
746
+ };
747
+
748
+ /** One action's knowledge, cached for the life of the client: an
749
+ * action's guide does not change between two calls. */
750
+ const knowledgeCache = new Map();
751
+ const getActionKnowledge = (userId, actionId) => {
752
+ const cached = knowledgeCache.get(actionId);
753
+ if (cached) return cached;
754
+ const loading = (async () => {
755
+ const response = await oneFetch(userId, `/knowledge?_id=${encodeURIComponent(actionId)}`);
756
+ if (!response.ok) throw await catalogRefusal(userId, "action knowledge", response);
757
+ const body = await response.json();
758
+ const row = body.rows?.[0];
759
+ if (!row) throw new OneConnectError("request_failed", `One has no action with the id ${actionId}.`, 404);
760
+ return toKnowledge(row);
761
+ })();
762
+ knowledgeCache.set(actionId, loading);
763
+ loading.catch(() => knowledgeCache.delete(actionId));
764
+ return loading;
765
+ };
766
+
767
+ /**
768
+ * Runs one action through One the way the One CLI's `actions execute`
769
+ * does: the method and path come from the action itself, the path's
770
+ * placeholders are filled, an action One serves gets the connection key
771
+ * in its body, and the body is encoded as the provider reads it.
772
+ */
684
773
  const runAction = async (userId, input) => {
685
- const method = input.method.toUpperCase();
686
- const query = input.query ? `?${new URLSearchParams(input.query)}` : "";
774
+ // The caller may hand over method and path from the catalog; otherwise
775
+ // the action says, and its tags say whether it is one One serves.
776
+ const action = input.method && input.path ? {
777
+ method: input.method,
778
+ path: input.path,
779
+ tags: []
780
+ } : await getActionKnowledge(userId, input.actionId);
781
+ const method = action.method.toUpperCase();
782
+ const path = fillPath(action.path, input.pathParams);
783
+ const search = new URLSearchParams();
784
+ for (const [key, value] of Object.entries(input.query ?? {})) for (const item of Array.isArray(value) ? value : [value]) search.append(key, item);
785
+ const query = search.size ? `?${search}` : "";
687
786
  const headers = {
787
+ ...input.headers,
688
788
  "x-one-connection-key": input.connectionKey,
689
789
  "x-one-action-id": input.actionId
690
790
  };
691
- const hasBody = method !== "GET" && method !== "HEAD" && input.body !== undefined;
692
- if (hasBody) headers["Content-Type"] = "application/json";
693
- const response = await oneFetch(userId, `/passthrough${input.path}${query}`, {
791
+ const takesBody = method !== "GET" && method !== "HEAD";
792
+ let payload = input.body;
793
+ // An action One serves itself reads the connection key from its body.
794
+ if (takesBody && action.tags.includes("custom") && (payload === undefined || isPlainObject(payload))) payload = {
795
+ ...(payload ?? {}),
796
+ connectionKey: input.connectionKey
797
+ };
798
+ let body;
799
+ if (takesBody && payload !== undefined) {
800
+ const encoding = input.encoding ?? "json";
801
+ if (encoding === "form") {
802
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
803
+ body = formEncode(payload);
804
+ } else if (encoding === "multipart") {
805
+ const form = new FormData();
806
+ for (const [key, value] of Object.entries(isPlainObject(payload) ? payload : {})) form.append(key, value instanceof Blob ? value : typeof value === "object" ? JSON.stringify(value) : String(value));
807
+ body = form; // fetch sets the multipart boundary itself
808
+ } else {
809
+ headers["Content-Type"] = "application/json";
810
+ body = JSON.stringify(payload);
811
+ }
812
+ }
813
+ const response = await oneFetch(userId, `/passthrough${path}${query}`, {
694
814
  method,
695
815
  headers,
696
- body: hasBody ? JSON.stringify(input.body) : undefined
816
+ body
697
817
  });
698
818
  const text = await response.text();
699
819
  if (!response.ok) {
@@ -737,6 +857,8 @@ function createOneConnect(config) {
737
857
  disconnect: credential.disconnect,
738
858
  listConnections,
739
859
  listActions,
860
+ searchActions,
861
+ getActionKnowledge,
740
862
  runAction,
741
863
  fetch: oneFetch
742
864
  };
@@ -761,6 +883,8 @@ function createOneConnect(config) {
761
883
  exports.OneConnectError = OneConnectError;
762
884
  exports.createOneConnect = createOneConnect;
763
885
  exports.encodeUserReference = encodeUserReference;
886
+ exports.fillPath = fillPath;
887
+ exports.formEncode = formEncode;
764
888
  exports.parseUserReference = parseUserReference;
765
889
  exports.refreshTokenExpiresAt = refreshTokenExpiresAt;
766
890
  exports.tenancyHeaders = tenancyHeaders;
@@ -1,7 +1,13 @@
1
- import { type CompleteAuthorizationInput, type CompleteAuthorizationResult, type OneConnectKeyConfig, type OneConnectMode, type OneConnectServerConfig, type OneConnectTokenConfig, type OneConnectTokens, type PlatformAction, type ReachableConnection, type RefreshIfExpiringOptions, type RunActionInput, type RunActionResult, type StartAuthorizationInput, type StartAuthorizationResult } from "./types";
1
+ import { type ActionKnowledge, type CompleteAuthorizationInput, type CompleteAuthorizationResult, type OneConnectKeyConfig, type OneConnectMode, type OneConnectServerConfig, type OneConnectTokenConfig, type OneConnectTokens, type PlatformAction, type ReachableConnection, type RefreshIfExpiringOptions, type RunActionInput, type RunActionResult, type SearchActionsOptions, type StartAuthorizationInput, type StartAuthorizationResult } from "./types";
2
2
  export * from "./types";
3
3
  export { encodeUserReference, parseUserReference } from "./key";
4
4
  export { refreshTokenExpiresAt, tenancyHeaders, tokenScopes } from "./oauth";
5
+ /** Fills a path's `{{placeholders}}`. A placeholder with no value is an
6
+ * error the caller can read, rather than a request One cannot route. */
7
+ export declare function fillPath(path: string, params?: Record<string, string | number | boolean>): string;
8
+ /** `application/x-www-form-urlencoded` with nested objects and arrays in
9
+ * bracket notation, as Stripe and other form providers read it. */
10
+ export declare function formEncode(value: unknown): string;
5
11
  /** What an app does with One in either mode. */
6
12
  export interface OneConnectClient {
7
13
  /** How this client holds each user's grant. */
@@ -24,7 +30,13 @@ export interface OneConnectClient {
24
30
  /** Every catalog action of a platform. What exists, not what is
25
31
  * permitted; the grant decides that when the action runs. */
26
32
  listActions: (userId: string, platform: string) => Promise<PlatformAction[]>;
27
- /** Runs one action through One with the grant. */
33
+ /** The actions of a platform that fit a request in words, best first. */
34
+ searchActions: (userId: string, platform: string, query: string, options?: SearchActionsOptions) => Promise<PlatformAction[]>;
35
+ /** An action's guide, input shape, method and path. Read it before
36
+ * running an action for the first time. */
37
+ getActionKnowledge: (userId: string, actionId: string) => Promise<ActionKnowledge>;
38
+ /** Runs one action through One with the grant: fills the path, encodes
39
+ * the body, adds what the action needs. */
28
40
  runAction: (userId: string, input: RunActionInput) => Promise<RunActionResult>;
29
41
  /** Any authenticated request to One's /v1 API, headers handled. */
30
42
  fetch: (userId: string, path: string, init?: RequestInit) => Promise<Response>;
@@ -151,6 +151,12 @@ function tokenScopes(accessToken) {
151
151
 
152
152
  /** One catalog action for a platform. */
153
153
 
154
+ /**
155
+ * Everything One knows about one action: the guide a caller reads before
156
+ * running it (`knowledge`, Markdown), the shape of its input, and the
157
+ * method and path the SDK runs it with.
158
+ */
159
+
154
160
  /**
155
161
  * - `not_connected`: nothing is stored for this user.
156
162
  * - `reconnect_required` (key mode): One will not act for this user.
@@ -466,15 +472,53 @@ function createTokenCredential(tokenStore, postToken) {
466
472
  * const { redirectUrl, cookie } = oneConnect.startAuthorization({ loginHint });
467
473
  * // in the callback route
468
474
  * const result = await oneConnect.completeAuthorization({ userId, url, getCookie });
469
- * // afterwards
475
+ * // afterwards, the same way the One CLI works: find the action, read
476
+ * // its knowledge, run it
470
477
  * const rows = await oneConnect.listConnections(userId);
471
- * const reply = await oneConnect.runAction(userId, { connectionKey, actionId, method, path });
478
+ * const [action] = await oneConnect.searchActions(userId, "stripe", "create an invoice");
479
+ * const guide = await oneConnect.getActionKnowledge(userId, action._id);
480
+ * const reply = await oneConnect.runAction(userId, { connectionKey, actionId: action._id, body });
472
481
  *
473
482
  * The Next.js and Node adapters turn the first two into route handlers.
474
483
  * See `./key` and `./token` for what each mode stores and sends.
475
484
  */
476
485
  const CATALOG_PAGE_SIZE = 100;
477
486
  const CATALOG_MAX_PAGES = 20;
487
+ const SEARCH_DEFAULT_LIMIT = 5;
488
+ const isPlainObject = value => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Blob) && !(value instanceof FormData);
489
+
490
+ /** Fills a path's `{{placeholders}}`. A placeholder with no value is an
491
+ * error the caller can read, rather than a request One cannot route. */
492
+ function fillPath(path, params = {}) {
493
+ return path.replace(/\{\{([^}]+)\}\}/g, (_match, name) => {
494
+ const key = name.trim();
495
+ const value = params[key];
496
+ if (value === undefined || value === null || value === "") throw new OneConnectError("request_failed", `The action's path needs a value for {{${key}}}; pass it in pathParams.`);
497
+ return encodeURIComponent(String(value));
498
+ });
499
+ }
500
+
501
+ /** `application/x-www-form-urlencoded` with nested objects and arrays in
502
+ * bracket notation, as Stripe and other form providers read it. */
503
+ function formEncode(value) {
504
+ const out = new URLSearchParams();
505
+ const walk = (prefix, v) => {
506
+ if (v === undefined || v === null) return;
507
+ if (Array.isArray(v)) v.forEach((item, i) => walk(`${prefix}[${typeof item === "object" ? i : ""}]`, item));else if (typeof v === "object") for (const [k, inner] of Object.entries(v)) walk(prefix ? `${prefix}[${k}]` : k, inner);else out.append(prefix, String(v));
508
+ };
509
+ walk("", value);
510
+ return out.toString();
511
+ }
512
+ const toKnowledge = row => ({
513
+ _id: String(row._id ?? ""),
514
+ title: String(row.title ?? ""),
515
+ method: String(row.method ?? "").toUpperCase(),
516
+ path: String(row.path ?? ""),
517
+ tags: Array.isArray(row.tags) ? row.tags.map(String) : [],
518
+ knowledge: String(row.knowledge ?? ""),
519
+ ioSchema: row.ioSchema,
520
+ platform: row.connectionPlatform ? String(row.connectionPlatform) : undefined
521
+ });
478
522
 
479
523
  /** What an app does with One in either mode. */
480
524
 
@@ -649,29 +693,34 @@ function createOneConnect(config) {
649
693
  const body = await response.json();
650
694
  return body.rows ?? [];
651
695
  };
696
+
697
+ /**
698
+ * Turns a catalog refusal into the right error. A One API that does not
699
+ * take the credential on its catalog answers the same bare 403 as a
700
+ * consent that is gone; asking what the grant reaches tells them apart.
701
+ */
702
+ const catalogRefusal = async (userId, what, response) => {
703
+ const refused = credential.refusal(response.status, await response.text());
704
+ if (refused?.code === "reconnect_required") {
705
+ if (await consentStands(userId, refused)) return new OneConnectError("request_failed", `One's ${what} refused the ${keyCredential ? "connect key" : "access token"} (HTTP 403) although the user's consent stands: this One API does not accept it on the ${what}.`, response.status);
706
+ return refused;
707
+ }
708
+ if (refused) return refused;
709
+ return new OneConnectError("request_failed", `One refused the ${what} request (HTTP ${response.status}).`, response.status);
710
+ };
711
+ const slim = rows => rows.map(row => ({
712
+ _id: String(row._id ?? row.systemId ?? ""),
713
+ title: String(row.title ?? ""),
714
+ method: String(row.method ?? "").toUpperCase(),
715
+ path: String(row.path ?? ""),
716
+ ...(Array.isArray(row.tags) ? {
717
+ tags: row.tags.map(String)
718
+ } : {})
719
+ }));
652
720
  const listActions = async (userId, platform) => {
653
721
  const pageUrl = page => `/knowledge?connectionPlatform=${encodeURIComponent(platform)}&limit=${CATALOG_PAGE_SIZE}&page=${page}`;
654
- const slim = rows => rows.map(row => ({
655
- _id: String(row._id ?? ""),
656
- title: String(row.title ?? ""),
657
- method: String(row.method ?? ""),
658
- path: String(row.path ?? "")
659
- }));
660
722
  const first = await oneFetch(userId, pageUrl(1));
661
- if (!first.ok) {
662
- const refused = credential.refusal(first.status, await first.text());
663
- if (refused?.code === "reconnect_required") {
664
- // The same bare 403 is what a One API answers when its catalog
665
- // does not take the connect key. If the consent stands, that is
666
- // what happened, and asking the user to reconnect would not help.
667
- if (await consentStands(userId, refused)) {
668
- throw new OneConnectError("request_failed", "One's action catalog refused the connect key (HTTP 403) although the user's consent stands: this One API does not accept the connect key on the catalog.", first.status);
669
- }
670
- throw refused;
671
- }
672
- if (refused) throw refused;
673
- throw new OneConnectError("request_failed", `One refused the catalog request (HTTP ${first.status}).`, first.status);
674
- }
723
+ if (!first.ok) throw await catalogRefusal(userId, "action catalog", first);
675
724
  const page1 = await first.json();
676
725
  const pages = Math.min(Math.max(page1.pages ?? 1, 1), CATALOG_MAX_PAGES);
677
726
  const rest = await Promise.all(Array.from({
@@ -679,19 +728,90 @@ function createOneConnect(config) {
679
728
  }, (_, index) => oneFetch(userId, pageUrl(index + 2)).then(response => response.ok ? response.json() : null).catch(() => null)));
680
729
  return slim([page1, ...rest].flatMap(page => page?.rows ?? []));
681
730
  };
731
+
732
+ /** The actions that fit a request in words, best first: One's search,
733
+ * the one the One CLI's `actions search` uses. */
734
+ const searchActions = async (userId, platform, query, options = {}) => {
735
+ const params = new URLSearchParams({
736
+ query,
737
+ limit: String(options.limit ?? SEARCH_DEFAULT_LIMIT),
738
+ [options.mode === "knowledge" ? "knowledgeAgent" : "executeAgent"]: "true"
739
+ });
740
+ const response = await oneFetch(userId, `/available-actions/search/${encodeURIComponent(platform)}?${params}`);
741
+ if (!response.ok) throw await catalogRefusal(userId, "action search", response);
742
+ const body = await response.json();
743
+ return slim(Array.isArray(body) ? body : body.rows ?? []);
744
+ };
745
+
746
+ /** One action's knowledge, cached for the life of the client: an
747
+ * action's guide does not change between two calls. */
748
+ const knowledgeCache = new Map();
749
+ const getActionKnowledge = (userId, actionId) => {
750
+ const cached = knowledgeCache.get(actionId);
751
+ if (cached) return cached;
752
+ const loading = (async () => {
753
+ const response = await oneFetch(userId, `/knowledge?_id=${encodeURIComponent(actionId)}`);
754
+ if (!response.ok) throw await catalogRefusal(userId, "action knowledge", response);
755
+ const body = await response.json();
756
+ const row = body.rows?.[0];
757
+ if (!row) throw new OneConnectError("request_failed", `One has no action with the id ${actionId}.`, 404);
758
+ return toKnowledge(row);
759
+ })();
760
+ knowledgeCache.set(actionId, loading);
761
+ loading.catch(() => knowledgeCache.delete(actionId));
762
+ return loading;
763
+ };
764
+
765
+ /**
766
+ * Runs one action through One the way the One CLI's `actions execute`
767
+ * does: the method and path come from the action itself, the path's
768
+ * placeholders are filled, an action One serves gets the connection key
769
+ * in its body, and the body is encoded as the provider reads it.
770
+ */
682
771
  const runAction = async (userId, input) => {
683
- const method = input.method.toUpperCase();
684
- const query = input.query ? `?${new URLSearchParams(input.query)}` : "";
772
+ // The caller may hand over method and path from the catalog; otherwise
773
+ // the action says, and its tags say whether it is one One serves.
774
+ const action = input.method && input.path ? {
775
+ method: input.method,
776
+ path: input.path,
777
+ tags: []
778
+ } : await getActionKnowledge(userId, input.actionId);
779
+ const method = action.method.toUpperCase();
780
+ const path = fillPath(action.path, input.pathParams);
781
+ const search = new URLSearchParams();
782
+ for (const [key, value] of Object.entries(input.query ?? {})) for (const item of Array.isArray(value) ? value : [value]) search.append(key, item);
783
+ const query = search.size ? `?${search}` : "";
685
784
  const headers = {
785
+ ...input.headers,
686
786
  "x-one-connection-key": input.connectionKey,
687
787
  "x-one-action-id": input.actionId
688
788
  };
689
- const hasBody = method !== "GET" && method !== "HEAD" && input.body !== undefined;
690
- if (hasBody) headers["Content-Type"] = "application/json";
691
- const response = await oneFetch(userId, `/passthrough${input.path}${query}`, {
789
+ const takesBody = method !== "GET" && method !== "HEAD";
790
+ let payload = input.body;
791
+ // An action One serves itself reads the connection key from its body.
792
+ if (takesBody && action.tags.includes("custom") && (payload === undefined || isPlainObject(payload))) payload = {
793
+ ...(payload ?? {}),
794
+ connectionKey: input.connectionKey
795
+ };
796
+ let body;
797
+ if (takesBody && payload !== undefined) {
798
+ const encoding = input.encoding ?? "json";
799
+ if (encoding === "form") {
800
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
801
+ body = formEncode(payload);
802
+ } else if (encoding === "multipart") {
803
+ const form = new FormData();
804
+ for (const [key, value] of Object.entries(isPlainObject(payload) ? payload : {})) form.append(key, value instanceof Blob ? value : typeof value === "object" ? JSON.stringify(value) : String(value));
805
+ body = form; // fetch sets the multipart boundary itself
806
+ } else {
807
+ headers["Content-Type"] = "application/json";
808
+ body = JSON.stringify(payload);
809
+ }
810
+ }
811
+ const response = await oneFetch(userId, `/passthrough${path}${query}`, {
692
812
  method,
693
813
  headers,
694
- body: hasBody ? JSON.stringify(input.body) : undefined
814
+ body
695
815
  });
696
816
  const text = await response.text();
697
817
  if (!response.ok) {
@@ -735,6 +855,8 @@ function createOneConnect(config) {
735
855
  disconnect: credential.disconnect,
736
856
  listConnections,
737
857
  listActions,
858
+ searchActions,
859
+ getActionKnowledge,
738
860
  runAction,
739
861
  fetch: oneFetch
740
862
  };
@@ -756,4 +878,4 @@ function createOneConnect(config) {
756
878
  };
757
879
  }
758
880
 
759
- export { OneConnectError, createOneConnect, encodeUserReference, parseUserReference, refreshTokenExpiresAt, tenancyHeaders, tokenScopes };
881
+ export { OneConnectError, createOneConnect, encodeUserReference, fillPath, formEncode, parseUserReference, refreshTokenExpiresAt, tenancyHeaders, tokenScopes };
@@ -207,17 +207,54 @@ export interface PlatformAction {
207
207
  title: string;
208
208
  method: string;
209
209
  path: string;
210
+ /** One's tags for the action. "custom" marks an action One itself
211
+ * serves, which takes the connection key in its body. */
212
+ tags?: string[];
210
213
  }
214
+ export interface SearchActionsOptions {
215
+ /** How many candidates to return. Five when omitted. */
216
+ limit?: number;
217
+ /** What the search ranks for: actions to run now (the default), or
218
+ * actions to write code and flows against. */
219
+ mode?: "execute" | "knowledge";
220
+ }
221
+ /**
222
+ * Everything One knows about one action: the guide a caller reads before
223
+ * running it (`knowledge`, Markdown), the shape of its input, and the
224
+ * method and path the SDK runs it with.
225
+ */
226
+ export interface ActionKnowledge extends PlatformAction {
227
+ tags: string[];
228
+ /** The action's documentation: what it does, every field it takes,
229
+ * what it answers. Markdown. */
230
+ knowledge: string;
231
+ /** The input and output shape, when One has one. */
232
+ ioSchema?: unknown;
233
+ /** The platform the action belongs to. */
234
+ platform?: string;
235
+ }
236
+ export type ActionBodyEncoding = "json" | "form" | "multipart";
211
237
  export interface RunActionInput {
212
238
  /** From `listConnections`. */
213
239
  connectionKey: string;
214
- /** From `listActions`. */
240
+ /** From `searchActions`, `listActions` or `getActionKnowledge`. */
215
241
  actionId: string;
216
- method: string;
217
- /** The action's path, appended to /v1/passthrough. */
218
- path: string;
242
+ /** The action's method. Looked up from `actionId` when omitted. */
243
+ method?: string;
244
+ /** The action's path, appended to /v1/passthrough. Looked up from
245
+ * `actionId` when omitted. */
246
+ path?: string;
247
+ /** Values for the path's `{{placeholders}}`, such as `{ calendarId: "primary" }`. */
248
+ pathParams?: Record<string, string | number | boolean>;
219
249
  body?: unknown;
220
- query?: Record<string, string>;
250
+ query?: Record<string, string | string[]>;
251
+ /** Extra request headers an action's knowledge asks for, such as a
252
+ * provider's version header. */
253
+ headers?: Record<string, string>;
254
+ /** How `body` is sent. JSON when omitted; "form" for providers that
255
+ * take `application/x-www-form-urlencoded` (nested fields in bracket
256
+ * notation); "multipart" for file-style uploads. */
257
+ encoding?: ActionBodyEncoding;
221
258
  }
222
259
  export interface RunActionResult {
223
260
  status: number;
package/dist/svelte.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Svelte compiler or dependency is involved:
6
6
  *
7
7
  * <div use:connectButton={{ authorizeUrl: "/api/one/authorize",
8
- * platforms: ["stripe", "notion"], connected: data.hasOneGrant,
8
+ * logos: ["stripe", "notion"], connected: data.hasOneGrant,
9
9
  * onSuccess: () => { ... } }} />
10
10
  */
11
11
  import { type ConnectButtonProps } from "@withone/connect";
package/dist/types.d.ts CHANGED
@@ -27,8 +27,6 @@ export interface OneConnectFlowOptions {
27
27
  /** Theme for One's hosted page. Carried on the URL fragment, which
28
28
  * survives the redirect chain, so the backend forwards nothing. */
29
29
  connectTheme?: OneConnectTheme;
30
- /** @deprecated Renamed to `connectTheme`; removed in the next minor. */
31
- appTheme?: OneConnectTheme;
32
30
  /** The grant completed and the backend stored the tokens. Fires once
33
31
  * per page load, on the first flow still mounted when the tab
34
32
  * returns. Treat it as a hint to refetch: your server is the truth. */
@@ -55,21 +53,26 @@ export interface OneConnectReturn {
55
53
  message?: string;
56
54
  }
57
55
  /**
58
- * A connector chip on the button. Pass One's connector slug ("stripe",
56
+ * A logo chip on the button. Pass One's connector slug ("stripe",
59
57
  * "google-calendar") and the SDK shows its logo and name; pass an
60
- * object to override either.
58
+ * object to override either. Decoration only: what One asks the user
59
+ * for comes from the app's permission set, not from this list.
61
60
  */
62
- export type ConnectButtonPlatformInput = string | {
61
+ export type ConnectButtonLogoInput = string | {
63
62
  slug?: string;
64
63
  name?: string;
65
64
  imageUrl?: string;
66
65
  };
67
66
  /** A normalized chip: what the button actually draws. */
68
- export interface ConnectButtonPlatform {
67
+ export interface ConnectButtonLogo {
69
68
  slug: string;
70
69
  name: string;
71
70
  imageUrl: string;
72
71
  }
72
+ /** @deprecated Renamed to `ConnectButtonLogoInput`; removed in the next minor. */
73
+ export type ConnectButtonPlatformInput = ConnectButtonLogoInput;
74
+ /** @deprecated Renamed to `ConnectButtonLogo`; removed in the next minor. */
75
+ export type ConnectButtonPlatform = ConnectButtonLogo;
73
76
  export type ConnectButtonVariant = "default" | "accent" | "block";
74
77
  export type ConnectButtonSize = "sm" | "md" | "lg";
75
78
  export type ConnectButtonState = "idle" | "connecting" | "connected";
@@ -78,17 +81,22 @@ export type ConnectButtonState = "idle" | "connecting" | "connected";
78
81
  export interface ConnectButtonProps {
79
82
  /** The app's own backend authorize route; relative is fine. */
80
83
  authorizeUrl: string;
81
- /** Connector slugs, or objects that override the name or the logo.
82
- * The first three draw as logos; the rest fold into a "+N" chip. */
83
- platforms?: ConnectButtonPlatformInput[];
84
+ /** Logos to draw on the button: connector slugs, or objects that
85
+ * override the name or the image. The first three draw; the rest fold
86
+ * into a "+N" chip. Decoration only; the permission set decides what
87
+ * One asks for. */
88
+ logos?: ConnectButtonLogoInput[];
89
+ /** @deprecated Renamed to `logos`; removed in the next minor. */
90
+ platforms?: ConnectButtonLogoInput[];
84
91
  /** Whether this user has a live grant, from your server. When set, it
85
92
  * decides the Connected state. When omitted, the button shows
86
93
  * Connected only right after a successful return. */
87
94
  connected?: boolean;
88
95
  /** Not clickable, for example until terms are accepted. */
89
96
  disabled?: boolean;
90
- /** default = neutral, accent = your brand colour, block = a card with
91
- * a description and a "Secured by One" foot. */
97
+ /** default = neutral, accent = your brand colour (set
98
+ * `--one-connect-accent` and `--one-connect-accent-fg` on the host),
99
+ * block = a card with a description and a "Secured by One" foot. */
92
100
  variant?: ConnectButtonVariant;
93
101
  size?: ConnectButtonSize;
94
102
  /** Stretches to the width of its container. */
@@ -97,11 +105,6 @@ export interface ConnectButtonProps {
97
105
  theme?: ConnectButtonTheme;
98
106
  /** Theme of One's hosted page. */
99
107
  connectTheme?: OneConnectTheme;
100
- /** @deprecated Renamed to `connectTheme`; removed in the next minor. */
101
- appTheme?: OneConnectTheme;
102
- /** Fill of the accent variant; One's lime when omitted. The label is
103
- * black or white, whichever reads better on it. */
104
- accentColor?: string;
105
108
  /** "Connect your apps" unless set. */
106
109
  label?: string;
107
110
  /** "Connected" unless set. */