openvibers/openvibe.host Commits
Provision Directory opt-in listing storage and OAuth (#383)
Changes
@@@ -1,7 +1,7 @@# Directory launch--Directory is a public read service at openvibe.directory, port 5281. It refreshes public source feeds at startup and every six hours. The local-business pilot reads a bounded OpenStreetMap map area. No database, account principal, or provider credentials are needed for this public first release.++Directory serves source-linked public listings at openvibe.directory, port 5281. It refreshes public feeds at startup and every six hours. The local-business pilot reads a bounded OpenStreetMap map area. Signed-in, opt-in people listings use the dedicated `ov_directory` PostgreSQL database and a Network OAuth client.Prepare `/opt/openvibe.directory` from OpenVibers/OpenVibe.Directory with PORT=5281, HOST=127.0.0.1, and BASE_URL=https://openvibe.directory. Run `sudo python3 scripts/register-directory.py` from the current Host checkout; it checks port collisions and refuses to replace a differing entry. Deploy the ready Directory PR through merge-deploy.sh, install the nginx vhost with ov-vhost-install, and verify the home, `/api/v1/kinds`, `/api/v1/entries`, `/sitemap.xml`, and `/llms.txt`.--The service has no private-person search or reverse phone lookup. A later opt-in claim flow needs a signed-in identity and persistent storage before it can publish any person entry.++After the people-listing PRs ship, rerun the registration script to upgrade the existing inventory entry. Run `sudo ovhost data provision directory`; use Network's service-principal script to rotate the seeded `directory` OAuth client into `/etc/openvibe/directory.env` without printing its secret. Deploy Directory, then check `/auth/health` and that `/people/me` directs an anonymous visitor to sign-in. The service has no imported people, private-person search, or reverse phone lookup.@@@ -7,7 +7,7 @@ import tempfileimport sys--def entry():++def old_entry():return {'_note': 'Directory v1: stateless, source-linked public directories; no database or service credentials.','repo': '/opt/openvibe.directory',@@@ -32,16 +32,30 @@ def entry():}++def entry():++ service = old_entry()++ service['_note'] = 'Directory: sourced public reads plus opt-in people listings in PostgreSQL.'++ service['databases'] = [{'name': 'directory', 'engine': 'postgresql', 'database': 'ov_directory'}]++ service['env'] = {'required': ['PORT', 'HOST', 'BASE_URL', 'DATABASE_URL', 'OV_OAUTH_CLIENT_ID', 'OV_OAUTH_CLIENT_SECRET']}++ service['lifecycle']['startupRecovery']['resumes'].append({'kind': 'state', 'what': 'Reload opt-in people listings from PostgreSQL.'})++ service['lifecycle']['rollback']['blockers'] = {'none': 'People migration is expand-only and the previous version ignores the new table.'}++ service['lifecycle']['contracts'] = {'none': 'Directory uses signed Network user tokens and its own PostgreSQL table; the service manifest release is separate.'}++ service['lifecycle']['leases'] = {'none': 'No background writer lease; public source refresh stays in process.'}++ return service++++def register(target):target = pathlib.Path(target)data = json.loads(target.read_text())if 'directory' in data['services']:-- if data['services']['directory'] != entry():++ if data['services']['directory'] == entry():++ print('directory already registered')++ return++ if data['services']['directory'] != old_entry():raise SystemExit('directory already registered with different settings; refusing overwrite')-- print('directory already registered')-- return++ data['services']['directory'] = entry()for name, service in data['services'].items():-- if service.get('port') == 5281 or service.get('drill', {}).get('port') == 5281:++ if name != 'directory' and (service.get('port') == 5281 or service.get('drill', {}).get('port') == 5281):raise SystemExit(f'Port 5281 already belongs to {name}')data['services']['directory'] = entry()st = target.stat()@@@ -3,7 +3,9 @@ const assert=require('node:assert/strict'),fs=require('node:fs'),os=require('nodconst dir=fs.mkdtempSync(path.join(os.tmpdir(),'directory-inventory-')),file=path.join(dir,'host.json');try {const run=()=>spawnSync('python3',[path.join(__dirname,'../scripts/register-directory.py'),file],{encoding:'utf8'});-- fs.writeFileSync(file,JSON.stringify({services:{existing:{port:5000}}}));assert.equal(run().status,0);let d=JSON.parse(fs.readFileSync(file));assert.equal(d.services.existing.port,5000);assert.equal(d.services.directory.port,5281);assert.deepEqual(d.services.directory.databases,[]);assert.deepEqual(require('../lib/lifecycle').check(d.services.directory.lifecycle,{runs:true}),[]);assert.equal(run().status,0);++ fs.writeFileSync(file,JSON.stringify({services:{existing:{port:5000}}}));assert.equal(run().status,0);let d=JSON.parse(fs.readFileSync(file));assert.equal(d.services.existing.port,5000);assert.equal(d.services.directory.port,5281);assert.equal(d.services.directory.databases[0].database,'ov_directory');assert.deepEqual(require('../lib/lifecycle').check(d.services.directory.lifecycle,{runs:true}),[]);assert.equal(run().status,0);++ const legacy=spawnSync('python3',['-c',`import runpy,json;print(json.dumps(runpy.run_path('${path.join(__dirname,'../scripts/register-directory.py')}')['old_entry']()))`],{encoding:'utf8'});++ fs.writeFileSync(file,JSON.stringify({services:{directory:JSON.parse(legacy.stdout)}}));assert.equal(run().status,0);d=JSON.parse(fs.readFileSync(file));assert.equal(d.services.directory.databases[0].database,'ov_directory');fs.writeFileSync(file,JSON.stringify({services:{collision:{port:5281}}}));assert.notEqual(run().status,0);assert(!JSON.parse(fs.readFileSync(file)).services.directory);console.log('directory registration: preserves inventory, idempotent, rejects port collision');} finally {fs.rmSync(dir,{recursive:true,force:true});}
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
# Directory launch
# Directory launch
-Directory is a public read service at openvibe.directory, port 5281. It refreshes public source feeds at startup and every six hours. The local-business pilot reads a bounded OpenStreetMap map area. No database, account principal, or provider credentials are needed for this public first release.
+Directory serves source-linked public listings at openvibe.directory, port 5281. It refreshes public feeds at startup and every six hours. The local-business pilot reads a bounded OpenStreetMap map area. Signed-in, opt-in people listings use the dedicated `ov_directory` PostgreSQL database and a Network OAuth client.
Prepare `/opt/openvibe.directory` from OpenVibers/OpenVibe.Directory with PORT=5281, HOST=127.0.0.1, and BASE_URL=https://openvibe.directory. Run `sudo python3 scripts/register-directory.py` from the current Host checkout; it checks port collisions and refuses to replace a differing entry. Deploy the ready Directory PR through merge-deploy.sh, install the nginx vhost with ov-vhost-install, and verify the home, `/api/v1/kinds`, `/api/v1/entries`, `/sitemap.xml`, and `/llms.txt`.
Prepare `/opt/openvibe.directory` from OpenVibers/OpenVibe.Directory with PORT=5281, HOST=127.0.0.1, and BASE_URL=https://openvibe.directory. Run `sudo python3 scripts/register-directory.py` from the current Host checkout; it checks port collisions and refuses to replace a differing entry. Deploy the ready Directory PR through merge-deploy.sh, install the nginx vhost with ov-vhost-install, and verify the home, `/api/v1/kinds`, `/api/v1/entries`, `/sitemap.xml`, and `/llms.txt`.
-The service has no private-person search or reverse phone lookup. A later opt-in claim flow needs a signed-in identity and persistent storage before it can publish any person entry.
+After the people-listing PRs ship, rerun the registration script to upgrade the existing inventory entry. Run `sudo ovhost data provision directory`; use Network's service-principal script to rotate the seeded `directory` OAuth client into `/etc/openvibe/directory.env` without printing its secret. Deploy Directory, then check `/auth/health` and that `/people/me` directs an anonymous visitor to sign-in. The service has no imported people, private-person search, or reverse phone lookup.
@@ -7,7 +7,7 @@ import tempfile
@@ -7,7 +7,7 @@ import tempfile
import sys
import sys
-def entry():
+def old_entry():
return {
return {
'_note': 'Directory v1: stateless, source-linked public directories; no database or service credentials.',
'_note': 'Directory v1: stateless, source-linked public directories; no database or service credentials.',
'repo': '/opt/openvibe.directory',
'repo': '/opt/openvibe.directory',
@@ -32,16 +32,30 @@ def entry():
@@ -32,16 +32,30 @@ def entry():
}
}
+def entry():
+ service = old_entry()
+ service['_note'] = 'Directory: sourced public reads plus opt-in people listings in PostgreSQL.'
+ service['databases'] = [{'name': 'directory', 'engine': 'postgresql', 'database': 'ov_directory'}]
+ service['env'] = {'required': ['PORT', 'HOST', 'BASE_URL', 'DATABASE_URL', 'OV_OAUTH_CLIENT_ID', 'OV_OAUTH_CLIENT_SECRET']}
+ service['lifecycle']['startupRecovery']['resumes'].append({'kind': 'state', 'what': 'Reload opt-in people listings from PostgreSQL.'})
+ service['lifecycle']['rollback']['blockers'] = {'none': 'People migration is expand-only and the previous version ignores the new table.'}
+ service['lifecycle']['contracts'] = {'none': 'Directory uses signed Network user tokens and its own PostgreSQL table; the service manifest release is separate.'}
+ service['lifecycle']['leases'] = {'none': 'No background writer lease; public source refresh stays in process.'}
+ return service
+
+
def register(target):
def register(target):
target = pathlib.Path(target)
target = pathlib.Path(target)
data = json.loads(target.read_text())
data = json.loads(target.read_text())
if 'directory' in data['services']:
if 'directory' in data['services']:
- if data['services']['directory'] != entry():
+ if data['services']['directory'] == entry():
+ print('directory already registered')
+ return
+ if data['services']['directory'] != old_entry():
raise SystemExit('directory already registered with different settings; refusing overwrite')
raise SystemExit('directory already registered with different settings; refusing overwrite')
- print('directory already registered')
- return
+ data['services']['directory'] = entry()
for name, service in data['services'].items():
for name, service in data['services'].items():
- if service.get('port') == 5281 or service.get('drill', {}).get('port') == 5281:
+ if name != 'directory' and (service.get('port') == 5281 or service.get('drill', {}).get('port') == 5281):
raise SystemExit(f'Port 5281 already belongs to {name}')
raise SystemExit(f'Port 5281 already belongs to {name}')
data['services']['directory'] = entry()
data['services']['directory'] = entry()
st = target.stat()
st = target.stat()
@@ -3,7 +3,9 @@ const assert=require('node:assert/strict'),fs=require('node:fs'),os=require('nod
@@ -3,7 +3,9 @@ const assert=require('node:assert/strict'),fs=require('node:fs'),os=require('nod
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'directory-inventory-')),file=path.join(dir,'host.json');
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'directory-inventory-')),file=path.join(dir,'host.json');
try {
try {
const run=()=>spawnSync('python3',[path.join(__dirname,'../scripts/register-directory.py'),file],{encoding:'utf8'});
const run=()=>spawnSync('python3',[path.join(__dirname,'../scripts/register-directory.py'),file],{encoding:'utf8'});
- fs.writeFileSync(file,JSON.stringify({services:{existing:{port:5000}}}));assert.equal(run().status,0);let d=JSON.parse(fs.readFileSync(file));assert.equal(d.services.existing.port,5000);assert.equal(d.services.directory.port,5281);assert.deepEqual(d.services.directory.databases,[]);assert.deepEqual(require('../lib/lifecycle').check(d.services.directory.lifecycle,{runs:true}),[]);assert.equal(run().status,0);
+ fs.writeFileSync(file,JSON.stringify({services:{existing:{port:5000}}}));assert.equal(run().status,0);let d=JSON.parse(fs.readFileSync(file));assert.equal(d.services.existing.port,5000);assert.equal(d.services.directory.port,5281);assert.equal(d.services.directory.databases[0].database,'ov_directory');assert.deepEqual(require('../lib/lifecycle').check(d.services.directory.lifecycle,{runs:true}),[]);assert.equal(run().status,0);
+ const legacy=spawnSync('python3',['-c',`import runpy,json;print(json.dumps(runpy.run_path('${path.join(__dirname,'../scripts/register-directory.py')}')['old_entry']()))`],{encoding:'utf8'});
+ fs.writeFileSync(file,JSON.stringify({services:{directory:JSON.parse(legacy.stdout)}}));assert.equal(run().status,0);d=JSON.parse(fs.readFileSync(file));assert.equal(d.services.directory.databases[0].database,'ov_directory');
fs.writeFileSync(file,JSON.stringify({services:{collision:{port:5281}}}));assert.notEqual(run().status,0);assert(!JSON.parse(fs.readFileSync(file)).services.directory);
fs.writeFileSync(file,JSON.stringify({services:{collision:{port:5281}}}));assert.notEqual(run().status,0);assert(!JSON.parse(fs.readFileSync(file)).services.directory);
console.log('directory registration: preserves inventory, idempotent, rejects port collision');
console.log('directory registration: preserves inventory, idempotent, rejects port collision');
} finally {fs.rmSync(dir,{recursive:true,force:true});}
} finally {fs.rmSync(dir,{recursive:true,force:true});}