studio-engine 0.66.1 → 0.67.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.
@@ -1,37 +0,0 @@
1
- <%#
2
- tweetnacl, for the Phantom mobile deep link. Both halves of that flow need
3
- window.nacl: the deep link generates an x25519 keypair so Phantom can encrypt
4
- its reply, and the callback opens that box with the stashed secret.
5
-
6
- IDEMPOTENT ON PURPOSE. turf-monster already loads tweetnacl from its own
7
- layout, so this must not load a second copy there; the guard means a consumer
8
- that already supplies nacl keeps its own and one that does not (the hub, which
9
- had no mobile wallet path at all) gets it here.
10
-
11
- Pinned to an exact version rather than a range: this is signing-path crypto,
12
- and a range lets a CDN swap it under a running app. The same reasoning is
13
- written out beside turf-monster's web3.js pin.
14
-
15
- Render before studio/solana/phantom_deeplink and in the callback view.
16
- %>
17
- <script>
18
- // NOT document.write. Under turbo-rails a Drive visit re-executes body scripts
19
- // at readyState === 'complete', where document.write implicitly calls
20
- // document.open() and BLANKS THE PAGE — on exactly the consumer this guard
21
- // exists for. Append a real element instead; it is also non-blocking.
22
- (function () {
23
- if (typeof window.nacl !== 'undefined') return;
24
- if (document.querySelector('script[data-studio-nacl]')) return;
25
- var s = document.createElement('script');
26
- s.src = 'https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js';
27
- // Derived with `openssl dgst -sha384 -binary | openssl base64 -A` against the
28
- // fetched file, and cross-checked against the value turf-monster already pins
29
- // for this exact URL. NEVER hand-write an SRI: a plausible-looking wrong one
30
- // makes the browser refuse the script and nacl silently absent, which fails
31
- // every mobile sign-in before any other code runs.
32
- s.integrity = 'sha384-05+sicyRJQ56XpL4U9HJ8YbtSzFDvAg7apPKOGV6A0JsAJKFM68jp5oLnUjG5mEp';
33
- s.crossOrigin = 'anonymous';
34
- s.setAttribute('data-studio-nacl', '');
35
- document.head.appendChild(s);
36
- })();
37
- </script>
@@ -1,167 +0,0 @@
1
- <%#
2
- Phantom MOBILE deep link — the round trip a phone needs to sign in with a
3
- wallet. On desktop Phantom is a browser extension the page can call directly;
4
- on a phone it is a separate APP, so the page hands off to it and comes back.
5
-
6
- PROMOTED from turf-monster (app/javascript/phantom_deeplink.js) so every
7
- consumer gets it. The server half was already shared — the engine's
8
- solana_sessions_controller owns nonce + verify and Solana::SessionAuth owns
9
- verify_solana_signature! — only this mobile round trip was app-side.
10
-
11
- Render it once, wherever the Connect-Wallet flow can be reached — an ERB
12
- output tag rendering the partial path "studio/solana/phantom_deeplink".
13
- (Described rather than quoted on purpose: an ERB comment ends at the first
14
- close sequence, so spelling the tag out here would terminate this comment and
15
- leak the rest of it as visible text into every consuming app. The engine's
16
- erb_comment_leak_test enforces that, and it caught this very paragraph.)
17
-
18
- It defines window.startPhantomDeepLink(linkMode, currentUserId). The engine's
19
- wallet picker (studio/modals/_wallet_connect) gates its mobile Phantom row on
20
- that function EXISTING, so a consumer that does not render this partial keeps
21
- its install row instead of painting a button that does nothing.
22
-
23
- THREE THINGS THAT MUST NOT BE MADE CONFIGURABLE — traced through
24
- Solana::SessionAuth#verify_solana_signature! before this was generalised:
25
-
26
- domain the client builds it from window.location.host and the server
27
- checks it against request.host_with_port (OPSEC-018). An app
28
- override breaks sign-in.
29
- User-ID line OPSEC-005 does a SUBSTRING match on "User-ID: <id>". Do not
30
- reformat it and do not translate it.
31
- the nonce deleted server-side BEFORE verification; that is the replay
32
- protection for the login path.
33
-
34
- The STATEMENT is the one part that varies, and it varies safely: the server
35
- does NOT verify it — it is what the human reads inside Phantom. It derives
36
- from Studio.app_name, which reproduces turf-monster's previous literal
37
- ("Sign in to Turf Monster") BYTE FOR BYTE, so promoting this changed no
38
- signed message. It MUST match the callback's copy exactly, because the
39
- callback reconstructs the signed message to post for verification — both read
40
- this same helper, which is why they cannot drift.
41
-
42
- Requires tweetnacl (window.nacl); render "studio/solana/deeplink_assets"
43
- first, or supply it yourself.
44
- %>
45
- <script>
46
- (function () {
47
- var STATEMENT = <%= Studio.wallet_sign_in_statement.to_json.html_safe %>;
48
-
49
- // Base58, INLINE and self-contained. turf-monster took this from a separate
50
- // app/javascript/base58.js module that assigned window.encodeBase58, so its
51
- // deep link depended on a second global. Promoting only the deep link would
52
- // have left encodeBase58 undefined in any consumer without that file — it
53
- // throws on the first keypair encode, at the moment the user taps Connect.
54
- // The callback view already carries its own codec for the same reason; this
55
- // matches it, so the partial has exactly one dependency (nacl) and it is a
56
- // guarded one.
57
- // THE CONSTANT THE ENCODER READS. It was left behind when the function body
58
- // was inlined: base58.js declares it at module scope, so it is unreachable
59
- // from a classic script, and encodeBase58 referenced a free variable that
60
- // resolved to nothing AT CALL TIME. Every mobile sign-in threw
61
- // "B58_ALPHABET is not defined" on the first keypair encode.
62
- //
63
- // Invisible to eleven passing view tests and to a browser spec that checked
64
- // `typeof startPhantomDeepLink` without ever CALLING it. A parse-time
65
- // mutation cannot reach this class either — the program parses fine.
66
- const B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
67
-
68
- function encodeBase58(bytes) {
69
- const digits = [0];
70
- for (let i = 0; i < bytes.length; i++) {
71
- let carry = bytes[i];
72
- for (let j = 0; j < digits.length; j++) {
73
- carry += digits[j] << 8;
74
- digits[j] = carry % 58;
75
- carry = (carry / 58) | 0;
76
- }
77
- while (carry) { digits.push(carry % 58); carry = (carry / 58) | 0; }
78
- }
79
- let str = '';
80
- for (let i = 0; i < bytes.length && bytes[i] === 0; i++) str += '1';
81
- for (let i = digits.length - 1; i >= 0; i--) str += B58_ALPHABET[digits[i]];
82
- return str;
83
- }
84
-
85
- // Phantom deep link protocol for mobile browsers
86
- // Uses signIn deep link — ONE trip to Phantom (connect + sign combined)
87
- // Flow: generate keypair → fetch nonce → build SIWS input → redirect to Phantom signIn
88
-
89
- function startPhantomDeepLink(linkMode, currentUserId) {
90
- // The consumer declares its cluster on <body data-solana-cluster="...">.
91
- // turf-monster does; an app that does NOT silently signs against devnet while
92
- // its wallet is on mainnet, which reads to the user as a rejected signature
93
- // and to the log as nothing. Warn loudly rather than guess quietly — the
94
- // default stays devnet so behaviour is unchanged for anyone already relying
95
- // on it.
96
- var cluster = document.body.dataset.solanaCluster;
97
- if (!cluster) {
98
- console.warn('[StudioSolana] no data-solana-cluster on <body>; defaulting to devnet. ' +
99
- 'Set it on the body tag to sign against the cluster you mean.');
100
- cluster = 'devnet';
101
- }
102
- var callbackUrl = window.location.origin + '/auth/phantom/callback';
103
-
104
- // Generate x25519 keypair for decrypting Phantom's response
105
- var dappKeyPair = nacl.box.keyPair();
106
-
107
- // Fetch nonce from server, then redirect to Phantom signIn
108
- fetch('/auth/solana/nonce')
109
- .then(function(r) { return r.json(); })
110
- .then(function(data) {
111
- // OPSEC-005: when linking a wallet to a logged-in user, embed
112
- // `User-ID: <id>` so the server can refuse signatures captured
113
- // against a different session's nonce. Login (no currentUserId)
114
- // skips the binding line.
115
- var statement = STATEMENT;
116
- if (linkMode && currentUserId) {
117
- statement = statement + '\nUser-ID: ' + currentUserId;
118
- }
119
-
120
- // Build SIWS input (CAIP-122 / Sign In With Solana format)
121
- // Note: chainId omitted — optional per spec, avoids mismatch warning
122
- // when app is on devnet but user's wallet is on mainnet
123
- var signInInput = {
124
- domain: window.location.host,
125
- statement: statement,
126
- uri: window.location.origin,
127
- version: '1',
128
- nonce: data.nonce,
129
- issuedAt: new Date().toISOString()
130
- };
131
-
132
- // Save state to localStorage (persists across redirect)
133
- localStorage.setItem('phantom_dl_secret', encodeBase58(dappKeyPair.secretKey));
134
- localStorage.setItem('phantom_dl_pubkey', encodeBase58(dappKeyPair.publicKey));
135
- localStorage.setItem('phantom_dl_nonce', data.nonce);
136
- localStorage.setItem('phantom_dl_nonce_at', Date.now().toString());
137
- localStorage.setItem('phantom_dl_step', 'signIn');
138
- localStorage.setItem('phantom_dl_link_mode', linkMode ? 'true' : 'false');
139
- localStorage.setItem('phantom_dl_cluster', cluster);
140
- if (currentUserId) {
141
- localStorage.setItem('phantom_dl_user_id', String(currentUserId));
142
- } else {
143
- localStorage.removeItem('phantom_dl_user_id');
144
- }
145
-
146
- // Base58-encode the SIWS input JSON (NOT encrypted — per Phantom signIn protocol)
147
- var payloadB58 = encodeBase58(new TextEncoder().encode(JSON.stringify(signInInput)));
148
-
149
- // Build Phantom signIn deep link
150
- var params = new URLSearchParams({
151
- dapp_encryption_public_key: encodeBase58(dappKeyPair.publicKey),
152
- cluster: cluster,
153
- app_url: window.location.origin,
154
- redirect_link: callbackUrl,
155
- payload: payloadB58
156
- });
157
-
158
- window.location.href = 'https://phantom.app/ul/v1/signIn?' + params.toString();
159
- })
160
- .catch(function(err) {
161
- alert('Failed to start Phantom connection: ' + err.message);
162
- });
163
- }
164
-
165
- window.startPhantomDeepLink = startPhantomDeepLink;
166
- })();
167
- </script>