com.robotsquid.squidhub 0.5.10 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  // GENERATED FILE — do not edit by hand.
2
2
  // Source: engine/bridge-entry.js. Regenerate: npm run generate
3
3
  // Installs window.SquidHub. Load it BEFORE your engine's bootstrap.
4
- "use strict";(()=>{var h="squidhub",C=["https://robotsquid.dev","https://robotsquid.com"],N=["saves.get","saves.put","analytics.track","commerce.purchase"];var s=class extends Error{code;retryAfter;constructor(e,t,r){super(t,r?.cause!==void 0?{cause:r.cause}:void 0),this.name="SquidHubError",this.code=e,r?.retryAfter!==void 0&&(this.retryAfter=r.retryAfter)}},g=class extends s{serverCopy;constructor(e){super("CONFLICT","save version conflict \u2014 another device wrote first"),this.name="SaveConflictError",this.serverCopy=e}};var I=new Set,v=!1,R=!1,k=null,H=null;function M(n){try{n.gain.value=v?0:1}catch{}}function x(n){n.querySelectorAll("audio, video").forEach(e=>{e.muted=v})}function V(n=window){if(R)return;R=!0;let e=["AudioContext","webkitAudioContext"];for(let t of e){let r=n[t];if(typeof r!="function")continue;let o=function(...c){let a=new r(...c);try{let y=a.destination,l=a.createGain();l.connect(y),M(l),I.add(l),Object.defineProperty(a,"destination",{configurable:!0,get:()=>l})}catch{}return a};o.prototype=r.prototype,n[t]=o}typeof n.MutationObserver=="function"&&n.document&&(k=new n.MutationObserver(()=>x(n.document)),k.observe(n.document.documentElement,{childList:!0,subtree:!0}))}function P(n){v=n;for(let e of I)M(e);typeof document<"u"&&x(document);try{H?.(v)}catch{}}function $(n){if(!n)throw new s("UNTRUSTED_EMBEDDER","no referrer \u2014 the game must be embedded by the SquidHub portal");let e;try{e=new URL(n).origin}catch(t){throw new s("UNTRUSTED_EMBEDDER",`malformed referrer: ${n}`,{cause:t})}if(!C.includes(e))throw new s("UNTRUSTED_EMBEDDER",`embedder ${e} is not a SquidHub portal \u2014 refusing to hand shake`);return e}function j(n){let t=new URLSearchParams(n.replace(/^#/,"")).get("s");if(!t)throw new s("PROTOCOL","missing launch nonce \u2014 the game was not opened through the SquidHub portal");return t}var A=class{session;#n;#e=new Map;#o=0;#r;constructor(e,t,r){this.session=e,this.#n=t,this.#r=r,t.onmessage=o=>this.#s(o.data)}get connected(){return this.#n!==null}can(e){return this.session.capabilities.includes(e)}close(){for(let[,e]of this.#e)clearTimeout(e.timer),e.reject(new s("NOT_CONNECTED","SDK closed"));this.#e.clear(),this.#n?.close(),this.#n=null}#s(e){if(typeof e!="object"||e===null)return;let t=e;if(t.t===`${h}.audio`&&typeof t.muted=="boolean"){P(t.muted);return}if(typeof t.id!="string")return;let r=this.#e.get(t.id);if(!r)return;if(this.#e.delete(t.id),clearTimeout(r.timer),t.ok===!0){r.resolve(t.r);return}let o=t.e??{};r.reject(new s(o.code??"PROTOCOL",o.message??"request failed",o.retryAfter!==void 0?{retryAfter:o.retryAfter}:void 0))}#t(e,t){let r=this.#n;if(!r)return Promise.reject(new s("NOT_CONNECTED","SDK is not connected"));let o=`r${++this.#o}`;return new Promise((c,a)=>{let y=setTimeout(()=>{this.#e.delete(o),a(new s("TIMEOUT",`${e} timed out`))},this.#r);this.#e.set(o,{resolve:c,reject:a,timer:y}),r.postMessage({id:o,v:1,t:e,p:t})})}saves={get:()=>this.#t("saves.get",{}),put:async(e,t)=>{if(e.byteLength>8388608)throw new s("INVALID",`save is ${e.byteLength} bytes, over the ${8388608} limit`);try{return await this.#t("saves.put",{data:e,...t?.ifVersion!==void 0?{ifVersion:t.ifVersion}:{}})}catch(r){if(r instanceof s&&r.code==="CONFLICT"){let o=await this.saves.get();if(o)throw new g(o)}throw r}}};analytics={track:(e,t)=>{this.#t("analytics.track",{name:e,props:t}).catch(()=>{})}};commerce={purchase:(e,t)=>this.#t("commerce.purchase",{sku:e,idempotencyKey:t})}};async function D(n={},e=globalThis.window){let t=n.handshakeTimeoutMs??15e3,r=n.requestTimeoutMs??2e4;V(e);let o=$(e.document.referrer),c=j(e.location.hash),a=n.helloRetryMs??150;return new Promise((y,l)=>{let p,m=()=>{clearTimeout(b),p!==void 0&&clearInterval(p),e.removeEventListener("message",i)},b=setTimeout(()=>{m(),l(new s("TIMEOUT","portal did not complete the handshake"))},t);function i(u){if(u.origin!==o)return;let f=u.data;if(!f||f.t!==`${h}.ready`||f.v!==1)return;let w=u.ports[0];w&&(m(),w.start?.(),y(new A(f.session,w,r)))}e.addEventListener("message",i);let d=()=>e.parent.postMessage({t:`${h}.hello`,v:1,nonce:c},o);d(),p=setInterval(d,a)})}var B="SquidHub",_=32768;function G(n){let e="";for(let t=0;t<n.length;t+=_)e+=String.fromCharCode(...n.subarray(t,t+_));return btoa(e)}function J(n){let e=atob(n),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}function O(n){return n instanceof Uint8Array?G(n):Array.isArray(n)?n.map(O):n&&typeof n=="object"?Object.fromEntries(Object.entries(n).map(([e,t])=>[e,O(t)])):n}function F(n,e){let t=n[e];if(typeof t!="number"||!Number.isInteger(t))throw new s("INVALID",`${e} must be an integer, got ${JSON.stringify(t)}`);return t}function E(n,e){let t=n[e];if(typeof t!="string")throw new s("INVALID",`${e} must be a string, got ${JSON.stringify(t)}`);return t}function W(n,e){try{return J(E(n,e))}catch(t){throw new s("INVALID",`${e} is not valid base64`,{cause:t})}}function K(n,e){let t=n[e];if(t==null)return;if(typeof t!="object"||Array.isArray(t))throw new s("INVALID",`${e} must be an object`);let r={};for(let[o,c]of Object.entries(t)){if(typeof c!="string"&&typeof c!="number"&&typeof c!="boolean")throw new s("INVALID",`${e}.${o} must be a string, number or boolean`);r[o]=c}return r}var X={"saves.get":n=>n.saves.get(),"saves.put":(n,e)=>n.saves.put(W(e,"data"),e.ifVersion===void 0?void 0:{ifVersion:F(e,"ifVersion")}),"analytics.track":(n,e)=>(n.analytics.track(E(e,"name"),K(e,"props")),null),"commerce.purchase":(n,e)=>n.commerce.purchase(E(e,"sku"),E(e,"idempotencyKey"))};function Y(n){return N.includes(n)}function U(n){return n instanceof s?n.code:"PROTOCOL"}function z(n){let e=n instanceof s?n:void 0;return JSON.stringify({ok:!1,code:U(n),message:n instanceof Error?n.message:String(n),...e?.retryAfter!==void 0?{retryAfter:e.retryAfter}:{},...n instanceof g?{serverCopy:O(n.serverCopy)}:{}})}function T(n={},e=globalThis){let t=null,r="connecting",o="",c=D(n).then(i=>(t=i,r="ready",i),i=>{throw r="failed",o=`${U(i)}: ${i instanceof Error?i.message:String(i)}`,i});c.catch(()=>{});let a=new Map,y=0,l=64;function p(i,d){if(a.size>=l){let u=a.keys().next();u.done||a.delete(u.value)}a.set(i,d)}async function m(i,d){try{if(!Y(i))throw new s("INVALID",`unknown verb ${i}`);let u;try{u=JSON.parse(d||"{}")??{}}catch(q){throw new s("INVALID","payload is not valid JSON",{cause:q})}let f=await c,w=await X[i](f,u);return JSON.stringify({ok:!0,value:O(w)??null})}catch(u){return z(u)}}let b={status:()=>r,error:()=>o,session:()=>t?JSON.stringify(t.session):"",invoke(i,d="{}"){let u=`c${++y}`;return m(i,d).then(f=>p(u,f)),u},notify(i,d="{}"){m(i,d)},poll(i){let d=a.get(i);return d===void 0?"":(a.delete(i),d)},close(){t?.close()}};return e[B]=b,b}T();})();
4
+ "use strict";(()=>{var v="squidhub",M=["https://robotsquid.dev","https://robotsquid.com"],I=["saves.get","saves.put","analytics.track","commerce.purchase"];var i=class extends Error{code;retryAfter;constructor(e,n,r){super(n,r?.cause!==void 0?{cause:r.cause}:void 0),this.name="SquidHubError",this.code=e,r?.retryAfter!==void 0&&(this.retryAfter=r.retryAfter)}},g=class extends i{serverCopy;constructor(e){super("CONFLICT","save version conflict \u2014 another device wrote first"),this.name="SaveConflictError",this.serverCopy=e}};var C=new Set,A=new WeakSet,E=!1,x=!1,V=null,P=null,G=null;function L(t){try{t.gain.value=E?0:1}catch{}}function D(t){t.querySelectorAll("audio, video").forEach(e=>{let n=e;if(E){n.muted||(A.add(n),n.muted=!0);return}A.has(n)&&(A.delete(n),n.muted=!1)})}function _(t=window){if(x)return;x=!0,P=t.document??null;let e=["AudioContext","webkitAudioContext"];for(let n of e){let r=t[n];if(typeof r!="function")continue;let o=function(...u){let s=new r(...u);try{let p=s.destination,f=s.createGain();f.connect(p),L(f),C.add(f),Object.defineProperty(s,"destination",{configurable:!0,get:()=>f});try{let y=s.close.bind(s);s.close=()=>(C.delete(f),y())}catch{}}catch{}return s};o.prototype=r.prototype,t[n]=o}typeof t.MutationObserver=="function"&&t.document&&(V=new t.MutationObserver(n=>{let r=!1;for(let o of n){if(r)break;o.addedNodes.forEach(u=>{if(r)return;let s=u;(s.nodeName==="AUDIO"||s.nodeName==="VIDEO"||s.querySelector?.("audio, video"))&&(r=!0)})}r&&D(t.document)}),V.observe(t.document.documentElement,{childList:!0,subtree:!0}))}function U(t){E=t;for(let n of C)L(n);let e=P??(typeof document<"u"?document:null);e&&D(e);try{G?.(E)}catch{}}function J(t){if(!t)throw new i("UNTRUSTED_EMBEDDER","no referrer \u2014 the game must be embedded by the SquidHub portal");let e;try{e=new URL(t).origin}catch(n){throw new i("UNTRUSTED_EMBEDDER",`malformed referrer: ${t}`,{cause:n})}if(!M.includes(e))throw new i("UNTRUSTED_EMBEDDER",`embedder ${e} is not a SquidHub portal \u2014 refusing to hand shake`);return e}function W(t){let n=new URLSearchParams(t.replace(/^#/,"")).get("s");if(!n)throw new i("PROTOCOL","missing launch nonce \u2014 the game was not opened through the SquidHub portal");return n}var R=class{session;#n;#e=new Map;#o=0;#r;constructor(e,n,r){this.session=e,this.#n=n,this.#r=r,n.onmessage=o=>this.#s(o.data),n.start?.()}get connected(){return this.#n!==null}can(e){return this.session.capabilities.includes(e)}close(){for(let[,e]of this.#e)clearTimeout(e.timer),e.reject(new i("NOT_CONNECTED","SDK closed"));this.#e.clear(),this.#n?.close(),this.#n=null}#s(e){if(typeof e!="object"||e===null)return;let n=e;if(n.t===`${v}.audio`&&typeof n.muted=="boolean"){U(n.muted);return}if(typeof n.id!="string")return;let r=this.#e.get(n.id);if(!r)return;if(this.#e.delete(n.id),clearTimeout(r.timer),n.ok===!0){r.resolve(n.r);return}let o=n.e??{};r.reject(new i(o.code??"PROTOCOL",o.message??"request failed",o.retryAfter!==void 0?{retryAfter:o.retryAfter}:void 0))}#t(e,n){let r=this.#n;if(!r)return Promise.reject(new i("NOT_CONNECTED","SDK is not connected"));let o=`r${++this.#o}`;return new Promise((u,s)=>{let p=setTimeout(()=>{this.#e.delete(o),s(new i("TIMEOUT",`${e} timed out`))},this.#r);this.#e.set(o,{resolve:u,reject:s,timer:p}),r.postMessage({id:o,v:1,t:e,p:n})})}saves={get:()=>this.#t("saves.get",{}),put:async(e,n)=>{if(e.byteLength>8388608)throw new i("INVALID",`save is ${e.byteLength} bytes, over the ${8388608} limit`);try{return await this.#t("saves.put",{data:e,...n?.ifVersion!==void 0?{ifVersion:n.ifVersion}:{}})}catch(r){if(r instanceof i&&r.code==="CONFLICT"){let o=await this.saves.get();if(o)throw new g(o)}throw r}}};analytics={track:(e,n)=>{this.#t("analytics.track",{name:e,props:n}).catch(()=>{})}};commerce={purchase:(e,n)=>this.#t("commerce.purchase",{sku:e,idempotencyKey:n})}};async function q(t={},e=globalThis.window){let n=t.handshakeTimeoutMs??15e3,r=t.requestTimeoutMs??2e4;_(e);let o=J(e.document.referrer),u=W(e.location.hash),s=t.helloRetryMs??150;return new Promise((p,f)=>{let y,b=()=>{clearTimeout(h),y!==void 0&&clearInterval(y),e.removeEventListener("message",w)},h=setTimeout(()=>{b(),f(new i("TIMEOUT","portal did not complete the handshake"))},n);function w(d){if(d.origin!==o)return;let c=d.data;if(!c||c.t!==`${v}.ready`||c.v!==1)return;let l=d.ports[0];l&&(b(),l.start?.(),p(new R(c.session,l,r)))}e.addEventListener("message",w);let a=()=>e.parent.postMessage({t:`${v}.hello`,v:1,nonce:u},o);a(),y=setInterval(a,s)})}var $="SquidHub",H=32768;function F(t){let e="";for(let n=0;n<t.length;n+=H)e+=String.fromCharCode(...t.subarray(n,n+H));return btoa(e)}function K(t){let e=atob(t),n=new Uint8Array(e.length);for(let r=0;r<e.length;r++)n[r]=e.charCodeAt(r);return n}function T(t){return t instanceof Uint8Array?F(t):Array.isArray(t)?t.map(T):t&&typeof t=="object"?Object.fromEntries(Object.entries(t).map(([e,n])=>[e,T(n)])):t}function X(t,e){let n=t[e];if(typeof n!="number"||!Number.isInteger(n))throw new i("INVALID",`${e} must be an integer, got ${JSON.stringify(n)}`);return n}function O(t,e){let n=t[e];if(typeof n!="string")throw new i("INVALID",`${e} must be a string, got ${JSON.stringify(n)}`);return n}function Y(t,e){try{return K(O(t,e))}catch(n){throw new i("INVALID",`${e} is not valid base64`,{cause:n})}}function z(t,e){let n=t[e];if(n==null)return;if(typeof n!="object"||Array.isArray(n))throw new i("INVALID",`${e} must be an object`);let r={};for(let[o,u]of Object.entries(n)){if(typeof u!="string"&&typeof u!="number"&&typeof u!="boolean")throw new i("INVALID",`${e}.${o} must be a string, number or boolean`);r[o]=u}return r}var Q={"saves.get":t=>t.saves.get(),"saves.put":(t,e)=>t.saves.put(Y(e,"data"),e.ifVersion===void 0?void 0:{ifVersion:X(e,"ifVersion")}),"analytics.track":(t,e)=>(t.analytics.track(O(e,"name"),z(e,"props")),null),"commerce.purchase":(t,e)=>t.commerce.purchase(O(e,"sku"),O(e,"idempotencyKey"))};function Z(t){return I.includes(t)}function j(t){return t instanceof i?t.code:"PROTOCOL"}function ee(t){let e=t instanceof i?t:void 0;return JSON.stringify({ok:!1,code:j(t),message:t instanceof Error?t.message:String(t),...e?.retryAfter!==void 0?{retryAfter:e.retryAfter}:{},...t instanceof g?{serverCopy:T(t.serverCopy)}:{}})}function k(t={},e=globalThis){let n=null,r="connecting",o="",u=q(t).then(a=>(n=a,r="ready",a),a=>{throw r="failed",o=`${j(a)}: ${a instanceof Error?a.message:String(a)}`,a});u.catch(()=>{});let s=new Map,p=0,f=256,y=6e4;function b(a,d){let c=Date.now();if(s.size>=f){let l=!1;for(let[m,S]of s)c-S.at>y&&(s.delete(m),l=!0);if(!l){let m=s.keys().next();m.done||s.delete(m.value)}}s.set(a,{json:d,at:c})}async function h(a,d){try{if(!Z(a))throw new i("INVALID",`unknown verb ${a}`);let c;try{c=JSON.parse(d||"{}")??{}}catch(S){throw new i("INVALID","payload is not valid JSON",{cause:S})}let l=await u,m=await Q[a](l,c);return JSON.stringify({ok:!0,value:T(m)??null})}catch(c){return ee(c)}}let w={status:()=>r,error:()=>o,session:()=>n?JSON.stringify(n.session):"",invoke(a,d="{}"){let c=`c${++p}`;return h(a,d).then(l=>b(c,l)),c},notify(a,d="{}"){h(a,d)},poll(a){let d=s.get(a);return d===void 0?"":(s.delete(a),d.json)},close(){n?.close()}};return e[$]=w,w}k();})();
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.12
4
+
5
+ **A manifest change is a new version now — the platform changed underneath this.**
6
+
7
+ Editing `manifest.json` and re-uploading used to report "these exact bytes are
8
+ already published, nothing to upload" and discard the edit. Server migration
9
+ 0029 makes version identity `(build_hash, manifest_sha256)`, so a changed
10
+ declaration registers a new version and takes effect.
11
+
12
+ ⚠️ **`manifest.json` is still excluded from the build hash and must be** — it
13
+ CONTAINS that hash, so folding it in has no fixed point. A separate digest of
14
+ the manifest file has no such circularity, because a manifest never contains its
15
+ own digest. The bytes are still deduped: two versions differing only in
16
+ declaration share one copy in R2.
17
+
18
+ ⚠️ **The 0.5.11 "your manifest was ignored" explanation is GONE**, because it is
19
+ now false. That message fired when the bytes matched; the bytes matching now
20
+ also means the manifest matched. Telling a publisher their edit was discarded at
21
+ the one moment we know it was not is worse than saying nothing.
22
+
23
+ **Requires the api deployed with migration 0029.** Against an older server this
24
+ package still works and a manifest-only change still silently does nothing.
25
+
26
+ ## 0.5.11
27
+
28
+ **"Nothing to upload" now says what it did to your manifest.**
29
+
30
+ Editing `manifest.json` and pressing Build and Upload reported *"These exact
31
+ bytes are already published. Nothing to upload."* and stopped — never mentioning
32
+ that the declaration had not been applied. With a manifest editor in the package
33
+ that is a normal thing to do, and the result was unreadable.
34
+
35
+ `manifest.json` is excluded from the build hash — it CONTAINS that hash, so
36
+ including it would have no fixed point — so a build whose only difference is its
37
+ declaration hashes identically and matches a version already registered.
38
+
39
+ ⚠️ **It stops at `start`, before a byte moves, which is EARLIER than the
40
+ server's own `manifest-ignored` warning.** That one is raised at `complete`, and
41
+ such a build never gets there, so this message is the only thing that will ever
42
+ say it.
43
+
44
+ The dialog now names the manifest and both ways forward: change a real file and
45
+ rebuild, or set the override in the publisher console — display settings are
46
+ live-tunable there and need no new build at all.
47
+
3
48
  ## 0.5.10
4
49
 
5
50
  **Fullscreen: the check that makes it actually work.**
@@ -186,9 +186,26 @@ namespace SquidHub.Editor
186
186
  if (SquidHubBuildScan.JsonField(startResponse, "alreadyPublished") == "true")
187
187
  {
188
188
  EditorUtility.ClearProgressBar();
189
+
190
+ // ⚠️ A no-op now means the BYTES AND THE DECLARATION both match.
191
+ // It used to mean only the bytes did, so a changed manifest.json
192
+ // landed here and was discarded — this dialog carried a long
193
+ // explanation of that trap. Migration 0029 made version identity
194
+ // (build_hash, manifest_sha256), so a declaration change
195
+ // registers a new version and never reaches this branch.
196
+ //
197
+ // ⚠️ Do NOT reinstate the old wording. Telling a publisher their
198
+ // manifest was ignored, at the one moment we know it was not, is
199
+ // worse than saying nothing.
200
+ var hash = SquidHubBuildScan.JsonField(startResponse, "buildHash");
201
+ Debug.Log("[SquidHub] " + hash + " is already published — same bytes, same " +
202
+ "manifest. Nothing to upload.");
203
+
189
204
  EditorUtility.DisplayDialog("SquidHub",
190
- "These exact bytes are already published as " + SquidHubBuildScan.JsonField(startResponse, "buildHash") +
191
- ". Nothing to upload.", "OK");
205
+ "Already published as " + hash + ".\n\n" +
206
+ "Nothing to upload: both the build and its manifest.json match what is " +
207
+ "already registered.",
208
+ "OK");
192
209
  yield break;
193
210
  }
194
211
 
package/README.md CHANGED
@@ -209,10 +209,14 @@ on your `AudioContext`. It works without you doing anything — but only because
209
209
  the bridge is injected ahead of the loader, which is why step 2 above is not
210
210
  merely tidy. Tick **Sound** in `SquidHub → Game Manifest…` or no button appears.
211
211
 
212
- ⚠️ **Editing `manifest.json` alone changes nothing.** It is excluded from the
213
- build hash, so a manifest-only re-upload registers no new version and your edit
214
- is discarded — the server now says so in a warning rather than staying silent.
215
- Change a real file and rebuild, or set an override in the console.
212
+ ⚠️ **Editing `manifest.json` alone registers a new version.** It used not to —
213
+ the manifest is excluded from the build hash, so the build looked identical and
214
+ the edit was discarded. A version is now identified by its bytes *and* its
215
+ declaration, so a manifest-only change ships. The bytes are still deduped: the
216
+ two versions share one copy in R2.
217
+
218
+ To change display on a build that is already live, the publisher console's
219
+ override is still the faster path — it needs no upload at all.
216
220
 
217
221
  ## ⚠️ Status
218
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.robotsquid.squidhub",
3
- "version": "0.5.10",
3
+ "version": "0.5.13",
4
4
  "displayName": "SquidHub",
5
5
  "description": "Cloud saves, analytics and the coin economy for Unity WebGL games published on SquidHub. Installs the C# wrapper, the .jslib half and the page bridge, and rewrites the exported index.html so it survives the platform's CSP.",
6
6
  "unity": "2021.3",