gitspace 0.2.0-rc.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 (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,512 @@
1
+ /**
2
+ * Unit tests for access control list management
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from "bun:test";
6
+ import {
7
+ AccessControlList,
8
+ DEFAULT_ACCESS_TYPE,
9
+ isAccessExpired,
10
+ } from "./access-control.js";
11
+ import { generateIdentity, getPublicIdentity, sign } from "./identity.js";
12
+ import type { AccessEntry, PublicIdentity } from "../../../types/identity.js";
13
+
14
+ describe("AccessControlList", () => {
15
+ let acl: AccessControlList;
16
+ let identity1: ReturnType<typeof generateIdentity>;
17
+ let identity2: ReturnType<typeof generateIdentity>;
18
+ let publicIdentity1: PublicIdentity;
19
+ let publicIdentity2: PublicIdentity;
20
+
21
+ beforeEach(() => {
22
+ acl = new AccessControlList();
23
+ identity1 = generateIdentity("User 1");
24
+ identity2 = generateIdentity("User 2");
25
+ publicIdentity1 = getPublicIdentity(identity1);
26
+ publicIdentity2 = getPublicIdentity(identity2);
27
+ });
28
+
29
+ describe("addEntry", () => {
30
+ it("should add a new entry with default access type", () => {
31
+ const entry = acl.addEntry(publicIdentity1);
32
+
33
+ expect(entry.identityId).toBe(publicIdentity1.id);
34
+ expect(entry.signingPublicKey).toBe(publicIdentity1.signingPublicKey);
35
+ expect(entry.keyExchangePublicKey).toBe(
36
+ publicIdentity1.keyExchangePublicKey
37
+ );
38
+ expect(entry.label).toBe(publicIdentity1.label);
39
+ expect(entry.accessType).toBe(DEFAULT_ACCESS_TYPE);
40
+ expect(entry.grantedAt).toBeGreaterThan(0);
41
+ expect(entry.expiresAt).toBeUndefined();
42
+ });
43
+
44
+ it("should add a new entry with custom access type", () => {
45
+ const entry = acl.addEntry(publicIdentity1, 'session-invite');
46
+
47
+ expect(entry.accessType).toBe('session-invite');
48
+ });
49
+
50
+ it("should add a new entry with session ID for session-invite", () => {
51
+ const entry = acl.addEntry(publicIdentity1, 'session-invite', 'session-123');
52
+
53
+ expect(entry.accessType).toBe('session-invite');
54
+ expect(entry.sessionId).toBe('session-123');
55
+ });
56
+
57
+ it("should replace existing entry", () => {
58
+ const entry1 = acl.addEntry(publicIdentity1, 'full');
59
+ const entry2 = acl.addEntry(publicIdentity1, 'session-invite');
60
+
61
+ expect(acl.size).toBe(1);
62
+ expect(entry2.accessType).toBe('session-invite');
63
+ expect(entry2.grantedAt).toBeGreaterThanOrEqual(entry1.grantedAt);
64
+ });
65
+
66
+ it("should handle identity without label", () => {
67
+ const identityNoLabel = generateIdentity();
68
+ const publicIdentityNoLabel = getPublicIdentity(identityNoLabel);
69
+ const entry = acl.addEntry(publicIdentityNoLabel);
70
+
71
+ expect(entry.label).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe("removeEntry", () => {
76
+ it("should remove an existing entry", () => {
77
+ acl.addEntry(publicIdentity1);
78
+ const removed = acl.removeEntry(publicIdentity1.id);
79
+
80
+ expect(removed).toBe(true);
81
+ expect(acl.size).toBe(0);
82
+ });
83
+
84
+ it("should return false when removing non-existent entry", () => {
85
+ const removed = acl.removeEntry("non-existent-id");
86
+
87
+ expect(removed).toBe(false);
88
+ });
89
+
90
+ it("should not affect other entries", () => {
91
+ acl.addEntry(publicIdentity1);
92
+ acl.addEntry(publicIdentity2);
93
+
94
+ acl.removeEntry(publicIdentity1.id);
95
+
96
+ expect(acl.size).toBe(1);
97
+ expect(acl.hasAccess(publicIdentity2.id)).toBe(true);
98
+ });
99
+ });
100
+
101
+ describe("hasAccess", () => {
102
+ it("should return true for existing entry", () => {
103
+ acl.addEntry(publicIdentity1);
104
+
105
+ expect(acl.hasAccess(publicIdentity1.id)).toBe(true);
106
+ });
107
+
108
+ it("should return false for non-existent entry", () => {
109
+ expect(acl.hasAccess("non-existent-id")).toBe(false);
110
+ });
111
+
112
+ it("should return false for expired entry", () => {
113
+ const entry = acl.addEntry(publicIdentity1);
114
+ // Manually set expiry in the past
115
+ entry.expiresAt = Date.now() - 1000;
116
+ acl.import([entry]);
117
+
118
+ expect(acl.hasAccess(publicIdentity1.id)).toBe(false);
119
+ });
120
+
121
+ it("should return true for entry with future expiry", () => {
122
+ const entry = acl.addEntry(publicIdentity1);
123
+ entry.expiresAt = Date.now() + 10000;
124
+ acl.import([entry]);
125
+
126
+ expect(acl.hasAccess(publicIdentity1.id)).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe("hasFullAccess", () => {
131
+ it("should return true for entry with full access", () => {
132
+ acl.addEntry(publicIdentity1, 'full');
133
+
134
+ expect(acl.hasFullAccess(publicIdentity1.id)).toBe(true);
135
+ });
136
+
137
+ it("should return false for session-invite entry", () => {
138
+ acl.addEntry(publicIdentity1, 'session-invite');
139
+
140
+ expect(acl.hasFullAccess(publicIdentity1.id)).toBe(false);
141
+ });
142
+
143
+ it("should return false for non-existent entry", () => {
144
+ expect(acl.hasFullAccess("non-existent-id")).toBe(false);
145
+ });
146
+
147
+ it("should return false for expired entry", () => {
148
+ const entry = acl.addEntry(publicIdentity1, 'full');
149
+ entry.expiresAt = Date.now() - 1000;
150
+ acl.import([entry]);
151
+
152
+ expect(acl.hasFullAccess(publicIdentity1.id)).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe("hasSessionAccess", () => {
157
+ it("should return true for matching session", () => {
158
+ acl.addEntry(publicIdentity1, 'session-invite', 'session-123');
159
+
160
+ expect(acl.hasSessionAccess(publicIdentity1.id, 'session-123')).toBe(true);
161
+ });
162
+
163
+ it("should return false for non-matching session", () => {
164
+ acl.addEntry(publicIdentity1, 'session-invite', 'session-123');
165
+
166
+ expect(acl.hasSessionAccess(publicIdentity1.id, 'session-456')).toBe(false);
167
+ });
168
+
169
+ it("should return true for full access entry", () => {
170
+ acl.addEntry(publicIdentity1, 'full');
171
+
172
+ expect(acl.hasSessionAccess(publicIdentity1.id, 'any-session')).toBe(true);
173
+ });
174
+
175
+ it("should return false for non-existent entry", () => {
176
+ expect(acl.hasSessionAccess("non-existent-id", 'session-123')).toBe(false);
177
+ });
178
+ });
179
+
180
+ describe("getEntry", () => {
181
+ it("should return entry for existing identity", () => {
182
+ const added = acl.addEntry(publicIdentity1);
183
+ const retrieved = acl.getEntry(publicIdentity1.id);
184
+
185
+ expect(retrieved).toEqual(added);
186
+ });
187
+
188
+ it("should return undefined for non-existent identity", () => {
189
+ const entry = acl.getEntry("non-existent-id");
190
+
191
+ expect(entry).toBeUndefined();
192
+ });
193
+
194
+ it("should return undefined for expired entry", () => {
195
+ const entry = acl.addEntry(publicIdentity1);
196
+ entry.expiresAt = Date.now() - 1000;
197
+ acl.import([entry]);
198
+
199
+ expect(acl.getEntry(publicIdentity1.id)).toBeUndefined();
200
+ });
201
+ });
202
+
203
+ describe("getAllEntries", () => {
204
+ it("should return empty array when no entries", () => {
205
+ expect(acl.getAllEntries()).toEqual([]);
206
+ });
207
+
208
+ it("should return all entries", () => {
209
+ acl.addEntry(publicIdentity1);
210
+ acl.addEntry(publicIdentity2);
211
+
212
+ const entries = acl.getAllEntries();
213
+
214
+ expect(entries).toHaveLength(2);
215
+ expect(entries.map((e) => e.identityId)).toContain(publicIdentity1.id);
216
+ expect(entries.map((e) => e.identityId)).toContain(publicIdentity2.id);
217
+ });
218
+
219
+ it("should include expired entries", () => {
220
+ const entry = acl.addEntry(publicIdentity1);
221
+ entry.expiresAt = Date.now() - 1000;
222
+ acl.import([entry]);
223
+
224
+ const entries = acl.getAllEntries();
225
+
226
+ expect(entries).toHaveLength(1);
227
+ expect(entries[0].identityId).toBe(publicIdentity1.id);
228
+ });
229
+ });
230
+
231
+ describe("updateAccessType", () => {
232
+ it("should update access type for existing entry", () => {
233
+ acl.addEntry(publicIdentity1, 'full');
234
+
235
+ const updated = acl.updateAccessType(publicIdentity1.id, 'session-invite', 'session-123');
236
+
237
+ expect(updated).toBe(true);
238
+ const entry = acl.getEntry(publicIdentity1.id);
239
+ expect(entry?.accessType).toBe('session-invite');
240
+ expect(entry?.sessionId).toBe('session-123');
241
+ });
242
+
243
+ it("should return false for non-existent entry", () => {
244
+ const updated = acl.updateAccessType("non-existent-id", 'full');
245
+
246
+ expect(updated).toBe(false);
247
+ });
248
+ });
249
+
250
+ describe("updateLabel", () => {
251
+ it("should update label for existing entry", () => {
252
+ acl.addEntry(publicIdentity1);
253
+
254
+ const updated = acl.updateLabel(publicIdentity1.id, "New Label");
255
+
256
+ expect(updated).toBe(true);
257
+ const entry = acl.getEntry(publicIdentity1.id);
258
+ expect(entry?.label).toBe("New Label");
259
+ });
260
+
261
+ it("should return false for non-existent entry", () => {
262
+ const updated = acl.updateLabel("non-existent-id", "Label");
263
+
264
+ expect(updated).toBe(false);
265
+ });
266
+ });
267
+
268
+ describe("verifyAndCheckAccess", () => {
269
+ it("should return entry for valid signature with access", () => {
270
+ const entry = acl.addEntry(publicIdentity1);
271
+
272
+ const message = new TextEncoder().encode("test message");
273
+ const signature = sign(message, identity1.signing.secretKey);
274
+
275
+ const result = acl.verifyAndCheckAccess(
276
+ message,
277
+ signature,
278
+ identity1.signing.publicKey
279
+ );
280
+
281
+ expect(result).toEqual(entry);
282
+ });
283
+
284
+ it("should return null for invalid signature", () => {
285
+ acl.addEntry(publicIdentity1);
286
+
287
+ const message = new TextEncoder().encode("test message");
288
+ const wrongMessage = new TextEncoder().encode("wrong message");
289
+ const signature = sign(message, identity1.signing.secretKey);
290
+
291
+ const result = acl.verifyAndCheckAccess(
292
+ wrongMessage,
293
+ signature,
294
+ identity1.signing.publicKey
295
+ );
296
+
297
+ expect(result).toBeNull();
298
+ });
299
+
300
+ it("should return null for identity without access", () => {
301
+ const message = new TextEncoder().encode("test message");
302
+ const signature = sign(message, identity1.signing.secretKey);
303
+
304
+ const result = acl.verifyAndCheckAccess(
305
+ message,
306
+ signature,
307
+ identity1.signing.publicKey
308
+ );
309
+
310
+ expect(result).toBeNull();
311
+ });
312
+
313
+ it("should return null for expired entry", () => {
314
+ const entry = acl.addEntry(publicIdentity1);
315
+ entry.expiresAt = Date.now() - 1000;
316
+ acl.import([entry]);
317
+
318
+ const message = new TextEncoder().encode("test message");
319
+ const signature = sign(message, identity1.signing.secretKey);
320
+
321
+ const result = acl.verifyAndCheckAccess(
322
+ message,
323
+ signature,
324
+ identity1.signing.publicKey
325
+ );
326
+
327
+ expect(result).toBeNull();
328
+ });
329
+
330
+ it("should return null for signature from different identity", () => {
331
+ acl.addEntry(publicIdentity1);
332
+
333
+ const message = new TextEncoder().encode("test message");
334
+ const signature = sign(message, identity2.signing.secretKey);
335
+
336
+ const result = acl.verifyAndCheckAccess(
337
+ message,
338
+ signature,
339
+ identity1.signing.publicKey
340
+ );
341
+
342
+ expect(result).toBeNull();
343
+ });
344
+ });
345
+
346
+ describe("export", () => {
347
+ it("should export empty list", () => {
348
+ const exported = acl.export();
349
+
350
+ expect(exported).toEqual([]);
351
+ });
352
+
353
+ it("should export all entries", () => {
354
+ acl.addEntry(publicIdentity1);
355
+ acl.addEntry(publicIdentity2);
356
+
357
+ const exported = acl.export();
358
+
359
+ expect(exported).toHaveLength(2);
360
+ expect(exported.map((e) => e.identityId)).toContain(publicIdentity1.id);
361
+ expect(exported.map((e) => e.identityId)).toContain(publicIdentity2.id);
362
+ });
363
+
364
+ it("should export serializable JSON", () => {
365
+ acl.addEntry(publicIdentity1);
366
+
367
+ const exported = acl.export();
368
+ const json = JSON.stringify(exported);
369
+ const parsed = JSON.parse(json) as AccessEntry[];
370
+
371
+ expect(parsed).toHaveLength(1);
372
+ expect(parsed[0].identityId).toBe(publicIdentity1.id);
373
+ });
374
+ });
375
+
376
+ describe("import", () => {
377
+ it("should import entries", () => {
378
+ const entry1 = acl.addEntry(publicIdentity1);
379
+ const entry2 = acl.addEntry(publicIdentity2);
380
+ const exported = acl.export();
381
+
382
+ const newAcl = new AccessControlList();
383
+ newAcl.import(exported);
384
+
385
+ expect(newAcl.size).toBe(2);
386
+ expect(newAcl.getEntry(publicIdentity1.id)).toEqual(entry1);
387
+ expect(newAcl.getEntry(publicIdentity2.id)).toEqual(entry2);
388
+ });
389
+
390
+ it("should clear existing entries on import", () => {
391
+ acl.addEntry(publicIdentity1);
392
+
393
+ const entry2: AccessEntry = {
394
+ identityId: publicIdentity2.id,
395
+ signingPublicKey: publicIdentity2.signingPublicKey,
396
+ keyExchangePublicKey: publicIdentity2.keyExchangePublicKey,
397
+ label: publicIdentity2.label,
398
+ grantedAt: Date.now(),
399
+ accessType: 'full',
400
+ };
401
+
402
+ acl.import([entry2]);
403
+
404
+ expect(acl.size).toBe(1);
405
+ expect(acl.hasAccess(publicIdentity1.id)).toBe(false);
406
+ expect(acl.hasAccess(publicIdentity2.id)).toBe(true);
407
+ });
408
+
409
+ it("should handle empty import", () => {
410
+ acl.addEntry(publicIdentity1);
411
+
412
+ acl.import([]);
413
+
414
+ expect(acl.size).toBe(0);
415
+ });
416
+ });
417
+
418
+ describe("clear", () => {
419
+ it("should remove all entries", () => {
420
+ acl.addEntry(publicIdentity1);
421
+ acl.addEntry(publicIdentity2);
422
+
423
+ acl.clear();
424
+
425
+ expect(acl.size).toBe(0);
426
+ expect(acl.getAllEntries()).toEqual([]);
427
+ });
428
+
429
+ it("should handle clearing empty list", () => {
430
+ acl.clear();
431
+
432
+ expect(acl.size).toBe(0);
433
+ });
434
+ });
435
+
436
+ describe("size", () => {
437
+ it("should return 0 for empty list", () => {
438
+ expect(acl.size).toBe(0);
439
+ });
440
+
441
+ it("should return correct count", () => {
442
+ acl.addEntry(publicIdentity1);
443
+ expect(acl.size).toBe(1);
444
+
445
+ acl.addEntry(publicIdentity2);
446
+ expect(acl.size).toBe(2);
447
+
448
+ acl.removeEntry(publicIdentity1.id);
449
+ expect(acl.size).toBe(1);
450
+ });
451
+ });
452
+ });
453
+
454
+ describe("isAccessExpired", () => {
455
+ it("should return false for entry without expiry", () => {
456
+ const entry: AccessEntry = {
457
+ identityId: "test-id",
458
+ signingPublicKey: "test-key",
459
+ keyExchangePublicKey: "test-kx-key",
460
+ grantedAt: Date.now(),
461
+ accessType: 'full',
462
+ };
463
+
464
+ expect(isAccessExpired(entry)).toBe(false);
465
+ });
466
+
467
+ it("should return false for entry with future expiry", () => {
468
+ const entry: AccessEntry = {
469
+ identityId: "test-id",
470
+ signingPublicKey: "test-key",
471
+ keyExchangePublicKey: "test-kx-key",
472
+ grantedAt: Date.now(),
473
+ accessType: 'full',
474
+ expiresAt: Date.now() + 10000,
475
+ };
476
+
477
+ expect(isAccessExpired(entry)).toBe(false);
478
+ });
479
+
480
+ it("should return true for entry with past expiry", () => {
481
+ const entry: AccessEntry = {
482
+ identityId: "test-id",
483
+ signingPublicKey: "test-key",
484
+ keyExchangePublicKey: "test-kx-key",
485
+ grantedAt: Date.now() - 2000,
486
+ accessType: 'full',
487
+ expiresAt: Date.now() - 1000,
488
+ };
489
+
490
+ expect(isAccessExpired(entry)).toBe(true);
491
+ });
492
+
493
+ it("should return true for entry expiring now", () => {
494
+ const now = Date.now();
495
+ const entry: AccessEntry = {
496
+ identityId: "test-id",
497
+ signingPublicKey: "test-key",
498
+ keyExchangePublicKey: "test-kx-key",
499
+ grantedAt: now - 1000,
500
+ accessType: 'full',
501
+ expiresAt: now,
502
+ };
503
+
504
+ expect(isAccessExpired(entry)).toBe(true);
505
+ });
506
+ });
507
+
508
+ describe("DEFAULT_ACCESS_TYPE", () => {
509
+ it("should be full access", () => {
510
+ expect(DEFAULT_ACCESS_TYPE).toBe('full');
511
+ });
512
+ });