alchemy 0.84.0 → 0.85.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/bin/alchemy.js +32 -23
  2. package/lib/cloudflare/account-api-token.d.ts +5 -0
  3. package/lib/cloudflare/account-api-token.d.ts.map +1 -1
  4. package/lib/cloudflare/account-api-token.js +13 -2
  5. package/lib/cloudflare/account-api-token.js.map +1 -1
  6. package/lib/cloudflare/ai-crawler.d.ts +27 -0
  7. package/lib/cloudflare/ai-crawler.d.ts.map +1 -0
  8. package/lib/cloudflare/ai-crawler.js +75 -0
  9. package/lib/cloudflare/ai-crawler.js.map +1 -0
  10. package/lib/cloudflare/ai-search-token.d.ts +150 -0
  11. package/lib/cloudflare/ai-search-token.d.ts.map +1 -0
  12. package/lib/cloudflare/ai-search-token.js +171 -0
  13. package/lib/cloudflare/ai-search-token.js.map +1 -0
  14. package/lib/cloudflare/ai-search.d.ts +397 -0
  15. package/lib/cloudflare/ai-search.d.ts.map +1 -0
  16. package/lib/cloudflare/ai-search.js +307 -0
  17. package/lib/cloudflare/ai-search.js.map +1 -0
  18. package/lib/cloudflare/api-error.d.ts +11 -0
  19. package/lib/cloudflare/api-error.d.ts.map +1 -1
  20. package/lib/cloudflare/api-error.js +11 -0
  21. package/lib/cloudflare/api-error.js.map +1 -1
  22. package/lib/cloudflare/api.js +1 -1
  23. package/lib/cloudflare/api.js.map +1 -1
  24. package/lib/cloudflare/auth.d.ts +3 -2
  25. package/lib/cloudflare/auth.d.ts.map +1 -1
  26. package/lib/cloudflare/auth.js +17 -14
  27. package/lib/cloudflare/auth.js.map +1 -1
  28. package/lib/cloudflare/bindings.d.ts +12 -2
  29. package/lib/cloudflare/bindings.d.ts.map +1 -1
  30. package/lib/cloudflare/bindings.js.map +1 -1
  31. package/lib/cloudflare/bound.d.ts +2 -1
  32. package/lib/cloudflare/bound.d.ts.map +1 -1
  33. package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -1
  34. package/lib/cloudflare/bucket-custom-domain.js +4 -2
  35. package/lib/cloudflare/bucket-custom-domain.js.map +1 -1
  36. package/lib/cloudflare/bucket.d.ts.map +1 -1
  37. package/lib/cloudflare/bucket.js +8 -2
  38. package/lib/cloudflare/bucket.js.map +1 -1
  39. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  40. package/lib/cloudflare/d1-database.js +9 -1
  41. package/lib/cloudflare/d1-database.js.map +1 -1
  42. package/lib/cloudflare/index.d.ts +4 -0
  43. package/lib/cloudflare/index.d.ts.map +1 -1
  44. package/lib/cloudflare/index.js +4 -0
  45. package/lib/cloudflare/index.js.map +1 -1
  46. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  47. package/lib/cloudflare/miniflare/build-worker-options.js +15 -0
  48. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  49. package/lib/cloudflare/miniflare/miniflare-controller.d.ts +1 -1
  50. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  51. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  52. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +41 -3
  53. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  54. package/lib/cloudflare/permission-groups.d.ts +5 -0
  55. package/lib/cloudflare/permission-groups.d.ts.map +1 -1
  56. package/lib/cloudflare/permission-groups.js +6 -0
  57. package/lib/cloudflare/permission-groups.js.map +1 -1
  58. package/lib/cloudflare/vpc-service.d.ts +288 -0
  59. package/lib/cloudflare/vpc-service.d.ts.map +1 -0
  60. package/lib/cloudflare/vpc-service.js +238 -0
  61. package/lib/cloudflare/vpc-service.js.map +1 -0
  62. package/lib/cloudflare/worker-metadata.d.ts +1 -0
  63. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  64. package/lib/cloudflare/worker-metadata.js +8 -0
  65. package/lib/cloudflare/worker-metadata.js.map +1 -1
  66. package/lib/cloudflare/worker.d.ts +9 -0
  67. package/lib/cloudflare/worker.d.ts.map +1 -1
  68. package/lib/cloudflare/worker.js.map +1 -1
  69. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  70. package/lib/cloudflare/wrangler.json.js +9 -0
  71. package/lib/cloudflare/wrangler.json.js.map +1 -1
  72. package/lib/planetscale/api/sdk.gen.d.ts +874 -190
  73. package/lib/planetscale/api/sdk.gen.d.ts.map +1 -1
  74. package/lib/planetscale/api/sdk.gen.js +1250 -306
  75. package/lib/planetscale/api/sdk.gen.js.map +1 -1
  76. package/lib/planetscale/api/types.gen.d.ts +8241 -4128
  77. package/lib/planetscale/api/types.gen.d.ts.map +1 -1
  78. package/lib/planetscale/branch.js +4 -4
  79. package/lib/planetscale/branch.js.map +1 -1
  80. package/lib/planetscale/database.d.ts +43 -24
  81. package/lib/planetscale/database.d.ts.map +1 -1
  82. package/lib/planetscale/database.js +39 -18
  83. package/lib/planetscale/database.js.map +1 -1
  84. package/lib/planetscale/default-role.d.ts +51 -0
  85. package/lib/planetscale/default-role.d.ts.map +1 -0
  86. package/lib/planetscale/default-role.js +100 -0
  87. package/lib/planetscale/default-role.js.map +1 -0
  88. package/lib/planetscale/index.d.ts +1 -0
  89. package/lib/planetscale/index.d.ts.map +1 -1
  90. package/lib/planetscale/index.js +1 -0
  91. package/lib/planetscale/index.js.map +1 -1
  92. package/lib/planetscale/organization.js +1 -1
  93. package/lib/planetscale/organization.js.map +1 -1
  94. package/lib/planetscale/role.d.ts +1 -1
  95. package/lib/planetscale/role.d.ts.map +1 -1
  96. package/lib/planetscale/role.js +1 -1
  97. package/lib/planetscale/role.js.map +1 -1
  98. package/lib/planetscale/utils.d.ts +1 -1
  99. package/lib/planetscale/utils.d.ts.map +1 -1
  100. package/lib/planetscale/utils.js +9 -10
  101. package/lib/planetscale/utils.js.map +1 -1
  102. package/lib/scope.d.ts +1 -1
  103. package/lib/scope.d.ts.map +1 -1
  104. package/lib/scope.js +10 -5
  105. package/lib/scope.js.map +1 -1
  106. package/lib/state/sqlite-state-store.js +2 -0
  107. package/lib/state/sqlite-state-store.js.map +1 -1
  108. package/lib/test/bun.d.ts +17 -0
  109. package/lib/test/bun.d.ts.map +1 -1
  110. package/lib/test/bun.js +1 -0
  111. package/lib/test/bun.js.map +1 -1
  112. package/lib/test/vitest.d.ts +17 -0
  113. package/lib/test/vitest.d.ts.map +1 -1
  114. package/lib/test/vitest.js +3 -0
  115. package/lib/test/vitest.js.map +1 -1
  116. package/lib/util/snake-to-camel.d.ts +21 -0
  117. package/lib/util/snake-to-camel.d.ts.map +1 -0
  118. package/lib/util/snake-to-camel.js +34 -0
  119. package/lib/util/snake-to-camel.js.map +1 -0
  120. package/package.json +2 -2
  121. package/src/cloudflare/account-api-token.ts +23 -6
  122. package/src/cloudflare/ai-crawler.ts +89 -0
  123. package/src/cloudflare/ai-search-token.ts +324 -0
  124. package/src/cloudflare/ai-search.ts +952 -0
  125. package/src/cloudflare/api-error.ts +19 -0
  126. package/src/cloudflare/api.ts +1 -1
  127. package/src/cloudflare/auth.ts +17 -14
  128. package/src/cloudflare/bindings.ts +15 -2
  129. package/src/cloudflare/bound.ts +6 -3
  130. package/src/cloudflare/bucket-custom-domain.ts +4 -1
  131. package/src/cloudflare/bucket.ts +9 -2
  132. package/src/cloudflare/d1-database.ts +11 -4
  133. package/src/cloudflare/index.ts +4 -0
  134. package/src/cloudflare/miniflare/build-worker-options.ts +30 -18
  135. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +47 -2
  136. package/src/cloudflare/permission-groups.ts +6 -0
  137. package/src/cloudflare/vpc-service.ts +466 -0
  138. package/src/cloudflare/worker-metadata.ts +8 -0
  139. package/src/cloudflare/worker.ts +9 -0
  140. package/src/cloudflare/wrangler.json.ts +8 -0
  141. package/src/planetscale/api/sdk.gen.ts +2331 -987
  142. package/src/planetscale/api/types.gen.ts +8980 -4574
  143. package/src/planetscale/branch.ts +4 -4
  144. package/src/planetscale/database.ts +87 -46
  145. package/src/planetscale/default-role.ts +162 -0
  146. package/src/planetscale/index.ts +1 -0
  147. package/src/planetscale/organization.ts +1 -1
  148. package/src/planetscale/role.ts +3 -2
  149. package/src/planetscale/utils.ts +10 -10
  150. package/src/scope.ts +14 -5
  151. package/src/state/sqlite-state-store.ts +2 -0
  152. package/src/test/bun.ts +25 -0
  153. package/src/test/vitest.ts +33 -0
  154. package/src/util/snake-to-camel.ts +59 -0
  155. package/workers/tunnel-proxy.js +1 -1
