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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ inkbox phone call -i my-agent --to +15551234567
# Read text messages
inkbox text list -i my-agent

# Initialize vault (first time only — requires INKBOX_VAULT_KEY)
inkbox vault init --vault-key "my-vault-key"

# Manage vault secrets
inkbox vault create --name "CRM Login" --type login --username bot@crm.com --password s3cret
inkbox vault secrets
Expand Down
3 changes: 3 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ inkbox text mark-conversation-read <remote-number> -i <handle> # Mark conversat
Encrypted vault operations. `get`, `create`, and credential listing require a vault key.

```bash
inkbox vault init # Initialize vault (creates primary + recovery keys)
--vault-key <key> # Vault key (or set INKBOX_VAULT_KEY)

inkbox vault info # Show vault info
inkbox vault secrets # List secrets (metadata only)
--type <type> # Filter: login, api_key, ssh_key, key_pair, other
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@inkbox/cli",
"version": "0.2.3",
"version": "0.2.4",
"description": "CLI for the Inkbox API",
"license": "MIT",
"type": "module",
Expand Down
5 changes: 2 additions & 3 deletions cli/src/commands/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ export function registerVaultCommands(program: Command): void {
vault
.command("init")
.description("Initialize a vault for an organization")
.requiredOption("--organization-id <id>", "Organization ID")
.option("--vault-key <key>", "Vault key to initialize with")
.action(
withErrorHandler(async function (
this: Command,
cmdOpts: { organizationId: string; vaultKey?: string },
cmdOpts: { vaultKey?: string },
) {
const opts = getGlobalOpts(this);
const vaultKey = cmdOpts.vaultKey ?? opts.vaultKey ?? process.env.INKBOX_VAULT_KEY;
Expand All @@ -29,7 +28,7 @@ export function registerVaultCommands(program: Command): void {
process.exit(1);
}
const inkbox = createClient(opts);
const result = await inkbox.vault.initialize(vaultKey, cmdOpts.organizationId);
const result = await inkbox.vault.initialize(vaultKey);
output(result, { json: !!opts.json });
}),
);
Expand Down
4 changes: 0 additions & 4 deletions examples/use-inkbox-browser-use/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ include = ["src*"]
[project.scripts]
inkbox-browser-use = "src.cli:main"

## override browser-use's pinned python-dotenv to resolve with inkbox
[tool.uv]
override-dependencies = ["python-dotenv>=1.2.2"]

## ruff setup
[tool.ruff]
line-length = 125
2 changes: 1 addition & 1 deletion sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ info = inkbox.vault.info()
print(info.secret_count, info.key_count)

# Initialize a new vault (creates primary key + recovery keys)
result = inkbox.vault.initialize("my-Vault-key-01!", organization_id="org-uuid")
result = inkbox.vault.initialize("my-Vault-key-01!")
for recovery_key in result.recovery_keys:
print(recovery_key.recovery_code) # save these immediately

Expand Down
9 changes: 8 additions & 1 deletion sdk/python/inkbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ def __init__(
base_url=f"{_api_root}/vault",
timeout=timeout,
)
_api_base = f"{base_url.rstrip('/')}/api"
self._root_api_http = MailHttpTransport(
api_key=api_key,
base_url=_api_base,
timeout=timeout,
)
self._api_http = MailHttpTransport(
api_key=api_key,
base_url=_api_root,
Expand All @@ -124,7 +130,7 @@ def __init__(
self._texts = TextsResource(self._phone_http)
self._transcripts = TranscriptsResource(self._phone_http)

self._vault_resource = VaultResource(self._vault_http)
self._vault_resource = VaultResource(self._vault_http, api_http=self._root_api_http)

self._signing_keys = SigningKeysResource(self._api_http)
self._ids_resource = IdentitiesResource(self._ids_http)
Expand All @@ -146,6 +152,7 @@ def close(self) -> None:
self._phone_http.close()
self._ids_http.close()
self._vault_http.close()
self._root_api_http.close()
self._api_http.close()

## Public resource accessors
Expand Down
28 changes: 23 additions & 5 deletions sdk/python/inkbox/vault/resources/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ class VaultResource:
:meth:`unlock` first.
"""

def __init__(self, http: HttpTransport) -> None:
def __init__(
self,
http: HttpTransport,
api_http: HttpTransport | None = None,
) -> None:
self._http = http
self._api_http = api_http
self._unlocked: UnlockedVault | None = None

## Vault metadata
Expand All @@ -67,10 +72,24 @@ def info(self) -> VaultInfo:
data = self._http.get("/info")
return VaultInfo._from_dict(data)

def _fetch_organization_id(self) -> str:
"""Fetch the organisation ID via ``/api/whoami``."""
if self._api_http is None:
raise ValueError(
"organization_id is required when the vault resource "
"is not connected to the API (no api_http transport)"
)
data = self._api_http.get("/whoami")
org_id = data.get("organization_id")
if not org_id:
raise ValueError(
"Could not determine organization ID from API key"
)
return org_id

def initialize(
self,
vault_key: str,
organization_id: str,
) -> VaultInitializeResult:
"""
Initialize a new vault for the organisation.
Expand All @@ -84,9 +103,6 @@ def initialize(
vault_key: The vault key (password) to protect the vault.
Must be at least 16 characters with uppercase, lowercase,
digit, and special character.
organization_id: The organisation ID (needed for key
derivation; the vault does not exist yet so it cannot
be fetched).

Returns:
:class:`~inkbox.vault.types.VaultInitializeResult` containing
Expand All @@ -97,6 +113,8 @@ def initialize(
Raises:
InkboxAPIError: If the organisation already has an active vault (409).
"""
organization_id = self._fetch_organization_id()

org_encryption_key = generate_org_encryption_key()

primary_material = generate_vault_key_material(
Expand Down
3 changes: 1 addition & 2 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "inkbox"
version = "0.2.3"
version = "0.2.4"
description = "Python SDK for the Inkbox API"
readme = "README.md"
requires-python = ">=3.11"
Expand All @@ -15,7 +15,6 @@ dependencies = [
"argon2-cffi>=23.1",
"cryptography>=43.0",
"httpx>=0.27",
"python-dotenv>=1.2.2",
]

[project.optional-dependencies]
Expand Down
14 changes: 8 additions & 6 deletions sdk/python/tests/test_vault_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@

def _resource():
http = MagicMock()
return VaultResource(http), http
api_http = MagicMock()
api_http.get.return_value = {"organization_id": "org_test_123"}
return VaultResource(http, api_http=api_http), http


class TestVaultResourceInfo:
Expand Down Expand Up @@ -679,7 +681,7 @@ def test_posts_crypto_material_and_returns_result(self):
"recovery_key_count": 4,
}

result = res.initialize(VALID_VAULT_KEY, "org_test_123")
result = res.initialize(VALID_VAULT_KEY)

assert isinstance(result, VaultInitializeResult)
assert str(result.vault_id) == "aaaa1111-0000-0000-0000-000000000099"
Expand Down Expand Up @@ -716,7 +718,7 @@ def test_recovery_codes_match_expected_pattern(self):
"vault_id": "aaaa1111-0000-0000-0000-000000000099", "vault_key_id": "bbbb2222-0000-0000-0000-000000000099", "recovery_key_count": 4,
}

result = res.initialize(VALID_VAULT_KEY, "org_test_123")
result = res.initialize(VALID_VAULT_KEY)

pattern = re.compile(r"^[A-Z2-9]{4}(-[A-Z2-9]{4}){7}$")
for code in result.recovery_codes:
Expand All @@ -727,7 +729,7 @@ def test_all_key_ids_and_auth_hashes_unique(self):
http.post.return_value = {
"vault_id": "aaaa1111-0000-0000-0000-000000000099", "vault_key_id": "bbbb2222-0000-0000-0000-000000000099", "recovery_key_count": 4,
}
res.initialize(VALID_VAULT_KEY, "org_test_123")
res.initialize(VALID_VAULT_KEY)

body = http.post.call_args[1]["json"]
all_keys = [body["vault_key"]] + body["recovery_keys"]
Expand All @@ -743,7 +745,7 @@ def test_wrapped_org_key_can_be_round_tripped(self):
http.post.return_value = {
"vault_id": "aaaa1111-0000-0000-0000-000000000099", "vault_key_id": "bbbb2222-0000-0000-0000-000000000099", "recovery_key_count": 4,
}
res.initialize(VALID_VAULT_KEY, "org_test_123")
res.initialize(VALID_VAULT_KEY)

body = http.post.call_args[1]["json"]
vk = body["vault_key"]
Expand All @@ -759,7 +761,7 @@ def test_all_recovery_codes_unwrap_same_org_key(self):
http.post.return_value = {
"vault_id": "aaaa1111-0000-0000-0000-000000000099", "vault_key_id": "bbbb2222-0000-0000-0000-000000000099", "recovery_key_count": 4,
}
result = res.initialize(VALID_VAULT_KEY, "org_test_123")
result = res.initialize(VALID_VAULT_KEY)

body = http.post.call_args[1]["json"]
salt = derive_salt("org_test_123")
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ const info = await inkbox.vault.info();
console.log(info.secretCount, info.keyCount);

// Initialize a new vault (creates primary key + recovery keys)
const result = await inkbox.vault.initialize("my-Vault-key-01!", "org-uuid");
const result = await inkbox.vault.initialize("my-Vault-key-01!");
for (const key of result.recoveryKeys) {
console.log(key.recoveryCode); // save these immediately
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@inkbox/sdk",
"version": "0.2.3",
"version": "0.2.4",
"description": "TypeScript SDK for the Inkbox API",
"license": "MIT",
"type": "module",
Expand Down
13 changes: 7 additions & 6 deletions sdk/typescript/src/inkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,12 @@ export class Inkbox {
const apiRoot = `${baseUrl.replace(/\/$/, "")}/api/v1`;
const ms = options.timeoutMs ?? 30_000;

const mailHttp = new HttpTransport(options.apiKey, `${apiRoot}/mail`, ms);
const phoneHttp = new HttpTransport(options.apiKey, `${apiRoot}/phone`, ms);
const idsHttp = new HttpTransport(options.apiKey, `${apiRoot}/identities`, ms);
const vaultHttp = new HttpTransport(options.apiKey, `${apiRoot}/vault`, ms);
const apiHttp = new HttpTransport(options.apiKey, apiRoot, ms);
const mailHttp = new HttpTransport(options.apiKey, `${apiRoot}/mail`, ms);
const phoneHttp = new HttpTransport(options.apiKey, `${apiRoot}/phone`, ms);
const idsHttp = new HttpTransport(options.apiKey, `${apiRoot}/identities`, ms);
const vaultHttp = new HttpTransport(options.apiKey, `${apiRoot}/vault`, ms);
const rootApiHttp = new HttpTransport(options.apiKey, `${baseUrl.replace(/\/$/, "")}/api`, ms);
const apiHttp = new HttpTransport(options.apiKey, apiRoot, ms);

this._mailboxes = new MailboxesResource(mailHttp);
this._messages = new MessagesResource(mailHttp);
Expand All @@ -119,7 +120,7 @@ export class Inkbox {

this._idsResource = new IdentitiesResource(idsHttp);

this._vaultResource = new VaultResource(vaultHttp);
this._vaultResource = new VaultResource(vaultHttp, rootApiHttp);

if (options.vaultKey !== undefined) {
this._vaultUnlockPromise = this._vaultResource.unlock(options.vaultKey);
Expand Down
27 changes: 23 additions & 4 deletions sdk/typescript/src/vault/resources/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,16 @@ export class VaultResource {
/** @internal */
readonly http: HttpTransport;

/** @internal */
private readonly apiHttp: HttpTransport | null;

/** @internal */
_unlocked: UnlockedVault | null = null;

/** @internal */
constructor(http: HttpTransport) {
constructor(http: HttpTransport, apiHttp?: HttpTransport) {
this.http = http;
this.apiHttp = apiHttp ?? null;
}

// ------------------------------------------------------------------
Expand All @@ -80,6 +84,23 @@ export class VaultResource {
return parseVaultInfo(data);
}

/**
* Fetch the organisation ID via `/api/whoami`.
* @internal
*/
private async fetchOrganizationId(): Promise<string> {
if (!this.apiHttp) {
throw new Error(
"Cannot fetch organization ID: no API transport available",
);
}
const data = await this.apiHttp.get<{ organization_id: string }>("/whoami");
if (!data.organization_id) {
throw new Error("Could not determine organization ID from API key");
}
return data.organization_id;
}

/**
* Initialize a new vault for the organisation.
*
Expand All @@ -91,17 +112,15 @@ export class VaultResource {
* @param vaultKey - The vault key (password) to protect the vault.
* Must be at least 16 characters with uppercase, lowercase, digit,
* and special character.
* @param organizationId - The organisation ID (needed for key
* derivation; the vault does not exist yet so it cannot be fetched).
* @returns {@link VaultInitializeResult} containing the vault ID,
* primary key ID, and recovery codes. The recovery codes must be
* stored securely — they cannot be retrieved again.
* @throws If the organisation already has an active vault (409).
*/
async initialize(
vaultKey: string,
organizationId: string,
): Promise<VaultInitializeResult> {
const organizationId = await this.fetchOrganizationId();
const orgEncryptionKey = generateOrgEncryptionKey();

const primaryMaterial = await generateVaultKeyMaterial(
Expand Down
Loading
Loading