diff --git a/.u8.json b/.u8.json index 883d38a..9c905b1 100644 --- a/.u8.json +++ b/.u8.json @@ -96,6 +96,16 @@ "packageVersion": "1.0.0", "packageName": "bootstrap" } + }, + { + "timestamp": "2025-10-23T12:52:15.389Z", + "template": "pkg", + "values": { + "monoRepo": true, + "packagePrefix": "@morten-olsen/box-", + "packageVersion": "1.0.0", + "packageName": "resource-cloudflare" + } } ] } \ No newline at end of file diff --git a/packages/k8s/src/config/config.ts b/packages/k8s/src/config/config.ts index 262fc0a..8d2b118 100644 --- a/packages/k8s/src/config/config.ts +++ b/packages/k8s/src/config/config.ts @@ -1,9 +1,39 @@ -import { KubeConfig } from '@kubernetes/client-node'; +import { ApiextensionsV1Api, CustomObjectsApi, KubeConfig, KubernetesObjectApi } from '@kubernetes/client-node'; +class K8sConfig { + #config: KubeConfig; + #objectsApi?: KubernetesObjectApi; + #customObjectsApi?: CustomObjectsApi; + #extensionsApi?: ApiextensionsV1Api; -class K8sConfig extends KubeConfig { constructor() { - super(); - this.loadFromDefault(); + this.#config = new KubeConfig(); + this.#config.loadFromDefault(); + + } + + public get kubeConfig() { + return this.#config; + } + + public get objectsApi() { + if (!this.#objectsApi) { + this.#objectsApi = this.#config.makeApiClient(KubernetesObjectApi) + } + return this.#objectsApi; + } + + public get customObjectsApi() { + if (!this.#customObjectsApi) { + this.#customObjectsApi = this.#config.makeApiClient(CustomObjectsApi); + } + return this.#customObjectsApi; + } + + public get extensionsApi() { + if (!this.#extensionsApi) { + this.#extensionsApi = this.#config.makeApiClient(ApiextensionsV1Api) + } + return this.#extensionsApi; } } diff --git a/packages/k8s/src/core/core.crd.ts b/packages/k8s/src/core/core.crd.ts index 4658741..bf0d2c8 100644 --- a/packages/k8s/src/core/core.crd.ts +++ b/packages/k8s/src/core/core.crd.ts @@ -1,6 +1,6 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1CustomResourceDefinition } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; class CRD extends Resource { public static readonly apiVersion = 'apiextensions.k8s.io/v1'; diff --git a/packages/k8s/src/core/core.deployment.ts b/packages/k8s/src/core/core.deployment.ts index 3de17f0..0807123 100644 --- a/packages/k8s/src/core/core.deployment.ts +++ b/packages/k8s/src/core/core.deployment.ts @@ -1,6 +1,7 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1Deployment } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; + class Deployment extends Resource { public static readonly apiVersion = 'apps/v1'; public static readonly kind = 'Deployment'; diff --git a/packages/k8s/src/core/core.namespace.ts b/packages/k8s/src/core/core.namespace.ts index 352ee91..9021e25 100644 --- a/packages/k8s/src/core/core.namespace.ts +++ b/packages/k8s/src/core/core.namespace.ts @@ -1,6 +1,7 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1Namespace } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; + class Namespace extends Resource { public static readonly apiVersion = 'v1'; public static readonly kind = 'Namespace'; diff --git a/packages/k8s/src/core/core.pv.ts b/packages/k8s/src/core/core.pv.ts index 1a9fbf6..637c75c 100644 --- a/packages/k8s/src/core/core.pv.ts +++ b/packages/k8s/src/core/core.pv.ts @@ -1,6 +1,7 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1PersistentVolume } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; + class PersistentVolume extends Resource { public static readonly apiVersion = 'v1'; public static readonly kind = 'PersistentVolume'; diff --git a/packages/k8s/src/core/core.secret.ts b/packages/k8s/src/core/core.secret.ts index 1181421..4ca3aaf 100644 --- a/packages/k8s/src/core/core.secret.ts +++ b/packages/k8s/src/core/core.secret.ts @@ -1,23 +1,19 @@ -import { Resource, type ResourceOptions } from '../resources/resource/resource.js'; import type { KubernetesObject, V1Secret } from '@kubernetes/client-node'; -import { decodeSecret, encodeSecret } from '../utils/utils.secrets.js'; +import { decodeSecret, encodeSecret } from '../utils/utils.secrets.js'; +import { Resource } from '../exports.js'; type SetOptions> = T | ((current: T | undefined) => T | Promise); -class Secret = Record> extends Resource { +class Secret extends Resource { public static readonly apiVersion = 'v1'; public static readonly kind = 'Secret'; - constructor(options: ResourceOptions) { - super(options); - } - public get value() { - return decodeSecret(this.data) as T | undefined; + return decodeSecret(this.data); } - public set = async (options: SetOptions, data?: KubernetesObject) => { + public set = async (options: SetOptions>, data?: KubernetesObject) => { const value = typeof options === 'function' ? await Promise.resolve(options(this.value)) : options; await this.ensure({ ...data, diff --git a/packages/k8s/src/core/core.service.ts b/packages/k8s/src/core/core.service.ts index a70c191..baa0075 100644 --- a/packages/k8s/src/core/core.service.ts +++ b/packages/k8s/src/core/core.service.ts @@ -1,6 +1,7 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1Service } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; + class Service extends Resource { public static readonly apiVersion = 'v1'; public static readonly kind = 'Service'; diff --git a/packages/k8s/src/core/core.stateful-set.ts b/packages/k8s/src/core/core.stateful-set.ts index c51ca12..26c0c87 100644 --- a/packages/k8s/src/core/core.stateful-set.ts +++ b/packages/k8s/src/core/core.stateful-set.ts @@ -1,6 +1,7 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1StatefulSet } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; + class StatefulSet extends Resource { public static readonly apiVersion = 'apps/v1'; public static readonly kind = 'StatefulSet'; diff --git a/packages/k8s/src/core/core.storage-class.ts b/packages/k8s/src/core/core.storage-class.ts index e78b6ca..e57b46b 100644 --- a/packages/k8s/src/core/core.storage-class.ts +++ b/packages/k8s/src/core/core.storage-class.ts @@ -1,6 +1,7 @@ -import { Resource } from '../resources/resource/resource.js'; import type { V1StorageClass } from '@kubernetes/client-node'; +import { Resource } from '../resources/resource/resource.js'; + class StorageClass extends Resource { public static readonly apiVersion = 'storage.k8s.io/v1'; public static readonly kind = 'StorageClass'; diff --git a/packages/k8s/src/core/core.ts b/packages/k8s/src/core/core.ts index bf8d64d..277a1b2 100644 --- a/packages/k8s/src/core/core.ts +++ b/packages/k8s/src/core/core.ts @@ -1,19 +1,10 @@ -import { CRD } from "./core.crd.js"; -import { Deployment } from "./core.deployment.js"; -import { Namespace } from "./core.namespace.js"; -import { PersistentVolume } from "./core.pv.js"; -import { Secret } from "./core.secret.js"; -import { Service } from "./core.service.js"; -import { StatefulSet } from "./core.stateful-set.js"; -import { StorageClass } from "./core.storage-class.js"; +import { CRD } from './core.crd.js'; +import { Deployment } from './core.deployment.js'; +import { Namespace } from './core.namespace.js'; +import { PersistentVolume } from './core.pv.js'; +import { Secret } from './core.secret.js'; +import { Service } from './core.service.js'; +import { StatefulSet } from './core.stateful-set.js'; +import { StorageClass } from './core.storage-class.js'; -export { - CRD, - Deployment, - Namespace, - PersistentVolume, - Secret, - Service, - StatefulSet, - StorageClass, -} +export { CRD, Deployment, Namespace, PersistentVolume, Secret, Service, StatefulSet, StorageClass }; diff --git a/packages/k8s/src/operator.ts b/packages/k8s/src/operator.ts index 16ba62d..5b8bcb9 100644 --- a/packages/k8s/src/operator.ts +++ b/packages/k8s/src/operator.ts @@ -1,4 +1,5 @@ import { Services } from '@morten-olsen/box-utils/services'; + import { ResourceService } from './resources/resources.js'; class K8sOperator { diff --git a/packages/k8s/src/resources/resource/resource.custom.ts b/packages/k8s/src/resources/resource/resource.custom.ts index b8deadc..4744117 100644 --- a/packages/k8s/src/resources/resource/resource.custom.ts +++ b/packages/k8s/src/resources/resource/resource.custom.ts @@ -1,12 +1,14 @@ import { z, type ZodType } from 'zod'; -import { CustomObjectsApi, PatchStrategy, setHeaderOptions, type KubernetesObject } from '@kubernetes/client-node'; +import { PatchStrategy, setHeaderOptions, type KubernetesObject } from '@kubernetes/client-node'; import { CronJob, CronTime } from 'cron'; - -import { K8sConfig } from '../../config/config.js'; - import { CoalescingQueue } from '@morten-olsen/box-utils/coalescing-queue'; +import { FINALIZER } from '@morten-olsen/box-utils/consts'; + +import { NotReadyError } from '../../errors/errors.js'; + import { Resource, type ResourceOptions } from './resource.js'; -import { NotReadyError } from '../../errors/errors.js' +import { K8sConfig } from '../../config/config.js'; +import type { ResourceClass } from '../resources.service.js'; const customResourceStatusSchema = z.object({ observedGeneration: z.number().optional(), @@ -35,7 +37,7 @@ class CustomResource extends Resource< public static readonly apiVersion: string; public static readonly status = customResourceStatusSchema; public static readonly labels: Record = {}; - public static readonly dependsOn?: Resource[]; + public static readonly dependsOn?: ResourceClass[]; #reconcileQueue: CoalescingQueue; #cron: CronJob; @@ -45,9 +47,31 @@ class CustomResource extends Resource< this.#reconcileQueue = new CoalescingQueue({ action: async () => { try { - if (!this.exists || this.manifest?.metadata?.deletionTimestamp) { + if (!this.exists) { return; } + // TODO: Read FINALIZER + // const finalizers = this.metadata?.finalizers || []; + if (this.manifest?.metadata?.deletionTimestamp) { + await this.destroy?.(); + // if (this.metadata?.finalizers?.includes(FINALIZER)) { + // await this.patch({ + // metadata: { + // finalizers: finalizers.filter((f) => f !== FINALIZER), + // deletionTimestamp: this.metadata?.deletionTimestamp, + // }, + // } as any) + // } + return; + } + // if (this.destroy && !finalizers.includes(FINALIZER)) { + // return await this.patch({ + // metadata: { + // finalizers: [...finalizers, FINALIZER] + // }, + // spec: this.spec!, + // }); + // } await this.markSeen(); await this.reconcile?.(); await this.markReady(); @@ -111,6 +135,8 @@ class CustomResource extends Resource< }; public reconcile?: () => Promise; + public destroy?: () => Promise; + public queueReconcile = () => { return this.#reconcileQueue.run(); }; @@ -151,7 +177,7 @@ class CustomResource extends Resource< public patchStatus = (status: Partial>) => this.queue.add(async () => { const config = this.services.get(K8sConfig); - const customObjectsApi = config.makeApiClient(CustomObjectsApi); + const customObjectsApi = config.customObjectsApi; if (this.scope === 'Cluster') { await customObjectsApi.patchClusterCustomObjectStatus( { @@ -180,4 +206,3 @@ class CustomResource extends Resource< } export { CustomResource, type CustomResourceOptions }; - diff --git a/packages/k8s/src/resources/resource/resource.reference.ts b/packages/k8s/src/resources/resource/resource.reference.ts index c7ce77c..af118c4 100644 --- a/packages/k8s/src/resources/resource/resource.reference.ts +++ b/packages/k8s/src/resources/resource/resource.reference.ts @@ -1,4 +1,5 @@ import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; + import type { ResourceClass } from '../resources.js'; import type { ResourceEvents } from './resource.js'; @@ -19,6 +20,9 @@ class ResourceReference> extends EventEmitt } public set current(value: InstanceType | undefined) { + if (value === this.#current?.instance) { + return; + } const previous = this.#current; this.#current?.unsubscribe(); if (value) { diff --git a/packages/k8s/src/resources/resource/resource.ts b/packages/k8s/src/resources/resource/resource.ts index b3618bc..f8d09e6 100644 --- a/packages/k8s/src/resources/resource/resource.ts +++ b/packages/k8s/src/resources/resource/resource.ts @@ -1,11 +1,11 @@ import { ApiException, KubernetesObjectApi, PatchStrategy, type KubernetesObject } from '@kubernetes/client-node'; import deepEqual from 'deep-equal'; - -import { ResourceService } from '../resources.service.js'; import type { Services } from '@morten-olsen/box-utils/services'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import { Queue } from '@morten-olsen/box-utils/queue'; import { isDeepSubset } from '@morten-olsen/box-utils/objects'; + +import { ResourceService } from '../resources.service.js'; import { K8sConfig } from '../../config/config.js'; type ResourceSelector = { @@ -41,7 +41,7 @@ class Resource extends EventEmitter extends EventEmitter { const { services } = this.#options; const config = services.get(K8sConfig); - const objectsApi = config.makeApiClient(KubernetesObjectApi); + const { objectsApi } = config; const body = { ...patch, apiVersion: this.selector.apiVersion, @@ -190,4 +190,3 @@ class Resource extends EventEmitter = (new (options: ResourceOptions) => InstanceType>) & { +type ResourceClass = (new ( + options: ResourceOptions, +) => InstanceType>) & { apiVersion: string; kind: string; plural?: string; @@ -47,13 +48,17 @@ class ResourceService extends EventEmitter { public register = async (...resources: ResourceClass[]) => { for (const resource of resources) { - if (!this.#registry.has(resource)) { - this.#registry.set(resource, { - apiVersion: resource.apiVersion, - kind: resource.kind, - plural: resource.plural, - resources: [], - }); + if (this.#registry.has(resource)) { + return; + } + this.#registry.set(resource, { + apiVersion: resource.apiVersion, + kind: resource.kind, + plural: resource.plural, + resources: [], + }); + if ('dependsOn' in resource && Array.isArray(resource.dependsOn)) { + await this.register(...resource.dependsOn as ResourceClass[]); } const watcherService = this.#services.get(WatcherService); const watcher = watcherService.create({ @@ -65,7 +70,7 @@ class ResourceService extends EventEmitter { if (!name) { return; } - const current = this.get(resource, name, namespace); + const current = this.#get(resource, name, namespace, manifest); current.manifest = manifest; }); await watcher.start(); @@ -76,7 +81,7 @@ class ResourceService extends EventEmitter { return (this.#registry.get(type)?.resources?.filter((r) => r.exists) as InstanceType[]) || []; }; - public get = >(type: T, name: string, namespace?: string) => { + #get = >(type: T, name: string, namespace?: string, manifest?: unknown) => { let resourceRegistry = this.#registry.get(type); if (!resourceRegistry) { resourceRegistry = { @@ -98,6 +103,7 @@ class ResourceService extends EventEmitter { namespace, }, services: this.#services, + manifest, }); current.on('changed', this.emit.bind(this, 'changed', current)); resources.push(current); @@ -105,9 +111,13 @@ class ResourceService extends EventEmitter { return current as InstanceType; }; + public get = >(type: T, name: string, namespace?: string) => { + return this.#get(type, name, namespace); + }; + public install = async (...resources: InstallableResourceClass[]) => { const config = this.#services.get(K8sConfig); - const extensionsApi = config.makeApiClient(ApiextensionsV1Api); + const { extensionsApi } = config; for (const resource of resources) { try { const manifest = createManifest(resource); @@ -136,4 +146,3 @@ class ResourceService extends EventEmitter { } export { ResourceService, Resource, type ResourceOptions, type ResourceClass, type InstallableResourceClass }; - diff --git a/packages/k8s/src/resources/resources.ts b/packages/k8s/src/resources/resources.ts index f2466f9..db6fa66 100644 --- a/packages/k8s/src/resources/resources.ts +++ b/packages/k8s/src/resources/resources.ts @@ -1,4 +1,9 @@ export { CustomResource, type CustomResourceOptions } from './resource/resource.custom.js'; export { ResourceReference } from './resource/resource.reference.js'; -export { ResourceService, Resource, type ResourceOptions, type ResourceClass, type InstallableResourceClass } from './resources.service.js'; - +export { + ResourceService, + Resource, + type ResourceOptions, + type ResourceClass, + type InstallableResourceClass, +} from './resources.service.js'; diff --git a/packages/k8s/src/resources/resources.utils.ts b/packages/k8s/src/resources/resources.utils.ts index 43018ec..5c05f6b 100644 --- a/packages/k8s/src/resources/resources.utils.ts +++ b/packages/k8s/src/resources/resources.utils.ts @@ -52,4 +52,3 @@ const createManifest = (defintion: InstallableResourceClass) => { }; export { createManifest }; - diff --git a/packages/k8s/src/watchers/watcher/watcher.ts b/packages/k8s/src/watchers/watcher/watcher.ts index 4ced4cf..893963d 100644 --- a/packages/k8s/src/watchers/watcher/watcher.ts +++ b/packages/k8s/src/watchers/watcher/watcher.ts @@ -1,6 +1,7 @@ import { KubernetesObjectApi, makeInformer, type Informer, type KubernetesObject } from '@kubernetes/client-node'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import type { Services } from '@morten-olsen/box-utils/services'; + import { K8sConfig } from '../../config/config.js'; type ResourceChangedAction = 'add' | 'update' | 'delete'; @@ -32,11 +33,10 @@ class Watcher extends EventEmitter> const { services, apiVersion, kind, selector } = this.#options; const plural = this.#options.plural ?? kind.toLowerCase() + 's'; const [version, group] = apiVersion.split('/').toReversed(); - const config = services.get(K8sConfig); + const { kubeConfig, objectsApi } = services.get(K8sConfig); const path = group ? `/apis/${group}/${version}/${plural}` : `/api/${version}/${plural}`; - const objectsApi = config.makeApiClient(KubernetesObjectApi); const informer = makeInformer( - config, + kubeConfig, path, async () => { return objectsApi.list(apiVersion, kind); @@ -67,4 +67,3 @@ class Watcher extends EventEmitter> } export { Watcher, type WatcherOptions, type ResourceChangedAction }; - diff --git a/packages/k8s/src/watchers/watchers.ts b/packages/k8s/src/watchers/watchers.ts index 9ccbcc6..2c5188a 100644 --- a/packages/k8s/src/watchers/watchers.ts +++ b/packages/k8s/src/watchers/watchers.ts @@ -1,5 +1,6 @@ -import { Services, destroy } from "@morten-olsen/box-utils/services"; -import { Watcher, type WatcherOptions } from "./watcher/watcher.js"; +import { Services, destroy } from '@morten-olsen/box-utils/services'; + +import { Watcher, type WatcherOptions } from './watcher/watcher.js'; class WatcherService { #services: Services; diff --git a/packages/k8s/tsconfig.tsbuildinfo b/packages/k8s/tsconfig.tsbuildinfo new file mode 100644 index 0000000..ccbf7d4 --- /dev/null +++ b/packages/k8s/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/exports.ts","./src/global.d.ts","./src/operator.ts","./src/config/config.ts","./src/core/core.crd.ts","./src/core/core.deployment.ts","./src/core/core.namespace.ts","./src/core/core.pv.ts","./src/core/core.secret.ts","./src/core/core.service.ts","./src/core/core.stateful-set.ts","./src/core/core.storage-class.ts","./src/core/core.ts","./src/errors/errors.ts","./src/resources/resources.service.ts","./src/resources/resources.ts","./src/resources/resources.utils.ts","./src/resources/resource/resource.custom.ts","./src/resources/resource/resource.reference.ts","./src/resources/resource/resource.ts","./src/utils/utils.secrets.ts","./src/watchers/watchers.ts","./src/watchers/watcher/watcher.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/packages/resource-authentik/src/resources/server/server.schemas.ts b/packages/resource-authentik/src/resources/server/server.schemas.ts index dc70c0f..5cf958a 100644 --- a/packages/resource-authentik/src/resources/server/server.schemas.ts +++ b/packages/resource-authentik/src/resources/server/server.schemas.ts @@ -1,4 +1,4 @@ -import { z } from "@morten-olsen/box-k8s"; +import { z } from '@morten-olsen/box-k8s'; const valueOrSecret = z.object({ value: z.string().optional(), @@ -16,9 +16,11 @@ const serverSpec = z.object({ database: z.object({ url: valueOrSecret, }), - clients: z.object({ - substitutions: z.record(z.string(), z.string()).optional() - }).optional(), + clients: z + .object({ + substitutions: z.record(z.string(), z.string()).optional(), + }) + .optional(), }); export { serverSpec }; diff --git a/packages/resource-authentik/src/resources/server/server.ts b/packages/resource-authentik/src/resources/server/server.ts index c121312..5f7480f 100644 --- a/packages/resource-authentik/src/resources/server/server.ts +++ b/packages/resource-authentik/src/resources/server/server.ts @@ -1,10 +1,7 @@ -import { CustomResource, Secret, type CustomResourceOptions } from "@morten-olsen/box-k8s"; -import { serverSpec } from "./server.schemas.js"; +import { CustomResource, NotReadyError, Secret, type CustomResourceOptions } from '@morten-olsen/box-k8s'; import { API_VERSION } from '@morten-olsen/box-utils/consts'; -type SecretData = { - secret: string; -}; +import { serverSpec } from './server.schemas.js'; class AuthentikServer extends CustomResource { public static readonly apiVersion = API_VERSION; @@ -12,16 +9,30 @@ class AuthentikServer extends CustomResource { public static readonly spec = serverSpec; public static readonly scope = 'Namespaced'; - #secret: Secret; + #secret: Secret; constructor(options: CustomResourceOptions) { super(options); - this.#secret = this.resources.get(Secret, `${this.name}-secret`, this.namespace); + this.#secret = this.resources.get(Secret, `${this.name}-secret`, this.namespace); this.#secret.on('changed', this.queueReconcile); } public reconcile = async () => { + if (!this.#secret.value?.secret) { + await this.#secret.set( + { + secret: crypto.randomUUID(), + }, + { + metadata: { + ownerReferences: [this.ref], + }, + }, + ); + throw new NotReadyError(); + } + const { secret } = this.#secret.value; }; } -export { AuthentikServer } +export { AuthentikServer }; diff --git a/packages/resource-authentik/tsconfig.tsbuildinfo b/packages/resource-authentik/tsconfig.tsbuildinfo new file mode 100644 index 0000000..292e9f0 --- /dev/null +++ b/packages/resource-authentik/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/exports.ts","./src/global.d.ts","./src/resources/oidc-client/oidc-client.ts","./src/resources/server/server.schemas.ts","./src/resources/server/server.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/packages/resource-cloudflare/.gitignore b/packages/resource-cloudflare/.gitignore new file mode 100644 index 0000000..8511d52 --- /dev/null +++ b/packages/resource-cloudflare/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/dist/ +/coverage/ +/.env diff --git a/packages/resource-cloudflare/manifests/cloudflare-account.yaml b/packages/resource-cloudflare/manifests/cloudflare-account.yaml new file mode 100644 index 0000000..30c6a94 --- /dev/null +++ b/packages/resource-cloudflare/manifests/cloudflare-account.yaml @@ -0,0 +1,9 @@ +apiVersion: playground.homelab.olsen.cloud/v1 +kind: CloudflareAccount +metadata: + name: main +spec: + token: + secret: cloudflare + namespace: homelab + key: token diff --git a/packages/resource-cloudflare/manifests/cloudflare-dns-record.yaml b/packages/resource-cloudflare/manifests/cloudflare-dns-record.yaml new file mode 100644 index 0000000..0843e91 --- /dev/null +++ b/packages/resource-cloudflare/manifests/cloudflare-dns-record.yaml @@ -0,0 +1,10 @@ +apiVersion: playground.homelab.olsen.cloud/v1 +kind: CloudflareDnsRecord +metadata: + name: test +spec: + account: main + domain: olsen.cloud + subdomain: testing1 + type: CNAME + value: hello diff --git a/packages/resource-cloudflare/package.json b/packages/resource-cloudflare/package.json new file mode 100644 index 0000000..24a7c25 --- /dev/null +++ b/packages/resource-cloudflare/package.json @@ -0,0 +1,33 @@ +{ + "type": "module", + "main": "dist/exports.js", + "scripts": { + "dev": "tsx --watch src/start.ts", + "build": "tsc --build", + "test:unit": "vitest --run --passWithNoTests", + "test": "pnpm run \"/^test:/\"" + }, + "packageManager": "pnpm@10.6.0", + "files": [ + "dist" + ], + "exports": { + ".": "./dist/exports.js" + }, + "devDependencies": { + "@morten-olsen/box-configs": "workspace:*", + "@morten-olsen/box-tests": "workspace:*", + "@types/node": "24.9.1", + "@vitest/coverage-v8": "4.0.1", + "tsx": "^4.20.6", + "typescript": "5.9.3", + "vitest": "4.0.1" + }, + "dependencies": { + "@morten-olsen/box-k8s": "workspace:*", + "@morten-olsen/box-utils": "workspace:*", + "cloudflare": "^5.2.0" + }, + "name": "@morten-olsen/box-resource-cloudflare", + "version": "1.0.0" +} diff --git a/packages/resource-cloudflare/src/exports.ts b/packages/resource-cloudflare/src/exports.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/resource-cloudflare/src/resources/account/account.schemas.ts b/packages/resource-cloudflare/src/resources/account/account.schemas.ts new file mode 100644 index 0000000..7da131b --- /dev/null +++ b/packages/resource-cloudflare/src/resources/account/account.schemas.ts @@ -0,0 +1,12 @@ +import { z } from '@morten-olsen/box-k8s'; + +const cloudflareAccountSchema = z.object({ + token: z.object({ + secret: z.string(), + namespace: z.string(), + key: z.string(), + }), + allowedNamespaces: z.array(z.string()).optional(), +}); + +export { cloudflareAccountSchema }; diff --git a/packages/resource-cloudflare/src/resources/account/account.ts b/packages/resource-cloudflare/src/resources/account/account.ts new file mode 100644 index 0000000..1854748 --- /dev/null +++ b/packages/resource-cloudflare/src/resources/account/account.ts @@ -0,0 +1,48 @@ +import { CustomResource, NotReadyError, ResourceReference, Secret, type CustomResourceOptions } from "@morten-olsen/box-k8s"; +import { cloudflareAccountSchema } from "./account.schemas.js"; +import { API_VERSION } from "@morten-olsen/box-utils/consts"; + +class CloudflareAccountResource extends CustomResource { + public static readonly apiVersion = API_VERSION; + public static readonly kind = "CloudflareAccount"; + public static readonly spec = cloudflareAccountSchema; + public static readonly scope = "Cluster"; + public static readonly dependsOn = [Secret]; + + #secret: ResourceReference; + + constructor(options: CustomResourceOptions) { + super(options); + this.#secret = new ResourceReference(this.#getSecret()); + this.#secret.on('changed', this.queueReconcile); + } + + public get token() { + const spec = this.spec; + if (!spec) { + return; + } + return this.#secret.current?.value?.[spec.token.key]; + } + + #getSecret = () => { + const spec = this.spec; + if (!spec) { + return; + } + return this.resources.get( + Secret, + spec.token.secret, + spec.token.namespace, + ) + } + + public reconcile = async () => { + this.#secret.current = this.#getSecret(); + if (!this.token) { + throw new NotReadyError('Token not found'); + } + } +} + +export { CloudflareAccountResource }; diff --git a/packages/resource-cloudflare/src/resources/dns-record/dns-record.schemas.ts b/packages/resource-cloudflare/src/resources/dns-record/dns-record.schemas.ts new file mode 100644 index 0000000..7be3445 --- /dev/null +++ b/packages/resource-cloudflare/src/resources/dns-record/dns-record.schemas.ts @@ -0,0 +1,13 @@ +import { z } from '@morten-olsen/box-k8s'; + +const cloudflareDnsRecordSchema = z.object({ + account: z.string(), + domain: z.string(), + subdomain: z.string().optional(), + type: z.enum(['A', 'CNAME', 'MX']), + proxy: z.boolean().optional(), + value: z.string(), + ttl: z.number().optional(), +}); + +export { cloudflareDnsRecordSchema }; diff --git a/packages/resource-cloudflare/src/resources/dns-record/dns-record.ts b/packages/resource-cloudflare/src/resources/dns-record/dns-record.ts new file mode 100644 index 0000000..af506bd --- /dev/null +++ b/packages/resource-cloudflare/src/resources/dns-record/dns-record.ts @@ -0,0 +1,40 @@ +import { CustomResource, NotReadyError } from '@morten-olsen/box-k8s'; + +import { CloudflareService } from '../../services/cloudflare/cloudflare.js'; + +import { cloudflareDnsRecordSchema } from './dns-record.schemas.js'; +import { API_VERSION } from '@morten-olsen/box-utils/consts'; +import { CloudflareAccountResource } from '../account/account.js'; + +class CloudflareDnsRecordResource extends CustomResource { + public static readonly apiVersion = API_VERSION; + public static readonly kind = "CloudflareDnsRecord"; + public static readonly spec = cloudflareDnsRecordSchema; + public static readonly scope = "Namespaced"; + public static readonly dependsOn = [CloudflareAccountResource]; + + public get dnsId() { + return `homelab|${this.namespace}|${this.name}` + } + + public reconcile = async () => { + if (!this.spec) { + throw new NotReadyError('Missing spec'); + } + const service = this.services.get(CloudflareService); + const { getDnsRecord, ensrureDnsRecord } = service.getAccount(this.spec.account); + await ensrureDnsRecord(this.dnsId, this.spec); + }; + + public destroy = async () => { + + if (!this.spec) { + throw new NotReadyError('Missing spec'); + } + const service = this.services.get(CloudflareService); + const { removeDnsRecord } = service.getAccount(this.spec.account); + await removeDnsRecord(this.dnsId, this.spec.domain); + } +} + +export { CloudflareDnsRecordResource, cloudflareDnsRecordSchema }; diff --git a/packages/resource-cloudflare/src/resources/tunnel-route/tunnel-route.ts b/packages/resource-cloudflare/src/resources/tunnel-route/tunnel-route.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/resource-cloudflare/src/resources/tunnel/tunnel.ts b/packages/resource-cloudflare/src/resources/tunnel/tunnel.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/resource-cloudflare/src/services/cloudflare/cloudflare.account.ts b/packages/resource-cloudflare/src/services/cloudflare/cloudflare.account.ts new file mode 100644 index 0000000..40c9a02 --- /dev/null +++ b/packages/resource-cloudflare/src/services/cloudflare/cloudflare.account.ts @@ -0,0 +1,121 @@ +import type { Services } from "@morten-olsen/box-utils/services"; +import type { CloudflareAccountResource } from "../../resources/account/account.js" +import API from 'cloudflare'; +import type { Zone } from "cloudflare/resources/zones/zones.mjs"; +import type { z } from "@morten-olsen/box-k8s"; +import type { cloudflareDnsRecordSchema } from "../../resources/dns-record/dns-record.schemas.js"; + +type CloudflareAccountOptions = { + services: Services; + resource: CloudflareAccountResource; +} + +class CloudflareAccount { + #options: CloudflareAccountOptions; + #api?: API; + #zones: Map> + + constructor(options: CloudflareAccountOptions) { + this.#zones = new Map(); + this.#options = options; + this.#api = new API({ + apiToken: this.token, + }); + } + + public get token() { + return this.#options.resource.token; + } + + public get api() { + if (!this.#api) { + this.#api = new API({ + apiToken: this.token, + }) + } + return this.#api; + } + + #getZone = async (name: string) => { + const zones = await this.api.zones.list({ + name, + }) + const [zone] = zones.result; + return zone; + } + + public getZone = async (name: string) => { + if (!this.#zones.has(name)) { + this.#zones.set(name, this.#getZone(name)); + } + const current = this.#zones.get(name); + return await current; + } + + public getDnsRecord = async (id: string, domain: string) => { + const zone = await this.getZone(domain); + + if (!zone) { + return; + } + + const dnsRecords = await this.api.dns.records.list({ + zone_id: zone.id, + comment: { + exact: id, + }, + }) + const [dnsRecord] = dnsRecords.result; + + return dnsRecord; + } + + public removeDnsRecord = async (id: string, domain: string) => { + const zone = await this.getZone(domain); + if (!zone) { + return; + } + const record = await this.getDnsRecord(id, domain); + if (!record) { + return; + } + + await this.api.dns.records.delete(record.id, { + zone_id: zone.id, + }); + } + + public ensrureDnsRecord = async (id: string, options: z.infer) => { + const { domain, subdomain, value, type, ttl = 1, proxy } = options; + const zone = await this.getZone(options.domain); + if (!zone) { + throw new Error('Zone not found'); + } + const current = await this.getDnsRecord(id, domain); + + if (!current) { + await this.api.dns.records.create({ + zone_id: zone.id, + type, + name: subdomain ? `${subdomain}.${domain}` : domain, + content: value, + comment: id, + ttl, + proxied: proxy, + }) + } else { + + await this.api.dns.records.update(current.id, { + zone_id: zone.id, + type, + name: subdomain ? `${subdomain}.${domain}` : domain, + content: value, + comment: id, + ttl, + proxied: proxy, + }) + } + } +} + +export { CloudflareAccount }; diff --git a/packages/resource-cloudflare/src/services/cloudflare/cloudflare.ts b/packages/resource-cloudflare/src/services/cloudflare/cloudflare.ts new file mode 100644 index 0000000..756e7a4 --- /dev/null +++ b/packages/resource-cloudflare/src/services/cloudflare/cloudflare.ts @@ -0,0 +1,32 @@ +import type { Services } from "@morten-olsen/box-utils/services"; +import { CloudflareAccount } from "./cloudflare.account.js"; +import { ResourceService } from "@morten-olsen/box-k8s"; +import { CloudflareAccountResource } from "../../resources/account/account.js"; + +class CloudflareService { + #services: Services; + #instances: Map; + + constructor(services: Services) { + this.#services = services; + this.#instances = new Map(); + } + + public getAccount = (name: string) => { + if (!this.#instances.has(name)) { + const resourceService = this.#services.get(ResourceService); + const resource = resourceService.get(CloudflareAccountResource, name); + this.#instances.set(name, new CloudflareAccount({ + resource, + services: this.#services, + })) + } + const current = this.#instances.get(name); + if (!current) { + throw new Error('Could not get cloudflare account'); + } + return current; + } +} + +export { CloudflareService }; diff --git a/packages/resource-cloudflare/src/start.ts b/packages/resource-cloudflare/src/start.ts new file mode 100644 index 0000000..77b7adb --- /dev/null +++ b/packages/resource-cloudflare/src/start.ts @@ -0,0 +1,13 @@ +import { K8sOperator } from '@morten-olsen/box-k8s'; +import { CloudflareAccountResource } from './resources/account/account.js'; +import { CloudflareDnsRecordResource } from './resources/dns-record/dns-record.js'; + +const operator = new K8sOperator(); +await operator.resources.install( + CloudflareAccountResource, + CloudflareDnsRecordResource, +) +await operator.resources.register( + CloudflareAccountResource, + CloudflareDnsRecordResource, +) diff --git a/packages/resource-cloudflare/tsconfig.json b/packages/resource-cloudflare/tsconfig.json new file mode 100644 index 0000000..2a3ec7e --- /dev/null +++ b/packages/resource-cloudflare/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "src/**/*.ts" + ], + "extends": "@morten-olsen/box-configs/tsconfig.json" +} diff --git a/packages/resource-cloudflare/vitest.config.ts b/packages/resource-cloudflare/vitest.config.ts new file mode 100644 index 0000000..0a3d383 --- /dev/null +++ b/packages/resource-cloudflare/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; +import { getAliases } from '@morten-olsen/box-tests/vitest'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig(async () => { + const aliases = await getAliases(); + return { + resolve: { + alias: aliases, + }, + }; +}); diff --git a/packages/resource-redis/tsconfig.tsbuildinfo b/packages/resource-redis/tsconfig.tsbuildinfo new file mode 100644 index 0000000..e7b6088 --- /dev/null +++ b/packages/resource-redis/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/exports.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/packages/utils/src/consts.ts b/packages/utils/src/consts.ts index 80f9843..e6eca8f 100644 --- a/packages/utils/src/consts.ts +++ b/packages/utils/src/consts.ts @@ -1,4 +1,5 @@ const API_GROUP = 'playground.homelab.olsen.cloud'; const API_VERSION = `${API_GROUP}/v1`; +const FINALIZER = `finalizer.${API_GROUP}`; -export { API_VERSION, API_GROUP }; +export { API_VERSION, API_GROUP, FINALIZER }; diff --git a/packages/utils/src/event-emitter.ts b/packages/utils/src/event-emitter.ts index b756028..49823aa 100644 --- a/packages/utils/src/event-emitter.ts +++ b/packages/utils/src/event-emitter.ts @@ -23,7 +23,7 @@ class EventEmitter void | P abortController.signal.addEventListener('abort', () => { this.#listeners.set(event, listeners?.difference(new Set([callbackClone]))); }); - return abortController.abort; + return () => abortController.abort(); }; once = (event: K, callback: EventListener>, options: OnOptions = {}) => { @@ -62,4 +62,3 @@ class EventEmitter void | P } export { EventEmitter }; - diff --git a/packages/utils/src/objects.ts b/packages/utils/src/objects.ts index cc99c07..e850cc3 100644 --- a/packages/utils/src/objects.ts +++ b/packages/utils/src/objects.ts @@ -28,6 +28,6 @@ function isDeepSubset(actual: ExplicitAny, expected: T): expected is T { } return true; -}; +} export { isDeepSubset }; diff --git a/packages/utils/src/queue.ts b/packages/utils/src/queue.ts index 8e794d9..9c074e4 100644 --- a/packages/utils/src/queue.ts +++ b/packages/utils/src/queue.ts @@ -38,4 +38,3 @@ class Queue { } export { Queue }; - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd472e5..80c817a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 6.0.9(@pnpm/logger@5.2.0) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) eslint: specifier: 9.38.0 version: 9.38.0 @@ -61,7 +61,7 @@ importers: version: 8.46.2(eslint@9.38.0)(typescript@5.9.3) vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/bootstrap: devDependencies: @@ -76,13 +76,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/configs: {} @@ -118,13 +118,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/operator: dependencies: @@ -137,13 +137,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/resource-authentik: dependencies: @@ -165,13 +165,47 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) + + packages/resource-cloudflare: + dependencies: + '@morten-olsen/box-k8s': + specifier: workspace:* + version: link:../k8s + '@morten-olsen/box-utils': + specifier: workspace:* + version: link:../utils + cloudflare: + specifier: ^5.2.0 + version: 5.2.0 + devDependencies: + '@morten-olsen/box-configs': + specifier: workspace:* + version: link:../configs + '@morten-olsen/box-tests': + specifier: workspace:* + version: link:../tests + '@types/node': + specifier: 24.9.1 + version: 24.9.1 + '@vitest/coverage-v8': + specifier: 4.0.1 + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) + tsx: + specifier: ^4.20.6 + version: 4.20.6 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.0.1 + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/resource-postgres: dependencies: @@ -193,13 +227,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/resource-redis: dependencies: @@ -221,13 +255,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/tests: dependencies: @@ -243,13 +277,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages/utils: dependencies: @@ -271,13 +305,13 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)) + version: 4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6)) typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.0.1(@types/node@24.9.1) + version: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) packages: @@ -845,6 +879,9 @@ packages: '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@24.9.1': resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} @@ -956,6 +993,10 @@ packages: engines: {node: '>= 8'} hasBin: true + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -970,6 +1011,10 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1171,6 +1216,9 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cloudflare@5.2.0: + resolution: {integrity: sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1417,6 +1465,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1488,10 +1540,17 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1530,6 +1589,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1597,6 +1659,9 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1942,6 +2007,11 @@ packages: engines: {node: '>=10'} hasBin: true + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2168,6 +2238,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -2420,6 +2493,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + turbo-darwin-64@2.5.8: resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==} cpu: [x64] @@ -2498,6 +2576,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2588,6 +2669,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -3213,6 +3298,10 @@ snapshots: '@types/node': 24.9.1 form-data: 4.0.4 + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@24.9.1': dependencies: undici-types: 7.16.0 @@ -3318,7 +3407,7 @@ snapshots: '@typescript-eslint/types': 8.46.2 eslint-visitor-keys: 4.2.1 - '@vitest/coverage-v8@4.0.1(vitest@4.0.1(@types/node@24.9.1))': + '@vitest/coverage-v8@4.0.1(vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.1 @@ -3331,7 +3420,7 @@ snapshots: magicast: 0.3.5 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.1(@types/node@24.9.1) + vitest: 4.0.1(@types/node@24.9.1)(tsx@4.20.6) transitivePeerDependencies: - supports-color @@ -3344,13 +3433,13 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.1(vite@7.1.12(@types/node@24.9.1))': + '@vitest/mocker@4.0.1(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6))': dependencies: '@vitest/spy': 4.0.1 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.12(@types/node@24.9.1) + vite: 7.1.12(@types/node@24.9.1)(tsx@4.20.6) '@vitest/pretty-format@4.0.1': dependencies: @@ -3378,6 +3467,10 @@ snapshots: dependencies: isexe: 2.0.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3386,6 +3479,10 @@ snapshots: agent-base@7.1.4: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3613,6 +3710,18 @@ snapshots: clone@1.0.4: {} + cloudflare@5.2.0: + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3991,6 +4100,8 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} events-universal@1.0.1: @@ -4065,6 +4176,8 @@ snapshots: dependencies: is-callable: 1.2.7 + form-data-encoder@1.7.2: {} + form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -4073,6 +4186,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + fsevents@2.3.3: optional: true @@ -4122,6 +4240,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4173,6 +4295,10 @@ snapshots: human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4488,6 +4614,8 @@ snapshots: split2: 3.2.2 through2: 4.0.2 + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -4707,6 +4835,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -5023,6 +5153,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.20.6: + dependencies: + esbuild: 0.25.11 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + turbo-darwin-64@2.5.8: optional: true @@ -5111,6 +5248,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@5.26.5: {} + undici-types@7.16.0: {} unique-string@2.0.0: @@ -5123,7 +5262,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@7.1.12(@types/node@24.9.1): + vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6): dependencies: esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) @@ -5134,11 +5273,12 @@ snapshots: optionalDependencies: '@types/node': 24.9.1 fsevents: 2.3.3 + tsx: 4.20.6 - vitest@4.0.1(@types/node@24.9.1): + vitest@4.0.1(@types/node@24.9.1)(tsx@4.20.6): dependencies: '@vitest/expect': 4.0.1 - '@vitest/mocker': 4.0.1(vite@7.1.12(@types/node@24.9.1)) + '@vitest/mocker': 4.0.1(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6)) '@vitest/pretty-format': 4.0.1 '@vitest/runner': 4.0.1 '@vitest/snapshot': 4.0.1 @@ -5155,7 +5295,7 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.12(@types/node@24.9.1) + vite: 7.1.12(@types/node@24.9.1)(tsx@4.20.6) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.1 @@ -5177,6 +5317,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@4.0.0-beta.3: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: