OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-sidecar commits / a8fae42

0.2.52: subscribe to the default NameRegistry on a fresh machine
Name resolution is subscription-based — `resolve-name` searches this sidecar's own registry, then each subscribed replica. On a fresh machine both sources were empty (nothing seeds subscriptions.json), so the sidecar booted its own empty NameRegistry with no replicas and every documented `pgh app install <name>` failed with "name not found in pgh-sidecar's NameRegistry" — only a pgh://<registry-key>/<name> URL worked. Falls back to pgh's DEFAULT_NAME_REGISTRIES when subscriptions.json and PGH_NAME_REGISTRIES are BOTH empty. Explicit configuration wins outright rather than merging, so a peer subscribed to its own namespace registries is never handed ours, and an explicit empty list stays a real "subscribe to nothing" choice. Replicas remain read-only, and the boot loop already skips a subscription matching this sidecar's own key. test/name-registry-default-subscription.test.js covers all five inputs (no file / file without nameRegistries / configured file / env / opts). Constructor-only, so no swarm — opening replicas is gated on enableSeed. Note for future tests here: under Bare `process.env` is a Proxy whose deleteProperty trap returns false, so `delete process.env.X` THROWS; clear by assigning '' instead. Requires pgh 0.1.139 (DEFAULT_NAME_REGISTRIES). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a8fae428058c3dae10e769d3cfa01a97ad0a3574
by plankatron <fleeky@gmail.com> · 2026-08-09T00:57:10.000Z

Changed files (3) +120 -2

