diff --git a/README.md b/README.md index 30326d3..00a4eae 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cli/README.md b/cli/README.md index 0907e09..084953e 100644 --- a/cli/README.md +++ b/cli/README.md @@ -175,6 +175,9 @@ inkbox text mark-conversation-read -i # 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 # Vault key (or set INKBOX_VAULT_KEY) + inkbox vault info # Show vault info inkbox vault secrets # List secrets (metadata only) --type # Filter: login, api_key, ssh_key, key_pair, other diff --git a/cli/package.json b/cli/package.json index 3c1dc2c..eea983a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -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", diff --git a/cli/src/commands/vault.ts b/cli/src/commands/vault.ts index 66b0e94..d9c2407 100644 --- a/cli/src/commands/vault.ts +++ b/cli/src/commands/vault.ts @@ -13,12 +13,11 @@ export function registerVaultCommands(program: Command): void { vault .command("init") .description("Initialize a vault for an organization") - .requiredOption("--organization-id ", "Organization ID") .option("--vault-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; @@ -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 }); }), ); diff --git a/examples/use-inkbox-browser-use/pyproject.toml b/examples/use-inkbox-browser-use/pyproject.toml index fae1f4a..f04e034 100644 --- a/examples/use-inkbox-browser-use/pyproject.toml +++ b/examples/use-inkbox-browser-use/pyproject.toml @@ -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 diff --git a/sdk/python/README.md b/sdk/python/README.md index 358482e..1791c0b 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -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 diff --git a/sdk/python/inkbox/client.py b/sdk/python/inkbox/client.py index fa5365a..82b2b77 100644 --- a/sdk/python/inkbox/client.py +++ b/sdk/python/inkbox/client.py @@ -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, @@ -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) @@ -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 diff --git a/sdk/python/inkbox/vault/resources/vault.py b/sdk/python/inkbox/vault/resources/vault.py index cfd0571..59f13aa 100644 --- a/sdk/python/inkbox/vault/resources/vault.py +++ b/sdk/python/inkbox/vault/resources/vault.py @@ -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 @@ -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. @@ -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 @@ -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( diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 5973c39..a1fa622 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -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" @@ -15,7 +15,6 @@ dependencies = [ "argon2-cffi>=23.1", "cryptography>=43.0", "httpx>=0.27", - "python-dotenv>=1.2.2", ] [project.optional-dependencies] diff --git a/sdk/python/tests/test_vault_resource.py b/sdk/python/tests/test_vault_resource.py index 8551829..de3b20a 100644 --- a/sdk/python/tests/test_vault_resource.py +++ b/sdk/python/tests/test_vault_resource.py @@ -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: @@ -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" @@ -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: @@ -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"] @@ -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"] @@ -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") diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index e4fc8e3..a9fb8f4 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -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 } diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 4d1a8a7..6ef81f3 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -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", diff --git a/sdk/typescript/src/inkbox.ts b/sdk/typescript/src/inkbox.ts index 455992a..f463717 100644 --- a/sdk/typescript/src/inkbox.ts +++ b/sdk/typescript/src/inkbox.ts @@ -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); @@ -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); diff --git a/sdk/typescript/src/vault/resources/vault.ts b/sdk/typescript/src/vault/resources/vault.ts index d688d7d..f6d0558 100644 --- a/sdk/typescript/src/vault/resources/vault.ts +++ b/sdk/typescript/src/vault/resources/vault.ts @@ -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; } // ------------------------------------------------------------------ @@ -80,6 +84,23 @@ export class VaultResource { return parseVaultInfo(data); } + /** + * Fetch the organisation ID via `/api/whoami`. + * @internal + */ + private async fetchOrganizationId(): Promise { + 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. * @@ -91,8 +112,6 @@ 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. @@ -100,8 +119,8 @@ export class VaultResource { */ async initialize( vaultKey: string, - organizationId: string, ): Promise { + const organizationId = await this.fetchOrganizationId(); const orgEncryptionKey = generateOrgEncryptionKey(); const primaryMaterial = await generateVaultKeyMaterial( diff --git a/sdk/typescript/tests/vault/resource.test.ts b/sdk/typescript/tests/vault/resource.test.ts index 21b7426..6d39eaa 100644 --- a/sdk/typescript/tests/vault/resource.test.ts +++ b/sdk/typescript/tests/vault/resource.test.ts @@ -634,6 +634,12 @@ describe("strict AAD enforcement", () => { // ---- VaultResource.initialize tests ---- +function mockApiHttp() { + const apiHttp = mockHttp(); + vi.mocked(apiHttp.get).mockResolvedValue({ organization_id: "org_test_123" }); + return apiHttp; +} + describe("VaultResource.initialize", () => { it("posts crypto material to /initialize and returns result with recovery codes", async () => { const http = mockHttp(); @@ -643,8 +649,8 @@ describe("VaultResource.initialize", () => { recovery_key_count: 4, }); - const res = new VaultResource(http); - const result = await res.initialize(VALID_VAULT_KEY, "org_test_123"); + const res = new VaultResource(http, mockApiHttp()); + const result = await res.initialize(VALID_VAULT_KEY); expect(result.vaultId).toBe("vault-uuid-1234"); expect(result.vaultKeyId).toBe("key-uuid-5678"); @@ -677,8 +683,8 @@ describe("VaultResource.initialize", () => { vi.mocked(http.post).mockResolvedValue({ vault_id: "v1", vault_key_id: "k1", recovery_key_count: 4, }); - const res = new VaultResource(http); - const result = await res.initialize(VALID_VAULT_KEY, "org_test_123"); + const res = new VaultResource(http, mockApiHttp()); + const result = await res.initialize(VALID_VAULT_KEY); for (const code of result.recoveryCodes) { expect(code).toMatch(/^[A-Z2-9]{4}(-[A-Z2-9]{4}){7}$/); @@ -690,8 +696,8 @@ describe("VaultResource.initialize", () => { vi.mocked(http.post).mockResolvedValue({ vault_id: "v1", vault_key_id: "k1", recovery_key_count: 4, }); - const res = new VaultResource(http); - await res.initialize(VALID_VAULT_KEY, "org_test_123"); + const res = new VaultResource(http, mockApiHttp()); + await res.initialize(VALID_VAULT_KEY); const [, body] = vi.mocked(http.post).mock.calls[0]; const allKeys = [body.vault_key, ...body.recovery_keys]; @@ -706,8 +712,8 @@ describe("VaultResource.initialize", () => { vi.mocked(http.post).mockResolvedValue({ vault_id: "v1", vault_key_id: "k1", recovery_key_count: 4, }); - const res = new VaultResource(http); - await res.initialize(VALID_VAULT_KEY, "org_test_123"); + const res = new VaultResource(http, mockApiHttp()); + await res.initialize(VALID_VAULT_KEY); const [, body] = vi.mocked(http.post).mock.calls[0]; const salt = deriveSalt("org_test_123"); @@ -722,8 +728,8 @@ describe("VaultResource.initialize", () => { vi.mocked(http.post).mockResolvedValue({ vault_id: "v1", vault_key_id: "k1", recovery_key_count: 4, }); - const res = new VaultResource(http); - const result = await res.initialize(VALID_VAULT_KEY, "org_test_123"); + const res = new VaultResource(http, mockApiHttp()); + const result = await res.initialize(VALID_VAULT_KEY); const [, body] = vi.mocked(http.post).mock.calls[0]; const salt = deriveSalt("org_test_123"); diff --git a/skills/inkbox-openclaw/SKILL.md b/skills/inkbox-openclaw/SKILL.md index b2563b2..c6009a5 100644 --- a/skills/inkbox-openclaw/SKILL.md +++ b/skills/inkbox-openclaw/SKILL.md @@ -254,6 +254,17 @@ await inkbox.texts.update(phone.id, "text-uuid", { status: "deleted" }); Encrypted credential vault with client-side Argon2id key derivation and AES-256-GCM encryption. The server never sees plaintext secrets. Requires `hash-wasm` (included as a dependency). +### Initialize + +```js +// Initialize a new vault (org ID is fetched automatically from the API key) +const result = await inkbox.vault.initialize("my-Vault-key-01!"); +console.log(result.vaultId, result.vaultKeyId); +for (const code of result.recoveryCodes) { + console.log(code); // save these immediately — they cannot be retrieved again +} +``` + ### Unlock & Read ```js diff --git a/skills/inkbox-python/SKILL.md b/skills/inkbox-python/SKILL.md index e24040c..1a1d158 100644 --- a/skills/inkbox-python/SKILL.md +++ b/skills/inkbox-python/SKILL.md @@ -188,6 +188,16 @@ inkbox.texts.update(phone.id, "text-uuid", status="deleted") Encrypted credential vault with client-side Argon2id key derivation and AES-256-GCM encryption. The server never sees plaintext secrets. Requires `argon2-cffi` and `cryptography` (included as dependencies). +### Initialize + +```python +# Initialize a new vault (org ID is fetched automatically from the API key) +result = inkbox.vault.initialize("my-Vault-key-01!") +print(result.vault_id, result.vault_key_id) +for code in result.recovery_codes: + print(code) # save these immediately — they cannot be retrieved again +``` + ### Unlock & Read ```python diff --git a/skills/inkbox-ts/SKILL.md b/skills/inkbox-ts/SKILL.md index 43fde67..740a04b 100644 --- a/skills/inkbox-ts/SKILL.md +++ b/skills/inkbox-ts/SKILL.md @@ -200,6 +200,17 @@ await inkbox.texts.update(phone.id, "text-uuid", { status: "deleted" }); Encrypted credential vault with client-side Argon2id key derivation and AES-256-GCM encryption. The server never sees plaintext secrets. Requires `hash-wasm` (included as a dependency). +### Initialize + +```typescript +// Initialize a new vault (org ID is fetched automatically from the API key) +const result = await inkbox.vault.initialize("my-Vault-key-01!"); +console.log(result.vaultId, result.vaultKeyId); +for (const code of result.recoveryCodes) { + console.log(code); // save these immediately — they cannot be retrieved again +} +``` + ### Unlock & Read ```typescript