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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile +25 -0
- data/README.md +22 -24
- data/app/controllers/studio/knowledge_docs_controller.rb +89 -0
- data/app/models/studio/knowledge_doc.rb +207 -0
- data/app/views/solana_sessions/phantom_callback.html.erb +6 -5
- data/app/views/studio/knowledge_docs/_access_chips.html.erb +10 -0
- data/app/views/studio/knowledge_docs/_browser.html.erb +115 -0
- data/app/views/studio/knowledge_docs/_doc_row.html.erb +18 -0
- data/app/views/studio/knowledge_docs/_upload_form.html.erb +48 -0
- data/app/views/studio/knowledge_docs/index.html.erb +6 -0
- data/app/views/studio/knowledge_docs/show.html.erb +87 -0
- data/app/views/style/_modals.html.erb +73 -21
- data/app/views/style/modals/_wallet_connect.html.erb +11 -5
- data/db/migrate/20260901000001_create_studio_knowledge_docs.rb +49 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +21 -0
- metadata +10 -5
- data/app/views/studio/modals/_wallet_connect.html.erb +0 -330
- data/app/views/studio/modals/_web3_step_up.html.erb +0 -292
- data/app/views/studio/solana/_deeplink_assets.html.erb +0 -37
- data/app/views/studio/solana/_phantom_deeplink.html.erb +0 -167
|
@@ -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>
|