@hazem3500/komo 0.0.0-stage → 0.9.0

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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/NODE.md +80 -0
  3. package/NOTICE.md +45 -0
  4. package/README.md +411 -2
  5. package/cli/agent.mjs +615 -0
  6. package/cli/config.mjs +34 -0
  7. package/cli/index.mjs +405 -0
  8. package/cli/project.mjs +136 -0
  9. package/cli/workflow.mjs +42 -0
  10. package/dist/MorphingMenu.d.ts +30 -0
  11. package/dist/PointerIcon.d.ts +2 -0
  12. package/dist/SmileIcon.d.ts +2 -0
  13. package/dist/THIRD_PARTY_NOTICES.txt +387 -0
  14. package/dist/accent-picker-F5UI2NP7.js +1 -0
  15. package/dist/accent-picker.d.ts +4 -0
  16. package/dist/accent.d.ts +2 -0
  17. package/dist/account-usage.d.ts +2 -0
  18. package/dist/action-menu.d.ts +10 -0
  19. package/dist/agent-prompt.d.ts +6 -0
  20. package/dist/agent-prompt.js +1 -0
  21. package/dist/anchors.d.ts +48 -0
  22. package/dist/api.d.ts +39 -0
  23. package/dist/approved-sites.d.ts +9 -0
  24. package/dist/card-motion.d.ts +8 -0
  25. package/dist/chunk-7YUXYVQA.js +1 -0
  26. package/dist/chunk-I7AUKTXE.js +1 -0
  27. package/dist/chunk-JQVBZ2BB.js +1 -0
  28. package/dist/chunk-Q7WSWLMV.js +1 -0
  29. package/dist/chunk-Y2GRLW4Q.js +1 -0
  30. package/dist/chunk-YZP3KX4Y.js +1 -0
  31. package/dist/chunk-Z2ABBESO.js +1 -0
  32. package/dist/config.d.ts +11 -0
  33. package/dist/connect-project.d.ts +14 -0
  34. package/dist/connection-issue.d.ts +7 -0
  35. package/dist/design-comment-QPTW66JO.js +1 -0
  36. package/dist/design-comment.d.ts +42 -0
  37. package/dist/design-export-BRBI7S3V.js +1 -0
  38. package/dist/design-export.d.ts +8 -0
  39. package/dist/design-mode-styles.d.ts +1 -0
  40. package/dist/design-mode.d.ts +19 -0
  41. package/dist/design-mode.js +1 -0
  42. package/dist/design-panel-BPVXFERR.js +1 -0
  43. package/dist/design-panel-styles.d.ts +1 -0
  44. package/dist/design-panel.d.ts +33 -0
  45. package/dist/design-tokens.d.ts +76 -0
  46. package/dist/dom.d.ts +20 -0
  47. package/dist/drawer-expand-motion.d.ts +46 -0
  48. package/dist/drawer-presence.d.ts +4 -0
  49. package/dist/edge-sidebar.d.ts +39 -0
  50. package/dist/emoji-history.d.ts +8 -0
  51. package/dist/emoji.d.ts +1 -0
  52. package/dist/floating-drag.d.ts +9 -0
  53. package/dist/icon-markup.d.ts +4 -0
  54. package/dist/index.d.ts +8 -0
  55. package/dist/index.js +1 -0
  56. package/dist/inline-text-editor-F7B6FQQ3.js +1 -0
  57. package/dist/inline-text-editor.d.ts +70 -0
  58. package/dist/lazy-accent-picker.d.ts +4 -0
  59. package/dist/lazy-toolbar.d.ts +19 -0
  60. package/dist/message-links.d.ts +2 -0
  61. package/dist/morphing-menu-styles.d.ts +1 -0
  62. package/dist/node.mjs +2607 -0
  63. package/dist/onboarding-NO3RQWR7.js +1 -0
  64. package/dist/onboarding.d.ts +6 -0
  65. package/dist/optimistic.d.ts +26 -0
  66. package/dist/page.d.ts +2 -0
  67. package/dist/picker-WQN53YJ5.js +1 -0
  68. package/dist/pin-direction.d.ts +12 -0
  69. package/dist/pin-stacks.d.ts +3 -0
  70. package/dist/polling.d.ts +10 -0
  71. package/dist/postgres/001_initial.sql +128 -0
  72. package/dist/postgres/002_thread_tags.sql +1 -0
  73. package/dist/postgres/003_project_webhooks.sql +4 -0
  74. package/dist/project-management.d.ts +2 -0
  75. package/dist/react-inspector.d.ts +43 -0
  76. package/dist/react-props-panel-XSMDR2CY.js +1 -0
  77. package/dist/react-props-panel.d.ts +39 -0
  78. package/dist/react.d.ts +38 -0
  79. package/dist/react.js +1 -0
  80. package/dist/reaction-picker.d.ts +1 -0
  81. package/dist/review-layout.d.ts +23 -0
  82. package/dist/setup-browser.d.ts +1 -0
  83. package/dist/setup.d.ts +4 -0
  84. package/dist/setup.js +1 -0
  85. package/dist/shortcuts.d.ts +15 -0
  86. package/dist/static-svg.d.ts +3 -0
  87. package/dist/styles.d.ts +1 -0
  88. package/dist/tag-editor-VDPZ6CO3.js +1 -0
  89. package/dist/tag-editor.d.ts +12 -0
  90. package/dist/toolbar-runtime-2SIUE6SQ.js +1 -0
  91. package/dist/toolbar-runtime.d.ts +5 -0
  92. package/dist/types.d.ts +163 -0
  93. package/package.json +114 -4
  94. package/server/digest.ts +8 -0
  95. package/server/import-project.ts +164 -0
  96. package/server/index.ts +1656 -0
  97. package/server/migrations/0001_comments.sql +54 -0
  98. package/server/migrations/0002_identity.sql +2 -0
  99. package/server/migrations/0003_accent_color.sql +1 -0
  100. package/server/migrations/0004_workspaces.sql +46 -0
  101. package/server/migrations/0005_domains.sql +6 -0
  102. package/server/migrations/0006_member_budget.sql +16 -0
  103. package/server/migrations/0007_revisions.sql +17 -0
  104. package/server/migrations/0008_starter_comments.sql +1 -0
  105. package/server/migrations/0009_retention.sql +19 -0
  106. package/server/migrations/0010_reviewer_history.sql +1 -0
  107. package/server/migrations/0011_project_management.sql +52 -0
  108. package/server/migrations/0012_setup_sites.sql +3 -0
  109. package/server/migrations/0013_project_sites.sql +8 -0
  110. package/server/migrations/0014_project_site_removals.sql +3 -0
  111. package/server/migrations/0015_query_efficiency.sql +6 -0
  112. package/server/migrations/0016_thread_tags.sql +1 -0
  113. package/server/migrations/0017_project_webhooks.sql +4 -0
  114. package/server/node-webhooks.ts +126 -0
  115. package/server/project-management.ts +387 -0
  116. package/server/project-sites.ts +117 -0
  117. package/server/secrets.d.ts +24 -0
  118. package/server/setup-client.txt +2932 -0
  119. package/server/setup-page.ts +14 -0
  120. package/server/tsconfig.json +12 -0
  121. package/server/validation.ts +220 -0
  122. package/server/webhooks.ts +206 -0
  123. package/server/workspaces.ts +168 -0
  124. package/src/emoji.ts +7 -0
  125. package/src/page.ts +4 -0
  126. package/src/types.ts +166 -0
