Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/facade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ These accept JSON strings for complex configuration:
| `allow_additional_tags` | `{}` | `hostConfig.allowAdditionalTags` | HTML sanitizer tag whitelist (e.g. `{"w-chart":["data","type"]}`) |
| `chat` | `{}` | `hostConfig.chat` | Chat config (e.g. `{"convertPasteToFile":{"enabled":true,"minFileSize":1024,"allowHtml":false}}`) |
| `axios_defaults` | `{}` | `axiosDefaults` | HTTP client defaults (e.g. `{"timeout":30000}`) — top-level, not under hostConfig |
| `extra_scripts` | `[]` | `extraScripts` | External `<script>` tags injected into `index.html` before the Web Host bundle loads. See [Extra scripts](#extra-scripts). |

### Auth

Expand Down Expand Up @@ -183,6 +184,28 @@ Only override what differs from defaults.
value: "custom:logo"
```

### Extra scripts

Inject external `<script>` tags into the facade `index.html` before the Web Host bundle loads. Useful for host-context integrations (analytics, third-party bridges) that need to run in the top-level window, not inside child iframes.

Each entry is either a string (shorthand for `{ src }`) or an object with any of: `src` (required), `async`, `defer`, `type`, `noModule`, `crossorigin`, `integrity`.

```yaml
- name: extra_scripts
value: '["/bridge.js"]'
```

Object form:

```yaml
- name: extra_scripts
value: '[{"src":"/bridge.js"},{"src":"https://cdn.example.com/analytics.js","defer":true}]'
```

Scripts are fetched in parallel and awaited before the Web Host bundle is imported, so any globals they define are available to the bundle. Load failures are logged to `console.warn` and do NOT block app startup.

> **Security:** entries come from `ns.requirement` defaults — i.e. from the application owner, not from end users — so arbitrary URLs in this list are trusted. Still, prefer `integrity` hashes for third-party CDN scripts.

## Config Response

`GET /api/public/facade/config` returns (wippy-context-2.0 format):
Expand Down Expand Up @@ -217,7 +240,8 @@ Only override what differs from defaults.
"hideNavBar": false,
"disableRightPanel": false,
"hideSessionSelector": false
}
},
"extraScripts": null
}
```

Expand Down
9 changes: 9 additions & 0 deletions src/facade/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ entries:
path: .default
default: "{}"

- name: extra_scripts
kind: ns.requirement
meta:
description: "External scripts injected into facade index.html before the Web Host bundle loads. JSON array of strings (src shorthand) or objects ({\"src\":\"...\",\"async\":true,\"defer\":true,\"type\":\"module\",\"crossorigin\":\"anonymous\",\"integrity\":\"...\"}). e.g. [\"/script.js\",{\"src\":\"https://cdn.example.com/x.js\",\"defer\":true}]"
targets:
- entry: wippy.facade:extra_scripts
path: .default
default: "[]"

# Static files
- name: public_files
kind: fs.directory
Expand Down
9 changes: 9 additions & 0 deletions src/facade/config_handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ local function non_empty_map_or_nil(m: any): {[string]: any}?
return m :: {[string]: any}
end

local function non_empty_array_or_nil(a: any): {any}?
if not a or type(a) ~= "table" or #a == 0 then
return nil
end
return a :: {any}
end

-- Build a theming scope from requirement name prefixes.
-- Each scope can have: customCSS, cssVariables, iconSets.
local function build_theming_scope(css_req: string, vars_req: string, icon_sets_req: string?): {[string]: any}?
Expand Down Expand Up @@ -158,6 +165,7 @@ local function handler()
end

local axios_defaults = non_empty_map_or_nil(get_req_json_any("axios_defaults"))
local extra_scripts = non_empty_array_or_nil(get_req_json_any("extra_scripts"))

local config = {
facade_url = facade_url,
Expand All @@ -173,6 +181,7 @@ local function handler()
routePrefix = non_empty_or_nil(api_url),
apiRoutes = api_routes,
axiosDefaults = axios_defaults,
extraScripts = extra_scripts,
theming = {
global = global_scope,
host = host_scope,
Expand Down
36 changes: 36 additions & 0 deletions src/facade/config_handler_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ local REQ_NAMES: {string} = {
"app_title", "app_icon", "app_name", "login_path",
"api_routes", "additional_nav_items", "state_cache",
"allow_additional_tags", "chat", "axios_defaults",
"extra_scripts",
}

local function setup_registry(overrides: {[string]: string}?)
Expand Down Expand Up @@ -46,6 +47,7 @@ local function setup_registry(overrides: {[string]: string}?)
allow_additional_tags = "{}",
chat = "{}",
axios_defaults = "{}",
extra_scripts = "[]",
}

if overrides then
Expand Down Expand Up @@ -206,6 +208,40 @@ local function define_tests()
end)
end)

test.describe("extra scripts", function()
test.it("parses string shorthand as src", function()
local raw = '["/bridge.js"]'
local decoded, err = json.decode(raw)
test.is_nil(err)
test.eq(decoded[1], "/bridge.js")
end)

test.it("parses object form with attributes", function()
local raw = '[{"src":"https://cdn.example.com/x.js","defer":true,"type":"module"}]'
local decoded, err = json.decode(raw)
test.is_nil(err)
test.eq(decoded[1].src, "https://cdn.example.com/x.js")
test.is_true(decoded[1].defer)
test.eq(decoded[1].type, "module")
end)

test.it("empty array results in nil (omitted from config)", function()
local raw = '[]'
local decoded, err = json.decode(raw)
test.is_nil(err)
test.eq(#decoded, 0)
end)

test.it("mixed string and object entries", function()
local raw = '["/a.js",{"src":"/b.js","defer":true}]'
local decoded, err = json.decode(raw)
test.is_nil(err)
test.eq(#decoded, 2)
test.eq(decoded[1], "/a.js")
test.eq(decoded[2].src, "/b.js")
end)
end)

test.describe("config JSON structure (wippy-context-2.0)", function()
test.it("builds complete config object", function()
local config = {
Expand Down
23 changes: 23 additions & 0 deletions src/facade/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@
try { token = JSON.parse(stored).token; }
catch (e) { window.location.href = cfg.login_path || '/login.html'; throw new Error('bad token'); }

if (Array.isArray(cfg.extraScripts) && cfg.extraScripts.length) {
await Promise.all(cfg.extraScripts.map(function (entry) {
return new Promise(function (resolve) {
var attrs = typeof entry === 'string' ? { src: entry } : (entry || {});
if (!attrs.src) { resolve(); return; }
var s = document.createElement('script');
s.src = attrs.src;
if (attrs.type) s.type = attrs.type;
if (attrs.async) s.async = true;
if (attrs.defer) s.defer = true;
if (attrs.noModule) s.noModule = true;
if (attrs.crossorigin) s.crossOrigin = attrs.crossorigin;
if (attrs.integrity) s.integrity = attrs.integrity;
s.onload = function () { resolve(); };
s.onerror = function () {
console.warn('Failed to load extra script:', attrs.src);
resolve();
};
document.head.appendChild(s);
});
}));
}

// Fallback env URLs if backend didn't provide them
if (!cfg.env.APP_API_URL) {
cfg.env.APP_API_URL = window.location.origin;
Expand Down
Loading