aimeat 1.26.0 → 1.27.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.
Files changed (167) hide show
  1. package/README.md +7 -0
  2. package/dist/.env.example +17 -0
  3. package/dist/locales/en.json +49 -7
  4. package/dist/locales/fi.json +56 -14
  5. package/dist/public/css/components/app-sandbox.css +62 -0
  6. package/dist/public/css/views/app-grant.css +51 -0
  7. package/dist/public/css/views/landing.css +8 -2
  8. package/dist/public/js/app-sandbox.js +83 -0
  9. package/dist/public/spa.html +16 -1
  10. package/dist/public/views/app-grant.js +107 -0
  11. package/dist/public/views/business.js +8 -1
  12. package/dist/public/views/how-it-works.js +5 -1
  13. package/dist/public/views/landing.js +83 -76
  14. package/dist/public/views/profile/access-tab.js +75 -0
  15. package/dist/scripts/screenshot-worker.d.ts +2 -0
  16. package/dist/scripts/screenshot-worker.d.ts.map +1 -0
  17. package/dist/scripts/screenshot-worker.js +14 -0
  18. package/dist/scripts/screenshot-worker.js.map +1 -0
  19. package/dist/src/auth/jwt.d.ts +2 -0
  20. package/dist/src/auth/jwt.d.ts.map +1 -1
  21. package/dist/src/auth/jwt.js +2 -0
  22. package/dist/src/auth/jwt.js.map +1 -1
  23. package/dist/src/auth/middleware.d.ts.map +1 -1
  24. package/dist/src/auth/middleware.js +6 -2
  25. package/dist/src/auth/middleware.js.map +1 -1
  26. package/dist/src/cli/init-wizard.d.ts +7 -3
  27. package/dist/src/cli/init-wizard.d.ts.map +1 -1
  28. package/dist/src/cli/init-wizard.js +50 -5
  29. package/dist/src/cli/init-wizard.js.map +1 -1
  30. package/dist/src/cli/screenshot-worker.d.ts +2 -0
  31. package/dist/src/cli/screenshot-worker.d.ts.map +1 -0
  32. package/dist/src/cli/screenshot-worker.js +156 -0
  33. package/dist/src/cli/screenshot-worker.js.map +1 -0
  34. package/dist/src/config.d.ts +17 -0
  35. package/dist/src/config.d.ts.map +1 -1
  36. package/dist/src/config.js +25 -0
  37. package/dist/src/config.js.map +1 -1
  38. package/dist/src/generated/api-types.d.ts +529 -0
  39. package/dist/src/generated/api-types.d.ts.map +1 -1
  40. package/dist/src/generated/prisma-postgres/edge.js +20 -3
  41. package/dist/src/generated/prisma-postgres/index-browser.js +17 -0
  42. package/dist/src/generated/prisma-postgres/index.d.ts +1618 -154
  43. package/dist/src/generated/prisma-postgres/index.js +20 -3
  44. package/dist/src/generated/prisma-postgres/package.json +1 -1
  45. package/dist/src/generated/prisma-postgres/schema.prisma +20 -0
  46. package/dist/src/generated/prisma-postgres/wasm.js +20 -3
  47. package/dist/src/index.js +8 -0
  48. package/dist/src/index.js.map +1 -1
  49. package/dist/src/mcp/apps.d.ts +2 -0
  50. package/dist/src/mcp/apps.d.ts.map +1 -1
  51. package/dist/src/mcp/apps.js +18 -1
  52. package/dist/src/mcp/apps.js.map +1 -1
  53. package/dist/src/mcp/extensions.d.ts +1 -0
  54. package/dist/src/mcp/extensions.d.ts.map +1 -1
  55. package/dist/src/mcp/extensions.js +10 -1
  56. package/dist/src/mcp/extensions.js.map +1 -1
  57. package/dist/src/middleware/rate-limit.d.ts +10 -0
  58. package/dist/src/middleware/rate-limit.d.ts.map +1 -1
  59. package/dist/src/middleware/rate-limit.js +24 -4
  60. package/dist/src/middleware/rate-limit.js.map +1 -1
  61. package/dist/src/middleware/subdomain.d.ts +15 -3
  62. package/dist/src/middleware/subdomain.d.ts.map +1 -1
  63. package/dist/src/middleware/subdomain.js +35 -12
  64. package/dist/src/middleware/subdomain.js.map +1 -1
  65. package/dist/src/routes/admin.js +4 -2
  66. package/dist/src/routes/admin.js.map +1 -1
  67. package/dist/src/routes/agent-webhook.d.ts.map +1 -1
  68. package/dist/src/routes/agent-webhook.js +3 -2
  69. package/dist/src/routes/agent-webhook.js.map +1 -1
  70. package/dist/src/routes/app-grants.d.ts +32 -0
  71. package/dist/src/routes/app-grants.d.ts.map +1 -0
  72. package/dist/src/routes/app-grants.js +257 -0
  73. package/dist/src/routes/app-grants.js.map +1 -0
  74. package/dist/src/routes/apps.d.ts +9 -0
  75. package/dist/src/routes/apps.d.ts.map +1 -1
  76. package/dist/src/routes/apps.js +127 -2
  77. package/dist/src/routes/apps.js.map +1 -1
  78. package/dist/src/routes/extensions.d.ts +2 -0
  79. package/dist/src/routes/extensions.d.ts.map +1 -1
  80. package/dist/src/routes/extensions.js +18 -9
  81. package/dist/src/routes/extensions.js.map +1 -1
  82. package/dist/src/routes/foundry.d.ts.map +1 -1
  83. package/dist/src/routes/foundry.js +13 -1
  84. package/dist/src/routes/foundry.js.map +1 -1
  85. package/dist/src/routes/generator.d.ts.map +1 -1
  86. package/dist/src/routes/generator.js +13 -1
  87. package/dist/src/routes/generator.js.map +1 -1
  88. package/dist/src/routes/libs.d.ts +6 -0
  89. package/dist/src/routes/libs.d.ts.map +1 -1
  90. package/dist/src/routes/libs.js +40 -0
  91. package/dist/src/routes/libs.js.map +1 -1
  92. package/dist/src/routes/oauth-login.d.ts +22 -0
  93. package/dist/src/routes/oauth-login.d.ts.map +1 -0
  94. package/dist/src/routes/oauth-login.js +219 -0
  95. package/dist/src/routes/oauth-login.js.map +1 -0
  96. package/dist/src/routes/portal.d.ts.map +1 -1
  97. package/dist/src/routes/portal.js +1 -0
  98. package/dist/src/routes/portal.js.map +1 -1
  99. package/dist/src/routes/subdomains.d.ts +5 -0
  100. package/dist/src/routes/subdomains.d.ts.map +1 -1
  101. package/dist/src/routes/subdomains.js +55 -9
  102. package/dist/src/routes/subdomains.js.map +1 -1
  103. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  104. package/dist/src/server-bootstrap/routes-loader.js +16 -0
  105. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  106. package/dist/src/server-bootstrap/static-files.d.ts +3 -0
  107. package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
  108. package/dist/src/server-bootstrap/static-files.js +19 -2
  109. package/dist/src/server-bootstrap/static-files.js.map +1 -1
  110. package/dist/src/server.d.ts.map +1 -1
  111. package/dist/src/server.js +5 -0
  112. package/dist/src/server.js.map +1 -1
  113. package/dist/src/services/hooks.js +2 -2
  114. package/dist/src/services/hooks.js.map +1 -1
  115. package/dist/src/services/oidc-client.d.ts +3 -0
  116. package/dist/src/services/oidc-client.d.ts.map +1 -1
  117. package/dist/src/services/oidc-client.js +17 -2
  118. package/dist/src/services/oidc-client.js.map +1 -1
  119. package/dist/src/services/quota.d.ts +14 -1
  120. package/dist/src/services/quota.d.ts.map +1 -1
  121. package/dist/src/services/quota.js +28 -1
  122. package/dist/src/services/quota.js.map +1 -1
  123. package/dist/src/services/webhook-dispatcher.d.ts.map +1 -1
  124. package/dist/src/services/webhook-dispatcher.js +3 -2
  125. package/dist/src/services/webhook-dispatcher.js.map +1 -1
  126. package/dist/src/storage/interface.d.ts +17 -2
  127. package/dist/src/storage/interface.d.ts.map +1 -1
  128. package/dist/src/storage/interface.js.map +1 -1
  129. package/dist/src/storage/providers/mongodb/index.d.ts +15 -1
  130. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  131. package/dist/src/storage/providers/mongodb/index.js +106 -0
  132. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  133. package/dist/src/storage/providers/sqlite/index.d.ts +9 -1
  134. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  135. package/dist/src/storage/providers/sqlite/index.js +97 -4
  136. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  137. package/dist/src/storage/providers/sqlite/repos/identity.d.ts +1 -0
  138. package/dist/src/storage/providers/sqlite/repos/identity.d.ts.map +1 -1
  139. package/dist/src/storage/providers/sqlite/repos/identity.js +10 -4
  140. package/dist/src/storage/providers/sqlite/repos/identity.js.map +1 -1
  141. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  142. package/dist/src/storage/providers/sqlite/schema.js +21 -0
  143. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  144. package/dist/src/storage/repositories/app-grant.repository.d.ts +22 -0
  145. package/dist/src/storage/repositories/app-grant.repository.d.ts.map +1 -0
  146. package/dist/src/storage/repositories/app-grant.repository.js +2 -0
  147. package/dist/src/storage/repositories/app-grant.repository.js.map +1 -0
  148. package/dist/src/storage/repositories/identity.repository.d.ts +1 -0
  149. package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
  150. package/dist/src/storage/repositories/index.d.ts +1 -0
  151. package/dist/src/storage/repositories/index.d.ts.map +1 -1
  152. package/dist/src/utils/env-config.d.ts +5 -2
  153. package/dist/src/utils/env-config.d.ts.map +1 -1
  154. package/dist/src/utils/env-config.js +45 -2
  155. package/dist/src/utils/env-config.js.map +1 -1
  156. package/dist/src/utils/env-validator.d.ts +6 -3
  157. package/dist/src/utils/env-validator.d.ts.map +1 -1
  158. package/dist/src/utils/env-validator.js +14 -3
  159. package/dist/src/utils/env-validator.js.map +1 -1
  160. package/dist/src/utils/url-validator.d.ts +15 -0
  161. package/dist/src/utils/url-validator.d.ts.map +1 -1
  162. package/dist/src/utils/url-validator.js +120 -30
  163. package/dist/src/utils/url-validator.js.map +1 -1
  164. package/dist/static/app-catalog.html +81 -24
  165. package/package.json +3 -1
  166. package/prisma/schema.postgres.prisma +20 -0
  167. package/prisma/schema.prisma +20 -0
