Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'feat: replace Caddy with Hono server-side access gate' (#25) from feat/server-access-gate into main

scott 5eb79d1b c4b0ce73

+1470 -118
+6 -6
Dockerfile
··· 5 5 COPY . . 6 6 RUN npm run build 7 7 8 - FROM caddy:2-alpine 8 + FROM node:22-alpine 9 9 LABEL org.opencontainers.image.title="Atmosphere Office" 10 10 LABEL org.opencontainers.image.description="Local-first office suite for the AT Protocol ecosystem" 11 11 LABEL org.opencontainers.image.url="https://tangled.org/scottlanoue.com/atmosphere-office" 12 12 LABEL org.opencontainers.image.source="https://tangled.org/scottlanoue.com/atmosphere-office" 13 13 LABEL org.opencontainers.image.licenses="AGPL-3.0" 14 - COPY Caddyfile /etc/caddy/Caddyfile 15 - COPY docker-entrypoint.sh /docker-entrypoint.sh 16 - COPY --from=builder /app/dist /srv 14 + WORKDIR /app 15 + COPY --from=builder /app/dist ./dist 16 + COPY --from=builder /app/dist-server ./dist-server 17 + COPY --from=builder /app/package.json ./ 17 18 EXPOSE 8080 18 - ENTRYPOINT ["/docker-entrypoint.sh"] 19 - CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] 19 + CMD ["node", "dist-server/index.js"]
+616 -107
package-lock.json
··· 10 10 "dependencies": { 11 11 "@atproto/api": "^0.15.12", 12 12 "@atproto/oauth-client-browser": "^0.3.41", 13 + "@hono/node-server": "^2.0.0", 13 14 "@tiptap/core": "^3.22.3", 14 15 "@tiptap/extension-code-block-lowlight": "^3.22.3", 15 16 "@tiptap/extension-collaboration": "^3.22.3", ··· 35 36 "chart.js": "^4.5.1", 36 37 "dompurify": "^3.3.3", 37 38 "exceljs": "^4.4.0", 39 + "hono": "^4.12.14", 38 40 "html2pdf.js": "^0.14.0", 39 41 "katex": "^0.16.45", 40 42 "lib0": "^0.2.99", ··· 53 55 "@playwright/test": "^1.58.2", 54 56 "@types/dompurify": "^3.0.5", 55 57 "@types/node": "^25.5.0", 58 + "esbuild": "^0.28.0", 56 59 "fake-indexeddb": "^6.2.5", 57 60 "jsdom": "^29.0.0", 58 61 "jszip": "^3.10.1", ··· 598 601 } 599 602 }, 600 603 "node_modules/@esbuild/aix-ppc64": { 601 - "version": "0.25.12", 602 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 603 - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 604 + "version": "0.28.0", 605 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", 606 + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", 604 607 "cpu": [ 605 608 "ppc64" 606 609 ], ··· 615 618 } 616 619 }, 617 620 "node_modules/@esbuild/android-arm": { 618 - "version": "0.25.12", 619 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 620 - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 621 + "version": "0.28.0", 622 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", 623 + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", 621 624 "cpu": [ 622 625 "arm" 623 626 ], ··· 632 635 } 633 636 }, 634 637 "node_modules/@esbuild/android-arm64": { 635 - "version": "0.25.12", 636 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 637 - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 638 + "version": "0.28.0", 639 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", 640 + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", 638 641 "cpu": [ 639 642 "arm64" 640 643 ], ··· 649 652 } 650 653 }, 651 654 "node_modules/@esbuild/android-x64": { 652 - "version": "0.25.12", 653 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 654 - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 655 + "version": "0.28.0", 656 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", 657 + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", 655 658 "cpu": [ 656 659 "x64" 657 660 ], ··· 666 669 } 667 670 }, 668 671 "node_modules/@esbuild/darwin-arm64": { 669 - "version": "0.25.12", 670 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 671 - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 672 + "version": "0.28.0", 673 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", 674 + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", 672 675 "cpu": [ 673 676 "arm64" 674 677 ], ··· 683 686 } 684 687 }, 685 688 "node_modules/@esbuild/darwin-x64": { 686 - "version": "0.25.12", 687 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 688 - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 689 + "version": "0.28.0", 690 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", 691 + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", 689 692 "cpu": [ 690 693 "x64" 691 694 ], ··· 700 703 } 701 704 }, 702 705 "node_modules/@esbuild/freebsd-arm64": { 703 - "version": "0.25.12", 704 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 705 - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 706 + "version": "0.28.0", 707 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", 708 + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", 706 709 "cpu": [ 707 710 "arm64" 708 711 ], ··· 717 720 } 718 721 }, 719 722 "node_modules/@esbuild/freebsd-x64": { 720 - "version": "0.25.12", 721 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 722 - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 723 + "version": "0.28.0", 724 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", 725 + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", 723 726 "cpu": [ 724 727 "x64" 725 728 ], ··· 734 737 } 735 738 }, 736 739 "node_modules/@esbuild/linux-arm": { 737 - "version": "0.25.12", 738 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 739 - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 740 + "version": "0.28.0", 741 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", 742 + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", 740 743 "cpu": [ 741 744 "arm" 742 745 ], ··· 751 754 } 752 755 }, 753 756 "node_modules/@esbuild/linux-arm64": { 754 - "version": "0.25.12", 755 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 756 - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 757 + "version": "0.28.0", 758 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", 759 + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", 757 760 "cpu": [ 758 761 "arm64" 759 762 ], ··· 768 771 } 769 772 }, 770 773 "node_modules/@esbuild/linux-ia32": { 771 - "version": "0.25.12", 772 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 773 - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 774 + "version": "0.28.0", 775 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", 776 + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", 774 777 "cpu": [ 775 778 "ia32" 776 779 ], ··· 785 788 } 786 789 }, 787 790 "node_modules/@esbuild/linux-loong64": { 788 - "version": "0.25.12", 789 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 790 - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 791 + "version": "0.28.0", 792 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", 793 + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", 791 794 "cpu": [ 792 795 "loong64" 793 796 ], ··· 802 805 } 803 806 }, 804 807 "node_modules/@esbuild/linux-mips64el": { 805 - "version": "0.25.12", 806 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 807 - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 808 + "version": "0.28.0", 809 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", 810 + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", 808 811 "cpu": [ 809 812 "mips64el" 810 813 ], ··· 819 822 } 820 823 }, 821 824 "node_modules/@esbuild/linux-ppc64": { 822 - "version": "0.25.12", 823 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 824 - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 825 + "version": "0.28.0", 826 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", 827 + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", 825 828 "cpu": [ 826 829 "ppc64" 827 830 ], ··· 836 839 } 837 840 }, 838 841 "node_modules/@esbuild/linux-riscv64": { 839 - "version": "0.25.12", 840 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 841 - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 842 + "version": "0.28.0", 843 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", 844 + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", 842 845 "cpu": [ 843 846 "riscv64" 844 847 ], ··· 853 856 } 854 857 }, 855 858 "node_modules/@esbuild/linux-s390x": { 856 - "version": "0.25.12", 857 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 858 - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 859 + "version": "0.28.0", 860 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", 861 + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", 859 862 "cpu": [ 860 863 "s390x" 861 864 ], ··· 870 873 } 871 874 }, 872 875 "node_modules/@esbuild/linux-x64": { 873 - "version": "0.25.12", 874 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 875 - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 876 + "version": "0.28.0", 877 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", 878 + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", 876 879 "cpu": [ 877 880 "x64" 878 881 ], ··· 887 890 } 888 891 }, 889 892 "node_modules/@esbuild/netbsd-arm64": { 890 - "version": "0.25.12", 891 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 892 - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 893 + "version": "0.28.0", 894 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", 895 + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", 893 896 "cpu": [ 894 897 "arm64" 895 898 ], ··· 904 907 } 905 908 }, 906 909 "node_modules/@esbuild/netbsd-x64": { 907 - "version": "0.25.12", 908 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 909 - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 910 + "version": "0.28.0", 911 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", 912 + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", 910 913 "cpu": [ 911 914 "x64" 912 915 ], ··· 921 924 } 922 925 }, 923 926 "node_modules/@esbuild/openbsd-arm64": { 924 - "version": "0.25.12", 925 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 926 - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 927 + "version": "0.28.0", 928 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", 929 + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", 927 930 "cpu": [ 928 931 "arm64" 929 932 ], ··· 938 941 } 939 942 }, 940 943 "node_modules/@esbuild/openbsd-x64": { 941 - "version": "0.25.12", 942 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 943 - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 944 + "version": "0.28.0", 945 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", 946 + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", 944 947 "cpu": [ 945 948 "x64" 946 949 ], ··· 955 958 } 956 959 }, 957 960 "node_modules/@esbuild/openharmony-arm64": { 958 - "version": "0.25.12", 959 - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 960 - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 961 + "version": "0.28.0", 962 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", 963 + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", 961 964 "cpu": [ 962 965 "arm64" 963 966 ], ··· 972 975 } 973 976 }, 974 977 "node_modules/@esbuild/sunos-x64": { 975 - "version": "0.25.12", 976 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 977 - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 978 + "version": "0.28.0", 979 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", 980 + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", 978 981 "cpu": [ 979 982 "x64" 980 983 ], ··· 989 992 } 990 993 }, 991 994 "node_modules/@esbuild/win32-arm64": { 992 - "version": "0.25.12", 993 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 994 - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 995 + "version": "0.28.0", 996 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", 997 + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", 995 998 "cpu": [ 996 999 "arm64" 997 1000 ], ··· 1006 1009 } 1007 1010 }, 1008 1011 "node_modules/@esbuild/win32-ia32": { 1009 - "version": "0.25.12", 1010 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 1011 - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 1012 + "version": "0.28.0", 1013 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", 1014 + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", 1012 1015 "cpu": [ 1013 1016 "ia32" 1014 1017 ], ··· 1023 1026 } 1024 1027 }, 1025 1028 "node_modules/@esbuild/win32-x64": { 1026 - "version": "0.25.12", 1027 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 1028 - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 1029 + "version": "0.28.0", 1030 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", 1031 + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", 1029 1032 "cpu": [ 1030 1033 "x64" 1031 1034 ], ··· 1097 1100 "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", 1098 1101 "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", 1099 1102 "license": "MIT" 1103 + }, 1104 + "node_modules/@hono/node-server": { 1105 + "version": "2.0.0", 1106 + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.0.tgz", 1107 + "integrity": "sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==", 1108 + "license": "MIT", 1109 + "engines": { 1110 + "node": ">=20" 1111 + }, 1112 + "peerDependencies": { 1113 + "hono": "^4" 1114 + } 1100 1115 }, 1101 1116 "node_modules/@iconify/types": { 1102 1117 "version": "2.0.0", ··· 4049 4064 "license": "MIT" 4050 4065 }, 4051 4066 "node_modules/esbuild": { 4052 - "version": "0.25.12", 4053 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 4054 - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 4067 + "version": "0.28.0", 4068 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", 4069 + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", 4055 4070 "dev": true, 4056 4071 "hasInstallScript": true, 4057 4072 "license": "MIT", ··· 4062 4077 "node": ">=18" 4063 4078 }, 4064 4079 "optionalDependencies": { 4065 - "@esbuild/aix-ppc64": "0.25.12", 4066 - "@esbuild/android-arm": "0.25.12", 4067 - "@esbuild/android-arm64": "0.25.12", 4068 - "@esbuild/android-x64": "0.25.12", 4069 - "@esbuild/darwin-arm64": "0.25.12", 4070 - "@esbuild/darwin-x64": "0.25.12", 4071 - "@esbuild/freebsd-arm64": "0.25.12", 4072 - "@esbuild/freebsd-x64": "0.25.12", 4073 - "@esbuild/linux-arm": "0.25.12", 4074 - "@esbuild/linux-arm64": "0.25.12", 4075 - "@esbuild/linux-ia32": "0.25.12", 4076 - "@esbuild/linux-loong64": "0.25.12", 4077 - "@esbuild/linux-mips64el": "0.25.12", 4078 - "@esbuild/linux-ppc64": "0.25.12", 4079 - "@esbuild/linux-riscv64": "0.25.12", 4080 - "@esbuild/linux-s390x": "0.25.12", 4081 - "@esbuild/linux-x64": "0.25.12", 4082 - "@esbuild/netbsd-arm64": "0.25.12", 4083 - "@esbuild/netbsd-x64": "0.25.12", 4084 - "@esbuild/openbsd-arm64": "0.25.12", 4085 - "@esbuild/openbsd-x64": "0.25.12", 4086 - "@esbuild/openharmony-arm64": "0.25.12", 4087 - "@esbuild/sunos-x64": "0.25.12", 4088 - "@esbuild/win32-arm64": "0.25.12", 4089 - "@esbuild/win32-ia32": "0.25.12", 4090 - "@esbuild/win32-x64": "0.25.12" 4080 + "@esbuild/aix-ppc64": "0.28.0", 4081 + "@esbuild/android-arm": "0.28.0", 4082 + "@esbuild/android-arm64": "0.28.0", 4083 + "@esbuild/android-x64": "0.28.0", 4084 + "@esbuild/darwin-arm64": "0.28.0", 4085 + "@esbuild/darwin-x64": "0.28.0", 4086 + "@esbuild/freebsd-arm64": "0.28.0", 4087 + "@esbuild/freebsd-x64": "0.28.0", 4088 + "@esbuild/linux-arm": "0.28.0", 4089 + "@esbuild/linux-arm64": "0.28.0", 4090 + "@esbuild/linux-ia32": "0.28.0", 4091 + "@esbuild/linux-loong64": "0.28.0", 4092 + "@esbuild/linux-mips64el": "0.28.0", 4093 + "@esbuild/linux-ppc64": "0.28.0", 4094 + "@esbuild/linux-riscv64": "0.28.0", 4095 + "@esbuild/linux-s390x": "0.28.0", 4096 + "@esbuild/linux-x64": "0.28.0", 4097 + "@esbuild/netbsd-arm64": "0.28.0", 4098 + "@esbuild/netbsd-x64": "0.28.0", 4099 + "@esbuild/openbsd-arm64": "0.28.0", 4100 + "@esbuild/openbsd-x64": "0.28.0", 4101 + "@esbuild/openharmony-arm64": "0.28.0", 4102 + "@esbuild/sunos-x64": "0.28.0", 4103 + "@esbuild/win32-arm64": "0.28.0", 4104 + "@esbuild/win32-ia32": "0.28.0", 4105 + "@esbuild/win32-x64": "0.28.0" 4091 4106 } 4092 4107 }, 4093 4108 "node_modules/escape-string-regexp": { ··· 4310 4325 "peer": true, 4311 4326 "engines": { 4312 4327 "node": ">=12.0.0" 4328 + } 4329 + }, 4330 + "node_modules/hono": { 4331 + "version": "4.12.14", 4332 + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", 4333 + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", 4334 + "license": "MIT", 4335 + "peer": true, 4336 + "engines": { 4337 + "node": ">=16.9.0" 4313 4338 } 4314 4339 }, 4315 4340 "node_modules/html-encoding-sniffer": { ··· 6701 6726 "yaml": { 6702 6727 "optional": true 6703 6728 } 6729 + } 6730 + }, 6731 + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { 6732 + "version": "0.25.12", 6733 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 6734 + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 6735 + "cpu": [ 6736 + "ppc64" 6737 + ], 6738 + "dev": true, 6739 + "license": "MIT", 6740 + "optional": true, 6741 + "os": [ 6742 + "aix" 6743 + ], 6744 + "engines": { 6745 + "node": ">=18" 6746 + } 6747 + }, 6748 + "node_modules/vite/node_modules/@esbuild/android-arm": { 6749 + "version": "0.25.12", 6750 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 6751 + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 6752 + "cpu": [ 6753 + "arm" 6754 + ], 6755 + "dev": true, 6756 + "license": "MIT", 6757 + "optional": true, 6758 + "os": [ 6759 + "android" 6760 + ], 6761 + "engines": { 6762 + "node": ">=18" 6763 + } 6764 + }, 6765 + "node_modules/vite/node_modules/@esbuild/android-arm64": { 6766 + "version": "0.25.12", 6767 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 6768 + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 6769 + "cpu": [ 6770 + "arm64" 6771 + ], 6772 + "dev": true, 6773 + "license": "MIT", 6774 + "optional": true, 6775 + "os": [ 6776 + "android" 6777 + ], 6778 + "engines": { 6779 + "node": ">=18" 6780 + } 6781 + }, 6782 + "node_modules/vite/node_modules/@esbuild/android-x64": { 6783 + "version": "0.25.12", 6784 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 6785 + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 6786 + "cpu": [ 6787 + "x64" 6788 + ], 6789 + "dev": true, 6790 + "license": "MIT", 6791 + "optional": true, 6792 + "os": [ 6793 + "android" 6794 + ], 6795 + "engines": { 6796 + "node": ">=18" 6797 + } 6798 + }, 6799 + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { 6800 + "version": "0.25.12", 6801 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 6802 + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 6803 + "cpu": [ 6804 + "arm64" 6805 + ], 6806 + "dev": true, 6807 + "license": "MIT", 6808 + "optional": true, 6809 + "os": [ 6810 + "darwin" 6811 + ], 6812 + "engines": { 6813 + "node": ">=18" 6814 + } 6815 + }, 6816 + "node_modules/vite/node_modules/@esbuild/darwin-x64": { 6817 + "version": "0.25.12", 6818 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 6819 + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 6820 + "cpu": [ 6821 + "x64" 6822 + ], 6823 + "dev": true, 6824 + "license": "MIT", 6825 + "optional": true, 6826 + "os": [ 6827 + "darwin" 6828 + ], 6829 + "engines": { 6830 + "node": ">=18" 6831 + } 6832 + }, 6833 + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { 6834 + "version": "0.25.12", 6835 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 6836 + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 6837 + "cpu": [ 6838 + "arm64" 6839 + ], 6840 + "dev": true, 6841 + "license": "MIT", 6842 + "optional": true, 6843 + "os": [ 6844 + "freebsd" 6845 + ], 6846 + "engines": { 6847 + "node": ">=18" 6848 + } 6849 + }, 6850 + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { 6851 + "version": "0.25.12", 6852 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 6853 + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 6854 + "cpu": [ 6855 + "x64" 6856 + ], 6857 + "dev": true, 6858 + "license": "MIT", 6859 + "optional": true, 6860 + "os": [ 6861 + "freebsd" 6862 + ], 6863 + "engines": { 6864 + "node": ">=18" 6865 + } 6866 + }, 6867 + "node_modules/vite/node_modules/@esbuild/linux-arm": { 6868 + "version": "0.25.12", 6869 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 6870 + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 6871 + "cpu": [ 6872 + "arm" 6873 + ], 6874 + "dev": true, 6875 + "license": "MIT", 6876 + "optional": true, 6877 + "os": [ 6878 + "linux" 6879 + ], 6880 + "engines": { 6881 + "node": ">=18" 6882 + } 6883 + }, 6884 + "node_modules/vite/node_modules/@esbuild/linux-arm64": { 6885 + "version": "0.25.12", 6886 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 6887 + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 6888 + "cpu": [ 6889 + "arm64" 6890 + ], 6891 + "dev": true, 6892 + "license": "MIT", 6893 + "optional": true, 6894 + "os": [ 6895 + "linux" 6896 + ], 6897 + "engines": { 6898 + "node": ">=18" 6899 + } 6900 + }, 6901 + "node_modules/vite/node_modules/@esbuild/linux-ia32": { 6902 + "version": "0.25.12", 6903 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 6904 + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 6905 + "cpu": [ 6906 + "ia32" 6907 + ], 6908 + "dev": true, 6909 + "license": "MIT", 6910 + "optional": true, 6911 + "os": [ 6912 + "linux" 6913 + ], 6914 + "engines": { 6915 + "node": ">=18" 6916 + } 6917 + }, 6918 + "node_modules/vite/node_modules/@esbuild/linux-loong64": { 6919 + "version": "0.25.12", 6920 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 6921 + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 6922 + "cpu": [ 6923 + "loong64" 6924 + ], 6925 + "dev": true, 6926 + "license": "MIT", 6927 + "optional": true, 6928 + "os": [ 6929 + "linux" 6930 + ], 6931 + "engines": { 6932 + "node": ">=18" 6933 + } 6934 + }, 6935 + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { 6936 + "version": "0.25.12", 6937 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 6938 + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 6939 + "cpu": [ 6940 + "mips64el" 6941 + ], 6942 + "dev": true, 6943 + "license": "MIT", 6944 + "optional": true, 6945 + "os": [ 6946 + "linux" 6947 + ], 6948 + "engines": { 6949 + "node": ">=18" 6950 + } 6951 + }, 6952 + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { 6953 + "version": "0.25.12", 6954 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 6955 + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 6956 + "cpu": [ 6957 + "ppc64" 6958 + ], 6959 + "dev": true, 6960 + "license": "MIT", 6961 + "optional": true, 6962 + "os": [ 6963 + "linux" 6964 + ], 6965 + "engines": { 6966 + "node": ">=18" 6967 + } 6968 + }, 6969 + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { 6970 + "version": "0.25.12", 6971 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 6972 + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 6973 + "cpu": [ 6974 + "riscv64" 6975 + ], 6976 + "dev": true, 6977 + "license": "MIT", 6978 + "optional": true, 6979 + "os": [ 6980 + "linux" 6981 + ], 6982 + "engines": { 6983 + "node": ">=18" 6984 + } 6985 + }, 6986 + "node_modules/vite/node_modules/@esbuild/linux-s390x": { 6987 + "version": "0.25.12", 6988 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 6989 + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 6990 + "cpu": [ 6991 + "s390x" 6992 + ], 6993 + "dev": true, 6994 + "license": "MIT", 6995 + "optional": true, 6996 + "os": [ 6997 + "linux" 6998 + ], 6999 + "engines": { 7000 + "node": ">=18" 7001 + } 7002 + }, 7003 + "node_modules/vite/node_modules/@esbuild/linux-x64": { 7004 + "version": "0.25.12", 7005 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 7006 + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 7007 + "cpu": [ 7008 + "x64" 7009 + ], 7010 + "dev": true, 7011 + "license": "MIT", 7012 + "optional": true, 7013 + "os": [ 7014 + "linux" 7015 + ], 7016 + "engines": { 7017 + "node": ">=18" 7018 + } 7019 + }, 7020 + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { 7021 + "version": "0.25.12", 7022 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 7023 + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 7024 + "cpu": [ 7025 + "arm64" 7026 + ], 7027 + "dev": true, 7028 + "license": "MIT", 7029 + "optional": true, 7030 + "os": [ 7031 + "netbsd" 7032 + ], 7033 + "engines": { 7034 + "node": ">=18" 7035 + } 7036 + }, 7037 + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { 7038 + "version": "0.25.12", 7039 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 7040 + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 7041 + "cpu": [ 7042 + "x64" 7043 + ], 7044 + "dev": true, 7045 + "license": "MIT", 7046 + "optional": true, 7047 + "os": [ 7048 + "netbsd" 7049 + ], 7050 + "engines": { 7051 + "node": ">=18" 7052 + } 7053 + }, 7054 + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { 7055 + "version": "0.25.12", 7056 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 7057 + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 7058 + "cpu": [ 7059 + "arm64" 7060 + ], 7061 + "dev": true, 7062 + "license": "MIT", 7063 + "optional": true, 7064 + "os": [ 7065 + "openbsd" 7066 + ], 7067 + "engines": { 7068 + "node": ">=18" 7069 + } 7070 + }, 7071 + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { 7072 + "version": "0.25.12", 7073 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 7074 + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 7075 + "cpu": [ 7076 + "x64" 7077 + ], 7078 + "dev": true, 7079 + "license": "MIT", 7080 + "optional": true, 7081 + "os": [ 7082 + "openbsd" 7083 + ], 7084 + "engines": { 7085 + "node": ">=18" 7086 + } 7087 + }, 7088 + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { 7089 + "version": "0.25.12", 7090 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 7091 + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 7092 + "cpu": [ 7093 + "arm64" 7094 + ], 7095 + "dev": true, 7096 + "license": "MIT", 7097 + "optional": true, 7098 + "os": [ 7099 + "openharmony" 7100 + ], 7101 + "engines": { 7102 + "node": ">=18" 7103 + } 7104 + }, 7105 + "node_modules/vite/node_modules/@esbuild/sunos-x64": { 7106 + "version": "0.25.12", 7107 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 7108 + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 7109 + "cpu": [ 7110 + "x64" 7111 + ], 7112 + "dev": true, 7113 + "license": "MIT", 7114 + "optional": true, 7115 + "os": [ 7116 + "sunos" 7117 + ], 7118 + "engines": { 7119 + "node": ">=18" 7120 + } 7121 + }, 7122 + "node_modules/vite/node_modules/@esbuild/win32-arm64": { 7123 + "version": "0.25.12", 7124 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 7125 + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 7126 + "cpu": [ 7127 + "arm64" 7128 + ], 7129 + "dev": true, 7130 + "license": "MIT", 7131 + "optional": true, 7132 + "os": [ 7133 + "win32" 7134 + ], 7135 + "engines": { 7136 + "node": ">=18" 7137 + } 7138 + }, 7139 + "node_modules/vite/node_modules/@esbuild/win32-ia32": { 7140 + "version": "0.25.12", 7141 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 7142 + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 7143 + "cpu": [ 7144 + "ia32" 7145 + ], 7146 + "dev": true, 7147 + "license": "MIT", 7148 + "optional": true, 7149 + "os": [ 7150 + "win32" 7151 + ], 7152 + "engines": { 7153 + "node": ">=18" 7154 + } 7155 + }, 7156 + "node_modules/vite/node_modules/@esbuild/win32-x64": { 7157 + "version": "0.25.12", 7158 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 7159 + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 7160 + "cpu": [ 7161 + "x64" 7162 + ], 7163 + "dev": true, 7164 + "license": "MIT", 7165 + "optional": true, 7166 + "os": [ 7167 + "win32" 7168 + ], 7169 + "engines": { 7170 + "node": ">=18" 7171 + } 7172 + }, 7173 + "node_modules/vite/node_modules/esbuild": { 7174 + "version": "0.25.12", 7175 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 7176 + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 7177 + "dev": true, 7178 + "hasInstallScript": true, 7179 + "license": "MIT", 7180 + "bin": { 7181 + "esbuild": "bin/esbuild" 7182 + }, 7183 + "engines": { 7184 + "node": ">=18" 7185 + }, 7186 + "optionalDependencies": { 7187 + "@esbuild/aix-ppc64": "0.25.12", 7188 + "@esbuild/android-arm": "0.25.12", 7189 + "@esbuild/android-arm64": "0.25.12", 7190 + "@esbuild/android-x64": "0.25.12", 7191 + "@esbuild/darwin-arm64": "0.25.12", 7192 + "@esbuild/darwin-x64": "0.25.12", 7193 + "@esbuild/freebsd-arm64": "0.25.12", 7194 + "@esbuild/freebsd-x64": "0.25.12", 7195 + "@esbuild/linux-arm": "0.25.12", 7196 + "@esbuild/linux-arm64": "0.25.12", 7197 + "@esbuild/linux-ia32": "0.25.12", 7198 + "@esbuild/linux-loong64": "0.25.12", 7199 + "@esbuild/linux-mips64el": "0.25.12", 7200 + "@esbuild/linux-ppc64": "0.25.12", 7201 + "@esbuild/linux-riscv64": "0.25.12", 7202 + "@esbuild/linux-s390x": "0.25.12", 7203 + "@esbuild/linux-x64": "0.25.12", 7204 + "@esbuild/netbsd-arm64": "0.25.12", 7205 + "@esbuild/netbsd-x64": "0.25.12", 7206 + "@esbuild/openbsd-arm64": "0.25.12", 7207 + "@esbuild/openbsd-x64": "0.25.12", 7208 + "@esbuild/openharmony-arm64": "0.25.12", 7209 + "@esbuild/sunos-x64": "0.25.12", 7210 + "@esbuild/win32-arm64": "0.25.12", 7211 + "@esbuild/win32-ia32": "0.25.12", 7212 + "@esbuild/win32-x64": "0.25.12" 6704 7213 } 6705 7214 }, 6706 7215 "node_modules/vitest": {
+8 -2
package.json
··· 5 5 "type": "module", 6 6 "scripts": { 7 7 "dev": "vite", 8 - "build": "vite build", 8 + "build": "vite build && npm run build:server", 9 + "build:client": "vite build", 10 + "build:server": "npx esbuild server/index.ts --bundle --platform=node --format=esm --outdir=dist-server", 9 11 "preview": "vite preview", 12 + "start": "node dist-server/index.js", 10 13 "test": "vitest run", 11 14 "e2e": "playwright test", 12 - "typecheck": "tsc --noEmit" 15 + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.server.json" 13 16 }, 14 17 "dependencies": { 15 18 "@atproto/api": "^0.15.12", 16 19 "@atproto/oauth-client-browser": "^0.3.41", 20 + "@hono/node-server": "^2.0.0", 17 21 "@tiptap/core": "^3.22.3", 18 22 "@tiptap/extension-code-block-lowlight": "^3.22.3", 19 23 "@tiptap/extension-collaboration": "^3.22.3", ··· 39 43 "chart.js": "^4.5.1", 40 44 "dompurify": "^3.3.3", 41 45 "exceljs": "^4.4.0", 46 + "hono": "^4.12.14", 42 47 "html2pdf.js": "^0.14.0", 43 48 "katex": "^0.16.45", 44 49 "lib0": "^0.2.99", ··· 57 62 "@playwright/test": "^1.58.2", 58 63 "@types/dompurify": "^3.0.5", 59 64 "@types/node": "^25.5.0", 65 + "esbuild": "^0.28.0", 60 66 "fake-indexeddb": "^6.2.5", 61 67 "jsdom": "^29.0.0", 62 68 "jszip": "^3.10.1",
+119
server/app.ts
··· 1 + import { Hono } from 'hono'; 2 + import { logger } from 'hono/logger'; 3 + import { serveStatic } from '@hono/node-server/serve-static'; 4 + import { readFileSync, existsSync } from 'node:fs'; 5 + import { join } from 'node:path'; 6 + import type { InstanceInfo } from './config.js'; 7 + import { 8 + generateSecret, 9 + authMiddleware, 10 + handleVerify, 11 + handleLogout, 12 + } from './auth.js'; 13 + 14 + const APP_NAMES = ['docs', 'sheets', 'forms', 'slides', 'diagrams', 'calendar']; 15 + 16 + export interface ServerConfig { 17 + instanceInfo: InstanceInfo; 18 + distPath: string; 19 + cookieSecret?: Buffer; 20 + version?: string; 21 + } 22 + 23 + export function createApp(config: ServerConfig): Hono { 24 + const { instanceInfo, distPath, version = 'dev' } = config; 25 + const secret = config.cookieSecret ?? generateSecret(); 26 + const startTime = Date.now(); 27 + 28 + const appHtml: Record<string, string> = {}; 29 + for (const name of APP_NAMES) { 30 + const htmlPath = join(distPath, name, 'index.html'); 31 + if (existsSync(htmlPath)) { 32 + appHtml[name] = readFileSync(htmlPath, 'utf-8'); 33 + } 34 + } 35 + 36 + const landingPath = join(distPath, 'index.html'); 37 + const landingHtml = existsSync(landingPath) 38 + ? readFileSync(landingPath, 'utf-8') 39 + : '<html><body>Atmosphere Office</body></html>'; 40 + 41 + const app = new Hono(); 42 + 43 + app.use('*', logger()); 44 + 45 + app.use('*', async (c, next) => { 46 + const start = performance.now(); 47 + await next(); 48 + const ms = (performance.now() - start).toFixed(1); 49 + c.res.headers.set('X-Response-Time', `${ms}ms`); 50 + c.res.headers.set('X-Version', version); 51 + }); 52 + 53 + app.get('/health', (c) => 54 + c.json({ 55 + status: 'ok', 56 + version, 57 + uptime: Math.floor((Date.now() - startTime) / 1000), 58 + flavor: instanceInfo.flavor, 59 + accessMode: instanceInfo.accessControl?.mode ?? 'open', 60 + }), 61 + ); 62 + 63 + app.get('/instance-info.json', (c) => { 64 + c.header('Cache-Control', 'no-cache'); 65 + return c.json(instanceInfo); 66 + }); 67 + 68 + app.post('/api/auth/verify', async (c) => { 69 + try { 70 + const body = await c.req.json(); 71 + return handleVerify(c, body.did, secret, instanceInfo); 72 + } catch { 73 + return c.json({ error: 'Invalid request body' }, 400); 74 + } 75 + }); 76 + 77 + app.post('/api/auth/logout', (c) => handleLogout(c)); 78 + 79 + const needsAuth = 80 + instanceInfo.accessControl?.mode === 'allowlist' && 81 + instanceInfo.flavor !== 'self-hosted'; 82 + 83 + for (const name of APP_NAMES) { 84 + const html = appHtml[name]; 85 + if (!html) continue; 86 + 87 + const serve = (c: import('hono').Context) => c.html(html); 88 + 89 + if (needsAuth) { 90 + const mw = authMiddleware(secret, instanceInfo); 91 + app.get(`/${name}`, mw, serve); 92 + app.get(`/${name}/*`, mw, serve); 93 + } else { 94 + app.get(`/${name}`, serve); 95 + app.get(`/${name}/*`, serve); 96 + } 97 + } 98 + 99 + app.use( 100 + '/assets/*', 101 + async (c, next) => { 102 + c.header('Cache-Control', 'public, max-age=31536000, immutable'); 103 + await next(); 104 + }, 105 + serveStatic({ root: distPath }), 106 + ); 107 + 108 + app.use( 109 + '/*', 110 + serveStatic({ 111 + root: distPath, 112 + onNotFound: () => {}, 113 + }), 114 + ); 115 + 116 + app.get('*', (c) => c.html(landingHtml)); 117 + 118 + return app; 119 + }
+97
server/auth.ts
··· 1 + import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; 2 + import type { Context, Next } from 'hono'; 3 + import { getCookie, setCookie, deleteCookie } from 'hono/cookie'; 4 + import type { InstanceInfo } from './config.js'; 5 + 6 + const COOKIE_NAME = 'atmos-session'; 7 + const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds 8 + 9 + export function generateSecret(): Buffer { 10 + return randomBytes(32); 11 + } 12 + 13 + export function signCookie(did: string, secret: Buffer): string { 14 + const ts = Date.now().toString(); 15 + const payload = `${did}:${ts}`; 16 + const sig = createHmac('sha256', secret).update(payload).digest('hex'); 17 + return `${payload}:${sig}`; 18 + } 19 + 20 + export function verifyCookie(value: string, secret: Buffer): string | null { 21 + const lastColon = value.lastIndexOf(':'); 22 + if (lastColon === -1) return null; 23 + 24 + const sig = value.slice(lastColon + 1); 25 + const payload = value.slice(0, lastColon); 26 + 27 + const secondLastColon = payload.lastIndexOf(':'); 28 + if (secondLastColon === -1) return null; 29 + 30 + const did = payload.slice(0, secondLastColon); 31 + const ts = payload.slice(secondLastColon + 1); 32 + 33 + if (!did.startsWith('did:')) return null; 34 + 35 + const expected = createHmac('sha256', secret).update(payload).digest(); 36 + const actual = Buffer.from(sig, 'hex'); 37 + if (actual.length !== expected.length) return null; 38 + if (!timingSafeEqual(expected, actual)) return null; 39 + 40 + const age = Date.now() - parseInt(ts, 10); 41 + if (isNaN(age) || age < 0 || age > COOKIE_MAX_AGE * 1000) return null; 42 + 43 + return did; 44 + } 45 + 46 + export function authMiddleware(secret: Buffer, config: InstanceInfo) { 47 + return async (c: Context, next: Next) => { 48 + if (config.flavor === 'self-hosted') return next(); 49 + if (!config.accessControl || config.accessControl.mode === 'open') return next(); 50 + 51 + const cookie = getCookie(c, COOKIE_NAME); 52 + if (!cookie) return c.redirect('/'); 53 + 54 + const did = verifyCookie(cookie, secret); 55 + if (!did) { 56 + deleteCookie(c, COOKIE_NAME); 57 + return c.redirect('/'); 58 + } 59 + 60 + const allowed = config.accessControl.allowlist ?? []; 61 + if (!allowed.includes(did)) { 62 + deleteCookie(c, COOKIE_NAME); 63 + return c.redirect('/'); 64 + } 65 + 66 + return next(); 67 + }; 68 + } 69 + 70 + export function handleVerify(c: Context, did: string, secret: Buffer, config: InstanceInfo) { 71 + if (!did || typeof did !== 'string' || !did.startsWith('did:')) { 72 + return c.json({ error: 'Invalid DID' }, 400); 73 + } 74 + 75 + const ac = config.accessControl; 76 + if (ac && ac.mode === 'allowlist') { 77 + const allowed = ac.allowlist ?? []; 78 + if (!allowed.includes(did)) { 79 + return c.json({ error: 'Not on allowlist' }, 403); 80 + } 81 + } 82 + 83 + const signed = signCookie(did, secret); 84 + setCookie(c, COOKIE_NAME, signed, { 85 + httpOnly: true, 86 + sameSite: 'Strict', 87 + maxAge: COOKIE_MAX_AGE, 88 + path: '/', 89 + }); 90 + 91 + return c.json({ ok: true }); 92 + } 93 + 94 + export function handleLogout(c: Context) { 95 + deleteCookie(c, COOKIE_NAME, { path: '/' }); 96 + return c.json({ ok: true }); 97 + }
+67
server/config.ts
··· 1 + export type InstanceFlavor = 'public' | 'pds-operator' | 'self-hosted'; 2 + 3 + export interface InstanceFeatures { 4 + sync: boolean; 5 + sharing: boolean; 6 + ai: boolean; 7 + } 8 + 9 + export interface AccessControl { 10 + mode: 'open' | 'allowlist'; 11 + allowlist?: string[]; 12 + } 13 + 14 + export interface InstanceInfo { 15 + flavor: InstanceFlavor; 16 + operator: string | null; 17 + pds: string | null; 18 + features: InstanceFeatures; 19 + notice: string | null; 20 + accessControl: AccessControl | null; 21 + } 22 + 23 + export function buildInstanceInfo(): InstanceInfo { 24 + const flavor = validateFlavor(process.env.INSTANCE_FLAVOR); 25 + const features = parseFeatures(process.env.INSTANCE_FEATURES); 26 + const accessControl = parseAccessControl( 27 + process.env.INSTANCE_ACCESS_MODE, 28 + process.env.INSTANCE_ALLOWLIST, 29 + ); 30 + 31 + return { 32 + flavor, 33 + operator: process.env.INSTANCE_OPERATOR || null, 34 + pds: process.env.INSTANCE_PDS || null, 35 + features, 36 + notice: process.env.INSTANCE_NOTICE || null, 37 + accessControl, 38 + }; 39 + } 40 + 41 + function validateFlavor(v: string | undefined): InstanceFlavor { 42 + if (v === 'pds-operator' || v === 'self-hosted') return v; 43 + return 'public'; 44 + } 45 + 46 + function parseFeatures(v: string | undefined): InstanceFeatures { 47 + const parts = (v || '').split(',').map((s) => s.trim()); 48 + return { 49 + sync: parts.includes('sync'), 50 + sharing: parts.includes('sharing'), 51 + ai: parts.includes('ai'), 52 + }; 53 + } 54 + 55 + function parseAccessControl( 56 + mode: string | undefined, 57 + allowlist: string | undefined, 58 + ): AccessControl | null { 59 + if (mode === 'allowlist' && allowlist) { 60 + const dids = allowlist.split(',').map((s) => s.trim()).filter(Boolean); 61 + return { mode: 'allowlist', allowlist: dids }; 62 + } 63 + if (mode === 'open') { 64 + return { mode: 'open' }; 65 + } 66 + return null; 67 + }
+26
server/index.ts
··· 1 + import { serve } from '@hono/node-server'; 2 + import { createApp } from './app.js'; 3 + import { buildInstanceInfo } from './config.js'; 4 + import { readFileSync } from 'node:fs'; 5 + import { resolve } from 'node:path'; 6 + 7 + const PORT = parseInt(process.env.PORT || '8080', 10); 8 + const DIST_PATH = resolve(process.env.DIST_PATH || './dist'); 9 + 10 + let version = 'dev'; 11 + try { 12 + const pkg = JSON.parse(readFileSync(resolve('./package.json'), 'utf-8')); 13 + version = pkg.version || 'dev'; 14 + } catch {} 15 + 16 + const instanceInfo = buildInstanceInfo(); 17 + const app = createApp({ instanceInfo, distPath: DIST_PATH, version }); 18 + 19 + console.log(`Atmosphere Office v${version}`); 20 + console.log(` flavor: ${instanceInfo.flavor}`); 21 + console.log(` access: ${instanceInfo.accessControl?.mode ?? 'open'}`); 22 + console.log(` port: ${PORT}`); 23 + 24 + serve({ fetch: app.fetch, port: PORT }, (info) => { 25 + console.log(`Listening on http://localhost:${info.port}`); 26 + });
+17 -1
src/css/app.css
··· 728 728 cursor: pointer; 729 729 transition: background var(--transition-fast); 730 730 } 731 - .new-menu-item:hover { 731 + .new-menu-item:hover:not(:disabled) { 732 732 background: var(--color-hover); 733 + } 734 + 735 + .new-menu-item-disabled { 736 + opacity: 0.45; 737 + cursor: default; 738 + pointer-events: auto; 739 + } 740 + .new-menu-item-disabled .new-menu-badge { 741 + margin-left: auto; 742 + font-size: 0.7rem; 743 + padding: 1px 6px; 744 + border-radius: var(--radius-sm); 745 + background: var(--color-border); 746 + color: var(--color-text-muted); 747 + text-transform: uppercase; 748 + letter-spacing: 0.03em; 733 749 } 734 750 735 751 .new-menu-icon {
+2 -1
src/index.html
··· 39 39 <span class="new-menu-icon">&#9998;</span> 40 40 <span class="new-menu-label">Document</span> 41 41 </button> 42 - <button class="new-menu-item" data-new="sheet" role="menuitem"> 42 + <button class="new-menu-item new-menu-item-disabled" data-new="sheet" role="menuitem" disabled title="Coming soon"> 43 43 <span class="new-menu-icon">&#9638;</span> 44 44 <span class="new-menu-label">Spreadsheet</span> 45 + <span class="new-menu-badge">Soon</span> 45 46 </button> 46 47 <button class="new-menu-item" data-new="form" role="menuitem"> 47 48 <span class="new-menu-icon">&#9783;</span>
+16
src/landing-events-identity.ts
··· 135 135 }); 136 136 } 137 137 138 + async function notifyServerAuth(did: string): Promise<void> { 139 + try { 140 + await fetch('/api/auth/verify', { 141 + method: 'POST', 142 + headers: { 'Content-Type': 'application/json' }, 143 + body: JSON.stringify({ did }), 144 + }); 145 + } catch { 146 + // Server auth unavailable (dev mode) — client gate is sufficient 147 + } 148 + } 149 + 138 150 export async function initUsername(deps: EventDeps): Promise<void> { 139 151 await applyAccessGate(); 140 152 ··· 145 157 showWaitlistModal(session.handle); 146 158 return; 147 159 } 160 + await notifyServerAuth(session.did); 148 161 showUserBadge(deps, session.displayName, session.avatar); 149 162 deps.usernameModal.style.display = 'none'; 150 163 return; ··· 157 170 showWaitlistModal(restored.handle); 158 171 return; 159 172 } 173 + await notifyServerAuth(restored.did); 160 174 showUserBadge(deps, restored.displayName, restored.avatar); 161 175 deps.usernameModal.style.display = 'none'; 162 176 return; ··· 215 229 cancelLabel: 'Close', 216 230 }); 217 231 if (confirmed) { 232 + try { await fetch('/api/auth/logout', { method: 'POST' }); } catch {} 218 233 await session.signOut(); 219 234 window.location.reload(); 220 235 } ··· 227 242 showWaitlistModal(session.handle); 228 243 return; 229 244 } 245 + await notifyServerAuth(session.did); 230 246 showUserBadge(deps, session.displayName, session.avatar); 231 247 deps.usernameModal.style.display = 'none'; 232 248 } else {
+1
src/landing.ts
··· 196 196 newMenu.addEventListener('click', (e) => { 197 197 const target = (e.target as HTMLElement).closest('.new-menu-item') as HTMLElement | null; 198 198 if (!target) return; 199 + if ((target as HTMLButtonElement).disabled) return; 199 200 const kind = target.dataset.new; 200 201 if (!kind) return; 201 202 closeNewMenu();
+492
tests/server.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 + import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; 3 + import { join } from 'node:path'; 4 + import { tmpdir } from 'node:os'; 5 + import { randomBytes } from 'node:crypto'; 6 + import { createApp, type ServerConfig } from '../server/app.js'; 7 + import { signCookie, verifyCookie, generateSecret } from '../server/auth.js'; 8 + import { buildInstanceInfo, type InstanceInfo } from '../server/config.js'; 9 + 10 + function makeDist(): string { 11 + const dir = mkdtempSync(join(tmpdir(), 'atmos-test-')); 12 + writeFileSync(join(dir, 'index.html'), '<html><body>Landing</body></html>'); 13 + for (const app of ['docs', 'sheets', 'forms', 'slides', 'diagrams', 'calendar']) { 14 + mkdirSync(join(dir, app)); 15 + writeFileSync(join(dir, app, 'index.html'), `<html><body>${app}</body></html>`); 16 + } 17 + mkdirSync(join(dir, 'assets')); 18 + writeFileSync(join(dir, 'assets', 'main.js'), 'console.log("ok")'); 19 + writeFileSync(join(dir, 'favicon.svg'), '<svg/>'); 20 + return dir; 21 + } 22 + 23 + function makeConfig(overrides?: Partial<InstanceInfo>): InstanceInfo { 24 + return { 25 + flavor: 'public', 26 + operator: null, 27 + pds: null, 28 + features: { sync: false, sharing: false, ai: false }, 29 + notice: null, 30 + accessControl: null, 31 + ...overrides, 32 + }; 33 + } 34 + 35 + function makeServerConfig( 36 + instanceInfo: InstanceInfo, 37 + distPath: string, 38 + cookieSecret?: Buffer, 39 + ): ServerConfig { 40 + return { 41 + instanceInfo, 42 + distPath, 43 + cookieSecret: cookieSecret ?? generateSecret(), 44 + version: '0.1.0-test', 45 + }; 46 + } 47 + 48 + // ── Cookie signing ────────────────────────────────────────── 49 + 50 + describe('cookie signing', () => { 51 + const secret = generateSecret(); 52 + 53 + it('signs and verifies a DID cookie', () => { 54 + const signed = signCookie('did:plc:abc123', secret); 55 + const result = verifyCookie(signed, secret); 56 + expect(result).toBe('did:plc:abc123'); 57 + }); 58 + 59 + it('handles DIDs with multiple colons', () => { 60 + const signed = signCookie('did:web:example.com', secret); 61 + const result = verifyCookie(signed, secret); 62 + expect(result).toBe('did:web:example.com'); 63 + }); 64 + 65 + it('rejects tampered signature', () => { 66 + const signed = signCookie('did:plc:abc123', secret); 67 + const tampered = signed.slice(0, -4) + 'dead'; 68 + expect(verifyCookie(tampered, secret)).toBeNull(); 69 + }); 70 + 71 + it('rejects tampered DID', () => { 72 + const signed = signCookie('did:plc:abc123', secret); 73 + const tampered = signed.replace('abc123', 'xyz789'); 74 + expect(verifyCookie(tampered, secret)).toBeNull(); 75 + }); 76 + 77 + it('rejects wrong secret', () => { 78 + const signed = signCookie('did:plc:abc123', secret); 79 + const wrongSecret = generateSecret(); 80 + expect(verifyCookie(signed, wrongSecret)).toBeNull(); 81 + }); 82 + 83 + it('rejects empty string', () => { 84 + expect(verifyCookie('', secret)).toBeNull(); 85 + }); 86 + 87 + it('rejects garbage input', () => { 88 + expect(verifyCookie('not-a-cookie', secret)).toBeNull(); 89 + }); 90 + 91 + it('rejects non-DID payload', () => { 92 + const fake = `notadid:${Date.now()}:${'a'.repeat(64)}`; 93 + expect(verifyCookie(fake, secret)).toBeNull(); 94 + }); 95 + 96 + it('rejects expired cookies (> 7 days)', () => { 97 + const ts = (Date.now() - 8 * 24 * 60 * 60 * 1000).toString(); 98 + const payload = `did:plc:abc:${ts}`; 99 + const sig = require('node:crypto') 100 + .createHmac('sha256', secret) 101 + .update(payload) 102 + .digest('hex'); 103 + expect(verifyCookie(`${payload}:${sig}`, secret)).toBeNull(); 104 + }); 105 + }); 106 + 107 + // ── Config builder ────────────────────────────────────────── 108 + 109 + describe('buildInstanceInfo', () => { 110 + const originalEnv = { ...process.env }; 111 + 112 + afterEach(() => { 113 + for (const key of Object.keys(process.env)) { 114 + if (key.startsWith('INSTANCE_')) delete process.env[key]; 115 + } 116 + Object.assign(process.env, originalEnv); 117 + }); 118 + 119 + it('returns defaults when no env vars set', () => { 120 + delete process.env.INSTANCE_FLAVOR; 121 + const info = buildInstanceInfo(); 122 + expect(info.flavor).toBe('public'); 123 + expect(info.features).toEqual({ sync: false, sharing: false, ai: false }); 124 + expect(info.accessControl).toBeNull(); 125 + }); 126 + 127 + it('parses pds-operator flavor', () => { 128 + process.env.INSTANCE_FLAVOR = 'pds-operator'; 129 + process.env.INSTANCE_OPERATOR = 'Test Op'; 130 + process.env.INSTANCE_PDS = 'https://pds.example.com'; 131 + const info = buildInstanceInfo(); 132 + expect(info.flavor).toBe('pds-operator'); 133 + expect(info.operator).toBe('Test Op'); 134 + expect(info.pds).toBe('https://pds.example.com'); 135 + }); 136 + 137 + it('parses features', () => { 138 + process.env.INSTANCE_FEATURES = 'sync,ai'; 139 + const info = buildInstanceInfo(); 140 + expect(info.features).toEqual({ sync: true, sharing: false, ai: true }); 141 + }); 142 + 143 + it('parses allowlist', () => { 144 + process.env.INSTANCE_ACCESS_MODE = 'allowlist'; 145 + process.env.INSTANCE_ALLOWLIST = 'did:plc:aaa,did:plc:bbb'; 146 + const info = buildInstanceInfo(); 147 + expect(info.accessControl).toEqual({ 148 + mode: 'allowlist', 149 + allowlist: ['did:plc:aaa', 'did:plc:bbb'], 150 + }); 151 + }); 152 + 153 + it('parses open mode', () => { 154 + process.env.INSTANCE_ACCESS_MODE = 'open'; 155 + const info = buildInstanceInfo(); 156 + expect(info.accessControl).toEqual({ mode: 'open' }); 157 + }); 158 + }); 159 + 160 + // ── Health endpoint ───────────────────────────────────────── 161 + 162 + describe('GET /health', () => { 163 + let distPath: string; 164 + beforeEach(() => { distPath = makeDist(); }); 165 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 166 + 167 + it('returns status, version, uptime, and config info', async () => { 168 + const app = createApp(makeServerConfig(makeConfig(), distPath)); 169 + const res = await app.request('/health'); 170 + expect(res.status).toBe(200); 171 + const body = await res.json(); 172 + expect(body.status).toBe('ok'); 173 + expect(body.version).toBe('0.1.0-test'); 174 + expect(typeof body.uptime).toBe('number'); 175 + expect(body.flavor).toBe('public'); 176 + expect(body.accessMode).toBe('open'); 177 + }); 178 + 179 + it('reflects allowlist access mode', async () => { 180 + const info = makeConfig({ 181 + accessControl: { mode: 'allowlist', allowlist: ['did:plc:x'] }, 182 + }); 183 + const app = createApp(makeServerConfig(info, distPath)); 184 + const res = await app.request('/health'); 185 + const body = await res.json(); 186 + expect(body.accessMode).toBe('allowlist'); 187 + }); 188 + }); 189 + 190 + // ── Instance info endpoint ────────────────────────────────── 191 + 192 + describe('GET /instance-info.json', () => { 193 + let distPath: string; 194 + beforeEach(() => { distPath = makeDist(); }); 195 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 196 + 197 + it('returns the instance config', async () => { 198 + const info = makeConfig({ flavor: 'pds-operator', operator: 'Test' }); 199 + const app = createApp(makeServerConfig(info, distPath)); 200 + const res = await app.request('/instance-info.json'); 201 + expect(res.status).toBe(200); 202 + const body = await res.json(); 203 + expect(body.flavor).toBe('pds-operator'); 204 + expect(body.operator).toBe('Test'); 205 + }); 206 + 207 + it('sets no-cache header', async () => { 208 + const app = createApp(makeServerConfig(makeConfig(), distPath)); 209 + const res = await app.request('/instance-info.json'); 210 + expect(res.headers.get('cache-control')).toBe('no-cache'); 211 + }); 212 + }); 213 + 214 + // ── Auth verify endpoint ──────────────────────────────────── 215 + 216 + describe('POST /api/auth/verify', () => { 217 + let distPath: string; 218 + let secret: Buffer; 219 + beforeEach(() => { 220 + distPath = makeDist(); 221 + secret = generateSecret(); 222 + }); 223 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 224 + 225 + it('sets cookie for allowed DID', async () => { 226 + const info = makeConfig({ 227 + accessControl: { mode: 'allowlist', allowlist: ['did:plc:ok'] }, 228 + }); 229 + const app = createApp(makeServerConfig(info, distPath, secret)); 230 + const res = await app.request('/api/auth/verify', { 231 + method: 'POST', 232 + headers: { 'Content-Type': 'application/json' }, 233 + body: JSON.stringify({ did: 'did:plc:ok' }), 234 + }); 235 + expect(res.status).toBe(200); 236 + const cookie = res.headers.get('set-cookie'); 237 + expect(cookie).toContain('atmos-session='); 238 + expect(cookie).toContain('HttpOnly'); 239 + expect(cookie).toContain('SameSite=Strict'); 240 + }); 241 + 242 + it('returns 403 for DID not on allowlist', async () => { 243 + const info = makeConfig({ 244 + accessControl: { mode: 'allowlist', allowlist: ['did:plc:ok'] }, 245 + }); 246 + const app = createApp(makeServerConfig(info, distPath, secret)); 247 + const res = await app.request('/api/auth/verify', { 248 + method: 'POST', 249 + headers: { 'Content-Type': 'application/json' }, 250 + body: JSON.stringify({ did: 'did:plc:nope' }), 251 + }); 252 + expect(res.status).toBe(403); 253 + }); 254 + 255 + it('returns 400 for missing DID', async () => { 256 + const app = createApp(makeServerConfig(makeConfig(), distPath, secret)); 257 + const res = await app.request('/api/auth/verify', { 258 + method: 'POST', 259 + headers: { 'Content-Type': 'application/json' }, 260 + body: JSON.stringify({}), 261 + }); 262 + expect(res.status).toBe(400); 263 + }); 264 + 265 + it('returns 400 for non-DID string', async () => { 266 + const app = createApp(makeServerConfig(makeConfig(), distPath, secret)); 267 + const res = await app.request('/api/auth/verify', { 268 + method: 'POST', 269 + headers: { 'Content-Type': 'application/json' }, 270 + body: JSON.stringify({ did: 'not-a-did' }), 271 + }); 272 + expect(res.status).toBe(400); 273 + }); 274 + 275 + it('returns 400 for invalid JSON', async () => { 276 + const app = createApp(makeServerConfig(makeConfig(), distPath, secret)); 277 + const res = await app.request('/api/auth/verify', { 278 + method: 'POST', 279 + headers: { 'Content-Type': 'application/json' }, 280 + body: 'not json', 281 + }); 282 + expect(res.status).toBe(400); 283 + }); 284 + 285 + it('sets cookie in open mode (any DID accepted)', async () => { 286 + const info = makeConfig({ accessControl: { mode: 'open' } }); 287 + const app = createApp(makeServerConfig(info, distPath, secret)); 288 + const res = await app.request('/api/auth/verify', { 289 + method: 'POST', 290 + headers: { 'Content-Type': 'application/json' }, 291 + body: JSON.stringify({ did: 'did:plc:anyone' }), 292 + }); 293 + expect(res.status).toBe(200); 294 + expect(res.headers.get('set-cookie')).toContain('atmos-session='); 295 + }); 296 + }); 297 + 298 + // ── Auth logout endpoint ──────────────────────────────────── 299 + 300 + describe('POST /api/auth/logout', () => { 301 + let distPath: string; 302 + beforeEach(() => { distPath = makeDist(); }); 303 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 304 + 305 + it('clears the session cookie', async () => { 306 + const app = createApp(makeServerConfig(makeConfig(), distPath)); 307 + const res = await app.request('/api/auth/logout', { method: 'POST' }); 308 + expect(res.status).toBe(200); 309 + const cookie = res.headers.get('set-cookie'); 310 + expect(cookie).toContain('atmos-session='); 311 + expect(cookie).toContain('Max-Age=0'); 312 + }); 313 + }); 314 + 315 + // ── Route protection: allowlist mode ──────────────────────── 316 + 317 + describe('route protection (allowlist mode)', () => { 318 + let distPath: string; 319 + let secret: Buffer; 320 + let info: InstanceInfo; 321 + 322 + beforeEach(() => { 323 + distPath = makeDist(); 324 + secret = generateSecret(); 325 + info = makeConfig({ 326 + accessControl: { mode: 'allowlist', allowlist: ['did:plc:allowed'] }, 327 + }); 328 + }); 329 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 330 + 331 + it('serves landing page without auth', async () => { 332 + const app = createApp(makeServerConfig(info, distPath, secret)); 333 + const res = await app.request('/'); 334 + expect(res.status).toBe(200); 335 + expect(await res.text()).toContain('Landing'); 336 + }); 337 + 338 + it('redirects /docs to / without cookie', async () => { 339 + const app = createApp(makeServerConfig(info, distPath, secret)); 340 + const res = await app.request('/docs/abc123', { redirect: 'manual' }); 341 + expect(res.status).toBe(302); 342 + expect(res.headers.get('location')).toBe('/'); 343 + }); 344 + 345 + it('redirects /sheets to / without cookie', async () => { 346 + const app = createApp(makeServerConfig(info, distPath, secret)); 347 + const res = await app.request('/sheets/abc123', { redirect: 'manual' }); 348 + expect(res.status).toBe(302); 349 + expect(res.headers.get('location')).toBe('/'); 350 + }); 351 + 352 + it('redirects /forms to / without cookie', async () => { 353 + const app = createApp(makeServerConfig(info, distPath, secret)); 354 + const res = await app.request('/forms/abc123', { redirect: 'manual' }); 355 + expect(res.status).toBe(302); 356 + expect(res.headers.get('location')).toBe('/'); 357 + }); 358 + 359 + it('serves app HTML with valid cookie', async () => { 360 + const app = createApp(makeServerConfig(info, distPath, secret)); 361 + const cookie = signCookie('did:plc:allowed', secret); 362 + const res = await app.request('/docs/abc123', { 363 + headers: { Cookie: `atmos-session=${cookie}` }, 364 + }); 365 + expect(res.status).toBe(200); 366 + expect(await res.text()).toContain('docs'); 367 + }); 368 + 369 + it('serves sheets with valid cookie', async () => { 370 + const app = createApp(makeServerConfig(info, distPath, secret)); 371 + const cookie = signCookie('did:plc:allowed', secret); 372 + const res = await app.request('/sheets/abc123', { 373 + headers: { Cookie: `atmos-session=${cookie}` }, 374 + }); 375 + expect(res.status).toBe(200); 376 + expect(await res.text()).toContain('sheets'); 377 + }); 378 + 379 + it('redirects with cookie for non-allowed DID', async () => { 380 + const app = createApp(makeServerConfig(info, distPath, secret)); 381 + const cookie = signCookie('did:plc:intruder', secret); 382 + const res = await app.request('/docs/abc123', { 383 + headers: { Cookie: `atmos-session=${cookie}` }, 384 + redirect: 'manual', 385 + }); 386 + expect(res.status).toBe(302); 387 + }); 388 + 389 + it('redirects with tampered cookie', async () => { 390 + const app = createApp(makeServerConfig(info, distPath, secret)); 391 + const res = await app.request('/docs/abc123', { 392 + headers: { Cookie: 'atmos-session=garbage' }, 393 + redirect: 'manual', 394 + }); 395 + expect(res.status).toBe(302); 396 + }); 397 + 398 + it('redirects with expired cookie', async () => { 399 + const app = createApp(makeServerConfig(info, distPath, secret)); 400 + const ts = (Date.now() - 8 * 24 * 60 * 60 * 1000).toString(); 401 + const payload = `did:plc:allowed:${ts}`; 402 + const { createHmac } = require('node:crypto'); 403 + const sig = createHmac('sha256', secret).update(payload).digest('hex'); 404 + const cookie = `${payload}:${sig}`; 405 + const res = await app.request('/docs/abc123', { 406 + headers: { Cookie: `atmos-session=${cookie}` }, 407 + redirect: 'manual', 408 + }); 409 + expect(res.status).toBe(302); 410 + }); 411 + }); 412 + 413 + // ── Route protection: open mode ───────────────────────────── 414 + 415 + describe('route protection (open mode)', () => { 416 + let distPath: string; 417 + 418 + beforeEach(() => { distPath = makeDist(); }); 419 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 420 + 421 + it('serves app routes without auth', async () => { 422 + const info = makeConfig({ accessControl: { mode: 'open' } }); 423 + const app = createApp(makeServerConfig(info, distPath)); 424 + const res = await app.request('/docs/abc123'); 425 + expect(res.status).toBe(200); 426 + expect(await res.text()).toContain('docs'); 427 + }); 428 + 429 + it('serves all app types without auth', async () => { 430 + const info = makeConfig({ accessControl: { mode: 'open' } }); 431 + const app = createApp(makeServerConfig(info, distPath)); 432 + for (const name of ['docs', 'sheets', 'forms', 'diagrams', 'calendar']) { 433 + const res = await app.request(`/${name}/test`); 434 + expect(res.status).toBe(200); 435 + } 436 + }); 437 + }); 438 + 439 + // ── Route protection: self-hosted ─────────────────────────── 440 + 441 + describe('route protection (self-hosted)', () => { 442 + let distPath: string; 443 + 444 + beforeEach(() => { distPath = makeDist(); }); 445 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 446 + 447 + it('serves app routes without auth even with allowlist configured', async () => { 448 + const info = makeConfig({ 449 + flavor: 'self-hosted', 450 + accessControl: { mode: 'allowlist', allowlist: ['did:plc:only'] }, 451 + }); 452 + const app = createApp(makeServerConfig(info, distPath)); 453 + const res = await app.request('/docs/abc123'); 454 + expect(res.status).toBe(200); 455 + expect(await res.text()).toContain('docs'); 456 + }); 457 + }); 458 + 459 + // ── Response headers ──────────────────────────────────────── 460 + 461 + describe('response headers', () => { 462 + let distPath: string; 463 + beforeEach(() => { distPath = makeDist(); }); 464 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 465 + 466 + it('includes X-Response-Time on all responses', async () => { 467 + const app = createApp(makeServerConfig(makeConfig(), distPath)); 468 + const res = await app.request('/health'); 469 + expect(res.headers.get('x-response-time')).toMatch(/^\d+\.\d+ms$/); 470 + }); 471 + 472 + it('includes X-Version on all responses', async () => { 473 + const app = createApp(makeServerConfig(makeConfig(), distPath)); 474 + const res = await app.request('/health'); 475 + expect(res.headers.get('x-version')).toBe('0.1.0-test'); 476 + }); 477 + }); 478 + 479 + // ── Landing page fallback ─────────────────────────────────── 480 + 481 + describe('landing page fallback', () => { 482 + let distPath: string; 483 + beforeEach(() => { distPath = makeDist(); }); 484 + afterEach(() => { rmSync(distPath, { recursive: true, force: true }); }); 485 + 486 + it('serves landing page for unknown routes', async () => { 487 + const app = createApp(makeServerConfig(makeConfig(), distPath)); 488 + const res = await app.request('/unknown-path'); 489 + expect(res.status).toBe(200); 490 + expect(await res.text()).toContain('Landing'); 491 + }); 492 + });
+3 -1
tsconfig.server.json
··· 4 4 "module": "ES2022", 5 5 "moduleResolution": "bundler", 6 6 "lib": ["ES2022"], 7 - "outDir": "./dist-server" 7 + "types": ["node"], 8 + "outDir": "./dist-server", 9 + "noEmit": true 8 10 }, 9 11 "include": ["server/**/*.ts"] 10 12 }