From 835ffb6929a7884624159cf4fbc6a2c3d1d258e4 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Mon, 15 Jun 2026 19:35:06 -0400 Subject: [PATCH 1/4] add api keys to store interface Signed-off-by: Alex Matson --- .../src/store-internal.ts | 61 ++++++++++++++++ .../src/migrations/013-add-api-key.ts | 20 ++++++ core/wallet-store-sql/src/schema.ts | 10 +++ core/wallet-store-sql/src/store-sql.ts | 69 +++++++++++++++++++ core/wallet-store/src/Store.ts | 15 ++++ 5 files changed, 175 insertions(+) create mode 100644 core/wallet-store-sql/src/migrations/013-add-api-key.ts diff --git a/core/wallet-store-inmemory/src/store-internal.ts b/core/wallet-store-inmemory/src/store-internal.ts index 47efe3270..9097f8769 100644 --- a/core/wallet-store-inmemory/src/store-internal.ts +++ b/core/wallet-store-inmemory/src/store-internal.ts @@ -22,6 +22,7 @@ import { UserLevelRight, MessageRaw, MessageRawStatusUpdate, + ApiKey, } from '@canton-network/core-wallet-store' import { CurrentNetworkWalletFilter } from '@canton-network/core-wallet-store' @@ -30,6 +31,7 @@ interface UserStorage { transactions: Map messageRaws: Map session: Session | undefined + apiKeys: Map userRightsByNetwork: Map> } @@ -77,6 +79,7 @@ export class StoreInternal implements Store, AuthAware { transactions: new Map(), messageRaws: new Map(), session: undefined, + apiKeys: new Map(), userRightsByNetwork: new Map>(), } } @@ -529,4 +532,62 @@ export class StoreInternal implements Store, AuthAware { storage.messageRaws.delete(messageId) this.updateStorage(storage) } + + // API keys + async addApiKey(apiKey: ApiKey): Promise { + const userId = this.assertConnected() + if (apiKey.userId !== userId) { + throw new Error( + `ApiKey userId mismatch: expected ${userId}, got ${apiKey.userId}` + ) + } + + const network = await this.getCurrentNetwork() + if (apiKey.networkId !== network.id) { + throw new Error( + `ApiKey networkId mismatch: expected ${network.id}, got ${apiKey.networkId}` + ) + } + + const storage = this.getStorage() + storage.apiKeys.set(apiKey.id, apiKey) + this.updateStorage(storage) + } + + async listApiKeys(): Promise> { + const userId = this.assertConnected() + const network = await this.getCurrentNetwork() + const storage = this.getStorage() + + return Object.values(storage.apiKeys).filter( + (apiKey) => + apiKey.userId === userId && apiKey.networkId === network.id + ) + } + + async removeApiKey(apiKeyId: string): Promise { + const storage = this.getStorage() + const apiKey = storage.apiKeys.get(apiKeyId) + + if (!apiKey) { + return + } + + const userId = this.assertConnected() + if (apiKey.userId !== userId) { + throw new Error( + `ApiKey userId mismatch: expected ${userId}, got ${apiKey.userId}` + ) + } + + const network = await this.getCurrentNetwork() + if (apiKey.networkId !== network.id) { + throw new Error( + `ApiKey networkId mismatch: expected ${network.id}, got ${apiKey.networkId}` + ) + } + + storage.apiKeys.delete(apiKeyId) + this.updateStorage(storage) + } } diff --git a/core/wallet-store-sql/src/migrations/013-add-api-key.ts b/core/wallet-store-sql/src/migrations/013-add-api-key.ts new file mode 100644 index 000000000..cf28feeb7 --- /dev/null +++ b/core/wallet-store-sql/src/migrations/013-add-api-key.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('apiKeys') + .addColumn('id', 'text', (col) => col.notNull().primaryKey()) + .addColumn('key', 'text', (col) => col.notNull()) + .addColumn('name', 'text', (col) => col.notNull()) + .addColumn('userId', 'text', (col) => col.notNull()) + .addColumn('networkId', 'text', (col) => col.notNull()) + .addColumn('createdAt', 'text', (col) => col.notNull()) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('apiKeys').execute() +} diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index 529da7761..a7518899b 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -115,6 +115,15 @@ interface SessionTable extends Session { userId: UserId } +interface ApiKeyTable { + id: string + key: string + name: string + userId: UserId + networkId: string + createdAt: string +} + export interface DB { migrations: MigrationTable idps: IdpTable @@ -125,6 +134,7 @@ export interface DB { transactions: TransactionTable messagesRaw: MessageRawTable sessions: SessionTable + apiKeys: ApiKeyTable } export const toIdp = (table: IdpTable): Idp => { diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 9aef4a1ba..352cc3a20 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -25,6 +25,7 @@ import { PartyLevelRight, TransactionStatusUpdate, UserLevelRight, + ApiKey, } from '@canton-network/core-wallet-store' import { CamelCasePlugin, Kysely, PostgresDialect, SqliteDialect } from 'kysely' import Database from 'better-sqlite3' @@ -849,6 +850,74 @@ export class StoreSql implements BaseStore, AuthAware { ) .execute() } + + // API keys + async addApiKey(apiKey: ApiKey): Promise { + const userId = this.assertConnected() + if (apiKey.userId !== userId) { + throw new Error( + `ApiKey userId mismatch: expected ${userId}, got ${apiKey.userId}` + ) + } + + const network = await this.getCurrentNetwork() + if (apiKey.networkId !== network.id) { + throw new Error( + `ApiKey networkId mismatch: expected ${network.id}, got ${apiKey.networkId}` + ) + } + + await this.db + .insertInto('apiKeys') + .values({ + id: apiKey.id, + name: apiKey.name, + key: apiKey.key, + createdAt: apiKey.createdAt.toISOString(), + userId, + networkId: network.id, + }) + .execute() + } + + async listApiKeys(): Promise> { + const userId = this.assertConnected() + const network = await this.getCurrentNetwork() + const apiKeys = await this.db + .selectFrom('apiKeys') + .selectAll() + .where((eb) => + eb.and([ + eb('userId', '=', userId), + eb('networkId', '=', network.id), + ]) + ) + .execute() + + return apiKeys.map((row) => ({ + id: row.id, + name: row.name, + key: row.key, + createdAt: new Date(row.createdAt), + userId: row.userId, + networkId: row.networkId, + })) + } + + async removeApiKey(apiKeyId: string): Promise { + const userId = this.assertConnected() + const network = await this.getCurrentNetwork() + await this.db + .deleteFrom('apiKeys') + .where((eb) => + eb.and([ + eb('id', '=', apiKeyId), + eb('userId', '=', userId), + eb('networkId', '=', network.id), + ]) + ) + .execute() + } } export const connection = (config: StoreConfig) => { diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index b25153ffa..63ba1c3f7 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -115,6 +115,16 @@ export interface MessageRawStatusUpdate { signature?: string } +// API keys +export interface ApiKey { + id: string + name: string + key: string + createdAt: Date + userId: string + networkId: string +} + // Store interface for managing wallets, sessions, networks, and transactions export interface Store { @@ -181,4 +191,9 @@ export interface Store { getMessageRaw(messageId: string): Promise listMessageRaws(): Promise> removeMessageRaw(messageId: string): Promise + + // API Key methods + addApiKey(apiKey: ApiKey): Promise + listApiKeys(apiKeyId: string): Promise> + removeApiKey(apiKeyId: string): Promise } From f23bbd0f0d56f0d420ad1ce1ec0e305b59586b11 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Mon, 15 Jun 2026 20:41:28 -0400 Subject: [PATCH 2/4] add rpc methods for api key management Signed-off-by: Alex Matson --- api-specs/openrpc-user-api.json | 122 ++++++++++++++++++ .../src/migrations/013-add-api-key.ts | 2 +- core/wallet-store-sql/src/schema.ts | 2 +- core/wallet-store-sql/src/store-sql.ts | 4 +- core/wallet-store/src/Store.ts | 4 +- core/wallet-user-rpc-client/src/index.ts | 56 +++++++- core/wallet-user-rpc-client/src/openrpc.json | 122 ++++++++++++++++++ .../remote/src/user-api/controller.ts | 59 +++++++++ .../remote/src/user-api/rpc-gen/index.ts | 9 ++ .../remote/src/user-api/rpc-gen/typings.ts | 41 +++++- 10 files changed, 409 insertions(+), 12 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index 4ab3ba3cd..b24b45859 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -942,6 +942,86 @@ "required": ["userId", "isAdmin"] } } + }, + { + "name": "generateApiKey", + "description": "Generates a new API key for the current user and network.", + "params": [ + { + "name": "params", + "schema": { + "title": "GenerateApiKeyParams", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "title": "name", + "type": "string", + "description": "A name to identify the API key." + } + }, + "required": ["name"] + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/GeneratedApiKey" + } + } + }, + { + "name": "listApiKeys", + "description": "Lists all API keys for the current user and network.", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "ListApiKeysResult", + "type": "object", + "additionalProperties": false, + "properties": { + "apiKeys": { + "title": "apiKeys", + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + }, + "description": "The list of API keys." + } + }, + "required": ["apiKeys"] + } + } + }, + { + "name": "removeApiKey", + "description": "Removes an API key for the current user and network.", + "params": [ + { + "name": "params", + "schema": { + "title": "RemoveApiKeyParams", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the API key." + } + }, + "required": ["id"] + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/Null" + } + } } ], "components": { @@ -1468,6 +1548,48 @@ "type": "string", "format": "uuid", "description": "The internal identifier of the pending message-signing request." + }, + "GeneratedApiKey": { + "title": "GeneratedApiKey", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the API key." + }, + "apiKey": { + "title": "apiKeyResult", + "type": "string", + "description": "The generated API key." + } + }, + "required": ["apiKey", "id", "createdAt"] + }, + "ApiKey": { + "title": "ApiKey", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the API key." + }, + "name": { + "title": "name", + "type": "string", + "description": "The name of the API key." + }, + "createdAt": { + "title": "createdAt", + "type": "string", + "format": "date-time", + "description": "The timestamp when the API key was created." + } + }, + "required": ["id", "name", "createdAt"] } } } diff --git a/core/wallet-store-sql/src/migrations/013-add-api-key.ts b/core/wallet-store-sql/src/migrations/013-add-api-key.ts index cf28feeb7..a91b71508 100644 --- a/core/wallet-store-sql/src/migrations/013-add-api-key.ts +++ b/core/wallet-store-sql/src/migrations/013-add-api-key.ts @@ -7,7 +7,7 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('apiKeys') .addColumn('id', 'text', (col) => col.notNull().primaryKey()) - .addColumn('key', 'text', (col) => col.notNull()) + .addColumn('digest', 'text', (col) => col.notNull()) .addColumn('name', 'text', (col) => col.notNull()) .addColumn('userId', 'text', (col) => col.notNull()) .addColumn('networkId', 'text', (col) => col.notNull()) diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index a7518899b..a2efe345f 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -117,7 +117,7 @@ interface SessionTable extends Session { interface ApiKeyTable { id: string - key: string + digest: string name: string userId: UserId networkId: string diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 352cc3a20..00579f5f1 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -872,7 +872,7 @@ export class StoreSql implements BaseStore, AuthAware { .values({ id: apiKey.id, name: apiKey.name, - key: apiKey.key, + digest: apiKey.digest, createdAt: apiKey.createdAt.toISOString(), userId, networkId: network.id, @@ -897,7 +897,7 @@ export class StoreSql implements BaseStore, AuthAware { return apiKeys.map((row) => ({ id: row.id, name: row.name, - key: row.key, + digest: row.digest, createdAt: new Date(row.createdAt), userId: row.userId, networkId: row.networkId, diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index 63ba1c3f7..9d9bd3b63 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -119,7 +119,7 @@ export interface MessageRawStatusUpdate { export interface ApiKey { id: string name: string - key: string + digest: string createdAt: Date userId: string networkId: string @@ -194,6 +194,6 @@ export interface Store { // API Key methods addApiKey(apiKey: ApiKey): Promise - listApiKeys(apiKeyId: string): Promise> + listApiKeys(): Promise> removeApiKey(apiKeyId: string): Promise } diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index ab341a547..00efd4228 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -13,7 +13,7 @@ import { RpcTransport } from '@canton-network/core-rpc-transport' export type NetworkId = string /** * - * Name of network + * The name of the API key. * */ export type Name = string @@ -88,7 +88,7 @@ export interface Network { export type NetworkName = string /** * - * ID of the identity provider + * The unique identifier of the API key. * */ export type Id = string @@ -356,7 +356,7 @@ export type Message = string export type Origin = string /** * - * The timestamp when the transaction was created. + * The timestamp when the API key was created. * */ export type CreatedAt = string @@ -443,6 +443,23 @@ export type UserIdentifier = string * */ export type IsAdminFlag = boolean +/** + * + * The generated API key. + * + */ +export type ApiKeyResult = string +export interface ApiKey { + id: Id + name: Name + createdAt: CreatedAt +} +/** + * + * The list of API keys. + * + */ +export type ApiKeys = ApiKey[] export interface AddNetworkParams { network: Network } @@ -508,6 +525,12 @@ export interface GetTransactionParams { export interface DeleteTransactionParams { transactionId: TransactionId } +export interface GenerateApiKeyParams { + name: Name +} +export interface RemoveApiKeyParams { + id: Id +} /** * * Represents a null value, used in responses where no data is returned. @@ -608,6 +631,13 @@ export interface GetUserResult { userId: UserIdentifier isAdmin: IsAdminFlag } +export interface GeneratedApiKey { + id: Id + apiKey: ApiKeyResult +} +export interface ListApiKeysResult { + apiKeys: ApiKeys +} /** * * Generated! Represents an alias to any of the provided schemas @@ -662,6 +692,11 @@ export type DeleteTransaction = ( params: DeleteTransactionParams ) => Promise export type GetUser = () => Promise +export type GenerateApiKey = ( + params: GenerateApiKeyParams +) => Promise +export type ListApiKeys = () => Promise +export type RemoveApiKey = (params: RemoveApiKeyParams) => Promise /* eslint-enable @typescript-eslint/no-unused-vars */ type Params = T extends (...args: infer A) => any @@ -811,6 +846,21 @@ export type RpcTypes = { params: Params result: Result } + + generateApiKey: { + params: Params + result: Result + } + + listApiKeys: { + params: Params + result: Result + } + + removeApiKey: { + params: Params + result: Result + } } export class WalletJSONRPCUserAPI { diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 4ab3ba3cd..b24b45859 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -942,6 +942,86 @@ "required": ["userId", "isAdmin"] } } + }, + { + "name": "generateApiKey", + "description": "Generates a new API key for the current user and network.", + "params": [ + { + "name": "params", + "schema": { + "title": "GenerateApiKeyParams", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "title": "name", + "type": "string", + "description": "A name to identify the API key." + } + }, + "required": ["name"] + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/GeneratedApiKey" + } + } + }, + { + "name": "listApiKeys", + "description": "Lists all API keys for the current user and network.", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "ListApiKeysResult", + "type": "object", + "additionalProperties": false, + "properties": { + "apiKeys": { + "title": "apiKeys", + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + }, + "description": "The list of API keys." + } + }, + "required": ["apiKeys"] + } + } + }, + { + "name": "removeApiKey", + "description": "Removes an API key for the current user and network.", + "params": [ + { + "name": "params", + "schema": { + "title": "RemoveApiKeyParams", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the API key." + } + }, + "required": ["id"] + } + } + ], + "result": { + "name": "result", + "schema": { + "$ref": "#/components/schemas/Null" + } + } } ], "components": { @@ -1468,6 +1548,48 @@ "type": "string", "format": "uuid", "description": "The internal identifier of the pending message-signing request." + }, + "GeneratedApiKey": { + "title": "GeneratedApiKey", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the API key." + }, + "apiKey": { + "title": "apiKeyResult", + "type": "string", + "description": "The generated API key." + } + }, + "required": ["apiKey", "id", "createdAt"] + }, + "ApiKey": { + "title": "ApiKey", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the API key." + }, + "name": { + "title": "name", + "type": "string", + "description": "The name of the API key." + }, + "createdAt": { + "title": "createdAt", + "type": "string", + "format": "date-time", + "description": "The timestamp when the API key was created." + } + }, + "required": ["id", "name", "createdAt"] } } } diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index ce94b388c..411b54d1b 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -38,6 +38,10 @@ import { SelfSignedAccessTokenResult, Network as ApiNetwork, PublicNetwork, + GenerateApiKeyParams, + GeneratedApiKey, + ListApiKeysResult, + RemoveApiKeyParams, } from './rpc-gen/typings.js' import { Store, Network } from '@canton-network/core-wallet-store' import { Logger } from 'pino' @@ -62,6 +66,7 @@ import { TransactionService } from '../ledger/transaction-service.js' import { StatusEvent } from '../dapp-api/rpc-gen/typings.js' import type { MessageSignatureEvent } from '../dapp-api/rpc-gen/typings.js' import { rpcErrors } from '@canton-network/core-rpc-errors' +import crypto from 'crypto' export const userController = ( kernelInfo: KernelInfo, @@ -1025,6 +1030,60 @@ export const userController = ( await store.removeTransaction(transaction.id) return null }, + generateApiKey: async ( + params: GenerateApiKeyParams + ): Promise => { + const userId = assertConnected(authContext).userId + const network = await store.getCurrentNetwork() + + const apiKeyId = v4() + const generatedApiKey = crypto.randomBytes(32).toString('hex') + const hashedApiKey = crypto + .createHash('sha256') + .update(generatedApiKey) + .digest('hex') + + const storedApiKey = { + id: apiKeyId, + name: params.name, + digest: hashedApiKey, + userId, + networkId: network.id, + createdAt: new Date(), + } + + await store.addApiKey(storedApiKey) + + logDynamically(logger, 'Generated new API key', { + info: { apiKeyId: storedApiKey.id }, + debug: { + id: storedApiKey.id, + name: storedApiKey.name, + userId: storedApiKey.userId, + networkId: storedApiKey.networkId, + createdAt: storedApiKey.createdAt, + }, + }) + + return { + id: storedApiKey.id, + apiKey: generatedApiKey, + } + }, + listApiKeys: async (): Promise => { + const apiKeys = await store.listApiKeys().then((keys) => + keys.map((key) => ({ + id: key.id, + name: key.name, + createdAt: key.createdAt.toISOString(), + })) + ) + return { apiKeys } + }, + removeApiKey: async (params: RemoveApiKeyParams): Promise => { + await store.removeApiKey(params.id) + return null + }, }) } diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/index.ts b/wallet-gateway/remote/src/user-api/rpc-gen/index.ts index 8e263c76a..047640e25 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/index.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/index.ts @@ -29,6 +29,9 @@ import { GetTransaction } from './typings.js' import { ListTransactions } from './typings.js' import { DeleteTransaction } from './typings.js' import { GetUser } from './typings.js' +import { GenerateApiKey } from './typings.js' +import { ListApiKeys } from './typings.js' +import { RemoveApiKey } from './typings.js' export type Methods = { addNetwork: AddNetwork @@ -59,6 +62,9 @@ export type Methods = { listTransactions: ListTransactions deleteTransaction: DeleteTransaction getUser: GetUser + generateApiKey: GenerateApiKey + listApiKeys: ListApiKeys + removeApiKey: RemoveApiKey } function buildController(methods: Methods) { @@ -91,6 +97,9 @@ function buildController(methods: Methods) { listTransactions: methods.listTransactions, deleteTransaction: methods.deleteTransaction, getUser: methods.getUser, + generateApiKey: methods.generateApiKey, + listApiKeys: methods.listApiKeys, + removeApiKey: methods.removeApiKey, } } diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts index cdfbe658d..a618b8a61 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts @@ -12,7 +12,7 @@ export type NetworkId = string /** * - * Name of network + * The name of the API key. * */ export type Name = string @@ -87,7 +87,7 @@ export interface Network { export type NetworkName = string /** * - * ID of the identity provider + * The unique identifier of the API key. * */ export type Id = string @@ -355,7 +355,7 @@ export type Message = string export type Origin = string /** * - * The timestamp when the transaction was created. + * The timestamp when the API key was created. * */ export type CreatedAt = string @@ -442,6 +442,23 @@ export type UserIdentifier = string * */ export type IsAdminFlag = boolean +/** + * + * The generated API key. + * + */ +export type ApiKeyResult = string +export interface ApiKey { + id: Id + name: Name + createdAt: CreatedAt +} +/** + * + * The list of API keys. + * + */ +export type ApiKeys = ApiKey[] export interface AddNetworkParams { network: Network } @@ -507,6 +524,12 @@ export interface GetTransactionParams { export interface DeleteTransactionParams { transactionId: TransactionId } +export interface GenerateApiKeyParams { + name: Name +} +export interface RemoveApiKeyParams { + id: Id +} /** * * Represents a null value, used in responses where no data is returned. @@ -607,6 +630,13 @@ export interface GetUserResult { userId: UserIdentifier isAdmin: IsAdminFlag } +export interface GeneratedApiKey { + id: Id + apiKey: ApiKeyResult +} +export interface ListApiKeysResult { + apiKeys: ApiKeys +} /** * * Generated! Represents an alias to any of the provided schemas @@ -661,3 +691,8 @@ export type DeleteTransaction = ( params: DeleteTransactionParams ) => Promise export type GetUser = () => Promise +export type GenerateApiKey = ( + params: GenerateApiKeyParams +) => Promise +export type ListApiKeys = () => Promise +export type RemoveApiKey = (params: RemoveApiKeyParams) => Promise From 7e933e0e008eea62ee640a8327cd241fbd11451c Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Tue, 16 Jun 2026 10:09:39 -0400 Subject: [PATCH 3/4] store email for signing drivers Signed-off-by: Alex Matson --- core/wallet-store-sql/src/migrations/013-add-api-key.ts | 1 + core/wallet-store-sql/src/schema.ts | 1 + core/wallet-store-sql/src/store-sql.ts | 2 ++ core/wallet-store/src/Store.ts | 1 + wallet-gateway/remote/src/user-api/controller.ts | 1 + 5 files changed, 6 insertions(+) diff --git a/core/wallet-store-sql/src/migrations/013-add-api-key.ts b/core/wallet-store-sql/src/migrations/013-add-api-key.ts index a91b71508..3c3754631 100644 --- a/core/wallet-store-sql/src/migrations/013-add-api-key.ts +++ b/core/wallet-store-sql/src/migrations/013-add-api-key.ts @@ -10,6 +10,7 @@ export async function up(db: Kysely): Promise { .addColumn('digest', 'text', (col) => col.notNull()) .addColumn('name', 'text', (col) => col.notNull()) .addColumn('userId', 'text', (col) => col.notNull()) + .addColumn('email', 'text') .addColumn('networkId', 'text', (col) => col.notNull()) .addColumn('createdAt', 'text', (col) => col.notNull()) .execute() diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index a2efe345f..0ac54ed4c 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -120,6 +120,7 @@ interface ApiKeyTable { digest: string name: string userId: UserId + email: string | null networkId: string createdAt: string } diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 00579f5f1..830c9db3c 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -875,6 +875,7 @@ export class StoreSql implements BaseStore, AuthAware { digest: apiKey.digest, createdAt: apiKey.createdAt.toISOString(), userId, + email: apiKey.email, networkId: network.id, }) .execute() @@ -900,6 +901,7 @@ export class StoreSql implements BaseStore, AuthAware { digest: row.digest, createdAt: new Date(row.createdAt), userId: row.userId, + email: null, // omit email for privacy reasons, even though it's stored in the database networkId: row.networkId, })) } diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index 9d9bd3b63..beb5a2c7b 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -122,6 +122,7 @@ export interface ApiKey { digest: string createdAt: Date userId: string + email: string | null networkId: string } diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 411b54d1b..555b2accd 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -1049,6 +1049,7 @@ export const userController = ( digest: hashedApiKey, userId, networkId: network.id, + email: authContext?.email || null, createdAt: new Date(), } From 911769af304832a362c450d6c81bbee511b5ddb9 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Tue, 16 Jun 2026 10:16:56 -0400 Subject: [PATCH 4/4] migration lock Signed-off-by: Alex Matson --- core/wallet-store-sql/migrations.lock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/wallet-store-sql/migrations.lock.json b/core/wallet-store-sql/migrations.lock.json index 86a5c7fb5..8936584e4 100644 --- a/core/wallet-store-sql/migrations.lock.json +++ b/core/wallet-store-sql/migrations.lock.json @@ -12,6 +12,7 @@ "009-add-wallet-rights-columns": "e967a0db45118b5a736b139b6c484c6f715d2357027633c0c9be4329f3aea6c0", "010-add-transaction-network-id": "e6dfb3ec57036a8b36d2e55f17c619874f87257d2bedc9791010deb661aadd7f", "011-add-transaction-id-primary-key": "8cf57d47b2d3e0a3cd1e76fb6dda00d5eeb44741b8eb22e3419f20f9de069c20", - "012-add-messages-to-sign": "513ae59fb4eb1a69f44ed8a481dce46576ae8899c6e5c2c933579b5fdda09145" + "012-add-messages-to-sign": "513ae59fb4eb1a69f44ed8a481dce46576ae8899c6e5c2c933579b5fdda09145", + "013-add-api-key": "34fbbe1f2807a7ece0aa8f617c4cb05fe5943c3f727c3e0081008011e1a99857" } }