your personal website on atproto - mirror blento.app
26
fork

Configure Feed

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

Merge pull request #278 from flo-bit/v2-refactor

V2 refactor

authored by

Florian and committed by
GitHub
cb65835f 7b77d172

+3542 -908
+1
eslint.config.js
··· 9 9 10 10 export default [ 11 11 includeIgnoreFile(gitignorePath), 12 + { ignores: ['src/lexicon-types/**', 'lexicons-generated/**'] }, 12 13 js.configs.recommended, 13 14 ...ts.configs.recommended, 14 15 ...svelte.configs['flat/recommended'],
+22
lex.config.js
··· 1 + import { defineLexiconConfig } from '@atcute/lex-cli'; 2 + 3 + export default defineLexiconConfig({ 4 + files: ['lexicons/**/*.json', 'lexicons-pulled/**/*.json', 'lexicons-generated/**/*.json'], 5 + outdir: 'src/lexicon-types/', 6 + imports: ['@atcute/atproto'], 7 + pull: { 8 + outdir: 'lexicons-pulled/', 9 + sources: [ 10 + { 11 + type: 'atproto', 12 + mode: 'nsids', 13 + nsids: [ 14 + 'app.blento.card', 15 + 'app.blento.page', 16 + 'app.bsky.actor.profile', 17 + 'site.standard.publication' 18 + ] 19 + } 20 + ] 21 + } 22 + });
+96
lexicons-generated/app/blento/card/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.card.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single app.blento.card record by AT URI", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["uri"], 11 + "properties": { 12 + "uri": { 13 + "type": "string", 14 + "format": "at-uri", 15 + "description": "AT URI of the record" 16 + }, 17 + "profiles": { 18 + "type": "boolean", 19 + "description": "Include profile + identity info keyed by DID" 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["uri", "did", "collection", "rkey", "time_us"], 28 + "properties": { 29 + "uri": { 30 + "type": "string", 31 + "format": "at-uri" 32 + }, 33 + "did": { 34 + "type": "string", 35 + "format": "did" 36 + }, 37 + "collection": { 38 + "type": "string", 39 + "format": "nsid" 40 + }, 41 + "rkey": { 42 + "type": "string" 43 + }, 44 + "cid": { 45 + "type": "string" 46 + }, 47 + "record": { 48 + "type": "ref", 49 + "ref": "app.blento.card#main" 50 + }, 51 + "time_us": { 52 + "type": "integer" 53 + }, 54 + "profiles": { 55 + "type": "array", 56 + "items": { 57 + "type": "ref", 58 + "ref": "#profileEntry" 59 + } 60 + } 61 + } 62 + } 63 + } 64 + }, 65 + "profileEntry": { 66 + "type": "object", 67 + "required": ["did"], 68 + "properties": { 69 + "did": { 70 + "type": "string", 71 + "format": "did" 72 + }, 73 + "handle": { 74 + "type": "string" 75 + }, 76 + "uri": { 77 + "type": "string", 78 + "format": "at-uri" 79 + }, 80 + "collection": { 81 + "type": "string", 82 + "format": "nsid" 83 + }, 84 + "rkey": { 85 + "type": "string" 86 + }, 87 + "cid": { 88 + "type": "string" 89 + }, 90 + "record": { 91 + "type": "unknown" 92 + } 93 + } 94 + } 95 + } 96 + }
+236
lexicons-generated/app/blento/card/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.card.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Query app.blento.card records with filters", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 200, 15 + "default": 50 16 + }, 17 + "cursor": { 18 + "type": "string" 19 + }, 20 + "actor": { 21 + "type": "string", 22 + "format": "at-identifier", 23 + "description": "Filter by DID or handle (triggers on-demand backfill)" 24 + }, 25 + "profiles": { 26 + "type": "boolean", 27 + "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "wMin": { 30 + "type": "string", 31 + "description": "Minimum value for w" 32 + }, 33 + "wMax": { 34 + "type": "string", 35 + "description": "Maximum value for w" 36 + }, 37 + "hMin": { 38 + "type": "string", 39 + "description": "Minimum value for h" 40 + }, 41 + "hMax": { 42 + "type": "string", 43 + "description": "Maximum value for h" 44 + }, 45 + "xMin": { 46 + "type": "string", 47 + "description": "Minimum value for x" 48 + }, 49 + "xMax": { 50 + "type": "string", 51 + "description": "Maximum value for x" 52 + }, 53 + "yMin": { 54 + "type": "string", 55 + "description": "Minimum value for y" 56 + }, 57 + "yMax": { 58 + "type": "string", 59 + "description": "Maximum value for y" 60 + }, 61 + "mobileWMin": { 62 + "type": "string", 63 + "description": "Minimum value for mobileW" 64 + }, 65 + "mobileWMax": { 66 + "type": "string", 67 + "description": "Maximum value for mobileW" 68 + }, 69 + "mobileHMin": { 70 + "type": "string", 71 + "description": "Minimum value for mobileH" 72 + }, 73 + "mobileHMax": { 74 + "type": "string", 75 + "description": "Maximum value for mobileH" 76 + }, 77 + "mobileXMin": { 78 + "type": "string", 79 + "description": "Minimum value for mobileX" 80 + }, 81 + "mobileXMax": { 82 + "type": "string", 83 + "description": "Maximum value for mobileX" 84 + }, 85 + "mobileYMin": { 86 + "type": "string", 87 + "description": "Minimum value for mobileY" 88 + }, 89 + "mobileYMax": { 90 + "type": "string", 91 + "description": "Maximum value for mobileY" 92 + }, 93 + "cardType": { 94 + "type": "string", 95 + "description": "Filter by cardType" 96 + }, 97 + "color": { 98 + "type": "string", 99 + "description": "Filter by color" 100 + }, 101 + "page": { 102 + "type": "string", 103 + "description": "Filter by page" 104 + }, 105 + "updatedAtMin": { 106 + "type": "string", 107 + "description": "Minimum value for updatedAt" 108 + }, 109 + "updatedAtMax": { 110 + "type": "string", 111 + "description": "Maximum value for updatedAt" 112 + }, 113 + "versionMin": { 114 + "type": "string", 115 + "description": "Minimum value for version" 116 + }, 117 + "versionMax": { 118 + "type": "string", 119 + "description": "Maximum value for version" 120 + }, 121 + "sort": { 122 + "type": "string", 123 + "knownValues": [ 124 + "w", 125 + "h", 126 + "x", 127 + "y", 128 + "mobileW", 129 + "mobileH", 130 + "mobileX", 131 + "mobileY", 132 + "cardType", 133 + "color", 134 + "page", 135 + "updatedAt", 136 + "version" 137 + ], 138 + "description": "Field to sort by (default: time_us)" 139 + }, 140 + "order": { 141 + "type": "string", 142 + "knownValues": ["asc", "desc"], 143 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 144 + } 145 + } 146 + }, 147 + "output": { 148 + "encoding": "application/json", 149 + "schema": { 150 + "type": "object", 151 + "required": ["records"], 152 + "properties": { 153 + "records": { 154 + "type": "array", 155 + "items": { 156 + "type": "ref", 157 + "ref": "#record" 158 + } 159 + }, 160 + "cursor": { 161 + "type": "string" 162 + }, 163 + "profiles": { 164 + "type": "array", 165 + "items": { 166 + "type": "ref", 167 + "ref": "#profileEntry" 168 + } 169 + } 170 + } 171 + } 172 + } 173 + }, 174 + "record": { 175 + "type": "object", 176 + "required": ["uri", "did", "collection", "rkey", "time_us"], 177 + "properties": { 178 + "uri": { 179 + "type": "string", 180 + "format": "at-uri" 181 + }, 182 + "did": { 183 + "type": "string", 184 + "format": "did" 185 + }, 186 + "collection": { 187 + "type": "string", 188 + "format": "nsid" 189 + }, 190 + "rkey": { 191 + "type": "string" 192 + }, 193 + "cid": { 194 + "type": "string" 195 + }, 196 + "record": { 197 + "type": "ref", 198 + "ref": "app.blento.card#main" 199 + }, 200 + "time_us": { 201 + "type": "integer" 202 + } 203 + } 204 + }, 205 + "profileEntry": { 206 + "type": "object", 207 + "required": ["did"], 208 + "properties": { 209 + "did": { 210 + "type": "string", 211 + "format": "did" 212 + }, 213 + "handle": { 214 + "type": "string" 215 + }, 216 + "uri": { 217 + "type": "string", 218 + "format": "at-uri" 219 + }, 220 + "collection": { 221 + "type": "string", 222 + "format": "nsid" 223 + }, 224 + "rkey": { 225 + "type": "string" 226 + }, 227 + "cid": { 228 + "type": "string" 229 + }, 230 + "record": { 231 + "type": "unknown" 232 + } 233 + } 234 + } 235 + } 236 + }
+27
lexicons-generated/app/blento/getCursor.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.getCursor", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the current cursor position", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "time_us": { 14 + "type": "integer" 15 + }, 16 + "date": { 17 + "type": "string" 18 + }, 19 + "seconds_ago": { 20 + "type": "integer" 21 + } 22 + } 23 + } 24 + } 25 + } 26 + } 27 + }
+44
lexicons-generated/app/blento/getOverview.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.getOverview", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get an overview of all indexed collections", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["total_records", "collections"], 13 + "properties": { 14 + "total_records": { 15 + "type": "integer" 16 + }, 17 + "collections": { 18 + "type": "array", 19 + "items": { 20 + "type": "ref", 21 + "ref": "#collectionStats" 22 + } 23 + } 24 + } 25 + } 26 + } 27 + }, 28 + "collectionStats": { 29 + "type": "object", 30 + "required": ["collection", "records", "unique_users"], 31 + "properties": { 32 + "collection": { 33 + "type": "string" 34 + }, 35 + "records": { 36 + "type": "integer" 37 + }, 38 + "unique_users": { 39 + "type": "integer" 40 + } 41 + } 42 + } 43 + } 44 + }
+67
lexicons-generated/app/blento/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.getProfile", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a user's profiles by DID or handle", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "DID or handle of the user" 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["profiles"], 24 + "properties": { 25 + "profiles": { 26 + "type": "array", 27 + "items": { 28 + "type": "ref", 29 + "ref": "#profileEntry" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + }, 36 + "profileEntry": { 37 + "type": "object", 38 + "required": ["did"], 39 + "properties": { 40 + "did": { 41 + "type": "string", 42 + "format": "did" 43 + }, 44 + "handle": { 45 + "type": "string" 46 + }, 47 + "uri": { 48 + "type": "string", 49 + "format": "at-uri" 50 + }, 51 + "collection": { 52 + "type": "string", 53 + "format": "nsid" 54 + }, 55 + "rkey": { 56 + "type": "string" 57 + }, 58 + "cid": { 59 + "type": "string" 60 + }, 61 + "record": { 62 + "type": "unknown" 63 + } 64 + } 65 + } 66 + } 67 + }
+56
lexicons-generated/app/blento/notifyOfUpdate.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.notifyOfUpdate", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Notify of a record change for immediate indexing. Fetches the record from the user's PDS and indexes (or deletes) it.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "uri": { 14 + "type": "string", 15 + "format": "at-uri", 16 + "description": "Single AT URI to fetch and index" 17 + }, 18 + "uris": { 19 + "type": "array", 20 + "items": { 21 + "type": "string", 22 + "format": "at-uri" 23 + }, 24 + "maxLength": 25, 25 + "description": "Batch of AT URIs to fetch and index (max 25)" 26 + } 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": ["indexed", "deleted"], 35 + "properties": { 36 + "indexed": { 37 + "type": "integer", 38 + "description": "Number of records created or updated" 39 + }, 40 + "deleted": { 41 + "type": "integer", 42 + "description": "Number of records deleted (not found on PDS)" 43 + }, 44 + "errors": { 45 + "type": "array", 46 + "items": { 47 + "type": "string" 48 + }, 49 + "description": "Errors for individual URIs that could not be processed" 50 + } 51 + } 52 + } 53 + } 54 + } 55 + } 56 + }
+96
lexicons-generated/app/blento/page/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.page.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single app.blento.page record by AT URI", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["uri"], 11 + "properties": { 12 + "uri": { 13 + "type": "string", 14 + "format": "at-uri", 15 + "description": "AT URI of the record" 16 + }, 17 + "profiles": { 18 + "type": "boolean", 19 + "description": "Include profile + identity info keyed by DID" 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["uri", "did", "collection", "rkey", "time_us"], 28 + "properties": { 29 + "uri": { 30 + "type": "string", 31 + "format": "at-uri" 32 + }, 33 + "did": { 34 + "type": "string", 35 + "format": "did" 36 + }, 37 + "collection": { 38 + "type": "string", 39 + "format": "nsid" 40 + }, 41 + "rkey": { 42 + "type": "string" 43 + }, 44 + "cid": { 45 + "type": "string" 46 + }, 47 + "record": { 48 + "type": "ref", 49 + "ref": "app.blento.page#main" 50 + }, 51 + "time_us": { 52 + "type": "integer" 53 + }, 54 + "profiles": { 55 + "type": "array", 56 + "items": { 57 + "type": "ref", 58 + "ref": "#profileEntry" 59 + } 60 + } 61 + } 62 + } 63 + } 64 + }, 65 + "profileEntry": { 66 + "type": "object", 67 + "required": ["did"], 68 + "properties": { 69 + "did": { 70 + "type": "string", 71 + "format": "did" 72 + }, 73 + "handle": { 74 + "type": "string" 75 + }, 76 + "uri": { 77 + "type": "string", 78 + "format": "at-uri" 79 + }, 80 + "collection": { 81 + "type": "string", 82 + "format": "nsid" 83 + }, 84 + "rkey": { 85 + "type": "string" 86 + }, 87 + "cid": { 88 + "type": "string" 89 + }, 90 + "record": { 91 + "type": "unknown" 92 + } 93 + } 94 + } 95 + } 96 + }
+138
lexicons-generated/app/blento/page/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.blento.page.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Query app.blento.page records with filters", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 200, 15 + "default": 50 16 + }, 17 + "cursor": { 18 + "type": "string" 19 + }, 20 + "actor": { 21 + "type": "string", 22 + "format": "at-identifier", 23 + "description": "Filter by DID or handle (triggers on-demand backfill)" 24 + }, 25 + "profiles": { 26 + "type": "boolean", 27 + "description": "Include profile + identity info keyed by DID" 28 + }, 29 + "name": { 30 + "type": "string", 31 + "description": "Filter by name" 32 + }, 33 + "description": { 34 + "type": "string", 35 + "description": "Filter by description" 36 + }, 37 + "sort": { 38 + "type": "string", 39 + "knownValues": ["name", "description"], 40 + "description": "Field to sort by (default: time_us)" 41 + }, 42 + "order": { 43 + "type": "string", 44 + "knownValues": ["asc", "desc"], 45 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 46 + } 47 + } 48 + }, 49 + "output": { 50 + "encoding": "application/json", 51 + "schema": { 52 + "type": "object", 53 + "required": ["records"], 54 + "properties": { 55 + "records": { 56 + "type": "array", 57 + "items": { 58 + "type": "ref", 59 + "ref": "#record" 60 + } 61 + }, 62 + "cursor": { 63 + "type": "string" 64 + }, 65 + "profiles": { 66 + "type": "array", 67 + "items": { 68 + "type": "ref", 69 + "ref": "#profileEntry" 70 + } 71 + } 72 + } 73 + } 74 + } 75 + }, 76 + "record": { 77 + "type": "object", 78 + "required": ["uri", "did", "collection", "rkey", "time_us"], 79 + "properties": { 80 + "uri": { 81 + "type": "string", 82 + "format": "at-uri" 83 + }, 84 + "did": { 85 + "type": "string", 86 + "format": "did" 87 + }, 88 + "collection": { 89 + "type": "string", 90 + "format": "nsid" 91 + }, 92 + "rkey": { 93 + "type": "string" 94 + }, 95 + "cid": { 96 + "type": "string" 97 + }, 98 + "record": { 99 + "type": "ref", 100 + "ref": "app.blento.page#main" 101 + }, 102 + "time_us": { 103 + "type": "integer" 104 + } 105 + } 106 + }, 107 + "profileEntry": { 108 + "type": "object", 109 + "required": ["did"], 110 + "properties": { 111 + "did": { 112 + "type": "string", 113 + "format": "did" 114 + }, 115 + "handle": { 116 + "type": "string" 117 + }, 118 + "uri": { 119 + "type": "string", 120 + "format": "at-uri" 121 + }, 122 + "collection": { 123 + "type": "string", 124 + "format": "nsid" 125 + }, 126 + "rkey": { 127 + "type": "string" 128 + }, 129 + "cid": { 130 + "type": "string" 131 + }, 132 + "record": { 133 + "type": "unknown" 134 + } 135 + } 136 + } 137 + } 138 + }
+31
lexicons/app/blento/card.json
··· 1 + { 2 + "$type": "com.atproto.lexicon.schema", 3 + "lexicon": 1, 4 + "id": "app.blento.card", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["w", "h", "x", "y", "cardType"], 12 + "properties": { 13 + "w": { "type": "integer" }, 14 + "h": { "type": "integer" }, 15 + "x": { "type": "integer" }, 16 + "y": { "type": "integer" }, 17 + "mobileW": { "type": "integer" }, 18 + "mobileH": { "type": "integer" }, 19 + "mobileX": { "type": "integer" }, 20 + "mobileY": { "type": "integer" }, 21 + "cardType": { "type": "string" }, 22 + "cardData": { "type": "unknown" }, 23 + "color": { "type": "string" }, 24 + "page": { "type": "string" }, 25 + "updatedAt": { "type": "string", "format": "datetime" }, 26 + "version": { "type": "integer" } 27 + } 28 + } 29 + } 30 + } 31 + }
+35
lexicons/app/blento/page.json
··· 1 + { 2 + "$type": "com.atproto.lexicon.schema", 3 + "lexicon": 1, 4 + "id": "app.blento.page", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "key": "any", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "url": { "type": "string", "format": "uri" }, 13 + "name": { "type": "string" }, 14 + "description": { "type": "string" }, 15 + "icon": { "type": "blob", "accept": ["image/*"] }, 16 + "preferences": { 17 + "type": "ref", 18 + "ref": "#preferences" 19 + } 20 + } 21 + } 22 + }, 23 + "preferences": { 24 + "type": "object", 25 + "properties": { 26 + "hideProfile": { "type": "boolean" }, 27 + "hideProfileSection": { "type": "boolean" }, 28 + "profilePosition": { "type": "string" }, 29 + "accentColor": { "type": "string" }, 30 + "baseColor": { "type": "string" }, 31 + "editedOn": { "type": "integer" } 32 + } 33 + } 34 + } 35 + }
+14 -2
package.json
··· 5 5 "type": "module", 6 6 "scripts": { 7 7 "dev": "vite dev", 8 - "build": "NODE_OPTIONS='--max-old-space-size=4096' vite build", 8 + "build": "NODE_OPTIONS='--max-old-space-size=4096' vite build && tsx scripts/append-scheduled.ts", 9 9 "preview": "pnpm run build && wrangler dev", 10 10 "prepare": "svelte-kit sync || echo ''", 11 11 "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", ··· 14 14 "format": "eslint --fix . && prettier --write .", 15 15 "test": "vitest run", 16 16 "deploy": "pnpm run build && wrangler deploy", 17 - "cf-typegen": "wrangler types ./src/worker-configuration.d.ts" 17 + "cf-typegen": "wrangler types ./src/worker-configuration.d.ts", 18 + "env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", 19 + "env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", 20 + "env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts", 21 + "tunnel": "npx tsx src/lib/atproto/scripts/tunnel.ts", 22 + "generate": "npx tsx scripts/generate.ts", 23 + "sync": "npx tsx scripts/sync.ts", 24 + "sync:remote": "npx tsx scripts/sync.ts --remote" 18 25 }, 19 26 "devDependencies": { 27 + "@atcute/lex-cli": "^2.6.1", 28 + "@atcute/lexicon-doc": "^2.1.2", 20 29 "@eslint/compat": "^2.0.3", 21 30 "@eslint/js": "^10.0.1", 22 31 "@sveltejs/adapter-cloudflare": "^7.2.8", ··· 36 45 "svelte-check": "^4.4.5", 37 46 "svelte-maplibre-gl": "^1.0.3", 38 47 "tailwindcss": "^4.2.1", 48 + "tsx": "^4.21.0", 39 49 "typescript": "^5.9.3", 40 50 "typescript-eslint": "^8.57.0", 41 51 "valibot": "^1.3.1", ··· 51 61 "@atcute/identity-resolver": "^1.2.2", 52 62 "@atcute/lexicons": "^1.2.9", 53 63 "@atcute/oauth-browser-client": "^3.0.0", 64 + "@atcute/oauth-node-client": "^1.1.0", 54 65 "@atcute/standard-site": "^1.0.1", 55 66 "@atcute/tid": "^1.1.2", 67 + "@atmo-dev/contrail": "^0.0.8", 56 68 "@cloudflare/workers-types": "^4.20260313.1", 57 69 "@ethercorps/sveltekit-og": "^4.2.1", 58 70 "@floating-ui/dom": "^1.7.6",
+377 -64
pnpm-lock.yaml
··· 32 32 '@atcute/oauth-browser-client': 33 33 specifier: ^3.0.0 34 34 version: 3.0.0(@atcute/identity@1.1.3) 35 + '@atcute/oauth-node-client': 36 + specifier: ^1.1.0 37 + version: 1.1.0 35 38 '@atcute/standard-site': 36 39 specifier: ^1.0.1 37 40 version: 1.0.1 38 41 '@atcute/tid': 39 42 specifier: ^1.1.2 40 43 version: 1.1.2 44 + '@atmo-dev/contrail': 45 + specifier: ^0.0.8 46 + version: 0.0.8(@atcute/identity@1.1.3)(react@19.2.4) 41 47 '@cloudflare/workers-types': 42 48 specifier: ^4.20260313.1 43 49 version: 4.20260313.1 44 50 '@ethercorps/sveltekit-og': 45 51 specifier: ^4.2.1 46 - version: 4.2.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1))) 52 + version: 4.2.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))) 47 53 '@floating-ui/dom': 48 54 specifier: ^1.7.6 49 55 version: 1.7.6 50 56 '@foxui/3d': 51 57 specifier: ^0.8.0 52 - version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 58 + version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 53 59 '@foxui/colors': 54 60 specifier: ^0.8.0 55 - version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 61 + version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 56 62 '@foxui/core': 57 63 specifier: ^0.8.0 58 - version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 64 + version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 59 65 '@foxui/social': 60 66 specifier: ^0.8.0 61 - version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 67 + version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 62 68 '@foxui/time': 63 69 specifier: ^0.8.0 64 - version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 70 + version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 65 71 '@foxui/visual': 66 72 specifier: ^0.8.0 67 - version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 73 + version: 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 68 74 '@internationalized/date': 69 75 specifier: ^3.12.0 70 76 version: 3.12.0 ··· 130 136 version: 0.183.1 131 137 bits-ui: 132 138 specifier: ^2.16.3 133 - version: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 139 + version: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 134 140 clsx: 135 141 specifier: ^2.1.1 136 142 version: 2.1.1 ··· 201 207 specifier: ^4.73.0 202 208 version: 4.73.0(@cloudflare/workers-types@4.20260313.1) 203 209 devDependencies: 210 + '@atcute/lex-cli': 211 + specifier: ^2.6.1 212 + version: 2.8.0 213 + '@atcute/lexicon-doc': 214 + specifier: ^2.1.2 215 + version: 2.2.0 204 216 '@eslint/compat': 205 217 specifier: ^2.0.3 206 218 version: 2.0.3(eslint@10.0.3(jiti@2.6.1)) ··· 209 221 version: 10.0.1(eslint@10.0.3(jiti@2.6.1)) 210 222 '@sveltejs/adapter-cloudflare': 211 223 specifier: ^7.2.8 212 - version: 7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(wrangler@4.73.0(@cloudflare/workers-types@4.20260313.1)) 224 + version: 7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.73.0(@cloudflare/workers-types@4.20260313.1)) 213 225 '@sveltejs/kit': 214 226 specifier: ^2.55.0 215 - version: 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 227 + version: 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 216 228 '@sveltejs/vite-plugin-svelte': 217 229 specifier: ^7.0.0 218 - version: 7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 230 + version: 7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 219 231 '@tailwindcss/forms': 220 232 specifier: ^0.5.11 221 233 version: 0.5.11(tailwindcss@4.2.1) 222 234 '@tailwindcss/vite': 223 235 specifier: ^4.2.1 224 - version: 4.2.1(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 236 + version: 4.2.1(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 225 237 '@types/turndown': 226 238 specifier: ^5.0.6 227 239 version: 5.0.6 ··· 258 270 tailwindcss: 259 271 specifier: ^4.2.1 260 272 version: 4.2.1 273 + tsx: 274 + specifier: ^4.21.0 275 + version: 4.21.0 261 276 typescript: 262 277 specifier: ^5.9.3 263 278 version: 5.9.3 ··· 269 284 version: 1.3.1(typescript@5.9.3) 270 285 vite: 271 286 specifier: ^8.0.0 272 - version: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 287 + version: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 273 288 vitest: 274 289 specifier: ^4.1.4 275 - version: 4.1.4(@types/node@25.0.10)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 290 + version: 4.1.4(@types/node@25.0.10)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 276 291 277 292 packages: 278 293 ··· 287 302 288 303 '@atcute/bluesky@3.3.0': 289 304 resolution: {integrity: sha512-TrLnlxuD6F/D2ZYzJ3aCiRD0yiFuhmVsd6oULNzzr8V9Xzlufg0yxkRiGmbMiF2iI508y/MFi6vzo625301c5A==} 305 + 306 + '@atcute/car@5.1.1': 307 + resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} 308 + 309 + '@atcute/cbor@2.3.2': 310 + resolution: {integrity: sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ==} 311 + 312 + '@atcute/cid@2.4.1': 313 + resolution: {integrity: sha512-bwhna69RCv7yetXudtj+2qrMPYvhhIQqvJz6YUpUS98v7OdF3X2dnye9Nig2NDrklZcuyOsu7sQo7GOykJXRLQ==} 290 314 291 315 '@atcute/client@4.2.1': 292 316 resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} 293 317 318 + '@atcute/crypto@2.4.1': 319 + resolution: {integrity: sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==} 320 + 294 321 '@atcute/identity-resolver@1.2.2': 295 322 resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==} 296 323 peerDependencies: ··· 299 326 '@atcute/identity@1.1.3': 300 327 resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 301 328 329 + '@atcute/identity@1.1.4': 330 + resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==} 331 + 332 + '@atcute/jetstream@1.1.2': 333 + resolution: {integrity: sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==} 334 + 335 + '@atcute/lex-cli@2.8.0': 336 + resolution: {integrity: sha512-eNPO0hhGhrCXQ7vEgVhqAaSHSsT3me1Jcc99rHaPgne1xP7fBfprf+E02M6BUqwrBz95YpnyuLPmVKNEk1jLwA==} 337 + hasBin: true 338 + 339 + '@atcute/lexicon-doc@2.2.0': 340 + resolution: {integrity: sha512-6l4lDlL6KPLDGknRh6HlfGbv98haUgQ0DFaAr1yA4vA95b8YYZUZ4/370ENpiq+d6Lv0tdDAMvOon2mynrp3pQ==} 341 + 342 + '@atcute/lexicon-resolver@0.1.6': 343 + resolution: {integrity: sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==} 344 + peerDependencies: 345 + '@atcute/identity': ^1.1.0 346 + '@atcute/identity-resolver': ^1.1.3 347 + 302 348 '@atcute/lexicons@1.2.9': 303 349 resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==} 304 350 351 + '@atcute/lexicons@1.3.0': 352 + resolution: {integrity: sha512-Eq5y+9onnCXNVUlNiMf31beSXHKqptB7lUo/68YbhlmxdaR7ooywHmahya9goP5AsmlYEA1z+dRPXIDAa9O7cg==} 353 + 354 + '@atcute/mst@1.0.0': 355 + resolution: {integrity: sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==} 356 + 305 357 '@atcute/multibase@1.1.8': 306 358 resolution: {integrity: sha512-pJgtImMZKCjqwRbu+2GzB+4xQjKBXDwdZOzeqe0u97zYKRGftpGYGvYv3+pMe2xXe+msDyu7Nv8iJp+U14otTA==} 359 + 360 + '@atcute/multibase@1.2.0': 361 + resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} 307 362 308 363 '@atcute/oauth-browser-client@3.0.0': 309 364 resolution: {integrity: sha512-7AbKV8tTe7aRJNJV7gCcWHSVEADb2nr58O1p7dQsf73HSe9pvlBkj/Vk1yjjtH691uAVYkwhHSh0bC7D8XdwJw==} ··· 314 369 '@atcute/oauth-keyset@0.1.0': 315 370 resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==} 316 371 372 + '@atcute/oauth-node-client@1.1.0': 373 + resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==} 374 + 317 375 '@atcute/oauth-types@0.1.1': 318 376 resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} 377 + 378 + '@atcute/repo@0.1.4': 379 + resolution: {integrity: sha512-uzbGJkE+1A8UFviosJrtw7HW87u8nCCH1V3yOQ79FPrRhS67EvEHF6GTg4aMkP21ze/pRtttJ1k9pFfDmyTlTg==} 319 380 320 381 '@atcute/standard-site@1.0.1': 321 382 resolution: {integrity: sha512-wL4ZAvbe3p7NxC92rRgc6vbd+0feNDEAfEcBLA+68KDTUtmtEko5lr09R31P7AWlN4MVTMRj5iLb9UaTThIzWw==} ··· 334 395 335 396 '@atcute/util-text@1.1.1': 336 397 resolution: {integrity: sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w==} 398 + 399 + '@atcute/util-text@1.2.0': 400 + resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==} 401 + 402 + '@atcute/varint@2.0.0': 403 + resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==} 404 + 405 + '@atmo-dev/contrail@0.0.8': 406 + resolution: {integrity: sha512-sXtdd3Z8VNVoSinrX3ww978ctxtHBl0bX5tP51XOY0IWKJ4xl9zqkPOIIBMzbVE3IyU2Vq2B9Whi3VAhyd2Qdg==} 407 + peerDependencies: 408 + pg: ^8.0.0 409 + peerDependenciesMeta: 410 + pg: 411 + optional: true 337 412 338 413 '@badrap/valita@0.4.6': 339 414 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} ··· 872 947 '@maplibre/vt-pbf@4.3.0': 873 948 resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==} 874 949 950 + '@mary-ext/event-iterator@1.0.0': 951 + resolution: {integrity: sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==} 952 + 953 + '@mary-ext/simple-event-emitter@1.0.1': 954 + resolution: {integrity: sha512-9+VvZisxZ/gSg+JJH7hmXaA8Qj42Qjz3O58RSB+INYc8iLA0icATZxHB9vKbj59ojDGZjO3hCKzMXocx3L0H8w==} 955 + 875 956 '@mixmark-io/domino@2.2.0': 876 957 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} 877 958 878 959 '@napi-rs/wasm-runtime@1.1.1': 879 960 resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 880 961 962 + '@noble/secp256k1@3.1.0': 963 + resolution: {integrity: sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==} 964 + 881 965 '@number-flow/svelte@0.4.0': 882 966 resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==} 883 967 peerDependencies: 884 968 svelte: ^4 || ^5 969 + 970 + '@optique/core@0.10.7': 971 + resolution: {integrity: sha512-FwSX8ILFqzcCqZi6Xetsa4flJp/yyqFG4d4eFD98BtqdzxxuylzdrKvsXj/ow8mcoVjYkTuaIkqHSBxonqMcQg==} 972 + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} 973 + 974 + '@optique/run@0.10.7': 975 + resolution: {integrity: sha512-1CVdH8uyptj1nFGS2MLacSmZceRClez4LD/G/Gm38wrAVnJq6I+9Fvyh2bVHErsZLQzR0a12CYMUWIgDKY3X1w==} 976 + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} 885 977 886 978 '@oxc-project/runtime@0.115.0': 887 979 resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} ··· 1900 1992 resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 1901 1993 engines: {node: '>=0.10.0'} 1902 1994 1995 + event-target-polyfill@0.0.4: 1996 + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} 1997 + 1903 1998 expect-type@1.3.0: 1904 1999 resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 1905 2000 engines: {node: '>=12.0.0'} ··· 1954 2049 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1955 2050 os: [darwin] 1956 2051 2052 + get-tsconfig@4.14.0: 2053 + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} 2054 + 1957 2055 gl-matrix@3.4.4: 1958 2056 resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} 1959 2057 ··· 1985 2083 1986 2084 hls.js@1.6.15: 1987 2085 resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} 2086 + 2087 + hono@4.12.14: 2088 + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} 2089 + engines: {node: '>=16.9.0'} 1988 2090 1989 2091 htmlparser2@10.1.0: 1990 2092 resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} ··· 2373 2475 parse5@7.3.0: 2374 2476 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 2375 2477 2478 + partysocket@1.1.16: 2479 + resolution: {integrity: sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w==} 2480 + peerDependencies: 2481 + react: '>=17' 2482 + peerDependenciesMeta: 2483 + react: 2484 + optional: true 2485 + 2376 2486 path-exists@4.0.0: 2377 2487 resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 2378 2488 engines: {node: '>=8'} ··· 2522 2632 engines: {node: '>=14'} 2523 2633 hasBin: true 2524 2634 2635 + prettier@3.8.3: 2636 + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} 2637 + engines: {node: '>=14'} 2638 + hasBin: true 2639 + 2525 2640 prop-types@15.8.1: 2526 2641 resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 2527 2642 ··· 2651 2766 2652 2767 resize-observer-polyfill@1.5.1: 2653 2768 resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} 2769 + 2770 + resolve-pkg-maps@1.0.0: 2771 + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 2654 2772 2655 2773 resolve-protobuf-schema@2.1.0: 2656 2774 resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} ··· 2919 3037 tslib@2.8.1: 2920 3038 resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 2921 3039 3040 + tsx@4.21.0: 3041 + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} 3042 + engines: {node: '>=18.0.0'} 3043 + hasBin: true 3044 + 2922 3045 turndown@7.2.2: 2923 3046 resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==} 2924 3047 ··· 2929 3052 resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 2930 3053 engines: {node: '>= 0.8.0'} 2931 3054 3055 + type-fest@4.41.0: 3056 + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} 3057 + engines: {node: '>=16'} 3058 + 2932 3059 typescript-eslint@8.57.0: 2933 3060 resolution: {integrity: sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==} 2934 3061 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} ··· 3167 3294 resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 3168 3295 engines: {node: '>=10'} 3169 3296 3297 + yocto-queue@1.2.2: 3298 + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} 3299 + engines: {node: '>=12.20'} 3300 + 3170 3301 yoga-wasm-web@0.3.3: 3171 3302 resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} 3172 3303 ··· 3194 3325 '@atcute/atproto': 3.1.10 3195 3326 '@atcute/lexicons': 1.2.9 3196 3327 3328 + '@atcute/car@5.1.1': 3329 + dependencies: 3330 + '@atcute/cbor': 2.3.2 3331 + '@atcute/cid': 2.4.1 3332 + '@atcute/uint8array': 1.1.1 3333 + '@atcute/varint': 2.0.0 3334 + 3335 + '@atcute/cbor@2.3.2': 3336 + dependencies: 3337 + '@atcute/cid': 2.4.1 3338 + '@atcute/multibase': 1.1.8 3339 + '@atcute/uint8array': 1.1.1 3340 + 3341 + '@atcute/cid@2.4.1': 3342 + dependencies: 3343 + '@atcute/multibase': 1.1.8 3344 + '@atcute/uint8array': 1.1.1 3345 + 3197 3346 '@atcute/client@4.2.1': 3198 3347 dependencies: 3199 3348 '@atcute/identity': 1.1.3 3200 3349 '@atcute/lexicons': 1.2.9 3201 3350 3351 + '@atcute/crypto@2.4.1': 3352 + dependencies: 3353 + '@atcute/multibase': 1.2.0 3354 + '@atcute/uint8array': 1.1.1 3355 + '@noble/secp256k1': 3.1.0 3356 + 3202 3357 '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3)': 3203 3358 dependencies: 3204 3359 '@atcute/identity': 1.1.3 ··· 3206 3361 '@atcute/util-fetch': 1.0.5 3207 3362 '@badrap/valita': 0.4.6 3208 3363 3364 + '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': 3365 + dependencies: 3366 + '@atcute/identity': 1.1.4 3367 + '@atcute/lexicons': 1.2.9 3368 + '@atcute/util-fetch': 1.0.5 3369 + '@badrap/valita': 0.4.6 3370 + 3209 3371 '@atcute/identity@1.1.3': 3210 3372 dependencies: 3211 3373 '@atcute/lexicons': 1.2.9 3212 3374 '@badrap/valita': 0.4.6 3213 3375 3376 + '@atcute/identity@1.1.4': 3377 + dependencies: 3378 + '@atcute/lexicons': 1.3.0 3379 + '@badrap/valita': 0.4.6 3380 + 3381 + '@atcute/jetstream@1.1.2(react@19.2.4)': 3382 + dependencies: 3383 + '@atcute/lexicons': 1.2.9 3384 + '@badrap/valita': 0.4.6 3385 + '@mary-ext/event-iterator': 1.0.0 3386 + '@mary-ext/simple-event-emitter': 1.0.1 3387 + partysocket: 1.1.16(react@19.2.4) 3388 + type-fest: 4.41.0 3389 + yocto-queue: 1.2.2 3390 + transitivePeerDependencies: 3391 + - react 3392 + 3393 + '@atcute/lex-cli@2.8.0': 3394 + dependencies: 3395 + '@atcute/identity': 1.1.4 3396 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) 3397 + '@atcute/lexicon-doc': 2.2.0 3398 + '@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.4) 3399 + '@atcute/lexicons': 1.3.0 3400 + '@badrap/valita': 0.4.6 3401 + '@optique/core': 0.10.7 3402 + '@optique/run': 0.10.7 3403 + picocolors: 1.1.1 3404 + prettier: 3.8.3 3405 + 3406 + '@atcute/lexicon-doc@2.2.0': 3407 + dependencies: 3408 + '@atcute/identity': 1.1.4 3409 + '@atcute/lexicons': 1.3.0 3410 + '@atcute/uint8array': 1.1.1 3411 + '@atcute/util-text': 1.2.0 3412 + '@badrap/valita': 0.4.6 3413 + 3414 + '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.4)': 3415 + dependencies: 3416 + '@atcute/crypto': 2.4.1 3417 + '@atcute/identity': 1.1.4 3418 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 3419 + '@atcute/lexicon-doc': 2.2.0 3420 + '@atcute/lexicons': 1.3.0 3421 + '@atcute/repo': 0.1.4 3422 + '@atcute/util-fetch': 1.0.5 3423 + '@badrap/valita': 0.4.6 3424 + 3214 3425 '@atcute/lexicons@1.2.9': 3215 3426 dependencies: 3216 3427 '@atcute/uint8array': 1.1.1 ··· 3218 3429 '@standard-schema/spec': 1.1.0 3219 3430 esm-env: 1.2.2 3220 3431 3432 + '@atcute/lexicons@1.3.0': 3433 + dependencies: 3434 + '@atcute/uint8array': 1.1.1 3435 + '@atcute/util-text': 1.2.0 3436 + '@standard-schema/spec': 1.1.0 3437 + esm-env: 1.2.2 3438 + 3439 + '@atcute/mst@1.0.0': 3440 + dependencies: 3441 + '@atcute/cbor': 2.3.2 3442 + '@atcute/cid': 2.4.1 3443 + '@atcute/uint8array': 1.1.1 3444 + 3221 3445 '@atcute/multibase@1.1.8': 3446 + dependencies: 3447 + '@atcute/uint8array': 1.1.1 3448 + 3449 + '@atcute/multibase@1.2.0': 3222 3450 dependencies: 3223 3451 '@atcute/uint8array': 1.1.1 3224 3452 ··· 3245 3473 dependencies: 3246 3474 '@atcute/oauth-crypto': 0.1.0 3247 3475 3476 + '@atcute/oauth-node-client@1.1.0': 3477 + dependencies: 3478 + '@atcute/client': 4.2.1 3479 + '@atcute/identity': 1.1.3 3480 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 3481 + '@atcute/lexicons': 1.2.9 3482 + '@atcute/oauth-crypto': 0.1.0 3483 + '@atcute/oauth-keyset': 0.1.0 3484 + '@atcute/oauth-types': 0.1.1 3485 + '@atcute/util-fetch': 1.0.5 3486 + '@badrap/valita': 0.4.6 3487 + nanoid: 5.1.6 3488 + 3248 3489 '@atcute/oauth-types@0.1.1': 3249 3490 dependencies: 3250 3491 '@atcute/identity': 1.1.3 ··· 3252 3493 '@atcute/oauth-keyset': 0.1.0 3253 3494 '@badrap/valita': 0.4.6 3254 3495 3496 + '@atcute/repo@0.1.4': 3497 + dependencies: 3498 + '@atcute/car': 5.1.1 3499 + '@atcute/cbor': 2.3.2 3500 + '@atcute/cid': 2.4.1 3501 + '@atcute/crypto': 2.4.1 3502 + '@atcute/lexicons': 1.3.0 3503 + '@atcute/mst': 1.0.0 3504 + '@atcute/uint8array': 1.1.1 3505 + 3255 3506 '@atcute/standard-site@1.0.1': 3256 3507 dependencies: 3257 3508 '@atcute/atproto': 3.1.10 ··· 3272 3523 '@atcute/util-text@1.1.1': 3273 3524 dependencies: 3274 3525 unicode-segmenter: 0.14.5 3526 + 3527 + '@atcute/util-text@1.2.0': 3528 + dependencies: 3529 + unicode-segmenter: 0.14.5 3530 + 3531 + '@atcute/varint@2.0.0': {} 3532 + 3533 + '@atmo-dev/contrail@0.0.8(@atcute/identity@1.1.3)(react@19.2.4)': 3534 + dependencies: 3535 + '@atcute/atproto': 3.1.10 3536 + '@atcute/client': 4.2.1 3537 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 3538 + '@atcute/jetstream': 1.1.2(react@19.2.4) 3539 + '@atcute/lexicons': 1.2.9 3540 + hono: 4.12.14 3541 + transitivePeerDependencies: 3542 + - '@atcute/identity' 3543 + - react 3275 3544 3276 3545 '@badrap/valita@0.4.6': {} 3277 3546 ··· 3440 3709 '@eslint/core': 1.1.1 3441 3710 levn: 0.4.1 3442 3711 3443 - '@ethercorps/sveltekit-og@4.2.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))': 3712 + '@ethercorps/sveltekit-og@4.2.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))': 3444 3713 dependencies: 3445 3714 '@resvg/resvg-wasm': 2.6.2 3446 - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 3715 + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 3447 3716 '@takumi-rs/helpers': 0.55.4 3448 3717 '@takumi-rs/image-response': 0.55.4 3449 3718 '@takumi-rs/wasm': 0.55.4 ··· 3463 3732 3464 3733 '@floating-ui/utils@0.2.11': {} 3465 3734 3466 - '@foxui/3d@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3735 + '@foxui/3d@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3467 3736 dependencies: 3468 - '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3737 + '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3469 3738 '@threlte/core': 8.4.1(svelte@5.53.11)(three@0.183.2) 3470 3739 '@threlte/extras': 9.8.1(@types/three@0.183.1)(svelte@5.53.11)(three@0.183.2) 3471 3740 '@types/three': 0.183.1 3472 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3741 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3473 3742 svelte: 5.53.11 3474 3743 tailwindcss: 4.2.1 3475 3744 three: 0.183.2 ··· 3480 3749 - react-dom 3481 3750 - vue 3482 3751 3483 - '@foxui/colors@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3752 + '@foxui/colors@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3484 3753 dependencies: 3485 - '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3754 + '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3486 3755 '@texel/color': 1.1.11 3487 3756 '@use-gesture/vanilla': 10.3.1 3488 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3757 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3489 3758 svelte: 5.53.11 3490 3759 tailwindcss: 4.2.1 3491 3760 transitivePeerDependencies: ··· 3495 3764 - react-dom 3496 3765 - vue 3497 3766 3498 - '@foxui/core@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3767 + '@foxui/core@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3499 3768 dependencies: 3500 3769 '@number-flow/svelte': 0.4.0(svelte@5.53.11) 3501 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3770 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3502 3771 clsx: 2.1.1 3503 3772 dompurify: 3.3.3 3504 3773 mode-watcher: 1.1.0(svelte@5.53.11) ··· 3515 3784 - react-dom 3516 3785 - vue 3517 3786 3518 - '@foxui/social@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3787 + '@foxui/social@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3519 3788 dependencies: 3520 3789 '@atcute/bluesky': 3.3.0 3521 3790 '@atcute/bluesky-richtext-segmenter': 3.0.0 3522 - '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3523 - '@foxui/text': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3524 - '@foxui/time': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3791 + '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3792 + '@foxui/text': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3793 + '@foxui/time': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3525 3794 '@use-gesture/vanilla': 10.3.1 3526 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3795 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3527 3796 emoji-picker-element: 1.29.1 3528 3797 hls.js: 1.6.15 3529 3798 is-emoji-supported: 0.0.5 ··· 3540 3809 - react-dom 3541 3810 - vue 3542 3811 3543 - '@foxui/text@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3812 + '@foxui/text@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/extensions@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(highlight.js@11.11.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3544 3813 dependencies: 3545 3814 '@floating-ui/dom': 1.7.6 3546 - '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3815 + '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3547 3816 '@tiptap/core': 3.20.1(@tiptap/pm@3.20.1) 3548 3817 '@tiptap/extension-bubble-menu': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1) 3549 3818 '@tiptap/extension-code-block-lowlight': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/extension-code-block@3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1)(highlight.js@11.11.1)(lowlight@3.3.0) ··· 3563 3832 '@tiptap/pm': 3.20.1 3564 3833 '@tiptap/starter-kit': 3.20.1 3565 3834 '@tiptap/suggestion': 3.20.1(@tiptap/core@3.20.1(@tiptap/pm@3.20.1))(@tiptap/pm@3.20.1) 3566 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3835 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3567 3836 lowlight: 3.3.0 3568 3837 state: link:@tiptap/pm/state 3569 3838 svelte: 5.53.11 ··· 3579 3848 - react-dom 3580 3849 - vue 3581 3850 3582 - '@foxui/time@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3851 + '@foxui/time@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3583 3852 dependencies: 3584 - '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3853 + '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3585 3854 '@number-flow/svelte': 0.4.0(svelte@5.53.11) 3586 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3855 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3587 3856 svelte: 5.53.11 3588 3857 tailwindcss: 4.2.1 3589 3858 transitivePeerDependencies: ··· 3593 3862 - react-dom 3594 3863 - vue 3595 3864 3596 - '@foxui/visual@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3865 + '@foxui/visual@0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1)': 3597 3866 dependencies: 3598 - '@foxui/colors': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3599 - '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3600 - bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 3867 + '@foxui/colors': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3868 + '@foxui/core': 0.8.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.53.11)(tailwindcss@4.2.1) 3869 + bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 3601 3870 canvas-confetti: 1.9.4 3602 3871 cheerio: 1.2.0 3603 3872 svelte: 5.53.11 ··· 3790 4059 pbf: 4.0.1 3791 4060 supercluster: 8.0.1 3792 4061 4062 + '@mary-ext/event-iterator@1.0.0': 4063 + dependencies: 4064 + yocto-queue: 1.2.2 4065 + 4066 + '@mary-ext/simple-event-emitter@1.0.1': {} 4067 + 3793 4068 '@mixmark-io/domino@2.2.0': {} 3794 4069 3795 4070 '@napi-rs/wasm-runtime@1.1.1': ··· 3799 4074 '@tybys/wasm-util': 0.10.1 3800 4075 optional: true 3801 4076 4077 + '@noble/secp256k1@3.1.0': {} 4078 + 3802 4079 '@number-flow/svelte@0.4.0(svelte@5.53.11)': 3803 4080 dependencies: 3804 4081 esm-env: 1.2.2 3805 4082 number-flow: 0.6.0 3806 4083 svelte: 5.53.11 3807 4084 4085 + '@optique/core@0.10.7': {} 4086 + 4087 + '@optique/run@0.10.7': 4088 + dependencies: 4089 + '@optique/core': 0.10.7 4090 + 3808 4091 '@oxc-project/runtime@0.115.0': {} 3809 4092 3810 4093 '@oxc-project/types@0.115.0': {} ··· 3891 4174 dependencies: 3892 4175 acorn: 8.16.0 3893 4176 3894 - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(wrangler@4.73.0(@cloudflare/workers-types@4.20260313.1))': 4177 + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.73.0(@cloudflare/workers-types@4.20260313.1))': 3895 4178 dependencies: 3896 4179 '@cloudflare/workers-types': 4.20260313.1 3897 - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 4180 + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 3898 4181 worktop: 0.8.0-next.18 3899 4182 wrangler: 4.73.0(@cloudflare/workers-types@4.20260313.1) 3900 4183 3901 - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1))': 4184 + '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 3902 4185 dependencies: 3903 4186 '@standard-schema/spec': 1.1.0 3904 4187 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) 3905 - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 4188 + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 3906 4189 '@types/cookie': 0.6.0 3907 4190 acorn: 8.16.0 3908 4191 cookie: 0.6.0 ··· 3914 4197 set-cookie-parser: 3.0.1 3915 4198 sirv: 3.0.2 3916 4199 svelte: 5.53.11 3917 - vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 4200 + vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 3918 4201 optionalDependencies: 3919 4202 typescript: 5.9.3 3920 4203 3921 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1))': 4204 + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 3922 4205 dependencies: 3923 4206 deepmerge: 4.3.1 3924 4207 magic-string: 0.30.21 3925 4208 obug: 2.1.1 3926 4209 svelte: 5.53.11 3927 - vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 3928 - vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 4210 + vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 4211 + vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 3929 4212 3930 4213 '@swc/helpers@0.5.19': 3931 4214 dependencies: ··· 4002 4285 postcss-selector-parser: 6.0.10 4003 4286 tailwindcss: 4.2.1 4004 4287 4005 - '@tailwindcss/vite@4.2.1(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1))': 4288 + '@tailwindcss/vite@4.2.1(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 4006 4289 dependencies: 4007 4290 '@tailwindcss/node': 4.2.1 4008 4291 '@tailwindcss/oxide': 4.2.1 4009 4292 tailwindcss: 4.2.1 4010 - vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 4293 + vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 4011 4294 4012 4295 '@takumi-rs/core-darwin-arm64@0.55.4': 4013 4296 optional: true ··· 4451 4734 chai: 6.2.2 4452 4735 tinyrainbow: 3.1.0 4453 4736 4454 - '@vitest/mocker@4.1.4(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1))': 4737 + '@vitest/mocker@4.1.4(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': 4455 4738 dependencies: 4456 4739 '@vitest/spy': 4.1.4 4457 4740 estree-walker: 3.0.3 4458 4741 magic-string: 0.30.21 4459 4742 optionalDependencies: 4460 - vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 4743 + vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 4461 4744 4462 4745 '@vitest/pretty-format@4.1.4': 4463 4746 dependencies: ··· 4514 4797 dependencies: 4515 4798 require-from-string: 2.0.2 4516 4799 4517 - bits-ui@2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11): 4800 + bits-ui@2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11): 4518 4801 dependencies: 4519 4802 '@floating-ui/core': 1.7.5 4520 4803 '@floating-ui/dom': 1.7.6 4521 4804 '@internationalized/date': 3.12.0 4522 4805 esm-env: 1.2.2 4523 - runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 4806 + runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 4524 4807 svelte: 5.53.11 4525 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 4808 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 4526 4809 tabbable: 6.4.0 4527 4810 transitivePeerDependencies: 4528 4811 - '@sveltejs/kit' ··· 4847 5130 '@types/estree': 1.0.8 4848 5131 4849 5132 esutils@2.0.3: {} 5133 + 5134 + event-target-polyfill@0.0.4: {} 4850 5135 4851 5136 expect-type@1.3.0: {} 4852 5137 ··· 4887 5172 fsevents@2.3.3: 4888 5173 optional: true 4889 5174 5175 + get-tsconfig@4.14.0: 5176 + dependencies: 5177 + resolve-pkg-maps: 1.0.0 5178 + 4890 5179 gl-matrix@3.4.4: {} 4891 5180 4892 5181 glob-parent@6.0.2: ··· 4906 5195 highlight.js@11.11.1: {} 4907 5196 4908 5197 hls.js@1.6.15: {} 5198 + 5199 + hono@4.12.14: {} 4909 5200 4910 5201 htmlparser2@10.1.0: 4911 5202 dependencies: ··· 5259 5550 dependencies: 5260 5551 entities: 6.0.1 5261 5552 5553 + partysocket@1.1.16(react@19.2.4): 5554 + dependencies: 5555 + event-target-polyfill: 0.0.4 5556 + optionalDependencies: 5557 + react: 19.2.4 5558 + 5262 5559 path-exists@4.0.0: {} 5263 5560 5264 5561 path-key@3.1.1: {} ··· 5346 5643 prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.53.11) 5347 5644 5348 5645 prettier@3.8.1: {} 5646 + 5647 + prettier@3.8.3: {} 5349 5648 5350 5649 prop-types@15.8.1: 5351 5650 dependencies: ··· 5514 5813 5515 5814 resize-observer-polyfill@1.5.1: {} 5516 5815 5816 + resolve-pkg-maps@1.0.0: {} 5817 + 5517 5818 resolve-protobuf-schema@2.1.0: 5518 5819 dependencies: 5519 5820 protocol-buffers-schema: 3.6.0 ··· 5556 5857 esm-env: 1.2.2 5557 5858 svelte: 5.53.11 5558 5859 5559 - runed@0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11): 5860 + runed@0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11): 5560 5861 dependencies: 5561 5862 dequal: 2.0.3 5562 5863 esm-env: 1.2.2 5563 5864 lz-string: 1.5.0 5564 5865 svelte: 5.53.11 5565 5866 optionalDependencies: 5566 - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 5867 + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 5567 5868 5568 5869 rw@1.3.3: {} 5569 5870 ··· 5708 6009 '@tiptap/pm': 3.20.1 5709 6010 svelte: 5.53.11 5710 6011 5711 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11): 6012 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11): 5712 6013 dependencies: 5713 6014 clsx: 2.1.1 5714 - runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)))(svelte@5.53.11) 6015 + runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.53.11)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.53.11) 5715 6016 style-to-object: 1.0.14 5716 6017 svelte: 5.53.11 5717 6018 transitivePeerDependencies: ··· 5819 6120 5820 6121 tslib@2.8.1: {} 5821 6122 6123 + tsx@4.21.0: 6124 + dependencies: 6125 + esbuild: 0.27.3 6126 + get-tsconfig: 4.14.0 6127 + optionalDependencies: 6128 + fsevents: 2.3.3 6129 + 5822 6130 turndown@7.2.2: 5823 6131 dependencies: 5824 6132 '@mixmark-io/domino': 2.2.0 ··· 5828 6136 type-check@0.4.0: 5829 6137 dependencies: 5830 6138 prelude-ls: 1.2.1 6139 + 6140 + type-fest@4.41.0: {} 5831 6141 5832 6142 typescript-eslint@8.57.0(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): 5833 6143 dependencies: ··· 5887 6197 optionalDependencies: 5888 6198 typescript: 5.9.3 5889 6199 5890 - vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1): 6200 + vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): 5891 6201 dependencies: 5892 6202 '@oxc-project/runtime': 0.115.0 5893 6203 lightningcss: 1.32.0 ··· 5900 6210 esbuild: 0.27.3 5901 6211 fsevents: 2.3.3 5902 6212 jiti: 2.6.1 6213 + tsx: 4.21.0 5903 6214 5904 - vitefu@1.1.2(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)): 6215 + vitefu@1.1.2(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): 5905 6216 optionalDependencies: 5906 - vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 6217 + vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 5907 6218 5908 - vitest@4.1.4(@types/node@25.0.10)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)): 6219 + vitest@4.1.4(@types/node@25.0.10)(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): 5909 6220 dependencies: 5910 6221 '@vitest/expect': 4.1.4 5911 - '@vitest/mocker': 4.1.4(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)) 6222 + '@vitest/mocker': 4.1.4(vite@8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) 5912 6223 '@vitest/pretty-format': 4.1.4 5913 6224 '@vitest/runner': 4.1.4 5914 6225 '@vitest/snapshot': 4.1.4 ··· 5925 6236 tinyexec: 1.1.1 5926 6237 tinyglobby: 0.2.15 5927 6238 tinyrainbow: 3.1.0 5928 - vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1) 6239 + vite: 8.0.0(@types/node@25.0.10)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) 5929 6240 why-is-node-running: 2.3.0 5930 6241 optionalDependencies: 5931 6242 '@types/node': 25.0.10 ··· 5994 6305 yaml@1.10.2: {} 5995 6306 5996 6307 yocto-queue@0.1.0: {} 6308 + 6309 + yocto-queue@1.2.2: {} 5997 6310 5998 6311 yoga-wasm-web@0.3.3: {} 5999 6312
+29
scripts/append-scheduled.ts
··· 1 + /** 2 + * Post-build script: appends a `scheduled` handler to the SvelteKit worker output. 3 + * 4 + * SvelteKit's adapter-cloudflare doesn't support the `scheduled` export natively 5 + * (see https://github.com/sveltejs/kit/issues/4841). This script patches the 6 + * generated _worker.js to add one that self-calls the /api/cron endpoint. 7 + */ 8 + import { readFileSync, writeFileSync } from 'fs'; 9 + import { join, dirname } from 'path'; 10 + import { fileURLToPath } from 'url'; 11 + 12 + const root = join(dirname(fileURLToPath(import.meta.url)), '..'); 13 + const workerPath = join(root, '.svelte-kit', 'cloudflare', '_worker.js'); 14 + 15 + let code = readFileSync(workerPath, 'utf-8'); 16 + 17 + code += ` 18 + // --- Appended by scripts/append-scheduled.ts --- 19 + worker_default.scheduled = async function (event, env, ctx) { 20 + const req = new Request('http://localhost/api/cron', { 21 + method: 'POST', 22 + headers: { 'X-Cron-Secret': env.CRON_SECRET || '' } 23 + }); 24 + ctx.waitUntil(this.fetch(req, env, ctx)); 25 + }; 26 + `; 27 + 28 + writeFileSync(workerPath, code); 29 + console.log('Appended scheduled handler to _worker.js');
+14
scripts/generate.ts
··· 1 + import { join, dirname } from 'path'; 2 + import { fileURLToPath } from 'url'; 3 + import { config } from '../src/lib/contrail/config'; 4 + import { generateLexicons } from '@atmo-dev/contrail/generate'; 5 + 6 + const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); 7 + 8 + generateLexicons({ 9 + config, 10 + rootDir: ROOT_DIR, 11 + lexiconDir: join(ROOT_DIR, 'lexicons'), 12 + outputDir: join(ROOT_DIR, 'lexicons-generated'), 13 + writeRuntimeFiles: true 14 + });
+68
scripts/sync.ts
··· 1 + /** 2 + * Discover users from relays and backfill their records from PDS. 3 + * 4 + * Usage: 5 + * pnpm sync # local D1 6 + * pnpm sync:remote # prod D1 7 + */ 8 + import { Contrail } from '@atmo-dev/contrail'; 9 + import { config } from '../src/lib/contrail/config'; 10 + import { getPlatformProxy } from 'wrangler'; 11 + 12 + function elapsed(start: number): string { 13 + const ms = Date.now() - start; 14 + if (ms < 1000) return `${ms}ms`; 15 + if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; 16 + const mins = Math.floor(ms / 60_000); 17 + const secs = ((ms % 60_000) / 1000).toFixed(0); 18 + return `${mins}m ${secs}s`; 19 + } 20 + 21 + async function main() { 22 + const remote = process.argv.includes('--remote'); 23 + const syncStart = Date.now(); 24 + 25 + console.log(`=== Sync (${remote ? 'remote/prod' : 'local'} D1) ===\n`); 26 + 27 + const { env, dispose } = await getPlatformProxy<{ DB: D1Database }>({ 28 + environment: remote ? 'production' : undefined 29 + }); 30 + 31 + const contrail = new Contrail({ ...config, db: env.DB }); 32 + 33 + try { 34 + await contrail.init(); 35 + 36 + console.log('--- Discovery ---'); 37 + const discoveryStart = Date.now(); 38 + const discovered = await contrail.discover(); 39 + console.log(` Done: ${discovered.length} users in ${elapsed(discoveryStart)}\n`); 40 + 41 + console.log('--- Backfill ---'); 42 + const backfillStart = Date.now(); 43 + const total = await contrail.backfill({ 44 + concurrency: 100, 45 + onProgress: ({ records, usersComplete, usersTotal, usersFailed }) => { 46 + const secs = (Date.now() - backfillStart) / 1000; 47 + const rate = secs > 0 ? Math.round(records / secs) : 0; 48 + const failStr = usersFailed > 0 ? ` | ${usersFailed} failed` : ''; 49 + process.stdout.write( 50 + `\r ${records} records | ${usersComplete}/${usersTotal} users | ${rate}/s | ${elapsed(backfillStart)}${failStr} ` 51 + ); 52 + } 53 + }); 54 + process.stdout.write('\n'); 55 + console.log(` Done: ${total} records in ${elapsed(backfillStart)}\n`); 56 + 57 + console.log(`=== Finished in ${elapsed(syncStart)} ===`); 58 + console.log(` Discovered: ${discovered.length} users`); 59 + console.log(` Backfilled: ${total} records`); 60 + } finally { 61 + await dispose(); 62 + } 63 + } 64 + 65 + main().catch((err) => { 66 + console.error(err); 67 + process.exit(1); 68 + });
+8
src/app.css
··· 10 10 @custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *))); 11 11 @custom-variant accent (&:where(.accent, .accent *)); 12 12 13 + /* Hide @foxui/core Avatar fallback (person icon / initials) whenever an image 14 + is rendered, so partially transparent avatars don't show the icon behind them. 15 + Targets the Avatar root by its distinctive class combo. */ 16 + .not-prose.rounded-full.isolate:has(img) > svg, 17 + .not-prose.rounded-full.isolate:has(img) > span { 18 + display: none; 19 + } 20 + 13 21 .body::-webkit-scrollbar-track { 14 22 background-color: transparent; 15 23 }
+16 -2
src/app.d.ts
··· 1 - import { KVNamespace } from '@cloudflare/workers-types'; 1 + import type { KVNamespace, D1Database } from '@cloudflare/workers-types'; 2 + import type { OAuthSession } from '@atcute/oauth-node-client'; 3 + import type { Client } from '@atcute/client'; 4 + import type { Did } from '@atcute/lexicons'; 2 5 3 6 // See https://svelte.dev/docs/kit/types#app.d.ts 4 7 // for information about these interfaces 5 8 declare global { 6 9 namespace App { 7 10 // interface Error {} 8 - // interface Locals {} 11 + interface Locals { 12 + session: OAuthSession | null; 13 + client: Client | null; 14 + did: Did | null; 15 + } 9 16 // interface PageData {} 10 17 // interface PageState {} 11 18 interface Platform { 12 19 env: { 13 20 USER_DATA_CACHE: KVNamespace; 14 21 CUSTOM_DOMAINS: KVNamespace; 22 + OAUTH_SESSIONS: KVNamespace; 23 + OAUTH_STATES: KVNamespace; 24 + DB: D1Database; 25 + CLIENT_ASSERTION_KEY: string; 26 + COOKIE_SECRET: string; 27 + CRON_SECRET: string; 15 28 }; 16 29 } 17 30 } ··· 19 32 20 33 import type {} from '@atcute/atproto'; 21 34 import type {} from '@atcute/bluesky'; 35 + import type {} from './lexicon-types'; 22 36 23 37 export {};
+18
src/hooks.server.ts
··· 1 + import type { Handle } from '@sveltejs/kit'; 2 + import { restoreSession } from '$lib/atproto/server/session'; 3 + 4 + export const handle: Handle = async ({ event, resolve }) => { 5 + const customDomain = event.request.headers.get('X-Custom-Domain')?.toLowerCase() || undefined; 6 + 7 + const { session, client, did } = await restoreSession( 8 + event.cookies, 9 + event.platform?.env, 10 + customDomain 11 + ); 12 + 13 + event.locals.session = session; 14 + event.locals.client = client; 15 + event.locals.did = did; 16 + 17 + return resolve(event); 18 + };
+10
src/lexicon-types/index.ts
··· 1 + export * as AppBlentoCard from './types/app/blento/card.js'; 2 + export * as AppBlentoCardGetRecord from './types/app/blento/card/getRecord.js'; 3 + export * as AppBlentoCardListRecords from './types/app/blento/card/listRecords.js'; 4 + export * as AppBlentoGetCursor from './types/app/blento/getCursor.js'; 5 + export * as AppBlentoGetOverview from './types/app/blento/getOverview.js'; 6 + export * as AppBlentoGetProfile from './types/app/blento/getProfile.js'; 7 + export * as AppBlentoNotifyOfUpdate from './types/app/blento/notifyOfUpdate.js'; 8 + export * as AppBlentoPage from './types/app/blento/page.js'; 9 + export * as AppBlentoPageGetRecord from './types/app/blento/page/getRecord.js'; 10 + export * as AppBlentoPageListRecords from './types/app/blento/page/listRecords.js';
+38
src/lexicon-types/types/app/blento/card.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.record( 6 + /*#__PURE__*/ v.tidString(), 7 + /*#__PURE__*/ v.object({ 8 + $type: /*#__PURE__*/ v.literal('app.blento.card'), 9 + cardData: /*#__PURE__*/ v.unknown(), 10 + cardType: /*#__PURE__*/ v.string(), 11 + color: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 12 + h: /*#__PURE__*/ v.integer(), 13 + mobileH: /*#__PURE__*/ v.integer(), 14 + mobileW: /*#__PURE__*/ v.integer(), 15 + mobileX: /*#__PURE__*/ v.integer(), 16 + mobileY: /*#__PURE__*/ v.integer(), 17 + page: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 18 + updatedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 19 + version: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 20 + w: /*#__PURE__*/ v.integer(), 21 + x: /*#__PURE__*/ v.integer(), 22 + y: /*#__PURE__*/ v.integer() 23 + }) 24 + ); 25 + 26 + type main$schematype = typeof _mainSchema; 27 + 28 + export interface mainSchema extends main$schematype {} 29 + 30 + export const mainSchema = _mainSchema as mainSchema; 31 + 32 + export interface Main extends v.InferInput<typeof mainSchema> {} 33 + 34 + declare module '@atcute/lexicons/ambient' { 35 + interface Records { 36 + 'app.blento.card': mainSchema; 37 + } 38 + }
+66
src/lexicon-types/types/app/blento/card/getRecord.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + import * as AppBlentoCard from '../card.js'; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.card.getRecord', { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Include profile + identity info keyed by DID 10 + */ 11 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 12 + /** 13 + * AT URI of the record 14 + */ 15 + uri: /*#__PURE__*/ v.resourceUriString() 16 + }), 17 + output: { 18 + type: 'lex', 19 + schema: /*#__PURE__*/ v.object({ 20 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 21 + collection: /*#__PURE__*/ v.nsidString(), 22 + did: /*#__PURE__*/ v.didString(), 23 + get profiles() { 24 + return /*#__PURE__*/ v.optional(/*#__PURE__*/ v.array(profileEntrySchema)); 25 + }, 26 + get record() { 27 + return /*#__PURE__*/ v.optional(AppBlentoCard.mainSchema); 28 + }, 29 + rkey: /*#__PURE__*/ v.string(), 30 + time_us: /*#__PURE__*/ v.integer(), 31 + uri: /*#__PURE__*/ v.resourceUriString() 32 + }) 33 + } 34 + }); 35 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 36 + $type: /*#__PURE__*/ v.optional( 37 + /*#__PURE__*/ v.literal('app.blento.card.getRecord#profileEntry') 38 + ), 39 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 40 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 41 + did: /*#__PURE__*/ v.didString(), 42 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 43 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 44 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 45 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()) 46 + }); 47 + 48 + type main$schematype = typeof _mainSchema; 49 + type profileEntry$schematype = typeof _profileEntrySchema; 50 + 51 + export interface mainSchema extends main$schematype {} 52 + export interface profileEntrySchema extends profileEntry$schematype {} 53 + 54 + export const mainSchema = _mainSchema as mainSchema; 55 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 56 + 57 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 58 + 59 + export interface $params extends v.InferInput<mainSchema['params']> {} 60 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 61 + 62 + declare module '@atcute/lexicons/ambient' { 63 + interface XRPCQueries { 64 + 'app.blento.card.getRecord': mainSchema; 65 + } 66 + }
+204
src/lexicon-types/types/app/blento/card/listRecords.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + import * as AppBlentoCard from '../card.js'; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.card.listRecords', { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Filter by DID or handle (triggers on-demand backfill) 10 + */ 11 + actor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.actorIdentifierString()), 12 + /** 13 + * Filter by cardType 14 + */ 15 + cardType: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 16 + /** 17 + * Filter by color 18 + */ 19 + color: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 20 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 21 + /** 22 + * Maximum value for h 23 + */ 24 + hMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 25 + /** 26 + * Minimum value for h 27 + */ 28 + hMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 29 + /** 30 + * @minimum 1 31 + * @maximum 200 32 + * @default 50 33 + */ 34 + limit: /*#__PURE__*/ v.optional( 35 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [/*#__PURE__*/ v.integerRange(1, 200)]), 36 + 50 37 + ), 38 + /** 39 + * Maximum value for mobileH 40 + */ 41 + mobileHMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 42 + /** 43 + * Minimum value for mobileH 44 + */ 45 + mobileHMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 46 + /** 47 + * Maximum value for mobileW 48 + */ 49 + mobileWMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 50 + /** 51 + * Minimum value for mobileW 52 + */ 53 + mobileWMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 54 + /** 55 + * Maximum value for mobileX 56 + */ 57 + mobileXMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 58 + /** 59 + * Minimum value for mobileX 60 + */ 61 + mobileXMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 62 + /** 63 + * Maximum value for mobileY 64 + */ 65 + mobileYMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 66 + /** 67 + * Minimum value for mobileY 68 + */ 69 + mobileYMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 70 + /** 71 + * Sort direction (default: desc for dates/numbers/counts, asc for strings) 72 + */ 73 + order: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string<'asc' | 'desc' | (string & {})>()), 74 + /** 75 + * Filter by page 76 + */ 77 + page: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 78 + /** 79 + * Include profile + identity info keyed by DID 80 + */ 81 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 82 + /** 83 + * Field to sort by (default: time_us) 84 + */ 85 + sort: /*#__PURE__*/ v.optional( 86 + /*#__PURE__*/ v.string< 87 + | 'cardType' 88 + | 'color' 89 + | 'h' 90 + | 'mobileH' 91 + | 'mobileW' 92 + | 'mobileX' 93 + | 'mobileY' 94 + | 'page' 95 + | 'updatedAt' 96 + | 'version' 97 + | 'w' 98 + | 'x' 99 + | 'y' 100 + | (string & {}) 101 + >() 102 + ), 103 + /** 104 + * Maximum value for updatedAt 105 + */ 106 + updatedAtMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 107 + /** 108 + * Minimum value for updatedAt 109 + */ 110 + updatedAtMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 111 + /** 112 + * Maximum value for version 113 + */ 114 + versionMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 115 + /** 116 + * Minimum value for version 117 + */ 118 + versionMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 119 + /** 120 + * Maximum value for w 121 + */ 122 + wMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 123 + /** 124 + * Minimum value for w 125 + */ 126 + wMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 127 + /** 128 + * Maximum value for x 129 + */ 130 + xMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 131 + /** 132 + * Minimum value for x 133 + */ 134 + xMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 135 + /** 136 + * Maximum value for y 137 + */ 138 + yMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 139 + /** 140 + * Minimum value for y 141 + */ 142 + yMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()) 143 + }), 144 + output: { 145 + type: 'lex', 146 + schema: /*#__PURE__*/ v.object({ 147 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 148 + get profiles() { 149 + return /*#__PURE__*/ v.optional(/*#__PURE__*/ v.array(profileEntrySchema)); 150 + }, 151 + get records() { 152 + return /*#__PURE__*/ v.array(recordSchema); 153 + } 154 + }) 155 + } 156 + }); 157 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 158 + $type: /*#__PURE__*/ v.optional( 159 + /*#__PURE__*/ v.literal('app.blento.card.listRecords#profileEntry') 160 + ), 161 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 162 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 163 + did: /*#__PURE__*/ v.didString(), 164 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 165 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 166 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 167 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()) 168 + }); 169 + const _recordSchema = /*#__PURE__*/ v.object({ 170 + $type: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.literal('app.blento.card.listRecords#record')), 171 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 172 + collection: /*#__PURE__*/ v.nsidString(), 173 + did: /*#__PURE__*/ v.didString(), 174 + get record() { 175 + return /*#__PURE__*/ v.optional(AppBlentoCard.mainSchema); 176 + }, 177 + rkey: /*#__PURE__*/ v.string(), 178 + time_us: /*#__PURE__*/ v.integer(), 179 + uri: /*#__PURE__*/ v.resourceUriString() 180 + }); 181 + 182 + type main$schematype = typeof _mainSchema; 183 + type profileEntry$schematype = typeof _profileEntrySchema; 184 + type record$schematype = typeof _recordSchema; 185 + 186 + export interface mainSchema extends main$schematype {} 187 + export interface profileEntrySchema extends profileEntry$schematype {} 188 + export interface recordSchema extends record$schematype {} 189 + 190 + export const mainSchema = _mainSchema as mainSchema; 191 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 192 + export const recordSchema = _recordSchema as recordSchema; 193 + 194 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 195 + export interface Record extends v.InferInput<typeof recordSchema> {} 196 + 197 + export interface $params extends v.InferInput<mainSchema['params']> {} 198 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 199 + 200 + declare module '@atcute/lexicons/ambient' { 201 + interface XRPCQueries { 202 + 'app.blento.card.listRecords': mainSchema; 203 + } 204 + }
+30
src/lexicon-types/types/app/blento/getCursor.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.getCursor', { 6 + params: null, 7 + output: { 8 + type: 'lex', 9 + schema: /*#__PURE__*/ v.object({ 10 + date: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 11 + seconds_ago: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 12 + time_us: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()) 13 + }) 14 + } 15 + }); 16 + 17 + type main$schematype = typeof _mainSchema; 18 + 19 + export interface mainSchema extends main$schematype {} 20 + 21 + export const mainSchema = _mainSchema as mainSchema; 22 + 23 + export interface $params {} 24 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 25 + 26 + declare module '@atcute/lexicons/ambient' { 27 + interface XRPCQueries { 28 + 'app.blento.getCursor': mainSchema; 29 + } 30 + }
+44
src/lexicon-types/types/app/blento/getOverview.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + 5 + const _collectionStatsSchema = /*#__PURE__*/ v.object({ 6 + $type: /*#__PURE__*/ v.optional( 7 + /*#__PURE__*/ v.literal('app.blento.getOverview#collectionStats') 8 + ), 9 + collection: /*#__PURE__*/ v.string(), 10 + records: /*#__PURE__*/ v.integer(), 11 + unique_users: /*#__PURE__*/ v.integer() 12 + }); 13 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.getOverview', { 14 + params: null, 15 + output: { 16 + type: 'lex', 17 + schema: /*#__PURE__*/ v.object({ 18 + get collections() { 19 + return /*#__PURE__*/ v.array(collectionStatsSchema); 20 + }, 21 + total_records: /*#__PURE__*/ v.integer() 22 + }) 23 + } 24 + }); 25 + 26 + type collectionStats$schematype = typeof _collectionStatsSchema; 27 + type main$schematype = typeof _mainSchema; 28 + 29 + export interface collectionStatsSchema extends collectionStats$schematype {} 30 + export interface mainSchema extends main$schematype {} 31 + 32 + export const collectionStatsSchema = _collectionStatsSchema as collectionStatsSchema; 33 + export const mainSchema = _mainSchema as mainSchema; 34 + 35 + export interface CollectionStats extends v.InferInput<typeof collectionStatsSchema> {} 36 + 37 + export interface $params {} 38 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 39 + 40 + declare module '@atcute/lexicons/ambient' { 41 + interface XRPCQueries { 42 + 'app.blento.getOverview': mainSchema; 43 + } 44 + }
+50
src/lexicon-types/types/app/blento/getProfile.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.getProfile', { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * DID or handle of the user 9 + */ 10 + actor: /*#__PURE__*/ v.actorIdentifierString() 11 + }), 12 + output: { 13 + type: 'lex', 14 + schema: /*#__PURE__*/ v.object({ 15 + get profiles() { 16 + return /*#__PURE__*/ v.array(profileEntrySchema); 17 + } 18 + }) 19 + } 20 + }); 21 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 22 + $type: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.literal('app.blento.getProfile#profileEntry')), 23 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 24 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 25 + did: /*#__PURE__*/ v.didString(), 26 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 27 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 28 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 29 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()) 30 + }); 31 + 32 + type main$schematype = typeof _mainSchema; 33 + type profileEntry$schematype = typeof _profileEntrySchema; 34 + 35 + export interface mainSchema extends main$schematype {} 36 + export interface profileEntrySchema extends profileEntry$schematype {} 37 + 38 + export const mainSchema = _mainSchema as mainSchema; 39 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 40 + 41 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 42 + 43 + export interface $params extends v.InferInput<mainSchema['params']> {} 44 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 45 + 46 + declare module '@atcute/lexicons/ambient' { 47 + interface XRPCQueries { 48 + 'app.blento.getProfile': mainSchema; 49 + } 50 + }
+58
src/lexicon-types/types/app/blento/notifyOfUpdate.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure('app.blento.notifyOfUpdate', { 6 + params: null, 7 + input: { 8 + type: 'lex', 9 + schema: /*#__PURE__*/ v.object({ 10 + /** 11 + * Single AT URI to fetch and index 12 + */ 13 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 14 + /** 15 + * Batch of AT URIs to fetch and index (max 25) 16 + * @maxLength 25 17 + */ 18 + uris: /*#__PURE__*/ v.optional( 19 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), [ 20 + /*#__PURE__*/ v.arrayLength(0, 25) 21 + ]) 22 + ) 23 + }) 24 + }, 25 + output: { 26 + type: 'lex', 27 + schema: /*#__PURE__*/ v.object({ 28 + /** 29 + * Number of records deleted (not found on PDS) 30 + */ 31 + deleted: /*#__PURE__*/ v.integer(), 32 + /** 33 + * Errors for individual URIs that could not be processed 34 + */ 35 + errors: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.array(/*#__PURE__*/ v.string())), 36 + /** 37 + * Number of records created or updated 38 + */ 39 + indexed: /*#__PURE__*/ v.integer() 40 + }) 41 + } 42 + }); 43 + 44 + type main$schematype = typeof _mainSchema; 45 + 46 + export interface mainSchema extends main$schematype {} 47 + 48 + export const mainSchema = _mainSchema as mainSchema; 49 + 50 + export interface $params {} 51 + export interface $input extends v.InferXRPCBodyInput<mainSchema['input']> {} 52 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 53 + 54 + declare module '@atcute/lexicons/ambient' { 55 + interface XRPCProcedures { 56 + 'app.blento.notifyOfUpdate': mainSchema; 57 + } 58 + }
+47
src/lexicon-types/types/app/blento/page.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.record( 6 + /*#__PURE__*/ v.string(), 7 + /*#__PURE__*/ v.object({ 8 + $type: /*#__PURE__*/ v.literal('app.blento.page'), 9 + description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 10 + /** 11 + * @accept image/* 12 + */ 13 + icon: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()), 14 + name: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 15 + get preferences() { 16 + return /*#__PURE__*/ v.optional(preferencesSchema); 17 + }, 18 + url: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()) 19 + }) 20 + ); 21 + const _preferencesSchema = /*#__PURE__*/ v.object({ 22 + $type: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.literal('app.blento.page#preferences')), 23 + accentColor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 24 + baseColor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 25 + editedOn: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 26 + hideProfile: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 27 + hideProfileSection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 28 + profilePosition: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()) 29 + }); 30 + 31 + type main$schematype = typeof _mainSchema; 32 + type preferences$schematype = typeof _preferencesSchema; 33 + 34 + export interface mainSchema extends main$schematype {} 35 + export interface preferencesSchema extends preferences$schematype {} 36 + 37 + export const mainSchema = _mainSchema as mainSchema; 38 + export const preferencesSchema = _preferencesSchema as preferencesSchema; 39 + 40 + export interface Main extends v.InferInput<typeof mainSchema> {} 41 + export interface Preferences extends v.InferInput<typeof preferencesSchema> {} 42 + 43 + declare module '@atcute/lexicons/ambient' { 44 + interface Records { 45 + 'app.blento.page': mainSchema; 46 + } 47 + }
+66
src/lexicon-types/types/app/blento/page/getRecord.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + import * as AppBlentoPage from '../page.js'; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.page.getRecord', { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Include profile + identity info keyed by DID 10 + */ 11 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 12 + /** 13 + * AT URI of the record 14 + */ 15 + uri: /*#__PURE__*/ v.resourceUriString() 16 + }), 17 + output: { 18 + type: 'lex', 19 + schema: /*#__PURE__*/ v.object({ 20 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 21 + collection: /*#__PURE__*/ v.nsidString(), 22 + did: /*#__PURE__*/ v.didString(), 23 + get profiles() { 24 + return /*#__PURE__*/ v.optional(/*#__PURE__*/ v.array(profileEntrySchema)); 25 + }, 26 + get record() { 27 + return /*#__PURE__*/ v.optional(AppBlentoPage.mainSchema); 28 + }, 29 + rkey: /*#__PURE__*/ v.string(), 30 + time_us: /*#__PURE__*/ v.integer(), 31 + uri: /*#__PURE__*/ v.resourceUriString() 32 + }) 33 + } 34 + }); 35 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 36 + $type: /*#__PURE__*/ v.optional( 37 + /*#__PURE__*/ v.literal('app.blento.page.getRecord#profileEntry') 38 + ), 39 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 40 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 41 + did: /*#__PURE__*/ v.didString(), 42 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 43 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 44 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 45 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()) 46 + }); 47 + 48 + type main$schematype = typeof _mainSchema; 49 + type profileEntry$schematype = typeof _profileEntrySchema; 50 + 51 + export interface mainSchema extends main$schematype {} 52 + export interface profileEntrySchema extends profileEntry$schematype {} 53 + 54 + export const mainSchema = _mainSchema as mainSchema; 55 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 56 + 57 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 58 + 59 + export interface $params extends v.InferInput<mainSchema['params']> {} 60 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 61 + 62 + declare module '@atcute/lexicons/ambient' { 63 + interface XRPCQueries { 64 + 'app.blento.page.getRecord': mainSchema; 65 + } 66 + }
+103
src/lexicon-types/types/app/blento/page/listRecords.ts
··· 1 + import type {} from '@atcute/lexicons'; 2 + import * as v from '@atcute/lexicons/validations'; 3 + import type {} from '@atcute/lexicons/ambient'; 4 + import * as AppBlentoPage from '../page.js'; 5 + 6 + const _mainSchema = /*#__PURE__*/ v.query('app.blento.page.listRecords', { 7 + params: /*#__PURE__*/ v.object({ 8 + /** 9 + * Filter by DID or handle (triggers on-demand backfill) 10 + */ 11 + actor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.actorIdentifierString()), 12 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 13 + /** 14 + * Filter by description 15 + */ 16 + description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 17 + /** 18 + * @minimum 1 19 + * @maximum 200 20 + * @default 50 21 + */ 22 + limit: /*#__PURE__*/ v.optional( 23 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [/*#__PURE__*/ v.integerRange(1, 200)]), 24 + 50 25 + ), 26 + /** 27 + * Filter by name 28 + */ 29 + name: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 30 + /** 31 + * Sort direction (default: desc for dates/numbers/counts, asc for strings) 32 + */ 33 + order: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string<'asc' | 'desc' | (string & {})>()), 34 + /** 35 + * Include profile + identity info keyed by DID 36 + */ 37 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 38 + /** 39 + * Field to sort by (default: time_us) 40 + */ 41 + sort: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string<'description' | 'name' | (string & {})>()) 42 + }), 43 + output: { 44 + type: 'lex', 45 + schema: /*#__PURE__*/ v.object({ 46 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 47 + get profiles() { 48 + return /*#__PURE__*/ v.optional(/*#__PURE__*/ v.array(profileEntrySchema)); 49 + }, 50 + get records() { 51 + return /*#__PURE__*/ v.array(recordSchema); 52 + } 53 + }) 54 + } 55 + }); 56 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 57 + $type: /*#__PURE__*/ v.optional( 58 + /*#__PURE__*/ v.literal('app.blento.page.listRecords#profileEntry') 59 + ), 60 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 61 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 62 + did: /*#__PURE__*/ v.didString(), 63 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 64 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 65 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 66 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()) 67 + }); 68 + const _recordSchema = /*#__PURE__*/ v.object({ 69 + $type: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.literal('app.blento.page.listRecords#record')), 70 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 71 + collection: /*#__PURE__*/ v.nsidString(), 72 + did: /*#__PURE__*/ v.didString(), 73 + get record() { 74 + return /*#__PURE__*/ v.optional(AppBlentoPage.mainSchema); 75 + }, 76 + rkey: /*#__PURE__*/ v.string(), 77 + time_us: /*#__PURE__*/ v.integer(), 78 + uri: /*#__PURE__*/ v.resourceUriString() 79 + }); 80 + 81 + type main$schematype = typeof _mainSchema; 82 + type profileEntry$schematype = typeof _profileEntrySchema; 83 + type record$schematype = typeof _recordSchema; 84 + 85 + export interface mainSchema extends main$schematype {} 86 + export interface profileEntrySchema extends profileEntry$schematype {} 87 + export interface recordSchema extends record$schematype {} 88 + 89 + export const mainSchema = _mainSchema as mainSchema; 90 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 91 + export const recordSchema = _recordSchema as recordSchema; 92 + 93 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 94 + export interface Record extends v.InferInput<typeof recordSchema> {} 95 + 96 + export interface $params extends v.InferInput<mainSchema['params']> {} 97 + export interface $output extends v.InferXRPCBodyInput<mainSchema['output']> {} 98 + 99 + declare module '@atcute/lexicons/ambient' { 100 + interface XRPCQueries { 101 + 'app.blento.page.listRecords': mainSchema; 102 + } 103 + }
+67 -224
src/lib/atproto/auth.svelte.ts
··· 1 - import { 2 - configureOAuth, 3 - createAuthorizationUrl, 4 - finalizeAuthorization, 5 - OAuthUserAgent, 6 - getSession, 7 - deleteStoredSession 8 - } from '@atcute/oauth-browser-client'; 9 - import { AppBskyActorDefs } from '@atcute/bluesky'; 10 - import { 11 - CompositeDidDocumentResolver, 12 - CompositeHandleResolver, 13 - DohJsonHandleResolver, 14 - LocalActorResolver, 15 - PlcDidDocumentResolver, 16 - WebDidDocumentResolver, 17 - WellKnownHandleResolver 18 - } from '@atcute/identity-resolver'; 19 - import { Client } from '@atcute/client'; 20 - 21 - import { dev } from '$app/environment'; 22 - import { replaceState } from '$app/navigation'; 23 - 24 - import { metadata } from './metadata'; 25 - import { describeRepo, getDetailedProfile } from './methods'; 26 - import { DOH_RESOLVER, REDIRECT_PATH, signUpPDS } from './settings'; 27 - import { SvelteURLSearchParams } from 'svelte/reactivity'; 28 - 1 + import { type AppBskyActorDefs } from '@atcute/bluesky'; 29 2 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 3 + import { page } from '$app/state'; 4 + import { saveRecentLogin } from '@foxui/social'; 30 5 31 - export const user = $state({ 32 - agent: null as OAuthUserAgent | null, 33 - client: null as Client | null, 34 - profile: null as AppBskyActorDefs.ProfileViewDetailed | null | undefined, 35 - isInitializing: true, 36 - isLoggedIn: false, 37 - did: undefined as Did | undefined 38 - }); 6 + let cachedProfile = $state<AppBskyActorDefs.ProfileViewDetailed | null>(null); 7 + let cachedDid = $state<Did | null>(null); 39 8 40 - export async function initClient(options?: { customDomain?: string }) { 41 - user.isInitializing = true; 9 + function rememberProfile(profile: AppBskyActorDefs.ProfileViewDetailed) { 10 + cachedProfile = profile; 11 + saveRecentLogin(profile); 12 + } 42 13 43 - let client_id = dev 44 - ? `http://localhost` + 45 - `?redirect_uri=${encodeURIComponent('http://127.0.0.1:5179' + REDIRECT_PATH)}` + 46 - `&scope=${encodeURIComponent(metadata.scope)}` 47 - : metadata.client_id; 14 + // Load profile client-side when authDid changes 15 + $effect.root(() => { 16 + $effect(() => { 17 + const did = page.data?.authDid as Did | undefined; 48 18 49 - const handleResolver = new CompositeHandleResolver({ 50 - methods: { 51 - dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), 52 - http: new WellKnownHandleResolver() 19 + if (!did) { 20 + cachedProfile = null; 21 + cachedDid = null; 22 + return; 53 23 } 54 - }); 55 24 56 - let redirect_uri = dev ? 'http://127.0.0.1:5179' + REDIRECT_PATH : metadata.redirect_uris[0]; 25 + if (did === cachedDid && cachedProfile) return; 57 26 58 - if (options?.customDomain) { 59 - client_id = client_id.replace('blento.app', options.customDomain); 60 - redirect_uri = redirect_uri.replace('blento.app', options.customDomain); 27 + cachedDid = did; 61 28 62 - console.log(client_id, redirect_uri); 63 - } else { 64 - console.log('no custom domain'); 65 - } 29 + // If the current page already has this user's profile (e.g. viewing own page), use it 30 + if (page.data?.profile?.did === did) { 31 + rememberProfile(page.data.profile); 32 + return; 33 + } 66 34 67 - configureOAuth({ 68 - metadata: { 69 - client_id, 70 - redirect_uri 71 - }, 72 - identityResolver: new LocalActorResolver({ 73 - handleResolver: handleResolver, 74 - didDocumentResolver: new CompositeDidDocumentResolver({ 75 - methods: { 76 - plc: new PlcDidDocumentResolver(), 77 - web: new WebDidDocumentResolver() 78 - } 79 - }) 80 - }) 35 + // Otherwise fetch it client-side 36 + import('@atcute/client').then(({ Client, simpleFetchHandler }) => { 37 + const client = new Client({ 38 + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) 39 + }); 40 + client 41 + .get('app.bsky.actor.getProfile', { params: { actor: did } }) 42 + .then((res) => { 43 + if (res.ok && cachedDid === did) { 44 + rememberProfile(res.data); 45 + } 46 + }) 47 + .catch(() => {}); 48 + }); 81 49 }); 50 + }); 82 51 83 - const params = new SvelteURLSearchParams(location.hash.slice(1)); 84 - 85 - const did = (localStorage.getItem('current-login') as Did) ?? undefined; 86 - 87 - if (params.size > 0) { 88 - await finalizeLogin(params, did); 89 - } else if (did) { 90 - await resumeSession(did); 52 + export const user: { 53 + profile: AppBskyActorDefs.ProfileViewDetailed | null | undefined; 54 + isLoggedIn: boolean; 55 + did: Did | undefined; 56 + } = { 57 + get profile() { 58 + return cachedProfile; 59 + }, 60 + get isLoggedIn() { 61 + return !!page.data?.authDid; 62 + }, 63 + get did() { 64 + return page.data?.authDid ?? undefined; 91 65 } 66 + }; 92 67 93 - user.isInitializing = false; 94 - } 68 + export async function login(handle: ActorIdentifier) { 69 + const { oauthLogin } = await import('./server/oauth.remote'); 95 70 96 - export async function login(handle: ActorIdentifier) { 97 - console.log('login in with', handle); 98 71 if (handle.startsWith('did:')) { 99 72 if (handle.length < 6) throw new Error('DID must be at least 6 characters'); 100 - 101 - await startAuthorization(handle as ActorIdentifier); 102 73 } else if (handle.includes('.') && handle.length > 3) { 103 - const processed = handle.startsWith('@') ? handle.slice(1) : handle; 104 - if (processed.length < 4) throw new Error('Handle must be at least 4 characters'); 105 - 106 - await startAuthorization(processed as ActorIdentifier); 74 + handle = (handle.startsWith('@') ? handle.slice(1) : handle) as ActorIdentifier; 107 75 } else if (handle.length > 3) { 108 - const processed = (handle.startsWith('@') ? handle.slice(1) : handle) + '.bsky.social'; 109 - await startAuthorization(processed as ActorIdentifier); 76 + handle = ((handle.startsWith('@') ? handle.slice(1) : handle) + 77 + '.bsky.social') as ActorIdentifier; 110 78 } else { 111 79 throw new Error('Please provide a valid handle or DID.'); 112 80 } 81 + 82 + const { url } = await oauthLogin({ handle }); 83 + window.location.assign(url); 113 84 } 114 85 115 86 export async function signup() { 116 - await startAuthorization(); 117 - } 87 + const { oauthLogin } = await import('./server/oauth.remote'); 118 88 119 - async function startAuthorization(identity?: ActorIdentifier) { 120 - const authUrl = await createAuthorizationUrl({ 121 - target: identity 122 - ? { type: 'account', identifier: identity } 123 - : { type: 'pds', serviceUrl: signUpPDS }, 124 - // @ts-expect-error - new stuff 125 - prompt: identity ? undefined : 'create', 126 - scope: metadata.scope 127 - }); 128 - 129 - localStorage.setItem('login-redirect', location.pathname + location.search); 130 - 131 - // let browser persist local storage 132 - await new Promise((resolve) => setTimeout(resolve, 200)); 133 - 134 - window.location.assign(authUrl); 135 - 136 - await new Promise((_resolve, reject) => { 137 - const listener = () => { 138 - reject(new Error(`user aborted the login request`)); 139 - }; 140 - 141 - window.addEventListener('pageshow', listener, { once: true }); 142 - }); 89 + const { url } = await oauthLogin({ signup: true }); 90 + window.location.assign(url); 143 91 } 144 92 145 93 export async function logout() { 146 - const currentAgent = user.agent; 147 - if (currentAgent) { 148 - const did = currentAgent.session.info.sub; 149 - 150 - localStorage.removeItem('current-login'); 151 - localStorage.removeItem(`profile-${did}`); 152 - 153 - try { 154 - await currentAgent.signOut(); 155 - } catch { 156 - deleteStoredSession(did); 157 - } 158 - 159 - user.agent = null; 160 - user.profile = null; 161 - user.isLoggedIn = false; 162 - } else { 163 - console.error('trying to logout, but user not signed in'); 164 - return false; 165 - } 166 - } 167 - 168 - async function finalizeLogin(params: SvelteURLSearchParams, did?: Did) { 169 - try { 170 - const { session } = await finalizeAuthorization(params); 171 - replaceState(location.pathname + location.search, {}); 172 - 173 - user.agent = new OAuthUserAgent(session); 174 - user.did = session.info.sub; 175 - user.client = new Client({ handler: user.agent }); 176 - 177 - localStorage.setItem('current-login', session.info.sub); 178 - 179 - await loadProfile(session.info.sub); 180 - 181 - user.isLoggedIn = true; 182 - 183 - try { 184 - if (!user.profile) return; 185 - const recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 186 - 187 - recentLogins[session.info.sub] = user.profile; 188 - 189 - localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 190 - } catch { 191 - console.log('failed to save to recent logins'); 192 - } 193 - } catch (error) { 194 - console.error('error finalizing login', error); 195 - if (did) { 196 - await resumeSession(did); 197 - } 198 - } 199 - } 200 - 201 - async function resumeSession(did: Did) { 202 - try { 203 - const session = await getSession(did); 204 - 205 - if (session.token.expires_at && session.token.expires_at < Date.now()) { 206 - throw Error('session expired'); 207 - } 208 - 209 - const requestedScopes = metadata.scope.split(' ').filter((s) => !s.startsWith('include:')); 210 - const tokenScopes = new Set(session.token.scope?.split(' ')); 211 - if (!requestedScopes.every((s) => tokenScopes.has(s))) { 212 - throw Error('scope changed, signing out!'); 213 - } 214 - 215 - user.agent = new OAuthUserAgent(session); 216 - user.did = session.info.sub; 217 - user.client = new Client({ handler: user.agent }); 218 - 219 - await loadProfile(session.info.sub); 220 - 221 - user.isLoggedIn = true; 222 - } catch (error) { 223 - console.error('error resuming session', error); 224 - deleteStoredSession(did); 225 - } 226 - } 227 - 228 - async function loadProfile(actor: Did) { 229 - // check if profile is already loaded in local storage 230 - const profile = localStorage.getItem(`profile-${actor}`); 231 - if (profile) { 232 - try { 233 - user.profile = JSON.parse(profile); 234 - return; 235 - } catch { 236 - console.error('error loading profile from local storage'); 237 - } 238 - } 239 - 240 - const response = await getDetailedProfile(); 241 - 242 - if (!response || response.handle === 'handle.invalid') { 243 - console.log('invalid handle or no profile from bsky, fetching from repo description'); 244 - const repo = await describeRepo({ did: actor }); 245 - user.profile = { 246 - did: actor, 247 - handle: repo?.handle || 'handle.invalid' 248 - }; 249 - localStorage.setItem(`profile-${actor}`, JSON.stringify(user.profile)); 250 - } else { 251 - user.profile = response; 252 - localStorage.setItem(`profile-${actor}`, JSON.stringify(response)); 253 - } 94 + const { oauthLogout } = await import('./server/oauth.remote'); 95 + await oauthLogout(); 96 + window.location.href = '/'; 254 97 }
+1 -2
src/lib/atproto/index.ts
··· 1 - export { user, login, signup, logout, initClient } from './auth.svelte'; 2 - export { metadata } from './metadata'; 1 + export { user, login, signup, logout } from './auth.svelte'; 3 2 4 3 export { 5 4 parseUri,
-44
src/lib/atproto/metadata.ts
··· 1 - import { resolve } from '$app/paths'; 2 - import { permissions, REDIRECT_PATH, SITE } from './settings'; 3 - 4 - function constructScope() { 5 - const repos = permissions.collections.map((collection) => 'repo:' + collection).join(' '); 6 - 7 - let rpcs = ''; 8 - for (const [key, value] of Object.entries(permissions.rpc ?? {})) { 9 - if (Array.isArray(value)) { 10 - rpcs += value.map((lxm) => 'rpc?lxm=' + lxm + '&aud=' + key).join(' '); 11 - } else { 12 - rpcs += 'rpc?lxm=' + value + '&aud=' + key; 13 - } 14 - } 15 - 16 - let blobScope: string | undefined = undefined; 17 - if (Array.isArray(permissions.blobs) && permissions.blobs.length > 0) { 18 - blobScope = 'blob?' + permissions.blobs.map((b) => 'accept=' + b).join('&'); 19 - } else if (permissions.blobs && permissions.blobs.length > 0) { 20 - blobScope = 'blob:' + permissions.blobs; 21 - } 22 - 23 - const scope = [ 24 - 'atproto', 25 - repos, 26 - rpcs, 27 - blobScope, 28 - 'include:app.bsky.authCreatePosts include:site.standard.authFull' 29 - ] 30 - .filter((v) => v?.trim()) 31 - .join(' '); 32 - return scope; 33 - } 34 - 35 - export const metadata = { 36 - client_id: SITE + resolve('/oauth-client-metadata.json'), 37 - redirect_uris: [SITE + resolve(REDIRECT_PATH)], 38 - scope: constructScope(), 39 - grant_types: ['authorization_code', 'refresh_token'], 40 - response_types: ['code'], 41 - token_endpoint_auth_method: 'none', 42 - application_type: 'web', 43 - dpop_bound_access_tokens: true 44 - };
+15 -124
src/lib/atproto/methods.ts
··· 58 58 59 59 /** 60 60 * Gets the PDS (Personal Data Server) URL for a given DID. 61 - * @param did - The DID to look up 62 - * @returns The PDS service endpoint URL 63 - * @throws If no PDS is found in the DID document 64 61 */ 65 62 export async function getPDS(did: Did) { 66 63 const doc = await didResolver.resolve(did as Did<'plc'> | Did<'web'>); ··· 74 71 75 72 /** 76 73 * Fetches a detailed Bluesky profile for a user. 77 - * @param data - Optional object with did and client 78 - * @param data.did - The DID to fetch the profile for (defaults to current user) 79 - * @param data.client - The client to use (defaults to public Bluesky API) 80 - * @returns The profile data or undefined if not found 81 74 */ 82 75 export async function getDetailedProfile(data?: { did?: Did; client?: Client }) { 83 76 data ??= {}; ··· 109 102 > { 110 103 let blentoProfile; 111 104 try { 112 - // try getting blento profile first 113 105 blentoProfile = await getRecord({ 114 106 collection: 'site.standard.publication', 115 107 did: data?.did, ··· 117 109 client: data?.client 118 110 }); 119 111 } catch { 120 - console.error('error getting blento profile, falling back to bsky profile'); 112 + // User doesn't have a blento publication — expected for most users 121 113 } 122 114 123 115 let response; ··· 143 135 144 136 /** 145 137 * Creates an AT Protocol client for a user's PDS. 146 - * @param did - The DID of the user 147 - * @returns A client configured for the user's PDS 148 - * @throws If the PDS cannot be found 149 138 */ 150 139 export async function getClient({ did }: { did: Did }) { 151 140 const pds = await getPDS(did); ··· 160 149 161 150 /** 162 151 * Lists records from a repository collection with pagination support. 163 - * @param did - The DID of the repository (defaults to current user) 164 - * @param collection - The collection to list records from 165 - * @param cursor - Pagination cursor for continuing from a previous request 166 - * @param limit - Maximum number of records to return (default 100, set to 0 for all records) 167 - * @param client - The client to use (defaults to user's PDS client) 168 - * @returns An array of records from the collection 169 152 */ 170 153 export async function listRecords({ 171 154 did, ··· 216 199 217 200 /** 218 201 * Fetches a single record from a repository. 219 - * @param did - The DID of the repository (defaults to current user) 220 - * @param collection - The collection the record belongs to 221 - * @param rkey - The record key (defaults to "self") 222 - * @param client - The client to use (defaults to user's PDS client) 223 - * @returns The record data 224 202 */ 225 203 export async function getRecord({ 226 204 did, ··· 259 237 } 260 238 261 239 /** 262 - * Creates or updates a record in the current user's repository. 263 - * Only accepts collections that are configured in permissions. 264 - * @param collection - The collection to write to (must be in permissions.collections) 265 - * @param rkey - The record key (defaults to "self") 266 - * @param record - The record data to write 267 - * @returns The response from the PDS 268 - * @throws If the user is not logged in 240 + * Creates or updates a record in the current user's repository via server-side proxy. 269 241 */ 270 242 export async function putRecord({ 271 243 collection, ··· 276 248 rkey?: string; 277 249 record: Record<string, unknown>; 278 250 }) { 279 - if (!user.client || !user.did) throw new Error('No rpc or did'); 280 - 281 - const response = await user.client.post('com.atproto.repo.putRecord', { 282 - input: { 283 - collection, 284 - repo: user.did, 285 - rkey, 286 - record: { 287 - ...record 288 - } 289 - } 290 - }); 291 - 292 - return response; 251 + const { putRecord: serverPutRecord } = await import('./server/repo.remote'); 252 + return serverPutRecord({ collection, rkey, record }); 293 253 } 294 254 295 255 /** 296 - * Deletes a record from the current user's repository. 297 - * Only accepts collections that are configured in permissions. 298 - * @param collection - The collection the record belongs to (must be in permissions.collections) 299 - * @param rkey - The record key (defaults to "self") 300 - * @returns True if the deletion was successful 301 - * @throws If the user is not logged in 256 + * Deletes a record from the current user's repository via server-side proxy. 302 257 */ 303 258 export async function deleteRecord({ 304 259 collection, ··· 307 262 collection: AllowedCollection; 308 263 rkey: string; 309 264 }) { 310 - if (!user.client || !user.did) throw new Error('No profile or rpc or did'); 311 - 312 - const response = await user.client.post('com.atproto.repo.deleteRecord', { 313 - input: { 314 - collection, 315 - repo: user.did, 316 - rkey 317 - } 318 - }); 319 - 320 - return response.ok; 265 + const { deleteRecord: serverDeleteRecord } = await import('./server/repo.remote'); 266 + const result = await serverDeleteRecord({ collection, rkey }); 267 + return result.ok; 321 268 } 322 269 323 270 /** 324 - * Uploads a blob to the current user's PDS. 325 - * @param blob - The blob data to upload 326 - * @returns The blob metadata including ref, mimeType, and size, or undefined on failure 327 - * @throws If the user is not logged in 271 + * Uploads a blob to the current user's PDS via server-side proxy. 328 272 */ 329 273 export async function uploadBlob({ blob }: { blob: Blob }) { 330 - if (!user.did || !user.client) throw new Error("Can't upload blob: Not logged in"); 331 - 332 - const blobResponse = await user.client.post('com.atproto.repo.uploadBlob', { 333 - params: { 334 - repo: user.did 335 - }, 336 - input: blob 337 - }); 338 - 339 - if (!blobResponse?.ok) return; 340 - 341 - const blobInfo = blobResponse?.data.blob as { 342 - $type: 'blob'; 343 - ref: { 344 - $link: string; 345 - }; 346 - mimeType: string; 347 - size: number; 348 - }; 349 - 350 - return blobInfo; 274 + const { uploadBlob: serverUploadBlob } = await import('./server/repo.remote'); 275 + const bytes = Array.from(new Uint8Array(await blob.arrayBuffer())); 276 + return serverUploadBlob({ bytes, mimeType: blob.type }); 351 277 } 352 278 353 279 /** 354 280 * Gets metadata about a repository. 355 - * @param client - The client to use 356 - * @param did - The DID of the repository (defaults to current user) 357 - * @returns Repository metadata or undefined on failure 358 281 */ 359 282 export async function describeRepo({ client, did }: { client?: Client; did?: Did }) { 360 283 did ??= user.did; ··· 375 298 376 299 /** 377 300 * Constructs a URL to fetch a blob directly from a user's PDS. 378 - * @param did - The DID of the user who owns the blob 379 - * @param blob - The blob reference object 380 - * @returns The URL to fetch the blob 381 301 */ 382 302 export async function getBlobURL({ 383 303 did, ··· 397 317 398 318 /** 399 319 * Constructs a Bluesky CDN URL for an image blob. 400 - * @param did - The DID of the user who owns the blob (defaults to current user) 401 - * @param blob - The blob reference object 402 - * @returns The CDN URL for the image in webp format 403 320 */ 404 321 export function getCDNImageBlobUrl({ 405 322 did, ··· 423 340 424 341 /** 425 342 * Searches for actors with typeahead/autocomplete functionality. 426 - * @param q - The search query 427 - * @param limit - Maximum number of results (default 10) 428 - * @param host - The API host to use (defaults to public Bluesky API) 429 - * @returns An object containing matching actors and the original query 430 343 */ 431 344 export async function searchActorsTypeahead( 432 345 q: string, ··· 453 366 454 367 /** 455 368 * Return a TID based on current time 456 - * 457 - * @returns TID for current time 458 369 */ 459 370 export function createTID() { 460 371 return TID.now(); ··· 492 403 493 404 /** 494 405 * Fetches posts by their AT URIs. 495 - * @param uris - Array of AT URIs (e.g., "at://did:plc:xyz/app.bsky.feed.post/abc123") 496 - * @param client - The client to use (defaults to public Bluesky API) 497 - * @returns Array of posts or undefined on failure 498 406 */ 499 407 export async function getPosts(data: { uris: string[]; client?: Client }) { 500 408 data.client ??= new Client({ ··· 520 428 521 429 /** 522 430 * Fetches a post's thread including replies. 523 - * @param uri - The AT URI of the post 524 - * @param depth - How many levels of replies to fetch (default 1) 525 - * @param client - The client to use (defaults to public Bluesky API) 526 - * @returns The thread data or undefined on failure 527 431 */ 528 432 export async function getPostThread({ 529 433 uri, ··· 548 452 } 549 453 550 454 /** 551 - * Creates a Bluesky post on the authenticated user's account. 552 - * @param text - The post text 553 - * @param facets - Optional rich text facets (links, mentions, etc.) 554 - * @returns The response containing the post's URI and CID 555 - * @throws If the user is not logged in 455 + * Creates a Bluesky post on the authenticated user's account via server-side proxy. 556 456 */ 557 457 export async function createPost({ 558 458 text, ··· 564 464 features: Array<{ $type: string; uri?: string; did?: string; tag?: string }>; 565 465 }>; 566 466 }) { 567 - if (!user.client || !user.did) throw new Error('No client or did'); 568 - 569 467 const record: Record<string, unknown> = { 570 468 $type: 'app.bsky.feed.post', 571 469 text, ··· 576 474 record.facets = facets; 577 475 } 578 476 579 - const response = await user.client.post('com.atproto.repo.createRecord', { 580 - input: { 581 - collection: 'app.bsky.feed.post', 582 - repo: user.did, 583 - record 584 - } 585 - }); 586 - 587 - return response; 477 + const { createRecord } = await import('./server/repo.remote'); 478 + return createRecord({ collection: 'app.bsky.feed.post', record }); 588 479 }
+4
src/lib/atproto/scripts/generate-key.ts
··· 1 + import { generateClientAssertionKey } from '@atcute/oauth-node-client'; 2 + 3 + const key = await generateClientAssertionKey('main-key'); 4 + console.log(JSON.stringify(key));
+3
src/lib/atproto/scripts/generate-secret.ts
··· 1 + import { randomBytes } from 'node:crypto'; 2 + 3 + console.log(randomBytes(32).toString('base64url'));
+47
src/lib/atproto/scripts/setup-dev.ts
··· 1 + import { existsSync } from 'node:fs'; 2 + import { copyFile, readFile, writeFile } from 'node:fs/promises'; 3 + import { resolve } from 'node:path'; 4 + import { randomBytes } from 'node:crypto'; 5 + 6 + import { generateClientAssertionKey } from '@atcute/oauth-node-client'; 7 + 8 + const cwd = process.cwd(); 9 + const examplePath = resolve(cwd, '.env.example'); 10 + const envPath = resolve(cwd, '.env'); 11 + 12 + if (!existsSync(envPath)) { 13 + if (!existsSync(examplePath)) { 14 + throw new Error(`missing .env.example (expected at ${examplePath})`); 15 + } 16 + await copyFile(examplePath, envPath); 17 + console.log(`created ${envPath}`); 18 + } 19 + 20 + const upsertVar = (input: string, key: string, value: string): string => { 21 + const line = `${key}=${value}`; 22 + const re = new RegExp(`^${key}=.*$`, 'm'); 23 + 24 + if (re.test(input)) { 25 + const match = input.match(re); 26 + const current = match ? match[0].slice(key.length + 1).trim() : ''; 27 + // Only overwrite if empty/placeholder 28 + if (current === '' || current === "''" || current === '""' || current.includes('...')) { 29 + return input.replace(re, line); 30 + } 31 + return input; 32 + } 33 + 34 + const suffix = input.endsWith('\n') || input.length === 0 ? '' : '\n'; 35 + return `${input}${suffix}${line}\n`; 36 + }; 37 + 38 + let vars = await readFile(envPath, 'utf8'); 39 + 40 + const secret = randomBytes(32).toString('base64url'); 41 + vars = upsertVar(vars, 'COOKIE_SECRET', secret); 42 + 43 + const jwk = await generateClientAssertionKey('main-key'); 44 + vars = upsertVar(vars, 'CLIENT_ASSERTION_KEY', JSON.stringify(jwk)); 45 + 46 + await writeFile(envPath, vars); 47 + console.log(`updated ${envPath}`);
+176
src/lib/atproto/scripts/tunnel.ts
··· 1 + import { readFileSync, writeFileSync } from 'node:fs'; 2 + import { resolve } from 'node:path'; 3 + import { spawn } from 'node:child_process'; 4 + 5 + const DEV_PORT = 5179; 6 + 7 + const cwd = process.cwd(); 8 + const envPath = resolve(cwd, '.env'); 9 + const vitePath = resolve(cwd, 'vite.config.ts'); 10 + 11 + let tunnelUrl: string | null = null; 12 + let statusBarActive = false; 13 + 14 + // ── ANSI status bar ────────────────────────────────────────────── 15 + function getColumns(): number { 16 + return process.stdout.columns || 80; 17 + } 18 + 19 + function getRows(): number { 20 + return process.stdout.rows || 24; 21 + } 22 + 23 + function setupScrollRegion(): void { 24 + if (!process.stdout.isTTY) return; 25 + statusBarActive = true; 26 + const rows = getRows(); 27 + process.stdout.write(`\x1b[1;${rows - 1}r`); 28 + process.stdout.write(`\x1b[${rows - 1};1H`); 29 + } 30 + 31 + function drawStatusBar(text: string): void { 32 + if (!process.stdout.isTTY) { 33 + process.stdout.write(text + '\n'); 34 + return; 35 + } 36 + const rows = getRows(); 37 + const cols = getColumns(); 38 + process.stdout.write('\x1b7'); 39 + process.stdout.write(`\x1b[${rows};1H`); 40 + process.stdout.write('\x1b[2K'); 41 + process.stdout.write(`\x1b[7m ${text.padEnd(cols - 1)}\x1b[0m`); 42 + process.stdout.write('\x1b8'); 43 + } 44 + 45 + function clearStatusBar(): void { 46 + if (!process.stdout.isTTY || !statusBarActive) return; 47 + const rows = getRows(); 48 + process.stdout.write(`\x1b[1;${rows}r`); 49 + process.stdout.write(`\x1b[${rows};1H\x1b[2K`); 50 + process.stdout.write(`\x1b[${rows - 1};1H`); 51 + statusBarActive = false; 52 + } 53 + 54 + function writeLog(text: string): void { 55 + process.stdout.write(text); 56 + } 57 + 58 + process.stdout.on('resize', () => { 59 + if (!statusBarActive || !tunnelUrl) return; 60 + setupScrollRegion(); 61 + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); 62 + }); 63 + 64 + // ── .env helpers ───────────────────────────────────────────────── 65 + function readEnv(): string { 66 + return readFileSync(envPath, 'utf8'); 67 + } 68 + 69 + function writeEnv(content: string): void { 70 + writeFileSync(envPath, content); 71 + } 72 + 73 + function setEnvVar(key: string, value: string): void { 74 + let env = readEnv(); 75 + const re = new RegExp(`^(#\\s*)?${key}=.*$`, 'm'); 76 + const line = `${key}=${value}`; 77 + 78 + if (re.test(env)) { 79 + env = env.replace(re, line); 80 + } else { 81 + env = env.trimEnd() + '\n' + line + '\n'; 82 + } 83 + writeEnv(env); 84 + } 85 + 86 + function clearEnvVar(key: string): void { 87 + let env = readEnv(); 88 + const re = new RegExp(`^${key}=.*$`, 'm'); 89 + 90 + if (re.test(env)) { 91 + env = env.replace(re, `# ${key}=`); 92 + writeEnv(env); 93 + } 94 + } 95 + 96 + // ── vite config helpers ────────────────────────────────────────── 97 + function setViteAllowedHosts(hostname: string): void { 98 + let vite = readFileSync(vitePath, 'utf8'); 99 + 100 + if (/allowedHosts\s*:/.test(vite)) { 101 + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, `allowedHosts: ['${hostname}']`); 102 + } else if (/server\s*:\s*\{/.test(vite)) { 103 + vite = vite.replace(/server\s*:\s*\{/, `server: {\n\t\tallowedHosts: ['${hostname}'],`); 104 + } 105 + 106 + writeFileSync(vitePath, vite); 107 + } 108 + 109 + function clearViteAllowedHosts(): void { 110 + let vite = readFileSync(vitePath, 'utf8'); 111 + 112 + if (/allowedHosts\s*:/.test(vite)) { 113 + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, 'allowedHosts: []'); 114 + } 115 + 116 + writeFileSync(vitePath, vite); 117 + } 118 + 119 + // ── cleanup ────────────────────────────────────────────────────── 120 + function cleanup(): void { 121 + clearStatusBar(); 122 + console.log('\nCleaning up...'); 123 + if (tunnelUrl) { 124 + clearEnvVar('OAUTH_PUBLIC_URL'); 125 + console.log(' Cleared OAUTH_PUBLIC_URL from .env'); 126 + clearViteAllowedHosts(); 127 + console.log(' Cleared allowedHosts from vite.config.ts'); 128 + } 129 + } 130 + 131 + // ── main ───────────────────────────────────────────────────────── 132 + const child = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${DEV_PORT}`], { 133 + stdio: ['ignore', 'pipe', 'pipe'] 134 + }); 135 + 136 + child.stderr.on('data', (data: Buffer) => { 137 + const output = data.toString(); 138 + 139 + if (!tunnelUrl) { 140 + const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); 141 + if (match) { 142 + tunnelUrl = match[0]; 143 + const hostname = new URL(tunnelUrl).hostname; 144 + 145 + setEnvVar('OAUTH_PUBLIC_URL', tunnelUrl); 146 + setViteAllowedHosts(hostname); 147 + 148 + writeLog(`\n Set OAUTH_PUBLIC_URL=${tunnelUrl}\n`); 149 + writeLog(` Set vite allowedHosts to [${hostname}]\n`); 150 + writeLog(` Tunnel is ready! Restart your dev server to pick up the new URL.\n\n`); 151 + 152 + setupScrollRegion(); 153 + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); 154 + return; 155 + } 156 + } 157 + 158 + writeLog(output); 159 + }); 160 + 161 + child.stdout.on('data', (data: Buffer) => { 162 + writeLog(data.toString()); 163 + }); 164 + 165 + child.on('close', (code) => { 166 + cleanup(); 167 + process.exit(code ?? 0); 168 + }); 169 + 170 + process.on('SIGINT', () => { 171 + child.kill('SIGINT'); 172 + }); 173 + 174 + process.on('SIGTERM', () => { 175 + child.kill('SIGTERM'); 176 + });
+39
src/lib/atproto/server/kv-store.ts
··· 1 + import type { KVNamespace } from '@cloudflare/workers-types'; 2 + import type { Store } from '@atcute/oauth-node-client'; 3 + 4 + export class KVStore<K extends string, V> implements Store<K, V> { 5 + private kv: KVNamespace; 6 + private expirationTtl?: number; 7 + 8 + constructor(kv: KVNamespace, options?: { expirationTtl?: number }) { 9 + this.kv = kv; 10 + this.expirationTtl = options?.expirationTtl; 11 + } 12 + 13 + async get(key: K): Promise<V | undefined> { 14 + const value = await this.kv.get(key, 'text'); 15 + if (value === null) return undefined; 16 + return JSON.parse(value) as V; 17 + } 18 + 19 + async set(key: K, value: V): Promise<void> { 20 + await this.kv.put(key, JSON.stringify(value), { 21 + expirationTtl: this.expirationTtl 22 + }); 23 + } 24 + 25 + async delete(key: K): Promise<void> { 26 + await this.kv.delete(key); 27 + } 28 + 29 + async clear(): Promise<void> { 30 + let cursor: string | undefined; 31 + do { 32 + const result = await this.kv.list({ cursor }); 33 + for (const key of result.keys) { 34 + await this.kv.delete(key.name); 35 + } 36 + cursor = result.list_complete ? undefined : result.cursor; 37 + } while (cursor); 38 + } 39 + }
+62
src/lib/atproto/server/oauth.remote.ts
··· 1 + import * as v from 'valibot'; 2 + import { error } from '@sveltejs/kit'; 3 + import { command, getRequestEvent } from '$app/server'; 4 + import { createOAuthClient } from './oauth'; 5 + import { getSignedCookie } from './signed-cookie'; 6 + import { signUpPDS } from '../settings'; 7 + import { scopes } from './scopes'; 8 + import type { ActorIdentifier, Did } from '@atcute/lexicons'; 9 + 10 + function getDomain(): string | undefined { 11 + const { request } = getRequestEvent(); 12 + return request.headers.get('X-Custom-Domain')?.toLowerCase() || undefined; 13 + } 14 + 15 + export const oauthLogin = command( 16 + v.object({ 17 + handle: v.optional(v.pipe(v.string(), v.minLength(3))), 18 + signup: v.optional(v.boolean()) 19 + }), 20 + async (input) => { 21 + const { platform } = getRequestEvent(); 22 + 23 + try { 24 + const oauth = createOAuthClient(platform?.env, getDomain()); 25 + 26 + const target = input.signup 27 + ? ({ type: 'pds', serviceUrl: signUpPDS } as const) 28 + : ({ type: 'account', identifier: input.handle as ActorIdentifier } as const); 29 + 30 + const { url } = await oauth.authorize({ 31 + target, 32 + scope: scopes.join(' '), 33 + prompt: input.signup ? 'create' : undefined 34 + }); 35 + 36 + return { url: url.toString() }; 37 + } catch (e) { 38 + if (e && typeof e === 'object' && 'status' in e) throw e; 39 + const message = e instanceof Error ? e.message : 'Login failed'; 40 + error(400, message); 41 + } 42 + } 43 + ); 44 + 45 + export const oauthLogout = command(async () => { 46 + const { cookies, platform } = getRequestEvent(); 47 + const did = getSignedCookie(cookies, 'did') as Did | null; 48 + 49 + if (did) { 50 + try { 51 + const oauth = createOAuthClient(platform?.env, getDomain()); 52 + await oauth.revoke(did); 53 + } catch (e) { 54 + console.error('Error revoking session:', e); 55 + } 56 + } 57 + 58 + cookies.delete('did', { path: '/' }); 59 + cookies.delete('scope', { path: '/' }); 60 + 61 + return { ok: true }; 62 + });
+107
src/lib/atproto/server/oauth.ts
··· 1 + import { 2 + OAuthClient, 3 + MemoryStore, 4 + type ClientAssertionPrivateJwk, 5 + type OAuthClientStores, 6 + type OAuthSession, 7 + type StoredSession, 8 + type StoredState 9 + } from '@atcute/oauth-node-client'; 10 + import type { Did } from '@atcute/lexicons'; 11 + import { 12 + CompositeDidDocumentResolver, 13 + CompositeHandleResolver, 14 + DohJsonHandleResolver, 15 + LocalActorResolver, 16 + PlcDidDocumentResolver, 17 + WebDidDocumentResolver, 18 + WellKnownHandleResolver 19 + } from '@atcute/identity-resolver'; 20 + import { KVStore } from './kv-store'; 21 + import { DOH_RESOLVER, REDIRECT_PATH, SITE } from '../settings'; 22 + import { scopes } from './scopes'; 23 + import { dev } from '$app/environment'; 24 + 25 + const DEV_PORT = 5179; 26 + 27 + function createActorResolver() { 28 + return new LocalActorResolver({ 29 + handleResolver: new CompositeHandleResolver({ 30 + methods: { 31 + dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), 32 + http: new WellKnownHandleResolver() 33 + } 34 + }), 35 + didDocumentResolver: new CompositeDidDocumentResolver({ 36 + methods: { 37 + plc: new PlcDidDocumentResolver(), 38 + web: new WebDidDocumentResolver() 39 + } 40 + }) 41 + }); 42 + } 43 + 44 + function createStores(env?: App.Platform['env']): OAuthClientStores { 45 + if (env?.OAUTH_SESSIONS && env?.OAUTH_STATES) { 46 + return { 47 + sessions: new KVStore<Did, StoredSession>(env.OAUTH_SESSIONS), 48 + states: new KVStore<string, StoredState>(env.OAUTH_STATES, { expirationTtl: 600 }) 49 + }; 50 + } 51 + // Fallback to in-memory stores (dev without wrangler) 52 + return { 53 + sessions: new MemoryStore<Did, StoredSession>(), 54 + states: new MemoryStore<string, StoredState>({ ttl: 600_000 }) 55 + }; 56 + } 57 + 58 + // Cache OAuth clients per domain to avoid re-creating them on every request 59 + const clientCache = new Map<string, OAuthClient>(); 60 + 61 + export function createOAuthClient(env?: App.Platform['env'], domain?: string): OAuthClient { 62 + const cacheKey = domain ?? '__default__'; 63 + const cached = clientCache.get(cacheKey); 64 + if (cached) return cached; 65 + 66 + const actorResolver = createActorResolver(); 67 + const stores = createStores(env); 68 + 69 + if (dev && !env?.CLIENT_ASSERTION_KEY) { 70 + // Dev without secrets: loopback public client (no keyset). 71 + const client = new OAuthClient({ 72 + metadata: { 73 + redirect_uris: [`http://127.0.0.1:${DEV_PORT}${REDIRECT_PATH}`], 74 + scope: scopes 75 + }, 76 + actorResolver, 77 + stores 78 + }); 79 + clientCache.set(cacheKey, client); 80 + return client; 81 + } 82 + 83 + // Confidential client (production, or dev with secrets) 84 + if (!env?.CLIENT_ASSERTION_KEY) { 85 + throw new Error('CLIENT_ASSERTION_KEY secret is not set'); 86 + } 87 + 88 + const site = domain ? `https://${domain}` : (SITE ?? 'https://blento.app'); 89 + const key: ClientAssertionPrivateJwk = JSON.parse(env.CLIENT_ASSERTION_KEY); 90 + 91 + const client = new OAuthClient({ 92 + metadata: { 93 + client_id: site + '/oauth-client-metadata.json', 94 + redirect_uris: [site + REDIRECT_PATH], 95 + scope: scopes, 96 + jwks_uri: site + '/oauth/jwks.json' 97 + }, 98 + keyset: [key], 99 + actorResolver, 100 + stores 101 + }); 102 + 103 + clientCache.set(cacheKey, client); 104 + return client; 105 + } 106 + 107 + export type { OAuthSession };
+139
src/lib/atproto/server/repo.remote.ts
··· 1 + import { error } from '@sveltejs/kit'; 2 + import { command, getRequestEvent } from '$app/server'; 3 + import * as v from 'valibot'; 4 + import { collections } from '../settings'; 5 + import { contrail, ensureInit } from '$lib/contrail'; 6 + 7 + // Validate collection format and check against allowed list 8 + const collectionSchema = v.pipe( 9 + v.string(), 10 + v.regex(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*){2,}$/), 11 + v.check( 12 + (c) => collections.includes(c as (typeof collections)[number]), 13 + 'Collection not in allowed list' 14 + ) 15 + ); 16 + 17 + // AT Protocol rkey: TID, 'self', or other valid record keys 18 + const rkeySchema = v.optional(v.pipe(v.string(), v.regex(/^[a-zA-Z0-9._:~-]{1,512}$/))); 19 + 20 + export const putRecord = command( 21 + v.object({ 22 + collection: collectionSchema, 23 + rkey: rkeySchema, 24 + record: v.record(v.string(), v.unknown()) 25 + }), 26 + async (input) => { 27 + const { locals } = getRequestEvent(); 28 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 29 + 30 + const response = await locals.client.post('com.atproto.repo.putRecord', { 31 + input: { 32 + collection: input.collection as `${string}.${string}.${string}`, 33 + repo: locals.did, 34 + rkey: input.rkey || 'self', 35 + record: input.record 36 + } 37 + }); 38 + 39 + if (!response.ok) error(500, 'Failed to put record'); 40 + 41 + // Immediately index in contrail 42 + const { platform } = getRequestEvent(); 43 + const db = platform?.env?.DB; 44 + if (db) { 45 + await ensureInit(db); 46 + await contrail.notify(response.data.uri, db).catch(() => {}); 47 + } 48 + 49 + return response.data; 50 + } 51 + ); 52 + 53 + export const deleteRecord = command( 54 + v.object({ 55 + collection: collectionSchema, 56 + rkey: rkeySchema 57 + }), 58 + async (input) => { 59 + const { locals, platform } = getRequestEvent(); 60 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 61 + 62 + const rkey = input.rkey || 'self'; 63 + const response = await locals.client.post('com.atproto.repo.deleteRecord', { 64 + input: { 65 + collection: input.collection as `${string}.${string}.${string}`, 66 + repo: locals.did, 67 + rkey 68 + } 69 + }); 70 + 71 + // Tell contrail the record is gone — notify() re-fetches and removes on 404 72 + const db = platform?.env?.DB; 73 + if (db && response.ok) { 74 + await ensureInit(db); 75 + const uri = `at://${locals.did}/${input.collection}/${rkey}`; 76 + await contrail.notify(uri, db).catch(() => {}); 77 + } 78 + 79 + return { ok: response.ok }; 80 + } 81 + ); 82 + 83 + export const uploadBlob = command( 84 + v.object({ 85 + bytes: v.array(v.number()), 86 + mimeType: v.string() 87 + }), 88 + async (input) => { 89 + const { locals } = getRequestEvent(); 90 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 91 + 92 + const blob = new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }); 93 + 94 + const response = await locals.client.post('com.atproto.repo.uploadBlob', { 95 + params: { repo: locals.did }, 96 + input: blob 97 + }); 98 + 99 + if (!response.ok) error(500, 'Upload failed'); 100 + 101 + return response.data.blob as { 102 + $type: 'blob'; 103 + ref: { $link: string }; 104 + mimeType: string; 105 + size: number; 106 + }; 107 + } 108 + ); 109 + 110 + export const createRecord = command( 111 + v.object({ 112 + collection: collectionSchema, 113 + record: v.record(v.string(), v.unknown()) 114 + }), 115 + async (input) => { 116 + const { locals } = getRequestEvent(); 117 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 118 + 119 + const response = await locals.client.post('com.atproto.repo.createRecord', { 120 + input: { 121 + collection: input.collection as `${string}.${string}.${string}`, 122 + repo: locals.did, 123 + record: input.record 124 + } 125 + }); 126 + 127 + if (!response.ok) error(500, 'Failed to create record'); 128 + 129 + // Immediately index in contrail 130 + const { platform } = getRequestEvent(); 131 + const db = platform?.env?.DB; 132 + if (db) { 133 + await ensureInit(db); 134 + await contrail.notify(response.data.uri, db).catch(() => {}); 135 + } 136 + 137 + return response.data; 138 + } 139 + );
+10
src/lib/atproto/server/scopes.ts
··· 1 + import { scope } from '@atcute/oauth-node-client'; 2 + import { collections } from '../settings'; 3 + 4 + export const scopes = [ 5 + 'atproto', 6 + scope.repo({ collection: [...collections] }), 7 + scope.blob({ accept: ['*/*'] }), 8 + 'include:app.bsky.authCreatePosts', 9 + 'include:site.standard.authFull' 10 + ];
+69
src/lib/atproto/server/session.ts
··· 1 + import type { Cookies } from '@sveltejs/kit'; 2 + import { Client } from '@atcute/client'; 3 + import type { Did } from '@atcute/lexicons'; 4 + import { 5 + type OAuthSession, 6 + TokenInvalidError, 7 + TokenRefreshError, 8 + TokenRevokedError, 9 + AuthMethodUnsatisfiableError 10 + } from '@atcute/oauth-node-client'; 11 + import { createOAuthClient } from './oauth'; 12 + import { getSignedCookie } from './signed-cookie'; 13 + import { scopes } from './scopes'; 14 + 15 + export type SessionLocals = { 16 + session: OAuthSession | null; 17 + client: Client | null; 18 + did: Did | null; 19 + }; 20 + 21 + /** 22 + * Restores an OAuth session from the signed `did` cookie. 23 + * Deletes the cookie only if the session is genuinely unrecoverable. 24 + */ 25 + export async function restoreSession( 26 + cookies: Cookies, 27 + env?: App.Platform['env'], 28 + domain?: string 29 + ): Promise<SessionLocals> { 30 + const did = getSignedCookie(cookies, 'did') as Did | null; 31 + 32 + if (!did) { 33 + return { session: null, client: null, did: null }; 34 + } 35 + 36 + // If permissions changed since login, invalidate the session 37 + const savedScope = getSignedCookie(cookies, 'scope'); 38 + if (savedScope !== null && savedScope !== scopes.join(' ')) { 39 + cookies.delete('did', { path: '/' }); 40 + cookies.delete('scope', { path: '/' }); 41 + return { session: null, client: null, did: null }; 42 + } 43 + 44 + try { 45 + const oauth = createOAuthClient(env, domain); 46 + const session = await oauth.restore(did); 47 + 48 + return { 49 + session, 50 + client: new Client({ handler: session }), 51 + did 52 + }; 53 + } catch (e) { 54 + console.error('Failed to restore session:', e); 55 + 56 + const isSessionGone = 57 + e instanceof TokenInvalidError || 58 + e instanceof TokenRevokedError || 59 + e instanceof TokenRefreshError || 60 + e instanceof AuthMethodUnsatisfiableError; 61 + 62 + if (isSessionGone) { 63 + cookies.delete('did', { path: '/' }); 64 + cookies.delete('scope', { path: '/' }); 65 + } 66 + 67 + return { session: null, client: null, did: null }; 68 + } 69 + }
+69
src/lib/atproto/server/signed-cookie.ts
··· 1 + import { createHmac, timingSafeEqual } from 'node:crypto'; 2 + 3 + import type { Cookies } from '@sveltejs/kit'; 4 + 5 + import { env } from '$env/dynamic/private'; 6 + import { dev } from '$app/environment'; 7 + 8 + const SEPARATOR = '.'; 9 + 10 + function getSecret(): string { 11 + const secret = env.COOKIE_SECRET; 12 + if (secret) return secret; 13 + if (dev) return 'dev-cookie-secret-not-for-production'; 14 + throw new Error('COOKIE_SECRET is not set'); 15 + } 16 + 17 + function toBase64Url(bytes: Uint8Array): string { 18 + let binary = ''; 19 + for (const byte of bytes) binary += String.fromCharCode(byte); 20 + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 21 + } 22 + 23 + function fromBase64Url(str: string): Uint8Array { 24 + const padded = str + '='.repeat((4 - (str.length % 4)) % 4); 25 + const base64 = padded.replace(/-/g, '+').replace(/_/g, '/'); 26 + const binary = atob(base64); 27 + const bytes = new Uint8Array(binary.length); 28 + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); 29 + return bytes; 30 + } 31 + 32 + function hmacSha256(data: string): Uint8Array { 33 + return createHmac('sha256', getSecret()).update(data).digest(); 34 + } 35 + 36 + export function getSignedCookie(cookies: Cookies, name: string): string | null { 37 + const signed = cookies.get(name); 38 + if (!signed) return null; 39 + 40 + const idx = signed.lastIndexOf(SEPARATOR); 41 + if (idx === -1) return null; 42 + 43 + const value = signed.slice(0, idx); 44 + const sig = signed.slice(idx + 1); 45 + 46 + let expected: Uint8Array; 47 + let got: Uint8Array; 48 + try { 49 + expected = hmacSha256(value); 50 + got = fromBase64Url(sig); 51 + } catch { 52 + return null; 53 + } 54 + 55 + if (got.length !== expected.length || !timingSafeEqual(got, expected)) return null; 56 + 57 + return value; 58 + } 59 + 60 + export function setSignedCookie( 61 + cookies: Cookies, 62 + name: string, 63 + value: string, 64 + options: Parameters<Cookies['set']>[2] 65 + ): void { 66 + const sig = toBase64Url(hmacSha256(value)); 67 + const signed = `${value}${SEPARATOR}${sig}`; 68 + cookies.set(name, signed, options); 69 + }
+15 -45
src/lib/atproto/settings.ts
··· 3 3 4 4 export const SITE = env.PUBLIC_DOMAIN; 5 5 6 - type Permissions = { 7 - collections: readonly string[]; 8 - rpc: Record<string, string | string[]>; 9 - blobs: readonly string[]; 10 - }; 11 - 12 - export const permissions = { 13 - // collections you can create/delete/update 14 - 15 - // example: only allow create and delete 16 - // collections: ['xyz.statusphere.status?action=create&action=update'], 17 - collections: [ 18 - 'app.blento.card', 19 - 'app.blento.page', 20 - 'app.blento.settings', 21 - 'app.blento.comment', 22 - 'app.blento.guestbook.entry', 23 - 'site.standard.publication', 24 - 'site.standard.document', 25 - 'xyz.statusphere.status', 26 - 'community.lexicon.calendar.rsvp', 27 - 'community.lexicon.calendar.event', 28 - 'app.nearhorizon.actor.pronouns' 29 - ], 30 - 31 - // what types of authenticated proxied requests you can make to services 32 - 33 - // example: allow authenticated proxying to bsky appview to get a users liked posts 34 - //rpc: {'did:web:api.bsky.app#bsky_appview': ['app.bsky.feed.getActorLikes']} 35 - rpc: {}, 36 - 37 - // what types of blobs you can upload to a users PDS 38 - 39 - // example: allowing video and html uploads 40 - // blobs: ['video/*', 'text/html'] 41 - // example: allowing all blob types 42 - // blobs: ['*/*'] 43 - blobs: ['*/*'] 44 - } as const satisfies Permissions; 45 - 46 - // Extract base collection name (before any query params) 47 - type ExtractCollectionBase<T extends string> = T extends `${infer Base}?${string}` ? Base : T; 6 + export const collections = [ 7 + 'app.blento.card', 8 + 'app.blento.page', 9 + 'app.blento.settings', 10 + 'app.blento.comment', 11 + 'app.blento.guestbook.entry', 12 + 'site.standard.publication', 13 + 'site.standard.document', 14 + 'xyz.statusphere.status', 15 + 'community.lexicon.calendar.rsvp', 16 + 'community.lexicon.calendar.event', 17 + 'app.nearhorizon.actor.pronouns' 18 + ] as const; 48 19 49 - export type AllowedCollection = ExtractCollectionBase<(typeof permissions.collections)[number]>; 20 + export type AllowedCollection = (typeof collections)[number]; 50 21 51 22 // which PDS to use for signup 52 - // ATTENTION: pds.rip is only for development, all accounts get deleted automatically after a week 53 23 const devPDS = 'https://pds.rip/'; 54 24 const prodPDS = 'https://selfhosted.social/'; 55 25 export const signUpPDS = dev ? devPDS : prodPDS; 56 26 57 - // where to redirect after oauth login/signup, e.g. /oauth/callback 27 + // where to redirect after oauth login/signup 58 28 export const REDIRECT_PATH = '/oauth/callback'; 59 29 60 30 export const DOH_RESOLVER = 'https://mozilla.cloudflare-dns.com/dns-query';
+1 -33
src/lib/cache.ts
··· 1 - import type { ActorIdentifier, Did } from '@atcute/lexicons'; 2 - import { isDid } from '@atcute/lexicons/syntax'; 1 + import type { Did } from '@atcute/lexicons'; 3 2 import type { KVNamespace } from '@cloudflare/workers-types'; 4 3 import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; 5 4 6 5 /** TTL in seconds for each cache namespace */ 7 6 const NAMESPACE_TTL = { 8 - blento: 60 * 60 * 24, // 24 hours 9 - identity: 60 * 60 * 24 * 7, // 7 days 10 7 github: 60 * 60 * 12, // 12 hours 11 8 'gh-contrib': 60 * 60 * 12, // 12 hours 12 9 lastfm: 60 * 60, // 1 hour (default, overridable per-put) ··· 82 79 ): Promise<void> { 83 80 const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0; 84 81 await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); 85 - } 86 - 87 - // === blento data (keyed by DID, with handle↔did resolution) === 88 - async getBlento(identifier: ActorIdentifier): Promise<string | null> { 89 - const did = await this.resolveDid(identifier); 90 - if (!did) return null; 91 - return this.get('blento', did); 92 - } 93 - 94 - async putBlento(did: string, handle: string, data: string): Promise<void> { 95 - await Promise.all([ 96 - this.put('blento', did, data), 97 - this.put('identity', `h:${handle}`, did), 98 - this.put('identity', `d:${did}`, handle) 99 - ]); 100 - } 101 - 102 - async listBlentos(): Promise<string[]> { 103 - return this.list('blento'); 104 - } 105 - 106 - // === Identity resolution === 107 - async resolveDid(identifier: ActorIdentifier): Promise<string | null> { 108 - if (isDid(identifier)) return identifier; 109 - return this.get('identity', `h:${identifier}`); 110 - } 111 - 112 - async resolveHandle(did: Did): Promise<string | null> { 113 - return this.get('identity', `d:${did}`); 114 82 } 115 83 116 84 // === Profile cache (did → profile data) ===
+2 -6
src/lib/cards/social/GuestbookCard/CreateGuestbookCardModal.svelte
··· 59 59 const facets = buildFacets(postText, profileUrl); 60 60 const response = await createPost({ text: postText, facets }); 61 61 62 - if (!response.ok) { 63 - throw new Error('Failed to create post'); 64 - } 65 - 66 - item.cardData.uri = response.data.uri; 62 + item.cardData.uri = response.uri; 67 63 68 - const rkey = response.data.uri.split('/').pop(); 64 + const rkey = response.uri.split('/').pop(); 69 65 item.cardData.href = `https://bsky.app/profile/${user.profile?.handle}/post/${rkey}`; 70 66 71 67 oncreate();
+88 -41
src/lib/cards/special/UpdatedBlentos/index.ts
··· 1 1 import type { CardDefinition } from '../../types'; 2 2 import UpdatedBlentosCard from './UpdatedBlentosCard.svelte'; 3 + import { getCDNImageBlobUrl } from '$lib/atproto/methods'; 4 + import { getServerClient } from '$lib/contrail'; 3 5 import type { Did } from '@atcute/lexicons'; 4 - import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; 6 + 7 + type ProfileWithBlentoFlag = { 8 + did: Did; 9 + handle: `${string}.${string}`; 10 + displayName?: string; 11 + avatar?: string; 12 + hasBlento: boolean; 13 + url?: string; 14 + }; 15 + 16 + function extractProfiles( 17 + profiles: Array<{ 18 + did: string; 19 + handle?: string; 20 + collection?: string; 21 + rkey?: string; 22 + record?: unknown; 23 + }> 24 + ): Map<string, ProfileWithBlentoFlag> { 25 + const map = new Map<string, ProfileWithBlentoFlag>(); 26 + 27 + for (const p of profiles) { 28 + const existing = map.get(p.did) ?? { 29 + did: p.did as Did, 30 + handle: (p.handle ?? p.did) as `${string}.${string}`, 31 + hasBlento: false 32 + }; 33 + 34 + if (p.handle && p.handle !== 'handle.invalid') { 35 + existing.handle = p.handle as `${string}.${string}`; 36 + } 5 37 6 - type ProfileWithBlentoFlag = Awaited<ReturnType<typeof getBlentoOrBskyProfile>>; 38 + const record = p.record as Record<string, unknown> | undefined; 39 + 40 + if (p.collection === 'app.bsky.actor.profile' && record) { 41 + existing.displayName ??= record.displayName as string | undefined; 42 + if (!existing.avatar && record.avatar) { 43 + const cdnUrl = getCDNImageBlobUrl({ 44 + did: p.did, 45 + blob: record.avatar as { $type: 'blob'; ref: { $link: string } } 46 + }); 47 + if (cdnUrl) existing.avatar = cdnUrl; 48 + } 49 + } 50 + 51 + if (p.collection === 'site.standard.publication' && record) { 52 + existing.hasBlento = true; 53 + existing.displayName = (record.name as string) ?? existing.displayName; 54 + existing.url = record.url as string | undefined; 55 + if (record.icon) { 56 + const cdnUrl = getCDNImageBlobUrl({ 57 + did: p.did, 58 + blob: record.icon as { $type: 'blob'; ref: { $link: string } } 59 + }); 60 + if (cdnUrl) existing.avatar = cdnUrl; 61 + } 62 + } 63 + 64 + map.set(p.did, existing); 65 + } 66 + 67 + return map; 68 + } 7 69 8 70 export const UpdatedBlentosCardDefitition = { 9 71 type: 'updatedBlentos', 10 72 contentComponent: UpdatedBlentosCard, 11 73 keywords: ['feed', 'updates', 'recent', 'activity'], 12 - loadData: async (items, { cache }) => { 74 + loadDataServer: async (items, { platform }) => { 13 75 try { 14 - const response = await fetch( 15 - 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 16 - ); 17 - const recentRecords = await response.json(); 18 - const existingUsers = await cache?.get('meta', 'updatedBlentos'); 19 - const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers 20 - ? JSON.parse(existingUsers) 21 - : []; 76 + const db = platform?.env?.DB; 77 + if (!db) return []; 22 78 23 - const uniqueDids = new Set<Did>(recentRecords.map((v: { did: string }) => v.did as Did)); 79 + const client = getServerClient(db); 80 + const res = await client.get('app.blento.card.listRecords', { 81 + params: { limit: 100, profiles: true, sort: 'time_us', order: 'desc' } 82 + }); 24 83 25 - const profiles: Promise<ProfileWithBlentoFlag | undefined>[] = []; 84 + if (!res.ok) return []; 26 85 27 - for (const did of Array.from(uniqueDids)) { 28 - profiles.push(getBlentoOrBskyProfile({ did })); 29 - } 86 + // Build profile map from contrail (includes bsky profile + blento publication) 87 + const profileMap = res.data.profiles ? extractProfiles(res.data.profiles) : new Map(); 30 88 31 - for (let i = existingUsersArray.length - 1; i >= 0; i--) { 32 - // if handle is handle.invalid, remove from existing users and add to profiles to refresh 33 - if ( 34 - (existingUsersArray[i].handle === 'handle.invalid' || 35 - (!existingUsersArray[i].avatar && !existingUsersArray[i].hasBlento)) && 36 - !uniqueDids.has(existingUsersArray[i].did) 37 - ) { 38 - const removed = existingUsersArray.splice(i, 1)[0]; 39 - profiles.push(getBlentoOrBskyProfile({ did: removed.did })); 40 - // if in unique dids, remove from older existing users and keep the newer one 41 - // so updated profiles go first 42 - } else if (uniqueDids.has(existingUsersArray[i].did)) { 43 - existingUsersArray.splice(i, 1); 89 + // Extract unique DIDs in order of recency 90 + const seen = new Set<string>(); 91 + const uniqueDids: string[] = []; 92 + for (const r of res.data.records) { 93 + if (!seen.has(r.did) && !r.did.endsWith('.pds.rip')) { 94 + seen.add(r.did); 95 + uniqueDids.push(r.did); 44 96 } 45 97 } 46 98 47 - let result = [...(await Promise.all(profiles)), ...existingUsersArray]; 48 - 49 - result = result.filter( 50 - (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 51 - ); 52 - 53 - await cache?.put('meta', 'updatedBlentos', JSON.stringify(result)); 54 - 55 - return JSON.parse(JSON.stringify(result.slice(0, 20))); 99 + return uniqueDids 100 + .slice(0, 20) 101 + .map((did) => profileMap.get(did)) 102 + .filter( 103 + (v): v is ProfileWithBlentoFlag => 104 + !!v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 105 + ); 56 106 } catch (error) { 57 107 console.error('error fetching updated blentos', error); 58 108 return []; 59 109 } 60 110 } 61 - // name: 'Updated Blentos', 62 - // groups: ['Social'], 63 - // icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM12 6c-1.602 0-3.155.474-4.434 1.357L18 16.791A8.959 8.959 0 0 0 21 12h-4.5Z" /></svg>` 64 111 } as CardDefinition & { type: 'updatedBlentos' };
+3 -1
src/lib/cards/types.ts
··· 48 48 did, 49 49 handle, 50 50 cache, 51 - env 51 + env, 52 + platform 52 53 }: { 53 54 did: Did; 54 55 handle: string; 55 56 cache?: CacheService; 56 57 env?: Record<string, string | undefined>; 58 + platform?: App.Platform; 57 59 } 58 60 ) => Promise<unknown>; 59 61
+8
src/lib/contrail/client.ts
··· 1 + import { Client, simpleFetchHandler } from '@atcute/client'; 2 + 3 + /** 4 + * Client-side: fully typed @atcute/client that queries the app's own /xrpc/ endpoints. 5 + */ 6 + export function getClient() { 7 + return new Client({ handler: simpleFetchHandler({ service: '' }) }); 8 + }
+21
src/lib/contrail/config.ts
··· 1 + import type { ContrailConfig } from '@atmo-dev/contrail'; 2 + 3 + export const config: ContrailConfig = { 4 + namespace: 'app.blento', 5 + collections: { 6 + 'app.blento.card': { 7 + queryable: { 8 + page: {}, 9 + cardType: {} 10 + } 11 + }, 12 + 'app.blento.page': { 13 + queryable: {} 14 + } 15 + }, 16 + profiles: [ 17 + 'app.bsky.actor.profile', 18 + { collection: 'site.standard.publication', rkey: 'blento.self' }, 19 + { collection: 'app.nearhorizon.actor.pronouns', rkey: 'self' } 20 + ] 21 + };
+32
src/lib/contrail/index.ts
··· 1 + import type { D1Database } from '@cloudflare/workers-types'; 2 + import { Contrail } from '@atmo-dev/contrail'; 3 + import { createHandler } from '@atmo-dev/contrail/server'; 4 + import { Client } from '@atcute/client'; 5 + import { config } from './config'; 6 + 7 + export const contrail = new Contrail(config); 8 + 9 + let initialized = false; 10 + 11 + export async function ensureInit(db: D1Database) { 12 + if (!initialized) { 13 + await contrail.init(db); 14 + initialized = true; 15 + } 16 + } 17 + 18 + const handle = createHandler(contrail); 19 + 20 + /** 21 + * Server-side: fully typed @atcute/client that routes through contrail in-process. 22 + * No HTTP roundtrip — calls createHandler directly. 23 + */ 24 + export function getServerClient(db: D1Database) { 25 + return new Client({ 26 + handler: async (pathname, init) => { 27 + await ensureInit(db); 28 + const url = new URL(pathname, 'http://localhost'); 29 + return handle(new Request(url, init), db) as Promise<Response>; 30 + } 31 + }); 32 + }
+3 -14
src/lib/helper.ts
··· 49 49 ); 50 50 } 51 51 52 - export async function refreshData(data: { updatedAt?: number; handle: string }) { 53 - const TEN_MINUTES = 10 * 60 * 1000; 54 - const now = Date.now(); 55 - 56 - if (now - (data.updatedAt || 0) > TEN_MINUTES) { 57 - try { 58 - await fetch('/' + data.handle + '/api/refresh'); 59 - console.log('successfully refreshed data', data.handle); 60 - } catch (error) { 61 - console.error('error refreshing data', error); 62 - } 63 - } else { 64 - console.log('data still fresh, skipping refreshing', data.handle); 65 - } 52 + /** @deprecated No longer needed — Contrail keeps data fresh via notify() */ 53 + export async function refreshData(_data: { updatedAt?: number; handle: string }) { 54 + // no-op: Contrail indexes are updated immediately on writes 66 55 } 67 56 68 57 export function getName(data: WebsiteData): string {
-5
src/lib/website/CustomDomainModal.svelte
··· 118 118 return; 119 119 } 120 120 121 - // Refresh cached profile 122 - if (user.profile) { 123 - fetch(`/${getHandleOrDid(user.profile)}/api/refresh`).catch(() => {}); 124 - } 125 - 126 121 launchConfetti(); 127 122 step = 'success'; 128 123 } catch (err: unknown) {
+1 -4
src/lib/website/EditableWebsite.svelte
··· 54 54 } = $props(); 55 55 56 56 // Check if floating login button will be visible (to hide MadeWithBlento) 57 - const showLoginOnEditPage = $derived(!user.isInitializing && !user.isLoggedIn); 57 + const showLoginOnEditPage = $derived(!user.isLoggedIn); 58 58 59 59 // Snapshot the original cards so savePage can detect deletions. 60 60 const originalCards: Item[] = structuredClone(data.cards); ··· 263 263 saveSuccess = true; 264 264 265 265 launchConfetti(); 266 - 267 - // Refresh cached data 268 - await fetch('/' + data.handle + '/api/refresh'); 269 266 } catch (error) { 270 267 console.error(error); 271 268 showSaveModal = false;
+2 -2
src/lib/website/FloatingEditButton.svelte
··· 14 14 const isBlento = $derived(!env.PUBLIC_IS_SELFHOSTED && data.handle === 'blento.app'); 15 15 const isEditPage = $derived(page.url.pathname.endsWith('/edit')); 16 16 const showLoginOnBlento = $derived( 17 - isBlento && !user.isInitializing && !user.isLoggedIn && user.profile?.handle !== data.handle 17 + isBlento && !user.isLoggedIn && user.profile?.handle !== data.handle 18 18 ); 19 - const showLoginOnEditPage = $derived(isEditPage && !user.isInitializing && !user.isLoggedIn); 19 + const showLoginOnEditPage = $derived(isEditPage && !user.isLoggedIn); 20 20 const showEditBlentoButton = $derived( 21 21 isBlento && user.isLoggedIn && user.profile?.handle !== data.handle 22 22 );
+14 -1
src/lib/website/Profile.svelte
··· 21 21 renderer.link = ({ href, title, text }) => 22 22 `<a target="_blank" href="${href}" title="${title ?? ''}">${text}</a>`; 23 23 24 - const profileUrl = $derived(`${page.url.origin}/${data.handle}`); 24 + const profileUrl = $derived.by(() => { 25 + if (page.data.customDomain) return `${page.url.origin}/`; 26 + const pubUrl = data.publication?.url; 27 + if ( 28 + pubUrl && 29 + /^https?:\/\//.test(pubUrl) && 30 + !/^https?:\/\/([^/]*\.)?blento\.app/i.test(pubUrl) 31 + ) { 32 + return pubUrl; 33 + } 34 + const handle = data.profile?.handle; 35 + const actor = handle && handle !== 'handle.invalid' ? handle : data.did; 36 + return `${page.url.origin}/${actor}`; 37 + }); 25 38 const profilePosition = $derived(getProfilePosition(data)); 26 39 </script> 27 40
+2 -2
src/lib/website/Website.svelte
··· 31 31 const isOwnPage = $derived(user.isLoggedIn && user.profile?.did === data.did); 32 32 const isBlento = $derived(!env.PUBLIC_IS_SELFHOSTED && data.handle === 'blento.app'); 33 33 const isEditPage = $derived(page.url.pathname.endsWith('/edit')); 34 - const showLoginOnEditPage = $derived(isEditPage && !user.isInitializing && !user.isLoggedIn); 34 + const showLoginOnEditPage = $derived(isEditPage && !user.isLoggedIn); 35 35 const showFloatingButton = $derived( 36 36 (isOwnPage && !isEditPage) || 37 37 showLoginOnEditPage || 38 - (isBlento && !user.isInitializing && !user.isLoggedIn) || 38 + (isBlento && !user.isLoggedIn) || 39 39 (isBlento && user.isLoggedIn && user.profile?.handle !== data.handle) 40 40 ); 41 41
+315 -170
src/lib/website/load.ts
··· 1 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 + import { getCDNImageBlobUrl } from '$lib/atproto/methods'; 2 3 import { CardDefinitionsByType } from '$lib/cards'; 3 4 import type { CacheService } from '$lib/cache'; 4 5 import { createEmptyCard } from '$lib/helper'; ··· 6 7 import { error } from '@sveltejs/kit'; 7 8 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 8 9 10 + import type { D1Database } from '@cloudflare/workers-types'; 9 11 import { isDid, isHandle } from '@atcute/lexicons/syntax'; 10 12 import { fixAllCollisions, compactItems, hasOverlaps } from '$lib/layout'; 11 - 12 - const CURRENT_CACHE_VERSION = 1; 13 + import { getServerClient } from '$lib/contrail'; 14 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 13 15 14 16 function formatPronouns( 15 17 record: PronounsRecord | undefined, 16 18 profile: WebsiteData['profile'] | undefined 17 19 ): string | undefined { 18 - // nearhorizon.actor.pronouns - https://github.com/skydeval/atproto-pronouns 19 20 if (record?.value?.sets?.length) { 20 21 const sets = record.value.sets; 21 22 const displayMode = record.value.displayMode ?? 'all'; 22 23 const setsToShow = displayMode === 'firstOnly' ? sets.slice(0, 1) : sets; 23 24 return setsToShow.map((s) => s.forms.join('/')).join(' · '); 24 25 } 25 - // fallback to bsky pronouns 26 26 const pronouns = (profile as Record<string, unknown>)?.pronouns; 27 27 if (pronouns && typeof pronouns === 'string') return pronouns; 28 28 return undefined; 29 29 } 30 30 31 - export async function getCache(identifier: ActorIdentifier, page: string, cache?: CacheService) { 31 + function resolveDid(handle: ActorIdentifier): Promise<Did | undefined> { 32 + if (isDid(handle)) return Promise.resolve(handle); 33 + if (isHandle(handle)) return resolveHandle({ handle }); 34 + return Promise.resolve(undefined); 35 + } 36 + 37 + type ContrailProfile = { 38 + did: string; 39 + handle?: string; 40 + collection?: string; 41 + rkey?: string; 42 + record?: unknown; 43 + }; 44 + 45 + /** 46 + * Extract a bsky-style profile, publication, and pronouns from contrail profile entries. 47 + */ 48 + function extractProfileData( 49 + did: string, 50 + profiles: ContrailProfile[] 51 + ): { 52 + profile: AppBskyActorDefs.ProfileViewDetailed; 53 + publication: WebsiteData['publication'] | undefined; 54 + pronounsRecord: PronounsRecord | undefined; 55 + } { 56 + let bskyRecord: Record<string, unknown> | undefined; 57 + let pubRecord: Record<string, unknown> | undefined; 58 + let pronounsValue: Record<string, unknown> | undefined; 59 + let handle = did; 60 + 61 + for (const p of profiles) { 62 + if (p.did !== did) continue; 63 + if (p.handle && p.handle !== 'handle.invalid') handle = p.handle; 64 + 65 + const record = p.record as Record<string, unknown> | undefined; 66 + if (p.collection === 'app.bsky.actor.profile' && record) { 67 + bskyRecord = record; 68 + } 69 + if (p.collection === 'site.standard.publication' && record) { 70 + pubRecord = record; 71 + } 72 + if (p.collection === 'app.nearhorizon.actor.pronouns' && record) { 73 + pronounsValue = record; 74 + } 75 + } 76 + 77 + const avatar = bskyRecord?.avatar 78 + ? getCDNImageBlobUrl({ 79 + did, 80 + blob: bskyRecord.avatar as { $type: 'blob'; ref: { $link: string } } 81 + }) 82 + : undefined; 83 + 84 + const profile = { 85 + did: did as Did, 86 + handle: handle as `${string}.${string}`, 87 + displayName: (bskyRecord?.displayName as string) ?? handle, 88 + description: bskyRecord?.description as string | undefined, 89 + avatar 90 + } as AppBskyActorDefs.ProfileViewDetailed; 91 + 92 + const publication = pubRecord ? (pubRecord as WebsiteData['publication']) : undefined; 93 + 94 + const pronounsRecord = pronounsValue ? ({ value: pronounsValue } as PronounsRecord) : undefined; 95 + 96 + return { profile, publication, pronounsRecord }; 97 + } 98 + 99 + async function tryContrail<T>(label: string, fn: () => Promise<T | null>): Promise<T | null> { 32 100 try { 33 - const cachedResult = await cache?.getBlento(identifier); 101 + return await fn(); 102 + } catch (e) { 103 + console.error(`Contrail ${label} failed`, e); 104 + return null; 105 + } 106 + } 107 + 108 + /** 109 + * Fetch only the profile bundle (bsky profile + publication + pronouns) from contrail. 110 + * Used by single-card routes that don't need the full card list. 111 + */ 112 + function loadProfilesFromContrail(actor: ActorIdentifier, db: D1Database) { 113 + return tryContrail('getProfile', async () => { 114 + const res = await getServerClient(db).get('app.blento.getProfile', { params: { actor } }); 115 + if (!res.ok) return null; 116 + return (res.data.profiles ?? []) as ContrailProfile[]; 117 + }); 118 + } 119 + 120 + /** 121 + * Fetch a single card from contrail by DID + rkey. 122 + */ 123 + function loadCardFromContrail(did: Did, rkey: string, db: D1Database) { 124 + return tryContrail('card.getRecord', async () => { 125 + const uri = `at://${did}/app.blento.card/${rkey}` as const; 126 + const res = await getServerClient(db).get('app.blento.card.getRecord', { 127 + params: { uri } 128 + }); 129 + if (!res.ok) return null; 130 + return { ...(res.data.record as object) } as Item; 131 + }); 132 + } 133 + 134 + /** 135 + * Load cards for a single page + all pages for the actor from Contrail. 136 + * Filtering cards by page at query time means we only run additional-data loaders 137 + * for card types that actually appear on the current page. 138 + */ 139 + function loadFromContrail(actor: ActorIdentifier, db: D1Database, page: string) { 140 + return tryContrail('cards+pages query', async () => { 141 + const client = getServerClient(db); 142 + const [cardRes, pageRes] = await Promise.all([ 143 + client.get('app.blento.card.listRecords', { 144 + params: { actor, limit: 200, profiles: true, page } 145 + }), 146 + client.get('app.blento.page.listRecords', { 147 + params: { actor, limit: 200 } 148 + }) 149 + ]); 34 150 35 - if (!cachedResult) return; 36 - const result = JSON.parse(cachedResult); 151 + if (!cardRes.ok) return null; 37 152 38 - if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 39 - console.log('skipping cache because of version mismatch'); 40 - return; 41 - } 153 + const cards = cardRes.data.records.map((r) => ({ ...(r.record as object) }) as Item); 42 154 43 - result.page = 'blento.' + page; 155 + const pages = pageRes.ok 156 + ? pageRes.data.records 157 + .filter((r) => r.record) 158 + .map((r) => ({ 159 + uri: r.uri, 160 + cid: r.cid ?? '', 161 + value: r.record as Record<string, unknown> 162 + })) 163 + : []; 44 164 45 - result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( 46 - (v) => parseUri(v.uri)?.rkey === result.page 47 - )?.value; 48 - result.publication ??= { 49 - name: result.profile?.displayName || result.profile?.handle, 50 - description: result.profile?.description 165 + return { 166 + cards, 167 + pages, 168 + profiles: (cardRes.data.profiles ?? []) as ContrailProfile[] 51 169 }; 170 + }); 171 + } 52 172 53 - delete result['publications']; 173 + function defaultPublication( 174 + profile: WebsiteData['profile'] | undefined 175 + ): WebsiteData['publication'] { 176 + return { 177 + name: profile?.displayName || profile?.handle, 178 + description: profile?.description 179 + } as WebsiteData['publication']; 180 + } 54 181 55 - return checkData(result); 56 - } catch (error) { 57 - console.log('getting cached result failed', error); 58 - } 182 + function getPronounsFromPDS(did: Did) { 183 + return getRecord({ 184 + did, 185 + collection: 'app.nearhorizon.actor.pronouns', 186 + rkey: 'self' 187 + }).catch(() => undefined) as Promise<PronounsRecord | undefined>; 188 + } 189 + 190 + function getSelfPublicationFromPDS(did: Did) { 191 + return getRecord({ 192 + did, 193 + collection: 'site.standard.publication', 194 + rkey: 'blento.self' 195 + }).catch(() => undefined); 59 196 } 60 197 61 198 export async function loadData( 62 199 handle: ActorIdentifier, 63 200 cache: CacheService | undefined, 64 - forceUpdate: boolean = false, 65 201 page: string = 'self', 66 - env?: Record<string, string | undefined> 202 + env?: Record<string, string | undefined>, 203 + platform?: App.Platform 67 204 ): Promise<WebsiteData> { 68 205 if (!handle) throw error(404); 69 206 if (handle === 'favicon.ico') throw error(404); 70 207 71 - if (!forceUpdate) { 72 - const cachedResult = await getCache(handle, page, cache); 208 + const did = await resolveDid(handle); 209 + if (!did) throw error(404); 73 210 74 - if (cachedResult) return cachedResult; 75 - } 211 + const db = platform?.env?.DB; 212 + const fullPage = 'blento.' + page; 213 + const contrailData = db ? await loadFromContrail(handle, db, fullPage) : null; 76 214 77 - let did: Did | undefined = undefined; 78 - if (isHandle(handle)) { 79 - did = await resolveHandle({ handle }); 80 - } else if (isDid(handle)) { 81 - did = handle; 215 + let cards: Item[]; 216 + let pageRecords: Awaited<ReturnType<typeof listRecords>>; 217 + let profile: WebsiteData['profile']; 218 + let publication: WebsiteData['publication'] | undefined; 219 + let pronounsRecord: PronounsRecord | undefined; 220 + 221 + if (contrailData) { 222 + cards = contrailData.cards; 223 + pageRecords = contrailData.pages; 224 + 225 + const extracted = extractProfileData(did, contrailData.profiles); 226 + profile = extracted.profile; 227 + publication = extracted.publication; 228 + pronounsRecord = extracted.pronounsRecord; 82 229 } else { 83 - throw error(404); 230 + // Fallback: no D1 available (e.g. vite dev) — use PDS directly 231 + const [cardRecords, pageRecs, mainPub, prof, pronouns] = await Promise.all([ 232 + listRecords({ did, collection: 'app.blento.card', limit: 0 }).catch((e) => { 233 + console.error('error getting records for collection app.blento.card', e); 234 + return [] as Awaited<ReturnType<typeof listRecords>>; 235 + }), 236 + listRecords({ did, collection: 'app.blento.page' }).catch( 237 + () => [] as Awaited<ReturnType<typeof listRecords>> 238 + ), 239 + getSelfPublicationFromPDS(did), 240 + getDetailedProfile({ did }), 241 + getPronounsFromPDS(did) 242 + ]); 243 + 244 + cards = cardRecords.map((v) => ({ ...v.value }) as Item); 245 + pageRecords = pageRecs; 246 + profile = prof; 247 + publication = mainPub?.value as WebsiteData['publication'] | undefined; 248 + pronounsRecord = pronouns; 249 + } 250 + 251 + // If no publication found from contrail profiles, check page records 252 + if (!publication) { 253 + const pubFromPages = pageRecords.find((v) => parseUri(v.uri)?.rkey === 'blento.' + page); 254 + publication = pubFromPages?.value as WebsiteData['publication'] | undefined; 84 255 } 85 256 86 - const [cards, mainPublication, pages, profile, pronounsRecord] = await Promise.all([ 87 - listRecords({ did, collection: 'app.blento.card', limit: 0 }).catch((e) => { 88 - console.error('error getting records for collection app.blento.card', e); 89 - return [] as Awaited<ReturnType<typeof listRecords>>; 90 - }), 91 - getRecord({ 92 - did, 93 - collection: 'site.standard.publication', 94 - rkey: 'blento.self' 95 - }).catch(() => { 96 - console.error('error getting record for collection site.standard.publication'); 97 - return undefined; 98 - }), 99 - listRecords({ did, collection: 'app.blento.page' }).catch(() => { 100 - console.error('error getting records for collection app.blento.page'); 101 - return [] as Awaited<ReturnType<typeof listRecords>>; 102 - }), 103 - getDetailedProfile({ did }), 104 - getRecord({ 105 - did, 106 - collection: 'app.nearhorizon.actor.pronouns', 107 - rkey: 'self' 108 - }).catch(() => undefined) 109 - ]); 257 + publication ??= defaultPublication(profile); 110 258 111 - const additionalData = await loadAdditionalData( 112 - cards.map((v) => ({ ...v.value })) as Item[], 113 - { did, handle, cache }, 114 - env 115 - ); 259 + const additionalData = await loadAdditionalData(cards, { did, handle, cache, platform }, env); 116 260 117 - const result = { 261 + return checkData({ 118 262 page: 'blento.' + page, 119 263 handle, 120 264 did, 121 - cards: (cards.map((v) => { 122 - return { ...v.value }; 123 - }) ?? []) as Item[], 124 - publications: [mainPublication, ...pages].filter((v) => v), 265 + cards, 266 + publication, 125 267 additionalData, 126 268 profile, 127 269 pronouns: formatPronouns(pronounsRecord, profile), 128 - pronounsRecord: pronounsRecord as PronounsRecord | undefined, 270 + pronounsRecord, 129 271 updatedAt: Date.now(), 130 - version: CURRENT_CACHE_VERSION 131 - }; 132 - 133 - // Only cache results that have cards to avoid caching PDS errors 134 - if (result.cards.length > 0) { 135 - const stringifiedResult = JSON.stringify(result); 136 - await cache?.putBlento(did, handle as string, stringifiedResult); 137 - } 138 - 139 - const parsedResult = structuredClone(result) as any; 140 - 141 - parsedResult.publication = ( 142 - parsedResult.publications as Awaited<ReturnType<typeof listRecords>> 143 - ).find((v) => parseUri(v.uri)?.rkey === parsedResult.page)?.value; 144 - parsedResult.publication ??= { 145 - name: profile?.displayName || profile?.handle, 146 - description: profile?.description 147 - }; 148 - 149 - delete parsedResult['publications']; 150 - 151 - return checkData(parsedResult); 272 + version: 1 273 + }); 152 274 } 153 275 154 276 export async function loadCardData( 155 277 handle: ActorIdentifier, 156 278 rkey: string, 157 279 cache: CacheService | undefined, 158 - env?: Record<string, string | undefined> 280 + env?: Record<string, string | undefined>, 281 + platform?: App.Platform 159 282 ): Promise<WebsiteData> { 160 283 if (!handle) throw error(404); 161 284 if (handle === 'favicon.ico') throw error(404); 162 285 163 - let did: Did | undefined = undefined; 164 - if (isHandle(handle)) { 165 - did = await resolveHandle({ handle }); 166 - } else if (isDid(handle)) { 167 - did = handle; 168 - } else { 169 - throw error(404); 286 + const did = await resolveDid(handle); 287 + if (!did) throw error(404); 288 + 289 + const db = platform?.env?.DB; 290 + 291 + let cardValue: Item | undefined; 292 + let profile: WebsiteData['profile'] | undefined; 293 + let publication: WebsiteData['publication'] | undefined; 294 + let pronounsRecord: PronounsRecord | undefined; 295 + 296 + if (db) { 297 + const [card, profiles] = await Promise.all([ 298 + loadCardFromContrail(did, rkey, db), 299 + loadProfilesFromContrail(handle, db) 300 + ]); 301 + 302 + if (!card) throw error(404, 'Card not found'); 303 + cardValue = card; 304 + 305 + if (profiles) { 306 + const extracted = extractProfileData(did, profiles); 307 + profile = extracted.profile; 308 + publication = extracted.publication; 309 + pronounsRecord = extracted.pronounsRecord; 310 + } 170 311 } 171 312 172 - const [cardRecord, profile, pronounsRecord] = await Promise.all([ 173 - getRecord({ 174 - did, 175 - collection: 'app.blento.card', 176 - rkey 177 - }).catch(() => undefined), 178 - getDetailedProfile({ did }), 179 - getRecord({ 180 - did, 181 - collection: 'app.nearhorizon.actor.pronouns', 182 - rkey: 'self' 183 - }).catch(() => undefined) 184 - ]); 313 + if (!cardValue) { 314 + // Fallback: no D1 (e.g. vite dev) — fetch from PDS 315 + const [cardRecord, prof, pronouns] = await Promise.all([ 316 + getRecord({ did, collection: 'app.blento.card', rkey }).catch(() => undefined), 317 + getDetailedProfile({ did }), 318 + getPronounsFromPDS(did) 319 + ]); 185 320 186 - if (!cardRecord?.value) { 187 - throw error(404, 'Card not found'); 321 + if (!cardRecord?.value) throw error(404, 'Card not found'); 322 + cardValue = cardRecord.value as Item; 323 + profile = prof; 324 + pronounsRecord = pronouns; 188 325 } 189 326 190 - const card = migrateCard(structuredClone(cardRecord.value) as Item); 327 + if (!profile) throw error(404); 328 + 329 + const card = migrateCard(structuredClone(cardValue)); 191 330 const page = card.page ?? 'blento.self'; 192 331 193 - const publication = await getRecord({ 194 - did, 195 - collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page', 196 - rkey: page 197 - }).catch(() => undefined); 332 + // For non-self pages, publication comes from app.blento.page (not in contrail profiles). 333 + if (!publication || page !== 'blento.self') { 334 + const pubRecord = await getRecord({ 335 + did, 336 + collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page', 337 + rkey: page 338 + }).catch(() => undefined); 339 + if (pubRecord?.value) publication = pubRecord.value as WebsiteData['publication']; 340 + } 198 341 199 342 const cards = [card]; 200 343 const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did); 201 344 202 345 const additionalData = await loadAdditionalData( 203 346 cards, 204 - { did, handle: resolvedHandle, cache }, 347 + { did, handle: resolvedHandle, cache, platform }, 205 348 env 206 349 ); 207 350 208 - const result = { 351 + return { 209 352 page, 210 353 handle: resolvedHandle, 211 354 did, 212 355 cards, 213 - publication: 214 - publication?.value ?? 215 - ({ 216 - name: profile?.displayName || profile?.handle, 217 - description: profile?.description 218 - } as WebsiteData['publication']), 356 + publication: publication ?? defaultPublication(profile), 219 357 additionalData, 220 358 profile, 221 359 pronouns: formatPronouns(pronounsRecord, profile), 222 - pronounsRecord: pronounsRecord as PronounsRecord | undefined, 360 + pronounsRecord, 223 361 updatedAt: Date.now(), 224 - version: CURRENT_CACHE_VERSION 362 + version: 1 225 363 }; 226 - 227 - return result; 228 364 } 229 365 230 366 export async function loadCardTypeData( ··· 232 368 type: string, 233 369 cardData: Record<string, unknown>, 234 370 cache: CacheService | undefined, 235 - env?: Record<string, string | undefined> 371 + env?: Record<string, string | undefined>, 372 + platform?: App.Platform 236 373 ): Promise<WebsiteData> { 237 374 if (!handle) throw error(404); 238 375 if (handle === 'favicon.ico') throw error(404); ··· 242 379 throw error(404, 'Card type not found'); 243 380 } 244 381 245 - let did: Did | undefined = undefined; 246 - if (isHandle(handle)) { 247 - did = await resolveHandle({ handle }); 248 - } else if (isDid(handle)) { 249 - did = handle; 250 - } else { 251 - throw error(404); 382 + const did = await resolveDid(handle); 383 + if (!did) throw error(404); 384 + 385 + const db = platform?.env?.DB; 386 + 387 + let profile: WebsiteData['profile'] | undefined; 388 + let publication: WebsiteData['publication'] | undefined; 389 + let pronounsRecord: PronounsRecord | undefined; 390 + 391 + if (db) { 392 + const profiles = await loadProfilesFromContrail(handle, db); 393 + if (profiles) { 394 + const extracted = extractProfileData(did, profiles); 395 + profile = extracted.profile; 396 + publication = extracted.publication; 397 + pronounsRecord = extracted.pronounsRecord; 398 + } 399 + } 400 + 401 + if (!profile) { 402 + const [pubRecord, prof, pronouns] = await Promise.all([ 403 + getSelfPublicationFromPDS(did), 404 + getDetailedProfile({ did }), 405 + getPronounsFromPDS(did) 406 + ]); 407 + profile = prof; 408 + publication = pubRecord?.value as WebsiteData['publication'] | undefined; 409 + pronounsRecord = pronouns; 252 410 } 253 411 254 - const [publication, profile, pronounsRecord] = await Promise.all([ 255 - getRecord({ 256 - did, 257 - collection: 'site.standard.publication', 258 - rkey: 'blento.self' 259 - }).catch(() => undefined), 260 - getDetailedProfile({ did }), 261 - getRecord({ 262 - did, 263 - collection: 'app.nearhorizon.actor.pronouns', 264 - rkey: 'self' 265 - }).catch(() => undefined) 266 - ]); 412 + if (!profile) throw error(404); 267 413 268 414 const card = createEmptyCard('blento.self'); 269 415 card.cardType = type; ··· 279 425 280 426 const additionalData = await loadAdditionalData( 281 427 cards, 282 - { did, handle: resolvedHandle, cache }, 428 + { did, handle: resolvedHandle, cache, platform }, 283 429 env 284 430 ); 285 431 286 - const result = { 432 + return checkData({ 287 433 page: 'blento.self', 288 434 handle: resolvedHandle, 289 435 did, 290 436 cards, 291 - publication: 292 - publication?.value ?? 293 - ({ 294 - name: profile?.displayName || profile?.handle, 295 - description: profile?.description 296 - } as WebsiteData['publication']), 437 + publication: publication ?? defaultPublication(profile), 297 438 additionalData, 298 439 profile, 299 440 pronouns: formatPronouns(pronounsRecord, profile), 300 - pronounsRecord: pronounsRecord as PronounsRecord | undefined, 441 + pronounsRecord, 301 442 updatedAt: Date.now(), 302 - version: CURRENT_CACHE_VERSION 303 - }; 304 - 305 - return checkData(result); 443 + version: 1 444 + }); 306 445 } 307 446 308 447 function migrateCard(card: Item): Item { ··· 331 470 332 471 async function loadAdditionalData( 333 472 cards: Item[], 334 - { did, handle, cache }: { did: Did; handle: string; cache?: CacheService }, 473 + { 474 + did, 475 + handle, 476 + cache, 477 + platform 478 + }: { did: Did; handle: string; cache?: CacheService; platform?: App.Platform }, 335 479 env?: Record<string, string | undefined> 336 480 ) { 337 481 const cardTypes = new Set(cards.map((v) => v.cardType ?? '') as string[]); ··· 348 492 did, 349 493 handle, 350 494 cache, 351 - env 495 + env, 496 + platform 352 497 }); 353 498 } else if (cardDef?.loadData) { 354 499 additionDataPromises[cardType] = cardDef.loadData(items, { did, handle, cache });
+4 -12
src/routes/(auth)/oauth-client-metadata.json/+server.ts
··· 1 - import { metadata } from '$lib/atproto'; 2 1 import { json } from '@sveltejs/kit'; 2 + import { createOAuthClient } from '$lib/atproto/server/oauth'; 3 3 4 - export async function GET({ request }) { 4 + export async function GET({ request, platform }) { 5 5 const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 6 - 7 - if (customDomain) { 8 - const changedMetadata = metadata; 9 - changedMetadata.redirect_uris = changedMetadata.redirect_uris.map((s) => 10 - s.replace('blento.app', customDomain) 11 - ); 12 - changedMetadata.client_id = changedMetadata.client_id.replace('blento.app', customDomain); 13 - return json(changedMetadata); 14 - } 6 + const oauth = createOAuthClient(platform?.env, customDomain || undefined); 15 7 16 - return json(metadata); 8 + return json(oauth.metadata); 17 9 }
-54
src/routes/(auth)/oauth/callback/+page.svelte
··· 1 - <script lang="ts"> 2 - import { goto } from '$app/navigation'; 3 - import { user } from '$lib/atproto'; 4 - import { getHandleOrDid } from '$lib/atproto/methods'; 5 - 6 - let showError = $state(false); 7 - 8 - let startedErrorTimer = $state(); 9 - 10 - let hasRedirected = $state(false); 11 - 12 - $effect(() => { 13 - if (user.profile) { 14 - if (hasRedirected) return; 15 - 16 - let redirect = localStorage.getItem('login-redirect'); 17 - localStorage.removeItem('login-redirect'); 18 - 19 - const editPath = '/' + getHandleOrDid(user.profile) + '/edit'; 20 - if ( 21 - !redirect || 22 - redirect === '/' || 23 - redirect === 'https://blento.app' || 24 - redirect === 'https://blento.app/' 25 - ) { 26 - redirect = editPath; 27 - } 28 - 29 - goto(redirect, {}); 30 - 31 - hasRedirected = true; 32 - } 33 - 34 - if (!user.isInitializing && !startedErrorTimer) { 35 - startedErrorTimer = true; 36 - 37 - setTimeout(() => { 38 - showError = true; 39 - }, 5000); 40 - } 41 - }); 42 - </script> 43 - 44 - {#if !showError} 45 - <div class="flex min-h-screen w-full items-center justify-center text-3xl">Loading...</div> 46 - {:else} 47 - <div class="flex min-h-screen w-full items-center justify-center text-3xl"> 48 - <span class="max-w-xl text-center font-medium" 49 - >There was an error signing you in, please go back to the 50 - <a class="text-accent-600 dark:text-accent-400" href="/">homepage</a> 51 - and try again. 52 - </span> 53 - </div> 54 - {/if}
+37
src/routes/(auth)/oauth/callback/+server.ts
··· 1 + import { redirect } from '@sveltejs/kit'; 2 + import { createOAuthClient } from '$lib/atproto/server/oauth'; 3 + import { setSignedCookie } from '$lib/atproto/server/signed-cookie'; 4 + import { scopes } from '$lib/atproto/server/scopes'; 5 + import { getDetailedProfile } from '$lib/atproto/methods'; 6 + import { dev } from '$app/environment'; 7 + import type { Did } from '@atcute/lexicons'; 8 + import type { RequestHandler } from './$types'; 9 + 10 + export const GET: RequestHandler = async ({ url, platform, cookies, request }) => { 11 + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase() || undefined; 12 + const oauth = createOAuthClient(platform?.env, customDomain); 13 + 14 + let did: Did; 15 + try { 16 + const { session } = await oauth.callback(url.searchParams); 17 + did = session.did; 18 + 19 + const cookieOpts = { 20 + path: '/', 21 + httpOnly: true, 22 + secure: !dev, 23 + sameSite: 'lax' as const, 24 + maxAge: 60 * 60 * 24 * 180 // 180 days 25 + }; 26 + 27 + setSignedCookie(cookies, 'did', did, cookieOpts); 28 + setSignedCookie(cookies, 'scope', scopes.join(' '), cookieOpts); 29 + } catch (e) { 30 + console.error('OAuth callback failed:', e); 31 + redirect(303, '/?error=auth_failed'); 32 + } 33 + 34 + const profile = await getDetailedProfile({ did }).catch(() => undefined); 35 + const actor = profile?.handle && profile.handle !== 'handle.invalid' ? profile.handle : did; 36 + redirect(303, `/${actor}`); 37 + };
+8
src/routes/(auth)/oauth/jwks.json/+server.ts
··· 1 + import { json } from '@sveltejs/kit'; 2 + import { createOAuthClient } from '$lib/atproto/server/oauth'; 3 + import type { RequestHandler } from './$types'; 4 + 5 + export const GET: RequestHandler = async ({ platform }) => { 6 + const oauth = createOAuthClient(platform?.env); 7 + return json(oauth.jwks ?? { keys: [] }); 8 + };
+5 -2
src/routes/+layout.server.ts
··· 1 - export async function load({ request }) { 1 + export async function load({ request, locals, platform }) { 2 2 const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 3 3 4 - return { customDomain }; 4 + return { 5 + customDomain, 6 + authDid: locals.did 7 + }; 5 8 }
-6
src/routes/+layout.svelte
··· 3 3 4 4 import { Tooltip } from 'bits-ui'; 5 5 import { ThemeToggle, Toaster, toast } from '@foxui/core'; 6 - import { onMount } from 'svelte'; 7 - import { initClient } from '$lib/atproto'; 8 6 import YoutubeVideoPlayer, { videoPlayer } from '$lib/components/YoutubeVideoPlayer.svelte'; 9 7 import { page } from '$app/state'; 10 8 import { goto } from '$app/navigation'; ··· 20 18 const errorMessages: Record<string, (params: URLSearchParams) => string> = { 21 19 handle_not_found: (p) => `Handle ${p.get('handle') ?? ''} not found!` 22 20 }; 23 - 24 - onMount(() => { 25 - initClient({ customDomain: data.customDomain }); 26 - }); 27 21 </script> 28 22 29 23 <Tooltip.Provider delayDuration={300}>
+1 -1
src/routes/[[actor=actor]]/(pages)/+layout.server.ts
··· 15 15 throw error(404, 'Page not found'); 16 16 } 17 17 18 - return await loadData(actor, cache, false, params.page, env); 18 + return await loadData(actor, cache, params.page, env, platform); 19 19 }
-3
src/routes/[[actor=actor]]/(pages)/p/[[page]]/copy/+page.svelte
··· 153 153 } 154 154 } 155 155 156 - // Refresh the logged-in user's cache 157 - await fetch(`/${userHandle}/api/refresh`); 158 - 159 156 success = true; 160 157 161 158 // Redirect to the logged-in user's destination page edit
+1 -1
src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts
··· 14 14 throw error(404, 'Page not found'); 15 15 } 16 16 17 - const data = await loadData(actor, cache, false, params.page, env); 17 + const data = await loadData(actor, cache, params.page, env, platform); 18 18 19 19 if (!data.publication) throw error(300); 20 20
-21
src/routes/[[actor=actor]]/api/refresh/+server.ts
··· 1 - import { createCache } from '$lib/cache'; 2 - import { loadData } from '$lib/website/load.js'; 3 - import { env } from '$env/dynamic/private'; 4 - import { error, json } from '@sveltejs/kit'; 5 - import { getActor } from '$lib/actor'; 6 - 7 - export async function GET({ params, platform, request }) { 8 - const cache = createCache(platform); 9 - if (!cache) return json('no cache'); 10 - 11 - const actor = await getActor({ request, paramActor: params.actor, platform, blockBoth: false }); 12 - 13 - if (!actor) { 14 - throw error(404, 'Page not found'); 15 - } 16 - 17 - // Invalidate cached OG image so it gets regenerated 18 - cache.delete('og', actor).catch(() => {}); 19 - 20 - return json(await loadData(actor, cache, true, 'self', env)); 21 - }
+8 -11
src/routes/[[actor=actor]]/blog/new/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { user } from '$lib/atproto/auth.svelte'; 3 3 import { atProtoLoginModalState } from '@foxui/social'; 4 - import { uploadBlob, createTID } from '$lib/atproto/methods'; 4 + import { uploadBlob, createTID, putRecord } from '$lib/atproto/methods'; 5 5 import { compressImage } from '$lib/atproto/image-helper'; 6 6 import { Badge, Button } from '@foxui/core'; 7 7 import { goto } from '$app/navigation'; ··· 241 241 error = 'Title is required.'; 242 242 return; 243 243 } 244 - if (!user.client || !user.did) { 244 + if (!user.did) { 245 245 error = 'You must be logged in.'; 246 246 return; 247 247 } ··· 301 301 if (description.trim()) record.description = description.trim(); 302 302 if (coverImageBlob) record.coverImage = coverImageBlob; 303 303 304 - const response = await user.client.post('com.atproto.repo.createRecord', { 305 - input: { 306 - collection: 'site.standard.document', 307 - repo: user.did, 308 - rkey, 309 - record 310 - } 304 + const response = await putRecord({ 305 + collection: 'site.standard.document', 306 + rkey, 307 + record 311 308 }); 312 309 313 - if (response.ok) { 310 + if (response) { 314 311 clearDraft(); 315 312 const handle = 316 313 user.profile?.handle && user.profile.handle !== 'handle.invalid' ··· 374 371 375 372 <div class="min-h-screen px-6 py-12"> 376 373 <div class="mx-auto max-w-3xl"> 377 - {#if user.isInitializing || !draftRestored} 374 + {#if !draftRestored} 378 375 <div class="flex items-center gap-3"> 379 376 <div class="bg-base-300 dark:bg-base-700 size-5 animate-pulse rounded-full"></div> 380 377 <div class="bg-base-300 dark:bg-base-700 h-4 w-48 animate-pulse rounded"></div>
+1 -1
src/routes/[[actor=actor]]/card/[rkey]/+page.server.ts
··· 12 12 throw error(404, 'Page not found'); 13 13 } 14 14 15 - return await loadCardData(actor, params.rkey, cache, env); 15 + return await loadCardData(actor, params.rkey, cache, env, platform); 16 16 }
+2 -1
src/routes/[[actor=actor]]/card/[rkey]/type/[type]/+page.server.ts
··· 54 54 params.type, 55 55 getCardDataFromSearchParams(url.searchParams), 56 56 cache, 57 - env 57 + env, 58 + platform 58 59 ); 59 60 }
+1 -1
src/routes/[[actor=actor]]/og-new.png/+server.ts
··· 64 64 height: 630, 65 65 deviceScaleFactor: 2 66 66 }, 67 - waitForTimeout: 1000 67 + waitForTimeout: 3000 68 68 }) 69 69 } 70 70 );
+15
src/routes/api/cron/+server.ts
··· 1 + import { contrail, ensureInit } from '$lib/contrail'; 2 + import type { RequestHandler } from './$types'; 3 + 4 + export const POST: RequestHandler = async ({ request, platform }) => { 5 + const secret = request.headers.get('X-Cron-Secret'); 6 + if (secret !== platform!.env.CRON_SECRET) { 7 + return new Response('Unauthorized', { status: 401 }); 8 + } 9 + 10 + const db = platform!.env.DB; 11 + await ensureInit(db); 12 + await contrail.ingest({}, db); 13 + 14 + return new Response('OK'); 15 + };
+14
src/routes/xrpc/[...path]/+server.ts
··· 1 + import { createHandler } from '@atmo-dev/contrail/server'; 2 + import { contrail, ensureInit } from '$lib/contrail'; 3 + import type { RequestHandler } from './$types'; 4 + 5 + const handle = createHandler(contrail); 6 + 7 + async function handler(request: Request, platform: App.Platform | undefined) { 8 + const db = platform!.env.DB; 9 + await ensureInit(db); 10 + return handle(request, db) as Promise<Response>; 11 + } 12 + 13 + export const GET: RequestHandler = async ({ request, platform }) => handler(request, platform); 14 + export const POST: RequestHandler = async ({ request, platform }) => handler(request, platform);
+21 -8
wrangler.jsonc
··· 7 7 "name": "blento", 8 8 "main": ".svelte-kit/cloudflare/_worker.js", 9 9 "compatibility_date": "2025-12-25", 10 - "compatibility_flags": ["nodejs_als"], 10 + "compatibility_flags": ["nodejs_als", "nodejs_compat"], 11 11 "assets": { 12 12 "binding": "ASSETS", 13 13 "directory": ".svelte-kit/cloudflare" ··· 46 46 { 47 47 "binding": "CUSTOM_DOMAINS", 48 48 "id": "f449b3b5c8a349478405e2c04ed265f0" 49 + }, 50 + { 51 + "binding": "OAUTH_SESSIONS", 52 + "id": "4d872b4630a042d69ecd63f87d6b2094" 53 + }, 54 + { 55 + "binding": "OAUTH_STATES", 56 + "id": "c644b498d4af4c5892b3106d52c7760a" 49 57 } 50 - ] 51 - 52 - /** 53 - * Service Bindings (communicate between multiple Workers) 54 - * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 55 - */ 56 - // "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ] 58 + ], 59 + "d1_databases": [ 60 + { 61 + "binding": "DB", 62 + "database_name": "blento", 63 + "database_id": "922639e7-6321-42c8-a4bd-cdf48428fdac", 64 + "remote": true 65 + } 66 + ], 67 + "triggers": { 68 + "crons": ["*/5 * * * *"] 69 + } 57 70 }