@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.
- package/NODE.md +1 -1
- package/README.md +84 -9
- package/cli/agent.mjs +122 -27
- package/cli/design-edit.mjs +269 -0
- 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/agent-prompt.d.ts +3 -1
- package/dist/agent-prompt.js +1 -1
- package/dist/{chunk-Y2GRLW4Q.js → chunk-DH22G2S5.js} +1 -1
- package/dist/chunk-NWRSH6AL.js +1 -0
- package/dist/design-comment-MPAF2YAN.js +1 -0
- package/dist/design-export-DD2NJI4O.js +1 -0
- package/dist/design-export.d.ts +9 -2
- package/dist/design-mode-styles.d.ts +1 -1
- package/dist/design-mode.js +1 -1
- package/dist/design-panel-NU2WUI4D.js +1 -0
- package/dist/design-panel-styles.d.ts +1 -1
- package/dist/design-system.d.ts +24 -5
- package/dist/index.js +1 -1
- package/dist/{inline-text-editor-3JLDAUPO.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 +19 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/select-menu-V7RYOZFK.js +1 -0
- package/dist/select-menu.d.ts +5 -0
- 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 +54 -59
- package/server/tokens.ts +324 -0
- package/server/workspaces.ts +2 -0
- package/dist/chunk-HCGLCO3R.js +0 -1
- package/dist/design-comment-MWNQNKWT.js +0 -1
- package/dist/design-export-ZHCBPSWY.js +0 -1
- package/dist/design-panel-BPVXFERR.js +0 -1
- 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(
|
|
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. */
|