@fuul/mcp-server 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/README.md +218 -0
- package/dist/affiliate-portal/affiliate-portal-queries.d.ts +15 -0
- package/dist/affiliate-portal/affiliate-portal-queries.d.ts.map +1 -0
- package/dist/affiliate-portal/affiliate-portal-queries.js +35 -0
- package/dist/affiliate-portal/affiliate-portal-queries.js.map +1 -0
- package/dist/agent/write-confirmation.d.ts +20 -0
- package/dist/agent/write-confirmation.d.ts.map +1 -0
- package/dist/agent/write-confirmation.js +34 -0
- package/dist/agent/write-confirmation.js.map +1 -0
- package/dist/auth/oauth-client.d.ts +13 -0
- package/dist/auth/oauth-client.d.ts.map +1 -0
- package/dist/auth/oauth-client.js +185 -0
- package/dist/auth/oauth-client.js.map +1 -0
- package/dist/auth/pkce.d.ts +5 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +12 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/token-store.d.ts +7 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +44 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/types.d.ts +13 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +67 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/env.d.ts +10 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +30 -0
- package/dist/config/env.js.map +1 -0
- package/dist/http/fuul-api-client.d.ts +51 -0
- package/dist/http/fuul-api-client.d.ts.map +1 -0
- package/dist/http/fuul-api-client.js +161 -0
- package/dist/http/fuul-api-client.js.map +1 -0
- package/dist/http/nest-query.d.ts +6 -0
- package/dist/http/nest-query.d.ts.map +1 -0
- package/dist/http/nest-query.js +24 -0
- package/dist/http/nest-query.js.map +1 -0
- package/dist/http/retry-after.d.ts +8 -0
- package/dist/http/retry-after.d.ts.map +1 -0
- package/dist/http/retry-after.js +28 -0
- package/dist/http/retry-after.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +366 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata/metadata-service.d.ts +11 -0
- package/dist/metadata/metadata-service.d.ts.map +1 -0
- package/dist/metadata/metadata-service.js +71 -0
- package/dist/metadata/metadata-service.js.map +1 -0
- package/dist/payouts/payout-batch-handlers.d.ts +4 -0
- package/dist/payouts/payout-batch-handlers.d.ts.map +1 -0
- package/dist/payouts/payout-batch-handlers.js +34 -0
- package/dist/payouts/payout-batch-handlers.js.map +1 -0
- package/dist/tools/tool-descriptions.d.ts +25 -0
- package/dist/tools/tool-descriptions.d.ts.map +1 -0
- package/dist/tools/tool-descriptions.js +40 -0
- package/dist/tools/tool-descriptions.js.map +1 -0
- package/dist/tools/tool-schemas.d.ts +35 -0
- package/dist/tools/tool-schemas.d.ts.map +1 -0
- package/dist/tools/tool-schemas.js +145 -0
- package/dist/tools/tool-schemas.js.map +1 -0
- package/dist/util/compact-query.d.ts +3 -0
- package/dist/util/compact-query.d.ts.map +1 -0
- package/dist/util/compact-query.js +5 -0
- package/dist/util/compact-query.js.map +1 -0
- package/dist/util/with-timeout.d.ts +12 -0
- package/dist/util/with-timeout.d.ts.map +1 -0
- package/dist/util/with-timeout.js +35 -0
- package/dist/util/with-timeout.js.map +1 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# @fuul/mcp-server
|
|
2
|
+
|
|
3
|
+
Fuul [Model Context Protocol](https://modelcontextprotocol.io/) server: OAuth CLI (`fuul-mcp`), stdio MCP entry (`fuul-mcp-server`), metadata tools, project and affiliate analytics, incentive and payout operations (reads plus `dry_run` / `confirmed` writes), and rate-limit-aware errors.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
| Resource | Purpose |
|
|
8
|
+
| -------- | ------- |
|
|
9
|
+
| [docs/README.md](docs/README.md) | Index of all docs in this repo |
|
|
10
|
+
| [docs/AGENTS.md](docs/AGENTS.md) | Tool ↔ HTTP map (audit, support, PR review) |
|
|
11
|
+
| [docs/mcp-phase2/CONSUMER.md](docs/mcp-phase2/CONSUMER.md) | Staging/production URLs, API expectations |
|
|
12
|
+
| [docs/mcp-phase2/tool-prompts.md](docs/mcp-phase2/tool-prompts.md) | Sample prompts for tooling and evals |
|
|
13
|
+
| [CHANGELOG.md](CHANGELOG.md) | Release notes |
|
|
14
|
+
|
|
15
|
+
## Install (pick one path)
|
|
16
|
+
|
|
17
|
+
### 1. Claude Code plugin (marketplace)
|
|
18
|
+
|
|
19
|
+
Adds the MCP server plus a **skill** that documents how to use Fuul tools.
|
|
20
|
+
|
|
21
|
+
In **Claude Code**:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
/plugin marketplace add kuyen-labs/mcp_server
|
|
25
|
+
/plugin install fuul-mcp@fuul-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
One-time OAuth in a terminal (same tokens the MCP uses):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y @fuul/mcp-server@latest fuul-mcp login
|
|
32
|
+
npx -y @fuul/mcp-server@latest fuul-mcp whoami
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Optional: set **staging** in the plugin’s user settings — `FUUL_API_BASE_URL` = `https://api.stg.fuul.xyz`. Default is production `https://api.fuul.xyz`.
|
|
36
|
+
|
|
37
|
+
Requires **`@fuul/mcp-server@0.2.0`** or newer on npm (for the `fuul-mcp-server` binary). After the first release with this version, `npx @latest` resolves correctly.
|
|
38
|
+
|
|
39
|
+
### 2. npm / npx (any MCP client)
|
|
40
|
+
|
|
41
|
+
Use the published package without cloning:
|
|
42
|
+
|
|
43
|
+
| Command | Role |
|
|
44
|
+
| ------- | ---- |
|
|
45
|
+
| `npx -y @fuul/mcp-server@latest fuul-mcp-server` | Stdio MCP server (what clients spawn) |
|
|
46
|
+
| `npx -y @fuul/mcp-server@latest fuul-mcp login` | Browser OAuth; writes `~/.fuul/tokens.json` |
|
|
47
|
+
| `npx -y @fuul/mcp-server@latest fuul-mcp whoami` | `GET /api/v1/auth/user` |
|
|
48
|
+
|
|
49
|
+
Point your client at `fuul-mcp-server` with `cwd` optional; config can be passed via `env` (see **Configuration**).
|
|
50
|
+
|
|
51
|
+
### 3. Clone (development)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/kuyen-labs/mcp_server.git
|
|
55
|
+
cd mcp_server
|
|
56
|
+
npm ci
|
|
57
|
+
cp .env.example .env # optional; defaults match production Agent OAuth
|
|
58
|
+
npm run build
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Run the CLI from the repo:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm run cli -- login
|
|
65
|
+
npm run cli -- whoami
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run the MCP server:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm start
|
|
72
|
+
# or: npm run dev
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Environment variables are read from `process.env` and, when present, a **`.env` file in the current working directory** (`dotenv`). See [.env.example](.env.example).
|
|
78
|
+
|
|
79
|
+
| Variable | Purpose |
|
|
80
|
+
| -------- | ------- |
|
|
81
|
+
| `FUUL_API_BASE_URL` | API origin only, no trailing slash. Production: `https://api.fuul.xyz`. Staging: `https://api.stg.fuul.xyz`. |
|
|
82
|
+
| `FUUL_OAUTH_CLIENT_ID` | OAuth client id (default `fuul-agent`). |
|
|
83
|
+
| `FUUL_OAUTH_REDIRECT_URI` | Loopback callback (default `http://127.0.0.1:8765/callback`). |
|
|
84
|
+
| `FUUL_MCP_TOOL_TIMEOUT_MS` | Per-tool timeout in ms (default `90000`). |
|
|
85
|
+
| `FUUL_MCP_DEBUG` | Set to `1` or `true` for debug logging. |
|
|
86
|
+
|
|
87
|
+
**Note:** Values in `.env` are local dev convenience; they are **not** published in the npm package (`package.json` only ships `dist/`). OAuth **tokens** live in `~/.fuul/tokens.json`, not in `.env`.
|
|
88
|
+
|
|
89
|
+
## MCP tool examples
|
|
90
|
+
|
|
91
|
+
Clients send JSON arguments; shapes match [docs/AGENTS.md](docs/AGENTS.md). Illustrative only — use real UUIDs from your tenant.
|
|
92
|
+
|
|
93
|
+
**Session**
|
|
94
|
+
|
|
95
|
+
- `ping` → `{}` (no API call)
|
|
96
|
+
- `whoami` → `{}` (requires login)
|
|
97
|
+
|
|
98
|
+
**Metadata**
|
|
99
|
+
|
|
100
|
+
- `list_chains`, `list_trigger_types`, `list_payout_schemas` → `{}`
|
|
101
|
+
|
|
102
|
+
**Projects**
|
|
103
|
+
|
|
104
|
+
- `list_projects` → `{ "page": 1, "query": "acme" }`
|
|
105
|
+
- `get_project` → `{ "project_id": "<uuid>" }`
|
|
106
|
+
- `list_incentives` / `get_incentive` / `get_trigger` → see tool descriptions in the client
|
|
107
|
+
|
|
108
|
+
**Affiliate analytics**
|
|
109
|
+
|
|
110
|
+
- `get_affiliate_portal_stats` → `project_id`, `user_identifier` (e.g. `evm:0x...`)
|
|
111
|
+
- `get_project_affiliate_total_stats` → `project_id`, optional `dateRange`, filters
|
|
112
|
+
- `get_project_affiliates_breakdown` → `project_id`, **`groupBy`** (`audience` \| `tier` \| `region` \| `status`)
|
|
113
|
+
|
|
114
|
+
**Writes (two steps: `dry_run: true` then `confirmed: true`)**
|
|
115
|
+
|
|
116
|
+
- `create_incentive_program`, `update_incentive_program`, `approve_payouts`, `reject_payouts`
|
|
117
|
+
|
|
118
|
+
## Run and debug
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm run build && npm start
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
MCP Inspector:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run build
|
|
128
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Client setup
|
|
132
|
+
|
|
133
|
+
### Cursor
|
|
134
|
+
|
|
135
|
+
1. Complete **Clone** or use **npx** so `dist/index.js` or `fuul-mcp-server` exists; run **`fuul-mcp login`** once.
|
|
136
|
+
2. **Settings → MCP** (or user `mcp.json`): spawn stdio with `command` + `args`, and set **`cwd`** to a folder that contains `.env` if you use one.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"fuul": {
|
|
144
|
+
"command": "node",
|
|
145
|
+
"args": ["C:\\path\\to\\mcp_server\\dist\\index.js"],
|
|
146
|
+
"cwd": "C:\\path\\to\\mcp_server"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Or with npx:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"fuul": {
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["-y", "@fuul/mcp-server@latest", "fuul-mcp-server"],
|
|
160
|
+
"env": {
|
|
161
|
+
"FUUL_API_BASE_URL": "https://api.fuul.xyz"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Claude Desktop
|
|
169
|
+
|
|
170
|
+
Use the same `mcpServers` idea in the app’s developer config. See [Anthropic MCP docs](https://docs.anthropic.com/en/docs/mcp).
|
|
171
|
+
|
|
172
|
+
### Claude Code (manual MCP, without the plugin)
|
|
173
|
+
|
|
174
|
+
Configure stdio per your Claude Code version: `node` + path to `dist/index.js`, or `npx` + `fuul-mcp-server` as above.
|
|
175
|
+
|
|
176
|
+
## Repository layout
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
mcp_server/
|
|
180
|
+
├── .claude-plugin/ # Claude Code marketplace manifest
|
|
181
|
+
│ └── marketplace.json
|
|
182
|
+
├── plugins/
|
|
183
|
+
│ └── fuul-mcp/ # Plugin: MCP config + skill
|
|
184
|
+
│ ├── .claude-plugin/
|
|
185
|
+
│ │ └── plugin.json
|
|
186
|
+
│ ├── .mcp.json
|
|
187
|
+
│ └── skills/fuul/SKILL.md
|
|
188
|
+
├── src/ # TypeScript source
|
|
189
|
+
├── docs/ # Maintainer and integrator docs
|
|
190
|
+
├── .github/workflows/ # ci.yml, publish.yml
|
|
191
|
+
└── dist/ # `npm run build` (gitignored)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Requirements
|
|
195
|
+
|
|
196
|
+
- **Node.js** 18+
|
|
197
|
+
- A running **fuul-server** (staging or production) with **Agent OAuth** configured
|
|
198
|
+
|
|
199
|
+
## CI and releases
|
|
200
|
+
|
|
201
|
+
On each push/PR to `main` / `master`, GitHub Actions runs **lint**, **test**, and **build** in parallel.
|
|
202
|
+
|
|
203
|
+
Publishing to npm is triggered by publishing a **GitHub Release** (see [.github/workflows/publish.yml](.github/workflows/publish.yml)); the repository needs an `NPM_TOKEN` secret.
|
|
204
|
+
|
|
205
|
+
## Scripts
|
|
206
|
+
|
|
207
|
+
| Script | Description |
|
|
208
|
+
| ------ | ----------- |
|
|
209
|
+
| `npm run build` | Compile TypeScript → `dist/` |
|
|
210
|
+
| `npm start` | Run MCP server (`node dist/index.js`) |
|
|
211
|
+
| `npm run cli` | OAuth CLI via `tsx` (`src/cli.ts`) |
|
|
212
|
+
| `npm run dev` | MCP via `tsx` (`src/index.ts`) |
|
|
213
|
+
| `npm run lint` | ESLint on `src/` |
|
|
214
|
+
| `npm run test` | Vitest |
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT — see `package.json`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GetAffiliatePortalStatsInput, GetProjectAffiliatesBreakdownInput, GetProjectAffiliateTotalStatsInput } from '../tools/tool-schemas.js';
|
|
2
|
+
/** Query object for GET /api/v1/projects/:projectId/affiliate-portal/stats */
|
|
3
|
+
export declare function affiliatePortalStatsQueryFromInput(input: GetAffiliatePortalStatsInput): Record<string, unknown>;
|
|
4
|
+
/** Query object for GET .../affiliate-portal/total-stats */
|
|
5
|
+
export declare function projectAffiliateTotalStatsQueryFromInput(input: GetProjectAffiliateTotalStatsInput): Record<string, unknown>;
|
|
6
|
+
/** Query object for GET .../affiliate-portal/global-breakdown */
|
|
7
|
+
export declare function projectAffiliatesBreakdownQueryFromInput(input: GetProjectAffiliatesBreakdownInput): Record<string, unknown>;
|
|
8
|
+
type AffiliatePortalSegment = 'stats' | 'total-stats' | 'global-breakdown';
|
|
9
|
+
/** Relative URL path with query string for affiliate-portal GET routes. */
|
|
10
|
+
export declare function buildAffiliatePortalGetPath(projectId: string, segment: AffiliatePortalSegment, query: Record<string, unknown>): string;
|
|
11
|
+
export declare function affiliatePortalStatsPath(input: GetAffiliatePortalStatsInput): string;
|
|
12
|
+
export declare function projectAffiliateTotalStatsPath(input: GetProjectAffiliateTotalStatsInput): string;
|
|
13
|
+
export declare function projectAffiliatesBreakdownPath(input: GetProjectAffiliatesBreakdownInput): string;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=affiliate-portal-queries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affiliate-portal-queries.d.ts","sourceRoot":"","sources":["../../src/affiliate-portal/affiliate-portal-queries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,4BAA4B,EAAE,kCAAkC,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AASrJ,8EAA8E;AAC9E,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,4BAA4B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE/G;AAED,4DAA4D;AAC5D,wBAAgB,wCAAwC,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE3H;AAED,iEAAiE;AACjE,wBAAgB,wCAAwC,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE3H;AAED,KAAK,sBAAsB,GAAG,OAAO,GAAG,aAAa,GAAG,kBAAkB,CAAC;AAE3E,2EAA2E;AAC3E,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAItI;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,4BAA4B,GAAG,MAAM,CAEpF;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAEhG;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,kCAAkC,GAAG,MAAM,CAEhG"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { buildNestQueryString } from '../http/nest-query.js';
|
|
2
|
+
import { compactQuery } from '../util/compact-query.js';
|
|
3
|
+
function omitProjectId(input) {
|
|
4
|
+
const { project_id, ...rest } = input;
|
|
5
|
+
void project_id;
|
|
6
|
+
return rest;
|
|
7
|
+
}
|
|
8
|
+
/** Query object for GET /api/v1/projects/:projectId/affiliate-portal/stats */
|
|
9
|
+
export function affiliatePortalStatsQueryFromInput(input) {
|
|
10
|
+
return omitProjectId(input);
|
|
11
|
+
}
|
|
12
|
+
/** Query object for GET .../affiliate-portal/total-stats */
|
|
13
|
+
export function projectAffiliateTotalStatsQueryFromInput(input) {
|
|
14
|
+
return omitProjectId(input);
|
|
15
|
+
}
|
|
16
|
+
/** Query object for GET .../affiliate-portal/global-breakdown */
|
|
17
|
+
export function projectAffiliatesBreakdownQueryFromInput(input) {
|
|
18
|
+
return omitProjectId(input);
|
|
19
|
+
}
|
|
20
|
+
/** Relative URL path with query string for affiliate-portal GET routes. */
|
|
21
|
+
export function buildAffiliatePortalGetPath(projectId, segment, query) {
|
|
22
|
+
const base = `/api/v1/projects/${projectId}/affiliate-portal/${segment}`;
|
|
23
|
+
const q = buildNestQueryString(compactQuery(query));
|
|
24
|
+
return q ? `${base}?${q}` : base;
|
|
25
|
+
}
|
|
26
|
+
export function affiliatePortalStatsPath(input) {
|
|
27
|
+
return buildAffiliatePortalGetPath(input.project_id, 'stats', affiliatePortalStatsQueryFromInput(input));
|
|
28
|
+
}
|
|
29
|
+
export function projectAffiliateTotalStatsPath(input) {
|
|
30
|
+
return buildAffiliatePortalGetPath(input.project_id, 'total-stats', projectAffiliateTotalStatsQueryFromInput(input));
|
|
31
|
+
}
|
|
32
|
+
export function projectAffiliatesBreakdownPath(input) {
|
|
33
|
+
return buildAffiliatePortalGetPath(input.project_id, 'global-breakdown', projectAffiliatesBreakdownQueryFromInput(input));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=affiliate-portal-queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affiliate-portal-queries.js","sourceRoot":"","sources":["../../src/affiliate-portal/affiliate-portal-queries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,SAAS,aAAa,CAAmC,KAAQ;IAC/D,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IACtC,KAAK,UAAU,CAAC;IAChB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,kCAAkC,CAAC,KAAmC;IACpF,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,wCAAwC,CAAC,KAAyC;IAChG,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,wCAAwC,CAAC,KAAyC;IAChG,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAID,2EAA2E;AAC3E,MAAM,UAAU,2BAA2B,CAAC,SAAiB,EAAE,OAA+B,EAAE,KAA8B;IAC5H,MAAM,IAAI,GAAG,oBAAoB,SAAS,qBAAqB,OAAO,EAAE,CAAC;IACzE,MAAM,CAAC,GAAG,oBAAoB,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAmC;IAC1E,OAAO,2BAA2B,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,kCAAkC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,KAAyC;IACtF,OAAO,2BAA2B,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,EAAE,wCAAwC,CAAC,KAAK,CAAC,CAAC,CAAC;AACvH,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,KAAyC;IACtF,OAAO,2BAA2B,CAAC,KAAK,CAAC,UAAU,EAAE,kBAAkB,EAAE,wCAAwC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5H,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Standard input fragment for Phase 2 write tools. Extend with tool-specific fields.
|
|
4
|
+
* Flow: first call with dry_run: true (or omit confirmed) to get validation / preview;
|
|
5
|
+
* second call with confirmed: true to execute.
|
|
6
|
+
*/
|
|
7
|
+
export declare const writeConfirmationFieldsSchema: any;
|
|
8
|
+
export type WriteConfirmationFields = z.infer<typeof writeConfirmationFieldsSchema>;
|
|
9
|
+
export declare class WriteNotConfirmedError extends Error {
|
|
10
|
+
readonly code: "WRITE_NOT_CONFIRMED";
|
|
11
|
+
constructor(message?: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Enforces the mandatory confirmation pattern for mutating tools.
|
|
15
|
+
* - dry_run: true → caller should return preview only (no throw).
|
|
16
|
+
* - confirmed: true → mutation allowed.
|
|
17
|
+
* - Otherwise → throws WriteNotConfirmedError.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertWriteConfirmedOrDryRun(input: WriteConfirmationFields): void;
|
|
20
|
+
//# sourceMappingURL=write-confirmation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-confirmation.d.ts","sourceRoot":"","sources":["../../src/agent/write-confirmation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,KAGxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAG,qBAAqB,CAAU;gBAEnC,OAAO,SAAiG;CAKrH;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAQjF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Standard input fragment for Phase 2 write tools. Extend with tool-specific fields.
|
|
4
|
+
* Flow: first call with dry_run: true (or omit confirmed) to get validation / preview;
|
|
5
|
+
* second call with confirmed: true to execute.
|
|
6
|
+
*/
|
|
7
|
+
export const writeConfirmationFieldsSchema = z.object({
|
|
8
|
+
dry_run: z.boolean().optional().describe('If true, validate and return a preview only; no server mutation.'),
|
|
9
|
+
confirmed: z.boolean().optional().describe('Must be true to perform the mutation after reviewing dry_run output.'),
|
|
10
|
+
});
|
|
11
|
+
export class WriteNotConfirmedError extends Error {
|
|
12
|
+
code = 'WRITE_NOT_CONFIRMED';
|
|
13
|
+
constructor(message = 'Mutation blocked: use dry_run: true first to validate, then call again with confirmed: true.') {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'WriteNotConfirmedError';
|
|
16
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Enforces the mandatory confirmation pattern for mutating tools.
|
|
21
|
+
* - dry_run: true → caller should return preview only (no throw).
|
|
22
|
+
* - confirmed: true → mutation allowed.
|
|
23
|
+
* - Otherwise → throws WriteNotConfirmedError.
|
|
24
|
+
*/
|
|
25
|
+
export function assertWriteConfirmedOrDryRun(input) {
|
|
26
|
+
if (input.dry_run === true) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (input.confirmed === true) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw new WriteNotConfirmedError();
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=write-confirmation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-confirmation.js","sourceRoot":"","sources":["../../src/agent/write-confirmation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kEAAkE,CAAC;IAC5G,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;CACnH,CAAC,CAAC;AAIH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,qBAA8B,CAAC;IAE/C,YAAY,OAAO,GAAG,8FAA8F;QAClH,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAA8B;IACzE,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO;IACT,CAAC;IACD,MAAM,IAAI,sBAAsB,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Env } from '../config/env.js';
|
|
2
|
+
import { TokenStore } from './token-store.js';
|
|
3
|
+
import type { StoredTokens } from './types.js';
|
|
4
|
+
export declare class OAuthClient {
|
|
5
|
+
private readonly env;
|
|
6
|
+
private readonly tokenStore;
|
|
7
|
+
constructor(env: Env, tokenStore: TokenStore);
|
|
8
|
+
private log;
|
|
9
|
+
private http;
|
|
10
|
+
login(): Promise<void>;
|
|
11
|
+
refreshFromStore(): Promise<StoredTokens | null>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=oauth-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-client.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAoB,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,YAAY,CAAC;AAI9D,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,UAAU;gBADV,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,UAAU;IAGzC,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,IAAI;IAUN,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoDtB,gBAAgB,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAiBvD"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { apiOriginFromEnv } from '../config/env.js';
|
|
6
|
+
import { createCodeChallengeS256, createCodeVerifier, createOAuthState } from './pkce.js';
|
|
7
|
+
const LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
|
|
8
|
+
export class OAuthClient {
|
|
9
|
+
env;
|
|
10
|
+
tokenStore;
|
|
11
|
+
constructor(env, tokenStore) {
|
|
12
|
+
this.env = env;
|
|
13
|
+
this.tokenStore = tokenStore;
|
|
14
|
+
}
|
|
15
|
+
log(...args) {
|
|
16
|
+
if (this.env.debug) {
|
|
17
|
+
console.error('[fuul-mcp]', ...args);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
http() {
|
|
21
|
+
const baseURL = apiOriginFromEnv(this.env);
|
|
22
|
+
return axios.create({
|
|
23
|
+
baseURL,
|
|
24
|
+
timeout: 30_000,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
validateStatus: () => true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async login() {
|
|
30
|
+
const origin = apiOriginFromEnv(this.env);
|
|
31
|
+
const verifier = createCodeVerifier();
|
|
32
|
+
const challenge = createCodeChallengeS256(verifier);
|
|
33
|
+
const state = createOAuthState();
|
|
34
|
+
const redirectUri = this.env.FUUL_OAUTH_REDIRECT_URI;
|
|
35
|
+
const { port, hostname, pathname } = parseRedirect(redirectUri);
|
|
36
|
+
const authorizeUrl = new URL(`${origin}/oauth/authorize`);
|
|
37
|
+
authorizeUrl.searchParams.set('response_type', 'code');
|
|
38
|
+
authorizeUrl.searchParams.set('client_id', this.env.FUUL_OAUTH_CLIENT_ID);
|
|
39
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
40
|
+
authorizeUrl.searchParams.set('state', state);
|
|
41
|
+
authorizeUrl.searchParams.set('code_challenge', challenge);
|
|
42
|
+
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
|
43
|
+
const codePromise = captureAuthorizationCode({
|
|
44
|
+
port,
|
|
45
|
+
hostname,
|
|
46
|
+
pathname,
|
|
47
|
+
expectedState: state,
|
|
48
|
+
timeoutMs: LOGIN_TIMEOUT_MS,
|
|
49
|
+
log: this.log.bind(this),
|
|
50
|
+
});
|
|
51
|
+
this.log('Opening browser for login:', authorizeUrl.toString());
|
|
52
|
+
await open(authorizeUrl.toString());
|
|
53
|
+
const { code } = await codePromise;
|
|
54
|
+
const client = this.http();
|
|
55
|
+
const tokenRes = await client.post('/oauth/token', {
|
|
56
|
+
grant_type: 'authorization_code',
|
|
57
|
+
code,
|
|
58
|
+
redirect_uri: redirectUri,
|
|
59
|
+
client_id: this.env.FUUL_OAUTH_CLIENT_ID,
|
|
60
|
+
code_verifier: verifier,
|
|
61
|
+
});
|
|
62
|
+
if (tokenRes.status !== 200 || !tokenRes.data?.access_token) {
|
|
63
|
+
const body = tokenRes.data;
|
|
64
|
+
const msg = body?.error_description || `Token exchange failed (HTTP ${tokenRes.status})`;
|
|
65
|
+
throw new Error(msg);
|
|
66
|
+
}
|
|
67
|
+
const tokens = tokenResponseToStored(tokenRes.data);
|
|
68
|
+
await this.tokenStore.write(tokens);
|
|
69
|
+
console.log('Logged in. Tokens saved to ~/.fuul/tokens.json');
|
|
70
|
+
}
|
|
71
|
+
async refreshFromStore() {
|
|
72
|
+
const current = await this.tokenStore.read();
|
|
73
|
+
if (!current?.refresh_token) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const client = this.http();
|
|
77
|
+
const tokenRes = await client.post('/oauth/token', {
|
|
78
|
+
grant_type: 'refresh_token',
|
|
79
|
+
refresh_token: current.refresh_token,
|
|
80
|
+
});
|
|
81
|
+
if (tokenRes.status !== 200 || !tokenRes.data?.access_token) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const tokens = tokenResponseToStored(tokenRes.data);
|
|
85
|
+
await this.tokenStore.write(tokens);
|
|
86
|
+
return tokens;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function tokenResponseToStored(data) {
|
|
90
|
+
const expires_at_ms = Date.now() + Math.max(0, data.expires_in) * 1000;
|
|
91
|
+
return {
|
|
92
|
+
access_token: data.access_token,
|
|
93
|
+
refresh_token: data.refresh_token,
|
|
94
|
+
expires_at_ms,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseRedirect(redirectUri) {
|
|
98
|
+
const u = new URL(redirectUri);
|
|
99
|
+
const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80;
|
|
100
|
+
const hostname = u.hostname;
|
|
101
|
+
const pathname = u.pathname || '/';
|
|
102
|
+
return { port, hostname, pathname };
|
|
103
|
+
}
|
|
104
|
+
function captureAuthorizationCode(opts) {
|
|
105
|
+
const { port, hostname, pathname, expectedState, timeoutMs, log } = opts;
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
let settled = false;
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
if (!settled) {
|
|
110
|
+
settled = true;
|
|
111
|
+
server.close();
|
|
112
|
+
reject(new Error('Login timed out waiting for browser redirect'));
|
|
113
|
+
}
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
const server = http.createServer((req, res) => {
|
|
116
|
+
void (async () => {
|
|
117
|
+
try {
|
|
118
|
+
const hostUrl = `http://${req.headers.host ?? `${hostname}:${port}`}`;
|
|
119
|
+
const url = new URL(req.url ?? '/', hostUrl);
|
|
120
|
+
if (url.pathname !== pathname) {
|
|
121
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
122
|
+
res.end('Not found');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const oauthError = url.searchParams.get('error');
|
|
126
|
+
if (oauthError) {
|
|
127
|
+
const desc = url.searchParams.get('error_description') ?? oauthError;
|
|
128
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
129
|
+
res.end(`<p>Authorization error: ${escapeHtml(desc)}</p>`);
|
|
130
|
+
if (!settled) {
|
|
131
|
+
settled = true;
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
server.close();
|
|
134
|
+
reject(new Error(desc));
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const code = url.searchParams.get('code');
|
|
139
|
+
const state = url.searchParams.get('state');
|
|
140
|
+
if (!code || state !== expectedState) {
|
|
141
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
142
|
+
res.end('<p>Invalid callback (missing code or state mismatch).</p>');
|
|
143
|
+
if (!settled) {
|
|
144
|
+
settled = true;
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
server.close();
|
|
147
|
+
reject(new Error('Invalid OAuth callback'));
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
152
|
+
res.end('<p>Login complete. You can close this window.</p>');
|
|
153
|
+
if (!settled) {
|
|
154
|
+
settled = true;
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
server.close(() => resolve({ code }));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
log(e);
|
|
161
|
+
if (!settled) {
|
|
162
|
+
settled = true;
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
server.close();
|
|
165
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
});
|
|
170
|
+
server.on('error', (e) => {
|
|
171
|
+
if (!settled) {
|
|
172
|
+
settled = true;
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
reject(e);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
server.listen(port, hostname, () => {
|
|
178
|
+
log(`Listening for OAuth callback on http://${hostname}:${port}${pathname}`);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function escapeHtml(s) {
|
|
183
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=oauth-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-client.js","sourceRoot":"","sources":["../../src/auth/oauth-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** RFC 7636 code verifier (43–128 characters). */
|
|
2
|
+
export declare function createCodeVerifier(): string;
|
|
3
|
+
export declare function createCodeChallengeS256(verifier: string): string;
|
|
4
|
+
export declare function createOAuthState(): string;
|
|
5
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA,kDAAkD;AAClD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
/** RFC 7636 code verifier (43–128 characters). */
|
|
3
|
+
export function createCodeVerifier() {
|
|
4
|
+
return randomBytes(32).toString('base64url');
|
|
5
|
+
}
|
|
6
|
+
export function createCodeChallengeS256(verifier) {
|
|
7
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
8
|
+
}
|
|
9
|
+
export function createOAuthState() {
|
|
10
|
+
return randomBytes(16).toString('base64url');
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,kDAAkD;AAClD,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/C,qBAAa,UAAU;IACf,IAAI,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAapC,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO7B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const DIR_NAME = '.fuul';
|
|
5
|
+
const FILE_NAME = 'tokens.json';
|
|
6
|
+
function tokenFilePath() {
|
|
7
|
+
return join(homedir(), DIR_NAME, FILE_NAME);
|
|
8
|
+
}
|
|
9
|
+
export class TokenStore {
|
|
10
|
+
async read() {
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(tokenFilePath(), 'utf8');
|
|
13
|
+
const data = JSON.parse(raw);
|
|
14
|
+
if (!data.access_token || !data.refresh_token || typeof data.expires_at_ms !== 'number') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async write(tokens) {
|
|
24
|
+
const dir = join(homedir(), DIR_NAME);
|
|
25
|
+
await mkdir(dir, { recursive: true });
|
|
26
|
+
const path = tokenFilePath();
|
|
27
|
+
await writeFile(path, `${JSON.stringify(tokens, null, 2)}\n`, 'utf8');
|
|
28
|
+
try {
|
|
29
|
+
await chmod(path, 0o600);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Windows may ignore mode; ignore errors.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async clear() {
|
|
36
|
+
try {
|
|
37
|
+
await unlink(tokenFilePath());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore if missing
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,MAAM,QAAQ,GAAG,OAAO,CAAC;AACzB,MAAM,SAAS,GAAG,aAAa,CAAC;AAEhC,SAAS,aAAa;IACpB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,CAAC,CAAC;YACpD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;YAC7C,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;gBACxF,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,MAAoB;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;QACtC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;QAC7B,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtE,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB;QACtB,CAAC;IACH,CAAC;CACF"}
|