@squadbase/vite-server 0.1.12-dev.a9ac647 → 0.1.17-dev.3b633bb

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.
Files changed (77) hide show
  1. package/dist/cli/index.js +14375 -1652
  2. package/dist/connectors/airtable-oauth.js +282 -46
  3. package/dist/connectors/airtable.js +319 -51
  4. package/dist/connectors/amplitude.js +322 -47
  5. package/dist/connectors/anthropic.js +135 -47
  6. package/dist/connectors/asana.js +327 -49
  7. package/dist/connectors/attio.js +302 -49
  8. package/dist/connectors/aws-billing.js +287 -46
  9. package/dist/connectors/azure-sql.js +421 -102
  10. package/dist/connectors/backlog-api-key.js +317 -47
  11. package/dist/connectors/clickup.js +338 -49
  12. package/dist/connectors/cosmosdb.js +305 -50
  13. package/dist/connectors/customerio.js +319 -47
  14. package/dist/connectors/dbt.js +340 -47
  15. package/dist/connectors/freshdesk.js +342 -53
  16. package/dist/connectors/freshsales.js +333 -52
  17. package/dist/connectors/freshservice.js +361 -53
  18. package/dist/connectors/gamma.js +327 -52
  19. package/dist/connectors/gemini.js +134 -47
  20. package/dist/connectors/github.js +386 -49
  21. package/dist/connectors/gmail-oauth.js +204 -7
  22. package/dist/connectors/gmail.js +350 -47
  23. package/dist/connectors/google-ads.js +288 -46
  24. package/dist/connectors/google-analytics-oauth.js +310 -46
  25. package/dist/connectors/google-analytics.js +547 -87
  26. package/dist/connectors/google-audit-log.js +438 -47
  27. package/dist/connectors/google-calendar-oauth.js +259 -46
  28. package/dist/connectors/google-calendar.js +359 -47
  29. package/dist/connectors/google-docs.js +220 -6
  30. package/dist/connectors/google-drive.js +262 -5
  31. package/dist/connectors/google-search-console-oauth.js +256 -46
  32. package/dist/connectors/google-sheets.js +272 -47
  33. package/dist/connectors/google-slides.js +205 -6
  34. package/dist/connectors/grafana.js +332 -49
  35. package/dist/connectors/hubspot-oauth.js +208 -5
  36. package/dist/connectors/hubspot.js +306 -49
  37. package/dist/connectors/influxdb.js +416 -51
  38. package/dist/connectors/intercom-oauth.js +210 -5
  39. package/dist/connectors/intercom.js +302 -49
  40. package/dist/connectors/jdbc.js +762 -110
  41. package/dist/connectors/jira-api-key.js +326 -47
  42. package/dist/connectors/kintone-api-token.js +281 -47
  43. package/dist/connectors/kintone.js +328 -47
  44. package/dist/connectors/linear.js +330 -49
  45. package/dist/connectors/linkedin-ads.js +268 -50
  46. package/dist/connectors/mailchimp-oauth.js +268 -46
  47. package/dist/connectors/mailchimp.js +320 -49
  48. package/dist/connectors/meta-ads-oauth.js +273 -48
  49. package/dist/connectors/meta-ads.js +285 -50
  50. package/dist/connectors/mixpanel.js +338 -47
  51. package/dist/connectors/monday.js +360 -49
  52. package/dist/connectors/mongodb.js +319 -57
  53. package/dist/connectors/notion-oauth.js +231 -5
  54. package/dist/connectors/notion.js +323 -51
  55. package/dist/connectors/openai.js +134 -47
  56. package/dist/connectors/oracle.js +454 -103
  57. package/dist/connectors/outlook-oauth.js +204 -5
  58. package/dist/connectors/powerbi-oauth.js +498 -5
  59. package/dist/connectors/salesforce.js +384 -49
  60. package/dist/connectors/semrush.js +609 -49
  61. package/dist/connectors/sentry.js +289 -50
  62. package/dist/connectors/shopify-oauth.js +187 -5
  63. package/dist/connectors/shopify.js +357 -47
  64. package/dist/connectors/sqlserver.js +415 -102
  65. package/dist/connectors/stripe-api-key.js +269 -46
  66. package/dist/connectors/stripe-oauth.js +202 -5
  67. package/dist/connectors/supabase.js +303 -48
  68. package/dist/connectors/tableau.js +536 -163
  69. package/dist/connectors/tiktok-ads.js +279 -48
  70. package/dist/connectors/wix-store.js +320 -49
  71. package/dist/connectors/zendesk-oauth.js +239 -5
  72. package/dist/connectors/zendesk.js +358 -47
  73. package/dist/index.d.ts +149 -1
  74. package/dist/index.js +15057 -2117
  75. package/dist/main.js +15005 -2073
  76. package/dist/vite-plugin.js +14752 -2019
  77. package/package.json +1 -1
