0e97e9ef33
Replace the old profile-era settings flow with online human-admin CRUD backed by the Tribes admin API. Derive the first-admin tutorial step from transient live admin state and remove ACME email plumbing from Legion deployment inputs.
586 lines
16 KiB
TypeScript
586 lines
16 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
|
|
import {
|
|
createBootstrapAdmin,
|
|
decryptBootstrapAdminPrivateKey
|
|
} from "../../src/main/bootstrap-crypto"
|
|
import { buildManagementUrl, TribesAdminApiClient } from "../../src/main/tribes-admin-api"
|
|
import type { TribesSystemTarget } from "../../src/shared/tribes-deployment"
|
|
|
|
const slowTest = process.env["LEGION_RUN_SLOW_TESTS"] === "1" ? test : test.skip
|
|
|
|
test("buildManagementUrl omits default ports", () => {
|
|
assert.equal(
|
|
buildManagementUrl({
|
|
host: "node-a.example.test",
|
|
port: 443,
|
|
scheme: "https"
|
|
}),
|
|
"https://node-a.example.test/api/admin/management"
|
|
)
|
|
|
|
assert.equal(
|
|
buildManagementUrl({
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
}),
|
|
"http://node-a.example.test:4000/api/admin/management"
|
|
)
|
|
|
|
assert.equal(
|
|
buildManagementUrl({
|
|
host: "2001:db8::10",
|
|
port: 443,
|
|
scheme: "https"
|
|
}),
|
|
"https://[2001:db8::10]/api/admin/management"
|
|
)
|
|
})
|
|
|
|
slowTest("TribesAdminApiClient sends NIP-98 authorization header", async () => {
|
|
const admin = createBootstrapAdmin("bootstrap-password-123")
|
|
const privateKeyHex = decryptBootstrapAdminPrivateKey({
|
|
password: "bootstrap-password-123",
|
|
encryptedNostrPrivateKey: admin.encryptedNostrPrivateKey,
|
|
nostrPrivateKeyNonce: admin.nostrPrivateKeyNonce,
|
|
nostrPrivateKeySalt: admin.nostrPrivateKeySalt
|
|
})
|
|
|
|
const originalFetch = globalThis.fetch
|
|
let requestUrl = ""
|
|
let requestHeaders: Headers | undefined
|
|
let requestBody = ""
|
|
|
|
globalThis.fetch = (async (input, init) => {
|
|
requestUrl = String(input)
|
|
requestHeaders = new Headers(init?.headers)
|
|
requestBody = String(init?.body ?? "")
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result: {
|
|
ok: true,
|
|
node: {
|
|
pubkey: "a".repeat(64),
|
|
transport_address: "wss://node-a.example.test:4413/relay",
|
|
scope: "all",
|
|
status: "active",
|
|
activated_at: "2026-04-18T00:00:00Z",
|
|
deactivated_at: null
|
|
}
|
|
}
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
}
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
}
|
|
)
|
|
|
|
const result = await client.clusterNodeUpsert({
|
|
pubkey: "a".repeat(64),
|
|
transportAddress: "wss://node-a.example.test:4413/relay"
|
|
})
|
|
|
|
assert.equal(requestUrl, "http://node-a.example.test:4000/api/admin/management")
|
|
assert.equal(requestHeaders?.get("content-type"), "application/json")
|
|
assert.match(requestHeaders?.get("authorization") ?? "", /^Nostr /)
|
|
assert.match(requestBody, /"cluster_nodes\.upsert"/)
|
|
assert.equal(result.node.pubkey, "a".repeat(64))
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
slowTest("TribesAdminApiClient retries explicit HTTP 503 management responses", async () => {
|
|
const admin = createBootstrapAdmin("bootstrap-password-123")
|
|
const privateKeyHex = decryptBootstrapAdminPrivateKey({
|
|
password: "bootstrap-password-123",
|
|
encryptedNostrPrivateKey: admin.encryptedNostrPrivateKey,
|
|
nostrPrivateKeyNonce: admin.nostrPrivateKeyNonce,
|
|
nostrPrivateKeySalt: admin.nostrPrivateKeySalt
|
|
})
|
|
|
|
const originalFetch = globalThis.fetch
|
|
let attempts = 0
|
|
|
|
globalThis.fetch = (async () => {
|
|
attempts += 1
|
|
|
|
if (attempts === 1) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
ok: false,
|
|
error: "service starting; database not ready"
|
|
}),
|
|
{
|
|
status: 503,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result: {
|
|
ok: true,
|
|
schema_version: "1",
|
|
generated_at: "2026-04-20T00:00:00Z",
|
|
node_count: 0,
|
|
nodes: []
|
|
}
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
}
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
}
|
|
)
|
|
|
|
await client.clusterNodesList()
|
|
assert.equal(attempts, 2)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
slowTest("TribesAdminApiClient reports non-JSON management responses clearly", async () => {
|
|
const admin = createBootstrapAdmin("bootstrap-password-123")
|
|
const privateKeyHex = decryptBootstrapAdminPrivateKey({
|
|
password: "bootstrap-password-123",
|
|
encryptedNostrPrivateKey: admin.encryptedNostrPrivateKey,
|
|
nostrPrivateKeyNonce: admin.nostrPrivateKeyNonce,
|
|
nostrPrivateKeySalt: admin.nostrPrivateKeySalt
|
|
})
|
|
|
|
const originalFetch = globalThis.fetch
|
|
|
|
globalThis.fetch = (async () =>
|
|
new Response("Not Found", {
|
|
status: 404,
|
|
headers: {
|
|
"content-type": "text/plain"
|
|
}
|
|
})) as typeof fetch
|
|
|
|
try {
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
}
|
|
)
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
client.clusterNodeUpsert({
|
|
pubkey: "a".repeat(64),
|
|
transportAddress: "wss://node-a.example.test:4413/relay"
|
|
}),
|
|
/non-JSON response with HTTP 404: Not Found/
|
|
)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
slowTest("TribesAdminApiClient exports, previews, and stores system targets", async () => {
|
|
const admin = createBootstrapAdmin("bootstrap-password-123")
|
|
const privateKeyHex = decryptBootstrapAdminPrivateKey({
|
|
password: "bootstrap-password-123",
|
|
encryptedNostrPrivateKey: admin.encryptedNostrPrivateKey,
|
|
nostrPrivateKeyNonce: admin.nostrPrivateKeyNonce,
|
|
nostrPrivateKeySalt: admin.nostrPrivateKeySalt
|
|
})
|
|
const originalFetch = globalThis.fetch
|
|
const methods: string[] = []
|
|
const target: TribesSystemTarget = {
|
|
channels: [],
|
|
plugins: [],
|
|
rolloutPolicy: {
|
|
prepare_timeout_seconds: 1800,
|
|
commit_timeout_seconds: 300,
|
|
require_all_nodes_ready: true
|
|
}
|
|
}
|
|
|
|
globalThis.fetch = (async (_input, init) => {
|
|
const body = JSON.parse(String(init?.body ?? "{}")) as { method?: string }
|
|
methods.push(String(body.method ?? ""))
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result:
|
|
body.method === "cluster_preview_rollout"
|
|
? {
|
|
ok: true,
|
|
schema_version: "1",
|
|
mode: "auto_switch",
|
|
target,
|
|
plan: {
|
|
plan_schema_version: "1",
|
|
plan_hash: "plan-test",
|
|
resolved_channels: [],
|
|
resolved_plugins: [],
|
|
resolved_extra_packages: [],
|
|
core_migration_target: false,
|
|
core_destructive_rollback_migrations: [],
|
|
closure_estimate_bytes: false
|
|
},
|
|
preview: {}
|
|
}
|
|
: body.method === "cluster_update_sources.export"
|
|
? {
|
|
ok: true,
|
|
schema_version: "1",
|
|
generated_at: "2026-06-18T00:00:00.000Z",
|
|
update_defaults: {
|
|
trusted_signers: [],
|
|
channels: [],
|
|
substitute_servers: [],
|
|
substitute_keys: []
|
|
}
|
|
}
|
|
: {
|
|
ok: true,
|
|
schema_version: "1",
|
|
target
|
|
}
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
}
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
}
|
|
)
|
|
|
|
const exported = await client.clusterExportSystemTarget()
|
|
const preview = await client.clusterPreviewRollout({ mode: "auto_switch" })
|
|
const updateSources = await client.clusterUpdateSourcesExport()
|
|
await client.clusterSetSystemTarget(target)
|
|
|
|
assert.deepEqual(methods, [
|
|
"cluster_export_system_target",
|
|
"cluster_preview_rollout",
|
|
"cluster_update_sources.export",
|
|
"cluster_set_system_target"
|
|
])
|
|
assert.equal(exported.target.rolloutPolicy.prepare_timeout_seconds, 1800)
|
|
assert.equal(preview.plan.plan_hash, "plan-test")
|
|
assert.deepEqual(updateSources.update_defaults.channels, [])
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
slowTest("TribesAdminApiClient calls plugin management methods through plugin.call", async () => {
|
|
const admin = createBootstrapAdmin("bootstrap-password-123")
|
|
const privateKeyHex = decryptBootstrapAdminPrivateKey({
|
|
password: "bootstrap-password-123",
|
|
encryptedNostrPrivateKey: admin.encryptedNostrPrivateKey,
|
|
nostrPrivateKeyNonce: admin.nostrPrivateKeyNonce,
|
|
nostrPrivateKeySalt: admin.nostrPrivateKeySalt
|
|
})
|
|
const originalFetch = globalThis.fetch
|
|
const requests: Array<{ method?: string; params?: Record<string, unknown> }> = []
|
|
|
|
globalThis.fetch = (async (_input, init) => {
|
|
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
method?: string
|
|
params?: Record<string, unknown>
|
|
}
|
|
requests.push(body)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result:
|
|
body.method === "plugin.management_methods"
|
|
? {
|
|
ok: true,
|
|
schema_version: "1",
|
|
plugin: "tribe-one-sender",
|
|
method_count: 1,
|
|
methods: [
|
|
{
|
|
name: "capabilities",
|
|
version: "1",
|
|
auth: "admin",
|
|
sensitive_response: false
|
|
}
|
|
]
|
|
}
|
|
: {
|
|
ok: true,
|
|
stream: {
|
|
id: "stream-1",
|
|
slug: "default"
|
|
}
|
|
}
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
}
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
}
|
|
)
|
|
|
|
const methods = await client.pluginManagementMethods("tribe-one-sender")
|
|
const result = await client.pluginCall<{ ok: true; stream: { id: string } }>(
|
|
"tribe-one-sender",
|
|
"stream.ensure_default",
|
|
{ title: "Main stream" }
|
|
)
|
|
|
|
assert.equal(methods.plugin, "tribe-one-sender")
|
|
assert.equal(result.stream.id, "stream-1")
|
|
assert.deepEqual(
|
|
requests.map((request) => request.method),
|
|
["plugin.management_methods", "plugin.call"]
|
|
)
|
|
assert.deepEqual(requests[1]?.params, {
|
|
plugin: "tribe-one-sender",
|
|
method: "stream.ensure_default",
|
|
version: "1",
|
|
params: {
|
|
title: "Main stream"
|
|
}
|
|
})
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
test("TribesAdminApiClient creates hosted human admins and refreshes the list", async () => {
|
|
const privateKeyHex = "1".repeat(64)
|
|
const originalFetch = globalThis.fetch
|
|
const requests: Array<{ method: string; params: Record<string, unknown> }> = []
|
|
|
|
globalThis.fetch = (async (_input, init) => {
|
|
const body = JSON.parse(String(init?.body ?? "{}")) as {
|
|
method: string
|
|
params: Record<string, unknown>
|
|
}
|
|
requests.push(body)
|
|
|
|
const account = {
|
|
id: "user-1",
|
|
username: "alice",
|
|
display_name: "Alice",
|
|
account_type: "human",
|
|
role: "admin",
|
|
status: "active",
|
|
public_key_hex: "a".repeat(64),
|
|
npub: "npub1alice",
|
|
key_custody: "password_encrypted_private_key",
|
|
inserted_at: "2026-06-26T12:00:00Z",
|
|
updated_at: "2026-06-26T12:00:00Z"
|
|
}
|
|
|
|
const result =
|
|
body.method === "admin_accounts.list"
|
|
? {
|
|
ok: true,
|
|
schema_version: "1",
|
|
accounts: [account],
|
|
account_count: 1,
|
|
next_cursor: null
|
|
}
|
|
: {
|
|
ok: true,
|
|
schema_version: "1",
|
|
account
|
|
}
|
|
|
|
return new Response(JSON.stringify({ ok: true, result }), {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
})
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 4000,
|
|
scheme: "http"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
}
|
|
)
|
|
|
|
const result = await client.adminAccountCreate({
|
|
username: "alice",
|
|
displayName: "Alice",
|
|
password: "alice-password-123",
|
|
privateKeyHex: "b".repeat(64)
|
|
})
|
|
|
|
assert.deepEqual(
|
|
requests.map((request) => request.method),
|
|
["admin_accounts.create", "admin_accounts.list"]
|
|
)
|
|
assert.deepEqual(requests[0]?.params, {
|
|
username: "alice",
|
|
display_name: "Alice",
|
|
account_type: "human",
|
|
role: "admin",
|
|
status: "active",
|
|
key_custody: {
|
|
mode: "password_encrypted_private_key",
|
|
private_key_hex: "b".repeat(64),
|
|
password: "alice-password-123"
|
|
}
|
|
})
|
|
assert.deepEqual(requests[1]?.params, {
|
|
account_type: "human",
|
|
role: "admin"
|
|
})
|
|
assert.equal(result.accounts[0]?.username, "alice")
|
|
assert.equal(result.accounts[0]?.keyCustody, "password_encrypted_private_key")
|
|
assert.equal(result.accounts[0]?.publicKeyHex, "a".repeat(64))
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
slowTest(
|
|
"TribesAdminApiClient enables test-only TLS bypass for self-signed endpoints",
|
|
async () => {
|
|
const admin = createBootstrapAdmin("bootstrap-password-123")
|
|
const privateKeyHex = decryptBootstrapAdminPrivateKey({
|
|
password: "bootstrap-password-123",
|
|
encryptedNostrPrivateKey: admin.encryptedNostrPrivateKey,
|
|
nostrPrivateKeyNonce: admin.nostrPrivateKeyNonce,
|
|
nostrPrivateKeySalt: admin.nostrPrivateKeySalt
|
|
})
|
|
|
|
const originalFetch = globalThis.fetch
|
|
const previousTlsFlag = process.env["NODE_TLS_REJECT_UNAUTHORIZED"]
|
|
let tlsFlagDuringFetch: string | undefined
|
|
|
|
globalThis.fetch = (async () => {
|
|
tlsFlagDuringFetch = process.env["NODE_TLS_REJECT_UNAUTHORIZED"]
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result: {
|
|
ok: true,
|
|
schema_version: "1",
|
|
generated_at: "2026-04-20T00:00:00Z",
|
|
node_count: 0,
|
|
nodes: []
|
|
}
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json"
|
|
}
|
|
}
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
delete process.env["NODE_TLS_REJECT_UNAUTHORIZED"]
|
|
|
|
const client = new TribesAdminApiClient(
|
|
{
|
|
host: "node-a.example.test",
|
|
port: 443,
|
|
scheme: "https"
|
|
},
|
|
{
|
|
privateKeyHex
|
|
},
|
|
{
|
|
allowSelfSignedTls: true
|
|
}
|
|
)
|
|
|
|
await client.clusterNodesList()
|
|
|
|
assert.equal(tlsFlagDuringFetch, "0")
|
|
assert.equal(process.env["NODE_TLS_REJECT_UNAUTHORIZED"], undefined)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
if (typeof previousTlsFlag === "string") {
|
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = previousTlsFlag
|
|
} else {
|
|
delete process.env["NODE_TLS_REJECT_UNAUTHORIZED"]
|
|
}
|
|
}
|
|
}
|
|
)
|