@hazem3500/komo 0.9.2 → 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.
Files changed (44) hide show
  1. package/NODE.md +1 -1
  2. package/README.md +84 -9
  3. package/cli/agent.mjs +122 -27
  4. package/cli/design-edit.mjs +269 -0
  5. package/dist/adapters/css-variables.d.ts +9 -0
  6. package/dist/adapters/css-variables.js +1 -0
  7. package/dist/adapters/react-fiber.d.ts +27 -0
  8. package/dist/adapters/react-fiber.js +1 -0
  9. package/dist/agent-prompt.d.ts +3 -1
  10. package/dist/agent-prompt.js +1 -1
  11. package/dist/{chunk-Y2GRLW4Q.js → chunk-DH22G2S5.js} +1 -1
  12. package/dist/chunk-NWRSH6AL.js +1 -0
  13. package/dist/design-comment-MPAF2YAN.js +1 -0
  14. package/dist/design-export-DD2NJI4O.js +1 -0
  15. package/dist/design-export.d.ts +9 -2
  16. package/dist/design-mode-styles.d.ts +1 -1
  17. package/dist/design-mode.js +1 -1
  18. package/dist/design-panel-NU2WUI4D.js +1 -0
  19. package/dist/design-panel-styles.d.ts +1 -1
  20. package/dist/design-system.d.ts +24 -5
  21. package/dist/index.js +1 -1
  22. package/dist/{inline-text-editor-3JLDAUPO.js → inline-text-editor-JWVS7J75.js} +1 -1
  23. package/dist/node.mjs +513 -18
  24. package/dist/postgres/004_personal_access_tokens.sql +25 -0
  25. package/dist/react-props-panel-BW4ZQPD7.js +1 -0
  26. package/dist/react-props-panel.d.ts +19 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/select-menu-V7RYOZFK.js +1 -0
  30. package/dist/select-menu.d.ts +5 -0
  31. package/dist/setup.js +1 -1
  32. package/package.json +14 -3
  33. package/server/account-page.ts +231 -0
  34. package/server/index.ts +62 -5
  35. package/server/migrations/0018_personal_access_tokens.sql +25 -0
  36. package/server/secrets.d.ts +2 -0
  37. package/server/setup-client.txt +54 -59
  38. package/server/tokens.ts +324 -0
  39. package/server/workspaces.ts +2 -0
  40. package/dist/chunk-HCGLCO3R.js +0 -1
  41. package/dist/design-comment-MWNQNKWT.js +0 -1
  42. package/dist/design-export-ZHCBPSWY.js +0 -1
  43. package/dist/design-panel-BPVXFERR.js +0 -1
  44. package/dist/react-props-panel-R5XEVATL.js +0 -1
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(request, env, project);
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);
@@ -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. */