@slates/provider 1.0.0-rc.16 → 1.0.0-rc.17

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.
@@ -56,6 +56,124 @@ var SlateAction = class {
56
56
  }
57
57
  };
58
58
 
59
+ // src/error/api.ts
60
+ import { badRequestError, ServiceError } from "@lowerdeck/error";
61
+ var DEFAULT_DETAIL_KEYS = ["message", "detail", "error", "error_description", "code"];
62
+ var DEFAULT_NESTED_KEYS = ["errors"];
63
+ var isApiErrorRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
64
+ var pushDetail = (details, value, options = {}) => {
65
+ if (typeof value !== "string" && (options.includeNumbers === false || typeof value !== "number")) {
66
+ return;
67
+ }
68
+ let detail = String(value).trim();
69
+ if (detail && !details.includes(detail)) {
70
+ details.push(detail);
71
+ }
72
+ };
73
+ var collectApiErrorDetails = (value, details, options = {}) => {
74
+ if (Array.isArray(value)) {
75
+ for (let item of value) {
76
+ collectApiErrorDetails(item, details, options);
77
+ }
78
+ return;
79
+ }
80
+ if (!isApiErrorRecord(value)) {
81
+ pushDetail(details, value, options);
82
+ return;
83
+ }
84
+ for (let key of options.detailKeys ?? DEFAULT_DETAIL_KEYS) {
85
+ pushDetail(details, value[key], options);
86
+ }
87
+ for (let key of options.nestedKeys ?? DEFAULT_NESTED_KEYS) {
88
+ collectApiErrorDetails(value[key], details, options);
89
+ }
90
+ };
91
+ var getSlateErrorResponse = (error) => {
92
+ if (error.name !== "SlateError" || !isApiErrorRecord(error.data)) {
93
+ return void 0;
94
+ }
95
+ let data = error.data;
96
+ let upstream = isApiErrorRecord(data.upstream) ? data.upstream : void 0;
97
+ let status = upstream?.status ?? data.status;
98
+ return typeof status === "number" || typeof status === "string" ? { status } : void 0;
99
+ };
100
+ var getApiErrorResponse = (error) => {
101
+ if (!isApiErrorRecord(error)) {
102
+ return void 0;
103
+ }
104
+ if (!isApiErrorRecord(error.response)) {
105
+ return getSlateErrorResponse(error);
106
+ }
107
+ let response = error.response;
108
+ return {
109
+ status: typeof response.status === "number" || typeof response.status === "string" ? response.status : void 0,
110
+ statusText: typeof response.statusText === "string" ? response.statusText : void 0,
111
+ data: response.data
112
+ };
113
+ };
114
+ var getApiErrorStatus = (error) => getApiErrorResponse(error)?.status;
115
+ var statusLabelForApiError = (status, statusText) => status !== void 0 ? `HTTP ${status}${statusText ? ` ${statusText}` : ""}: ` : "";
116
+ var extractApiErrorMessage = (error, options = {}) => {
117
+ let details = [];
118
+ let response = options.response ?? getApiErrorResponse(error);
119
+ collectApiErrorDetails(response?.data, details, options);
120
+ if (details.length > 0) {
121
+ return details.join(options.separator ?? " - ");
122
+ }
123
+ if (error instanceof Error && error.message) {
124
+ return error.message;
125
+ }
126
+ return options.fallbackMessage ?? "Unknown error";
127
+ };
128
+ var createApiServiceError = (message, options = {}) => {
129
+ let error = new ServiceError(badRequestError({ message }));
130
+ if (options.reason !== void 0) {
131
+ error.data.reason = options.reason;
132
+ }
133
+ if ("upstreamStatus" in options) {
134
+ error.data.upstreamStatus = options.upstreamStatus;
135
+ }
136
+ if ("upstreamCode" in options) {
137
+ error.data.upstreamCode = options.upstreamCode;
138
+ }
139
+ if (options.parent instanceof Error) {
140
+ error.setParent(options.parent);
141
+ }
142
+ return error;
143
+ };
144
+ var helpers = {
145
+ collectDetails: collectApiErrorDetails,
146
+ extractMessage: extractApiErrorMessage,
147
+ getResponse: getApiErrorResponse,
148
+ getStatus: getApiErrorStatus,
149
+ isRecord: isApiErrorRecord,
150
+ statusLabelFor: statusLabelForApiError
151
+ };
152
+ var buildApiServiceError = (error, options) => {
153
+ if (error instanceof ServiceError) {
154
+ return error;
155
+ }
156
+ let operation = options.operation ?? "request";
157
+ let response = options.extractResponse?.(error, helpers) ?? getApiErrorResponse(error);
158
+ let status = options.extractStatus?.(error, response, helpers) ?? response?.status;
159
+ let message = options.extractMessage?.(error, helpers) ?? extractApiErrorMessage(error, { ...options, response });
160
+ let statusLabel = statusLabelForApiError(status, response?.statusText);
161
+ let fullMessage = options.formatMessage?.({
162
+ providerLabel: options.providerLabel,
163
+ operation,
164
+ statusLabel,
165
+ message,
166
+ response,
167
+ status
168
+ }) ?? `${options.providerLabel} API ${operation} failed: ${statusLabel}${message}`;
169
+ return createApiServiceError(fullMessage, {
170
+ reason: options.reason,
171
+ upstreamStatus: status,
172
+ upstreamCode: options.extractUpstreamCode?.(error, response, helpers),
173
+ parent: options.parent ?? error
174
+ });
175
+ };
176
+
59
177
  // src/error/axios.ts
