Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ jobs:
- name: Smoke test keymaster CLI
run: |
keymaster --help > /dev/null
python -c "import argparse; from keymaster.cli import build_parser; p=build_parser(); sp=[a for a in p._actions if isinstance(a, argparse._SubParsersAction)][0]; assert len(sp.choices)==132, f'expected 132 commands, got {len(sp.choices)}'; print(f'keymaster CLI OK ({len(sp.choices)} commands)')"
python -c "import argparse; from keymaster.cli import build_parser; p=build_parser(); sp=[a for a in p._actions if isinstance(a, argparse._SubParsersAction)][0]; assert len(sp.choices)==134, f'expected 134 commands, got {len(sp.choices)}'; print(f'keymaster CLI OK ({len(sp.choices)} commands)')"
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ These rules apply to coding agents working in this repository.
- When generating or updating npm lockfiles, use the repo-pinned npm version from the root `package.json` so lockfiles stay compatible with CI.
- Internal service-to-service admin auth should use `X-Archon-Admin-Key` consistently. Reserve `Authorization` for user/session/OAuth-style flows unless a file explicitly documents a different scheme.
- For Herald agent guidance, prefer Keymaster address commands (`check-address`, `add-address`, `remove-address`, etc.) in quick starts while keeping direct API endpoint documentation available for lower-level integrations.
- Keymaster address metadata should stay in parity across TypeScript and Python implementations; when Herald exposes a domain relay agent, store it with the address as `relay` and surface it through list/get address APIs.
- Publishing a Keymaster address always sets `didDocumentData.address`; it adds the `#email` service endpoint with `type: "Email"` and `serviceEndpoint: "mailto:<address>"` only when the stored address has a Herald `relay`. Unpublishing removes both the property and the service.
- After a PR is merged, always do the standard local cleanup unless the user says otherwise: switch to `main`, fast-forward from `origin/main`, and delete the merged local branch.
- Do not use stash-based branch juggling as the default workflow.
- Never run mutating git operations in parallel. Serialize `git add`, `git commit`, `git push`, branch moves, stash operations, and any command that writes to `.git`.
Expand All @@ -35,3 +37,4 @@ These rules apply to coding agents working in this repository.
- Python keymaster flavor runs in CLI CI use `ARCHON_KEYMASTER_DB=redis` exactly like the TypeScript service; no override is needed.
- The Python keymaster service MUST be a drop-in replacement for the TypeScript keymaster. `docker-compose.keymaster-py.yml` and `docker-compose.keymaster-ts.yml` must agree on env, ports, healthcheck behaviour, volumes, and `user:` overrides. The data dir is a host bind mount of `./data` running as `${ARCHON_UID}:${ARCHON_GID}`, identical to the ts flavor — do not switch py to a named volume to dodge UID issues; fix the UID setup instead.
- For Python package publishing prep, build and check artifacts locally, but do not upload to TestPyPI or PyPI unless the user explicitly asks for publication.
- When reproducing Python CI checks locally, prefer the repo `.venv` Python so helper scripts and imports run in the same prepared environment.
6 changes: 3 additions & 3 deletions apps/browser-extension/src/components/IdentitiesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ function IdentitiesTab() {
setError("Select an address to publish");
return;
}
await keymaster.mergeData(currentId, { address: normalizedAddress });
await keymaster.publishAddress(normalizedAddress, currentId);
setPublishedAddress(normalizedAddress);
await refreshCurrentIdDocs();
setSuccess(`${normalizedAddress} published`);
Expand All @@ -822,7 +822,7 @@ function IdentitiesTab() {
setError("Select an identity first");
return;
}
await keymaster.mergeData(currentId, { address: null });
await keymaster.unpublishAddress(currentId);
setPublishedAddress("");
await refreshCurrentIdDocs();
setSuccess("Address unpublished");
Expand All @@ -846,7 +846,7 @@ function IdentitiesTab() {
}
await keymaster.removeAddress(normalizedAddress);
if (publishedAddress === normalizedAddress && currentId) {
await keymaster.mergeData(currentId, { address: null });
await keymaster.unpublishAddress(currentId);
setPublishedAddress("");
await refreshCurrentIdDocs();
}
Expand Down
6 changes: 3 additions & 3 deletions apps/gatekeeper-client/src/KeymasterUI.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1879,7 +1879,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn
return;
}

await keymaster.mergeData(selectedId, { address: normalizedAddress });
await keymaster.publishAddress(normalizedAddress, selectedId);
setPublishedAddress(normalizedAddress);
showSuccess(`${normalizedAddress} published`);
await resolveId();
Expand All @@ -1898,7 +1898,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn
return;
}

await keymaster.mergeData(selectedId, { address: null });
await keymaster.unpublishAddress(selectedId);
setPublishedAddress('');
showSuccess('Address unpublished');
await resolveId();
Expand All @@ -1922,7 +1922,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn
if (await showConfirm(`Are you sure you want to remove ${normalizedAddress}?`)) {
await keymaster.removeAddress(normalizedAddress);
if (publishedAddress === normalizedAddress && selectedId) {
await keymaster.mergeData(selectedId, { address: null });
await keymaster.unpublishAddress(selectedId);
setPublishedAddress('');
await resolveId();
}
Expand Down
6 changes: 3 additions & 3 deletions apps/keymaster-client/src/KeymasterUI.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1879,7 +1879,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn
return;
}

await keymaster.mergeData(selectedId, { address: normalizedAddress });
await keymaster.publishAddress(normalizedAddress, selectedId);
setPublishedAddress(normalizedAddress);
showSuccess(`${normalizedAddress} published`);
await resolveId();
Expand All @@ -1898,7 +1898,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn
return;
}

