diff --git a/README.md b/README.md index 8eda56f..690d38f 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ dictionary SubAppsListResult { - **Returned Object**: `SubAppsAddResponse` contains records mapping each input `InstallPath` provided to the call to either its success or failure state. Developers must use the `InstallPath` key to identify which call failed and which succeeded: - `installedApps`: A record mapping the `InstallPath` to the successfully installed sub-app's `ManifestId`. `ManifestId` is a stable identified of a sub app, it is later used in `subApps.remove` and `subApps.list` calls. For what is `ManifestId` exactly, please, consult [Sub App identity](#sub-app-identity) section. - `failedApps`: A record mapping the `InstallPath` to a `DOMException` explaining why that individual sub-app failed to install. Possible exceptions include: - - `ConstraintError`: Provided sub app scope web manifest property overlaps with scopes of other sub apps or the parent app or the provided install url pointed to the app web manifest (itself). + - `ConstraintError`: Provided sub app scope is a prefix of another sub app's scope (or vice versa), or the parent app's scope is a prefix of the sub app's scope, or the provided install url pointed to the parent app's web manifest (itself). - `DataError`: The referenced web manifest was invalid or could not be parsed. - `InvalidStateError`: The sub-app is already installed. - `OperationError`: A generic system or database failure occurred during the installation of this specific sub-app. @@ -314,14 +314,14 @@ Two distinct options were considered for the return types of the `add()` and `re ### Sub App Identity -Sub apps rely on standard web manifest identity fields, but enforce specific isolation and overlap rules to guarantee they do not conflict with each other or the parent application. +Sub apps rely on standard web manifest identity fields, but enforce specific isolation and scope validation rules to guarantee they do not conflict with each other or the parent application. | Identifier | Relationship to Parent / Generation Rule | | :--- | :--- | | **Origin** | Identical to the parent IWA. | | **SignedWebBundleId** | Identical to the parent IWA. | | **Start URL** | Must be defined in the web manifest. Must be unique between sub-apps and the parent app. | -| **Scope** | Might be defined in the web manifest. Scopes must not overlap between sub-apps. The scope of a sub-app must not overlap with or cover the parent app's scope. Otherwise, the installation/update fails. | +| **Scope** | Might be defined in the web manifest. Scopes of sub apps must not be a prefix of one another, and scope of the parent app must not be prefix of sub app scope. Otherwise, the installation/update fails. | | **URL** | Parent IWA origin + `/` + `sub_app_start_url`. *Example:* `isolated-app://pl2ctdpnkf7ltse22mpjdb376etd3ydo7s72lgspuopgzcwl5tkqaaic/sub/calculator.html` | | **AppId** | It is unique between all apps of a particular user (regardless if it is a sub app or usual app). It is derived from ManifestId. | | **ManifestId** | Unique for each sub app. Defined in the web manifest, if it is empty, it falls back to the `start_url` without the reference/hash fragment. *Example:* `/sub/calculator.html` | diff --git a/index.bs b/index.bs index d6954f7..38172b4 100644 --- a/index.bs +++ b/index.bs @@ -2,11 +2,12 @@ Title: Sub Apps Shortname: sub-apps Level: None -Status: w3c/UD -Repository: explainers-by-googlers/sub-apps -URL: https://explainers-by-googlers.github.io/sub-apps +Status: UD +Group: wicg +Repository: WICG/sub-apps +URL: https://WICG.github.io/sub-apps Editor: Vlad Krot, Google https://google.com, vkrot@google.com -Abstract: The Sub Apps API allows a parent Isolated Web App (IWA) to programmatically install, list, and remove auxiliary applications sharing the parent's origin, storage, and lifecycle bounds, while presenting distinct names, icons, and window identities to the operating system. +Abstract: The Sub Apps API allows a parent application context to programmatically install, list, and remove auxiliary applications sharing the parent's origin, storage, and lifecycle bounds, while presenting distinct names, icons, and window identities to the operating system. Markup Shorthands: markdown yes, css no Complain About: accidental-2119 yes, missing-example-ids yes Assume Explicit For: yes @@ -15,14 +16,336 @@ WPT Path Prefix: sub-apps WPT Display: closed Include MDN Panels: if possible Include Can I Use Panels: yes +Boilerplate: conformance-tests no + + + Introduction {#intro} ===================== -For now, see the explainer. +The Sub Apps API allows a parent application to programmatically install, list, and remove auxiliary applications (Sub Apps) that: + +1. Appear to the operating system and user as fully distinct applications (separate launcher icons, distinct taskbar/shelf windows, and individual OS integrations). + +1. Share the underlying resources, origin, storage, permissions, and update lifecycle of the parent application. + +This API is restricted to [=isolated contexts=] to ensure security and data integrity. + +Concepts {#concepts} +==================== + +An [=installed web application=] has an associated parent app, which is either null or an [=installed web application=]. + +An [=installed web application=] has an associated sub-apps set, which is a [=set=] of [=installed web application=]s. + +A {{Document}} has an associated installed web application, which is the [=installed web application=] that the {{Document}} is presented as part of. The exact mechanism for this association is [=implementation-defined=]. + +A {{Document}} is a sub-app document if it has an [=Document/associated installed web application=] and its [=Document/associated installed web application=]'s [=installed web application/parent app=] is not null. + +Extensions to the {{Window}} interface {#window-extensions} +======================================================= + + +[Exposed=Window, SecureContext, IsolatedContext] +partial interface Window { + [SameObject] readonly attribute SubApps subApps; +}; + + +## {{Window/subApps}} attribute ## {#window-subapps-attribute} + +Each {{Window}} object has an associated subApps, which is a {{SubApps}} instance created alongside the {{Window}}. + +
+ The subApps getter steps are: + + 1. Return [=this=]'s [=Window/subApps=]. +
+ +{{SubApps}} interface {#subapps-interface} +========================================== + + +// Represents the https://w3c.github.io/manifest/#id-member +typedef USVString ManifestId; + +dictionary SubAppsAddResponse { + record<USVString, ManifestId> installedApps; + record<USVString, DOMException> failedApps; +}; + +dictionary SubAppsRemoveResponse { + sequence<ManifestId> removedApps; + record<USVString, DOMException> failedApps; +}; + +dictionary SubAppsListResult { + required DOMString appName; +}; + +[ + Exposed=Window, + SecureContext, + IsolatedContext +] interface SubApps { + Promise<SubAppsAddResponse> add(sequence<USVString> install_paths); + Promise<SubAppsRemoveResponse> remove(sequence<ManifestId> manifest_ids); + Promise<record<USVString, SubAppsListResult>> list(); +}; + + +A string |path| is a valid relative path for a {{Document}} |document| if all of the following conditions are met: + +1. |path| is not a valid absolute URL. + +1. |path| starts with `"/"`. + +1. |path| is not empty. + +1. |path| does not start with `"//"`. + +1. The result of [=parse a url|parsing=] |path| with |document|'s [=document base URL=] as the base URL is not failure. + +## {{SubApps/add()}} method ## {#subapps-add} + +The {{SubApps/add(install_paths)}} method steps are: + +

The {{SubApps/add(install_paths)/install_paths}} argument is a list of relative paths pointing to the sub-apps' start HTML pages.

+ +1. Let |promise| be [=a new promise=]. + +1. Let |document| be the [=relevant global object=]'s [=associated Document=]. + +1. If |document| is not [=allowed to use=] the [=policy-controlled feature=] named "[=policy-controlled feature/sub-apps=]", [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}} and return |promise|. + +1. If |document| is a [=Document/sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + +1. Let |parsedUrls| be an empty list. + +1. [=list/iterate|For each=] |installPath| in {{SubApps/add(install_paths)/install_paths}}: + + 1. If |installPath| is not a [=valid relative path=] for |document|, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + + 1. Let |absoluteUrl| be the result of [=parse a url|parsing=] |installPath| with |document|'s [=document base URL=] as the base URL. + + 1. If |absoluteUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + + 1. Append |absoluteUrl| to |parsedUrls|. + +1. Let |parentApp| be |document|'s [=Document/associated installed web application=]. + +1. Let |currentSubAppsCount| be the [=set/size|size=] of |parentApp|'s [=installed web application/sub-apps set=]. + +1. If |currentSubAppsCount| + {{SubApps/add(install_paths)/install_paths}}'s [=list/size=] is greater than 20, [=reject=] |promise| with a "{{QuotaExceededError}}" {{DOMException}} and return |promise|. + +1. Let |subApps| be [=this=]. + +1. Run the following steps [=in parallel=]: + + 1. Let |userConsent| be the result of requesting user consent for installing the sub-apps in {{SubApps/add(install_paths)/install_paths}} (e.g. by presenting a unified installation dialog). + + 1. If |userConsent| is denied, [=queue a global task=] on the [=relevant global object=] of |subApps| to [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}}, and abort these steps. + + 1. Let |installedApps| be an empty map. + + 1. Let |failedApps| be an empty map. + + 1. [=list/iterate|For each=] |absoluteUrl| in |parsedUrls|: + + 1. Let |installPath| be |absoluteUrl|'s [=url/path=]. + + 1. Let |manifest| be the result of [=fetch and process the manifest=] given |absoluteUrl|. + + 1. If |manifest| is failure, perform the following steps: + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. + 1. [=iteration/continue|Continue=]. + + 1. Let |manifestId| be |manifest|'s [=manifest/id=]. If it is not defined, fall back to |manifest|'s [=manifest/start_url=] (without reference/hash fragment). + + 1. If |parentApp| already has a sub-app with |manifestId| installed, perform the following steps: + + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{InvalidStateError}}" {{DOMException}}. + + 1. [=iteration/continue|Continue=]. + + 1. If the [=manifest/scope=] of |parentApp|'s [=manifest=] is a [=code unit prefix|prefix=] of the |manifest|'s [=manifest/scope=], or the |manifest|'s [=manifest/scope=] is a [=code unit prefix|prefix=] of the [=manifest/scope=] of any currently installed sub-app in |parentApp|'s [=installed web application/sub-apps set=], or the [=manifest/scope=] of any currently installed sub-app in |parentApp|'s [=installed web application/sub-apps set=] is a [=code unit prefix|prefix=] of the |manifest|'s [=manifest/scope=], or the |absoluteUrl| points to |parentApp|'s [=manifest=] itself, perform the following steps: + + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{ConstraintError}}" {{DOMException}}. + + 1. [=iteration/continue|Continue=]. + + 1. Try to install the sub-app on the platform's application launcher. + + 1. If the installation fails due to a system or database error: + + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{OperationError}}" {{DOMException}}. + + 1. [=iteration/continue|Continue=]. + + 1. [=map/set|Set=] |installedApps|[|installPath|] to |manifestId|. + + 1. Let |response| be a new {{SubAppsAddResponse}} dictionary with: + - {{SubAppsAddResponse/installedApps}} set to |installedApps|. + - {{SubAppsAddResponse/failedApps}} set to |failedApps|. + + 1. [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=resolve=] |promise| with |response|. + +1. Return |promise|. + +## {{SubApps/remove()}} method ## {#subapps-remove} + +

The {{SubApps/remove(manifest_ids)/manifest_ids}} argument is a list of [=manifest/id=]s of the sub-apps to remove.

+ +The {{SubApps/remove(manifest_ids)}} method steps are: + +1. Let |promise| be [=a new promise=]. + +1. Let |document| be the [=relevant global object=]'s [=associated Document=]. + +1. If |document| is not [=allowed to use=] the [=policy-controlled feature=] named "[=policy-controlled feature/sub-apps=]", [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}} and return |promise|. + +1. If |document| is a [=Document/sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + +1. Let |parentApp| be |document|'s [=Document/associated installed web application=]. + +1. Let |parsedManifestIds| be an empty list. + +1. [=list/iterate|For each=] |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: + + 1. If |manifestId| is not a [=valid relative path=] for |document|, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + + 1. Let |parsedUrl| be the result of [=parse a url|parsing=] |manifestId| with |document|'s [=document base URL=] as the base URL. + + 1. If |parsedUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + + 1. Append |parsedUrl| to |parsedManifestIds|. + +1. Let |subApps| be [=this=]. + +1. Run the following steps [=in parallel=]: + + 1. Let |removedApps| be an empty sequence. + + 1. Let |failedApps| be an empty map. + + 1. [=list/iterate|For each=] |parsedUrl| in |parsedManifestIds|: + + 1. Let |manifestId| be |parsedUrl|'s [=url/path=]. + + 1. If there is no [=installed web application=] in |parentApp|'s [=installed web application/sub-apps set=] whose [=manifest/id=] is |manifestId|, perform the following steps: + + 1. [=map/set|Set=] |failedApps|[|manifestId|] to a new "{{NotFoundError}}" {{DOMException}}. + + 1. [=iteration/continue|Continue=]. + + 1. Attempt to uninstall the sub-app with ID |manifestId| from the system launcher and registry. + + 1. If uninstallation fails due to a system error, perform the following steps: + + 1. [=map/set|Set=] |failedApps|[|manifestId|] to a new "{{OperationError}}" {{DOMException}}. + + 1. [=iteration/continue|Continue=]. + + 1. Append |manifestId| to |removedApps|. + + 1. Let |response| be a new {{SubAppsRemoveResponse}} dictionary with: + - {{SubAppsRemoveResponse/removedApps}} set to |removedApps|. + - {{SubAppsRemoveResponse/failedApps}} set to |failedApps|. + + 1. [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=resolve=] |promise| with |response|. + +1. Return |promise|. + +## {{SubApps/list()}} method ## {#subapps-list} + +The {{SubApps/list()}} method steps are: + +1. Let |promise| be [=a new promise=]. + +1. Let |document| be the [=relevant global object=]'s [=associated Document=]. + +1. If |document| is not [=allowed to use=] the [=policy-controlled feature=] named "[=policy-controlled feature/sub-apps=]", [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}} and return |promise|. + +1. If |document| is a [=Document/sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + +1. Let |parentApp| be |document|'s [=Document/associated installed web application=]. + +1. Let |subApps| be [=this=]. + +1. Run the following steps [=in parallel=]: + + 1. Let |listResult| be an empty map. + + 1. Retrieve the list of all currently installed sub-apps for |parentApp| from the platform registry. + + 1. If retrieving the list fails due to a platform error, [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. + + 1. [=list/iterate|For each=] installed sub-app |subApp|: + + 1. Let |manifestId| be |subApp|'s [=manifest/id=]. + + 1. Let |appName| be the [=manifest/name=] of the sub-app as extracted from its web manifest. + + 1. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with {{SubAppsListResult/appName}} set to |appName|. + + 1. [=map/set|Set=] |listResult|[|manifestId|] to |resultEntry|. + + 1. [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=resolve=] |promise| with |listResult|. + +1. Return |promise|. + +## Fetch and process the manifest ## {#fetch-and-process-manifest} + +Issue(2): write the "fetch and process the manifest" algorithm. + +To fetch and process the manifest given a url (a [=URL=]), run the following steps: + +1. Return failure. + +Security and Privacy Considerations {#security-privacy} +====================================================== + +Installing and managing auxiliary applications is a [=powerful feature=]. A user agent MUST NOT allow a web application to install or manage sub-apps without [=express permission=]. + +This section outlines the threats considered and the normative requirements for user agents to mitigate them. + +## Shared Origin Identity ## {#shared-origin} +A sub-app does not possess a separate security origin. It shares the exact same [=origin=] and local data stores (such as Cookies, IndexedDB, LocalStorage, and Cache Storage) with its parent app. Standard web security boundaries (such as the Same-Origin Policy) treat the parent and all its sub-apps as a single entity. + +## Permission Inheritance ## {#permission-inheritance} +All permissions are shared across the parent app and its sub-apps. Granting a permission (e.g., camera, file system access, USB) to a sub-app automatically grants it to the parent, and vice versa. + +To access the Sub Apps API, the parent app's document must explicitly declare the permission policy `sub-apps`. Permission policies declared for a sub-app have no effect. + +## Explicit User Consent ## {#user-consent} +User consent MUST be obtained for a specific [=origin=]. When {{SubApps/add()}} is called, the user agent MUST present a unified installation dialog to the user displaying all requested sub-apps. If multiple sub-apps are added at once, they should be presented inside a single prompt to avoid dialog spam. + +The user agent MUST display a permission prompt that clearly indicates which origin is requesting access and provides the user with enough information to make an informed decision (for example, by displaying the names and icons of the sub-apps being installed). + +## Identity Spoofing Risks ## {#identity-spoofing} +Since developers can customize the names and icons of sub-apps, there is a risk that a malicious application could create a sub-app that mimics system dialogs or trusted third-party applications. +To mitigate this risk, the Sub Apps API is restricted to [=isolated contexts=], which guarantee integrity and signature verification. + +## OS Integration Extension Risk ## {#os-integration-risk} +Sub-apps have the ability to register their own OS integrations (such as protocol handlers or file type associations). This means an application could potentially extend its reach into the OS far beyond what was declared in the parent app's primary manifest. +This is mitigated by the fact that most operating system integrations require explicit user approval (for example, choosing the sub-app as the default application for a file type) before they become active. + +## Quota and Limits ## {#quota-limits} +To protect the host operating system and the user's application launcher from potential exhaustion or abuse, the platform enforces a hard limit of **20 installed sub-apps** per parent application. +If a batch installation call exceeds the platform limit, the entire {{SubApps/add()}} call rejects with a "{{QuotaExceededError}}" {{DOMException}}. + +Integrations {#integrations} +============================ + +## Permissions Policy ## {#permissions-policy} + +This specification defines a feature that controls whether the methods exposed by the {{Window/subApps}} attribute on the {{Window}} object may be used. + +The feature name for this feature is "sub-apps". -See [https://garykac.github.io/procspec/](https://garykac.github.io/procspec/), -[https://dlaliberte.github.io/bikeshed-intro/index.html](https://dlaliberte.github.io/bikeshed-intro/index.html), -and [https://speced.github.io/bikeshed/](https://speced.github.io/bikeshed/) to get started on your -specification. +The [=policy-controlled feature/default allowlist=] for this feature is `'none'`. User agents MAY override this +to `'self'` for particular origins (for example based on user decision).