60
178
  import { isAxiosError } from "axios";
61
179
 
@@ -745,7 +863,8 @@ var SlateActionBuilder = class {
745
863
  handleEvent: props.handleEvent,
746
864
  handleRequest: props.handleRequest,
747
865
  autoRegisterWebhook: props.autoRegisterWebhook,
748
- autoUnregisterWebhook: props.autoUnregisterWebhook
866
+ autoUnregisterWebhook: props.autoUnregisterWebhook,
867
+ http: props.http
749
868
  };
750
869
  return this;
751
870
  }
@@ -820,6 +939,7 @@ var SlateTrigger = class _SlateTrigger extends SlateAction {
820
939
  #polling;
821
940
  #handleEvent;
822
941
  #handleRequest;
942
+ #http;
823
943
  #pollEvents;
824
944
  #autoRegisterWebhook;
825
945
  #autoUnregisterWebhook;
@@ -831,6 +951,7 @@ var SlateTrigger = class _SlateTrigger extends SlateAction {
831
951
  };
832
952
  this.#handleEvent = params.handleEvent;
833
953
  this.#handleRequest = params.handleRequest;
954
+ this.#http = params.http;
834
955
  this.#pollEvents = params.pollEvents;
835
956
  this.#autoRegisterWebhook = params.autoRegisterWebhook;
836
957
  this.#autoUnregisterWebhook = params.autoUnregisterWebhook;
@@ -853,6 +974,9 @@ var SlateTrigger = class _SlateTrigger extends SlateAction {
853
974
  get handleRequest() {
854
975
  return this.#handleRequest;
855
976
  }
977
+ get http() {
978
+ return this.#http;
979
+ }
856
980
  get pollEvents() {
857
981
  return this.#pollEvents;
858
982
  }
@@ -947,6 +1071,90 @@ var SlateAuth = class _SlateAuth {
947
1071
  };
948
1072
  var auth = () => SlateAuth.create();
949
1073
 
1074
+ // src/auth/oauth.ts
1075
+ var isOAuthTokenResponse = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1076
+ var formatOAuthTokenResponseMessage = (fieldLabel, providerLabel = "OAuth provider", operation = "token response") => `${providerLabel} OAuth ${operation} did not return ${fieldLabel}.`;
1077
+ var getOAuthExpiresAtFromExpiresIn = (expiresIn, options = {}) => {
1078
+ let {
1079
+ nowMs = Date.now(),
1080
+ providerLabel,
1081
+ operation,
1082
+ required = false,
1083
+ expiresInType = "numberLike",
1084
+ message
1085
+ } = options;
1086
+ if (expiresIn === void 0 || expiresIn === null || expiresIn === "") {
1087
+ if (required) {
1088
+ throw createApiServiceError(
1089
+ message ?? formatOAuthTokenResponseMessage(
1090
+ "a valid expires_in value",
1091
+ providerLabel,
1092
+ operation
1093
+ ),
1094
+ { reason: "oauth_token_response" }
1095
+ );
1096
+ }
1097
+ return void 0;
1098
+ }
1099
+ let seconds = typeof expiresIn === "number" ? expiresIn : expiresInType === "numberLike" && typeof expiresIn === "string" ? Number(expiresIn) : Number.NaN;
1100
+ if (!Number.isFinite(seconds)) {
1101
+ throw createApiServiceError(
1102
+ message ?? formatOAuthTokenResponseMessage("a valid expires_in value", providerLabel, operation),
1103
+ { reason: "oauth_token_response" }
1104
+ );
1105
+ }
1106
+ return new Date(nowMs + seconds * 1e3).toISOString();
1107
+ };
1108
+ var getRefreshToken = (refreshToken, previousRefreshToken, fallbackMode) => {
1109
+ if (typeof refreshToken === "string") {
1110
+ if (fallbackMode === "falsy" && !refreshToken) {
1111
+ return previousRefreshToken;
1112
+ }
1113
+ return refreshToken;
1114
+ }
1115
+ if (fallbackMode === "falsy" && !refreshToken) {
1116
+ return previousRefreshToken;
1117
+ }
1118
+ return refreshToken === void 0 || refreshToken === null ? previousRefreshToken : void 0;
1119
+ };
1120
+ var normalizeOAuthTokenResponse = (data, options = {}) => {
1121
+ let {
1122
+ providerLabel,
1123
+ operation,
1124
+ previousRefreshToken,
1125
+ refreshTokenFallbackMode = "nullish",
1126
+ accessTokenMessage,
1127
+ expiresInMessage,
1128
+ ...expiresAtOptions
1129
+ } = options;
1130
+ if (!isOAuthTokenResponse(data)) {
1131
+ throw createApiServiceError(
1132
+ accessTokenMessage ?? formatOAuthTokenResponseMessage("a token response object", providerLabel, operation),
1133
+ { reason: "oauth_token_response" }
1134
+ );
1135
+ }
1136
+ if (typeof data.access_token !== "string" || !data.access_token) {
1137
+ throw createApiServiceError(
1138
+ accessTokenMessage ?? formatOAuthTokenResponseMessage("an access token", providerLabel, operation),
1139
+ { reason: "oauth_token_response" }
1140
+ );
1141
+ }
1142
+ return {
1143
+ token: data.access_token,
1144
+ refreshToken: getRefreshToken(
1145
+ data.refresh_token,
1146
+ previousRefreshToken,
1147
+ refreshTokenFallbackMode
1148
+ ),
1149
+ expiresAt: getOAuthExpiresAtFromExpiresIn(data.expires_in, {
1150
+ ...expiresAtOptions,
1151
+ providerLabel,
1152
+ operation,
1153
+ message: expiresInMessage
1154
+ })
1155
+ };
1156
+ };
1157
+
950
1158
  // src/axios/index.ts
951
1159
  import baseAxios, { getAdapter, isAxiosError as isAxiosError2 } from "axios";
952
1160
 
@@ -1530,6 +1738,35 @@ var createAxios = (config2) => {
1530
1738
  applySlateInterceptors(instance, errorMapping);
1531
1739
  return instance;
1532
1740
  };
1741
+ var createAuthenticatedAxios = (config2) => {
1742
+ let { authHeader, contentType = "application/json", errorAdapter, ...axiosConfig } = config2;
1743
+ let instance = createAxios({
1744
+ ...axiosConfig,
1745
+ headers: {
1746
+ ...contentType === false ? {} : { "Content-Type": contentType },
1747
+ ...authHeader ? { [authHeader.name ?? "Authorization"]: authHeader.value } : {},
1748
+ ...axiosConfig.headers
1749
+ }
1750
+ });
1751
+ if (errorAdapter) {
1752
+ instance.interceptors.response.use(
1753
+ (response) => response,
1754
+ (error) => Promise.reject(errorAdapter(error))
1755
+ );
1756
+ }
1757
+ return instance;
1758
+ };
1759
+ var requestAxios = async (operation, request, errorAdapter) => {
1760
+ try {
1761
+ return await request();
1762
+ } catch (error) {
1763
+ throw errorAdapter(error, operation);
1764
+ }
1765
+ };
1766
+ var requestAxiosData = async (operation, request, errorAdapter) => {
1767
+ let response = await requestAxios(operation, request, errorAdapter);
1768
+ return response.data;
1769
+ };
1533
1770
  var axios = createAxios();
1534
1771
 
1535
1772
  // src/config/config.ts
@@ -1608,6 +1845,11 @@ var SlateContext = class {
1608
1845
  get request() {
1609
1846
  return Object.freeze(this.#input.request);
1610
1847
  }
1848
+ get registrationDetails() {
1849
+ return Object.freeze(
1850
+ this.#input.registrationDetails
1851
+ );
1852
+ }
1611
1853
  get auth() {
1612
1854
  return Object.freeze(this.#auth);
1613
1855
  }
@@ -1646,6 +1888,45 @@ var SlateContext = class {
1646
1888
  }
1647
1889
  };
1648
1890
 
1891
+ // src/crypto/hmac.ts
1892
+ import { createHmac, timingSafeEqual } from "crypto";
1893
+ var createHmacSignature = ({
1894
+ secret,
1895
+ payload,
1896
+ algorithm = "sha256",
1897
+ digest,
1898
+ prefix = ""
1899
+ }) => `${prefix}${createHmac(algorithm, secret).update(payload).digest(digest)}`;
1900
+ var verifyHmacSignature = (options) => {
1901
+ let expected = createHmacSignature(options);
1902
+ let expectedBytes = Buffer.from(expected);
1903
+ let signatureBytes = Buffer.from(options.signature);
1904
+ let lengthsMatch = signatureBytes.length === expectedBytes.length;
1905
+ let comparableSignature = lengthsMatch ? signatureBytes : Buffer.alloc(expectedBytes.length);
1906
+ return timingSafeEqual(comparableSignature, expectedBytes) && lengthsMatch;
1907
+ };
1908
+
1909
+ // src/file/index.ts
1910
+ var getResponseHeaderValue = (headers, name) => {
1911
+ if (!headers) return void 0;
1912
+ if (typeof headers.get === "function") {
1913
+ let value = headers.get(name);
1914
+ return typeof value === "string" ? value : void 0;
1915
+ }
1916
+ if (typeof headers !== "object") return void 0;
1917
+ let lowerName = name.toLowerCase();
1918
+ for (let [headerName, value] of Object.entries(headers)) {
1919
+ if (headerName.toLowerCase() !== lowerName) continue;
1920
+ if (Array.isArray(value)) {
1921
+ let first = value.find((item) => typeof item === "string");
1922
+ return typeof first === "string" ? first : void 0;
1923
+ }
1924
+ return typeof value === "string" ? value : void 0;
1925
+ }
1926
+ return void 0;
1927
+ };
1928
+ var getBase64ByteLength = (contentBase64) => Buffer.from(contentBase64, "base64").byteLength;
1929
+
1649
1930
  // src/logger/logger.ts
1650
1931
  import { encode } from "@toon-format/toon";
1651
1932
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1741,6 +2022,17 @@ var SlateLogger = class {
1741
2022
  }
1742
2023
  };
1743
2024
 
2025
+ // src/object/index.ts
2026
+ var pickDefined = (value) => Object.fromEntries(
2027
+ Object.entries(value).filter(([, child]) => child !== void 0)
2028
+ );
2029
+ var setIfDefined = (target, key, value) => {
2030
+ if (value !== void 0) {
2031
+ target[key] = value;
2032
+ }
2033
+ return target;
2034
+ };
2035
+
1744
2036
  // src/specification/slate.ts
1745
2037
  var Slate = class _Slate {
1746
2038
  constructor(_spec, _actions) {
@@ -1803,6 +2095,59 @@ var SlateSpecification = class _SlateSpecification {
1803
2095
  }
1804
2096
  };
1805
2097
  var spec = (params) => SlateSpecification.create(params);
2098
+
2099
+ // src/webhook/graph.ts
2100
+ var graphWebhookHttp = {
2101
+ methods: ["POST"],
2102
+ sync: {
2103
+ mode: "match",
2104
+ match: [{ hasQueryParam: "validationToken" }]
2105
+ }
2106
+ };
2107
+ var getGraphWebhookValidationResponse = (request) => {
2108
+ let validationToken = new URL(request.url).searchParams.get("validationToken");
2109
+ if (validationToken === null) return null;
2110
+ return {
2111
+ status: 200,
2112
+ headers: { "content-type": "text/plain" },
2113
+ body: validationToken
2114
+ };
2115
+ };
2116
+
2117
+ // src/webhook/meta.ts
2118
+ var metaWebhookHttp = {
2119
+ methods: ["GET", "POST"],
2120
+ sync: {
2121
+ mode: "match",
2122
+ match: [{ method: "GET", hasQueryParam: "hub.mode" }]
2123
+ }
2124
+ };
2125
+ var getMetaWebhookVerificationResponse = (request, expectedVerifyToken) => {
2126
+ if (request.method !== "GET") return null;
2127
+ let url = new URL(request.url);
2128
+ let mode = url.searchParams.get("hub.mode");
2129
+ let verifyToken = url.searchParams.get("hub.verify_token");
2130
+ let challenge = url.searchParams.get("hub.challenge");
2131
+ if (mode !== "subscribe" || challenge === null) {
2132
+ return {
2133
+ status: 400,
2134
+ headers: { "content-type": "text/plain" },
2135
+ body: "invalid webhook verification request"
2136
+ };
2137
+ }
2138
+ if (expectedVerifyToken !== void 0 && verifyToken !== expectedVerifyToken) {
2139
+ return {
2140
+ status: 403,
2141
+ headers: { "content-type": "text/plain" },
2142
+ body: "invalid verify token"
2143
+ };
2144
+ }
2145
+ return {
2146
+ status: 200,
2147
+ headers: { "content-type": "text/plain" },
2148
+ body: challenge
2149
+ };
2150
+ };
1806
2151
  export {
1807
2152
  Slate,
1808
2153
  SlateAction,
@@ -1820,24 +2165,47 @@ export {
1820
2165
  anyOf,
1821
2166
  auth,
1822
2167
  axios,
2168
+ buildApiServiceError,
2169
+ collectApiErrorDetails,
1823
2170
  config,
2171
+ createApiServiceError,
2172
+ createAuthenticatedAxios,
1824
2173
  createAxios,
1825
2174
  createBase64Attachment,
2175
+ createHmacSignature,
1826
2176
  createSlateError,
1827
2177
  createTextAttachment,
1828
2178
  createUrlAttachment,
2179
+ extractApiErrorMessage,
2180
+ getApiErrorResponse,
2181
+ getApiErrorStatus,
2182
+ getBase64ByteLength,
1829
2183
  getCurrentContext,
2184
+ getGraphWebhookValidationResponse,
2185
+ getMetaWebhookVerificationResponse,
2186
+ getOAuthExpiresAtFromExpiresIn,
2187
+ getResponseHeaderValue,
1830
2188
  getServiceErrorData,
2189
+ graphWebhookHttp,
1831
2190
  inferSlateErrorFromAxios,
2191
+ isApiErrorRecord,
1832
2192
  isAxiosErrorLike,
1833
2193
  isServiceErrorData,
1834
2194
  mapServiceErrorCode,
1835
2195
  mapServiceErrorToSlateErrorInput,
1836
2196
  mergeScopes,
2197
+ metaWebhookHttp,
2198
+ normalizeOAuthTokenResponse,
2199
+ pickDefined,
2200
+ requestAxios,
2201
+ requestAxiosData,
1837
2202
  runWithContext,
1838
2203
  scopeClause,
2204
+ setIfDefined,
1839
2205
  spec,
2206
+ statusLabelForApiError,
1840
2207
  tool,
1841
2208
  trigger,
1842
- validateScopes
2209
+ validateScopes,
2210
+ verifyHmacSignature
1843
2211
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/provider",
3
- "version": "1.0.0-rc.16",
3
+ "version": "1.0.0-rc.17",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -62,15 +62,58 @@ export type SlateTriggerPollingHandler<
62
62
  updatedState?: any;
63
63
  }>;
64
64
 
65
+ export interface SlateWebhookHttpResponseInit {
66
+ status?: number;
67
+ headers?: Record<string, string>;
68
+ body?: string | Uint8Array | null;
69
+ }
70
+
71
+ export type SlateWebhookHttpMethod =
72
+ | 'GET'
73
+ | 'POST'
74
+ | 'PUT'
75
+ | 'PATCH'
76
+ | 'DELETE'
77
+ | 'HEAD'
78
+ | 'OPTIONS';
79
+
80
+ export interface SlateWebhookRequestMatcher {
81
+ method?: string;
82
+ hasQueryParam?: string;
83
+ hasHeader?: string;
84
+ jsonBodyField?: {
85
+ path: string;
86
+ equals?: string;
87
+ };
88
+ formBodyField?: {
89
+ path: string;
90
+ equals?: string;
91
+ };
92
+ }
93
+
94
+ export interface SlateWebhookHttpOptions {
95
+ methods?: SlateWebhookHttpMethod[];
96
+ sync?: {
97
+ mode: 'never' | 'match' | 'always';
98
+ match?: SlateWebhookRequestMatcher[];
99
+ timeoutMs?: number;
100
+ };
101
+ }
102
+
65
103
  export type SlateTriggerWebhookRequestHandler<
66
104
  ConfigType extends {},
67
105
  AuthType extends {},
68
106
  InputType extends {}
69
107
  > = (
70
- context: SlateContext<ConfigType, AuthType, { request: Request; state: any | null }>
108
+ context: SlateContext<
109
+ ConfigType,
110
+ AuthType,
111
+ { request: Request; state: any | null; registrationDetails: any | null }
112
+ >
71
113
  ) => Promise<{
72
114
  inputs: InputType[];
73
115
  updatedState?: any;
116
+ response?: Response | SlateWebhookHttpResponseInit;
74
117
  }>;
75
118
 
76
119
  export type SlateTriggerWebhookAutoRegistrationHandler<
@@ -117,6 +160,7 @@ export interface SlateActionParametersTrigger<
117
160
  polling?: SlatePollingOptions;
118
161
  handleEvent: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
119
162
  handleRequest?: SlateTriggerWebhookRequestHandler<ConfigType, AuthType, InputType>;
163
+ http?: SlateWebhookHttpOptions;
120
164
  pollEvents?: SlateTriggerPollingHandler<ConfigType, AuthType, InputType>;
121
165
  autoRegisterWebhook?: SlateTriggerWebhookAutoRegistrationHandler<ConfigType, AuthType>;
122
166
  autoUnregisterWebhook?: SlateTriggerWebhookAutoUnregistrationHandler<ConfigType, AuthType>;
@@ -15,7 +15,8 @@ import type {
15
15
  SlateTriggerPollingHandler,
16
16
  SlateTriggerWebhookAutoRegistrationHandler,
17
17
  SlateTriggerWebhookAutoUnregistrationHandler,
18
- SlateTriggerWebhookRequestHandler
18
+ SlateTriggerWebhookRequestHandler,
19
+ SlateWebhookHttpOptions
19
20
  } from './action';
20
21
  import { validateScopes } from './scopes';
21
22
 
@@ -115,6 +116,7 @@ export class SlateActionBuilder<
115
116
  handleRequest: SlateTriggerWebhookRequestHandler<ConfigType, AuthType, InputType>;
116
117
  autoRegisterWebhook?: SlateTriggerWebhookAutoRegistrationHandler<ConfigType, AuthType>;
117
118
  autoUnregisterWebhook?: SlateTriggerWebhookAutoUnregistrationHandler<ConfigType, AuthType>;
119
+ http?: SlateWebhookHttpOptions;
118
120
  }): SlateActionBuilder<Type, ConfigType, AuthType, InputType, OutputType, Result> {
119
121
  if (this.type !== 'trigger') {
120
122
  throw new SlateDeclarationError('handleEvent can only be set for trigger actions');
@@ -126,7 +128,8 @@ export class SlateActionBuilder<
126
128
  handleEvent: props.handleEvent,
127
129
  handleRequest: props.handleRequest,
128
130
  autoRegisterWebhook: props.autoRegisterWebhook,
129
- autoUnregisterWebhook: props.autoUnregisterWebhook
131
+ autoUnregisterWebhook: props.autoUnregisterWebhook,
132
+ http: props.http
130
133
  };
131
134
 
132
135
  return this;
@@ -8,7 +8,8 @@ import {
8
8
  type SlateTriggerPollingHandler,
9
9
  type SlateTriggerWebhookAutoRegistrationHandler,
10
10
  type SlateTriggerWebhookAutoUnregistrationHandler,
11
- type SlateTriggerWebhookRequestHandler
11
+ type SlateTriggerWebhookRequestHandler,
12
+ type SlateWebhookHttpOptions
12
13
  } from './action';
13
14
  import { SlateActionBuilder } from './builder';
14
15
 
@@ -27,6 +28,7 @@ export interface SlateTriggerParameters<
27
28
  polling?: SlatePollingOptions;
28
29
  handleEvent: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
29
30
  handleRequest?: SlateTriggerWebhookRequestHandler<ConfigType, AuthType, InputType>;
31
+ http?: SlateWebhookHttpOptions;
30
32
  pollEvents?: SlateTriggerPollingHandler<ConfigType, AuthType, InputType>;
31
33
  autoRegisterWebhook?: SlateTriggerWebhookAutoRegistrationHandler<ConfigType, AuthType>;
32
34
  autoUnregisterWebhook?: SlateTriggerWebhookAutoUnregistrationHandler<ConfigType, AuthType>;
@@ -44,6 +46,7 @@ export class SlateTrigger<
44
46
  #polling: SlatePollingOptions;
45
47
  #handleEvent: SlateTriggerMappingHandler<ConfigType, AuthType, InputType, OutputType>;
46
48
  #handleRequest?: SlateTriggerWebhookRequestHandler<ConfigType, AuthType, InputType>;
49
+ #http?: SlateWebhookHttpOptions;
47
50
  #pollEvents?: SlateTriggerPollingHandler<ConfigType, AuthType, InputType>;
48
51
  #autoRegisterWebhook?: SlateTriggerWebhookAutoRegistrationHandler<ConfigType, AuthType>;
49
52
  #autoUnregisterWebhook?: SlateTriggerWebhookAutoUnregistrationHandler<ConfigType, AuthType>;
@@ -62,6 +65,7 @@ export class SlateTrigger<
62
65
  };
63
66
  this.#handleEvent = params.handleEvent;
64
67
  this.#handleRequest = params.handleRequest;
68
+ this.#http = params.http;
65
69
  this.#pollEvents = params.pollEvents;
66
70
  this.#autoRegisterWebhook = params.autoRegisterWebhook;
67
71
  this.#autoUnregisterWebhook = params.autoUnregisterWebhook;
@@ -93,6 +97,10 @@ export class SlateTrigger<
93
97
  return this.#handleRequest;
94
98
  }
95
99
 
100
+ get http() {
101
+ return this.#http;
102
+ }
103
+
96
104
  get pollEvents() {
97
105
  return this.#pollEvents;
98
106
  }
package/src/auth/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './auth';
2
+ export * from './oauth';
2
3
  export * from './types';
@@ -0,0 +1,83 @@
1
+ import { ServiceError } from '@lowerdeck/error';
2
+ import { describe, expect, it } from 'vitest';
3
+ import type { ApiServiceError } from '../error/api';
4
+ import { getOAuthExpiresAtFromExpiresIn, normalizeOAuthTokenResponse } from './oauth';
5
+
6
+ describe('OAuth auth helpers', () => {
7
+ it('converts expires_in seconds to an ISO expiresAt timestamp', () => {
8
+ expect(getOAuthExpiresAtFromExpiresIn(60, { nowMs: 0 })).toBe('1970-01-01T00:01:00.000Z');
9
+ });
10
+
11
+ it('normalizes token responses and preserves omitted refresh tokens', () => {
12
+ expect(
13
+ normalizeOAuthTokenResponse(
14
+ {
15
+ access_token: 'access-token',
16
+ expires_in: '120'
17
+ },
18
+ {
19
+ nowMs: 0,
20
+ previousRefreshToken: 'existing-refresh-token'
21
+ }
22
+ )
23
+ ).toEqual({
24
+ token: 'access-token',
25
+ refreshToken: 'existing-refresh-token',
26
+ expiresAt: '1970-01-01T00:02:00.000Z'
27
+ });
28
+ });
29
+
30
+ it('supports falsy refresh-token fallback semantics', () => {
31
+ expect(
32
+ normalizeOAuthTokenResponse(
33
+ {
34
+ access_token: 'access-token',
35
+ refresh_token: ''
36
+ },
37
+ {
38
+ previousRefreshToken: 'existing-refresh-token',
39
+ refreshTokenFallbackMode: 'falsy'
40
+ }
41
+ ).refreshToken
42
+ ).toBe('existing-refresh-token');
43
+ });
44
+
45
+ it('throws ServiceError for missing access tokens', () => {
46
+ let call = () =>
47
+ normalizeOAuthTokenResponse(
48
+ {
49
+ expires_in: 120
50
+ },
51
+ {
52
+ providerLabel: 'Example',
53
+ operation: 'token exchange'
54
+ }
55
+ );
56
+
57
+ expect(call).toThrow(ServiceError);
58
+
59
+ try {
60
+ call();
61
+ } catch (error) {
62
+ expect(error).toBeInstanceOf(ServiceError);
63
+ expect((error as ApiServiceError).data.reason).toBe('oauth_token_response');
64
+ }
65
+ });
66
+
67
+ it('can require a numeric expires_in value', () => {
68
+ expect(() =>
69
+ normalizeOAuthTokenResponse(
70
+ {
71
+ access_token: 'access-token',
72
+ expires_in: '120'
73
+ },
74
+ {
75
+ providerLabel: 'Example',
76
+ operation: 'token exchange',
77
+ required: true,
78
+ expiresInType: 'number'
79
+ }
80
+ )
81
+ ).toThrow(ServiceError);
82
+ });
83
+ });