@graffiti-garden/implementation-decentralized 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,7 @@ import {
34
34
  MESSAGE_METADATA_KEY,
35
35
  MESSAGE_OBJECT_KEY,
36
36
  MESSAGE_TAGS_KEY,
37
+ type LabeledMessage,
37
38
  type MessageStream,
38
39
  } from "../1-services/4-inboxes";
39
40
 
@@ -131,6 +132,8 @@ export interface GraffitiDecentralizedOptions {
131
132
  defaultInboxEndpoints?: string[];
132
133
  }
133
134
 
135
+ const CONCURRENCY = 16;
136
+
134
137
  export class GraffitiDecentralized implements Graffiti {
135
138
  protected readonly dids = new DecentralizedIdentifiers();
136
139
  protected readonly authorization = new Authorization();
@@ -785,8 +788,6 @@ export class GraffitiDecentralized implements Graffiti {
785
788
  channels,
786
789
  cursors,
787
790
  } satisfies infer_<typeof CursorSchema>),
788
- continue: (session) =>
789
- this.discoverMeta<Schema>(channels, schema, cursors, session),
790
791
  };
791
792
  }
792
793
 
@@ -795,6 +796,7 @@ export class GraffitiDecentralized implements Graffiti {
795
796
  return this.discoverMeta<(typeof args)[1]>(channels, schema, {}, session);
796
797
  };
797
798
 
799
+ // @ts-ignore
798
800
  continueDiscover: Graffiti["continueDiscover"] = (...args) => {
799
801
  const [cursor, session] = args;
800
802
  // Extract the channels from the cursor
@@ -1009,237 +1011,277 @@ export class GraffitiDecentralized implements Graffiti {
1009
1011
  inboxToken,
1010
1012
  ) as unknown as MessageStream<Schema>);
1011
1013
 
