+14
.env.example
+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
+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
.npmrc
···
1
+
engine-strict=true
+9
.prettierignore
+9
.prettierignore
+15
.prettierrc
+15
.prettierrc
+8
.vscode/settings.json
+8
.vscode/settings.json
+236
ARCHITECTURE.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+16
src/lib/services/types.ts
+78
src/lib/utils/encoding.ts
+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
+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
+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
+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
+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
+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
+9
src/routes/favicon/favicon.ico/+server.ts
+17
svelte.config.js
+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
+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
+
}