openclacky 0.9.2 → 0.9.4
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 +32 -0
- data/docs/security-design.md +109 -0
- data/lib/clacky/agent/message_compressor_helper.rb +82 -69
- data/lib/clacky/agent/session_serializer.rb +9 -1
- data/lib/clacky/agent/skill_manager.rb +7 -0
- data/lib/clacky/agent.rb +11 -3
- data/lib/clacky/banner.rb +65 -0
- data/lib/clacky/block_font.rb +331 -0
- data/lib/clacky/brand_config.rb +73 -5
- data/lib/clacky/client.rb +129 -633
- data/lib/clacky/default_skills/activate-license/SKILL.md +118 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +10 -20
- data/lib/clacky/message_format/anthropic.rb +241 -0
- data/lib/clacky/message_format/open_ai.rb +135 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -0
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +13 -0
- data/lib/clacky/server/http_server.rb +12 -2
- data/lib/clacky/session_manager.rb +7 -2
- data/lib/clacky/tools/browser.rb +109 -280
- data/lib/clacky/ui2/block_font.rb +10 -0
- data/lib/clacky/ui2/components/welcome_banner.rb +23 -22
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +588 -6
- data/lib/clacky/web/app.js +30 -15
- data/lib/clacky/web/brand.js +141 -9
- data/lib/clacky/web/i18n.js +28 -2
- data/lib/clacky/web/index.html +142 -127
- data/lib/clacky/web/onboard.js +192 -225
- data/lib/clacky/web/sessions.js +12 -8
- data/lib/clacky/web/settings.js +57 -4
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +60 -15
- metadata +8 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
# BlockFont renders text using Unicode box-drawing characters (█ ║ ╗ ╔ ╚ ╝ ═)
|
|
5
|
+
# in the same visual style as the hardcoded OpenClacky logo.
|
|
6
|
+
#
|
|
7
|
+
# Each glyph is 6 lines tall. Characters are joined horizontally with one
|
|
8
|
+
# space column between them.
|
|
9
|
+
module BlockFont
|
|
10
|
+
# Each letter is defined as an array of exactly 6 strings (equal width).
|
|
11
|
+
# rubocop:disable Layout/ExtraSpacing, Layout/SpaceInsideArrayLiteralBrackets
|
|
12
|
+
GLYPHS = {
|
|
13
|
+
"0" => [
|
|
14
|
+
" ██████╗ ",
|
|
15
|
+
"██╔═══██╗",
|
|
16
|
+
"██║██╗██║",
|
|
17
|
+
"██║╚█╔╝██║",
|
|
18
|
+
"╚██████╔╝",
|
|
19
|
+
" ╚═════╝ ",
|
|
20
|
+
],
|
|
21
|
+
"1" => [
|
|
22
|
+
" ██╗",
|
|
23
|
+
"███║",
|
|
24
|
+
"╚██║",
|
|
25
|
+
" ██║",
|
|
26
|
+
" ██║",
|
|
27
|
+
" ╚═╝",
|
|
28
|
+
],
|
|
29
|
+
"2" => [
|
|
30
|
+
"██████╗ ",
|
|
31
|
+
"╚════██╗",
|
|
32
|
+
" █████╔╝",
|
|
33
|
+
"██╔═══╝ ",
|
|
34
|
+
"███████╗",
|
|
35
|
+
"╚══════╝",
|
|
36
|
+
],
|
|
37
|
+
"3" => [
|
|
38
|
+
"██████╗ ",
|
|
39
|
+
"╚════██╗",
|
|
40
|
+
" █████╔╝",
|
|
41
|
+
" ╚═══██╗",
|
|
42
|
+
"██████╔╝",
|
|
43
|
+
"╚═════╝ ",
|
|
44
|
+
],
|
|
45
|
+
"4" => [
|
|
46
|
+
"██╗ ██╗",
|
|
47
|
+
"██║ ██║",
|
|
48
|
+
"███████║",
|
|
49
|
+
"╚════██║",
|
|
50
|
+
" ██║",
|
|
51
|
+
" ╚═╝",
|
|
52
|
+
],
|
|
53
|
+
"5" => [
|
|
54
|
+
"███████╗",
|
|
55
|
+
"██╔════╝",
|
|
56
|
+
"███████╗",
|
|
57
|
+
"╚════██║",
|
|
58
|
+
"███████║",
|
|
59
|
+
"╚══════╝",
|
|
60
|
+
],
|
|
61
|
+
"6" => [
|
|
62
|
+
" ██████╗ ",
|
|
63
|
+
"██╔════╝ ",
|
|
64
|
+
"███████╗ ",
|
|
65
|
+
"██╔═══██╗",
|
|
66
|
+
"╚██████╔╝",
|
|
67
|
+
" ╚═════╝ ",
|
|
68
|
+
],
|
|
69
|
+
"7" => [
|
|
70
|
+
"███████╗",
|
|
71
|
+
"╚════██║",
|
|
72
|
+
" ██╔╝",
|
|
73
|
+
" ██╔╝ ",
|
|
74
|
+
" ██║ ",
|
|
75
|
+
" ╚═╝ ",
|
|
76
|
+
],
|
|
77
|
+
"8" => [
|
|
78
|
+
" █████╗ ",
|
|
79
|
+
"██╔══██╗",
|
|
80
|
+
"╚█████╔╝",
|
|
81
|
+
"██╔══██╗",
|
|
82
|
+
"╚█████╔╝",
|
|
83
|
+
" ╚════╝ ",
|
|
84
|
+
],
|
|
85
|
+
"9" => [
|
|
86
|
+
" █████╗ ",
|
|
87
|
+
"██╔══██╗",
|
|
88
|
+
"╚██████║",
|
|
89
|
+
" ╚═══██║",
|
|
90
|
+
" █████╔╝",
|
|
91
|
+
" ╚════╝ ",
|
|
92
|
+
],
|
|
93
|
+
" " => [
|
|
94
|
+
" ",
|
|
95
|
+
" ",
|
|
96
|
+
" ",
|
|
97
|
+
" ",
|
|
98
|
+
" ",
|
|
99
|
+
" ",
|
|
100
|
+
],
|
|
101
|
+
"a" => [
|
|
102
|
+
" ████╗ ",
|
|
103
|
+
"██╔══██╗",
|
|
104
|
+
"███████║",
|
|
105
|
+
"██╔══██║",
|
|
106
|
+
"██║ ██║",
|
|
107
|
+
"╚═╝ ╚═╝",
|
|
108
|
+
],
|
|
109
|
+
"b" => [
|
|
110
|
+
"██████╗ ",
|
|
111
|
+
"██╔══██╗",
|
|
112
|
+
"██████╔╝",
|
|
113
|
+
"██╔══██╗",
|
|
114
|
+
"██████╔╝",
|
|
115
|
+
"╚═════╝ ",
|
|
116
|
+
],
|
|
117
|
+
"c" => [
|
|
118
|
+
" ██████╗",
|
|
119
|
+
"██╔════╝",
|
|
120
|
+
"██║ ",
|
|
121
|
+
"██║ ",
|
|
122
|
+
"╚██████╗",
|
|
123
|
+
" ╚═════╝",
|
|
124
|
+
],
|
|
125
|
+
"d" => [
|
|
126
|
+
"██████╗ ",
|
|
127
|
+
"██╔══██╗",
|
|
128
|
+
"██║ ██║",
|
|
129
|
+
"██║ ██║",
|
|
130
|
+
"██████╔╝",
|
|
131
|
+
"╚═════╝ ",
|
|
132
|
+
],
|
|
133
|
+
"e" => [
|
|
134
|
+
"███████╗",
|
|
135
|
+
"██╔════╝",
|
|
136
|
+
"█████╗ ",
|
|
137
|
+
"██╔══╝ ",
|
|
138
|
+
"███████╗",
|
|
139
|
+
"╚══════╝",
|
|
140
|
+
],
|
|
141
|
+
"f" => [
|
|
142
|
+
"███████╗",
|
|
143
|
+
"██╔════╝",
|
|
144
|
+
"█████╗ ",
|
|
145
|
+
"██╔══╝ ",
|
|
146
|
+
"██║ ",
|
|
147
|
+
"╚═╝ ",
|
|
148
|
+
],
|
|
149
|
+
"g" => [
|
|
150
|
+
" ██████╗ ",
|
|
151
|
+
"██╔════╝ ",
|
|
152
|
+
"██║ ███╗",
|
|
153
|
+
"██║ ██║",
|
|
154
|
+
"╚██████╔╝",
|
|
155
|
+
" ╚═════╝ ",
|
|
156
|
+
],
|
|
157
|
+
"h" => [
|
|
158
|
+
"██╗ ██╗",
|
|
159
|
+
"██║ ██║",
|
|
160
|
+
"███████║",
|
|
161
|
+
"██╔══██║",
|
|
162
|
+
"██║ ██║",
|
|
163
|
+
"╚═╝ ╚═╝",
|
|
164
|
+
],
|
|
165
|
+
"i" => [
|
|
166
|
+
"██╗",
|
|
167
|
+
"██║",
|
|
168
|
+
"██║",
|
|
169
|
+
"██║",
|
|
170
|
+
"██║",
|
|
171
|
+
"╚═╝",
|
|
172
|
+
],
|
|
173
|
+
"j" => [
|
|
174
|
+
" ██╗",
|
|
175
|
+
" ██║",
|
|
176
|
+
" ██║",
|
|
177
|
+
" ██║",
|
|
178
|
+
"███║",
|
|
179
|
+
"╚══╝",
|
|
180
|
+
],
|
|
181
|
+
"k" => [
|
|
182
|
+
"██╗ ██╗",
|
|
183
|
+
"██║ ██╔╝",
|
|
184
|
+
"█████╔╝ ",
|
|
185
|
+
"██╔═██╗ ",
|
|
186
|
+
"██║ ██╗",
|
|
187
|
+
"╚═╝ ╚═╝",
|
|
188
|
+
],
|
|
189
|
+
"l" => [
|
|
190
|
+
"██╗ ",
|
|
191
|
+
"██║ ",
|
|
192
|
+
"██║ ",
|
|
193
|
+
"██║ ",
|
|
194
|
+
"███████╗",
|
|
195
|
+
"╚══════╝",
|
|
196
|
+
],
|
|
197
|
+
"m" => [
|
|
198
|
+
"███╗ ███╗",
|
|
199
|
+
"████╗ ████║",
|
|
200
|
+
"██╔████╔██║",
|
|
201
|
+
"██║╚██╔╝██║",
|
|
202
|
+
"██║ ╚═╝ ██║",
|
|
203
|
+
"╚═╝ ╚═╝",
|
|
204
|
+
],
|
|
205
|
+
"n" => [
|
|
206
|
+
"███╗ ██╗",
|
|
207
|
+
"████╗ ██║",
|
|
208
|
+
"██╔██╗ ██║",
|
|
209
|
+
"██║╚██╗██║",
|
|
210
|
+
"██║ ╚████║",
|
|
211
|
+
"╚═╝ ╚═══╝",
|
|
212
|
+
],
|
|
213
|
+
"o" => [
|
|
214
|
+
" ██████╗ ",
|
|
215
|
+
"██╔═══██╗",
|
|
216
|
+
"██║ ██║",
|
|
217
|
+
"██║ ██║",
|
|
218
|
+
"╚██████╔╝",
|
|
219
|
+
" ╚═════╝ ",
|
|
220
|
+
],
|
|
221
|
+
"p" => [
|
|
222
|
+
"██████╗ ",
|
|
223
|
+
"██╔══██╗",
|
|
224
|
+
"██████╔╝",
|
|
225
|
+
"██╔═══╝ ",
|
|
226
|
+
"██║ ",
|
|
227
|
+
"╚═╝ ",
|
|
228
|
+
],
|
|
229
|
+
"q" => [
|
|
230
|
+
" ██████╗ ",
|
|
231
|
+
"██╔═══██╗",
|
|
232
|
+
"██║ ██║",
|
|
233
|
+
"██║▄▄ ██║",
|
|
234
|
+
"╚██████╔╝",
|
|
235
|
+
" ╚══▀▀═╝ ",
|
|
236
|
+
],
|
|
237
|
+
"r" => [
|
|
238
|
+
"██████╗ ",
|
|
239
|
+
"██╔══██╗",
|
|
240
|
+
"██████╔╝",
|
|
241
|
+
"██╔══██╗",
|
|
242
|
+
"██║ ██║",
|
|
243
|
+
"╚═╝ ╚═╝",
|
|
244
|
+
],
|
|
245
|
+
"s" => [
|
|
246
|
+
"███████╗",
|
|
247
|
+
"██╔════╝",
|
|
248
|
+
"███████╗",
|
|
249
|
+
"╚════██║",
|
|
250
|
+
"███████║",
|
|
251
|
+
"╚══════╝",
|
|
252
|
+
],
|
|
253
|
+
"t" => [
|
|
254
|
+
"████████╗",
|
|
255
|
+
"╚══██╔══╝",
|
|
256
|
+
" ██║ ",
|
|
257
|
+
" ██║ ",
|
|
258
|
+
" ██║ ",
|
|
259
|
+
" ╚═╝ ",
|
|
260
|
+
],
|
|
261
|
+
"u" => [
|
|
262
|
+
"██╗ ██╗",
|
|
263
|
+
"██║ ██║",
|
|
264
|
+
"██║ ██║",
|
|
265
|
+
"██║ ██║",
|
|
266
|
+
"╚██████╔╝",
|
|
267
|
+
" ╚═════╝ ",
|
|
268
|
+
],
|
|
269
|
+
"v" => [
|
|
270
|
+
"██╗ ██╗",
|
|
271
|
+
"██║ ██║",
|
|
272
|
+
"██║ ██║",
|
|
273
|
+
"╚██╗ ██╔╝",
|
|
274
|
+
" ╚████╔╝ ",
|
|
275
|
+
" ╚═══╝ ",
|
|
276
|
+
],
|
|
277
|
+
"w" => [
|
|
278
|
+
"██╗ ██╗",
|
|
279
|
+
"██║ ██║",
|
|
280
|
+
"██║ █╗ ██║",
|
|
281
|
+
"██║███╗██║",
|
|
282
|
+
"╚███╔███╔╝",
|
|
283
|
+
" ╚══╝╚══╝ ",
|
|
284
|
+
],
|
|
285
|
+
"x" => [
|
|
286
|
+
"██╗ ██╗",
|
|
287
|
+
"╚██╗██╔╝",
|
|
288
|
+
" ╚███╔╝ ",
|
|
289
|
+
" ██╔██╗ ",
|
|
290
|
+
"██╔╝ ██╗",
|
|
291
|
+
"╚═╝ ╚═╝",
|
|
292
|
+
],
|
|
293
|
+
"y" => [
|
|
294
|
+
"██╗ ██╗",
|
|
295
|
+
"╚██╗ ██╔╝",
|
|
296
|
+
" ╚████╔╝ ",
|
|
297
|
+
" ╚██╔╝ ",
|
|
298
|
+
" ██║ ",
|
|
299
|
+
" ╚═╝ ",
|
|
300
|
+
],
|
|
301
|
+
"z" => [
|
|
302
|
+
"███████╗",
|
|
303
|
+
"╚════██║",
|
|
304
|
+
" ██╔╝",
|
|
305
|
+
" ██╔╝ ",
|
|
306
|
+
"███████╗",
|
|
307
|
+
"╚══════╝",
|
|
308
|
+
],
|
|
309
|
+
}.freeze
|
|
310
|
+
# rubocop:enable Layout/ExtraSpacing, Layout/SpaceInsideArrayLiteralBrackets
|
|
311
|
+
|
|
312
|
+
HEIGHT = 6
|
|
313
|
+
GLYPH_GAP = " "
|
|
314
|
+
|
|
315
|
+
# Render a string as block-font art. Unknown characters fall back to space.
|
|
316
|
+
# Returns a multi-line string (6 lines joined by newlines).
|
|
317
|
+
def self.render(text)
|
|
318
|
+
chars = text.downcase.chars
|
|
319
|
+
glyphs = chars.map { |c| GLYPHS[c] || GLYPHS[" "] }
|
|
320
|
+
|
|
321
|
+
HEIGHT.times.map do |row|
|
|
322
|
+
glyphs.map { |g| g[row] }.join(GLYPH_GAP)
|
|
323
|
+
end.join("\n")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Return the pixel width of the rendered text (longest line length).
|
|
327
|
+
def self.width(text)
|
|
328
|
+
render(text).lines.map { |l| l.chomp.length }.max.to_i
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -22,6 +22,9 @@ module Clacky
|
|
|
22
22
|
# product_name: "JohnAI Pro"
|
|
23
23
|
# logo_url: "https://example.com/logo.png"
|
|
24
24
|
# support_contact: "support@johnai.com"
|
|
25
|
+
# support_qr_url: "https://example.com/qr.png"
|
|
26
|
+
# theme_color: "#3B82F6"
|
|
27
|
+
# homepage_url: "https://johnai.com"
|
|
25
28
|
# license_key: "0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
26
29
|
# license_activated_at: "2025-03-01T00:00:00Z"
|
|
27
30
|
# license_expires_at: "2026-03-01T00:00:00Z"
|
|
@@ -43,7 +46,8 @@ module Clacky
|
|
|
43
46
|
attr_reader :brand_name, :license_key, :license_activated_at,
|
|
44
47
|
:license_expires_at, :license_last_heartbeat, :device_id,
|
|
45
48
|
:brand_command, :distribution_name, :product_name,
|
|
46
|
-
:logo_url, :support_contact, :license_user_id
|
|
49
|
+
:logo_url, :support_contact, :license_user_id,
|
|
50
|
+
:support_qr_url, :theme_color, :homepage_url
|
|
47
51
|
|
|
48
52
|
def initialize(attrs = {})
|
|
49
53
|
@brand_name = attrs["brand_name"]
|
|
@@ -52,6 +56,9 @@ module Clacky
|
|
|
52
56
|
@product_name = attrs["product_name"]
|
|
53
57
|
@logo_url = attrs["logo_url"]
|
|
54
58
|
@support_contact = attrs["support_contact"]
|
|
59
|
+
@support_qr_url = attrs["support_qr_url"]
|
|
60
|
+
@theme_color = attrs["theme_color"]
|
|
61
|
+
@homepage_url = attrs["homepage_url"]
|
|
55
62
|
@license_key = attrs["license_key"]
|
|
56
63
|
@license_activated_at = parse_time(attrs["license_activated_at"])
|
|
57
64
|
@license_expires_at = parse_time(attrs["license_expires_at"])
|
|
@@ -159,6 +166,13 @@ module Clacky
|
|
|
159
166
|
# Server returns "owner_user_id" for system licenses; plan-based licenses return nil.
|
|
160
167
|
owner_uid = data["owner_user_id"]
|
|
161
168
|
@license_user_id = owner_uid.to_s.strip if owner_uid && !owner_uid.to_s.strip.empty?
|
|
169
|
+
# Pin the device_id used in this activation request so that future API calls
|
|
170
|
+
# (e.g. skill_keys, heartbeat) always send the exact same device_id that was
|
|
171
|
+
# recorded in activated_devices on the server side.
|
|
172
|
+
# If the server echoes back device_id in the response, prefer that value;
|
|
173
|
+
# otherwise keep the one we just sent (@device_id is already set above).
|
|
174
|
+
server_device_id = data["device_id"].to_s.strip
|
|
175
|
+
@device_id = server_device_id unless server_device_id.empty?
|
|
162
176
|
apply_distribution(data["distribution"])
|
|
163
177
|
save
|
|
164
178
|
{ success: true, message: "License activated successfully!", brand_name: @brand_name,
|
|
@@ -179,6 +193,8 @@ module Clacky
|
|
|
179
193
|
# Returns the same { success:, message:, brand_name:, data: } shape as activate!
|
|
180
194
|
def activate_mock!(license_key)
|
|
181
195
|
@license_key = license_key.strip
|
|
196
|
+
# Pin a stable device_id for this activation. Once set (from a prior load or
|
|
197
|
+
# a previous call), never regenerate — the same rule as activate!.
|
|
182
198
|
@device_id ||= generate_device_id
|
|
183
199
|
|
|
184
200
|
# Always derive brand_name fresh from the key in mock mode,
|
|
@@ -452,6 +468,7 @@ module Clacky
|
|
|
452
468
|
# Auto-detect whether the zip has a single root folder to strip.
|
|
453
469
|
# Uses get_input_stream instead of entry.extract to avoid rubyzip 3.x
|
|
454
470
|
# path-safety restrictions on absolute destination paths.
|
|
471
|
+
# Uses chunked read + size verification for robustness.
|
|
455
472
|
Zip::File.open(tmp_zip) do |zip|
|
|
456
473
|
entries = zip.entries.reject(&:directory?)
|
|
457
474
|
top_dirs = entries.map { |e| e.name.split("/").first }.uniq
|
|
@@ -469,7 +486,22 @@ module Clacky
|
|
|
469
486
|
|
|
470
487
|
out = File.join(dest_dir, rel_path)
|
|
471
488
|
FileUtils.mkdir_p(File.dirname(out))
|
|
472
|
-
|
|
489
|
+
|
|
490
|
+
# Chunked copy with size verification
|
|
491
|
+
written = 0
|
|
492
|
+
File.open(out, "wb") do |f|
|
|
493
|
+
entry.get_input_stream do |input|
|
|
494
|
+
while (chunk = input.read(65536))
|
|
495
|
+
f.write(chunk)
|
|
496
|
+
written += chunk.bytesize
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Verify file size matches ZIP entry declaration
|
|
502
|
+
if written != entry.size
|
|
503
|
+
raise "Size mismatch for #{entry.name}: expected #{entry.size}, got #{written}"
|
|
504
|
+
end
|
|
473
505
|
end
|
|
474
506
|
end
|
|
475
507
|
|
|
@@ -708,6 +740,9 @@ module Clacky
|
|
|
708
740
|
product_name: @product_name,
|
|
709
741
|
logo_url: @logo_url,
|
|
710
742
|
support_contact: @support_contact,
|
|
743
|
+
support_qr_url: @support_qr_url,
|
|
744
|
+
theme_color: @theme_color,
|
|
745
|
+
homepage_url: @homepage_url,
|
|
711
746
|
branded: branded?,
|
|
712
747
|
activated: activated?,
|
|
713
748
|
expired: expired?,
|
|
@@ -727,6 +762,9 @@ module Clacky
|
|
|
727
762
|
data["product_name"] = @product_name if @product_name
|
|
728
763
|
data["logo_url"] = @logo_url if @logo_url
|
|
729
764
|
data["support_contact"] = @support_contact if @support_contact
|
|
765
|
+
data["support_qr_url"] = @support_qr_url if @support_qr_url
|
|
766
|
+
data["theme_color"] = @theme_color if @theme_color
|
|
767
|
+
data["homepage_url"] = @homepage_url if @homepage_url
|
|
730
768
|
data["license_key"] = @license_key if @license_key
|
|
731
769
|
data["license_activated_at"] = @license_activated_at.iso8601 if @license_activated_at
|
|
732
770
|
data["license_expires_at"] = @license_expires_at.iso8601 if @license_expires_at
|
|
@@ -756,14 +794,19 @@ module Clacky
|
|
|
756
794
|
end
|
|
757
795
|
|
|
758
796
|
# Apply distribution fields from API response.
|
|
759
|
-
# Updates name, product_name, logo_url, support_contact
|
|
797
|
+
# Updates name, product_name, logo_url, support_contact, support_qr_url,
|
|
798
|
+
# theme_color, and homepage_url from the distribution hash.
|
|
760
799
|
private def apply_distribution(dist)
|
|
761
800
|
return unless dist.is_a?(Hash)
|
|
762
801
|
|
|
763
|
-
@distribution_name = dist["name"]
|
|
802
|
+
@distribution_name = dist["name"] if dist["name"].to_s.strip != ""
|
|
764
803
|
@product_name = dist["product_name"] if dist["product_name"].to_s.strip != ""
|
|
765
804
|
@logo_url = dist["logo_url"] if dist["logo_url"].to_s.strip != ""
|
|
766
805
|
@support_contact = dist["support_contact"] if dist["support_contact"].to_s.strip != ""
|
|
806
|
+
# New branding fields returned by the API (logo, QR code, theme, homepage)
|
|
807
|
+
@support_qr_url = dist["support_qr_url"] if dist.key?("support_qr_url")
|
|
808
|
+
@theme_color = dist["theme_color"] if dist.key?("theme_color")
|
|
809
|
+
@homepage_url = dist["homepage_url"] if dist.key?("homepage_url")
|
|
767
810
|
end
|
|
768
811
|
|
|
769
812
|
# Download a remote URL to a local file path.
|
|
@@ -844,6 +887,19 @@ module Clacky
|
|
|
844
887
|
return cached[:key] if key_valid && within_grace
|
|
845
888
|
end
|
|
846
889
|
|
|
890
|
+
# Guard: @device_id must match the value recorded in activated_devices on the
|
|
891
|
+
# server. If it is nil (e.g. loaded from a brand.yml that predates the
|
|
892
|
+
# device_id field), reload from disk as a last-chance recovery — the file
|
|
893
|
+
# may have been written by a concurrent process or a newer gem version.
|
|
894
|
+
# If still nil after reload, raise an actionable error rather than sending
|
|
895
|
+
# an empty device_id that will always be rejected by the server.
|
|
896
|
+
if @device_id.nil? || @device_id.strip.empty?
|
|
897
|
+
reloaded = BrandConfig.load
|
|
898
|
+
@device_id = reloaded.device_id if reloaded.device_id && !reloaded.device_id.strip.empty?
|
|
899
|
+
end
|
|
900
|
+
raise "Device ID is missing. Please re-activate your license with `clacky license activate`." \
|
|
901
|
+
if @device_id.nil? || @device_id.strip.empty?
|
|
902
|
+
|
|
847
903
|
# Build signed request payload
|
|
848
904
|
user_id = parse_user_id_from_key(@license_key)
|
|
849
905
|
key_hash = Digest::SHA256.hexdigest(@license_key)
|
|
@@ -902,7 +958,19 @@ module Clacky
|
|
|
902
958
|
hex[0..7].to_i(16)
|
|
903
959
|
end
|
|
904
960
|
|
|
905
|
-
# Generate a stable device ID based on system identifiers.
|
|
961
|
+
# Generate a one-time stable device ID based on system identifiers.
|
|
962
|
+
#
|
|
963
|
+
# IMPORTANT: This method MUST only be called once — during the very first
|
|
964
|
+
# activation — via `@device_id ||= generate_device_id`. The result is
|
|
965
|
+
# immediately persisted to brand.yml by `save`. All subsequent calls
|
|
966
|
+
# (heartbeat, skill_keys, etc.) must read @device_id from memory (which was
|
|
967
|
+
# populated by `initialize` from the stored brand.yml), never call this
|
|
968
|
+
# method again.
|
|
969
|
+
#
|
|
970
|
+
# The generated ID is deterministic for the same environment, but can change
|
|
971
|
+
# if the hostname, user, or platform changes (e.g. inside a Docker container
|
|
972
|
+
# with a random hostname). That is why we pin it to disk immediately and
|
|
973
|
+
# never regenerate once saved.
|
|
906
974
|
private def generate_device_id
|
|
907
975
|
components = [
|
|
908
976
|
Socket.gethostname,
|