granttap-mcp 0.8.38 → 0.8.39

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.
@@ -37,7 +37,7 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
37
37
  <dt>Last encrypted message from iPhone</dt><dd id="phone">Not observed</dd></dl>
38
38
  <h3>Provider readiness</h3><ul id="providers"></ul>
39
39
  <p>QR pairs a phone directly. A passkey links this Mac to your GrantTap account so your signed-in phone can discover its computers. Provider sign-in is separate.</p>
40
- <p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.38</code> and <code>granttap setup</code> locally.
40
+ <p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.39</code> and <code>granttap setup</code> locally.
41
41
  A saved pairing alone does not prove your phone or the background helper is online.</p>
42
42
  <p><a href="https://granttap.com" target="_blank" rel="noopener noreferrer">Website</a> ·
43
43
  <a href="https://granttap.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> ·
@@ -10,6 +10,9 @@ may send an opaque Origin. The callback is fixed to
10
10
  and codes expire after five minutes. A private file stores only token hashes,
11
11
  up to eight active grants, expiring after 30 days. The app keeps its token in
12
12
  its device-only Keychain and never shares it with a provider.
13
+ The fallback consent page is styled and localized by the loopback service;
14
+ the stylesheet has no script or external dependency. A Mac app with a verified
15
+ passkey account uses `/desktop/account/authorize` instead of opening that page.
13
16
 
14
17
  `/desktop/invoke` requires that grant before parsing a bounded request. It can
15
18
  invoke only the existing allowlisted desktop operations; inputs are still
@@ -0,0 +1,43 @@
1
+ const copy = {
2
+ en: {
3
+ heading: 'Allow GrantTap on this Mac',
4
+ detail: 'GrantTap for Mac needs access to the local GrantTap service to show your tasks, send messages, handle approvals, and configure the relay.',
5
+ scope: 'This approval applies only to this Mac app on this computer. It does not authorize another computer or a coding provider.',
6
+ action: 'Allow this Mac app',
7
+ expires: 'The authorization request expires after five minutes.',
8
+ },
9
+ ru: {
10
+ heading: 'Разрешить GrantTap на этом Mac',
11
+ detail: 'Приложению GrantTap для Mac нужен доступ к локальному сервису, чтобы показывать задачи, отправлять сообщения, обрабатывать одобрения и настраивать relay.',
12
+ scope: 'Разрешение действует только для приложения на этом Mac. Оно не открывает доступ другому компьютеру или агенту.',
13
+ action: 'Разрешить на этом Mac',
14
+ expires: 'Запрос разрешения истечёт через пять минут.',
15
+ },
16
+ } as const;
17
+
18
+ type Consent = { id: string; confirmation: string; state: string };
19
+
20
+ export function desktopConsentPage(row: Consent, language: string | undefined): string {
21
+ const locale = language?.toLowerCase().startsWith('ru') ? 'ru' : 'en';
22
+ const words = copy[locale];
23
+ return `<!doctype html><html lang="${locale}"><head><meta charset="utf-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1">
25
+ <meta name="color-scheme" content="light dark"><title>GrantTap for Mac</title>
26
+ <link rel="stylesheet" href="/desktop/consent.css"></head><body>
27
+ <main class="consent-card"><div class="brand"><span class="brand-mark" aria-hidden="true">✓</span><span>GrantTap</span></div>
28
+ <h1>${words.heading}</h1><p>${words.detail}</p><p class="scope">${words.scope}</p>
29
+ <form action="/desktop/approve" method="post"><input type="hidden" name="id" value="${row.id}">
30
+ <input type="hidden" name="confirmation" value="${row.confirmation}">
31
+ <input type="hidden" name="state" value="${row.state}"><button type="submit">${words.action}</button></form>
32
+ <p class="footnote">${words.expires}</p></main></body></html>`;
33
+ }
34
+
35
+ export const desktopConsentStyles = `:root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color-scheme:light dark}
36
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f4f1ed;color:#211f1d}
37
+ .consent-card{width:min(100%,480px);padding:32px;border:1px solid #dfd8d0;border-radius:24px;background:#fff;box-shadow:0 16px 48px #20170e18}
38
+ .brand{display:flex;align-items:center;gap:10px;font-size:19px;font-weight:750}.brand-mark{display:grid;place-items:center;width:38px;height:38px;border-radius:10px;background:#f17b39;color:#17110e;font-size:26px}
39
+ h1{margin:32px 0 14px;font-size:27px;line-height:1.2;letter-spacing:-.025em}p{margin:0 0 18px;line-height:1.5;color:#56504a}
40
+ .scope{padding:15px 16px;border-radius:12px;background:#f4f1ed;color:#38332e}button{display:block;width:100%;margin-top:24px;padding:14px 18px;border:0;border-radius:12px;background:#ca4e24;color:white;font:inherit;font-weight:700;cursor:pointer}
41
+ button:hover{background:#a93c1b}button:focus-visible{outline:3px solid #2171d1;outline-offset:3px}.footnote{margin:18px 0 0;text-align:center;font-size:13px;color:#817a73}
42
+ @media(prefers-color-scheme:dark){body{background:#101114;color:#f5f2ef}.consent-card{background:#22252a;border-color:#3b3e45;box-shadow:0 16px 48px #0007}p{color:#bdb8b2}.scope{background:#30343a;color:#e0dad3}.footnote{color:#a39e98}}
43
+ `;
@@ -5,23 +5,23 @@ import { invokeDesktop } from './invoke';
5
5
  import { DESKTOP_OPERATIONS, desktopOperationTimeout } from '../engine-bridge';
