@monadeo.com/grimoire-core 0.3.7 → 0.4.1

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 CHANGED
@@ -1,8 +1,28 @@
1
- export declare function storeRefreshToken(token: string): void;
2
- export declare function readRefreshToken(): string | undefined;
3
- export declare function clearRefreshToken(): void;
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 declare function browserLogin(apiBase: string, openBrowser: (url: string) => void): Promise<void>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAmBA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIrD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAWrD;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAaD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIrD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAOrD;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAWD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDrG"}
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(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "grimoire", "credentials.json");
16
+ return join(configDir(), "credentials.json");
13
17
  }
14
- export function storeRefreshToken(token) {
18
+ export function storeSession(session) {
15
19
  const path = credentialsPath();
16
20
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
17
- writeFileSync(path, JSON.stringify({ refresh_token: token }) + "\n", { mode: 0o600 });
21
+ writeFileSync(path, JSON.stringify(session) + "\n", { mode: 0o600 });
18
22
  }
19
- export function readRefreshToken() {
23
+ export function readSession() {
20
24
  try {
21
25
  const parsed = JSON.parse(readFileSync(credentialsPath(), "utf8"));
22
- return typeof parsed.refresh_token === "string" && parsed.refresh_token !== ""
23
- ? parsed.refresh_token
24
- : undefined;
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 clearRefreshToken() {
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(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "grimoire", "machine-token");
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
- // Browser login via the Grimoire auth broker (Firebase is not an OAuth AS): open
62
- // the hosted page with a PKCE challenge, receive the one-time code on a loopback
63
- // callback, exchange it for the Firebase refresh token. The random state binds the
64
- // callback to this login attempt; anything but GET /callback with that state gets
65
- // a 404 and the server keeps waiting.
66
- export async function browserLogin(apiBase, openBrowser) {
67
- // The broker lives at the bare origin (/auth/cli/*, no /v1 prefix): normalize
68
- // away trailing slashes so the URLs are always built against the origin.
69
- const broker = apiBase.replace(/\/+$/, "");
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
- const received = url.searchParams.get("code");
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
- // Land the user on the hosted branded page rather than raw loopback text.
85
- res.writeHead(302, { Location: `${broker}/auth/cli/success` }).end();
86
- clearTimeout(timer);
87
- server.close();
88
- resolve(received);
89
- });
90
- server.listen(0, "127.0.0.1", () => {
91
- const port = server.address().port;
92
- const redirect = `http://127.0.0.1:${port}/callback`;
93
- openBrowser(`${broker}/auth/cli/start?code_challenge=${challenge}&code_challenge_method=S256&state=${state}&redirect_uri=${encodeURIComponent(redirect)}`);
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
- const timer = setTimeout(() => {
96
- server.close();
97
- reject(new Error("Login timed out"));
98
- }, 300_000);
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 res = await fetchWithTimeout(`${broker}/auth/cli/exchange`, {
101
- method: "POST",
102
- headers: { "Content-Type": "application/json" },
103
- body: JSON.stringify({ code, code_verifier: verifier }),
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,20 @@
1
- import type { components, operations } from "./generated/schema.js";
2
- export type SearchResultChunk = components["schemas"]["Chunk"];
3
- export type SourceCard = components["schemas"]["SourceCard"];
4
- export type ContextChunk = components["schemas"]["ContextChunk"];
5
- export type SearchRequest = operations["search"]["requestBody"]["content"]["application/json"];
6
- export type SearchResponse = operations["search"]["responses"][200]["content"]["application/json"];
7
- export type ListSourcesResponse = operations["listSources"]["responses"][200]["content"]["application/json"];
8
- export type ListVersionsResponse = operations["listVersions"]["responses"][200]["content"]["application/json"];
9
- export type ContextResponse = operations["getDocumentContext"]["responses"][200]["content"]["application/json"];
10
- export type Job = components["schemas"]["Job"];
11
- export type SubmitSourceResponse = operations["submitSource"]["responses"][201]["content"]["application/json"];
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"];
17
+ export type TokenMinted = Schemas["TokenMinted"];
12
18
  export { ApiError } from "./errors.js";
13
19
  export interface ClientOptions {
14
20
  baseUrl?: string;
@@ -17,21 +23,30 @@ export interface ClientOptions {
17
23
  export declare class GrimoireClient {
18
24
  private readonly baseUrl;
19
25
  private readonly machineToken?;
20
- private cachedIdToken?;
26
+ private cachedAccessToken?;
21
27
  private refreshInFlight?;
22
28
  constructor(opts?: ClientOptions);
23
29
  private bearer;
24
- private refreshIdToken;
30
+ private refreshAccessToken;
25
31
  refreshSession(): Promise<void>;
26
32
  private send;
27
33
  private request;
28
34
  private parseResponse;
29
35
  search(input: SearchRequest): Promise<SearchResponse>;
30
- listSources(q?: string): Promise<ListSourcesResponse>;
31
- listVersions(sourceId: string): Promise<ListVersionsResponse>;
32
- getContext(chunkId: string, window?: number): Promise<ContextResponse>;
33
- reportResult(chunkId: string, verdict: string, note?: string): Promise<unknown>;
34
- submitSource(body: Record<string, unknown>): Promise<SubmitSourceResponse>;
35
- getJob(jobId: string): Promise<Job>;
36
+ listSources(): Promise<SourceOut[]>;
37
+ listVersions(product: string): Promise<VersionsOut>;
38
+ getDoc(pointId: string, window?: number): Promise<DocWindowOut>;
39
+ reportResult(pointId: string, verdict: ReportIn["verdict"], note?: string): Promise<ReportOut>;
40
+ submitSource(body: SubmissionIn): Promise<SubmitAccepted>;
41
+ getJob(jobId: string): Promise<JobOut>;
42
+ me(): Promise<MeOut>;
43
+ reviewQueue(): Promise<JobOut[]>;
44
+ approveJob(jobId: string): Promise<JobOut>;
45
+ listUsers(): Promise<UserGrantOut[]>;
46
+ grantUser(subject: string, name: string): Promise<UserGrantOut>;
47
+ revokeUser(subject: string): Promise<UserGrantOut>;
48
+ mintToken(name: string, quotaPerDay: number): Promise<TokenMinted>;
49
+ recrawlSource(sourceId: string): Promise<JobOut>;
50
+ rejectJob(jobId: string, reason: string): Promise<JobOut>;
36
51
  }
37
52
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEpE,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC;AAC/D,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;AAC7D,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,CAAC;AACjE,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC;AAC/F,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC;AACnG,MAAM,MAAM,mBAAmB,GAC7B,UAAU,CAAC,aAAa,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC;AAC7E,MAAM,MAAM,oBAAoB,GAC9B,UAAU,CAAC,cAAc,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC;AAC9E,MAAM,MAAM,eAAe,GACzB,UAAU,CAAC,oBAAoB,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC;AAEpF,MAAM,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;AAC/C,MAAM,MAAM,oBAAoB,GAC9B,UAAU,CAAC,cAAc,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,kBAAkB,CAAC,CAAC;AAE9E,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IACvC,OAAO,CAAC,aAAa,CAAC,CAAuC;IAC7D,OAAO,CAAC,eAAe,CAAC,CAAkB;gBAE9B,IAAI,GAAE,aAAkB;YAKtB,MAAM;YAYN,cAAc;IAuBtB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;YAIvB,IAAI;YASJ,OAAO;YAWP,aAAa;IAwB3B,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAOrD,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAI7D,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,OAAO,CAAC,eAAe,CAAC;IAIjE,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO/E,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAI1E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;CAGpC"}
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, readRefreshToken } from "./auth.js";
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
- cachedIdToken;
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.cachedIdToken && this.cachedIdToken.expiresAt > Date.now() + 60_000) {
19
- return this.cachedIdToken.token;
25
+ if (this.cachedAccessToken && this.cachedAccessToken.expiresAt > Date.now() + 60_000) {
26
+ return this.cachedAccessToken.token;
20
27
  }
21
- this.refreshInFlight ??= this.refreshIdToken().finally(() => {
28
+ this.refreshInFlight ??= this.refreshAccessToken().finally(() => {
22
29
  this.refreshInFlight = undefined;
23
30
  });
24
31
  return this.refreshInFlight;
25
32
  }
26
- // Exchange the Firebase refresh token for a fresh ID token (silent refresh).
27
- async refreshIdToken() {
28
- const refresh = readRefreshToken();
29
- if (!refresh)
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
- const res = await fetchWithTimeout(`${this.baseUrl}/auth/cli/refresh`, {
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
- payload = await res.json();
41
- }
42
- catch {
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
- const { id_token, expires_in } = payload;
46
- if (typeof id_token !== "string" || typeof expires_in !== "number") {
47
- throw new ApiError(res.status, "refresh_failed", "Malformed refresh response");
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
- this.cachedIdToken = { token: id_token, expiresAt: Date.now() + expires_in * 1000 };
50
- return id_token;
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 ID token means it was revoked server-side:
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.cachedIdToken = undefined;
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 code = parsed && typeof body === "object" && body !== null
90
- ? (body.error ?? "error")
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(q) {
105
- return this.request(`/v1/sources${q ? `?q=${encodeURIComponent(q)}` : ""}`);
105
+ listSources() {
106
+ return this.request("/v1/sources");
106
107
  }
107
- listVersions(sourceId) {
108
- return this.request(`/v1/sources/${encodeURIComponent(sourceId)}/versions`);
108
+ listVersions(product) {
109
+ return this.request(`/v1/sources/${encodeURIComponent(product)}/versions`);
109
110
  }
110
- getContext(chunkId, window = 2) {
111
- return this.request(`/v1/documents/${encodeURIComponent(chunkId)}/context?window=${window}`);
111
+ getDoc(pointId, window = 2) {
112
+ return this.request(`/v1/doc/${encodeURIComponent(pointId)}?window=${window}`);
112
113
  }
113
- reportResult(chunkId, verdict, note) {
114
- return this.request("/v1/feedback", {
115
- method: "POST",
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,40 @@ 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
+ mintToken(name, quotaPerDay) {
146
+ return this.request("/v1/staff/tokens", {
147
+ method: "POST",
148
+ body: JSON.stringify({ name, quota_per_day: quotaPerDay }),
149
+ });
150
+ }
151
+ recrawlSource(sourceId) {
152
+ return this.request(`/v1/staff/sources/${encodeURIComponent(sourceId)}/recrawl`, { method: "POST" });
153
+ }
154
+ rejectJob(jobId, reason) {
155
+ return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/reject`, {
156
+ method: "POST",
157
+ body: JSON.stringify({ reason }),
158
+ });
159
+ }
125
160
  }
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-api.monadeo.com";
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;
@@ -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,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;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"}
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/..., /auth/cli/...), so the base must
5
- // never carry a path prefix or auth-broker URLs would be built wrong.
6
- export const DEFAULT_API_BASE = "https://grimoire-api.monadeo.com";
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
  }