From 19684d79afc88dc5549c7641f9658d7bc3234d1d Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Thu, 18 Jun 2026 16:39:42 +0200 Subject: [PATCH 01/24] add spec --- index.bs | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 219 insertions(+), 7 deletions(-) diff --git a/index.bs b/index.bs index d6954f7..c8b8d78 100644 --- a/index.bs +++ b/index.bs @@ -2,11 +2,12 @@ Title: Sub Apps Shortname: sub-apps Level: None -Status: w3c/UD +Status: UD +Group: wicg Repository: explainers-by-googlers/sub-apps URL: https://explainers-by-googlers.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,225 @@ 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). +2. Share the underlying resources, origin identification, storage, permissions, and update lifecycle of the parent application. + +This API is restricted to [=isolated contexts=] to ensure security and data integrity. + +Extensions to the {{Window}} interface {#window-extensions} +======================================================= + + +[Exposed=Window, SecureContext, IsolatedContext] +partial interface Window { + [SameObject] readonly attribute SubApps subApps; +}; + + +## {{Window/subApps}} attribute ## {#window-subapps} + +When getting, the {{Window/subApps}} attribute always returns the same +instance of the {{SubApps}} object. + +{{SubApps}} interface {#subapps-interface} +========================================== + + +dictionary SubAppsAddResponse { + record<USVString, USVString> installedApps; + record<USVString, DOMException> failedApps; +}; + +dictionary SubAppsRemoveResponse { + sequence<USVString> 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<USVString> manifest_ids); + Promise<record<USVString, SubAppsListResult>> list(); +}; + + +Each application has a primary application context (the parent app). A {{Document}} is a sub-app document if it is not loaded within the primary application context of the parent app. + +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. +2. |path| starts with `"/"`. +3. |path| is not empty. +4. |path| does not start with `"//"`. +5. The result of [=parse a url|parsing=] |path| with |document|'s [=origin=] as the base URL is not failure. + +## {{SubApps/add()}} method ## {#subapps-add} + +The {{SubApps/add(install_paths)}} method steps are: + +1. Let |promise| be [=a new promise=]. +2. If the [=relevant global object=]'s [=associated 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|. +3. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +4. Let |parsedUrls| be an empty list. +5. For each |installPath| in {{SubApps/add(install_paths)/install_paths}}: + 1. If |installPath| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + 2. Let |absoluteUrl| be the result of parsing |installPath| with the parent app's [=origin=] as the base URL. + 3. If |absoluteUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + 4. Append |absoluteUrl| to |parsedUrls|. +6. Let |parentApp| be the parent app. +7. Let |currentSubAppsCount| be the number of sub-apps currently installed for |parentApp|. +8. If |currentSubAppsCount| + {{SubApps/add(install_paths)/install_paths}}'s [=list/size=] is greater than 20, [=reject=] |promise| with a "{{QuotaExceededError}}" {{DOMException}} and return |promise|. +9. 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). +10. If |userConsent| is denied, [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}} and return |promise|. +11. Run the following steps [=in parallel=]: + 1. Let |installedApps| be an empty map. + 2. Let |failedApps| be an empty map. + 3. For each |absoluteUrl| in |parsedUrls|: + 1. Let |installPath| be the relative path portion of |absoluteUrl|. + 2. Fetch the resource at |absoluteUrl| and parse its web manifest [[!APPMANIFEST]]. + 3. If fetching or parsing the web manifest fails, perform the following steps: + 1. Add a mapping from |installPath| to a new "{{DataError}}" {{DOMException}} to |failedApps|. + 2. Continue. + 4. Let |manifest| be the parsed web manifest. + 5. Let |manifestId| be |manifest|'s [=manifest/id=]. If it is not defined, fall back to |manifest|'s [=manifest/start_url=] (without reference/hash fragment). + 6. If |parentApp| already has a sub-app with |manifestId| installed, perform the following steps: + 1. Add a mapping from |installPath| to a new "{{InvalidStateError}}" {{DOMException}} to |failedApps|. + 2. Continue. + 7. If the scope of |manifest| overlaps with the scope of |parentApp|, or the scope of |manifest| overlaps with any currently installed sub-app's scope under |parentApp|, or the |absoluteUrl| points to |parentApp|'s manifest itself, perform the following steps: + 1. Add a mapping from |installPath| to a new "{{ConstraintError}}" {{DOMException}} to |failedApps|. + 2. Continue. + 8. Try to install the sub-app on the platform's application launcher. + 9. If the installation fails due to a system or database error: + 1. Add a mapping from |installPath| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. + 2. Continue. + 10. Add a mapping from |installPath| to |manifestId| to |installedApps|. + 12. Let |response| be a new {{SubAppsAddResponse}} dictionary with: + - `installedApps` set to |installedApps|. + - `failedApps` set to |failedApps|. + 13. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. +12. Return |promise|. + +## {{SubApps/remove()}} method ## {#subapps-remove} + +The {{SubApps/remove(manifest_ids)}} method steps are: + +1. Let |promise| be [=a new promise=]. +2. If the [=relevant global object=]'s [=associated 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|. +3. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +4. Let |parsedManifestIds| be an empty list. +5. For each |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: + 1. If |manifestId| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + 2. Let |parsedUrl| be the result of parsing |manifestId| with the parent app's [=origin=] as the base URL. + 3. If |parsedUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + 4. Append |parsedUrl| to |parsedManifestIds|. +6. Run the following steps [=in parallel=]: + 1. Let |removedApps| be an empty sequence. + 2. Let |failedApps| be an empty map. + 3. For each |parsedUrl| in |parsedManifestIds|: + 1. Let |manifestId| be the relative path portion of |parsedUrl|. + 2. If |manifestId| does not exist under the parent app, perform the following steps: + - Add a mapping from |manifestId| to a new "{{NotFoundError}}" {{DOMException}} to |failedApps|. + - Continue. + 3. Attempt to uninstall the sub-app with ID |manifestId| from the system launcher and registry. + 4. If uninstallation fails due to a system error, perform the following steps: + - Add a mapping from |manifestId| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. + - Continue. + 5. Append |manifestId| to |removedApps|. + 4. Let |response| be a new {{SubAppsRemoveResponse}} dictionary with: + - `removedApps` set to |removedApps|. + - `failedApps` set to |failedApps|. + 5. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. +7. Return |promise|. + +## {{SubApps/list()}} method ## {#subapps-list} + +The {{SubApps/list()}} method steps are: + +1. Let |promise| be [=a new promise=]. +2. If the [=relevant global object=]'s [=associated 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|. +3. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +4. Run the following steps [=in parallel=]: + 1. Let |listResult| be an empty map. + 2. Retrieve the list of all currently installed sub-apps for the parent app from the platform registry. + 3. If retrieving the list fails due to a platform error, [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. + 4. For each installed sub-app |subApp|: + 1. Let |manifestId| be |subApp|'s manifest ID. + 2. Let |appName| be the name of the sub-app as extracted from its web manifest. + 3. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with `appName` set to |appName|. + 4. Add a mapping from |manifestId| to |resultEntry| to |listResult|. + 5. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |listResult|. +5. Return |promise|. + +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 `'self'`. From c672d8db2e5c5ad526d83ddbf59aa4c2ea8b8dc0 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Mon, 6 Jul 2026 17:54:46 +0200 Subject: [PATCH 02/24] make list number be always 1 --- index.bs | 217 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 149 insertions(+), 68 deletions(-) diff --git a/index.bs b/index.bs index c8b8d78..bae9ed9 100644 --- a/index.bs +++ b/index.bs @@ -42,7 +42,8 @@ Introduction {#intro} 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). -2. Share the underlying resources, origin identification, storage, permissions, and update lifecycle of the parent application. + +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. @@ -93,108 +94,188 @@ dictionary SubAppsListResult { Each application has a primary application context (the parent app). A {{Document}} is a sub-app document if it is not loaded within the primary application context of the parent app. 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. -2. |path| starts with `"/"`. -3. |path| is not empty. -4. |path| does not start with `"//"`. -5. The result of [=parse a url|parsing=] |path| with |document|'s [=origin=] as the base URL is not failure. + +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 [=origin=] as the base URL is not failure. ## {{SubApps/add()}} method ## {#subapps-add} The {{SubApps/add(install_paths)}} method steps are: 1. Let |promise| be [=a new promise=]. -2. If the [=relevant global object=]'s [=associated 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|. -3. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. -4. Let |parsedUrls| be an empty list. -5. For each |installPath| in {{SubApps/add(install_paths)/install_paths}}: + +1. If the [=relevant global object=]'s [=associated 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 the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + +1. Let |parsedUrls| be an empty list. + +1. For each |installPath| in {{SubApps/add(install_paths)/install_paths}}: + 1. If |installPath| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. - 2. Let |absoluteUrl| be the result of parsing |installPath| with the parent app's [=origin=] as the base URL. - 3. If |absoluteUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. - 4. Append |absoluteUrl| to |parsedUrls|. -6. Let |parentApp| be the parent app. -7. Let |currentSubAppsCount| be the number of sub-apps currently installed for |parentApp|. -8. If |currentSubAppsCount| + {{SubApps/add(install_paths)/install_paths}}'s [=list/size=] is greater than 20, [=reject=] |promise| with a "{{QuotaExceededError}}" {{DOMException}} and return |promise|. -9. 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). -10. If |userConsent| is denied, [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}} and return |promise|. -11. Run the following steps [=in parallel=]: + + 1. Let |absoluteUrl| be the result of parsing |installPath| with the parent app's [=origin=] 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 the parent app. + +1. Let |currentSubAppsCount| be the number of sub-apps currently installed for |parentApp|. + +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 |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, [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}} and return |promise|. + +1. Run the following steps [=in parallel=]: + 1. Let |installedApps| be an empty map. - 2. Let |failedApps| be an empty map. - 3. For each |absoluteUrl| in |parsedUrls|: + + 1. Let |failedApps| be an empty map. + + 1. For each |absoluteUrl| in |parsedUrls|: + 1. Let |installPath| be the relative path portion of |absoluteUrl|. - 2. Fetch the resource at |absoluteUrl| and parse its web manifest [[!APPMANIFEST]]. - 3. If fetching or parsing the web manifest fails, perform the following steps: + + 1. Fetch the resource at |absoluteUrl| and parse its web manifest [[!APPMANIFEST]]. + + 1. If fetching or parsing the web manifest fails, perform the following steps: + 1. Add a mapping from |installPath| to a new "{{DataError}}" {{DOMException}} to |failedApps|. - 2. Continue. - 4. Let |manifest| be the parsed web manifest. - 5. Let |manifestId| be |manifest|'s [=manifest/id=]. If it is not defined, fall back to |manifest|'s [=manifest/start_url=] (without reference/hash fragment). - 6. If |parentApp| already has a sub-app with |manifestId| installed, perform the following steps: + + 1. Continue. + + 1. Let |manifest| be the parsed web manifest. + + 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. Add a mapping from |installPath| to a new "{{InvalidStateError}}" {{DOMException}} to |failedApps|. - 2. Continue. - 7. If the scope of |manifest| overlaps with the scope of |parentApp|, or the scope of |manifest| overlaps with any currently installed sub-app's scope under |parentApp|, or the |absoluteUrl| points to |parentApp|'s manifest itself, perform the following steps: + + 1. Continue. + + 1. If the scope of |manifest| overlaps with the scope of |parentApp|, or the scope of |manifest| overlaps with any currently installed sub-app's scope under |parentApp|, or the |absoluteUrl| points to |parentApp|'s manifest itself, perform the following steps: + 1. Add a mapping from |installPath| to a new "{{ConstraintError}}" {{DOMException}} to |failedApps|. - 2. Continue. - 8. Try to install the sub-app on the platform's application launcher. - 9. If the installation fails due to a system or database error: + + 1. 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. Add a mapping from |installPath| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. - 2. Continue. - 10. Add a mapping from |installPath| to |manifestId| to |installedApps|. - 12. Let |response| be a new {{SubAppsAddResponse}} dictionary with: + + 1. Continue. + + 1. Add a mapping from |installPath| to |manifestId| to |installedApps|. + + 1. Let |response| be a new {{SubAppsAddResponse}} dictionary with: - `installedApps` set to |installedApps|. - `failedApps` set to |failedApps|. - 13. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. -12. Return |promise|. + + 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. + +1. Return |promise|. ## {{SubApps/remove()}} method ## {#subapps-remove} The {{SubApps/remove(manifest_ids)}} method steps are: 1. Let |promise| be [=a new promise=]. -2. If the [=relevant global object=]'s [=associated 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|. -3. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. -4. Let |parsedManifestIds| be an empty list. -5. For each |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: + +1. If the [=relevant global object=]'s [=associated 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 the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + +1. Let |parsedManifestIds| be an empty list. + +1. For each |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: + 1. If |manifestId| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. - 2. Let |parsedUrl| be the result of parsing |manifestId| with the parent app's [=origin=] as the base URL. - 3. If |parsedUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. - 4. Append |parsedUrl| to |parsedManifestIds|. -6. Run the following steps [=in parallel=]: + + 1. Let |parsedUrl| be the result of parsing |manifestId| with the parent app's [=origin=] as the base URL. + + 1. If |parsedUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + + 1. Append |parsedUrl| to |parsedManifestIds|. + +1. Run the following steps [=in parallel=]: + 1. Let |removedApps| be an empty sequence. - 2. Let |failedApps| be an empty map. - 3. For each |parsedUrl| in |parsedManifestIds|: + + 1. Let |failedApps| be an empty map. + + 1. For each |parsedUrl| in |parsedManifestIds|: + 1. Let |manifestId| be the relative path portion of |parsedUrl|. - 2. If |manifestId| does not exist under the parent app, perform the following steps: - - Add a mapping from |manifestId| to a new "{{NotFoundError}}" {{DOMException}} to |failedApps|. - - Continue. - 3. Attempt to uninstall the sub-app with ID |manifestId| from the system launcher and registry. - 4. If uninstallation fails due to a system error, perform the following steps: - - Add a mapping from |manifestId| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. - - Continue. - 5. Append |manifestId| to |removedApps|. - 4. Let |response| be a new {{SubAppsRemoveResponse}} dictionary with: + + 1. If |manifestId| does not exist under the parent app, perform the following steps: + + 1. Add a mapping from |manifestId| to a new "{{NotFoundError}}" {{DOMException}} to |failedApps|. + + 1. 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. Add a mapping from |manifestId| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. + + 1. Continue. + + 1. Append |manifestId| to |removedApps|. + + 1. Let |response| be a new {{SubAppsRemoveResponse}} dictionary with: - `removedApps` set to |removedApps|. - `failedApps` set to |failedApps|. - 5. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. -7. Return |promise|. + + 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] 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=]. -2. If the [=relevant global object=]'s [=associated 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|. -3. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. -4. Run the following steps [=in parallel=]: + +1. If the [=relevant global object=]'s [=associated 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 the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + +1. Run the following steps [=in parallel=]: + 1. Let |listResult| be an empty map. - 2. Retrieve the list of all currently installed sub-apps for the parent app from the platform registry. - 3. If retrieving the list fails due to a platform error, [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. - 4. For each installed sub-app |subApp|: + + 1. Retrieve the list of all currently installed sub-apps for the parent app 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 [=this=] to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. + + 1. For each installed sub-app |subApp|: + 1. Let |manifestId| be |subApp|'s manifest ID. - 2. Let |appName| be the name of the sub-app as extracted from its web manifest. - 3. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with `appName` set to |appName|. - 4. Add a mapping from |manifestId| to |resultEntry| to |listResult|. - 5. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |listResult|. -5. Return |promise|. + + 1. Let |appName| be the name of the sub-app as extracted from its web manifest. + + 1. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with `appName` set to |appName|. + + 1. Add a mapping from |manifestId| to |resultEntry| to |listResult|. + + 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |listResult|. + +1. Return |promise|. Security and Privacy Considerations {#security-privacy} ====================================================== From b9300b100f687118f17bf8dd4b6bdd8b1f161358 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 14:39:36 +0200 Subject: [PATCH 03/24] formalize for each iterations --- index.bs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/index.bs b/index.bs index bae9ed9..d270239 100644 --- a/index.bs +++ b/index.bs @@ -117,7 +117,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Let |parsedUrls| be an empty list. -1. For each |installPath| in {{SubApps/add(install_paths)/install_paths}}: +1. [=list/iterate|For each=] |installPath| in {{SubApps/add(install_paths)/install_paths}}: 1. If |installPath| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. @@ -143,7 +143,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Let |failedApps| be an empty map. - 1. For each |absoluteUrl| in |parsedUrls|: + 1. [=list/iterate|For each=] |absoluteUrl| in |parsedUrls|: 1. Let |installPath| be the relative path portion of |absoluteUrl|. @@ -153,7 +153,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Add a mapping from |installPath| to a new "{{DataError}}" {{DOMException}} to |failedApps|. - 1. Continue. + 1. [=iteration/continue|Continue=]. 1. Let |manifest| be the parsed web manifest. @@ -163,13 +163,13 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Add a mapping from |installPath| to a new "{{InvalidStateError}}" {{DOMException}} to |failedApps|. - 1. Continue. + 1. [=iteration/continue|Continue=]. - 1. If the scope of |manifest| overlaps with the scope of |parentApp|, or the scope of |manifest| overlaps with any currently installed sub-app's scope under |parentApp|, or the |absoluteUrl| points to |parentApp|'s manifest itself, perform the following steps: + 1. If the [=manifest/scope=] of |parentApp| is a prefix of the [=manifest/scope=], or the |manifest|'s [=manifest/scope=] is a prefix of the [=manifest/scope=] of any currently installed sub-app under |parentApp|, or the [=manifest/scope=] of any currently installed sub-app under |parentApp| is a prefix of the |manifest|'s [=manifest/scope=], or the |absoluteUrl| points to |parentApp|'s manifest itself, perform the following steps: 1. Add a mapping from |installPath| to a new "{{ConstraintError}}" {{DOMException}} to |failedApps|. - 1. Continue. + 1. [=iteration/continue|Continue=]. 1. Try to install the sub-app on the platform's application launcher. @@ -177,7 +177,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Add a mapping from |installPath| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. - 1. Continue. + 1. [=iteration/continue|Continue=]. 1. Add a mapping from |installPath| to |manifestId| to |installedApps|. @@ -201,7 +201,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Let |parsedManifestIds| be an empty list. -1. For each |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: +1. [=list/iterate|For each=] |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: 1. If |manifestId| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. @@ -217,7 +217,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Let |failedApps| be an empty map. - 1. For each |parsedUrl| in |parsedManifestIds|: + 1. [=list/iterate|For each=] |parsedUrl| in |parsedManifestIds|: 1. Let |manifestId| be the relative path portion of |parsedUrl|. @@ -225,7 +225,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Add a mapping from |manifestId| to a new "{{NotFoundError}}" {{DOMException}} to |failedApps|. - 1. Continue. + 1. [=iteration/continue|Continue=]. 1. Attempt to uninstall the sub-app with ID |manifestId| from the system launcher and registry. @@ -233,7 +233,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Add a mapping from |manifestId| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. - 1. Continue. + 1. [=iteration/continue|Continue=]. 1. Append |manifestId| to |removedApps|. @@ -263,7 +263,7 @@ The {{SubApps/list()}} method steps are: 1. If retrieving the list fails due to a platform error, [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. - 1. For each installed sub-app |subApp|: + 1. [=list/iterate|For each=] installed sub-app |subApp|: 1. Let |manifestId| be |subApp|'s manifest ID. From 88513c7a6fa19aa689900ee933fb3ca94eaa0fd6 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 14:45:55 +0200 Subject: [PATCH 04/24] async step for permission prompt --- index.bs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.bs b/index.bs index d270239..d3eb339 100644 --- a/index.bs +++ b/index.bs @@ -133,11 +133,11 @@ The {{SubApps/add(install_paths)}} method steps are: 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 |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. Run the following steps [=in parallel=]: -1. If |userConsent| is denied, [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}} and return |promise|. + 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. Run the following steps [=in parallel=]: + 1. If |userConsent| is denied, [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}}, and abort these steps. 1. Let |installedApps| be an empty map. From 3be75404a9f75fcfcc07d984c4da04017969d81e Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 14:56:52 +0200 Subject: [PATCH 05/24] formalize subapps in window creation --- index.bs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/index.bs b/index.bs index d3eb339..c340e97 100644 --- a/index.bs +++ b/index.bs @@ -57,10 +57,15 @@ partial interface Window { }; -## {{Window/subApps}} attribute ## {#window-subapps} +## {{Window/subApps}} attribute ## {#window-subapps-attribute} -When getting, the {{Window/subApps}} attribute always returns the same -instance of the {{SubApps}} object. +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} ========================================== From 7065a11045823ee3b90b1caa6979c7804ab5e2dd Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:03:59 +0200 Subject: [PATCH 06/24] Remove link defaults --- index.bs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/index.bs b/index.bs index c340e97..385f166 100644 --- a/index.bs +++ b/index.bs @@ -19,22 +19,7 @@ Include Can I Use Panels: yes Boilerplate: conformance-tests no - + Introduction {#intro} ===================== From ba45b824f69f4ebb0a187758394df14650ead49f Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:09:41 +0200 Subject: [PATCH 07/24] formalize prefix --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 385f166..4c789a1 100644 --- a/index.bs +++ b/index.bs @@ -155,7 +155,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. [=iteration/continue|Continue=]. - 1. If the [=manifest/scope=] of |parentApp| is a prefix of the [=manifest/scope=], or the |manifest|'s [=manifest/scope=] is a prefix of the [=manifest/scope=] of any currently installed sub-app under |parentApp|, or the [=manifest/scope=] of any currently installed sub-app under |parentApp| is a prefix of the |manifest|'s [=manifest/scope=], or the |absoluteUrl| points to |parentApp|'s manifest itself, perform the following steps: + 1. If the [=manifest/scope=] of |parentApp| is a [=code unit prefix|prefix=] of the [=manifest/scope=], or the |manifest|'s [=manifest/scope=] is a [=code unit prefix|prefix=] of the [=manifest/scope=] of any currently installed sub-app under |parentApp|, or the [=manifest/scope=] of any currently installed sub-app under |parentApp| 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. Add a mapping from |installPath| to a new "{{ConstraintError}}" {{DOMException}} to |failedApps|. From b3c3ca2438490b39051b3f6f8ff3b48e82474d65 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:12:23 +0200 Subject: [PATCH 08/24] correctly link appmanifest --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 4c789a1..7155886 100644 --- a/index.bs +++ b/index.bs @@ -255,9 +255,9 @@ The {{SubApps/list()}} method steps are: 1. [=list/iterate|For each=] installed sub-app |subApp|: - 1. Let |manifestId| be |subApp|'s manifest ID. + 1. Let |manifestId| be |subApp|'s [=manifest/id=]. - 1. Let |appName| be the name of the sub-app as extracted from its web manifest. + 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 `appName` set to |appName|. From 523faabea90f379f2b9a7aa2cca5cdb497f22219 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:14:32 +0200 Subject: [PATCH 09/24] relative path fix --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 7155886..b518e4f 100644 --- a/index.bs +++ b/index.bs @@ -135,7 +135,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. [=list/iterate|For each=] |absoluteUrl| in |parsedUrls|: - 1. Let |installPath| be the relative path portion of |absoluteUrl|. + 1. Let |installPath| be |absoluteUrl|'s [=url/path=]. 1. Fetch the resource at |absoluteUrl| and parse its web manifest [[!APPMANIFEST]]. @@ -209,7 +209,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. [=list/iterate|For each=] |parsedUrl| in |parsedManifestIds|: - 1. Let |manifestId| be the relative path portion of |parsedUrl|. + 1. Let |manifestId| be |parsedUrl|'s [=url/path=]. 1. If |manifestId| does not exist under the parent app, perform the following steps: From 73bc8595a761c39aa981a373bfac70e06b2e59ea Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:16:36 +0200 Subject: [PATCH 10/24] Formalize mapping --- index.bs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/index.bs b/index.bs index b518e4f..67aaa9b 100644 --- a/index.bs +++ b/index.bs @@ -141,7 +141,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. If fetching or parsing the web manifest fails, perform the following steps: - 1. Add a mapping from |installPath| to a new "{{DataError}}" {{DOMException}} to |failedApps|. + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. @@ -151,13 +151,13 @@ The {{SubApps/add(install_paths)}} method steps are: 1. If |parentApp| already has a sub-app with |manifestId| installed, perform the following steps: - 1. Add a mapping from |installPath| to a new "{{InvalidStateError}}" {{DOMException}} to |failedApps|. + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{InvalidStateError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. 1. If the [=manifest/scope=] of |parentApp| is a [=code unit prefix|prefix=] of the [=manifest/scope=], or the |manifest|'s [=manifest/scope=] is a [=code unit prefix|prefix=] of the [=manifest/scope=] of any currently installed sub-app under |parentApp|, or the [=manifest/scope=] of any currently installed sub-app under |parentApp| 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. Add a mapping from |installPath| to a new "{{ConstraintError}}" {{DOMException}} to |failedApps|. + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{ConstraintError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. @@ -165,11 +165,11 @@ The {{SubApps/add(install_paths)}} method steps are: 1. If the installation fails due to a system or database error: - 1. Add a mapping from |installPath| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{OperationError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. - 1. Add a mapping from |installPath| to |manifestId| to |installedApps|. + 1. [=map/set|Set=] |installedApps|[|installPath|] to |manifestId|. 1. Let |response| be a new {{SubAppsAddResponse}} dictionary with: - `installedApps` set to |installedApps|. @@ -213,7 +213,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. If |manifestId| does not exist under the parent app, perform the following steps: - 1. Add a mapping from |manifestId| to a new "{{NotFoundError}}" {{DOMException}} to |failedApps|. + 1. [=map/set|Set=] |failedApps|[|manifestId|] to a new "{{NotFoundError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. @@ -221,7 +221,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. If uninstallation fails due to a system error, perform the following steps: - 1. Add a mapping from |manifestId| to a new "{{OperationError}}" {{DOMException}} to |failedApps|. + 1. [=map/set|Set=] |failedApps|[|manifestId|] to a new "{{OperationError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. @@ -261,7 +261,7 @@ The {{SubApps/list()}} method steps are: 1. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with `appName` set to |appName|. - 1. Add a mapping from |manifestId| to |resultEntry| to |listResult|. + 1. [=map/set|Set=] |listResult|[|manifestId|] to |resultEntry|. 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |listResult|. From 6d791d583c1fa9f3433ec60ffbe91b9441087556 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:50:48 +0200 Subject: [PATCH 11/24] specify how we fetch manifest --- index.bs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 67aaa9b..1d5fcfb 100644 --- a/index.bs +++ b/index.bs @@ -19,7 +19,9 @@ Include Can I Use Panels: yes Boilerplate: conformance-tests no - + Introduction {#intro} ===================== @@ -99,6 +101,8 @@ A string |path| is a valid relative path for a {{Document}} |document 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. If the [=relevant global object=]'s [=associated 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|. @@ -137,15 +141,33 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Let |installPath| be |absoluteUrl|'s [=url/path=]. - 1. Fetch the resource at |absoluteUrl| and parse its web manifest [[!APPMANIFEST]]. + 1. Let |htmlRequest| be a new [=request=] whose [=request/url=] is |absoluteUrl|, [=request/client=] is the [=relevant settings object=] of [=this=], and [=request/destination=] is "`document`". - 1. If fetching or parsing the web manifest fails, perform the following steps: + 1. Let |htmlResponse| be the result of [=fetch|fetching=] |htmlRequest|. + 1. If |htmlResponse| is a [=network error=], perform the following steps: 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. + 1. [=iteration/continue|Continue=]. + 1. Let |document| be the HTML document obtained by parsing |htmlResponse|'s [=response/body=]. + + 1. Let |manifestUrl| be the URL obtained by resolving the `href` attribute of the first <{link}> element whose `rel` attribute contains "`manifest`" inside |document|. + + 1. If the <{link}> element is not found, or |manifestUrl| is failure, perform the following steps: + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. 1. [=iteration/continue|Continue=]. - 1. Let |manifest| be the parsed web manifest. + 1. Let |manifestRequest| be a new [=request=] whose [=request/url=] is |manifestUrl|, [=request/client=] is the [=relevant settings object=] of [=this=], and [=request/destination=] is "`manifest`". + + 1. Let |manifestResponse| be the result of [=fetch|fetching=] |manifestRequest|. + + 1. If |manifestResponse| is a [=network error=], perform the following steps: + 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. + 1. [=iteration/continue|Continue=]. + + 1. Let |bodyBytes| be the [=byte sequence=] of |manifestResponse|'s [=response/body=]. + + 1. Let |manifest| be the result of [=processing a manifest=] given [=this=]'s [=relevant global object=]'s [=associated Document=]'s [=Document/URL=], |manifestUrl|, |bodyBytes|, and the [=relevant settings object=] of [=this=]. 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). From 0631a58cc55284fadc7a0d52ac50f6bffa4a7683 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 15:56:23 +0200 Subject: [PATCH 12/24] fix overlap of scopes in readme. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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` | From a95e57a2019bd7be919007a121073cdb3f8aae8f Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 7 Jul 2026 16:04:50 +0200 Subject: [PATCH 13/24] formalize sub app, parent app, installed app. --- index.bs | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/index.bs b/index.bs index 1d5fcfb..d8b5d03 100644 --- a/index.bs +++ b/index.bs @@ -21,6 +21,7 @@ Boilerplate: conformance-tests no Introduction {#intro} @@ -34,6 +35,19 @@ The Sub Apps API allows a parent application to programmatically install, list, 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}} can have 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. + +The associated parent app of a {{Document}} |document| is its [=associated installed web application=]'s [=installed web application/parent app=] if that is not null, or its [=associated installed web application=] otherwise. + +A {{Document}} is a sub-app document if it has an [=associated installed web application=] and its [=associated installed web application=]'s [=installed web application/parent app=] is not null. + Extensions to the {{Window}} interface {#window-extensions} ======================================================= @@ -83,8 +97,6 @@ dictionary SubAppsListResult { }; -Each application has a primary application context (the parent app). A {{Document}} is a sub-app document if it is not loaded within the primary application context of the parent app. - 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. @@ -115,15 +127,17 @@ The {{SubApps/add(install_paths)}} method steps are: 1. If |installPath| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. - 1. Let |absoluteUrl| be the result of parsing |installPath| with the parent app's [=origin=] as the base URL. + 1. Let |absoluteUrl| be the result of parsing |installPath| with the [=relevant global object=]'s [=associated Document=]'s [=origin=] 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 the parent app. +1. Let |parentApp| be the [=relevant global object=]'s [=associated Document=]'s [=associated parent app=]. + +1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. -1. Let |currentSubAppsCount| be the number of sub-apps currently installed for |parentApp|. +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|. @@ -177,7 +191,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. [=iteration/continue|Continue=]. - 1. If the [=manifest/scope=] of |parentApp| is a [=code unit prefix|prefix=] of the [=manifest/scope=], or the |manifest|'s [=manifest/scope=] is a [=code unit prefix|prefix=] of the [=manifest/scope=] of any currently installed sub-app under |parentApp|, or the [=manifest/scope=] of any currently installed sub-app under |parentApp| 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. 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}}. @@ -211,13 +225,17 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +1. Let |parentApp| be the [=relevant global object=]'s [=associated Document=]'s [=associated parent app=]. + +1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + 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 the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. - 1. Let |parsedUrl| be the result of parsing |manifestId| with the parent app's [=origin=] as the base URL. + 1. Let |parsedUrl| be the result of parsing |manifestId| with the [=relevant global object=]'s [=associated Document=]'s [=origin=] as the base URL. 1. If |parsedUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. @@ -233,7 +251,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Let |manifestId| be |parsedUrl|'s [=url/path=]. - 1. If |manifestId| does not exist under the parent app, perform the following steps: + 1. If |manifestId| does not exist under |parentApp|, perform the following steps: 1. [=map/set|Set=] |failedApps|[|manifestId|] to a new "{{NotFoundError}}" {{DOMException}}. @@ -267,11 +285,15 @@ The {{SubApps/list()}} method steps are: 1. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +1. Let |parentApp| be the [=relevant global object=]'s [=associated Document=]'s [=associated parent app=]. + +1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. + 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 the parent app from the platform registry. + 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 [=this=] to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. From e61b247ffb622621b8e157a771e641f3e1e463ee Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:18:56 +0200 Subject: [PATCH 14/24] Fix permission policy default value --- index.bs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.bs b/index.bs index d8b5d03..5476f4f 100644 --- a/index.bs +++ b/index.bs @@ -352,4 +352,5 @@ This specification defines a feature that controls whether the methods exposed b The feature name for this feature is "sub-apps". -The [=policy-controlled feature/default allowlist=] for this feature is `'self'`. +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). From 12881c70e0b1bc8f874b8f6666f3f505c556329e Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:27:20 +0200 Subject: [PATCH 15/24] specify about id-member --- index.bs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/index.bs b/index.bs index 5476f4f..2ca91f7 100644 --- a/index.bs +++ b/index.bs @@ -72,13 +72,16 @@ Each {{Window}} object has an associated subApps, which is ========================================== +// Represents the https://w3c.github.io/manifest/#id-member +typedef USVString ManifestId; + dictionary SubAppsAddResponse { - record<USVString, USVString> installedApps; + record<USVString, ManifestId> installedApps; record<USVString, DOMException> failedApps; }; dictionary SubAppsRemoveResponse { - sequence<USVString> removedApps; + sequence<ManifestId> removedApps; record<USVString, DOMException> failedApps; }; @@ -92,7 +95,7 @@ dictionary SubAppsListResult { IsolatedContext ] interface SubApps { Promise<SubAppsAddResponse> add(sequence<USVString> install_paths); - Promise<SubAppsRemoveResponse> remove(sequence<USVString> manifest_ids); + Promise<SubAppsRemoveResponse> remove(sequence<ManifestId> manifest_ids); Promise<record<USVString, SubAppsListResult>> list(); }; @@ -217,6 +220,8 @@ The {{SubApps/add(install_paths)}} method steps are: ## {{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=]. From e965dc8c564ed9e74db9eb131a66805d4cffecbd Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:36:11 +0200 Subject: [PATCH 16/24] Update index.bs Co-authored-by: Dominic Farolino --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 5476f4f..0786d67 100644 --- a/index.bs +++ b/index.bs @@ -107,7 +107,7 @@ A string |path| is a valid relative path for a {{Document}} |document 1. |path| does not start with `"//"`. -1. The result of [=parse a url|parsing=] |path| with |document|'s [=origin=] as the base URL is not failure. +1. The result of [=parse a url|parsing=] |path| with |document|'s [=Document/origin=] as the base URL is not failure. ## {{SubApps/add()}} method ## {#subapps-add} From c2cafb8fa8e8cc948f3ed137f92492089083219b Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:36:34 +0200 Subject: [PATCH 17/24] Update index.bs Co-authored-by: Dominic Farolino --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 0786d67..4a3c250 100644 --- a/index.bs +++ b/index.bs @@ -145,7 +145,7 @@ The {{SubApps/add(install_paths)}} method steps are: 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 [=this=] to [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}}, and abort these steps. + 1. If |userConsent| is denied, [=queue a global task=] on the [=relevant global object=] of [=this=] to [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}}, and abort these steps. 1. Let |installedApps| be an empty map. From 7f946180a675a117892d63b65bf4d8b38f9f0f5a Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:38:57 +0200 Subject: [PATCH 18/24] Correctly reference dict members --- index.bs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.bs b/index.bs index 2ca91f7..6420159 100644 --- a/index.bs +++ b/index.bs @@ -211,8 +211,8 @@ The {{SubApps/add(install_paths)}} method steps are: 1. [=map/set|Set=] |installedApps|[|installPath|] to |manifestId|. 1. Let |response| be a new {{SubAppsAddResponse}} dictionary with: - - `installedApps` set to |installedApps|. - - `failedApps` set to |failedApps|. + - [=SubAppsAddResponse/installedApps=] set to |installedApps|. + - [=SubAppsAddResponse/failedApps=] set to |failedApps|. 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. @@ -273,8 +273,8 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Append |manifestId| to |removedApps|. 1. Let |response| be a new {{SubAppsRemoveResponse}} dictionary with: - - `removedApps` set to |removedApps|. - - `failedApps` set to |failedApps|. + - [=SubAppsRemoveResponse/removedApps=] set to |removedApps|. + - [=SubAppsRemoveResponse/failedApps=] set to |failedApps|. 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. @@ -308,7 +308,7 @@ The {{SubApps/list()}} method steps are: 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 `appName` set to |appName|. + 1. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with [=SubAppsListResult/appName=] set to |appName|. 1. [=map/set|Set=] |listResult|[|manifestId|] to |resultEntry|. From 840d74a3dd7245538dc583b8e417a6da2ab7f86a Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:46:52 +0200 Subject: [PATCH 19/24] clarify how we check existance of sub apps --- index.bs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.bs b/index.bs index e8309b6..bc817e5 100644 --- a/index.bs +++ b/index.bs @@ -211,8 +211,8 @@ The {{SubApps/add(install_paths)}} method steps are: 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|. + - {{SubAppsAddResponse/installedApps}} set to |installedApps|. + - {{SubAppsAddResponse/failedApps}} set to |failedApps|. 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. @@ -256,7 +256,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Let |manifestId| be |parsedUrl|'s [=url/path=]. - 1. If |manifestId| does not exist under |parentApp|, perform the following steps: + 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}}. @@ -273,8 +273,8 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Append |manifestId| to |removedApps|. 1. Let |response| be a new {{SubAppsRemoveResponse}} dictionary with: - - [=SubAppsRemoveResponse/removedApps=] set to |removedApps|. - - [=SubAppsRemoveResponse/failedApps=] set to |failedApps|. + - {{SubAppsRemoveResponse/removedApps}} set to |removedApps|. + - {{SubAppsRemoveResponse/failedApps}} set to |failedApps|. 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. @@ -308,7 +308,7 @@ The {{SubApps/list()}} method steps are: 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. Let |resultEntry| be a new {{SubAppsListResult}} dictionary with {{SubAppsListResult/appName}} set to |appName|. 1. [=map/set|Set=] |listResult|[|manifestId|] to |resultEntry|. From 581d7ca0ed5a4292d4b60ecd0c3f6492ba558f5a Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:53:34 +0200 Subject: [PATCH 20/24] make fetch and process manifest to be todos --- index.bs | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/index.bs b/index.bs index bc817e5..45fd3f5 100644 --- a/index.bs +++ b/index.bs @@ -4,8 +4,8 @@ Shortname: sub-apps Level: None Status: UD Group: wicg -Repository: explainers-by-googlers/sub-apps -URL: https://explainers-by-googlers.github.io/sub-apps +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 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 @@ -158,34 +158,12 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Let |installPath| be |absoluteUrl|'s [=url/path=]. - 1. Let |htmlRequest| be a new [=request=] whose [=request/url=] is |absoluteUrl|, [=request/client=] is the [=relevant settings object=] of [=this=], and [=request/destination=] is "`document`". + 1. Let |manifest| be the result of [=fetch and process the manifest=] given |absoluteUrl|. - 1. Let |htmlResponse| be the result of [=fetch|fetching=] |htmlRequest|. - - 1. If |htmlResponse| is a [=network error=], perform the following steps: + 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 |document| be the HTML document obtained by parsing |htmlResponse|'s [=response/body=]. - - 1. Let |manifestUrl| be the URL obtained by resolving the `href` attribute of the first <{link}> element whose `rel` attribute contains "`manifest`" inside |document|. - - 1. If the <{link}> element is not found, or |manifestUrl| is failure, perform the following steps: - 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. - 1. [=iteration/continue|Continue=]. - - 1. Let |manifestRequest| be a new [=request=] whose [=request/url=] is |manifestUrl|, [=request/client=] is the [=relevant settings object=] of [=this=], and [=request/destination=] is "`manifest`". - - 1. Let |manifestResponse| be the result of [=fetch|fetching=] |manifestRequest|. - - 1. If |manifestResponse| is a [=network error=], perform the following steps: - 1. [=map/set|Set=] |failedApps|[|installPath|] to a new "{{DataError}}" {{DOMException}}. - 1. [=iteration/continue|Continue=]. - - 1. Let |bodyBytes| be the [=byte sequence=] of |manifestResponse|'s [=response/body=]. - - 1. Let |manifest| be the result of [=processing a manifest=] given [=this=]'s [=relevant global object=]'s [=associated Document=]'s [=Document/URL=], |manifestUrl|, |bodyBytes|, and the [=relevant settings object=] of [=this=]. - 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: @@ -316,6 +294,14 @@ The {{SubApps/list()}} method steps are: 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} ====================================================== From 1916a18906e0164768f722ea6575f65619e83ebc Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 17:59:05 +0200 Subject: [PATCH 21/24] extract document and this into variables --- index.bs | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/index.bs b/index.bs index 45fd3f5..019b12e 100644 --- a/index.bs +++ b/index.bs @@ -120,23 +120,25 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Let |promise| be [=a new promise=]. -1. If the [=relevant global object=]'s [=associated 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. Let |document| be the [=relevant global object=]'s [=associated Document=]. -1. If the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +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 [=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 the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + 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 parsing |installPath| with the [=relevant global object=]'s [=associated Document=]'s [=origin=] as the base URL. + 1. Let |absoluteUrl| be the result of parsing |installPath| with |document|'s [=origin=] 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 the [=relevant global object=]'s [=associated Document=]'s [=associated parent app=]. +1. Let |parentApp| be |document|'s [=associated parent app=]. 1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. @@ -144,11 +146,13 @@ The {{SubApps/add(install_paths)}} method steps are: 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 [=this=] to [=reject=] |promise| with a "{{NotAllowedError}}" {{DOMException}}, and abort these steps. + 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. @@ -192,7 +196,7 @@ The {{SubApps/add(install_paths)}} method steps are: - {{SubAppsAddResponse/installedApps}} set to |installedApps|. - {{SubAppsAddResponse/failedApps}} set to |failedApps|. - 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. + 1. [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=resolve=] |promise| with |response|. 1. Return |promise|. @@ -204,11 +208,13 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. Let |promise| be [=a new promise=]. -1. If the [=relevant global object=]'s [=associated 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. 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 the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +1. If |document| is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. -1. Let |parentApp| be the [=relevant global object=]'s [=associated Document=]'s [=associated parent app=]. +1. Let |parentApp| be |document|'s [=associated parent app=]. 1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. @@ -216,14 +222,16 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 1. [=list/iterate|For each=] |manifestId| in {{SubApps/remove(manifest_ids)/manifest_ids}}: - 1. If |manifestId| is not a [=valid relative path=] for the [=relevant global object=]'s [=associated Document=], [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. + 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 parsing |manifestId| with the [=relevant global object=]'s [=associated Document=]'s [=origin=] as the base URL. + 1. Let |parsedUrl| be the result of parsing |manifestId| with |document|'s [=origin=] 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. @@ -254,7 +262,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: - {{SubAppsRemoveResponse/removedApps}} set to |removedApps|. - {{SubAppsRemoveResponse/failedApps}} set to |failedApps|. - 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |response|. + 1. [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=resolve=] |promise| with |response|. 1. Return |promise|. @@ -264,21 +272,25 @@ The {{SubApps/list()}} method steps are: 1. Let |promise| be [=a new promise=]. -1. If the [=relevant global object=]'s [=associated 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. 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 the [=relevant global object=]'s [=associated Document=] is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +1. If |document| is a [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. -1. Let |parentApp| be the [=relevant global object=]'s [=associated Document=]'s [=associated parent app=]. +1. Let |parentApp| be |document|'s [=associated parent app=]. 1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +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 [=this=] to [=reject=] |promise| with an "{{OperationError}}" {{DOMException}}, and abort these steps. + 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|: @@ -290,7 +302,7 @@ The {{SubApps/list()}} method steps are: 1. [=map/set|Set=] |listResult|[|manifestId|] to |resultEntry|. - 1. [=Queue a global task=] on the [=relevant global object=] of [=this=] to [=resolve=] |promise| with |listResult|. + 1. [=Queue a global task=] on the [=relevant global object=] of |subApps| to [=resolve=] |promise| with |listResult|. 1. Return |promise|. From fc7cafd3dec06ef23ee91c3baafc5ed3176d6f62 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Tue, 14 Jul 2026 18:04:18 +0200 Subject: [PATCH 22/24] Fix associated document link --- index.bs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/index.bs b/index.bs index 019b12e..963b328 100644 --- a/index.bs +++ b/index.bs @@ -42,11 +42,11 @@ An [=installed web application=] has an associated sub-apps set, which is a [=set=] of [=installed web application=]s. -A {{Document}} can have 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}} 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=]. -The associated parent app of a {{Document}} |document| is its [=associated installed web application=]'s [=installed web application/parent app=] if that is not null, or its [=associated installed web application=] otherwise. +The associated parent app of a {{Document}} |document| is its [=Document/associated installed web application=]'s [=installed web application/parent app=] if that is not null, or its [=Document/associated installed web application=] otherwise. -A {{Document}} is a sub-app document if it has an [=associated installed web application=] and its [=associated installed web application=]'s [=installed web application/parent app=] is not null. +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} ======================================================= @@ -124,7 +124,7 @@ The {{SubApps/add(install_paths)}} method steps are: 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 [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{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. @@ -132,13 +132,13 @@ The {{SubApps/add(install_paths)}} method steps are: 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 parsing |installPath| with |document|'s [=origin=] as the base URL. + 1. Let |absoluteUrl| be the result of parsing |installPath| with |document|'s [=Document/origin=] 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 [=associated parent app=]. +1. Let |parentApp| be |document|'s [=Document/associated parent app=]. 1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. @@ -212,9 +212,9 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 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 [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{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 [=associated parent app=]. +1. Let |parentApp| be |document|'s [=Document/associated parent app=]. 1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. @@ -224,7 +224,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 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 parsing |manifestId| with |document|'s [=origin=] as the base URL. + 1. Let |parsedUrl| be the result of parsing |manifestId| with |document|'s [=Document/origin=] as the base URL. 1. If |parsedUrl| is failure, [=reject=] |promise| with a "{{TypeError}}" {{DOMException}} and return |promise|. @@ -276,9 +276,9 @@ The {{SubApps/list()}} method steps are: 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 [=sub-app document=], [=reject=] |promise| with a "{{NotSupportedError}}" {{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 [=associated parent app=]. +1. Let |parentApp| be |document|'s [=Document/associated parent app=]. 1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. From e0cff05d0697d635be0e6b312747d0ac9b5d88f3 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Wed, 15 Jul 2026 11:45:41 +0200 Subject: [PATCH 23/24] parent app non null --- index.bs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/index.bs b/index.bs index 963b328..c4855be 100644 --- a/index.bs +++ b/index.bs @@ -138,9 +138,7 @@ The {{SubApps/add(install_paths)}} method steps are: 1. Append |absoluteUrl| to |parsedUrls|. -1. Let |parentApp| be |document|'s [=Document/associated parent app=]. - -1. If |parentApp| is null, [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and return |promise|. +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=]. @@ -214,9 +212,7 @@ The {{SubApps/remove(manifest_ids)}} method steps are: 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 parent app=]. - -1. If |parentApp| is null, [=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. @@ -278,9 +274,7 @@ The {{SubApps/list()}} method steps are: 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 parent app=]. - -1. If |parentApp| is null, [=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=]. From ec7e9174d4a89da1ea6a2e0afa52f92ffbb58f14 Mon Sep 17 00:00:00 2001 From: Vlad Krot Date: Wed, 15 Jul 2026 12:11:55 +0200 Subject: [PATCH 24/24] specify parsing better --- index.bs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/index.bs b/index.bs index c4855be..38172b4 100644 --- a/index.bs +++ b/index.bs @@ -44,8 +44,6 @@ An [=installed web application=] has an associated