@assembly-js/app-foundation 0.0.0 → 0.0.1-rc.1
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/CLAUDE.md +1119 -0
- package/dist/api-errors.d.ts +5 -0
- package/dist/api-errors.js +33 -0
- package/dist/app-storage.d.ts +7 -0
- package/dist/app-storage.js +22 -0
- package/dist/assembly.d.ts +49 -0
- package/dist/assembly.js +235 -0
- package/dist/auth.d.ts +17 -0
- package/dist/auth.js +118 -0
- package/dist/client-companies.d.ts +16 -0
- package/dist/client-companies.js +23 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +14 -3
- package/dist/labels.d.ts +9 -0
- package/dist/labels.js +17 -0
- package/dist/mint-bearer.d.ts +18 -0
- package/dist/mint-bearer.js +152 -0
- package/dist/nango.d.ts +174 -0
- package/dist/nango.js +430 -0
- package/dist/network-capture.d.ts +36 -0
- package/dist/network-capture.js +473 -0
- package/dist/params.d.ts +3 -0
- package/dist/params.js +20 -0
- package/dist/resource-resolution.d.ts +89 -0
- package/dist/resource-resolution.js +142 -0
- package/dist/server.d.ts +9 -2
- package/dist/server.js +15 -5
- package/dist/session-exchange.d.ts +20 -0
- package/dist/session-exchange.js +101 -0
- package/dist/theme.d.ts +5 -0
- package/dist/theme.js +20 -0
- package/package.json +9 -3
- package/skills/assembly-ai/SKILL.md +133 -0
- package/skills/assembly-app-bridge-header/SKILL.md +88 -0
- package/skills/assembly-app-bridge-toast/SKILL.md +79 -0
- package/skills/assembly-avatar-sub/SKILL.md +103 -0
- package/skills/assembly-client-users/SKILL.md +45 -0
- package/skills/assembly-clients-companies/SKILL.md +143 -0
- package/skills/assembly-companies/SKILL.md +45 -0
- package/skills/assembly-custom-fields/SKILL.md +86 -0
- package/skills/assembly-db-transactions/SKILL.md +79 -0
- package/skills/assembly-dev-server-logs/SKILL.md +26 -0
- package/skills/assembly-forms/SKILL.md +103 -0
- package/skills/assembly-internal-users/SKILL.md +69 -0
- package/skills/assembly-mutations/SKILL.md +58 -0
- package/skills/assembly-native-resources/SKILL.md +117 -0
- package/skills/assembly-route-auth/SKILL.md +223 -0
- package/skills/assembly-route-errors/SKILL.md +81 -0
- package/skills/assembly-semantic-colors/SKILL.md +76 -0
- package/skills/assembly-session-token/SKILL.md +153 -0
- package/skills/assembly-storage/SKILL.md +157 -0
- package/skills/assembly-user-company-selector/SKILL.md +98 -0
- package/skills/assembly-workspace-labels/SKILL.md +50 -0
- package/skills/clearing-client-notifications/SKILL.md +100 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { assemblyApi, OpenAPI } from '@assembly-js/node-sdk';
|
|
2
|
+
import { BEARER_TTL_MS, classifyExchangeFailure, } from './session-exchange';
|
|
3
|
+
// Trusted API origin for this stack (build-env supplied); empty in prod where
|
|
4
|
+
// the SDK default is correct.
|
|
5
|
+
const mintBaseUrl = process.env.ASSEMBLY_API_BASE_URL || undefined;
|
|
6
|
+
// Loose bound for the destructive /session exchange. Aborting it does NOT undo
|
|
7
|
+
// the server-side mint — the single-use session token is consumed either way —
|
|
8
|
+
// so an early abort strands the caller with no bearer AND a spent token. Wait
|
|
9
|
+
// out a cold platform-api lambda rather than risk that.
|
|
10
|
+
const EXCHANGE_TIMEOUT_MS = 15_000;
|
|
11
|
+
// Trades a single-use session token for a short-lived, user-scoped bearer by
|
|
12
|
+
// calling platform-api POST /v1/session. This is the ONE sanctioned use of the
|
|
13
|
+
// app API key as a request credential — it mints the bearer; every subsequent
|
|
14
|
+
// data call uses the bearer alone.
|
|
15
|
+
//
|
|
16
|
+
// Shared by the proxy (first-load exchange) and the /api/assembly-reexchange
|
|
17
|
+
// route (silent renewal on bearer expiry). Deliberately uses only assemblyApi +
|
|
18
|
+
// fetch so it is safe in BOTH the Edge middleware runtime and the Node route-
|
|
19
|
+
// handler runtime.
|
|
20
|
+
//
|
|
21
|
+
// On failure returns ok:false with a reason (see MintFailure) so the caller can
|
|
22
|
+
// tell a spent token from an exchange it simply could not complete.
|
|
23
|
+
export async function mintBearerFromSessionToken(token) {
|
|
24
|
+
let assembly;
|
|
25
|
+
try {
|
|
26
|
+
assembly = await assemblyApi({
|
|
27
|
+
apiKey: process.env.ASSEMBLY_API_KEY ?? '',
|
|
28
|
+
token,
|
|
29
|
+
baseUrl: mintBaseUrl,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { ok: false, reason: 'rejected' };
|
|
34
|
+
}
|
|
35
|
+
// An exchange-capable app now receives a slim token carrying only workspaceId
|
|
36
|
+
// + tokenId (see marketplace_application CreateAppToken), so identity
|
|
37
|
+
// (clientId/internalUserId/companyId) is NOT read from the decrypted payload —
|
|
38
|
+
// it is sourced below from the /session response, the authoritative
|
|
39
|
+
// server-side record that backs both slim and legacy full tokens.
|
|
40
|
+
let payload = null;
|
|
41
|
+
if (typeof assembly.getTokenPayload === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
payload = await assembly.getTokenPayload();
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
// Undecodable, or no tokenId → not exchange-capable. Fail closed: a bearer-
|
|
48
|
+
// capable app never authenticates from a raw token.
|
|
49
|
+
if (!payload?.workspaceId || !payload.tokenId) {
|
|
50
|
+
return { ok: false, reason: 'rejected' };
|
|
51
|
+
}
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeoutId = setTimeout(() => controller.abort(), EXCHANGE_TIMEOUT_MS);
|
|
55
|
+
try {
|
|
56
|
+
const resp = await assembly.fetch('/v1/session', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({ sessionId: payload.tokenId }),
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!resp.ok) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: classifyExchangeFailure(resp.status),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// session.* mirrors platform-api's ExchangeResponse.Session — the
|
|
69
|
+
// authoritative server-side record backing the (possibly slim) token, so
|
|
70
|
+
// identity is sourced from it rather than the decrypted token payload.
|
|
71
|
+
const json = await resp.json();
|
|
72
|
+
const bearerExpiresAt = Math.min(json.tokenExpiresAt * 1_000, now + BEARER_TTL_MS);
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
bearer: json.token,
|
|
76
|
+
bearerExpiresAt,
|
|
77
|
+
session: {
|
|
78
|
+
workspaceId: json.session?.portalId || payload.workspaceId,
|
|
79
|
+
clientId: json.session?.clientUserId || undefined,
|
|
80
|
+
internalUserId: json.session?.internalUserId || undefined,
|
|
81
|
+
companyId: json.session?.companyId || undefined,
|
|
82
|
+
notificationId: json.session?.notificationId || undefined,
|
|
83
|
+
instanceId: json.session?.instanceId || undefined,
|
|
84
|
+
sessionId: payload.tokenId,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Network failure, or the abort above fired. Either way the exchange did not
|
|
90
|
+
// complete, so we cannot claim the token was spent.
|
|
91
|
+
return { ok: false, reason: 'unavailable' };
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Mints a short-lived, APP-scoped bearer from the app API key ALONE by calling
|
|
98
|
+
// platform-api POST /v1/token (client-credentials). There is NO session token
|
|
99
|
+
// and NO user in the loop: this is the OFFLINE/background path (cron, webhooks)
|
|
100
|
+
// where no iframe ever handed us a session token to exchange. The app key mints
|
|
101
|
+
// the bearer once; every subsequent data call then uses that bearer alone.
|
|
102
|
+
//
|
|
103
|
+
// Uses a runtime-agnostic fetch (safe in BOTH the Edge middleware and Node route-
|
|
104
|
+
// handler runtimes) with the same abort-based timeout as the session exchange,
|
|
105
|
+
// and fails closed (null) on any error — including a malformed 2xx — so a caller
|
|
106
|
+
// never proceeds with a missing or bad credential.
|
|
107
|
+
export async function mintBearerFromAppKey() {
|
|
108
|
+
const apiKey = process.env.ASSEMBLY_API_KEY;
|
|
109
|
+
if (!apiKey)
|
|
110
|
+
return null;
|
|
111
|
+
// Client-credentials mint: POST /v1/token authenticated by the app key sent as
|
|
112
|
+
// X-API-Key. Unlike mintBearerFromSessionToken (which decrypts a session token
|
|
113
|
+
// and lets the SDK derive the header), there is no token here, and the SDK only
|
|
114
|
+
// turns a bare apiKey into an X-API-Key in local/staging env modes — so in prod
|
|
115
|
+
// `assemblyApi({ apiKey })` would fail closed before ever reaching /v1/token.
|
|
116
|
+
// Send the credential directly against the trusted build-env base (or the SDK
|
|
117
|
+
// default in prod, where it is already correct).
|
|
118
|
+
const base = (mintBaseUrl ?? OpenAPI.BASE).replace(/\/$/, '');
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeoutId = setTimeout(() => controller.abort(), EXCHANGE_TIMEOUT_MS);
|
|
122
|
+
try {
|
|
123
|
+
const resp = await fetch(`${base}/v1/token`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'X-API-Key': apiKey },
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
if (!resp.ok)
|
|
129
|
+
return null;
|
|
130
|
+
const json = (await resp.json());
|
|
131
|
+
// Fail closed on a malformed or already-expired 2xx: a missing/mistyped token
|
|
132
|
+
// would yield a client that sends `Authorization: Bearer undefined`, a
|
|
133
|
+
// missing/NaN tokenExpiresAt would produce `bearerExpiresAt: NaN`, and an
|
|
134
|
+
// expiry at/before now would return a bearer whose first data call fails with
|
|
135
|
+
// auth errors instead of failing closed here at mint time. None is usable.
|
|
136
|
+
if (typeof json.token !== 'string' ||
|
|
137
|
+
json.token.length === 0 ||
|
|
138
|
+
typeof json.tokenExpiresAt !== 'number' ||
|
|
139
|
+
!Number.isFinite(json.tokenExpiresAt) ||
|
|
140
|
+
json.tokenExpiresAt * 1_000 <= now) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const bearerExpiresAt = Math.min(json.tokenExpiresAt * 1_000, now + BEARER_TTL_MS);
|
|
144
|
+
return { bearer: json.token, bearerExpiresAt };
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
}
|
|
152
|
+
}
|
package/dist/nango.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { type ResourceFieldMap, type ResourcePage } from './resource-resolution';
|
|
2
|
+
export type Provider = 'slack' | 'gmail' | 'google-calendar' | 'google-drive' | 'google-sheet' | 'hubspot' | 'salesforce' | 'notion' | 'linear' | 'stripe' | 'github';
|
|
3
|
+
export type ConnectorErrorCode = 'app_key_required' | 'provider_not_declared' | 'connection_required' | 'needs_reauth' | 'needs_reauth_scope' | 'connection_throttled' | 'connector_unavailable' | 'action_failed' | 'invalid_request';
|
|
4
|
+
export interface ConnectionStatus {
|
|
5
|
+
provider: string;
|
|
6
|
+
connected: boolean;
|
|
7
|
+
status: 'connected' | 'connection_required' | 'needs_reauth' | 'needs_reauth_scope' | string;
|
|
8
|
+
}
|
|
9
|
+
export declare class ConnectorError extends Error {
|
|
10
|
+
readonly code: ConnectorErrorCode | string;
|
|
11
|
+
readonly provider: string;
|
|
12
|
+
readonly status: number;
|
|
13
|
+
/**
|
|
14
|
+
* For `action_failed`: what the action script threw — usually the
|
|
15
|
+
* provider-level failure, e.g. `{ type: 'slack_error', message, response }`
|
|
16
|
+
* where `response.error` is Slack's error code.
|
|
17
|
+
*/
|
|
18
|
+
readonly detail?: unknown;
|
|
19
|
+
constructor(args: {
|
|
20
|
+
code: ConnectorErrorCode | string;
|
|
21
|
+
provider: string;
|
|
22
|
+
status: number;
|
|
23
|
+
detail?: unknown;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export interface ProxyOpts {
|
|
27
|
+
endpoint: string;
|
|
28
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | string;
|
|
29
|
+
data?: unknown;
|
|
30
|
+
params?: Record<string, string | number | boolean | undefined>;
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns a thin, server-side-only client for a connected provider. Every method
|
|
35
|
+
* POSTs to the Assembly connector broker (`/v1/connectors/:provider/...`) via
|
|
36
|
+
* getAssembly().fetch — there is no Nango client and no provider credentials in
|
|
37
|
+
* the app. The broker resolves the workspace's connection server-side and
|
|
38
|
+
* relays the provider response transparently.
|
|
39
|
+
*
|
|
40
|
+
* MUST be called from server-side code only (API routes, server components,
|
|
41
|
+
* server actions) within an authenticated request scope. `getAssembly()` reads
|
|
42
|
+
* the viewer bearer from the request cookie (same as every other data call);
|
|
43
|
+
* there is no `x-assembly-token` header and no token argument. Background/cron
|
|
44
|
+
* calls with no viewer are out of scope for v1 — see connector Phase-2.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getIntegration(provider: Provider): Promise<{
|
|
47
|
+
proxy: (opts: ProxyOpts) => Promise<Response>;
|
|
48
|
+
get: (opts: Omit<ProxyOpts, "method">) => Promise<Response>;
|
|
49
|
+
post: (opts: Omit<ProxyOpts, "method">) => Promise<Response>;
|
|
50
|
+
put: (opts: Omit<ProxyOpts, "method">) => Promise<Response>;
|
|
51
|
+
patch: (opts: Omit<ProxyOpts, "method">) => Promise<Response>;
|
|
52
|
+
delete: (opts: Omit<ProxyOpts, "method">) => Promise<Response>;
|
|
53
|
+
action: <T = unknown>(name: string, input?: unknown) => Promise<T>;
|
|
54
|
+
status: () => Promise<ConnectionStatus>;
|
|
55
|
+
records: (_model: string, _query?: Record<string, unknown>) => never;
|
|
56
|
+
}>;
|
|
57
|
+
export type IntegrationClient = Awaited<ReturnType<typeof getIntegration>>;
|
|
58
|
+
/**
|
|
59
|
+
* Report whether a provider is connected for the current workspace via the
|
|
60
|
+
* broker's read-only status endpoint — no provider call is made, so an
|
|
61
|
+
* unrelated call failure can never masquerade as "not connected".
|
|
62
|
+
*
|
|
63
|
+
* - status says connected → true
|
|
64
|
+
* - status says connection_required /
|
|
65
|
+
* needs_reauth / needs_reauth_scope → false (user must (re)connect)
|
|
66
|
+
* - provider_not_declared / app_key_required → false (a wiring bug — the
|
|
67
|
+
* provider is unusable until the manifest / call site is fixed)
|
|
68
|
+
* - the status call itself fails
|
|
69
|
+
* (connector_unavailable, transport) → true (unknown ≠ disconnected; a
|
|
70
|
+
* healthy connection must never render a Connect prompt over a blip)
|
|
71
|
+
*
|
|
72
|
+
* Do NOT hand-roll connectivity checks by making a provider call and branching
|
|
73
|
+
* on whether it succeeded — that reports a healthy connection as "not
|
|
74
|
+
* connected" the moment any unrelated call fails (the classic bug: probing a
|
|
75
|
+
* non-existent endpoint 404s, and the app shows "not connected" while real
|
|
76
|
+
* reads/writes succeed).
|
|
77
|
+
*/
|
|
78
|
+
export declare function isConnected(provider: Provider): Promise<boolean>;
|
|
79
|
+
export interface ResolveResourceOpts {
|
|
80
|
+
/** Search text. Trimmed; an empty query returns the unfiltered first page. */
|
|
81
|
+
query?: string;
|
|
82
|
+
/** Opaque next-page cursor from the previous page's result. */
|
|
83
|
+
cursor?: string;
|
|
84
|
+
/** Where id/label/items/cursor live on the action's result — take it from the action's catalog signature. */
|
|
85
|
+
fields: ResourceFieldMap;
|
|
86
|
+
/**
|
|
87
|
+
* Action input key that carries the search query. Omit when the resolver
|
|
88
|
+
* action has no server-side search — the fetched page is then filtered by
|
|
89
|
+
* label locally instead.
|
|
90
|
+
*/
|
|
91
|
+
queryInput?: string;
|
|
92
|
+
/** Action input key that carries the page cursor. Defaults to 'cursor'. */
|
|
93
|
+
cursorInput?: string;
|
|
94
|
+
/** Extra action input merged verbatim (e.g. `{ baseId }` for Airtable). */
|
|
95
|
+
input?: Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetch one page of provider resources as normalized `{ id, label }` options —
|
|
99
|
+
* the server half of a resource picker (channel/folder/spreadsheet/base
|
|
100
|
+
* selection). Runs the resolver action through the broker and maps the result
|
|
101
|
+
* via `fields`, so apps never re-derive pagination, id/label mapping, or
|
|
102
|
+
* connector error handling per picker.
|
|
103
|
+
*
|
|
104
|
+
* Returns a serializable ResourcePage rather than throwing ConnectorError:
|
|
105
|
+
* call it from a server action and hand the result (or the action itself) to
|
|
106
|
+
* `<ResourcePicker>`, which renders the matching option list, "Load more",
|
|
107
|
+
* and reconnect/error states. Non-connector errors still propagate.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // app/actions/channels.ts
|
|
111
|
+
* 'use server';
|
|
112
|
+
* import { getIntegration, resolveResource } from '@assembly-js/app-foundation/server';
|
|
113
|
+
*
|
|
114
|
+
* export async function searchChannels(args: { query: string; cursor?: string }) {
|
|
115
|
+
* // list-channels has no query input, so pages are label-filtered locally.
|
|
116
|
+
* return resolveResource('slack', 'list-channels', {
|
|
117
|
+
* ...args,
|
|
118
|
+
* fields: { id: 'id', label: 'name', items: 'conversations', nextCursor: 'next_cursor' },
|
|
119
|
+
* });
|
|
120
|
+
* }
|
|
121
|
+
*/
|
|
122
|
+
export declare function resolveResource(provider: Provider, resolverAction: string, opts: ResolveResourceOpts): Promise<ResourcePage>;
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a Slack channel name to its ID, walking the cursor-paginated
|
|
125
|
+
* conversations.list until found (Slack treats `limit` as a per-page cap and
|
|
126
|
+
* may return fewer; a single un-paginated call misses channels in large
|
|
127
|
+
* workspaces). Input is normalized (leading '#' stripped, lowercased — Slack
|
|
128
|
+
* channel names are always lowercase). Resolved IDs are memoized for the
|
|
129
|
+
* process lifetime; Slack rate limits (conversations.list is a low-tier
|
|
130
|
+
* method) are absorbed by waiting and resuming from the same cursor.
|
|
131
|
+
*
|
|
132
|
+
* Use the returned ID for every follow-up call — conversations.join /
|
|
133
|
+
* conversations.history / chat.postMessage should all be given IDs; most
|
|
134
|
+
* methods reject a `#name` with `invalid_arguments`.
|
|
135
|
+
*
|
|
136
|
+
* Membership: public channels are listed whether or not the bot joined, but
|
|
137
|
+
* posting still needs membership — on a `not_in_channel` error from
|
|
138
|
+
* chat.postMessage, call conversations.join with the ID and retry once.
|
|
139
|
+
* Private channels are listed only when the bot is already a member (and only
|
|
140
|
+
* when the connection has the groups:read scope — without it this helper
|
|
141
|
+
* automatically falls back to public channels only). A private channel cannot
|
|
142
|
+
* be self-joined: surface "invite the bot to #channel in Slack" to the user.
|
|
143
|
+
*/
|
|
144
|
+
export declare function slackFindChannelId(slack: IntegrationClient, name: string): Promise<string | null>;
|
|
145
|
+
export interface SlackPostMessageResult {
|
|
146
|
+
ok: boolean;
|
|
147
|
+
/** Timestamp ID of the posted message (present when ok). */
|
|
148
|
+
ts?: string;
|
|
149
|
+
/** Resolved channel ID (present once the channel was found). */
|
|
150
|
+
channelId?: string;
|
|
151
|
+
/** Slack error code when not ok, e.g. 'channel_not_found', 'not_in_channel'. */
|
|
152
|
+
error?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Post a message to a Slack channel by name or ID. Use this instead of calling
|
|
156
|
+
* the send-message action or chat.postMessage yourself: it resolves `#name` →
|
|
157
|
+
* ID via slackFindChannelId (Slack rejects names with `invalid_arguments`),
|
|
158
|
+
* sends via the pre-built `send-message` action, and on `not_in_channel` it
|
|
159
|
+
* joins the channel and retries once (posting can require membership; joining
|
|
160
|
+
* needs the `channels:join` scope).
|
|
161
|
+
*
|
|
162
|
+
* Slack-level failures come back as `{ ok: false, error }` rather than throwing,
|
|
163
|
+
* so render the right state per code:
|
|
164
|
+
* - `channel_not_found` — no such channel; check the name.
|
|
165
|
+
* - `not_in_channel` — the bot couldn't self-join (private channel, or
|
|
166
|
+
* `channels:join` not declared in the manifest scopes): tell the user to
|
|
167
|
+
* invite the bot to the channel in Slack.
|
|
168
|
+
* Broker-level failures (provider not connected, throttled) still throw
|
|
169
|
+
* ConnectorError like every other call.
|
|
170
|
+
*/
|
|
171
|
+
export declare function slackPostMessage(slack: IntegrationClient, args: {
|
|
172
|
+
channel: string;
|
|
173
|
+
text: string;
|
|
174
|
+
}): Promise<SlackPostMessageResult>;
|