bibliocanvas 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 karaage0703
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # bibliocanvas-cli
2
+
3
+ [BiblioCanvas](https://bibliocanvas.web.app) の書籍・本棚をコマンドラインから管理するCLIツール。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ npm install -g bibliocanvas
9
+ ```
10
+
11
+ または `npx` で直接実行:
12
+
13
+ ```bash
14
+ npx bibliocanvas <command>
15
+ ```
16
+
17
+ ## 使い方
18
+
19
+ ### ログイン
20
+
21
+ ```bash
22
+ bibliocanvas login
23
+ ```
24
+
25
+ ブラウザが開いてGoogleログイン画面が表示されます。ログインすると認証情報が `~/.bibliocanvas/credentials.json` に保存されます。
26
+
27
+ ```bash
28
+ # 開発環境にログイン
29
+ bibliocanvas login --dev
30
+
31
+ # 現在のユーザーを確認
32
+ bibliocanvas whoami
33
+
34
+ # ログアウト
35
+ bibliocanvas logout
36
+ ```
37
+
38
+ ### 書籍の管理
39
+
40
+ ```bash
41
+ # 書籍一覧
42
+ bibliocanvas list
43
+ bibliocanvas list --status READ # 読了した本のみ
44
+ bibliocanvas list -q "ChatGPT" # タイトル・著者で検索
45
+ bibliocanvas list --sort title --dir asc # タイトル順
46
+ bibliocanvas list --shelf <shelfId> # 特定の本棚の書籍のみ
47
+ bibliocanvas list --rating 5 # ★5の本のみ
48
+ bibliocanvas list --limit 20 # 20件まで表示
49
+ bibliocanvas list --shelf <id> --rating 4 --status READ # 組み合わせ
50
+
51
+ # ISBN で追加
52
+ bibliocanvas add --isbn 9784065371534
53
+
54
+ # タイトルで検索して追加
55
+ bibliocanvas add --search "面倒なことはChatGPT"
56
+
57
+ # 手動で追加
58
+ bibliocanvas add --title "本のタイトル" --authors "著者名"
59
+
60
+ # Google Books 検索(追加はしない)
61
+ bibliocanvas search "Pythonプログラミング"
62
+ bibliocanvas search 9784065371534 --isbn
63
+
64
+ # 読了ステータス更新
65
+ bibliocanvas update <bookId> --status READ
66
+
67
+ # 評価をつける
68
+ bibliocanvas update <bookId> --rating 5
69
+
70
+ # メモを追加
71
+ bibliocanvas update <bookId> --memo "面白かった"
72
+
73
+ # 書籍を削除
74
+ bibliocanvas delete <bookId>
75
+ ```
76
+
77
+ ### 本棚の管理
78
+
79
+ ```bash
80
+ # 本棚一覧
81
+ bibliocanvas shelf list
82
+
83
+ # 本棚の中身を表示
84
+ bibliocanvas shelf books <shelfId>
85
+
86
+ # 本棚を作成
87
+ bibliocanvas shelf create "AI・機械学習" -d "AI関連の本をまとめた本棚"
88
+
89
+ # 本棚に書籍を追加
90
+ bibliocanvas shelf add-book <shelfId> <bookId>
91
+
92
+ # 本棚から書籍を削除
93
+ bibliocanvas shelf remove-book <shelfId> <bookId>
94
+
95
+ # 本棚を削除
96
+ bibliocanvas shelf delete <shelfId>
97
+ ```
98
+
99
+ ### 公開本棚の閲覧(ログイン不要)
100
+
101
+ ```bash
102
+ # 公開本棚一覧
103
+ bibliocanvas public shelves
104
+ bibliocanvas public shelves --user karaage0703 # ユーザー指定
105
+ bibliocanvas public shelves --limit 10 # 件数制限
106
+
107
+ # 公開本棚の詳細(書籍一覧+メモ)
108
+ bibliocanvas public shelf <shelfId>
109
+ bibliocanvas public shelf <shelfId> --json # JSON出力
110
+ ```
111
+
112
+ ### 共通オプション
113
+
114
+ | オプション | 説明 |
115
+ |-----------|------|
116
+ | `--dev` | 開発環境(bibliocanvas-dev)を使用 |
117
+ | `--json` | JSON形式で出力 |
118
+
119
+ ### 読了ステータスの値
120
+
121
+ | 値 | 説明 |
122
+ |----|------|
123
+ | `NONE` | 未設定 |
124
+ | `UNREAD` | 未読 |
125
+ | `READING` | 読書中 |
126
+ | `READ` | 読了 |
127
+ | `BACKLOG` | 積読 |
128
+
129
+ ## 認証情報の保存先
130
+
131
+ 認証情報は `~/.bibliocanvas/credentials.json` に保存されます。このファイルにはFirebaseのリフレッシュトークンが含まれるため、他人と共有しないでください。
132
+
133
+ ## 開発
134
+
135
+ ```bash
136
+ git clone https://github.com/karaage0703/bibliocanvas-cli.git
137
+ cd bibliocanvas-cli
138
+ npm install
139
+ npm run build
140
+ node dist/index.js --help
141
+ ```
142
+
143
+ ## ライセンス
144
+
145
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * API client for BiblioCanvas REST API
3
+ */
4
+ export interface Book {
5
+ bookId: string;
6
+ title: string;
7
+ authors: string;
8
+ acquiredTime: number;
9
+ readStatus: 'NONE' | 'UNREAD' | 'READING' | 'READ' | 'BACKLOG';
10
+ productImage: string;
11
+ source: string;
12
+ addedDate: number;
13
+ rating?: number;
14
+ memo?: string;
15
+ detailPageUrl?: string;
16
+ }
17
+ export interface Shelf {
18
+ id: string;
19
+ name: string;
20
+ description: string;
21
+ books: string[];
22
+ isPublic: boolean;
23
+ color: string;
24
+ }
25
+ export interface SearchResult {
26
+ volumeId: string;
27
+ title: string;
28
+ authors: string;
29
+ thumbnail: string;
30
+ isbn?: string;
31
+ }
32
+ type Env = 'production' | 'development';
33
+ export interface PublicShelf {
34
+ id: string;
35
+ name: string;
36
+ description: string;
37
+ ownerUsername: string;
38
+ ownerDisplayName: string;
39
+ slug: string;
40
+ bookCount: number;
41
+ likeCount: number;
42
+ commentCount: number;
43
+ updatedAt: string | null;
44
+ books?: PublicBook[];
45
+ }
46
+ export interface PublicBook {
47
+ bookId: string;
48
+ title: string;
49
+ authors: string;
50
+ productImage: string;
51
+ rating: number | null;
52
+ memo: string | null;
53
+ readStatus: string | null;
54
+ }
55
+ export declare function listPublicShelves(env: Env, options?: {
56
+ limit?: number;
57
+ }): Promise<{
58
+ shelves: PublicShelf[];
59
+ total: number;
60
+ }>;
61
+ export declare function listUserPublicShelves(env: Env, username: string): Promise<{
62
+ shelves: PublicShelf[];
63
+ total: number;
64
+ }>;
65
+ export declare function getPublicShelf(env: Env, shelfId: string, options?: {
66
+ books?: boolean;
67
+ }): Promise<PublicShelf>;
68
+ export declare function listBooks(env: Env, options?: {
69
+ q?: string;
70
+ status?: string;
71
+ sort?: string;
72
+ dir?: string;
73
+ }): Promise<{
74
+ books: Book[];
75
+ total: number;
76
+ }>;
77
+ export declare function getBook(env: Env, bookId: string): Promise<Book>;
78
+ export declare function addBookByIsbn(env: Env, isbn: string): Promise<Book>;
79
+ export declare function addBookBySearch(env: Env, search: string, volumeId?: string): Promise<Book | {
80
+ message: string;
81
+ results: SearchResult[];
82
+ }>;
83
+ export declare function addBookManual(env: Env, data: {
84
+ title: string;
85
+ authors?: string;
86
+ readStatus?: string;
87
+ rating?: number;
88
+ memo?: string;
89
+ bookId?: string;
90
+ source?: string;
91
+ productImage?: string;
92
+ }): Promise<Book>;
93
+ export declare function updateBook(env: Env, bookId: string, updates: Record<string, unknown>): Promise<Book>;
94
+ export declare function deleteBook(env: Env, bookId: string): Promise<{
95
+ deleted: string;
96
+ }>;
97
+ export declare function searchBooks(env: Env, q: string, type?: 'isbn' | 'title'): Promise<{
98
+ results: SearchResult[];
99
+ }>;
100
+ export declare function listShelves(env: Env): Promise<{
101
+ shelves: Shelf[];
102
+ total: number;
103
+ }>;
104
+ export declare function createShelf(env: Env, data: {
105
+ name: string;
106
+ description?: string;
107
+ color?: string;
108
+ }): Promise<Shelf>;
109
+ export declare function updateShelf(env: Env, shelfId: string, updates: Record<string, unknown>): Promise<Shelf>;
110
+ export declare function deleteShelf(env: Env, shelfId: string): Promise<{
111
+ deleted: string;
112
+ }>;
113
+ export declare function addBookToShelf(env: Env, shelfId: string, bookId: string): Promise<{
114
+ shelfId: string;
115
+ bookId: string;
116
+ added: boolean;
117
+ }>;
118
+ export declare function removeBookFromShelf(env: Env, shelfId: string, bookId: string): Promise<{
119
+ shelfId: string;
120
+ bookId: string;
121
+ removed: boolean;
122
+ }>;
123
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * API client for BiblioCanvas REST API
3
+ */
4
+ import { getIdToken, getApiBaseUrl } from './auth.js';
5
+ async function apiRequest(method, path, env, body, query) {
6
+ const token = await getIdToken(env);
7
+ const baseUrl = getApiBaseUrl(env);
8
+ let url = `${baseUrl}${path}`;
9
+ if (query) {
10
+ const params = new URLSearchParams(Object.entries(query).filter(([, v]) => v !== undefined));
11
+ if (params.toString()) {
12
+ url += `?${params.toString()}`;
13
+ }
14
+ }
15
+ const options = {
16
+ method,
17
+ headers: {
18
+ Authorization: `Bearer ${token}`,
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ };
22
+ if (body && (method === 'POST' || method === 'PATCH')) {
23
+ options.body = JSON.stringify(body);
24
+ }
25
+ const response = await fetch(url, options);
26
+ const data = await response.json();
27
+ if (!response.ok) {
28
+ throw new Error(data.error || `API error: ${response.status}`);
29
+ }
30
+ return data;
31
+ }
32
+ async function publicApiRequest(path, env, query) {
33
+ const baseUrl = getApiBaseUrl(env);
34
+ let url = `${baseUrl}${path}`;
35
+ if (query) {
36
+ const params = new URLSearchParams(Object.entries(query).filter(([, v]) => v !== undefined));
37
+ if (params.toString()) {
38
+ url += `?${params.toString()}`;
39
+ }
40
+ }
41
+ const response = await fetch(url);
42
+ const data = await response.json();
43
+ if (!response.ok) {
44
+ throw new Error(data.error || `API error: ${response.status}`);
45
+ }
46
+ return data;
47
+ }
48
+ export async function listPublicShelves(env, options) {
49
+ const query = {};
50
+ if (options?.limit)
51
+ query.limit = options.limit.toString();
52
+ return publicApiRequest('/public/shelves', env, query);
53
+ }
54
+ export async function listUserPublicShelves(env, username) {
55
+ return publicApiRequest(`/public/users/${username}/shelves`, env);
56
+ }
57
+ export async function getPublicShelf(env, shelfId, options) {
58
+ const query = {};
59
+ if (options?.books === false)
60
+ query.books = 'false';
61
+ return publicApiRequest(`/public/shelves/${shelfId}`, env, query);
62
+ }
63
+ // ==================== Books ====================
64
+ export async function listBooks(env, options) {
65
+ const query = {};
66
+ if (options?.q)
67
+ query.q = options.q;
68
+ if (options?.status)
69
+ query.status = options.status;
70
+ if (options?.sort)
71
+ query.sort = options.sort;
72
+ if (options?.dir)
73
+ query.dir = options.dir;
74
+ return apiRequest('GET', '/books', env, undefined, query);
75
+ }
76
+ export async function getBook(env, bookId) {
77
+ return apiRequest('GET', `/books/${bookId}`, env);
78
+ }
79
+ export async function addBookByIsbn(env, isbn) {
80
+ return apiRequest('POST', '/books', env, { isbn });
81
+ }
82
+ export async function addBookBySearch(env, search, volumeId) {
83
+ const body = { search };
84
+ if (volumeId)
85
+ body.volumeId = volumeId;
86
+ return apiRequest('POST', '/books', env, body);
87
+ }
88
+ export async function addBookManual(env, data) {
89
+ return apiRequest('POST', '/books', env, data);
90
+ }
91
+ export async function updateBook(env, bookId, updates) {
92
+ return apiRequest('PATCH', `/books/${bookId}`, env, updates);
93
+ }
94
+ export async function deleteBook(env, bookId) {
95
+ return apiRequest('DELETE', `/books/${bookId}`, env);
96
+ }
97
+ export async function searchBooks(env, q, type) {
98
+ const query = { q };
99
+ if (type)
100
+ query.type = type;
101
+ return apiRequest('GET', '/books/search', env, undefined, query);
102
+ }
103
+ // ==================== Shelves ====================
104
+ export async function listShelves(env) {
105
+ return apiRequest('GET', '/shelves', env);
106
+ }
107
+ export async function createShelf(env, data) {
108
+ return apiRequest('POST', '/shelves', env, data);
109
+ }
110
+ export async function updateShelf(env, shelfId, updates) {
111
+ return apiRequest('PATCH', `/shelves/${shelfId}`, env, updates);
112
+ }
113
+ export async function deleteShelf(env, shelfId) {
114
+ return apiRequest('DELETE', `/shelves/${shelfId}`, env);
115
+ }
116
+ export async function addBookToShelf(env, shelfId, bookId) {
117
+ return apiRequest('POST', `/shelves/${shelfId}/books`, env, {
118
+ bookId,
119
+ });
120
+ }
121
+ export async function removeBookFromShelf(env, shelfId, bookId) {
122
+ return apiRequest('DELETE', `/shelves/${shelfId}/books/${bookId}`, env);
123
+ }
124
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Authentication module for BiblioCanvas CLI
3
+ *
4
+ * All authentication is handled server-side via the BiblioCanvas API.
5
+ * No Firebase config or API keys are stored in the CLI.
6
+ */
7
+ /**
8
+ * Get API base URL based on environment
9
+ */
10
+ export declare function getApiBaseUrl(env: 'production' | 'development'): string;
11
+ /**
12
+ * Delete stored credentials
13
+ */
14
+ export declare function deleteCredentials(): boolean;
15
+ /**
16
+ * Get a valid ID token (refresh via server-side API)
17
+ */
18
+ export declare function getIdToken(env?: 'production' | 'development'): Promise<string>;
19
+ /**
20
+ * Get current user info from stored credentials
21
+ */
22
+ export declare function getCurrentUser(): {
23
+ uid: string;
24
+ email: string;
25
+ displayName: string;
26
+ env: string;
27
+ } | null;
28
+ /**
29
+ * Login via browser OAuth flow (server-side token exchange)
30
+ *
31
+ * 1. Fetch OAuth client ID from server
32
+ * 2. Start local HTTP server
33
+ * 3. Open browser to Google OAuth consent screen
34
+ * 4. Receive authorization code via redirect
35
+ * 5. Send code to server for token exchange
36
+ * 6. Server returns Firebase tokens
37
+ * 7. Save refresh token to disk
38
+ */
39
+ export declare function login(env?: 'production' | 'development'): Promise<{
40
+ email: string;
41
+ displayName: string;
42
+ }>;
package/dist/auth.js ADDED
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Authentication module for BiblioCanvas CLI
3
+ *
4
+ * All authentication is handled server-side via the BiblioCanvas API.
5
+ * No Firebase config or API keys are stored in the CLI.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as http from 'node:http';
10
+ import * as crypto from 'node:crypto';
11
+ const CREDENTIALS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.bibliocanvas');
12
+ const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
13
+ const API_URLS = {
14
+ production: 'https://bibliocanvas.web.app/api',
15
+ development: 'https://bibliocanvas-dev.web.app/api',
16
+ };
17
+ /**
18
+ * Get API base URL based on environment
19
+ */
20
+ export function getApiBaseUrl(env) {
21
+ return API_URLS[env];
22
+ }
23
+ /**
24
+ * Load stored credentials
25
+ */
26
+ function loadCredentials() {
27
+ try {
28
+ if (fs.existsSync(CREDENTIALS_FILE)) {
29
+ const data = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
30
+ return JSON.parse(data);
31
+ }
32
+ }
33
+ catch {
34
+ // ignore
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Save credentials to disk
40
+ */
41
+ function saveCredentials(creds) {
42
+ if (!fs.existsSync(CREDENTIALS_DIR)) {
43
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
44
+ }
45
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), {
46
+ mode: 0o600,
47
+ });
48
+ }
49
+ /**
50
+ * Delete stored credentials
51
+ */
52
+ export function deleteCredentials() {
53
+ try {
54
+ if (fs.existsSync(CREDENTIALS_FILE)) {
55
+ fs.unlinkSync(CREDENTIALS_FILE);
56
+ return true;
57
+ }
58
+ }
59
+ catch {
60
+ // ignore
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Get a valid ID token (refresh via server-side API)
66
+ */
67
+ export async function getIdToken(env = 'production') {
68
+ const creds = loadCredentials();
69
+ if (!creds) {
70
+ throw new Error('Not logged in. Run `bibliocanvas login` first.');
71
+ }
72
+ if (creds.env !== env) {
73
+ throw new Error(`Logged in to ${creds.env} but trying to use ${env}. Run \`bibliocanvas login --dev\` or \`bibliocanvas login\`.`);
74
+ }
75
+ // Refresh token via server-side API (no API key needed on client)
76
+ const baseUrl = getApiBaseUrl(env);
77
+ const response = await fetch(`${baseUrl}/auth/refresh`, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({ refreshToken: creds.refreshToken }),
81
+ });
82
+ if (!response.ok) {
83
+ throw new Error('Token refresh failed. Run `bibliocanvas login` again.');
84
+ }
85
+ const data = await response.json();
86
+ // Update stored refresh token if it changed
87
+ if (data.refreshToken && data.refreshToken !== creds.refreshToken) {
88
+ creds.refreshToken = data.refreshToken;
89
+ saveCredentials(creds);
90
+ }
91
+ return data.idToken;
92
+ }
93
+ /**
94
+ * Get current user info from stored credentials
95
+ */
96
+ export function getCurrentUser() {
97
+ const creds = loadCredentials();
98
+ if (!creds)
99
+ return null;
100
+ return {
101
+ uid: creds.uid,
102
+ email: creds.email,
103
+ displayName: creds.displayName,
104
+ env: creds.env,
105
+ };
106
+ }
107
+ /**
108
+ * Login via browser OAuth flow (server-side token exchange)
109
+ *
110
+ * 1. Fetch OAuth client ID from server
111
+ * 2. Start local HTTP server
112
+ * 3. Open browser to Google OAuth consent screen
113
+ * 4. Receive authorization code via redirect
114
+ * 5. Send code to server for token exchange
115
+ * 6. Server returns Firebase tokens
116
+ * 7. Save refresh token to disk
117
+ */
118
+ export async function login(env = 'production') {
119
+ const baseUrl = getApiBaseUrl(env);
120
+ // Fetch OAuth client ID from server (no secrets in CLI)
121
+ const configResponse = await fetch(`${baseUrl}/auth/config`);
122
+ if (!configResponse.ok) {
123
+ throw new Error('Failed to fetch OAuth config from server');
124
+ }
125
+ const config = await configResponse.json();
126
+ const clientId = config.clientId;
127
+ return new Promise((resolve, reject) => {
128
+ const server = http.createServer();
129
+ server.listen(0, '127.0.0.1', async () => {
130
+ const address = server.address();
131
+ if (!address || typeof address === 'string') {
132
+ reject(new Error('Failed to start local server'));
133
+ return;
134
+ }
135
+ const port = address.port;
136
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
137
+ // Generate state for CSRF protection
138
+ const state = crypto.randomBytes(16).toString('hex');
139
+ // Build Google OAuth URL
140
+ const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
141
+ authUrl.searchParams.set('client_id', clientId);
142
+ authUrl.searchParams.set('redirect_uri', redirectUri);
143
+ authUrl.searchParams.set('response_type', 'code');
144
+ authUrl.searchParams.set('scope', 'openid email profile');
145
+ authUrl.searchParams.set('state', state);
146
+ authUrl.searchParams.set('access_type', 'offline');
147
+ authUrl.searchParams.set('prompt', 'consent');
148
+ console.log(`\nOpening browser for Google login...`);
149
+ console.log(`If the browser doesn't open, visit:\n${authUrl.toString()}\n`);
150
+ // Open browser
151
+ const open = (await import('open')).default;
152
+ open(authUrl.toString()).catch(() => {
153
+ // Browser open failed, user can use the URL manually
154
+ });
155
+ // Handle the OAuth callback
156
+ server.on('request', async (req, res) => {
157
+ if (!req.url?.startsWith('/callback'))
158
+ return;
159
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
160
+ const code = url.searchParams.get('code');
161
+ const returnedState = url.searchParams.get('state');
162
+ if (returnedState !== state) {
163
+ res.writeHead(400, { 'Content-Type': 'text/html' });
164
+ res.end('<h1>Error: Invalid state</h1>');
165
+ server.close();
166
+ reject(new Error('Invalid OAuth state'));
167
+ return;
168
+ }
169
+ if (!code) {
170
+ res.writeHead(400, { 'Content-Type': 'text/html' });
171
+ res.end('<h1>Error: No authorization code</h1>');
172
+ server.close();
173
+ reject(new Error('No authorization code received'));
174
+ return;
175
+ }
176
+ try {
177
+ // Send code to server for token exchange (no secrets on client)
178
+ const loginResponse = await fetch(`${baseUrl}/auth/login`, {
179
+ method: 'POST',
180
+ headers: { 'Content-Type': 'application/json' },
181
+ body: JSON.stringify({ code, redirectUri }),
182
+ });
183
+ if (!loginResponse.ok) {
184
+ const errorData = await loginResponse.json();
185
+ throw new Error(errorData.error || 'Authentication failed');
186
+ }
187
+ const loginData = await loginResponse.json();
188
+ // Save credentials (only refresh token, no API keys)
189
+ saveCredentials({
190
+ refreshToken: loginData.refreshToken,
191
+ uid: loginData.uid,
192
+ email: loginData.email || '',
193
+ displayName: loginData.displayName || '',
194
+ env,
195
+ });
196
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
197
+ res.end(`
198
+ <html>
199
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
200
+ <h1>✓ ログイン成功!</h1>
201
+ <p>${loginData.displayName || loginData.email} としてログインしました。</p>
202
+ <p>このウィンドウを閉じてください。</p>
203
+ </body>
204
+ </html>
205
+ `);
206
+ server.close();
207
+ resolve({
208
+ email: loginData.email || '',
209
+ displayName: loginData.displayName || '',
210
+ });
211
+ }
212
+ catch (err) {
213
+ res.writeHead(500, { 'Content-Type': 'text/html' });
214
+ res.end(`<h1>Error: ${err.message}</h1>`);
215
+ server.close();
216
+ reject(err);
217
+ }
218
+ });
219
+ // Timeout after 2 minutes
220
+ setTimeout(() => {
221
+ server.close();
222
+ reject(new Error('Login timed out. Please try again.'));
223
+ }, 120000);
224
+ });
225
+ });
226
+ }
227
+ //# sourceMappingURL=auth.js.map