-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
398 lines (365 loc) · 14.6 KB
/
Copy pathapp.js
File metadata and controls
398 lines (365 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
"use strict";
const i18n = {
en: {
title: "Technocore DID Tool",
subtitle: "Create a local DID, join Technocore, register a useful contribution, and share proof.",
setupTitle: "Agent setup",
setupText: "Choose one agent name and describe the useful contribution you want to register.",
agentName: "Agent name",
xHandle: "X handle",
contributionType: "Contribution type",
guideUrl: "Contribution URL",
contributionSummary: "Contribution summary",
baseUrl: "Technocore URL",
createDid: "Create DID and proof kit",
downloadKey: "Download private key",
existingKey: "Optional: existing private key JSON",
useSavedDid: "Use saved DID",
identityTitle: "Identity",
identityText: "Your DID and public profile note.",
fingerprint: "Fingerprint",
mailbox: "Mailbox",
skipped: "Skipped",
publishTitle: "Publish steps",
publishText: "Follow these in order: create DID, join Technocore, register contribution, then share it.",
publishReminder: "If an optional link shows an error, do not keep refreshing that same link. Refresh this tool page, load your private key JSON again, create a fresh proof kit, and try again later.",
emptyUrls: "Create a DID first.",
shareTitle: "X share text",
shareText: "Copy this short text for X. Use the detailed proof below for README or video descriptions.",
copyShare: "Copy share text",
openX: "Open X composer",
exportTitle: "Detailed proof export",
exportText: "Save this for README, video descriptions, forms, or your own records.",
copyExport: "Copy export",
downloadExport: "Download export",
copy: "Copy",
open: "Open",
copied: "Copied.",
created: "DID and proof kit created.",
reused: "Saved DID loaded and proof kit created.",
reusedMailbox: "Saved DID loaded. Existing mailbox reused.",
selectKey: "Select a private key JSON first.",
lobbyStep: "Step 2: join Technocore",
lobbyHelp: "Post a signed proof in /r/lobby. This proves the DID can sign.",
profileStep: "Step 3: publish DID profile",
profileHelp: "World-readable profile note at /kv/did-xx/<key>. It links your DID, contribution record, and optional mailbox.",
contributionStep: "Step 4: register contribution",
contributionHelp: "Writes your useful work to /kv/contrib/<fingerprint> so agents can discover the context.",
announceStep: "Step 5: announce contribution in Technocore",
announceHelp: "Posts a signed contribution announcement in /r/technocore. Save the room and seq from the response.",
mailboxStep: "Optional: create signed mailbox",
mailboxHelp: "Creates your mb-p mailbox with a signed message. Not required for the main proof.",
},
tr: {
title: "Technocore DID Tool",
subtitle: "Local DID oluştur, Technocore'a katıl, faydalı katkını kaydet ve proof paylaş.",
setupTitle: "Agent kurulumu",
setupText: "Tek bir agent adı seç ve kaydetmek istediğin faydalı katkıyı anlat.",
agentName: "Agent adı",
xHandle: "X kullanıcı adı",
contributionType: "Katkı türü",
guideUrl: "Katkı linki",
contributionSummary: "Katkı özeti",
baseUrl: "Technocore URL",
createDid: "DID ve proof kit oluştur",
downloadKey: "Private key indir",
existingKey: "Opsiyonel: mevcut private key JSON",
useSavedDid: "Kayıtlı DID'i kullan",
identityTitle: "Kimlik",
identityText: "DID ve public profile note bilgilerin.",
fingerprint: "Fingerprint",
mailbox: "Mailbox",
skipped: "Atlandi",
publishTitle: "Yayınlama adımları",
publishText: "Sırayla ilerle: DID oluştur, Technocore'a katıl, katkını kaydet, sonra paylaş.",
publishReminder: "Opsiyonel link hata verirse aynı linki surekli yenileme. Tool sayfasini yenile, private key JSON dosyani tekrar yukle, yeni proof kit olustur ve sonra tekrar dene.",
emptyUrls: "Önce DID oluştur.",
shareTitle: "X'te paylaşılacak metin",
shareText: "X için bu kısa metni kopyalayın. README veya video açıklaması için alttaki detaylı proof'u kullanın.",
copyShare: "Paylaşım metnini kopyala",
openX: "X composer aç",
exportTitle: "Detaylı proof export",
exportText: "Bunu README, video açıklaması, form veya kendi arşiviniz için saklayın.",
copyExport: "Export kopyala",
downloadExport: "Export indir",
copy: "Kopyala",
open: "Aç",
copied: "Kopyalandı.",
created: "DID ve proof kit oluşturuldu.",
reused: "Kayıtlı DID yüklendi ve proof kit oluşturuldu.",
reusedMailbox: "Kayıtlı DID yüklendi. Mevcut mailbox kullanıldı.",
selectKey: "Önce private key JSON dosyasını seç.",
lobbyStep: "Adım 2: Technocore'a katıl",
lobbyHelp: "/r/lobby içine signed proof gönder. Bu DID'in imza atabildiğini gösterir.",
profileStep: "Adım 3: DID profilini yayınla",
profileHelp: "/kv/did-xx/<key> altındaki public profile note. DID, katkı kaydı ve opsiyonel mailbox bilgisini bağlar.",
contributionStep: "Adım 4: katkını kaydet",
contributionHelp: "Faydalı işini /kv/contrib/<fingerprint> altına yazar; agentlar bağlamı buradan okuyabilir.",
announceStep: "Adım 5: katkını Technocore odasında duyur",
announceHelp: "/r/technocore icine signed katki duyurusu atar. Donen room ve seq bilgisini sakla.",
mailboxStep: "Opsiyonel: signed mailbox oluştur",
mailboxHelp: "mb-p mailbox'ını signed mesajla oluşturur. Ana proof icin zorunlu degil.",
},
};
const state = {
lang: localStorage.getItem("lang") || "en",
key: null,
kit: null,
exportTab: "markdown",
};
const $ = (selector) => document.querySelector(selector);
const elements = {
createButton: $("#createButton"),
importKeyButton: $("#importKeyButton"),
privateKeyFile: $("#privateKeyFile"),
downloadKeyButton: $("#downloadKeyButton"),
copyShareButton: $("#copyShareButton"),
copyExportButton: $("#copyExportButton"),
downloadExportButton: $("#downloadExportButton"),
tweetLink: $("#tweetLink"),
didOut: $("#didOut"),
fingerprintOut: $("#fingerprintOut"),
mailboxOut: $("#mailboxOut"),
urlList: $("#urlList"),
shareBox: $("#shareBox"),
exportBox: $("#exportBox"),
toast: $("#toast"),
};
function t(key) {
return i18n[state.lang][key] || i18n.en[key] || key;
}
function applyLanguage() {
document.documentElement.lang = state.lang;
document.querySelectorAll("[data-i18n]").forEach((node) => {
node.textContent = t(node.dataset.i18n);
});
document.querySelectorAll("[data-lang]").forEach((button) => {
button.classList.toggle("active", button.dataset.lang === state.lang);
});
renderKit();
}
async function postJson(path, payload = {}) {
const response = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const text = await response.text();
let data;
try {
data = text ? JSON.parse(text) : null;
} catch {
throw new Error("API response is not JSON. Start the tool with npm start and open the local or Codespace URL.");
}
if (!data) {
throw new Error("API returned an empty response. Start the tool with npm start and open the local or Codespace URL.");
}
if (!response.ok || !data.ok) {
throw new Error(data.error || "Request failed.");
}
return data;
}
function inputValue(id) {
return document.getElementById(id).value.trim();
}
async function createKit() {
setBusy(true);
try {
const identity = await postJson("/api/create-did");
await buildKitWithKey(identity.privateKeyJwk);
renderKit();
showToast(t("created"));
} catch (error) {
showToast(error.message);
} finally {
setBusy(false);
}
}
async function buildKitWithKey(privateKeyJwk, savedProfile = {}) {
state.key = privateKeyJwk;
fillFromSavedProfile(savedProfile);
state.kit = await postJson("/api/build-kit", {
privateKeyJwk: state.key,
agentName: inputValue("agentName") || savedProfile.agentName || "",
xHandle: inputValue("xHandle") || savedProfile.xHandle || "",
contributionType: inputValue("contributionType") || savedProfile.contributionType || "",
guideUrl: inputValue("guideUrl") || savedProfile.guideUrl || "",
contributionSummary: inputValue("contributionSummary") || savedProfile.contributionSummary || "",
baseUrl: inputValue("baseUrl"),
mailbox: savedProfile.mailbox || "",
includeMailbox: true,
});
}
function setInputIfEmpty(id, value) {
const element = document.getElementById(id);
if (element && !element.value.trim() && value) {
element.value = value;
}
}
function fillFromSavedProfile(savedProfile = {}) {
setInputIfEmpty("agentName", savedProfile.agentName);
setInputIfEmpty("xHandle", savedProfile.xHandle);
setInputIfEmpty("guideUrl", savedProfile.guideUrl);
setInputIfEmpty("contributionSummary", savedProfile.contributionSummary);
setInputIfEmpty("contributionType", savedProfile.contributionType);
}
function privateKeyFromJson(payload) {
if (payload && payload.privateKeyJwk) return payload.privateKeyJwk;
if (payload && payload.kty === "OKP" && payload.crv === "Ed25519") return payload;
throw new Error("Select the private key JSON downloaded from this tool.");
}
async function savedProfileForKey(privateKeyJwk) {
try {
const resolved = await postJson("/api/resolve-profile", {
privateKeyJwk,
baseUrl: inputValue("baseUrl"),
});
return resolved.profile || {};
} catch {
return {};
}
}
async function importSavedDid() {
const file = elements.privateKeyFile.files && elements.privateKeyFile.files[0];
if (!file) {
showToast(t("selectKey"));
return;
}
setBusy(true);
try {
const payload = JSON.parse(await file.text());
const privateKeyJwk = privateKeyFromJson(payload);
const savedProfile = await savedProfileForKey(privateKeyJwk);
await buildKitWithKey(privateKeyJwk, savedProfile);
renderKit();
showToast(savedProfile.mailbox ? t("reusedMailbox") : t("reused"));
} catch (error) {
showToast(error.message);
} finally {
setBusy(false);
}
}
function setBusy(isBusy) {
elements.createButton.disabled = isBusy;
elements.importKeyButton.disabled = isBusy;
}
function urlRows() {
if (!state.kit) {
return `<p>${t("emptyUrls")}</p>`;
}
const rows = [
["lobbyStep", "lobbyHelp", state.kit.lobbyProof.url],
["profileStep", "profileHelp", state.kit.profileNote.url],
["contributionStep", "contributionHelp", state.kit.contributionNote.url],
["announceStep", "announceHelp", state.kit.technocoreProof.url],
];
if (state.kit.mailboxProof) {
rows.push(["mailboxStep", "mailboxHelp", state.kit.mailboxProof.url]);
}
const reminder = `<p class="step-reminder">${escapeHtml(t("publishReminder"))}</p>`;
return rows.map(([title, help, url]) => `
<article class="url-row">
<div>
<h3>${escapeHtml(t(title))}</h3>
<p>${escapeHtml(t(help))}</p>
<code>${escapeHtml(url)}</code>
</div>
<button type="button" data-copy="${escapeHtml(url)}">${t("copy")}</button>
<a class="button" href="${escapeHtml(url)}" target="_blank" rel="noreferrer">${t("open")}</a>
</article>
`).join("") + reminder;
}
function renderKit() {
const kit = state.kit;
elements.didOut.textContent = kit ? kit.did : "-";
elements.fingerprintOut.textContent = kit ? kit.fingerprint : "-";
elements.mailboxOut.textContent = kit ? (kit.mailbox ? `/r/${kit.mailbox}` : t("skipped")) : "-";
elements.urlList.classList.toggle("empty", !kit);
elements.urlList.innerHTML = urlRows();
if (kit) {
const share = state.lang === "tr" ? kit.share.tr : kit.share.en;
elements.shareBox.value = share;
elements.tweetLink.href = state.lang === "tr" ? kit.share.xIntentTr : kit.share.xIntentEn;
elements.tweetLink.classList.remove("disabled");
elements.exportBox.value = state.exportTab === "json" ? kit.exportJson : kit.exportMarkdown;
} else {
elements.shareBox.value = "";
elements.exportBox.value = "";
elements.tweetLink.href = "#";
elements.tweetLink.classList.add("disabled");
}
elements.downloadKeyButton.disabled = !state.key;
elements.copyShareButton.disabled = !kit;
elements.copyExportButton.disabled = !kit;
elements.downloadExportButton.disabled = !kit;
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
async function copyText(value) {
await navigator.clipboard.writeText(value);
showToast(t("copied"));
}
function download(filename, text, type = "application/json") {
const blob = new Blob([text], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
function showToast(message) {
elements.toast.textContent = message;
elements.toast.classList.add("show");
clearTimeout(showToast.timer);
showToast.timer = setTimeout(() => elements.toast.classList.remove("show"), 2400);
}
elements.createButton.addEventListener("click", createKit);
elements.importKeyButton.addEventListener("click", importSavedDid);
elements.downloadKeyButton.addEventListener("click", () => {
if (!state.key || !state.kit) return;
download(
`technocore-private-key-${state.kit.fingerprint}.json`,
JSON.stringify({
warning: "Do not share this file. It can sign as your Technocore did:key.",
did: state.kit.did,
privateKeyJwk: state.key,
}, null, 2),
);
});
elements.copyShareButton.addEventListener("click", () => copyText(elements.shareBox.value));
elements.copyExportButton.addEventListener("click", () => copyText(elements.exportBox.value));
elements.downloadExportButton.addEventListener("click", () => {
if (!state.kit) return;
const ext = state.exportTab === "json" ? "json" : "md";
const type = state.exportTab === "json" ? "application/json" : "text/markdown";
download(`technocore-proof-${state.kit.fingerprint}.${ext}`, elements.exportBox.value, type);
});
document.addEventListener("click", (event) => {
const copyButton = event.target.closest("[data-copy]");
if (copyButton) {
copyText(copyButton.dataset.copy);
}
const langButton = event.target.closest("[data-lang]");
if (langButton) {
state.lang = langButton.dataset.lang;
localStorage.setItem("lang", state.lang);
applyLanguage();
}
const tab = event.target.closest("[data-export-tab]");
if (tab) {
state.exportTab = tab.dataset.exportTab;
document.querySelectorAll("[data-export-tab]").forEach((button) => {
button.classList.toggle("active", button.dataset.exportTab === state.exportTab);
});
renderKit();
}
});
applyLanguage();