@jskit-ai/connectors-catalog 0.2.2 → 0.2.4

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 (192) hide show
  1. package/README.md +4 -4
  2. package/docs/ai.md +3 -3
  3. package/docs/airtable.md +1 -1
  4. package/docs/algolia.md +2 -2
  5. package/docs/amazon-redshift.md +2 -2
  6. package/docs/apollo-io.md +2 -2
  7. package/docs/asana.md +2 -2
  8. package/docs/ashby.md +1 -1
  9. package/docs/attention.md +2 -2
  10. package/docs/aws-athena.md +1 -1
  11. package/docs/aws-credentials.md +3 -3
  12. package/docs/aws-s3.md +1 -1
  13. package/docs/bigquery.md +2 -2
  14. package/docs/brevo.md +2 -2
  15. package/docs/calendly.md +3 -3
  16. package/docs/canva.md +2 -2
  17. package/docs/chargebee.md +2 -2
  18. package/docs/clay.md +2 -2
  19. package/docs/clickhouse.md +1 -1
  20. package/docs/confidence-flags.md +1 -1
  21. package/docs/contentful.md +1 -1
  22. package/docs/dbt-semantic-layer.md +2 -2
  23. package/docs/elevenlabs.md +2 -2
  24. package/docs/firebase-cloud-messaging.md +3 -3
  25. package/docs/firecrawl.md +4 -4
  26. package/docs/fireflies.md +1 -1
  27. package/docs/gatewayapi.md +1 -1
  28. package/docs/gemini-enterprise.md +1 -1
  29. package/docs/github-api.md +1 -1
  30. package/docs/gitlab-api.md +1 -1
  31. package/docs/gmail.md +1 -1
  32. package/docs/gong.md +1 -1
  33. package/docs/google-ads.md +3 -3
  34. package/docs/google-analytics.md +2 -2
  35. package/docs/google-docs.md +1 -1
  36. package/docs/google-drive.md +1 -1
  37. package/docs/google-maps-platform.md +2 -2
  38. package/docs/google-search-console.md +1 -1
  39. package/docs/google-sheets.md +1 -1
  40. package/docs/google-slides.md +2 -2
  41. package/docs/granola.md +2 -2
  42. package/docs/hex.md +2 -2
  43. package/docs/heygen.md +2 -2
  44. package/docs/inngest.md +1 -1
  45. package/docs/klipy.md +1 -1
  46. package/docs/lightspeed.md +2 -2
  47. package/docs/linear.md +1 -1
  48. package/docs/linkedin.md +2 -2
  49. package/docs/mailgun.md +1 -1
  50. package/docs/microsoft-excel.md +2 -2
  51. package/docs/microsoft-fabric.md +1 -1
  52. package/docs/microsoft-outlook.md +1 -1
  53. package/docs/microsoft-powerpoint.md +1 -1
  54. package/docs/microsoft-teams.md +1 -1
  55. package/docs/microsoft-word.md +1 -1
  56. package/docs/notion.md +1 -1
  57. package/docs/oura.md +3 -3
  58. package/docs/paddle.md +3 -3
  59. package/docs/pipedrive.md +2 -2
  60. package/docs/prestashop.md +1 -1
  61. package/docs/replicate.md +1 -1
  62. package/docs/resend.md +1 -1
  63. package/docs/salesforce.md +1 -1
  64. package/docs/sanity.md +3 -3
  65. package/docs/semrush.md +2 -2
  66. package/docs/sentry.md +1 -1
  67. package/docs/slack.md +1 -1
  68. package/docs/snowflake.md +5 -5
  69. package/docs/storyblok.md +1 -1
  70. package/docs/stripe.md +2 -2
  71. package/docs/tally.md +1 -1
  72. package/docs/telegram.md +2 -2
  73. package/docs/tiktok.md +3 -3
  74. package/docs/twilio.md +3 -3
  75. package/docs/twitch.md +2 -2
  76. package/docs/wave.md +3 -3
  77. package/docs/wix.md +13 -13
  78. package/docs/wordpress-com.md +1 -1
  79. package/docs/wordpress-self-hosted.md +1 -1
  80. package/docs/workday.md +2 -2
  81. package/docs/x-twitter.md +3 -3
  82. package/docs/xero.md +3 -3
  83. package/docs/zoho-books.md +2 -2
  84. package/docs/zoho-crm.md +2 -2
  85. package/package.json +4 -3
  86. package/patterns/ai-connections/PATTERN.md +16 -1
  87. package/patterns/ai-connections/example/ai-model.js +12 -3
  88. package/patterns/ai-connections/example/perplexity-answer.js +15 -4
  89. package/patterns/api-key-connection/PATTERN.md +9 -1
  90. package/patterns/api-key-connection/example/connections.js +5 -2
  91. package/patterns/aws-storage-queries/PATTERN.md +13 -3
  92. package/patterns/aws-storage-queries/example/s3-transfer.js +14 -3
  93. package/patterns/event-delivery/PATTERN.md +1 -1
  94. package/patterns/firebase-messaging/PATTERN.md +10 -0
  95. package/patterns/firebase-messaging/example/server/notifications.js +8 -0
  96. package/patterns/google-ads-search/PATTERN.md +18 -7
  97. package/patterns/google-ads-search/example/ads-setup.js +13 -5
  98. package/patterns/oauth-connection/PATTERN.md +1 -1
  99. package/patterns/paddle-catalogue/PATTERN.md +11 -2
  100. package/patterns/paddle-catalogue/example/create-products.js +5 -2
  101. package/patterns/public-image/PATTERN.md +10 -1
  102. package/patterns/public-image/example/logo-url.js +4 -1
  103. package/patterns/redshift-queries/PATTERN.md +1 -1
  104. package/patterns/source-scanning/PATTERN.md +13 -2
  105. package/patterns/source-scanning/example/source-scanner.js +9 -0
  106. package/src/server/ai.js +19 -5
  107. package/src/server/apify.js +2 -2
  108. package/src/server/asana.js +6 -6
  109. package/src/server/ashby.js +2 -1
  110. package/src/server/attention.js +12 -11
  111. package/src/server/awsTransport.js +6 -0
  112. package/src/server/bigquery.js +5 -4
  113. package/src/server/brevo.js +2 -1
  114. package/src/server/clay.js +2 -1
  115. package/src/server/clickhouse.js +2 -1
  116. package/src/server/confidence.js +6 -1
  117. package/src/server/connectorValues.js +33 -0
  118. package/src/server/databricks.js +7 -7
  119. package/src/server/dbt-semantic-layer.js +5 -5
  120. package/src/server/elevenlabs.js +2 -2
  121. package/src/server/firebase-cloud-messaging.js +2 -2
  122. package/src/server/gemini-enterprise.js +5 -5
  123. package/src/server/google-ads-search.js +68 -48
  124. package/src/server/google-ads.js +15 -17
  125. package/src/server/google-drive.js +7 -7
  126. package/src/server/google-maps-platform.js +4 -2
  127. package/src/server/google-search-console.js +4 -3
  128. package/src/server/google.js +8 -4
  129. package/src/server/googleAdsApi.js +14 -0
  130. package/src/server/granola.js +2 -2
  131. package/src/server/hubspot.js +2 -1
  132. package/src/server/incident-io.js +3 -2
  133. package/src/server/lexware.js +5 -4
  134. package/src/server/lightspeed.js +7 -5
  135. package/src/server/linkedin.js +2 -2
  136. package/src/server/mcpTransport.js +8 -4
  137. package/src/server/microsoft-fabric.js +3 -3
  138. package/src/server/microsoft-onedrive.js +2 -2
  139. package/src/server/microsoft-onenote.js +4 -3
  140. package/src/server/microsoft-outlook.js +6 -6
  141. package/src/server/microsoft-sharepoint.js +4 -2
  142. package/src/server/microsoft-teams.js +2 -1
  143. package/src/server/microsoft.js +20 -13
  144. package/src/server/notion.js +2 -1
  145. package/src/server/pipedrive.js +2 -1
  146. package/src/server/salesforce.js +12 -12
  147. package/src/server/semrush.js +9 -8
  148. package/src/server/snowflake.js +20 -20
  149. package/src/server/storyblok.js +2 -1
  150. package/src/server/tally.js +2 -1
  151. package/src/server/telegram.js +2 -6
  152. package/src/server/tiktok.js +3 -3
  153. package/src/server/twilio.js +3 -2
  154. package/src/server/twitch.js +5 -3
  155. package/src/server/types.js +13 -0
  156. package/src/server/wix.js +20 -878
  157. package/src/server/wixBookings.js +449 -0
  158. package/src/server/wixCms.js +132 -0
  159. package/src/server/wixStores.js +296 -0
  160. package/src/server/wixValues.js +35 -0
  161. package/src/server/wiz.js +18 -4
  162. package/src/server/woocommerce.js +1 -1
  163. package/src/server/wordpress-com.js +2 -1
  164. package/src/server/workday.js +9 -9
  165. package/src/server/x-twitter.js +2 -1
  166. package/src/server/xero.js +40 -40
  167. package/src/server/zoho-books.js +6 -6
  168. package/src/server/zoho-crm.js +5 -5
  169. package/src/shared/confidence.js +6 -1
  170. package/src/shared/dbt-semantic-layer.js +1 -1
  171. package/src/shared/figma.js +1 -1
  172. package/src/shared/google.js +8 -4
  173. package/src/shared/googleScope.js +4 -0
  174. package/src/shared/microsoft-fabric.js +4 -5
  175. package/src/shared/microsoft.js +8 -3
  176. package/src/shared/salesforce.js +1 -1
  177. package/src/shared/tokens.js +7 -3
  178. package/src/shared/uuid.js +13 -0
  179. package/src/shared/wix.js +4 -3
  180. package/src/shared/zoho-books.js +3 -4
  181. package/src/shared/zoho-crm.js +1 -1
  182. package/src/shared/{zoho-regions.js → zoho.js} +4 -1
  183. package/test/ai.test.js +1 -1
  184. package/test/brevo.test.js +1 -1
  185. package/test/chargebee.test.js +1 -1
  186. package/test/google-ads.test.js +8 -4
  187. package/test/linkedin.test.js +1 -1
  188. package/test/resend.test.js +2 -2
  189. package/test/salesforce.test.js +1 -1
  190. package/test/snowflake.test.js +7 -7
  191. package/test/tiktok.test.js +1 -1
  192. package/test/wiz.test.js +1 -1
