[
  {
    "path": ".eslintrc.json",
    "content": "{\n\t\"extends\": [\"next/core-web-vitals\", \"next/typescript\"],\n\t\"rules\": {\n\t\t\"react/no-unescaped-entities\": \"off\",\n\t\t\"@next/next/no-img-element\": \"off\",\n\t\t\"@typescript-eslint/no-namespace\": \"off\"\n\t}\n}\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: 'lint'\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n\njobs:\n  build:\n    name: 'Lint'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Begin CI...\n        uses: actions/checkout@v2\n\n      - uses: oven-sh/setup-bun@v1\n        with:\n          bun-version: latest\n\n      - name: Install dependencies\n        run: bun install\n        env:\n          CI: true\n\n      - name: Lint\n        run: bun lint\n        env:\n          CI: true\n"
  },
  {
    "path": ".gitignore",
    "content": "/node_modules\n/.pnp\n.pnp.js\n/build\n.DS_Store\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n*.log\n.vercel\n.idea\n.eslintcache\n.next\nout\n*.tsbuildinfo\n# Yarn\n.yarn/*\n!.yarn/releases\n!.yarn/plugins\n!.yarn/sdks\n!.yarn/versions\n\n# bun\n*.bun\n\n# Cloud9 IDE files\n.c9\n"
  },
  {
    "path": ".prettierignore",
    "content": ".next\ndist\nbuild\nout\nnode_modules\n.yarn\n.git\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\t\"typescript.tsdk\": \"node_modules/typescript/lib\",\n\t\"typescript.enablePromptUseWorkspaceTsdk\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "🦄 alii/website\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nDon't use this and expect it to be totally secure, but on that note, it's just a React app.\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "next.config.mjs",
    "content": "import { config as dotenv } from 'dotenv';\n\n// @ts-check\n\n/** @type {import(\"next\").NextConfig} */\nconst config = {\n\tenv: dotenv().parsed,\n\n\timages: {\n\t\tremotePatterns: [\n\t\t\t{\n\t\t\t\tprotocol: 'https',\n\t\t\t\thostname: 'i.scdn.co',\n\t\t\t\tpathname: '/image/*',\n\t\t\t},\n\n\t\t\t{\n\t\t\t\tprotocol: 'https',\n\t\t\t\thostname: 'snapshot.apple-mapkit.com',\n\t\t\t\tpathname: '/api/v1/snapshot',\n\t\t\t},\n\t\t],\n\t},\n\n\tasync redirects() {\n\t\treturn [\n\t\t\t{\n\t\t\t\tsource: '/outro',\n\t\t\t\tdestination: 'https://www.youtube.com/watch?v=HeF11Av9WuU',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/desu',\n\t\t\t\tdestination: 'https://www.youtube.com/watch?v=HotGxCSas6A',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/10',\n\t\t\t\tdestination: 'https://youtu.be/G5HcvgepK-I',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/lulzsec',\n\t\t\t\tdestination: 'https://www.youtube.com/watch?v=DurOYPdXyF4',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/wheels',\n\t\t\t\tdestination: 'https://www.youtube.com/watch?v=9xRFN2i1cwQ',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/letterone',\n\t\t\t\tdestination: 'https://hyperfollow.com/alistair6/letter100-3',\n\t\t\t\tpermanent: true,\n\t\t\t},\n{\n\t\t\t\tsource: '/live-25-01-2024',\n\t\t\t\tdestination: 'https://www.youtube.com/watch?v=OvTy9xYH7LA',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/live-02-02-2024',\n\t\t\t\tdestination: 'https://youtube.com/watch?v=zEoTeUEElZc',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tsource: '/live-04-07-2024',\n\t\t\t\tdestination: 'https://youtube.com/watch?v=-XsKN44b7ho',\n\t\t\t\tpermanent: true,\n\t\t\t},\n\t\t];\n\t},\n};\n\nexport default config;\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"website\",\n\t\"version\": \"1.0.0\",\n\t\"repository\": \"git@github.com:alii/website.git\",\n\t\"author\": \"Alistair Smith <hi@alistair.sh>\",\n\t\"license\": \"Apache-2.0\",\n\t\"private\": true,\n\t\"packageManager\": \"bun@1.0.0\",\n\t\"scripts\": {\n\t\t\"dev\": \"next dev\",\n\t\t\"build\": \"next build\",\n\t\t\"start\": \"next start\",\n\t\t\"lint\": \"next lint\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@tailwindcss/forms\": \"^0.5.11\",\n\t\t\"@tailwindcss/postcss\": \"^4.2.2\",\n\t\t\"@types/common-tags\": \"^1.8.4\",\n\t\t\"@types/cookie\": \"^1.0.0\",\n\t\t\"@types/jsonwebtoken\": \"^9.0.10\",\n\t\t\"@types/jwa\": \"^2.0.3\",\n\t\t\"@types/react\": \"^19.2.14\",\n\t\t\"@types/react-dom\": \"^19.2.3\",\n\t\t\"@types/react-syntax-highlighter\": \"^15.5.13\",\n\t\t\"@types/uuid\": \"^10.0.0\",\n\t\t\"discord-api-types\": \"^0.38.42\",\n\t\t\"eslint\": \"9.27.0\",\n\t\t\"eslint-config-next\": \"15.1.8\",\n\t\t\"postcss\": \"^8.5.8\",\n\t\t\"prettier\": \"^3.8.1\",\n\t\t\"prettier-plugin-tailwindcss\": \"^0.6.14\",\n\t\t\"sharp\": \"^0.34.5\",\n\t\t\"tailwindcss\": \"^4.2.2\",\n\t\t\"typescript\": \"^6.0.2\"\n\t},\n\t\"dependencies\": {\n\t\t\"@altano/satori-fit-text\": \"^1.0.2\",\n\t\t\"@c-side/next\": \"^1.0.0\",\n\t\t\"@marsidev/react-turnstile\": \"^1.5.0\",\n\t\t\"@next/third-parties\": \"^15.5.14\",\n\t\t\"@otters/monzo\": \"^2.1.2\",\n\t\t\"@prequist/lanyard\": \"^1.1.0\",\n\t\t\"@tailwindcss/typography\": \"^0.5.19\",\n\t\t\"@vercel/og\": \"^0.6.8\",\n\t\t\"alistair\": \"^1.17.0\",\n\t\t\"axios\": \"^1.14.0\",\n\t\t\"bwitch\": \"^0.3.0\",\n\t\t\"clsx\": \"^2.1.1\",\n\t\t\"common-tags\": \"^1.8.2\",\n\t\t\"cookie\": \"^1.1.1\",\n\t\t\"dayjs\": \"^1.11.20\",\n\t\t\"dotenv\": \"^16.6.1\",\n\t\t\"envsafe\": \"^2.0.3\",\n\t\t\"framer-motion\": \"^12.38.0\",\n\t\t\"jsonwebtoken\": \"^9.0.3\",\n\t\t\"jwa\": \"^2.0.1\",\n\t\t\"next\": \"^16.2.1\",\n\t\t\"nextkit\": \"^3.4.3\",\n\t\t\"react\": \"^19.2.4\",\n\t\t\"react-dom\": \"^19.2.4\",\n\t\t\"react-hot-toast\": \"^2.6.0\",\n\t\t\"react-icons\": \"^5.6.0\",\n\t\t\"react-syntax-highlighter\": \"^15.6.6\",\n\t\t\"satori\": \"^0.13.2\",\n\t\t\"use-lanyard\": \"^1.7.0\",\n\t\t\"uuid\": \"^11.1.0\",\n\t\t\"zod\": \"^3.25.76\"\n\t}\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n\tplugins: {\n\t\t'@tailwindcss/postcss': {},\n\t},\n};\n"
  },
  {
    "path": "prettier.config.js",
    "content": "const alistair = require('alistair/prettier');\n\nmodule.exports = {\n\t...alistair,\n\tplugins: ['prettier-plugin-tailwindcss'],\n};\n"
  },
  {
    "path": "src/blog/2022/01/mochip/mochip.tsx",
    "content": "import {stripIndent} from 'common-tags';\nimport {Highlighter} from '../../../../components/syntax-highligher';\nimport {Post} from '../../../Post';\nimport emailFromColin from './email-from-colin.png';\nimport gmeet from './gmeet.png';\nimport goodbyeMochip from './goodbye-mochip.png';\nimport hegartyTimeExploit from './hegarty-time-exploit.jpeg';\nimport mochipLanding from './landing.jpeg';\n\nexport class Mochip extends Post {\n\tpublic name = 'Avoiding homework with code (and getting caught)';\n\tpublic slug = 'mochip';\n\tpublic date = new Date('6 Jan 2022');\n\tpublic excerpt = 'The eventful tale of me getting fed up with my homework';\n\tpublic hidden = false;\n\tpublic keywords = [\n\t\t'school',\n\t\t'homework',\n\t\t'clout',\n\t\t'hegarty maths',\n\t\t'educake',\n\t\t'homework hack',\n\t\t'maths homework',\n\t\t'programming',\n\t];\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<h1 className=\"font-serif italic\">Avoiding homework with code (and getting caught)</h1>\n\n\t\t\t\t<p>\n\t\t\t\t\tBack in 2020, my school used a few online learning platforms that allowed\n\t\t\t\t\tprofessors/teachers to assign homework to students. I, as a lazy developer, wanted to\n\t\t\t\t\tspend more time playing games and writing code, especially when everyone was spending\n\t\t\t\t\ttheir time at home because of lockdown. I started writing this post in January of 2022,\n\t\t\t\t\tbut I put off publicizing it for a while. It has been long enough since this all happened,\n\t\t\t\t\tso please sit back and enjoy.\n\t\t\t\t</p>\n\n\t\t\t\t<h2>The back story</h2>\n\t\t\t\t<p>\n\t\t\t\t\tLet's set the scene. 2018, my school introduces a new online homework platform for\n\t\t\t\t\tstudents. It's called HegartyMaths and it does a <i>lot</i>. It's fairly simple, teachers\n\t\t\t\t\tchoose a topic to set for us as homework, with that we get a 10-15 minute\n\t\t\t\t\ttutorial/informational video on the subject (of which we have to write down notes whilst\n\t\t\t\t\twatching) and a shortish quiz to complete after finishing the video. It's a lot of work,\n\t\t\t\t\tespecially the quiz, and in the worst cases can take up to an hour to complete one topic\n\t\t\t\t\t(bad).\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tMostly, software engineers are rather lazy individuals. We tell metal how to do stuff for\n\t\t\t\t\tus. Homework then, naturally, is an arduous task for a developer who is still at school.\n\t\t\t\t\tSo, still 2018, a close friend of mine by the name of{' '}\n\t\t\t\t\t<a href=\"https://hiett.dev\" target=\"_blank\" rel=\"noreferrer\">\n\t\t\t\t\t\tScott Hiett\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\tand I decided to do something about the Hegarty situation. We started to reverse engineer\n\t\t\t\t\tthe frontend app and eventually came up with a Tampermonkey userscript that would glitch\n\t\t\t\t\tthe embedded YouTube player to say that we'd watched the video at least 1x. Crucially, our\n\t\t\t\t\tteachers could see how many times we'd watched the video, so being able to skip up to 20\n\t\t\t\t\tminutes of homework time was especially useful – and it was a lot of fun to build too.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tSo we flexed it on our Snapchat stories and had our school friends message us to use it\n\t\t\t\t\tblah blah. We eventually figured out that we could also set it to be watched over 9999x\n\t\t\t\t\ttimes; every time we did that our accounts were reset by the Hegarty team.\n\t\t\t\t</p>\n\t\t\t\t<h2>The first email</h2>\n\t\t\t\t<p>\n\t\t\t\t\tAfter this, we got in contact with our Math teacher in November of 2018 and got her to\n\t\t\t\t\tsend an email to HegartyMaths informing them of our petty exploit and they got back to us\n\t\t\t\t\tvery quickly.{' '}\n\t\t\t\t\t<span className=\"line-through\">\n\t\t\t\t\t\tI don't have the original email anymore but I distinctly remember it saying something\n\t\t\t\t\t\talong the lines of \"Stop trying to hack our platform and get back to doing your\n\t\t\t\t\t\thomework.\"\n\t\t\t\t\t</span>{' '}\n\t\t\t\t\tEdit: While writing this, I was able to uncover the deleted email from a photo we had\n\t\t\t\t\ttaken of it in 2020. See below{' '}\n\t\t\t\t\t<span className=\"opacity-50\">(certain details redacted for obvious reasons)</span>:\n\t\t\t\t</p>\n\t\t\t\t<img src={hegartyTimeExploit.src} alt=\"Hegarty Time Exploit Email\" />\n\t\t\t\t<p>\n\t\t\t\t\tThis response excited us a bit, as they were now aware of us messing around with the site\n\t\t\t\t\tand they had no intention of fixing the minor vuln we had anyway, so we kept using it. We\n\t\t\t\t\thad tried to build a script to answer the questions for us, but it was too much work at\n\t\t\t\t\tthe time (complex data structures, weird API responses, etc etc).\n\t\t\t\t</p>\n\t\t\t\t<h2>Educake</h2>\n\t\t\t\t<p>\n\t\t\t\t\tFor a while, students had access to another platform called Educake. Similar to\n\t\t\t\t\tHegartyMaths but targeting Biology, Chemistry and Physics. There was no video to watch at\n\t\t\t\t\tthe beginning. We'd used it for a few years, in fact since I joined the school, but I'd\n\t\t\t\t\tnever thought about reversing until all of this began.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tOne common factor between Hegarty and Educake is that they immediately give you the\n\t\t\t\t\tcorrect answer if you got a question wrong. We took advantage of this and wrote a small\n\t\t\t\t\tnode/mongo app & tampermonkey script to detect when a user was on a quiz page, answer\n\t\t\t\t\tevery question with a random number, and then store the correct answer in mongodb. I don't\n\t\t\t\t\thave the original source but the TamperMonkey script was <i>probably something</i> like\n\t\t\t\t\tthe following:\n\t\t\t\t</p>\n\t\t\t\t<Highlighter>\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tconst guess = Math.random();\n\n\t\t\t\t\t\tconst result = await post('/api/answer', {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tanswer: guess,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tawait post('http://localhost:8080/save', {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tquestion_id: question.id,\n\t\t\t\t\t\t\t\tanswer: result.success ? guess : result.correct_answer,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Go to next question and repeat code above\n\t\t\t\t\t\tnextQuestion();\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<p>\n\t\t\t\t\tAs you can see, it was quite literally a loop through every question, saving the correct\n\t\t\t\t\tanswer as we got it and moving on. Eventually I added a few more features to fetch from\n\t\t\t\t\tthe database if we already had the right answer (meaning we don't answer{' '}\n\t\t\t\t\t<code>Math.random</code> every time) and also I added in support for multiple choice (so\n\t\t\t\t\tthat we actually pick one of the possible answers rather than making it up – however I was\n\t\t\t\t\tsurprised that the Educake backend would allow an answer that wasn't even in the possible\n\t\t\t\t\tchoices).\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tNow working on the project solo, I decided it would be time to build a nice UI for it all\n\t\t\t\t\tand bundle it all into a simple Tampermonkey script for both flexing rights on Snapchat\n\t\t\t\t\t(people constantly begging me to be able to use it was certainly ego fuel I hadn't\n\t\t\t\t\texperienced before) and also for myself to get out of homework I didn't want to do.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tThe end result? A ~200 line codebase that scooped up all questions and answers on the site\n\t\t\t\t\tthat could repeatedly get 100% on every single assignment and a 15mb mongo database.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tBelow is a small video of what it all looked like. It also demonstrates a feature I added\n\t\t\t\t\tallowing for a \"target percentage\" — meaning users could get something other than 100% to\n\t\t\t\t\tlook like more real/human score. Video was recorded on my Snapchat in November 2019.\n\t\t\t\t</p>\n\n\t\t\t\t<video controls src=\"/videos/mochip-educake.mp4\" />\n\n\t\t\t\t<h2>Hegarty 2</h2>\n\t\t\t\t<p>\n\t\t\t\t\tThe success of this script, along with pressure from my peers, led me to gain a lot of\n\t\t\t\t\tmotivation to start working on reversing Hegarty again. I reached out to an internet\n\t\t\t\t\tfriend who, for the sake of his privacy, will be named \"Jake.\" He also used HegartyMaths\n\t\t\t\t\tat his school and was in the same boat as me trying to avoid doing our homework. Together,\n\t\t\t\t\twe managed to figure out how to answer many varying types of questions, including multiple\n\t\t\t\t\tchoice and ordered answers, resulting in a huge amount of data stored. We had sacrificial\n\t\t\t\t\tuser accounts and managed to answer 60,000 questions in a couple minutes, rocketing our\n\t\t\t\t\tway to the top of the HegartyMaths global leaderboard.{' '}\n\t\t\t\t\t<i>\n\t\t\t\t\t\tWould like to give a special shoutout to Boon for lending us his login and letting us\n\t\t\t\t\t\tdecimate his statistics.\n\t\t\t\t\t</i>\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tTogether, Jake and I scraped the entirety of Hegarty's database and now had a JSON file\n\t\t\t\t\tthat could be argued to be worth as much as Hegarty the company itself due to the entire\n\t\t\t\t\tproduct quite literally being the database we had copied.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tWith this file, I wanted to take it a step further and allow my friends and other people\n\t\t\t\t\tto make good use of it without directly giving out the database (irresponsible)... And\n\t\t\t\t\there Mochip was coined.\n\t\t\t\t</p>\n\t\t\t\t<h2>Mochip</h2>\n\t\t\t\t<p>\n\t\t\t\t\tSo, where does Mochip tie in to this? Mochip was a Chrome extension, a collection of both\n\t\t\t\t\tour scraped Hegarty and scraped Educake databases sat behind a TypeScript API and a small\n\t\t\t\t\tReact app. Hosted on Heroku free tier and MongoDB Atlas free tier, users could log in,\n\t\t\t\t\tenter a question (from either site) and get back a list of answers Mochip has for that\n\t\t\t\t\tquestion. Here's what the landing page looked like:\n\t\t\t\t</p>\n\t\t\t\t<img src={mochipLanding.src} alt=\"Screenshot of Mochip's main dashboard page\" />\n\t\t\t\t<p>\n\t\t\t\t\tIn the screenshot we can see a few stats on the right like total estimated time saved and\n\t\t\t\t\thow long you've had your account for. We gamified it a little just to keep people engaged\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tOur chrome extension was made for Educake as they disabled copying question text with the\n\t\t\t\t\tclipboard. We re-enabled that just by clicking a button that was injected into the UI. The\n\t\t\t\t\tchrome extension is no longer on the chrome web store, but we've found that mirrors still\n\t\t\t\t\thave listings that we can't get taken down:{' '}\n\t\t\t\t\t<a href=\"https://extpose.com/ext/195388\" target=\"_blank\" rel=\"noreferrer\">\n\t\t\t\t\t\textpose.com/ext/195388\n\t\t\t\t\t</a>\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tOur userbase grew so big that we ended up with a Discord server and even our own listing\n\t\t\t\t\ton Urban dictionary — I'm yet to find out who made it!{' '}\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://www.urbandictionary.com/define.php?term=mochip\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noreferrer\"\n\t\t\t\t\t>\n\t\t\t\t\t\turbandictionary.com/define.php?term=mochip\n\t\t\t\t\t</a>\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tEventually we \"rebranded\" as I wanted to disassociate my name from the project.\n\t\t\t\t\tUnfortunately I do not have any screenshots from this era to show. I made an alt discord\n\t\t\t\t\taccount and a few announcements saying we'd \"passed on ownership\" however this ineveitably\n\t\t\t\t\tonly lasted for a couple weeks before we were rumbled.\n\t\t\t\t</p>\n\t\t\t\t<h2>Crashing down</h2>\n\t\t\t\t<p>\n\t\t\t\t\tAll good things must come to and end, and Mochip's did after Scott posted about Mochip on\n\t\t\t\t\this reddit account. Like any good CEO, Colin searches his company every now and then on\n\t\t\t\t\tGoogle to see what people are saying or doing and unfortunately came across our reddit\n\t\t\t\t\tpost. He signed up (although under a different email) and tested out the app and was\n\t\t\t\t\tshocked to see it working. Shortly after this I received an email from Colin directly. See\n\t\t\t\t\tbelow\n\t\t\t\t</p>\n\t\t\t\t<img src={emailFromColin.src} alt=\"Email from Colin\" />\n\t\t\t\t<p>\n\t\t\t\t\tI was upset but also a little content — it was sort of validation that I'd successfully\n\t\t\t\t\tmade it and that catching the attention of Colin himself was sort of a good thing. We\n\t\t\t\t\tquickly scheduled a Google Meet, also inviting Scott, and I had one of the most memorable\n\t\t\t\t\tconversations of my life. I am extremely grateful for the advice Colin gave us in the\n\t\t\t\t\tcall.\n\t\t\t\t</p>\n\t\t\t\t<img src={gmeet.src} alt=\"Screenshot of Google Meet\" />\n\t\t\t\t<p>\n\t\t\t\t\tI'd like to give a special thank you to the legendary Colin Hegarty for his kindness and\n\t\t\t\t\tconsideration when reaching out to me. Things could have gone a lot worse for me had this\n\t\t\t\t\tnot been the case. HegartyMaths is a brilliant learning resource and at the end of the\n\t\t\t\t\tday, it's there to help students learn rather than be an inconvenience.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tShortly after, Colin reached out to the Educake team, who we also scheduled a call with.\n\t\t\t\t\tWe explained our complete methodology and suggested ways to prevent this in the future.\n\t\t\t\t\tThe easiest fix from our point of view would be to implement an easy rate limit with Redis\n\t\t\t\t\tthat would make it wildly infeasible to automate a test. The other thing we suggested was\n\t\t\t\t\tto scramble IDs in the database to invalidate <b>our</b> cloned database as much as\n\t\t\t\t\tpossible (e.g. we only had the Hegarty IDs, so we could no longer reverse lookup a\n\t\t\t\t\tquestion).\n\t\t\t\t</p>\n\n\t\t\t\t<img src={goodbyeMochip.src} alt=\"My email replying to Colin\" />\n\n\t\t\t\t<p>\n\t\t\t\t\tThank you for reading, truly. Mochip was a real passion project and I had a wild time\n\t\t\t\t\tbuilding it. ⭐\n\t\t\t\t</p>\n\n\t\t\t\t<hr />\n\n\t\t\t\t<p>\n\t\t\t\t\t<b>Edit 23 Sept, 2022</b>: After making this post public, I posted this on HackerNews and\n\t\t\t\t\tamazingly sat in the #1 spot overnight. This site consequently received a lot of traffic,\n\t\t\t\t\tand I served almost 1.5TB in just shy of 6 hours. Some of the employees at Sparx (the\n\t\t\t\t\tparent company of HegartyMaths) ended up seeing this and forwarded it to Colin. A few\n\t\t\t\t\tminutes ago I just received a really lovely email from Mr Hegarty himself with the subject\n\t\t\t\t\t\"Congrats to you!\" I am so grateful for the kindness and consideration Colin has shown\n\t\t\t\t\tScott and me, so if you are a teacher reading this, then please consider using\n\t\t\t\t\tHegartyMaths at your school! This was the happy ending!\n\t\t\t\t</p>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/2022/01/serverless-discord-oauth/serverless-discord-oauth.tsx",
    "content": "import {stripIndent} from 'common-tags';\nimport Link from 'next/link';\nimport {Highlighter, Shell} from '../../../../components/syntax-highligher';\nimport {Post} from '../../../Post';\nimport discordOAuthDashboardImage from './discord-oauth-dashboard.png';\n\nexport class ServerlessDiscordOAuth extends Post {\n\tpublic name = 'Serverless Discord OAuth with Next.js';\n\tpublic slug = 'serverless-discord-oauth';\n\tpublic date = new Date('2 January 2022');\n\tpublic excerpt = \"Implementing basic Discord OAuth on Vercel's serverless platform\";\n\tpublic hidden = false;\n\tpublic keywords = ['serverless', 'vercel', 'discord', 'oauth', 'node'];\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<h1 className=\"font-serif italic\">Serverless Discord OAuth with Next.js</h1>\n\n\t\t\t\t<p>\n\t\t\t\t\tOAuth is a brilliant solution to a difficult problem, but it can be hard to implement,\n\t\t\t\t\tespecially in a serverless environment. Hopefully, this post will help you get started.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tLive demo:{' '}\n\t\t\t\t\t<Link href=\"/demos/serverless-discord-oauth\">/demos/serverless-discord-oauth</Link>\n\t\t\t\t</p>\n\n\t\t\t\t<h2>The setup</h2>\n\t\t\t\t<p>\n\t\t\t\t\tFirstly, we're going to need to create a Next.js with TypeScript app. Feel free to skip\n\t\t\t\t\tthis if you \"have one that you made earlier.\"\n\t\t\t\t</p>\n\t\t\t\t<Shell>bun create next-app my-app --typescript</Shell>\n\t\t\t\t<h3>Dependencies</h3>\n\t\t\t\t<p>\n\t\t\t\t\tWe will be relying on a few dependencies, the first is <code>discord-api-types</code>{' '}\n\t\t\t\t\twhich provides up-to-date type definitions for Discord's API (who could've guessed). We'll\n\t\t\t\t\talso need <code>axios</code> (or whatever your favourite http lib is) to make requests to\n\t\t\t\t\tDiscord. Additionally, we'll be encoding our user info into a JWT token & using the cookie\n\t\t\t\t\tpackage to serialize and send cookies down to the client. Finally, we'll use{' '}\n\t\t\t\t\t<code>dayjs</code> for basic date manipulation and <code>pathcat</code> to easily build\n\t\t\t\t\turls with query params.\n\t\t\t\t</p>\n\t\t\t\t<Shell>\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tbun add axios cookie pathcat dayjs jsonwebtoken\n\t\t\t\t\t\tbun add --dev discord-api-types @types/jsonwebtoken @types/cookie\n\t\t\t\t\t`}\n\t\t\t\t</Shell>\n\t\t\t\t<h2>Code</h2>\n\t\t\t\t<p>Dope, you've made it this far already! Let's get some code written</p>\n\t\t\t\t<p>\n\t\t\t\t\tFirstly, you're going to want to open up the folder <code>pages/api</code> and create a\n\t\t\t\t\tnew file. We can call it <code>oauth.ts</code>. The api folder is where Next.js will\n\t\t\t\t\tlocate our serverless functions. Handily, I've written a library called{' '}\n\t\t\t\t\t<code>nextkit</code> that can assist us with this process but for the time being it's out\n\t\t\t\t\tof scope for this post – I'll eventually write a small migration guide.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"pages/api/oauth.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\timport type {NextApiHandler} from 'next';\n\t\t\t\t\t\timport type {RESTGetAPIUserResult} from 'discord-api-types/v8';\n\t\t\t\t\t\timport {serialize} from 'cookie';\n\t\t\t\t\t\timport {sign} from 'jsonwebtoken';\n\t\t\t\t\t\timport dayjs from 'dayjs';\n\t\t\t\t\t\timport {pathcat} from 'pathcat';\n\t\t\t\t\t\timport axios from 'axios';\n\n\t\t\t\t\t\t// Configuration constants\n\t\t\t\t\t\t// TODO: Add these to environment variables\n\t\t\t\t\t\tconst CLIENT_ID = 'CLIENT_ID';\n\t\t\t\t\t\tconst CLIENT_SECRET = 'CLIENT_SECRET';\n\t\t\t\t\t\tconst JWT_SECRET = 'CHANGE ME!!!';\n\n\t\t\t\t\t\t// The URL that we will redirect to\n\t\t\t\t\t\t// note: this should be an environment variable\n\t\t\t\t\t\t// but I'll cover that in part 2 since\n\t\t\t\t\t\t// it will work fine locally for the time being\n\t\t\t\t\t\tconst REDIRECT_URI = 'http://localhost:3000/api/oauth';\n\n\t\t\t\t\t\t// Scopes we want to be able to access as a user\n\t\t\t\t\t\tconst scope = ['identify'].join(' ');\n\n\t\t\t\t\t\t// URL to redirect to outbound (to request authorization)\n\t\t\t\t\t\tconst OAUTH_URL = pathcat('https://discord.com/api/oauth2/authorize', {\n\t\t\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\t\t\t\t\tresponse_type: 'code',\n\t\t\t\t\t\t\tscope,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Exchanges an OAuth code for a full user object\n\t\t\t\t\t\t * @param code The code from the callback querystring\n\t\t\t\t\t\t */\n\t\t\t\t\t\tasync function exchangeCode(code: string) {\n\t\t\t\t\t\t\tconst body = new URLSearchParams({\n\t\t\t\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\t\t\t\tclient_secret: CLIENT_SECRET,\n\t\t\t\t\t\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\t\t\t\t\t\tgrant_type: 'authorization_code',\n\t\t\t\t\t\t\t\tcode,\n\t\t\t\t\t\t\t\tscope,\n\t\t\t\t\t\t\t}).toString();\n\n\t\t\t\t\t\t\tconst {data: auth} = await axios.post<{access_token: string; token_type: string}>(\n\t\t\t\t\t\t\t\t'https://discord.com/api/oauth2/token',\n\t\t\t\t\t\t\t\tbody,\n\t\t\t\t\t\t\t\t{headers: {'Content-Type': 'application/x-www-form-urlencoded'}},\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tconst {data: user} = await axios.get<RESTGetAPIUserResult>(\n\t\t\t\t\t\t\t\t'https://discord.com/api/users/@me',\n\t\t\t\t\t\t\t\t{headers: {Authorization: \\`Bearer \\${auth.access_token}\\`}},\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\treturn {user, auth};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Generates the set-cookie header value from a given JWT token\n\t\t\t\t\t\t */\n\t\t\t\t\t\tfunction getCookieHeader(token: string) {\n\t\t\t\t\t\t\treturn serialize('token', token, {\n\t\t\t\t\t\t\t\thttpOnly: true,\n\t\t\t\t\t\t\t\tpath: '/',\n\t\t\t\t\t\t\t\tsecure: process.env.NODE_ENV !== 'development',\n\t\t\t\t\t\t\t\texpires: dayjs().add(1, 'day').toDate(),\n\t\t\t\t\t\t\t\tsameSite: 'lax',\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst handler: NextApiHandler = async (req, res) => {\n\t\t\t\t\t\t\t// Find our callback code from req.query\n\t\t\t\t\t\t\tconst {code = null} = req.query as {code?: string};\n\n\t\t\t\t\t\t\t// If it doesn't exist, we need to redirect the user\n\t\t\t\t\t\t\t// so that we can get the code\n\t\t\t\t\t\t\tif (typeof code !== 'string') {\n\t\t\t\t\t\t\t\tres.redirect(OAUTH_URL);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Exchange the code for a valid user object\n\t\t\t\t\t\t\tconst {user} = await exchangeCode(code);\n\n\t\t\t\t\t\t\t// Sign a JWT token with the user's details\n\t\t\t\t\t\t\t// encoded into it\n\t\t\t\t\t\t\tconst token = sign(user, JWT_SECRET, {expiresIn: '24h'});\n\n\t\t\t\t\t\t\t// Serialize a cookie and set it\n\t\t\t\t\t\t\tconst cookie = getCookieHeader(token);\n\t\t\t\t\t\t\tres.setHeader('Set-Cookie', cookie);\n\n\t\t\t\t\t\t\t// Redirect the user to wherever we want\n\t\t\t\t\t\t\t// in our application\n\t\t\t\t\t\t\tres.redirect('/');\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\texport default handler;\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tCool! This is the bare bones that we will need to start writing our OAuth. It's quite a\n\t\t\t\t\tlot to bite, but if you break it down line by line and read the comments, it should be\n\t\t\t\t\tfairly self-explanatory. We're still missing a few prerequisites to tell Discord who we\n\t\t\t\t\tare: the client id and secret.\n\t\t\t\t</p>\n\n\t\t\t\t<h3>Obtaining keys</h3>\n\t\t\t\t<p>\n\t\t\t\t\tOur tokens can be obtained by visiting{' '}\n\t\t\t\t\t<a href=\"https://discord.com/developers/applications\" target=\"_blank\" rel=\"noreferrer\">\n\t\t\t\t\t\tdiscord.com/developers/applications\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\tand registering a new application.\n\t\t\t\t</p>\n\t\t\t\t<img\n\t\t\t\t\tsrc={discordOAuthDashboardImage.src}\n\t\t\t\t\talt=\"Screenshot of Discord's Developer OAuth page\"\n\t\t\t\t/>\n\t\t\t\t<ol>\n\t\t\t\t\t<li>\n\t\t\t\t\t\tCopy and paste your client ID into your <code>oauth.ts</code> file\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\tCopy and paste your client secret into your <code>oauth.ts</code> file\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\tAdd your redirect URI (<code>http://localhost:3000/api/oauth</code>) on the dashboard\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\tMake sure all your changes are saved and then we are ready to test it out for the first\n\t\t\t\t\t\ttime!\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\n\t\t\t\t<h2>Testing it</h2>\n\t\t\t\t<p>\n\t\t\t\t\tAwesome, we've got everything setup correctly. Now we can give it a quick spin. You can\n\t\t\t\t\tstart your Next.js development server if you haven't already by running{' '}\n\t\t\t\t\t<code>bun dev</code> in your terminal, you should be able to navigate to{' '}\n\t\t\t\t\t<a target=\"_blank\" href=\"http://localhost:3000/api/oauth\" rel=\"noreferrer\">\n\t\t\t\t\t\tlocalhost:3000/api/oauth\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\tand successfully authenticate.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tAfterwards, if you open up your browser's devtools and check for the cookie section, you\n\t\t\t\t\tshould see a cookie by the name of <code>token</code> – this is ours! Copy the value and\n\t\t\t\t\tpaste it into{' '}\n\t\t\t\t\t<a href=\"https://jwt.io\" target=\"_blank\" rel=\"noreferrer\">\n\t\t\t\t\t\tjwt.io\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\tto decode it and see your details encoded inside it!\n\t\t\t\t</p>\n\n\t\t\t\t<h3>Why JWT?</h3>\n\t\t\t\t<p>\n\t\t\t\t\tWe've picked JWT because it lets us store information on the client side where only the\n\t\t\t\t\tserver can mutate and verify that the server created it. This means users can't modify the\n\t\t\t\t\tdata inside a JWT token, allowing the server to make guarantees about the data encoded.\n\t\t\t\t</p>\n\n\t\t\t\t<h2>Environment variables</h2>\n\t\t\t\t<p>Okay, we're almost there. Final stretch</p>\n\t\t\t\t<p>\n\t\t\t\t\tRight now, we have our constants defined in this file which is fine for prototyping but it\n\t\t\t\t\tnow means that if you want to push your code to github, for example, your client secret\n\t\t\t\t\tand perhaps other private information will be publicly available on your project's\n\t\t\t\t\trepository! The solution? Environment varibles.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tEnvironment variables are bits of information that are provided to a process at runtime.\n\t\t\t\t\tIt means we don't have to store secrets inside our source code.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tThankfully, Next.js makes it super easy for us to use environment variables with something\n\t\t\t\t\tcalled an env file.\n\t\t\t\t</p>\n\n\t\t\t\t<h3>Creating our env file</h3>\n\t\t\t\t<p>\n\t\t\t\t\tFirstly, make a new file in your project's file structure called <code>.env</code> and add\n\t\t\t\t\tthe content below. The format for env files is <code>KEY=value</code>. You can use{' '}\n\t\t\t\t\t<code>openssl rand -hex 64</code> to generate a JWT secret.\n\t\t\t\t</p>\n\n\t\t\t\t<Highlighter filename=\".env\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tCLIENT_ID=<our discord client id>\n\t\t\t\t\t\tCLIENT_SECRET=<our discord client secret>\n\t\t\t\t\t\tJWT_SECRET=<a secure, randomly generated string>\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<p>\n\t\t\t\t\tFinally, we need to update our code to make sure that our <code>api/oauth.ts</code> file\n\t\t\t\t\tcan use the newly generated environment variables.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"pages/api/oauth.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t// ...\n\t\t\t\t\t\tconst CLIENT_ID = process.env.CLIENT_ID;\n\t\t\t\t\t\tconst CLIENT_SECRET = process.env.CLIENT_SECRET;\n\t\t\t\t\t\tconst JWT_SECRET = process.env.JWT_SECRET;\n\t\t\t\t\t\t// ...\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<p>\n\t\t\t\t\tAnd that should be all good! I'll be writing a part two and three later on that will cover\n\t\t\t\t\taccessing the JWT from the server and also deployment to vercel.\n\t\t\t\t</p>\n\n\t\t\t\t<p>Thanks for reading!</p>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/2022/01/zero-kb-blog/zero-kb-blog.tsx",
    "content": "import {stripIndent} from 'common-tags';\nimport {Note} from '../../../../components/note';\nimport {Highlighter} from '../../../../components/syntax-highligher';\nimport {Post} from '../../../Post';\n\nexport class ZeroKbBlog extends Post {\n\tpublic name = 'The 0kb Next.js blog';\n\tpublic slug = 'zero-kb-nextjs-blog';\n\tpublic date = new Date('6 Jan 2022');\n\tpublic hidden = false;\n\tpublic excerpt = 'How I shipped a Next.js app with a 0kb bundle';\n\tpublic keywords = ['nextjs', 'zero', 'bundle', 'nextjs-zero-bundle', 'unstable_runtimeJS'];\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<h1 className=\"font-serif italic\">The 0kb Next.js blog</h1>\n\n\t\t\t\t<Note variant=\"warning\" title=\"Update 3rd April 2023\">\n\t\t\t\t\tThis only applies to apps using the <code>pages</code> directory of Next.js as App Dir\n\t\t\t\t\t(released in v13) does not support the settings used here. RSCs offer a similar idealogy\n\t\t\t\t\tof rendering components on the server only, while also allowing for client side JS.\n\t\t\t\t</Note>\n\n\t\t\t\t<p>\n\t\t\t\t\tOk so the title was a <i>liiittle</i> bit clickbaity, but it's not technically a lie. This\n\t\t\t\t\tentire website has zero JavaScript on <i>every single page</i>... a Next.js app with zero\n\t\t\t\t\tclient side JS. How can this be possible?\n\t\t\t\t</p>\n\n\t\t\t\t<h2>Some context</h2>\n\n\t\t\t\t<p>\n\t\t\t\t\tNext.js is a huge abstraction of <code>react-dom/server</code> and other helpful utilities\n\t\t\t\t\tfor building server side rendered apps powered by React. It's really easy to get started\n\t\t\t\t\twith, and features things like file system routing, statically generated content and much\n\t\t\t\t\tmuch more. The most important thing to understand here is that there's a server...\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tFor those of you who are not familiar with React, it's a <b>JavaScript</b> framework for\n\t\t\t\t\tbuilding user interfaces. It handles the view layer of an app and is used to render the\n\t\t\t\t\tUI. Next.js takes this a step further and allows you to write your own view layer to have\n\t\t\t\t\tthe first render performed on a server, which allows for a lot of performance and UI\n\t\t\t\t\toptimizations because we can ship back a lot less to the client (foreshadowing).\n\t\t\t\t</p>\n\n\t\t\t\t<h2>Runtime JS</h2>\n\n\t\t\t\t<p>\n\t\t\t\t\tWith something called <code>PageConfig</code>, we can instruct Next.js to supply zero\n\t\t\t\t\truntime JS to the client. It comes with a couple of trade-offs, but the general idea is\n\t\t\t\t\tthat we perform our first render on the server and the resulting HTML and CSS is \"frozen\"\n\t\t\t\t\tand sent down to the client over the wire. Zero JavaScript runtime in the browser, no{' '}\n\t\t\t\t\t<code>&lt;script&gt;</code> tags in sight!\n\t\t\t\t</p>\n\n\t\t\t\t<h2>What's the catch?</h2>\n\n\t\t\t\t<p>\n\t\t\t\t\tAs the saying goes, there's no such thing as a free lunch. As with anything, doing this\n\t\t\t\t\tcomes with some trade-offs. For example, Next has a lot of built-in React components that\n\t\t\t\t\tcan speed up not only your app, but also development speed. Using the zero kb mode, we\n\t\t\t\t\tunfortunately cannot make full use of the <code>next/link</code> component; a component\n\t\t\t\t\tthat prefetches pages so that clicks on links don't cause a full page reload.\n\t\t\t\t\tAdditionally, we cannot use <code>next/image</code> as this also requires some minimal\n\t\t\t\t\truntime JavaScript (a regular <code>img</code> works just fine). This also means zero\n\t\t\t\t\tstate updates or literally any JavaScript that would've otherwise been bundled can run.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tSo if you're fine with that, then you can go ahead and enable it. No more worrying about\n\t\t\t\t\tworrying about installing huge npm packages and watching your user count drop in realtime.\n\t\t\t\t\tSee your gorgeous website in pure static HTML & CSS. Gone are the days of\n\t\t\t\t\tbundlephobia.com... I bet at this point you are itching to know how to enable it. Well,\n\t\t\t\t\there's a quick example:\n\t\t\t\t</p>\n\n\t\t\t\t<Highlighter>\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\timport type {PageConfig} from 'next';\n\n\t\t\t\t\t\texport const config: PageConfig = {\n\t\t\t\t\t\t\tunstable_runtimeJS: false,\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\texport default function IndexPage() {\n\t\t\t\t\t\t\treturn <h1>This page has no JavaScript!</h1>;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<p>\n\t\t\t\t\tAnd boom! just like that, 0kb bundle. If you are going to use this though, just bear in\n\t\t\t\t\tmind that as well as the trade offs mentioned above, you literally cannot use any\n\t\t\t\t\tJavaScript in the client anymore for this page. Zero. Nada. Null. Void. That means no\n\t\t\t\t\tstate updates, no network requests, no useEffect, no event listeners, no timers. Nothing.\n\t\t\t\t</p>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/2022/03/open-source/open-source.tsx",
    "content": "import {Post} from '../../../Post';\n\nexport class OpenSource extends Post {\n\tpublic name = 'Open Source';\n\tpublic slug = 'open-source';\n\tpublic date = new Date('20 Mar 2022');\n\tpublic hidden = true;\n\tpublic excerpt = 'Thoughts & feelings on Open Source';\n\tpublic keywords = ['Developer', 'Open Source', 'GitHub', 'sponsorships'];\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<h1 className=\"font-serif italic\">Open Source</h1>\n\n\t\t\t\t<p>\n\t\t\t\t\t<small>\n\t\t\t\t\t\tRelevant xkcd: <a href=\"https://xkcd.com/2347/\">#2347</a>\n\t\t\t\t\t</small>\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tI really do love open source. I love being able to build software that I know people will\n\t\t\t\t\tbe able to make great use of. I love that we can extend already existing open source\n\t\t\t\t\tsoftware &amp; I love that we're able to put licenses on our code and explain where and\n\t\t\t\t\thow you can use it.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tBut open source is a truly double-edged sword. There are always stories on Twitter with\n\t\t\t\t\tpeople explaining how they were taken advantage of, or don't have the resources to keep\n\t\t\t\t\tmaintaining a project.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tRecently, a library called <code>faker.js</code> was nuked by the author in an attempt to\n\t\t\t\t\tmake a \"political\" point. Wildly unsuccessful and, in my opinion, extremely immature.\n\t\t\t\t\tHowever, the community responded really quickly and quickly forked the project into{' '}\n\t\t\t\t\t<code>@faker-js/faker</code>. This meant developers could easily switch their project over\n\t\t\t\t\tto use a 100% API compatible, up-to-date and community-managed version of the project. The\n\t\t\t\t\toriginal author, Marak Squires, ended up deleting the original repo. As well as that,\n\t\t\t\t\tSquires also placed malicious code in another project of his, <code>colors.js</code>, that\n\t\t\t\t\twould infinite loop the victim's computer.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tBut there was a reason for this. Thanks to the wonderful archive.org, we're able to see\n\t\t\t\t\told and deleted posts explaining why this had happened — and it's a common frustration\n\t\t\t\t\twithin the community. Famously, Marak mentions he will do{' '}\n\t\t\t\t\t<a href=\"http://web.archive.org/web/20210704022108/https:/github.com/Marak/faker.js/issues/1046\">\n\t\t\t\t\t\tNo more free work\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\tand he's also written a good{' '}\n\t\t\t\t\t<a href=\"https://web.archive.org/web/20210516172305/https:/marak.com/blog/2021-04-25-monetizing-open-source-is-problematic\">\n\t\t\t\t\t\tfew hundred words\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\ton the topic, too.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tOkay, so he's frustrated with people taking Open Source for granted. What's the solution\n\t\t\t\t\there? Well, fantastic companies like OpenCollective and GitHub are taking the initiative\n\t\t\t\t\tto provide a direct, low-fee method of sponsoring open source projects. Big companies like\n\t\t\t\t\tDiscord, Stripe, and Microsoft have all sponsored small and large projects and sometimes\n\t\t\t\t\tthey get their name on the README in return. At the moment, we're not quite there\n\t\t\t\t\tcompletely, but we're definitely heading in the right direction.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tAlternatively, a recent JavaScript library popped up called <code>motion</code> (\n\t\t\t\t\t<a href=\"https://motion.dev/\">motion.dev</a>) which caused a bit of a stir in the\n\t\t\t\t\tcommunity for shaking things up a little bit with regards to their monetization strategy.\n\t\t\t\t\tThe library itself exists on npm and can be installed as you would any other node module,\n\t\t\t\t\texcept there is no GitHub URL for the package..? Taking a look at the README on npm says\n\t\t\t\t\tthe following:\n\t\t\t\t</p>\n\n\t\t\t\t<blockquote>\n\t\t\t\t\tBecome a sponsor and get access to the private Motion One repo. File issues, read the\n\t\t\t\t\tchangelog and source code, and join discussions that help shape the future of the API.\n\t\t\t\t</blockquote>\n\n\t\t\t\t<p>\n\t\t\t\t\tOkay, interesting, so you can use the module and read the documentation for free, but\n\t\t\t\t\taccessing the source code requires somewhat of a paid subscription. There is valid\n\t\t\t\t\tincentive for companies to do this as it allows them to not only audit the codebase (under\n\t\t\t\t\tsecurity concerns), but also it allows for reading the source code to see how everything\n\t\t\t\t\tworks and learn a lot.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tSo what's the downside to this? Well, one of the reasons <b>traditional</b> open source is\n\t\t\t\t\tbrilliant is because it allows anybody to freely see how something works — so assuming\n\t\t\t\t\tthat is true, could we even call <code>motion</code> an open source project? What's more,\n\t\t\t\t\ta lot of individual developers are students or kids learning and as such, they're not\n\t\t\t\t\tfinancially able to support projects. On top of that, they are the generation we want to\n\t\t\t\t\tbe educating the MOST about programming and so immediately cutting them off is definitely\n\t\t\t\t\tnot a win.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tOne final example of open source being painful has got to be Fastify. Fastify's creator is\n\t\t\t\t\tactive on Twitter very often and there have been a few Tweets describing some headaches\n\t\t\t\t\tthey have had to go through as a team to get Fastify to be successful. Keeping it short,\n\t\t\t\t\tbut one thing they have done extensively has been advertising Fastify, which has kept it\n\t\t\t\t\tmainstream and allowed for more users and therefore sponsorships. Matteo Collina,\n\t\t\t\t\tFastify's creator, has explained that had there not been the advertising done, Fastify\n\t\t\t\t\twould not be as maintained, if at all, as it is today. Right now, Fastify has two core\n\t\t\t\t\tmaintainers and 16 on the core team overall.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tThere is plenty of room for innovation in this space. I'm excited to see where GitHub\n\t\t\t\t\tsponsors and OpenCollective go and if we can see some large tech companies spreading the\n\t\t\t\t\tword about open source.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tBy the way, I do accept sponsorships via GitHub, so if you enjoy my work or writing then\n\t\t\t\t\tplease consider any spare VC funding you have just raised 😊{' '}\n\t\t\t\t\t<a href=\"https://github.com/sponsors/alii\">github.com/sponsors/alii</a>\n\t\t\t\t</p>\n\n\t\t\t\t<p>Thanks for reading!</p>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/2022/08/strict-tsconfig/strict-tsconfig.tsx",
    "content": "import {stripIndent} from 'common-tags';\nimport {Highlighter} from '../../../../components/syntax-highligher';\nimport {Post} from '../../../Post';\n\nexport class StrictTSConfig extends Post {\n\tpublic name = 'A strict TSConfig';\n\tpublic slug = 'strict-tsconfig';\n\tpublic date = new Date('08 Sep 2022');\n\tpublic hidden = false;\n\tpublic excerpt = 'The strictest TypeScript configuration possible. \"Look ma, no errors!\"';\n\tpublic keywords = ['strict', 'tsconfig', 'typescript'];\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<p>\n\t\t\t\t\tHere's a very strict TypeScript configuration file. All the safety checks you could ever\n\t\t\t\t\twant. \"Look ma, no errors!\"\n\t\t\t\t</p>\n\n\t\t\t\t<Highlighter>\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"compilerOptions\": {\n\t\t\t\t\t\t\t\t\"lib\": [\"ESNext\"],\n\t\t\t\t\t\t\t\t\"moduleResolution\": \"NodeNext\",\n\t\t\t\t\t\t\t\t\"module\": \"NodeNext\",\n\t\t\t\t\t\t\t\t\"target\": \"ESNext\",\n\t\t\t\t\t\t\t\t\"strict\": true,\n\t\t\t\t\t\t\t\t\"noEmit\": true,\n\t\t\t\t\t\t\t\t\"useUnknownInCatchVariables\": true,\n\t\t\t\t\t\t\t\t\"noImplicitOverride\": true,\n\t\t\t\t\t\t\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\t\t\t\t\t\t\"noUnusedLocals\": true,\n\t\t\t\t\t\t\t\t\"noUnusedParameters\": true,\n\t\t\t\t\t\t\t\t\"exactOptionalPropertyTypes\": true,\n\t\t\t\t\t\t\t\t\"noImplicitReturns\": true,\n\t\t\t\t\t\t\t\t\"noUncheckedIndexedAccess\": true,\n\t\t\t\t\t\t\t\t\"allowImportingTsExtensions\": true,\n\t\t\t\t\t\t\t\t\"verbatimModuleSyntax\": true,\n\t\t\t\t\t\t\t\t\"isolatedModules\": true,\n\t\t\t\t\t\t\t\t\"noPropertyAccessFromIndexSignature\": true,\n\t\t\t\t\t\t\t\t\"allowUnreachableCode\": false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<hr />\n\n\t\t\t\t<p>\n\t\t\t\t\tAlthough, nowadays I'm just using <code>bun init</code>...\n\t\t\t\t</p>\n\n\t\t\t\t<svg id=\"Bun\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 80 70\">\n\t\t\t\t\t<title>Bun Logo</title>\n\t\t\t\t\t<path\n\t\t\t\t\t\tid=\"Shadow\"\n\t\t\t\t\t\td=\"M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z\"\n\t\t\t\t\t/>\n\t\t\t\t\t<g id=\"Body\">\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Background\"\n\t\t\t\t\t\t\td=\"M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z\"\n\t\t\t\t\t\t\tstyle={{fill: '#fbf0df'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Bottom_Shadow\"\n\t\t\t\t\t\t\tdata-name=\"Bottom Shadow\"\n\t\t\t\t\t\t\td=\"M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z\"\n\t\t\t\t\t\t\tstyle={{fill: '#f6dece'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Light_Shine\"\n\t\t\t\t\t\t\tdata-name=\"Light Shine\"\n\t\t\t\t\t\t\td=\"M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z\"\n\t\t\t\t\t\t\tstyle={{fill: '#fffefc'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Top\"\n\t\t\t\t\t\t\td=\"M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z\"\n\t\t\t\t\t\t\tstyle={{fill: '#ccbea7', fillRule: 'evenodd'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Outline\"\n\t\t\t\t\t\t\td=\"M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</g>\n\t\t\t\t\t<g id=\"Mouth\">\n\t\t\t\t\t\t<g id=\"Background-2\" data-name=\"Background\">\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\td=\"M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z\"\n\t\t\t\t\t\t\t\tstyle={{fill: '#b71422'}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</g>\n\t\t\t\t\t\t<g id=\"Tongue\">\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tid=\"Background-3\"\n\t\t\t\t\t\t\t\tdata-name=\"Background\"\n\t\t\t\t\t\t\t\td=\"M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z\"\n\t\t\t\t\t\t\t\tstyle={{fill: '#ff6164'}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tid=\"Outline-2\"\n\t\t\t\t\t\t\t\tdata-name=\"Outline\"\n\t\t\t\t\t\t\t\td=\"M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</g>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Outline-3\"\n\t\t\t\t\t\t\tdata-name=\"Outline\"\n\t\t\t\t\t\t\td=\"M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</g>\n\t\t\t\t\t<g id=\"Face\">\n\t\t\t\t\t\t<ellipse\n\t\t\t\t\t\t\tid=\"Right_Blush\"\n\t\t\t\t\t\t\tdata-name=\"Right Blush\"\n\t\t\t\t\t\t\tcx=\"53.22\"\n\t\t\t\t\t\t\tcy=\"40.18\"\n\t\t\t\t\t\t\trx=\"5.85\"\n\t\t\t\t\t\t\try=\"3.44\"\n\t\t\t\t\t\t\tstyle={{fill: '#febbd0'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<ellipse\n\t\t\t\t\t\t\tid=\"Left_Bluch\"\n\t\t\t\t\t\t\tdata-name=\"Left Bluch\"\n\t\t\t\t\t\t\tcx=\"22.95\"\n\t\t\t\t\t\t\tcy=\"40.18\"\n\t\t\t\t\t\t\trx=\"5.85\"\n\t\t\t\t\t\t\try=\"3.44\"\n\t\t\t\t\t\t\tstyle={{fill: '#febbd0'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Eyes\"\n\t\t\t\t\t\t\td=\"M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z\"\n\t\t\t\t\t\t\tstyle={{fillRule: 'evenodd'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tid=\"Iris\"\n\t\t\t\t\t\t\td=\"M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z\"\n\t\t\t\t\t\t\tstyle={{fill: '#fff', fillRule: 'evenodd'}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</g>\n\t\t\t\t</svg>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/2023/wtf-esm/wtf-esm.tsx",
    "content": "import {stripIndent} from 'common-tags';\nimport {ExternalLink} from '../../../components/external-link';\nimport {Note} from '../../../components/note';\nimport {Highlighter} from '../../../components/syntax-highligher';\nimport {Post} from '../../Post';\n\nexport class WTFESM extends Post {\n\tpublic name = 'WTF, ESM!?';\n\tpublic slug = 'wtf-esm';\n\tpublic date = new Date('2023-04-03');\n\tpublic hidden = true;\n\tpublic excerpt =\n\t\t'I recently Tweeted about publishing a dual ESM and CJS package to npm. It got a lot of likes, and here is why that matters.';\n\n\tpublic keywords = ['javascript', 'esm', 'typescript', 'publish', 'package', 'npm', 'node'];\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<h1 className=\"font-serif italic\">WTF, ESM!?</h1>\n\n\t\t\t\t<p>\n\t\t\t\t\tI{' '}\n\t\t\t\t\t<ExternalLink href=\"https://twitter.com/alistaiir/status/1634274673876783120\">\n\t\t\t\t\t\trecently Tweeted\n\t\t\t\t\t</ExternalLink>{' '}\n\t\t\t\t\tabout publishing a dual ESM and CJS package to npm. It got a lot of likes, and here is why\n\t\t\t\t\tthat matters. It's important that you understand that I was wrong in my Tweet, and things\n\t\t\t\t\tare arguably easier or more difficult than they seem. This is the current state of\n\t\t\t\t\tpublishing a JS package.\n\t\t\t\t</p>\n\n\t\t\t\t<h3>Preface</h3>\n\n\t\t\t\t<p>\n\t\t\t\t\tI am so incredibly grateful for the absolutely wonderful{' '}\n\t\t\t\t\t<ExternalLink href=\"https://twitter.com/atcb\">Andrew Branch</ExternalLink>, who took a lot\n\t\t\t\t\tof time out of his vacation to correct my Tweet and wrote{' '}\n\t\t\t\t\t<ExternalLink href=\"https://twitter.com/atcb/status/1634653474041503744\">\n\t\t\t\t\t\tthis excellent thread\n\t\t\t\t\t</ExternalLink>\n\t\t\t\t\t. A lot of this blog post is regurgitated text of how I interpreted his Tweets.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tAndrew works on TypeScript itself at Microsoft, specifically on auto imports and modules.\n\t\t\t\t\tIt's likely he's the only person on the planet who knows exactly how this works inside\n\t\t\t\t\tout. It's been rumoured that he will be summoned if you utter \"module resolution\" three\n\t\t\t\t\ttimes in the dark. Thank you Andy - you're truly a super star ⭐💖\n\t\t\t\t</p>\n\n\t\t\t\t<hr />\n\n\t\t\t\t<p>\n\t\t\t\t\tRight now, it's extraordinarily clear we are experiencing growing pains in our great\n\t\t\t\t\tmigration to ECMAScript Modules. Below is the part of my <code>package.json</code> that I\n\t\t\t\t\tposted.\n\t\t\t\t</p>\n\n\t\t\t\t<Highlighter language=\"json\">\n\t\t\t\t\t{stripIndent`\n                        {\n                            \"type\": \"module\",\n                            \"main\": \"./dist/index.cjs\",\n                            \"module\": \"./dist/index.js\",\n                            \"types\": \"./dist/index.d.ts\",\n                            \"exports\": {\n                                \".\": {\n                                    \"types\": \"./dist/index.d.ts\",\n                                    \"import\": \"./dist/index.js\",\n                                    \"require\": \"./dist/index.cjs\"\n                                },\n                                \"./package.json\": \"./package.json\"\n                            }\n                        }\n                    `}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<p>\n\t\t\t\t\tAs mentioned above, I made some mistakes here. First of all, it's important to\n\t\t\t\t\tdifferentiate between what is runtime code that engines will understand (what is\n\t\t\t\t\tJavaScript), and what is type definitions (what is TypeScript). This (seems) easy enough,\n\t\t\t\t\twe can see clearly that there are two <code>types</code> fields. One is under the{' '}\n\t\t\t\t\t<code>.</code> entrypoint for <code>exports</code>, the other is at the root. Let's break\n\t\t\t\t\tit down.\n\t\t\t\t</p>\n\n\t\t\t\t<h3>Where did I go wrong?</h3>\n\n\t\t\t\t<p>\n\t\t\t\t\tIt's pretty hard to get a conclusive answer from the \"crowd\" of JavaScript developers\n\t\t\t\t\tabout the best way to publish a package to npm. Everyone has conflicting answers &amp; we\n\t\t\t\t\tall seem to be following what already exists on GitHub and npm. There are lots of packages\n\t\t\t\t\tthat are published technically incorrectly but used and installed by millions of people.\n\t\t\t\t\tThis means a lot of packages follow what I'm calling a colloquial standard. Here's what I{' '}\n\t\t\t\t\t<b>*thought to be true*</b>, and so do most other devs...\n\t\t\t\t</p>\n\n\t\t\t\t<Note variant=\"warning\" title=\"Warning\">\n\t\t\t\t\tBelow is not the correct way to publish a package to npm. This is what I thought was\n\t\t\t\t\tcorrect at the time of Tweeting.\n\t\t\t\t</Note>\n\n\t\t\t\t<ul>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.types</code> at the root is for TypeScript type definitions. A single{' '}\n\t\t\t\t\t\t<code>.d.ts</code> file can define all exported symbols in your package.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.main</code> is for CJS before <code>exports</code> existed. You can emit a single\n\t\t\t\t\t\tCJS compatible file that can be consumed by (legacy) runtimes.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.module</code> is for an ESM entrypoint before <code>exports</code> existed. This\n\t\t\t\t\t\twas mostly used by bundlers like Webpack, and has never been part of any standard. It's\n\t\t\t\t\t\tsuperseded by <code>exports</code>, but it might be good to keep in order to support the\n\t\t\t\t\t\tolder bundlers.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.exports</code> is the new standard for defining entrypoints for your package. It\n\t\t\t\t\t\tis a map of entrypoints to files. The <code>.</code> entrypoint is the default\n\t\t\t\t\t\tentrypoint. We also include <code>./package.json</code> so the package.json file is also\n\t\t\t\t\t\taccessible. The <code>exports</code> field is supported in modern runtimes. Node has\n\t\t\t\t\t\tsupported it since v16.0.0 - for this reason, you will see <code>exports</code>{' '}\n\t\t\t\t\t\tsometimes referenced as node16.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.exports.*.types</code> is for TypeScript type definitions. A single{' '}\n\t\t\t\t\t\t<code>.d.ts</code> file can define all exported symbols in your package for both CJS and\n\t\t\t\t\t\tESM.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.exports.*.import</code> is for ESM. This is the entrypoint for how a modern\n\t\t\t\t\t\truntime should import your package when running under CommonJS. It is a single ESM\n\t\t\t\t\t\tcompatible file.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.exports.*.require</code> is for CJS. This is the entrypoint for how a modern\n\t\t\t\t\t\truntime should import your package when running under CommonJS. It is a single CJS\n\t\t\t\t\t\tcompatible file.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<code>.exports.*.default</code> is for when a runtime does not match any other\n\t\t\t\t\t\tcondition, and is a fallback. It's also within the spec to specify <code>default</code>{' '}\n\t\t\t\t\t\tas the <b>only</b> entrypoint. I did not use <code>default</code> in my initial Tweet.\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\n\t\t\t\t<p>\n\t\t\t\t\tI made a few mistakes here. First of all, types are specific to ESM and CJS. This means\n\t\t\t\t\tthere should be <b>two</b> <code>types</code> fields. One for ESM, one for CJS. Even the\n\t\t\t\t\tTypeScript documentation gets this wrong, and is something they're working on updating.\n\t\t\t\t\tSolutions for this are also pretty wild. I've managed to get things working by simply\n\t\t\t\t\tcopying <code>./dist/index.d.ts</code> to <code>./dist/index.d.cts</code> after bundling,\n\t\t\t\t\tand making the following changes to my <code>package.json</code>.\n\t\t\t\t</p>\n\n\t\t\t\t<Highlighter language=\"json\">\n\t\t\t\t\t{stripIndent`\n                            {\n                                \"exports\": {\n                                    \".\": {\n                                        \"import\": {\n                                            \"types\": \"./dist/index.d.ts\",\n                                            \"default\": \"./dist/index.js\"\n                                        },\n                                        \"require\": {\n                                            \"types\": \"./dist/index.d.cts\",\n                                            \"default\": \"./dist/index.cjs\"\n                                        }\n                                    },\n                                    \"./package.json\": \"./package.json\"\n                                }\n                            }\n                        `}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<p>\n\t\t\t\t\tNote that we point to a .js file and not .mjs when targeting ESM. This is because our\n\t\t\t\t\tpackage.json has <code>type</code> set to <code>module</code>. This tells our runtime that\n\t\t\t\t\tall files are assumed to be ESM unless they have a <code>.cjs</code> extension. There's no\n\t\t\t\t\tsuch thing as an ESM package, only ESM files. Using <code>\"type\": \"module\",</code> is just\n\t\t\t\t\ta way to tell the runtime to interpret existing files as ESM.\n\t\t\t\t</p>\n\n\t\t\t\t<h3>What gives?</h3>\n\n\t\t\t\t<Note variant=\"info\" title=\"Note\">\n\t\t\t\t\tI'm still figuring this all out, and I'm not an expert. I'm just trying to share what I\n\t\t\t\t\thave learned so far. If you have any corrections or suggestions, please let me know!\n\t\t\t\t</Note>\n\n\t\t\t\t<p>\n\t\t\t\t\tClearly, this is messy. It's messy because we're trying to support a lot of different\n\t\t\t\t\truntimes, and we're trying to support them all at once. We're trying to support ESM, CJS,\n\t\t\t\t\tlegacy bundlers, modern bundlers, and TypeScript. We're trying to support all of these\n\t\t\t\t\truntimes at once, and finally, we're trying to support them all at once in a single{' '}\n\t\t\t\t\t<code>package.json</code> file. Few other languages suffer from this level of complexity\n\t\t\t\t\tand fragmentation.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\tLet's break down the mess and why all these things are the way they are. Starting off with{' '}\n\t\t\t\t\t<code>exports</code>.\n\t\t\t\t</p>\n\n\t\t\t\t<p>\n\t\t\t\t\t<code>exports</code> is the modern way to define what your package exports. We have\n\t\t\t\t\talready established that it is a map of entrypoints to files. Let's step through what\n\t\t\t\t\thappens when a runtime/consumer (we'll use the word consumer, because TypeScript - which\n\t\t\t\t\tis not a runtime - is also reading our code in this case) wants to import our package.\n\t\t\t\t</p>\n\n\t\t\t\t<ol>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>Consumer encounters an import statement</p>\n\n\t\t\t\t\t\t<Highlighter>\n\t\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t\t\timport {something} from 'my-package';\n\t\t\t\t\t\t\t`}\n\t\t\t\t\t\t</Highlighter>\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tConsumer resolve the source code for <code>my-package</code>. In Node.js this is done\n\t\t\t\t\t\t\tby looking for the folder name in <code>node_modules</code>, and then finding the{' '}\n\t\t\t\t\t\t\t<code>package.json</code>. In any case, this is up to the consumer to implement\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tConsumer finds <code>package.json</code> file in the source code folder, and begins to\n\t\t\t\t\t\t\tread the <code>exports</code> field\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\tIt steps through each field (in order, despite it being an object) and checks if the\n\t\t\t\t\t\tcondition the consumer is looking for exists in the <code>exports</code> field.\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tIf the condition is met, the consumer will use the file specified in the{' '}\n\t\t\t\t\t\t\t<code>exports</code> field as the entrypoint for the package. If the condition is not\n\t\t\t\t\t\t\tmet, it will continue to the next field. If no condition is met, a consumer will\n\t\t\t\t\t\t\tusually exit/throw an error.\n\t\t\t\t\t\t</p>\n\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tAn example of a condition being met could be Node.js looking for an ESM file. In this\n\t\t\t\t\t\t\tcase, it would look for the <code>import</code> condition first, before trying to fall\n\t\t\t\t\t\t\tback to <code>default</code> if it exists.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/2025/ambient-declarations/ambient-declarations.tsx",
    "content": "import {stripIndent} from 'common-tags';\nimport {Note} from '../../../components/note';\nimport {Highlighter, Shell} from '../../../components/syntax-highligher';\nimport {Post} from '../../Post';\n\nexport class AmbientDeclarations extends Post {\n\tpublic name = 'Ambient Declarations';\n\tpublic slug = 'ambient-declarations';\n\tpublic date = new Date('9 May 2025');\n\tpublic hidden = false;\n\tpublic keywords = ['Ambient Modules', 'TypeScript', 'Module Resolution'];\n\tpublic excerpt = 'Explaining ambient declarations with @types/bun as an example';\n\n\tpublic render() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<h1 className=\"font-serif italic\">Ambient Declarations</h1>\n\t\t\t\t<p>\n\t\t\t\t\tI recently landed a pull request (\n\t\t\t\t\t<a href=\"https://github.com/oven-sh/bun/pull/18024\">#18024</a>) in{' '}\n\t\t\t\t\t<a href=\"https://bun.com/\">Bun</a> that reorganized and rewrote significant portions of\n\t\t\t\t\tBun's TypeScript definitions. Working on this PR made me realize how little documentation\n\t\t\t\t\tthere is on ambient declarations, so I wanted to write about it.\n\t\t\t\t</p>\n\t\t\t\t<h2>What are ambient declarations?</h2>\n\t\t\t\t<p>I'll start by answering this question with a couple questions...</p>\n\t\t\t\t<blockquote>\n\t\t\t\t\t1. How does TypeScript know the types of my <code>node_modules</code>, which are mostly\n\t\t\t\t\tall .js files?\n\t\t\t\t</blockquote>\n\t\t\t\t<blockquote>\n\t\t\t\t\t2. How does TypeScript know the types of APIs that exist in my runtime?\n\t\t\t\t</blockquote>\n\t\t\t\t<p>\n\t\t\t\t\tThe short answer: <b>It can't!</b>\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tThe short but slightly longer answer is that it <i>CAN</i> with some extra files - ambient\n\t\t\t\t\tdeclarations! These are files that exist somewhere in your project (usually in{' '}\n\t\t\t\t\t<code>node_modules</code>) that contain type information and tell TypeScript what{' '}\n\t\t\t\t\t<i>things</i> exist at runtime. They use the file extension <code>.d.ts</code>, with the\n\t\t\t\t\t`.d` denoting \"declaration\".\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tBy <i>things</i> I mean anything you import and use. That could be functions, classes,\n\t\t\t\t\tvariables, modules themselves, APIs from your runtime, etc.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tThey're called \"ambient\" declarations because in the TypeScript universe ambient simply\n\t\t\t\t\tmeans \"without implementation\"\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tIf you've ever imported a package and magically got autocomplete and type checking, you've\n\t\t\t\t\tbenefited from ambient declarations.\n\t\t\t\t</p>\n\t\t\t\t<p>A simple ambient declaration file could look like this:</p>\n\t\t\t\t<Highlighter filename=\"add.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * Performs addition using AI and LLMs\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @param a - The first number\n\t\t\t\t\t\t * @param b - The second number\n\t\t\t\t\t\t *\n\t\t\t\t\t\t * @returns The sum of a and b (probably)\n\t\t\t\t\t\t */\n\t\t\t\t\t\texport declare function add(a: number, b: number): Promise<number>;\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tIf you can already read TypeScript this ambient declaration will be very easy to\n\t\t\t\t\tunderstand. You can clearly see a JSDoc comment, the types of the arguments, the return\n\t\t\t\t\ttype, an export keyword, etc. It almost looks like real TypeScript, except the really\n\t\t\t\t\timportant part to note here is the keyword <code>declare</code> is used. This keyword\n\t\t\t\t\ttells TypeScript to not expect any runtime code to exist here, it's purely a type\n\t\t\t\t\tdeclaration only.\n\t\t\t\t</p>\n\t\t\t\t<Note variant=\"info\" title=\"Using the declare keyword in source code\">\n\t\t\t\t\tIt's completely legitimate and legal to use the <code>declare</code> keyword inside of\n\t\t\t\t\tregular .ts files. There are many use cases for this, a common one being declaring types\n\t\t\t\t\tof globals.\n\t\t\t\t</Note>\n\t\t\t\t<hr />\n\t\t\t\t<h2>How Does TypeScript Find Types?</h2>\n\t\t\t\t<p>\n\t\t\t\t\tModule resolution is an incredibly complex topic, but it boils down to TypeScript looking\n\t\t\t\t\tfor relevant types in a few places.\n\t\t\t\t</p>\n\t\t\t\t<ul className=\"space-y-6\">\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Bundled types</b>: Some packages include their own <code>.d.ts</code> files.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>DefinitelyTyped</b>: If not, TypeScript looks in <code>@types/</code> packages in{' '}\n\t\t\t\t\t\t<code>node_modules</code>.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Your own project</b>: You can add <code>.d.ts</code> files anywhere in your project\n\t\t\t\t\t\tto describe types for JS code, global variables, or even new modules.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Source</b>: If the module resolution algorithm resolves to an actual TypeScript file,\n\t\t\t\t\t\tthen the types can be read from the original source code anyway. Some packages on NPM\n\t\t\t\t\t\talso publish their TypeScript source and allow modern tooling to consume it directly.{' '}\n\t\t\t\t\t\t<b>\n\t\t\t\t\t\t\t<u>Ambient declarations are NOT used in either of these scenarios</u>\n\t\t\t\t\t\t</b>\n\t\t\t\t\t\t.\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<h2>Ambient vs. Regular Declarations</h2>\n\t\t\t\t<p>\n\t\t\t\t\t<b>Regular declarations</b> are for code you write and control.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"add.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\texport function add(a: number, b: number): number {\n\t\t\t\t\t\t\treturn a + b;\n\t\t\t\t\t\t}`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\t<b>Ambient declarations</b> are for code that exists elsewhere.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"add.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\texport declare function add(a: number, b: number): number;\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<h2>Module vs. Script Declarations</h2>\n\t\t\t\t<ul>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Module declarations</b>: Any <code>.d.ts</code> file with a top-level{' '}\n\t\t\t\t\t\t<code>import</code> or <code>export</code>. Types are added to the module.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Script (global) declarations</b>: No top-level import/export. Types are added to the\n\t\t\t\t\t\tglobal scope.\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<table>\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th>File Type</th>\n\t\t\t\t\t\t\t<th>Example Syntax</th>\n\t\t\t\t\t\t\t<th>Scope</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td>Module</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<code>export declare function foo(): void;</code>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>Module only (must be imported)</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<td>Script (global)</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<code>declare function setTimeout(...): number;</code>\n\t\t\t\t\t\t\t\t<br />\n\t\t\t\t\t\t\t\t<code>declare function foo(): void;</code>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>Global (available everywhere)</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t\t<p>\n\t\t\t\t\t<b>Rule of thumb:</b> An ambient declaration file is global unless it has a top-level\n\t\t\t\t\timport/export.\n\t\t\t\t</p>\n\t\t\t\t<Note variant=\"warning\" title=\"Global pollution\">\n\t\t\t\t\tScript files can pollute the global namespace and can very easily{' '}\n\t\t\t\t\t<a href=\"https://github.com/oven-sh/bun/issues/8761\">clash with other declarations</a>.\n\t\t\t\t\tPrefer the module pattern unless you <i>really</i> need to patch <code>globalThis</code>.\n\t\t\t\t</Note>\n\n\t\t\t\t<p>\n\t\t\t\t\tWhy does this distinction exist? TypeScript is old in JavaScript's history - it predates\n\t\t\t\t\tthe modern module system (ESM) and needed to support the \"everything is global\" style of\n\t\t\t\t\tearly JS. That's why it still supports both module and script (global) declaration files.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\t<b>How does TypeScript treat these differently?</b>\n\t\t\t\t</p>\n\t\t\t\t<ul>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Module:</b> Everything you declare is private to that module and must be explicitly\n\t\t\t\t\t\timported by the consumer - just like regular TypeScript/ESM code.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Script (global):</b> Everything is injected directly into the global scope of every\n\t\t\t\t\t\tfile in your program. This is how the DOM lib ships types like <code>window</code>,{' '}\n\t\t\t\t\t\t<code>document</code>, and functions like <code>setTimeout</code>.\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<p>\n\t\t\t\t\t<b>When would you use each?</b>\n\t\t\t\t</p>\n\t\t\t\t<ul>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Module:</b> For packages, libraries, and almost all modern code.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Script:</b> For patching browser globals, legacy code, or when you really need to add\n\t\t\t\t\t\tsomething to the global scope.\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<Note variant=\"info\" title=\"Augmenting the global scope from a module\">\n\t\t\t\t\tYou can still augment the global scope from inside a module-style declaration file by\n\t\t\t\t\tusing the <code>global {'{ ... }'}</code> escape hatch, but that should be reserved for\n\t\t\t\t\tunavoidable edge-cases.\n\t\t\t\t</Note>\n\n\t\t\t\t<h2>Declaring Global Types</h2>\n\t\t\t\t<p>\n\t\t\t\t\tSuppose you want to add a global variable that your runtime creates, or perhaps a library\n\t\t\t\t\tyou're using doesn't have types for:\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"globals.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare function myAwesomeFunction(x: string): number;\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tBecause this declaration file is NOT a module, this will be accessible everywhere in your\n\t\t\t\t\tprogram.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tWhat if you wanted to add something to the <code>window</code> object? TypeScript declares\n\t\t\t\t\tthe window variable exists by assigning it to an interface called <code>Window</code>,\n\t\t\t\t\twhich is also declared globally. You can perform{' '}\n\t\t\t\t\t<a href=\"https://www.typescriptlang.org/docs/handbook/declaration-merging.html\">\n\t\t\t\t\t\tDeclaration Merging\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\tto extend that interface, and tell TypeScript about new properties that exist.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"globals.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tinterface Window {\n\t\t\t\t\t\t\tmyAwesomeFunction: (x: string) => number;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\n\t\t\t\t<h2>Declaring modules by name</h2>\n\t\t\t\t<p>\n\t\t\t\t\tYou can declare a module by its name. As long as the ambient declaration file gets\n\t\t\t\t\treferenced or included in your build somehow, then TypeScript will make the module\n\t\t\t\t\tavailable.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"my-legacy-lib.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare module 'my-legacy-lib' {\n\t\t\t\t\t\t\texport function doSomething(): void;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tThis syntax also allows for declaring modules with wildcard matching. We do this in{' '}\n\t\t\t\t\t<code>@types/bun</code>, since Bun allows for importing <code>.toml</code> and{' '}\n\t\t\t\t\t<code>.html</code> files.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"bun.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare module '*.toml' {\n\t\t\t\t\t\t\tconst content: unknown;\n\t\t\t\t\t\t\texport default content;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdeclare module '*.html' {\n\t\t\t\t\t\t\tconst content: string;\n\t\t\t\t\t\t\texport default content;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<h2>Writing Your Own .d.ts Files</h2>\n\t\t\t\t<p>Suppose you're using a JS library with no types. Here's how to add them:</p>\n\t\t\t\t<ol>\n\t\t\t\t\t<li>\n\t\t\t\t\t\tCreate a new <code>.d.ts</code> file (you could put this in a <code>types/</code>{' '}\n\t\t\t\t\t\tfolder)\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>Write a module declaration:</p>\n\n\t\t\t\t\t\t<Highlighter filename=\"types/my-lib.d.ts\">\n\t\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare module 'my-lib' {\n\t\t\t\t\t\t\texport function coolFeature(x: string): number;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t\t\t</Highlighter>\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li className=\"pt-4\">\n\t\t\t\t\t\tMake sure your <code>tsconfig.json</code> includes the types folder (usually automatic).\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t\t<h2>Compiler contract</h2>\n\t\t\t\t<p>\n\t\t\t\t\tSince ambient modules don't contain runtime code, they should be treated like \"promises\"\n\t\t\t\t\tor \"contracts\" that you are making with the compiler. They're like documentation that\n\t\t\t\t\tTypeScript can understand. Just like documentation for humans, it can get out of sync with\n\t\t\t\t\tthe actual runtime code. A lot of the work I'm doing at Bun is ensuring our type\n\t\t\t\t\tdefinitions are up to date with Bun's runtime APIs.\n\t\t\t\t</p>\n\t\t\t\t<h2>Conflicts</h2>\n\t\t\t\t<p>\n\t\t\t\t\tWhile doing research for the pull request mentioned at the beginning, I found a few cases\n\t\t\t\t\twhere the compiler was not able to resolve the types of some of Bun's APIs because we had\n\t\t\t\t\tdeclared that certain symbols existed, where they might have already been declared by{' '}\n\t\t\t\t\t<a href=\"https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts\">\n\t\t\t\t\t\tlib.dom.d.ts\n\t\t\t\t\t</a>{' '}\n\t\t\t\t\t(the builtin types that TypeScript provides by default) or things like{' '}\n\t\t\t\t\t<code>@types/node</code> (the types for Node.js). .\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tAvoiding these conflicts is unfortunately not always possible. Bun implements a really\n\t\t\t\t\tsolid best-effort approach to this, but sometimes you just have to get creative. For\n\t\t\t\t\texample, you might see code like this to \"force\" TypeScript to use one type over another:\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"my-globals.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare var Worker: globalThis extends { onmessage: any; Worker: infer T }\n\t\t\t\t\t\t\t? T\n\t\t\t\t\t\t\t: never;\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tBun's types take this a step further by using a clever trick that lets us use the built-in\n\t\t\t\t\ttypes if they exist, with a graceful fallback when they don't.\n\t\t\t\t</p>\n\t\t\t\t<div className=\"space-y-1\">\n\t\t\t\t\t<Highlighter filename=\"bun.d.ts\">\n\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare module \"bun\" {\n\t\t\t\t\t\t\tnamespace __internal {\n\t\t\t\t\t\t\t\t// \\`onabort\\` is defined in lib.dom.d.ts, so we can check to see if lib dom is loaded by checking if \\`onabort\\` is defined\n\t\t\t\t\t\t\t\ttype LibDomIsLoaded = typeof globalThis extends { onabort: any } ? true : false;\n\n\t\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t\t * Helper type for avoiding conflicts in types.\n\t\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t\t * Uses the lib.dom.d.ts definition if it exists, otherwise defines it locally.\n\t\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t\t * This is to avoid type conflicts between lib.dom.d.ts and \\@types/bun.\n\t\t\t\t\t\t\t\t *\n\t\t\t\t\t\t\t\t * Unfortunately some symbols cannot be defined when both Bun types and lib.dom.d.ts types are loaded,\n\t\t\t\t\t\t\t\t * and since we can't redeclare the symbol in a way that satisfies both, we need to fallback\n\t\t\t\t\t\t\t\t * to the type that lib.dom.d.ts provides.\n\t\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t\ttype UseLibDomIfAvailable<GlobalThisKeyName extends PropertyKey, Otherwise> =\n\t\t\t\t\t\t\t\t\tLibDomIsLoaded extends true\n\t\t\t\t\t\t\t\t\t\t? typeof globalThis extends { [K in GlobalThisKeyName]: infer T } // if it is loaded, infer it from \\`globalThis\\` and use that value\n\t\t\t\t\t\t\t\t\t\t\t? T\n\t\t\t\t\t\t\t\t\t\t\t: Otherwise // Not defined in lib dom (or anywhere else), so no conflict. We can safely use our own definition\n\t\t\t\t\t\t\t\t\t\t: Otherwise; // Lib dom not loaded anyway, so no conflict. We can safely use our own definition\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t\t</Highlighter>\n\n\t\t\t\t\t<Highlighter filename=\"globals.d.ts\">\n\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t\tdeclare var Worker: Bun.__internal.UseLibDomIfAvailable<'Worker', {\n\t\t\t\t\t\t\t\tnew(filename: string, options?: Bun.WorkerOptions): Worker;\n\t\t\t\t\t\t\t}>;\n\t\t\t\t\t\t`}\n\t\t\t\t\t</Highlighter>\n\t\t\t\t</div>\n\t\t\t\t<p>\n\t\t\t\t\tThis declares that the <code>Worker</code> runtime value exists, and will use the version\n\t\t\t\t\tfrom TypeScript's builtin lib files if they're loaded already in the program, and if not\n\t\t\t\t\tit will use the version passed as the second argument.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tThis trick means we can write types that can exist in many different environments without\n\t\t\t\t\tworrying about impossible-to-fix conflicts breaking the build.\n\t\t\t\t</p>\n\t\t\t\t<hr />\n\t\t\t\t<h2>Declaring entire modules as global namespaces</h2>\n\t\t\t\t<p>\n\t\t\t\t\tIn Bun, everything importable from the <code>'bun'</code> module is also available on the\n\t\t\t\t\tglobal namespace <code>Bun</code>\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"app.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\timport { file } from 'bun';\n\t\t\t\t\t\tawait file('test.txt').text();\n\n\t\t\t\t\t\t// Or, exactly the same thing:\n\n\t\t\t\t\t\tawait Bun.file('test.txt').text();\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tIn fact, you can do an equality to check to see that importing the module gives you the\n\t\t\t\t\tsame reference to the global namespace.\n\t\t\t\t</p>\n\t\t\t\t<Shell hasDollarOnFirstLineOnly>\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tbun repl\n\n\t\t\t\t\t\tWelcome to Bun v1.2.13\n\t\t\t\t\t\tType \".help\" for more information.\n\n\t\t\t\t\t\t> require(\"bun\") === Bun\n\t\t\t\t\t\ttrue\n\t\t\t\t\t`}\n\t\t\t\t</Shell>\n\t\t\t\t<p>\n\t\t\t\t\tDeclaring this in TypeScript uses some strange syntax. You can{' '}\n\t\t\t\t\t<a href=\"https://github.com/oven-sh/bun/blob/main/packages/bun-types/bun.ns.d.ts#L3-L5\">\n\t\t\t\t\t\tfind the declaration here\n\t\t\t\t\t</a>\n\t\t\t\t\t, but it looks like this:\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"bun.ns.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\timport * as BunModule from \"bun\";\n\n\t\t\t\t\t\tdeclare global {\n\t\t\t\t\t\t\texport import Bun = BunModule;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>Let's break it down</p>\n\t\t\t\t<ol>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>We have an import statement, so this file becomes a module.</p>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tWe import everything from the <code>bun</code> module and alias to a namespace called{' '}\n\t\t\t\t\t\t\t<code>BunModule</code>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tWe use the `declare global` block to escape back into global scope, and then use the\n\t\t\t\t\t\t\tfunky syntax <code>export import</code> to re-export the namespace to the global scope\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t\t<p>\n\t\t\t\t\tThis <code>export import</code> syntax a way of saying \"re-export this namespace\" - except\n\t\t\t\t\twhen declaring on the global scope (inside a <code>declare global {'{ }'}</code> block or\n\t\t\t\t\tinside a script/global file) the export keyword kind of turns into a namespace declaration\n\t\t\t\t\tfor the global scope.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tHere is the \"rest\" of <code>@types/bun</code> that piece this all together\n\t\t\t\t</p>\n\t\t\t\t<div className=\"space-y-1\">\n\t\t\t\t\t<Highlighter filename=\"bun.d.ts\">\n\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t\tdeclare module \"bun\" {\n\t\t\t\t\t\t\t\t/**\n\t\t\t\t\t\t\t\t * Creates a new BunFile instance\n\t\t\t\t\t\t\t\t * @param path - The path to the file\n\t\t\t\t\t\t\t\t * @returns A new BunFile instance\n\t\t\t\t\t\t\t\t */\n\t\t\t\t\t\t\t\tfunction file(path: string): BunFile;\n\n\t\t\t\t\t\t\t\tinterface BunFile {\n\t\t\t\t\t\t\t\t\t/* ... */\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t`}\n\t\t\t\t\t</Highlighter>\n\n\t\t\t\t\t<Highlighter filename=\"index.d.ts\">\n\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t\t// You \"import\" types by using triple-slash references,\n\t\t\t\t\t\t\t// which tell TypeScript to add these declarations to the build.\n\n\t\t\t\t\t\t\t/// <reference path=\"./bun.d.ts\" />\n\t\t\t\t\t\t\t/// <reference path=\"./bun.ns.d.ts\" />\n\t\t\t\t\t\t`}\n\t\t\t\t\t</Highlighter>\n\n\t\t\t\t\t<Highlighter filename=\"package.json\" language=\"json\">\n\t\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"name\": \"@types/bun\",\n\t\t\t\t\t\t\t\t\"version\": \"1.2.13\",\n\t\t\t\t\t\t\t\t\"types\": \"./index.d.ts\",\n\t\t\t\t\t\t\t\t// ...\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t`}\n\t\t\t\t\t</Highlighter>\n\t\t\t\t</div>\n\t\t\t\t<p>\n\t\t\t\t\tIn previous versions of Bun's types, the Bun global was defined as a variable that\n\t\t\t\t\timported the <code>bun</code> module.\n\t\t\t\t</p>\n\t\t\t\t<Highlighter filename=\"globals.d.ts\">\n\t\t\t\t\t{stripIndent`\n\t\t\t\t\t\tdeclare var Bun: typeof import(\"bun\");\n\t\t\t\t\t`}\n\t\t\t\t</Highlighter>\n\t\t\t\t<p>\n\t\t\t\t\tBut since this is a runtime value, we have lost all of the types that are exported from\n\t\t\t\t\tthe <code>bun</code> module. For example we can't use <code>Bun.BunFile</code> in our\n\t\t\t\t\tcode.\n\t\t\t\t</p>\n\t\t\t\t<p>\n\t\t\t\t\tIn the pull request mentioned at the beginning, I changed previous declaration to use the{' '}\n\t\t\t\t\t<code>export import</code> syntax which fixed this issue. It means you can now use the Bun\n\t\t\t\t\tnamespace exactly like you'd expect the <code>bun</code> module to behave.\n\t\t\t\t</p>\n\t\t\t\t<hr />\n\t\t\t\t<h2>Ambient declaration gotchas</h2>\n\t\t\t\t<ul>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>\"Cannot find module\" or \"type not found\" errors:</b> Make sure your{' '}\n\t\t\t\t\t\t<code>.d.ts</code> file is included in the project (check <code>tsconfig.json</code>'s{' '}\n\t\t\t\t\t\t<code>include</code>/<code>exclude</code>).\n\t\t\t\t\t</li>\n\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<b>Conflicts:</b> If two libraries declare the same global, you'll get errors. Prefer\n\t\t\t\t\t\tmodule declarations, and avoid globals unless necessary.\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<h2>Resources</h2>\n\t\t\t\t<ul>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<a href=\"https://github.com/oven-sh/bun/tree/main/packages/bun-types\">\n\t\t\t\t\t\t\tBun's TypeScript types\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<a href=\"https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html\">\n\t\t\t\t\t\t\tTypeScript Handbook: Declaration Files\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<a href=\"https://github.com/DefinitelyTyped/DefinitelyTyped\">DefinitelyTyped</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\n\t\t\t\t<hr />\n\n\t\t\t\t<div className=\"text-xs\">\n\t\t\t\t\t<p>\n\t\t\t\t\t\tSpecial thanks to the following people for reading revisions and helping with this post:\n\t\t\t\t\t</p>\n\t\t\t\t\t<a href=\"https://cnrad.dev\">Conrad Crawford</a>,{' '}\n\t\t\t\t\t<a href=\"https://looskie.com\">Cody Miller</a>\n\t\t\t\t</div>\n\t\t\t</>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/blog/Post.ts",
    "content": "import type {ReactNode} from 'react';\n\nexport abstract class Post {\n\tpublic abstract readonly name: string;\n\tpublic abstract readonly slug: string;\n\tpublic abstract readonly date: Date;\n\tpublic abstract readonly hidden: boolean;\n\tpublic abstract readonly excerpt: string;\n\tpublic abstract readonly keywords: string[];\n\n\tpublic toJSON(): Post.PartialJSON {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tslug: this.slug,\n\t\t\tdate: this.date.toISOString(),\n\t\t\thidden: this.hidden,\n\t\t\texcerpt: this.excerpt,\n\t\t\tkeywords: this.keywords,\n\t\t};\n\t}\n\n\tpublic toTinyJSON(): Post.TinyJSON {\n\t\treturn {\n\t\t\tname: this.name,\n\t\t\tslug: this.slug,\n\t\t\tdate: this.date.toISOString(),\n\t\t\texcerpt: this.excerpt,\n\t\t};\n\t}\n\n\tpublic abstract render(): ReactNode;\n}\n\nexport namespace Post {\n\texport interface PartialJSON {\n\t\tname: string;\n\t\tslug: string;\n\t\tdate: string;\n\t\thidden: boolean;\n\t\texcerpt: string;\n\t\tkeywords: string[];\n\t}\n\n\texport interface TinyJSON {\n\t\tname: string;\n\t\tslug: string;\n\t\tdate: string;\n\t\texcerpt: string;\n\t}\n}\n"
  },
  {
    "path": "src/blog/posts.ts",
    "content": "import {Mochip} from './2022/01/mochip/mochip';\nimport {ServerlessDiscordOAuth} from './2022/01/serverless-discord-oauth/serverless-discord-oauth';\nimport {ZeroKbBlog} from './2022/01/zero-kb-blog/zero-kb-blog';\nimport {OpenSource} from './2022/03/open-source/open-source';\nimport {StrictTSConfig} from './2022/08/strict-tsconfig/strict-tsconfig';\nimport {WTFESM} from './2023/wtf-esm/wtf-esm';\nimport {AmbientDeclarations} from './2025/ambient-declarations/ambient-declarations';\n\nexport const posts = [\n\tnew AmbientDeclarations(),\n\tnew WTFESM(),\n\tnew OpenSource(),\n\tnew Mochip(),\n\tnew ZeroKbBlog(),\n\tnew ServerlessDiscordOAuth(),\n\tnew StrictTSConfig(),\n] as const;\n\nexport function sortPosts(p: typeof posts) {\n\treturn [...p].sort((a, b) => {\n\t\tif (a.date > b.date) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tif (a.date < b.date) {\n\t\t\treturn 1;\n\t\t}\n\n\t\treturn 0;\n\t});\n}\n"
  },
  {
    "path": "src/components/blog-footer.tsx",
    "content": "import {CiGlobe, CiTwitter} from 'react-icons/ci';\nimport {VscGithubAlt} from 'react-icons/vsc';\nimport {ExternalLink} from '../components/external-link';\n\nexport const BlogFooter = (\n\t<footer>\n\t\t<p className=\"font-mono [&_a]:inline-block [&_a]:px-6 [&_a]:py-4 [&_a:first-child]:pl-0 [&_a:last-child]:pr-0\">\n\t\t\t<FooterLink href=\"https://alistair.sh\">\n\t\t\t\t<CiGlobe className=\"mr-[3px] mb-[1.5px] inline size-[15px]\" />\n\t\t\t\t<span>alistair.sh</span>\n\t\t\t</FooterLink>\n\n\t\t\t<FooterLink href=\"https://twitter.com/alistaiir\">\n\t\t\t\t<CiTwitter className=\"mr-0.5 inline size-[18px]\" />\n\t\t\t\t<span>alistaiir</span>\n\t\t\t</FooterLink>\n\n\t\t\t<FooterLink href=\"https://github.com/alii\">\n\t\t\t\t<VscGithubAlt className=\"mr-[3px] inline size-[14px]\" />\n\t\t\t\t<span>alii</span>\n\t\t\t</FooterLink>\n\t\t</p>\n\t</footer>\n);\n\nfunction FooterLink({href, children}: {href: string; children: React.ReactNode}) {\n\treturn (\n\t\t<ExternalLink\n\t\t\thref={href}\n\t\t\tclassName=\"cursor-default text-sm text-zinc-400 decoration-blue-500/20 hover:underline hover:decoration-blue-500/50 dark:text-zinc-700\"\n\t\t>\n\t\t\t{children}\n\t\t</ExternalLink>\n\t);\n}\n"
  },
  {
    "path": "src/components/blog-post-list.tsx",
    "content": "import {useLocalStorage} from 'alistair/hooks';\nimport {AnimatePresence, motion} from 'framer-motion';\nimport {useRef, useState} from 'react';\nimport {flushSync} from 'react-dom';\nimport {TbLock, TbLockOpen} from 'react-icons/tb';\nimport {posts} from '../blog/posts';\nimport {useIsomorphicValue} from '../hooks/use-isomorphic-value';\n\nconst allPosts = posts.filter(post => !post.hidden);\n\nexport function BlogPostList() {\n\tconst [isFocused, setIsFocused] = useState(false);\n\tconst [isHovered, setIsHovered] = useState(false);\n\n\tconst [didHoverOrFocusOnceLocalStorage, setDidHoverOrFocusOnce] = useLocalStorage(\n\t\t'blog-post-list:did-hover-or-focus-once',\n\t\t() => false,\n\t);\n\n\tconst [isLockedOpenLocalStorage, setIsLockedOpen] = useLocalStorage(\n\t\t'blog-post-list:is-locked-open',\n\t\t() => false,\n\t);\n\n\tconst didHoverOrFocusOnce = useIsomorphicValue(\n\t\t() => didHoverOrFocusOnceLocalStorage,\n\t\t() => false,\n\t);\n\n\tconst isLockedOpen = useIsomorphicValue(\n\t\t() => isLockedOpenLocalStorage,\n\t\t() => false,\n\t);\n\n\tconst isActuallyExpanded = isFocused || isHovered || isLockedOpen;\n\n\tconst openSideEffect = () => {\n\t\tif (!didHoverOrFocusOnce) {\n\t\t\tsetIsLockedOpen(true);\n\t\t\tsetDidHoverOrFocusOnce(true);\n\t\t}\n\t};\n\n\tconst hover = () => {\n\t\topenSideEffect();\n\t\tsetIsHovered(true);\n\t};\n\n\tconst focus = () => {\n\t\topenSideEffect();\n\t\tsetIsFocused(true);\n\t};\n\n\tconst blur = () => setIsFocused(false);\n\tconst out = () => setIsHovered(false);\n\n\tconst button = useRef<HTMLButtonElement>(null);\n\n\treturn (\n\t\t<div\n\t\t\tclassName=\"relative pt-4\"\n\t\t\tonMouseOver={hover}\n\t\t\tonMouseOut={out}\n\t\t\tonFocus={focus}\n\t\t\tonBlur={blur}\n\t\t>\n\t\t\t<div className=\"px-4\">\n\t\t\t\t<div className=\"items-tart flex justify-between border-b border-zinc-200 pb-4 dark:border-zinc-800\">\n\t\t\t\t\t<p className=\"mr-4\">\n\t\t\t\t\t\tI write every now and then, often about stuff I've recently worked on.\n\t\t\t\t\t\tHover your mouse here to see the list.\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<motion.button\n\t\t\t\t\t\tref={button}\n\t\t\t\t\t\ttitle={isLockedOpen ? 'Lock the list open' : 'Unlock the list'}\n\t\t\t\t\t\tclassName=\"group z-10 -my-3 -mr-3.5 -ml-6 inline h-fit cursor-pointer px-5 focus-visible:outline-none\"\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tflushSync(() => {\n\t\t\t\t\t\t\t\tsetIsLockedOpen(x => !x);\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tbutton.current?.blur();\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tinitial={didHoverOrFocusOnce ? 'hidden' : 'shown'}\n\t\t\t\t\t\tanimate={didHoverOrFocusOnce ? 'hidden' : 'shown'}\n\t\t\t\t\t\tvariants={{\n\t\t\t\t\t\t\tshown: {\n\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\thidden: {\n\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"sr-only\">Toggle blog post list</span>\n\n\t\t\t\t\t\t<div className=\"py-6\">\n\t\t\t\t\t\t\t<span className=\"-m-2 block rounded-md p-2 group-hover:bg-zinc-200 group-focus-visible:ring-2 group-focus-visible:ring-sky-500 dark:group-hover:bg-zinc-800\">\n\t\t\t\t\t\t\t\t{isLockedOpen && didHoverOrFocusOnce ? (\n\t\t\t\t\t\t\t\t\t<TbLock className=\"size-4 text-zinc-800 duration-200 dark:text-zinc-400\" />\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<TbLockOpen className=\"size-4 text-zinc-800 duration-200 dark:text-zinc-400\" />\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</motion.button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<motion.div\n\t\t\t\tinitial={isActuallyExpanded ? 'expanded' : 'collapsed'}\n\t\t\t\tanimate={isActuallyExpanded ? 'expanded' : 'collapsed'}\n\t\t\t\tclassName=\"relative overflow-hidden\"\n\t\t\t\tvariants={{\n\t\t\t\t\tcollapsed: {\n\t\t\t\t\t\theight: 72,\n\t\t\t\t\t},\n\t\t\t\t\texpanded: {\n\t\t\t\t\t\theight: 'auto',\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\ttype: 'spring',\n\t\t\t\t\tmass: 0.2,\n\t\t\t\t\tstiffness: 170,\n\t\t\t\t\tdamping: 20,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div className=\"flex flex-col p-4 pt-1 pb-1.5\">\n\t\t\t\t\t{allPosts.map(post => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\tkey={post.slug}\n\t\t\t\t\t\t\t\tclassName=\"group -mx-2 block rounded-lg py-1\"\n\t\t\t\t\t\t\t\thref={`/${post.slug}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<div className=\"rounded-md px-3 py-2 duration-100 group-last:rounded-b-xl group-hover:bg-zinc-200/50 group-active:scale-[0.98] dark:group-hover:bg-zinc-800\">\n\t\t\t\t\t\t\t\t\t<h2 className=\"font-serif text-base text-black italic dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t{post.name}\n\t\t\t\t\t\t\t\t\t</h2>\n\n\t\t\t\t\t\t\t\t\t<p className=\"text-zinc-800 dark:text-zinc-400\">{post.excerpt}</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</div>\n\n\t\t\t\t<AnimatePresence initial={false}>\n\t\t\t\t\t{!isActuallyExpanded && (\n\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\tclassName=\"pointer-events-none absolute right-0 bottom-0 left-0 h-full bg-gradient-to-t from-zinc-100 to-transparent dark:from-zinc-950/80\"\n\t\t\t\t\t\t\tinitial={{\n\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tanimate={{\n\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\texit={{\n\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</AnimatePresence>\n\t\t\t</motion.div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/components/external-link.tsx",
    "content": "import type {ComponentProps} from 'react';\n\nexport function ExternalLink(props: ComponentProps<'a'>) {\n\treturn <a {...props} target=\"_blank\" rel=\"noopener noreferrer\" />;\n}\n"
  },
  {
    "path": "src/components/message.tsx",
    "content": "import clsx from 'clsx';\nimport {motion} from 'framer-motion';\nimport type {ReactNode} from 'react';\nimport alistair from '../../public/alistair.jpeg';\nimport type {DistributedOmit} from '../utils/types';\n\nexport function message(\n\t...args:\n\t\t| [key: string, content: ReactNode, className?: string]\n\t\t| [message: DistributedOmit<Extract<MessageOrNode, {type?: 'message'}>, 'type'>]\n): MessageOrNode {\n\tif (args.length === 1) {\n\t\tconst [message] = args;\n\t\treturn {...message, type: 'message'};\n\t} else {\n\t\tconst [key, content, className] = args;\n\t\treturn {\n\t\t\ttype: 'message',\n\t\t\tkey,\n\t\t\tcontent,\n\t\t\tclassName,\n\t\t};\n\t}\n}\n\nmessage.node = (node: ReactNode): MessageOrNode => ({\n\ttype: 'node',\n\tnode,\n});\n\nexport type MessageOrNode =\n\t| {\n\t\t\ttype?: 'message';\n\t\t\tkey: string;\n\t\t\tcontent: ReactNode;\n\t\t\tclassName?: string | undefined;\n\t  }\n\t| {\n\t\t\ttype: 'node';\n\t\t\tnode: ReactNode;\n\t  };\n\nexport interface MessageGroupProps {\n\tmessages: Array<MessageOrNode>;\n}\n\nconst group = {\n\thidden: {opacity: 0, y: 20},\n\tshow: {opacity: 1, y: 0},\n};\n\nconst item = {\n\thidden: {opacity: 0, y: 20},\n\tshow: {opacity: 1, y: 0},\n};\n\nfunction MessageBubble({\n\tcontent,\n\tclassName,\n}: {\n\tisLast?: boolean;\n\tisFirst?: boolean;\n\tcontent: ReactNode;\n\tclassName?: string | undefined;\n}) {\n\treturn (\n\t\t<motion.div\n\t\t\ttransition={{\n\t\t\t\ttype: 'spring',\n\t\t\t\tmass: 1,\n\t\t\t\tdamping: 100,\n\t\t\t\tstiffness: 500,\n\t\t\t}}\n\t\t\tvariants={item}\n\t\t\tclassName={clsx(\n\t\t\t\t'w-fit overflow-hidden bg-zinc-100 text-sm text-zinc-700 dark:bg-zinc-900 dark:text-zinc-400 dark:shadow-none',\n\t\t\t\tclassName,\n\n\t\t\t\t'rounded-[20px]',\n\t\t\t)}\n\t\t>\n\t\t\t{content}\n\t\t</motion.div>\n\t);\n}\n\nexport function MessageGroupContainer({children}: {children: ReactNode}) {\n\treturn (\n\t\t<motion.li\n\t\t\ttransition={{\n\t\t\t\ttype: 'spring',\n\t\t\t\tmass: 11,\n\t\t\t\tdamping: 80,\n\t\t\t\tstiffness: 500,\n\t\t\t\tstaggerChildren: 0.2,\n\t\t\t}}\n\t\t\tvariants={group}\n\t\t\tclassName=\"flex items-end space-x-2\"\n\t\t>\n\t\t\t<img\n\t\t\t\tsrc={alistair.src}\n\t\t\t\tclassName=\"size-8 rounded-full\"\n\t\t\t\talt=\"Me standing in front of some tents\"\n\t\t\t/>\n\n\t\t\t<div className=\"space-y-1\">{children}</div>\n\t\t</motion.li>\n\t);\n}\n\nexport function MessageGroup({messages}: MessageGroupProps) {\n\treturn (\n\t\t<MessageGroupContainer>\n\t\t\t{messages.map((messageOrReactNode, i) => {\n\t\t\t\tif (messageOrReactNode.type === 'node') {\n\t\t\t\t\treturn messageOrReactNode.node;\n\t\t\t\t}\n\n\t\t\t\tconst {key, content, className} = messageOrReactNode;\n\n\t\t\t\treturn (\n\t\t\t\t\t<MessageBubble\n\t\t\t\t\t\tkey={key}\n\t\t\t\t\t\tcontent={content}\n\t\t\t\t\t\tisFirst={i === 0}\n\t\t\t\t\t\tisLast={i === messages.length - 1}\n\t\t\t\t\t\tclassName={className}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\t})}\n\t\t</MessageGroupContainer>\n\t);\n}\n"
  },
  {
    "path": "src/components/note.tsx",
    "content": "import clsx from 'clsx';\nimport type {ReactNode} from 'react';\nimport {VscCheck, VscInfo, VscWarning} from 'react-icons/vsc';\n\nexport type NoteProps = {\n\treadonly title?: string;\n\treadonly children: ReactNode;\n\treadonly variant: 'warning' | 'info' | 'success';\n};\n\nconst icons = {\n\twarning: <VscWarning className=\"mr-2 inline text-sm select-none\" />,\n\tinfo: <VscInfo className=\"mr-2 inline text-sm select-none\" />,\n\tsuccess: <VscCheck className=\"mr-2 inline text-sm select-none\" />,\n\ttip: <VscInfo className=\"mr-2 inline text-sm select-none\" />,\n};\n\nexport function Note(props: NoteProps) {\n\tconst className = clsx(\n\t\t'p-4 pt-3 not-prose rounded-md space-y-2',\n\t\t'[&_code]:inline [&_code]:rounded [&_code]:text-xs [&_code]:p-0.5',\n\t\t'[&_a]:underline [&_a:hover]:text-blue-500',\n\n\t\t{\n\t\t\t'bg-yellow-100/90 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-500':\n\t\t\t\tprops.variant === 'warning',\n\n\t\t\t'bg-blue-50 text-blue-600 dark:bg-blue-900/40 dark:text-blue-500': props.variant === 'info',\n\t\t\t'bg-green-50 text-green-600 dark:bg-green-900/40 dark:text-green-500':\n\t\t\t\tprops.variant === 'success',\n\n\t\t\t'[&_code]:bg-yellow-50 [&_code]:dark:bg-yellow-900/40': props.variant === 'warning',\n\t\t\t'[&_code]:bg-blue-200/90 [&_code]:dark:bg-blue-900/40': props.variant === 'info',\n\t\t\t'[&_code]:bg-green-200/90 [&_code]:dark:bg-green-900/40': props.variant === 'success',\n\t\t},\n\t);\n\n\treturn (\n\t\t<div className={className}>\n\t\t\t<div>\n\t\t\t\t{icons[props.variant]}\n\t\t\t\t{props.title && <h2 className=\"inline text-xs\">{props.title}</h2>}\n\t\t\t</div>\n\n\t\t\t<div className=\"text-sm\">{props.children}</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/components/stats.tsx",
    "content": "import {useMemo} from 'react';\nimport {useFirstEverLoad, useVisitCounts} from '../hooks/use-first-ever-load';\n\nexport function Stats() {\n\tconst [stats] = useFirstEverLoad();\n\tconst [visits] = useVisitCounts();\n\n\tconst firstEverLoadTime = useMemo(() => new Date(stats.time), [stats.time]);\n\n\treturn (\n\t\t<div className=\"m-4 mx-auto max-w-2xl rounded-md bg-blue-100 p-6 py-12 text-blue-800 dark:bg-blue-900 dark:text-blue-100\">\n\t\t\t<p>\n\t\t\t\tYou first visited my website on {firstEverLoadTime.toLocaleDateString()} at{' '}\n\t\t\t\t{firstEverLoadTime.toLocaleTimeString()} and on this first visit, you were on the{' '}\n\t\t\t\t{stats.path} page. Since then, you have visited {visits - 1} more times.{' '}\n\t\t\t\t{visits > 1 && 'Thanks for coming back!'}\n\t\t\t</p>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/components/syntax-highligher.tsx",
    "content": "import type {PropsWithChildren} from 'react';\nimport SyntaxHighlighter from 'react-syntax-highlighter';\n\nimport light from 'react-syntax-highlighter/dist/cjs/styles/hljs/lightfair';\nimport dark from 'react-syntax-highlighter/dist/cjs/styles/hljs/vs2015';\n\nimport clsx from 'clsx';\nimport {TbBrandCss3, TbBrandHtml5, TbBrandJavascript, TbBrandTypescript} from 'react-icons/tb';\n\nconst Pre = ({children}: PropsWithChildren) => <pre className=\"px-4\">{children}</pre>;\n\nexport function Shell({\n\tchildren,\n\thasDollarOnFirstLineOnly,\n}: {\n\treadonly children: string;\n\treadonly hasDollarOnFirstLineOnly?: boolean;\n}) {\n\tconst lines = children.split('\\n');\n\n\treturn (\n\t\t<pre className=\"px-4\">\n\t\t\t{lines.map((line, index) => {\n\t\t\t\tconst isFirst = index === 0;\n\n\t\t\t\treturn (\n\t\t\t\t\t<p\n\t\t\t\t\t\tkey={line}\n\t\t\t\t\t\tclassName={clsx(\n\t\t\t\t\t\t\t'!my-0 before:select-none',\n\t\t\t\t\t\t\thasDollarOnFirstLineOnly\n\t\t\t\t\t\t\t\t? isFirst &&\n\t\t\t\t\t\t\t\t\t\t'text-yellow-800 before:text-yellow-600 before:content-[\"$_\"] dark:text-yellow-200 dark:before:text-yellow-400'\n\t\t\t\t\t\t\t\t: 'before:content-[\"$_\"]',\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{line === '' ? <br /> : line}\n\t\t\t\t\t</p>\n\t\t\t\t);\n\t\t\t})}\n\t\t</pre>\n\t);\n}\n\nfunction Filename({filename}: {readonly filename: string}) {\n\tconst icon = (() => {\n\t\tswitch (true) {\n\t\t\tcase filename.endsWith('.ts'):\n\t\t\t\treturn <TbBrandTypescript className=\"inline\" />;\n\t\t\tcase filename.endsWith('.js'):\n\t\t\t\treturn <TbBrandJavascript className=\"inline\" />;\n\t\t\tcase filename.endsWith('.html'):\n\t\t\t\treturn <TbBrandHtml5 className=\"inline\" />;\n\t\t\tcase filename.endsWith('.css'):\n\t\t\t\treturn <TbBrandCss3 className=\"inline\" />;\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t})();\n\n\treturn (\n\t\t<p className=\"mx-1 mt-1 mb-0 rounded bg-zinc-100 px-3 py-1.5 text-sm text-zinc-600 dark:bg-zinc-900/50 dark:text-zinc-400\">\n\t\t\t<span className=\"mr-2\">{icon}</span>\n\t\t\t<span>{filename}</span>\n\t\t</p>\n\t);\n}\n\nexport function Highlighter({\n\tchildren,\n\tlanguage = 'typescript',\n\tfilename,\n}: {\n\treadonly children: string;\n\treadonly language?: 'typescript' | 'javascript' | 'bash' | 'json' | 'css' | 'html' | 'markdown';\n\treadonly filename?: string;\n}) {\n\treturn (\n\t\t<div className=\"[&_pre]:!m-0 [&_pre]:border-none\">\n\t\t\t<div className=\"hidden overflow-hidden rounded-md border border-zinc-800 dark:block\">\n\t\t\t\t{filename && <Filename filename={filename} />}\n\n\t\t\t\t<SyntaxHighlighter language={language} style={dark} PreTag={Pre}>\n\t\t\t\t\t{children}\n\t\t\t\t</SyntaxHighlighter>\n\t\t\t</div>\n\n\t\t\t<div className=\"rounded-md border border-zinc-200 dark:hidden\">\n\t\t\t\t{filename && <Filename filename={filename} />}\n\n\t\t\t\t<SyntaxHighlighter language={language} style={light} PreTag={Pre}>\n\t\t\t\t\t{children}\n\t\t\t\t</SyntaxHighlighter>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/global.d.ts",
    "content": "declare module '*.css';\n"
  },
  {
    "path": "src/globals.css",
    "content": "@import 'tailwindcss';\n\n@plugin \"@tailwindcss/typography\";\n\n@theme {\n\t--font-serif: var(--next-font-serif), --default-serif-font-family;\n\t--font-sans: var(--next-font-sans), --default-sans-font-family;\n\t--font-mono: var(--next-font-mono), --default-mono-font-family;\n}\n\nbody {\n\t@apply overscroll-none bg-[#fefefe] font-sans text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100;\n}\n\n#__next {\n\t@apply absolute inset-0;\n}\n\na[href] {\n\t@apply ring-sky-500 focus-visible:ring-[2.5px] focus-visible:outline-none;\n}\n"
  },
  {
    "path": "src/hooks/layout.ts",
    "content": "import {useLayoutEffect} from 'react';\n\nexport const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : () => {};\n"
  },
  {
    "path": "src/hooks/use-did-initial-page-animations.ts",
    "content": "import {useEffect} from 'react';\n\nconst store = {did: false};\n\n// This is basically a \"did already mount some pages\" flag\n// which means that if a user is going back and forth between\n// pages, we don't want to do the initial page animations\n// because it makes them wait unnecessarily\nexport function useShouldDoInitialPageAnimations() {\n\tuseEffect(() => {\n\t\tstore.did = true;\n\t}, []);\n\n\treturn !store.did;\n}\n"
  },
  {
    "path": "src/hooks/use-first-ever-load.ts",
    "content": "import {useLocalStorage} from 'alistair/hooks';\nimport {useRouter} from 'next/router';\n\nexport function useFirstEverLoad() {\n\tconst router = useRouter();\n\n\treturn useLocalStorage('user:first-ever-load', () => ({\n\t\tpath: router.pathname,\n\t\ttime: Date.now(),\n\t\tquery: router.query,\n\t}));\n}\n\nexport function useVisitCounts() {\n\treturn useLocalStorage('user:visit-counts', () => 1);\n}\n"
  },
  {
    "path": "src/hooks/use-isomorphic-value.ts",
    "content": "import {useSyncExternalStore} from 'react';\n\nconst noopsub = () => () => {};\nexport function useIsomorphicValue<T>(client: () => T, server: () => T) {\n\treturn useSyncExternalStore(noopsub, client, server);\n}\n"
  },
  {
    "path": "src/hooks/use-lerp-transform.ts",
    "content": "import {MotionValue, useTransform} from 'framer-motion';\nimport {useRef} from 'react';\n\nexport function useLerpTransform<I extends number>(value: MotionValue<I>) {\n\tconst prev = useRef<I | null>(null);\n\n\treturn useTransform(value, newValue => {\n\t\tconst prevValue = prev.current ?? newValue;\n\t\tconst lerpValue = prevValue + (newValue - prevValue) * 10;\n\n\t\tprev.current = newValue;\n\n\t\treturn lerpValue;\n\t});\n}\n"
  },
  {
    "path": "src/pages/404.tsx",
    "content": "import Link from 'next/link';\nimport {TbArrowLeft} from 'react-icons/tb';\n\nexport default function Page404() {\n\treturn (\n\t\t<main className=\"mx-auto max-w-3xl space-y-4 py-20\">\n\t\t\t<p className=\"font-serif text-xl text-sky-700 dark:text-sky-200\">\n\t\t\t\t<span className=\"text-sky-600\">404</span> Sorry, I could not find that page\n\t\t\t</p>\n\n\t\t\t<div>\n\t\t\t\t<Link\n\t\t\t\t\thref=\"/\"\n\t\t\t\t\tclassName=\"flex w-fit items-center space-x-1.5 rounded-full bg-sky-100 px-4 py-2 text-sky-700 dark:bg-sky-950 dark:text-sky-500\"\n\t\t\t\t>\n\t\t\t\t\t<TbArrowLeft className=\"inline\" /> <span>Go home</span>\n\t\t\t\t</Link>\n\t\t\t</div>\n\t\t</main>\n\t);\n}\n"
  },
  {
    "path": "src/pages/[slug].tsx",
    "content": "import clsx from 'clsx';\nimport type {GetStaticPaths, GetStaticProps, PageConfig} from 'next';\nimport Head from 'next/head';\nimport Link from 'next/link';\nimport {posts} from '../blog/posts';\nimport {BlogFooter} from '../components/blog-footer';\nimport {Note} from '../components/note';\n\nexport const config: PageConfig = {\n\tunstable_runtimeJS: false,\n};\n\ninterface Props {\n\treadonly slug: string;\n}\n\nexport default function PostPage({slug}: Props) {\n\tconst post = posts.find(post => post.slug === slug)!;\n\n\treturn (\n\t\t<div className=\"px-4\">\n\t\t\t<div className=\"mx-auto max-w-prose space-y-4 py-28\">\n\t\t\t\t<Head>\n\t\t\t\t\t<title>{post.name}</title>\n\t\t\t\t\t<meta name=\"description\" content={post.excerpt} />\n\t\t\t\t\t<meta name=\"keywords\" content={post.keywords.join(', ')} />\n\t\t\t\t\t<meta name=\"theme-color\" content={post.hidden ? '#ebb305' : '#020711'} />\n\t\t\t\t\t<meta property=\"og:image\" content={`https://alistair.sh/api/og?slug=${post.slug}`} />\n\t\t\t\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" />\n\t\t\t\t\t<meta name=\"twitter:title\" content={post.name} />\n\t\t\t\t\t<meta name=\"twitter:description\" content={post.excerpt} />\n\t\t\t\t\t<meta name=\"twitter:image\" content={`https://alistair.sh/api/og?slug=${post.slug}`} />\n\t\t\t\t\t<meta name=\"twitter:site\" content=\"@alistaiir\" />\n\t\t\t\t\t<meta name=\"twitter:creator\" content=\"@alistaiir\" />\n\t\t\t\t</Head>\n\n\t\t\t\t<div>\n\t\t\t\t\t<Link\n\t\t\t\t\t\tclassName=\"font-mono text-blue-500 hover:text-blue-800 dark:text-zinc-400 dark:hover:text-zinc-600\"\n\t\t\t\t\t\thref=\"/\"\n\t\t\t\t\t>\n\t\t\t\t\t\tcd ../\n\t\t\t\t\t</Link>\n\t\t\t\t</div>\n\n\t\t\t\t{post.hidden && (\n\t\t\t\t\t<Note variant=\"warning\" title=\"Hidden post\">\n\t\t\t\t\t\t<p>This post is not listed on the homepage. Please don't share the link</p>\n\t\t\t\t\t</Note>\n\t\t\t\t)}\n\n\t\t\t\t<p>\n\t\t\t\t\t<time dateTime={post.date.toISOString()} className=\"dark:text-zinc-400\">\n\t\t\t\t\t\t{post.date.toDateString()}\n\t\t\t\t\t</time>\n\t\t\t\t</p>\n\n\t\t\t\t<main\n\t\t\t\t\tclassName={clsx(\n\t\t\t\t\t\t'prose dark:prose-hr:border-zinc-800 prose-sky prose-img:rounded-md prose-img:w-full dark:prose-invert max-w-prose dark:text-zinc-400',\n\t\t\t\t\t\t'prose-hr:border-zinc-200',\n\t\t\t\t\t\t'dark:prose-headings:text-zinc-300',\n\n\t\t\t\t\t\t'prose-pre:border prose-pre:border-zinc-200 prose-pre:bg-transparent prose-pre:text-zinc-700 dark:prose-pre:border-zinc-800 dark:prose-pre:text-zinc-300',\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{post.render()}\n\t\t\t\t</main>\n\n\t\t\t\t{BlogFooter}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport const getStaticProps: GetStaticProps<Props> = async ({params}) => {\n\tconst slug = params!.slug as string;\n\n\tconst post = posts.find(post => post.slug === slug);\n\n\tif (!post) {\n\t\treturn {notFound: true};\n\t}\n\n\treturn {\n\t\tprops: {slug},\n\t};\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => ({\n\tpaths: posts.map(post => ({params: {slug: post.slug}})),\n\tfallback: 'blocking',\n});\n"
  },
  {
    "path": "src/pages/_app.tsx",
    "content": "import '../globals.css';\n\nimport {GoogleAnalytics} from '@next/third-parties/google';\nimport type {AppProps} from 'next/app';\nimport {Inter, JetBrains_Mono, Newsreader} from 'next/font/google';\nimport Head from 'next/head';\nimport {useEffect} from 'react';\nimport {Toaster} from 'react-hot-toast';\nimport {useShouldDoInitialPageAnimations} from '../hooks/use-did-initial-page-animations';\nimport {useFirstEverLoad, useVisitCounts} from '../hooks/use-first-ever-load';\n\nconst mono = JetBrains_Mono({\n\tsubsets: ['latin'],\n});\n\nconst serif = Newsreader({\n\tsubsets: ['latin'],\n\tstyle: ['italic'],\n\tweight: ['400'],\n});\n\nconst body = Inter({subsets: ['latin']});\n\nconst tag = process.env.NEXT_PUBLIC_GTM_ID;\n\nexport default function App({Component, pageProps}: AppProps) {\n\tuseFirstEverLoad();\n\tuseShouldDoInitialPageAnimations();\n\n\tconst [, set] = useVisitCounts();\n\n\tuseEffect(() => {\n\t\tset(x => x + 1);\n\t}, [set]);\n\n\treturn (\n\t\t<>\n\t\t\t<style jsx global>\n\t\t\t\t{`\n\t\t\t\t\t:root {\n\t\t\t\t\t\t--next-font-serif: ${serif.style.fontFamily};\n\t\t\t\t\t\t--next-font-sans: ${body.style.fontFamily};\n\t\t\t\t\t\t--next-font-mono: ${mono.style.fontFamily};\n\t\t\t\t\t}\n\t\t\t\t`}\n\t\t\t</style>\n\n\t\t\t<Head>\n\t\t\t\t<title>Alistair Smith</title>\n\t\t\t\t<meta content=\"width=device-width, initial-scale=1\" name=\"viewport\" />\n\t\t\t\t<link rel=\"icon\" href=\"/favicon.ico\" />\n\t\t\t</Head>\n\n\t\t\t<Component {...pageProps} />\n\n\t\t\t<Toaster />\n\n\t\t\t{tag && <GoogleAnalytics gaId={tag} />}\n\t\t</>\n\t);\n}\n"
  },
  {
    "path": "src/pages/_document.tsx",
    "content": "import Document, {Head, Html, Main, NextScript} from 'next/document';\n\nexport default class WebsiteDocument extends Document {\n\toverride render() {\n\t\treturn (\n\t\t\t<Html>\n\t\t\t\t<Head />\n\t\t\t\t<body>\n\t\t\t\t\t<Main />\n\t\t\t\t\t<NextScript />\n\t\t\t\t\t<script async defer src=\"https://lab.alistair.cloud/latest.js\" />\n\t\t\t\t\t<script defer src=\"https://assets.onedollarstats.com/stonks.js\" />\n\t\t\t\t\t<script\n\t\t\t\t\t\tdangerouslySetInnerHTML={{\n\t\t\t\t\t\t\t__html: `if(\"paintWorklet\" in CSS)CSS.paintWorklet.addModule(\"https://www.unpkg.com/css-houdini-squircle@0.3.0/squircle.min.js\")`,\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</body>\n\t\t\t</Html>\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "src/pages/_error.tsx",
    "content": "export default function ErrorPage() {\n\treturn (\n\t\t<main className=\"mx-auto max-w-3xl px-6 pt-16 pb-40\">\n\t\t\t<p className=\"font-serif text-3xl italic\">Uh oh...</p>\n\t\t\t<p>Apologies, something went wrong there...</p>\n\t\t</main>\n\t);\n}\n"
  },
  {
    "path": "src/pages/api/contact.ts",
    "content": "import {bwitch} from 'bwitch';\nimport {NextkitError} from 'nextkit';\nimport {z} from 'zod';\nimport {api} from '../../server/api';\nimport {env} from '../../server/env';\nimport {codeblock} from '../../utils/discord';\n\nconst schema = z.object({\n\temail: z.string().email(),\n\tbody: z.string().max(500).min(10),\n\tturnstile: z.string(),\n});\n\nexport default api({\n\tasync POST({req, ctx}) {\n\t\tconst body = schema.parse(req.body);\n\n\t\tconst ip = (req.headers['x-forwarded-for'] as string) ?? req.socket.remoteAddress ?? null;\n\n\t\tconst outcome = await ctx.turnstile(body.turnstile, ip);\n\n\t\tif (!outcome.success) {\n\t\t\tthrow new NextkitError(400, 'Invalid turnstile token, robot!');\n\t\t}\n\n\t\tconst result = await fetch(env.DISCORD_WEBHOOK, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {'Content-Type': 'application/json'},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tcontent: 'contact form submission',\n\t\t\t\tembeds: [\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: body.body,\n\t\t\t\t\t\tauthor: {name: body.email},\n\t\t\t\t\t\tfields: [{name: 'ip', value: ip ?? 'unknown!?'}],\n\t\t\t\t\t},\n\n\t\t\t\t\t{\n\t\t\t\t\t\ttitle: 'turnstile',\n\t\t\t\t\t\tdescription: codeblock(JSON.stringify(outcome, null, 2), 'json'),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t});\n\n\t\tif (result.status >= 400) {\n\t\t\tthrow new NextkitError(result.status, 'Error sending notification');\n\t\t}\n\n\t\treturn bwitch(req.headers['content-type'])\n\t\t\t.case('application/json', () => ({sent: true}))\n\t\t\t.or(() => ({_redirect: '/thanks'}));\n\t},\n});\n"
  },
  {
    "path": "src/pages/api/map.ts",
    "content": "import {NextkitError} from 'nextkit';\nimport {z} from 'zod';\nimport {api} from '../../server/api';\nimport {getMapURL} from '../../server/apple-maps';\n\nconst querySchema = z.object({\n\ttheme: z.union([z.literal('light'), z.literal('dark')]),\n});\n\nexport default api({\n\tGET: async ({ctx, req}) => {\n\t\tconst lanyard = await ctx.lanyard.get();\n\n\t\tif (!lanyard.kv.location) {\n\t\t\tthrow new NextkitError(404, 'No location found');\n\t\t}\n\n\t\tconst {theme} = querySchema.parse(req.query);\n\n\t\treturn {\n\t\t\t_redirect: getMapURL(lanyard.kv.location, theme),\n\t\t};\n\t},\n});\n"
  },
  {
    "path": "src/pages/api/oauth/[platform]/callback.ts",
    "content": "import {z} from 'zod';\nimport {api} from '../../../../server/api';\nimport {monzoOAuthAPI} from '../../../../server/monzo';\nimport {createSessionJWT, getCookieHeader} from '../../../../server/sessions';\n\nconst querySchema = z.object({\n\tcode: z.string(),\n});\n\nexport default api({\n\tasync GET({req, res}) {\n\t\tconst result = querySchema.safeParse(req.query);\n\n\t\tif (!result.success) {\n\t\t\treturn {\n\t\t\t\t_redirect: '/oauth/error?message=Invalid%20code',\n\t\t\t};\n\t\t}\n\n\t\tif (req.query.platform !== 'monzo') {\n\t\t\treturn {\n\t\t\t\t_redirect: '/oauth/error?message=Invalid%20platform',\n\t\t\t};\n\t\t}\n\n\t\tconst api = await monzoOAuthAPI.exchangeAuthorizationCode(result.data.code);\n\n\t\tconst token = createSessionJWT({\n\t\t\tmonzo_user_credentials: api.credentials,\n\t\t});\n\n\t\tres.setHeader('Set-Cookie', getCookieHeader(token));\n\n\t\treturn {\n\t\t\t_redirect: '/monzo/dashboard',\n\t\t};\n\t},\n});\n"
  },
  {
    "path": "src/pages/api/oauth/[platform]/redirect.ts",
    "content": "import {z} from 'zod';\nimport {api} from '../../../../server/api';\nimport {monzoOAuthAPI} from '../../../../server/monzo';\n\nconst toRedirectUrlSchema = z.literal('monzo').transform(() => monzoOAuthAPI.getOAuthURL());\n\nconst urlSchema = z\n\t.object({\n\t\tplatform: toRedirectUrlSchema,\n\t})\n\t.transform(result => result.platform);\n\nexport default api({\n\tasync GET({req}) {\n\t\tconst result = urlSchema.safeParse(req.query);\n\n\t\tif (!result.success) {\n\t\t\treturn {\n\t\t\t\t_redirect: '/oauth/error?message=Invalid%20platform',\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\t_redirect: result.data.url,\n\t\t};\n\t},\n});\n"
  },
  {
    "path": "src/pages/api/oauth.ts",
    "content": "import axios from 'axios';\nimport {serialize} from 'cookie';\nimport dayjs from 'dayjs';\nimport type {RESTGetAPIUserResult} from 'discord-api-types/v10';\nimport {sign} from 'jsonwebtoken';\nimport type {NextApiHandler} from 'next';\nimport {pathcat} from 'pathcat';\nimport {env} from '../../server/env';\n\nconst {\n\tDISCORD_DEMO_DISCORD_CLIENT_ID: CLIENT_ID,\n\tDISCORD_DEMO_DISCORD_CLIENT_SECRET: CLIENT_SECRET,\n\tDISCORD_DEMO_JWT_SECRET: JWT_SECRET,\n\tDISCORD_DEMO_REDIRECT_URI: REDIRECT_URI,\n} = env;\n\n// Scopes we want to be able to access as a user\nconst scope = ['identify', 'email'].join(' ');\n\n// URL to redirect to outbound (to request authorization)\nconst OAUTH_URL = pathcat('https://discord.com/api/oauth2/authorize', {\n\tclient_id: CLIENT_ID,\n\tredirect_uri: REDIRECT_URI,\n\tresponse_type: 'code',\n\tscope,\n});\n\n/**\n * Exchanges an OAuth code for a full user object\n * @param code The code from the callback querystring\n */\nasync function exchangeCode(code: string) {\n\tconst body = new URLSearchParams({\n\t\tclient_id: CLIENT_ID,\n\t\tclient_secret: CLIENT_SECRET,\n\t\tredirect_uri: REDIRECT_URI,\n\t\tgrant_type: 'authorization_code',\n\t\tcode,\n\t\tscope,\n\t}).toString();\n\n\tconst {data: auth} = await axios.post<{access_token: string; token_type: string}>(\n\t\t'https://discord.com/api/oauth2/token',\n\t\tbody,\n\t\t{headers: {'Content-Type': 'application/x-www-form-urlencoded'}},\n\t);\n\n\tconst {data: user} = await axios.get<RESTGetAPIUserResult>('https://discord.com/api/users/@me', {\n\t\theaders: {Authorization: `Bearer ${auth.access_token}`},\n\t});\n\n\treturn {user, auth};\n}\n\n/**\n * Generates the set-cookie header value from a given JWT token\n */\nfunction getCookieHeader(token: string) {\n\treturn serialize('token', token, {\n\t\thttpOnly: true,\n\t\tpath: '/',\n\t\tsecure: process.env.NODE_ENV !== 'development',\n\t\texpires: dayjs().add(1, 'day').toDate(),\n\t\tsameSite: 'lax',\n\t});\n}\n\nconst handler: NextApiHandler<never> = async (req, res) => {\n\t// Find our callback code from req.query\n\tconst {code = null} = req.query as {code?: string};\n\n\t// If it doesn't exist, we need to redirect the user\n\t// so that we can get the code\n\tif (typeof code !== 'string') {\n\t\tres.redirect(OAUTH_URL);\n\t\treturn;\n\t}\n\n\t// Exchange the code for a valid user object\n\tconst {user} = await exchangeCode(code);\n\n\t// Sign a JWT token with the user's details\n\t// encoded into it\n\tconst token = sign(user, JWT_SECRET, {expiresIn: '24h'});\n\n\t// Serialize a cookie and set it\n\tconst cookie = getCookieHeader(token);\n\tres.setHeader('Set-Cookie', cookie);\n\n\t// Redirect the user to wherever we want\n\t// in our application\n\tres.redirect('/demos/serverless-discord-oauth');\n};\n\nexport default handler;\n"
  },
  {
    "path": "src/pages/api/og.tsx",
    "content": "import {findLargestUsableFontSize} from '@altano/satori-fit-text';\nimport {unstable_createNodejsStream} from '@vercel/og';\nimport type {NextApiRequest, NextApiResponse, ServerRuntime} from 'next';\nimport type {Font} from 'satori';\nimport {posts} from '../../blog/posts';\n\nexport const runtime: ServerRuntime = 'nodejs';\n\nasync function loadGoogleFont(font: string) {\n\tconst url = `https://fonts.googleapis.com/css2?family=${font}`;\n\tconst css = await (await fetch(url)).text();\n\tconst resource = /src: url\\((.+)\\) format\\('(opentype|truetype)'\\)/.exec(css);\n\n\tconst fontResourceUrl = resource?.[1];\n\n\tif (fontResourceUrl) {\n\t\tconst response = await fetch(fontResourceUrl);\n\n\t\tif (response.status === 200) {\n\t\t\treturn response.arrayBuffer();\n\t\t}\n\t}\n\n\tthrow new Error('failed to load font data');\n}\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n\tconst {slug} = req.query;\n\n\tif (typeof slug !== 'string') {\n\t\tres.status(400).send('Missing slug');\n\t\treturn;\n\t}\n\n\tconst post = posts.find(p => p.slug === slug);\n\n\tif (!post) {\n\t\tres.status(404).send('Not found');\n\t\treturn;\n\t}\n\n\tconst MONO: Font = {\n\t\tname: 'JetBrains Mono',\n\t\tdata: await loadGoogleFont('JetBrains+Mono'),\n\t\tstyle: 'normal',\n\t};\n\n\tconst SANS_SERIF: Font = {\n\t\tname: 'Geist',\n\t\tdata: await loadGoogleFont('Geist'),\n\t\tstyle: 'normal',\n\t};\n\n\tconst dimensions = {\n\t\twidth: 1200,\n\t\theight: 630,\n\t};\n\n\tconst xPadding = 60;\n\n\tconst excerptFontSize = await findLargestUsableFontSize({\n\t\ttext: post.excerpt,\n\t\tfont: MONO,\n\t\tmaxWidth: dimensions.width - xPadding * 2,\n\t\tmaxHeight: (dimensions.height / 3) * 1.5,\n\t\tlineHeight: 1.2,\n\t});\n\n\tconst titleFontSize = await findLargestUsableFontSize({\n\t\ttext: post.name,\n\t\tfont: SANS_SERIF,\n\t\tmaxWidth: dimensions.width - xPadding * 2,\n\t\tmaxHeight: (dimensions.height / 3) * 0.5,\n\t\tmaxFontSize: Math.ceil(excerptFontSize - 12),\n\t});\n\n\tconst node = (\n\t\t<div\n\t\t\ttw=\"flex flex-col justify-center items-start w-full h-full text-zinc-400 font-mono\"\n\t\t\tstyle={{\n\t\t\t\tpadding: `0px ${xPadding}px`,\n\t\t\t\tbackgroundColor: '#030712',\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\ttw=\"font-bold mb-6 leading-tight\"\n\t\t\t\tstyle={{fontSize: titleFontSize, fontFamily: `${SANS_SERIF.name}, sans-serif`}}\n\t\t\t>\n\t\t\t\t{post.name}\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\ttw=\"font-normal text-white\"\n\t\t\t\tstyle={{fontSize: excerptFontSize, lineHeight: 1.2, fontFamily: `${MONO.name}, monospace`}}\n\t\t\t>\n\t\t\t\t{post.excerpt}\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\ttw=\"text-[35px] text-zinc-500 mt-10\"\n\t\t\t\tstyle={{fontFamily: `${SANS_SERIF.name}, sans-serif`}}\n\t\t\t>\n\t\t\t\talistair.sh\n\t\t\t</div>\n\t\t</div>\n\t);\n\n\tconst stream = await unstable_createNodejsStream(node, {\n\t\twidth: 1200,\n\t\theight: 630,\n\t\tfonts: [MONO, SANS_SERIF],\n\t});\n\n\tres.setHeader('Content-Type', 'image/png');\n\tres.setHeader('Cache-Control', 'public, max-age=31536000, immutable');\n\tres.statusCode = 200;\n\tres.statusMessage = 'OK';\n\n\tstream.pipe(res);\n}\n"
  },
  {
    "path": "src/pages/api/ping.ts",
    "content": "import {api} from '../../server/api';\n\nexport default api({GET: async () => Date.now()});\n"
  },
  {
    "path": "src/pages/api/posts.ts",
    "content": "import {posts} from '../../blog/posts';\nimport {api} from '../../server/api';\n\nconst filtered = posts.filter(post => !post.hidden);\n\nexport default api({GET: async () => filtered});\n"
  },
  {
    "path": "src/pages/blog.tsx",
    "content": "import {motion} from 'framer-motion';\nimport Link from 'next/link';\nimport type {ReactNode} from 'react';\nimport {TbArrowLeft} from 'react-icons/tb';\nimport {posts, sortPosts} from '../blog/posts';\nimport {BlogFooter} from '../components/blog-footer';\nimport {useShouldDoInitialPageAnimations} from '../hooks/use-did-initial-page-animations';\n\nexport default function Blog() {\n\tconst shouldAnimate = useShouldDoInitialPageAnimations();\n\n\treturn (\n\t\t<main className=\"mx-auto max-w-xl space-y-4 px-3 pt-24 pb-16\">\n\t\t\t<p className=\"text-sm text-zinc-500 dark:text-zinc-600\">\n\t\t\t\t<Link href=\"/\">\n\t\t\t\t\t<TbArrowLeft className=\"mb-0.5 inline-block\" /> Home\n\t\t\t\t</Link>\n\t\t\t</p>\n\n\t\t\t<h2 className=\"font-serif text-xl italic\">alistair.sh/blog</h2>\n\n\t\t\t<motion.ul\n\t\t\t\tclassName=\"list-inside list-disc space-y-1\"\n\t\t\t\tinitial={shouldAnimate ? 'hidden' : 'show'}\n\t\t\t\tanimate=\"show\"\n\t\t\t\tvariants={{\n\t\t\t\t\thidden: {opacity: 0, y: 32},\n\t\t\t\t\tshow: {\n\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\ty: 0,\n\t\t\t\t\t\ttransition: {staggerChildren: 0.1, ease: [0.22, 1, 0.36, 1]},\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\ttype: 'spring',\n\t\t\t\t\tstiffness: 60,\n\t\t\t\t\tdamping: 18,\n\t\t\t\t\tmass: 1.2,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{sortPosts(posts).flatMap(post => {\n\t\t\t\t\tif (post.hidden) {\n\t\t\t\t\t\treturn [];\n\t\t\t\t\t}\n\n\t\t\t\t\treturn [\n\t\t\t\t\t\t<BlogLink key={post.slug} href={`/${post.slug}`}>\n\t\t\t\t\t\t\t{post.name}\n\t\t\t\t\t\t</BlogLink>,\n\t\t\t\t\t];\n\t\t\t\t})}\n\t\t\t</motion.ul>\n\n\t\t\t<motion.div\n\t\t\t\tinitial={shouldAnimate ? 'hidden' : 'show'}\n\t\t\t\tanimate=\"show\"\n\t\t\t\tvariants={{hidden: {opacity: 0, y: 32}, show: {opacity: 1, y: 0}}}\n\t\t\t\ttransition={{\n\t\t\t\t\ttype: 'spring',\n\t\t\t\t\tstiffness: 60,\n\t\t\t\t\tdamping: 18,\n\t\t\t\t\tmass: 1.2,\n\t\t\t\t\tdelay: 1,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{BlogFooter}\n\t\t\t</motion.div>\n\t\t</main>\n\t);\n}\n\nfunction BlogLink(props: {readonly href: string; readonly children: ReactNode}) {\n\treturn (\n\t\t<motion.li\n\t\t\tvariants={{\n\t\t\t\thidden: {opacity: 0, y: 32},\n\t\t\t\tshow: {\n\t\t\t\t\topacity: 1,\n\t\t\t\t\ty: 0,\n\t\t\t\t\ttransition: {\n\t\t\t\t\t\ttype: 'spring',\n\t\t\t\t\t\tstiffness: 60,\n\t\t\t\t\t\tdamping: 18,\n\t\t\t\t\t\tmass: 1.2,\n\t\t\t\t\t\tease: [0.22, 1, 0.36, 1],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\t\t>\n\t\t\t<Link\n\t\t\t\tclassName=\"cursor-default text-sky-500 hover:text-sky-700 dark:hover:text-sky-600\"\n\t\t\t\thref={props.href}\n\t\t\t>\n\t\t\t\t{props.children}\n\t\t\t</Link>\n\t\t</motion.li>\n\t);\n}\n"
  },
  {
    "path": "src/pages/demos/serverless-discord-oauth.tsx",
    "content": "import type {APIUser} from 'discord-api-types/v10';\nimport {verify} from 'jsonwebtoken';\nimport type {GetServerSideProps, PageConfig} from 'next';\nimport Link from 'next/link';\nimport {env} from '../../server/env';\n\ninterface Props {\n\treadonly user: APIUser | null;\n}\n\nexport const config: PageConfig = {\n\tunstable_runtimeJS: false,\n};\n\nexport default function ServerlessDiscordOAuthDemoPage({user}: Props) {\n\tif (!user) {\n\t\treturn (\n\t\t\t<div className=\"mx-auto max-w-md py-20\">\n\t\t\t\t<h1>you are not signed in!</h1>\n\n\t\t\t\t<Link className=\"text-blue-500 dark:text-blue-300\" href=\"/api/oauth\">\n\t\t\t\t\tLog in with Discord ↗\n\t\t\t\t</Link>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst avatar_url = user.avatar\n\t\t? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}`\n\t\t: user.discriminator === '0'\n\t\t\t? `https://cdn.discordapp.com/embed/avatars/${(BigInt(user.id) >> BigInt(22)) % BigInt(6)}.png`\n\t\t\t: `https://cdn.discordapp.com/embed/avatars/${Number(user.discriminator) % 5}.png`;\n\n\treturn (\n\t\t<div className=\"mx-auto max-w-md py-20\">\n\t\t\t<img src={avatar_url} alt={`Avatar URL for ${user.username}.`} />\n\n\t\t\t<h1>hello, {user.username}!</h1>\n\n\t\t\t<p>clear your cookies to logout!</p>\n\t\t</div>\n\t);\n}\n\nexport const getServerSideProps: GetServerSideProps<Props> = async ctx => {\n\tconst {token = null} = ctx.req.cookies;\n\n\tif (!token) {\n\t\treturn {\n\t\t\tprops: {user: null},\n\t\t};\n\t}\n\n\treturn {\n\t\tprops: {\n\t\t\tuser: verify(token, env.DISCORD_DEMO_JWT_SECRET) as APIUser,\n\t\t},\n\t};\n};\n"
  },
  {
    "path": "src/pages/experiments/index.tsx",
    "content": "import Link from 'next/link';\n\nexport default function ExperimentsList() {\n\treturn (\n\t\t<div className=\"mx-auto max-w-prose space-y-8 px-6 py-24\">\n\t\t\t<p>\n\t\t\t\tThis is a list of random experiments I've built on this website. There's not a lot here and\n\t\t\t\tthis is all quite old.\n\t\t\t</p>\n\n\t\t\t<ul className=\"list-outside list-disc space-y-4 [&_a]:text-blue-400 [&_a:hover]:underline\">\n\t\t\t\t<li>\n\t\t\t\t\t<Link href=\"/experiments/morphing-shapes\">Morphing Shapes</Link>\n\t\t\t\t\t<p className=\"text-sm\">\n\t\t\t\t\t\tAnimating and shifting divs using just css transitions and JS to initiate them\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\n\t\t\t\t<li>\n\t\t\t\t\t<Link href=\"/monzo/dashboard\">Monzo Dashboard</Link>\n\t\t\t\t\t<p className=\"text-sm\">\n\t\t\t\t\t\tUsing the Monzo API to display personal account details. Unfortunately, the Monzo API\n\t\t\t\t\t\trequires me to manually add users, so if you want access, contact me.\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\n\t\t\t\t<li>\n\t\t\t\t\t<Link href=\"/experiments/rekordbox-history-parser\">Rekordbox History Parser</Link>\n\t\t\t\t\t<p className=\"text-sm\">\n\t\t\t\t\t\tRekordbox exports history in a format not so useful for copy pasting. This is a tiny\n\t\t\t\t\t\ttool to fix that\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/pages/experiments/morphing-shapes.tsx",
    "content": "import {useSearchParams} from 'next/navigation';\nimport {useEffect, useMemo, useState} from 'react';\nimport {v4 as uuid} from 'uuid';\n\nconst THEME_COLORS = ['#fc9494', '#94e4fc', '#bffc94', '#ffffff'];\nconst randomColor = () => THEME_COLORS[Math.floor(Math.random() * THEME_COLORS.length)]!;\n\nconst IS_BROWSER = typeof window !== 'undefined';\n\nconst LIMITS = {\n\tTOP: IS_BROWSER ? window.innerHeight - 50 : 750,\n\tLEFT: IS_BROWSER ? window.innerWidth - 50 : 550,\n\tSCALE: 2,\n};\n\nconst randomNumber = (scale: number) => Math.ceil(Math.random() * scale);\n\ninterface BoxState {\n\ttop: number;\n\tleft: number;\n\tscale: number;\n\trotation: number;\n\tborderRadius: string;\n\tbackground: string;\n\tclipped: boolean;\n}\n\nfunction generateNewState(oldState: Partial<BoxState>): BoxState {\n\tconst random = Math.random() > 0.5;\n\n\treturn {\n\t\t...oldState,\n\t\trotation: randomNumber(360),\n\t\ttop: Math.ceil(Math.random() * LIMITS.TOP),\n\t\tleft: Math.ceil(Math.random() * LIMITS.LEFT),\n\t\tscale: Math.ceil(Math.random() * LIMITS.SCALE),\n\n\t\tborderRadius: random ? '0px' : Math.random() > 0.5 ? '50%' : '3px',\n\t\tbackground: random ? '#595959' : randomColor(),\n\n\t\tclipped: random,\n\t};\n}\n\nfunction Box() {\n\tconst delay = 3 * Math.random() + 0.5;\n\n\tconst [state, setState] = useState(() => generateNewState({}));\n\n\tuseEffect(() => {\n\t\tconst timer = setTimeout(() => setState(old => generateNewState(old)), delay * 1000);\n\n\t\treturn () => clearTimeout(timer);\n\t}, [delay]);\n\n\tuseEffect(() => setState(old => generateNewState(old)), []);\n\n\tconst {top, left, scale, rotation, borderRadius, background, clipped} = state;\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tposition: 'fixed',\n\t\t\t\ttop,\n\t\t\t\tleft,\n\t\t\t\tborderRadius,\n\t\t\t\tbackground,\n\t\t\t\theight: '50px',\n\t\t\t\twidth: '50px',\n\t\t\t\ttransform: `scale(${scale}) rotate(${rotation}deg)`,\n\t\t\t\ttransition: `all ${delay}s cubic-bezier(1, 0.1, 0, 0.9)`,\n\t\t\t\tzIndex: -1,\n\n\t\t\t\tclipPath: clipped\n\t\t\t\t\t? 'polygon(50% 0, 50% 0, 100% 100%, 0% 100%)'\n\t\t\t\t\t: 'polygon(0 0, 100% 0, 100% 100%, 0% 100%)',\n\t\t\t}}\n\t\t/>\n\t);\n}\n\nfunction Boxes({count}: {count: number}) {\n\tconst boxes = useMemo(() => [...new Array(count)].map(() => <Box key={uuid()} />), [count]);\n\n\treturn <>{boxes}</>;\n}\n\nexport default function MorphingShapes() {\n\tconst count = useSearchParams().get('count') ?? 4;\n\n\tuseEffect(() => {\n\t\tconst onResize = () => {\n\t\t\tLIMITS.LEFT = window.innerWidth - 50;\n\t\t\tLIMITS.TOP = window.innerHeight - 50;\n\t\t};\n\n\t\twindow.addEventListener('resize', onResize);\n\n\t\treturn () => window.removeEventListener('resize', onResize);\n\t}, []);\n\n\treturn <Boxes count={Number(count)} />;\n}\n"
  },
  {
    "path": "src/pages/experiments/rekordbox-history-parser.tsx",
    "content": "import {useState} from 'react';\n\nfunction parseTitleAndArtist(title: string, artist: string) {\n\tif (artist === '') {\n\t\tconst [actualTitle, actualArtist] = title.split('-');\n\n\t\tif (actualTitle && actualArtist) {\n\t\t\treturn {\n\t\t\t\ttitle: actualTitle.trim(),\n\t\t\t\tartist: actualArtist.trim(),\n\t\t\t};\n\t\t}\n\t}\n\n\treturn {\n\t\ttitle,\n\t\tartist,\n\t};\n}\n\nexport default function RekordboxHistoryParser() {\n\tconst [state, setState] = useState('');\n\n\tconst result = state\n\t\t.split('\\n')\n\t\t.map(l => l.split('\\t'))\n\t\t.slice(1)\n\t\t.map(l => {\n\t\t\tconst [, title, artist, album, bpm, time, key] = l as [\n\t\t\t\tstring,\n\t\t\t\tstring,\n\t\t\t\tstring,\n\t\t\t\tstring,\n\t\t\t\tstring,\n\t\t\t\tstring,\n\t\t\t\tstring,\n\t\t\t];\n\n\t\t\tconst {title: actualTitle, artist: actualArtist} = parseTitleAndArtist(title, artist);\n\n\t\t\treturn {\n\t\t\t\ttitle: actualTitle,\n\t\t\t\tartist: actualArtist,\n\t\t\t\talbum,\n\t\t\t\tbpm,\n\t\t\t\ttime,\n\t\t\t\tkey,\n\t\t\t};\n\t\t})\n\t\t.map(l => {\n\t\t\tif (l.artist === '') {\n\t\t\t\treturn l.title;\n\t\t\t}\n\n\t\t\treturn `${l.title} - ${l.artist}`;\n\t\t})\n\t\t.join('\\n');\n\n\treturn (\n\t\t<div className=\"flex space-x-4\">\n\t\t\t<textarea value={state} className=\"bg-zinc-800\" onChange={e => setState(e.target.value)} />\n\n\t\t\t<pre>{result}</pre>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/pages/index.tsx",
    "content": "import {get} from '@prequist/lanyard';\nimport {motion} from 'framer-motion';\nimport type {GetStaticProps} from 'next';\nimport Link from 'next/link';\nimport {CiTwitter} from 'react-icons/ci';\nimport {SiBun, SiClaude, SiGithub, SiSpotify} from 'react-icons/si';\nimport {useLanyardWS, type Types} from 'use-lanyard';\nimport album from '../../public/album.png';\nimport type {Post} from '../blog/Post';\nimport {posts} from '../blog/posts';\nimport {BlogPostList} from '../components/blog-post-list';\nimport {message, MessageGroup} from '../components/message';\nimport {useShouldDoInitialPageAnimations} from '../hooks/use-did-initial-page-animations';\nimport {env} from '../server/env';\nimport {backupDiscordId, discordId} from '../utils/constants';\n\nexport interface Props {\n\tlanyard: Types.Presence;\n\tbackupLanyard: Types.Presence;\n\tlocation: string;\n\trecentBlogPosts: Post.TinyJSON[];\n}\n\nexport const getStaticProps: GetStaticProps<Props> = async () => {\n\tconst lanyard = await get(discordId);\n\tconst backupLanyard = await get(backupDiscordId);\n\tconst location = lanyard.kv.location ?? env.DEFAULT_LOCATION;\n\n\tconst recentBlogPosts = [...posts]\n\t\t.filter(post => !post.hidden)\n\t\t// .sort((a, b) => dayjs(b.date).unix() - dayjs(a.date).unix())\n\t\t.slice(0, 3)\n\t\t.map(post => post.toTinyJSON());\n\n\treturn {\n\t\trevalidate: 10,\n\t\tprops: {\n\t\t\tlocation,\n\t\t\tlanyard,\n\t\t\tbackupLanyard,\n\t\t\trecentBlogPosts,\n\t\t},\n\t};\n};\n\nexport default function Home(props: Props) {\n\tconst lanyard = useLanyardWS(discordId, {\n\t\tinitialData: props.lanyard,\n\t});\n\n\tconst backupLanyard = useLanyardWS(backupDiscordId, {\n\t\tinitialData: props.backupLanyard,\n\t});\n\n\tconst spotify = lanyard.spotify ?? backupLanyard?.spotify ?? null;\n\n\tconst shouldAnimate = useShouldDoInitialPageAnimations();\n\n\treturn (\n\t\t<main className=\"mx-auto max-w-xl px-3 pt-24 pb-16\">\n\t\t\t<motion.ul\n\t\t\t\ttransition={{\n\t\t\t\t\tstaggerChildren: 0.1,\n\t\t\t\t\tdelayChildren: 0.1,\n\t\t\t\t}}\n\t\t\t\tinitial={shouldAnimate ? 'hidden' : 'show'}\n\t\t\t\tanimate=\"show\"\n\t\t\t\tclassName=\"space-y-8\"\n\t\t\t>\n\t\t\t\t<MessageGroup\n\t\t\t\t\tmessages={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'intro',\n\t\t\t\t\t\t\tclassName: '!max-w-[450px]',\n\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t<div className=\"px-4 py-2.5\">\n\t\t\t\t\t\t\t\t\tI'm <span className=\"font-serif italic\">Alistair</span>. I work at{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"https://www.anthropic.com/\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tAnthropic\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\ton <SiBun className=\"mb-[3px] ml-[2px] inline\" />{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"https://bun.com\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tBun\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\tand <SiClaude className=\"mb-px ml-px inline\" />{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"https://www.claude.com/product/claude-code\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tClaude Code\n\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t. I'm interested in things like language specifications and type systems. I've\n\t\t\t\t\t\t\t\t\tbeen called a TypeScript wizard at least a few times. It's nice to meet you.\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t{spotify && (\n\t\t\t\t\t<MessageGroup\n\t\t\t\t\t\tmessages={[\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tkey: 'music',\n\t\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t\t<div className=\"max-w-[380px] space-y-3 px-4 py-2.5\">\n\t\t\t\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t\t\t\tI listen to a lot of music, and{' '}\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"font-serif italic\">right now</span> I'm listening to this\n\t\t\t\t\t\t\t\t\t\t\tsong on Spotify:\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tkey: 'the-current-song',\n\t\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref={`https://open.spotify.com/track/${spotify.track_id}`}\n\t\t\t\t\t\t\t\t\t\tclassName=\"group relative block w-full min-w-[300px] cursor-default overflow-hidden rounded-[20px] p-4\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<div className=\"absolute inset-0\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"absolute inset-0 z-10 bg-white/70 transition-colors group-hover:bg-white/80 dark:bg-zinc-800/80 dark:group-hover:bg-zinc-800/85\"></div>\n\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\tsrc={spotify.album_art_url ?? album.src}\n\t\t\t\t\t\t\t\t\t\t\t\talt=\"Album art\"\n\t\t\t\t\t\t\t\t\t\t\t\taria-hidden\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute top-1/2 -translate-y-1/2 blur-3xl saturate-[50] dark:saturate-[10]\"\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t<div className=\"relative z-10 flex items-center space-x-4 pr-8\">\n\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\tsrc={spotify.album_art_url ?? album.src}\n\t\t\t\t\t\t\t\t\t\t\t\talt=\"Album art\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"size-12 rounded-md border-2\"\n\t\t\t\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"line-clamp-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<strong>{spotify.song}</strong>\n\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t{spotify.artist && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"line-clamp-1 text-zinc-800 dark:text-white/60\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{spotify.artist.split('; ').join(', ')}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t<div className=\"absolute top-4 right-4 z-10\">\n\t\t\t\t\t\t\t\t\t\t\t<SiSpotify className=\"size-4 text-zinc-900/80 dark:text-white/50\" />\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\n\t\t\t\t{/* <MessageGroup\n\t\t\t\t\tmessages={[\n\t\t\t\t\t\t...(spotify\n\t\t\t\t\t\t\t? []\n\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t// {\n\t\t\t\t\t\t// \tkey: 'not-music',\n\t\t\t\t\t\t// \tcontent: (\n\t\t\t\t\t\t// \t\t<div className=\"px-4 py-2.5\">\n\t\t\t\t\t\t// \t\t\tIn the rare case I'm not listening to anything, you can usually find me out and\n\t\t\t\t\t\t// \t\t\tabout riding my{' '}\n\t\t\t\t\t\t// \t\t\t<Link\n\t\t\t\t\t\t// \t\t\t\thref=\"https://www.youtube.com/watch?v=LBx-JCj-7Y8\"\n\t\t\t\t\t\t// \t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t// \t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t// \t\t\t>\n\t\t\t\t\t\t// \t\t\t\tEvolve skateboard\n\t\t\t\t\t\t// \t\t\t</Link>\n\t\t\t\t\t\t// \t\t\t,{' '}\n\t\t\t\t\t\t// \t\t\t<Link\n\t\t\t\t\t\t// \t\t\t\thref=\"https://www.youtube.com/watch?v=x6vlL9Sscmw\"\n\t\t\t\t\t\t// \t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t// \t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t// \t\t\t>\n\t\t\t\t\t\t// \t\t\t\tDJing (on YouTube)\n\t\t\t\t\t\t// \t\t\t</Link>{' '}\n\t\t\t\t\t\t// \t\t\tor{' '}\n\t\t\t\t\t\t// \t\t\t<Link\n\t\t\t\t\t\t// \t\t\t\thref=\"https://soundcloud.com/alistairsmusic/\"\n\t\t\t\t\t\t// \t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t// \t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t// \t\t\t>\n\t\t\t\t\t\t// \t\t\t\ttrying my hardest to figure out Ableton Live\n\t\t\t\t\t\t// \t\t\t</Link>\n\t\t\t\t\t\t// \t\t</div>\n\t\t\t\t\t\t// \t),\n\t\t\t\t\t\t// },\n\t\t\t\t\t]}\n\t\t\t\t/> */}\n\n\t\t\t\t<MessageGroup messages={[message('remaining-blog-posts', <BlogPostList />)]} />\n\n\t\t\t\t<MessageGroup\n\t\t\t\t\tmessages={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'location',\n\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t<div className=\"relative h-[150px] w-[300px]\">\n\t\t\t\t\t\t\t\t\t<div className=\"absolute inset-0 overflow-hidden rounded-[20px]\">\n\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\tsrc={`/api/map?location=${lanyard.kv.location}&theme=light`}\n\t\t\t\t\t\t\t\t\t\t\talt=\"Map\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute inset-0 h-full w-full scale-125 object-cover dark:hidden\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\tsrc={`/api/map?location=${lanyard.kv.location}&theme=dark`}\n\t\t\t\t\t\t\t\t\t\t\talt=\"Map\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute inset-0 hidden h-full w-full scale-125 object-cover dark:block\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<span className=\"absolute top-1/2 left-1/2 z-10 -mt-7 -ml-7 block size-14 animate-[ping_2s_cubic-bezier(0,_0,_0.2,_1)_infinite] rounded-full bg-lime-500\" />\n\n\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\tsrc={`https://cdn.discordapp.com/avatars/${lanyard.discord_user.id}/${lanyard.discord_user.avatar}.webp?size=160`}\n\t\t\t\t\t\t\t\t\t\talt=\"Avatar\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"absolute top-1/2 left-1/2 z-10 size-16 -translate-x-1/2 -translate-y-1/2 rounded-full border-2\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'location-caption',\n\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t<p className=\"px-4 py-2.5\">\n\t\t\t\t\t\t\t\t\tI'm currently in{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref={`https://maps.apple.com/?q=${lanyard.kv.location}`}\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{lanyard.kv.location}\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\t📍\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t<MessageGroup\n\t\t\t\t\tmessages={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'chat-1',\n\t\t\t\t\t\t\tcontent: <div className=\"max-w-[384px] px-4 py-2.5\">Find me online:</div>,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'github',\n\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t<div className=\"px-4 py-2.5\">\n\t\t\t\t\t\t\t\t\tI'm{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alii\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t@alii on GitHub\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\t<SiGithub className=\"mb-[3px] inline\" />{' '}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'chat-2',\n\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t<div className=\"px-4 py-2.5\">\n\t\t\t\t\t\t\t\t\tI'm{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"https://x.com/intent/user?screen_name=alistaiir\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t@alistaiir on Twitter/X\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\t<CiTwitter className=\"mb-[3px] inline\" />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t{/* <MessageGroup\n\t\t\t\t\tmessages={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkey: 'experiments',\n\t\t\t\t\t\t\tcontent: (\n\t\t\t\t\t\t\t\t<div className=\"px-4 py-2.5\">\n\t\t\t\t\t\t\t\t\tI have some fun experiments on this site, some are functional things I use, others\n\t\t\t\t\t\t\t\t\tare just me messing around.{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"/experiments\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline decoration-zinc-400 dark:decoration-zinc-500/80\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tClick here to see them\n\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t.\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/> */}\n\t\t\t</motion.ul>\n\t\t</main>\n\t);\n}\n"
  },
  {
    "path": "src/pages/monzo/dashboard/index.tsx",
    "content": "import {MonzoAPI, type Id, type Models} from '@otters/monzo';\nimport {HTTPClientError} from 'alistair/http';\nimport {bwitch} from 'bwitch';\nimport type {GetServerSideProps, Redirect} from 'next';\nimport Link from 'next/link';\nimport {parseSessionJWT} from '../../../server/sessions';\n\ntype Props =\n\t| {\n\t\t\tsuccess: true;\n\t\t\tdata: {\n\t\t\t\taccounts: Array<\n\t\t\t\t\tModels.Account & {\n\t\t\t\t\t\tbalance?: Models.Balance | null;\n\t\t\t\t\t\tpots?: Models.Pot[] | null;\n\t\t\t\t\t\twebhooks?: Array<{\n\t\t\t\t\t\t\tid: Id<'webhook'>;\n\t\t\t\t\t\t\taccount_id: Models.Account['id'];\n\t\t\t\t\t\t\turl: string;\n\t\t\t\t\t\t}> | null;\n\t\t\t\t\t}\n\t\t\t\t>;\n\t\t\t};\n\t  }\n\t| {\n\t\t\tsuccess: false;\n\t\t\terror: string;\n\t\t\tbody: unknown;\n\t  };\n\nexport default function MonzoDashboard(props: Props) {\n\tif (!props.success) {\n\t\treturn (\n\t\t\t<div className=\"mx-auto my-16 max-w-2xl space-y-8 border bg-zinc-100 p-10 dark:border-zinc-800 dark:bg-zinc-900\">\n\t\t\t\t<p>{props.error}</p>\n\t\t\t\t<pre className=\"w-full overflow-x-auto border px-2 py-1.5 text-zinc-600 dark:border-zinc-800 dark:text-zinc-400\">\n\t\t\t\t\t{JSON.stringify(props.body, null, 4)}\n\t\t\t\t</pre>\n\t\t\t\t<p className=\"text-zinc-400\">\n\t\t\t\t\tYou may need to explicitly enable permissions in the Monzo app, on your phone.\n\t\t\t\t</p>\n\n\t\t\t\t<div>\n\t\t\t\t\t<Link\n\t\t\t\t\t\tclassName=\"bg-purple-100 px-2 py-1.5 text-sm text-purple-600 dark:bg-purple-600 dark:text-purple-100\"\n\t\t\t\t\t\thref=\"/api/oauth/monzo/redirect\"\n\t\t\t\t\t>\n\t\t\t\t\t\tRetry authorization flow\n\t\t\t\t\t</Link>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst {data} = props;\n\n\treturn (\n\t\t<div className=\"mx-auto my-16 max-w-3xl space-y-8 border bg-zinc-100 p-10 shadow-2xl shadow-black/25 dark:border-zinc-800 dark:bg-zinc-900\">\n\t\t\t<div className=\"space-y-4\">\n\t\t\t\t<h1 className=\"text-xl font-bold\">Accounts</h1>\n\t\t\t\t{data.accounts\n\t\t\t\t\t.filter(x => !x.closed)\n\t\t\t\t\t.map(acct => {\n\t\t\t\t\t\tconst formatter = new Intl.NumberFormat(undefined, {\n\t\t\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\t\t\tcurrency: acct.currency,\n\t\t\t\t\t\t\tunitDisplay: 'narrow',\n\t\t\t\t\t\t\tcurrencyDisplay: 'narrowSymbol',\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst format = (pennies: number) =>\n\t\t\t\t\t\t\tformatter.format(pennies / 100).replace(/\\.00$/, '');\n\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div key={acct.id} className=\"border dark:border-zinc-800\">\n\t\t\t\t\t\t\t\t<div className=\"flex justify-between p-2.5\">\n\t\t\t\t\t\t\t\t\t<div className=\"space-y-0.5\">\n\t\t\t\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t\t\t\t{acct.owners.map(o => o.preferred_first_name).join(', ')} ({acct.type})\n\t\t\t\t\t\t\t\t\t\t</p>\n\n\t\t\t\t\t\t\t\t\t\t{acct.balance ? (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xl text-zinc-600 dark:text-zinc-300\">\n\t\t\t\t\t\t\t\t\t\t\t\t{format(acct.balance.balance)}{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t{!acct.balance || acct.balance.spend_today === 0 ? null : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t-{format(Math.abs(acct.balance.spend_today))} today\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t{bwitch(acct.type)\n\t\t\t\t\t\t\t\t\t\t\t\t\t.case(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t'uk_monzo_flex_backing_loan',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'This is a loan account used for flex transactions. It has no balance and will be considered closed once the debt is paid off.',\n\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t.or(() => 'This type of acccount has no balance')}\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col items-end space-y-1\">\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-zinc-500\">{acct.description}</p>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-zinc-500\">{acct.id}</p>\n\n\t\t\t\t\t\t\t\t\t\t{acct.payment_details && (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-zinc-500\">\n\t\t\t\t\t\t\t\t\t\t\t\t{acct.payment_details.locale_uk.account_number} •{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t{acct.payment_details.locale_uk.sort_code}\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t{acct.pots && acct.pots.length !== 0 && (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<hr className=\"dark:border-zinc-800\" />\n\n\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-1.5 pb-2.5 pt-1.5\">\n\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"px-2.5 font-bold text-zinc-700 dark:text-zinc-200\">Pots</p>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex w-full space-x-2.5 overflow-x-auto px-2.5\">\n\t\t\t\t\t\t\t\t\t\t\t\t{[...acct.pots]\n\t\t\t\t\t\t\t\t\t\t\t\t\t.filter(x => !x.deleted)\n\t\t\t\t\t\t\t\t\t\t\t\t\t.map(pot => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={pot.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"shrink-0 grow border px-3 py-2 dark:border-zinc-800\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-zinc-700 dark:text-zinc-200\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{pot.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{pot.round_up ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t({pot.round_up_multiplier}x)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-lg\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{format(pot.balance)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{typeof pot.goal_amount !== 'number' ? null : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-zinc-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/ {format(pot.goal_amount)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t{acct.webhooks && acct.webhooks.length !== 0 && (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<hr className=\"border-zinc-800\" />\n\n\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-2 py-2.5\">\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"px-2.5\">Webhooks</p>\n\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex w-full space-x-2.5 overflow-x-auto px-2.5\">\n\t\t\t\t\t\t\t\t\t\t\t\t{acct.webhooks.map(wehook => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={wehook.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"shrink-0 grow border border-zinc-800 px-3 py-2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-zinc-200\">{wehook.url}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-zinc-400\">{wehook.id}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t</div>\n\n\t\t\t<p className=\"text-sm text-zinc-500\">\n\t\t\t\tWebhooks are currently misbehaving due to an issue with Monzo's API\n\t\t\t</p>\n\t\t</div>\n\t);\n}\n\nconst redirect: {redirect: Redirect} = {\n\tredirect: {\n\t\tdestination: '/api/oauth/monzo/redirect',\n\t\tpermanent: false,\n\t},\n};\n\nexport const getServerSideProps: GetServerSideProps<Props> = async ({req}) => {\n\tconst {token} = req.cookies;\n\n\tif (typeof token !== 'string') {\n\t\treturn redirect;\n\t}\n\n\tconst session = parseSessionJWT(token);\n\n\tif (!session || !session.monzo_user_credentials) {\n\t\treturn redirect;\n\t}\n\n\tconst monzo = new MonzoAPI(session.monzo_user_credentials.access_token);\n\n\ttry {\n\t\tconst accounts = await monzo.getAccounts();\n\t\tconst accountsExpanded = await Promise.all(\n\t\t\taccounts.map(async account => {\n\t\t\t\tif (account.closed) {\n\t\t\t\t\treturn account;\n\t\t\t\t}\n\n\t\t\t\tconst [balance, webhooks, pots] = await Promise.all([\n\t\t\t\t\tmonzo.getBalance(account.id).catch(() => null),\n\t\t\t\t\tmonzo.listWebhooks(account.id).catch(() => null),\n\t\t\t\t\tmonzo.getPots(account.id).catch(() => null),\n\t\t\t\t]);\n\n\t\t\t\treturn {\n\t\t\t\t\t...account,\n\t\t\t\t\tpots,\n\t\t\t\t\tbalance,\n\t\t\t\t\twebhooks,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\n\t\treturn {\n\t\t\tprops: {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\taccounts: accountsExpanded,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t} catch (e) {\n\t\tif (!(e instanceof Error)) {\n\t\t\treturn {\n\t\t\t\tprops: {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: 'An unknown error occurred',\n\n\t\t\t\t\t// Do this just to make sure Next.js doesn't complain\n\t\t\t\t\t// about the body not being serializable.\n\t\t\t\t\tbody: JSON.parse(JSON.stringify(e)),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst body = await (e as HTTPClientError).response.json();\n\n\t\treturn {\n\t\t\tprops: {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: e.message,\n\t\t\t\tbody,\n\t\t\t},\n\t\t};\n\t}\n};\n"
  },
  {
    "path": "src/pages/stats.tsx",
    "content": "import dynamic from 'next/dynamic';\n\nconst Stats = dynamic(() => import('../components/stats').then(mod => mod.Stats), {\n\tssr: false,\n});\n\nexport default function StatsPage() {\n\treturn <Stats />;\n}\n"
  },
  {
    "path": "src/server/api.ts",
    "content": "import {get} from '@prequist/lanyard';\nimport {createAPI} from 'nextkit';\nimport {discordId} from '../utils/constants';\nimport {env} from './env';\n\nexport const api = createAPI({\n\tasync onError(_req, _res, error) {\n\t\tconsole.warn(error);\n\n\t\treturn {\n\t\t\tstatus: 500,\n\t\t\tmessage: error.message,\n\t\t};\n\t},\n\n\tasync getContext() {\n\t\treturn {\n\t\t\tlanyard: {\n\t\t\t\tget: () => get(discordId),\n\t\t\t},\n\n\t\t\tasync turnstile(token: string, ip: string | null) {\n\t\t\t\tconst formData = new URLSearchParams();\n\n\t\t\t\tformData.append('secret', env.TURNSTILE_SECRET_KEY);\n\t\t\t\tformData.append('response', token);\n\n\t\t\t\tif (ip) {\n\t\t\t\t\tformData.append('remoteip', ip);\n\t\t\t\t}\n\n\t\t\t\tconst url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n\n\t\t\t\tconst result = await fetch(url, {\n\t\t\t\t\tbody: formData,\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t});\n\n\t\t\t\treturn (await result.json()) as\n\t\t\t\t\t| {\n\t\t\t\t\t\t\t'success': true;\n\t\t\t\t\t\t\t'challenge_ts': string;\n\t\t\t\t\t\t\t'hostname': string;\n\t\t\t\t\t\t\t'error-codes': string[];\n\t\t\t\t\t\t\t'action': string;\n\t\t\t\t\t\t\t'cdata': string;\n\t\t\t\t\t  }\n\t\t\t\t\t| {\n\t\t\t\t\t\t\t'success': false;\n\t\t\t\t\t\t\t'error-codes': [string, ...string[]];\n\t\t\t\t\t  };\n\t\t\t},\n\t\t};\n\t},\n});\n"
  },
  {
    "path": "src/server/apple-maps.ts",
    "content": "import jwa from 'jwa';\nimport {env} from './env';\n\nconst es256 = jwa('ES256');\n\nexport function getMapURL(center: string, theme: 'light' | 'dark') {\n\tconst params = new URLSearchParams({\n\t\tcenter,\n\t\tteamId: env.APPLE_TEAM_ID,\n\t\tkeyId: env.APPLE_KEY_ID,\n\t\tz: '13',\n\t\tcolorScheme: theme,\n\t\tsize: '340x200',\n\t\tscale: '2',\n\t\tt: 'mutedStandard',\n\t\tpoi: '0',\n\t});\n\n\tconst completePath = `/api/v1/snapshot?${params.toString()}`;\n\n\tconst signature = es256.sign(completePath, env.APPLE_PRIV_KEY);\n\n\treturn `https://snapshot.apple-mapkit.com${completePath}&signature=${signature}`;\n}\n"
  },
  {
    "path": "src/server/env.ts",
    "content": "import {validateId, type Id} from '@otters/monzo';\nimport {z} from 'zod';\n\nexport const env = z\n\t.object({\n\t\tAPPLE_TEAM_ID: z.string(),\n\t\tAPPLE_KEY_ID: z.string(),\n\t\tAPPLE_PRIV_KEY: z.string(),\n\t\tDISCORD_WEBHOOK: z.string().url(),\n\t\tTURNSTILE_SECRET_KEY: z.string(),\n\t\tAPP_URL: z.string().default('http://localhost:3000').pipe(z.string().url()),\n\t\tDEFAULT_LOCATION: z.string().default('London'),\n\t\tMONZO_CLIENT_ID: z\n\t\t\t.string()\n\t\t\t.refine((id): id is Id<'oauth2client'> => validateId(id, 'oauth2client')),\n\t\tMONZO_CLIENT_SECRET: z.string(),\n\t\tJWT_SIGNING_SECRET: z.string(),\n\n\t\tDISCORD_DEMO_JWT_SECRET: z.string(),\n\t\tDISCORD_DEMO_REDIRECT_URI: z.string().url(),\n\t\tDISCORD_DEMO_DISCORD_CLIENT_ID: z.string(),\n\t\tDISCORD_DEMO_DISCORD_CLIENT_SECRET: z.string(),\n\t})\n\t.parse(process.env);\n"
  },
  {
    "path": "src/server/monzo.ts",
    "content": "import {MonzoOAuthAPI, type AppCredentials} from '@otters/monzo';\nimport {env} from './env';\n\nexport const monzoAppCredentials: AppCredentials = {\n\tclient_id: env.MONZO_CLIENT_ID,\n\tclient_secret: env.MONZO_CLIENT_SECRET,\n\tredirect_uri: `${env.APP_URL}/api/oauth/monzo/callback`,\n};\n\nexport const monzoOAuthAPI = new MonzoOAuthAPI(monzoAppCredentials);\n"
  },
  {
    "path": "src/server/sessions.ts",
    "content": "import {serialize} from 'cookie';\nimport dayjs from 'dayjs';\nimport {sign, verify} from 'jsonwebtoken';\nimport {env} from './env';\n\nexport interface SessionData {\n\tmonzo_user_credentials?:\n\t\t| {\n\t\t\t\taccess_token: string;\n\t\t\t\trefresh_token: string;\n\t\t\t\ttoken_type: string;\n\t\t  }\n\t\t| {\n\t\t\t\ttoken_type: string;\n\t\t\t\taccess_token: string;\n\t\t  };\n}\n\nexport function createSessionJWT(data: SessionData) {\n\treturn sign(data, env.JWT_SIGNING_SECRET, {\n\t\texpiresIn: '1d',\n\t});\n}\n\nexport function parseSessionJWT(token: string): SessionData | null {\n\ttry {\n\t\tconst result = verify(token, env.JWT_SIGNING_SECRET);\n\n\t\tif (typeof result === 'string') {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn result as SessionData;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getCookieHeader(token: string) {\n\treturn serialize('token', token, {\n\t\thttpOnly: true,\n\t\tpath: '/',\n\t\tsecure: process.env.NODE_ENV !== 'development',\n\t\texpires: dayjs().add(1, 'day').toDate(),\n\t\tsameSite: 'strict',\n\t});\n}\n"
  },
  {
    "path": "src/utils/constants.ts",
    "content": "import type {Types} from 'use-lanyard';\n\nexport const UKTimeFormatter = new Intl.DateTimeFormat(undefined, {\n\ttimeZone: 'Europe/London',\n\thour: 'numeric',\n\tminute: 'numeric',\n\thour12: false,\n});\n\nexport const RelativeTimeFormatter = new Intl.RelativeTimeFormat('en', {\n\tstyle: 'long',\n});\n\nexport const discordId: Types.Snowflake = '268798547439255572';\nexport const backupDiscordId: Types.Snowflake = '1448512517209981028';\n\nexport const dob = new Date('2004-11-02');\nexport const age = new Date(Date.now() - dob.getTime()).getUTCFullYear() - 1970;\nexport const hasHadBirthdayThisYear =\n\tnew Date().getMonth() >= dob.getMonth() && new Date().getDate() >= dob.getDate();\n\nexport const nextBirthdayYear = new Date().getFullYear() + (hasHadBirthdayThisYear ? 1 : 0);\nexport const daysUntilBirthday = RelativeTimeFormatter.formatToParts(\n\tMath.floor(\n\t\t(new Date(nextBirthdayYear, dob.getMonth(), dob.getDay() + 1).getTime() - Date.now()) /\n\t\t\t1000 /\n\t\t\t60 /\n\t\t\t60 /\n\t\t\t24,\n\t),\n\t'day',\n)[1]!.value.toString();\n"
  },
  {
    "path": "src/utils/discord.ts",
    "content": "export function codeblock(code: string, lang = 'ts') {\n\treturn `\\`\\`\\`${lang}\n${code}\n\\`\\`\\``;\n}\n"
  },
  {
    "path": "src/utils/lists.ts",
    "content": "import {bwitch} from 'bwitch';\n\nexport const SUPPORTS_INTL = typeof Intl !== 'undefined';\n\nexport function formatList(list: string[], type: Intl.ListFormatType): string {\n\treturn bwitch(SUPPORTS_INTL)\n\t\t.case(true, () => new Intl.ListFormat('en-US', {type}).format(list))\n\t\t.or(() => list.join(', '));\n}\n"
  },
  {
    "path": "src/utils/timers.ts",
    "content": "import type {NativeTimeout} from './types';\n\nexport function debounce<A extends unknown[] = []>(func: (...args: A) => void, ms: number) {\n\tlet timeout: NativeTimeout | null = null;\n\n\tconst debounced = (...args: A) => {\n\t\tif (timeout !== null) {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\n\t\ttimeout = setTimeout(() => func(...args), ms);\n\t};\n\n\treturn debounced;\n}\n"
  },
  {
    "path": "src/utils/types.ts",
    "content": "export type NativeTimeout = ReturnType<typeof setTimeout>;\nexport type DistributedOmit<T, K extends keyof T> = T extends T ? Omit<T, K> : never;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n\t\t\"allowJs\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"strict\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"noEmit\": true,\n\t\t\"incremental\": true,\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"jsx\": \"react-jsx\",\n\t\t\"verbatimModuleSyntax\": true,\n\t\t\"plugins\": [\n\t\t\t{\n\t\t\t\t\"name\": \"next\"\n\t\t\t}\n\t\t],\n\t\t\"types\": [],\n\t\t\"target\": \"ESNext\",\n\t\t\"useUnknownInCatchVariables\": true,\n\t\t\"noImplicitOverride\": true,\n\t\t\"noFallthroughCasesInSwitch\": true,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"exactOptionalPropertyTypes\": true,\n\t\t\"noImplicitReturns\": true,\n\t\t\"noUncheckedIndexedAccess\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"esModuleInterop\": true\n\t},\n\t\"include\": [\"next-env.d.ts\", \".next/types/**/*.ts\", \"**/*.ts\", \"**/*.tsx\"],\n\t\"exclude\": [\"node_modules\", \".next\"]\n}\n"
  }
]