@celilo/e2e 0.9.2 → 0.9.3

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 (33) hide show
  1. package/config/proxy/squid.conf +7 -2
  2. package/config/resolver/unbound.conf +13 -3
  3. package/docker/Dockerfile.minio +1 -1
  4. package/docker/Dockerfile.target-machine +1 -1
  5. package/docker/Dockerfile.target-machine-docker +1 -1
  6. package/docker/Dockerfile.target-technitium +1 -1
  7. package/package.json +3 -3
  8. package/scripts/pack-celilo-packages.ts +79 -51
  9. package/src/cli/build.ts +28 -9
  10. package/src/cli/command-registry.ts +34 -6
  11. package/src/cli/index.ts +53 -21
  12. package/src/container-manager.ts +1 -1
  13. package/src/runner.ts +128 -57
  14. package/config/dns/example.net.zone +0 -16
  15. package/config/dns/iamtheinternet.org.zone +0 -16
  16. package/registry-server/package.json +0 -17
  17. package/registry-server/src/auth.test.ts +0 -76
  18. package/registry-server/src/auth.ts +0 -105
  19. package/registry-server/src/bootstrap-packaging.test.ts +0 -71
  20. package/registry-server/src/bootstrap.ts +0 -246
  21. package/registry-server/src/index.ts +0 -16
  22. package/registry-server/src/landing.ts +0 -90
  23. package/registry-server/src/rate-limit.test.ts +0 -62
  24. package/registry-server/src/rate-limit.ts +0 -83
  25. package/registry-server/src/scoped-token-store.test.ts +0 -93
  26. package/registry-server/src/scoped-token-store.ts +0 -110
  27. package/registry-server/src/server.test.ts +0 -509
  28. package/registry-server/src/server.ts +0 -572
  29. package/registry-server/src/storage.test.ts +0 -148
  30. package/registry-server/src/storage.ts +0 -150
  31. package/registry-server/src/validation.test.ts +0 -86
  32. package/registry-server/src/validation.ts +0 -60
  33. package/registry-server/tsconfig.json +0 -15
