@doany-ai/sdk 0.2.6 → 0.2.8
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/dist/client.js +73 -1
- package/dist/client.types.d.ts +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/modules/connectors.d.ts +7 -0
- package/dist/modules/connectors.js +71 -8
- package/dist/modules/connectors.types.d.ts +85 -0
- package/dist/modules/payments.d.ts +12 -1
- package/dist/modules/payments.js +40 -0
- package/dist/modules/payments.types.d.ts +93 -8
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createEntitiesModule } from "./modules/entities.js";
|
|
|
3
3
|
import { createIntegrationsModule } from "./modules/integrations.js";
|
|
4
4
|
import { createAuthModule } from "./modules/auth.js";
|
|
5
5
|
import { createSsoModule } from "./modules/sso.js";
|
|
6
|
-
import { createConnectorsModule, createUserConnectorsModule, } from "./modules/connectors.js";
|
|
6
|
+
import { confirmAppUserConnection, createConnectorsModule, createUserConnectorsModule, } from "./modules/connectors.js";
|
|
7
7
|
import { getAccessToken } from "./utils/auth-utils.js";
|
|
8
8
|
import { createFunctionsModule } from "./modules/functions.js";
|
|
9
9
|
import { createPaymentsModule } from "./modules/payments.js";
|
|
@@ -202,6 +202,9 @@ export function createClient(config) {
|
|
|
202
202
|
return {
|
|
203
203
|
getCheckoutSession: full.getCheckoutSession.bind(full),
|
|
204
204
|
getSubscription: full.getSubscription.bind(full),
|
|
205
|
+
// Reading the products is not a decision for any browser, and a
|
|
206
|
+
// fulfilment function often needs what it just sold.
|
|
207
|
+
products: full.products,
|
|
205
208
|
};
|
|
206
209
|
})(),
|
|
207
210
|
functions: createFunctionsModule(serviceRoleFunctionsAxiosClient, appId, {
|
|
@@ -231,6 +234,14 @@ export function createClient(config) {
|
|
|
231
234
|
},
|
|
232
235
|
};
|
|
233
236
|
// If authentication is required, verify token and redirect to login if needed
|
|
237
|
+
// An app user coming back from a connector sign-in carries a one-time code
|
|
238
|
+
// (doany_connector, doany_connector_code). Confirm it with THIS user's
|
|
239
|
+
// session and drop it from the URL — app code never has to do this, and a
|
|
240
|
+
// forwarded link cannot attach a stranger's account because the confirm
|
|
241
|
+
// rides the session, not the code alone.
|
|
242
|
+
if (typeof window !== "undefined" && window.location) {
|
|
243
|
+
void finishPendingConnectorSignIn(axiosClient, appId);
|
|
244
|
+
}
|
|
234
245
|
if (requiresAuth && typeof window !== "undefined") {
|
|
235
246
|
// We perform this check asynchronously to not block client creation
|
|
236
247
|
setTimeout(async () => {
|
|
@@ -376,6 +387,7 @@ export function createClientFromRequest(request) {
|
|
|
376
387
|
const stateHeader = request.headers.get("Doany-State");
|
|
377
388
|
const dataEnvHeader = request.headers.get("X-Data-Env");
|
|
378
389
|
const originHeader = request.headers.get("X-Doany-Origin");
|
|
390
|
+
const runIdHeader = request.headers.get("X-Run-Id");
|
|
379
391
|
if (!appId) {
|
|
380
392
|
throw new Error("Doany-App-Id header is required, but is was not found on the request");
|
|
381
393
|
}
|
|
@@ -440,6 +452,29 @@ export function createClientFromRequest(request) {
|
|
|
440
452
|
// Not a URL. Forwarding it would only make the failure harder to read.
|
|
441
453
|
}
|
|
442
454
|
}
|
|
455
|
+
// The agent run this work belongs to. A backend function called from a
|
|
456
|
+
// run and one an app calls on its own are the same request shape, and this
|
|
457
|
+
// header is the only thing that tells them apart — the platform bills the
|
|
458
|
+
// run for the first and the app for the second. Dropped here, an agent's
|
|
459
|
+
// connector calls were attributed to the app.
|
|
460
|
+
if (runIdHeader) {
|
|
461
|
+
additionalHeaders["X-Run-Id"] = runIdHeader.slice(0, 128);
|
|
462
|
+
}
|
|
463
|
+
// Workflow diagnostics are separate from Annie's X-Run-Id attribution.
|
|
464
|
+
// The function proxy owns these headers; neither their presence nor their
|
|
465
|
+
// contents grant permissions or change billing. Match its bounded format.
|
|
466
|
+
if (serviceRoleToken) {
|
|
467
|
+
for (const name of [
|
|
468
|
+
"X-Doany-Workflow-Id",
|
|
469
|
+
"X-Doany-Workflow-Run-Id",
|
|
470
|
+
"X-Doany-Workflow-Step-Id",
|
|
471
|
+
]) {
|
|
472
|
+
const value = request.headers.get(name);
|
|
473
|
+
if (value && /^[A-Za-z0-9_.:/-]{1,128}$/.test(value)) {
|
|
474
|
+
additionalHeaders[name] = value;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
443
478
|
return createClient({
|
|
444
479
|
serverUrl: serverUrlHeader || "https://api.doany.ai",
|
|
445
480
|
appId,
|
|
@@ -449,3 +484,40 @@ export function createClientFromRequest(request) {
|
|
|
449
484
|
headers: additionalHeaders,
|
|
450
485
|
});
|
|
451
486
|
}
|
|
487
|
+
const CONNECTOR_RETURN_PARAMS = ["doany_connector", "doany_connector_code", "doany_connector_status"];
|
|
488
|
+
async function finishPendingConnectorSignIn(axios, appId) {
|
|
489
|
+
var _a, _b;
|
|
490
|
+
let params;
|
|
491
|
+
try {
|
|
492
|
+
params = new URLSearchParams(window.location.search);
|
|
493
|
+
}
|
|
494
|
+
catch (_c) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const connectorId = params.get("doany_connector");
|
|
498
|
+
const code = params.get("doany_connector_code");
|
|
499
|
+
if (!connectorId || !code)
|
|
500
|
+
return;
|
|
501
|
+
try {
|
|
502
|
+
await confirmAppUserConnection(axios, appId, connectorId, code);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
console.error("Could not finish the connector sign-in:", error);
|
|
506
|
+
// The code is one-time, so it stays in the URL while the failure may
|
|
507
|
+
// pass — no network, a 5xx, or a sign-in the app has not restored yet
|
|
508
|
+
// (401): a reload, or the app's own login, retries the confirmation.
|
|
509
|
+
// A definite rejection (spent, wrong or expired code) is cleared like a
|
|
510
|
+
// success, because retrying cannot help.
|
|
511
|
+
// The client's interceptor turns Axios failures into DoanyError, which
|
|
512
|
+
// carries the HTTP code as `status`; a raw Axios error keeps it under
|
|
513
|
+
// `response.status`.
|
|
514
|
+
const status = (_a = error === null || error === void 0 ? void 0 : error.status) !== null && _a !== void 0 ? _a : (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status;
|
|
515
|
+
const definite = typeof status === "number" && status >= 400 && status < 500 && ![401, 408, 429].includes(status);
|
|
516
|
+
if (!definite)
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
for (const name of CONNECTOR_RETURN_PARAMS)
|
|
520
|
+
params.delete(name);
|
|
521
|
+
const query = params.toString();
|
|
522
|
+
window.history.replaceState({}, document.title, `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`);
|
|
523
|
+
}
|
package/dist/client.types.d.ts
CHANGED
|
@@ -150,10 +150,11 @@ export interface DoanyClient {
|
|
|
150
150
|
* service-role caller, so the ordinary client cannot resolve a mode and
|
|
151
151
|
* the read fails whatever the function forwards.
|
|
152
152
|
*
|
|
153
|
-
* Only the reads are here
|
|
154
|
-
* is
|
|
153
|
+
* Only the reads are here — the two above and the products. There
|
|
154
|
+
* is no service-role checkout: opening one is a decision that belongs to
|
|
155
|
+
* the browser that is actually there.
|
|
155
156
|
*/
|
|
156
|
-
payments: Pick<PaymentsModule, "getCheckoutSession" | "getSubscription">;
|
|
157
|
+
payments: Pick<PaymentsModule, "getCheckoutSession" | "getSubscription" | "products">;
|
|
157
158
|
/** {@link SsoModule | SSO module} for generating SSO tokens.
|
|
158
159
|
* @internal
|
|
159
160
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type { FunctionsModule, FunctionName, FunctionNameRegistry, } from "./mod
|
|
|
11
11
|
export type { AgentsModule, AgentName, AgentNameRegistry, AgentConversation, AgentMessage, AgentMessageReasoning, AgentMessageToolCall, AgentMessageUsage, AgentMessageCustomContext, AgentMessageMetadata, CreateConversationParams, } from "./modules/agents.types.js";
|
|
12
12
|
export type { AiGatewayModule, AiGatewayConnection, } from "./modules/ai-gateway.types.js";
|
|
13
13
|
export type { AppLogsModule } from "./modules/app-logs.types.js";
|
|
14
|
-
export type { PaymentsModule, CheckoutLineItem, CreateCheckoutParams, CreateCheckoutResult, CreateEmbeddedCheckoutResult, CheckoutSession, SubscriptionState, BillingPortalParams, } from "./modules/payments.types.js";
|
|
14
|
+
export type { PaymentsModule, ProductsModule, Product, ProductQuery, ProductSort, CheckoutLineItem, CreateCheckoutParams, CreateCheckoutResult, CreateEmbeddedCheckoutResult, CheckoutSession, SubscriptionState, BillingPortalParams, } from "./modules/payments.types.js";
|
|
15
15
|
export type { SsoModule, SsoAccessTokenResponse } from "./modules/sso.types.js";
|
|
16
|
-
export type { ConnectorsModule, UserConnectorsModule, } from "./modules/connectors.types.js";
|
|
16
|
+
export type { ConnectorsModule, UserConnectorsModule, ConnectorApiRequest, ConnectorApiResponse, ConnectorProxyRawResponse, } from "./modules/connectors.types.js";
|
|
17
17
|
export type { CustomIntegrationsModule, CustomIntegrationCallParams, CustomIntegrationCallResponse, } from "./modules/custom-integrations.types.js";
|
|
18
18
|
export type { GetAccessTokenOptions, SaveAccessTokenOptions, RemoveAccessTokenOptions, GetLoginUrlOptions, } from "./utils/auth-utils.types.js";
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
2
|
import { ConnectorsModule, UserConnectorsModule } from "./connectors.types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Finishes an app-user connection with the one-time code the platform put in
|
|
5
|
+
* the return URL. Called by the client on page load, never by app code: the
|
|
6
|
+
* confirm must ride the same user's session that started the sign-in.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function confirmAppUserConnection(axios: AxiosInstance, appId: string, connectorId: string, code: string): Promise<void>;
|
|
3
10
|
/**
|
|
4
11
|
* Creates the Connectors module for the Doany SDK.
|
|
5
12
|
*
|
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
const CONNECTOR_API_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
2
|
+
function assertNonEmptyString(value, label) {
|
|
3
|
+
if (!value || typeof value !== "string") {
|
|
4
|
+
throw new Error(`${label} is required and must be a string`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* One proxied call, exactly as @base44/sdk does it: validate the request,
|
|
9
|
+
* post the raw shape, map the raw (snake_case) answer to the typed one.
|
|
10
|
+
*/
|
|
11
|
+
async function proxyCall(axios, url, request) {
|
|
12
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
13
|
+
if (!request || typeof request !== "object") {
|
|
14
|
+
throw new Error("Request is required and must be an object");
|
|
15
|
+
}
|
|
16
|
+
assertNonEmptyString(request.path, "Request path");
|
|
17
|
+
const method = (_a = request.method) !== null && _a !== void 0 ? _a : "GET";
|
|
18
|
+
if (!CONNECTOR_API_METHODS.has(method)) {
|
|
19
|
+
throw new Error("Request method must be one of GET, POST, PUT, PATCH, DELETE, or HEAD");
|
|
20
|
+
}
|
|
21
|
+
const response = await axios.post(url, {
|
|
22
|
+
method,
|
|
23
|
+
// Omitted when unset so the proxy applies the connector's default host.
|
|
24
|
+
...(request.host == null ? {} : { host: request.host }),
|
|
25
|
+
path: request.path,
|
|
26
|
+
query: (_b = request.query) !== null && _b !== void 0 ? _b : {},
|
|
27
|
+
headers: (_c = request.headers) !== null && _c !== void 0 ? _c : {},
|
|
28
|
+
body: (_d = request.body) !== null && _d !== void 0 ? _d : null,
|
|
29
|
+
});
|
|
30
|
+
const data = response;
|
|
31
|
+
return {
|
|
32
|
+
success: data.success,
|
|
33
|
+
phase: data.phase,
|
|
34
|
+
status: (_e = data.status_code) !== null && _e !== void 0 ? _e : null,
|
|
35
|
+
data: data.data,
|
|
36
|
+
dataBase64: (_f = data.data_base64) !== null && _f !== void 0 ? _f : null,
|
|
37
|
+
contentType: (_g = data.content_type) !== null && _g !== void 0 ? _g : null,
|
|
38
|
+
headers: (_h = data.headers) !== null && _h !== void 0 ? _h : {},
|
|
39
|
+
creditsCharged: (_j = data.credits_charged) !== null && _j !== void 0 ? _j : 0,
|
|
40
|
+
// A 200 envelope can still carry a failure (`sent_unconfirmed`, or the
|
|
41
|
+
// provider's own error); without these the caller knows a write may
|
|
42
|
+
// have happened but not why the answer is missing.
|
|
43
|
+
...(data.error == null ? {} : { error: data.error }),
|
|
44
|
+
...(data.error_message == null ? {} : { errorMessage: data.error_message }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Finishes an app-user connection with the one-time code the platform put in
|
|
49
|
+
* the return URL. Called by the client on page load, never by app code: the
|
|
50
|
+
* confirm must ride the same user's session that started the sign-in.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export async function confirmAppUserConnection(axios, appId, connectorId, code) {
|
|
54
|
+
assertNonEmptyString(connectorId, "Connector ID");
|
|
55
|
+
assertNonEmptyString(code, "Confirmation code");
|
|
56
|
+
await axios.post(`/apps/${appId}/app-user-auth/connectors/${encodeURIComponent(connectorId)}/confirm`, { code });
|
|
57
|
+
}
|
|
1
58
|
/**
|
|
2
59
|
* Creates the Connectors module for the Doany SDK.
|
|
3
60
|
*
|
|
@@ -8,6 +65,16 @@
|
|
|
8
65
|
*/
|
|
9
66
|
export function createConnectorsModule(axios, appId) {
|
|
10
67
|
return {
|
|
68
|
+
async callApi(integrationType, request) {
|
|
69
|
+
assertNonEmptyString(integrationType, "Integration type");
|
|
70
|
+
// Encoded so a runtime-built identifier can only ever select a
|
|
71
|
+
// connector, never re-target another route under this token.
|
|
72
|
+
return proxyCall(axios, `/apps/${appId}/connectors/${encodeURIComponent(integrationType)}/call`, request);
|
|
73
|
+
},
|
|
74
|
+
async callCurrentAppUserApi(integrationType, request) {
|
|
75
|
+
assertNonEmptyString(integrationType, "Integration type");
|
|
76
|
+
return proxyCall(axios, `/apps/${appId}/app-user-connectors/${encodeURIComponent(integrationType)}/call`, request);
|
|
77
|
+
},
|
|
11
78
|
/**
|
|
12
79
|
* Retrieve an OAuth access token for a specific external integration type.
|
|
13
80
|
* @deprecated Use getConnection(integrationType) and use the returned accessToken (and connectionConfig when needed) instead.
|
|
@@ -81,18 +148,14 @@ export function createConnectorsModule(axios, appId) {
|
|
|
81
148
|
export function createUserConnectorsModule(axios, appId) {
|
|
82
149
|
return {
|
|
83
150
|
async connectAppUser(connectorId) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
const response = await axios.post(`/apps/${appId}/app-user-auth/connectors/${connectorId}/initiate`);
|
|
151
|
+
assertNonEmptyString(connectorId, "Connector ID");
|
|
152
|
+
const response = await axios.post(`/apps/${appId}/app-user-auth/connectors/${encodeURIComponent(connectorId)}/initiate`);
|
|
88
153
|
const data = response;
|
|
89
154
|
return data.redirect_url;
|
|
90
155
|
},
|
|
91
156
|
async disconnectAppUser(connectorId) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
await axios.delete(`/apps/${appId}/app-user-auth/connectors/${connectorId}`);
|
|
157
|
+
assertNonEmptyString(connectorId, "Connector ID");
|
|
158
|
+
await axios.delete(`/apps/${appId}/app-user-auth/connectors/${encodeURIComponent(connectorId)}`);
|
|
96
159
|
},
|
|
97
160
|
};
|
|
98
161
|
}
|
|
@@ -41,6 +41,65 @@ export interface AppUserConnectorConnectionResponse {
|
|
|
41
41
|
/** Key-value configuration for the connection, or `null` if the connector does not provide one. */
|
|
42
42
|
connectionConfig: Record<string, string> | null;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A request proxied to the external service on the app's behalf. The
|
|
46
|
+
* platform injects the credential; the app never sees a token.
|
|
47
|
+
*/
|
|
48
|
+
export interface ConnectorApiRequest {
|
|
49
|
+
/** HTTP method. Defaults to `GET`. */
|
|
50
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
51
|
+
/** Path relative to the provider's API root. Must start with `/`. */
|
|
52
|
+
path: string;
|
|
53
|
+
/** Provider host to call; defaults to the connector's primary API host. Must be one of the connector's allowed hosts. */
|
|
54
|
+
host?: string;
|
|
55
|
+
/** Query parameters. */
|
|
56
|
+
query?: Record<string, string>;
|
|
57
|
+
/** Extra request headers. `Authorization` is ignored — the platform sets it. */
|
|
58
|
+
headers?: Record<string, string>;
|
|
59
|
+
/** JSON body. */
|
|
60
|
+
body?: unknown;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* What a proxied call produced. HTTP 200 from the platform means only that
|
|
64
|
+
* the proxy worked; read `success`, `phase` and `status` for what the
|
|
65
|
+
* provider did.
|
|
66
|
+
*/
|
|
67
|
+
export interface ConnectorApiResponse<T = unknown> {
|
|
68
|
+
/** The provider answered 2xx. */
|
|
69
|
+
success: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* `not_sent` — the request never left the platform (safe to retry).
|
|
72
|
+
* `responded` — the provider answered; see `status`.
|
|
73
|
+
* `sent_unconfirmed` — sent, but no answer came back; a write may have happened. Never auto-retry a write on this.
|
|
74
|
+
*/
|
|
75
|
+
phase: "not_sent" | "responded" | "sent_unconfirmed";
|
|
76
|
+
/** The provider's HTTP status, or `null` when there was no response. */
|
|
77
|
+
status: number | null;
|
|
78
|
+
/** The provider's JSON body, when it sent one. */
|
|
79
|
+
data: T | null;
|
|
80
|
+
/** The provider's body as base64 when it was binary. */
|
|
81
|
+
dataBase64: string | null;
|
|
82
|
+
contentType: string | null;
|
|
83
|
+
headers: Record<string, string>;
|
|
84
|
+
/** Credits charged for this call. */
|
|
85
|
+
creditsCharged: number;
|
|
86
|
+
/** Set when `phase` is not `responded`. */
|
|
87
|
+
error?: string;
|
|
88
|
+
errorMessage?: string;
|
|
89
|
+
}
|
|
90
|
+
/** The proxy's raw answer on the wire (snake_case); {@link ConnectorApiResponse} is its typed form. */
|
|
91
|
+
export interface ConnectorProxyRawResponse<T = unknown> {
|
|
92
|
+
success: boolean;
|
|
93
|
+
phase: "not_sent" | "responded" | "sent_unconfirmed";
|
|
94
|
+
status_code?: number | null;
|
|
95
|
+
data: T | null;
|
|
96
|
+
data_base64?: string | null;
|
|
97
|
+
content_type?: string | null;
|
|
98
|
+
headers?: Record<string, string>;
|
|
99
|
+
credits_charged?: number;
|
|
100
|
+
error?: string;
|
|
101
|
+
error_message?: string;
|
|
102
|
+
}
|
|
44
103
|
/**
|
|
45
104
|
* Connectors module for managing OAuth tokens for external services.
|
|
46
105
|
*
|
|
@@ -131,6 +190,32 @@ export interface AppUserConnectorConnectionResponse {
|
|
|
131
190
|
* If you're working in a TypeScript project, you can generate types from your app's connector configurations to get autocomplete on integration type names when calling {@link getConnection}. See the [Dynamic Types](/developers/references/sdk/getting-started/dynamic-types) guide to get started.
|
|
132
191
|
*/
|
|
133
192
|
export interface ConnectorsModule {
|
|
193
|
+
/**
|
|
194
|
+
* Calls the external service through the platform with the app's shared connection.
|
|
195
|
+
*
|
|
196
|
+
* This is the only way a backend function reaches a connector on doany: the platform injects the credential and forwards the request. The token methods below answer 403.
|
|
197
|
+
*
|
|
198
|
+
* @param integrationType - The connector's integration type, e.g. `'gmail'`.
|
|
199
|
+
* @param request - Method, path (relative to the provider's API root), query, headers and body.
|
|
200
|
+
* @returns The provider's answer wrapped in `success` / `phase` / `status`.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const res = await doany.asServiceRole.connectors.callApi('gmail', {
|
|
205
|
+
* method: 'POST',
|
|
206
|
+
* path: '/gmail/v1/users/me/messages/send',
|
|
207
|
+
* body: { raw },
|
|
208
|
+
* });
|
|
209
|
+
* if (!res.success) { /* res.status is the provider's code, res.data its body *\/ }
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
callApi<T = unknown>(integrationType: ConnectorIntegrationType, request: ConnectorApiRequest): Promise<ConnectorApiResponse<T>>;
|
|
213
|
+
/**
|
|
214
|
+
* Calls the external service as the signed-in app user, through their own connection.
|
|
215
|
+
*
|
|
216
|
+
* Only works from a backend function the user triggered (the platform needs their identity). Same envelope as {@linkcode callApi}.
|
|
217
|
+
*/
|
|
218
|
+
callCurrentAppUserApi<T = unknown>(integrationType: ConnectorIntegrationType, request: ConnectorApiRequest): Promise<ConnectorApiResponse<T>>;
|
|
134
219
|
/**
|
|
135
220
|
* Retrieves an OAuth access token for a specific [external integration type](#available-connectors).
|
|
136
221
|
*
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { AxiosInstance } from "axios";
|
|
2
|
-
import { PaymentsModule } from "./payments.types";
|
|
2
|
+
import { PaymentsModule, ProductsModule } from "./payments.types";
|
|
3
|
+
/**
|
|
4
|
+
* The product reads.
|
|
5
|
+
*
|
|
6
|
+
* Parameters go over the wire exactly as an entity's do (`sort`, `limit`,
|
|
7
|
+
* `skip`, `q` as JSON), because the point of this module is that code written
|
|
8
|
+
* against `entities.Product` moves over by renaming it — same arguments, same
|
|
9
|
+
* records back.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function createProductsModule(axios: AxiosInstance, appId: string): ProductsModule;
|
|
3
14
|
/**
|
|
4
15
|
* Creates the payments module for the Doany SDK.
|
|
5
16
|
*
|
package/dist/modules/payments.js
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The product reads.
|
|
3
|
+
*
|
|
4
|
+
* Parameters go over the wire exactly as an entity's do (`sort`, `limit`,
|
|
5
|
+
* `skip`, `q` as JSON), because the point of this module is that code written
|
|
6
|
+
* against `entities.Product` moves over by renaming it — same arguments, same
|
|
7
|
+
* records back.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function createProductsModule(axios, appId) {
|
|
12
|
+
const baseURL = `/apps/${appId}/payments/products`;
|
|
13
|
+
function params(sort, limit, skip) {
|
|
14
|
+
const out = {};
|
|
15
|
+
if (sort)
|
|
16
|
+
out.sort = sort;
|
|
17
|
+
if (limit)
|
|
18
|
+
out.limit = limit;
|
|
19
|
+
if (skip)
|
|
20
|
+
out.skip = skip;
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
async list(sort, limit, skip) {
|
|
25
|
+
const data = await axios.get(baseURL, { params: params(sort, limit, skip) });
|
|
26
|
+
return data;
|
|
27
|
+
},
|
|
28
|
+
async filter(query, sort, limit, skip) {
|
|
29
|
+
const data = await axios.get(baseURL, {
|
|
30
|
+
params: { q: JSON.stringify(query), ...params(sort, limit, skip) },
|
|
31
|
+
});
|
|
32
|
+
return data;
|
|
33
|
+
},
|
|
34
|
+
async get(id) {
|
|
35
|
+
const data = await axios.get(`${baseURL}/${encodeURIComponent(id)}`);
|
|
36
|
+
return data;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
1
40
|
/**
|
|
2
41
|
* Creates the payments module for the Doany SDK.
|
|
3
42
|
*
|
|
@@ -21,6 +60,7 @@ export function createPaymentsModule(axios, appId) {
|
|
|
21
60
|
// Axios's declared return type does not reflect that, so the results below
|
|
22
61
|
// are cast through `unknown`.
|
|
23
62
|
return {
|
|
63
|
+
products: createProductsModule(axios, appId),
|
|
24
64
|
createCheckoutSession,
|
|
25
65
|
async getSubscription(subscriptionId) {
|
|
26
66
|
const data = await axios.request({
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One thing being sold in a checkout.
|
|
3
3
|
*
|
|
4
|
-
* A line item names a record in your own app data — doany keeps your
|
|
4
|
+
* A line item names a record in your own app data — doany keeps your products
|
|
5
5
|
* there rather than as objects inside Stripe, which is why switching from test
|
|
6
6
|
* to real payments needs no migration.
|
|
7
7
|
*/
|
|
8
8
|
export type CheckoutLineItem = {
|
|
9
9
|
/**
|
|
10
|
-
* The `id` of
|
|
10
|
+
* The `id` of one of this app's products — a record from
|
|
11
|
+
* {@linkcode ProductsModule | doany.payments.products}, or from the `Product`
|
|
12
|
+
* entity on an app whose products predate it.
|
|
11
13
|
*
|
|
12
14
|
* The price, name and currency all come from that record — you cannot pass
|
|
13
15
|
* a price. This endpoint is reachable by anyone (a shop has to sell to
|
|
@@ -65,7 +67,7 @@ export type CreateCheckoutResult = {
|
|
|
65
67
|
/** `test` in the preview, `live` on a published site that has gone live. */
|
|
66
68
|
mode: "test" | "live";
|
|
67
69
|
/**
|
|
68
|
-
* What the customer is about to agree to, decided by the
|
|
70
|
+
* What the customer is about to agree to, decided by the product.
|
|
69
71
|
*
|
|
70
72
|
* `subscription` when the products carry a `recurring_interval`, `payment`
|
|
71
73
|
* otherwise. Say the right word on the button: "Subscribe" over a one-off
|
|
@@ -203,17 +205,98 @@ export type CheckoutSession = {
|
|
|
203
205
|
*/
|
|
204
206
|
access_token?: string;
|
|
205
207
|
};
|
|
208
|
+
/**
|
|
209
|
+
* One of this app's products, as {@linkcode ProductsModule} returns it.
|
|
210
|
+
*
|
|
211
|
+
* The same shape an entity record has: the platform's fields and the system
|
|
212
|
+
* fields by their entity names, and every field of the app's own (`slug`,
|
|
213
|
+
* `category`, `image_url`, …) at the top level beside them.
|
|
214
|
+
*/
|
|
215
|
+
export type Product = {
|
|
216
|
+
id: string;
|
|
217
|
+
name: string;
|
|
218
|
+
description: string | null;
|
|
219
|
+
/** Minor units — 2400 is 24.00. `null` for something shown but not sold. */
|
|
220
|
+
price_cents: number | null;
|
|
221
|
+
/** Lowercase ISO code, or `null` for the checkout's default. */
|
|
222
|
+
currency: string | null;
|
|
223
|
+
/** Present => a subscription billed this often. `null` => a one-off. */
|
|
224
|
+
recurring_interval: "week" | "month" | "year" | null;
|
|
225
|
+
/** `false` is off sale: still readable, cannot be bought. */
|
|
226
|
+
is_available: boolean;
|
|
227
|
+
created_date: string;
|
|
228
|
+
updated_date: string;
|
|
229
|
+
created_by: string | null;
|
|
230
|
+
created_by_id: string | null;
|
|
231
|
+
is_sample: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Only ever on {@linkcode ProductsModule.get}: the product was removed from
|
|
234
|
+
* the store. Kept readable because past orders and subscriptions name it.
|
|
235
|
+
*/
|
|
236
|
+
is_deleted?: true;
|
|
237
|
+
/**
|
|
238
|
+
* The app's own fields. `any`, as on an entity record, so code that read
|
|
239
|
+
* `product.slug.toLowerCase()` off `entities.Product` still type-checks.
|
|
240
|
+
*/
|
|
241
|
+
[field: string]: any;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* A field to order by, `-` first for descending: `"price_cents"`,
|
|
245
|
+
* `"-created_date"`, or one of the app's own fields.
|
|
246
|
+
*/
|
|
247
|
+
export type ProductSort = string;
|
|
248
|
+
/**
|
|
249
|
+
* Equality only — `{ slug: "starter" }`, `{ is_available: true }`. Several
|
|
250
|
+
* fields must all match. `{ field: null }` also matches a product that does not
|
|
251
|
+
* have the field. Operators (`$gt`, `$in`, …) are refused.
|
|
252
|
+
*/
|
|
253
|
+
export type ProductQuery = Record<string, string | number | boolean | null>;
|
|
254
|
+
/**
|
|
255
|
+
* Read this app's products. Read-only: products are added and priced by
|
|
256
|
+
* the app owner in the Payments panel, or by asking Annie.
|
|
257
|
+
*
|
|
258
|
+
* Same methods, arguments and results as an entity's reads, so code written
|
|
259
|
+
* against `entities.Product` moves over by renaming it. Records come back in
|
|
260
|
+
* the order they were created when no `sort` is given.
|
|
261
|
+
*/
|
|
262
|
+
export interface ProductsModule {
|
|
263
|
+
/**
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* const products = await doany.payments.products.list();
|
|
267
|
+
* const newest = await doany.payments.products.list('-created_date', 20);
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
list(sort?: ProductSort, limit?: number, skip?: number): Promise<Product[]>;
|
|
271
|
+
/**
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* const [plan] = await doany.payments.products.filter({ slug: 'pro' });
|
|
275
|
+
* const onSale = await doany.payments.products.filter(
|
|
276
|
+
* { is_available: true }, 'price_cents', 10,
|
|
277
|
+
* );
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
filter(query: ProductQuery, sort?: ProductSort, limit?: number, skip?: number): Promise<Product[]>;
|
|
281
|
+
/**
|
|
282
|
+
* One product by id. A removed product is still returned, with
|
|
283
|
+
* `is_deleted: true`; an id that was never a product is a 404.
|
|
284
|
+
*/
|
|
285
|
+
get(id: string): Promise<Product>;
|
|
286
|
+
}
|
|
206
287
|
/**
|
|
207
288
|
* Take card payments on your site.
|
|
208
289
|
*
|
|
209
290
|
* Money goes to the app owner's own Stripe account — doany never holds it and
|
|
210
291
|
* takes no cut. Stripe's usual per-transaction fee applies.
|
|
211
292
|
*
|
|
212
|
-
* ## Prices live
|
|
293
|
+
* ## Prices live with the product, not in your code
|
|
213
294
|
*
|
|
214
|
-
* Sellable things are
|
|
215
|
-
*
|
|
216
|
-
* looks up what
|
|
295
|
+
* Sellable things are this app's products, read with
|
|
296
|
+
* {@linkcode PaymentsModule.products}, with the price in an integer
|
|
297
|
+
* `price_cents` field. A checkout names the product; the server looks up what
|
|
298
|
+
* it costs. (An app whose products predate this keeps them in a `Product`
|
|
299
|
+
* entity; checkout reads whichever one the app uses.)
|
|
217
300
|
*
|
|
218
301
|
* ## Test and real payments
|
|
219
302
|
*
|
|
@@ -229,12 +312,14 @@ export type CheckoutSession = {
|
|
|
229
312
|
* The same code covers both. There is no key to configure and no mode to set.
|
|
230
313
|
*/
|
|
231
314
|
export interface PaymentsModule {
|
|
315
|
+
/** This app's products. See {@linkcode ProductsModule}. */
|
|
316
|
+
products: ProductsModule;
|
|
232
317
|
/**
|
|
233
318
|
* Opens a Stripe checkout and returns the URL to send the customer to.
|
|
234
319
|
*
|
|
235
320
|
* @example Sell one item
|
|
236
321
|
* ```typescript
|
|
237
|
-
* // The price comes from the
|
|
322
|
+
* // The price comes from the product, not from this call.
|
|
238
323
|
* const { url } = await doany.payments.createCheckoutSession({
|
|
239
324
|
* line_items: [{ product_id: product.id, quantity: 1 }],
|
|
240
325
|
* success_path: '/thanks',
|