lib/index.js +11 -1
……@@ -38,6 +38,7 @@ import { loadOrCreateIdentity } from 'pgh/identity'
3838import { ReleaseRegistry } from 'pgh/release-registry'
3939import { NameRegistry } from 'pgh/name-registry'
4040import { readSubscriptions as readSubscriptionsFromDisk, readNameSubscriptions, addNameSubscription } from 'pgh/subscriptions'
41import { DEFAULT_NAME_REGISTRIES } from 'pgh/bootstrap'
4142
4243import { OverlayManager } from './overlay-manager.js'
4344import { SyncEngine } from 'hyper-overlay/sync-engine'
……@@ -140,12 +141,21 @@ export class Sidecar {
140141 // NameRegistry subscriptions — read-only replicas of OTHER publishers'
141142 // name registries so bare app-name resolution works cross-publisher.
142143 // Sourced from subscriptions.json `nameRegistries` + PGH_NAME_REGISTRIES.
144 //
145 // When BOTH are empty (a fresh machine — nothing seeds either file) fall
146 // back to pgh's DEFAULT_NAME_REGISTRIES. Without it a fresh peer boots its
147 // own empty NameRegistry, `_resolveNameAcross` has nothing to search, and
148 // the documented `pgh app install <name>` fails on every bare name. Any
149 // explicit configuration WINS OUTRIGHT rather than merging, so a peer that
150 // subscribes to its own set of namespace registries never has the default
151 // forced on it (and `pgh names unsubscribe`-style edits stay meaningful).
143152 const nameFromConfig = readNameSubscriptions()
144153 const nameFromEnv = process.env.PGH_NAME_REGISTRIES
145154 ? process.env.PGH_NAME_REGISTRIES.split(',').map(s => s.trim()).filter(Boolean)
146155 : []
156 const nameConfigured = [...new Set([...nameFromConfig, ...nameFromEnv])]
147157 this.nameRegistrySubscriptions = opts.nameRegistrySubscriptions ||
148 [...new Set([...nameFromConfig, ...nameFromEnv])]
158 (nameConfigured.length ? nameConfigured : [...DEFAULT_NAME_REGISTRIES])
149159 this.passphrase = opts.passphrase || null
150160 // Seeding can be disabled in tests (no swarm / no DeployServer)
151161 this.enableSeed = opts.enableSeed !== false
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh-sidecar",
3 "version": "0.2.51",
3 "version": "0.2.52",
44 "description": "Small optional user-level daemon for pgh \u2014 shared identity vault, OTA polling, device linking, bootstrap seed, NameRegistry host",
55 "type": "module",
66 "main": "lib/index.js",
test/name-registry-default-subscription.test.js +108 -0
……@@ -0,0 +1,108 @@
1/**
2 * Default NameRegistry subscription — a fresh machine must resolve bare
3 * app names without a manual `pgh names subscribe`.
4 *
5 * Bug this pins: name resolution is subscription-based (`_resolveNameAcross`
6 * = own registry, then each subscribed replica). Nothing seeded either source
7 * on a fresh machine — not `pgh init`, not install.sh, not any package.json
8 * default — so a new peer booted its OWN empty NameRegistry, had zero
9 * replicas, and every documented `pgh app install <name>` failed with
10 * "name not found in pgh-sidecar's NameRegistry". Only a pgh:// URL worked.
11 *
12 * Contract: configuration WINS OUTRIGHT over the default (no merging), so a
13 * peer subscribed to its own namespace registries never has ours forced on it.
14 *
15 * Constructor-only — no boot, no swarm. The subscription decision happens in
16 * the Sidecar constructor; opening the replicas is gated behind enableSeed.
17 */
18
19import test from 'brittle'
20import fs from '#node:fs'
21import path from '#node:path'
22import os from '#node:os'
23import process from '#node:process'
24
25import { DEFAULT_NAME_REGISTRIES } from 'pgh/bootstrap'
26import { Sidecar } from '../lib/index.js'
27
28const OTHER_KEY = 'a'.repeat(64)
29const THIRD_KEY = 'b'.repeat(64)
30
31// Point subscriptions.json at a scratch file so the test never reads (or
32// writes) the real ~/.config/pgh/subscriptions.json.
33function withSubscriptions (contents, fn) {
34 const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgh-subs-'))
35 const file = path.join(dir, 'subscriptions.json')
36 if (contents !== null) fs.writeFileSync(file, JSON.stringify(contents, null, 2))
37 // NB: under Bare `process.env` is a Proxy whose deleteProperty trap returns
38 // false — `delete process.env.X` THROWS. Both readers treat '' as unset
39 // (plain falsy checks), so clear by assignment instead.
40 const prevPath = process.env.PGH_SUBSCRIPTIONS_PATH || ''
41 const prevEnv = process.env.PGH_NAME_REGISTRIES || ''
42 process.env.PGH_SUBSCRIPTIONS_PATH = file
43 process.env.PGH_NAME_REGISTRIES = ''
44 try {
45 return fn()
46 } finally {
47 process.env.PGH_SUBSCRIPTIONS_PATH = prevPath
48 process.env.PGH_NAME_REGISTRIES = prevEnv
49 fs.rmSync(dir, { recursive: true, force: true })
50 }
51}
52
53// Constructing a Sidecar touches no disk and starts nothing — `_open` does.
54function subscriptionsFor (opts = {}) {
55 return new Sidecar({ enableSeed: false, ...opts }).nameRegistrySubscriptions
56}
57
58test('default is non-empty and well-formed (64-hex keys)', (t) => {
59 t.ok(DEFAULT_NAME_REGISTRIES.length > 0, 'ships at least one default registry')
60 for (const k of DEFAULT_NAME_REGISTRIES) {
61 t.ok(/^[0-9a-f]{64}$/.test(k), k.slice(0, 12) + '… is a 64-char hex key')
62 }
63})
64
65test('fresh machine (no subscriptions.json) gets the default', (t) => {
66 withSubscriptions(null, () => {
67 const subs = subscriptionsFor()
68 for (const k of DEFAULT_NAME_REGISTRIES) {
69 t.ok(subs.includes(k), 'subscribed to default ' + k.slice(0, 12) + '…')
70 }
71 })
72})
73
74test('subscriptions.json without a nameRegistries key gets the default', (t) => {
75 // The real-world fresh shape: `pgh app install pgh://…` writes `registries`
76 // (release) and never touches `nameRegistries`.
77 withSubscriptions({ registries: [OTHER_KEY] }, () => {
78 const subs = subscriptionsFor()
79 t.alike(subs, [...DEFAULT_NAME_REGISTRIES], 'release subs do not count as name subs')
80 })
81})
82
83test('configured nameRegistries win outright — default is NOT merged in', (t) => {
84 withSubscriptions({ nameRegistries: [OTHER_KEY] }, () => {
85 const subs = subscriptionsFor()
86 t.alike(subs, [OTHER_KEY], 'exactly what the user configured')
87 for (const k of DEFAULT_NAME_REGISTRIES) {
88 t.absent(subs.includes(k), 'default not forced on a configured peer')
89 }
90 })
91})
92
93test('PGH_NAME_REGISTRIES wins outright over the default', (t) => {
94 withSubscriptions(null, () => {
95 process.env.PGH_NAME_REGISTRIES = THIRD_KEY
96 const subs = subscriptionsFor()
97 t.alike(subs, [THIRD_KEY], 'env-configured set replaces the default')
98 })
99})
100
101test('explicit opts win over everything', (t) => {
102 withSubscriptions(null, () => {
103 t.alike(subscriptionsFor({ nameRegistrySubscriptions: [OTHER_KEY] }), [OTHER_KEY])
104 // An explicit empty list is a real choice (subscribe to nothing), not
105 // "unset" — it must not fall through to the default.
106 t.alike(subscriptionsFor({ nameRegistrySubscriptions: [] }), [], 'explicit [] opts out')
107 })
108})