6
6
  import { enrollMacAccount } from '../../../../bridge/src/account-recovery/enroll';
7
7
  import { websiteOrigin } from '../../oauth/consent/website-session';
8
+ import { desktopConsentPage, desktopConsentStyles } from './consent-page';
8
9
 
9
10
  export function installDesktopNativeRoutes(app: Express, socketPath: string, origin: string,
10
11
  access = new DesktopNativeAccess(), enroll = enrollMacAccount): void {
11
12
  app.use('/desktop', (_req, res, next) => {
12
13
  res.set({ 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer',
13
- 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'none'; form-action 'self'; frame-ancestors 'none'" });
14
+ 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'none'; style-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" });
14
15
  next();
15
16
  });
17
+ app.get('/desktop/consent.css', (_req, res) => res.type('css').send(desktopConsentStyles));
16
18
  app.get('/desktop/authorize', (req, res) => {
17
19
  try {
18
20
  const row = access.begin(String(req.query.challenge ?? ''), String(req.query.state ?? ''));
19
21
  res.cookie('granttap_desktop_consent', row.confirmation, {
20
22
  httpOnly: true, sameSite: 'strict', path: '/desktop/approve', maxAge: 300_000,
21
23
  });
22
- res.type('html').send(`<!doctype html><html lang="en"><meta charset="utf-8"><title>GrantTap for Mac</title>
23
- <h1>Allow GrantTap for Mac on this computer</h1><p>The app can inspect tasks, send your messages, handle approvals and configure your local relay. This does not authorize another computer or a coding provider.</p>
24
- <form action="/desktop/approve" method="post"><input type="hidden" name="id" value="${row.id}"><input type="hidden" name="confirmation" value="${row.confirmation}"><input type="hidden" name="state" value="${row.state}"><button type="submit">Allow this Mac app</button></form></html>`);
24
+ res.type('html').send(desktopConsentPage(row, req.headers['accept-language']));
25
25
  } catch { res.status(400).json({ error: 'Invalid native authorization' }); }
26
26
  });
27
27
  app.post('/desktop/approve', (req, res) => {
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
10
10
  Install the supported CLI on this computer, then run:
11
11
 
12
12
  ```bash
13
- npm install -g granttap-mcp@0.8.38
13
+ npm install -g granttap-mcp@0.8.39
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -4,7 +4,7 @@
4
4
  "command": "node",
5
5
  "args": [
6
6
  "-e",
7
- "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.38\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
7
+ "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.39\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
8
8
  ]
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
7
7
  const { existsSync } = require("node:fs");
8
8
  const { delimiter, dirname, join } = require("node:path");
9
9
 
10
- const PACKAGE = "granttap-mcp@0.8.38";
10
+ const PACKAGE = "granttap-mcp@0.8.39";
11
11
  const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
12
12
  const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
13
13
  ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.38",
3
+ "version": "0.8.39",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {