diff --git a/assets/app.js b/assets/app.js
index 86b2951..42cbfad 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -57,9 +57,8 @@ async function render() {
if (data) { const img = new Image(); img.src = data; img.style.cssText = 'width:100%;height:100%;object-fit:contain'; cv.replaceWith(img); }
}
}
-(async () => {
- PARTS = await (await fetch('/assets/catalog.json')).json();
- $('#cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
+window.ppcbReady(async () => {
+ PARTS = await window.getCatalog(); // authed — only reachable once signed in
renderChips(); render();
let t; $('#q').addEventListener('input', () => { clearTimeout(t); t = setTimeout(render, 160); });
-})();
+});
diff --git a/assets/auth.js b/assets/auth.js
new file mode 100644
index 0000000..62719a1
--- /dev/null
+++ b/assets/auth.js
@@ -0,0 +1,86 @@
+// auth.js — gates the whole site behind a PromptPCB account (Corben tenant auth for this site).
+// Nothing loads until you're signed in; the catalog + file URLs come only from the authed function.
+(function () {
+ var API = 'https://api.corben.world';
+ var SITE = '6ab5ec79-8b91-453d-aacc-5be76f9271d1'; // this site IS the tenant
+ var FN_CATALOG = '0f3cd09e-81ce-43f4-88e7-cde3d91f122e';
+ var TK_CATALOG = '687d2466cea418815433b60946d6742a';
+ var LS = 'ppcb_token';
+ window.PPCB = { token: null, user: null };
+
+ window.ppcbReady = function (fn) {
+ if (window.PPCB.token) fn(); else addEventListener('ppcb:auth', function () { fn(); }, { once: true });
+ };
+ window.getCatalog = async function () {
+ var r = await fetch(API + '/functions/' + FN_CATALOG + '/invoke?token=' + TK_CATALOG, {
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.PPCB.token }, body: '{}' });
+ if (r.status === 401) { logout(); throw new Error('session expired'); }
+ var d = await r.json(); return d.parts || [];
+ };
+ window.ppcbAuth = function () { return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.PPCB.token || '') }; };
+
+ function authed(user) {
+ window.PPCB.user = user; document.documentElement.classList.add('ppcb-authed');
+ var g = document.getElementById('ppcb-gate'); if (g) g.style.display = 'none';
+ var c = document.querySelector('.credits');
+ if (c) c.innerHTML = '◆ ' + (user.display_name || user.email.split('@')[0]) + ' · sign out';
+ var lo = document.getElementById('ppcb-logout'); if (lo) lo.onclick = function (e) { e.preventDefault(); logout(); };
+ dispatchEvent(new Event('ppcb:auth'));
+ }
+ function logout() { localStorage.removeItem(LS); location.reload(); }
+ window.ppcbLogout = logout;
+
+ async function check() {
+ var t = localStorage.getItem(LS);
+ if (!t) return gate();
+ try {
+ var r = await fetch(API + '/auth/tenant/me', { headers: { Authorization: 'Bearer ' + t } });
+ if (!r.ok) return gate();
+ var u = await r.json(); window.PPCB.token = t; authed(u);
+ } catch (e) { gate(); }
+ }
+
+ function gate() {
+ document.documentElement.classList.remove('ppcb-authed');
+ if (document.getElementById('ppcb-gate')) { document.getElementById('ppcb-gate').style.display = 'flex'; return; }
+ var el = document.createElement('div'); el.id = 'ppcb-gate';
+ el.innerHTML =
+ '
' +
+ '
PromptPCB / parts
' +
+ '
Sign in to the part library
' +
+ '
One PromptPCB account. Every part—real specs, 3D, SPICE—once you’re in.
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
';
+ document.body.appendChild(el);
+ var mode = 'login';
+ el.querySelectorAll('.gate-tabs button').forEach(function (b) {
+ b.onclick = function () {
+ mode = b.dataset.m; el.querySelectorAll('.gate-tabs button').forEach(function (x) { x.classList.toggle('on', x === b); });
+ document.getElementById('g-name').style.display = mode === 'signup' ? 'block' : 'none';
+ document.getElementById('g-go').textContent = mode === 'signup' ? 'Create account' : 'Sign in';
+ document.getElementById('g-msg').textContent = '';
+ };
+ });
+ document.getElementById('g-go').onclick = go;
+ el.addEventListener('keydown', function (e) { if (e.key === 'Enter') go(); });
+ async function go() {
+ var email = document.getElementById('g-email').value.trim(), pass = document.getElementById('g-pass').value;
+ var msg = document.getElementById('g-msg'); msg.style.color = 'var(--dim)'; msg.innerHTML = ' …';
+ var path = mode === 'signup' ? '/auth/tenant/register' : '/auth/tenant/login';
+ var body = { tenant_id: SITE, email: email, password: pass };
+ if (mode === 'signup') body.display_name = document.getElementById('g-name').value.trim();
+ try {
+ var r = await fetch(API + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ var d = await r.json();
+ if (!r.ok) { msg.style.color = 'var(--bad,#ff6b6b)'; msg.textContent = d.error || 'failed'; return; }
+ localStorage.setItem(LS, d.token); window.PPCB.token = d.token; authed(d.user);
+ } catch (e) { msg.style.color = '#ff6b6b'; msg.textContent = 'network error'; }
+ }
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', check); else check();
+})();
diff --git a/assets/part.js b/assets/part.js
index 0418faf..0f383a3 100644
--- a/assets/part.js
+++ b/assets/part.js
@@ -49,11 +49,10 @@ function schematicSVG(p) {
return s + '';
}
-(async () => {
- const parts = await (await fetch('/assets/catalog.json')).json();
+window.ppcbReady(async () => {
+ const parts = await window.getCatalog();
const p = parts.find((x) => x.id === id);
const root = document.getElementById('root');
- document.getElementById('cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
if (!p) { root.innerHTML = ''; return; }
document.title = `${p.mpn || p.id} — PromptPCB Parts`;
@@ -105,7 +104,6 @@ function schematicSVG(p) {
show('d3');
document.getElementById('dl-all').onclick = async () => {
- if (!spendCredit()) return;
try { const wrl = await (await fetch(p.model_3d)).text(); dl(`${p.id}.wrl`, wrl); } catch {}
dl(`${p.id}.cir`, `* ${p.mpn} SPICE model — PromptPCB Parts\n${p.spice || ''}\n`);
dl(`${p.id}.json`, JSON.stringify(p, null, 2), 'application/json');
@@ -115,4 +113,4 @@ function schematicSVG(p) {
if (!lib.includes(p.id)) { lib.push(p.id); localStorage.setItem('ppcb_lib', JSON.stringify(lib)); }
document.getElementById('lib').textContent = '✓ In library';
};
-})();
+});
diff --git a/assets/style.css b/assets/style.css
index d198916..adf48bf 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -152,3 +152,21 @@ body{animation:pagein .5s ease both}
.vizpanel .hint{position:absolute;bottom:12px;left:12px;font-family:var(--mono);font-size:11px;color:#555}
.viznote{font-family:var(--mono);font-size:12px;color:var(--dim);margin-top:12px;line-height:1.5}
.viznote b{color:var(--acc)}
+
+/* login gate */
+html:not(.ppcb-authed) body{overflow:hidden}
+#ppcb-gate{position:fixed;inset:0;z-index:200;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:24px}
+.gate-card{width:100%;max-width:400px;display:flex;flex-direction:column}
+.gate-brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:20px;margin-bottom:30px}
+.gate-brand .mk{width:13px;height:13px;background:var(--acc);border-radius:2px}
+.gate-brand small{font-family:var(--mono);font-size:12px;color:var(--dim);font-weight:400}
+.gate-h{font-size:29px;font-weight:700;letter-spacing:-.02em;margin-bottom:8px;line-height:1.1}
+.gate-sub{color:var(--dim);font-size:15px;line-height:1.5;margin-bottom:26px}
+.gate-tabs{display:flex;border:1px solid var(--line2);border-radius:2px;width:fit-content;overflow:hidden;margin-bottom:18px}
+.gate-tabs button{background:transparent;border:none;border-right:1px solid var(--line2);color:var(--dim);font-family:var(--mono);font-size:13px;padding:8px 18px;cursor:pointer}
+.gate-tabs button:last-child{border-right:none}
+.gate-tabs button.on{background:var(--acc);color:var(--acc-ink);font-weight:600}
+#ppcb-gate input{background:var(--panel);border:1px solid var(--line2);border-radius:2px;color:var(--text);padding:14px 15px;font-size:15px;font-family:var(--display);outline:none;margin-bottom:12px}
+#ppcb-gate input:focus{border-color:var(--acc)}
+.gate-go{width:100%;justify-content:center;margin-top:4px}
+.gate-msg{font-family:var(--mono);font-size:13px;margin-top:14px;min-height:18px}
diff --git a/assets/submit.js b/assets/submit.js
index 136b632..c858b17 100644
--- a/assets/submit.js
+++ b/assets/submit.js
@@ -6,11 +6,9 @@ const FN_SUBMIT = 'b02d634b-cd6d-414d-bf9c-80cf6cc2ccdc';
const TK_SUBMIT = '9c3ed2ece766bcc18ed7d33996bfeb86';
const $ = (id) => document.getElementById(id);
-$('cred-n').textContent = localStorage.getItem('ppcb_credits') || '5';
-
async function invoke(fn, token, payload) {
const r = await fetch(`${API}/functions/${fn}/invoke?token=${token}`, {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
+ method: 'POST', headers: window.ppcbAuth(), body: JSON.stringify(payload) }); // authed: sends the session token
return r.json();
}
@@ -57,7 +55,6 @@ $('submit').onclick = async () => {
try {
const d = await invoke(FN_SUBMIT, TK_SUBMIT, { email, part });
if (!d.ok) throw new Error(d.error || 'submit failed');
- localStorage.setItem('ppcb_credits', d.credits); $('cred-n').textContent = d.credits;
document.querySelector('.form').innerHTML = `Thank you — submitted for review ✓
Your part ${part.mpn} is queued for curation review. +5 download credits added — you now have ${d.credits}.
Ref: ${d.sub_id}
Browse the catalog →`;
diff --git a/index.html b/index.html
index 8c10505..8c2513c 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
+