@hazem3500/komo 0.8.0 → 0.9.1

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 (46) hide show
  1. package/NODE.md +20 -0
  2. package/README.md +64 -0
  3. package/dist/accent-picker-F5UI2NP7.js +1 -0
  4. package/dist/adapters/class-theme.d.ts +22 -0
  5. package/dist/adapters/class-theme.js +1 -0
  6. package/dist/adapters/tailwind.d.ts +62 -0
  7. package/dist/adapters/tailwind.js +1 -0
  8. package/dist/chunk-5LHGJFWQ.js +1 -0
  9. package/dist/chunk-JC6FXUNO.js +1 -0
  10. package/dist/{chunk-RPKLGTEU.js → chunk-Q7WSWLMV.js} +1 -1
  11. package/dist/{chunk-E5X4IQJ2.js → chunk-YZP3KX4Y.js} +1 -1
  12. package/dist/design-comment-RTVUFVDE.js +1 -0
  13. package/dist/design-comment.d.ts +50 -0
  14. package/dist/design-mode-styles.d.ts +1 -1
  15. package/dist/design-mode.d.ts +2 -0
  16. package/dist/design-mode.js +1 -1
  17. package/dist/design-system.d.ts +150 -0
  18. package/dist/design-tokens.d.ts +2 -0
  19. package/dist/dom.d.ts +10 -0
  20. package/dist/index.js +1 -1
  21. package/dist/inline-text-editor-SWPVB35B.js +1 -0
  22. package/dist/inline-text-editor.d.ts +3 -0
  23. package/dist/node.mjs +218 -65
  24. package/dist/{onboarding-YA63RUKK.js → onboarding-NO3RQWR7.js} +1 -1
  25. package/dist/react-inspector.d.ts +9 -1
  26. package/dist/react-props-panel-XQ4T3RLV.js +1 -0
  27. package/dist/react-props-panel.d.ts +3 -1
  28. package/dist/react.js +1 -1
  29. package/dist/review-layout.d.ts +3 -1
  30. package/dist/setup.js +1 -1
  31. package/dist/tag-editor-ZA7NLPOJ.js +1 -0
  32. package/dist/toolbar-runtime-2JO6YXD5.js +1 -0
  33. package/dist/types.d.ts +20 -0
  34. package/package.json +11 -2
  35. package/server/iap.ts +133 -0
  36. package/server/index.ts +108 -17
  37. package/server/secrets.d.ts +6 -0
  38. package/server/setup-client.txt +142 -70
  39. package/src/types.ts +21 -0
  40. package/dist/accent-picker-HSUCA4KE.js +0 -1
  41. package/dist/chunk-Z2ABBESO.js +0 -1
  42. package/dist/chunk-ZUNTYWJF.js +0 -1
  43. package/dist/inline-text-editor-F7B6FQQ3.js +0 -1
  44. package/dist/react-props-panel-XSMDR2CY.js +0 -1
  45. package/dist/tag-editor-VDPZ6CO3.js +0 -1
  46. package/dist/toolbar-runtime-MYOLQPTJ.js +0 -1
package/server/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  requireMember,
6
6
  } from "./project-management";
7
7
  import { setupPage } from "./setup-page";
8
+ import { authPrefix, iapIdentity, type IapIdentity } from "./iap";
8
9
  import setupClient from "./setup-client.txt";
