alchemy 0.92.2 → 0.93.0

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 (53) hide show
  1. package/bin/alchemy.js +1 -1
  2. package/lib/cloudflare/ai-search-namespace.d.ts +150 -0
  3. package/lib/cloudflare/ai-search-namespace.d.ts.map +1 -0
  4. package/lib/cloudflare/ai-search-namespace.js +281 -0
  5. package/lib/cloudflare/ai-search-namespace.js.map +1 -0
  6. package/lib/cloudflare/ai-search-token.d.ts +11 -1
  7. package/lib/cloudflare/ai-search-token.d.ts.map +1 -1
  8. package/lib/cloudflare/ai-search-token.js +35 -7
  9. package/lib/cloudflare/ai-search-token.js.map +1 -1
  10. package/lib/cloudflare/ai-search.d.ts +123 -18
  11. package/lib/cloudflare/ai-search.d.ts.map +1 -1
  12. package/lib/cloudflare/ai-search.js +239 -68
  13. package/lib/cloudflare/ai-search.js.map +1 -1
  14. package/lib/cloudflare/bindings.d.ts +29 -2
  15. package/lib/cloudflare/bindings.d.ts.map +1 -1
  16. package/lib/cloudflare/bindings.js.map +1 -1
  17. package/lib/cloudflare/bound.d.ts +4 -2
  18. package/lib/cloudflare/bound.d.ts.map +1 -1
  19. package/lib/cloudflare/container.d.ts +79 -6
  20. package/lib/cloudflare/container.d.ts.map +1 -1
  21. package/lib/cloudflare/container.js +117 -2
  22. package/lib/cloudflare/container.js.map +1 -1
  23. package/lib/cloudflare/index.d.ts +1 -0
  24. package/lib/cloudflare/index.d.ts.map +1 -1
  25. package/lib/cloudflare/index.js +1 -0
  26. package/lib/cloudflare/index.js.map +1 -1
  27. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  28. package/lib/cloudflare/miniflare/build-worker-options.js +37 -0
  29. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  30. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  31. package/lib/cloudflare/worker-metadata.js +25 -1
  32. package/lib/cloudflare/worker-metadata.js.map +1 -1
  33. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  34. package/lib/cloudflare/wrangler.json.js +32 -0
  35. package/lib/cloudflare/wrangler.json.js.map +1 -1
  36. package/lib/docker/api.d.ts +4 -1
  37. package/lib/docker/api.d.ts.map +1 -1
  38. package/lib/docker/api.js +8 -2
  39. package/lib/docker/api.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/cloudflare/ai-search-namespace.ts +439 -0
  42. package/src/cloudflare/ai-search-token.ts +43 -9
  43. package/src/cloudflare/ai-search.ts +353 -73
  44. package/src/cloudflare/bindings.ts +33 -0
  45. package/src/cloudflare/bound.ts +87 -74
  46. package/src/cloudflare/container.ts +228 -11
  47. package/src/cloudflare/index.ts +1 -0
  48. package/src/cloudflare/miniflare/build-worker-options.ts +39 -0
  49. package/src/cloudflare/worker-metadata.ts +25 -1
  50. package/src/cloudflare/wrangler.json.ts +32 -0
  51. package/src/docker/api.ts +11 -2
  52. package/workers/cloudflare-state-store.js +57 -57
  53. package/workers/tunnel-proxy.js +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { Context } from "../context.ts";
2
- import { Resource } from "../resource.ts";
2
+ import { Resource, ResourceKind } from "../resource.ts";
3
3
  import { logger } from "../util/logger.ts";
4
4
  import { poll } from "../util/poll.ts";
5
5
  import { sleep } from "../util/sleep.ts";
@@ -7,6 +7,10 @@ import {
7
7
  snakeToCamelObjectDeep,
8
8
  type SnakeToCamel,
9
9
  } from "../util/snake-to-camel.ts";
10
+ import {
11
+ type AiSearchNamespace,
12
+ isAiSearchNamespace,
13
+ } from "./ai-search-namespace.ts";
10
14
  import { AiSearchToken } from "./ai-search-token.ts";
11
15
  import { CloudflareApiError, isCloudflareApiError } from "./api-error.ts";