@@ -0,0 +1,296 @@
1
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
+ import { createSchema } from "json-rest-schema";
3
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
4
+ import { requestPath, sameRecord, cursor, contactIdField, nativeJsonObjectField } from "./wixValues.js";
5
+ import { isUuid } from "../shared/uuid.js";
6
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
7
+
8
+ // Wix eCommerce and Stores operations: carts, products, catalog version, orders, fulfillments and business locations.
9
+
10
+ function productReadOperation(version, action) {
11
+ const schema = createSchema(action === "get" ? {
12
+ productId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix product ID." }
13
+ } : {
14
+ limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
15
+ ...(version === 1 ? { offset: { type: "integer", min: 0, max: 100000, defaultTo: 0 }, includeVariants: { type: "boolean", defaultTo: false } } :
16
+ { cursor: { type: "string", validator: value => cursor(value) || "Use the returned product cursor." } })
17
+ });
18
+ const validProduct = value => isUuid(value?.id) && typeof value.name === "string" && value.name.length > 0;
19
+ return {
20
+ scopes: [],
21
+ request(input) {
22
+ const value = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
23
+ const base = version === 1 ? "https://www.wixapis.com/stores-reader/v1/products" : "https://www.wixapis.com/stores/v3/products";
24
+ if (action === "get") return { method: "GET", url: `${base}/${value.productId}` };
25
+ const { includeVariants, ...paging } = value;
26
+ return { method: "POST", url: `${base}/query`,
27
+ body: version === 1 ? { query: { paging }, includeVariants } : { query: { cursorPaging: paging } } };
28
+ },
29
+ validateResult: (value, request) => action === "get" ? validProduct(value?.product) && sameRecord(value.product.id === requestPath(request, -1), "Wix returned a different product.", { messageCode: "connectors.wix.returnedDifferentProduct" }) : Array.isArray(value?.products) && value.products.length <= 100 && value.products.every(validProduct) &&
30
+ (version === 1 ? (value.totalResults === undefined || Number.isSafeInteger(value.totalResults) && value.totalResults >= 0) &&
31
+ (value.metadata === undefined || isRecord(value.metadata) &&
32
+ ["items", "offset"].every(key => value.metadata[key] === undefined || Number.isSafeInteger(value.metadata[key]) && value.metadata[key] >= 0)) :
33
+ (value.pagingMetadata === undefined || isRecord(value.pagingMetadata) &&
34
+ (value.pagingMetadata.cursors === undefined || isRecord(value.pagingMetadata.cursors) &&
35
+ ["next", "prev"].every(key => value.pagingMetadata.cursors[key] === undefined || cursor(value.pagingMetadata.cursors[key])))))
36
+ };
37
+ }
38
+ function locationWriteOperation(action) {
39
+ const schema = createSchema({
40
+ ...(action === "update" ? {
41
+ locationId: contactIdField,
42
+ revision: { type: "none", required: true, validator: value => typeof value === "string" && /^\d{1,19}$/u.test(value) && BigInt(value) <= 9223372036854775807n || "Use the current location revision string." }
43
+ } : {}),
44
+ name: { type: "string", required: true, minLength: 1, maxLength: 150 },
45
+ timeZone: { type: "string", required: true, maxLength: 150, validator: value => {
46
+ try { new Intl.DateTimeFormat("en", { timeZone: value }); return true; } catch { return "Use an IANA time zone."; }
47
+ } },
48
+ description: { type: "string", maxLength: 500 }, email: { type: "string", maxLength: 320 },
49
+ phone: { type: "string", maxLength: 100 }, fax: { type: "string", maxLength: 100 },
50
+ status: { type: "string", enum: ["ACTIVE"] },
51
+ locationTypes: { type: "array", validator: values => values.length <= 10 || "Use at most 10 location types.", items: { type: "string", enum: ["UNKNOWN", "BRANCH", "OFFICES", "RECEPTION", "HEADQUARTERS", "INVENTORY"] } },
52
+ businessSchedule: nativeJsonObjectField, extendedFields: nativeJsonObjectField,
53
+ address: { type: "object", required: true, schema: createSchema({
54
+ country: { type: "string", validator: value => /^[A-Z]{2}$/u.test(value) || "Use a two-letter country code." },
55
+ subdivision: { type: "string", maxLength: 100 }, city: { type: "string", maxLength: 1000 }, postalCode: { type: "string", maxLength: 20 },
56
+ formattedAddress: { type: "string", maxLength: 4000 }, hint: { type: "string", maxLength: 500 },
57
+ streetAddress: { type: "object", schema: createSchema({ number: { type: "string", maxLength: 100 }, name: { type: "string", maxLength: 1000 }, apt: { type: "string", maxLength: 100 } }) },
58
+ geocode: { type: "object", schema: createSchema({ latitude: { type: "number", required: true, min: -90, max: 90 }, longitude: { type: "number", required: true, min: -180, max: 180 } }) }
59
+ }) }
60
+ });
61
+ return {
62
+ scopes: [],
63
+ request(input) {
64
+ const { locationId, ...location } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
65
+ if (!Object.keys(location.address).length) throw new ConnectorError("connector_input_invalid", "Provide the location address.", { statusCode: 422, messageCode: "connectors.wix.provideLocationAddress" });
66
+ if (location.businessSchedule && (["periods", "specialHourPeriod"].some(key => location.businessSchedule[key] !== undefined &&
67
+ (!Array.isArray(location.businessSchedule[key]) || location.businessSchedule[key].length > 100)) ||
68
+ Object.keys(location.businessSchedule).some(key => !["periods", "specialHourPeriod"].includes(key)))) {
69
+ throw new ConnectorError("connector_input_invalid", "Use native businessSchedule periods and specialHourPeriod arrays with at most 100 entries each.", { statusCode: 422, messageCode: "connectors.wix.businessScheduleLimits" });
70
+ }
71
+ return { method: action === "create" ? "POST" : "PUT", url: `https://www.wixapis.com/locations/v1/locations${locationId ? `/${locationId}` : ""}`,
72
+ body: { location: { ...(locationId ? { id: locationId } : {}), ...location } } };
73
+ },
74
+ validateResult: (value, request) => validLocation(value?.location) && typeof value.location.revision === "string" && /^\d{1,19}$/u.test(value.location.revision) &&
75
+ (action === "create" || sameRecord(value.location.id === requestPath(request, -1), "Wix returned a different business location.", { messageCode: "connectors.wix.returnedDifferentBusinessLocation" }))
76
+ };
77
+ }
78
+ function locationActionOperation(action) {
79
+ const schema = createSchema({ locationId: contactIdField });
80
+ return {
81
+ scopes: [],
82
+ request(input) {
83
+ const { locationId } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
84
+ return { method: "POST", url: `https://www.wixapis.com/locations/v1/locations/${locationId}/${action}`,
85
+ body: {} };
86
+ },
87
+ validateResult: (value, request) => validLocation(value?.location) && sameRecord(value.location.id === requestPath(request, -2), "Wix returned a different business location.", { messageCode: "connectors.wix.returnedDifferentBusinessLocation" }) &&
88
+ (action === "archive" ? value.location.archived === true : value.location.default === true)
89
+ };
90
+ }
91
+ const locationQuerySchema = createSchema({
92
+ limit: { type: "integer", min: 1, max: 100, defaultTo: 20 }, offset: { type: "integer", min: 0, max: 100000, defaultTo: 0 },
93
+ archived: { type: "boolean", defaultTo: false }
94
+ });
95
+ const locationGetSchema = createSchema({ locationId: { type: "string", required: true, validator: value => isUuid(value) || "Use the returned location ID." } });
96
+ const validLocation = value => isUuid(value?.id) && typeof value.name === "string" &&
97
+ (value.timeZone === undefined || typeof value.timeZone === "string") && (value.archived === undefined || typeof value.archived === "boolean");
98
+ const productV1UpdateSchema = createSchema({
99
+ productId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix product ID." },
100
+ name: { type: "string", minLength: 1, maxLength: 80 }, visible: { type: "boolean" },
101
+ price: { type: "none", validator: value => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 999999999.99 || "Use a numeric Wix base price from 0 to 999999999.99." }
102
+ });
103
+ const productV3UpdateSchema = createSchema({
104
+ productId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix product ID." },
105
+ revision: { type: "none", required: true, validator: value => typeof value === "string" && /^\d{1,19}$/u.test(value) && BigInt(value) <= 9223372036854775807n || "Use the current product revision string." },
106
+ name: { type: "string", minLength: 1, maxLength: 80 },
107
+ visible: { type: "boolean" }
108
+ });
109
+ const catalogVersionSchema = createSchema({});
110
+ const orderSearchSchema = createSchema({
111
+ limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
112
+ cursor: { type: "string", validator: value => cursor(value) || "Use the returned order cursor." },
113
+ status: { type: "string", minLength: 1, maxLength: 100 },
114
+ contactId: { type: "string", validator: value => isUuid(value) || "Use the buyer's contact ID." }
115
+ });
116
+ const orderIdSchema = createSchema({ orderId: { type: "string", required: true, minLength: 1, maxLength: 100, validator: value => !/[\p{Cc}]/u.test(value) && ![".", ".."].includes(value) || "Use the returned order ID." } });
117
+ const cartCreateSchema = createSchema({
118
+ note: { type: "string", maxLength: 1000 },
119
+ catalogItems: { type: "array", required: true, validator: values => values.length > 0 && values.length <= 300 || "Select 1–300 catalogue items.",
120
+ items: { type: "object", schema: createSchema({
121
+ quantity: { type: "integer", required: true, min: 1, max: 100000 },
122
+ catalogReference: { type: "object", required: true, schema: createSchema({
123
+ appId: { type: "string", required: true, validator: value => isUuid(value) || "Use the catalogue app ID." },
124
+ catalogItemId: { type: "string", required: true, minLength: 1, maxLength: 36, validator: value => !/[\p{Cc}]/u.test(value) || "Use the selected catalogue item ID." },
125
+ options: nativeJsonObjectField
126
+ }) }
127
+ }) } }
128
+ });
129
+ const cartIdSchema = createSchema({ cartId: { type: "string", required: true, validator: value => isUuid(value) || "Use the cart ID returned by Wix." } });
130
+ const validOrder = value => typeof value?.id === "string" && value.id.length > 0 && value.id.length <= 100 &&
131
+ typeof value.status === "string" && value.status.length > 0 && (value.currency === undefined || typeof value.currency === "string") &&
132
+ (value.lineItems === undefined || Array.isArray(value.lineItems));
133
+ function fulfillmentWriteOperation(update) {
134
+ const trackingSchema = createSchema({
135
+ trackingNumber: { type: "string", minLength: 1, maxLength: 100 },
136
+ shippingProvider: { type: "string", minLength: 1, maxLength: 100 },
137
+ trackingLink: { type: "string", minLength: 1, maxLength: 2048, validator: value => {
138
+ try { const url = new URL(value); return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password || "Use a public tracking URL."; }
139
+ catch { return "Use a public tracking URL."; }
140
+ } }
141
+ });
142
+ const schema = createSchema({
143
+ orderId: { type: "string", required: true, minLength: 1, maxLength: 100, validator: value => !/[\p{Cc}]/u.test(value) && ![".", ".."].includes(value) || "Use the returned order ID." },
144
+ ...(update ? { fulfillmentId: contactIdField } : {}),
145
+ lineItems: { type: "array", required: !update, validator: values => values.length > 0 && values.length <= 300 && new Set(values.map(item => item?.id)).size === values.length || "Select 1–300 distinct order line items.",
146
+ items: { type: "object", schema: createSchema({ id: contactIdField, quantity: { type: "integer", required: true, min: 1, max: 100000 } }) } },
147
+ trackingInfo: { type: "object", schema: trackingSchema },
148
+ status: { type: "string", enum: ["Pending", "Accepted", "Ready", "In_Delivery", "Fulfilled"] },
149
+ completed: { type: "boolean" }
150
+ });
151
+ return {
152
+ scopes: [],
153
+ request(input) {
154
+ const { orderId, fulfillmentId, ...fulfillment } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
155
+ if (!Object.keys(fulfillment).length || fulfillment.trackingInfo && !Object.keys(fulfillment.trackingInfo).length) {
156
+ throw new ConnectorError("connector_input_invalid", "Supply the fulfillment properties to write.", { statusCode: 422, messageCode: "connectors.wix.supplyFulfillmentPropertiesWrite" });
157
+ }
158
+ const tracking = fulfillment.trackingInfo;
159
+ if (!update && tracking && (!tracking.trackingNumber || !tracking.shippingProvider ||
160
+ !["fedex", "ups", "usps", "dhl", "canadaPost"].includes(tracking.shippingProvider) && !tracking.trackingLink)) {
161
+ throw new ConnectorError("connector_input_invalid", "Supply tracking number and provider; custom carriers also require a tracking URL.", { statusCode: 422, messageCode: "connectors.wix.supplyTrackingNumberProviderCustom" });
162
+ }
163
+ return { method: update ? "PATCH" : "POST", url: update
164
+ ? `https://www.wixapis.com/ecom/v1/fulfillments/${fulfillmentId}/orders/${encodeURIComponent(orderId)}`
165
+ : `https://www.wixapis.com/ecom/v1/fulfillments/orders/${encodeURIComponent(orderId)}/create-fulfillment`,
166
+ body: { fulfillment } };
167
+ },
168
+ validateResult: (value, request) => typeof value?.orderWithFulfillments?.orderId === "string" &&
169
+ sameRecord(value.orderWithFulfillments.orderId === requestPath(request, update ? -1 : -2), "Wix returned fulfillments for a different order.", { messageCode: "connectors.wix.returnedFulfillmentsDifferentOrder" }) &&
170
+ Array.isArray(value.orderWithFulfillments.fulfillments) &&
171
+ value.orderWithFulfillments.fulfillments.every(item => isUuid(item?.id) && Array.isArray(item.lineItems) && item.lineItems.length <= 300 &&
172
+ item.lineItems.every(line => isUuid(line?.id) && Number.isInteger(line.quantity) && line.quantity >= 1 && line.quantity <= 100000)) &&
173
+ (update ? sameRecord(value.orderWithFulfillments.fulfillments.some(item => item.id === requestPath(request, -3)),
174
+ "Wix did not return the updated fulfillment. Inspect the order before retrying.", { messageCode: "connectors.wix.didNotReturnUpdatedFulfillment" }) :
175
+ isUuid(value.fulfillmentId) && value.orderWithFulfillments.fulfillments.some(item => item.id === value.fulfillmentId))
176
+ };
177
+ }
178
+ const wixStoreOperations = {
179
+ "carts.create": {
180
+ scopes: [],
181
+ request(input) {
182
+ const { note, catalogItems } = validateSchemaPayload({ schema: cartCreateSchema, mode: "replace" }, input, { statusCode: 422 });
183
+ return { method: "POST", url: "https://www.wixapis.com/ecom/v2/carts",
184
+ body: { cart: { source: { channelType: "WEB" }, ...(note === undefined ? {} : { note }) }, catalogItems } };
185
+ },
186
+ validateResult: value => isUuid(value?.cart?.id) && Array.isArray(value.cart.lineItems) && value.cart.lineItems.length <= 300 &&
187
+ value.cart.lineItems.every(item => isUuid(item?.id))
188
+ },
189
+ "carts.getCheckoutUrl": {
190
+ scopes: [],
191
+ request(input) {
192
+ const { cartId } = validateSchemaPayload({ schema: cartIdSchema, mode: "replace" }, input, { statusCode: 422 });
193
+ return { method: "POST", url: `https://www.wixapis.com/ecom/v2/carts/${cartId}/get-checkout-url`, body: {} };
194
+ },
195
+ validateResult(value) {
196
+ if (typeof value?.checkoutUrl !== "string" || value.checkoutUrl.length > 8000 || /[\p{Cc}]/u.test(value.checkoutUrl)) return false;
197
+ try { const url = new URL(value.checkoutUrl); return url.protocol === "https:" && !url.username && !url.password; } catch { return false; }
198
+ }
199
+ },
200
+ "locations.archive": locationActionOperation("archive"),
201
+ "locations.setDefault": locationActionOperation("set-default"),
202
+ "locations.create": locationWriteOperation("create"),
203
+ "locations.update": locationWriteOperation("update"),
204
+ "locations.list": {
205
+ scopes: [],
206
+ request(input) {
207
+ const { archived, ...paging } = validateSchemaPayload({ schema: locationQuerySchema, mode: "replace" }, input, { statusCode: 422 });
208
+ return { method: "POST", url: "https://www.wixapis.com/locations/v1/locations/query",
209
+ body: { query: { filter: { archived }, paging }, filterAuthorizedLocationEntities: true } };
210
+ },
211
+ validateResult: value => Array.isArray(value?.locations) && value.locations.length <= 100 && value.locations.every(validLocation) &&
212
+ (value.pagingMetadata === undefined || isRecord(value.pagingMetadata) &&
213
+ ["count", "offset", "total"].every(key => value.pagingMetadata[key] === undefined || Number.isSafeInteger(value.pagingMetadata[key]) && value.pagingMetadata[key] >= 0))
214
+ },
215
+ "locations.get": {
216
+ scopes: [],
217
+ request(input) {
218
+ const { locationId } = validateSchemaPayload({ schema: locationGetSchema, mode: "replace" }, input, { statusCode: 422 });
219
+ return { method: "GET", url: `https://www.wixapis.com/locations/v1/locations/${locationId}` };
220
+ },
221
+ validateResult: (value, request) => validLocation(value?.location) && sameRecord(value.location.id === requestPath(request, -1), "Wix returned a different business location.", { messageCode: "connectors.wix.returnedDifferentBusinessLocation" })
222
+ },
223
+ "productsV1.list": productReadOperation(1, "list"),
224
+ "productsV1.get": productReadOperation(1, "get"),
225
+ "productsV1.update": {
226
+ scopes: [],
227
+ request(input) {
228
+ const { productId, price, ...changes } = validateSchemaPayload({ schema: productV1UpdateSchema, mode: "replace" }, input, { statusCode: 422 });
229
+ if (!Object.keys(changes).length && price === undefined) throw new ConnectorError("connector_input_invalid", "Provide a product name, visibility or base-price change.", { statusCode: 422, messageCode: "connectors.wix.provideProductNameVisibilityBase" });
230
+ return { method: "PATCH", url: `https://www.wixapis.com/stores/v1/products/${productId}`,
231
+ body: { product: { ...changes, ...(price === undefined ? {} : { priceData: { price } }) } } };
232
+ },
233
+ validateResult: (value, request) => isUuid(value?.product?.id) && typeof value.product.name === "string" && value.product.name.length > 0 &&
234
+ sameRecord(value.product.id === requestPath(request, -1), "Wix returned a different product.", { messageCode: "connectors.wix.returnedDifferentProduct" })
235
+ },
236
+ "productsV3.list": productReadOperation(3, "list"),
237
+ "productsV3.get": productReadOperation(3, "get"),
238
+ "productsV3.update": {
239
+ scopes: [],
240
+ request(input) {
241
+ const { productId, revision, ...changes } = validateSchemaPayload({ schema: productV3UpdateSchema, mode: "replace" }, input, { statusCode: 422 });
242
+ if (!Object.keys(changes).length) throw new ConnectorError("connector_input_invalid", "Provide a product name or visibility change.", { statusCode: 422, messageCode: "connectors.wix.provideProductNameVisibilityChange" });
243
+ return { method: "PATCH", url: `https://www.wixapis.com/stores/v3/products/${productId}`,
244
+ body: { product: { id: productId, revision, ...changes } } };
245
+ },
246
+ validateResult: (value, request) => isUuid(value?.product?.id) && typeof value.product.name === "string" && value.product.name.length > 0 &&
247
+ sameRecord(value.product.id === requestPath(request, -1), "Wix returned a different product.", { messageCode: "connectors.wix.returnedDifferentProduct" }) &&
248
+ typeof value.product.revision === "string" && /^\d{1,19}$/u.test(value.product.revision) && BigInt(value.product.revision) <= 9223372036854775807n
249
+ },
250
+ "catalog.version": {
251
+ scopes: [],
252
+ request(input) {
253
+ validateSchemaPayload({ schema: catalogVersionSchema, mode: "replace" }, input, { statusCode: 422 });
254
+ return { method: "GET", url: "https://www.wixapis.com/stores/v3/provision/version" };
255
+ }, validateResult: value => ["V1_CATALOG", "V3_CATALOG", "STORES_NOT_INSTALLED"].includes(value?.catalogVersion)
256
+ },
257
+ "orders.list": {
258
+ scopes: [],
259
+ request(input) {
260
+ const { status, contactId, ...cursorPaging } = validateSchemaPayload({ schema: orderSearchSchema, mode: "replace" }, input, { statusCode: 422 });
261
+ if (cursorPaging.cursor && (status || contactId)) throw new ConnectorError("connector_input_invalid", "Use only cursor and limit for the next order page.", { statusCode: 422, messageCode: "connectors.wix.useCursorLimitNextOrder" });
262
+ const filter = { ...(status ? { status } : {}), ...(contactId ? { "buyerInfo.contactId": contactId } : {}) };
263
+ return { method: "POST", url: "https://www.wixapis.com/ecom/v1/orders/search",
264
+ body: { search: { cursorPaging, ...(Object.keys(filter).length ? { filter } : {}) } } };
265
+ },
266
+ validateResult: value => Array.isArray(value?.orders) && value.orders.length <= 100 && value.orders.every(validOrder) &&
267
+ (value.metadata === undefined || isRecord(value.metadata) &&
268
+ (value.metadata.count === undefined || value.metadata.count === value.orders.length) &&
269
+ (value.metadata.hasNext === undefined || typeof value.metadata.hasNext === "boolean") &&
270
+ (value.metadata.cursors === undefined || isRecord(value.metadata.cursors) &&
271
+ ["next", "prev"].every(key => value.metadata.cursors[key] === undefined || cursor(value.metadata.cursors[key]))))
272
+ },
273
+ "orders.get": {
274
+ scopes: [],
275
+ request(input) {
276
+ const { orderId } = validateSchemaPayload({ schema: orderIdSchema, mode: "replace" }, input, { statusCode: 422 });
277
+ return { method: "GET", url: `https://www.wixapis.com/ecom/v1/orders/${encodeURIComponent(orderId)}` };
278
+ }, validateResult: (value, request) => validOrder(value?.order) && sameRecord(value.order.id === requestPath(request, -1), "Wix returned a different order.", { messageCode: "connectors.wix.returnedDifferentOrder" })
279
+ },
280
+ "orderFulfillments.create": fulfillmentWriteOperation(false),
281
+ "orderFulfillments.update": fulfillmentWriteOperation(true),
282
+ "orderFulfillments.list": {
283
+ scopes: [],
284
+ request(input) {
285
+ const { orderId } = validateSchemaPayload({ schema: orderIdSchema, mode: "replace" }, input, { statusCode: 422 });
286
+ return { method: "GET", url: `https://www.wixapis.com/ecom/v1/fulfillments/orders/${encodeURIComponent(orderId)}` };
287
+ },
288
+ validateResult: (value, request) => isUuid(value?.orderWithFulfillments?.orderId) &&
289
+ sameRecord(value.orderWithFulfillments.orderId === requestPath(request, -1), "Wix returned fulfillments for a different order.", { messageCode: "connectors.wix.returnedFulfillmentsDifferentOrder" }) &&
290
+ Array.isArray(value.orderWithFulfillments.fulfillments) &&
291
+ value.orderWithFulfillments.fulfillments.every(item => isUuid(item?.id) && Array.isArray(item.lineItems) && item.lineItems.length <= 300 &&
292
+ item.lineItems.every(line => isUuid(line?.id) && Number.isInteger(line.quantity) && line.quantity >= 1 && line.quantity <= 100000))
293
+ }
294
+ };
295
+
296
+ export { wixStoreOperations };
@@ -0,0 +1,35 @@
1
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
+ import { createSchema } from "json-rest-schema";
3
+ import { isUuid } from "../shared/uuid.js";
4
+
5
+ // The Wix value checks, fields and record checks the operation groups share.
6
+
7
+ // Preserve native Wix JSON without silently dropping unsupported values.
8
+ function serializeNativeJson(value) {
9
+ return JSON.stringify(value, (key, item) => {
10
+ if (["undefined", "function", "symbol", "bigint"].includes(typeof item) ||
11
+ typeof item === "number" && !Number.isFinite(item)) throw new Error("JSON required");
12
+ return item;
13
+ });
14
+ }
15
+ // Results are checked against the operation's own request; a different record is reported with what Wix got wrong.
16
+ const requestPath = (request, index) => decodeURIComponent(new URL(request.url).pathname.split("/").at(index));
17
+ function sameRecord(matches, message, codes) {
18
+ if (!matches) throw new ConnectorError("connector_response_invalid", message, { statusCode: 502, ...codes });
19
+ return true;
20
+ }
21
+ const cursor = (value) => typeof value === "string" && value.length > 0 && value.length <= 16000 && !/[\p{Cc}]/u.test(value);
22
+ const querySchema = createSchema({
23
+ limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
24
+ cursor: { type: "string", minLength: 1, maxLength: 16000, noTrim: true, validator: (value) => cursor(value) || "Use the returned Wix page cursor." }
25
+ });
26
+ const contactIdField = { type: "string", required: true, validator: value => isUuid(value) || "Use the contact ID returned by Wix." };
27
+ const nativeJsonObjectField = { type: "none", validator: value => {
28
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "Use a native Wix JSON object.";
29
+ try {
30
+ const json = serializeNativeJson(value);
31
+ return Buffer.byteLength(json) <= 100000 || "Keep native Wix object data within 100KB.";
32
+ } catch { return "Use serializable native Wix JSON data."; }
33
+ } };
34
+
35
+ export { serializeNativeJson, requestPath, sameRecord, cursor, querySchema, contactIdField, nativeJsonObjectField };
package/src/server/wiz.js CHANGED
@@ -6,8 +6,8 @@ import path from "node:path";
6
6
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
7
7
  import { validateIntegrationConfiguration } from "@jskit-ai/connectors-core/shared/configuration";
