feat: introduce mnie

This commit is contained in:
Shotaro Nakamura
2026-06-21 21:37:07 +09:00
parent 614ea8a992
commit fed3ac1b61
104 changed files with 1815 additions and 218 deletions
+5
View File
@@ -0,0 +1,5 @@
# client-sbi
SBI provider client for Mnie.
The public finance method contract is exported from `@repo/mnie-types`; this package contains SBI-specific login, parsing, and transport code.
@@ -1,5 +1,5 @@
{
"name": "@repo/sbi-client",
"name": "@repo/client-sbi",
"private": true,
"type": "module",
"module": "index.ts",
@@ -15,6 +15,7 @@
"typecheck": "tsc"
},
"dependencies": {
"@repo/mnie-types": "workspace:*",
"iconv-lite": "^0.7.2"
},
"devDependencies": {
@@ -6,6 +6,7 @@ export {
} from './methods/error-map'
export type { SbiServerErrorOptions } from './methods/error-map'
export { loginWithPasskey } from './session'
export type * from '@repo/mnie-types'
export type {
LoginWithPasskeyOptions,
PasskeyLoginResponse,
+1
View File
@@ -0,0 +1 @@
export type * from '@repo/mnie-types'
@@ -207,7 +207,7 @@ const resolveForeignStockEndpointConfig = (
restUrl: optionalUrl(restUrl) ?? requiredDerivedUrl(baseUrl, '/rest/'),
graphqlBffUrl: optionalUrl(graphqlBffUrl) ?? requiredDerivedUrl(baseUrl, '/graphql/bff'),
graphqlIntUrl: optionalUrl(graphqlIntUrl) ?? requiredDerivedUrl(baseUrl, '/graphql/int'),
userAgent: userAgent || 'SBIFStockAndroid/1.6.10(csbi/0)',
userAgent: userAgent || 'SBIFStockAndroid/1.6.10(mnie/0)',
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@repo/mnie-cli",
"private": true,
"bin": {
"mnie": "./src/index.ts"
},
"type": "module",
"scripts": {
"clean": "rm -rf dist",
"typecheck": "tsc"
},
"dependencies": {
"@napi-rs/keyring": "^1.2.0",
"@repo/mnie-sdk": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env bun
import { createHash, randomBytes } from 'node:crypto'
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { createMnieClient } from '@repo/mnie-sdk'
type Profile = {
origin: string
apiKeyStorage: 'file' | 'keyring'
apiKey?: string
keyringAccount?: string
}
type ProfilesFile = {
defaultProfile?: string
profiles: Record<string, Profile>
}
const SERVICE = 'mnie-cli'
const configDir = join(homedir(), '.mnie-cli')
const configPath = join(configDir, 'profiles.json')
const help = `mnie cli
Usage:
mnie --help
mnie profile add <name> --origin <origin> --api-key <key> [--storage file|keyring]
mnie profile list
mnie profile use <name>
mnie rpc methods [--profile <name>]
mnie rpc call <method> [json-params] [--profile <name>] [--passkey-id <id>]
mnie login --origin <origin> [--profile <name>] [--scopes <scopes>] [--storage file|keyring]
Examples:
mnie profile add local --origin http://127.0.0.1:8787 --api-key mnie_xxx
mnie rpc call account.profile --passkey-id sbi_xxx
`
const parseOptions = (args: string[]) => {
const positionals: string[] = []
const options: Record<string, string | true> = {}
for (let index = 0; index < args.length; index++) {
const arg = args[index]!
if (!arg.startsWith('--')) {
positionals.push(arg)
continue
}
const key = arg.slice(2)
const next = args[index + 1]
if (!next || next.startsWith('--')) {
options[key] = true
continue
}
options[key] = next
index++
}
return { positionals, options }
}
const option = (options: Record<string, string | true>, key: string) => {
const value = options[key]
return typeof value === 'string' ? value : undefined
}
const isString = (value: string | undefined): value is string => typeof value === 'string'
const loadProfiles = async (): Promise<ProfilesFile> => {
try {
return JSON.parse(await readFile(configPath, 'utf8')) as ProfilesFile
} catch (cause) {
if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 'ENOENT') {
return { profiles: {} }
}
throw cause
}
}
const saveProfiles = async (profiles: ProfilesFile) => {
await mkdir(dirname(configPath), { recursive: true })
await writeFile(configPath, `${JSON.stringify(profiles, null, 2)}\n`, { mode: 0o600 })
await chmod(configPath, 0o600)
}
const keyring = async () => import('@napi-rs/keyring/keytar')
const saveApiKey = async (
name: string,
origin: string,
apiKey: string,
storage: 'file' | 'keyring',
) => {
if (storage === 'file') return { apiKey, apiKeyStorage: storage } as const
const account = `profile:${name}`
const { setPassword } = await keyring()
await setPassword(SERVICE, account, JSON.stringify({ origin, apiKey }))
return { keyringAccount: account, apiKeyStorage: storage } as const
}
const readApiKey = async (name: string, profile: Profile) => {
if (profile.apiKeyStorage === 'file') {
if (!profile.apiKey) throw new Error(`profile ${name} has no file api key`)
return profile.apiKey
}
if (!profile.keyringAccount) throw new Error(`profile ${name} has no keyring account`)
const { getPassword } = await keyring()
const payload = await getPassword(SERVICE, profile.keyringAccount)
if (!payload) throw new Error(`profile ${name} api key was not found in keyring`)
return (JSON.parse(payload) as { apiKey: string }).apiKey
}
const requireProfile = async (name?: string) => {
const profiles = await loadProfiles()
const selected = name ?? profiles.defaultProfile
if (!selected) throw new Error('profile is required')
const profile = profiles.profiles[selected]
if (!profile) throw new Error(`profile not found: ${selected}`)
return { name: selected, profile, apiKey: await readApiKey(selected, profile) }
}
const printJson = (value: unknown) => {
console.log(JSON.stringify(value, null, 2))
}
const addProfile = async (args: string[]) => {
const { positionals, options } = parseOptions(args)
const name = positionals[0]
const origin = option(options, 'origin')
const apiKey = option(options, 'api-key')
const storage = option(options, 'storage') ?? 'file'
if (!name || !origin || !apiKey)
throw new Error('profile add requires name, --origin and --api-key')
if (storage !== 'file' && storage !== 'keyring')
throw new Error('--storage must be file or keyring')
const profiles = await loadProfiles()
profiles.profiles[name] = {
origin: new URL(origin).origin,
...(await saveApiKey(name, new URL(origin).origin, apiKey, storage)),
}
profiles.defaultProfile ??= name
await saveProfiles(profiles)
printJson({ ok: true, profile: name })
}
const login = async (args: string[]) => {
const { options } = parseOptions(args)
const origin = option(options, 'origin')
if (!origin) throw new Error('login requires --origin')
const profileName = option(options, 'profile') ?? 'default'
const scopes = option(options, 'scopes') ?? 'mcp read write trade'
const storage = option(options, 'storage') ?? 'keyring'
if (storage !== 'file' && storage !== 'keyring')
throw new Error('--storage must be file or keyring')
const callback = await listenForOAuthCallback()
const redirectUri = `http://127.0.0.1:${callback.port}/callback`
const verifier = randomBytes(32).toString('base64url')
const challenge = createHash('sha256').update(verifier).digest('base64url')
const client = await registerOAuthClient(new URL(origin).origin, redirectUri)
const authorize = new URL('/authorize', origin)
authorize.searchParams.set('response_type', 'code')
authorize.searchParams.set('client_id', client.client_id)
authorize.searchParams.set('redirect_uri', redirectUri)
authorize.searchParams.set('code_challenge', challenge)
authorize.searchParams.set('code_challenge_method', 'S256')
authorize.searchParams.set('scope', scopes)
console.error(`Open this URL to approve Mnie CLI:\n${authorize.toString()}`)
openBrowser(authorize.toString())
const code = await callback.code
const tokens = await exchangeCode(new URL(origin).origin, {
clientId: client.client_id,
redirectUri,
code,
verifier,
})
const profiles = await loadProfiles()
profiles.profiles[profileName] = {
origin: new URL(origin).origin,
...(await saveApiKey(profileName, new URL(origin).origin, tokens.access_token, storage)),
}
profiles.defaultProfile = profileName
await saveProfiles(profiles)
printJson({ ok: true, profile: profileName, scope: tokens.scope })
}
const listenForOAuthCallback = async () => {
let resolveCode!: (code: string) => void
let rejectCode!: (error: Error) => void
const code = new Promise<string>((resolve, reject) => {
resolveCode = resolve
rejectCode = reject
})
const server = Bun.serve({
hostname: '127.0.0.1',
port: 0,
fetch(request) {
const url = new URL(request.url)
const error = url.searchParams.get('error')
const value = url.searchParams.get('code')
queueMicrotask(() => server.stop(true))
if (error) {
rejectCode(new Error(error))
return new Response('Mnie login failed. You can close this tab.', { status: 400 })
}
if (!value) {
rejectCode(new Error('oauth callback did not include a code'))
return new Response('Mnie login failed. You can close this tab.', { status: 400 })
}
resolveCode(value)
return new Response('Mnie login complete. You can close this tab.')
},
})
return { port: server.port, code }
}
const registerOAuthClient = async (origin: string, redirectUri: string) => {
const response = await fetch(new URL('/register', origin), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
client_name: 'Mnie CLI',
redirect_uris: [redirectUri],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
}),
})
if (!response.ok) throw new Error(await response.text())
return response.json() as Promise<{ client_id: string }>
}
const exchangeCode = async (
origin: string,
options: { clientId: string; redirectUri: string; code: string; verifier: string },
) => {
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: options.clientId,
redirect_uri: options.redirectUri,
code: options.code,
code_verifier: options.verifier,
})
const response = await fetch(new URL('/token', origin), {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
})
if (!response.ok) throw new Error(await response.text())
return response.json() as Promise<{ access_token: string; scope?: string }>
}
const openBrowser = (url: string) => {
const command =
process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
Bun.spawn({ cmd: [command, ...args], stdout: 'ignore', stderr: 'ignore' })
}
const rpc = async (args: string[]) => {
const { positionals, options } = parseOptions(args)
const { name, profile, apiKey } = await requireProfile(option(options, 'profile'))
const client = createMnieClient({ origin: profile.origin, apiKey })
try {
if (positionals[0] === 'methods') {
printJson(await client.methods())
return
}
if (positionals[0] !== 'call' || !positionals[1])
throw new Error('rpc requires methods or call')
const passkeyId = option(options, 'passkey-id')
if (passkeyId) await client.connect({ passkeyId })
const params = positionals[2] ? JSON.parse(positionals[2]) : undefined
const method = positionals[1]
const result = await method.split('.').reduce<unknown>((target, key) => {
if (!target || (typeof target !== 'object' && typeof target !== 'function')) return undefined
return (target as Record<string, unknown>)[key]
}, client)
if (typeof result !== 'function') throw new Error(`method is not callable: ${method}`)
printJson(await result(params))
} finally {
client.close()
console.error(`profile: ${name}`)
}
}
const main = async () => {
const [command, subcommand, ...rest] = Bun.argv.slice(2)
if (!command || command === '--help' || command === '-h') {
console.log(help)
return
}
if (command === 'profile' && subcommand === 'add') return addProfile(rest)
if (command === 'profile' && subcommand === 'list') return printJson(await loadProfiles())
if (command === 'profile' && subcommand === 'use') {
const [name] = rest
if (!name) throw new Error('profile use requires a name')
const profiles = await loadProfiles()
if (!profiles.profiles[name]) throw new Error(`profile not found: ${name}`)
profiles.defaultProfile = name
await saveProfiles(profiles)
return printJson({ ok: true, profile: name })
}
if (command === 'rpc') return rpc([subcommand, ...rest].filter(isString))
if (command === 'login') return login([subcommand, ...rest].filter(isString))
throw new Error(`unknown command: ${command}`)
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
})
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"]
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@repo/mnie-sdk",
"private": true,
"type": "module",
"module": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"scripts": {
"clean": "rm -rf dist",
"typecheck": "tsc"
},
"dependencies": {
"@repo/mnie-types": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}
+113
View File
@@ -0,0 +1,113 @@
import type { SbiClientMethods } from '@repo/mnie-types'
export type MnieSdkOptions = {
origin: string
apiKey: string
WebSocket?: typeof WebSocket
}
export type JsonRpcError = {
code: number
message: string
}
export type MnieClient = SbiClientMethods & {
connect(options: { passkeyId: string }): Promise<{ connected: true; passkeyId: string }>
methods(): Promise<string[]>
close(): void
}
type JsonRpcResponse = {
jsonrpc?: '2.0'
id?: string | number | null
result?: unknown
error?: JsonRpcError
}
type PendingCall = {
resolve: (value: unknown) => void
reject: (error: Error) => void
}
const websocketUrl = (origin: string, apiKey: string) => {
const url = new URL('/api/ws', origin)
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
url.searchParams.set('key', apiKey)
return url.toString()
}
class MnieJsonRpcClient {
readonly #socket: WebSocket
readonly #pending = new Map<string, PendingCall>()
#nextId = 1
#opened: Promise<void>
constructor(options: MnieSdkOptions) {
const WebSocketCtor = options.WebSocket ?? WebSocket
this.#socket = new WebSocketCtor(websocketUrl(options.origin, options.apiKey))
this.#opened = new Promise((resolve, reject) => {
this.#socket.addEventListener('open', () => resolve(), { once: true })
this.#socket.addEventListener(
'error',
() => reject(new Error('failed to connect to Mnie RPC')),
{ once: true },
)
})
this.#socket.addEventListener('message', (event) => this.#handleMessage(event))
this.#socket.addEventListener('close', () => this.#rejectPending('Mnie RPC socket closed'))
}
close() {
this.#socket.close()
}
async call(method: string, params?: unknown) {
await this.#opened
const id = String(this.#nextId++)
const response = new Promise((resolve, reject) => {
this.#pending.set(id, { resolve, reject })
})
this.#socket.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }))
return response
}
#handleMessage(event: MessageEvent) {
const message = JSON.parse(String(event.data)) as JsonRpcResponse
if (message.id == null) return
const pending = this.#pending.get(String(message.id))
if (!pending) return
this.#pending.delete(String(message.id))
if (message.error) {
pending.reject(new Error(message.error.message))
return
}
pending.resolve(message.result)
}
#rejectPending(message: string) {
for (const pending of this.#pending.values()) pending.reject(new Error(message))
this.#pending.clear()
}
}
const methodProxy = (rpc: MnieJsonRpcClient, path: string[] = []): unknown =>
new Proxy(() => undefined, {
get(_target, property) {
if (property === 'then') return undefined
if (property === 'connect') {
return (params: { passkeyId: string }) => rpc.call('sbi.connect', params)
}
if (property === 'methods') return () => rpc.call('rpc.methods')
if (property === 'close') return () => rpc.close()
if (typeof property !== 'string') return undefined
return methodProxy(rpc, [...path, property])
},
apply(_target, _thisArg, args) {
return rpc.call(path.join('.'), args.length > 1 ? args : args[0])
},
})
export const createMnieClient = (options: MnieSdkOptions) =>
methodProxy(new MnieJsonRpcClient(options)) as MnieClient
export type * from '@repo/mnie-types'
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"]
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@repo/mnie-types",
"private": true,
"type": "module",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"scripts": {
"clean": "rm -rf dist",
"typecheck": "tsc"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}
+2
View File
@@ -0,0 +1,2 @@
export type * from './types'
export type * from './methods'
@@ -40,7 +40,7 @@ import type {
TradeRecordList,
TradeSide,
Watchlist,
} from '../types'
} from './types'
export type PagingOptions = {
/** Start index for the result list. Defaults to the first item when omitted. */
+951
View File
@@ -0,0 +1,951 @@
export type WebAuthnAlgorithm = -7 | -257
export type WebAuthnUserVerification = 'required' | 'preferred' | 'discouraged'
export type WebAuthnTransport = 'ble' | 'hybrid' | 'internal' | 'nfc' | 'usb'
export type WebAuthnJwk = {
kty: string
crv?: string
x?: string
y?: string
d?: string
n?: string
e?: string
key_ops?: string[]
ext?: boolean
[key: string]: unknown
}
export type StoredWebAuthnCredentialSecret = {
privateKey: {
format: 'jwk'
jwk: WebAuthnJwk
}
cosePrivateKey?: string
registration?: {
attestationObject?: string
clientDataJSON?: string
}
}
export type StoredWebAuthnCredential = {
version: 1
kind: 'webauthn-credential'
provider: 'sbi-sec'
rpId: string
origin: string
credentialId: string
userHandle?: string
alg: WebAuthnAlgorithm
publicKey: {
format: 'jwk'
jwk: WebAuthnJwk
}
authenticator: {
aaguid?: string
signCount: number
discoverable: boolean
userVerification: WebAuthnUserVerification
transports?: WebAuthnTransport[]
backupEligible?: boolean
backupState?: boolean
}
secret: {
encrypted: true
format: 'jwe-like-v1'
kdf: {
name: 'argon2id' | 'scrypt'
salt: string
params: Record<string, unknown>
}
cipher: {
name: 'AES-256-GCM'
nonce: string
aad: string
ciphertext: string
tag: string
}
}
createdAt: string
updatedAt: string
}
export type PlaintextStoredWebAuthnCredential = Omit<StoredWebAuthnCredential, 'secret'> & {
label?: string
secretPlaintext: StoredWebAuthnCredentialSecret
}
export type PasskeyLoginResponse = {
type: 'passkey-login-response'
requestUrl: string
status: number
body: ArrayBuffer
text: string
accessToken?: string
header: {
sessionId: string
trCode: string
resultCode: string
} | null
}
export type ForeignStockEndpointConfig = {
baseUrl?: string
restUrl: string
graphqlBffUrl: string
graphqlIntUrl: string
userAgent?: string
}
export type ForeignStockSession = {
endpoints: ForeignStockEndpointConfig
ssoToken?: string
sessionId?: string
accountId?: string
marketPriceHash?: string
candleHash?: string
loginAuthenticated?: boolean
}
export type MainSiteAuthCache = {
baseUrl: string
assetsUrl: string
cookieHeader: string
authenticatedAt: string
}
export type MainSiteSession = {
baseUrl?: string
etGatePath?: string
assetsValuationsPath?: string
exchangeOrderInputPath?: string
exchangeOrderPasswordPath?: string
exchangeOrderConfirmPath?: string
exchangeOrderCompletePath?: string
auth?: MainSiteAuthCache
authPromise?: Promise<MainSiteAuthCache>
}
export type SbiSession = {
mtsBaseUrl: string
izanagiBaseUrl?: string
foreignStock?: ForeignStockSession
mainSite?: MainSiteSession
profile: AccountProfile
loginResponse: PasskeyLoginResponse
tradePassword?: string
deviceIdRegistered?: boolean
tradeAuthentication?: SbiTradeAuthenticationOptions
}
export type LoginWithPasskeyOptions = {
passkeyCredential: PlaintextStoredWebAuthnCredential
authBaseUrl?: string
mtsBaseUrl?: string
izanagiBaseUrl?: string
foreignStockBaseUrl?: string
usStockBaseUrl?: string
foreignStockRestUrl?: string
foreignStockGraphqlBffUrl?: string
foreignStockGraphqlIntUrl?: string
foreignStockUserAgent?: string
mainSiteBaseUrl?: string
mainSiteEtGatePath?: string
mainSiteAssetsValuationsPath?: string
mainSiteExchangeOrderInputPath?: string
mainSiteExchangeOrderPasswordPath?: string
mainSiteExchangeOrderConfirmPath?: string
mainSiteExchangeOrderCompletePath?: string
}
export type SbiClientOptions = {
tradePassword?: string
deviceId?: string
tradeAuthentication?: SbiTradeAuthenticationOptions
}
export type SbiTradeAuthenticationRequest = {
type: 'phone'
telNo?: string
phoneNo?: string
sbiCallNo?: string
authLimitTime?: string
}
export type SbiTradeAuthenticationOptions = {
onRequired?: (request: SbiTradeAuthenticationRequest) => void | Promise<void>
confirmAttempts?: number
confirmIntervalMs?: number
}
export type IssueCode = string
export type DomesticMarketCode = 'XTKS' | 'XNGO' | 'XFKA' | 'XSAP'
export type SKabuMarketCode = 'STK'
export type UsStockMarketCode = 'XNAS' | 'XNYS' | 'ARCX'
export type MarketCode = DomesticMarketCode | SKabuMarketCode | UsStockMarketCode
export type OrderId = string
export type WatchlistId = string
export type PositionId = string
export type ThemeId = string
export type CurrencyAmount = {
value: number | null
text: string
currency: 'JPY' | 'USD'
}
export type PercentValue = {
value: number | null
text: string
}
export type SignedTextValue = {
value: number | null
text: string
sign?: 'positive' | 'negative' | 'zero'
}
export type AccountAssetsValuationSummary = {
assetsErrorType: unknown
valuation: number | null
netChange: number | null
percentChange: number | null
monthOnMonth: number | null
monthOnMonthRatio: unknown
profitLoss: number | null
profitLossRate: number | null
acquisitionCost: number | null
}
export type AccountAssetsValuationDetail = AccountAssetsValuationSummary & {
category: string
compositionRatio: number | null
}
export type AccountAssetsValuations = {
fetchedAt: string
summary: AccountAssetsValuationSummary
summaryWithoutDeposit: AccountAssetsValuationSummary
summaryWithoutIdeco?: AccountAssetsValuationSummary
summaryWithoutDepositAndIdeco?: AccountAssetsValuationSummary
summaryDetails: AccountAssetsValuationDetail[]
summaryDetailsWithoutDeposit: AccountAssetsValuationDetail[]
summaryDetailsWithoutIdeco: AccountAssetsValuationDetail[]
summaryDetailsWithoutDepositAndIdeco: AccountAssetsValuationDetail[]
}
export type AccountType =
| 'general'
| 'specific'
| 'growthInvestment'
| 'nisa'
| 'juniorNisa'
| 'unknown'
export type DepositType =
| 'general'
| 'specific'
| 'growthInvestment'
| 'nisa'
| 'juniorNisa'
| 'unknown'
export type TradeSide = 'buy' | 'sell'
export type MarginTradeSide = 'buy' | 'sell'
export type OrderStatus = 'open' | 'executed' | 'cancelled' | 'expired' | 'rejected' | 'unknown'
export type OrderKind = 'market' | 'limit' | 'stop' | 'oco' | 'ifd' | 'ifdo' | 's' | 'unknown'
export type LoginStatus =
| 'success'
| 'invalidUser'
| 'tradeForbidden'
| 'locked'
| 'fidoAuthorizationIncorrect'
| 'fidoAuthorization'
| 'passwordChangeRequired'
| 'unknown'
export type LoginType = 'passkey' | 'password' | 'unknown'
export type SpecificAccountType =
| 'withHolding'
| 'withoutHolding'
| 'nonSpecific'
| 'notApply'
| 'unknown'
export type IsaAccountType =
| 'nisaTradeForbidden'
| 'oldNisaTradePermitted'
| 'newNisaTradePermitted'
| 'nisaTradePermitted'
| 'unknown'
export type IssueRef = {
code: IssueCode
market?: MarketCode
name?: string
}
export type IssueSearchItem = IssueRef & {
extract?: string
extractWord?: string
boldFrom?: string
boldTo?: string
hitString?: string
}
export type IssueSearchStatus = 'success' | 'searchError' | 'tooManyResults' | 'unknown'
export type IssueSearchResult = {
status?: string
statusText: IssueSearchStatus
issues: IssueSearchItem[]
}
export type ChartPeriod = 'minute' | 'day' | 'week' | 'month'
export type ChartPrice = {
dateTime: string
open: CurrencyAmount
high: CurrencyAmount
low: CurrencyAmount
close: CurrencyAmount
volume?: number | null
}
export type IssueChart = {
issue: IssueRef
period: ChartPeriod
unit: number
prices: ChartPrice[]
previousClose?: CurrencyAmount
currentPrice?: CurrencyAmount
highPrice?: CurrencyAmount
lowPrice?: CurrencyAmount
latestDateTime?: string
error?: SbiMethodError
}
export type SessionInfo = {
sessionId: string
loginType: LoginType
resultCode: string
}
export type AccountProfile = {
session: SessionInfo
branchCode?: string
butenCode?: string
accountNumber?: string
userId?: string
loginStatus?: LoginStatus
loginType?: LoginType
accountType?: AccountType
specificAccountType?: SpecificAccountType
hasMarginAccount?: boolean
marginAccount?: string
corporateFlag?: string
commissionPlan?: string
expireDate?: string
lastLoginDate?: string
lastLoginTime?: string
tradingPassword?: string
fxShareCol?: string
fullTerm?: string
fullAccount?: string
securityAuthenticationResponseCode?: string
fidoResponseCode?: string
passkeyStatus?: string
trId?: string
txId?: string
actionToken?: string
nisa?: {
enabled: boolean
tradePermitted?: boolean
juniorEnabled?: boolean
accountType?: IsaAccountType
jrNisaAccount?: string
jrNisaSpecific?: SpecificAccountType
jrNisaSeigen?: string
}
sor?: {
defaultEnabled?: boolean
defaultCode?: string
lastMarket?: MarketCode
juniorNisaLastMarket?: MarketCode
}
notices?: {
hasImportantNotice?: boolean
importantNoticeFlag?: string
count?: number
}
restrictions?: {
tradeRestricted?: boolean
restrictedTradeFlag?: string
message?: string
}
deficit?: {
hasMessage?: boolean
messageFlag?: string
message?: string
}
maintenance?: {
referenceable?: boolean
referenceableMaintenanceFlag?: string
}
}
export type BuyingPower = {
cashBuyingPower?: CurrencyAmount
marginBuyingPower?: CurrencyAmount
withdrawableAmount?: CurrencyAmount
collateralValue?: CurrencyAmount
collateralRatio?: PercentValue
sbiHybridDepositBalance?: CurrencyAmount
noticeMessage?: string
records?: CollateralRatioRecord[]
error?: SbiMethodError
}
export type CollateralRatioRecord = {
marginRequirements?: CurrencyAmount
referenceMarginRequirements?: CurrencyAmount
collateralRatioCash?: CurrencyAmount
substituteSecuritiesValuationAmount?: CurrencyAmount
unsettledPositionLoss?: SignedTextValue
unsettledPositionLossFlag?: string
settlementLoss?: SignedTextValue
settlementLossFlag?: string
paymentExpenses?: SignedTextValue
paymentExpensesFlag?: string
actualCollateral?: CurrencyAmount
positionAmount?: CurrencyAmount
sbiHybridDepositBalance?: CurrencyAmount
minimumCollateral?: CurrencyAmount
}
export type CashPosition = {
issue: IssueRef
accountType?: AccountType
depositType?: DepositType
depositTypeCode?: string
depositTypeText?: string
quantity: number | null
availableQuantity?: number | null
unexecutedOrderQuantity?: number | null
averagePrice?: CurrencyAmount
purchasePrice?: CurrencyAmount
/** Current unit price, not multiplied by quantity. */
currentPrice?: CurrencyAmount
priceText?: string
/** Total position valuation amount. For cash positions this is quantity-adjusted. */
marketValue?: CurrencyAmount
presentValueFlag?: string
/** Raw SBI valuation amount. Kept for source compatibility; prefer `marketValue` for totals. */
valuationPrice?: CurrencyAmount
valuationPriceChange?: SignedTextValue
valuationPriceChangeRate?: PercentValue
valuationPriceChangeFlag?: string
profitLoss?: SignedTextValue
profitLossRate?: PercentValue
profitLossFlag?: string
holdingCategory?: string
accountInformation?: string
}
export type CashPositionList = {
positions: CashPosition[]
index?: number
totalCount?: number
totalMarketValue?: CurrencyAmount
totalProfitLoss?: SignedTextValue
totalProfitLossRate?: PercentValue
totalProfitLossFlag?: string
hasMore?: boolean
error?: SbiMethodError
}
export type MarginPosition = {
id?: PositionId
issue: IssueRef
side: MarginTradeSide
sideText?: string
accountType?: AccountType
tradeKind?: string
quantity: number | null
availableCloseQuantity?: number | null
unexecutedOrderQuantity?: number | null
openPrice?: CurrencyAmount
openAmount?: CurrencyAmount
/** Current unit price or rate, not multiplied by quantity. */
currentPrice?: CurrencyAmount
rate?: CurrencyAmount
/** Total position valuation amount when provided by SBI. */
marketValue?: CurrencyAmount
presentValueFlag?: string
/** Raw SBI valuation amount. Prefer `marketValue` when calculating totals. */
valuationPrice?: CurrencyAmount
valuationPriceChange?: SignedTextValue
valuationPriceChangeRate?: PercentValue
valuationPriceChangeFlag?: string
profitLoss?: SignedTextValue
profitLossRate?: PercentValue
profitLossFlag?: string
openDate?: string
dueDate?: string
dueDateCode?: string
dueDateText?: string
depositTypeText?: string
cost?: CurrencyAmount
commission?: CurrencyAmount
managementFee?: CurrencyAmount
nameTransferFee?: CurrencyAmount
interest?: CurrencyAmount
backwardation?: CurrencyAmount
collateralRatio?: PercentValue
bargainMarketCode?: string
bargainMarket?: string
}
export type MarginPositionList = {
positions: MarginPosition[]
index?: number
totalCount?: number
totalMarketValue?: CurrencyAmount
totalProfitLoss?: SignedTextValue
totalProfitLossRate?: PercentValue
totalProfitLossFlag?: string
hasMore?: boolean
error?: SbiMethodError
}
export type ProfitLossSummary = {
cash?: SignedTextValue
margin?: SignedTextValue
total?: SignedTextValue
totalRate?: PercentValue
error?: SbiMethodError
}
export type Quote = {
issue: IssueRef
price?: CurrencyAmount
change?: SignedTextValue
changeRate?: PercentValue
changeFlag?: string
open?: CurrencyAmount
high?: CurrencyAmount
low?: CurrencyAmount
previousClose?: CurrencyAmount
volume?: number | null
timestamp?: string
nominalPrices?: CurrencyAmount[]
error?: SbiMethodError
}
export type BoardPriceLevel = {
price: CurrencyAmount
quantity?: number | null
}
export type Board = {
issue: IssueRef
bids: BoardPriceLevel[]
asks: BoardPriceLevel[]
quote?: Quote
error?: SbiMethodError
}
export type MarketIndex = {
code?: string
categoryCode?: string
name: string
value?: number | null
valueText?: string
change?: SignedTextValue
changeRate?: PercentValue
colorFlag?: string
timestamp?: string
open?: CurrencyAmount
high?: CurrencyAmount
low?: CurrencyAmount
previousClose?: CurrencyAmount
}
export type DomesticMarket = {
status?: string
indexes: MarketIndex[]
error?: SbiMethodError
}
export type RankingItem = {
rank: number
issue: IssueRef
value?: number | string | null
values?: Array<number | string | null>
change?: SignedTextValue
changeRate?: PercentValue
exchangeName?: string
colorFlag?: string
}
export type Ranking = {
items: RankingItem[]
category?: string
updatedAt?: string
error?: SbiMethodError
}
export type NewsItem = {
id?: string
title: string
source?: string
publishedAt?: string
url?: string
summary?: string
storyDate?: string
storyTime?: string
processedDate?: string
takeTime?: string
pnac?: string
}
export type NewsList = {
items: NewsItem[]
error?: SbiMethodError
}
export type WatchlistItem = {
issue: IssueRef
sortOrder?: number
memo?: string
quote?: Quote
}
export type Watchlist = {
id: WatchlistId
name: string
items: WatchlistItem[]
error?: SbiMethodError
}
export type Order = {
id: OrderId
issue: IssueRef
side: TradeSide
sideText?: string
status: OrderStatus
statusText?: string
executionStatus?: string
executionStatusText?: string
kind?: OrderKind
accountType?: AccountType
depositType?: DepositType
depositTypeCode?: string
depositTypeText?: string
quantity?: number | null
unexecutedQuantity?: number | null
executedQuantity?: number | null
price?: CurrencyAmount
executedPrice?: CurrencyAmount
orderedAt?: string
expiresAt?: string
orderNumber?: string
orderSubNo?: string
tradeId?: string
exchangeCode?: string
accountInformation?: string
cancelable?: boolean
correctable?: boolean
}
export type OrderList = {
orders: Order[]
hasMore?: boolean
error?: SbiMethodError
}
export type TradeRecord = {
id: string
issue: IssueRef
tradeRecordTypeCode?: string
tradeCurrencyCode?: string
listedSecuritiesStatus?: string
orderPriceKindCode?: string
accountType?: AccountType
settlementCurrencyCode?: string
amount?: CurrencyAmount
quantity?: number | null
price?: CurrencyAmount
tradeDate?: string
valueDate?: string
marginCloseLimitType?: string
}
export type TradeRecordList = {
records: TradeRecord[]
hasMore?: boolean
error?: SbiMethodError
}
export type OrderCorrectionPreOrderDetail = {
exchangeName?: string
marketLoanKbn?: string
marketIppanLoanKbn?: string
currentPrice?: CurrencyAmount
tradeColorFlag?: string
priceTick?: string
priceTickText?: string
tradeTime?: string
changeText?: string
volumeText?: string
}
export type OrderCorrectionPreOrder = {
issue: IssueRef
tradeTitle?: string
buyingPowerTotal?: CurrencyAmount
controlledStockCode?: string
hasTradeWarning?: boolean
deficitMessageFlag?: string
deficitMessage?: string
details: OrderCorrectionPreOrderDetail[]
orderNumber?: string
orderId?: string
primaryOrderMethod?: string
primaryTriggerZone?: string
primaryTriggerPrice?: number | null
status?: string
statusText?: string
tradeId?: string
tradeName?: string
quantity?: number | null
quantityText?: string
orderLimit?: string
orderLimitText?: string
priceSteps: StockOrderPreOrderPriceStep[]
sessionRange?: string
inputDateText?: string
primaryOrderTerm?: string
nonSpecificTradeText?: string
marketName?: string
rbeOrderStatus?: string
priceCondition?: string
price?: number | null
priceAmount?: CurrencyAmount
exchangeName?: string
transId?: string
ptsDayNightFlag?: string
smallTickFlag?: string
juniorBuyingPowerTotal?: CurrencyAmount
secondaryPriceCondition?: string
secondaryPrice?: number | null
secondaryPriceAmount?: CurrencyAmount
autoOrderKind?: string
autoOrderNumber?: string
autoOrderInputDate?: string
secondaryOrderMethod?: string
secondaryTriggerZone?: string
secondaryTriggerPrice?: number | null
secondaryOrderCondition?: string
secondaryLimitPrice?: number | null
secondaryLimitPriceAmount?: CurrencyAmount
secondaryOrderTerm?: string
secondaryOcoPriceCondition?: string
secondaryOcoPrice?: number | null
secondaryOcoPriceAmount?: CurrencyAmount
exchangeList?: string
}
export type OrderPreview = {
issue: IssueRef
side: TradeSide
quantity?: number
price?: CurrencyAmount
estimatedAmount?: CurrencyAmount
commission?: CurrencyAmount
tax?: CurrencyAmount
warnings: string[]
confirmationId?: string
message?: string
correction?: OrderCorrectionPreOrder
error?: SbiMethodError
}
export type StockOrderPreOrderPriceStep = {
from?: CurrencyAmount
to?: CurrencyAmount
}
export type StockOrderPreOrderPaymentLimit = {
text?: string
code?: string
}
export type StockOrderPreOrderMarginTradeType =
| 'standard'
| 'generalBuy'
| 'generalSellShort'
| 'generalSellInventoryLimited'
| 'generalSellInventoryUnlimited'
| 'day'
| 'hyper'
export type StockOrderPreOrder = {
issue: IssueRef
tradeTitle?: string
buyingPowerTotal?: CurrencyAmount
controlledStockCode?: string
hasTradeWarning?: boolean
market?: MarketCode
exchangeList?: string
exchangeListName?: string
exchangeListIndexFlag?: string
marketLoanKbn?: string
marketIppanLoanKbn?: string
currentPrice?: CurrencyAmount
tradeColorFlag?: string
priceTick?: string
priceTickText?: string
tradeTime?: string
changeText?: string
volume?: number | null
lotSize?: number | null
priceSteps: StockOrderPreOrderPriceStep[]
sessionRange?: string
basePrice?: CurrencyAmount
orderTerms: string[]
orderTermDates: string[]
paymentLimits: StockOrderPreOrderPaymentLimit[]
nonSpecificTradeText?: string
paymentLimitText?: string
acquisitionPrice?: CurrencyAmount
position?: number | null
unexecutedQuantity?: number | null
lotSize2?: number | null
ptsDayNightFlag?: string
sorServiceType?: string
nisa?: {
serviceKbn?: string
buyLimit?: CurrencyAmount
growthServiceKbn?: string
juniorServiceKbn?: string
juniorBuyLimit?: CurrencyAmount
juniorBuyingPowerTotal?: CurrencyAmount
}
smallTickFlag?: string
margin?: {
tradeTypes?: StockOrderPreOrderMarginTradeType[]
ippanShort?: string
ippanLong?: string
dayBuy?: string
daySell?: string
premiumShortSelling?: string
premiumFee?: CurrencyAmount
ippanPaymentLimit?: string
positionStatus?: string
}
sKabu?: {
code?: string
available?: boolean
}
deficitMessageFlag?: string
deficitMessage?: string
error?: SbiMethodError
}
export type OrderReceipt = {
accepted: boolean
orderId?: OrderId
acceptedAt?: string
message?: string
error?: SbiMethodError
}
export type ExchangeOrderSide = 'buy' | 'sell'
export type ExchangeSpecificMethod = 'foreign' | 'domestic'
export type ExchangeAccountKind = 'GENERAL' | 'JR_NISA'
export type ExchangeSellMethod = 'SELL_PART' | 'SELL_ALL'
export type ExchangeOrderPreview = {
currencyCode: string
currencyName?: string
side: ExchangeOrderSide
exchangeType?: string
accountKind?: ExchangeAccountKind
specificMethod?: ExchangeSpecificMethod
sellMethod?: ExchangeSellMethod | null
tradeQuantity?: string
orderAmount?: string
exchangeRate?: string
netAmount?: string
valueDate?: string
rateDateTime?: string
warningMessage?: string | null
isMaintenance?: boolean
csrfToken: string
}
export type ExchangeOrderReceipt = {
accepted: boolean
currencyCode?: string
side?: ExchangeOrderSide
message?: string
warningMessage?: string | null
rawTitle?: string
}
export type ExchangeRateInfo = {
currencyCode: string
side: ExchangeOrderSide
referenceExchangeRate?: string
computeExchangeRate?: string
basePrice?: string
exchangeTradeType?: string
updateTime?: string
buyPossibleAmount?: string
sellPossibleAmount?: string
buyUnit?: string
sellUnit?: string
buyLimitMin?: string
buyLimitMax?: string
sellLimitMin?: string
sellLimitMax?: string
raw: Record<string, unknown>
}
export type ThemeInvestment = {
id: ThemeId
name: string
issues: ThemeInvestmentIssue[]
minimumAmount?: CurrencyAmount
}
export type ThemeInvestmentIssue = IssueRef & {
controlledStockCode?: string
hasTradeWarning?: boolean
nisaServiceKbn?: string
juniorNisaServiceKbn?: string
growthNisaServiceKbn?: string
sKabuCode?: string
sKabuAvailable?: boolean
lotSize?: number | null
currentPrice?: CurrencyAmount
tradeColorFlag?: string
priceTick?: string
priceTickText?: string
tradeTime?: string
}
export type ThemeInvestmentList = {
themes: ThemeInvestment[]
buyingPowerTotal?: CurrencyAmount
isaBuyLimit?: CurrencyAmount
juniorNisaBuyLimit?: CurrencyAmount
buyingPowerTotalJuniorNisa?: CurrencyAmount
deficitMessage?: string
deficitMessageFlag?: string
error?: SbiMethodError
}
export type SbiMethodError = {
status?: string
code?: string
message?: string
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"]
}
-15
View File
@@ -1,15 +0,0 @@
# sbi-client
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.