12
16
  import {
@@ -37,9 +41,49 @@ interface BaseAiSearchProps extends CloudflareApiOptions {
37
41
 
38
42
  /**
39
43
  * Data source for indexing.
40
- * Can be an R2Bucket directly, an R2 source config, or a web crawler config.
44
+ *
45
+ * Accepts three forms:
46
+ * - **R2Bucket (shorthand)**: pass an `R2Bucket` resource directly for
47
+ * default indexing. `prefix`, `includePaths`, and `excludePaths` cannot
48
+ * be set in this form — use the full R2 config form below.
49
+ * - **R2 config**: `{ type: "r2", bucket, prefix?, includePaths?, excludePaths?, jurisdiction? }`.
50
+ * - **Web crawler**: `{ type: "web-crawler", domain, ... }`.
51
+ *
52
+ * When omitted, creates a built-in storage instance for manual file uploads
53
+ * (via the Items API or the AI Search binding).
54
+ */
55
+ source?: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource;
56
+
57
+ /**
58
+ * The namespace this instance belongs to.
59
+ * Can be a namespace name string or an AiSearchNamespace resource.
60
+ *
61
+ * @remarks
62
+ * Single-instance Worker bindings (`bindings: { MY: aiSearch }`) can only
63
+ * bind instances in the `default` namespace. To bind instances in a
64
+ * non-default namespace, use an `AiSearchNamespace` binding instead and
65
+ * access the instance via `env.NS.get(name)`.
66
+ *
67
+ * Changing `namespace` on an existing instance triggers a replace
68
+ * (delete + create) because namespaces are immutable on the Cloudflare
69
+ * side.
70
+ *
71
+ * @default "default"
72
+ */
73
+ namespace?: string | AiSearchNamespace;
74
+
75
+ /**
76
+ * Controls which storage backends are used during indexing.
77
+ * Defaults to vector-only. Set both `vector` and `keyword` to `true` for hybrid search.
78
+ */
79
+ indexMethod?: { vector?: boolean; keyword?: boolean };
80
+
81
+ /**
82
+ * Fusion method for combining vector and keyword results.
83
+ *
84
+ * @default "rrf"
41
85
  */
42
- source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource;
86
+ fusionMethod?: "max" | "rrf";
43
87
 
44
88
  /**
45
89
  * Text generation model for AI responses
@@ -142,7 +186,8 @@ interface BaseAiSearchProps extends CloudflareApiOptions {
142
186
  metadata?: Record<string, unknown>;
143
187
 
144
188
  /**
145
- * Whether to index the source documents when the AI Search instance is created
189
+ * Whether to index the source documents when the AI Search instance is created.
190
+ * Only applicable when a source is provided.
146
191
  * @default true
147
192
  */
148
193
  indexOnCreate?: boolean;
@@ -249,13 +294,73 @@ export interface AiSearchWebCrawlerSource {
249
294
  };
250
295
  }
251
296
 
297
+ /**
298
+ * Type guard for AiSearch
299
+ */
300
+ export function isAiSearch(resource: unknown): resource is AiSearch {
301
+ return (
302
+ typeof resource === "object" &&
303
+ resource !== null &&
304
+ (resource as any)[ResourceKind] === "cloudflare::AiSearch"
305
+ );
306
+ }
307
+
252
308
  export type AiSearch = SnakeToCamel<AiSearch.ApiResponse> & {
253
309
  /**
254
- * The name of the AI Search instance (this is an alias for the `id` property)
310
+ * The instance name on the Cloudflare side. Equal to `id`. This is what
311
+ * gets emitted as `instance_name` in single-instance `ai_search` bindings.
255
312
  */
256
313
  name: string;
314
+
315
+ /**
316
+ * The namespace this instance belongs to.
317
+ *
318
+ * Optional for backwards compatibility with state files that predate
319
+ * namespace support; at write-time this is always populated (defaults to
320
+ * `"default"` when the user did not specify a namespace).
321
+ */
322
+ namespace?: string;
257
323
  };
258
324
 
325
+ /**
326
+ * An AI Search instance: a managed search index with optional built-in
327
+ * storage and optional external data source (R2 or web crawler).
328
+ *
329
+ * @see https://developers.cloudflare.com/ai-search/
330
+ *
331
+ * @example
332
+ * ## Built-in storage (no source)
333
+ *
334
+ * Creates an instance whose content is uploaded directly via the items API.
335
+ *
336
+ * ```ts
337
+ * const kb = await AiSearch("knowledge-base", {
338
+ * name: "knowledge-base",
339
+ * });
340
+ * ```
341
+ *
342
+ * @example
343
+ * ## R2-backed instance
344
+ *
345
+ * ```ts
346
+ * const bucket = await R2Bucket("docs");
347
+ * const search = await AiSearch("docs-search", {
348
+ * name: "docs-search",
349
+ * source: bucket,
350
+ * });
351
+ * ```
352
+ *
353
+ * @example
354
+ * ## Instance in a custom namespace
355
+ *
356
+ * ```ts
357
+ * const ns = await AiSearchNamespace("tenants", { name: "tenants" });
358
+ * const search = await AiSearch("tenant-a", {
359
+ * name: "tenant-a",
360
+ * namespace: ns,
361
+ * });
362
+ * ```
363
+ */
259
364
  export const AiSearch = Resource(
260
365
  "cloudflare::AiSearch",
261
366
  async function (
@@ -263,9 +368,39 @@ export const AiSearch = Resource(
263
368
  id: string,
264
369
  props: AiSearchProps,
265
370
  ): Promise<AiSearch> {
266
- const api = await createCloudflareApi(props);
267
371
  const adopt = props.adopt ?? this.scope.adopt;
268
372
 
373
+ // Resolve namespace: AiSearchNamespace resource → string, default to "default"
374
+ const namespace = resolveNamespace(props.namespace);
375
+
376
+ if (this.scope.local) {
377
+ // Local development mode — return a mock shape so `alchemy dev` does
378
+ // not hit the real Cloudflare API. Matches the AiSearchNamespace
379
+ // local-mode pattern and satisfies AGENTS.md requirements.
380
+ const name =
381
+ props.name ??
382
+ this.output?.name ??
383
+ this.scope.createPhysicalName(id, "-", 32);
384
+ const now = new Date().toISOString();
385
+ // `AiSearch` is `SnakeToCamel<ApiResponse>` intersected with extras —
386
+ // all non-required fields are optional, so we only populate what's
387
+ // needed. No `as unknown as` escape hatch required.
388
+ const mock: AiSearch = {
389
+ id: name,
390
+ name,
391
+ namespace,
392
+ accountId: "local",
393
+ accountTag: "local",
394
+ createdAt: now,
395
+ internalId: name,
396
+ modifiedAt: now,
397
+ vectorizeName: "",
398
+ };
399
+ return mock;
400
+ }
401
+
402
+ const api = await createCloudflareApi(props);
403
+
269
404
  const validateBucketSource = async (
270
405
  bucket: R2Bucket | string,
271
406
  jurisdiction: R2BucketJurisdiction = "default",
@@ -288,16 +423,25 @@ export const AiSearch = Resource(
288
423
  await getBucket(api, name, { jurisdiction });
289
424
  } catch (error) {
290
425
  throw new Error(
291
- `Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${error instanceof Error ? error.message : String(error)}`,
426
+ `Failed to validate R2 bucket "${name}" (${jurisdiction}) for AI search "${id}": ${
427
+ error instanceof Error ? error.message : String(error)
428
+ }`,
292
429
  { cause: error },
293
430
  );
294
431
  }
295
432
  };
296
433
  const normalizeSource = async (
297
- source: R2Bucket | AiSearchR2Source | AiSearchWebCrawlerSource,
434
+ source:
435
+ | R2Bucket
436
+ | AiSearchR2Source
437
+ | AiSearchWebCrawlerSource
438
+ | undefined,
298
439
  ): Promise<
299
- (AiSearchR2Source & { bucket: string }) | AiSearchWebCrawlerSource
440
+ | (AiSearchR2Source & { bucket: string })
441
+ | AiSearchWebCrawlerSource
442
+ | undefined
300
443
  > => {
444
+ if (!source) return undefined;
301
445
  if (isBucket(source)) {
302
446
  await validateBucketSource(source, source.jurisdiction);
303
447
  return {
@@ -334,14 +478,34 @@ export const AiSearch = Resource(
334
478
  );
335
479
  }
336
480
  };
337
- const normalizeTokenId = async (): Promise<string> => {
481
+ /**
482
+ * Only resolve token for R2 sources. Web-crawler and built-in storage
483
+ * instances do not require a token.
484
+ */
485
+ const normalizeTokenId = async (
486
+ source:
487
+ | (AiSearchR2Source & { bucket: string })
488
+ | AiSearchWebCrawlerSource
489
+ | undefined,
490
+ ): Promise<string | undefined> => {
491
+ // Token only needed for R2 sources
492
+ if (!source || source.type !== "r2") {
493
+ return undefined;
494
+ }
338
495
  if ("tokenId" in props) {
339
496
  validateTokenId(props.tokenId);
340
497
  return props.tokenId;
341
- } else if (props.token) {
498
+ } else if ("token" in props && props.token) {
342
499
  validateTokenId(props.token.tokenId);
343
500
  return props.token.tokenId;
344
501
  } else {
502
+ // Auto-created tokens are an implementation detail of this resource —
503
+ // `adopt: true` so updates reuse a prior auto-created token with the
504
+ // same stable child id. We forward `delete: false` when the user
505
+ // preserves the instance, because Cloudflare rejects token deletion
506
+ // while the token is still referenced by the preserved instance
507
+ // (error 7076 `token_in_use_by_instances`). When the instance IS
508
+ // being deleted normally, the token deletes with it.
345
509
  const token = await AiSearchToken("token", {
346
510
  baseUrl: props.baseUrl,
347
511
  profile: props.profile,
@@ -349,8 +513,8 @@ export const AiSearch = Resource(
349
513
  apiToken: props.apiToken,
350
514
  accountId: props.accountId,
351
515
  email: props.email,
352
- adopt: props.adopt,
353
- delete: props.delete,
516
+ adopt: true,
517
+ ...(props.delete === false ? { delete: false } : {}),
354
518
  });
355
519
  return token.tokenId;
356
520
  }
@@ -358,8 +522,11 @@ export const AiSearch = Resource(
358
522
 
359
523
  if (this.phase === "delete") {
360
524
  if (props.delete !== false && this.output?.id) {
361
- await deleteIndex(api, this.output.vectorizeName);
362
- await deleteAiSearchInstance(api, this.output.id);
525
+ if (this.output.vectorizeName) {
526
+ await deleteIndex(api, this.output.vectorizeName);
527
+ }
528
+ const deleteNs = this.output.namespace ?? "default";
529
+ await deleteAiSearchInstance(api, deleteNs, this.output.id);
363
530
  }
364
531
  return this.destroy();
365
532
  }
@@ -370,35 +537,40 @@ export const AiSearch = Resource(
370
537
  `AI Search instance name must be 1-32 characters, got ${name.length} ("${name}")`,
371
538
  );
372
539
  }
373
- const [source, tokenId] = await Promise.all([
374
- normalizeSource(props.source),
375
- normalizeTokenId(),
376
- ]);
540
+
541
+ const source = await normalizeSource(props.source);
542
+ const tokenId = await normalizeTokenId(source);
377
543
 
378
544
  const payload: AiSearch.ApiPayload = {
379
545
  id: name,
380
- source: source.type === "r2" ? source.bucket : source.domain,
381
- type: source.type,
546
+ source: source
547
+ ? source.type === "r2"
548
+ ? source.bucket
549
+ : source.domain
550
+ : undefined,
551
+ type: source?.type,
382
552
  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
- },
553
+ source_params: source
554
+ ? {
555
+ include_items: source.includePaths,
556
+ exclude_items: source.excludePaths,
557
+ ...(source.type === "r2"
558
+ ? {
559
+ r2_jurisdiction:
560
+ source.jurisdiction !== "default"
561
+ ? source.jurisdiction
562
+ : undefined,
563
+ prefix: source.prefix,
564
+ }
565
+ : {
566
+ web_crawler: {
567
+ parse_type: source.parseType,
568
+ parse_options: source.parseOptions,
569
+ store_options: source.storeOptions,
570
+ },
571
+ }),
572
+ }
573
+ : undefined,
402
574
  embedding_model: props.embeddingModel,
403
575
  chunk: props.chunk,
404
576
  chunk_size: props.chunkSize,
@@ -413,6 +585,8 @@ export const AiSearch = Resource(
413
585
  cache_threshold: props.cacheThreshold,
414
586
  metadata: props.metadata,
415
587
  token_id: tokenId,
588
+ index_method: props.indexMethod,
589
+ fusion_method: props.fusionMethod,
416
590
  };
417
591
 
418
592
  let instance: AiSearch.ApiResponse;
@@ -431,29 +605,73 @@ export const AiSearch = Resource(
431
605
  (payload.type === "web-crawler" &&
432
606
  "sourceDomain" in this.output &&
433
607
  payload.source !== this.output.sourceDomain));
434
- if (replace || replaceLegacy) {
608
+ // Namespace is immutable: moving an instance between namespaces must
609
+ // replace (delete old, create new) rather than attempt an in-place
610
+ // update against a non-existent `PUT /namespaces/{new}/instances/{id}`.
611
+ // Default to "default" for legacy state files that predate the
612
+ // namespace prop.
613
+ const currentNamespace =
614
+ ("namespace" in this.output && this.output.namespace) || "default";
615
+ const namespaceChanged = currentNamespace !== namespace;
616
+ if (replace || replaceLegacy || namespaceChanged) {
435
617
  return this.replace(true);
436
618
  }
437
- instance = await updateAiSearchInstance(api, this.output.id, payload);
619
+ instance = await updateAiSearchInstance(
620
+ api,
621
+ namespace,
622
+ this.output.id,
623
+ payload,
624
+ );
438
625
  } 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,
626
+ // Pre-flight adopt: if `adopt: true` and an instance already exists,
627
+ // adopt it directly via GET→PUT. This avoids racing on 400/7022 error
628
+ // codes and the fragile `errorData as CloudflareApiErrorPayload[]` cast.
629
+ if (adopt) {
630
+ const existing = await getAiSearchInstance(api, namespace, name).catch(
631
+ (e: unknown) => {
632
+ if (e instanceof CloudflareApiError && e.status === 404) {
633
+ return undefined;
634
+ }
635
+ throw e;
636
+ },
637
+ );
638
+ if (existing) {
639
+ instance = await updateAiSearchInstance(
640
+ api,
641
+ namespace,
642
+ existing.id,
643
+ payload,
447
644
  );
448
- if (isAlreadyExistsError && adopt) {
449
- instance = await getAiSearchInstance(api, name);
450
- instance = await updateAiSearchInstance(api, instance.id, payload);
451
645
  } else {
646
+ instance = await createAiSearchInstance(api, namespace, payload);
647
+ }
648
+ } else {
649
+ try {
650
+ instance = await createAiSearchInstance(api, namespace, payload);
651
+ } catch (error) {
652
+ // Wrap "already exists" errors with a clearer message pointing at
653
+ // the adoption path (AGENTS.md convention).
654
+ const errorData = Array.isArray(
655
+ (error as CloudflareApiError | undefined)?.errorData,
656
+ )
657
+ ? ((error as CloudflareApiError)
658
+ .errorData as CloudflareApiErrorPayload[])
659
+ : [];
660
+ const isAlreadyExistsError =
661
+ error instanceof CloudflareApiError &&
662
+ error.status === 400 &&
663
+ errorData.some((e) => e.code === 7022);
664
+ if (isAlreadyExistsError) {
665
+ throw new Error(
666
+ `AI Search instance "${name}" already exists in namespace "${namespace}". Use \`adopt: true\` to adopt it.`,
667
+ { cause: error },
668
+ );
669
+ }
452
670
  throw error;
453
671
  }
454
672
  }
455
- if (props.indexOnCreate !== false) {
456
- await runAiSearchJob(api, instance.id, (message) =>
673
+ if (props.indexOnCreate !== false && source) {
674
+ await runAiSearchJob(api, namespace, instance.id, (message) =>
457
675
  logger.task(id, {
458
676
  prefix: "index",
459
677
  prefixColor: "gray",
@@ -461,15 +679,33 @@ export const AiSearch = Resource(
461
679
  message,
462
680
  }),
463
681
  );
682
+ } else if (props.indexOnCreate === true && !source) {
683
+ logger.warn(
684
+ `AI Search "${id}": \`indexOnCreate: true\` has no effect because no \`source\` was provided.`,
685
+ );
464
686
  }
465
687
  }
466
688
  return {
467
689
  ...snakeToCamelObjectDeep(instance),
468
690
  name: instance.id,
691
+ // The API response may include a `namespace` field; explicitly use the
692
+ // resolved local value to ensure the output always matches the prop
693
+ // (and defaults to "default" when unspecified).
694
+ namespace,
469
695
  };
470
696
  },
471
697
  );
472
698
 
699
+ /**
700
+ * Resolve a namespace prop to a string name.
701
+ */
702
+ function resolveNamespace(ns: string | AiSearchNamespace | undefined): string {
703
+ if (!ns) return "default";
704
+ if (typeof ns === "string") return ns;
705
+ if (isAiSearchNamespace(ns)) return ns.namespace;
706
+ return "default";
707
+ }
708
+
473
709
  /**
474
710
  * Validate that a domain string is a valid domain format (not a URL).
475
711
  * Throws a helpful error if the input looks like a URL.
@@ -576,8 +812,8 @@ export declare namespace AiSearch {
576
812
 
577
813
  interface ApiPayload {
578
814
  id: string;
579
- source: string;
580
- type: "r2" | "web-crawler";
815
+ source?: string;
816
+ type?: "r2" | "web-crawler";
581
817
  ai_gateway_id?: string;
582
818
  ai_search_model?: Model;
583
819
  cache?: boolean;
@@ -599,6 +835,8 @@ export declare namespace AiSearch {
599
835
  }>;
600
836
  embedding_model?: EmbeddingModel;
601
837
  hybrid_search_enabled?: boolean;
838
+ index_method?: { vector?: boolean; keyword?: boolean };
839
+ fusion_method?: "max" | "rrf";
602
840
  max_num_results?: number;
603
841
  metadata?: {
604
842
  created_from_aisearch_wizard?: boolean;
@@ -670,9 +908,10 @@ export declare namespace AiSearch {
670
908
  created_at: string;
671
909
  internal_id: string;
672
910
  modified_at: string;
673
- source: string;
674
- type: "r2" | "web-crawler";
911
+ source?: string;
912
+ type?: "r2" | "web-crawler";
675
913
  vectorize_name: string;
914
+ namespace?: string;
676
915
  ai_gateway_id?: string;
677
916
  ai_search_model?: Model;
678
917
  cache?: boolean; // default: true
@@ -693,6 +932,8 @@ export declare namespace AiSearch {
693
932
  enable?: boolean;
694
933
  engine_version?: number; // default: 1
695
934
  hybrid_search_enabled?: boolean;
935
+ index_method?: { vector?: boolean; keyword?: boolean };
936
+ fusion_method?: "max" | "rrf";
696
937
  last_activity?: string;
697
938
  max_num_results?: number; // maximum: 50, minimum: 1, default: 10
698
939
  metadata?: {
@@ -782,54 +1023,71 @@ export declare namespace AiSearch {
782
1023
  }
783
1024
  }
784
1025
 
1026
+ // ─── Namespace-scoped API Helper Functions ───────────────────────────────────
1027
+
1028
+ /**
1029
+ * Base path for namespace-scoped instance operations
1030
+ */
1031
+ function aiSearchInstanceBasePath(
1032
+ api: CloudflareApi,
1033
+ namespace: string,
1034
+ ): string {
1035
+ return `/accounts/${api.accountId}/ai-search/namespaces/${namespace}/instances`;
1036
+ }
1037
+
785
1038
  export async function listAiSearchInstances(
786
1039
  api: CloudflareApi,
1040
+ namespace = "default",
787
1041
  ): Promise<AiSearch.ApiResponse[]> {
788
1042
  return await extractCloudflareResult<AiSearch.ApiResponse[]>(
789
1043
  "list AI Search instances",
790
- api.get(`/accounts/${api.accountId}/ai-search/instances`),
1044
+ api.get(aiSearchInstanceBasePath(api, namespace)),
791
1045
  );
792
1046
  }
793
1047
 
794
1048
  export async function createAiSearchInstance(
795
1049
  api: CloudflareApi,
1050
+ namespace: string,
796
1051
  payload: AiSearch.ApiPayload,
797
1052
  ): Promise<AiSearch.ApiResponse> {
798
1053
  return await extractCloudflareResult<AiSearch.ApiResponse>(
799
1054
  `create AI Search instance "${payload.id}"`,
800
- api.post(`/accounts/${api.accountId}/ai-search/instances`, payload),
1055
+ api.post(aiSearchInstanceBasePath(api, namespace), payload),
801
1056
  );
802
1057
  }
803
1058
 
804
1059
  export async function getAiSearchInstance(
805
1060
  api: CloudflareApi,
1061
+ namespace: string,
806
1062
  id: string,
807
1063
  ): Promise<AiSearch.ApiResponse> {
808
1064
  return await extractCloudflareResult<AiSearch.ApiResponse>(
809
1065
  `get AI Search instance "${id}"`,
810
- api.get(`/accounts/${api.accountId}/ai-search/instances/${id}`),
1066
+ api.get(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
811
1067
  );
812
1068
  }
813
1069
 
814
1070
  export async function updateAiSearchInstance(
815
1071
  api: CloudflareApi,
1072
+ namespace: string,
816
1073
  id: string,
817
1074
  payload: AiSearch.ApiPayload,
818
1075
  ): Promise<AiSearch.ApiResponse> {
819
1076
  return await extractCloudflareResult<AiSearch.ApiResponse>(
820
1077
  `update AI Search instance "${id}"`,
821
- api.put(`/accounts/${api.accountId}/ai-search/instances/${id}`, payload),
1078
+ api.put(`${aiSearchInstanceBasePath(api, namespace)}/${id}`, payload),
822
1079
  );
823
1080
  }
824
1081
 
825
1082
  export async function deleteAiSearchInstance(
826
1083
  api: CloudflareApi,
1084
+ namespace: string,
827
1085
  id: string,
828
1086
  ): Promise<void> {
829
1087
  try {
830
1088
  await extractCloudflareResult(
831
1089
  `delete AI Search instance "${id}"`,
832
- api.delete(`/accounts/${api.accountId}/ai-search/instances/${id}`),
1090
+ api.delete(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
833
1091
  );
834
1092
  } catch (error) {
835
1093
  if (error instanceof CloudflareApiError && error.status === 404) {
@@ -837,8 +1095,27 @@ export async function deleteAiSearchInstance(
837
1095
  }
838
1096
  throw error;
839
1097
  }
1098
+
1099
+ // Cloudflare's DELETE returns 204 before the instance fully disappears
1100
+ // from the backing services. A subsequent GET can return the stale row
1101
+ // for a bounded window — same-colo is invalidated immediately via the
1102
+ // edge cache, cross-colo is bounded by a 60s KV TTL.
1103
+ //
1104
+ // Actively wait here so the destroy phase presents a strongly-
1105
+ // consistent "instance is gone" guarantee to its callers (child token
1106
+ // delete, user-facing teardown assertions, dependent resources).
1107
+ await poll({
1108
+ description: `wait for AI Search instance "${id}" deletion to propagate`,
1109
+ fn: () => api.get(`${aiSearchInstanceBasePath(api, namespace)}/${id}`),
1110
+ predicate: (res) => res.status === 404,
1111
+ initialDelay: 500,
1112
+ maxDelay: 5000,
1113
+ timeout: 90_000,
1114
+ });
840
1115
  }
841
1116
 
1117
+ // ─── Job API Helper Functions ────────────────────────────────────────────────
1118
+
842
1119
  interface AiSearchJobApiResponse {
843
1120
  id: string;
844
1121
  source: "user" | "schedule";
@@ -850,24 +1127,24 @@ interface AiSearchJobApiResponse {
850
1127
 
851
1128
  export async function listAiSearchJobs(
852
1129
  api: CloudflareApi,
1130
+ namespace: string,
853
1131
  aiSearchId: string,
854
1132
  ): Promise<AiSearchJobApiResponse[]> {
855
1133
  return await extractCloudflareResult<AiSearchJobApiResponse[]>(
856
1134
  `list AI Search jobs for instance "${aiSearchId}"`,
857
- api.get(
858
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
859
- ),
1135
+ api.get(`${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs`),
860
1136
  );
861
1137
  }
862
1138
 
863
1139
  export async function createAiSearchJob(
864
1140
  api: CloudflareApi,
1141
+ namespace: string,
865
1142
  aiSearchId: string,
866
1143
  ): Promise<AiSearchJobApiResponse> {
867
1144
  return await extractCloudflareResult<AiSearchJobApiResponse>(
868
1145
  `create AI Search job for instance "${aiSearchId}"`,
869
1146
  api.post(
870
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs`,
1147
+ `${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs`,
871
1148
  {},
872
1149
  ),
873
1150
  );
@@ -875,13 +1152,14 @@ export async function createAiSearchJob(
875
1152
 
876
1153
  export async function getAiSearchJob(
877
1154
  api: CloudflareApi,
1155
+ namespace: string,
878
1156
  aiSearchId: string,
879
1157
  jobId: string,
880
1158
  ): Promise<AiSearchJobApiResponse> {
881
1159
  return await extractCloudflareResult<AiSearchJobApiResponse>(
882
1160
  `get AI Search job "${jobId}" for instance "${aiSearchId}"`,
883
1161
  api.get(
884
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}`,
1162
+ `${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs/${jobId}`,
885
1163
  ),
886
1164
  );
887
1165
  }
@@ -895,6 +1173,7 @@ interface AiSearchJobLogItem {
895
1173
 
896
1174
  export async function listAiSearchJobLogs(
897
1175
  api: CloudflareApi,
1176
+ namespace: string,
898
1177
  aiSearchId: string,
899
1178
  jobId: string,
900
1179
  ): Promise<AiSearchJobLogItem[]> {
@@ -902,7 +1181,7 @@ export async function listAiSearchJobLogs(
902
1181
  return await extractCloudflareResult<AiSearchJobLogItem[]>(
903
1182
  `list AI Search job logs for job "${jobId}" for instance "${aiSearchId}"`,
904
1183
  api.get(
905
- `/accounts/${api.accountId}/ai-search/instances/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
1184
+ `${aiSearchInstanceBasePath(api, namespace)}/${aiSearchId}/jobs/${jobId}/logs?per_page=500`,
906
1185
  ),
907
1186
  );
908
1187
  } catch (error) {
@@ -917,16 +1196,17 @@ export async function listAiSearchJobLogs(
917
1196
 
918
1197
  export async function runAiSearchJob(
919
1198
  api: CloudflareApi,
1199
+ namespace: string,
920
1200
  aiSearchId: string,
921
1201
  log: (message: string) => void,
922
1202
  ): Promise<void> {
923
1203
  log("Preparing to index...");
924
- const job = await createAiSearchJob(api, aiSearchId);
1204
+ const job = await createAiSearchJob(api, namespace, aiSearchId);
925
1205
  let lastLogId = 0;
926
1206
  let done = false;
927
1207
  const resultPromise = poll({
928
1208
  description: `run AI Search job "${job.id}" for instance "${aiSearchId}"`,
929
- fn: () => getAiSearchJob(api, aiSearchId, job.id),
1209
+ fn: () => getAiSearchJob(api, namespace, aiSearchId, job.id),
930
1210
  predicate: (result) => result.ended_at !== null,
931
1211
  });
932
1212
  pollLogs();
@@ -936,7 +1216,7 @@ export async function runAiSearchJob(
936
1216
  log(`Sync completed: ${result.end_reason}`);
937
1217
 
938
1218
  async function pollLogs() {
939
- const logs = await listAiSearchJobLogs(api, aiSearchId, job.id);
1219
+ const logs = await listAiSearchJobLogs(api, namespace, aiSearchId, job.id);
940
1220
  for (let i = logs.length - 1; i >= 0; i--) {
941
1221
  const item = logs[i];
942
1222
  if (item.id > lastLogId) {