diff --git a/README.md b/README.md index 1fee5e9..86ef69a 100644 --- a/README.md +++ b/README.md @@ -165,11 +165,19 @@ Example: "schemaVersion": 1, "id": "com.example.notes", "name": "Notes", - "version": "0.0.0", - "runtime": "route-source", - "permissions": [ - "storage:readWrite" - ], + "version": "0.1.0", + "runtime": "standalone", + "runtimeConfig": { + "engine": "react" + }, + "permissions": ["storage:readWrite"], + "launch": { + "path": "/apps/com.example.notes" + }, + "extensionPoints": { + "launcher": true, + "sidebar": true + }, "compatibility": { "minPlatformVersion": "0.1.0" } @@ -183,21 +191,106 @@ Example: Current runtime types: ```txt -route-source -iframe-local -iframe-remote -external +standalone +dom +iframe ``` -Initial versions of Sovereign focus on: +## `standalone` + +Apps loaded directly by the Sovereign platform. + +Use this for trusted plugins that should be served or bundled by the platform itself. + +Supported engines: ```txt -route-source +react +html +``` + +Example: + +```json +{ + "runtime": "standalone", + "runtimeConfig": { + "engine": "react" + } +} +``` + +Standalone HTML apps use a plugin-local entrypoint: + +```json +{ + "runtime": "standalone", + "runtimeConfig": { + "engine": "html", + "entrypoint": "index.html" + } +} ``` -apps built directly into the platform runtime. +## `dom` -Sandboxed runtimes will be introduced later. +Apps served by a DOM application server and embedded by Sovereign. + +Use this for Vite, React, or other DOM app development servers. + +Example: + +```json +{ + "runtime": "dom", + "runtimeConfig": { + "engine": "react", + "host": "localhost", + "port": "4000" + } +} +``` + +## `iframe` + +Apps hosted in an iframe. The iframe can point at a local plugin entrypoint, a local dev server, or a remote host. + +Local HTML entrypoint: + +```json +{ + "runtime": "iframe", + "runtimeConfig": { + "engine": "html", + "entrypoint": "iframe/index.html" + } +} +``` + +Local or remote host: + +```json +{ + "runtime": "iframe", + "runtimeConfig": { + "engine": "*", + "host": "example.com", + "https": true, + "uri": "/#test" + } +} +``` + +The current manifest schema uses flat `runtimeConfig` fields: + +```txt +engine +entrypoint +host +port +https +uri +``` --- @@ -209,10 +302,16 @@ Examples: ```txt auth:profile -storage:readWrite +auth:read +auth:write +storage:read +storage:write notifications:send -files:pick +notifications:recieve +fs:read +fs:write events:publish +events:subscribe ``` Apps interact with platform features through the Sovereign SDK. @@ -249,7 +348,7 @@ yarn generate ## Validate App Manifest ```bash -yarn validate:manifest plugins/com.sovereign.launcher/manifest.json +yarn validate:manifest plugins/com.sovereign-demo.iframe-html/manifest.json ``` --- @@ -272,4 +371,4 @@ Current focus areas: # License -AGPL-3.0 \ No newline at end of file +AGPL-3.0 diff --git a/packages/manifest/schema/manifest.schema.json b/packages/manifest/schema/manifest.schema.json index 1461854..4ddb633 100644 --- a/packages/manifest/schema/manifest.schema.json +++ b/packages/manifest/schema/manifest.schema.json @@ -20,7 +20,7 @@ "version": { "type": "string" }, "runtime": { "type": "string", - "enum": ["internal", "route-source", "iframe-local", "iframe-remote", "external"] + "enum": ["standalone", "dom", "iframe"] }, "permissions": { "type": "array", @@ -42,40 +42,35 @@ "properties": { "engine": { "type": "string", - "enum": ["vite:react-ts"] + "enum": ["react", "html", "*"] }, - "iframeLocal": { - "type": "object", - "required": ["entrypoint"], - "properties": { - "entrypoint": { - "type": "string", - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" - } - }, - "additionalProperties": false + "host": { + "type": "string", + "minLength": 1 }, - "iframeRemote": { - "type": "object", - "required": ["url"], - "properties": { - "url": { + "port": { + "oneOf": [ + { "type": "string", - "pattern": "^https://.+" + "pattern": "^[0-9]+$" + }, + { + "type": "integer", + "minimum": 1, + "maximum": 65535 } - }, - "additionalProperties": false + ] }, - "external": { - "type": "object", - "required": ["url"], - "properties": { - "url": { - "type": "string", - "pattern": "^https://.+" - } - }, - "additionalProperties": false + "https": { + "type": "boolean" + }, + "uri": { + "type": "string", + "pattern": "^/" + }, + "entrypoint": { + "type": "string", + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" } }, "additionalProperties": false @@ -107,7 +102,7 @@ { "if": { "properties": { - "runtime": { "const": "internal" } + "runtime": { "const": "standalone" } }, "required": ["runtime"] }, @@ -116,7 +111,12 @@ "properties": { "runtimeConfig": { "type": "object", - "required": ["engine"] + "required": ["engine"], + "properties": { + "engine": { + "enum": ["react", "html"] + } + } } } } @@ -124,7 +124,7 @@ { "if": { "properties": { - "runtime": { "const": "iframe-local" } + "runtime": { "const": "dom" } }, "required": ["runtime"] }, @@ -133,7 +133,7 @@ "properties": { "runtimeConfig": { "type": "object", - "required": ["iframeLocal"] + "required": ["engine", "host", "port"] } } } @@ -141,16 +141,22 @@ { "if": { "properties": { - "runtime": { "const": "iframe-remote" } + "runtime": { "const": "standalone" }, + "runtimeConfig": { + "type": "object", + "properties": { + "engine": { "const": "html" } + }, + "required": ["engine"] + } }, - "required": ["runtime"] + "required": ["runtime", "runtimeConfig"] }, "then": { - "required": ["runtimeConfig"], "properties": { "runtimeConfig": { "type": "object", - "required": ["iframeRemote"] + "required": ["entrypoint"] } } } @@ -158,16 +164,36 @@ { "if": { "properties": { - "runtime": { "const": "external" } + "runtime": { "const": "iframe" }, + "runtimeConfig": { + "type": "object", + "required": ["entrypoint"] + } }, - "required": ["runtime"] + "required": ["runtime", "runtimeConfig"] }, "then": { - "required": ["runtimeConfig"], "properties": { "runtimeConfig": { "type": "object", - "required": ["external"] + "required": ["engine"] + } + } + }, + "else": { + "if": { + "properties": { + "runtime": { "const": "iframe" } + }, + "required": ["runtime"] + }, + "then": { + "required": ["runtimeConfig"], + "properties": { + "runtimeConfig": { + "type": "object", + "required": ["engine", "host"] + } } } } diff --git a/packages/manifest/src/index.ts b/packages/manifest/src/index.ts index 518f71b..c3ab932 100644 --- a/packages/manifest/src/index.ts +++ b/packages/manifest/src/index.ts @@ -4,11 +4,14 @@ export * from "./validate"; export * from "./permissions"; export type SovereignRuntime = - | "internal" - | "route-source" - | "iframe-local" - | "iframe-remote" - | "external"; + | "standalone" + | "dom" + | "iframe"; + +export type SovereignRuntimeEngine = + | "react" + | "html" + | "*"; export interface SovereignAppManifest { schemaVersion: 1; @@ -21,16 +24,12 @@ export interface SovereignAppManifest { path: string; }; runtimeConfig?: { - engine?: "vite:react-ts"; - iframeLocal?: { - entrypoint: string; - }; - iframeRemote?: { - url: string; - }; - external?: { - url: string; - }; + engine?: SovereignRuntimeEngine; + host?: string; + port?: string | number; + https?: boolean; + uri?: string; + entrypoint?: string; }; extensionPoints?: { launcher?: boolean; diff --git a/packages/manifest/src/permissions.ts b/packages/manifest/src/permissions.ts index f07472d..ed657d2 100644 --- a/packages/manifest/src/permissions.ts +++ b/packages/manifest/src/permissions.ts @@ -1,5 +1,7 @@ export const SovereignPermissions = { AuthProfile: "auth:profile", + AuthRead: "auth:read", + AuthWrite: "auth:write", StorageReadWrite: "storage:readWrite", EventsPublish: "events:publish", NotificationsSend: "notifications:send", diff --git a/platform/app/api/apps/[appId]/iframe/[[...assetPath]]/route.ts b/platform/app/api/apps/[appId]/iframe/[[...assetPath]]/route.ts index f588946..5a490ab 100644 --- a/platform/app/api/apps/[appId]/iframe/[[...assetPath]]/route.ts +++ b/platform/app/api/apps/[appId]/iframe/[[...assetPath]]/route.ts @@ -26,11 +26,11 @@ export async function GET(_request: Request, { params }: IframeAssetRouteProps) const { appId, assetPath = [] } = await params; const app = resolveApp(appId); - if (app?.runtime !== "iframe-local") { + if (!app || !app.runtimeConfig?.entrypoint) { notFound(); } - const entrypoint = app.runtimeConfig?.iframeLocal?.entrypoint; + const entrypoint = app.runtimeConfig?.entrypoint; if (!entrypoint || !isSafeRelativePath(entrypoint)) { notFound(); diff --git a/platform/src/launcher/get-installed-apps.ts b/platform/src/launcher/get-installed-apps.ts index 6b2ee26..fda72cf 100644 --- a/platform/src/launcher/get-installed-apps.ts +++ b/platform/src/launcher/get-installed-apps.ts @@ -1,6 +1,7 @@ import { installedApps } from "../../generated/apps.generated"; +import type { InstalledSovereignApp } from "../runtime"; -export function getInstalledApps() { +export function getInstalledApps(): readonly InstalledSovereignApp[] { return installedApps; } diff --git a/platform/src/runtime/external-app-runtime.tsx b/platform/src/runtime/external-app-runtime.tsx deleted file mode 100644 index d50558e..0000000 --- a/platform/src/runtime/external-app-runtime.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { InstalledSovereignApp } from "./types"; - -interface ExternalAppRuntimeProps { - app: InstalledSovereignApp; -} - -export function ExternalAppRuntime({ app }: ExternalAppRuntimeProps) { - const external = app.runtimeConfig?.external; - - if (!external || !isHttpsUrl(external.url)) { - return

External runtime URL not configured.

; - } - - return ( -
-

{app.name} runs outside the Sovereign runtime.

-

- - Open {app.name} - -

-
- ); -} - -function isHttpsUrl(input: string) { - try { - return new URL(input).protocol === "https:"; - } catch { - return false; - } -} diff --git a/platform/src/runtime/iframe-local-runtime.tsx b/platform/src/runtime/iframe-local-runtime.tsx index a066ee3..0f162fd 100644 --- a/platform/src/runtime/iframe-local-runtime.tsx +++ b/platform/src/runtime/iframe-local-runtime.tsx @@ -7,13 +7,31 @@ interface IframeLocalRuntimeProps { } export function IframeLocalRuntime({ app, appPath }: IframeLocalRuntimeProps) { - const iframeLocal = app.runtimeConfig?.iframeLocal; + const entrypoint = app.runtimeConfig?.entrypoint; - if (!iframeLocal) { - return

Iframe runtime entrypoint not configured.

; + if (!entrypoint) { + const remoteUrl = getRemoteIframeUrl(app); + + if (!remoteUrl) { + return

Iframe runtime URL not configured.

; + } + + return ( +