feat: introduce mnie
This commit is contained in:
+13
-13
@@ -2,21 +2,21 @@
|
|||||||
|
|
||||||
# Local development
|
# Local development
|
||||||
PORT=8787
|
PORT=8787
|
||||||
CSBIE_SERVER_PORT=8787
|
MNIE_SERVER_PORT=8787
|
||||||
CSBIE_UI_DEV_PORT=5173
|
MNIE_UI_DEV_PORT=5173
|
||||||
CSBIE_DATABASE_PATH=./data/csbie.sqlite
|
MNIE_DATABASE_PATH=./data/mnie-app.sqlite
|
||||||
CSBIE_KEYRING_BACKEND=platform
|
MNIE_KEYRING_BACKEND=platform
|
||||||
CSBIE_KEYRING_SQLITE_PATH=./data/csbie.keyring.sqlite
|
MNIE_KEYRING_SQLITE_PATH=./data/mnie-app.keyring.sqlite
|
||||||
CSBIE_KEYRING_SECRET=
|
MNIE_KEYRING_SECRET=
|
||||||
CSBIE_ORIGIN=http://127.0.0.1:5173
|
MNIE_ORIGIN=http://127.0.0.1:5173
|
||||||
CSBIE_CORS_ORIGIN=http://127.0.0.1:5173
|
MNIE_CORS_ORIGIN=http://127.0.0.1:5173
|
||||||
CSBIE_SESSION_COOKIE=csbie_session
|
MNIE_SESSION_COOKIE=mnie_session
|
||||||
CSBIE_RP_NAME=CSBIE
|
MNIE_RP_NAME=MNIE
|
||||||
CSBIE_RP_ID=127.0.0.1
|
MNIE_RP_ID=127.0.0.1
|
||||||
|
|
||||||
# Initial owner passkey setup. Prefer the hash form outside throwaway local dev.
|
# Initial owner passkey setup. Prefer the hash form outside throwaway local dev.
|
||||||
CSBIE_SETUP_PASSWORD=
|
MNIE_SETUP_PASSWORD=
|
||||||
CSBIE_SETUP_PASSWORD_HASH=
|
MNIE_SETUP_PASSWORD_HASH=
|
||||||
|
|
||||||
# SBI endpoints and local SBI session options. Values are loaded at runtime only.
|
# SBI endpoints and local SBI session options. Values are loaded at runtime only.
|
||||||
SBI_AUTH_BASE_URL=
|
SBI_AUTH_BASE_URL=
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
# cSBI
|
# Mnie
|
||||||
|
|
||||||
[](https://技術者倫理.com)
|
[](https://技術者倫理.com)
|
||||||
|
|
||||||
A client for SBI-compatible servers. This project is not intended for use with a real SBI server.
|
Self-host finance management for running your own portfolio, trading, API key, OAuth, MCP, and CLI workflows.
|
||||||
|
|
||||||
## cSBI SDK
|
## Mnie SDK
|
||||||
|
|
||||||
## cSBIe
|
`@repo/mnie-sdk` connects to a Mnie server with an API key and exposes typed finance methods from `@repo/mnie-types`.
|
||||||
|
|
||||||
cSBIe is a web ui and server for managing SBI-compatible API.
|
## Apps
|
||||||
|
|
||||||
|
- `apps/mnie-app`: bundled server plus UI runtime
|
||||||
|
- `apps/mnie-server`: API, OAuth, MCP, RPC, storage, and provider session management
|
||||||
|
- `apps/mnie-ui`: web UI
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
- `packages/mnie-types`: shared finance SDK interfaces
|
||||||
|
- `packages/mnie-sdk`: API-key SDK for Mnie
|
||||||
|
- `packages/mnie-cli`: CLI using `mnie-sdk`
|
||||||
|
- `packages/client-sbi`: SBI provider client
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
@@ -16,15 +27,15 @@ Before running, setup .env, refering `.env.example`.
|
|||||||
|
|
||||||
#### With docker
|
#### With docker
|
||||||
|
|
||||||
Docker uses `CSBIE_KEYRING_BACKEND=sqlite` because platform keyrings are not available in
|
Docker uses `MNIE_KEYRING_BACKEND=sqlite` because platform keyrings are not available in
|
||||||
headless containers. Set `CSBIE_KEYRING_SECRET` in `.env` before saving SBI passkeys.
|
headless containers. Set `MNIE_KEYRING_SECRET` in `.env` before saving SBI passkeys.
|
||||||
|
|
||||||
docker-compose.yml:
|
docker-compose.yml:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
csbie:
|
mnie:
|
||||||
image: git.yutakobayashi.com/nakasyou/csbi:latest
|
image: git.yutakobayashi.com/nakasyou/mnie:latest
|
||||||
ports:
|
ports:
|
||||||
- '18787:8787'
|
- '18787:8787'
|
||||||
env_file:
|
env_file:
|
||||||
@@ -36,15 +47,15 @@ services:
|
|||||||
or
|
or
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -it --name csbie \
|
docker run --rm -it --name mnie \
|
||||||
-p 18787:8787 \
|
-p 18787:8787 \
|
||||||
--env-file .env \
|
--env-file .env \
|
||||||
-v "$PWD/data:/app/data" \
|
-v "$PWD/data:/app/data" \
|
||||||
git.yutakobayashi.com/nakasyou/csbi:latest
|
git.yutakobayashi.com/nakasyou/mnie:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### With Nix
|
#### With Nix
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix run 'git+https://git.yutakobayashi.com/nakasyou/csbi.git#csbie'
|
nix run 'git+https://git.yutakobayashi.com/nakasyou/mnie.git#mnie'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
FROM oven/bun:1 AS deps
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json bun.lock tsconfig.json vite.config.ts ./
|
|
||||||
COPY packages/sbi-client/package.json packages/sbi-client/package.json
|
|
||||||
COPY apps/csbie/package.json apps/csbie/package.json
|
|
||||||
COPY apps/csbie-server/package.json apps/csbie-server/package.json
|
|
||||||
COPY apps/csbie-ui/package.json apps/csbie-ui/package.json
|
|
||||||
RUN bun install --frozen-lockfile
|
|
||||||
|
|
||||||
FROM deps AS build
|
|
||||||
COPY . .
|
|
||||||
RUN bun --filter @repo/csbie build
|
|
||||||
|
|
||||||
FROM oven/bun:1-slim AS runtime
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV CSBIE_DATABASE_PATH=/app/data/csbie.sqlite
|
|
||||||
ENV CSBIE_KEYRING_BACKEND=sqlite
|
|
||||||
ENV CSBIE_KEYRING_SQLITE_PATH=/app/data/csbie.keyring.sqlite
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/apps/csbie/dist ./apps/csbie/dist
|
|
||||||
COPY --from=build /app/apps/csbie-ui/dist ./apps/csbie-ui/dist
|
|
||||||
COPY --from=build /app/node_modules/.bun/@napi-rs+keyring@*/node_modules/@napi-rs/keyring ./node_modules/@napi-rs/keyring
|
|
||||||
COPY --from=build /app/node_modules/.bun/@napi-rs+keyring-linux-x64-gnu@*/node_modules/@napi-rs/keyring-linux-x64-gnu ./node_modules/@napi-rs/keyring-linux-x64-gnu
|
|
||||||
WORKDIR /app/apps/csbie/dist
|
|
||||||
EXPOSE 8787
|
|
||||||
CMD ["bun", "index.js"]
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
FROM oven/bun:1 AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json bun.lock tsconfig.json vite.config.ts ./
|
||||||
|
COPY packages/client-sbi/package.json packages/client-sbi/package.json
|
||||||
|
COPY packages/mnie-types/package.json packages/mnie-types/package.json
|
||||||
|
COPY packages/mnie-sdk/package.json packages/mnie-sdk/package.json
|
||||||
|
COPY packages/mnie-cli/package.json packages/mnie-cli/package.json
|
||||||
|
COPY apps/mnie-app/package.json apps/mnie-app/package.json
|
||||||
|
COPY apps/mnie-server/package.json apps/mnie-server/package.json
|
||||||
|
COPY apps/mnie-ui/package.json apps/mnie-ui/package.json
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN bun --filter @repo/mnie-app build
|
||||||
|
|
||||||
|
FROM oven/bun:1-slim AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV MNIE_DATABASE_PATH=/app/data/mnie-app.sqlite
|
||||||
|
ENV MNIE_KEYRING_BACKEND=sqlite
|
||||||
|
ENV MNIE_KEYRING_SQLITE_PATH=/app/data/mnie-app.keyring.sqlite
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/apps/mnie-app/dist ./apps/mnie-app/dist
|
||||||
|
COPY --from=build /app/apps/mnie-ui/dist ./apps/mnie-ui/dist
|
||||||
|
COPY --from=build /app/node_modules/.bun/@napi-rs+keyring@*/node_modules/@napi-rs/keyring ./node_modules/@napi-rs/keyring
|
||||||
|
COPY --from=build /app/node_modules/.bun/@napi-rs+keyring-linux-x64-gnu@*/node_modules/@napi-rs/keyring-linux-x64-gnu ./node_modules/@napi-rs/keyring-linux-x64-gnu
|
||||||
|
WORKDIR /app/apps/mnie-app/dist
|
||||||
|
EXPOSE 8787
|
||||||
|
CMD ["bun", "index.js"]
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/csbie",
|
"name": "@repo/mnie-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist ../csbie-ui/dist",
|
"clean": "rm -rf dist ../mnie-ui/dist",
|
||||||
"dev": "bun --env-file=../../.env src/dev.ts",
|
"dev": "bun --env-file=../../.env src/dev.ts",
|
||||||
"build": "bun --filter @repo/csbie-ui build && bun build src/index.ts --target=bun --outdir=dist --external @napi-rs/keyring",
|
"build": "bun --filter @repo/mnie-ui build && bun build src/index.ts --target=bun --outdir=dist --external @napi-rs/keyring",
|
||||||
"start": "bun --env-file=../../.env dist/index.js",
|
"start": "bun --env-file=../../.env dist/index.js",
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/csbie-server": "workspace:*",
|
"@repo/mnie-server": "workspace:*",
|
||||||
"hono": "^4.8.3"
|
"hono": "^4.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
const uiPort = Number(process.env.CSBIE_UI_DEV_PORT ?? 5173)
|
const uiPort = Number(process.env.MNIE_UI_DEV_PORT ?? 5173)
|
||||||
const serverPort = Number(process.env.PORT ?? process.env.CSBIE_SERVER_PORT ?? 8787)
|
const serverPort = Number(process.env.PORT ?? process.env.MNIE_SERVER_PORT ?? 8787)
|
||||||
const uiOrigin = `http://127.0.0.1:${uiPort}`
|
const uiOrigin = `http://127.0.0.1:${uiPort}`
|
||||||
const serverEntry = new URL('../../csbie-server/src/index.ts', import.meta.url).pathname
|
const serverEntry = new URL('../../mnie-server/src/index.ts', import.meta.url).pathname
|
||||||
|
|
||||||
const ui = Bun.spawn({
|
const ui = Bun.spawn({
|
||||||
cmd: ['bun', '--filter', '@repo/csbie-ui', 'dev'],
|
cmd: ['bun', '--filter', '@repo/mnie-ui', 'dev'],
|
||||||
stdout: 'inherit',
|
stdout: 'inherit',
|
||||||
stderr: 'inherit',
|
stderr: 'inherit',
|
||||||
stdin: 'inherit',
|
stdin: 'inherit',
|
||||||
@@ -19,8 +19,8 @@ const server = Bun.spawn({
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
PORT: String(serverPort),
|
PORT: String(serverPort),
|
||||||
CSBIE_ORIGIN: uiOrigin,
|
MNIE_ORIGIN: uiOrigin,
|
||||||
CSBIE_CORS_ORIGIN: uiOrigin,
|
MNIE_CORS_ORIGIN: uiOrigin,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { extname, isAbsolute, join, relative, resolve } from 'node:path'
|
import { extname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { createServerApp } from '@repo/csbie-server/app'
|
import { createServerApp } from '@repo/mnie-server/app'
|
||||||
import { loadConfig } from '@repo/csbie-server/config'
|
import { loadConfig } from '@repo/mnie-server/config'
|
||||||
import { createDb } from '@repo/csbie-server/db'
|
import { createDb } from '@repo/mnie-server/db'
|
||||||
|
|
||||||
const config = loadConfig()
|
const config = loadConfig()
|
||||||
const db = createDb(config.databasePath)
|
const db = createDb(config.databasePath)
|
||||||
const api = createServerApp(db, config)
|
const api = createServerApp(db, config)
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
const uiDist = resolve(import.meta.dir, '../../csbie-ui/dist')
|
const uiDist = resolve(import.meta.dir, '../../mnie-ui/dist')
|
||||||
|
|
||||||
const contentTypes: Record<string, string> = {
|
const contentTypes: Record<string, string> = {
|
||||||
'.css': 'text/css; charset=utf-8',
|
'.css': 'text/css; charset=utf-8',
|
||||||
@@ -63,4 +63,4 @@ const server = Bun.serve({
|
|||||||
websocket: api.websocket,
|
websocket: api.websocket,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`csbie listening on http://localhost:${server.port}`)
|
console.log(`mnie listening on http://localhost:${server.port}`)
|
||||||
@@ -5,6 +5,6 @@ export default defineConfig({
|
|||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.CSBIE_DATABASE_PATH ?? './data/csbie.sqlite',
|
url: process.env.MNIE_DATABASE_PATH ?? './data/mnie-app.sqlite',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/csbie-server",
|
"name": "@repo/mnie-server",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"@hono/mcp": "^0.3.0",
|
"@hono/mcp": "^0.3.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@napi-rs/keyring": "^1.2.0",
|
"@napi-rs/keyring": "^1.2.0",
|
||||||
"@repo/sbi-client": "workspace:*",
|
"@repo/client-sbi": "workspace:*",
|
||||||
"@simplewebauthn/server": "^13.1.2",
|
"@simplewebauthn/server": "^13.1.2",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
"hono": "^4.8.3",
|
"hono": "^4.8.3",
|
||||||
@@ -48,8 +48,8 @@ export const createServerApp = (db: Db, config: ServerConfig) => {
|
|||||||
issuerUrl: new URL(config.origin),
|
issuerUrl: new URL(config.origin),
|
||||||
baseUrl: new URL(config.origin),
|
baseUrl: new URL(config.origin),
|
||||||
resourceServerUrl: new URL('/api/mcp', config.origin),
|
resourceServerUrl: new URL('/api/mcp', config.origin),
|
||||||
resourceName: 'CSBIE MCP',
|
resourceName: 'Mnie finance management',
|
||||||
scopesSupported: ['mcp'],
|
scopesSupported: ['read', 'write', 'trade', 'mcp'],
|
||||||
provider: oauthProvider,
|
provider: oauthProvider,
|
||||||
authorizationOptions: { rateLimit: false },
|
authorizationOptions: { rateLimit: false },
|
||||||
tokenOptions: { rateLimit: false },
|
tokenOptions: { rateLimit: false },
|
||||||
@@ -32,19 +32,19 @@ const optionalUrl = (value: string | undefined) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const loadConfig = (): ServerConfig => {
|
export const loadConfig = (): ServerConfig => {
|
||||||
const port = Number(process.env.PORT ?? process.env.CSBIE_SERVER_PORT ?? 8787)
|
const port = Number(process.env.PORT ?? process.env.MNIE_SERVER_PORT ?? 8787)
|
||||||
const databasePath = resolve(process.env.CSBIE_DATABASE_PATH ?? './data/csbie.sqlite')
|
const databasePath = resolve(process.env.MNIE_DATABASE_PATH ?? './data/mnie-app.sqlite')
|
||||||
mkdirSync(dirname(databasePath), { recursive: true })
|
mkdirSync(dirname(databasePath), { recursive: true })
|
||||||
|
|
||||||
const origin = process.env.CSBIE_ORIGIN ?? `http://localhost:${port}`
|
const origin = process.env.MNIE_ORIGIN ?? `http://localhost:${port}`
|
||||||
const rpId = process.env.CSBIE_RP_ID ?? new URL(origin).hostname
|
const rpId = process.env.MNIE_RP_ID ?? new URL(origin).hostname
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port,
|
port,
|
||||||
databasePath,
|
databasePath,
|
||||||
corsOrigin: process.env.CSBIE_CORS_ORIGIN ?? origin,
|
corsOrigin: process.env.MNIE_CORS_ORIGIN ?? origin,
|
||||||
sessionCookieName: process.env.CSBIE_SESSION_COOKIE ?? 'csbie_session',
|
sessionCookieName: process.env.MNIE_SESSION_COOKIE ?? 'mnie_session',
|
||||||
rpName: process.env.CSBIE_RP_NAME ?? 'CSBIE',
|
rpName: process.env.MNIE_RP_NAME ?? 'MNIE',
|
||||||
rpId,
|
rpId,
|
||||||
origin,
|
origin,
|
||||||
authBaseUrl: optionalUrl(process.env.SBI_AUTH_BASE_URL),
|
authBaseUrl: optionalUrl(process.env.SBI_AUTH_BASE_URL),
|
||||||
@@ -24,4 +24,5 @@ export type AuthContext =
|
|||||||
type: 'apiKey'
|
type: 'apiKey'
|
||||||
authenticated: true
|
authenticated: true
|
||||||
apiKeyId: string
|
apiKeyId: string
|
||||||
|
scopes: string[]
|
||||||
}
|
}
|
||||||
@@ -50,6 +50,7 @@ export const apiKeys = sqliteTable(
|
|||||||
maxOrderPriceJpy: integer('max_order_price_jpy'),
|
maxOrderPriceJpy: integer('max_order_price_jpy'),
|
||||||
maxOrderAmountJpy: integer('max_order_amount_jpy'),
|
maxOrderAmountJpy: integer('max_order_amount_jpy'),
|
||||||
allowedMethods: text('allowed_methods', { mode: 'json' }).$type<string[] | null>(),
|
allowedMethods: text('allowed_methods', { mode: 'json' }).$type<string[] | null>(),
|
||||||
|
scopes: text('scopes', { mode: 'json' }).$type<string[] | null>(),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
||||||
lastUsedAt: integer('last_used_at', { mode: 'timestamp_ms' }),
|
lastUsedAt: integer('last_used_at', { mode: 'timestamp_ms' }),
|
||||||
revokedAt: integer('revoked_at', { mode: 'timestamp_ms' }),
|
revokedAt: integer('revoked_at', { mode: 'timestamp_ms' }),
|
||||||
@@ -14,4 +14,4 @@ const server = Bun.serve({
|
|||||||
websocket,
|
websocket,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`csbie-server listening on http://localhost:${server.port}`)
|
console.log(`mnie-server listening on http://localhost:${server.port}`)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import type { MiddlewareHandler } from 'hono'
|
import type { MiddlewareHandler } from 'hono'
|
||||||
import type { PlaintextStoredWebAuthnCredential } from '@repo/sbi-client'
|
import type { PlaintextStoredWebAuthnCredential } from '@repo/client-sbi'
|
||||||
import type { AppBindings } from '../context'
|
import type { AppBindings } from '../context'
|
||||||
import { sbiPasskeys } from '../db/schema'
|
import { sbiPasskeys } from '../db/schema'
|
||||||
import {
|
import {
|
||||||
@@ -74,6 +74,21 @@ const passkeyForCredential = async (db: AppBindings['Variables']['db'], credenti
|
|||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loopbackHosts = new Set(['localhost', '127.0.0.1'])
|
||||||
|
|
||||||
|
const expectedWebAuthnOrigins = (origin: string) => {
|
||||||
|
const url = new URL(origin)
|
||||||
|
if (!loopbackHosts.has(url.hostname)) return origin
|
||||||
|
return [...loopbackHosts].map((hostname) => {
|
||||||
|
const candidate = new URL(origin)
|
||||||
|
candidate.hostname = hostname
|
||||||
|
return candidate.origin
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedWebAuthnRpIds = (rpId: string) =>
|
||||||
|
loopbackHosts.has(rpId) ? [...loopbackHosts] : rpId
|
||||||
|
|
||||||
export const createAuthRoutes = () => {
|
export const createAuthRoutes = () => {
|
||||||
const app = new Hono<AppBindings>()
|
const app = new Hono<AppBindings>()
|
||||||
|
|
||||||
@@ -125,8 +140,8 @@ export const createAuthRoutes = () => {
|
|||||||
const verification = await verifyRegistrationResponse({
|
const verification = await verifyRegistrationResponse({
|
||||||
response: response as never,
|
response: response as never,
|
||||||
expectedChallenge,
|
expectedChallenge,
|
||||||
expectedOrigin: config.origin,
|
expectedOrigin: expectedWebAuthnOrigins(config.origin),
|
||||||
expectedRPID: config.rpId,
|
expectedRPID: expectedWebAuthnRpIds(config.rpId),
|
||||||
requireUserVerification: true,
|
requireUserVerification: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -186,8 +201,8 @@ export const createAuthRoutes = () => {
|
|||||||
const verification = await verifyAuthenticationResponse({
|
const verification = await verifyAuthenticationResponse({
|
||||||
response: response as never,
|
response: response as never,
|
||||||
expectedChallenge,
|
expectedChallenge,
|
||||||
expectedOrigin: config.origin,
|
expectedOrigin: expectedWebAuthnOrigins(config.origin),
|
||||||
expectedRPID: config.rpId,
|
expectedRPID: expectedWebAuthnRpIds(config.rpId),
|
||||||
credential: {
|
credential: {
|
||||||
id: passkey.credentialId,
|
id: passkey.credentialId,
|
||||||
publicKey: Buffer.from(passkey.publicKey, 'base64url'),
|
publicKey: Buffer.from(passkey.publicKey, 'base64url'),
|
||||||
@@ -25,6 +25,9 @@ const textResult = (value: unknown) => ({
|
|||||||
|
|
||||||
const requireAuthenticated = (auth: AuthContext) => {
|
const requireAuthenticated = (auth: AuthContext) => {
|
||||||
if (!auth.authenticated) throw new Error('unauthorized')
|
if (!auth.authenticated) throw new Error('unauthorized')
|
||||||
|
if (auth.type === 'apiKey' && !auth.scopes.includes('mcp')) {
|
||||||
|
throw new Error('missing OAuth scope: mcp')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ORDER_SUBMIT_TICKET_TTL_MS = 10 * 60 * 1000
|
const ORDER_SUBMIT_TICKET_TTL_MS = 10 * 60 * 1000
|
||||||
@@ -835,7 +838,7 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
const auth = c.get('auth')
|
const auth = c.get('auth')
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: 'csbie',
|
name: 'mnie',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -869,6 +872,10 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
requireAuthenticated(auth)
|
requireAuthenticated(auth)
|
||||||
|
|
||||||
if (auth.type === 'apiKey') {
|
if (auth.type === 'apiKey') {
|
||||||
|
const requiredScope = isTradingMethod(method) ? 'trade' : 'read'
|
||||||
|
if (!auth.scopes.includes(requiredScope)) {
|
||||||
|
throw new Error(`missing OAuth scope: ${requiredScope}`)
|
||||||
|
}
|
||||||
await assertApiKeyMethodAllowed(db, auth.apiKeyId, method)
|
await assertApiKeyMethodAllowed(db, auth.apiKeyId, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,7 +950,7 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'csbie-get',
|
'mnie-get',
|
||||||
{
|
{
|
||||||
title: 'Get SBI Data',
|
title: 'Get SBI Data',
|
||||||
description: `Read SBI data through a small abstract action API. This tool never places, corrects, cancels, or otherwise changes real orders. ${getActionDescription}`,
|
description: `Read SBI data through a small abstract action API. This tool never places, corrects, cancels, or otherwise changes real orders. ${getActionDescription}`,
|
||||||
@@ -958,7 +965,7 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
return textResult({
|
return textResult({
|
||||||
getActions,
|
getActions,
|
||||||
changeActions,
|
changeActions,
|
||||||
changeTool: 'csbie-request-change',
|
changeTool: 'mnie-request-change',
|
||||||
confirmTool: 'confirm-request',
|
confirmTool: 'confirm-request',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -976,7 +983,7 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'csbie-request-change',
|
'mnie-request-change',
|
||||||
{
|
{
|
||||||
title: 'Create SBI Change Request',
|
title: 'Create SBI Change Request',
|
||||||
description: `Prepare a real SBI change by running the corresponding estimate/preview and returning a UUID. This tool never submits the change; pass the UUID to confirm-request. ${changeActionDescription}`,
|
description: `Prepare a real SBI change by running the corresponding estimate/preview and returning a UUID. This tool never submits the change; pass the UUID to confirm-request. ${changeActionDescription}`,
|
||||||
@@ -999,7 +1006,7 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
description:
|
description:
|
||||||
'Submit a previously prepared SBI change request by UUID. The UUID expires shortly, is single-use, and is bound to the same authenticated caller.',
|
'Submit a previously prepared SBI change request by UUID. The UUID expires shortly, is single-use, and is bound to the same authenticated caller.',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
uuid: z.string().uuid().describe('UUID returned by csbie-request-change'),
|
uuid: z.string().uuid().describe('UUID returned by mnie-request-change'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ uuid }) => {
|
async ({ uuid }) => {
|
||||||
@@ -7,6 +7,7 @@ import type { ApiKeySettings } from '../security/api-keys'
|
|||||||
import { createOAuthAuthorizationCode } from '../security/oauth-provider'
|
import { createOAuthAuthorizationCode } from '../security/oauth-provider'
|
||||||
|
|
||||||
const loopbackHosts = new Set(['localhost', '127.0.0.1', '[::1]'])
|
const loopbackHosts = new Set(['localhost', '127.0.0.1', '[::1]'])
|
||||||
|
const supportedScopes = new Set(['read', 'write', 'trade', 'mcp'])
|
||||||
|
|
||||||
const redirectUriAllowed = (redirectUri: string, registeredUris: string[]) => {
|
const redirectUriAllowed = (redirectUri: string, registeredUris: string[]) => {
|
||||||
if (registeredUris.includes(redirectUri)) return true
|
if (registeredUris.includes(redirectUri)) return true
|
||||||
@@ -62,6 +63,9 @@ export const createOAuthRoutes = () => {
|
|||||||
if (!body.clientId || !body.redirectUri || !body.codeChallenge) {
|
if (!body.clientId || !body.redirectUri || !body.codeChallenge) {
|
||||||
return c.json({ error: 'clientId, redirectUri and codeChallenge are required' }, 400)
|
return c.json({ error: 'clientId, redirectUri and codeChallenge are required' }, 400)
|
||||||
}
|
}
|
||||||
|
const requestedScopes = body.scope?.split(' ').filter(Boolean) ?? []
|
||||||
|
const unsupportedScope = requestedScopes.find((scope) => !supportedScopes.has(scope))
|
||||||
|
if (unsupportedScope) return c.json({ error: `unsupported scope: ${unsupportedScope}` }, 400)
|
||||||
|
|
||||||
const [client] = await c
|
const [client] = await c
|
||||||
.get('db')
|
.get('db')
|
||||||
@@ -78,7 +82,7 @@ export const createOAuthRoutes = () => {
|
|||||||
clientId: body.clientId,
|
clientId: body.clientId,
|
||||||
redirectUri: body.redirectUri,
|
redirectUri: body.redirectUri,
|
||||||
codeChallenge: body.codeChallenge,
|
codeChallenge: body.codeChallenge,
|
||||||
scopes: body.scope?.split(' ').filter(Boolean) ?? [],
|
scopes: requestedScopes.length ? requestedScopes : ['read'],
|
||||||
resource: body.resource,
|
resource: body.resource,
|
||||||
apiKeySettings: body.settings,
|
apiKeySettings: body.settings,
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SbiClientMethods } from '@repo/sbi-client'
|
import type { SbiClientMethods } from '@repo/client-sbi'
|
||||||
|
|
||||||
export const RPC_METHODS = [
|
export const RPC_METHODS = [
|
||||||
'session.profile',
|
'session.profile',
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { loginWithPasskey } from '@repo/sbi-client'
|
import { loginWithPasskey } from '@repo/client-sbi'
|
||||||
import type { SbiClientMethods, SbiClientOptions } from '@repo/sbi-client'
|
import type { SbiClientMethods, SbiClientOptions } from '@repo/client-sbi'
|
||||||
import type { ServerConfig } from '../config'
|
import type { ServerConfig } from '../config'
|
||||||
import type { Db } from '../db'
|
import type { Db } from '../db'
|
||||||
import { sbiPasskeys } from '../db/schema'
|
import { sbiPasskeys } from '../db/schema'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { MarketCode, SbiClientMethods } from '@repo/sbi-client'
|
import type { MarketCode, SbiClientMethods } from '@repo/client-sbi'
|
||||||
import { createBunWebSocket } from 'hono/bun'
|
import { createBunWebSocket } from 'hono/bun'
|
||||||
import type { WSContext } from 'hono/ws'
|
import type { WSContext } from 'hono/ws'
|
||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
@@ -22,6 +22,7 @@ type RpcSocketState = {
|
|||||||
client?: SbiClientMethods
|
client?: SbiClientMethods
|
||||||
sbiPasskeyId?: string
|
sbiPasskeyId?: string
|
||||||
apiKeyId?: string
|
apiKeyId?: string
|
||||||
|
scopes?: string[]
|
||||||
boardPollingSubscriptions: Map<string, AbortController>
|
boardPollingSubscriptions: Map<string, AbortController>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +104,12 @@ const stopBoardPollingSubscriptions = (state: RpcSocketState) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assertScope = (state: RpcSocketState, scope: 'read' | 'trade') => {
|
||||||
|
if (!state.apiKeyId) return
|
||||||
|
const scopes = state.scopes ?? ['read', 'write', 'trade', 'mcp']
|
||||||
|
if (!scopes.includes(scope)) throw new Error(`missing OAuth scope: ${scope}`)
|
||||||
|
}
|
||||||
|
|
||||||
const subscribeBoardPolling = async (
|
const subscribeBoardPolling = async (
|
||||||
db: Db,
|
db: Db,
|
||||||
state: RpcSocketState,
|
state: RpcSocketState,
|
||||||
@@ -110,6 +117,7 @@ const subscribeBoardPolling = async (
|
|||||||
request: JsonRpcRequest,
|
request: JsonRpcRequest,
|
||||||
) => {
|
) => {
|
||||||
if (!state.client) return error(request.id, 4001, 'SBI session is not connected')
|
if (!state.client) return error(request.id, 4001, 'SBI session is not connected')
|
||||||
|
assertScope(state, 'read')
|
||||||
|
|
||||||
if (state.apiKeyId) {
|
if (state.apiKeyId) {
|
||||||
await assertApiKeyMethodAllowed(db, state.apiKeyId, 'market.issue.board')
|
await assertApiKeyMethodAllowed(db, state.apiKeyId, 'market.issue.board')
|
||||||
@@ -159,6 +167,7 @@ const handleRpc = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request.method === 'sbi.connect') {
|
if (request.method === 'sbi.connect') {
|
||||||
|
assertScope(state, 'read')
|
||||||
const passkeyId =
|
const passkeyId =
|
||||||
request.params && typeof request.params === 'object'
|
request.params && typeof request.params === 'object'
|
||||||
? (request.params as { passkeyId?: string }).passkeyId
|
? (request.params as { passkeyId?: string }).passkeyId
|
||||||
@@ -190,6 +199,7 @@ const handleRpc = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!state.client) return error(request.id, 4001, 'SBI session is not connected')
|
if (!state.client) return error(request.id, 4001, 'SBI session is not connected')
|
||||||
|
assertScope(state, isTradingMethod(request.method) ? 'trade' : 'read')
|
||||||
|
|
||||||
if (state.apiKeyId) {
|
if (state.apiKeyId) {
|
||||||
await assertApiKeyMethodAllowed(db, state.apiKeyId, request.method)
|
await assertApiKeyMethodAllowed(db, state.apiKeyId, request.method)
|
||||||
@@ -228,6 +238,7 @@ export const createRpcWebSocket = (db: Db, config: ServerConfig) => {
|
|||||||
const auth = c.get('auth')
|
const auth = c.get('auth')
|
||||||
const state: RpcSocketState = {
|
const state: RpcSocketState = {
|
||||||
apiKeyId: auth.type === 'apiKey' ? auth.apiKeyId : undefined,
|
apiKeyId: auth.type === 'apiKey' ? auth.apiKeyId : undefined,
|
||||||
|
scopes: auth.type === 'apiKey' ? auth.scopes : undefined,
|
||||||
boardPollingSubscriptions: new Map(),
|
boardPollingSubscriptions: new Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
+9
-1
@@ -10,6 +10,7 @@ export type ApiKeySettings = {
|
|||||||
maxOrderPriceJpy?: number | null
|
maxOrderPriceJpy?: number | null
|
||||||
maxOrderAmountJpy?: number | null
|
maxOrderAmountJpy?: number | null
|
||||||
allowedMethods?: string[] | null
|
allowedMethods?: string[] | null
|
||||||
|
scopes?: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeLimit = (value: unknown) => {
|
const normalizeLimit = (value: unknown) => {
|
||||||
@@ -31,6 +32,12 @@ export const normalizeApiKeySettings = (settings: ApiKeySettings = {}) => ({
|
|||||||
: settings.allowedMethods?.length
|
: settings.allowedMethods?.length
|
||||||
? [...new Set(settings.allowedMethods)].sort()
|
? [...new Set(settings.allowedMethods)].sort()
|
||||||
: null,
|
: null,
|
||||||
|
scopes:
|
||||||
|
settings.scopes === undefined
|
||||||
|
? null
|
||||||
|
: settings.scopes?.length
|
||||||
|
? [...new Set(settings.scopes)].sort()
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const listApiKeys = async (db: Db) =>
|
export const listApiKeys = async (db: Db) =>
|
||||||
@@ -44,6 +51,7 @@ export const listApiKeys = async (db: Db) =>
|
|||||||
maxOrderPriceJpy: apiKeys.maxOrderPriceJpy,
|
maxOrderPriceJpy: apiKeys.maxOrderPriceJpy,
|
||||||
maxOrderAmountJpy: apiKeys.maxOrderAmountJpy,
|
maxOrderAmountJpy: apiKeys.maxOrderAmountJpy,
|
||||||
allowedMethods: apiKeys.allowedMethods,
|
allowedMethods: apiKeys.allowedMethods,
|
||||||
|
scopes: apiKeys.scopes,
|
||||||
createdAt: apiKeys.createdAt,
|
createdAt: apiKeys.createdAt,
|
||||||
lastUsedAt: apiKeys.lastUsedAt,
|
lastUsedAt: apiKeys.lastUsedAt,
|
||||||
revokedAt: apiKeys.revokedAt,
|
revokedAt: apiKeys.revokedAt,
|
||||||
@@ -53,7 +61,7 @@ export const listApiKeys = async (db: Db) =>
|
|||||||
.orderBy(apiKeys.createdAt)
|
.orderBy(apiKeys.createdAt)
|
||||||
|
|
||||||
export const createApiKey = async (db: Db, label: string, settings: ApiKeySettings = {}) => {
|
export const createApiKey = async (db: Db, label: string, settings: ApiKeySettings = {}) => {
|
||||||
const token = `csbie_${randomToken()}`
|
const token = `mnie_${randomToken()}`
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const row = {
|
const row = {
|
||||||
id: randomId('key'),
|
id: randomId('key'),
|
||||||
+6
-1
@@ -22,7 +22,12 @@ const readQueryApiKey = (request: Request) => {
|
|||||||
const apiKeyAuth = async (db: Db, token: string) => {
|
const apiKeyAuth = async (db: Db, token: string) => {
|
||||||
const apiKey = await verifyApiKey(db, token)
|
const apiKey = await verifyApiKey(db, token)
|
||||||
return apiKey
|
return apiKey
|
||||||
? ({ type: 'apiKey', authenticated: true, apiKeyId: apiKey.id } satisfies AuthContext)
|
? ({
|
||||||
|
type: 'apiKey',
|
||||||
|
authenticated: true,
|
||||||
|
apiKeyId: apiKey.id,
|
||||||
|
scopes: apiKey.scopes?.length ? apiKey.scopes : ['read', 'write', 'trade', 'mcp'],
|
||||||
|
} satisfies AuthContext)
|
||||||
: ({ type: 'none', authenticated: false } satisfies AuthContext)
|
: ({ type: 'none', authenticated: false } satisfies AuthContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
+8
-8
@@ -3,7 +3,7 @@ import { mkdirSync } from 'node:fs'
|
|||||||
import { dirname, resolve } from 'node:path'
|
import { dirname, resolve } from 'node:path'
|
||||||
import { Database } from 'bun:sqlite'
|
import { Database } from 'bun:sqlite'
|
||||||
|
|
||||||
const SERVICE = 'csbie'
|
const SERVICE = 'mnie'
|
||||||
const SQLITE_BACKEND = 'sqlite'
|
const SQLITE_BACKEND = 'sqlite'
|
||||||
const PLATFORM_BACKEND = 'platform'
|
const PLATFORM_BACKEND = 'platform'
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ let keytar: Keytar | undefined
|
|||||||
let sqlite: Database | undefined
|
let sqlite: Database | undefined
|
||||||
|
|
||||||
const keyringBackend = (): KeyringBackend => {
|
const keyringBackend = (): KeyringBackend => {
|
||||||
const backend = process.env.CSBIE_KEYRING_BACKEND ?? PLATFORM_BACKEND
|
const backend = process.env.MNIE_KEYRING_BACKEND ?? PLATFORM_BACKEND
|
||||||
if (backend === PLATFORM_BACKEND || backend === SQLITE_BACKEND) return backend
|
if (backend === PLATFORM_BACKEND || backend === SQLITE_BACKEND) return backend
|
||||||
throw new Error(`unsupported CSBIE_KEYRING_BACKEND: ${backend}`)
|
throw new Error(`unsupported MNIE_KEYRING_BACKEND: ${backend}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadKeytar = async () => {
|
const loadKeytar = async () => {
|
||||||
@@ -27,15 +27,15 @@ const loadKeytar = async () => {
|
|||||||
|
|
||||||
const sqlitePath = () =>
|
const sqlitePath = () =>
|
||||||
resolve(
|
resolve(
|
||||||
process.env.CSBIE_KEYRING_SQLITE_PATH ??
|
process.env.MNIE_KEYRING_SQLITE_PATH ??
|
||||||
process.env.CSBIE_DATABASE_PATH?.replace(/\.sqlite$/u, '.keyring.sqlite') ??
|
process.env.MNIE_DATABASE_PATH?.replace(/\.sqlite$/u, '.keyring.sqlite') ??
|
||||||
'./data/csbie.keyring.sqlite',
|
'./data/mnie-app.keyring.sqlite',
|
||||||
)
|
)
|
||||||
|
|
||||||
const sqliteKey = () => {
|
const sqliteKey = () => {
|
||||||
const secret = process.env.CSBIE_KEYRING_SECRET
|
const secret = process.env.MNIE_KEYRING_SECRET
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new Error('CSBIE_KEYRING_SECRET is required when CSBIE_KEYRING_BACKEND=sqlite')
|
throw new Error('MNIE_KEYRING_SECRET is required when MNIE_KEYRING_BACKEND=sqlite')
|
||||||
}
|
}
|
||||||
return createHash('sha256').update(secret).digest()
|
return createHash('sha256').update(secret).digest()
|
||||||
}
|
}
|
||||||
+18
-11
@@ -16,6 +16,7 @@ import { randomToken, sha256 } from './crypto'
|
|||||||
const CODE_TTL_MS = 10 * 60 * 1000
|
const CODE_TTL_MS = 10 * 60 * 1000
|
||||||
const ACCESS_TOKEN_TTL_SECONDS = 60 * 60
|
const ACCESS_TOKEN_TTL_SECONDS = 60 * 60
|
||||||
const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
||||||
|
const DEFAULT_SCOPES = ['read', 'write', 'trade', 'mcp'] as const
|
||||||
|
|
||||||
export const createOAuthAuthorizationCode = async (
|
export const createOAuthAuthorizationCode = async (
|
||||||
db: Db,
|
db: Db,
|
||||||
@@ -54,7 +55,11 @@ export const createOAuthServerProvider = (db: Db, config: ServerConfig): OAuthSe
|
|||||||
registerClient: async (
|
registerClient: async (
|
||||||
client: Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>,
|
client: Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>,
|
||||||
) => {
|
) => {
|
||||||
const clientInfo = client as OAuthClientInformationFull
|
const clientInfo = {
|
||||||
|
...client,
|
||||||
|
client_id: randomToken(),
|
||||||
|
client_id_issued_at: Math.floor(Date.now() / 1000),
|
||||||
|
} satisfies OAuthClientInformationFull
|
||||||
await db
|
await db
|
||||||
.insert(oauthClients)
|
.insert(oauthClients)
|
||||||
.values({
|
.values({
|
||||||
@@ -117,14 +122,13 @@ export const createOAuthServerProvider = (db: Db, config: ServerConfig): OAuthSe
|
|||||||
.delete(oauthAuthorizationCodes)
|
.delete(oauthAuthorizationCodes)
|
||||||
.where(eq(oauthAuthorizationCodes.code, authorizationCode))
|
.where(eq(oauthAuthorizationCodes.code, authorizationCode))
|
||||||
|
|
||||||
const key = await createApiKey(
|
|
||||||
db,
|
|
||||||
`OAuth: ${client.client_name ?? client.client_id}`,
|
|
||||||
(code.apiKeySettings ?? {}) as ApiKeySettings,
|
|
||||||
)
|
|
||||||
const refreshToken = `csbie_refresh_${randomToken()}`
|
|
||||||
const now = new Date()
|
|
||||||
const scopes = code.scopes
|
const scopes = code.scopes
|
||||||
|
const key = await createApiKey(db, `OAuth: ${client.client_name ?? client.client_id}`, {
|
||||||
|
...((code.apiKeySettings ?? {}) as ApiKeySettings),
|
||||||
|
scopes,
|
||||||
|
})
|
||||||
|
const refreshToken = `mnie_refresh_${randomToken()}`
|
||||||
|
const now = new Date()
|
||||||
await db.insert(oauthRefreshTokens).values({
|
await db.insert(oauthRefreshTokens).values({
|
||||||
tokenHash: sha256(refreshToken),
|
tokenHash: sha256(refreshToken),
|
||||||
clientId: client.client_id,
|
clientId: client.client_id,
|
||||||
@@ -157,12 +161,15 @@ export const createOAuthServerProvider = (db: Db, config: ServerConfig): OAuthSe
|
|||||||
)
|
)
|
||||||
if (!token || token.expiresAt <= new Date()) throw new Error('refresh token expired')
|
if (!token || token.expiresAt <= new Date()) throw new Error('refresh token expired')
|
||||||
|
|
||||||
const newAccess = await createApiKey(db, `OAuth: ${client.client_name ?? client.client_id}`)
|
const nextScopes = scopes ?? token.scopes
|
||||||
|
const newAccess = await createApiKey(db, `OAuth: ${client.client_name ?? client.client_id}`, {
|
||||||
|
scopes: nextScopes,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
access_token: newAccess.token,
|
access_token: newAccess.token,
|
||||||
token_type: 'Bearer',
|
token_type: 'Bearer',
|
||||||
expires_in: ACCESS_TOKEN_TTL_SECONDS,
|
expires_in: ACCESS_TOKEN_TTL_SECONDS,
|
||||||
scope: (scopes ?? token.scopes).join(' '),
|
scope: nextScopes.join(' '),
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
} satisfies OAuthTokens
|
} satisfies OAuthTokens
|
||||||
},
|
},
|
||||||
@@ -173,7 +180,7 @@ export const createOAuthServerProvider = (db: Db, config: ServerConfig): OAuthSe
|
|||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
clientId: apiKey.id,
|
clientId: apiKey.id,
|
||||||
scopes: ['mcp'],
|
scopes: apiKey.scopes?.length ? apiKey.scopes : [...DEFAULT_SCOPES],
|
||||||
expiresAt: Math.floor(Date.now() / 1000) + ACCESS_TOKEN_TTL_SECONDS,
|
expiresAt: Math.floor(Date.now() / 1000) + ACCESS_TOKEN_TTL_SECONDS,
|
||||||
} satisfies AuthInfo
|
} satisfies AuthInfo
|
||||||
},
|
},
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { sha256, safeEqual } from './crypto'
|
import { sha256, safeEqual } from './crypto'
|
||||||
|
|
||||||
export const verifySetupPassword = (password: string) => {
|
export const verifySetupPassword = (password: string) => {
|
||||||
const hash = process.env.CSBIE_SETUP_PASSWORD_HASH
|
const hash = process.env.MNIE_SETUP_PASSWORD_HASH
|
||||||
if (hash) return safeEqual(sha256(password), hash)
|
if (hash) return safeEqual(sha256(password), hash)
|
||||||
|
|
||||||
const plaintext = process.env.CSBIE_SETUP_PASSWORD
|
const plaintext = process.env.MNIE_SETUP_PASSWORD
|
||||||
if (plaintext) return safeEqual(password, plaintext)
|
if (plaintext) return safeEqual(password, plaintext)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>cSBIe</title>
|
<title>Mnie</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/csbie-ui",
|
"name": "@repo/mnie-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -25,6 +25,7 @@ const router = useRouter()
|
|||||||
const activeTab = computed<RouteName>(() => {
|
const activeTab = computed<RouteName>(() => {
|
||||||
return routeNames.includes(route.name as RouteName) ? (route.name as RouteName) : 'portfolio'
|
return routeNames.includes(route.name as RouteName) ? (route.name as RouteName) : 'portfolio'
|
||||||
})
|
})
|
||||||
|
const isOAuthRoute = computed(() => route.name === 'oauthAuthorize')
|
||||||
const showAuthGate = computed(() => true)
|
const showAuthGate = computed(() => true)
|
||||||
const decodedRouteParam = (value: string) => {
|
const decodedRouteParam = (value: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -208,10 +209,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main :class="ui.appShell">
|
<main :class="ui.appShell">
|
||||||
<AppSidebar :active-tab="activeTab" @navigate="navigate" />
|
<AppSidebar v-if="!isOAuthRoute" :active-tab="activeTab" @navigate="navigate" />
|
||||||
|
|
||||||
<section :class="ui.workspace">
|
<section :class="ui.workspace">
|
||||||
<AppHeader v-if="activeTab !== 'settings'" :active-tab="activeTab" />
|
<AppHeader v-if="!isOAuthRoute && activeTab !== 'settings'" :active-tab="activeTab" />
|
||||||
|
|
||||||
<OAuthApprovalPanel
|
<OAuthApprovalPanel
|
||||||
v-if="oauthApproval.active && status.authenticated"
|
v-if="oauthApproval.active && status.authenticated"
|
||||||
@@ -12,6 +12,7 @@ export type ApiKey = {
|
|||||||
maxOrderPriceJpy?: number | null
|
maxOrderPriceJpy?: number | null
|
||||||
maxOrderAmountJpy?: number | null
|
maxOrderAmountJpy?: number | null
|
||||||
allowedMethods?: string[] | null
|
allowedMethods?: string[] | null
|
||||||
|
scopes?: string[] | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
lastUsedAt?: string | null
|
lastUsedAt?: string | null
|
||||||
revokedAt?: string | null
|
revokedAt?: string | null
|
||||||
@@ -26,6 +27,7 @@ export type ApiKeySettings = Pick<
|
|||||||
| 'maxOrderPriceJpy'
|
| 'maxOrderPriceJpy'
|
||||||
| 'maxOrderAmountJpy'
|
| 'maxOrderAmountJpy'
|
||||||
| 'allowedMethods'
|
| 'allowedMethods'
|
||||||
|
| 'scopes'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type SbiPasskey = {
|
export type SbiPasskey = {
|
||||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
+21
@@ -92,6 +92,14 @@ const tradingMethods = [
|
|||||||
|
|
||||||
const tradingMethodSet = new Set<string>(tradingMethods)
|
const tradingMethodSet = new Set<string>(tradingMethods)
|
||||||
const readMethods = rpcMethods.filter((method) => !tradingMethodSet.has(method))
|
const readMethods = rpcMethods.filter((method) => !tradingMethodSet.has(method))
|
||||||
|
const scopes = ['read', 'write', 'trade', 'mcp'] as const
|
||||||
|
|
||||||
|
const toggleScope = (scope: string) => {
|
||||||
|
const current = settings.value.scopes ?? []
|
||||||
|
settings.value.scopes = current.includes(scope)
|
||||||
|
? current.filter((candidate) => candidate !== scope)
|
||||||
|
: [...current, scope].sort()
|
||||||
|
}
|
||||||
|
|
||||||
const setMethods = (methods: readonly string[] | null) => {
|
const setMethods = (methods: readonly string[] | null) => {
|
||||||
settings.value.allowedMethods = methods ? [...methods] : null
|
settings.value.allowedMethods = methods ? [...methods] : null
|
||||||
@@ -116,6 +124,7 @@ const ui = {
|
|||||||
input:
|
input:
|
||||||
'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition focus:border-[#a8c7fa]',
|
'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition focus:border-[#a8c7fa]',
|
||||||
permissions: 'border-t border-[#33383f] pt-3',
|
permissions: 'border-t border-[#33383f] pt-3',
|
||||||
|
scopeGrid: 'grid grid-cols-2 gap-2 md:grid-cols-4',
|
||||||
summary: 'cursor-pointer font-black text-[#e3e3e9]',
|
summary: 'cursor-pointer font-black text-[#e3e3e9]',
|
||||||
actions: 'mt-3 flex flex-wrap gap-2',
|
actions: 'mt-3 flex flex-wrap gap-2',
|
||||||
button:
|
button:
|
||||||
@@ -129,6 +138,18 @@ const ui = {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="ui.root">
|
<div :class="ui.root">
|
||||||
|
<div :class="ui.scopeGrid">
|
||||||
|
<label v-for="scope in scopes" :key="scope" :class="ui.methodToggle">
|
||||||
|
<input
|
||||||
|
:class="ui.checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="settings.scopes?.includes(scope)"
|
||||||
|
@change="toggleScope(scope)"
|
||||||
|
/>
|
||||||
|
<span>{{ scope }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div :class="[ui.limitGrid, compact && ui.limitGridCompact]">
|
<div :class="[ui.limitGrid, compact && ui.limitGridCompact]">
|
||||||
<label :class="ui.label">
|
<label :class="ui.label">
|
||||||
1時間 取引上限
|
1時間 取引上限
|
||||||
+1
-1
@@ -17,7 +17,7 @@ const items = sidebarItems
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside :class="ui.sidebar">
|
<aside :class="ui.sidebar">
|
||||||
<div :class="ui.brandMark" aria-label="CSBIE">
|
<div :class="ui.brandMark" aria-label="MNIE">
|
||||||
<Activity class="h-8 w-8" :stroke-width="2.75" aria-hidden="true" />
|
<Activity class="h-8 w-8" :stroke-width="2.75" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<nav :class="ui.navStack" aria-label="Main">
|
<nav :class="ui.navStack" aria-label="Main">
|
||||||
+1
@@ -7,4 +7,5 @@ export const defaultApiKeyPolicy = (): ApiKeySettings => ({
|
|||||||
maxOrderPriceJpy: null,
|
maxOrderPriceJpy: null,
|
||||||
maxOrderAmountJpy: null,
|
maxOrderAmountJpy: null,
|
||||||
allowedMethods: null,
|
allowedMethods: null,
|
||||||
|
scopes: ['read'],
|
||||||
})
|
})
|
||||||
+2
@@ -39,6 +39,8 @@ export const useOAuthApproval = () => {
|
|||||||
state: url.searchParams.get('state') ?? '',
|
state: url.searchParams.get('state') ?? '',
|
||||||
resource: url.searchParams.get('resource') ?? '',
|
resource: url.searchParams.get('resource') ?? '',
|
||||||
}
|
}
|
||||||
|
const requestedScopes = oauthApproval.value.scope.split(' ').filter(Boolean)
|
||||||
|
oauthSettings.value.scopes = requestedScopes.length ? requestedScopes : ['read']
|
||||||
if (!oauthApproval.value.clientId) return
|
if (!oauthApproval.value.clientId) return
|
||||||
|
|
||||||
const response = await fetch(`/api/oauth/client/${oauthApproval.value.clientId}`, {
|
const response = await fetch(`/api/oauth/client/${oauthApproval.value.clientId}`, {
|
||||||
+2
-2
@@ -261,10 +261,10 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
|
|
||||||
const reportDataError = (message: string, cause?: unknown) => {
|
const reportDataError = (message: string, cause?: unknown) => {
|
||||||
if (cause) {
|
if (cause) {
|
||||||
console.error(`[csbie-ui] データ取得エラー: ${message}`, cause)
|
console.error(`[mnie-ui] データ取得エラー: ${message}`, cause)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error(`[csbie-ui] データ取得エラー: ${message}`)
|
console.error(`[mnie-ui] データ取得エラー: ${message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedStock = computed(() => {
|
const selectedStock = computed(() => {
|
||||||
@@ -2,8 +2,8 @@ import { defineConfig } from 'vite'
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
const serverPort = Number(process.env.PORT ?? process.env.CSBIE_SERVER_PORT ?? 8787)
|
const serverPort = Number(process.env.PORT ?? process.env.MNIE_SERVER_PORT ?? 8787)
|
||||||
const serverOrigin = process.env.CSBIE_SERVER_ORIGIN ?? `http://127.0.0.1:${serverPort}`
|
const serverOrigin = process.env.MNIE_SERVER_ORIGIN ?? `http://127.0.0.1:${serverPort}`
|
||||||
const proxyToServer = {
|
const proxyToServer = {
|
||||||
target: serverOrigin,
|
target: serverOrigin,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@@ -13,11 +13,11 @@ const proxyToServer = {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), tailwindcss()],
|
plugins: [vue(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
port: Number(process.env.CSBIE_UI_DEV_PORT ?? 5173),
|
port: Number(process.env.MNIE_UI_DEV_PORT ?? 5173),
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
hmr: {
|
hmr: {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
clientPort: Number(process.env.CSBIE_UI_DEV_PORT ?? 5173),
|
clientPort: Number(process.env.MNIE_UI_DEV_PORT ?? 5173),
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/csbie": {
|
"apps/mnie-app": {
|
||||||
"name": "@repo/csbie",
|
"name": "@repo/mnie-app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/csbie-server": "workspace:*",
|
"@repo/mnie-server": "workspace:*",
|
||||||
"hono": "^4.8.3",
|
"hono": "^4.8.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -23,13 +23,13 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/csbie-server": {
|
"apps/mnie-server": {
|
||||||
"name": "@repo/csbie-server",
|
"name": "@repo/mnie-server",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/mcp": "^0.3.0",
|
"@hono/mcp": "^0.3.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@napi-rs/keyring": "^1.2.0",
|
"@napi-rs/keyring": "^1.2.0",
|
||||||
"@repo/sbi-client": "workspace:*",
|
"@repo/client-sbi": "workspace:*",
|
||||||
"@simplewebauthn/server": "^13.1.2",
|
"@simplewebauthn/server": "^13.1.2",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
"hono": "^4.8.3",
|
"hono": "^4.8.3",
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/csbie-ui": {
|
"apps/mnie-ui": {
|
||||||
"name": "@repo/csbie-ui",
|
"name": "@repo/mnie-ui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.2",
|
"@simplewebauthn/browser": "^13.1.2",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
@@ -61,9 +61,10 @@
|
|||||||
"vue-tsc": "^3",
|
"vue-tsc": "^3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/sbi-client": {
|
"packages/client-sbi": {
|
||||||
"name": "@repo/sbi-client",
|
"name": "@repo/client-sbi",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/mnie-types": "workspace:*",
|
||||||
"iconv-lite": "^0.7.2",
|
"iconv-lite": "^0.7.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -73,6 +74,43 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/mnie-cli": {
|
||||||
|
"name": "@repo/mnie-cli",
|
||||||
|
"bin": {
|
||||||
|
"mnie": "./src/index.ts",
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@napi-rs/keyring": "^1.2.0",
|
||||||
|
"@repo/mnie-sdk": "workspace:*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/mnie-sdk": {
|
||||||
|
"name": "@repo/mnie-sdk",
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/mnie-types": "workspace:*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/mnie-types": {
|
||||||
|
"name": "@repo/mnie-types",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^8.0.0", "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g=="],
|
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^8.0.0", "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g=="],
|
||||||
@@ -319,13 +357,19 @@
|
|||||||
|
|
||||||
"@polka/url": ["@polka/[email protected]", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/[email protected]", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
|
|
||||||
"@repo/csbie": ["@repo/csbie@workspace:apps/csbie"],
|
"@repo/client-sbi": ["@repo/client-sbi@workspace:packages/client-sbi"],
|
||||||
|
|
||||||
"@repo/csbie-server": ["@repo/csbie-server@workspace:apps/csbie-server"],
|
"@repo/mnie-app": ["@repo/mnie-app@workspace:apps/mnie-app"],
|
||||||
|
|
||||||
"@repo/csbie-ui": ["@repo/csbie-ui@workspace:apps/csbie-ui"],
|
"@repo/mnie-cli": ["@repo/mnie-cli@workspace:packages/mnie-cli"],
|
||||||
|
|
||||||
"@repo/sbi-client": ["@repo/sbi-client@workspace:packages/sbi-client"],
|
"@repo/mnie-sdk": ["@repo/mnie-sdk@workspace:packages/mnie-sdk"],
|
||||||
|
|
||||||
|
"@repo/mnie-server": ["@repo/mnie-server@workspace:apps/mnie-server"],
|
||||||
|
|
||||||
|
"@repo/mnie-types": ["@repo/mnie-types@workspace:packages/mnie-types"],
|
||||||
|
|
||||||
|
"@repo/mnie-ui": ["@repo/mnie-ui@workspace:apps/mnie-ui"],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
|
"@rolldown/binding-android-arm64": ["@rolldown/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
csbie:
|
mnie:
|
||||||
image: git.yutakobayashi.com/nakasyou/csbi:latest
|
image: git.yutakobayashi.com/nakasyou/mnie:latest
|
||||||
ports:
|
ports:
|
||||||
- '${CSBIE_HOST_PORT:-18787}:8787'
|
- '${MNIE_HOST_PORT:-18787}:8787'
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "cSBI development and runtime helpers";
|
description = "Mnie development and runtime helpers";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
composeUp = pkgs.writeShellApplication {
|
composeUp = pkgs.writeShellApplication {
|
||||||
name = "csbi-compose-up";
|
name = "mnie-compose-up";
|
||||||
runtimeInputs = [
|
runtimeInputs = [
|
||||||
pkgs.docker-compose
|
pkgs.docker-compose
|
||||||
];
|
];
|
||||||
@@ -49,17 +49,17 @@
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
nativeRun = pkgs.writeShellApplication {
|
nativeRun = pkgs.writeShellApplication {
|
||||||
name = "csbi-native-run";
|
name = "mnie-native-run";
|
||||||
runtimeInputs = [
|
runtimeInputs = [
|
||||||
pkgs.bun
|
pkgs.bun
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
];
|
];
|
||||||
text = ''
|
text = ''
|
||||||
env_file="''${CSBIE_ENV_FILE:-$PWD/.env}"
|
env_file="''${MNIE_ENV_FILE:-$PWD/.env}"
|
||||||
data_dir="''${CSBIE_DATA_DIR:-$PWD/data}"
|
data_dir="''${MNIE_DATA_DIR:-$PWD/data}"
|
||||||
|
|
||||||
if [ ! -f "$env_file" ]; then
|
if [ ! -f "$env_file" ]; then
|
||||||
echo "CSBIE_ENV_FILE does not exist: $env_file" >&2
|
echo "MNIE_ENV_FILE does not exist: $env_file" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -74,30 +74,30 @@
|
|||||||
|
|
||||||
cd "$source_dir"
|
cd "$source_dir"
|
||||||
bun install --frozen-lockfile --linker hoisted
|
bun install --frozen-lockfile --linker hoisted
|
||||||
bun --filter @repo/csbie build
|
bun --filter @repo/mnie-app build
|
||||||
|
|
||||||
export CSBIE_DATABASE_PATH="''${CSBIE_DATABASE_PATH:-$data_dir/csbie.sqlite}"
|
export MNIE_DATABASE_PATH="''${MNIE_DATABASE_PATH:-$data_dir/mnie-app.sqlite}"
|
||||||
exec bun --env-file="$env_file" apps/csbie/dist/index.js "$@"
|
exec bun --env-file="$env_file" apps/mnie-app/dist/index.js "$@"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = {
|
default = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${composeUp}/bin/csbi-compose-up";
|
program = "${composeUp}/bin/mnie-compose-up";
|
||||||
meta.description = "Run cSBI with docker-compose";
|
meta.description = "Run Mnie with docker-compose";
|
||||||
};
|
};
|
||||||
|
|
||||||
compose-up = {
|
compose-up = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${composeUp}/bin/csbi-compose-up";
|
program = "${composeUp}/bin/mnie-compose-up";
|
||||||
meta.description = "Run cSBI with docker-compose";
|
meta.description = "Run Mnie with docker-compose";
|
||||||
};
|
};
|
||||||
|
|
||||||
csbie = {
|
mnie = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${nativeRun}/bin/csbi-native-run";
|
program = "${nativeRun}/bin/mnie-native-run";
|
||||||
meta.description = "Build and run cSBI directly with Bun";
|
meta.description = "Build and run Mnie directly with Bun";
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -111,19 +111,19 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.services.csbie;
|
cfg = config.services.mnie;
|
||||||
nativeRun = pkgs.writeShellApplication {
|
nativeRun = pkgs.writeShellApplication {
|
||||||
name = "csbi-native-run";
|
name = "mnie-native-run";
|
||||||
runtimeInputs = [
|
runtimeInputs = [
|
||||||
pkgs.bun
|
pkgs.bun
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
];
|
];
|
||||||
text = ''
|
text = ''
|
||||||
env_file="''${CSBIE_ENV_FILE:-$PWD/.env}"
|
env_file="''${MNIE_ENV_FILE:-$PWD/.env}"
|
||||||
data_dir="''${CSBIE_DATA_DIR:-$PWD/data}"
|
data_dir="''${MNIE_DATA_DIR:-$PWD/data}"
|
||||||
|
|
||||||
if [ ! -f "$env_file" ]; then
|
if [ ! -f "$env_file" ]; then
|
||||||
echo "CSBIE_ENV_FILE does not exist: $env_file" >&2
|
echo "MNIE_ENV_FILE does not exist: $env_file" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -138,16 +138,16 @@
|
|||||||
|
|
||||||
cd "$source_dir"
|
cd "$source_dir"
|
||||||
bun install --frozen-lockfile --linker hoisted
|
bun install --frozen-lockfile --linker hoisted
|
||||||
bun --filter @repo/csbie build
|
bun --filter @repo/mnie-app build
|
||||||
|
|
||||||
export CSBIE_DATABASE_PATH="''${CSBIE_DATABASE_PATH:-$data_dir/csbie.sqlite}"
|
export MNIE_DATABASE_PATH="''${MNIE_DATABASE_PATH:-$data_dir/mnie-app.sqlite}"
|
||||||
exec bun --env-file="$env_file" apps/csbie/dist/index.js "$@"
|
exec bun --env-file="$env_file" apps/mnie-app/dist/index.js "$@"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.csbie = {
|
options.services.mnie = {
|
||||||
enable = lib.mkEnableOption "cSBI";
|
enable = lib.mkEnableOption "Mnie";
|
||||||
|
|
||||||
runtime = lib.mkOption {
|
runtime = lib.mkOption {
|
||||||
type = lib.types.enum [
|
type = lib.types.enum [
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
|
|
||||||
image = lib.mkOption {
|
image = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "git.yutakobayashi.com/nakasyou/csbi:latest";
|
default = "git.yutakobayashi.com/nakasyou/mnie:latest";
|
||||||
description = "Container image to run when runtime is docker.";
|
description = "Container image to run when runtime is docker.";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,14 +171,14 @@
|
|||||||
|
|
||||||
dataDir = lib.mkOption {
|
dataDir = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "/var/lib/csbie";
|
default = "/var/lib/mnie-app";
|
||||||
description = "Host directory mounted at /app/data.";
|
description = "Host directory mounted at /app/data.";
|
||||||
};
|
};
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
default = 18787;
|
default = 18787;
|
||||||
description = "Host port exposed for cSBI.";
|
description = "Host port exposed for Mnie.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
|
|
||||||
(lib.mkIf (cfg.runtime == "docker") {
|
(lib.mkIf (cfg.runtime == "docker") {
|
||||||
virtualisation.oci-containers.backend = lib.mkDefault "docker";
|
virtualisation.oci-containers.backend = lib.mkDefault "docker";
|
||||||
virtualisation.oci-containers.containers.csbie = {
|
virtualisation.oci-containers.containers.mnie = {
|
||||||
image = cfg.image;
|
image = cfg.image;
|
||||||
ports = [
|
ports = [
|
||||||
"${toString cfg.port}:8787"
|
"${toString cfg.port}:8787"
|
||||||
@@ -206,8 +206,8 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf (cfg.runtime == "native") {
|
(lib.mkIf (cfg.runtime == "native") {
|
||||||
systemd.services.csbie = {
|
systemd.services.mnie = {
|
||||||
description = "cSBI";
|
description = "Mnie";
|
||||||
wantedBy = [
|
wantedBy = [
|
||||||
"multi-user.target"
|
"multi-user.target"
|
||||||
];
|
];
|
||||||
@@ -218,8 +218,8 @@
|
|||||||
"network-online.target"
|
"network-online.target"
|
||||||
];
|
];
|
||||||
environment = {
|
environment = {
|
||||||
CSBIE_ENV_FILE = toString cfg.envFile;
|
MNIE_ENV_FILE = toString cfg.envFile;
|
||||||
CSBIE_DATA_DIR = cfg.dataDir;
|
MNIE_DATA_DIR = cfg.dataDir;
|
||||||
PORT = toString cfg.port;
|
PORT = toString cfg.port;
|
||||||
};
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
};
|
};
|
||||||
script = ''
|
script = ''
|
||||||
exec ${nativeRun}/bin/csbi-native-run
|
exec ${nativeRun}/bin/mnie-native-run
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "csbi",
|
"name": "mnie",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
|
|||||||
@@ -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,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/mnie-types": "workspace:*",
|
||||||
"iconv-lite": "^0.7.2"
|
"iconv-lite": "^0.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
} from './methods/error-map'
|
} from './methods/error-map'
|
||||||
export type { SbiServerErrorOptions } from './methods/error-map'
|
export type { SbiServerErrorOptions } from './methods/error-map'
|
||||||
export { loginWithPasskey } from './session'
|
export { loginWithPasskey } from './session'
|
||||||
|
export type * from '@repo/mnie-types'
|
||||||
export type {
|
export type {
|
||||||
LoginWithPasskeyOptions,
|
LoginWithPasskeyOptions,
|
||||||
PasskeyLoginResponse,
|
PasskeyLoginResponse,
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export type * from '@repo/mnie-types'
|
||||||
@@ -207,7 +207,7 @@ const resolveForeignStockEndpointConfig = (
|
|||||||
restUrl: optionalUrl(restUrl) ?? requiredDerivedUrl(baseUrl, '/rest/'),
|
restUrl: optionalUrl(restUrl) ?? requiredDerivedUrl(baseUrl, '/rest/'),
|
||||||
graphqlBffUrl: optionalUrl(graphqlBffUrl) ?? requiredDerivedUrl(baseUrl, '/graphql/bff'),
|
graphqlBffUrl: optionalUrl(graphqlBffUrl) ?? requiredDerivedUrl(baseUrl, '/graphql/bff'),
|
||||||
graphqlIntUrl: optionalUrl(graphqlIntUrl) ?? requiredDerivedUrl(baseUrl, '/graphql/int'),
|
graphqlIntUrl: optionalUrl(graphqlIntUrl) ?? requiredDerivedUrl(baseUrl, '/graphql/int'),
|
||||||
userAgent: userAgent || 'SBIFStockAndroid/1.6.10(csbi/0)',
|
userAgent: userAgent || 'SBIFStockAndroid/1.6.10(mnie/0)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type * from './types'
|
||||||
|
export type * from './methods'
|
||||||
@@ -40,7 +40,7 @@ import type {
|
|||||||
TradeRecordList,
|
TradeRecordList,
|
||||||
TradeSide,
|
TradeSide,
|
||||||
Watchlist,
|
Watchlist,
|
||||||
} from '../types'
|
} from './types'
|
||||||
|
|
||||||
export type PagingOptions = {
|
export type PagingOptions = {
|
||||||
/** Start index for the result list. Defaults to the first item when omitted. */
|
/** Start index for the result list. Defaults to the first item when omitted. */
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user