diff --git a/checks/locks/go.mod b/checks/locks/go.mod new file mode 100644 index 00000000..d76e7992 --- /dev/null +++ b/checks/locks/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/locks + +go 1.22 diff --git a/checks/locks/main.go b/checks/locks/main.go new file mode 100644 index 00000000..e0b5c582 --- /dev/null +++ b/checks/locks/main.go @@ -0,0 +1,105 @@ +// Command locks refuses a TypeScript module that names packages beyond the SDK in its package.json and commits no +// package-lock.json (novox/hq ADR 0469 §5, issue 1868): the Node build module installs a module's own packages only +// from its committed lock, so such a module is refused by the builder and could not be delivered. +// +// go run . +// +// Exits 1 on a finding. The packages that count are those the build module counts (mesh-build-node fetch.go +// ownDependencies): the `dependencies` beyond the SDK, typescript and the @types packages. +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// Finding is one module refused: it names Packages beyond the SDK and has no lock file. +type Finding struct { + Module string + Packages []string +} + +// Check reads every module under root/modules that builds a TypeScript artifact. +func Check(root string) ([]Finding, error) { + manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) + if err != nil { + return nil, err + } + sort.Strings(manifests) + var out []Finding + for _, path := range manifests { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var m struct { + Module string `json:"module"` + Build struct { + Artifacts []struct { + Language string `json:"language"` + } `json:"artifacts"` + } `json:"build"` + } + if err := json.Unmarshal(raw, &m); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + typescript := false + for _, a := range m.Build.Artifacts { + typescript = typescript || a.Language == "typescript" + } + if !typescript { + continue + } + dir := filepath.Dir(path) + pkgRaw, err := os.ReadFile(filepath.Join(dir, "package.json")) + if os.IsNotExist(err) { + continue + } + if err != nil { + return nil, err + } + var pkg struct { + Dependencies map[string]json.RawMessage `json:"dependencies"` + } + if err := json.Unmarshal(pkgRaw, &pkg); err != nil { + return nil, fmt.Errorf("%s: package.json: %w", m.Module, err) + } + var names []string + for n := range pkg.Dependencies { + if n != "@novox/mesh-sdk" && n != "typescript" && !strings.HasPrefix(n, "@types/") { + names = append(names, n) + } + } + if len(names) == 0 { + continue + } + if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); err != nil { + sort.Strings(names) + out = append(out, Finding{Module: m.Module, Packages: names}) + } + } + return out, nil +} + +func main() { + root := "." + if len(os.Args) > 1 { + root = os.Args[1] + } + found, err := Check(root) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + for _, f := range found { + fmt.Printf("%s names packages beyond the SDK (%s) and has no package-lock.json: commit one made by the build module's toolchain (novox/hq ADR 0469 §5)\n", f.Module, strings.Join(f.Packages, ", ")) + } + if len(found) > 0 { + os.Exit(1) + } + fmt.Println("locks: every TypeScript module that names packages beyond the SDK commits its lock") +} diff --git a/checks/locks/main_test.go b/checks/locks/main_test.go new file mode 100644 index 00000000..958a706f --- /dev/null +++ b/checks/locks/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "testing" +) + +// module writes modules/ with a TypeScript artifact, the package.json given ("" for none) and, when lock, a lock. +func module(t *testing.T, root, name, language, pkg string, lock bool) { + t.Helper() + dir := filepath.Join(root, "modules", name) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + write := func(file, body string) { + if err := os.WriteFile(filepath.Join(dir, file), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + write("module.json", `{"module":"`+name+`","build":{"artifacts":[{"name":"tools","kind":"bundle","language":"`+language+`","from":"index.ts"}]}}`) + if pkg != "" { + write("package.json", pkg) + } + if lock { + write("package-lock.json", "{}") + } +} + +const sdkOnly = `{"dependencies":{"@novox/mesh-sdk":"^0.1.1"},"devDependencies":{"typescript":"^5","@types/node":"^22"}}` +const withPg = `{"dependencies":{"@novox/mesh-sdk":"^0.1.1","pg":"^8","@types/pg":"^8"}}` + +func TestAModuleNamingOnlyTheSDKNeedsNoLock(t *testing.T) { + root := t.TempDir() + module(t, root, "plain", "typescript", sdkOnly, false) + module(t, root, "none", "typescript", "", false) + got, err := Check(root) + if err != nil || len(got) != 0 { + t.Fatalf("%v %v", got, err) + } +} + +func TestAModuleNamingMorePackagesWithoutALockIsRefusedNamingThem(t *testing.T) { + root := t.TempDir() + module(t, root, "db", "typescript", withPg, false) + module(t, root, "db-locked", "typescript", withPg, true) + got, err := Check(root) + if err != nil { + t.Fatal(err) + } + if want := []Finding{{Module: "db", Packages: []string{"pg"}}}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestAModuleThatIsNotTypeScriptIsNotJudged(t *testing.T) { + root := t.TempDir() + module(t, root, "go", "go", withPg, false) + if got, err := Check(root); err != nil || len(got) != 0 { + t.Fatalf("%v %v", got, err) + } +} diff --git a/merge-check.sh b/merge-check.sh index 3fe55bd1..2ea4bb6f 100755 --- a/merge-check.sh +++ b/merge-check.sh @@ -64,6 +64,8 @@ set -eu (cd checks/imagebuild && go test -count=1 . && go run . ../..) # The image's place (novox/hq ADR 0487 §1): every image builds from a Dockerfile in its own module's directory. (cd checks/imageplace && go test -count=1 . && go run . ../..) +# The Node lock files (novox/hq ADR 0469 §5, issue 1868): a TypeScript module that names packages beyond the SDK commits its lock. +(cd checks/locks && go test -count=1 . && go run . ../..) # The agent's home (novox/hq ADR 0266): no archive, and no file written into, under it. (cd checks/agenthome && go test -count=1 . && go run . ../..) # The mail-store seat (novox/hq ADR 0378 §3): only mesh-mail may claim it. diff --git a/modules/clipmenu/cmd/clipmenu-tools/manifest_test.go b/modules/clipmenu/cmd/clipmenu-tools/manifest_test.go index 4e769c4c..fea70036 100644 --- a/modules/clipmenu/cmd/clipmenu-tools/manifest_test.go +++ b/modules/clipmenu/cmd/clipmenu-tools/manifest_test.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "context" "net" "os" @@ -170,20 +171,33 @@ func TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock(t *testing.T) store := dir + "/cm/clipmenu.6.op" os.MkdirAll(store, 0o700) lock := store + "/session_lock" - // the older clipmenud: it holds the lock the way clipmenud does, until it is killed - holder := exec.Command("flock", "-x", "-n", lock, "sleep", "60") + // the older clipmenud: it holds the lock the way clipmenud does, until it is killed. It says "held" once it has the + // lock, so the test goes on only when the lock is held for certain: a holder that is slow to start (the checker + // runs thousands of tests at once) must not let the launcher find the lock free (issue 1814). + holder := exec.Command("flock", "-x", "-n", lock, "sh", "-c", "echo held; exec sleep 60") holder.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + held, err := holder.StdoutPipe() + if err != nil { + t.Fatal(err) + } if err := holder.Start(); err != nil { t.Fatal(err) } // flock's child shares the lock, so the whole group goes release := func() { syscall.Kill(-holder.Process.Pid, syscall.SIGKILL); holder.Wait() } defer release() - for i := 0; i < 50; i++ { - if probe := exec.Command("flock", "-n", lock, "true"); probe.Run() != nil { - break + said := make(chan string, 1) + go func() { + line, _ := bufio.NewReader(held).ReadString('\n') + said <- strings.TrimSpace(line) + }() + select { + case line := <-said: + if line != "held" { + t.Fatalf("the older clipmenud did not take the lock: %q", line) } - time.Sleep(50 * time.Millisecond) + case <-time.After(30 * time.Second): + t.Fatal("the older clipmenud did not take the lock within 30 s") } var out syncBuffer cmd := exec.Command("sh", file) diff --git a/modules/gitea/client.ts b/modules/gitea/client.ts index 33afefe5..6a2ba438 100644 --- a/modules/gitea/client.ts +++ b/modules/gitea/client.ts @@ -533,7 +533,7 @@ export class GiteaClient { * **A listing the forge cut is no listing** (it says `truncated` on a page with nothing more to read, or the pages * run past a bound): the call fails naming the repository and the commit, never answering the directories it did * see, since a directory left out would be taken for one without a module. Reads only. */ - async commitTrees(owner: string, repo: string, sha: string): Promise<{ root: string; trees: Record }> { + async commitTrees(owner: string, repo: string, sha: string, paths?: Record): Promise<{ root: string; trees: Record; paths?: Record> }> { const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`; const cut = (why: string) => new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} was cut (${why}); no trees are answered from a part`); const entries: any[] = []; @@ -560,7 +560,22 @@ export class GiteaClient { if (!tree) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} holds ${e.path} but not the tree of ${dir}`); trees[dir] = tree; } - return { root, trees }; + if (paths === undefined) return { root, trees }; + // **Named paths** (novox/hq ADR 0499 §3, issue 1758): each asked path's git object id at the commit, a file's or a + // directory's, from the same listing, by directory and by path relative to it. A path the listing lacks fails + // naming both, and nothing is answered in part. + const objectOf = new Map(); + for (const e of entries) if (typeof e?.path === "string" && (e.type === "tree" || e.type === "blob")) objectOf.set(e.path, String(e.sha)); + const named: Record> = {}; + for (const [dir, list] of Object.entries(paths)) { + named[dir] = {}; + for (const p of list) { + const id = objectOf.get(dir === "" ? p : `${dir}/${p}`); + if (!id) throw new Error(`the path ${p} of ${dir === "" ? "the repository root" : dir} is not in ${owner}/${repo} at ${sha}`); + named[dir][p] = id; + } + } + return { root, trees, paths: named }; } /** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */ diff --git a/modules/gitea/index.ts b/modules/gitea/index.ts index 8251b295..f178cfca 100644 --- a/modules/gitea/index.ts +++ b/modules/gitea/index.ts @@ -40,6 +40,7 @@ import { emit, on } from "@novox/mesh-sdk/events"; import { registerModuleTools } from "@novox/mesh-sdk/tools"; +import { statesAnnouncement } from "./kept.js"; import { checkStatusTool, commitStatusTool } from "./delivery.js"; import { announce, keepWatermark, readWatermark } from "./announce.js"; import { mergeTool } from "./merge.js"; @@ -482,6 +483,10 @@ const gates = new GateWriter({ const pace = new Pace(); if (!gitea) pace.unheard("the forge is not watched: no way to a token", false); +// The states this module keeps and which others may read, said on the bus before it serves (novox/hq ADR 0495 clause 2). +// Said with `emit`, not the SDK's `keeps`, which the toolchain's SDK does not have yet: switch to `keeps` once that SDK is 0.1.15 or later (issue 1852). +await emit("states", statesAnnouncement(process.env.MESH_MODULE || "gitea", process.env.MESH_NODE)); + // gitea_events is served whether or not the forge is watched: it says so (novox/hq issue 729). registerModuleTools("gitea", () => [eventsTool(pace), ...(gitea ? [commitStatusTool({ setCommitStatus: (owner, repo, sha, status) => (gitea as GiteaClient).setCommitStatus(owner, repo, sha, status), diff --git a/modules/gitea/kept.ts b/modules/gitea/kept.ts new file mode 100644 index 00000000..fccc5514 --- /dev/null +++ b/modules/gitea/kept.ts @@ -0,0 +1,20 @@ +// The states this module keeps on the bus and whether other modules may read them (novox/hq ADR 0495 clause 2; issue +// 1790): the module says it itself, from its code, as it starts (index.ts calls keeps with this list), and a reader is +// granted what it said (clause 3). A state is readable when another module's manifest reads it; none reads gitea's. +// test/kept.test.ts holds the list to the manifest's `state`. +export const KEPT: readonly { readonly name: string; readonly readable?: boolean }[] = [ + { name: "announced" }, + { name: "bound" }, +]; + +/** The announcement of these states, in the layout mesh-sdk's go/announce reads (novox/hq ADR 0495 clause 2): version 1, + * the module, the machine when known, and the states sorted by name, `readable` only where true. It is said as the event + * `states`, which is the subject mesh.mod.gitea.event.states. gitea says it with the SDK's `emit` because the + * toolchain's SDK, which judges R4e's group, predates the SDK's `keeps` (0.1.15); switch to `keeps` once the toolchain's + * SDK is 0.1.15 or later (issue 1852). */ +export function statesAnnouncement(module: string, node: string | undefined) { + const states = [...KEPT] + .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) + .map((k) => (k.readable ? { name: k.name, readable: true } : { name: k.name })); + return { v: 1, module, ...(node ? { node } : {}), states }; +} diff --git a/modules/gitea/test/commit-trees.test.ts b/modules/gitea/test/commit-trees.test.ts index 51801d7d..e685a5c0 100644 --- a/modules/gitea/test/commit-trees.test.ts +++ b/modules/gitea/test/commit-trees.test.ts @@ -171,3 +171,53 @@ test("the root tree is git's hash of the root's own entries, never the listing's server.close(); } }); + +test("named paths: each asked path's git object id at the commit, a file's or a directory's, by directory (1758 S1, ADR 0499 §3)", async () => { + const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]); + try { + const got = await client.commitTrees("novox", "hq", COMMIT, { "modules/a": ["main.go", "module.json"], "modules/b": ["c"], "": ["README.md", "modules/d"] }); + assert.deepEqual(got.paths, { + "modules/a": { "main.go": h("6"), "module.json": h("5") }, + "modules/b": { c: h("8") }, + "": { "README.md": h("1"), "modules/d": h("a") }, + }); + assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") }); + assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered"); + } finally { + server.close(); + } +}); + +test("a named path the listing lacks fails naming the directory and the path, and answers nothing", async () => { + const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]); + try { + await assert.rejects(client.commitTrees("novox", "hq", COMMIT, { "modules/a": ["main.go", "gone.go"] }), (err: Error) => { + assert.match(err.message, /modules\/a/); + assert.match(err.message, /gone\.go/); + return true; + }); + for (const bad of [[""], ["../x"], ["/abs"], ["a//b"]]) { + await assert.rejects(client.commitTrees("novox", "hq", COMMIT, { "modules/a": bad }), /./, JSON.stringify(bad)); + } + } finally { + server.close(); + } +}); + +test("gitea_commit_trees takes paths as {directory: [paths]} and refuses another shape before the forge is asked", async () => { + const { getGiteaTools } = await import("../tools/index.ts"); + const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]); + try { + const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!; + assert.ok("paths" in tool.input, "the tool names the argument"); + const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any); + assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } }); + const before = asked.length; + for (const paths of ["modules/a", ["main.go"], { "modules/a": "main.go" }, { "modules/a": [1] }]) { + await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths } as any), /paths/, JSON.stringify(paths)); + } + assert.equal(asked.length, before, "the forge was asked for a refused call"); + } finally { + server.close(); + } +}); diff --git a/modules/gitea/test/kept.test.ts b/modules/gitea/test/kept.test.ts new file mode 100644 index 00000000..5702f8e1 --- /dev/null +++ b/modules/gitea/test/kept.test.ts @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { readFileSync } from "node:fs"; + +// 1790: the states the module announces are the states its manifest declares, no more, no fewer; none is readable +// because no other module's manifest reads one of them. +test("the states the module announces are the states its manifest declares", async () => { + const { KEPT } = await import("../kept.ts"); + const manifest = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8")); + const declared = (manifest.state as (string | { name: string })[]).map((s) => (typeof s === "string" ? s : s.name)).sort(); + assert.deepEqual(KEPT.map((k) => k.name).sort(), declared); + assert.ok(KEPT.every((k) => !k.readable), "no other module's manifest reads a state of gitea"); +}); + +test("the announcement has the layout mesh-sdk's go/announce reads: version 1, the module, the node, the states sorted by name", async () => { + const { statesAnnouncement } = await import("../kept.ts"); + assert.deepEqual(statesAnnouncement("gitea", "ace"), { v: 1, module: "gitea", node: "ace", states: [{ name: "announced" }, { name: "bound" }] }); + assert.deepEqual(statesAnnouncement("gitea", undefined), { v: 1, module: "gitea", states: [{ name: "announced" }, { name: "bound" }] }); +}); + +test("index.ts says the announcement as the event `states` before the tools are served", () => { + const source = readFileSync(new URL("../index.ts", import.meta.url), "utf8"); + assert.match(source, /await emit\("states", statesAnnouncement\(/); + assert.ok(source.indexOf('await emit("states"') < source.indexOf('registerModuleTools("gitea"'), "announced before the tools are served"); +}); diff --git a/modules/gitea/tools/index.ts b/modules/gitea/tools/index.ts index 910e9496..7100fdbc 100644 --- a/modules/gitea/tools/index.ts +++ b/modules/gitea/tools/index.ts @@ -60,6 +60,22 @@ function namesChecked(tools: ToolDefinition[]): ToolDefinition[] { })); } +/** The `paths` argument of gitea_commit_trees: {directory: [path relative to it]}, each path plain (no empty, `.` or `..` + * segment, not absolute); anything else is refused before the forge is asked. Absent is absent. */ +function namedPaths(raw: unknown): Record | undefined { + if (raw === undefined || raw === null) return undefined; + if (typeof raw !== "object" || Array.isArray(raw)) throw new Error("paths is {directory: [path, ...]}"); + const out: Record = {}; + for (const [dir, list] of Object.entries(raw as Record)) { + if (!Array.isArray(list) || list.some((p) => typeof p !== "string")) throw new Error(`paths of ${dir || "the root"} is a list of paths`); + for (const p of list as string[]) { + if (p === "" || p.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) throw new Error(`paths: ${JSON.stringify(p)} of ${dir || "the root"} is not a plain relative path`); + } + out[dir] = list as string[]; + } + return out; +} + export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefinition[] { return namesChecked(allGiteaTools(gitea, admin)); } @@ -525,18 +541,19 @@ function allGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefini }, { name: "gitea_commit_trees", - description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). The commit is a full 40-hex sha. A listing the forge cut is refused, never answered in part. Reads only.", + description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. A listing the forge cut is refused, never answered in part. Reads only.", input: { owner: { type: "string", description: "the repository owner" }, repo: { type: "string", description: "the repository name" }, sha: { type: "string", description: "the commit, its full 40-hex sha" }, + paths: { type: "object", description: "optional, {directory: [path relative to it]}: the git object id of each named file or directory is answered under paths; the root directory is the empty name" }, }, run: async (args) => { const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim(); const sha = String(args.sha ?? "").trim(); if (!owner || !repo) throw new Error("name the repository: owner and repo"); if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error("sha is a commit's full 40-hex sha"); - return gitea.commitTrees(owner, repo, sha); + return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths)); }, }, { diff --git a/modules/i3/cmd/i3-tools/manifest_test.go b/modules/i3/cmd/i3-tools/manifest_test.go index 366a6723..d41fb7ad 100644 --- a/modules/i3/cmd/i3-tools/manifest_test.go +++ b/modules/i3/cmd/i3-tools/manifest_test.go @@ -10,7 +10,6 @@ import ( "os" "os/exec" "path/filepath" - "sort" "strings" "testing" ) @@ -204,41 +203,37 @@ func TestTheLoginManagersEntryRunsTheSessionsStart(t *testing.T) { } } -// Every module of the catalogue that contributes window-manager lines (novox/hq ADR 0212) is placed in -// the main file as the controller places them — in module order, each module's under a line naming it -// — and loaded as i3 would load them on a machine with all of them assigned: no two bind one key, and -// every line parses. +// The window-manager lines the catalogue's modules contribute to the main file (novox/hq ADR 0212) are kept in +// testdata/window-manager-lines.json, one entry per contributing module in module order, as the controller places +// them. The test reads that file, never the other modules' manifests: a module may not read a manifest (ADR 0495 +// §1), and a test that reads its siblings' fails wherever they are not beside it (issue 1814). The main file and +// those lines are placed together — each module's under a line naming it — and loaded as i3 would load them on a +// machine with all of them assigned: no two bind one key, and every line parses. func TestTheMainFileAndEveryModulesContributionLoadTogether(t *testing.T) { i3, err := exec.LookPath("i3") if err != nil { t.Skip("no i3 here to check with") } dir := t.TempDir() - manifests, _ := filepath.Glob(filepath.Join("..", "..", "..", "*", "module.json")) - sort.Strings(manifests) + raw, err := os.ReadFile(filepath.Join("testdata", "window-manager-lines.json")) + if err != nil { + t.Fatal(err) + } + var contributions []struct { + Module string `json:"module"` + Content []string `json:"content"` + } + if err := json.Unmarshal(raw, &contributions); err != nil { + t.Fatal(err) + } var placed strings.Builder var found []string - for _, p := range manifests { - raw, _ := os.ReadFile(p) - var m struct { - Module string `json:"module"` - Contributions []struct { - Seat, Kind, Content string - } `json:"contributions"` - } - json.Unmarshal(raw, &m) - named := false - for _, c := range m.Contributions { - if c.Seat != "node-display-session" || c.Kind != "config" { - continue - } - if !named { - placed.WriteString("# " + m.Module + "\n") - named = true - found = append(found, m.Module) - } - placed.WriteString(c.Content) - if !strings.HasSuffix(c.Content, "\n") { + for _, c := range contributions { + placed.WriteString("# " + c.Module + "\n") + found = append(found, c.Module) + for _, content := range c.Content { + placed.WriteString(content) + if !strings.HasSuffix(content, "\n") { placed.WriteString("\n") } } diff --git a/modules/i3/cmd/i3-tools/testdata/window-manager-lines.json b/modules/i3/cmd/i3-tools/testdata/window-manager-lines.json new file mode 100644 index 00000000..16d0692a --- /dev/null +++ b/modules/i3/cmd/i3-tools/testdata/window-manager-lines.json @@ -0,0 +1,50 @@ +[ + { + "module": "asus-zephyrus-g14", + "content": [ + "# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The keys the firmware turns into ordinary key presses. The vendor keys that reach no X client are\n# triggerhappy's (/etc/triggerhappy/triggers.d/asus-g14.conf): M4 and Fn+F4/F5 for media, Fn+F7/F8 for\n# the panel, Fn+F10 for the touchpad. The keyboard backlight (Fn+F2/F3) is the firmware's and asusd's.\n\n# Fn+F6, the screenshot key: the firmware sends Super+Shift+S. Released before it runs, because the\n# screenshot grabs the pointer to select a region, which fails while the key is still held.\nbindsym --release $mod+Shift+s exec --no-startup-id $XDG_CONFIG_HOME/i3/scripts/screenshot.sh\n\n# Fn+F9, the display key: the firmware sends Super+P. Odd workspaces to the panel, even ones to the\n# external output.\nbindsym $mod+p exec --no-startup-id /usr/local/lib/example-laptop/bin/zephyrus-display order\n\n# The keyboard backlight's level, shown when it changes.\nexec --no-startup-id /usr/local/lib/example-laptop/bin/zephyrus-kbd-notify\n# The laptop's own lines in i3 (module asus-zephyrus-g14, novox/hq ADR 0208, ADR 0210). Owned by the\n# mesh: replaced at every push. Once the controller places contributions to node-display-session\n# (ADR 0210), these become the module's contribution instead of a file in i3's directory.\n#\n# The model's panel and touchpad.\n\n# The internal panel is the primary output, where the bars' tray goes. Which monitors are on and where\n# is the display server's (autorandr, run at every session start).\nexec --no-startup-id /usr/local/lib/example-laptop/bin/zephyrus-display primary\n\n# The touchpad's settings again, by hand. X applies them itself whenever the device appears.\nbindsym $mod+Shift+x exec --no-startup-id /usr/local/lib/example-laptop/bin/zephyrus-touchpad reset\n" + ] + }, + { + "module": "clipmenu", + "content": [ + "# The clipboard's history key (module clipmenu, novox/hq ADR 0208). Owned by the mesh: replaced at\n# every push. clipmenu shows the history through `dmenu`, the node's dmenu-compatible command, which\n# the holder of node-launcher answers (rofi on the workstations); the chosen entry is put back on the\n# clipboard.\nbindsym $mod+period exec --no-startup-id clipmenu -p Clipboard\n" + ] + }, + { + "module": "dunst", + "content": [ + "# The notifications' key (module dunst, novox/hq ADR 0208). Owned by the mesh: replaced at every push.\n# Super+N opens the menu (rofi) of the actions and links of every notification on screen, so one is\n# chosen from the keyboard as a tap chooses it on the notification.\nbindsym $mod+n exec --no-startup-id dunstctl context\n" + ] + }, + { + "module": "feh", + "content": [ + "# The wallpaper's key (module feh, novox/hq ADR 0208). Owned by the mesh: replaced at every push.\n# It puts the declared wallpaper back, after a monitor change or a wallpaper set for the session.\nbindsym $mod+Shift+b exec --no-startup-id ~/.fehbg\n" + ] + }, + { + "module": "i3status-rust", + "content": [ + "# The bars (module i3status-rust, novox/hq ADR 0208). Owned by the mesh: replaced at every push.\n# i3 reads this file through its configuration's `include ~/.config/i3/config.d/*.conf`. The bottom\n# bar shows the machine; the top bar the focused window and the tray, on the primary output. The\n# face is the monospace one every desktop module names.\nbar {\n font pango:JetBrainsMono Nerd Font 11\n position bottom\n status_command i3status-rs ~/.config/i3status-rust/bottom-bar.toml\n tray_output none\n colors {\n separator #ffffff\n background #000000\n statusline #ffffff\n # The text on an accent-coloured button is dark: light text on the accent was barely\n # legible.\n focused_workspace #de5200 #de5200 #000000\n active_workspace #de5200 #de5200 #000000\n inactive_workspace #000000 #000000 #ffffff\n urgent_workspace #2f343a #900000 #ffffff\n }\n}\n\nbar {\n font pango:JetBrainsMono Nerd Font 11\n position top\n status_command i3status-rs ~/.config/i3status-rust/top-bar.toml\n workspace_buttons no\n tray_output primary\n colors {\n separator #ffffff\n background #000000\n statusline #ffffff\n }\n}\n" + ] + }, + { + "module": "polychromatic", + "content": [ + "# The Razer peripherals' tray (module polychromatic, novox/hq ADR 0208, ADR 0212). Owned by the mesh:\n# replaced at every push. This line is the tray's one start, at the session's start (exec, not\n# exec_always, so a reload starts nothing). The package's own login helper starts it too while the\n# application's setting \"Start the tray applet when I log on\" is ticked; polychromatic_check names\n# that as a second start.\nexec --no-startup-id polychromatic-tray-applet\n" + ] + }, + { + "module": "rofi", + "content": [ + "# The launcher's key bindings (module rofi, novox/hq ADR 0208). Owned by the mesh: replaced at every\n# push. i3 reads this file through its configuration's `include ~/.config/i3/config.d/*.conf`, after\n# the variables it sets, so $mod is i3's.\nbindsym $mod+d exec --no-startup-id ~/.local/bin/rofi-launch drun\nbindsym $mod+t exec --no-startup-id ~/.local/bin/rofi-launch run\nbindsym $mod+Shift+t exec --no-startup-id ~/.local/bin/rofi-launch sudo\nbindsym $mod+Shift+w exec --no-startup-id ~/.local/bin/rofi-launch window\nbindsym $mod+Escape exec --no-startup-id ~/.local/bin/rofi-powermenu\n" + ] + }, + { + "module": "slack", + "content": [ + "# Slack (module slack, novox/hq ADR 0208, ADR 0212). Owned by the mesh: replaced at every push.\n# Its one start: at login, to the tray (-s), with the arguments of the package's own desktop entry.\n# No XDG autostart entry starts it as well; slack_check names one if it appears.\nexec --no-startup-id /usr/bin/slack --gtk-version=3 -s\n# The chat window. Slack owns four X windows, and the only one i3 manages has the class \"slack\" in\n# lower case; the three of class \"Slack\" (the packaged entry's StartupWMClass) are its unmanaged\n# helpers and its tray icon, so a rule on class=\"Slack\" matches nothing. window_role keeps a call or\n# screen-share window out of the rules. $ws3 is i3's, in scope here.\nfor_window [class=\"(?i)^slack$\" window_role=\"browser-window\"] move to workspace $ws3\nfor_window [class=\"(?i)^slack$\" window_role=\"browser-window\"] floating disable\nfor_window [class=\"(?i)^slack$\" window_role=\"browser-window\"] border pixel 2\n" + ] + } +] \ No newline at end of file diff --git a/modules/mesh-build-image/README.md b/modules/mesh-build-image/README.md index 9ebca6cb..e25dd475 100644 --- a/modules/mesh-build-image/README.md +++ b/modules/mesh-build-image/README.md @@ -30,6 +30,10 @@ The Dockerfile's steps may use the network (ADR 0487 §6); nothing a step can re `/:` (the first 12 hex digits of the digest unless the call gives one), waiting out a registry held still, and the key carries the digest the registry serves and the reference pinned by it. Without `publish` (a pull request) nothing is pushed. +- `build_image_copy` copies an upstream artifact (`module`, `artifact`, `from`) registry to registry into the artifact store + over the registry API, the whole index and every blob, by the digest the manifest pins (ADR 0498 §4), and verifies the + store serves that digest. Answers the digest and the reference `//@sha256:`. A + `from` without a digest fails `not pinned:`. It needs no workspace and no buildah. - It refuses, naming why, an image whose build context is a whole repository (`context`) or whose `build.on` names another module's artifact (`unsupportedImage` in `build.go`): those forms belong to the images build-agent builds until it is removed at R4g. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/build.go b/modules/mesh-build-image/cmd/mesh-build-image/build.go index 2e149852..baee275e 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/build.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/build.go @@ -74,6 +74,23 @@ func unsupportedImage(manifest catalogue.Manifest, a catalogue.Artifact) string return "" } +// qualifyImage names a base image in full, as the controller's parseReference reads it: a name without a registry host +// is the public hub's (docker.io), a single name under `library/`. buildah, with this machine's registry configuration, +// has no unqualified-search registry and refuses a short name (issue 1824); a name that has a host is kept. +func qualifyImage(ref string) string { + name := ref + if at := strings.Index(name, "@"); at >= 0 { + name = name[:at] + } + if slash := strings.Index(name, "/"); slash >= 0 && (strings.ContainsAny(name[:slash], ".:") || name[:slash] == "localhost") { + return ref + } + if !strings.Contains(name, "/") { + return "docker.io/library/" + ref + } + return "docker.io/" + ref +} + // imageArgs are the --build-arg words of the images the manifest pins in build.on, in its order. func imageArgs(manifest catalogue.Manifest) []string { var out []string @@ -82,7 +99,7 @@ func imageArgs(manifest catalogue.Manifest) []string { } for _, on := range manifest.Build.On { if on.Image != "" && on.Arg != "" { - out = append(out, "--build-arg", on.Arg+"="+on.Image) + out = append(out, "--build-arg", on.Arg+"="+qualifyImage(on.Image)) } } return out diff --git a/modules/mesh-build-image/cmd/mesh-build-image/copy.go b/modules/mesh-build-image/cmd/mesh-build-image/copy.go new file mode 100644 index 00000000..6c88dc08 --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/copy.go @@ -0,0 +1,374 @@ +package main + +// The copy step (novox/hq ADR 0498 §4; issue 1587): an upstream artifact is copied registry to registry into the +// artifact store, whole (an index with every manifest it lists, every blob), by the digest its manifest pins. The copy +// runs no code of a change, so it needs neither buildah nor an isolation; it is made over the registry API, which +// keeps every digest as it is (a pull and a push through buildah may rewrite a manifest). An upstream without a +// digest is not copied: `not pinned:`. A copy that does not end with the pinned digest in the store is `not stored:`. + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// CopyTool is the copy step's tool. +const CopyTool = "build_image_copy" + +// CopyResult is what a passed copy step says: the pinned digest, now in the store, and the reference to it in the +// form an image's is (//@sha256:). +type CopyResult struct { + Artifact string `json:"artifact"` + Digest string `json:"digest"` + Reference string `json:"reference"` + Pushed bool `json:"pushed"` +} + +var ( + nameOK = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$`) + pinnedOK = regexp.MustCompile(`@(sha256:[0-9a-f]{64})$`) + manifestKind = strings.Join([]string{ + "application/vnd.oci.image.index.v1+json", "application/vnd.docker.distribution.manifest.list.v2+json", + "application/vnd.oci.image.manifest.v1+json", "application/vnd.docker.distribution.manifest.v2+json"}, ", ") +) + +func (m *Module) copyTool() StepTool { + return StepTool{ + Name: CopyTool, + Description: "Copy an upstream image, registry to registry, into the artifact store by the digest the manifest pins " + + "(novox/hq ADR 0498 §4). Answers a step id at once; the step ends on its key with the digest in the store and the " + + "reference to it. A reference without a digest fails `not pinned:`. Needs no workspace and runs no code of a change.", + Input: map[string]any{ + "type": "object", + "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module that names the upstream artifact"}, + "artifact": map[string]any{"type": "string", "description": "the upstream artifact's name in the module's manifest"}, + "from": map[string]any{"type": "string", "description": "the image reference the manifest gives, pinned by digest, such as nats:2.11.17-alpine@sha256:..."}, + }, + "required": []string{"module", "artifact", "from"}, + }, + Long: true, + Run: m.copy, + } +} + +// upstreamRef is an image reference split the way a container runtime does: no host means the public hub. +type upstreamRef struct { + base, repository, digest string +} + +func parseUpstream(from string) (upstreamRef, error) { + pin := pinnedOK.FindStringSubmatch(from) + if pin == nil { + return upstreamRef{}, fmt.Errorf("%q names no digest", from) + } + name := strings.TrimSuffix(from, "@"+pin[1]) + if colon := strings.LastIndex(name, ":"); colon > strings.LastIndex(name, "/") { + name = name[:colon] // the tag is only what it was called; the digest names the image + } + if name == "" { + return upstreamRef{}, fmt.Errorf("%q is not an image reference", from) + } + host, repository := "docker.io", name + if slash := strings.Index(name, "/"); slash >= 0 && (strings.ContainsAny(name[:slash], ".:") || name[:slash] == "localhost") { + host, repository = name[:slash], name[slash+1:] + } + if host == "docker.io" { + host = "registry-1.docker.io" + if !strings.Contains(repository, "/") { + repository = "library/" + repository + } + } + scheme := "https://" + if strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.") { + scheme = "http://" + } + return upstreamRef{base: scheme + host, repository: repository, digest: pin[1]}, nil +} + +func (m *Module) copy(ctx context.Context, args map[string]any) (any, error) { + module, _ := args["module"].(string) + name, _ := args["artifact"].(string) + from, _ := args["from"].(string) + if !nameOK.MatchString(module) || !nameOK.MatchString(name) { + return nil, fmt.Errorf("%q / %q: a module and an artifact are named in lower case letters, digits, dots, dashes and underscores", module, name) + } + src, err := parseUpstream(from) + if err != nil { + return nil, fmt.Errorf("not pinned: %s: %v; an upstream artifact is copied by the digest its manifest pins", name, err) + } + addr, err := m.registry() + if err != nil { + return nil, fmt.Errorf("not stored: %s: %w", name, err) + } + repository := module + "/" + name + c := &copier{m: m, src: src, dstBase: "http://" + addr, dstRepo: repository, done: map[string]bool{}} + if err := m.waitForRegistry(ctx, addr, "copying "+from, func() error { return c.copyManifest(ctx, src.digest) }); err != nil { + return nil, fmt.Errorf("not stored: %s: %w", name, err) + } + if err := c.verify(ctx); err != nil { + return nil, fmt.Errorf("not stored: %s: %w", name, err) + } + return CopyResult{Artifact: name, Digest: src.digest, Reference: addr + "/" + repository + "@" + src.digest, Pushed: true}, nil +} + +// copier copies one image from its upstream registry into the store's repository. +type copier struct { + m *Module + src upstreamRef + dstBase string + dstRepo string + token string + done map[string]bool +} + +func (c *copier) client() *http.Client { + if c.m.HTTP != nil { + return c.m.HTTP + } + return &http.Client{Timeout: 30 * time.Minute} +} + +// get reads from the upstream registry, answering one bearer challenge with an anonymous token. +func (c *copier) get(ctx context.Context, path, accept string) (*http.Response, error) { + for attempt := 0; attempt < 2; attempt++ { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.src.base+"/v2/"+c.src.repository+path, nil) + if err != nil { + return nil, err + } + if accept != "" { + req.Header.Set("Accept", accept) + } + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } + resp, err := c.client().Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode == http.StatusUnauthorized && attempt == 0 { + challenge := resp.Header.Get("Www-Authenticate") + resp.Body.Close() + if c.token, err = c.fetchToken(ctx, challenge); err != nil { + return nil, err + } + continue + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("upstream answered %s for %s%s", resp.Status, c.src.repository, path) + } + return resp, nil + } + return nil, fmt.Errorf("upstream refused %s%s after a token", c.src.repository, path) +} + +var challengeParam = regexp.MustCompile(`(\w+)="([^"]*)"`) + +func (c *copier) fetchToken(ctx context.Context, challenge string) (string, error) { + if !strings.HasPrefix(strings.ToLower(challenge), "bearer ") { + return "", fmt.Errorf("upstream asks for credentials (%q) and this copy holds none", challenge) + } + p := map[string]string{} + for _, kv := range challengeParam.FindAllStringSubmatch(challenge, -1) { + p[kv[1]] = kv[2] + } + realm := p["realm"] + if realm == "" { + return "", fmt.Errorf("upstream's challenge %q names no realm", challenge) + } + q := url.Values{"service": {p["service"]}, "scope": {"repository:" + c.src.repository + ":pull"}} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, realm+"?"+q.Encode(), nil) + if err != nil { + return "", err + } + resp, err := c.client().Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + var tok struct { + Token string `json:"token"` + Access string `json:"access_token"` + } + if resp.StatusCode != http.StatusOK || json.NewDecoder(resp.Body).Decode(&tok) != nil || (tok.Token == "" && tok.Access == "") { + return "", fmt.Errorf("upstream's token service answered %s", resp.Status) + } + if tok.Token != "" { + return tok.Token, nil + } + return tok.Access, nil +} + +// copyManifest copies the manifest of digest and everything it names, children before the manifest itself. +func (c *copier) copyManifest(ctx context.Context, digest string) error { + if c.done[digest] { + return nil + } + resp, err := c.get(ctx, "/manifests/"+digest, manifestKind) + if err != nil { + return err + } + body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20)) + resp.Body.Close() + if err != nil { + return err + } + sum := sha256.Sum256(body) + if got := "sha256:" + hex.EncodeToString(sum[:]); got != digest { + return fmt.Errorf("upstream served %s for the manifest pinned as %s", got, digest) + } + var doc struct { + MediaType string `json:"mediaType"` + Config struct{ Digest string } `json:"config"` + Layers []struct{ Digest string } `json:"layers"` + Manifests []struct{ Digest string } `json:"manifests"` + } + if err := json.Unmarshal(body, &doc); err != nil { + return fmt.Errorf("the manifest %s is not JSON: %w", digest, err) + } + for _, child := range doc.Manifests { + if err := c.copyManifest(ctx, child.Digest); err != nil { + return err + } + } + blobs := []string{} + if doc.Config.Digest != "" { + blobs = append(blobs, doc.Config.Digest) + } + for _, l := range doc.Layers { + blobs = append(blobs, l.Digest) + } + for _, b := range blobs { + if err := c.copyBlob(ctx, b); err != nil { + return err + } + } + mediaType := resp.Header.Get("Content-Type") + if mediaType == "" { + mediaType = doc.MediaType + } + if err := c.putManifest(ctx, digest, mediaType, body); err != nil { + return err + } + c.done[digest] = true + return nil +} + +func (c *copier) copyBlob(ctx context.Context, digest string) error { + if !digestOK.MatchString(digest) { + return fmt.Errorf("%q is not a blob digest", digest) + } + if c.done[digest] { + return nil + } + head, err := http.NewRequestWithContext(ctx, http.MethodHead, c.dstBase+"/v2/"+c.dstRepo+"/blobs/"+digest, nil) + if err != nil { + return err + } + if r, err := c.client().Do(head); err != nil { + return err + } else { + r.Body.Close() + if r.StatusCode == http.StatusOK { + c.done[digest] = true + return nil + } + } + post, err := http.NewRequestWithContext(ctx, http.MethodPost, c.dstBase+"/v2/"+c.dstRepo+"/blobs/uploads/", nil) + if err != nil { + return err + } + pr, err := c.client().Do(post) + if err != nil { + return err + } + pr.Body.Close() + loc, err := pr.Location() + if pr.StatusCode != http.StatusAccepted || err != nil { + return fmt.Errorf("the store answered %s to a blob upload of %s", pr.Status, digest) + } + q := loc.Query() + q.Set("digest", digest) + loc.RawQuery = q.Encode() + blob, err := c.get(ctx, "/blobs/"+digest, "") + if err != nil { + return err + } + defer blob.Body.Close() + put, err := http.NewRequestWithContext(ctx, http.MethodPut, loc.String(), blob.Body) + if err != nil { + return err + } + put.ContentLength = blob.ContentLength + put.Header.Set("Content-Type", "application/octet-stream") + resp, err := c.client().Do(put) + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("the store answered %s to the blob %s", resp.Status, digest) + } + c.done[digest] = true + return nil +} + +func (c *copier) putManifest(ctx context.Context, digest, mediaType string, body []byte) error { + req, err := http.NewRequestWithContext(ctx, http.MethodPut, c.dstBase+"/v2/"+c.dstRepo+"/manifests/"+digest, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", mediaType) + resp, err := c.client().Do(req) + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + return fmt.Errorf("the store answered %s to the manifest %s", resp.Status, digest) + } + return nil +} + +// verify reads back what the store serves for the pinned digest: only the store can say. +func (c *copier) verify(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, c.dstBase+"/v2/"+c.dstRepo+"/manifests/"+c.src.digest, nil) + if err != nil { + return err + } + req.Header.Set("Accept", manifestKind) + resp, err := c.client().Do(req) + if err != nil { + return err + } + resp.Body.Close() + if got := resp.Header.Get("Docker-Content-Digest"); resp.StatusCode != http.StatusOK || got != c.src.digest { + return fmt.Errorf("the store answered %s with digest %q for the pinned %s", resp.Status, got, c.src.digest) + } + return nil +} + +// Plain is a long step that needs nothing of the machine: no guard. The copy runs no buildah. +func (m *Module) Plain(tool string, run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) { + return func(args map[string]any) (any, error) { + id, err := buildstep.Start(m.Ctx, m.Steps, m.Node, tool, func(ctx context.Context) (any, error) { return run(ctx, args) }) + if err != nil { + return nil, err + } + key, _ := delivery.StepKey(m.Node, id) + return map[string]any{"step": id, "key": key, "state": delivery.StepRunning}, nil + } +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/copy_test.go b/modules/mesh-build-image/cmd/mesh-build-image/copy_test.go new file mode 100644 index 00000000..d546dde0 --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/copy_test.go @@ -0,0 +1,218 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +func digestOf(b []byte) string { s := sha256.Sum256(b); return "sha256:" + hex.EncodeToString(s[:]) } + +// memRegistry is a registry in memory: blobs and manifests by digest, per repository. +type memRegistry struct { + mu sync.Mutex + blobs map[string][]byte // repo|digest + manifests map[string][]byte // repo|digest + types map[string]string + puts []string + srv *httptest.Server +} + +func newMem(t *testing.T) *memRegistry { + r := &memRegistry{blobs: map[string][]byte{}, manifests: map[string][]byte{}, types: map[string]string{}} + r.srv = httptest.NewServer(http.HandlerFunc(r.serve)) + t.Cleanup(r.srv.Close) + return r +} + +func (r *memRegistry) addr() string { return r.srv.Listener.Addr().String() } + +func (r *memRegistry) serve(w http.ResponseWriter, req *http.Request) { + r.mu.Lock() + defer r.mu.Unlock() + path := strings.TrimPrefix(req.URL.Path, "/v2/") + if i := strings.Index(path, "/blobs/uploads/"); i >= 0 { + if req.Method == http.MethodPost { + w.Header().Set("Location", "/v2/"+path[:i]+"/blobs/uploads/u1") + w.WriteHeader(http.StatusAccepted) + return + } + body, _ := io.ReadAll(req.Body) + d := req.URL.Query().Get("digest") + if digestOf(body) != d { + w.WriteHeader(http.StatusBadRequest) + return + } + r.blobs[path[:i]+"|"+d] = body + r.puts = append(r.puts, "blob "+d) + w.WriteHeader(http.StatusCreated) + return + } + for _, kind := range []string{"blobs", "manifests"} { + if i := strings.Index(path, "/"+kind+"/"); i >= 0 { + key := path[:i] + "|" + path[i+len(kind)+2:] + store := r.blobs + if kind == "manifests" { + store = r.manifests + } + if req.Method == http.MethodPut { + body, _ := io.ReadAll(req.Body) + if digestOf(body) != path[i+len(kind)+2:] { + w.WriteHeader(http.StatusBadRequest) + return + } + r.manifests[key] = body + r.types[key] = req.Header.Get("Content-Type") + r.puts = append(r.puts, "manifest "+path[i+len(kind)+2:]) + w.WriteHeader(http.StatusCreated) + return + } + body, ok := store[key] + if !ok { + http.NotFound(w, req) + return + } + w.Header().Set("Docker-Content-Digest", path[i+len(kind)+2:]) + w.Header().Set("Content-Type", r.types[key]) + if req.Method == http.MethodHead { + return + } + w.Write(body) + return + } + } + http.NotFound(w, req) +} + +// upstreamImage puts a two-platform index (two manifests, each with a config and a layer) into r and answers the index digest. +func upstreamImage(r *memRegistry, repo string) string { + var children []map[string]string + for _, tag := range []string{"amd64", "arm64"} { + cfg, layer := []byte(`{"arch":"`+tag+`"}`), []byte("layer-"+tag) + r.blobs[repo+"|"+digestOf(cfg)], r.blobs[repo+"|"+digestOf(layer)] = cfg, layer + mf, _ := json.Marshal(map[string]any{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": map[string]string{"digest": digestOf(cfg)}, "layers": []map[string]string{{"digest": digestOf(layer)}}}) + r.manifests[repo+"|"+digestOf(mf)] = mf + r.types[repo+"|"+digestOf(mf)] = "application/vnd.oci.image.manifest.v1+json" + children = append(children, map[string]string{"digest": digestOf(mf)}) + } + idx, _ := json.Marshal(map[string]any{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": children}) + d := digestOf(idx) + r.manifests[repo+"|"+d] = idx + r.types[repo+"|"+d] = "application/vnd.oci.image.index.v1+json" + return d +} + +func runCopy(t *testing.T, m *Module, states *keptStates, args map[string]any) (CopyResult, delivery.Step) { + t.Helper() + if _, err := tool(t, m, CopyTool)(args); err != nil { + t.Fatal(err) + } + st := states.waitEnd(t) + var res CopyResult + if st.State == delivery.StepPassed { + if err := json.Unmarshal(st.Result, &res); err != nil { + t.Fatal(err) + } + } + return res, st +} + +// ADR 0498 §4 / AC: the copy leaves the pinned digest in the store and answers it, with the whole index (every +// manifest it lists, every blob), and runs no buildah. +func TestTheCopyLeavesThePinnedDigestInTheStoreAndAnswersIt(t *testing.T) { + up, store := newMem(t), newMem(t) + digest := upstreamImage(up, "library/nats") + f := newFake(t) + states := newStates() + m := moduleWith(t, f, states) + m.HTTP = nil + reg := store.addr() + writeFile(t, m.RegistryFile, reg+"\n") + from := up.addr() + "/library/nats:2.11.17-alpine@" + digest + res, st := runCopy(t, m, states, map[string]any{"module": "nats", "artifact": "server", "from": from}) + if st.State != delivery.StepPassed { + t.Fatalf("step = %+v", st) + } + if res.Digest != digest || res.Reference != reg+"/nats/server@"+digest || !res.Pushed || res.Artifact != "server" { + t.Errorf("result = %+v", res) + } + if _, ok := store.manifests["nats/server|"+digest]; !ok { + t.Fatal("the index is not in the store") + } + if len(store.manifests) != 3 || len(store.blobs) != 4 { + t.Errorf("store holds %d manifests and %d blobs, want 3 and 4", len(store.manifests), len(store.blobs)) + } + if len(f.lines()) != 0 { + t.Errorf("the copy ran commands: %v", f.lines()) + } + // A second copy writes nothing new but the manifests. + store.puts = nil + if _, st := runCopy(t, m, states, map[string]any{"module": "nats", "artifact": "server", "from": from}); st.State != delivery.StepPassed { + t.Fatalf("again: %+v", st) + } + for _, p := range store.puts { + if strings.HasPrefix(p, "blob ") { + t.Errorf("a blob the store holds was sent again: %s", p) + } + } +} + +// ADR 0498 §4 / AC: an upstream without a digest fails `not pinned:`, and nothing is read from the upstream. +func TestAnUpstreamWithoutADigestFailsNotPinned(t *testing.T) { + up, store := newMem(t), newMem(t) + upstreamImage(up, "library/nats") + f := newFake(t) + states := newStates() + m := moduleWith(t, f, states) + writeFile(t, m.RegistryFile, store.addr()+"\n") + _, st := runCopy(t, m, states, map[string]any{"module": "nats", "artifact": "server", "from": up.addr() + "/library/nats:2.11.17-alpine"}) + if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "not pinned: server") { + t.Fatalf("step = %+v", st) + } + if len(store.puts) != 0 { + t.Errorf("the store was written: %v", store.puts) + } +} + +// A manifest the upstream serves under another digest than the pin is not copied; the failure is `not stored:`. +func TestAnUpstreamServingAnotherManifestThanPinnedIsNotStored(t *testing.T) { + up, store := newMem(t), newMem(t) + d := upstreamImage(up, "library/nats") + up.manifests["library/nats|"+d] = []byte(`{"tampered":true}`) + f := newFake(t) + states := newStates() + m := moduleWith(t, f, states) + writeFile(t, m.RegistryFile, store.addr()+"\n") + _, st := runCopy(t, m, states, map[string]any{"module": "nats", "artifact": "server", "from": up.addr() + "/library/nats@" + d}) + if st.State != delivery.StepFailed || !strings.HasPrefix(st.Reason, "not stored: server:") { + t.Fatalf("step = %+v", st) + } + if len(store.manifests) != 0 { + t.Errorf("the store holds %v", store.manifests) + } +} + +func TestAShortNameIsQualifiedAsTheHubsAndAHostIsKept(t *testing.T) { + d := "sha256:" + strings.Repeat("a", 64) + for from, want := range map[string]upstreamRef{ + "alpine@" + d: {"https://registry-1.docker.io", "library/alpine", d}, + "nats:2.11.17-alpine@" + d: {"https://registry-1.docker.io", "library/nats", d}, + "grafana/grafana:11@" + d: {"https://registry-1.docker.io", "grafana/grafana", d}, + "ghcr.io/org/app:1@" + d: {"https://ghcr.io", "org/app", d}, + "localhost:5000/x/y@" + d: {"http://localhost:5000", "x/y", d}, + } { + got, err := parseUpstream(from) + if err != nil || got != want { + t.Errorf("%s = %+v, %v; want %+v", from, got, err, want) + } + } +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/qualify_test.go b/modules/mesh-build-image/cmd/mesh-build-image/qualify_test.go new file mode 100644 index 00000000..4f78cc6c --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/qualify_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "slices" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +const pin = "@sha256:e4bf19f15fd3218814a4e3c9e0064e1334bd8aa20d5984b9f1a0afd084f8cc00" + +// 1824: a short base image name is qualified the way the controller's parseReference does (the public hub, `library/` +// for a single name), because buildah has no unqualified-search registry here; a qualified name is left as it is. +func TestAShortBaseImageNameIsQualifiedAsTheHubsAndAQualifiedOneIsKept(t *testing.T) { + for in, want := range map[string]string{ + "nats" + pin: "docker.io/library/nats" + pin, + "nats:2.11.17-alpine" + pin: "docker.io/library/nats:2.11.17-alpine" + pin, + "n8nio/n8n" + pin: "docker.io/n8nio/n8n" + pin, + "docker.io/library/alpine" + pin: "docker.io/library/alpine" + pin, + "ghcr.io/org/app:1" + pin: "ghcr.io/org/app:1" + pin, + "localhost:5000/x/y" + pin: "localhost:5000/x/y" + pin, + "registry-1.docker.io/library/a": "registry-1.docker.io/library/a", + } { + if got := qualifyImage(in); got != want { + t.Errorf("%s = %s, want %s", in, got, want) + } + } +} + +func TestTheBuildArgsCarryTheQualifiedName(t *testing.T) { + m := catalogue.Manifest{Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "NATS_BASE", Image: "nats" + pin}}}} + if got, want := imageArgs(m), []string{"--build-arg", "NATS_BASE=docker.io/library/nats" + pin}; !slices.Equal(got, want) { + t.Errorf("%v, want %v", got, want) + } +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/tools.go b/modules/mesh-build-image/cmd/mesh-build-image/tools.go index a1f0cc0e..f0ebf98a 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/tools.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/tools.go @@ -25,14 +25,18 @@ type StepTool struct { } // StepTools are the module's step tools. -func (m *Module) StepTools() []StepTool { return []StepTool{m.fetchTool(), m.buildTool()} } +func (m *Module) StepTools() []StepTool { + return []StepTool{m.fetchTool(), m.buildTool(), m.copyTool()} +} // Tools is every tool the module serves: its step tools behind the guard, its read tools, its status and its health. func (m *Module) Tools(steps []StepTool) []stdio.Tool { tools := []stdio.Tool{} for _, s := range steps { t := stdio.Tool{Name: s.Name, Description: s.Description, Input: s.Input} - if s.Long { + if s.Name == CopyTool { + t.Run = m.Plain(s.Name, s.Run) // a copy needs no buildah, so the buildah guard does not stand in front of it + } else if s.Long { t.Run = m.Long(s.Name, s.Run) } else { t.Run = m.Short(s.Run) diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index 2f9ecb9c..2cee8afa 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -14,6 +14,7 @@ "tools": [ "build_image_fetch", "build_image_build", + "build_image_copy", "build_image_step", "build_image_steps", "build_image_status", diff --git a/modules/mesh-catalog/package-lock.json b/modules/mesh-catalog/package-lock.json new file mode 100644 index 00000000..a70fb429 --- /dev/null +++ b/modules/mesh-catalog/package-lock.json @@ -0,0 +1,161 @@ +{ + "name": "@novox/module-mesh-catalog", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@novox/module-mesh-catalog", + "version": "0.1.0", + "dependencies": { + "pg": "^8" + } + }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "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-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "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/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/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/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/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/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" + } + } + } +} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go new file mode 100644 index 00000000..74013044 --- /dev/null +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps.go @@ -0,0 +1,12 @@ +package main + +import "git.novox.be/novox/mesh-sdk/go/announce" + +// Kept is every state this module keeps on the bus and whether other modules may read it (novox/hq ADR 0495 clause 2; +// issue 1790): the module says it itself, from its code, before it serves, and a reader is granted what it said +// (clause 3). A state is readable when another module's manifest reads it. keeps_test.go holds the list to the +// manifest's `state`, and keeps_integration_test.go that the module announces it on a bus. +var Kept = []announce.State{{Name: "asks"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges"}, {Name: "owed"}, {Name: "pulls"}, {Name: "source"}} + +// announceKept says Kept through keeps, which is stdio.Keeps. +func announceKept(keeps func(...announce.State) error) error { return keeps(Kept...) } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_integration_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_integration_test.go new file mode 100644 index 00000000..6269a4c7 --- /dev/null +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_integration_test.go @@ -0,0 +1,26 @@ +//go:build integration + +package main + +import ( + "testing" + + "git.novox.be/novox/mesh-sdk/go/announce" + "git.novox.be/novox/mesh-sdk/go/announcetest" + "git.novox.be/novox/mesh-sdk/go/process" +) + +// 1790/S (ADR 0495 clause 5): started against a bus of the test's own, the module announces on it every state it keeps, +// readable as other modules' manifests need. +func TestTheModuleAnnouncesEveryStateItKeepsOnTheBus(t *testing.T) { + announcetest.Check(t, "mesh-delivery-gate", Kept, func(url string) func() { + conn, err := process.Connect(process.Account{URL: url, Module: "mesh-delivery-gate", Node: "ace", User: "u", Password: "p"}) + if err != nil { + t.Fatal(err) + } + if err := announceKept(func(s ...announce.State) error { return conn.Keeps(s...) }); err != nil { + t.Fatal(err) + } + return conn.Close + }) +} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go new file mode 100644 index 00000000..bfccf856 --- /dev/null +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/keeps_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "slices" + "testing" +) + +// 1790/S: the states the module announces are the states its manifest declares, no more, no fewer, and the readable +// ones are the states other modules' manifests read. +func TestTheStatesTheModuleAnnouncesAreTheStatesItsManifestDeclares(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) + if err != nil { + t.Fatal(err) + } + var m struct { + State []json.RawMessage `json:"state"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + declared := []string{} + for _, s := range m.State { + var name string + if json.Unmarshal(s, &name) != nil { + var o struct{ Name string } + if err := json.Unmarshal(s, &o); err != nil { + t.Fatal(err) + } + name = o.Name + } + declared = append(declared, name) + } + announced := []string{} + for _, s := range Kept { + announced = append(announced, s.Name) + } + slices.Sort(declared) + slices.Sort(announced) + if !slices.Equal(declared, announced) { + t.Errorf("announces %v, the manifest declares %v", announced, declared) + } + want := map[string]bool{} + for _, s := range Kept { + if s.Readable != want[s.Name] { + t.Errorf("state %s readable = %v, other modules' manifests read it: %v", s.Name, s.Readable, want[s.Name]) + } + } +} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go index 79e58504..5f12ef4d 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -77,6 +77,10 @@ func main() { h.init() l := &listening{now: "not yet: starting"} go run(h, l, occupancy) + if err := announceKept(stdio.Keeps); err != nil { + logf("%v", err) + os.Exit(1) + } if err := stdio.Serve("", tools(h, l)); err != nil { logf("%v", err) os.Exit(1) diff --git a/modules/mesh-delivery-gate/go.mod b/modules/mesh-delivery-gate/go.mod index b37ee2ea..3a0bfa4f 100644 --- a/modules/mesh-delivery-gate/go.mod +++ b/modules/mesh-delivery-gate/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-gate go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020060201-88581d0acaef + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-gate/go.sum b/modules/mesh-delivery-gate/go.sum index 3807e8df..910d3ad5 100644 --- a/modules/mesh-delivery-gate/go.sum +++ b/modules/mesh-delivery-gate/go.sum @@ -1,15 +1,9 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44 h1:r5xIo5qWOXQdCSMxvqJy8kV4U+P/QWXFK6971Ha6wU8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 h1:kvtV/SPsdRVUL/BZ3xxGS6U8YrXy/t7TRBjVrD6VLw8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020060201-88581d0acaef h1:xy3wuGWJpqMAXYdXolocYG6w9xnWqBltVUffoaziIKo= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020060201-88581d0acaef/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52 h1:5k04XjpfYCq6j7rCM6Dy6FfrsBk5iUJlbwGf6WhUOUc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= @@ -18,23 +12,15 @@ github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= -github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= -github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= diff --git a/modules/model-usage/package-lock.json b/modules/model-usage/package-lock.json new file mode 100644 index 00000000..9354cc90 --- /dev/null +++ b/modules/model-usage/package-lock.json @@ -0,0 +1,161 @@ +{ + "name": "@novox/module-model-usage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@novox/module-model-usage", + "version": "0.1.0", + "dependencies": { + "pg": "^8" + } + }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "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-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "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/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/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/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/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/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" + } + } + } +} diff --git a/modules/mongodb/package-lock.json b/modules/mongodb/package-lock.json new file mode 100644 index 00000000..c1ad4488 --- /dev/null +++ b/modules/mongodb/package-lock.json @@ -0,0 +1,162 @@ +{ + "name": "@novox/module-mongodb", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@novox/module-mongodb", + "version": "0.1.0", + "dependencies": { + "mongodb": "^6.21.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.6.1.tgz", + "integrity": "sha512-D4CQtk9IwjCLAvCf1xvHEod3960q8XMUzfmdRU9qjUNlHnesGuTOvJcOTaPm9koZ1caUKA2aTbszZiAkcmSgmg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/modules/mssql/package-lock.json b/modules/mssql/package-lock.json new file mode 100644 index 00000000..d3dadae6 --- /dev/null +++ b/modules/mssql/package-lock.json @@ -0,0 +1,944 @@ +{ + "name": "@novox/module-mssql", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@novox/module-mssql", + "version": "0.1.0", + "dependencies": { + "mssql": "^11.0.2" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.9.0.tgz", + "integrity": "sha512-6933vNLqh06RR7rumnrq3UZIeBtRLeBHDPMrieDPIljiCaMMwt0nRw++nd3TOxiohtasNtm29rC+b/9ZVKCOqg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.1.tgz", + "integrity": "sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.7.0.tgz", + "integrity": "sha512-7GEAoIsaoBr6KELNRb8nypowCqvk8dnCHFCYg4XD4lOQGY2GqjQg5IhkRjyBFRO18CGSMq05PaNqSOE9GQro3g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.0.tgz", + "integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^6.0.0", + "open": "^11.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.2.tgz", + "integrity": "sha512-VmUSLbXRAbSzDD8grXHGPaknYs0SKr3yuf6U+d4XMpX4XuVYskNqbTTwXce0zR1LyxfTZm9rWEBcvs3vdYwCmQ==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.1.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.26.0.tgz", + "integrity": "sha512-GYKRquzDZmZggCdStWy2bNJKnPui1upssSuznJgP+/h6Mb0ldkUkaGvaY6uSGGmZOm0CGiBQ3b5BiGy43BpXSg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.3" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.3.tgz", + "integrity": "sha512-tz3GvMrR9V/plb2lDdW8qaO8CPpfZevdExBKjltzzbAtyhK0eB2UIvji1aU+gkyHj3fxQ2ie+LNHyMXdmo8wOg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "license": "BSD-3-Clause" + }, + "node_modules/@tediousjs/connection-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz", + "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.25.tgz", + "integrity": "sha512-7GsxL8Wd5X3jh73/qgW7WCnbBvGSdD8U3E4Jiuus3/8I+PN20DtlkJtwFhr/sImXfDDQ2Z03i/9wNiIF3TVSsw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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==", + "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/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "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/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "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/mssql": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-11.0.2.tgz", + "integrity": "sha512-x3Rdo3aR7/gQgpcVWA5EexMTd/BtHzdOkv94MTvV137UglFOZUJER6wRlq2Fl4R0F7JqBpDYyV4hX1kMTP0j7A==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^0.5.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "rfdc": "^1.3.0", + "tarn": "^3.0.2", + "tedious": "^18.2.1" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, + "node_modules/open": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz", + "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "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/tarn": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.1.2.tgz", + "integrity": "sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "18.6.2", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.2.tgz", + "integrity": "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.1", + "@types/node": ">=18", + "bl": "^6.0.11", + "iconv-lite": "^0.6.3", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "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/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/wsl-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +}