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
package/README.md CHANGED
@@ -346,6 +346,13 @@ npx aimeat seed # seed example packages (in another terminal, server must be
346
346
  npx aimeat connect --url https://your-node --owner your-handle
347
347
  ```
348
348
 
349
+ **App thumbnails (optional).** `aimeat screenshot-worker` renders each published app and stores a
350
+ thumbnail shown in the App Catalogue and on the landing wall (`--watch N` keeps it backfilling). It
351
+ drives your machine's installed **Chrome/Edge** via Playwright — **no browser download** on
352
+ Windows/desktop; a headless server runs `npx playwright install chromium` once. Auth uses a
353
+ long-lived operator token (mint one with `POST /v1/access/tokens`, `grant_operator: true`). The node
354
+ runs fine without it — screenshots are an opt-in operator feature.
355
+
349
356
  ### From source
350
357
 
351
358
  Requires Node.js 24+ and pnpm 10+. MongoDB is optional.
package/dist/.env.example CHANGED
@@ -8,6 +8,15 @@ AIMEAT_PORT=40050
8
8
  # AIMEAT_NODE_TYPE="full" # full | relay | mirror
9
9
  # AIMEAT_BASE_URL="https://your-domain.com" # Public URL (default: http://localhost:$PORT)
10
10
 
11
+ # ── App Origin Isolation (H-2) ─────────────────────────────────
12
+ # User-published apps run on a dedicated origin so they can never reach the
13
+ # authenticated SPA's session. Requires DNS + a *.apps.<host> TLS cert + an nginx
14
+ # server_name block (see docs/internal/app-origin-deployment.md). Keep DISABLED
15
+ # until that infra is live — enabling it without DNS/cert would 301 apps to a host
16
+ # that does not resolve.
17
+ # AIMEAT_APP_HOST="apps.your-domain.com" # default: apps.<AIMEAT_BASE_URL host>, empty for localhost/IP
18
+ # AIMEAT_APP_ORIGIN_ENABLED=false # true → apex inline app HTML 301s to the app origin
19
+
11
20
  # ── Storage Backend ───────────────────────────────────────────
12
21
  # Choose storage: memory (default, data lost on restart), sqlite (file-based, zero-config),
13
22
  # mongodb (production), or postgresql (production). `postgres` is accepted as an alias.
@@ -250,6 +259,14 @@ AIMEAT_PORT=40050
250
259
  # AIMEAT_NONCE_TTL_SECONDS=300 # verification nonce TTL (60-3600)
251
260
  # AIMEAT_NATIONAL_EID_PID_CLAIM="personal_identity_code" # FI: personal_identity_code, SE: personalNumber, DK: dk.cpr
252
261
 
262
+ # ── Social login — Google sign-in (OIDC) ─────────────────────
263
+ # Create OAuth credentials at https://console.cloud.google.com/apis/credentials
264
+ # Authorized redirect URI must exactly match the callback below.
265
+ # AIMEAT_GOOGLE_OAUTH_ENABLED=false
266
+ # AIMEAT_GOOGLE_OAUTH_CLIENT_ID="" # OAuth 2.0 client ID from Google Cloud Console
267
+ # AIMEAT_GOOGLE_OAUTH_CLIENT_SECRET="" # OAuth 2.0 client secret
268
+ # AIMEAT_GOOGLE_OAUTH_REDIRECT_URI="" # default: <baseUrl>/v1/ghii/login/google/callback
269
+
253
270
  # ── Consul (optional fleet management) ───────────────────────
254
271
  # Connect to HashiCorp Consul for centralized config management across nodes.
255
272
  # Values in Consul KV take precedence over file/.env but below database overrides.
@@ -52,7 +52,7 @@
52
52
  "profile.landing.menu": "Menu",
53
53
  "profile.tabs.notebook": "Notebook",
54
54
  "profile.notebook.title": "Notebook",
55
- "profile.notebook.desc": "Write anything freely — capture it now, find it later. The librarian searches across everything you have: every organism you have contributed to, plus your personal notes.",
55
+ "profile.notebook.desc": "Write anything freely — capture it now, find it later. The librarian searches across everything you have: every shared space (organism) you have contributed to, plus your personal notes.",
56
56
  "profile.notebook.capturePlaceholder": "Write anything — a thought, an idea, a note to keep…",
57
57
  "profile.notebook.captureBtn": "Capture",
58
58
  "profile.notebook.captureHint": "Saved to your notebook inbox. Sorting into organisms/workspaces comes next.",
@@ -243,10 +243,25 @@
243
243
  "profile.scheduler.soon": "soon",
244
244
  "profile.scheduler.paused": "paused",
245
245
  "profile.scheduler.autoKey": "(auto)",
246
+ "appGrant": {
247
+ "title": "Allow this app to access your data?",
248
+ "intro": "It will receive its own scoped, revocable token — never your login session. You can revoke it anytime in Profile › Access.",
249
+ "scopesLabel": "Requested access",
250
+ "approve": "Allow access",
251
+ "approving": "Approving…",
252
+ "deny": "Deny",
253
+ "loginTitle": "Log in to continue",
254
+ "loginBody": "Log in on aimeat.io, then re-open the app’s link to approve access.",
255
+ "loginCta": "Log in",
256
+ "errorTitle": "Cannot grant access",
257
+ "missing": "No authorization request.",
258
+ "expired": "This request has expired."
259
+ },
246
260
  "common": {
247
261
  "cancel": "Cancel",
248
262
  "confirm": "Confirm",
249
263
  "delete": "Delete",
264
+ "back": "Back",
250
265
  "somethingWentWrong": "Something went wrong",
251
266
  "retry": "Retry",
252
267
  "save": "Save",
@@ -380,8 +395,8 @@
380
395
  "offerPosted": "Your service is now listed in the directory.",
381
396
  "backToChoices": "Back",
382
397
  "registerTitle": "Create your free identity",
383
- "registerDesc": "When you register, you get a GHII (Global Human Intelligence Identifier) — your own digital identity. Your data is protected by your password: only you can access it. No one else can see your private memory, not even the server operator. You don't have to share anything with anyone.",
384
- "registerBtn": "Create free GHII",
398
+ "registerDesc": "When you register, you get your own private space — your own digital identity. Your data is protected by your password: only you can access it. No one else can see your private memory, not even the server operator. You don't have to share anything with anyone.",
399
+ "registerBtn": "Create your free space",
385
400
  "registerBenefits": [
386
401
  "Your own private memory space, password-protected",
387
402
  "Full control — only you access your data",
@@ -769,7 +784,7 @@
769
784
  "setupStep3Why": "Turn on automation and your agent runs the moment new data arrives — you get fresh insights without lifting a finger.",
770
785
  "setupStep3Cta": "Set up automation",
771
786
  "setupStep4Title": "Get your insights everywhere",
772
- "setupStep4Why": "Pick an organism and your results show up in Claude / Grok / ChatGPT, ready to reuse for marketing or reports and to share with your team — not locked inside this one app.",
787
+ "setupStep4Why": "Pick a shared space (an organism) and your results show up in Claude / Grok / ChatGPT, ready to reuse for marketing or reports and to share with your team — not locked inside this one app.",
773
788
  "setupStep4Cta": "Pick an organism",
774
789
  "mcpTitle": "Ask your insights in Claude, Grok, or ChatGPT",
775
790
  "mcpSub": "Connect AIMEAT to your AI chat once → then just ask, and get answers, exploration, save and follow-up — straight from everything this app and your agent produce.",
@@ -2074,6 +2089,17 @@
2074
2089
  "hoverReveal": "Click to copy",
2075
2090
  "keepSafe": "Keep this safe — it's your owner recovery key.",
2076
2091
  "keyCopied": "Key copied to clipboard!",
2092
+ "agTitle": "Connected Apps",
2093
+ "agDesc": "Published apps you have granted scoped access to your data. Each holds its own revocable token — never your login session.",
2094
+ "agLoading": "Loading...",
2095
+ "agEmpty": "No apps have access to your data.",
2096
+ "agScopes": "scopes",
2097
+ "agGrantedAt": "Granted",
2098
+ "agLastUsed": "Last used",
2099
+ "agNever": "never",
2100
+ "agConfirmRevoke": "Revoke access for \"{name}\"? It loses access immediately.",
2101
+ "agRevoked": "App access revoked",
2102
+ "agRevoke": "Revoke",
2077
2103
  "patTitle": "Agent Access Tokens",
2078
2104
  "patDesc": "Create a revocable token an agent can use (as a Bearer header) to log in and test your apps. One token can be shared across all your agents.",
2079
2105
  "patLabelRequired": "Give the token a name",
@@ -3649,7 +3675,11 @@
3649
3675
  "realtimeMaxPeersPerRoom": "Maximum peers per room",
3650
3676
  "extensionsEnabled": "Enable V8 isolate extension system?",
3651
3677
  "extensionMaxMemoryMb": "Max memory per extension isolate (MB)",
3652
- "extensionTimeoutMs": "Extension execution timeout (ms)"
3678
+ "extensionTimeoutMs": "Extension execution timeout (ms)",
3679
+ "appOrigin": "Serve user apps from a separate app origin (*.apps.<domain>) to isolate them from your session? Requires DNS + a wildcard TLS cert (see docs/internal/app-origin-deployment.md). Defaults to off.",
3680
+ "appHost": "App origin host (e.g. apps.example.com)",
3681
+ "appHostHint": "Set up DNS (apps.<domain> + wildcard *.apps.<domain>) and a wildcard TLS cert first — see docs/internal/app-origin-deployment.md",
3682
+ "appHostRequired": "App host is required when app origin isolation is enabled"
3653
3683
  },
3654
3684
  "join": {
3655
3685
  "title": "Join AIMEAT Federation",
@@ -3730,6 +3760,8 @@
3730
3760
  "displayNamePlaceholder": "Display Name (optional, for new accounts)",
3731
3761
  "signInBtn": "Sign In / Register",
3732
3762
  "cancelBtn": "Cancel",
3763
+ "orLabel": "OR",
3764
+ "googleSignIn": "Continue with Google",
3733
3765
  "working": "Working...",
3734
3766
  "errUserShort": "Username must be at least 3 characters",
3735
3767
  "errPassShort": "Password must be at least 4 characters",
@@ -3762,7 +3794,7 @@
3762
3794
  "sendUsername": "Send My Username",
3763
3795
  "usernameSent": "If an account with that email exists, your username was sent.",
3764
3796
  "whyTitle": "✨ What do you get?",
3765
- "whyGhii": "A free GHII (Global Human Intelligence Identifier), your personal AI identity",
3797
+ "whyGhii": "Your own digital identity only you control it",
3766
3798
  "whyPrivacy": "Your own private memory space, protected by your password",
3767
3799
  "whyControl": "Full control: only you can access your data, no need to share with anyone",
3768
3800
  "whyAgents": "Connect AI agents that remember you and work on your behalf",
@@ -5536,7 +5568,7 @@
5536
5568
  "loggedIn": "logged in",
5537
5569
  "logoutBtn": "Logout",
5538
5570
  "whyTitle": "✨ What do you get?",
5539
- "whyGhii": "A free GHII (Global Human Intelligence Identifier), your personal AI identity",
5571
+ "whyGhii": "Your own digital identity only you control it",
5540
5572
  "whyPrivacy": "Your own private memory space, protected by your password",
5541
5573
  "whyControl": "Full control: only you can access your data, no need to share with anyone",
5542
5574
  "whyAgents": "Connect AI agents that remember you and work on your behalf",
@@ -7100,6 +7132,16 @@
7100
7132
  "heroSub2": "A real app, built and run by AI on AIMEAT — look before you sign up.",
7101
7133
  "heroTryFree": "Try it free →",
7102
7134
  "heroGetOwn": "Get your own →",
7135
+ "buildHeroKicker": "A safe place to build with AI.",
7136
+ "buildHeroTitle": "Build a real app with your AI in minutes, and it’s yours.",
7137
+ "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.",
7138
+ "buildHeroCopy": "Copy the build prompt →",
7139
+ "buildHeroCopied": "Copied ✓ — paste into your AI",
7140
+ "wallTitle": "Built by people with their AI. Yours goes here too.",
7141
+ "wallEmpty": "Be the first — copy the prompt above, build something, and it lands here.",
7142
+ "wallAnon": "someone",
7143
+ "wallSearch": "Search apps…",
7144
+ "wallNoMatch": "No apps match your search.",
7103
7145
  "agentBuildTitle": "Build an agent in 10 minutes — copy this prompt",
7104
7146
  "agentBuildSub": "Paste into Claude, ChatGPT or any AI. It builds a local AI agent — running on your own machine, no API keys — connected to your node, and shows you how to share it. For coders and tinkerers; beginners can use the desktop app instead."
7105
7147
  },
@@ -52,7 +52,7 @@
52
52
  "profile.landing.menu": "Valikko",
53
53
  "profile.tabs.notebook": "Muistikirja",
54
54
  "profile.notebook.title": "Muistikirja",
55
- "profile.notebook.desc": "Kirjoita vapaasti mitä vain — talteen heti, löydä myöhemmin. Kirjastonhoitaja hakee kaikesta mitä sinulla on: jokaisesta organismista johon olet tuottanut sisältöä, sekä henkilökohtaisista muistiinpanoistasi.",
55
+ "profile.notebook.desc": "Kirjoita vapaasti mitä vain — talteen heti, löydä myöhemmin. Kirjastonhoitaja hakee kaikesta mitä sinulla on: jokaisesta yhteisestä tilasta (organismista) johon olet tuottanut sisältöä, sekä henkilökohtaisista muistiinpanoistasi.",
56
56
  "profile.notebook.capturePlaceholder": "Kirjoita mitä vain — ajatus, idea, muistiinpano talteen…",
57
57
  "profile.notebook.captureBtn": "Tallenna",
58
58
  "profile.notebook.captureHint": "Tallennettu muistikirjan inboxiin. Lajittelu organismeihin/workspaceihin tulee seuraavaksi.",
@@ -243,10 +243,25 @@
243
243
  "profile.scheduler.soon": "pian",
244
244
  "profile.scheduler.paused": "tauolla",
245
245
  "profile.scheduler.autoKey": "(auto)",
246
+ "appGrant": {
247
+ "title": "Sallitaanko tämän sovelluksen käyttää tietojasi?",
248
+ "intro": "Se saa oman rajatun ja peruutettavissa olevan tunnuksen — ei koskaan kirjautumisistuntoasi. Voit peruuttaa sen milloin tahansa kohdassa Profiili › Pääsy.",
249
+ "scopesLabel": "Pyydetty pääsy",
250
+ "approve": "Salli pääsy",
251
+ "approving": "Hyväksytään…",
252
+ "deny": "Estä",
253
+ "loginTitle": "Kirjaudu jatkaaksesi",
254
+ "loginBody": "Kirjaudu sisään aimeat.io-palveluun ja avaa sitten sovelluksen linkki uudelleen hyväksyäksesi pääsyn.",
255
+ "loginCta": "Kirjaudu",
256
+ "errorTitle": "Pääsyä ei voida myöntää",
257
+ "missing": "Ei valtuutuspyyntöä.",
258
+ "expired": "Tämä pyyntö on vanhentunut."
259
+ },
246
260
  "common": {
247
261
  "cancel": "Peruuta",
248
262
  "confirm": "Vahvista",
249
263
  "delete": "Poista",
264
+ "back": "Takaisin",
250
265
  "somethingWentWrong": "Jokin meni pieleen",
251
266
  "retry": "Yritä uudelleen",
252
267
  "save": "Tallenna",
@@ -380,8 +395,8 @@
380
395
  "offerPosted": "Palvelusi on nyt listattu hakemistoon.",
381
396
  "backToChoices": "Takaisin",
382
397
  "registerTitle": "Luo ilmainen identiteettisi",
383
- "registerDesc": "Kun rekisteröidyt, saat GHII:n (Global Human Intelligence Identifier) — oman digitaalisen identiteettisi. Tietosi on suojattu salasanallasi: vain sinä pääset niihin käsiksi. Kukaan muu ei näe yksityistä muistiasi, ei edes palvelimen ylläpitäjä. Sinun ei tarvitse jakaa mitään kenellekään.",
384
- "registerBtn": "Luo ilmainen GHII",
398
+ "registerDesc": "Kun rekisteröidyt, saat oman yksityisen tilan — oman digitaalisen identiteettisi. Tietosi on suojattu salasanallasi: vain sinä pääset niihin käsiksi. Kukaan muu ei näe yksityistä muistiasi, ei edes palvelimen ylläpitäjä. Sinun ei tarvitse jakaa mitään kenellekään.",
399
+ "registerBtn": "Luo oma tila — ilmainen",
385
400
  "registerBenefits": [
386
401
  "Oma yksityinen muistitila, salasanasuojattu",
387
402
  "Täysi hallinta — vain sinä pääset tietoihisi",
@@ -510,7 +525,7 @@
510
525
  "notFederated": "Vain paikallinen",
511
526
  "stats": {
512
527
  "agents": "Agentit",
513
- "morsels": "Muruset",
528
+ "morsels": "Sydänmuruset",
514
529
  "memories": "Muistit",
515
530
  "services": "Palvelut",
516
531
  "tasks": "Tehtävät",
@@ -586,12 +601,12 @@
586
601
  "rateFailed": "Arviointi epäonnistui",
587
602
  "selling": "Myynti",
588
603
  "priceMorsels": "Hinta",
589
- "morsels": "morselia",
604
+ "morsels": "murusia",
590
605
  "perCall": "kutsu",
591
606
  "saveBilling": "Tallenna",
592
607
  "billSaved": "Tallennettu.",
593
608
  "billSaveFailed": "Tallennus epäonnistui",
594
- "billHint": "Muut maksavat {n} morselia/kutsu; saat sen miinus markkinapaikan palkkio. Oma käyttösi on ilmaista.",
609
+ "billHint": "Muut maksavat {n} murusia/kutsu; saat sen miinus markkinapaikan palkkio. Oma käyttösi on ilmaista.",
595
610
  "notCallableHint": "Ei vielä koneellisesti kutsuttavissa — muut löytävät sen, mutta kutsuminen vaatii kyvykkyyssidoksen (action_id).",
596
611
  "openInbox": "Katso Inboxista →",
597
612
  "needsFirst": "Vaatii ensin",
@@ -769,7 +784,7 @@
769
784
  "setupStep3Why": "Kytke automaatio päälle, niin agenttisi käynnistyy heti kun uutta dataa saapuu — saat tuoreet oivallukset sormea liikuttamatta.",
770
785
  "setupStep3Cta": "Ota automaatio käyttöön",
771
786
  "setupStep4Title": "Saat oivallukset kaikkialle",
772
- "setupStep4Why": "Valitse organismi, niin tuloksesi näkyvät Claudessa / Grokissa / ChatGPT:ssä, valmiina uudelleenkäyttöön markkinointiin tai raportteihin ja jaettavissa tiimillesi — eivät lukittuna tämän yhden apin sisään.",
787
+ "setupStep4Why": "Valitse yhteinen tila (organismi), niin tuloksesi näkyvät Claudessa / Grokissa / ChatGPT:ssä, valmiina uudelleenkäyttöön markkinointiin tai raportteihin ja jaettavissa tiimillesi — eivät lukittuna tämän yhden apin sisään.",
773
788
  "setupStep4Cta": "Valitse organismi",
774
789
  "mcpTitle": "Kysy oivalluksiasi Claudessa, Grokissa tai ChatGPT:ssä",
775
790
  "mcpSub": "Yhdistä AIMEAT AI-chattiisi kerran → sen jälkeen vain kysy, ja saat heti vastaukset, tutki, tallenna ja seuraa — suoraan kaikesta mitä tämä appi ja agenttisi tuottavat.",
@@ -1228,7 +1243,7 @@
1228
1243
  "unpublished": "Palvelun julkaisu poistettu",
1229
1244
  "unnamed": "Nimetön",
1230
1245
  "free": "Ilmainen",
1231
- "morsels": "morselia",
1246
+ "morsels": "murusia",
1232
1247
  "avg": "ka.",
1233
1248
  "unpublishError": "Julkaisun poisto epäonnistui",
1234
1249
  "loadError": "Palveluiden lataus epäonnistui",
@@ -1715,7 +1730,7 @@
1715
1730
  },
1716
1731
  "wallet": {
1717
1732
  "title": "Lompakko",
1718
- "desc": "Muruset ovat verkon tapa sanoa “kiitos.” Ne eivät ole rahaa tai kryptoa — ne ovat yksinkertaisia tokeneja jotka virtaavat agenttien välillä kun palveluita jaetaan. Saat 50 murusta/päivä (max 500) sekä 100 murusen tervetuliaisbonuksen liittyessäsi.",
1733
+ "desc": "Sydänmuruset ovat verkon tapa sanoa “kiitos.” Ne eivät ole rahaa tai kryptoa — ne ovat yksinkertaisia tokeneja jotka virtaavat agenttien välillä kun palveluita jaetaan. Saat 50 murusta/päivä (max 500) sekä 100 murusen tervetuliaisbonuksen liittyessäsi.",
1719
1734
  "loading": "Ladataan lompakkoa...",
1720
1735
  "empty": "Lompakkotietoja ei saatavilla.",
1721
1736
  "error": "Lompakkotietojen lataus epäonnistui.",
@@ -1751,7 +1766,7 @@
1751
1766
  "lifetimeSpent": "Käytetty",
1752
1767
  "lifetimeAllowance": "Kiintiö vastaanotettu",
1753
1768
  "lifetimeWelcome": "Tervetuliaisbonus",
1754
- "federatedInfo": "Lompakkosi on kotisolmullasi. Morselisaldo, tapahtumat ja päiväkohtaiset lisäykset hallitaan siellä.",
1769
+ "federatedInfo": "Lompakkosi on kotisolmullasi. Murusaldo, tapahtumat ja päiväkohtaiset lisäykset hallitaan siellä.",
1755
1770
  "federatedHomeNode": "Kotisolmu"
1756
1771
  },
1757
1772
  "memory": {
@@ -2074,6 +2089,17 @@
2074
2089
  "hoverReveal": "Klikkaa kopioidaksesi",
2075
2090
  "keepSafe": "Pidä tämä turvassa — se on palautusavaimesi.",
2076
2091
  "keyCopied": "Avain kopioitu leikepöydälle!",
2092
+ "agTitle": "Yhdistetyt sovellukset",
2093
+ "agDesc": "Julkaistut sovellukset, joille olet myöntänyt rajatun pääsyn tietoihisi. Jokaisella on oma peruutettavissa oleva tunnus — ei koskaan kirjautumisistuntoasi.",
2094
+ "agLoading": "Ladataan...",
2095
+ "agEmpty": "Millään sovelluksella ei ole pääsyä tietoihisi.",
2096
+ "agScopes": "oikeutta",
2097
+ "agGrantedAt": "Myönnetty",
2098
+ "agLastUsed": "Viimeksi käytetty",
2099
+ "agNever": "ei koskaan",
2100
+ "agConfirmRevoke": "Peruutetaanko sovelluksen \"{name}\" pääsy? Se menettää pääsyn välittömästi.",
2101
+ "agRevoked": "Sovelluksen pääsy peruutettu",
2102
+ "agRevoke": "Peruuta",
2077
2103
  "patTitle": "Agenttien käyttötokenit",
2078
2104
  "patDesc": "Luo peruutettava token, jota agentti voi käyttää (Bearer-headerina) kirjautuakseen ja testatakseen appejasi. Yhtä tokenia voi jakaa kaikille agenteillesi.",
2079
2105
  "patLabelRequired": "Anna tokenille nimi",
@@ -3649,7 +3675,11 @@
3649
3675
  "realtimeMaxPeersPerRoom": "Enimmäismäärä osallistujia per huone",
3650
3676
  "extensionsEnabled": "Ota V8-eristetty laajennusjärjestelmä käyttöön?",
3651
3677
  "extensionMaxMemoryMb": "Enimmäismuisti per laajennus (MB)",
3652
- "extensionTimeoutMs": "Laajennuksen suorituksen aikakatkaisu (ms)"
3678
+ "extensionTimeoutMs": "Laajennuksen suorituksen aikakatkaisu (ms)",
3679
+ "appOrigin": "Tarjotaanko käyttäjien sovellukset erillisestä sovellusalkuperästä (*.apps.<domain>) niiden eristämiseksi istunnostasi? Vaatii DNS:n + jokerimerkki-TLS-varmenteen (katso docs/internal/app-origin-deployment.md). Oletuksena pois päältä.",
3680
+ "appHost": "Sovellusalkuperän isäntä (esim. apps.example.com)",
3681
+ "appHostHint": "Määritä ensin DNS (apps.<domain> + jokerimerkki *.apps.<domain>) ja jokerimerkki-TLS-varmenne — katso docs/internal/app-origin-deployment.md",
3682
+ "appHostRequired": "Sovellusisäntä vaaditaan, kun sovellusalkuperän eristys on käytössä"
3653
3683
  },
3654
3684
  "join": {
3655
3685
  "title": "Liity AIMEAT-federaatioon",
@@ -3730,6 +3760,8 @@
3730
3760
  "displayNamePlaceholder": "Näyttönimi (valinnainen, uusille tileille)",
3731
3761
  "signInBtn": "Kirjaudu / Rekisteröidy",
3732
3762
  "cancelBtn": "Peruuta",
3763
+ "orLabel": "TAI",
3764
+ "googleSignIn": "Jatka Googlella",
3733
3765
  "working": "Käsitellään...",
3734
3766
  "errUserShort": "Käyttäjänimen tulee olla vähintään 3 merkkiä",
3735
3767
  "errPassShort": "Salasanan tulee olla vähintään 4 merkkiä",
@@ -3762,7 +3794,7 @@
3762
3794
  "sendUsername": "Lähetä käyttäjätunnukseni",
3763
3795
  "usernameSent": "Jos sähköpostiosoitteella on tili, käyttäjätunnus lähetettiin.",
3764
3796
  "whyTitle": "✨ Mitä saat?",
3765
- "whyGhii": "Ilmaisen GHII:n (Global Human Intelligence Identifier), oma henkilökohtainen tekoälyidentiteettisi",
3797
+ "whyGhii": "Oma digitaalinen identiteetti vain sinä hallitset sitä",
3766
3798
  "whyPrivacy": "Oman yksityisen muistitilan, suojattuna salasanallasi",
3767
3799
  "whyControl": "Täyden hallinnan: vain sinä pääset tietoihisi, ei tarvitse jakaa kenellekään",
3768
3800
  "whyAgents": "Yhdistä AI-agentteja jotka muistavat sinut ja toimivat puolestasi",
@@ -5536,7 +5568,7 @@
5536
5568
  "loggedIn": "kirjautuneena",
5537
5569
  "logoutBtn": "Kirjaudu ulos",
5538
5570
  "whyTitle": "✨ Mitä saat?",
5539
- "whyGhii": "Ilmaisen GHII:n (Global Human Intelligence Identifier), oma henkilökohtainen tekoälyidentiteettisi",
5571
+ "whyGhii": "Oma digitaalinen identiteetti vain sinä hallitset sitä",
5540
5572
  "whyPrivacy": "Oman yksityisen muistitilan, suojattuna salasanallasi",
5541
5573
  "whyControl": "Täyden hallinnan: vain sinä pääset tietoihisi, ei tarvitse jakaa kenellekään",
5542
5574
  "whyAgents": "Yhdistä AI-agentteja jotka muistavat sinut ja toimivat puolestasi",
@@ -6560,7 +6592,7 @@
6560
6592
  "usernameLabel": "Käyttäjänimi",
6561
6593
  "passwordLabel": "Salasana",
6562
6594
  "totpLabel": "2FA-koodi (jos käytössä)",
6563
- "morsels": "morselia",
6595
+ "morsels": "murusia",
6564
6596
  "doneTitle": "Valmis",
6565
6597
  "authCodeLabel": "Valtuutuskoodi:",
6566
6598
  "authCodeHint": "Liitä tämä koodi takaisin sovellukseen, joka pyysi pääsyä.",
@@ -7100,6 +7132,16 @@
7100
7132
  "heroSub2": "Oikea appi, jonka tekoäly rakensi ja ajaa AIMEATissa — katso ennen kuin rekisteröidyt.",
7101
7133
  "heroTryFree": "Kokeile ilmaiseksi →",
7102
7134
  "heroGetOwn": "Hanki oma →",
7135
+ "buildHeroKicker": "Turvallinen paikka rakentaa AI:lla.",
7136
+ "buildHeroTitle": "Rakenna oikea appsi AI:llasi minuuteissa, ja se on sinun.",
7137
+ "buildHeroSub": "Kopioi yksi prompti Claudeen, ChatGPT:hen tai mihin tahansa AI:hin. Se rakentaa sinulle toimivan appsin AIMEATiin, julkaistuna heti ja sinun pidettäväksi. Anna sitten agenttiesi pyörittää sitä puolestasi, kuten AIMEAT Sanomat kirjoittaa itsensä joka ilta.",
7138
+ "buildHeroCopy": "Kopioi build-prompti →",
7139
+ "buildHeroCopied": "Kopioitu ✓ — liitä AI:hisi",
7140
+ "wallTitle": "Ihmisten AI:llaan rakentamia. Sinun tulee tähän.",
7141
+ "wallEmpty": "Ole ensimmäinen — kopioi prompti yltä, rakenna jotain, ja se ilmestyy tähän.",
7142
+ "wallAnon": "joku",
7143
+ "wallSearch": "Hae appseja…",
7144
+ "wallNoMatch": "Mikään appsi ei vastaa hakuasi.",
7103
7145
  "agentBuildTitle": "Rakenna agentti 10 minuutissa — kopioi tämä prompti",
7104
7146
  "agentBuildSub": "Liitä Claudeen, ChatGPT:hen tai mihin tahansa tekoälyyn. Se rakentaa paikallisen AI-agentin — pyörii omalla koneellasi, ei API-avaimia — yhdistettynä nodeesi, ja näyttää miten jaat sen. Koodareille ja näpertelijöille; aloittelijat voivat käyttää desktop-sovellusta."
7105
7147
  },
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @file public/css/components/app-sandbox.css
3
+ * @description Full-screen overlay that hosts a sandboxed (opaque-origin) iframe for
4
+ * viewing user-published apps without granting them the apex origin (H-2). Paired with
5
+ * /js/app-sandbox.js (openAppSandboxed).
6
+ * @usage Linked from spa.html; classes applied by /js/app-sandbox.js.
7
+ * @version-history
8
+ * v1.0.0 — 2026-06-20 — Initial (H-2 app-origin isolation, Phase 0).
9
+ */
10
+
11
+ .app-sandbox-overlay {
12
+ position: fixed;
13
+ inset: 0;
14
+ z-index: 10000;
15
+ display: flex;
16
+ flex-direction: column;
17
+ background: var(--bg, #0e0e14);
18
+ }
19
+
20
+ .app-sandbox-toolbar {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 0.75rem;
24
+ padding: 0.5rem 0.75rem;
25
+ background: var(--surface, #1a1a26);
26
+ border-bottom: 1px solid var(--border, #2a2a3a);
27
+ flex: 0 0 auto;
28
+ }
29
+
30
+ .app-sandbox-title {
31
+ flex: 1 1 auto;
32
+ font-weight: 600;
33
+ font-size: 0.95rem;
34
+ color: var(--text, #e8e8f0);
35
+ white-space: nowrap;
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+ }
39
+
40
+ .app-sandbox-close {
41
+ flex: 0 0 auto;
42
+ width: 2rem;
43
+ height: 2rem;
44
+ border: 1px solid var(--border, #2a2a3a);
45
+ border-radius: 6px;
46
+ background: var(--bg-dim, #14141c);
47
+ color: var(--text, #e8e8f0);
48
+ font-size: 1rem;
49
+ line-height: 1;
50
+ cursor: pointer;
51
+ }
52
+
53
+ .app-sandbox-close:hover {
54
+ border-color: var(--border-hover, #3a3a4a);
55
+ }
56
+
57
+ .app-sandbox-frame {
58
+ flex: 1 1 auto;
59
+ width: 100%;
60
+ border: 0;
61
+ background: #fff;
62
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @file public/css/views/app-grant.css
3
+ * @description Styles for the app-grant consent page (agr- prefix) — the trusted apex approval
4
+ * screen where an owner reviews a published app's requested scopes (H-2 Phase 3).
5
+ * @version-history
6
+ * v1.0.0 — 2026-06-20 — Initial (H-2 app-origin isolation, Phase 3).
7
+ */
8
+
9
+ .agr-wrap { max-width: 540px; margin: 3rem auto; padding: 0 1.25rem; }
10
+
11
+ .agr-card {
12
+ border: 1px solid var(--border);
13
+ border-radius: 14px;
14
+ background: var(--card);
15
+ padding: 1.75rem 1.6rem;
16
+ }
17
+
18
+ .agr-title { font-size: 1.4rem; font-weight: 800; margin: 0 0 1rem; color: var(--text); }
19
+
20
+ .agr-app {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 0.15rem;
24
+ padding: 0.8rem 1rem;
25
+ border: 1px solid var(--border);
26
+ border-radius: 10px;
27
+ background: var(--bg-dim);
28
+ margin-bottom: 1rem;
29
+ }
30
+ .agr-app-name { font-weight: 700; color: var(--text); }
31
+ .agr-app-origin { font-size: 0.82rem; color: var(--text-dim); font-family: var(--font-mono, monospace); }
32
+
33
+ .agr-muted { color: var(--text-dim); font-size: 0.92rem; margin: 0 0 1rem; }
34
+
35
+ .agr-scopes-label { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.5rem; color: var(--text); }
36
+
37
+ .agr-scopes { list-style: none; margin: 0 0 1.5rem; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
38
+ .agr-scope {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 0.1rem;
42
+ padding: 0.6rem 0.8rem;
43
+ border: 1px solid var(--border);
44
+ border-radius: 8px;
45
+ background: var(--surface, var(--bg-dim));
46
+ }
47
+ .agr-scope-name { font-family: var(--font-mono, monospace); font-size: 0.82rem; color: var(--accent); }
48
+ .agr-scope-desc { font-size: 0.88rem; color: var(--text); }
49
+
50
+ .agr-actions { display: flex; gap: 0.75rem; justify-content: flex-end; }
51
+ .agr-btn { min-width: 8rem; text-align: center; }
@@ -7,6 +7,8 @@
7
7
  * v1.1.0 — 2026-06-16 — Add public activity feed styles (pa- prefix).
8
8
  * v1.2.0 — 2026-06-16 — Add reward-first Hero v2 styles (ld-hero2-*): newspaper-framed
9
9
  * Sanomat teaser (designed masthead card; real screenshot when one exists), mobile-first, two CTAs.
10
+ * v1.3.0 — 2026-06-20 — Live wall: .ld-gallery is now a fixed 3-up grid (2-up tablet, 1-up phone)
11
+ * + .ld-wall-search filter box, for the real published-apps wall.
10
12
  */
11
13
  .ld { max-width: 980px; margin: 0 auto; padding: 2rem 1.25rem 3rem; color: var(--text); }
12
14
 
@@ -59,9 +61,13 @@
59
61
  .ld-loop-arrow { color: var(--text-dim); }
60
62
  .ld-section { margin: 2rem 0; }
61
63
  .ld-h2 { font-size: 1.3rem; margin: 0 0 1rem; }
62
- .ld-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .9rem; }
63
- .ld-app-card { display: block; border: 1px solid var(--border); border-radius: 10px; background: var(--card); padding: .9rem 1rem; text-decoration: none; color: var(--text); }
64
+ .ld-wall-search { width: 100%; max-width: 360px; margin: 0 0 1rem; padding: .55rem .8rem; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--text); font-size: .9rem; }
65
+ .ld-gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: .9rem; }
66
+ @media (max-width: 900px) { .ld-gallery { grid-template-columns: repeat(2, 1fr); } }
67
+ @media (max-width: 560px) { .ld-gallery { grid-template-columns: 1fr; } }
68
+ .ld-app-card { display: block; border: 1px solid var(--border); border-radius: 10px; background: var(--card); padding: .9rem 1rem; text-decoration: none; color: var(--text); cursor: pointer; }
64
69
  .ld-app-card:hover { border-color: var(--accent); }
70
+ .ld-app-shot { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); margin-bottom: .55rem; background: var(--bg-dim); }
65
71
  .ld-app-name { font-weight: 700; margin-bottom: .25rem; }
66
72
  .ld-app-desc { font-size: .82rem; color: var(--text-dim); margin-bottom: .4rem; }
67
73
  .ld-app-meta { font-size: .72rem; color: var(--muted); }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @file public/js/app-sandbox.js
3
+ * @description Open a user-published app's HTML inside a full-screen, sandboxed,
4
+ * opaque-origin iframe overlay — the H-2-safe way to "view" an app from anywhere in
5
+ * the SPA. The iframe's `sandbox` deliberately omits `allow-same-origin`, so even
6
+ * apex-hosted (`/v1/apps/...?mode=inline`) content runs with an opaque origin and can
7
+ * neither read aimeat.io localStorage nor send credentialed same-origin requests
8
+ * (e.g. POST /v1/auth/refresh). Replaces every top-level `window.open(...?mode=inline)`
9
+ * / `<a target="_blank">` app link that previously ran app HTML on the apex origin.
10
+ * @structure isAppHtmlUrl() route matcher · openAppSandboxed() overlay builder.
11
+ * @usage import { openAppSandboxed, isAppHtmlUrl } from '/js/app-sandbox.js'
12
+ * @version-history
13
+ * v1.0.0 — 2026-06-20 — Initial (H-2 app-origin isolation, Phase 0).
14
+ */
15
+
16
+ /** Matches the published-app HTML route: /v1/apps/<owner>/<file> (optionally ?mode=inline). */
17
+ export function isAppHtmlUrl(href) {
18
+ if (!href) return false;
19
+ let path;
20
+ try {
21
+ path = new URL(href, window.location.href).pathname;
22
+ } catch {
23
+ return false;
24
+ }
25
+ // /v1/apps/<owner>/<file> — two path segments after /v1/apps/. The bare collection
26
+ // (/v1/apps) and single-segment forms are API/JSON, not runnable HTML.
27
+ return /^\/v1\/apps\/[^/]+\/[^/]+$/.test(path);
28
+ }
29
+
30
+ let escHandler = null;
31
+
32
+ /** Remove the overlay (if present) and detach the Escape handler. */
33
+ function closeOverlay() {
34
+ const existing = document.getElementById('app-sandbox-overlay');
35
+ if (existing) existing.remove();
36
+ if (escHandler) {
37
+ document.removeEventListener('keydown', escHandler);
38
+ escHandler = null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Show `url` in a sandboxed (opaque-origin) iframe overlay. `name` labels the toolbar.
44
+ * Any previously-open overlay is replaced.
45
+ */
46
+ export function openAppSandboxed(url, name) {
47
+ closeOverlay();
48
+
49
+ const overlay = document.createElement('div');
50
+ overlay.id = 'app-sandbox-overlay';
51
+ overlay.className = 'app-sandbox-overlay';
52
+
53
+ const toolbar = document.createElement('div');
54
+ toolbar.className = 'app-sandbox-toolbar';
55
+
56
+ const title = document.createElement('span');
57
+ title.className = 'app-sandbox-title';
58
+ title.textContent = name || 'App';
59
+
60
+ const close = document.createElement('button');
61
+ close.type = 'button';
62
+ close.className = 'app-sandbox-close';
63
+ close.setAttribute('aria-label', 'Close');
64
+ close.textContent = '✕';
65
+ close.addEventListener('click', closeOverlay);
66
+
67
+ toolbar.appendChild(title);
68
+ toolbar.appendChild(close);
69
+
70
+ const iframe = document.createElement('iframe');
71
+ iframe.className = 'app-sandbox-frame';
72
+ // No allow-same-origin → opaque origin. This is the entire H-2 protection.
73
+ iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals');
74
+ iframe.setAttribute('referrerpolicy', 'no-referrer');
75
+ iframe.src = url;
76
+
77
+ overlay.appendChild(toolbar);
78
+ overlay.appendChild(iframe);
79
+ document.body.appendChild(overlay);
80
+
81
+ escHandler = (e) => { if (e.key === 'Escape') closeOverlay(); };
82
+ document.addEventListener('keydown', escHandler);
83
+ }