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

Configure Feed

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

all these changes

Florian a95f6b4e 54253064

+3398 -795
+3 -1
.gitignore
··· 22 22 vite.config.js.timestamp-* 23 23 vite.config.ts.timestamp-* 24 24 25 - references 25 + references 26 + 27 + sveltekit-cloudflare-workers
+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 + });
+106
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": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the record" 18 + }, 19 + "profiles": { 20 + "type": "boolean", 21 + "description": "Include profile + identity info keyed by DID" 22 + } 23 + } 24 + }, 25 + "output": { 26 + "encoding": "application/json", 27 + "schema": { 28 + "type": "object", 29 + "required": [ 30 + "uri", 31 + "did", 32 + "collection", 33 + "rkey", 34 + "time_us" 35 + ], 36 + "properties": { 37 + "uri": { 38 + "type": "string", 39 + "format": "at-uri" 40 + }, 41 + "did": { 42 + "type": "string", 43 + "format": "did" 44 + }, 45 + "collection": { 46 + "type": "string", 47 + "format": "nsid" 48 + }, 49 + "rkey": { 50 + "type": "string" 51 + }, 52 + "cid": { 53 + "type": "string" 54 + }, 55 + "record": { 56 + "type": "ref", 57 + "ref": "app.blento.card#main" 58 + }, 59 + "time_us": { 60 + "type": "integer" 61 + }, 62 + "profiles": { 63 + "type": "array", 64 + "items": { 65 + "type": "ref", 66 + "ref": "#profileEntry" 67 + } 68 + } 69 + } 70 + } 71 + } 72 + }, 73 + "profileEntry": { 74 + "type": "object", 75 + "required": [ 76 + "did" 77 + ], 78 + "properties": { 79 + "did": { 80 + "type": "string", 81 + "format": "did" 82 + }, 83 + "handle": { 84 + "type": "string" 85 + }, 86 + "uri": { 87 + "type": "string", 88 + "format": "at-uri" 89 + }, 90 + "collection": { 91 + "type": "string", 92 + "format": "nsid" 93 + }, 94 + "rkey": { 95 + "type": "string" 96 + }, 97 + "cid": { 98 + "type": "string" 99 + }, 100 + "record": { 101 + "type": "unknown" 102 + } 103 + } 104 + } 105 + } 106 + }
+249
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": [ 143 + "asc", 144 + "desc" 145 + ], 146 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 147 + } 148 + } 149 + }, 150 + "output": { 151 + "encoding": "application/json", 152 + "schema": { 153 + "type": "object", 154 + "required": [ 155 + "records" 156 + ], 157 + "properties": { 158 + "records": { 159 + "type": "array", 160 + "items": { 161 + "type": "ref", 162 + "ref": "#record" 163 + } 164 + }, 165 + "cursor": { 166 + "type": "string" 167 + }, 168 + "profiles": { 169 + "type": "array", 170 + "items": { 171 + "type": "ref", 172 + "ref": "#profileEntry" 173 + } 174 + } 175 + } 176 + } 177 + } 178 + }, 179 + "record": { 180 + "type": "object", 181 + "required": [ 182 + "uri", 183 + "did", 184 + "collection", 185 + "rkey", 186 + "time_us" 187 + ], 188 + "properties": { 189 + "uri": { 190 + "type": "string", 191 + "format": "at-uri" 192 + }, 193 + "did": { 194 + "type": "string", 195 + "format": "did" 196 + }, 197 + "collection": { 198 + "type": "string", 199 + "format": "nsid" 200 + }, 201 + "rkey": { 202 + "type": "string" 203 + }, 204 + "cid": { 205 + "type": "string" 206 + }, 207 + "record": { 208 + "type": "ref", 209 + "ref": "app.blento.card#main" 210 + }, 211 + "time_us": { 212 + "type": "integer" 213 + } 214 + } 215 + }, 216 + "profileEntry": { 217 + "type": "object", 218 + "required": [ 219 + "did" 220 + ], 221 + "properties": { 222 + "did": { 223 + "type": "string", 224 + "format": "did" 225 + }, 226 + "handle": { 227 + "type": "string" 228 + }, 229 + "uri": { 230 + "type": "string", 231 + "format": "at-uri" 232 + }, 233 + "collection": { 234 + "type": "string", 235 + "format": "nsid" 236 + }, 237 + "rkey": { 238 + "type": "string" 239 + }, 240 + "cid": { 241 + "type": "string" 242 + }, 243 + "record": { 244 + "type": "unknown" 245 + } 246 + } 247 + } 248 + } 249 + }
+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 + }
+51
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": [ 13 + "total_records", 14 + "collections" 15 + ], 16 + "properties": { 17 + "total_records": { 18 + "type": "integer" 19 + }, 20 + "collections": { 21 + "type": "array", 22 + "items": { 23 + "type": "ref", 24 + "ref": "#collectionStats" 25 + } 26 + } 27 + } 28 + } 29 + } 30 + }, 31 + "collectionStats": { 32 + "type": "object", 33 + "required": [ 34 + "collection", 35 + "records", 36 + "unique_users" 37 + ], 38 + "properties": { 39 + "collection": { 40 + "type": "string" 41 + }, 42 + "records": { 43 + "type": "integer" 44 + }, 45 + "unique_users": { 46 + "type": "integer" 47 + } 48 + } 49 + } 50 + } 51 + }
+73
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": [ 11 + "actor" 12 + ], 13 + "properties": { 14 + "actor": { 15 + "type": "string", 16 + "format": "at-identifier", 17 + "description": "DID or handle of the user" 18 + } 19 + } 20 + }, 21 + "output": { 22 + "encoding": "application/json", 23 + "schema": { 24 + "type": "object", 25 + "required": [ 26 + "profiles" 27 + ], 28 + "properties": { 29 + "profiles": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "#profileEntry" 34 + } 35 + } 36 + } 37 + } 38 + } 39 + }, 40 + "profileEntry": { 41 + "type": "object", 42 + "required": [ 43 + "did" 44 + ], 45 + "properties": { 46 + "did": { 47 + "type": "string", 48 + "format": "did" 49 + }, 50 + "handle": { 51 + "type": "string" 52 + }, 53 + "uri": { 54 + "type": "string", 55 + "format": "at-uri" 56 + }, 57 + "collection": { 58 + "type": "string", 59 + "format": "nsid" 60 + }, 61 + "rkey": { 62 + "type": "string" 63 + }, 64 + "cid": { 65 + "type": "string" 66 + }, 67 + "record": { 68 + "type": "unknown" 69 + } 70 + } 71 + } 72 + } 73 + }
+59
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": [ 35 + "indexed", 36 + "deleted" 37 + ], 38 + "properties": { 39 + "indexed": { 40 + "type": "integer", 41 + "description": "Number of records created or updated" 42 + }, 43 + "deleted": { 44 + "type": "integer", 45 + "description": "Number of records deleted (not found on PDS)" 46 + }, 47 + "errors": { 48 + "type": "array", 49 + "items": { 50 + "type": "string" 51 + }, 52 + "description": "Errors for individual URIs that could not be processed" 53 + } 54 + } 55 + } 56 + } 57 + } 58 + } 59 + }
+106
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": [ 11 + "uri" 12 + ], 13 + "properties": { 14 + "uri": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "AT URI of the record" 18 + }, 19 + "profiles": { 20 + "type": "boolean", 21 + "description": "Include profile + identity info keyed by DID" 22 + } 23 + } 24 + }, 25 + "output": { 26 + "encoding": "application/json", 27 + "schema": { 28 + "type": "object", 29 + "required": [ 30 + "uri", 31 + "did", 32 + "collection", 33 + "rkey", 34 + "time_us" 35 + ], 36 + "properties": { 37 + "uri": { 38 + "type": "string", 39 + "format": "at-uri" 40 + }, 41 + "did": { 42 + "type": "string", 43 + "format": "did" 44 + }, 45 + "collection": { 46 + "type": "string", 47 + "format": "nsid" 48 + }, 49 + "rkey": { 50 + "type": "string" 51 + }, 52 + "cid": { 53 + "type": "string" 54 + }, 55 + "record": { 56 + "type": "ref", 57 + "ref": "app.blento.page#main" 58 + }, 59 + "time_us": { 60 + "type": "integer" 61 + }, 62 + "profiles": { 63 + "type": "array", 64 + "items": { 65 + "type": "ref", 66 + "ref": "#profileEntry" 67 + } 68 + } 69 + } 70 + } 71 + } 72 + }, 73 + "profileEntry": { 74 + "type": "object", 75 + "required": [ 76 + "did" 77 + ], 78 + "properties": { 79 + "did": { 80 + "type": "string", 81 + "format": "did" 82 + }, 83 + "handle": { 84 + "type": "string" 85 + }, 86 + "uri": { 87 + "type": "string", 88 + "format": "at-uri" 89 + }, 90 + "collection": { 91 + "type": "string", 92 + "format": "nsid" 93 + }, 94 + "rkey": { 95 + "type": "string" 96 + }, 97 + "cid": { 98 + "type": "string" 99 + }, 100 + "record": { 101 + "type": "unknown" 102 + } 103 + } 104 + } 105 + } 106 + }
+154
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": [ 40 + "name", 41 + "description" 42 + ], 43 + "description": "Field to sort by (default: time_us)" 44 + }, 45 + "order": { 46 + "type": "string", 47 + "knownValues": [ 48 + "asc", 49 + "desc" 50 + ], 51 + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" 52 + } 53 + } 54 + }, 55 + "output": { 56 + "encoding": "application/json", 57 + "schema": { 58 + "type": "object", 59 + "required": [ 60 + "records" 61 + ], 62 + "properties": { 63 + "records": { 64 + "type": "array", 65 + "items": { 66 + "type": "ref", 67 + "ref": "#record" 68 + } 69 + }, 70 + "cursor": { 71 + "type": "string" 72 + }, 73 + "profiles": { 74 + "type": "array", 75 + "items": { 76 + "type": "ref", 77 + "ref": "#profileEntry" 78 + } 79 + } 80 + } 81 + } 82 + } 83 + }, 84 + "record": { 85 + "type": "object", 86 + "required": [ 87 + "uri", 88 + "did", 89 + "collection", 90 + "rkey", 91 + "time_us" 92 + ], 93 + "properties": { 94 + "uri": { 95 + "type": "string", 96 + "format": "at-uri" 97 + }, 98 + "did": { 99 + "type": "string", 100 + "format": "did" 101 + }, 102 + "collection": { 103 + "type": "string", 104 + "format": "nsid" 105 + }, 106 + "rkey": { 107 + "type": "string" 108 + }, 109 + "cid": { 110 + "type": "string" 111 + }, 112 + "record": { 113 + "type": "ref", 114 + "ref": "app.blento.page#main" 115 + }, 116 + "time_us": { 117 + "type": "integer" 118 + } 119 + } 120 + }, 121 + "profileEntry": { 122 + "type": "object", 123 + "required": [ 124 + "did" 125 + ], 126 + "properties": { 127 + "did": { 128 + "type": "string", 129 + "format": "did" 130 + }, 131 + "handle": { 132 + "type": "string" 133 + }, 134 + "uri": { 135 + "type": "string", 136 + "format": "at-uri" 137 + }, 138 + "collection": { 139 + "type": "string", 140 + "format": "nsid" 141 + }, 142 + "rkey": { 143 + "type": "string" 144 + }, 145 + "cid": { 146 + "type": "string" 147 + }, 148 + "record": { 149 + "type": "unknown" 150 + } 151 + } 152 + } 153 + } 154 + }
+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", "mobileW", "mobileH", "mobileX", "mobileY", "cardType", "cardData"], 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 + }
+13 -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", ··· 13 13 "lint": "prettier --check . && eslint .", 14 14 "format": "eslint --fix . && prettier --write .", 15 15 "deploy": "pnpm run build && wrangler deploy", 16 - "cf-typegen": "wrangler types ./src/worker-configuration.d.ts" 16 + "cf-typegen": "wrangler types ./src/worker-configuration.d.ts", 17 + "env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", 18 + "env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", 19 + "env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts", 20 + "tunnel": "npx tsx src/lib/atproto/scripts/tunnel.ts", 21 + "generate": "npx tsx scripts/generate.ts", 22 + "sync": "npx tsx scripts/sync.ts", 23 + "sync:remote": "npx tsx scripts/sync.ts --remote" 17 24 }, 18 25 "devDependencies": { 26 + "@atcute/lex-cli": "^2.6.1", 27 + "@atcute/lexicon-doc": "^2.1.2", 19 28 "@eslint/compat": "^2.0.3", 20 29 "@eslint/js": "^10.0.1", 21 30 "@sveltejs/adapter-cloudflare": "^7.2.8", ··· 49 58 "@atcute/identity-resolver": "^1.2.2", 50 59 "@atcute/lexicons": "^1.2.9", 51 60 "@atcute/oauth-browser-client": "^3.0.0", 61 + "@atcute/oauth-node-client": "^1.1.0", 52 62 "@atcute/standard-site": "^1.0.1", 53 63 "@atcute/tid": "^1.1.2", 64 + "@atmo-dev/contrail": "^0.0.8", 54 65 "@cloudflare/workers-types": "^4.20260313.1", 55 66 "@ethercorps/sveltekit-og": "^4.2.1", 56 67 "@floating-ui/dom": "^1.7.6",
+278
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 ··· 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.6.1 213 + '@atcute/lexicon-doc': 214 + specifier: ^2.1.2 215 + version: 2.1.2 204 216 '@eslint/compat': 205 217 specifier: ^2.0.3 206 218 version: 2.0.3(eslint@10.0.3(jiti@2.6.1)) ··· 285 297 '@atcute/bluesky@3.3.0': 286 298 resolution: {integrity: sha512-TrLnlxuD6F/D2ZYzJ3aCiRD0yiFuhmVsd6oULNzzr8V9Xzlufg0yxkRiGmbMiF2iI508y/MFi6vzo625301c5A==} 287 299 300 + '@atcute/car@5.1.1': 301 + resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} 302 + 303 + '@atcute/cbor@2.3.2': 304 + resolution: {integrity: sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ==} 305 + 306 + '@atcute/cid@2.4.1': 307 + resolution: {integrity: sha512-bwhna69RCv7yetXudtj+2qrMPYvhhIQqvJz6YUpUS98v7OdF3X2dnye9Nig2NDrklZcuyOsu7sQo7GOykJXRLQ==} 308 + 288 309 '@atcute/client@4.2.1': 289 310 resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} 311 + 312 + '@atcute/crypto@2.4.1': 313 + resolution: {integrity: sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==} 290 314 291 315 '@atcute/identity-resolver@1.2.2': 292 316 resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==} ··· 296 320 '@atcute/identity@1.1.3': 297 321 resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 298 322 323 + '@atcute/identity@1.1.4': 324 + resolution: {integrity: sha512-RCw1IqflfuSYCxK5m0lZCm0UnvIzcUnuhngiBhJEJb9a9Mc2SEf1xP3H8N5r8pvEH1LoAYd6/zrvCNU+uy9esw==} 325 + 326 + '@atcute/jetstream@1.1.2': 327 + resolution: {integrity: sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==} 328 + 329 + '@atcute/lex-cli@2.6.1': 330 + resolution: {integrity: sha512-eSF2/ANOfegrDsbOi4iLbdCsubCBtRigoyShOix0wBCm1TVc7L7QsTgkZAy8Tet+spT1zPVVysPLlywUzMYSYw==} 331 + hasBin: true 332 + 333 + '@atcute/lexicon-doc@2.1.2': 334 + resolution: {integrity: sha512-jTLcOka7b8BIn2SnIZm2m7l6unlJ0gpgW1MnRpSqNbly/AvyRUR/GREduh/QmjT4SGasDm8vdhrM0kOSPFpDLQ==} 335 + 336 + '@atcute/lexicon-resolver@0.1.6': 337 + resolution: {integrity: sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw==} 338 + peerDependencies: 339 + '@atcute/identity': ^1.1.0 340 + '@atcute/identity-resolver': ^1.1.3 341 + 342 + '@atcute/lexicons@1.2.10': 343 + resolution: {integrity: sha512-0EfRDQQjOgb06VSFOUWXLnqKY11ljWB2bXS3cJVPYJp0jTWudgRp6OTW4vReNAeVZaY4kVr2ud/I/Zn9mjix3g==} 344 + 299 345 '@atcute/lexicons@1.2.9': 300 346 resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==} 347 + 348 + '@atcute/mst@1.0.0': 349 + resolution: {integrity: sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==} 301 350 302 351 '@atcute/multibase@1.1.8': 303 352 resolution: {integrity: sha512-pJgtImMZKCjqwRbu+2GzB+4xQjKBXDwdZOzeqe0u97zYKRGftpGYGvYv3+pMe2xXe+msDyu7Nv8iJp+U14otTA==} 304 353 354 + '@atcute/multibase@1.2.0': 355 + resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} 356 + 305 357 '@atcute/oauth-browser-client@3.0.0': 306 358 resolution: {integrity: sha512-7AbKV8tTe7aRJNJV7gCcWHSVEADb2nr58O1p7dQsf73HSe9pvlBkj/Vk1yjjtH691uAVYkwhHSh0bC7D8XdwJw==} 307 359 ··· 311 363 '@atcute/oauth-keyset@0.1.0': 312 364 resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==} 313 365 366 + '@atcute/oauth-node-client@1.1.0': 367 + resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==} 368 + 314 369 '@atcute/oauth-types@0.1.1': 315 370 resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} 371 + 372 + '@atcute/repo@0.1.4': 373 + resolution: {integrity: sha512-uzbGJkE+1A8UFviosJrtw7HW87u8nCCH1V3yOQ79FPrRhS67EvEHF6GTg4aMkP21ze/pRtttJ1k9pFfDmyTlTg==} 316 374 317 375 '@atcute/standard-site@1.0.1': 318 376 resolution: {integrity: sha512-wL4ZAvbe3p7NxC92rRgc6vbd+0feNDEAfEcBLA+68KDTUtmtEko5lr09R31P7AWlN4MVTMRj5iLb9UaTThIzWw==} ··· 332 390 '@atcute/util-text@1.1.1': 333 391 resolution: {integrity: sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w==} 334 392 393 + '@atcute/util-text@1.2.0': 394 + resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==} 395 + 396 + '@atcute/varint@2.0.0': 397 + resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==} 398 + 399 + '@atmo-dev/contrail@0.0.8': 400 + resolution: {integrity: sha512-sXtdd3Z8VNVoSinrX3ww978ctxtHBl0bX5tP51XOY0IWKJ4xl9zqkPOIIBMzbVE3IyU2Vq2B9Whi3VAhyd2Qdg==} 401 + peerDependencies: 402 + pg: ^8.0.0 403 + peerDependenciesMeta: 404 + pg: 405 + optional: true 406 + 335 407 '@badrap/valita@0.4.6': 336 408 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 337 409 engines: {node: '>= 18'} ··· 869 941 '@maplibre/vt-pbf@4.3.0': 870 942 resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==} 871 943 944 + '@mary-ext/event-iterator@1.0.0': 945 + resolution: {integrity: sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==} 946 + 947 + '@mary-ext/simple-event-emitter@1.0.1': 948 + resolution: {integrity: sha512-9+VvZisxZ/gSg+JJH7hmXaA8Qj42Qjz3O58RSB+INYc8iLA0icATZxHB9vKbj59ojDGZjO3hCKzMXocx3L0H8w==} 949 + 872 950 '@mixmark-io/domino@2.2.0': 873 951 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} 874 952 875 953 '@napi-rs/wasm-runtime@1.1.1': 876 954 resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 877 955 956 + '@noble/secp256k1@3.0.0': 957 + resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} 958 + 878 959 '@number-flow/svelte@0.4.0': 879 960 resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==} 880 961 peerDependencies: 881 962 svelte: ^4 || ^5 882 963 964 + '@optique/core@0.10.7': 965 + resolution: {integrity: sha512-FwSX8ILFqzcCqZi6Xetsa4flJp/yyqFG4d4eFD98BtqdzxxuylzdrKvsXj/ow8mcoVjYkTuaIkqHSBxonqMcQg==} 966 + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} 967 + 968 + '@optique/run@0.10.7': 969 + resolution: {integrity: sha512-1CVdH8uyptj1nFGS2MLacSmZceRClez4LD/G/Gm38wrAVnJq6I+9Fvyh2bVHErsZLQzR0a12CYMUWIgDKY3X1w==} 970 + engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} 971 + 883 972 '@oxc-project/runtime@0.115.0': 884 973 resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} 885 974 engines: {node: ^20.19.0 || >=22.12.0} ··· 1845 1934 resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 1846 1935 engines: {node: '>=0.10.0'} 1847 1936 1937 + event-target-polyfill@0.0.4: 1938 + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} 1939 + 1848 1940 exsolve@1.0.8: 1849 1941 resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} 1850 1942 ··· 1926 2018 1927 2019 hls.js@1.6.15: 1928 2020 resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} 2021 + 2022 + hono@4.12.12: 2023 + resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} 2024 + engines: {node: '>=16.9.0'} 1929 2025 1930 2026 htmlparser2@10.1.0: 1931 2027 resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} ··· 2314 2410 parse5@7.3.0: 2315 2411 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} 2316 2412 2413 + partysocket@1.1.16: 2414 + resolution: {integrity: sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w==} 2415 + peerDependencies: 2416 + react: '>=17' 2417 + peerDependenciesMeta: 2418 + react: 2419 + optional: true 2420 + 2317 2421 path-exists@4.0.0: 2318 2422 resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 2319 2423 engines: {node: '>=8'} ··· 2850 2954 resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 2851 2955 engines: {node: '>= 0.8.0'} 2852 2956 2957 + type-fest@4.41.0: 2958 + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} 2959 + engines: {node: '>=16'} 2960 + 2853 2961 typescript-eslint@8.57.0: 2854 2962 resolution: {integrity: sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==} 2855 2963 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} ··· 3042 3150 resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 3043 3151 engines: {node: '>=10'} 3044 3152 3153 + yocto-queue@1.2.2: 3154 + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} 3155 + engines: {node: '>=12.20'} 3156 + 3045 3157 yoga-wasm-web@0.3.3: 3046 3158 resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} 3047 3159 ··· 3069 3181 '@atcute/atproto': 3.1.10 3070 3182 '@atcute/lexicons': 1.2.9 3071 3183 3184 + '@atcute/car@5.1.1': 3185 + dependencies: 3186 + '@atcute/cbor': 2.3.2 3187 + '@atcute/cid': 2.4.1 3188 + '@atcute/uint8array': 1.1.1 3189 + '@atcute/varint': 2.0.0 3190 + 3191 + '@atcute/cbor@2.3.2': 3192 + dependencies: 3193 + '@atcute/cid': 2.4.1 3194 + '@atcute/multibase': 1.1.8 3195 + '@atcute/uint8array': 1.1.1 3196 + 3197 + '@atcute/cid@2.4.1': 3198 + dependencies: 3199 + '@atcute/multibase': 1.1.8 3200 + '@atcute/uint8array': 1.1.1 3201 + 3072 3202 '@atcute/client@4.2.1': 3073 3203 dependencies: 3074 3204 '@atcute/identity': 1.1.3 3075 3205 '@atcute/lexicons': 1.2.9 3076 3206 3207 + '@atcute/crypto@2.4.1': 3208 + dependencies: 3209 + '@atcute/multibase': 1.2.0 3210 + '@atcute/uint8array': 1.1.1 3211 + '@noble/secp256k1': 3.0.0 3212 + 3077 3213 '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3)': 3078 3214 dependencies: 3079 3215 '@atcute/identity': 1.1.3 ··· 3081 3217 '@atcute/util-fetch': 1.0.5 3082 3218 '@badrap/valita': 0.4.6 3083 3219 3220 + '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4)': 3221 + dependencies: 3222 + '@atcute/identity': 1.1.4 3223 + '@atcute/lexicons': 1.2.9 3224 + '@atcute/util-fetch': 1.0.5 3225 + '@badrap/valita': 0.4.6 3226 + 3084 3227 '@atcute/identity@1.1.3': 3085 3228 dependencies: 3086 3229 '@atcute/lexicons': 1.2.9 3087 3230 '@badrap/valita': 0.4.6 3088 3231 3232 + '@atcute/identity@1.1.4': 3233 + dependencies: 3234 + '@atcute/lexicons': 1.2.10 3235 + '@badrap/valita': 0.4.6 3236 + 3237 + '@atcute/jetstream@1.1.2(react@19.2.4)': 3238 + dependencies: 3239 + '@atcute/lexicons': 1.2.10 3240 + '@badrap/valita': 0.4.6 3241 + '@mary-ext/event-iterator': 1.0.0 3242 + '@mary-ext/simple-event-emitter': 1.0.1 3243 + partysocket: 1.1.16(react@19.2.4) 3244 + type-fest: 4.41.0 3245 + yocto-queue: 1.2.2 3246 + transitivePeerDependencies: 3247 + - react 3248 + 3249 + '@atcute/lex-cli@2.6.1': 3250 + dependencies: 3251 + '@atcute/identity': 1.1.4 3252 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) 3253 + '@atcute/lexicon-doc': 2.1.2 3254 + '@atcute/lexicon-resolver': 0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.4) 3255 + '@atcute/lexicons': 1.2.10 3256 + '@badrap/valita': 0.4.6 3257 + '@optique/core': 0.10.7 3258 + '@optique/run': 0.10.7 3259 + picocolors: 1.1.1 3260 + prettier: 3.8.1 3261 + 3262 + '@atcute/lexicon-doc@2.1.2': 3263 + dependencies: 3264 + '@atcute/identity': 1.1.3 3265 + '@atcute/lexicons': 1.2.9 3266 + '@atcute/uint8array': 1.1.1 3267 + '@atcute/util-text': 1.1.1 3268 + '@badrap/valita': 0.4.6 3269 + 3270 + '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3))(@atcute/identity@1.1.4)': 3271 + dependencies: 3272 + '@atcute/crypto': 2.4.1 3273 + '@atcute/identity': 1.1.4 3274 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 3275 + '@atcute/lexicon-doc': 2.1.2 3276 + '@atcute/lexicons': 1.2.10 3277 + '@atcute/repo': 0.1.4 3278 + '@atcute/util-fetch': 1.0.5 3279 + '@badrap/valita': 0.4.6 3280 + 3281 + '@atcute/lexicons@1.2.10': 3282 + dependencies: 3283 + '@atcute/uint8array': 1.1.1 3284 + '@atcute/util-text': 1.2.0 3285 + '@standard-schema/spec': 1.1.0 3286 + esm-env: 1.2.2 3287 + 3089 3288 '@atcute/lexicons@1.2.9': 3090 3289 dependencies: 3091 3290 '@atcute/uint8array': 1.1.1 ··· 3093 3292 '@standard-schema/spec': 1.1.0 3094 3293 esm-env: 1.2.2 3095 3294 3295 + '@atcute/mst@1.0.0': 3296 + dependencies: 3297 + '@atcute/cbor': 2.3.2 3298 + '@atcute/cid': 2.4.1 3299 + '@atcute/uint8array': 1.1.1 3300 + 3096 3301 '@atcute/multibase@1.1.8': 3302 + dependencies: 3303 + '@atcute/uint8array': 1.1.1 3304 + 3305 + '@atcute/multibase@1.2.0': 3097 3306 dependencies: 3098 3307 '@atcute/uint8array': 1.1.1 3099 3308 ··· 3120 3329 dependencies: 3121 3330 '@atcute/oauth-crypto': 0.1.0 3122 3331 3332 + '@atcute/oauth-node-client@1.1.0': 3333 + dependencies: 3334 + '@atcute/client': 4.2.1 3335 + '@atcute/identity': 1.1.3 3336 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 3337 + '@atcute/lexicons': 1.2.9 3338 + '@atcute/oauth-crypto': 0.1.0 3339 + '@atcute/oauth-keyset': 0.1.0 3340 + '@atcute/oauth-types': 0.1.1 3341 + '@atcute/util-fetch': 1.0.5 3342 + '@badrap/valita': 0.4.6 3343 + nanoid: 5.1.6 3344 + 3123 3345 '@atcute/oauth-types@0.1.1': 3124 3346 dependencies: 3125 3347 '@atcute/identity': 1.1.3 ··· 3127 3349 '@atcute/oauth-keyset': 0.1.0 3128 3350 '@badrap/valita': 0.4.6 3129 3351 3352 + '@atcute/repo@0.1.4': 3353 + dependencies: 3354 + '@atcute/car': 5.1.1 3355 + '@atcute/cbor': 2.3.2 3356 + '@atcute/cid': 2.4.1 3357 + '@atcute/crypto': 2.4.1 3358 + '@atcute/lexicons': 1.2.10 3359 + '@atcute/mst': 1.0.0 3360 + '@atcute/uint8array': 1.1.1 3361 + 3130 3362 '@atcute/standard-site@1.0.1': 3131 3363 dependencies: 3132 3364 '@atcute/atproto': 3.1.10 ··· 3148 3380 dependencies: 3149 3381 unicode-segmenter: 0.14.5 3150 3382 3383 + '@atcute/util-text@1.2.0': 3384 + dependencies: 3385 + unicode-segmenter: 0.14.5 3386 + 3387 + '@atcute/varint@2.0.0': {} 3388 + 3389 + '@atmo-dev/contrail@0.0.8(@atcute/identity@1.1.3)(react@19.2.4)': 3390 + dependencies: 3391 + '@atcute/atproto': 3.1.10 3392 + '@atcute/client': 4.2.1 3393 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 3394 + '@atcute/jetstream': 1.1.2(react@19.2.4) 3395 + '@atcute/lexicons': 1.2.10 3396 + hono: 4.12.12 3397 + transitivePeerDependencies: 3398 + - '@atcute/identity' 3399 + - react 3400 + 3151 3401 '@badrap/valita@0.4.6': {} 3152 3402 3153 3403 '@cloudflare/kv-asset-handler@0.4.2': {} ··· 3665 3915 pbf: 4.0.1 3666 3916 supercluster: 8.0.1 3667 3917 3918 + '@mary-ext/event-iterator@1.0.0': 3919 + dependencies: 3920 + yocto-queue: 1.2.2 3921 + 3922 + '@mary-ext/simple-event-emitter@1.0.1': {} 3923 + 3668 3924 '@mixmark-io/domino@2.2.0': {} 3669 3925 3670 3926 '@napi-rs/wasm-runtime@1.1.1': ··· 3674 3930 '@tybys/wasm-util': 0.10.1 3675 3931 optional: true 3676 3932 3933 + '@noble/secp256k1@3.0.0': {} 3934 + 3677 3935 '@number-flow/svelte@0.4.0(svelte@5.53.11)': 3678 3936 dependencies: 3679 3937 esm-env: 1.2.2 3680 3938 number-flow: 0.6.0 3681 3939 svelte: 5.53.11 3682 3940 3941 + '@optique/core@0.10.7': {} 3942 + 3943 + '@optique/run@0.10.7': 3944 + dependencies: 3945 + '@optique/core': 0.10.7 3946 + 3683 3947 '@oxc-project/runtime@0.115.0': {} 3684 3948 3685 3949 '@oxc-project/types@0.115.0': {} ··· 4663 4927 4664 4928 esutils@2.0.3: {} 4665 4929 4930 + event-target-polyfill@0.0.4: {} 4931 + 4666 4932 exsolve@1.0.8: {} 4667 4933 4668 4934 fast-deep-equal@3.1.3: {} ··· 4719 4985 highlight.js@11.11.1: {} 4720 4986 4721 4987 hls.js@1.6.15: {} 4988 + 4989 + hono@4.12.12: {} 4722 4990 4723 4991 htmlparser2@10.1.0: 4724 4992 dependencies: ··· 5072 5340 dependencies: 5073 5341 entities: 6.0.1 5074 5342 5343 + partysocket@1.1.16(react@19.2.4): 5344 + dependencies: 5345 + event-target-polyfill: 0.0.4 5346 + optionalDependencies: 5347 + react: 19.2.4 5348 + 5075 5349 path-exists@4.0.0: {} 5076 5350 5077 5351 path-key@3.1.1: {} ··· 5630 5904 dependencies: 5631 5905 prelude-ls: 1.2.1 5632 5906 5907 + type-fest@4.41.0: {} 5908 + 5633 5909 typescript-eslint@8.57.0(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): 5634 5910 dependencies: 5635 5911 '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) ··· 5763 6039 yaml@1.10.2: {} 5764 6040 5765 6041 yocto-queue@0.1.0: {} 6042 + 6043 + yocto-queue@1.2.2: {} 5766 6044 5767 6045 yoga-wasm-web@0.3.3: {} 5768 6046
+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 + });
+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 + }
+68
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( 25 + /*#__PURE__*/ v.array(profileEntrySchema), 26 + ); 27 + }, 28 + get record() { 29 + return /*#__PURE__*/ v.optional(AppBlentoCard.mainSchema); 30 + }, 31 + rkey: /*#__PURE__*/ v.string(), 32 + time_us: /*#__PURE__*/ v.integer(), 33 + uri: /*#__PURE__*/ v.resourceUriString(), 34 + }), 35 + }, 36 + }); 37 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 38 + $type: /*#__PURE__*/ v.optional( 39 + /*#__PURE__*/ v.literal("app.blento.card.getRecord#profileEntry"), 40 + ), 41 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 42 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 43 + did: /*#__PURE__*/ v.didString(), 44 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 45 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 46 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 47 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 48 + }); 49 + 50 + type main$schematype = typeof _mainSchema; 51 + type profileEntry$schematype = typeof _profileEntrySchema; 52 + 53 + export interface mainSchema extends main$schematype {} 54 + export interface profileEntrySchema extends profileEntry$schematype {} 55 + 56 + export const mainSchema = _mainSchema as mainSchema; 57 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 58 + 59 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 60 + 61 + export interface $params extends v.InferInput<mainSchema["params"]> {} 62 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 63 + 64 + declare module "@atcute/lexicons/ambient" { 65 + interface XRPCQueries { 66 + "app.blento.card.getRecord": mainSchema; 67 + } 68 + }
+212
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(), [ 36 + /*#__PURE__*/ v.integerRange(1, 200), 37 + ]), 38 + 50, 39 + ), 40 + /** 41 + * Maximum value for mobileH 42 + */ 43 + mobileHMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 44 + /** 45 + * Minimum value for mobileH 46 + */ 47 + mobileHMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 48 + /** 49 + * Maximum value for mobileW 50 + */ 51 + mobileWMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 52 + /** 53 + * Minimum value for mobileW 54 + */ 55 + mobileWMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 56 + /** 57 + * Maximum value for mobileX 58 + */ 59 + mobileXMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 60 + /** 61 + * Minimum value for mobileX 62 + */ 63 + mobileXMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 64 + /** 65 + * Maximum value for mobileY 66 + */ 67 + mobileYMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 68 + /** 69 + * Minimum value for mobileY 70 + */ 71 + mobileYMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 72 + /** 73 + * Sort direction (default: desc for dates/numbers/counts, asc for strings) 74 + */ 75 + order: /*#__PURE__*/ v.optional( 76 + /*#__PURE__*/ v.string<"asc" | "desc" | (string & {})>(), 77 + ), 78 + /** 79 + * Filter by page 80 + */ 81 + page: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 82 + /** 83 + * Include profile + identity info keyed by DID 84 + */ 85 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 86 + /** 87 + * Field to sort by (default: time_us) 88 + */ 89 + sort: /*#__PURE__*/ v.optional( 90 + /*#__PURE__*/ v.string< 91 + | "cardType" 92 + | "color" 93 + | "h" 94 + | "mobileH" 95 + | "mobileW" 96 + | "mobileX" 97 + | "mobileY" 98 + | "page" 99 + | "updatedAt" 100 + | "version" 101 + | "w" 102 + | "x" 103 + | "y" 104 + | (string & {}) 105 + >(), 106 + ), 107 + /** 108 + * Maximum value for updatedAt 109 + */ 110 + updatedAtMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 111 + /** 112 + * Minimum value for updatedAt 113 + */ 114 + updatedAtMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 115 + /** 116 + * Maximum value for version 117 + */ 118 + versionMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 119 + /** 120 + * Minimum value for version 121 + */ 122 + versionMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 123 + /** 124 + * Maximum value for w 125 + */ 126 + wMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 127 + /** 128 + * Minimum value for w 129 + */ 130 + wMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 131 + /** 132 + * Maximum value for x 133 + */ 134 + xMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 135 + /** 136 + * Minimum value for x 137 + */ 138 + xMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 139 + /** 140 + * Maximum value for y 141 + */ 142 + yMax: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 143 + /** 144 + * Minimum value for y 145 + */ 146 + yMin: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 147 + }), 148 + output: { 149 + type: "lex", 150 + schema: /*#__PURE__*/ v.object({ 151 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 152 + get profiles() { 153 + return /*#__PURE__*/ v.optional( 154 + /*#__PURE__*/ v.array(profileEntrySchema), 155 + ); 156 + }, 157 + get records() { 158 + return /*#__PURE__*/ v.array(recordSchema); 159 + }, 160 + }), 161 + }, 162 + }); 163 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 164 + $type: /*#__PURE__*/ v.optional( 165 + /*#__PURE__*/ v.literal("app.blento.card.listRecords#profileEntry"), 166 + ), 167 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 168 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 169 + did: /*#__PURE__*/ v.didString(), 170 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 171 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 172 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 173 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 174 + }); 175 + const _recordSchema = /*#__PURE__*/ v.object({ 176 + $type: /*#__PURE__*/ v.optional( 177 + /*#__PURE__*/ v.literal("app.blento.card.listRecords#record"), 178 + ), 179 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 180 + collection: /*#__PURE__*/ v.nsidString(), 181 + did: /*#__PURE__*/ v.didString(), 182 + get record() { 183 + return /*#__PURE__*/ v.optional(AppBlentoCard.mainSchema); 184 + }, 185 + rkey: /*#__PURE__*/ v.string(), 186 + time_us: /*#__PURE__*/ v.integer(), 187 + uri: /*#__PURE__*/ v.resourceUriString(), 188 + }); 189 + 190 + type main$schematype = typeof _mainSchema; 191 + type profileEntry$schematype = typeof _profileEntrySchema; 192 + type record$schematype = typeof _recordSchema; 193 + 194 + export interface mainSchema extends main$schematype {} 195 + export interface profileEntrySchema extends profileEntry$schematype {} 196 + export interface recordSchema extends record$schematype {} 197 + 198 + export const mainSchema = _mainSchema as mainSchema; 199 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 200 + export const recordSchema = _recordSchema as recordSchema; 201 + 202 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 203 + export interface Record extends v.InferInput<typeof recordSchema> {} 204 + 205 + export interface $params extends v.InferInput<mainSchema["params"]> {} 206 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 207 + 208 + declare module "@atcute/lexicons/ambient" { 209 + interface XRPCQueries { 210 + "app.blento.card.listRecords": mainSchema; 211 + } 212 + }
+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 + }
+47
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 = 33 + _collectionStatsSchema as collectionStatsSchema; 34 + export const mainSchema = _mainSchema as mainSchema; 35 + 36 + export interface CollectionStats extends v.InferInput< 37 + typeof collectionStatsSchema 38 + > {} 39 + 40 + export interface $params {} 41 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 42 + 43 + declare module "@atcute/lexicons/ambient" { 44 + interface XRPCQueries { 45 + "app.blento.getOverview": mainSchema; 46 + } 47 + }
+52
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( 23 + /*#__PURE__*/ v.literal("app.blento.getProfile#profileEntry"), 24 + ), 25 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 26 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 27 + did: /*#__PURE__*/ v.didString(), 28 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 29 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 30 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 31 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 32 + }); 33 + 34 + type main$schematype = typeof _mainSchema; 35 + type profileEntry$schematype = typeof _profileEntrySchema; 36 + 37 + export interface mainSchema extends main$schematype {} 38 + export interface profileEntrySchema extends profileEntry$schematype {} 39 + 40 + export const mainSchema = _mainSchema as mainSchema; 41 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 42 + 43 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 44 + 45 + export interface $params extends v.InferInput<mainSchema["params"]> {} 46 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 47 + 48 + declare module "@atcute/lexicons/ambient" { 49 + interface XRPCQueries { 50 + "app.blento.getProfile": mainSchema; 51 + } 52 + }
+61
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( 20 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), 21 + [/*#__PURE__*/ v.arrayLength(0, 25)], 22 + ), 23 + ), 24 + }), 25 + }, 26 + output: { 27 + type: "lex", 28 + schema: /*#__PURE__*/ v.object({ 29 + /** 30 + * Number of records deleted (not found on PDS) 31 + */ 32 + deleted: /*#__PURE__*/ v.integer(), 33 + /** 34 + * Errors for individual URIs that could not be processed 35 + */ 36 + errors: /*#__PURE__*/ v.optional( 37 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.string()), 38 + ), 39 + /** 40 + * Number of records created or updated 41 + */ 42 + indexed: /*#__PURE__*/ v.integer(), 43 + }), 44 + }, 45 + }); 46 + 47 + type main$schematype = typeof _mainSchema; 48 + 49 + export interface mainSchema extends main$schematype {} 50 + 51 + export const mainSchema = _mainSchema as mainSchema; 52 + 53 + export interface $params {} 54 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 55 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 56 + 57 + declare module "@atcute/lexicons/ambient" { 58 + interface XRPCProcedures { 59 + "app.blento.notifyOfUpdate": mainSchema; 60 + } 61 + }
+49
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( 23 + /*#__PURE__*/ v.literal("app.blento.page#preferences"), 24 + ), 25 + accentColor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 26 + baseColor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 27 + editedOn: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), 28 + hideProfile: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 29 + hideProfileSection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 30 + profilePosition: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 31 + }); 32 + 33 + type main$schematype = typeof _mainSchema; 34 + type preferences$schematype = typeof _preferencesSchema; 35 + 36 + export interface mainSchema extends main$schematype {} 37 + export interface preferencesSchema extends preferences$schematype {} 38 + 39 + export const mainSchema = _mainSchema as mainSchema; 40 + export const preferencesSchema = _preferencesSchema as preferencesSchema; 41 + 42 + export interface Main extends v.InferInput<typeof mainSchema> {} 43 + export interface Preferences extends v.InferInput<typeof preferencesSchema> {} 44 + 45 + declare module "@atcute/lexicons/ambient" { 46 + interface Records { 47 + "app.blento.page": mainSchema; 48 + } 49 + }
+68
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( 25 + /*#__PURE__*/ v.array(profileEntrySchema), 26 + ); 27 + }, 28 + get record() { 29 + return /*#__PURE__*/ v.optional(AppBlentoPage.mainSchema); 30 + }, 31 + rkey: /*#__PURE__*/ v.string(), 32 + time_us: /*#__PURE__*/ v.integer(), 33 + uri: /*#__PURE__*/ v.resourceUriString(), 34 + }), 35 + }, 36 + }); 37 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 38 + $type: /*#__PURE__*/ v.optional( 39 + /*#__PURE__*/ v.literal("app.blento.page.getRecord#profileEntry"), 40 + ), 41 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 42 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 43 + did: /*#__PURE__*/ v.didString(), 44 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 45 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 46 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 47 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 48 + }); 49 + 50 + type main$schematype = typeof _mainSchema; 51 + type profileEntry$schematype = typeof _profileEntrySchema; 52 + 53 + export interface mainSchema extends main$schematype {} 54 + export interface profileEntrySchema extends profileEntry$schematype {} 55 + 56 + export const mainSchema = _mainSchema as mainSchema; 57 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 58 + 59 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 60 + 61 + export interface $params extends v.InferInput<mainSchema["params"]> {} 62 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 63 + 64 + declare module "@atcute/lexicons/ambient" { 65 + interface XRPCQueries { 66 + "app.blento.page.getRecord": mainSchema; 67 + } 68 + }
+113
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(), [ 24 + /*#__PURE__*/ v.integerRange(1, 200), 25 + ]), 26 + 50, 27 + ), 28 + /** 29 + * Filter by name 30 + */ 31 + name: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 32 + /** 33 + * Sort direction (default: desc for dates/numbers/counts, asc for strings) 34 + */ 35 + order: /*#__PURE__*/ v.optional( 36 + /*#__PURE__*/ v.string<"asc" | "desc" | (string & {})>(), 37 + ), 38 + /** 39 + * Include profile + identity info keyed by DID 40 + */ 41 + profiles: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 42 + /** 43 + * Field to sort by (default: time_us) 44 + */ 45 + sort: /*#__PURE__*/ v.optional( 46 + /*#__PURE__*/ v.string<"description" | "name" | (string & {})>(), 47 + ), 48 + }), 49 + output: { 50 + type: "lex", 51 + schema: /*#__PURE__*/ v.object({ 52 + cursor: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 53 + get profiles() { 54 + return /*#__PURE__*/ v.optional( 55 + /*#__PURE__*/ v.array(profileEntrySchema), 56 + ); 57 + }, 58 + get records() { 59 + return /*#__PURE__*/ v.array(recordSchema); 60 + }, 61 + }), 62 + }, 63 + }); 64 + const _profileEntrySchema = /*#__PURE__*/ v.object({ 65 + $type: /*#__PURE__*/ v.optional( 66 + /*#__PURE__*/ v.literal("app.blento.page.listRecords#profileEntry"), 67 + ), 68 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 69 + collection: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.nsidString()), 70 + did: /*#__PURE__*/ v.didString(), 71 + handle: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 72 + record: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.unknown()), 73 + rkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 74 + uri: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.resourceUriString()), 75 + }); 76 + const _recordSchema = /*#__PURE__*/ v.object({ 77 + $type: /*#__PURE__*/ v.optional( 78 + /*#__PURE__*/ v.literal("app.blento.page.listRecords#record"), 79 + ), 80 + cid: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 81 + collection: /*#__PURE__*/ v.nsidString(), 82 + did: /*#__PURE__*/ v.didString(), 83 + get record() { 84 + return /*#__PURE__*/ v.optional(AppBlentoPage.mainSchema); 85 + }, 86 + rkey: /*#__PURE__*/ v.string(), 87 + time_us: /*#__PURE__*/ v.integer(), 88 + uri: /*#__PURE__*/ v.resourceUriString(), 89 + }); 90 + 91 + type main$schematype = typeof _mainSchema; 92 + type profileEntry$schematype = typeof _profileEntrySchema; 93 + type record$schematype = typeof _recordSchema; 94 + 95 + export interface mainSchema extends main$schematype {} 96 + export interface profileEntrySchema extends profileEntry$schematype {} 97 + export interface recordSchema extends record$schematype {} 98 + 99 + export const mainSchema = _mainSchema as mainSchema; 100 + export const profileEntrySchema = _profileEntrySchema as profileEntrySchema; 101 + export const recordSchema = _recordSchema as recordSchema; 102 + 103 + export interface ProfileEntry extends v.InferInput<typeof profileEntrySchema> {} 104 + export interface Record extends v.InferInput<typeof recordSchema> {} 105 + 106 + export interface $params extends v.InferInput<mainSchema["params"]> {} 107 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 108 + 109 + declare module "@atcute/lexicons/ambient" { 110 + interface XRPCQueries { 111 + "app.blento.page.listRecords": mainSchema; 112 + } 113 + }
+65 -225
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'; 30 4 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 - }); 5 + let cachedProfile = $state<AppBskyActorDefs.ProfileViewDetailed | null>(null); 6 + let cachedDid = $state<Did | null>(null); 39 7 40 - export async function initClient(options?: { customDomain?: string }) { 41 - user.isInitializing = true; 42 - 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; 8 + // Load profile client-side when authDid changes 9 + $effect.root(() => { 10 + $effect(() => { 11 + const did = page.data?.authDid as Did | undefined; 48 12 49 - const handleResolver = new CompositeHandleResolver({ 50 - methods: { 51 - dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), 52 - http: new WellKnownHandleResolver() 13 + if (!did) { 14 + cachedProfile = null; 15 + cachedDid = null; 16 + return; 53 17 } 54 - }); 55 18 56 - let redirect_uri = dev ? 'http://127.0.0.1:5179' + REDIRECT_PATH : metadata.redirect_uris[0]; 19 + if (did === cachedDid && cachedProfile) return; 57 20 58 - if (options?.customDomain) { 59 - client_id = client_id.replace('blento.app', options.customDomain); 60 - redirect_uri = redirect_uri.replace('blento.app', options.customDomain); 21 + cachedDid = did; 61 22 62 - console.log(client_id, redirect_uri); 63 - } else { 64 - console.log('no custom domain'); 65 - } 23 + // If the current page already has this user's profile (e.g. viewing own page), use it 24 + if (page.data?.profile?.did === did) { 25 + cachedProfile = page.data.profile; 26 + return; 27 + } 66 28 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 - }) 29 + // Otherwise fetch it client-side 30 + import('@atcute/client').then(({ Client, simpleFetchHandler }) => { 31 + const client = new Client({ 32 + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) 33 + }); 34 + client 35 + .get('app.bsky.actor.getProfile', { params: { actor: did } }) 36 + .then((res) => { 37 + if (res.ok && cachedDid === did) { 38 + cachedProfile = res.data; 39 + } 40 + }) 41 + .catch(() => {}); 42 + }); 81 43 }); 82 - 83 - const params = new SvelteURLSearchParams(location.hash.slice(1)); 84 - 85 - const did = (localStorage.getItem('current-login') as Did) ?? undefined; 44 + }); 86 45 87 - if (params.size > 0) { 88 - await finalizeLogin(params, did); 89 - } else if (did) { 90 - await resumeSession(did); 46 + export const user: { 47 + profile: AppBskyActorDefs.ProfileViewDetailed | null | undefined; 48 + isLoggedIn: boolean; 49 + did: Did | undefined; 50 + } = { 51 + get profile() { 52 + return cachedProfile; 53 + }, 54 + get isLoggedIn() { 55 + return !!page.data?.authDid; 56 + }, 57 + get did() { 58 + return page.data?.authDid ?? undefined; 91 59 } 92 - 93 - user.isInitializing = false; 94 - } 60 + }; 95 61 96 62 export async function login(handle: ActorIdentifier) { 97 - console.log('login in with', handle); 63 + const { oauthLogin } = await import('./server/oauth.remote'); 64 + 65 + const returnTo = location.pathname + location.search; 66 + 98 67 if (handle.startsWith('did:')) { 99 68 if (handle.length < 6) throw new Error('DID must be at least 6 characters'); 100 - 101 - await startAuthorization(handle as ActorIdentifier); 102 69 } 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); 70 + handle = (handle.startsWith('@') ? handle.slice(1) : handle) as ActorIdentifier; 107 71 } else if (handle.length > 3) { 108 - const processed = (handle.startsWith('@') ? handle.slice(1) : handle) + '.bsky.social'; 109 - await startAuthorization(processed as ActorIdentifier); 72 + handle = ((handle.startsWith('@') ? handle.slice(1) : handle) + 73 + '.bsky.social') as ActorIdentifier; 110 74 } else { 111 75 throw new Error('Please provide a valid handle or DID.'); 112 76 } 77 + 78 + const { url } = await oauthLogin({ handle, returnTo }); 79 + window.location.assign(url); 113 80 } 114 81 115 82 export async function signup() { 116 - await startAuthorization(); 117 - } 83 + const { oauthLogin } = await import('./server/oauth.remote'); 118 84 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 - }); 85 + const returnTo = location.pathname + location.search; 86 + const { url } = await oauthLogin({ signup: true, returnTo }); 87 + window.location.assign(url); 143 88 } 144 89 145 90 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 - } 91 + const { oauthLogout } = await import('./server/oauth.remote'); 92 + await oauthLogout(); 93 + window.location.href = '/'; 254 94 }
+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 + }
+72
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 + returnTo: v.optional(v.string()) 20 + }), 21 + async (input) => { 22 + const { platform, cookies } = getRequestEvent(); 23 + 24 + try { 25 + const oauth = createOAuthClient(platform?.env, getDomain()); 26 + 27 + const target = input.signup 28 + ? ({ type: 'pds', serviceUrl: signUpPDS } as const) 29 + : ({ type: 'account', identifier: input.handle as ActorIdentifier } as const); 30 + 31 + const { url } = await oauth.authorize({ 32 + target, 33 + scope: scopes.join(' '), 34 + prompt: input.signup ? 'create' : undefined 35 + }); 36 + 37 + // Store return path in a cookie so the callback can redirect back 38 + if (input.returnTo) { 39 + cookies.set('oauth_return_to', encodeURIComponent(input.returnTo), { 40 + path: '/', 41 + httpOnly: true, 42 + maxAge: 600 // 10 minutes 43 + }); 44 + } 45 + 46 + return { url: url.toString() }; 47 + } catch (e) { 48 + if (e && typeof e === 'object' && 'status' in e) throw e; 49 + const message = e instanceof Error ? e.message : 'Login failed'; 50 + error(400, message); 51 + } 52 + } 53 + ); 54 + 55 + export const oauthLogout = command(async () => { 56 + const { cookies, platform } = getRequestEvent(); 57 + const did = getSignedCookie(cookies, 'did') as Did | null; 58 + 59 + if (did) { 60 + try { 61 + const oauth = createOAuthClient(platform?.env, getDomain()); 62 + await oauth.revoke(did); 63 + } catch (e) { 64 + console.error('Error revoking session:', e); 65 + } 66 + } 67 + 68 + cookies.delete('did', { path: '/' }); 69 + cookies.delete('scope', { path: '/' }); 70 + 71 + return { ok: true }; 72 + });
+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 };
+130
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 } = getRequestEvent(); 60 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 61 + 62 + const response = await locals.client.post('com.atproto.repo.deleteRecord', { 63 + input: { 64 + collection: input.collection as `${string}.${string}.${string}`, 65 + repo: locals.did, 66 + rkey: input.rkey || 'self' 67 + } 68 + }); 69 + 70 + return { ok: response.ok }; 71 + } 72 + ); 73 + 74 + export const uploadBlob = command( 75 + v.object({ 76 + bytes: v.array(v.number()), 77 + mimeType: v.string() 78 + }), 79 + async (input) => { 80 + const { locals } = getRequestEvent(); 81 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 82 + 83 + const blob = new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }); 84 + 85 + const response = await locals.client.post('com.atproto.repo.uploadBlob', { 86 + params: { repo: locals.did }, 87 + input: blob 88 + }); 89 + 90 + if (!response.ok) error(500, 'Upload failed'); 91 + 92 + return response.data.blob as { 93 + $type: 'blob'; 94 + ref: { $link: string }; 95 + mimeType: string; 96 + size: number; 97 + }; 98 + } 99 + ); 100 + 101 + export const createRecord = command( 102 + v.object({ 103 + collection: collectionSchema, 104 + record: v.record(v.string(), v.unknown()) 105 + }), 106 + async (input) => { 107 + const { locals } = getRequestEvent(); 108 + if (!locals.client || !locals.did) error(401, 'Not authenticated'); 109 + 110 + const response = await locals.client.post('com.atproto.repo.createRecord', { 111 + input: { 112 + collection: input.collection as `${string}.${string}.${string}`, 113 + repo: locals.did, 114 + record: input.record 115 + } 116 + }); 117 + 118 + if (!response.ok) error(500, 'Failed to create record'); 119 + 120 + // Immediately index in contrail 121 + const { platform } = getRequestEvent(); 122 + const db = platform?.env?.DB; 123 + if (db) { 124 + await ensureInit(db); 125 + await contrail.notify(response.data.uri, db).catch(() => {}); 126 + } 127 + 128 + return response.data; 129 + } 130 + );
+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 + }
+16 -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 - }; 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 + 'app.bsky.feed.post' 19 + ] as const; 11 20 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; 48 - 49 - export type AllowedCollection = ExtractCollectionBase<(typeof permissions.collections)[number]>; 21 + export type AllowedCollection = (typeof collections)[number]; 50 22 51 23 // which PDS to use for signup 52 - // ATTENTION: pds.rip is only for development, all accounts get deleted automatically after a week 53 24 const devPDS = 'https://pds.rip/'; 54 25 const prodPDS = 'https://selfhosted.social/'; 55 26 export const signUpPDS = dev ? devPDS : prodPDS; 56 27 57 - // where to redirect after oauth login/signup, e.g. /oauth/callback 28 + // where to redirect after oauth login/signup 58 29 export const REDIRECT_PATH = '/oauth/callback'; 59 30 60 31 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) ===
-1
src/lib/cards/media/SecretImageCard/EditingSecretImageCard.svelte
··· 102 102 </div> 103 103 {/if} 104 104 </button> 105 - 106 105 </div>
+1 -3
src/lib/cards/media/StatusphereCard/EditStatusphereCard.svelte
··· 29 29 let mode = $derived(item.cardData?.mode ?? 'emoji'); 30 30 // Emoji mode: use cardData. Statusphere mode: use latest record or preview. 31 31 let emoji = $derived( 32 - mode === 'statusphere' 33 - ? (item.cardData?.emoji ?? record?.value?.status) 34 - : item.cardData?.emoji 32 + mode === 'statusphere' ? (item.cardData?.emoji ?? record?.value?.status) : item.cardData?.emoji 35 33 ); 36 34 37 35 let showPopover = $state(false);
+1 -3
src/lib/cards/media/StatusphereCard/StatusphereCard.svelte
··· 11 11 12 12 let mode = $derived(item.cardData?.mode ?? 'emoji'); 13 13 // Emoji mode: use cardData. Statusphere mode: use latest record from PDS. 14 - let emoji = $derived( 15 - mode === 'statusphere' ? record?.value?.status : item.cardData?.emoji 16 - ); 14 + let emoji = $derived(mode === 'statusphere' ? record?.value?.status : item.cardData?.emoji); 17 15 let animated = $derived(emojiToNotoAnimatedWebp(emoji)); 18 16 </script> 19 17
+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();
+1 -1
src/lib/cards/social/KichRecipeCard/KichRecipeCard.svelte
··· 121 121 <img 122 122 src={imageUrl} 123 123 alt={title} 124 - class="recipe-image rounded-t-xl aspect-16/9 w-full object-cover" 124 + class="recipe-image aspect-16/9 w-full rounded-t-xl object-cover" 125 125 /> 126 126 <div class="image-overlay pointer-events-none absolute inset-0"></div> 127 127 <div class="compact-overlay pointer-events-none absolute right-0 bottom-0 left-0 p-4">
+2 -2
src/lib/cards/social/KichRecipeCollectionCard/KichRecipeCollectionCard.svelte
··· 84 84 class="relative block min-h-0 flex-1" 85 85 > 86 86 {#if imageUrl} 87 - <img src={imageUrl} alt={title} class="rounded-t-xl h-full w-full object-cover" /> 87 + <img src={imageUrl} alt={title} class="h-full w-full rounded-t-xl object-cover" /> 88 88 {:else} 89 89 <div 90 - class="rounded-t-xl from-base-300 to-base-200 dark:from-base-800 dark:to-base-900 h-full w-full bg-gradient-to-br" 90 + class="from-base-300 to-base-200 dark:from-base-800 dark:to-base-900 h-full w-full rounded-t-xl bg-gradient-to-br" 91 91 ></div> 92 92 {/if} 93 93
+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 + }
+20
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 + ] 20 + };
+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 -3
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 // svelte-ignore state_referenced_locally 60 60 let items: Item[] = $state(data.cards); ··· 239 239 240 240 launchConfetti(); 241 241 242 - // Refresh cached data 243 - await fetch('/' + data.handle + '/api/refresh'); 244 242 } catch (error) { 245 243 console.error(error); 246 244 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 );
+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
+193 -115
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 } 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) { 32 - try { 33 - const cachedResult = await cache?.getBlento(identifier); 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 and publication 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 + } { 55 + let bskyRecord: Record<string, unknown> | undefined; 56 + let pubRecord: Record<string, unknown> | undefined; 57 + let handle = did; 34 58 35 - if (!cachedResult) return; 36 - const result = JSON.parse(cachedResult); 59 + for (const p of profiles) { 60 + if (p.did !== did) continue; 61 + if (p.handle && p.handle !== 'handle.invalid') handle = p.handle; 37 62 38 - if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 39 - console.log('skipping cache because of version mismatch'); 40 - return; 63 + const record = p.record as Record<string, unknown> | undefined; 64 + if (p.collection === 'app.bsky.actor.profile' && record) { 65 + bskyRecord = record; 66 + } 67 + if (p.collection === 'site.standard.publication' && record) { 68 + pubRecord = record; 41 69 } 70 + } 42 71 43 - result.page = 'blento.' + page; 72 + const avatar = bskyRecord?.avatar 73 + ? getCDNImageBlobUrl({ 74 + did, 75 + blob: bskyRecord.avatar as { $type: 'blob'; ref: { $link: string } } 76 + }) 77 + : undefined; 44 78 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 51 - }; 79 + const profile = { 80 + did: did as Did, 81 + handle: handle as `${string}.${string}`, 82 + displayName: (bskyRecord?.displayName as string) ?? handle, 83 + description: bskyRecord?.description as string | undefined, 84 + avatar 85 + } as AppBskyActorDefs.ProfileViewDetailed; 52 86 53 - delete result['publications']; 87 + const publication = pubRecord 88 + ? (pubRecord as WebsiteData['publication']) 89 + : undefined; 54 90 55 - return checkData(result); 56 - } catch (error) { 57 - console.log('getting cached result failed', error); 91 + return { profile, publication }; 92 + } 93 + 94 + /** 95 + * Load all data for a user from Contrail in a single call (cards + profiles). 96 + */ 97 + async function loadFromContrail( 98 + actor: ActorIdentifier, 99 + db: D1Database 100 + ): Promise<{ 101 + cards: Item[]; 102 + pages: Awaited<ReturnType<typeof listRecords>>; 103 + profiles: ContrailProfile[]; 104 + } | null> { 105 + try { 106 + const client = getServerClient(db); 107 + const [cardRes, pageRes] = await Promise.all([ 108 + client.get('app.blento.card.listRecords', { 109 + params: { actor, limit: 200, profiles: true } 110 + }), 111 + client.get('app.blento.page.listRecords', { 112 + params: { actor, limit: 200 } 113 + }) 114 + ]); 115 + 116 + if (!cardRes.ok) return null; 117 + 118 + const cards = cardRes.data.records.map((r) => ({ ...(r.record as object) }) as Item); 119 + 120 + const pages = pageRes.ok 121 + ? pageRes.data.records 122 + .filter((r) => r.record) 123 + .map((r) => ({ 124 + uri: r.uri, 125 + cid: r.cid ?? '', 126 + value: r.record as Record<string, unknown> 127 + })) 128 + : []; 129 + 130 + return { 131 + cards, 132 + pages, 133 + profiles: (cardRes.data.profiles ?? []) as ContrailProfile[] 134 + }; 135 + } catch (e) { 136 + console.error('Contrail query failed', e); 137 + return null; 58 138 } 59 139 } 60 140 ··· 63 143 cache: CacheService | undefined, 64 144 forceUpdate: boolean = false, 65 145 page: string = 'self', 66 - env?: Record<string, string | undefined> 146 + env?: Record<string, string | undefined>, 147 + platform?: App.Platform 67 148 ): Promise<WebsiteData> { 68 149 if (!handle) throw error(404); 69 150 if (handle === 'favicon.ico') throw error(404); 70 151 71 - if (!forceUpdate) { 72 - const cachedResult = await getCache(handle, page, cache); 152 + const did = await resolveDid(handle); 153 + if (!did) throw error(404); 73 154 74 - if (cachedResult) return cachedResult; 75 - } 155 + const db = platform?.env?.DB; 156 + const contrailData = db ? await loadFromContrail(handle, db) : null; 76 157 77 - let did: Did | undefined = undefined; 78 - if (isHandle(handle)) { 79 - did = await resolveHandle({ handle }); 80 - } else if (isDid(handle)) { 81 - did = handle; 82 - } else { 83 - throw error(404); 84 - } 158 + let cards: Item[]; 159 + let pageRecords: Awaited<ReturnType<typeof listRecords>>; 160 + let profile: WebsiteData['profile']; 161 + let publication: WebsiteData['publication'] | undefined; 162 + let pronounsRecord: PronounsRecord | undefined; 85 163 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({ 164 + if (contrailData) { 165 + cards = contrailData.cards; 166 + pageRecords = contrailData.pages; 167 + 168 + const extracted = extractProfileData(did, contrailData.profiles); 169 + profile = extracted.profile; 170 + publication = extracted.publication; 171 + 172 + // Pronouns still from PDS (not in contrail) 173 + pronounsRecord = await getRecord({ 105 174 did, 106 175 collection: 'app.nearhorizon.actor.pronouns', 107 176 rkey: 'self' 108 - }).catch(() => undefined) 109 - ]); 177 + }).catch(() => undefined) as PronounsRecord | undefined; 178 + } else { 179 + // Fallback: no D1 available (e.g. vite dev) — use PDS directly 180 + const [cardRecords, pageRecs, mainPub, prof, pronouns] = await Promise.all([ 181 + listRecords({ did, collection: 'app.blento.card', limit: 0 }).catch((e) => { 182 + console.error('error getting records for collection app.blento.card', e); 183 + return [] as Awaited<ReturnType<typeof listRecords>>; 184 + }), 185 + listRecords({ did, collection: 'app.blento.page' }).catch(() => { 186 + return [] as Awaited<ReturnType<typeof listRecords>>; 187 + }), 188 + getRecord({ 189 + did, 190 + collection: 'site.standard.publication', 191 + rkey: 'blento.self' 192 + }).catch(() => undefined), 193 + getDetailedProfile({ did }), 194 + getRecord({ 195 + did, 196 + collection: 'app.nearhorizon.actor.pronouns', 197 + rkey: 'self' 198 + }).catch(() => undefined) 199 + ]); 200 + 201 + cards = cardRecords.map((v) => ({ ...v.value }) as Item); 202 + pageRecords = pageRecs; 203 + profile = prof; 204 + publication = mainPub?.value as WebsiteData['publication'] | undefined; 205 + pronounsRecord = pronouns as PronounsRecord | undefined; 206 + } 207 + 208 + // If no publication found from contrail profiles, check page records 209 + if (!publication) { 210 + const pubFromPages = pageRecords.find( 211 + (v) => parseUri(v.uri)?.rkey === 'blento.' + page 212 + ); 213 + publication = pubFromPages?.value as WebsiteData['publication'] | undefined; 214 + } 215 + 216 + publication ??= { 217 + name: profile?.displayName || profile?.handle, 218 + description: profile?.description 219 + } as WebsiteData['publication']; 110 220 111 221 const additionalData = await loadAdditionalData( 112 - cards.map((v) => ({ ...v.value })) as Item[], 113 - { did, handle, cache }, 222 + cards, 223 + { did, handle, cache, platform }, 114 224 env 115 225 ); 116 226 117 - const result = { 227 + return checkData({ 118 228 page: 'blento.' + page, 119 229 handle, 120 230 did, 121 - cards: (cards.map((v) => { 122 - return { ...v.value }; 123 - }) ?? []) as Item[], 124 - publications: [mainPublication, ...pages].filter((v) => v), 231 + cards, 232 + publication, 125 233 additionalData, 126 234 profile, 127 235 pronouns: formatPronouns(pronounsRecord, profile), 128 - pronounsRecord: pronounsRecord as PronounsRecord | undefined, 236 + pronounsRecord, 129 237 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); 238 + version: 1 239 + }); 152 240 } 153 241 154 242 export async function loadCardData( ··· 160 248 if (!handle) throw error(404); 161 249 if (handle === 'favicon.ico') throw error(404); 162 250 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); 170 - } 251 + const did = await resolveDid(handle); 252 + if (!did) throw error(404); 171 253 172 254 const [cardRecord, profile, pronounsRecord] = await Promise.all([ 173 255 getRecord({ ··· 205 287 env 206 288 ); 207 289 208 - const result = { 290 + return { 209 291 page, 210 292 handle: resolvedHandle, 211 293 did, ··· 221 303 pronouns: formatPronouns(pronounsRecord, profile), 222 304 pronounsRecord: pronounsRecord as PronounsRecord | undefined, 223 305 updatedAt: Date.now(), 224 - version: CURRENT_CACHE_VERSION 306 + version: 1 225 307 }; 226 - 227 - return result; 228 308 } 229 309 230 310 export async function loadCardTypeData( ··· 242 322 throw error(404, 'Card type not found'); 243 323 } 244 324 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); 252 - } 325 + const did = await resolveDid(handle); 326 + if (!did) throw error(404); 253 327 254 328 const [publication, profile, pronounsRecord] = await Promise.all([ 255 329 getRecord({ ··· 283 357 env 284 358 ); 285 359 286 - const result = { 360 + return checkData({ 287 361 page: 'blento.self', 288 362 handle: resolvedHandle, 289 363 did, ··· 299 373 pronouns: formatPronouns(pronounsRecord, profile), 300 374 pronounsRecord: pronounsRecord as PronounsRecord | undefined, 301 375 updatedAt: Date.now(), 302 - version: CURRENT_CACHE_VERSION 303 - }; 304 - 305 - return checkData(result); 376 + version: 1 377 + }); 306 378 } 307 379 308 380 function migrateCard(card: Item): Item { ··· 331 403 332 404 async function loadAdditionalData( 333 405 cards: Item[], 334 - { did, handle, cache }: { did: Did; handle: string; cache?: CacheService }, 406 + { 407 + did, 408 + handle, 409 + cache, 410 + platform 411 + }: { did: Did; handle: string; cache?: CacheService; platform?: App.Platform }, 335 412 env?: Record<string, string | undefined> 336 413 ) { 337 414 const cardTypes = new Set(cards.map((v) => v.cardType ?? '') as string[]); ··· 348 425 did, 349 426 handle, 350 427 cache, 351 - env 428 + env, 429 + platform 352 430 }); 353 431 } else if (cardDef?.loadData) { 354 432 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}
+40
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 { dev } from '$app/environment'; 6 + import type { RequestHandler } from './$types'; 7 + 8 + export const GET: RequestHandler = async ({ url, platform, cookies, request }) => { 9 + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase() || undefined; 10 + const oauth = createOAuthClient(platform?.env, customDomain); 11 + 12 + try { 13 + const { session } = await oauth.callback(url.searchParams); 14 + 15 + const cookieOpts = { 16 + path: '/', 17 + httpOnly: true, 18 + secure: !dev, 19 + sameSite: 'lax' as const, 20 + maxAge: 60 * 60 * 24 * 180 // 180 days 21 + }; 22 + 23 + setSignedCookie(cookies, 'did', session.did, cookieOpts); 24 + setSignedCookie(cookies, 'scope', scopes.join(' '), cookieOpts); 25 + } catch (e) { 26 + console.error('OAuth callback failed:', e); 27 + redirect(303, '/?error=auth_failed'); 28 + } 29 + 30 + const returnTo = cookies.get('oauth_return_to'); 31 + if (returnTo) { 32 + cookies.delete('oauth_return_to', { path: '/' }); 33 + const decoded = decodeURIComponent(returnTo); 34 + if (decoded.startsWith('/') && !decoded.startsWith('//')) { 35 + redirect(303, decoded); 36 + } 37 + } 38 + 39 + redirect(303, '/'); 40 + };
+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, false, 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, false, 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>
+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);
+19 -7
wrangler.jsonc
··· 46 46 { 47 47 "binding": "CUSTOM_DOMAINS", 48 48 "id": "f449b3b5c8a349478405e2c04ed265f0" 49 + }, 50 + { 51 + "binding": "OAUTH_SESSIONS", 52 + "id": "REPLACE_WITH_KV_NAMESPACE_ID" 53 + }, 54 + { 55 + "binding": "OAUTH_STATES", 56 + "id": "REPLACE_WITH_KV_NAMESPACE_ID" 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 + } 65 + ], 66 + "triggers": { 67 + "crons": ["*/5 * * * *"] 68 + } 57 69 }