@monadeo.com/grimoire-core 0.3.7 → 0.4.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/dist/auth.d.ts +24 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +147 -48
- package/dist/client.d.ts +31 -19
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +74 -48
- package/dist/config.d.ts +1 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/generated/schema.d.ts +604 -1131
- package/dist/generated/schema.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export interface StoredSession {
|
|
2
|
+
refresh_token: string;
|
|
3
|
+
supabase_url: string;
|
|
4
|
+
supabase_anon_key: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function storeSession(session: StoredSession): void;
|
|
7
|
+
export declare function readSession(): StoredSession | undefined;
|
|
8
|
+
export declare function clearSession(): void;
|
|
4
9
|
export declare function storeMachineToken(token: string): void;
|
|
5
10
|
export declare function readMachineToken(): string | undefined;
|
|
6
11
|
export declare function clearMachineToken(): void;
|
|
7
|
-
export
|
|
12
|
+
export interface AuthConfig {
|
|
13
|
+
supabase_url: string;
|
|
14
|
+
supabase_anon_key: string;
|
|
15
|
+
oauth_provider: string;
|
|
16
|
+
redirect_urls: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface TokenResponse {
|
|
19
|
+
access_token: string;
|
|
20
|
+
refresh_token: string;
|
|
21
|
+
expires_in: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function fetchAuthConfig(apiBase: string): Promise<AuthConfig>;
|
|
24
|
+
type GrantType = "pkce" | "refresh_token";
|
|
25
|
+
export declare function gotrueToken(supabaseUrl: string, anonKey: string, grantType: GrantType, body: Record<string, string>): Promise<TokenResponse>;
|
|
26
|
+
export declare function browserLogin(apiBase: string, openBrowser: (url: string) => void, timeoutMs?: number): Promise<void>;
|
|
27
|
+
export {};
|
|
8
28
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAIzD;AAED,wBAAgB,WAAW,IAAI,aAAa,GAAG,SAAS,CAmBvD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AASD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIrD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAOrD;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB1E;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,eAAe,CAAC;AAY1C,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,OAAO,CAAC,aAAa,CAAC,CA0BxB;AAkCD,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAClC,SAAS,SAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAuDf"}
|
package/dist/auth.js
CHANGED
|
@@ -3,31 +3,43 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { ApiError } from "./errors.js";
|
|
6
7
|
import { fetchWithTimeout } from "./http.js";
|
|
7
8
|
// Session storage is a 0600 file, not the OS keychain (Astro 2026-07-14):
|
|
8
9
|
// keychain ACLs are per binary, so every agent runtime spawning the MCP server
|
|
9
10
|
// re-prompted the user — gcloud and Codex accept the same file-based model.
|
|
10
11
|
// A machine token via GRIMOIRE_AUTH_TOKEN overrides interactive auth (CI only).
|
|
12
|
+
function configDir() {
|
|
13
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "grimoire");
|
|
14
|
+
}
|
|
11
15
|
function credentialsPath() {
|
|
12
|
-
return join(
|
|
16
|
+
return join(configDir(), "credentials.json");
|
|
13
17
|
}
|
|
14
|
-
export function
|
|
18
|
+
export function storeSession(session) {
|
|
15
19
|
const path = credentialsPath();
|
|
16
20
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
17
|
-
writeFileSync(path, JSON.stringify(
|
|
21
|
+
writeFileSync(path, JSON.stringify(session) + "\n", { mode: 0o600 });
|
|
18
22
|
}
|
|
19
|
-
export function
|
|
23
|
+
export function readSession() {
|
|
20
24
|
try {
|
|
21
25
|
const parsed = JSON.parse(readFileSync(credentialsPath(), "utf8"));
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
if (typeof parsed.refresh_token === "string" &&
|
|
27
|
+
parsed.refresh_token !== "" &&
|
|
28
|
+
typeof parsed.supabase_url === "string" &&
|
|
29
|
+
typeof parsed.supabase_anon_key === "string") {
|
|
30
|
+
return {
|
|
31
|
+
refresh_token: parsed.refresh_token,
|
|
32
|
+
supabase_url: parsed.supabase_url,
|
|
33
|
+
supabase_anon_key: parsed.supabase_anon_key,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
25
37
|
}
|
|
26
38
|
catch {
|
|
27
39
|
return undefined;
|
|
28
40
|
}
|
|
29
41
|
}
|
|
30
|
-
export function
|
|
42
|
+
export function clearSession() {
|
|
31
43
|
if (existsSync(credentialsPath()))
|
|
32
44
|
rmSync(credentialsPath());
|
|
33
45
|
}
|
|
@@ -35,7 +47,7 @@ export function clearRefreshToken() {
|
|
|
35
47
|
// GRIMOIRE_AUTH_TOKEN in a shell profile (that env var still overrides). Same
|
|
36
48
|
// 0600 model as credentials.json — never the world-readable config.json.
|
|
37
49
|
function machineTokenPath() {
|
|
38
|
-
return join(
|
|
50
|
+
return join(configDir(), "machine-token");
|
|
39
51
|
}
|
|
40
52
|
export function storeMachineToken(token) {
|
|
41
53
|
const path = machineTokenPath();
|
|
@@ -55,58 +67,145 @@ export function clearMachineToken() {
|
|
|
55
67
|
if (existsSync(machineTokenPath()))
|
|
56
68
|
rmSync(machineTokenPath());
|
|
57
69
|
}
|
|
70
|
+
// The API publishes everything a client needs to log in: the public Supabase
|
|
71
|
+
// URL, its anon key (public by design), the OAuth provider, and the loopback
|
|
72
|
+
// callbacks that are allow-listed for the PKCE flow.
|
|
73
|
+
export async function fetchAuthConfig(apiBase) {
|
|
74
|
+
const res = await fetchWithTimeout(`${apiBase.replace(/\/+$/, "")}/v1/auth/config`);
|
|
75
|
+
if (!res.ok)
|
|
76
|
+
throw new ApiError(res.status, "auth_config_unavailable", await res.text());
|
|
77
|
+
const body = (await res.json());
|
|
78
|
+
if (typeof body.supabase_url !== "string" ||
|
|
79
|
+
typeof body.supabase_anon_key !== "string" ||
|
|
80
|
+
typeof body.oauth_provider !== "string" ||
|
|
81
|
+
!Array.isArray(body.redirect_urls) ||
|
|
82
|
+
body.redirect_urls.length === 0) {
|
|
83
|
+
throw new ApiError(res.status, "auth_config_malformed", body);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
supabase_url: body.supabase_url,
|
|
87
|
+
supabase_anon_key: body.supabase_anon_key,
|
|
88
|
+
oauth_provider: body.oauth_provider,
|
|
89
|
+
redirect_urls: body.redirect_urls.map(String),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function parseDetail(text) {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Supabase Auth (GoTrue) token endpoint: POST /auth/v1/token?grant_type=…, the
|
|
101
|
+
// anon key in the apikey header, JSON body per grant.
|
|
102
|
+
export async function gotrueToken(supabaseUrl, anonKey, grantType, body) {
|
|
103
|
+
const res = await fetchWithTimeout(`${supabaseUrl.replace(/\/+$/, "")}/auth/v1/token?grant_type=${grantType}`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json", apikey: anonKey },
|
|
106
|
+
body: JSON.stringify(body),
|
|
107
|
+
});
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
throw new ApiError(res.status, grantType === "pkce" ? "login_failed" : "refresh_failed", parseDetail(text));
|
|
111
|
+
}
|
|
112
|
+
const parsed = JSON.parse(text);
|
|
113
|
+
if (typeof parsed.access_token !== "string" ||
|
|
114
|
+
typeof parsed.refresh_token !== "string" ||
|
|
115
|
+
typeof parsed.expires_in !== "number") {
|
|
116
|
+
throw new ApiError(res.status, "token_malformed", parsed);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
access_token: parsed.access_token,
|
|
120
|
+
refresh_token: parsed.refresh_token,
|
|
121
|
+
expires_in: parsed.expires_in,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
58
124
|
function base64url(buf) {
|
|
59
125
|
return buf.toString("base64url");
|
|
60
126
|
}
|
|
61
|
-
//
|
|
62
|
-
// the
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
127
|
+
// Bind the first free allow-listed loopback callback (IPv4). The bases come
|
|
128
|
+
// from the API, so the client and the Supabase allow list cannot drift.
|
|
129
|
+
async function bindCallback(server, redirectUrls) {
|
|
130
|
+
for (const candidate of redirectUrls) {
|
|
131
|
+
const port = Number(new URL(candidate).port);
|
|
132
|
+
const bound = await new Promise((resolve) => {
|
|
133
|
+
const onError = () => {
|
|
134
|
+
server.off("error", onError);
|
|
135
|
+
resolve(false);
|
|
136
|
+
};
|
|
137
|
+
server.once("error", onError);
|
|
138
|
+
server.listen(port, "127.0.0.1", () => {
|
|
139
|
+
server.off("error", onError);
|
|
140
|
+
resolve(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
if (bound)
|
|
144
|
+
return candidate;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`No free login callback port among: ${redirectUrls.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
// OAuth Authorization Code with PKCE through Supabase Auth: open the provider
|
|
149
|
+
// login in the browser, receive the code on a loopback callback, exchange code +
|
|
150
|
+
// verifier for tokens. No client secret exists anywhere in the client.
|
|
151
|
+
//
|
|
152
|
+
// Supabase Auth's /authorize carries no `state` parameter, so the per-attempt
|
|
153
|
+
// state rides in the callback path (`/callback/<state>`); any other path is a
|
|
154
|
+
// 404 and the server keeps waiting. The allow list holds `/callback/*`.
|
|
155
|
+
export async function browserLogin(apiBase, openBrowser, timeoutMs = 300_000) {
|
|
156
|
+
const config = await fetchAuthConfig(apiBase);
|
|
70
157
|
const verifier = base64url(randomBytes(32));
|
|
71
158
|
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
72
159
|
const state = base64url(randomBytes(16));
|
|
73
160
|
const code = await new Promise((resolve, reject) => {
|
|
161
|
+
let timer;
|
|
162
|
+
const finish = (outcome) => {
|
|
163
|
+
if (timer)
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
server.closeAllConnections();
|
|
166
|
+
server.close();
|
|
167
|
+
if ("code" in outcome)
|
|
168
|
+
resolve(outcome.code);
|
|
169
|
+
else
|
|
170
|
+
reject(outcome.error);
|
|
171
|
+
};
|
|
74
172
|
const server = createServer((req, res) => {
|
|
75
173
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
76
|
-
|
|
77
|
-
if (req.method !== "GET" ||
|
|
78
|
-
url.pathname !== "/callback" ||
|
|
79
|
-
url.searchParams.get("state") !== state ||
|
|
80
|
-
!received) {
|
|
174
|
+
if (req.method !== "GET" || url.pathname !== `/callback/${state}`) {
|
|
81
175
|
res.writeHead(404).end();
|
|
82
176
|
return;
|
|
83
177
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
178
|
+
const failure = url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
|
179
|
+
const received = url.searchParams.get("code");
|
|
180
|
+
if (failure || !received) {
|
|
181
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end(`Login failed: ${failure ?? "no code"}`);
|
|
182
|
+
finish({ error: new Error(`Login failed: ${failure ?? "no code returned"}`) });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
res
|
|
186
|
+
.writeHead(200, { "Content-Type": "text/plain" })
|
|
187
|
+
.end("Logged in to Grimoire. You can close this tab.");
|
|
188
|
+
finish({ code: received });
|
|
94
189
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
190
|
+
bindCallback(server, config.redirect_urls)
|
|
191
|
+
.then((redirectBase) => {
|
|
192
|
+
timer = setTimeout(() => finish({ error: new Error("Login timed out") }), timeoutMs);
|
|
193
|
+
const authorize = new URL(`${config.supabase_url.replace(/\/+$/, "")}/auth/v1/authorize`);
|
|
194
|
+
authorize.searchParams.set("provider", config.oauth_provider);
|
|
195
|
+
authorize.searchParams.set("redirect_to", `${redirectBase.replace(/\/+$/, "")}/${state}`);
|
|
196
|
+
authorize.searchParams.set("code_challenge", challenge);
|
|
197
|
+
authorize.searchParams.set("code_challenge_method", "s256");
|
|
198
|
+
openBrowser(authorize.toString());
|
|
199
|
+
})
|
|
200
|
+
.catch((err) => finish({ error: err }));
|
|
99
201
|
});
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
202
|
+
const tokens = await gotrueToken(config.supabase_url, config.supabase_anon_key, "pkce", {
|
|
203
|
+
auth_code: code,
|
|
204
|
+
code_verifier: verifier,
|
|
205
|
+
});
|
|
206
|
+
storeSession({
|
|
207
|
+
refresh_token: tokens.refresh_token,
|
|
208
|
+
supabase_url: config.supabase_url,
|
|
209
|
+
supabase_anon_key: config.supabase_anon_key,
|
|
104
210
|
});
|
|
105
|
-
if (!res.ok)
|
|
106
|
-
throw new Error(`Token exchange failed: ${res.status}`);
|
|
107
|
-
const { refresh_token } = (await res.json());
|
|
108
|
-
if (typeof refresh_token !== "string" || refresh_token.length === 0) {
|
|
109
|
-
throw new Error("Token exchange succeeded but returned no refresh token");
|
|
110
|
-
}
|
|
111
|
-
storeRefreshToken(refresh_token);
|
|
112
211
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import type { components
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
5
|
-
export type
|
|
6
|
-
export type
|
|
7
|
-
export type
|
|
8
|
-
export type
|
|
9
|
-
export type
|
|
10
|
-
export type
|
|
11
|
-
export type
|
|
1
|
+
import type { components } from "./generated/schema.js";
|
|
2
|
+
type Schemas = components["schemas"];
|
|
3
|
+
export type SearchRequest = Schemas["SearchRequest"];
|
|
4
|
+
export type SearchResponse = Schemas["SearchResponse"];
|
|
5
|
+
export type SearchResult = Schemas["SearchResult"];
|
|
6
|
+
export type SourceSelector = Schemas["SourceSelector"];
|
|
7
|
+
export type SourceOut = Schemas["SourceOut"];
|
|
8
|
+
export type VersionsOut = Schemas["VersionsOut"];
|
|
9
|
+
export type DocWindowOut = Schemas["DocWindowOut"];
|
|
10
|
+
export type ReportIn = Schemas["ReportIn"];
|
|
11
|
+
export type ReportOut = Schemas["ReportOut"];
|
|
12
|
+
export type SubmissionIn = Schemas["SubmissionIn"];
|
|
13
|
+
export type SubmitAccepted = Schemas["SubmitAccepted"];
|
|
14
|
+
export type JobOut = Schemas["JobOut"];
|
|
15
|
+
export type MeOut = Schemas["MeOut"];
|
|
16
|
+
export type UserGrantOut = Schemas["UserGrantOut"];
|
|
12
17
|
export { ApiError } from "./errors.js";
|
|
13
18
|
export interface ClientOptions {
|
|
14
19
|
baseUrl?: string;
|
|
@@ -17,21 +22,28 @@ export interface ClientOptions {
|
|
|
17
22
|
export declare class GrimoireClient {
|
|
18
23
|
private readonly baseUrl;
|
|
19
24
|
private readonly machineToken?;
|
|
20
|
-
private
|
|
25
|
+
private cachedAccessToken?;
|
|
21
26
|
private refreshInFlight?;
|
|
22
27
|
constructor(opts?: ClientOptions);
|
|
23
28
|
private bearer;
|
|
24
|
-
private
|
|
29
|
+
private refreshAccessToken;
|
|
25
30
|
refreshSession(): Promise<void>;
|
|
26
31
|
private send;
|
|
27
32
|
private request;
|
|
28
33
|
private parseResponse;
|
|
29
34
|
search(input: SearchRequest): Promise<SearchResponse>;
|
|
30
|
-
listSources(
|
|
31
|
-
listVersions(
|
|
32
|
-
|
|
33
|
-
reportResult(
|
|
34
|
-
submitSource(body:
|
|
35
|
-
getJob(jobId: string): Promise<
|
|
35
|
+
listSources(): Promise<SourceOut[]>;
|
|
36
|
+
listVersions(product: string): Promise<VersionsOut>;
|
|
37
|
+
getDoc(pointId: string, window?: number): Promise<DocWindowOut>;
|
|
38
|
+
reportResult(pointId: string, verdict: ReportIn["verdict"], note?: string): Promise<ReportOut>;
|
|
39
|
+
submitSource(body: SubmissionIn): Promise<SubmitAccepted>;
|
|
40
|
+
getJob(jobId: string): Promise<JobOut>;
|
|
41
|
+
me(): Promise<MeOut>;
|
|
42
|
+
reviewQueue(): Promise<JobOut[]>;
|
|
43
|
+
approveJob(jobId: string): Promise<JobOut>;
|
|
44
|
+
listUsers(): Promise<UserGrantOut[]>;
|
|
45
|
+
grantUser(subject: string, name: string): Promise<UserGrantOut>;
|
|
46
|
+
revokeUser(subject: string): Promise<UserGrantOut>;
|
|
47
|
+
rejectJob(jobId: string, reason: string): Promise<JobOut>;
|
|
36
48
|
}
|
|
37
49
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { readMachineToken,
|
|
1
|
+
import { gotrueToken, readMachineToken, readSession, storeSession } from "./auth.js";
|
|
2
2
|
import { loadGlobalConfig } from "./config.js";
|
|
3
3
|
import { ApiError } from "./errors.js";
|
|
4
4
|
import { fetchWithTimeout } from "./http.js";
|
|
5
5
|
export { ApiError } from "./errors.js";
|
|
6
|
+
const CODES = {
|
|
7
|
+
401: "unauthorized",
|
|
8
|
+
403: "forbidden",
|
|
9
|
+
404: "not_found",
|
|
10
|
+
422: "invalid_request",
|
|
11
|
+
429: "quota_exceeded",
|
|
12
|
+
};
|
|
6
13
|
export class GrimoireClient {
|
|
7
14
|
baseUrl;
|
|
8
15
|
machineToken;
|
|
9
|
-
|
|
16
|
+
cachedAccessToken;
|
|
10
17
|
refreshInFlight;
|
|
11
18
|
constructor(opts = {}) {
|
|
12
19
|
this.baseUrl = (opts.baseUrl ?? loadGlobalConfig().apiBaseUrl).replace(/\/+$/, "");
|
|
@@ -15,39 +22,37 @@ export class GrimoireClient {
|
|
|
15
22
|
async bearer() {
|
|
16
23
|
if (this.machineToken)
|
|
17
24
|
return this.machineToken;
|
|
18
|
-
if (this.
|
|
19
|
-
return this.
|
|
25
|
+
if (this.cachedAccessToken && this.cachedAccessToken.expiresAt > Date.now() + 60_000) {
|
|
26
|
+
return this.cachedAccessToken.token;
|
|
20
27
|
}
|
|
21
|
-
this.refreshInFlight ??= this.
|
|
28
|
+
this.refreshInFlight ??= this.refreshAccessToken().finally(() => {
|
|
22
29
|
this.refreshInFlight = undefined;
|
|
23
30
|
});
|
|
24
31
|
return this.refreshInFlight;
|
|
25
32
|
}
|
|
26
|
-
// Exchange the
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
// Exchange the stored refresh token for a fresh access token. GoTrue rotates
|
|
34
|
+
// refresh tokens, so the new one replaces the stored one every time.
|
|
35
|
+
async refreshAccessToken() {
|
|
36
|
+
const session = readSession();
|
|
37
|
+
if (!session)
|
|
30
38
|
throw new ApiError(401, "not_logged_in", "Run `grimoire login`");
|
|
31
|
-
|
|
32
|
-
method: "POST",
|
|
33
|
-
headers: { "Content-Type": "application/json" },
|
|
34
|
-
body: JSON.stringify({ refresh_token: refresh }),
|
|
35
|
-
});
|
|
36
|
-
if (!res.ok)
|
|
37
|
-
throw new ApiError(res.status, "refresh_failed", "Run `grimoire login`");
|
|
38
|
-
let payload;
|
|
39
|
+
let tokens;
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
throw new ApiError(res.status, "refresh_failed", "Malformed refresh response");
|
|
41
|
+
tokens = await gotrueToken(session.supabase_url, session.supabase_anon_key, "refresh_token", {
|
|
42
|
+
refresh_token: session.refresh_token,
|
|
43
|
+
});
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof ApiError)
|
|
47
|
+
throw new ApiError(err.status, "refresh_failed", "Run `grimoire login`");
|
|
48
|
+
throw err;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
storeSession({ ...session, refresh_token: tokens.refresh_token });
|
|
51
|
+
this.cachedAccessToken = {
|
|
52
|
+
token: tokens.access_token,
|
|
53
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
54
|
+
};
|
|
55
|
+
return tokens.access_token;
|
|
51
56
|
}
|
|
52
57
|
async refreshSession() {
|
|
53
58
|
await this.bearer();
|
|
@@ -63,14 +68,15 @@ export class GrimoireClient {
|
|
|
63
68
|
}
|
|
64
69
|
async request(path, init = {}, auth = true) {
|
|
65
70
|
let res = await this.send(path, init, auth);
|
|
66
|
-
// A 401 despite a locally-unexpired
|
|
67
|
-
// force one refresh and retry exactly once.
|
|
71
|
+
// A 401 despite a locally-unexpired access token means it was revoked
|
|
72
|
+
// server-side: force one refresh and retry exactly once.
|
|
68
73
|
if (res.status === 401 && auth && !this.machineToken) {
|
|
69
|
-
this.
|
|
74
|
+
this.cachedAccessToken = undefined;
|
|
70
75
|
res = await this.send(path, init, auth);
|
|
71
76
|
}
|
|
72
77
|
return this.parseResponse(res);
|
|
73
78
|
}
|
|
79
|
+
// FastAPI errors carry {"detail": "..."} (string) or {"detail": [...]} (422).
|
|
74
80
|
async parseResponse(res) {
|
|
75
81
|
const isJson = res.headers.get("content-type")?.includes("json") ?? false;
|
|
76
82
|
const text = await res.text();
|
|
@@ -86,35 +92,28 @@ export class GrimoireClient {
|
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
if (!res.ok) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
: "error";
|
|
92
|
-
throw new ApiError(res.status, code, parsed ? body : text);
|
|
95
|
+
const detail = parsed && typeof body === "object" && body !== null ? body.detail : undefined;
|
|
96
|
+
throw new ApiError(res.status, CODES[res.status] ?? "error", detail ?? (parsed ? body : text));
|
|
93
97
|
}
|
|
94
98
|
if (isJson)
|
|
95
99
|
return (parsed ? body : {});
|
|
96
100
|
return text;
|
|
97
101
|
}
|
|
98
102
|
search(input) {
|
|
99
|
-
return this.request("/v1/search", {
|
|
100
|
-
method: "POST",
|
|
101
|
-
body: JSON.stringify(input),
|
|
102
|
-
});
|
|
103
|
+
return this.request("/v1/search", { method: "POST", body: JSON.stringify(input) });
|
|
103
104
|
}
|
|
104
|
-
listSources(
|
|
105
|
-
return this.request(
|
|
105
|
+
listSources() {
|
|
106
|
+
return this.request("/v1/sources");
|
|
106
107
|
}
|
|
107
|
-
listVersions(
|
|
108
|
-
return this.request(`/v1/sources/${encodeURIComponent(
|
|
108
|
+
listVersions(product) {
|
|
109
|
+
return this.request(`/v1/sources/${encodeURIComponent(product)}/versions`);
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
return this.request(`/v1/
|
|
111
|
+
getDoc(pointId, window = 2) {
|
|
112
|
+
return this.request(`/v1/doc/${encodeURIComponent(pointId)}?window=${window}`);
|
|
112
113
|
}
|
|
113
|
-
reportResult(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
body: JSON.stringify({ chunk_id: chunkId, verdict, note }),
|
|
117
|
-
});
|
|
114
|
+
reportResult(pointId, verdict, note) {
|
|
115
|
+
const body = { point_id: pointId, verdict, ...(note ? { note } : {}) };
|
|
116
|
+
return this.request("/v1/report", { method: "POST", body: JSON.stringify(body) });
|
|
118
117
|
}
|
|
119
118
|
submitSource(body) {
|
|
120
119
|
return this.request("/v1/sources", { method: "POST", body: JSON.stringify(body) });
|
|
@@ -122,4 +121,31 @@ export class GrimoireClient {
|
|
|
122
121
|
getJob(jobId) {
|
|
123
122
|
return this.request(`/v1/jobs/${encodeURIComponent(jobId)}`);
|
|
124
123
|
}
|
|
124
|
+
me() {
|
|
125
|
+
return this.request("/v1/me");
|
|
126
|
+
}
|
|
127
|
+
reviewQueue() {
|
|
128
|
+
return this.request("/v1/staff/review-queue");
|
|
129
|
+
}
|
|
130
|
+
approveJob(jobId) {
|
|
131
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/approve`, { method: "POST" });
|
|
132
|
+
}
|
|
133
|
+
listUsers() {
|
|
134
|
+
return this.request("/v1/staff/users");
|
|
135
|
+
}
|
|
136
|
+
grantUser(subject, name) {
|
|
137
|
+
return this.request("/v1/staff/users", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
body: JSON.stringify({ subject, name }),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
revokeUser(subject) {
|
|
143
|
+
return this.request(`/v1/staff/users/${encodeURIComponent(subject)}`, { method: "DELETE" });
|
|
144
|
+
}
|
|
145
|
+
rejectJob(jobId, reason) {
|
|
146
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/reject`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
body: JSON.stringify({ reason }),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
125
151
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -5,15 +5,12 @@ export interface SourcePin {
|
|
|
5
5
|
export interface GlobalConfig {
|
|
6
6
|
apiBaseUrl: string;
|
|
7
7
|
defaultSources?: SourcePin[];
|
|
8
|
-
defaultLanguage?: string;
|
|
9
|
-
maxResponseTokens?: number;
|
|
10
8
|
updateCheckHours?: number;
|
|
11
9
|
}
|
|
12
10
|
export interface ProjectConfig {
|
|
13
11
|
sources?: SourcePin[];
|
|
14
|
-
language?: string;
|
|
15
12
|
}
|
|
16
|
-
export declare const DEFAULT_API_BASE = "https://grimoire
|
|
13
|
+
export declare const DEFAULT_API_BASE = "https://cli.grimoire.monadeo.com";
|
|
17
14
|
export declare function globalConfigPath(): string;
|
|
18
15
|
export declare function loadGlobalConfig(): GlobalConfig;
|
|
19
16
|
export declare function saveGlobalConfig(config: GlobalConfig): void;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC;IAC7B,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACvB;AAID,eAAO,MAAM,gBAAgB,qCAAqC,CAAC;AAEnE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAiBD,wBAAgB,gBAAgB,IAAI,YAAY,CAQ/C;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAI3D;AAKD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAU1F;AAID,wBAAgB,iBAAiB,CAAC,QAAQ,SAAgB,GAAG,aAAa,GAAG,SAAS,CAYrF;AAED,wBAAgB,qBAAqB,IAAI,SAAS,EAAE,CAEnD"}
|
package/dist/config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
// Bare origin — API paths are absolute (/v1
|
|
5
|
-
//
|
|
6
|
-
export const DEFAULT_API_BASE = "https://grimoire
|
|
4
|
+
// Bare origin — API paths are absolute (/v1/...), so the base must never carry a
|
|
5
|
+
// path prefix.
|
|
6
|
+
export const DEFAULT_API_BASE = "https://cli.grimoire.monadeo.com";
|
|
7
7
|
export function globalConfigPath() {
|
|
8
8
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "grimoire", "config.json");
|
|
9
9
|
}
|