@grimoire-rs/indexer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/cli/build.d.ts +6 -0
  4. package/dist/cli/build.d.ts.map +1 -0
  5. package/dist/cli/build.js +29 -0
  6. package/dist/cli/build.js.map +1 -0
  7. package/dist/cli/exit.d.ts +28 -0
  8. package/dist/cli/exit.d.ts.map +1 -0
  9. package/dist/cli/exit.js +33 -0
  10. package/dist/cli/exit.js.map +1 -0
  11. package/dist/cli/index.d.ts +3 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +8 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/cli/init.d.ts +40 -0
  16. package/dist/cli/init.d.ts.map +1 -0
  17. package/dist/cli/init.js +317 -0
  18. package/dist/cli/init.js.map +1 -0
  19. package/dist/cli/main.d.ts +2 -0
  20. package/dist/cli/main.d.ts.map +1 -0
  21. package/dist/cli/main.js +112 -0
  22. package/dist/cli/main.js.map +1 -0
  23. package/dist/cli/validate.d.ts +18 -0
  24. package/dist/cli/validate.d.ts.map +1 -0
  25. package/dist/cli/validate.js +139 -0
  26. package/dist/cli/validate.js.map +1 -0
  27. package/dist/config.d.ts +76 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +134 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/data/index.d.ts +40 -0
  32. package/dist/data/index.d.ts.map +1 -0
  33. package/dist/data/index.js +103 -0
  34. package/dist/data/index.js.map +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +4 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/integration.d.ts +2 -0
  40. package/dist/integration.d.ts.map +1 -0
  41. package/dist/integration.js +4 -0
  42. package/dist/integration.js.map +1 -0
  43. package/dist/renderer/astro/components/Catalog.d.ts +6 -0
  44. package/dist/renderer/astro/components/Catalog.d.ts.map +1 -0
  45. package/dist/renderer/astro/components/Catalog.js +73 -0
  46. package/dist/renderer/astro/components/Catalog.js.map +1 -0
  47. package/dist/renderer/astro/components/Catalog.tsx +256 -0
  48. package/dist/renderer/astro/components/CopyButton.astro +24 -0
  49. package/dist/renderer/astro/components/VersionMenu.astro +105 -0
  50. package/dist/renderer/astro/content.config.ts +30 -0
  51. package/dist/renderer/astro/layouts/Base.astro +623 -0
  52. package/dist/renderer/astro/lib/catalog.d.ts +7 -0
  53. package/dist/renderer/astro/lib/catalog.d.ts.map +1 -0
  54. package/dist/renderer/astro/lib/catalog.js +61 -0
  55. package/dist/renderer/astro/lib/catalog.js.map +1 -0
  56. package/dist/renderer/astro/lib/catalog.ts +69 -0
  57. package/dist/renderer/astro/lib/data.d.ts +3 -0
  58. package/dist/renderer/astro/lib/data.d.ts.map +1 -0
  59. package/dist/renderer/astro/lib/data.js +4 -0
  60. package/dist/renderer/astro/lib/data.js.map +1 -0
  61. package/dist/renderer/astro/lib/data.ts +12 -0
  62. package/dist/renderer/astro/pages/index.astro +69 -0
  63. package/dist/renderer/astro/pages/p/[...slug].astro +367 -0
  64. package/dist/renderer/index.d.ts +20 -0
  65. package/dist/renderer/index.d.ts.map +1 -0
  66. package/dist/renderer/index.js +192 -0
  67. package/dist/renderer/index.js.map +1 -0
  68. package/dist/renderer/public/favicon.svg +4 -0
  69. package/dist/renderer/types.d.ts +34 -0
  70. package/dist/renderer/types.d.ts.map +1 -0
  71. package/dist/renderer/types.js +4 -0
  72. package/dist/renderer/types.js.map +1 -0
  73. package/dist/validate/adapters/files.d.ts +17 -0
  74. package/dist/validate/adapters/files.d.ts.map +1 -0
  75. package/dist/validate/adapters/files.js +50 -0
  76. package/dist/validate/adapters/files.js.map +1 -0
  77. package/dist/validate/adapters/forge.d.ts +25 -0
  78. package/dist/validate/adapters/forge.d.ts.map +1 -0
  79. package/dist/validate/adapters/forge.js +100 -0
  80. package/dist/validate/adapters/forge.js.map +1 -0
  81. package/dist/validate/adapters/http.d.ts +24 -0
  82. package/dist/validate/adapters/http.d.ts.map +1 -0
  83. package/dist/validate/adapters/http.js +75 -0
  84. package/dist/validate/adapters/http.js.map +1 -0
  85. package/dist/validate/adapters/registry.d.ts +16 -0
  86. package/dist/validate/adapters/registry.d.ts.map +1 -0
  87. package/dist/validate/adapters/registry.js +91 -0
  88. package/dist/validate/adapters/registry.js.map +1 -0
  89. package/dist/validate/core/metadata.d.ts +67 -0
  90. package/dist/validate/core/metadata.d.ts.map +1 -0
  91. package/dist/validate/core/metadata.js +149 -0
  92. package/dist/validate/core/metadata.js.map +1 -0
  93. package/dist/validate/core/ownership.d.ts +43 -0
  94. package/dist/validate/core/ownership.d.ts.map +1 -0
  95. package/dist/validate/core/ownership.js +33 -0
  96. package/dist/validate/core/ownership.js.map +1 -0
  97. package/dist/validate/core/paths.d.ts +21 -0
  98. package/dist/validate/core/paths.d.ts.map +1 -0
  99. package/dist/validate/core/paths.js +34 -0
  100. package/dist/validate/core/paths.js.map +1 -0
  101. package/dist/validate/index.d.ts +40 -0
  102. package/dist/validate/index.d.ts.map +1 -0
  103. package/dist/validate/index.js +206 -0
  104. package/dist/validate/index.js.map +1 -0
  105. package/package.json +50 -0
  106. package/templates/README.md +48 -0
  107. package/templates/github-pages.yml +24 -0
  108. package/templates/github-validate.yml +36 -0
  109. package/templates/gitignore +9 -0
  110. package/templates/gitlab-ci.yml +13 -0
  111. package/templates/publish.toml +17 -0
  112. package/templates/reusable/gitlab-index-ci.yml +61 -0
  113. package/templates/reusable/index-pages.yml +80 -0
  114. package/templates/reusable/index-validate.yml +88 -0
