@graffiti-garden/implementation-decentralized 0.1.2 → 0.1.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.
- package/dist/1-services/4-inboxes/cache.d.ts +34 -0
- package/dist/1-services/4-inboxes/cache.d.ts.map +1 -0
- package/dist/1-services/4-inboxes/messages.d.ts +70 -0
- package/dist/1-services/4-inboxes/messages.d.ts.map +1 -0
- package/dist/1-services/4-inboxes.d.ts +28 -67
- package/dist/1-services/4-inboxes.d.ts.map +1 -1
- package/dist/3-protocol/2-handles.d.ts.map +1 -1
- package/dist/3-protocol/4-graffiti.d.ts +2 -2
- package/dist/3-protocol/4-graffiti.d.ts.map +1 -1
- package/dist/3-protocol/login-dialog.html.d.ts +1 -1
- package/dist/3-protocol/login-dialog.html.d.ts.map +1 -1
- package/dist/browser/index.js +7 -7
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/{login-dialog.html-E6VRKMXH.js → login-dialog.html-WIDO3XZF.js} +18 -4
- package/dist/browser/{login-dialog.html-E6VRKMXH.js.map → login-dialog.html-WIDO3XZF.js.map} +2 -2
- package/dist/cjs/1-services/4-inboxes/cache.js +186 -0
- package/dist/cjs/1-services/4-inboxes/cache.js.map +7 -0
- package/dist/cjs/1-services/4-inboxes/messages.js +69 -0
- package/dist/cjs/1-services/4-inboxes/messages.js.map +7 -0
- package/dist/cjs/1-services/4-inboxes-tests.js +339 -9
- package/dist/cjs/1-services/4-inboxes-tests.js.map +3 -3
- package/dist/cjs/1-services/4-inboxes.js +210 -399
- package/dist/cjs/1-services/4-inboxes.js.map +3 -3
- package/dist/cjs/3-protocol/2-handles-tests.js +8 -0
- package/dist/cjs/3-protocol/2-handles-tests.js.map +2 -2
- package/dist/cjs/3-protocol/2-handles.js +3 -4
- package/dist/cjs/3-protocol/2-handles.js.map +2 -2
- package/dist/cjs/3-protocol/4-graffiti.js +49 -53
- package/dist/cjs/3-protocol/4-graffiti.js.map +2 -2
- package/dist/cjs/3-protocol/login-dialog.html.js +17 -3
- package/dist/cjs/3-protocol/login-dialog.html.js.map +2 -2
- package/dist/cjs/index.spec.js +90 -0
- package/dist/cjs/index.spec.js.map +3 -3
- package/dist/esm/1-services/4-inboxes/cache.js +156 -0
- package/dist/esm/1-services/4-inboxes/cache.js.map +7 -0
- package/dist/esm/1-services/4-inboxes/messages.js +60 -0
- package/dist/esm/1-services/4-inboxes/messages.js.map +7 -0
- package/dist/esm/1-services/4-inboxes-tests.js +340 -10
- package/dist/esm/1-services/4-inboxes-tests.js.map +3 -3
- package/dist/esm/1-services/4-inboxes.js +222 -383
- package/dist/esm/1-services/4-inboxes.js.map +3 -3
- package/dist/esm/3-protocol/2-handles-tests.js +8 -0
- package/dist/esm/3-protocol/2-handles-tests.js.map +2 -2
- package/dist/esm/3-protocol/2-handles.js +3 -4
- package/dist/esm/3-protocol/2-handles.js.map +2 -2
- package/dist/esm/3-protocol/4-graffiti.js +49 -53
- package/dist/esm/3-protocol/4-graffiti.js.map +2 -2
- package/dist/esm/3-protocol/login-dialog.html.js +17 -3
- package/dist/esm/3-protocol/login-dialog.html.js.map +2 -2
- package/dist/esm/index.spec.js +92 -2
- package/dist/esm/index.spec.js.map +2 -2
- package/package.json +1 -1
- package/src/1-services/4-inboxes/cache.ts +227 -0
- package/src/1-services/4-inboxes/messages.ts +63 -0
- package/src/1-services/4-inboxes-tests.ts +387 -2
- package/src/1-services/4-inboxes.ts +297 -512
- package/src/3-protocol/2-handles-tests.ts +12 -0
- package/src/3-protocol/2-handles.ts +3 -5
- package/src/3-protocol/4-graffiti.ts +69 -83
- package/src/3-protocol/login-dialog.html.ts +17 -3
- package/src/index.spec.ts +106 -2
|
@@ -13,5 +13,17 @@ export function handleTests(handle: string) {
|
|
|
13
13
|
const resolvedHandle = await handles.actorToHandle(actor);
|
|
14
14
|
expect(resolvedHandle).toBe(handle);
|
|
15
15
|
});
|
|
16
|
+
|
|
17
|
+
test("actorToHandle uses the first did:web alias", async () => {
|
|
18
|
+
const actor = "did:plc:example";
|
|
19
|
+
const handleDid = "did:web:alice.example";
|
|
20
|
+
const dids = {
|
|
21
|
+
resolve: async (did: string) => did === actor
|
|
22
|
+
? { alsoKnownAs: ["at://alice.example", handleDid, "did:web:other.example"] }
|
|
23
|
+
: { alsoKnownAs: [actor] },
|
|
24
|
+
} as DecentralizedIdentifiers;
|
|
25
|
+
expect(await new Handles({ dids }).actorToHandle(actor)).toBe("alice.example");
|
|
26
|
+
});
|
|
27
|
+
|
|
16
28
|
});
|
|
17
29
|
}
|
|
@@ -13,16 +13,14 @@ export class Handles {
|
|
|
13
13
|
actorToHandle: Graffiti["actorToHandle"] = async (actor) => {
|
|
14
14
|
const actorDocument = await this.services.dids.resolve(actor);
|
|
15
15
|
|
|
16
|
-
const handleDid = actorDocument.alsoKnownAs?.
|
|
16
|
+
const handleDid = actorDocument.alsoKnownAs?.find((alias) =>
|
|
17
|
+
alias.startsWith(HANDLE_DID_PREFIX),
|
|
18
|
+
);
|
|
17
19
|
if (!handleDid) {
|
|
18
20
|
throw new GraffitiErrorNotFound(
|
|
19
21
|
`Handle for actor DID ${actor} not found`,
|
|
20
22
|
);
|
|
21
23
|
}
|
|
22
|
-
if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {
|
|
23
|
-
throw new Error(`Handle DID ${handleDid} is not a valid handle`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
24
|
const handle = handleDid.slice(HANDLE_DID_PREFIX.length);
|
|
27
25
|
|
|
28
26
|
const handleDocument = await this.services.dids.resolve(handleDid);
|
|
@@ -220,7 +220,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
if (handle) {
|
|
223
|
-
|
|
223
|
+
this.rememberHandleInBrowserStorage(handle);
|
|
224
224
|
}
|
|
225
225
|
});
|
|
226
226
|
|
|
@@ -282,8 +282,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
282
282
|
"#username",
|
|
283
283
|
) as HTMLInputElement | null;
|
|
284
284
|
const rememberedHandle =
|
|
285
|
-
proposedHandle ||
|
|
286
|
-
(await this.getRememberedHandleFromPasswordManager());
|
|
285
|
+
proposedHandle || this.getRememberedHandleFromBrowserStorage();
|
|
287
286
|
if (input) input.value = rememberedHandle ?? "";
|
|
288
287
|
input?.addEventListener("focus", () => input?.select());
|
|
289
288
|
setTimeout(() => input?.focus(), 0);
|
|
@@ -328,6 +327,12 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
328
327
|
this.login_(handle);
|
|
329
328
|
}
|
|
330
329
|
});
|
|
330
|
+
template
|
|
331
|
+
?.querySelector("#graffiti-login-forgot")
|
|
332
|
+
?.addEventListener("click", (e) => {
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
void this.loginWithoutHandle();
|
|
335
|
+
});
|
|
331
336
|
} else {
|
|
332
337
|
template = await this.modal?.displayTemplate("graffiti-login-welcome");
|
|
333
338
|
template
|
|
@@ -396,79 +401,44 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
396
401
|
}
|
|
397
402
|
}
|
|
398
403
|
|
|
399
|
-
protected async
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
password: "no-password",
|
|
428
|
-
name: handle,
|
|
404
|
+
protected async loginWithoutHandle() {
|
|
405
|
+
const template = await this.modal?.displayTemplate("graffiti-login-provider");
|
|
406
|
+
const defaultProvider = new URL("/", this.identityCreatorEndpoint);
|
|
407
|
+
const provider = template?.querySelector("#graffiti-provider") as HTMLInputElement | null;
|
|
408
|
+
if (provider) provider.value = defaultProvider.host;
|
|
409
|
+
provider?.addEventListener("focus", () => provider.select());
|
|
410
|
+
setTimeout(() => provider?.focus(), 0);
|
|
411
|
+
|
|
412
|
+
template
|
|
413
|
+
?.querySelector("#graffiti-login-provider-form")
|
|
414
|
+
?.addEventListener("submit", (event) => {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
try {
|
|
417
|
+
const domain = provider?.value.trim() ?? "";
|
|
418
|
+
const origin = domain === defaultProvider.host
|
|
419
|
+
? defaultProvider
|
|
420
|
+
: new URL(`https://${domain}`);
|
|
421
|
+
if (
|
|
422
|
+
!origin.hostname || origin.username || origin.password ||
|
|
423
|
+
origin.pathname !== "/" || origin.search || origin.hash
|
|
424
|
+
) throw new Error("Invalid provider domain");
|
|
425
|
+
|
|
426
|
+
const url = new URL("/forgot-handle", origin);
|
|
427
|
+
url.searchParams.set("redirect_uri", window.location.toString());
|
|
428
|
+
window.location.assign(url.toString());
|
|
429
|
+
} catch {
|
|
430
|
+
alert("Enter a valid provider domain.");
|
|
431
|
+
}
|
|
429
432
|
});
|
|
430
|
-
await credentials.store(credential);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.error("Failed to remember handle in password manager:", error);
|
|
433
|
-
this.rememberHandleInBrowserStorage(handle);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
433
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
PasswordCredential?: new (data: {
|
|
444
|
-
id: string;
|
|
445
|
-
password: string;
|
|
446
|
-
name?: string;
|
|
447
|
-
}) => Credential;
|
|
448
|
-
}
|
|
449
|
-
).PasswordCredential;
|
|
450
|
-
if (!credentials?.get || !PasswordCredentialConstructor) {
|
|
451
|
-
return this.getRememberedHandleFromBrowserStorage();
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
const credential = await credentials.get({
|
|
456
|
-
password: true,
|
|
457
|
-
mediation: "optional",
|
|
458
|
-
} as any);
|
|
459
|
-
if (
|
|
460
|
-
credential &&
|
|
461
|
-
"id" in credential &&
|
|
462
|
-
typeof credential.id === "string"
|
|
463
|
-
) {
|
|
464
|
-
return credential.id;
|
|
465
|
-
}
|
|
466
|
-
} catch (error) {
|
|
467
|
-
console.error("Failed to load handle from password manager:", error);
|
|
468
|
-
return this.getRememberedHandleFromBrowserStorage();
|
|
469
|
-
}
|
|
434
|
+
template
|
|
435
|
+
?.querySelector("#graffiti-login-provider-back")
|
|
436
|
+
?.addEventListener("click", (event) => {
|
|
437
|
+
event.preventDefault();
|
|
438
|
+
void this.login_("");
|
|
439
|
+
});
|
|
470
440
|
|
|
471
|
-
|
|
441
|
+
await this.modal?.open();
|
|
472
442
|
}
|
|
473
443
|
|
|
474
444
|
protected rememberHandleInBrowserStorage(handle: string) {
|
|
@@ -1221,6 +1191,19 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1221
1191
|
}
|
|
1222
1192
|
}
|
|
1223
1193
|
|
|
1194
|
+
// Labeling is advisory and should not hold up discovery, but its failure
|
|
1195
|
+
// must be handled even if the caller has already stopped reading the stream.
|
|
1196
|
+
protected labelInboxMessage(
|
|
1197
|
+
inboxEndpoint: string,
|
|
1198
|
+
messageId: string,
|
|
1199
|
+
label: number,
|
|
1200
|
+
inboxToken?: string | null,
|
|
1201
|
+
): void {
|
|
1202
|
+
void this.inboxes.label(inboxEndpoint, messageId, label, inboxToken).catch(
|
|
1203
|
+
(error) => console.error("Could not label inbox message:", error),
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1224
1207
|
protected async processOneLabeledMessage<Schema extends JSONSchema>(
|
|
1225
1208
|
inboxEndpoint: string,
|
|
1226
1209
|
result: LabeledMessage<Schema>,
|
|
@@ -1244,7 +1227,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1244
1227
|
const metadataRaw = dagCborDecode(metadataBytes);
|
|
1245
1228
|
metadata = MessageMetadataSchema.parse(metadataRaw);
|
|
1246
1229
|
} catch (e) {
|
|
1247
|
-
this.
|
|
1230
|
+
this.labelInboxMessage(
|
|
1248
1231
|
inboxEndpoint,
|
|
1249
1232
|
messageId,
|
|
1250
1233
|
MESSAGE_LABEL_INVALID,
|
|
@@ -1297,7 +1280,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1297
1280
|
// device and the device cache is out of date.
|
|
1298
1281
|
if (past[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH) {
|
|
1299
1282
|
// Label the message as trash
|
|
1300
|
-
this.
|
|
1283
|
+
this.labelInboxMessage(
|
|
1301
1284
|
inboxEndpoint,
|
|
1302
1285
|
tombstonedMessageId,
|
|
1303
1286
|
MESSAGE_LABEL_TRASH,
|
|
@@ -1373,7 +1356,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1373
1356
|
if (tombstonedMessageId) {
|
|
1374
1357
|
if (validationError instanceof GraffitiErrorNotFound) {
|
|
1375
1358
|
// Not found == The tombstone is correct
|
|
1376
|
-
this.inboxes
|
|
1359
|
+
void this.inboxes
|
|
1377
1360
|
// Get the referenced message
|
|
1378
1361
|
.get(inboxEndpoint, tombstonedMessageId, inboxToken)
|
|
1379
1362
|
.then((result) => {
|
|
@@ -1386,7 +1369,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1386
1369
|
result[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH
|
|
1387
1370
|
) {
|
|
1388
1371
|
// If valid but not yet trash, label the message as trash
|
|
1389
|
-
this.
|
|
1372
|
+
this.labelInboxMessage(
|
|
1390
1373
|
inboxEndpoint,
|
|
1391
1374
|
tombstonedMessageId,
|
|
1392
1375
|
MESSAGE_LABEL_TRASH,
|
|
@@ -1395,13 +1378,16 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1395
1378
|
}
|
|
1396
1379
|
|
|
1397
1380
|
// Then, label the tombstone message as trash
|
|
1398
|
-
this.
|
|
1381
|
+
this.labelInboxMessage(
|
|
1399
1382
|
inboxEndpoint,
|
|
1400
1383
|
messageId,
|
|
1401
1384
|
MESSAGE_LABEL_TRASH,
|
|
1402
1385
|
inboxToken,
|
|
1403
1386
|
);
|
|
1404
|
-
})
|
|
1387
|
+
})
|
|
1388
|
+
.catch((error) =>
|
|
1389
|
+
console.error("Could not inspect tombstone message:", error),
|
|
1390
|
+
);
|
|
1405
1391
|
|
|
1406
1392
|
return {
|
|
1407
1393
|
messageId,
|
|
@@ -1415,7 +1401,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1415
1401
|
} else {
|
|
1416
1402
|
console.error("Recieved an incorrect tombstone object");
|
|
1417
1403
|
console.error(validationError);
|
|
1418
|
-
this.
|
|
1404
|
+
this.labelInboxMessage(
|
|
1419
1405
|
inboxEndpoint,
|
|
1420
1406
|
messageId,
|
|
1421
1407
|
MESSAGE_LABEL_INVALID,
|
|
@@ -1424,7 +1410,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1424
1410
|
}
|
|
1425
1411
|
} else {
|
|
1426
1412
|
if (validationError === undefined) {
|
|
1427
|
-
this.
|
|
1413
|
+
this.labelInboxMessage(
|
|
1428
1414
|
inboxEndpoint,
|
|
1429
1415
|
messageId,
|
|
1430
1416
|
MESSAGE_LABEL_VALID,
|
|
@@ -1441,7 +1427,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1441
1427
|
} else if (validationError instanceof GraffitiErrorNotFound) {
|
|
1442
1428
|
// Item was deleted before we got a chance to
|
|
1443
1429
|
// validate it. Just label the message as trash.
|
|
1444
|
-
this.
|
|
1430
|
+
this.labelInboxMessage(
|
|
1445
1431
|
inboxEndpoint,
|
|
1446
1432
|
messageId,
|
|
1447
1433
|
MESSAGE_LABEL_TRASH,
|
|
@@ -1450,7 +1436,7 @@ export class GraffitiDecentralized implements Graffiti {
|
|
|
1450
1436
|
} else {
|
|
1451
1437
|
console.error("Recieved an incorrect object");
|
|
1452
1438
|
console.error(validationError);
|
|
1453
|
-
this.
|
|
1439
|
+
this.labelInboxMessage(
|
|
1454
1440
|
inboxEndpoint,
|
|
1455
1441
|
messageId,
|
|
1456
1442
|
MESSAGE_LABEL_INVALID,
|
|
@@ -37,7 +37,21 @@ export const template = `<template id="graffiti-login-welcome">
|
|
|
37
37
|
</button>
|
|
38
38
|
</form>
|
|
39
39
|
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
<div style="display: grid; gap: 0.5em">
|
|
41
|
+
<p><a href="#" id="graffiti-login-forgot">Forgot your handle?</a></p>
|
|
42
|
+
<p>Don't have a Graffiti handle? <a id="graffiti-login-new">Create one</a>.</p>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template id="graffiti-login-provider">
|
|
47
|
+
<h1>
|
|
48
|
+
<a target="_blank" href="https://graffiti.garden">Graffiti Log In</a>
|
|
49
|
+
</h1>
|
|
50
|
+
|
|
51
|
+
<form id="graffiti-login-provider-form">
|
|
52
|
+
<label for="graffiti-provider">Where did you create your Graffiti account?</label>
|
|
53
|
+
<input id="graffiti-provider" type="text" autocapitalize="none" spellcheck="false" required>
|
|
54
|
+
<button type="submit">Continue to provider</button>
|
|
55
|
+
</form>
|
|
56
|
+
<p><a href="#" id="graffiti-login-provider-back">← Back</a></p>
|
|
43
57
|
</template>`;
|
package/src/index.spec.ts
CHANGED
|
@@ -13,11 +13,12 @@ import { Authorization } from "./1-services/1-authorization";
|
|
|
13
13
|
import { StorageBuckets } from "./1-services/3-storage-buckets";
|
|
14
14
|
import { Inboxes } from "./1-services/4-inboxes";
|
|
15
15
|
import { Sessions } from "./3-protocol/1-sessions";
|
|
16
|
-
import { afterAll, beforeAll, describe } from "vitest";
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
17
|
+
import { decode as dagCborDecode, encode as dagCborEncode } from "@ipld/dag-cbor";
|
|
17
18
|
import { Handles } from "./3-protocol/2-handles";
|
|
18
19
|
import { didTests } from "./1-services/2-dids-tests";
|
|
19
20
|
import { storageBucketTests } from "./1-services/3-storage-buckets-tests";
|
|
20
|
-
import { inboxTests } from "./1-services/4-inboxes-tests";
|
|
21
|
+
import { inboxTests, inboxUnitTests } from "./1-services/4-inboxes-tests";
|
|
21
22
|
import { stringEncodingTests } from "./2-primitives/1-string-encoding-tests";
|
|
22
23
|
import { contentAddressesTests } from "./2-primitives/2-content-addresses-tests";
|
|
23
24
|
import { channelAttestationTests } from "./2-primitives/3-channel-attestations-tests";
|
|
@@ -106,6 +107,107 @@ describe("GraffitiDecentralized Tests", () => {
|
|
|
106
107
|
() => sessions[1],
|
|
107
108
|
);
|
|
108
109
|
|
|
110
|
+
test("OAuth codes require approval from the selected account", async () => {
|
|
111
|
+
const account = decentralizedTestUsers[0];
|
|
112
|
+
const redirectUri = "https://client.example/callback";
|
|
113
|
+
const endpoint = "https://localhost:5173/app/oauth/authorize";
|
|
114
|
+
const cookie = `account_${account.accountId}=${account.token}`;
|
|
115
|
+
const headers = {
|
|
116
|
+
Cookie: cookie,
|
|
117
|
+
"X-Graffiti-Account": String(account.accountId),
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Origin: "https://localhost:5173",
|
|
120
|
+
};
|
|
121
|
+
const request = (scope: string[], requestHeaders: HeadersInit = headers) =>
|
|
122
|
+
fetch(endpoint, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: requestHeaders,
|
|
125
|
+
body: JSON.stringify({ redirect_uri: redirectUri, state: "test-state", scope }),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const navigation = await fetch(
|
|
129
|
+
`${endpoint}?account=${account.accountId}&redirect_uri=${encodeURIComponent(redirectUri)}`,
|
|
130
|
+
{ headers: { Cookie: cookie } },
|
|
131
|
+
);
|
|
132
|
+
expect(navigation.status).toBe(404);
|
|
133
|
+
|
|
134
|
+
expect((await request([account.bucketEndpoint], { Cookie: cookie })).status).toBe(401);
|
|
135
|
+
expect((await request([account.bucketEndpoint], {
|
|
136
|
+
...headers,
|
|
137
|
+
Origin: "https://attacker.example",
|
|
138
|
+
})).status).toBe(403);
|
|
139
|
+
expect((await request([decentralizedTestUsers[1].bucketEndpoint])).status).toBe(403);
|
|
140
|
+
|
|
141
|
+
const approval = await request([account.bucketEndpoint]);
|
|
142
|
+
expect(approval.status).toBe(200);
|
|
143
|
+
const { redirectUri: callback } = await approval.json() as { redirectUri: string };
|
|
144
|
+
const code = new URL(callback).searchParams.get("code");
|
|
145
|
+
expect(code).toBeTruthy();
|
|
146
|
+
expect(new URL(callback).searchParams.get("state")).toBe("test-state");
|
|
147
|
+
|
|
148
|
+
const exchange = await fetch("https://localhost:5173/app/oauth/token", {
|
|
149
|
+
method: "POST",
|
|
150
|
+
body: new URLSearchParams({ code: code!, redirect_uri: redirectUri }),
|
|
151
|
+
});
|
|
152
|
+
expect(exchange.status).toBe(200);
|
|
153
|
+
expect((await exchange.json()).access_token).toBeTypeOf("string");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("deleting a bucket removes its values and rejects requests to its old URL", async () => {
|
|
157
|
+
const account = decentralizedTestUsers[0];
|
|
158
|
+
const smallKey = crypto.randomUUID();
|
|
159
|
+
const largeKey = crypto.randomUUID();
|
|
160
|
+
const largeValue = new Uint8Array(32 * 1024 + 1).fill(7);
|
|
161
|
+
const authorization = { Authorization: `Bearer ${account.token}` };
|
|
162
|
+
|
|
163
|
+
for (const [key, value] of [
|
|
164
|
+
[smallKey, new Uint8Array([1, 2, 3])],
|
|
165
|
+
[largeKey, largeValue],
|
|
166
|
+
] as const) {
|
|
167
|
+
const response = await fetch(`${account.bucketEndpoint}/value/${key}`, {
|
|
168
|
+
method: "PUT",
|
|
169
|
+
headers: authorization,
|
|
170
|
+
body: value,
|
|
171
|
+
});
|
|
172
|
+
expect(response.status).toBe(201);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const r2Key = `${account.bucketId}/${largeKey}`;
|
|
176
|
+
expect(await environment.storedR2ObjectExists(r2Key)).toBe(true);
|
|
177
|
+
|
|
178
|
+
const deletion = await fetch(
|
|
179
|
+
`https://localhost:5173/app/service-instances/bucket/service/${account.bucketId}`,
|
|
180
|
+
{
|
|
181
|
+
method: "DELETE",
|
|
182
|
+
headers: {
|
|
183
|
+
Cookie: `account_${account.accountId}=${account.token}`,
|
|
184
|
+
"X-Graffiti-Account": String(account.accountId),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
expect(deletion.status).toBe(200);
|
|
189
|
+
|
|
190
|
+
expect(await environment.storedR2ObjectExists(r2Key)).toBe(false);
|
|
191
|
+
for (const key of [smallKey, largeKey]) {
|
|
192
|
+
const response = await fetch(`${account.bucketEndpoint}/value/${key}`);
|
|
193
|
+
expect(response.status).toBe(404);
|
|
194
|
+
}
|
|
195
|
+
const batch = await fetch(`${account.bucketEndpoint}/values`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "Content-Type": "application/cbor" },
|
|
198
|
+
body: dagCborEncode({ keys: [smallKey, largeKey], maxValueBytes: largeValue.length }).slice(),
|
|
199
|
+
});
|
|
200
|
+
expect((dagCborDecode(await batch.arrayBuffer()) as { results: { status: number }[] }).results)
|
|
201
|
+
.toEqual([{ status: 404 }, { status: 404 }]);
|
|
202
|
+
|
|
203
|
+
const putAfterDeletion = await fetch(`${account.bucketEndpoint}/value/${smallKey}`, {
|
|
204
|
+
method: "PUT",
|
|
205
|
+
headers: authorization,
|
|
206
|
+
body: new Uint8Array([4]),
|
|
207
|
+
});
|
|
208
|
+
expect(putAfterDeletion.status).toBe(404);
|
|
209
|
+
});
|
|
210
|
+
|
|
109
211
|
// How to log in/out vvv
|
|
110
212
|
async function login(handle: string) {
|
|
111
213
|
const actor = await handleMethods.handleToActor(handle);
|
|
@@ -174,3 +276,5 @@ describe("GraffitiDecentralized Tests", () => {
|
|
|
174
276
|
});
|
|
175
277
|
}
|
|
176
278
|
});
|
|
279
|
+
|
|
280
|
+
inboxUnitTests();
|