granola-toolkit 0.22.0 → 0.23.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 +20 -1
- package/dist/cli.js +605 -131
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,12 +193,17 @@ The machine-readable `export` command includes:
|
|
|
193
193
|
The initial server API includes:
|
|
194
194
|
|
|
195
195
|
- `GET /health`
|
|
196
|
+
- `GET /auth/status`
|
|
196
197
|
- `GET /state`
|
|
197
198
|
- `GET /events` for server-sent state updates
|
|
198
199
|
- `GET /meetings`
|
|
199
200
|
- `GET /meetings/resolve?q=<query>`
|
|
200
201
|
- `GET /meetings/:id`
|
|
201
202
|
- `GET /exports/jobs`
|
|
203
|
+
- `POST /auth/login`
|
|
204
|
+
- `POST /auth/logout`
|
|
205
|
+
- `POST /auth/mode`
|
|
206
|
+
- `POST /auth/refresh`
|
|
202
207
|
- `POST /exports/notes`
|
|
203
208
|
- `POST /exports/jobs/:id/rerun`
|
|
204
209
|
- `POST /exports/transcripts`
|
|
@@ -217,6 +222,7 @@ The initial browser client includes:
|
|
|
217
222
|
- a focused meeting workspace with notes, transcript, metadata, and raw tabs
|
|
218
223
|
- keyboard-first workspace switching with `1`-`4`, `[` and `]`
|
|
219
224
|
- app-state status from the shared core
|
|
225
|
+
- an auth session panel for login, refresh, source switching, and sign-out
|
|
220
226
|
- note and transcript export actions backed by the same local API
|
|
221
227
|
- a recent export-jobs panel with rerun actions
|
|
222
228
|
- stronger empty and error states for list/detail failures
|
|
@@ -239,9 +245,22 @@ If you do not want to keep passing `--supabase`, import the desktop app session
|
|
|
239
245
|
```bash
|
|
240
246
|
granola auth login
|
|
241
247
|
granola auth status
|
|
248
|
+
granola auth refresh
|
|
249
|
+
granola auth use stored
|
|
250
|
+
granola auth use supabase
|
|
242
251
|
```
|
|
243
252
|
|
|
244
|
-
That stores a reusable Granola session locally and lets `granola notes` use it directly.
|
|
253
|
+
That stores a reusable Granola session locally and lets `granola notes` use it directly.
|
|
254
|
+
|
|
255
|
+
`granola auth` now supports:
|
|
256
|
+
|
|
257
|
+
- `login` to import the desktop app session into the toolkit store
|
|
258
|
+
- `status` to inspect the active source, stored-session availability, refresh support, and any last auth error
|
|
259
|
+
- `refresh` to refresh the stored session explicitly
|
|
260
|
+
- `use stored` or `use supabase` to switch the active auth source
|
|
261
|
+
- `logout` to delete the stored session
|
|
262
|
+
|
|
263
|
+
The same auth actions are also available from the web workspace.
|
|
245
264
|
|
|
246
265
|
### Incremental Writes
|
|
247
266
|
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
3
|
import { mkdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
4
|
import { homedir, platform } from "node:os";
|
|
6
5
|
import { dirname, join } from "node:path";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
6
|
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
//#region src/utils.ts
|
|
@@ -153,6 +153,60 @@ function transcriptSpeakerLabel(segment) {
|
|
|
153
153
|
return segment.source === "microphone" ? "You" : "System";
|
|
154
154
|
}
|
|
155
155
|
//#endregion
|
|
156
|
+
//#region src/cache.ts
|
|
157
|
+
function parseCacheDocument(id, value) {
|
|
158
|
+
const record = asRecord(value);
|
|
159
|
+
if (!record) return;
|
|
160
|
+
return {
|
|
161
|
+
createdAt: stringValue(record.created_at),
|
|
162
|
+
id,
|
|
163
|
+
title: stringValue(record.title),
|
|
164
|
+
updatedAt: stringValue(record.updated_at)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseTranscriptSegments(value) {
|
|
168
|
+
if (!Array.isArray(value)) return;
|
|
169
|
+
return value.flatMap((segment) => {
|
|
170
|
+
const record = asRecord(segment);
|
|
171
|
+
if (!record) return [];
|
|
172
|
+
return [{
|
|
173
|
+
documentId: stringValue(record.document_id),
|
|
174
|
+
endTimestamp: stringValue(record.end_timestamp),
|
|
175
|
+
id: stringValue(record.id),
|
|
176
|
+
isFinal: Boolean(record.is_final),
|
|
177
|
+
source: stringValue(record.source),
|
|
178
|
+
startTimestamp: stringValue(record.start_timestamp),
|
|
179
|
+
text: stringValue(record.text)
|
|
180
|
+
}];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function parseCacheContents(contents) {
|
|
184
|
+
const outer = parseJsonString(contents);
|
|
185
|
+
if (!outer) throw new Error("failed to parse cache JSON");
|
|
186
|
+
const rawCache = outer.cache;
|
|
187
|
+
let cachePayload;
|
|
188
|
+
if (typeof rawCache === "string") cachePayload = parseJsonString(rawCache);
|
|
189
|
+
else cachePayload = asRecord(rawCache);
|
|
190
|
+
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
191
|
+
if (!state) throw new Error("failed to parse cache state");
|
|
192
|
+
const rawDocuments = asRecord(state.documents) ?? {};
|
|
193
|
+
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
194
|
+
const documents = {};
|
|
195
|
+
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
196
|
+
const document = parseCacheDocument(id, rawDocument);
|
|
197
|
+
if (document) documents[id] = document;
|
|
198
|
+
}
|
|
199
|
+
const transcripts = {};
|
|
200
|
+
for (const [id, rawTranscript] of Object.entries(rawTranscripts)) {
|
|
201
|
+
const segments = parseTranscriptSegments(rawTranscript);
|
|
202
|
+
if (segments) transcripts[id] = segments;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
documents,
|
|
206
|
+
transcripts
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
156
210
|
//#region src/client/auth.ts
|
|
157
211
|
const execFileAsync$1 = promisify(execFile);
|
|
158
212
|
const DEFAULT_CLIENT_ID = "client_GranolaMac";
|
|
@@ -380,121 +434,126 @@ function createDefaultSessionStore() {
|
|
|
380
434
|
return platform() === "darwin" ? new KeychainSessionStore() : new FileSessionStore();
|
|
381
435
|
}
|
|
382
436
|
//#endregion
|
|
383
|
-
//#region src/
|
|
384
|
-
function
|
|
385
|
-
return
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
flags: { help: { type: "boolean" } },
|
|
403
|
-
help: authHelp,
|
|
404
|
-
name: "auth",
|
|
405
|
-
async run({ commandArgs, globalFlags }) {
|
|
406
|
-
const [action] = commandArgs;
|
|
407
|
-
switch (action) {
|
|
408
|
-
case "login": return await login(globalFlags.supabase);
|
|
409
|
-
case "logout": return await logout();
|
|
410
|
-
case "status": return await status();
|
|
411
|
-
case void 0:
|
|
412
|
-
console.log(authHelp());
|
|
413
|
-
return 1;
|
|
414
|
-
default: throw new Error("invalid auth command: expected login, status, or logout");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
async function login(supabaseFlag) {
|
|
419
|
-
const supabasePath = typeof supabaseFlag === "string" && supabaseFlag.trim() || firstExistingPath(granolaSupabaseCandidates());
|
|
420
|
-
if (!supabasePath) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
421
|
-
if (!existsSync(supabasePath)) throw new Error(`supabase.json not found: ${supabasePath}`);
|
|
422
|
-
const sessionStore = createDefaultSessionStore();
|
|
423
|
-
const session = await new SupabaseFileSessionSource(supabasePath).loadSession();
|
|
424
|
-
await sessionStore.writeSession(session);
|
|
425
|
-
console.log(`Imported Granola session from ${supabasePath}`);
|
|
426
|
-
return 0;
|
|
427
|
-
}
|
|
428
|
-
async function status() {
|
|
429
|
-
const session = await createDefaultSessionStore().readSession();
|
|
430
|
-
if (!session) {
|
|
431
|
-
console.log("No stored Granola session");
|
|
432
|
-
return 1;
|
|
433
|
-
}
|
|
434
|
-
console.log("Stored Granola session");
|
|
435
|
-
console.log(`Client ID: ${session.clientId}`);
|
|
436
|
-
console.log(`Refresh token: ${session.refreshToken ? "available" : "missing"}`);
|
|
437
|
-
console.log(`Sign-in method: ${session.signInMethod ?? "unknown"}`);
|
|
438
|
-
return 0;
|
|
439
|
-
}
|
|
440
|
-
async function logout() {
|
|
441
|
-
await createDefaultSessionStore().clearSession();
|
|
442
|
-
console.log("Stored Granola session deleted");
|
|
443
|
-
return 0;
|
|
444
|
-
}
|
|
445
|
-
//#endregion
|
|
446
|
-
//#region src/cache.ts
|
|
447
|
-
function parseCacheDocument(id, value) {
|
|
448
|
-
const record = asRecord(value);
|
|
449
|
-
if (!record) return;
|
|
437
|
+
//#region src/client/default-auth.ts
|
|
438
|
+
function hasStoredSession(session) {
|
|
439
|
+
return Boolean(session?.accessToken.trim());
|
|
440
|
+
}
|
|
441
|
+
function resolveActiveMode(options) {
|
|
442
|
+
if (options.preferredMode === "stored-session" && options.storedSessionAvailable) return "stored-session";
|
|
443
|
+
if (options.preferredMode === "supabase-file" && options.supabaseAvailable) return "supabase-file";
|
|
444
|
+
if (options.storedSessionAvailable) return "stored-session";
|
|
445
|
+
return "supabase-file";
|
|
446
|
+
}
|
|
447
|
+
function missingSupabaseError() {
|
|
448
|
+
return /* @__PURE__ */ new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
function buildDefaultGranolaAuthInfo(config, options = {}) {
|
|
451
|
+
const existsSyncImpl = options.existsSyncImpl ?? existsSync;
|
|
452
|
+
const session = options.session;
|
|
453
|
+
const storedSessionAvailable = hasStoredSession(session);
|
|
454
|
+
const supabasePath = config.supabase || void 0;
|
|
455
|
+
const supabaseAvailable = Boolean(supabasePath && existsSyncImpl(supabasePath));
|
|
450
456
|
return {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
457
|
+
clientId: session?.clientId,
|
|
458
|
+
lastError: options.lastError,
|
|
459
|
+
mode: resolveActiveMode({
|
|
460
|
+
preferredMode: options.preferredMode,
|
|
461
|
+
storedSessionAvailable,
|
|
462
|
+
supabaseAvailable
|
|
463
|
+
}),
|
|
464
|
+
refreshAvailable: Boolean(session?.refreshToken?.trim()),
|
|
465
|
+
signInMethod: session?.signInMethod,
|
|
466
|
+
storedSessionAvailable,
|
|
467
|
+
supabaseAvailable,
|
|
468
|
+
supabasePath
|
|
455
469
|
};
|
|
456
470
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
id: stringValue(record.id),
|
|
466
|
-
isFinal: Boolean(record.is_final),
|
|
467
|
-
source: stringValue(record.source),
|
|
468
|
-
startTimestamp: stringValue(record.start_timestamp),
|
|
469
|
-
text: stringValue(record.text)
|
|
470
|
-
}];
|
|
471
|
+
async function inspectDefaultGranolaAuth(config, options = {}) {
|
|
472
|
+
const sessionStore = options.sessionStore ?? createDefaultSessionStore();
|
|
473
|
+
const session = options.session ?? await sessionStore.readSession();
|
|
474
|
+
return buildDefaultGranolaAuthInfo(config, {
|
|
475
|
+
existsSyncImpl: options.existsSyncImpl,
|
|
476
|
+
lastError: options.lastError,
|
|
477
|
+
preferredMode: options.preferredMode,
|
|
478
|
+
session
|
|
471
479
|
});
|
|
472
480
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
else cachePayload = asRecord(rawCache);
|
|
480
|
-
const state = cachePayload ? asRecord(cachePayload.state) : void 0;
|
|
481
|
-
if (!state) throw new Error("failed to parse cache state");
|
|
482
|
-
const rawDocuments = asRecord(state.documents) ?? {};
|
|
483
|
-
const rawTranscripts = asRecord(state.transcripts) ?? {};
|
|
484
|
-
const documents = {};
|
|
485
|
-
for (const [id, rawDocument] of Object.entries(rawDocuments)) {
|
|
486
|
-
const document = parseCacheDocument(id, rawDocument);
|
|
487
|
-
if (document) documents[id] = document;
|
|
481
|
+
var DefaultAuthController = class {
|
|
482
|
+
#lastError;
|
|
483
|
+
#preferredMode;
|
|
484
|
+
constructor(config, options = {}) {
|
|
485
|
+
this.config = config;
|
|
486
|
+
this.options = options;
|
|
488
487
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const segments = parseTranscriptSegments(rawTranscript);
|
|
492
|
-
if (segments) transcripts[id] = segments;
|
|
488
|
+
sessionStore() {
|
|
489
|
+
return this.options.sessionStore ?? createDefaultSessionStore();
|
|
493
490
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
491
|
+
readSession() {
|
|
492
|
+
return this.sessionStore().readSession();
|
|
493
|
+
}
|
|
494
|
+
resolveSupabasePath(overridePath) {
|
|
495
|
+
const supabasePath = overridePath?.trim() || this.config.supabase || "";
|
|
496
|
+
if (!supabasePath) throw missingSupabaseError();
|
|
497
|
+
if (!(this.options.existsSyncImpl ?? existsSync)(supabasePath)) throw new Error(`supabase.json not found: ${supabasePath}`);
|
|
498
|
+
return supabasePath;
|
|
499
|
+
}
|
|
500
|
+
sessionSource(supabasePath) {
|
|
501
|
+
return this.options.sessionSourceFactory?.(supabasePath) ?? new SupabaseFileSessionSource(supabasePath);
|
|
502
|
+
}
|
|
503
|
+
async inspect() {
|
|
504
|
+
const session = await this.readSession();
|
|
505
|
+
return buildDefaultGranolaAuthInfo(this.config, {
|
|
506
|
+
existsSyncImpl: this.options.existsSyncImpl,
|
|
507
|
+
lastError: this.#lastError,
|
|
508
|
+
preferredMode: this.#preferredMode,
|
|
509
|
+
session
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
async login(options = {}) {
|
|
513
|
+
const supabasePath = this.resolveSupabasePath(options.supabasePath);
|
|
514
|
+
const session = await this.sessionSource(supabasePath).loadSession();
|
|
515
|
+
await this.sessionStore().writeSession(session);
|
|
516
|
+
this.#lastError = void 0;
|
|
517
|
+
this.#preferredMode = "stored-session";
|
|
518
|
+
return await this.inspect();
|
|
519
|
+
}
|
|
520
|
+
async logout() {
|
|
521
|
+
await this.sessionStore().clearSession();
|
|
522
|
+
this.#lastError = void 0;
|
|
523
|
+
this.#preferredMode = void 0;
|
|
524
|
+
return await this.inspect();
|
|
525
|
+
}
|
|
526
|
+
async refresh() {
|
|
527
|
+
const session = await this.readSession();
|
|
528
|
+
if (!hasStoredSession(session)) {
|
|
529
|
+
this.#lastError = "no stored Granola session found";
|
|
530
|
+
throw new Error(this.#lastError);
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const refreshed = await refreshGranolaSession(session, this.options.fetchImpl);
|
|
534
|
+
await this.sessionStore().writeSession(refreshed);
|
|
535
|
+
this.#lastError = void 0;
|
|
536
|
+
this.#preferredMode = "stored-session";
|
|
537
|
+
return await this.inspect();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async switchMode(mode) {
|
|
544
|
+
const state = await this.inspect();
|
|
545
|
+
if (mode === "stored-session" && !state.storedSessionAvailable) {
|
|
546
|
+
this.#lastError = "no stored Granola session found";
|
|
547
|
+
throw new Error(this.#lastError);
|
|
548
|
+
}
|
|
549
|
+
if (mode === "supabase-file") this.resolveSupabasePath();
|
|
550
|
+
this.#lastError = void 0;
|
|
551
|
+
this.#preferredMode = mode;
|
|
552
|
+
return await this.inspect();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
function createDefaultGranolaAuthController(config, options = {}) {
|
|
556
|
+
return new DefaultAuthController(config, options);
|
|
498
557
|
}
|
|
499
558
|
//#endregion
|
|
500
559
|
//#region src/client/parsers.ts
|
|
@@ -690,26 +749,16 @@ var AuthenticatedHttpClient = class {
|
|
|
690
749
|
};
|
|
691
750
|
//#endregion
|
|
692
751
|
//#region src/client/default.ts
|
|
693
|
-
async function
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
mode: hasStoredSession ? "stored-session" : "supabase-file",
|
|
698
|
-
storedSessionAvailable: hasStoredSession,
|
|
699
|
-
supabasePath: config.supabase || void 0
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
async function createDefaultGranolaRuntime(config, logger = console) {
|
|
752
|
+
async function createDefaultGranolaRuntime(config, logger = console, options = {}) {
|
|
753
|
+
const auth = await inspectDefaultGranolaAuth(config, { preferredMode: options.preferredMode });
|
|
754
|
+
if (!auth.storedSessionAvailable && !config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
755
|
+
if (!auth.storedSessionAvailable && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
|
|
703
756
|
const sessionStore = createDefaultSessionStore();
|
|
704
|
-
const auth = await inspectDefaultGranolaAuth(config);
|
|
705
|
-
const hasStoredSession = auth.storedSessionAvailable;
|
|
706
|
-
if (!hasStoredSession && !config.supabase) throw new Error(`supabase.json not found. Pass --supabase or create .granola.toml. Expected locations include: ${granolaSupabaseCandidates().join(", ")}`);
|
|
707
|
-
if (!hasStoredSession && config.supabase && !existsSync(config.supabase)) throw new Error(`supabase.json not found: ${config.supabase}`);
|
|
708
757
|
return {
|
|
709
758
|
auth,
|
|
710
759
|
client: new GranolaApiClient(new AuthenticatedHttpClient({
|
|
711
760
|
logger,
|
|
712
|
-
tokenProvider:
|
|
761
|
+
tokenProvider: auth.mode === "stored-session" ? new StoredSessionTokenProvider(sessionStore, { source: config.supabase && existsSync(config.supabase) ? new SupabaseFileSessionSource(config.supabase) : void 0 }) : new CachedTokenProvider(new SupabaseFileTokenSource(config.supabase), new NoopTokenStore())
|
|
713
762
|
}))
|
|
714
763
|
};
|
|
715
764
|
}
|
|
@@ -1675,6 +1724,24 @@ var GranolaApp = class {
|
|
|
1675
1724
|
this.emitStateUpdate();
|
|
1676
1725
|
return this.getState();
|
|
1677
1726
|
}
|
|
1727
|
+
resetDocumentsState() {
|
|
1728
|
+
this.#granolaClient = void 0;
|
|
1729
|
+
this.#documents = void 0;
|
|
1730
|
+
this.#state.documents = {
|
|
1731
|
+
count: 0,
|
|
1732
|
+
loaded: false
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
applyAuthState(auth, options = {}) {
|
|
1736
|
+
if (options.resetDocuments) this.resetDocumentsState();
|
|
1737
|
+
this.#state.auth = { ...auth };
|
|
1738
|
+
if (options.view) this.#state.ui = {
|
|
1739
|
+
...this.#state.ui,
|
|
1740
|
+
view: options.view
|
|
1741
|
+
};
|
|
1742
|
+
this.emitStateUpdate();
|
|
1743
|
+
return { ...auth };
|
|
1744
|
+
}
|
|
1678
1745
|
nowIso() {
|
|
1679
1746
|
return (this.deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
1680
1747
|
}
|
|
@@ -1693,10 +1760,9 @@ var GranolaApp = class {
|
|
|
1693
1760
|
return this.#granolaClient;
|
|
1694
1761
|
}
|
|
1695
1762
|
if (!this.deps.createGranolaClient) throw new Error("Granola API client is not configured");
|
|
1696
|
-
const runtime = await this.deps.createGranolaClient();
|
|
1763
|
+
const runtime = await this.deps.createGranolaClient(this.#state.auth.mode);
|
|
1697
1764
|
this.#granolaClient = runtime.client;
|
|
1698
|
-
this
|
|
1699
|
-
this.emitStateUpdate();
|
|
1765
|
+
this.applyAuthState(runtime.auth);
|
|
1700
1766
|
return this.#granolaClient;
|
|
1701
1767
|
}
|
|
1702
1768
|
missingCacheError() {
|
|
@@ -1751,6 +1817,64 @@ var GranolaApp = class {
|
|
|
1751
1817
|
written: patch.written
|
|
1752
1818
|
});
|
|
1753
1819
|
}
|
|
1820
|
+
requireAuthController() {
|
|
1821
|
+
if (!this.deps.authController) throw new Error("Granola auth control is not configured");
|
|
1822
|
+
return this.deps.authController;
|
|
1823
|
+
}
|
|
1824
|
+
async inspectAuth() {
|
|
1825
|
+
if (!this.deps.authController) return { ...this.#state.auth };
|
|
1826
|
+
const auth = await this.deps.authController.inspect();
|
|
1827
|
+
return this.applyAuthState(auth, { view: "auth" });
|
|
1828
|
+
}
|
|
1829
|
+
async loginAuth(options = {}) {
|
|
1830
|
+
const controller = this.requireAuthController();
|
|
1831
|
+
try {
|
|
1832
|
+
const auth = await controller.login(options);
|
|
1833
|
+
return this.applyAuthState(auth, {
|
|
1834
|
+
resetDocuments: true,
|
|
1835
|
+
view: "auth"
|
|
1836
|
+
});
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
const auth = await controller.inspect();
|
|
1839
|
+
this.applyAuthState(auth, { view: "auth" });
|
|
1840
|
+
throw error;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async logoutAuth() {
|
|
1844
|
+
const auth = await this.requireAuthController().logout();
|
|
1845
|
+
return this.applyAuthState(auth, {
|
|
1846
|
+
resetDocuments: true,
|
|
1847
|
+
view: "auth"
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
async refreshAuth() {
|
|
1851
|
+
const controller = this.requireAuthController();
|
|
1852
|
+
try {
|
|
1853
|
+
const auth = await controller.refresh();
|
|
1854
|
+
return this.applyAuthState(auth, {
|
|
1855
|
+
resetDocuments: true,
|
|
1856
|
+
view: "auth"
|
|
1857
|
+
});
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
const auth = await controller.inspect();
|
|
1860
|
+
this.applyAuthState(auth, { view: "auth" });
|
|
1861
|
+
throw error;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async switchAuthMode(mode) {
|
|
1865
|
+
const controller = this.requireAuthController();
|
|
1866
|
+
try {
|
|
1867
|
+
const auth = await controller.switchMode(mode);
|
|
1868
|
+
return this.applyAuthState(auth, {
|
|
1869
|
+
resetDocuments: true,
|
|
1870
|
+
view: "auth"
|
|
1871
|
+
});
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
const auth = await controller.inspect();
|
|
1874
|
+
this.applyAuthState(auth, { view: "auth" });
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1754
1878
|
async listDocuments() {
|
|
1755
1879
|
if (this.#documents) return this.#documents;
|
|
1756
1880
|
const documents = await (await this.getGranolaClient()).listDocuments({ timeoutMs: this.config.notes.timeoutMs });
|
|
@@ -1950,11 +2074,13 @@ var GranolaApp = class {
|
|
|
1950
2074
|
};
|
|
1951
2075
|
async function createGranolaApp(config, options = {}) {
|
|
1952
2076
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
2077
|
+
const authController = createDefaultGranolaAuthController(config);
|
|
1953
2078
|
const exportJobStore = createDefaultExportJobStore();
|
|
1954
2079
|
return new GranolaApp(config, {
|
|
1955
2080
|
auth,
|
|
2081
|
+
authController,
|
|
1956
2082
|
cacheLoader: loadOptionalGranolaCache,
|
|
1957
|
-
createGranolaClient: async () => await createDefaultGranolaRuntime(config, options.logger),
|
|
2083
|
+
createGranolaClient: async (mode) => await createDefaultGranolaRuntime(config, options.logger, { preferredMode: mode }),
|
|
1958
2084
|
exportJobs: await exportJobStore.readJobs(),
|
|
1959
2085
|
exportJobStore,
|
|
1960
2086
|
now: options.now
|
|
@@ -2076,6 +2202,103 @@ async function waitForShutdown(close) {
|
|
|
2076
2202
|
});
|
|
2077
2203
|
}
|
|
2078
2204
|
//#endregion
|
|
2205
|
+
//#region src/commands/auth.ts
|
|
2206
|
+
function authHelp() {
|
|
2207
|
+
return `Granola auth
|
|
2208
|
+
|
|
2209
|
+
Usage:
|
|
2210
|
+
granola auth <login|status|logout|refresh|use> [options]
|
|
2211
|
+
|
|
2212
|
+
Subcommands:
|
|
2213
|
+
login Import credentials from the Granola desktop app
|
|
2214
|
+
status Show the current Granola auth state
|
|
2215
|
+
logout Delete the stored Granola session
|
|
2216
|
+
refresh Refresh the stored Granola session
|
|
2217
|
+
use <stored|supabase>
|
|
2218
|
+
Switch the active auth source for this toolkit instance
|
|
2219
|
+
|
|
2220
|
+
Options:
|
|
2221
|
+
--supabase <path> Path to supabase.json for auth login
|
|
2222
|
+
--config <path> Path to .granola.toml
|
|
2223
|
+
--debug Enable debug logging
|
|
2224
|
+
-h, --help Show help
|
|
2225
|
+
`;
|
|
2226
|
+
}
|
|
2227
|
+
function formatAuthSource(mode) {
|
|
2228
|
+
return mode === "stored-session" ? "stored session" : "supabase.json";
|
|
2229
|
+
}
|
|
2230
|
+
function printAuthState(state) {
|
|
2231
|
+
console.log(`Active source: ${formatAuthSource(state.mode)}`);
|
|
2232
|
+
console.log(`Stored session: ${state.storedSessionAvailable ? "available" : "missing"}`);
|
|
2233
|
+
console.log(`supabase.json: ${state.supabaseAvailable ? "available" : "missing"}`);
|
|
2234
|
+
if (state.supabasePath) console.log(`supabase path: ${state.supabasePath}`);
|
|
2235
|
+
if (state.clientId) console.log(`Client ID: ${state.clientId}`);
|
|
2236
|
+
console.log(`Refresh token: ${state.refreshAvailable ? "available" : "missing"}`);
|
|
2237
|
+
if (state.signInMethod) console.log(`Sign-in method: ${state.signInMethod}`);
|
|
2238
|
+
if (state.lastError) console.log(`Last error: ${state.lastError}`);
|
|
2239
|
+
}
|
|
2240
|
+
const authCommand = {
|
|
2241
|
+
description: "Manage stored Granola sessions",
|
|
2242
|
+
flags: { help: { type: "boolean" } },
|
|
2243
|
+
help: authHelp,
|
|
2244
|
+
name: "auth",
|
|
2245
|
+
async run({ commandArgs, globalFlags }) {
|
|
2246
|
+
const [action, value] = commandArgs;
|
|
2247
|
+
const config = await loadConfig({
|
|
2248
|
+
globalFlags,
|
|
2249
|
+
subcommandFlags: {}
|
|
2250
|
+
});
|
|
2251
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
2252
|
+
debug(config.debug, "supabase", config.supabase);
|
|
2253
|
+
const app = await createGranolaApp(config);
|
|
2254
|
+
switch (action) {
|
|
2255
|
+
case "login": {
|
|
2256
|
+
const state = await app.loginAuth();
|
|
2257
|
+
console.log(`Imported Granola session from ${state.supabasePath ?? "desktop app defaults"}`);
|
|
2258
|
+
printAuthState(state);
|
|
2259
|
+
return 0;
|
|
2260
|
+
}
|
|
2261
|
+
case "logout": {
|
|
2262
|
+
const state = await app.logoutAuth();
|
|
2263
|
+
console.log("Stored Granola session deleted");
|
|
2264
|
+
printAuthState(state);
|
|
2265
|
+
return 0;
|
|
2266
|
+
}
|
|
2267
|
+
case "refresh": {
|
|
2268
|
+
const state = await app.refreshAuth();
|
|
2269
|
+
console.log("Stored Granola session refreshed");
|
|
2270
|
+
printAuthState(state);
|
|
2271
|
+
return 0;
|
|
2272
|
+
}
|
|
2273
|
+
case "status": {
|
|
2274
|
+
const state = await app.inspectAuth();
|
|
2275
|
+
printAuthState(state);
|
|
2276
|
+
return state.storedSessionAvailable ? 0 : 1;
|
|
2277
|
+
}
|
|
2278
|
+
case "use": {
|
|
2279
|
+
const mode = resolveAuthMode(value);
|
|
2280
|
+
const state = await app.switchAuthMode(mode);
|
|
2281
|
+
console.log(`Switched auth source to ${formatAuthSource(state.mode)}`);
|
|
2282
|
+
printAuthState(state);
|
|
2283
|
+
return 0;
|
|
2284
|
+
}
|
|
2285
|
+
case void 0:
|
|
2286
|
+
console.log(authHelp());
|
|
2287
|
+
return 1;
|
|
2288
|
+
default: throw new Error("invalid auth command: expected login, status, logout, refresh, or use");
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
function resolveAuthMode(value) {
|
|
2293
|
+
switch (value) {
|
|
2294
|
+
case "stored":
|
|
2295
|
+
case "stored-session": return "stored-session";
|
|
2296
|
+
case "supabase":
|
|
2297
|
+
case "supabase-file": return "supabase-file";
|
|
2298
|
+
default: throw new Error("invalid auth mode: expected stored or supabase");
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
//#endregion
|
|
2079
2302
|
//#region src/commands/exports.ts
|
|
2080
2303
|
function exportsHelp() {
|
|
2081
2304
|
return `Granola exports
|
|
@@ -2460,6 +2683,7 @@ const state = {
|
|
|
2460
2683
|
|
|
2461
2684
|
const els = {
|
|
2462
2685
|
appState: document.querySelector("[data-app-state]"),
|
|
2686
|
+
authPanel: document.querySelector("[data-auth-panel]"),
|
|
2463
2687
|
detailBody: document.querySelector("[data-detail-body]"),
|
|
2464
2688
|
detailMeta: document.querySelector("[data-detail-meta]"),
|
|
2465
2689
|
empty: document.querySelector("[data-empty]"),
|
|
@@ -2526,6 +2750,7 @@ function renderWorkspaceTabs() {
|
|
|
2526
2750
|
function renderAppState() {
|
|
2527
2751
|
if (!state.appState) {
|
|
2528
2752
|
els.appState.innerHTML = "<p>Waiting for server state…</p>";
|
|
2753
|
+
els.authPanel.innerHTML = "<p>Waiting for auth state…</p>";
|
|
2529
2754
|
return;
|
|
2530
2755
|
}
|
|
2531
2756
|
|
|
@@ -2548,9 +2773,75 @@ function renderAppState() {
|
|
|
2548
2773
|
"</div>",
|
|
2549
2774
|
].join("");
|
|
2550
2775
|
|
|
2776
|
+
renderAuthPanel();
|
|
2551
2777
|
renderExportJobs();
|
|
2552
2778
|
}
|
|
2553
2779
|
|
|
2780
|
+
function authActionButton(label, action, disabled) {
|
|
2781
|
+
return (
|
|
2782
|
+
'<button class="button button--secondary" data-auth-action="' +
|
|
2783
|
+
escapeHtml(action) +
|
|
2784
|
+
'"' +
|
|
2785
|
+
(disabled ? " disabled" : "") +
|
|
2786
|
+
">" +
|
|
2787
|
+
escapeHtml(label) +
|
|
2788
|
+
"</button>"
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function authModeButton(label, mode, disabled) {
|
|
2793
|
+
return (
|
|
2794
|
+
'<button class="button button--secondary" data-auth-mode="' +
|
|
2795
|
+
escapeHtml(mode) +
|
|
2796
|
+
'"' +
|
|
2797
|
+
(disabled ? " disabled" : "") +
|
|
2798
|
+
">" +
|
|
2799
|
+
escapeHtml(label) +
|
|
2800
|
+
"</button>"
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function renderAuthPanel() {
|
|
2805
|
+
const auth = state.appState?.auth;
|
|
2806
|
+
if (!auth) {
|
|
2807
|
+
els.authPanel.innerHTML = '<div class="auth-card"><div class="auth-card__meta">Auth state unavailable.</div></div>';
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const activeSource = auth.mode === "stored-session" ? "Stored session" : "supabase.json";
|
|
2812
|
+
const lastError = auth.lastError
|
|
2813
|
+
? '<div class="auth-card__meta auth-card__error">' + escapeHtml(auth.lastError) + "</div>"
|
|
2814
|
+
: "";
|
|
2815
|
+
|
|
2816
|
+
els.authPanel.innerHTML = [
|
|
2817
|
+
'<div class="auth-card">',
|
|
2818
|
+
'<div class="status-grid">',
|
|
2819
|
+
'<div><span class="status-label">Active</span><strong>' + escapeHtml(activeSource) + "</strong></div>",
|
|
2820
|
+
'<div><span class="status-label">Stored</span><strong>' + escapeHtml(auth.storedSessionAvailable ? "available" : "missing") + "</strong></div>",
|
|
2821
|
+
'<div><span class="status-label">supabase.json</span><strong>' + escapeHtml(auth.supabaseAvailable ? "available" : "missing") + "</strong></div>",
|
|
2822
|
+
'<div><span class="status-label">Refresh</span><strong>' + escapeHtml(auth.refreshAvailable ? "available" : "missing") + "</strong></div>",
|
|
2823
|
+
"</div>",
|
|
2824
|
+
auth.clientId
|
|
2825
|
+
? '<div class="auth-card__meta">Client ID: ' + escapeHtml(auth.clientId) + "</div>"
|
|
2826
|
+
: "",
|
|
2827
|
+
auth.signInMethod
|
|
2828
|
+
? '<div class="auth-card__meta">Sign-in method: ' + escapeHtml(auth.signInMethod) + "</div>"
|
|
2829
|
+
: "",
|
|
2830
|
+
auth.supabasePath
|
|
2831
|
+
? '<div class="auth-card__meta">supabase path: ' + escapeHtml(auth.supabasePath) + "</div>"
|
|
2832
|
+
: "",
|
|
2833
|
+
lastError,
|
|
2834
|
+
'<div class="auth-card__actions">',
|
|
2835
|
+
authActionButton("Import desktop session", "login", !auth.supabaseAvailable),
|
|
2836
|
+
authActionButton("Refresh stored session", "refresh", !auth.storedSessionAvailable || !auth.refreshAvailable),
|
|
2837
|
+
authModeButton("Use stored session", "stored-session", !auth.storedSessionAvailable || auth.mode === "stored-session"),
|
|
2838
|
+
authModeButton("Use supabase.json", "supabase-file", !auth.supabaseAvailable || auth.mode === "supabase-file"),
|
|
2839
|
+
authActionButton("Sign out", "logout", !auth.storedSessionAvailable),
|
|
2840
|
+
"</div>",
|
|
2841
|
+
"</div>",
|
|
2842
|
+
].join("");
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2554
2845
|
function renderMeetingList() {
|
|
2555
2846
|
if (state.listError) {
|
|
2556
2847
|
els.list.innerHTML =
|
|
@@ -2805,12 +3096,24 @@ async function quickOpenMeeting() {
|
|
|
2805
3096
|
|
|
2806
3097
|
async function refreshAll() {
|
|
2807
3098
|
setStatus("Refreshing…", "busy");
|
|
2808
|
-
const [appState] = await Promise.all([fetchJson("/state"), loadMeetings()]);
|
|
2809
|
-
state.appState =
|
|
3099
|
+
const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status"), loadMeetings()]);
|
|
3100
|
+
state.appState = {
|
|
3101
|
+
...appState,
|
|
3102
|
+
auth: authState,
|
|
3103
|
+
};
|
|
2810
3104
|
renderAppState();
|
|
2811
3105
|
setStatus("Connected", "ok");
|
|
2812
3106
|
}
|
|
2813
3107
|
|
|
3108
|
+
async function syncAuthState() {
|
|
3109
|
+
const [appState, authState] = await Promise.all([fetchJson("/state"), fetchJson("/auth/status")]);
|
|
3110
|
+
state.appState = {
|
|
3111
|
+
...appState,
|
|
3112
|
+
auth: authState,
|
|
3113
|
+
};
|
|
3114
|
+
renderAppState();
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2814
3117
|
async function exportNotes() {
|
|
2815
3118
|
setStatus("Exporting notes…", "busy");
|
|
2816
3119
|
await fetchJson("/exports/notes", {
|
|
@@ -2845,6 +3148,62 @@ async function rerunJob(id) {
|
|
|
2845
3148
|
}
|
|
2846
3149
|
}
|
|
2847
3150
|
|
|
3151
|
+
async function loginAuth() {
|
|
3152
|
+
setStatus("Importing desktop session…", "busy");
|
|
3153
|
+
try {
|
|
3154
|
+
await fetchJson("/auth/login", { method: "POST" });
|
|
3155
|
+
await refreshAll();
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
await syncAuthState();
|
|
3158
|
+
setStatus("Auth import failed", "error");
|
|
3159
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3160
|
+
renderMeetingDetail();
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
async function logoutAuth() {
|
|
3165
|
+
setStatus("Signing out…", "busy");
|
|
3166
|
+
try {
|
|
3167
|
+
await fetchJson("/auth/logout", { method: "POST" });
|
|
3168
|
+
await refreshAll();
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
await syncAuthState();
|
|
3171
|
+
setStatus("Sign out failed", "error");
|
|
3172
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3173
|
+
renderMeetingDetail();
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
async function refreshAuth() {
|
|
3178
|
+
setStatus("Refreshing session…", "busy");
|
|
3179
|
+
try {
|
|
3180
|
+
await fetchJson("/auth/refresh", { method: "POST" });
|
|
3181
|
+
await refreshAll();
|
|
3182
|
+
} catch (error) {
|
|
3183
|
+
await syncAuthState();
|
|
3184
|
+
setStatus("Refresh failed", "error");
|
|
3185
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3186
|
+
renderMeetingDetail();
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
async function switchAuthMode(mode) {
|
|
3191
|
+
setStatus("Switching auth source…", "busy");
|
|
3192
|
+
try {
|
|
3193
|
+
await fetchJson("/auth/mode", {
|
|
3194
|
+
body: JSON.stringify({ mode }),
|
|
3195
|
+
headers: { "content-type": "application/json" },
|
|
3196
|
+
method: "POST",
|
|
3197
|
+
});
|
|
3198
|
+
await refreshAll();
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
await syncAuthState();
|
|
3201
|
+
setStatus("Switch failed", "error");
|
|
3202
|
+
state.detailError = error instanceof Error ? error.message : String(error);
|
|
3203
|
+
renderMeetingDetail();
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2848
3207
|
els.list.addEventListener("click", (event) => {
|
|
2849
3208
|
if (!(event.target instanceof Element)) {
|
|
2850
3209
|
return;
|
|
@@ -2868,6 +3227,36 @@ els.jobsList.addEventListener("click", (event) => {
|
|
|
2868
3227
|
void rerunJob(button.dataset.rerunJobId);
|
|
2869
3228
|
});
|
|
2870
3229
|
|
|
3230
|
+
els.authPanel.addEventListener("click", (event) => {
|
|
3231
|
+
if (!(event.target instanceof Element)) {
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
const actionButton = event.target.closest("[data-auth-action]");
|
|
3236
|
+
if (actionButton) {
|
|
3237
|
+
switch (actionButton.dataset.authAction) {
|
|
3238
|
+
case "login":
|
|
3239
|
+
void loginAuth();
|
|
3240
|
+
return;
|
|
3241
|
+
case "logout":
|
|
3242
|
+
void logoutAuth();
|
|
3243
|
+
return;
|
|
3244
|
+
case "refresh":
|
|
3245
|
+
void refreshAuth();
|
|
3246
|
+
return;
|
|
3247
|
+
default:
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
const modeButton = event.target.closest("[data-auth-mode]");
|
|
3253
|
+
if (!modeButton) {
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
void switchAuthMode(modeButton.dataset.authMode);
|
|
3258
|
+
});
|
|
3259
|
+
|
|
2871
3260
|
els.refreshButton.addEventListener("click", () => {
|
|
2872
3261
|
void refreshAll();
|
|
2873
3262
|
});
|
|
@@ -3066,6 +3455,13 @@ const granolaWebMarkup = String.raw`
|
|
|
3066
3455
|
</div>
|
|
3067
3456
|
<p>Initial beta web client. It speaks to the same local API that future TUI and attach flows will use.</p>
|
|
3068
3457
|
</section>
|
|
3458
|
+
<section class="auth-panel">
|
|
3459
|
+
<div class="auth-panel__head">
|
|
3460
|
+
<h3>Auth Session</h3>
|
|
3461
|
+
<p>Inspect, refresh, and switch between stored session and <code>supabase.json</code>.</p>
|
|
3462
|
+
</div>
|
|
3463
|
+
<div class="auth-panel__body" data-auth-panel></div>
|
|
3464
|
+
</section>
|
|
3069
3465
|
<section class="jobs-panel">
|
|
3070
3466
|
<div class="jobs-panel__head">
|
|
3071
3467
|
<h3>Recent Export Jobs</h3>
|
|
@@ -3295,10 +3691,12 @@ body {
|
|
|
3295
3691
|
width: min(440px, 100%);
|
|
3296
3692
|
}
|
|
3297
3693
|
|
|
3694
|
+
.auth-panel,
|
|
3298
3695
|
.jobs-panel {
|
|
3299
3696
|
padding: 0 24px 18px;
|
|
3300
3697
|
}
|
|
3301
3698
|
|
|
3699
|
+
.auth-panel__head h3,
|
|
3302
3700
|
.jobs-panel__head h3 {
|
|
3303
3701
|
margin: 0;
|
|
3304
3702
|
font-size: 0.92rem;
|
|
@@ -3306,12 +3704,43 @@ body {
|
|
|
3306
3704
|
text-transform: uppercase;
|
|
3307
3705
|
}
|
|
3308
3706
|
|
|
3707
|
+
.auth-panel__head p,
|
|
3309
3708
|
.jobs-panel__head p {
|
|
3310
3709
|
margin: 6px 0 0;
|
|
3311
3710
|
color: var(--muted);
|
|
3312
3711
|
font-size: 0.9rem;
|
|
3313
3712
|
}
|
|
3314
3713
|
|
|
3714
|
+
.auth-panel__body {
|
|
3715
|
+
display: grid;
|
|
3716
|
+
gap: 12px;
|
|
3717
|
+
margin-top: 14px;
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
.auth-card {
|
|
3721
|
+
display: grid;
|
|
3722
|
+
gap: 12px;
|
|
3723
|
+
padding: 14px 16px;
|
|
3724
|
+
border: 1px solid var(--line);
|
|
3725
|
+
border-radius: 18px;
|
|
3726
|
+
background: rgba(255, 255, 255, 0.72);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
.auth-card__meta {
|
|
3730
|
+
color: var(--muted);
|
|
3731
|
+
font-size: 0.9rem;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
.auth-card__actions {
|
|
3735
|
+
display: flex;
|
|
3736
|
+
flex-wrap: wrap;
|
|
3737
|
+
gap: 8px;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
.auth-card__error {
|
|
3741
|
+
color: var(--error);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3315
3744
|
.jobs-list {
|
|
3316
3745
|
display: grid;
|
|
3317
3746
|
gap: 10px;
|
|
@@ -3419,6 +3848,11 @@ body {
|
|
|
3419
3848
|
cursor: pointer;
|
|
3420
3849
|
}
|
|
3421
3850
|
|
|
3851
|
+
.button:disabled {
|
|
3852
|
+
cursor: not-allowed;
|
|
3853
|
+
opacity: 0.56;
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3422
3856
|
.button--primary {
|
|
3423
3857
|
background: var(--ink);
|
|
3424
3858
|
color: white;
|
|
@@ -3566,6 +4000,13 @@ function parseMeetingSort(value) {
|
|
|
3566
4000
|
default: throw new Error("invalid sort: expected updated-desc, updated-asc, title-asc, or title-desc");
|
|
3567
4001
|
}
|
|
3568
4002
|
}
|
|
4003
|
+
function parseAuthMode(value) {
|
|
4004
|
+
switch (value) {
|
|
4005
|
+
case "stored-session":
|
|
4006
|
+
case "supabase-file": return value;
|
|
4007
|
+
default: throw new Error("invalid auth mode: expected stored-session or supabase-file");
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
3569
4010
|
function sendJson(response, body, init = {}) {
|
|
3570
4011
|
const payload = `${JSON.stringify(body, null, 2)}\n`;
|
|
3571
4012
|
response.writeHead(init.status ?? 200, {
|
|
@@ -3648,6 +4089,10 @@ async function startGranolaServer(app, options = {}) {
|
|
|
3648
4089
|
sendJson(response, app.getState());
|
|
3649
4090
|
return;
|
|
3650
4091
|
}
|
|
4092
|
+
if (method === "GET" && path === "/auth/status") {
|
|
4093
|
+
sendJson(response, await app.inspectAuth());
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
3651
4096
|
if (method === "GET" && path === "/events") {
|
|
3652
4097
|
response.writeHead(200, {
|
|
3653
4098
|
"cache-control": "no-cache, no-transform",
|
|
@@ -3701,6 +4146,25 @@ async function startGranolaServer(app, options = {}) {
|
|
|
3701
4146
|
sendJson(response, await app.getMeeting(id, { requireCache: url.searchParams.get("includeTranscript") === "true" }));
|
|
3702
4147
|
return;
|
|
3703
4148
|
}
|
|
4149
|
+
if (method === "POST" && path === "/auth/login") {
|
|
4150
|
+
const body = await readJsonBody(request);
|
|
4151
|
+
const supabasePath = typeof body.supabasePath === "string" && body.supabasePath.trim() ? body.supabasePath.trim() : void 0;
|
|
4152
|
+
sendJson(response, await app.loginAuth({ supabasePath }));
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
if (method === "POST" && path === "/auth/logout") {
|
|
4156
|
+
sendJson(response, await app.logoutAuth());
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
if (method === "POST" && path === "/auth/refresh") {
|
|
4160
|
+
sendJson(response, await app.refreshAuth());
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
if (method === "POST" && path === "/auth/mode") {
|
|
4164
|
+
const body = await readJsonBody(request);
|
|
4165
|
+
sendJson(response, await app.switchAuthMode(parseAuthMode(body.mode)));
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
3704
4168
|
if (method === "POST" && path === "/exports/notes") {
|
|
3705
4169
|
const body = await readJsonBody(request);
|
|
3706
4170
|
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format)), { status: 202 });
|
|
@@ -3803,11 +4267,16 @@ const serveCommand = {
|
|
|
3803
4267
|
console.log(`Granola server listening on ${server.url.href}`);
|
|
3804
4268
|
console.log("Endpoints:");
|
|
3805
4269
|
console.log(" GET /health");
|
|
4270
|
+
console.log(" GET /auth/status");
|
|
3806
4271
|
console.log(" GET /state");
|
|
3807
4272
|
console.log(" GET /events");
|
|
3808
4273
|
console.log(" GET /meetings");
|
|
3809
4274
|
console.log(" GET /meetings/:id");
|
|
3810
4275
|
console.log(" GET /exports/jobs");
|
|
4276
|
+
console.log(" POST /auth/login");
|
|
4277
|
+
console.log(" POST /auth/logout");
|
|
4278
|
+
console.log(" POST /auth/mode");
|
|
4279
|
+
console.log(" POST /auth/refresh");
|
|
3811
4280
|
console.log(" POST /exports/notes");
|
|
3812
4281
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
3813
4282
|
console.log(" POST /exports/transcripts");
|
|
@@ -3964,11 +4433,16 @@ const commands = [
|
|
|
3964
4433
|
console.log("Routes:");
|
|
3965
4434
|
console.log(" GET /");
|
|
3966
4435
|
console.log(" GET /health");
|
|
4436
|
+
console.log(" GET /auth/status");
|
|
3967
4437
|
console.log(" GET /state");
|
|
3968
4438
|
console.log(" GET /events");
|
|
3969
4439
|
console.log(" GET /meetings");
|
|
3970
4440
|
console.log(" GET /meetings/:id");
|
|
3971
4441
|
console.log(" GET /exports/jobs");
|
|
4442
|
+
console.log(" POST /auth/login");
|
|
4443
|
+
console.log(" POST /auth/logout");
|
|
4444
|
+
console.log(" POST /auth/mode");
|
|
4445
|
+
console.log(" POST /auth/refresh");
|
|
3972
4446
|
console.log(" POST /exports/notes");
|
|
3973
4447
|
console.log(" POST /exports/jobs/:id/rerun");
|
|
3974
4448
|
console.log(" POST /exports/transcripts");
|