@@ -0,0 +1,192 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ // Renders the catalog site. The Astro sources ship inside this package
4
+ // (`./astro`, `../public`); the *data* comes from `<outDir>/all.json`,
5
+ // which `src/data/` compiles — nothing here walks an index tree.
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { build } from "astro";
10
+ import preact from "@astrojs/preact";
11
+ import { resolveConfig } from "../config.js";
12
+ /** Thrown when `<outDir>/all.json` is missing or malformed. */
13
+ export class RenderInputError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "RenderInputError";
17
+ }
18
+ }
19
+ /**
20
+ * Keep `@astrojs/preact` inside the server bundle.
21
+ *
22
+ * Its `server.js` imports the virtual module `astro:preact:opts`, which only
23
+ * exists inside Vite — left external, Node loads the built prerender entry
24
+ * and dies on the `astro:` scheme. Astro infers this for a normal project by
25
+ * crawling the *root's* `package.json`, but the root here is the index repo,
26
+ * which has no reason to name the renderer's own dependencies.
27
+ *
28
+ * It has to be a plugin hook rather than `vite.environments`: Astro rebuilds
29
+ * `environments.prerender` from scratch (`core/build/vite-build-config.js`),
30
+ * so a user-supplied entry under that key is dropped. `configEnvironment`
31
+ * returns are merged for every environment, including `prerender` — which is
32
+ * the one that actually renders the pages.
33
+ */
34
+ const bundlePreactRenderer = {
35
+ name: "grim-indexer:bundle-preact-renderer",
36
+ configEnvironment(_name, options) {
37
+ // `true` already means "bundle everything" — adding to it would replace
38
+ // the boolean with an array and externalize the rest.
39
+ if (options.resolve?.noExternal === true)
40
+ return;
41
+ return { resolve: { noExternal: ["@astrojs/preact"] } };
42
+ },
43
+ };
44
+ const here = path.dirname(fileURLToPath(import.meta.url));
45
+ /** Astro `srcDir` — pages, layouts, components, content config. */
46
+ export const ASTRO_SRC_DIR = path.join(here, "astro");
47
+ /** Default `public/` layer; holds the fallback favicon. */
48
+ export const DEFAULT_PUBLIC_DIR = path.join(here, "public");
49
+ async function readPackages(outDir) {
50
+ const file = path.join(outDir, "all.json");
51
+ let text;
52
+ try {
53
+ text = await fs.readFile(file, "utf8");
54
+ }
55
+ catch (err) {
56
+ if (err.code === "ENOENT") {
57
+ throw new RenderInputError(`${file}: not found — compile the index into outDir first`);
58
+ }
59
+ throw err;
60
+ }
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(text);
64
+ }
65
+ catch (err) {
66
+ throw new RenderInputError(`${file}: ${err.message}`);
67
+ }
68
+ if (!Array.isArray(parsed))
69
+ throw new RenderInputError(`${file}: expected a JSON array`);
70
+ return parsed.sort((a, b) => a.name.localeCompare(b.name));
71
+ }
72
+ async function exists(target) {
73
+ return fs.access(target).then(() => true, () => false);
74
+ }
75
+ /**
76
+ * Read the user CSS override. Its path is contained to `root` because a
77
+ * public index takes contribution PRs, and `grim-indexer validate` builds
78
+ * them — an unconstrained path would let a PR inline any readable file into
79
+ * the published site.
80
+ */
81
+ async function readCustomCss(root, customCss) {
82
+ if (!customCss)
83
+ return "";
84
+ const file = path.resolve(root, customCss);
85
+ if (!file.startsWith(path.resolve(root) + path.sep)) {
86
+ throw new RenderInputError(`customCss must stay inside the index root: ${customCss}`);
87
+ }
88
+ try {
89
+ return await fs.readFile(file, "utf8");
90
+ }
91
+ catch (err) {
92
+ if (err.code === "ENOENT") {
93
+ throw new RenderInputError(`${file}: customCss file not found`);
94
+ }
95
+ throw err;
96
+ }
97
+ }
98
+ /**
99
+ * What Vite must be able to resolve from the Astro root. An index repo has
100
+ * none of them — `npx @grimoire-rs/indexer build` runs with no local install — so
101
+ * the staged root gets a `node_modules/` of symlinks to wherever *this*
102
+ * package's copies actually live.
103
+ */
104
+ const RUNTIME_DEPS = ["astro", "@astrojs/preact", "preact"];
105
+ // Node refuses to create a directory symlink on Windows without elevation;
106
+ // a junction needs no privilege and behaves the same for resolution.
107
+ const LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
108
+ /**
109
+ * Build the directory Astro treats as its project root, for one build.
110
+ *
111
+ * It cannot be the index repo. Astro resolves its own runtime, and every
112
+ * `optimizeDeps` entry, starting from `root` — and a user's index repo has
113
+ * no `node_modules` anywhere on its parent chain, so nothing resolves. It
114
+ * cannot be this package's own directory either: Astro writes `.astro/` and
115
+ * a Vite cache into the root, and an installed package (or an `npx` cache)
116
+ * is the wrong place for that, besides serialising concurrent builds.
117
+ *
118
+ * So: a scratch dir that borrows this package's dependencies by symlink. The
119
+ * index repo contributes data (`all.json`, already read), `enrich/`,
120
+ * `public/`, and the output location — nothing that has to resolve.
121
+ *
122
+ * It sits under the index repo rather than in `os.tmpdir()` because Astro
123
+ * renames the built server assets out of it and into `outDir`, and that
124
+ * rename fails with EXDEV across filesystems. Removed again in `finally`.
125
+ *
126
+ * `public/` is three layers, later wins: the packaged defaults, the index
127
+ * repo's own `public/`, and whatever the data compile already wrote into
128
+ * `outDir`. That last layer is the point — Astro empties `outDir` before it
129
+ * builds, and `/all.json` is a frozen public URL that must survive.
130
+ */
131
+ async function stage(root, outDir) {
132
+ const dir = await fs.mkdtemp(path.join(root, ".index-"));
133
+ const src = path.join(dir, "src");
134
+ await fs.cp(ASTRO_SRC_DIR, src, { recursive: true });
135
+ for (const dep of RUNTIME_DEPS) {
136
+ const real = path.dirname(fileURLToPath(import.meta.resolve(`${dep}/package.json`)));
137
+ const link = path.join(dir, "node_modules", dep);
138
+ await fs.mkdir(path.dirname(link), { recursive: true }); // `@scope/` needs its dir
139
+ await fs.symlink(real, link, LINK_TYPE);
140
+ }
141
+ // `content.config.ts` globs `./enrich` relative to the Astro root, so the
142
+ // index repo's tree has to appear there. Absent is fine: the glob loader
143
+ // degrades to an empty collection.
144
+ const enrich = path.join(root, "enrich");
145
+ if (await exists(enrich))
146
+ await fs.symlink(enrich, path.join(dir, "enrich"), LINK_TYPE);
147
+ const staticDir = path.join(dir, "public");
148
+ await fs.mkdir(staticDir, { recursive: true });
149
+ for (const layer of [DEFAULT_PUBLIC_DIR, path.join(root, "public"), outDir]) {
150
+ // ponytail: copy, not rename — an index's data is kilobytes plus logos.
151
+ if (await exists(layer))
152
+ await fs.cp(layer, staticDir, { recursive: true, force: true });
153
+ }
154
+ return { dir, src };
155
+ }
156
+ /** Render the catalog site from `<outDir>/all.json` into `outDir`. */
157
+ export async function buildSite(opts) {
158
+ const config = resolveConfig(opts.config);
159
+ const packages = await readPackages(opts.outDir);
160
+ const css = await readCustomCss(opts.root, config.customCss);
161
+ const staged = await stage(opts.root, opts.outDir);
162
+ // Astro routes its server build through `<cwd>/.astro/` whenever `outDir`
163
+ // is not under `process.cwd()` (`getOutDirWithinCwd`). That fallback is
164
+ // wanted here: the built prerender entry is imported by plain Node, so it
165
+ // has to sit where the staged `node_modules` resolves, not in an index
166
+ // repo that has none. Running from the staged dir puts it there.
167
+ const cwd = process.cwd();
168
+ process.chdir(staged.dir);
169
+ try {
170
+ await build({
171
+ // The staged dir, never the index repo — see `stage`.
172
+ root: staged.dir,
173
+ // The index repo owns no Astro config; picking one up would let a
174
+ // stray file silently change the frozen routes.
175
+ configFile: false,
176
+ srcDir: staged.src,
177
+ publicDir: path.join(staged.dir, "public"),
178
+ outDir: opts.outDir,
179
+ site: config.site,
180
+ integrations: [preact()],
181
+ vite: {
182
+ define: { __GRIMOIRE_DATA__: JSON.stringify({ config, packages, css }) },
183
+ plugins: [bundlePreactRenderer],
184
+ },
185
+ });
186
+ }
187
+ finally {
188
+ process.chdir(cwd);
189
+ await fs.rm(staged.dir, { recursive: true, force: true });
190
+ }
191
+ }
192
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/renderer/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <rect width="100" height="100" rx="20" fill="#6d4fc4"/>
3
+ <text x="50" y="72" font-size="62" font-family="ui-monospace, monospace" font-weight="bold" fill="#ffffff" text-anchor="middle">g</text>
4
+ </svg>
@@ -0,0 +1,34 @@
1
+ import type { IndexRecord } from "../data/index.js";
2
+ import type { ResolvedSiteConfig } from "../config.js";
3
+ /**
4
+ * A record from `all.json` viewed through the fields the catalog renders.
5
+ * `IndexRecord` types the frozen index metadata and leaves enrichment as
6
+ * `unknown` under its index signature; this narrows the ones the templates
7
+ * touch. All optional — enrichment is best-effort, the site renders without.
8
+ */
9
+ export interface CatalogPackage extends IndexRecord {
10
+ repository?: string;
11
+ title?: string;
12
+ summary?: string;
13
+ version?: string;
14
+ license?: string;
15
+ keywords?: string[];
16
+ created?: string;
17
+ deprecated?: string | null;
18
+ replacedBy?: string;
19
+ tags?: string[];
20
+ logo?: string;
21
+ }
22
+ /**
23
+ * The build-time payload `buildSite` injects through Vite `define`. Read it
24
+ * through `astro/lib/data.ts`, never from a hydrated island — that would
25
+ * inline the whole catalog into the client bundle.
26
+ */
27
+ export interface GrimoireData {
28
+ config: ResolvedSiteConfig;
29
+ /** Every record from `<outDir>/all.json`, sorted by name. */
30
+ packages: CatalogPackage[];
31
+ /** Contents of `config.customCss`, already read from disk. Empty = none. */
32
+ css: string;
33
+ }
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/renderer/types.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;CACb"}
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/renderer/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC"}
@@ -0,0 +1,17 @@
1
+ export type FileRead = {
2
+ kind: "missing";
3
+ } | {
4
+ kind: "text";
5
+ text: string;
6
+ } | {
7
+ kind: "error";
8
+ reason: string;
9
+ };
10
+ /**
11
+ * Read `relative` from inside `root`.
12
+ *
13
+ * `missing` is a real outcome, not an error — a changed path with no file in
14
+ * the PR tree is a deletion by the namespace owner.
15
+ */
16
+ export declare function readIndexFile(root: string, relative: string): Promise<FileRead>;
17
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../src/validate/adapters/files.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAMtC;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAgCnB"}
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ /**
4
+ * Filesystem reads of a checkout. The PR tree is untrusted data: every read is
5
+ * confined to the tree root, symlinks are refused rather than followed, and a
6
+ * size cap applies before any content is loaded.
7
+ */
8
+ import { lstat, readFile, realpath } from "node:fs/promises";
9
+ import { isAbsolute, resolve, sep } from "node:path";
10
+ import { MAX_METADATA_BYTES } from "../core/metadata.js";
11
+ function contained(base, target) {
12
+ return target === base || target.startsWith(base + sep);
13
+ }
14
+ /**
15
+ * Read `relative` from inside `root`.
16
+ *
17
+ * `missing` is a real outcome, not an error — a changed path with no file in
18
+ * the PR tree is a deletion by the namespace owner.
19
+ */
20
+ export async function readIndexFile(root, relative) {
21
+ if (relative.includes("\0") || isAbsolute(relative)) {
22
+ return { kind: "error", reason: "path must be relative to the index root" };
23
+ }
24
+ const base = await realpath(root).catch(() => null);
25
+ if (base === null) {
26
+ return { kind: "error", reason: `cannot resolve index root ${root}` };
27
+ }
28
+ const target = resolve(base, relative);
29
+ if (!contained(base, target)) {
30
+ return { kind: "error", reason: "path escapes the index root" };
31
+ }
32
+ const stat = await lstat(target).catch(() => null);
33
+ if (stat === null)
34
+ return { kind: "missing" };
35
+ if (stat.isSymbolicLink()) {
36
+ return { kind: "error", reason: "symlinks are not accepted" };
37
+ }
38
+ if (!stat.isFile())
39
+ return { kind: "error", reason: "not a regular file" };
40
+ if (stat.size > MAX_METADATA_BYTES) {
41
+ return { kind: "error", reason: `larger than ${MAX_METADATA_BYTES} bytes` };
42
+ }
43
+ // Catches a symlinked *parent* directory, which lstat on the leaf cannot see.
44
+ const real = await realpath(target).catch(() => null);
45
+ if (real === null || !contained(base, real)) {
46
+ return { kind: "error", reason: "path escapes the index root" };
47
+ }
48
+ return { kind: "text", text: await readFile(target, "utf8") };
49
+ }
50
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/validate/adapters/files.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAOzD,SAAS,SAAS,CAAC,IAAY,EAAE,MAAc;IAC7C,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAY,EACZ,QAAgB;IAEhB,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,yCAAyC,EAAE,CAAC;IAC9E,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,6BAA6B,IAAI,EAAE,EAAE,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;IAClE,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC9C,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;IAChE,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC3E,IAAI,IAAI,CAAC,IAAI,GAAG,kBAAkB,EAAE,CAAC;QACnC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,kBAAkB,QAAQ,EAAE,CAAC;IAC9E,CAAC;IAED,8EAA8E;IAC9E,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAC5C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;IAClE,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;AAChE,CAAC"}
@@ -0,0 +1,25 @@
1
+ export interface NamespaceIdentity {
2
+ kind: "user" | "org" | "group";
3
+ /** Numeric account/group id as a string — the authorization key. */
4
+ id: string;
5
+ /** Canonical path/login the forge reports. Display and case-folding only. */
6
+ path: string;
7
+ }
8
+ export interface Forge {
9
+ /** Identity of `namespace`, or null when the forge does not know it. */
10
+ namespaceIdentity(namespace: string): Promise<NamespaceIdentity | null>;
11
+ /** Is `author` a member of this org/group namespace? */
12
+ isMember(namespace: string, author: {
13
+ login: string;
14
+ id: string;
15
+ }): Promise<boolean>;
16
+ }
17
+ export interface ForgeConfig {
18
+ /** API base, e.g. `https://api.github.com` or `https://gitlab.com/api/v4`. */
19
+ apiBase: string;
20
+ token?: string;
21
+ /** GitLab only: membership threshold (30 = Developer). */
22
+ minAccessLevel?: number;
23
+ }
24
+ export declare function createForge(forge: "github" | "gitlab", config: ForgeConfig): Forge;
25
+ //# sourceMappingURL=forge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forge.d.ts","sourceRoot":"","sources":["../../../src/validate/adapters/forge.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;IAC/B,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,wEAAwE;IACxE,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACxE,wDAAwD;IACxD,QAAQ,CACN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GACpC,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA8GD,wBAAgB,WAAW,CACzB,KAAK,EAAE,QAAQ,GAAG,QAAQ,EAC1B,MAAM,EAAE,WAAW,GAClB,KAAK,CAEP"}
@@ -0,0 +1,100 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ /**
4
+ * Forge lookups — GitHub and GitLab. Read-only: this module resolves who owns a
5
+ * namespace and whether an account belongs to it. It never merges, comments, or
6
+ * approves; deciding is `core/`'s job and acting is the caller's.
7
+ */
8
+ import { requestJson, USER_AGENT } from "./http.js";
9
+ function field(data, key) {
10
+ if (typeof data !== "object" || data === null)
11
+ return undefined;
12
+ return data[key];
13
+ }
14
+ function idOf(data) {
15
+ const id = field(data, "id");
16
+ return typeof id === "number" && Number.isInteger(id) ? String(id) : null;
17
+ }
18
+ function stringOf(data, key) {
19
+ const value = field(data, key);
20
+ return typeof value === "string" ? value : "";
21
+ }
22
+ /** Trailing slash off, so every path below can start with one. */
23
+ function base(apiBase) {
24
+ return apiBase.replace(/\/+$/, "");
25
+ }
26
+ function github(config) {
27
+ const headers = {
28
+ Accept: "application/vnd.github+json",
29
+ "User-Agent": USER_AGENT,
30
+ };
31
+ if (config.token)
32
+ headers.Authorization = `Bearer ${config.token}`;
33
+ const api = base(config.apiBase);
34
+ return {
35
+ async namespaceIdentity(namespace) {
36
+ const { status, data } = await requestJson(`${api}/users/${encodeURIComponent(namespace)}`, headers);
37
+ if (status !== 200)
38
+ return null;
39
+ const id = idOf(data);
40
+ if (id === null)
41
+ return null;
42
+ return {
43
+ kind: stringOf(data, "type") === "Organization" ? "org" : "user",
44
+ id,
45
+ path: stringOf(data, "login"),
46
+ };
47
+ },
48
+ async isMember(namespace, author) {
49
+ // Public membership only — GitHub does not disclose private membership to
50
+ // a token that is not itself an org member.
51
+ const { status } = await requestJson(`${api}/orgs/${encodeURIComponent(namespace)}/public_members/${encodeURIComponent(author.login)}`, headers);
52
+ return status === 204;
53
+ },
54
+ };
55
+ }
56
+ function gitlab(config) {
57
+ const headers = { "User-Agent": USER_AGENT };
58
+ if (config.token)
59
+ headers["PRIVATE-TOKEN"] = config.token;
60
+ const api = base(config.apiBase);
61
+ const minAccess = config.minAccessLevel ?? 30;
62
+ return {
63
+ async namespaceIdentity(namespace) {
64
+ const group = await requestJson(`${api}/namespaces/${encodeURIComponent(namespace)}`, headers);
65
+ if (group.status === 200 && field(group.data, "kind") !== "user") {
66
+ const id = idOf(group.data);
67
+ if (id !== null) {
68
+ return { kind: "group", id, path: stringOf(group.data, "full_path") };
69
+ }
70
+ }
71
+ // `/namespaces` is membership-scoped, so a bot token cannot see foreign
72
+ // user namespaces. The public `/users` lookup also carries the USER id,
73
+ // which is what grim stamps into `owner.id` (a user namespace's own id is
74
+ // invisible to anyone but its owner).
75
+ const users = await requestJson(`${api}/users?username=${encodeURIComponent(namespace)}`, headers);
76
+ if (users.status !== 200 || !Array.isArray(users.data))
77
+ return null;
78
+ const hits = users.data.filter((user) => stringOf(user, "username").toLowerCase() === namespace.toLowerCase());
79
+ if (hits.length !== 1)
80
+ return null;
81
+ const id = idOf(hits[0]);
82
+ if (id === null)
83
+ return null;
84
+ return { kind: "user", id, path: stringOf(hits[0], "username") };
85
+ },
86
+ async isMember(namespace, author) {
87
+ if (!/^\d+$/.test(author.id))
88
+ return false;
89
+ // `/members/all/` includes inherited membership: a parent-group member
90
+ // owns subgroup namespaces too.
91
+ const { status, data } = await requestJson(`${api}/groups/${encodeURIComponent(namespace)}/members/all/${author.id}`, headers);
92
+ const level = field(data, "access_level");
93
+ return status === 200 && typeof level === "number" && level >= minAccess;
94
+ },
95
+ };
96
+ }
97
+ export function createForge(forge, config) {
98
+ return forge === "github" ? github(config) : gitlab(config);
99
+ }
100
+ //# sourceMappingURL=forge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forge.js","sourceRoot":"","sources":["../../../src/validate/adapters/forge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The one HTTP call site. Node 22's global `fetch`, no HTTP dependency.
3
+ *
4
+ * Bounded on every axis a hostile endpoint controls: a wall-clock timeout, a
5
+ * response-size cap enforced while streaming, and no redirect following — a
6
+ * `Location` under someone else's control is an SSRF pivot past the host
7
+ * policy that was checked before the call.
8
+ */
9
+ export declare const USER_AGENT = "grim-indexer-bot";
10
+ export declare const TIMEOUT_MS = 30000;
11
+ export declare const MAX_RESPONSE_BYTES: number;
12
+ export interface HttpResponse {
13
+ /** HTTP status, or 0 when the request never completed. */
14
+ status: number;
15
+ body: string;
16
+ header(name: string): string;
17
+ }
18
+ export declare function request(url: string, headers?: Record<string, string>): Promise<HttpResponse>;
19
+ /** `request` plus a `JSON.parse` that yields `null` instead of throwing. */
20
+ export declare function requestJson(url: string, headers?: Record<string, string>): Promise<{
21
+ status: number;
22
+ data: unknown;
23
+ }>;
24
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/validate/adapters/http.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,eAAO,MAAM,UAAU,qBAAqB,CAAC;AAC7C,eAAO,MAAM,UAAU,QAAS,CAAC;AACjC,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AA4BD,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,OAAO,CAAC,YAAY,CAAC,CAoBvB;AAED,4EAA4E;AAC5E,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAQ5C"}
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ /**
4
+ * The one HTTP call site. Node 22's global `fetch`, no HTTP dependency.
5
+ *
6
+ * Bounded on every axis a hostile endpoint controls: a wall-clock timeout, a
7
+ * response-size cap enforced while streaming, and no redirect following — a
8
+ * `Location` under someone else's control is an SSRF pivot past the host
9
+ * policy that was checked before the call.
10
+ */
11
+ export const USER_AGENT = "grim-indexer-bot";
12
+ export const TIMEOUT_MS = 30_000;
13
+ export const MAX_RESPONSE_BYTES = 1024 * 1024;
14
+ async function readCapped(response, cap) {
15
+ if (!response.body)
16
+ return "";
17
+ const reader = response.body.getReader();
18
+ const chunks = [];
19
+ let size = 0;
20
+ try {
21
+ for (;;) {
22
+ const { done, value } = await reader.read();
23
+ if (done)
24
+ break;
25
+ if (!value)
26
+ continue;
27
+ size += value.byteLength;
28
+ if (size > cap) {
29
+ await reader.cancel();
30
+ return null;
31
+ }
32
+ chunks.push(value);
33
+ }
34
+ }
35
+ finally {
36
+ reader.releaseLock();
37
+ }
38
+ return Buffer.concat(chunks).toString("utf8");
39
+ }
40
+ export async function request(url, headers = {}) {
41
+ const none = { status: 0, body: "", header: () => "" };
42
+ try {
43
+ const response = await fetch(url, {
44
+ headers: { "User-Agent": USER_AGENT, ...headers },
45
+ // ponytail: redirects are refused, not followed. If a real registry ever
46
+ // needs one, re-run the host policy on the Location and follow one hop.
47
+ redirect: "manual",
48
+ signal: AbortSignal.timeout(TIMEOUT_MS),
49
+ });
50
+ const body = await readCapped(response, MAX_RESPONSE_BYTES);
51
+ if (body === null)
52
+ return none;
53
+ return {
54
+ status: response.status,
55
+ body,
56
+ header: (name) => response.headers.get(name) ?? "",
57
+ };
58
+ }
59
+ catch {
60
+ return none;
61
+ }
62
+ }
63
+ /** `request` plus a `JSON.parse` that yields `null` instead of throwing. */
64
+ export async function requestJson(url, headers = {}) {
65
+ const response = await request(url, headers);
66
+ if (response.body === "")
67
+ return { status: response.status, data: null };
68
+ try {
69
+ return { status: response.status, data: JSON.parse(response.body) };
70
+ }
71
+ catch {
72
+ return { status: response.status, data: null };
73
+ }
74
+ }
75
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/validate/adapters/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export interface RegistryConfig {
2
+ /** `user:token` for registries that do not serve anonymous pull tokens. */
3
+ auth?: string;
4
+ /** Committed registry-host allowlist; also bounds where credentials go. */
5
+ allowlist?: readonly string[];
6
+ }
7
+ interface TokenRequest {
8
+ url: string;
9
+ /** May the configured credential be sent to this realm? */
10
+ credentialed: boolean;
11
+ }
12
+ export declare function tokenRequest(challenge: string, registryHost: string, repository: string, allowlist?: readonly string[]): TokenRequest | null;
13
+ /** True when `<host>/v2/<repository>/tags/list` lists at least one tag. */
14
+ export declare function refReachable(host: string, repository: string, config?: RegistryConfig): Promise<boolean>;
15
+ export {};
16
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/validate/adapters/registry.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAOD,UAAU,YAAY;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAC5B,YAAY,GAAG,IAAI,CAuBrB;AA0BD,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAAmB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAsBlB"}