Skip to content
Draft
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
122 changes: 122 additions & 0 deletions api-specs/openrpc-user-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"]
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions core/wallet-store-inmemory/src/store-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
UserLevelRight,
MessageRaw,
MessageRawStatusUpdate,
ApiKey,
} from '@canton-network/core-wallet-store'
import { CurrentNetworkWalletFilter } from '@canton-network/core-wallet-store'

Expand All @@ -30,6 +31,7 @@ interface UserStorage {
transactions: Map<string, Transaction>
messageRaws: Map<string, MessageRaw>
session: Session | undefined
apiKeys: Map<string, ApiKey>
userRightsByNetwork: Map<string, Set<UserLevelRight>>
}

Expand Down Expand Up @@ -77,6 +79,7 @@ export class StoreInternal implements Store, AuthAware<StoreInternal> {
transactions: new Map<string, Transaction>(),
messageRaws: new Map<string, MessageRaw>(),
session: undefined,
apiKeys: new Map<string, ApiKey>(),
userRightsByNetwork: new Map<string, Set<UserLevelRight>>(),
}
}
Expand Down Expand Up @@ -529,4 +532,62 @@ export class StoreInternal implements Store, AuthAware<StoreInternal> {
storage.messageRaws.delete(messageId)
this.updateStorage(storage)
}

// API keys
async addApiKey(apiKey: ApiKey): Promise<void> {
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<Array<ApiKey>> {
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<void> {
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)
}
}
3 changes: 2 additions & 1 deletion core/wallet-store-sql/migrations.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
21 changes: 21 additions & 0 deletions core/wallet-store-sql/src/migrations/013-add-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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<unknown>): Promise<void> {
await db.schema
.createTable('apiKeys')
.addColumn('id', 'text', (col) => col.notNull().primaryKey())
.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()
}

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('apiKeys').execute()
}
11 changes: 11 additions & 0 deletions core/wallet-store-sql/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ interface SessionTable extends Session {
userId: UserId
}

interface ApiKeyTable {
id: string
digest: string
name: string
userId: UserId
email: string | null
networkId: string
createdAt: string
}

export interface DB {
migrations: MigrationTable
idps: IdpTable
Expand All @@ -125,6 +135,7 @@ export interface DB {
transactions: TransactionTable
messagesRaw: MessageRawTable
sessions: SessionTable
apiKeys: ApiKeyTable
}

export const toIdp = (table: IdpTable): Idp => {
Expand Down
71 changes: 71 additions & 0 deletions core/wallet-store-sql/src/store-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -849,6 +850,76 @@ export class StoreSql implements BaseStore, AuthAware<StoreSql> {
)
.execute()
}

// API keys
async addApiKey(apiKey: ApiKey): Promise<void> {
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,
digest: apiKey.digest,
createdAt: apiKey.createdAt.toISOString(),
userId,
email: apiKey.email,
networkId: network.id,
})
.execute()
}

async listApiKeys(): Promise<Array<ApiKey>> {
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,
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,
}))
}

async removeApiKey(apiKeyId: string): Promise<void> {
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) => {
Expand Down
16 changes: 16 additions & 0 deletions core/wallet-store/src/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ export interface MessageRawStatusUpdate {
signature?: string
}

// API keys
export interface ApiKey {
id: string
name: string
digest: string
createdAt: Date
userId: string
email: string | null
networkId: string
}

// Store interface for managing wallets, sessions, networks, and transactions

export interface Store {
Expand Down Expand Up @@ -181,4 +192,9 @@ export interface Store {
getMessageRaw(messageId: string): Promise<MessageRaw | undefined>
listMessageRaws(): Promise<Array<MessageRaw>>
removeMessageRaw(messageId: string): Promise<void>

// API Key methods
addApiKey(apiKey: ApiKey): Promise<void>
listApiKeys(): Promise<Array<ApiKey>>
removeApiKey(apiKeyId: string): Promise<void>
}
Loading
Loading