studio-engine 0.72.3 → 0.74.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -2
- data/Gemfile +3 -2
- data/app/assets/tailwind/studio_engine/engine.css +1 -1
- data/app/views/layouts/_navbar.html.erb +1 -1
- data/app/views/solana_sessions/phantom_callback.html.erb +62 -0
- data/app/views/studio/emails/show.html.erb +14 -3
- data/app/views/studio/modals/_crop_photo.html.erb +7 -1
- data/app/views/studio/modals/blocks/_entry_confirmed.html.erb +7 -1
- data/app/views/studio/modals/blocks/_error_card.html.erb +11 -2
- data/app/views/studio/modals/blocks/_solana_tx_link.html.erb +6 -1
- data/app/views/studio/modals/blocks/_success_card.html.erb +16 -3
- data/app/views/studio/modals/onboarding/_first_name.html.erb +19 -19
- data/app/views/studio/profiles/_birthday_fields.html.erb +8 -1
- data/app/views/style/_modals.html.erb +21 -4
- data/lib/studio/js_literal.rb +80 -0
- data/lib/studio/s3.rb +10 -1
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a6961114226763cef0af1b1500c982609412516d95c78c0985f63012882e92e
|
|
4
|
+
data.tar.gz: 4694c467547769140156eac7824a72fd0d510d84f0c30637cba40a87c3344187
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 509affb42c8bf58a11289e0e0c16cfb2b298625456284b0d56775373fba4ac225c9bc6ba6bd695acb028a44a517836989204dfbb97bd96b1ed69ca2f1e9e7023
|
|
7
|
+
data.tar.gz: 36ed3258453f8025f591335348f989e333a6d805de78979e325c2a005a197d74c17f04995c6fcf6776bbebb2dae9d4404c483f0effe1e095bf29f2e02947ca83
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The app census in these comments was short by the most important app.** Ten
|
|
10
|
+
sites said this engine is mounted by SIX apps and that THREE of them bundle no
|
|
11
|
+
`solana-studio`. Measured 2026-09-07 by the criterion that reproduces it — a
|
|
12
|
+
checkout whose own `config/routes.rb` calls `Studio.routes(self)`. A Gemfile
|
|
13
|
+
grep returns seven instead, catching `solana-studio`'s dev-group pin and the
|
|
14
|
+
`mcritchie-studio-ai-builder-cache` clone of the hub. **Five** apps mount it —
|
|
15
|
+
`mcritchie-studio`, `turf-monster`, `acquisition-studio`,
|
|
16
|
+
`mcritchie-industries`, `moms-app` — and **four** of those bundle no
|
|
17
|
+
`solana-studio`. `turf-monster` is the only one that does.
|
|
18
|
+
|
|
19
|
+
**The omission was the HUB.** Every roster of BASE consumers named
|
|
20
|
+
`acquisition-studio`, `mcritchie-industries` and `moms-app` and left out
|
|
21
|
+
`mcritchie-studio` — the app a developer reading those comments is most likely
|
|
22
|
+
to be sitting in, and one whose `/admin/style` is exactly what the
|
|
23
|
+
missing-template gate protects. The error ran in the direction that
|
|
24
|
+
UNDERSTATES the risk the comments exist to explain.
|
|
25
|
+
|
|
26
|
+
**Two near misses are what make a naive count wrong.** `chain-ops` bundles
|
|
27
|
+
`solana-studio` and NOT this engine, so it is not in this set at all;
|
|
28
|
+
`acquisition-studio` is a retired prototype whose `Gemfile` still pins the
|
|
29
|
+
engine, so it is. `rolio` bundles neither and is not a consumer.
|
|
30
|
+
|
|
31
|
+
**Corrected at every site, because a partial correction is a contradiction:**
|
|
32
|
+
`app/views/style/_modals.html.erb`, `test/dummy/config/application.rb`,
|
|
33
|
+
`Gemfile`, `test/views/style_web3_specimens_test.rb`,
|
|
34
|
+
`test/views/style_host_section_test.rb` (both sites),
|
|
35
|
+
`app/views/layouts/_navbar.html.erb`,
|
|
36
|
+
`app/assets/tailwind/studio_engine/engine.css`,
|
|
37
|
+
`test/views/nav_collapse_contract_test.rb`, `test/lib/vendored_alpine_test.rb`,
|
|
38
|
+
`test/lib/vendored_montserrat_test.rb`, and two earlier Unreleased entries
|
|
39
|
+
below. The navbar-fork claim rides the same denominator — three of FIVE fork
|
|
40
|
+
it, not three of six, and the named three are unchanged — and the
|
|
41
|
+
Sprockets/propshaft split is two of five rather than "exactly half the fleet",
|
|
42
|
+
a phrase that encoded an even total no census supports.
|
|
43
|
+
|
|
44
|
+
**What was already right, and stays.** The five-consumer roster in
|
|
45
|
+
`lib/studio.rb`'s `draw_profile_routes` note (each checked 2026-08-14) named
|
|
46
|
+
the correct five all along; this change brings the rest of the repo to it. The
|
|
47
|
+
"all six consumer DATABASES" figures elsewhere in this file are three apps
|
|
48
|
+
times two environments, not an app count.
|
|
49
|
+
|
|
50
|
+
Comment and prose only. No behaviour changes.
|
|
51
|
+
|
|
7
52
|
### Added
|
|
8
53
|
|
|
9
54
|
- **The first-name card now says WHICH path finished it, and can type its
|
|
@@ -80,7 +125,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
80
125
|
that ships no such partial gets **nothing** — no pill, no heading, no
|
|
81
126
|
container — and that is asserted as a whole-document comparison rather than a
|
|
82
127
|
handful of refutes: the page an app WITH a section gets, minus that section
|
|
83
|
-
and its pill, must equal the page a base app gets.
|
|
128
|
+
and its pill, must equal the page a base app gets. Four of the five apps
|
|
84
129
|
mounting this engine will ship no host section, and their page is unchanged.
|
|
85
130
|
|
|
86
131
|
**A host specimen drives `$store.modals`, not `dsModals`, and the distinction
|
|
@@ -272,6 +317,57 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
272
317
|
|
|
273
318
|
### Fixed
|
|
274
319
|
|
|
320
|
+
- **A host's apostrophe no longer kills the card it was passed to — now across the
|
|
321
|
+
blocks, not just the first-name step.** The same defect
|
|
322
|
+
`onboarding/_first_name` fixed one release ago turns out to be an engine-wide
|
|
323
|
+
idiom: a host-supplied local interpolated into a JS string literal inside a
|
|
324
|
+
JS-evaluating attribute. Repaired at eleven more splices.
|
|
325
|
+
|
|
326
|
+
**WHY IT IS WORTH A CHANGELOG LINE WHEN NOTHING VISIBLE CHANGES.** The failure
|
|
327
|
+
mode is silent. A bare apostrophe closes the JS literal, the whole expression
|
|
328
|
+
becomes a SyntaxError, and Alpine mounts the component as a NO-OP that still
|
|
329
|
+
renders every element — a card that looks perfect and whose buttons do nothing.
|
|
330
|
+
There is no error on screen, nothing in the server log, and no markup assertion
|
|
331
|
+
that can see it. Every value in every consumer resolves to a source literal or a
|
|
332
|
+
frozen constant today, so this is latent cover rather than a live fix; it is
|
|
333
|
+
worth doing because the next local to carry prose will look like an ordinary
|
|
334
|
+
change to whoever writes it.
|
|
335
|
+
|
|
336
|
+
**WHAT MOVED.** `blocks/_success_card` (`cta_event` at both CTA branches,
|
|
337
|
+
`secondary_event`), `blocks/_error_card` (`cta_event`, `secondary_event`),
|
|
338
|
+
`blocks/_entry_confirmed` and `blocks/_solana_tx_link` (`cluster_param`),
|
|
339
|
+
`modals/_crop_photo` (`store`), `studio/emails/show` (the two upload filenames
|
|
340
|
+
and the success sentence), and `profiles/_birthday_fields` (the date value).
|
|
341
|
+
No default or in-repo value contains a character either escaper touches, so
|
|
342
|
+
every shipped card renders byte-for-byte what it did.
|
|
343
|
+
|
|
344
|
+
**THE MECHANISM NOW HAS ONE HOME AND ONE GUARD.** `Studio::JsLiteral.in_attribute`
|
|
345
|
+
replaces the four inline copies in `_first_name`. TWO escapers have to run — one
|
|
346
|
+
for the JS literal, one for the HTML attribute — and the second only runs on a
|
|
347
|
+
value ERB still believes is unsafe, which is why the value is interpolated before
|
|
348
|
+
it is escaped. That subtlety was re-derived at every call site and had no test
|
|
349
|
+
anywhere; deleting it used to leave the suite green.
|
|
350
|
+
|
|
351
|
+
**NOT A FIX FOR IDENTIFIER POSITION, deliberately.** A local spliced in as a bare
|
|
352
|
+
NAME — `$store.<name>.close()` — must be VALIDATED, never escaped, because
|
|
353
|
+
`escape_javascript` also escapes `$` and mangles a legal store name. That fleet
|
|
354
|
+
(about 36 splices across 19 partials) and a third class found alongside it —
|
|
355
|
+
Ruby-ASSEMBLED JS emitted into an attribute, some of it already `html_safe` — are
|
|
356
|
+
scoped OUT of this change and carry their own tickets. The SHAPE of the splice
|
|
357
|
+
decides the repair, never the name of the local.
|
|
358
|
+
|
|
359
|
+
- **`Studio::S3` no longer mistakes a Rails NAMESPACE for a Rails application.**
|
|
360
|
+
`environment` guarded on `defined?(Rails)` and then called `Rails.env`.
|
|
361
|
+
rails-html-sanitizer — a transitive dependency of `action_view`, which arrives
|
|
362
|
+
long before any application does — ships a namespace-only `module Rails` with no
|
|
363
|
+
singleton methods, so that guard reads true and the next call raises
|
|
364
|
+
`NoMethodError: undefined method 'env' for module Rails`. It now asks
|
|
365
|
+
`Rails.respond_to?(:env)`, which is the form `lib/studio.rb` already uses in three
|
|
366
|
+
places; this was the straggler. No shipped app can reach it — every host boots a
|
|
367
|
+
real application — but the engine's own pure-Ruby unit lane can, and it did:
|
|
368
|
+
adding one `require` for a file that needs `action_view` turned an untouched
|
|
369
|
+
`email_catalog_test` red with two errors about email uploads.
|
|
370
|
+
|
|
275
371
|
- **The style guide's two "Sign Wallet" thumbnails no longer crown themselves
|
|
276
372
|
with a padlock the card stopped drawing, and the guide's lock no longer trails
|
|
277
373
|
the release that removed it.** solana-studio 0.6.1 replaced the step-up card's
|
|
@@ -606,7 +702,7 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
606
702
|
|
|
607
703
|
**The band table shipped from the wrong file.** The `.nav-shell` `--nav-*`
|
|
608
704
|
sizes were inline in `layouts/_navbar.html.erb`, so only an app rendering that
|
|
609
|
-
partial got them — and **three of
|
|
705
|
+
partial got them — and **three of five apps fork the navbar**
|
|
610
706
|
(`turf-monster`, `mcritchie-studio`, `moms-app`), each left to hand-write its
|
|
611
707
|
own. That is precisely how four independent copies of this collapse came to
|
|
612
708
|
exist. They now ship from `engine.css`, which every engine-consuming app
|
data/Gemfile
CHANGED
|
@@ -40,8 +40,9 @@ end
|
|
|
40
40
|
#
|
|
41
41
|
# DEVELOPMENT AND TEST ONLY, and deliberately NOT a gemspec runtime dependency.
|
|
42
42
|
# Declaring it there would push a Solana stack onto every BASE consumer
|
|
43
|
-
# (acquisition-studio, mcritchie-industries
|
|
44
|
-
# bundle no solana-studio
|
|
43
|
+
# (mcritchie-studio, acquisition-studio, mcritchie-industries and moms-app all
|
|
44
|
+
# mount this engine and bundle no solana-studio — four of the five consumers,
|
|
45
|
+
# the hub included), which is precisely the coupling the split removed.
|
|
45
46
|
# It is here so the DUMMY app can resolve the gem's partials and the style guide
|
|
46
47
|
# renders the real shipped cards rather than a fork of them.
|
|
47
48
|
#
|
|
@@ -415,7 +415,7 @@ body.modal-open .studio-app-banner {
|
|
|
415
415
|
/* === THE NAVBAR COLLAPSE BAND TABLE =====================================
|
|
416
416
|
LAYER 2 OF THE PRIMITIVE, and it lives here rather than in
|
|
417
417
|
layouts/_navbar.html.erb for one reason: every engine-consuming app imports
|
|
418
|
-
this stylesheet, but only some render that partial. Three of
|
|
418
|
+
this stylesheet, but only some render that partial. Three of five FORK the
|
|
419
419
|
navbar (turf-monster, mcritchie-studio, moms-app), and while the table sat
|
|
420
420
|
inline each fork had to hand-write its own — which is exactly how four
|
|
421
421
|
independent copies of this collapse came to exist.
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
@media (min-width: 400px) { .user-nav-col { width: 15rem; } .user-nav-fit { max-width: 15rem; } }
|
|
72
72
|
@media (min-width: 768px) { .user-nav-col { width: 20rem; } .user-nav-fit { max-width: 20rem; } }
|
|
73
73
|
/* The COLLAPSE band table used to live here, which meant only an app
|
|
74
|
-
rendering THIS partial got it. Three of
|
|
74
|
+
rendering THIS partial got it. Three of five apps fork the navbar, so each
|
|
75
75
|
was left to hand-write its own — the way four independent copies happened
|
|
76
76
|
in the first place. It ships from engine.css now (every consuming app
|
|
77
77
|
imports it), so a forking app OPTS IN with `nav-shell` +
|
|
@@ -137,6 +137,68 @@
|
|
|
137
137
|
dbg(' ' + key, truncate(val, 40));
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
// === THE REDIRECT TRANSPORT'S RETURN LEG ==============================
|
|
141
|
+
//
|
|
142
|
+
// TRIED FIRST, AND EVERYTHING BELOW IS LEFT EXACTLY AS IT WAS. The legacy
|
|
143
|
+
// block that follows handles ONE step — Phantom's signIn — with keys named
|
|
144
|
+
// phantom_dl_*, and it is what production mobile sign-in runs on TODAY. It is
|
|
145
|
+
// not touched, moved, or reworded here: a consumer that has not yet adopted
|
|
146
|
+
// the new transport must behave identically after this change, and the only
|
|
147
|
+
// honest way to promise that is to leave its code alone.
|
|
148
|
+
//
|
|
149
|
+
// GUARDED THE WAY THIS CODEBASE GUARDS EVERY OPTIONAL CAPABILITY: a host that
|
|
150
|
+
// does not load solana_studio/wallet_ops.js + wallet_journal.js has no
|
|
151
|
+
// SolanaStudio.walletOps, so this branch never runs and the legacy path is
|
|
152
|
+
// the whole program. An absent capability must not default to the permissive
|
|
153
|
+
// branch — the same rule the wallet picker's canDeepLink learned.
|
|
154
|
+
//
|
|
155
|
+
// WHY THE PENDING-JOURNAL CHECK IS PART OF THE GUARD, not just the globals:
|
|
156
|
+
// a host may load the new scripts while a signIn started under the OLD keys
|
|
157
|
+
// is still in flight — a user mid-round-trip across a deploy. Requiring an
|
|
158
|
+
// actual wallet_dl journal means that user finishes on the path they started,
|
|
159
|
+
// rather than being handed to a resumer with nothing to resume.
|
|
160
|
+
//
|
|
161
|
+
// WHAT THIS BRANCH DOES NOT DO: decrypt, verify, or know what the user was
|
|
162
|
+
// trying to accomplish. walletOps owns the step machine and the registered
|
|
163
|
+
// intent owns the outcome. This is a seam, and keeping it a seam is what lets
|
|
164
|
+
// the same callback serve sign-in and a contest entry without learning either.
|
|
165
|
+
var studio = window.SolanaStudio;
|
|
166
|
+
var hasResumer = !!(studio && studio.walletOps && typeof studio.walletOps.resume === 'function' &&
|
|
167
|
+
studio.walletJournal && typeof studio.walletJournal.peek === 'function');
|
|
168
|
+
|
|
169
|
+
if (hasResumer && studio.walletJournal.peek()) {
|
|
170
|
+
dbg('Transport', 'wallet_dl journal present — resuming through walletOps');
|
|
171
|
+
|
|
172
|
+
studio.walletOps.resume(params, {
|
|
173
|
+
navigate: function(url) { window.location.href = url; }
|
|
174
|
+
}).then(function(result) {
|
|
175
|
+
// SUSPENDED means resume already navigated to the next hop; this document
|
|
176
|
+
// is on its way out and must not also redirect, or the two race.
|
|
177
|
+
if (result && result.suspended) {
|
|
178
|
+
dbg('Transport', 'advanced to the next hop');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (result && result.done) {
|
|
182
|
+
dbg('Transport', 'intent complete');
|
|
183
|
+
// The registered intent owns where to land — it knows what the user was
|
|
184
|
+
// doing. Falling back to '/' rather than guessing a wallet-specific
|
|
185
|
+
// destination, which this file has no business knowing.
|
|
186
|
+
window.location.href = (result.value && result.value.redirect) || '/';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// No pending request is a STATE, not a failure: a stale bookmark, a second
|
|
190
|
+
// tab, a reload after the journal was consumed. peek() said otherwise a
|
|
191
|
+
// moment ago, so this is a race, and the honest answer is to start over.
|
|
192
|
+
showError('No pending wallet request. Redirecting to login...');
|
|
193
|
+
}, function(err) {
|
|
194
|
+
// A user rejection arrives here with .rejected set — the wallet's own
|
|
195
|
+
// words, already mapped. Anything else is a genuine fault.
|
|
196
|
+
dbg('Transport ERROR', (err && err.message) || String(err));
|
|
197
|
+
showError((err && err.message) || 'Wallet request failed');
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
140
202
|
// Check for Phantom error
|
|
141
203
|
if (params.get('errorCode')) {
|
|
142
204
|
dbg('ERROR', 'errorCode=' + params.get('errorCode'));
|
|
@@ -197,6 +197,17 @@
|
|
|
197
197
|
|
|
198
198
|
<%# ROW TWO — the pictures, and the words that ride on them. %>
|
|
199
199
|
<div class="grid gap-6 md:grid-cols-2 mb-6">
|
|
200
|
+
<%# THE FILENAMES AND THE LABEL ARE ESCAPED ON THE WAY INTO THIS x-data. All
|
|
201
|
+
three come off the email REGISTRY entry rather than a form, so nothing
|
|
202
|
+
reaches them with a quote today — but they are the only values in the
|
|
203
|
+
attribute that are not authored right here, and bare interpolation of one
|
|
204
|
+
would close its JS literal and mount the whole upload host as a silent
|
|
205
|
+
no-op: an image picker that opens, crops, and never saves.
|
|
206
|
+
|
|
207
|
+
successMessage previously used the BARE `j` form. That is correct only
|
|
208
|
+
while the label is a plain String — `j` preserves an html_safe marking, and
|
|
209
|
+
a safe value would skip ERB's attribute-escaping half and let a raw double
|
|
210
|
+
quote out of the attribute. Studio::JsLiteral removes that condition. %>
|
|
200
211
|
<section class="card p-4"
|
|
201
212
|
<% if @uploads_available %>
|
|
202
213
|
x-data="imageUploadHost({
|
|
@@ -205,10 +216,10 @@
|
|
|
205
216
|
maxWidth: <%= max_width %>,
|
|
206
217
|
transparent: false,
|
|
207
218
|
allowGifs: true,
|
|
208
|
-
filename: '<%= @entry.key %>.png',
|
|
219
|
+
filename: '<%= Studio::JsLiteral.in_attribute(@entry.key) %>.png',
|
|
209
220
|
saving: 'Saving banner…',
|
|
210
221
|
success: 'Banner updated',
|
|
211
|
-
successMessage: '<%=
|
|
222
|
+
successMessage: '<%= Studio::JsLiteral.in_attribute(@entry.label) %> now uses this app\'s own image.',
|
|
212
223
|
failure: 'Couldn\'t save the banner'
|
|
213
224
|
})"
|
|
214
225
|
@crop-photo-confirmed.window="onCropConfirmed($event.detail)"
|
|
@@ -513,7 +524,7 @@
|
|
|
513
524
|
maxWidth: 600,
|
|
514
525
|
transparent: true,
|
|
515
526
|
allowGifs: false,
|
|
516
|
-
filename: '<%= @entry.key %>-logo.png',
|
|
527
|
+
filename: '<%= Studio::JsLiteral.in_attribute(@entry.key) %>-logo.png',
|
|
517
528
|
saving: 'Saving logo…',
|
|
518
529
|
success: 'Logo updated',
|
|
519
530
|
successMessage: 'This email now uses its own logo.',
|
|
@@ -26,7 +26,13 @@
|
|
|
26
26
|
The living style guide passes store: "dsModals" for its page-scoped host.
|
|
27
27
|
%>
|
|
28
28
|
<% crop_store = local_assigns.fetch(:store, "modals") %>
|
|
29
|
-
|
|
29
|
+
<%# STRING position, so it is ESCAPED rather than validated: the store name is
|
|
30
|
+
handed to cropPhotoModal as a JS string argument, not spliced in as a bare
|
|
31
|
+
identifier. An apostrophe would close that literal and mount the modal as a
|
|
32
|
+
silent no-op that still renders every element below. Studio::JsLiteral carries
|
|
33
|
+
why both escapers have to run. %>
|
|
34
|
+
<% crop_store_js = Studio::JsLiteral.in_attribute(crop_store) %>
|
|
35
|
+
<div x-data="cropPhotoModal({ store: '<%= crop_store_js %>' })">
|
|
30
36
|
<h3 class="text-heading font-bold text-lg text-center mb-4">Crop Photo</h3>
|
|
31
37
|
|
|
32
38
|
<template x-if="error">
|
|
@@ -71,6 +71,12 @@
|
|
|
71
71
|
subtitle_key = local_assigns[:subtitle_key]
|
|
72
72
|
tx_signature_key = local_assigns.fetch(:tx_signature_key, "props.txSignature")
|
|
73
73
|
cluster_param = local_assigns.fetch(:cluster_param, "")
|
|
74
|
+
# STRING position — clusterParam is a JS string on the root scope that
|
|
75
|
+
# _success_card's tx-link concatenates into a URL. An apostrophe closes the
|
|
76
|
+
# literal and the whole x-data becomes a SyntaxError, which mounts this card and
|
|
77
|
+
# every nested block as a silent no-op. props_expr one line up is the OPPOSITE
|
|
78
|
+
# shape (an Alpine expression by contract) and is deliberately left alone.
|
|
79
|
+
cluster_param_js = Studio::JsLiteral.in_attribute(cluster_param)
|
|
74
80
|
lobby_url_key = local_assigns.fetch(:lobby_url_key, "props.lobbyUrl")
|
|
75
81
|
cta_label = local_assigns.fetch(:cta_label, "Continue")
|
|
76
82
|
seeds_earned_key = local_assigns.fetch(:seeds_earned_key, "props.seedsEarned")
|
|
@@ -113,7 +119,7 @@
|
|
|
113
119
|
<%# props getter + clusterParam live on the root scope so _success_card's nested
|
|
114
120
|
Alpine expressions (props.txSignature, props.lobbyUrl, clusterParam) resolve up
|
|
115
121
|
the scope chain. clusterParam feeds _success_card's branded tx-link URL. %>
|
|
116
|
-
<div x-data="{ get props() { return <%= props_expr %>; }, clusterParam: '<%=
|
|
122
|
+
<div x-data="{ get props() { return <%= props_expr %>; }, clusterParam: '<%= cluster_param_js %>' }">
|
|
117
123
|
<%= render "studio/modals/blocks/success_card", sc_locals do %>
|
|
118
124
|
<%# The app's own enrichment, above the seeds bar. turf puts its kickoff
|
|
119
125
|
countdown here — "when does it start" reads before the celebratory seeds
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
<%
|
|
25
25
|
icon_emoji = local_assigns[:icon_emoji] || "⏳" # ⏳
|
|
26
26
|
cta_label = local_assigns[:cta_label] || 'Refresh'
|
|
27
|
+
# THE TWO EVENT NAMES ARE THE ONLY LOCALS HERE THAT LAND INSIDE JS. Both sit in a
|
|
28
|
+
# single-quoted literal inside a double-quoted @click, so an apostrophe in either
|
|
29
|
+
# closes the literal, the handler becomes a SyntaxError, and the button renders
|
|
30
|
+
# perfectly and does NOTHING when clicked — no error, no log, nothing a markup
|
|
31
|
+
# assertion can see. Escaped, not validated: an event name is a STRING to
|
|
32
|
+
# $dispatch, and any character is legal in one. Studio::JsLiteral says why the
|
|
33
|
+
# interpolated form is load-bearing.
|
|
34
|
+
cta_event_js = Studio::JsLiteral.in_attribute(local_assigns[:cta_event])
|
|
35
|
+
secondary_event_js = Studio::JsLiteral.in_attribute(local_assigns[:secondary_event])
|
|
27
36
|
%>
|
|
28
37
|
<%# role="alert" on the CARD, not on its message: the message is server-rendered
|
|
29
38
|
static text, so it never "changes" for a live region to notice. The modal host
|
|
@@ -42,11 +51,11 @@
|
|
|
42
51
|
<% if local_assigns[:cta_reload] %>
|
|
43
52
|
<button @click="window.location.reload()" class="btn btn-outline btn-sm"><%= cta_label %></button>
|
|
44
53
|
<% elsif local_assigns[:cta_event] %>
|
|
45
|
-
<button @click="$dispatch('<%=
|
|
54
|
+
<button @click="$dispatch('<%= cta_event_js %>')" class="btn btn-outline btn-sm"><%= cta_label %></button>
|
|
46
55
|
<% end %>
|
|
47
56
|
|
|
48
57
|
<% if local_assigns[:secondary_label] && local_assigns[:secondary_event] %>
|
|
49
|
-
<button @click="$dispatch('<%=
|
|
58
|
+
<button @click="$dispatch('<%= secondary_event_js %>')"
|
|
50
59
|
class="block mx-auto mt-3 text-xs text-secondary hover:text-heading underline underline-offset-2">
|
|
51
60
|
<%= secondary_label %>
|
|
52
61
|
</button>
|
|
@@ -17,10 +17,15 @@
|
|
|
17
17
|
<%
|
|
18
18
|
tx_signature_key = local_assigns.fetch(:tx_signature_key)
|
|
19
19
|
cluster_param = local_assigns.fetch(:cluster_param, "")
|
|
20
|
+
# STRING position, inside the :href expression below. tx_signature_key beside it
|
|
21
|
+
# is an Alpine EXPRESSION by contract and is spliced in bare; this one is a
|
|
22
|
+
# literal suffix, so an apostrophe would close it and turn the whole :href
|
|
23
|
+
# binding into a SyntaxError — the link then renders with no href at all.
|
|
24
|
+
cluster_param_js = Studio::JsLiteral.in_attribute(cluster_param)
|
|
20
25
|
%>
|
|
21
26
|
<template x-if="(<%= tx_signature_key %>)">
|
|
22
27
|
<div class="text-center mb-3 -mt-1">
|
|
23
|
-
<a :href="'https://explorer.solana.com/tx/' + (<%= tx_signature_key %>) + '<%=
|
|
28
|
+
<a :href="'https://explorer.solana.com/tx/' + (<%= tx_signature_key %>) + '<%= cluster_param_js %>'"
|
|
24
29
|
target="_blank" rel="noopener"
|
|
25
30
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border border-subtle hover:border-primary/50 transition group no-underline"
|
|
26
31
|
style="background: rgb(var(--color-primary-500-rgb) / 0.06);">
|
|
@@ -106,6 +106,19 @@
|
|
|
106
106
|
}
|
|
107
107
|
}".gsub(/\s+/, ' ').html_safe
|
|
108
108
|
|
|
109
|
+
# THE TWO EVENT NAMES, ESCAPED FOR THE @click THEY ARE SPLICED INTO. Each sits in
|
|
110
|
+
# a JS single-quoted literal inside a double-quoted attribute, so an apostrophe in
|
|
111
|
+
# either closes the literal and the handler becomes a SyntaxError — a CTA that
|
|
112
|
+
# renders exactly right and does nothing when clicked. This is the engine's
|
|
113
|
+
# most-rendered block, so it is also the widest reach that failure has.
|
|
114
|
+
#
|
|
115
|
+
# ESCAPED, NOT VALIDATED: an event name is a STRING argument to $dispatch and any
|
|
116
|
+
# character is legal in one. Contrast cta_href_key / props_expr below, which are
|
|
117
|
+
# Alpine EXPRESSIONS by contract and must not be touched. Studio::JsLiteral
|
|
118
|
+
# carries why the interpolated form is load-bearing.
|
|
119
|
+
cta_event_js = Studio::JsLiteral.in_attribute(local_assigns[:cta_event])
|
|
120
|
+
secondary_event_js = Studio::JsLiteral.in_attribute(local_assigns[:secondary_event])
|
|
121
|
+
|
|
109
122
|
init_calls = []
|
|
110
123
|
init_calls << "fireConfetti()" if fire_confetti
|
|
111
124
|
init_calls << "startCountdown(#{local_assigns[:auto_redirect_url_key]})" if has_redirect
|
|
@@ -229,7 +242,7 @@
|
|
|
229
242
|
<span class="relative z-10"><%= cta_label %></span>
|
|
230
243
|
</a>
|
|
231
244
|
<% elsif use_drain && local_assigns[:cta_event] %>
|
|
232
|
-
<button @click="$dispatch('<%=
|
|
245
|
+
<button @click="$dispatch('<%= cta_event_js %>')"
|
|
233
246
|
class="btn btn-primary btn-lg w-full relative overflow-hidden no-underline">
|
|
234
247
|
<div class="absolute inset-0 pointer-events-none origin-left"
|
|
235
248
|
style="background: rgba(255,255,255,0.18);"
|
|
@@ -246,12 +259,12 @@
|
|
|
246
259
|
<% elsif local_assigns[:cta_href_key] %>
|
|
247
260
|
<a :href="<%= cta_href_key %>" class="btn btn-primary btn-lg w-full"><%= cta_label %></a>
|
|
248
261
|
<% elsif local_assigns[:cta_event] %>
|
|
249
|
-
<button @click="$dispatch('<%=
|
|
262
|
+
<button @click="$dispatch('<%= cta_event_js %>')" class="btn btn-primary btn-lg w-full"><%= cta_label %></button>
|
|
250
263
|
<% end %>
|
|
251
264
|
<% end %>
|
|
252
265
|
|
|
253
266
|
<% if local_assigns[:secondary_label] && local_assigns[:secondary_event] %>
|
|
254
|
-
<button @click="$dispatch('<%=
|
|
267
|
+
<button @click="$dispatch('<%= secondary_event_js %>')"
|
|
255
268
|
class="block mx-auto mt-3 text-xs text-secondary hover:text-heading underline underline-offset-2">
|
|
256
269
|
<%= secondary_label %>
|
|
257
270
|
</button>
|
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
STRING position — empty_error, submit_path, skip_path, done_event — each sits
|
|
95
95
|
inside a JS SINGLE-quoted literal. A bare apostrophe closes the literal, the
|
|
96
96
|
expression becomes a SyntaxError, and the card is that silent no-op. Repair:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
Studio::JsLiteral.in_attribute, which is where the mechanism now lives and is
|
|
98
|
+
tested. Two escapers have to run — one for the JS literal, one for the
|
|
99
|
+
attribute — and the second only runs on a value ERB still believes is unsafe.
|
|
100
|
+
This card used to spell that out inline at four sites; the engine has eleven
|
|
101
|
+
more of them, so it is one method with one guard now.
|
|
102
102
|
|
|
103
103
|
IDENTIFIER position — modal_store — is spliced in as a bare NAME, at three
|
|
104
104
|
sites: the props getter, finish's close, and the Ruby-built dismiss_action
|
|
@@ -145,15 +145,15 @@
|
|
|
145
145
|
# apostrophes. Interpolated raw, an error reading "We'll need a name" would
|
|
146
146
|
# close the JS single-quoted string, make the whole expression a SyntaxError,
|
|
147
147
|
# and mount the component as a SILENT NO-OP that still renders every element
|
|
148
|
-
# below.
|
|
149
|
-
# backslash and the newline
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
148
|
+
# below. Studio::JsLiteral.in_attribute covers the apostrophe, the double quote,
|
|
149
|
+
# the backslash and the newline, AND hands ERB a value it still believes is
|
|
150
|
+
# unsafe so the attribute's own escaping runs too — a safe string would skip that
|
|
151
|
+
# half and could smuggle a raw double quote in, the exact failure the CRITICAL
|
|
152
|
+
# note above describes.
|
|
153
153
|
#
|
|
154
154
|
# INERT ON BOTH DEFAULTS. Neither default string contains a character either
|
|
155
155
|
# escaper touches, so the shipped card is byte-for-byte what it was.
|
|
156
|
-
empty_error_js =
|
|
156
|
+
empty_error_js = Studio::JsLiteral.in_attribute(empty_error)
|
|
157
157
|
placeholder = local_assigns.fetch(:placeholder, "Alex")
|
|
158
158
|
# OFF unless a host passes a non-empty array. Normalised to nil so that an
|
|
159
159
|
# empty list behaves exactly like an absent local rather than emitting the
|
|
@@ -201,11 +201,11 @@
|
|
|
201
201
|
# Alpine mounts the component as a SILENT NO-OP that still renders every element
|
|
202
202
|
# — perfect-looking markup, dead card.
|
|
203
203
|
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
204
|
+
# WHY A METHOD RATHER THAN escape_javascript AT EACH SITE: the second escaper is
|
|
205
|
+
# conditional. ERB skips a value that answers true to html_safe, and
|
|
206
|
+
# escape_javascript preserves its argument's marking, so the naive call silently
|
|
207
|
+
# loses the attribute half. Studio::JsLiteral.in_attribute owns that, and owns the
|
|
208
|
+
# single test that fails when it is removed.
|
|
209
209
|
#
|
|
210
210
|
# INERT ON EVERY DEFAULT. Two paths, an event name and a store name carry no
|
|
211
211
|
# character either escaper touches, so the shipped card is byte-for-byte what it
|
|
@@ -213,9 +213,9 @@
|
|
|
213
213
|
# host supplies these as prose today, but empty_error established that prose does
|
|
214
214
|
# belong in this attribute, and the next local to carry an apostrophe will look
|
|
215
215
|
# like an ordinary change to whoever writes it.
|
|
216
|
-
submit_path_js =
|
|
217
|
-
skip_path_js =
|
|
218
|
-
done_event_js =
|
|
216
|
+
submit_path_js = Studio::JsLiteral.in_attribute(submit_path)
|
|
217
|
+
skip_path_js = Studio::JsLiteral.in_attribute(skip_path)
|
|
218
|
+
done_event_js = Studio::JsLiteral.in_attribute(done_event)
|
|
219
219
|
field_id = local_assigns.fetch(:id, "onboarding-first-name")
|
|
220
220
|
# The × mirrors the skip affordance it sits beside: it SKIPS while the step is
|
|
221
221
|
# skippable, and merely CLOSES once it is required. Resolved here rather than in
|
|
@@ -37,9 +37,16 @@
|
|
|
37
37
|
m = user.birth_month
|
|
38
38
|
d = user.birth_day
|
|
39
39
|
value = (y.present? && m.present? && d.present?) ? format("%04d-%02d-%02d", y, m, d) : nil
|
|
40
|
+
# ESCAPED FOR THE x-data BELOW. format() cannot produce a quote today, so this is
|
|
41
|
+
# latent cover rather than a live fix — but the local is read off a USER record,
|
|
42
|
+
# and the guard has to sit on the splice rather than on today's formatter. Bare,
|
|
43
|
+
# an apostrophe would close the JS literal and studioBirthdayFields would never
|
|
44
|
+
# evaluate, leaving three selects that render and never save. The <input value="">
|
|
45
|
+
# copy below needs no such treatment: ERB already escapes it, and it is not JS.
|
|
46
|
+
value_js = Studio::JsLiteral.in_attribute(value)
|
|
40
47
|
%>
|
|
41
48
|
|
|
42
|
-
<div x-data="studioBirthdayFields('<%=
|
|
49
|
+
<div x-data="studioBirthdayFields('<%= value_js %>')">
|
|
43
50
|
<%# --- the JS-less path ------------------------------------------------- %>
|
|
44
51
|
<template x-if="!alpine">
|
|
45
52
|
<input type="date" name="profile[birthday]" id="profile_birthday"
|
|
@@ -43,13 +43,30 @@
|
|
|
43
43
|
# /auth/solana routes, Solana::SessionAuth, the phantom callback) and ships no
|
|
44
44
|
# wallet UI at all.
|
|
45
45
|
#
|
|
46
|
-
# So resolution is a RUNTIME question here, not a build-time one.
|
|
47
|
-
#
|
|
48
|
-
# moms-app) bundle no solana-studio
|
|
49
|
-
#
|
|
46
|
+
# So resolution is a RUNTIME question here, not a build-time one. FOUR of the
|
|
47
|
+
# five apps mounting this engine (mcritchie-studio, acquisition-studio,
|
|
48
|
+
# mcritchie-industries, moms-app) bundle no solana-studio — turf-monster is
|
|
49
|
+
# the only one that does — and an unconditional render would turn their
|
|
50
|
+
# /admin/style into a missing-template 500. The gem is a development and
|
|
50
51
|
# test dependency of this engine precisely so the guide can show the REAL
|
|
51
52
|
# cards; where it is absent the specimens stay listed but unopenable.
|
|
52
53
|
#
|
|
54
|
+
# THE EXCEPTION IS THE DURABLE HALF; the count rots the day an app is born.
|
|
55
|
+
# Derived 2026-09-07 — and the CRITERION is recorded, not just the count,
|
|
56
|
+
# because the obvious grep does not reproduce it. The five are the checkouts
|
|
57
|
+
# whose own config/routes.rb calls `Studio.routes(self)`, the same five
|
|
58
|
+
# lib/studio.rb's draw_profile_routes note names. Grepping Gemfiles for
|
|
59
|
+
# `gem "studio-engine"` returns SEVEN: it also catches solana-studio, which
|
|
60
|
+
# pins the engine in `group :development, :test` for its dummy and draws no
|
|
61
|
+
# routes, and mcritchie-studio-ai-builder-cache, which is the HUB checked out
|
|
62
|
+
# twice (same Heroku remote; HEAD c7d5d326 is a commit inside
|
|
63
|
+
# mcritchie-studio) and would double-count it. This line used to read "three
|
|
64
|
+
# of the six" and to omit the HUB — the app a reader is likeliest to be
|
|
65
|
+
# sitting in, and one that bundles no solana-studio. Two further near misses:
|
|
66
|
+
# chain-ops bundles solana-studio WITHOUT this engine, so it is not in this
|
|
67
|
+
# set at all; acquisition-studio is a retired prototype whose Gemfile still
|
|
68
|
+
# pins the engine, so it is.
|
|
69
|
+
#
|
|
53
70
|
# Same three-term lookup_context.exists? the modal host uses for host_extras
|
|
54
71
|
# (studio/modals/_host.html.erb): name, prefixes, partial.
|
|
55
72
|
web3_gem = lookup_context.exists?("wallet_connect", ["solana_studio/modals"], true)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "action_view"
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# ONE home for the repair that keeps a host-supplied value from bricking an
|
|
5
|
+
# Alpine component when it is spliced into a JS-evaluating HTML attribute.
|
|
6
|
+
#
|
|
7
|
+
# THE FAILURE THIS EXISTS FOR is silent, which is the whole reason it is worth a
|
|
8
|
+
# module instead of a convention. A local sits inside a JS single-quoted literal
|
|
9
|
+
# in a double-quoted attribute:
|
|
10
|
+
#
|
|
11
|
+
# <button @click="$dispatch('<%= cta_event %>')">
|
|
12
|
+
#
|
|
13
|
+
# A bare apostrophe in `cta_event` closes that literal, the expression becomes a
|
|
14
|
+
# SyntaxError, and Alpine mounts the component as a NO-OP that still renders
|
|
15
|
+
# every element. Perfect-looking markup, dead card. Nothing raises, nothing logs
|
|
16
|
+
# a server-side warning, and every string assertion about the response bytes
|
|
17
|
+
# still passes — which is how this class of defect survives a review.
|
|
18
|
+
#
|
|
19
|
+
# BOTH ESCAPERS HAVE TO RUN, and that is the part a hand-written call gets wrong.
|
|
20
|
+
# There are two nested contexts, so there are two ways out of the attribute:
|
|
21
|
+
#
|
|
22
|
+
# * out of the JS STRING, with `'` — repaired by escape_javascript
|
|
23
|
+
# * out of the HTML ATTRIBUTE, with `"` — repaired by ERB's own escaping
|
|
24
|
+
#
|
|
25
|
+
# escape_javascript covers the first. ERB covers the second, but ONLY for a value
|
|
26
|
+
# it believes is unsafe, and `escape_javascript(SafeBuffer)` answers true to
|
|
27
|
+
# html_safe?. Hand an html_safe string straight to escape_javascript and ERB
|
|
28
|
+
# steps aside, a raw double quote reaches the attribute, and the attribute closes
|
|
29
|
+
# early — the same dead card by a longer route.
|
|
30
|
+
#
|
|
31
|
+
# So the interpolation wrapper below is LOAD-BEARING, not a style tic: `"#{value}"`
|
|
32
|
+
# produces a plain String, escape_javascript therefore returns a plain String, and
|
|
33
|
+
# ERB does its half on the way into the attribute. That subtlety was re-derived at
|
|
34
|
+
# every call site before this module existed, and it had no test cover anywhere;
|
|
35
|
+
# it now has exactly one implementation and one guard
|
|
36
|
+
# (test/lib/studio/js_literal_test.rb).
|
|
37
|
+
#
|
|
38
|
+
# DELIBERATELY NARROW — there is no script-body variant here, and adding one is
|
|
39
|
+
# not a copy of this method. Inside <script>…</script> the HTML parser does no
|
|
40
|
+
# entity decoding, so ERB's escaping is not a second layer of safety there, it is
|
|
41
|
+
# CORRUPTION: `\"` would arrive as `\"` and break the JS this method exists
|
|
42
|
+
# to protect. A script body wants JS escaping ONLY (escape_javascript, marked
|
|
43
|
+
# safe). The name says `in_attribute` so that the next person reaching for this in
|
|
44
|
+
# a <script> has to stop and notice the difference.
|
|
45
|
+
#
|
|
46
|
+
# AND IT IS THE WRONG REPAIR FOR IDENTIFIER POSITION. A local spliced in as a bare
|
|
47
|
+
# NAME — `$store.<%= modal_store %>.close()` — must be VALIDATED, never escaped:
|
|
48
|
+
# escape_javascript also escapes `$`, so a legitimate store name like `dsModals$2`
|
|
49
|
+
# comes back mangled and the card dies anyway. The SHAPE of the splice decides the
|
|
50
|
+
# repair, never the name of the local. studio/modals/onboarding/_first_name
|
|
51
|
+
# carries the worked example of both.
|
|
52
|
+
module JsLiteral
|
|
53
|
+
extend ActionView::Helpers::JavaScriptHelper
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# Escape +value+ for a JS string literal that lives inside an HTML attribute.
|
|
58
|
+
#
|
|
59
|
+
# Returns a plain (NOT html_safe) String on purpose — see the note above. The
|
|
60
|
+
# caller interpolates it inside the JS quotes, exactly as it would have
|
|
61
|
+
# interpolated the raw local:
|
|
62
|
+
#
|
|
63
|
+
# cta_event_js = Studio::JsLiteral.in_attribute(cta_event)
|
|
64
|
+
# # …
|
|
65
|
+
# <button @click="$dispatch('<%= cta_event_js %>')">
|
|
66
|
+
#
|
|
67
|
+
# nil becomes "" rather than "nil", because every current caller reaches this
|
|
68
|
+
# with an optional local and an empty JS string is the honest rendering of an
|
|
69
|
+
# absent value.
|
|
70
|
+
# ONE MECHANISM, DELIBERATELY. An earlier draft also re-stripped the marking on
|
|
71
|
+
# the way out, as belt-and-braces. That made both halves redundant, and a
|
|
72
|
+
# redundant guard is one that survives being deleted: removing the interpolation
|
|
73
|
+
# below left every test green, because the second stripper covered for it. There
|
|
74
|
+
# is one way this works now, and test/lib/studio/js_literal_test.rb fails when it
|
|
75
|
+
# is removed.
|
|
76
|
+
def in_attribute(value)
|
|
77
|
+
JsLiteral.escape_javascript("#{value}")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/studio/s3.rb
CHANGED
|
@@ -123,7 +123,16 @@ module Studio
|
|
|
123
123
|
def environment
|
|
124
124
|
return "dev" if EnvironmentBanner.qa_environment?
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
# ASKS FOR THE METHOD, NOT THE CONSTANT, and the difference is not
|
|
127
|
+
# theoretical. rails-html-sanitizer — which arrives with action_view, long
|
|
128
|
+
# before any Rails APPLICATION does — defines a namespace-only `module
|
|
129
|
+
# Rails` with no singleton methods on it. Against that, a bare
|
|
130
|
+
# `defined?(Rails)` reads TRUE and the very next call dies with
|
|
131
|
+
# NoMethodError: undefined method `env` for module Rails. lib/studio.rb
|
|
132
|
+
# already spells the guard this way in three places; this was the
|
|
133
|
+
# straggler, and it turned an unrelated green suite red the first time a
|
|
134
|
+
# unit test pulled action_view in.
|
|
135
|
+
defined?(Rails) && Rails.respond_to?(:env) && Rails.env&.production? ? "production" : "dev"
|
|
127
136
|
end
|
|
128
137
|
end
|
|
129
138
|
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "studio/color_scale"
|
|
|
8
8
|
require "studio/environment_banner"
|
|
9
9
|
require "studio/theme_resolver"
|
|
10
10
|
require "studio/ui_primitives"
|
|
11
|
+
require "studio/js_literal"
|
|
11
12
|
require "studio/sidebar_sections"
|
|
12
13
|
require "studio/profile_sections"
|
|
13
14
|
require "studio/profile_image"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.74.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -573,6 +573,7 @@ files:
|
|
|
573
573
|
- lib/studio/geo/lookup.rb
|
|
574
574
|
- lib/studio/image_cache.rb
|
|
575
575
|
- lib/studio/ip_locations.rb
|
|
576
|
+
- lib/studio/js_literal.rb
|
|
576
577
|
- lib/studio/link_resolution.rb
|
|
577
578
|
- lib/studio/link_token.rb
|
|
578
579
|
- lib/studio/log_rotation.rb
|