@@ -1,148 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
- import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import type { IndexEntry } from './storage';
6
- import { RegistryStorage } from './storage';
7
-
8
- let dataDir: string;
9
- let storage: RegistryStorage;
10
-
11
- beforeEach(() => {
12
- dataDir = mkdtempSync(join(tmpdir(), 'celilo-registry-test-'));
13
- storage = new RegistryStorage(dataDir);
14
- });
15
-
16
- afterEach(() => {
17
- rmSync(dataDir, { recursive: true, force: true });
18
- });
19
-
20
- // ── indexPath ─────────────────────────────────────────────────────────────────
21
-
22
- describe('RegistryStorage.indexPath', () => {
23
- test('1-char name → index/1/{name}', () => {
24
- expect(storage.indexPath('a')).toBe(join(dataDir, 'index', '1', 'a'));
25
- });
26
-
27
- test('2-char name → index/2/{name}', () => {
28
- expect(storage.indexPath('ab')).toBe(join(dataDir, 'index', '2', 'ab'));
29
- });
30
-
31
- test('3-char name → index/3/{first2}/{name}', () => {
32
- expect(storage.indexPath('abc')).toBe(join(dataDir, 'index', '3', 'ab', 'abc'));
33
- });
34
-
35
- test('4-char name → index/{01}/{23}/{name}', () => {
36
- expect(storage.indexPath('abcd')).toBe(join(dataDir, 'index', 'ab', 'cd', 'abcd'));
37
- });
38
-
39
- test('long name uses first 4 chars for directories', () => {
40
- expect(storage.indexPath('homebridge')).toBe(
41
- join(dataDir, 'index', 'ho', 'me', 'homebridge'),
42
- );
43
- });
44
-
45
- test('real module names produce expected paths', () => {
46
- expect(storage.indexPath('caddy')).toBe(join(dataDir, 'index', 'ca', 'dd', 'caddy'));
47
- expect(storage.indexPath('celilo-registry')).toBe(
48
- join(dataDir, 'index', 'ce', 'li', 'celilo-registry'),
49
- );
50
- });
51
- });
52
-
53
- // ── readIndex / appendIndex ───────────────────────────────────────────────────
54
-
55
- describe('RegistryStorage index read/write', () => {
56
- function entry(vers: string, yanked = false): IndexEntry {
57
- return { name: 'homebridge', vers, deps: [], cksum: 'abc123', yanked };
58
- }
59
-
60
- test('readIndex returns empty array for unknown module', () => {
61
- expect(storage.readIndex('nonexistent')).toEqual([]);
62
- });
63
-
64
- test('appendIndex then readIndex round-trips entries', () => {
65
- storage.appendIndex(entry('1.0.0+1'));
66
- storage.appendIndex(entry('1.0.0+2'));
67
- const result = storage.readIndex('homebridge');
68
- expect(result).toHaveLength(2);
69
- expect(result[0].vers).toBe('1.0.0+1');
70
- expect(result[1].vers).toBe('1.0.0+2');
71
- });
72
-
73
- test('updateIndex replaces all entries', () => {
74
- storage.appendIndex(entry('1.0.0+1'));
75
- storage.appendIndex(entry('1.0.0+2'));
76
- storage.updateIndex('homebridge', [entry('1.0.0+2', true), entry('1.0.0+3')]);
77
- const result = storage.readIndex('homebridge');
78
- expect(result).toHaveLength(2);
79
- expect(result[0].yanked).toBe(true);
80
- expect(result[1].vers).toBe('1.0.0+3');
81
- });
82
-
83
- test('appendIndex creates parent directories', () => {
84
- storage.appendIndex(entry('1.0.0+1'));
85
- expect(existsSync(storage.indexPath('homebridge'))).toBe(true);
86
- });
87
- });
88
-
89
- // ── storePackage / readPackage ────────────────────────────────────────────────
90
-
91
- describe('RegistryStorage package store/read', () => {
92
- test('storePackage returns sha256 hex', () => {
93
- const data = Buffer.from('fake netapp bytes');
94
- const cksum = storage.storePackage('homebridge', '1.0.0+1', data);
95
- expect(cksum).toMatch(/^[0-9a-f]{64}$/);
96
- });
97
-
98
- test('packageExists returns false before store, true after', () => {
99
- expect(storage.packageExists('homebridge', '1.0.0+1')).toBe(false);
100
- storage.storePackage('homebridge', '1.0.0+1', Buffer.from('data'));
101
- expect(storage.packageExists('homebridge', '1.0.0+1')).toBe(true);
102
- });
103
-
104
- test('readPackage returns stored bytes', () => {
105
- const data = Buffer.from('fake netapp bytes');
106
- storage.storePackage('homebridge', '1.0.0+1', data);
107
- const result = storage.readPackage('homebridge', '1.0.0+1');
108
- expect(result?.toString()).toBe('fake netapp bytes');
109
- });
110
-
111
- test('readPackage returns null for missing version', () => {
112
- expect(storage.readPackage('homebridge', '9.9.9+1')).toBeNull();
113
- });
114
-
115
- test('sha256 is consistent with stored data', () => {
116
- const data = Buffer.from('deterministic content');
117
- const cksum = storage.storePackage('homebridge', '1.0.0+1', data);
118
- expect(cksum).toBe(storage.sha256(data));
119
- });
120
- });
121
-
122
- // ── listModules / getModule ───────────────────────────────────────────────────
123
-
124
- describe('RegistryStorage listModules / getModule', () => {
125
- test('listModules returns empty when no modules stored', () => {
126
- expect(storage.listModules()).toEqual([]);
127
- });
128
-
129
- test('listModules returns stored modules with their index entries', () => {
130
- storage.storePackage('homebridge', '1.0.0+1', Buffer.from('a'));
131
- storage.appendIndex({ name: 'homebridge', vers: '1.0.0+1', deps: [], cksum: 'x', yanked: false });
132
- const list = storage.listModules();
133
- expect(list).toHaveLength(1);
134
- expect(list[0].name).toBe('homebridge');
135
- expect(list[0].versions).toHaveLength(1);
136
- });
137
-
138
- test('getModule returns null for unknown module', () => {
139
- expect(storage.getModule('nonexistent')).toBeNull();
140
- });
141
-
142
- test('getModule returns module with versions when index has entries', () => {
143
- storage.appendIndex({ name: 'caddy', vers: '2.0.0+1', deps: [], cksum: 'y', yanked: false });
144
- const mod = storage.getModule('caddy');
145
- expect(mod?.name).toBe('caddy');
146
- expect(mod?.versions).toHaveLength(1);
147
- });
148
- });
@@ -1,150 +0,0 @@
1
- /**
2
- * File-backed storage for the registry.
3
- *
4
- * Layout under DATA_DIR:
5
- * modules/{name}/{version}/{name}-{version}.netapp — package files
6
- * index/{1,2,ab/cd}/{name} — sparse index files (one JSON line per version)
7
- * downloads/{name} — per-module download counter (single integer)
8
- */
9
-
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
11
- import { createHash } from 'node:crypto';
12
- import { dirname, join } from 'node:path';
13
-
14
- export interface IndexEntry {
15
- name: string;
16
- vers: string;
17
- deps: string[];
18
- cksum: string;
19
- yanked: boolean;
20
- /**
21
- * One-line summary from the module's `manifest.yml#description`,
22
- * captured at publish time. Optional because pre-Phase-2 published
23
- * modules have no description recorded and need a republish to
24
- * backfill — see apps/celilo/designs/REGISTRY_BROWSE_UI.md (Phase 2
25
- * step 0). Old index files without this field still parse cleanly.
26
- */
27
- description?: string;
28
- }
29
-
30
- export interface ModuleVersion {
31
- name: string;
32
- vers: string;
33
- cksum: string;
34
- yanked: boolean;
35
- uploadedAt: string;
36
- }
37
-
38
- export class RegistryStorage {
39
- private readonly dataDir: string;
40
-
41
- constructor(dataDir: string) {
42
- this.dataDir = dataDir;
43
- mkdirSync(join(dataDir, 'modules'), { recursive: true });
44
- mkdirSync(join(dataDir, 'index'), { recursive: true });
45
- }
46
-
47
- // Returns Cargo-style index path for a module name.
48
- // 1-char names → index/1/{name}
49
- // 2-char names → index/2/{name}
50
- // 3-char names → index/3/{first-two-chars}/{name}
51
- // 4+ char names → index/{chars 0-1}/{chars 2-3}/{name}
52
- indexPath(name: string): string {
53
- if (name.length === 1) return join(this.dataDir, 'index', '1', name);
54
- if (name.length === 2) return join(this.dataDir, 'index', '2', name);
55
- if (name.length === 3) return join(this.dataDir, 'index', '3', name.slice(0, 2), name);
56
- return join(this.dataDir, 'index', name.slice(0, 2), name.slice(2, 4), name);
57
- }
58
-
59
- packagePath(name: string, version: string): string {
60
- return join(this.dataDir, 'modules', name, version, `${name}-${version}.netapp`);
61
- }
62
-
63
- sha256(data: Buffer): string {
64
- return createHash('sha256').update(data).digest('hex');
65
- }
66
-
67
- readIndex(name: string): IndexEntry[] {
68
- const path = this.indexPath(name);
69
- if (!existsSync(path)) return [];
70
- return readFileSync(path, 'utf-8')
71
- .split('\n')
72
- .filter(Boolean)
73
- .map((line) => JSON.parse(line) as IndexEntry);
74
- }
75
-
76
- appendIndex(entry: IndexEntry): void {
77
- const path = this.indexPath(entry.name);
78
- mkdirSync(dirname(path), { recursive: true });
79
- writeFileSync(path, `${JSON.stringify(entry)}\n`, { flag: 'a' });
80
- }
81
-
82
- updateIndex(name: string, entries: IndexEntry[]): void {
83
- const path = this.indexPath(name);
84
- mkdirSync(dirname(path), { recursive: true });
85
- writeFileSync(path, `${entries.map((e) => JSON.stringify(e)).join('\n')}\n`);
86
- }
87
-
88
- storePackage(name: string, version: string, data: Buffer): string {
89
- const path = this.packagePath(name, version);
90
- mkdirSync(dirname(path), { recursive: true });
91
- writeFileSync(path, data);
92
- return this.sha256(data);
93
- }
94
-
95
- packageExists(name: string, version: string): boolean {
96
- return existsSync(this.packagePath(name, version));
97
- }
98
-
99
- readPackage(name: string, version: string): Buffer | null {
100
- const path = this.packagePath(name, version);
101
- return existsSync(path) ? readFileSync(path) : null;
102
- }
103
-
104
- listModules(): Array<{ name: string; versions: IndexEntry[] }> {
105
- const modulesDir = join(this.dataDir, 'modules');
106
- if (!existsSync(modulesDir)) return [];
107
- const names = readdirSync(modulesDir, { withFileTypes: true })
108
- .filter((d) => d.isDirectory())
109
- .map((d) => d.name);
110
- return names.map((name) => ({ name, versions: this.readIndex(name) }));
111
- }
112
-
113
- getModule(name: string): { name: string; versions: IndexEntry[] } | null {
114
- const versions = this.readIndex(name);
115
- return versions.length > 0 ? { name, versions } : null;
116
- }
117
-
118
- /**
119
- * Per-module download counter. Stored as a single integer in
120
- * `<dataDir>/downloads/<name>`. Read-modify-write, NOT atomic
121
- * across concurrent processes — a lost increment under contention
122
- * is acceptable for a low-traffic registry. If we ever need real
123
- * concurrency safety this is the place to swap in fcntl locks or
124
- * a sqlite counter.
125
- *
126
- * Validation: `name` is checked by the caller (server.ts uses
127
- * `isValidName` before reaching this layer); we still constrain
128
- * the path lookup to the downloads/ subdirectory as defense in
129
- * depth against path traversal in `name`.
130
- */
131
- private downloadsPath(name: string): string {
132
- return join(this.dataDir, 'downloads', name);
133
- }
134
-
135
- getDownloads(name: string): number {
136
- const path = this.downloadsPath(name);
137
- if (!existsSync(path)) return 0;
138
- const raw = readFileSync(path, 'utf-8').trim();
139
- const n = Number.parseInt(raw, 10);
140
- return Number.isFinite(n) && n >= 0 ? n : 0;
141
- }
142
-
143
- incrementDownloads(name: string): number {
144
- const path = this.downloadsPath(name);
145
- mkdirSync(dirname(path), { recursive: true });
146
- const next = this.getDownloads(name) + 1;
147
- writeFileSync(path, `${next}\n`);
148
- return next;
149
- }
150
- }
@@ -1,86 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { isValidName, isValidVersion, validateNameAndVersion } from './validation';
3
-
4
- describe('isValidName', () => {
5
- test.each(['homebridge', 'my-module', 'a', 'a1', 'dns-external', 'x-y-z-2'])(
6
- 'accepts %s',
7
- (name) => {
8
- expect(isValidName(name)).toBe(true);
9
- },
10
- );
11
-
12
- test.each([
13
- ['empty', ''],
14
- ['uppercase', 'Homebridge'],
15
- ['underscore', 'my_module'],
16
- ['leading hyphen', '-foo'],
17
- ['trailing hyphen', 'foo-'],
18
- ['double hyphen', 'foo--bar'],
19
- ['space', 'foo bar'],
20
- ['path traversal', '../etc'],
21
- ['slash', 'foo/bar'],
22
- ['dot', 'foo.bar'],
23
- ['null byte', 'foo\0bar'],
24
- ['newline', 'foo\nbar'],
25
- ['carriage return', 'foo\rbar'],
26
- ['URL-encoded slash after decode would be', '..%2F..'],
27
- ['over 128 chars', `${'a'.repeat(129)}`],
28
- ])('rejects %s', (_label, name) => {
29
- expect(isValidName(name)).toBe(false);
30
- });
31
- });
32
-
33
- describe('isValidVersion', () => {
34
- test.each(['1.0.0+1', '1.4.2+1', '0.0.1+99', '1.5.0+1', '2:1.0.0+1', '0.0.0+1'])(
35
- 'accepts %s',
36
- (vers) => {
37
- expect(isValidVersion(vers)).toBe(true);
38
- },
39
- );
40
-
41
- test.each([
42
- ['empty', ''],
43
- ['no pkgrev', '1.0.0'],
44
- ['pkgrev not numeric', '1.0.0+abc'],
45
- ['semver not numeric', 'a.b.c+1'],
46
- ['hyphen pkgrev', '1.0.0-1'],
47
- ['path traversal', '../../etc'],
48
- ['slash', '1.0.0+1/foo'],
49
- ['dot-dot', '1.0.0+1/..'],
50
- ['null byte', '1.0.0+1\0'],
51
- ['newline', '1.0.0+1\n'],
52
- ['space', '1.0.0 +1'],
53
- ['extra dot', '1.0.0.0+1'],
54
- ['negative pkgrev', '1.0.0+-1'],
55
- ['four parts', '1.2.3.4+1'],
56
- ['pkgrev missing', '1.0.0+'],
57
- ['URL-encoded bad version', '..%2F..%2Fetc'],
58
- ['over 128 chars', `${'1'.repeat(129)}.0.0+1`],
59
- ])('rejects %s', (_label, vers) => {
60
- expect(isValidVersion(vers)).toBe(false);
61
- });
62
- });
63
-
64
- describe('validateNameAndVersion', () => {
65
- test('accepts a matched valid pair', () => {
66
- expect(validateNameAndVersion('homebridge', '1.4.2+1')).toEqual({ ok: true });
67
- });
68
-
69
- test('flags the name field when bad', () => {
70
- const r = validateNameAndVersion('../etc', '1.0.0+1');
71
- expect(r.ok).toBe(false);
72
- if (!r.ok) expect(r.field).toBe('name');
73
- });
74
-
75
- test('flags the vers field when bad', () => {
76
- const r = validateNameAndVersion('homebridge', '../etc');
77
- expect(r.ok).toBe(false);
78
- if (!r.ok) expect(r.field).toBe('vers');
79
- });
80
-
81
- test('reports name first when both are bad (fail-fast)', () => {
82
- const r = validateNameAndVersion('../etc', '1.0.0');
83
- expect(r.ok).toBe(false);
84
- if (!r.ok) expect(r.field).toBe('name');
85
- });
86
- });
@@ -1,60 +0,0 @@
1
- /**
2
- * Input validation for every user-supplied identifier that ends up on the
3
- * filesystem path. These two are the ONLY allowed shapes for module names
4
- * and versions on this registry; both must be enforced at every public
5
- * entry point in server.ts (publish, download, metadata, yank, unyank,
6
- * sparse index) because `storage.packagePath()` / `storage.indexPath()`
7
- * feed their inputs straight to `node:path.join`, which happily normalizes
8
- * `..` segments and escapes the data directory.
9
- *
10
- * Keep these as pure regex predicates so there is no way to accidentally
11
- * skip them — if the string passes, the path is guaranteed to contain
12
- * only safe chars (lowercase alphanum + `-` / digits + `.` / `+`). No
13
- * slashes, no `..`, no control chars.
14
- */
15
-
16
- /** Module names: kebab-case lowercase, no leading/trailing hyphen. */
17
- const NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
18
-
19
- /**
20
- * Module versions: optional Debian-style epoch (`N:`), then semver
21
- * (major.minor.patch), then a literal `+`, then a numeric pkg revision.
22
- * Examples: `1.4.2+1`, `1.0.0+12`, `2:1.0.0+1` (epoch bump).
23
- * Rejects: `1.0.0+abc`, `1.0.0+1/../etc`, `1.0.0 +1`, empty, traversal.
24
- */
25
- const VERSION_REGEX = /^(\d+:)?\d+\.\d+\.\d+\+\d+$/;
26
-
27
- /** Max bytes we'll accept for either identifier — far beyond any real use. */
28
- const MAX_IDENT_LENGTH = 128;
29
-
30
- export function isValidName(name: string): boolean {
31
- return name.length > 0 && name.length <= MAX_IDENT_LENGTH && NAME_REGEX.test(name);
32
- }
33
-
34
- export function isValidVersion(vers: string): boolean {
35
- return vers.length > 0 && vers.length <= MAX_IDENT_LENGTH && VERSION_REGEX.test(vers);
36
- }
37
-
38
- /** Throw-style version for handler call sites — returns the offending field name. */
39
- export function validateNameAndVersion(
40
- name: string,
41
- vers: string,
42
- ): { ok: true } | { ok: false; field: 'name' | 'vers'; message: string } {
43
- if (!isValidName(name)) {
44
- return {
45
- ok: false,
46
- field: 'name',
47
- message:
48
- 'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
49
- };
50
- }
51
- if (!isValidVersion(vers)) {
52
- return {
53
- ok: false,
54
- field: 'vers',
55
- message:
56
- 'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
57
- };
58
- }
59
- return { ok: true };
60
- }
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "skipLibCheck": true,
8
- "types": ["bun-types"],
9
- "allowImportingTsExtensions": true,
10
- "noEmit": true,
11
- "resolveJsonModule": true,
12
- "isolatedModules": true
13
- },
14
- "include": ["src/**/*"]
15
- }