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 +21 -0
- package/README.md +145 -0
- package/dist/api.d.ts +123 -0
- package/dist/api.js +124 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +42 -0
- package/dist/auth.js +227 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +514 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/skills/bibliocanvas/SKILL.md +138 -0
- package/src/api.ts +315 -0
- package/src/auth.ts +270 -0
- package/src/index.ts +545 -0
- package/tsconfig.json +17 -0
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
|
package/dist/api.js.map
ADDED
|
@@ -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
|