@@ -67,6 +67,8 @@
67
67
  <!-- Canonical styles for the shared <Markdown> component — load before views so it styles
68
68
  rendered markdown everywhere the component is used (no per-view re-definition). -->
69
69
  <link rel="stylesheet" href="/css/components/markdown.css">
70
+ <link rel="stylesheet" href="/css/components/app-sandbox.css">
71
+ <link rel="stylesheet" href="/css/views/app-grant.css">
70
72
  <!-- View CSS preloaded so they are ready before any view renders (prevents FOUC) -->
71
73
  <!-- hrefs are stamped with ?v=BUILD_ID at serve time by portal.ts serveSpa() -->
72
74
  <link rel="stylesheet" href="/css/views/portal.css">
@@ -105,6 +107,7 @@
105
107
  "/js/i18n.js": "/js/i18n.js",
106
108
  "/js/theme.js": "/js/theme.js",
107
109
  "/js/utils.js": "/js/utils.js",
110
+ "/js/app-sandbox.js": "/js/app-sandbox.js",
108
111
  "/js/format.js": "/js/format.js",
109
112
  "/js/api.js": "/js/api.js",
110
113
  "/js/hooks.js": "/js/hooks.js",
@@ -276,6 +279,7 @@
276
279
  import { loadTranslations, t, getLocale, switchLocale, onLocaleChange } from '/js/i18n.js';
277
280
  import { initThemeSystem, getTheme, toggleTheme, onThemeChange } from '/js/theme.js';
278
281
  import { NotificationBell } from '/components/NotificationBell.js';
282
+ import { openAppSandboxed, isAppHtmlUrl } from '/js/app-sandbox.js';
279
283
  // generateStars no longer needed for light theme
280
284
 
