Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/mock-servers/cloudflare/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"$schema": "../../../node_modules/wrangler/config-schema.json",
"name": "kody-mock-cloudflare",
"compatibility_date": "2026-04-16",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public",
"enhanced_error_serialization",
],
"main": "./src/worker.ts",
"preview_urls": true,
"observability": { "enabled": true },
Expand Down
3 changes: 3 additions & 0 deletions packages/worker/src/dynamic-worker-compatibility.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {

test('dynamic and mock Cloudflare worker compatibility match the main wrangler config', () => {
const mainWorker = readMainWorkerWranglerCompatibility()
expect(mainWorker.compatibilityFlags).toContain(
'enhanced_error_serialization',
)
expect(createDynamicWorkerCompatibilityOptions()).toEqual({
compatibilityDate: mainWorker.compatibilityDate,
compatibilityFlags: mainWorker.compatibilityFlags,
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/dynamic-worker-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const dynamicWorkerCompatibilityDate = '2026-04-16'
export const dynamicWorkerCompatibilityFlags = [
'nodejs_compat',
'global_fetch_strictly_public',
'enhanced_error_serialization',
] as const

export type DynamicWorkerCompatibilityOptions = {
Expand Down
9 changes: 9 additions & 0 deletions packages/worker/src/repo/artifacts.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,15 @@ test('getArtifactsBinding prefers the native ARTIFACTS binding for the env names
await expect(binding.get('repo-unrelated-not-found')).rejects.toThrow(
/git: repository not found/,
)
nativeGet.mockReset()
nativeGet.mockImplementation(async () => {
throw new Error(
'ArtifactsError: Repository not found: repo-native-prefixed',
)
})
await expect(binding.get('repo-native-prefixed')).resolves.toEqual({
status: 'not_found',
})

nativeGet.mockReset()
nativeGet.mockImplementation(async () => {
Expand Down
59 changes: 41 additions & 18 deletions packages/worker/src/repo/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,30 +200,53 @@ function isArtifactsBindingError(error: unknown): error is ArtifactsError {
)
}

function artifactsBindingErrorCode(error: unknown) {
if (isArtifactsBindingError(error)) return error.code
if (error === null || typeof error !== 'object') return null
const candidate = error as { name?: unknown; message?: unknown }
// Message fallback only for ArtifactsError with known prefixes so a
// generic git/HTTP "not found" cannot look like a create-safe miss.
function readArtifactsErrorMessage(error: unknown) {
if (typeof error === 'string') return error
if (error instanceof Error) return error.message
if (
candidate.name !== 'ArtifactsError' ||
typeof candidate.message !== 'string'
error !== null &&
typeof error === 'object' &&
'message' in error &&
typeof error.message === 'string'
) {
return null
return error.message
}
if (/^Repository not found(?::|\b)/.test(candidate.message)) {
return 'NOT_FOUND'
}
if (/^Repository already exists(?::|\b)/.test(candidate.message)) {
return ''
}

function artifactsBindingErrorCode(error: unknown) {
if (isArtifactsBindingError(error)) return error.code
// JSRPC may flatten the class so `name` is Error and the message is
// `ArtifactsError: Repository not found: <repo>`. Strip that prefix,
// then require the known create-safe starts.
const message = readArtifactsErrorMessage(error).replace(
/^ArtifactsError:\s*/,
'',
)
if (/^Repository not found(?::|\b)/.test(message)) return 'NOT_FOUND'
if (/^Repository already exists(?::|\b)/.test(message)) {
return 'ALREADY_EXISTS'
}
if (/^Import in progress(?::|\b)/i.test(candidate.message)) {
if (/^Import in progress(?::|\b)/i.test(message)) {
return 'IMPORT_IN_PROGRESS'
}
return null
}

async function getNativeRepoOrThrow(native: Artifacts, name: string) {
try {
return await native.get(name)
} catch (error) {
const code = artifactsBindingErrorCode(error)
if (code === 'NOT_FOUND') {
throw new Error(`Artifacts repo "${name}" was not found.`, {
cause: error,
})
}
throw error
}
}

function adaptNativeRepoHandle(repo: ArtifactsRepo): ArtifactRepoHandle {
return {
info: async () => ({
Expand Down Expand Up @@ -267,19 +290,19 @@ function adaptNativeArtifactsBinding(
): ArtifactNamespaceBinding & Record<string, unknown> {
const repo = (name: string): ArtifactRepoHandle => ({
info: async () => {
const handle = await native.get(name)
const handle = await getNativeRepoOrThrow(native, name)
return adaptNativeRepoHandle(handle).info()
},
createToken: async (scope = 'write', ttl = 3600) => {
const handle = await native.get(name)
const handle = await getNativeRepoOrThrow(native, name)
return adaptNativeRepoHandle(handle).createToken(scope, ttl)
},
listTokens: async () => {
const handle = await native.get(name)
const handle = await getNativeRepoOrThrow(native, name)
return adaptNativeRepoHandle(handle).listTokens?.() ?? []
},
revokeToken: async (idOrPlaintext) => {
const handle = await native.get(name)
const handle = await getNativeRepoOrThrow(native, name)
await adaptNativeRepoHandle(handle).revokeToken?.(idOrPlaintext)
},
})
Expand Down
12 changes: 11 additions & 1 deletion packages/worker/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
"$schema": "../../node_modules/wrangler/config-schema.json",
"name": "kody",
"compatibility_date": "2026-04-16",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
// `enhanced_error_serialization` is date-default on 2026-04-21. Opt in
// explicitly so JSRPC from the Artifacts binding preserves `name`/`code`
// instead of flattening to `Error` + `ArtifactsError: …` message. Keep
// the structural matcher in `src/repo/artifacts.ts`; this flag does not
// restore custom prototype identity. Do not advance the date just to
// pick this up — 2026-04-21 also defaults `enable_python_external_sdk`.
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public",
"enhanced_error_serialization",
],
// Paid default is 10_000 subrequests/invocation. The MCP Agent Durable
// Object keeps a long-lived session and each mutating storage_query pays
// several D1 + StorageRunner RPCs (entitlement baseline, sqlQuery,
Expand Down
Loading