8
8
  import { wizDefinition } from "../shared/wiz.js";
9
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
9
10
 
10
- const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
11
11
  const failure = (code, message, statusCode = 502, codes = {}) => new ConnectorError(code, message, { statusCode, ...codes });
12
12
 
13
13
  // This process belongs to the scanner, not the application's chat/agent runtime.
@@ -40,11 +40,25 @@ async function runCli(executable, args, options, signal, timeoutMs) {
40
40
  });
41
41
  }
42
42
 
43
+ /**
44
+ * @param {object} options
45
+ * @param {Record<string, any>} options.configuration The integration configuration.
46
+ * @param {import("@jskit-ai/connectors-core/server/types").ConnectorAuthorize} options.authorize
47
+ * @param {import("@jskit-ai/connectors-core/server/types").ResolveReference} options.resolveReference Resolves the registration's client secret.
48
+ * @param {import("./types.js").WizScanTargetResolver} options.resolveScanTarget
49
+ * @param {string} options.executable The installed Wiz CLI's absolute path.
50
+ * @param {Record<string, string>} [options.environment] The scan worker's PATH, proxy and CA variables.
51
+ * @param {number} [options.timeoutMs]
52
+ * @param {number} [options.maxReportBytes]
53
+ */
43
54
  function createWizScanner({ configuration, authorize, resolveReference, resolveScanTarget, executable,
44
55
  environment = { PATH: process.env.PATH || "/usr/bin:/bin" }, timeoutMs = 600_000, maxReportBytes = 16 * 1024 * 1024 }) {
45
56
  if (!["linux", "darwin"].includes(process.platform)) throw new TypeError("Run Wiz in a Linux or macOS scan worker.");
46
57
  if (typeof executable !== "string" || !path.isAbsolute(executable) || executable.includes("\0")) throw new TypeError("Supply the installed Wiz CLI's absolute path.");
47
- if (![authorize, resolveReference, resolveScanTarget].every((value) => typeof value === "function")) throw new TypeError("Wiz requires host authorization, secret resolution and source resolution.");
58
+ // readable-check: allow method-probe - input validation: the application passes its access policy, reference resolver and scan target resolver
59
+ if (typeof authorize !== "function" || typeof resolveReference !== "function" || typeof resolveScanTarget !== "function") {
60
+ throw new TypeError("Wiz requires host authorization, secret resolution and source resolution.");
61
+ }
48
62
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000) throw new TypeError("Use a scan timeout between 1 and 3600000 milliseconds.");
49
63
  if (!Number.isSafeInteger(maxReportBytes) || maxReportBytes < 1 || maxReportBytes > 64 * 1024 * 1024) throw new TypeError("Use a positive report limit of at most 64 MiB.");
