From fe3d6a5d679a9c180cc7d94c6934d1e40a4520e1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Tue, 8 Sep 2026 23:45:28 +0000 Subject: [PATCH 1/7] refactor: make application configuration portable --- .env.example | 46 +- .../workflows/github-activity-backfill.yml | 11 +- .github/workflows/validate.yml | 3 + .worktreeinclude | 3 +- BLOG.md | 2 +- LICENSES/agent-browser-LICENSE.txt | 201 + LICENSES/geist-OFL.txt | 92 + LICENSES/ibm-plex-OFL.txt | 93 + LICENSES/jetbrains-mono-OFL.txt | 93 + LICENSES/literata-OFL.txt | 93 + LICENSES/source-sans-OFL.txt | 93 + README.md | 66 +- THIRD_PARTY.md | 25 + bun.lock | 88 +- bunfig.toml | 5 + docs/analytics.md | 2 +- docs/github-commits.md | 47 +- docs/open-source-readiness.md | 65 + drizzle/0022_portable_github_accounts.sql | 14 + drizzle/meta/0022_snapshot.json | 3871 +++++++++++++++++ drizzle/meta/_journal.json | 7 + next.config.ts | 5 + package.json | 88 +- scripts/backfill-github-activity.ts | 36 +- scripts/configure-supabase-cron.ts | 172 +- scripts/migrate-production-database.ts | 1 + src/app/(blog)/writing/page.tsx | 4 +- src/app/(portfolio)/journey/page.tsx | 8 +- src/app/(portfolio)/work/page.tsx | 7 +- src/app/opengraph-image.tsx | 3 +- src/app/sitemap.ts | 4 +- src/components/github-activity-days.tsx | 4 +- src/components/site-footer.tsx | 23 +- src/content/resume.ts | 74 +- src/content/site.ts | 11 + src/db/schema.ts | 14 +- src/env.ts | 7 +- src/instrumentation-client.ts | 15 +- src/lib/analytics.ts | 4 +- src/lib/blog-markdown.ts | 20 +- src/lib/blog-share-image.tsx | 5 +- src/lib/blog-utils.tsx | 7 +- src/lib/date.ts | 9 +- src/lib/github-accounts.ts | 60 + src/lib/github-activity-processor.ts | 55 +- src/lib/github-activity-store.ts | 9 + src/lib/github-activity-worker-store.ts | 31 +- src/lib/github-activity-worker.ts | 49 +- src/lib/github-api.ts | 4 +- src/lib/github-backfill-core.ts | 4 +- src/lib/github-commits-core.ts | 24 +- src/lib/github-commits.ts | 44 +- src/lib/github-profile.ts | 5 +- src/lib/github-ref-membership-store.ts | 6 +- src/lib/github-repository-inventory.ts | 6 +- src/lib/github-work-unit-projection-state.ts | 9 + src/lib/github-work-unit-store.ts | 16 +- src/lib/github-work-unit-summary-store.ts | 17 +- src/lib/remark-static-image-imports.mjs | 57 +- src/lib/resume.ts | 62 +- src/lib/site-url.ts | 54 +- src/lib/site.ts | 17 +- src/lib/structured-data.ts | 40 +- src/proxy.ts | 7 +- tests/blog-markdown.test.ts | 16 +- tests/github-accounts.test.ts | 99 + tests/github-activity-days.test.tsx | 3 +- tests/github-activity-feed-postgres.test.ts | 17 +- tests/github-activity-processor.test.ts | 89 +- tests/github-activity-worker.test.ts | 15 +- tests/github-backfill.test.ts | 10 +- tests/github-commits.test.ts | 4 +- tests/github-diff.test.ts | 8 +- tests/github-pull-request-store.test.ts | 91 +- tests/github-work-unit-store-postgres.test.ts | 105 +- tests/production-database-migration.test.ts | 53 +- tests/setup.ts | 4 + tests/site-seo.test.ts | 70 +- tests/site-url.test.ts | 33 + 79 files changed, 6020 insertions(+), 614 deletions(-) create mode 100644 LICENSES/agent-browser-LICENSE.txt create mode 100644 LICENSES/geist-OFL.txt create mode 100644 LICENSES/ibm-plex-OFL.txt create mode 100644 LICENSES/jetbrains-mono-OFL.txt create mode 100644 LICENSES/literata-OFL.txt create mode 100644 LICENSES/source-sans-OFL.txt create mode 100644 THIRD_PARTY.md create mode 100644 bunfig.toml create mode 100644 docs/open-source-readiness.md create mode 100644 drizzle/0022_portable_github_accounts.sql create mode 100644 drizzle/meta/0022_snapshot.json create mode 100644 src/content/site.ts create mode 100644 src/lib/github-accounts.ts create mode 100644 tests/github-accounts.test.ts create mode 100644 tests/setup.ts create mode 100644 tests/site-url.test.ts diff --git a/.env.example b/.env.example index 8f30a62e..b35825e5 100644 --- a/.env.example +++ b/.env.example @@ -1,31 +1,35 @@ -# Copy this file to .env.local for local work. Platform-provided variables and -# compatibility aliases are intentionally omitted. - -# Supabase transaction-pooler URL at runtime and a direct or session-pooler URL -# for migrations and cron configuration. +# Copy to .env.local. All values are intentionally blank; basic dev/build needs none. +# Database: runtime pooler URL, and optional direct/session URL for operations. DATABASE_URL= DATABASE_URL_UNPOOLED= -# Optional fallback for public repository discovery and pinned GitHub embeds. +# GitHub activity: JSON object mapping configured logins to personal access tokens. +# Public authors and IDs live in src/content/site.ts. Keep tokens only in this environment. +GITHUB_TOKENS= +# Optional public repository/embed access; GH_TOKEN is the GitHub CLI alias. GITHUB_TOKEN= +GH_TOKEN= -# Account-specific tokens used to read each authenticated user event feed and -# repository commits, including private repositories the account can access. -GITHUB_F0RR0_TOKEN= -GITHUB_YUPPIESTECHDEV_TOKEN= - -# Authenticates Supabase Cron requests. -# Use at least 32 random characters. +# Independent secrets, each at least 32 random characters. CRON_SECRET= - -# Signs activity pagination cursors independently of cron authentication. -# Use at least 32 random characters. GITHUB_ACTIVITY_CURSOR_SECRET= - -# Verifies GitHub webhook signatures. Use at least 32 random characters and -# configure the same value on the webhook sender. GITHUB_WEBHOOK_SECRET= -# Optional. Generates outcome summaries for public work units. Missing -# credentials, exhausted budgets, and provider failures remain facts-only. +# Optional outcome summaries (activity works without this). OPENAI_API_KEY= +# Optional public PostHog capture key and region (us or eu; defaults to us). +NEXT_PUBLIC_POSTHOG_KEY= +NEXT_PUBLIC_POSTHOG_REGION= + +# Optional local server port and public fallback port (default 3000). +PORT= +NEXT_PUBLIC_PORT= + +# Supplied by Next/Vercel. Leave blank locally; do not manage a separate site URL. +NODE_ENV= +VERCEL= +VERCEL_ENV= +VERCEL_PROJECT_PRODUCTION_URL= +VERCEL_URL= +# NEXT_PUBLIC_SITE_ORIGIN and NEXT_PUBLIC_DEPLOYMENT_ENV are derived in next.config.ts. +# Codex account credentials belong in Supabase Vault; see docs/codex-stats.md. diff --git a/.github/workflows/github-activity-backfill.yml b/.github/workflows/github-activity-backfill.yml index 7fb92dff..0df271d3 100644 --- a/.github/workflows/github-activity-backfill.yml +++ b/.github/workflows/github-activity-backfill.yml @@ -14,12 +14,8 @@ on: account: description: Account whose token and accessible repositories are scanned required: true - default: f0rr0 - type: choice - options: - - f0rr0 - - yuppiestechdev - - all + default: all + type: string repository_id: description: Numeric repository ID; blank scans repositories active in the window required: false @@ -57,8 +53,7 @@ jobs: ACCOUNT: ${{ inputs.account }} DATABASE_URL: ${{ secrets.ACTIVITY_DATABASE_URL }} END_DATE: ${{ inputs.end_date }} - GITHUB_F0RR0_TOKEN: ${{ secrets.ACTIVITY_F0RR0_TOKEN }} - GITHUB_YUPPIESTECHDEV_TOKEN: ${{ secrets.ACTIVITY_YUPPIESTECHDEV_TOKEN }} + GITHUB_TOKENS: ${{ secrets.GITHUB_TOKENS }} MAXIMUM_MINUTES: ${{ inputs.maximum_minutes }} REPOSITORY_ID: ${{ inputs.repository_id }} START_DATE: ${{ inputs.start_date }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 12bc5a36..40925d36 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,6 +3,9 @@ name: validate on: pull_request: +permissions: + contents: read + jobs: validate: name: validate diff --git a/.worktreeinclude b/.worktreeinclude index 728495dd..e04c51c8 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -1,2 +1,3 @@ -# Copy the complete local runtime configuration into Codex-managed worktrees. +# Copy development-only local configuration into managed worktrees. +# Keep production credentials in deployment secret storage, not .env.local. .env.local diff --git a/BLOG.md b/BLOG.md index a28dd3c3..dcdc5611 100644 --- a/BLOG.md +++ b/BLOG.md @@ -11,7 +11,7 @@ prompts, and desktop/mobile/social crops. Keep research reports, per-post image prompts and production briefs transient; do not commit them to the repository. Reserve `docs/` for current implementation and usage documentation. -Public identity URLs always use `https://f0rr0.dev`; preview deployments return +Public identity URLs use Vercel’s `VERCEL_PROJECT_PRODUCTION_URL`; preview deployments return `noindex`. The sitemap lists intended search destinations, while alternate Markdown and profile exports remain discoverable through links and `llms.txt`. diff --git a/LICENSES/agent-browser-LICENSE.txt b/LICENSES/agent-browser-LICENSE.txt new file mode 100644 index 00000000..8226d363 --- /dev/null +++ b/LICENSES/agent-browser-LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 Vercel Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/geist-OFL.txt b/LICENSES/geist-OFL.txt new file mode 100644 index 00000000..8d003fec --- /dev/null +++ b/LICENSES/geist-OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2023 Vercel, in collaboration with basement.studio + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/ibm-plex-OFL.txt b/LICENSES/ibm-plex-OFL.txt new file mode 100644 index 00000000..c35c4c61 --- /dev/null +++ b/LICENSES/ibm-plex-OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/jetbrains-mono-OFL.txt b/LICENSES/jetbrains-mono-OFL.txt new file mode 100644 index 00000000..5ceee002 --- /dev/null +++ b/LICENSES/jetbrains-mono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/literata-OFL.txt b/LICENSES/literata-OFL.txt new file mode 100644 index 00000000..9d3b4e3b --- /dev/null +++ b/LICENSES/literata-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/source-sans-OFL.txt b/LICENSES/source-sans-OFL.txt new file mode 100644 index 00000000..22c601b8 --- /dev/null +++ b/LICENSES/source-sans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2010-2024 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/README.md b/README.md index 11b3db0a..25b56055 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,68 @@ bun test bun run build ``` -Vercel production builds apply pending migrations before building the site, -using the database connection already synchronized by Supabase. Preview and -local builds skip migrations. Apply them manually in other environments with: +`bun run build` only builds the application. To use the existing production +migrations and Supabase scheduling, set Vercel's **Build Command** to: ```sh -bun run db:migrate +bun scripts/migrate-production-database.ts && bun run build && bun scripts/configure-supabase-cron.ts --production-build ``` + +Those operational scripts act only on Vercel production deployments. The +migration runs before the build because pages may read the database while +building. Cron configuration follows a successful build; it is not a +post-deployment hook. Local operations remain `bun run db:migrate` and +`bun run supabase:cron`. + +## Customize once + +- `src/content/resume.ts`: identity, social profiles, career, education and PDF paths. +- `src/content/home.ts`: introduction and featured work. +- `src/content/site.ts`: tracked GitHub authors, language and work-log timezone. +- `.env.example`: every supported environment variable, with blank values. + +Vercel's `VERCEL_PROJECT_PRODUCTION_URL` supplies the canonical domain, including +for preview metadata. Enable **Automatically expose System Environment Variables** +in the project settings. Local URLs use localhost and the configured port. +There is no separately maintained site URL or blog asset base URL: exported +Markdown uses the image URLs already emitted by the MDX compiler. + +GitHub activity uses the public author list in `src/content/site.ts` and a separate +`GITHUB_TOKENS` JSON object, such as `{"alice":"","bob":""}`. +Each key references a configured author; credentials grant access and never select +which authors appear. Rotation, expiration or removal preserves stored history. +Polling and repository inventory verify the assigned token against the configured +GitHub identity. Webhooks, summaries and database-only processing need no GitHub +token. See the service guides before enabling activity or Codex stats. PostHog is +disabled unless a capture key is supplied and runs only on the canonical production host. + +Each installation needs its own database. Cron/Vault names are installation-wide; +sharing one database between independent sites is unsupported. Set `vercel.json` +regions to match your database location (the existing deployment uses Tokyo). +Personal content and the authoring defaults under `.rulesync/` can remain or be +edited independently of the application configuration. + +`.worktreeinclude` automatically copies `.env.local` into local worktrees. Use +only development-scoped credentials there; keep production credentials in the +deployment secret store. The copied file remains ignored by Git. + +Docker is required for PostgreSQL integration tests; Bun reports those tests as +skipped when Docker is unavailable. Typst tooling is needed to regenerate the +résumé PDF. Versions are pinned in `mise.toml`, `package.json` and `bun.lock`; +CI installs with the frozen lockfile. + +GitHub native secret scanning and push protection are enabled for this repository. +Enable those repository settings when creating a fork. The historical Firebase +project configuration still needs an owner-side restrictions/retirement review. +Report credential exposure privately through GitHub's security reporting feature +when enabled. Otherwise, use the maintainer contact in `src/content/resume.ts`; +never paste credentials into a public issue. + +The [audit checklist](docs/open-source-readiness.md) records completed work and +remaining owner decisions. + +## Reuse status + +A code license and the reuse policy for personal writing/images still need to be +chosen. Public source availability alone is not a grant of reuse rights. Preserve +upstream copyright/license notices for vendored fonts, assets and authoring skills. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 00000000..ac18ecb4 --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,25 @@ +# Third-party material + +These notices cover bundled material separately from the application code and +personal content. Original embedded notices remain in the font files. + +| Material | Upstream | Bundled license | +| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------ | +| IBM Plex Sans (`career/typst/fonts/IBMPlexSans-*`) | [IBM Plex](https://github.com/IBM/plex) | [SIL OFL 1.1](LICENSES/ibm-plex-OFL.txt) | +| Source Sans 3 (`career/typst/fonts/SourceSans3-*`, `src/app/fonts/SourceSans3-Latin.woff2`) | [Adobe Source Sans](https://github.com/adobe-fonts/source-sans) | [SIL OFL 1.1](LICENSES/source-sans-OFL.txt) | +| Literata (`career/typst/fonts/Literata.ttf`, `src/app/fonts/Literata-Latin.woff2`) | [Literata](https://github.com/googlefonts/literata) | [SIL OFL 1.1](LICENSES/literata-OFL.txt) | +| Geist (`src/app/fonts/Geist-Latin.woff2`) | [Vercel Geist](https://github.com/vercel/geist-font) | [SIL OFL 1.1](LICENSES/geist-OFL.txt) | +| JetBrains Mono (`src/app/fonts/JetBrainsMono-Latin.woff2`) | [JetBrains Mono](https://github.com/JetBrains/JetBrainsMono) | [SIL OFL 1.1](LICENSES/jetbrains-mono-OFL.txt) | +| `.rulesync/skills/agent-browser/` | [agent-browser](https://github.com/vercel-labs/agent-browser) | [Apache 2.0](LICENSES/agent-browser-LICENSE.txt) | + +License texts were retrieved from the linked upstream projects on September 8, 2026. The IBM and Adobe fonts also retain their version-specific embedded +copyright notices. Do not remove those notices when regenerating or subsetting. + +The vendored Vercel React and composition skills declare MIT in their own +frontmatter. Their exact upstream revision and complete copyright/license +notices, and the provenance of the remaining vendored skills, still need to be +verified before making a blanket redistribution claim for `.rulesync/`. + +Personal writing, photographs, résumé content and third-party company logos are +not relicensed by these notices. Company logos retain their owners' trademarks. +Package dependencies retain their own licenses in their distributions. diff --git a/bun.lock b/bun.lock index 853addde..f817bb4f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,61 +6,61 @@ "name": "f0rr0.dev", "dependencies": { "@ai-sdk/openai": "4.0.41", - "@base-ui/react": "^1.6.0", - "@mdx-js/loader": "^3.1.1", - "@mdx-js/react": "^3.1.1", + "@base-ui/react": "1.6.0", + "@mdx-js/loader": "3.1.1", + "@mdx-js/react": "3.1.1", "@mermaid-js/layout-elk": "0.2.3", - "@next/mdx": "^16.3.4", - "@remark-embedder/core": "^3.0.3", - "@t3-oss/env-nextjs": "^0.13.11", - "@tailwindcss/typography": "^0.5.20", - "@tanstack/react-query": "^5.102.8", + "@next/mdx": "16.3.4", + "@remark-embedder/core": "3.0.3", + "@t3-oss/env-nextjs": "0.13.11", + "@tailwindcss/typography": "0.5.20", + "@tanstack/react-query": "5.102.8", "ai": "7.0.63", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "drizzle-orm": "0.45.2", - "feed": "^5.2.1", + "feed": "5.2.1", "gpt-tokenizer": "4.0.0", - "lucide-react": "^1.21.0", - "mermaid": "^11.17.2", - "motion": "^13.2.0", - "next": "^16.3.4", - "next-themes": "^0.4.6", + "lucide-react": "1.21.0", + "mermaid": "11.17.2", + "motion": "13.2.0", + "next": "16.3.4", + "next-themes": "0.4.6", "postgres": "3.4.9", "posthog-js": "1.428.7", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "reading-time": "^1.5.0", - "rehype-autolink-headings": "^7.1.0", - "rehype-pretty-code": "^0.14.5", - "rehype-slug": "^6.0.0", - "remark-gfm": "^4.0.1", + "react": "19.2.8", + "react-dom": "19.2.8", + "reading-time": "1.5.0", + "rehype-autolink-headings": "7.1.0", + "rehype-pretty-code": "0.14.5", + "rehype-slug": "6.0.0", + "remark-gfm": "4.0.1", "server-only": "0.0.1", - "shadcn": "^4.11.0", - "shiki": "^4.4.3", - "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", - "unist-util-visit": "^5.1.0", - "zod": "^4.4.3", + "shadcn": "4.11.0", + "shiki": "4.4.3", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "unist-util-visit": "5.1.0", + "zod": "4.4.3", }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.1", - "@types/bun": "^1.4.1", - "@types/mdx": "^2.0.14", + "@tailwindcss/postcss": "4.3.1", + "@types/bun": "1.4.1", + "@types/mdx": "2.0.14", "@types/node": "24.13.3", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260624.1", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "7.0.0-dev.20260624.1", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "oxfmt": "^0.56.0", - "oxlint": "^1.71.0", - "oxlint-tsgolint": "^0.23.0", - "schema-dts": "^2.0.0", - "tailwindcss": "^4.3.1", - "tw-animate-css": "^1.4.0", - "typescript": "^7.0.1-rc", - "ultracite": "^7.8.3", + "oxfmt": "0.56.0", + "oxlint": "1.71.0", + "oxlint-tsgolint": "0.23.0", + "schema-dts": "2.0.0", + "tailwindcss": "4.3.1", + "tw-animate-css": "1.4.0", + "typescript": "7.0.1-rc", + "ultracite": "7.8.3", }, }, }, @@ -305,8 +305,6 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], - "@neondatabase/serverless": ["@neondatabase/serverless@1.1.0", "", {}, "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q=="], - "@next/env": ["@next/env@16.3.4", "", {}, "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q=="], "@next/mdx": ["@next/mdx@16.3.4", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-XEmW3ccWWNybofVOmgIEVhXtmNXM0u423iqkVXwPf29fht/tdpUiMDuMHQ4hMerE1477nOJrv5kM/F6qMcEMwA=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..965d3188 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,5 @@ +[install] +exact = true + +[test] +preload = ["./tests/setup.ts"] diff --git a/docs/analytics.md b/docs/analytics.md index 650690b6..e88601e3 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -4,7 +4,7 @@ The site uses PostHog US Cloud for pageviews, acquisition attribution, and expli ## Configuration -- `src/instrumentation-client.ts` initializes the SDK only in production on `f0rr0.dev`. The public project token and API path are configured in source; no PostHog environment variables are needed. +- `src/instrumentation-client.ts` initializes the SDK only on the canonical production host when `NEXT_PUBLIC_POSTHOG_KEY` is configured. `NEXT_PUBLIC_POSTHOG_REGION` selects `us` (default) or `eu` for both the SDK and proxy; preview and local deployments do not capture events. - `src/proxy.ts` forwards `/_r7k2/*` to fixed US ingestion and asset hosts. It strips Cookie, Authorization, and Referer headers. Collector trailing slashes are preserved; ordinary page trailing slashes receive a 308 redirect. - The SDK uses always-cookieless mode, memory persistence, and no person profiles. Autocapture, replay, surveys, heatmaps, automatic exceptions, performance collection, and feature flags are disabled. - Do Not Track and Global Privacy Control signals do not change capture behavior. diff --git a/docs/github-commits.md b/docs/github-commits.md index 0d0e1076..f60f9737 100644 --- a/docs/github-commits.md +++ b/docs/github-commits.md @@ -90,12 +90,11 @@ after activation even if an older worker cleared a deployment-time token. ## Runtime configuration -Required server-side values are: +GitHub activity is optional. Its server-side configuration is: ```dotenv DATABASE_URL=postgresql://... -GITHUB_F0RR0_TOKEN=github_pat_... -GITHUB_YUPPIESTECHDEV_TOKEN=github_pat_... +GITHUB_TOKENS={"alice":""} GITHUB_WEBHOOK_SECRET= CRON_SECRET= GITHUB_ACTIVITY_CURSOR_SECRET= @@ -104,9 +103,49 @@ GITHUB_ACTIVITY_CURSOR_SECRET= `OPENAI_API_KEY` is optional. Without it, factual work units continue to publish and summary claims remain untouched. `DATABASE_URL_UNPOOLED` is the optional direct/session-pooler override used by migrations and Supabase Cron -configuration. `GITHUB_TOKEN` is an optional additional read token. Secrets and +configuration. `GITHUB_TOKEN` (or `GH_TOKEN`) is optional for public discovery and code embeds. Secrets and private evidence stay server-side. +Configure tracked authors once in `src/content/site.ts` as `{ login, id }` records. +The first account supplies the primary public GitHub profile. IDs are GitHub's +permanent numeric user IDs (stored as strings). GitHub social URLs and author +lookups derive from these records. + +`GITHUB_TOKENS` is a JSON object mapping those logins to personal access tokens. +Blank input or `{}` means no account credentials. Keys are case-insensitive; +unknown accounts, case-colliding keys and empty token values are rejected without +logging tokens. Each account can have one token; omit its key to disable its +account-specific polling and inventory. The existing `/user` check verifies the +configured login and ID before acquisition. GitHub App installation tokens cannot +substitute for a personal account identity in these jobs. + +Repository reads try the account's token first, then other configured credentials. +Changing tokens never changes the selected authors, rewrites stored identities or +prunes published history. Missing credentials and lost access defer fetching. +Intentional changes to the public author list request a work-unit rebuild and +filter issue visibility; raw evidence is retained. Username changes require an +explicit configuration/checkpoint maintenance operation, not automatic database +renaming. Keep the numeric ID unchanged when an existing account is renamed. + +The forward migration only replaces the seven personal account-name constraints +with generic login shape checks. It adds no identity columns and preserves +existing checkpoints. Applied migrations remain unchanged. + +Webhooks require their own `GITHUB_WEBHOOK_SECRET`; removing a token does not +revoke a webhook. Summaries require only their own `OPENAI_API_KEY` and stored facts. +Cron setup schedules polling and refs when tokens are configured; the existing +worker remains scheduled for database-only publication, and summaries are scheduled +when their provider key is configured. Codex scheduling follows enabled database +accounts. Rerun cron setup after enabling or disabling a service. Local cron setup +requires the Vercel production hostname in `VERCEL_PROJECT_PRODUCTION_URL`. +The manual backfill Action uses repository secrets `ACTIVITY_DATABASE_URL` and +`GITHUB_TOKENS`; its account input defaults to all configured authors. Every +selected backfill author needs a credential; use `--account` to select a subset. + +Public output includes private-activity counts, timestamps and line/file facts, +with repository names masked; private repository IDs/avatar URLs can also appear. +Enable ingestion only if that publication policy fits the installation. + Routine entry points are: - `POST /api/github/webhook` diff --git a/docs/open-source-readiness.md b/docs/open-source-readiness.md new file mode 100644 index 00000000..c61d7027 --- /dev/null +++ b/docs/open-source-readiness.md @@ -0,0 +1,65 @@ +# Open-source readiness checklist + +Rechecked September 8, 2026 against default branch `next` at `fdc15f9` and the +changes in this pull request. Personal writing, photographs, résumé, journey, +project content and authoring preferences remain intentionally in the repository. + +**20 of 23 findings are addressed.** One remains an owner-side review, one is +partially addressed, and the code license is deliberately undecided. This is a +configuration-portability cleanup, not a claim that every security or licensing +question is settled. + +| Audit finding | Status | Resolution or remaining work | +| --------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1. Historical Firebase configuration | Open | Owner must review current key restrictions and database/storage rules, or retire the old project. Copies remain in historical commits and old branches. No credential validity probes or history rewrites were performed. | +| 2. Cron targeting the original deployment | Addressed | Requires the Vercel production hostname; missing/invalid targets fail before writes. | +| 3. Hardcoded GitHub identities | Addressed | Public author records in `src/content/site.ts`; immutable-ID checks retained. | +| 4. Seven personal database constraints | Addressed | Forward migration replaces personal allowlists with login-shape checks, preserving existing checkpoints. | +| 5. Duplicated, two-account token selection | Addressed | One validated `GITHUB_TOKENS` object and shared lookup/fallback helpers. Credentials do not control author visibility. | +| 6. Personal backfill workflow inputs/secrets | Addressed | Generic account input and token object; configured accounts are validated. | +| 7. Build requires/mutates infrastructure | Addressed | `build` only builds; existing operational scripts compose through Vercel's Build Command when wanted. | +| 8. Dedicated-database assumption | Addressed | Documented one database per installation; existing lock/job names retained for compatibility. | +| 9. Fixed canonical domain | Addressed | Vercel supplies the production hostname; previews retain canonical production identity. | +| 10. Duplicated page/social-card identity | Addressed | Consumers derive names, images and domain text from shared configuration. | +| 11. Separate machine-readable biography | Addressed | JSON résumé and structured data use shared person, social and education records. | +| 12. Hardcoded footer/PDF links | Addressed | Consumers reuse configured social links and PDF output path. | +| 13. Markdown images tied to one GitHub branch | Addressed | Exports reuse the MDX compiler's deployed image URLs. | +| 14. Fixed public GitHub discovery account | Addressed | Uses the configured primary GitHub profile. | +| 15. Fixed PostHog project/region | Addressed | Optional capture key and shared US/EU region; production-host guard retained. | +| 16. Automatic copying of runtime credentials | Addressed | README and `.worktreeinclude` explicitly require development-only local credentials; production credentials belong in deployment storage. | +| 17. Optional services/publication boundaries | Addressed | Setup/publication documented; polling, summaries and Codex scheduling follow their own prerequisites. Database-only publication remains available without GitHub tokens. | +| 18. Timezone/region preferences | Addressed | Shared timezone with derived label; deployment region/database relationship documented. | +| 19. Application code license | Deferred by choice | User explicitly left the license undecided. This PR does not introduce one. | +| 20. Vendored licenses/provenance | Partial | Font and agent-browser notices bundled; remaining skill origins/revisions and complete notices still need verification. See `THIRD_PARTY.md`. | +| 21. Secret-scanning prevention | Addressed | GitHub native secret scanning and push protection verified enabled. Additional Gitleaks CI removed as requested; forks must enable their own protection settings. | +| 22. Configurability regression coverage | Addressed | Alternate profile/domain/account tests and disposable PostgreSQL upgrade/history tests; Docker prerequisite documented. | +| 23. Contributor configuration guidance | Addressed | README customization map, blank environment template, deployment instructions and private-reporting guidance. | + +Direct dependencies are also pinned to the existing resolved versions. Bun saves +exact versions and CI uses the frozen lockfile; no package upgrades were bundled. + +## Validation + +The application changes passed 324 tests across 43 files, including real disposable +PostgreSQL databases; the added alternate-profile regression also passes. Lint, +full/deployment-source typechecks and a Vercel-style build with no service credentials +passed. The build reports nonfatal unauthenticated GitHub embed 403s and existing +filesystem-tracing warnings. + +Coverage includes token rotation/removal, zero-token polling/publication, retained +commit-to-PR evidence after reduced visibility, configured-author issue filtering, +and cache/cursor invalidation after author-policy changes. These checks do not +verify live Firebase rules, production credentials or every vendored file's origin. + +## Before deployment + +- Set `GITHUB_TOKENS` to the documented login-to-token object in Vercel and the + backfill Action. Remove obsolete personal token variable names after switching. +- Set optional analytics configuration if keeping analytics enabled. +- For the existing database-backed deployment, apply the README's Vercel Build + Command so migrations run before the build and cron setup follows it. +- Resolve the remaining Firebase review and vendored provenance separately. The + code-license decision remains deferred. + +Production database migrations and Vercel/cron configuration have not been applied +by this change. Applied historical migrations and personal content remain intact. diff --git a/drizzle/0022_portable_github_accounts.sql b/drizzle/0022_portable_github_accounts.sql new file mode 100644 index 00000000..a51d327d --- /dev/null +++ b/drizzle/0022_portable_github_accounts.sql @@ -0,0 +1,14 @@ +ALTER TABLE "github_account_checkpoints" DROP CONSTRAINT "github_account_checkpoints_tracked_account";--> statement-breakpoint +ALTER TABLE "github_commits" DROP CONSTRAINT "github_commits_tracked_author";--> statement-breakpoint +ALTER TABLE "github_issues" DROP CONSTRAINT "github_issues_tracked_account";--> statement-breakpoint +ALTER TABLE "github_pull_request_signals" DROP CONSTRAINT "github_pull_request_signals_account";--> statement-breakpoint +ALTER TABLE "github_pull_requests" DROP CONSTRAINT "github_pull_requests_tracked_account";--> statement-breakpoint +ALTER TABLE "github_push_observations" DROP CONSTRAINT "github_push_observations_tracked_account";--> statement-breakpoint +ALTER TABLE "github_webhook_deliveries" DROP CONSTRAINT "github_webhook_deliveries_tracked_account";--> statement-breakpoint +ALTER TABLE "github_account_checkpoints" ADD CONSTRAINT "github_account_checkpoints_tracked_account" CHECK ("github_account_checkpoints"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_commits" ADD CONSTRAINT "github_commits_tracked_author" CHECK ("github_commits"."author_login" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_issues" ADD CONSTRAINT "github_issues_tracked_account" CHECK ("github_issues"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_pull_request_signals" ADD CONSTRAINT "github_pull_request_signals_account" CHECK ("github_pull_request_signals"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_pull_requests" ADD CONSTRAINT "github_pull_requests_tracked_account" CHECK ("github_pull_requests"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_push_observations" ADD CONSTRAINT "github_push_observations_tracked_account" CHECK ("github_push_observations"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_webhook_deliveries" ADD CONSTRAINT "github_webhook_deliveries_tracked_account" CHECK ("github_webhook_deliveries"."account" IS NULL OR "github_webhook_deliveries"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'); diff --git a/drizzle/meta/0022_snapshot.json b/drizzle/meta/0022_snapshot.json new file mode 100644 index 00000000..3f40985f --- /dev/null +++ b/drizzle/meta/0022_snapshot.json @@ -0,0 +1,3871 @@ +{ + "id": "9948f913-1fd3-4315-a57d-c9f1bbc15e7f", + "prevId": "6070194c-d84b-488f-9507-6aa51ce8bb1f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.codex_accounts": { + "name": "codex_accounts", + "schema": "", + "columns": { + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "codex_accounts_id_shape": { + "name": "codex_accounts_id_shape", + "value": "\"codex_accounts\".\"id\" ~ '^[a-z0-9][a-z0-9_-]{0,63}$'" + }, + "codex_accounts_snapshot_pair": { + "name": "codex_accounts_snapshot_pair", + "value": "(\"codex_accounts\".\"snapshot\" IS NULL) = (\"codex_accounts\".\"snapshot_at\" IS NULL)" + }, + "codex_accounts_snapshot_object": { + "name": "codex_accounts_snapshot_object", + "value": "\"codex_accounts\".\"snapshot\" IS NULL OR jsonb_typeof(\"codex_accounts\".\"snapshot\") = 'object'" + } + }, + "isRLSEnabled": true + }, + "public.github_account_checkpoints": { + "name": "github_account_checkpoints", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": true, + "notNull": true + }, + "events_etag": { + "name": "events_etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "events_last_attempted_at": { + "name": "events_last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "events_last_succeeded_at": { + "name": "events_last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "events_next_poll_at": { + "name": "events_next_poll_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "gap_detected_at": { + "name": "gap_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "gap_expected_event_id": { + "name": "gap_expected_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "gap_oldest_available_event_id": { + "name": "gap_oldest_available_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "gap_state": { + "name": "gap_state", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "default": "'clear'" + }, + "latest_event_id": { + "name": "latest_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "paused": { + "name": "paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pull_request_backfill_digest": { + "name": "pull_request_backfill_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "head_ref_cursor_repository_id": { + "name": "head_ref_cursor_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "head_ref_cycle_started_at": { + "name": "head_ref_cycle_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_last_attempted_at": { + "name": "head_ref_last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_last_succeeded_at": { + "name": "head_ref_last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_lease_token": { + "name": "head_ref_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "head_ref_lease_until": { + "name": "head_ref_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_next_page": { + "name": "head_ref_next_page", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "head_ref_scan_started_at": { + "name": "head_ref_scan_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ref_backfill_since_at": { + "name": "ref_backfill_since_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tag_ref_cursor_repository_id": { + "name": "tag_ref_cursor_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "tag_ref_cycle_started_at": { + "name": "tag_ref_cycle_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_last_attempted_at": { + "name": "tag_ref_last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_last_succeeded_at": { + "name": "tag_ref_last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_lease_token": { + "name": "tag_ref_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tag_ref_lease_until": { + "name": "tag_ref_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_next_page": { + "name": "tag_ref_next_page", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tag_ref_scan_started_at": { + "name": "tag_ref_scan_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_account_checkpoints_tracked_account": { + "name": "github_account_checkpoints_tracked_account", + "value": "\"github_account_checkpoints\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_account_checkpoints_event_id_shape": { + "name": "github_account_checkpoints_event_id_shape", + "value": "(\"github_account_checkpoints\".\"latest_event_id\" IS NULL OR \"github_account_checkpoints\".\"latest_event_id\" ~ '^[0-9]{1,64}$') AND (\"github_account_checkpoints\".\"gap_expected_event_id\" IS NULL OR \"github_account_checkpoints\".\"gap_expected_event_id\" ~ '^[0-9]{1,64}$') AND (\"github_account_checkpoints\".\"gap_oldest_available_event_id\" IS NULL OR \"github_account_checkpoints\".\"gap_oldest_available_event_id\" ~ '^[0-9]{1,64}$')" + }, + "github_account_checkpoints_gap_state": { + "name": "github_account_checkpoints_gap_state", + "value": "\"github_account_checkpoints\".\"gap_state\" IN ('clear', 'detected')" + }, + "github_account_checkpoints_gap_details": { + "name": "github_account_checkpoints_gap_details", + "value": "(\"github_account_checkpoints\".\"gap_state\" = 'detected' AND \"github_account_checkpoints\".\"gap_detected_at\" IS NOT NULL) OR (\"github_account_checkpoints\".\"gap_state\" = 'clear' AND \"github_account_checkpoints\".\"gap_detected_at\" IS NULL AND \"github_account_checkpoints\".\"gap_expected_event_id\" IS NULL AND \"github_account_checkpoints\".\"gap_oldest_available_event_id\" IS NULL)" + }, + "github_account_checkpoints_ref_cursor_shape": { + "name": "github_account_checkpoints_ref_cursor_shape", + "value": "(\"github_account_checkpoints\".\"head_ref_cursor_repository_id\" IS NULL OR \"github_account_checkpoints\".\"head_ref_cursor_repository_id\" ~ '^[0-9]{1,32}$') AND (\"github_account_checkpoints\".\"tag_ref_cursor_repository_id\" IS NULL OR \"github_account_checkpoints\".\"tag_ref_cursor_repository_id\" ~ '^[0-9]{1,32}$')" + }, + "github_account_checkpoints_ref_leases": { + "name": "github_account_checkpoints_ref_leases", + "value": "(\"github_account_checkpoints\".\"head_ref_lease_token\" IS NULL) = (\"github_account_checkpoints\".\"head_ref_lease_until\" IS NULL) AND (\"github_account_checkpoints\".\"tag_ref_lease_token\" IS NULL) = (\"github_account_checkpoints\".\"tag_ref_lease_until\" IS NULL)" + }, + "github_account_checkpoints_ref_scans": { + "name": "github_account_checkpoints_ref_scans", + "value": "(\"github_account_checkpoints\".\"head_ref_next_page\" IS NULL AND \"github_account_checkpoints\".\"head_ref_scan_started_at\" IS NULL OR \"github_account_checkpoints\".\"head_ref_next_page\" >= 2 AND \"github_account_checkpoints\".\"head_ref_scan_started_at\" IS NOT NULL) AND (\"github_account_checkpoints\".\"tag_ref_next_page\" IS NULL AND \"github_account_checkpoints\".\"tag_ref_scan_started_at\" IS NULL OR \"github_account_checkpoints\".\"tag_ref_next_page\" >= 2 AND \"github_account_checkpoints\".\"tag_ref_scan_started_at\" IS NOT NULL)" + }, + "github_account_checkpoints_pr_backfill_digest": { + "name": "github_account_checkpoints_pr_backfill_digest", + "value": "\"github_account_checkpoints\".\"pull_request_backfill_digest\" IS NULL OR \"github_account_checkpoints\".\"pull_request_backfill_digest\" ~ '^[a-f0-9]{64}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_account_repository_catalogs": { + "name": "github_account_repository_catalogs", + "schema": "", + "columns": { + "account_user_id": { + "name": "account_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "active_access": { + "name": "active_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inventory_generation": { + "name": "inventory_generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_account_repo_catalogs_current_idx": { + "name": "gh_account_repo_catalogs_current_idx", + "columns": [ + { + "expression": "account_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "inventory_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_access", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_account_repo_catalogs_account_fk": { + "name": "gh_account_repo_catalogs_account_fk", + "tableFrom": "github_account_repository_catalogs", + "tableTo": "github_repository_inventory_heads", + "columnsFrom": ["account_user_id"], + "columnsTo": ["account_user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_account_repo_catalogs_repository_fk": { + "name": "gh_account_repo_catalogs_repository_fk", + "tableFrom": "github_account_repository_catalogs", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_account_repo_catalogs_pk": { + "name": "gh_account_repo_catalogs_pk", + "columns": ["account_user_id", "repository_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_account_repo_catalogs_generation": { + "name": "gh_account_repo_catalogs_generation", + "value": "\"github_account_repository_catalogs\".\"inventory_generation\" > 0" + } + }, + "isRLSEnabled": true + }, + "public.github_commit_pull_request_associations": { + "name": "github_commit_pull_request_associations", + "schema": "", + "columns": { + "commit_repository_id": { + "name": "commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "pull_request_node_id": { + "name": "pull_request_node_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gh_commit_pr_associations_commit_fk": { + "name": "gh_commit_pr_associations_commit_fk", + "tableFrom": "github_commit_pull_request_associations", + "tableTo": "github_commits", + "columnsFrom": ["commit_repository_id", "commit_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_commit_pr_associations_pr_fk": { + "name": "gh_commit_pr_associations_pr_fk", + "tableFrom": "github_commit_pull_request_associations", + "tableTo": "github_pull_requests", + "columnsFrom": ["pull_request_node_id"], + "columnsTo": ["node_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_commit_pr_associations_pk": { + "name": "gh_commit_pr_associations_pk", + "columns": [ + "commit_repository_id", + "commit_sha", + "pull_request_node_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_commit_pr_associations_sha_shape": { + "name": "gh_commit_pr_associations_sha_shape", + "value": "\"github_commit_pull_request_associations\".\"commit_sha\" ~ '^[a-f0-9]{40}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_commits": { + "name": "github_commits", + "schema": "", + "columns": { + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "authored_at": { + "name": "authored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "committer_at": { + "name": "committer_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "committer_user_id": { + "name": "committer_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enrichment_error": { + "name": "enrichment_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "enrichment_attempts": { + "name": "enrichment_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enrichment_lease_token": { + "name": "enrichment_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "enrichment_lease_until": { + "name": "enrichment_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enrichment_state": { + "name": "enrichment_state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "file_facts": { + "name": "file_facts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "file_facts_digest": { + "name": "file_facts_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN \"file_facts\" IS NULL THEN NULL ELSE encode(sha256(jsonb_send(\"file_facts\")), 'hex') END", + "type": "stored" + } + }, + "file_facts_complete": { + "name": "file_facts_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_shas": { + "name": "parent_shas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_file_cap_reached": { + "name": "provider_file_cap_reached", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pr_discovery_error": { + "name": "pr_discovery_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "pr_discovery_attempts": { + "name": "pr_discovery_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pr_discovery_lease_token": { + "name": "pr_discovery_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_discovery_lease_until": { + "name": "pr_discovery_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_discovery_state": { + "name": "pr_discovery_state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "sha": { + "name": "sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_commits_committed_at_idx": { + "name": "github_commits_committed_at_idx", + "columns": [ + { + "expression": "committed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_commits_enrichment_pending_idx": { + "name": "github_commits_enrichment_pending_idx", + "columns": [ + { + "expression": "enrichment_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enrichment_lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "committed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_commits_pr_discovery_pending_idx": { + "name": "github_commits_pr_discovery_pending_idx", + "columns": [ + { + "expression": "pr_discovery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_discovery_lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "first_observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_commits_repository_fk": { + "name": "github_commits_repository_fk", + "tableFrom": "github_commits", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "github_commits_repository_id_sha_pk": { + "name": "github_commits_repository_id_sha_pk", + "columns": ["repository_id", "sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_commits_sha_shape": { + "name": "github_commits_sha_shape", + "value": "\"github_commits\".\"sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_commits_parent_shas_array": { + "name": "github_commits_parent_shas_array", + "value": "\"github_commits\".\"parent_shas\" IS NULL OR jsonb_typeof(\"github_commits\".\"parent_shas\") = 'array'" + }, + "github_commits_file_facts_array": { + "name": "github_commits_file_facts_array", + "value": "\"github_commits\".\"file_facts\" IS NULL OR jsonb_typeof(\"github_commits\".\"file_facts\") = 'array'" + }, + "github_commits_file_facts_completeness": { + "name": "github_commits_file_facts_completeness", + "value": "NOT \"github_commits\".\"file_facts_complete\" OR (\"github_commits\".\"file_facts\" IS NOT NULL AND NOT \"github_commits\".\"provider_file_cap_reached\")" + }, + "github_commits_enrichment_state": { + "name": "github_commits_enrichment_state", + "value": "\"github_commits\".\"enrichment_state\" IN ('pending', 'processing', 'complete', 'unavailable')" + }, + "github_commits_enrichment_lease": { + "name": "github_commits_enrichment_lease", + "value": "(\"github_commits\".\"enrichment_state\" = 'processing') = (\"github_commits\".\"enrichment_lease_token\" IS NOT NULL) AND (\"github_commits\".\"enrichment_state\" <> 'processing' OR \"github_commits\".\"enrichment_lease_until\" IS NOT NULL) AND (\"github_commits\".\"enrichment_state\" NOT IN ('complete', 'unavailable') OR \"github_commits\".\"enrichment_lease_until\" IS NULL)" + }, + "github_commits_pr_discovery_state": { + "name": "github_commits_pr_discovery_state", + "value": "\"github_commits\".\"pr_discovery_state\" IN ('pending', 'processing', 'complete', 'unavailable')" + }, + "github_commits_pr_discovery_lease": { + "name": "github_commits_pr_discovery_lease", + "value": "(\"github_commits\".\"pr_discovery_state\" = 'processing') = (\"github_commits\".\"pr_discovery_lease_token\" IS NOT NULL) AND (\"github_commits\".\"pr_discovery_state\" <> 'processing' OR \"github_commits\".\"pr_discovery_lease_until\" IS NOT NULL) AND (\"github_commits\".\"pr_discovery_state\" NOT IN ('complete', 'unavailable') OR \"github_commits\".\"pr_discovery_lease_until\" IS NULL)" + }, + "github_commits_tracked_author": { + "name": "github_commits_tracked_author", + "value": "\"github_commits\".\"author_login\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_commits_nonnegative_activity_counts": { + "name": "github_commits_nonnegative_activity_counts", + "value": "(\"github_commits\".\"changed_files\" IS NULL OR \"github_commits\".\"changed_files\" >= 0) AND (\"github_commits\".\"additions\" IS NULL OR \"github_commits\".\"additions\" >= 0) AND (\"github_commits\".\"deletions\" IS NULL OR \"github_commits\".\"deletions\" >= 0)" + }, + "github_commits_nonnegative_attempts": { + "name": "github_commits_nonnegative_attempts", + "value": "\"github_commits\".\"enrichment_attempts\" >= 0 AND \"github_commits\".\"pr_discovery_attempts\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_issues": { + "name": "github_issues", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "author_login": { + "name": "author_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "node_id": { + "name": "node_id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "title_snapshot": { + "name": "title_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url_snapshot": { + "name": "url_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_issues_repository_number_unique": { + "name": "github_issues_repository_number_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_issues_author_idx": { + "name": "github_issues_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_issues_repository_id_github_repositories_id_fk": { + "name": "github_issues_repository_id_github_repositories_id_fk", + "tableFrom": "github_issues", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_issues_tracked_account": { + "name": "github_issues_tracked_account", + "value": "\"github_issues\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_issues_positive_number": { + "name": "github_issues_positive_number", + "value": "\"github_issues\".\"number\" > 0" + } + }, + "isRLSEnabled": true + }, + "public.github_public_feed_head": { + "name": "github_public_feed_head", + "schema": "", + "columns": { + "feed_revision": { + "name": "feed_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "head_content_revision": { + "name": "head_content_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "last_published_at": { + "name": "last_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ordering_revision": { + "name": "ordering_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "projection_request_token": { + "name": "projection_request_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "summary_policy_digest": { + "name": "summary_policy_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "summarizing": { + "name": "summarizing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_public_feed_head_singleton": { + "name": "gh_public_feed_head_singleton", + "value": "\"github_public_feed_head\".\"id\"" + }, + "gh_public_feed_head_revisions": { + "name": "gh_public_feed_head_revisions", + "value": "\"github_public_feed_head\".\"feed_revision\" >= 0 AND \"github_public_feed_head\".\"head_content_revision\" >= 0 AND \"github_public_feed_head\".\"ordering_revision\" >= 0" + }, + "gh_public_feed_head_summary_policy_digest": { + "name": "gh_public_feed_head_summary_policy_digest", + "value": "\"github_public_feed_head\".\"summary_policy_digest\" IS NULL OR \"github_public_feed_head\".\"summary_policy_digest\" ~ '^[a-f0-9]{64}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_request_memberships": { + "name": "github_pull_request_memberships", + "schema": "", + "columns": { + "commit_repository_id": { + "name": "commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "is_head": { + "name": "is_head", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_pull_request_memberships_position_unique": { + "name": "github_pull_request_memberships_position_unique", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_request_memberships_commit_lookup_idx": { + "name": "github_pull_request_memberships_commit_lookup_idx", + "columns": [ + { + "expression": "commit_repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "commit_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_pr_memberships_version_fk": { + "name": "gh_pr_memberships_version_fk", + "tableFrom": "github_pull_request_memberships", + "tableTo": "github_pull_request_versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_pr_memberships_pk": { + "name": "gh_pr_memberships_pk", + "columns": ["version_id", "commit_repository_id", "commit_sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_request_memberships_sha_shape": { + "name": "github_pull_request_memberships_sha_shape", + "value": "\"github_pull_request_memberships\".\"commit_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_pull_request_memberships_nonnegative_position": { + "name": "github_pull_request_memberships_nonnegative_position", + "value": "\"github_pull_request_memberships\".\"position\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_request_signals": { + "name": "github_pull_request_signals", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "repository_name_snapshot": { + "name": "repository_name_snapshot", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": { + "github_pull_request_signals_event_unique": { + "name": "github_pull_request_signals_event_unique", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_request_signals_pending_idx": { + "name": "github_pull_request_signals_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_request_signals_account": { + "name": "github_pull_request_signals_account", + "value": "\"github_pull_request_signals\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_pull_request_signals_event_id": { + "name": "github_pull_request_signals_event_id", + "value": "\"github_pull_request_signals\".\"event_id\" ~ '^[0-9]{1,64}$'" + }, + "github_pull_request_signals_repository_id": { + "name": "github_pull_request_signals_repository_id", + "value": "\"github_pull_request_signals\".\"repository_id\" ~ '^[0-9]{1,32}$'" + }, + "github_pull_request_signals_state": { + "name": "github_pull_request_signals_state", + "value": "\"github_pull_request_signals\".\"state\" IN ('pending', 'processing', 'complete', 'unavailable')" + }, + "github_pull_request_signals_lease": { + "name": "github_pull_request_signals_lease", + "value": "(\"github_pull_request_signals\".\"state\" = 'processing') = (\"github_pull_request_signals\".\"lease_token\" IS NOT NULL) AND (\"github_pull_request_signals\".\"state\" <> 'processing' OR \"github_pull_request_signals\".\"lease_until\" IS NOT NULL)" + }, + "github_pull_request_signals_values": { + "name": "github_pull_request_signals_values", + "value": "\"github_pull_request_signals\".\"number\" > 0 AND \"github_pull_request_signals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_request_versions": { + "name": "github_pull_request_versions", + "schema": "", + "columns": { + "base_ref_name": { + "name": "base_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_repository_id": { + "name": "base_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "base_sha": { + "name": "base_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "commit_count": { + "name": "commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_facts": { + "name": "file_facts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "file_facts_digest": { + "name": "file_facts_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN \"file_facts\" IS NULL THEN NULL ELSE encode(sha256(jsonb_send(\"file_facts\")), 'hex') END", + "type": "stored" + } + }, + "file_facts_complete": { + "name": "file_facts_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "head_ref_name": { + "name": "head_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_repository_id": { + "name": "head_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_current": { + "name": "is_current", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "membership_complete": { + "name": "membership_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "merge_snapshot": { + "name": "merge_snapshot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "pull_request_node_id": { + "name": "pull_request_node_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_pull_request_versions_head_unique": { + "name": "github_pull_request_versions_head_unique", + "columns": [ + { + "expression": "pull_request_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_request_versions_current_unique": { + "name": "github_pull_request_versions_current_unique", + "columns": [ + { + "expression": "pull_request_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_pull_request_versions\".\"is_current\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_pr_versions_pull_request_fk": { + "name": "gh_pr_versions_pull_request_fk", + "tableFrom": "github_pull_request_versions", + "tableTo": "github_pull_requests", + "columnsFrom": ["pull_request_node_id"], + "columnsTo": ["node_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_request_versions_sha_shapes": { + "name": "github_pull_request_versions_sha_shapes", + "value": "\"github_pull_request_versions\".\"base_sha\" ~ '^[a-f0-9]{40}$' AND \"github_pull_request_versions\".\"head_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_pull_request_versions_nonnegative_count": { + "name": "github_pull_request_versions_nonnegative_count", + "value": "\"github_pull_request_versions\".\"commit_count\" IS NULL OR \"github_pull_request_versions\".\"commit_count\" >= 0" + }, + "github_pull_request_versions_file_facts_array": { + "name": "github_pull_request_versions_file_facts_array", + "value": "\"github_pull_request_versions\".\"file_facts\" IS NULL OR jsonb_typeof(\"github_pull_request_versions\".\"file_facts\") = 'array'" + }, + "github_pull_request_versions_file_facts_complete": { + "name": "github_pull_request_versions_file_facts_complete", + "value": "NOT \"github_pull_request_versions\".\"file_facts_complete\" OR \"github_pull_request_versions\".\"file_facts\" IS NOT NULL" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_requests": { + "name": "github_pull_requests", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "base_ref_name": { + "name": "base_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_repository_id": { + "name": "base_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "base_sha": { + "name": "base_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_snapshot": { + "name": "body_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_count": { + "name": "commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "draft": { + "name": "draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "head_ref_name": { + "name": "head_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_repository_id": { + "name": "head_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "last_reconciled_at": { + "name": "last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "merge_sha": { + "name": "merge_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "merge_sha_verified_at": { + "name": "merge_sha_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "node_id": { + "name": "node_id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reconcile_attempts": { + "name": "reconcile_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reconcile_error": { + "name": "reconcile_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_snapshot": { + "name": "title_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_pull_requests_repository_number_unique": { + "name": "github_pull_requests_repository_number_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_reconciliation_idx": { + "name": "github_pull_requests_reconciliation_idx", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_author_idx": { + "name": "github_pull_requests_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_pull_requests_repository_id_github_repositories_id_fk": { + "name": "github_pull_requests_repository_id_github_repositories_id_fk", + "tableFrom": "github_pull_requests", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_requests_tracked_account": { + "name": "github_pull_requests_tracked_account", + "value": "\"github_pull_requests\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_pull_requests_state": { + "name": "github_pull_requests_state", + "value": "\"github_pull_requests\".\"state\" IN ('open', 'closed', 'merged')" + }, + "github_pull_requests_terminal_state": { + "name": "github_pull_requests_terminal_state", + "value": "(\"github_pull_requests\".\"state\" = 'open' AND \"github_pull_requests\".\"terminal_at\" IS NULL) OR (\"github_pull_requests\".\"state\" IN ('closed', 'merged') AND \"github_pull_requests\".\"terminal_at\" IS NOT NULL)" + }, + "github_pull_requests_merged_state": { + "name": "github_pull_requests_merged_state", + "value": "(\"github_pull_requests\".\"state\" = 'merged') = (\"github_pull_requests\".\"merged_at\" IS NOT NULL)" + }, + "github_pull_requests_sha_shapes": { + "name": "github_pull_requests_sha_shapes", + "value": "(\"github_pull_requests\".\"base_sha\" IS NULL OR \"github_pull_requests\".\"base_sha\" ~ '^[a-f0-9]{40}$') AND (\"github_pull_requests\".\"head_sha\" IS NULL OR \"github_pull_requests\".\"head_sha\" ~ '^[a-f0-9]{40}$') AND (\"github_pull_requests\".\"merge_sha\" IS NULL OR \"github_pull_requests\".\"merge_sha\" ~ '^[a-f0-9]{40}$')" + }, + "github_pull_requests_verified_merge_sha": { + "name": "github_pull_requests_verified_merge_sha", + "value": "(\"github_pull_requests\".\"merge_sha\" IS NULL AND \"github_pull_requests\".\"merge_sha_verified_at\" IS NULL) OR (\"github_pull_requests\".\"state\" = 'merged' AND \"github_pull_requests\".\"merge_sha_verified_at\" IS NOT NULL)" + }, + "github_pull_requests_positive_number": { + "name": "github_pull_requests_positive_number", + "value": "\"github_pull_requests\".\"number\" > 0" + }, + "github_pull_requests_nonnegative_counts": { + "name": "github_pull_requests_nonnegative_counts", + "value": "(\"github_pull_requests\".\"changed_files\" IS NULL OR \"github_pull_requests\".\"changed_files\" >= 0) AND (\"github_pull_requests\".\"additions\" IS NULL OR \"github_pull_requests\".\"additions\" >= 0) AND (\"github_pull_requests\".\"deletions\" IS NULL OR \"github_pull_requests\".\"deletions\" >= 0) AND (\"github_pull_requests\".\"commit_count\" IS NULL OR \"github_pull_requests\".\"commit_count\" >= 0)" + }, + "github_pull_requests_nonnegative_attempts": { + "name": "github_pull_requests_nonnegative_attempts", + "value": "\"github_pull_requests\".\"reconcile_attempts\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_push_observation_commits": { + "name": "github_push_observation_commits", + "schema": "", + "columns": { + "observation_id": { + "name": "observation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "sha": { + "name": "sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_push_observation_commits_position_unique": { + "name": "github_push_observation_commits_position_unique", + "columns": [ + { + "expression": "observation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_push_observation_commits_observation_fk": { + "name": "gh_push_observation_commits_observation_fk", + "tableFrom": "github_push_observation_commits", + "tableTo": "github_push_observations", + "columnsFrom": ["observation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_push_observation_commits_pk": { + "name": "gh_push_observation_commits_pk", + "columns": ["observation_id", "repository_id", "sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_push_observation_commits_sha_shape": { + "name": "github_push_observation_commits_sha_shape", + "value": "\"github_push_observation_commits\".\"sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_push_observation_commits_nonnegative_position": { + "name": "github_push_observation_commits_nonnegative_position", + "value": "\"github_push_observation_commits\".\"position\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_push_observations": { + "name": "github_push_observations", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "after_sha": { + "name": "after_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "before_sha": { + "name": "before_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "expected_commit_count": { + "name": "expected_commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "history_since_at": { + "name": "history_since_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "history_until_at": { + "name": "history_until_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider_created_at": { + "name": "provider_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "repository_name_snapshot": { + "name": "repository_name_snapshot", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": { + "github_push_observations_source_unique": { + "name": "github_push_observations_source_unique", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_push_observations_push_unique": { + "name": "github_push_observations_push_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ref_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "before_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "after_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_push_observations\".\"source\" <> 'backfill'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_push_observations_pending_idx": { + "name": "github_push_observations_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_push_observations_account_idx": { + "name": "github_push_observations_account_idx", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_push_observations_repository_fk": { + "name": "gh_push_observations_repository_fk", + "tableFrom": "github_push_observations", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_push_observations_tracked_account": { + "name": "github_push_observations_tracked_account", + "value": "\"github_push_observations\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_push_observations_source": { + "name": "github_push_observations_source", + "value": "\"github_push_observations\".\"source\" IN ('webhook', 'events', 'refs', 'backfill')" + }, + "github_push_observations_sha_shape": { + "name": "github_push_observations_sha_shape", + "value": "\"github_push_observations\".\"before_sha\" ~ '^[a-f0-9]{40}$' AND \"github_push_observations\".\"after_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_push_observations_nonnegative_count": { + "name": "github_push_observations_nonnegative_count", + "value": "\"github_push_observations\".\"attempt_count\" >= 0 AND (\"github_push_observations\".\"expected_commit_count\" IS NULL OR \"github_push_observations\".\"expected_commit_count\" >= 0)" + }, + "github_push_observations_history_bounds": { + "name": "github_push_observations_history_bounds", + "value": "(\"github_push_observations\".\"source\" <> 'backfill' AND \"github_push_observations\".\"history_since_at\" IS NULL AND \"github_push_observations\".\"history_until_at\" IS NULL) OR (\"github_push_observations\".\"source\" = 'backfill' AND \"github_push_observations\".\"history_since_at\" IS NOT NULL AND \"github_push_observations\".\"history_until_at\" IS NOT NULL AND \"github_push_observations\".\"history_since_at\" <= \"github_push_observations\".\"history_until_at\" AND \"github_push_observations\".\"expected_commit_count\" IS NULL AND \"github_push_observations\".\"before_sha\" = repeat('0', 40))" + }, + "github_push_observations_state": { + "name": "github_push_observations_state", + "value": "\"github_push_observations\".\"state\" IN ('pending', 'processing', 'complete', 'deferred', 'unavailable')" + }, + "github_push_observations_lease": { + "name": "github_push_observations_lease", + "value": "(\"github_push_observations\".\"state\" = 'processing') = (\"github_push_observations\".\"lease_token\" IS NOT NULL) AND (\"github_push_observations\".\"state\" <> 'processing' OR \"github_push_observations\".\"lease_until\" IS NOT NULL)" + } + }, + "isRLSEnabled": true + }, + "public.github_ref_generations": { + "name": "github_ref_generations", + "schema": "", + "columns": { + "branch_lineage_id": { + "name": "branch_lineage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "coverage_since_at": { + "name": "coverage_since_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_ref_generations_lineage_idx": { + "name": "gh_ref_generations_lineage_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_lineage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_ref_generations_repository_fk": { + "name": "gh_ref_generations_repository_fk", + "tableFrom": "github_ref_generations", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_ref_generations_pk": { + "name": "gh_ref_generations_pk", + "columns": ["repository_id", "ref_name"] + } + }, + "uniqueConstraints": { + "gh_ref_generations_version_unique": { + "name": "gh_ref_generations_version_unique", + "nullsNotDistinct": false, + "columns": ["repository_id", "ref_name", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "gh_ref_generations_head_name": { + "name": "gh_ref_generations_head_name", + "value": "\"github_ref_generations\".\"ref_name\" LIKE 'refs/heads/%'" + }, + "gh_ref_generations_sha_shape": { + "name": "gh_ref_generations_sha_shape", + "value": "\"github_ref_generations\".\"head_sha\" ~ '^[a-f0-9]{40}$'" + }, + "gh_ref_generations_positive": { + "name": "gh_ref_generations_positive", + "value": "\"github_ref_generations\".\"generation\" > 0" + } + }, + "isRLSEnabled": true + }, + "public.github_ref_memberships": { + "name": "github_ref_memberships", + "schema": "", + "columns": { + "commit_repository_id": { + "name": "commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_ref_memberships_position_unique": { + "name": "gh_ref_memberships_position_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ref_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_ref_memberships_commit_idx": { + "name": "gh_ref_memberships_commit_idx", + "columns": [ + { + "expression": "commit_repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "commit_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_ref_memberships_generation_fk": { + "name": "gh_ref_memberships_generation_fk", + "tableFrom": "github_ref_memberships", + "tableTo": "github_ref_generations", + "columnsFrom": ["repository_id", "ref_name", "generation"], + "columnsTo": ["repository_id", "ref_name", "generation"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_ref_memberships_commit_fk": { + "name": "gh_ref_memberships_commit_fk", + "tableFrom": "github_ref_memberships", + "tableTo": "github_commits", + "columnsFrom": ["commit_repository_id", "commit_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_ref_memberships_pk": { + "name": "gh_ref_memberships_pk", + "columns": [ + "repository_id", + "ref_name", + "commit_repository_id", + "commit_sha" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_ref_memberships_values": { + "name": "gh_ref_memberships_values", + "value": "\"github_ref_memberships\".\"generation\" > 0 AND \"github_ref_memberships\".\"position\" >= 0 AND \"github_ref_memberships\".\"commit_sha\" ~ '^[a-f0-9]{40}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_repositories": { + "name": "github_repositories", + "schema": "", + "columns": { + "default_branch": { + "name": "default_branch", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "facts_verified_at": { + "name": "facts_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "inventory_verified_at": { + "name": "inventory_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "homepage_url": { + "name": "homepage_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heads_last_reconciled_at": { + "name": "heads_last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "last_observed_at": { + "name": "last_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "owner_avatar_url": { + "name": "owner_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "owner_login": { + "name": "owner_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "topics": { + "name": "topics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags_last_reconciled_at": { + "name": "tags_last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "github_repositories_full_name_idx": { + "name": "github_repositories_full_name_idx", + "columns": [ + { + "expression": "full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_repositories_full_name": { + "name": "github_repositories_full_name", + "value": "length(btrim(\"github_repositories\".\"full_name\")) > 0" + }, + "github_repositories_owner_type": { + "name": "github_repositories_owner_type", + "value": "\"github_repositories\".\"owner_type\" IS NULL OR \"github_repositories\".\"owner_type\" IN ('Organization', 'User')" + }, + "github_repositories_visibility": { + "name": "github_repositories_visibility", + "value": "\"github_repositories\".\"visibility\" IS NULL OR \"github_repositories\".\"visibility\" IN ('public', 'private', 'internal')" + }, + "github_repositories_topics_array": { + "name": "github_repositories_topics_array", + "value": "\"github_repositories\".\"topics\" IS NULL OR jsonb_typeof(\"github_repositories\".\"topics\") = 'array'" + }, + "github_repositories_observation_order": { + "name": "github_repositories_observation_order", + "value": "\"github_repositories\".\"last_observed_at\" >= \"github_repositories\".\"first_observed_at\"" + } + }, + "isRLSEnabled": true + }, + "public.github_repository_inventory_heads": { + "name": "github_repository_inventory_heads", + "schema": "", + "columns": { + "account_login": { + "name": "account_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "account_user_id": { + "name": "account_user_id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_repo_inventory_head_account_id": { + "name": "gh_repo_inventory_head_account_id", + "value": "\"github_repository_inventory_heads\".\"account_user_id\" ~ '^[0-9]{1,32}$'" + }, + "gh_repo_inventory_head_generation": { + "name": "gh_repo_inventory_head_generation", + "value": "(\"github_repository_inventory_heads\".\"generation\" = 0 AND \"github_repository_inventory_heads\".\"completed_at\" IS NULL) OR (\"github_repository_inventory_heads\".\"generation\" > 0 AND \"github_repository_inventory_heads\".\"completed_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": true + }, + "public.github_repository_refs": { + "name": "github_repository_refs", + "schema": "", + "columns": { + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "branch_lineage_id": { + "name": "branch_lineage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "last_observed_at": { + "name": "last_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "projection_relevant": { + "name": "projection_relevant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repair_attempts": { + "name": "repair_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "repair_error": { + "name": "repair_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "repair_lease_token": { + "name": "repair_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repair_lease_until": { + "name": "repair_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_repository_refs_active_idx": { + "name": "github_repository_refs_active_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_repository_refs_projection_idx": { + "name": "github_repository_refs_projection_idx", + "columns": [ + { + "expression": "projection_relevant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_repository_refs_repository_fk": { + "name": "github_repository_refs_repository_fk", + "tableFrom": "github_repository_refs", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "github_repository_refs_repository_id_ref_name_pk": { + "name": "github_repository_refs_repository_id_ref_name_pk", + "columns": ["repository_id", "ref_name"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_repository_refs_kind": { + "name": "github_repository_refs_kind", + "value": "\"github_repository_refs\".\"kind\" IN ('head', 'tag')" + }, + "github_repository_refs_name": { + "name": "github_repository_refs_name", + "value": "(\"github_repository_refs\".\"kind\" = 'head' AND \"github_repository_refs\".\"ref_name\" LIKE 'refs/heads/%') OR (\"github_repository_refs\".\"kind\" = 'tag' AND \"github_repository_refs\".\"ref_name\" LIKE 'refs/tags/%')" + }, + "github_repository_refs_lineage": { + "name": "github_repository_refs_lineage", + "value": "(\"github_repository_refs\".\"kind\" = 'head') = (\"github_repository_refs\".\"branch_lineage_id\" IS NOT NULL)" + }, + "github_repository_refs_projection_relevance": { + "name": "github_repository_refs_projection_relevance", + "value": "NOT \"github_repository_refs\".\"projection_relevant\" OR \"github_repository_refs\".\"kind\" = 'head'" + }, + "github_repository_refs_sha_shape": { + "name": "github_repository_refs_sha_shape", + "value": "\"github_repository_refs\".\"head_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_repository_refs_observation_order": { + "name": "github_repository_refs_observation_order", + "value": "\"github_repository_refs\".\"last_observed_at\" >= \"github_repository_refs\".\"first_observed_at\"" + }, + "github_repository_refs_repair_lease": { + "name": "github_repository_refs_repair_lease", + "value": "(\"github_repository_refs\".\"repair_lease_token\" IS NULL) = (\"github_repository_refs\".\"repair_lease_until\" IS NULL) AND (\"github_repository_refs\".\"repair_lease_token\" IS NULL OR \"github_repository_refs\".\"kind\" = 'head')" + }, + "github_repository_refs_repair_attempts": { + "name": "github_repository_refs_repair_attempts", + "value": "\"github_repository_refs\".\"repair_attempts\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_webhook_deliveries": { + "name": "github_webhook_deliveries", + "schema": "", + "columns": { + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "delivery_id": { + "name": "delivery_id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "github_webhook_deliveries_audit_idx": { + "name": "github_webhook_deliveries_audit_idx", + "columns": [ + { + "expression": "event", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_webhook_deliveries_id_shape": { + "name": "github_webhook_deliveries_id_shape", + "value": "\"github_webhook_deliveries\".\"delivery_id\" ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'" + }, + "github_webhook_deliveries_event_shape": { + "name": "github_webhook_deliveries_event_shape", + "value": "\"github_webhook_deliveries\".\"event\" ~ '^[a-z][a-z0-9_]{0,39}$'" + }, + "github_webhook_deliveries_action_shape": { + "name": "github_webhook_deliveries_action_shape", + "value": "\"github_webhook_deliveries\".\"action\" IS NULL OR \"github_webhook_deliveries\".\"action\" ~ '^[a-z][a-z0-9_]{0,39}$'" + }, + "github_webhook_deliveries_repository_id_shape": { + "name": "github_webhook_deliveries_repository_id_shape", + "value": "\"github_webhook_deliveries\".\"repository_id\" IS NULL OR \"github_webhook_deliveries\".\"repository_id\" ~ '^[0-9]{1,32}$'" + }, + "github_webhook_deliveries_tracked_account": { + "name": "github_webhook_deliveries_tracked_account", + "value": "\"github_webhook_deliveries\".\"account\" IS NULL OR \"github_webhook_deliveries\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_accepted_summaries": { + "name": "github_work_unit_accepted_summaries", + "schema": "", + "columns": { + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attribution_mode": { + "name": "attribution_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "identity_key": { + "name": "identity_key", + "type": "varchar(180)", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome_digest": { + "name": "outcome_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "recipe": { + "name": "recipe", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "summary_input_digest": { + "name": "summary_input_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_unit_accepted_summaries_identity_idx": { + "name": "gh_work_unit_accepted_summaries_identity_idx", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attribution_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_accepted_summaries_outcome_idx": { + "name": "gh_work_unit_accepted_summaries_outcome_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "outcome_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attribution_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "gh_work_unit_accepted_summaries_pk": { + "name": "gh_work_unit_accepted_summaries_pk", + "columns": ["identity_key", "summary_input_digest", "recipe"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_unit_accepted_summaries_digests": { + "name": "gh_work_unit_accepted_summaries_digests", + "value": "\"github_work_unit_accepted_summaries\".\"outcome_digest\" ~ '^[a-f0-9]{64}$' AND \"github_work_unit_accepted_summaries\".\"summary_input_digest\" ~ '^[a-f0-9]{64}$'" + }, + "gh_work_unit_accepted_summaries_attribution": { + "name": "gh_work_unit_accepted_summaries_attribution", + "value": "\"github_work_unit_accepted_summaries\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution', 'canonical_owned_composite', 'branch_owned_composite')" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_memberships": { + "name": "github_work_unit_memberships", + "schema": "", + "columns": { + "logical_repository_id": { + "name": "logical_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "logical_sha": { + "name": "logical_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "work_unit_id": { + "name": "work_unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_unit_memberships_position_unique": { + "name": "gh_work_unit_memberships_position_unique", + "columns": [ + { + "expression": "work_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_memberships_commit_unique": { + "name": "gh_work_unit_memberships_commit_unique", + "columns": [ + { + "expression": "logical_repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "logical_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_work_unit_memberships_unit_fk": { + "name": "gh_work_unit_memberships_unit_fk", + "tableFrom": "github_work_unit_memberships", + "tableTo": "github_work_units", + "columnsFrom": ["work_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_work_unit_memberships_commit_fk": { + "name": "gh_work_unit_memberships_commit_fk", + "tableFrom": "github_work_unit_memberships", + "tableTo": "github_commits", + "columnsFrom": ["logical_repository_id", "logical_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_work_unit_memberships_pk": { + "name": "gh_work_unit_memberships_pk", + "columns": ["work_unit_id", "logical_repository_id", "logical_sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_unit_memberships_values": { + "name": "gh_work_unit_memberships_values", + "value": "\"github_work_unit_memberships\".\"position\" >= 0 AND \"github_work_unit_memberships\".\"logical_sha\" ~ '^[a-f0-9]{40}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_summary_attempts": { + "name": "github_work_unit_summary_attempts", + "schema": "", + "columns": { + "identity_key": { + "name": "identity_key", + "type": "varchar(180)", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attribution_mode": { + "name": "attribution_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "debounce_until": { + "name": "debounce_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_started_at": { + "name": "request_started_at", + "type": "timestamp with time zone[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::timestamptz[]" + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_digest": { + "name": "outcome_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recipe": { + "name": "recipe", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "request_payload": { + "name": "request_payload", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_requests": { + "name": "started_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "state": { + "name": "state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "summary_input_digest": { + "name": "summary_input_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "work_unit_id": { + "name": "work_unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_unit_summary_input_unique": { + "name": "gh_work_unit_summary_input_unique", + "columns": [ + { + "expression": "work_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "summary_input_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_summary_claim_idx": { + "name": "gh_work_unit_summary_claim_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "debounce_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_summary_attempts_identity_idx": { + "name": "gh_work_unit_summary_attempts_identity_idx", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "gh_work_unit_summary_attempts_pk": { + "name": "gh_work_unit_summary_attempts_pk", + "columns": ["work_unit_id", "revision"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_unit_summary_state": { + "name": "gh_work_unit_summary_state", + "value": "\"github_work_unit_summary_attempts\".\"state\" IN ('pending', 'processing', 'retryable', 'accepted', 'terminal')" + }, + "gh_work_unit_summary_digests": { + "name": "gh_work_unit_summary_digests", + "value": "\"github_work_unit_summary_attempts\".\"outcome_digest\" ~ '^[a-f0-9]{64}$' AND \"github_work_unit_summary_attempts\".\"summary_input_digest\" ~ '^[a-f0-9]{64}$'" + }, + "gh_work_unit_summary_attribution": { + "name": "gh_work_unit_summary_attribution", + "value": "\"github_work_unit_summary_attempts\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution', 'canonical_owned_composite', 'branch_owned_composite')" + }, + "gh_work_unit_summary_revisions": { + "name": "gh_work_unit_summary_revisions", + "value": "\"github_work_unit_summary_attempts\".\"revision\" > 0 AND \"github_work_unit_summary_attempts\".\"started_requests\" BETWEEN 0 AND 2" + }, + "gh_work_unit_summary_lease": { + "name": "gh_work_unit_summary_lease", + "value": "(\"github_work_unit_summary_attempts\".\"lease_token\" IS NULL) = (\"github_work_unit_summary_attempts\".\"lease_until\" IS NULL) AND (\"github_work_unit_summary_attempts\".\"state\" = 'processing') = (\"github_work_unit_summary_attempts\".\"lease_token\" IS NOT NULL) AND (\"github_work_unit_summary_attempts\".\"state\" <> 'processing' OR (\"github_work_unit_summary_attempts\".\"started_requests\" > 0 AND \"github_work_unit_summary_attempts\".\"request_payload\" IS NOT NULL))" + }, + "gh_work_unit_summary_terminal_payload": { + "name": "gh_work_unit_summary_terminal_payload", + "value": "\"github_work_unit_summary_attempts\".\"state\" NOT IN ('accepted', 'terminal') OR \"github_work_unit_summary_attempts\".\"request_payload\" IS NULL" + }, + "gh_work_unit_summary_accepted_output": { + "name": "gh_work_unit_summary_accepted_output", + "value": "(\"github_work_unit_summary_attempts\".\"state\" = 'accepted' AND \"github_work_unit_summary_attempts\".\"outcome\" IS NOT NULL AND \"github_work_unit_summary_attempts\".\"accepted_at\" IS NOT NULL AND \"github_work_unit_summary_attempts\".\"completed_at\" IS NOT NULL) OR (\"github_work_unit_summary_attempts\".\"state\" <> 'accepted' AND \"github_work_unit_summary_attempts\".\"outcome\" IS NULL AND \"github_work_unit_summary_attempts\".\"accepted_at\" IS NULL AND (\"github_work_unit_summary_attempts\".\"state\" <> 'terminal' OR \"github_work_unit_summary_attempts\".\"completed_at\" IS NOT NULL))" + }, + "gh_work_unit_summary_started": { + "name": "gh_work_unit_summary_started", + "value": "(\"github_work_unit_summary_attempts\".\"started_requests\" = 0) = (\"github_work_unit_summary_attempts\".\"last_started_at\" IS NULL) AND (\"github_work_unit_summary_attempts\".\"state\" <> 'pending' OR \"github_work_unit_summary_attempts\".\"request_payload\" IS NOT NULL)" + }, + "gh_work_unit_summary_request_cap": { + "name": "gh_work_unit_summary_request_cap", + "value": "\"github_work_unit_summary_attempts\".\"request_payload\" IS NULL OR octet_length(\"github_work_unit_summary_attempts\".\"request_payload\") <= 393216" + }, + "gh_work_unit_summary_request_times": { + "name": "gh_work_unit_summary_request_times", + "value": "cardinality(\"github_work_unit_summary_attempts\".\"request_started_at\") = \"github_work_unit_summary_attempts\".\"started_requests\"" + }, + "gh_work_unit_summary_metrics": { + "name": "gh_work_unit_summary_metrics", + "value": "(\"github_work_unit_summary_attempts\".\"input_tokens\" IS NULL OR \"github_work_unit_summary_attempts\".\"input_tokens\" >= 0) AND (\"github_work_unit_summary_attempts\".\"output_tokens\" IS NULL OR \"github_work_unit_summary_attempts\".\"output_tokens\" >= 0) AND (\"github_work_unit_summary_attempts\".\"latency_ms\" IS NULL OR \"github_work_unit_summary_attempts\".\"latency_ms\" >= 0)" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_summary_daily_usage": { + "name": "github_work_unit_summary_daily_usage", + "schema": "", + "columns": { + "day": { + "name": "day", + "type": "date", + "primaryKey": true, + "notNull": true + }, + "started_requests": { + "name": "started_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.github_work_units": { + "name": "github_work_units", + "schema": "", + "columns": { + "activity_anchor_at": { + "name": "activity_anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "activity_at": { + "name": "activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "activity_day": { + "name": "activity_day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attribution_mode": { + "name": "attribution_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "branch_lineage_id": { + "name": "branch_lineage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_observed_at": { + "name": "content_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "facts_digest": { + "name": "facts_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_activity_at": { + "name": "first_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identity_key": { + "name": "identity_key", + "type": "varchar(180)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "member_count": { + "name": "member_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "membership_digest": { + "name": "membership_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "newest_commit_repository_id": { + "name": "newest_commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "newest_commit_sha": { + "name": "newest_commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "outcome_digest": { + "name": "outcome_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "pull_request_node_id": { + "name": "pull_request_node_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "summary_evaluation_digest": { + "name": "summary_evaluation_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "summary_evaluated_digest": { + "name": "summary_evaluated_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "summary_input_digest": { + "name": "summary_input_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_units_identity_unique": { + "name": "gh_work_units_identity_unique", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_pr_unique": { + "name": "gh_work_units_pr_unique", + "columns": [ + { + "expression": "pull_request_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_work_units\".\"kind\" = 'pull_request'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_canonical_day_unique": { + "name": "gh_work_units_canonical_day_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_work_units\".\"kind\" = 'canonical_day'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_branch_unique": { + "name": "gh_work_units_branch_unique", + "columns": [ + { + "expression": "branch_lineage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_work_units\".\"kind\" = 'branch'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_feed_idx": { + "name": "gh_work_units_feed_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_day", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_work_units_repository_fk": { + "name": "gh_work_units_repository_fk", + "tableFrom": "github_work_units", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_work_units_pull_request_fk": { + "name": "gh_work_units_pull_request_fk", + "tableFrom": "github_work_units", + "tableTo": "github_pull_requests", + "columnsFrom": ["pull_request_node_id"], + "columnsTo": ["node_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_work_units_newest_commit_fk": { + "name": "gh_work_units_newest_commit_fk", + "tableFrom": "github_work_units", + "tableTo": "github_commits", + "columnsFrom": ["newest_commit_repository_id", "newest_commit_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_units_kind": { + "name": "gh_work_units_kind", + "value": "\"github_work_units\".\"kind\" IN ('pull_request', 'canonical_day', 'branch')" + }, + "gh_work_units_owner_shape": { + "name": "gh_work_units_owner_shape", + "value": "(\"github_work_units\".\"kind\" = 'pull_request' AND \"github_work_units\".\"pull_request_node_id\" IS NOT NULL AND \"github_work_units\".\"branch_lineage_id\" IS NULL) OR (\"github_work_units\".\"kind\" = 'canonical_day' AND \"github_work_units\".\"pull_request_node_id\" IS NULL AND \"github_work_units\".\"branch_lineage_id\" IS NULL) OR (\"github_work_units\".\"kind\" = 'branch' AND \"github_work_units\".\"pull_request_node_id\" IS NULL AND \"github_work_units\".\"branch_lineage_id\" IS NOT NULL)" + }, + "gh_work_units_identity": { + "name": "gh_work_units_identity", + "value": "(\"github_work_units\".\"kind\" = 'pull_request' AND \"github_work_units\".\"identity_key\" = 'pr:' || \"github_work_units\".\"pull_request_node_id\") OR (\"github_work_units\".\"kind\" = 'canonical_day' AND \"github_work_units\".\"identity_key\" = 'canonical:' || \"github_work_units\".\"repository_id\" || ':' || \"github_work_units\".\"activity_day\"::text) OR (\"github_work_units\".\"kind\" = 'branch' AND \"github_work_units\".\"identity_key\" = 'branch:' || \"github_work_units\".\"branch_lineage_id\"::text)" + }, + "gh_work_units_attribution_mode": { + "name": "gh_work_units_attribution_mode", + "value": "\"github_work_units\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution', 'canonical_owned_composite', 'branch_owned_composite')" + }, + "gh_work_units_kind_attribution": { + "name": "gh_work_units_kind_attribution", + "value": "(\"github_work_units\".\"kind\" = 'pull_request' AND \"github_work_units\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution')) OR (\"github_work_units\".\"kind\" = 'canonical_day' AND \"github_work_units\".\"attribution_mode\" = 'canonical_owned_composite') OR (\"github_work_units\".\"kind\" = 'branch' AND \"github_work_units\".\"attribution_mode\" = 'branch_owned_composite')" + }, + "gh_work_units_visibility": { + "name": "gh_work_units_visibility", + "value": "\"github_work_units\".\"visibility\" IN ('public', 'private')" + }, + "gh_work_units_nonnegative_facts": { + "name": "gh_work_units_nonnegative_facts", + "value": "\"github_work_units\".\"member_count\" > 0 AND \"github_work_units\".\"file_count\" >= 0 AND \"github_work_units\".\"additions\" >= 0 AND \"github_work_units\".\"deletions\" >= 0 AND \"github_work_units\".\"revision\" > 0" + }, + "gh_work_units_activity_order": { + "name": "gh_work_units_activity_order", + "value": "\"github_work_units\".\"first_activity_at\" <= \"github_work_units\".\"last_activity_at\" AND \"github_work_units\".\"activity_day\" = (\"github_work_units\".\"activity_at\" AT TIME ZONE 'UTC')::date" + }, + "gh_work_units_digest_shapes": { + "name": "gh_work_units_digest_shapes", + "value": "\"github_work_units\".\"facts_digest\" ~ '^[a-f0-9]{64}$' AND \"github_work_units\".\"membership_digest\" ~ '^[a-f0-9]{64}$' AND (\"github_work_units\".\"outcome_digest\" IS NULL OR \"github_work_units\".\"outcome_digest\" ~ '^[a-f0-9]{64}$') AND (\"github_work_units\".\"summary_evaluation_digest\" IS NULL OR \"github_work_units\".\"summary_evaluation_digest\" ~ '^[a-f0-9]{64}$') AND (\"github_work_units\".\"summary_evaluated_digest\" IS NULL OR \"github_work_units\".\"summary_evaluated_digest\" ~ '^[a-f0-9]{64}$') AND (\"github_work_units\".\"summary_input_digest\" IS NULL OR \"github_work_units\".\"summary_input_digest\" ~ '^[a-f0-9]{64}$')" + }, + "gh_work_units_languages_array": { + "name": "gh_work_units_languages_array", + "value": "\"github_work_units\".\"languages\" IS NULL OR jsonb_typeof(\"github_work_units\".\"languages\") = 'array'" + } + }, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 4d760007..1ec284a1 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1788813490378, "tag": "0021_windy_midnight", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1788907570264, + "tag": "0022_portable_github_accounts", + "breakpoints": true } ] } diff --git a/next.config.ts b/next.config.ts index 772798dc..2a11bc7c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,8 +2,13 @@ import createMDX from "@next/mdx"; import type { NextConfig } from "next"; import { env } from "./src/env"; +import { siteOriginFrom } from "./src/lib/site-url"; const nextConfig: NextConfig = { + env: { + NEXT_PUBLIC_SITE_ORIGIN: siteOriginFrom(env), + NEXT_PUBLIC_DEPLOYMENT_ENV: env.VERCEL_ENV ?? env.NODE_ENV ?? "development", + }, headers: async () => env.VERCEL_ENV === "preview" ? [ diff --git a/package.json b/package.json index 05988b0b..9e2887ea 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "next dev", - "build": "bun scripts/migrate-production-database.ts && next build && bun scripts/configure-supabase-cron.ts --production-build", + "build": "next build", "start": "next start", "test": "bun test ./tests", "typecheck": "tsgo --noEmit", @@ -28,61 +28,61 @@ }, "dependencies": { "@ai-sdk/openai": "4.0.41", - "@base-ui/react": "^1.6.0", - "@mdx-js/loader": "^3.1.1", - "@mdx-js/react": "^3.1.1", + "@base-ui/react": "1.6.0", + "@mdx-js/loader": "3.1.1", + "@mdx-js/react": "3.1.1", "@mermaid-js/layout-elk": "0.2.3", - "@next/mdx": "^16.3.4", - "@remark-embedder/core": "^3.0.3", - "@t3-oss/env-nextjs": "^0.13.11", - "@tailwindcss/typography": "^0.5.20", - "@tanstack/react-query": "^5.102.8", + "@next/mdx": "16.3.4", + "@remark-embedder/core": "3.0.3", + "@t3-oss/env-nextjs": "0.13.11", + "@tailwindcss/typography": "0.5.20", + "@tanstack/react-query": "5.102.8", "ai": "7.0.63", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "drizzle-orm": "0.45.2", - "feed": "^5.2.1", + "feed": "5.2.1", "gpt-tokenizer": "4.0.0", - "lucide-react": "^1.21.0", - "mermaid": "^11.17.2", - "motion": "^13.2.0", - "next": "^16.3.4", - "next-themes": "^0.4.6", + "lucide-react": "1.21.0", + "mermaid": "11.17.2", + "motion": "13.2.0", + "next": "16.3.4", + "next-themes": "0.4.6", "postgres": "3.4.9", "posthog-js": "1.428.7", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "reading-time": "^1.5.0", - "rehype-autolink-headings": "^7.1.0", - "rehype-pretty-code": "^0.14.5", - "rehype-slug": "^6.0.0", - "remark-gfm": "^4.0.1", + "react": "19.2.8", + "react-dom": "19.2.8", + "reading-time": "1.5.0", + "rehype-autolink-headings": "7.1.0", + "rehype-pretty-code": "0.14.5", + "rehype-slug": "6.0.0", + "remark-gfm": "4.0.1", "server-only": "0.0.1", - "shadcn": "^4.11.0", - "shiki": "^4.4.3", - "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", - "unist-util-visit": "^5.1.0", - "zod": "^4.4.3" + "shadcn": "4.11.0", + "shiki": "4.4.3", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "unist-util-visit": "5.1.0", + "zod": "4.4.3" }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.1", - "@types/bun": "^1.4.1", - "@types/mdx": "^2.0.14", + "@tailwindcss/postcss": "4.3.1", + "@types/bun": "1.4.1", + "@types/mdx": "2.0.14", "@types/node": "24.13.3", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260624.1", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "7.0.0-dev.20260624.1", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "oxfmt": "^0.56.0", - "oxlint": "^1.71.0", - "oxlint-tsgolint": "^0.23.0", - "schema-dts": "^2.0.0", - "tailwindcss": "^4.3.1", - "tw-animate-css": "^1.4.0", - "typescript": "^7.0.1-rc", - "ultracite": "^7.8.3" + "oxfmt": "0.56.0", + "oxlint": "1.71.0", + "oxlint-tsgolint": "0.23.0", + "schema-dts": "2.0.0", + "tailwindcss": "4.3.1", + "tw-animate-css": "1.4.0", + "typescript": "7.0.1-rc", + "ultracite": "7.8.3" }, "engines": { "node": "24.x" diff --git a/scripts/backfill-github-activity.ts b/scripts/backfill-github-activity.ts index c6258073..a241bbc2 100644 --- a/scripts/backfill-github-activity.ts +++ b/scripts/backfill-github-activity.ts @@ -1,3 +1,5 @@ +import { tokenForGitHubAccount } from "@/lib/github-accounts"; + import { closeDatabase } from "../src/db/client"; import { env } from "../src/env"; import { runGitHubActivityWorker } from "../src/lib/github-activity-worker"; @@ -41,8 +43,7 @@ interface BackfillArguments { interface BackfillEnvironment { DATABASE_URL?: string; - GITHUB_F0RR0_TOKEN?: string; - GITHUB_YUPPIESTECHDEV_TOKEN?: string; + GITHUB_TOKENS?: string; } type GitHubBackfillProgressStage = @@ -123,7 +124,7 @@ export const backfillArgumentsFrom = ( ); } return { - account: requiredArgument(values, "account"), + account: values.get("account")?.trim() ?? "all", endDate: requiredArgument(values, "end-date"), maximumMinutes, repositoryId: values.get("repository-id")?.trim() ?? "", @@ -138,32 +139,9 @@ export const requireBackfillEnvironment = ( if (environment.DATABASE_URL === undefined) { throw new Error("DATABASE_URL is not configured."); } - if ( - request.accounts.includes("f0rr0") && - environment.GITHUB_F0RR0_TOKEN === undefined - ) { - throw new Error("GITHUB_F0RR0_TOKEN is not configured."); - } - if ( - request.accounts.includes("yuppiestechdev") && - environment.GITHUB_YUPPIESTECHDEV_TOKEN === undefined - ) { - throw new Error("GITHUB_YUPPIESTECHDEV_TOKEN is not configured."); - } -}; - -const tokenFor = ( - account: TrackedGitHubAccount, - environment: BackfillEnvironment -) => { - const token = - account === "f0rr0" - ? environment.GITHUB_F0RR0_TOKEN - : environment.GITHUB_YUPPIESTECHDEV_TOKEN; - if (token === undefined) { - throw new Error(`The GitHub token for ${account} is not configured.`); + for (const account of request.accounts) { + tokenForGitHubAccount(account, environment); } - return token; }; interface GitHubBackfillFactualDrainDependencies { @@ -371,7 +349,7 @@ export const runGitHubBackfillDiscovery = async ( ): Promise => { const identities = await Promise.all( input.request.accounts.map(async (account) => { - const token = tokenFor(account, input.environment); + const token = tokenForGitHubAccount(account, input.environment); input.onProgress?.({ account, phase: "started", diff --git a/scripts/configure-supabase-cron.ts b/scripts/configure-supabase-cron.ts index 7c785e06..85595811 100644 --- a/scripts/configure-supabase-cron.ts +++ b/scripts/configure-supabase-cron.ts @@ -1,6 +1,7 @@ import postgres from "postgres"; import { env } from "../src/env"; +import { githubTokensFrom } from "../src/lib/github-accounts"; import { GITHUB_CRON_EXECUTION_DURATION_MS, GITHUB_EVENTS_CRON_JOB, @@ -10,7 +11,7 @@ import { GITHUB_WORKER_HTTP_TIMEOUT_MS, GITHUB_WORKER_CRON_JOB, } from "../src/lib/github-cron-config"; -import { CANONICAL_SITE_URL } from "../src/lib/site-url"; +import { productionSiteOrigin } from "../src/lib/site-url"; import { shouldApplyProductionMigrations } from "./migrate-production-database"; const SECRET_DESCRIPTION = "Vercel cron configuration"; @@ -25,10 +26,13 @@ const LEGACY_SUMMARY_JOB_NAME = "github-summary-worker-every-five-minutes"; const LEGACY_JOB_NAME = "github-sync-every-three-hours"; const LEGACY_REFS_JOB_NAME = "github-refs-every-fifteen-minutes"; const LEGACY_TAG_REFS_JOB_NAME = "github-tag-refs-every-fifteen-minutes"; +// Keep the historical lock key so overlapping old/new deployments still coordinate. const CRON_CONFIGURATION_LOCK_NAME = "f0rr0.dev:supabase-cron"; interface SupabaseCronEnvironment { CRON_SECRET?: string; + GITHUB_TOKENS?: string; + OPENAI_API_KEY?: string; DATABASE_URL?: string; DATABASE_URL_UNPOOLED?: string; VERCEL?: string; @@ -78,14 +82,8 @@ export const supabaseCronUrlsFrom = (configuredSiteUrl: string) => { }; }; -export const supabaseCronSiteUrlFrom = ( - environment: SupabaseCronEnvironment -) => { - const productionHostname = environment.VERCEL_PROJECT_PRODUCTION_URL?.trim(); - return productionHostname === undefined || productionHostname.length === 0 - ? CANONICAL_SITE_URL - : `https://${productionHostname}`; -}; +export const supabaseCronSiteUrlFrom = (environment: SupabaseCronEnvironment) => + productionSiteOrigin(environment.VERCEL_PROJECT_PRODUCTION_URL); const upsertVaultSecret = async ( sql: postgres.TransactionSql, @@ -142,6 +140,53 @@ const cronHttpPostCommand = ( ) as request_id `; +export const supabaseCronJobsFrom = ( + environment: SupabaseCronEnvironment, + codexEnabled: boolean +) => { + const githubEnabled = + Object.keys(githubTokensFrom(environment.GITHUB_TOKENS)).length > 0; + const urls = supabaseCronUrlsFrom(supabaseCronSiteUrlFrom(environment)); + return [ + { + ...GITHUB_EVENTS_CRON_JOB, + urlName: URL_NAME, + url: urls.events, + timeout: GITHUB_CRON_EXECUTION_DURATION_MS, + enabled: githubEnabled, + }, + { + ...GITHUB_HEAD_REFS_CRON_JOB, + urlName: HEAD_REFS_URL_NAME, + url: urls.headRefs, + timeout: GITHUB_CRON_EXECUTION_DURATION_MS, + enabled: githubEnabled, + }, + { + ...GITHUB_WORKER_CRON_JOB, + urlName: WORKER_URL_NAME, + url: urls.worker, + timeout: GITHUB_WORKER_HTTP_TIMEOUT_MS, + enabled: true, + }, + { + ...GITHUB_SUMMARY_CRON_JOB, + urlName: SUMMARY_URL_NAME, + url: urls.summary, + timeout: GITHUB_WORKER_HTTP_TIMEOUT_MS, + enabled: Boolean(environment.OPENAI_API_KEY?.trim()), + }, + { + name: CODEX_STATS_JOB_NAME, + schedule: "7,22,37,52 * * * *", + urlName: CODEX_STATS_URL_NAME, + url: urls.codexStats, + timeout: GITHUB_WORKER_HTTP_TIMEOUT_MS, + enabled: codexEnabled, + }, + ]; +}; + export const configureSupabaseCron = async ( environment: SupabaseCronEnvironment = env ) => { @@ -153,7 +198,8 @@ export const configureSupabaseCron = async ( if (cronSecret.length < 32) { throw new Error("CRON_SECRET must contain at least 32 characters."); } - const urls = supabaseCronUrlsFrom(supabaseCronSiteUrlFrom(environment)); + // Validate configuration before opening a connection. + supabaseCronJobsFrom(environment, false); const sql = postgres(databaseUrl, { connect_timeout: 10, idle_timeout: 20, @@ -174,26 +220,14 @@ export const configureSupabaseCron = async ( await transaction`create extension if not exists pg_net with schema extensions`; await transaction`create extension if not exists supabase_vault with schema vault`; - await upsertVaultSecret(transaction, { - name: URL_NAME, - value: urls.events, - }); - await upsertVaultSecret(transaction, { - name: HEAD_REFS_URL_NAME, - value: urls.headRefs, - }); - await upsertVaultSecret(transaction, { - name: SUMMARY_URL_NAME, - value: urls.summary, - }); - await upsertVaultSecret(transaction, { - name: WORKER_URL_NAME, - value: urls.worker, - }); - await upsertVaultSecret(transaction, { - name: CODEX_STATS_URL_NAME, - value: urls.codexStats, - }); + const [codex] = await transaction<{ enabled: boolean }[]>` + select exists(select 1 from codex_accounts where enabled) as enabled + `; + const configuredJobs = supabaseCronJobsFrom( + environment, + + codex?.enabled + ); await upsertVaultSecret(transaction, { name: SECRET_NAME, value: cronSecret, @@ -202,68 +236,34 @@ export const configureSupabaseCron = async ( await transaction` select cron.unschedule(jobid) from cron.job - where jobname in ( - ${LEGACY_JOB_NAME}, - ${LEGACY_REFS_JOB_NAME}, - ${LEGACY_TAG_REFS_JOB_NAME}, - ${LEGACY_SUMMARY_JOB_NAME}, - ${GITHUB_EVENTS_CRON_JOB.name}, - ${GITHUB_HEAD_REFS_CRON_JOB.name}, - ${GITHUB_SUMMARY_CRON_JOB.name}, - ${GITHUB_WORKER_CRON_JOB.name}, - ${CODEX_STATS_JOB_NAME} - ) + where jobname = any(${[ + LEGACY_JOB_NAME, + LEGACY_REFS_JOB_NAME, + LEGACY_TAG_REFS_JOB_NAME, + LEGACY_SUMMARY_JOB_NAME, + ...configuredJobs.map(({ name }) => name), + ]}::text[]) `; - const [eventsJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_EVENTS_CRON_JOB.name}, - ${GITHUB_EVENTS_CRON_JOB.schedule}, - ${cronHttpPostCommand(URL_NAME)} - ) as "jobId" - `; - const [headRefsJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_HEAD_REFS_CRON_JOB.name}, - ${GITHUB_HEAD_REFS_CRON_JOB.schedule}, - ${cronHttpPostCommand(HEAD_REFS_URL_NAME)} - ) as "jobId" - `; - const [workerJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_WORKER_CRON_JOB.name}, - ${GITHUB_WORKER_CRON_JOB.schedule}, - ${cronHttpPostCommand(WORKER_URL_NAME, GITHUB_WORKER_HTTP_TIMEOUT_MS)} - ) as "jobId" - `; - const [summaryJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_SUMMARY_CRON_JOB.name}, - ${GITHUB_SUMMARY_CRON_JOB.schedule}, - ${cronHttpPostCommand(SUMMARY_URL_NAME, GITHUB_WORKER_HTTP_TIMEOUT_MS)} - ) as "jobId" - `; - const [codexStatsJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${CODEX_STATS_JOB_NAME}, - '7,22,37,52 * * * *', - ${cronHttpPostCommand(CODEX_STATS_URL_NAME, GITHUB_WORKER_HTTP_TIMEOUT_MS)} - ) as "jobId" - `; - if ( - codexStatsJob === undefined || - eventsJob === undefined || - headRefsJob === undefined || - summaryJob === undefined || - workerJob === undefined - ) { - throw new Error("Supabase did not return every scheduled cron job."); + const jobs: { name: string; jobId: number }[] = []; + for (const job of configuredJobs.filter((value) => value.enabled)) { + await upsertVaultSecret(transaction, { + name: job.urlName, + value: job.url, + }); + const [scheduled] = await transaction<{ jobId: number }[]>` + select cron.schedule(${job.name}, ${job.schedule}, ${cronHttpPostCommand(job.urlName, job.timeout)}) as "jobId" + `; + if (scheduled === undefined) { + throw new Error("Supabase did not return the scheduled cron job."); + } + jobs.push({ name: job.name, jobId: scheduled.jobId }); } - return { codexStatsJob, eventsJob, headRefsJob, summaryJob, workerJob }; + return jobs; }); process.stdout.write( - `Configured Supabase cron jobs ${String(jobs.eventsJob.jobId)}, ${String(jobs.headRefsJob.jobId)}, ${String(jobs.workerJob.jobId)}, ${String(jobs.summaryJob.jobId)}, and ${String(jobs.codexStatsJob.jobId)}.\n` + `Configured ${String(jobs.length)} Supabase cron jobs.\n` ); return jobs; } finally { diff --git a/scripts/migrate-production-database.ts b/scripts/migrate-production-database.ts index 2c86631f..67756b68 100644 --- a/scripts/migrate-production-database.ts +++ b/scripts/migrate-production-database.ts @@ -5,6 +5,7 @@ import postgres from "postgres"; import { env } from "../src/env"; +// Keep the historical lock key so overlapping old/new deployments still coordinate. const MIGRATION_LOCK_NAME = "f0rr0.dev:drizzle-migrations"; interface Environment { diff --git a/src/app/(blog)/writing/page.tsx b/src/app/(blog)/writing/page.tsx index 1002d5ff..961b1bc0 100644 --- a/src/app/(blog)/writing/page.tsx +++ b/src/app/(blog)/writing/page.tsx @@ -22,7 +22,7 @@ export const metadata: Metadata = { images: [siteConfig.author.image], locale: siteConfig.locale, siteName: siteConfig.name, - title: "Sid Jain Writing", + title: `${siteConfig.name} Writing`, type: "website", url: publicUrl("/writing"), }, @@ -31,7 +31,7 @@ export const metadata: Metadata = { card: "summary", description, images: [siteConfig.author.image], - title: "Sid Jain Writing", + title: `${siteConfig.name} Writing`, }, }; diff --git a/src/app/(portfolio)/journey/page.tsx b/src/app/(portfolio)/journey/page.tsx index f74e99bd..d00afe42 100644 --- a/src/app/(portfolio)/journey/page.tsx +++ b/src/app/(portfolio)/journey/page.tsx @@ -6,7 +6,7 @@ import { JsonLd } from "@/components/json-ld"; import { SiteMain } from "@/components/site-page"; import { SiteShell } from "@/components/site-shell"; import { resumeData } from "@/content/resume"; -import { publicUrl, siteConfig } from "@/lib/site"; +import { publicUrl, resumePdfUrl, siteConfig } from "@/lib/site"; import { buildProfilePageJsonLd } from "@/lib/structured-data"; const resumeDescription = siteConfig.description; @@ -21,7 +21,7 @@ export const metadata: Metadata = { images: [resumeData.person.image], locale: siteConfig.locale, siteName: siteConfig.name, - title: "Sid Jain Journey", + title: `${siteConfig.name} Journey`, type: "profile", url: publicUrl("/journey"), }, @@ -30,7 +30,7 @@ export const metadata: Metadata = { card: "summary", description: resumeDescription, images: [resumeData.person.image], - title: "Sid Jain Journey", + title: `${siteConfig.name} Journey`, }, }; @@ -52,7 +52,7 @@ export default function JourneyPage() { diff --git a/src/app/(portfolio)/work/page.tsx b/src/app/(portfolio)/work/page.tsx index b0f9c60d..78b5d21b 100644 --- a/src/app/(portfolio)/work/page.tsx +++ b/src/app/(portfolio)/work/page.tsx @@ -6,8 +6,7 @@ import { SiteShell } from "@/components/site-shell"; import { getInitialGitHubActivity } from "@/lib/github-activity-feed"; import { publicUrl, siteConfig } from "@/lib/site"; -const description = - "A day-by-day record of what Sid Jain is building, fixing, and shipping."; +const description = `A day-by-day record of what ${siteConfig.name} is building, fixing, and shipping.`; export const metadata: Metadata = { alternates: { canonical: "/work" }, @@ -17,7 +16,7 @@ export const metadata: Metadata = { images: [siteConfig.author.image], locale: siteConfig.locale, siteName: siteConfig.name, - title: "Sid Jain Work", + title: `${siteConfig.name} Work`, type: "website", url: publicUrl("/work"), }, @@ -26,7 +25,7 @@ export const metadata: Metadata = { card: "summary", description, images: [siteConfig.author.image], - title: "Sid Jain Work", + title: `${siteConfig.name} Work`, }, }; diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index 898ffa92..56c1fe2e 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -1,6 +1,7 @@ import { ImageResponse } from "next/og"; import { resumeData } from "@/content/resume"; +import { siteConfig } from "@/lib/site"; export const alt = `${resumeData.person.name} — ${resumeData.person.role}`; export const size = { @@ -36,7 +37,7 @@ export default function Image() { > {resumeData.person.name} - f0rr0.dev + {new URL(siteConfig.url).hostname}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 7f0b5175..f4338595 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -2,7 +2,7 @@ import type { MetadataRoute } from "next"; import { resumeData } from "@/content/resume"; import { getBlogPosts } from "@/lib/blog-utils"; -import { publicUrl } from "@/lib/site"; +import { publicUrl, resumePdfUrl } from "@/lib/site"; const newestDate = (dates: Date[]) => dates.toSorted((a, b) => b.getTime() - a.getTime()).at(0); @@ -30,7 +30,7 @@ export default async function sitemap(): Promise { }, { lastModified: resumeUpdatedAt, - url: publicUrl("/resume/sid-jain-resume.pdf"), + url: publicUrl(resumePdfUrl), }, { lastModified: latestPostDate, diff --git a/src/components/github-activity-days.tsx b/src/components/github-activity-days.tsx index 8304439a..fd6cc3e2 100644 --- a/src/components/github-activity-days.tsx +++ b/src/components/github-activity-days.tsx @@ -299,7 +299,9 @@ function GitHubActivityDay({ > {formatDate(day.day, "weekday")} - IST + + {WORK_LOG_TIME_ZONE} +
; export type BlogPost = BlogPostEntry & { metadata: BlogPostMetadata; + images?: Record; date: Date; updatedAt?: Date; readingTime: string; @@ -228,7 +229,10 @@ export const getBlogPosts = cache(async (): Promise => { const posts = await Promise.all( entries.map(async ({ slug, importPath }) => { - const mod = await importBlogPostModule<{ metadata: unknown }>(importPath); + const mod = await importBlogPostModule<{ + metadata: unknown; + blogImages?: Record; + }>(importPath); const metadata = parseBlogPostMetadata(mod.metadata); const stats = await getPostStats(importPath); const date = toDate(metadata.date, slug); @@ -241,6 +245,7 @@ export const getBlogPosts = cache(async (): Promise => { date, importPath, metadata, + images: mod.blogImages ?? {}, readingTime: stats.readingTime, slug, updatedAt, diff --git a/src/lib/date.ts b/src/lib/date.ts index bd9caec1..eacd7350 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -1,4 +1,6 @@ -export const WORK_LOG_TIME_ZONE = "Asia/Kolkata"; +import { sitePreferences } from "@/content/site"; + +export const WORK_LOG_TIME_ZONE = sitePreferences.workLogTimeZone; export const dateFormats = { date: { dateStyle: "medium" }, @@ -31,7 +33,10 @@ export const formatDate = ( const key = `${format}:${timeZone}`; const formatter = dateFormatters.get(key) ?? - new Intl.DateTimeFormat("en-US", { ...dateFormats[format], timeZone }); + new Intl.DateTimeFormat(sitePreferences.language, { + ...dateFormats[format], + timeZone, + }); dateFormatters.set(key, formatter); return formatter.format(new Date(value)); }; diff --git a/src/lib/github-accounts.ts b/src/lib/github-accounts.ts new file mode 100644 index 00000000..870344f5 --- /dev/null +++ b/src/lib/github-accounts.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +import { env } from "@/env"; +import { trackedGitHubAccounts } from "@/lib/github-commits-core"; + +const tokensSchema = z.record(z.string(), z.string().trim().min(1)); + +export const githubTokensFrom = ( + value?: string, + accounts: readonly string[] = trackedGitHubAccounts() +): Record => { + if (value === undefined || value.trim() === "") { + return {}; + } + try { + const tokens: Record = {}; + for (const [key, token] of Object.entries( + tokensSchema.parse(JSON.parse(value)) + )) { + const login = key.toLowerCase(); + if (!accounts.includes(login) || Object.hasOwn(tokens, login)) { + throw new TypeError("Unknown or duplicate account."); + } + tokens[login] = token; + } + return tokens; + } catch { + // Parser errors can include credentials; never expose their original details. + throw new TypeError( + "GITHUB_TOKENS must be a JSON object mapping configured GitHub logins to nonempty tokens, without duplicate logins." + ); + } +}; + +export const tokenForGitHubAccount = ( + login: string, + environment: { GITHUB_TOKENS?: string } = env +) => { + const tokens = githubTokensFrom(environment.GITHUB_TOKENS); + const token = Object.hasOwn(tokens, login) ? tokens[login] : undefined; + if (token === undefined) { + throw new Error(`No GitHub token is configured for ${login}.`); + } + return token; +}; + +export const tokensForGitHubAccount = (login?: string) => { + const tokens = githubTokensFrom(env.GITHUB_TOKENS); + return [ + ...new Set( + [ + ...(login === undefined ? [] : [tokens[login]]), + ...Object.values(tokens), + env.GITHUB_TOKEN ?? env.GH_TOKEN, + ].flatMap((value) => + value === undefined || value.trim() === "" ? [] : [value.trim()] + ) + ), + ]; +}; diff --git a/src/lib/github-activity-processor.ts b/src/lib/github-activity-processor.ts index 0956d3a2..f4d5ead9 100644 --- a/src/lib/github-activity-processor.ts +++ b/src/lib/github-activity-processor.ts @@ -1,4 +1,4 @@ -import { env } from "@/env"; +import { tokensForGitHubAccount } from "@/lib/github-accounts"; import { fetchGitHub, GitHubResponseError, @@ -132,6 +132,7 @@ export class ActivityProcessingError extends Error { } export type GitHubGraphQlResponseErrorKind = + | "access_denied" | "invalid_response" | "partial_response" | "rate_limited" @@ -251,10 +252,7 @@ const graphQlErrorIsRateLimited = ( const graphQlErrorIsPermanent = (errors: readonly JsonObject[]) => { const permanentSignals = [ "BAD_USER_INPUT", - "FORBIDDEN", "GRAPHQL_VALIDATION_FAILED", - "NOT_FOUND", - "UNAUTHORIZED", "UNDEFINED_FIELD", ]; const signals = graphQlErrorSignals(errors, false); @@ -368,30 +366,18 @@ const repositoryReferenceFrom = (row: { return repository; }; -const tokenCandidatesFor = (account: TrackedGitHubAccount) => { - const accountToken = - account === "f0rr0" - ? env.GITHUB_F0RR0_TOKEN - : env.GITHUB_YUPPIESTECHDEV_TOKEN; - const otherToken = - account === "f0rr0" - ? env.GITHUB_YUPPIESTECHDEV_TOKEN - : env.GITHUB_F0RR0_TOKEN; - return [ - ...new Set( - [accountToken, otherToken, env.GITHUB_TOKEN].flatMap((value) => { - const token = value?.trim(); - return token === undefined || token.length === 0 ? [] : [token]; - }) - ), - ]; -}; +const githubSourceAccessIsDenied = (error: unknown) => + (error instanceof GitHubResponseError && + [401, 403, 404].includes(error.status) && + !error.retryable) || + (error instanceof GitHubGraphQlResponseError && + error.kind === "access_denied"); const withGitHubTokenCandidate = async ( account: TrackedGitHubAccount, fetcher: (token: string) => Promise ) => { - const tokens = tokenCandidatesFor(account); + const tokens = tokensForGitHubAccount(account); if (tokens.length === 0) { throw new ActivityProcessingError( "source_auth_missing", @@ -404,10 +390,7 @@ const withGitHubTokenCandidate = async ( return await fetcher(token); } catch (error) { lastError = error; - if ( - !(error instanceof GitHubResponseError) || - ![401, 403, 404].includes(error.status) - ) { + if (!githubSourceAccessIsDenied(error)) { throw error; } } @@ -487,6 +470,17 @@ export const githubGraphQlPayloadFrom = async (response: Response) => { retryable: true, }); } + if ( + graphQlErrorSignals(errors, false).some((signal) => + ["FORBIDDEN", "NOT_FOUND", "UNAUTHORIZED"].some((access) => + signal.includes(access) + ) + ) + ) { + throw new GitHubGraphQlResponseError("access_denied", { + retryable: true, + }); + } const retryable = !graphQlErrorIsPermanent(errors); const hasPartialData = Object.hasOwn(value, "data") && value.data !== null; throw new GitHubGraphQlResponseError( @@ -1647,7 +1641,7 @@ export const fetchGitHubAssociatedPullRequests = async ( row: GitHubActivityCommitReference, options: GitHubProviderRequestOptions = {} ) => { - const tokens = tokenCandidatesFor(row.author); + const tokens = tokensForGitHubAccount(row.author); if (tokens.length === 0) { throw new ActivityProcessingError( "source_auth_missing", @@ -1670,10 +1664,7 @@ export const fetchGitHubAssociatedPullRequests = async ( } } catch (error) { lastError = error; - const hiddenFromToken = - error instanceof GitHubResponseError && - [401, 403, 404].includes(error.status) && - !error.retryable; + const hiddenFromToken = githubSourceAccessIsDenied(error); if (!hiddenFromToken) { throw error; } diff --git a/src/lib/github-activity-store.ts b/src/lib/github-activity-store.ts index cf01611c..731d95a2 100644 --- a/src/lib/github-activity-store.ts +++ b/src/lib/github-activity-store.ts @@ -23,6 +23,7 @@ import type { PublicGitHubActivityRepository, PublicGitHubWorkUnitKind, } from "@/lib/github-activity-types"; +import { trackedGitHubUserIds } from "@/lib/github-commits-core"; import { decodeGitHubWorkUnitSummary, GITHUB_WORK_UNIT_SUMMARY_RECIPE, @@ -264,6 +265,10 @@ const readAvailableDays = async ( .where( and( beforeIssue, + inArray( + githubIssues.authorUserId, + Object.values(trackedGitHubUserIds()) + ), inArray(githubRepositories.visibility, [ "public", "private", @@ -371,6 +376,10 @@ const readPublicRows = async ( .where( and( inArray(issueDay, selectedDays), + inArray( + githubIssues.authorUserId, + Object.values(trackedGitHubUserIds()) + ), inArray(githubRepositories.visibility, [ "public", "private", diff --git a/src/lib/github-activity-worker-store.ts b/src/lib/github-activity-worker-store.ts index a6a4af21..eed09550 100644 --- a/src/lib/github-activity-worker-store.ts +++ b/src/lib/github-activity-worker-store.ts @@ -1133,28 +1133,21 @@ export const completeGitHubPullRequestDiscovery = async ( now ); } - await transaction - .delete(githubCommitPullRequestAssociations) - .where( - and( - eq( - githubCommitPullRequestAssociations.commitRepositoryId, - commit.repositoryId - ), - eq(githubCommitPullRequestAssociations.commitSha, commit.sha) - ) - ); + // Discovery is limited by credential visibility. Absence does not revoke prior evidence. const associatedPullRequestNodeIds = [ ...new Set(pullRequests.map(({ nodeId }) => nodeId)), ]; if (associatedPullRequestNodeIds.length > 0) { - await transaction.insert(githubCommitPullRequestAssociations).values( - associatedPullRequestNodeIds.map((pullRequestNodeId) => ({ - commitRepositoryId: commit.repositoryId, - commitSha: commit.sha, - pullRequestNodeId, - })) - ); + await transaction + .insert(githubCommitPullRequestAssociations) + .values( + associatedPullRequestNodeIds.map((pullRequestNodeId) => ({ + commitRepositoryId: commit.repositoryId, + commitSha: commit.sha, + pullRequestNodeId, + })) + ) + .onConflictDoNothing(); } const [completed] = await transaction .update(githubCommits) @@ -1270,7 +1263,7 @@ export const claimDueGitHubPullRequests = async ( .returning({ nodeId: githubPullRequests.nodeId }); if (updated !== undefined) { claimed.push({ - account: candidate.account as TrackedGitHubAccount, + account: candidate.account, attemptCount: candidate.attemptCount + 1, createdAt: candidate.createdAt, lastReconciledAt: candidate.lastReconciledAt, diff --git a/src/lib/github-activity-worker.ts b/src/lib/github-activity-worker.ts index c8acab0a..9b824b14 100644 --- a/src/lib/github-activity-worker.ts +++ b/src/lib/github-activity-worker.ts @@ -1,3 +1,4 @@ +import { tokensForGitHubAccount } from "@/lib/github-accounts"; import { ActivityProcessingError, fetchGitHubActivityCommitSource, @@ -54,7 +55,7 @@ import { } from "@/lib/github-api"; import { repositoryIdFrom, - TRACKED_GITHUB_ACCOUNTS, + trackedGitHubAccounts, } from "@/lib/github-commits-core"; import type { TrackedGitHubAccount } from "@/lib/github-commits-core"; import { @@ -78,10 +79,9 @@ import { reconcileGitHubWorkUnitSummaryStatus } from "@/lib/github-work-unit-sum const DEFAULT_WORKER_MAXIMUM_DURATION_MS = 90_000; const MAXIMUM_PUBLICATION_RESERVE_MS = 30_000; const MINIMUM_FACTUAL_PROCESSING_MS = 8000; -const TERMINAL_GITHUB_STATUSES = new Set([403, 404, 410, 422]); +const TERMINAL_GITHUB_STATUSES = new Set([410, 422]); const TERMINAL_ACTIVITY_PROCESSING_CODES = new Set([ "membership_incomplete", - "source_auth_missing", "source_incomplete", "source_invalid", "source_unavailable", @@ -161,7 +161,10 @@ const processRefRepairs = async ( const claimed = await claimGitHubRefRepairs({ limit, now: new Date() }); result.claimed = claimed.length; for (const repair of claimed) { - if (context.deadlineReached()) { + if ( + context.deadlineReached() || + (repair.active && !context.hasCredentials) + ) { await releaseGitHubRefRepair(repair); result.deferred += 1; continue; @@ -222,7 +225,7 @@ export const githubActivityFailureIsTerminal = ( }; const activeTrackedAccounts = async ( - requestedAccounts: readonly TrackedGitHubAccount[] = TRACKED_GITHUB_ACCOUNTS + requestedAccounts: readonly TrackedGitHubAccount[] = trackedGitHubAccounts() ) => { const accounts: TrackedGitHubAccount[] = []; for (const account of requestedAccounts) { @@ -240,6 +243,7 @@ const observedSinceLastReconciliation = (due: DueGitHubPullRequest) => due.versionObservedAt > due.lastReconciledAt); interface WorkerContext { + hasCredentials: boolean; activeAccounts: readonly TrackedGitHubAccount[]; deadlineAt: number; deadlineReached: () => boolean; @@ -593,11 +597,11 @@ const processPullRequests = async ( const checkedWorkerAccounts = ( accounts: readonly TrackedGitHubAccount[] | undefined ) => { - const requested = accounts ?? TRACKED_GITHUB_ACCOUNTS; + const requested = accounts ?? trackedGitHubAccounts(); if ( requested.length === 0 || new Set(requested).size !== requested.length || - requested.some((account) => !TRACKED_GITHUB_ACCOUNTS.includes(account)) + requested.some((account) => !trackedGitHubAccounts().includes(account)) ) { throw new RangeError( "The GitHub activity worker account scope is invalid." @@ -672,6 +676,7 @@ export const runGitHubActivityWorker = async ( const refs = emptyStageResult(); const activeAccounts = await activeTrackedAccounts(requestedAccounts); const context: WorkerContext = { + hasCredentials: tokensForGitHubAccount().length > 0, activeAccounts, deadlineAt: startedAt + processingDurationMs, deadlineReached, @@ -679,22 +684,26 @@ export const runGitHubActivityWorker = async ( }; const overallDeadlineReached = () => workerDeadlineReached(startedAt, maximumDurationMs); - await processObservations(context, observationLimit, observations); - await processPullRequestSignals( - context, - pullRequestSignalLimit, - pullRequestSignals - ); + if (context.hasCredentials) { + await processObservations(context, observationLimit, observations); + await processPullRequestSignals( + context, + pullRequestSignalLimit, + pullRequestSignals + ); + } if (options.includeRefs !== false) { await processRefRepairs(context, refLimit, refs); } - await processCommits(context, commitLimit, commits); - await processPullRequestDiscovery( - context, - pullRequestDiscoveryLimit, - pullRequestDiscovery - ); - await processPullRequests(context, pullRequestLimit, pullRequests); + if (context.hasCredentials) { + await processCommits(context, commitLimit, commits); + await processPullRequestDiscovery( + context, + pullRequestDiscoveryLimit, + pullRequestDiscovery + ); + await processPullRequests(context, pullRequestLimit, pullRequests); + } const projection = options.includeProjection !== false && (await ensureGitHubWorkUnitProjectionRequest()) !== null diff --git a/src/lib/github-api.ts b/src/lib/github-api.ts index 4f2b5367..b1232a80 100644 --- a/src/lib/github-api.ts +++ b/src/lib/github-api.ts @@ -148,7 +148,7 @@ const isRateLimited = async (response: Response) => (await hasSecondaryRateLimitEvidence(response)))); const readDefaultGitHubToken = () => { - const token = env.GITHUB_TOKEN?.trim() ?? env.GITHUB_F0RR0_TOKEN?.trim(); + const token = env.GITHUB_TOKEN?.trim() ?? env.GH_TOKEN?.trim(); return token === undefined || token.length === 0 ? null : token; }; @@ -286,7 +286,7 @@ export const fetchGitHub = async ( ? {} : { "If-None-Match": options.ifNoneMatch }), ...(method === "POST" ? { "Content-Type": "application/json" } : {}), - "User-Agent": "f0rr0.dev", + "User-Agent": "Figment", "X-GitHub-Api-Version": GITHUB_API_VERSION, }, method, diff --git a/src/lib/github-backfill-core.ts b/src/lib/github-backfill-core.ts index 23b8f182..8598c4b5 100644 --- a/src/lib/github-backfill-core.ts +++ b/src/lib/github-backfill-core.ts @@ -1,7 +1,7 @@ import type { GitHubFactualWorkerBacklog } from "@/lib/github-backfill-store"; import { repositoryIdFrom, - TRACKED_GITHUB_ACCOUNTS, + trackedGitHubAccounts, trackedGitHubAccountFrom, } from "@/lib/github-commits-core"; import type { TrackedGitHubAccount } from "@/lib/github-commits-core"; @@ -85,7 +85,7 @@ export const githubBackfillRequestFrom = ( return null; } const accounts = - account === "all" ? TRACKED_GITHUB_ACCOUNTS : ([account] as const); + account === "all" ? trackedGitHubAccounts() : ([account] as const); return { accounts, endDate: endDay.toISOString().slice(0, 10), diff --git a/src/lib/github-commits-core.ts b/src/lib/github-commits-core.ts index e390ad3d..a6a085ec 100644 --- a/src/lib/github-commits-core.ts +++ b/src/lib/github-commits-core.ts @@ -1,3 +1,10 @@ +import { githubAccounts } from "@/content/site"; + +export const trackedGitHubAccounts = () => + githubAccounts.map(({ login }) => login); +export const trackedGitHubUserIds = (): Record => + Object.fromEntries(githubAccounts.map(({ login, id }) => [login, id])); + const COMMIT_SHA = /^[a-f0-9]{40}$/; const EVENT_ID = /^\d{1,64}$/; const GITHUB_DELIVERY_ID = @@ -8,14 +15,7 @@ const REPOSITORY_FULL_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?\/[A-Za-z0-9._-]{1,100}$/; const ZERO_SHA = "0".repeat(40); -export const TRACKED_GITHUB_ACCOUNTS = ["f0rr0", "yuppiestechdev"] as const; - -export type TrackedGitHubAccount = (typeof TRACKED_GITHUB_ACCOUNTS)[number]; - -export const TRACKED_GITHUB_USER_IDS = { - f0rr0: "8574219", - yuppiestechdev: "99666891", -} as const satisfies Record; +export type TrackedGitHubAccount = string; type JsonObject = Record; @@ -157,7 +157,7 @@ export const githubCommitReferenceValuesFrom = ( firstObservedAt: Date ) => ({ author: commit.author, - authorUserId: TRACKED_GITHUB_USER_IDS[commit.author], + authorUserId: trackedGitHubUserIds()[commit.author], committedAt: new Date(commit.committedAt), firstObservedAt, message: commit.message, @@ -310,7 +310,7 @@ export const trackedGitHubAccountFrom = ( value: unknown ): TrackedGitHubAccount | null => { const login = normalizedText(value, 39)?.toLowerCase(); - return TRACKED_GITHUB_ACCOUNTS.find((account) => account === login) ?? null; + return trackedGitHubAccounts().find((account) => account === login) ?? null; }; export const trackedGitHubAccountFromUserId = ( @@ -319,8 +319,8 @@ export const trackedGitHubAccountFromUserId = ( const userId = repositoryIdFrom(value); return userId === null ? null - : (TRACKED_GITHUB_ACCOUNTS.find( - (account) => TRACKED_GITHUB_USER_IDS[account] === userId + : (trackedGitHubAccounts().find( + (account) => trackedGitHubUserIds()[account] === userId ) ?? null); }; diff --git a/src/lib/github-commits.ts b/src/lib/github-commits.ts index b2fdd0eb..7b7a3d3d 100644 --- a/src/lib/github-commits.ts +++ b/src/lib/github-commits.ts @@ -1,5 +1,6 @@ import { DatabaseConfigurationError, isDatabaseConfigured } from "@/db/client"; import { env } from "@/env"; +import { githubTokensFrom, tokenForGitHubAccount } from "@/lib/github-accounts"; import { fetchGitHub, githubApiUrl, @@ -10,7 +11,6 @@ import { import { authenticatedGitHubAccountFrom, githubEventFrom, - TRACKED_GITHUB_ACCOUNTS, } from "@/lib/github-commits-core"; import type { GitHubEvent, @@ -26,10 +26,6 @@ import { import type { GitHubRepositoryRefKind } from "@/lib/github-commits-store"; import { reconcileGitHubRepositoryRefBatch } from "@/lib/github-ref-reconciliation-batch"; -const ACCOUNT_TOKEN_VARIABLES = { - f0rr0: "GITHUB_F0RR0_TOKEN", - yuppiestechdev: "GITHUB_YUPPIESTECHDEV_TOKEN", -} as const satisfies Record; const CHECKPOINT_ATTEMPTS = 3; const EVENT_PAGES = 3; const GITHUB_PAGE_SIZE = 100; @@ -93,22 +89,6 @@ export interface GitHubRefReconciliationResult { repositories: number; } -class GitHubSyncConfigurationError extends Error { - constructor(variable: string) { - super(`${variable} is not configured.`); - this.name = "GitHubSyncConfigurationError"; - } -} - -const tokenFor = (account: TrackedGitHubAccount) => { - const variable = ACCOUNT_TOKEN_VARIABLES[account]; - const token = env[variable]?.trim(); - if (token === undefined || token.length === 0) { - throw new GitHubSyncConfigurationError(variable); - } - return token; -}; - interface GitHubCronRequestOptions { deadlineAt?: number; } @@ -119,7 +99,8 @@ const settleTrackedGitHubAccounts = async ( failedAccounts: readonly FailedGitHubAccount[]; results: readonly Result[]; }> => { - const settled = await Promise.allSettled(TRACKED_GITHUB_ACCOUNTS.map(action)); + const accounts = Object.keys(githubTokensFrom(env.GITHUB_TOKENS)); + const settled = await Promise.allSettled(accounts.map(action)); const results: Result[] = []; const failedAccounts: FailedGitHubAccount[] = []; @@ -128,7 +109,7 @@ const settleTrackedGitHubAccounts = async ( results.push(outcome.value); continue; } - const account = TRACKED_GITHUB_ACCOUNTS[index]; + const account = accounts[index]; if (account === undefined) { throw new Error("A GitHub account result has no tracked account."); } @@ -162,10 +143,15 @@ export const assertGitHubTokenIdentity = async ( options: GitHubCronRequestOptions = {} ) => { const { payload } = await fetchJson(githubApiUrl("/user"), token, options); - if (authenticatedGitHubAccountFrom(payload) !== account) { - throw new Error( - `${ACCOUNT_TOKEN_VARIABLES[account]} is not authenticated as ${account}.` - ); + if ( + authenticatedGitHubAccountFrom(payload) !== account || + typeof payload !== "object" || + payload === null || + !("login" in payload) || + typeof payload.login !== "string" || + payload.login.toLowerCase() !== account + ) { + throw new Error(`The GitHub token is not authenticated as ${account}.`); } }; @@ -314,7 +300,7 @@ export const syncGitHubAccount = async ( }; } if (token === null) { - token = tokenFor(account); + token = tokenForGitHubAccount(account); await assertGitHubTokenIdentity(account, token, options); } const collected = await collectGitHubEvents( @@ -417,7 +403,7 @@ export const reconcileGitHubAccountRefs = async ( repositories: 0, }; } - const token = tokenFor(account); + const token = tokenForGitHubAccount(account); await assertGitHubTokenIdentity(account, token, options); return { account, diff --git a/src/lib/github-profile.ts b/src/lib/github-profile.ts index 49ce7e52..2bacc7b0 100644 --- a/src/lib/github-profile.ts +++ b/src/lib/github-profile.ts @@ -1,6 +1,7 @@ -import "server-only"; import { unstable_cache } from "next/cache"; +import "server-only"; +import { primaryGitHubProfile } from "@/content/resume"; import { fetchGitHub, githubApiUrl } from "@/lib/github-api"; import { createUnavailableGitHubProfile, @@ -10,7 +11,7 @@ import type { GitHubProfile } from "@/lib/github-profile-core"; export type { GitHubProfile, GitHubProject } from "@/lib/github-profile-core"; -const GITHUB_LOGIN = "f0rr0"; +const GITHUB_LOGIN = primaryGitHubProfile.username; const GITHUB_CACHE_SECONDS = 60 * 60 * 12; const fetchGitHubProfile = async (): Promise => { diff --git a/src/lib/github-ref-membership-store.ts b/src/lib/github-ref-membership-store.ts index 4b9a6f59..8e0d84fb 100644 --- a/src/lib/github-ref-membership-store.ts +++ b/src/lib/github-ref-membership-store.ts @@ -29,7 +29,7 @@ import type { } from "@/lib/github-activity-processor"; import { githubActivityRetryAt } from "@/lib/github-activity-worker-core"; import { - TRACKED_GITHUB_USER_IDS, + trackedGitHubUserIds, trackedGitHubAccountFrom, } from "@/lib/github-commits-core"; import type { TrackedGitHubAccount } from "@/lib/github-commits-core"; @@ -409,7 +409,7 @@ export const validateGitHubRefRepairSource = ( const trackedShas = new Set(); for (const commit of source.commits) { if ( - !Object.hasOwn(TRACKED_GITHUB_USER_IDS, commit.author) || + !Object.hasOwn(trackedGitHubUserIds(), commit.author) || commit.repositoryId !== repair.repositoryId || commit.repository !== repair.repository || !reachableShas.has(commit.sha) || @@ -470,7 +470,7 @@ export const completeGitHubRefRepair = async ( .values( source.commits.map((commit) => ({ author: commit.author, - authorUserId: TRACKED_GITHUB_USER_IDS[commit.author], + authorUserId: trackedGitHubUserIds()[commit.author], committedAt: new Date(commit.committedAt), firstObservedAt: repair.observedAt, message: commit.message, diff --git a/src/lib/github-repository-inventory.ts b/src/lib/github-repository-inventory.ts index 3e5cbf73..8b7ab4b7 100644 --- a/src/lib/github-repository-inventory.ts +++ b/src/lib/github-repository-inventory.ts @@ -17,7 +17,7 @@ import { githubRepositoryInventoryHeads, githubRepositoryRefs, } from "@/db/schema"; -import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; +import { trackedGitHubUserIds } from "@/lib/github-commits-core"; import type { GitHubRepositoryInventoryFacts, TrackedGitHubAccount, @@ -55,7 +55,7 @@ const claimGitHubRepositoryInventoryRefresh = async (input: { force: boolean; now: Date; }): Promise => { - const accountUserId = TRACKED_GITHUB_USER_IDS[input.account]; + const accountUserId = trackedGitHubUserIds()[input.account]; const staleBefore = new Date( input.now.getTime() - INVENTORY_REFRESH_INTERVAL_MS ); @@ -236,7 +236,7 @@ const publishGitHubRepositoryInventory = async ( const readCurrentGitHubRepositoryInventory = async ( account: TrackedGitHubAccount ): Promise => { - const accountUserId = TRACKED_GITHUB_USER_IDS[account]; + const accountUserId = trackedGitHubUserIds()[account]; return await getDatabase().transaction( async (transaction) => { const [head] = await transaction diff --git a/src/lib/github-work-unit-projection-state.ts b/src/lib/github-work-unit-projection-state.ts index 54c2df9f..642a2b20 100644 --- a/src/lib/github-work-unit-projection-state.ts +++ b/src/lib/github-work-unit-projection-state.ts @@ -4,6 +4,7 @@ import { and, eq, sql } from "drizzle-orm"; import { getDatabase } from "@/db/client"; import { githubPublicFeedHead } from "@/db/schema"; +import { trackedGitHubUserIds } from "@/lib/github-commits-core"; import { GITHUB_WORK_UNIT_SUMMARY_POLICY_DIGEST } from "@/lib/github-work-unit-summary"; type Database = ReturnType; @@ -18,6 +19,7 @@ const PROJECTION_POLICY = const PIPELINE_POLICY_DIGEST = createHash("sha256") .update( JSON.stringify({ + authors: Object.values(trackedGitHubUserIds()).toSorted(), projection: PROJECTION_POLICY, summary: GITHUB_WORK_UNIT_SUMMARY_POLICY_DIGEST, }) @@ -69,11 +71,18 @@ export const ensureGitHubWorkUnitProjectionRequest = async () => export const completeGitHubWorkUnitProjectionRequest = async ( token: string ) => { + // Author changes can alter issue-only pages even when no work unit changes. + const policyChanged = sql`${githubPublicFeedHead.summaryPolicyDigest} IS DISTINCT FROM ${PIPELINE_POLICY_DIGEST}`; + const revisionIncrement = sql`CASE WHEN ${policyChanged} THEN 1 ELSE 0 END`; const [cleared] = await getDatabase() .update(githubPublicFeedHead) .set({ projectionRequestToken: null, summaryPolicyDigest: PIPELINE_POLICY_DIGEST, + feedRevision: sql`${githubPublicFeedHead.feedRevision} + ${revisionIncrement}`, + headContentRevision: sql`${githubPublicFeedHead.headContentRevision} + ${revisionIncrement}`, + orderingRevision: sql`${githubPublicFeedHead.orderingRevision} + ${revisionIncrement}`, + lastPublishedAt: sql`CASE WHEN ${policyChanged} THEN now() ELSE ${githubPublicFeedHead.lastPublishedAt} END`, }) .where( and( diff --git a/src/lib/github-work-unit-store.ts b/src/lib/github-work-unit-store.ts index 7f65282e..4b4db605 100644 --- a/src/lib/github-work-unit-store.ts +++ b/src/lib/github-work-unit-store.ts @@ -23,7 +23,7 @@ import type { GitHubLanguageFact, GitHubWorkUnitFileFact, } from "@/lib/github-change-evidence"; -import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; +import { trackedGitHubUserIds } from "@/lib/github-commits-core"; import { chooseEffectivePullRequest, githubLogicalChangeKey, @@ -63,10 +63,6 @@ const SUMMARY_DEBOUNCE_MS = 5 * 60 * 1000; const DIGEST = /^[a-f0-9]{64}$/u; const SHA = /^[a-f0-9]{40}$/u; -const trackedAuthorUserIds = new Set( - Object.values(TRACKED_GITHUB_USER_IDS) -); - type GitHubWorkUnitDatabase = ReturnType; type GitHubWorkUnitTransaction = Parameters< Parameters[0] @@ -775,6 +771,7 @@ const loadProjectionSnapshot = async ( transaction: GitHubWorkUnitTransaction, { lockCurrentUnits, summaryEvaluationLimit }: ProjectionSnapshotOptions ): Promise => { + const trackedAuthorUserIds = new Set(Object.values(trackedGitHubUserIds())); const currentUnits = await readCurrentUnits(transaction, lockCurrentUnits); const repositoryRows = await transaction .select({ @@ -1278,7 +1275,14 @@ const loadProjectionSnapshot = async ( eq(githubIssues.repositoryId, githubRepositories.id) ) .where( - inArray(githubRepositories.visibility, ["public", "private", "internal"]) + and( + inArray(githubIssues.authorUserId, [...trackedAuthorUserIds]), + inArray(githubRepositories.visibility, [ + "public", + "private", + "internal", + ]) + ) ); const issueDays = issueRows.map((issue) => issueDayFrom(issue.createdAt)); return { diff --git a/src/lib/github-work-unit-summary-store.ts b/src/lib/github-work-unit-summary-store.ts index 2df1da27..e1b63c09 100644 --- a/src/lib/github-work-unit-summary-store.ts +++ b/src/lib/github-work-unit-summary-store.ts @@ -26,6 +26,7 @@ import { } from "@/db/schema"; import { env } from "@/env"; import { PUBLIC_GITHUB_ACTIVITY_DAY_PAGE_SIZE } from "@/lib/github-activity-store"; +import { trackedGitHubUserIds } from "@/lib/github-commits-core"; import { GITHUB_SUMMARY_REQUEST_BUDGET } from "@/lib/github-cron-config"; import { acquireGitHubWorkUnitProjectionLock } from "@/lib/github-work-unit-projection-state"; import { @@ -644,11 +645,17 @@ async function readInitialPageDays(transaction: SummaryTransaction) { eq(githubIssues.repositoryId, githubRepositories.id) ) .where( - inArray(githubRepositories.visibility, [ - "public", - "private", - "internal", - ]) + and( + inArray( + githubIssues.authorUserId, + Object.values(trackedGitHubUserIds()) + ), + inArray(githubRepositories.visibility, [ + "public", + "private", + "internal", + ]) + ) ) .orderBy(desc(issueDay)) .limit(PUBLIC_GITHUB_ACTIVITY_DAY_PAGE_SIZE), diff --git a/src/lib/remark-static-image-imports.mjs b/src/lib/remark-static-image-imports.mjs index 73635836..80b81c96 100644 --- a/src/lib/remark-static-image-imports.mjs +++ b/src/lib/remark-static-image-imports.mjs @@ -153,6 +153,57 @@ const transformJsxImage = (node, imports, importAliases, counterRef) => { srcAttribute.value = createIdentifierExpression(identifier); }; +const createImageManifest = (aliases) => ({ + type: "mdxjsEsm", + value: "export const blogImages = {};", + data: { + estree: { + type: "Program", + sourceType: "module", + body: [ + { + type: "ExportNamedDeclaration", + specifiers: [], + source: null, + declaration: { + type: "VariableDeclaration", + kind: "const", + declarations: [ + { + type: "VariableDeclarator", + id: { type: "Identifier", name: "blogImages" }, + init: { + type: "ObjectExpression", + properties: [...aliases].map(([url, identifier]) => ({ + type: "Property", + kind: "init", + method: false, + shorthand: false, + computed: false, + key: { + type: "Literal", + value: url.startsWith("@/../public") + ? url.slice("@/../public".length) + : url, + }, + value: { + type: "MemberExpression", + computed: false, + optional: false, + object: { type: "Identifier", name: identifier }, + property: { type: "Identifier", name: "src" }, + }, + })), + }, + }, + ], + }, + }, + ], + }, + }, +}); + const remarkStaticImageImports = () => (tree) => { const imports = []; const importAliases = new Map(); @@ -188,7 +239,11 @@ const remarkStaticImageImports = () => (tree) => { }); if (imports.length > 0 && Array.isArray(tree.children)) { - tree.children = [...imports, ...tree.children]; + tree.children = [ + ...imports, + createImageManifest(importAliases), + ...tree.children, + ]; } }; diff --git a/src/lib/resume.ts b/src/lib/resume.ts index 5025033d..3804b61e 100644 --- a/src/lib/resume.ts +++ b/src/lib/resume.ts @@ -1,11 +1,12 @@ import { resumeCompanyStageLabels, resumeData, + socialProfiles, resumeRoleMarkerLabels, } from "@/content/resume"; import type { PublicReference, ResumeRole } from "@/content/resume"; import type { BlogPost } from "@/lib/blog-utils"; -import { publicUrl } from "@/lib/site"; +import { publicUrl, resumePdfUrl } from "@/lib/site"; export interface AskAgentAction { description: string; @@ -112,7 +113,7 @@ const buildAskAboutMePrompt = () => { `Start at ${contextUrl} and follow the relevant links for context about ${resumeData.person.name}.`, "This is an informational research chat, not a code-editing task.", `I want to ask questions about ${resumeData.person.name}'s work, technical depth, projects, and fit for roles such as ${resumeData.person.targetPositioning}.`, - "Use his résumé and linked work as sources for your answers.", + "Use the résumé and linked work as sources for your answers.", ].join(" "); }; @@ -145,29 +146,9 @@ export const buildJsonResume = () => ({ email: resumeData.person.email, image: publicUrl(resumeData.person.image), label: currentRole?.title ?? resumeData.person.role, - location: { - city: "Mumbai", - countryCode: "IN", - region: "Maharashtra", - }, + location: resumeData.person.address, name: resumeData.person.name, - profiles: [ - { - network: "LinkedIn", - url: "https://linkedin.com/in/f0rr0", - username: "f0rr0", - }, - { - network: "GitHub", - url: "https://github.com/f0rr0", - username: "f0rr0", - }, - { - network: "GitHub", - url: "https://github.com/yuppiestechdev", - username: "yuppiestechdev", - }, - ], + profiles: socialProfiles, summary: resumeData.summary, url: publicUrl("/"), }, @@ -276,12 +257,9 @@ export const buildLlmsTxt = ( - [Detailed career context](${localProfileUrl("/llms-full.txt")}): Full work history, engineering decisions, leadership scope, client engagements, and source links. Read for technical interviews or role-fit questions. - [Journey](${localProfileUrl("/journey")}): Human-readable experience and education. -## Namefi Work +## Selected References -${resumeData.machineReadable.publicReferences - .filter((reference) => new URL(reference.href).hostname === "namefi.io") - .map(markdownLink) - .join("\n")} +${resumeData.machineReadable.publicReferences.map(markdownLink).join("\n")} ## Code and Technical Writing @@ -293,7 +271,7 @@ ${resumeData.links.map((link) => `- [${link.label}](${link.href})`).join("\n")} ## Optional -- [PDF résumé](${localProfileUrl("/resume/sid-jain-resume.pdf")}): Downloadable résumé. +- [PDF résumé](${localProfileUrl(resumePdfUrl)}): Downloadable résumé. - [Work](${localProfileUrl("/work")}): Recent code activity. - [Writing](${localProfileUrl("/writing")}): All published articles; each article is also available at /writing/{slug}.md. - [RSS](${localProfileUrl("/rss.xml")}): Article feed. @@ -319,29 +297,19 @@ export const buildLlmsFullTxt = (blogPosts: BlogPost[] = []) => { note: "Structured experience and skills.", }, { - href: localProfileUrl("/resume/sid-jain-resume.pdf"), + href: localProfileUrl(resumePdfUrl), label: "PDF Resume", note: "Downloadable résumé.", }, - { - href: "https://linkedin.com/in/f0rr0", - label: "LinkedIn", - note: "Professional profile.", - }, - { - href: "https://github.com/f0rr0", - label: "GitHub: f0rr0", - note: "Open-source projects.", - }, - { - href: "https://github.com/yuppiestechdev", - label: "GitHub: yuppiestechdev", - note: "Additional engineering work.", - }, + ...socialProfiles.map(({ network, username, url }) => ({ + href: url, + label: `${network}: ${username}`, + note: "Public profile.", + })), { href: `mailto:${resumeData.person.email}`, label: "Email", - note: "Contact Sid.", + note: `Contact ${resumeData.person.name}.`, }, ]; diff --git a/src/lib/site-url.ts b/src/lib/site-url.ts index ebc19627..3a15f7c1 100644 --- a/src/lib/site-url.ts +++ b/src/lib/site-url.ts @@ -1 +1,53 @@ -export const CANONICAL_SITE_URL = "https://f0rr0.dev"; +interface SiteEnvironment { + VERCEL?: string; + VERCEL_PROJECT_PRODUCTION_URL?: string; + PORT?: string; + NEXT_PUBLIC_PORT?: string; +} + +export const productionSiteOrigin = (hostname: string | undefined) => { + const host = hostname?.trim(); + if (host === undefined || host.length === 0) { + throw new Error( + "VERCEL_PROJECT_PRODUCTION_URL is required for production URLs." + ); + } + const url = new URL(`https://${host}`); + if ( + url.host !== host || + url.username || + url.password || + url.pathname !== "/" || + url.search || + url.hash || + url.hostname === "localhost" || + url.hostname.endsWith(".localhost") + ) { + throw new Error( + "VERCEL_PROJECT_PRODUCTION_URL must be a production hostname." + ); + } + return url.origin; +}; + +export const siteOriginFrom = (environment: SiteEnvironment) => { + if ( + (environment.VERCEL_PROJECT_PRODUCTION_URL?.trim().length ?? 0) > 0 || + environment.VERCEL === "1" + ) { + return productionSiteOrigin(environment.VERCEL_PROJECT_PRODUCTION_URL); + } + return new URL( + `http://localhost:${(environment.PORT?.trim() ?? "") || (environment.NEXT_PUBLIC_PORT?.trim() ?? "") || "3000"}` + ).origin; +}; + +// Next config supplies this public value to browser bundles. Scripts use Vercel's original variable. +export const CANONICAL_SITE_URL = + process.env.NEXT_PUBLIC_SITE_ORIGIN ?? + siteOriginFrom({ + VERCEL: process.env.VERCEL, + VERCEL_PROJECT_PRODUCTION_URL: process.env.VERCEL_PROJECT_PRODUCTION_URL, + PORT: process.env.PORT, + NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT, + }); diff --git a/src/lib/site.ts b/src/lib/site.ts index c7f3b549..c2ce916a 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -1,21 +1,24 @@ -import { resumeData } from "@/content/resume"; +import { primaryGitHubProfile, resumeData } from "@/content/resume"; +import { sitePreferences } from "@/content/site"; import { CANONICAL_SITE_URL } from "@/lib/site-url"; export const siteConfig = { author: { bio: resumeData.summary, - handle: "f0rr0", - image: "/resume/sid-jain-profile.png", + handle: primaryGitHubProfile.username, + image: resumeData.person.image, name: resumeData.person.name, role: resumeData.person.role, }, description: `${resumeData.person.name}. ${resumeData.summary}`, - language: "en-US", - locale: "en_US", - name: "Sid Jain", - shortName: "F0RR0", + language: sitePreferences.language, + locale: sitePreferences.language.replace("-", "_"), + name: resumeData.person.name, + shortName: primaryGitHubProfile.username.toUpperCase(), url: CANONICAL_SITE_URL, }; export const publicUrl = (path: string) => new URL(path, CANONICAL_SITE_URL).toString(); + +export const resumePdfUrl = resumeData.pdf.outputPath.replace(/^public\//, "/"); diff --git a/src/lib/structured-data.ts b/src/lib/structured-data.ts index 09afcefd..cdcca21b 100644 --- a/src/lib/structured-data.ts +++ b/src/lib/structured-data.ts @@ -10,18 +10,13 @@ import type { WithContext, } from "schema-dts"; -import { resumeData } from "@/content/resume"; +import { resumeData, socialProfiles } from "@/content/resume"; import type { BlogPost } from "@/lib/blog-utils"; import { publicUrl, siteConfig } from "@/lib/site"; -const linkedInUrl = "https://linkedin.com/in/f0rr0"; -const githubUrl = "https://github.com/f0rr0"; -const yuppiesGithubUrl = "https://github.com/yuppiestechdev"; - -const personId = () => publicUrl("/#sid-jain"); +const personId = () => publicUrl("/#person"); const websiteId = () => publicUrl("/#website"); - -const sameAs = [linkedInUrl, githubUrl, yuppiesGithubUrl]; +const sameAs = socialProfiles.map(({ url }) => url); const [currentExperience] = resumeData.experience; const [currentRole] = currentExperience?.roles ?? []; const currentCompanyReference = @@ -33,18 +28,11 @@ const buildPersonNode = (): Person => ({ "@id": personId(), "@type": "Person", alternateName: resumeData.person.alternateNames, - alumniOf: [ - { - "@type": "CollegeOrUniversity", - name: "University of California, Los Angeles", - sameAs: "https://www.ucla.edu/", - }, - { - "@type": "EducationalOrganization", - name: "Delhi Public School, R. K. Puram", - sameAs: "https://dpsrkp.net/", - }, - ], + alumniOf: resumeData.education.map((school) => ({ + "@type": "EducationalOrganization" as const, + name: school.company, + sameAs: school.url, + })), description: resumeData.summary, email: `mailto:${resumeData.person.email}`, image: publicUrl(resumeData.person.image), @@ -68,14 +56,14 @@ const buildPersonNode = (): Person => ({ const buildWebsiteNode = (): WebSite => ({ "@id": websiteId(), "@type": "WebSite", - alternateName: "F0RR0", + alternateName: siteConfig.shortName, author: { "@id": personId(), "@type": "Person", name: resumeData.person.name, }, description: siteConfig.description, - inLanguage: "en-US", + inLanguage: siteConfig.language, name: resumeData.person.name, publisher: { "@id": personId(), @@ -103,7 +91,7 @@ export const buildProfilePageJsonLd = (): WithContext => ({ url: publicUrl("/"), }, mainEntity: buildPersonNode(), - name: "Sid Jain Journey", + name: `${siteConfig.name} Journey`, url: publicUrl("/journey"), }); @@ -130,7 +118,7 @@ export const buildBlogPostingJsonLd = ({ datePublished: post.date.toISOString(), description: post.metadata.summary, headline: post.metadata.title, - inLanguage: "en-US", + inLanguage: siteConfig.language, isPartOf: { "@id": websiteId(), "@type": "WebSite", @@ -164,7 +152,7 @@ export const buildBlogCollectionJsonLd = ( "@id": publicUrl("/writing#collection"), "@type": "CollectionPage", description: `Notes on what ${resumeData.person.name} is building across product design, engineering, AI, and creative development.`, - inLanguage: "en-US", + inLanguage: siteConfig.language, isPartOf: { "@id": websiteId(), "@type": "WebSite", @@ -189,6 +177,6 @@ export const buildBlogCollectionJsonLd = ( ), numberOfItems: posts.length, } satisfies ItemList, - name: "Sid Jain Writing", + name: `${siteConfig.name} Writing`, url: publicUrl("/writing"), }); diff --git a/src/proxy.ts b/src/proxy.ts index d08babbd..72e3c8ab 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; +import { env } from "@/env"; + export function proxy(request: NextRequest) { const url = new URL(request.url); if (url.pathname !== "/_r7k2" && !url.pathname.startsWith("/_r7k2/")) { @@ -12,10 +14,11 @@ export function proxy(request: NextRequest) { } url.pathname = url.pathname.slice("/_r7k2".length) || "/"; + const region = env.NEXT_PUBLIC_POSTHOG_REGION ?? "us"; url.hostname = url.pathname.startsWith("/static/") || url.pathname.startsWith("/array/") - ? "us-assets.i.posthog.com" - : "us.i.posthog.com"; + ? `${region}-assets.i.posthog.com` + : `${region}.i.posthog.com`; url.protocol = "https:"; url.port = ""; const headers = new Headers(request.headers); diff --git a/tests/blog-markdown.test.ts b/tests/blog-markdown.test.ts index 86d87ec0..42429729 100644 --- a/tests/blog-markdown.test.ts +++ b/tests/blog-markdown.test.ts @@ -9,11 +9,17 @@ describe("blog Markdown", () => { test("resolves authored image assets without rewriting fenced examples", () => { const body = '![A chart](./chart.svg)\n\n```md\n![Example](./example.png)\n```\n~~~~md\n![Another](./example.png)\n~~~~\n![Remote](https://example.com/image.png)'; - const output = resolveBlogMarkdownImages(body, "a-post/page.mdx"); - const base = - "https://raw.githubusercontent.com/f0rr0/f0rr0.dev/next/src/content/blog/a-post/"; - expect(output).toContain(`![A chart](${base}chart.svg)`); - expect(output).toContain(`src="${base}screen.png"`); + const output = resolveBlogMarkdownImages( + body, + { + "./chart.svg": "/_next/static/media/chart.hash.svg", + "./screen.png": "/_next/static/media/screen.hash.png", + }, + "https://example.com/writing/a-post" + ); + const base = "https://example.com/_next/static/media/"; + expect(output).toContain(`![A chart](${base}chart.hash.svg)`); + expect(output).toContain(`src="${base}screen.hash.png"`); expect(output).toContain("```md\n![Example](./example.png)\n```"); expect(output).toContain("~~~~md\n![Another](./example.png)\n~~~~"); expect(output).toContain("![Remote](https://example.com/image.png)"); diff --git a/tests/github-accounts.test.ts b/tests/github-accounts.test.ts new file mode 100644 index 00000000..374e0ebe --- /dev/null +++ b/tests/github-accounts.test.ts @@ -0,0 +1,99 @@ +import { afterEach, expect, test } from "bun:test"; + +import { + githubTokensFrom, + tokenForGitHubAccount, + tokensForGitHubAccount, +} from "../src/lib/github-accounts"; +import { assertGitHubTokenIdentity } from "../src/lib/github-commits"; +import { + trackedGitHubAccounts, + trackedGitHubUserIds, +} from "../src/lib/github-commits-core"; +import { env, mockFetch } from "./helpers"; + +const originalFetch = globalThis.fetch; +const originalEnvironment = { + GITHUB_TOKENS: env.GITHUB_TOKENS, + GITHUB_TOKEN: env.GITHUB_TOKEN, + GH_TOKEN: env.GH_TOKEN, +}; +afterEach(() => { + globalThis.fetch = originalFetch; + Object.assign(env, originalEnvironment); +}); + +test("parses arbitrary configured account keys and rejects malformed or ambiguous input without exposing tokens", () => { + expect(githubTokensFrom()).toEqual({}); + expect(githubTokensFrom(" ")).toEqual({}); + expect( + githubTokensFrom('{"Alice":" one ","bob":"two"}', ["alice", "bob"]) + ).toEqual({ alice: "one", bob: "two" }); + for (const value of [ + '{"unknown":"private-value"}', + '{"alice":"private-value","ALICE":"second"}', + '{"alice":""}', + '["private-value"]', + "null", + '"private-value"', + "private-value", + ]) { + expect(() => githubTokensFrom(value, ["alice"])).toThrow(TypeError); + try { + githubTokensFrom(value, ["alice"]); + } catch (error) { + expect(String(error)).not.toContain("private-value"); + } + } +}); + +test("credential rotation, removal and order never change authors or require network calls", () => { + globalThis.fetch = mockFetch(() => { + throw new Error("Unexpected discovery request"); + }); + const authors = trackedGitHubUserIds(); + const accounts = trackedGitHubAccounts(); + delete env.GITHUB_TOKEN; + delete env.GH_TOKEN; + for (const tokens of [ + { f0rr0: "first", yuppiestechdev: "second" }, + { yuppiestechdev: "second", f0rr0: "rotated" }, + { yuppiestechdev: "second" }, + {}, + ]) { + env.GITHUB_TOKENS = JSON.stringify(tokens); + expect(trackedGitHubUserIds()).toEqual(authors); + expect(trackedGitHubAccounts()).toEqual(accounts); + expect(tokensForGitHubAccount()).toEqual(Object.values(tokens)); + } + expect(() => tokenForGitHubAccount("f0rr0")).toThrow("No GitHub token"); + env.GITHUB_TOKENS = JSON.stringify({ + f0rr0: "first", + yuppiestechdev: "second", + }); + env.GH_TOKEN = "first"; + expect(tokensForGitHubAccount("yuppiestechdev")).toEqual(["second", "first"]); + expect(tokenForGitHubAccount("f0rr0")).toBe("first"); +}); + +test("existing verification checks both stable identity and configured login in one request", async () => { + let calls = 0; + let identity = { id: 8_574_219, login: "F0rr0" }; + globalThis.fetch = mockFetch(async (input) => { + calls += 1; + expect(new URL(input instanceof Request ? input.url : input).pathname).toBe( + "/user" + ); + return Response.json(identity); + }); + await assertGitHubTokenIdentity("f0rr0", "test-token"); + expect(calls).toBe(1); + identity = { id: 99_666_891, login: "f0rr0" }; + expect(assertGitHubTokenIdentity("f0rr0", "test-token")).rejects.toThrow( + "not authenticated" + ); + identity = { id: 8_574_219, login: "renamed" }; + expect(assertGitHubTokenIdentity("f0rr0", "test-token")).rejects.toThrow( + "not authenticated" + ); +}); diff --git a/tests/github-activity-days.test.tsx b/tests/github-activity-days.test.tsx index 5b1b7725..2f029fe4 100644 --- a/tests/github-activity-days.test.tsx +++ b/tests/github-activity-days.test.tsx @@ -3,6 +3,7 @@ import { expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; import { GitHubActivityDays } from "../src/components/github-activity-days"; +import { WORK_LOG_TIME_ZONE } from "../src/lib/date"; import { buildPublicGitHubActivityDays } from "../src/lib/github-activity-feed-core"; test("the initial homepage HTML includes all IST work and totals across UTC midnight", () => { @@ -45,6 +46,6 @@ test("the initial homepage HTML includes all IST work and totals across UTC midn expect(html).toContain("5 commits across 2 repos"); expect(html).toContain("+4,474"); expect(html).toContain("−3,427"); - expect(html).toContain("IST"); + expect(html).toContain(WORK_LOG_TIME_ZONE); expect(html).not.toContain("yesterday"); }); diff --git a/tests/github-activity-feed-postgres.test.ts b/tests/github-activity-feed-postgres.test.ts index 30c6a4ef..3ef30a59 100644 --- a/tests/github-activity-feed-postgres.test.ts +++ b/tests/github-activity-feed-postgres.test.ts @@ -230,13 +230,13 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit feed projection", () => { account, author_user_id, created_at, node_id, number, repository_id, title_snapshot, url_snapshot ) values - ('f0rr0', '1', '2026-08-29T12:00:00Z', 'ISSUE_public_feed_1', 7, + ('f0rr0', '8574219', '2026-08-29T12:00:00Z', 'ISSUE_public_feed_1', 7, '102', 'Track deterministic activity', 'https://github.com/untrusted/ignored/issues/700'), - ('f0rr0', '1', '2026-08-30T08:00:00Z', 'ISSUE_private_feed_1', 8, + ('f0rr0', '8574219', '2026-08-30T08:00:00Z', 'ISSUE_private_feed_1', 8, '201', 'private issue title sentinel', 'https://github.com/secret-owner/private-repository-sentinel/issues/8'), - ('f0rr0', '1', '2026-08-31T08:00:00Z', 'ISSUE_unknown_feed_1', 9, + ('f0rr0', '8574219', '2026-08-31T08:00:00Z', 'ISSUE_unknown_feed_1', 9, '301', 'unknown issue title sentinel', 'https://github.com/unknown-owner/unknown-repository-sentinel/issues/9') `; @@ -609,4 +609,15 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit feed projection", () => { title: "revoked private issue title sentinel", }); }); + test("keeps unselected authors out of issue rows and pagination without deleting evidence", async () => { + await admin`insert into github_issues ( + account, author_user_id, created_at, node_id, number, repository_id, title_snapshot, url_snapshot + ) values ('alice', '12345678', '2099-01-01T12:00:00Z', 'ISSUE_foreign_author', 999, '102', 'foreign author sentinel', 'https://github.com/example/repo/issues/999')`; + const page = await readPublicGitHubActivityPage(null); + expect(JSON.stringify(page)).not.toContain("foreign author sentinel"); + expect(JSON.stringify(page)).not.toContain("2099-01-01"); + const rows = + await admin`select node_id from github_issues where node_id = 'ISSUE_foreign_author'`; + expect(rows).toHaveLength(1); + }); }); diff --git a/tests/github-activity-processor.test.ts b/tests/github-activity-processor.test.ts index 64ff39e6..58085ec3 100644 --- a/tests/github-activity-processor.test.ts +++ b/tests/github-activity-processor.test.ts @@ -15,8 +15,8 @@ import { GitHubRequestDeadlineError } from "../src/lib/github-api.ts"; import { mockFetch, env } from "./helpers.ts"; const originalFetch = globalThis.fetch; -const originalF0rr0Token = env.GITHUB_F0RR0_TOKEN; -const originalYuppiesTechDevToken = env.GITHUB_YUPPIESTECHDEV_TOKEN; +const originalTokens = env.GITHUB_TOKENS; +const originalGhToken = env.GH_TOKEN; const originalDefaultToken = env.GITHUB_TOKEN; const pushCommitValue = (sha: string, login: string, id: number) => ({ @@ -42,18 +42,15 @@ const restoreEnvironmentValue = ( }; beforeEach(() => { - delete env.GITHUB_F0RR0_TOKEN; - delete env.GITHUB_YUPPIESTECHDEV_TOKEN; + delete env.GITHUB_TOKENS; + delete env.GH_TOKEN; delete env.GITHUB_TOKEN; }); afterEach(() => { globalThis.fetch = originalFetch; - restoreEnvironmentValue("GITHUB_F0RR0_TOKEN", originalF0rr0Token); - restoreEnvironmentValue( - "GITHUB_YUPPIESTECHDEV_TOKEN", - originalYuppiesTechDevToken - ); + restoreEnvironmentValue("GITHUB_TOKENS", originalTokens); + restoreEnvironmentValue("GH_TOKEN", originalGhToken); restoreEnvironmentValue("GITHUB_TOKEN", originalDefaultToken); }); @@ -70,7 +67,7 @@ describe("GitHub activity commit acquisition", () => { filenames[2] = "A.ts"; const requestedPages: number[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -127,7 +124,7 @@ describe("GitHub activity commit acquisition", () => { const sha = "d".repeat(40); const ancestryResponses = [undefined, [{ sha: "not-a-sha" }], []]; let commitReads = 0; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -175,7 +172,7 @@ describe("GitHub activity commit acquisition", () => { const trackedSha = "3".repeat(40); const foreignSha = "4".repeat(40); const afterSha = foreignSha; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -216,7 +213,7 @@ describe("GitHub activity commit acquisition", () => { const surplusSha = "3".repeat(40); const afterSha = "4".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -266,7 +263,7 @@ describe("GitHub activity commit acquisition", () => { const otherSha = "3".repeat(40); const afterSha = "4".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -313,7 +310,7 @@ describe("GitHub activity commit acquisition", () => { const firstSha = "2".repeat(40); const surplusSha = "3".repeat(40); const afterSha = "4".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 3, @@ -347,7 +344,7 @@ describe("GitHub activity commit acquisition", () => { const beforeSha = "1".repeat(40); const firstSha = "2".repeat(40); const afterSha = "3".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 2, @@ -380,7 +377,7 @@ describe("GitHub activity commit acquisition", () => { const beforeSha = "1".repeat(40); const foreignSha = "2".repeat(40); const afterSha = "3".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 2, @@ -426,7 +423,7 @@ describe("GitHub activity commit acquisition", () => { test("rejects a tracked commit without a provider timestamp", async () => { const beforeSha = "1".repeat(40); const afterSha = "2".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 1, @@ -461,7 +458,7 @@ describe("GitHub activity commit acquisition", () => { test("accepts a ref rewind with no newly reachable commits", async () => { const beforeSha = "1".repeat(40); const afterSha = "2".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 0, commits: [], total_commits: 0 }) ); @@ -487,7 +484,7 @@ describe("GitHub activity commit acquisition", () => { const olderSha = "2".repeat(40); const afterSha = "3".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input, init) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -555,7 +552,7 @@ describe("GitHub activity commit acquisition", () => { const firstSha = "3".repeat(40); const afterSha = "4".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -599,7 +596,7 @@ describe("GitHub activity commit acquisition", () => { const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -636,8 +633,10 @@ describe("GitHub activity commit acquisition", () => { const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); let calls = 0; - env.GITHUB_F0RR0_TOKEN = "first-token"; - env.GITHUB_YUPPIESTECHDEV_TOKEN = "second-token"; + env.GITHUB_TOKENS = JSON.stringify({ + f0rr0: "first-token", + yuppiestechdev: "second-token", + }); globalThis.fetch = mockFetch(async () => { calls += 1; if (calls === 1) { @@ -677,7 +676,7 @@ describe("GitHub activity commit acquisition", () => { const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); let calls = 0; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => { calls += 1; return new Response(null, { status: 409 }); @@ -708,7 +707,7 @@ describe("GitHub activity commit acquisition", () => { ); const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json( { @@ -748,7 +747,7 @@ describe("GitHub activity commit acquisition", () => { test("bounds a new branch by the observed count without slicing history", async () => { const olderSha = "1".repeat(40); const afterSha = "2".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input, init) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -811,7 +810,7 @@ describe("GitHub activity commit acquisition", () => { const middleSha = "2".repeat(40); const afterSha = "3".repeat(40); const cursors: (string | null)[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (_input, init) => { const request = JSON.parse(await new Response(init?.body).text()); cursors.push(request.variables.cursor); @@ -892,7 +891,7 @@ describe("GitHub activity commit acquisition", () => { test("backfills a closed date window without requiring the current ref head", async () => { const rangedSha = "4".repeat(40); const afterSha = "5".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (_input, init) => { const request = JSON.parse(await new Response(init?.body).text()); expect(request.variables.since).toBe("2026-07-01T00:00:00.000Z"); @@ -939,7 +938,7 @@ describe("GitHub activity commit acquisition", () => { test("accepts a ref whose reachable history predates the boundary", async () => { const afterSha = "4".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (_input, init) => { const request = JSON.parse(await new Response(init?.body).text()); expect(request.variables.since).toBe("2026-08-18T00:00:00.000Z"); @@ -1124,7 +1123,7 @@ describe("GitHub pull request merge commit resolution", () => { ); }); - test("classifies a rejected GraphQL request as non-retryable", async () => { + test("defers GraphQL access loss without declaring source evidence invalid", async () => { globalThis.fetch = mockFetch(async () => Response.json({ data: null, @@ -1135,9 +1134,9 @@ describe("GitHub pull request merge commit resolution", () => { expect( resolveGitHubPullRequestMergeCommits(["PR_hidden"], "test-token") ).rejects.toMatchObject({ - code: "source_invalid", - kind: "request_rejected", - retryable: false, + code: "source_incomplete", + kind: "access_denied", + retryable: true, }); }); }); @@ -1190,7 +1189,7 @@ describe("GitHub pull request acquisition", () => { }; test("rejects an invalid associated-PR item instead of completing empty", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json([{ ...pullRequest, node_id: null }]) ); @@ -1211,7 +1210,7 @@ describe("GitHub pull request acquisition", () => { expect(Object.hasOwn(rest2026MergedPullRequest, "merge_commit_sha")).toBe( false ); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1244,7 +1243,7 @@ describe("GitHub pull request acquisition", () => { }); test("resolves an authoritative merge SHA for REST 2026 snapshots", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); const requestedPaths: string[] = []; globalThis.fetch = mockFetch(async (input) => { const url = @@ -1281,7 +1280,7 @@ describe("GitHub pull request acquisition", () => { }); test("preserves an authoritative null merge SHA for a rebase merge", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1401,7 +1400,7 @@ describe("GitHub pull request acquisition", () => { sha: headSha, }, }; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1428,8 +1427,10 @@ describe("GitHub pull request acquisition", () => { test("unions associated PR visibility across both tracked identities", async () => { const commitSha = "8".repeat(40); - env.GITHUB_F0RR0_TOKEN = "f0-token"; - env.GITHUB_YUPPIESTECHDEV_TOKEN = "yuppies-token"; + env.GITHUB_TOKENS = JSON.stringify({ + f0rr0: "f0-token", + yuppiestechdev: "yuppies-token", + }); let calls = 0; globalThis.fetch = mockFetch(async () => { calls += 1; @@ -1453,7 +1454,7 @@ describe("GitHub pull request acquisition", () => { test("discovers associated tracked pull requests and reconciles membership", async () => { const commitSha = "a".repeat(40); const requestedPaths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1554,7 +1555,7 @@ describe("GitHub pull request acquisition", () => { }); test("uses a complete paginated comparison beyond the PR commit cap", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); const comparePages: number[] = []; globalThis.fetch = mockFetch(async (input) => { const url = @@ -1695,7 +1696,7 @@ describe("GitHub pull request acquisition", () => { describe("GitHub activity provider deadlines", () => { test("propagates one absolute deadline through every worker acquisition path", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); let calls = 0; globalThis.fetch = mockFetch(async () => { calls += 1; diff --git a/tests/github-activity-worker.test.ts b/tests/github-activity-worker.test.ts index 6b864fd0..232c059e 100644 --- a/tests/github-activity-worker.test.ts +++ b/tests/github-activity-worker.test.ts @@ -21,8 +21,20 @@ import { import { GitHubResponseError } from "../src/lib/github-api.ts"; describe("GitHub activity terminal gaps", () => { + test("missing credentials and lost repository access never become terminal gaps", () => { + const errors = [ + ...[401, 403, 404].map( + (status) => new GitHubResponseError(status, { retryable: false }) + ), + new ActivityProcessingError("source_auth_missing", "No credentials"), + new GitHubGraphQlResponseError("access_denied", { retryable: true }), + ]; + for (const error of errors) { + expect(githubActivityFailureIsTerminal(error, 100)).toBe(false); + } + }); test("given a deterministic REST gap, it becomes unavailable on the third worker claim", () => { - for (const status of [403, 404, 410, 422]) { + for (const status of [410, 422]) { const error = new GitHubResponseError(status, { retryable: false }); expect( githubActivityFailureIsTerminal( @@ -71,7 +83,6 @@ describe("GitHub activity terminal gaps", () => { ).toBe(true); for (const code of [ "membership_incomplete", - "source_auth_missing", "source_incomplete", "source_invalid", "source_unavailable", diff --git a/tests/github-backfill.test.ts b/tests/github-backfill.test.ts index fd47bf3e..b71d5aba 100644 --- a/tests/github-backfill.test.ts +++ b/tests/github-backfill.test.ts @@ -205,7 +205,7 @@ describe("GitHub factual history backfill", () => { expect(() => { requireBackfillEnvironment(request, { DATABASE_URL: "postgresql://activity.example/database", - GITHUB_F0RR0_TOKEN: "token", + GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }), }); }).not.toThrow(); expect(() => { @@ -360,7 +360,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -413,7 +413,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -444,7 +444,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -476,7 +476,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { diff --git a/tests/github-commits.test.ts b/tests/github-commits.test.ts index 40f459cb..b0fe75f1 100644 --- a/tests/github-commits.test.ts +++ b/tests/github-commits.test.ts @@ -987,7 +987,7 @@ describe("token identity", () => { expect(new Headers(init?.headers).get("authorization")).toBe( "Bearer token" ); - return Response.json({ id: 8_574_219, login: "renamed-account" }); + return Response.json({ id: 8_574_219, login: "f0rr0" }); }); expect( @@ -1001,7 +1001,7 @@ describe("token identity", () => { ); expect(assertGitHubTokenIdentity("f0rr0", "token")).rejects.toThrow( - "GITHUB_F0RR0_TOKEN is not authenticated as f0rr0" + "The GitHub token is not authenticated as f0rr0" ); }); }); diff --git a/tests/github-diff.test.ts b/tests/github-diff.test.ts index 1fcc0e20..7684e6cf 100644 --- a/tests/github-diff.test.ts +++ b/tests/github-diff.test.ts @@ -105,8 +105,8 @@ test("a real binary change remains explicit while the text diff stays eligible", test("PR acquisition falls back to a SHA-pinned diff and rejects a moving PR", async () => { const originalFetch = globalThis.fetch; - const originalToken = env.GITHUB_F0RR0_TOKEN; - env.GITHUB_F0RR0_TOKEN = "test-token"; + const originalToken = env.GITHUB_TOKENS; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); let head = "b".repeat(40); const base = "a".repeat(40); const paths: string[] = []; @@ -145,9 +145,9 @@ test("PR acquisition falls back to a SHA-pinned diff and rejects a moving PR", a } finally { globalThis.fetch = originalFetch; if (originalToken === undefined) { - delete env.GITHUB_F0RR0_TOKEN; + delete env.GITHUB_TOKENS; } else { - env.GITHUB_F0RR0_TOKEN = originalToken; + env.GITHUB_TOKENS = originalToken; } } }); diff --git a/tests/github-pull-request-store.test.ts b/tests/github-pull-request-store.test.ts index 3c7dc867..00cef226 100644 --- a/tests/github-pull-request-store.test.ts +++ b/tests/github-pull-request-store.test.ts @@ -7,6 +7,9 @@ import { test, } from "bun:test"; import assert from "node:assert/strict"; +import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { and, asc, eq } from "drizzle-orm"; @@ -14,6 +17,7 @@ import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; +import migrationJournal from "../drizzle/meta/_journal.json"; import type * as DatabaseClient from "../src/db/client.ts"; import type * as DatabaseSchema from "../src/db/schema.ts"; import type * as GithubActivityWorkerStore from "../src/lib/github-activity-worker-store.ts"; @@ -171,7 +175,44 @@ describe.skipIf(!dockerAvailable)("GitHub pull request persistence", () => { onnotice: (notice) => void notice, prepare: false, }); - await migrate(drizzle({ client: admin }), { migrationsFolder }); + // Exercise an upgrade with an existing checkpoint, not only an empty install. + const previousMigrations = await mkdtemp( + path.join(tmpdir(), "github-account-upgrade-") + ); + const previousEntries = migrationJournal.entries.filter( + ({ idx }) => idx < 22 + ); + try { + await mkdir(path.join(previousMigrations, "meta")); + await writeFile( + path.join(previousMigrations, "meta/_journal.json"), + JSON.stringify({ ...migrationJournal, entries: previousEntries }) + ); + await Promise.all( + previousEntries.map(async ({ tag }) => { + await copyFile( + path.join(migrationsFolder, `${tag}.sql`), + path.join(previousMigrations, `${tag}.sql`) + ); + }) + ); + await migrate(drizzle({ client: admin }), { + migrationsFolder: previousMigrations, + }); + await admin`insert into github_account_checkpoints (account, latest_event_id, paused) values ('f0rr0', '42', true)`; + await admin`insert into github_repository_inventory_heads (account_user_id, account_login) values ('8574219', 'f0rr0')`; + await migrate(drizzle({ client: admin }), { migrationsFolder }); + const [upgraded] = + await admin`select latest_event_id, paused from github_account_checkpoints where account = 'f0rr0'`; + expect(upgraded).toMatchObject({ + latest_event_id: "42", + paused: true, + }); + await admin`delete from github_repository_inventory_heads where account_user_id = '8574219'`; + await admin`delete from github_account_checkpoints where account = 'f0rr0'`; + } finally { + await rm(previousMigrations, { recursive: true, force: true }); + } env.DATABASE_URL = databaseUrl; schema = await import("../src/db/schema.ts"); const client = await import("../src/db/client.ts"); @@ -211,6 +252,24 @@ describe.skipIf(!dockerAvailable)("GitHub pull request persistence", () => { } }); + test("accepts a configured account name without adding identity registration columns", async () => { + await database + .insert(schema.githubAccountCheckpoints) + .values({ account: "alice" }); + await database.insert(schema.githubWebhookDeliveries).values({ + deliveryId: "00000000-0000-4000-8000-000000999999", + event: "ping", + accepted: false, + account: "alice", + }); + const [checkpoint] = await database + .select() + .from(schema.githubAccountCheckpoints) + .where(eq(schema.githubAccountCheckpoints.account, "alice")); + expect(checkpoint?.account).toBe("alice"); + expect(checkpoint).not.toHaveProperty("userId"); + }); + test("promotes an equal-time terminal signal without trusting its merge SHA", async () => { const initial = await persistGitHubWebhookPullRequest( "00000000-0000-4000-8000-000000000001", @@ -715,5 +774,35 @@ describe.skipIf(!dockerAvailable)("GitHub pull request persistence", () => { ); expect(discovery.repository).toBe("f0rr0/renamed-worker-path"); await releaseGitHubPullRequestDiscovery(discovery); + + // A smaller visible result cannot erase a previously verified association. + const { completeGitHubPullRequestDiscovery } = + await import("../src/lib/github-activity-worker-store"); + await database.insert(schema.githubCommitPullRequestAssociations).values({ + commitRepositoryId: workerRepositoryId, + commitSha, + pullRequestNodeId: pullRequest().nodeId, + }); + const [reclaimed] = await claimGitHubCommitsForPullRequestDiscovery( + 1, + ["f0rr0"], + new Date("2026-09-02T10:03:00.000Z") + ); + expect(await completeGitHubPullRequestDiscovery(reclaimed, [])).toBe(true); + const associations = await database + .select() + .from(schema.githubCommitPullRequestAssociations) + .where( + and( + eq( + schema.githubCommitPullRequestAssociations.commitRepositoryId, + workerRepositoryId + ), + eq(schema.githubCommitPullRequestAssociations.commitSha, commitSha) + ) + ); + expect(associations.map((row) => row.pullRequestNodeId)).toEqual([ + pullRequest().nodeId, + ]); }); }); diff --git a/tests/github-work-unit-store-postgres.test.ts b/tests/github-work-unit-store-postgres.test.ts index 513bbe34..d9027c61 100644 --- a/tests/github-work-unit-store-postgres.test.ts +++ b/tests/github-work-unit-store-postgres.test.ts @@ -555,6 +555,7 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { }); test("atomically swaps public facts, revisions, and summary eligibility", async () => { + const [before] = await database.select().from(schema.githubPublicFeedHead); const first = await refreshGitHubWorkUnitProjection(observedAt); expect(first).toMatchObject({ changed: true, @@ -578,9 +579,9 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { expect(unit.summaryInputDigest).toMatch(/^[a-f0-9]{64}$/u); expect(unit.summaryEvaluatedDigest).toMatch(/^[a-f0-9]{64}$/u); expect(head).toMatchObject({ - feedRevision: 1, - headContentRevision: 1, - orderingRevision: 1, + feedRevision: before.feedRevision + 1, + headContentRevision: before.headContentRevision + 1, + orderingRevision: before.orderingRevision + 1, }); expect(attempts).toHaveLength(1); expect(attempts[0]).toMatchObject({ @@ -603,9 +604,9 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { summaryAttemptsQueued: 0, }); expect(unchangedHead).toMatchObject({ - feedRevision: 1, - headContentRevision: 1, - orderingRevision: 1, + feedRevision: before.feedRevision + 1, + headContentRevision: before.headContentRevision + 1, + orderingRevision: before.orderingRevision + 1, }); }); @@ -1727,4 +1728,96 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { }); expect(await ensureGitHubWorkUnitProjectionRequest()).toBeNull(); }); + test("credential changes preserve the persisted projection and checkpoints without discovery", async () => { + const originalEnvironment = { + GITHUB_TOKENS: env.GITHUB_TOKENS, + GITHUB_TOKEN: env.GITHUB_TOKEN, + GH_TOKEN: env.GH_TOKEN, + }; + const originalFetch = globalThis.fetch; + try { + delete env.GITHUB_TOKEN; + delete env.GH_TOKEN; + globalThis.fetch = Object.assign( + () => { + throw new Error("Unexpected GitHub request during projection"); + }, + { preconnect: originalFetch.preconnect } + ); + await refreshGitHubWorkUnitProjection(new Date()); + const snapshot = async () => ({ + units: await database + .select({ + identity: schema.githubWorkUnits.identityKey, + membership: schema.githubWorkUnits.membershipDigest, + facts: schema.githubWorkUnits.factsDigest, + }) + .from(schema.githubWorkUnits) + .orderBy(schema.githubWorkUnits.identityKey), + checkpoints: await database + .select() + .from(schema.githubAccountCheckpoints) + .orderBy(schema.githubAccountCheckpoints.account), + commits: await database + .select({ + sha: schema.githubCommits.sha, + author: schema.githubCommits.authorUserId, + }) + .from(schema.githubCommits) + .orderBy(schema.githubCommits.repositoryId, schema.githubCommits.sha), + }); + const before = await snapshot(); + expect(before.units.length).toBeGreaterThan(0); + for (const credentials of [ + { f0rr0: "first", yuppiestechdev: "second" }, + { yuppiestechdev: "second", f0rr0: "rotated" }, + { yuppiestechdev: "second" }, + {}, + ]) { + env.GITHUB_TOKENS = JSON.stringify(credentials); + await refreshGitHubWorkUnitProjection(new Date()); + expect(await snapshot()).toEqual(before); + } + const { syncGitHubAccounts, reconcileGitHubRefs } = + await import("../src/lib/github-commits"); + expect(await syncGitHubAccounts()).toMatchObject({ + accounts: 0, + failedAccounts: [], + }); + expect( + await reconcileGitHubRefs({ + kind: "head", + repositoryLimit: 1, + deadlineAt: Date.now() + 1000, + }) + ).toMatchObject({ accounts: 0, failedAccounts: [] }); + expect(await runGitHubActivityWorker()).toMatchObject({ + commits: { claimed: 0 }, + observations: { claimed: 0 }, + }); + expect(await snapshot()).toEqual(before); + } finally { + Object.assign(env, originalEnvironment); + globalThis.fetch = originalFetch; + } + }); + test("an author-policy rebuild invalidates feed caches and cursors even without changed work units", async () => { + await database + .update(schema.githubPublicFeedHead) + .set({ + summaryPolicyDigest: "0".repeat(64), + projectionRequestToken: null, + }) + .where(eq(schema.githubPublicFeedHead.id, true)); + const [before] = await database.select().from(schema.githubPublicFeedHead); + const token = await ensureGitHubWorkUnitProjectionRequest(); + assert.ok(token !== null); + expect(await completeGitHubWorkUnitProjectionRequest(token)).toBe(true); + const [after] = await database.select().from(schema.githubPublicFeedHead); + expect(after.feedRevision).toBe(before.feedRevision + 1); + expect(after.headContentRevision).toBe(before.headContentRevision + 1); + expect(after.orderingRevision).toBe(before.orderingRevision + 1); + expect(await completeGitHubWorkUnitProjectionRequest(token)).toBe(false); + expect(await ensureGitHubWorkUnitProjectionRequest()).toBeNull(); + }); }); diff --git a/tests/production-database-migration.test.ts b/tests/production-database-migration.test.ts index 697dfc50..eca4d69a 100644 --- a/tests/production-database-migration.test.ts +++ b/tests/production-database-migration.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test"; import { shouldConfigureSupabaseCronForProduction, supabaseCronSiteUrlFrom, + supabaseCronJobsFrom, supabaseCronUrlsFrom, } from "../scripts/configure-supabase-cron.ts"; import { @@ -45,19 +46,19 @@ describe("production Supabase cron URLs", () => { test("uses Vercel's production hostname without a managed site URL", () => { expect( supabaseCronSiteUrlFrom({ - VERCEL_PROJECT_PRODUCTION_URL: "f0rr0.dev", + VERCEL_PROJECT_PRODUCTION_URL: "project.example", }) - ).toBe("https://f0rr0.dev"); - expect(supabaseCronSiteUrlFrom({})).toBe("https://f0rr0.dev"); + ).toBe("https://project.example"); + expect(() => supabaseCronSiteUrlFrom({})).toThrow(); }); test("builds every bounded production endpoint from the site URL", () => { - expect(supabaseCronUrlsFrom("https://f0rr0.dev")).toEqual({ - codexStats: "https://f0rr0.dev/api/cron/codex-stats", - events: "https://f0rr0.dev/api/cron/github-sync", - headRefs: "https://f0rr0.dev/api/cron/github-refs?repositories=8", - summary: "https://f0rr0.dev/api/cron/github-summary", - worker: "https://f0rr0.dev/api/cron/github-worker", + expect(supabaseCronUrlsFrom("https://project.example")).toEqual({ + codexStats: "https://project.example/api/cron/codex-stats", + events: "https://project.example/api/cron/github-sync", + headRefs: "https://project.example/api/cron/github-refs?repositories=8", + summary: "https://project.example/api/cron/github-summary", + worker: "https://project.example/api/cron/github-worker", }); expect(() => supabaseCronUrlsFrom("http://localhost:3000")).toThrow( "HTTPS" @@ -95,3 +96,37 @@ describe("production migration database URL", () => { ).toThrow(ProductionMigrationConfigurationError); }); }); + +const enabledNames = ( + configuration: Parameters[0], + codex = false +) => + supabaseCronJobsFrom(configuration, codex) + .filter((job) => job.enabled) + .map((job) => job.name); + +test("schedules only configured services while retaining disabled jobs for cleanup", () => { + const environment = { VERCEL_PROJECT_PRODUCTION_URL: "example.vercel.app" }; + expect(enabledNames(environment)).toEqual([ + "github-activity-worker-every-five-minutes", + ]); + expect(enabledNames(environment, true)).toEqual([ + "github-activity-worker-every-five-minutes", + "codex-stats-every-fifteen-minutes", + ]); + expect( + enabledNames({ ...environment, OPENAI_API_KEY: "test-key" }) + ).toHaveLength(2); + const github = { + ...environment, + GITHUB_TOKENS: JSON.stringify({ f0rr0: "test-token" }), + }; + expect(enabledNames(github)).toHaveLength(3); + expect( + enabledNames({ ...github, OPENAI_API_KEY: "test-key" }, true) + ).toHaveLength(5); + expect(supabaseCronJobsFrom(environment, false)).toHaveLength(5); + for (const job of supabaseCronJobsFrom(github, true)) { + expect(new URL(job.url).origin).toBe("https://example.vercel.app"); + } +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 00000000..55f8ccd3 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,4 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +// Next installs this global in its server runtime; Bun uses the same native implementation. +Object.assign(globalThis, { AsyncLocalStorage }); diff --git a/tests/site-seo.test.ts b/tests/site-seo.test.ts index b132e68c..7e35caf1 100644 --- a/tests/site-seo.test.ts +++ b/tests/site-seo.test.ts @@ -22,8 +22,10 @@ test("deployment aliases never replace public identity, and only previews receiv ); expect(result.exitCode).toBe(0); const output = JSON.parse(result.stdout.toString()); - expect(output.origin).toBe("https://f0rr0.dev"); - expect(output.article).toBe("https://f0rr0.dev/writing/example"); + expect(output.origin).toBe("https://project-alias.vercel.app"); + expect(output.article).toBe( + "https://project-alias.vercel.app/writing/example" + ); expect(output.redirects).toEqual([ { source: "/blog/:path*", @@ -52,3 +54,67 @@ test("deployment aliases never replace public identity, and only previews receiv ); } }); + +test("an alternate profile drives site identity, structured exports, education and PDF URLs", () => { + const result = Bun.spawnSync( + [ + process.execPath, + "--eval", + ` + const { githubAccounts } = await import("./src/content/site.ts"); + githubAccounts.splice(0, githubAccounts.length, {login: "alice", id: "12345678"}); + const { resumeData, socialProfiles } = await import("./src/content/resume.ts"); + Object.assign(resumeData.person, { + name: "Alice Example", role: "Engineer", email: "alice@example.com", + image: "/alice.png", alternateNames: ["alice"], + address: {city: "London", region: "England", countryCode: "GB"} + }); + const linkedin = socialProfiles.find(profile => profile.network === "LinkedIn"); + Object.assign(linkedin, {username: "alice", url: "https://linkedin.com/in/alice"}); + resumeData.summary = "Builds useful software."; + resumeData.experience.splice(0); + resumeData.education.splice(0, resumeData.education.length, { + company: "Example University", url: "https://university.example", tagline: "Education", + roles: [{title: "Computer Science", dates: "2020 - 2024", location: "London"}] + }); + resumeData.pdf.outputPath = "public/resume/alice.pdf"; + const { siteConfig, resumePdfUrl } = await import("./src/lib/site.ts"); + const { buildJsonResume, buildLlmsTxt } = await import("./src/lib/resume.ts"); + const { buildProfilePageJsonLd } = await import("./src/lib/structured-data.ts"); + console.log(JSON.stringify({siteConfig, resumePdfUrl, resume: buildJsonResume(), profile: buildProfilePageJsonLd(), guide: buildLlmsTxt()})); + `, + ], + { env: { ...process.env, VERCEL_PROJECT_PRODUCTION_URL: "alice.example" } } + ); + expect(result.exitCode).toBe(0); + const output = JSON.parse(result.stdout.toString()); + expect(output.siteConfig).toMatchObject({ + name: "Alice Example", + url: "https://alice.example", + author: { handle: "alice", image: "/alice.png" }, + }); + expect(output.resume.basics).toMatchObject({ + name: "Alice Example", + email: "alice@example.com", + location: { city: "London", countryCode: "GB" }, + }); + expect(output.resume.basics.profiles).toContainEqual({ + network: "GitHub", + username: "alice", + url: "https://github.com/alice", + }); + expect(output.profile.mainEntity).toMatchObject({ + name: "Alice Example", + alternateName: ["alice"], + alumniOf: [ + { + "@type": "EducationalOrganization", + name: "Example University", + sameAs: "https://university.example", + }, + ], + }); + expect(output.resume.education[0].institution).toBe("Example University"); + expect(output.resumePdfUrl).toBe("/resume/alice.pdf"); + expect(output.guide).toContain("https://alice.example/resume/alice.pdf"); +}); diff --git a/tests/site-url.test.ts b/tests/site-url.test.ts new file mode 100644 index 00000000..45947b5f --- /dev/null +++ b/tests/site-url.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test"; + +import { productionSiteOrigin, siteOriginFrom } from "../src/lib/site-url"; + +test("derives local and production URLs without an author-specific fallback", () => { + expect(siteOriginFrom({})).toBe("http://localhost:3000"); + expect( + siteOriginFrom({ + PORT: "", + NEXT_PUBLIC_PORT: "", + VERCEL_PROJECT_PRODUCTION_URL: "", + }) + ).toBe("http://localhost:3000"); + expect(siteOriginFrom({ PORT: "4200" })).toBe("http://localhost:4200"); + expect( + siteOriginFrom({ + VERCEL: "1", + VERCEL_PROJECT_PRODUCTION_URL: "example.vercel.app", + }) + ).toBe("https://example.vercel.app"); + for (const hostname of [ + undefined, + "", + "localhost", + "https://example.com", + "example.com/path", + "user:password@example.com", + "example.com?target=other", + ]) { + expect(() => productionSiteOrigin(hostname)).toThrow(); + } + expect(() => siteOriginFrom({ VERCEL: "1" })).toThrow(); +}); From bd27b9ab6d97b4ed16c3729adb75ab74b07b7f6b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Tue, 8 Sep 2026 23:55:26 +0000 Subject: [PATCH 2/7] docs: replace audit checklist with deployment instructions --- README.md | 24 +++++++++++-- docs/open-source-readiness.md | 65 ----------------------------------- 2 files changed, 21 insertions(+), 68 deletions(-) delete mode 100644 docs/open-source-readiness.md diff --git a/README.md b/README.md index 25b56055..0084237c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,27 @@ building. Cron configuration follows a successful build; it is not a post-deployment hook. Local operations remain `bun run db:migrate` and `bun run supabase:cron`. +## Updating an existing deployment + +Move the existing per-account token values into one `GITHUB_TOKENS` JSON object, +keyed by the logins in `src/content/site.ts`. The same tokens can be reused; no +new tokens or account IDs are required. Set this variable in Vercel and set the +same repository secret for the manual backfill Action. Keep the old token +variables until the new deployment has been verified, then remove them. + +Keep the existing database, webhook, cron, cursor-signing and OpenAI credentials. +To retain analytics, set `NEXT_PUBLIC_POSTHOG_KEY` to the existing project's public +capture key; `NEXT_PUBLIC_POSTHOG_REGION` defaults to `us`. + +For the database-backed installation, set the Build Command above before deploying +this change. It applies migration `0022`, builds against the updated schema, then +updates cron configuration. Migration `0022` only replaces seven username +allowlists with valid-login checks; it adds no tables or columns and rewrites no +stored history. Leave older applied migrations intact. + +Vercel supplies the production hostname when system environment variables are +exposed. No manually maintained site URL or blog asset URL is needed. + ## Customize once - `src/content/resume.ts`: identity, social profiles, career, education and PDF paths. @@ -85,9 +106,6 @@ Report credential exposure privately through GitHub's security reporting feature when enabled. Otherwise, use the maintainer contact in `src/content/resume.ts`; never paste credentials into a public issue. -The [audit checklist](docs/open-source-readiness.md) records completed work and -remaining owner decisions. - ## Reuse status A code license and the reuse policy for personal writing/images still need to be diff --git a/docs/open-source-readiness.md b/docs/open-source-readiness.md deleted file mode 100644 index c61d7027..00000000 --- a/docs/open-source-readiness.md +++ /dev/null @@ -1,65 +0,0 @@ -# Open-source readiness checklist - -Rechecked September 8, 2026 against default branch `next` at `fdc15f9` and the -changes in this pull request. Personal writing, photographs, résumé, journey, -project content and authoring preferences remain intentionally in the repository. - -**20 of 23 findings are addressed.** One remains an owner-side review, one is -partially addressed, and the code license is deliberately undecided. This is a -configuration-portability cleanup, not a claim that every security or licensing -question is settled. - -| Audit finding | Status | Resolution or remaining work | -| --------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1. Historical Firebase configuration | Open | Owner must review current key restrictions and database/storage rules, or retire the old project. Copies remain in historical commits and old branches. No credential validity probes or history rewrites were performed. | -| 2. Cron targeting the original deployment | Addressed | Requires the Vercel production hostname; missing/invalid targets fail before writes. | -| 3. Hardcoded GitHub identities | Addressed | Public author records in `src/content/site.ts`; immutable-ID checks retained. | -| 4. Seven personal database constraints | Addressed | Forward migration replaces personal allowlists with login-shape checks, preserving existing checkpoints. | -| 5. Duplicated, two-account token selection | Addressed | One validated `GITHUB_TOKENS` object and shared lookup/fallback helpers. Credentials do not control author visibility. | -| 6. Personal backfill workflow inputs/secrets | Addressed | Generic account input and token object; configured accounts are validated. | -| 7. Build requires/mutates infrastructure | Addressed | `build` only builds; existing operational scripts compose through Vercel's Build Command when wanted. | -| 8. Dedicated-database assumption | Addressed | Documented one database per installation; existing lock/job names retained for compatibility. | -| 9. Fixed canonical domain | Addressed | Vercel supplies the production hostname; previews retain canonical production identity. | -| 10. Duplicated page/social-card identity | Addressed | Consumers derive names, images and domain text from shared configuration. | -| 11. Separate machine-readable biography | Addressed | JSON résumé and structured data use shared person, social and education records. | -| 12. Hardcoded footer/PDF links | Addressed | Consumers reuse configured social links and PDF output path. | -| 13. Markdown images tied to one GitHub branch | Addressed | Exports reuse the MDX compiler's deployed image URLs. | -| 14. Fixed public GitHub discovery account | Addressed | Uses the configured primary GitHub profile. | -| 15. Fixed PostHog project/region | Addressed | Optional capture key and shared US/EU region; production-host guard retained. | -| 16. Automatic copying of runtime credentials | Addressed | README and `.worktreeinclude` explicitly require development-only local credentials; production credentials belong in deployment storage. | -| 17. Optional services/publication boundaries | Addressed | Setup/publication documented; polling, summaries and Codex scheduling follow their own prerequisites. Database-only publication remains available without GitHub tokens. | -| 18. Timezone/region preferences | Addressed | Shared timezone with derived label; deployment region/database relationship documented. | -| 19. Application code license | Deferred by choice | User explicitly left the license undecided. This PR does not introduce one. | -| 20. Vendored licenses/provenance | Partial | Font and agent-browser notices bundled; remaining skill origins/revisions and complete notices still need verification. See `THIRD_PARTY.md`. | -| 21. Secret-scanning prevention | Addressed | GitHub native secret scanning and push protection verified enabled. Additional Gitleaks CI removed as requested; forks must enable their own protection settings. | -| 22. Configurability regression coverage | Addressed | Alternate profile/domain/account tests and disposable PostgreSQL upgrade/history tests; Docker prerequisite documented. | -| 23. Contributor configuration guidance | Addressed | README customization map, blank environment template, deployment instructions and private-reporting guidance. | - -Direct dependencies are also pinned to the existing resolved versions. Bun saves -exact versions and CI uses the frozen lockfile; no package upgrades were bundled. - -## Validation - -The application changes passed 324 tests across 43 files, including real disposable -PostgreSQL databases; the added alternate-profile regression also passes. Lint, -full/deployment-source typechecks and a Vercel-style build with no service credentials -passed. The build reports nonfatal unauthenticated GitHub embed 403s and existing -filesystem-tracing warnings. - -Coverage includes token rotation/removal, zero-token polling/publication, retained -commit-to-PR evidence after reduced visibility, configured-author issue filtering, -and cache/cursor invalidation after author-policy changes. These checks do not -verify live Firebase rules, production credentials or every vendored file's origin. - -## Before deployment - -- Set `GITHUB_TOKENS` to the documented login-to-token object in Vercel and the - backfill Action. Remove obsolete personal token variable names after switching. -- Set optional analytics configuration if keeping analytics enabled. -- For the existing database-backed deployment, apply the README's Vercel Build - Command so migrations run before the build and cron setup follows it. -- Resolve the remaining Firebase review and vendored provenance separately. The - code-license decision remains deferred. - -Production database migrations and Vercel/cron configuration have not been applied -by this change. Applied historical migrations and personal content remain intact. From 4bc07e62ad4a0a0db5f13063c8d0a9cdddba20b8 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 9 Sep 2026 00:03:18 +0000 Subject: [PATCH 3/7] fix: use a supported Actions secret name for GitHub tokens --- .github/workflows/github-activity-backfill.yml | 2 +- README.md | 3 ++- docs/github-commits.md | 3 ++- tests/github-backfill.test.ts | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-activity-backfill.yml b/.github/workflows/github-activity-backfill.yml index 0df271d3..ac7e0d62 100644 --- a/.github/workflows/github-activity-backfill.yml +++ b/.github/workflows/github-activity-backfill.yml @@ -53,7 +53,7 @@ jobs: ACCOUNT: ${{ inputs.account }} DATABASE_URL: ${{ secrets.ACTIVITY_DATABASE_URL }} END_DATE: ${{ inputs.end_date }} - GITHUB_TOKENS: ${{ secrets.GITHUB_TOKENS }} + GITHUB_TOKENS: ${{ secrets.ACTIVITY_GITHUB_TOKENS }} MAXIMUM_MINUTES: ${{ inputs.maximum_minutes }} REPOSITORY_ID: ${{ inputs.repository_id }} START_DATE: ${{ inputs.start_date }} diff --git a/README.md b/README.md index 0084237c..67b4e550 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ post-deployment hook. Local operations remain `bun run db:migrate` and Move the existing per-account token values into one `GITHUB_TOKENS` JSON object, keyed by the logins in `src/content/site.ts`. The same tokens can be reused; no new tokens or account IDs are required. Set this variable in Vercel and set the -same repository secret for the manual backfill Action. Keep the old token +`ACTIVITY_GITHUB_TOKENS` repository secret for the manual backfill Action (GitHub +reserves secret names beginning with `GITHUB_`). Keep the old token variables until the new deployment has been verified, then remove them. Keep the existing database, webhook, cron, cursor-signing and OpenAI credentials. diff --git a/docs/github-commits.md b/docs/github-commits.md index f60f9737..552764c9 100644 --- a/docs/github-commits.md +++ b/docs/github-commits.md @@ -139,7 +139,8 @@ when their provider key is configured. Codex scheduling follows enabled database accounts. Rerun cron setup after enabling or disabling a service. Local cron setup requires the Vercel production hostname in `VERCEL_PROJECT_PRODUCTION_URL`. The manual backfill Action uses repository secrets `ACTIVITY_DATABASE_URL` and -`GITHUB_TOKENS`; its account input defaults to all configured authors. Every +`ACTIVITY_GITHUB_TOKENS` (mapped to the runtime variable `GITHUB_TOKENS`; GitHub +reserves the `GITHUB_` secret prefix). Its account input defaults to all configured authors. Every selected backfill author needs a credential; use `--account` to select a subset. Public output includes private-activity counts, timestamps and line/file facts, diff --git a/tests/github-backfill.test.ts b/tests/github-backfill.test.ts index b71d5aba..7e4a1b6d 100644 --- a/tests/github-backfill.test.ts +++ b/tests/github-backfill.test.ts @@ -705,3 +705,18 @@ describe("GitHub factual history backfill", () => { expect(workerPasses).toBe(1); }); }); + +test("workflow secrets avoid GitHub's reserved prefix except its built-in token", async () => { + const directory = new URL("../.github/workflows/", import.meta.url); + let checked = 0; + for await (const file of new Bun.Glob("*.yml").scan(directory.pathname)) { + const workflow = await Bun.file(new URL(file, directory)).text(); + for (const [, name] of workflow.matchAll(/\bsecrets\.([A-Za-z_]\w*)/g)) { + expect( + name === "GITHUB_TOKEN" || !name.toUpperCase().startsWith("GITHUB_") + ).toBe(true); + checked += 1; + } + } + expect(checked).toBeGreaterThan(0); +}); From a803a7ab9b67bd7931be51b2b3dfa412a8edfbd4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 9 Sep 2026 00:09:41 +0000 Subject: [PATCH 4/7] docs: remove added license files --- LICENSES/agent-browser-LICENSE.txt | 201 ----------------------------- LICENSES/geist-OFL.txt | 92 ------------- LICENSES/ibm-plex-OFL.txt | 93 ------------- LICENSES/jetbrains-mono-OFL.txt | 93 ------------- LICENSES/literata-OFL.txt | 93 ------------- LICENSES/source-sans-OFL.txt | 93 ------------- THIRD_PARTY.md | 25 ---- 7 files changed, 690 deletions(-) delete mode 100644 LICENSES/agent-browser-LICENSE.txt delete mode 100644 LICENSES/geist-OFL.txt delete mode 100644 LICENSES/ibm-plex-OFL.txt delete mode 100644 LICENSES/jetbrains-mono-OFL.txt delete mode 100644 LICENSES/literata-OFL.txt delete mode 100644 LICENSES/source-sans-OFL.txt delete mode 100644 THIRD_PARTY.md diff --git a/LICENSES/agent-browser-LICENSE.txt b/LICENSES/agent-browser-LICENSE.txt deleted file mode 100644 index 8226d363..00000000 --- a/LICENSES/agent-browser-LICENSE.txt +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2025 Vercel Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/LICENSES/geist-OFL.txt b/LICENSES/geist-OFL.txt deleted file mode 100644 index 8d003fec..00000000 --- a/LICENSES/geist-OFL.txt +++ /dev/null @@ -1,92 +0,0 @@ -Copyright (c) 2023 Vercel, in collaboration with basement.studio - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION AND CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/ibm-plex-OFL.txt b/LICENSES/ibm-plex-OFL.txt deleted file mode 100644 index c35c4c61..00000000 --- a/LICENSES/ibm-plex-OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/jetbrains-mono-OFL.txt b/LICENSES/jetbrains-mono-OFL.txt deleted file mode 100644 index 5ceee002..00000000 --- a/LICENSES/jetbrains-mono-OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/literata-OFL.txt b/LICENSES/literata-OFL.txt deleted file mode 100644 index 9d3b4e3b..00000000 --- a/LICENSES/literata-OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/source-sans-OFL.txt b/LICENSES/source-sans-OFL.txt deleted file mode 100644 index 22c601b8..00000000 --- a/LICENSES/source-sans-OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2010-2024 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md deleted file mode 100644 index ac18ecb4..00000000 --- a/THIRD_PARTY.md +++ /dev/null @@ -1,25 +0,0 @@ -# Third-party material - -These notices cover bundled material separately from the application code and -personal content. Original embedded notices remain in the font files. - -| Material | Upstream | Bundled license | -| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------ | -| IBM Plex Sans (`career/typst/fonts/IBMPlexSans-*`) | [IBM Plex](https://github.com/IBM/plex) | [SIL OFL 1.1](LICENSES/ibm-plex-OFL.txt) | -| Source Sans 3 (`career/typst/fonts/SourceSans3-*`, `src/app/fonts/SourceSans3-Latin.woff2`) | [Adobe Source Sans](https://github.com/adobe-fonts/source-sans) | [SIL OFL 1.1](LICENSES/source-sans-OFL.txt) | -| Literata (`career/typst/fonts/Literata.ttf`, `src/app/fonts/Literata-Latin.woff2`) | [Literata](https://github.com/googlefonts/literata) | [SIL OFL 1.1](LICENSES/literata-OFL.txt) | -| Geist (`src/app/fonts/Geist-Latin.woff2`) | [Vercel Geist](https://github.com/vercel/geist-font) | [SIL OFL 1.1](LICENSES/geist-OFL.txt) | -| JetBrains Mono (`src/app/fonts/JetBrainsMono-Latin.woff2`) | [JetBrains Mono](https://github.com/JetBrains/JetBrainsMono) | [SIL OFL 1.1](LICENSES/jetbrains-mono-OFL.txt) | -| `.rulesync/skills/agent-browser/` | [agent-browser](https://github.com/vercel-labs/agent-browser) | [Apache 2.0](LICENSES/agent-browser-LICENSE.txt) | - -License texts were retrieved from the linked upstream projects on September 8, 2026. The IBM and Adobe fonts also retain their version-specific embedded -copyright notices. Do not remove those notices when regenerating or subsetting. - -The vendored Vercel React and composition skills declare MIT in their own -frontmatter. Their exact upstream revision and complete copyright/license -notices, and the provenance of the remaining vendored skills, still need to be -verified before making a blanket redistribution claim for `.rulesync/`. - -Personal writing, photographs, résumé content and third-party company logos are -not relicensed by these notices. Company logos retain their owners' trademarks. -Package dependencies retain their own licenses in their distributions. From 0e88126eb40851bfd7f96346ee49b3aac7fdb958 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 9 Sep 2026 00:14:27 +0000 Subject: [PATCH 5/7] refactor: centralize site environment with T3 Vercel preset --- src/env.ts | 12 ++++++++---- src/instrumentation-client.ts | 2 +- src/lib/site-url.ts | 17 +++-------------- tests/site-url.test.ts | 24 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/env.ts b/src/env.ts index 133858d6..54952147 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,16 +1,24 @@ import { createEnv } from "@t3-oss/env-nextjs"; +import { vercel } from "@t3-oss/env-nextjs/presets-zod"; import { z } from "zod"; const optionalString = z.string().trim().min(1).optional(); export const env = createEnv({ + extends: [vercel()], client: { + NEXT_PUBLIC_SITE_ORIGIN: z.url().optional(), + NEXT_PUBLIC_DEPLOYMENT_ENV: z + .enum(["development", "preview", "production", "test"]) + .optional(), NEXT_PUBLIC_PORT: optionalString, NEXT_PUBLIC_POSTHOG_KEY: optionalString, NEXT_PUBLIC_POSTHOG_REGION: z.enum(["us", "eu"]).optional(), }, emptyStringAsUndefined: true, experimental__runtimeEnv: { + NEXT_PUBLIC_SITE_ORIGIN: process.env.NEXT_PUBLIC_SITE_ORIGIN, + NEXT_PUBLIC_DEPLOYMENT_ENV: process.env.NEXT_PUBLIC_DEPLOYMENT_ENV, NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT, NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_REGION: process.env.NEXT_PUBLIC_POSTHOG_REGION, @@ -27,9 +35,5 @@ export const env = createEnv({ NODE_ENV: z.enum(["development", "production", "test"]).optional(), OPENAI_API_KEY: optionalString, PORT: optionalString, - VERCEL: z.literal("1").optional(), - VERCEL_ENV: z.enum(["development", "preview", "production"]).optional(), - VERCEL_PROJECT_PRODUCTION_URL: optionalString, - VERCEL_URL: optionalString, }, }); diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts index 6503f459..08d2cce9 100644 --- a/src/instrumentation-client.ts +++ b/src/instrumentation-client.ts @@ -8,7 +8,7 @@ const projectToken = env.NEXT_PUBLIC_POSTHOG_KEY; if ( projectToken !== undefined && - process.env.NEXT_PUBLIC_DEPLOYMENT_ENV === "production" && + env.NEXT_PUBLIC_DEPLOYMENT_ENV === "production" && location.origin === CANONICAL_SITE_URL ) { posthog.init(projectToken, { diff --git a/src/lib/site-url.ts b/src/lib/site-url.ts index 3a15f7c1..c701c123 100644 --- a/src/lib/site-url.ts +++ b/src/lib/site-url.ts @@ -1,9 +1,4 @@ -interface SiteEnvironment { - VERCEL?: string; - VERCEL_PROJECT_PRODUCTION_URL?: string; - PORT?: string; - NEXT_PUBLIC_PORT?: string; -} +import { env } from "../env"; export const productionSiteOrigin = (hostname: string | undefined) => { const host = hostname?.trim(); @@ -30,7 +25,7 @@ export const productionSiteOrigin = (hostname: string | undefined) => { return url.origin; }; -export const siteOriginFrom = (environment: SiteEnvironment) => { +export const siteOriginFrom = (environment: Partial) => { if ( (environment.VERCEL_PROJECT_PRODUCTION_URL?.trim().length ?? 0) > 0 || environment.VERCEL === "1" @@ -44,10 +39,4 @@ export const siteOriginFrom = (environment: SiteEnvironment) => { // Next config supplies this public value to browser bundles. Scripts use Vercel's original variable. export const CANONICAL_SITE_URL = - process.env.NEXT_PUBLIC_SITE_ORIGIN ?? - siteOriginFrom({ - VERCEL: process.env.VERCEL, - VERCEL_PROJECT_PRODUCTION_URL: process.env.VERCEL_PROJECT_PRODUCTION_URL, - PORT: process.env.PORT, - NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT, - }); + env.NEXT_PUBLIC_SITE_ORIGIN ?? siteOriginFrom(env); diff --git a/tests/site-url.test.ts b/tests/site-url.test.ts index 45947b5f..c3187bb9 100644 --- a/tests/site-url.test.ts +++ b/tests/site-url.test.ts @@ -31,3 +31,27 @@ test("derives local and production URLs without an author-specific fallback", () } expect(() => siteOriginFrom({ VERCEL: "1" })).toThrow(); }); + +test("browser URL uses public configuration while T3 blocks server variables", () => { + const result = Bun.spawnSync( + [ + process.execPath, + "--eval", + ` + globalThis.window = {}; + const { env } = await import("./src/env.ts"); + const { CANONICAL_SITE_URL } = await import("./src/lib/site-url.ts"); + const blocked = ["DATABASE_URL", "GITHUB_TOKENS", "VERCEL_PROJECT_PRODUCTION_URL"].every(key => { + try { env[key]; return false; } catch { return true; } + }); + console.log(JSON.stringify({ origin: CANONICAL_SITE_URL, blocked })); + `, + ], + { env: { NEXT_PUBLIC_SITE_ORIGIN: "https://example.com" } } + ); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout.toString())).toEqual({ + origin: "https://example.com", + blocked: true, + }); +}); From 07f2c5f5f5b7b705232172753b66360beb6a02d0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 9 Sep 2026 00:21:16 +0000 Subject: [PATCH 6/7] docs: clarify explicit database connection setup --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 67b4e550..5381562c 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,13 @@ bun run dev ``` The website works without secrets; the persisted commit feed stays empty until -Postgres is configured. See [the commit sync guide](docs/github-commits.md) for -database, Supabase Cron, account polling, and webhook setup. +PostgreSQL is configured. Set `DATABASE_URL` for application queries and, when +using a runtime transaction pooler, `DATABASE_URL_UNPOOLED` to a direct or +session-pooler connection for migrations and cron setup. Supply these in +`.env.local` for local use and in Vercel environment settings for deployments; +the application does not provision a database or synchronize connection settings. +See [the commit sync guide](docs/github-commits.md) for Supabase Cron, account +polling, and webhook setup. The separate [Codex stats guide](docs/codex-stats.md) covers its encrypted account snapshots and scheduled sync. From 9b0c0142701174694048dfa118c7dc9adb81e2a4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 9 Sep 2026 00:24:59 +0000 Subject: [PATCH 7/7] refactor: simplify shared configuration and use native Vercel variables --- .env.example | 2 +- README.md | 14 +---- docs/analytics.md | 2 +- next.config.ts | 5 -- scripts/backfill-github-activity.ts | 5 +- scripts/configure-supabase-cron.ts | 21 +++---- scripts/migrate-production-database.ts | 10 ++-- src/content/resume.ts | 46 +++++++-------- src/db/schema.ts | 16 +++--- src/env.ts | 13 +++-- src/instrumentation-client.ts | 4 +- src/lib/github-accounts.ts | 6 +- src/lib/github-activity-store.ts | 6 +- src/lib/github-activity-worker.ts | 8 +-- src/lib/github-backfill-core.ts | 4 +- src/lib/github-commits-core.ts | 13 ++--- src/lib/github-ref-membership-store.ts | 6 +- src/lib/github-repository-inventory.ts | 6 +- src/lib/github-work-unit-projection-state.ts | 4 +- src/lib/github-work-unit-store.ts | 4 +- src/lib/github-work-unit-summary-store.ts | 4 +- src/lib/site-url.ts | 8 ++- src/proxy.ts | 2 +- tests/github-accounts.test.ts | 12 ++-- tests/site-url.test.ts | 60 +++++++++++++------- 25 files changed, 141 insertions(+), 140 deletions(-) diff --git a/.env.example b/.env.example index b35825e5..4e322694 100644 --- a/.env.example +++ b/.env.example @@ -31,5 +31,5 @@ VERCEL= VERCEL_ENV= VERCEL_PROJECT_PRODUCTION_URL= VERCEL_URL= -# NEXT_PUBLIC_SITE_ORIGIN and NEXT_PUBLIC_DEPLOYMENT_ENV are derived in next.config.ts. +# Vercel also supplies NEXT_PUBLIC_VERCEL_ENV and NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL at build time. # Codex account credentials belong in Supabase Vault; see docs/codex-stats.md. diff --git a/README.md b/README.md index 5381562c..7cd34d5a 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,6 @@ updates cron configuration. Migration `0022` only replaces seven username allowlists with valid-login checks; it adds no tables or columns and rewrites no stored history. Leave older applied migrations intact. -Vercel supplies the production hostname when system environment variables are -exposed. No manually maintained site URL or blog asset URL is needed. - ## Customize once - `src/content/resume.ts`: identity, social profiles, career, education and PDF paths. @@ -81,14 +78,9 @@ in the project settings. Local URLs use localhost and the configured port. There is no separately maintained site URL or blog asset base URL: exported Markdown uses the image URLs already emitted by the MDX compiler. -GitHub activity uses the public author list in `src/content/site.ts` and a separate -`GITHUB_TOKENS` JSON object, such as `{"alice":"","bob":""}`. -Each key references a configured author; credentials grant access and never select -which authors appear. Rotation, expiration or removal preserves stored history. -Polling and repository inventory verify the assigned token against the configured -GitHub identity. Webhooks, summaries and database-only processing need no GitHub -token. See the service guides before enabling activity or Codex stats. PostHog is -disabled unless a capture key is supplied and runs only on the canonical production host. +GitHub activity uses configured public authors and separate credentials. See the +[GitHub activity guide](docs/github-commits.md) for token format and service setup, +and the [analytics guide](docs/analytics.md) for optional PostHog configuration. Each installation needs its own database. Cron/Vault names are installation-wide; sharing one database between independent sites is unsupported. Set `vercel.json` diff --git a/docs/analytics.md b/docs/analytics.md index e88601e3..636b866d 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -5,7 +5,7 @@ The site uses PostHog US Cloud for pageviews, acquisition attribution, and expli ## Configuration - `src/instrumentation-client.ts` initializes the SDK only on the canonical production host when `NEXT_PUBLIC_POSTHOG_KEY` is configured. `NEXT_PUBLIC_POSTHOG_REGION` selects `us` (default) or `eu` for both the SDK and proxy; preview and local deployments do not capture events. -- `src/proxy.ts` forwards `/_r7k2/*` to fixed US ingestion and asset hosts. It strips Cookie, Authorization, and Referer headers. Collector trailing slashes are preserved; ordinary page trailing slashes receive a 308 redirect. +- `src/proxy.ts` forwards `/_r7k2/*` to the selected region’s ingestion and asset hosts. It strips Cookie, Authorization, and Referer headers. Collector trailing slashes are preserved; ordinary page trailing slashes receive a 308 redirect. - The SDK uses always-cookieless mode, memory persistence, and no person profiles. Autocapture, replay, surveys, heatmaps, automatic exceptions, performance collection, and feature flags are disabled. - Do Not Track and Global Privacy Control signals do not change capture behavior. - `src/lib/analytics.ts` contains the typed event contract, link classification, and property redaction. URL queries and fragments, referrer paths, ad click IDs, and search terms are removed. Campaign values must be short public slugs. diff --git a/next.config.ts b/next.config.ts index 2a11bc7c..772798dc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,13 +2,8 @@ import createMDX from "@next/mdx"; import type { NextConfig } from "next"; import { env } from "./src/env"; -import { siteOriginFrom } from "./src/lib/site-url"; const nextConfig: NextConfig = { - env: { - NEXT_PUBLIC_SITE_ORIGIN: siteOriginFrom(env), - NEXT_PUBLIC_DEPLOYMENT_ENV: env.VERCEL_ENV ?? env.NODE_ENV ?? "development", - }, headers: async () => env.VERCEL_ENV === "preview" ? [ diff --git a/scripts/backfill-github-activity.ts b/scripts/backfill-github-activity.ts index a241bbc2..ed64f2a7 100644 --- a/scripts/backfill-github-activity.ts +++ b/scripts/backfill-github-activity.ts @@ -41,10 +41,7 @@ interface BackfillArguments { startDate: string; } -interface BackfillEnvironment { - DATABASE_URL?: string; - GITHUB_TOKENS?: string; -} +type BackfillEnvironment = Pick; type GitHubBackfillProgressStage = | "identity" diff --git a/scripts/configure-supabase-cron.ts b/scripts/configure-supabase-cron.ts index 85595811..7ad034a7 100644 --- a/scripts/configure-supabase-cron.ts +++ b/scripts/configure-supabase-cron.ts @@ -29,16 +29,17 @@ const LEGACY_TAG_REFS_JOB_NAME = "github-tag-refs-every-fifteen-minutes"; // Keep the historical lock key so overlapping old/new deployments still coordinate. const CRON_CONFIGURATION_LOCK_NAME = "f0rr0.dev:supabase-cron"; -interface SupabaseCronEnvironment { - CRON_SECRET?: string; - GITHUB_TOKENS?: string; - OPENAI_API_KEY?: string; - DATABASE_URL?: string; - DATABASE_URL_UNPOOLED?: string; - VERCEL?: string; - VERCEL_ENV?: string; - VERCEL_PROJECT_PRODUCTION_URL?: string; -} +type SupabaseCronEnvironment = Pick< + typeof env, + | "CRON_SECRET" + | "GITHUB_TOKENS" + | "OPENAI_API_KEY" + | "DATABASE_URL" + | "DATABASE_URL_UNPOOLED" + | "VERCEL" + | "VERCEL_ENV" + | "VERCEL_PROJECT_PRODUCTION_URL" +>; const requiredEnvironmentValue = ( name: string, diff --git a/scripts/migrate-production-database.ts b/scripts/migrate-production-database.ts index 67756b68..b15d4c24 100644 --- a/scripts/migrate-production-database.ts +++ b/scripts/migrate-production-database.ts @@ -8,12 +8,10 @@ import { env } from "../src/env"; // Keep the historical lock key so overlapping old/new deployments still coordinate. const MIGRATION_LOCK_NAME = "f0rr0.dev:drizzle-migrations"; -interface Environment { - DATABASE_URL?: string; - DATABASE_URL_UNPOOLED?: string; - VERCEL?: string; - VERCEL_ENV?: string; -} +type Environment = Pick< + typeof env, + "DATABASE_URL" | "DATABASE_URL_UNPOOLED" | "VERCEL" | "VERCEL_ENV" +>; export class ProductionMigrationConfigurationError extends Error { constructor(message: string) { diff --git a/src/content/resume.ts b/src/content/resume.ts index 47168abd..285511f6 100644 --- a/src/content/resume.ts +++ b/src/content/resume.ts @@ -188,14 +188,28 @@ const dpsLogo: LogoAsset = { tileClassName: "bg-[#016b2f]", }; +const linkedInUsername = "f0rr0"; +const githubProfiles = githubAccounts.map(({ login }) => ({ + network: "GitHub", + username: login, + url: `https://github.com/${login}`, +})); +const [githubProfile] = githubProfiles; +if (githubProfile === undefined) { + throw new Error("Configure a public GitHub account in src/content/site.ts."); +} +export const primaryGitHubProfile = githubProfile; +export const socialProfiles = [ + ...githubProfiles, + { + network: "LinkedIn", + username: linkedInUsername, + url: `https://linkedin.com/in/${linkedInUsername}`, + }, +]; + const person = { - profiles: [ - ...githubAccounts.map(({ login }) => ({ - network: "GitHub", - url: `https://github.com/${login}`, - })), - { network: "LinkedIn", url: "https://linkedin.com/in/f0rr0" }, - ], + profiles: socialProfiles, avatarImage: "/resume/sid-jain-profile-avatar.png", email: "sid_26@outlook.com", image: "/resume/sid-jain-profile.png", @@ -207,24 +221,6 @@ const person = { "senior full-stack engineer, frontend-focused product engineer, and AI product engineer", }; -export const socialProfiles = person.profiles.map((profile) => { - const username = new URL(profile.url).pathname - .split("/") - .filter(Boolean) - .at(-1); - if (username === undefined) { - throw new Error("Social profile URLs must include a username."); - } - return { ...profile, username }; -}); -const githubProfile = socialProfiles.find( - (profile) => profile.network === "GitHub" -); -if (githubProfile === undefined) { - throw new Error("Configure a public GitHub profile in resume.ts."); -} -export const primaryGitHubProfile = githubProfile; - export const resumeData = { lastUpdated: "2026-09-06", person: { diff --git a/src/db/schema.ts b/src/db/schema.ts index 3732e160..34e3ecf7 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -23,6 +23,8 @@ import type { GitHubWorkUnitFileFact, } from "@/lib/github-change-evidence"; +const githubLoginPattern = sql`'^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'`; + export const githubRepositories = pgTable( "github_repositories", { @@ -213,7 +215,7 @@ export const githubCommits = pgTable( ), check( "github_commits_tracked_author", - sql`${table.author} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.author} ~ ${githubLoginPattern}` ), check( "github_commits_nonnegative_activity_counts", @@ -317,7 +319,7 @@ export const githubAccountCheckpoints = pgTable( (table) => [ check( "github_account_checkpoints_tracked_account", - sql`${table.account} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.account} ~ ${githubLoginPattern}` ), check( "github_account_checkpoints_event_id_shape", @@ -469,7 +471,7 @@ export const githubWebhookDeliveries = pgTable( ), check( "github_webhook_deliveries_tracked_account", - sql`${table.account} IS NULL OR ${table.account} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.account} IS NULL OR ${table.account} ~ ${githubLoginPattern}` ), ] ).enableRLS(); @@ -544,7 +546,7 @@ export const githubPushObservations = pgTable( }), check( "github_push_observations_tracked_account", - sql`${table.account} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.account} ~ ${githubLoginPattern}` ), check( "github_push_observations_source", @@ -693,7 +695,7 @@ export const githubPullRequests = pgTable( ), check( "github_pull_requests_tracked_account", - sql`${table.account} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.account} ~ ${githubLoginPattern}` ), check( "github_pull_requests_state", @@ -774,7 +776,7 @@ export const githubPullRequestSignals = pgTable( ), check( "github_pull_request_signals_account", - sql`${table.account} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.account} ~ ${githubLoginPattern}` ), check( "github_pull_request_signals_event_id", @@ -978,7 +980,7 @@ export const githubIssues = pgTable( index("github_issues_author_idx").on(table.authorUserId, table.createdAt), check( "github_issues_tracked_account", - sql`${table.account} ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'` + sql`${table.account} ~ ${githubLoginPattern}` ), check("github_issues_positive_number", sql`${table.number} > 0`), ] diff --git a/src/env.ts b/src/env.ts index 54952147..7a23e2be 100644 --- a/src/env.ts +++ b/src/env.ts @@ -7,18 +7,19 @@ const optionalString = z.string().trim().min(1).optional(); export const env = createEnv({ extends: [vercel()], client: { - NEXT_PUBLIC_SITE_ORIGIN: z.url().optional(), - NEXT_PUBLIC_DEPLOYMENT_ENV: z - .enum(["development", "preview", "production", "test"]) + NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: optionalString, + NEXT_PUBLIC_VERCEL_ENV: z + .enum(["development", "preview", "production"]) .optional(), NEXT_PUBLIC_PORT: optionalString, NEXT_PUBLIC_POSTHOG_KEY: optionalString, - NEXT_PUBLIC_POSTHOG_REGION: z.enum(["us", "eu"]).optional(), + NEXT_PUBLIC_POSTHOG_REGION: z.enum(["us", "eu"]).default("us"), }, emptyStringAsUndefined: true, experimental__runtimeEnv: { - NEXT_PUBLIC_SITE_ORIGIN: process.env.NEXT_PUBLIC_SITE_ORIGIN, - NEXT_PUBLIC_DEPLOYMENT_ENV: process.env.NEXT_PUBLIC_DEPLOYMENT_ENV, + NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: + process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL, + NEXT_PUBLIC_VERCEL_ENV: process.env.NEXT_PUBLIC_VERCEL_ENV, NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT, NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_REGION: process.env.NEXT_PUBLIC_POSTHOG_REGION, diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts index 08d2cce9..2acc3aad 100644 --- a/src/instrumentation-client.ts +++ b/src/instrumentation-client.ts @@ -8,12 +8,12 @@ const projectToken = env.NEXT_PUBLIC_POSTHOG_KEY; if ( projectToken !== undefined && - env.NEXT_PUBLIC_DEPLOYMENT_ENV === "production" && + env.NEXT_PUBLIC_VERCEL_ENV === "production" && location.origin === CANONICAL_SITE_URL ) { posthog.init(projectToken, { api_host: "/_r7k2", - ui_host: `https://${env.NEXT_PUBLIC_POSTHOG_REGION ?? "us"}.posthog.com`, + ui_host: `https://${env.NEXT_PUBLIC_POSTHOG_REGION}.posthog.com`, defaults: "2026-08-30", cookieless_mode: "always", persistence: "memory", diff --git a/src/lib/github-accounts.ts b/src/lib/github-accounts.ts index 870344f5..b4246ae2 100644 --- a/src/lib/github-accounts.ts +++ b/src/lib/github-accounts.ts @@ -1,13 +1,13 @@ import { z } from "zod"; import { env } from "@/env"; -import { trackedGitHubAccounts } from "@/lib/github-commits-core"; +import { TRACKED_GITHUB_ACCOUNTS } from "@/lib/github-commits-core"; const tokensSchema = z.record(z.string(), z.string().trim().min(1)); export const githubTokensFrom = ( value?: string, - accounts: readonly string[] = trackedGitHubAccounts() + accounts: readonly string[] = TRACKED_GITHUB_ACCOUNTS ): Record => { if (value === undefined || value.trim() === "") { return {}; @@ -34,7 +34,7 @@ export const githubTokensFrom = ( export const tokenForGitHubAccount = ( login: string, - environment: { GITHUB_TOKENS?: string } = env + environment: Pick = env ) => { const tokens = githubTokensFrom(environment.GITHUB_TOKENS); const token = Object.hasOwn(tokens, login) ? tokens[login] : undefined; diff --git a/src/lib/github-activity-store.ts b/src/lib/github-activity-store.ts index 731d95a2..fb999bc6 100644 --- a/src/lib/github-activity-store.ts +++ b/src/lib/github-activity-store.ts @@ -23,7 +23,7 @@ import type { PublicGitHubActivityRepository, PublicGitHubWorkUnitKind, } from "@/lib/github-activity-types"; -import { trackedGitHubUserIds } from "@/lib/github-commits-core"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { decodeGitHubWorkUnitSummary, GITHUB_WORK_UNIT_SUMMARY_RECIPE, @@ -267,7 +267,7 @@ const readAvailableDays = async ( beforeIssue, inArray( githubIssues.authorUserId, - Object.values(trackedGitHubUserIds()) + Object.values(TRACKED_GITHUB_USER_IDS) ), inArray(githubRepositories.visibility, [ "public", @@ -378,7 +378,7 @@ const readPublicRows = async ( inArray(issueDay, selectedDays), inArray( githubIssues.authorUserId, - Object.values(trackedGitHubUserIds()) + Object.values(TRACKED_GITHUB_USER_IDS) ), inArray(githubRepositories.visibility, [ "public", diff --git a/src/lib/github-activity-worker.ts b/src/lib/github-activity-worker.ts index 9b824b14..cdc83002 100644 --- a/src/lib/github-activity-worker.ts +++ b/src/lib/github-activity-worker.ts @@ -55,7 +55,7 @@ import { } from "@/lib/github-api"; import { repositoryIdFrom, - trackedGitHubAccounts, + TRACKED_GITHUB_ACCOUNTS, } from "@/lib/github-commits-core"; import type { TrackedGitHubAccount } from "@/lib/github-commits-core"; import { @@ -225,7 +225,7 @@ export const githubActivityFailureIsTerminal = ( }; const activeTrackedAccounts = async ( - requestedAccounts: readonly TrackedGitHubAccount[] = trackedGitHubAccounts() + requestedAccounts: readonly TrackedGitHubAccount[] = TRACKED_GITHUB_ACCOUNTS ) => { const accounts: TrackedGitHubAccount[] = []; for (const account of requestedAccounts) { @@ -597,11 +597,11 @@ const processPullRequests = async ( const checkedWorkerAccounts = ( accounts: readonly TrackedGitHubAccount[] | undefined ) => { - const requested = accounts ?? trackedGitHubAccounts(); + const requested = accounts ?? TRACKED_GITHUB_ACCOUNTS; if ( requested.length === 0 || new Set(requested).size !== requested.length || - requested.some((account) => !trackedGitHubAccounts().includes(account)) + requested.some((account) => !TRACKED_GITHUB_ACCOUNTS.includes(account)) ) { throw new RangeError( "The GitHub activity worker account scope is invalid." diff --git a/src/lib/github-backfill-core.ts b/src/lib/github-backfill-core.ts index 8598c4b5..23b8f182 100644 --- a/src/lib/github-backfill-core.ts +++ b/src/lib/github-backfill-core.ts @@ -1,7 +1,7 @@ import type { GitHubFactualWorkerBacklog } from "@/lib/github-backfill-store"; import { repositoryIdFrom, - trackedGitHubAccounts, + TRACKED_GITHUB_ACCOUNTS, trackedGitHubAccountFrom, } from "@/lib/github-commits-core"; import type { TrackedGitHubAccount } from "@/lib/github-commits-core"; @@ -85,7 +85,7 @@ export const githubBackfillRequestFrom = ( return null; } const accounts = - account === "all" ? trackedGitHubAccounts() : ([account] as const); + account === "all" ? TRACKED_GITHUB_ACCOUNTS : ([account] as const); return { accounts, endDate: endDay.toISOString().slice(0, 10), diff --git a/src/lib/github-commits-core.ts b/src/lib/github-commits-core.ts index a6a085ec..b55d61d3 100644 --- a/src/lib/github-commits-core.ts +++ b/src/lib/github-commits-core.ts @@ -1,8 +1,7 @@ import { githubAccounts } from "@/content/site"; -export const trackedGitHubAccounts = () => - githubAccounts.map(({ login }) => login); -export const trackedGitHubUserIds = (): Record => +export const TRACKED_GITHUB_ACCOUNTS = githubAccounts.map(({ login }) => login); +export const TRACKED_GITHUB_USER_IDS: Readonly> = Object.fromEntries(githubAccounts.map(({ login, id }) => [login, id])); const COMMIT_SHA = /^[a-f0-9]{40}$/; @@ -157,7 +156,7 @@ export const githubCommitReferenceValuesFrom = ( firstObservedAt: Date ) => ({ author: commit.author, - authorUserId: trackedGitHubUserIds()[commit.author], + authorUserId: TRACKED_GITHUB_USER_IDS[commit.author], committedAt: new Date(commit.committedAt), firstObservedAt, message: commit.message, @@ -310,7 +309,7 @@ export const trackedGitHubAccountFrom = ( value: unknown ): TrackedGitHubAccount | null => { const login = normalizedText(value, 39)?.toLowerCase(); - return trackedGitHubAccounts().find((account) => account === login) ?? null; + return TRACKED_GITHUB_ACCOUNTS.find((account) => account === login) ?? null; }; export const trackedGitHubAccountFromUserId = ( @@ -319,8 +318,8 @@ export const trackedGitHubAccountFromUserId = ( const userId = repositoryIdFrom(value); return userId === null ? null - : (trackedGitHubAccounts().find( - (account) => trackedGitHubUserIds()[account] === userId + : (TRACKED_GITHUB_ACCOUNTS.find( + (account) => TRACKED_GITHUB_USER_IDS[account] === userId ) ?? null); }; diff --git a/src/lib/github-ref-membership-store.ts b/src/lib/github-ref-membership-store.ts index 8e0d84fb..4b9a6f59 100644 --- a/src/lib/github-ref-membership-store.ts +++ b/src/lib/github-ref-membership-store.ts @@ -29,7 +29,7 @@ import type { } from "@/lib/github-activity-processor"; import { githubActivityRetryAt } from "@/lib/github-activity-worker-core"; import { - trackedGitHubUserIds, + TRACKED_GITHUB_USER_IDS, trackedGitHubAccountFrom, } from "@/lib/github-commits-core"; import type { TrackedGitHubAccount } from "@/lib/github-commits-core"; @@ -409,7 +409,7 @@ export const validateGitHubRefRepairSource = ( const trackedShas = new Set(); for (const commit of source.commits) { if ( - !Object.hasOwn(trackedGitHubUserIds(), commit.author) || + !Object.hasOwn(TRACKED_GITHUB_USER_IDS, commit.author) || commit.repositoryId !== repair.repositoryId || commit.repository !== repair.repository || !reachableShas.has(commit.sha) || @@ -470,7 +470,7 @@ export const completeGitHubRefRepair = async ( .values( source.commits.map((commit) => ({ author: commit.author, - authorUserId: trackedGitHubUserIds()[commit.author], + authorUserId: TRACKED_GITHUB_USER_IDS[commit.author], committedAt: new Date(commit.committedAt), firstObservedAt: repair.observedAt, message: commit.message, diff --git a/src/lib/github-repository-inventory.ts b/src/lib/github-repository-inventory.ts index 8b7ab4b7..3e5cbf73 100644 --- a/src/lib/github-repository-inventory.ts +++ b/src/lib/github-repository-inventory.ts @@ -17,7 +17,7 @@ import { githubRepositoryInventoryHeads, githubRepositoryRefs, } from "@/db/schema"; -import { trackedGitHubUserIds } from "@/lib/github-commits-core"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import type { GitHubRepositoryInventoryFacts, TrackedGitHubAccount, @@ -55,7 +55,7 @@ const claimGitHubRepositoryInventoryRefresh = async (input: { force: boolean; now: Date; }): Promise => { - const accountUserId = trackedGitHubUserIds()[input.account]; + const accountUserId = TRACKED_GITHUB_USER_IDS[input.account]; const staleBefore = new Date( input.now.getTime() - INVENTORY_REFRESH_INTERVAL_MS ); @@ -236,7 +236,7 @@ const publishGitHubRepositoryInventory = async ( const readCurrentGitHubRepositoryInventory = async ( account: TrackedGitHubAccount ): Promise => { - const accountUserId = trackedGitHubUserIds()[account]; + const accountUserId = TRACKED_GITHUB_USER_IDS[account]; return await getDatabase().transaction( async (transaction) => { const [head] = await transaction diff --git a/src/lib/github-work-unit-projection-state.ts b/src/lib/github-work-unit-projection-state.ts index 642a2b20..37d1dda4 100644 --- a/src/lib/github-work-unit-projection-state.ts +++ b/src/lib/github-work-unit-projection-state.ts @@ -4,7 +4,7 @@ import { and, eq, sql } from "drizzle-orm"; import { getDatabase } from "@/db/client"; import { githubPublicFeedHead } from "@/db/schema"; -import { trackedGitHubUserIds } from "@/lib/github-commits-core"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { GITHUB_WORK_UNIT_SUMMARY_POLICY_DIGEST } from "@/lib/github-work-unit-summary"; type Database = ReturnType; @@ -19,7 +19,7 @@ const PROJECTION_POLICY = const PIPELINE_POLICY_DIGEST = createHash("sha256") .update( JSON.stringify({ - authors: Object.values(trackedGitHubUserIds()).toSorted(), + authors: Object.values(TRACKED_GITHUB_USER_IDS).toSorted(), projection: PROJECTION_POLICY, summary: GITHUB_WORK_UNIT_SUMMARY_POLICY_DIGEST, }) diff --git a/src/lib/github-work-unit-store.ts b/src/lib/github-work-unit-store.ts index 4b4db605..5088382e 100644 --- a/src/lib/github-work-unit-store.ts +++ b/src/lib/github-work-unit-store.ts @@ -23,7 +23,7 @@ import type { GitHubLanguageFact, GitHubWorkUnitFileFact, } from "@/lib/github-change-evidence"; -import { trackedGitHubUserIds } from "@/lib/github-commits-core"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { chooseEffectivePullRequest, githubLogicalChangeKey, @@ -62,6 +62,7 @@ const SUMMARY_EVALUATION_LIMIT = 8; const SUMMARY_DEBOUNCE_MS = 5 * 60 * 1000; const DIGEST = /^[a-f0-9]{64}$/u; const SHA = /^[a-f0-9]{40}$/u; +const trackedAuthorUserIds = new Set(Object.values(TRACKED_GITHUB_USER_IDS)); type GitHubWorkUnitDatabase = ReturnType; type GitHubWorkUnitTransaction = Parameters< @@ -771,7 +772,6 @@ const loadProjectionSnapshot = async ( transaction: GitHubWorkUnitTransaction, { lockCurrentUnits, summaryEvaluationLimit }: ProjectionSnapshotOptions ): Promise => { - const trackedAuthorUserIds = new Set(Object.values(trackedGitHubUserIds())); const currentUnits = await readCurrentUnits(transaction, lockCurrentUnits); const repositoryRows = await transaction .select({ diff --git a/src/lib/github-work-unit-summary-store.ts b/src/lib/github-work-unit-summary-store.ts index e1b63c09..c80ed1ed 100644 --- a/src/lib/github-work-unit-summary-store.ts +++ b/src/lib/github-work-unit-summary-store.ts @@ -26,7 +26,7 @@ import { } from "@/db/schema"; import { env } from "@/env"; import { PUBLIC_GITHUB_ACTIVITY_DAY_PAGE_SIZE } from "@/lib/github-activity-store"; -import { trackedGitHubUserIds } from "@/lib/github-commits-core"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { GITHUB_SUMMARY_REQUEST_BUDGET } from "@/lib/github-cron-config"; import { acquireGitHubWorkUnitProjectionLock } from "@/lib/github-work-unit-projection-state"; import { @@ -648,7 +648,7 @@ async function readInitialPageDays(transaction: SummaryTransaction) { and( inArray( githubIssues.authorUserId, - Object.values(trackedGitHubUserIds()) + Object.values(TRACKED_GITHUB_USER_IDS) ), inArray(githubRepositories.visibility, [ "public", diff --git a/src/lib/site-url.ts b/src/lib/site-url.ts index c701c123..7c84363a 100644 --- a/src/lib/site-url.ts +++ b/src/lib/site-url.ts @@ -37,6 +37,10 @@ export const siteOriginFrom = (environment: Partial) => { ).origin; }; -// Next config supplies this public value to browser bundles. Scripts use Vercel's original variable. +// Vercel provides the public hostname at build time; standalone scripts use its server variable. export const CANONICAL_SITE_URL = - env.NEXT_PUBLIC_SITE_ORIGIN ?? siteOriginFrom(env); + env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL === undefined + ? typeof window === "undefined" + ? siteOriginFrom(env) + : window.location.origin + : productionSiteOrigin(env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL); diff --git a/src/proxy.ts b/src/proxy.ts index 72e3c8ab..e65345a3 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -14,7 +14,7 @@ export function proxy(request: NextRequest) { } url.pathname = url.pathname.slice("/_r7k2".length) || "/"; - const region = env.NEXT_PUBLIC_POSTHOG_REGION ?? "us"; + const region = env.NEXT_PUBLIC_POSTHOG_REGION; url.hostname = url.pathname.startsWith("/static/") || url.pathname.startsWith("/array/") ? `${region}-assets.i.posthog.com` diff --git a/tests/github-accounts.test.ts b/tests/github-accounts.test.ts index 374e0ebe..04f63cf8 100644 --- a/tests/github-accounts.test.ts +++ b/tests/github-accounts.test.ts @@ -7,8 +7,8 @@ import { } from "../src/lib/github-accounts"; import { assertGitHubTokenIdentity } from "../src/lib/github-commits"; import { - trackedGitHubAccounts, - trackedGitHubUserIds, + TRACKED_GITHUB_ACCOUNTS, + TRACKED_GITHUB_USER_IDS, } from "../src/lib/github-commits-core"; import { env, mockFetch } from "./helpers"; @@ -51,8 +51,8 @@ test("credential rotation, removal and order never change authors or require net globalThis.fetch = mockFetch(() => { throw new Error("Unexpected discovery request"); }); - const authors = trackedGitHubUserIds(); - const accounts = trackedGitHubAccounts(); + const authors = TRACKED_GITHUB_USER_IDS; + const accounts = TRACKED_GITHUB_ACCOUNTS; delete env.GITHUB_TOKEN; delete env.GH_TOKEN; for (const tokens of [ @@ -62,8 +62,8 @@ test("credential rotation, removal and order never change authors or require net {}, ]) { env.GITHUB_TOKENS = JSON.stringify(tokens); - expect(trackedGitHubUserIds()).toEqual(authors); - expect(trackedGitHubAccounts()).toEqual(accounts); + expect(TRACKED_GITHUB_USER_IDS).toEqual(authors); + expect(TRACKED_GITHUB_ACCOUNTS).toEqual(accounts); expect(tokensForGitHubAccount()).toEqual(Object.values(tokens)); } expect(() => tokenForGitHubAccount("f0rr0")).toThrow("No GitHub token"); diff --git a/tests/site-url.test.ts b/tests/site-url.test.ts index c3187bb9..6a076be6 100644 --- a/tests/site-url.test.ts +++ b/tests/site-url.test.ts @@ -32,26 +32,42 @@ test("derives local and production URLs without an author-specific fallback", () expect(() => siteOriginFrom({ VERCEL: "1" })).toThrow(); }); -test("browser URL uses public configuration while T3 blocks server variables", () => { - const result = Bun.spawnSync( - [ - process.execPath, - "--eval", - ` - globalThis.window = {}; - const { env } = await import("./src/env.ts"); - const { CANONICAL_SITE_URL } = await import("./src/lib/site-url.ts"); - const blocked = ["DATABASE_URL", "GITHUB_TOKENS", "VERCEL_PROJECT_PRODUCTION_URL"].every(key => { - try { env[key]; return false; } catch { return true; } - }); - console.log(JSON.stringify({ origin: CANONICAL_SITE_URL, blocked })); - `, - ], - { env: { NEXT_PUBLIC_SITE_ORIGIN: "https://example.com" } } - ); - expect(result.exitCode).toBe(0); - expect(JSON.parse(result.stdout.toString())).toEqual({ - origin: "https://example.com", - blocked: true, - }); +test("browser deployment configuration keeps previews and local development out of analytics", () => { + for (const deployment of ["production", "preview", "development"]) { + const result = Bun.spawnSync( + [ + process.execPath, + "--eval", + ` + globalThis.window = { location: { origin: "http://localhost:4200" } }; + const { env } = await import("./src/env.ts"); + const { CANONICAL_SITE_URL } = await import("./src/lib/site-url.ts"); + const blocked = ["DATABASE_URL", "GITHUB_TOKENS", "VERCEL_PROJECT_PRODUCTION_URL"].every(key => { + try { env[key]; return false; } catch { return true; } + }); + console.log(JSON.stringify({ origin: CANONICAL_SITE_URL, blocked, production: env.NEXT_PUBLIC_VERCEL_ENV === "production", region: env.NEXT_PUBLIC_POSTHOG_REGION })); + `, + ], + { + env: + deployment === "development" + ? {} + : { + NEXT_PUBLIC_VERCEL_ENV: deployment, + NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: "example.com", + NODE_ENV: "production", + }, + } + ); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout.toString())).toEqual({ + origin: + deployment === "development" + ? "http://localhost:4200" + : "https://example.com", + production: deployment === "production", + region: "us", + blocked: true, + }); + } });