This commit is contained in:
Morten Olsen
2025-10-23 20:31:15 +02:00
parent b851dc3006
commit 78995406ca
46 changed files with 707 additions and 119 deletions

View File

@@ -96,6 +96,16 @@
"packageVersion": "1.0.0", "packageVersion": "1.0.0",
"packageName": "bootstrap" "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"
}
} }
] ]
} }

View File

@@ -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() { constructor() {
super(); this.#config = new KubeConfig();
this.loadFromDefault(); 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;
} }
} }

View File

@@ -1,6 +1,6 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1CustomResourceDefinition } from '@kubernetes/client-node'; import type { V1CustomResourceDefinition } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class CRD extends Resource<V1CustomResourceDefinition> { class CRD extends Resource<V1CustomResourceDefinition> {
public static readonly apiVersion = 'apiextensions.k8s.io/v1'; public static readonly apiVersion = 'apiextensions.k8s.io/v1';

View File

@@ -1,6 +1,7 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1Deployment } from '@kubernetes/client-node'; import type { V1Deployment } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class Deployment extends Resource<V1Deployment> { class Deployment extends Resource<V1Deployment> {
public static readonly apiVersion = 'apps/v1'; public static readonly apiVersion = 'apps/v1';
public static readonly kind = 'Deployment'; public static readonly kind = 'Deployment';

View File

@@ -1,6 +1,7 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1Namespace } from '@kubernetes/client-node'; import type { V1Namespace } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class Namespace extends Resource<V1Namespace> { class Namespace extends Resource<V1Namespace> {
public static readonly apiVersion = 'v1'; public static readonly apiVersion = 'v1';
public static readonly kind = 'Namespace'; public static readonly kind = 'Namespace';

View File

@@ -1,6 +1,7 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1PersistentVolume } from '@kubernetes/client-node'; import type { V1PersistentVolume } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class PersistentVolume extends Resource<V1PersistentVolume> { class PersistentVolume extends Resource<V1PersistentVolume> {
public static readonly apiVersion = 'v1'; public static readonly apiVersion = 'v1';
public static readonly kind = 'PersistentVolume'; public static readonly kind = 'PersistentVolume';

View File

@@ -1,23 +1,19 @@
import { Resource, type ResourceOptions } from '../resources/resource/resource.js';
import type { KubernetesObject, V1Secret } from '@kubernetes/client-node'; 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 extends Record<string, string | undefined>> = T | ((current: T | undefined) => T | Promise<T>); type SetOptions<T extends Record<string, string | undefined>> = T | ((current: T | undefined) => T | Promise<T>);
class Secret<T extends Record<string, string> = Record<string, string>> extends Resource<V1Secret> { class Secret extends Resource<V1Secret> {
public static readonly apiVersion = 'v1'; public static readonly apiVersion = 'v1';
public static readonly kind = 'Secret'; public static readonly kind = 'Secret';
constructor(options: ResourceOptions<V1Secret>) {
super(options);
}
public get value() { public get value() {
return decodeSecret(this.data) as T | undefined; return decodeSecret(this.data);
} }
public set = async (options: SetOptions<T>, data?: KubernetesObject) => { public set = async (options: SetOptions<Record<string, string>>, data?: KubernetesObject) => {
const value = typeof options === 'function' ? await Promise.resolve(options(this.value)) : options; const value = typeof options === 'function' ? await Promise.resolve(options(this.value)) : options;
await this.ensure({ await this.ensure({
...data, ...data,

View File

@@ -1,6 +1,7 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1Service } from '@kubernetes/client-node'; import type { V1Service } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class Service extends Resource<V1Service> { class Service extends Resource<V1Service> {
public static readonly apiVersion = 'v1'; public static readonly apiVersion = 'v1';
public static readonly kind = 'Service'; public static readonly kind = 'Service';

View File

@@ -1,6 +1,7 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1StatefulSet } from '@kubernetes/client-node'; import type { V1StatefulSet } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class StatefulSet extends Resource<V1StatefulSet> { class StatefulSet extends Resource<V1StatefulSet> {
public static readonly apiVersion = 'apps/v1'; public static readonly apiVersion = 'apps/v1';
public static readonly kind = 'StatefulSet'; public static readonly kind = 'StatefulSet';

View File

@@ -1,6 +1,7 @@
import { Resource } from '../resources/resource/resource.js';
import type { V1StorageClass } from '@kubernetes/client-node'; import type { V1StorageClass } from '@kubernetes/client-node';
import { Resource } from '../resources/resource/resource.js';
class StorageClass extends Resource<V1StorageClass> { class StorageClass extends Resource<V1StorageClass> {
public static readonly apiVersion = 'storage.k8s.io/v1'; public static readonly apiVersion = 'storage.k8s.io/v1';
public static readonly kind = 'StorageClass'; public static readonly kind = 'StorageClass';

View File

@@ -1,19 +1,10 @@
import { CRD } from "./core.crd.js"; import { CRD } from './core.crd.js';
import { Deployment } from "./core.deployment.js"; import { Deployment } from './core.deployment.js';
import { Namespace } from "./core.namespace.js"; import { Namespace } from './core.namespace.js';
import { PersistentVolume } from "./core.pv.js"; import { PersistentVolume } from './core.pv.js';
import { Secret } from "./core.secret.js"; import { Secret } from './core.secret.js';
import { Service } from "./core.service.js"; import { Service } from './core.service.js';
import { StatefulSet } from "./core.stateful-set.js"; import { StatefulSet } from './core.stateful-set.js';
import { StorageClass } from "./core.storage-class.js"; import { StorageClass } from './core.storage-class.js';
export { export { CRD, Deployment, Namespace, PersistentVolume, Secret, Service, StatefulSet, StorageClass };
CRD,
Deployment,
Namespace,
PersistentVolume,
Secret,
Service,
StatefulSet,
StorageClass,
}

View File

@@ -1,4 +1,5 @@
import { Services } from '@morten-olsen/box-utils/services'; import { Services } from '@morten-olsen/box-utils/services';
import { ResourceService } from './resources/resources.js'; import { ResourceService } from './resources/resources.js';
class K8sOperator { class K8sOperator {

View File

@@ -1,12 +1,14 @@
import { z, type ZodType } from 'zod'; 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 { CronJob, CronTime } from 'cron';
import { K8sConfig } from '../../config/config.js';
import { CoalescingQueue } from '@morten-olsen/box-utils/coalescing-queue'; 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 { 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({ const customResourceStatusSchema = z.object({
observedGeneration: z.number().optional(), observedGeneration: z.number().optional(),
@@ -35,7 +37,7 @@ class CustomResource<TSpec extends ZodType> extends Resource<
public static readonly apiVersion: string; public static readonly apiVersion: string;
public static readonly status = customResourceStatusSchema; public static readonly status = customResourceStatusSchema;
public static readonly labels: Record<string, string> = {}; public static readonly labels: Record<string, string> = {};
public static readonly dependsOn?: Resource<KubernetesObject>[]; public static readonly dependsOn?: ResourceClass<ExplicitAny>[];
#reconcileQueue: CoalescingQueue<void>; #reconcileQueue: CoalescingQueue<void>;
#cron: CronJob; #cron: CronJob;
@@ -45,9 +47,31 @@ class CustomResource<TSpec extends ZodType> extends Resource<
this.#reconcileQueue = new CoalescingQueue({ this.#reconcileQueue = new CoalescingQueue({
action: async () => { action: async () => {
try { try {
if (!this.exists || this.manifest?.metadata?.deletionTimestamp) { if (!this.exists) {
return; 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.markSeen();
await this.reconcile?.(); await this.reconcile?.();
await this.markReady(); await this.markReady();
@@ -111,6 +135,8 @@ class CustomResource<TSpec extends ZodType> extends Resource<
}; };
public reconcile?: () => Promise<void>; public reconcile?: () => Promise<void>;
public destroy?: () => Promise<void>;
public queueReconcile = () => { public queueReconcile = () => {
return this.#reconcileQueue.run(); return this.#reconcileQueue.run();
}; };
@@ -151,7 +177,7 @@ class CustomResource<TSpec extends ZodType> extends Resource<
public patchStatus = (status: Partial<z.infer<typeof customResourceStatusSchema>>) => public patchStatus = (status: Partial<z.infer<typeof customResourceStatusSchema>>) =>
this.queue.add(async () => { this.queue.add(async () => {
const config = this.services.get(K8sConfig); const config = this.services.get(K8sConfig);
const customObjectsApi = config.makeApiClient(CustomObjectsApi); const customObjectsApi = config.customObjectsApi;
if (this.scope === 'Cluster') { if (this.scope === 'Cluster') {
await customObjectsApi.patchClusterCustomObjectStatus( await customObjectsApi.patchClusterCustomObjectStatus(
{ {
@@ -180,4 +206,3 @@ class CustomResource<TSpec extends ZodType> extends Resource<
} }
export { CustomResource, type CustomResourceOptions }; export { CustomResource, type CustomResourceOptions };

View File

@@ -1,4 +1,5 @@
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
import type { ResourceClass } from '../resources.js'; import type { ResourceClass } from '../resources.js';
import type { ResourceEvents } from './resource.js'; import type { ResourceEvents } from './resource.js';
@@ -19,6 +20,9 @@ class ResourceReference<T extends ResourceClass<ExplicitAny>> extends EventEmitt
} }
public set current(value: InstanceType<T> | undefined) { public set current(value: InstanceType<T> | undefined) {
if (value === this.#current?.instance) {
return;
}
const previous = this.#current; const previous = this.#current;
this.#current?.unsubscribe(); this.#current?.unsubscribe();
if (value) { if (value) {

View File

@@ -1,11 +1,11 @@
import { ApiException, KubernetesObjectApi, PatchStrategy, type KubernetesObject } from '@kubernetes/client-node'; import { ApiException, KubernetesObjectApi, PatchStrategy, type KubernetesObject } from '@kubernetes/client-node';
import deepEqual from 'deep-equal'; import deepEqual from 'deep-equal';
import { ResourceService } from '../resources.service.js';
import type { Services } from '@morten-olsen/box-utils/services'; import type { Services } from '@morten-olsen/box-utils/services';
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
import { Queue } from '@morten-olsen/box-utils/queue'; import { Queue } from '@morten-olsen/box-utils/queue';
import { isDeepSubset } from '@morten-olsen/box-utils/objects'; import { isDeepSubset } from '@morten-olsen/box-utils/objects';
import { ResourceService } from '../resources.service.js';
import { K8sConfig } from '../../config/config.js'; import { K8sConfig } from '../../config/config.js';
type ResourceSelector = { type ResourceSelector = {
@@ -41,7 +41,7 @@ class Resource<T extends KubernetesObject> extends EventEmitter<ResourceEvents<T
return this.#queue; return this.#queue;
} }
public get services() { public get services(): Services {
return this.#options.services; return this.#options.services;
} }
@@ -144,7 +144,7 @@ class Resource<T extends KubernetesObject> extends EventEmitter<ResourceEvents<T
this.#queue.add(async () => { this.#queue.add(async () => {
const { services } = this.#options; const { services } = this.#options;
const config = services.get(K8sConfig); const config = services.get(K8sConfig);
const objectsApi = config.makeApiClient(KubernetesObjectApi); const { objectsApi } = config;
const body = { const body = {
...patch, ...patch,
apiVersion: this.selector.apiVersion, apiVersion: this.selector.apiVersion,
@@ -190,4 +190,3 @@ class Resource<T extends KubernetesObject> extends EventEmitter<ResourceEvents<T
} }
export { Resource, type ResourceOptions, type ResourceEvents }; export { Resource, type ResourceOptions, type ResourceEvents };

View File

@@ -1,16 +1,17 @@
import { ApiException, ApiextensionsV1Api, type KubernetesObject } from '@kubernetes/client-node'; import { ApiException, ApiextensionsV1Api, type KubernetesObject } from '@kubernetes/client-node';
import type { ZodType } from 'zod'; import type { ZodType } from 'zod';
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
import type { Services } from '@morten-olsen/box-utils/services';
import { WatcherService } from '../watchers/watchers.js'; import { WatcherService } from '../watchers/watchers.js';
import { K8sConfig } from '../config/config.js'; import { K8sConfig } from '../config/config.js';
import { createManifest } from './resources.utils.js'; import { createManifest } from './resources.utils.js';
import { Resource, type ResourceOptions } from './resource/resource.js'; import { Resource, type ResourceOptions } from './resource/resource.js';
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
import type { Services } from '@morten-olsen/box-utils/services';
type ResourceClass<T extends KubernetesObject> = (new (options: ResourceOptions<T>) => InstanceType<typeof Resource<T>>) & { type ResourceClass<T extends KubernetesObject> = (new (
options: ResourceOptions<T>,
) => InstanceType<typeof Resource<T>>) & {
apiVersion: string; apiVersion: string;
kind: string; kind: string;
plural?: string; plural?: string;
@@ -47,13 +48,17 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
public register = async (...resources: ResourceClass<ExplicitAny>[]) => { public register = async (...resources: ResourceClass<ExplicitAny>[]) => {
for (const resource of resources) { for (const resource of resources) {
if (!this.#registry.has(resource)) { if (this.#registry.has(resource)) {
return;
}
this.#registry.set(resource, { this.#registry.set(resource, {
apiVersion: resource.apiVersion, apiVersion: resource.apiVersion,
kind: resource.kind, kind: resource.kind,
plural: resource.plural, plural: resource.plural,
resources: [], resources: [],
}); });
if ('dependsOn' in resource && Array.isArray(resource.dependsOn)) {
await this.register(...resource.dependsOn as ResourceClass<ExplicitAny>[]);
} }
const watcherService = this.#services.get(WatcherService); const watcherService = this.#services.get(WatcherService);
const watcher = watcherService.create({ const watcher = watcherService.create({
@@ -65,7 +70,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
if (!name) { if (!name) {
return; return;
} }
const current = this.get(resource, name, namespace); const current = this.#get(resource, name, namespace, manifest);
current.manifest = manifest; current.manifest = manifest;
}); });
await watcher.start(); await watcher.start();
@@ -76,7 +81,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
return (this.#registry.get(type)?.resources?.filter((r) => r.exists) as InstanceType<T>[]) || []; return (this.#registry.get(type)?.resources?.filter((r) => r.exists) as InstanceType<T>[]) || [];
}; };
public get = <T extends ResourceClass<ExplicitAny>>(type: T, name: string, namespace?: string) => { #get = <T extends ResourceClass<ExplicitAny>>(type: T, name: string, namespace?: string, manifest?: unknown) => {
let resourceRegistry = this.#registry.get(type); let resourceRegistry = this.#registry.get(type);
if (!resourceRegistry) { if (!resourceRegistry) {
resourceRegistry = { resourceRegistry = {
@@ -98,6 +103,7 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
namespace, namespace,
}, },
services: this.#services, services: this.#services,
manifest,
}); });
current.on('changed', this.emit.bind(this, 'changed', current)); current.on('changed', this.emit.bind(this, 'changed', current));
resources.push(current); resources.push(current);
@@ -105,9 +111,13 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
return current as InstanceType<T>; return current as InstanceType<T>;
}; };
public get = <T extends ResourceClass<ExplicitAny>>(type: T, name: string, namespace?: string) => {
return this.#get(type, name, namespace);
};
public install = async (...resources: InstallableResourceClass<ExplicitAny>[]) => { public install = async (...resources: InstallableResourceClass<ExplicitAny>[]) => {
const config = this.#services.get(K8sConfig); const config = this.#services.get(K8sConfig);
const extensionsApi = config.makeApiClient(ApiextensionsV1Api); const { extensionsApi } = config;
for (const resource of resources) { for (const resource of resources) {
try { try {
const manifest = createManifest(resource); const manifest = createManifest(resource);
@@ -136,4 +146,3 @@ class ResourceService extends EventEmitter<ResourceServiceEvents> {
} }
export { ResourceService, Resource, type ResourceOptions, type ResourceClass, type InstallableResourceClass }; export { ResourceService, Resource, type ResourceOptions, type ResourceClass, type InstallableResourceClass };

View File

@@ -1,4 +1,9 @@
export { CustomResource, type CustomResourceOptions } from './resource/resource.custom.js'; export { CustomResource, type CustomResourceOptions } from './resource/resource.custom.js';
export { ResourceReference } from './resource/resource.reference.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';

View File

@@ -52,4 +52,3 @@ const createManifest = (defintion: InstallableResourceClass<ExplicitAny>) => {
}; };
export { createManifest }; export { createManifest };

View File

@@ -1,6 +1,7 @@
import { KubernetesObjectApi, makeInformer, type Informer, type KubernetesObject } from '@kubernetes/client-node'; import { KubernetesObjectApi, makeInformer, type Informer, type KubernetesObject } from '@kubernetes/client-node';
import { EventEmitter } from '@morten-olsen/box-utils/event-emitter'; import { EventEmitter } from '@morten-olsen/box-utils/event-emitter';
import type { Services } from '@morten-olsen/box-utils/services'; import type { Services } from '@morten-olsen/box-utils/services';
import { K8sConfig } from '../../config/config.js'; import { K8sConfig } from '../../config/config.js';
type ResourceChangedAction = 'add' | 'update' | 'delete'; type ResourceChangedAction = 'add' | 'update' | 'delete';
@@ -32,11 +33,10 @@ class Watcher<T extends KubernetesObject> extends EventEmitter<WatcherEvents<T>>
const { services, apiVersion, kind, selector } = this.#options; const { services, apiVersion, kind, selector } = this.#options;
const plural = this.#options.plural ?? kind.toLowerCase() + 's'; const plural = this.#options.plural ?? kind.toLowerCase() + 's';
const [version, group] = apiVersion.split('/').toReversed(); 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 path = group ? `/apis/${group}/${version}/${plural}` : `/api/${version}/${plural}`;
const objectsApi = config.makeApiClient(KubernetesObjectApi);
const informer = makeInformer<T>( const informer = makeInformer<T>(
config, kubeConfig,
path, path,
async () => { async () => {
return objectsApi.list(apiVersion, kind); return objectsApi.list(apiVersion, kind);
@@ -67,4 +67,3 @@ class Watcher<T extends KubernetesObject> extends EventEmitter<WatcherEvents<T>>
} }
export { Watcher, type WatcherOptions, type ResourceChangedAction }; export { Watcher, type WatcherOptions, type ResourceChangedAction };

View File

@@ -1,5 +1,6 @@
import { Services, destroy } from "@morten-olsen/box-utils/services"; import { Services, destroy } from '@morten-olsen/box-utils/services';
import { Watcher, type WatcherOptions } from "./watcher/watcher.js";
import { Watcher, type WatcherOptions } from './watcher/watcher.js';
class WatcherService { class WatcherService {
#services: Services; #services: Services;

View File

@@ -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"}

View File

@@ -1,4 +1,4 @@
import { z } from "@morten-olsen/box-k8s"; import { z } from '@morten-olsen/box-k8s';
const valueOrSecret = z.object({ const valueOrSecret = z.object({
value: z.string().optional(), value: z.string().optional(),
@@ -16,9 +16,11 @@ const serverSpec = z.object({
database: z.object({ database: z.object({
url: valueOrSecret, url: valueOrSecret,
}), }),
clients: z.object({ clients: z
substitutions: z.record(z.string(), z.string()).optional() .object({
}).optional(), substitutions: z.record(z.string(), z.string()).optional(),
})
.optional(),
}); });
export { serverSpec }; export { serverSpec };

View File

@@ -1,10 +1,7 @@
import { CustomResource, Secret, type CustomResourceOptions } from "@morten-olsen/box-k8s"; import { CustomResource, NotReadyError, Secret, type CustomResourceOptions } from '@morten-olsen/box-k8s';
import { serverSpec } from "./server.schemas.js";
import { API_VERSION } from '@morten-olsen/box-utils/consts'; import { API_VERSION } from '@morten-olsen/box-utils/consts';
type SecretData = { import { serverSpec } from './server.schemas.js';
secret: string;
};
class AuthentikServer extends CustomResource<typeof serverSpec> { class AuthentikServer extends CustomResource<typeof serverSpec> {
public static readonly apiVersion = API_VERSION; public static readonly apiVersion = API_VERSION;
@@ -12,16 +9,30 @@ class AuthentikServer extends CustomResource<typeof serverSpec> {
public static readonly spec = serverSpec; public static readonly spec = serverSpec;
public static readonly scope = 'Namespaced'; public static readonly scope = 'Namespaced';
#secret: Secret<SecretData>; #secret: Secret;
constructor(options: CustomResourceOptions<typeof serverSpec>) { constructor(options: CustomResourceOptions<typeof serverSpec>) {
super(options); super(options);
this.#secret = this.resources.get(Secret<SecretData>, `${this.name}-secret`, this.namespace); this.#secret = this.resources.get(Secret, `${this.name}-secret`, this.namespace);
this.#secret.on('changed', this.queueReconcile); this.#secret.on('changed', this.queueReconcile);
} }
public reconcile = async () => { 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 };

View File

@@ -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"}

View File

@@ -0,0 +1,4 @@
/node_modules/
/dist/
/coverage/
/.env

View File

@@ -0,0 +1,9 @@
apiVersion: playground.homelab.olsen.cloud/v1
kind: CloudflareAccount
metadata:
name: main
spec:
token:
secret: cloudflare
namespace: homelab
key: token

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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 };

View File

@@ -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<typeof cloudflareAccountSchema> {
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<typeof Secret>;
constructor(options: CustomResourceOptions<typeof cloudflareAccountSchema>) {
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 };

View File

@@ -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 };

View File

@@ -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<typeof cloudflareDnsRecordSchema> {
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 };

View File

@@ -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<string, Promise<Zone | undefined>>
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<typeof cloudflareDnsRecordSchema>) => {
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 };

View File

@@ -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<string, CloudflareAccount>;
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 };

View File

@@ -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,
)

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"src/**/*.ts"
],
"extends": "@morten-olsen/box-configs/tsconfig.json"
}

View File

@@ -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,
},
};
});

View File

@@ -0,0 +1 @@
{"root":["./src/exports.ts"],"version":"5.9.3"}

View File

@@ -1,4 +1,5 @@
const API_GROUP = 'playground.homelab.olsen.cloud'; const API_GROUP = 'playground.homelab.olsen.cloud';
const API_VERSION = `${API_GROUP}/v1`; const API_VERSION = `${API_GROUP}/v1`;
const FINALIZER = `finalizer.${API_GROUP}`;
export { API_VERSION, API_GROUP }; export { API_VERSION, API_GROUP, FINALIZER };

View File

@@ -23,7 +23,7 @@ class EventEmitter<T extends Record<string, (...args: ExplicitAny[]) => void | P
abortController.signal.addEventListener('abort', () => { abortController.signal.addEventListener('abort', () => {
this.#listeners.set(event, listeners?.difference(new Set([callbackClone]))); this.#listeners.set(event, listeners?.difference(new Set([callbackClone])));
}); });
return abortController.abort; return () => abortController.abort();
}; };
once = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => { once = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => {
@@ -62,4 +62,3 @@ class EventEmitter<T extends Record<string, (...args: ExplicitAny[]) => void | P
} }
export { EventEmitter }; export { EventEmitter };

View File

@@ -28,6 +28,6 @@ function isDeepSubset<T>(actual: ExplicitAny, expected: T): expected is T {
} }
return true; return true;
}; }
export { isDeepSubset }; export { isDeepSubset };

View File

@@ -38,4 +38,3 @@ class Queue {
} }
export { Queue }; export { Queue };

194
pnpm-lock.yaml generated
View File

@@ -34,7 +34,7 @@ importers:
version: 6.0.9(@pnpm/logger@5.2.0) version: 6.0.9(@pnpm/logger@5.2.0)
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: eslint:
specifier: 9.38.0 specifier: 9.38.0
version: 9.38.0 version: 9.38.0
@@ -61,7 +61,7 @@ importers:
version: 8.46.2(eslint@9.38.0)(typescript@5.9.3) version: 8.46.2(eslint@9.38.0)(typescript@5.9.3)
vitest: vitest:
specifier: 'catalog:' 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: packages/bootstrap:
devDependencies: devDependencies:
@@ -76,13 +76,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: {} packages/configs: {}
@@ -118,13 +118,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages/operator:
dependencies: dependencies:
@@ -137,13 +137,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages/resource-authentik:
dependencies: dependencies:
@@ -165,13 +165,47 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages/resource-postgres:
dependencies: dependencies:
@@ -193,13 +227,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages/resource-redis:
dependencies: dependencies:
@@ -221,13 +255,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages/tests:
dependencies: dependencies:
@@ -243,13 +277,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages/utils:
dependencies: dependencies:
@@ -271,13 +305,13 @@ importers:
version: 24.9.1 version: 24.9.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 'catalog:' 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: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: 'catalog:' 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: packages:
@@ -845,6 +879,9 @@ packages:
'@types/node-fetch@2.6.13': '@types/node-fetch@2.6.13':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@24.9.1': '@types/node@24.9.1':
resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==}
@@ -956,6 +993,10 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
hasBin: true hasBin: true
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:
@@ -970,6 +1011,10 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
agentkeepalive@4.6.0:
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
engines: {node: '>= 8.0.0'}
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1171,6 +1216,9 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
cloudflare@5.2.0:
resolution: {integrity: sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A==}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -1417,6 +1465,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} 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: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@@ -1488,10 +1540,17 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data@4.0.4: form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1530,6 +1589,9 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1597,6 +1659,9 @@ packages:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -1942,6 +2007,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true 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: node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@@ -2168,6 +2238,9 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve@1.22.11: resolve@1.22.11:
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2420,6 +2493,11 @@ packages:
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 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: turbo-darwin-64@2.5.8:
resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==} resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==}
cpu: [x64] cpu: [x64]
@@ -2498,6 +2576,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@7.16.0: undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
@@ -2588,6 +2669,10 @@ packages:
wcwidth@1.0.1: wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} 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: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -3213,6 +3298,10 @@ snapshots:
'@types/node': 24.9.1 '@types/node': 24.9.1
form-data: 4.0.4 form-data: 4.0.4
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
'@types/node@24.9.1': '@types/node@24.9.1':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
@@ -3318,7 +3407,7 @@ snapshots:
'@typescript-eslint/types': 8.46.2 '@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1 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: dependencies:
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.1 '@vitest/utils': 4.0.1
@@ -3331,7 +3420,7 @@ snapshots:
magicast: 0.3.5 magicast: 0.3.5
std-env: 3.10.0 std-env: 3.10.0
tinyrainbow: 3.0.3 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: transitivePeerDependencies:
- supports-color - supports-color
@@ -3344,13 +3433,13 @@ snapshots:
chai: 6.2.0 chai: 6.2.0
tinyrainbow: 3.0.3 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: dependencies:
'@vitest/spy': 4.0.1 '@vitest/spy': 4.0.1
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.19 magic-string: 0.30.19
optionalDependencies: 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': '@vitest/pretty-format@4.0.1':
dependencies: dependencies:
@@ -3378,6 +3467,10 @@ snapshots:
dependencies: dependencies:
isexe: 2.0.0 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): acorn-jsx@5.3.2(acorn@8.15.0):
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
@@ -3386,6 +3479,10 @@ snapshots:
agent-base@7.1.4: {} agent-base@7.1.4: {}
agentkeepalive@4.6.0:
dependencies:
humanize-ms: 1.2.1
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -3613,6 +3710,18 @@ snapshots:
clone@1.0.4: {} 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: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -3991,6 +4100,8 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
event-target-shim@5.0.1: {}
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
events-universal@1.0.1: events-universal@1.0.1:
@@ -4065,6 +4176,8 @@ snapshots:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
form-data-encoder@1.7.2: {}
form-data@4.0.4: form-data@4.0.4:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
@@ -4073,6 +4186,11 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
mime-types: 2.1.35 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: fsevents@2.3.3:
optional: true optional: true
@@ -4122,6 +4240,10 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 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: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -4173,6 +4295,10 @@ snapshots:
human-signals@2.1.0: {} human-signals@2.1.0: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
ignore@5.3.2: {} ignore@5.3.2: {}
ignore@7.0.5: {} ignore@7.0.5: {}
@@ -4488,6 +4614,8 @@ snapshots:
split2: 3.2.2 split2: 3.2.2
through2: 4.0.2 through2: 4.0.2
node-domexception@1.0.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
@@ -4707,6 +4835,8 @@ snapshots:
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
resolve@1.22.11: resolve@1.22.11:
dependencies: dependencies:
is-core-module: 2.16.1 is-core-module: 2.16.1
@@ -5023,6 +5153,13 @@ snapshots:
tslib@2.8.1: {} 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: turbo-darwin-64@2.5.8:
optional: true optional: true
@@ -5111,6 +5248,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
which-boxed-primitive: 1.1.1 which-boxed-primitive: 1.1.1
undici-types@5.26.5: {}
undici-types@7.16.0: {} undici-types@7.16.0: {}
unique-string@2.0.0: unique-string@2.0.0:
@@ -5123,7 +5262,7 @@ snapshots:
util-deprecate@1.0.2: {} 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: dependencies:
esbuild: 0.25.11 esbuild: 0.25.11
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -5134,11 +5273,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 24.9.1 '@types/node': 24.9.1
fsevents: 2.3.3 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: dependencies:
'@vitest/expect': 4.0.1 '@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/pretty-format': 4.0.1
'@vitest/runner': 4.0.1 '@vitest/runner': 4.0.1
'@vitest/snapshot': 4.0.1 '@vitest/snapshot': 4.0.1
@@ -5155,7 +5295,7 @@ snapshots:
tinyexec: 0.3.2 tinyexec: 0.3.2
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinyrainbow: 3.0.3 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 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.9.1 '@types/node': 24.9.1
@@ -5177,6 +5317,8 @@ snapshots:
dependencies: dependencies:
defaults: 1.0.4 defaults: 1.0.4
web-streams-polyfill@4.0.0-beta.3: {}
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
whatwg-url@5.0.0: whatwg-url@5.0.0: