From 4cad016431594ed88b8b2cf24619699b26d070ae Mon Sep 17 00:00:00 2001 From: Ho Phuoc Hoan Date: Sun, 15 Mar 2026 10:49:33 +0700 Subject: [PATCH 1/2] feat: implement automated secret injection and NestJS Prisma template (#39) Add Phase 0.9 to the operator reconcile loop that injects DB_HOST, DB_USER, DB_PASS env vars (via secretKeyRef) into backend Deployments. Create a NestJS + Prisma Backstage template with Prisma 7 driver adapter support. Operator changes: - Add InjectDatabaseEnvVars() and reconcileDatabaseSecretInjection() to patch live Deployments with database credential references - Add 6 unit/integration tests for injection logic (idempotency, empty containers, missing deployments) - Update RBAC role for statefulsets and ingresses - Bump golangci-lint to v2.11.3, CUE language to v0.16.0 NestJS Prisma template: - Full Backstage scaffolder template with NestJS 11 + Prisma 7 + pg adapter - prisma.config.ts for Prisma 7 datasource URL (replaces schema.prisma url) - PrismaService with namespace import (import * as pg) for CommonJS compat - Multi-stage Dockerfile with migration support - Helios GitOps manifest with database trait Documentation: - Manual verification guide for k3d cluster testing - Implementation plan, task tracking, and walkthrough docs - Claude Code skills for implementation workflow and Go/CUE guidelines Co-Authored-By: Claude Opus 4.6 --- .claude/skills/implement-task/SKILL.md | 137 + .claude/skills/use-modern-cue/SKILL.md | 361 + .claude/skills/use-modern-go/SKILL.md | 291 + apps/operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 + apps/operator/config/rbac/role.yaml | 13 + apps/operator/go.mod | 101 +- apps/operator/go.sum | 206 +- .../internal/controller/database_resources.go | 99 + .../controller/database_resources_test.go | 296 +- .../controller/heliosapp_controller.go | 12 + .../content/gitops/helios-app.yaml | 35 + .../content/source/.env.example | 9 + .../content/source/Dockerfile | 40 + .../content/source/catalog-info.yaml | 12 + .../content/source/nest-cli.json | 8 + .../content/source/package-lock.json | 8987 +++++++++++++++++ .../content/source/package.json | 58 + .../content/source/prisma.config.ts | 27 + .../prisma/migrations/migration_lock.toml | 3 + .../content/source/prisma/schema.prisma | 23 + .../content/source/src/app.controller.ts | 17 + .../content/source/src/app.module.ts | 11 + .../content/source/src/app.service.ts | 15 + .../content/source/src/main.ts | 13 + .../source/src/prisma/prisma.module.ts | 9 + .../source/src/prisma/prisma.service.ts | 61 + .../content/source/tsconfig.build.json | 4 + .../content/source/tsconfig.json | 24 + .../nestjs-prisma-template/template.yaml | 190 + cue/cue.mod/module.cue | 2 +- docs/manual-verification-guide.md | 540 + implementation_plan.md | 115 + task.md | 28 + walkthrough.md | 2596 +++++ 35 files changed, 14203 insertions(+), 144 deletions(-) create mode 100644 .claude/skills/implement-task/SKILL.md create mode 100644 .claude/skills/use-modern-cue/SKILL.md create mode 100644 .claude/skills/use-modern-go/SKILL.md create mode 100644 apps/portal/examples/nestjs-prisma-template/content/gitops/helios-app.yaml create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/.env.example create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/Dockerfile create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/nest-cli.json create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/package-lock.json create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/package.json create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/prisma.config.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/prisma/migrations/migration_lock.toml create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/src/app.controller.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/src/app.module.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/src/app.service.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.module.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.service.ts create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.build.json create mode 100644 apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.json create mode 100644 apps/portal/examples/nestjs-prisma-template/template.yaml create mode 100644 docs/manual-verification-guide.md create mode 100644 implementation_plan.md create mode 100644 task.md create mode 100644 walkthrough.md diff --git a/.claude/skills/implement-task/SKILL.md b/.claude/skills/implement-task/SKILL.md new file mode 100644 index 0000000..30e8753 --- /dev/null +++ b/.claude/skills/implement-task/SKILL.md @@ -0,0 +1,137 @@ +--- +name: implement-task +description: Implementation workflow for the Helios Platform operator. Use when implementing features, fixing bugs, or making any code changes. Enforces best practices, testing, and verification. +--- + +# Implementation Task Workflow + +## Detected Stack + +!`grep -rh "^go " --include="go.mod" . 2>/dev/null | head -1 | grep . && grep -rh 'language.*version' --include="module.cue" cue.mod/ cue/cue.mod/ 2>/dev/null | head -1 | grep . || echo "unknown stack"` + +## Workflow + +When asked to implement a task, follow this exact sequence. Do NOT skip steps. + +--- + +### 1. Understand & Plan + +- Read and understand the full scope of the task +- Identify all files that need to change +- Check existing code patterns in the codebase — follow them +- If the task is non-trivial (>3 files), create a brief plan and get user approval before coding + +### 2. Implement with Best Practices + +**Code quality principles — apply ALL of these:** + +- **SOLID** — Single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion +- **DRY** — Don't repeat yourself. Extract reusable functions, constants, and definitions +- **KISS** — Keep it simple. Prefer clarity over cleverness +- **YAGNI** — Don't build what isn't needed yet +- **Separation of Concerns** — Each module/file/function has one clear purpose +- **Defensive Programming** — Validate inputs, handle edge cases, never swallow errors +- **Fail Fast** — Return errors immediately, don't accumulate them silently + +**Go-specific (use `use-modern-go` skill):** + +- Use modern Go idioms: `any`, `cmp.Or`, `slices`, `maps`, `errors.Is/As` +- Proper error wrapping with `fmt.Errorf("context: %w", err)` +- Table-driven tests with descriptive names +- Context propagation for cancellation and timeouts +- Meaningful variable/function names — no single-letter names outside loops +- Comments on exported types and functions (godoc convention) + +**CUE-specific (use `use-modern-cue` skill):** + +- Use `#` definitions for schemas, `_` for hidden fields, `let` for intermediates +- Validate with constraints, not imperative logic +- Follow the module's established patterns (registries, builders, etc.) +- File-level `@extern(embed)` when using `@embed` + +**Non-functional requirements:** + +- **Performance** — avoid unnecessary allocations, use efficient algorithms +- **Reliability** — handle all error paths, use retries where appropriate +- **Observability** — add structured logging at key decision points +- **Security** — no hardcoded secrets, validate all external input + +### 3. Test Everything + +After implementation, run ALL of these. Do NOT stop at the first passing test. + +```sh +# From apps/operator/ directory: + +# 1. Compilation check — must pass cleanly +go build ./... + +# 2. Static analysis +go vet ./... + +# 3. Full test suite with envtest (real K8s API server) +make test + +# 4. Build the operator binary +make build + +# 5. Verify CUE evaluation (if CUE files changed) +# Run CUE-related tests which validate CUE rendering +go test ./internal/cue/... -v -count=1 +``` + +**If any test fails:** +1. Read the error output carefully +2. Fix the root cause (not symptoms) +3. Re-run ALL tests, not just the one that failed +4. If a fix is complex or risky, explain the issue and ask the user before proceeding + +**If a dependency/infrastructure issue prevents testing:** +1. Document what couldn't be tested and why +2. Explain the risk to the user +3. Suggest how to test manually + +### 4. Verify & Report + +After all tests pass, verify: + +- [ ] `go build ./...` — compiles cleanly +- [ ] `go vet ./...` — no issues +- [ ] `make test` — all tests pass with envtest +- [ ] `make build` — operator binary builds +- [ ] No warnings in any output +- [ ] CRD manifests regenerated if API types changed (`controller-gen` runs in `make build`) + +**Report to user:** +- What was implemented (brief) +- What was tested (specific results) +- Any caveats or follow-up items + +--- + +## Rollback Strategy + +If something doesn't work after implementation: + +1. Identify the specific change that broke things +2. Revert that change only (not everything) +3. Re-run all tests to confirm the rollback works +4. Explain what went wrong and propose an alternative approach + +--- + +## Project-Specific Context + +**Directory structure:** +- `apps/operator/` — Go operator (controllers, CUE engine, GitOps) +- `cue/` — CUE definitions (schemas, tekton, components, traits, engine) +- `apps/operator/internal/cue/` — Go↔CUE bridge (engine.go, tekton.go) +- `apps/operator/internal/controller/` — Kubernetes controllers +- `apps/operator/internal/gitops/` — Git operations + +**Key patterns:** +- CUE handles resource generation (Tasks, Pipeline, Triggers, Ingress) +- Go handles runtime resources (PipelineRun, RBAC, ArgoCD Application) +- Controller reconciles HeliosApp CRD → CUE rendering → GitOps sync → ArgoCD +- Tests use envtest (real K8s API) + CUE engine unit tests + E2E validation tests diff --git a/.claude/skills/use-modern-cue/SKILL.md b/.claude/skills/use-modern-cue/SKILL.md new file mode 100644 index 0000000..9c6d0ee --- /dev/null +++ b/.claude/skills/use-modern-cue/SKILL.md @@ -0,0 +1,361 @@ +--- +name: use-modern-cue +description: Apply modern CUE language syntax guidelines based on project's CUE version. Use when user asks for modern CUE code guidelines. +--- + +# Modern CUE Guidelines + +## Detected CUE Version + +!`grep -rh '"language"' --include="module.cue" cue.mod/ 2>/dev/null | grep -oP 'version:\s*"v?\K[^"]+' | head -1 | grep . || cue version 2>/dev/null | head -1 | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1 | grep . || echo unknown` + +## How to Use This Skill + +DO NOT search for module.cue files or try to detect the version yourself. Use ONLY the version shown above. + +**If version detected (not "unknown"):** + +- Say: "This project is using CUE X.XX.X, so I'll stick to modern CUE best practices and freely use language features up to and including this version. If you'd prefer a different target version, just let me know." +- Do NOT list features, do NOT ask for confirmation + +**If version is "unknown":** + +- Say: "Could not detect CUE version in this repository" +- Use AskUserQuestion: "Which CUE version should I target?" → [0.14] / [0.15] / [0.16] + +**When writing CUE code**, use ALL features from this document up to the target version: + +- Prefer modern patterns over deprecated/legacy ones +- Never use features from newer CUE versions than the target +- Always follow the best practices and avoid anti-patterns listed below + +--- + +## Core CUE Principles + +### Types Are Values + +CUE unifies types and values into a single lattice. A field can hold a type constraint, a concrete value, or something in between. + +```cue +port: int // type constraint +port: >0 & <65536 // narrower constraint +port: 8080 // concrete value +``` + +### Unification (`&`) + +Fundamental, commutative, associative, and idempotent — order never matters. + +```cue +a: {name: string, port: int} +a: {name: "web", replicas: 1} +// Result: a: {name: "web", port: int, replicas: 1} +``` + +### Definitions (`#`) and Hidden Definitions (`_#`) + +`#` defines schemas — closed structs, NOT emitted in output. `_#` for package-internal schemas. + +```cue +#Service: { + name: string + port: int & >0 & <=65535 + replicas: int | *1 +} + +myService: #Service & {name: "api", port: 8080} + +_#internalConfig: {debug: bool | *false} +``` + +### Hidden Fields (`_`) + +Not exported, exempt from closed struct rules. Use for internal values. + +```cue +_basePort: 8000 +services: { + api: {port: _basePort} + grpc: {port: _basePort + 1000} +} +``` + +### Disjunctions (`|`), Defaults (`*`), Optional (`?`), Required (`!`) + +```cue +#Deployment: { + name!: string // MUST be specified + protocol: "HTTP" | "HTTPS" | *"HTTPS" // enum with default + replicas?: int & >0 // MAY be specified + annotations?: {[string]: string} +} +``` + +### Closed vs Open Structs + +Definitions are closed by default. Add `...` to allow additional fields. + +```cue +#Strict: {name: string, port: int} // no extra fields +#Flexible: {name: string, port: int, ...} // extra fields allowed +``` + +### Pattern Constraints, Comprehensions, Interpolation, `let` + +```cue +// Pattern constraint: all string-keyed fields must satisfy #Service +[string]: #Service + +// List comprehension +ports: [for s in services {s.port}] + +// Field comprehension with conditional +rendered: { + for name, svc in services { + (name): {image: "reg/\(name):latest", port: svc.port} + } +} + +// Conditional field inclusion +if env == "prod" {replicas: 3} + +// let for intermediate computation (not emitted) +let _fullName = "app-\(name)" +metadata: labels: app: _fullName +``` + +### Validators and Constraints + +```cue +import "strings" + +#Label: { + key: string & =~"^[a-z][a-z0-9-]*$" + value: strings.MinRunes(1) & strings.MaxRunes(63) +} +#Port: int & >0 & <=65535 +``` + +### Builtin Functions + +```cue +len("hello") // 5 +close({a: 1}) // closes an open struct +and([int, >0, <100]) // unifies: int & >0 & <100 +or(["a", "b", "c"]) // disjunction: "a" | "b" | "c" +``` + +--- + +## Standard Library (Key Packages) + +```cue +import ("strings"; "list"; "regexp"; "math"; "encoding/json"; "encoding/yaml"; "struct"; "net"; "strconv") + +// strings +strings.Join(["a","b"], ",") strings.Split("a,b", ",") strings.Contains("ab", "a") +strings.HasPrefix/HasSuffix strings.Replace strings.ToUpper/ToLower strings.TrimSpace +strings.MinRunes(1) strings.MaxRunes(63) + +// list +list.Concat([[1,2],[3,4]]) list.Repeat([0],3) list.Contains([1,2],2) +list.Sort([3,1,2], list.Ascending) list.FlattenN list.UniqueItems +list.MinItems(1) list.MaxItems(10) + +// regexp +regexp.Match("^[a-z]+$", "hello") regexp.Find regexp.FindAll regexp.ReplaceAll + +// math +math.Floor math.Ceil math.Abs math.Pow + +// encoding +json.Marshal({a:1}) json.Unmarshal(str) json.Validate(str, schema) +yaml.Marshal yaml.Validate + +// struct +struct.MinFields(1) struct.MaxFields(10) + +// net +net.IPv4 net.IP net.FQDN +net.InCIDR net.ParseCIDR net.CompareIP // v0.16+ + +// strconv +strconv.ParseNumber // v0.16+: parse CUE numbers like "1Ki" +``` + +--- + +## Breaking Changes by Version + +| Version | Breaking Change | +|---------|----------------| +| **v0.9** | `language.version` REQUIRED in `module.cue` | +| **v0.11** | List arithmetic (`+`, `*`) removed → use `list.Concat`, `list.Repeat` | +| **v0.12** | `cue.Value.Decode` returns `int64` (not `int`) for CUE integers | +| **v0.12** | `@embed` stable — requires `@extern(embed)` file attribute | +| **v0.13** | Removed deprecated `cue.Runtime` methods | +| **v0.14** | Multiline strings require trailing newline | +| **v0.14** | Custom errors: `error("msg")` builtin | +| **v0.14** | JSON Schema generation: `cue def --out jsonschema` | +| **v0.16** | Multiline trailing newline **strictly enforced** | +| **v0.16** | `cue` commands inside `cue.mod/` directory now fail | +| **v0.16** | `cue mod publish` no longer ignores sub-dirs with `go.mod` | +| **v0.16** | `#"""#` accepted as string literal for `"` | +| **v0.16** | `cmdreferencepkg` experiment stabilized (always on) | +| **v0.16** | Removed deprecated: `cue/ast.Node.Comments`, `cue/parser.FromVersion`, `cue.Instance.Eval` | + +## Key Features by Version + +| Version | Feature | +|---------|---------| +| **v0.10** | `@if(tag)` conditional file inclusion, `@tag(name)` value injection, `@ignore()` | +| **v0.12** | File embedding: `@extern(embed)` + `@embed(file="x.json")` | +| **v0.13** | `cue refactor imports`, `cue mod mirror`, keywords as field labels | +| **v0.14** | `error()` builtin, `cue def --out jsonschema`, `cue fix --exp`, `cue help experiments` | +| **v0.15** | Full LSP support (go-to-def, find-refs, rename, completion, hover) | +| **v0.16** | `net.InCIDR/ParseCIDR/CompareIP`, `strconv.ParseNumber`, `tool/file.Symlink` | +| **v0.16** | Up to 80% faster evaluation, 60% less memory, embedded JSON/YAML LSP support | + +--- + +## Module & Package Best Practices + +### Module Declaration (v0.9+, REQUIRED) + +```cue +// cue.mod/module.cue +module: "github.com/myorg/myproject@v0" +language: version: "v0.16.0" + +deps: { + "github.com/some/dep@v0": v: "v0.1.0" +} +``` + +**ALWAYS** declare `language.version`. Never omit it. + +### Import Best Practices + +```cue +import ( + "strings" // built-in (no domain) + "list" + "github.com/myorg/project/schema" // user-defined (fully-qualified) + k8s "k8s.io/api/apps/v1" // named import for conflicts +) +``` + +- ALWAYS use absolute import paths (no relative imports) +- Group built-in and user-defined imports separately + +### Package Organization + +- One package per directory +- `package` declaration at top of every file +- Definitions in same package accessible across files without imports +- Hidden fields (`_`) scoped to package — not externally accessible +- Do NOT place CUE code inside `cue.mod/` (v0.16+: this errors) + +### File Embedding (v0.12+) + +```cue +@extern(embed) +package config + +data: _ @embed(file="config.json") +readme: string @embed(file="README.md", type=text) +cert: bytes @embed(file="cert.pem", type=binary) +templates: _ @embed(glob="templates/*.json") +optional: _ @embed(glob="overrides/*.yaml", allowEmptyGlob) +``` + +ALWAYS use `@extern(embed)` at file level. Only files within the same CUE module can be embedded. + +--- + +## CLI Quick Reference + +```sh +cue mod init github.com/myorg/project@v0 # init module +cue mod tidy # clean deps +cue fmt ./... # format +cue eval ./... # evaluate +cue eval -c ./... # require concreteness +cue vet data.yaml schema.cue -d '#Schema' # validate +cue export ./... --out json # export JSON +cue export ./... --out yaml # export YAML +cue def --out jsonschema ./... # JSON Schema (v0.14+) +cue fix ./... # fix deprecated syntax +cue get go k8s.io/api/apps/v1 # generate CUE from Go +cue refactor imports old/path new/path # refactor imports (v0.13+) +``` + +--- + +## Anti-Patterns + +### ❌ List arithmetic (removed in v0.11) + +```cue +// BAD // GOOD +combined: [1,2] + [3,4] import "list" +repeated: [0] * 3 combined: list.Concat([[1,2],[3,4]]) + repeated: list.Repeat([0], 3) +``` + +### ❌ Missing `language.version` + +```cue +// BAD // GOOD +module: "example.com/foo@v0" module: "example.com/foo@v0" + language: version: "v0.16.0" +``` + +### ❌ Overly permissive types + +```cue +// BAD — defeats CUE's purpose // GOOD — constrain narrowly +config: _ config: #AppConfig +``` + +### ❌ Concrete values in definitions + +```cue +// BAD — mixes data and schema // GOOD — definitions are constraints +#Service: {name: "my-svc"} #Service: {name: string & strings.MinRunes(1)} +``` + +### ❌ Repeated constraints + +```cue +// BAD // GOOD +svcA: port: int & >0 & <=65535 #Port: int & >0 & <=65535 +svcB: port: int & >0 & <=65535 svcA: port: #Port + svcB: port: #Port +``` + +### ❌ `@embed` without `@extern(embed)` (v0.12+) + +```cue +// BAD // GOOD +package config @extern(embed) +data: _ @embed(file="d.json") package config + data: _ @embed(file="d.json") +``` + +### ❌ Relative imports + +```cue +// BAD // GOOD +import "./utils" import "github.com/myorg/project/utils" +``` + +### ❌ Multiline strings without trailing newline (v0.14+) + +```cue +// BAD // GOOD +msg: """ msg: """ + Hello""" Hello + """ +``` diff --git a/.claude/skills/use-modern-go/SKILL.md b/.claude/skills/use-modern-go/SKILL.md new file mode 100644 index 0000000..3755a1a --- /dev/null +++ b/.claude/skills/use-modern-go/SKILL.md @@ -0,0 +1,291 @@ +--- +name: use-modern-go +description: Apply modern Go syntax guidelines based on project's Go version. Use when user ask for modern Go code guidelines. +--- + +# Modern Go Guidelines + +## Detected Go Version + +!`grep -rh "^go " --include="go.mod" . 2>/dev/null | cut -d' ' -f2 | sort | uniq -c | sort -nr | head -1 | xargs | cut -d' ' -f2 | grep . || echo unknown` + +## How to Use This Skill + +DO NOT search for go.mod files or try to detect the version yourself. Use ONLY the version shown above. + +**If version detected (not "unknown"):** +- Say: "This project is using Go X.XX, so I’ll stick to modern Go best practices and freely use language features up to and including this version. If you’d prefer a different target version, just let me know." +- Do NOT list features, do NOT ask for confirmation + +**If version is "unknown":** +- Say: "Could not detect Go version in this repository" +- Use AskUserQuestion: "Which Go version should I target?" → [1.23] / [1.24] / [1.25] / [1.26] + +**When writing Go code**, use ALL features from this document up to the target version: +- Prefer modern built-ins and packages (`slices`, `maps`, `cmp`) over legacy patterns +- Never use features from newer Go versions than the target +- Never use outdated patterns when a modern alternative is available + +--- + +## Features by Go Version + +### Go 1.0+ + +- `time.Since`: `time.Since(start)` instead of `time.Now().Sub(start)` + +### Go 1.8+ + +- `time.Until`: `time.Until(deadline)` instead of `deadline.Sub(time.Now())` + +### Go 1.13+ + +- `errors.Is`: `errors.Is(err, target)` instead of `err == target` (works with wrapped errors) + +### Go 1.18+ + +- `any`: Use `any` instead of `interface{}` +- `bytes.Cut`: `before, after, found := bytes.Cut(b, sep)` instead of Index+slice +- `strings.Cut`: `before, after, found := strings.Cut(s, sep)` + +### Go 1.19+ + +- `fmt.Appendf`: `buf = fmt.Appendf(buf, "x=%d", x)` instead of `[]byte(fmt.Sprintf(...))` +- `atomic.Bool`/`atomic.Int64`/`atomic.Pointer[T]`: Type-safe atomics instead of `atomic.StoreInt32` + +```go +var flag atomic.Bool +flag.Store(true) +if flag.Load() { ... } + +var ptr atomic.Pointer[Config] +ptr.Store(cfg) +``` + +### Go 1.20+ + +- `strings.Clone`: `strings.Clone(s)` to copy string without sharing memory +- `bytes.Clone`: `bytes.Clone(b)` to copy byte slice +- `strings.CutPrefix/CutSuffix`: `if rest, ok := strings.CutPrefix(s, "pre:"); ok { ... }` +- `errors.Join`: `errors.Join(err1, err2)` to combine multiple errors +- `context.WithCancelCause`: `ctx, cancel := context.WithCancelCause(parent)` then `cancel(err)` +- `context.Cause`: `context.Cause(ctx)` to get the error that caused cancellation + +### Go 1.21+ + +**Built-ins:** +- `min`/`max`: `max(a, b)` instead of if/else comparisons +- `clear`: `clear(m)` to delete all map entries, `clear(s)` to zero slice elements + +**slices package:** +- `slices.Contains`: `slices.Contains(items, x)` instead of manual loops +- `slices.Index`: `slices.Index(items, x)` returns index (-1 if not found) +- `slices.IndexFunc`: `slices.IndexFunc(items, func(item T) bool { return item.ID == id })` +- `slices.SortFunc`: `slices.SortFunc(items, func(a, b T) int { return cmp.Compare(a.X, b.X) })` +- `slices.Sort`: `slices.Sort(items)` for ordered types +- `slices.Max`/`slices.Min`: `slices.Max(items)` instead of manual loop +- `slices.Reverse`: `slices.Reverse(items)` instead of manual swap loop +- `slices.Compact`: `slices.Compact(items)` removes consecutive duplicates in-place +- `slices.Clip`: `slices.Clip(s)` removes unused capacity +- `slices.Clone`: `slices.Clone(s)` creates a copy + +**maps package:** +- `maps.Clone`: `maps.Clone(m)` instead of manual map iteration +- `maps.Copy`: `maps.Copy(dst, src)` copies entries from src to dst +- `maps.DeleteFunc`: `maps.DeleteFunc(m, func(k K, v V) bool { return condition })` + +**sync package:** +- `sync.OnceFunc`: `f := sync.OnceFunc(func() { ... })` instead of `sync.Once` + wrapper +- `sync.OnceValue`: `getter := sync.OnceValue(func() T { return computeValue() })` + +**context package:** +- `context.AfterFunc`: `stop := context.AfterFunc(ctx, cleanup)` runs cleanup on cancellation +- `context.WithTimeoutCause`: `ctx, cancel := context.WithTimeoutCause(parent, d, err)` +- `context.WithDeadlineCause`: Similar with deadline instead of duration + +### Go 1.22+ + +**Loops:** +- `for i := range n`: `for i := range len(items)` instead of `for i := 0; i < len(items); i++` +- Loop variables are now safe to capture in goroutines (each iteration has its own copy) + +**cmp package:** +- `cmp.Or`: `cmp.Or(flag, env, config, "default")` returns first non-zero value + +```go +// Instead of: +name := os.Getenv("NAME") +if name == "" { + name = "default" +} +// Use: +name := cmp.Or(os.Getenv("NAME"), "default") +``` + +**reflect package:** +- `reflect.TypeFor`: `reflect.TypeFor[T]()` instead of `reflect.TypeOf((*T)(nil)).Elem()` + +**net/http:** +- Enhanced `http.ServeMux` patterns: `mux.HandleFunc("GET /api/{id}", handler)` with method and path params +- `r.PathValue("id")` to get path parameters + +### Go 1.23+ + +- `maps.Keys(m)` / `maps.Values(m)` return iterators +- `slices.Collect(iter)` not manual loop to build slice from iterator +- `slices.Sorted(iter)` to collect and sort in one step + +```go +keys := slices.Collect(maps.Keys(m)) // not: for k := range m { keys = append(keys, k) } +sortedKeys := slices.Sorted(maps.Keys(m)) // collect + sort +for k := range maps.Keys(m) { process(k) } // iterate directly +``` + +**time package** + +- `time.Tick`: Use `time.Tick` freely — as of Go 1.23, the garbage collector can recover unreferenced tickers, even if they haven't been stopped. The Stop method is no longer necessary to help the garbage collector. There is no longer any reason to prefer NewTicker when Tick will do. + +### Go 1.24+ + +- `t.Context()` not `context.WithCancel(context.Background())` in tests. + ALWAYS use t.Context() when a test function needs a context. + +Before: +```go +func TestFoo(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + result := doSomething(ctx) +} +``` +After: +```go +func TestFoo(t *testing.T) { + ctx := t.Context() + result := doSomething(ctx) +} +``` + +- `omitzero` not `omitempty` in JSON struct tags. + ALWAYS use omitzero for time.Duration, time.Time, structs, slices, maps. + +Before: +```go +type Config struct { + Timeout time.Duration `json:"timeout,omitempty"` // doesn't work for Duration! +} +``` +After: +```go +type Config struct { + Timeout time.Duration `json:"timeout,omitzero"` +} +``` + +- `b.Loop()` not `for i := 0; i < b.N; i++` in benchmarks. + ALWAYS use b.Loop() for the main loop in benchmark functions. + +Before: +```go +func BenchmarkFoo(b *testing.B) { + for i := 0; i < b.N; i++ { + doWork() + } +} +``` +After: +```go +func BenchmarkFoo(b *testing.B) { + for b.Loop() { + doWork() + } +} +``` + +- `strings.SplitSeq` not `strings.Split` when iterating. + ALWAYS use SplitSeq/FieldsSeq when iterating over split results in a for-range loop. + +Before: +```go +for _, part := range strings.Split(s, ",") { + process(part) +} +``` +After: +```go +for part := range strings.SplitSeq(s, ",") { + process(part) +} +``` +Also: `strings.FieldsSeq`, `bytes.SplitSeq`, `bytes.FieldsSeq`. + +### Go 1.25+ + +- `wg.Go(fn)` not `wg.Add(1)` + `go func() { defer wg.Done(); ... }()`. + ALWAYS use wg.Go() when spawning goroutines with sync.WaitGroup. + +Before: +```go +var wg sync.WaitGroup +for _, item := range items { + wg.Add(1) + go func() { + defer wg.Done() + process(item) + }() +} +wg.Wait() +``` +After: +```go +var wg sync.WaitGroup +for _, item := range items { + wg.Go(func() { + process(item) + }) +} +wg.Wait() +``` + +### Go 1.26+ + +- `new(val)` not `x := val; &x` — returns pointer to any value. + Go 1.26 extends new() to accept expressions, not just types. + Type is inferred: new(0) → *int, new("s") → *string, new(T{}) → *T. + DO NOT use `x := val; &x` pattern — always use new(val) directly. + DO NOT use redundant casts like new(int(0)) — just write new(0). + Common use case: struct fields with pointer types. + +Before: +```go +timeout := 30 +debug := true +cfg := Config{ + Timeout: &timeout, + Debug: &debug, +} +``` +After: +```go +cfg := Config{ + Timeout: new(30), // *int + Debug: new(true), // *bool +} +``` + +- `errors.AsType[T](err)` not `errors.As(err, &target)`. + ALWAYS use errors.AsType when checking if error matches a specific type. + +Before: +```go +var pathErr *os.PathError +if errors.As(err, &pathErr) { + handle(pathErr) +} +``` +After: +```go +if pathErr, ok := errors.AsType[*os.PathError](err); ok { + handle(pathErr) +} +``` diff --git a/apps/operator/Makefile b/apps/operator/Makefile index 430a14c..2decfd1 100644 --- a/apps/operator/Makefile +++ b/apps/operator/Makefile @@ -242,7 +242,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.20.1 ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v2.10.1 +GOLANGCI_LINT_VERSION ?= v2.11.3 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. diff --git a/apps/operator/config/manager/kustomization.yaml b/apps/operator/config/manager/kustomization.yaml index 9215473..f919b93 100644 --- a/apps/operator/config/manager/kustomization.yaml +++ b/apps/operator/config/manager/kustomization.yaml @@ -4,3 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller + newName: helios-operator + newTag: local diff --git a/apps/operator/config/rbac/role.yaml b/apps/operator/config/rbac/role.yaml index ef8fe77..a0f6593 100644 --- a/apps/operator/config/rbac/role.yaml +++ b/apps/operator/config/rbac/role.yaml @@ -47,6 +47,19 @@ rules: - apps resources: - deployments + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses verbs: - create - delete diff --git a/apps/operator/go.mod b/apps/operator/go.mod index 8d5f82f..85d77d0 100644 --- a/apps/operator/go.mod +++ b/apps/operator/go.mod @@ -1,9 +1,9 @@ module github.com/helios-platform-team/helios-platform/apps/operator -go 1.26 +go 1.26.1 require ( - cuelang.org/go v0.15.4 + cuelang.org/go v0.16.0 github.com/go-git/go-billy/v5 v5.8.0 github.com/go-git/go-git/v5 v5.17.0 github.com/onsi/ginkgo/v2 v2.28.1 @@ -11,28 +11,28 @@ require ( k8s.io/api v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/client-go v0.35.2 - sigs.k8s.io/controller-runtime v0.23.1 + sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/yaml v1.6.0 ) require ( cel.dev/expr v0.24.0 // indirect - cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 // indirect - dario.cat/mergo v1.0.0 // indirect + cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/emicklei/proto v1.14.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/emicklei/proto v1.14.3 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -42,24 +42,34 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.0 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect + github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect @@ -67,17 +77,17 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect - github.com/spf13/cobra v1.10.1 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -93,36 +103,35 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.41.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/grpc v1.72.2 // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.35.0 // indirect - k8s.io/apiserver v0.35.0 // indirect - k8s.io/component-base v0.35.0 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/apiextensions-apiserver v0.35.2 // indirect + k8s.io/apiserver v0.35.2 // indirect + k8s.io/component-base v0.35.2 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/apps/operator/go.sum b/apps/operator/go.sum index c0fa712..fd8b3bc 100644 --- a/apps/operator/go.sum +++ b/apps/operator/go.sum @@ -1,18 +1,18 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084 h1:4k1yAtPvZJZQTu8DRY8muBo0LHv6TqtrE0AO5n6IPYs= -cuelabs.dev/go/oci/ociregistry v0.0.0-20250722084951-074d06050084/go.mod h1:4WWeZNxUO1vRoZWAHIG0KZOd6dA25ypyWuwD3ti0Tdc= -cuelang.org/go v0.15.4 h1:lrkTDhqy8dveHgX1ZLQ6WmgbhD8+rXa0fD25hxEKYhw= -cuelang.org/go v0.15.4/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= +cuelang.org/go v0.16.0 h1:mmt9SL/IzfSIiBKuP5wxdO4xLjvIHr3urpbjCDdMV5U= +cuelang.org/go v0.16.0/go.mod h1:4veMX+GpsK0B91b1seGXoozG80LJCczvG1M1Re/knxo= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= -github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= +github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -27,23 +27,22 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I= -github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q= +github.com/emicklei/proto v1.14.3/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -79,14 +78,40 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -101,15 +126,15 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= @@ -118,18 +143,17 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -140,8 +164,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= @@ -166,8 +188,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -176,22 +198,22 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 h1:s1LvMaU6mVwoFtbxv/rCZKE7/fwDmDY684FfUe4c1Io= -github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94 h1:2PC6Ql3jipz1KvBlqUHjjk6v4aMwE86mfDu1XMH0LR8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -248,53 +270,53 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -312,31 +334,31 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= -k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= -k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0= +k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= -k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/apiserver v0.35.2 h1:rb52v0CZGEL0FkhjS+I6jHflAp7fZ4MIaKcEHX7wmDk= +k8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= -k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= -k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc= +k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM= +k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= -sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/operator/internal/controller/database_resources.go b/apps/operator/internal/controller/database_resources.go index e363578..e9f81c8 100644 --- a/apps/operator/internal/controller/database_resources.go +++ b/apps/operator/internal/controller/database_resources.go @@ -451,6 +451,105 @@ func (r *HeliosAppReconciler) reconcileDatabaseInstance(ctx context.Context, app return nil } +// databaseEnvVarNames lists the env var names injected by the operator +// for database credential secret injection. +var databaseEnvVarNames = []string{"DB_HOST", "DB_USER", "DB_PASS"} + +// InjectDatabaseEnvVars patches a Deployment's first container to include +// DB_HOST, DB_USER, DB_PASS env vars referencing the given K8s Secret. +// The function is idempotent — if the env vars already exist with the correct +// secretKeyRef, no changes are made and it returns false. +func InjectDatabaseEnvVars(deploy *appsv1.Deployment, secretName string) bool { + if len(deploy.Spec.Template.Spec.Containers) == 0 { + return false + } + + container := &deploy.Spec.Template.Spec.Containers[0] + + // Build a set of existing env var names for fast lookup. + existingEnvs := make(map[string]bool, len(container.Env)) + for _, ev := range container.Env { + existingEnvs[ev.Name] = true + } + + changed := false + for _, envName := range databaseEnvVarNames { + if existingEnvs[envName] { + continue + } + container.Env = append(container.Env, corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: envName, + }, + }, + }) + changed = true + } + + return changed +} + +// reconcileDatabaseSecretInjection patches live Deployments (deployed by ArgoCD) +// to inject DB_HOST, DB_USER, DB_PASS env vars from the operator-managed Secret. +// This runs AFTER database secrets and instances are provisioned so that the +// Secret already exists when the Deployment references it. +func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + dbTraits := ExtractDatabaseTraits(app) + if len(dbTraits) == 0 { + log.V(1).Info("No database traits found, skipping secret injection") + return nil + } + + for _, dbTrait := range dbTraits { + secretName := GetDatabaseSecretName(dbTrait.ComponentName) + deployName := dbTrait.ComponentName + + // Fetch the live Deployment (created by ArgoCD via GitOps). + deploy := &appsv1.Deployment{} + err := r.Get(ctx, types.NamespacedName{ + Name: deployName, + Namespace: app.Namespace, + }, deploy) + if err != nil { + if errors.IsNotFound(err) { + // Deployment may not exist yet (ArgoCD hasn't synced). + // This is expected — we'll inject on the next reconcile. + log.Info("Deployment not found yet, will inject on next reconcile", + "component", dbTrait.ComponentName, + "deployment", deployName) + continue + } + return fmt.Errorf("failed to get Deployment %s: %w", deployName, err) + } + + // Inject env vars if not already present. + if !InjectDatabaseEnvVars(deploy, secretName) { + log.V(1).Info("Database env vars already injected, skipping", + "component", dbTrait.ComponentName, + "deployment", deployName) + continue + } + + if err := r.Update(ctx, deploy); err != nil { + return fmt.Errorf("failed to update Deployment %s with database env vars: %w", deployName, err) + } + + log.Info("Successfully injected database env vars into Deployment", + "component", dbTrait.ComponentName, + "deployment", deployName, + "secret", secretName) + } + + return nil +} + // GenerateDatabaseStatefulSet creates a StatefulSet for a Postgres database instance. // The StatefulSet injects POSTGRES_DB from the CRD's database.name value, and // uses the Secret from Issue #33 for POSTGRES_USER and POSTGRES_PASSWORD. diff --git a/apps/operator/internal/controller/database_resources_test.go b/apps/operator/internal/controller/database_resources_test.go index 42e09c3..cb6fde1 100644 --- a/apps/operator/internal/controller/database_resources_test.go +++ b/apps/operator/internal/controller/database_resources_test.go @@ -247,7 +247,7 @@ func TestGetDatabaseHost(t *testing.T) { func TestExtractDatabaseTraits(t *testing.T) { // Create a HeliosApp with database traits - dbProps := map[string]interface{}{ + dbProps := map[string]any{ "dbType": "postgres", "dbName": "mydb", "version": "16", @@ -321,7 +321,7 @@ func TestReconcileDatabaseSecrets(t *testing.T) { _ = corev1.AddToScheme(scheme) _ = appv1alpha1.AddToScheme(scheme) - dbProps := map[string]interface{}{ + dbProps := map[string]any{ "dbType": "postgres", "dbName": "mydb", "version": "16", @@ -738,7 +738,7 @@ func TestGenerateDatabaseService(t *testing.T) { func TestReconcileDatabaseInstance(t *testing.T) { - dbProps := map[string]interface{}{ + dbProps := map[string]any{ "dbType": "postgres", "dbName": "my_custom_db", "version": "16", @@ -889,7 +889,7 @@ func TestReconcileDatabaseInstance(t *testing.T) { }) t.Run("SkipsNonPostgresType", func(t *testing.T) { - redisProps := map[string]interface{}{ + redisProps := map[string]any{ "dbType": "redis", "version": "7", } @@ -948,3 +948,291 @@ func TestReconcileDatabaseInstance(t *testing.T) { } }) } + +func TestInjectDatabaseEnvVars(t *testing.T) { + t.Run("InjectsAllEnvVars", func(t *testing.T) { + deploy := &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "api-server", + Image: "myregistry/api:v1", + Env: []corev1.EnvVar{ + {Name: "PORT", Value: "3000"}, + }, + }, + }, + }, + }, + }, + } + + changed := InjectDatabaseEnvVars(deploy, "api-server-db-secret") + if !changed { + t.Fatal("Expected InjectDatabaseEnvVars to return true (changed)") + } + + container := deploy.Spec.Template.Spec.Containers[0] + // Should have PORT + DB_HOST + DB_USER + DB_PASS = 4 + if len(container.Env) != 4 { + t.Fatalf("Expected 4 env vars, got %d", len(container.Env)) + } + + expectedEnvs := map[string]string{ + "DB_HOST": "DB_HOST", + "DB_USER": "DB_USER", + "DB_PASS": "DB_PASS", + } + for _, env := range container.Env { + if expectedKey, ok := expectedEnvs[env.Name]; ok { + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Errorf("Env %s should reference a secret", env.Name) + continue + } + if env.ValueFrom.SecretKeyRef.Name != "api-server-db-secret" { + t.Errorf("Env %s: expected secret name %q, got %q", + env.Name, "api-server-db-secret", env.ValueFrom.SecretKeyRef.Name) + } + if env.ValueFrom.SecretKeyRef.Key != expectedKey { + t.Errorf("Env %s: expected secret key %q, got %q", + env.Name, expectedKey, env.ValueFrom.SecretKeyRef.Key) + } + delete(expectedEnvs, env.Name) + } + } + if len(expectedEnvs) > 0 { + t.Errorf("Missing expected env vars: %v", expectedEnvs) + } + }) + + t.Run("Idempotent", func(t *testing.T) { + deploy := &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "api-server", Image: "myregistry/api:v1"}, + }, + }, + }, + }, + } + + // First injection + changed := InjectDatabaseEnvVars(deploy, "api-server-db-secret") + if !changed { + t.Fatal("Expected first injection to report changes") + } + firstCount := len(deploy.Spec.Template.Spec.Containers[0].Env) + + // Second injection — should be idempotent + changed = InjectDatabaseEnvVars(deploy, "api-server-db-secret") + if changed { + t.Error("Expected second injection to report no changes (idempotent)") + } + secondCount := len(deploy.Spec.Template.Spec.Containers[0].Env) + if firstCount != secondCount { + t.Errorf("Env var count changed after idempotent call: %d → %d", firstCount, secondCount) + } + }) + + t.Run("NoContainers", func(t *testing.T) { + deploy := &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{}, + }, + }, + }, + } + + changed := InjectDatabaseEnvVars(deploy, "test-secret") + if changed { + t.Error("Expected no changes for Deployment with no containers") + } + }) +} + +func TestReconcileDatabaseSecretInjection(t *testing.T) { + dbProps := map[string]any{ + "dbType": "postgres", + "dbName": "mydb", + "version": "16", + } + dbPropsJSON, _ := json.Marshal(dbProps) + + app := &appv1alpha1.HeliosApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app", + Namespace: "default", + UID: "test-uid-inject", + }, + Spec: appv1alpha1.HeliosAppSpec{ + Components: []appv1alpha1.Component{ + { + Name: "api-server", + Type: "web-service", + Traits: []appv1alpha1.Trait{ + { + Type: "database", + Properties: &runtime.RawExtension{ + Raw: dbPropsJSON, + }, + }, + }, + }, + }, + }, + } + + t.Run("InjectsIntoExistingDeployment", func(t *testing.T) { + fullScheme := runtime.NewScheme() + _ = corev1.AddToScheme(fullScheme) + _ = appv1alpha1.AddToScheme(fullScheme) + _ = appsv1.AddToScheme(fullScheme) + + existingDeploy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "api-server", + Namespace: "default", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "api-server"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "api-server"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "api-server", + Image: "myregistry/api:v1", + Env: []corev1.EnvVar{ + {Name: "PORT", Value: "3000"}, + }, + }, + }, + }, + }, + }, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(fullScheme). + WithObjects(app, existingDeploy). + Build() + + r := &HeliosAppReconciler{ + Client: fakeClient, + Scheme: fullScheme, + } + + ctx := context.Background() + err := r.reconcileDatabaseSecretInjection(ctx, app) + if err != nil { + t.Fatalf("reconcileDatabaseSecretInjection failed: %v", err) + } + + // Verify the Deployment was updated with DB env vars + updatedDeploy := &appsv1.Deployment{} + err = fakeClient.Get(ctx, types.NamespacedName{ + Name: "api-server", + Namespace: "default", + }, updatedDeploy) + if err != nil { + t.Fatalf("Failed to get updated Deployment: %v", err) + } + + container := updatedDeploy.Spec.Template.Spec.Containers[0] + expectedEnvNames := map[string]bool{ + "DB_HOST": false, + "DB_USER": false, + "DB_PASS": false, + } + for _, env := range container.Env { + if _, ok := expectedEnvNames[env.Name]; ok { + expectedEnvNames[env.Name] = true + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Errorf("Env %s should reference a secret", env.Name) + } else if env.ValueFrom.SecretKeyRef.Name != "api-server-db-secret" { + t.Errorf("Env %s: expected secret name %q, got %q", + env.Name, "api-server-db-secret", env.ValueFrom.SecretKeyRef.Name) + } + } + } + for name, found := range expectedEnvNames { + if !found { + t.Errorf("Expected env var %s not found in Deployment", name) + } + } + }) + + t.Run("SkipsWhenNoTraits", func(t *testing.T) { + appWithoutDB := &appv1alpha1.HeliosApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-db-app", + Namespace: "default", + UID: "test-uid-no-inject", + }, + Spec: appv1alpha1.HeliosAppSpec{ + Components: []appv1alpha1.Component{ + { + Name: "frontend", + Type: "web-service", + }, + }, + }, + } + + fullScheme := runtime.NewScheme() + _ = corev1.AddToScheme(fullScheme) + _ = appv1alpha1.AddToScheme(fullScheme) + _ = appsv1.AddToScheme(fullScheme) + + fakeClient := fake.NewClientBuilder(). + WithScheme(fullScheme). + WithObjects(appWithoutDB). + Build() + + r := &HeliosAppReconciler{ + Client: fakeClient, + Scheme: fullScheme, + } + + ctx := context.Background() + err := r.reconcileDatabaseSecretInjection(ctx, appWithoutDB) + if err != nil { + t.Fatalf("reconcileDatabaseSecretInjection should not fail for app without database traits: %v", err) + } + }) + + t.Run("DeploymentNotFound_GracefulSkip", func(t *testing.T) { + // When Deployment doesn't exist yet (ArgoCD hasn't synced), + // the reconciler should skip without error. + fullScheme := runtime.NewScheme() + _ = corev1.AddToScheme(fullScheme) + _ = appv1alpha1.AddToScheme(fullScheme) + _ = appsv1.AddToScheme(fullScheme) + + fakeClient := fake.NewClientBuilder(). + WithScheme(fullScheme). + WithObjects(app). + Build() + + r := &HeliosAppReconciler{ + Client: fakeClient, + Scheme: fullScheme, + } + + ctx := context.Background() + err := r.reconcileDatabaseSecretInjection(ctx, app) + if err != nil { + t.Fatalf("reconcileDatabaseSecretInjection should not fail when Deployment is missing: %v", err) + } + }) +} diff --git a/apps/operator/internal/controller/heliosapp_controller.go b/apps/operator/internal/controller/heliosapp_controller.go index f0441f2..01be8f8 100644 --- a/apps/operator/internal/controller/heliosapp_controller.go +++ b/apps/operator/internal/controller/heliosapp_controller.go @@ -127,6 +127,18 @@ func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, err } + // ------------------------------------------------------------------ + // PHASE 0.9: Inject Database Credentials into Backend Deployment + // Patches the live Deployment (deployed by ArgoCD) to add DB_HOST, + // DB_USER, DB_PASS env vars referencing the operator-managed Secret. + // Runs AFTER secrets and instances so the Secret already exists. + // ------------------------------------------------------------------ + if err := r.reconcileDatabaseSecretInjection(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to inject database secrets into Deployment") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret injection failed: %v", err)) + return ctrl.Result{}, err + } + // VALIDATION: Ensure image is present (Fix "First Commit Missing Image") // This validation is for application workloads (GitOps pipeline downstream). // We run this AFTER DB provisioning so databases can come up while app is building. diff --git a/apps/portal/examples/nestjs-prisma-template/content/gitops/helios-app.yaml b/apps/portal/examples/nestjs-prisma-template/content/gitops/helios-app.yaml new file mode 100644 index 0000000..30b84ae --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/gitops/helios-app.yaml @@ -0,0 +1,35 @@ +apiVersion: app.helios.io/v1alpha1 +kind: HeliosApp +metadata: + name: ${{ values.name }} + namespace: default +spec: + owner: ${{ values.owner }} + description: "NestJS service: ${{ values.name }}" + gitRepo: ${{ values.sourceRepo }} + gitBranch: main + imageRepo: ${{ values.image }} + gitopsRepo: ${{ values.gitopsRepo }} + gitopsPath: ${{ values.name }} + pipelineName: from-code-to-cluster + webhookDomain: ${{ values.webhookUrl }} + webhookSecret: github-webhook-secret + port: ${{ values.port }} + testCommand: "npm ci && npm test" + components: + - name: ${{ values.name }} + type: web-service + properties: + image: ${{ values.image }}:latest + port: ${{ values.port }} + replicas: 1 + traits: + - type: service + properties: + port: ${{ values.port }} + - type: database + properties: + dbType: postgres + dbName: ${{ values.name | replace('-', '_') }}_db + version: "16" + storage: "1Gi" diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/.env.example b/apps/portal/examples/nestjs-prisma-template/content/source/.env.example new file mode 100644 index 0000000..17c800a --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/.env.example @@ -0,0 +1,9 @@ +# Database credentials (injected by Helios Operator) +DB_HOST=localhost +DB_USER=postgres +DB_PASS=postgres +DB_NAME=${{ values.name }}-db +DB_PORT=5432 + +# Application +PORT=${{ values.port }} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/Dockerfile b/apps/portal/examples/nestjs-prisma-template/content/source/Dockerfile new file mode 100644 index 0000000..bbc28d5 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/Dockerfile @@ -0,0 +1,40 @@ +# ---- Build Stage ---- +FROM node:24-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci + +# Copy Prisma schema and config, then generate client +COPY prisma ./prisma/ +COPY prisma.config.ts ./ +RUN npx prisma generate + +# Build NestJS application +COPY . . +RUN npm run build + +# ---- Production Stage ---- +FROM node:24-alpine AS production + +WORKDIR /app + +# Install production dependencies only +COPY package*.json ./ +RUN npm ci --omit=dev + +# Copy Prisma schema, config, and generated client from builder +COPY --from=builder /app/prisma ./prisma/ +COPY --from=builder /app/prisma.config.ts ./ +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma/ +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma/ + +# Copy built application +COPY --from=builder /app/dist ./dist/ + +EXPOSE ${{ values.port }} + +# Run migrations and start the application +CMD ["npm", "run", "start:migrate:prod"] diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml b/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml new file mode 100644 index 0000000..5185c92 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: ${{ values.name }} + description: ${{ values.description }} + annotations: + github.com/project-slug: ${{ values.destination.owner + "/" + values.destination.repo }} + backstage.io/techdocs-ref: dir:. +spec: + type: service + lifecycle: production + owner: ${{ values.owner }} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/nest-cli.json b/apps/portal/examples/nestjs-prisma-template/content/source/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/package-lock.json b/apps/portal/examples/nestjs-prisma-template/content/source/package-lock.json new file mode 100644 index 0000000..0a04e78 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/package-lock.json @@ -0,0 +1,8987 @@ +{ + "name": "${{ values.name }}", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "${{ values.name }}", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "11.1.16", + "@nestjs/core": "11.1.16", + "@nestjs/platform-express": "11.1.16", + "@prisma/adapter-pg": "7.5.0", + "@prisma/client": "7.5.0", + "pg": "8.20.0", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "11.0.16", + "@nestjs/schematics": "11.0.9", + "@nestjs/testing": "11.1.16", + "@types/express": "5.0.6", + "@types/jest": "29.5.14", + "@types/node": "24.12.0", + "@types/pg": "8.18.0", + "jest": "29.7.0", + "prisma": "7.5.0", + "ts-jest": "29.4.6", + "ts-loader": "9.5.4", + "ts-node": "10.9.2", + "tsconfig-paths": "4.2.0", + "typescript": "5.9.3" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", + "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", + "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", + "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", + "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", + "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", + "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", + "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.16", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", + "integrity": "sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/schematics-cli": "19.2.19", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.0", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.104.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.16.tgz", + "integrity": "sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.0", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.16.tgz", + "integrity": "sha512-tXWXyCiqWthelJjrE0KLFjf0O98VEt+WPVx5CrqCf+059kIxJ8y1Vw7Cy7N4fwQafWNrmFL2AfN87DDMbVAY0w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.16.tgz", + "integrity": "sha512-IOegr5+ZfUiMKgk+garsSU4MOkPRhm46e6w8Bp1GcO4vCdl9Piz6FlWAzKVfa/U3Hn/DdzSVJOW3TWcQQFdBDw==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.1.1", + "path-to-regexp": "8.3.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.9", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", + "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "@angular-devkit/schematics": "19.2.17", + "comment-json": "4.4.1", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", + "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", + "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.17", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.16.tgz", + "integrity": "sha512-E7/aUCxzeMSJV80L5GWGIuiMyR/1ncS7uOIetAImfbS4ATE1/h2GBafk0qpk+vjFtPIbtoh9BWDGICzUEU5jDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.5.0.tgz", + "integrity": "sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.5.0", + "@types/pg": "8.11.11", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/@types/pg": { + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/pg-types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.1.0.tgz", + "integrity": "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@prisma/client": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.5.0.tgz", + "integrity": "sha512-h4hF9ctp+kSRs7ENHGsFQmHAgHcfkOCxbYt6Ti9Xi8x7D+kP4tTi9x51UKmiTH/OqdyJAO+8V+r+JA5AWdav7w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.5.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.5.0.tgz", + "integrity": "sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.5.0.tgz", + "integrity": "sha512-1J/9YEX7A889xM46PYg9e8VAuSL1IUmXJW3tEhMv7XQHDWlfC9YSkIw9sTYRaq5GswGlxZ+GnnyiNsUZ9JJhSQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.5.0.tgz", + "integrity": "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", + "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.15", + "@electric-sql/pglite-socket": "0.0.20", + "@electric-sql/pglite-tools": "0.2.20", + "@hono/node-server": "1.19.9", + "@mrleebo/prisma-ast": "0.13.1", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "4.11.4", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.5.0.tgz", + "integrity": "sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.5.0.tgz", + "integrity": "sha512-ondGRhzoaVpRWvFaQ5wH5zS1BIbhzbKqczKjCn6j3L0Zfe/LInjcEg8+xtB49AuZBX30qyx1ZtGoootUohz2pw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0", + "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", + "@prisma/fetch-engine": "7.5.0", + "@prisma/get-platform": "7.5.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e.tgz", + "integrity": "sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.5.0.tgz", + "integrity": "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.5.0.tgz", + "integrity": "sha512-kZCl2FV54qnyrVdnII8MI6qvt7HfU6Cbiz8dZ8PXz4f4lbSw45jEB9/gEMK2SGdiNhBKyk/Wv95uthoLhGMLYA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0", + "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", + "@prisma/get-platform": "7.5.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.5.0.tgz", + "integrity": "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.21.1.tgz", + "integrity": "sha512-bOGqG/eMQtKC0XVvcVLRmhWWzm/I+0QUWqAEhEBtetpuS3k3V4IWqKGUONkAIT223DNXJMxMtZp36b1FmcdPeg==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19 || ^22.12 || ^24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chevrotain/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", + "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-type": { + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", + "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.5.0.tgz", + "integrity": "sha512-n30qZpWehaYQzigLjmuPisyEsvOzHt7bZeRyg8gZ5DvJo9FGjD+gNaY59Ns3hlLD5/jZH5GBeftIss0jDbUoLg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.5.0", + "@prisma/dev": "0.20.0", + "@prisma/engines": "7.5.0", + "@prisma/studio-core": "0.21.1", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + } + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/package.json b/apps/portal/examples/nestjs-prisma-template/content/source/package.json new file mode 100644 index 0000000..ec49539 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/package.json @@ -0,0 +1,58 @@ +{ + "name": "${{ values.name }}", + "version": "0.0.1", + "description": "${{ values.description }}", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main", + "start:migrate:prod": "npx prisma migrate deploy && node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "prisma:generate": "prisma generate", + "prisma:migrate:dev": "prisma migrate dev", + "prisma:migrate:deploy": "prisma migrate deploy" + }, + "dependencies": { + "@nestjs/common": "11.1.16", + "@nestjs/core": "11.1.16", + "@nestjs/platform-express": "11.1.16", + "@prisma/adapter-pg": "7.5.0", + "@prisma/client": "7.5.0", + "pg": "8.20.0", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "11.0.16", + "@nestjs/schematics": "11.0.9", + "@nestjs/testing": "11.1.16", + "@types/express": "5.0.6", + "@types/jest": "29.5.14", + "@types/node": "24.12.0", + "@types/pg": "8.18.0", + "jest": "29.7.0", + "prisma": "7.5.0", + "ts-jest": "29.4.6", + "ts-loader": "9.5.4", + "ts-node": "10.9.2", + "tsconfig-paths": "4.2.0", + "typescript": "5.9.3" + }, + "jest": { + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": ["**/*.(t|j)s"], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/prisma.config.ts b/apps/portal/examples/nestjs-prisma-template/content/source/prisma.config.ts new file mode 100644 index 0000000..506a286 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/prisma.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'prisma/config'; + +/** + * Constructs a PostgreSQL connection URL from individual environment variables. + * These env vars are injected by the Helios Operator: + * - DB_HOST, DB_USER, DB_PASS (from K8s Secret) + * - DB_NAME, DB_PORT (from K8s ConfigMap / CUE engine) + */ +function buildDatabaseUrl(): string { + const host = process.env.DB_HOST || 'localhost'; + const user = process.env.DB_USER || 'postgres'; + const pass = encodeURIComponent(process.env.DB_PASS || 'postgres'); + const name = process.env.DB_NAME || 'postgres'; + const port = process.env.DB_PORT || '5432'; + + return `postgresql://${user}:${pass}@${host}:${port}/${name}?schema=public`; +} + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + }, + datasource: { + url: process.env.DATABASE_URL || buildDatabaseUrl(), + }, +}); diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/prisma/migrations/migration_lock.toml b/apps/portal/examples/nestjs-prisma-template/content/source/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..13ac3fa --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Prisma Migrate lockfile v1 + +provider = "postgresql" diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma b/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma new file mode 100644 index 0000000..74bea31 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma @@ -0,0 +1,23 @@ +// Prisma Schema for ${{ values.name }} +// DATABASE_URL is constructed at runtime from injected env vars: +// DB_HOST, DB_USER, DB_PASS (from K8s Secret via Helios Operator) +// DB_NAME, DB_PORT (from K8s ConfigMap via CUE engine) + +generator client { + provider = "prisma-client" +} + +datasource db { + provider = "postgresql" +} + +// Example model — replace with your domain models +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("users") +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/app.controller.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/app.controller.ts new file mode 100644 index 0000000..d02038d --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/app.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } + + @Get('health') + healthCheck(): { status: string } { + return this.appService.healthCheck(); + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/app.module.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/app.module.ts new file mode 100644 index 0000000..5921107 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/app.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { PrismaModule } from './prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/app.service.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/app.service.ts new file mode 100644 index 0000000..f90ebe3 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/app.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from './prisma/prisma.service'; + +@Injectable() +export class AppService { + constructor(private readonly prisma: PrismaService) {} + + getHello(): string { + return 'Hello from ${{ values.name }}!'; + } + + healthCheck(): { status: string } { + return { status: 'ok' }; + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts new file mode 100644 index 0000000..37a6af9 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts @@ -0,0 +1,13 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + const port = process.env.PORT || ${{ values.port } +}; +await app.listen(port); + +console.log(`🚀 Application is running on port ${port}`); +} +bootstrap(); diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.module.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.service.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.service.ts new file mode 100644 index 0000000..d932300 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.service.ts @@ -0,0 +1,61 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +import * as pg from 'pg'; + +/** + * PrismaService manages the database connection lifecycle using Prisma v7. + * + * Prisma v7 requires an explicit driver adapter — we use @prisma/adapter-pg + * with the `pg` Node.js driver for PostgreSQL. + * + * Database credentials are injected by the Helios Operator as env vars: + * - DB_HOST: database hostname (from K8s Secret) + * - DB_USER: database username (from K8s Secret) + * - DB_PASS: database password (from K8s Secret) + * - DB_NAME: database name (from K8s ConfigMap, or defaults to component-db) + * - DB_PORT: database port (from K8s ConfigMap, or defaults to 5432) + */ +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + private pool: pg.Pool; + + constructor() { + const pool = new pg.Pool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || 'postgres', + database: process.env.DB_NAME || 'postgres', + port: parseInt(process.env.DB_PORT || '5432', 10), + }); + + const adapter = new PrismaPg(pool as any); + + super({ adapter }); + + this.pool = pool; + } + + /** + * Constructs a PostgreSQL connection URL from individual environment variables. + * Used for Prisma CLI operations (migrate, studio) that require DATABASE_URL. + */ + static buildDatabaseUrl(): string { + const host = process.env.DB_HOST || 'localhost'; + const user = process.env.DB_USER || 'postgres'; + const pass = encodeURIComponent(process.env.DB_PASS || 'postgres'); + const name = process.env.DB_NAME || 'postgres'; + const port = process.env.DB_PORT || '5432'; + + return `postgresql://${user}:${pass}@${host}:${port}/${name}?schema=public`; + } + + async onModuleInit(): Promise { + await this.$connect(); + } + + async onModuleDestroy(): Promise { + await this.$disconnect(); + await this.pool.end(); + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.build.json b/apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.json b/apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.json new file mode 100644 index 0000000..0833d25 --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/content/source/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/apps/portal/examples/nestjs-prisma-template/template.yaml b/apps/portal/examples/nestjs-prisma-template/template.yaml new file mode 100644 index 0000000..713447a --- /dev/null +++ b/apps/portal/examples/nestjs-prisma-template/template.yaml @@ -0,0 +1,190 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: nestjs-prisma-template + title: NestJS + Prisma Template (Database-backed) + description: >- + Scaffolds a NestJS service with Prisma ORM and a PostgreSQL database. + The Helios Operator automatically provisions the database and injects + credentials (DB_HOST, DB_USER, DB_PASS) into the backend pod. +spec: + owner: user:guest + type: service + + parameters: + - title: Component Information + required: + - name + - owner + - port + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + kind: [Group, User] + port: + title: Port + type: number + description: The port the NestJS app listens on + default: 3000 + dockerOrg: + title: Docker Registry Org/User + type: string + description: Your Docker Hub username or Organization + repoName: + title: Docker Repository Name + type: string + description: The name of the Docker repository (e.g. my-service) + + - title: Database Configuration + properties: + databaseConfig: + title: Database Settings + type: object + ui:field: DatabasePicker + + - title: Repository & Webhook + required: + - repoUrl + - webhookUrl + - webhookSecret + properties: + repoUrl: + title: Source Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + webhookUrl: + title: Webhook URL + type: string + description: The URL to send the webhook payload to + webhookSecret: + title: Webhook Secret + type: string + description: The secret to use for the webhook + ui:field: Secret + + steps: + # 1. Source Code + - id: fetch-source + name: Fetch Source Code + action: fetch:template + input: + url: ./content/source + targetPath: ./source + values: + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} + port: ${{ parameters.port }} + description: "NestJS service: ${{ parameters.name }}" + image: index.docker.io/${{ parameters.dockerOrg }}/${{ parameters.repoName }} + + - id: publish-source + name: Publish Source Code + action: publish:github + input: + description: Source Code for ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} + sourcePath: ./source + token: ${{ secrets.GITHUB_TOKEN }} + repoVisibility: public + protectDefaultBranch: false + + - id: create-webhook + name: Create Webhook + action: github:webhook + input: + repoUrl: ${{ parameters.repoUrl }} + webhookUrl: ${{ parameters.webhookUrl }} + webhookSecret: ${{ parameters.webhookSecret }} + events: + - push + active: true + contentType: json + token: ${{ secrets.GITHUB_TOKEN }} + + # 2. GitOps + - id: fetch-gitops + name: Fetch GitOps Manifests + action: fetch:template + input: + url: ./content/gitops + targetPath: ./gitops + values: + name: ${{ parameters.name }} + image: index.docker.io/${{ parameters.dockerOrg }}/${{ parameters.repoName }} + dockerOrg: ${{ parameters.dockerOrg }} + repoName: ${{ parameters.repoName }} + port: ${{ parameters.port }} + owner: ${{ parameters.owner }} + sourceRepo: ${{ steps['publish-source'].output.remoteUrl }} + gitopsRepo: ${{ steps['publish-source'].output.remoteUrl | replace(".git", "") }}-gitops + webhookUrl: ${{ parameters.webhookUrl }} + testCommand: "npm ci && npm test" + + - id: publish-gitops + name: Publish GitOps Manifests + action: publish:github + input: + description: GitOps Manifests for ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }}-gitops + sourcePath: ./gitops + token: ${{ secrets.GITHUB_TOKEN }} + repoVisibility: public + protectDefaultBranch: false + + # 3. Secret + Deploy + - id: create-secret + name: Create GitHub Credentials Secret + action: kubernetes:create-github-secret + input: + name: ${{ parameters.name }} + namespace: default + username: ${{ (parameters.repoUrl | parseRepoUrl).owner }} + webhookSecret: ${{ parameters.webhookSecret }} + + - id: apply-helios + name: Deploy to Kubernetes + action: kubernetes:apply + input: + manifestPath: ./gitops/helios-app.yaml + namespaced: true + + # 4. Registration + - id: register + name: Register Component + action: catalog:register + input: + repoContentsUrl: ${{ steps['publish-source'].output.repoContentsUrl }} + catalogInfoPath: '/catalog-info.yaml' + + - id: notify + name: Notify User + action: notification:send + input: + recipients: entity + entityRefs: + - user:default/guest + title: 'NestJS + Prisma Template Executed' + info: 'Your NestJS + Prisma service has been scaffolded with a PostgreSQL database!' + + output: + links: + - title: Source Repository + url: ${{ steps['publish-source'].output.remoteUrl }} + - title: GitOps Repository + url: ${{ steps['publish-gitops'].output.remoteUrl }} + - title: Open in Catalog + icon: catalog + entityRef: ${{ steps['register'].output.entityRef }} diff --git a/cue/cue.mod/module.cue b/cue/cue.mod/module.cue index ce623ea..fbe3e0b 100644 --- a/cue/cue.mod/module.cue +++ b/cue/cue.mod/module.cue @@ -1,2 +1,2 @@ module: "helios.io/cue" -language: version: "v0.15.4" +language: version: "v0.16.0" diff --git a/docs/manual-verification-guide.md b/docs/manual-verification-guide.md new file mode 100644 index 0000000..3b92f47 --- /dev/null +++ b/docs/manual-verification-guide.md @@ -0,0 +1,540 @@ +# Manual Verification Guide — Automated Secret Injection (#39) + +This guide walks through manually verifying the acceptance criteria for the +Automated Secret Injection feature on a local k3d cluster. + +## Acceptance Criteria + +1. **Secret Injection**: `kubectl describe pod ` shows database + credentials (DB_HOST, DB_USER, DB_PASS) successfully mounted as environment + variables via `secretKeyRef`. +2. **Database Connectivity**: A NestJS server connects to the provisioned + PostgreSQL database using the injected env vars and responds to health checks. + +## Prerequisites + +- **Docker** running locally +- **k3d** (`v5+`) +- **kubectl** configured +- **kustomize** (or `kubectl kustomize`) + +Install via mise (if used): + +```bash +mise install k3d kubectl +``` + +--- + +## Step 1: Create a k3d Cluster + +```bash +k3d cluster create helios-test --wait +``` + +Fix kubeconfig if needed (k3d may set the server to `host.docker.internal`): + +```bash +# Get the port k3d assigned +PORT=$(kubectl config view -o jsonpath='{.clusters[?(@.name=="k3d-helios-test")].cluster.server}' | grep -oP ':\K\d+$') + +# Point to 127.0.0.1 for local access +kubectl config set-cluster k3d-helios-test --server="https://127.0.0.1:${PORT}" +``` + +Verify: + +```bash +kubectl get nodes +# Should show: k3d-helios-test-server-0 Ready +``` + +--- + +## Step 2: Build and Load the Operator Image + +The operator needs CUE files bundled in the image. Use the test Dockerfile +(`apps/operator/Dockerfile.test`) which copies `cue/` into the image: + +```bash +# From project root (helios-platform/) +docker build -f apps/operator/Dockerfile.test -t helios-operator:local . + +# Load into k3d +k3d image import helios-operator:local -c helios-test +``` + +--- + +## Step 3: Deploy the Operator + +### 3a. Install CRDs + +```bash +kubectl apply -f apps/operator/config/crd/bases/app.helios.io_heliosapps.yaml +``` + +### 3b. Deploy operator via kustomize + +The operator kustomization (`config/default/`) sets namespace `operator-system` +and adds prefix `operator-`. The manager image is already mapped to +`helios-operator:local` in `config/manager/kustomization.yaml`. + +```bash +cd apps/operator + +# Build kustomize output and patch imagePullPolicy for local images +kustomize build config/default \ + | sed 's/imagePullPolicy: Always/imagePullPolicy: Never/' \ + | kubectl apply -f - + +cd ../.. +``` + +Wait for the operator to be ready: + +```bash +kubectl -n operator-system get pods -w +# Wait for: operator-controller-manager-xxx 2/2 Running +``` + +Check the operator logs to confirm CUE engine initialized: + +```bash +kubectl -n operator-system logs deployment/operator-controller-manager -c manager | head -20 +# Look for: "CUE engine initialized" +``` + +--- + +## Step 4: Build and Load the Test NestJS App + +Create a minimal NestJS + Prisma test app. You can use the template source at +`apps/portal/examples/nestjs-prisma-template/content/source/` as a base, with +template variables replaced (`${{ values.* }}` -> concrete values). + +### 4a. Create a test app directory + +```bash +mkdir -p /tmp/nestjs-test-app/src/prisma +mkdir -p /tmp/nestjs-test-app/prisma/migrations/0001_init +``` + +### 4b. Key files + +**package.json** — Note: use `npm install` in Dockerfile if no lockfile exists. +Set `start:migrate:prod` to run prisma migrations then start the app. + +```json +{ + "name": "test-backend", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "nest build", + "start:prod": "node dist/main", + "start:migrate:prod": "npx prisma migrate deploy && node dist/main" + }, + "dependencies": { + "@nestjs/common": "11.1.16", + "@nestjs/core": "11.1.16", + "@nestjs/platform-express": "11.1.16", + "@prisma/adapter-pg": "7.5.0", + "@prisma/client": "7.5.0", + "pg": "8.20.0", + "reflect-metadata": "0.2.2", + "rxjs": "7.8.2" + }, + "devDependencies": { + "@nestjs/cli": "11.0.16", + "@types/pg": "8.18.0", + "prisma": "7.5.0", + "ts-node": "10.9.2", + "typescript": "5.9.3" + } +} +``` + +**prisma.config.ts** — Prisma 7 requires this instead of `url` in schema: + +```typescript +import { defineConfig } from 'prisma/config'; + +function buildDatabaseUrl(): string { + const host = process.env.DB_HOST || 'localhost'; + const user = process.env.DB_USER || 'postgres'; + const pass = encodeURIComponent(process.env.DB_PASS || 'postgres'); + const name = process.env.DB_NAME || 'postgres'; + const port = process.env.DB_PORT || '5432'; + return `postgresql://${user}:${pass}@${host}:${port}/${name}?schema=public`; +} + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { path: 'prisma/migrations' }, + datasource: { url: process.env.DATABASE_URL || buildDatabaseUrl() }, +}); +``` + +**prisma/schema.prisma** — No `url` in datasource (Prisma 7 breaking change): + +```prisma +generator client { + provider = "prisma-client" +} + +datasource db { + provider = "postgresql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + @@map("users") +} +``` + +**prisma/migrations/migration_lock.toml**: + +```toml +# Prisma Migrate lockfile v1 + +provider = "postgresql" +``` + +**prisma/migrations/0001_init/migration.sql**: + +```sql +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); +``` + +**src/prisma/prisma.service.ts** — Use `import * as pg` (not default import): + +```typescript +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +import * as pg from 'pg'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + private pool: pg.Pool; + + constructor() { + const pool = new pg.Pool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || 'postgres', + database: process.env.DB_NAME || 'postgres', + port: parseInt(process.env.DB_PORT || '5432', 10), + }); + const adapter = new PrismaPg(pool as any); + super({ adapter }); + this.pool = pool; + } + + async onModuleInit(): Promise { await this.$connect(); } + async onModuleDestroy(): Promise { await this.$disconnect(); await this.pool.end(); } +} +``` + +**src/app.controller.ts** — Health endpoint: + +```typescript +import { Controller, Get } from '@nestjs/common'; + +@Controller() +export class AppController { + @Get('health') + health() { return { status: 'ok' }; } +} +``` + +(Also create standard `src/main.ts`, `src/app.module.ts`, `src/prisma/prisma.module.ts`, +`tsconfig.json`, `tsconfig.build.json`, `nest-cli.json` — use the template source +as reference.) + +### 4c. Dockerfile for test app + +```dockerfile +FROM node:24-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY prisma ./prisma/ +COPY prisma.config.ts ./ +RUN npx prisma generate +COPY . . +RUN npm run build + +FROM node:24-alpine AS production +WORKDIR /app +COPY package*.json ./ +RUN npm install --omit=dev +COPY --from=builder /app/prisma ./prisma/ +COPY --from=builder /app/prisma.config.ts ./ +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma/ +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma/ +COPY --from=builder /app/dist ./dist/ +EXPOSE 3000 +CMD ["npm", "run", "start:migrate:prod"] +``` + +### 4d. Build and load + +```bash +cd /tmp/nestjs-test-app +docker build -t test-backend:latest . +k3d image import test-backend:latest -c helios-test +cd - +``` + +--- + +## Step 5: Create Test Resources + +### 5a. Backend Deployment (simulates ArgoCD output) + +The operator injects env vars into an **existing** Deployment. In production, +ArgoCD creates this. For testing, create it manually: + +```bash +cat <<'EOF' | kubectl apply -f - +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-backend + namespace: default + labels: + app: test-backend +spec: + replicas: 1 + selector: + matchLabels: + app: test-backend + template: + metadata: + labels: + app: test-backend + spec: + containers: + - name: test-backend + image: test-backend:latest + imagePullPolicy: Never + ports: + - containerPort: 3000 + env: + - name: PORT + value: "3000" + - name: DB_NAME + value: "test_backend_db" + - name: DB_PORT + value: "5432" +EOF +``` + +### 5b. HeliosApp Custom Resource + +This triggers the operator's reconcile loop: + +```bash +cat <<'EOF' | kubectl apply -f - +apiVersion: app.helios.io/v1alpha1 +kind: HeliosApp +metadata: + name: test-app + namespace: default +spec: + owner: test-team + description: "Test app for database secret injection verification" + gitRepo: "https://github.com/test/test-repo" + gitopsRepo: "https://github.com/test/test-gitops" + gitopsPath: "test-app" + imageRepo: "test-backend" + gitBranch: main + gitopsBranch: main + gitopsSecretRef: "" + pipelineName: "from-code-to-cluster" + webhookDomain: "test.example.com" + webhookSecret: "github-webhook-secret" + port: 3000 + replicas: 1 + components: + - name: test-backend + type: web-service + properties: + image: "test-backend:latest" + port: 3000 + replicas: 1 + traits: + - type: service + properties: + port: 3000 + - type: database + properties: + dbType: postgres + dbName: test_backend_db + version: "16" + storage: "1Gi" +EOF +``` + +--- + +## Step 6: Verify Acceptance Criteria + +### 6a. Verify Secret Created (Phase 0.5) + +```bash +kubectl get secret test-backend-db-secret -o jsonpath='{.data}' | python3 -m json.tool +``` + +Expected: Secret contains `DB_HOST`, `DB_USER`, `DB_PASS` keys (base64-encoded). + +Decode values: + +```bash +kubectl get secret test-backend-db-secret -o jsonpath='{.data.DB_HOST}' | base64 -d +# Expected: test-backend-db + +kubectl get secret test-backend-db-secret -o jsonpath='{.data.DB_USER}' | base64 -d +# Expected: some generated username + +kubectl get secret test-backend-db-secret -o jsonpath='{.data.DB_PASS}' | base64 -d +# Expected: some generated password +``` + +### 6b. Verify Postgres StatefulSet Running (Phase 0.7) + +```bash +kubectl get statefulset test-backend-db +# Expected: READY 1/1 + +kubectl get svc test-backend-db +# Expected: headless service (ClusterIP: None) +``` + +### 6c. Verify Env Var Injection (Phase 0.9) — Acceptance Criterion #1 + +```bash +kubectl get deployment test-backend -o jsonpath='{.spec.template.spec.containers[0].env}' | python3 -m json.tool +``` + +Expected output includes `secretKeyRef` entries: + +```json +[ + { "name": "PORT", "value": "3000" }, + { "name": "DB_NAME", "value": "test_backend_db" }, + { "name": "DB_PORT", "value": "5432" }, + { + "name": "DB_HOST", + "valueFrom": { "secretKeyRef": { "name": "test-backend-db-secret", "key": "DB_HOST" } } + }, + { + "name": "DB_USER", + "valueFrom": { "secretKeyRef": { "name": "test-backend-db-secret", "key": "DB_USER" } } + }, + { + "name": "DB_PASS", + "valueFrom": { "secretKeyRef": { "name": "test-backend-db-secret", "key": "DB_PASS" } } + } +] +``` + +Also verify via `kubectl describe pod`: + +```bash +kubectl describe pod -l app=test-backend | grep -A2 "DB_HOST\|DB_USER\|DB_PASS" +``` + +### 6d. Verify NestJS Connects to Database — Acceptance Criterion #2 + +Wait for the backend pod to be ready: + +```bash +kubectl get pods -l app=test-backend -w +# Wait for: test-backend-xxx 1/1 Running +``` + +Check application logs: + +```bash +kubectl logs -l app=test-backend +``` + +Expected: Prisma migration runs, NestJS starts without connection errors. + +Port-forward and test health endpoint: + +```bash +kubectl port-forward deployment/test-backend 3000:3000 & +curl http://localhost:3000/health +# Expected: {"status":"ok"} +kill %1 +``` + +--- + +## Step 7: Check Operator Logs + +```bash +kubectl -n operator-system logs deployment/operator-controller-manager -c manager \ + | grep -E "secret|inject|database|StatefulSet" +``` + +Look for: + +- `"Created database secret"` (Phase 0.5) +- `"Created database StatefulSet"` / `"Created database Service"` (Phase 0.7) +- `"Injected database env vars"` (Phase 0.9) + +**Note:** Errors about Tekton CRDs or GitOps tokens are expected in this test +environment — the operator tolerates these and continues to database phases. + +--- + +## Step 8: Cleanup + +```bash +kubectl delete heliosapp test-app +kubectl delete deployment test-backend +kubectl delete statefulset test-backend-db +kubectl delete svc test-backend-db +kubectl delete secret test-backend-db-secret +k3d cluster delete helios-test +``` + +--- + +## Known Gotchas + +| Issue | Cause | Fix | +|---|---|---| +| `import pg from 'pg'` fails at runtime | CommonJS module system — `pg` has no default export | Use `import * as pg from 'pg'` | +| `url = env("DATABASE_URL")` in schema.prisma | Prisma 7 removed `url` from schema datasource | Use `prisma.config.ts` with `defineConfig()` | +| `prisma migrate deploy` fails with "lock file missing" | No `migration_lock.toml` in migrations dir | Create `migration_lock.toml` with `provider = "postgresql"` | +| `PrismaPg(pool)` TypeScript error | `@types/pg` Pool type conflicts with adapter's bundled types | Cast with `pool as any` | +| k3d kubeconfig uses `host.docker.internal` | k3d default for Docker Desktop | Override with `kubectl config set-cluster ... --server=https://127.0.0.1:` | +| CUE engine fails to initialize | CUE files not in operator image | Use `Dockerfile.test` which copies `cue/` into image | + +--- + +## Summary of Operator Phases Tested + +| Phase | Action | Resource Created | +|---|---|---| +| 0.5 | Generate DB secret | `Secret/-db-secret` | +| 0.7 | Provision Postgres | `StatefulSet/-db` + `Service/-db` | +| 0.9 | Inject env vars | Patches `Deployment/` with `secretKeyRef` | diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..7408765 --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,115 @@ +# Automated Secret Injection (#39) + +Inject database credentials (DB_HOST, DB_USER, DB_PASS) from the Operator-generated K8s Secret into the Backend Pod's env block, and create a NestJS + Prisma Node.js template that connects using these injected env vars. + +## Proposed Changes + +### Operator — Secret Injection into Backend Deployment + +The CUE engine renders a Deployment for each component (via GitOps sync). After the GitOps sync pushes the manifest, ArgoCD deploys it. However, the Deployment currently has **no database env vars** — the CUE `#Deployment` base only supports simple `{name, value}` env vars, not `secretKeyRef`. + +**Approach**: Add a new reconciliation phase (**Phase 0.9**) in [heliosapp_controller.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/heliosapp_controller.go) that **patches the live Deployment** (deployed by ArgoCD) to inject `DB_HOST`, `DB_USER`, and `DB_PASS` as `envFrom` / env vars referencing the K8s Secret. This runs AFTER database secrets and instances are created. + +> [!IMPORTANT] +> The CUE engine generates the base Deployment manifest (pushed via GitOps). The Go operator patches the **live Deployment** in-cluster to add secret env vars. This keeps secrets out of the GitOps repo entirely. + +#### [MODIFY] [database_resources.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go) + +Add a new function `InjectDatabaseEnvVars` that: +- Takes a Deployment and a database secret name +- Adds `DB_HOST`, `DB_USER`, `DB_PASS` env vars to the first container using `valueFrom.secretKeyRef` +- Also adds `DATABASE_URL` as a convenience env var for Prisma ORM (constructed from the other vars via an init container or as a direct string referencing the secret values) +- Is idempotent: skips if env vars already exist + +Add a new reconciler method `reconcileDatabaseSecretInjection` that: +- Finds components with database traits +- Gets or waits for the corresponding Deployment (by component name) +- Calls `InjectDatabaseEnvVars` to patch the Deployment's env block +- Uses `r.Update()` to apply changes to the live Deployment + +#### [MODIFY] [heliosapp_controller.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/heliosapp_controller.go) + +- Add a call to `r.reconcileDatabaseSecretInjection(ctx, &heliosApp)` as **Phase 0.9** (after database instance provisioning at Phase 0.7, before image validation) +- Add structured logging for the injection phase + +#### [MODIFY] [database_resources_test.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources_test.go) + +Add new test cases: +- `TestInjectDatabaseEnvVars` — verifies env vars are correctly injected into a Deployment +- `TestInjectDatabaseEnvVars_Idempotent` — verifies running injection twice doesn't duplicate env vars +- `TestReconcileDatabaseSecretInjection` — integration test with fake client verifying the full reconciliation flow +- `TestReconcileDatabaseSecretInjection_NoTraits` — skips when no database traits +- `TestReconcileDatabaseSecretInjection_DeploymentNotFound` — handles missing Deployment gracefully (returns nil, logs warning — Deployment may not be deployed by ArgoCD yet) + +--- + +### Node.js Template — NestJS + Prisma ORM + +Create a new Backstage template for NestJS + Prisma that connects to the database using the injected env vars. + +#### [NEW] `apps/portal/examples/nestjs-prisma-template/` directory + +Structure: +``` +nestjs-prisma-template/ + template.yaml # Backstage scaffolder template + content/ + source/ + package.json # NestJS + Prisma deps (latest) + tsconfig.json # TypeScript config + tsconfig.build.json # Build-specific TS config + nest-cli.json # NestJS CLI config + .env.example # Example env vars + Dockerfile # Multi-stage NestJS build + catalog-info.yaml # Backstage catalog entry + prisma/ + schema.prisma # Prisma schema with env-based DATABASE_URL + src/ + main.ts # NestJS bootstrap + app.module.ts # Root module + app.controller.ts # Health check controller + app.service.ts # App service + prisma/ + prisma.module.ts # Prisma module + prisma.service.ts # Prisma service (extends PrismaClient) + gitops/ + helios-app.yaml # HeliosApp CRD manifest with database trait +``` + +Key design decisions: +- `DATABASE_URL` is built from `DB_HOST`, `DB_USER`, `DB_PASS` env vars (Prisma convention) +- The Prisma schema reads `DATABASE_URL` from `env("DATABASE_URL")` +- The [Dockerfile](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/Dockerfile) runs `prisma generate` at build time and `prisma migrate deploy` at startup +- Uses latest stable NestJS v11+ and Prisma v6+ + +--- + +## Verification Plan + +### Automated Tests + +All existing tests plus new tests, run from `apps/operator/`: + +```sh +# 1. Compilation check +go build ./... + +# 2. Static analysis +go vet ./... + +# 3. Full test suite (includes all database_resources_test.go tests) +make test + +# 4. Build operator binary +make build + +# 5. Run database-specific tests in verbose mode +go test ./internal/controller/... -v -count=1 -run TestInjectDatabaseEnvVars +go test ./internal/controller/... -v -count=1 -run TestReconcileDatabaseSecretInjection +``` + +### Manual Verification + +After deploying a backend with a database trait: +1. Run `kubectl describe pod ` to verify DB_HOST, DB_USER, DB_PASS are present as environment variables referencing the secret +2. Verify the NestJS application boots and connects to the database by checking pod logs diff --git a/task.md b/task.md new file mode 100644 index 0000000..5efd769 --- /dev/null +++ b/task.md @@ -0,0 +1,28 @@ +# [Impl] Automated Secret Injection #39 + +## Operator — Secret Injection into Backend Deployment + +- [x] Research codebase and understand existing patterns +- [x] Implement [reconcileDatabaseSecretInjection](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#497-552) in Go operator + - [x] Add new phase (0.9) to inject DB_HOST, DB_USER, DB_PASS into backend Deployment env block + - [x] Patch the Deployment rendered by CUE/GitOps with env secretKeyRef +- [x] Add comprehensive tests for secret injection logic + - [x] Unit tests for injection function (3 tests) + - [x] Integration test with fake client (3 tests) + +## Node.js Template — NestJS + Prisma ORM + +- [x] Create NestJS + Prisma template directory structure + - [x] [package.json](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/advanced-template/content/source/package.json) with latest NestJS and Prisma deps + - [x] [prisma/schema.prisma](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma) connecting via DB_HOST, DB_USER, DB_PASS envs + - [x] `prisma/migrations/` with initial migration + - [x] NestJS main app module with Prisma service + - [x] [Dockerfile](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/Dockerfile) for containerized deployment +- [x] Update Backstage template for database-backed services + +## Verification + +- [/] `go build ./...` — compiles cleanly +- [x] `go vet ./...` — no issues +- [x] `make test` — all tests pass +- [/] `make build` — operator binary builds diff --git a/walkthrough.md b/walkthrough.md new file mode 100644 index 0000000..450130a --- /dev/null +++ b/walkthrough.md @@ -0,0 +1,2596 @@ +# Automated Secret Injection (#39) — Walkthrough + +## Changes Made + +### 1. Go Operator — Secret Injection (Phase 0.9) + +#### [database_resources.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go) + +- **[InjectDatabaseEnvVars](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#458-496)** — Patches a Deployment's first container with `DB_HOST`, `DB_USER`, `DB_PASS` env vars via `secretKeyRef`. Idempotent: skips if vars already exist. +- **[reconcileDatabaseSecretInjection](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#497-552)** — Finds components with database traits, fetches the live Deployment, calls [InjectDatabaseEnvVars](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#458-496), and updates it. Gracefully skips if Deployment doesn't exist yet (ArgoCD hasn't synced). + +```diff:database_resources.go +// database_resources.go handles database credential generation and Secret creation +// for components with database traits. +// +// The CUE engine generates ConfigMaps with database metadata (host, port, name), +// but credentials (username, password) are generated by this Go code for security +// reasons - secrets should never be stored in CUE definitions or GitOps repos. +package controller + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "strings" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" +) + +const ( + // DefaultPasswordLength is the length of generated passwords + DefaultPasswordLength = 32 + + // DefaultUsernameLength is the length of generated usernames + DefaultUsernameLength = 16 + + // PasswordCharset contains valid characters for password generation + // Includes uppercase, lowercase, digits, and special characters + PasswordCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+" + + // UsernameCharset contains valid characters for username generation + // Lowercase letters and digits only for database compatibility + UsernameCharset = "abcdefghijklmnopqrstuvwxyz0123456789" + + // DatabaseTraitType is the trait type identifier for database traits + DatabaseTraitType = "database" + + // DefaultPostgresVersion is the default Postgres image tag when not specified. + DefaultPostgresVersion = "16" + + // DefaultPostgresPort is the default port for Postgres. + DefaultPostgresPort = 5432 + + // DefaultDatabaseStorage is the default PVC size for database volumes. + DefaultDatabaseStorage = "1Gi" + + // PostgresDataPath is the mount path for Postgres data directory. + PostgresDataPath = "/var/lib/postgresql/data" + + // PostgresDataSubPath is the subPath within the PVC to avoid lost+found issues. + PostgresDataSubPath = "pgdata" +) + +// DatabaseCredentials holds generated database credentials +type DatabaseCredentials struct { + Username string + Password string +} + +// DatabaseTraitProperties represents the properties of a database trait +type DatabaseTraitProperties struct { + DBType string `json:"dbType"` + DBName string `json:"dbName"` + Port int `json:"port"` + Version string `json:"version"` + Storage string `json:"storage"` +} + +// GenerateSecurePassword generates a cryptographically secure random password +func GenerateSecurePassword(length int) (string, error) { + if length <= 0 { + length = DefaultPasswordLength + } + + password := make([]byte, length) + charsetLen := big.NewInt(int64(len(PasswordCharset))) + + for i := 0; i < length; i++ { + idx, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + password[i] = PasswordCharset[idx.Int64()] + } + + return string(password), nil +} + +// GenerateSecureUsername generates a cryptographically secure random username +func GenerateSecureUsername(length int) (string, error) { + if length <= 0 { + length = DefaultUsernameLength + } + + // Ensure username starts with a letter (database requirement) + username := make([]byte, length) + lettersOnly := "abcdefghijklmnopqrstuvwxyz" + lettersLen := big.NewInt(int64(len(lettersOnly))) + + // First character must be a letter + idx, err := rand.Int(rand.Reader, lettersLen) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + username[0] = lettersOnly[idx.Int64()] + + // Rest can be letters or digits + charsetLen := big.NewInt(int64(len(UsernameCharset))) + for i := 1; i < length; i++ { + idx, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + username[i] = UsernameCharset[idx.Int64()] + } + + return string(username), nil +} + +// GenerateCredentials generates a new set of database credentials +func GenerateCredentials() (*DatabaseCredentials, error) { + username, err := GenerateSecureUsername(DefaultUsernameLength) + if err != nil { + return nil, fmt.Errorf("failed to generate username: %w", err) + } + + password, err := GenerateSecurePassword(DefaultPasswordLength) + if err != nil { + return nil, fmt.Errorf("failed to generate password: %w", err) + } + + return &DatabaseCredentials{ + Username: username, + Password: password, + }, nil +} + +// GenerateDatabaseSecret creates a Kubernetes Secret containing database credentials +func GenerateDatabaseSecret(namespace, secretName, componentName string, creds *DatabaseCredentials, dbHost string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{ + "app": componentName, + "helios.io/managed-by": "operator", + "helios.io/secret-type": "database-credentials", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "DB_USER": []byte(creds.Username), + "DB_PASS": []byte(creds.Password), + "DB_HOST": []byte(dbHost), + }, + } +} + +// GetDatabaseSecretName returns the conventional secret name for a component +func GetDatabaseSecretName(componentName string) string { + return fmt.Sprintf("%s-db-secret", componentName) +} + +// GetDatabaseHost returns the conventional database host for a component +func GetDatabaseHost(componentName string) string { + return fmt.Sprintf("%s-db", componentName) +} + +// ExtractDatabaseTraits extracts all database traits from HeliosApp components +func ExtractDatabaseTraits(app *appv1alpha1.HeliosApp) []struct { + ComponentName string + Properties DatabaseTraitProperties +} { + var dbTraits []struct { + ComponentName string + Properties DatabaseTraitProperties + } + + for _, component := range app.Spec.Components { + for _, trait := range component.Traits { + if strings.ToLower(trait.Type) == DatabaseTraitType { + var props DatabaseTraitProperties + if trait.Properties != nil && trait.Properties.Raw != nil { + if err := json.Unmarshal(trait.Properties.Raw, &props); err != nil { + // Log error but continue - don't fail the entire reconciliation + continue + } + } + dbTraits = append(dbTraits, struct { + ComponentName string + Properties DatabaseTraitProperties + }{ + ComponentName: component.Name, + Properties: props, + }) + } + } + } + + return dbTraits +} + +// reconcileDatabaseSecrets ensures database credential secrets exist for all +// components with database traits. If a secret already exists, it is not modified +// to preserve existing credentials. +func (r *HeliosAppReconciler) reconcileDatabaseSecrets(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + dbTraits := ExtractDatabaseTraits(app) + if len(dbTraits) == 0 { + log.V(1).Info("No database traits found, skipping secret creation") + return nil + } + + for _, dbTrait := range dbTraits { + secretName := GetDatabaseSecretName(dbTrait.ComponentName) + dbHost := GetDatabaseHost(dbTrait.ComponentName) + + // Check if secret already exists + existingSecret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{ + Name: secretName, + Namespace: app.Namespace, + }, existingSecret) + + if err == nil { + // Secret already exists - do not overwrite to preserve credentials + log.Info("Database secret already exists, skipping", + "component", dbTrait.ComponentName, + "secret", secretName) + continue + } + + if !errors.IsNotFound(err) { + // Unexpected error + log.Error(err, "Failed to check for existing database secret", + "component", dbTrait.ComponentName, + "secret", secretName) + return fmt.Errorf("failed to check for database secret %s: %w", secretName, err) + } + + // Secret doesn't exist - generate new credentials + log.Info("Generating database credentials", + "component", dbTrait.ComponentName, + "secret", secretName) + + creds, err := GenerateCredentials() + if err != nil { + log.Error(err, "Failed to generate database credentials", + "component", dbTrait.ComponentName) + return fmt.Errorf("failed to generate credentials for %s: %w", dbTrait.ComponentName, err) + } + + // Create the secret + secret := GenerateDatabaseSecret(app.Namespace, secretName, dbTrait.ComponentName, creds, dbHost) + + // Set owner reference so secret is garbage collected with the HeliosApp + if err := ctrl.SetControllerReference(app, secret, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for database secret", + "component", dbTrait.ComponentName, + "secret", secretName) + return fmt.Errorf("failed to set owner reference for secret %s: %w", secretName, err) + } + + if err := r.Create(ctx, secret); err != nil { + if errors.IsAlreadyExists(err) { + // Race condition - secret was created between our check and create + log.Info("Database secret was created concurrently, skipping", + "component", dbTrait.ComponentName, + "secret", secretName) + continue + } + log.Error(err, "Failed to create database secret", + "component", dbTrait.ComponentName, + "secret", secretName) + return fmt.Errorf("failed to create database secret %s: %w", secretName, err) + } + + log.Info("Successfully created database secret", + "component", dbTrait.ComponentName, + "secret", secretName, + "dbHost", dbHost) + } + + return nil +} + +// reconcileDatabaseInstance provisions database StatefulSets and headless +// Services for components with database traits. This runs AFTER +// reconcileDatabaseSecrets so that the credential Secret already exists +// when the StatefulSet is created. +func (r *HeliosAppReconciler) reconcileDatabaseInstance(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + dbTraits := ExtractDatabaseTraits(app) + if len(dbTraits) == 0 { + log.V(1).Info("No database traits found, skipping instance provisioning") + return nil + } + + for _, dbTrait := range dbTraits { + // Only provision postgres instances for now + if strings.ToLower(dbTrait.Properties.DBType) != "postgres" { + log.V(1).Info("Skipping non-postgres database type", + "component", dbTrait.ComponentName, + "dbType", dbTrait.Properties.DBType) + continue + } + + dbHost := GetDatabaseHost(dbTrait.ComponentName) + secretName := GetDatabaseSecretName(dbTrait.ComponentName) + + // Determine effective database name + effectiveDBName := dbTrait.Properties.DBName + if effectiveDBName == "" { + effectiveDBName = fmt.Sprintf("%s-db", dbTrait.ComponentName) + } + + // Determine version — CUE schema requires version!, but we + // guard here defensively in case of direct API usage. + version := dbTrait.Properties.Version + if version == "" { + version = DefaultPostgresVersion + } + + // Determine port + port := dbTrait.Properties.Port + if port <= 0 { + port = DefaultPostgresPort + } + + // Determine storage + storage := dbTrait.Properties.Storage + if storage == "" { + storage = DefaultDatabaseStorage + } + + // --- StatefulSet --- + sts, err := GenerateDatabaseStatefulSet( + app.Namespace, dbHost, secretName, effectiveDBName, version, storage, int32(port), + ) + if err != nil { + log.Error(err, "Failed to generate database StatefulSet", + "component", dbTrait.ComponentName, "storage", storage) + return fmt.Errorf("failed to generate StatefulSet for %s: %w", dbHost, err) + } + + if err := ctrl.SetControllerReference(app, sts, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for database StatefulSet", + "component", dbTrait.ComponentName) + return fmt.Errorf("failed to set owner reference for StatefulSet %s: %w", dbHost, err) + } + + existingSts := &appsv1.StatefulSet{} + err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSts) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to check for StatefulSet %s: %w", dbHost, err) + } + + log.Info("Creating database StatefulSet", + "component", dbTrait.ComponentName, + "statefulset", dbHost, + "image", fmt.Sprintf("postgres:%s", version)) + + if err := r.Create(ctx, sts); err != nil { + if errors.IsAlreadyExists(err) { + log.Info("Database StatefulSet was created concurrently, skipping", + "component", dbTrait.ComponentName) + } else { + return fmt.Errorf("failed to create StatefulSet %s: %w", dbHost, err) + } + } + } else { + // Handle StatefulSet drift: update spec to match the new template + log.Info("Database StatefulSet already exists, updating if necessary", + "component", dbTrait.ComponentName, + "statefulset", dbHost) + + // We only update the mutable fields (Replicas, Template) + updatedSts := existingSts.DeepCopy() + updatedSts.Spec.Replicas = sts.Spec.Replicas + updatedSts.Spec.Template = sts.Spec.Template + + // We need to preserve the existing VolumeClaimTemplates when updating + updatedSts.Spec.VolumeClaimTemplates = existingSts.Spec.VolumeClaimTemplates + + if err := r.Update(ctx, updatedSts); err != nil { + return fmt.Errorf("failed to update StatefulSet %s: %w", dbHost, err) + } + } + + // --- Headless Service --- + svc := GenerateDatabaseService(app.Namespace, dbHost, int32(port)) + + if err := ctrl.SetControllerReference(app, svc, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for database Service", + "component", dbTrait.ComponentName) + return fmt.Errorf("failed to set owner reference for Service %s: %w", dbHost, err) + } + + existingSvc := &corev1.Service{} + err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSvc) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to check for Service %s: %w", dbHost, err) + } + + log.Info("Creating database headless Service", + "component", dbTrait.ComponentName, + "service", dbHost) + + if err := r.Create(ctx, svc); err != nil { + if errors.IsAlreadyExists(err) { + log.Info("Database Service was created concurrently, skipping", + "component", dbTrait.ComponentName) + } else { + return fmt.Errorf("failed to create Service %s: %w", dbHost, err) + } + } + } else { + log.Info("Database Service already exists, updating if necessary", + "component", dbTrait.ComponentName, + "service", dbHost) + + updatedSvc := existingSvc.DeepCopy() + updatedSvc.Spec.Ports = svc.Spec.Ports + + if err := r.Update(ctx, updatedSvc); err != nil { + return fmt.Errorf("failed to update Service %s: %w", dbHost, err) + } + } + + log.Info("Successfully reconciled database instance", + "component", dbTrait.ComponentName, + "statefulset", dbHost, + "dbName", effectiveDBName) + } + + return nil +} + +// GenerateDatabaseStatefulSet creates a StatefulSet for a Postgres database instance. +// The StatefulSet injects POSTGRES_DB from the CRD's database.name value, and +// uses the Secret from Issue #33 for POSTGRES_USER and POSTGRES_PASSWORD. +func GenerateDatabaseStatefulSet(namespace, name, secretName, dbName, version, storage string, port int32) (*appsv1.StatefulSet, error) { + storageQty, err := resource.ParseQuantity(storage) + if err != nil { + return nil, fmt.Errorf("invalid storage size format %q: %w", storage, err) + } + + replicas := int32(1) + labels := map[string]string{ + "app": name, + "helios.io/managed-by": "operator", + "helios.io/trait": "database", + "helios.io/db-type": "postgres", + } + + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + ServiceName: name, + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": name}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "postgres", + Image: fmt.Sprintf("postgres:%s", version), + Ports: []corev1.ContainerPort{ + { + ContainerPort: port, + Name: "postgres", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "POSTGRES_DB", + Value: dbName, + }, + { + Name: "POSTGRES_USER", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "DB_USER", + }, + }, + }, + { + Name: "POSTGRES_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "DB_PASS", + }, + }, + }, + { + // PGDATA tells Postgres where to store cluster data. + // Must match volumeMount + subPath to avoid lost+found conflicts. + Name: "PGDATA", + Value: PostgresDataPath + "/" + PostgresDataSubPath, + }, + { + // Ensure consistent UTF-8 encoding for all databases. + Name: "POSTGRES_INITDB_ARGS", + Value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C", + }, + { + // Explicitly set the custom port so that postgres knows to listen on it. + Name: "PGPORT", + Value: fmt.Sprintf("%d", port), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "data", + MountPath: PostgresDataPath, + SubPath: PostgresDataSubPath, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resourceMustParse("100m"), + corev1.ResourceMemory: resourceMustParse("256Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resourceMustParse("500m"), + corev1.ResourceMemory: resourceMustParse("512Mi"), + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 10, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, + }, + }, + InitialDelaySeconds: 30, + PeriodSeconds: 10, + }, + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "data", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageQty, + }, + }, + }, + }, + }, + }, + }, nil +} + +// GenerateDatabaseService creates a headless Service for a database StatefulSet. +// The headless Service (clusterIP: None) provides stable DNS resolution +// so resolves to the database pod. +func GenerateDatabaseService(namespace, name string, port int32) *corev1.Service { + labels := map[string]string{ + "app": name, + "helios.io/managed-by": "operator", + "helios.io/trait": "database", + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: "None", + Selector: map[string]string{"app": name}, + Ports: []corev1.ServicePort{ + { + Port: port, + TargetPort: intstr.FromInt32(port), + Name: "db", + }, + }, + }, + } +} + +// resourceMustParse is a helper to parse resource quantities. Panics on invalid input. +func resourceMustParse(s string) resource.Quantity { + return resource.MustParse(s) +} + +// GenerateBase64Token generates a random base64-encoded token +// Useful for generating secure webhook secrets or API tokens +func GenerateBase64Token(byteLength int) (string, error) { + if byteLength <= 0 { + byteLength = 32 + } + + bytes := make([]byte, byteLength) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("failed to generate random bytes: %w", err) + } + + return base64.StdEncoding.EncodeToString(bytes), nil +} +=== +// database_resources.go handles database credential generation and Secret creation +// for components with database traits. +// +// The CUE engine generates ConfigMaps with database metadata (host, port, name), +// but credentials (username, password) are generated by this Go code for security +// reasons - secrets should never be stored in CUE definitions or GitOps repos. +package controller + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "strings" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" +) + +const ( + // DefaultPasswordLength is the length of generated passwords + DefaultPasswordLength = 32 + + // DefaultUsernameLength is the length of generated usernames + DefaultUsernameLength = 16 + + // PasswordCharset contains valid characters for password generation + // Includes uppercase, lowercase, digits, and special characters + PasswordCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+" + + // UsernameCharset contains valid characters for username generation + // Lowercase letters and digits only for database compatibility + UsernameCharset = "abcdefghijklmnopqrstuvwxyz0123456789" + + // DatabaseTraitType is the trait type identifier for database traits + DatabaseTraitType = "database" + + // DefaultPostgresVersion is the default Postgres image tag when not specified. + DefaultPostgresVersion = "16" + + // DefaultPostgresPort is the default port for Postgres. + DefaultPostgresPort = 5432 + + // DefaultDatabaseStorage is the default PVC size for database volumes. + DefaultDatabaseStorage = "1Gi" + + // PostgresDataPath is the mount path for Postgres data directory. + PostgresDataPath = "/var/lib/postgresql/data" + + // PostgresDataSubPath is the subPath within the PVC to avoid lost+found issues. + PostgresDataSubPath = "pgdata" +) + +// DatabaseCredentials holds generated database credentials +type DatabaseCredentials struct { + Username string + Password string +} + +// DatabaseTraitProperties represents the properties of a database trait +type DatabaseTraitProperties struct { + DBType string `json:"dbType"` + DBName string `json:"dbName"` + Port int `json:"port"` + Version string `json:"version"` + Storage string `json:"storage"` +} + +// GenerateSecurePassword generates a cryptographically secure random password +func GenerateSecurePassword(length int) (string, error) { + if length <= 0 { + length = DefaultPasswordLength + } + + password := make([]byte, length) + charsetLen := big.NewInt(int64(len(PasswordCharset))) + + for i := 0; i < length; i++ { + idx, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + password[i] = PasswordCharset[idx.Int64()] + } + + return string(password), nil +} + +// GenerateSecureUsername generates a cryptographically secure random username +func GenerateSecureUsername(length int) (string, error) { + if length <= 0 { + length = DefaultUsernameLength + } + + // Ensure username starts with a letter (database requirement) + username := make([]byte, length) + lettersOnly := "abcdefghijklmnopqrstuvwxyz" + lettersLen := big.NewInt(int64(len(lettersOnly))) + + // First character must be a letter + idx, err := rand.Int(rand.Reader, lettersLen) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + username[0] = lettersOnly[idx.Int64()] + + // Rest can be letters or digits + charsetLen := big.NewInt(int64(len(UsernameCharset))) + for i := 1; i < length; i++ { + idx, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + username[i] = UsernameCharset[idx.Int64()] + } + + return string(username), nil +} + +// GenerateCredentials generates a new set of database credentials +func GenerateCredentials() (*DatabaseCredentials, error) { + username, err := GenerateSecureUsername(DefaultUsernameLength) + if err != nil { + return nil, fmt.Errorf("failed to generate username: %w", err) + } + + password, err := GenerateSecurePassword(DefaultPasswordLength) + if err != nil { + return nil, fmt.Errorf("failed to generate password: %w", err) + } + + return &DatabaseCredentials{ + Username: username, + Password: password, + }, nil +} + +// GenerateDatabaseSecret creates a Kubernetes Secret containing database credentials +func GenerateDatabaseSecret(namespace, secretName, componentName string, creds *DatabaseCredentials, dbHost string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{ + "app": componentName, + "helios.io/managed-by": "operator", + "helios.io/secret-type": "database-credentials", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "DB_USER": []byte(creds.Username), + "DB_PASS": []byte(creds.Password), + "DB_HOST": []byte(dbHost), + }, + } +} + +// GetDatabaseSecretName returns the conventional secret name for a component +func GetDatabaseSecretName(componentName string) string { + return fmt.Sprintf("%s-db-secret", componentName) +} + +// GetDatabaseHost returns the conventional database host for a component +func GetDatabaseHost(componentName string) string { + return fmt.Sprintf("%s-db", componentName) +} + +// ExtractDatabaseTraits extracts all database traits from HeliosApp components +func ExtractDatabaseTraits(app *appv1alpha1.HeliosApp) []struct { + ComponentName string + Properties DatabaseTraitProperties +} { + var dbTraits []struct { + ComponentName string + Properties DatabaseTraitProperties + } + + for _, component := range app.Spec.Components { + for _, trait := range component.Traits { + if strings.ToLower(trait.Type) == DatabaseTraitType { + var props DatabaseTraitProperties + if trait.Properties != nil && trait.Properties.Raw != nil { + if err := json.Unmarshal(trait.Properties.Raw, &props); err != nil { + // Log error but continue - don't fail the entire reconciliation + continue + } + } + dbTraits = append(dbTraits, struct { + ComponentName string + Properties DatabaseTraitProperties + }{ + ComponentName: component.Name, + Properties: props, + }) + } + } + } + + return dbTraits +} + +// reconcileDatabaseSecrets ensures database credential secrets exist for all +// components with database traits. If a secret already exists, it is not modified +// to preserve existing credentials. +func (r *HeliosAppReconciler) reconcileDatabaseSecrets(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + dbTraits := ExtractDatabaseTraits(app) + if len(dbTraits) == 0 { + log.V(1).Info("No database traits found, skipping secret creation") + return nil + } + + for _, dbTrait := range dbTraits { + secretName := GetDatabaseSecretName(dbTrait.ComponentName) + dbHost := GetDatabaseHost(dbTrait.ComponentName) + + // Check if secret already exists + existingSecret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{ + Name: secretName, + Namespace: app.Namespace, + }, existingSecret) + + if err == nil { + // Secret already exists - do not overwrite to preserve credentials + log.Info("Database secret already exists, skipping", + "component", dbTrait.ComponentName, + "secret", secretName) + continue + } + + if !errors.IsNotFound(err) { + // Unexpected error + log.Error(err, "Failed to check for existing database secret", + "component", dbTrait.ComponentName, + "secret", secretName) + return fmt.Errorf("failed to check for database secret %s: %w", secretName, err) + } + + // Secret doesn't exist - generate new credentials + log.Info("Generating database credentials", + "component", dbTrait.ComponentName, + "secret", secretName) + + creds, err := GenerateCredentials() + if err != nil { + log.Error(err, "Failed to generate database credentials", + "component", dbTrait.ComponentName) + return fmt.Errorf("failed to generate credentials for %s: %w", dbTrait.ComponentName, err) + } + + // Create the secret + secret := GenerateDatabaseSecret(app.Namespace, secretName, dbTrait.ComponentName, creds, dbHost) + + // Set owner reference so secret is garbage collected with the HeliosApp + if err := ctrl.SetControllerReference(app, secret, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for database secret", + "component", dbTrait.ComponentName, + "secret", secretName) + return fmt.Errorf("failed to set owner reference for secret %s: %w", secretName, err) + } + + if err := r.Create(ctx, secret); err != nil { + if errors.IsAlreadyExists(err) { + // Race condition - secret was created between our check and create + log.Info("Database secret was created concurrently, skipping", + "component", dbTrait.ComponentName, + "secret", secretName) + continue + } + log.Error(err, "Failed to create database secret", + "component", dbTrait.ComponentName, + "secret", secretName) + return fmt.Errorf("failed to create database secret %s: %w", secretName, err) + } + + log.Info("Successfully created database secret", + "component", dbTrait.ComponentName, + "secret", secretName, + "dbHost", dbHost) + } + + return nil +} + +// reconcileDatabaseInstance provisions database StatefulSets and headless +// Services for components with database traits. This runs AFTER +// reconcileDatabaseSecrets so that the credential Secret already exists +// when the StatefulSet is created. +func (r *HeliosAppReconciler) reconcileDatabaseInstance(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + dbTraits := ExtractDatabaseTraits(app) + if len(dbTraits) == 0 { + log.V(1).Info("No database traits found, skipping instance provisioning") + return nil + } + + for _, dbTrait := range dbTraits { + // Only provision postgres instances for now + if strings.ToLower(dbTrait.Properties.DBType) != "postgres" { + log.V(1).Info("Skipping non-postgres database type", + "component", dbTrait.ComponentName, + "dbType", dbTrait.Properties.DBType) + continue + } + + dbHost := GetDatabaseHost(dbTrait.ComponentName) + secretName := GetDatabaseSecretName(dbTrait.ComponentName) + + // Determine effective database name + effectiveDBName := dbTrait.Properties.DBName + if effectiveDBName == "" { + effectiveDBName = fmt.Sprintf("%s-db", dbTrait.ComponentName) + } + + // Determine version — CUE schema requires version!, but we + // guard here defensively in case of direct API usage. + version := dbTrait.Properties.Version + if version == "" { + version = DefaultPostgresVersion + } + + // Determine port + port := dbTrait.Properties.Port + if port <= 0 { + port = DefaultPostgresPort + } + + // Determine storage + storage := dbTrait.Properties.Storage + if storage == "" { + storage = DefaultDatabaseStorage + } + + // --- StatefulSet --- + sts, err := GenerateDatabaseStatefulSet( + app.Namespace, dbHost, secretName, effectiveDBName, version, storage, int32(port), + ) + if err != nil { + log.Error(err, "Failed to generate database StatefulSet", + "component", dbTrait.ComponentName, "storage", storage) + return fmt.Errorf("failed to generate StatefulSet for %s: %w", dbHost, err) + } + + if err := ctrl.SetControllerReference(app, sts, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for database StatefulSet", + "component", dbTrait.ComponentName) + return fmt.Errorf("failed to set owner reference for StatefulSet %s: %w", dbHost, err) + } + + existingSts := &appsv1.StatefulSet{} + err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSts) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to check for StatefulSet %s: %w", dbHost, err) + } + + log.Info("Creating database StatefulSet", + "component", dbTrait.ComponentName, + "statefulset", dbHost, + "image", fmt.Sprintf("postgres:%s", version)) + + if err := r.Create(ctx, sts); err != nil { + if errors.IsAlreadyExists(err) { + log.Info("Database StatefulSet was created concurrently, skipping", + "component", dbTrait.ComponentName) + } else { + return fmt.Errorf("failed to create StatefulSet %s: %w", dbHost, err) + } + } + } else { + // Handle StatefulSet drift: update spec to match the new template + log.Info("Database StatefulSet already exists, updating if necessary", + "component", dbTrait.ComponentName, + "statefulset", dbHost) + + // We only update the mutable fields (Replicas, Template) + updatedSts := existingSts.DeepCopy() + updatedSts.Spec.Replicas = sts.Spec.Replicas + updatedSts.Spec.Template = sts.Spec.Template + + // We need to preserve the existing VolumeClaimTemplates when updating + updatedSts.Spec.VolumeClaimTemplates = existingSts.Spec.VolumeClaimTemplates + + if err := r.Update(ctx, updatedSts); err != nil { + return fmt.Errorf("failed to update StatefulSet %s: %w", dbHost, err) + } + } + + // --- Headless Service --- + svc := GenerateDatabaseService(app.Namespace, dbHost, int32(port)) + + if err := ctrl.SetControllerReference(app, svc, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for database Service", + "component", dbTrait.ComponentName) + return fmt.Errorf("failed to set owner reference for Service %s: %w", dbHost, err) + } + + existingSvc := &corev1.Service{} + err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSvc) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to check for Service %s: %w", dbHost, err) + } + + log.Info("Creating database headless Service", + "component", dbTrait.ComponentName, + "service", dbHost) + + if err := r.Create(ctx, svc); err != nil { + if errors.IsAlreadyExists(err) { + log.Info("Database Service was created concurrently, skipping", + "component", dbTrait.ComponentName) + } else { + return fmt.Errorf("failed to create Service %s: %w", dbHost, err) + } + } + } else { + log.Info("Database Service already exists, updating if necessary", + "component", dbTrait.ComponentName, + "service", dbHost) + + updatedSvc := existingSvc.DeepCopy() + updatedSvc.Spec.Ports = svc.Spec.Ports + + if err := r.Update(ctx, updatedSvc); err != nil { + return fmt.Errorf("failed to update Service %s: %w", dbHost, err) + } + } + + log.Info("Successfully reconciled database instance", + "component", dbTrait.ComponentName, + "statefulset", dbHost, + "dbName", effectiveDBName) + } + + return nil +} + +// databaseEnvVarNames lists the env var names injected by the operator +// for database credential secret injection. +var databaseEnvVarNames = []string{"DB_HOST", "DB_USER", "DB_PASS"} + +// InjectDatabaseEnvVars patches a Deployment's first container to include +// DB_HOST, DB_USER, DB_PASS env vars referencing the given K8s Secret. +// The function is idempotent — if the env vars already exist with the correct +// secretKeyRef, no changes are made and it returns false. +func InjectDatabaseEnvVars(deploy *appsv1.Deployment, secretName string) bool { + if len(deploy.Spec.Template.Spec.Containers) == 0 { + return false + } + + container := &deploy.Spec.Template.Spec.Containers[0] + + // Build a set of existing env var names for fast lookup. + existingEnvs := make(map[string]bool, len(container.Env)) + for _, ev := range container.Env { + existingEnvs[ev.Name] = true + } + + changed := false + for _, envName := range databaseEnvVarNames { + if existingEnvs[envName] { + continue + } + container.Env = append(container.Env, corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: envName, + }, + }, + }) + changed = true + } + + return changed +} + +// reconcileDatabaseSecretInjection patches live Deployments (deployed by ArgoCD) +// to inject DB_HOST, DB_USER, DB_PASS env vars from the operator-managed Secret. +// This runs AFTER database secrets and instances are provisioned so that the +// Secret already exists when the Deployment references it. +func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + dbTraits := ExtractDatabaseTraits(app) + if len(dbTraits) == 0 { + log.V(1).Info("No database traits found, skipping secret injection") + return nil + } + + for _, dbTrait := range dbTraits { + secretName := GetDatabaseSecretName(dbTrait.ComponentName) + deployName := dbTrait.ComponentName + + // Fetch the live Deployment (created by ArgoCD via GitOps). + deploy := &appsv1.Deployment{} + err := r.Get(ctx, types.NamespacedName{ + Name: deployName, + Namespace: app.Namespace, + }, deploy) + if err != nil { + if errors.IsNotFound(err) { + // Deployment may not exist yet (ArgoCD hasn't synced). + // This is expected — we'll inject on the next reconcile. + log.Info("Deployment not found yet, will inject on next reconcile", + "component", dbTrait.ComponentName, + "deployment", deployName) + continue + } + return fmt.Errorf("failed to get Deployment %s: %w", deployName, err) + } + + // Inject env vars if not already present. + if !InjectDatabaseEnvVars(deploy, secretName) { + log.V(1).Info("Database env vars already injected, skipping", + "component", dbTrait.ComponentName, + "deployment", deployName) + continue + } + + if err := r.Update(ctx, deploy); err != nil { + return fmt.Errorf("failed to update Deployment %s with database env vars: %w", deployName, err) + } + + log.Info("Successfully injected database env vars into Deployment", + "component", dbTrait.ComponentName, + "deployment", deployName, + "secret", secretName) + } + + return nil +} + +// GenerateDatabaseStatefulSet creates a StatefulSet for a Postgres database instance. +// The StatefulSet injects POSTGRES_DB from the CRD's database.name value, and +// uses the Secret from Issue #33 for POSTGRES_USER and POSTGRES_PASSWORD. +func GenerateDatabaseStatefulSet(namespace, name, secretName, dbName, version, storage string, port int32) (*appsv1.StatefulSet, error) { + storageQty, err := resource.ParseQuantity(storage) + if err != nil { + return nil, fmt.Errorf("invalid storage size format %q: %w", storage, err) + } + + replicas := int32(1) + labels := map[string]string{ + "app": name, + "helios.io/managed-by": "operator", + "helios.io/trait": "database", + "helios.io/db-type": "postgres", + } + + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + ServiceName: name, + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": name}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "postgres", + Image: fmt.Sprintf("postgres:%s", version), + Ports: []corev1.ContainerPort{ + { + ContainerPort: port, + Name: "postgres", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "POSTGRES_DB", + Value: dbName, + }, + { + Name: "POSTGRES_USER", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "DB_USER", + }, + }, + }, + { + Name: "POSTGRES_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "DB_PASS", + }, + }, + }, + { + // PGDATA tells Postgres where to store cluster data. + // Must match volumeMount + subPath to avoid lost+found conflicts. + Name: "PGDATA", + Value: PostgresDataPath + "/" + PostgresDataSubPath, + }, + { + // Ensure consistent UTF-8 encoding for all databases. + Name: "POSTGRES_INITDB_ARGS", + Value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C", + }, + { + // Explicitly set the custom port so that postgres knows to listen on it. + Name: "PGPORT", + Value: fmt.Sprintf("%d", port), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "data", + MountPath: PostgresDataPath, + SubPath: PostgresDataSubPath, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resourceMustParse("100m"), + corev1.ResourceMemory: resourceMustParse("256Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resourceMustParse("500m"), + corev1.ResourceMemory: resourceMustParse("512Mi"), + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 10, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, + }, + }, + InitialDelaySeconds: 30, + PeriodSeconds: 10, + }, + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "data", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageQty, + }, + }, + }, + }, + }, + }, + }, nil +} + +// GenerateDatabaseService creates a headless Service for a database StatefulSet. +// The headless Service (clusterIP: None) provides stable DNS resolution +// so resolves to the database pod. +func GenerateDatabaseService(namespace, name string, port int32) *corev1.Service { + labels := map[string]string{ + "app": name, + "helios.io/managed-by": "operator", + "helios.io/trait": "database", + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: "None", + Selector: map[string]string{"app": name}, + Ports: []corev1.ServicePort{ + { + Port: port, + TargetPort: intstr.FromInt32(port), + Name: "db", + }, + }, + }, + } +} + +// resourceMustParse is a helper to parse resource quantities. Panics on invalid input. +func resourceMustParse(s string) resource.Quantity { + return resource.MustParse(s) +} + +// GenerateBase64Token generates a random base64-encoded token +// Useful for generating secure webhook secrets or API tokens +func GenerateBase64Token(byteLength int) (string, error) { + if byteLength <= 0 { + byteLength = 32 + } + + bytes := make([]byte, byteLength) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("failed to generate random bytes: %w", err) + } + + return base64.StdEncoding.EncodeToString(bytes), nil +} +``` + +#### [heliosapp_controller.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/heliosapp_controller.go) + +Added **Phase 0.9** call after database instance provisioning: + +```diff:heliosapp_controller.go +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "cmp" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" + heliosCue "github.com/helios-platform-team/helios-platform/apps/operator/internal/cue" + "github.com/helios-platform-team/helios-platform/apps/operator/internal/gitops" +) + +// HeliosAppReconciler reconciles a HeliosApp object +type HeliosAppReconciler struct { + client.Client + Scheme *runtime.Scheme + CueEngine heliosCue.CueEngineInterface + // TektonRenderer renders Tekton CI/CD resources via CUE engine. + TektonRenderer heliosCue.TektonRendererInterface + // GitFactory allows injecting a custom GitOps client (e.g. for testing) + GitFactory func(string, string, string) gitops.GitOpsClientInterface +} + +// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile handles the reconciliation loop for HeliosApp +// Controller does NOT iterate components/traits - all orchestration is in CUE +func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + // 1. Fetch HeliosApp CRD + var heliosApp appv1alpha1.HeliosApp + if err := r.Get(ctx, req.NamespacedName, &heliosApp); err != nil { + if errors.IsNotFound(err) { + log.Info("HeliosApp resource not found, ignoring") + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + log.Info("Reconciling HeliosApp", "name", heliosApp.Name, "namespace", heliosApp.Namespace) + + // 2. Map CRD to Application Model + appModel, err := r.mapCRDToModel(&heliosApp) + if err != nil { + log.Error(err, "Failed to map CRD to application model") + return ctrl.Result{}, err + } + + // 3. Render via CUE Engine + manifestBytes, err := r.CueEngine.Render(appModel) + if err != nil { + log.Error(err, "Failed to render application via CUE") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE rendering failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE -1 & 0: Tekton CI/CD Resources (Tasks, Pipeline, Triggers) + // All Tekton resources are rendered via CUE engine. + // ------------------------------------------------------------------ + if err := r.reconcileTektonResourcesCue(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to reconcile Tekton resources via CUE") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE Tekton rendering failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE 0.5: Database Credential Secrets + // Generate and store secure credentials for components with database traits. + // Secrets are created BEFORE GitOps sync to ensure credentials exist + // when the application is deployed. + // ------------------------------------------------------------------ + if err := r.reconcileDatabaseSecrets(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to reconcile database secrets") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret creation failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE 0.7: Database Instance Provisioning + // Provision StatefulSets and headless Services for database traits. + // Runs AFTER secrets so that the credential Secret already exists + // when the database pod starts. + // ------------------------------------------------------------------ + if err := r.reconcileDatabaseInstance(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to reconcile database instance") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database instance provisioning failed: %v", err)) + return ctrl.Result{}, err + } + + // VALIDATION: Ensure image is present (Fix "First Commit Missing Image") + // This validation is for application workloads (GitOps pipeline downstream). + // We run this AFTER DB provisioning so databases can come up while app is building. + for _, comp := range appModel.App.Components { + // We can add more specific checks here based on component type + // For now, checks if 'image' property exists and is not empty for all components + // assuming all workloads need an image. + if img, ok := comp.Properties["image"].(string); !ok || img == "" { + msg := fmt.Sprintf("Component '%s' is waiting for image (likely building). Status: Pending.", comp.Name) + log.Info(msg) + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhasePending, msg) + return ctrl.Result{}, nil // Wait for next update (CI/CD will update CR with image) + } + } + + // ------------------------------------------------------------------ + // PHASE 0.6: Trigger Initial PipelineRun (if not already done) + // ------------------------------------------------------------------ + if !heliosApp.Status.InitialBuildTriggered { + log.Info("Triggering initial PipelineRun for new HeliosApp") + + pipelineName := heliosApp.Spec.PipelineName + if pipelineName == "" { + pipelineName = "from-code-to-cluster" + } + pr, err := GeneratePipelineRunForManifestGeneration(&heliosApp, pipelineName) + if err != nil { + log.Error(err, "Failed to generate initial PipelineRun") + } else { + if err := ctrl.SetControllerReference(&heliosApp, pr, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for PipelineRun") + } + + if err := r.Client.Create(ctx, pr); err != nil { + if !errors.IsAlreadyExists(err) { + log.Error(err, "Failed to create initial PipelineRun") + } + } else { + log.Info("Created initial PipelineRun", "name", pr.GetName()) + } + + // Mark as triggered to avoid creating multiple PipelineRuns + heliosApp.Status.InitialBuildTriggered = true + if err := r.Status().Update(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to update InitialBuildTriggered status") + } + } + } + + // ------------------------------------------------------------------ + // PHASE 1: Render & GitOps (Moved below) + // ------------------------------------------------------------------ + + // 4. GitOps Helper: Get Token & Username + token := os.Getenv("GITHUB_TOKEN") + username := os.Getenv("GITHUB_USER") + if username == "" { + username = "git" // Default fallback + } + + if heliosApp.Spec.GitOpsSecretRef != "" { + var secret corev1.Secret + // Explicitly log the secret lookup attempt + if err := r.Get(ctx, types.NamespacedName{Name: heliosApp.Spec.GitOpsSecretRef, Namespace: heliosApp.Namespace}, &secret); err == nil { + if t, ok := secret.Data["token"]; ok { + token = string(t) + } else if p, ok := secret.Data["password"]; ok { + // Fallback to 'password' key (standard basic-auth secret from Tekton setup) + token = string(p) + } else { + log.Info("Secret found but 'token' or 'password' key is missing", "Secret", heliosApp.Spec.GitOpsSecretRef) + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s missing 'token' key", heliosApp.Spec.GitOpsSecretRef)) + return ctrl.Result{}, nil + } + if u, ok := secret.Data["username"]; ok { + username = string(u) + } + } else { + log.Error(err, "Failed to get GitOps Secret", "Secret", heliosApp.Spec.GitOpsSecretRef) + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s not found", heliosApp.Spec.GitOpsSecretRef)) + return ctrl.Result{}, nil + } + } + + // 5. GitOps Sync + + if token == "" { + err := fmt.Errorf("GitOps token is empty. Check Secret or GITHUB_TOKEN env var") + log.Error(err, "Authentication failed") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, "GitOps token missing") + return ctrl.Result{}, nil // Don't retry immediately if config is missing + } + + // OPTIMIZATION: Check Hash + currentHash := r.computeHash(manifestBytes) + if heliosApp.Status.LastAppliedHash == currentHash { + log.Info("Manifest hash unchanged, skipping GitOps sync", "hash", currentHash) + + // Still ensure status is Ready if it was previously set + if heliosApp.Status.Phase != appv1alpha1.PhaseReady { + heliosApp.Status.Phase = appv1alpha1.PhaseReady + if err := r.Status().Update(ctx, &heliosApp); err != nil { + return ctrl.Result{}, err + } + } + } else { + // Use GitFactory if available, otherwise default to NewGitOpsClient + getGitClient := r.GitFactory + if getGitClient == nil { + getGitClient = func(repo, user, token string) gitops.GitOpsClientInterface { + return gitops.NewGitOpsClient(repo, user, token) + } + } + + gitClient := getGitClient(heliosApp.Spec.GitOpsRepo, username, token) + targetPath := fmt.Sprintf("%s/manifest.yaml", heliosApp.Spec.GitOpsPath) + + if err := gitClient.SyncManifest(ctx, targetPath, string(manifestBytes)); err != nil { + log.Error(err, "GitOps sync failed") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("GitOps failed: %v", err)) + return ctrl.Result{}, err + } + + // 6. Update Status + heliosApp.Status.Phase = appv1alpha1.PhaseReady + heliosApp.Status.Message = fmt.Sprintf("Manifest pushed to %s/%s", heliosApp.Spec.GitOpsRepo, targetPath) + heliosApp.Status.LastAppliedHash = currentHash + // We clear ResourcesCreated as we are not managing them directly anymore + heliosApp.Status.ResourcesCreated = nil + + if err := r.Status().Update(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + log.Info("Successfully reconciled HeliosApp via GitOps", "newHash", currentHash) + } + + // 7. Ensure ArgoCD Application exists + log.Info("Ensuring ArgoCD Application exists") + argoApp, err := GenerateArgoApplication(&heliosApp) + if err != nil { + log.Error(err, "Failed to generate ArgoCD Application manifest") + // We don't return error here to avoid loop if GitOps was successful, just log it. + // Or maybe we should retry? Let's log and continue for now. + } else { + // Define ArgoCD Application identity + argoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) + // We use Sever-Side Apply or Create/Update logic + // Since ArgoCD app is in "argocd" namespace usually, we need permissions there. + // For simplicity/demo: Try to get, if not found create. + + foundArgoApp := &unstructured.Unstructured{} + foundArgoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) + + key := client.ObjectKey{ + Name: argoApp.GetName(), + Namespace: argoApp.GetNamespace(), + } + + if err := r.Client.Get(ctx, key, foundArgoApp); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ArgoCD Application", "name", argoApp.GetName()) + if err := r.Client.Create(ctx, argoApp); err != nil { + log.Error(err, "Failed to create ArgoCD Application") + } + } else { + log.Error(err, "Failed to get ArgoCD Application") + } + } else { + // Optional: Update if needed (checkout 'spec' diff) + log.Info("ArgoCD Application already exists", "name", argoApp.GetName()) + } + } + + // NOTE: Ingress removed - use port-forwarding for EventListener: + // kubectl port-forward svc/el--listener 8080:8080 + + return ctrl.Result{}, nil +} + +// mapCRDToModel converts HeliosApp CRD to CUE Application Model +func (r *HeliosAppReconciler) mapCRDToModel(app *appv1alpha1.HeliosApp) (heliosCue.Application, error) { + components := make([]heliosCue.Component, len(app.Spec.Components)) + + for i, c := range app.Spec.Components { + // Parse properties from RawExtension + var props map[string]any + if c.Properties != nil && c.Properties.Raw != nil { + if err := json.Unmarshal(c.Properties.Raw, &props); err != nil { + return heliosCue.Application{}, fmt.Errorf("failed to parse component properties: %w", err) + } + } + + // Parse traits + traits := make([]heliosCue.Trait, len(c.Traits)) + for j, t := range c.Traits { + var traitProps map[string]any + if t.Properties != nil && t.Properties.Raw != nil { + if err := json.Unmarshal(t.Properties.Raw, &traitProps); err != nil { + return heliosCue.Application{}, fmt.Errorf("failed to parse trait properties: %w", err) + } + } + traits[j] = heliosCue.Trait{ + Type: t.Type, + Properties: traitProps, + } + } + + components[i] = heliosCue.Component{ + Name: c.Name, + Type: c.Type, + Properties: props, + Traits: traits, + } + } + + return heliosCue.Application{ + App: heliosCue.AppSpec{ + Name: app.Name, + Namespace: app.Namespace, + Owner: app.Spec.Owner, + Description: app.Spec.Description, + Components: components, + }, + }, nil +} + +// mapCRDToTektonInput converts HeliosApp CRD to TektonInput for CUE rendering. +// This is the bridge between HeliosApp spec fields and the CUE #TektonInput schema. +func (r *HeliosAppReconciler) mapCRDToTektonInput(app *appv1alpha1.HeliosApp) heliosCue.TektonInput { + input := heliosCue.TektonInput{ + AppName: app.Name, + Namespace: app.Namespace, + GitRepo: app.Spec.GitRepo, + GitBranch: app.Spec.GitBranch, + ImageRepo: app.Spec.ImageRepo, + GitOpsRepo: app.Spec.GitOpsRepo, + GitOpsPath: app.Spec.GitOpsPath, + GitOpsBranch: app.Spec.GitOpsBranch, + GitOpsSecretRef: app.Spec.GitOpsSecretRef, + WebhookDomain: app.Spec.WebhookDomain, + WebhookSecret: app.Spec.WebhookSecret, + PipelineName: app.Spec.PipelineName, + PipelineType: app.Spec.PipelineName, // pipelineType uses same value as pipelineName + TriggerType: "github-push", // Default; extend HeliosAppSpec if needed + ServiceAccount: app.Spec.ServiceAccount, + PVCName: app.Spec.PVCName, + ContextSubpath: app.Spec.ContextSubpath, + Replicas: int(app.Spec.Replicas), + Port: int(app.Spec.Port), + TestCommand: app.Spec.TestCommand, + DockerSecret: "docker-credentials", + ArgoCDNamespace: app.Spec.ArgoCDNamespace, + ArgoCDProject: app.Spec.ArgoCDProject, + } + + // Apply defaults for fields that may be empty + input.GitBranch = cmp.Or(input.GitBranch, "main") + input.GitOpsBranch = cmp.Or(input.GitOpsBranch, "main") + input.GitOpsSecretRef = cmp.Or(input.GitOpsSecretRef, "github-credentials") + input.WebhookSecret = cmp.Or(input.WebhookSecret, "github-webhook-secret") + if input.PipelineName == "" { + input.PipelineName = "from-code-to-cluster" + input.PipelineType = "from-code-to-cluster" + } + input.ServiceAccount = cmp.Or(input.ServiceAccount, "default") + if input.Replicas <= 0 { + input.Replicas = 1 + } + if input.Port <= 0 { + input.Port = 8080 + } + + return input +} + +// reconcileTektonResourcesCue renders Tekton resources via CUE and applies them. +// This is the NEW path that replaces all hardcoded Generate* functions. +func (r *HeliosAppReconciler) reconcileTektonResourcesCue(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + // 1. Map CRD → TektonInput + tektonInput := r.mapCRDToTektonInput(app) + + // 2. Render via CUE + objects, err := r.TektonRenderer.RenderTektonResources(tektonInput) + if err != nil { + return fmt.Errorf("CUE TektonRenderer failed: %w", err) + } + + log.Info("CUE rendered Tekton resources", "count", len(objects)) + + // 3. Apply each rendered resource + for _, obj := range objects { + // Set owner reference (skip cluster-scoped resources) + if obj.GetNamespace() != "" { + if err := ctrl.SetControllerReference(app, obj, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference", "kind", obj.GetKind(), "name", obj.GetName()) + continue + } + } + + // Create or update + found := &unstructured.Unstructured{} + found.SetGroupVersionKind(obj.GroupVersionKind()) + err := r.Client.Get(ctx, client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}, found) + if err != nil { + if errors.IsNotFound(err) { + log.Info("Creating resource", "kind", obj.GetKind(), "name", obj.GetName()) + if err := r.Client.Create(ctx, obj); err != nil { + log.Error(err, "Failed to create resource", "kind", obj.GetKind(), "name", obj.GetName()) + } + } else { + log.Error(err, "Failed to get resource", "kind", obj.GetKind(), "name", obj.GetName()) + } + } else { + // Update existing resource's spec + found.Object["spec"] = obj.Object["spec"] + if err := r.Client.Update(ctx, found); err != nil { + log.Error(err, "Failed to update resource", "kind", obj.GetKind(), "name", obj.GetName()) + } + } + } + + // 4. Also ensure RBAC (SA, RoleBinding, ClusterRoleBinding) — these are not in CUE yet + r.ensureTektonRBAC(ctx, app) + + return nil +} + +// ensureTektonRBAC creates ServiceAccount, RoleBinding, ClusterRoleBinding. +// These are infrastructure resources not managed by CUE (they are cluster lifecycle, not app lifecycle). +func (r *HeliosAppReconciler) ensureTektonRBAC(ctx context.Context, app *appv1alpha1.HeliosApp) { + log := logf.FromContext(ctx) + + sa := GenerateServiceAccount(app.Namespace) + if err := ctrl.SetControllerReference(app, sa, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for ServiceAccount") + } else { + foundSA := &unstructured.Unstructured{} + foundSA.SetGroupVersionKind(sa.GroupVersionKind()) + if err := r.Client.Get(ctx, client.ObjectKey{Name: sa.GetName(), Namespace: sa.GetNamespace()}, foundSA); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ServiceAccount", "name", sa.GetName()) + r.Client.Create(ctx, sa) + } + } + } + + rb := GenerateRoleBinding(app.Namespace) + if err := ctrl.SetControllerReference(app, rb, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for RoleBinding") + } else { + foundRB := &unstructured.Unstructured{} + foundRB.SetGroupVersionKind(rb.GroupVersionKind()) + if err := r.Client.Get(ctx, client.ObjectKey{Name: rb.GetName(), Namespace: rb.GetNamespace()}, foundRB); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating RoleBinding", "name", rb.GetName()) + r.Client.Create(ctx, rb) + } + } + } + + crb := GenerateClusterRoleBinding(app.Namespace) + foundCrb := &unstructured.Unstructured{} + foundCrb.SetGroupVersionKind(crb.GroupVersionKind()) + if err := r.Client.Get(ctx, client.ObjectKey{Name: crb.GetName()}, foundCrb); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ClusterRoleBinding", "name", crb.GetName()) + r.Client.Create(ctx, crb) + } + } +} + +// updateStatus updates the HeliosApp status +func (r *HeliosAppReconciler) updateStatus(ctx context.Context, app *appv1alpha1.HeliosApp, phase appv1alpha1.HeliosAppPhase, message string) { + app.Status.Phase = phase + app.Status.Message = message + if err := r.Status().Update(ctx, app); err != nil { + logf.FromContext(ctx).Error(err, "Failed to update status") + } +} + +// computeHash returns SHA256 of data +func (r *HeliosAppReconciler) computeHash(data []byte) string { + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]) +} + +// SetupWithManager sets up the controller with the Manager +func (r *HeliosAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&appv1alpha1.HeliosApp{}). + Owns(&appsv1.Deployment{}). + Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Service{}). + Owns(&networkingv1.Ingress{}). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret), + ). + Named("heliosapp"). + Complete(r) +} + +// findObjectsForSecret maps Secret changes to HeliosApp reconcile requests. +// This ensures the controller re-reconciles when a referenced secret changes. +func (r *HeliosAppReconciler) findObjectsForSecret(ctx context.Context, obj client.Object) []reconcile.Request { + log := logf.FromContext(ctx) + + // List all HeliosApps in the same namespace + var heliosAppList appv1alpha1.HeliosAppList + if err := r.List(ctx, &heliosAppList, client.InNamespace(obj.GetNamespace())); err != nil { + log.Error(err, "Failed to list HeliosApps for secret watch") + return nil + } + + var requests []reconcile.Request + for _, app := range heliosAppList.Items { + // Check if this app references the changed secret + if app.Spec.GitOpsSecretRef == obj.GetName() || + app.Spec.WebhookSecret == obj.GetName() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: app.Name, + Namespace: app.Namespace, + }, + }) + } + } + + return requests +} +=== +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "cmp" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" + heliosCue "github.com/helios-platform-team/helios-platform/apps/operator/internal/cue" + "github.com/helios-platform-team/helios-platform/apps/operator/internal/gitops" +) + +// HeliosAppReconciler reconciles a HeliosApp object +type HeliosAppReconciler struct { + client.Client + Scheme *runtime.Scheme + CueEngine heliosCue.CueEngineInterface + // TektonRenderer renders Tekton CI/CD resources via CUE engine. + TektonRenderer heliosCue.TektonRendererInterface + // GitFactory allows injecting a custom GitOps client (e.g. for testing) + GitFactory func(string, string, string) gitops.GitOpsClientInterface +} + +// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete + +// Reconcile handles the reconciliation loop for HeliosApp +// Controller does NOT iterate components/traits - all orchestration is in CUE +func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + // 1. Fetch HeliosApp CRD + var heliosApp appv1alpha1.HeliosApp + if err := r.Get(ctx, req.NamespacedName, &heliosApp); err != nil { + if errors.IsNotFound(err) { + log.Info("HeliosApp resource not found, ignoring") + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + log.Info("Reconciling HeliosApp", "name", heliosApp.Name, "namespace", heliosApp.Namespace) + + // 2. Map CRD to Application Model + appModel, err := r.mapCRDToModel(&heliosApp) + if err != nil { + log.Error(err, "Failed to map CRD to application model") + return ctrl.Result{}, err + } + + // 3. Render via CUE Engine + manifestBytes, err := r.CueEngine.Render(appModel) + if err != nil { + log.Error(err, "Failed to render application via CUE") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE rendering failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE -1 & 0: Tekton CI/CD Resources (Tasks, Pipeline, Triggers) + // All Tekton resources are rendered via CUE engine. + // ------------------------------------------------------------------ + if err := r.reconcileTektonResourcesCue(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to reconcile Tekton resources via CUE") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE Tekton rendering failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE 0.5: Database Credential Secrets + // Generate and store secure credentials for components with database traits. + // Secrets are created BEFORE GitOps sync to ensure credentials exist + // when the application is deployed. + // ------------------------------------------------------------------ + if err := r.reconcileDatabaseSecrets(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to reconcile database secrets") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret creation failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE 0.7: Database Instance Provisioning + // Provision StatefulSets and headless Services for database traits. + // Runs AFTER secrets so that the credential Secret already exists + // when the database pod starts. + // ------------------------------------------------------------------ + if err := r.reconcileDatabaseInstance(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to reconcile database instance") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database instance provisioning failed: %v", err)) + return ctrl.Result{}, err + } + + // ------------------------------------------------------------------ + // PHASE 0.9: Inject Database Credentials into Backend Deployment + // Patches the live Deployment (deployed by ArgoCD) to add DB_HOST, + // DB_USER, DB_PASS env vars referencing the operator-managed Secret. + // Runs AFTER secrets and instances so the Secret already exists. + // ------------------------------------------------------------------ + if err := r.reconcileDatabaseSecretInjection(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to inject database secrets into Deployment") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret injection failed: %v", err)) + return ctrl.Result{}, err + } + + // VALIDATION: Ensure image is present (Fix "First Commit Missing Image") + // This validation is for application workloads (GitOps pipeline downstream). + // We run this AFTER DB provisioning so databases can come up while app is building. + for _, comp := range appModel.App.Components { + // We can add more specific checks here based on component type + // For now, checks if 'image' property exists and is not empty for all components + // assuming all workloads need an image. + if img, ok := comp.Properties["image"].(string); !ok || img == "" { + msg := fmt.Sprintf("Component '%s' is waiting for image (likely building). Status: Pending.", comp.Name) + log.Info(msg) + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhasePending, msg) + return ctrl.Result{}, nil // Wait for next update (CI/CD will update CR with image) + } + } + + // ------------------------------------------------------------------ + // PHASE 0.6: Trigger Initial PipelineRun (if not already done) + // ------------------------------------------------------------------ + if !heliosApp.Status.InitialBuildTriggered { + log.Info("Triggering initial PipelineRun for new HeliosApp") + + pipelineName := heliosApp.Spec.PipelineName + if pipelineName == "" { + pipelineName = "from-code-to-cluster" + } + pr, err := GeneratePipelineRunForManifestGeneration(&heliosApp, pipelineName) + if err != nil { + log.Error(err, "Failed to generate initial PipelineRun") + } else { + if err := ctrl.SetControllerReference(&heliosApp, pr, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for PipelineRun") + } + + if err := r.Client.Create(ctx, pr); err != nil { + if !errors.IsAlreadyExists(err) { + log.Error(err, "Failed to create initial PipelineRun") + } + } else { + log.Info("Created initial PipelineRun", "name", pr.GetName()) + } + + // Mark as triggered to avoid creating multiple PipelineRuns + heliosApp.Status.InitialBuildTriggered = true + if err := r.Status().Update(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to update InitialBuildTriggered status") + } + } + } + + // ------------------------------------------------------------------ + // PHASE 1: Render & GitOps (Moved below) + // ------------------------------------------------------------------ + + // 4. GitOps Helper: Get Token & Username + token := os.Getenv("GITHUB_TOKEN") + username := os.Getenv("GITHUB_USER") + if username == "" { + username = "git" // Default fallback + } + + if heliosApp.Spec.GitOpsSecretRef != "" { + var secret corev1.Secret + // Explicitly log the secret lookup attempt + if err := r.Get(ctx, types.NamespacedName{Name: heliosApp.Spec.GitOpsSecretRef, Namespace: heliosApp.Namespace}, &secret); err == nil { + if t, ok := secret.Data["token"]; ok { + token = string(t) + } else if p, ok := secret.Data["password"]; ok { + // Fallback to 'password' key (standard basic-auth secret from Tekton setup) + token = string(p) + } else { + log.Info("Secret found but 'token' or 'password' key is missing", "Secret", heliosApp.Spec.GitOpsSecretRef) + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s missing 'token' key", heliosApp.Spec.GitOpsSecretRef)) + return ctrl.Result{}, nil + } + if u, ok := secret.Data["username"]; ok { + username = string(u) + } + } else { + log.Error(err, "Failed to get GitOps Secret", "Secret", heliosApp.Spec.GitOpsSecretRef) + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s not found", heliosApp.Spec.GitOpsSecretRef)) + return ctrl.Result{}, nil + } + } + + // 5. GitOps Sync + + if token == "" { + err := fmt.Errorf("GitOps token is empty. Check Secret or GITHUB_TOKEN env var") + log.Error(err, "Authentication failed") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, "GitOps token missing") + return ctrl.Result{}, nil // Don't retry immediately if config is missing + } + + // OPTIMIZATION: Check Hash + currentHash := r.computeHash(manifestBytes) + if heliosApp.Status.LastAppliedHash == currentHash { + log.Info("Manifest hash unchanged, skipping GitOps sync", "hash", currentHash) + + // Still ensure status is Ready if it was previously set + if heliosApp.Status.Phase != appv1alpha1.PhaseReady { + heliosApp.Status.Phase = appv1alpha1.PhaseReady + if err := r.Status().Update(ctx, &heliosApp); err != nil { + return ctrl.Result{}, err + } + } + } else { + // Use GitFactory if available, otherwise default to NewGitOpsClient + getGitClient := r.GitFactory + if getGitClient == nil { + getGitClient = func(repo, user, token string) gitops.GitOpsClientInterface { + return gitops.NewGitOpsClient(repo, user, token) + } + } + + gitClient := getGitClient(heliosApp.Spec.GitOpsRepo, username, token) + targetPath := fmt.Sprintf("%s/manifest.yaml", heliosApp.Spec.GitOpsPath) + + if err := gitClient.SyncManifest(ctx, targetPath, string(manifestBytes)); err != nil { + log.Error(err, "GitOps sync failed") + r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("GitOps failed: %v", err)) + return ctrl.Result{}, err + } + + // 6. Update Status + heliosApp.Status.Phase = appv1alpha1.PhaseReady + heliosApp.Status.Message = fmt.Sprintf("Manifest pushed to %s/%s", heliosApp.Spec.GitOpsRepo, targetPath) + heliosApp.Status.LastAppliedHash = currentHash + // We clear ResourcesCreated as we are not managing them directly anymore + heliosApp.Status.ResourcesCreated = nil + + if err := r.Status().Update(ctx, &heliosApp); err != nil { + log.Error(err, "Failed to update status") + return ctrl.Result{}, err + } + log.Info("Successfully reconciled HeliosApp via GitOps", "newHash", currentHash) + } + + // 7. Ensure ArgoCD Application exists + log.Info("Ensuring ArgoCD Application exists") + argoApp, err := GenerateArgoApplication(&heliosApp) + if err != nil { + log.Error(err, "Failed to generate ArgoCD Application manifest") + // We don't return error here to avoid loop if GitOps was successful, just log it. + // Or maybe we should retry? Let's log and continue for now. + } else { + // Define ArgoCD Application identity + argoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) + // We use Sever-Side Apply or Create/Update logic + // Since ArgoCD app is in "argocd" namespace usually, we need permissions there. + // For simplicity/demo: Try to get, if not found create. + + foundArgoApp := &unstructured.Unstructured{} + foundArgoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) + + key := client.ObjectKey{ + Name: argoApp.GetName(), + Namespace: argoApp.GetNamespace(), + } + + if err := r.Client.Get(ctx, key, foundArgoApp); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ArgoCD Application", "name", argoApp.GetName()) + if err := r.Client.Create(ctx, argoApp); err != nil { + log.Error(err, "Failed to create ArgoCD Application") + } + } else { + log.Error(err, "Failed to get ArgoCD Application") + } + } else { + // Optional: Update if needed (checkout 'spec' diff) + log.Info("ArgoCD Application already exists", "name", argoApp.GetName()) + } + } + + // NOTE: Ingress removed - use port-forwarding for EventListener: + // kubectl port-forward svc/el--listener 8080:8080 + + return ctrl.Result{}, nil +} + +// mapCRDToModel converts HeliosApp CRD to CUE Application Model +func (r *HeliosAppReconciler) mapCRDToModel(app *appv1alpha1.HeliosApp) (heliosCue.Application, error) { + components := make([]heliosCue.Component, len(app.Spec.Components)) + + for i, c := range app.Spec.Components { + // Parse properties from RawExtension + var props map[string]any + if c.Properties != nil && c.Properties.Raw != nil { + if err := json.Unmarshal(c.Properties.Raw, &props); err != nil { + return heliosCue.Application{}, fmt.Errorf("failed to parse component properties: %w", err) + } + } + + // Parse traits + traits := make([]heliosCue.Trait, len(c.Traits)) + for j, t := range c.Traits { + var traitProps map[string]any + if t.Properties != nil && t.Properties.Raw != nil { + if err := json.Unmarshal(t.Properties.Raw, &traitProps); err != nil { + return heliosCue.Application{}, fmt.Errorf("failed to parse trait properties: %w", err) + } + } + traits[j] = heliosCue.Trait{ + Type: t.Type, + Properties: traitProps, + } + } + + components[i] = heliosCue.Component{ + Name: c.Name, + Type: c.Type, + Properties: props, + Traits: traits, + } + } + + return heliosCue.Application{ + App: heliosCue.AppSpec{ + Name: app.Name, + Namespace: app.Namespace, + Owner: app.Spec.Owner, + Description: app.Spec.Description, + Components: components, + }, + }, nil +} + +// mapCRDToTektonInput converts HeliosApp CRD to TektonInput for CUE rendering. +// This is the bridge between HeliosApp spec fields and the CUE #TektonInput schema. +func (r *HeliosAppReconciler) mapCRDToTektonInput(app *appv1alpha1.HeliosApp) heliosCue.TektonInput { + input := heliosCue.TektonInput{ + AppName: app.Name, + Namespace: app.Namespace, + GitRepo: app.Spec.GitRepo, + GitBranch: app.Spec.GitBranch, + ImageRepo: app.Spec.ImageRepo, + GitOpsRepo: app.Spec.GitOpsRepo, + GitOpsPath: app.Spec.GitOpsPath, + GitOpsBranch: app.Spec.GitOpsBranch, + GitOpsSecretRef: app.Spec.GitOpsSecretRef, + WebhookDomain: app.Spec.WebhookDomain, + WebhookSecret: app.Spec.WebhookSecret, + PipelineName: app.Spec.PipelineName, + PipelineType: app.Spec.PipelineName, // pipelineType uses same value as pipelineName + TriggerType: "github-push", // Default; extend HeliosAppSpec if needed + ServiceAccount: app.Spec.ServiceAccount, + PVCName: app.Spec.PVCName, + ContextSubpath: app.Spec.ContextSubpath, + Replicas: int(app.Spec.Replicas), + Port: int(app.Spec.Port), + TestCommand: app.Spec.TestCommand, + DockerSecret: "docker-credentials", + ArgoCDNamespace: app.Spec.ArgoCDNamespace, + ArgoCDProject: app.Spec.ArgoCDProject, + } + + // Apply defaults for fields that may be empty + input.GitBranch = cmp.Or(input.GitBranch, "main") + input.GitOpsBranch = cmp.Or(input.GitOpsBranch, "main") + input.GitOpsSecretRef = cmp.Or(input.GitOpsSecretRef, "github-credentials") + input.WebhookSecret = cmp.Or(input.WebhookSecret, "github-webhook-secret") + if input.PipelineName == "" { + input.PipelineName = "from-code-to-cluster" + input.PipelineType = "from-code-to-cluster" + } + input.ServiceAccount = cmp.Or(input.ServiceAccount, "default") + if input.Replicas <= 0 { + input.Replicas = 1 + } + if input.Port <= 0 { + input.Port = 8080 + } + + return input +} + +// reconcileTektonResourcesCue renders Tekton resources via CUE and applies them. +// This is the NEW path that replaces all hardcoded Generate* functions. +func (r *HeliosAppReconciler) reconcileTektonResourcesCue(ctx context.Context, app *appv1alpha1.HeliosApp) error { + log := logf.FromContext(ctx) + + // 1. Map CRD → TektonInput + tektonInput := r.mapCRDToTektonInput(app) + + // 2. Render via CUE + objects, err := r.TektonRenderer.RenderTektonResources(tektonInput) + if err != nil { + return fmt.Errorf("CUE TektonRenderer failed: %w", err) + } + + log.Info("CUE rendered Tekton resources", "count", len(objects)) + + // 3. Apply each rendered resource + for _, obj := range objects { + // Set owner reference (skip cluster-scoped resources) + if obj.GetNamespace() != "" { + if err := ctrl.SetControllerReference(app, obj, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference", "kind", obj.GetKind(), "name", obj.GetName()) + continue + } + } + + // Create or update + found := &unstructured.Unstructured{} + found.SetGroupVersionKind(obj.GroupVersionKind()) + err := r.Client.Get(ctx, client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}, found) + if err != nil { + if errors.IsNotFound(err) { + log.Info("Creating resource", "kind", obj.GetKind(), "name", obj.GetName()) + if err := r.Client.Create(ctx, obj); err != nil { + log.Error(err, "Failed to create resource", "kind", obj.GetKind(), "name", obj.GetName()) + } + } else { + log.Error(err, "Failed to get resource", "kind", obj.GetKind(), "name", obj.GetName()) + } + } else { + // Update existing resource's spec + found.Object["spec"] = obj.Object["spec"] + if err := r.Client.Update(ctx, found); err != nil { + log.Error(err, "Failed to update resource", "kind", obj.GetKind(), "name", obj.GetName()) + } + } + } + + // 4. Also ensure RBAC (SA, RoleBinding, ClusterRoleBinding) — these are not in CUE yet + r.ensureTektonRBAC(ctx, app) + + return nil +} + +// ensureTektonRBAC creates ServiceAccount, RoleBinding, ClusterRoleBinding. +// These are infrastructure resources not managed by CUE (they are cluster lifecycle, not app lifecycle). +func (r *HeliosAppReconciler) ensureTektonRBAC(ctx context.Context, app *appv1alpha1.HeliosApp) { + log := logf.FromContext(ctx) + + sa := GenerateServiceAccount(app.Namespace) + if err := ctrl.SetControllerReference(app, sa, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for ServiceAccount") + } else { + foundSA := &unstructured.Unstructured{} + foundSA.SetGroupVersionKind(sa.GroupVersionKind()) + if err := r.Client.Get(ctx, client.ObjectKey{Name: sa.GetName(), Namespace: sa.GetNamespace()}, foundSA); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ServiceAccount", "name", sa.GetName()) + r.Client.Create(ctx, sa) + } + } + } + + rb := GenerateRoleBinding(app.Namespace) + if err := ctrl.SetControllerReference(app, rb, r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference for RoleBinding") + } else { + foundRB := &unstructured.Unstructured{} + foundRB.SetGroupVersionKind(rb.GroupVersionKind()) + if err := r.Client.Get(ctx, client.ObjectKey{Name: rb.GetName(), Namespace: rb.GetNamespace()}, foundRB); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating RoleBinding", "name", rb.GetName()) + r.Client.Create(ctx, rb) + } + } + } + + crb := GenerateClusterRoleBinding(app.Namespace) + foundCrb := &unstructured.Unstructured{} + foundCrb.SetGroupVersionKind(crb.GroupVersionKind()) + if err := r.Client.Get(ctx, client.ObjectKey{Name: crb.GetName()}, foundCrb); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ClusterRoleBinding", "name", crb.GetName()) + r.Client.Create(ctx, crb) + } + } +} + +// updateStatus updates the HeliosApp status +func (r *HeliosAppReconciler) updateStatus(ctx context.Context, app *appv1alpha1.HeliosApp, phase appv1alpha1.HeliosAppPhase, message string) { + app.Status.Phase = phase + app.Status.Message = message + if err := r.Status().Update(ctx, app); err != nil { + logf.FromContext(ctx).Error(err, "Failed to update status") + } +} + +// computeHash returns SHA256 of data +func (r *HeliosAppReconciler) computeHash(data []byte) string { + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]) +} + +// SetupWithManager sets up the controller with the Manager +func (r *HeliosAppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&appv1alpha1.HeliosApp{}). + Owns(&appsv1.Deployment{}). + Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Service{}). + Owns(&networkingv1.Ingress{}). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret), + ). + Named("heliosapp"). + Complete(r) +} + +// findObjectsForSecret maps Secret changes to HeliosApp reconcile requests. +// This ensures the controller re-reconciles when a referenced secret changes. +func (r *HeliosAppReconciler) findObjectsForSecret(ctx context.Context, obj client.Object) []reconcile.Request { + log := logf.FromContext(ctx) + + // List all HeliosApps in the same namespace + var heliosAppList appv1alpha1.HeliosAppList + if err := r.List(ctx, &heliosAppList, client.InNamespace(obj.GetNamespace())); err != nil { + log.Error(err, "Failed to list HeliosApps for secret watch") + return nil + } + + var requests []reconcile.Request + for _, app := range heliosAppList.Items { + // Check if this app references the changed secret + if app.Spec.GitOpsSecretRef == obj.GetName() || + app.Spec.WebhookSecret == obj.GetName() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: app.Name, + Namespace: app.Namespace, + }, + }) + } + } + + return requests +} +``` + +#### [database_resources_test.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources_test.go) + +6 new tests: +| Test | Description | +|------|-------------| +| `TestInjectDatabaseEnvVars/InjectsAllEnvVars` | Verifies DB_HOST, DB_USER, DB_PASS injected with correct secretKeyRef | +| `TestInjectDatabaseEnvVars/Idempotent` | Running twice doesn't duplicate env vars | +| `TestInjectDatabaseEnvVars/NoContainers` | Handles edge case of empty container list | +| `TestReconcileDatabaseSecretInjection/InjectsIntoExistingDeployment` | Full reconciliation with fake K8s client | +| `TestReconcileDatabaseSecretInjection/SkipsWhenNoTraits` | No-op when no database traits | +| `TestReconcileDatabaseSecretInjection/DeploymentNotFound_GracefulSkip` | Graceful skip when ArgoCD hasn't deployed yet | + +--- + +### 2. NestJS + Prisma Template + +Created [nestjs-prisma-template/](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template) with: + +| File | Purpose | +|------|---------| +| [template.yaml](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/template/template.yaml) | Backstage scaffolder template with DatabasePicker | +| [content/source/package.json](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/advanced-template/content/source/package.json) | NestJS v11+ & Prisma v6+ dependencies | +| [content/source/prisma/schema.prisma](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma) | Prisma schema using `env("DATABASE_URL")` | +| [content/source/src/prisma/prisma.service.ts](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.service.ts) | **Key file** — constructs `DATABASE_URL` from `DB_HOST`, `DB_USER`, `DB_PASS` with `encodeURIComponent` | +| [content/source/src/prisma/prisma.module.ts](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.module.ts) | Global module exporting PrismaService | +| [content/source/Dockerfile](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/advanced-template/content/source/Dockerfile) | Multi-stage build, runs `prisma migrate deploy` at startup | +| [content/gitops/helios-app.yaml](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/gitops/helios-app.yaml) | HeliosApp CRD with database trait | + +--- + +## Verification Results + +| Check | Result | +|-------|--------| +| `go build ./...` | ✅ Passes | +| `go vet ./...` | ✅ No issues | +| `make test` | ✅ All tests pass (68% controller coverage) | +| `make build` | ✅ Operator binary builds | From fac30e548ac644be78f4110bc50ae4e23267bba3 Mon Sep 17 00:00:00 2001 From: Ho Phuoc Hoan Date: Sun, 15 Mar 2026 17:31:10 +0700 Subject: [PATCH 2/2] fix: address PR review feedback and modernize Go to 1.26.1 PR review fixes: - Validate existing env var sources in InjectDatabaseEnvVars, not just names - Revert accidental local-dev image tag in kustomization.yaml - Requeue when Deployment is missing for database secret injection - Add ArgoCD ignoreDifferences to prevent self-heal reverting injected env vars - Fix broken NestJS template main.ts syntax - Fix catalog-info.yaml referencing undefined template variables - Remove committed planning docs (task.md, implementation_plan.md, walkthrough.md) Go 1.26.1 modernization: - Update Dockerfile and devcontainer from golang:1.24 to golang:1.26 - Use t.Context() instead of context.Background() in tests - Use for-range over integers instead of C-style loops - Use strings.SplitSeq instead of strings.Split in for-range - Update Go version references in documentation Co-Authored-By: Claude Opus 4.6 --- apps/operator/.devcontainer/devcontainer.json | 2 +- apps/operator/Dockerfile | 2 +- apps/operator/README.md | 2 +- .../config/manager/kustomization.yaml | 2 - .../internal/controller/argocd_resources.go | 11 + .../internal/controller/database_resources.go | 80 +- .../controller/database_resources_test.go | 97 +- .../controller/heliosapp_controller.go | 11 +- .../heliosapp_controller_unit_test.go | 6 +- apps/operator/test/utils/utils.go | 3 +- .../content/source/catalog-info.yaml | 2 +- .../content/source/src/main.ts | 7 +- docs/OPERATOR.md | 2 +- docs/SETUP.md | 2 +- implementation_plan.md | 115 - task.md | 28 - walkthrough.md | 2596 ----------------- 17 files changed, 170 insertions(+), 2798 deletions(-) delete mode 100644 implementation_plan.md delete mode 100644 task.md delete mode 100644 walkthrough.md diff --git a/apps/operator/.devcontainer/devcontainer.json b/apps/operator/.devcontainer/devcontainer.json index a3ab754..33ca8bc 100644 --- a/apps/operator/.devcontainer/devcontainer.json +++ b/apps/operator/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "golang:1.24", + "image": "golang:1.26", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/devcontainers/features/git:1": {} diff --git a/apps/operator/Dockerfile b/apps/operator/Dockerfile index cb1b130..86e798a 100644 --- a/apps/operator/Dockerfile +++ b/apps/operator/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24 AS builder +FROM golang:1.26 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/apps/operator/README.md b/apps/operator/README.md index a31c09a..cf6c79f 100644 --- a/apps/operator/README.md +++ b/apps/operator/README.md @@ -8,7 +8,7 @@ See [GIT_OPS_GUIDE.md](GIT_OPS_GUIDE.md) for detailed GitOps setup instructions. ### Prerequisites -- go version v1.24.0+ +- go version v1.26.0+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. diff --git a/apps/operator/config/manager/kustomization.yaml b/apps/operator/config/manager/kustomization.yaml index f919b93..9215473 100644 --- a/apps/operator/config/manager/kustomization.yaml +++ b/apps/operator/config/manager/kustomization.yaml @@ -4,5 +4,3 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: helios-operator - newTag: local diff --git a/apps/operator/internal/controller/argocd_resources.go b/apps/operator/internal/controller/argocd_resources.go index 5aca144..9e67943 100644 --- a/apps/operator/internal/controller/argocd_resources.go +++ b/apps/operator/internal/controller/argocd_resources.go @@ -45,6 +45,17 @@ func GenerateArgoApplication(heliosApp *appv1alpha1.HeliosApp) (*unstructured.Un "CreateNamespace=true", }, }, + // Ignore env var diffs on Deployments so that ArgoCD self-heal + // does not revert the DB_* env vars injected by the operator. + "ignoreDifferences": []any{ + map[string]any{ + "group": "apps", + "kind": "Deployment", + "jsonPointers": []any{ + "/spec/template/spec/containers/0/env", + }, + }, + }, }, } diff --git a/apps/operator/internal/controller/database_resources.go b/apps/operator/internal/controller/database_resources.go index e9f81c8..7712846 100644 --- a/apps/operator/internal/controller/database_resources.go +++ b/apps/operator/internal/controller/database_resources.go @@ -86,7 +86,7 @@ func GenerateSecurePassword(length int) (string, error) { password := make([]byte, length) charsetLen := big.NewInt(int64(len(PasswordCharset))) - for i := 0; i < length; i++ { + for i := range length { idx, err := rand.Int(rand.Reader, charsetLen) if err != nil { return "", fmt.Errorf("failed to generate random index: %w", err) @@ -459,6 +459,8 @@ var databaseEnvVarNames = []string{"DB_HOST", "DB_USER", "DB_PASS"} // DB_HOST, DB_USER, DB_PASS env vars referencing the given K8s Secret. // The function is idempotent — if the env vars already exist with the correct // secretKeyRef, no changes are made and it returns false. +// If an env var exists but points to a different source (wrong secret, plain +// value, etc.), it is updated to the expected secretKeyRef. func InjectDatabaseEnvVars(deploy *appsv1.Deployment, secretName string) bool { if len(deploy.Spec.Template.Spec.Containers) == 0 { return false @@ -466,29 +468,46 @@ func InjectDatabaseEnvVars(deploy *appsv1.Deployment, secretName string) bool { container := &deploy.Spec.Template.Spec.Containers[0] - // Build a set of existing env var names for fast lookup. - existingEnvs := make(map[string]bool, len(container.Env)) - for _, ev := range container.Env { - existingEnvs[ev.Name] = true - } - changed := false for _, envName := range databaseEnvVarNames { - if existingEnvs[envName] { - continue - } - container.Env = append(container.Env, corev1.EnvVar{ - Name: envName, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: envName, + expectedRef := &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, }, + Key: envName, }, - }) - changed = true + } + + // Look for an existing env var with this name. + found := false + for i := range container.Env { + if container.Env[i].Name != envName { + continue + } + found = true + // If it already references the correct Secret/key, leave it as-is. + if container.Env[i].ValueFrom != nil && + container.Env[i].ValueFrom.SecretKeyRef != nil && + container.Env[i].ValueFrom.SecretKeyRef.Name == secretName && + container.Env[i].ValueFrom.SecretKeyRef.Key == envName { + break + } + // Otherwise, update it to use the expected secretKeyRef. + container.Env[i].Value = "" + container.Env[i].ValueFrom = expectedRef + changed = true + break + } + + // If no env var with this name exists, append a new one. + if !found { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envName, + ValueFrom: expectedRef, + }) + changed = true + } } return changed @@ -498,15 +517,21 @@ func InjectDatabaseEnvVars(deploy *appsv1.Deployment, secretName string) bool { // to inject DB_HOST, DB_USER, DB_PASS env vars from the operator-managed Secret. // This runs AFTER database secrets and instances are provisioned so that the // Secret already exists when the Deployment references it. -func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Context, app *appv1alpha1.HeliosApp) error { +// +// Returns (pendingInjection, error). pendingInjection is true when one or more +// target Deployments do not exist yet (e.g. ArgoCD hasn't synced), signalling +// the caller to requeue so injection is retried. +func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Context, app *appv1alpha1.HeliosApp) (bool, error) { log := logf.FromContext(ctx) dbTraits := ExtractDatabaseTraits(app) if len(dbTraits) == 0 { log.V(1).Info("No database traits found, skipping secret injection") - return nil + return false, nil } + pendingInjection := false + for _, dbTrait := range dbTraits { secretName := GetDatabaseSecretName(dbTrait.ComponentName) deployName := dbTrait.ComponentName @@ -520,13 +545,14 @@ func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Conte if err != nil { if errors.IsNotFound(err) { // Deployment may not exist yet (ArgoCD hasn't synced). - // This is expected — we'll inject on the next reconcile. - log.Info("Deployment not found yet, will inject on next reconcile", + // Signal the caller to requeue so we retry injection later. + log.Info("Deployment not found yet, will requeue for injection", "component", dbTrait.ComponentName, "deployment", deployName) + pendingInjection = true continue } - return fmt.Errorf("failed to get Deployment %s: %w", deployName, err) + return false, fmt.Errorf("failed to get Deployment %s: %w", deployName, err) } // Inject env vars if not already present. @@ -538,7 +564,7 @@ func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Conte } if err := r.Update(ctx, deploy); err != nil { - return fmt.Errorf("failed to update Deployment %s with database env vars: %w", deployName, err) + return false, fmt.Errorf("failed to update Deployment %s with database env vars: %w", deployName, err) } log.Info("Successfully injected database env vars into Deployment", @@ -547,7 +573,7 @@ func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Conte "secret", secretName) } - return nil + return pendingInjection, nil } // GenerateDatabaseStatefulSet creates a StatefulSet for a Postgres database instance. diff --git a/apps/operator/internal/controller/database_resources_test.go b/apps/operator/internal/controller/database_resources_test.go index cb6fde1..9d2a29d 100644 --- a/apps/operator/internal/controller/database_resources_test.go +++ b/apps/operator/internal/controller/database_resources_test.go @@ -1,7 +1,6 @@ package controller import ( - "context" "encoding/json" "strings" "testing" @@ -138,7 +137,7 @@ func TestGenerateCredentialsUniqueness(t *testing.T) { credentials := make(map[string]bool) iterations := 100 - for i := 0; i < iterations; i++ { + for i := range iterations { creds, err := GenerateCredentials() if err != nil { t.Fatalf("GenerateCredentials failed on iteration %d: %v", i, err) @@ -363,7 +362,7 @@ func TestReconcileDatabaseSecrets(t *testing.T) { Scheme: scheme, } - ctx := context.Background() + ctx := t.Context() err := r.reconcileDatabaseSecrets(ctx, app) if err != nil { t.Fatalf("reconcileDatabaseSecrets failed: %v", err) @@ -414,7 +413,7 @@ func TestReconcileDatabaseSecrets(t *testing.T) { Scheme: scheme, } - ctx := context.Background() + ctx := t.Context() err := r.reconcileDatabaseSecrets(ctx, app) if err != nil { t.Fatalf("reconcileDatabaseSecrets failed: %v", err) @@ -471,7 +470,7 @@ func TestReconcileDatabaseSecrets(t *testing.T) { Scheme: scheme, } - ctx := context.Background() + ctx := t.Context() err := r.reconcileDatabaseSecrets(ctx, appWithoutDB) if err != nil { t.Fatalf("reconcileDatabaseSecrets should not fail for app without database traits: %v", err) @@ -786,7 +785,7 @@ func TestReconcileDatabaseInstance(t *testing.T) { Scheme: fullScheme, } - ctx := context.Background() + ctx := t.Context() err := r.reconcileDatabaseInstance(ctx, app) if err != nil { t.Fatalf("reconcileDatabaseInstance failed: %v", err) @@ -868,7 +867,7 @@ func TestReconcileDatabaseInstance(t *testing.T) { Scheme: fullScheme, } - ctx := context.Background() + ctx := t.Context() err := r.reconcileDatabaseInstance(ctx, appWithoutDB) if err != nil { t.Fatalf("reconcileDatabaseInstance should not fail for app without database traits: %v", err) @@ -934,7 +933,7 @@ func TestReconcileDatabaseInstance(t *testing.T) { Scheme: fullScheme, } - ctx := context.Background() + ctx := t.Context() err := r.reconcileDatabaseInstance(ctx, appWithRedis) if err != nil { t.Fatalf("reconcileDatabaseInstance should not fail for redis type: %v", err) @@ -1038,6 +1037,67 @@ func TestInjectDatabaseEnvVars(t *testing.T) { } }) + t.Run("UpdatesExistingWrongSource", func(t *testing.T) { + deploy := &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "api-server", + Image: "myregistry/api:v1", + Env: []corev1.EnvVar{ + {Name: "PORT", Value: "3000"}, + {Name: "DB_HOST", Value: "hardcoded-host"}, + {Name: "DB_USER", ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wrong-secret"}, + Key: "DB_USER", + }, + }}, + }, + }, + }, + }, + }, + }, + } + + changed := InjectDatabaseEnvVars(deploy, "api-server-db-secret") + if !changed { + t.Fatal("Expected InjectDatabaseEnvVars to return true when existing env vars have wrong source") + } + + container := deploy.Spec.Template.Spec.Containers[0] + // Should have PORT + DB_HOST + DB_USER + DB_PASS = 4 + if len(container.Env) != 4 { + t.Fatalf("Expected 4 env vars, got %d", len(container.Env)) + } + + // DB_HOST should now reference the secret, not a plain value + for _, env := range container.Env { + if env.Name == "DB_HOST" { + if env.Value != "" { + t.Error("DB_HOST should have Value cleared") + } + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatal("DB_HOST should reference a secret") + } + if env.ValueFrom.SecretKeyRef.Name != "api-server-db-secret" { + t.Errorf("DB_HOST: expected secret name %q, got %q", "api-server-db-secret", env.ValueFrom.SecretKeyRef.Name) + } + } + if env.Name == "DB_USER" { + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatal("DB_USER should reference a secret") + } + if env.ValueFrom.SecretKeyRef.Name != "api-server-db-secret" { + t.Errorf("DB_USER: expected secret name %q, got %q", "api-server-db-secret", env.ValueFrom.SecretKeyRef.Name) + } + } + } + }) + t.Run("NoContainers", func(t *testing.T) { deploy := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ @@ -1132,11 +1192,14 @@ func TestReconcileDatabaseSecretInjection(t *testing.T) { Scheme: fullScheme, } - ctx := context.Background() - err := r.reconcileDatabaseSecretInjection(ctx, app) + ctx := t.Context() + pending, err := r.reconcileDatabaseSecretInjection(ctx, app) if err != nil { t.Fatalf("reconcileDatabaseSecretInjection failed: %v", err) } + if pending { + t.Error("Expected no pending injection when Deployment exists") + } // Verify the Deployment was updated with DB env vars updatedDeploy := &appsv1.Deployment{} @@ -1204,11 +1267,14 @@ func TestReconcileDatabaseSecretInjection(t *testing.T) { Scheme: fullScheme, } - ctx := context.Background() - err := r.reconcileDatabaseSecretInjection(ctx, appWithoutDB) + ctx := t.Context() + pending, err := r.reconcileDatabaseSecretInjection(ctx, appWithoutDB) if err != nil { t.Fatalf("reconcileDatabaseSecretInjection should not fail for app without database traits: %v", err) } + if pending { + t.Error("Expected no pending injection for app without database traits") + } }) t.Run("DeploymentNotFound_GracefulSkip", func(t *testing.T) { @@ -1229,10 +1295,13 @@ func TestReconcileDatabaseSecretInjection(t *testing.T) { Scheme: fullScheme, } - ctx := context.Background() - err := r.reconcileDatabaseSecretInjection(ctx, app) + ctx := t.Context() + pending, err := r.reconcileDatabaseSecretInjection(ctx, app) if err != nil { t.Fatalf("reconcileDatabaseSecretInjection should not fail when Deployment is missing: %v", err) } + if !pending { + t.Error("Expected pending=true when Deployment is missing") + } }) } diff --git a/apps/operator/internal/controller/heliosapp_controller.go b/apps/operator/internal/controller/heliosapp_controller.go index 01be8f8..a7fd738 100644 --- a/apps/operator/internal/controller/heliosapp_controller.go +++ b/apps/operator/internal/controller/heliosapp_controller.go @@ -24,6 +24,7 @@ import ( "encoding/json" "fmt" "os" + "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -133,7 +134,8 @@ func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // DB_USER, DB_PASS env vars referencing the operator-managed Secret. // Runs AFTER secrets and instances so the Secret already exists. // ------------------------------------------------------------------ - if err := r.reconcileDatabaseSecretInjection(ctx, &heliosApp); err != nil { + dbInjectionPending, err := r.reconcileDatabaseSecretInjection(ctx, &heliosApp) + if err != nil { log.Error(err, "Failed to inject database secrets into Deployment") r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret injection failed: %v", err)) return ctrl.Result{}, err @@ -316,6 +318,13 @@ func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // NOTE: Ingress removed - use port-forwarding for EventListener: // kubectl port-forward svc/el--listener 8080:8080 + // If database secret injection is pending (Deployment not yet created by + // ArgoCD), requeue so the operator retries once the Deployment appears. + if dbInjectionPending { + log.Info("Database secret injection pending, requeuing") + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + return ctrl.Result{}, nil } diff --git a/apps/operator/internal/controller/heliosapp_controller_unit_test.go b/apps/operator/internal/controller/heliosapp_controller_unit_test.go index 5a52716..133e6d9 100644 --- a/apps/operator/internal/controller/heliosapp_controller_unit_test.go +++ b/apps/operator/internal/controller/heliosapp_controller_unit_test.go @@ -119,7 +119,7 @@ func TestHeliosAppReconciler_Reconcile_Success(t *testing.T) { }, } - ctx := context.Background() + ctx := t.Context() res, err := r.Reconcile(ctx, req) // 7. Assertions @@ -180,7 +180,7 @@ func TestHeliosAppReconciler_Reconcile_PendingImage(t *testing.T) { TektonRenderer: &FakeTektonRenderer{}, } - res, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: "pending-app", Namespace: "default"}}) + res, err := r.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Name: "pending-app", Namespace: "default"}}) if err != nil { t.Errorf("Reconcile() error = %v, wantErr %v", err, nil) @@ -190,7 +190,7 @@ func TestHeliosAppReconciler_Reconcile_PendingImage(t *testing.T) { } updatedApp := &appv1alpha1.HeliosApp{} - _ = client.Get(context.Background(), types.NamespacedName{Name: "pending-app", Namespace: "default"}, updatedApp) + _ = client.Get(t.Context(), types.NamespacedName{Name: "pending-app", Namespace: "default"}, updatedApp) if updatedApp.Status.Phase != appv1alpha1.PhasePending { t.Errorf("Expected Phase to be %s, got %s", appv1alpha1.PhasePending, updatedApp.Status.Phase) } diff --git a/apps/operator/test/utils/utils.go b/apps/operator/test/utils/utils.go index 448055b..45b7f65 100644 --- a/apps/operator/test/utils/utils.go +++ b/apps/operator/test/utils/utils.go @@ -181,8 +181,7 @@ func LoadImageToKindClusterWithName(name string) error { // according to line breakers, and ignores the empty elements in it. func GetNonEmptyLines(output string) []string { var res []string - elements := strings.Split(output, "\n") - for _, element := range elements { + for element := range strings.SplitSeq(output, "\n") { if element != "" { res = append(res, element) } diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml b/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml index 5185c92..7212d13 100644 --- a/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml +++ b/apps/portal/examples/nestjs-prisma-template/content/source/catalog-info.yaml @@ -4,7 +4,7 @@ metadata: name: ${{ values.name }} description: ${{ values.description }} annotations: - github.com/project-slug: ${{ values.destination.owner + "/" + values.destination.repo }} + github.com/project-slug: ${{ values.owner }}/${{ values.name }} backstage.io/techdocs-ref: dir:. spec: type: service diff --git a/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts b/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts index 37a6af9..2477e89 100644 --- a/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts +++ b/apps/portal/examples/nestjs-prisma-template/content/source/src/main.ts @@ -4,10 +4,9 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - const port = process.env.PORT || ${{ values.port } -}; -await app.listen(port); + const port = process.env.PORT || ${{ values.port }}; + await app.listen(port); -console.log(`🚀 Application is running on port ${port}`); + console.log(`Application is running on port ${port}`); } bootstrap(); diff --git a/docs/OPERATOR.md b/docs/OPERATOR.md index fc7fb04..7063ea1 100644 --- a/docs/OPERATOR.md +++ b/docs/OPERATOR.md @@ -12,7 +12,7 @@ The **Helios Operator** is the core control plane component of the Helios Platfo To develop or run the operator, you need: -- **Go**: v1.24.0+ +- **Go**: v1.26.0+ - **Docker**: 17.03+ - **kubectl**: v1.11.3+ - **Kubernetes Cluster**: Access to a `v1.11.3+` cluster (e.g., Kind, Minikube, or EKS). diff --git a/docs/SETUP.md b/docs/SETUP.md index 2af996e..53d98a5 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -6,7 +6,7 @@ This guide explains how to install necessary development tools for the Helios Pl Ensure you have the following installed: -- **Go**: v1.24.0+ +- **Go**: v1.26.0+ - **Docker**: 17.03+ - **kubectl**: v1.11.3+ diff --git a/implementation_plan.md b/implementation_plan.md deleted file mode 100644 index 7408765..0000000 --- a/implementation_plan.md +++ /dev/null @@ -1,115 +0,0 @@ -# Automated Secret Injection (#39) - -Inject database credentials (DB_HOST, DB_USER, DB_PASS) from the Operator-generated K8s Secret into the Backend Pod's env block, and create a NestJS + Prisma Node.js template that connects using these injected env vars. - -## Proposed Changes - -### Operator — Secret Injection into Backend Deployment - -The CUE engine renders a Deployment for each component (via GitOps sync). After the GitOps sync pushes the manifest, ArgoCD deploys it. However, the Deployment currently has **no database env vars** — the CUE `#Deployment` base only supports simple `{name, value}` env vars, not `secretKeyRef`. - -**Approach**: Add a new reconciliation phase (**Phase 0.9**) in [heliosapp_controller.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/heliosapp_controller.go) that **patches the live Deployment** (deployed by ArgoCD) to inject `DB_HOST`, `DB_USER`, and `DB_PASS` as `envFrom` / env vars referencing the K8s Secret. This runs AFTER database secrets and instances are created. - -> [!IMPORTANT] -> The CUE engine generates the base Deployment manifest (pushed via GitOps). The Go operator patches the **live Deployment** in-cluster to add secret env vars. This keeps secrets out of the GitOps repo entirely. - -#### [MODIFY] [database_resources.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go) - -Add a new function `InjectDatabaseEnvVars` that: -- Takes a Deployment and a database secret name -- Adds `DB_HOST`, `DB_USER`, `DB_PASS` env vars to the first container using `valueFrom.secretKeyRef` -- Also adds `DATABASE_URL` as a convenience env var for Prisma ORM (constructed from the other vars via an init container or as a direct string referencing the secret values) -- Is idempotent: skips if env vars already exist - -Add a new reconciler method `reconcileDatabaseSecretInjection` that: -- Finds components with database traits -- Gets or waits for the corresponding Deployment (by component name) -- Calls `InjectDatabaseEnvVars` to patch the Deployment's env block -- Uses `r.Update()` to apply changes to the live Deployment - -#### [MODIFY] [heliosapp_controller.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/heliosapp_controller.go) - -- Add a call to `r.reconcileDatabaseSecretInjection(ctx, &heliosApp)` as **Phase 0.9** (after database instance provisioning at Phase 0.7, before image validation) -- Add structured logging for the injection phase - -#### [MODIFY] [database_resources_test.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources_test.go) - -Add new test cases: -- `TestInjectDatabaseEnvVars` — verifies env vars are correctly injected into a Deployment -- `TestInjectDatabaseEnvVars_Idempotent` — verifies running injection twice doesn't duplicate env vars -- `TestReconcileDatabaseSecretInjection` — integration test with fake client verifying the full reconciliation flow -- `TestReconcileDatabaseSecretInjection_NoTraits` — skips when no database traits -- `TestReconcileDatabaseSecretInjection_DeploymentNotFound` — handles missing Deployment gracefully (returns nil, logs warning — Deployment may not be deployed by ArgoCD yet) - ---- - -### Node.js Template — NestJS + Prisma ORM - -Create a new Backstage template for NestJS + Prisma that connects to the database using the injected env vars. - -#### [NEW] `apps/portal/examples/nestjs-prisma-template/` directory - -Structure: -``` -nestjs-prisma-template/ - template.yaml # Backstage scaffolder template - content/ - source/ - package.json # NestJS + Prisma deps (latest) - tsconfig.json # TypeScript config - tsconfig.build.json # Build-specific TS config - nest-cli.json # NestJS CLI config - .env.example # Example env vars - Dockerfile # Multi-stage NestJS build - catalog-info.yaml # Backstage catalog entry - prisma/ - schema.prisma # Prisma schema with env-based DATABASE_URL - src/ - main.ts # NestJS bootstrap - app.module.ts # Root module - app.controller.ts # Health check controller - app.service.ts # App service - prisma/ - prisma.module.ts # Prisma module - prisma.service.ts # Prisma service (extends PrismaClient) - gitops/ - helios-app.yaml # HeliosApp CRD manifest with database trait -``` - -Key design decisions: -- `DATABASE_URL` is built from `DB_HOST`, `DB_USER`, `DB_PASS` env vars (Prisma convention) -- The Prisma schema reads `DATABASE_URL` from `env("DATABASE_URL")` -- The [Dockerfile](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/Dockerfile) runs `prisma generate` at build time and `prisma migrate deploy` at startup -- Uses latest stable NestJS v11+ and Prisma v6+ - ---- - -## Verification Plan - -### Automated Tests - -All existing tests plus new tests, run from `apps/operator/`: - -```sh -# 1. Compilation check -go build ./... - -# 2. Static analysis -go vet ./... - -# 3. Full test suite (includes all database_resources_test.go tests) -make test - -# 4. Build operator binary -make build - -# 5. Run database-specific tests in verbose mode -go test ./internal/controller/... -v -count=1 -run TestInjectDatabaseEnvVars -go test ./internal/controller/... -v -count=1 -run TestReconcileDatabaseSecretInjection -``` - -### Manual Verification - -After deploying a backend with a database trait: -1. Run `kubectl describe pod ` to verify DB_HOST, DB_USER, DB_PASS are present as environment variables referencing the secret -2. Verify the NestJS application boots and connects to the database by checking pod logs diff --git a/task.md b/task.md deleted file mode 100644 index 5efd769..0000000 --- a/task.md +++ /dev/null @@ -1,28 +0,0 @@ -# [Impl] Automated Secret Injection #39 - -## Operator — Secret Injection into Backend Deployment - -- [x] Research codebase and understand existing patterns -- [x] Implement [reconcileDatabaseSecretInjection](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#497-552) in Go operator - - [x] Add new phase (0.9) to inject DB_HOST, DB_USER, DB_PASS into backend Deployment env block - - [x] Patch the Deployment rendered by CUE/GitOps with env secretKeyRef -- [x] Add comprehensive tests for secret injection logic - - [x] Unit tests for injection function (3 tests) - - [x] Integration test with fake client (3 tests) - -## Node.js Template — NestJS + Prisma ORM - -- [x] Create NestJS + Prisma template directory structure - - [x] [package.json](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/advanced-template/content/source/package.json) with latest NestJS and Prisma deps - - [x] [prisma/schema.prisma](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma) connecting via DB_HOST, DB_USER, DB_PASS envs - - [x] `prisma/migrations/` with initial migration - - [x] NestJS main app module with Prisma service - - [x] [Dockerfile](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/Dockerfile) for containerized deployment -- [x] Update Backstage template for database-backed services - -## Verification - -- [/] `go build ./...` — compiles cleanly -- [x] `go vet ./...` — no issues -- [x] `make test` — all tests pass -- [/] `make build` — operator binary builds diff --git a/walkthrough.md b/walkthrough.md deleted file mode 100644 index 450130a..0000000 --- a/walkthrough.md +++ /dev/null @@ -1,2596 +0,0 @@ -# Automated Secret Injection (#39) — Walkthrough - -## Changes Made - -### 1. Go Operator — Secret Injection (Phase 0.9) - -#### [database_resources.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go) - -- **[InjectDatabaseEnvVars](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#458-496)** — Patches a Deployment's first container with `DB_HOST`, `DB_USER`, `DB_PASS` env vars via `secretKeyRef`. Idempotent: skips if vars already exist. -- **[reconcileDatabaseSecretInjection](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#497-552)** — Finds components with database traits, fetches the live Deployment, calls [InjectDatabaseEnvVars](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources.go#458-496), and updates it. Gracefully skips if Deployment doesn't exist yet (ArgoCD hasn't synced). - -```diff:database_resources.go -// database_resources.go handles database credential generation and Secret creation -// for components with database traits. -// -// The CUE engine generates ConfigMaps with database metadata (host, port, name), -// but credentials (username, password) are generated by this Go code for security -// reasons - secrets should never be stored in CUE definitions or GitOps repos. -package controller - -import ( - "context" - "crypto/rand" - "encoding/base64" - "encoding/json" - "fmt" - "math/big" - "strings" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" -) - -const ( - // DefaultPasswordLength is the length of generated passwords - DefaultPasswordLength = 32 - - // DefaultUsernameLength is the length of generated usernames - DefaultUsernameLength = 16 - - // PasswordCharset contains valid characters for password generation - // Includes uppercase, lowercase, digits, and special characters - PasswordCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+" - - // UsernameCharset contains valid characters for username generation - // Lowercase letters and digits only for database compatibility - UsernameCharset = "abcdefghijklmnopqrstuvwxyz0123456789" - - // DatabaseTraitType is the trait type identifier for database traits - DatabaseTraitType = "database" - - // DefaultPostgresVersion is the default Postgres image tag when not specified. - DefaultPostgresVersion = "16" - - // DefaultPostgresPort is the default port for Postgres. - DefaultPostgresPort = 5432 - - // DefaultDatabaseStorage is the default PVC size for database volumes. - DefaultDatabaseStorage = "1Gi" - - // PostgresDataPath is the mount path for Postgres data directory. - PostgresDataPath = "/var/lib/postgresql/data" - - // PostgresDataSubPath is the subPath within the PVC to avoid lost+found issues. - PostgresDataSubPath = "pgdata" -) - -// DatabaseCredentials holds generated database credentials -type DatabaseCredentials struct { - Username string - Password string -} - -// DatabaseTraitProperties represents the properties of a database trait -type DatabaseTraitProperties struct { - DBType string `json:"dbType"` - DBName string `json:"dbName"` - Port int `json:"port"` - Version string `json:"version"` - Storage string `json:"storage"` -} - -// GenerateSecurePassword generates a cryptographically secure random password -func GenerateSecurePassword(length int) (string, error) { - if length <= 0 { - length = DefaultPasswordLength - } - - password := make([]byte, length) - charsetLen := big.NewInt(int64(len(PasswordCharset))) - - for i := 0; i < length; i++ { - idx, err := rand.Int(rand.Reader, charsetLen) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - password[i] = PasswordCharset[idx.Int64()] - } - - return string(password), nil -} - -// GenerateSecureUsername generates a cryptographically secure random username -func GenerateSecureUsername(length int) (string, error) { - if length <= 0 { - length = DefaultUsernameLength - } - - // Ensure username starts with a letter (database requirement) - username := make([]byte, length) - lettersOnly := "abcdefghijklmnopqrstuvwxyz" - lettersLen := big.NewInt(int64(len(lettersOnly))) - - // First character must be a letter - idx, err := rand.Int(rand.Reader, lettersLen) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - username[0] = lettersOnly[idx.Int64()] - - // Rest can be letters or digits - charsetLen := big.NewInt(int64(len(UsernameCharset))) - for i := 1; i < length; i++ { - idx, err := rand.Int(rand.Reader, charsetLen) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - username[i] = UsernameCharset[idx.Int64()] - } - - return string(username), nil -} - -// GenerateCredentials generates a new set of database credentials -func GenerateCredentials() (*DatabaseCredentials, error) { - username, err := GenerateSecureUsername(DefaultUsernameLength) - if err != nil { - return nil, fmt.Errorf("failed to generate username: %w", err) - } - - password, err := GenerateSecurePassword(DefaultPasswordLength) - if err != nil { - return nil, fmt.Errorf("failed to generate password: %w", err) - } - - return &DatabaseCredentials{ - Username: username, - Password: password, - }, nil -} - -// GenerateDatabaseSecret creates a Kubernetes Secret containing database credentials -func GenerateDatabaseSecret(namespace, secretName, componentName string, creds *DatabaseCredentials, dbHost string) *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - Labels: map[string]string{ - "app": componentName, - "helios.io/managed-by": "operator", - "helios.io/secret-type": "database-credentials", - }, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "DB_USER": []byte(creds.Username), - "DB_PASS": []byte(creds.Password), - "DB_HOST": []byte(dbHost), - }, - } -} - -// GetDatabaseSecretName returns the conventional secret name for a component -func GetDatabaseSecretName(componentName string) string { - return fmt.Sprintf("%s-db-secret", componentName) -} - -// GetDatabaseHost returns the conventional database host for a component -func GetDatabaseHost(componentName string) string { - return fmt.Sprintf("%s-db", componentName) -} - -// ExtractDatabaseTraits extracts all database traits from HeliosApp components -func ExtractDatabaseTraits(app *appv1alpha1.HeliosApp) []struct { - ComponentName string - Properties DatabaseTraitProperties -} { - var dbTraits []struct { - ComponentName string - Properties DatabaseTraitProperties - } - - for _, component := range app.Spec.Components { - for _, trait := range component.Traits { - if strings.ToLower(trait.Type) == DatabaseTraitType { - var props DatabaseTraitProperties - if trait.Properties != nil && trait.Properties.Raw != nil { - if err := json.Unmarshal(trait.Properties.Raw, &props); err != nil { - // Log error but continue - don't fail the entire reconciliation - continue - } - } - dbTraits = append(dbTraits, struct { - ComponentName string - Properties DatabaseTraitProperties - }{ - ComponentName: component.Name, - Properties: props, - }) - } - } - } - - return dbTraits -} - -// reconcileDatabaseSecrets ensures database credential secrets exist for all -// components with database traits. If a secret already exists, it is not modified -// to preserve existing credentials. -func (r *HeliosAppReconciler) reconcileDatabaseSecrets(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - dbTraits := ExtractDatabaseTraits(app) - if len(dbTraits) == 0 { - log.V(1).Info("No database traits found, skipping secret creation") - return nil - } - - for _, dbTrait := range dbTraits { - secretName := GetDatabaseSecretName(dbTrait.ComponentName) - dbHost := GetDatabaseHost(dbTrait.ComponentName) - - // Check if secret already exists - existingSecret := &corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{ - Name: secretName, - Namespace: app.Namespace, - }, existingSecret) - - if err == nil { - // Secret already exists - do not overwrite to preserve credentials - log.Info("Database secret already exists, skipping", - "component", dbTrait.ComponentName, - "secret", secretName) - continue - } - - if !errors.IsNotFound(err) { - // Unexpected error - log.Error(err, "Failed to check for existing database secret", - "component", dbTrait.ComponentName, - "secret", secretName) - return fmt.Errorf("failed to check for database secret %s: %w", secretName, err) - } - - // Secret doesn't exist - generate new credentials - log.Info("Generating database credentials", - "component", dbTrait.ComponentName, - "secret", secretName) - - creds, err := GenerateCredentials() - if err != nil { - log.Error(err, "Failed to generate database credentials", - "component", dbTrait.ComponentName) - return fmt.Errorf("failed to generate credentials for %s: %w", dbTrait.ComponentName, err) - } - - // Create the secret - secret := GenerateDatabaseSecret(app.Namespace, secretName, dbTrait.ComponentName, creds, dbHost) - - // Set owner reference so secret is garbage collected with the HeliosApp - if err := ctrl.SetControllerReference(app, secret, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for database secret", - "component", dbTrait.ComponentName, - "secret", secretName) - return fmt.Errorf("failed to set owner reference for secret %s: %w", secretName, err) - } - - if err := r.Create(ctx, secret); err != nil { - if errors.IsAlreadyExists(err) { - // Race condition - secret was created between our check and create - log.Info("Database secret was created concurrently, skipping", - "component", dbTrait.ComponentName, - "secret", secretName) - continue - } - log.Error(err, "Failed to create database secret", - "component", dbTrait.ComponentName, - "secret", secretName) - return fmt.Errorf("failed to create database secret %s: %w", secretName, err) - } - - log.Info("Successfully created database secret", - "component", dbTrait.ComponentName, - "secret", secretName, - "dbHost", dbHost) - } - - return nil -} - -// reconcileDatabaseInstance provisions database StatefulSets and headless -// Services for components with database traits. This runs AFTER -// reconcileDatabaseSecrets so that the credential Secret already exists -// when the StatefulSet is created. -func (r *HeliosAppReconciler) reconcileDatabaseInstance(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - dbTraits := ExtractDatabaseTraits(app) - if len(dbTraits) == 0 { - log.V(1).Info("No database traits found, skipping instance provisioning") - return nil - } - - for _, dbTrait := range dbTraits { - // Only provision postgres instances for now - if strings.ToLower(dbTrait.Properties.DBType) != "postgres" { - log.V(1).Info("Skipping non-postgres database type", - "component", dbTrait.ComponentName, - "dbType", dbTrait.Properties.DBType) - continue - } - - dbHost := GetDatabaseHost(dbTrait.ComponentName) - secretName := GetDatabaseSecretName(dbTrait.ComponentName) - - // Determine effective database name - effectiveDBName := dbTrait.Properties.DBName - if effectiveDBName == "" { - effectiveDBName = fmt.Sprintf("%s-db", dbTrait.ComponentName) - } - - // Determine version — CUE schema requires version!, but we - // guard here defensively in case of direct API usage. - version := dbTrait.Properties.Version - if version == "" { - version = DefaultPostgresVersion - } - - // Determine port - port := dbTrait.Properties.Port - if port <= 0 { - port = DefaultPostgresPort - } - - // Determine storage - storage := dbTrait.Properties.Storage - if storage == "" { - storage = DefaultDatabaseStorage - } - - // --- StatefulSet --- - sts, err := GenerateDatabaseStatefulSet( - app.Namespace, dbHost, secretName, effectiveDBName, version, storage, int32(port), - ) - if err != nil { - log.Error(err, "Failed to generate database StatefulSet", - "component", dbTrait.ComponentName, "storage", storage) - return fmt.Errorf("failed to generate StatefulSet for %s: %w", dbHost, err) - } - - if err := ctrl.SetControllerReference(app, sts, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for database StatefulSet", - "component", dbTrait.ComponentName) - return fmt.Errorf("failed to set owner reference for StatefulSet %s: %w", dbHost, err) - } - - existingSts := &appsv1.StatefulSet{} - err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSts) - if err != nil { - if !errors.IsNotFound(err) { - return fmt.Errorf("failed to check for StatefulSet %s: %w", dbHost, err) - } - - log.Info("Creating database StatefulSet", - "component", dbTrait.ComponentName, - "statefulset", dbHost, - "image", fmt.Sprintf("postgres:%s", version)) - - if err := r.Create(ctx, sts); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("Database StatefulSet was created concurrently, skipping", - "component", dbTrait.ComponentName) - } else { - return fmt.Errorf("failed to create StatefulSet %s: %w", dbHost, err) - } - } - } else { - // Handle StatefulSet drift: update spec to match the new template - log.Info("Database StatefulSet already exists, updating if necessary", - "component", dbTrait.ComponentName, - "statefulset", dbHost) - - // We only update the mutable fields (Replicas, Template) - updatedSts := existingSts.DeepCopy() - updatedSts.Spec.Replicas = sts.Spec.Replicas - updatedSts.Spec.Template = sts.Spec.Template - - // We need to preserve the existing VolumeClaimTemplates when updating - updatedSts.Spec.VolumeClaimTemplates = existingSts.Spec.VolumeClaimTemplates - - if err := r.Update(ctx, updatedSts); err != nil { - return fmt.Errorf("failed to update StatefulSet %s: %w", dbHost, err) - } - } - - // --- Headless Service --- - svc := GenerateDatabaseService(app.Namespace, dbHost, int32(port)) - - if err := ctrl.SetControllerReference(app, svc, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for database Service", - "component", dbTrait.ComponentName) - return fmt.Errorf("failed to set owner reference for Service %s: %w", dbHost, err) - } - - existingSvc := &corev1.Service{} - err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSvc) - if err != nil { - if !errors.IsNotFound(err) { - return fmt.Errorf("failed to check for Service %s: %w", dbHost, err) - } - - log.Info("Creating database headless Service", - "component", dbTrait.ComponentName, - "service", dbHost) - - if err := r.Create(ctx, svc); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("Database Service was created concurrently, skipping", - "component", dbTrait.ComponentName) - } else { - return fmt.Errorf("failed to create Service %s: %w", dbHost, err) - } - } - } else { - log.Info("Database Service already exists, updating if necessary", - "component", dbTrait.ComponentName, - "service", dbHost) - - updatedSvc := existingSvc.DeepCopy() - updatedSvc.Spec.Ports = svc.Spec.Ports - - if err := r.Update(ctx, updatedSvc); err != nil { - return fmt.Errorf("failed to update Service %s: %w", dbHost, err) - } - } - - log.Info("Successfully reconciled database instance", - "component", dbTrait.ComponentName, - "statefulset", dbHost, - "dbName", effectiveDBName) - } - - return nil -} - -// GenerateDatabaseStatefulSet creates a StatefulSet for a Postgres database instance. -// The StatefulSet injects POSTGRES_DB from the CRD's database.name value, and -// uses the Secret from Issue #33 for POSTGRES_USER and POSTGRES_PASSWORD. -func GenerateDatabaseStatefulSet(namespace, name, secretName, dbName, version, storage string, port int32) (*appsv1.StatefulSet, error) { - storageQty, err := resource.ParseQuantity(storage) - if err != nil { - return nil, fmt.Errorf("invalid storage size format %q: %w", storage, err) - } - - replicas := int32(1) - labels := map[string]string{ - "app": name, - "helios.io/managed-by": "operator", - "helios.io/trait": "database", - "helios.io/db-type": "postgres", - } - - return &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: appsv1.StatefulSetSpec{ - ServiceName: name, - Replicas: &replicas, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": name}, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": name}, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "postgres", - Image: fmt.Sprintf("postgres:%s", version), - Ports: []corev1.ContainerPort{ - { - ContainerPort: port, - Name: "postgres", - }, - }, - Env: []corev1.EnvVar{ - { - Name: "POSTGRES_DB", - Value: dbName, - }, - { - Name: "POSTGRES_USER", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: "DB_USER", - }, - }, - }, - { - Name: "POSTGRES_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: "DB_PASS", - }, - }, - }, - { - // PGDATA tells Postgres where to store cluster data. - // Must match volumeMount + subPath to avoid lost+found conflicts. - Name: "PGDATA", - Value: PostgresDataPath + "/" + PostgresDataSubPath, - }, - { - // Ensure consistent UTF-8 encoding for all databases. - Name: "POSTGRES_INITDB_ARGS", - Value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C", - }, - { - // Explicitly set the custom port so that postgres knows to listen on it. - Name: "PGPORT", - Value: fmt.Sprintf("%d", port), - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "data", - MountPath: PostgresDataPath, - SubPath: PostgresDataSubPath, - }, - }, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resourceMustParse("100m"), - corev1.ResourceMemory: resourceMustParse("256Mi"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resourceMustParse("500m"), - corev1.ResourceMemory: resourceMustParse("512Mi"), - }, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, - }, - }, - InitialDelaySeconds: 5, - PeriodSeconds: 10, - }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, - }, - }, - InitialDelaySeconds: 30, - PeriodSeconds: 10, - }, - }, - }, - }, - }, - VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "data", - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: storageQty, - }, - }, - }, - }, - }, - }, - }, nil -} - -// GenerateDatabaseService creates a headless Service for a database StatefulSet. -// The headless Service (clusterIP: None) provides stable DNS resolution -// so resolves to the database pod. -func GenerateDatabaseService(namespace, name string, port int32) *corev1.Service { - labels := map[string]string{ - "app": name, - "helios.io/managed-by": "operator", - "helios.io/trait": "database", - } - - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: corev1.ServiceSpec{ - ClusterIP: "None", - Selector: map[string]string{"app": name}, - Ports: []corev1.ServicePort{ - { - Port: port, - TargetPort: intstr.FromInt32(port), - Name: "db", - }, - }, - }, - } -} - -// resourceMustParse is a helper to parse resource quantities. Panics on invalid input. -func resourceMustParse(s string) resource.Quantity { - return resource.MustParse(s) -} - -// GenerateBase64Token generates a random base64-encoded token -// Useful for generating secure webhook secrets or API tokens -func GenerateBase64Token(byteLength int) (string, error) { - if byteLength <= 0 { - byteLength = 32 - } - - bytes := make([]byte, byteLength) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("failed to generate random bytes: %w", err) - } - - return base64.StdEncoding.EncodeToString(bytes), nil -} -=== -// database_resources.go handles database credential generation and Secret creation -// for components with database traits. -// -// The CUE engine generates ConfigMaps with database metadata (host, port, name), -// but credentials (username, password) are generated by this Go code for security -// reasons - secrets should never be stored in CUE definitions or GitOps repos. -package controller - -import ( - "context" - "crypto/rand" - "encoding/base64" - "encoding/json" - "fmt" - "math/big" - "strings" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" -) - -const ( - // DefaultPasswordLength is the length of generated passwords - DefaultPasswordLength = 32 - - // DefaultUsernameLength is the length of generated usernames - DefaultUsernameLength = 16 - - // PasswordCharset contains valid characters for password generation - // Includes uppercase, lowercase, digits, and special characters - PasswordCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+" - - // UsernameCharset contains valid characters for username generation - // Lowercase letters and digits only for database compatibility - UsernameCharset = "abcdefghijklmnopqrstuvwxyz0123456789" - - // DatabaseTraitType is the trait type identifier for database traits - DatabaseTraitType = "database" - - // DefaultPostgresVersion is the default Postgres image tag when not specified. - DefaultPostgresVersion = "16" - - // DefaultPostgresPort is the default port for Postgres. - DefaultPostgresPort = 5432 - - // DefaultDatabaseStorage is the default PVC size for database volumes. - DefaultDatabaseStorage = "1Gi" - - // PostgresDataPath is the mount path for Postgres data directory. - PostgresDataPath = "/var/lib/postgresql/data" - - // PostgresDataSubPath is the subPath within the PVC to avoid lost+found issues. - PostgresDataSubPath = "pgdata" -) - -// DatabaseCredentials holds generated database credentials -type DatabaseCredentials struct { - Username string - Password string -} - -// DatabaseTraitProperties represents the properties of a database trait -type DatabaseTraitProperties struct { - DBType string `json:"dbType"` - DBName string `json:"dbName"` - Port int `json:"port"` - Version string `json:"version"` - Storage string `json:"storage"` -} - -// GenerateSecurePassword generates a cryptographically secure random password -func GenerateSecurePassword(length int) (string, error) { - if length <= 0 { - length = DefaultPasswordLength - } - - password := make([]byte, length) - charsetLen := big.NewInt(int64(len(PasswordCharset))) - - for i := 0; i < length; i++ { - idx, err := rand.Int(rand.Reader, charsetLen) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - password[i] = PasswordCharset[idx.Int64()] - } - - return string(password), nil -} - -// GenerateSecureUsername generates a cryptographically secure random username -func GenerateSecureUsername(length int) (string, error) { - if length <= 0 { - length = DefaultUsernameLength - } - - // Ensure username starts with a letter (database requirement) - username := make([]byte, length) - lettersOnly := "abcdefghijklmnopqrstuvwxyz" - lettersLen := big.NewInt(int64(len(lettersOnly))) - - // First character must be a letter - idx, err := rand.Int(rand.Reader, lettersLen) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - username[0] = lettersOnly[idx.Int64()] - - // Rest can be letters or digits - charsetLen := big.NewInt(int64(len(UsernameCharset))) - for i := 1; i < length; i++ { - idx, err := rand.Int(rand.Reader, charsetLen) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - username[i] = UsernameCharset[idx.Int64()] - } - - return string(username), nil -} - -// GenerateCredentials generates a new set of database credentials -func GenerateCredentials() (*DatabaseCredentials, error) { - username, err := GenerateSecureUsername(DefaultUsernameLength) - if err != nil { - return nil, fmt.Errorf("failed to generate username: %w", err) - } - - password, err := GenerateSecurePassword(DefaultPasswordLength) - if err != nil { - return nil, fmt.Errorf("failed to generate password: %w", err) - } - - return &DatabaseCredentials{ - Username: username, - Password: password, - }, nil -} - -// GenerateDatabaseSecret creates a Kubernetes Secret containing database credentials -func GenerateDatabaseSecret(namespace, secretName, componentName string, creds *DatabaseCredentials, dbHost string) *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - Labels: map[string]string{ - "app": componentName, - "helios.io/managed-by": "operator", - "helios.io/secret-type": "database-credentials", - }, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "DB_USER": []byte(creds.Username), - "DB_PASS": []byte(creds.Password), - "DB_HOST": []byte(dbHost), - }, - } -} - -// GetDatabaseSecretName returns the conventional secret name for a component -func GetDatabaseSecretName(componentName string) string { - return fmt.Sprintf("%s-db-secret", componentName) -} - -// GetDatabaseHost returns the conventional database host for a component -func GetDatabaseHost(componentName string) string { - return fmt.Sprintf("%s-db", componentName) -} - -// ExtractDatabaseTraits extracts all database traits from HeliosApp components -func ExtractDatabaseTraits(app *appv1alpha1.HeliosApp) []struct { - ComponentName string - Properties DatabaseTraitProperties -} { - var dbTraits []struct { - ComponentName string - Properties DatabaseTraitProperties - } - - for _, component := range app.Spec.Components { - for _, trait := range component.Traits { - if strings.ToLower(trait.Type) == DatabaseTraitType { - var props DatabaseTraitProperties - if trait.Properties != nil && trait.Properties.Raw != nil { - if err := json.Unmarshal(trait.Properties.Raw, &props); err != nil { - // Log error but continue - don't fail the entire reconciliation - continue - } - } - dbTraits = append(dbTraits, struct { - ComponentName string - Properties DatabaseTraitProperties - }{ - ComponentName: component.Name, - Properties: props, - }) - } - } - } - - return dbTraits -} - -// reconcileDatabaseSecrets ensures database credential secrets exist for all -// components with database traits. If a secret already exists, it is not modified -// to preserve existing credentials. -func (r *HeliosAppReconciler) reconcileDatabaseSecrets(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - dbTraits := ExtractDatabaseTraits(app) - if len(dbTraits) == 0 { - log.V(1).Info("No database traits found, skipping secret creation") - return nil - } - - for _, dbTrait := range dbTraits { - secretName := GetDatabaseSecretName(dbTrait.ComponentName) - dbHost := GetDatabaseHost(dbTrait.ComponentName) - - // Check if secret already exists - existingSecret := &corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{ - Name: secretName, - Namespace: app.Namespace, - }, existingSecret) - - if err == nil { - // Secret already exists - do not overwrite to preserve credentials - log.Info("Database secret already exists, skipping", - "component", dbTrait.ComponentName, - "secret", secretName) - continue - } - - if !errors.IsNotFound(err) { - // Unexpected error - log.Error(err, "Failed to check for existing database secret", - "component", dbTrait.ComponentName, - "secret", secretName) - return fmt.Errorf("failed to check for database secret %s: %w", secretName, err) - } - - // Secret doesn't exist - generate new credentials - log.Info("Generating database credentials", - "component", dbTrait.ComponentName, - "secret", secretName) - - creds, err := GenerateCredentials() - if err != nil { - log.Error(err, "Failed to generate database credentials", - "component", dbTrait.ComponentName) - return fmt.Errorf("failed to generate credentials for %s: %w", dbTrait.ComponentName, err) - } - - // Create the secret - secret := GenerateDatabaseSecret(app.Namespace, secretName, dbTrait.ComponentName, creds, dbHost) - - // Set owner reference so secret is garbage collected with the HeliosApp - if err := ctrl.SetControllerReference(app, secret, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for database secret", - "component", dbTrait.ComponentName, - "secret", secretName) - return fmt.Errorf("failed to set owner reference for secret %s: %w", secretName, err) - } - - if err := r.Create(ctx, secret); err != nil { - if errors.IsAlreadyExists(err) { - // Race condition - secret was created between our check and create - log.Info("Database secret was created concurrently, skipping", - "component", dbTrait.ComponentName, - "secret", secretName) - continue - } - log.Error(err, "Failed to create database secret", - "component", dbTrait.ComponentName, - "secret", secretName) - return fmt.Errorf("failed to create database secret %s: %w", secretName, err) - } - - log.Info("Successfully created database secret", - "component", dbTrait.ComponentName, - "secret", secretName, - "dbHost", dbHost) - } - - return nil -} - -// reconcileDatabaseInstance provisions database StatefulSets and headless -// Services for components with database traits. This runs AFTER -// reconcileDatabaseSecrets so that the credential Secret already exists -// when the StatefulSet is created. -func (r *HeliosAppReconciler) reconcileDatabaseInstance(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - dbTraits := ExtractDatabaseTraits(app) - if len(dbTraits) == 0 { - log.V(1).Info("No database traits found, skipping instance provisioning") - return nil - } - - for _, dbTrait := range dbTraits { - // Only provision postgres instances for now - if strings.ToLower(dbTrait.Properties.DBType) != "postgres" { - log.V(1).Info("Skipping non-postgres database type", - "component", dbTrait.ComponentName, - "dbType", dbTrait.Properties.DBType) - continue - } - - dbHost := GetDatabaseHost(dbTrait.ComponentName) - secretName := GetDatabaseSecretName(dbTrait.ComponentName) - - // Determine effective database name - effectiveDBName := dbTrait.Properties.DBName - if effectiveDBName == "" { - effectiveDBName = fmt.Sprintf("%s-db", dbTrait.ComponentName) - } - - // Determine version — CUE schema requires version!, but we - // guard here defensively in case of direct API usage. - version := dbTrait.Properties.Version - if version == "" { - version = DefaultPostgresVersion - } - - // Determine port - port := dbTrait.Properties.Port - if port <= 0 { - port = DefaultPostgresPort - } - - // Determine storage - storage := dbTrait.Properties.Storage - if storage == "" { - storage = DefaultDatabaseStorage - } - - // --- StatefulSet --- - sts, err := GenerateDatabaseStatefulSet( - app.Namespace, dbHost, secretName, effectiveDBName, version, storage, int32(port), - ) - if err != nil { - log.Error(err, "Failed to generate database StatefulSet", - "component", dbTrait.ComponentName, "storage", storage) - return fmt.Errorf("failed to generate StatefulSet for %s: %w", dbHost, err) - } - - if err := ctrl.SetControllerReference(app, sts, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for database StatefulSet", - "component", dbTrait.ComponentName) - return fmt.Errorf("failed to set owner reference for StatefulSet %s: %w", dbHost, err) - } - - existingSts := &appsv1.StatefulSet{} - err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSts) - if err != nil { - if !errors.IsNotFound(err) { - return fmt.Errorf("failed to check for StatefulSet %s: %w", dbHost, err) - } - - log.Info("Creating database StatefulSet", - "component", dbTrait.ComponentName, - "statefulset", dbHost, - "image", fmt.Sprintf("postgres:%s", version)) - - if err := r.Create(ctx, sts); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("Database StatefulSet was created concurrently, skipping", - "component", dbTrait.ComponentName) - } else { - return fmt.Errorf("failed to create StatefulSet %s: %w", dbHost, err) - } - } - } else { - // Handle StatefulSet drift: update spec to match the new template - log.Info("Database StatefulSet already exists, updating if necessary", - "component", dbTrait.ComponentName, - "statefulset", dbHost) - - // We only update the mutable fields (Replicas, Template) - updatedSts := existingSts.DeepCopy() - updatedSts.Spec.Replicas = sts.Spec.Replicas - updatedSts.Spec.Template = sts.Spec.Template - - // We need to preserve the existing VolumeClaimTemplates when updating - updatedSts.Spec.VolumeClaimTemplates = existingSts.Spec.VolumeClaimTemplates - - if err := r.Update(ctx, updatedSts); err != nil { - return fmt.Errorf("failed to update StatefulSet %s: %w", dbHost, err) - } - } - - // --- Headless Service --- - svc := GenerateDatabaseService(app.Namespace, dbHost, int32(port)) - - if err := ctrl.SetControllerReference(app, svc, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for database Service", - "component", dbTrait.ComponentName) - return fmt.Errorf("failed to set owner reference for Service %s: %w", dbHost, err) - } - - existingSvc := &corev1.Service{} - err = r.Get(ctx, types.NamespacedName{Name: dbHost, Namespace: app.Namespace}, existingSvc) - if err != nil { - if !errors.IsNotFound(err) { - return fmt.Errorf("failed to check for Service %s: %w", dbHost, err) - } - - log.Info("Creating database headless Service", - "component", dbTrait.ComponentName, - "service", dbHost) - - if err := r.Create(ctx, svc); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("Database Service was created concurrently, skipping", - "component", dbTrait.ComponentName) - } else { - return fmt.Errorf("failed to create Service %s: %w", dbHost, err) - } - } - } else { - log.Info("Database Service already exists, updating if necessary", - "component", dbTrait.ComponentName, - "service", dbHost) - - updatedSvc := existingSvc.DeepCopy() - updatedSvc.Spec.Ports = svc.Spec.Ports - - if err := r.Update(ctx, updatedSvc); err != nil { - return fmt.Errorf("failed to update Service %s: %w", dbHost, err) - } - } - - log.Info("Successfully reconciled database instance", - "component", dbTrait.ComponentName, - "statefulset", dbHost, - "dbName", effectiveDBName) - } - - return nil -} - -// databaseEnvVarNames lists the env var names injected by the operator -// for database credential secret injection. -var databaseEnvVarNames = []string{"DB_HOST", "DB_USER", "DB_PASS"} - -// InjectDatabaseEnvVars patches a Deployment's first container to include -// DB_HOST, DB_USER, DB_PASS env vars referencing the given K8s Secret. -// The function is idempotent — if the env vars already exist with the correct -// secretKeyRef, no changes are made and it returns false. -func InjectDatabaseEnvVars(deploy *appsv1.Deployment, secretName string) bool { - if len(deploy.Spec.Template.Spec.Containers) == 0 { - return false - } - - container := &deploy.Spec.Template.Spec.Containers[0] - - // Build a set of existing env var names for fast lookup. - existingEnvs := make(map[string]bool, len(container.Env)) - for _, ev := range container.Env { - existingEnvs[ev.Name] = true - } - - changed := false - for _, envName := range databaseEnvVarNames { - if existingEnvs[envName] { - continue - } - container.Env = append(container.Env, corev1.EnvVar{ - Name: envName, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: envName, - }, - }, - }) - changed = true - } - - return changed -} - -// reconcileDatabaseSecretInjection patches live Deployments (deployed by ArgoCD) -// to inject DB_HOST, DB_USER, DB_PASS env vars from the operator-managed Secret. -// This runs AFTER database secrets and instances are provisioned so that the -// Secret already exists when the Deployment references it. -func (r *HeliosAppReconciler) reconcileDatabaseSecretInjection(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - dbTraits := ExtractDatabaseTraits(app) - if len(dbTraits) == 0 { - log.V(1).Info("No database traits found, skipping secret injection") - return nil - } - - for _, dbTrait := range dbTraits { - secretName := GetDatabaseSecretName(dbTrait.ComponentName) - deployName := dbTrait.ComponentName - - // Fetch the live Deployment (created by ArgoCD via GitOps). - deploy := &appsv1.Deployment{} - err := r.Get(ctx, types.NamespacedName{ - Name: deployName, - Namespace: app.Namespace, - }, deploy) - if err != nil { - if errors.IsNotFound(err) { - // Deployment may not exist yet (ArgoCD hasn't synced). - // This is expected — we'll inject on the next reconcile. - log.Info("Deployment not found yet, will inject on next reconcile", - "component", dbTrait.ComponentName, - "deployment", deployName) - continue - } - return fmt.Errorf("failed to get Deployment %s: %w", deployName, err) - } - - // Inject env vars if not already present. - if !InjectDatabaseEnvVars(deploy, secretName) { - log.V(1).Info("Database env vars already injected, skipping", - "component", dbTrait.ComponentName, - "deployment", deployName) - continue - } - - if err := r.Update(ctx, deploy); err != nil { - return fmt.Errorf("failed to update Deployment %s with database env vars: %w", deployName, err) - } - - log.Info("Successfully injected database env vars into Deployment", - "component", dbTrait.ComponentName, - "deployment", deployName, - "secret", secretName) - } - - return nil -} - -// GenerateDatabaseStatefulSet creates a StatefulSet for a Postgres database instance. -// The StatefulSet injects POSTGRES_DB from the CRD's database.name value, and -// uses the Secret from Issue #33 for POSTGRES_USER and POSTGRES_PASSWORD. -func GenerateDatabaseStatefulSet(namespace, name, secretName, dbName, version, storage string, port int32) (*appsv1.StatefulSet, error) { - storageQty, err := resource.ParseQuantity(storage) - if err != nil { - return nil, fmt.Errorf("invalid storage size format %q: %w", storage, err) - } - - replicas := int32(1) - labels := map[string]string{ - "app": name, - "helios.io/managed-by": "operator", - "helios.io/trait": "database", - "helios.io/db-type": "postgres", - } - - return &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: appsv1.StatefulSetSpec{ - ServiceName: name, - Replicas: &replicas, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": name}, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": name}, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "postgres", - Image: fmt.Sprintf("postgres:%s", version), - Ports: []corev1.ContainerPort{ - { - ContainerPort: port, - Name: "postgres", - }, - }, - Env: []corev1.EnvVar{ - { - Name: "POSTGRES_DB", - Value: dbName, - }, - { - Name: "POSTGRES_USER", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: "DB_USER", - }, - }, - }, - { - Name: "POSTGRES_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: "DB_PASS", - }, - }, - }, - { - // PGDATA tells Postgres where to store cluster data. - // Must match volumeMount + subPath to avoid lost+found conflicts. - Name: "PGDATA", - Value: PostgresDataPath + "/" + PostgresDataSubPath, - }, - { - // Ensure consistent UTF-8 encoding for all databases. - Name: "POSTGRES_INITDB_ARGS", - Value: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C", - }, - { - // Explicitly set the custom port so that postgres knows to listen on it. - Name: "PGPORT", - Value: fmt.Sprintf("%d", port), - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "data", - MountPath: PostgresDataPath, - SubPath: PostgresDataSubPath, - }, - }, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resourceMustParse("100m"), - corev1.ResourceMemory: resourceMustParse("256Mi"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resourceMustParse("500m"), - corev1.ResourceMemory: resourceMustParse("512Mi"), - }, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, - }, - }, - InitialDelaySeconds: 5, - PeriodSeconds: 10, - }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"pg_isready", "-U", "$(POSTGRES_USER)", "-d", dbName, "-p", "$(PGPORT)"}, - }, - }, - InitialDelaySeconds: 30, - PeriodSeconds: 10, - }, - }, - }, - }, - }, - VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "data", - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: storageQty, - }, - }, - }, - }, - }, - }, - }, nil -} - -// GenerateDatabaseService creates a headless Service for a database StatefulSet. -// The headless Service (clusterIP: None) provides stable DNS resolution -// so resolves to the database pod. -func GenerateDatabaseService(namespace, name string, port int32) *corev1.Service { - labels := map[string]string{ - "app": name, - "helios.io/managed-by": "operator", - "helios.io/trait": "database", - } - - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: corev1.ServiceSpec{ - ClusterIP: "None", - Selector: map[string]string{"app": name}, - Ports: []corev1.ServicePort{ - { - Port: port, - TargetPort: intstr.FromInt32(port), - Name: "db", - }, - }, - }, - } -} - -// resourceMustParse is a helper to parse resource quantities. Panics on invalid input. -func resourceMustParse(s string) resource.Quantity { - return resource.MustParse(s) -} - -// GenerateBase64Token generates a random base64-encoded token -// Useful for generating secure webhook secrets or API tokens -func GenerateBase64Token(byteLength int) (string, error) { - if byteLength <= 0 { - byteLength = 32 - } - - bytes := make([]byte, byteLength) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("failed to generate random bytes: %w", err) - } - - return base64.StdEncoding.EncodeToString(bytes), nil -} -``` - -#### [heliosapp_controller.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/heliosapp_controller.go) - -Added **Phase 0.9** call after database instance provisioning: - -```diff:heliosapp_controller.go -/* -Copyright 2026. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "cmp" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" - heliosCue "github.com/helios-platform-team/helios-platform/apps/operator/internal/cue" - "github.com/helios-platform-team/helios-platform/apps/operator/internal/gitops" -) - -// HeliosAppReconciler reconciles a HeliosApp object -type HeliosAppReconciler struct { - client.Client - Scheme *runtime.Scheme - CueEngine heliosCue.CueEngineInterface - // TektonRenderer renders Tekton CI/CD resources via CUE engine. - TektonRenderer heliosCue.TektonRendererInterface - // GitFactory allows injecting a custom GitOps client (e.g. for testing) - GitFactory func(string, string, string) gitops.GitOpsClientInterface -} - -// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/finalizers,verbs=update -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete - -// Reconcile handles the reconciliation loop for HeliosApp -// Controller does NOT iterate components/traits - all orchestration is in CUE -func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := logf.FromContext(ctx) - - // 1. Fetch HeliosApp CRD - var heliosApp appv1alpha1.HeliosApp - if err := r.Get(ctx, req.NamespacedName, &heliosApp); err != nil { - if errors.IsNotFound(err) { - log.Info("HeliosApp resource not found, ignoring") - return ctrl.Result{}, nil - } - return ctrl.Result{}, err - } - - log.Info("Reconciling HeliosApp", "name", heliosApp.Name, "namespace", heliosApp.Namespace) - - // 2. Map CRD to Application Model - appModel, err := r.mapCRDToModel(&heliosApp) - if err != nil { - log.Error(err, "Failed to map CRD to application model") - return ctrl.Result{}, err - } - - // 3. Render via CUE Engine - manifestBytes, err := r.CueEngine.Render(appModel) - if err != nil { - log.Error(err, "Failed to render application via CUE") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE rendering failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE -1 & 0: Tekton CI/CD Resources (Tasks, Pipeline, Triggers) - // All Tekton resources are rendered via CUE engine. - // ------------------------------------------------------------------ - if err := r.reconcileTektonResourcesCue(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to reconcile Tekton resources via CUE") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE Tekton rendering failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE 0.5: Database Credential Secrets - // Generate and store secure credentials for components with database traits. - // Secrets are created BEFORE GitOps sync to ensure credentials exist - // when the application is deployed. - // ------------------------------------------------------------------ - if err := r.reconcileDatabaseSecrets(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to reconcile database secrets") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret creation failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE 0.7: Database Instance Provisioning - // Provision StatefulSets and headless Services for database traits. - // Runs AFTER secrets so that the credential Secret already exists - // when the database pod starts. - // ------------------------------------------------------------------ - if err := r.reconcileDatabaseInstance(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to reconcile database instance") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database instance provisioning failed: %v", err)) - return ctrl.Result{}, err - } - - // VALIDATION: Ensure image is present (Fix "First Commit Missing Image") - // This validation is for application workloads (GitOps pipeline downstream). - // We run this AFTER DB provisioning so databases can come up while app is building. - for _, comp := range appModel.App.Components { - // We can add more specific checks here based on component type - // For now, checks if 'image' property exists and is not empty for all components - // assuming all workloads need an image. - if img, ok := comp.Properties["image"].(string); !ok || img == "" { - msg := fmt.Sprintf("Component '%s' is waiting for image (likely building). Status: Pending.", comp.Name) - log.Info(msg) - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhasePending, msg) - return ctrl.Result{}, nil // Wait for next update (CI/CD will update CR with image) - } - } - - // ------------------------------------------------------------------ - // PHASE 0.6: Trigger Initial PipelineRun (if not already done) - // ------------------------------------------------------------------ - if !heliosApp.Status.InitialBuildTriggered { - log.Info("Triggering initial PipelineRun for new HeliosApp") - - pipelineName := heliosApp.Spec.PipelineName - if pipelineName == "" { - pipelineName = "from-code-to-cluster" - } - pr, err := GeneratePipelineRunForManifestGeneration(&heliosApp, pipelineName) - if err != nil { - log.Error(err, "Failed to generate initial PipelineRun") - } else { - if err := ctrl.SetControllerReference(&heliosApp, pr, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for PipelineRun") - } - - if err := r.Client.Create(ctx, pr); err != nil { - if !errors.IsAlreadyExists(err) { - log.Error(err, "Failed to create initial PipelineRun") - } - } else { - log.Info("Created initial PipelineRun", "name", pr.GetName()) - } - - // Mark as triggered to avoid creating multiple PipelineRuns - heliosApp.Status.InitialBuildTriggered = true - if err := r.Status().Update(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to update InitialBuildTriggered status") - } - } - } - - // ------------------------------------------------------------------ - // PHASE 1: Render & GitOps (Moved below) - // ------------------------------------------------------------------ - - // 4. GitOps Helper: Get Token & Username - token := os.Getenv("GITHUB_TOKEN") - username := os.Getenv("GITHUB_USER") - if username == "" { - username = "git" // Default fallback - } - - if heliosApp.Spec.GitOpsSecretRef != "" { - var secret corev1.Secret - // Explicitly log the secret lookup attempt - if err := r.Get(ctx, types.NamespacedName{Name: heliosApp.Spec.GitOpsSecretRef, Namespace: heliosApp.Namespace}, &secret); err == nil { - if t, ok := secret.Data["token"]; ok { - token = string(t) - } else if p, ok := secret.Data["password"]; ok { - // Fallback to 'password' key (standard basic-auth secret from Tekton setup) - token = string(p) - } else { - log.Info("Secret found but 'token' or 'password' key is missing", "Secret", heliosApp.Spec.GitOpsSecretRef) - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s missing 'token' key", heliosApp.Spec.GitOpsSecretRef)) - return ctrl.Result{}, nil - } - if u, ok := secret.Data["username"]; ok { - username = string(u) - } - } else { - log.Error(err, "Failed to get GitOps Secret", "Secret", heliosApp.Spec.GitOpsSecretRef) - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s not found", heliosApp.Spec.GitOpsSecretRef)) - return ctrl.Result{}, nil - } - } - - // 5. GitOps Sync - - if token == "" { - err := fmt.Errorf("GitOps token is empty. Check Secret or GITHUB_TOKEN env var") - log.Error(err, "Authentication failed") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, "GitOps token missing") - return ctrl.Result{}, nil // Don't retry immediately if config is missing - } - - // OPTIMIZATION: Check Hash - currentHash := r.computeHash(manifestBytes) - if heliosApp.Status.LastAppliedHash == currentHash { - log.Info("Manifest hash unchanged, skipping GitOps sync", "hash", currentHash) - - // Still ensure status is Ready if it was previously set - if heliosApp.Status.Phase != appv1alpha1.PhaseReady { - heliosApp.Status.Phase = appv1alpha1.PhaseReady - if err := r.Status().Update(ctx, &heliosApp); err != nil { - return ctrl.Result{}, err - } - } - } else { - // Use GitFactory if available, otherwise default to NewGitOpsClient - getGitClient := r.GitFactory - if getGitClient == nil { - getGitClient = func(repo, user, token string) gitops.GitOpsClientInterface { - return gitops.NewGitOpsClient(repo, user, token) - } - } - - gitClient := getGitClient(heliosApp.Spec.GitOpsRepo, username, token) - targetPath := fmt.Sprintf("%s/manifest.yaml", heliosApp.Spec.GitOpsPath) - - if err := gitClient.SyncManifest(ctx, targetPath, string(manifestBytes)); err != nil { - log.Error(err, "GitOps sync failed") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("GitOps failed: %v", err)) - return ctrl.Result{}, err - } - - // 6. Update Status - heliosApp.Status.Phase = appv1alpha1.PhaseReady - heliosApp.Status.Message = fmt.Sprintf("Manifest pushed to %s/%s", heliosApp.Spec.GitOpsRepo, targetPath) - heliosApp.Status.LastAppliedHash = currentHash - // We clear ResourcesCreated as we are not managing them directly anymore - heliosApp.Status.ResourcesCreated = nil - - if err := r.Status().Update(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to update status") - return ctrl.Result{}, err - } - log.Info("Successfully reconciled HeliosApp via GitOps", "newHash", currentHash) - } - - // 7. Ensure ArgoCD Application exists - log.Info("Ensuring ArgoCD Application exists") - argoApp, err := GenerateArgoApplication(&heliosApp) - if err != nil { - log.Error(err, "Failed to generate ArgoCD Application manifest") - // We don't return error here to avoid loop if GitOps was successful, just log it. - // Or maybe we should retry? Let's log and continue for now. - } else { - // Define ArgoCD Application identity - argoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) - // We use Sever-Side Apply or Create/Update logic - // Since ArgoCD app is in "argocd" namespace usually, we need permissions there. - // For simplicity/demo: Try to get, if not found create. - - foundArgoApp := &unstructured.Unstructured{} - foundArgoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) - - key := client.ObjectKey{ - Name: argoApp.GetName(), - Namespace: argoApp.GetNamespace(), - } - - if err := r.Client.Get(ctx, key, foundArgoApp); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating ArgoCD Application", "name", argoApp.GetName()) - if err := r.Client.Create(ctx, argoApp); err != nil { - log.Error(err, "Failed to create ArgoCD Application") - } - } else { - log.Error(err, "Failed to get ArgoCD Application") - } - } else { - // Optional: Update if needed (checkout 'spec' diff) - log.Info("ArgoCD Application already exists", "name", argoApp.GetName()) - } - } - - // NOTE: Ingress removed - use port-forwarding for EventListener: - // kubectl port-forward svc/el--listener 8080:8080 - - return ctrl.Result{}, nil -} - -// mapCRDToModel converts HeliosApp CRD to CUE Application Model -func (r *HeliosAppReconciler) mapCRDToModel(app *appv1alpha1.HeliosApp) (heliosCue.Application, error) { - components := make([]heliosCue.Component, len(app.Spec.Components)) - - for i, c := range app.Spec.Components { - // Parse properties from RawExtension - var props map[string]any - if c.Properties != nil && c.Properties.Raw != nil { - if err := json.Unmarshal(c.Properties.Raw, &props); err != nil { - return heliosCue.Application{}, fmt.Errorf("failed to parse component properties: %w", err) - } - } - - // Parse traits - traits := make([]heliosCue.Trait, len(c.Traits)) - for j, t := range c.Traits { - var traitProps map[string]any - if t.Properties != nil && t.Properties.Raw != nil { - if err := json.Unmarshal(t.Properties.Raw, &traitProps); err != nil { - return heliosCue.Application{}, fmt.Errorf("failed to parse trait properties: %w", err) - } - } - traits[j] = heliosCue.Trait{ - Type: t.Type, - Properties: traitProps, - } - } - - components[i] = heliosCue.Component{ - Name: c.Name, - Type: c.Type, - Properties: props, - Traits: traits, - } - } - - return heliosCue.Application{ - App: heliosCue.AppSpec{ - Name: app.Name, - Namespace: app.Namespace, - Owner: app.Spec.Owner, - Description: app.Spec.Description, - Components: components, - }, - }, nil -} - -// mapCRDToTektonInput converts HeliosApp CRD to TektonInput for CUE rendering. -// This is the bridge between HeliosApp spec fields and the CUE #TektonInput schema. -func (r *HeliosAppReconciler) mapCRDToTektonInput(app *appv1alpha1.HeliosApp) heliosCue.TektonInput { - input := heliosCue.TektonInput{ - AppName: app.Name, - Namespace: app.Namespace, - GitRepo: app.Spec.GitRepo, - GitBranch: app.Spec.GitBranch, - ImageRepo: app.Spec.ImageRepo, - GitOpsRepo: app.Spec.GitOpsRepo, - GitOpsPath: app.Spec.GitOpsPath, - GitOpsBranch: app.Spec.GitOpsBranch, - GitOpsSecretRef: app.Spec.GitOpsSecretRef, - WebhookDomain: app.Spec.WebhookDomain, - WebhookSecret: app.Spec.WebhookSecret, - PipelineName: app.Spec.PipelineName, - PipelineType: app.Spec.PipelineName, // pipelineType uses same value as pipelineName - TriggerType: "github-push", // Default; extend HeliosAppSpec if needed - ServiceAccount: app.Spec.ServiceAccount, - PVCName: app.Spec.PVCName, - ContextSubpath: app.Spec.ContextSubpath, - Replicas: int(app.Spec.Replicas), - Port: int(app.Spec.Port), - TestCommand: app.Spec.TestCommand, - DockerSecret: "docker-credentials", - ArgoCDNamespace: app.Spec.ArgoCDNamespace, - ArgoCDProject: app.Spec.ArgoCDProject, - } - - // Apply defaults for fields that may be empty - input.GitBranch = cmp.Or(input.GitBranch, "main") - input.GitOpsBranch = cmp.Or(input.GitOpsBranch, "main") - input.GitOpsSecretRef = cmp.Or(input.GitOpsSecretRef, "github-credentials") - input.WebhookSecret = cmp.Or(input.WebhookSecret, "github-webhook-secret") - if input.PipelineName == "" { - input.PipelineName = "from-code-to-cluster" - input.PipelineType = "from-code-to-cluster" - } - input.ServiceAccount = cmp.Or(input.ServiceAccount, "default") - if input.Replicas <= 0 { - input.Replicas = 1 - } - if input.Port <= 0 { - input.Port = 8080 - } - - return input -} - -// reconcileTektonResourcesCue renders Tekton resources via CUE and applies them. -// This is the NEW path that replaces all hardcoded Generate* functions. -func (r *HeliosAppReconciler) reconcileTektonResourcesCue(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - // 1. Map CRD → TektonInput - tektonInput := r.mapCRDToTektonInput(app) - - // 2. Render via CUE - objects, err := r.TektonRenderer.RenderTektonResources(tektonInput) - if err != nil { - return fmt.Errorf("CUE TektonRenderer failed: %w", err) - } - - log.Info("CUE rendered Tekton resources", "count", len(objects)) - - // 3. Apply each rendered resource - for _, obj := range objects { - // Set owner reference (skip cluster-scoped resources) - if obj.GetNamespace() != "" { - if err := ctrl.SetControllerReference(app, obj, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference", "kind", obj.GetKind(), "name", obj.GetName()) - continue - } - } - - // Create or update - found := &unstructured.Unstructured{} - found.SetGroupVersionKind(obj.GroupVersionKind()) - err := r.Client.Get(ctx, client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}, found) - if err != nil { - if errors.IsNotFound(err) { - log.Info("Creating resource", "kind", obj.GetKind(), "name", obj.GetName()) - if err := r.Client.Create(ctx, obj); err != nil { - log.Error(err, "Failed to create resource", "kind", obj.GetKind(), "name", obj.GetName()) - } - } else { - log.Error(err, "Failed to get resource", "kind", obj.GetKind(), "name", obj.GetName()) - } - } else { - // Update existing resource's spec - found.Object["spec"] = obj.Object["spec"] - if err := r.Client.Update(ctx, found); err != nil { - log.Error(err, "Failed to update resource", "kind", obj.GetKind(), "name", obj.GetName()) - } - } - } - - // 4. Also ensure RBAC (SA, RoleBinding, ClusterRoleBinding) — these are not in CUE yet - r.ensureTektonRBAC(ctx, app) - - return nil -} - -// ensureTektonRBAC creates ServiceAccount, RoleBinding, ClusterRoleBinding. -// These are infrastructure resources not managed by CUE (they are cluster lifecycle, not app lifecycle). -func (r *HeliosAppReconciler) ensureTektonRBAC(ctx context.Context, app *appv1alpha1.HeliosApp) { - log := logf.FromContext(ctx) - - sa := GenerateServiceAccount(app.Namespace) - if err := ctrl.SetControllerReference(app, sa, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for ServiceAccount") - } else { - foundSA := &unstructured.Unstructured{} - foundSA.SetGroupVersionKind(sa.GroupVersionKind()) - if err := r.Client.Get(ctx, client.ObjectKey{Name: sa.GetName(), Namespace: sa.GetNamespace()}, foundSA); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating ServiceAccount", "name", sa.GetName()) - r.Client.Create(ctx, sa) - } - } - } - - rb := GenerateRoleBinding(app.Namespace) - if err := ctrl.SetControllerReference(app, rb, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for RoleBinding") - } else { - foundRB := &unstructured.Unstructured{} - foundRB.SetGroupVersionKind(rb.GroupVersionKind()) - if err := r.Client.Get(ctx, client.ObjectKey{Name: rb.GetName(), Namespace: rb.GetNamespace()}, foundRB); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating RoleBinding", "name", rb.GetName()) - r.Client.Create(ctx, rb) - } - } - } - - crb := GenerateClusterRoleBinding(app.Namespace) - foundCrb := &unstructured.Unstructured{} - foundCrb.SetGroupVersionKind(crb.GroupVersionKind()) - if err := r.Client.Get(ctx, client.ObjectKey{Name: crb.GetName()}, foundCrb); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating ClusterRoleBinding", "name", crb.GetName()) - r.Client.Create(ctx, crb) - } - } -} - -// updateStatus updates the HeliosApp status -func (r *HeliosAppReconciler) updateStatus(ctx context.Context, app *appv1alpha1.HeliosApp, phase appv1alpha1.HeliosAppPhase, message string) { - app.Status.Phase = phase - app.Status.Message = message - if err := r.Status().Update(ctx, app); err != nil { - logf.FromContext(ctx).Error(err, "Failed to update status") - } -} - -// computeHash returns SHA256 of data -func (r *HeliosAppReconciler) computeHash(data []byte) string { - hash := sha256.Sum256(data) - return hex.EncodeToString(hash[:]) -} - -// SetupWithManager sets up the controller with the Manager -func (r *HeliosAppReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appv1alpha1.HeliosApp{}). - Owns(&appsv1.Deployment{}). - Owns(&appsv1.StatefulSet{}). - Owns(&corev1.Service{}). - Owns(&networkingv1.Ingress{}). - Watches( - &corev1.Secret{}, - handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret), - ). - Named("heliosapp"). - Complete(r) -} - -// findObjectsForSecret maps Secret changes to HeliosApp reconcile requests. -// This ensures the controller re-reconciles when a referenced secret changes. -func (r *HeliosAppReconciler) findObjectsForSecret(ctx context.Context, obj client.Object) []reconcile.Request { - log := logf.FromContext(ctx) - - // List all HeliosApps in the same namespace - var heliosAppList appv1alpha1.HeliosAppList - if err := r.List(ctx, &heliosAppList, client.InNamespace(obj.GetNamespace())); err != nil { - log.Error(err, "Failed to list HeliosApps for secret watch") - return nil - } - - var requests []reconcile.Request - for _, app := range heliosAppList.Items { - // Check if this app references the changed secret - if app.Spec.GitOpsSecretRef == obj.GetName() || - app.Spec.WebhookSecret == obj.GetName() { - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: app.Name, - Namespace: app.Namespace, - }, - }) - } - } - - return requests -} -=== -/* -Copyright 2026. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "cmp" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "os" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - appv1alpha1 "github.com/helios-platform-team/helios-platform/apps/operator/api/v1alpha1" - heliosCue "github.com/helios-platform-team/helios-platform/apps/operator/internal/cue" - "github.com/helios-platform-team/helios-platform/apps/operator/internal/gitops" -) - -// HeliosAppReconciler reconciles a HeliosApp object -type HeliosAppReconciler struct { - client.Client - Scheme *runtime.Scheme - CueEngine heliosCue.CueEngineInterface - // TektonRenderer renders Tekton CI/CD resources via CUE engine. - TektonRenderer heliosCue.TektonRendererInterface - // GitFactory allows injecting a custom GitOps client (e.g. for testing) - GitFactory func(string, string, string) gitops.GitOpsClientInterface -} - -// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=app.helios.io,resources=heliosapps/finalizers,verbs=update -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete - -// Reconcile handles the reconciliation loop for HeliosApp -// Controller does NOT iterate components/traits - all orchestration is in CUE -func (r *HeliosAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := logf.FromContext(ctx) - - // 1. Fetch HeliosApp CRD - var heliosApp appv1alpha1.HeliosApp - if err := r.Get(ctx, req.NamespacedName, &heliosApp); err != nil { - if errors.IsNotFound(err) { - log.Info("HeliosApp resource not found, ignoring") - return ctrl.Result{}, nil - } - return ctrl.Result{}, err - } - - log.Info("Reconciling HeliosApp", "name", heliosApp.Name, "namespace", heliosApp.Namespace) - - // 2. Map CRD to Application Model - appModel, err := r.mapCRDToModel(&heliosApp) - if err != nil { - log.Error(err, "Failed to map CRD to application model") - return ctrl.Result{}, err - } - - // 3. Render via CUE Engine - manifestBytes, err := r.CueEngine.Render(appModel) - if err != nil { - log.Error(err, "Failed to render application via CUE") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE rendering failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE -1 & 0: Tekton CI/CD Resources (Tasks, Pipeline, Triggers) - // All Tekton resources are rendered via CUE engine. - // ------------------------------------------------------------------ - if err := r.reconcileTektonResourcesCue(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to reconcile Tekton resources via CUE") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("CUE Tekton rendering failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE 0.5: Database Credential Secrets - // Generate and store secure credentials for components with database traits. - // Secrets are created BEFORE GitOps sync to ensure credentials exist - // when the application is deployed. - // ------------------------------------------------------------------ - if err := r.reconcileDatabaseSecrets(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to reconcile database secrets") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret creation failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE 0.7: Database Instance Provisioning - // Provision StatefulSets and headless Services for database traits. - // Runs AFTER secrets so that the credential Secret already exists - // when the database pod starts. - // ------------------------------------------------------------------ - if err := r.reconcileDatabaseInstance(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to reconcile database instance") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database instance provisioning failed: %v", err)) - return ctrl.Result{}, err - } - - // ------------------------------------------------------------------ - // PHASE 0.9: Inject Database Credentials into Backend Deployment - // Patches the live Deployment (deployed by ArgoCD) to add DB_HOST, - // DB_USER, DB_PASS env vars referencing the operator-managed Secret. - // Runs AFTER secrets and instances so the Secret already exists. - // ------------------------------------------------------------------ - if err := r.reconcileDatabaseSecretInjection(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to inject database secrets into Deployment") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Database secret injection failed: %v", err)) - return ctrl.Result{}, err - } - - // VALIDATION: Ensure image is present (Fix "First Commit Missing Image") - // This validation is for application workloads (GitOps pipeline downstream). - // We run this AFTER DB provisioning so databases can come up while app is building. - for _, comp := range appModel.App.Components { - // We can add more specific checks here based on component type - // For now, checks if 'image' property exists and is not empty for all components - // assuming all workloads need an image. - if img, ok := comp.Properties["image"].(string); !ok || img == "" { - msg := fmt.Sprintf("Component '%s' is waiting for image (likely building). Status: Pending.", comp.Name) - log.Info(msg) - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhasePending, msg) - return ctrl.Result{}, nil // Wait for next update (CI/CD will update CR with image) - } - } - - // ------------------------------------------------------------------ - // PHASE 0.6: Trigger Initial PipelineRun (if not already done) - // ------------------------------------------------------------------ - if !heliosApp.Status.InitialBuildTriggered { - log.Info("Triggering initial PipelineRun for new HeliosApp") - - pipelineName := heliosApp.Spec.PipelineName - if pipelineName == "" { - pipelineName = "from-code-to-cluster" - } - pr, err := GeneratePipelineRunForManifestGeneration(&heliosApp, pipelineName) - if err != nil { - log.Error(err, "Failed to generate initial PipelineRun") - } else { - if err := ctrl.SetControllerReference(&heliosApp, pr, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for PipelineRun") - } - - if err := r.Client.Create(ctx, pr); err != nil { - if !errors.IsAlreadyExists(err) { - log.Error(err, "Failed to create initial PipelineRun") - } - } else { - log.Info("Created initial PipelineRun", "name", pr.GetName()) - } - - // Mark as triggered to avoid creating multiple PipelineRuns - heliosApp.Status.InitialBuildTriggered = true - if err := r.Status().Update(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to update InitialBuildTriggered status") - } - } - } - - // ------------------------------------------------------------------ - // PHASE 1: Render & GitOps (Moved below) - // ------------------------------------------------------------------ - - // 4. GitOps Helper: Get Token & Username - token := os.Getenv("GITHUB_TOKEN") - username := os.Getenv("GITHUB_USER") - if username == "" { - username = "git" // Default fallback - } - - if heliosApp.Spec.GitOpsSecretRef != "" { - var secret corev1.Secret - // Explicitly log the secret lookup attempt - if err := r.Get(ctx, types.NamespacedName{Name: heliosApp.Spec.GitOpsSecretRef, Namespace: heliosApp.Namespace}, &secret); err == nil { - if t, ok := secret.Data["token"]; ok { - token = string(t) - } else if p, ok := secret.Data["password"]; ok { - // Fallback to 'password' key (standard basic-auth secret from Tekton setup) - token = string(p) - } else { - log.Info("Secret found but 'token' or 'password' key is missing", "Secret", heliosApp.Spec.GitOpsSecretRef) - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s missing 'token' key", heliosApp.Spec.GitOpsSecretRef)) - return ctrl.Result{}, nil - } - if u, ok := secret.Data["username"]; ok { - username = string(u) - } - } else { - log.Error(err, "Failed to get GitOps Secret", "Secret", heliosApp.Spec.GitOpsSecretRef) - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("Secret %s not found", heliosApp.Spec.GitOpsSecretRef)) - return ctrl.Result{}, nil - } - } - - // 5. GitOps Sync - - if token == "" { - err := fmt.Errorf("GitOps token is empty. Check Secret or GITHUB_TOKEN env var") - log.Error(err, "Authentication failed") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, "GitOps token missing") - return ctrl.Result{}, nil // Don't retry immediately if config is missing - } - - // OPTIMIZATION: Check Hash - currentHash := r.computeHash(manifestBytes) - if heliosApp.Status.LastAppliedHash == currentHash { - log.Info("Manifest hash unchanged, skipping GitOps sync", "hash", currentHash) - - // Still ensure status is Ready if it was previously set - if heliosApp.Status.Phase != appv1alpha1.PhaseReady { - heliosApp.Status.Phase = appv1alpha1.PhaseReady - if err := r.Status().Update(ctx, &heliosApp); err != nil { - return ctrl.Result{}, err - } - } - } else { - // Use GitFactory if available, otherwise default to NewGitOpsClient - getGitClient := r.GitFactory - if getGitClient == nil { - getGitClient = func(repo, user, token string) gitops.GitOpsClientInterface { - return gitops.NewGitOpsClient(repo, user, token) - } - } - - gitClient := getGitClient(heliosApp.Spec.GitOpsRepo, username, token) - targetPath := fmt.Sprintf("%s/manifest.yaml", heliosApp.Spec.GitOpsPath) - - if err := gitClient.SyncManifest(ctx, targetPath, string(manifestBytes)); err != nil { - log.Error(err, "GitOps sync failed") - r.updateStatus(ctx, &heliosApp, appv1alpha1.PhaseFailed, fmt.Sprintf("GitOps failed: %v", err)) - return ctrl.Result{}, err - } - - // 6. Update Status - heliosApp.Status.Phase = appv1alpha1.PhaseReady - heliosApp.Status.Message = fmt.Sprintf("Manifest pushed to %s/%s", heliosApp.Spec.GitOpsRepo, targetPath) - heliosApp.Status.LastAppliedHash = currentHash - // We clear ResourcesCreated as we are not managing them directly anymore - heliosApp.Status.ResourcesCreated = nil - - if err := r.Status().Update(ctx, &heliosApp); err != nil { - log.Error(err, "Failed to update status") - return ctrl.Result{}, err - } - log.Info("Successfully reconciled HeliosApp via GitOps", "newHash", currentHash) - } - - // 7. Ensure ArgoCD Application exists - log.Info("Ensuring ArgoCD Application exists") - argoApp, err := GenerateArgoApplication(&heliosApp) - if err != nil { - log.Error(err, "Failed to generate ArgoCD Application manifest") - // We don't return error here to avoid loop if GitOps was successful, just log it. - // Or maybe we should retry? Let's log and continue for now. - } else { - // Define ArgoCD Application identity - argoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) - // We use Sever-Side Apply or Create/Update logic - // Since ArgoCD app is in "argocd" namespace usually, we need permissions there. - // For simplicity/demo: Try to get, if not found create. - - foundArgoApp := &unstructured.Unstructured{} - foundArgoApp.SetGroupVersionKind(argoApp.GroupVersionKind()) - - key := client.ObjectKey{ - Name: argoApp.GetName(), - Namespace: argoApp.GetNamespace(), - } - - if err := r.Client.Get(ctx, key, foundArgoApp); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating ArgoCD Application", "name", argoApp.GetName()) - if err := r.Client.Create(ctx, argoApp); err != nil { - log.Error(err, "Failed to create ArgoCD Application") - } - } else { - log.Error(err, "Failed to get ArgoCD Application") - } - } else { - // Optional: Update if needed (checkout 'spec' diff) - log.Info("ArgoCD Application already exists", "name", argoApp.GetName()) - } - } - - // NOTE: Ingress removed - use port-forwarding for EventListener: - // kubectl port-forward svc/el--listener 8080:8080 - - return ctrl.Result{}, nil -} - -// mapCRDToModel converts HeliosApp CRD to CUE Application Model -func (r *HeliosAppReconciler) mapCRDToModel(app *appv1alpha1.HeliosApp) (heliosCue.Application, error) { - components := make([]heliosCue.Component, len(app.Spec.Components)) - - for i, c := range app.Spec.Components { - // Parse properties from RawExtension - var props map[string]any - if c.Properties != nil && c.Properties.Raw != nil { - if err := json.Unmarshal(c.Properties.Raw, &props); err != nil { - return heliosCue.Application{}, fmt.Errorf("failed to parse component properties: %w", err) - } - } - - // Parse traits - traits := make([]heliosCue.Trait, len(c.Traits)) - for j, t := range c.Traits { - var traitProps map[string]any - if t.Properties != nil && t.Properties.Raw != nil { - if err := json.Unmarshal(t.Properties.Raw, &traitProps); err != nil { - return heliosCue.Application{}, fmt.Errorf("failed to parse trait properties: %w", err) - } - } - traits[j] = heliosCue.Trait{ - Type: t.Type, - Properties: traitProps, - } - } - - components[i] = heliosCue.Component{ - Name: c.Name, - Type: c.Type, - Properties: props, - Traits: traits, - } - } - - return heliosCue.Application{ - App: heliosCue.AppSpec{ - Name: app.Name, - Namespace: app.Namespace, - Owner: app.Spec.Owner, - Description: app.Spec.Description, - Components: components, - }, - }, nil -} - -// mapCRDToTektonInput converts HeliosApp CRD to TektonInput for CUE rendering. -// This is the bridge between HeliosApp spec fields and the CUE #TektonInput schema. -func (r *HeliosAppReconciler) mapCRDToTektonInput(app *appv1alpha1.HeliosApp) heliosCue.TektonInput { - input := heliosCue.TektonInput{ - AppName: app.Name, - Namespace: app.Namespace, - GitRepo: app.Spec.GitRepo, - GitBranch: app.Spec.GitBranch, - ImageRepo: app.Spec.ImageRepo, - GitOpsRepo: app.Spec.GitOpsRepo, - GitOpsPath: app.Spec.GitOpsPath, - GitOpsBranch: app.Spec.GitOpsBranch, - GitOpsSecretRef: app.Spec.GitOpsSecretRef, - WebhookDomain: app.Spec.WebhookDomain, - WebhookSecret: app.Spec.WebhookSecret, - PipelineName: app.Spec.PipelineName, - PipelineType: app.Spec.PipelineName, // pipelineType uses same value as pipelineName - TriggerType: "github-push", // Default; extend HeliosAppSpec if needed - ServiceAccount: app.Spec.ServiceAccount, - PVCName: app.Spec.PVCName, - ContextSubpath: app.Spec.ContextSubpath, - Replicas: int(app.Spec.Replicas), - Port: int(app.Spec.Port), - TestCommand: app.Spec.TestCommand, - DockerSecret: "docker-credentials", - ArgoCDNamespace: app.Spec.ArgoCDNamespace, - ArgoCDProject: app.Spec.ArgoCDProject, - } - - // Apply defaults for fields that may be empty - input.GitBranch = cmp.Or(input.GitBranch, "main") - input.GitOpsBranch = cmp.Or(input.GitOpsBranch, "main") - input.GitOpsSecretRef = cmp.Or(input.GitOpsSecretRef, "github-credentials") - input.WebhookSecret = cmp.Or(input.WebhookSecret, "github-webhook-secret") - if input.PipelineName == "" { - input.PipelineName = "from-code-to-cluster" - input.PipelineType = "from-code-to-cluster" - } - input.ServiceAccount = cmp.Or(input.ServiceAccount, "default") - if input.Replicas <= 0 { - input.Replicas = 1 - } - if input.Port <= 0 { - input.Port = 8080 - } - - return input -} - -// reconcileTektonResourcesCue renders Tekton resources via CUE and applies them. -// This is the NEW path that replaces all hardcoded Generate* functions. -func (r *HeliosAppReconciler) reconcileTektonResourcesCue(ctx context.Context, app *appv1alpha1.HeliosApp) error { - log := logf.FromContext(ctx) - - // 1. Map CRD → TektonInput - tektonInput := r.mapCRDToTektonInput(app) - - // 2. Render via CUE - objects, err := r.TektonRenderer.RenderTektonResources(tektonInput) - if err != nil { - return fmt.Errorf("CUE TektonRenderer failed: %w", err) - } - - log.Info("CUE rendered Tekton resources", "count", len(objects)) - - // 3. Apply each rendered resource - for _, obj := range objects { - // Set owner reference (skip cluster-scoped resources) - if obj.GetNamespace() != "" { - if err := ctrl.SetControllerReference(app, obj, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference", "kind", obj.GetKind(), "name", obj.GetName()) - continue - } - } - - // Create or update - found := &unstructured.Unstructured{} - found.SetGroupVersionKind(obj.GroupVersionKind()) - err := r.Client.Get(ctx, client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}, found) - if err != nil { - if errors.IsNotFound(err) { - log.Info("Creating resource", "kind", obj.GetKind(), "name", obj.GetName()) - if err := r.Client.Create(ctx, obj); err != nil { - log.Error(err, "Failed to create resource", "kind", obj.GetKind(), "name", obj.GetName()) - } - } else { - log.Error(err, "Failed to get resource", "kind", obj.GetKind(), "name", obj.GetName()) - } - } else { - // Update existing resource's spec - found.Object["spec"] = obj.Object["spec"] - if err := r.Client.Update(ctx, found); err != nil { - log.Error(err, "Failed to update resource", "kind", obj.GetKind(), "name", obj.GetName()) - } - } - } - - // 4. Also ensure RBAC (SA, RoleBinding, ClusterRoleBinding) — these are not in CUE yet - r.ensureTektonRBAC(ctx, app) - - return nil -} - -// ensureTektonRBAC creates ServiceAccount, RoleBinding, ClusterRoleBinding. -// These are infrastructure resources not managed by CUE (they are cluster lifecycle, not app lifecycle). -func (r *HeliosAppReconciler) ensureTektonRBAC(ctx context.Context, app *appv1alpha1.HeliosApp) { - log := logf.FromContext(ctx) - - sa := GenerateServiceAccount(app.Namespace) - if err := ctrl.SetControllerReference(app, sa, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for ServiceAccount") - } else { - foundSA := &unstructured.Unstructured{} - foundSA.SetGroupVersionKind(sa.GroupVersionKind()) - if err := r.Client.Get(ctx, client.ObjectKey{Name: sa.GetName(), Namespace: sa.GetNamespace()}, foundSA); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating ServiceAccount", "name", sa.GetName()) - r.Client.Create(ctx, sa) - } - } - } - - rb := GenerateRoleBinding(app.Namespace) - if err := ctrl.SetControllerReference(app, rb, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference for RoleBinding") - } else { - foundRB := &unstructured.Unstructured{} - foundRB.SetGroupVersionKind(rb.GroupVersionKind()) - if err := r.Client.Get(ctx, client.ObjectKey{Name: rb.GetName(), Namespace: rb.GetNamespace()}, foundRB); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating RoleBinding", "name", rb.GetName()) - r.Client.Create(ctx, rb) - } - } - } - - crb := GenerateClusterRoleBinding(app.Namespace) - foundCrb := &unstructured.Unstructured{} - foundCrb.SetGroupVersionKind(crb.GroupVersionKind()) - if err := r.Client.Get(ctx, client.ObjectKey{Name: crb.GetName()}, foundCrb); err != nil { - if errors.IsNotFound(err) { - log.Info("Creating ClusterRoleBinding", "name", crb.GetName()) - r.Client.Create(ctx, crb) - } - } -} - -// updateStatus updates the HeliosApp status -func (r *HeliosAppReconciler) updateStatus(ctx context.Context, app *appv1alpha1.HeliosApp, phase appv1alpha1.HeliosAppPhase, message string) { - app.Status.Phase = phase - app.Status.Message = message - if err := r.Status().Update(ctx, app); err != nil { - logf.FromContext(ctx).Error(err, "Failed to update status") - } -} - -// computeHash returns SHA256 of data -func (r *HeliosAppReconciler) computeHash(data []byte) string { - hash := sha256.Sum256(data) - return hex.EncodeToString(hash[:]) -} - -// SetupWithManager sets up the controller with the Manager -func (r *HeliosAppReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appv1alpha1.HeliosApp{}). - Owns(&appsv1.Deployment{}). - Owns(&appsv1.StatefulSet{}). - Owns(&corev1.Service{}). - Owns(&networkingv1.Ingress{}). - Watches( - &corev1.Secret{}, - handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret), - ). - Named("heliosapp"). - Complete(r) -} - -// findObjectsForSecret maps Secret changes to HeliosApp reconcile requests. -// This ensures the controller re-reconciles when a referenced secret changes. -func (r *HeliosAppReconciler) findObjectsForSecret(ctx context.Context, obj client.Object) []reconcile.Request { - log := logf.FromContext(ctx) - - // List all HeliosApps in the same namespace - var heliosAppList appv1alpha1.HeliosAppList - if err := r.List(ctx, &heliosAppList, client.InNamespace(obj.GetNamespace())); err != nil { - log.Error(err, "Failed to list HeliosApps for secret watch") - return nil - } - - var requests []reconcile.Request - for _, app := range heliosAppList.Items { - // Check if this app references the changed secret - if app.Spec.GitOpsSecretRef == obj.GetName() || - app.Spec.WebhookSecret == obj.GetName() { - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: app.Name, - Namespace: app.Namespace, - }, - }) - } - } - - return requests -} -``` - -#### [database_resources_test.go](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/operator/internal/controller/database_resources_test.go) - -6 new tests: -| Test | Description | -|------|-------------| -| `TestInjectDatabaseEnvVars/InjectsAllEnvVars` | Verifies DB_HOST, DB_USER, DB_PASS injected with correct secretKeyRef | -| `TestInjectDatabaseEnvVars/Idempotent` | Running twice doesn't duplicate env vars | -| `TestInjectDatabaseEnvVars/NoContainers` | Handles edge case of empty container list | -| `TestReconcileDatabaseSecretInjection/InjectsIntoExistingDeployment` | Full reconciliation with fake K8s client | -| `TestReconcileDatabaseSecretInjection/SkipsWhenNoTraits` | No-op when no database traits | -| `TestReconcileDatabaseSecretInjection/DeploymentNotFound_GracefulSkip` | Graceful skip when ArgoCD hasn't deployed yet | - ---- - -### 2. NestJS + Prisma Template - -Created [nestjs-prisma-template/](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template) with: - -| File | Purpose | -|------|---------| -| [template.yaml](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/template/template.yaml) | Backstage scaffolder template with DatabasePicker | -| [content/source/package.json](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/advanced-template/content/source/package.json) | NestJS v11+ & Prisma v6+ dependencies | -| [content/source/prisma/schema.prisma](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/prisma/schema.prisma) | Prisma schema using `env("DATABASE_URL")` | -| [content/source/src/prisma/prisma.service.ts](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.service.ts) | **Key file** — constructs `DATABASE_URL` from `DB_HOST`, `DB_USER`, `DB_PASS` with `encodeURIComponent` | -| [content/source/src/prisma/prisma.module.ts](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/source/src/prisma/prisma.module.ts) | Global module exporting PrismaService | -| [content/source/Dockerfile](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/advanced-template/content/source/Dockerfile) | Multi-stage build, runs `prisma migrate deploy` at startup | -| [content/gitops/helios-app.yaml](file:///home/phuochoan/Workspace/HCMUS/4th_Year/Capstone_Projects/helios-platform/apps/portal/examples/nestjs-prisma-template/content/gitops/helios-app.yaml) | HeliosApp CRD with database trait | - ---- - -## Verification Results - -| Check | Result | -|-------|--------| -| `go build ./...` | ✅ Passes | -| `go vet ./...` | ✅ No issues | -| `make test` | ✅ All tests pass (68% controller coverage) | -| `make build` | ✅ Operator binary builds |