9
10
  import {
10
11
  dispatchWebhook,
@@ -338,7 +339,7 @@ async function oauthAuthorize(
338
339
  authorize.search = new URLSearchParams({
339
340
  client_id:
340
341
  provider === "google" ? env.GOOGLE_CLIENT_ID : env.GITHUB_CLIENT_ID,
341
- redirect_uri: `${url.origin}/auth/${provider}/callback`,
342
+ redirect_uri: `${url.origin}${env.KOMO_PUBLIC_PATH ?? ""}/auth/${provider}/callback`,
342
343
  ...(provider === "google"
343
344
  ? { response_type: "code", scope: "openid profile email" }
344
345
  : {}),
@@ -413,7 +414,7 @@ async function oauthCallback(
413
414
  : env.GITHUB_CLIENT_SECRET,
414
415
  grant_type: "authorization_code",
415
416
  code,
416
- redirect_uri: `${url.origin}/auth/${provider}/callback`,
417
+ redirect_uri: `${url.origin}${env.KOMO_PUBLIC_PATH ?? ""}/auth/${provider}/callback`,
417
418
  code_verifier: saved.verifier,
418
419
  }),
419
420
  }
@@ -495,16 +496,23 @@ async function oauthCallback(
495
496
  )
496
497
  : undefined;
497
498
  const accessToken = await session(env, created?.project ?? saved.project, id);
499
+ return signedInPage(
500
+ {
501
+ type: created ? "komo:setup" : "branch-comments:auth",
502
+ ...(created ? { ...created, code: setupCode } : {}),
503
+ token: accessToken,
504
+ user,
505
+ },
506
+ saved.origin
507
+ );
508
+ }
509
+ // The popup posts the session back to the site that opened it, then closes.
510
+ function signedInPage(message: Record<string, unknown>, origin: string) {
498
511
  const nonce = crypto.randomUUID();
499
- const message = JSON.stringify({
500
- type: created ? "komo:setup" : "branch-comments:auth",
501
- ...(created ? { ...created, code: setupCode } : {}),
502
- token: accessToken,
503
- user,
504
- }).replace(/</g, "\\u003c");
505
- const target = JSON.stringify(saved.origin).replace(/</g, "\\u003c");
512
+ const body = JSON.stringify(message).replace(/</g, "\\u003c");
513
+ const target = JSON.stringify(origin).replace(/</g, "\\u003c");
506
514
  return new Response(
507
- `<!doctype html><meta charset="utf-8"><title>Signed in</title><p>You’re signed in. You can close this window and return to your comments.</p><script nonce="${nonce}">if(window.opener){window.opener.postMessage(${message},${target});window.close();}</script>`,
515
+ `<!doctype html><meta charset="utf-8"><title>Signed in</title><p>You’re signed in. You can close this window and return to your comments.</p><script nonce="${nonce}">if(window.opener){window.opener.postMessage(${body},${target});window.close();}</script>`,
508
516
  {
509
517
  headers: {
510
518
  "Content-Type": "text/html; charset=utf-8",
@@ -516,6 +524,44 @@ async function oauthCallback(
516
524
  }
517
525
  );
518
526
  }
527
+ // Records the IAP-verified Google account as a verified reviewer.
528
+ async function iapUser(env: Env, signedIn: IapIdentity) {
529
+ await env.DB.prepare(
530
+ "INSERT INTO users(id,name,verified) VALUES(?,?,1) ON CONFLICT(id) DO NOTHING"
531
+ )
532
+ .bind(signedIn.id, signedIn.email.split("@")[0].slice(0, 60))
533
+ .run();
534
+ await env.DB.prepare("UPDATE users SET email=?,verified=1 WHERE id=?")
535
+ .bind(signedIn.email, signedIn.id)
536
+ .run();
537
+ return identity(
538
+ (await env.DB.prepare("SELECT * FROM users WHERE id=?")
539
+ .bind(signedIn.id)
540
+ .first<UserRow>())!
541
+ );
542
+ }
543
+ // Behind IAP the authorize step is the whole sign-in: IAP has already
544
+ // authenticated the browser, so there is no Google redirect or callback.
545
+ async function iapSignIn(request: Request, env: Env) {
546
+ const url = new URL(request.url);
547
+ const state = string(url.searchParams.get("state"), 200, "state");
548
+ const signedIn = await iapIdentity(request, env.KOMO_IAP_AUDIENCE!);
549
+ const saved = await env.DB.prepare(
550
+ "DELETE FROM oauth_states WHERE state_hash=? AND expires_at>? AND provider=? RETURNING project,origin"
551
+ )
552
+ .bind(await hash(state), Date.now(), "google")
553
+ .first<{ project: string; origin: string }>();
554
+ check(saved, 400, "Sign-in expired. Return to the site and try again.");
555
+ const user = await iapUser(env, signedIn);
556
+ return signedInPage(
557
+ {
558
+ type: "branch-comments:auth",
559
+ token: await session(env, saved.project, user.id),
560
+ user,
561
+ },
562
+ saved.origin
563
+ );
564
+ }
519
565
  async function route(
520
566
  request: Request,
521
567
  env: Env,
@@ -640,7 +686,7 @@ async function route(
640
686
  )
641
687
  .bind(id, await hash(secret), JSON.stringify(config), Date.now() + 600000)
642
688
  .run();
643
- return json({ id, secret, url: `${url.origin}/setup?code=${id}` });
689
+ return json({ id, secret, url: `${url.origin}${env.KOMO_PUBLIC_PATH ?? ""}/setup?code=${id}` });
644
690
  }
645
691
  if (
646
692
  url.pathname === "/setup/poll" &&
@@ -668,7 +714,7 @@ async function route(
668
714
  ? {
669
715
  project: row.project,
670
716
  repo: JSON.parse(row.config).repo,
671
- endpoint: url.origin,
717
+ endpoint: `${url.origin}${env.KOMO_PUBLIC_PATH ?? ""}`,
672
718
  }
673
719
  : { pending: true }
674
720
  );
@@ -676,6 +722,33 @@ async function route(
676
722
  const oauthPath = url.pathname.match(
677
723
  /^\/auth\/(github|google)\/(authorize|callback)$/
678
724
  );
725
+ if (env.KOMO_IAP_AUDIENCE) {
726
+ // IAP sign-in replaces OAuth. Its handlers live only under KOMO_AUTH_PREFIX,
727
+ // the path the load balancer puts behind IAP.
728
+ const prefix = authPrefix(env.KOMO_AUTH_PREFIX);
729
+ const signInPath =
730
+ !prefix || url.pathname.startsWith(`${prefix}/auth/`)
731
+ ? url.pathname.slice(prefix.length)
732
+ : "";
733
+ if (signInPath === "/auth/google/authorize") return iapSignIn(request, env);
734
+ // For agents: an IAP-authenticated caller exchanges its identity for a
735
+ // project session, the non-interactive counterpart of the popup.
736
+ if (signInPath === "/auth/iap/session" && request.method === "POST") {
737
+ const signedIn = await iapIdentity(request, env.KOMO_IAP_AUDIENCE);
738
+ const target = string(url.searchParams.get("project"), 100, "project");
739
+ check(await projectConfig(env, target), 404, "Unknown comments project.");
740
+ const user = await iapUser(env, signedIn);
741
+ return json({ token: await session(env, target, user.id), user }, 201);
742
+ }
743
+ check(!oauthPath || oauthPath[1] === "google", 403, "Sign in with your Google account.");
744
+ check(
745
+ !oauthPath &&
746
+ url.pathname !== "/auth/iap/session" &&
747
+ !(prefix && url.pathname.startsWith(`${prefix}/`)),
748
+ 404,
749
+ "Not found."
750
+ );
751
+ }
679
752
  if (oauthPath)
680
753
  return oauthPath[2] === "authorize"
681
754
  ? oauthAuthorize(request, env, oauthPath[1] as "github" | "google")
@@ -861,8 +934,12 @@ async function route(
861
934
  return json({
862
935
  repo: config.repo,
863
936
  github: !!(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET),
864
- google: !!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET),
937
+ google: !!(
938
+ env.KOMO_IAP_AUDIENCE ||
939
+ (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET)
940
+ ),
865
941
  guests:
942
+ !env.KOMO_IAP_AUDIENCE &&
866
943
  config.allowGuests !== false &&
867
944
  (!config.requireOwner || !!owner) &&
868
945
  !(await privateAccess(env, project)),
@@ -931,6 +1008,7 @@ async function route(
931
1008
  });
932
1009
  }
933
1010
  if (url.pathname === "/auth/guest" && request.method === "POST") {
1011
+ check(!env.KOMO_IAP_AUDIENCE, 403, "Guest comments are disabled.");
934
1012
  check(
935
1013
  !(await privateAccess(env, project)),
936
1014
  403,
@@ -958,9 +1036,11 @@ async function route(
958
1036
  ) {
959
1037
  const provider = url.pathname.includes("/google/") ? "google" : "github";
960
1038
  check(
961
- provider === "google"
962
- ? env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
963
- : env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET,
1039
+ env.KOMO_IAP_AUDIENCE
1040
+ ? provider === "google"
1041
+ : provider === "google"
1042
+ ? env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
1043
+ : env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET,
964
1044
  503,
965
1045
  "Sign-in is not configured."
966
1046
  );
@@ -984,7 +1064,8 @@ async function route(
984
1064
  )
985
1065
  .run();
986
1066
  return json({
987
- url: `${url.origin}/auth/${provider}/authorize?state=${encodeURIComponent(state)}`,
1067
+ // KOMO_PUBLIC_PATH covers apps served under a path behind a proxy that strips it.
1068
+ url: `${url.origin}${env.KOMO_PUBLIC_PATH ?? ""}${env.KOMO_IAP_AUDIENCE ? authPrefix(env.KOMO_AUTH_PREFIX) : ""}/auth/${provider}/authorize?state=${encodeURIComponent(state)}`,
988
1069
  });
989
1070
  }
990
1071
  if (url.pathname === "/me") {
@@ -1047,6 +1128,16 @@ async function route(
1047
1128
  check(request.method === "GET", 405, "Method not allowed.");
1048
1129
  return json({ user });
1049
1130
  }
1131
+ // Behind IAP, comments are never readable or writable without a session
1132
+ // minted from an IAP-verified Google account.
1133
+ if (env.KOMO_IAP_AUDIENCE) {
1134
+ const reader = await authenticate(request, env, project);
1135
+ check(
1136
+ reader.verified && reader.id.startsWith("google:"),
1137
+ 403,
1138
+ "Sign in with your Google account to see comments."
1139
+ );
1140
+ }
1050
1141
  if (await privateAccess(env, project)) {
1051
1142
  await requireMember(
1052
1143
  env,
@@ -8,6 +8,12 @@ interface Env {
8
8
  ) => Promise<void>;
9
9
  KOMO_HOSTED?: string;
10
10
  KOMO_PAUSED?: string;
11
+ /** Enables Google Cloud IAP sign-in: the IAP backend service audience. */
12
+ KOMO_IAP_AUDIENCE?: string;
13
+ /** Path prefix for IAP sign-in handlers, e.g. "/backoffice". */
14
+ KOMO_AUTH_PREFIX?: string;
15
+ /** Path the app is served under behind a proxy that strips it, e.g. "/komo". */
16
+ KOMO_PUBLIC_PATH?: string;
11
17
  EDGE_LIMIT?: RateLimit;
12
18
  KOMO_VERSION?: { id: string };
13
19
  }