@hazem3500/komo 0.9.3 → 0.9.5
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.
- package/NODE.md +1 -1
- package/README.md +83 -8
- package/cli/agent.mjs +85 -18
- package/dist/adapters/css-variables.d.ts +9 -0
- package/dist/adapters/css-variables.js +1 -0
- package/dist/adapters/react-fiber.d.ts +27 -0
- package/dist/adapters/react-fiber.js +1 -0
- package/dist/{chunk-SOY4SE7F.js → chunk-NWRSH6AL.js} +1 -1
- package/dist/design-mode.js +1 -1
- package/dist/design-system.d.ts +12 -3
- package/dist/index.js +1 -1
- package/dist/{inline-text-editor-2J7DVJQY.js → inline-text-editor-JWVS7J75.js} +1 -1
- package/dist/node.mjs +513 -18
- package/dist/postgres/004_personal_access_tokens.sql +25 -0
- package/dist/react-props-panel-BW4ZQPD7.js +1 -0
- package/dist/react-props-panel.d.ts +12 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/setup.js +1 -1
- package/package.json +14 -3
- package/server/account-page.ts +231 -0
- package/server/index.ts +62 -5
- package/server/migrations/0018_personal_access_tokens.sql +25 -0
- package/server/secrets.d.ts +2 -0
- package/server/setup-client.txt +43 -43
- package/server/tokens.ts +324 -0
- package/server/workspaces.ts +2 -0
- package/dist/react-props-panel-YJOE56ZS.js +0 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
CREATE TABLE personal_access_tokens (
|
|
2
|
+
id text PRIMARY KEY,
|
|
3
|
+
user_id text NOT NULL REFERENCES users(id),
|
|
4
|
+
name text NOT NULL,
|
|
5
|
+
last4 text NOT NULL,
|
|
6
|
+
salt text NOT NULL,
|
|
7
|
+
token_hash text NOT NULL,
|
|
8
|
+
scopes text NOT NULL,
|
|
9
|
+
projects text,
|
|
10
|
+
created_at bigint NOT NULL,
|
|
11
|
+
expires_at bigint,
|
|
12
|
+
last_used_at bigint,
|
|
13
|
+
revoked_at bigint
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX personal_access_tokens_user ON personal_access_tokens(user_id, created_at);
|
|
16
|
+
CREATE TABLE audit_log (
|
|
17
|
+
id text PRIMARY KEY,
|
|
18
|
+
at bigint NOT NULL,
|
|
19
|
+
actor text NOT NULL,
|
|
20
|
+
action text NOT NULL,
|
|
21
|
+
target text NOT NULL,
|
|
22
|
+
detail text NOT NULL
|
|
23
|
+
);
|
|
24
|
+
CREATE INDEX audit_log_at ON audit_log(at);
|
|
25
|
+
CREATE INDEX audit_log_actor ON audit_log(actor, at);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as e,d as t,e as n,g as o,h as i,i as r,j as a,k as s,l,m as d,n as p,o as c,p as u,q as f,r as g,s as m,t as h,u as b,v as k,w as v,x as y,y as x}from"./chunk-JXIYWNFL.js";import"./chunk-I7AUKTXE.js";var w=new Map,E=new WeakMap,$=!1;function C(e){let t={};for(let n of e?.themes??[])try{t[n.id]=n.current()}catch{}return t}function S(e,t={}){let n=E.get(e);n||E.set(e,n=new Map);let o=JSON.stringify(Object.entries(t).sort()),i=n.get(o);return i||(i=Promise.resolve().then(()=>e.list(Object.keys(t).length?t:void 0)).then(e=>Array.isArray(e)?e.filter(e=>e&&"string"==typeof e.path):[]),i.catch(()=>n.delete(o)),n.set(o,i)),i}function P(e,t){return Promise.resolve().then(()=>e.resolve(t)).then(e=>e??{},()=>({}))}function A(e){let t=e.trim().toLowerCase().replace(/\s+/g,""),n=/^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.exec(t)?.[1];if(!n)return t.replace(/^rgba\((\d+,\d+,\d+),1\)$/,"rgb($1)");let o=n.length<5?[...n].map(e=>e+e).join(""):n,[i,r,a,s=255]=o.match(/../g).map(e=>parseInt(e,16));return 255===s?`rgb(${i},${r},${a})`:`rgba(${i},${r},${a},${+(s/255).toFixed(3)})`}function N(e,t){let n=A(t);return n?e.find(e=>("color"===e.type||"dimension"===e.type)&&A(e.value)===n):void 0}function M(e){return e.label||e.path}function T(e){return[e.path,e.ref,e.value,e.states?.length?`States: ${e.states.join(", ")}`:"",e.description].filter(Boolean).join(" · ")}function L(){let e=document.createElement("span");return e.className="design-token-off",e.setAttribute("role","img"),e}function j(e,t,n){e.title=`Not from a token · ${t||"unset"}${n?` · matches ${M(n)}`:""}`,e.setAttribute("aria-label",e.title)}function z(e,t,n,o){try{if(e.preview)return e.preview(t,n,o)??(()=>{})}catch{return()=>{}}return function(e,t,n){let o=e.style;if(!o)return()=>{};let i=o.getPropertyValue(t),r=o.getPropertyPriority(t);return o.setProperty(t,n,r),()=>{i?o.setProperty(t,i,r):o.removeProperty(t)}}(t,n,o.value)}function q(e){let t=w.get(e);t?.pop()?.undo(),t&&!t.length&&w.delete(e)}function O(e){return!!w.get(e)?.length}function R(e){for(;w.has(e);)q(e)}function D(e){return[...new Set(w.get(e)?.map(e=>e.property))]}function V(e,t){try{let n=t.adapter.describe?.(e,t.property,t.from,t.token);return n?String(n).replace(/\s+/g," ").replace(/→/g,"->").trim().slice(0,300):void 0}catch{return}}function B(e){let t=[],n=new Set;for(let o of function(e){let t=new Map;for(let n of w.get(e)??[])t.set(n.property,n);return[...t.values()].filter(e=>!("path"in e.from)||e.from.path!==e.token.path)}(e)){n.add(o.adapter.id);let i="path"in o.from?`${o.fromToken?.value?`${o.fromToken.value} `:""}[${o.from.path}]`:o.from.offToken||"(unset)";t.push(`Element ${o.property}: ${i} → ${o.token.value} [${o.token.path}]`);let r=V(e,o);r&&t.push(`Apply: ${r}`)}if(t.length)return["Proposed design-token changes (preview only)",...t,`Token adapter: ${[...n].join(", ")}`].join("\n")}var H=new Map,I=new Map,U=new WeakSet,_=new WeakMap;function F(e){return void 0===e?"unset":JSON.stringify(e)??"unset"}var W=new Map;function J(e){if(e)for(let[e,n]of H){let o=t(e);o&&(W.set(e,o.props),o.setProps({...o.props,...Object.fromEntries(n)}))}else for(let[e,n]of W)t(e)?.setProps(n);e||W.clear(),s(e),function(e){if(e!==$){$=e;for(let[t,n]of w)if(e)for(let e of[...n].reverse())e.undo();else for(let e of n)e.undo=z(e.adapter,t,e.property,e.token)}}(e)}function K(e){return!!H.get(e)?.size}function X(e){let n=t(e),o=n?.definition.textProp,i=n?.definition.props.find(e=>e.name===o);if(!n||!o||"string"!==i?.kind)return null;let r=n.props[o];if("string"!=typeof r||r.length>500)return null;let a=H.get(e);return a||H.set(e,a=new Map),a.has(o)||a.set(o,r),{propName:o,before:r}}function Y(n,o,i){let r=t(n),a=r?.definition.props.find(e=>e.name===o);return!(!r||r.definition.textProp!==o||"string"!==a?.kind||i.length>500||!e(a,i))&&(r.setProps({...r.props,[o]:i}),!0)}function G(e,t,n,o){if(n===o){let o=H.get(e);return void(o?.get(t)===n&&!(I.get(e)??[]).some(e=>e.propName===t)&&(o.delete(t),o.size||H.delete(e)))}let i=I.get(e);i||I.set(e,i=[]),i.push({propName:t,before:n,after:o})}function Q(e,t,n){Y(e,t,n);let o=H.get(e);o?.get(t)===n&&!(I.get(e)??[]).some(e=>e.propName===t)&&(o.delete(t),o.size||H.delete(e))}function Z(e){return!!e&&!!I.get(e)?.length}function ee(e){let n=H.get(e),i=t(e);if(!n?.size||!i)return;let r=o(e),a=[...n].map(([e,t])=>{let n=i.definition.props.find(t=>t.name===e)?.styleProperty,o=i.props[e],a=n&&"string"==typeof o?r?.tokens.find(e=>e.properties.includes(n)&&o===`var(${e.cssVariable})`):void 0;return`- ${e}: ${F(t)} → ${F(o)}${a?` [${a.path}]`:" [custom value]"}`});return["Proposed React prop changes (preview only)",`Component: ${i.definition.name}`,`Source: ${i.definition.source}:${i.definition.line}`,...a,"Preview values use the app's React state callback; Komo does not write source files."].join("\n")}function te(e,o){if(t(e))return null;let i;try{i=o?.components?.identify(e)}catch{}let r=n(e);return i?.name&&(!r||r.root!==i.element&&r.root.contains(i.element))?i:null}var ne=e=>String(e).replace(/\s+/g," ").trim();function oe(e,o){let i=Object.entries(C(o)).map(([e,t])=>`${ne(e)}=${ne(t)}`).join(", "),r=te(e,o),a=t(e),s=a?{root:e,component:a}:r?void 0:n(e);if(s){let{definition:e,props:t}=s.component;r={name:e.name,source:`${e.source}:${e.line}`,props:{...t,...Object.fromEntries(H.get(s.root)??[])}}}let l=Object.entries(r?.props??{}).filter(([,e])=>null===e||["boolean","number"].includes(typeof e)||"string"==typeof e&&e.length<=80).slice(0,12).map(([e,t])=>`${ne(e)}=${JSON.stringify(t).replace(/→/g,"\\u2192")}`);return[i&&`Theme: ${i}`,r?.name&&`Component: ${ne(r.name)}${r.source?` (${ne(r.source)})`:""}`,l.length&&`Props: ${l.join(", ")}`].filter(e=>!!e)}function ie(e){return[ee(e),x(e),B(e)].filter(e=>!!e).join("\n\n")||void 0}function re(e,n,o){_.get(e)?.abort();let i=new AbortController;_.set(e,i),e.addEventListener("click",e=>{let i=n();if(!i)return;let r=x(i)||O(i);!t(i)&&!r||(e.stopImmediatePropagation(),o(i,ie(i)))},{capture:!0,signal:i.signal})}function ae(e){return b(e)}function se(e){return h(e)||O(e)}function le(e){k(e)}function de(e){v(e),R(e)}function pe(e){if(!e||U.has(e))return;let t=document.createElement("style");t.textContent=".design-react-source{display:block;padding:0 0 8px;color:#77736f;font-size:10px;overflow-wrap:anywhere}.design-react-inside{padding:10px 12px}.design-react-inside button{padding:0;border:0;background:none;color:#bda6ef;font:inherit;cursor:pointer}.design-react-inside button:hover{text-decoration:underline}.design-react-field.design-field{display:grid;grid-template-columns:76px minmax(0,1fr);gap:0 8px;height:auto;min-height:28px;margin-top:4px;background:none;box-shadow:none;color:#898680;font-size:11px}.design-react-field>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.design-react-field.design-field>:is(input,select){box-sizing:border-box;width:100%;min-width:0;height:28px;padding:0 8px;border:0;border-radius:6px;background-color:#ffffff0a;color:#f1efeb;font:inherit;font-size:11px}.design-react-field.design-field>:is(input,select):focus-visible{outline:1px solid #bda6ef;outline-offset:-1px}.design-react-field.design-field>select{padding-right:20px}.design-react-field input[type=checkbox]{justify-self:end;width:14px;height:14px;accent-color:#bda6ef}.design-react-field input:disabled{opacity:.55;cursor:not-allowed}.design-react-type{display:none}.design-react-field .design-token-binding{grid-column:2;display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:6px;margin-top:4px;color:#77736f;font-size:10px}.design-react-field .design-token-binding select{min-width:0;height:24px;border:0;border-radius:6px;appearance:none;background:#ffffff0a url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 3 4 5.5 6.5 3' fill='none' stroke='%23898680' stroke-width='1.2'/%3E%3C/svg%3E\") right 6px center/8px no-repeat;color:#e9e6e1;padding:0 18px 0 6px;font:inherit;font-size:10px;cursor:pointer}.design-react-field .design-token-binding[data-mode=token] select{background-color:#bda6ef24;color:#e6dcfb}.design-token-binding__state{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.design-field>.design-token-binding{display:contents}.design-field:has(>.design-token-binding)>input[data-property]{padding-right:28px}.design-token-icon,.design-token-detach,.design-field>.design-token-binding select{position:absolute;top:4px;right:4px;width:20px;height:20px;box-sizing:border-box;border-radius:5px}.design-token-icon{display:grid;place-items:center;color:#77736f;opacity:0;pointer-events:none;transition:opacity .12s}.design-token-icon::before{content:'';width:6px;height:6px;border:1.2px solid currentColor;border-radius:1.5px;rotate:45deg}.design-field:is(:hover,:focus-within) .design-token-icon{opacity:1}.design-field>.design-token-binding select{margin:0;padding:0;border:0;opacity:0;cursor:pointer;appearance:none}.design-field>.design-token-binding select:hover+.design-token-icon,.design-field:has(.design-token-binding select:hover) .design-token-icon{background:#ffffff14;color:#e9e6e1}.design-field:has(.design-token-binding select:focus-visible) :is(.design-token-icon,.design-token-chip){outline:2px solid #bda6ef;outline-offset:1px;opacity:1}.design-field[data-token-mode=override] .design-token-icon{opacity:1;color:#e7c27d}.design-token-chip,.design-token-detach{display:none}.design-field:is([data-token-mode=token],[data-token-mode=connected])>input[data-property]{visibility:hidden}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-chip{position:absolute;inset:4px 28px 4px var(--lead,4px);display:flex;align-items:center;gap:5px;padding:0 6px;border-radius:4px;background:#bda6ef1f;color:#e6dcfb;font-size:11px;white-space:nowrap;overflow:hidden;pointer-events:none}.design-token-chip::before{content:'';flex:none;width:5px;height:5px;border:1.2px solid #c8b5f4;border-radius:1px;rotate:45deg}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-binding select{left:var(--lead,4px);right:28px;width:auto}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-icon{display:none}.design-field:is([data-token-mode=token],[data-token-mode=connected]) .design-token-detach{display:grid;place-items:center;color:#898680}.design-token-detach:hover{background:#ffffff14;color:#f1efeb}.design-token-detach svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.2;stroke-linecap:round}.design-theme-group .design-react-source{padding:0 0 6px}.design-theme-heading{display:block;padding:8px 0 2px;color:#77736f;font-size:10px;text-transform:capitalize}.design-theme-token{position:relative;display:grid;grid-template-columns:96px minmax(0,1fr) 20px;align-items:center;gap:6px;margin-top:4px}.design-theme-token>span{overflow:hidden;color:#898680;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.design-theme-token[data-changed]>span{color:#e6dcfb}.design-theme-token[data-changed]::before{content:'';position:absolute;top:calc(50% - 2px);left:-8px;width:4px;height:4px;border-radius:50%;background:#bda6ef}.design-theme-reset{display:grid;place-items:center;width:20px;height:20px;padding:0;border:0;border-radius:5px;background:none;color:#77736f;cursor:pointer}.design-theme-reset:disabled{visibility:hidden}.design-theme-reset:hover{background:#ffffff14;color:#f1efeb}.design-theme-reset svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}.design-token-off{position:absolute;top:8px;right:24px;display:grid;place-items:center;width:12px;height:12px}.design-token-off::before{content:'';width:4px;height:4px;border-radius:50%;background:#e7c27d;opacity:.7}.design-token-off[hidden]{display:none}.design-field:has(>.design-token-binding>.design-token-off:not([hidden]))>input[data-property]{padding-right:38px}.design-theme-switch{padding:6px 12px 8px}.design-theme-switch .design-react-field>span{text-transform:capitalize}.design-theme-options{display:flex;min-width:0;padding:2px;border-radius:6px;background:#ffffff0a}.design-theme-options button{flex:1;min-width:0;height:24px;padding:0 6px;border:0;border-radius:4px;background:none;color:#898680;font:inherit;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.design-theme-options button:hover{color:#f1efeb}.design-theme-options button[aria-pressed=true]{background:#ffffff1a;color:#f1efeb}.design-theme-options button:focus-visible{outline:1px solid #bda6ef;outline-offset:-1px}@media (max-width:520px){.design-theme-options button{height:30px}.design-react-field.design-field>:is(input,select){height:34px}.design-token-icon,.design-token-detach,.design-field>.design-token-binding select{top:7px}.design-token-off{top:11px}}",e.append(t),U.add(e)}function ce(e,t,n,r){let a=o(e);if(!a)return null;let s=m(a,t);if(!s.length)return null;pe(r);let l=document.createElement("span");l.className="design-token-binding",l.setAttribute("data-mode","custom");let c=document.createElement("span");c.className="design-token-icon",c.setAttribute("aria-hidden","true");let h=document.createElement("span");h.className="design-token-chip",h.setAttribute("aria-hidden","true");let b=document.createElement("button");b.type="button",b.className="design-token-detach",b.title="Detach token",b.setAttribute("aria-label",`Detach token from ${t}`),b.innerHTML='<svg viewBox="0 0 12 12"><path d="M5 3.5 6.5 2a2.1 2.1 0 0 1 3 3L8 6.5M7 8.5 5.5 10a2.1 2.1 0 0 1-3-3L4 5.5M2 2l8 8"/></svg>';let k=document.createElement("select");k.setAttribute("aria-label",`Style token for ${t}`);let v=document.createElement("small");v.className="design-token-binding__state",v.setAttribute("aria-live","polite");let y=`komo-token-${t}-${Math.random().toString(36).slice(2,8)}`;v.id=y,k.setAttribute("aria-describedby",y);let x=document.createElement("option");x.value="custom",x.textContent="Custom value",k.append(x);for(let e of s){let t=document.createElement("option"),n=i(a,e);t.value=e.path,t.textContent=n?`${e.path} · ${n}`:e.path,k.append(t)}let w=L(),E=()=>{let n=u(a,e,t),o=n?null:f(a,e,t),r="token"===n?.mode?s.find(e=>e.path===n.tokenPath):void 0,d=r??o?.token;k.value=d?.path??"custom",h.textContent=d?.path??"";let p=n?.declaredPath,c=r?"token":o?"connected":p?"override":"custom";if(l.dataset.mode=c,l.parentElement?.setAttribute("data-token-mode",c),v.textContent="token"===c?p&&p!==r?.path?`Rebound from ${p} · preview`:`Linked to ${r?.path} · preview`:"connected"===c?"Connected "+("inline"===o?.origin?"inline":"from stylesheet"):"override"===c?`Custom override · was ${p}`:"",k.title=v.textContent||"Choose a token",w.hidden="custom"!==c,"custom"===c){let n=getComputedStyle(e).getPropertyValue(t).trim();j(w,n,N(s.map(e=>({path:e.path,type:e.type,value:i(a,e)})),n))}},$=new MutationObserver(E);$.observe(e,{attributes:!0,attributeFilter:["style"]});let C=new MutationObserver(()=>{l.isConnected||($.disconnect(),C.disconnect())});return C.observe(l.ownerDocument,{childList:!0,subtree:!0}),k.addEventListener("change",()=>{let o=e.style.getPropertyValue(t),i=s.find(e=>e.path===k.value),r=u(a,e,t);if(i&&r&&i.path===r.declaredPath)g(a,e,t),n(o,e.style.getPropertyValue(t),i.path);else if(i)(i.path!==f(a,e,t)?.token.path||r)&&d(a,e,i,t,{recordHistory:!1})&&n(o,e.style.getPropertyValue(t),i.path);else{let i=l.parentElement?.querySelector("[data-property]")?.value??getComputedStyle(e).getPropertyValue(t).trim(),r=/^-?[\d.]+$/.test(i.trim())&&!/weight|line/.test(t)?`${i.trim()}px`:i;p(a,e,t,r),e.style.setProperty(t,r),n(o,r)}E()}),b.addEventListener("click",()=>{let e=s.find(e=>e.path===k.value),t=l.parentElement?.querySelector("input[data-property]"),n=e&&i(a,e);t&&n&&(t.value="number"===t.type?String(parseFloat(n)):n),k.value="custom",k.dispatchEvent(new Event("change")),l.parentElement?.querySelector("[data-property]")?.focus()}),l.append(h,k,c,b,w,v),queueMicrotask(E),E(),l}function ue(e){let t=o(e);return new Set([...t?.elementBindings.get(e)?.keys()??[],...D(e)])}function fe(e,t,n,o,i,r,a,s,l){let d=t.flatMap(e=>{let t=[...n.querySelectorAll("[data-field-property]")].find(t=>t.dataset.fieldProperty===e.property),o=t?.querySelector("[data-property]");return t&&o?[{field:e,label:t,input:o}]:[]}),p=t=>{for(let{field:o,label:i,input:s}of d){if(i.querySelector(".design-token-binding")||!n.contains(i))continue;let l=t(o,i,s,()=>{r&&(s.value=r(o,e));let t=i.querySelector('input[type="range"],input[type="color"]');t&&(t.value="color"===o.kind&&a?a(s.value):s.value)});l&&i.append(l)}},c=s?.tokens;if(c)return pe(i),Promise.all([S(c,C(s)),P(c,e)]).then(([t,n])=>{p((o,i,r,a)=>ge(c,e,o.property,t,n[o.property],e=>{l?.(e),a()},i,r))},()=>{});p((t,n,r,a)=>ce(e,t.property,e=>{o(t,r,e),a()},i))}function ge(e,t,n,o,i,r,a,s){let l=function(e,t){return e.filter(e=>e.properties?.includes(t))}(o,n);if(!l.length&&!i)return null;let d=i&&"offToken"in i?i.offToken:void 0,p=void 0===d?void 0:N(l,d),c=document.createElement("span");c.className="design-token-binding";let u=document.createElement("span");u.className="design-token-chip",u.setAttribute("aria-hidden","true");let f=document.createElement("span");f.className="design-token-icon",f.setAttribute("aria-hidden","true");let g=document.createElement("button");g.type="button",g.className="design-token-detach",g.title="Edit the value",g.setAttribute("aria-label",`Edit ${n} value`),g.innerHTML='<svg viewBox="0 0 12 12"><path d="M5 3.5 6.5 2a2.1 2.1 0 0 1 3 3L8 6.5M7 8.5 5.5 10a2.1 2.1 0 0 1-3-3L4 5.5M2 2l8 8"/></svg>';let m=document.createElement("select");m.setAttribute("aria-label",`Style token for ${n}`),m.disabled=!l.length;let h=document.createElement("option");h.value="",h.textContent="Choose a token",m.append(h);let b=new Map([["",m]]);for(let e of l){let t=e.group??"",n=b.get(t);n||(n=document.createElement("optgroup"),n.label=t,b.set(t,n));let o=document.createElement("option");o.value=e.path,o.textContent=`${M(e)}${e.ref?` · ${e.ref}`:""}${e===p?" · same value":""}`,o.title=T(e),n.append(o)}for(let[e,t]of b)e&&m.append(t);let k=L();void 0!==d&&j(k,d,p);let v=document.createElement("small");v.className="design-token-binding__state",v.setAttribute("aria-live","polite");let y=!1,x=()=>{let e=function(e,t){return[...w.get(e)??[]].reverse().find(e=>e.property===t)?.token}(t,n),r=y?void 0:e?.path??(i&&"path"in i?i.path:void 0),s=r?o.find(e=>e.path===r):void 0;m.value=r&&l.some(e=>e.path===r)?r:"",u.textContent=s?M(s):r??"";let p=r?e?"token":"connected":"custom";c.dataset.mode=p,(a??c.parentElement)?.setAttribute("data-token-mode",p),k.hidden=void 0===d||!!e,v.textContent=s?`${e?"Preview":"Uses"} ${T(s)}`:r?`Uses ${r}`:k.hidden?"":k.title,m.title=v.textContent||"Choose a token"};return m.addEventListener("change",()=>{let a=l.find(e=>e.path===m.value);if(!a)return x();y=!1;let s=i??{offToken:getComputedStyle(t).getPropertyValue(n).trim()},d="path"in s?o.find(e=>e.path===s.path):void 0;r(function(e,t,n,o,i,r){let a=w.get(t);a||w.set(t,a=[]);let s=a.find(e=>e.property===n);return a.push({adapter:e,property:n,token:o,from:s?.from??i,fromToken:s?s.fromToken:r,undo:z(e,t,n,o)}),()=>q(t)}(e,t,n,a,s,d)),x()}),g.addEventListener("click",()=>{y=!0,x(),s?.focus()}),c.append(u,m,f,g,k,v),x(),c}var me=!1;function he(e,t,n,i){let r=o(e);if(!r||!r.tokens.length)return null;pe(i);let a=document.createElement("details");a.className="design-group design-theme-group",a.open=me,a.addEventListener("toggle",()=>{a.isConnected&&(me=a.open)});let s=document.createElement("summary");s.textContent="Shared theme";let l=document.createElement("small");l.className="design-react-source",l.textContent="Changes every element using these tokens",a.append(s,l);let d=new Map;for(let e of r.tokens){let t=e.path.split(".")[0];d.set(t,[...d.get(t)??[],e])}let p=r.tokens.length>6&&d.size>1;for(let[e,o]of p?d:new Map([["",r.tokens]])){if(e){let t=document.createElement("small");t.className="design-theme-heading",t.textContent=e,a.append(t)}for(let i of o)a.append(be(r,i,t,n,e))}return a}function be(e,t,n,o,s){let d=document.createElement("div");d.className="design-theme-token";let p=document.createElement("span");p.textContent=s&&t.path.slice(s.length+1)||t.path,p.title=`${t.path} · ${r(e,t)} matching · ${t.impactSelector}`;let c=n.fields.find(e=>t.properties.includes(e.property)),u={property:t.cssVariable,label:`Shared theme value for ${t.path}`,kind:"dimension"===t.type?"size":c?.kind??"text",glyph:"color"===t.type?void 0:c?.glyph,values:c?.values},f=!1,g=n.fieldControl(u,{element:e.root,begin:()=>{f=!1},apply:(n,i,r)=>{let s=null!==r&&a(e,t,r.trim(),f);n.setAttribute("aria-invalid",String(!s)),s&&(f=!0,k(),o())},commit:(e,t,n)=>{!1!==n&&(f=!1,"true"===e.getAttribute("aria-invalid")&&b())}}),m=g.querySelector("[data-property]"),h=g.querySelector('input[type="color"]'),b=()=>{let o=i(e,t);m.value=o,h&&(h.value=n.toHex(o)),m.removeAttribute("aria-invalid"),k()},k=()=>{v.disabled=!d.toggleAttribute("data-changed",e.themeEdits.has(t.path)),"fontFamily"===t.type&&(m.title=m.value)},v=document.createElement("button");return v.type="button",v.className="design-theme-reset",v.title="Reset to page value",v.setAttribute("aria-label",`Reset ${t.path}`),v.innerHTML='<svg viewBox="0 0 16 16"><path d="M2.5 2.5v3h3M3 5.5a5.5 5.5 0 1 1-.5 2.5"/></svg>',v.addEventListener("click",()=>{l(e,t),b(),o()}),b(),d.append(p,g,v),d}function ke(e,t,n){let i=o(e);i&&p(i,e,t,n)}function ve(e,t){let n=o(e);n&&c(n,e,t)}function ye(e){let n=I.get(e)?.pop(),o=t(e);if(!n||!o)return;o.setProps({...o.props,[n.propName]:n.before});let i=H.get(e);if(i){let t=i.get(n.propName);Object.is(t,n.before)&&i.delete(n.propName),i.size||H.delete(e)}}function xe(){for(let e of[...H.keys()])Ae(e);H.clear(),I.clear(),y(),function(){for(let e of[...w.keys()])R(e)}(),Ce()}var we,Ee=new Map;function $e(e){try{return String(e.current())}catch{return}}function Ce(){for(let[e,t]of Ee)try{$e(e)!==t&&e.apply(t)}catch{}Ee.clear()}var Se=(e,t)=>e||t.charAt(0).toUpperCase()+t.slice(1);function Pe(e,t,n,o){if(e.querySelector(".design-theme-switch"))return;let i=document.createElement("div");i.className="design-group design-theme-switch";let r,a=t?.themes;for(let e of Array.isArray(a)?a:[]){let t=$e(e),o=Array.isArray(e?.options)?e.options.filter(e=>"string"==typeof e?.id):[];if(void 0===t||o.length<2)continue;let a=Se(e.label,String(e.id)),s=o=>{let i=$e(e);if(o!==i){Ee.has(e)||Ee.set(e,i??t),we=e;try{e.apply(o)}catch{}n()}},l=document.createElement(o.length>3?"label":"div");l.className="design-field design-react-field";let d,p=document.createElement("span");if(p.textContent=a,o.length>3){let e=document.createElement("select");for(let t of o)e.append(new Option(Se(t.label,t.id),t.id));e.value=t,e.setAttribute("aria-label",`${a} theme`),e.addEventListener("change",()=>s(e.value)),d=e}else{d=document.createElement("div"),d.className="design-theme-options",d.setAttribute("role","group"),d.setAttribute("aria-label",`${a} theme`);for(let e of o){let n=document.createElement("button");n.type="button",n.textContent=Se(e.label,e.id),n.setAttribute("aria-pressed",String(e.id===t)),n.addEventListener("click",()=>s(e.id)),d.append(n)}}we===e&&(r=d.querySelector("[aria-pressed=true]")??d),l.append(p,d),i.append(l)}we=void 0,i.childElementCount&&(pe(o),e.prepend(i),r?.focus({preventScroll:!0}))}function Ae(e){let n=H.get(e),o=t(e);!n?.size||!o||(o.setProps({...o.props,...Object.fromEntries(n)}),H.delete(e),I.delete(e))}function Ne(t,n,o,i,r,a){let s=n.definition.props.find(e=>e.name===o);if(!s||!e(s,i))return r.setAttribute("aria-invalid","true"),!1;r.removeAttribute("aria-invalid");let l=n.props[o];if(Object.is(l,i))return!0;let d=H.get(t);d||H.set(t,d=new Map);let p=d.has(o)?d.get(o):l;Object.is(p,i)?d.delete(o):d.set(o,p),d.size||H.delete(t);let c=I.get(t);return c||I.set(t,c=[]),c.push({propName:o,before:l,after:i}),n.setProps({...n.props,[o]:i}),a(),!0}function Me(e,i,r,a,s){let l=t(e);if(!l){let t=te(e,s),o=null;try{o=t&&s?.components?.panel?.(t)||null}catch{}return o&&pe(r),o??function(e,t,o){let i=n(e);if(!i)return null;pe(o);let r=document.createElement("small");if(r.className="design-react-source design-react-group design-react-inside",r.append(`Inside ${i.component.definition.name}`),t){let e=document.createElement("button");e.type="button",e.textContent="Select component",e.addEventListener("click",()=>t(i.root)),r.append(" · ",e)}return r}(e,a,r)}pe(r);let d=document.createElement("details");d.className="design-group design-react-group",d.open=!0;let p=document.createElement("summary");p.textContent="Properties",d.append(p);let c=document.createElement("small");c.className="design-react-source",c.textContent=`${l.definition.name} · ${l.definition.source}:${l.definition.line}`,d.append(c);for(let t of l.definition.props){let n=t.styleProperty,r=document.createElement("label");r.className="design-field design-react-field";let a=document.createElement("span");a.textContent=t.name;let s,p=l.props[t.name];if("enum"===t.kind){let e=document.createElement("select");for(let n of t.values??[]){let t=document.createElement("option");t.value=String(n),t.textContent=String(n),e.append(t)}if(void 0===p&&!t.required){let t=document.createElement("option");t.value="",t.textContent="unset",e.prepend(t)}e.value=void 0===p?"":String(p),s=e}else{let e=document.createElement("input");"boolean"===t.kind?(e.type="checkbox",e.checked=!0===p):"number"===t.kind?(e.type="number",e.step="any",e.value="number"==typeof p?String(p):""):(e.type="text",e.maxLength=500,e.autocomplete="off",e.value="string"==typeof p?p:""),s=e}if(s.setAttribute("aria-label",`${l.definition.name} ${t.name}`),"unsupported"===t.kind)s=document.createElement("input"),s.disabled=!0,s.value=`Read only · ${t.typeLabel}`,s.setAttribute("aria-label",`${l.definition.name} ${t.name}, read only`),r.title=`Komo does not edit ${t.typeLabel} props.`;else{let n=()=>{let n=s instanceof HTMLInputElement&&"checkbox"===s.type?s.checked:s.value,o=n;if("number"===t.kind)o=""===String(n).trim()?void 0:Number(n);else if("enum"===t.kind){let e=(t.values??[]).find(e=>String(e)===n);o=""!==n||t.required?e:void 0}else"boolean"===t.kind&&(o=!0===n);Ne(e,l,t.name,o,s,i)};s.addEventListener("change",n)}r.append(a,s);let c=document.createElement("small");if(c.className="design-react-type",c.textContent=`${t.typeLabel}${t.required?" · required":" · optional"}`,r.append(c),r.title||=c.textContent,"string"===t.kind&&n){let a=o(e),d=a?m(a,n):[];if(d.length){let o=document.createElement("span");o.className="design-token-binding";let a=document.createElement("span");a.textContent="Style token";let p=document.createElement("select");p.setAttribute("aria-label",`Style token for ${t.name}`);let c=document.createElement("option");c.value="custom",c.textContent="Custom value",p.append(c);for(let e of d){let t=document.createElement("option");t.value=e.path,t.textContent=e.path,p.append(t)}let u=()=>{let e=d.find(e=>l.props[t.name]===`var(${e.cssVariable})`);p.value=e?.path??"custom",o.dataset.mode=e?"token":"custom"};p.addEventListener("change",()=>{let o=d.find(e=>e.path===p.value),r=o?`var(${o.cssVariable})`:"string"==typeof l.props[t.name]&&String(l.props[t.name]).startsWith("var(")?e.ownerDocument.defaultView?.getComputedStyle(e).getPropertyValue(n).trim()??"":"string"==typeof l.props[t.name]?l.props[t.name]:"";Ne(e,l,t.name,r,s,i)&&u()}),o.append(a,p),r.append(o),u()}}d.append(r)}return d}export{fe as attachStyleTokenControls,Pe as attachThemeSwitcher,X as beginReactTextEdit,ae as canUndoDesignTokenEdit,Z as canUndoReactProp,Q as cancelReactTextEdit,ge as createAdapterTokenControl,he as createDesignThemePanel,ce as createElementTokenControl,Me as createReactPropsPanel,oe as designContext,G as finishReactTextEdit,se as hasDesignTokenEdit,K as hasReactPropChanges,re as installCommentCapture,ke as noteDesignTokenOverride,Y as previewReactTextEdit,ie as proposedDesignChanges,ee as proposedReactChanges,de as resetDesignTokenEdits,xe as resetPreviews,Ae as resetReactProps,Ce as restoreThemes,J as showDesignBefore,ve as syncDesignTokenBinding,ue as tokenBoundProperties,le as undoDesignTokenEdit,ye as undoReactProp};
|
|
@@ -46,8 +46,18 @@ export declare function createDesignThemePanel(element: HTMLElement, panel: Them
|
|
|
46
46
|
export declare function noteDesignTokenOverride(element: HTMLElement, property: string, value: string): void;
|
|
47
47
|
export declare function syncDesignTokenBinding(element: HTMLElement, property: string): void;
|
|
48
48
|
export declare function undoReactProp(element: HTMLElement): void;
|
|
49
|
-
/** Undo every prop, token and theme preview, e.g. when Design mode closes. */
|
|
49
|
+
/** Undo every prop, token and theme preview, and put switched themes back, e.g. when Design mode closes. */
|
|
50
50
|
export declare function resetPreviews(): void;
|
|
51
|
+
/** Put every axis Design mode switched back to its original option. */
|
|
52
|
+
export declare function restoreThemes(): void;
|
|
53
|
+
/**
|
|
54
|
+
* One row per theme axis at the top of the panel: a segmented control for two or
|
|
55
|
+
* three options, a select for more. Theme is context, not an edit, so switching
|
|
56
|
+
* isn't counted, undone, reset or flipped by Before/After; Design mode restores
|
|
57
|
+
* each axis's original option when it closes. `onSwitch` re-renders the panel,
|
|
58
|
+
* so token lists, chips and values follow the new theme.
|
|
59
|
+
*/
|
|
60
|
+
export declare function attachThemeSwitcher(container: HTMLElement, design: DesignSystem | undefined, onSwitch: () => void, styleRoot?: ShadowRoot): void;
|
|
51
61
|
export declare function resetReactProps(element: HTMLElement): void;
|
|
52
|
-
export declare function createReactPropsPanel(element: HTMLElement, onEdit: () => void, styleRoot?: ShadowRoot, select?: (root: HTMLElement) => void): HTMLElement | null;
|
|
62
|
+
export declare function createReactPropsPanel(element: HTMLElement, onEdit: () => void, styleRoot?: ShadowRoot, select?: (root: HTMLElement) => void, design?: DesignSystem): HTMLElement | null;
|
|
53
63
|
export {};
|
package/dist/react.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface UseKomoInspectableOptions {
|
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Opt one React DOM root into the Design mode prop inspector. The component
|
|
24
|
-
* must provide its own state update callback; Komo never reads Fiber internals
|
|
24
|
+
* must provide its own state update callback; Komo's core never reads Fiber internals
|
|
25
25
|
* or writes the host application's source files.
|
|
26
26
|
*/
|
|
27
27
|
export declare function useKomoInspectable(options: UseKomoInspectableOptions): {
|
package/dist/react.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{b as e,c as n,f as t,z as r}from"./chunk-JXIYWNFL.js";import{b as o}from"./chunk-
|
|
1
|
+
"use client";import{b as e,c as n,f as t,z as r}from"./chunk-JXIYWNFL.js";import{b as o}from"./chunk-NWRSH6AL.js";import"./chunk-KMYXE4O6.js";import"./chunk-DH22G2S5.js";import"./chunk-7YUXYVQA.js";import"./chunk-62YUEO4G.js";import"./chunk-I7AUKTXE.js";import{useEffect as s,useRef as c}from"react";function u(e){let n=c(null);s(()=>{if(n.current&&function(e,n){return[...new Set([...Object.keys(e),...Object.keys(n)])].every(t=>{if("shortcuts"===t||"features"===t){let r=e[t]??{},o=n[t]??{};return Object.keys({...r,...o}).every(e=>Object.is(r[e],o[e]))}if("design"===t){let t=e.design,r=n.design;if(!t||!r)return t===r;let o=t.themes??[],s=r.themes??[];return t.tokens===r.tokens&&t.components===r.components&&o.length===s.length&&o.every((e,n)=>e===s[n])}if("onboarding"!==t)return Object.is(e[t],n[t]);let r=e.onboarding,o=n.onboarding;return r&&o?Object.keys({...r,...o}).every(e=>"sites"===e?r.sites?.length===o.sites?.length&&(r.sites??[]).every((e,n)=>e===o.sites?.[n]):Object.is(r[e],o[e])):r===o})}(n.current.config,e)||(n.current?.controller.destroy(),n.current=null,!1===e.enabled))return;let t={...e,...e.onboarding?{onboarding:{...e.onboarding,...e.onboarding.sites?{sites:[...e.onboarding.sites]}:{}}}:{}};n.current={config:t,controller:o(t)}}),s(()=>()=>{n.current?.controller.destroy(),n.current=null},[])}function i(t){let r=c(null),o=c(null);return o.current||(o.current=e=>{r.current=e}),s(()=>{if(!0===t.enabled&&r.current)return e(r.current,t.component,t.props,t.onChange)},[t.enabled,t.component]),s(()=>{!0===t.enabled&&n(r.current,t.component,t.props,t.onChange)},[t.enabled,t.component,t.props,t.onChange]),{ref:o.current,"data-komo-inspectable":t.component.id}}function l(e){s(()=>{if(!0!==e.enabled)return;let n=e.root??document.documentElement;return t(n,e.tokens)},[e.root,e.tokens,e.enabled])}export{r as cssVariableTokens,u as useKomo,l as useKomoDesignTokens,i as useKomoInspectable};
|
package/dist/setup.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as o,c as n}from"./chunk-
|
|
1
|
+
import{a as o,c as n}from"./chunk-NWRSH6AL.js";import"./chunk-KMYXE4O6.js";import"./chunk-DH22G2S5.js";import"./chunk-7YUXYVQA.js";import"./chunk-62YUEO4G.js";import"./chunk-I7AUKTXE.js";function r(r){return(s={})=>n(o({...r,...s}))}export{r as defineKomo};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazem3500/komo",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Point, area, and threaded comments for website branches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"server/import-project.ts",
|
|
29
29
|
"server/digest.ts",
|
|
30
30
|
"server/project-sites.ts",
|
|
31
|
-
"server/iap.ts"
|
|
31
|
+
"server/iap.ts",
|
|
32
|
+
"server/tokens.ts",
|
|
33
|
+
"server/account-page.ts"
|
|
32
34
|
],
|
|
33
35
|
"main": "./dist/index.js",
|
|
34
36
|
"types": "./dist/index.d.ts",
|
|
@@ -50,6 +52,14 @@
|
|
|
50
52
|
"types": "./dist/adapters/tailwind.d.ts",
|
|
51
53
|
"import": "./dist/adapters/tailwind.js"
|
|
52
54
|
},
|
|
55
|
+
"./adapters/css-variables": {
|
|
56
|
+
"types": "./dist/adapters/css-variables.d.ts",
|
|
57
|
+
"import": "./dist/adapters/css-variables.js"
|
|
58
|
+
},
|
|
59
|
+
"./adapters/react-fiber": {
|
|
60
|
+
"types": "./dist/adapters/react-fiber.d.ts",
|
|
61
|
+
"import": "./dist/adapters/react-fiber.js"
|
|
62
|
+
},
|
|
53
63
|
"./adapters/class-theme": {
|
|
54
64
|
"types": "./dist/adapters/class-theme.d.ts",
|
|
55
65
|
"import": "./dist/adapters/class-theme.js"
|
|
@@ -96,6 +106,7 @@
|
|
|
96
106
|
"esbuild": "^0.25.0",
|
|
97
107
|
"jsdom": "^30.1.0",
|
|
98
108
|
"motion": "^12.43.0",
|
|
109
|
+
"playwright-core": "1.64.1",
|
|
99
110
|
"react": "19.2.7",
|
|
100
111
|
"react-colorful": "^5.8.1",
|
|
101
112
|
"react-dom": "19.2.7",
|
|
@@ -109,7 +120,7 @@
|
|
|
109
120
|
"pg": "^8.23.0"
|
|
110
121
|
},
|
|
111
122
|
"bin": {
|
|
112
|
-
"komo": "
|
|
123
|
+
"komo": "cli/index.mjs"
|
|
113
124
|
},
|
|
114
125
|
"engines": {
|
|
115
126
|
"node": ">=22.12"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// The hosted account page: sign in with Google, then create, list and revoke
|
|
2
|
+
// personal access tokens for bots and CI. Rendered with DOM APIs only (no
|
|
3
|
+
// innerHTML), under a nonce-based CSP.
|
|
4
|
+
const css = `
|
|
5
|
+
:root{--bg:#fff;--surface:#f7f7f9;--fg:#1d1d1f;--fg2:#6e6e73;--fg3:#6f6f75;--accent:#c8b5f4;--accent-fg:#352a50;--line:#0000001a;--danger:#b3261e;--ok:#1d7a46;color-scheme:light}
|
|
6
|
+
@media (prefers-color-scheme:dark){:root{--bg:#1b1b1e;--surface:#262629;--fg:#eeeef0;--fg2:#b0afb7;--fg3:#a4a2ad;--accent:#d3bdf5;--line:#ffffff1f;--danger:#ff8a80;--ok:#7fd69f;color-scheme:dark}}
|
|
7
|
+
*{box-sizing:border-box}
|
|
8
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased}
|
|
9
|
+
button,input,select{font:inherit;color:inherit}
|
|
10
|
+
button{cursor:pointer}
|
|
11
|
+
a{color:inherit}
|
|
12
|
+
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
|
13
|
+
.skip{position:absolute;left:12px;top:-60px;background:var(--surface);padding:8px 12px;border-radius:8px;z-index:5}
|
|
14
|
+
.skip:focus{top:12px}
|
|
15
|
+
header{display:flex;align-items:center;justify-content:space-between;gap:16px;max-width:760px;margin:0 auto;padding:28px 24px 0}
|
|
16
|
+
.brand{font-size:24px;font-weight:600;letter-spacing:-.8px;color:var(--accent);text-decoration:none}
|
|
17
|
+
.who{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--fg2);min-width:0}
|
|
18
|
+
.who img,.who .initial{width:28px;height:28px;border-radius:50%;flex:none;background:var(--surface);display:grid;place-items:center;font-weight:600;color:var(--fg2)}
|
|
19
|
+
.who span.name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
20
|
+
main{max-width:760px;margin:0 auto;padding:44px 24px 96px}
|
|
21
|
+
h1{font-size:32px;letter-spacing:-1.2px;line-height:1.15;font-weight:600;margin:0 0 10px}
|
|
22
|
+
.lede{margin:0;color:var(--fg2);max-width:560px}
|
|
23
|
+
.bar{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:28px}
|
|
24
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 20px;border:0;border-radius:12px;background:var(--surface);color:var(--fg);font-weight:500;box-shadow:inset 0 0 0 1px var(--line);transition:background .15s,transform .15s}
|
|
25
|
+
.btn:hover{background:var(--line)}
|
|
26
|
+
.btn:active{transform:scale(.98)}
|
|
27
|
+
.btn.primary{background:var(--accent);color:var(--accent-fg);box-shadow:inset 0 1px 0 #ffffff40,0 2px 4px #0002}
|
|
28
|
+
.btn.primary:hover{filter:brightness(1.05)}
|
|
29
|
+
.btn.danger{color:var(--danger)}
|
|
30
|
+
.btn[disabled]{opacity:.55;cursor:default}
|
|
31
|
+
.panel{background:var(--surface);border-radius:16px;box-shadow:inset 0 0 0 1px var(--line)}
|
|
32
|
+
.notice{padding:20px 22px;color:var(--fg2)}
|
|
33
|
+
.notice strong{display:block;color:var(--fg);font-weight:600;margin-bottom:4px}
|
|
34
|
+
.notice.error{color:var(--danger)}
|
|
35
|
+
.tokens{list-style:none;margin:0;padding:0;display:grid;gap:12px}
|
|
36
|
+
.token{padding:18px 20px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 16px;align-items:start}
|
|
37
|
+
.token h2{margin:0;font-size:16px;font-weight:600;display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
38
|
+
.mono{font-family:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace;font-size:13px;color:var(--fg2);overflow-wrap:anywhere}
|
|
39
|
+
.badge{font-size:12px;font-weight:500;line-height:1;padding:5px 8px;border-radius:999px;background:var(--bg);box-shadow:inset 0 0 0 1px var(--line);color:var(--fg2)}
|
|
40
|
+
.badge.warn{color:var(--danger)}
|
|
41
|
+
.meta{grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px 20px;margin:10px 0 0;font-size:13px}
|
|
42
|
+
.meta div{min-width:0}
|
|
43
|
+
.meta dt{color:var(--fg3);font-size:12px;margin-bottom:2px}
|
|
44
|
+
.meta dd{margin:0;overflow-wrap:anywhere}
|
|
45
|
+
.skeleton{height:96px;border-radius:16px;background:linear-gradient(90deg,var(--surface),var(--line),var(--surface));background-size:200% 100%;animation:shimmer 1.4s infinite}
|
|
46
|
+
@keyframes shimmer{to{background-position:-200% 0}}
|
|
47
|
+
dialog{width:min(480px,calc(100vw - 24px));max-height:calc(100dvh - 24px);overflow:auto;border:0;border-radius:20px;padding:0;background:var(--bg);color:var(--fg);box-shadow:0 0 0 1px var(--line),0 24px 60px -12px #0007}
|
|
48
|
+
dialog::backdrop{background:#0006;backdrop-filter:blur(2px)}
|
|
49
|
+
dialog[open]{animation:pop .18s ease-out}
|
|
50
|
+
@keyframes pop{from{opacity:0;transform:translateY(8px) scale(.98)}}
|
|
51
|
+
form,.reveal{padding:24px;display:grid;gap:18px}
|
|
52
|
+
dialog h2{margin:0;font-size:20px;letter-spacing:-.4px}
|
|
53
|
+
label.field,fieldset{display:grid;gap:6px;border:0;margin:0;padding:0;min-width:0;font-size:14px}
|
|
54
|
+
legend,label.field>span{font-weight:500;padding:0}
|
|
55
|
+
input[type=text],select{min-height:44px;padding:0 12px;border-radius:10px;border:0;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line);width:100%}
|
|
56
|
+
.choice{display:flex;align-items:center;gap:10px;min-height:36px;color:var(--fg)}
|
|
57
|
+
.choice input{width:18px;height:18px;accent-color:var(--accent);margin:0}
|
|
58
|
+
.choice small{color:var(--fg3)}
|
|
59
|
+
.projects{display:grid;gap:2px;max-height:160px;overflow:auto;padding:6px 10px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line)}
|
|
60
|
+
.hint{color:var(--fg3);font-size:13px;margin:0}
|
|
61
|
+
.actions{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}
|
|
62
|
+
.secret{display:flex;gap:8px;align-items:stretch}
|
|
63
|
+
.secret code{flex:1;min-width:0;padding:12px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line);font:13px/1.5 ui-monospace,"SFMono-Regular",Consolas,monospace;overflow-wrap:anywhere;user-select:all}
|
|
64
|
+
.warning{padding:12px 14px;border-radius:10px;background:var(--surface);box-shadow:inset 0 0 0 1px var(--line);font-size:14px;margin:0}
|
|
65
|
+
.error-text{color:var(--danger);font-size:14px;margin:0}
|
|
66
|
+
.live{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
|
|
67
|
+
[hidden]{display:none!important}
|
|
68
|
+
@media (max-width:560px){main{padding-top:32px}h1{font-size:28px}.bar{flex-direction:column;align-items:stretch}.bar .btn{width:100%}.token{grid-template-columns:1fr}.token .btn{width:100%}.secret{flex-direction:column}.actions .btn{flex:1}.who span.name{display:none}}
|
|
69
|
+
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const script = `
|
|
73
|
+
const cfg=JSON.parse(document.getElementById("cfg").textContent);
|
|
74
|
+
const $=(s)=>document.querySelector(s);
|
|
75
|
+
const KEY="komo-account-session";
|
|
76
|
+
let session=null,user=null,data=null;
|
|
77
|
+
const el=(tag,props,...kids)=>{const n=document.createElement(tag);for(const k in props||{}){if(k==="class")n.className=props[k];else if(k.startsWith("on"))n.addEventListener(k.slice(2),props[k]);else if(props[k]===true)n.setAttribute(k,"");else if(props[k]!==false&&props[k]!=null)n.setAttribute(k,props[k])}n.append(...kids.flat().filter((x)=>x!=null));return n};
|
|
78
|
+
const live=(t)=>{$("#live").textContent="";setTimeout(()=>{$("#live").textContent=t},30)};
|
|
79
|
+
const date=(t)=>new Intl.DateTimeFormat(undefined,{dateStyle:"medium"}).format(new Date(t));
|
|
80
|
+
const ago=(t)=>{if(!t)return "Never";const s=Math.max(0,(Date.now()-t)/1000);if(s<90)return "Just now";if(s<5400)return Math.round(s/60)+" min ago";if(s<129600)return Math.round(s/3600)+" hr ago";return date(t)};
|
|
81
|
+
async function api(path,method,body){
|
|
82
|
+
const r=await fetch(cfg.base+path+"?project="+encodeURIComponent(cfg.project),{method:method||"GET",headers:{...(session?{Authorization:"Bearer "+session}:{}),...(body?{"Content-Type":"application/json"}:{})},body:body?JSON.stringify(body):undefined});
|
|
83
|
+
let j={};try{j=await r.json()}catch{}
|
|
84
|
+
if(r.status===401&&session&&path!=="/auth/google/start"){session=null;sessionStorage.removeItem(KEY);user=null;render()}
|
|
85
|
+
if(!r.ok){const e=new Error(j.error||"Request failed ("+r.status+").");e.status=r.status;throw e}
|
|
86
|
+
return j}
|
|
87
|
+
function view(name){for(const v of ["loading","signin","guest","tokens"])$("#v-"+v).hidden=v!==name}
|
|
88
|
+
function render(){
|
|
89
|
+
const header=$("#who");header.replaceChildren();
|
|
90
|
+
if(!session||!user){view(session?"loading":"signin");return}
|
|
91
|
+
const google=user.verified&&String(user.id).startsWith("google:");
|
|
92
|
+
header.append(user.avatarUrl?el("img",{src:user.avatarUrl,alt:"",referrerpolicy:"no-referrer"}):el("span",{class:"initial","aria-hidden":"true"},(user.name||"?").slice(0,1).toUpperCase()),el("span",{class:"name"},user.name),el("button",{class:"btn",type:"button",onclick:signOut},"Sign out"));
|
|
93
|
+
if(!google){view("guest");return}
|
|
94
|
+
view("tokens");list()}
|
|
95
|
+
function list(){
|
|
96
|
+
const ul=$("#list");ul.replaceChildren();
|
|
97
|
+
if(!data){ul.append(el("li",{class:"skeleton"}));return}
|
|
98
|
+
$("#empty").hidden=data.tokens.length>0;
|
|
99
|
+
for(const t of data.tokens){
|
|
100
|
+
const scopes=t.scopes.join(", ");
|
|
101
|
+
const projects=t.projects?t.projects.join(", "):"All projects";
|
|
102
|
+
ul.append(el("li",{class:"panel token"},
|
|
103
|
+
el("div",null,el("h2",null,t.name,t.expired?el("span",{class:"badge warn"},"Expired"):null),el("div",{class:"mono"},t.prefix+"…"+t.last4)),
|
|
104
|
+
el("button",{class:"btn danger",type:"button","aria-label":"Revoke "+t.name,onclick:()=>confirmRevoke(t)},"Revoke"),
|
|
105
|
+
el("dl",{class:"meta"},
|
|
106
|
+
meta("Scopes",scopes),meta("Projects",projects),meta("Created",date(t.createdAt)),meta("Expires",t.expiresAt?date(t.expiresAt):"Never"),meta("Last used",ago(t.lastUsedAt)))))}}
|
|
107
|
+
const meta=(k,v)=>el("div",null,el("dt",null,k),el("dd",null,v));
|
|
108
|
+
async function load(){
|
|
109
|
+
try{data=await api("/account/tokens");list()}
|
|
110
|
+
catch(e){if(e.status===403)view("guest");else if(e.status!==401){$("#list").replaceChildren(el("li",{class:"panel notice error"},e.message))}}}
|
|
111
|
+
async function boot(){
|
|
112
|
+
session=sessionStorage.getItem(KEY);
|
|
113
|
+
if(!session){render();return}
|
|
114
|
+
view("loading");
|
|
115
|
+
try{user=(await api("/me")).user;render();await load()}catch(e){if(e.status!==401){view("signin");$("#signin-error").textContent=e.message}}}
|
|
116
|
+
async function signIn(){
|
|
117
|
+
const btn=$("#signin");btn.disabled=true;$("#signin-error").textContent="";
|
|
118
|
+
const popup=window.open("about:blank","komo-account-auth","width=500,height=700");
|
|
119
|
+
try{
|
|
120
|
+
const {url}=await api("/auth/google/start","POST");
|
|
121
|
+
if(!popup){throw new Error("Allow pop-ups to sign in with Google.")}
|
|
122
|
+
popup.location.href=url;
|
|
123
|
+
await new Promise((resolve,reject)=>{
|
|
124
|
+
const done=(fn,v)=>{removeEventListener("message",on);clearInterval(poll);fn(v)};
|
|
125
|
+
const on=(e)=>{if(e.origin!==location.origin||e.source!==popup||e.data?.type!=="branch-comments:auth")return;session=e.data.token;sessionStorage.setItem(KEY,session);user=e.data.user;done(resolve)};
|
|
126
|
+
const poll=setInterval(()=>{if(popup.closed&&!session)done(reject,new Error("Sign-in was cancelled."))},600);
|
|
127
|
+
addEventListener("message",on)});
|
|
128
|
+
render();await load()
|
|
129
|
+
}catch(e){try{popup&&popup.close()}catch{}$("#signin-error").textContent=e.message}finally{btn.disabled=false}}
|
|
130
|
+
async function signOut(){
|
|
131
|
+
try{await api("/me","DELETE")}catch{}
|
|
132
|
+
session=null;user=null;data=null;sessionStorage.removeItem(KEY);render()}
|
|
133
|
+
|
|
134
|
+
const dlg=$("#create"),rev=$("#reveal"),conf=$("#confirm");
|
|
135
|
+
$("#new").onclick=()=>{
|
|
136
|
+
$("#form").reset();$("#form-error").textContent="";
|
|
137
|
+
const has=data&&data.projects.length>0;
|
|
138
|
+
$("#project-scope").hidden=!has;
|
|
139
|
+
const box=$("#project-list");box.replaceChildren(...(data?data.projects:[]).map((p)=>el("label",{class:"choice"},el("input",{type:"checkbox",name:"project",value:p}),p)));
|
|
140
|
+
box.hidden=true;
|
|
141
|
+
dlg.showModal();$("#name").focus()};
|
|
142
|
+
for(const r of document.querySelectorAll("input[name=limit]"))r.onchange=()=>{$("#project-list").hidden=document.querySelector("input[name=limit]:checked").value!=="some"};
|
|
143
|
+
$("#cancel").onclick=()=>dlg.close();
|
|
144
|
+
$("#form").onsubmit=async(e)=>{
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
const f=e.target,b=$("#submit");$("#form-error").textContent="";
|
|
147
|
+
const scopes=[...f.querySelectorAll("input[name=scope]:checked")].map((i)=>i.value);
|
|
148
|
+
if(!scopes.length){$("#form-error").textContent="Choose at least one scope.";return}
|
|
149
|
+
const some=f.querySelector("input[name=limit]:checked").value==="some";
|
|
150
|
+
const projects=some?[...f.querySelectorAll("input[name=project]:checked")].map((i)=>i.value):[];
|
|
151
|
+
if(some&&!projects.length){$("#form-error").textContent="Choose at least one project, or allow all projects.";return}
|
|
152
|
+
const days=f.expiry.value;
|
|
153
|
+
b.disabled=true;
|
|
154
|
+
try{
|
|
155
|
+
const r=await api("/account/tokens","POST",{name:f.tokenName.value.trim(),scopes,projects,expiresInDays:days==="never"?null:Number(days)});
|
|
156
|
+
dlg.close();
|
|
157
|
+
$("#secret").textContent=r.token;$("#copy").textContent="Copy";
|
|
158
|
+
rev.showModal();$("#copy").focus();live("Token created. Copy it now; it will not be shown again.");
|
|
159
|
+
data.tokens.unshift(r.item);list();
|
|
160
|
+
}catch(err){$("#form-error").textContent=err.message}finally{b.disabled=false}};
|
|
161
|
+
$("#copy").onclick=async()=>{
|
|
162
|
+
const t=$("#secret").textContent;
|
|
163
|
+
try{await navigator.clipboard.writeText(t)}catch{const r=document.createRange();r.selectNodeContents($("#secret"));getSelection().removeAllRanges();getSelection().addRange(r);document.execCommand&&document.execCommand("copy")}
|
|
164
|
+
$("#copy").textContent="Copied";live("Token copied to clipboard.")};
|
|
165
|
+
$("#done").onclick=()=>{rev.close()};
|
|
166
|
+
rev.addEventListener("close",()=>{$("#secret").textContent=""});
|
|
167
|
+
rev.addEventListener("cancel",(e)=>{if(!confirm("Close without copying? This token will not be shown again."))e.preventDefault()});
|
|
168
|
+
let target=null;
|
|
169
|
+
function confirmRevoke(t){target=t;$("#confirm-text").textContent="Revoke \\u201C"+t.name+"\\u201D? Anything using it stops working immediately. This can\\u2019t be undone.";$("#confirm-error").textContent="";conf.showModal()}
|
|
170
|
+
$("#keep").onclick=()=>conf.close();
|
|
171
|
+
$("#revoke").onclick=async()=>{
|
|
172
|
+
const b=$("#revoke");b.disabled=true;
|
|
173
|
+
try{await api("/account/tokens/"+target.id,"DELETE");data.tokens=data.tokens.filter((t)=>t.id!==target.id);conf.close();list();live("Token revoked.");$("#new").focus()}
|
|
174
|
+
catch(e){$("#confirm-error").textContent=e.message}finally{b.disabled=false}};
|
|
175
|
+
$("#signin").onclick=signIn;
|
|
176
|
+
$("#guest-out").onclick=signOut;
|
|
177
|
+
boot();
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
export function accountPage(env: Env, url: URL) {
|
|
181
|
+
const nonce = crypto.randomUUID();
|
|
182
|
+
const configured = Object.keys(JSON.parse(env.PROJECTS));
|
|
183
|
+
const requested = url.searchParams.get("project");
|
|
184
|
+
// Hosted sign-in uses the management project; self-hosted servers use one of their own.
|
|
185
|
+
const project =
|
|
186
|
+
env.KOMO_HOSTED === "true"
|
|
187
|
+
? "_komo"
|
|
188
|
+
: requested && configured.includes(requested)
|
|
189
|
+
? requested
|
|
190
|
+
: (configured[0] ?? "_komo");
|
|
191
|
+
const config = JSON.stringify({
|
|
192
|
+
base: env.KOMO_PUBLIC_PATH ?? "",
|
|
193
|
+
project,
|
|
194
|
+
}).replace(/</g, "\\u003c");
|
|
195
|
+
return new Response(
|
|
196
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><meta name="robots" content="noindex"><title>Access tokens · komo</title><link rel="icon" href="data:,"><style nonce="${nonce}">${css}</style></head><body>
|
|
197
|
+
<a class="skip" href="#main">Skip to content</a>
|
|
198
|
+
<header><a class="brand" href="/">komo</a><div class="who" id="who"></div></header>
|
|
199
|
+
<main id="main">
|
|
200
|
+
<div id="v-loading"><div class="skeleton"></div></div>
|
|
201
|
+
<section id="v-signin" hidden><h1>Access tokens</h1><p class="lede">Sign in with Google to create tokens that let bots and CI use the komo CLI as you.</p><p><button class="btn primary" id="signin" type="button">Continue with Google</button></p><p class="error-text" id="signin-error" role="alert"></p></section>
|
|
202
|
+
<section id="v-guest" hidden><h1>Access tokens</h1><div class="panel notice"><strong>Sign in with Google to manage tokens</strong><span>Guest sessions can’t create access tokens, because a token would carry an identity that can’t be verified.</span></div><p><button class="btn" id="guest-out" type="button">Sign out</button></p></section>
|
|
203
|
+
<section id="v-tokens" hidden>
|
|
204
|
+
<div class="bar"><div><h1>Access tokens</h1><p class="lede">Use a token as <code class="mono">KOMO_TOKEN</code> so bots and CI can read or write comments as you. Treat tokens like passwords.</p></div><button class="btn primary" id="new" type="button">New token</button></div>
|
|
205
|
+
<ul class="tokens" id="list" aria-label="Your access tokens"></ul>
|
|
206
|
+
<div class="panel notice" id="empty" hidden><strong>No tokens yet</strong>Create one for a bot, a script, or a CI job.</div>
|
|
207
|
+
</section>
|
|
208
|
+
</main>
|
|
209
|
+
<dialog id="create" aria-labelledby="create-title"><form id="form" novalidate><h2 id="create-title">New access token</h2>
|
|
210
|
+
<label class="field"><span>Name</span><input id="name" name="tokenName" type="text" required maxlength="60" autocomplete="off" placeholder="e.g. Deploy bot"></label>
|
|
211
|
+
<label class="field"><span>Expiration</span><select name="expiry"><option value="7">7 days</option><option value="30" selected>30 days</option><option value="90">90 days</option><option value="365">1 year</option><option value="never">No expiration</option></select></label>
|
|
212
|
+
<fieldset><legend>Scopes</legend><label class="choice"><input type="checkbox" name="scope" value="comments:read" checked>comments:read <small>list and read threads</small></label><label class="choice"><input type="checkbox" name="scope" value="comments:write">comments:write <small>create, reply, resolve</small></label></fieldset>
|
|
213
|
+
<fieldset id="project-scope"><legend>Projects</legend><label class="choice"><input type="radio" name="limit" value="all" checked>All projects you can access</label><label class="choice"><input type="radio" name="limit" value="some">Only selected projects</label><div class="projects" id="project-list" hidden></div></fieldset>
|
|
214
|
+
<p class="error-text" id="form-error" role="alert"></p>
|
|
215
|
+
<div class="actions"><button class="btn" id="cancel" type="button">Cancel</button><button class="btn primary" id="submit" type="submit">Create token</button></div></form></dialog>
|
|
216
|
+
<dialog id="reveal" aria-labelledby="reveal-title"><div class="reveal"><h2 id="reveal-title">Copy your token</h2><p class="warning">This is the only time it will be shown. Store it in your secret manager, never in source control.</p><div class="secret"><code id="secret" tabindex="0" aria-label="Access token"></code><button class="btn primary" id="copy" type="button">Copy</button></div><div class="actions"><button class="btn" id="done" type="button">Done</button></div></div></dialog>
|
|
217
|
+
<dialog id="confirm" aria-labelledby="confirm-title"><div class="reveal"><h2 id="confirm-title">Revoke token</h2><p id="confirm-text" class="hint"></p><p class="error-text" id="confirm-error" role="alert"></p><div class="actions"><button class="btn" id="keep" type="button">Keep token</button><button class="btn danger" id="revoke" type="button">Revoke</button></div></div></dialog>
|
|
218
|
+
<div class="live" id="live" role="status" aria-live="polite"></div>
|
|
219
|
+
<script type="application/json" id="cfg">${config}</script>
|
|
220
|
+
<script nonce="${nonce}">${script}</script></body></html>`,
|
|
221
|
+
{
|
|
222
|
+
headers: {
|
|
223
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
224
|
+
"Cache-Control": "no-store",
|
|
225
|
+
"Referrer-Policy": "no-referrer",
|
|
226
|
+
"X-Content-Type-Options": "nosniff",
|
|
227
|
+
"Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src https: data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'`,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
}
|
package/server/index.ts
CHANGED
|
@@ -5,6 +5,17 @@ import {
|
|
|
5
5
|
requireMember,
|
|
6
6
|
} from "./project-management";
|
|
7
7
|
import { setupPage } from "./setup-page";
|
|
8
|
+
import { accountPage } from "./account-page";
|
|
9
|
+
import {
|
|
10
|
+
accountProjects,
|
|
11
|
+
authenticateToken,
|
|
12
|
+
createToken,
|
|
13
|
+
isPersonalToken,
|
|
14
|
+
listTokens,
|
|
15
|
+
PAT_SCOPES,
|
|
16
|
+
revokeToken,
|
|
17
|
+
type PatAccess,
|
|
18
|
+
} from "./tokens";
|
|
8
19
|
import { authPrefix, iapIdentity, type IapIdentity } from "./iap";
|
|
9
20
|
import setupClient from "./setup-client.txt";
|
|
10
21
|
import {
|
|
@@ -166,6 +177,8 @@ async function body(request: Request): Promise<Record<string, unknown>> {
|
|
|
166
177
|
: path === "/project/webhook"
|
|
167
178
|
? ["url", "secret", "events", "enabled"]
|
|
168
179
|
: ["access", "confirm", "threadIds"]
|
|
180
|
+
: path === "/account/tokens"
|
|
181
|
+
? ["name", "expiresInDays", "scopes", "projects"]
|
|
169
182
|
: ["/auth/google/start", "/auth/github/start"].includes(path)
|
|
170
183
|
? ["returnOrigin"]
|
|
171
184
|
: path === "/setup/start"
|
|
@@ -230,15 +243,21 @@ async function session(env: Env, project: string, userId: string) {
|
|
|
230
243
|
]);
|
|
231
244
|
return accessToken;
|
|
232
245
|
}
|
|
246
|
+
// Personal access tokens only pass where a route names the access it needs.
|
|
233
247
|
async function authenticate(
|
|
234
248
|
request: Request,
|
|
235
249
|
env: Env,
|
|
236
|
-
project: string
|
|
250
|
+
project: string,
|
|
251
|
+
access?: PatAccess
|
|
237
252
|
): Promise<Identity> {
|
|
238
253
|
const bearer = request.headers
|
|
239
254
|
.get("Authorization")
|
|
240
255
|
?.match(/^Bearer (.{1,200})$/)?.[1];
|
|
241
256
|
check(bearer, 401, "Enter your name to comment.");
|
|
257
|
+
if (isPersonalToken(bearer))
|
|
258
|
+
return identity(
|
|
259
|
+
await authenticateToken(env, bearer, project, access, request)
|
|
260
|
+
);
|
|
242
261
|
const row = await env.DB.prepare(
|
|
243
262
|
"SELECT u.* FROM sessions s JOIN users u ON u.id=s.user_id WHERE s.token_hash=? AND s.project=? AND s.expires_at>?"
|
|
244
263
|
)
|
|
@@ -662,6 +681,8 @@ async function route(
|
|
|
662
681
|
return oauthAuthorize(new Request(authorize), env, "google");
|
|
663
682
|
}
|
|
664
683
|
if (url.pathname === "/setup") return setupPage();
|
|
684
|
+
if (url.pathname === "/account" && request.method === "GET")
|
|
685
|
+
return accountPage(env, url);
|
|
665
686
|
if (
|
|
666
687
|
url.pathname === "/setup/start" &&
|
|
667
688
|
request.method === "POST" &&
|
|
@@ -762,9 +783,13 @@ async function route(
|
|
|
762
783
|
const origin =
|
|
763
784
|
request.headers.get("Origin") ??
|
|
764
785
|
(request.headers.get("Sec-Fetch-Site") === "same-origin" ? url.origin : "");
|
|
786
|
+
const accountRoute = /^\/account\/tokens(?:\/[a-z0-9]{12})?$/.test(
|
|
787
|
+
url.pathname
|
|
788
|
+
);
|
|
765
789
|
if (
|
|
766
790
|
!(
|
|
767
791
|
originAllowed(origin, config.origins) ||
|
|
792
|
+
(origin === url.origin && accountRoute) ||
|
|
768
793
|
(project !== "_komo" && localOrigin(origin)) ||
|
|
769
794
|
(origin === url.origin &&
|
|
770
795
|
[
|
|
@@ -793,6 +818,7 @@ async function route(
|
|
|
793
818
|
check(
|
|
794
819
|
project !== "_komo" ||
|
|
795
820
|
url.pathname.startsWith("/project") ||
|
|
821
|
+
accountRoute ||
|
|
796
822
|
[
|
|
797
823
|
"/setup/complete",
|
|
798
824
|
"/workspace/verify",
|
|
@@ -889,6 +915,31 @@ async function route(
|
|
|
889
915
|
: [],
|
|
890
916
|
});
|
|
891
917
|
}
|
|
918
|
+
if (accountRoute) {
|
|
919
|
+
// Sessions only: an access token can never mint or revoke access tokens.
|
|
920
|
+
const user = await authenticate(request, env, project);
|
|
921
|
+
check(
|
|
922
|
+
user.verified && user.id.startsWith("google:"),
|
|
923
|
+
403,
|
|
924
|
+
"Sign in with Google to manage access tokens."
|
|
925
|
+
);
|
|
926
|
+
const tokenId = url.pathname.match(/^\/account\/tokens\/([a-z0-9]{12})$/)?.[1];
|
|
927
|
+
if (url.pathname === "/account/tokens" && request.method === "GET")
|
|
928
|
+
return json({
|
|
929
|
+
tokens: await listTokens(env, user.id),
|
|
930
|
+
projects: await accountProjects(env, user.id),
|
|
931
|
+
scopes: PAT_SCOPES,
|
|
932
|
+
});
|
|
933
|
+
if (url.pathname === "/account/tokens" && request.method === "POST") {
|
|
934
|
+
await limit(env, `pat:create:${user.id}`, 10, 3600);
|
|
935
|
+
return json(await createToken(env, user.id, await body(request)), 201);
|
|
936
|
+
}
|
|
937
|
+
if (tokenId && request.method === "DELETE") {
|
|
938
|
+
await revokeToken(env, user.id, tokenId);
|
|
939
|
+
return json({ ok: true });
|
|
940
|
+
}
|
|
941
|
+
throw new HttpError(405, "Method not allowed.");
|
|
942
|
+
}
|
|
892
943
|
if (url.pathname === "/project" || url.pathname.startsWith("/project/")) {
|
|
893
944
|
const user = await authenticate(request, env, project);
|
|
894
945
|
const target =
|
|
@@ -1069,7 +1120,12 @@ async function route(
|
|
|
1069
1120
|
});
|
|
1070
1121
|
}
|
|
1071
1122
|
if (url.pathname === "/me") {
|
|
1072
|
-
const user = await authenticate(
|
|
1123
|
+
const user = await authenticate(
|
|
1124
|
+
request,
|
|
1125
|
+
env,
|
|
1126
|
+
project,
|
|
1127
|
+
request.method === "GET" ? "any" : undefined
|
|
1128
|
+
);
|
|
1073
1129
|
if (request.method === "DELETE") {
|
|
1074
1130
|
const bearer = request.headers.get("Authorization")!.slice(7);
|
|
1075
1131
|
await env.DB.prepare("DELETE FROM sessions WHERE token_hash=?")
|
|
@@ -1128,10 +1184,11 @@ async function route(
|
|
|
1128
1184
|
check(request.method === "GET", 405, "Method not allowed.");
|
|
1129
1185
|
return json({ user });
|
|
1130
1186
|
}
|
|
1187
|
+
const access: PatAccess = request.method === "GET" ? "read" : "write";
|
|
1131
1188
|
// Behind IAP, comments are never readable or writable without a session
|
|
1132
1189
|
// minted from an IAP-verified Google account.
|
|
1133
1190
|
if (env.KOMO_IAP_AUDIENCE) {
|
|
1134
|
-
const reader = await authenticate(request, env, project);
|
|
1191
|
+
const reader = await authenticate(request, env, project, access);
|
|
1135
1192
|
check(
|
|
1136
1193
|
reader.verified && reader.id.startsWith("google:"),
|
|
1137
1194
|
403,
|
|
@@ -1142,7 +1199,7 @@ async function route(
|
|
|
1142
1199
|
await requireMember(
|
|
1143
1200
|
env,
|
|
1144
1201
|
project,
|
|
1145
|
-
await authenticate(request, env, project)
|
|
1202
|
+
await authenticate(request, env, project, access)
|
|
1146
1203
|
);
|
|
1147
1204
|
}
|
|
1148
1205
|
const requestedRepo = url.searchParams.get("repo");
|
|
@@ -1278,7 +1335,7 @@ async function route(
|
|
|
1278
1335
|
revision,
|
|
1279
1336
|
});
|
|
1280
1337
|
}
|
|
1281
|
-
const user = await authenticate(request, env, project);
|
|
1338
|
+
const user = await authenticate(request, env, project, access);
|
|
1282
1339
|
await limit(env, `${project}:session:${user.id}`, 60);
|
|
1283
1340
|
const now = Date.now();
|
|
1284
1341
|
if (url.pathname === "/threads" && request.method === "POST") {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
CREATE TABLE personal_access_tokens (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
4
|
+
name TEXT NOT NULL,
|
|
5
|
+
last4 TEXT NOT NULL,
|
|
6
|
+
salt TEXT NOT NULL,
|
|
7
|
+
token_hash TEXT NOT NULL,
|
|
8
|
+
scopes TEXT NOT NULL,
|
|
9
|
+
projects TEXT,
|
|
10
|
+
created_at INTEGER NOT NULL,
|
|
11
|
+
expires_at INTEGER,
|
|
12
|
+
last_used_at INTEGER,
|
|
13
|
+
revoked_at INTEGER
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX personal_access_tokens_user ON personal_access_tokens(user_id, created_at);
|
|
16
|
+
CREATE TABLE audit_log (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
at INTEGER NOT NULL,
|
|
19
|
+
actor TEXT NOT NULL,
|
|
20
|
+
action TEXT NOT NULL,
|
|
21
|
+
target TEXT NOT NULL,
|
|
22
|
+
detail TEXT NOT NULL
|
|
23
|
+
);
|
|
24
|
+
CREATE INDEX audit_log_at ON audit_log(at);
|
|
25
|
+
CREATE INDEX audit_log_actor ON audit_log(actor, at);
|
package/server/secrets.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface Env {
|
|
|
6
6
|
body: string,
|
|
7
7
|
headers: Record<string, string>
|
|
8
8
|
) => Promise<void>;
|
|
9
|
+
/** Optional server secret mixed into access token hashes. Changing it revokes every token. */
|
|
10
|
+
KOMO_TOKEN_PEPPER?: string;
|
|
9
11
|
KOMO_HOSTED?: string;
|
|
10
12
|
KOMO_PAUSED?: string;
|
|
11
13
|
/** Enables Google Cloud IAP sign-in: the IAP backend service audience. */
|