A server-side link shortening service powered by Linkat

Initial commit: add files

+14
.env.example
··· 1 + # AT Protocol Link Shortener Configuration 2 + # 3 + # Your AT Protocol DID (e.g., did:plc:abc123xyz or did:web:example.com) 4 + # Find your DID at: https://pdsls.dev/ 5 + # 6 + # This is the only required configuration! 7 + # The service will automatically: 8 + # - Resolve your PDS endpoint 9 + # - Fetch your Linkat board data 10 + # - Create short links from your link titles 11 + # 12 + # Test your configuration with: npm run test:config 13 + 14 + ATPROTO_DID=
+23
.gitignore
··· 1 + node_modules 2 + 3 + # Output 4 + .output 5 + .vercel 6 + .netlify 7 + .wrangler 8 + /.svelte-kit 9 + /build 10 + 11 + # OS 12 + .DS_Store 13 + Thumbs.db 14 + 15 + # Env 16 + .env 17 + .env.* 18 + !.env.example 19 + !.env.test 20 + 21 + # Vite 22 + vite.config.js.timestamp-* 23 + vite.config.ts.timestamp-*
+1
.npmrc
··· 1 + engine-strict=true
+9
.prettierignore
··· 1 + # Package Managers 2 + package-lock.json 3 + pnpm-lock.yaml 4 + yarn.lock 5 + bun.lock 6 + bun.lockb 7 + 8 + # Miscellaneous 9 + /static/
+15
.prettierrc
··· 1 + { 2 + "useTabs": true, 3 + "singleQuote": true, 4 + "trailingComma": "none", 5 + "printWidth": 100, 6 + "plugins": ["prettier-plugin-svelte"], 7 + "overrides": [ 8 + { 9 + "files": "*.svelte", 10 + "options": { 11 + "parser": "svelte" 12 + } 13 + } 14 + ] 15 + }
+8
.vscode/settings.json
··· 1 + { 2 + "cSpell.words": [ 3 + "atproto", 4 + "FOUC", 5 + "Linkat", 6 + "shortlink" 7 + ] 8 + }
+236
ARCHITECTURE.md
··· 1 + # Code Architecture 2 + 3 + This document describes the modular architecture of the AT Protocol Link Shortener. 4 + 5 + ## Directory Structure 6 + 7 + ``` 8 + src/ 9 + ├── lib/ 10 + │ ├── constants.ts # Application-wide constants 11 + │ ├── index.ts # Main library exports 12 + │ ├── services/ 13 + │ │ ├── atproto/ # AT Protocol services 14 + │ │ │ ├── agent-factory.ts # Agent creation utilities 15 + │ │ │ ├── agent-manager.ts # Agent caching and fallback 16 + │ │ │ ├── identity-resolver.ts # Slingshot DID resolution 17 + │ │ │ └── index.ts # Module exports 18 + │ │ ├── cache/ # Caching utilities 19 + │ │ │ └── index.ts # Generic TTL cache 20 + │ │ ├── linkat/ # Linkat service 21 + │ │ │ ├── fetcher.ts # Raw board fetching 22 + │ │ │ ├── generator.ts # Shortcode generation 23 + │ │ │ └── index.ts # Main service with caching 24 + │ │ ├── agent.ts # Backwards compatibility 25 + │ │ ├── linkat.ts # Backwards compatibility 26 + │ │ └── types.ts # Shared type definitions 27 + │ └── utils/ 28 + │ └── encoding.ts # URL encoding utilities 29 + └── routes/ 30 + ├── +layout.svelte # Global layout with dark mode 31 + ├── +page.server.ts # Homepage server logic 32 + ├── +page.svelte # Homepage UI 33 + ├── [shortcode]/ 34 + │ └── +server.ts # Redirect handler 35 + ├── api/ 36 + │ └── links/ 37 + │ └── +server.ts # Links API endpoint 38 + └── favicon/ 39 + └── favicon.ico/ 40 + └── +server.ts # Favicon handler 41 + ``` 42 + 43 + ## Module Responsibilities 44 + 45 + ### Constants (`lib/constants.ts`) 46 + 47 + Central location for all configuration values: 48 + 49 + - Cache settings (TTL, prefixes) 50 + - Shortcode configuration (length, base62 chars) 51 + - AT Protocol endpoints (Slingshot, public API) 52 + - HTTP status codes 53 + 54 + **Benefits:** 55 + 56 + - Single source of truth 57 + - Easy to modify configuration 58 + - Type-safe constants 59 + 60 + ### AT Protocol Services (`lib/services/atproto/`) 61 + 62 + #### `agent-factory.ts` 63 + 64 + - Creates AtpAgent instances 65 + - Handles fetch function injection for server-side contexts 66 + - Wraps fetch to ensure proper headers 67 + 68 + #### `identity-resolver.ts` 69 + 70 + - Resolves DIDs to PDS endpoints via Slingshot 71 + - Error handling and logging 72 + - Uses constants for endpoint configuration 73 + 74 + #### `agent-manager.ts` 75 + 76 + - Manages agent lifecycle and caching 77 + - Provides fallback logic (PDS → public API) 78 + - Exports helper functions for common operations 79 + 80 + **Benefits:** 81 + 82 + - Separation of concerns 83 + - Easy to test individual components 84 + - Reusable across different contexts 85 + 86 + ### Cache Service (`lib/services/cache/`) 87 + 88 + Generic TTL-based cache implementation: 89 + 90 + - Set/get/delete operations 91 + - Automatic expiration 92 + - Cache pruning utility 93 + - Type-safe generic interface 94 + 95 + **Benefits:** 96 + 97 + - Reusable for any cached data 98 + - Not tied to specific use case 99 + - Clean API with proper TypeScript support 100 + 101 + ### Linkat Service (`lib/services/linkat/`) 102 + 103 + #### `fetcher.ts` 104 + 105 + - Raw Linkat board data fetching 106 + - AT Protocol record retrieval 107 + - Data validation 108 + 109 + #### `generator.ts` 110 + 111 + - Shortcode generation from URLs 112 + - Collision handling 113 + - Link search functionality 114 + 115 + #### `index.ts` 116 + 117 + - Main service interface 118 + - Combines fetching + generation 119 + - Implements caching layer 120 + 121 + **Benefits:** 122 + 123 + - Pure functions in fetcher and generator (easy to test) 124 + - Side effects isolated to main service 125 + - Clear data flow 126 + 127 + ### Utilities (`lib/utils/`) 128 + 129 + #### `encoding.ts` 130 + 131 + - URL to shortcode encoding 132 + - Base62 conversion 133 + - Validation helpers 134 + 135 + **Benefits:** 136 + 137 + - Stateless utility functions 138 + - Reusable across application 139 + - Easy to unit test 140 + 141 + ## Backwards Compatibility 142 + 143 + The old `agent.ts` and `linkat.ts` files remain as re-export wrappers: 144 + 145 + - Existing imports continue to work 146 + - No breaking changes to route handlers 147 + - Smooth migration path 148 + 149 + ## Design Principles 150 + 151 + ### 1. Single Responsibility 152 + 153 + Each module has one clear purpose: 154 + 155 + - `agent-factory`: Create agents 156 + - `identity-resolver`: Resolve DIDs 157 + - `cache`: Cache data 158 + 159 + ### 2. Dependency Injection 160 + 161 + - Fetch functions can be injected 162 + - Agents are created with custom configs 163 + - Makes testing easier 164 + 165 + ### 3. Separation of Concerns 166 + 167 + - Pure logic in utilities 168 + - Side effects in services 169 + - UI in routes 170 + 171 + ### 4. Type Safety 172 + 173 + - Explicit TypeScript types 174 + - Shared type definitions 175 + - Constants with `as const` 176 + 177 + ### 5. Testability 178 + 179 + - Pure functions are easy to test 180 + - Services use dependency injection 181 + - Clear interfaces 182 + 183 + ## Import Patterns 184 + 185 + ### Using the main library export: 186 + 187 + ```typescript 188 + import { getShortLinks, encodeUrl, CACHE } from '$lib'; 189 + ``` 190 + 191 + ### Using specific modules: 192 + 193 + ```typescript 194 + import { getPublicAgent } from '$lib/services/atproto'; 195 + import { Cache } from '$lib/services/cache'; 196 + ``` 197 + 198 + ### Using backwards-compatible imports: 199 + 200 + ```typescript 201 + import { createAgentForDID } from '$lib/services/agent'; 202 + import { findShortLink } from '$lib/services/linkat'; 203 + ``` 204 + 205 + ## Future Improvements 206 + 207 + ### Easy to Add: 208 + 209 + 1. **Database caching** - Replace in-memory cache 210 + 2. **Custom shortcodes** - Extend generator 211 + 3. **Analytics** - Add tracking module 212 + 4. **Rate limiting** - Add middleware 213 + 5. **Multiple DID support** - Extend agent manager 214 + 215 + ### Testing Strategy: 216 + 217 + 1. **Unit tests** for utils and pure functions 218 + 2. **Integration tests** for services 219 + 3. **E2E tests** for routes 220 + 221 + ## Configuration 222 + 223 + All configuration is centralized in `constants.ts`: 224 + 225 + - Change cache TTL in one place 226 + - Update API endpoints easily 227 + - Modify shortcode length globally 228 + 229 + ## Error Handling 230 + 231 + Consistent error handling pattern: 232 + 233 + 1. Log errors with context 234 + 2. Throw with descriptive messages 235 + 3. Fallback to sensible defaults 236 + 4. Surface errors to users when appropriate
+661
LICENCE
··· 1 + GNU AFFERO GENERAL PUBLIC LICENSE 2 + Version 3, 19 November 2007 3 + 4 + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 5 + Everyone is permitted to copy and distribute verbatim copies 6 + of this license document, but changing it is not allowed. 7 + 8 + Preamble 9 + 10 + The GNU Affero General Public License is a free, copyleft license for 11 + software and other kinds of works, specifically designed to ensure 12 + cooperation with the community in the case of network server software. 13 + 14 + The licenses for most software and other practical works are designed 15 + to take away your freedom to share and change the works. By contrast, 16 + our General Public Licenses are intended to guarantee your freedom to 17 + share and change all versions of a program--to make sure it remains free 18 + software for all its users. 19 + 20 + When we speak of free software, we are referring to freedom, not 21 + price. Our General Public Licenses are designed to make sure that you 22 + have the freedom to distribute copies of free software (and charge for 23 + them if you wish), that you receive source code or can get it if you 24 + want it, that you can change the software or use pieces of it in new 25 + free programs, and that you know you can do these things. 26 + 27 + Developers that use our General Public Licenses protect your rights 28 + with two steps: (1) assert copyright on the software, and (2) offer 29 + you this License which gives you legal permission to copy, distribute 30 + and/or modify the software. 31 + 32 + A secondary benefit of defending all users' freedom is that 33 + improvements made in alternate versions of the program, if they 34 + receive widespread use, become available for other developers to 35 + incorporate. Many developers of free software are heartened and 36 + encouraged by the resulting cooperation. However, in the case of 37 + software used on network servers, this result may fail to come about. 38 + The GNU General Public License permits making a modified version and 39 + letting the public access it on a server without ever releasing its 40 + source code to the public. 41 + 42 + The GNU Affero General Public License is designed specifically to 43 + ensure that, in such cases, the modified source code becomes available 44 + to the community. It requires the operator of a network server to 45 + provide the source code of the modified version running there to the 46 + users of that server. Therefore, public use of a modified version, on 47 + a publicly accessible server, gives the public access to the source 48 + code of the modified version. 49 + 50 + An older license, called the Affero General Public License and 51 + published by Affero, was designed to accomplish similar goals. This is 52 + a different license, not a version of the Affero GPL, but Affero has 53 + released a new version of the Affero GPL which permits relicensing under 54 + this license. 55 + 56 + The precise terms and conditions for copying, distribution and 57 + modification follow. 58 + 59 + TERMS AND CONDITIONS 60 + 61 + 0. Definitions. 62 + 63 + "This License" refers to version 3 of the GNU Affero General Public License. 64 + 65 + "Copyright" also means copyright-like laws that apply to other kinds of 66 + works, such as semiconductor masks. 67 + 68 + "The Program" refers to any copyrightable work licensed under this 69 + License. Each licensee is addressed as "you". "Licensees" and 70 + "recipients" may be individuals or organizations. 71 + 72 + To "modify" a work means to copy from or adapt all or part of the work 73 + in a fashion requiring copyright permission, other than the making of an 74 + exact copy. The resulting work is called a "modified version" of the 75 + earlier work or a work "based on" the earlier work. 76 + 77 + A "covered work" means either the unmodified Program or a work based 78 + on the Program. 79 + 80 + To "propagate" a work means to do anything with it that, without 81 + permission, would make you directly or secondarily liable for 82 + infringement under applicable copyright law, except executing it on a 83 + computer or modifying a private copy. Propagation includes copying, 84 + distribution (with or without modification), making available to the 85 + public, and in some countries other activities as well. 86 + 87 + To "convey" a work means any kind of propagation that enables other 88 + parties to make or receive copies. Mere interaction with a user through 89 + a computer network, with no transfer of a copy, is not conveying. 90 + 91 + An interactive user interface displays "Appropriate Legal Notices" 92 + to the extent that it includes a convenient and prominently visible 93 + feature that (1) displays an appropriate copyright notice, and (2) 94 + tells the user that there is no warranty for the work (except to the 95 + extent that warranties are provided), that licensees may convey the 96 + work under this License, and how to view a copy of this License. If 97 + the interface presents a list of user commands or options, such as a 98 + menu, a prominent item in the list meets this criterion. 99 + 100 + 1. Source Code. 101 + 102 + The "source code" for a work means the preferred form of the work 103 + for making modifications to it. "Object code" means any non-source 104 + form of a work. 105 + 106 + A "Standard Interface" means an interface that either is an official 107 + standard defined by a recognized standards body, or, in the case of 108 + interfaces specified for a particular programming language, one that 109 + is widely used among developers working in that language. 110 + 111 + The "System Libraries" of an executable work include anything, other 112 + than the work as a whole, that (a) is included in the normal form of 113 + packaging a Major Component, but which is not part of that Major 114 + Component, and (b) serves only to enable use of the work with that 115 + Major Component, or to implement a Standard Interface for which an 116 + implementation is available to the public in source code form. A 117 + "Major Component", in this context, means a major essential component 118 + (kernel, window system, and so on) of the specific operating system 119 + (if any) on which the executable work runs, or a compiler used to 120 + produce the work, or an object code interpreter used to run it. 121 + 122 + The "Corresponding Source" for a work in object code form means all 123 + the source code needed to generate, install, and (for an executable 124 + work) run the object code and to modify the work, including scripts to 125 + control those activities. However, it does not include the work's 126 + System Libraries, or general-purpose tools or generally available free 127 + programs which are used unmodified in performing those activities but 128 + which are not part of the work. For example, Corresponding Source 129 + includes interface definition files associated with source files for 130 + the work, and the source code for shared libraries and dynamically 131 + linked subprograms that the work is specifically designed to require, 132 + such as by intimate data communication or control flow between those 133 + subprograms and other parts of the work. 134 + 135 + The Corresponding Source need not include anything that users 136 + can regenerate automatically from other parts of the Corresponding 137 + Source. 138 + 139 + The Corresponding Source for a work in source code form is that 140 + same work. 141 + 142 + 2. Basic Permissions. 143 + 144 + All rights granted under this License are granted for the term of 145 + copyright on the Program, and are irrevocable provided the stated 146 + conditions are met. This License explicitly affirms your unlimited 147 + permission to run the unmodified Program. The output from running a 148 + covered work is covered by this License only if the output, given its 149 + content, constitutes a covered work. This License acknowledges your 150 + rights of fair use or other equivalent, as provided by copyright law. 151 + 152 + You may make, run and propagate covered works that you do not 153 + convey, without conditions so long as your license otherwise remains 154 + in force. You may convey covered works to others for the sole purpose 155 + of having them make modifications exclusively for you, or provide you 156 + with facilities for running those works, provided that you comply with 157 + the terms of this License in conveying all material for which you do 158 + not control copyright. Those thus making or running the covered works 159 + for you must do so exclusively on your behalf, under your direction 160 + and control, on terms that prohibit them from making any copies of 161 + your copyrighted material outside their relationship with you. 162 + 163 + Conveying under any other circumstances is permitted solely under 164 + the conditions stated below. Sublicensing is not allowed; section 10 165 + makes it unnecessary. 166 + 167 + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 + 169 + No covered work shall be deemed part of an effective technological 170 + measure under any applicable law fulfilling obligations under article 171 + 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 + similar laws prohibiting or restricting circumvention of such 173 + measures. 174 + 175 + When you convey a covered work, you waive any legal power to forbid 176 + circumvention of technological measures to the extent such circumvention 177 + is effected by exercising rights under this License with respect to 178 + the covered work, and you disclaim any intention to limit operation or 179 + modification of the work as a means of enforcing, against the work's 180 + users, your or third parties' legal rights to forbid circumvention of 181 + technological measures. 182 + 183 + 4. Conveying Verbatim Copies. 184 + 185 + You may convey verbatim copies of the Program's source code as you 186 + receive it, in any medium, provided that you conspicuously and 187 + appropriately publish on each copy an appropriate copyright notice; 188 + keep intact all notices stating that this License and any 189 + non-permissive terms added in accord with section 7 apply to the code; 190 + keep intact all notices of the absence of any warranty; and give all 191 + recipients a copy of this License along with the Program. 192 + 193 + You may charge any price or no price for each copy that you convey, 194 + and you may offer support or warranty protection for a fee. 195 + 196 + 5. Conveying Modified Source Versions. 197 + 198 + You may convey a work based on the Program, or the modifications to 199 + produce it from the Program, in the form of source code under the 200 + terms of section 4, provided that you also meet all of these conditions: 201 + 202 + a) The work must carry prominent notices stating that you modified 203 + it, and giving a relevant date. 204 + 205 + b) The work must carry prominent notices stating that it is 206 + released under this License and any conditions added under section 207 + 7. This requirement modifies the requirement in section 4 to 208 + "keep intact all notices". 209 + 210 + c) You must license the entire work, as a whole, under this 211 + License to anyone who comes into possession of a copy. This 212 + License will therefore apply, along with any applicable section 7 213 + additional terms, to the whole of the work, and all its parts, 214 + regardless of how they are packaged. This License gives no 215 + permission to license the work in any other way, but it does not 216 + invalidate such permission if you have separately received it. 217 + 218 + d) If the work has interactive user interfaces, each must display 219 + Appropriate Legal Notices; however, if the Program has interactive 220 + interfaces that do not display Appropriate Legal Notices, your 221 + work need not make them do so. 222 + 223 + A compilation of a covered work with other separate and independent 224 + works, which are not by their nature extensions of the covered work, 225 + and which are not combined with it such as to form a larger program, 226 + in or on a volume of a storage or distribution medium, is called an 227 + "aggregate" if the compilation and its resulting copyright are not 228 + used to limit the access or legal rights of the compilation's users 229 + beyond what the individual works permit. Inclusion of a covered work 230 + in an aggregate does not cause this License to apply to the other 231 + parts of the aggregate. 232 + 233 + 6. Conveying Non-Source Forms. 234 + 235 + You may convey a covered work in object code form under the terms 236 + of sections 4 and 5, provided that you also convey the 237 + machine-readable Corresponding Source under the terms of this License, 238 + in one of these ways: 239 + 240 + a) Convey the object code in, or embodied in, a physical product 241 + (including a physical distribution medium), accompanied by the 242 + Corresponding Source fixed on a durable physical medium 243 + customarily used for software interchange. 244 + 245 + b) Convey the object code in, or embodied in, a physical product 246 + (including a physical distribution medium), accompanied by a 247 + written offer, valid for at least three years and valid for as 248 + long as you offer spare parts or customer support for that product 249 + model, to give anyone who possesses the object code either (1) a 250 + copy of the Corresponding Source for all the software in the 251 + product that is covered by this License, on a durable physical 252 + medium customarily used for software interchange, for a price no 253 + more than your reasonable cost of physically performing this 254 + conveying of source, or (2) access to copy the 255 + Corresponding Source from a network server at no charge. 256 + 257 + c) Convey individual copies of the object code with a copy of the 258 + written offer to provide the Corresponding Source. This 259 + alternative is allowed only occasionally and noncommercially, and 260 + only if you received the object code with such an offer, in accord 261 + with subsection 6b. 262 + 263 + d) Convey the object code by offering access from a designated 264 + place (gratis or for a charge), and offer equivalent access to the 265 + Corresponding Source in the same way through the same place at no 266 + further charge. You need not require recipients to copy the 267 + Corresponding Source along with the object code. If the place to 268 + copy the object code is a network server, the Corresponding Source 269 + may be on a different server (operated by you or a third party) 270 + that supports equivalent copying facilities, provided you maintain 271 + clear directions next to the object code saying where to find the 272 + Corresponding Source. Regardless of what server hosts the 273 + Corresponding Source, you remain obligated to ensure that it is 274 + available for as long as needed to satisfy these requirements. 275 + 276 + e) Convey the object code using peer-to-peer transmission, provided 277 + you inform other peers where the object code and Corresponding 278 + Source of the work are being offered to the general public at no 279 + charge under subsection 6d. 280 + 281 + A separable portion of the object code, whose source code is excluded 282 + from the Corresponding Source as a System Library, need not be 283 + included in conveying the object code work. 284 + 285 + A "User Product" is either (1) a "consumer product", which means any 286 + tangible personal property which is normally used for personal, family, 287 + or household purposes, or (2) anything designed or sold for incorporation 288 + into a dwelling. In determining whether a product is a consumer product, 289 + doubtful cases shall be resolved in favor of coverage. For a particular 290 + product received by a particular user, "normally used" refers to a 291 + typical or common use of that class of product, regardless of the status 292 + of the particular user or of the way in which the particular user 293 + actually uses, or expects or is expected to use, the product. A product 294 + is a consumer product regardless of whether the product has substantial 295 + commercial, industrial or non-consumer uses, unless such uses represent 296 + the only significant mode of use of the product. 297 + 298 + "Installation Information" for a User Product means any methods, 299 + procedures, authorization keys, or other information required to install 300 + and execute modified versions of a covered work in that User Product from 301 + a modified version of its Corresponding Source. The information must 302 + suffice to ensure that the continued functioning of the modified object 303 + code is in no case prevented or interfered with solely because 304 + modification has been made. 305 + 306 + If you convey an object code work under this section in, or with, or 307 + specifically for use in, a User Product, and the conveying occurs as 308 + part of a transaction in which the right of possession and use of the 309 + User Product is transferred to the recipient in perpetuity or for a 310 + fixed term (regardless of how the transaction is characterized), the 311 + Corresponding Source conveyed under this section must be accompanied 312 + by the Installation Information. But this requirement does not apply 313 + if neither you nor any third party retains the ability to install 314 + modified object code on the User Product (for example, the work has 315 + been installed in ROM). 316 + 317 + The requirement to provide Installation Information does not include a 318 + requirement to continue to provide support service, warranty, or updates 319 + for a work that has been modified or installed by the recipient, or for 320 + the User Product in which it has been modified or installed. Access to a 321 + network may be denied when the modification itself materially and 322 + adversely affects the operation of the network or violates the rules and 323 + protocols for communication across the network. 324 + 325 + Corresponding Source conveyed, and Installation Information provided, 326 + in accord with this section must be in a format that is publicly 327 + documented (and with an implementation available to the public in 328 + source code form), and must require no special password or key for 329 + unpacking, reading or copying. 330 + 331 + 7. Additional Terms. 332 + 333 + "Additional permissions" are terms that supplement the terms of this 334 + License by making exceptions from one or more of its conditions. 335 + Additional permissions that are applicable to the entire Program shall 336 + be treated as though they were included in this License, to the extent 337 + that they are valid under applicable law. If additional permissions 338 + apply only to part of the Program, that part may be used separately 339 + under those permissions, but the entire Program remains governed by 340 + this License without regard to the additional permissions. 341 + 342 + When you convey a copy of a covered work, you may at your option 343 + remove any additional permissions from that copy, or from any part of 344 + it. (Additional permissions may be written to require their own 345 + removal in certain cases when you modify the work.) You may place 346 + additional permissions on material, added by you to a covered work, 347 + for which you have or can give appropriate copyright permission. 348 + 349 + Notwithstanding any other provision of this License, for material you 350 + add to a covered work, you may (if authorized by the copyright holders of 351 + that material) supplement the terms of this License with terms: 352 + 353 + a) Disclaiming warranty or limiting liability differently from the 354 + terms of sections 15 and 16 of this License; or 355 + 356 + b) Requiring preservation of specified reasonable legal notices or 357 + author attributions in that material or in the Appropriate Legal 358 + Notices displayed by works containing it; or 359 + 360 + c) Prohibiting misrepresentation of the origin of that material, or 361 + requiring that modified versions of such material be marked in 362 + reasonable ways as different from the original version; or 363 + 364 + d) Limiting the use for publicity purposes of names of licensors or 365 + authors of the material; or 366 + 367 + e) Declining to grant rights under trademark law for use of some 368 + trade names, trademarks, or service marks; or 369 + 370 + f) Requiring indemnification of licensors and authors of that 371 + material by anyone who conveys the material (or modified versions of 372 + it) with contractual assumptions of liability to the recipient, for 373 + any liability that these contractual assumptions directly impose on 374 + those licensors and authors. 375 + 376 + All other non-permissive additional terms are considered "further 377 + restrictions" within the meaning of section 10. If the Program as you 378 + received it, or any part of it, contains a notice stating that it is 379 + governed by this License along with a term that is a further 380 + restriction, you may remove that term. If a license document contains 381 + a further restriction but permits relicensing or conveying under this 382 + License, you may add to a covered work material governed by the terms 383 + of that license document, provided that the further restriction does 384 + not survive such relicensing or conveying. 385 + 386 + If you add terms to a covered work in accord with this section, you 387 + must place, in the relevant source files, a statement of the 388 + additional terms that apply to those files, or a notice indicating 389 + where to find the applicable terms. 390 + 391 + Additional terms, permissive or non-permissive, may be stated in the 392 + form of a separately written license, or stated as exceptions; 393 + the above requirements apply either way. 394 + 395 + 8. Termination. 396 + 397 + You may not propagate or modify a covered work except as expressly 398 + provided under this License. Any attempt otherwise to propagate or 399 + modify it is void, and will automatically terminate your rights under 400 + this License (including any patent licenses granted under the third 401 + paragraph of section 11). 402 + 403 + However, if you cease all violation of this License, then your 404 + license from a particular copyright holder is reinstated (a) 405 + provisionally, unless and until the copyright holder explicitly and 406 + finally terminates your license, and (b) permanently, if the copyright 407 + holder fails to notify you of the violation by some reasonable means 408 + prior to 60 days after the cessation. 409 + 410 + Moreover, your license from a particular copyright holder is 411 + reinstated permanently if the copyright holder notifies you of the 412 + violation by some reasonable means, this is the first time you have 413 + received notice of violation of this License (for any work) from that 414 + copyright holder, and you cure the violation prior to 30 days after 415 + your receipt of the notice. 416 + 417 + Termination of your rights under this section does not terminate the 418 + licenses of parties who have received copies or rights from you under 419 + this License. If your rights have been terminated and not permanently 420 + reinstated, you do not qualify to receive new licenses for the same 421 + material under section 10. 422 + 423 + 9. Acceptance Not Required for Having Copies. 424 + 425 + You are not required to accept this License in order to receive or 426 + run a copy of the Program. Ancillary propagation of a covered work 427 + occurring solely as a consequence of using peer-to-peer transmission 428 + to receive a copy likewise does not require acceptance. However, 429 + nothing other than this License grants you permission to propagate or 430 + modify any covered work. These actions infringe copyright if you do 431 + not accept this License. Therefore, by modifying or propagating a 432 + covered work, you indicate your acceptance of this License to do so. 433 + 434 + 10. Automatic Licensing of Downstream Recipients. 435 + 436 + Each time you convey a covered work, the recipient automatically 437 + receives a license from the original licensors, to run, modify and 438 + propagate that work, subject to this License. You are not responsible 439 + for enforcing compliance by third parties with this License. 440 + 441 + An "entity transaction" is a transaction transferring control of an 442 + organization, or substantially all assets of one, or subdividing an 443 + organization, or merging organizations. If propagation of a covered 444 + work results from an entity transaction, each party to that 445 + transaction who receives a copy of the work also receives whatever 446 + licenses to the work the party's predecessor in interest had or could 447 + give under the previous paragraph, plus a right to possession of the 448 + Corresponding Source of the work from the predecessor in interest, if 449 + the predecessor has it or can get it with reasonable efforts. 450 + 451 + You may not impose any further restrictions on the exercise of the 452 + rights granted or affirmed under this License. For example, you may 453 + not impose a license fee, royalty, or other charge for exercise of 454 + rights granted under this License, and you may not initiate litigation 455 + (including a cross-claim or counterclaim in a lawsuit) alleging that 456 + any patent claim is infringed by making, using, selling, offering for 457 + sale, or importing the Program or any portion of it. 458 + 459 + 11. Patents. 460 + 461 + A "contributor" is a copyright holder who authorizes use under this 462 + License of the Program or a work on which the Program is based. The 463 + work thus licensed is called the contributor's "contributor version". 464 + 465 + A contributor's "essential patent claims" are all patent claims 466 + owned or controlled by the contributor, whether already acquired or 467 + hereafter acquired, that would be infringed by some manner, permitted 468 + by this License, of making, using, or selling its contributor version, 469 + but do not include claims that would be infringed only as a 470 + consequence of further modification of the contributor version. For 471 + purposes of this definition, "control" includes the right to grant 472 + patent sublicenses in a manner consistent with the requirements of 473 + this License. 474 + 475 + Each contributor grants you a non-exclusive, worldwide, royalty-free 476 + patent license under the contributor's essential patent claims, to 477 + make, use, sell, offer for sale, import and otherwise run, modify and 478 + propagate the contents of its contributor version. 479 + 480 + In the following three paragraphs, a "patent license" is any express 481 + agreement or commitment, however denominated, not to enforce a patent 482 + (such as an express permission to practice a patent or covenant not to 483 + sue for patent infringement). To "grant" such a patent license to a 484 + party means to make such an agreement or commitment not to enforce a 485 + patent against the party. 486 + 487 + If you convey a covered work, knowingly relying on a patent license, 488 + and the Corresponding Source of the work is not available for anyone 489 + to copy, free of charge and under the terms of this License, through a 490 + publicly available network server or other readily accessible means, 491 + then you must either (1) cause the Corresponding Source to be so 492 + available, or (2) arrange to deprive yourself of the benefit of the 493 + patent license for this particular work, or (3) arrange, in a manner 494 + consistent with the requirements of this License, to extend the patent 495 + license to downstream recipients. "Knowingly relying" means you have 496 + actual knowledge that, but for the patent license, your conveying the 497 + covered work in a country, or your recipient's use of the covered work 498 + in a country, would infringe one or more identifiable patents in that 499 + country that you have reason to believe are valid. 500 + 501 + If, pursuant to or in connection with a single transaction or 502 + arrangement, you convey, or propagate by procuring conveyance of, a 503 + covered work, and grant a patent license to some of the parties 504 + receiving the covered work authorizing them to use, propagate, modify 505 + or convey a specific copy of the covered work, then the patent license 506 + you grant is automatically extended to all recipients of the covered 507 + work and works based on it. 508 + 509 + A patent license is "discriminatory" if it does not include within 510 + the scope of its coverage, prohibits the exercise of, or is 511 + conditioned on the non-exercise of one or more of the rights that are 512 + specifically granted under this License. You may not convey a covered 513 + work if you are a party to an arrangement with a third party that is 514 + in the business of distributing software, under which you make payment 515 + to the third party based on the extent of your activity of conveying 516 + the work, and under which the third party grants, to any of the 517 + parties who would receive the covered work from you, a discriminatory 518 + patent license (a) in connection with copies of the covered work 519 + conveyed by you (or copies made from those copies), or (b) primarily 520 + for and in connection with specific products or compilations that 521 + contain the covered work, unless you entered into that arrangement, 522 + or that patent license was granted, prior to 28 March 2007. 523 + 524 + Nothing in this License shall be construed as excluding or limiting 525 + any implied license or other defenses to infringement that may 526 + otherwise be available to you under applicable patent law. 527 + 528 + 12. No Surrender of Others' Freedom. 529 + 530 + If conditions are imposed on you (whether by court order, agreement or 531 + otherwise) that contradict the conditions of this License, they do not 532 + excuse you from the conditions of this License. If you cannot convey a 533 + covered work so as to satisfy simultaneously your obligations under this 534 + License and any other pertinent obligations, then as a consequence you may 535 + not convey it at all. For example, if you agree to terms that obligate you 536 + to collect a royalty for further conveying from those to whom you convey 537 + the Program, the only way you could satisfy both those terms and this 538 + License would be to refrain entirely from conveying the Program. 539 + 540 + 13. Remote Network Interaction; Use with the GNU General Public License. 541 + 542 + Notwithstanding any other provision of this License, if you modify the 543 + Program, your modified version must prominently offer all users 544 + interacting with it remotely through a computer network (if your version 545 + supports such interaction) an opportunity to receive the Corresponding 546 + Source of your version by providing access to the Corresponding Source 547 + from a network server at no charge, through some standard or customary 548 + means of facilitating copying of software. This Corresponding Source 549 + shall include the Corresponding Source for any work covered by version 3 550 + of the GNU General Public License that is incorporated pursuant to the 551 + following paragraph. 552 + 553 + Notwithstanding any other provision of this License, you have 554 + permission to link or combine any covered work with a work licensed 555 + under version 3 of the GNU General Public License into a single 556 + combined work, and to convey the resulting work. The terms of this 557 + License will continue to apply to the part which is the covered work, 558 + but the work with which it is combined will remain governed by version 559 + 3 of the GNU General Public License. 560 + 561 + 14. Revised Versions of this License. 562 + 563 + The Free Software Foundation may publish revised and/or new versions of 564 + the GNU Affero General Public License from time to time. Such new versions 565 + will be similar in spirit to the present version, but may differ in detail to 566 + address new problems or concerns. 567 + 568 + Each version is given a distinguishing version number. If the 569 + Program specifies that a certain numbered version of the GNU Affero General 570 + Public License "or any later version" applies to it, you have the 571 + option of following the terms and conditions either of that numbered 572 + version or of any later version published by the Free Software 573 + Foundation. If the Program does not specify a version number of the 574 + GNU Affero General Public License, you may choose any version ever published 575 + by the Free Software Foundation. 576 + 577 + If the Program specifies that a proxy can decide which future 578 + versions of the GNU Affero General Public License can be used, that proxy's 579 + public statement of acceptance of a version permanently authorizes you 580 + to choose that version for the Program. 581 + 582 + Later license versions may give you additional or different 583 + permissions. However, no additional obligations are imposed on any 584 + author or copyright holder as a result of your choosing to follow a 585 + later version. 586 + 587 + 15. Disclaimer of Warranty. 588 + 589 + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 + 598 + 16. Limitation of Liability. 599 + 600 + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 + SUCH DAMAGES. 609 + 610 + 17. Interpretation of Sections 15 and 16. 611 + 612 + If the disclaimer of warranty and limitation of liability provided 613 + above cannot be given local legal effect according to their terms, 614 + reviewing courts shall apply local law that most closely approximates 615 + an absolute waiver of all civil liability in connection with the 616 + Program, unless a warranty or assumption of liability accompanies a 617 + copy of the Program in return for a fee. 618 + 619 + END OF TERMS AND CONDITIONS 620 + 621 + How to Apply These Terms to Your New Programs 622 + 623 + If you develop a new program, and you want it to be of the greatest 624 + possible use to the public, the best way to achieve this is to make it 625 + free software which everyone can redistribute and change under these terms. 626 + 627 + To do so, attach the following notices to the program. It is safest 628 + to attach them to the start of each source file to most effectively 629 + state the exclusion of warranty; and each file should have at least 630 + the "copyright" line and a pointer to where the full notice is found. 631 + 632 + <one line to give the program's name and a brief idea of what it does.> 633 + Copyright (C) <year> <name of author> 634 + 635 + This program is free software: you can redistribute it and/or modify 636 + it under the terms of the GNU Affero General Public License as published by 637 + the Free Software Foundation, either version 3 of the License, or 638 + (at your option) any later version. 639 + 640 + This program is distributed in the hope that it will be useful, 641 + but WITHOUT ANY WARRANTY; without even the implied warranty of 642 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 + GNU Affero General Public License for more details. 644 + 645 + You should have received a copy of the GNU Affero General Public License 646 + along with this program. If not, see <https://www.gnu.org/licenses/>. 647 + 648 + Also add information on how to contact you by electronic and paper mail. 649 + 650 + If your software can interact with users remotely through a computer 651 + network, you should also make sure that it provides a way for users to 652 + get its source. For example, if your program is a web application, its 653 + interface could display a "Source" link that leads users to an archive 654 + of the code. There are many ways you could offer source, and different 655 + solutions will be better for different programs; see section 13 for the 656 + specific requirements. 657 + 658 + You should also get your employer (if you work as a programmer) or school, 659 + if any, to sign a "copyright disclaimer" for the program, if necessary. 660 + For more information on this, and how to apply and follow the GNU AGPL, see 661 + <https://www.gnu.org/licenses/>.
+247
README.md
··· 1 + # AT Protocol Link Shortener 2 + 3 + A **server-side** link shortening service powered by your [Linkat](https://linkat.blue) board. No database required - all links are fetched directly from AT Protocol! 4 + 5 + ## ✨ Features 6 + 7 + - **Zero Configuration Database**: Uses your existing Linkat board as the data source 8 + - ⚡ **Hash-Based Shortcodes**: Automatic 6-character codes generated from URLs (e.g., `/a3k9zx`) 9 + - 🚀 **Server-Side Only**: Pure API-based, no client UI needed 10 + - 🎯 **Smart Redirects**: Instant HTTP 301 redirects to your target URLs 11 + - 🔍 **Automatic PDS Discovery**: Resolves your PDS endpoint via Slingshot 12 + - ⚡ **Built-in Cache**: 5-minute cache for optimal performance 13 + 14 + ## 🚀 Quick Start 15 + 16 + ### 1. Clone and Install 17 + 18 + ```bash 19 + git clone git@github.com:ewanc26/atproto-shortlink # or git@tangled.sh:ewancroft.uk/atproto-shortlink 20 + cd atproto-shortlink 21 + npm install 22 + ``` 23 + 24 + ### 2. Configure Your DID 25 + 26 + Create a `.env` file: 27 + 28 + ```bash 29 + cp .env.example .env 30 + ``` 31 + 32 + Edit `.env` and add your AT Protocol DID: 33 + 34 + ```ini 35 + # Find your DID at https://pdsls.dev/ by entering your handle 36 + ATPROTO_DID=did:plc:your-did-here 37 + ``` 38 + 39 + **How to find your DID:** 40 + 41 + 1. Visit [PDSls](https://pdsls.dev/) 42 + 2. Enter your AT Protocol handle (e.g., `yourname.bsky.social`) 43 + 3. Copy the `did:plc:...` identifier 44 + 45 + ### 3. Set Up Your Linkat Board 46 + 47 + If you don't have a Linkat board yet: 48 + 49 + 1. Visit [https://linkat.blue](https://linkat.blue) 50 + 2. Create a board with your links 51 + 3. Add your links with titles and emojis 52 + 53 + The shortener will automatically generate unique 6-character codes for each URL! 54 + 55 + ### 4. Test Your Configuration (Optional) 56 + 57 + Run the configuration test to verify everything is set up correctly: 58 + 59 + ```bash 60 + npm run test:config 61 + ``` 62 + 63 + This will: 64 + 65 + - ✅ Check if `.env` exists and is configured 66 + - ✅ Validate your DID format 67 + - ✅ Test PDS connectivity 68 + - ✅ Verify your Linkat board is accessible 69 + - ✅ Show a preview of your first few links 70 + 71 + ### 5. Run the Server 72 + 73 + ```bash 74 + npm run dev 75 + ``` 76 + 77 + Visit `http://localhost:5173` to see your service running! 78 + 79 + ## 📖 Usage 80 + 81 + Once running, your short links work like this: 82 + 83 + ```bash 84 + # Redirect to your configured URLs 85 + http://localhost:5173/a3k9zx → Redirects to your GitHub 86 + http://localhost:5173/b7m2wp → Redirects to your blog 87 + http://localhost:5173/c4n8qz → Redirects to your portfolio 88 + 89 + # View service info 90 + http://localhost:5173/ → Shows API information and available links 91 + 92 + # Get JSON list of links 93 + http://localhost:5173/api/links → Returns all short links as JSON 94 + ``` 95 + 96 + ## 🔧 API Endpoints 97 + 98 + | Endpoint | Method | Description | Response | 99 + | ------------- | ------ | ------------------------------- | ------------ | 100 + | `/` | GET | Service status and link listing | HTML | 101 + | `/:shortcode` | GET | Redirect to full URL | 301 Redirect | 102 + | `/api/links` | GET | List all available short links | JSON | 103 + 104 + ### Example API Response 105 + 106 + ```json 107 + { 108 + "success": true, 109 + "count": 3, 110 + "links": [ 111 + { 112 + "shortcode": "a3k9zx", 113 + "url": "https://github.com/yourname", 114 + "title": "My GitHub Profile", 115 + "emoji": "💻", 116 + "shortUrl": "/a3k9zx" 117 + }, 118 + { 119 + "shortcode": "b7m2wp", 120 + "url": "https://yourblog.com", 121 + "title": "Personal Blog", 122 + "emoji": "📝", 123 + "shortUrl": "/b7m2wp" 124 + } 125 + ] 126 + } 127 + ``` 128 + 129 + ## 📝 How Shortcodes Work 130 + 131 + Shortcodes are automatically generated as 6-character base62 hashes from your URLs. Each URL will always produce the same shortcode, ensuring consistency. 132 + 133 + - **Base62 encoding**: Uses 0-9, a-z, A-Z (62 characters) 134 + - **Collision-resistant**: 62^6 = ~56 billion possible combinations 135 + - **Deterministic**: Same URL = same shortcode every time 136 + - **URL-safe**: No special characters needed 137 + 138 + ## 🌐 Deployment 139 + 140 + ### Build for Production 141 + 142 + ```bash 143 + npm run build 144 + npm run preview # Test the production build locally 145 + ``` 146 + 147 + ### Deploy to Platforms 148 + 149 + This project uses `@sveltejs/adapter-auto` which works with: 150 + 151 + - **Vercel**: Push to GitHub and connect your repo 152 + - **Netlify**: Push to GitHub and connect your repo 153 + - **Cloudflare Pages**: Push to GitHub and connect your repo 154 + - **Node.js**: Use `adapter-node` for standalone Node servers 155 + 156 + For specific platforms, see [SvelteKit adapters](https://kit.svelte.dev/docs/adapters). 157 + 158 + ### Environment Variables for Deployment 159 + 160 + Make sure to set `ATPROTO_DID` in your deployment platform's environment variables! 161 + 162 + ## ⚙️ Configuration 163 + 164 + | Variable | Required | Description | Example | 165 + | ------------- | -------- | -------------------- | ------------------- | 166 + | `ATPROTO_DID` | ✅ Yes | Your AT Protocol DID | `did:plc:abc123xyz` | 167 + 168 + ## 🏗️ How It Works 169 + 170 + 1. **You maintain your links** in [Linkat](https://linkat.blue) (stored in `blue.linkat.board` collection) 171 + 2. **Service fetches on-demand** from your AT Protocol PDS via Slingshot resolution 172 + 3. **URLs are shortened** using deterministic base62 hash encoding 173 + 4. **Accessing a short link** (e.g., `/a3k9zx`) triggers an instant 301 redirect 174 + 175 + ```mermaid 176 + graph LR 177 + A[User visits /a3k9zx] --> B[Service fetches Linkat data] 178 + B --> C[Looks up shortcode in links] 179 + C --> D[301 Redirect to target URL] 180 + ``` 181 + 182 + ## 🔒 Security 183 + 184 + - ✅ All Linkat data is public by design 185 + - ✅ No authentication required 186 + - ✅ Read-only access to AT Protocol data 187 + - ✅ No data storage (fetches on-demand with cache) 188 + - ✅ 5-minute cache to prevent abuse 189 + 190 + ## 🛠️ Development 191 + 192 + ```bash 193 + # Install dependencies 194 + npm install 195 + 196 + # Start dev server 197 + npm run dev 198 + 199 + # Type check 200 + npm run check 201 + 202 + # Format code 203 + npm run format 204 + 205 + # Check formatting 206 + npm run lint 207 + ``` 208 + 209 + ## 📦 Tech Stack 210 + 211 + - **Framework**: [SvelteKit 2](https://kit.svelte.dev/) 212 + - **Runtime**: Server-side only (no client JavaScript required) 213 + - **Data Source**: AT Protocol (`blue.linkat.board` collection) 214 + - **PDS Resolution**: [Slingshot](https://slingshot.microcosm.blue) by Microcosm 215 + - **Redirects**: HTTP 301 (permanent) 216 + - **Shortcode Format**: Base62 hash encoding 217 + 218 + ## 🔧 Troubleshooting 219 + 220 + Having issues? Check the [Troubleshooting Guide](./TROUBLESHOOTING.md) for common problems and solutions. 221 + 222 + Quick checks: 223 + 224 + 1. Run `npm run test:config` to verify your setup 225 + 2. Make sure Node.js 18+ is installed: `node --version` 226 + 3. Check your DID at [pdsls.dev](https://pdsls.dev/) 227 + 4. Verify your Linkat board at [linkat.blue](https://linkat.blue) 228 + 229 + ## 🤝 Contributing 230 + 231 + Contributions are welcome! Please feel free to submit a Pull Request. 232 + 233 + ## 📄 Licence 234 + 235 + AGPLv3 Licence - See [LICENCE](./LICENCE) file for details 236 + 237 + ## Links 238 + 239 + - [Linkat](https://linkat.blue) - The link board service 240 + - [AT Protocol](https://atproto.com) - The underlying protocol 241 + - [SvelteKit](https://kit.svelte.dev) - The web framework 242 + - [PDSls](https://pdsls.dev/) - Find your DID 243 + - [Slingshot](https://slingshot.microcosm.blue) - Identity resolver 244 + 245 + --- 246 + 247 + Made with ❤️ using AT Protocol and Linkat
+226
TROUBLESHOOTING.md
··· 1 + # Troubleshooting Guide 2 + 3 + ## Common Issues and Solutions 4 + 5 + ### 1. "ATPROTO_DID not configured" Error 6 + 7 + **Symptoms:** 8 + 9 + - Homepage shows a red error message 10 + - Service won't start or shows configuration error 11 + 12 + **Solution:** 13 + 14 + 1. Create a `.env` file in your project root (copy from `.env.example`) 15 + 2. Add your DID: `ATPROTO_DID=did:plc:your-did-here` 16 + 3. Find your DID at [pdsls.dev](https://pdsls.dev/) 17 + 4. Run `npm run test:config` to verify 18 + 5. Restart the server with `npm run dev` 19 + 20 + ### 2. "Failed to fetch Linkat data" Error 21 + 22 + **Symptoms:** 23 + 24 + - Service starts but shows 0 links 25 + - Error in console about failed fetch 26 + 27 + **Possible Causes & Solutions:** 28 + 29 + **A. No Linkat Board** 30 + 31 + - Visit [linkat.blue](https://linkat.blue) 32 + - Create a board 33 + - Add some links 34 + - Wait a few seconds for data to propagate 35 + 36 + **B. PDS Connection Issues** 37 + 38 + - Check your internet connection 39 + - Verify your DID is correct with `npm run test:config` 40 + - Try again in a few minutes (PDS might be temporarily down) 41 + 42 + **C. Invalid DID Format** 43 + 44 + - DID must start with `did:plc:` or `did:web:` 45 + - No spaces or special characters 46 + - Run `npm run test:config` to validate 47 + 48 + ### 3. Short Links Not Working (404) 49 + 50 + **Symptoms:** 51 + 52 + - Homepage shows links but accessing them gives 404 53 + - Shortcode appears but doesn't redirect 54 + 55 + **Possible Causes:** 56 + 57 + **A. Cache Issue** 58 + 59 + - Links are cached for 5 minutes 60 + - If you just added/changed links, wait 5 minutes 61 + - Or restart the server to clear cache 62 + 63 + **B. Shortcode Conflict** 64 + 65 + - Two links can't have the same first word 66 + - Example: "blog My Blog" and "blog Tech Blog" will conflict 67 + - Make first words unique: "blog" and "tech" 68 + 69 + **C. Special Characters** 70 + 71 + - Shortcodes are lowercase only 72 + - Access `/github` not `/GitHub` 73 + - Spaces and special chars are removed 74 + 75 + ### 4. Slow Redirects 76 + 77 + **Symptoms:** 78 + 79 + - First redirect after server start is slow 80 + - Subsequent redirects are fast 81 + 82 + **Explanation:** 83 + 84 + - First request fetches from AT Protocol (takes ~1-2 seconds) 85 + - Data is then cached for 5 minutes 86 + - This is normal behavior 87 + 88 + **To Improve:** 89 + 90 + - Consider pre-warming cache on server start 91 + - Use a CDN or edge function for faster global access 92 + 93 + ### 5. Port Already in Use 94 + 95 + **Symptoms:** 96 + 97 + ``` 98 + Error: listen EADDRINUSE: address already in use :::5173 99 + ``` 100 + 101 + **Solution:** 102 + 103 + 1. Find what's using port 5173: `lsof -i :5173` (Mac/Linux) or `netstat -ano | findstr :5173` (Windows) 104 + 2. Kill that process 105 + 3. Or change the port: `npm run dev -- --port 3000` 106 + 107 + ### 6. Module Not Found Errors 108 + 109 + **Symptoms:** 110 + 111 + ``` 112 + Cannot find module '@atproto/api' 113 + ``` 114 + 115 + **Solution:** 116 + 117 + ```bash 118 + # Delete node_modules and reinstall 119 + rm -rf node_modules package-lock.json 120 + npm install 121 + ``` 122 + 123 + ### 7. TypeScript Errors 124 + 125 + **Symptoms:** 126 + 127 + - Red squiggly lines in VS Code 128 + - Type errors when running `npm run check` 129 + 130 + **Solution:** 131 + 132 + ```bash 133 + # Sync SvelteKit types 134 + npx svelte-kit sync 135 + 136 + # Or run the full check 137 + npm run check 138 + ``` 139 + 140 + ## Debugging Tips 141 + 142 + ### Enable Verbose Logging 143 + 144 + The service logs important events. Check your terminal for: 145 + 146 + - `[Linkat]` - Data fetching operations 147 + - `[Redirect]` - Redirect attempts 148 + - `[API]` - API endpoint calls 149 + 150 + ### Test Your Configuration 151 + 152 + Always run this first when having issues: 153 + 154 + ```bash 155 + npm run test:config 156 + ``` 157 + 158 + This will tell you exactly what's wrong! 159 + 160 + ### Check the API 161 + 162 + Visit `http://localhost:5173/api/links` to see the JSON response: 163 + 164 + - Check if links are being fetched 165 + - Verify shortcodes are correct 166 + - See what data is available 167 + 168 + ### Verify Your Linkat Board 169 + 170 + 1. Visit [linkat.blue](https://linkat.blue) 171 + 2. Check your board has links 172 + 3. Verify link titles are formatted correctly 173 + 4. First word before space = shortcode 174 + 175 + ### Common Link Title Mistakes 176 + 177 + ❌ **Wrong:** 178 + 179 + - `MyGitHub` (no space, shortcode will be "mygithub") 180 + - `"GitHub"` (will be "github" but might want just "gh") 181 + - `` (empty title) 182 + 183 + ✅ **Right:** 184 + 185 + - `"gh GitHub Profile"` (shortcode: "gh") 186 + - `"blog My Blog"` (shortcode: "blog") 187 + - `"cv Resume"` (shortcode: "cv") 188 + 189 + ## Still Having Issues? 190 + 191 + 1. **Check the README**: Most setup instructions are there 192 + 2. **Run test:config**: `npm run test:config` 193 + 3. **Check Console Logs**: Look for error messages 194 + 4. **Verify Linkat Board**: Visit linkat.blue and check your board 195 + 5. **Try the API Directly**: `curl http://localhost:5173/api/links` 196 + 197 + ## Getting Help 198 + 199 + If you're still stuck: 200 + 201 + 1. Make sure you're using Node.js 18 or higher: `node --version` 202 + 2. Try the test config: `npm run test:config` 203 + 3. Check if your DID works at [pdsls.dev](https://pdsls.dev/) 204 + 4. Verify your Linkat board at [linkat.blue](https://linkat.blue) 205 + 206 + ## Quick Reference 207 + 208 + ```bash 209 + # Test configuration 210 + npm run test:config 211 + 212 + # Start dev server 213 + npm run dev 214 + 215 + # Build for production 216 + npm run build 217 + 218 + # Preview production build 219 + npm run preview 220 + 221 + # Check types 222 + npm run check 223 + 224 + # Format code 225 + npm run format 226 + ```
+1717
package-lock.json
··· 1 + { 2 + "name": "atproto-shortlink", 3 + "version": "0.0.1", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "atproto-shortlink", 9 + "version": "0.0.1", 10 + "dependencies": { 11 + "@atproto/api": "^0.18.1" 12 + }, 13 + "devDependencies": { 14 + "@sveltejs/adapter-auto": "^7.0.0", 15 + "@sveltejs/kit": "^2.49.0", 16 + "@sveltejs/vite-plugin-svelte": "^6.2.1", 17 + "prettier": "^3.6.2", 18 + "prettier-plugin-svelte": "^3.4.0", 19 + "svelte": "^5.43.14", 20 + "svelte-check": "^4.3.4", 21 + "typescript": "^5.9.3", 22 + "vite": "^7.2.4" 23 + } 24 + }, 25 + "node_modules/@atproto/api": { 26 + "version": "0.18.1", 27 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.1.tgz", 28 + "integrity": "sha512-eK8Us3kRfK+KjxEq/abF3XL4qtqxh7a5GbKHaUGQqPxNGmLiIdFn4Ve4PkpP/OsDfcRMZF5CK47Jr7SARc7ttg==", 29 + "license": "MIT", 30 + "dependencies": { 31 + "@atproto/common-web": "^0.4.3", 32 + "@atproto/lexicon": "^0.5.1", 33 + "@atproto/syntax": "^0.4.1", 34 + "@atproto/xrpc": "^0.7.5", 35 + "await-lock": "^2.2.2", 36 + "multiformats": "^9.9.0", 37 + "tlds": "^1.234.0", 38 + "zod": "^3.23.8" 39 + } 40 + }, 41 + "node_modules/@atproto/common-web": { 42 + "version": "0.4.3", 43 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", 44 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 45 + "license": "MIT", 46 + "dependencies": { 47 + "graphemer": "^1.4.0", 48 + "multiformats": "^9.9.0", 49 + "uint8arrays": "3.0.0", 50 + "zod": "^3.23.8" 51 + } 52 + }, 53 + "node_modules/@atproto/lexicon": { 54 + "version": "0.5.1", 55 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", 56 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 57 + "license": "MIT", 58 + "dependencies": { 59 + "@atproto/common-web": "^0.4.3", 60 + "@atproto/syntax": "^0.4.1", 61 + "iso-datestring-validator": "^2.2.2", 62 + "multiformats": "^9.9.0", 63 + "zod": "^3.23.8" 64 + } 65 + }, 66 + "node_modules/@atproto/syntax": { 67 + "version": "0.4.1", 68 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 69 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 70 + "license": "MIT" 71 + }, 72 + "node_modules/@atproto/xrpc": { 73 + "version": "0.7.5", 74 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", 75 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 76 + "license": "MIT", 77 + "dependencies": { 78 + "@atproto/lexicon": "^0.5.1", 79 + "zod": "^3.23.8" 80 + } 81 + }, 82 + "node_modules/@esbuild/aix-ppc64": { 83 + "version": "0.25.12", 84 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 85 + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 86 + "cpu": [ 87 + "ppc64" 88 + ], 89 + "dev": true, 90 + "license": "MIT", 91 + "optional": true, 92 + "os": [ 93 + "aix" 94 + ], 95 + "engines": { 96 + "node": ">=18" 97 + } 98 + }, 99 + "node_modules/@esbuild/android-arm": { 100 + "version": "0.25.12", 101 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 102 + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 103 + "cpu": [ 104 + "arm" 105 + ], 106 + "dev": true, 107 + "license": "MIT", 108 + "optional": true, 109 + "os": [ 110 + "android" 111 + ], 112 + "engines": { 113 + "node": ">=18" 114 + } 115 + }, 116 + "node_modules/@esbuild/android-arm64": { 117 + "version": "0.25.12", 118 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 119 + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 120 + "cpu": [ 121 + "arm64" 122 + ], 123 + "dev": true, 124 + "license": "MIT", 125 + "optional": true, 126 + "os": [ 127 + "android" 128 + ], 129 + "engines": { 130 + "node": ">=18" 131 + } 132 + }, 133 + "node_modules/@esbuild/android-x64": { 134 + "version": "0.25.12", 135 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 136 + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 137 + "cpu": [ 138 + "x64" 139 + ], 140 + "dev": true, 141 + "license": "MIT", 142 + "optional": true, 143 + "os": [ 144 + "android" 145 + ], 146 + "engines": { 147 + "node": ">=18" 148 + } 149 + }, 150 + "node_modules/@esbuild/darwin-arm64": { 151 + "version": "0.25.12", 152 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 153 + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 154 + "cpu": [ 155 + "arm64" 156 + ], 157 + "dev": true, 158 + "license": "MIT", 159 + "optional": true, 160 + "os": [ 161 + "darwin" 162 + ], 163 + "engines": { 164 + "node": ">=18" 165 + } 166 + }, 167 + "node_modules/@esbuild/darwin-x64": { 168 + "version": "0.25.12", 169 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 170 + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 171 + "cpu": [ 172 + "x64" 173 + ], 174 + "dev": true, 175 + "license": "MIT", 176 + "optional": true, 177 + "os": [ 178 + "darwin" 179 + ], 180 + "engines": { 181 + "node": ">=18" 182 + } 183 + }, 184 + "node_modules/@esbuild/freebsd-arm64": { 185 + "version": "0.25.12", 186 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 187 + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 188 + "cpu": [ 189 + "arm64" 190 + ], 191 + "dev": true, 192 + "license": "MIT", 193 + "optional": true, 194 + "os": [ 195 + "freebsd" 196 + ], 197 + "engines": { 198 + "node": ">=18" 199 + } 200 + }, 201 + "node_modules/@esbuild/freebsd-x64": { 202 + "version": "0.25.12", 203 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 204 + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 205 + "cpu": [ 206 + "x64" 207 + ], 208 + "dev": true, 209 + "license": "MIT", 210 + "optional": true, 211 + "os": [ 212 + "freebsd" 213 + ], 214 + "engines": { 215 + "node": ">=18" 216 + } 217 + }, 218 + "node_modules/@esbuild/linux-arm": { 219 + "version": "0.25.12", 220 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 221 + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 222 + "cpu": [ 223 + "arm" 224 + ], 225 + "dev": true, 226 + "license": "MIT", 227 + "optional": true, 228 + "os": [ 229 + "linux" 230 + ], 231 + "engines": { 232 + "node": ">=18" 233 + } 234 + }, 235 + "node_modules/@esbuild/linux-arm64": { 236 + "version": "0.25.12", 237 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 238 + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 239 + "cpu": [ 240 + "arm64" 241 + ], 242 + "dev": true, 243 + "license": "MIT", 244 + "optional": true, 245 + "os": [ 246 + "linux" 247 + ], 248 + "engines": { 249 + "node": ">=18" 250 + } 251 + }, 252 + "node_modules/@esbuild/linux-ia32": { 253 + "version": "0.25.12", 254 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 255 + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 256 + "cpu": [ 257 + "ia32" 258 + ], 259 + "dev": true, 260 + "license": "MIT", 261 + "optional": true, 262 + "os": [ 263 + "linux" 264 + ], 265 + "engines": { 266 + "node": ">=18" 267 + } 268 + }, 269 + "node_modules/@esbuild/linux-loong64": { 270 + "version": "0.25.12", 271 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 272 + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 273 + "cpu": [ 274 + "loong64" 275 + ], 276 + "dev": true, 277 + "license": "MIT", 278 + "optional": true, 279 + "os": [ 280 + "linux" 281 + ], 282 + "engines": { 283 + "node": ">=18" 284 + } 285 + }, 286 + "node_modules/@esbuild/linux-mips64el": { 287 + "version": "0.25.12", 288 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 289 + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 290 + "cpu": [ 291 + "mips64el" 292 + ], 293 + "dev": true, 294 + "license": "MIT", 295 + "optional": true, 296 + "os": [ 297 + "linux" 298 + ], 299 + "engines": { 300 + "node": ">=18" 301 + } 302 + }, 303 + "node_modules/@esbuild/linux-ppc64": { 304 + "version": "0.25.12", 305 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 306 + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 307 + "cpu": [ 308 + "ppc64" 309 + ], 310 + "dev": true, 311 + "license": "MIT", 312 + "optional": true, 313 + "os": [ 314 + "linux" 315 + ], 316 + "engines": { 317 + "node": ">=18" 318 + } 319 + }, 320 + "node_modules/@esbuild/linux-riscv64": { 321 + "version": "0.25.12", 322 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 323 + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 324 + "cpu": [ 325 + "riscv64" 326 + ], 327 + "dev": true, 328 + "license": "MIT", 329 + "optional": true, 330 + "os": [ 331 + "linux" 332 + ], 333 + "engines": { 334 + "node": ">=18" 335 + } 336 + }, 337 + "node_modules/@esbuild/linux-s390x": { 338 + "version": "0.25.12", 339 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 340 + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 341 + "cpu": [ 342 + "s390x" 343 + ], 344 + "dev": true, 345 + "license": "MIT", 346 + "optional": true, 347 + "os": [ 348 + "linux" 349 + ], 350 + "engines": { 351 + "node": ">=18" 352 + } 353 + }, 354 + "node_modules/@esbuild/linux-x64": { 355 + "version": "0.25.12", 356 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 357 + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 358 + "cpu": [ 359 + "x64" 360 + ], 361 + "dev": true, 362 + "license": "MIT", 363 + "optional": true, 364 + "os": [ 365 + "linux" 366 + ], 367 + "engines": { 368 + "node": ">=18" 369 + } 370 + }, 371 + "node_modules/@esbuild/netbsd-arm64": { 372 + "version": "0.25.12", 373 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 374 + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 375 + "cpu": [ 376 + "arm64" 377 + ], 378 + "dev": true, 379 + "license": "MIT", 380 + "optional": true, 381 + "os": [ 382 + "netbsd" 383 + ], 384 + "engines": { 385 + "node": ">=18" 386 + } 387 + }, 388 + "node_modules/@esbuild/netbsd-x64": { 389 + "version": "0.25.12", 390 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 391 + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 392 + "cpu": [ 393 + "x64" 394 + ], 395 + "dev": true, 396 + "license": "MIT", 397 + "optional": true, 398 + "os": [ 399 + "netbsd" 400 + ], 401 + "engines": { 402 + "node": ">=18" 403 + } 404 + }, 405 + "node_modules/@esbuild/openbsd-arm64": { 406 + "version": "0.25.12", 407 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 408 + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 409 + "cpu": [ 410 + "arm64" 411 + ], 412 + "dev": true, 413 + "license": "MIT", 414 + "optional": true, 415 + "os": [ 416 + "openbsd" 417 + ], 418 + "engines": { 419 + "node": ">=18" 420 + } 421 + }, 422 + "node_modules/@esbuild/openbsd-x64": { 423 + "version": "0.25.12", 424 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 425 + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 426 + "cpu": [ 427 + "x64" 428 + ], 429 + "dev": true, 430 + "license": "MIT", 431 + "optional": true, 432 + "os": [ 433 + "openbsd" 434 + ], 435 + "engines": { 436 + "node": ">=18" 437 + } 438 + }, 439 + "node_modules/@esbuild/openharmony-arm64": { 440 + "version": "0.25.12", 441 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 442 + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 443 + "cpu": [ 444 + "arm64" 445 + ], 446 + "dev": true, 447 + "license": "MIT", 448 + "optional": true, 449 + "os": [ 450 + "openharmony" 451 + ], 452 + "engines": { 453 + "node": ">=18" 454 + } 455 + }, 456 + "node_modules/@esbuild/sunos-x64": { 457 + "version": "0.25.12", 458 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 459 + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 460 + "cpu": [ 461 + "x64" 462 + ], 463 + "dev": true, 464 + "license": "MIT", 465 + "optional": true, 466 + "os": [ 467 + "sunos" 468 + ], 469 + "engines": { 470 + "node": ">=18" 471 + } 472 + }, 473 + "node_modules/@esbuild/win32-arm64": { 474 + "version": "0.25.12", 475 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 476 + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 477 + "cpu": [ 478 + "arm64" 479 + ], 480 + "dev": true, 481 + "license": "MIT", 482 + "optional": true, 483 + "os": [ 484 + "win32" 485 + ], 486 + "engines": { 487 + "node": ">=18" 488 + } 489 + }, 490 + "node_modules/@esbuild/win32-ia32": { 491 + "version": "0.25.12", 492 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 493 + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 494 + "cpu": [ 495 + "ia32" 496 + ], 497 + "dev": true, 498 + "license": "MIT", 499 + "optional": true, 500 + "os": [ 501 + "win32" 502 + ], 503 + "engines": { 504 + "node": ">=18" 505 + } 506 + }, 507 + "node_modules/@esbuild/win32-x64": { 508 + "version": "0.25.12", 509 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 510 + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 511 + "cpu": [ 512 + "x64" 513 + ], 514 + "dev": true, 515 + "license": "MIT", 516 + "optional": true, 517 + "os": [ 518 + "win32" 519 + ], 520 + "engines": { 521 + "node": ">=18" 522 + } 523 + }, 524 + "node_modules/@jridgewell/gen-mapping": { 525 + "version": "0.3.13", 526 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 527 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 528 + "dev": true, 529 + "license": "MIT", 530 + "dependencies": { 531 + "@jridgewell/sourcemap-codec": "^1.5.0", 532 + "@jridgewell/trace-mapping": "^0.3.24" 533 + } 534 + }, 535 + "node_modules/@jridgewell/remapping": { 536 + "version": "2.3.5", 537 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 538 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 539 + "dev": true, 540 + "license": "MIT", 541 + "dependencies": { 542 + "@jridgewell/gen-mapping": "^0.3.5", 543 + "@jridgewell/trace-mapping": "^0.3.24" 544 + } 545 + }, 546 + "node_modules/@jridgewell/resolve-uri": { 547 + "version": "3.1.2", 548 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 549 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 550 + "dev": true, 551 + "license": "MIT", 552 + "engines": { 553 + "node": ">=6.0.0" 554 + } 555 + }, 556 + "node_modules/@jridgewell/sourcemap-codec": { 557 + "version": "1.5.5", 558 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 559 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 560 + "dev": true, 561 + "license": "MIT" 562 + }, 563 + "node_modules/@jridgewell/trace-mapping": { 564 + "version": "0.3.31", 565 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 566 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 567 + "dev": true, 568 + "license": "MIT", 569 + "dependencies": { 570 + "@jridgewell/resolve-uri": "^3.1.0", 571 + "@jridgewell/sourcemap-codec": "^1.4.14" 572 + } 573 + }, 574 + "node_modules/@polka/url": { 575 + "version": "1.0.0-next.29", 576 + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", 577 + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", 578 + "dev": true, 579 + "license": "MIT" 580 + }, 581 + "node_modules/@rollup/rollup-android-arm-eabi": { 582 + "version": "4.53.3", 583 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", 584 + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", 585 + "cpu": [ 586 + "arm" 587 + ], 588 + "dev": true, 589 + "license": "MIT", 590 + "optional": true, 591 + "os": [ 592 + "android" 593 + ] 594 + }, 595 + "node_modules/@rollup/rollup-android-arm64": { 596 + "version": "4.53.3", 597 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", 598 + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", 599 + "cpu": [ 600 + "arm64" 601 + ], 602 + "dev": true, 603 + "license": "MIT", 604 + "optional": true, 605 + "os": [ 606 + "android" 607 + ] 608 + }, 609 + "node_modules/@rollup/rollup-darwin-arm64": { 610 + "version": "4.53.3", 611 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", 612 + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", 613 + "cpu": [ 614 + "arm64" 615 + ], 616 + "dev": true, 617 + "license": "MIT", 618 + "optional": true, 619 + "os": [ 620 + "darwin" 621 + ] 622 + }, 623 + "node_modules/@rollup/rollup-darwin-x64": { 624 + "version": "4.53.3", 625 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", 626 + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", 627 + "cpu": [ 628 + "x64" 629 + ], 630 + "dev": true, 631 + "license": "MIT", 632 + "optional": true, 633 + "os": [ 634 + "darwin" 635 + ] 636 + }, 637 + "node_modules/@rollup/rollup-freebsd-arm64": { 638 + "version": "4.53.3", 639 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", 640 + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", 641 + "cpu": [ 642 + "arm64" 643 + ], 644 + "dev": true, 645 + "license": "MIT", 646 + "optional": true, 647 + "os": [ 648 + "freebsd" 649 + ] 650 + }, 651 + "node_modules/@rollup/rollup-freebsd-x64": { 652 + "version": "4.53.3", 653 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", 654 + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", 655 + "cpu": [ 656 + "x64" 657 + ], 658 + "dev": true, 659 + "license": "MIT", 660 + "optional": true, 661 + "os": [ 662 + "freebsd" 663 + ] 664 + }, 665 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 666 + "version": "4.53.3", 667 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", 668 + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", 669 + "cpu": [ 670 + "arm" 671 + ], 672 + "dev": true, 673 + "license": "MIT", 674 + "optional": true, 675 + "os": [ 676 + "linux" 677 + ] 678 + }, 679 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 680 + "version": "4.53.3", 681 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", 682 + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", 683 + "cpu": [ 684 + "arm" 685 + ], 686 + "dev": true, 687 + "license": "MIT", 688 + "optional": true, 689 + "os": [ 690 + "linux" 691 + ] 692 + }, 693 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 694 + "version": "4.53.3", 695 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", 696 + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", 697 + "cpu": [ 698 + "arm64" 699 + ], 700 + "dev": true, 701 + "license": "MIT", 702 + "optional": true, 703 + "os": [ 704 + "linux" 705 + ] 706 + }, 707 + "node_modules/@rollup/rollup-linux-arm64-musl": { 708 + "version": "4.53.3", 709 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", 710 + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", 711 + "cpu": [ 712 + "arm64" 713 + ], 714 + "dev": true, 715 + "license": "MIT", 716 + "optional": true, 717 + "os": [ 718 + "linux" 719 + ] 720 + }, 721 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 722 + "version": "4.53.3", 723 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", 724 + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", 725 + "cpu": [ 726 + "loong64" 727 + ], 728 + "dev": true, 729 + "license": "MIT", 730 + "optional": true, 731 + "os": [ 732 + "linux" 733 + ] 734 + }, 735 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 736 + "version": "4.53.3", 737 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", 738 + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", 739 + "cpu": [ 740 + "ppc64" 741 + ], 742 + "dev": true, 743 + "license": "MIT", 744 + "optional": true, 745 + "os": [ 746 + "linux" 747 + ] 748 + }, 749 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 750 + "version": "4.53.3", 751 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", 752 + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", 753 + "cpu": [ 754 + "riscv64" 755 + ], 756 + "dev": true, 757 + "license": "MIT", 758 + "optional": true, 759 + "os": [ 760 + "linux" 761 + ] 762 + }, 763 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 764 + "version": "4.53.3", 765 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", 766 + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", 767 + "cpu": [ 768 + "riscv64" 769 + ], 770 + "dev": true, 771 + "license": "MIT", 772 + "optional": true, 773 + "os": [ 774 + "linux" 775 + ] 776 + }, 777 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 778 + "version": "4.53.3", 779 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", 780 + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", 781 + "cpu": [ 782 + "s390x" 783 + ], 784 + "dev": true, 785 + "license": "MIT", 786 + "optional": true, 787 + "os": [ 788 + "linux" 789 + ] 790 + }, 791 + "node_modules/@rollup/rollup-linux-x64-gnu": { 792 + "version": "4.53.3", 793 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", 794 + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", 795 + "cpu": [ 796 + "x64" 797 + ], 798 + "dev": true, 799 + "license": "MIT", 800 + "optional": true, 801 + "os": [ 802 + "linux" 803 + ] 804 + }, 805 + "node_modules/@rollup/rollup-linux-x64-musl": { 806 + "version": "4.53.3", 807 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", 808 + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", 809 + "cpu": [ 810 + "x64" 811 + ], 812 + "dev": true, 813 + "license": "MIT", 814 + "optional": true, 815 + "os": [ 816 + "linux" 817 + ] 818 + }, 819 + "node_modules/@rollup/rollup-openharmony-arm64": { 820 + "version": "4.53.3", 821 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", 822 + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", 823 + "cpu": [ 824 + "arm64" 825 + ], 826 + "dev": true, 827 + "license": "MIT", 828 + "optional": true, 829 + "os": [ 830 + "openharmony" 831 + ] 832 + }, 833 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 834 + "version": "4.53.3", 835 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", 836 + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", 837 + "cpu": [ 838 + "arm64" 839 + ], 840 + "dev": true, 841 + "license": "MIT", 842 + "optional": true, 843 + "os": [ 844 + "win32" 845 + ] 846 + }, 847 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 848 + "version": "4.53.3", 849 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", 850 + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", 851 + "cpu": [ 852 + "ia32" 853 + ], 854 + "dev": true, 855 + "license": "MIT", 856 + "optional": true, 857 + "os": [ 858 + "win32" 859 + ] 860 + }, 861 + "node_modules/@rollup/rollup-win32-x64-gnu": { 862 + "version": "4.53.3", 863 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", 864 + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", 865 + "cpu": [ 866 + "x64" 867 + ], 868 + "dev": true, 869 + "license": "MIT", 870 + "optional": true, 871 + "os": [ 872 + "win32" 873 + ] 874 + }, 875 + "node_modules/@rollup/rollup-win32-x64-msvc": { 876 + "version": "4.53.3", 877 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", 878 + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", 879 + "cpu": [ 880 + "x64" 881 + ], 882 + "dev": true, 883 + "license": "MIT", 884 + "optional": true, 885 + "os": [ 886 + "win32" 887 + ] 888 + }, 889 + "node_modules/@standard-schema/spec": { 890 + "version": "1.0.0", 891 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", 892 + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", 893 + "dev": true, 894 + "license": "MIT" 895 + }, 896 + "node_modules/@sveltejs/acorn-typescript": { 897 + "version": "1.0.7", 898 + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz", 899 + "integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==", 900 + "dev": true, 901 + "license": "MIT", 902 + "peerDependencies": { 903 + "acorn": "^8.9.0" 904 + } 905 + }, 906 + "node_modules/@sveltejs/adapter-auto": { 907 + "version": "7.0.0", 908 + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.0.tgz", 909 + "integrity": "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==", 910 + "dev": true, 911 + "license": "MIT", 912 + "peerDependencies": { 913 + "@sveltejs/kit": "^2.0.0" 914 + } 915 + }, 916 + "node_modules/@sveltejs/kit": { 917 + "version": "2.49.0", 918 + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz", 919 + "integrity": "sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==", 920 + "dev": true, 921 + "license": "MIT", 922 + "peer": true, 923 + "dependencies": { 924 + "@standard-schema/spec": "^1.0.0", 925 + "@sveltejs/acorn-typescript": "^1.0.5", 926 + "@types/cookie": "^0.6.0", 927 + "acorn": "^8.14.1", 928 + "cookie": "^0.6.0", 929 + "devalue": "^5.3.2", 930 + "esm-env": "^1.2.2", 931 + "kleur": "^4.1.5", 932 + "magic-string": "^0.30.5", 933 + "mrmime": "^2.0.0", 934 + "sade": "^1.8.1", 935 + "set-cookie-parser": "^2.6.0", 936 + "sirv": "^3.0.0" 937 + }, 938 + "bin": { 939 + "svelte-kit": "svelte-kit.js" 940 + }, 941 + "engines": { 942 + "node": ">=18.13" 943 + }, 944 + "peerDependencies": { 945 + "@opentelemetry/api": "^1.0.0", 946 + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", 947 + "svelte": "^4.0.0 || ^5.0.0-next.0", 948 + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" 949 + }, 950 + "peerDependenciesMeta": { 951 + "@opentelemetry/api": { 952 + "optional": true 953 + } 954 + } 955 + }, 956 + "node_modules/@sveltejs/vite-plugin-svelte": { 957 + "version": "6.2.1", 958 + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", 959 + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", 960 + "dev": true, 961 + "license": "MIT", 962 + "peer": true, 963 + "dependencies": { 964 + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", 965 + "debug": "^4.4.1", 966 + "deepmerge": "^4.3.1", 967 + "magic-string": "^0.30.17", 968 + "vitefu": "^1.1.1" 969 + }, 970 + "engines": { 971 + "node": "^20.19 || ^22.12 || >=24" 972 + }, 973 + "peerDependencies": { 974 + "svelte": "^5.0.0", 975 + "vite": "^6.3.0 || ^7.0.0" 976 + } 977 + }, 978 + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { 979 + "version": "5.0.1", 980 + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", 981 + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", 982 + "dev": true, 983 + "license": "MIT", 984 + "dependencies": { 985 + "debug": "^4.4.1" 986 + }, 987 + "engines": { 988 + "node": "^20.19 || ^22.12 || >=24" 989 + }, 990 + "peerDependencies": { 991 + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", 992 + "svelte": "^5.0.0", 993 + "vite": "^6.3.0 || ^7.0.0" 994 + } 995 + }, 996 + "node_modules/@types/cookie": { 997 + "version": "0.6.0", 998 + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", 999 + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", 1000 + "dev": true, 1001 + "license": "MIT" 1002 + }, 1003 + "node_modules/@types/estree": { 1004 + "version": "1.0.8", 1005 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1006 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1007 + "dev": true, 1008 + "license": "MIT" 1009 + }, 1010 + "node_modules/acorn": { 1011 + "version": "8.15.0", 1012 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 1013 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 1014 + "dev": true, 1015 + "license": "MIT", 1016 + "peer": true, 1017 + "bin": { 1018 + "acorn": "bin/acorn" 1019 + }, 1020 + "engines": { 1021 + "node": ">=0.4.0" 1022 + } 1023 + }, 1024 + "node_modules/aria-query": { 1025 + "version": "5.3.2", 1026 + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", 1027 + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", 1028 + "dev": true, 1029 + "license": "Apache-2.0", 1030 + "engines": { 1031 + "node": ">= 0.4" 1032 + } 1033 + }, 1034 + "node_modules/await-lock": { 1035 + "version": "2.2.2", 1036 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 1037 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 1038 + "license": "MIT" 1039 + }, 1040 + "node_modules/axobject-query": { 1041 + "version": "4.1.0", 1042 + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", 1043 + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", 1044 + "dev": true, 1045 + "license": "Apache-2.0", 1046 + "engines": { 1047 + "node": ">= 0.4" 1048 + } 1049 + }, 1050 + "node_modules/chokidar": { 1051 + "version": "4.0.3", 1052 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", 1053 + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 1054 + "dev": true, 1055 + "license": "MIT", 1056 + "dependencies": { 1057 + "readdirp": "^4.0.1" 1058 + }, 1059 + "engines": { 1060 + "node": ">= 14.16.0" 1061 + }, 1062 + "funding": { 1063 + "url": "https://paulmillr.com/funding/" 1064 + } 1065 + }, 1066 + "node_modules/clsx": { 1067 + "version": "2.1.1", 1068 + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", 1069 + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", 1070 + "dev": true, 1071 + "license": "MIT", 1072 + "engines": { 1073 + "node": ">=6" 1074 + } 1075 + }, 1076 + "node_modules/cookie": { 1077 + "version": "0.6.0", 1078 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 1079 + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 1080 + "dev": true, 1081 + "license": "MIT", 1082 + "engines": { 1083 + "node": ">= 0.6" 1084 + } 1085 + }, 1086 + "node_modules/debug": { 1087 + "version": "4.4.3", 1088 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1089 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1090 + "dev": true, 1091 + "license": "MIT", 1092 + "dependencies": { 1093 + "ms": "^2.1.3" 1094 + }, 1095 + "engines": { 1096 + "node": ">=6.0" 1097 + }, 1098 + "peerDependenciesMeta": { 1099 + "supports-color": { 1100 + "optional": true 1101 + } 1102 + } 1103 + }, 1104 + "node_modules/deepmerge": { 1105 + "version": "4.3.1", 1106 + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 1107 + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 1108 + "dev": true, 1109 + "license": "MIT", 1110 + "engines": { 1111 + "node": ">=0.10.0" 1112 + } 1113 + }, 1114 + "node_modules/devalue": { 1115 + "version": "5.5.0", 1116 + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz", 1117 + "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==", 1118 + "dev": true, 1119 + "license": "MIT" 1120 + }, 1121 + "node_modules/esbuild": { 1122 + "version": "0.25.12", 1123 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 1124 + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 1125 + "dev": true, 1126 + "hasInstallScript": true, 1127 + "license": "MIT", 1128 + "bin": { 1129 + "esbuild": "bin/esbuild" 1130 + }, 1131 + "engines": { 1132 + "node": ">=18" 1133 + }, 1134 + "optionalDependencies": { 1135 + "@esbuild/aix-ppc64": "0.25.12", 1136 + "@esbuild/android-arm": "0.25.12", 1137 + "@esbuild/android-arm64": "0.25.12", 1138 + "@esbuild/android-x64": "0.25.12", 1139 + "@esbuild/darwin-arm64": "0.25.12", 1140 + "@esbuild/darwin-x64": "0.25.12", 1141 + "@esbuild/freebsd-arm64": "0.25.12", 1142 + "@esbuild/freebsd-x64": "0.25.12", 1143 + "@esbuild/linux-arm": "0.25.12", 1144 + "@esbuild/linux-arm64": "0.25.12", 1145 + "@esbuild/linux-ia32": "0.25.12", 1146 + "@esbuild/linux-loong64": "0.25.12", 1147 + "@esbuild/linux-mips64el": "0.25.12", 1148 + "@esbuild/linux-ppc64": "0.25.12", 1149 + "@esbuild/linux-riscv64": "0.25.12", 1150 + "@esbuild/linux-s390x": "0.25.12", 1151 + "@esbuild/linux-x64": "0.25.12", 1152 + "@esbuild/netbsd-arm64": "0.25.12", 1153 + "@esbuild/netbsd-x64": "0.25.12", 1154 + "@esbuild/openbsd-arm64": "0.25.12", 1155 + "@esbuild/openbsd-x64": "0.25.12", 1156 + "@esbuild/openharmony-arm64": "0.25.12", 1157 + "@esbuild/sunos-x64": "0.25.12", 1158 + "@esbuild/win32-arm64": "0.25.12", 1159 + "@esbuild/win32-ia32": "0.25.12", 1160 + "@esbuild/win32-x64": "0.25.12" 1161 + } 1162 + }, 1163 + "node_modules/esm-env": { 1164 + "version": "1.2.2", 1165 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 1166 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 1167 + "dev": true, 1168 + "license": "MIT" 1169 + }, 1170 + "node_modules/esrap": { 1171 + "version": "2.1.3", 1172 + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.3.tgz", 1173 + "integrity": "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==", 1174 + "dev": true, 1175 + "license": "MIT", 1176 + "dependencies": { 1177 + "@jridgewell/sourcemap-codec": "^1.4.15" 1178 + } 1179 + }, 1180 + "node_modules/fdir": { 1181 + "version": "6.5.0", 1182 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 1183 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 1184 + "dev": true, 1185 + "license": "MIT", 1186 + "engines": { 1187 + "node": ">=12.0.0" 1188 + }, 1189 + "peerDependencies": { 1190 + "picomatch": "^3 || ^4" 1191 + }, 1192 + "peerDependenciesMeta": { 1193 + "picomatch": { 1194 + "optional": true 1195 + } 1196 + } 1197 + }, 1198 + "node_modules/fsevents": { 1199 + "version": "2.3.3", 1200 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1201 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1202 + "dev": true, 1203 + "hasInstallScript": true, 1204 + "license": "MIT", 1205 + "optional": true, 1206 + "os": [ 1207 + "darwin" 1208 + ], 1209 + "engines": { 1210 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1211 + } 1212 + }, 1213 + "node_modules/graphemer": { 1214 + "version": "1.4.0", 1215 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 1216 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 1217 + "license": "MIT" 1218 + }, 1219 + "node_modules/is-reference": { 1220 + "version": "3.0.3", 1221 + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", 1222 + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", 1223 + "dev": true, 1224 + "license": "MIT", 1225 + "dependencies": { 1226 + "@types/estree": "^1.0.6" 1227 + } 1228 + }, 1229 + "node_modules/iso-datestring-validator": { 1230 + "version": "2.2.2", 1231 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 1232 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 1233 + "license": "MIT" 1234 + }, 1235 + "node_modules/kleur": { 1236 + "version": "4.1.5", 1237 + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", 1238 + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", 1239 + "dev": true, 1240 + "license": "MIT", 1241 + "engines": { 1242 + "node": ">=6" 1243 + } 1244 + }, 1245 + "node_modules/locate-character": { 1246 + "version": "3.0.0", 1247 + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", 1248 + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", 1249 + "dev": true, 1250 + "license": "MIT" 1251 + }, 1252 + "node_modules/magic-string": { 1253 + "version": "0.30.21", 1254 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 1255 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 1256 + "dev": true, 1257 + "license": "MIT", 1258 + "dependencies": { 1259 + "@jridgewell/sourcemap-codec": "^1.5.5" 1260 + } 1261 + }, 1262 + "node_modules/mri": { 1263 + "version": "1.2.0", 1264 + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 1265 + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", 1266 + "dev": true, 1267 + "license": "MIT", 1268 + "engines": { 1269 + "node": ">=4" 1270 + } 1271 + }, 1272 + "node_modules/mrmime": { 1273 + "version": "2.0.1", 1274 + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", 1275 + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", 1276 + "dev": true, 1277 + "license": "MIT", 1278 + "engines": { 1279 + "node": ">=10" 1280 + } 1281 + }, 1282 + "node_modules/ms": { 1283 + "version": "2.1.3", 1284 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1285 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1286 + "dev": true, 1287 + "license": "MIT" 1288 + }, 1289 + "node_modules/multiformats": { 1290 + "version": "9.9.0", 1291 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 1292 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 1293 + "license": "(Apache-2.0 AND MIT)" 1294 + }, 1295 + "node_modules/nanoid": { 1296 + "version": "3.3.11", 1297 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1298 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1299 + "dev": true, 1300 + "funding": [ 1301 + { 1302 + "type": "github", 1303 + "url": "https://github.com/sponsors/ai" 1304 + } 1305 + ], 1306 + "license": "MIT", 1307 + "bin": { 1308 + "nanoid": "bin/nanoid.cjs" 1309 + }, 1310 + "engines": { 1311 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1312 + } 1313 + }, 1314 + "node_modules/picocolors": { 1315 + "version": "1.1.1", 1316 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1317 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1318 + "dev": true, 1319 + "license": "ISC" 1320 + }, 1321 + "node_modules/picomatch": { 1322 + "version": "4.0.3", 1323 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 1324 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 1325 + "dev": true, 1326 + "license": "MIT", 1327 + "peer": true, 1328 + "engines": { 1329 + "node": ">=12" 1330 + }, 1331 + "funding": { 1332 + "url": "https://github.com/sponsors/jonschlinkert" 1333 + } 1334 + }, 1335 + "node_modules/postcss": { 1336 + "version": "8.5.6", 1337 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 1338 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 1339 + "dev": true, 1340 + "funding": [ 1341 + { 1342 + "type": "opencollective", 1343 + "url": "https://opencollective.com/postcss/" 1344 + }, 1345 + { 1346 + "type": "tidelift", 1347 + "url": "https://tidelift.com/funding/github/npm/postcss" 1348 + }, 1349 + { 1350 + "type": "github", 1351 + "url": "https://github.com/sponsors/ai" 1352 + } 1353 + ], 1354 + "license": "MIT", 1355 + "dependencies": { 1356 + "nanoid": "^3.3.11", 1357 + "picocolors": "^1.1.1", 1358 + "source-map-js": "^1.2.1" 1359 + }, 1360 + "engines": { 1361 + "node": "^10 || ^12 || >=14" 1362 + } 1363 + }, 1364 + "node_modules/prettier": { 1365 + "version": "3.6.2", 1366 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", 1367 + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 1368 + "dev": true, 1369 + "license": "MIT", 1370 + "peer": true, 1371 + "bin": { 1372 + "prettier": "bin/prettier.cjs" 1373 + }, 1374 + "engines": { 1375 + "node": ">=14" 1376 + }, 1377 + "funding": { 1378 + "url": "https://github.com/prettier/prettier?sponsor=1" 1379 + } 1380 + }, 1381 + "node_modules/prettier-plugin-svelte": { 1382 + "version": "3.4.0", 1383 + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", 1384 + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", 1385 + "dev": true, 1386 + "license": "MIT", 1387 + "peerDependencies": { 1388 + "prettier": "^3.0.0", 1389 + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" 1390 + } 1391 + }, 1392 + "node_modules/readdirp": { 1393 + "version": "4.1.2", 1394 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", 1395 + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", 1396 + "dev": true, 1397 + "license": "MIT", 1398 + "engines": { 1399 + "node": ">= 14.18.0" 1400 + }, 1401 + "funding": { 1402 + "type": "individual", 1403 + "url": "https://paulmillr.com/funding/" 1404 + } 1405 + }, 1406 + "node_modules/rollup": { 1407 + "version": "4.53.3", 1408 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", 1409 + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", 1410 + "dev": true, 1411 + "license": "MIT", 1412 + "dependencies": { 1413 + "@types/estree": "1.0.8" 1414 + }, 1415 + "bin": { 1416 + "rollup": "dist/bin/rollup" 1417 + }, 1418 + "engines": { 1419 + "node": ">=18.0.0", 1420 + "npm": ">=8.0.0" 1421 + }, 1422 + "optionalDependencies": { 1423 + "@rollup/rollup-android-arm-eabi": "4.53.3", 1424 + "@rollup/rollup-android-arm64": "4.53.3", 1425 + "@rollup/rollup-darwin-arm64": "4.53.3", 1426 + "@rollup/rollup-darwin-x64": "4.53.3", 1427 + "@rollup/rollup-freebsd-arm64": "4.53.3", 1428 + "@rollup/rollup-freebsd-x64": "4.53.3", 1429 + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", 1430 + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", 1431 + "@rollup/rollup-linux-arm64-gnu": "4.53.3", 1432 + "@rollup/rollup-linux-arm64-musl": "4.53.3", 1433 + "@rollup/rollup-linux-loong64-gnu": "4.53.3", 1434 + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", 1435 + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", 1436 + "@rollup/rollup-linux-riscv64-musl": "4.53.3", 1437 + "@rollup/rollup-linux-s390x-gnu": "4.53.3", 1438 + "@rollup/rollup-linux-x64-gnu": "4.53.3", 1439 + "@rollup/rollup-linux-x64-musl": "4.53.3", 1440 + "@rollup/rollup-openharmony-arm64": "4.53.3", 1441 + "@rollup/rollup-win32-arm64-msvc": "4.53.3", 1442 + "@rollup/rollup-win32-ia32-msvc": "4.53.3", 1443 + "@rollup/rollup-win32-x64-gnu": "4.53.3", 1444 + "@rollup/rollup-win32-x64-msvc": "4.53.3", 1445 + "fsevents": "~2.3.2" 1446 + } 1447 + }, 1448 + "node_modules/sade": { 1449 + "version": "1.8.1", 1450 + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", 1451 + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", 1452 + "dev": true, 1453 + "license": "MIT", 1454 + "dependencies": { 1455 + "mri": "^1.1.0" 1456 + }, 1457 + "engines": { 1458 + "node": ">=6" 1459 + } 1460 + }, 1461 + "node_modules/set-cookie-parser": { 1462 + "version": "2.7.2", 1463 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", 1464 + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", 1465 + "dev": true, 1466 + "license": "MIT" 1467 + }, 1468 + "node_modules/sirv": { 1469 + "version": "3.0.2", 1470 + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", 1471 + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", 1472 + "dev": true, 1473 + "license": "MIT", 1474 + "dependencies": { 1475 + "@polka/url": "^1.0.0-next.24", 1476 + "mrmime": "^2.0.0", 1477 + "totalist": "^3.0.0" 1478 + }, 1479 + "engines": { 1480 + "node": ">=18" 1481 + } 1482 + }, 1483 + "node_modules/source-map-js": { 1484 + "version": "1.2.1", 1485 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1486 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1487 + "dev": true, 1488 + "license": "BSD-3-Clause", 1489 + "engines": { 1490 + "node": ">=0.10.0" 1491 + } 1492 + }, 1493 + "node_modules/svelte": { 1494 + "version": "5.43.14", 1495 + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.14.tgz", 1496 + "integrity": "sha512-pHeUrp1A5S6RGaXhJB7PtYjL1VVjbVrJ2EfuAoPu9/1LeoMaJa/pcdCsCSb0gS4eUHAHnhCbUDxORZyvGK6kOQ==", 1497 + "dev": true, 1498 + "license": "MIT", 1499 + "peer": true, 1500 + "dependencies": { 1501 + "@jridgewell/remapping": "^2.3.4", 1502 + "@jridgewell/sourcemap-codec": "^1.5.0", 1503 + "@sveltejs/acorn-typescript": "^1.0.5", 1504 + "@types/estree": "^1.0.5", 1505 + "acorn": "^8.12.1", 1506 + "aria-query": "^5.3.1", 1507 + "axobject-query": "^4.1.0", 1508 + "clsx": "^2.1.1", 1509 + "esm-env": "^1.2.1", 1510 + "esrap": "^2.1.0", 1511 + "is-reference": "^3.0.3", 1512 + "locate-character": "^3.0.0", 1513 + "magic-string": "^0.30.11", 1514 + "zimmerframe": "^1.1.2" 1515 + }, 1516 + "engines": { 1517 + "node": ">=18" 1518 + } 1519 + }, 1520 + "node_modules/svelte-check": { 1521 + "version": "4.3.4", 1522 + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", 1523 + "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", 1524 + "dev": true, 1525 + "license": "MIT", 1526 + "dependencies": { 1527 + "@jridgewell/trace-mapping": "^0.3.25", 1528 + "chokidar": "^4.0.1", 1529 + "fdir": "^6.2.0", 1530 + "picocolors": "^1.0.0", 1531 + "sade": "^1.7.4" 1532 + }, 1533 + "bin": { 1534 + "svelte-check": "bin/svelte-check" 1535 + }, 1536 + "engines": { 1537 + "node": ">= 18.0.0" 1538 + }, 1539 + "peerDependencies": { 1540 + "svelte": "^4.0.0 || ^5.0.0-next.0", 1541 + "typescript": ">=5.0.0" 1542 + } 1543 + }, 1544 + "node_modules/tinyglobby": { 1545 + "version": "0.2.15", 1546 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 1547 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 1548 + "dev": true, 1549 + "license": "MIT", 1550 + "dependencies": { 1551 + "fdir": "^6.5.0", 1552 + "picomatch": "^4.0.3" 1553 + }, 1554 + "engines": { 1555 + "node": ">=12.0.0" 1556 + }, 1557 + "funding": { 1558 + "url": "https://github.com/sponsors/SuperchupuDev" 1559 + } 1560 + }, 1561 + "node_modules/tlds": { 1562 + "version": "1.261.0", 1563 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", 1564 + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", 1565 + "license": "MIT", 1566 + "bin": { 1567 + "tlds": "bin.js" 1568 + } 1569 + }, 1570 + "node_modules/totalist": { 1571 + "version": "3.0.1", 1572 + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", 1573 + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", 1574 + "dev": true, 1575 + "license": "MIT", 1576 + "engines": { 1577 + "node": ">=6" 1578 + } 1579 + }, 1580 + "node_modules/typescript": { 1581 + "version": "5.9.3", 1582 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 1583 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 1584 + "dev": true, 1585 + "license": "Apache-2.0", 1586 + "peer": true, 1587 + "bin": { 1588 + "tsc": "bin/tsc", 1589 + "tsserver": "bin/tsserver" 1590 + }, 1591 + "engines": { 1592 + "node": ">=14.17" 1593 + } 1594 + }, 1595 + "node_modules/uint8arrays": { 1596 + "version": "3.0.0", 1597 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 1598 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 1599 + "license": "MIT", 1600 + "dependencies": { 1601 + "multiformats": "^9.4.2" 1602 + } 1603 + }, 1604 + "node_modules/vite": { 1605 + "version": "7.2.4", 1606 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", 1607 + "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", 1608 + "dev": true, 1609 + "license": "MIT", 1610 + "peer": true, 1611 + "dependencies": { 1612 + "esbuild": "^0.25.0", 1613 + "fdir": "^6.5.0", 1614 + "picomatch": "^4.0.3", 1615 + "postcss": "^8.5.6", 1616 + "rollup": "^4.43.0", 1617 + "tinyglobby": "^0.2.15" 1618 + }, 1619 + "bin": { 1620 + "vite": "bin/vite.js" 1621 + }, 1622 + "engines": { 1623 + "node": "^20.19.0 || >=22.12.0" 1624 + }, 1625 + "funding": { 1626 + "url": "https://github.com/vitejs/vite?sponsor=1" 1627 + }, 1628 + "optionalDependencies": { 1629 + "fsevents": "~2.3.3" 1630 + }, 1631 + "peerDependencies": { 1632 + "@types/node": "^20.19.0 || >=22.12.0", 1633 + "jiti": ">=1.21.0", 1634 + "less": "^4.0.0", 1635 + "lightningcss": "^1.21.0", 1636 + "sass": "^1.70.0", 1637 + "sass-embedded": "^1.70.0", 1638 + "stylus": ">=0.54.8", 1639 + "sugarss": "^5.0.0", 1640 + "terser": "^5.16.0", 1641 + "tsx": "^4.8.1", 1642 + "yaml": "^2.4.2" 1643 + }, 1644 + "peerDependenciesMeta": { 1645 + "@types/node": { 1646 + "optional": true 1647 + }, 1648 + "jiti": { 1649 + "optional": true 1650 + }, 1651 + "less": { 1652 + "optional": true 1653 + }, 1654 + "lightningcss": { 1655 + "optional": true 1656 + }, 1657 + "sass": { 1658 + "optional": true 1659 + }, 1660 + "sass-embedded": { 1661 + "optional": true 1662 + }, 1663 + "stylus": { 1664 + "optional": true 1665 + }, 1666 + "sugarss": { 1667 + "optional": true 1668 + }, 1669 + "terser": { 1670 + "optional": true 1671 + }, 1672 + "tsx": { 1673 + "optional": true 1674 + }, 1675 + "yaml": { 1676 + "optional": true 1677 + } 1678 + } 1679 + }, 1680 + "node_modules/vitefu": { 1681 + "version": "1.1.1", 1682 + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", 1683 + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", 1684 + "dev": true, 1685 + "license": "MIT", 1686 + "workspaces": [ 1687 + "tests/deps/*", 1688 + "tests/projects/*", 1689 + "tests/projects/workspace/packages/*" 1690 + ], 1691 + "peerDependencies": { 1692 + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" 1693 + }, 1694 + "peerDependenciesMeta": { 1695 + "vite": { 1696 + "optional": true 1697 + } 1698 + } 1699 + }, 1700 + "node_modules/zimmerframe": { 1701 + "version": "1.1.4", 1702 + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", 1703 + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", 1704 + "dev": true, 1705 + "license": "MIT" 1706 + }, 1707 + "node_modules/zod": { 1708 + "version": "3.25.76", 1709 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 1710 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 1711 + "license": "MIT", 1712 + "funding": { 1713 + "url": "https://github.com/sponsors/colinhacks" 1714 + } 1715 + } 1716 + } 1717 + }
+31
package.json
··· 1 + { 2 + "name": "atproto-shortlink", 3 + "private": true, 4 + "version": "0.0.1", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite dev", 8 + "build": "vite build", 9 + "preview": "vite preview", 10 + "prepare": "svelte-kit sync || echo ''", 11 + "test:config": "node scripts/test-config.js", 12 + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 13 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 14 + "format": "prettier --write .", 15 + "lint": "prettier --check ." 16 + }, 17 + "dependencies": { 18 + "@atproto/api": "^0.18.1" 19 + }, 20 + "devDependencies": { 21 + "@sveltejs/adapter-auto": "^7.0.0", 22 + "@sveltejs/kit": "^2.49.0", 23 + "@sveltejs/vite-plugin-svelte": "^6.2.1", 24 + "prettier": "^3.6.2", 25 + "prettier-plugin-svelte": "^3.4.0", 26 + "svelte": "^5.43.14", 27 + "svelte-check": "^4.3.4", 28 + "typescript": "^5.9.3", 29 + "vite": "^7.2.4" 30 + } 31 + }
+190
scripts/test-config.js
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Test script to verify AT Protocol Link Shortener configuration 5 + * Run with: node scripts/test-config.js 6 + */ 7 + 8 + import { AtpAgent } from '@atproto/api'; 9 + import * as fs from 'fs'; 10 + import * as path from 'path'; 11 + import { fileURLToPath } from 'url'; 12 + 13 + const __filename = fileURLToPath(import.meta.url); 14 + const __dirname = path.dirname(__filename); 15 + 16 + // ANSI color codes 17 + const colors = { 18 + reset: '\x1b[0m', 19 + red: '\x1b[31m', 20 + green: '\x1b[32m', 21 + yellow: '\x1b[33m', 22 + blue: '\x1b[34m', 23 + cyan: '\x1b[36m' 24 + }; 25 + 26 + function log(message, color = 'reset') { 27 + console.log(`${colors[color]}${message}${colors.reset}`); 28 + } 29 + 30 + function success(message) { 31 + log(`✓ ${message}`, 'green'); 32 + } 33 + 34 + function error(message) { 35 + log(`✗ ${message}`, 'red'); 36 + } 37 + 38 + function info(message) { 39 + log(`ℹ ${message}`, 'blue'); 40 + } 41 + 42 + function warning(message) { 43 + log(`⚠ ${message}`, 'yellow'); 44 + } 45 + 46 + async function resolvePDS(did) { 47 + const split = did.split(':'); 48 + 49 + if (split[0] !== 'did') { 50 + throw new Error(`Invalid DID format: ${did}`); 51 + } 52 + 53 + if (split[1] === 'plc') { 54 + const response = await fetch(`https://plc.directory/${did}`); 55 + if (!response.ok) { 56 + throw new Error(`Failed to resolve DID: ${response.statusText}`); 57 + } 58 + 59 + const diddoc = await response.json(); 60 + const services = diddoc.service || []; 61 + 62 + for (const service of services) { 63 + if (service.id === '#atproto_pds') { 64 + return service.serviceEndpoint; 65 + } 66 + } 67 + 68 + throw new Error(`No PDS endpoint found for DID: ${did}`); 69 + } else if (split[1] === 'web') { 70 + return `https://${split[2]}`; 71 + } 72 + 73 + throw new Error(`Unsupported DID method: ${split[1]}`); 74 + } 75 + 76 + async function testConfiguration() { 77 + log('\n🔧 AT Protocol Link Shortener - Configuration Test\n', 'cyan'); 78 + 79 + // Check for .env file 80 + const envPath = path.join(__dirname, '..', '.env'); 81 + if (!fs.existsSync(envPath)) { 82 + error('.env file not found'); 83 + warning('Please create a .env file by copying .env.example:'); 84 + info(' cp .env.example .env'); 85 + info(' # Then edit .env and add your ATPROTO_DID'); 86 + process.exit(1); 87 + } 88 + success('.env file exists'); 89 + 90 + // Read .env file 91 + const envContent = fs.readFileSync(envPath, 'utf-8'); 92 + const didMatch = envContent.match(/ATPROTO_DID=(.+)/); 93 + 94 + if (!didMatch || !didMatch[1] || didMatch[1].trim() === '') { 95 + error('ATPROTO_DID not configured in .env'); 96 + warning('Please add your AT Protocol DID to .env:'); 97 + info(' ATPROTO_DID=did:plc:your-did-here'); 98 + info('\nFind your DID at: https://pdsls.dev/'); 99 + process.exit(1); 100 + } 101 + 102 + const did = didMatch[1].trim(); 103 + success(`ATPROTO_DID configured: ${did}`); 104 + 105 + // Validate DID format 106 + if (!did.startsWith('did:plc:') && !did.startsWith('did:web:')) { 107 + error('Invalid DID format'); 108 + warning('DID should start with "did:plc:" or "did:web:"'); 109 + process.exit(1); 110 + } 111 + success('DID format is valid'); 112 + 113 + // Test PDS resolution 114 + info('\nResolving PDS endpoint...'); 115 + try { 116 + const pdsUrl = await resolvePDS(did); 117 + success(`PDS endpoint: ${pdsUrl}`); 118 + 119 + // Test connection to PDS 120 + info('\nTesting connection to PDS...'); 121 + const agent = new AtpAgent({ service: pdsUrl }); 122 + 123 + // Try to fetch profile as a connectivity test 124 + try { 125 + const profile = await agent.getProfile({ actor: did }); 126 + success(`Connected to PDS successfully`); 127 + success(`Profile: @${profile.data.handle}`); 128 + } catch (err) { 129 + warning('Could not fetch profile from PDS, trying Bluesky API...'); 130 + 131 + // Fallback to Bluesky API 132 + const bskyAgent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 133 + try { 134 + const profile = await bskyAgent.getProfile({ actor: did }); 135 + success(`Connected via Bluesky API`); 136 + success(`Profile: @${profile.data.handle}`); 137 + } catch (fallbackErr) { 138 + error('Failed to connect to both PDS and Bluesky API'); 139 + throw fallbackErr; 140 + } 141 + } 142 + 143 + // Test Linkat data fetch 144 + info('\nChecking for Linkat board...'); 145 + try { 146 + const response = await agent.com.atproto.repo.getRecord({ 147 + repo: did, 148 + collection: 'blue.linkat.board', 149 + rkey: 'self' 150 + }); 151 + 152 + if (response.data.value && Array.isArray(response.data.value.cards)) { 153 + const cardCount = response.data.value.cards.length; 154 + success(`Found Linkat board with ${cardCount} links`); 155 + 156 + if (cardCount > 0) { 157 + info('\nFirst few links:'); 158 + response.data.value.cards.slice(0, 3).forEach((card) => { 159 + const shortcode = card.text.split(/\s+/)[0].toLowerCase(); 160 + log(` • ${card.emoji || '🔗'} /${shortcode} → ${card.url}`, 'cyan'); 161 + }); 162 + } else { 163 + warning('Linkat board is empty - add some links at https://linkat.blue'); 164 + } 165 + } else { 166 + warning('Linkat board exists but has invalid structure'); 167 + } 168 + } catch (err) { 169 + if (err.error === 'RecordNotFound') { 170 + warning('No Linkat board found'); 171 + info('Create one at: https://linkat.blue'); 172 + } else { 173 + throw err; 174 + } 175 + } 176 + 177 + log('\n✨ Configuration test completed successfully!\n', 'green'); 178 + info('You can now run the server with: npm run dev'); 179 + info('Visit: http://localhost:5173\n'); 180 + } catch (err) { 181 + error(`\nConfiguration test failed: ${err.message}`); 182 + process.exit(1); 183 + } 184 + } 185 + 186 + testConfiguration().catch((err) => { 187 + error(`\nUnexpected error: ${err.message}`); 188 + console.error(err); 189 + process.exit(1); 190 + });
+13
src/app.d.ts
··· 1 + // See https://svelte.dev/docs/kit/types#app.d.ts 2 + // for information about these interfaces 3 + declare global { 4 + namespace App { 5 + // interface Error {} 6 + // interface Locals {} 7 + // interface PageData {} 8 + // interface PageState {} 9 + // interface Platform {} 10 + } 11 + } 12 + 13 + export {};
+11
src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + %sveltekit.head% 7 + </head> 8 + <body data-sveltekit-preload-data="hover"> 9 + <div style="display: contents">%sveltekit.body%</div> 10 + </body> 11 + </html>
+59
src/lib/constants.ts
··· 1 + /** 2 + * Application-wide constants and configuration 3 + */ 4 + 5 + /** 6 + * Cache configuration 7 + */ 8 + export const CACHE = { 9 + /** Default TTL for cached data (5 minutes) */ 10 + DEFAULT_TTL: 300000, 11 + 12 + /** Cache key prefix for Linkat data */ 13 + LINKAT_PREFIX: 'linkat:' 14 + } as const; 15 + 16 + /** 17 + * Shortcode configuration 18 + */ 19 + export const SHORTCODE = { 20 + /** Default length for generated shortcodes */ 21 + DEFAULT_LENGTH: 6, 22 + 23 + /** Maximum collision resolution attempts */ 24 + MAX_COLLISION_ATTEMPTS: 20, 25 + 26 + /** Base70 character set (includes special characters) */ 27 + BASE62_CHARS: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+=_-?<>' 28 + } as const; 29 + 30 + /** 31 + * AT Protocol configuration 32 + */ 33 + export const ATPROTO = { 34 + /** Slingshot identity resolver endpoint */ 35 + SLINGSHOT_ENDPOINT: 'https://slingshot.microcosm.blue', 36 + 37 + /** Default public Bluesky API endpoint */ 38 + PUBLIC_API: 'https://public.api.bsky.app', 39 + 40 + /** Linkat collection identifier */ 41 + LINKAT_COLLECTION: 'blue.linkat.board', 42 + 43 + /** Linkat record key */ 44 + LINKAT_RKEY: 'self' 45 + } as const; 46 + 47 + /** 48 + * HTTP configuration 49 + */ 50 + export const HTTP = { 51 + /** Status code for permanent redirect */ 52 + REDIRECT_PERMANENT: 301, 53 + 54 + /** Status code for not found */ 55 + NOT_FOUND: 404, 56 + 57 + /** Status code for no content */ 58 + NO_CONTENT: 204 59 + } as const;
+22
src/lib/index.ts
··· 1 + /** 2 + * Main library exports 3 + * 4 + * This file provides a clean public API for the application's core functionality. 5 + */ 6 + 7 + // Services 8 + export { getShortLinks, findShortLink, clearCache as clearLinkatCache } from './services/linkat'; 9 + export { createAgent, getPublicAgent, getPDSAgent, resetAgents } from './services/atproto'; 10 + 11 + // Utilities 12 + export { encodeUrl, isValidShortcode, getMaxCombinations } from './utils/encoding'; 13 + 14 + // Types 15 + export type { LinkCard, LinkData, ShortLink } from './services/types'; 16 + export type { ResolvedIdentity } from './services/atproto'; 17 + 18 + // Cache 19 + export { Cache } from './services/cache'; 20 + 21 + // Constants 22 + export { CACHE, SHORTCODE, ATPROTO, HTTP } from './constants';
+52
src/lib/services/agent.ts
··· 1 + /** 2 + * AT Protocol Agent Service 3 + * 4 + * This file provides backwards compatibility. 5 + * The actual implementation has been modularized into: 6 + * - agent-factory.ts: Agent creation 7 + * - identity-resolver.ts: DID resolution via Slingshot 8 + * - agent-manager.ts: Agent caching and fallback logic 9 + */ 10 + 11 + import { ATPROTO_DID } from '$env/static/private'; 12 + import { 13 + createAgent, 14 + resolveIdentity, 15 + defaultAgent, 16 + getPublicAgent, 17 + getPDSAgent, 18 + withFallback, 19 + resetAgents, 20 + type ResolvedIdentity 21 + } from './atproto'; 22 + 23 + // Re-export everything for backwards compatibility 24 + export { createAgent, resolveIdentity, defaultAgent, withFallback, resetAgents }; 25 + export type { ResolvedIdentity }; 26 + 27 + /** 28 + * Creates an AT Protocol agent for the configured DID 29 + */ 30 + export async function createAgentForDID(): Promise<import('@atproto/api').AtpAgent> { 31 + return await getPublicAgent(ATPROTO_DID); 32 + } 33 + 34 + /** 35 + * Creates an AT Protocol agent with fallback to public Bluesky API 36 + */ 37 + export async function createAgentWithFallback(): Promise<{ 38 + agent: import('@atproto/api').AtpAgent; 39 + isPDS: boolean; 40 + }> { 41 + try { 42 + const agent = await getPublicAgent(ATPROTO_DID); 43 + return { agent, isPDS: true }; 44 + } catch (error) { 45 + console.warn('Failed to resolve PDS, falling back to Bluesky public API:', error); 46 + const agent = defaultAgent; 47 + return { agent, isPDS: false }; 48 + } 49 + } 50 + 51 + // Also export the manager functions for direct use 52 + export { getPublicAgent, getPDSAgent };
+38
src/lib/services/atproto/agent-factory.ts
··· 1 + import { AtpAgent } from '@atproto/api'; 2 + 3 + /** 4 + * Creates an AtpAgent with optional fetch function injection 5 + * 6 + * @param service - Service URL for the agent 7 + * @param fetchFn - Optional custom fetch function (useful for server-side contexts) 8 + * @returns Configured AtpAgent instance 9 + */ 10 + export function createAgent(service: string, fetchFn?: typeof fetch): AtpAgent { 11 + // If we have an injected fetch, wrap it to ensure we handle headers correctly 12 + const wrappedFetch = fetchFn 13 + ? async (url: URL | RequestInfo, init?: RequestInit) => { 14 + // Convert URL to string if needed 15 + const urlStr = url instanceof URL ? url.toString() : url; 16 + 17 + // Make the request with the injected fetch 18 + const response = await fetchFn(urlStr, init); 19 + 20 + // Create a new response with the same body but add content-type if missing 21 + const headers = new Headers(response.headers); 22 + if (!headers.has('content-type')) { 23 + headers.set('content-type', 'application/json'); 24 + } 25 + 26 + return new Response(response.body, { 27 + status: response.status, 28 + statusText: response.statusText, 29 + headers 30 + }); 31 + } 32 + : undefined; 33 + 34 + return new AtpAgent({ 35 + service, 36 + ...(wrappedFetch && { fetch: wrappedFetch }) 37 + }); 38 + }
+111
src/lib/services/atproto/agent-manager.ts
··· 1 + import { ATPROTO } from '$lib/constants'; 2 + import type { AtpAgent } from '@atproto/api'; 3 + import { createAgent } from './agent-factory'; 4 + import { resolveIdentity } from './identity-resolver'; 5 + 6 + /** 7 + * Default fallback agent for public Bluesky API calls 8 + */ 9 + export const defaultAgent = createAgent(ATPROTO.PUBLIC_API); 10 + 11 + /** 12 + * Cached agents 13 + */ 14 + let resolvedAgent: AtpAgent | null = null; 15 + let pdsAgent: AtpAgent | null = null; 16 + 17 + /** 18 + * Gets or creates an agent using Slingshot resolution with fallback 19 + * 20 + * @param did - The DID to resolve 21 + * @param fetchFn - Optional custom fetch function 22 + * @returns Configured AtpAgent 23 + */ 24 + export async function getPublicAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> { 25 + console.info(`[Agent] Getting public agent for DID: ${did}`); 26 + 27 + if (resolvedAgent) { 28 + console.debug('[Agent] Using cached agent'); 29 + return resolvedAgent; 30 + } 31 + 32 + try { 33 + // Use Slingshot for PDS resolution 34 + console.info('[Agent] Attempting Slingshot resolution'); 35 + const resolved = await resolveIdentity(did, fetchFn); 36 + console.info(`[Agent] Resolved PDS endpoint: ${resolved.pds}`); 37 + resolvedAgent = createAgent(resolved.pds, fetchFn); 38 + return resolvedAgent; 39 + } catch (err) { 40 + console.error('[Agent] Slingshot resolution failed, falling back to Bluesky:', err); 41 + resolvedAgent = defaultAgent; 42 + return resolvedAgent; 43 + } 44 + } 45 + 46 + /** 47 + * Gets or creates a PDS-specific agent 48 + * 49 + * @param did - The DID to resolve 50 + * @param fetchFn - Optional custom fetch function 51 + * @returns Configured AtpAgent for the PDS 52 + * @throws Error if resolution fails 53 + */ 54 + export async function getPDSAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> { 55 + if (pdsAgent) return pdsAgent; 56 + 57 + try { 58 + const resolved = await resolveIdentity(did, fetchFn); 59 + pdsAgent = createAgent(resolved.pds, fetchFn); 60 + return pdsAgent; 61 + } catch (err) { 62 + console.error('Failed to resolve PDS for DID:', err); 63 + throw err; 64 + } 65 + } 66 + 67 + /** 68 + * Executes a function with automatic fallback between agents 69 + * 70 + * @param did - The DID to resolve 71 + * @param operation - The operation to execute with the agent 72 + * @param usePDSFirst - If true, tries PDS first before public API 73 + * @param fetchFn - Optional custom fetch function 74 + * @returns Result of the operation 75 + * @throws Error if all attempts fail 76 + */ 77 + export async function withFallback<T>( 78 + did: string, 79 + operation: (agent: AtpAgent) => Promise<T>, 80 + usePDSFirst = false, 81 + fetchFn?: typeof fetch 82 + ): Promise<T> { 83 + const defaultAgentFn = () => 84 + fetchFn ? createAgent(ATPROTO.PUBLIC_API, fetchFn) : Promise.resolve(defaultAgent); 85 + 86 + const agents = usePDSFirst 87 + ? [() => getPDSAgent(did, fetchFn), defaultAgentFn] 88 + : [defaultAgentFn, () => getPDSAgent(did, fetchFn)]; 89 + 90 + let lastError: any; 91 + 92 + for (const getAgent of agents) { 93 + try { 94 + const agent = await getAgent(); 95 + return await operation(agent); 96 + } catch (error) { 97 + console.warn('Operation failed, trying next agent:', error); 98 + lastError = error; 99 + } 100 + } 101 + 102 + throw lastError; 103 + } 104 + 105 + /** 106 + * Resets cached agents (useful for testing or when identity changes) 107 + */ 108 + export function resetAgents(): void { 109 + resolvedAgent = null; 110 + pdsAgent = null; 111 + }
+55
src/lib/services/atproto/identity-resolver.ts
··· 1 + import { ATPROTO } from '$lib/constants'; 2 + 3 + /** 4 + * Resolved identity from Slingshot 5 + */ 6 + export interface ResolvedIdentity { 7 + did: string; 8 + pds: string; 9 + } 10 + 11 + /** 12 + * Resolves a DID to find its PDS endpoint using Slingshot 13 + * 14 + * @param did - The DID to resolve 15 + * @param fetchFn - Optional custom fetch function 16 + * @returns Resolved identity with DID and PDS endpoint 17 + * @throws Error if resolution fails 18 + */ 19 + export async function resolveIdentity( 20 + did: string, 21 + fetchFn?: typeof fetch 22 + ): Promise<ResolvedIdentity> { 23 + console.info(`[Identity] Resolving DID: ${did}`); 24 + 25 + // Prefer an injected fetch (from SvelteKit load), fall back to global fetch 26 + const _fetch = fetchFn ?? globalThis.fetch; 27 + 28 + const url = `${ATPROTO.SLINGSHOT_ENDPOINT}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`; 29 + 30 + const response = await _fetch(url); 31 + 32 + if (!response.ok) { 33 + console.error(`[Identity] Resolution failed: ${response.status} ${response.statusText}`); 34 + throw new Error( 35 + `Failed to resolve identifier via Slingshot: ${response.status} ${response.statusText}` 36 + ); 37 + } 38 + 39 + const rawText = await response.text(); 40 + console.debug(`[Identity] Raw response:`, rawText); 41 + 42 + let data: any; 43 + try { 44 + data = JSON.parse(rawText); 45 + } catch (err) { 46 + console.error('[Identity] Failed to parse identity resolver response as JSON', err); 47 + throw err; 48 + } 49 + 50 + if (!data.did || !data.pds) { 51 + throw new Error('Invalid response from identity resolver'); 52 + } 53 + 54 + return data; 55 + }
+16
src/lib/services/atproto/index.ts
··· 1 + /** 2 + * AT Protocol service modules 3 + * 4 + * This module provides a clean interface for working with AT Protocol agents, 5 + * identity resolution, and PDS discovery. 6 + */ 7 + 8 + export { createAgent } from './agent-factory'; 9 + export { resolveIdentity, type ResolvedIdentity } from './identity-resolver'; 10 + export { 11 + defaultAgent, 12 + getPublicAgent, 13 + getPDSAgent, 14 + withFallback, 15 + resetAgents 16 + } from './agent-manager';
+94
src/lib/services/cache/index.ts
··· 1 + /** 2 + * Generic in-memory cache with TTL (Time To Live) support 3 + */ 4 + export class Cache<T> { 5 + private data = new Map<string, { value: T; expires: number }>(); 6 + 7 + /** 8 + * Set a value in the cache with optional TTL 9 + * @param key - Cache key 10 + * @param value - Value to cache 11 + * @param ttlMs - Time to live in milliseconds (default: 5 minutes) 12 + */ 13 + set(key: string, value: T, ttlMs: number = 300000): void { 14 + this.data.set(key, { 15 + value, 16 + expires: Date.now() + ttlMs 17 + }); 18 + } 19 + 20 + /** 21 + * Get a value from the cache 22 + * @param key - Cache key 23 + * @returns The cached value or null if expired/not found 24 + */ 25 + get(key: string): T | null { 26 + const entry = this.data.get(key); 27 + if (!entry) return null; 28 + 29 + if (Date.now() > entry.expires) { 30 + this.data.delete(key); 31 + return null; 32 + } 33 + 34 + return entry.value; 35 + } 36 + 37 + /** 38 + * Check if a key exists in the cache and is not expired 39 + * @param key - Cache key 40 + * @returns True if the key exists and is valid 41 + */ 42 + has(key: string): boolean { 43 + const entry = this.data.get(key); 44 + if (!entry) return false; 45 + 46 + if (Date.now() > entry.expires) { 47 + this.data.delete(key); 48 + return false; 49 + } 50 + 51 + return true; 52 + } 53 + 54 + /** 55 + * Delete a specific key from the cache 56 + * @param key - Cache key 57 + * @returns True if the key existed 58 + */ 59 + delete(key: string): boolean { 60 + return this.data.delete(key); 61 + } 62 + 63 + /** 64 + * Clear all cached data 65 + */ 66 + clear(): void { 67 + this.data.clear(); 68 + } 69 + 70 + /** 71 + * Get the number of items in the cache (including expired) 72 + */ 73 + size(): number { 74 + return this.data.size; 75 + } 76 + 77 + /** 78 + * Remove all expired entries from the cache 79 + * @returns Number of entries removed 80 + */ 81 + prune(): number { 82 + const now = Date.now(); 83 + let removed = 0; 84 + 85 + for (const [key, entry] of this.data.entries()) { 86 + if (now > entry.expires) { 87 + this.data.delete(key); 88 + removed++; 89 + } 90 + } 91 + 92 + return removed; 93 + } 94 + }
+11
src/lib/services/linkat.ts
··· 1 + /** 2 + * Linkat Service 3 + * 4 + * This file provides backwards compatibility. 5 + * The actual implementation has been modularized into: 6 + * - fetcher.ts: Raw Linkat board fetching 7 + * - generator.ts: Shortcode generation and link finding 8 + * - index.ts: Main service with caching 9 + */ 10 + 11 + export { fetchLinkatData, getShortLinks, findShortLink, clearCache } from './linkat/index';
+34
src/lib/services/linkat/fetcher.ts
··· 1 + import type { AtpAgent } from '@atproto/api'; 2 + import { ATPROTO } from '$lib/constants'; 3 + import type { LinkData } from '../types'; 4 + 5 + /** 6 + * Fetches Linkat board data from AT Protocol 7 + * 8 + * @param agent - AT Protocol agent to use for the request 9 + * @param did - DID of the user whose Linkat board to fetch 10 + * @returns Linkat board data or null if not found/invalid 11 + */ 12 + export async function fetchLinkatBoard(agent: AtpAgent, did: string): Promise<LinkData | null> { 13 + try { 14 + const response = await agent.com.atproto.repo.getRecord({ 15 + repo: did, 16 + collection: ATPROTO.LINKAT_COLLECTION, 17 + rkey: ATPROTO.LINKAT_RKEY 18 + }); 19 + 20 + const value = response.data.value; 21 + 22 + if (!value || !Array.isArray((value as any).cards)) { 23 + console.warn('[Linkat] Invalid data structure'); 24 + return null; 25 + } 26 + 27 + return { 28 + cards: (value as any).cards 29 + }; 30 + } catch (error) { 31 + console.error('[Linkat] Failed to fetch board data:', error); 32 + return null; 33 + } 34 + }
+52
src/lib/services/linkat/generator.ts
··· 1 + import { SHORTCODE } from '$lib/constants'; 2 + import type { LinkData, ShortLink } from '../types'; 3 + import { encodeUrl } from '$lib/utils/encoding'; 4 + 5 + /** 6 + * Converts Linkat card data to short links with generated shortcodes 7 + * 8 + * @param linkatData - Raw Linkat board data 9 + * @returns Array of short links with generated codes 10 + */ 11 + export function generateShortLinks(linkatData: LinkData): ShortLink[] { 12 + if (!linkatData || !linkatData.cards.length) { 13 + return []; 14 + } 15 + 16 + const shortLinks: ShortLink[] = []; 17 + const usedShortcodes = new Set<string>(); 18 + 19 + for (const card of linkatData.cards) { 20 + // Generate encoded shortcode from URL 21 + let shortcode = encodeUrl(card.url); 22 + 23 + // Ensure uniqueness (very unlikely to collide, but just in case) 24 + let attempt = 0; 25 + while (usedShortcodes.has(shortcode) && attempt < SHORTCODE.MAX_COLLISION_ATTEMPTS) { 26 + shortcode = encodeUrl(card.url + attempt); 27 + attempt++; 28 + } 29 + 30 + usedShortcodes.add(shortcode); 31 + 32 + shortLinks.push({ 33 + shortcode, 34 + url: card.url, 35 + title: card.text, 36 + emoji: card.emoji 37 + }); 38 + } 39 + 40 + return shortLinks; 41 + } 42 + 43 + /** 44 + * Finds a short link by its shortcode 45 + * 46 + * @param links - Array of short links to search 47 + * @param shortcode - The shortcode to find 48 + * @returns The matching short link or null if not found 49 + */ 50 + export function findLinkByShortcode(links: ShortLink[], shortcode: string): ShortLink | null { 51 + return links.find((link) => link.shortcode === shortcode) || null; 52 + }
+92
src/lib/services/linkat/index.ts
··· 1 + import { ATPROTO_DID } from '$env/static/private'; 2 + import { CACHE } from '$lib/constants'; 3 + import { Cache } from '../cache'; 4 + import { createAgentForDID, createAgentWithFallback } from '../agent'; 5 + import { fetchLinkatBoard } from './fetcher'; 6 + import { generateShortLinks, findLinkByShortcode } from './generator'; 7 + import type { LinkData, ShortLink } from '../types'; 8 + 9 + /** 10 + * Cache instance for Linkat data 11 + */ 12 + const cache = new Cache<LinkData>(); 13 + 14 + /** 15 + * Fetches Linkat board data with caching 16 + * 17 + * @returns Linkat board data or null if fetch fails 18 + */ 19 + export async function fetchLinkatData(): Promise<LinkData | null> { 20 + const cacheKey = `${CACHE.LINKAT_PREFIX}${ATPROTO_DID}`; 21 + const cached = cache.get(cacheKey); 22 + 23 + if (cached) { 24 + console.log('[Linkat] Returning cached data'); 25 + return cached; 26 + } 27 + 28 + console.log('[Linkat] Fetching from AT Protocol...'); 29 + 30 + try { 31 + // Try PDS first, fallback to public API 32 + let agent; 33 + let usedPDS = false; 34 + 35 + try { 36 + agent = await createAgentForDID(); 37 + usedPDS = true; 38 + console.log('[Linkat] Using PDS agent'); 39 + } catch (error) { 40 + console.warn('[Linkat] PDS unavailable, using fallback'); 41 + const result = await createAgentWithFallback(); 42 + agent = result.agent; 43 + usedPDS = result.isPDS; 44 + } 45 + 46 + const data = await fetchLinkatBoard(agent, ATPROTO_DID); 47 + 48 + if (!data) { 49 + return null; 50 + } 51 + 52 + console.log(`[Linkat] Successfully fetched ${data.cards.length} links`); 53 + cache.set(cacheKey, data, CACHE.DEFAULT_TTL); 54 + return data; 55 + } catch (error) { 56 + console.error('[Linkat] Failed to fetch data:', error); 57 + return null; 58 + } 59 + } 60 + 61 + /** 62 + * Gets all short links from the Linkat board 63 + * 64 + * @returns Array of short links with generated codes 65 + */ 66 + export async function getShortLinks(): Promise<ShortLink[]> { 67 + const linkatData = await fetchLinkatData(); 68 + 69 + if (!linkatData) { 70 + return []; 71 + } 72 + 73 + return generateShortLinks(linkatData); 74 + } 75 + 76 + /** 77 + * Finds a short link by its shortcode 78 + * 79 + * @param shortcode - The shortcode to find 80 + * @returns The matching short link or null if not found 81 + */ 82 + export async function findShortLink(shortcode: string): Promise<ShortLink | null> { 83 + const links = await getShortLinks(); 84 + return findLinkByShortcode(links, shortcode); 85 + } 86 + 87 + /** 88 + * Clears the Linkat cache 89 + */ 90 + export function clearCache(): void { 91 + cache.clear(); 92 + }
+16
src/lib/services/types.ts
··· 1 + export interface LinkCard { 2 + text: string; 3 + url: string; 4 + emoji?: string; 5 + } 6 + 7 + export interface LinkData { 8 + cards: LinkCard[]; 9 + } 10 + 11 + export interface ShortLink { 12 + shortcode: string; 13 + url: string; 14 + title: string; 15 + emoji?: string; 16 + }
+78
src/lib/utils/encoding.ts
··· 1 + /** 2 + * Utilities for encoding URLs into short codes 3 + */ 4 + 5 + import { SHORTCODE } from '$lib/constants'; 6 + 7 + /** 8 + * Base70 characters used for encoding (0-9, a-z, A-Z, and special chars: +=_-?<>) 9 + */ 10 + const BASE_CHARS = SHORTCODE.BASE62_CHARS; 11 + const BASE = BASE_CHARS.length; 12 + 13 + /** 14 + * Generates a simple hash from a string 15 + * @param text - Input string to hash 16 + * @returns Numeric hash value 17 + */ 18 + function hashString(text: string): number { 19 + let hash = 0; 20 + for (let i = 0; i < text.length; i++) { 21 + const char = text.charCodeAt(i); 22 + hash = (hash << 5) - hash + char; 23 + hash = hash & hash; // Convert to 32-bit integer 24 + } 25 + return Math.abs(hash); 26 + } 27 + 28 + /** 29 + * Encodes a number to base string 30 + * @param num - Number to encode 31 + * @param length - Target length of the encoded string 32 + * @returns Base70 encoded string 33 + */ 34 + function toBase(num: number, length: number): string { 35 + let encoded = ''; 36 + for (let i = 0; i < length; i++) { 37 + encoded = BASE_CHARS[num % BASE] + encoded; 38 + num = Math.floor(num / BASE); 39 + } 40 + return encoded; 41 + } 42 + 43 + /** 44 + * Encodes a URL to a short base70 string 45 + * Uses a deterministic hash-to-base70 encoding 46 + * 47 + * @param url - URL to encode 48 + * @param length - Target length of the shortcode (default: 6) 49 + * @returns Short base70 encoded string 50 + * 51 + * @example 52 + * encodeUrl('https://github.com/user') // Returns something like 'a3k9zx' 53 + */ 54 + export function encodeUrl(url: string, length: number = SHORTCODE.DEFAULT_LENGTH): string { 55 + const hash = hashString(url); 56 + return toBase(hash, length); 57 + } 58 + 59 + /** 60 + * Validates if a string is a valid base70 shortcode 61 + * @param code - String to validate 62 + * @returns True if the code contains only valid characters 63 + */ 64 + export function isValidShortcode(code: string): boolean { 65 + return /^[0-9a-zA-Z+=_\-?<>]+$/.test(code); 66 + } 67 + 68 + /** 69 + * Calculates the maximum number of possible shortcodes for a given length 70 + * @param length - Length of the shortcode 71 + * @returns Number of possible combinations 72 + * 73 + * @example 74 + * getMaxCombinations(6) // Returns 117649000000 (70^6) 75 + */ 76 + export function getMaxCombinations(length: number): number { 77 + return Math.pow(BASE, length); 78 + }
+87
src/routes/+layout.svelte
··· 1 + <script lang="ts"> 2 + let { children } = $props(); 3 + </script> 4 + 5 + <svelte:head> 6 + <script> 7 + // Prevent flash of unstyled content (FOUC) by applying theme before page renders 8 + (function () { 9 + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 10 + const htmlElement = document.documentElement; 11 + 12 + if (prefersDark) { 13 + htmlElement.classList.add('dark'); 14 + htmlElement.style.colorScheme = 'dark'; 15 + } else { 16 + htmlElement.classList.remove('dark'); 17 + htmlElement.style.colorScheme = 'light'; 18 + } 19 + })(); 20 + </script> 21 + </svelte:head> 22 + 23 + {@render children()} 24 + 25 + <style> 26 + :global(*) { 27 + margin: 0; 28 + padding: 0; 29 + box-sizing: border-box; 30 + } 31 + 32 + :global(html) { 33 + min-height: 100vh; 34 + background: #ffffff; 35 + color: #1a1a1a; 36 + transition: 37 + background-color 0.3s, 38 + color 0.3s; 39 + } 40 + 41 + :global(html.dark) { 42 + background: #1a1a1a; 43 + color: #e0e0e0; 44 + } 45 + 46 + :global(html.dark .info) { 47 + background: #1e3a1e; 48 + border-color: #4caf50; 49 + color: #e0e0e0; 50 + } 51 + 52 + :global(html.dark .error) { 53 + background: #3a1e1e; 54 + border-color: #f44336; 55 + color: #e0e0e0; 56 + } 57 + 58 + :global(html.dark code) { 59 + background: #2a2a2a; 60 + color: #e0e0e0; 61 + } 62 + 63 + :global(html.dark .links a), 64 + :global(html.dark .api li) { 65 + background: #2a2a2a; 66 + } 67 + 68 + :global(html.dark .links a:hover) { 69 + background: #333; 70 + } 71 + 72 + :global(html.dark h1), 73 + :global(html.dark h2) { 74 + color: #e0e0e0; 75 + } 76 + 77 + :global(html.dark .title), 78 + :global(html.dark .api span), 79 + :global(html.dark .empty), 80 + :global(html.dark footer) { 81 + color: #b0b0b0; 82 + } 83 + 84 + :global(html.dark footer) { 85 + border-top-color: #333; 86 + } 87 + </style>
+39
src/routes/+page.server.ts
··· 1 + import type { PageServerLoad } from './$types'; 2 + import { ATPROTO_DID } from '$env/static/private'; 3 + import { getShortLinks } from '$lib/services/linkat'; 4 + import type { ShortLink } from '$lib/services/types'; 5 + 6 + export const load: PageServerLoad = async () => { 7 + // Check if DID is configured 8 + if (!ATPROTO_DID || ATPROTO_DID === '') { 9 + console.error('[Homepage] ATPROTO_DID not configured'); 10 + return { 11 + did: 'NOT_CONFIGURED', 12 + linkCount: 0, 13 + links: [], 14 + error: 'ATPROTO_DID environment variable is not configured. Please add it to your .env file.' 15 + }; 16 + } 17 + 18 + try { 19 + const links = await getShortLinks(); 20 + 21 + return { 22 + did: ATPROTO_DID, 23 + linkCount: links.length, 24 + links: links.map((link: ShortLink) => ({ 25 + shortcode: link.shortcode, 26 + title: link.title, 27 + emoji: link.emoji 28 + })) 29 + }; 30 + } catch (error) { 31 + console.error('[Homepage] Error loading links:', error); 32 + return { 33 + did: ATPROTO_DID, 34 + linkCount: 0, 35 + links: [], 36 + error: 'Failed to load links from AT Protocol. Please check your DID and network connection.' 37 + }; 38 + } 39 + };
+274
src/routes/+page.svelte
··· 1 + <script lang="ts"> 2 + import type { PageData } from './$types'; 3 + 4 + export let data: PageData; 5 + </script> 6 + 7 + <svelte:head> 8 + <title>AT Protocol Link Shortener</title> 9 + <meta name="description" content="A server-side link shortening service powered by Linkat" /> 10 + </svelte:head> 11 + 12 + <main> 13 + <h1>AT Protocol Link Shortener</h1> 14 + 15 + <section> 16 + <h2>Service Status</h2> 17 + {#if data.error} 18 + <div class="error"> 19 + <p><strong>⚠️ Configuration Error</strong></p> 20 + <p>{data.error}</p> 21 + {#if data.did === 'NOT_CONFIGURED'} 22 + <div class="help"> 23 + <p><strong>Quick Fix:</strong></p> 24 + <ol> 25 + <li>Create a <code>.env</code> file in your project root</li> 26 + <li> 27 + Add: <code>ATPROTO_DID=did:plc:your-did-here</code> 28 + </li> 29 + <li> 30 + Find your DID at <a href="https://pdsls.dev/" target="_blank">pdsls.dev</a> 31 + </li> 32 + <li>Run <code>npm run test:config</code> to verify</li> 33 + <li>Restart the server</li> 34 + </ol> 35 + </div> 36 + {/if} 37 + </div> 38 + {:else} 39 + <div class="info"> 40 + <p>✓ Service is running</p> 41 + <p>✓ Configured DID: <code>{data.did}</code></p> 42 + <p>✓ Active links: {data.linkCount}</p> 43 + </div> 44 + {/if} 45 + </section> 46 + 47 + <section> 48 + <h2>Available Short Links</h2> 49 + {#if data.links && data.links.length > 0} 50 + <div class="info-box"> 51 + <p> 52 + <strong>Shortcodes:</strong> Each link has a unique 6-character code generated from its 53 + URL using base62 encoding (0-9, a-z, A-Z). 54 + </p> 55 + </div> 56 + <ul class="links"> 57 + {#each data.links as link} 58 + <li> 59 + <a href="/{link.shortcode}"> 60 + {#if link.emoji} 61 + <span class="emoji">{link.emoji}</span> 62 + {/if} 63 + <code>/{link.shortcode}</code> 64 + <span class="title">{link.title}</span> 65 + </a> 66 + </li> 67 + {/each} 68 + </ul> 69 + {:else if !data.error} 70 + <p class="empty"> 71 + No short links configured yet. Add links to your <a 72 + href="https://linkat.blue" 73 + target="_blank">Linkat board</a 74 + >! 75 + </p> 76 + {/if} 77 + </section> 78 + 79 + <section> 80 + <h2>API Endpoints</h2> 81 + <ul class="api"> 82 + <li> 83 + <a href="/api/links"> 84 + <code>GET /api/links</code> 85 + <span>List all short links (JSON)</span> 86 + </a> 87 + </li> 88 + <li> 89 + <code>GET /:shortcode</code> 90 + <span>Redirect to target URL (301)</span> 91 + </li> 92 + </ul> 93 + </section> 94 + 95 + <footer> 96 + <p> 97 + Powered by <a href="https://linkat.blue" target="_blank">Linkat</a>, 98 + <a href="https://atproto.com" target="_blank">AT Protocol</a>, and 99 + <a href="https://slingshot.microcosm.blue" target="_blank">Slingshot</a> 100 + </p> 101 + </footer> 102 + </main> 103 + 104 + <style> 105 + main { 106 + max-width: 800px; 107 + margin: 0 auto; 108 + padding: 2rem 1rem; 109 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 110 + line-height: 1.6; 111 + } 112 + 113 + h1 { 114 + font-size: 2rem; 115 + margin-bottom: 2rem; 116 + color: #1a1a1a; 117 + } 118 + 119 + h2 { 120 + font-size: 1.5rem; 121 + margin-top: 2rem; 122 + margin-bottom: 1rem; 123 + color: #333; 124 + } 125 + 126 + section { 127 + margin-bottom: 3rem; 128 + } 129 + 130 + .info, 131 + .error, 132 + .info-box { 133 + padding: 1rem; 134 + border-radius: 0.5rem; 135 + margin-bottom: 1rem; 136 + } 137 + 138 + .info { 139 + background: #e8f5e9; 140 + border: 1px solid #4caf50; 141 + } 142 + 143 + .error { 144 + background: #ffebee; 145 + border: 1px solid #f44336; 146 + } 147 + 148 + .info-box { 149 + background: #e3f2fd; 150 + border: 1px solid #2196f3; 151 + color: #0d47a1; 152 + } 153 + 154 + .help { 155 + margin-top: 1rem; 156 + padding-top: 1rem; 157 + border-top: 1px solid rgba(0, 0, 0, 0.1); 158 + } 159 + 160 + .help ol { 161 + margin-left: 1.5rem; 162 + margin-top: 0.5rem; 163 + line-height: 1.8; 164 + } 165 + 166 + .help a { 167 + color: #d32f2f; 168 + text-decoration: underline; 169 + } 170 + 171 + code { 172 + background: #f5f5f5; 173 + padding: 0.2rem 0.4rem; 174 + border-radius: 0.25rem; 175 + font-family: 'Courier New', monospace; 176 + font-size: 0.9rem; 177 + } 178 + 179 + .links { 180 + list-style: none; 181 + padding: 0; 182 + margin: 0; 183 + } 184 + 185 + .links li { 186 + margin-bottom: 0.5rem; 187 + } 188 + 189 + .links a { 190 + display: flex; 191 + align-items: center; 192 + gap: 0.75rem; 193 + padding: 0.75rem; 194 + background: #f8f9fa; 195 + border-radius: 0.5rem; 196 + text-decoration: none; 197 + color: inherit; 198 + transition: background 0.2s; 199 + } 200 + 201 + .links a:hover { 202 + background: #e9ecef; 203 + } 204 + 205 + .emoji { 206 + font-size: 1.5rem; 207 + flex-shrink: 0; 208 + } 209 + 210 + .title { 211 + color: #666; 212 + } 213 + 214 + .empty { 215 + color: #666; 216 + font-style: italic; 217 + } 218 + 219 + .empty a { 220 + color: #0066cc; 221 + text-decoration: none; 222 + } 223 + 224 + .empty a:hover { 225 + text-decoration: underline; 226 + } 227 + 228 + .api { 229 + list-style: none; 230 + padding: 0; 231 + margin: 0; 232 + } 233 + 234 + .api li { 235 + margin-bottom: 0.75rem; 236 + padding: 0.75rem; 237 + background: #f8f9fa; 238 + border-radius: 0.5rem; 239 + } 240 + 241 + .api a { 242 + display: flex; 243 + align-items: center; 244 + gap: 1rem; 245 + text-decoration: none; 246 + color: inherit; 247 + } 248 + 249 + .api a:hover { 250 + text-decoration: underline; 251 + } 252 + 253 + .api span { 254 + color: #666; 255 + } 256 + 257 + footer { 258 + margin-top: 4rem; 259 + padding-top: 2rem; 260 + border-top: 1px solid #e0e0e0; 261 + text-align: center; 262 + color: #666; 263 + font-size: 0.9rem; 264 + } 265 + 266 + footer a { 267 + color: #0066cc; 268 + text-decoration: none; 269 + } 270 + 271 + footer a:hover { 272 + text-decoration: underline; 273 + } 274 + </style>
+57
src/routes/[shortcode]/+server.ts
··· 1 + import type { RequestHandler } from './$types'; 2 + import { findShortLink } from '$lib/services/linkat'; 3 + import { HTTP } from '$lib/constants'; 4 + import { redirect } from '@sveltejs/kit'; 5 + 6 + export const GET: RequestHandler = async ({ params }) => { 7 + const { shortcode } = params; 8 + 9 + console.log(`[Redirect] Looking up shortcode: ${shortcode}`); 10 + 11 + const link = await findShortLink(shortcode); 12 + 13 + if (!link) { 14 + console.warn(`[Redirect] Shortcode not found: ${shortcode}`); 15 + return new Response( 16 + `<!DOCTYPE html> 17 + <html lang="en"> 18 + <head> 19 + <meta charset="UTF-8"> 20 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 21 + <title>Link Not Found - AT Protocol Link Shortener</title> 22 + <style> 23 + body { 24 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 25 + max-width: 600px; 26 + margin: 4rem auto; 27 + padding: 2rem; 28 + text-align: center; 29 + } 30 + h1 { font-size: 3rem; margin-bottom: 1rem; } 31 + p { color: #666; line-height: 1.6; margin-bottom: 1rem; } 32 + code { background: #f5f5f5; padding: 0.2rem 0.4rem; border-radius: 0.25rem; } 33 + a { color: #0066cc; text-decoration: none; } 34 + a:hover { text-decoration: underline; } 35 + </style> 36 + </head> 37 + <body> 38 + <h1>🔍 404</h1> 39 + <h2>Short Link Not Found</h2> 40 + <p>The short link <code>/${shortcode}</code> doesn't exist.</p> 41 + <p><a href="/">← View all available links</a></p> 42 + </body> 43 + </html>`, 44 + { 45 + status: HTTP.NOT_FOUND, 46 + headers: { 47 + 'Content-Type': 'text/html' 48 + } 49 + } 50 + ); 51 + } 52 + 53 + console.log(`[Redirect] Redirecting to: ${link.url}`); 54 + 55 + // Permanent redirect 56 + throw redirect(HTTP.REDIRECT_PERMANENT, link.url); 57 + };
+31
src/routes/api/links/+server.ts
··· 1 + import type { RequestHandler } from './$types'; 2 + import { getShortLinks } from '$lib/services/linkat'; 3 + import { json } from '@sveltejs/kit'; 4 + import type { ShortLink } from '$lib/services/types'; 5 + 6 + export const GET: RequestHandler = async () => { 7 + try { 8 + const links = await getShortLinks(); 9 + 10 + return json({ 11 + success: true, 12 + count: links.length, 13 + links: links.map((link: ShortLink) => ({ 14 + shortcode: link.shortcode, 15 + url: link.url, 16 + title: link.title, 17 + emoji: link.emoji, 18 + shortUrl: `/${link.shortcode}` 19 + })) 20 + }); 21 + } catch (error) { 22 + console.error('[API] Error fetching links:', error); 23 + return json( 24 + { 25 + success: false, 26 + error: 'Failed to fetch links' 27 + }, 28 + { status: 500 } 29 + ); 30 + } 31 + };
+9
src/routes/favicon/favicon.ico/+server.ts
··· 1 + import type { RequestHandler } from './$types'; 2 + import { HTTP } from '$lib/constants'; 3 + 4 + export const GET: RequestHandler = async () => { 5 + // Return 204 No Content to indicate no favicon is available 6 + return new Response(null, { 7 + status: HTTP.NO_CONTENT 8 + }); 9 + };
+3
static/robots.txt
··· 1 + # allow crawling everything by default 2 + User-agent: * 3 + Disallow:
+17
svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-auto'; 2 + import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 + 4 + /** @type {import('@sveltejs/kit').Config} */ 5 + const config = { 6 + // Consult https://svelte.dev/docs/kit/integrations 7 + // for more information about preprocessors 8 + preprocess: vitePreprocess(), 9 + kit: { 10 + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 11 + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 12 + // See https://svelte.dev/docs/kit/adapters for more information about adapters. 13 + adapter: adapter() 14 + } 15 + }; 16 + 17 + export default config;
+20
tsconfig.json
··· 1 + { 2 + "extends": "./.svelte-kit/tsconfig.json", 3 + "compilerOptions": { 4 + "rewriteRelativeImportExtensions": true, 5 + "allowJs": true, 6 + "checkJs": true, 7 + "esModuleInterop": true, 8 + "forceConsistentCasingInFileNames": true, 9 + "resolveJsonModule": true, 10 + "skipLibCheck": true, 11 + "sourceMap": true, 12 + "strict": true, 13 + "moduleResolution": "bundler" 14 + } 15 + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 + // 18 + // To make changes to top-level options such as include and exclude, we recommend extending 19 + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript 20 + }
+6
vite.config.ts
··· 1 + import { sveltekit } from '@sveltejs/kit/vite'; 2 + import { defineConfig } from 'vite'; 3 + 4 + export default defineConfig({ 5 + plugins: [sveltekit()] 6 + });