@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.
@@ -0,0 +1,324 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { check, string } from "./validation";
3
+
4
+ // Personal access tokens let a bot act as its owner with a narrower grant.
5
+ // Format: komo_pat_<12-char lookup id>_<43-char secret>. Only a salted SHA-256
6
+ // of the secret is stored (plus an optional server pepper), never the token.
7
+ export const PAT_PREFIX = "komo_pat_";
8
+ export const PAT_SCOPES = ["comments:read", "comments:write"] as const;
9
+ export type PatAccess = "read" | "write" | "any";
10
+ const PAT_PATTERN = /^komo_pat_([a-z0-9]{12})_([A-Za-z0-9_-]{43})$/;
11
+ const MAX_ACTIVE_TOKENS = 25;
12
+ const MAX_LIFETIME_DAYS = 365;
13
+ const USE_LOG_INTERVAL = 60000;
14
+ const AUDIT_RETENTION = 180 * 86400000;
15
+
16
+ type TokenRow = {
17
+ id: string;
18
+ user_id: string;
19
+ name: string;
20
+ last4: string;
21
+ salt: string;
22
+ token_hash: string;
23
+ scopes: string;
24
+ projects: string | null;
25
+ created_at: number;
26
+ expires_at: number | null;
27
+ last_used_at: number | null;
28
+ revoked_at: number | null;
29
+ };
30
+ type TokenUser = {
31
+ id: string;
32
+ name: string;
33
+ verified: number;
34
+ avatar_url?: string;
35
+ accent_color?: string;
36
+ };
37
+
38
+ async function sha256(value: string) {
39
+ return Array.from(
40
+ new Uint8Array(
41
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
42
+ ),
43
+ (byte) => byte.toString(16).padStart(2, "0")
44
+ ).join("");
45
+ }
46
+ const digest = (env: Env, salt: string, secret: string) =>
47
+ sha256(`${env.KOMO_TOKEN_PEPPER ?? ""}\0${salt}\0${secret}`);
48
+ function randomId(length: number) {
49
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
50
+ let result = "";
51
+ while (result.length < length)
52
+ for (const byte of crypto.getRandomValues(new Uint8Array(length)))
53
+ // 252 = 7 * 36: discard the remainder range to avoid modulo bias.
54
+ if (byte < 252 && result.length < length)
55
+ result += alphabet[byte % alphabet.length];
56
+ return result;
57
+ }
58
+ const randomSecret = () =>
59
+ btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))))
60
+ .replace(/\+/g, "-")
61
+ .replace(/\//g, "_")
62
+ .replace(/=+$/, "");
63
+ const list = (value: string | null): string[] | null => {
64
+ if (value === null) return null;
65
+ try {
66
+ const parsed = JSON.parse(value);
67
+ return Array.isArray(parsed) ? parsed.map(String) : [];
68
+ } catch {
69
+ return [];
70
+ }
71
+ };
72
+
73
+ export const isPersonalToken = (bearer: string) =>
74
+ bearer.startsWith(PAT_PREFIX);
75
+
76
+ export function auditStatement(
77
+ env: Env,
78
+ actor: string,
79
+ action: string,
80
+ target: string,
81
+ detail: Record<string, unknown>
82
+ ) {
83
+ return env.DB.prepare(
84
+ "INSERT INTO audit_log(id,at,actor,action,target,detail) VALUES(?,?,?,?,?,?)"
85
+ ).bind(
86
+ crypto.randomUUID(),
87
+ Date.now(),
88
+ actor,
89
+ action,
90
+ target,
91
+ JSON.stringify(detail)
92
+ );
93
+ }
94
+
95
+ export const pruneAudit = (db: D1Database, now: number) =>
96
+ db.prepare("DELETE FROM audit_log WHERE at<?").bind(now - AUDIT_RETENTION);
97
+
98
+ /** Projects an account holder may limit a token to: owned or joined. */
99
+ export async function accountProjects(env: Env, userId: string) {
100
+ const rows = await env.DB.prepare(
101
+ "SELECT id AS project FROM workspaces WHERE owner_id=? UNION SELECT project FROM project_members WHERE user_id=? UNION SELECT project FROM project_owners WHERE user_id=?"
102
+ )
103
+ .bind(userId, userId, userId)
104
+ .all<{ project: string }>();
105
+ return rows.results.map((row) => row.project).sort();
106
+ }
107
+
108
+ const publicToken = (row: TokenRow, now: number) => ({
109
+ id: row.id,
110
+ name: row.name,
111
+ prefix: `${PAT_PREFIX}${row.id}`,
112
+ last4: row.last4,
113
+ scopes: list(row.scopes) ?? [],
114
+ projects: list(row.projects),
115
+ createdAt: row.created_at,
116
+ expiresAt: row.expires_at,
117
+ lastUsedAt: row.last_used_at,
118
+ expired: row.expires_at !== null && row.expires_at <= now,
119
+ });
120
+
121
+ export async function listTokens(env: Env, userId: string) {
122
+ const rows = await env.DB.prepare(
123
+ "SELECT * FROM personal_access_tokens WHERE user_id=? AND revoked_at IS NULL ORDER BY created_at DESC,id"
124
+ )
125
+ .bind(userId)
126
+ .all<TokenRow>();
127
+ const now = Date.now();
128
+ return rows.results.map((row) => publicToken(row, now));
129
+ }
130
+
131
+ export async function createToken(
132
+ env: Env,
133
+ userId: string,
134
+ data: Record<string, unknown>
135
+ ) {
136
+ const name = string(data.name, 60, "token name").trim();
137
+ check(name.length > 0, 400, "Name your token.");
138
+ let expiresAt: number | null = null;
139
+ if (data.expiresInDays !== undefined && data.expiresInDays !== null) {
140
+ const days = data.expiresInDays;
141
+ check(
142
+ typeof days === "number" &&
143
+ Number.isInteger(days) &&
144
+ days >= 1 &&
145
+ days <= MAX_LIFETIME_DAYS,
146
+ 400,
147
+ `Choose an expiry between 1 and ${MAX_LIFETIME_DAYS} days.`
148
+ );
149
+ expiresAt = Date.now() + days * 86400000;
150
+ }
151
+ check(
152
+ Array.isArray(data.scopes) &&
153
+ data.scopes.length > 0 &&
154
+ data.scopes.every((scope) =>
155
+ (PAT_SCOPES as readonly unknown[]).includes(scope)
156
+ ),
157
+ 400,
158
+ `Choose at least one scope: ${PAT_SCOPES.join(", ")}.`
159
+ );
160
+ const scopes = PAT_SCOPES.filter((scope) =>
161
+ (data.scopes as unknown[]).includes(scope)
162
+ );
163
+ let projects: string[] | null = null;
164
+ if (data.projects !== undefined && data.projects !== null) {
165
+ check(
166
+ Array.isArray(data.projects) && data.projects.length <= 50,
167
+ 400,
168
+ "Invalid project list."
169
+ );
170
+ if (data.projects.length) {
171
+ projects = [...new Set(data.projects.map(String))].sort();
172
+ const allowed = new Set(await accountProjects(env, userId));
173
+ check(
174
+ projects.every((project) => allowed.has(project)),
175
+ 400,
176
+ "You can only limit a token to projects you own or have joined."
177
+ );
178
+ }
179
+ }
180
+ const active = await env.DB.prepare(
181
+ "SELECT COUNT(*) AS used FROM personal_access_tokens WHERE user_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)"
182
+ )
183
+ .bind(userId, Date.now())
184
+ .first<{ used: number }>();
185
+ check(
186
+ (active?.used ?? 0) < MAX_ACTIVE_TOKENS,
187
+ 409,
188
+ `You can have up to ${MAX_ACTIVE_TOKENS} active tokens. Revoke one first.`
189
+ );
190
+ const id = randomId(12);
191
+ const secret = randomSecret();
192
+ const salt = randomId(16);
193
+ const now = Date.now();
194
+ await env.DB.batch([
195
+ env.DB.prepare(
196
+ "INSERT INTO personal_access_tokens(id,user_id,name,last4,salt,token_hash,scopes,projects,created_at,expires_at) VALUES(?,?,?,?,?,?,?,?,?,?)"
197
+ ).bind(
198
+ id,
199
+ userId,
200
+ name,
201
+ secret.slice(-4),
202
+ salt,
203
+ await digest(env, salt, secret),
204
+ JSON.stringify(scopes),
205
+ projects && JSON.stringify(projects),
206
+ now,
207
+ expiresAt
208
+ ),
209
+ auditStatement(env, userId, "token.create", id, {
210
+ name,
211
+ scopes,
212
+ projects,
213
+ expiresAt,
214
+ }),
215
+ ]);
216
+ return {
217
+ token: `${PAT_PREFIX}${id}_${secret}`,
218
+ item: publicToken(
219
+ {
220
+ id,
221
+ user_id: userId,
222
+ name,
223
+ last4: secret.slice(-4),
224
+ salt,
225
+ token_hash: "",
226
+ scopes: JSON.stringify(scopes),
227
+ projects: projects && JSON.stringify(projects),
228
+ created_at: now,
229
+ expires_at: expiresAt,
230
+ last_used_at: null,
231
+ revoked_at: null,
232
+ },
233
+ now
234
+ ),
235
+ };
236
+ }
237
+
238
+ export async function revokeToken(env: Env, userId: string, id: string) {
239
+ const results = await env.DB.batch([
240
+ env.DB.prepare(
241
+ "UPDATE personal_access_tokens SET revoked_at=? WHERE id=? AND user_id=? AND revoked_at IS NULL"
242
+ ).bind(Date.now(), id, userId),
243
+ ]);
244
+ check(results[0].meta.changes, 404, "Token not found.");
245
+ await auditStatement(env, userId, "token.revoke", id, {}).run();
246
+ }
247
+
248
+ /**
249
+ * Resolves a bearer personal access token to its owner. 401 for unknown,
250
+ * revoked, or expired tokens; 403 when the token's scopes or project list
251
+ * do not cover the request. `access` is the permission the route needs;
252
+ * routes that pass none refuse tokens entirely.
253
+ */
254
+ export async function authenticateToken(
255
+ env: Env,
256
+ bearer: string,
257
+ project: string,
258
+ access: PatAccess | undefined,
259
+ request: Request
260
+ ): Promise<TokenUser> {
261
+ const parts = bearer.match(PAT_PATTERN);
262
+ const row = parts
263
+ ? await env.DB.prepare(
264
+ "SELECT * FROM personal_access_tokens WHERE id=?"
265
+ )
266
+ .bind(parts[1])
267
+ .first<TokenRow>()
268
+ : null;
269
+ const expected = await digest(
270
+ env,
271
+ row?.salt ?? "-",
272
+ parts?.[2] ?? bearer
273
+ );
274
+ const encoder = new TextEncoder();
275
+ const stored = encoder.encode(row?.token_hash ?? "0".repeat(64));
276
+ const given = encoder.encode(expected);
277
+ const now = Date.now();
278
+ const valid =
279
+ row &&
280
+ stored.length === given.length &&
281
+ timingSafeEqual(stored, given) &&
282
+ row.revoked_at === null &&
283
+ (row.expires_at === null || row.expires_at > now);
284
+ check(valid, 401, "This access token is invalid, expired, or revoked.");
285
+ const user = await env.DB.prepare("SELECT * FROM users WHERE id=?")
286
+ .bind(row.user_id)
287
+ .first<TokenUser>();
288
+ check(
289
+ user && user.verified && user.id.startsWith("google:"),
290
+ 401,
291
+ "This access token is invalid, expired, or revoked."
292
+ );
293
+ check(
294
+ access,
295
+ 403,
296
+ "Access tokens can only read and write comments."
297
+ );
298
+ const scopes = list(row.scopes) ?? [];
299
+ check(
300
+ access === "any" ||
301
+ scopes.includes(access === "read" ? "comments:read" : "comments:write"),
302
+ 403,
303
+ `This token needs the comments:${access === "read" ? "read" : "write"} scope.`
304
+ );
305
+ const projects = list(row.projects);
306
+ check(
307
+ !projects || projects.includes(project),
308
+ 403,
309
+ "This token is not allowed for this project."
310
+ );
311
+ // Record use at most once a minute per token: last_used_at is the throttle.
312
+ const touched = await env.DB.prepare(
313
+ "UPDATE personal_access_tokens SET last_used_at=? WHERE id=? AND (last_used_at IS NULL OR last_used_at<?)"
314
+ )
315
+ .bind(now, row.id, now - USE_LOG_INTERVAL)
316
+ .run();
317
+ if (touched.meta.changes)
318
+ await auditStatement(env, row.user_id, "token.use", row.id, {
319
+ project,
320
+ method: request.method,
321
+ path: new URL(request.url).pathname,
322
+ }).run();
323
+ return user;
324
+ }
@@ -1,5 +1,6 @@
1
1
  import { check, string, originAllowed } from "./validation";
2
2
  import type { Identity } from "../src/types";
3
+ import { pruneAudit } from "./tokens";
3
4
  import { editedOrigins, siteEdits } from "./project-sites";
4
5
  export type Project = {
5
6
  repo: string;
@@ -158,6 +159,7 @@ export async function maintain(db: D1Database, scheduled = false) {
158
159
  ...["rate_limits", "sessions", "project_invites", "oauth_states", "setup_requests"].map(
159
160
  table => db.prepare(`DELETE FROM ${table} WHERE expires_at<?`).bind(now)
160
161
  ),
162
+ pruneAudit(db, now),
161
163
  db.prepare("DELETE FROM users WHERE id LIKE 'guest:%' AND id NOT IN (SELECT user_id FROM sessions) AND id NOT IN (SELECT user_id FROM comments) AND id NOT IN (SELECT user_id FROM project_members)"),
162
164
  ]);
163
165
  } catch (error) {
@@ -1 +0,0 @@
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 as d,m as l,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,$=new WeakMap,E=!1;function C(e){let t={};for(let n of e?.themes??[])try{t[n.id]=n.current()}catch{}return t}function P(e,t={}){let n=$.get(e);n||$.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 S(e,t){return Promise.resolve().then(()=>e.resolve(t)).then(e=>e??{},()=>({}))}function N(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 A(e,t){let n=N(t);return n?e.find(e=>("color"===e.type||"dimension"===e.type)&&N(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 O(e){let t=w.get(e);t?.pop()?.undo(),t&&!t.length&&w.delete(e)}function R(e){return!!w.get(e)?.length}function q(e){for(;w.has(e);)O(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,_=new WeakSet,F=new WeakMap;function U(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){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}: ${U(t)} → ${U(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")}var te=e=>String(e).replace(/\s+/g," ").trim();function ne(e,o){let i,r=Object.entries(C(o)).map(([e,t])=>`${te(e)}=${te(t)}`).join(", "),a=t(e);if(!a)try{i=o?.components?.identify(e)}catch{}let s=a?{root:e,component:a}:i?.name?void 0:n(e);if(s){let{definition:e,props:t}=s.component;i={name:e.name,source:`${e.source}:${e.line}`,props:{...t,...Object.fromEntries(H.get(s.root)??[])}}}let d=Object.entries(i?.props??{}).filter(([,e])=>null===e||["boolean","number"].includes(typeof e)||"string"==typeof e&&e.length<=80).slice(0,12).map(([e,t])=>`${te(e)}=${JSON.stringify(t).replace(/→/g,"\\u2192")}`);return[r&&`Theme: ${r}`,i?.name&&`Component: ${te(i.name)}${i.source?` (${te(i.source)})`:""}`,d.length&&`Props: ${d.join(", ")}`].filter(e=>!!e)}function oe(e){return[ee(e),x(e),B(e)].filter(e=>!!e).join("\n\n")||void 0}function ie(e,n,o){F.get(e)?.abort();let i=new AbortController;F.set(e,i),e.addEventListener("click",e=>{let i=n();if(!i)return;let r=x(i)||R(i);!t(i)&&!r||(e.stopImmediatePropagation(),o(i,oe(i)))},{capture:!0,signal:i.signal})}function re(e){return b(e)}function ae(e){return h(e)||R(e)}function se(e){k(e)}function de(e){v(e),q(e)}function le(e){if(!e||_.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}@media (max-width:520px){.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),_.add(e)}function pe(e,t,n,r){let a=o(e);if(!a)return null;let s=m(a,t);if(!s.length)return null;le(r);let d=document.createElement("span");d.className="design-token-binding",d.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(),$=()=>{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,l=r??o?.token;k.value=l?.path??"custom",h.textContent=l?.path??"";let p=n?.declaredPath,c=r?"token":o?"connected":p?"override":"custom";if(d.dataset.mode=c,d.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,A(s.map(e=>({path:e.path,type:e.type,value:i(a,e)})),n))}},E=new MutationObserver($);E.observe(e,{attributes:!0,attributeFilter:["style"]});let C=new MutationObserver(()=>{d.isConnected||(E.disconnect(),C.disconnect())});return C.observe(d.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)&&l(a,e,i,t,{recordHistory:!1})&&n(o,e.style.getPropertyValue(t),i.path);else{let i=d.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)}$()}),b.addEventListener("click",()=>{let e=s.find(e=>e.path===k.value),t=d.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")),d.parentElement?.querySelector("[data-property]")?.focus()}),d.append(h,k,c,b,w,v),queueMicrotask($),$(),d}function ce(e){let t=o(e);return new Set([...t?.elementBindings.get(e)?.keys()??[],...D(e)])}function ue(e,t,n,o,i,r,a,s,d){let l=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 l){if(i.querySelector(".design-token-binding")||!n.contains(i))continue;let d=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)});d&&i.append(d)}},c=s?.tokens;if(c)return le(i),Promise.all([P(c,C(s)),S(c,e)]).then(([t,n])=>{p((o,i,r,a)=>fe(c,e,o.property,t,n[o.property],e=>{d?.(e),a()},i,r))},()=>{});p((t,n,r,a)=>pe(e,t.property,e=>{o(t,r,e),a()},i))}function fe(e,t,n,o,i,r,a,s){let d=function(e,t){return e.filter(e=>e.properties?.includes(t))}(o,n);if(!d.length&&!i)return null;let l=i&&"offToken"in i?i.offToken:void 0,p=void 0===l?void 0:A(d,l),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=!d.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 d){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!==l&&j(k,l,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&&d.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===l||!!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=d.find(e=>e.path===m.value);if(!a)return x();y=!1;let s=i??{offToken:getComputedStyle(t).getPropertyValue(n).trim()},l="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)}),()=>O(t)}(e,t,n,a,s,l)),x()}),g.addEventListener("click",()=>{y=!0,x(),s?.focus()}),c.append(u,m,f,g,k,v),x(),c}var ge=!1;function me(e,t,n,i){let r=o(e);if(!r||!r.tokens.length)return null;le(i);let a=document.createElement("details");a.className="design-group design-theme-group",a.open=ge,a.addEventListener("toggle",()=>{a.isConnected&&(ge=a.open)});let s=document.createElement("summary");s.textContent="Shared theme";let d=document.createElement("small");d.className="design-react-source",d.textContent="Changes every element using these tokens",a.append(s,d);let l=new Map;for(let e of r.tokens){let t=e.path.split(".")[0];l.set(t,[...l.get(t)??[],e])}let p=r.tokens.length>6&&l.size>1;for(let[e,o]of p?l: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(he(r,i,t,n,e))}return a}function he(e,t,n,o,s){let l=document.createElement("div");l.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=!l.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",()=>{d(e,t),b(),o()}),b(),l.append(p,g,v),l}function be(e,t,n){let i=o(e);i&&p(i,e,t,n)}function ke(e,t){let n=o(e);n&&c(n,e,t)}function ve(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 ye(){for(let e of[...H.keys()])xe(e);H.clear(),I.clear(),y(),function(){for(let e of[...w.keys()])q(e)}()}function xe(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 we(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 d=n.props[o];if(Object.is(d,i))return!0;let l=H.get(t);l||H.set(t,l=new Map);let p=l.has(o)?l.get(o):d;Object.is(p,i)?l.delete(o):l.set(o,p),l.size||H.delete(t);let c=I.get(t);return c||I.set(t,c=[]),c.push({propName:o,before:d,after:i}),n.setProps({...n.props,[o]:i}),a(),!0}function $e(e,i,r,a){let s=t(e);if(!s)return function(e,t,o){let i=n(e);if(!i)return null;le(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);le(r);let d=document.createElement("details");d.className="design-group design-react-group",d.open=!0;let l=document.createElement("summary");l.textContent="Properties",d.append(l);let p=document.createElement("small");p.className="design-react-source",p.textContent=`${s.definition.name} · ${s.definition.source}:${s.definition.line}`,d.append(p);for(let t of s.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 l,p=s.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),l=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:""),l=e}if(l.setAttribute("aria-label",`${s.definition.name} ${t.name}`),"unsupported"===t.kind)l=document.createElement("input"),l.disabled=!0,l.value=`Read only · ${t.typeLabel}`,l.setAttribute("aria-label",`${s.definition.name} ${t.name}, read only`),r.title=`Komo does not edit ${t.typeLabel} props.`;else{let n=()=>{let n=l instanceof HTMLInputElement&&"checkbox"===l.type?l.checked:l.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);we(e,s,t.name,o,l,i)};l.addEventListener("change",n)}r.append(a,l);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=>s.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 s.props[t.name]&&String(s.props[t.name]).startsWith("var(")?e.ownerDocument.defaultView?.getComputedStyle(e).getPropertyValue(n).trim()??"":"string"==typeof s.props[t.name]?s.props[t.name]:"";we(e,s,t.name,r,l,i)&&u()}),o.append(a,p),r.append(o),u()}}d.append(r)}return d}export{ue as attachStyleTokenControls,X as beginReactTextEdit,re as canUndoDesignTokenEdit,Z as canUndoReactProp,Q as cancelReactTextEdit,fe as createAdapterTokenControl,me as createDesignThemePanel,pe as createElementTokenControl,$e as createReactPropsPanel,ne as designContext,G as finishReactTextEdit,ae as hasDesignTokenEdit,K as hasReactPropChanges,ie as installCommentCapture,be as noteDesignTokenOverride,Y as previewReactTextEdit,oe as proposedDesignChanges,ee as proposedReactChanges,de as resetDesignTokenEdits,ye as resetPreviews,xe as resetReactProps,J as showDesignBefore,ke as syncDesignTokenBinding,ce as tokenBoundProperties,se as undoDesignTokenEdit,ve as undoReactProp};