alchemy 0.92.2 → 0.93.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.
- package/bin/alchemy.js +2 -2
- package/lib/cloudflare/ai-search-namespace.d.ts +150 -0
- package/lib/cloudflare/ai-search-namespace.d.ts.map +1 -0
- package/lib/cloudflare/ai-search-namespace.js +278 -0
- package/lib/cloudflare/ai-search-namespace.js.map +1 -0
- package/lib/cloudflare/ai-search-token.d.ts +11 -1
- package/lib/cloudflare/ai-search-token.d.ts.map +1 -1
- package/lib/cloudflare/ai-search-token.js +35 -7
- package/lib/cloudflare/ai-search-token.js.map +1 -1
- package/lib/cloudflare/ai-search.d.ts +123 -18
- package/lib/cloudflare/ai-search.d.ts.map +1 -1
- package/lib/cloudflare/ai-search.js +222 -68
- package/lib/cloudflare/ai-search.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +29 -2
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +4 -2
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/container.d.ts +79 -6
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.js +130 -9
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/custom-domain.d.ts +14 -0
- package/lib/cloudflare/custom-domain.d.ts.map +1 -1
- package/lib/cloudflare/custom-domain.js +3 -0
- package/lib/cloudflare/custom-domain.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +43 -0
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +31 -13
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/website.d.ts +4 -0
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +15 -1
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +25 -1
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +6 -0
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +3 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +32 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +4 -1
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +8 -2
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/image.d.ts +1 -1
- package/lib/docker/image.d.ts.map +1 -1
- package/lib/docker/image.js +4 -44
- package/lib/docker/image.js.map +1 -1
- package/lib/docker/registry.d.ts +11 -0
- package/lib/docker/registry.d.ts.map +1 -0
- package/lib/docker/registry.js +41 -0
- package/lib/docker/registry.js.map +1 -0
- package/package.json +3 -3
- package/src/cloudflare/ai-search-namespace.ts +435 -0
- package/src/cloudflare/ai-search-token.ts +43 -9
- package/src/cloudflare/ai-search.ts +334 -73
- package/src/cloudflare/bindings.ts +33 -0
- package/src/cloudflare/bound.ts +87 -74
- package/src/cloudflare/container.ts +242 -18
- package/src/cloudflare/custom-domain.ts +25 -1
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/miniflare/build-worker-options.ts +50 -1
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +58 -34
- package/src/cloudflare/website.ts +19 -1
- package/src/cloudflare/worker-metadata.ts +25 -1
- package/src/cloudflare/worker.ts +9 -0
- package/src/cloudflare/wrangler.json.ts +32 -0
- package/src/docker/api.ts +11 -2
- package/src/docker/image.ts +5 -56
- package/src/docker/registry.ts +62 -0
- package/workers/cloudflare-state-store.js +57 -57
- package/workers/tunnel-proxy.js +1 -1
|
@@ -4,6 +4,8 @@ import { assertNever } from "../../util/assert-never.ts";
|
|
|
4
4
|
import { reservePort } from "../../util/find-open-port.ts";
|
|
5
5
|
import type { HTTPServer } from "../../util/http.ts";
|
|
6
6
|
import { logger } from "../../util/logger.ts";
|
|
7
|
+
import { isAiSearchNamespace } from "../ai-search-namespace.ts";
|
|
8
|
+
import { isAiSearch } from "../ai-search.ts";
|
|
7
9
|
import type { CloudflareApi } from "../api.ts";
|
|
8
10
|
import type {
|
|
9
11
|
Binding,
|
|
@@ -53,7 +55,13 @@ type RemoteBinding =
|
|
|
53
55
|
| Extract<
|
|
54
56
|
WorkerBindingSpec,
|
|
55
57
|
{ type: "send_email" | "service" | "vpc_service" }
|
|
56
|
-
|
|
58
|
+
>
|
|
59
|
+
// AI Search bindings reject the `raw` flag at the Cloudflare API
|
|
60
|
+
// validation layer (10333). Wrangler's upload form deliberately omits
|
|
61
|
+
// `raw` for these two binding types in
|
|
62
|
+
// packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts,
|
|
63
|
+
// so we mirror that here for the remote-binding-proxy upload.
|
|
64
|
+
| Extract<WorkerBindingSpec, { type: "ai_search" | "ai_search_namespace" }>;
|
|
57
65
|
|
|
58
66
|
type BaseWorkerOptions = {
|
|
59
67
|
[K in keyof miniflare.WorkerOptions]: K extends
|
|
@@ -90,6 +98,35 @@ export const buildWorkerOptions = async (
|
|
|
90
98
|
(options.bindings ??= {})[key] = binding;
|
|
91
99
|
continue;
|
|
92
100
|
}
|
|
101
|
+
if (isAiSearch(binding)) {
|
|
102
|
+
// AI Search instance bindings are not supported natively by Miniflare;
|
|
103
|
+
// proxy them to the deployed instance via the `remote-binding-proxy`
|
|
104
|
+
// worker (same mechanism used by `ai`, `vectorize`, etc.). Instance
|
|
105
|
+
// bindings are always scoped to the default namespace on the CF side,
|
|
106
|
+
// so the namespace need not be surfaced in the remote-proxy metadata.
|
|
107
|
+
//
|
|
108
|
+
// Note: unlike `ai`/`browser`/`vectorize`, AI Search bindings must NOT
|
|
109
|
+
// carry `raw: true` — CF's API rejects that field on `ai_search` and
|
|
110
|
+
// `ai_search_namespace` bindings (error 10333). This mirrors wrangler's
|
|
111
|
+
// create-worker-upload-form.ts which also omits `raw` for these two
|
|
112
|
+
// binding types.
|
|
113
|
+
remoteBindings.push({
|
|
114
|
+
type: "ai_search",
|
|
115
|
+
name: key,
|
|
116
|
+
instance_name: binding.name,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (isAiSearchNamespace(binding)) {
|
|
121
|
+
// See comment above on `ai_search`: `raw: true` is rejected by CF for
|
|
122
|
+
// this binding type, so we push without it.
|
|
123
|
+
remoteBindings.push({
|
|
124
|
+
type: "ai_search_namespace",
|
|
125
|
+
name: key,
|
|
126
|
+
namespace: binding.namespace,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
93
130
|
if (binding.type === "cloudflare::Worker::Self") {
|
|
94
131
|
(options.serviceBindings ??= {})[key] = miniflare.kCurrentWorker;
|
|
95
132
|
continue;
|
|
@@ -527,6 +564,18 @@ export const buildWorkerOptions = async (
|
|
|
527
564
|
remoteProxyConnectionString: remoteProxy.connectionString,
|
|
528
565
|
};
|
|
529
566
|
break;
|
|
567
|
+
case "ai_search":
|
|
568
|
+
(options.aiSearchInstances ??= {})[binding.name] = {
|
|
569
|
+
instance_name: binding.instance_name,
|
|
570
|
+
remoteProxyConnectionString: remoteProxy.connectionString,
|
|
571
|
+
};
|
|
572
|
+
break;
|
|
573
|
+
case "ai_search_namespace":
|
|
574
|
+
(options.aiSearchNamespaces ??= {})[binding.name] = {
|
|
575
|
+
namespace: binding.namespace,
|
|
576
|
+
remoteProxyConnectionString: remoteProxy.connectionString,
|
|
577
|
+
};
|
|
578
|
+
break;
|
|
530
579
|
default: {
|
|
531
580
|
assertNever(binding);
|
|
532
581
|
}
|
|
@@ -39,7 +39,12 @@ export async function createRemoteProxyWorker(input: {
|
|
|
39
39
|
bindings: WorkerBindingSpec[];
|
|
40
40
|
}): Promise<RemoteBindingProxy> {
|
|
41
41
|
const script = await getInternalWorkerBundle("remote-binding-proxy");
|
|
42
|
-
|
|
42
|
+
// We must create the preview token BEFORE probing for Cloudflare Access:
|
|
43
|
+
// a preview worker URL only returns the Access 302 when the request carries
|
|
44
|
+
// the preview token (otherwise CF responds with 404 regardless of Access
|
|
45
|
+
// configuration). Probing without the token would incorrectly report
|
|
46
|
+
// "no Access" for accounts that do gate workers.dev behind Access.
|
|
47
|
+
const [token, subdomain] = await Promise.all([
|
|
43
48
|
createWorkersPreviewToken(input.api, {
|
|
44
49
|
name: input.name,
|
|
45
50
|
metadata: {
|
|
@@ -54,14 +59,10 @@ export async function createRemoteProxyWorker(input: {
|
|
|
54
59
|
minimal_mode: true,
|
|
55
60
|
},
|
|
56
61
|
}),
|
|
57
|
-
getAccountSubdomain(input.api)
|
|
58
|
-
const host = `${input.name}.${subdomain}.workers.dev`;
|
|
59
|
-
return {
|
|
60
|
-
host,
|
|
61
|
-
accessToken: await getAccessToken(host),
|
|
62
|
-
};
|
|
63
|
-
}),
|
|
62
|
+
getAccountSubdomain(input.api),
|
|
64
63
|
]);
|
|
64
|
+
const host = `${input.name}.${subdomain}.workers.dev`;
|
|
65
|
+
const accessToken = await getAccessToken(host, token);
|
|
65
66
|
|
|
66
67
|
const baseHeaders: Record<string, string> = {
|
|
67
68
|
"cf-workers-preview-token": token,
|
|
@@ -158,7 +159,13 @@ async function createWorkersPreviewToken(
|
|
|
158
159
|
|
|
159
160
|
async function prewarm(url: string, previewToken: string) {
|
|
160
161
|
try {
|
|
161
|
-
|
|
162
|
+
// Pass the preview token to the Access probe — prewarm targets a preview
|
|
163
|
+
// worker URL, which only surfaces an Access 302 when the request carries
|
|
164
|
+
// the preview token.
|
|
165
|
+
const accessToken = await getAccessToken(
|
|
166
|
+
new URL(url).hostname,
|
|
167
|
+
previewToken,
|
|
168
|
+
);
|
|
162
169
|
await fetch(url, {
|
|
163
170
|
method: "POST",
|
|
164
171
|
headers: {
|
|
@@ -197,42 +204,59 @@ async function createWorkersPreviewSession(api: CloudflareApi) {
|
|
|
197
204
|
/**
|
|
198
205
|
* If the given domain uses Cloudflare Access, fetches the access token for the domain.
|
|
199
206
|
* Otherwise, returns undefined.
|
|
207
|
+
*
|
|
208
|
+
* For workers.dev preview URLs, pass the `cf-workers-preview-token` alongside
|
|
209
|
+
* the HEAD probe: the preview worker only surfaces its Access 302 when the
|
|
210
|
+
* request carries the preview token (a bare HEAD returns 404 regardless of
|
|
211
|
+
* Access configuration).
|
|
212
|
+
*
|
|
200
213
|
* @see https://github.com/cloudflare/workers-sdk/blob/a5eb5134f73d3983655d325a4de71c6370c57faa/packages/wrangler/src/user/access.ts#L10
|
|
201
214
|
*/
|
|
202
|
-
const getAccessToken = memoize(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
215
|
+
const getAccessToken = memoize(
|
|
216
|
+
async (hostname: string, previewToken?: string) => {
|
|
217
|
+
if (!(await domainUsesAccess(hostname, previewToken))) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const result = await Scope.current
|
|
221
|
+
.exec(`access-${hostname}`, `cloudflared access login ${hostname}`)
|
|
222
|
+
.catch(() => {
|
|
223
|
+
throw new Error(
|
|
224
|
+
[
|
|
225
|
+
`The \`cloudflared\` CLI is not installed, but is required to access the domain "${hostname}".`,
|
|
226
|
+
`Please install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation and run \`cloudflare access login ${hostname}\`.`,
|
|
227
|
+
].join("\n"),
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
const matches = result.stdout.match(/fetched your token:\n\n(.*)/m);
|
|
231
|
+
if (matches?.[1]) {
|
|
232
|
+
return matches[1];
|
|
233
|
+
}
|
|
234
|
+
const error = new Error(
|
|
235
|
+
`Failed to get access token for domain "${hostname}".`,
|
|
236
|
+
);
|
|
237
|
+
Object.assign(error, result);
|
|
238
|
+
throw error;
|
|
239
|
+
},
|
|
240
|
+
(hostname) => hostname,
|
|
241
|
+
);
|
|
226
242
|
|
|
227
243
|
/**
|
|
228
244
|
* Returns true if the domain uses Cloudflare Access.
|
|
245
|
+
*
|
|
246
|
+
* @param hostname the workers.dev preview host
|
|
247
|
+
* @param previewToken optional preview token; required when probing a
|
|
248
|
+
* preview-worker URL because CF only serves the Access 302 to requests
|
|
249
|
+
* that carry the token.
|
|
229
250
|
*/
|
|
230
|
-
async function domainUsesAccess(hostname: string) {
|
|
251
|
+
async function domainUsesAccess(hostname: string, previewToken?: string) {
|
|
231
252
|
try {
|
|
232
253
|
const response = await fetch(`https://${hostname}`, {
|
|
233
254
|
method: "HEAD",
|
|
234
255
|
redirect: "manual",
|
|
235
256
|
signal: AbortSignal.timeout(1000),
|
|
257
|
+
headers: previewToken
|
|
258
|
+
? { "cf-workers-preview-token": previewToken }
|
|
259
|
+
: undefined,
|
|
236
260
|
});
|
|
237
261
|
return !!(
|
|
238
262
|
response.status === 302 &&
|
|
@@ -10,6 +10,7 @@ import { Assets } from "./assets.ts";
|
|
|
10
10
|
import type { Bindings } from "./bindings.ts";
|
|
11
11
|
import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.ts";
|
|
12
12
|
import { unionCompatibilityFlags } from "./compatibility-presets.ts";
|
|
13
|
+
import { quickTunnel } from "./quick-tunnel.ts";
|
|
13
14
|
import {
|
|
14
15
|
extractStringAndSecretBindings,
|
|
15
16
|
unencryptSecrets,
|
|
@@ -78,6 +79,10 @@ export interface WebsiteProps<
|
|
|
78
79
|
* The local domain to use for the dev server
|
|
79
80
|
*/
|
|
80
81
|
domain?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Whether to use a Cloudflare Tunnel for the dev server
|
|
84
|
+
*/
|
|
85
|
+
tunnel?: boolean;
|
|
81
86
|
/**
|
|
82
87
|
* Additional environment variables to set when running the dev command
|
|
83
88
|
*/
|
|
@@ -363,6 +368,10 @@ export async function Website<
|
|
|
363
368
|
ALCHEMY_ROOT: Scope.current.rootDir,
|
|
364
369
|
},
|
|
365
370
|
});
|
|
371
|
+
if (url && ((typeof dev === "object" && dev.tunnel) || scope.tunnel)) {
|
|
372
|
+
const { tunnelUrl } = await quickTunnel(scope, url);
|
|
373
|
+
url = tunnelUrl;
|
|
374
|
+
}
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
return (await Worker(id, {
|
|
@@ -377,7 +386,16 @@ export async function Website<
|
|
|
377
386
|
}
|
|
378
387
|
: {}),
|
|
379
388
|
},
|
|
380
|
-
dev:
|
|
389
|
+
dev: worker.dev
|
|
390
|
+
? {
|
|
391
|
+
url: url as never,
|
|
392
|
+
port: worker.dev.port,
|
|
393
|
+
remote: worker.dev.remote,
|
|
394
|
+
tunnel: worker.dev.tunnel,
|
|
395
|
+
}
|
|
396
|
+
: url
|
|
397
|
+
? { url }
|
|
398
|
+
: undefined,
|
|
381
399
|
})) as Website<B, RPC>;
|
|
382
400
|
}
|
|
383
401
|
|
|
@@ -2,6 +2,8 @@ import { assertNever } from "../util/assert-never.ts";
|
|
|
2
2
|
import { camelToSnakeObjectDeep } from "../util/camel-to-snake.ts";
|
|
3
3
|
import { logger } from "../util/logger.ts";
|
|
4
4
|
import { memoize } from "../util/memoize.ts";
|
|
5
|
+
import { isAiSearchNamespace } from "./ai-search-namespace.ts";
|
|
6
|
+
import { isAiSearch } from "./ai-search.ts";
|
|
5
7
|
import { extractCloudflareResult } from "./api-response.ts";
|
|
6
8
|
import type { CloudflareApi } from "./api.ts";
|
|
7
9
|
import type {
|
|
@@ -418,7 +420,29 @@ export async function prepareWorkerMetadata(
|
|
|
418
420
|
for (const [bindingName, binding] of Object.entries(bindings)) {
|
|
419
421
|
// Create a copy of the binding to avoid modifying the original
|
|
420
422
|
|
|
421
|
-
if (
|
|
423
|
+
if (isAiSearch(binding)) {
|
|
424
|
+
// Single-instance bindings (ai_search) are always scoped to the `default` namespace.
|
|
425
|
+
if (binding.namespace !== undefined && binding.namespace !== "default") {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Worker binding "${bindingName}" uses a single-instance AiSearch binding (type: "ai_search"), ` +
|
|
428
|
+
`but the bound AiSearch instance "${binding.name}" is in namespace "${binding.namespace}". ` +
|
|
429
|
+
`Single-instance bindings only support the "default" namespace.\n` +
|
|
430
|
+
`Fix: either (1) create the AiSearch without a custom namespace, or ` +
|
|
431
|
+
`(2) bind the enclosing AiSearchNamespace and use \`env.${bindingName}.get("${binding.name}")\`.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
meta.bindings.push({
|
|
435
|
+
type: "ai_search",
|
|
436
|
+
name: bindingName,
|
|
437
|
+
instance_name: binding.name,
|
|
438
|
+
});
|
|
439
|
+
} else if (isAiSearchNamespace(binding)) {
|
|
440
|
+
meta.bindings.push({
|
|
441
|
+
type: "ai_search_namespace",
|
|
442
|
+
name: bindingName,
|
|
443
|
+
namespace: binding.namespace,
|
|
444
|
+
});
|
|
445
|
+
} else if (typeof binding === "string") {
|
|
422
446
|
meta.bindings.push({
|
|
423
447
|
type: "plain_text",
|
|
424
448
|
name: bindingName,
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -329,6 +329,12 @@ export interface BaseWorkerProps<
|
|
|
329
329
|
* @default false
|
|
330
330
|
*/
|
|
331
331
|
adopt?: boolean;
|
|
332
|
+
/**
|
|
333
|
+
* Forcibly transfer the hostname when it is currently bound to a
|
|
334
|
+
* different Worker service. See {@link CustomDomainProps.overrideExistingOrigin}.
|
|
335
|
+
* @default false
|
|
336
|
+
*/
|
|
337
|
+
overrideExistingOrigin?: boolean;
|
|
332
338
|
}
|
|
333
339
|
)[];
|
|
334
340
|
|
|
@@ -1497,6 +1503,7 @@ async function provisionResources<B extends Bindings>(
|
|
|
1497
1503
|
name: domain,
|
|
1498
1504
|
zoneId: undefined,
|
|
1499
1505
|
adopt: props.adopt,
|
|
1506
|
+
overrideExistingOrigin: undefined,
|
|
1500
1507
|
};
|
|
1501
1508
|
}
|
|
1502
1509
|
if (domain.domainName === "") {
|
|
@@ -1506,6 +1513,7 @@ async function provisionResources<B extends Bindings>(
|
|
|
1506
1513
|
name: domain.domainName,
|
|
1507
1514
|
zoneId: domain.zoneId,
|
|
1508
1515
|
adopt: domain.adopt ?? props.adopt,
|
|
1516
|
+
overrideExistingOrigin: domain.overrideExistingOrigin,
|
|
1509
1517
|
};
|
|
1510
1518
|
}),
|
|
1511
1519
|
eventSources: props.eventSources?.map((eventSource) => {
|
|
@@ -1578,6 +1586,7 @@ async function provisionResources<B extends Bindings>(
|
|
|
1578
1586
|
name: domain.name,
|
|
1579
1587
|
zoneId: domain.zoneId,
|
|
1580
1588
|
adopt: domain.adopt,
|
|
1589
|
+
overrideExistingOrigin: domain.overrideExistingOrigin,
|
|
1581
1590
|
delete: props.delete,
|
|
1582
1591
|
workerName: options.name,
|
|
1583
1592
|
dev: options.local,
|
|
@@ -6,6 +6,8 @@ import { Resource } from "../resource.ts";
|
|
|
6
6
|
import { Scope } from "../scope.ts";
|
|
7
7
|
import { isSecret } from "../secret.ts";
|
|
8
8
|
import { assertNever } from "../util/assert-never.ts";
|
|
9
|
+
import { isAiSearchNamespace } from "./ai-search-namespace.ts";
|
|
10
|
+
import { isAiSearch } from "./ai-search.ts";
|
|
9
11
|
import { createCloudflareApi } from "./api.ts";
|
|
10
12
|
import type { Bindings } from "./bindings.ts";
|
|
11
13
|
import { getCloudflareRegistryWithAccountNamespace } from "./container.ts";
|
|
@@ -289,6 +291,8 @@ async function processBindings(
|
|
|
289
291
|
const containers: WranglerJsonConfig["containers"] = [];
|
|
290
292
|
const workerLoaders: WranglerJsonConfig["worker_loaders"] = [];
|
|
291
293
|
const vpcServices: WranglerJsonConfig["vpc_services"] = [];
|
|
294
|
+
const aiSearchInstances: WranglerJsonConfig["ai_search"] = [];
|
|
295
|
+
const aiSearchNamespaces: WranglerJsonConfig["ai_search_namespaces"] = [];
|
|
292
296
|
|
|
293
297
|
for (const eventSource of eventSources ?? []) {
|
|
294
298
|
if (isQueueEventSource(eventSource)) {
|
|
@@ -321,6 +325,26 @@ async function processBindings(
|
|
|
321
325
|
} else if (writeSecrets && isSecret(binding)) {
|
|
322
326
|
spec.vars ??= {};
|
|
323
327
|
spec.vars[bindingName] = binding as any;
|
|
328
|
+
} else if (isAiSearch(binding)) {
|
|
329
|
+
// AI Search discriminates via ResourceKind (isAiSearch) rather than
|
|
330
|
+
// binding.type — `AiSearch.type` is the *source* type ("r2" |
|
|
331
|
+
// "web-crawler") and so would collide with `r2_bucket` string
|
|
332
|
+
// matching below. Checking here at the top of the chain mirrors the
|
|
333
|
+
// ordering in worker-metadata.ts.
|
|
334
|
+
aiSearchInstances.push({
|
|
335
|
+
binding: bindingName,
|
|
336
|
+
instance_name: binding.name,
|
|
337
|
+
// AI Search is not natively supported by Miniflare — emitting
|
|
338
|
+
// `remote: true` lets `wrangler dev` proxy to the deployed
|
|
339
|
+
// instance (matches vectorize / browser / ai / vpc_service).
|
|
340
|
+
remote: true,
|
|
341
|
+
});
|
|
342
|
+
} else if (isAiSearchNamespace(binding)) {
|
|
343
|
+
aiSearchNamespaces.push({
|
|
344
|
+
binding: bindingName,
|
|
345
|
+
namespace: binding.namespace,
|
|
346
|
+
remote: true,
|
|
347
|
+
});
|
|
324
348
|
} else if (binding.type === "cloudflare::Worker::Self") {
|
|
325
349
|
// Self(service) binding
|
|
326
350
|
services.push({
|
|
@@ -646,4 +670,12 @@ async function processBindings(
|
|
|
646
670
|
if (workerLoaders.length > 0) {
|
|
647
671
|
spec.worker_loaders = workerLoaders;
|
|
648
672
|
}
|
|
673
|
+
|
|
674
|
+
if (aiSearchInstances.length > 0) {
|
|
675
|
+
spec.ai_search = aiSearchInstances;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (aiSearchNamespaces.length > 0) {
|
|
679
|
+
spec.ai_search_namespaces = aiSearchNamespaces;
|
|
680
|
+
}
|
|
649
681
|
}
|
package/src/docker/api.ts
CHANGED
|
@@ -256,10 +256,19 @@ export class DockerApi {
|
|
|
256
256
|
* Pull Docker image
|
|
257
257
|
*
|
|
258
258
|
* @param image Image name and tag
|
|
259
|
+
* @param options Pull options
|
|
259
260
|
* @returns Result of the pull command
|
|
260
261
|
*/
|
|
261
|
-
async pullImage(
|
|
262
|
-
|
|
262
|
+
async pullImage(
|
|
263
|
+
image: string,
|
|
264
|
+
options?: { platform?: string },
|
|
265
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
266
|
+
const args = ["pull"];
|
|
267
|
+
if (options?.platform) {
|
|
268
|
+
args.push("--platform", options.platform);
|
|
269
|
+
}
|
|
270
|
+
args.push(image);
|
|
271
|
+
return this.exec(args);
|
|
263
272
|
}
|
|
264
273
|
|
|
265
274
|
async tagImage(
|
package/src/docker/image.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "pathe";
|
|
4
3
|
import type { Context } from "../context.ts";
|
|
5
4
|
import { Resource } from "../resource.ts";
|
|
6
5
|
import type { Secret } from "../secret.ts";
|
|
7
6
|
import { DockerApi } from "./api.ts";
|
|
7
|
+
import { pushImageToRegistry } from "./registry.ts";
|
|
8
8
|
import type { RemoteImage } from "./remote-image.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -66,9 +66,9 @@ export interface DockerBuildOptions {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export interface ImageRegistry {
|
|
69
|
+
server: string;
|
|
69
70
|
username: string;
|
|
70
71
|
password: Secret;
|
|
71
|
-
server: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
@@ -285,63 +285,12 @@ export const Image = Resource(
|
|
|
285
285
|
let repoDigest: string | undefined;
|
|
286
286
|
let finalImageRef = imageRef;
|
|
287
287
|
if (props.registry && !props.skipPush) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const registryHost = server.replace(/\/$/, "");
|
|
292
|
-
|
|
293
|
-
// Determine if the built image already includes a registry host (e.g. ghcr.io/user/repo)
|
|
294
|
-
const firstSegment = imageRef.split("/")[0];
|
|
295
|
-
const hasRegistryPrefix = firstSegment.includes(".");
|
|
296
|
-
|
|
297
|
-
// Compose the target image reference that will be pushed
|
|
298
|
-
const targetImage = hasRegistryPrefix
|
|
299
|
-
? imageRef // already fully-qualified
|
|
300
|
-
: `${registryHost}/${imageRef}`;
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
// Create a temporary directory that will act as an isolated Docker config
|
|
304
|
-
// (credentials) directory. This prevents race-conditions when multiple
|
|
305
|
-
// concurrent tests perform `docker login` / `logout` by ensuring each
|
|
306
|
-
// Image operation has its own credential store.
|
|
307
|
-
const tempConfigDir = await fs.mkdtemp(
|
|
308
|
-
path.join(os.tmpdir(), "docker-config-"),
|
|
309
|
-
);
|
|
310
|
-
const api = new DockerApi({ configDir: tempConfigDir });
|
|
311
|
-
|
|
312
|
-
// Authenticate to registry using the isolated config directory
|
|
313
|
-
await api.login(registryHost, username, password.unencrypted);
|
|
314
|
-
|
|
315
|
-
// Tag local image with fully qualified name if necessary
|
|
316
|
-
if (targetImage !== imageRef) {
|
|
317
|
-
await api.exec(["tag", imageRef, targetImage]);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Push the image
|
|
321
|
-
const { stdout: pushOut } = await api.exec(["push", targetImage]);
|
|
322
|
-
|
|
323
|
-
// Attempt to extract the repo digest from push output
|
|
324
|
-
const digestMatch = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/.exec(pushOut);
|
|
325
|
-
if (digestMatch) {
|
|
326
|
-
const digestHash = digestMatch[1];
|
|
327
|
-
// Strip tag (anything after last :) to build image@digest reference
|
|
328
|
-
const [repoWithoutTag] =
|
|
329
|
-
targetImage.split(":").length > 2
|
|
330
|
-
? [targetImage] // unlikely but safety
|
|
331
|
-
: [targetImage.substring(0, targetImage.lastIndexOf(":"))];
|
|
332
|
-
repoDigest = `${repoWithoutTag}@${digestHash}`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Update the final image reference to point at the pushed image
|
|
336
|
-
finalImageRef = targetImage;
|
|
337
|
-
} finally {
|
|
338
|
-
// Clean up credentials from the isolated config
|
|
339
|
-
await api.logout(registryHost);
|
|
340
|
-
}
|
|
288
|
+
const pushedImage = await pushImageToRegistry(imageRef, props.registry);
|
|
289
|
+
finalImageRef = pushedImage.imageRef;
|
|
290
|
+
repoDigest = pushedImage.repoDigest;
|
|
341
291
|
}
|
|
342
292
|
return {
|
|
343
293
|
kind: "Image",
|
|
344
|
-
...props,
|
|
345
294
|
tag,
|
|
346
295
|
name,
|
|
347
296
|
imageRef: finalImageRef,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "pathe";
|
|
4
|
+
import { Secret, type Secret as SecretValue } from "../secret.ts";
|
|
5
|
+
import { DockerApi } from "./api.ts";
|
|
6
|
+
|
|
7
|
+
export interface RegistryPushCredentials {
|
|
8
|
+
server: string;
|
|
9
|
+
username: string;
|
|
10
|
+
password: string | SecretValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function pushImageToRegistry(
|
|
14
|
+
imageRef: string,
|
|
15
|
+
registry: RegistryPushCredentials,
|
|
16
|
+
): Promise<{
|
|
17
|
+
imageRef: string;
|
|
18
|
+
repoDigest?: string;
|
|
19
|
+
}> {
|
|
20
|
+
const registryHost = registry.server.replace(/\/$/, "");
|
|
21
|
+
const password = Secret.unwrap(registry.password);
|
|
22
|
+
|
|
23
|
+
const firstSegment = imageRef.split("/")[0];
|
|
24
|
+
const hasRegistryPrefix = firstSegment.includes(".");
|
|
25
|
+
const targetImage = hasRegistryPrefix
|
|
26
|
+
? imageRef
|
|
27
|
+
: `${registryHost}/${imageRef}`;
|
|
28
|
+
|
|
29
|
+
let repoDigest: string | undefined;
|
|
30
|
+
let api: DockerApi | undefined;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const tempConfigDir = await fs.mkdtemp(
|
|
34
|
+
path.join(os.tmpdir(), "docker-config-"),
|
|
35
|
+
);
|
|
36
|
+
api = new DockerApi({ configDir: tempConfigDir });
|
|
37
|
+
|
|
38
|
+
await api.login(registryHost, registry.username, password);
|
|
39
|
+
|
|
40
|
+
if (targetImage !== imageRef) {
|
|
41
|
+
await api.exec(["tag", imageRef, targetImage]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { stdout } = await api.exec(["push", targetImage]);
|
|
45
|
+
const digestMatch = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/.exec(stdout);
|
|
46
|
+
if (digestMatch) {
|
|
47
|
+
const digestHash = digestMatch[1];
|
|
48
|
+
const [repoWithoutTag] =
|
|
49
|
+
targetImage.split(":").length > 2
|
|
50
|
+
? [targetImage]
|
|
51
|
+
: [targetImage.substring(0, targetImage.lastIndexOf(":"))];
|
|
52
|
+
repoDigest = `${repoWithoutTag}@${digestHash}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
imageRef: targetImage,
|
|
57
|
+
repoDigest,
|
|
58
|
+
};
|
|
59
|
+
} finally {
|
|
60
|
+
await api?.logout(registryHost);
|
|
61
|
+
}
|
|
62
|
+
}
|