@rubytech/create-maxy-code 0.1.472 → 0.1.473
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/__tests__/google-client-id-env.test.js +12 -10
- package/dist/index.js +6 -29
- package/dist/port-resolution.js +5 -5
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +109 -0
- package/payload/platform/lib/ledger-core/dist/reconcile.d.ts +7 -0
- package/payload/platform/lib/ledger-core/dist/reconcile.d.ts.map +1 -1
- package/payload/platform/lib/ledger-core/dist/reconcile.js +5 -2
- package/payload/platform/lib/ledger-core/dist/reconcile.js.map +1 -1
- package/payload/platform/lib/ledger-core/src/__tests__/reconcile.test.ts +20 -0
- package/payload/platform/lib/ledger-core/src/reconcile.ts +12 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +24 -5
- package/payload/platform/plugins/docs/references/google-guide.md +10 -3
- package/payload/platform/plugins/docs/references/ledger.md +9 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +4 -0
- package/payload/platform/plugins/google/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/google/PLUGIN.md +4 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js +79 -28
- package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.d.ts +2 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.d.ts.map +1 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.js +54 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/boot.test.js.map +1 -0
- package/payload/platform/plugins/google/mcp/dist/__tests__/google-client.test.js +26 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/google-client.test.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/__tests__/index-registration.test.js +3 -2
- package/payload/platform/plugins/google/mcp/dist/__tests__/index-registration.test.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/index.d.ts +9 -5
- package/payload/platform/plugins/google/mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/index.js +38 -37
- package/payload/platform/plugins/google/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.d.ts +10 -7
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.d.ts.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.js +25 -13
- package/payload/platform/plugins/google/mcp/dist/lib/google-client.js.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts +58 -17
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts.map +1 -1
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.js +143 -37
- package/payload/platform/plugins/google/mcp/dist/tools/account-register.js.map +1 -1
- package/payload/platform/plugins/google/references/auth.md +91 -74
- package/payload/platform/plugins/ledger/PLUGIN.md +13 -1
- package/payload/platform/plugins/ledger/mcp/dist/index.js +23 -15
- package/payload/platform/plugins/ledger/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.d.ts +11 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.js +20 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/read.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/types.d.ts +14 -11
- package/payload/platform/plugins/ledger/mcp/dist/lib/types.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/types.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.d.ts +12 -0
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.d.ts.map +1 -0
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.js +50 -0
- package/payload/platform/plugins/ledger/mcp/dist/lib/write-rejects.js.map +1 -0
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.d.ts +5 -5
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.js +11 -11
- package/payload/platform/plugins/ledger/mcp/dist/tools/cash-record.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.d.ts +7 -2
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.js +9 -3
- package/payload/platform/plugins/ledger/mcp/dist/tools/invoice-record.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.d.ts +7 -0
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.js +3 -4
- package/payload/platform/plugins/ledger/mcp/dist/tools/payment-record.js.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.d.ts +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.js +4 -1
- package/payload/platform/plugins/ledger/mcp/dist/tools/reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +27 -2
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js +33 -2
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +3 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-BSTIQX52.js → chunk-Y6V45R6M.js} +1 -12
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/activity.html +6 -6
- package/payload/server/public/assets/{AdminLoginScreens-BfamGqS7.js → AdminLoginScreens-B7TQZV8u.js} +1 -1
- package/payload/server/public/assets/{AdminShell-5nHOAzXO.js → AdminShell-DAuri4Yh.js} +1 -1
- package/payload/server/public/assets/{Checkbox-CD6aHwac.js → Checkbox-B1XhnhPk.js} +1 -1
- package/payload/server/public/assets/activity-kRwzxEKU.js +1 -0
- package/payload/server/public/assets/{admin-Cpcw_YE-.js → admin-BdcccSb5.js} +1 -1
- package/payload/server/public/assets/{browser-0znOwk1W.js → browser-B6woQ2qX.js} +1 -1
- package/payload/server/public/assets/{calendar-CykigHdg.js → calendar-CalCeg5T.js} +1 -1
- package/payload/server/public/assets/chat-9s2-L06v.js +1 -0
- package/payload/server/public/assets/chevron-left-C3ho8SnB.js +1 -0
- package/payload/server/public/assets/data-Cfr-7ERI.js +1 -0
- package/payload/server/public/assets/{graph-JV5_xF3V.js → graph-HeZaatSQ.js} +1 -1
- package/payload/server/public/assets/{graph-labels-DPYORjBH.js → graph-labels-4W7JuOgG.js} +1 -1
- package/payload/server/public/assets/{maximize-2-8vKr3h21.js → maximize-2-DKx-aTPb.js} +1 -1
- package/payload/server/public/assets/{operator-Liul1QEq.js → operator-DD9L2VJM.js} +1 -1
- package/payload/server/public/assets/{page-Cg7B1RQx.js → page-Ceado9Kr.js} +1 -1
- package/payload/server/public/assets/{page-CYRA87CB.js → page-tJxw0zNO.js} +1 -1
- package/payload/server/public/assets/{public-B4HMtpjt.js → public-BYFWxyeC.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-sz5IQSnr.js → rotate-ccw-DrzdV9h7.js} +1 -1
- package/payload/server/public/assets/{tasks-D9oYgEzs.js → tasks-DSwoxGzh.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-CSuCexpM.js → time-entry-format-Bn79w6tq.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-ClS81-CU.js → triangle-alert-CjDVh6r-.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-Cn6YgeNx.js → useCopyFeedback-DXHkkJw-.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-BaD-ZZRk.js → useSelectionMode-D2H8jAhE.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-CNZL3PKn.css → useSubAccountSwitcher-Br7Z6Cpu.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-BOzHrDAU.js → useVoiceRecorder-zUvPe55I.js} +1 -1
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +1414 -1324
- package/payload/server/public/assets/activity-Dj2nJzPw.js +0 -1
- package/payload/server/public/assets/chat-DIOJZ411.js +0 -1
- package/payload/server/public/assets/chevron-left-Cq7UvOI0.js +0 -1
- package/payload/server/public/assets/data-lHgqlhmu.js +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-CvjWJSGv.js → useSubAccountSwitcher-CUJpgkKI.js} +0 -0
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authenticated access to Google Calendar API v3.
|
|
3
3
|
*
|
|
4
|
-
* Refresh
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* holds it and exposes POST /api/google/oauth/refresh for this purpose.
|
|
4
|
+
* Refresh happens HERE, direct to Google. The vendor app is a Desktop
|
|
5
|
+
* (installed app) client, whose secret ships in brand config — Google states an
|
|
6
|
+
* installed-app secret "is obviously not treated as a secret" — so there is no
|
|
7
|
+
* server-side holder to delegate to and no reason to route through one.
|
|
9
8
|
*/
|
|
10
9
|
import { TokenStore, type TokenBlob } from "../auth/token-store.js";
|
|
11
10
|
export interface GoogleConfig {
|
|
12
11
|
accountId: string;
|
|
13
12
|
clientId: string;
|
|
13
|
+
/** Ships in brand config alongside the client id. Sanctioned for installed
|
|
14
|
+
* apps; PKCE remains the protection against an intercepted code. */
|
|
15
|
+
clientSecret: string;
|
|
14
16
|
store: TokenStore;
|
|
15
|
-
/** Loopback URL of the platform/ui route that holds the client secret. */
|
|
16
|
-
refreshEndpoint: string;
|
|
17
17
|
}
|
|
18
18
|
export declare function buildQuery(q: Record<string, string | number | undefined>): string;
|
|
19
19
|
export declare function isExpiring(blob: TokenBlob): boolean;
|
|
20
|
+
/** Test seam. Production callers reach this through callCalendar. */
|
|
21
|
+
export declare function accessTokenForTest(config: GoogleConfig, doFetch: typeof fetch): Promise<string>;
|
|
20
22
|
export declare function callCalendar<T>(config: GoogleConfig, path: string, init?: {
|
|
21
23
|
method?: string;
|
|
22
24
|
body?: unknown;
|
|
23
25
|
query?: Record<string, string | number | undefined>;
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
24
27
|
}): Promise<T>;
|
|
25
28
|
/** The primary calendar's id IS the account's email address. Used instead of a
|
|
26
29
|
* userinfo call, because the identity scopes are deliberately not requested. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-client.d.ts","sourceRoot":"","sources":["../../src/lib/google-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"google-client.d.ts","sourceRoot":"","sources":["../../src/lib/google-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,UAAU,EAAwB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAM1F,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;yEACqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAOjF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAEnD;AAwDD,qEAAqE;AACrE,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IACJ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACrB,GACL,OAAO,CAAC,CAAC,CAAC,CA+BZ;AAED;iFACiF;AACjF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAG/E"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authenticated access to Google Calendar API v3.
|
|
3
3
|
*
|
|
4
|
-
* Refresh
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* holds it and exposes POST /api/google/oauth/refresh for this purpose.
|
|
4
|
+
* Refresh happens HERE, direct to Google. The vendor app is a Desktop
|
|
5
|
+
* (installed app) client, whose secret ships in brand config — Google states an
|
|
6
|
+
* installed-app secret "is obviously not treated as a secret" — so there is no
|
|
7
|
+
* server-side holder to delegate to and no reason to route through one.
|
|
9
8
|
*/
|
|
10
9
|
import { REFRESH_THRESHOLD_MS } from "../auth/token-store.js";
|
|
11
10
|
import { log } from "./log.js";
|
|
12
11
|
const CALENDAR_BASE = "https://www.googleapis.com/calendar/v3";
|
|
12
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
13
13
|
export function buildQuery(q) {
|
|
14
14
|
const parts = [];
|
|
15
15
|
for (const [k, v] of Object.entries(q)) {
|
|
@@ -28,14 +28,19 @@ export function isExpiring(blob) {
|
|
|
28
28
|
* cannot cost the account its credentials (Task 1601's lesson applied to the
|
|
29
29
|
* refresh path rather than the write path).
|
|
30
30
|
*/
|
|
31
|
-
async function refresh(config, blob) {
|
|
31
|
+
async function refresh(config, blob, doFetch) {
|
|
32
32
|
if (!blob.refreshToken) {
|
|
33
33
|
throw new Error("no refresh token held for this Google account; run google-account-register");
|
|
34
34
|
}
|
|
35
|
-
const res = await
|
|
35
|
+
const res = await doFetch(TOKEN_URL, {
|
|
36
36
|
method: "POST",
|
|
37
|
-
headers: { "content-type": "application/
|
|
38
|
-
body:
|
|
37
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
38
|
+
body: new URLSearchParams({
|
|
39
|
+
client_id: config.clientId,
|
|
40
|
+
client_secret: config.clientSecret,
|
|
41
|
+
refresh_token: blob.refreshToken,
|
|
42
|
+
grant_type: "refresh_token",
|
|
43
|
+
}),
|
|
39
44
|
});
|
|
40
45
|
const text = await res.text();
|
|
41
46
|
if (!res.ok) {
|
|
@@ -46,6 +51,8 @@ async function refresh(config, blob) {
|
|
|
46
51
|
status: res.status,
|
|
47
52
|
body: text.slice(0, 300),
|
|
48
53
|
});
|
|
54
|
+
// Throwing leaves the store untouched, so a transient outage never costs
|
|
55
|
+
// the account its credentials.
|
|
49
56
|
throw new Error(`token refresh failed (${res.status}): ${text.slice(0, 300)}`);
|
|
50
57
|
}
|
|
51
58
|
const json = JSON.parse(text);
|
|
@@ -55,19 +62,24 @@ async function refresh(config, blob) {
|
|
|
55
62
|
log({ tag: "[google-mcp]", op: "refresh-ok", account: config.accountId, ttlSec: json.expires_in });
|
|
56
63
|
return next;
|
|
57
64
|
}
|
|
58
|
-
async function accessToken(config) {
|
|
65
|
+
async function accessToken(config, doFetch) {
|
|
59
66
|
const blob = config.store.read();
|
|
60
67
|
if (!blob) {
|
|
61
68
|
throw new Error("no Google account connected for this account; run google-account-register");
|
|
62
69
|
}
|
|
63
70
|
if (!isExpiring(blob))
|
|
64
71
|
return blob.accessToken;
|
|
65
|
-
return (await refresh(config, blob)).accessToken;
|
|
72
|
+
return (await refresh(config, blob, doFetch)).accessToken;
|
|
73
|
+
}
|
|
74
|
+
/** Test seam. Production callers reach this through callCalendar. */
|
|
75
|
+
export async function accessTokenForTest(config, doFetch) {
|
|
76
|
+
return accessToken(config, doFetch);
|
|
66
77
|
}
|
|
67
78
|
export async function callCalendar(config, path, init = {}) {
|
|
68
|
-
const
|
|
79
|
+
const doFetch = init.fetchImpl ?? fetch;
|
|
80
|
+
const token = await accessToken(config, doFetch);
|
|
69
81
|
const url = `${CALENDAR_BASE}${path}${buildQuery(init.query ?? {})}`;
|
|
70
|
-
const res = await
|
|
82
|
+
const res = await doFetch(url, {
|
|
71
83
|
method: init.method ?? "GET",
|
|
72
84
|
headers: {
|
|
73
85
|
authorization: `Bearer ${token}`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-client.js","sourceRoot":"","sources":["../../src/lib/google-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"google-client.js","sourceRoot":"","sources":["../../src/lib/google-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,26 +6,32 @@ export declare function newPkce(): {
|
|
|
6
6
|
verifier: string;
|
|
7
7
|
challenge: string;
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
9
|
+
/** Desktop (installed app) clients permit ONLY loopback redirects. https is
|
|
10
|
+
* rejected outright and custom URI schemes are withdrawn, so this is the whole
|
|
11
|
+
* option space.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* names `state` as the mechanism for per-request application state.
|
|
13
|
+
* The IP literal is mandatory. RFC 8252 s7.3 makes the port irrelevant when
|
|
14
|
+
* matching a loopback redirect, but only for `127.0.0.1` / `::1` — NOT for the
|
|
15
|
+
* hostname `localhost`, which strict servers match literally.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
export declare
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
* Nothing listens on this port, by design: the consenting browser is on the
|
|
18
|
+
* user's own phone, so the redirect fails to load and the user copies the code
|
|
19
|
+
* out of the address bar. The value is fixed so the consent URL is
|
|
20
|
+
* deterministic and one register's logs compare against another's. */
|
|
21
|
+
export declare const LOOPBACK_REDIRECT_URI = "http://127.0.0.1:8571";
|
|
22
|
+
/** `state` is an opaque CSRF nonce and nothing more.
|
|
23
|
+
*
|
|
24
|
+
* It deliberately does NOT carry the account id. Under the Desktop flow the
|
|
25
|
+
* pending entry is keyed on the spawn's own ACCOUNT_ID, so submit can only ever
|
|
26
|
+
* read its own account's pending, and PKCE binds the code to that entry's
|
|
27
|
+
* verifier. Routing information in `state` would therefore buy nothing, while
|
|
28
|
+
* putting an account uuid into a URL opened on a personal phone, stored in
|
|
29
|
+
* browser history and Google's logs, and pasted back through chat.
|
|
30
|
+
*
|
|
31
|
+
* (Task 1761 encoded `<accountId>.<nonce>` because a shared hosted callback had
|
|
32
|
+
* to route an inbound request to an account. That callback is gone.) */
|
|
26
33
|
export declare function buildConsentUrl(args: {
|
|
27
34
|
clientId: string;
|
|
28
|
-
redirectUri: string;
|
|
29
35
|
state: string;
|
|
30
36
|
challenge: string;
|
|
31
37
|
accountId: string;
|
|
@@ -42,7 +48,6 @@ export declare function runAccountRegisterStart(args: {
|
|
|
42
48
|
accountId: string;
|
|
43
49
|
accountsDir: string;
|
|
44
50
|
clientId: string;
|
|
45
|
-
publicBase: string;
|
|
46
51
|
}): Promise<{
|
|
47
52
|
status: "pending" | "in-progress";
|
|
48
53
|
consentUrl?: string;
|
|
@@ -56,4 +61,40 @@ export declare function runAccountRegisterPoll(args: {
|
|
|
56
61
|
accounts: ConnectedAccount[];
|
|
57
62
|
ageSec?: number;
|
|
58
63
|
}>;
|
|
64
|
+
/** Users paste what they can see, and what they can see is Google's loopback
|
|
65
|
+
* redirect — which ALWAYS carries `scope` alongside `code`, and often
|
|
66
|
+
* `authuser` and `prompt` too. So the realistic pastes are a whole URL or a
|
|
67
|
+
* `code=…&scope=…` fragment, not a bare value.
|
|
68
|
+
*
|
|
69
|
+
* Extracting `code` properly matters because a mangled value reaches Google as
|
|
70
|
+
* an opaque string and comes back `invalid_grant`, which reads to the user as
|
|
71
|
+
* "your code expired" and sends them round the consent loop again. It never
|
|
72
|
+
* says the paste was the problem.
|
|
73
|
+
*
|
|
74
|
+
* A malformed percent escape must not throw either: `decodeURIComponent` raises
|
|
75
|
+
* URIError on a stray `%`, and surfacing that instead of Google's own error
|
|
76
|
+
* would be our parser blaming the user for a value Google would have explained.
|
|
77
|
+
*/
|
|
78
|
+
export declare function normalisePastedCode(raw: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Finish a registration from the code the user pasted back.
|
|
81
|
+
*
|
|
82
|
+
* The exchange happens HERE, in the plugin, because a Desktop client's secret
|
|
83
|
+
* ships in brand config — there is no server-side holder to delegate to. The
|
|
84
|
+
* redirect_uri sent here is the same constant the consent URL carried; Google
|
|
85
|
+
* compares them byte for byte.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runAccountRegisterSubmit(args: {
|
|
88
|
+
accountId: string;
|
|
89
|
+
accountsDir: string;
|
|
90
|
+
clientId: string;
|
|
91
|
+
clientSecret: string;
|
|
92
|
+
code: string;
|
|
93
|
+
fetchImpl?: typeof fetch;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
status: "registered";
|
|
96
|
+
email: string;
|
|
97
|
+
refreshTokenIssued: boolean;
|
|
98
|
+
warning?: string;
|
|
99
|
+
}>;
|
|
59
100
|
//# sourceMappingURL=account-register.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-register.d.ts","sourceRoot":"","sources":["../../src/tools/account-register.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"account-register.d.ts","sourceRoot":"","sources":["../../src/tools/account-register.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,cAAc,6CAA6C,CAAC;AAKzE;qDACqD;AACrD,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,wBAAgB,OAAO,IAAI;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAGjE;AAED;;;;;;;;;;;uEAWuE;AACvE,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAE7D;;;;;;;;;;yEAUyE;AACzE,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAaT;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,IAAI,GAAG,YAAY,CAAC;CAC7B;AAED;;6DAE6D;AAC7D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAaxF;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,GAAG,aAAa,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BvF;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IACV,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtD,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAUD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAoBvD;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0ElG"}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Register flow: authorization-code + PKCE.
|
|
3
3
|
*
|
|
4
4
|
* `runAccountRegisterStart` mints the PKCE pair and `state`, persists them, and
|
|
5
|
-
* returns a consent URL the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* returns a consent URL the user opens on any device. Google redirects to a
|
|
6
|
+
* loopback address nothing listens on, so the page fails to load and the user
|
|
7
|
+
* copies the code from the address bar; `runAccountRegisterSubmit` exchanges it
|
|
8
|
+
* here, in-process, using the brand-config client secret.
|
|
9
|
+
* `runAccountRegisterPoll` reports the outcome from local state only.
|
|
9
10
|
*/
|
|
10
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
12
|
import { existsSync, readdirSync } from "node:fs";
|
|
@@ -15,6 +16,8 @@ import { TokenStore } from "../auth/token-store.js";
|
|
|
15
16
|
import { log } from "../lib/log.js";
|
|
16
17
|
export const CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar";
|
|
17
18
|
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
19
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
20
|
+
const PRIMARY_CALENDAR_URL = "https://www.googleapis.com/calendar/v3/calendars/primary";
|
|
18
21
|
/** Google refuses an authorization code older than roughly ten minutes; a
|
|
19
22
|
* pending entry past that is stranded, not live. */
|
|
20
23
|
export const PENDING_LIFETIME_SEC = 600;
|
|
@@ -22,33 +25,34 @@ export function newPkce() {
|
|
|
22
25
|
const verifier = randomBytes(48).toString("base64url");
|
|
23
26
|
return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
|
|
24
27
|
}
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
28
|
+
/** Desktop (installed app) clients permit ONLY loopback redirects. https is
|
|
29
|
+
* rejected outright and custom URI schemes are withdrawn, so this is the whole
|
|
30
|
+
* option space.
|
|
27
31
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* names `state` as the mechanism for per-request application state.
|
|
32
|
+
* The IP literal is mandatory. RFC 8252 s7.3 makes the port irrelevant when
|
|
33
|
+
* matching a loopback redirect, but only for `127.0.0.1` / `::1` — NOT for the
|
|
34
|
+
* hostname `localhost`, which strict servers match literally.
|
|
32
35
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
* Nothing listens on this port, by design: the consenting browser is on the
|
|
37
|
+
* user's own phone, so the redirect fails to load and the user copies the code
|
|
38
|
+
* out of the address bar. The value is fixed so the consent URL is
|
|
39
|
+
* deterministic and one register's logs compare against another's. */
|
|
40
|
+
export const LOOPBACK_REDIRECT_URI = "http://127.0.0.1:8571";
|
|
41
|
+
/** `state` is an opaque CSRF nonce and nothing more.
|
|
42
|
+
*
|
|
43
|
+
* It deliberately does NOT carry the account id. Under the Desktop flow the
|
|
44
|
+
* pending entry is keyed on the spawn's own ACCOUNT_ID, so submit can only ever
|
|
45
|
+
* read its own account's pending, and PKCE binds the code to that entry's
|
|
46
|
+
* verifier. Routing information in `state` would therefore buy nothing, while
|
|
47
|
+
* putting an account uuid into a URL opened on a personal phone, stored in
|
|
48
|
+
* browser history and Google's logs, and pasted back through chat.
|
|
49
|
+
*
|
|
50
|
+
* (Task 1761 encoded `<accountId>.<nonce>` because a shared hosted callback had
|
|
51
|
+
* to route an inbound request to an account. That callback is gone.) */
|
|
46
52
|
export function buildConsentUrl(args) {
|
|
47
|
-
// redirect_uri is sent VERBATIM — no query string. See encodeState above.
|
|
48
|
-
const redirect = args.redirectUri;
|
|
49
53
|
const q = new URLSearchParams({
|
|
50
54
|
client_id: args.clientId,
|
|
51
|
-
redirect_uri:
|
|
55
|
+
redirect_uri: LOOPBACK_REDIRECT_URI,
|
|
52
56
|
response_type: "code",
|
|
53
57
|
scope: CALENDAR_SCOPE,
|
|
54
58
|
access_type: "offline",
|
|
@@ -91,24 +95,18 @@ export async function runAccountRegisterStart(args) {
|
|
|
91
95
|
});
|
|
92
96
|
return { status: "in-progress", ageSec: age };
|
|
93
97
|
}
|
|
94
|
-
const
|
|
95
|
-
const state = encodeState(args.accountId, nonce);
|
|
98
|
+
const state = randomBytes(16).toString("hex");
|
|
96
99
|
const { verifier, challenge } = newPkce();
|
|
97
|
-
log({ tag: "[google-oauth]", op: "register", account: args.accountId, reg:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// alone keeps the check independent of the encoding.
|
|
101
|
-
pending.write({ state: nonce, verifier, createdAt: Date.now() });
|
|
102
|
-
log({ tag: "[google-oauth]", op: "pending-write", reg: nonce, account: args.accountId });
|
|
103
|
-
const redirectUri = `${args.publicBase}/api/google/oauth/callback`;
|
|
100
|
+
log({ tag: "[google-oauth]", op: "register", account: args.accountId, reg: state });
|
|
101
|
+
pending.write({ state, verifier, createdAt: Date.now() });
|
|
102
|
+
log({ tag: "[google-oauth]", op: "pending-write", reg: state, account: args.accountId });
|
|
104
103
|
const consentUrl = buildConsentUrl({
|
|
105
104
|
clientId: args.clientId,
|
|
106
|
-
redirectUri,
|
|
107
105
|
state,
|
|
108
106
|
challenge,
|
|
109
107
|
accountId: args.accountId,
|
|
110
108
|
});
|
|
111
|
-
log({ tag: "[google-oauth]", op: "consent-url", reg:
|
|
109
|
+
log({ tag: "[google-oauth]", op: "consent-url", reg: state, scopes: CALENDAR_SCOPE, redirect: LOOPBACK_REDIRECT_URI });
|
|
112
110
|
return { status: "pending", consentUrl };
|
|
113
111
|
}
|
|
114
112
|
export async function runAccountRegisterPoll(args) {
|
|
@@ -123,4 +121,112 @@ export async function runAccountRegisterPoll(args) {
|
|
|
123
121
|
}
|
|
124
122
|
return { status: "pending", accounts, ageSec: age };
|
|
125
123
|
}
|
|
124
|
+
/** Users paste what they can see, and what they can see is Google's loopback
|
|
125
|
+
* redirect — which ALWAYS carries `scope` alongside `code`, and often
|
|
126
|
+
* `authuser` and `prompt` too. So the realistic pastes are a whole URL or a
|
|
127
|
+
* `code=…&scope=…` fragment, not a bare value.
|
|
128
|
+
*
|
|
129
|
+
* Extracting `code` properly matters because a mangled value reaches Google as
|
|
130
|
+
* an opaque string and comes back `invalid_grant`, which reads to the user as
|
|
131
|
+
* "your code expired" and sends them round the consent loop again. It never
|
|
132
|
+
* says the paste was the problem.
|
|
133
|
+
*
|
|
134
|
+
* A malformed percent escape must not throw either: `decodeURIComponent` raises
|
|
135
|
+
* URIError on a stray `%`, and surfacing that instead of Google's own error
|
|
136
|
+
* would be our parser blaming the user for a value Google would have explained.
|
|
137
|
+
*/
|
|
138
|
+
export function normalisePastedCode(raw) {
|
|
139
|
+
const s = raw.trim();
|
|
140
|
+
// A full URL, or a query fragment carrying more than one parameter.
|
|
141
|
+
if (s.includes("://") || s.startsWith("?") || s.includes("&")) {
|
|
142
|
+
const query = s.slice(s.indexOf("?") >= 0 ? s.indexOf("?") + 1 : 0);
|
|
143
|
+
const fromQuery = new URLSearchParams(query).get("code");
|
|
144
|
+
if (fromQuery)
|
|
145
|
+
return fromQuery.trim();
|
|
146
|
+
}
|
|
147
|
+
let out = s;
|
|
148
|
+
if (out.startsWith("code="))
|
|
149
|
+
out = out.slice("code=".length);
|
|
150
|
+
if (out.includes("%")) {
|
|
151
|
+
try {
|
|
152
|
+
out = decodeURIComponent(out);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Leave it as pasted; Google's error is more useful than ours.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out.trim();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Finish a registration from the code the user pasted back.
|
|
162
|
+
*
|
|
163
|
+
* The exchange happens HERE, in the plugin, because a Desktop client's secret
|
|
164
|
+
* ships in brand config — there is no server-side holder to delegate to. The
|
|
165
|
+
* redirect_uri sent here is the same constant the consent URL carried; Google
|
|
166
|
+
* compares them byte for byte.
|
|
167
|
+
*/
|
|
168
|
+
export async function runAccountRegisterSubmit(args) {
|
|
169
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
170
|
+
const pending = new PendingStore(args.accountId, args.accountsDir);
|
|
171
|
+
const held = pending.read();
|
|
172
|
+
if (!held) {
|
|
173
|
+
throw new Error("no registration in progress; run google-account-register first");
|
|
174
|
+
}
|
|
175
|
+
const code = normalisePastedCode(args.code);
|
|
176
|
+
log({ tag: "[google-oauth]", op: "submit", account: args.accountId, reg: held.state });
|
|
177
|
+
const res = await doFetch(TOKEN_URL, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
180
|
+
body: new URLSearchParams({
|
|
181
|
+
code,
|
|
182
|
+
client_id: args.clientId,
|
|
183
|
+
client_secret: args.clientSecret,
|
|
184
|
+
redirect_uri: LOOPBACK_REDIRECT_URI,
|
|
185
|
+
grant_type: "authorization_code",
|
|
186
|
+
code_verifier: held.verifier,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
const text = await res.text();
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
// Never discard the body: invalid_grant means expired or already used,
|
|
192
|
+
// redirect_uri_mismatch means the sent and registered redirects diverged.
|
|
193
|
+
log({
|
|
194
|
+
tag: "[google-oauth]", op: "submit", reg: held.state,
|
|
195
|
+
outcome: "error", status: res.status, body: text.slice(0, 300),
|
|
196
|
+
});
|
|
197
|
+
throw new Error(`token exchange failed (${res.status}): ${text.slice(0, 300)}`);
|
|
198
|
+
}
|
|
199
|
+
const tok = JSON.parse(text);
|
|
200
|
+
// Identity comes from the primary calendar's id, which IS the address, so the
|
|
201
|
+
// identity scopes are never requested.
|
|
202
|
+
const calRes = await doFetch(PRIMARY_CALENDAR_URL, {
|
|
203
|
+
headers: { authorization: `Bearer ${tok.access_token}` },
|
|
204
|
+
});
|
|
205
|
+
const calText = await calRes.text();
|
|
206
|
+
if (!calRes.ok) {
|
|
207
|
+
throw new Error(`identity lookup failed (${calRes.status}): ${calText.slice(0, 200)}`);
|
|
208
|
+
}
|
|
209
|
+
const email = JSON.parse(calText).id;
|
|
210
|
+
const stored = new TokenStore(args.accountId, args.accountsDir, email).store(tok.access_token, tok.refresh_token ?? null, tok.expires_in, { email, scopes: tok.scope ? tok.scope.split(" ") : [] });
|
|
211
|
+
log({
|
|
212
|
+
tag: "[google-oauth]", op: "tokens-write", reg: held.state,
|
|
213
|
+
account: args.accountId, refreshIssued: tok.refresh_token ? "true" : "false",
|
|
214
|
+
});
|
|
215
|
+
pending.clear();
|
|
216
|
+
log({ tag: "[google-oauth]", op: "registered", reg: held.state, account: args.accountId, email });
|
|
217
|
+
// A connection with no refresh token works for about an hour and then dies.
|
|
218
|
+
// Reporting only in the log would tell the operator "connected" and leave the
|
|
219
|
+
// failure to surface later as an unexplained loss of access.
|
|
220
|
+
const refreshTokenIssued = Boolean(stored.refreshToken);
|
|
221
|
+
return {
|
|
222
|
+
status: "registered",
|
|
223
|
+
email,
|
|
224
|
+
refreshTokenIssued,
|
|
225
|
+
...(refreshTokenIssued
|
|
226
|
+
? {}
|
|
227
|
+
: {
|
|
228
|
+
warning: "Google returned no refresh token, so this connection will stop working when the access token expires in about an hour. Re-run google-account-register to reconnect.",
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
126
232
|
//# sourceMappingURL=account-register.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-register.js","sourceRoot":"","sources":["../../src/tools/account-register.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"account-register.js","sourceRoot":"","sources":["../../src/tools/account-register.ts"],"names":[],"mappings":"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"}
|