Initial push

This commit is contained in:
2026-06-08 03:34:23 -04:00
parent 74b113d59c
commit a37fb60b42

View File

@@ -0,0 +1,241 @@
/**
* g1wallet.js — Ğ1 Wallet client-side skeleton.
*
* At this stage:
* - The g1wallet namespace and session object are declared.
* - The unlock button is wired — it intercepts the click but does NOT derive keys yet.
* - The event API stubs are in place for cry01 and poll01 to listen to.
* - scrypt-js is loaded (vendor/scrypt-js-3.0.1.min.js) but not yet called.
*
* CRITICAL RULES (never relax these):
* - The private key NEVER leaves the browser.
* - The pseudo and password NEVER leave the browser.
* - No key material in localStorage, sessionStorage, or cookies.
* - The private key object is marked non-extractable in WebCrypto.
* - One document, one confirmation, one click — no batch signing.
*/
(function () {
'use strict';
// ------------------------------------------------------------------------
// SESSION STATE
// Session is held in module-scope memory only.
// It is gone when the page is closed or navigated away from.
// ------------------------------------------------------------------------
var _session = {
unlocked: false,
pubkey: null, // base58-encoded Ed25519 public key
// _privkey is intentionally not stored here — it lives only in the
// WebCrypto CryptoKey object (non-extractable) once derivation is implemented.
_cryptoKey: null // CryptoKey (non-extractable) — null until derivation
};
// ------------------------------------------------------------------------
// PUBLIC API
// Exposed on window.g1wallet for other addons to use.
// ------------------------------------------------------------------------
window.g1wallet = {
/**
* Returns true if the wallet is currently unlocked.
*/
isUnlocked: function () {
return _session.unlocked;
},
/**
* Returns the current session pubkey (base58 string), or null if locked.
*/
getPubkey: function () {
return _session.unlocked ? _session.pubkey : null;
},
/**
* Requests a signature from the wallet for a given document.
* If the wallet is locked, dispatches 'g1wallet:sign_request_blocked' instead.
* Once signing is implemented, dispatches the result via the callback event.
*
* @param {string} document - The document string to sign
* @param {string} callback - Event name to dispatch with the signed result
*/
requestSignature: function (document, callback) {
if (!_session.unlocked || !_session._cryptoKey) {
window.dispatchEvent(new CustomEvent('g1wallet:sign_request_blocked', {
detail: { reason: 'Wallet is locked. Please unlock your wallet first.' }
}));
return;
}
// TODO: implement signing via SubtleCrypto.sign() with Ed25519.
// The signed bytes are base64-encoded and dispatched via the callback event.
console.warn('[g1wallet] requestSignature: signing not yet implemented.');
}
};
// ------------------------------------------------------------------------
// WIDGET API
// Used by Widget/G1wallet.php inline script to register widget instances.
// ------------------------------------------------------------------------
window.G1WalletWidget = {
init: function (uid, walletUrl) {
// Listen for session events and update the widget DOM.
window.addEventListener('g1wallet:unlocked', function (e) {
var statusEl = document.getElementById(uid + '-status');
if (!statusEl) return;
var pubkey = e.detail.pubkey || '';
var short = pubkey.substring(0, 12) + '…';
statusEl.innerHTML =
'<div class="g1wallet-status g1wallet-status-unlocked">' +
'<span class="g1wallet-status-indicator">&#x1F513;</span> ' +
'<span class="text-success small">Unlocked</span>' +
'<div class="g1wallet-cached-key text-muted small font-monospace mt-1">' +
_escHtml(short) +
'</div></div>';
});
window.addEventListener('g1wallet:locked', function () {
var statusEl = document.getElementById(uid + '-status');
if (!statusEl) return;
statusEl.innerHTML =
'<div class="g1wallet-status g1wallet-status-locked">' +
'<span class="g1wallet-status-indicator">&#x1F512;</span> ' +
'<span class="text-muted small">Locked</span>' +
'<div class="mt-1"><a href="' + _escHtml(walletUrl) + '" ' +
'class="btn btn-xs btn-outline-secondary btn-sm">Unlock</a></div>' +
'</div>';
});
}
};
// ------------------------------------------------------------------------
// UNLOCK BUTTON — wired on the wallet landing page
// ------------------------------------------------------------------------
document.addEventListener('DOMContentLoaded', function () {
var unlockBtn = document.getElementById('g1wallet-unlock-btn');
var lockedView = document.getElementById('g1wallet-locked-view');
var unlockedView = document.getElementById('g1wallet-unlocked-view');
var spinner = document.getElementById('g1wallet-unlock-spinner');
var errorEl = document.getElementById('g1wallet-unlock-error');
var lockBtn = document.getElementById('g1wallet-lock-btn');
if (!unlockBtn) return; // Not on the wallet page.
unlockBtn.addEventListener('click', function () {
var pseudo = (document.getElementById('g1wallet-pseudo') || {}).value || '';
var password = (document.getElementById('g1wallet-password') || {}).value || '';
_clearError();
if (!pseudo || !password) {
_showError('Both pseudo and password are required.');
return;
}
// TODO: call scrypt derivation here.
// scrypt(pseudo, password, N=4096, r=16, p=1) → 32-byte seed → Ed25519 keypair
// For now: log and show a placeholder message.
console.log('[g1wallet] Unlock clicked. Derivation not yet implemented.');
if (spinner) spinner.style.display = 'inline';
unlockBtn.disabled = true;
// Simulate async derivation (placeholder).
setTimeout(function () {
if (spinner) spinner.style.display = 'none';
unlockBtn.disabled = false;
_showError('Key derivation not yet implemented. This is a skeleton build.');
}, 500);
});
if (lockBtn) {
lockBtn.addEventListener('click', function () {
_lockWallet();
if (lockedView) lockedView.style.display = '';
if (unlockedView) unlockedView.style.display = 'none';
// Clear credential fields.
var pseudoEl = document.getElementById('g1wallet-pseudo');
var passwordEl = document.getElementById('g1wallet-password');
if (pseudoEl) pseudoEl.value = '';
if (passwordEl) passwordEl.value = '';
});
}
// Listen for wallet:unlocked event (dispatched by _unlockWallet below).
window.addEventListener('g1wallet:unlocked', function (e) {
if (lockedView) lockedView.style.display = 'none';
if (unlockedView) unlockedView.style.display = '';
var pubkeyDisplay = document.getElementById('g1wallet-pubkey-display');
if (pubkeyDisplay) pubkeyDisplay.textContent = e.detail.pubkey || '—';
});
});
// ------------------------------------------------------------------------
// INTERNAL HELPERS
// ------------------------------------------------------------------------
function _unlockWallet(pubkey, cryptoKey) {
_session.unlocked = true;
_session.pubkey = pubkey;
_session._cryptoKey = cryptoKey;
window.dispatchEvent(new CustomEvent('g1wallet:unlocked', {
detail: { pubkey: pubkey }
}));
}
function _lockWallet() {
_session.unlocked = false;
_session.pubkey = null;
_session._cryptoKey = null;
window.dispatchEvent(new CustomEvent('g1wallet:locked'));
}
function _showError(message) {
var el = document.getElementById('g1wallet-unlock-error');
if (el) {
el.textContent = message;
el.style.display = '';
}
}
function _clearError() {
var el = document.getElementById('g1wallet-unlock-error');
if (el) {
el.textContent = '';
el.style.display = 'none';
}
}
function _escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ------------------------------------------------------------------------
// AUTO-POPULATE: cry01 and poll01 form fields
// Other addons place data-g1wallet-target="pubkey" on their pubkey input fields.
// When the wallet unlocks, those fields are populated automatically.
// ------------------------------------------------------------------------
window.addEventListener('g1wallet:unlocked', function (e) {
var pubkey = e.detail.pubkey || '';
var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]');
targets.forEach(function (el) {
el.value = pubkey;
});
});
window.addEventListener('g1wallet:locked', function () {
var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]');
targets.forEach(function (el) {
el.value = '';
});
});
}());