@@ -1,48 +1,60 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __esm = (fn, res) => function __init() {
3
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ };
5
+
1
6
  // ../connectors/src/parameter-definition.ts
2
- var ParameterDefinition = class {
3
- slug;
4
- name;
5
- description;
6
- envVarBaseKey;
7
- type;
8
- secret;
9
- required;
10
- constructor(config) {
11
- this.slug = config.slug;
12
- this.name = config.name;
13
- this.description = config.description;
14
- this.envVarBaseKey = config.envVarBaseKey;
15
- this.type = config.type;
16
- this.secret = config.secret;
17
- this.required = config.required;
18
- }
19
- /**
20
- * Get the parameter value from a ConnectorConnectionObject.
21
- */
22
- getValue(connection2) {
23
- const param = connection2.parameters.find(
24
- (p) => p.parameterSlug === this.slug
25
- );
26
- if (!param || param.value == null) {
27
- throw new Error(
28
- `Parameter "${this.slug}" not found or has no value in connection "${connection2.id}"`
29
- );
30
- }
31
- return param.value;
32
- }
33
- /**
34
- * Try to get the parameter value. Returns undefined if not found (for optional params).
35
- */
36
- tryGetValue(connection2) {
37
- const param = connection2.parameters.find(
38
- (p) => p.parameterSlug === this.slug
39
- );
40
- if (!param || param.value == null) return void 0;
41
- return param.value;
7
+ var ParameterDefinition;
8
+ var init_parameter_definition = __esm({
9
+ "../connectors/src/parameter-definition.ts"() {
10
+ "use strict";
11
+ ParameterDefinition = class {
12
+ slug;
13
+ name;
14
+ description;
15
+ envVarBaseKey;
16
+ type;
17
+ secret;
18
+ required;
19
+ constructor(config) {
20
+ this.slug = config.slug;
21
+ this.name = config.name;
22
+ this.description = config.description;
23
+ this.envVarBaseKey = config.envVarBaseKey;
24
+ this.type = config.type;
25
+ this.secret = config.secret;
26
+ this.required = config.required;
27
+ }
28
+ /**
29
+ * Get the parameter value from a ConnectorConnectionObject.
30
+ */
31
+ getValue(connection2) {
32
+ const param = connection2.parameters.find(
33
+ (p) => p.parameterSlug === this.slug
34
+ );
35
+ if (!param || param.value == null) {
36
+ throw new Error(
37
+ `Parameter "${this.slug}" not found or has no value in connection "${connection2.id}"`
38
+ );
39
+ }
40
+ return param.value;
41
+ }
42
+ /**
43
+ * Try to get the parameter value. Returns undefined if not found (for optional params).
44
+ */
45
+ tryGetValue(connection2) {
46
+ const param = connection2.parameters.find(
47
+ (p) => p.parameterSlug === this.slug
48
+ );
49
+ if (!param || param.value == null) return void 0;
50
+ return param.value;
51
+ }
52
+ };
42
53
  }
43
- };
54
+ });
44
55
 
45
56
  // ../connectors/src/connectors/google-analytics-oauth/parameters.ts
57
+ init_parameter_definition();
46
58
  var parameters = {
47
59
  propertyId: new ParameterDefinition({
48
60
  slug: "property-id",
@@ -194,6 +206,28 @@ var ConnectorPlugin = class _ConnectorPlugin {
194
206
  tools;
195
207
  query;
196
208
  checkConnection;
209
+ /**
210
+ * SQPD-1212: Logic-based, rule-driven connection setup. Connectors that
211
+ * implement this expose a step-by-step exploration flow (database/schema/
212
+ * table/etc. discovery) that the dashboard backend drives via the
213
+ * `/connections/:connectionId/setup` endpoint. Implement by delegating to
214
+ * `runSetupFlow` from `setup-flow.ts`.
215
+ */
216
+ setup;
217
+ /**
218
+ * Opt-out of the default "verify before save" behavior on connection
219
+ * creation. The backend invokes `checkConnection` synchronously while
220
+ * creating the connection and aborts (no row inserted) if it fails — this
221
+ * flag disables that for connectors where the check cannot succeed pre-save:
222
+ *
223
+ * - `squadbase-db` populates `connection-url` only after Neon provisioning
224
+ * - OAuth connectors require an OAuth-aware proxyFetch keyed by the
225
+ * connectionId, which doesn't exist until the row is saved
226
+ *
227
+ * Exceptions are the explicit position; new credential-input connectors get
228
+ * the default verify-on-create behavior without opt-in.
229
+ */
230
+ skipConnectionCheckOnCreate;
197
231
  constructor(config) {
198
232
  this.slug = config.slug;
199
233
  this.authType = config.authType;
@@ -210,6 +244,8 @@ var ConnectorPlugin = class _ConnectorPlugin {
210
244
  this.tools = config.tools;
211
245
  this.query = config.query;
212
246
  this.checkConnection = config.checkConnection;
247
+ this.setup = config.setup;
248
+ this.skipConnectionCheckOnCreate = config.skipConnectionCheckOnCreate;
213
249
  }
214
250
  get connectorKey() {
215
251
  return _ConnectorPlugin.deriveKey(this.slug, this.authType);
@@ -274,6 +310,76 @@ var ConnectorPlugin = class _ConnectorPlugin {
274
310
  }
275
311
  };
276
312
 
313
+ // ../connectors/src/setup-flow.ts
314
+ async function runSetupFlow(flow, params, ctx, config) {
315
+ const runtime = {
316
+ params,
317
+ language: ctx.language,
318
+ config
319
+ };
320
+ let state = flow.initialState();
321
+ let answerIdx = 0;
322
+ const pendingParameterUpdates = [];
323
+ for (const step of flow.steps) {
324
+ const ans = ctx.answers[answerIdx];
325
+ if (ans && ans.questionSlug === step.slug) {
326
+ state = step.applyAnswer(state, ans.answer);
327
+ if (step.toParameterUpdates) {
328
+ pendingParameterUpdates.push(...step.toParameterUpdates(state));
329
+ }
330
+ answerIdx += 1;
331
+ continue;
332
+ }
333
+ const resolvedAllowFreeText = step.allowFreeText !== void 0 ? step.allowFreeText : true;
334
+ if (step.type === "text") {
335
+ if (step.fetchOptions) {
336
+ const options2 = await step.fetchOptions(state, runtime);
337
+ if (options2.length === 0) {
338
+ continue;
339
+ }
340
+ }
341
+ return {
342
+ type: "nextQuestion",
343
+ questionSlug: step.slug,
344
+ question: step.question[ctx.language],
345
+ questionType: "text",
346
+ allowFreeText: resolvedAllowFreeText,
347
+ ...pendingParameterUpdates.length > 0 && {
348
+ parameterUpdates: pendingParameterUpdates
349
+ }
350
+ };
351
+ }
352
+ const options = step.fetchOptions ? await step.fetchOptions(state, runtime) : [];
353
+ if (options.length === 0) {
354
+ continue;
355
+ }
356
+ return {
357
+ type: "nextQuestion",
358
+ questionSlug: step.slug,
359
+ question: step.question[ctx.language],
360
+ questionType: step.type,
361
+ options,
362
+ allowFreeText: resolvedAllowFreeText,
363
+ ...pendingParameterUpdates.length > 0 && {
364
+ parameterUpdates: pendingParameterUpdates
365
+ }
366
+ };
367
+ }
368
+ const dataInvestigationResult = await flow.finalize(state, runtime);
369
+ return {
370
+ type: "fulfilled",
371
+ dataInvestigationResult,
372
+ ...pendingParameterUpdates.length > 0 && {
373
+ parameterUpdates: pendingParameterUpdates
374
+ }
375
+ };
376
+ }
377
+ async function resolveSetupSelection(params) {
378
+ const { selected, allSentinel, fetchAll, limit } = params;
379
+ const resolved = selected.includes(allSentinel) ? await fetchAll() : selected.filter((v) => v !== allSentinel);
380
+ return resolved.slice(0, limit);
381
+ }
382
+
277
383
  // ../connectors/src/auth-types.ts
278
384
  var AUTH_TYPES = {
279
385
  OAUTH: "oauth",
@@ -564,6 +670,142 @@ var googleAnalyticsOauthOnboarding = new ConnectorOnboarding({
564
670
  }
565
671
  });
566
672
 
673
+ // ../connectors/src/connectors/google-analytics-oauth/setup-flow.ts
674
+ var ADMIN_BASE_URL3 = "https://analyticsadmin.googleapis.com/v1beta";
675
+ var ALL_PROPERTIES = "__ALL_PROPERTIES__";
676
+ var GOOGLE_ANALYTICS_OAUTH_SETUP_MAX_PROPERTIES = 20;
677
+ async function listAccountSummaries(proxyFetch) {
678
+ const summaries = [];
679
+ let pageToken;
680
+ do {
681
+ const url = pageToken ? `${ADMIN_BASE_URL3}/accountSummaries?pageToken=${encodeURIComponent(pageToken)}` : `${ADMIN_BASE_URL3}/accountSummaries`;
682
+ const res = await proxyFetch(url, { method: "GET" });
683
+ if (!res.ok) {
684
+ const body = await res.text().catch(() => res.statusText);
685
+ throw new Error(
686
+ `google-analytics-oauth: accountSummaries failed (${res.status}): ${body}`
687
+ );
688
+ }
689
+ const data = await res.json();
690
+ summaries.push(...data.accountSummaries ?? []);
691
+ pageToken = data.nextPageToken;
692
+ } while (pageToken);
693
+ return summaries;
694
+ }
695
+ function propertyIdFromResource(resource) {
696
+ return (resource ?? "").replace(/^properties\//, "");
697
+ }
698
+ async function getProperty(proxyFetch, propertyId) {
699
+ const res = await proxyFetch(`${ADMIN_BASE_URL3}/properties/${propertyId}`, {
700
+ method: "GET"
701
+ });
702
+ if (!res.ok) return null;
703
+ return await res.json();
704
+ }
705
+ var googleAnalyticsOauthSetupFlow = {
706
+ initialState: () => ({}),
707
+ steps: [
708
+ {
709
+ slug: "account",
710
+ type: "select",
711
+ question: {
712
+ ja: "Google Analytics \u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044",
713
+ en: "Select a Google Analytics account"
714
+ },
715
+ async fetchOptions(_state, rt) {
716
+ const summaries = await listAccountSummaries(rt.config.proxyFetch);
717
+ return summaries.map((s) => {
718
+ const accountResource = s.account ?? s.name ?? "";
719
+ if (!accountResource) return null;
720
+ return {
721
+ value: accountResource,
722
+ label: s.displayName ?? accountResource
723
+ };
724
+ }).filter((v) => v != null);
725
+ },
726
+ applyAnswer: (state, answer) => ({ ...state, account: answer[0] })
727
+ },
728
+ {
729
+ slug: "properties",
730
+ type: "multiSelect",
731
+ question: {
732
+ ja: "\u5BFE\u8C61\u306E GA4 \u30D7\u30ED\u30D1\u30C6\u30A3\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\uFF08\u8907\u6570\u9078\u629E\u53EF\uFF09",
733
+ en: "Select target GA4 properties (multi-select allowed)"
734
+ },
735
+ async fetchOptions(state, rt) {
736
+ if (!state.account) return [];
737
+ const summaries = await listAccountSummaries(rt.config.proxyFetch);
738
+ const account = summaries.find(
739
+ (s) => (s.account ?? s.name) === state.account
740
+ );
741
+ const props = (account?.propertySummaries ?? []).map((p) => {
742
+ const id = propertyIdFromResource(p.property);
743
+ if (!id) return null;
744
+ return {
745
+ value: id,
746
+ label: p.displayName ? `${p.displayName} (${id})` : id
747
+ };
748
+ }).filter((v) => v != null);
749
+ if (props.length === 0) return [];
750
+ return [
751
+ {
752
+ value: ALL_PROPERTIES,
753
+ label: rt.language === "ja" ? "\u3059\u3079\u3066\u306E\u30D7\u30ED\u30D1\u30C6\u30A3" : "All properties"
754
+ },
755
+ ...props
756
+ ];
757
+ },
758
+ applyAnswer: (state, answer) => ({ ...state, properties: answer })
759
+ }
760
+ ],
761
+ async finalize(state, rt) {
762
+ if (!state.account || !state.properties) {
763
+ throw new Error(
764
+ "Google Analytics OAuth setup: incomplete state on finalize"
765
+ );
766
+ }
767
+ const summaries = await listAccountSummaries(rt.config.proxyFetch);
768
+ const account = summaries.find(
769
+ (s) => (s.account ?? s.name) === state.account
770
+ );
771
+ const accountLabel = account?.displayName ?? state.account.replace(/^accounts\//, "");
772
+ const targetPropertyIds = await resolveSetupSelection({
773
+ selected: state.properties,
774
+ allSentinel: ALL_PROPERTIES,
775
+ fetchAll: async () => (account?.propertySummaries ?? []).map((p) => propertyIdFromResource(p.property)).filter((v) => v),
776
+ limit: GOOGLE_ANALYTICS_OAUTH_SETUP_MAX_PROPERTIES
777
+ });
778
+ const sections = [
779
+ "## Google Analytics",
780
+ "",
781
+ `### Account: ${accountLabel}`,
782
+ ""
783
+ ];
784
+ if (targetPropertyIds.length === 0) {
785
+ sections.push("_No properties selected._", "");
786
+ return sections.join("\n");
787
+ }
788
+ sections.push(
789
+ "| Property ID | Display Name | Time Zone | Currency | Industry |"
790
+ );
791
+ sections.push(
792
+ "|-------------|--------------|-----------|----------|----------|"
793
+ );
794
+ for (const propertyId of targetPropertyIds) {
795
+ const prop = await getProperty(rt.config.proxyFetch, propertyId);
796
+ const displayName = (prop?.displayName ?? "-").replace(/\|/g, "\\|");
797
+ const timeZone = prop?.timeZone ?? "-";
798
+ const currency = prop?.currencyCode ?? "-";
799
+ const industry = (prop?.industryCategory ?? "-").replace(/\|/g, "\\|");
800
+ sections.push(
801
+ `| ${propertyId} | ${displayName} | ${timeZone} | ${currency} | ${industry} |`
802
+ );
803
+ }
804
+ sections.push("");
805
+ return sections.join("\n");
806
+ }
807
+ };
808
+
567
809
  // ../connectors/src/connectors/google-analytics-oauth/tools/request.ts
568
810
  import { z as z3 } from "zod";
569
811
  var BASE_URL2 = "https://analyticsdata.googleapis.com/v1beta/";
@@ -689,6 +931,7 @@ var tools = {
689
931
  var googleAnalyticsOauthConnector = new ConnectorPlugin({
690
932
  slug: "google-analytics",
691
933
  authType: AUTH_TYPES.OAUTH,
934
+ skipConnectionCheckOnCreate: true,
692
935
  name: "Google Analytics",
693
936
  description: "Connect to Google Analytics for web analytics and reporting using OAuth.",
694
937
  iconUrl: "https://images.ctfassets.net/9ncizv60xc5y/7fs0ipzxuD9mACDzBATtxX/3c53ed90d15c96483e4f78cb29dab5e9/google-analytics.svg",
@@ -855,6 +1098,7 @@ const realtime = await ga.runRealtimeReport({
855
1098
  \`\`\``
856
1099
  },
857
1100
  tools,
1101
+ setup: (params, ctx, config) => runSetupFlow(googleAnalyticsOauthSetupFlow, params, ctx, config),
858
1102
  async checkConnection(params, config) {
859
1103
  const { proxyFetch } = config;
860
1104
  const propertyId = params[parameters.propertyId.slug];
@@ -907,6 +1151,7 @@ function resolveEnvVarOptional(entry, key) {
907
1151
  import { getContext } from "hono/context-storage";
908
1152
  import { getCookie } from "hono/cookie";
909
1153
  var APP_SESSION_COOKIE_NAME = "__Host-squadbase-session";
1154
+ var TABLEAU_SESSION_SENTINEL_URL = "squadbase://tableau-session/";
910
1155
  function normalizeHeaders(input) {
911
1156
  const out = {};
912
1157
  if (!input) return out;
@@ -915,6 +1160,11 @@ function normalizeHeaders(input) {
915
1160
  });
916
1161
  return out;
917
1162
  }
1163
+ function extractInputUrl(input) {
1164
+ if (typeof input === "string") return input;
1165
+ if (input instanceof URL) return input.href;
1166
+ return input.url;
1167
+ }
918
1168
  function createSandboxProxyFetch(connectionId) {
919
1169
  return async (input, init) => {
920
1170
  const token = process.env.INTERNAL_SQUADBASE_OAUTH_MACHINE_CREDENTIAL;
@@ -924,10 +1174,17 @@ function createSandboxProxyFetch(connectionId) {
924
1174
  "Connection proxy is not configured. Please check your deployment settings."
925
1175
  );
926
1176
  }
927
- const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1177
+ const originalUrl = extractInputUrl(input);
1178
+ const baseDomain = process.env["SQUADBASE_PREVIEW_BASE_DOMAIN"] ?? "preview.app.squadbase.dev";
1179
+ if (originalUrl === TABLEAU_SESSION_SENTINEL_URL) {
1180
+ const sessionUrl = `https://${sandboxId}.${baseDomain}/_sqcore/connections/${connectionId}/tableau-session`;
1181
+ return fetch(sessionUrl, {
1182
+ method: "POST",
1183
+ headers: { Authorization: `Bearer ${token}` }
1184
+ });
1185
+ }
928
1186
  const originalMethod = init?.method ?? "GET";
929
1187
  const originalBody = init?.body ? JSON.parse(init.body) : void 0;
930
- const baseDomain = process.env["SQUADBASE_PREVIEW_BASE_DOMAIN"] ?? "preview.app.squadbase.dev";
931
1188
  const proxyUrl = `https://${sandboxId}.${baseDomain}/_sqcore/connections/${connectionId}/request`;
932
1189
  return fetch(proxyUrl, {
933
1190
  method: "POST",
@@ -953,10 +1210,9 @@ function createDeployedAppProxyFetch(connectionId) {
953
1210
  }
954
1211
  const baseDomain = process.env["SQUADBASE_APP_BASE_DOMAIN"] ?? "squadbase.app";
955
1212
  const proxyUrl = `https://${projectId}.${baseDomain}/_sqcore/connections/${connectionId}/request`;
1213
+ const sessionUrl = `https://${projectId}.${baseDomain}/_sqcore/connections/${connectionId}/tableau-session`;
956
1214
  return async (input, init) => {
957
- const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
958
- const originalMethod = init?.method ?? "GET";
959
- const originalBody = init?.body ? JSON.parse(init.body) : void 0;
1215
+ const originalUrl = extractInputUrl(input);
960
1216
  const c = getContext();
961
1217
  const appSession = getCookie(c, APP_SESSION_COOKIE_NAME);
962
1218
  if (!appSession) {
@@ -964,6 +1220,14 @@ function createDeployedAppProxyFetch(connectionId) {
964
1220
  "No authentication method available for connection proxy."
965
1221
  );
966
1222
  }
1223
+ if (originalUrl === TABLEAU_SESSION_SENTINEL_URL) {
1224
+ return fetch(sessionUrl, {
1225
+ method: "POST",
1226
+ headers: { Authorization: `Bearer ${appSession}` }
1227
+ });
1228
+ }
1229
+ const originalMethod = init?.method ?? "GET";
1230
+ const originalBody = init?.body ? JSON.parse(init.body) : void 0;
967
1231
  return fetch(proxyUrl, {
968
1232
  method: "POST",
969
1233
  headers: {