@@ -0,0 +1,54 @@
1
+ CREATE TABLE users (
2
+ id TEXT PRIMARY KEY,
3
+ name TEXT NOT NULL,
4
+ verified INTEGER NOT NULL DEFAULT 0
5
+ );
6
+ CREATE TABLE sessions (
7
+ token_hash TEXT PRIMARY KEY,
8
+ user_id TEXT NOT NULL REFERENCES users(id),
9
+ project TEXT NOT NULL,
10
+ expires_at INTEGER NOT NULL
11
+ );
12
+ CREATE INDEX session_expiry ON sessions(expires_at);
13
+ CREATE TABLE threads (
14
+ id TEXT PRIMARY KEY,
15
+ project TEXT NOT NULL,
16
+ repo TEXT NOT NULL,
17
+ branch TEXT NOT NULL,
18
+ page TEXT NOT NULL,
19
+ anchor TEXT NOT NULL,
20
+ resolved INTEGER NOT NULL DEFAULT 0,
21
+ resolved_by TEXT REFERENCES users(id),
22
+ created_at INTEGER NOT NULL,
23
+ updated_at INTEGER NOT NULL
24
+ );
25
+ CREATE INDEX thread_scope ON threads(project, repo, branch, updated_at);
26
+ CREATE TABLE comments (
27
+ id TEXT PRIMARY KEY,
28
+ thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
29
+ user_id TEXT NOT NULL REFERENCES users(id),
30
+ body TEXT NOT NULL,
31
+ created_at INTEGER NOT NULL,
32
+ edited_at INTEGER
33
+ );
34
+ CREATE INDEX comment_thread ON comments(thread_id, created_at);
35
+ CREATE TABLE reactions (
36
+ comment_id TEXT NOT NULL REFERENCES comments(id) ON DELETE CASCADE,
37
+ user_id TEXT NOT NULL REFERENCES users(id),
38
+ emoji TEXT NOT NULL,
39
+ PRIMARY KEY(comment_id, user_id, emoji)
40
+ );
41
+ CREATE TABLE oauth_states (
42
+ state_hash TEXT PRIMARY KEY,
43
+ verifier TEXT NOT NULL,
44
+ project TEXT NOT NULL,
45
+ origin TEXT NOT NULL,
46
+ expires_at INTEGER NOT NULL,
47
+ exchange_hash TEXT NOT NULL,
48
+ session_token TEXT
49
+ );
50
+ CREATE TABLE rate_limits (
51
+ key TEXT PRIMARY KEY,
52
+ count INTEGER NOT NULL,
53
+ expires_at INTEGER NOT NULL
54
+ );
@@ -0,0 +1,2 @@
1
+ ALTER TABLE users ADD COLUMN avatar_url TEXT;
2
+ ALTER TABLE oauth_states ADD COLUMN provider TEXT NOT NULL DEFAULT 'github';
@@ -0,0 +1 @@
1
+ ALTER TABLE users ADD COLUMN accent_color TEXT;
@@ -0,0 +1,46 @@
1
+ CREATE TABLE workspaces (
2
+ id TEXT PRIMARY KEY,
3
+ owner_id TEXT NOT NULL REFERENCES users(id),
4
+ repo TEXT NOT NULL,
5
+ origins TEXT NOT NULL,
6
+ suspended INTEGER NOT NULL DEFAULT 0,
7
+ created_at INTEGER NOT NULL,
8
+ CHECK(owner_id LIKE 'google:%')
9
+ );
10
+ CREATE INDEX workspace_owner ON workspaces(owner_id);
11
+ CREATE TABLE project_owners (
12
+ project TEXT PRIMARY KEY,
13
+ user_id TEXT NOT NULL REFERENCES users(id),
14
+ CHECK(user_id LIKE 'google:%')
15
+ );
16
+ CREATE TABLE project_quotas (
17
+ project TEXT PRIMARY KEY,
18
+ max_comments INTEGER NOT NULL,
19
+ max_bytes INTEGER NOT NULL,
20
+ comments INTEGER NOT NULL DEFAULT 0,
21
+ bytes INTEGER NOT NULL DEFAULT 0
22
+ );
23
+ CREATE TRIGGER comment_quota_insert AFTER INSERT ON comments BEGIN
24
+ UPDATE project_quotas SET comments=comments+1, bytes=bytes+length(CAST(NEW.body AS BLOB))+256
25
+ WHERE project=(SELECT project FROM threads WHERE id=NEW.thread_id);
26
+ SELECT RAISE(ABORT, 'komo_quota_exceeded') WHERE EXISTS(SELECT 1 FROM project_quotas WHERE project=(SELECT project FROM threads WHERE id=NEW.thread_id) AND (comments>max_comments OR bytes>max_bytes));
27
+ END;
28
+ CREATE TRIGGER comment_quota_update AFTER UPDATE OF body ON comments BEGIN
29
+ UPDATE project_quotas SET bytes=bytes+length(CAST(NEW.body AS BLOB))-length(CAST(OLD.body AS BLOB)) WHERE project=(SELECT project FROM threads WHERE id=NEW.thread_id);
30
+ SELECT RAISE(ABORT, 'komo_quota_exceeded') WHERE EXISTS(SELECT 1 FROM project_quotas WHERE project=(SELECT project FROM threads WHERE id=NEW.thread_id) AND bytes>max_bytes);
31
+ END;
32
+ CREATE TRIGGER thread_quota_insert AFTER INSERT ON threads BEGIN
33
+ UPDATE project_quotas SET bytes=bytes+length(CAST(NEW.anchor AS BLOB))+length(CAST(NEW.page AS BLOB))+length(CAST(NEW.branch AS BLOB))+512 WHERE project=NEW.project;
34
+ SELECT RAISE(ABORT, 'komo_quota_exceeded') WHERE EXISTS(SELECT 1 FROM project_quotas WHERE project=NEW.project AND bytes>max_bytes);
35
+ END;
36
+ CREATE TRIGGER thread_quota_update AFTER UPDATE OF anchor ON threads BEGIN
37
+ UPDATE project_quotas SET bytes=bytes+length(CAST(NEW.anchor AS BLOB))-length(CAST(OLD.anchor AS BLOB)) WHERE project=NEW.project;
38
+ SELECT RAISE(ABORT, 'komo_quota_exceeded') WHERE EXISTS(SELECT 1 FROM project_quotas WHERE project=NEW.project AND bytes>max_bytes);
39
+ END;
40
+ CREATE TABLE setup_requests (
41
+ id TEXT PRIMARY KEY,
42
+ poll_hash TEXT NOT NULL,
43
+ config TEXT NOT NULL,
44
+ project TEXT,
45
+ expires_at INTEGER NOT NULL
46
+ );
@@ -0,0 +1,6 @@
1
+ CREATE TABLE workspace_domains (
2
+ project TEXT NOT NULL REFERENCES workspaces(id),
3
+ origin TEXT NOT NULL,
4
+ verified_at INTEGER NOT NULL,
5
+ PRIMARY KEY(project,origin)
6
+ );
@@ -0,0 +1,16 @@
1
+ CREATE TABLE project_members (
2
+ project TEXT NOT NULL,
3
+ user_id TEXT NOT NULL REFERENCES users(id),
4
+ PRIMARY KEY(project,user_id)
5
+ );
6
+ CREATE TRIGGER member_quota_insert AFTER INSERT ON project_members BEGIN
7
+ UPDATE project_quotas SET bytes=bytes+13312 WHERE project=NEW.project;
8
+ SELECT RAISE(ABORT, 'komo_quota_exceeded') WHERE EXISTS(SELECT 1 FROM project_quotas WHERE project=NEW.project AND bytes>max_bytes);
9
+ END;
10
+ CREATE TRIGGER reaction_quota_insert AFTER INSERT ON reactions BEGIN
11
+ UPDATE project_quotas SET bytes=bytes+256 WHERE project=(SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=NEW.comment_id);
12
+ SELECT RAISE(ABORT, 'komo_quota_exceeded') WHERE EXISTS(SELECT 1 FROM project_quotas WHERE project=(SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=NEW.comment_id) AND bytes>max_bytes);
13
+ END;
14
+ CREATE TRIGGER reaction_quota_delete AFTER DELETE ON reactions BEGIN
15
+ UPDATE project_quotas SET bytes=MAX(0,bytes-256) WHERE project=(SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=OLD.comment_id);
16
+ END;
@@ -0,0 +1,17 @@
1
+ CREATE TABLE scope_revisions (
2
+ project TEXT NOT NULL,
3
+ repo TEXT NOT NULL,
4
+ branch TEXT NOT NULL,
5
+ version INTEGER NOT NULL DEFAULT 0,
6
+ PRIMARY KEY(project,repo,branch)
7
+ );
8
+ INSERT INTO scope_revisions(project,repo,branch,version) SELECT DISTINCT project,repo,branch,1 FROM threads;
9
+ CREATE TRIGGER thread_revision_insert AFTER INSERT ON threads BEGIN
10
+ INSERT INTO scope_revisions(project,repo,branch,version) VALUES(NEW.project,NEW.repo,NEW.branch,1) ON CONFLICT(project,repo,branch) DO UPDATE SET version=version+1;
11
+ END;
12
+ CREATE TRIGGER thread_revision_update AFTER UPDATE ON threads BEGIN
13
+ INSERT INTO scope_revisions(project,repo,branch,version) VALUES(NEW.project,NEW.repo,NEW.branch,1) ON CONFLICT(project,repo,branch) DO UPDATE SET version=version+1;
14
+ END;
15
+ CREATE TRIGGER profile_revision_update AFTER UPDATE ON users BEGIN
16
+ UPDATE scope_revisions SET version=version+1 WHERE (project,repo,branch) IN (SELECT t.project,t.repo,t.branch FROM threads t JOIN comments c ON c.thread_id=t.id WHERE c.user_id=NEW.id);
17
+ END;
@@ -0,0 +1 @@
1
+ UPDATE project_quotas SET max_comments=250 WHERE max_comments=1000 AND project IN (SELECT id FROM workspaces);
@@ -0,0 +1,19 @@
1
+ -- Delete children while their scope and quota owner are still available.
2
+ CREATE TRIGGER thread_delete_comments BEFORE DELETE ON threads BEGIN
3
+ DELETE FROM comments WHERE thread_id=OLD.id;
4
+ END;
5
+ CREATE TRIGGER comment_retention_delete AFTER DELETE ON comments BEGIN
6
+ UPDATE project_quotas SET comments=MAX(0,comments-1),
7
+ bytes=MAX(0,bytes-length(CAST(OLD.body AS BLOB))-256)
8
+ WHERE project=(SELECT project FROM threads WHERE id=OLD.thread_id);
9
+ UPDATE threads SET updated_at=MAX(updated_at+1,CAST(unixepoch('subsec')*1000 AS INTEGER))
10
+ WHERE id=OLD.thread_id;
11
+ END;
12
+ CREATE TRIGGER thread_retention_delete AFTER DELETE ON threads BEGIN
13
+ UPDATE project_quotas SET bytes=MAX(0,bytes-length(CAST(OLD.anchor AS BLOB))
14
+ -length(CAST(OLD.page AS BLOB))-length(CAST(OLD.branch AS BLOB))-512)
15
+ WHERE project=OLD.project;
16
+ INSERT INTO scope_revisions(project,repo,branch,version)
17
+ VALUES(OLD.project,OLD.repo,OLD.branch,1)
18
+ ON CONFLICT(project,repo,branch) DO UPDATE SET version=version+1;
19
+ END;
@@ -0,0 +1 @@
1
+ CREATE INDEX comments_reviewer_history ON comments(user_id,created_at DESC);
@@ -0,0 +1,52 @@
1
+ ALTER TABLE users ADD COLUMN email TEXT;
2
+ CREATE TABLE project_settings (
3
+ project TEXT PRIMARY KEY,
4
+ access TEXT NOT NULL DEFAULT 'public' CHECK(access IN ('public','private'))
5
+ );
6
+ CREATE TABLE project_access (
7
+ project TEXT NOT NULL,
8
+ user_id TEXT NOT NULL REFERENCES users(id),
9
+ PRIMARY KEY(project,user_id)
10
+ );
11
+ CREATE TABLE project_invites (
12
+ token_hash TEXT PRIMARY KEY,
13
+ project TEXT NOT NULL,
14
+ email TEXT NOT NULL,
15
+ expires_at INTEGER NOT NULL
16
+ );
17
+ CREATE INDEX invite_project ON project_invites(project,expires_at);
18
+ CREATE TRIGGER member_quota_delete AFTER DELETE ON project_members BEGIN
19
+ UPDATE project_quotas SET bytes=MAX(0,bytes-13312) WHERE project=OLD.project;
20
+ END;
21
+
22
+ CREATE TABLE export_revisions (project TEXT PRIMARY KEY, version INTEGER NOT NULL DEFAULT 0);
23
+ CREATE TRIGGER export_threads_insert AFTER INSERT ON threads BEGIN
24
+ INSERT INTO export_revisions(project,version) SELECT NEW.project,1 WHERE NEW.project IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
25
+ END;
26
+ CREATE TRIGGER export_threads_update AFTER UPDATE ON threads BEGIN
27
+ INSERT INTO export_revisions(project,version) SELECT NEW.project,1 WHERE NEW.project IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
28
+ END;
29
+ CREATE TRIGGER export_threads_delete AFTER DELETE ON threads BEGIN
30
+ INSERT INTO export_revisions(project,version) SELECT OLD.project,1 WHERE OLD.project IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
31
+ END;
32
+ CREATE TRIGGER export_comments_insert AFTER INSERT ON comments BEGIN
33
+ INSERT INTO export_revisions(project,version) SELECT (SELECT project FROM threads WHERE id=NEW.thread_id),1 WHERE (SELECT project FROM threads WHERE id=NEW.thread_id) IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
34
+ END;
35
+ CREATE TRIGGER export_comments_update AFTER UPDATE ON comments BEGIN
36
+ INSERT INTO export_revisions(project,version) SELECT (SELECT project FROM threads WHERE id=NEW.thread_id),1 WHERE (SELECT project FROM threads WHERE id=NEW.thread_id) IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
37
+ END;
38
+ CREATE TRIGGER export_comments_delete AFTER DELETE ON comments BEGIN
39
+ INSERT INTO export_revisions(project,version) SELECT (SELECT project FROM threads WHERE id=OLD.thread_id),1 WHERE (SELECT project FROM threads WHERE id=OLD.thread_id) IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
40
+ END;
41
+ CREATE TRIGGER export_reactions_insert AFTER INSERT ON reactions BEGIN
42
+ INSERT INTO export_revisions(project,version) SELECT (SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=NEW.comment_id),1 WHERE (SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=NEW.comment_id) IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
43
+ END;
44
+ CREATE TRIGGER export_reactions_update AFTER UPDATE ON reactions BEGIN
45
+ INSERT INTO export_revisions(project,version) SELECT (SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=NEW.comment_id),1 WHERE (SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=NEW.comment_id) IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
46
+ END;
47
+ CREATE TRIGGER export_reactions_delete AFTER DELETE ON reactions BEGIN
48
+ INSERT INTO export_revisions(project,version) SELECT (SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=OLD.comment_id),1 WHERE (SELECT t.project FROM comments c JOIN threads t ON t.id=c.thread_id WHERE c.id=OLD.comment_id) IS NOT NULL ON CONFLICT(project) DO UPDATE SET version=version+1;
49
+ END;
50
+ CREATE TRIGGER export_profile_update AFTER UPDATE ON users BEGIN
51
+ UPDATE export_revisions SET version=version+1 WHERE project IN (SELECT project FROM project_members WHERE user_id=NEW.id);
52
+ END;
@@ -0,0 +1,3 @@
1
+ -- Bind the owner's explicit site approvals to this one-use OAuth handoff.
2
+ -- Existing reviewer and hosted-management sign-ins leave this column null.
3
+ ALTER TABLE oauth_states ADD COLUMN approved_origins TEXT;
@@ -0,0 +1,8 @@
1
+ -- Owner-approved sites for projects configured in PROJECTS. Hosted
2
+ -- workspaces keep theirs in workspace_domains, which references workspaces.
3
+ CREATE TABLE project_sites (
4
+ project TEXT NOT NULL,
5
+ origin TEXT NOT NULL,
6
+ added_at INTEGER NOT NULL,
7
+ PRIMARY KEY(project,origin)
8
+ );
@@ -0,0 +1,3 @@
1
+ -- Let owners remove sites that come from PROJECTS config. A removed row hides
2
+ -- that config origin; other rows add sites on top of the config.
3
+ ALTER TABLE project_sites ADD COLUMN removed INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,6 @@
1
+ -- Match the list ordering; retain thread_scope for updated-at consumers.
2
+ CREATE INDEX thread_created_scope ON threads(project,repo,branch,created_at,id);
3
+ CREATE INDEX rate_limit_expiry ON rate_limits(expires_at);
4
+ CREATE INDEX oauth_state_expiry ON oauth_states(expires_at);
5
+ CREATE INDEX invite_expiry ON project_invites(expires_at);
6
+ CREATE INDEX setup_request_expiry ON setup_requests(expires_at);
@@ -0,0 +1 @@
1
+ ALTER TABLE threads ADD COLUMN tags TEXT NOT NULL DEFAULT '[]';
@@ -0,0 +1,4 @@
1
+ ALTER TABLE project_settings ADD COLUMN webhook_url TEXT;
2
+ ALTER TABLE project_settings ADD COLUMN webhook_secret TEXT;
3
+ ALTER TABLE project_settings ADD COLUMN webhook_events TEXT;
4
+ ALTER TABLE project_settings ADD COLUMN webhook_enabled INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,126 @@
1
+ import { request as httpsRequest } from "node:https";
2
+ import { lookup } from "node:dns/promises";
3
+ import { isIP, type LookupFunction } from "node:net";
4
+ import { validateWebhookUrl } from "./webhooks";
5
+
6
+ const MAX_IN_FLIGHT = 8;
7
+ const MAX_BODY_BYTES = 16 * 1024;
8
+ let inFlight = 0;
9
+
10
+ export function isPublicAddress(address: string) {
11
+ const version = isIP(address);
12
+ if (version === 4) {
13
+ const octets = address.split(".").map(Number);
14
+ const [a, b, c] = octets;
15
+ if (octets.length !== 4 || octets.some((value) => value < 0 || value > 255))
16
+ return false;
17
+ return !(
18
+ a === 0 ||
19
+ a === 10 ||
20
+ a === 127 ||
21
+ (a === 100 && b >= 64 && b <= 127) ||
22
+ (a === 169 && b === 254) ||
23
+ (a === 172 && b >= 16 && b <= 31) ||
24
+ (a === 192 && b === 0 && c === 0) ||
25
+ (a === 192 && b === 0 && c === 2) ||
26
+ (a === 192 && b === 88 && c === 99) ||
27
+ (a === 192 && b === 168) ||
28
+ (a === 198 && (b === 18 || b === 19)) ||
29
+ (a === 198 && b === 51 && c === 100) ||
30
+ (a === 203 && b === 0 && c === 113) ||
31
+ a >= 224
32
+ );
33
+ }
34
+ if (version !== 6 || address.includes("%")) return false;
35
+ const normalized = address.toLowerCase();
36
+ // Permit only global-unicast 2000::/3; reject documentation, transition,
37
+ // protocol-assignment, and IPv4-mapped ranges within it as well.
38
+ if (!/^[23]/.test(normalized)) return false;
39
+ const secondGroup = Number.parseInt(normalized.split(":")[1] || "0", 16);
40
+ return !(
41
+ normalized.startsWith("2001:db8:") ||
42
+ (normalized.startsWith("2001:") && secondGroup <= 0x1ff) ||
43
+ normalized.startsWith("2002:") ||
44
+ normalized.startsWith("3fff:") ||
45
+ normalized.startsWith("::ffff:")
46
+ );
47
+ }
48
+
49
+ export function assertPublicDnsRecords(records: { address: string; family: number }[]) {
50
+ if (!records.length || records.some((record) => !isPublicAddress(record.address)))
51
+ throw new Error("Webhook host did not resolve exclusively to public addresses.");
52
+ }
53
+
54
+ export function pinnedLookup(address: { address: string; family: number }): LookupFunction {
55
+ return (_hostname, options, callback) => {
56
+ if (options.all) callback(null, [{ address: address.address, family: address.family }]);
57
+ else callback(null, address.address, address.family);
58
+ };
59
+ }
60
+
61
+ export async function sendWebhookPinned(
62
+ urlValue: string,
63
+ body: string,
64
+ headers: Record<string, string>
65
+ ) {
66
+ if (inFlight >= MAX_IN_FLIGHT) throw new Error("Webhook capacity reached.");
67
+ if (Buffer.byteLength(body, "utf8") > MAX_BODY_BYTES)
68
+ throw new Error("Webhook payload exceeds the delivery limit.");
69
+ const url = new URL(validateWebhookUrl(urlValue));
70
+ inFlight++;
71
+ try {
72
+ let dnsTimer: NodeJS.Timeout | undefined;
73
+ const records = await Promise.race([
74
+ lookup(url.hostname, { all: true, verbatim: true }),
75
+ new Promise<never>((_, reject) => {
76
+ dnsTimer = setTimeout(() => reject(new Error("Webhook DNS lookup timed out.")), 2_000);
77
+ }),
78
+ ]).finally(() => clearTimeout(dnsTimer));
79
+ assertPublicDnsRecords(records);
80
+ const address = records[0];
81
+ await new Promise<void>((resolve, reject) => {
82
+ let settled = false;
83
+ let totalTimer: NodeJS.Timeout;
84
+ const finish = (error?: Error) => {
85
+ if (settled) return;
86
+ settled = true;
87
+ clearTimeout(totalTimer);
88
+ if (error) reject(error);
89
+ else resolve();
90
+ };
91
+ const req = httpsRequest(
92
+ url,
93
+ {
94
+ method: "POST",
95
+ headers: {
96
+ ...headers,
97
+ "Content-Type": "application/json",
98
+ "Content-Length": Buffer.byteLength(body, "utf8"),
99
+ },
100
+ timeout: 2_000,
101
+ lookup: pinnedLookup(address),
102
+ },
103
+ (response) => {
104
+ // Read and discard a bounded amount. Never follow redirects or log bodies.
105
+ let read = 0;
106
+ response.on("data", (chunk: Buffer) => {
107
+ read += chunk.length;
108
+ if (read > 1024) response.destroy();
109
+ });
110
+ response.on("end", () => finish());
111
+ response.on("close", () => finish());
112
+ response.on("error", (error) => finish(error));
113
+ }
114
+ );
115
+ totalTimer = setTimeout(
116
+ () => req.destroy(new Error("Webhook timed out.")),
117
+ 2_000
118
+ );
119
+ req.on("timeout", () => req.destroy(new Error("Webhook timed out.")));
120
+ req.on("error", (error) => finish(error));
121
+ req.end(body);
122
+ });
123
+ } finally {
124
+ inFlight--;
125
+ }
126
+ }