@@ -0,0 +1,952 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { logger } from "../util/logger.ts";
4
+ import { poll } from "../util/poll.ts";
5
+ import { sleep } from "../util/sleep.ts";
6
+ import {
7
+ snakeToCamelObjectDeep,
8
+ type SnakeToCamel,
9
+ } from "../util/snake-to-camel.ts";
10
+ import { AiSearchToken } from "./ai-search-token.ts";
11
+ import { CloudflareApiError, isCloudflareApiError } from "./api-error.ts";
12
+ import {
13
+ extractCloudflareResult,
14
+ type CloudflareApiErrorPayload,
15
+ } from "./api-response.ts";
16
+ import {
17
+ createCloudflareApi,
18
+ type CloudflareApi,
19
+ type CloudflareApiOptions,
20
+ } from "./api.ts";
21
+ import {
22
+ getBucket,
23
+ isBucket,
24
+ type R2Bucket,
25
+ type R2BucketJurisdiction,
26
+ } from "./bucket.ts";
27
+ import { deleteIndex } from "./vectorize-index.ts";
28
+
29
+ interface BaseAiSearchProps extends CloudflareApiOptions {
30
+ /**
31
+ * Name of the AI Search instance
32
+ * @default `${app}-${stage}-${id}`
33
+ * @minLength 1
34
+ * @maxLength 32
35
+ */
36
+ name?: string;
37
+
38
+ /**
39
+ * Data source for indexing.
40
+ * Can be an R2Bucket directly, an R2 source config, or a web crawler config.
41
+ */
42
+ source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource;
43
+
44
+ /**
45
+ * Text generation model for AI responses
46
+ *
47
+ * @default "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
48
+ */
49
+ aiSearchModel?: AiSearch.Model;
50
+
51
+ /**
52
+ * Embedding model for vectorization
53
+ *
54
+ * @default "@cf/baai/bge-m3"
55
+ */
56
+ embeddingModel?: AiSearch.EmbeddingModel;
57
+
58
+ /**
59
+ * Enable chunking of source documents
60
+ *
61
+ * @default true
62
+ */
63
+ chunk?: boolean;
64
+
65
+ /**
66
+ * Size of each chunk (minimum 64)
67
+ *
68
+ * @default 256
69
+ */
70
+ chunkSize?: number;
71
+
72
+ /**
73
+ * Overlap between chunks (0-30)
74
+ *
75
+ * @default 10
76
+ */
77
+ chunkOverlap?: number;
78
+
79
+ /**
80
+ * Maximum search results (1-50)
81
+ *
82
+ * @default 10
83
+ */
84
+ maxNumResults?: number;
85
+
86
+ /**
87
+ * Minimum match score (0-1)
88
+ *
89
+ * @default 0.4
90
+ */
91
+ scoreThreshold?: number;
92
+
93
+ /**
94
+ * Enable result reranking
95
+ *
96
+ * @default false
97
+ */
98
+ reranking?: boolean;
99
+
100
+ /**
101
+ * Reranking model
102
+ *
103
+ * @default "@cf/baai/bge-reranker-base"
104
+ */
105
+ rerankingModel?: AiSearch.RerankingModel;
106
+
107
+ /**
108
+ * Enable query rewriting for better retrieval
109
+ *
110
+ * @default false
111
+ */
112
+ rewriteQuery?: boolean;
113
+
114
+ /**
115
+ * Query rewriting model
116
+ *
117
+ * @default "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
118
+ */
119
+ rewriteModel?: AiSearch.Model;
120
+
121
+ /**
122
+ * Enable similarity caching
123
+ *
124
+ * @default false
125
+ */
126
+ cache?: boolean;
127
+
128
+ /**
129
+ * Cache similarity threshold
130
+ *
131
+ * @default "close_enough"
132
+ */
133
+ cacheThreshold?:
134
+ | "super_strict_match"
135
+ | "close_enough"
136
+ | "flexible_friend"
137
+ | "anything_goes";
138
+
139
+ /**
140
+ * Custom metadata
141
+ */
142
+ metadata?: Record<string, unknown>;
143
+
144
+ /**
145
+ * Whether to index the source documents when the AI Search instance is created
146
+ * @default true
147
+ */
148
+ indexOnCreate?: boolean;
149
+
150
+ /**
151
+ * Whether to delete the AI Search instance when removed from Alchemy
152
+ * @default true
153
+ */
154
+ delete?: boolean;
155
+
156
+ /**
157
+ * Whether to adopt the AI Search instance if it already exists
158
+ * @default false
159
+ */
160
+ adopt?: boolean;
161
+ }
162
+
163
+ export type AiSearchProps = BaseAiSearchProps &
164
+ (
165
+ | {
166
+ token?: AiSearchToken;
167
+ }
168
+ | {
169
+ tokenId: string;
170
+ }
171
+ );
172
+
173
+ export interface AiSearchR2Source {
174
+ /**
175
+ * Source type
176
+ */
177
+ type: "r2";
178
+
179
+ /**
180
+ * R2 bucket - can be bucket name string or R2Bucket resource
181
+ */
182
+ bucket: string | R2Bucket;
183
+
184
+ /**
185
+ * Jurisdiction for the R2 bucket
186
+ * @default "default"
187
+ */
188
+ jurisdiction?: R2BucketJurisdiction;
189
+
190
+ /**
191
+ * Prefix for included items from the R2 bucket
192
+ */
193
+ prefix?: string;
194
+
195
+ /**
196
+ * Path patterns to include in the R2 bucket (up to 10 patterns).
197
+ * Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
198
+ */
199
+ includePaths?: string[];
200
+
201
+ /**
202
+ * Path patterns to exclude from the R2 bucket (up to 10 patterns).
203
+ * Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
204
+ */
205
+ excludePaths?: string[];
206
+ }
207
+
208
+ export interface AiSearchWebCrawlerSource {
209
+ /**
210
+ * Source type
211
+ */
212
+ type: "web-crawler";
213
+
214
+ /**
215
+ * Domain to crawl. Must be a domain that is onboarded to your Cloudflare account
216
+ * (added as a zone with active nameservers pointing to Cloudflare).
217
+ *
218
+ * Can be provided as just the domain (e.g., "docs.example.com") or with protocol
219
+ * (e.g., "https://docs.example.com") - the protocol will be stripped automatically.
220
+ *
221
+ * @example "docs.example.com"
222
+ * @example "https://example.com" // Protocol will be stripped
223
+ */
224
+ domain: string;
225
+
226
+ /**
227
+ * Path patterns to include in crawling (up to 10 patterns).
228
+ * Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
229
+ */
230
+ includePaths?: string[];
231
+
232
+ /**
233
+ * Path patterns to exclude from crawling (up to 10 patterns).
234
+ * Supports wildcards: `*` matches any characters except `/`, `**` matches any characters including `/`.
235
+ */
236
+ excludePaths?: string[];
237
+
238
+ parseOptions?: {
239
+ include_headers?: Record<string, string>;
240
+ include_images?: boolean;
241
+ specific_sitemaps?: string[];
242
+ use_browser_rendering?: boolean;
243
+ };
244
+ parseType?: "sitemap" | "feed-rss";
245
+ storeOptions?: {
246
+ storage_id: string;
247
+ jurisdiction?: R2BucketJurisdiction;
248
+ storage_type?: "r2";
249
+ };
250
+ }
251
+
252
+ export type AiSearch = SnakeToCamel<AiSearch.ApiResponse> & {
253
+ /**
254
+ * The name of the AI Search instance (this is an alias for the `id` property)
255
+ */
256
+ name: string;
257
+ };
258
+
259
+ export const AiSearch = Resource(
260
+ "cloudflare::AiSearch",
261
+ async function (
262
+ this: Context<AiSearch>,
263
+ id: string,
264
+ props: AiSearchProps,
265
+ ): Promise<AiSearch> {
266
+ const api = await createCloudflareApi(props);
267
+ const adopt = props.adopt ?? this.scope.adopt;
268
+
269
+ const validateBucketSource = async (
270
+ bucket: R2Bucket | string,
271
+ jurisdiction: R2BucketJurisdiction = "default",
272
+ ) => {
273
+ let name: string;
274
+ if (typeof bucket === "string") {
275
+ name = bucket;
276
+ } else {
277
+ if (this.scope.local && !bucket.dev?.remote) {
278
+ throw new Error(
279
+ [
280
+ `AI Search "${id}" depends on an R2Bucket that is running locally, but AI Search requires the bucket to be deployed.`,
281
+ "Add `dev: { remote: true }` to the R2Bucket to use it with AI Search.",
282
+ ].join("\n"),
283
+ );
284
+ }
285
+ name = bucket.name;
286
+ }
287
+ try {
288
+ await getBucket(api, name, { jurisdiction });
289
+ } catch (error) {
290
+ throw new Error(
291
+ `Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${error instanceof Error ? error.message : String(error)}`,
292
+ { cause: error },
293
+ );
294
+ }
295
+ };
296
+ const normalizeSource = async (
297
+ source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource,
298
+ ): Promise<
299
+ (AiSearchR2Source & { bucket: string }) | AiSearchWebCrawlerSource
300
+ > => {
301
+ if (isBucket(source)) {
302
+ await validateBucketSource(source, source.jurisdiction);
303
+ return {
304
+ type: "r2",
305
+ bucket: source.name,
306
+ jurisdiction: source.jurisdiction,
307
+ };
308
+ } else if (source.type === "r2" && isBucket(source.bucket)) {
309
+ await validateBucketSource(source.bucket, source.bucket.jurisdiction);
310
+ return {
311
+ ...source,
312
+ type: "r2",
313
+ bucket: source.bucket.name,
314
+ jurisdiction: source.bucket.jurisdiction,
315
+ };
316
+ } else if (source.type === "web-crawler") {
317
+ await validateWebCrawlerSourceDomain(api, source.domain);
318
+ return source;
319
+ } else {
320
+ await validateBucketSource(source.bucket, source.jurisdiction);
321
+ return source as AiSearchR2Source & { bucket: string };
322
+ }
323
+ };
324
+ const validateTokenId = (id: string): void => {
325
+ if (
326
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
327
+ id,
328
+ )
329
+ ) {
330
+ throw new Error(
331
+ `Invalid token ID: "${id}"\n` +
332
+ "The token ID must be a valid UUID for an AI Search service token.\n" +
333
+ "See https://developers.cloudflare.com/ai-search/get-started/api/#2-create-a-service-api-token",
334
+ );
335
+ }
336
+ };
337
+ const normalizeTokenId = async (): Promise<string> => {
338
+ if ("tokenId" in props) {
339
+ validateTokenId(props.tokenId);
340
+ return props.tokenId;
341
+ } else if (props.token) {
342
+ validateTokenId(props.token.tokenId);
343
+ return props.token.tokenId;
344
+ } else {
345
+ const token = await AiSearchToken("token", {
346
+ baseUrl: props.baseUrl,
347
+ profile: props.profile,
348
+ apiKey: props.apiKey,
349
+ apiToken: props.apiToken,
350
+ accountId: props.accountId,
351
+ email: props.email,
352
+ adopt: props.adopt,
353
+ delete: props.delete,
354
+ });
355
+ return token.tokenId;
356
+ }
357
+ };
358
+
359
+ if (this.phase === "delete") {
360
+ if (props.delete !== false && this.output?.id) {
361
+ await deleteIndex(api, this.output.vectorizeName);
362
+ await deleteAiSearchInstance(api, this.output.id);
363
+ }
364
+ return this.destroy();
365
+ }
366
+
367
+ const name = props.name ?? this.scope.createPhysicalName(id, "-", 32);
368
+ if (name.length < 1 || name.length > 32) {
369
+ throw new Error(
370
+ `AI Search instance name must be 1-32 characters, got ${name.length} ("${name}")`,
371
+ );
372
+ }
373
+ const [source, tokenId] = await Promise.all([
374
+ normalizeSource(props.source),
375
+ normalizeTokenId(),
376
+ ]);
377
+
378
+ const payload: AiSearch.ApiPayload = {
379
+ id: name,
380
+ source: source.type === "r2" ? source.bucket : source.domain,
381
+ type: source.type,
382
+ ai_search_model: props.aiSearchModel,
383
+ source_params: {
384
+ include_items: source.includePaths,
385
+ exclude_items: source.excludePaths,
386
+ ...(source.type === "r2"
387
+ ? {
388
+ r2_jurisdiction:
389
+ source.jurisdiction !== "default"
390
+ ? source.jurisdiction
391
+ : undefined,
392
+ prefix: source.prefix,
393
+ }
394
+ : {
395
+ web_crawler: {
396
+ parse_type: source.parseType,
397
+ parse_options: source.parseOptions,
398
+ store_options: source.storeOptions,
399
+ },
400
+ }),
401
+ },
402
+ embedding_model: props.embeddingModel,
403
+ chunk: props.chunk,
404
+ chunk_size: props.chunkSize,
405
+ chunk_overlap: props.chunkOverlap,
406
+ max_num_results: props.maxNumResults,
407
+ score_threshold: props.scoreThreshold,
408
+ reranking: props.reranking,
409
+ reranking_model: props.rerankingModel,
410
+ rewrite_query: props.rewriteQuery,
411
+ rewrite_model: props.rewriteModel,
412
+ cache: props.cache,
413
+ cache_threshold: props.cacheThreshold,
414
+ metadata: props.metadata,
415
+ token_id: tokenId,
416
+ };
417
+
418
+ let instance: AiSearch.ApiResponse;
419
+ if (this.phase === "update" && this.output?.id) {
420
+ const replace =
421
+ "source" in this.output &&
422
+ (payload.type !== this.output.type ||
423
+ payload.source !== this.output.source);
424
+ // the development version of this resource had different properties, so check those to avoid an unnecessary replacement
425
+ const replaceLegacy =
426
+ "sourceType" in this.output &&
427
+ (payload.type !== this.output.sourceType ||
428
+ (payload.type === "r2" &&
429
+ "sourceBucket" in this.output &&
430
+ payload.source !== this.output.sourceBucket) ||
431
+ (payload.type === "web-crawler" &&
432
+ "sourceDomain" in this.output &&
433
+ payload.source !== this.output.sourceDomain));
434
+ if (replace || replaceLegacy) {
435
+ return this.replace(true);
436
+ }
437
+ instance = await updateAiSearchInstance(api, this.output.id, payload);
438
+ } else {
439
+ try {
440
+ instance = await createAiSearchInstance(api, payload);
441
+ } catch (error) {
442
+ const isAlreadyExistsError =
443
+ error instanceof CloudflareApiError &&
444
+ error.status === 400 &&
445
+ (error.errorData as CloudflareApiErrorPayload[]).some(
446
+ (error) => error.code === 7022,
447
+ );
448
+ if (isAlreadyExistsError && adopt) {
449
+ instance = await getAiSearchInstance(api, name);
450
+ instance = await updateAiSearchInstance(api, instance.id, payload);
451
+ } else {
452
+ throw error;
453
+ }
454
+ }
455
+ if (props.indexOnCreate !== false) {
456
+ await runAiSearchJob(api, instance.id, (message) =>
457
+ logger.task(id, {
458
+ prefix: "index",
459
+ prefixColor: "gray",
460
+ resource: id,
461
+ message,
462
+ }),
463
+ );
464
+ }
465
+ }
466
+ return {
467
+ ...snakeToCamelObjectDeep(instance),
468
+ name: instance.id,
469
+ };
470
+ },
471
+ );
472
+
473
+ /**
474
+ * Validate that a domain string is a valid domain format (not a URL).
475
+ * Throws a helpful error if the input looks like a URL.
476
+ */
477
+ async function validateWebCrawlerSourceDomain(
478
+ api: CloudflareApi,
479
+ domain: string,
480
+ ): Promise<void> {
481
+ if (domain.includes("://")) {
482
+ throw new Error(
483
+ `Invalid domain format "${domain}". Provide just the domain (e.g., "docs.example.com"), not a URL. ` +
484
+ `For URL-based crawling, use AiCrawler instead of AiSearch.`,
485
+ );
486
+ }
487
+ if (domain.includes("/")) {
488
+ throw new Error(
489
+ `Invalid domain format "${domain}". Provide just the domain without paths (e.g., "docs.example.com"). ` +
490
+ `Use includePaths to filter specific paths, or use AiCrawler for URL-based crawling.`,
491
+ );
492
+ }
493
+
494
+ // The Cloudflare dashboard uses this undocumented endpoint to validate domains.
495
+ // If we don't validate here, the create request fails with a 500 error.
496
+ const response = await api.post(
497
+ `/accounts/${api.accountId}/ai-search/domains`,
498
+ {
499
+ domain,
500
+ },
501
+ );
502
+ // The error message is more like an error code. This mapping is from the Cloudflare dashboard.
503
+ const errorMap = {
504
+ not_a_valid_domain: "Not a valid domain.",
505
+ invalid_domain:
506
+ "Invalid domain. The domain needs to belong to this account.",
507
+ fail_to_find_domain_info: "Failed to find domain information.",
508
+ missing_sitemap: "Sitemap not found. Please check your robots.txt.",
509
+ domain_not_owned_by_user: "The domain needs to belong to this account.",
510
+ forbidden_robots_txt:
511
+ "Failed to fetch robots.txt: The file is inaccessible.",
512
+ forbidden_sitemap:
513
+ "Failed to fetch your sitemap: The file is inaccessible.",
514
+ };
515
+ const json = (await response.json()) as {
516
+ success: boolean;
517
+ errors: Array<{ code: number; message: string }>;
518
+ };
519
+ if (json.success) return;
520
+ throw new Error(
521
+ [
522
+ `Failed to validate domain "${domain}" (${response.status}):`,
523
+ ...json.errors.map(
524
+ (e) =>
525
+ `- [${e.code}] ${
526
+ e.message in errorMap
527
+ ? errorMap[e.message as keyof typeof errorMap]
528
+ : e.message
529
+ }`,
530
+ ),
531
+ "Learn more: https://developers.cloudflare.com/ai-search/configuration/data-source/website/",
532
+ ].join("\n"),
533
+ );
534
+ }
535
+
536
+ export declare namespace AiSearch {
537
+ type Model =
538
+ | "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
539
+ | "@cf/meta/llama-3.1-8b-instruct-fast"
540
+ | "@cf/meta/llama-3.1-8b-instruct-fp8"
541
+ | "@cf/meta/llama-4-scout-17b-16e-instruct"
542
+ | "@cf/qwen/qwen3-30b-a3b-fp8"
543
+ | "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b"
544
+ | "@cf/moonshotai/kimi-k2-instruct"
545
+ | "anthropic/claude-3-7-sonnet"
546
+ | "anthropic/claude-sonnet-4"
547
+ | "anthropic/claude-opus-4"
548
+ | "anthropic/claude-3-5-haiku"
549
+ | "cerebras/qwen-3-235b-a22b-instruct"
550
+ | "cerebras/qwen-3-235b-a22b-thinking"
551
+ | "cerebras/llama-3.3-70b"
552
+ | "cerebras/llama-4-maverick-17b-128e-instruct"
553
+ | "cerebras/llama-4-scout-17b-16e-instruct"
554
+ | "cerebras/gpt-oss-120b"
555
+ | "google-ai-studio/gemini-2.5-flash"
556
+ | "google-ai-studio/gemini-2.5-pro"
557
+ | "grok/grok-4"
558
+ | "groq/llama-3.3-70b-versatile"
559
+ | "groq/llama-3.1-8b-instant"
560
+ | "openai/gpt-5"
561
+ | "openai/gpt-5-mini"
562
+ | "openai/gpt-5-nano"
563
+ | (string & {});
564
+
565
+ type EmbeddingModel =
566
+ | "@cf/qwen/qwen3-embedding-0.6b"
567
+ | "@cf/baai/bge-m3"
568
+ | "@cf/baai/bge-large-en-v1.5"
569
+ | "@cf/google/embeddinggemma-300m"
570
+ | "google-ai-studio/gemini-embedding-001"
571
+ | "openai/text-embedding-3-small"
572
+ | "openai/text-embedding-3-large"
573
+ | (string & {});
574
+
575
+ type RerankingModel = "@cf/baai/bge-reranker-base" | (string & {});
576
+
577
+ interface ApiPayload {
578
+ id: string;
579
+ source: string;
580
+ type: "r2" | "web-crawler";
581
+ ai_gateway_id?: string;
582
+ ai_search_model?: Model;
583
+ cache?: boolean;
584
+ cache_threshold?:
585
+ | "super_strict_match"
586
+ | "close_enough"
587
+ | "flexible_friend"
588
+ | "anything_goes";
589
+ chunk?: boolean;
590
+ chunk_overlap?: number;
591
+ chunk_size?: number;
592
+ custom_metadata?: Array<{
593
+ data_type: "text" | "number" | "boolean";
594
+ /**
595
+ * @minLength 1
596
+ * @maxLength 64
597
+ */
598
+ field_name: string;
599
+ }>;
600
+ embedding_model?: EmbeddingModel;
601
+ hybrid_search_enabled?: boolean;
602
+ max_num_results?: number;
603
+ metadata?: {
604
+ created_from_aisearch_wizard?: boolean;
605
+ worker_domain?: string;
606
+ };
607
+ public_endpoint_params?: {
608
+ authorized_hosts?: string[];
609
+ chat_completions_endpoint?: {
610
+ disabled?: boolean;
611
+ };
612
+ enabled?: boolean;
613
+ mcp?: {
614
+ disabled?: boolean;
615
+ };
616
+ rate_limit?: {
617
+ /**
618
+ * Maximum: 3,600,000, Minimum: 60,000
619
+ */
620
+ period_ms?: number;
621
+ /**
622
+ * Minimum: 1
623
+ */
624
+ requests?: number;
625
+ technique?: "fixed" | "sliding";
626
+ };
627
+ search_endpoint?: {
628
+ disabled?: boolean;
629
+ };
630
+ };
631
+ reranking?: boolean;
632
+ reranking_model?: RerankingModel;
633
+ rewrite_model?: Model;
634
+ rewrite_query?: boolean;
635
+
636
+ /**
637
+ * Maximum: 1, Minimum: 0, Default: 0.4
638
+ */
639
+ score_threshold?: number;
640
+ source_params?: {
641
+ exclude_items?: string[];
642
+ include_items?: string[];
643
+ prefix?: string;
644
+ r2_jurisdiction?: string; // Default: "default"
645
+ web_crawler?: {
646
+ /**
647
+ * Default: {"parse_type":"sitemap"}
648
+ */
649
+ parse_options?: {
650
+ include_headers?: Record<string, string>;
651
+ include_images?: boolean;
652
+ specific_sitemaps?: string[]; // Only valid when parse_type is 'sitemap'
653
+ use_browser_rendering?: boolean;
654
+ };
655
+ parse_type?: "sitemap" | "feed-rss"; // Default: "sitemap"
656
+ store_options?: {
657
+ storage_id: string;
658
+ r2_jurisdiction?: string; // Default: "default"
659
+ storage_type?: "r2";
660
+ };
661
+ };
662
+ };
663
+ token_id?: string;
664
+ }
665
+
666
+ interface ApiResponse {
667
+ id: string;
668
+ account_id: string;
669
+ account_tag: string;
670
+ created_at: string;
671
+ internal_id: string;
672
+ modified_at: string;
673
+ source: string;
674
+ type: "r2" | "web-crawler";
675
+ vectorize_name: string;
676
+ ai_gateway_id?: string;
677
+ ai_search_model?: Model;
678
+ cache?: boolean; // default: true
679
+ cache_threshold?:
680
+ | "super_strict_match"
681
+ | "close_enough"
682
+ | "flexible_friend"
683
+ | "anything_goes"; // default: "close_enough"
684
+ chunk?: boolean; // default: true
685
+ chunk_overlap?: number; // maximum: 30, minimum: 0, default: 10
686
+ chunk_size?: number; // minimum: 64, default: 256
687
+ created_by?: string;
688
+ custom_metadata?: Array<{
689
+ data_type: "text" | "number" | "boolean";
690
+ field_name: string;
691
+ }>;
692
+ embedding_model?: EmbeddingModel;
693
+ enable?: boolean;
694
+ engine_version?: number; // default: 1
695
+ hybrid_search_enabled?: boolean;
696
+ last_activity?: string;
697
+ max_num_results?: number; // maximum: 50, minimum: 1, default: 10
698
+ metadata?: {
699
+ created_from_aisearch_wizard?: boolean;
700
+ worker_domain?: string;
701
+ };
702
+ modified_by?: string;
703
+ paused?: boolean;
704
+ public_endpoint_id?: string;
705
+ public_endpoint_params?: {
706
+ authorized_hosts?: string[];
707
+ chat_completions_endpoint?: {
708
+ disabled?: boolean;
709
+ };
710
+ enabled?: boolean;
711
+ mcp?: {
712
+ disabled?: boolean;
713
+ };
714
+ rate_limit?: {
715
+ period_ms?: number; // maximum: 3600000, minimum: 60000
716
+ requests?: number;
717
+ technique?: "fixed" | "sliding";
718
+ };
719
+ search_endpoint?: {
720
+ disabled?: boolean;
721
+ };
722
+ };
723
+ reranking?: boolean;
724
+ reranking_model?: RerankingModel;
725
+ rewrite_model?: Model;
726
+ rewrite_query?: boolean;
727
+ score_threshold?: number;
728
+ source_params?: {
729
+ exclude_items?: string[];
730
+ include_items?: string[];
731
+ prefix?: string;
732
+ r2_jurisdiction?: string; // default: "default"
733
+ web_crawler?: {
734
+ parse_options?: {
735
+ include_headers?: Record<string, string>;
736
+ include_images?: boolean;
737
+ specific_sitemaps?: string[]; // valid with 'sitemap' parse_type
738
+ use_browser_rendering?: boolean;
739
+ };
740
+ parse_type?: "sitemap" | "feed-rss"; // default: "sitemap"
741
+ store_options?: {
742
+ storage_id: string;
743
+ r2_jurisdiction?: string; // default: "default"
744
+ storage_type?: "r2";
745
+ };
746
+ };
747
+ };
748
+ status?: "waiting" | "ready" | "indexing" | "error";
749
+ summarization?: boolean;
750
+ summarization_model?:
751
+ | "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
752
+ | "@cf/meta/llama-3.1-8b-instruct-fast"
753
+ | "@cf/meta/llama-3.1-8b-instruct-fp8"
754
+ | "@cf/meta/llama-4-scout-17b-16e-instruct"
755
+ | "@cf/qwen/qwen3-30b-a3b-fp8"
756
+ | "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b"
757
+ | "@cf/moonshotai/kimi-k2-instruct"
758
+ | "anthropic/claude-3-7-sonnet"
759
+ | "anthropic/claude-sonnet-4"
760
+ | "anthropic/claude-opus-4"
761
+ | "anthropic/claude-3-5-haiku"
762
+ | "cerebras/qwen-3-235b-a22b-instruct"
763
+ | "cerebras/qwen-3-235b-a22b-thinking"
764
+ | "cerebras/llama-3.3-70b"
765
+ | "cerebras/llama-4-maverick-17b-128e-instruct"
766
+ | "cerebras/llama-4-scout-17b-16e-instruct"
767
+ | "cerebras/gpt-oss-120b"
768
+ | "google-ai-studio/gemini-2.5-flash"
769
+ | "google-ai-studio/gemini-2.5-pro"
770
+ | "grok/grok-4"
771
+ | "groq/llama-3.3-70b-versatile"
772
+ | "groq/llama-3.1-8b-instant"
773
+ | "openai/gpt-5"
774
+ | "openai/gpt-5-mini"
775
+ | "openai/gpt-5-nano"
776
+ | (string & {});
777
+ system_prompt_ai_search?: string;
778
+ system_prompt_index_summarization?: string;
779
+ system_prompt_rewrite_query?: string;
780
+ token_id?: string;
781
+ vectorize_active_namespace?: string;
782
+ }
783
+ }
784
+
785
+ export async function listAiSearchInstances(
786
+ api: CloudflareApi,
787
+ ): Promise<AiSearch.ApiResponse[]> {
788
+ return await extractCloudflareResult<AiSearch.ApiResponse[]>(
789
+ "list AI Search instances",
790
+ api.get(`/accounts/${api.accountId}/ai-search/instances`),
791
+ );
792
+ }
793
+
794
+ export async function createAiSearchInstance(
795
+ api: CloudflareApi,
796
+ payload: AiSearch.ApiPayload,
797
+ ): Promise<AiSearch.ApiResponse> {
798
+ return await extractCloudflareResult<AiSearch.ApiResponse>(
799
+ `create AI Search instance "${payload.id}"`,
800
+ api.post(`/accounts/${api.accountId}/ai-search/instances`, payload),
801
+ );
802
+ }
803
+
804
+ export async function getAiSearchInstance(
805
+ api: CloudflareApi,
806
+ id: string,
807
+ ): Promise<AiSearch.ApiResponse> {
808
+ return await extractCloudflareResult<AiSearch.ApiResponse>(
809
+ `get AI Search instance "${id}"`,
810
+ api.get(`/accounts/${api.accountId}/ai-search/instances/${id}`),
811
+ );
812
+ }
813
+
814
+ export async function updateAiSearchInstance(
815
+ api: CloudflareApi,
816
+ id: string,
817
+ payload: AiSearch.ApiPayload,
818
+ ): Promise<AiSearch.ApiResponse> {
819
+ return await extractCloudflareResult<AiSearch.ApiResponse>(
820
+ `update AI Search instance "${id}"`,
821
+ api.put(`/accounts/${api.accountId}/ai-search/instances/${id}`, payload),
822
+ );
823
+ }
824
+
825
+ export async function deleteAiSearchInstance(
826
+ api: CloudflareApi,
827
+ id: string,
828
+ ): Promise<void> {
829
+ try {
830
+ await extractCloudflareResult(
831
+ `delete AI Search instance "${id}"`,
832
+ api.delete(`/accounts/${api.accountId}/ai-search/instances/${id}`),
833
+ );
834
+ } catch (error) {
835
+ if (error instanceof CloudflareApiError && error.status === 404) {
836
+ return;
837
+ }
838
+ throw error;
839
+ }
840
+ }
841
+
842
+ interface AiSearchJobApiResponse {
843
+ id: string;
844
+ source: "user" | "schedule";
845
+ end_reason: string | null;
846
+ ended_at: string | null;
847
+ last_seen_at: string | null;
848
+ started_at: string | null;
849
+ }
850
+
851
+ export async function listAiSearchJobs(
852
+ api: CloudflareApi,
853
+ aiSearchId: string,
854
+ ): Promise<AiSearchJobApiResponse[]> {
855
+ return await extractCloudflareResult<AiSearchJobApiResponse[]>(
856
+ `list AI Search jobs for instance "${aiSearchId}"`,
857
+ api.get(
858
+ `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
859
+ ),
860
+ );
861
+ }
862
+
863
+ export async function createAiSearchJob(
864
+ api: CloudflareApi,
865
+ aiSearchId: string,
866
+ ): Promise<AiSearchJobApiResponse> {
867
+ return await extractCloudflareResult<AiSearchJobApiResponse>(
868
+ `create AI Search job for instance "${aiSearchId}"`,
869
+ api.post(
870
+ `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
871
+ {},
872
+ ),
873
+ );
874
+ }
875
+
876
+ export async function getAiSearchJob(
877
+ api: CloudflareApi,
878
+ aiSearchId: string,
879
+ jobId: string,
880
+ ): Promise<AiSearchJobApiResponse> {
881
+ return await extractCloudflareResult<AiSearchJobApiResponse>(
882
+ `get AI Search job "${jobId}" for instance "${aiSearchId}"`,
883
+ api.get(
884
+ `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}`,
885
+ ),
886
+ );
887
+ }
888
+
889
+ interface AiSearchJobLogItem {
890
+ id: number;
891
+ created_at: number;
892
+ message: string;
893
+ message_type: number;
894
+ }
895
+
896
+ export async function listAiSearchJobLogs(
897
+ api: CloudflareApi,
898
+ aiSearchId: string,
899
+ jobId: string,
900
+ ): Promise<AiSearchJobLogItem[]> {
901
+ try {
902
+ return await extractCloudflareResult<AiSearchJobLogItem[]>(
903
+ `list AI Search job logs for job "${jobId}" for instance "${aiSearchId}"`,
904
+ api.get(
905
+ `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
906
+ ),
907
+ );
908
+ } catch (error) {
909
+ if (
910
+ isCloudflareApiError(error, { code: 7002 }) // ai_search_not_found
911
+ ) {
912
+ return [];
913
+ }
914
+ throw error;
915
+ }
916
+ }
917
+
918
+ export async function runAiSearchJob(
919
+ api: CloudflareApi,
920
+ aiSearchId: string,
921
+ log: (message: string) => void,
922
+ ): Promise<void> {
923
+ log("Preparing to index...");
924
+ const job = await createAiSearchJob(api, aiSearchId);
925
+ let lastLogId = 0;
926
+ let done = false;
927
+ const resultPromise = poll({
928
+ description: `run AI Search job "${job.id}" for instance "${aiSearchId}"`,
929
+ fn: () => getAiSearchJob(api, aiSearchId, job.id),
930
+ predicate: (result) => result.ended_at !== null,
931
+ });
932
+ pollLogs();
933
+
934
+ const result = await resultPromise;
935
+ done = true;
936
+ log(`Sync completed: ${result.end_reason}`);
937
+
938
+ async function pollLogs() {
939
+ const logs = await listAiSearchJobLogs(api, aiSearchId, job.id);
940
+ for (let i = logs.length - 1; i >= 0; i--) {
941
+ const item = logs[i];
942
+ if (item.id > lastLogId) {
943
+ lastLogId = item.id;
944
+ log(item.message);
945
+ }
946
+ }
947
+ if (!done) {
948
+ await sleep(3000);
949
+ await pollLogs();
950
+ }
951
+ }
952
+ }