fix: keep IndeeHub provider injection idempotent for bundled frontends
When the IndeeHub container's own index.html already bundles nostr-provider.js, the managed nginx patch no longer injects a sub_filter provider and strips any existing one, staying idempotent either way; the provider cache-bust marker moves to tab-signer-v5. The rental request API now accepts an explicit boolean renewExpired choice, validates it, and forwards it to the dashboard bridge, with a focused provider regression.
This commit is contained in:
@@ -74,10 +74,15 @@ async fn local_podman_image_exists(image: &str) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
fn patched_indeedhub_nginx_config(original: &str) -> String {
|
||||
fn patched_indeedhub_nginx_config(original: &str, inject_provider: bool) -> String {
|
||||
let mut conf = original
|
||||
.lines()
|
||||
.filter(|line| !line.contains("X-Frame-Options"))
|
||||
.filter(|line| {
|
||||
!line.contains("X-Frame-Options")
|
||||
&& (inject_provider
|
||||
|| (!line.contains("sub_filter_once")
|
||||
&& !line.contains("sub_filter '</head>'")))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
conf.push('\n');
|
||||
@@ -91,21 +96,22 @@ fn patched_indeedhub_nginx_config(original: &str) -> String {
|
||||
location = /sw.js {",
|
||||
);
|
||||
}
|
||||
if conf.contains("try_files") && !conf.contains("sub_filter") {
|
||||
if inject_provider && conf.contains("try_files") && !conf.contains("sub_filter") {
|
||||
conf = conf.replacen(
|
||||
"try_files $uri $uri/ /index.html;",
|
||||
"try_files $uri $uri/ /index.html;\n\
|
||||
sub_filter_once on;\n\
|
||||
sub_filter '</head>' '<script src=\"/nostr-provider.js?v=tab-signer-v4\"></script></head>';",
|
||||
sub_filter '</head>' '<script src=\"/nostr-provider.js?v=tab-signer-v5\"></script></head>';",
|
||||
1,
|
||||
);
|
||||
}
|
||||
conf = conf.replace(
|
||||
"src=\"/nostr-provider.js\"",
|
||||
"src=\"/nostr-provider.js?v=tab-signer-v4\"",
|
||||
"src=\"/nostr-provider.js?v=tab-signer-v5\"",
|
||||
);
|
||||
conf = conf.replace("tab-signer-v2", "tab-signer-v4");
|
||||
conf = conf.replace("tab-signer-v3", "tab-signer-v4");
|
||||
conf = conf.replace("tab-signer-v2", "tab-signer-v5");
|
||||
conf = conf.replace("tab-signer-v3", "tab-signer-v5");
|
||||
conf = conf.replace("tab-signer-v4", "tab-signer-v5");
|
||||
conf.replace(
|
||||
"proxy_set_header X-Forwarded-Prefix /api;",
|
||||
"proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix/api;",
|
||||
@@ -165,6 +171,11 @@ pub(crate) async fn patch_indeedhub_nostr_provider() {
|
||||
|
||||
let provider_src = "/opt/archipelago/web-ui/nostr-provider.js";
|
||||
let provider_dest = format!("{container_root}/usr/share/nginx/html/nostr-provider.js");
|
||||
let provider_is_bundled =
|
||||
tokio::fs::read_to_string(format!("{container_root}/usr/share/nginx/html/index.html"))
|
||||
.await
|
||||
.map(|html| html.contains("src=\"/nostr-provider.js"))
|
||||
.unwrap_or(false);
|
||||
let provider_copied = tokio::fs::metadata(provider_src).await.is_ok()
|
||||
&& tokio::process::Command::new("podman")
|
||||
.args([
|
||||
@@ -189,7 +200,7 @@ pub(crate) async fn patch_indeedhub_nostr_provider() {
|
||||
if let Ok(out) = copy_out {
|
||||
if out.status.success() {
|
||||
if let Ok(original) = tokio::fs::read_to_string(&tmp_path).await {
|
||||
let conf = patched_indeedhub_nginx_config(&original);
|
||||
let conf = patched_indeedhub_nginx_config(&original, !provider_is_bundled);
|
||||
if conf != original && tokio::fs::write(&tmp_path, &conf).await.is_ok() {
|
||||
config_copied = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
@@ -219,7 +230,8 @@ pub(crate) async fn patch_indeedhub_nostr_provider() {
|
||||
}
|
||||
} else if conf == original
|
||||
&& conf.contains("location = /nostr-provider.js {")
|
||||
&& conf.contains("src=\"/nostr-provider.js?v=tab-signer-v4\"")
|
||||
&& (provider_is_bundled
|
||||
|| conf.contains("src=\"/nostr-provider.js?v=tab-signer-v5\""))
|
||||
{
|
||||
config_copied = true;
|
||||
}
|
||||
@@ -2618,19 +2630,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let patched = patched_indeedhub_nginx_config(original);
|
||||
let patched = patched_indeedhub_nginx_config(original, true);
|
||||
assert!(!patched.contains("X-Frame-Options"));
|
||||
assert!(patched.contains("location = /nostr-provider.js {"));
|
||||
assert!(patched.contains("Cache-Control \"no-cache, no-store, must-revalidate\""));
|
||||
assert!(patched.contains("src=\"/nostr-provider.js?v=tab-signer-v4\""));
|
||||
assert!(patched.contains("src=\"/nostr-provider.js?v=tab-signer-v5\""));
|
||||
assert!(patched.contains("X-Forwarded-Prefix $http_x_forwarded_prefix/api"));
|
||||
assert_eq!(patched_indeedhub_nginx_config(&patched), patched);
|
||||
assert_eq!(patched_indeedhub_nginx_config(&patched, true), patched);
|
||||
|
||||
let previous_broker = patched.replace("tab-signer-v4", "tab-signer-v3");
|
||||
let migrated = patched_indeedhub_nginx_config(&previous_broker);
|
||||
assert!(migrated.contains("tab-signer-v4"));
|
||||
let previous_broker = patched.replace("tab-signer-v5", "tab-signer-v3");
|
||||
let migrated = patched_indeedhub_nginx_config(&previous_broker, true);
|
||||
assert!(migrated.contains("tab-signer-v5"));
|
||||
assert!(!migrated.contains("tab-signer-v3"));
|
||||
assert_eq!(patched_indeedhub_nginx_config(&migrated), migrated);
|
||||
assert_eq!(patched_indeedhub_nginx_config(&migrated, true), migrated);
|
||||
|
||||
let bundled = patched_indeedhub_nginx_config(&patched, false);
|
||||
assert!(!bundled.contains("sub_filter"));
|
||||
assert_eq!(patched_indeedhub_nginx_config(&bundled, false), bundled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -439,7 +439,8 @@
|
||||
start: function (handle, readyId) { return rentalRequest('start', { handle: handle, ready_id: readyId }, 35000); },
|
||||
request: function (offer, options) {
|
||||
if (!options || options.playbackProtocol !== 2) return Promise.reject(new Error('Playback protocol 2 is required before requesting a rental.'));
|
||||
return rentalRequest('request', { offer: offer }, 600000, options.signal);
|
||||
if (options.renewExpired !== undefined && typeof options.renewExpired !== 'boolean') return Promise.reject(new Error('Invalid rental renewal request.'));
|
||||
return rentalRequest('request', { offer: offer, renewExpired: options.renewExpired === true }, 600000, options.signal);
|
||||
}
|
||||
};
|
||||
Object.defineProperty(window.archipelagoRental, 'playbackProtocol', { value: 2, writable: false, configurable: false, enumerable: true });
|
||||
|
||||
@@ -38,4 +38,27 @@ describe('native provider on ordinary HTTP node origins',()=>{
|
||||
await expect(window.archipelagoRental.request({title:'Already closed'},{playbackProtocol:2,signal:stop.signal})).rejects.toThrow('closed')
|
||||
expect(parent.postMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
it('validates and forwards the explicit rental renewal choice',async()=>{
|
||||
const listeners:((event:unknown)=>void)[]=[]
|
||||
const parent={postMessage:vi.fn()}
|
||||
const window:any={top:{},parent,location:{href:'http://node.local:7778/browse',pathname:'/browse',port:'7778',origin:'http://node.local:7778'},addEventListener:(_name:string,handler:(event:unknown)=>void)=>listeners.push(handler)}
|
||||
const timers=new Set<unknown>();let count=0
|
||||
runInNewContext(source,{window,document:{currentScript:{hasAttribute:()=>true},readyState:'complete'},crypto:{getRandomValues:(bytes:Uint8Array)=>{bytes.fill(++count);return bytes}},Uint8Array,URL,console,
|
||||
setTimeout:(fn:unknown)=>{timers.add(fn);return fn},clearTimeout:(fn:unknown)=>timers.delete(fn)})
|
||||
await expect(window.archipelagoRental.request({title:'Bad'},{playbackProtocol:2,renewExpired:'yes' as unknown as boolean})).rejects.toThrow('Invalid rental renewal request.')
|
||||
expect(parent.postMessage).not.toHaveBeenCalled()
|
||||
const renewal=window.archipelagoRental.request({title:'Expired'},{playbackProtocol:2,renewExpired:true})
|
||||
const fresh=window.archipelagoRental.request({title:'Fresh'},{playbackProtocol:2,renewExpired:false})
|
||||
const omitted=window.archipelagoRental.request({title:'Omitted'},{playbackProtocol:2})
|
||||
const messages=parent.postMessage.mock.calls.map(([message])=>message)
|
||||
expect(messages).toHaveLength(3)
|
||||
expect(messages[0]).toMatchObject({type:'archipelago-rental-request',action:'request',playbackProtocol:2,renewExpired:true})
|
||||
expect(messages[1]).toMatchObject({type:'archipelago-rental-request',action:'request',playbackProtocol:2,renewExpired:false})
|
||||
expect(messages[2]).toMatchObject({type:'archipelago-rental-request',action:'request',playbackProtocol:2,renewExpired:false})
|
||||
for(const message of messages) for(const receive of listeners) receive({source:parent,origin:'http://node.local',data:{type:'archipelago-rental-response',id:message.id,result:{ok:true}}})
|
||||
await expect(renewal).resolves.toEqual({ok:true})
|
||||
await expect(fresh).resolves.toEqual({ok:true})
|
||||
await expect(omitted).resolves.toEqual({ok:true})
|
||||
expect(timers.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user