await keymaster.mergeData(selectedId, { address: null });
await keymaster.unpublishAddress(selectedId);
setPublishedAddress('');
showSuccess('Address unpublished');
await resolveId();
Expand All @@ -1922,7 +1922,7 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload, hasLightn
if (await showConfirm(`Are you sure you want to remove ${normalizedAddress}?`)) {
await keymaster.removeAddress(normalizedAddress);
if (publishedAddress === normalizedAddress && selectedId) {
await keymaster.mergeData(selectedId, { address: null });
await keymaster.unpublishAddress(selectedId);
setPublishedAddress('');
await resolveId();
}
Expand Down
6 changes: 3 additions & 3 deletions apps/react-wallet/src/components/IdentitiesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ function IdentitiesTab() {
setError("Select an address to publish");
return;
}
await keymaster.mergeData(currentId, { address: normalizedAddress });
await keymaster.publishAddress(normalizedAddress, currentId);
setPublishedAddress(normalizedAddress);
await refreshCurrentIdDocs();
setSuccess(`${normalizedAddress} published`);
Expand All @@ -819,7 +819,7 @@ function IdentitiesTab() {
setError("Select an identity first");
return;
}
await keymaster.mergeData(currentId, { address: null });
await keymaster.unpublishAddress(currentId);
setPublishedAddress("");
await refreshCurrentIdDocs();
setSuccess("Address unpublished");
Expand All @@ -843,7 +843,7 @@ function IdentitiesTab() {
}
await keymaster.removeAddress(normalizedAddress);
if (publishedAddress === normalizedAddress && currentId) {
await keymaster.mergeData(currentId, { address: null });
await keymaster.unpublishAddress(currentId);
setPublishedAddress("");
await refreshCurrentIdDocs();
}
Expand Down
110 changes: 110 additions & 0 deletions docs/keymaster-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2199,6 +2199,116 @@
}
}
},
"/addresses/publish": {
"post": {
"summary": "Publish a stored address to the current identity DID document.",
"description": "Sets `didDocumentData.address` to the selected stored address. If the stored address has a Herald relay, also publishes an `Email` DID service endpoint using `mailto:<address>`.",
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Optional `name@domain` address to publish. Required when the identity has more than one stored address."
},
"name": {
"type": "string",
"description": "Optional identity name. Defaults to the current identity."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Indicates whether the address was successfully published.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
}
},
"delete": {
"summary": "Remove the published address from the current identity DID document.",
"description": "Removes `didDocumentData.address` and the `#email` DID service endpoint from the selected identity.",
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Optional identity name. Defaults to the current identity."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Indicates whether the address was successfully unpublished.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": {
"type": "boolean"
}
}
}
}
}
},
"400": {
"description": "Bad request.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/addresses/{address}": {
"delete": {
"summary": "Remove the stored address for the current identity and revoke it remotely.",
Expand Down
2 changes: 1 addition & 1 deletion docs/services/herald/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ several namespaces:
| Method | Path | Notes |
| --- | --- | --- |
| `GET` | `/api/version` | `1` (the literal integer). Stable schema version of the API. |
| `GET` | `/api/config` | `{ serviceName, serviceDomain, publicUrl, walletUrl }`. |
| `GET` | `/api/config` | `{ serviceName, serviceDID, serviceDomain, publicUrl, walletUrl }`, plus `relayAgent` when the email bridge is enabled. `relayAgent` is the Herald service DID clients can address for dmail/email relay. |

### 2.2 Login (challenge-response)

Expand Down
2 changes: 2 additions & 0 deletions packages/keymaster/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ It then uses `ARCHON_NODE_URL` and `ARCHON_PASSPHRASE` during setup, creates the
| Addresses | `check-address <address>` | Check whether an address is available |
| Addresses | `add-address <address>` | Claim an address for the current ID |
| Addresses | `remove-address <address>` | Remove an address from the current ID |
| Addresses | `publish-address [address] [id]` | Publish an address for the current ID |
| Addresses | `unpublish-address [id]` | Remove the published address |
| Groups | `create-group <name>` | Create a group |
| Groups | `list-groups` | List owned groups |
| Groups | `get-group <did>` | Get group details |
Expand Down
26 changes: 26 additions & 0 deletions packages/keymaster/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,32 @@ program
}
});

program
.command('publish-address [address] [id]')
.description('Publish a stored address for a DID')
.action(async (address, id) => {
try {
await keymaster.publishAddress(address, id);
console.log(UPDATE_OK);
}
catch (error: any) {
console.error(error.error || error.message || error);
}
});

program
.command('unpublish-address [id]')
.description('Remove the published address from a DID')
.action(async (id) => {
try {
await keymaster.unpublishAddress(id);
console.log(UPDATE_OK);
}
catch (error: any) {
console.error(error.error || error.message || error);
}
});

// Nostr commands
program
.command('add-nostr [id]')
Expand Down
20 changes: 20 additions & 0 deletions packages/keymaster/src/keymaster-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,26 @@ export default class KeymasterClient implements KeymasterInterface {
}
}

async publishAddress(address?: string, name?: string): Promise<boolean> {
try {
const response = await this.axios.post(`${this.API}/addresses/publish`, { address, name });
return response.data.ok;
}
catch (error) {
throwError(error);
}
}

async unpublishAddress(name?: string): Promise<boolean> {
try {
const response = await this.axios.delete(`${this.API}/addresses/publish`, { data: { name } });
return response.data.ok;
}
catch (error) {
throwError(error);
}
}

async addNostr(id?: string): Promise<NostrKeys> {
try {
const response = await this.axios.post(`${this.API}/nostr`, { id });
Expand Down
Loading
Loading