50
64
  const env = {};
@@ -69,7 +83,7 @@ function createWizScanner({ configuration, authorize, resolveReference, resolveS
69
83
  // The browser supplies no filesystem path. The trusted host selects an immutable
70
84
  // source snapshot and revision after checking the requesting principal's access.
71
85
  const target = await resolveScanTarget({ ...owner }, { integrationId, signal });
72
- if (!object(target) || typeof target.directory !== "string" || !path.isAbsolute(target.directory) ||
86
+ if (!isRecord(target) || typeof target.directory !== "string" || !path.isAbsolute(target.directory) ||
73
87
  target.directory.includes("\0") || typeof target.revision !== "string" || !target.revision.trim() || target.revision.length > 512) {
74
88
  throw failure("connector_source_invalid", "The host must supply an absolute source directory and revision.", 422, { messageCode: "connectors.wiz.hostMustSupplyAbsoluteSource" });
75
89
  }
@@ -103,7 +117,7 @@ function createWizScanner({ configuration, authorize, resolveReference, resolveS
103
117
  } finally { await file.close(); }
104
118
  } catch { throw failure("connector_response_invalid", "Wiz did not produce a readable report within the configured size limit.", 502, { messageCode: "connectors.wiz.didNotProduceReadableReport" }); }
105
119
  const verdict = report?.status?.verdict;
106
- if (!object(report) || report.status?.state !== "SUCCESS" || !object(report.result) || !["PASSED_BY_POLICY", "WARN_BY_POLICY", "FAILED_BY_POLICY"].includes(verdict) ||
120
+ if (!isRecord(report) || report.status?.state !== "SUCCESS" || !isRecord(report.result) || !["PASSED_BY_POLICY", "WARN_BY_POLICY", "FAILED_BY_POLICY"].includes(verdict) ||
107
121
  (exitCode === 4 && verdict !== "FAILED_BY_POLICY")) {
108
122
  throw failure("connector_response_invalid", "Wiz returned an unsupported or inconsistent scan report.", 502, { messageCode: "connectors.wiz.returnedUnsupportedInconsistentScanReport" });
109
123
  }
@@ -242,7 +242,7 @@ function refundOperation(action) {
242
242
  scopes: [],
243
243
  request(input, settings) {
244
244
  if (action === "create" && (typeof input?.amount !== "string" || typeof input.api_refund !== "boolean" || typeof input.api_restock !== "boolean" ||
245
- input.line_items?.some?.(line => typeof line?.refund_total !== "string"))) {
245
+ Array.isArray(input.line_items) && input.line_items.some(line => typeof line?.refund_total !== "string"))) {
246
246
  throw new ConnectorError("connector_input_invalid", "Choose explicit refund/restock flags and decimal-string amounts.", { statusCode: 422, messageCode: "connectors.woocommerce.chooseExplicitRefundRestockFlags" });
247
247
  }
248
248
  const { order_id, id, ...fields } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
@@ -3,6 +3,7 @@ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
3
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
4
  import { wordpressComDefinition } from "../shared/wordpress-com.js";
5
5
  import { jsonOperation } from "./jsonOperation.js";
6
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
6
7
 
7
8
  const postsSchema = createSchema({
8
9
  siteId: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
@@ -131,7 +132,7 @@ const wordpressComProvider = Object.freeze({
131
132
  }
132
133
  return { method: "GET", url: url.href };
133
134
  },
134
- validateResult: value => value !== null && typeof value === "object" && !Array.isArray(value) &&
135
+ validateResult: value => isRecord(value) &&
135
136
  Object.keys(value).length > 0 && Object.values(value).every(entry => entry !== null && typeof entry === "object")
136
137
  },
137
138
  "stats.read": siteOperation("", {}, {
@@ -3,21 +3,21 @@ import { jsonOperation } from "./jsonOperation.js";
3
3
  import { createSchema } from "json-rest-schema";
4
4
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
5
5
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
6
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
6
7
 
7
8
  function workersUrl(settings) {
8
9
  const { host, prefix = "", tenant } = workdayRestEndpoint(settings.restApiEndpoint);
9
10
  return `https://${host}${prefix}/api/staffing/v7/${tenant}/workers`;
10
11
  }
11
- const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12
- const worker = (value) => object(value) && /^[a-f0-9]{32}$/u.test(value.id) && typeof value.descriptor === "string" &&
12
+ const worker = (value) => isRecord(value) && /^[a-f0-9]{32}$/u.test(value.id) && typeof value.descriptor === "string" &&
13
13
  (value.workerId === undefined || typeof value.workerId === "string");
14
14
  const pageFields = {
15
15
  limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
16
16
  offset: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER, defaultTo: 0 }
17
17
  };
18
- const organizationRecord = (value) => object(value) && typeof value.id === "string" && value.id.length > 0 &&
18
+ const organizationRecord = (value) => isRecord(value) && typeof value.id === "string" && value.id.length > 0 &&
19
19
  typeof value.descriptor === "string";
20
- const organizationPage = (value) => object(value) && !value.error && !value.errors && Number.isSafeInteger(value.total) && value.total >= 0 &&
20
+ const organizationPage = (value) => isRecord(value) && !value.error && !value.errors && Number.isSafeInteger(value.total) && value.total >= 0 &&
21
21
  Array.isArray(value.data) && value.data.length <= 100 && value.data.every(organizationRecord);
22
22
  function organizationRead(part) {
23
23
  const schema = createSchema({
@@ -68,10 +68,10 @@ function absenceRead(kind) {
68
68
  return { method: "GET", url: url.href };
69
69
  },
70
70
  validateResult(value) {
71
- return object(value) && !value.error && !value.errors && Number.isSafeInteger(value.total) && value.total >= 0 &&
71
+ return isRecord(value) && !value.error && !value.errors && Number.isSafeInteger(value.total) && value.total >= 0 &&
72
72
  Array.isArray(value.data) && value.data.length <= 100 && value.data.every(row =>
73
- kind === "statuses" ? organizationRecord(row) : object(row) && Number.isFinite(row.quantity) && object(row.unit) &&
74
- (kind === "balances" ? object(row.absencePlan) : typeof row.date === "string" && object(row.status)));
73
+ kind === "statuses" ? organizationRecord(row) : isRecord(row) && Number.isFinite(row.quantity) && isRecord(row.unit) &&
74
+ (kind === "balances" ? isRecord(row.absencePlan) : typeof row.date === "string" && isRecord(row.status)));
75
75
  }
76
76
  };
77
77
  }
@@ -112,7 +112,7 @@ const workdayProvider = Object.freeze({
112
112
  if (url.href.length > 16384) throw new ConnectorError("connector_input_invalid", "Use a smaller report prompt request.", { statusCode: 422, messageCode: "connectors.workday.useSmallerReportPromptRequest" });
113
113
  return { method: "GET", url: url.href };
114
114
  },
115
- validateResult: result => object(result) && !result.error && !result.errors && Array.isArray(result.Report_Entry) && result.Report_Entry.every(object)
115
+ validateResult: result => isRecord(result) && !result.error && !result.errors && Array.isArray(result.Report_Entry) && result.Report_Entry.every(isRecord)
116
116
  },
117
117
  "timeOff.balances": absenceRead("balances"),
118
118
  "timeOff.details": absenceRead("details"),
@@ -129,7 +129,7 @@ const workdayProvider = Object.freeze({
129
129
  offset: { type: "integer", min: 0, max: Number.MAX_SAFE_INTEGER, defaultTo: 0 },
130
130
  search: { type: "string", minLength: 1, maxLength: 512, noTrim: true },
131
131
  includeTerminatedWorkers: { type: "boolean" }, filterByOrgVisibility: { type: "boolean" }
132
- }, (result) => object(result) && !result.error && !result.errors && Number.isSafeInteger(result.total) && result.total >= 0 &&
132
+ }, (result) => isRecord(result) && !result.error && !result.errors && Number.isSafeInteger(result.total) && result.total >= 0 &&
133
133
  Array.isArray(result.data) && result.data.length <= 100 && result.data.every(worker))
134
134
  }
135
135
  });
@@ -2,6 +2,7 @@ import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
3
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
4
  import { xTwitterDefinition } from "../shared/x-twitter.js";
5
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
5
6
 
6
7
  const validId = (value) => typeof value === "string" && /^[0-9]{1,19}$/u.test(value);
7
8
  const validUsername = (value) => typeof value === "string" && /^[A-Za-z0-9_]{1,15}$/u.test(value);
@@ -21,7 +22,7 @@ const searchSchema = createSchema({
21
22
  nextToken: { type: "string", noTrim: true, validator: (value) => validCursor(value) || "Use the returned next token." }
22
23
  });
23
24
 
24
- const validEnvelope = (value) => value && typeof value === "object" && !Array.isArray(value) && (value.errors === undefined || Array.isArray(value.errors));
25
+ const validEnvelope = (value) => isRecord(value) && (value.errors === undefined || Array.isArray(value.errors));
25
26
  // The profile must be the username the request asked for.
26
27
  const validUser = (value, request) => validEnvelope(value) && validId(value.data?.id) && validUsername(value.data?.username) &&
27
28
  typeof value.data.name === "string" && value.data.username.toLowerCase() === new URL(request.url).pathname.split("/").at(-1).toLowerCase();