281
285
  const html = htm.bind(h);
@@ -325,6 +329,7 @@
325
329
  '/v1/how-it-works': () => import('/views/how-it-works.js' + B),
326
330
  '/v1/business': () => import('/views/business.js' + B),
327
331
  '/v1/profile': () => import('/views/profile.js' + B),
332
+ '/v1/app-grant': () => import('/views/app-grant.js' + B),
328
333
  '/v1/guides': () => import('/views/guides.js' + B),
329
334
  '/v1/hobbies': () => import('/views/hobbies.js' + B),
330
335
  '/v1/marketplace': () => import('/views/marketplace.js' + B),
@@ -412,6 +417,16 @@
412
417
  const a = e.target.closest('a[href]');
413
418
  if (!a) return;
414
419
  const href = a.getAttribute('href');
420
+ // H-2: a published-app HTML link must NEVER open as a top-level document on the
421
+ // apex origin (same origin as this authenticated SPA) — it could read the
422
+ // visitor's session. Force it into a sandboxed, opaque-origin iframe instead,
423
+ // regardless of target=_blank / modifier keys.
424
+ if (isAppHtmlUrl(href)) {
425
+ e.preventDefault();
426
+ const label = (a.textContent || '').trim() || 'App';
427
+ openAppSandboxed(href, label);
428
+ return;
429
+ }
415
430
  // Only intercept internal /v1/ links
416
431
  if (!href || !href.startsWith('/v1/')) return;
417
432
  if (e.ctrlKey || e.metaKey || e.shiftKey) return;
