mysticals 0.4.0 → 0.6.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/cli.js +147 -41
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createElement } from "react";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.6.0";
|
|
9
9
|
|
|
10
10
|
// src/client.ts
|
|
11
11
|
import { spawn } from "child_process";
|
|
@@ -46,6 +46,7 @@ var methodSet = {
|
|
|
46
46
|
"accounts.addCaldav": true,
|
|
47
47
|
"accounts.update": true,
|
|
48
48
|
"accounts.remove": true,
|
|
49
|
+
"accounts.reauth": true,
|
|
49
50
|
"calendars.list": true,
|
|
50
51
|
"calendars.setVisible": true,
|
|
51
52
|
"events.list": true,
|
|
@@ -239,6 +240,21 @@ function when(e, now) {
|
|
|
239
240
|
return `${isSameDay(s, now) ? "Today" : format(s, "EEE, d MMM")} \xB7 ${format(s, "HH:mm")}`;
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
// ../../packages/core/src/providers/http.ts
|
|
244
|
+
var http = { timeoutMs: 3e4 };
|
|
245
|
+
var timedFetch = async (input, init) => {
|
|
246
|
+
const timeout = AbortSignal.timeout(http.timeoutMs);
|
|
247
|
+
try {
|
|
248
|
+
return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
|
|
249
|
+
} catch (e) {
|
|
250
|
+
if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
|
|
251
|
+
throw e;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var AuthError = class extends Error {
|
|
255
|
+
name = "AuthError";
|
|
256
|
+
};
|
|
257
|
+
|
|
242
258
|
// ../../packages/core/src/sync/engine.ts
|
|
243
259
|
var MIN = 6e4;
|
|
244
260
|
var SyncEngine = class {
|
|
@@ -261,6 +277,8 @@ var SyncEngine = class {
|
|
|
261
277
|
queued = /* @__PURE__ */ new Map();
|
|
262
278
|
/** Bumped by `edited`: a pass that started before a local edit must not overwrite it. */
|
|
263
279
|
edits = /* @__PURE__ */ new Map();
|
|
280
|
+
/** Accounts whose last pass listed no calendars while the cache had some (see syncAccount). */
|
|
281
|
+
emptied = /* @__PURE__ */ new Set();
|
|
264
282
|
running = false;
|
|
265
283
|
unsubscribe;
|
|
266
284
|
start() {
|
|
@@ -280,6 +298,7 @@ var SyncEngine = class {
|
|
|
280
298
|
/**
|
|
281
299
|
* Sync one account, or all accounts independently when id omitted.
|
|
282
300
|
* A single-account call rejects with that account's error; the all-accounts call never rejects.
|
|
301
|
+
* The all-accounts call skips accounts whose credentials were rejected (`authError`): retrying can't help.
|
|
283
302
|
*/
|
|
284
303
|
/**
|
|
285
304
|
* `quiet`: the change came from this app (own edit/RSVP), so don't notify about it.
|
|
@@ -288,7 +307,7 @@ var SyncEngine = class {
|
|
|
288
307
|
*/
|
|
289
308
|
async syncNow(accountId, opts = {}) {
|
|
290
309
|
if (accountId === void 0) {
|
|
291
|
-
await Promise.allSettled(this.store.list().map((a) => this.syncNow(a.id)));
|
|
310
|
+
await Promise.allSettled(this.store.list().filter((a) => !a.authError).map((a) => this.syncNow(a.id)));
|
|
292
311
|
return;
|
|
293
312
|
}
|
|
294
313
|
const existing = this.inflight.get(accountId);
|
|
@@ -326,7 +345,8 @@ var SyncEngine = class {
|
|
|
326
345
|
schedule(id2) {
|
|
327
346
|
clearTimeout(this.timers.get(id2));
|
|
328
347
|
this.timers.delete(id2);
|
|
329
|
-
|
|
348
|
+
const acc = this.store.list().find((a) => a.id === id2);
|
|
349
|
+
if (!this.running || !acc || acc.authError) {
|
|
330
350
|
this.failures.delete(id2);
|
|
331
351
|
return;
|
|
332
352
|
}
|
|
@@ -340,7 +360,8 @@ var SyncEngine = class {
|
|
|
340
360
|
}
|
|
341
361
|
// ponytail: quiet silences the whole sync, so an external change landing in the same pass is not announced.
|
|
342
362
|
async syncAccount(id2, quiet) {
|
|
343
|
-
const
|
|
363
|
+
const acc = this.store.list().find((a) => a.id === id2);
|
|
364
|
+
const hadError = !!acc?.error;
|
|
344
365
|
const edits = this.edits.get(id2) ?? 0;
|
|
345
366
|
try {
|
|
346
367
|
const provider = this.store.getProvider(id2);
|
|
@@ -363,10 +384,15 @@ var SyncEngine = class {
|
|
|
363
384
|
} catch {
|
|
364
385
|
prev = void 0;
|
|
365
386
|
}
|
|
387
|
+
if (!calendars.length && prev?.calendars.length && !this.emptied.has(id2)) {
|
|
388
|
+
this.emptied.add(id2);
|
|
389
|
+
throw new Error("The server returned no calendars; showing the last synced data");
|
|
390
|
+
}
|
|
391
|
+
this.emptied.delete(id2);
|
|
366
392
|
const changed = !prev || JSON.stringify([prev.calendars, prev.events]) !== JSON.stringify([next.calendars, next.events]);
|
|
367
393
|
await this.store.writeCache(id2, next);
|
|
368
394
|
this.failures.delete(id2);
|
|
369
|
-
if (hadError) await this.store.update(id2, { error: void 0 });
|
|
395
|
+
if (hadError) await this.store.update(id2, { error: void 0, authError: false });
|
|
370
396
|
if (changed || hadError) this.onChanged(id2);
|
|
371
397
|
if (changed && !quiet && prev?.syncedAt && this.opts.onEvents) {
|
|
372
398
|
const notes = diffEvents(prev.events, next.events, now);
|
|
@@ -375,9 +401,10 @@ var SyncEngine = class {
|
|
|
375
401
|
} catch (e) {
|
|
376
402
|
this.failures.set(id2, (this.failures.get(id2) ?? 0) + 1);
|
|
377
403
|
const message = e instanceof Error ? e.message : String(e);
|
|
378
|
-
|
|
404
|
+
const auth = e instanceof AuthError;
|
|
405
|
+
await this.store.update(id2, { error: message, ...auth ? { authError: true } : {} }).catch(() => {
|
|
379
406
|
});
|
|
380
|
-
if (!hadError) this.onChanged(id2);
|
|
407
|
+
if (!hadError || auth && !acc?.authError) this.onChanged(id2);
|
|
381
408
|
throw e;
|
|
382
409
|
}
|
|
383
410
|
}
|
|
@@ -527,6 +554,7 @@ var CaldavInput = z.object({
|
|
|
527
554
|
password: text(2e3).min(1),
|
|
528
555
|
color: color.optional()
|
|
529
556
|
});
|
|
557
|
+
var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
|
|
530
558
|
var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
|
|
531
559
|
var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
|
|
532
560
|
var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
|
|
@@ -592,6 +620,26 @@ function createApi(store, sync, deps) {
|
|
|
592
620
|
deps.onChanged?.(a.id);
|
|
593
621
|
return a;
|
|
594
622
|
},
|
|
623
|
+
reauth: async (accountId, raw) => {
|
|
624
|
+
const a = account(accountId);
|
|
625
|
+
let credentials;
|
|
626
|
+
if (a.kind === "google") {
|
|
627
|
+
const signed = await deps.googleSignIn();
|
|
628
|
+
if (signed.email.toLowerCase() !== a.email.toLowerCase())
|
|
629
|
+
throw new Error(`Signed in as ${signed.email}; sign in as ${a.email} to reconnect this account`);
|
|
630
|
+
credentials = signed.credentials;
|
|
631
|
+
} else {
|
|
632
|
+
const { password } = ReauthInput.parse(raw);
|
|
633
|
+
const old = store.readCreds(a.id);
|
|
634
|
+
if (old.kind !== "caldav") throw new Error("Credentials kind does not match account kind");
|
|
635
|
+
await deps.verifyCaldav({ label: a.label, serverUrl: old.serverUrl, username: old.username, password });
|
|
636
|
+
credentials = { ...old, password };
|
|
637
|
+
}
|
|
638
|
+
const updated = await store.update(a.id, { credentials, error: void 0, authError: false });
|
|
639
|
+
deps.onChanged?.(a.id);
|
|
640
|
+
syncOne(a.id);
|
|
641
|
+
return updated;
|
|
642
|
+
},
|
|
595
643
|
remove: async (accountId) => {
|
|
596
644
|
const a = account(accountId);
|
|
597
645
|
await store.remove(a.id);
|
|
@@ -740,6 +788,7 @@ var AccountStore = class {
|
|
|
740
788
|
await mkdir(dir, { recursive: true });
|
|
741
789
|
await writeAtomic(join4(dir, "creds.bin"), data);
|
|
742
790
|
}
|
|
791
|
+
/** Decrypted credentials; main/daemon only (accounts.reauth keeps the server and username), never to the renderer. */
|
|
743
792
|
readCreds(id2) {
|
|
744
793
|
return JSON.parse(this.crypto.decrypt(readFileSync2(join4(this.accountDir(id2), "creds.bin"))));
|
|
745
794
|
}
|
|
@@ -775,6 +824,10 @@ var AccountStore = class {
|
|
|
775
824
|
if (patch.error) acc.error = patch.error;
|
|
776
825
|
else delete acc.error;
|
|
777
826
|
}
|
|
827
|
+
if ("authError" in patch) {
|
|
828
|
+
if (patch.authError) acc.authError = true;
|
|
829
|
+
else delete acc.authError;
|
|
830
|
+
}
|
|
778
831
|
await this.saveRegistry();
|
|
779
832
|
return { ...acc };
|
|
780
833
|
});
|
|
@@ -1118,7 +1171,15 @@ var iso = (dayOffset, h, m = 0) => {
|
|
|
1118
1171
|
};
|
|
1119
1172
|
function createMockApi(onChanged) {
|
|
1120
1173
|
const accounts = [
|
|
1121
|
-
{
|
|
1174
|
+
{
|
|
1175
|
+
id: "work",
|
|
1176
|
+
kind: "caldav",
|
|
1177
|
+
label: "Work",
|
|
1178
|
+
email: "me@work.example",
|
|
1179
|
+
color: "#8be9fd",
|
|
1180
|
+
// e2e: a revoked app password; accounts.reauth with any password fixes it.
|
|
1181
|
+
...process.env.MYSTICALS_MOCK_AUTH_ERROR ? { error: "CalDAV login failed: the password was changed or revoked", authError: true } : {}
|
|
1182
|
+
},
|
|
1122
1183
|
{ id: "personal", kind: "google", label: "Personal", email: "me@gmail.example", color: "#50fa7b" }
|
|
1123
1184
|
];
|
|
1124
1185
|
const providers = /* @__PURE__ */ new Map([
|
|
@@ -1157,6 +1218,12 @@ function createMockApi(onChanged) {
|
|
|
1157
1218
|
myStatus: "needsAction",
|
|
1158
1219
|
attendees: [{ email: "pm@work.example", status: "accepted", organizer: true }, { email: "me@work.example", status: "needsAction", self: true }]
|
|
1159
1220
|
});
|
|
1221
|
+
seed(work, "work-main", {
|
|
1222
|
+
title: "Quarterly roadmap sync with design",
|
|
1223
|
+
start: iso(0, 11, 30),
|
|
1224
|
+
end: iso(0, 12),
|
|
1225
|
+
location: "Zoom https://zoom.us/j/1234567890?pwd=abc"
|
|
1226
|
+
});
|
|
1160
1227
|
seed(personal, "p-main", { title: "Gym", start: iso(0, 19), end: iso(0, 20) });
|
|
1161
1228
|
for (let d = -2; d <= 4; d++) {
|
|
1162
1229
|
seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
|
|
@@ -1165,6 +1232,8 @@ function createMockApi(onChanged) {
|
|
|
1165
1232
|
seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
|
|
1166
1233
|
const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
|
|
1167
1234
|
seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
|
|
1235
|
+
for (const title2 of ["Conference", "Team offsite", "Release freeze", "Birthday"])
|
|
1236
|
+
seed(personal, "p-main", { title: title2, start: day(3), end: day(4), allDay: true });
|
|
1168
1237
|
const prov = (id2) => {
|
|
1169
1238
|
const p = providers.get(id2);
|
|
1170
1239
|
if (!p) throw new Error(`unknown account ${id2}`);
|
|
@@ -1191,6 +1260,14 @@ function createMockApi(onChanged) {
|
|
|
1191
1260
|
onChanged(id2);
|
|
1192
1261
|
return structuredClone(a);
|
|
1193
1262
|
},
|
|
1263
|
+
reauth: async (id2) => {
|
|
1264
|
+
const a = accounts.find((x) => x.id === id2);
|
|
1265
|
+
if (!a) throw new Error("unknown account");
|
|
1266
|
+
delete a.error;
|
|
1267
|
+
delete a.authError;
|
|
1268
|
+
onChanged(id2);
|
|
1269
|
+
return structuredClone(a);
|
|
1270
|
+
},
|
|
1194
1271
|
remove: async (id2) => {
|
|
1195
1272
|
const i = accounts.findIndex((a) => a.id === id2);
|
|
1196
1273
|
if (i >= 0) accounts.splice(i, 1);
|
|
@@ -1692,18 +1769,6 @@ function applyRespond(raw, email, status) {
|
|
|
1692
1769
|
return root.toString();
|
|
1693
1770
|
}
|
|
1694
1771
|
|
|
1695
|
-
// ../../packages/core/src/providers/http.ts
|
|
1696
|
-
var http = { timeoutMs: 3e4 };
|
|
1697
|
-
var timedFetch = async (input, init) => {
|
|
1698
|
-
const timeout = AbortSignal.timeout(http.timeoutMs);
|
|
1699
|
-
try {
|
|
1700
|
-
return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
|
|
1701
|
-
} catch (e) {
|
|
1702
|
-
if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
|
|
1703
|
-
throw e;
|
|
1704
|
-
}
|
|
1705
|
-
};
|
|
1706
|
-
|
|
1707
1772
|
// ../../packages/core/src/providers/caldav/index.ts
|
|
1708
1773
|
var FALLBACK_COLOR = "#8e8e93";
|
|
1709
1774
|
var DAY = 864e5;
|
|
@@ -1728,7 +1793,7 @@ async function connect2(serverUrl, username, password) {
|
|
|
1728
1793
|
});
|
|
1729
1794
|
return { account, headers };
|
|
1730
1795
|
} catch (e) {
|
|
1731
|
-
if (unauthorized) throw new
|
|
1796
|
+
if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
|
|
1732
1797
|
throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
|
|
1733
1798
|
}
|
|
1734
1799
|
}
|
|
@@ -1754,10 +1819,15 @@ async function listVeventCalendars(conn) {
|
|
|
1754
1819
|
headers: conn.headers,
|
|
1755
1820
|
props: CALENDAR_PROPS,
|
|
1756
1821
|
projectedProps: { currentUserPrivilegeSet: true },
|
|
1757
|
-
fetch:
|
|
1822
|
+
fetch: davFetch
|
|
1758
1823
|
});
|
|
1759
1824
|
return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
|
|
1760
1825
|
}
|
|
1826
|
+
var davFetch = async (input, init) => {
|
|
1827
|
+
const res = await timedFetch(input, init);
|
|
1828
|
+
if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
|
|
1829
|
+
return res;
|
|
1830
|
+
};
|
|
1761
1831
|
function check(res, what) {
|
|
1762
1832
|
if (res.ok) return;
|
|
1763
1833
|
if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
|
|
@@ -1777,7 +1847,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1777
1847
|
const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
|
|
1778
1848
|
async function reload(calendarId, href, id2, around) {
|
|
1779
1849
|
const { headers } = await getConn();
|
|
1780
|
-
const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch:
|
|
1850
|
+
const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
|
|
1781
1851
|
if (!obj?.data) throw new Error("Saved event could not be read back from the server");
|
|
1782
1852
|
const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
|
|
1783
1853
|
const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
|
|
@@ -1789,7 +1859,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1789
1859
|
const put = async (event, ics, what) => {
|
|
1790
1860
|
const { headers } = await getConn();
|
|
1791
1861
|
const raw = event.raw;
|
|
1792
|
-
check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch:
|
|
1862
|
+
check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
|
|
1793
1863
|
};
|
|
1794
1864
|
const provider = {
|
|
1795
1865
|
async listCalendars() {
|
|
@@ -1798,7 +1868,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1798
1868
|
},
|
|
1799
1869
|
async listEvents(calendarId, range) {
|
|
1800
1870
|
const { headers } = await getConn();
|
|
1801
|
-
const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch:
|
|
1871
|
+
const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
|
|
1802
1872
|
return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
|
|
1803
1873
|
},
|
|
1804
1874
|
async createEvent(calendarId, input) {
|
|
@@ -1806,7 +1876,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1806
1876
|
const uid = randomUUID4();
|
|
1807
1877
|
const filename = `${uid}.ics`;
|
|
1808
1878
|
const ics = buildIcs(uid, input, ctx.email);
|
|
1809
|
-
check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch:
|
|
1879
|
+
check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
|
|
1810
1880
|
const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
|
|
1811
1881
|
return reload(calendarId, href, void 0, input);
|
|
1812
1882
|
},
|
|
@@ -1825,7 +1895,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1825
1895
|
await put(event, split.rest, "Update event");
|
|
1826
1896
|
const { headers } = await getConn();
|
|
1827
1897
|
const filename = `${uid}.ics`;
|
|
1828
|
-
check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch:
|
|
1898
|
+
check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
|
|
1829
1899
|
const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
|
|
1830
1900
|
return reload(event.calendarId, href, void 0, event);
|
|
1831
1901
|
},
|
|
@@ -1834,7 +1904,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1834
1904
|
const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
|
|
1835
1905
|
if (rest) return put(event, rest, "Delete event");
|
|
1836
1906
|
const { headers } = await getConn();
|
|
1837
|
-
check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch:
|
|
1907
|
+
check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
|
|
1838
1908
|
},
|
|
1839
1909
|
async getRecurrence(event) {
|
|
1840
1910
|
return recurrenceOf(event.raw);
|
|
@@ -1849,7 +1919,7 @@ var createCaldavProvider = (ctx) => {
|
|
|
1849
1919
|
async function verifyCaldav(input) {
|
|
1850
1920
|
const c = await connect2(input.serverUrl.trim(), input.username, input.password);
|
|
1851
1921
|
await listVeventCalendars(c);
|
|
1852
|
-
const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch:
|
|
1922
|
+
const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
|
|
1853
1923
|
const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
|
|
1854
1924
|
const login = cleanEmail(input.username);
|
|
1855
1925
|
const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
|
|
@@ -1902,7 +1972,10 @@ function emailFromIdToken(idToken) {
|
|
|
1902
1972
|
}
|
|
1903
1973
|
function parseTokenResponse(json, now = Date.now()) {
|
|
1904
1974
|
const j = json ?? {};
|
|
1905
|
-
if (typeof j.error === "string")
|
|
1975
|
+
if (typeof j.error === "string") {
|
|
1976
|
+
const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
|
|
1977
|
+
throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
|
|
1978
|
+
}
|
|
1906
1979
|
if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
|
|
1907
1980
|
const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
|
|
1908
1981
|
return {
|
|
@@ -2074,6 +2147,7 @@ function createGoogleProviderImpl(ctx) {
|
|
|
2074
2147
|
});
|
|
2075
2148
|
let res = await send(await token());
|
|
2076
2149
|
if (res.status === 401) res = await send(await refresh());
|
|
2150
|
+
if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
|
|
2077
2151
|
if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
|
|
2078
2152
|
if (!res.ok) {
|
|
2079
2153
|
const err = await res.json().catch(() => ({}));
|
|
@@ -3041,9 +3115,8 @@ function meetingUrl(location) {
|
|
|
3041
3115
|
return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
|
|
3042
3116
|
}
|
|
3043
3117
|
function place(location = "") {
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
return rest || (url ? "video call" : "");
|
|
3118
|
+
const rest = location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
|
|
3119
|
+
return rest || (meetingUrl(location) ? "video call" : "");
|
|
3047
3120
|
}
|
|
3048
3121
|
|
|
3049
3122
|
// ../../packages/core/src/logic/status.ts
|
|
@@ -4123,9 +4196,16 @@ function StatusLine({ now, message, update, width, onOpen, onInvites, onAccounts
|
|
|
4123
4196
|
invites === 1 ? "1 invite" : `${invites} invites`,
|
|
4124
4197
|
" (i)"
|
|
4125
4198
|
] }), onInvites]);
|
|
4126
|
-
|
|
4199
|
+
const off = failed.filter((a) => a.authError);
|
|
4200
|
+
const broken = failed.filter((a) => !a.authError);
|
|
4201
|
+
if (off.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
|
|
4127
4202
|
"\u26A0 ",
|
|
4128
|
-
|
|
4203
|
+
off.map((a) => a.label).join(", "),
|
|
4204
|
+
" disconnected (s)"
|
|
4205
|
+
] }), onAccounts]);
|
|
4206
|
+
if (broken.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
|
|
4207
|
+
"\u26A0 ",
|
|
4208
|
+
broken.map((a) => a.label).join(", "),
|
|
4129
4209
|
" sync failed (s)"
|
|
4130
4210
|
] }), onAccounts]);
|
|
4131
4211
|
if (message) parts.push([message]);
|
|
@@ -4331,13 +4411,13 @@ function Accounts({ onClose }) {
|
|
|
4331
4411
|
(e) => say(errorText(e), true)
|
|
4332
4412
|
);
|
|
4333
4413
|
};
|
|
4334
|
-
const
|
|
4414
|
+
const googleSignIn2 = (fn, done) => {
|
|
4335
4415
|
const s = ++googleSession.current;
|
|
4336
4416
|
setMode({ kind: "google" });
|
|
4337
4417
|
setMessage(void 0);
|
|
4338
4418
|
setAuthUrl(void 0);
|
|
4339
|
-
|
|
4340
|
-
(a) => s === googleSession.current && (setMode({ kind: "list" }), say(
|
|
4419
|
+
fn().then(
|
|
4420
|
+
(a) => s === googleSession.current && (setMode({ kind: "list" }), say(done(a))),
|
|
4341
4421
|
(e) => s === googleSession.current && (setMode({ kind: "list" }), say(errorText(e), true))
|
|
4342
4422
|
);
|
|
4343
4423
|
};
|
|
@@ -4345,7 +4425,7 @@ function Accounts({ onClose }) {
|
|
|
4345
4425
|
const act = (input) => {
|
|
4346
4426
|
const { row: row2 } = current();
|
|
4347
4427
|
if (input === "R") return run("Syncing all\u2026", "Synced all", () => api.sync.now());
|
|
4348
|
-
if (input === "g") return addGoogle();
|
|
4428
|
+
if (input === "g") return googleSignIn2(() => api.accounts.addGoogle(), (a2) => `Added ${a2.label}`);
|
|
4349
4429
|
if (input === "a") return setMode({ kind: "caldav" });
|
|
4350
4430
|
if (!row2) return;
|
|
4351
4431
|
const a = row2.account;
|
|
@@ -4364,6 +4444,11 @@ function Accounts({ onClose }) {
|
|
|
4364
4444
|
return run("Saving\u2026", `${a.label} colour changed`, () => api.accounts.update(a.id, { color: color2 }));
|
|
4365
4445
|
}
|
|
4366
4446
|
if (input === "x") return setMode({ kind: "confirm", account: a });
|
|
4447
|
+
if (input === "p" && a.authError) {
|
|
4448
|
+
if (a.kind === "google") return googleSignIn2(() => api.accounts.reauth(a.id), () => `${a.label} reconnected`);
|
|
4449
|
+
setMessage(void 0);
|
|
4450
|
+
return setMode({ kind: "reauth", account: a, value: "" });
|
|
4451
|
+
}
|
|
4367
4452
|
};
|
|
4368
4453
|
const confirmRemove = (yes) => {
|
|
4369
4454
|
const mode2 = getMode();
|
|
@@ -4389,6 +4474,19 @@ function Accounts({ onClose }) {
|
|
|
4389
4474
|
return;
|
|
4390
4475
|
}
|
|
4391
4476
|
if (mode2.kind === "confirm") return confirmRemove(input.toLowerCase() === "y");
|
|
4477
|
+
if (mode2.kind === "reauth") {
|
|
4478
|
+
if (key3.escape) return setMode({ kind: "list" });
|
|
4479
|
+
if (key3.return) {
|
|
4480
|
+
if (!mode2.value) return;
|
|
4481
|
+
const { id: id2, label } = mode2.account;
|
|
4482
|
+
const password = mode2.value;
|
|
4483
|
+
run("Reconnecting\u2026", `${label} reconnected`, () => api.accounts.reauth(id2, { password }));
|
|
4484
|
+
return setMode({ kind: "list" });
|
|
4485
|
+
}
|
|
4486
|
+
const value = editText(mode2.value, input, key3);
|
|
4487
|
+
if (value !== void 0) setMode({ ...mode2, value });
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4392
4490
|
if (mode2.kind === "rename") {
|
|
4393
4491
|
if (key3.escape) return setMode({ kind: "list" });
|
|
4394
4492
|
if (key3.return) {
|
|
@@ -4467,7 +4565,14 @@ function Accounts({ onClose }) {
|
|
|
4467
4565
|
/* @__PURE__ */ jsx12(Spinner, {}),
|
|
4468
4566
|
" Loading calendars\u2026"
|
|
4469
4567
|
] }),
|
|
4470
|
-
a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` })
|
|
4568
|
+
a.authError ? /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Disconnected: ${a.error ?? "credentials rejected"} \u2014 p to reconnect` }) : a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` }),
|
|
4569
|
+
mode.kind === "reauth" && mode.account.id === a.id && /* @__PURE__ */ jsxs10(Text10, { children: [
|
|
4570
|
+
" New app password: ",
|
|
4571
|
+
/* @__PURE__ */ jsxs10(Text10, { color: C.cyan, children: [
|
|
4572
|
+
"\u2022".repeat(mode.value.length),
|
|
4573
|
+
"\u258F"
|
|
4574
|
+
] })
|
|
4575
|
+
] })
|
|
4471
4576
|
] }, a.id);
|
|
4472
4577
|
}),
|
|
4473
4578
|
mode.kind === "confirm" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginTop: 1, children: [
|
|
@@ -4496,7 +4601,7 @@ function Accounts({ onClose }) {
|
|
|
4496
4601
|
/* @__PURE__ */ jsx12(Text10, { color: C.muted, children: `Over SSH, forward its port first: ssh -L ${loopbackPort(authUrl)}:127.0.0.1:${loopbackPort(authUrl)} <host>` })
|
|
4497
4602
|
] })
|
|
4498
4603
|
] }),
|
|
4499
|
-
mode.kind === "rename" && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
|
|
4604
|
+
(mode.kind === "rename" || mode.kind === "reauth") && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
|
|
4500
4605
|
loadError && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: loadError }),
|
|
4501
4606
|
message && /* @__PURE__ */ jsx12(Text10, { color: message.error ? C.red : C.green, children: message.text }),
|
|
4502
4607
|
mode.kind === "list" && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexWrap: "wrap", children: [
|
|
@@ -4507,6 +4612,7 @@ function Accounts({ onClose }) {
|
|
|
4507
4612
|
row && button("e", "rename"),
|
|
4508
4613
|
row && button("c", "colour"),
|
|
4509
4614
|
row && button("x", "remove"),
|
|
4615
|
+
row?.account.authError && button("p", "reconnect"),
|
|
4510
4616
|
button("g", "add Google"),
|
|
4511
4617
|
button("a", "add CalDAV"),
|
|
4512
4618
|
/* @__PURE__ */ jsx12(Button, { k: "esc", label: "close", onPress: onClose })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mysticals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Isolated multi-account calendar for the terminal on macOS, Linux and Windows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calendar",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"tsup": "^8.5.1",
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
49
|
"vitest": "^3.2.7",
|
|
50
|
-
"@mysticals/core": "0.
|
|
50
|
+
"@mysticals/core": "0.6.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"dev": "tsup --watch",
|