1014
+ const inFlight: Promise<SingleEndpointQueryResult<Schema> | void>[] = [];
1015
+ let doneValue: string | null = null;
1016
+
1012
1017
  while (true) {
1013
- const itResult = await iterator.next();
1014
- // Return the cursor if done
1015
- if (itResult.done) return itResult.value;
1018
+ while (doneValue === null && inFlight.length < CONCURRENCY) {
1019
+ const itResult = await iterator.next();
1020
+ if (itResult.done) {
1021
+ doneValue = itResult.value;
1022
+ break;
1023
+ }
1024
+
1025
+ const processPromise = this.processOneLabeledMessage<Schema>(
1026
+ inboxEndpoint,
1027
+ itResult.value,
1028
+ inboxToken,
1029
+ recipient,
1030
+ ).catch((e) => {
1031
+ throw e;
1032
+ });
1033
+
1034
+ inFlight.push(processPromise);
1035
+ }
1016
1036
 
1017
- const result = itResult.value;
1037
+ const nextProcessedPromise = inFlight.shift();
1018
1038
 
1019
- const label = result.l;
1020
- // Anything invalid or unexpected, we can skip
1039
+ if (!nextProcessedPromise) {
1040
+ if (doneValue !== null) return doneValue;
1041
+
1042
+ throw new Error("Process queue empty but no return value");
1043
+ }
1044
+
1045
+ const processed = await nextProcessedPromise;
1046
+ if (processed) yield processed;
1047
+ }
1048
+ }
1049
+
1050
+ protected async processOneLabeledMessage<Schema extends JSONSchema>(
1051
+ inboxEndpoint: string,
1052
+ result: LabeledMessage<Schema>,
1053
+ inboxToken?: string | null,
1054
+ recipient?: string | null,
1055
+ ): Promise<SingleEndpointQueryResult<Schema> | void> {
1056
+ const label = result.l;
1057
+ // Anything invalid or unexpected, we can skip
1058
+ if (
1059
+ label !== MESSAGE_LABEL_VALID &&
1060
+ label !== MESSAGE_LABEL_UNLABELED &&
1061
+ label !== MESSAGE_LABEL_TRASH
1062
+ )
1063
+ return;
1064
+
1065
+ const messageId = result.id;
1066
+ const { o: object, m: metadataBytes, t: receivedTags } = result.m;
1067
+
1068
+ let metadata: MessageMetadata;
1069
+ try {
1070
+ const metadataRaw = dagCborDecode(metadataBytes);
1071
+ metadata = MessageMetadataSchema.parse(metadataRaw);
1072
+ } catch (e) {
1073
+ this.inboxes.label(
1074
+ inboxEndpoint,
1075
+ messageId,
1076
+ MESSAGE_LABEL_INVALID,
1077
+ inboxToken,
1078
+ );
1079
+ return;
1080
+ }
1081
+
1082
+ const {
1083
+ [MESSAGE_DATA_STORAGE_BUCKET_KEY]: storageBucketKey,
1084
+ [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
1085
+ } = metadata;
1086
+
1087
+ const allowedTickets =
1088
+ MESSAGE_DATA_ALLOWED_TICKETS_KEY in metadata
1089
+ ? metadata[MESSAGE_DATA_ALLOWED_TICKETS_KEY]
1090
+ : undefined;
1091
+ const announcements =
1092
+ MESSAGE_DATA_ANNOUNCEMENTS_KEY in metadata
1093
+ ? metadata[MESSAGE_DATA_ANNOUNCEMENTS_KEY]
1094
+ : undefined;
1095
+
1096
+ if (label === MESSAGE_LABEL_VALID) {
1097
+ return {
1098
+ messageId,
1099
+ object,
1100
+ storageBucketKey,
1101
+ allowedTickets,
1102
+ tags: receivedTags,
1103
+ announcements,
1104
+ };
1105
+ } else if (label === MESSAGE_LABEL_TRASH) {
1106
+ // If it is simply trash, just continue.
1107
+ if (!tombstonedMessageId) return;
1108
+
1109
+ // Make sure the tombstone points to a real message
1110
+ const past = await this.inboxes.get(
1111
+ inboxEndpoint,
1112
+ tombstonedMessageId,
1113
+ inboxToken,
1114
+ );
1021
1115
  if (
1022
- label !== MESSAGE_LABEL_VALID &&
1023
- label !== MESSAGE_LABEL_UNLABELED &&
1024
- label !== MESSAGE_LABEL_TRASH
1116
+ !past ||
1117
+ past[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY].url !== object.url
1025
1118
  )
1026
- continue;
1027
-
1028
- const messageId = result.id;
1029
- const { o: object, m: metadataBytes, t: receivedTags } = result.m;
1119
+ return;
1030
1120
 
1031
- let metadata: MessageMetadata;
1032
- try {
1033
- const metadataRaw = dagCborDecode(metadataBytes);
1034
- metadata = MessageMetadataSchema.parse(metadataRaw);
1035
- } catch (e) {
1121
+ // If the referred to message isn't labeled as trash, trash it
1122
+ // This may happen if a trash message is processed on another
1123
+ // device and the device cache is out of date.
1124
+ if (past[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH) {
1125
+ // Label the message as trash
1036
1126
  this.inboxes.label(
1037
1127
  inboxEndpoint,
1038
- messageId,
1039
- MESSAGE_LABEL_INVALID,
1128
+ tombstonedMessageId,
1129
+ MESSAGE_LABEL_TRASH,
1040
1130
  inboxToken,
1041
1131
  );
1042
- continue;
1043
1132
  }
1044
1133
 
1045
- const {
1046
- [MESSAGE_DATA_STORAGE_BUCKET_KEY]: storageBucketKey,
1047
- [MESSAGE_DATA_TOMBSTONED_MESSAGE_ID_KEY]: tombstonedMessageId,
1048
- } = metadata;
1134
+ // Return the tombstone
1135
+ return {
1136
+ messageId,
1137
+ tombstone: true,
1138
+ object,
1139
+ storageBucketKey,
1140
+ allowedTickets,
1141
+ tags: receivedTags,
1142
+ announcements,
1143
+ };
1144
+ }
1049
1145
 
1050
- const allowedTickets =
1051
- MESSAGE_DATA_ALLOWED_TICKETS_KEY in metadata
1052
- ? metadata[MESSAGE_DATA_ALLOWED_TICKETS_KEY]
1053
- : undefined;
1054
- const announcements =
1055
- MESSAGE_DATA_ANNOUNCEMENTS_KEY in metadata
1056
- ? metadata[MESSAGE_DATA_ANNOUNCEMENTS_KEY]
1146
+ // Otherwise, unlabeled: try to validate the object
1147
+ let validationError: unknown | undefined = undefined;
1148
+ try {
1149
+ const actor = object.actor;
1150
+ const actorDocument = await this.dids.resolve(actor);
1151
+ const storageBucketService = actorDocument?.service?.find(
1152
+ (service) =>
1153
+ service.id === DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET &&
1154
+ service.type === DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET,
1155
+ );
1156
+ if (!storageBucketService) {
1157
+ throw new GraffitiErrorNotFound(
1158
+ `Actor ${actor} has no storage bucket service`,
1159
+ );
1160
+ }
1161
+ if (typeof storageBucketService.serviceEndpoint !== "string") {
1162
+ throw new GraffitiErrorNotFound(
1163
+ `Actor ${actor} does not have a valid storage bucket endpoint`,
1164
+ );
1165
+ }
1166
+ const storageBucketEndpoint = storageBucketService.serviceEndpoint;
1167
+
1168
+ const objectBytes = await this.storageBuckets.get(
1169
+ storageBucketEndpoint,
1170
+ storageBucketKey,
1171
+ MAX_OBJECT_SIZE_BYTES,
1172
+ );
1173
+
1174
+ if (MESSAGE_DATA_ALLOWED_TICKET_KEY in metadata && !recipient) {
1175
+ throw new GraffitiErrorForbidden(
1176
+ `Recipient is required when allowed ticket is present`,
1177
+ );
1178
+ }
1179
+ const privateObjectInfo = allowedTickets
1180
+ ? { allowedTickets }
1181
+ : MESSAGE_DATA_ALLOWED_TICKET_KEY in metadata
1182
+ ? {
1183
+ recipient: recipient ?? "null",
1184
+ allowedTicket: metadata[MESSAGE_DATA_ALLOWED_TICKET_KEY],
1185
+ allowedIndex: metadata[MESSAGE_DATA_ALLOWED_TICKET_INDEX_KEY],
1186
+ }
1057
1187
  : undefined;
1058
1188
 
1059
- if (label === MESSAGE_LABEL_VALID) {
1060
- yield {
1189
+ await this.objectEncoding.validate(
1190
+ object,
1191
+ receivedTags,
1192
+ objectBytes,
1193
+ privateObjectInfo,
1194
+ );
1195
+ } catch (e) {
1196
+ validationError = e;
1197
+ }
1198
+
1199
+ if (tombstonedMessageId) {
1200
+ if (validationError instanceof GraffitiErrorNotFound) {
1201
+ // Not found == The tombstone is correct
1202
+ this.inboxes
1203
+ // Get the referenced message
1204
+ .get(inboxEndpoint, tombstonedMessageId, inboxToken)
1205
+ .then((result) => {
1206
+ if (
1207
+ // Make sure that it actually references the object being deleted
1208
+ result &&
1209
+ result[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY].url ===
1210
+ object.url &&
1211
+ // And that the object is not already marked as trash
1212
+ result[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH
1213
+ ) {
1214
+ // If valid but not yet trash, label the message as trash
1215
+ this.inboxes.label(
1216
+ inboxEndpoint,
1217
+ tombstonedMessageId,
1218
+ MESSAGE_LABEL_TRASH,
1219
+ inboxToken,
1220
+ );
1221
+ }
1222
+
1223
+ // Then, label the tombstone message as trash
1224
+ this.inboxes.label(
1225
+ inboxEndpoint,
1226
+ messageId,
1227
+ MESSAGE_LABEL_TRASH,
1228
+ inboxToken,
1229
+ );
1230
+ });
1231
+
1232
+ return {
1061
1233
  messageId,
1234
+ tombstone: true,
1062
1235
  object,
1063
1236
  storageBucketKey,
1064
1237
  allowedTickets,
1065
1238
  tags: receivedTags,
1066
1239
  announcements,
1067
1240
  };
1068
- continue;
1069
- } else if (label === MESSAGE_LABEL_TRASH) {
1070
- // If it is simply trash, just continue.
1071
- if (!tombstonedMessageId) continue;
1072
-
1073
- // Make sure the tombstone points to a real message
1074
- const past = await this.inboxes.get(
1241
+ } else {
1242
+ console.error("Recieved an incorrect tombstone object");
1243
+ console.error(validationError);
1244
+ this.inboxes.label(
1075
1245
  inboxEndpoint,
1076
- tombstonedMessageId,
1246
+ messageId,
1247
+ MESSAGE_LABEL_INVALID,
1077
1248
  inboxToken,
1078
1249
  );
1079
- if (
1080
- !past ||
1081
- past[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY].url !==
1082
- object.url
1083
- )
1084
- continue;
1085
-
1086
- // If the referred to message isn't labeled as trash, trash it
1087
- // This may happen if a trash message is processed on another
1088
- // device and the device cache is out of date.
1089
- if (past[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH) {
1090
- // Label the message as trash
1091
- this.inboxes.label(
1092
- inboxEndpoint,
1093
- tombstonedMessageId,
1094
- MESSAGE_LABEL_TRASH,
1095
- inboxToken,
1096
- );
1097
- }
1098
-
1099
- // Return the tombstone
1100
- yield {
1250
+ }
1251
+ } else {
1252
+ if (validationError === undefined) {
1253
+ this.inboxes.label(
1254
+ inboxEndpoint,
1255
+ messageId,
1256
+ MESSAGE_LABEL_VALID,
1257
+ inboxToken,
1258
+ );
1259
+ return {
1101
1260
  messageId,
1102
- tombstone: true,
1103
1261
  object,
1104
1262
  storageBucketKey,
1105
- allowedTickets,
1106
1263
  tags: receivedTags,
1264
+ allowedTickets,
1107
1265
  announcements,
1108
1266
  };
1109
- continue;
1110
- }
1111
-
1112
- // Otherwise, unlabeled: try to validate the object
1113
- let validationError: unknown | undefined = undefined;
1114
- try {
1115
- const actor = object.actor;
1116
- const actorDocument = await this.dids.resolve(actor);
1117
- const storageBucketService = actorDocument?.service?.find(
1118
- (service) =>
1119
- service.id === DID_SERVICE_ID_GRAFFITI_STORAGE_BUCKET &&
1120
- service.type === DID_SERVICE_TYPE_GRAFFITI_STORAGE_BUCKET,
1121
- );
1122
- if (!storageBucketService) {
1123
- throw new GraffitiErrorNotFound(
1124
- `Actor ${actor} has no storage bucket service`,
1125
- );
1126
- }
1127
- if (typeof storageBucketService.serviceEndpoint !== "string") {
1128
- throw new GraffitiErrorNotFound(
1129
- `Actor ${actor} does not have a valid storage bucket endpoint`,
1130
- );
1131
- }
1132
- const storageBucketEndpoint = storageBucketService.serviceEndpoint;
1133
-
1134
- const objectBytes = await this.storageBuckets.get(
1135
- storageBucketEndpoint,
1136
- storageBucketKey,
1137
- MAX_OBJECT_SIZE_BYTES,
1138
- );
1139
-
1140
- if (MESSAGE_DATA_ALLOWED_TICKET_KEY in metadata && !recipient) {
1141
- throw new GraffitiErrorForbidden(
1142
- `Recipient is required when allowed ticket is present`,
1143
- );
1144
- }
1145
- const privateObjectInfo = allowedTickets
1146
- ? { allowedTickets }
1147
- : MESSAGE_DATA_ALLOWED_TICKET_KEY in metadata
1148
- ? {
1149
- recipient: recipient ?? "null",
1150
- allowedTicket: metadata[MESSAGE_DATA_ALLOWED_TICKET_KEY],
1151
- allowedIndex: metadata[MESSAGE_DATA_ALLOWED_TICKET_INDEX_KEY],
1152
- }
1153
- : undefined;
1154
-
1155
- await this.objectEncoding.validate(
1156
- object,
1157
- receivedTags,
1158
- objectBytes,
1159
- privateObjectInfo,
1267
+ } else if (validationError instanceof GraffitiErrorNotFound) {
1268
+ // Item was deleted before we got a chance to
1269
+ // validate it. Just label the message as trash.
1270
+ this.inboxes.label(
1271
+ inboxEndpoint,
1272
+ messageId,
1273
+ MESSAGE_LABEL_TRASH,
1274
+ inboxToken,
1160
1275
  );
1161
- } catch (e) {
1162
- validationError = e;
1163
- }
1164
-
1165
- if (tombstonedMessageId) {
1166
- if (validationError instanceof GraffitiErrorNotFound) {
1167
- // Not found == The tombstone is correct
1168
- this.inboxes
1169
- // Get the referenced message
1170
- .get(inboxEndpoint, tombstonedMessageId, inboxToken)
1171
- .then((result) => {
1172
- if (
1173
- // Make sure that it actually references the object being deleted
1174
- result &&
1175
- result[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY].url ===
1176
- object.url &&
1177
- // And that the object is not already marked as trash
1178
- result[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH
1179
- ) {
1180
- // If valid but not yet trash, label the message as trash
1181
- this.inboxes.label(
1182
- inboxEndpoint,
1183
- tombstonedMessageId,
1184
- MESSAGE_LABEL_TRASH,
1185
- inboxToken,
1186
- );
1187
- }
1188
-
1189
- // Then, label the tombstone message as trash
1190
- this.inboxes.label(
1191
- inboxEndpoint,
1192
- messageId,
1193
- MESSAGE_LABEL_TRASH,
1194
- inboxToken,
1195
- );
1196
- });
1197
-
1198
- yield {
1199
- messageId,
1200
- tombstone: true,
1201
- object,
1202
- storageBucketKey,
1203
- allowedTickets,
1204
- tags: receivedTags,
1205
- announcements,
1206
- };
1207
- } else {
1208
- console.error("Recieved an incorrect object");
1209
- console.error(validationError);
1210
- this.inboxes.label(
1211
- inboxEndpoint,
1212
- messageId,
1213
- MESSAGE_LABEL_INVALID,
1214
- inboxToken,
1215
- );
1216
- }
1217
1276
  } else {
1218
- if (validationError === undefined) {
1219
- this.inboxes.label(
1220
- inboxEndpoint,
1221
- messageId,
1222
- MESSAGE_LABEL_VALID,
1223
- inboxToken,
1224
- );
1225
- yield {
1226
- messageId,
1227
- object,
1228
- storageBucketKey,
1229
- tags: receivedTags,
1230
- allowedTickets,
1231
- announcements,
1232
- };
1233
- } else {
1234
- console.error("Recieved an incorrect object");
1235
- console.error(validationError);
1236
- this.inboxes.label(
1237
- inboxEndpoint,
1238
- messageId,
1239
- MESSAGE_LABEL_INVALID,
1240
- inboxToken,
1241
- );
1242
- }
1277
+ console.error("Recieved an incorrect object");
1278
+ console.error(validationError);
1279
+ this.inboxes.label(
1280
+ inboxEndpoint,
1281
+ messageId,
1282
+ MESSAGE_LABEL_INVALID,
1283
+ inboxToken,
1284
+ );
1243
1285
  }
1244
1286
  }
1245
1287
  }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../node_modules/@graffiti-garden/modal/src/style.css"],
4
- "sourcesContent": [".graffiti-modal {\n --back: rgba(26, 26, 26, 0.85);\n --halfback: rgba(80, 80, 80, 0.85);\n --halfback2: rgba(26, 26, 26, 0.85);\n --hover: rgba(202, 122, 204, 0.3);\n --frontfaded: rgba(190, 190, 190);\n --front: rgba(240, 240, 240);\n --emph: rgb(202, 122, 204);\n --blurpix: 3px;\n border-color: var(--emph);\n box-sizing: border-box;\n border-width: 2px;\n padding: 0;\n margin: 0;\n border-radius: 1rem;\n box-shadow: 0 0 2rem black;\n overflow: hidden;\n opacity: 0;\n transition: opacity 0.3s;\n pointer-events: none;\n min-width: 95dvw;\n min-height: 95dvh;\n height: 95dvh;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: center;\n font-family:\n Inter,\n -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n Oxygen,\n Ubuntu,\n Cantarell,\n \"Fira Sans\",\n \"Droid Sans\",\n \"Helvetica Neue\",\n sans-serif;\n color: var(--front);\n font-size: 150%;\n\n * {\n box-sizing: border-box;\n padding: 0;\n margin: 0;\n }\n\n ::selection {\n background: rgba(202, 122, 204, 0.3);\n }\n\n :focus {\n outline: 2px solid var(--front);\n }\n\n header {\n width: 100%;\n display: flex;\n justify-content: flex-end;\n }\n\n main {\n flex: 1;\n max-width: 600px;\n width: 100%;\n gap: 2em;\n padding-inline: clamp(1rem, 4dvw, 3rem);\n padding-block: clamp(1rem, 4dvh, 3rem);\n margin-inline: clamp(1rem, 4dvw, 3rem);\n margin-block: clamp(1rem, 4dvh, 3rem);\n background: var(--back);\n border-radius: 1rem;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n scrollbar-color: var(--emph) rgba(0, 0, 0, 0);\n }\n\n ul {\n list-style-type: none;\n display: flex;\n flex-direction: column;\n gap: 0.5em;\n align-items: stretch;\n justify-content: stretch;\n }\n\n aside {\n color: var(--frontfaded);\n }\n\n .secondary,\n a:not([type=\"button\"]) {\n color: var(--emph);\n }\n\n h1 {\n font-size: 120%;\n font-family:\n Rock Salt,\n cursive,\n sans-serif;\n letter-spacing: 0.1em;\n text-align: center;\n color: var(--front);\n }\n\n h1 a:not([type=\"button\"]) {\n color: inherit;\n }\n\n h1 a:hover {\n background: none;\n color: inherit;\n }\n\n button,\n input[type=\"submit\"],\n input[type=\"text\"],\n a[type=\"button\"] {\n font-size: inherit;\n width: 100%;\n text-align: center;\n display: block;\n border-radius: 1rem;\n border: 2px solid var(--emph);\n padding: 1em;\n padding-top: 0.5em;\n padding-bottom: 0.5em;\n transition: 0.1s;\n text-overflow: ellipsis;\n background: none;\n line-height: 1.2em;\n }\n\n input[type=\"text\"] {\n font-weight: 500;\n background: var(--front);\n text-align: left;\n color: black;\n }\n\n header button {\n border-radius: 0 0 0 1rem;\n border-right: none;\n border-top: none;\n background-color: var(--halfback2);\n width: fit-content;\n position: relative;\n overflow: hidden;\n }\n\n header button::before {\n z-index: -1;\n top: 0;\n left: 0;\n height: 100%;\n position: absolute;\n width: 100%;\n content: \"\";\n background-color: var(--back);\n }\n\n @media (max-height: 600px) {\n main {\n gap: 1em;\n }\n\n h1 {\n font-size: 100%;\n }\n }\n\n @media (max-width: 600px) {\n main {\n border-radius: 0;\n margin: auto;\n overflow: auto;\n }\n\n header button {\n border-radius: 0;\n border-left: none;\n width: 100%;\n }\n\n html {\n justify-content: safe center;\n }\n }\n\n a {\n text-decoration: none;\n }\n\n :is(button, ul a, input[type=\"submit\"]):hover {\n cursor: pointer;\n background: var(--hover);\n color: var(--front);\n }\n\n a:hover {\n text-decoration: underline;\n cursor: pointer;\n }\n\n form {\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: stretch;\n gap: 0.5em;\n }\n\n iframe {\n width: 100%;\n height: 100%;\n border: none;\n }\n\n :is(button, a[type=\"button\"], input[type=\"submit\"]).secondary {\n color: rgb(244, 213, 244);\n background: rgba(26, 26, 26, 0.6);\n }\n\n :is(button, a[type=\"button\"], input[type=\"submit\"]):not(.secondary) {\n background: var(--hover);\n color: white;\n }\n\n :is(button, a[type=\"button\"], input[type=\"submit\"]):hover {\n background: rgba(202, 122, 204, 0.6);\n color: white;\n text-decoration: none;\n }\n}\n\n.graffiti-modal[open] {\n pointer-events: inherit;\n opacity: 1;\n}\n\n.graffiti-modal::backdrop {\n background-color: black;\n opacity: 0.5;\n}\n\n.graffiti-modal::before {\n content: \"\";\n position: fixed;\n left: 0;\n right: 0;\n z-index: -1;\n background-image: url(graffiti.jpg);\n background-size: cover;\n background-repeat: no-repeat;\n background-position: 50% 50%;\n height: calc(100% + 2 * var(--blurpix));\n width: calc(100% + 2 * var(--blurpix));\n filter: blur(var(--blurpix));\n margin: calc(-1 * var(--blurpix));\n}\n"],
5
- "mappings": "4BAAA,IAAAA,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;",
6
- "names": ["style_default"]
7
- }