@@ -656,7 +671,7 @@
656
671
  const keys = [
657
672
  'descNew', 'descReturning', 'usernameLabel', 'usernamePlaceholder',
658
673
  'passwordLabel', 'passwordPlaceholder', 'displayNameLabel', 'displayNameHint',
659
- 'displayNamePlaceholder', 'signInBtn', 'cancelBtn', 'working',
674
+ 'displayNamePlaceholder', 'signInBtn', 'cancelBtn', 'orLabel', 'googleSignIn', 'working',
660
675
  'errUserShort', 'errPassShort', 'errWrongPass', 'loggedIn', 'logoutBtn',
661
676
  'forgotPassword', 'forgotUsername', 'resetPasswordTitle', 'resetPasswordDesc',
662
677
  'sendResetCode', 'backToLogin', 'resetCodeSent', 'enterNewPasswordTitle',
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file app-grant.js
3
+ * @description Trusted consent page (apex origin) for the H-2 app-grant flow. An app on the
4
+ * isolated app origin sends the owner here (GET /v1/app-grants/authorize → 302 → /v1/app-grant
5
+ * ?req=...). The owner, authenticated on aimeat.io, reviews the requesting app + the exact
6
+ * scopes and Approves or Denies. Approve POSTs to /v1/app-grants/authorize-consent and the
7
+ * server returns the app's redirect_url (carrying the one-time code) which we navigate to —
8
+ * the app never sees the session, only the scoped grant token it then exchanges.
9
+ * @structure default export AppGrant() — loads the pending request, renders scopes + actions.
10
+ * @usage routed at /v1/app-grant?req=<id> by spa.html
11
+ * @version-history
12
+ * v1.0.0 — 2026-06-20 — Initial (H-2 app-origin isolation, Phase 3: consent page).
13
+ */
14
+ import { h } from 'preact';
15
+ import { useState, useEffect } from 'preact/hooks';
16
+ import htm from 'htm';
17
+ import { t } from '/js/i18n.js';
18
+ import { api } from '/js/api.js';
19
+ import { escHtml } from '/js/utils.js';
20
+
21
+ const html = htm.bind(h);
22
+ const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
23
+
24
+ export default function AppGrant() {
25
+ const [state, setState] = useState({ status: 'loading', request: null, error: '' });
26
+ const [submitting, setSubmitting] = useState(false);
27
+
28
+ const requestId = new URLSearchParams(window.location.search).get('req') || '';
29
+ const loggedIn = !!window.AIMEAT?.auth?.hasSession;
30
+
31
+ useEffect(() => {
32
+ if (!requestId) { setState({ status: 'error', error: tr('appGrant.missing', 'No authorization request.') }); return; }
33
+ if (!loggedIn) { setState({ status: 'login' }); return; }
34
+ let live = true;
35
+ api(`/v1/app-grants/request/${encodeURIComponent(requestId)}`)
36
+ .then((res) => { if (live) setState({ status: 'ready', request: res.data }); })
37
+ .catch((e) => { if (live) setState({ status: 'error', error: e.message || tr('appGrant.expired', 'This request has expired.') }); });
38
+ return () => { live = false; };
39
+ }, [requestId, loggedIn]);
40
+
41
+ async function approve() {
42
+ setSubmitting(true);
43
+ try {
44
+ const res = await api('/v1/app-grants/authorize-consent', { method: 'POST', body: JSON.stringify({ request_id: requestId }) });
45
+ window.location.href = res.data.redirect_url; // back to the app, carrying the one-time code
46
+ } catch (e) {
47
+ setState((s) => ({ ...s, status: 'error', error: e.message || 'Failed to approve.' }));
48
+ setSubmitting(false);
49
+ }
50
+ }
51
+
52
+ function deny() {
53
+ const origin = state.request?.app_origin;
54
+ if (origin) window.location.href = origin; else window.location.href = '/v1/profile';
55
+ }
56
+
57
+ if (state.status === 'loading') {
58
+ return html`<div class="agr-wrap"><p class="agr-muted">${tr('common.loading', 'Loading…')}</p></div>`;
59
+ }
60
+ if (state.status === 'login') {
61
+ return html`
62
+ <div class="agr-wrap">
63
+ <div class="agr-card">
64
+ <h1 class="agr-title">${tr('appGrant.loginTitle', 'Log in to continue')}</h1>
65
+ <p class="agr-muted">${tr('appGrant.loginBody', 'Log in on aimeat.io, then re-open the app’s link to approve access.')}</p>
66
+ <a class="btn-primary agr-btn" href="/v1/profile">${tr('appGrant.loginCta', 'Log in')}</a>
67
+ </div>
68
+ </div>`;
69
+ }
70
+ if (state.status === 'error') {
71
+ return html`
72
+ <div class="agr-wrap">
73
+ <div class="agr-card">
74
+ <h1 class="agr-title">${tr('appGrant.errorTitle', 'Cannot grant access')}</h1>
75
+ <p class="agr-muted">${escHtml(state.error)}</p>
76
+ <a class="btn-outline agr-btn" href="/v1/profile">${tr('common.back', 'Back')}</a>
77
+ </div>
78
+ </div>`;
79
+ }
80
+
81
+ const req = state.request;
82
+ return html`
83
+ <div class="agr-wrap">
84
+ <div class="agr-card">
85
+ <h1 class="agr-title">${tr('appGrant.title', 'Allow this app to access your data?')}</h1>
86
+ <div class="agr-app">
87
+ <div class="agr-app-name">${escHtml(req.app_name)}</div>
88
+ <div class="agr-app-origin">${escHtml(req.app_origin)}</div>
89
+ </div>
90
+ <p class="agr-muted">${tr('appGrant.intro', 'It will receive its own scoped, revocable token — never your login session. You can revoke it anytime in Profile › Access.')}</p>
91
+ <div class="agr-scopes-label">${tr('appGrant.scopesLabel', 'Requested access')}</div>
92
+ <ul class="agr-scopes">
93
+ ${req.scopes.map((s) => html`
94
+ <li class="agr-scope" key=${s.scope}>
95
+ <span class="agr-scope-name">${escHtml(s.scope)}</span>
96
+ <span class="agr-scope-desc">${escHtml(s.description || '')}</span>
97
+ </li>`)}
98
+ </ul>
99
+ <div class="agr-actions">
100
+ <button class="btn-outline agr-btn" onClick=${deny} disabled=${submitting}>${tr('appGrant.deny', 'Deny')}</button>
101
+ <button class="btn-primary agr-btn" onClick=${approve} disabled=${submitting}>
102
+ ${submitting ? tr('appGrant.approving', 'Approving…') : tr('appGrant.approve', 'Allow access')}
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>`;
107
+ }
@@ -9,6 +9,8 @@
9
9
  * @usage routed at /v1/business by spa.html
10
10
  * @version-history
11
11
  * v1.0.0 — 2026-06-10 — Initial (owner spec).
12
+ * v1.1.0 — 2026-06-20 — H-2: published-app proof links open in a sandboxed opaque-origin
13
+ * iframe (openAppSandboxed) instead of a top-level apex ?mode=inline tab.
12
14
  */
13
15
  import { h } from 'preact';
14
16
  import { useState, useEffect } from 'preact/hooks';
@@ -17,6 +19,7 @@ const html = htm.bind(h);
17
19
  import { t } from '/js/i18n.js';
18
20
  import { escHtml } from '/js/utils.js';
19
21
  import { ContactCard } from '/components/ContactCard.js';
22
+ import { openAppSandboxed, isAppHtmlUrl } from '/js/app-sandbox.js';
20
23
 
21
24
  const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
22
25
  const CONTACT = 'mailto:jouni.miikki@aimeat.io';
@@ -42,7 +45,11 @@ export default function Business({ navigate }) {
42
45
  <p class="ld-case-text">${text}</p>
43
46
  ${quote && html`<blockquote class="ld-case-quote">“${quote}”<footer>— ${quoteBy}</footer></blockquote>`}
44
47
  ${proofHref
45
- ? html`<a class="ld-case-proof" href=${proofHref} target="_blank" rel="noopener">${proofLabel}</a>`
48
+ // H-2: published-app proof links open in a sandboxed opaque-origin iframe, never a
49
+ // top-level apex document. Non-app evidence (public viewers, external sites) opens normally.
50
+ ? (isAppHtmlUrl(proofHref)
51
+ ? html`<a class="ld-case-proof" href="#" role="button" onClick=${(e) => { e.preventDefault(); openAppSandboxed(proofHref, proofLabel); }}>${proofLabel}</a>`
52
+ : html`<a class="ld-case-proof" href=${proofHref} target="_blank" rel="noopener">${proofLabel}</a>`)
46
53
  : proofNote && html`<div class="ld-case-proofnote">${proofNote}</div>`}
47
54
  <a class="ld-path-cta" href=${ctaHref}>${ctaLabel}</a>
48
55
  </div>
@@ -8,6 +8,8 @@
8
8
  * @usage routed at /v1/how-it-works by spa.html
9
9
  * @version-history
10
10
  * v1.0.0 — 2026-06-10 — Initial (owner spec).
11
+ * v1.1.0 — 2026-06-20 — H-2: the live-example app link opens in a sandboxed opaque-origin
12
+ * iframe (openAppSandboxed) instead of a top-level apex ?mode=inline tab.
11
13
  */
12
14
  import { h } from 'preact';
13
15
  import { useState, useEffect } from 'preact/hooks';
@@ -15,6 +17,7 @@ import htm from 'htm';
15
17
  const html = htm.bind(h);
16
18
  import { t } from '/js/i18n.js';
17
19
  import { escHtml } from '/js/utils.js';
20
+ import { openAppSandboxed } from '/js/app-sandbox.js';
18
21
 
19
22
  const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
20
23
 
@@ -144,7 +147,8 @@ export default function HowItWorks({ navigate }) {
144
147
  <div class="ld-stats-own">
145
148
  ${tr('hiw.exampleText', 'AIMEAT Sanomat writes itself every evening. news-fetcher pulls raw material at 17:00, six writer agents produce the articles, and the paper ships with zero human hours.')}
146
149
  ${paper && html`
147
- <a class="ld-stats-cta" href=${`/v1/apps/${encodeURIComponent(paper.owner)}/${encodeURIComponent(paper.filename)}?mode=inline`} target="_blank">
150
+ <a class="ld-stats-cta" href="#" role="button" onClick=${(e) => { e.preventDefault();
151
+ openAppSandboxed(`/v1/apps/${encodeURIComponent(paper.owner)}/${encodeURIComponent(paper.filename)}?mode=inline`, paper.filename); }}>
148
152
  ${tr('hiw.exampleCta', "Read tonight's paper →")}</a>`}
149
153
  </div>
150
154
  </div>
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * @file landing.js
3
- * @description Logged-out landing page: reward first, explanation second. Hero is a
4
- * newspaper-framed teaser for tonight's Sanomat that opens the REAL app in one click —
5
- * no iframe (mobile-friendly) and no fake image: a designed masthead card by default,
6
- * auto-upgrading to a real screenshot if/when one is uploaded (apps API screenshot_url
7
- * only). Two CTAs (try it free · get your own) → proof gallery → live activity feed →
3
+ * @description Logged-out landing page: reward first, explanation second. Hero is the
4
+ * build-prompt pitch — copy one prompt and your AI builds you a real app on AIMEAT that you
5
+ * own and publish. Two CTAs (copy build prompt · get your own) → LIVE wall of the real apps
6
+ * people published here (manifest-driven, from the apps API) → live activity feed →
8
7
  * the 3-step build loop + build prompt → "ask your own AI" → today's node stats +
9
8
  * ownership line (the sales close) → footer. Logged-in visitors are forwarded to the
10
9
  * profile Home dashboard. No protocol terms (GHII/GAII/CSM/federation) above the fold;
11
10
  * a working result does the selling.
12
- * @structure default export Landing({ navigate }) + Hero/StatsPanel/BuildAppPrompt/BuildAgentPrompt/AskYourAI/Gallery
11
+ * @structure default export Landing({ navigate }) + BuildHero/Gallery(live wall)/StatsPanel/BuildAppPrompt/BuildAgentPrompt/AskYourAI
13
12
  * @usage routed at /v1/portal (and '/' for browsers) by spa.html
14
13
  * @version-history
15
14
  * v1.0.0 — 2026-06-10 — Initial: landing/portal split (owner spec).
@@ -24,6 +23,13 @@
24
23
  * v2.1.0 — 2026-06-17 — Add BuildAgentPrompt: a copy-paste "build an agent in 10 minutes" prompt
25
24
  * for the local crewaimeat fleet (Ollama/Gemma, no keys); Hero "Get your own →" now points to
26
25
  * the desktop installer GitHub Release (was /v1/pricing).
26
+ * v3.0.0 — 2026-06-20 — Value-first hero: replace the Sanomat newspaper Hero with BuildHero
27
+ * (copy the build prompt → your AI builds you an app you own + publish); the gallery becomes a
28
+ * LIVE wall of the real apps people published here (manifest-driven from /v1/apps).
29
+ * v3.1.0 — 2026-06-20 — Wall: fixed 3-up grid + filter search; cards show author + publish
30
+ * date/time. Hero subline adds "let your agents keep it running" (Sanomat as the example).
31
+ * v3.2.0 — 2026-06-20 — H-2: wall cards open published apps in a sandboxed opaque-origin
32
+ * iframe (openAppSandboxed) instead of a top-level apex ?mode=inline link.
27
33
  */
28
34
  import { h } from 'preact';
29
35
  import { useState, useEffect } from 'preact/hooks';
@@ -31,6 +37,7 @@ import htm from 'htm';
31
37
  const html = htm.bind(h);
32
38
  import { t } from '/js/i18n.js';
33
39
  import { escHtml } from '/js/utils.js';
40
+ import { openAppSandboxed } from '/js/app-sandbox.js';
34
41
  import PublicActivityFeed from './landing-activity.js';
35
42
 
36
43
  // t() echoes the key when a translation is missing — fall back to readable English.
@@ -67,44 +74,63 @@ function StatsPanel({ navigate }) {
67
74
  `;
68
75
  }
69
76
 
70
- /* ── Proof gallery: six fully curated cards. Sanomat and Comicland lead because they
71
- work solo, instantly; Deep Six needs two players (badged). Proof, not a shop. ── */
72
- function Gallery({ onApps }) {
73
- const [, setApps] = useState([]);
77
+ /* ── Live wall — the REAL apps people built with their AI and published to this node (from the
78
+ apps API, manifest-driven). Three per row + a filter. Each card: name · description · who made
79
+ it · when. The proof the loop works: your creation lands on this same wall. ── */
80
+ function fmtPublished(iso) {
81
+ try {
82
+ const d = new Date(iso);
83
+ return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
84
+ } catch { return ''; }
85
+ }
86
+
87
+ function Gallery() {
88
+ const [apps, setApps] = useState([]);
89
+ const [q, setQ] = useState('');
74
90
  useEffect(() => {
75
- // The catalog still feeds card A's local-Sanomat lookup via onApps.
76
- fetch('/v1/apps?sort=popular&limit=50').then(r => r.json())
77
- .then(j => { const list = j?.data?.apps || []; setApps(list); onApps?.(list); })
78
- .catch(() => { /* curated cards render regardless */ });
91
+ fetch('/v1/apps?sort=popular&limit=60').then(r => r.json())
92
+ .then(j => setApps(j?.data?.apps || []))
93
+ .catch(() => { /* empty state renders */ });
79
94
  }, []);
80
95
 
81
- const cards = [
82
- { name: 'AIMEAT Sanomat', desc: tr('landing.gallerySanomat', 'The paper that writes itself every evening. Six agents, zero human hours.'),
83
- href: 'https://aimeat.io/v1/apps/happydude500001/laimeat-sanomat.html?mode=inline' },
84
- { name: 'Comicland', desc: tr('landing.galleryComicland', 'Comics by agents — browse the catalog.'),
85
- href: 'https://aimeat.io/v1/apps/happydude500001/comicland-v2-app.html?mode=inline#/catalog' },
86
- { name: 'Battleship (Deep Six)', desc: tr('landing.galleryDeepSix', 'Two-player battleship on an agent platform. Bring an opponent — or two browsers.'),
87
- href: 'https://aimeat.io/v1/apps/anonymous/deep-six.html?mode=inline', badge: tr('landing.twoPlayers', '2 players') },
88
- { name: 'Solar System Simulator', desc: tr('landing.gallerySolar', 'Simulate the solar system.'),
89
- href: 'https://aimeat.io/v1/apps/anonymous/solar-system.html?mode=inline' },
90
- { name: 'Math Graph 3D', desc: tr('landing.galleryGraph', 'Plot 3D math functions in the browser.'),
91
- href: 'https://aimeat.io/v1/apps/anonymous/graph.html?mode=inline' },
92
- { name: 'Band Jam', desc: tr('landing.galleryBandJam', 'Make music with your friends, live!'),
93
- href: 'https://aimeat.io/v1/apps/anonymous/band-jam.html?mode=inline' },
94
- ];
96
+ const ql = q.trim().toLowerCase();
97
+ const shown = !ql ? apps : apps.filter((a) => {
98
+ const m = a.manifest || {};
99
+ return [m.name, m.description, m.authorDisplay, a.owner].some(v => (v || '').toLowerCase().includes(ql));
100
+ });
95
101
 
96
102
  return html`
97
103
  <div class="ld-section">
98
- <h2 class="ld-h2">${tr('landing.galleryTitle', 'Built with this loop — real apps, try them')}</h2>
99
- <div class="ld-gallery">
100
- ${cards.map(c => html`
101
- <a key=${c.href} class="ld-app-card" href=${c.href} target="_blank" rel="noopener">
102
- <div class="ld-app-name">${escHtml(c.name)}${c.badge && html` <span class="ld-app-badge">${c.badge}</span>`}</div>
103
- ${c.desc && html`<div class="ld-app-desc">${escHtml(c.desc)}</div>`}
104
- <div class="ld-app-meta">${tr('landing.builtInChat', 'built in an AI chat session')}</div>
105
- </a>
106
- `)}
107
- </div>
104
+ <h2 class="ld-h2">${tr('landing.wallTitle', 'Built by people with their AI. Yours goes here too.')}</h2>
105
+ <input class="ld-wall-search" type="search" value=${q}
106
+ onInput=${(e) => setQ(e.target.value)}
107
+ placeholder=${tr('landing.wallSearch', 'Search apps…')}
108
+ aria-label=${tr('landing.wallSearch', 'Search apps')} />
109
+ ${shown.length === 0
110
+ ? html`<p class="ld-app-desc">${apps.length === 0
111
+ ? tr('landing.wallEmpty', 'Be the first — copy the prompt above, build something, and it lands here.')
112
+ : tr('landing.wallNoMatch', 'No apps match your search.')}</p>`
113
+ : html`<div class="ld-gallery">
114
+ ${shown.map((a) => {
115
+ const m = a.manifest || {};
116
+ // H-2: open published apps in a sandboxed (opaque-origin) iframe, never as a
117
+ // top-level apex document. Click-to-open instead of an apex href, so middle-/
118
+ // ctrl-click can't bypass it either.
119
+ const href = `/v1/apps/${encodeURIComponent(a.owner)}/${encodeURIComponent(a.filename)}?mode=inline`;
120
+ const desc = (m.description || '').length > 140 ? m.description.slice(0, 140) + '…' : (m.description || '');
121
+ const author = m.authorDisplay || a.owner || tr('landing.wallAnon', 'someone');
122
+ const when = a.created_at ? fmtPublished(a.created_at) : '';
123
+ const open = () => openAppSandboxed(href, m.name || a.filename);
124
+ return html`
125
+ <div key=${a.owner + '/' + a.filename} class="ld-app-card" role="button" tabindex="0"
126
+ onClick=${open} onKeyDown=${(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }}>
127
+ ${a.screenshot_url ? html`<img class="ld-app-shot" src=${a.screenshot_url} loading="lazy" alt="" />` : ''}
128
+ <div class="ld-app-name">${m.icon ? escHtml(m.icon) + ' ' : ''}${escHtml(m.name || a.filename)}</div>
129
+ ${desc && html`<div class="ld-app-desc">${escHtml(desc)}</div>`}
130
+ <div class="ld-app-meta">${escHtml(author)}${when ? ' · ' + when : ''}</div>
131
+ </div>`;
132
+ })}
133
+ </div>`}
108
134
  </div>
109
135
  `;
110
136
  }
@@ -232,6 +258,7 @@ function buildLandingAppPrompt(nodeUrl) {
232
258
  p += 'Always include <meta name="viewport" content="width=device-width, initial-scale=1.0">. Mobile-first, single self-contained HTML file with embedded CSS + JS.\n\n';
233
259
  p += '### Important Rules\n';
234
260
  p += '- Return the COMPLETE HTML file, not fragments\n';
261
+ p += '- Also write a one-sentence description of what the app does - it is REQUIRED when publishing and shows in the catalogue and on the landing wall\n';
235
262
  p += '- Never use literal closing script tags in JS comments or strings\n';
236
263
  p += '- Keep it as a single self-contained HTML file\n';
237
264
  p += '- Load only the libraries you actually use; load aimeat-auth before libs that need a session\n';
@@ -381,35 +408,23 @@ function AskYourAI() {
381
408
  </section>`;
382
409
  }
383
410
 
384
- /* ── Hero v2 — reward first: a REAL screenshot of tonight's Sanomat (no iframe, so it
385
- works on mobile), framed as a newspaper that opens the REAL app in one click. No fake
386
- image: it shows a designed masthead card by default and auto-upgrades to a real
387
- screenshot the day one is uploaded (we trust ONLY the apps API's screenshot_url, never a
388
- guessed URL — every app currently has none, so the card is what shows). The two CTAs are
389
- the only fork (experience it now · own it). ── */
390
- function Hero({ sanomat, tryHref }) {
391
- const shot = sanomat?.screenshot_url || null; // only a real, API-reported screenshot
411
+ /* ── Hero — value first: AIMEAT is a safe place to build real apps with your AI in minutes, and
412
+ you own + publish them. One copyable prompt is the whole on-ramp; the live wall below is the
413
+ proof that your creation lands on the same shelf as everyone else's. ── */
414
+ function BuildHero() {
415
+ const [copied, setCopied] = useState(false);
416
+ const prompt = buildLandingAppPrompt(window.location.origin);
417
+ const copy = async () => {
418
+ try { await navigator.clipboard.writeText(prompt); setCopied(true); setTimeout(() => setCopied(false), 2000); }
419
+ catch { /* the full prompt is also visible lower on the page */ }
420
+ };
392
421
  return html`
393
422
  <section class="ld-hero2">
394
- <p class="ld-hero2-kicker">${tr('landing.heroKicker', 'Tonight’s paper wrote itself.')}</p>
395
- <h1 class="ld-hero2-title">${tr('landing.heroLead', 'Six agents. Zero human hours.')}</h1>
396
- <a class="ld-hero2-shot" href=${tryHref} target="_blank" rel="noopener"
397
- aria-label=${tr('landing.heroOpenPaper', 'Open tonight’s paper')}>
398
- ${shot ? html`
399
- <img class="ld-hero2-img" src=${shot} loading="lazy"
400
- alt=${tr('landing.heroShotAlt', 'AIMEAT Sanomat — tonight’s edition')} />
401
- ` : html`
402
- <div class="ld-hero2-paper">
403
- <span class="ld-hero2-masthead">AIMEAT Sanomat</span>
404
- <span class="ld-hero2-paper-rule"></span>
405
- <span class="ld-hero2-paper-line">${tr('landing.heroPaperLine', 'Tonight’s edition · written entirely by agents')}</span>
406
- </div>
407
- `}
408
- <span class="ld-hero2-openbar">${tr('landing.cardACta', 'Read tonight’s paper →')}</span>
409
- </a>
410
- <p class="ld-hero2-sub">${tr('landing.heroSub2', 'A real app, built and run by AI on AIMEAT — look before you sign up.')}</p>
423
+ <p class="ld-hero2-kicker">${tr('landing.buildHeroKicker', 'A safe place to build with AI.')}</p>
424
+ <h1 class="ld-hero2-title">${tr('landing.buildHeroTitle', 'Build a real app with your AI in minutes, and it’s yours.')}</h1>
425
+ <p class="ld-hero2-sub">${tr('landing.buildHeroSub', 'Copy one prompt into Claude, ChatGPT or any AI. It builds you a working app on AIMEAT, published live and yours to keep. Then let your agents run it for you, the way AIMEAT Sanomat writes itself every evening.')}</p>
411
426
  <div class="ld-hero2-cta">
412
- <a class="btn-primary" href=${tryHref} target="_blank" rel="noopener">${tr('landing.heroTryFree', 'Try it free →')}</a>
427
+ <button class="btn-primary" type="button" onClick=${copy}>${copied ? tr('landing.buildHeroCopied', 'Copied ✓ — paste into your AI') : tr('landing.buildHeroCopy', 'Copy the build prompt →')}</button>
413
428
  <a class="btn-outline" href="https://github.com/miikkij/aimeat-protocol/releases/latest" target="_blank" rel="noopener">${tr('landing.heroGetOwn', 'Get your own →')}</a>
414
429
  </div>
415
430
  </section>
@@ -436,21 +451,13 @@ export default function Landing({ navigate }) {
436
451
  return () => window.removeEventListener('aimeat-auth-change', onAuth);
437
452
  }, []);
438
453
 
439
- const [apps, setApps] = useState([]);
440
- // Card A sends the solo visitor to Sanomat — an instant experience alone, no login.
441
- // (Battleship/Deep Six lives in the gallery with its "2 players" badge.)
442
- const sanomat = apps.find(a => /sanomat/i.test((a.filename || '') + ' ' + (a.name || '')));
443
- const tryHref = sanomat
444
- ? `/v1/apps/${encodeURIComponent(sanomat.owner)}/${encodeURIComponent(sanomat.filename)}?mode=inline`
445
- : 'https://aimeat.io/v1/apps/happydude500001/laimeat-sanomat.html?mode=inline';
446
-
447
454
  return html`
448
455
  <div class="ld">
449
- <!-- 1. Hero — reward first: a real, mobile-friendly Sanomat screenshot + the two CTAs. -->
450
- <${Hero} sanomat=${sanomat} tryHref=${tryHref} />
456
+ <!-- 1. Hero — value first: build a real app with your AI; the live wall below is the proof. -->
457
+ <${BuildHero} />
451
458
 
452
- <!-- 2. Proof gallery — more apps to try instantly, no login (moved up: the reward, not the footer). -->
453
- <${Gallery} onApps=${setApps} />
459
+ <!-- 2. Live wall — the real apps people built with their AI and published here (yours goes here). -->
460
+ <${Gallery} />
454
461
 
455
462
  <!-- 3. Public activity feed — live proof it's happening right now. -->
456
463
  <${PublicActivityFeed} />
@@ -20,6 +20,8 @@
20
20
  * neutral (access-h3, emojis dropped) and Add Node / New Token / New Group de-accented;
21
21
  * Token Budget value is click-to-edit and the empty-rules state shows an example rule;
22
22
  * sharing-groups section accepts the Memory tab's deep link (aimeat.access.focus).
23
+ * v1.5.0 -- 2026-06-20 -- Add Connected Apps section (H-2 app grants): list + revoke the
24
+ * scoped, user-approved access tokens published apps hold (GET/DELETE /v1/app-grants).
23
25
  */
24
26
  import { h } from 'preact';
25
27
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -558,6 +560,78 @@ function AgentDefaultsSection({ showToast }) {
558
560
  `;
559
561
  }
560
562
 
563
+ /* ═══════════════════════════════════════════════════════════════════
564
+ Connected Apps Section (H-2 app grants)
565
+ ═══════════════════════════════════════════════════════════════════ */
566
+
567
+ // Apps the owner has explicitly granted scoped access to (the H-2 app-grant flow). Each
568
+ // holds its OWN revocable token bound to a narrow scope set — never the login session.
569
+ function ConnectedAppsSection({ showToast }) {
570
+ const { confirm, ConfirmUI } = useConfirm();
571
+ const [grants, setGrants] = useState(null);
572
+
573
+ const load = useCallback(async () => {
574
+ try { const r = await apiGet('/v1/app-grants'); setGrants(r.data?.grants || []); }
575
+ catch { setGrants([]); }
576
+ }, []);
577
+ useEffect(() => { load(); }, [load]);
578
+
579
+ const liveRef = useRef(load);
580
+ liveRef.current = load;
581
+ useEffect(() => {
582
+ const handler = () => liveRef.current();
583
+ window.addEventListener('aimeat-live-update', handler);
584
+ return () => window.removeEventListener('aimeat-live-update', handler);
585
+ }, []);
586
+
587
+ const handleRevoke = useCallback((id, name) => {
588
+ confirm(
589
+ (t('profile.access.agConfirmRevoke') || 'Revoke access for "{name}"? It loses access immediately.').replace('{name}', name),
590
+ async () => {
591
+ try {
592
+ await apiDelete('/v1/app-grants/' + id);
593
+ showToast(t('profile.access.agRevoked') || 'App access revoked');
594
+ load();
595
+ } catch (e) { showToast(e.message); }
596
+ },
597
+ { danger: true },
598
+ );
599
+ }, [confirm, showToast, load]);
600
+
601
+ return html`
602
+ <h3 class="card-h3 access-h3 mt-section">${t('profile.access.agTitle') || 'Connected Apps'}</h3>
603
+ <div class="section-desc">${t('profile.access.agDesc') || 'Published apps you have granted scoped access to your data. Each holds its own revocable token — never your login session.'}</div>
604
+
605
+ ${grants === null
606
+ ? html`<div class="empty">${t('profile.access.agLoading') || 'Loading...'}</div>`
607
+ : grants.length === 0
608
+ ? html`<div class="access-empty-row"><span class="text-meta-sm">${t('profile.access.agEmpty') || 'No apps have access to your data.'}</span></div>`
609
+ : grants.map(g => html`
610
+ <div class="card" key=${g.grant_id}>
611
+ <div class="flex-between">
612
+ <div class="card-title">${escHtml(g.app_name || g.app)}</div>
613
+ <span class="badge badge-info">${(g.scopes || []).length} ${t('profile.access.agScopes') || 'scopes'}</span>
614
+ </div>
615
+ <div class="card-subtitle access-mono">${escHtml(g.app_origin || '')}</div>
616
+ <div class="detail-grid">
617
+ <div class="detail-item"><span class="detail-label">${t('profile.access.agGrantedAt') || 'Granted'}</span><span class="detail-value">${g.granted_at ? new Date(g.granted_at).toLocaleDateString() : '-'}</span></div>
618
+ <div class="detail-item"><span class="detail-label">${t('profile.access.agLastUsed') || 'Last used'}</span><span class="detail-value">${g.last_used_at ? new Date(g.last_used_at).toLocaleString() : (t('profile.access.agNever') || 'never')}</span></div>
619
+ </div>
620
+ ${(g.scopes || []).length > 0 && html`
621
+ <div class="flex-row-wrap mt-half">
622
+ ${g.scopes.map(s => html`<span class="badge badge-muted" key=${s}>${escHtml(s)}</span>`)}
623
+ </div>
624
+ `}
625
+ <div class="card-actions">
626
+ <button class="btn-danger-solid btn-sm" onClick=${() => handleRevoke(g.grant_id, g.app_name || g.app)}>${t('profile.access.agRevoke') || 'Revoke'}</button>
627
+ </div>
628
+ </div>
629
+ `)
630
+ }
631
+ <${ConfirmUI} />
632
+ `;
633
+ }
634
+
561
635
  /* ═══════════════════════════════════════════════════════════════════
562
636
  Agent Access Tokens Section
563
637
  ═══════════════════════════════════════════════════════════════════ */
@@ -999,6 +1073,7 @@ export default function AccessTab({ session, showToast }) {
999
1073
  </div>`;
1000
1074
  })()}
1001
1075
 
1076
+ <${ConnectedAppsSection} showToast=${showToast} />
1002
1077
  <${AccessTokensSection} session=${session} showToast=${showToast} />
1003
1078
  <${SharingGroupsSection} showToast=${showToast} />
1004
1079
  <${AgentDefaultsSection} showToast=${showToast} />
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=screenshot-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-worker.d.ts","sourceRoot":"","sources":["../../scripts/screenshot-worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @file screenshot-worker.ts
3
+ * @description Dev wrapper for `pnpm screenshot:worker`. The implementation lives in
4
+ * src/cli/screenshot-worker.ts and ships to npm users as the `aimeat screenshot-worker`
5
+ * subcommand; this just invokes it with the current argv for local dev runs.
6
+ * @usage AIMEAT_OP_TOKEN=<token> pnpm screenshot:worker [--watch 600] [--dry-run] ...
7
+ * @version-history
8
+ * v1.0.0 — 2026-06-20 — initial standalone worker.
9
+ * v2.0.0 — 2026-06-20 — thin wrapper; logic moved to src/cli/screenshot-worker.ts (CLI subcommand
10
+ * + playwright-core system-browser channel so npm/npx installs work without a browser download).
11
+ */
12
+ import { runScreenshotWorker } from '../src/cli/screenshot-worker.js';
13
+ runScreenshotWorker();
14
+ //# sourceMappingURL=screenshot-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-worker.js","sourceRoot":"","sources":["../../scripts/screenshot-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAEtE,mBAAmB,EAAE,CAAC"}
@@ -15,6 +15,7 @@ export interface JWTPayload {
15
15
  homeNode?: string;
16
16
  homeUrl?: string;
17
17
  eco_app?: string;
18
+ app_grant?: string;
18
19
  }
19
20
  /** Generate a unique session ID for JWT tracking. */
20
21
  export declare function generateSessionId(): string;
@@ -33,6 +34,7 @@ export interface VerifiedToken {
33
34
  homeNode?: string;
34
35
  homeUrl?: string;
35
36
  eco_app?: string;
37
+ app_grant?: string;
36
38
  }
37
39
  export declare function verifyJWT(token: string): Promise<VerifiedToken | null>;
38
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/jwt.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAQvD,wBAAgB,iBAAiB,IAAI;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAGnF;AAED,wBAAsB,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnG;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB3G;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAwB5E;AAkBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmB5D;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB/D"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/auth/jwt.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAQvD,wBAAgB,iBAAiB,IAAI;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAGnF;AAED,wBAAsB,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnG;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qDAAqD;AACrD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B3G;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAyB5E;AAkBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmB5D;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjF;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB/D"}
@@ -55,6 +55,7 @@ export async function issueJWT(payload, ttlSeconds, sessionId) {
55
55
  ...(payload.homeNode ? { homeNode: payload.homeNode } : {}),
56
56
  ...(payload.homeUrl ? { homeUrl: payload.homeUrl } : {}),
57
57
  ...(payload.eco_app ? { eco_app: payload.eco_app } : {}),
58
+ ...(payload.app_grant ? { app_grant: payload.app_grant } : {}),
58
59
  })
59
60
  .setProtectedHeader({ alg: 'EdDSA', typ: 'JWT' })
60
61
  .setSubject(payload.sub)
@@ -86,6 +87,7 @@ export async function verifyJWT(token) {
86
87
  homeNode: payload.homeNode,
87
88
  homeUrl: payload.homeUrl,
88
89
  eco_app: payload.eco_app,
90
+ app_grant: payload.app_grant,
89
91
  };
90
92
  }
91
93
  catch {